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