|
|
#!/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<string, unknown>,
|
|
|
): void {
|
|
|
fs.writeFileSync(
|
|
|
statusPath,
|
|
|
`${JSON.stringify({ updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`,
|
|
|
"utf8",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function printBanner(runId: string, paths: ReturnType<typeof createRunPaths>): 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<void> {
|
|
|
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);
|
|
|
});
|