#!/usr/bin/env tsx /** * 真人 vs RPA 网络差分(同一地址对,不新增 fix-place 策略) * * 默认对齐录证 5519e0ea:293 Pike St Seattle → 826 Ocean Dr Miami Beach * * 用法: * npm run compare:human-rpa-network * $env:HUMAN_RUN_ID="5519e0ea"; $env:RPA_HEADLESS="false"; npm run compare:human-rpa-network */ import { randomUUID } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { loadDevEnv } from "@/lib/dev-env"; import { shouldKeepBrowserOpen } from "@/lib/rpa/env"; import { hostFetchMothershipCandidates } from "@/lib/frontend/api-client"; import { applyMothershipCandidate } from "@/lib/frontend/mothership-address"; import { MotherShipAddressAdapter } from "@/workers/rpa/address-adapter/mothership-address-adapter"; import { openAddressSide } from "@/workers/rpa/address-side"; 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 type { QuoteRequest } from "@/modules/providers/quote-provider"; import { diffHumanRpaNetwork, loadNetworkNdjson, } from "./human-commit-baseline/network-diff"; import { NetworkLogger } from "./human-commit-baseline/network-logger"; process.env.RPA_MOCK_MODE = "false"; process.env.RPA_ADDRESS_MODE = "real"; loadDevEnv(); const HUMAN_RUN_ID = process.env.HUMAN_RUN_ID ?? "5519e0ea"; const BASE = process.env.PROVE_BASE_URL ?? "http://localhost:3000"; const TOKEN = "demo-host-token"; const CUSTOMER_ID = "CUST_001"; /** 与真人录证 5519e0ea 一致的用户输入(候选 API 会解析为具体 display_label) */ const BASELINE_PAIR = { label: "5519e0ea 基准:Seattle Pike St → Miami Beach Ocean Dr", pickup: { street: "293 Pike St", city: "Seattle", state: "WA" }, delivery: { street: "826 Ocean Dr", city: "Miami Beach", state: "FL" }, }; function draftAddress(d: { street: string; city: string; state: string }) { const street = d.street.trim(); const city = d.city.trim(); const state = d.state.trim(); return { street, city, state, zip: "", place_id: `draft_${street}_${city}_${state}`.replace(/\s+/g, "_").slice(0, 64), formatted_address: `${street}, ${city}, ${state}`, selected_from_suggestions: true, }; } async function selectSide( ctx: RPAContext, side: "pickup" | "delivery", address: QuoteRequest["pickup"], logger: NetworkLogger, pickupAnchor?: QuoteRequest["pickup"], ): Promise<{ ok: boolean; error?: string; commitState?: string }> { const widgetSelector = ctx.selectors.resolve("RPA_SELECTOR_QUOTE_WIDGET"); logger.setCheckpoint(side === "pickup" ? "rpa_pickup" : "rpa_delivery"); const street = await openAddressSide(ctx, side); if (!street) { return { ok: false, error: `${side} 输入框不可见` }; } const adapter = new MotherShipAddressAdapter(); try { await adapter.selectAddress({ ctx, side, address, street, widgetSelector, pickupAnchor: side === "delivery" ? pickupAnchor : undefined, }); await adapter.waitForCommit(); } catch (e) { return { ok: false, error: e instanceof Error ? e.message : String(e), commitState: adapter.getCommitState(), }; } const diag = adapter.getDiagnostics(); return { ok: diag.placeRequestObserved && adapter.getCommitState() === "COMMITTED", commitState: adapter.getCommitState(), error: diag.placeRequestObserved ? undefined : "placeRequestObserved=false", }; } async function main(): Promise { const humanNetworkPath = join( process.cwd(), ".rpa", "human-commit-baseline", HUMAN_RUN_ID, "network.ndjson", ); if (!existsSync(humanNetworkPath)) { throw new Error(`找不到真人 network: ${humanNetworkPath}`); } const humanEvents = loadNetworkNdjson(humanNetworkPath); const rpaRunId = randomUUID().slice(0, 8); const outDir = join( process.cwd(), ".rpa", "human-commit-baseline", HUMAN_RUN_ID, `rpa-diff-${rpaRunId}`, ); mkdirSync(outDir, { recursive: true }); const rpaNetworkPath = join(outDir, "rpa-network.ndjson"); console.log(`[diff] 真人基准: ${HUMAN_RUN_ID}`); console.log(`[diff] RPA 录网: ${BASELINE_PAIR.label}`); console.log(`[diff] 产出: ${outDir}`); const userPickup = draftAddress(BASELINE_PAIR.pickup); const userDelivery = draftAddress(BASELINE_PAIR.delivery); const cand = await hostFetchMothershipCandidates( BASE, TOKEN, CUSTOMER_ID, userPickup, userDelivery, ); if (cand.code !== 0 || !cand.data?.pickup_candidates[0] || !cand.data.delivery_candidates[0]) { throw new Error(cand.message ?? "候选 API 失败"); } const pc = cand.data.pickup_candidates[0]; const dc = cand.data.delivery_candidates[0]; const pickupApplied = applyMothershipCandidate(userPickup, pc); const deliveryApplied = applyMothershipCandidate(userDelivery, dc); const pickup: QuoteRequest["pickup"] = { street: pickupApplied.street, city: pickupApplied.city, state: pickupApplied.state, zip: pickupApplied.zip ?? "", placeId: pc.option_id, formattedAddress: pickupApplied.formatted_address, selectedFromSuggestions: true, mothershipOptionId: pc.option_id, mothershipDisplayLabel: pc.display_label, selectedFromMothership: true, }; const delivery: QuoteRequest["delivery"] = { street: deliveryApplied.street, city: deliveryApplied.city, state: deliveryApplied.state, zip: deliveryApplied.zip ?? "", placeId: dc.option_id, formattedAddress: deliveryApplied.formatted_address, selectedFromSuggestions: true, mothershipOptionId: dc.option_id, mothershipDisplayLabel: dc.display_label, selectedFromMothership: true, }; console.log(`[diff] 确认提货: ${pc.display_label}`); console.log(`[diff] 确认派送: ${dc.display_label}`); const logger = new NetworkLogger(rpaNetworkPath); const opened = await openQuotePage(quotePageAdapter, false, undefined, { freshCargoContext: true, }); logger.setCheckpoint("rpa_page_loaded"); logger.attach(opened.page); const ctx = RPAContext.fromSession(opened.page, opened.context); console.log("[diff] 页面已打开,正在处理 Cookie 并滚动到报价区…"); await stabilizeQuoteLandingPage(ctx); console.log( "[diff] 等待 MotherShip 报价 widget 加载(含地图脚本,界面可能数十秒无输入,属正常)…", ); const hydration = await waitForQuoterWidgetHydrated(ctx.page, 90_000); if (hydration.hydrated) { console.log( `[diff] widget 已就绪(可编辑地址框 ${hydration.editableSearchInputs} 个)`, ); } else { console.warn( `[diff] widget 未完全就绪,继续尝试:${hydration.widgetTextPreview.slice(0, 80)}`, ); } console.log("[diff] 开始填写提货地址…"); const rpaResults: Record = {}; const pickupResult = await selectSide(ctx, "pickup", pickup, logger); rpaResults.pickup = pickupResult; console.log("[diff] RPA pickup:", pickupResult); let deliveryResult: Awaited> | null = null; if (pickupResult.ok) { console.log("[diff] 提货完成,开始填写派送地址…"); deliveryResult = await selectSide(ctx, "delivery", delivery, logger, pickup); rpaResults.delivery = deliveryResult; console.log("[diff] RPA delivery:", deliveryResult); } else { rpaResults.delivery = { skipped: true, reason: "pickup failed" }; } logger.flush(); const dwellMs = Number(process.env.RPA_DWELL_MS ?? 0); if (dwellMs > 0) { console.log(`[diff] 完成后停留 ${dwellMs}ms 便于观察…`); await opened.page.waitForTimeout(dwellMs).catch(() => undefined); } if (!shouldKeepBrowserOpen()) { await opened.page.close().catch(() => undefined); await opened.context.close().catch(() => undefined); await closeBrowser(); } else { console.log( "[diff] 浏览器保持打开(RPA_HEADLESS=false 默认行为;关闭窗口或 Ctrl+C 结束)", ); } const rpaEvents = loadNetworkNdjson(rpaNetworkPath); const diff = diffHumanRpaNetwork(humanEvents, rpaEvents, HUMAN_RUN_ID, rpaRunId); const humanPlaceIds = readFileSync( join(process.cwd(), ".rpa", "human-commit-baseline", HUMAN_RUN_ID, "commit-moments.json"), "utf8", ); const report = { finishedAt: new Date().toISOString(), humanRunId: HUMAN_RUN_ID, rpaRunId, baselinePair: BASELINE_PAIR, confirmedPickup: pc.display_label, confirmedDelivery: dc.display_label, humanCommitMoments: JSON.parse(humanPlaceIds), rpaSelectResults: rpaResults, diff, artifacts: { humanNetwork: humanNetworkPath, rpaNetwork: rpaNetworkPath, report: join(outDir, "diff-report.json"), }, }; writeFileSync( join(outDir, "diff-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8", ); console.log("\n════════ 差分摘要 ════════"); console.log(`verdict: ${diff.verdict}`); console.log( `place GET 200 — 真人: ${diff.human.placeGet200} | RPA: ${diff.rpa.placeGet200}`, ); console.log( `search→place 完整链 — 真人: ${diff.human.chains.filter((c) => c.complete).length}/${diff.human.chains.length} | RPA: ${diff.rpa.chains.filter((c) => c.complete).length}/${diff.rpa.chains.length}`, ); if (diff.gaps.length) { console.log("\n差异:"); for (const g of diff.gaps) { console.log(` - ${g}`); } } if (diff.recommendations.length) { console.log("\n建议:"); for (const r of diff.recommendations) { console.log(` → ${r}`); } } console.log(`\n完整报告: ${join(outDir, "diff-report.json")}`); const exitCode = diff.verdict === "MATCH" ? 0 : 1; if (!shouldKeepBrowserOpen()) { process.exit(exitCode); } console.log( "[diff] 填址已完成。按 Ctrl+C 结束会话(headed 模式默认保持浏览器窗口)", ); await new Promise((resolve) => { const done = () => resolve(); process.once("SIGINT", done); process.once("SIGTERM", done); }); await opened.page.close().catch(() => undefined); await opened.context.close().catch(() => undefined); await closeBrowser(); process.exit(exitCode); } main().catch((e) => { console.error(e); process.exit(1); });