新增样例验收报告工具

This commit is contained in:
czj
2026-07-01 14:59:36 +08:00
parent 1781088640
commit b699c227bd
2 changed files with 226 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join, relative } from "node:path";
import type { InvalidArticleSample, RunSummary, SampleResult } from "./types";
interface AggregateInput {
startedAt: string;
finishedAt: string;
provider: string;
model: string;
baseURL: string;
reportDir: string;
samples: SampleResult[];
invalidSamples: InvalidArticleSample[];
}
export function writeSampleResult(reportDir: string, result: SampleResult) {
const sampleDir = join(reportDir, "samples", result.slug);
mkdirSync(sampleDir, { recursive: true });
writeFileSync(
join(sampleDir, "result.json"),
`${JSON.stringify(result, null, 2)}\n`,
);
}
export function readSampleResults(reportDir: string): SampleResult[] {
const samplesDir = join(reportDir, "samples");
try {
return readdirSync(samplesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => join(samplesDir, entry.name, "result.json"))
.map(
(filePath) => JSON.parse(readFileSync(filePath, "utf8")) as SampleResult,
)
.sort((left, right) => left.file.localeCompare(right.file));
} catch {
return [];
}
}
export function aggregateSummary(input: AggregateInput): RunSummary {
const passed = input.samples.filter((sample) => sample.status === "passed").length;
const failed = input.samples.filter((sample) => sample.status === "failed").length;
const skipped =
input.samples.filter((sample) => sample.status === "skipped").length +
input.invalidSamples.length;
return {
started_at: input.startedAt,
finished_at: input.finishedAt,
mode: "live",
provider: input.provider,
model: input.model,
base_url: input.baseURL,
report_dir: input.reportDir,
totals: { passed, failed, skipped },
samples: input.samples,
invalid_samples: input.invalidSamples,
};
}
export function writeSummaryFiles(reportDir: string, summary: RunSummary) {
mkdirSync(reportDir, { recursive: true });
writeFileSync(
join(reportDir, "summary.json"),
`${JSON.stringify(summary, null, 2)}\n`,
);
writeFileSync(join(reportDir, "summary.md"), renderSummaryMarkdown(summary));
}
export function redactSensitiveText(
text: string,
secrets: Array<string | undefined>,
) {
let redacted = text
.replace(/(x-api-key:\s*)([^\s]+)/gi, "$1[REDACTED]")
.replace(/(authorization:\s*bearer\s+)([^\s]+)/gi, "$1[REDACTED]")
.replace(
/((?:DEEPSEEK|OPENAI|API)_?[A-Z_]*KEY=)"?([^"\s]+)"?/g,
'$1"[REDACTED]"',
);
for (const secret of secrets) {
if (!secret) continue;
redacted = redacted.split(secret).join("[REDACTED]");
}
return redacted;
}
function renderSummaryMarkdown(summary: RunSummary) {
const lines = [
"# GEO 样例文章 E2E 测试报告",
"",
`- 模式:${summary.mode}`,
`- Provider${summary.provider}`,
`- Model${summary.model}`,
`- Base URL${summary.base_url}`,
`- 报告目录:${summary.report_dir}`,
`- 结果:通过 ${summary.totals.passed},失败 ${summary.totals.failed},跳过 ${summary.totals.skipped}`,
"",
"| 样例 | 状态 | QA | 耗时 | 失败分类 |",
"| --- | --- | --- | ---: | --- |",
...summary.samples
.map((sample) =>
[
sample.name,
sample.status,
sample.qa_status ?? "",
`${Math.round(sample.duration_ms / 1000)}s`,
sample.failure_category ?? "",
].join(" | "),
)
.map((row) => `| ${row} |`),
];
if (summary.invalid_samples.length > 0) {
lines.push("", "## 无效样例", "");
for (const sample of summary.invalid_samples) {
lines.push(`- ${sample.fileName}: ${sample.reason}`);
}
}
lines.push("");
return lines.join("\n");
}
export function relativeArtifactPath(reportDir: string, artifactPath: string) {
return relative(reportDir, artifactPath);
}