import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { Page } from "playwright"; import { PLACE_DIAG_COLLECT_SNAPSHOT, PLACE_DIAG_SET_MARK, } from "@/workers/rpa/fix-place/place-diagnostic-init"; export type PlaceListenerFingerprint = { source: string; length: number; preview: string; hasFetch: boolean; hasAxelPlace: boolean; hasGetPlace: boolean; hasXMLHttpRequest: boolean; }; export type PlaceDiagSnapshot = { ok: boolean; reason?: string; mark?: string; topology?: { instanceCount: number; instances: Array<{ id: string; input: Record | null; listenerCount: number; listenerFingerprints: PlaceListenerFingerprint[]; getPlace: Record | null; }>; inputProbes: Array>; monkeyPatchInstances: number; capturedHandlers: number; }; listeners?: { total: number; anyHasFetch: boolean; anyHasAxelPlace: boolean; rows: Array<{ id: string; instanceId: string; fingerprint: PlaceListenerFingerprint }>; }; events?: Array>; handlerEvents?: Array>; fetchLog?: Array>; activeElement?: Record | null; }; export function isPlaceDiagEnabled(): boolean { return process.env.RPA_PLACE_DIAG === "true"; } export async function setPlaceDiagMark(page: Page, mark: string): Promise { if (!isPlaceDiagEnabled()) { return; } await page.evaluate( ({ fn, m }) => { const runner = eval(`(${fn})`) as (m: string) => unknown; return runner(m); }, { fn: PLACE_DIAG_SET_MARK, m: mark }, ); } export async function collectPlaceDiagSnapshot( page: Page, ): Promise { const raw = await page.evaluate((fn) => { const runner = eval(`(${fn})`) as () => unknown; return runner(); }, PLACE_DIAG_COLLECT_SNAPSHOT); return (raw ?? { ok: false, reason: "evaluate-undefined" }) as PlaceDiagSnapshot; } /** 按用户 P0 决策树输出中文结论 */ export function interpretPlaceDiagnostic( snapshot: PlaceDiagSnapshot | undefined | null, phase: string, ): string[] { const lines: string[] = [`[place-diag:${phase}]`]; if (!snapshot?.ok) { lines.push("P0 未注入或页面无 __rpaPlaceDiag"); return lines; } const topo = snapshot.topology; const listeners = snapshot.listeners; const handlerEvents = snapshot.handlerEvents ?? []; const fetchLog = snapshot.fetchLog ?? []; lines.push( `实例拓扑:${topo?.instanceCount ?? 0} 个 Autocomplete;input 探针 ${topo?.inputProbes?.length ?? 0};捕获 handler ${topo?.capturedHandlers ?? 0}`, ); lines.push( `监听器指纹:共 ${listeners?.total ?? 0};含 fetch=${listeners?.anyHasFetch ? "是" : "否"};含 axel/place=${listeners?.anyHasAxelPlace ? "是" : "否"}`, ); if (handlerEvents.length === 0) { lines.push("本阶段无 place_changed 触发(handler/trigger 均未记录)"); } else { for (const ev of handlerEvents) { const instId = String(ev.instanceId ?? "?"); const type = String(ev.type ?? ""); const gp = (ev.getPlaceBefore ?? ev.getPlaceAtTrigger) as Record | undefined; lines.push( `触发:${type} instance=${instId} place_id=${gp?.place_id ?? "空"} geometry=${gp?.hasGeometry ? "有" : "无"}`, ); } } if ((listeners?.total ?? 0) === 0) { lines.push( "判定:无任何 place_changed 监听器被 P0 捕获 → MotherShip 可能自管实例引用,需用 input[gm_*] 定位真实实例", ); const probes = topo?.inputProbes ?? []; const withGm = probes.filter((p) => (p as { hasGmInstance?: boolean }).hasGmInstance); if (withGm.length > 0) { lines.push(`input 上发现 ${withGm.length} 个 gm 实例(未走 addListener 包装)`); } } else if (!listeners?.anyHasFetch && !listeners?.anyHasAxelPlace) { lines.push( "判定:有监听器但指纹无 fetch/axel → commit 可能不在 listener 内发 HTTP,或由其他层发请求", ); } const lastHandler = handlerEvents .filter((e) => e.type === "place_changed_handler") .slice(-1)[0]; if (lastHandler) { const gp = lastHandler.getPlaceBefore as Record | undefined; if (!gp?.place_id) { lines.push("判定:监听器已触发但 getPlace() 无 place_id → 数据不完整,检查 mock/trigger 时机"); } else if (!gp?.hasGeometry) { lines.push("判定:place_id 有但无 geometry → getPlace 返回不完整 Place"); } } if (handlerEvents.length > 0 && fetchLog.length === 0) { lines.push( "判定:place_changed 有记录但 P2 无 place fetch → 可能被防抖/条件分支拦截,查 P2 stack 或 widget 状态机", ); } else if (fetchLog.length > 0) { lines.push(`P2 观测 place fetch ${fetchLog.length} 次(最近阶段)`); } return lines; } export async function logPlaceDiagnostic( page: Page, phase: string, ): Promise { if (!isPlaceDiagEnabled()) { return { ok: false, reason: "disabled" }; } const snapshot = await collectPlaceDiagSnapshot(page); for (const line of interpretPlaceDiagnostic(snapshot, phase)) { console.log(line); } return snapshot; } export function writePlaceDiagnosticArtifact( runId: string, phase: string, snapshot: PlaceDiagSnapshot, ): string { const dir = join(process.cwd(), ".rpa", "place-diagnostic"); mkdirSync(dir, { recursive: true }); const path = join(dir, `${runId}-${phase}.json`); writeFileSync(path, JSON.stringify({ phase, snapshot, at: new Date().toISOString() }, null, 2), "utf8"); return path; }