#!/usr/bin/env tsx /** * 必做任务 1:真人完整链路录证(找 commit 真正发生瞬间) * * 产出(.rpa/human-commit-baseline/{runId}/): * baseline.har — 全量 HAR * trace.zip — Playwright Trace(含 CDP / 截图 / DOM) * video/*.webm — 屏幕录像 * console.ndjson — 浏览器 Console * network.ndjson — axel/location + axel/quote + Google Places 网络 * checkpoints.json — 人工检查点时间轴 * commit-moments.json — 自动识别的 GET place/{id} 200 时刻 * summary.json — 录证摘要 * * 用法: * npm run record:human-commit-baseline * $env:HUMAN_RECORD_FILE_TRIGGER="1"; npm run record:human-commit-baseline * * 检查点触发文件(非 TTY 推荐): * .rpa/human-commit-baseline/{runId}/triggers/after-pickup * .rpa/human-commit-baseline/{runId}/triggers/after-delivery * .rpa/human-commit-baseline/{runId}/triggers/after-quote * .rpa/human-commit-baseline/{runId}/triggers/finish */ import "dotenv/config"; import fs from "node:fs"; import { randomUUID } from "node:crypto"; import { chromium } from "playwright"; import { getPrimaryMothershipQuoteUrl } from "@/lib/rpa/env"; import { createSharedReadline, waitForHumanCheckpoint, } from "./human-commit-baseline/checkpoint"; import { ConsoleLogger } from "./human-commit-baseline/console-logger"; import { NetworkLogger } from "./human-commit-baseline/network-logger"; import { createRunPaths } from "./human-commit-baseline/paths"; const PROMPTS = { afterPickup: "提货地址:键入联想 → 点选 → 确认 widget 已锁定(chip/摘要可见)。完成后触发检查点。", afterDelivery: "派送地址:键入联想 → 点选 → 确认双地址均在 widget。完成后触发检查点。", afterQuote: "货物已填、已点 Get Quote、报价档位已显示。完成后触发检查点。", finish: "确认录证结束,将写入 HAR / Trace / 摘要。", }; function writeStatus( statusPath: string, payload: Record, ): void { fs.writeFileSync( statusPath, `${JSON.stringify({ updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`, "utf8", ); } function printBanner(runId: string, paths: ReturnType): void { const tty = process.stdin.isTTY ? "是" : "否"; const mode = process.env.HUMAN_RECORD_FILE_TRIGGER === "1" || !process.stdin.isTTY ? "文件触发" : "Enter 键"; console.log(` ╔══════════════════════════════════════════════════════════════╗ ║ 真人完整链路录证 — 定位 place commit 瞬间 ║ ╚══════════════════════════════════════════════════════════════╝ runId: ${runId} 产出目录: ${paths.root} stdin TTY: ${tty} → 检查点: ${mode} 冻结说明(录证期间禁止新增): - fix-place/* - place-commit-strategy / selection-trace - 新回退策略 / 新点击方式 操作顺序: 1. 浏览器打开 MotherShip 报价页 2. 人工完成提货地址点选 → 检查点「提货 commit」 3. 人工完成派送地址点选 → 检查点「派送 commit」 4. 填货 + 提交 + 看到报价 → 检查点「报价完成」 5. 结束录证 自动标记: 每次 GET .../axel/location/place/{id} 200 会打印 ★ 并写入 commit-moments.json 查看 Trace: npx playwright show-trace "${paths.trace}" `); } async function main(): Promise { const runId = randomUUID().slice(0, 8); const paths = createRunPaths(runId); fs.mkdirSync(paths.triggerDir, { recursive: true }); fs.mkdirSync(paths.videoDir, { recursive: true }); const entryUrl = getPrimaryMothershipQuoteUrl(); writeStatus(paths.status, { phase: "started", runId, entryUrl }); printBanner(runId, paths); const channel = process.env.RPA_BROWSER_CHANNEL?.trim() || "chrome"; const browser = await chromium.launch({ headless: false, channel: channel as "chrome", slowMo: Number(process.env.RPA_SLOW_MO_MS ?? 0) || undefined, }); const context = await browser.newContext({ locale: process.env.RPA_BROWSER_LOCALE?.trim() || undefined, viewport: { width: 1400, height: 900 }, recordHar: { path: paths.har, mode: "full", content: "embed" }, recordVideo: { dir: paths.videoDir, size: { width: 1400, height: 900 } }, }); await context.tracing.start({ screenshots: true, snapshots: true, sources: true, title: `human-commit-baseline-${runId}`, }); const page = await context.newPage(); page.setDefaultTimeout(120_000); const networkLogger = new NetworkLogger(paths.networkLog); const consoleLogger = new ConsoleLogger(paths.consoleLog); networkLogger.attach(page); consoleLogger.attach(page); const checkpoints: Array<{ id: string; label: string; ts: number; iso: string; pageUrl: string; placeCommitsSoFar: number; }> = []; const rl = process.stdin.isTTY && process.env.HUMAN_RECORD_FILE_TRIGGER !== "1" ? createSharedReadline() : null; function markCheckpoint(id: string, label: string): void { networkLogger.setCheckpoint(id); consoleLogger.setPhase(id); checkpoints.push({ id, label, ts: Date.now(), iso: new Date().toISOString(), pageUrl: page.url(), placeCommitsSoFar: networkLogger.getPlaceCommits().length, }); writeStatus(paths.status, { phase: id, runId, checkpoints: checkpoints.length, placeCommits: networkLogger.getPlaceCommits().length, }); } console.log(`[录证] 导航 ${entryUrl}`); await page.goto(entryUrl, { waitUntil: "domcontentloaded", timeout: 90_000 }); markCheckpoint("page_loaded", "页面已加载"); await waitForHumanCheckpoint({ label: "检查点 1 — 提货 commit 后", message: PROMPTS.afterPickup, triggerPath: paths.triggerAfterPickup, rl, }); markCheckpoint("after_pickup", "提货地址已点选锁定"); await waitForHumanCheckpoint({ label: "检查点 2 — 派送 commit 后", message: PROMPTS.afterDelivery, triggerPath: paths.triggerAfterDelivery, rl, }); markCheckpoint("after_delivery", "派送地址已点选锁定"); await waitForHumanCheckpoint({ label: "检查点 3 — 报价已显示", message: PROMPTS.afterQuote, triggerPath: paths.triggerAfterQuote, rl, }); markCheckpoint("after_quote", "报价档位已显示"); await waitForHumanCheckpoint({ label: "检查点 4 — 结束录证", message: PROMPTS.finish, triggerPath: paths.triggerFinish, rl, }); markCheckpoint("finish", "录证结束"); if (rl) { rl.close(); } networkLogger.flush(); const placeCommits = networkLogger.getPlaceCommits(); await context.tracing.stop({ path: paths.trace }); await page.close(); await context.close(); await browser.close(); fs.writeFileSync( paths.checkpoints, `${JSON.stringify({ runId, checkpoints }, null, 2)}\n`, "utf8", ); fs.writeFileSync( paths.commitMoments, `${JSON.stringify({ runId, placeCommits }, null, 2)}\n`, "utf8", ); const summary = { runId, finishedAt: new Date().toISOString(), entryUrl, artifacts: { har: fs.existsSync(paths.har) ? paths.har : null, trace: fs.existsSync(paths.trace) ? paths.trace : null, videoDir: paths.videoDir, consoleLog: paths.consoleLog, networkLog: paths.networkLog, checkpoints: paths.checkpoints, commitMoments: paths.commitMoments, }, placeCommitCount: placeCommits.length, placeCommits, checkpoints, analysisHints: [ "对比 commit-moments 与 checkpoints:place 200 应落在 after_pickup / after_delivery 检查点之前或紧邻", "trace.zip: npx playwright show-trace 查看点击瞬间 DOM + Network", "baseline.har: 在 DevTools HAR 视图中过滤 axel/location/place", "若人工操作有 place 200 而 RPA 无 → 问题在自动化事件序列,非 API 层", ], }; fs.writeFileSync(paths.summary, `${JSON.stringify(summary, null, 2)}\n`, "utf8"); writeStatus(paths.status, { phase: "completed", runId, summary: paths.summary }); console.log(` [录证] 完成 runId=${runId} HAR: ${paths.har} Trace: ${paths.trace} Video: ${paths.videoDir} Network: ${paths.networkLog} Console: ${paths.consoleLog} Commits: ${placeCommits.length} 次 GET place 200 摘要: ${paths.summary} 下一步: npx playwright show-trace "${paths.trace}" `); } main().catch((error) => { console.error( "[录证] 失败:", error instanceof Error ? error.message : error, ); process.exit(1); });