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/benchmark-priority1-canonic...

230 lines
7.5 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
/**
* Priority1 全矩阵 canonical 路径批量询价24 条)
*
* npm run benchmark:priority1-canonical-matrix
* $env:PRIORITY1_MATRIX_FILTER="F04"; npm run benchmark:priority1-canonical-matrix
* $env:PRIORITY1_MATRIX_LIMIT="3"; npm run benchmark:priority1-canonical-matrix
*/
import { randomUUID } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
formatPriority1InputSummary,
PRIORITY1_CANONICAL_PATHS,
} from "@/workers/rpa/priority1/demo-input";
import { validatePriority1FormConstraints } from "@/workers/rpa/priority1/form-constraints";
import {
formatPriority1QuoteOutcomeMessage,
formatPriority1QuotesTable,
type Priority1QuoteOutcome,
} from "@/workers/rpa/priority1/quote-extract";
import { runPriority1VisualChain } from "./visual-priority1-full-chain";
function resolveMatrixBatchStatus(r: {
ok: boolean;
quoteCount: number;
quoteOutcome?: Priority1QuoteOutcome;
allObservedOk: boolean;
}): string {
if (r.quoteCount > 0) return "quote_ok";
if (r.quoteOutcome === "manual_followup") return "manual_followup";
if (r.allObservedOk) return "form_only";
return "fail";
}
function formatMatrixCaseLine(r: {
caseId: string;
ok: boolean;
quoteCount: number;
quoteOutcome?: Priority1QuoteOutcome;
message?: string;
}): string {
if (r.quoteCount > 0) return `${r.caseId} quotes=${r.quoteCount}`;
if (r.quoteOutcome === "manual_followup") {
return `${r.caseId} manual_followup — ${r.message ?? formatPriority1QuoteOutcomeMessage("manual_followup")}`;
}
return `${r.caseId} quotes=0`;
}
process.env.RPA_HEADLESS = process.env.RPA_HEADLESS ?? "true";
process.env.RPA_KEEP_BROWSER_OPEN = "false";
process.env.RPA_SLOW_MO_MS = process.env.RPA_SLOW_MO_MS ?? "0";
process.env.RPA_DWELL_MS = process.env.RPA_DWELL_MS ?? "0";
process.env.RPA_QUOTE_WAIT_MS = process.env.RPA_QUOTE_WAIT_MS ?? "120000";
const FILTER = process.env.PRIORITY1_MATRIX_FILTER?.trim();
const LIMIT = Number(process.env.PRIORITY1_MATRIX_LIMIT ?? 0);
const START = Number(process.env.PRIORITY1_MATRIX_START ?? 0);
function filterPaths() {
let paths = [...PRIORITY1_CANONICAL_PATHS];
if (FILTER) {
const re = new RegExp(FILTER, "i");
paths = paths.filter((p) => re.test(p.id) || re.test(p.title));
}
if (START > 0) paths = paths.slice(START);
if (LIMIT > 0) paths = paths.slice(0, LIMIT);
return paths;
}
async function main(): Promise<void> {
const runId = randomUUID().slice(0, 8);
const batchDir = join(process.cwd(), ".rpa", "visual-priority1-matrix", runId);
mkdirSync(batchDir, { recursive: true });
const paths = filterPaths();
if (paths.length === 0) {
console.error("[matrix] 无匹配路径");
process.exit(1);
}
console.log(
`\n[matrix] Priority1 canonical 全矩阵 batch=${runId} cases=${paths.length}/${PRIORITY1_CANONICAL_PATHS.length}`,
);
writeFileSync(
join(batchDir, "cases-input.json"),
`${JSON.stringify({ runId, filter: FILTER ?? null, paths: paths.map((p) => ({ id: p.id, title: p.title, verified: p.verified, input: p.input })) }, null, 2)}\n`,
"utf8",
);
const results: Awaited<ReturnType<typeof runPriority1VisualChain>>[] = [];
for (let i = 0; i < paths.length; i += 1) {
const path = paths[i]!;
console.log(`\n[matrix] ▶ ${i + 1}/${paths.length} ${path.id}${path.title}`);
console.log(formatPriority1InputSummary(path.input));
const constraintErrors = validatePriority1FormConstraints(path.input).filter(
(v) => v.severity === "error",
);
if (constraintErrors.length) {
console.error(`[matrix] ✗ ${path.id} 违反填写规则:${constraintErrors.map((e) => e.message).join("")}`);
results.push({
runId,
caseId: path.id,
outDir: join(batchDir, path.id),
demo: path.input,
finishedAt: new Date().toISOString(),
durationMs: 0,
finalUrl: "",
observations: [],
quotes: [],
quoteCount: 0,
quoteOutcome: "no_result",
allObservedOk: false,
ok: false,
error: constraintErrors.map((e) => e.message).join(""),
});
continue;
}
const result = await runPriority1VisualChain({
demo: path.input,
caseId: path.id,
runId,
outDir: join(batchDir, path.id),
keepBrowserOpen: false,
});
results.push(result);
console.log(`\n${formatMatrixCaseLine({ caseId: path.id, ...result })}`);
if (result.quotes.length) console.log(formatPriority1QuotesTable(result.quotes));
if (result.quoteOutcome === "manual_followup" && result.message) {
console.log(result.message);
}
if (result.error) console.log(`错误: ${result.error}`);
}
const instantPass = results.filter((r) => r.quoteCount > 0).length;
const manualPass = results.filter((r) => r.quoteOutcome === "manual_followup").length;
const pass = results.filter((r) => r.ok).length;
const formOk = results.filter((r) => r.allObservedOk).length;
const summary = {
runId,
total: paths.length,
canonicalTotal: PRIORITY1_CANONICAL_PATHS.length,
pass,
instantQuote: instantPass,
manualFollowup: manualPass,
fail: paths.length - pass,
formObservationsOk: formOk,
finishedAt: new Date().toISOString(),
cases: results.map((r, idx) => ({
caseId: r.caseId,
title: paths[idx]?.title,
persona: paths[idx]?.persona,
priorVerified: paths[idx]?.verified,
batchStatus: resolveMatrixBatchStatus(r),
ok: r.ok,
quoteCount: r.quoteCount,
quoteOutcome: r.quoteOutcome ?? null,
message: r.message ?? null,
allObservedOk: r.allObservedOk,
error: r.error,
outDir: r.outDir,
quotes: r.quotes,
})),
};
const summaryPath = join(batchDir, "batch-summary.json");
writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
const md: string[] = [
`# Priority1 Canonical 全矩阵报告`,
``,
`- 批次: \`${runId}\``,
`- 执行: ${paths.length} / ${PRIORITY1_CANONICAL_PATHS.length}`,
`- 即时报价: **${instantPass}/${paths.length}**`,
`- 人工跟进: **${manualPass}/${paths.length}**`,
`- 成功合计: **${pass}/${paths.length}**`,
`- 表单观测全绿: ${formOk}/${paths.length}`,
``,
`| ID | 路径 | 原状态 | 批量结果 | 报价数 |`,
`|----|------|--------|----------|--------|`,
];
for (let i = 0; i < results.length; i += 1) {
const r = results[i]!;
const p = paths[i]!;
const status =
r.quoteCount > 0
? "✓ 即时报价"
: r.quoteOutcome === "manual_followup"
? "✓ 人工跟进"
: r.allObservedOk
? "△ 仅填表"
: "✗ 失败";
md.push(
`| ${r.caseId} | ${p.title} | ${p.verified === "recorded" ? "已录制" : "待录制"} | ${status} | ${r.quoteCount} |`,
);
}
md.push("", "## 未成功详情", "");
for (let i = 0; i < results.length; i += 1) {
const r = results[i]!;
if (r.ok) continue;
md.push(`### ${r.caseId}`);
md.push(r.error ?? r.message ?? "(无报价)");
md.push("");
}
const mdPath = join(batchDir, "batch-summary.md");
writeFileSync(mdPath, `${md.join("\n")}\n`, "utf8");
console.log(
`\n[matrix] 完成 成功 ${pass}/${paths.length}(即时 ${instantPass} + 人工跟进 ${manualPass}`,
);
console.log(`[matrix] JSON: ${summaryPath}`);
console.log(`[matrix] MD: ${mdPath}`);
process.exit(pass === paths.length ? 0 : 1);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});