You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/scripts/record-human-commit-baselin...

272 lines
8.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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 与 checkpointsplace 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);
});