OINV1 加固真实发票OCR脱敏报告

这个提交包含在:
2026-05-23 21:03:23 +08:00
父节点 35db715aa1
当前提交 92b004592a
共修改 5 个文件,包含 129 行新增和 53 行删除
+46 -4
查看文件
@@ -17,6 +17,20 @@ export type LiveInvoiceOcrResult = {
invoiceDate: string | null;
};
export type SafeLiveInvoiceOcrEvidence = {
alias: string;
status: LiveInvoiceOcrStatus;
durationMs: number;
reasonCategory: "available" | "duplicate_or_same_invoice" | "already_used" | "ocr_failed" | "timeout_or_pending" | "unknown";
fieldHits: {
invoiceId: boolean;
invoiceCodeOrNumber: boolean;
sellerName: boolean;
totalAmount: boolean;
invoiceDate: boolean;
};
};
type UploadInvoiceResponse = {
data?: {
invoicePoolId?: string | null;
@@ -35,6 +49,35 @@ function extractField(detailText: string, label: string) {
return match?.[1]?.trim() || null;
}
function categorizeText(text: string) {
if (/重复|相同发票|duplicate/i.test(text)) return "duplicate_or_same_invoice";
if (/已用于报销|已使用|used/i.test(text)) return "already_used";
if (/超时|timeout|pending/i.test(text)) return "timeout_or_pending";
if (/失败|识别失败|ocr_failed|failed/i.test(text)) return "ocr_failed";
return "unknown";
}
export function categorizeLiveInvoiceOcrResult(result: LiveInvoiceOcrResult): SafeLiveInvoiceOcrEvidence["reasonCategory"] {
if (result.status === "available") return "available";
return categorizeText(`${result.bannerText}\n${result.detailText}`);
}
export function createSafeLiveInvoiceOcrEvidence(result: LiveInvoiceOcrResult, alias: string): SafeLiveInvoiceOcrEvidence {
return {
alias,
status: result.status,
durationMs: result.durationMs,
reasonCategory: categorizeLiveInvoiceOcrResult(result),
fieldHits: {
invoiceId: Boolean(result.invoiceId),
invoiceCodeOrNumber: Boolean(result.invoiceCode || result.invoiceNumber),
sellerName: Boolean(result.sellerName),
totalAmount: Boolean(result.totalAmount && result.totalAmount !== "-"),
invoiceDate: Boolean(result.invoiceDate),
},
};
}
export function resolveLiveOcrUser(fallback: E2EUser): E2EUser {
const identifier =
process.env.PLAYWRIGHT_OCR_LIVE_IDENTIFIER?.trim() ||
@@ -124,14 +167,13 @@ export async function uploadLiveInvoiceAndWait(page: Page, invoiceFilePath: stri
} catch (error) {
const detailText = await detailPanel.innerText().catch(() => "");
const bannerText = await page.getByTestId("invoice-ocr-status-banner").innerText().catch(() => "");
const invoiceId = uploadedInvoiceId ?? extractField(detailText, "票据ID:");
const reasonCategory = categorizeText(`${bannerText}\n${detailText}`);
throw new Error(
[
"live 腾讯 OCR 等待终态超时",
`invoiceId=${invoiceId ?? "unknown"}`,
`invoiceIdPresent=${Boolean(uploadedInvoiceId ?? extractField(detailText, "票据ID:"))}`,
`timeoutMs=${timeout}`,
`detail=${detailText || "<empty>"}`,
`banner=${bannerText || "<empty>"}`,
`reasonCategory=${reasonCategory}`,
`cause=${error instanceof Error ? error.message : String(error)}`,
].join("\n"),
);
@@ -3,15 +3,24 @@ import path from "node:path";
import { expect, test } from "../helpers/fixtures";
import { EMPLOYEE, login } from "../helpers/auth";
import { openInvoicePool, resolveLiveOcrUser, uploadLiveInvoiceAndWait, type LiveInvoiceOcrResult } from "../helpers/liveInvoiceOcr";
import {
createSafeLiveInvoiceOcrEvidence,
openInvoicePool,
resolveLiveOcrUser,
uploadLiveInvoiceAndWait,
type SafeLiveInvoiceOcrEvidence,
} from "../helpers/liveInvoiceOcr";
type BatchResult = LiveInvoiceOcrResult & {
type BatchSample = {
filePath: string;
fileName: string;
alias: string;
};
const rootDir = path.resolve(process.cwd(), "..");
const reportPath = path.join(rootDir, "artifacts/r70/live-ocr-batch-report.md");
const reportDir = process.env.PLAYWRIGHT_OCR_BATCH_REPORT_DIR?.trim()
|| path.join(rootDir, "artifacts/qa-flow", `live-ocr-batch-${new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}`);
const markdownReportPath = path.join(reportDir, "summary.md");
const jsonReportPath = path.join(reportDir, "summary.json");
function resolveInvoiceFiles() {
const files = new Set<string>();
@@ -37,39 +46,75 @@ function resolveInvoiceFiles() {
}
}
return [...files].filter((filePath) => existsSync(filePath));
return [...files]
.filter((filePath) => existsSync(filePath))
.sort((left, right) => left.localeCompare(right, "zh-CN"))
.map((filePath, index): BatchSample => ({
filePath,
alias: `SAMPLE-PDF-${String(index + 1).padStart(3, "0")}`,
}));
}
function writeReport(results: BatchResult[]) {
mkdirSync(path.dirname(reportPath), { recursive: true });
function writeReport(results: SafeLiveInvoiceOcrEvidence[]) {
mkdirSync(reportDir, { recursive: true });
const counts = {
total: results.length,
available: results.filter((item) => item.status === "available").length,
ocrFailed: results.filter((item) => item.status === "ocr_failed").length,
duplicateOrSameInvoice: results.filter((item) => item.reasonCategory === "duplicate_or_same_invoice").length,
alreadyUsed: results.filter((item) => item.reasonCategory === "already_used").length,
timeoutOrPending: results.filter((item) => item.reasonCategory === "timeout_or_pending").length,
unknown: results.filter((item) => item.reasonCategory === "unknown").length,
};
const fieldHitCounts = {
invoiceId: results.filter((item) => item.fieldHits.invoiceId).length,
invoiceCodeOrNumber: results.filter((item) => item.fieldHits.invoiceCodeOrNumber).length,
sellerName: results.filter((item) => item.fieldHits.sellerName).length,
totalAmount: results.filter((item) => item.fieldHits.totalAmount).length,
invoiceDate: results.filter((item) => item.fieldHits.invoiceDate).length,
};
const summary = {
mode: "live-ocr-batch-redacted",
checkedAt: new Date().toISOString(),
counts,
fieldHitCounts,
privacy: "redacted aliases and boolean field hits only; no invoice number, seller, amount, full filename, full path, raw OCR payload, invoice image, token, or password",
results,
};
const lines = [
"# R70 批量 live OCR 辅助复核报告",
"# 批量 live OCR 脱敏辅助复核报告",
"",
`生成时间:${new Date().toISOString()}`,
"",
"## 结果",
"",
`- 总文件数:${results.length}`,
`- 成功:${results.filter((item) => item.status === "available").length}`,
`- 失败:${results.filter((item) => item.status === "ocr_failed").length}`,
`- 总文件数:${counts.total}`,
`- 可用:${counts.available}`,
`- 识别失败:${counts.ocrFailed}`,
`- 重复或相同发票:${counts.duplicateOrSameInvoice}`,
`- 已用于报销/已使用:${counts.alreadyUsed}`,
`- 超时或仍在处理中:${counts.timeoutOrPending}`,
`- 未分类:${counts.unknown}`,
"",
"## 明细",
"",
"| 文件 | 状态 | 耗时(ms) | 票据ID | 发票代码 | 发票号码 | 销售方 | 开票日期 | 备注 |",
"| 样本 | 状态 | 耗时(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} |`;
}),
...results.map((item) => (
`| ${item.alias} | ${item.status} | ${item.durationMs} | ${item.reasonCategory} | ${item.fieldHits.invoiceId ? "Y" : "N"} | ${item.fieldHits.invoiceCodeOrNumber ? "Y" : "N"} | ${item.fieldHits.sellerName ? "Y" : "N"} | ${item.fieldHits.totalAmount ? "Y" : "N"} | ${item.fieldHits.invoiceDate ? "Y" : "N"} |`
)),
"",
"## 说明",
"",
"- 本报告用于真实腾讯 OCR 的本地辅助复核,不进入常规本地回归。",
"- 本报告用于真实腾讯 OCR 的受控辅助复核,不进入常规本地回归。",
"- 本报告只记录样本别名、状态、耗时、字段命中布尔值和原因分类。",
"- 本报告不得记录发票号码、销售方、金额、完整文件名、完整路径、原始 OCR payload 或票面原图。",
"- 如某张票失败,会继续处理后续文件,并在本报告统一汇总。",
];
writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf-8");
writeFileSync(markdownReportPath, `${lines.join("\n")}\n`, "utf-8");
writeFileSync(jsonReportPath, `${JSON.stringify(summary, null, 2)}\n`, "utf-8");
}
test("手工 live 腾讯 OCR 批量辅助复核:真实票样逐张上传并输出报告", async ({ page }) => {
@@ -79,27 +124,27 @@ test("手工 live 腾讯 OCR 批量辅助复核:真实票样逐张上传并输
const invoiceFiles = resolveInvoiceFiles();
test.skip(!invoiceFiles.length, "需要提供 PDF 格式的 PLAYWRIGHT_OCR_LIVE_FILE / PLAYWRIGHT_OCR_LIVE_FILES / PLAYWRIGHT_OCR_LIVE_DIR");
const results: BatchResult[] = [];
const results: SafeLiveInvoiceOcrEvidence[] = [];
const liveOcrUser = resolveLiveOcrUser(EMPLOYEE);
await login(page, liveOcrUser);
try {
await openInvoicePool(page);
for (const invoiceFilePath of invoiceFiles) {
for (const sample of invoiceFiles) {
await openInvoicePool(page);
const result = await uploadLiveInvoiceAndWait(page, invoiceFilePath);
results.push({
...result,
filePath: invoiceFilePath,
fileName: path.basename(invoiceFilePath),
});
const result = await uploadLiveInvoiceAndWait(page, sample.filePath);
results.push(createSafeLiveInvoiceOcrEvidence(result, sample.alias));
}
} finally {
writeReport(results);
await page.context().clearCookies().catch(() => undefined);
}
const failures = results.filter((item) => item.status === "ocr_failed");
expect(failures, JSON.stringify(failures, null, 2)).toHaveLength(0);
const allowDuplicate = process.env.PLAYWRIGHT_OCR_ALLOW_DUPLICATE === "1";
const blockingFailures = results.filter((item) => {
if (item.status !== "ocr_failed") return false;
return !(allowDuplicate && ["duplicate_or_same_invoice", "already_used"].includes(item.reasonCategory));
});
expect(blockingFailures, JSON.stringify(blockingFailures, null, 2)).toHaveLength(0);
});
@@ -3,7 +3,7 @@ import path from "node:path";
import { test, expect } from "../helpers/fixtures";
import { EMPLOYEE, login } from "../helpers/auth";
import { openInvoicePool, resolveLiveOcrUser, uploadLiveInvoiceAndWait } from "../helpers/liveInvoiceOcr";
import { createSafeLiveInvoiceOcrEvidence, openInvoicePool, resolveLiveOcrUser, uploadLiveInvoiceAndWait } from "../helpers/liveInvoiceOcr";
test("手工 live 腾讯 OCR 辅助复核:真实 VAT 发票上传后进入可处理状态", async ({ page }, testInfo) => {
test.skip(process.env.PLAYWRIGHT_MANUAL !== "1", "仅在显式手工 Playwright 模式下执行");
@@ -18,17 +18,7 @@ test("手工 live 腾讯 OCR 辅助复核:真实 VAT 发票上传后进入可
await openInvoicePool(page);
const result = await uploadLiveInvoiceAndWait(page, invoiceFilePath);
const evidence = {
status: result.status,
durationMs: result.durationMs,
invoiceId: result.invoiceId,
invoiceCode: result.invoiceCode,
invoiceNumber: result.invoiceNumber,
sellerName: result.sellerName,
totalAmount: result.totalAmount,
invoiceDate: result.invoiceDate,
bannerText: result.bannerText,
};
const evidence = createSafeLiveInvoiceOcrEvidence(result, process.env.PLAYWRIGHT_OCR_LIVE_ALIAS?.trim() || "SAMPLE-PDF-001");
console.log(`[live-ocr] ${JSON.stringify(evidence)}`);
await testInfo.attach("live-ocr-result.json", {
body: JSON.stringify(evidence, null, 2),
@@ -37,18 +27,17 @@ test("手工 live 腾讯 OCR 辅助复核:真实 VAT 发票上传后进入可
if (result.status === "ocr_failed") {
const allowDuplicate = process.env.PLAYWRIGHT_OCR_ALLOW_DUPLICATE === "1";
const resultText = `${result.bannerText}\n${result.detailText}`;
const explainableTerminal = /重复|相同发票|duplicate|已用于报销|已使用|used/i.test(resultText);
const explainableTerminal = ["duplicate_or_same_invoice", "already_used"].includes(evidence.reasonCategory);
if (!allowDuplicate || !explainableTerminal) {
throw new Error(`live 腾讯 OCR 失败:${result.bannerText || result.detailText}`);
throw new Error(`live 腾讯 OCR 失败:reasonCategory=${evidence.reasonCategory}`);
}
await page.context().clearCookies().catch(() => undefined);
return;
}
expect(result.invoiceCode ?? result.invoiceNumber, JSON.stringify(result)).toBeTruthy();
expect(result.totalAmount, JSON.stringify(result)).toBeTruthy();
expect(result.totalAmount, JSON.stringify(result)).not.toBe("-");
expect(result.invoiceCode ?? result.invoiceNumber, JSON.stringify(evidence)).toBeTruthy();
expect(result.totalAmount, JSON.stringify(evidence)).toBeTruthy();
expect(result.totalAmount, JSON.stringify(evidence)).not.toBe("-");
await page.context().clearCookies().catch(() => undefined);
});
@@ -2,7 +2,7 @@
"OINV1-0","建立真实发票自动化验证合同","基于项目 artifacts 中已有真实 PDF 发票样本,建立 OA 发票/OCR/报销自动化验证与脱敏加固合同;本合同不直接上传票面原图到报告、不提交真实报销、不执行付款。","Issue CSV 校验通过;明确自动化范围、隐私边界、可写边界和后续人工门禁;不把真实发票号、销售方、金额、完整文件名或本地路径写入 Issue/文档/报告。","受影响流程:真实发票样本 -> OCR/provider 验证 -> 公网上传型 smoke -> 报销闭环准备;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv。","python3|manual","DONE","DONE","DONE","issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv","[]","回滚/清理:仅删除本合同新增行或后续 QA-only 脚本;不删除真实发票样本。真实发票文件名和票面内容不得进入提交信息或公开报告。合同和 process gate 已通过。"
"OINV1-1","真实发票样本脱敏盘点","盘点 artifacts 和私有 env 中配置的真实 PDF 样本,只记录数量、类型、大小区间和可用性,不记录完整文件名、销售方、发票号码、金额、完整本地路径或票面原图。","生成或记录脱敏样本清单;至少确认 PDF 样本数量、文件存在性、文件类型和是否可供 OCR;报告隐私扫描通过。","受影响流程:样本准备 -> 自动化输入 -> 隐私边界;验证步骤:rg --files | rg -i 'pdf$|发票|invoice';node scripts/qa/check-ocr-provider-samples.mjs --json 使用别名或 env 样本,不输出原始票面值。","rg|node","DONE","DONE","DONE","artifacts;scripts/qa/check-ocr-provider-samples.mjs;artifacts/qa-flow/oinv1-provider-baseline-20260523-202701/summary.json","[""OINV1-0""]","已用 /tmp SAMPLE-PDF-xxx 别名盘点根部 PDF 样本 2 个;summary 只含 alias、mime、size、transport/status,不含完整文件名、本地路径或票面值。"
"OINV1-2","OCR provider 样本只读验证","使用现有 check-ocr-provider-samples.mjs 直接调用 OCR provider 或在缺凭据时给出 SKIP,验证 PDF native transport、必要字段命中情况和图片 OCR 是否继续冻结。","脚本输出 status/decision/counts/fieldHits,不输出发票号码、销售方、金额或原始 OCR payload;缺腾讯凭据时记录 SKIP,不误判业务失败;如 provider 返回 HOLD,进入修复分析。","受影响流程:真实 PDF -> OCR provider -> 归一化字段 -> 决策;验证步骤:PLAYWRIGHT_OCR_LIVE_FILES=<样本列表> node scripts/qa/check-ocr-provider-samples.mjs --json。","node","DONE","DONE","DONE","scripts/qa/check-ocr-provider-samples.mjs;server/src/services/ocr/tencent.js;artifacts/qa-flow/oinv1-provider-baseline-20260523-202701/summary.json","[""OINV1-1""]","provider 基线 status=SKIP,decision=FREEZE_BY_DEFAULT,counts total=2/pdf=2/skip=2,原因 missing_tencent_credentials;这是本地缺凭据,不是产品失败。不启用 OCR_IMAGE_OCR_EVALUATION;图片 OCR 继续冻结。回滚/清理:provider 只读验证不产生 OA 数据;只需删除本地脱敏报告或环境变量即可。"
"OINV1-3","公网上传型 OCR smoke 脱敏加固方案","审查 live-invoice-ocr 和 batch spec 的输出,避免真实发票成功识别时把发票号、销售方、金额、完整文件名或本地路径写入日志/附件;准备 QA-only 脱敏报告格式。","形成实现方案并经用户确认后再改代码;脱敏报告只保留 alias、状态、耗时、字段命中布尔值、重复/已使用等可解释原因;不输出原始票面值。","受影响流程:公网 OA 上传 -> OCR 等待终态 -> 报告留存;验证步骤:代码审查 client/tests/e2e/manual/live-invoice-ocr*.spec.ts 和 helpers/liveInvoiceOcr.ts;改造后跑 npm/Playwright。","manual|playwright","TODO","TODO","TODO","client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts","[""OINV1-2""]","编码前需要确认;公网上传会创建或复用票据记录,只允许 smoke 员工,且不提交报销、不付款。"
"OINV1-3","公网上传型 OCR smoke 脱敏加固方案","审查 live-invoice-ocr 和 batch spec 的输出,避免真实发票成功识别时把发票号、销售方、金额、完整文件名或本地路径写入日志/附件;准备 QA-only 脱敏报告格式。","形成实现方案并经用户确认后再改代码;脱敏报告只保留 alias、状态、耗时、字段命中布尔值、重复/已使用等可解释原因;不输出原始票面值。","受影响流程:公网 OA 上传 -> OCR 等待终态 -> 报告留存;验证步骤:代码审查 client/tests/e2e/manual/live-invoice-ocr*.spec.ts 和 helpers/liveInvoiceOcr.ts;改造后跑 npm/Playwright。","manual|playwright","DONE","DONE","DONE","client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;artifacts/qa-flow/live-smoke-readiness-20260523-205755/summary.json;artifacts/qa-flow/oinv1-redacted-batch-smoke-20260523-205755/summary.json","[""OINV1-2""]","完成:用户确认 Q1-Q6 全部 A 后实施脱敏加固。单票 live OCR 输出 alias/status/duration/reasonCategory/fieldHits;批量报告写 artifacts/qa-flow/*/summary.md 和 summary.json,仅含 alias、状态、耗时、字段命中布尔值和原因分类;最小公网单票与批量 smoke 均通过。回滚:恢复 3 个 Playwright 测试文件改动;不影响生产 API/schema。"
"OINV1-4","公网真实发票 OCR smoke","在脱敏加固后,用 OA-only/live smoke 员工账号逐张上传项目已有 PDF 样票,验证重复票、可用票、识别失败票的用户可理解终态;不提交报销,不创建付款。","生成脱敏 summary;每张样票有 alias、终态、耗时和字段命中;重复/已用于报销可解释时为 PASS_WITH_FINDINGS 或 PASS_DUPLICATE_EXPLAINED;不可解释 OCR 失败记录 finding。","受影响流程:员工登录 -> 票据上传 -> OCR 终态 -> 票据池查看;验证步骤:PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_ALLOW_DUPLICATE=1 npx playwright test tests/e2e/manual/live-invoice-ocr*.spec.ts --workers=1 --reporter=list。","playwright","TODO","TODO","TODO","client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;artifacts/qa-flow","[""OINV1-3""]","写入边界:只允许票据上传 smoke 数据;不提交报销、不审批、不付款、不导出含敏感内容资料;报告不得含完整账号、发票号、销售方、金额、路径或票面原图。"
"OINV1-5","报销闭环测试数据准备","在 OCR smoke 可解释通过后,评估是否用 TEST/SMOKE 标记创建一条报销测试闭环,覆盖绑定票据、提交、审批、付款批次、回单和 AP 导出;真实业务闭环仍需人工最终验收。","若执行自动闭环,必须带 TEST/SMOKE marker,报告脱敏;若不执行,输出人工步骤和数据准备要求;不得执行真实银行付款。","受影响流程:票据池 -> 报销 -> 审批 -> 付款/AP;验证步骤:复用 reimbursement-payment-batch-flow 或 OAOPS1 真实业务门禁,必要时新建专项合同。","playwright|manual","TODO","TODO","TODO","client/tests/e2e/reimbursement-payment-batch-flow.spec.ts;scripts/qa/run-oa-real-use-daily.sh","[""OINV1-4""]","付款仍由线下银行完成;自动化只能创建/验证 OA 测试数据,不能替代真实业务发票合规判断。"
"OINV1-6","本轮门禁和回填","完成合同校验、现有脚本审查、provider 样本基线和隐私检查;未获代码确认前,不改 live OCR spec;未获公网写入确认前,不运行上传型批量测试。","validate_issues_csv、process contract、git diff --check 通过;最终回复明确哪些自动完成、哪些需要用户批准或人工验收。","受影响流程:合同 -> 自动基线 -> 后续实施判断;验证步骤:validate_issues_csv.py <OINV1 csv>;node scripts/qa/check-process-contract.mjs --issue <OINV1 csv> --profile general --json;git diff --check。","python3|node|git","TODO","TODO","TODO","issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv","[""OINV1-0"",""OINV1-1"",""OINV1-2""]","本轮可先做只读/本地 provider 基线;公网上传和脚本改造按用户确认继续。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 OINV1-0 建立真实发票自动化验证合同 基于项目 artifacts 中已有真实 PDF 发票样本,建立 OA 发票/OCR/报销自动化验证与脱敏加固合同;本合同不直接上传票面原图到报告、不提交真实报销、不执行付款。 Issue CSV 校验通过;明确自动化范围、隐私边界、可写边界和后续人工门禁;不把真实发票号、销售方、金额、完整文件名或本地路径写入 Issue/文档/报告。 受影响流程:真实发票样本 -> OCR/provider 验证 -> 公网上传型 smoke -> 报销闭环准备;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv。 python3|manual DONE DONE DONE issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv [] 回滚/清理:仅删除本合同新增行或后续 QA-only 脚本;不删除真实发票样本。真实发票文件名和票面内容不得进入提交信息或公开报告。合同和 process gate 已通过。
3 OINV1-1 真实发票样本脱敏盘点 盘点 artifacts 和私有 env 中配置的真实 PDF 样本,只记录数量、类型、大小区间和可用性,不记录完整文件名、销售方、发票号码、金额、完整本地路径或票面原图。 生成或记录脱敏样本清单;至少确认 PDF 样本数量、文件存在性、文件类型和是否可供 OCR;报告隐私扫描通过。 受影响流程:样本准备 -> 自动化输入 -> 隐私边界;验证步骤:rg --files | rg -i 'pdf$|发票|invoice';node scripts/qa/check-ocr-provider-samples.mjs --json 使用别名或 env 样本,不输出原始票面值。 rg|node DONE DONE DONE artifacts;scripts/qa/check-ocr-provider-samples.mjs;artifacts/qa-flow/oinv1-provider-baseline-20260523-202701/summary.json ["OINV1-0"] 已用 /tmp SAMPLE-PDF-xxx 别名盘点根部 PDF 样本 2 个;summary 只含 alias、mime、size、transport/status,不含完整文件名、本地路径或票面值。
4 OINV1-2 OCR provider 样本只读验证 使用现有 check-ocr-provider-samples.mjs 直接调用 OCR provider 或在缺凭据时给出 SKIP,验证 PDF native transport、必要字段命中情况和图片 OCR 是否继续冻结。 脚本输出 status/decision/counts/fieldHits,不输出发票号码、销售方、金额或原始 OCR payload;缺腾讯凭据时记录 SKIP,不误判业务失败;如 provider 返回 HOLD,进入修复分析。 受影响流程:真实 PDF -> OCR provider -> 归一化字段 -> 决策;验证步骤:PLAYWRIGHT_OCR_LIVE_FILES=<样本列表> node scripts/qa/check-ocr-provider-samples.mjs --json。 node DONE DONE DONE scripts/qa/check-ocr-provider-samples.mjs;server/src/services/ocr/tencent.js;artifacts/qa-flow/oinv1-provider-baseline-20260523-202701/summary.json ["OINV1-1"] provider 基线 status=SKIP,decision=FREEZE_BY_DEFAULT,counts total=2/pdf=2/skip=2,原因 missing_tencent_credentials;这是本地缺凭据,不是产品失败。不启用 OCR_IMAGE_OCR_EVALUATION;图片 OCR 继续冻结。回滚/清理:provider 只读验证不产生 OA 数据;只需删除本地脱敏报告或环境变量即可。
5 OINV1-3 公网上传型 OCR smoke 脱敏加固方案 审查 live-invoice-ocr 和 batch spec 的输出,避免真实发票成功识别时把发票号、销售方、金额、完整文件名或本地路径写入日志/附件;准备 QA-only 脱敏报告格式。 形成实现方案并经用户确认后再改代码;脱敏报告只保留 alias、状态、耗时、字段命中布尔值、重复/已使用等可解释原因;不输出原始票面值。 受影响流程:公网 OA 上传 -> OCR 等待终态 -> 报告留存;验证步骤:代码审查 client/tests/e2e/manual/live-invoice-ocr*.spec.ts 和 helpers/liveInvoiceOcr.ts;改造后跑 npm/Playwright。 manual|playwright TODO DONE TODO DONE TODO DONE client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;artifacts/qa-flow/live-smoke-readiness-20260523-205755/summary.json;artifacts/qa-flow/oinv1-redacted-batch-smoke-20260523-205755/summary.json ["OINV1-2"] 编码前需要确认;公网上传会创建或复用票据记录,只允许 smoke 员工,且不提交报销、不付款。 完成:用户确认 Q1-Q6 全部 A 后实施脱敏加固。单票 live OCR 输出 alias/status/duration/reasonCategory/fieldHits;批量报告写 artifacts/qa-flow/*/summary.md 和 summary.json,仅含 alias、状态、耗时、字段命中布尔值和原因分类;最小公网单票与批量 smoke 均通过。回滚:恢复 3 个 Playwright 测试文件改动;不影响生产 API/schema。
6 OINV1-4 公网真实发票 OCR smoke 在脱敏加固后,用 OA-only/live smoke 员工账号逐张上传项目已有 PDF 样票,验证重复票、可用票、识别失败票的用户可理解终态;不提交报销,不创建付款。 生成脱敏 summary;每张样票有 alias、终态、耗时和字段命中;重复/已用于报销可解释时为 PASS_WITH_FINDINGS 或 PASS_DUPLICATE_EXPLAINED;不可解释 OCR 失败记录 finding。 受影响流程:员工登录 -> 票据上传 -> OCR 终态 -> 票据池查看;验证步骤:PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_ALLOW_DUPLICATE=1 npx playwright test tests/e2e/manual/live-invoice-ocr*.spec.ts --workers=1 --reporter=list。 playwright TODO TODO TODO client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;artifacts/qa-flow ["OINV1-3"] 写入边界:只允许票据上传 smoke 数据;不提交报销、不审批、不付款、不导出含敏感内容资料;报告不得含完整账号、发票号、销售方、金额、路径或票面原图。
7 OINV1-5 报销闭环测试数据准备 在 OCR smoke 可解释通过后,评估是否用 TEST/SMOKE 标记创建一条报销测试闭环,覆盖绑定票据、提交、审批、付款批次、回单和 AP 导出;真实业务闭环仍需人工最终验收。 若执行自动闭环,必须带 TEST/SMOKE marker,报告脱敏;若不执行,输出人工步骤和数据准备要求;不得执行真实银行付款。 受影响流程:票据池 -> 报销 -> 审批 -> 付款/AP;验证步骤:复用 reimbursement-payment-batch-flow 或 OAOPS1 真实业务门禁,必要时新建专项合同。 playwright|manual TODO TODO TODO client/tests/e2e/reimbursement-payment-batch-flow.spec.ts;scripts/qa/run-oa-real-use-daily.sh ["OINV1-4"] 付款仍由线下银行完成;自动化只能创建/验证 OA 测试数据,不能替代真实业务发票合规判断。
8 OINV1-6 本轮门禁和回填 完成合同校验、现有脚本审查、provider 样本基线和隐私检查;未获代码确认前,不改 live OCR spec;未获公网写入确认前,不运行上传型批量测试。 validate_issues_csv、process contract、git diff --check 通过;最终回复明确哪些自动完成、哪些需要用户批准或人工验收。 受影响流程:合同 -> 自动基线 -> 后续实施判断;验证步骤:validate_issues_csv.py <OINV1 csv>;node scripts/qa/check-process-contract.mjs --issue <OINV1 csv> --profile general --json;git diff --check。 python3|node|git TODO TODO TODO issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv ["OINV1-0","OINV1-1","OINV1-2"] 本轮可先做只读/本地 provider 基线;公网上传和脚本改造按用户确认继续。
@@ -1,7 +1,7 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"OANEXT2-0","建立后续工作细化采纳合同","基于 OINV1 外部发票样本池、OALAUNCH1 最终启用门禁、OANEXT1 采纳路线和 OAOPS1 自动巡检最新状态,建立下一轮后续工作细化合同;本合同只供用户审查采纳,不直接实施代码、部署或公网写入。","Issue CSV 校验通过;明确本合同不替代 OINV1、OALAUNCH1、OAOPS1 或 OANEXT1,只提供后续执行顺序、门禁和人工决策点;除已完成的审视/合同门禁外,执行项默认 TODO。","受影响流程:最新状态审视 -> 后续路线拆分 -> 用户审查采纳;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv。","python3|manual","DONE","DONE","DONE","issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv","[]","MAGI 审视:自动巡检和资产/发票样本准备较完整,但最终启用仍缺真实业务、权限、备份、7 天和 Go/No-Go;执行:新建采纳合同;提升:先自动补证据,再进入人工和运维门禁。回滚/清理:删除本规划 CSV 或后续提交即可,不影响公网、数据库或业务数据。"
"OANEXT2-1","最新状态基线审视","梳理当前未完成项:OINV1-3/4/5/6、OALAUNCH1-3..7、OANEXT1-2..9、OAOPS1-3..6;识别哪些可自动化、哪些需人工/运维批准。","列出当前最高优先级:1) OINV1 批量 OCR 脱敏与验证;2) 权限矩阵自动负例;3) TEST/SMOKE 业务闭环;4) 真实业务人工闭环;5) 备份恢复;6) 7 天稳定化;7) Go/No-Go。","受影响流程:Issue 状态读取 -> 后续排序 -> 采纳决策;验证步骤:读取 OINV1/OALAUNCH1/OANEXT1/OAOPS1 CSV 中 TODO 行并核对最新提交。","python3|git","DONE","DONE","DONE","issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv;issues/2026-05-23_16-49-39-oa-final-launch-readiness-v1.csv;issues/2026-05-23_19-14-44-oa-next-work-adoption-plan-v1.csv;issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv","[""OANEXT2-0""]","当前证据:OAOPS1 最新 PASS_OA_REAL_USE_DAILY_READY;OINV1 找到报销候选 114 个、PDF 105 个;OALAUNCH1 仍 PAUSED_MANUAL_CONFIRMATION。"
"OANEXT2-2","第一优先级:批量 OCR 报告脱敏加固","若用户采纳,优先实施 OINV1-3:改造或新增 QA-only 批量 OCR 报告逻辑,避免真实发票号、销售方、金额、完整文件名、完整路径和票面原图进入日志、附件或 summary。","批量 OCR 报告只输出 alias、状态、耗时、字段命中布尔值、重复/已使用/失败可解释原因;隐私扫描通过;不改变生产业务 API/schema。","受影响流程:真实 PDF 样本 -> 公网上传 OCR -> 脱敏报告;验证步骤:node/ts 语法检查;Playwright 单票 smoke;报告隐私扫描;validate OINV1。","playwright|node|git","TODO","TODO","TODO","client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;scripts/qa","[""OANEXT2-1""]","采纳选项:A 立即做脱敏加固;B 暂缓,只保留现有单票验证;C 不做批量 OCR。回滚:恢复测试脚本改动;不影响生产数据。"
"OANEXT2-2","第一优先级:批量 OCR 报告脱敏加固","若用户采纳,优先实施 OINV1-3:改造或新增 QA-only 批量 OCR 报告逻辑,避免真实发票号、销售方、金额、完整文件名、完整路径和票面原图进入日志、附件或 summary。","批量 OCR 报告只输出 alias、状态、耗时、字段命中布尔值、重复/已使用/失败可解释原因;隐私扫描通过;不改变生产业务 API/schema。","受影响流程:真实 PDF 样本 -> 公网上传 OCR -> 脱敏报告;验证步骤:node/ts 语法检查;Playwright 单票 smoke;报告隐私扫描;validate OINV1。","playwright|node|git","DONE","DONE","DONE","client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;artifacts/qa-flow/live-smoke-readiness-20260523-205755;artifacts/qa-flow/oinv1-redacted-batch-smoke-20260523-205755","[""OANEXT2-1""]","用户选择 A;已完成脱敏加固和最小公网验证。回滚:恢复测试脚本改动;不影响生产 API/schema 或既有业务数据。"
"OANEXT2-3","第二优先级:105 张 PDF 批量 OCR smoke","在 OANEXT2-2 脱敏加固后,使用 /tmp SAMPLE-PDF-xxx 别名池对现有 105 张 PDF 报销候选样本执行公网 OCR smoke;默认不提交报销、不审批、不付款。","输出批量脱敏 summary:总数、成功数、重复/已用数、失败/超时数、字段命中率和可解释失败分类;报告不含票面值、完整路径或原始 payload;发现不可解释失败进入修复合同。","受影响流程:PDF 样本池 -> 公网票据上传 -> OCR/重复终态 -> 脱敏统计;验证步骤:PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_ALLOW_DUPLICATE=1 执行批量 OCR Playwright;隐私扫描。","playwright|bash","TODO","TODO","TODO","artifacts/qa-flow;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts","[""OANEXT2-2""]","公网写入边界:只允许 smoke 员工票据上传测试记录;不提交报销、不审批、不付款。执行前需用户明确批准批量上传型 smoke。"
"OANEXT2-4","第三优先级:权限矩阵自动负例","补齐 OALAUNCH1-5/OANEXT1-3:用 OA-only smoke 账号自动验证员工、主管、财务、HR、CEO、管理员的入口可用性和普通员工越权负例。","普通员工不能访问财务/AP、合同敏感附件、他人审批、管理入口、审计导出、高敏或冻结模块;角色必要入口可用;报告脱敏且可作为 OALAUNCH_PERMISSION_EVIDENCE。","受影响流程:多角色登录 -> 页面入口 -> API 直连负例 -> 权限报告;验证步骤:复用 check-oa-only-smoke-readiness、permission-direct-access 或新增 QA-only 权限矩阵脚本。","playwright|node","TODO","TODO","TODO","scripts/qa/check-oa-only-smoke-readiness.mjs;client/tests/e2e/permission-direct-access.spec.ts;docs/trialrun/权限直连审计记录.md","[""OANEXT2-1""]","涉及权限模型修复时必须启用只读 reviewer subagent;本项优先只读验证,不直接改权限代码。"
"OANEXT2-5","第四优先级:TEST/SMOKE 业务闭环自动化","在不使用真实付款、不导入银行流水、不写 ERP 的前提下,准备用 TEST/SMOKE 标记自动跑 OA 主办域闭环:报销/OCR、审批、付款批次、银行回单附件、AP 导出、采购/费用、项目/合同关联。","自动化测试数据有 TEST/SMOKE marker;不含真实合同正文、敏感报价、银行流水原件或票面原图;可验证流程技术闭环,但不替代真实业务人工验收。","受影响流程:报销 -> 审批 -> 付款批次 -> 回单 -> AP;采购/费用 -> 项目/合同;验证步骤:复用 reimbursement-payment-batch-flow、run-oa-real-use-daily 或新增 QA-only flow;报告脱敏。","playwright|bash","TODO","TODO","TODO","client/tests/e2e/reimbursement-payment-batch-flow.spec.ts;scripts/qa/run-oa-real-use-daily.sh;artifacts/qa-flow","[""OANEXT2-2"",""OANEXT2-4""]","执行前需要用户确认是否允许公网创建 TEST/SMOKE 业务测试数据。实际付款仍由线下银行渠道完成,自动化不得触发真实支付。"
@@ -12,5 +12,5 @@
"OANEXT2-10","第九优先级:Day 7 Go/No-Go 决策","7 天稳定化和关键证据闭合后,由用户按 OALAUNCH1 做正式启用决策;系统不得自动替用户批准上线。","run-oa-final-launch-readiness 输出 PASS_OA_LAUNCH_READY,或用户明确接受 PASS_WITH_FINDINGS 并指定 owner/日期;否则保持暂停。","受影响流程:最终汇总 -> Go/No-Go -> 小团队正式启用或整改;验证步骤:用户明确选择 A/B/C 后设置 OALAUNCH_MANUAL_ACCEPT 并复跑最终汇总。","manual|bash","TODO","TODO","TODO","issues/2026-05-23_16-49-39-oa-final-launch-readiness-v1.csv;scripts/qa/run-oa-final-launch-readiness.sh","[""OANEXT2-8"",""OANEXT2-9""]","用户选项:A 接受 PASS_OA_LAUNCH_READY;B 接受 PASS_WITH_FINDINGS 但指定 owner/日期;C 暂停启用先整改。"
"OANEXT2-11","正式启用后 P1 候选池","在 OALAUNCH1 通过或稳定期反馈足够后,再从真实反馈中选择 1-2 个 P1:财务月结/代账交接、合同/项目提醒、轻量配置台、管理看板、资产运营、移动体验、通知可靠性。","每个 P1 候选都有业务价值、触发条件、验收方法、owner 和回滚边界;未通过 OALAUNCH1 前不得抢跑 P2 或复杂功能。","受影响流程:真实使用反馈 -> P1 排序 -> 新执行合同;验证步骤:人工复盘 7 天问题登记、OAOPS1 报告和用户反馈。","manual","TODO","TODO","TODO","issues/2026-05-23_17-20-00-oa-maturity-gap-review-v1.csv;docs/trialrun/人工试运行报告.md","[""OANEXT2-10""]","建议默认先看财务月结/代账交接和合同/项目提醒;若 7 天反馈指向移动或通知,则调整。"
"OANEXT2-12","P2 深集成继续冻结","正式启用前继续冻结复杂 BPM、预算、ERP/银行/税务/电子签章深集成、知识档案门户、AI 自动写规则、图片 OCR 扩展、企微 HR 深镜像、应急接管、恢复回写和 RuleOps。","冻结项不通过导航、搜索、API 或 feature flag 误开放给普通用户;后续重启必须另建专项 Issue CSV、风险评审和人工批准。","受影响流程:范围控制 -> 冻结入口 -> 后续专项评估;验证步骤:结合权限矩阵负例和 OALAUNCH1 权限证据检查冻结项不可见不可写。","manual|playwright|node","TODO","TODO","TODO","docs/集成边界.md;docs/产品口径.md;docs/企微优先开发路线图.md;client/src/features/navigation/schema.ts;server/src/routes/featureFlags.js","[""OANEXT2-4""]","成熟路线是先 P0 可控运行,再根据真实反馈螺旋升级,不追一次性大而全。"
"OANEXT2-13","用户采纳问答门禁","用户审查本合同后按选项决定是否采纳各组工作;未采纳项不得进入实施。","用户回复覆盖 Q1-Q6 或明确整体采纳;采纳结果回填后,才允许进入对应执行合同或更新 OINV1/OALAUNCH1 状态。","受影响流程:规划审查 -> A/B/C 决策 -> 后续执行;验证步骤:人工核对用户回复并 validate CSV。","manual|python3","TODO","TODO","TODO","issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv","[""OANEXT2-0""]","建议问答:Q1 批量 OCR 脱敏+验证:A 做/B 只单票/C 暂停;Q2 权限矩阵:A 自动跑/B 人工抽查/C 暂停;Q3 TEST/SMOKE 业务闭环:A 允许创建测试数据/B 只人工/C 暂停;Q4 真实业务闭环:A 本周开始/B 等 OCR 后/C 暂停;Q5 备份恢复:A 影子库演练/B 仅备份核对/C 暂停;Q6 7 天稳定化:A OCR+权限后启动/B 真实业务后启动/C 暂停。"
"OANEXT2-13","用户采纳问答门禁","用户审查本合同后按选项决定是否采纳各组工作;未采纳项不得进入实施。","用户回复覆盖 Q1-Q6 或明确整体采纳;采纳结果回填后,才允许进入对应执行合同或更新 OINV1/OALAUNCH1 状态。","受影响流程:规划审查 -> A/B/C 决策 -> 后续执行;验证步骤:人工核对用户回复并 validate CSV。","manual|python3","DONE","DONE","DONE","issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv","[""OANEXT2-0""]","2026-05-23 用户明确:Q1-Q6 全部选 A,并要求按方案实施。含义:允许 OCR 脱敏+批量验证、权限矩阵自动跑、TEST/SMOKE 业务闭环、真实业务本周开始、影子库恢复演练、OCR+权限后启动 7 天稳定化;仍不得伪造 7 天/人工/运维门禁完成。"
"OANEXT2-14","本规划合同门禁与停止点","完成本 CSV 校验、process contract、文档审计/skill 校验和 diff 检查;提交后等待用户审查采纳,不继续实施。","合同通过校验并提交;除审视和合同门禁外,执行项保持 TODO;最终回复说明下一步需要用户按 Q1-Q6 采纳。","受影响流程:规划 -> 校验 -> 提交 -> 用户审查;验证步骤:validate_issues_csv.py;node scripts/qa/check-process-contract.mjs --baseline --json;node scripts/qa/check-process-contract.mjs --issue <csv> --profile general --json;node scripts/qa/audit-docs.mjs --check-registry;quick_validate.py .agents/skills/hyb-oa-delivery;git diff --check。","python3|node|git","TODO","TODO","TODO","issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv","[""OANEXT2-0""]","本轮只做规划;不部署、不写公网、不修改业务代码。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 OANEXT2-0 建立后续工作细化采纳合同 基于 OINV1 外部发票样本池、OALAUNCH1 最终启用门禁、OANEXT1 采纳路线和 OAOPS1 自动巡检最新状态,建立下一轮后续工作细化合同;本合同只供用户审查采纳,不直接实施代码、部署或公网写入。 Issue CSV 校验通过;明确本合同不替代 OINV1、OALAUNCH1、OAOPS1 或 OANEXT1,只提供后续执行顺序、门禁和人工决策点;除已完成的审视/合同门禁外,执行项默认 TODO。 受影响流程:最新状态审视 -> 后续路线拆分 -> 用户审查采纳;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv。 python3|manual DONE DONE DONE issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv [] MAGI 审视:自动巡检和资产/发票样本准备较完整,但最终启用仍缺真实业务、权限、备份、7 天和 Go/No-Go;执行:新建采纳合同;提升:先自动补证据,再进入人工和运维门禁。回滚/清理:删除本规划 CSV 或后续提交即可,不影响公网、数据库或业务数据。
3 OANEXT2-1 最新状态基线审视 梳理当前未完成项:OINV1-3/4/5/6、OALAUNCH1-3..7、OANEXT1-2..9、OAOPS1-3..6;识别哪些可自动化、哪些需人工/运维批准。 列出当前最高优先级:1) OINV1 批量 OCR 脱敏与验证;2) 权限矩阵自动负例;3) TEST/SMOKE 业务闭环;4) 真实业务人工闭环;5) 备份恢复;6) 7 天稳定化;7) Go/No-Go。 受影响流程:Issue 状态读取 -> 后续排序 -> 采纳决策;验证步骤:读取 OINV1/OALAUNCH1/OANEXT1/OAOPS1 CSV 中 TODO 行并核对最新提交。 python3|git DONE DONE DONE issues/2026-05-23_20-24-42-oa-invoice-automation-hardening-v1.csv;issues/2026-05-23_16-49-39-oa-final-launch-readiness-v1.csv;issues/2026-05-23_19-14-44-oa-next-work-adoption-plan-v1.csv;issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv ["OANEXT2-0"] 当前证据:OAOPS1 最新 PASS_OA_REAL_USE_DAILY_READY;OINV1 找到报销候选 114 个、PDF 105 个;OALAUNCH1 仍 PAUSED_MANUAL_CONFIRMATION。
4 OANEXT2-2 第一优先级:批量 OCR 报告脱敏加固 若用户采纳,优先实施 OINV1-3:改造或新增 QA-only 批量 OCR 报告逻辑,避免真实发票号、销售方、金额、完整文件名、完整路径和票面原图进入日志、附件或 summary。 批量 OCR 报告只输出 alias、状态、耗时、字段命中布尔值、重复/已使用/失败可解释原因;隐私扫描通过;不改变生产业务 API/schema。 受影响流程:真实 PDF 样本 -> 公网上传 OCR -> 脱敏报告;验证步骤:node/ts 语法检查;Playwright 单票 smoke;报告隐私扫描;validate OINV1。 playwright|node|git TODO DONE TODO DONE TODO DONE client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;scripts/qa client/tests/e2e/manual/live-invoice-ocr.spec.ts;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;artifacts/qa-flow/live-smoke-readiness-20260523-205755;artifacts/qa-flow/oinv1-redacted-batch-smoke-20260523-205755 ["OANEXT2-1"] 采纳选项:A 立即做脱敏加固;B 暂缓,只保留现有单票验证;C 不做批量 OCR。回滚:恢复测试脚本改动;不影响生产数据。 用户选择 A;已完成脱敏加固和最小公网验证。回滚:恢复测试脚本改动;不影响生产 API/schema 或既有业务数据。
5 OANEXT2-3 第二优先级:105 张 PDF 批量 OCR smoke 在 OANEXT2-2 脱敏加固后,使用 /tmp SAMPLE-PDF-xxx 别名池对现有 105 张 PDF 报销候选样本执行公网 OCR smoke;默认不提交报销、不审批、不付款。 输出批量脱敏 summary:总数、成功数、重复/已用数、失败/超时数、字段命中率和可解释失败分类;报告不含票面值、完整路径或原始 payload;发现不可解释失败进入修复合同。 受影响流程:PDF 样本池 -> 公网票据上传 -> OCR/重复终态 -> 脱敏统计;验证步骤:PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_ALLOW_DUPLICATE=1 执行批量 OCR Playwright;隐私扫描。 playwright|bash TODO TODO TODO artifacts/qa-flow;client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts ["OANEXT2-2"] 公网写入边界:只允许 smoke 员工票据上传测试记录;不提交报销、不审批、不付款。执行前需用户明确批准批量上传型 smoke。
6 OANEXT2-4 第三优先级:权限矩阵自动负例 补齐 OALAUNCH1-5/OANEXT1-3:用 OA-only smoke 账号自动验证员工、主管、财务、HR、CEO、管理员的入口可用性和普通员工越权负例。 普通员工不能访问财务/AP、合同敏感附件、他人审批、管理入口、审计导出、高敏或冻结模块;角色必要入口可用;报告脱敏且可作为 OALAUNCH_PERMISSION_EVIDENCE。 受影响流程:多角色登录 -> 页面入口 -> API 直连负例 -> 权限报告;验证步骤:复用 check-oa-only-smoke-readiness、permission-direct-access 或新增 QA-only 权限矩阵脚本。 playwright|node TODO TODO TODO scripts/qa/check-oa-only-smoke-readiness.mjs;client/tests/e2e/permission-direct-access.spec.ts;docs/trialrun/权限直连审计记录.md ["OANEXT2-1"] 涉及权限模型修复时必须启用只读 reviewer subagent;本项优先只读验证,不直接改权限代码。
7 OANEXT2-5 第四优先级:TEST/SMOKE 业务闭环自动化 在不使用真实付款、不导入银行流水、不写 ERP 的前提下,准备用 TEST/SMOKE 标记自动跑 OA 主办域闭环:报销/OCR、审批、付款批次、银行回单附件、AP 导出、采购/费用、项目/合同关联。 自动化测试数据有 TEST/SMOKE marker;不含真实合同正文、敏感报价、银行流水原件或票面原图;可验证流程技术闭环,但不替代真实业务人工验收。 受影响流程:报销 -> 审批 -> 付款批次 -> 回单 -> AP;采购/费用 -> 项目/合同;验证步骤:复用 reimbursement-payment-batch-flow、run-oa-real-use-daily 或新增 QA-only flow;报告脱敏。 playwright|bash TODO TODO TODO client/tests/e2e/reimbursement-payment-batch-flow.spec.ts;scripts/qa/run-oa-real-use-daily.sh;artifacts/qa-flow ["OANEXT2-2","OANEXT2-4"] 执行前需要用户确认是否允许公网创建 TEST/SMOKE 业务测试数据。实际付款仍由线下银行渠道完成,自动化不得触发真实支付。
12 OANEXT2-10 第九优先级:Day 7 Go/No-Go 决策 7 天稳定化和关键证据闭合后,由用户按 OALAUNCH1 做正式启用决策;系统不得自动替用户批准上线。 run-oa-final-launch-readiness 输出 PASS_OA_LAUNCH_READY,或用户明确接受 PASS_WITH_FINDINGS 并指定 owner/日期;否则保持暂停。 受影响流程:最终汇总 -> Go/No-Go -> 小团队正式启用或整改;验证步骤:用户明确选择 A/B/C 后设置 OALAUNCH_MANUAL_ACCEPT 并复跑最终汇总。 manual|bash TODO TODO TODO issues/2026-05-23_16-49-39-oa-final-launch-readiness-v1.csv;scripts/qa/run-oa-final-launch-readiness.sh ["OANEXT2-8","OANEXT2-9"] 用户选项:A 接受 PASS_OA_LAUNCH_READY;B 接受 PASS_WITH_FINDINGS 但指定 owner/日期;C 暂停启用先整改。
13 OANEXT2-11 正式启用后 P1 候选池 在 OALAUNCH1 通过或稳定期反馈足够后,再从真实反馈中选择 1-2 个 P1:财务月结/代账交接、合同/项目提醒、轻量配置台、管理看板、资产运营、移动体验、通知可靠性。 每个 P1 候选都有业务价值、触发条件、验收方法、owner 和回滚边界;未通过 OALAUNCH1 前不得抢跑 P2 或复杂功能。 受影响流程:真实使用反馈 -> P1 排序 -> 新执行合同;验证步骤:人工复盘 7 天问题登记、OAOPS1 报告和用户反馈。 manual TODO TODO TODO issues/2026-05-23_17-20-00-oa-maturity-gap-review-v1.csv;docs/trialrun/人工试运行报告.md ["OANEXT2-10"] 建议默认先看财务月结/代账交接和合同/项目提醒;若 7 天反馈指向移动或通知,则调整。
14 OANEXT2-12 P2 深集成继续冻结 正式启用前继续冻结复杂 BPM、预算、ERP/银行/税务/电子签章深集成、知识档案门户、AI 自动写规则、图片 OCR 扩展、企微 HR 深镜像、应急接管、恢复回写和 RuleOps。 冻结项不通过导航、搜索、API 或 feature flag 误开放给普通用户;后续重启必须另建专项 Issue CSV、风险评审和人工批准。 受影响流程:范围控制 -> 冻结入口 -> 后续专项评估;验证步骤:结合权限矩阵负例和 OALAUNCH1 权限证据检查冻结项不可见不可写。 manual|playwright|node TODO TODO TODO docs/集成边界.md;docs/产品口径.md;docs/企微优先开发路线图.md;client/src/features/navigation/schema.ts;server/src/routes/featureFlags.js ["OANEXT2-4"] 成熟路线是先 P0 可控运行,再根据真实反馈螺旋升级,不追一次性大而全。
15 OANEXT2-13 用户采纳问答门禁 用户审查本合同后按选项决定是否采纳各组工作;未采纳项不得进入实施。 用户回复覆盖 Q1-Q6 或明确整体采纳;采纳结果回填后,才允许进入对应执行合同或更新 OINV1/OALAUNCH1 状态。 受影响流程:规划审查 -> A/B/C 决策 -> 后续执行;验证步骤:人工核对用户回复并 validate CSV。 manual|python3 TODO DONE TODO DONE TODO DONE issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv ["OANEXT2-0"] 建议问答:Q1 批量 OCR 脱敏+验证:A 做/B 只单票/C 暂停;Q2 权限矩阵:A 自动跑/B 人工抽查/C 暂停;Q3 TEST/SMOKE 业务闭环:A 允许创建测试数据/B 只人工/C 暂停;Q4 真实业务闭环:A 本周开始/B 等 OCR 后/C 暂停;Q5 备份恢复:A 影子库演练/B 仅备份核对/C 暂停;Q6 7 天稳定化:A OCR+权限后启动/B 真实业务后启动/C 暂停。 2026-05-23 用户明确:Q1-Q6 全部选 A,并要求按方案实施。含义:允许 OCR 脱敏+批量验证、权限矩阵自动跑、TEST/SMOKE 业务闭环、真实业务本周开始、影子库恢复演练、OCR+权限后启动 7 天稳定化;仍不得伪造 7 天/人工/运维门禁完成。
16 OANEXT2-14 本规划合同门禁与停止点 完成本 CSV 校验、process contract、文档审计/skill 校验和 diff 检查;提交后等待用户审查采纳,不继续实施。 合同通过校验并提交;除审视和合同门禁外,执行项保持 TODO;最终回复说明下一步需要用户按 Q1-Q6 采纳。 受影响流程:规划 -> 校验 -> 提交 -> 用户审查;验证步骤:validate_issues_csv.py;node scripts/qa/check-process-contract.mjs --baseline --json;node scripts/qa/check-process-contract.mjs --issue <csv> --profile general --json;node scripts/qa/audit-docs.mjs --check-registry;quick_validate.py .agents/skills/hyb-oa-delivery;git diff --check。 python3|node|git TODO TODO TODO issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv ["OANEXT2-0"] 本轮只做规划;不部署、不写公网、不修改业务代码。