You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
5.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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} 个 Autocompleteinput 探针 ${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;
}