/** * P1:headed 解剖 Axel Search / 地址输入组件 * * 自动化采集 gm_*、Fiber、联想 DOM、P2 fetch 栈;可选 page.pause() 供人工点选对照。 * * PowerShell: * $env:RPA_HEADLESS="false" * $env:RPA_PLACE_DIAG="true" * $env:PROBE_ANATOMY_PAUSE="true" # 打开 Playwright Inspector 暂停 * npm run probe:axel-anatomy */ process.env.RPA_PLACE_DIAG = "true"; process.env.RPA_HEADLESS = "false"; process.env.RPA_KEEP_BROWSER_OPEN = "true"; import { randomUUID } from "node:crypto"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { Page, Request, Response } from "playwright"; import { loadDevEnv } from "@/lib/dev-env"; import { hostFetchMothershipCandidates } from "@/lib/frontend/api-client"; import { applyMothershipCandidate } from "@/lib/frontend/mothership-address"; import { SCAN_GLOBAL_AXEL_HOOKS, SCAN_INPUT_GM_AND_DOM, SCAN_REACT_FIBER_ADDRESS, SCAN_SUGGESTION_ROWS, } from "@/workers/rpa/fix-place/axel-anatomy-scripts"; import { P0_PLACE_DIAG_INIT, P2_FETCH_HIJACK_INIT, } from "@/workers/rpa/fix-place/place-diagnostic-init"; import { collectPlaceDiagSnapshot, interpretPlaceDiagnostic, } from "@/workers/rpa/fix-place/place-diagnostic-probe"; import { openAddressSide, visibleStreetInput } from "@/workers/rpa/address-side"; import { ensureAddressSuggestions } from "@/workers/rpa/address-suggestions"; import { RPAContext } from "@/workers/rpa/kernel/context"; import { stabilizeQuoteLandingPage, waitForQuoterWidgetHydrated, } from "@/workers/rpa/page-prep"; import { closeBrowser, openQuotePage } from "@/workers/rpa/session-manager"; import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter"; import { waitForStyledSuggestionRows } from "@/workers/rpa/mothership-styled-suggestion-click"; loadDevEnv(); process.env.RPA_MOCK_MODE = "false"; process.env.RPA_ADDRESS_MODE = "real"; const BASE = process.env.PROVE_BASE_URL ?? "http://localhost:3000"; const TOKEN = "demo-host-token"; const CUSTOMER_ID = "CUST_001"; const OUT_DIR = join(process.cwd(), ".rpa", "axel-anatomy"); const PICKUP = { street: "Washington State Convention Center Main Garage, Pike Street", city: "Seattle", state: "WA", } as const; type NetEvent = { phase: string; method: string; url: string; status?: number; headers: Record; at: number; }; function evalScan(page: Page, fn: string): Promise { return page.evaluate((body) => { const runner = eval(`(${body})`) as () => T; return runner(); }, fn); } function attachNetworkTap(page: Page, phase: string, log: NetEvent[]): void { const onReq = (req: Request): void => { const url = req.url(); if (!url.includes("axel/location/")) { return; } const headers = req.headers(); const axelHeaders: Record = {}; for (const [k, v] of Object.entries(headers)) { if (/x-axel|x-mothership|authorization|cookie/i.test(k)) { axelHeaders[k] = k.toLowerCase() === "cookie" ? "(redacted)" : v; } } log.push({ phase, method: req.method(), url: url.slice(0, 280), headers: axelHeaders, at: Date.now(), }); }; const onRes = (res: Response): void => { const url = res.url(); if (!url.includes("axel/location/place/")) { return; } const row = log.find((e) => e.url === url.slice(0, 280) && e.status === undefined); if (row) { row.status = res.status(); } else { log.push({ phase, method: res.request().method(), url: url.slice(0, 280), status: res.status(), headers: {}, at: Date.now(), }); } }; page.on("request", onReq); page.on("response", onRes); } async function capturePhase( page: Page, phase: string, ): Promise> { const [globals, inputs, fiber, suggestions, p0] = await Promise.all([ evalScan>(page, SCAN_GLOBAL_AXEL_HOOKS), evalScan>(page, SCAN_INPUT_GM_AND_DOM), evalScan>(page, SCAN_REACT_FIBER_ADDRESS), evalScan>(page, SCAN_SUGGESTION_ROWS), collectPlaceDiagSnapshot(page), ]); return { phase, at: new Date().toISOString(), globals, inputs, fiber, suggestions, p0, p0Interpretation: interpretPlaceDiagnostic(p0, phase), }; } function summarizeVerdict(phases: Record[]): string[] { const lines: string[] = ["=== 解剖结论(自动)==="]; const afterSearch = phases.find((p) => p.phase === "after-search"); const inputScan = (afterSearch?.inputs ?? {}) as { inputs?: Array<{ gmKeys?: string[]; pacTarget?: boolean }>; pacItemCount?: number; styledRowCount?: number; }; const gmInputs = (inputScan.inputs ?? []).filter((r) => (r.gmKeys?.length ?? 0) > 0); if (gmInputs.length > 0) { lines.push("→ 输入框存在 gm_*:底层可能仍用 Google PAC,但 P0 未 patch 到(或实例在 iframe/早于 patch)"); } else { lines.push("→ 输入框无 gm_*:倾向 Axel 自研 Search UI(非标准 PAC 输入)"); } if ((inputScan.pacItemCount ?? 0) > 0) { lines.push(`→ 可见 .pac-item × ${inputScan.pacItemCount}`); } if ((inputScan.styledRowCount ?? 0) > 0) { lines.push(`→ styled-components 联想行容器 × ${inputScan.styledRowCount}`); } const fiberHits = ((afterSearch?.fiber as { hits?: unknown[] })?.hits ?? []).length; lines.push(`→ React Fiber 地址相关节点 × ${fiberHits}`); return lines; } async function main(): Promise { const runId = randomUUID().slice(0, 8); mkdirSync(OUT_DIR, { recursive: true }); const netLog: NetEvent[] = []; const phases: Record[] = []; const candRes = await hostFetchMothershipCandidates( BASE, TOKEN, CUSTOMER_ID, { ...PICKUP, zip: "" }, { street: "Ocean Drive", city: "Miami Beach", state: "FL", zip: "" }, ); if (candRes.code !== 0 || !candRes.data?.pickup_candidates[0]) { throw new Error("候选 API 失败"); } const pickupCand = candRes.data.pickup_candidates[0]; const label = pickupCand.display_label; console.log(`\n=== Axel Search 解剖 run=${runId} ===`); console.log(`地址:${label}\n`); const opened = await openQuotePage(quotePageAdapter, false, undefined, { freshCargoContext: true, }); await opened.context.addInitScript({ content: P0_PLACE_DIAG_INIT }); await opened.context.addInitScript({ content: P2_FETCH_HIJACK_INIT }); await opened.page.reload({ waitUntil: "domcontentloaded" }); const ctx = RPAContext.fromSession(opened.page, opened.context); attachNetworkTap(ctx.page, "session", netLog); await stabilizeQuoteLandingPage(ctx); await waitForQuoterWidgetHydrated(ctx.page, 90_000); phases.push(await capturePhase(ctx.page, "widget-hydrated")); await openAddressSide(ctx, "pickup"); const street = await visibleStreetInput(ctx, "pickup"); if (!street) { throw new Error("提货输入框不可见"); } phases.push(await capturePhase(ctx.page, "pickup-input-focused")); await ensureAddressSuggestions(ctx.page, street, PICKUP, { mothershipLabel: label, preferConfirmedLabel: true, }); await waitForStyledSuggestionRows(ctx.page, [], PICKUP, 15_000); phases.push(await capturePhase(ctx.page, "after-search")); console.log("\n--- 人工步骤(headed)---"); console.log("1. 在浏览器中手动点选一条联想"); console.log("2. DevTools → Elements:检查 input 是否有 gm_*"); console.log("3. DevTools → Network:点选后是否 GET axel/location/place/{id}"); console.log("4. React DevTools:找 Search/Candidate/Place 组件与 onSelect"); console.log("5. 完成后在 Playwright Inspector 点 Resume\n"); if (process.env.PROBE_ANATOMY_PAUSE !== "false") { await ctx.page.pause(); } phases.push(await capturePhase(ctx.page, "after-manual-or-pause")); const report = { runId, label, placeId: pickupCand.option_id, phases, network: netLog, verdict: summarizeVerdict(phases), finishedAt: new Date().toISOString(), }; const reportPath = join(OUT_DIR, `${runId}-anatomy.json`); writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8"); console.log("\n" + report.verdict.join("\n")); console.log(`\n报告:${reportPath}`); console.log("浏览器保持打开(RPA_KEEP_BROWSER_OPEN=true),关闭窗口或 Ctrl+C 结束。"); if (process.env.PROBE_ANATOMY_CLOSE === "true") { await opened.page.close().catch(() => undefined); await opened.context.close().catch(() => undefined); await closeBrowser(); } } main().catch((error) => { console.error(error); process.exit(1); });