|
|
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<string, unknown> | null;
|
|
|
listenerCount: number;
|
|
|
listenerFingerprints: PlaceListenerFingerprint[];
|
|
|
getPlace: Record<string, unknown> | null;
|
|
|
}>;
|
|
|
inputProbes: Array<Record<string, unknown>>;
|
|
|
monkeyPatchInstances: number;
|
|
|
capturedHandlers: number;
|
|
|
};
|
|
|
listeners?: {
|
|
|
total: number;
|
|
|
anyHasFetch: boolean;
|
|
|
anyHasAxelPlace: boolean;
|
|
|
rows: Array<{ id: string; instanceId: string; fingerprint: PlaceListenerFingerprint }>;
|
|
|
};
|
|
|
events?: Array<Record<string, unknown>>;
|
|
|
handlerEvents?: Array<Record<string, unknown>>;
|
|
|
fetchLog?: Array<Record<string, unknown>>;
|
|
|
activeElement?: Record<string, unknown> | null;
|
|
|
};
|
|
|
|
|
|
export function isPlaceDiagEnabled(): boolean {
|
|
|
return process.env.RPA_PLACE_DIAG === "true";
|
|
|
}
|
|
|
|
|
|
export async function setPlaceDiagMark(page: Page, mark: string): Promise<void> {
|
|
|
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<PlaceDiagSnapshot> {
|
|
|
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<string, unknown> | 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<string, unknown> | 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<PlaceDiagSnapshot> {
|
|
|
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;
|
|
|
}
|