"""Standalone Playwright screenshot worker (fresh process, Windows-safe). Invoked by fidelity_loop.capture_real_app_screenshot — never import uvicorn here. """ from __future__ import annotations import asyncio import base64 import json import os import sys from pathlib import Path def _force_proactor() -> None: if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) async def _shot_async(payload: dict) -> tuple[bytes | None, str, str | None]: from playwright.async_api import async_playwright draft = payload.get("draft") or {} rows = payload.get("rows") or [] web = (os.getenv("WEB_BASE") or "http://127.0.0.1:5173").rstrip("/") goto_timeout = int(os.getenv("FIDELITY_GOTO_TIMEOUT_MS") or "45000") slug = str(((draft.get("meta") or {}).get("slug")) or "preview") preview_id = str(payload.get("preview_id") or "preview") inject_payload = { "ok": True, "id": preview_id, "blueprint": draft, "rows": rows, "resource": payload.get("resource") or "records", } inject_js = ( "try{localStorage.removeItem('ajz_session');}catch(e){}" "window.__AJZ_PREVIEW__ = " + json.dumps(inject_payload, ensure_ascii=False) + ";" ) target = f"{web}/#/preview/{preview_id}" def fmt(err: BaseException) -> str: msg = str(err).strip() or repr(err) return msg.replace("\n", " ")[:500] try: async with async_playwright() as p: browser = await p.chromium.launch(headless=True) try: page = await browser.new_page(viewport={"width": 1600, "height": 1100}) await page.add_init_script(inject_js) async def fulfill(route): await route.fulfill( status=200, content_type="application/json; charset=utf-8", body=json.dumps(inject_payload, ensure_ascii=False).encode("utf-8"), ) await page.route("**/api/v1/preview/**", fulfill) await page.route("**/ai/api/v1/preview/**", fulfill) await page.goto(target, wait_until="domcontentloaded", timeout=goto_timeout) await page.evaluate(inject_js) try: await page.wait_for_selector( ".sf-shell-bar, .sf-root, .gen-app-ops, .gen-app, .sf-line-wrap", timeout=45000, ) except Exception: try: snip = (await page.locator("body").inner_text(timeout=2000))[:240] except Exception: snip = page.url raise RuntimeError( f"preview shell missing. page={page.url} text={snip!r}" ) from None login_btns = page.locator("button").filter(has_text="登录") shell = page.locator(".sf-shell-bar, .sf-root, .gen-app") if await login_btns.count() and not await shell.count(): raise RuntimeError("preview fell back to login") await page.wait_for_timeout(2500) if await page.locator(".gen-app").count(): png = await page.locator(".gen-app").first.screenshot(type="png") else: png = await page.screenshot(type="png", full_page=True) finally: await browser.close() return ( png, f"已截取草稿预览(/#/preview/{preview_id},注入数据无需再拉 AI)参与对比", slug, ) except Exception as e: # noqa: BLE001 return None, f"真页面截图失败: preview={fmt(e)}(目标 {target})", slug def main() -> int: _force_proactor() inp = Path(os.environ["FIDELITY_SHOT_INPUT"]) outp = Path(os.environ["FIDELITY_SHOT_OUTPUT"]) payload = json.loads(inp.read_text(encoding="utf-8")) png, note, slug = asyncio.run(_shot_async(payload)) out_dir = os.getenv("FIDELITY_SHOT_DIR") or "" if png and out_dir: d = Path(out_dir) d.mkdir(parents=True, exist_ok=True) (d / f"preview_{payload.get('preview_id') or 'x'}.png").write_bytes(png) outp.write_text( json.dumps( { "png_b64": base64.b64encode(png).decode("ascii") if png else None, "note": note, "slug": slug, }, ensure_ascii=False, ), encoding="utf-8", ) return 0 if png else 2 if __name__ == "__main__": raise SystemExit(main())