#!/usr/bin/env tsx /** * 对比人工录制 vs 自动化:联想 DOM 定位诊断 * 输出:.rpa/suggestion-compare/report-{id}.json + 截图 */ import { randomUUID } from "node:crypto"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { Page, Locator } from "playwright"; import { loadDevEnv } from "@/lib/dev-env"; import { COLLECT_SUGGESTION_MOUSE_TARGETS, COLLECT_WIDGET_SUGGESTION_LABELS } from "@/workers/rpa/kernel/browser-scripts"; import { collectSuggestionMouseTargets } from "@/workers/rpa/address-suggestion-mouse"; import { collectAddressSuggestionLabels, isAddressSuggestionDropdownOpen, } from "@/workers/rpa/address-suggestions"; import { isAddressPresentInWidget, readWidgetCombinedText, } from "@/workers/rpa/address-commit"; 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 { openAddressSide, visibleStreetInput } from "@/workers/rpa/address-side"; loadDevEnv(); process.env.RPA_HEADLESS = process.env.RPA_HEADLESS ?? "false"; process.env.RPA_KEEP_BROWSER_OPEN = "false"; const PICKUP = { street: "1234 Warehouse Street", city: "Los Angeles", state: "CA", mothershipDisplayLabel: "1234 Warehouse Street, Los Angeles, California, USA", }; /** 人工录制用的 partial text(codegen 生成) */ const MANUAL_PROBES = { pickupZh: "1234 Warehouse StreetLos", pickupEn: "1234 Warehouse StreetLos", deliveryZh: "600 East 7th StreetPomona,", }; const DOM_PROBE_SCRIPT = `(streetNum) => { var skipRe = /use my location|pick up from|deliver to|where to|freight details|search by address/i; var out = []; document.querySelectorAll("div, li, span, [role='option'], .pac-item").forEach(function(el) { if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") return; var text = (el.innerText || "").replace(/\\s+/g, " ").trim(); if (!text || text.length < 8 || skipRe.test(text)) return; if (streetNum && text.indexOf(streetNum) < 0) return; var rect = el.getBoundingClientRect(); if (!rect.width || !rect.height || rect.width < 80 || rect.height < 16) return; out.push({ tag: el.tagName, className: (el.className || "").toString().slice(0, 60), role: el.getAttribute("role"), text: text.slice(0, 100), x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2), w: Math.round(rect.width), h: Math.round(rect.height), }); }); out.sort(function(a, b) { return a.y - b.y || a.w * a.h - b.w * b.h; }); return out.slice(0, 30); }`; async function probeGetByText(page: Page, probe: string) { const loc = page.getByText(probe, { exact: false }).first(); const visible = await loc.isVisible().catch(() => false); const box = visible ? await loc.boundingBox().catch(() => null) : null; const count = await page.getByText(probe, { exact: false }).count().catch(() => 0); return { probe, visible, count, box }; } async function snapshotPhase( page: Page, input: Locator, widgetSelector: string, outDir: string, phase: string, ) { const shot = join(outDir, `${phase}.png`); await page.screenshot({ path: shot, fullPage: false }).catch(() => undefined); const streetNum = PICKUP.street.match(/^\d+/)?.[0] ?? ""; const domHits = (await page.evaluate(DOM_PROBE_SCRIPT, streetNum)) as unknown[]; const widgetLabels = (await page.evaluate(COLLECT_WIDGET_SUGGESTION_LABELS).catch( () => [], )) as string[]; const mouseTargets = await collectSuggestionMouseTargets(page, input); const autoLabels = await collectAddressSuggestionLabels(page, { street: PICKUP.street, city: PICKUP.city, state: PICKUP.state, }); const dropdownOpen = await isAddressSuggestionDropdownOpen(page); const visualPresent = await isAddressPresentInWidget(page, widgetSelector, PICKUP); const committedVisually = await import("@/workers/rpa/address-suggestion-mouse").then( (m) => m.isAddressCommittedVisually(page, widgetSelector, PICKUP, input), ); const widgetText = await readWidgetCombinedText(page, widgetSelector); const inputValue = await input.inputValue().catch(() => ""); const manualProbes = await Promise.all([ probeGetByText(page, MANUAL_PROBES.pickupZh), probeGetByText(page, MANUAL_PROBES.pickupEn), ]); return { phase, screenshot: shot, inputValue: inputValue.slice(0, 120), dropdownOpen, visualPresent, committedVisually, widgetTextPreview: widgetText.slice(0, 200), collectWidgetLabels: widgetLabels, collectAutoLabels: autoLabels, mouseTargets, domHits, manualProbes, listboxCount: await page.locator('[role="listbox"]').count(), pacItemCount: await page.locator(".pac-item").count(), pacVisible: await page.locator(".pac-container").first().isVisible().catch(() => false), }; } async function main(): Promise { const runId = randomUUID().slice(0, 8); const outDir = join(process.cwd(), ".rpa", "suggestion-compare", runId); mkdirSync(outDir, { recursive: true }); console.log(`[suggestion-compare] run=${runId} out=${outDir}`); const opened = await openQuotePage(quotePageAdapter, false, undefined, { freshCargoContext: true, }); const ctx = RPAContext.fromSession(opened.page, opened.context); const page = ctx.page; const widgetSelector = ctx.selectors.resolve("RPA_SELECTOR_QUOTE_WIDGET"); await stabilizeQuoteLandingPage(ctx); await waitForQuoterWidgetHydrated(page, 90_000); const phases: unknown[] = []; await openAddressSide(ctx, "pickup", widgetSelector); const input = await visibleStreetInput(ctx, "pickup"); if (!input) { throw new Error("提货输入框未找到"); } phases.push( await snapshotPhase(page, input, widgetSelector, outDir, "01-after-open-pickup"), ); const query = `${PICKUP.street}, ${PICKUP.city}, California`; await input.click({ force: true }); await input.fill(query); await page.waitForTimeout(800); phases.push( await snapshotPhase(page, input, widgetSelector, outDir, "02-after-fill-800ms"), ); await page.waitForTimeout(1200); phases.push( await snapshotPhase(page, input, widgetSelector, outDir, "03-after-fill-2s"), ); // 尝试人工录制同款 getByText 点击 const manualProbe = MANUAL_PROBES.pickupEn; const manualLoc = page.getByText(manualProbe, { exact: false }).first(); let manualClickResult: Record = { attempted: false }; if (await manualLoc.isVisible().catch(() => false)) { manualClickResult = { attempted: true, probe: manualProbe }; await manualLoc.click({ force: true, timeout: 3_000 }).catch((e) => { manualClickResult.error = String(e); }); await page.waitForTimeout(600); phases.push( await snapshotPhase(page, input, widgetSelector, outDir, "04-after-manual-getByText-click"), ); manualClickResult.afterClick = phases[phases.length - 1]; } else { manualClickResult = { attempted: false, probe: manualProbe, reason: "getByText 不可见", }; } const report = { runId, finishedAt: new Date().toISOString(), manualRecording: { file: ".rpa/recorded-mothership.ts", pickupClick: "page.getByText('1234 Warehouse StreetLos').click()", uiLocale: "zh-CN(用户系统浏览器)", note: "RPA 使用 en-US,placeholder=Search by address", }, automationEnv: { locale: process.env.RPA_BROWSER_LOCALE ?? "en-US", headless: process.env.RPA_HEADLESS, }, phases, manualClickResult, diagnosis: [] as string[], }; const p2 = phases[2] as { visualPresent?: boolean; dropdownOpen?: boolean; mouseTargets?: unknown[]; manualProbes?: { visible: boolean }[]; collectWidgetLabels?: string[] }; if (p2.visualPresent && p2.dropdownOpen) { report.diagnosis.push( "误判:isAddressPresentInWidget=true 但 dropdown 仍打开 → 输入框文字被当成已确认", ); } if ((p2.mouseTargets?.length ?? 0) === 0 && (p2.collectWidgetLabels?.length ?? 0) === 0) { report.diagnosis.push( "定位失败:COLLECT_WIDGET_SUGGESTION_LABELS 与 mouseTargets 均为空;listbox/PAC 不存在,styled-components 阈值或时机问题", ); } if (p2.manualProbes?.some((p) => p.visible)) { report.diagnosis.push( "人工录制 getByText 探针可见 → 应优先用 partial text 点击,而非 listbox/PAC 选择器", ); } else { report.diagnosis.push( "人工录制 getByText 探针也不可见 → 联想尚未渲染或 DOM 结构与录制时不一致", ); } const reportPath = join(outDir, "report.json"); writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); console.log("\n════════ 联想定位对比 ════════"); for (const line of report.diagnosis) { console.log(` • ${line}`); } console.log(`报告: ${reportPath}`); await closeBrowser(); } main().catch(async (err) => { console.error("[suggestion-compare] 失败:", err); await closeBrowser().catch(() => undefined); process.exitCode = 1; });