Files
ai_site/ai-service/shot_worker.py
2026-07-31 10:19:22 +08:00

126 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())