import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs"; import path from "node:path"; import { expect, test } from "../helpers/fixtures"; import { EMPLOYEE, login, logout } from "../helpers/auth"; import { openInvoicePool, uploadLiveInvoiceAndWait, type LiveInvoiceOcrResult } from "../helpers/liveInvoiceOcr"; type BatchResult = LiveInvoiceOcrResult & { filePath: string; fileName: string; }; const rootDir = path.resolve(process.cwd(), ".."); const reportPath = path.join(rootDir, "artifacts/r70/live-ocr-batch-report.md"); function resolveInvoiceFiles() { const files = new Set(); const directFile = process.env.PLAYWRIGHT_OCR_LIVE_FILE?.trim(); const batchFiles = process.env.PLAYWRIGHT_OCR_LIVE_FILES?.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) ?? []; const batchDir = process.env.PLAYWRIGHT_OCR_LIVE_DIR?.trim() ?? ""; if (directFile) { files.add(path.resolve(directFile)); } for (const file of batchFiles) { files.add(path.resolve(file)); } if (batchDir && existsSync(batchDir) && statSync(batchDir).isDirectory()) { for (const entry of readdirSync(batchDir).sort((left, right) => left.localeCompare(right, "zh-CN"))) { const fullPath = path.join(batchDir, entry); if (!statSync(fullPath).isFile()) continue; if (!/\.(pdf|png|jpg|jpeg|webp)$/i.test(entry)) continue; files.add(path.resolve(fullPath)); } } return [...files].filter((filePath) => existsSync(filePath)); } function writeReport(results: BatchResult[]) { mkdirSync(path.dirname(reportPath), { recursive: true }); const lines = [ "# R70 批量 live OCR 辅助复核报告", "", `生成时间:${new Date().toISOString()}`, "", "## 结果", "", `- 总文件数:${results.length}`, `- 成功:${results.filter((item) => item.status === "available").length}`, `- 失败:${results.filter((item) => item.status === "ocr_failed").length}`, "", "## 明细", "", "| 文件 | 状态 | 耗时(ms) | 票据ID | 发票代码 | 发票号码 | 销售方 | 开票日期 | 备注 |", "| --- | --- | ---: | --- | --- | --- | --- | --- | --- |", ...results.map((item) => { const remark = item.status === "ocr_failed" ? (item.bannerText || item.detailText).replaceAll("|", "\\|") : "ok"; return `| ${item.fileName} | ${item.status} | ${item.durationMs} | ${item.invoiceId ?? "-"} | ${item.invoiceCode ?? "-"} | ${item.invoiceNumber ?? "-"} | ${item.sellerName ?? "-"} | ${item.invoiceDate ?? "-"} | ${remark} |`; }), "", "## 说明", "", "- 本报告用于真实腾讯 OCR 的本地辅助复核,不进入常规本地回归。", "- 如某张票失败,会继续处理后续文件,并在本报告统一汇总。", ]; writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf-8"); } test("手工 live 腾讯 OCR 批量辅助复核:真实票样逐张上传并输出报告", async ({ page }) => { test.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行"); test.skip(process.env.PLAYWRIGHT_OCR_LIVE !== "1", "仅在显式 live OCR 辅助复核模式下执行"); const invoiceFiles = resolveInvoiceFiles(); test.skip(!invoiceFiles.length, "需要提供 PLAYWRIGHT_OCR_LIVE_FILE / PLAYWRIGHT_OCR_LIVE_FILES / PLAYWRIGHT_OCR_LIVE_DIR"); const results: BatchResult[] = []; await login(page, EMPLOYEE); try { await openInvoicePool(page); for (const invoiceFilePath of invoiceFiles) { await openInvoicePool(page); const result = await uploadLiveInvoiceAndWait(page, invoiceFilePath); results.push({ ...result, filePath: invoiceFilePath, fileName: path.basename(invoiceFilePath), }); } } finally { writeReport(results); await logout(page, EMPLOYEE.name).catch(() => undefined); } const failures = results.filter((item) => item.status === "ocr_failed"); expect(failures, JSON.stringify(failures, null, 2)).toHaveLength(0); });