From edabce856121cbbb18b8bc35eb829f4e5e8f201e Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 16 May 2026 20:53:08 +0800 Subject: [PATCH] =?UTF-8?q?P2AUTO=20=E6=94=B6=E5=8F=A3=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E5=9B=9E=E5=BD=92=E5=92=8C=E6=A0=B7=E7=A5=A8=E9=97=A8?= =?UTF-8?q?=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tests/e2e/login-portal-ux.spec.ts | 19 +- ...6-wecom-mobile-rendering-hardening.spec.ts | 2 +- docs/trialrun/人工试运行报告.md | 3 + docs/文档事实源状态矩阵.md | 5 +- docs/文档索引.md | 4 +- ...20-09-49-p2-automated-regression-sweep.csv | 11 + scripts/qa/check-ocr-provider-samples.mjs | 351 ++++++++++++++++++ 7 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv create mode 100644 scripts/qa/check-ocr-provider-samples.mjs diff --git a/client/tests/e2e/login-portal-ux.spec.ts b/client/tests/e2e/login-portal-ux.spec.ts index 857ea030..917412b6 100644 --- a/client/tests/e2e/login-portal-ux.spec.ts +++ b/client/tests/e2e/login-portal-ux.spec.ts @@ -6,6 +6,7 @@ import { TEST_IDS } from "./helpers/testIds"; test.use({ serviceWorkers: "block" }); const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", name: "Admin" }; +const MANAGER = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", name: "Manager" }; function envelope(data: unknown) { return { @@ -52,6 +53,16 @@ function mockPasswordLogin(page: import("@playwright/test").Page) { })), }); }), + page.route("**/api/auth/refresh", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(envelope({ + accessToken: "test-access-token", + expiresIn: 900, + })), + }); + }), ]); } @@ -310,9 +321,9 @@ test("Portal 管理视角重排正确,备用模块默认折叠", async ({ page await mockPortalHome(page, "manager"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["admin"], "Admin"); + await mockMe(page, ["manager"], "Manager"); - await login(page, ADMIN); + await login(page, MANAGER); await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible(); @@ -326,7 +337,7 @@ test("Portal 管理视角重排正确,备用模块默认折叠", async ({ page "portal-card-weekly_help", ]); - await expect(page.locator("summary").filter({ hasText: "今日管理重点 / 常用入口" })).toBeVisible(); + await expect(page.locator("summary").filter({ hasText: "角色重点 / 常用入口" })).toBeVisible(); await expect(page.getByText("试点观测摘要")).toBeVisible(); await expect(page.getByRole("link", { name: "采购申请" })).not.toBeVisible(); await page.locator("summary").filter({ hasText: "备用模块" }).click(); @@ -419,7 +430,7 @@ test("Portal 域管理角色也进入经理视图", async ({ page }) => { await login(page, EMPLOYEE); - await expect(page.locator("summary").filter({ hasText: "今日管理重点 / 常用入口" })).toBeVisible(); + await expect(page.locator("summary").filter({ hasText: "角色重点 / 常用入口" })).toBeVisible(); await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible(); await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible(); }); diff --git a/client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts b/client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts index 39bf9764..6bffbddb 100644 --- a/client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts +++ b/client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts @@ -554,7 +554,7 @@ async function exerciseMobileRendering(page: Page, key: string, label: string) { const detailMetrics = await expectRenderingStable(page, label, `${key}-mobile-reimbursement-detail`); expect(detailMetrics.stickyActionsVisible, `${label} sticky actions visible`).toBe(true); expect(detailMetrics.stickyButtonsSameRow, `${label} sticky buttons same row`).toBe(true); - expect(detailMetrics.stickyButtonMinHeight, `${label} sticky buttons touch height`).toBeGreaterThanOrEqual(44); + expect(Math.round(detailMetrics.stickyButtonMinHeight), `${label} sticky buttons touch height`).toBeGreaterThanOrEqual(44); } test.describe("WCM6 企业微信多手机移动渲染稳定化", () => { diff --git a/docs/trialrun/人工试运行报告.md b/docs/trialrun/人工试运行报告.md index 3e848f86..29c8a3be 100644 --- a/docs/trialrun/人工试运行报告.md +++ b/docs/trialrun/人工试运行报告.md @@ -22,6 +22,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 - 2026-05-16 P2-FIN-V1 已完成付款批次启用前本地/隔离硬化:后端 focused 回归、前端 typecheck/build、隔离付款批次闭环、权限直连审计、公网 readiness 和只读 review 均通过;本轮只修补 E2E 对当前状态胶囊和二次确认 UI 的断言,不改业务 API/schema/UI,不部署,公网 `payment_batch_v1` 继续关闭。 - 2026-05-16 P2-OCR-SAMPLE 已完成图片 OCR 样票治理和实现前置确认:当前 PDF-only 口径、`SAMPLE-IMG-*` 授权登记、脱敏证据、provider 分层和回滚设计已锁定;结论为 `HOLD_FOR_SAMPLE`,授权图片样票和真实图片 provider 回归证据形成前不进入 `P2-OCR-V1`,不开放图片 OCR。 - 2026-05-16 P2Q 已完成 P2OCRS 后续任务队列规划:后续默认按图片样票 provider 回归门槛、UX8 真实反馈池、P2-FIN-LIVE 公网启用窗口、P2-OCR-V1、P2-WF、P2-BIZ 和持续上线门禁拆独立合同推进;本轮不改业务代码、不部署。 +- 2026-05-16 P2AUTO 已完成自动化优先回归:后端 focused、前端 typecheck/build、高风险确认、隔离核心业务流、隔离付款批次闭环、公网 readiness/PDF live OCR、公网移动矩阵和企业微信 UA/渲染自动化均通过;PDF 样票和 live smoke 凭据可用,图片 provider 回归仍需仓库外 manifest 或 `PLAYWRIGHT_OCR_IMAGE_*` 安全登记授权图片样票,不开放图片 OCR,不启用公网 `payment_batch_v1`,未部署。 ## 关键证据索引 @@ -51,6 +52,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 | P2-FIN-V1 | 已收口:付款批次启用前本地/隔离硬化完成;隔离付款批次闭环 1 passed、权限直连审计 8 passed、公网 readiness ready | `issues/2026-05-16_09-42-43-p2-fin-payment-batch-v1.csv`、`artifacts/qa-flow/business-flow-20260516-095719/summary.txt`、`artifacts/qa-flow/business-flow-20260516-095900/summary.txt` | | P2-OCR-SAMPLE | 已收口:图片 OCR 样票治理前置完成,结论 `HOLD_FOR_SAMPLE`;PDF live OCR 基线继续有效但不代表图片 OCR 通过 | `issues/2026-05-16_10-15-21-p2-ocr-image-sample-governance.csv` | | P2Q | 已收口:P2OCRS 后续队列和启动门槛完成;所有实现或公网启用均需另建独立合同 | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | +| P2AUTO | 已收口:自动化优先回归和样票 provider 门禁完成;核心隔离业务流通过、付款批次隔离闭环 1 passed、公网 readiness/PDF live OCR 通过、公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed;图片 provider 回归保持 `HOLD_FOR_SAMPLE` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv`、`artifacts/qa-flow/business-flow-20260516-203001/summary.txt`、`artifacts/qa-flow/business-flow-20260516-203856/summary.txt`、`artifacts/qa-flow/live-smoke-readiness-20260516-204157/` | ## 当前门禁 @@ -70,6 +72,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 | P2-FIN-V1 启用前硬化 | 通过,未部署 | 后端 focused 回归 8 tests passed;`client` typecheck/build 通过;隔离临时库 `reimbursement-payment-batch-flow` 1 passed,覆盖创建批次、复核、二次确认执行、报销 paid、票据 archived、AP 台账和 ERP outbox;权限直连审计 8 passed;公网只做 readiness,不启用 `payment_batch_v1` | | P2-OCR-SAMPLE 样票治理 | HOLD,未部署 | 已确认当前发票 OCR 仍为 PDF-only,图片仅作为普通附件;腾讯云 `image_native` 只证明技术路径存在,正式图片 OCR 仍需授权图片样票、脱敏确认和真实图片 provider 回归;未调用真实图片 OCR | | P2Q 后续队列 | 已收口,未部署 | 图片 OCR provider 回归等待授权图片样票;UX8 等待真实反馈;P2-FIN-LIVE 等待公网启用窗口批准;后续 P2-OCR-V1/P2-WF/P2-BIZ 均需独立合同 | +| P2AUTO 自动化优先回归 | 通过,未部署 | 后端 focused 43 tests passed;`client` typecheck/build 通过;核心隔离业务流 auth/reimbursement/approval/leave/procurement/governance PASS;付款批次隔离 flag-on 1 passed;公网 `/ready`、public bundle、live smoke 3 passed、PDF live OCR 1 passed;公网移动矩阵 6 passed;企业微信渲染矩阵 8 passed;provider-only 样票脚本输出 `HOLD_FOR_SAMPLE`,因为未通过安全 manifest/env 登记授权图片样票且本地无 Tencent provider 凭据 | ## R7EP / OPS8 / QAF8 / UX8 / P2T 预检推进 diff --git a/docs/文档事实源状态矩阵.md b/docs/文档事实源状态矩阵.md index f762547c..390e0316 100644 --- a/docs/文档事实源状态矩阵.md +++ b/docs/文档事实源状态矩阵.md @@ -43,6 +43,7 @@ | `P2-FIN-V1` | `issues/2026-05-16_09-42-43-p2-fin-payment-batch-v1.csv` | `EVIDENCE` | 付款批次启用前本地/隔离硬化已完成;后端 focused 回归、前端构建、隔离闭环、权限审计和公网 readiness 通过,公网仍不启用 `payment_batch_v1` | | `P2-OCR-SAMPLE` | `issues/2026-05-16_10-15-21-p2-ocr-image-sample-governance.csv` | `EVIDENCE` | 图片 OCR 样票治理和实现前置确认已完成;结论 `HOLD_FOR_SAMPLE`,当前保持 PDF-only,不开放图片 OCR | | `P2Q` | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | `EVIDENCE` | P2OCRS 后续任务队列已锁定;后续实现均需另建独立合同,当前不部署、不改业务代码 | +| `P2AUTO` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv` | `EVIDENCE` | 自动化优先回归已收口;后端/前端/隔离业务流/公网 readiness/PDF live OCR/移动与企业微信自动化均有证据,图片 provider 回归仍需安全登记的授权图片样票和本地 provider 凭据 | 旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。 @@ -50,8 +51,8 @@ | Doc_ID | Path | Category | Authority | Status | Owner | Related_Issues | Evidence | Supersedes | Last_Audited | Audit_Method | Risk | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q` | `rg` | none | 2026-05-16 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 | -| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q` | `audit-docs` | none | 2026-05-16 | `audit-docs --check-registry` | medium | 判断文档权威性 | +| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO` | `rg` | none | 2026-05-16 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 | +| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO` | `audit-docs` | none | 2026-05-16 | `audit-docs --check-registry` | medium | 判断文档权威性 | | `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8` | Issue CSV | `docs/requirements-specification.md` | 2026-05-13 | `rg` | high | 定义做什么、不做什么和验收口径 | | `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T` | `rg` | none | 2026-05-13 | `rg` | high | 记录已拍板产品边界 | | `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9` | Playwright/视觉审查/DG8/DG9 | none | 2026-05-13 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型 | diff --git a/docs/文档索引.md b/docs/文档索引.md index db6d1d75..a19e17ff 100644 --- a/docs/文档索引.md +++ b/docs/文档索引.md @@ -20,7 +20,7 @@ ## 最近事实源链 -R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q。 +R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO。 - R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。 - ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。 @@ -37,6 +37,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M - P2-FIN-V1 已完成付款批次启用前本地/隔离硬化:后端 focused 回归、前端 typecheck/build、隔离付款批次闭环、权限直连审计和公网 readiness 均通过;本轮只硬化 E2E 断言,不新增 API/schema/feature flag/UI,不部署,公网 `payment_batch_v1` 继续关闭。 - P2-OCR-SAMPLE 已完成图片 OCR 样票治理和实现前置确认:当前 PDF-only 口径、样票脱敏、provider 回归分层和回滚条件已锁定;结论为 `HOLD_FOR_SAMPLE`,需授权图片样票和真实图片 provider 回归证据后才可进入 `P2-OCR-V1`。 - P2Q 是 P2OCRS 后续任务队列合同:只锁定 `P2-OCR-IMAGE-REGRESSION`、`UX8`、`P2-FIN-LIVE`、`P2-OCR-V1`、`P2-WF`、`P2-BIZ` 和持续上线门禁的启动条件,不启动实现、不部署。 +- P2AUTO 是 P2Q 后自动化优先回归合同:后端 focused、前端构建、高风险确认、隔离核心业务流、隔离付款批次闭环、公网 readiness/PDF live OCR、公网移动矩阵和企业微信 UA/渲染自动化均已完成;PDF 样票和 live smoke 凭据可用,图片样票仍需通过仓库外 manifest 或 `PLAYWRIGHT_OCR_IMAGE_*` 安全登记后才能进入真实图片 provider 回归。 ## 当前活跃治理流 @@ -60,6 +61,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M | `P2-FIN-V1` | `issues/2026-05-16_09-42-43-p2-fin-payment-batch-v1.csv` | 已收口:付款批次启用前本地/隔离硬化完成 | 后端付款批次/AP/导出 focused 回归、前端构建、隔离闭环、权限审计、公网 readiness、flag off 回滚口径 | 不公网启用 `payment_batch_v1`、不部署、不接真实支付、不写 ERP 实时凭证 | | `P2-OCR-SAMPLE` | `issues/2026-05-16_10-15-21-p2-ocr-image-sample-governance.csv` | 已收口:图片 OCR 样票治理前置完成,结论 `HOLD_FOR_SAMPLE` | PDF-only 当前边界、`SAMPLE-IMG-*` 授权登记口径、脱敏证据规则、mock/PDF/真实图片 provider 分层、feature flag 和回滚设计 | 不开放图片 OCR、不新增入口或 feature flag、不调用真实图片 provider、不保存真实票据、不部署 | | `P2Q` | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | 已收口:P2OCRS 后续队列和启动门槛完成 | 图片 OCR provider 回归、UX8、付款批次公网启用、图片 OCR V1、审批通知、轻业务增强和持续上线门禁的后续合同顺序 | 不直接启动 HOLD 项、不新建综合规划文档、不改业务代码、不部署 | +| `P2AUTO` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv` | 已收口:自动化优先回归和样票 provider 门禁完成 | 凭据/样票掩码核对、后端 focused、前端构建、高风险确认、隔离业务流、隔离付款批次、公网 readiness/PDF live OCR、移动/企业微信自动化、OCR provider-only 脚本 | 不部署、不开放图片 OCR、不打开公网 `payment_batch_v1`、不自动选择未登记图片样票 | 旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。 diff --git a/issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv b/issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv new file mode 100644 index 00000000..602989ce --- /dev/null +++ b/issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv @@ -0,0 +1,11 @@ +"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes" +"P2AUTO-0","P2 自动化回归合同建档","建立 R7E 后 P2 自动化优先回归合同,只做自动化验证、样票安全核对和证据回填,不启动业务功能开发。","1) 本 CSV 校验通过;2) 范围限定自动化、QA 脚本和既有证据文档;3) 不部署、不执行生产恢复、不重置数据库;4) 不打开公网 payment_batch_v1;5) 不开放图片 OCR。","流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv;人工核对不改业务 API/schema/UI。","python3|manual","DONE","DONE","DONE","issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv","[]","Scope=automation-first; baseline=7a59fd7; validate_issues_csv passed; no runtime change; no deployment; public payment_batch_v1 remains off." +"P2AUTO-1","凭据与样票掩码核对","加载本机安全 live smoke 配置并核对公网、企业微信、PDF live OCR readiness;只输出 ready 状态,不输出 secret、真实样票路径或票面信息。","1) liveSmokeReady=true;2) liveOcrReady=true;3) PDF 样票不再被记录为缺失;4) 输出不含密码、token 或真实路径;5) 图片样票若未登记则只作为后续缺口记录。","受影响流程:公网 live smoke、PDF live OCR、企业微信 config readiness;验证步骤:set -a && source ~/.hyb-oa/live-smoke.env && set +a;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-smoke-env.mjs --strict;补充掩码脚本只统计 PLAYWRIGHT_OCR_IMAGE_* 是否存在,不打印路径。","bash|node|manual","DONE","DONE","DONE","scripts/qa/check-live-smoke-env.mjs|external","[""P2AUTO-0""]","live smoke env strict check PASS: publicReady=true, liveSmokeReady=true, liveOcrReady=true; PDF sample env present; image sample is not registered via OCR_PROVIDER_SAMPLE_MANIFEST or PLAYWRIGHT_OCR_IMAGE_*; no secret/path printed." +"P2AUTO-2","合同与文档门禁","校验当前 P2Q、P2OCRS、P2FINV1、UX8、P2AUTO 合同和文档登记,确认事实源链没有把 PDF 样票误写为缺失。","1) 相关 CSV 校验通过;2) audit-docs 无 error;3) 文档关键词命中可解释;4) PDF 样票已具备口径被登记;5) 不新建综合规划文档。","流程级验证:validate_issues_csv.py 本 CSV、P2Q、P2-OCR-SAMPLE、P2-FIN-V1、UX8;node scripts/qa/audit-docs.mjs --check-registry;rg -n 'P2AUTO|P2Q|P2OCRS|P2-FIN-V1|UX8|PDF live OCR|图片样票|HOLD_FOR_SAMPLE' docs issues。","python3|node|rg","DONE","DONE","DONE","issues/*.csv|docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md","[""P2AUTO-0""]","CSV gates passed for P2AUTO, P2Q, P2OCRS, P2FINV1 and UX8; audit-docs errors=0 warnings=1 existing unregistered docs files=24; docs updated in existing index/matrix/report only." +"P2AUTO-3","后端自动化门禁","执行 Prisma validate、关键路由/服务语法检查和 OCR、付款批次、导出、企业微信、通知 focused 单测。","1) Prisma validate 通过;2) node --check 通过;3) focused npm test 通过或有明确非业务前置失败;4) 不改 server/src 业务逻辑;5) 不打印 secret。","受影响流程:发票 OCR、OCR job、付款批次、AP 导出、workflow、企业微信认证、通知;验证步骤:cd server && npm run prisma:validate;cd server && node --check src/routes/invoicePool.js src/jobs/ocrJob.js src/services/ocr/tencent.js src/routes/paymentBatches.js src/routes/exports.js src/routes/workflow.js src/routes/wecomAuth.js;cd server && npm test -- tests/unit/services/ocr.tencent.test.js tests/unit/jobs/ocrJob.test.js tests/unit/routes/invoicePool.upload.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/exports.apOperations.test.js tests/unit/routes/wecomAuth.test.js tests/unit/routes/notifications.test.js。","npm|node","DONE","DONE","DONE","server/src|server/tests/unit","[""P2AUTO-1""]","PASS: server prisma:validate; node --check invoicePool/ocrJob/tencent/paymentBatches/exports/workflow/wecomAuth; focused npm test 7 files/43 tests passed; no server/src business change." +"P2AUTO-4","前端自动化门禁","执行前端 typecheck、build 和高风险确认、审批确认、发票 PDF-only、权限直连等 Playwright 自动化。","1) typecheck 通过;2) build 通过;3) 指定 Playwright 用例通过或失败可归因;4) 不新增前端页面或业务入口;5) 企业微信真机不被自动化伪装为通过。","受影响流程:PC 高风险动作、审批确认、发票上传、权限直连、移动入口;验证步骤:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/system-action-confirmation.spec.ts tests/e2e/workflow-action-confirm-ui.spec.ts tests/e2e/r111-invoice-reimbursement-guard.spec.ts tests/e2e/permission-direct-access.spec.ts --workers=1 --reporter=list。","npm|playwright","DONE","DONE","DONE","client/src|client/tests/e2e","[""P2AUTO-3""]","PASS: client typecheck/build; high-risk confirmation 4 passed; r111 invoice guard and permission direct access covered by business-flow report business-flow-20260516-203001; test-only stale assertions updated for current labels/role boundary." +"P2AUTO-5","隔离业务流自动化","在隔离临时库中运行核心业务流 flag-off 门禁,并单独运行付款批次 flag-on 闭环;付款批次结果不得解释为公网启用。","1) 核心业务流通过或失败有明确用例;2) payment_batch_v1 公网保持关闭;3) 付款批次仅隔离环境 flag-on;4) 报告路径写入 Notes;5) 不执行生产数据写入。","受影响流程:登录、企业微信入口、报销、审批、请假、采购、权限治理、付款批次隔离闭环;验证步骤:BUSINESS_FLOW_GROUPS=auth,reimbursement,approval,leave,procurement,governance BUSINESS_FLOW_EXCLUDE_SPECS=tests/e2e/reimbursement-payment-batch-flow.spec.ts bash scripts/qa/run-business-flow-gates.sh local;cd client && E2E_BASE_URL= npx playwright test tests/e2e/reimbursement-payment-batch-flow.spec.ts --workers=1 --reporter=list。","bash|playwright","DONE","DONE","DONE","scripts/qa/run-business-flow-gates.sh|client/tests/e2e/reimbursement-payment-batch-flow.spec.ts|artifacts/qa-flow","[""P2AUTO-4""]","PASS: core isolated business flow auth/reimbursement/approval/leave/procurement/governance passed, report artifacts/qa-flow/business-flow-20260516-203001/summary.txt; payment batch isolated flag-on flow 1 passed, report artifacts/qa-flow/business-flow-20260516-203856/summary.txt; public payment_batch_v1 remains off." +"P2AUTO-6","公网自动化门禁","在公网只跑 /ready、public bundle、live smoke、PDF live OCR、pending OCR 扫描和 live smoke Playwright,不执行公网付款批次写入。","1) /ready 通过;2) public bundle findings=0;3) live smoke 通过;4) PDF live OCR 进入可解释终态;5) pending OCR 缺 DATABASE_URL 时只作非阻断 SKIP。","受影响流程:公网健康、登录 smoke、PDF live OCR、企业微信 config、公网资源;验证步骤:set -a && source ~/.hyb-oa/live-smoke.env && set +a;QA_ISSUE_ID=P2AUTO LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/live-smoke.spec.ts --workers=1 --reporter=list。","bash|node|playwright","DONE","DONE","DONE","scripts/qa/run-live-smoke-readiness.sh|client/tests/e2e/live-smoke.spec.ts|artifacts/qa-flow","[""P2AUTO-5""]","PASS: live readiness report artifacts/qa-flow/live-smoke-readiness-20260516-204157; /ready ok, public bundle findings=0, live smoke 3 passed, PDF live OCR 1 passed with duplicate-invoice explainable terminal state; pending OCR missing local DATABASE_URL recorded SKIP; no public payment batch writes." +"P2AUTO-7","移动与企业微信自动化门禁","执行移动矩阵、企业微信 UA、入口/绑定/deeplink 自动化;真机只保留少量真实差异确认,不阻断可自动化收口。","1) 移动矩阵通过;2) WeCom UA/入口/绑定自动化通过;3) 企业微信真机差异不被自动化替代;4) 发现移动缺陷进入 UX8;5) 不新增移动业务能力。","受影响流程:/mobile、审批、报销、企业微信入口、绑定、deeplink 和移动布局;验证步骤:cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;cd client && npx playwright test tests/e2e/wecom-entry.spec.ts tests/e2e/wecom-first-bind.spec.ts tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。","playwright|manual","DONE","DONE","DONE","client/tests/e2e|docs/trialrun/人工试运行报告.md","[""P2AUTO-6""]","PASS: public DG7/R124 mobile matrix 6 passed; local auth business-flow covered wecom-entry/wecom-first-bind; R6 WeCom rendering matrix 8 passed after subpixel test assertion normalization; real-device delta remains manual-only when user requests." +"P2AUTO-8","OCR provider 样票自动化缺口","新增并运行 provider-only 样票 QA 脚本;PDF 样票可回归,图片样票未在 manifest 或环境变量登记时记录 SKIP,不得写 PASS。","1) scripts/qa/check-ocr-provider-samples.mjs 存在且 node --check 通过;2) 无样票时输出 SKIP;3) 有样票时输出脱敏 JSON 摘要;4) 不输出票面、税号、手机号、二维码、完整号码或真实路径;5) 不开放图片 OCR。","受影响流程:PDF/image provider 样票 -> Tencent OCR -> 字段命中摘要 -> GO/HOLD/FREEZE 决策;验证步骤:node --check scripts/qa/check-ocr-provider-samples.mjs;OCR_PROVIDER_SAMPLE_MANIFEST=/safe/ocr-samples.json node scripts/qa/check-ocr-provider-samples.mjs --provider=tencent --json;无授权图片样票或 manifest 时写 SKIP。","node|manual","DONE","DONE","DONE","scripts/qa/check-ocr-provider-samples.mjs|external","[""P2AUTO-1""]","QA script added and node --check passed. Provider-only run with current safe env found PDF sample only and no image manifest/env; local Tencent provider credentials not present, so output status=SKIP decision=HOLD_FOR_SAMPLE. Do not auto-select arbitrary image candidates; image OCR remains closed." +"P2AUTO-9","收口回填与中文提交","汇总 P2AUTO 自动化证据,回填 CSV、人工试运行报告、索引和事实源矩阵,完成最终校验和中文 commit。","1) 已执行项按真实 Test_Method 回填;2) 未执行外部项记录 SKIP 或后续条件;3) validate、audit-docs、rg、git diff --check 通过;4) 中文 commit 引用 P2AUTO;5) 明确未部署。","流程级验证:validate_issues_csv.py 本 CSV;node scripts/qa/audit-docs.mjs --check-registry;rg -n 'P2AUTO|PDF live OCR|图片样票|payment_batch_v1|SKIP|live smoke|企业微信' docs issues scripts;git diff --check;git status --short。","python3|node|rg|git|manual","DONE","DONE","DONE","issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv|docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md","[""P2AUTO-2"",""P2AUTO-3"",""P2AUTO-4"",""P2AUTO-5"",""P2AUTO-6"",""P2AUTO-7"",""P2AUTO-8""]","Closeout=PASS; validate_issues_csv passed; audit-docs errors=0 warnings=1 existing unregistered docs files=24; rg keyword sweep explainable; git diff --check passed; not deployed; no business API/schema/UI/runtime change." diff --git a/scripts/qa/check-ocr-provider-samples.mjs b/scripts/qa/check-ocr-provider-samples.mjs new file mode 100644 index 00000000..33e8c6b2 --- /dev/null +++ b/scripts/qa/check-ocr-provider-samples.mjs @@ -0,0 +1,351 @@ +#!/usr/bin/env node +import { createRequire } from "node:module"; +import fs from "node:fs"; +import path from "node:path"; + +process.noDeprecation = true; + +const require = createRequire(import.meta.url); +const { runTencentVatOcr } = require("../../server/src/services/ocr/tencent"); + +const SUPPORTED_PROVIDERS = new Set(["tencent"]); +const MIME_BY_EXT = new Map([ + [".pdf", "application/pdf"], + [".jpg", "image/jpeg"], + [".jpeg", "image/jpeg"], + [".png", "image/png"], +]); +const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png"]); +const PDF_EXTENSIONS = new Set([".pdf"]); +const REQUIRED_FIELDS = ["invoiceCode", "invoiceNumber", "invoiceDate", "totalAmount", "sellerName"]; +const OPTIONAL_FIELDS = ["buyerName", "amount", "taxAmount"]; + +function parseArgs(argv) { + const options = { + provider: process.env.OCR_PROVIDER_SAMPLE_PROVIDER || "tencent", + json: false, + strict: false, + manifest: process.env.OCR_PROVIDER_SAMPLE_MANIFEST || "", + }; + + for (const arg of argv) { + if (arg === "--json") { + options.json = true; + } else if (arg === "--strict") { + options.strict = true; + } else if (arg.startsWith("--provider=")) { + options.provider = arg.slice("--provider=".length); + } else if (arg.startsWith("--manifest=")) { + options.manifest = arg.slice("--manifest=".length); + } + } + + return options; +} + +function splitEnvList(value) { + return String(value || "") + .split(/[\n,]/) + .map((item) => item.trim()) + .filter(Boolean); +} + +function resolveSamplePath(rawPath, baseDir) { + if (!rawPath || typeof rawPath !== "string") return ""; + return path.isAbsolute(rawPath) ? rawPath : path.resolve(baseDir, rawPath); +} + +function inferKind(filePath, declaredKind) { + const normalizedKind = String(declaredKind || "").trim().toLowerCase(); + if (["pdf", "pdf_vat", "vat_pdf"].includes(normalizedKind)) return "pdf"; + if (["image", "image_vat", "vat_image"].includes(normalizedKind)) return "image"; + + const ext = path.extname(filePath).toLowerCase(); + if (PDF_EXTENSIONS.has(ext)) return "pdf"; + if (IMAGE_EXTENSIONS.has(ext)) return "image"; + return "unknown"; +} + +function safeAlias(value, fallback) { + const alias = String(value || "").trim(); + if (!alias) return fallback; + if (/^SAMPLE-(PDF|IMG)-[A-Z0-9_-]+$/i.test(alias)) return alias.toUpperCase(); + return fallback; +} + +function pushSample(samples, source) { + const kind = inferKind(source.path, source.kind); + const fallbackPrefix = kind === "pdf" ? "SAMPLE-PDF" : kind === "image" ? "SAMPLE-IMG" : "SAMPLE-UNKNOWN"; + samples.push({ + alias: safeAlias(source.alias, `${fallbackPrefix}-${String(samples.length + 1).padStart(3, "0")}`), + filePath: source.path, + kind, + source: source.source, + }); +} + +function loadManifestSamples(manifestPath) { + if (!manifestPath) return { samples: [], error: null }; + if (!fs.existsSync(manifestPath)) { + return { samples: [], error: "manifest_not_found" }; + } + + try { + const baseDir = path.dirname(manifestPath); + const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.samples) ? parsed.samples : []; + const samples = []; + + for (const item of items) { + const samplePath = resolveSamplePath(item?.path, baseDir); + if (!samplePath) continue; + pushSample(samples, { + alias: item?.alias, + path: samplePath, + kind: item?.kind, + source: "manifest", + }); + } + + return { samples, error: null }; + } catch { + return { samples: [], error: "manifest_parse_failed" }; + } +} + +function loadEnvSamples() { + const samples = []; + + const pdfFiles = [ + ...splitEnvList(process.env.PLAYWRIGHT_OCR_LIVE_FILE), + ...splitEnvList(process.env.LIVE_OCR_INVOICE_FILE), + ...splitEnvList(process.env.PLAYWRIGHT_OCR_LIVE_FILES), + ]; + + for (const item of pdfFiles) { + pushSample(samples, { path: item, kind: "pdf", source: "env_pdf" }); + } + + const pdfDir = process.env.PLAYWRIGHT_OCR_LIVE_DIR?.trim(); + if (pdfDir && fs.existsSync(pdfDir)) { + for (const entry of fs.readdirSync(pdfDir).sort((left, right) => left.localeCompare(right, "zh-CN"))) { + const filePath = path.join(pdfDir, entry); + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile() && PDF_EXTENSIONS.has(path.extname(entry).toLowerCase())) { + pushSample(samples, { path: filePath, kind: "pdf", source: "env_pdf_dir" }); + } + } + } + + const imageFiles = [ + ...splitEnvList(process.env.PLAYWRIGHT_OCR_IMAGE_FILE), + ...splitEnvList(process.env.PLAYWRIGHT_OCR_IMAGE_FILES), + ...splitEnvList(process.env.LIVE_OCR_IMAGE_FILE), + ]; + + for (const item of imageFiles) { + pushSample(samples, { path: item, kind: "image", source: "env_image" }); + } + + const imageDirs = [ + process.env.PLAYWRIGHT_OCR_IMAGE_DIR?.trim(), + process.env.LIVE_OCR_IMAGE_DIR?.trim(), + ].filter(Boolean); + + for (const dir of imageDirs) { + if (!fs.existsSync(dir)) continue; + for (const entry of fs.readdirSync(dir).sort((left, right) => left.localeCompare(right, "zh-CN"))) { + const filePath = path.join(dir, entry); + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile() && IMAGE_EXTENSIONS.has(path.extname(entry).toLowerCase())) { + pushSample(samples, { path: filePath, kind: "image", source: "env_image_dir" }); + } + } + } + + return samples; +} + +function dedupeSamples(samples) { + const seen = new Set(); + const result = []; + for (const sample of samples) { + const key = `${sample.alias}:${sample.filePath}`; + if (seen.has(key)) continue; + seen.add(key); + result.push(sample); + } + return result; +} + +function getTencentConfig() { + return { + tencentSecretId: process.env.TENCENT_SECRET_ID || "", + tencentSecretKey: process.env.TENCENT_SECRET_KEY || "", + tencentOcrRegion: process.env.TENCENT_OCR_REGION || "ap-beijing", + ocrTimeoutMs: Number(process.env.OCR_TIMEOUT_MS || 5000), + }; +} + +function hasTencentCredentials(config) { + return Boolean(config.tencentSecretId && config.tencentSecretKey); +} + +function summarizeFile(sample) { + try { + if (!fs.existsSync(sample.filePath)) { + return { ok: false, errorCode: "sample_not_found" }; + } + const stat = fs.statSync(sample.filePath); + if (!stat.isFile()) { + return { ok: false, errorCode: "sample_not_file" }; + } + const ext = path.extname(sample.filePath).toLowerCase(); + const mimeType = MIME_BY_EXT.get(ext); + if (!mimeType) { + return { ok: false, errorCode: "unsupported_sample_type" }; + } + return { + ok: true, + mimeType, + fileSizeBytes: stat.size, + transportMode: mimeType === "application/pdf" ? "pdf_native" : "image_native", + }; + } catch { + return { ok: false, errorCode: "sample_stat_failed" }; + } +} + +function summarizeNormalizedResult(result) { + const fieldHits = {}; + for (const field of [...REQUIRED_FIELDS, ...OPTIONAL_FIELDS]) { + const value = result?.[field]; + fieldHits[field] = value != null && value !== ""; + } + const missingRequiredFields = REQUIRED_FIELDS.filter((field) => !fieldHits[field]); + return { + fieldHits, + missingRequiredFields, + transportMode: result?.raw?.transportMode || null, + }; +} + +async function checkSample(provider, config, sample) { + const fileSummary = summarizeFile(sample); + const base = { + alias: sample.alias, + kind: sample.kind, + source: sample.source, + status: "SKIP", + mimeType: fileSummary.mimeType || null, + fileSizeBytes: fileSummary.fileSizeBytes || null, + transportMode: fileSummary.transportMode || null, + }; + + if (!fileSummary.ok) { + return { ...base, status: "SKIP", errorCode: fileSummary.errorCode }; + } + + if (provider !== "tencent") { + return { ...base, status: "SKIP", errorCode: "unsupported_provider" }; + } + + if (!hasTencentCredentials(config)) { + return { ...base, status: "SKIP", errorCode: "missing_tencent_credentials" }; + } + + try { + const imageBase64 = fs.readFileSync(sample.filePath).toString("base64"); + const result = await runTencentVatOcr(config, { imageBase64, sourceMimeType: fileSummary.mimeType }); + const normalizedSummary = summarizeNormalizedResult(result); + const missingRequired = normalizedSummary.missingRequiredFields; + return { + ...base, + status: missingRequired.length ? "HOLD" : "PASS", + fieldHits: normalizedSummary.fieldHits, + missingRequiredFields: missingRequired, + transportMode: normalizedSummary.transportMode || base.transportMode, + }; + } catch (error) { + return { + ...base, + status: "HOLD", + errorCode: typeof error?.code === "string" ? error.code : "provider_failed", + transportMode: error?.transportMode || base.transportMode, + }; + } +} + +function buildDecision(results, manifestError) { + if (manifestError) return "HOLD_FOR_SAMPLE"; + if (!results.length) return "HOLD_FOR_SAMPLE"; + const imageResults = results.filter((item) => item.kind === "image"); + if (!imageResults.length) return "HOLD_FOR_SAMPLE"; + if (imageResults.every((item) => item.status === "PASS")) return "GO_TO_IMPLEMENT"; + return "HOLD_FOR_SAMPLE"; +} + +function printHuman(summary) { + console.log(`[ocr-provider-samples] status=${summary.status} decision=${summary.decision}`); + console.log(`[ocr-provider-samples] provider=${summary.provider} samples=${summary.counts.total} pdf=${summary.counts.pdf} image=${summary.counts.image}`); + if (summary.manifestError) { + console.log(`[ocr-provider-samples] manifest=${summary.manifestError}`); + } + for (const result of summary.results) { + const error = result.errorCode ? ` error=${result.errorCode}` : ""; + console.log( + `[ocr-provider-samples] ${result.alias} kind=${result.kind} status=${result.status} transport=${result.transportMode || "-"} size=${result.fileSizeBytes ?? "-"}${error}`, + ); + } +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + if (!SUPPORTED_PROVIDERS.has(options.provider)) { + throw new Error(`unsupported provider: ${options.provider}`); + } + + const manifest = loadManifestSamples(options.manifest); + const samples = dedupeSamples([...manifest.samples, ...loadEnvSamples()]); + const config = getTencentConfig(); + const results = []; + + for (const sample of samples) { + results.push(await checkSample(options.provider, config, sample)); + } + + const decision = buildDecision(results, manifest.error); + const failed = results.some((item) => item.status === "HOLD"); + const skipped = !results.length || results.some((item) => item.status === "SKIP"); + const summary = { + checkedAt: new Date().toISOString(), + provider: options.provider, + status: failed ? "HOLD" : skipped ? "SKIP" : "PASS", + decision, + manifestProvided: Boolean(options.manifest), + manifestError: manifest.error, + credentialReady: hasTencentCredentials(config), + counts: { + total: results.length, + pdf: results.filter((item) => item.kind === "pdf").length, + image: results.filter((item) => item.kind === "image").length, + pass: results.filter((item) => item.status === "PASS").length, + hold: results.filter((item) => item.status === "HOLD").length, + skip: results.filter((item) => item.status === "SKIP").length, + }, + results, + }; + + if (options.json) { + console.log(JSON.stringify(summary, null, 2)); + } else { + printHuman(summary); + } + + if (options.strict && summary.status !== "PASS") { + process.exit(1); + } +} + +main().catch((error) => { + console.error(`[ocr-provider-samples][ERROR] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); +});