From 5b47755241908aab102180cb0dace31cba9cb81f Mon Sep 17 00:00:00 2001 From: shunli Date: Tue, 19 May 2026 13:07:46 +0800 Subject: [PATCH] =?UTF-8?q?WAPC2F=20=E5=AE=9E=E7=8E=B0=E4=BC=81=E5=BE=AE?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E6=A0=B7=E4=BB=B6=20dry-run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/hyb-oa-delivery/SKILL.md | 3 +- docs/trialrun/人工试运行报告.md | 2 + docs/企微API能力目录.md | 11 +- docs/企微优先开发路线图.md | 17 +- docs/文档事实源状态矩阵.md | 22 +- docs/文档索引.md | 8 +- docs/管理员手册.md | 13 +- docs/需求-企业微信基础设施规约.md | 4 +- docs/需求-统一审批中心规约.md | 4 +- ...ecom-approval-fixture-auto-generate-v1.csv | 18 +- server/package.json | 1 + .../scripts/check-wecom-approval-fixtures.js | 772 ++++++++++++++++++ .../services/integrations/wecomApproval.js | 140 ++++ .../check-wecom-approval-fixtures.test.js | 318 ++++++++ 14 files changed, 1287 insertions(+), 46 deletions(-) create mode 100644 server/src/scripts/check-wecom-approval-fixtures.js create mode 100644 server/tests/unit/scripts/check-wecom-approval-fixtures.test.js diff --git a/.agents/skills/hyb-oa-delivery/SKILL.md b/.agents/skills/hyb-oa-delivery/SKILL.md index 5c27e1b3..8874535f 100644 --- a/.agents/skills/hyb-oa-delivery/SKILL.md +++ b/.agents/skills/hyb-oa-delivery/SKILL.md @@ -17,7 +17,7 @@ Read only what is relevant: - For approval workflow, roles, self-approval, notification, deeplink, reminder, or escalation work, read `docs/需求-统一审批中心规约.md`. - For HR lifecycle, employee onboarding/offboarding, Enterprise WeChat directory sync, account disablement, or Enterprise WeChat API work, read `docs/需求-企业微信基础设施规约.md`, `docs/企微API能力目录.md`, `docs/管理员手册.md`, and the latest HCLT/HRWD/WAPI/WRRA Issue CSV evidence first. - For any Enterprise WeChat capability, Secret, callback, approval, check-in, HR assistant, or contact-directory task, read `docs/企微API能力目录.md` before planning or coding. WAPI fixes the current Secret matrix: contacts use `WECOM_CONTACT_SECRET`; workbench/OAuth/messages/HR assistant/roster/check-in/approval use `WECOM_AGENT_SECRET`; `WECOM_CHECKIN_SECRET` is only an explicit check-in override; `WECOM_HR_ASSISTANT_SECRET` is historical compatibility and not a default. Do not write runtime code that tries multiple Secrets to guess the correct one. -- For Enterprise WeChat-first HR/attendance runtime migration, read `docs/企微优先开发路线图.md` before planning or coding. WDRG makes the roadmap authoritative for stage order; WAPI adds the runtime-first stage before deep mirror work; WRRA states that WOUA `PASS_WITH_FINDINGS` does not clear deep mirror work; WOUA-FIX has now soft-archived the 7 OA active test accounts missing in WeCom, fixed the `T0000` seed reactivation behavior, transferred the `SMOKE201` test-manager side effects, and rerun WOUA with `oaActiveMissingInWecom=0`. WCPC has rerun the capability POC with an existing active test account and reached `GO_TO_HR_MASTER_SYNC`; WHMS/WHMSLIVE has implemented, deployed, and publicly verified `wecom_hr_master_sync_v1`, with the production flag restored to `force_off` and production real-employee batch apply not approved. WMSG1 proves real app-message delivery and Deeplink return only; WAPC1 fixes the approval configuration preparation checklist before mirror work; WAPC2 adds the leave/missed-punch approval template and low-risk `sp_no` read-only POC runner. WPROC1 fixes the engineering route: WAPC2 must be followed by WAPC2F automatic POC fixtures, WAPC2 rerun, WCBK1 callback POC, WACP3 compensation-pull POC, and WADM1 mirror data model design before `wecom_attendance_approval_mirror_v1`. WPROC2 adds the executable `process gate`: before WAPC2F or other high-risk WeCom work, run `node scripts/qa/check-process-contract.mjs --baseline` and the issue-specific profile check to enforce DoR/DoD, fixed Secret source, non-self approval, live write switches, missing-config classification, and redaction boundaries. Do not start `wecom_attendance_approval_mirror_v1`, `oa_hr_emergency_mode_v1`, `wecom_recovery_writeback_v1`, or `wecom_ruleops_v1` without the next stage's own Issue CSV. +- For Enterprise WeChat-first HR/attendance runtime migration, read `docs/企微优先开发路线图.md` before planning or coding. WDRG makes the roadmap authoritative for stage order; WAPI adds the runtime-first stage before deep mirror work; WRRA states that WOUA `PASS_WITH_FINDINGS` does not clear deep mirror work; WOUA-FIX has now soft-archived the 7 OA active test accounts missing in WeCom, fixed the `T0000` seed reactivation behavior, transferred the `SMOKE201` test-manager side effects, and rerun WOUA with `oaActiveMissingInWecom=0`. WCPC has rerun the capability POC with an existing active test account and reached `GO_TO_HR_MASTER_SYNC`; WHMS/WHMSLIVE has implemented, deployed, and publicly verified `wecom_hr_master_sync_v1`, with the production flag restored to `force_off` and production real-employee batch apply not approved. WMSG1 proves real app-message delivery and Deeplink return only; WAPC1 fixes the approval configuration preparation checklist before mirror work; WAPC2 adds the leave/missed-punch approval template and low-risk `sp_no` read-only POC runner. WPROC1 fixes the engineering route: WAPC2 must be followed by WAPC2F automatic POC fixtures, WAPC2 rerun, WCBK1 callback POC, WACP3 compensation-pull POC, and WADM1 mirror data model design before `wecom_attendance_approval_mirror_v1`. WPROC2 adds the executable `process gate`: before WAPC2F or other high-risk WeCom work, run `node scripts/qa/check-process-contract.mjs --baseline` and the issue-specific profile check to enforce DoR/DoD, fixed Secret source, non-self approval, live write switches, missing-config classification, and redaction boundaries. WAPC2F has implemented and deployed `npm run check:wecom-approval-fixtures`; public dry-run reached `PASS_FIXTURE_DRY_RUN_READY` at `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json`, but no real WeCom template or approval was created because `WECOM_APPROVAL_FIXTURE_WRITE=1` was not set. Do not start `wecom_attendance_approval_mirror_v1`, `oa_hr_emergency_mode_v1`, `wecom_recovery_writeback_v1`, or `wecom_ruleops_v1` without the next stage's own Issue CSV. - For reimbursement, invoice, OCR, duplicate invoice, or attachment work, read `docs/报销发票附件规则.md` and `references/reimbursement.md`. - For local/WeCom/public trial-run, OCR runtime, Caddy, or DB drift work, read `docs/试运行与自动化验证手册.md` and `references/trialrun.md`. - For how this skill should evolve, read `docs/Agent开发交付实践.md`. @@ -111,6 +111,7 @@ Final responses should include: - WAPC1 has fixed the WeCom approval configuration preparation gate: first templates are leave, missed punch, outing, and overtime; each needs template ID, scope, fields, approver rules, visible scope, and self-built app permission; low-risk test `sp_no` must not contain procurement, contract, reimbursement, invoice, bank receipt, sensitive attachment, or real sensitive business material; callback URL/Token/EncodingAESKey are deployment-only values and must not be written to repo docs or Issue CSV. Do not implement real approval callback handling, deep mirror, emergency mode, recovery writeback, or RuleOps until a later approved Issue covers the POC or implementation. - WAPC2 has added the WeCom approval template/detail read-only POC runner for leave and missed punch. Use `npm run check:wecom-approval-poc`; pass template IDs with `--template=leave:` / `--template=missed_punch:` and low-risk test approval numbers with `--sp-no=leave:` / `--sp-no=missed_punch:`. The runner must use only `WECOM_AGENT_SECRET`, redact full template IDs and `sp_no`, and never output form values, attachments, comments, full userid, token, Secret, or raw payload. - WPROC1 fixes the WeCom approval engineering process. Future WeCom work must satisfy route gate, capability gate, and implementation gate; use automatic runners before asking the user to do manual work. WPROC2 makes this executable through `node scripts/qa/check-process-contract.mjs --baseline` plus issue-specific profiles such as `--profile wecom-approval-fixture`; run it before WAPC2F and later high-risk WeCom contracts. Manual work is limited to write approval, secrets/callback config, WeCom backend settings that cannot be API-driven, real-device confirmation, and risk decisions. High-risk stages require POC before implementation: callback signature/decryption/idempotency, compensation pull, mirror data model, then mirror implementation. +- WAPC2F has added the WeCom approval fixture dry-run runner. Use `npm run check:wecom-approval-fixtures`; it defaults to applicant `SMOKE002`, approver `SMOKE101`, fixed `WECOM_AGENT_SECRET`, minimal generic OA-POC leave/missed-punch template payloads, and no write token or WeCom writes. Real fixture creation requires `--live --create-templates --submit-test-approvals` plus one-process `WECOM_APPROVAL_FIXTURE_WRITE=1` and explicit user approval. The public dry-run report is `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` with `PASS_FIXTURE_DRY_RUN_READY`; WAPC2 auto-rerun still requires real generated template IDs and `sp_no`. - Approval-chain correctness can be proven by PC or automation, while Enterprise WeChat validates entry, binding,免登录, notification, deeplink, and mobile layout. - If real-device mobile validation is paused after automation passes, record the gate as `PAUSED`; do not close RS6/UI6/R6 mobile blockers until representative real-device confirmation is recorded. - Role-specific pages should use one shared OA/mobile framework and tailor summaries, entries, actions, and sensitive fields by role. Do not create unrelated UI systems for employee, approver, finance, admin, or CEO views. diff --git a/docs/trialrun/人工试运行报告.md b/docs/trialrun/人工试运行报告.md index 19a3f64a..c4f501c4 100644 --- a/docs/trialrun/人工试运行报告.md +++ b/docs/trialrun/人工试运行报告.md @@ -54,6 +54,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 - 2026-05-19 WMSG1 完成企微应用消息实发与 Deeplink 真机验证:使用现有 `check:wecom-runtime-ready` runner,在远端容器内仅对单次命令设置 `WECOM_RUNTIME_TEST_SEND=1`,向管理员本人对应 OA 员工号 `HYD000006` 发送一条脱敏运行就绪消息。第一次按 `wangshunli` 参数执行未命中 OA `employee_no`,`recipientCount=0` 且未实际发出;随后按 `HYD000006` 重跑成功,报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T234431Z/summary.json` 显示 `checks.message.status=OK`、`code=wecom_app_sent`、`errcode=0`、`recipientCount=1`、收件企微前缀 `WangShun***`。用户确认企业微信可收到“OA 运行就绪检查”,点击“打开 OA”后可进入 OA 通知页面,结论 `PASS_MESSAGE_DELIVERY_CONFIRMED`。本轮不补企微资料、不配置审批模板/`sp_no`/真实回调、不进入深度镜像。 - 2026-05-19 WAPC1 完成企微审批配置准备清单回写:WMSG1 只证明消息和 Deeplink 可用,不放行 `wecom_attendance_approval_mirror_v1`。本轮固化首批模板为请假、补卡、外出、加班;每个模板需准备模板 ID、适用范围、字段项、审批人规则、可见范围和所属自建应用权限;低风险测试 `sp_no` 不得包含采购、合同、报销、发票、银行回单或高敏附件;真实回调 Token/EncodingAESKey 只允许保存在部署环境,不写入文档或 Issue。后续 POC 需验证模板详情、审批详情、回调验签/解密/幂等、重试语义和补偿拉取。本轮不改业务代码、不部署、不接真实回调、不调用企微写接口。 - 2026-05-19 WAPC2 新增企微审批模板与低风险 `sp_no` 只读 POC:新增 `server/src/scripts/check-wecom-approval-poc.js` / `npm run check:wecom-approval-poc` 和只读集成服务,范围先锁定请假、补卡;固定 `WECOM_AGENT_SECRET` 调用 `POST /cgi-bin/oa/gettemplatedetail` 与 `POST /cgi-bin/oa/getapprovaldetail`,报告只输出脱敏模板 ID/`sp_no` 前缀、字段类型计数、审批节点计数和错误分类。本地 focused vitest 已覆盖 Secret 固定选择、无 fallback、缺配置、API 错误分类和脱敏输出。已以 `SYNC_ENV=0` 部署并远端执行 live 只读 POC,报告 `/app/artifacts/qa-flow/wecom-approval-poc-20260519T020429Z/summary.json` 结论为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`:远端 `WECOM_AGENT_SECRET` 可用,但缺请假/补卡模板 ID 和两条低风险 `sp_no`;未伪造成 PASS。本轮不接真实回调、不调用企微写接口、不写 OA/企微数据、不进入深度镜像。 +- 2026-05-19 WAPC2F 新增企微审批 POC 样件自动生成 dry-run:新增 `server/src/scripts/check-wecom-approval-fixtures.js` / `npm run check:wecom-approval-fixtures`,固定 `WECOM_AGENT_SECRET` 构建 OA-POC 请假/补卡最小通用模板候选和低风险审批申请候选;默认 dry-run 不获取写入 token、不调用企微写接口、不创建模板、不提交审批单。已以 `SYNC_ENV=0` 部署并远端执行 dry-run,报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 结论为 `PASS_FIXTURE_DRY_RUN_READY`:`SMOKE002 -> SMOKE101` 均 active、已绑定企微、非自审,payload 脱敏输出通过。live 创建 OA-POC 测试模板和测试审批单仍未批准、未执行;WAPC2 自动复跑需等 live 生成真实模板 ID 和 `sp_no` 后再执行。 ## 关键证据索引 @@ -106,6 +107,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开 | WMSG1 | 已完成:企微应用消息真实发送和 Deeplink 真机确认通过;报告 `checks.message.status=OK`、`errcode=0`、`recipientCount=1`,用户确认收到消息并可点击进入 OA 通知页面 | `issues/2026-05-19_07-41-37-wecom-message-live-verify-v1.csv`、`/app/artifacts/qa-flow/wecom-runtime-ready-20260518T234431Z/summary.json` | | WAPC1 | 已完成:企微审批配置准备清单已回写;首批模板为请假、补卡、外出、加班;低风险 `sp_no`、真实回调和后续 POC 准入项已固定;不代表审批镜像或回调接收已实现 | `issues/2026-05-19_08-44-58-wecom-approval-config-prep-v1.csv`、`docs/企微优先开发路线图.md`、`docs/企微API能力目录.md` | | WAPC2 | 已部署并公网只读复跑:新增请假/补卡模板详情和低风险审批详情 QA-only 只读 POC runner;远端 Secret 矩阵 OK,因缺请假/补卡模板 ID 和两条低风险 `sp_no` 返回 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`;不代表真实回调或审批镜像已实现 | `issues/2026-05-19_09-34-16-wecom-approval-template-spno-poc-v1.csv`、`server/src/scripts/check-wecom-approval-poc.js`、`server/src/services/integrations/wecomApproval.js`、`/app/artifacts/qa-flow/wecom-approval-poc-20260519T020429Z/summary.json` | +| WAPC2F | 已部署并公网 dry-run 通过:新增企微审批 POC 样件 runner;远端 dry-run 报告为 `PASS_FIXTURE_DRY_RUN_READY`,确认 `SMOKE002 -> SMOKE101` 非自审链、Secret 矩阵和脱敏报告边界;未真实创建模板或审批单 | `issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv`、`server/src/scripts/check-wecom-approval-fixtures.js`、`/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` | ## 当前门禁 diff --git a/docs/企微API能力目录.md b/docs/企微API能力目录.md index b57a286c..a9fbe57e 100644 --- a/docs/企微API能力目录.md +++ b/docs/企微API能力目录.md @@ -2,8 +2,8 @@ **文档状态**:当前企微 API 调研与本项目使用事实源。 **分类**:集成事实源 / API 能力目录。 -**适用时间**:WAPI / WAPC1 / WAPC2 / WPROC1 / WPROC2 之后。 -**关联 Issue**:`WAPI,WAPC1,WAPC2,WPROC1,WPROC2`。 +**适用时间**:WAPI / WAPC1 / WAPC2 / WPROC1 / WPROC2 / WAPC2F 之后。 +**关联 Issue**:`WAPI,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F`。 **最近核对**:2026-05-19。 **不得误读**:本文只记录本项目会用到的企业微信官方 API 摘要、链接和项目决策,不复制官方文档全文,不保存 Secret、access token、完整 userid、手机号、邮箱或企微原始 payload。官方文档和企业微信后台权限配置始终是最终依据。 @@ -59,9 +59,9 @@ | 审批申请详情 | [获取审批申请详情](https://developer.work.weixin.qq.com/document/path/91983) | `/cgi-bin/oa/getapprovaldetail` | `POST` | `WECOM_AGENT_SECRET` | `POC_RUNNER_READY_NEEDS_CONFIG` | WAPC2 已新增低风险 `sp_no` 只读 POC runner;测试单不得包含采购、合同、报销、发票、银行回单或高敏附件。 | | 审批状态回调 | [审批申请状态变化回调通知](https://developer.work.weixin.qq.com/document/path/91815) | 事件回调 | `POST` 事件 | `WECOM_AGENT_SECRET` 所属应用 | `NEEDS_CONFIG` | 需要真实回调 URL、Token、EncodingAESKey、验签、解密、幂等和重试设计;文档和报告只记录配置项名称,不保存密钥值。 | | 批量获取审批单号 | [批量获取审批单号](https://developer.work.weixin.qq.com/document/path/91816) | 官方审批单查询接口 | `POST` | `WECOM_AGENT_SECRET` | `NEEDS_API_POC` | 作为补偿拉取候选,WAPC1 后续 POC 前不得假定可用。 | -| 提交审批申请 | [提交审批申请](https://developer.work.weixin.qq.com/document/path/91853) | 官方提交审批接口 | `POST` | `WECOM_AGENT_SECRET` | `WAPC2F_CANDIDATE_WRITE_POC` | 仅可用于 OA-POC 低风险请假/补卡测试审批单自动样件;默认 dry-run,live 写入需显式开关和批准,申请人/审批人必须非自审。OA 主办报销、采购、合同、项目不迁到企微原生审批。 | +| 提交审批申请 | [提交审批申请](https://developer.work.weixin.qq.com/document/path/91853) | `/cgi-bin/oa/applyevent` | `POST` | `WECOM_AGENT_SECRET` | `WAPC2F_DRY_RUN_READY` | 已接入 WAPC2F QA-only runner;仅可用于 OA-POC 低风险请假/补卡测试审批单自动样件。默认 dry-run 不获取写入 token、不提交审批;live 写入需显式 CLI、`WECOM_APPROVAL_FIXTURE_WRITE=1` 和人工批准,申请人/审批人必须非自审。OA 主办报销、采购、合同、项目不迁到企微原生审批。 | | 假期余额 | [获取成员假期余额](https://developer.work.weixin.qq.com/document/path/93376) | `/cgi-bin/oa/vacation/getuservacationquota` | `POST` | `WECOM_AGENT_SECRET` | `CAN_REUSE_NOW_OR_NEEDS_PERMISSION` | WCPC 已证明可读但历史有权限错误;后续作为考勤镜像的只读项。 | -| 审批模板创建/更新 | [创建审批模板](https://developer.work.weixin.qq.com/document/path/97437)、[更新审批模板](https://developer.work.weixin.qq.com/document/path/97438) | 官方模板写接口 | `POST` | `WECOM_AGENT_SECRET` | `RULEOPS_LATER_WRITE` | 只允许 RuleOps 后期在人工确认、前后快照和回滚建议齐全时写入。 | +| 审批模板创建/更新 | [创建审批模板](https://developer.work.weixin.qq.com/document/path/97437)、[更新审批模板](https://developer.work.weixin.qq.com/document/path/97438) | `/cgi-bin/oa/approval/create_template` 等官方模板写接口 | `POST` | `WECOM_AGENT_SECRET` | `WAPC2F_DRY_RUN_READY_FOR_CREATE_TEMPLATE` | WAPC2F 仅实现 OA-POC 请假/补卡测试模板创建候选路径,当前公网只执行 dry-run;live 创建需显式 CLI、`WECOM_APPROVAL_FIXTURE_WRITE=1` 和人工批准。正式模板更新或 RuleOps 写入仍留后期合同。 | ## 4. WAPC1 审批配置准备清单 @@ -96,7 +96,8 @@ WPROC1 将 WAPC2 之后的审批路线固定为: 门禁要求: -- `WAPC2F` 只允许创建 OA-POC 前缀低风险测试模板和测试审批单;默认 dry-run,live 写入必须同时具备 CLI 写入参数和环境开关。 +- `WAPC2F` 已新增 `npm run check:wecom-approval-fixtures`,公网 dry-run 报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 为 `PASS_FIXTURE_DRY_RUN_READY`;它只证明 OA-POC 请假/补卡最小通用模板和低风险审批申请 payload 可构建、非自审预检通过。 +- `WAPC2F` 只允许创建 OA-POC 前缀低风险测试模板和测试审批单;默认 dry-run 不获取写入 token、不调用企微写接口,live 写入必须同时具备 CLI 写入参数、`WECOM_APPROVAL_FIXTURE_WRITE=1` 和人工批准。 - `WAPC2F` 固定 `WECOM_AGENT_SECRET`,不得 fallback 到通讯录、打卡或历史 HR assistant Secret。 - `WAPC2F` 默认申请人 `SMOKE002`、审批人 `SMOKE101`;命中自审、同一企微 userid、不可见、未绑定或 inactive 时硬阻断。 - `WCBK1` 必须先证明真实回调 URL 验证、验签、解密、幂等键和重复投递,不保存 Token、EncodingAESKey 或原始密文。 diff --git a/docs/企微优先开发路线图.md b/docs/企微优先开发路线图.md index dc40b200..e58d75f5 100644 --- a/docs/企微优先开发路线图.md +++ b/docs/企微优先开发路线图.md @@ -2,10 +2,10 @@ **文档状态**:当前开发路线图事实源。 **分类**:开发治理 / 路线图。 -**适用时间**:WDRG / WCP1 / WHMSLIVE / WOUA / WAPI / WRRA / WOUA-FIX / WRV1 / WRA1 / WMSG1 / WAPC1 / WAPC2 / WPROC1 / WPROC2 之后。 -**关联 Issue**:`WDRG,WCP1,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2`。 +**适用时间**:WDRG / WCP1 / WHMSLIVE / WOUA / WAPI / WRRA / WOUA-FIX / WRV1 / WRA1 / WMSG1 / WAPC1 / WAPC2 / WPROC1 / WPROC2 / WAPC2F 之后。 +**关联 Issue**:`WDRG,WCP1,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F`。 **最近审计**:2026-05-19。 -**不得误读**:本文只控制后续开发节奏、阶段顺序和门禁。WHMSLIVE 后只代表企微 HR 主数据镜像已部署并受控验证;WAPI 后进一步固定企微 API/Secret 口径并转为运行优先;WRRA 后明确 `PASS_WITH_FINDINGS` 只代表只读对账成功且存在差异,不代表企微考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps 已完成运行时实现。WMSG1 只证明企业微信应用消息可达和 Deeplink 可回到 OA;WAPC1 只固化审批配置准备清单;WAPC2 只新增请假/补卡模板详情和低风险 `sp_no` 审批详情只读 POC,不代表真实回调或镜像能力已完成。WPROC1 后,WAPC2 不直接放行深度镜像,必须先完成自动样件、回调、补偿拉取和镜像数据模型设计门禁。WPROC2 只新增可执行 process gate,不实现 WAPC2F 业务能力。 +**不得误读**:本文只控制后续开发节奏、阶段顺序和门禁。WHMSLIVE 后只代表企微 HR 主数据镜像已部署并受控验证;WAPI 后进一步固定企微 API/Secret 口径并转为运行优先;WRRA 后明确 `PASS_WITH_FINDINGS` 只代表只读对账成功且存在差异,不代表企微考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps 已完成运行时实现。WMSG1 只证明企业微信应用消息可达和 Deeplink 可回到 OA;WAPC1 只固化审批配置准备清单;WAPC2 只新增请假/补卡模板详情和低风险 `sp_no` 审批详情只读 POC,不代表真实回调或镜像能力已完成。WPROC1 后,WAPC2 不直接放行深度镜像,必须先完成自动样件、回调、补偿拉取和镜像数据模型设计门禁。WPROC2 只新增可执行 process gate。WAPC2F 当前只证明 OA-POC 请假/补卡样件 dry-run payload、非自审预检和公网 dry-run 可用;未真实创建企微模板或审批单,未自动复跑真实 WAPC2,仍不放行深度镜像。 ## 1. 路线图原则 @@ -37,6 +37,7 @@ | `WAPC2` | 已部署并公网只读复跑 | 新增 QA-only `npm run check:wecom-approval-poc`,范围先锁定请假和补卡;固定 `WECOM_AGENT_SECRET` 调用 `POST /cgi-bin/oa/gettemplatedetail` 与 `POST /cgi-bin/oa/getapprovaldetail`,报告只输出脱敏模板 ID/`sp_no` 前缀、字段/节点计数和错误分类。公网报告 `/app/artifacts/qa-flow/wecom-approval-poc-20260519T020429Z/summary.json` 为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`:Secret 可用,但缺请假/补卡模板 ID 和两条低风险 `sp_no`,未伪造成 PASS。 | | `WPROC1` | 路线与流程门禁已修正 | 固化三层门禁、Definition of Ready、Definition of Done、自动化优先和非自审规则。后续路线调整为 `WAPC2 -> WAPC2F 自动生成 POC 样件 -> WAPC2 自动复跑 -> WCBK1 回调只读 POC -> WACP3 补偿拉取 POC -> WADM1 镜像数据模型设计 -> wecom_attendance_approval_mirror_v1`。WAPC2F 默认测试链为 `SMOKE002 -> SMOKE101`,若 `SMOKE101` 不可用,不自动换人,先问答确认。 | | `WPROC2` | 流程自动化门禁已新增 | 新增 `scripts/qa/check-process-contract.mjs`,把 WPROC1 的三层门禁、DoR/DoD、自动化优先、非自审、Secret 固定、live 写入开关、缺配置分类和脱敏边界升级为可执行 process gate;WAPC2F 实施前必须先通过 baseline gate 和 `wecom-approval-fixture` profile。 | +| `WAPC2F` | 已实现并公网 dry-run 通过 | 新增 QA-only `npm run check:wecom-approval-fixtures`,固定 `WECOM_AGENT_SECRET` 构建 OA-POC 请假/补卡最小通用模板和低风险审批申请 payload;默认 dry-run 不获取写入 token、不调用企微写接口。公网报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 为 `PASS_FIXTURE_DRY_RUN_READY`:`SMOKE002 -> SMOKE101` active、已绑定、非自审,live 写入未批准且未执行。 | ## 3. 阶段路线 @@ -46,8 +47,8 @@ | 2 | `wecom_hr_master_sync_v1` | 企微/人事助手员工、部门、主管、职位、联系方式和在职状态镜像到 OA。 | POC 证明人事助手/通讯录读取和回调能力可用,或明确缺口处理方式。 | WHMSLIVE 后已完成本地门禁、部署和受控公网验证;OA 可自动创建/更新员工档案、账号、角色、企微绑定和审计;最终 `force_off`,真实员工批量 apply 仍需单独批准;缺配置时记录 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 | | 3 | `runtime_first_wecom_enablement` | 先把已可用企微能力用于系统运行:入口、免登/绑定、脱敏消息、deeplink、通讯录 userid 清单、人事助手读取、打卡读取和只读对账。 | WAPI API 目录和 Secret 矩阵完成;WOUA 按新读取策略复跑。 | WOUA-FIX 后 OA active 缺企微已清零;WRV1/WRA1 通过 `check:wecom-runtime-ready` 给出运行优先结论;WMSG1 已证明真实消息和 Deeplink 可用。`PASS_WITH_CONFIG_GAPS` 只允许继续入口、免登、消息和只读读取主链路,不放行深度镜像;审批模板、测试 `sp_no`、真实回调仍阻断 `wecom_attendance_approval_mirror_v1`。 | | 4 | `WAPC1 / WAPC2` | 审批配置准备和请假/补卡模板详情、低风险审批详情只读 POC。 | 运行优先门禁完成;WMSG1 消息和 Deeplink 已确认。 | WAPC2 runner 已部署,缺模板 ID 或 `sp_no` 时真实记录 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`,不放行深度镜像。 | -| 5 | `WAPC2F 自动生成 POC 样件` | 尽量用 QA-only runner 自动创建 OA-POC 请假/补卡测试模板和低风险测试审批单。 | WAPC2 已证明 Secret 可用但缺模板或 `sp_no`;WPROC2 baseline gate 和 WAPC2F profile gate 通过;用户批准低风险企微测试样件写入。 | 默认 dry-run;live 写入必须显式开关;申请人 `SMOKE002`、审批人 `SMOKE101` 通过 active、绑定、可见和非自审预检;缺权限或不支持时记录 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 | -| 6 | `WAPC2 自动复跑` | 使用 WAPC2F 生成的脱敏模板 ID 和 `sp_no` 自动复跑只读 POC。 | WAPC2F 生成全部或部分样件。 | 请假/补卡模板详情与审批详情可读为 `PASS_APPROVAL_POC`;部分缺项为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`。 | +| 5 | `WAPC2F 自动生成 POC 样件` | 尽量用 QA-only runner 自动创建 OA-POC 请假/补卡测试模板和低风险测试审批单。 | WAPC2 已证明 Secret 可用但缺模板或 `sp_no`;WPROC2 baseline gate 和 WAPC2F profile gate 通过;dry-run 先通过;live 写入需用户另行批准。 | 当前已完成 dry-run:`PASS_FIXTURE_DRY_RUN_READY`。下一步若批准 live,必须显式 `--live --create-templates --submit-test-approvals` 且单次进程设置 `WECOM_APPROVAL_FIXTURE_WRITE=1`;申请人 `SMOKE002`、审批人 `SMOKE101` 通过 active、绑定、可见和非自审预检;缺权限或不支持时记录 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 | +| 6 | `WAPC2 自动复跑` | 使用 WAPC2F 生成的脱敏模板 ID 和 `sp_no` 自动复跑只读 POC。 | WAPC2F live 写入生成全部或部分样件。 | 请假/补卡模板详情与审批详情可读为 `PASS_APPROVAL_POC`;部分缺项为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`。当前 dry-run 未生成真实模板 ID 或 `sp_no`,因此未执行真实 WAPC2 自动复跑。 | | 7 | `WCBK1 回调只读 POC` | 证明真实回调 URL 验证、验签、解密、幂等键和重复投递处理。 | 回调 URL、Token、EncodingAESKey 由部署环境配置;不得写入仓库或聊天。 | 真实回调可达、可验签/解密、重复投递幂等、失败重试语义明确;缺配置记录 `NEEDS_CONFIG`。 | | 8 | `WACP3 补偿拉取 POC` | 证明按时间窗口批量获取审批单号并可拉取审批详情,作为回调丢失补偿。 | WAPC2 或 WAPC2F 至少能提供低风险样例;官方 API 和权限明确。 | 能按窗口拿到审批单号并拉详情;不能伪造成 PASS,权限或可信 IP 问题记录 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 | | 9 | `WADM1 镜像数据模型设计` | 先设计镜像字段、状态机、幂等键、索引、隐私保留和回滚方案。 | WAPC2/WAPC2F、WCBK1、WACP3 结论可支撑模型设计。 | 数据模型、状态流、幂等、索引、脱敏、审计和迁移回滚方案经 Issue CSV 固化后,才允许实现镜像。 | @@ -213,10 +214,10 @@ WAPC2 承接 WAPC1 的配置准备清单,但范围先收窄到请假和补卡 - 报告输出到 `artifacts/qa-flow/wecom-approval-poc-/summary.json`,只记录类型、配置状态、脱敏 ID 前缀、字段类型计数、审批节点计数和错误分类。 - 若缺模板 ID 或低风险测试 `sp_no`,结论必须为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`;若权限、可信 IP、可见范围、Secret 或企微 API 错误阻断,结论必须为 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 -WAPC2 通过后,下一步不是直接进入 `wecom_attendance_approval_mirror_v1`。WPROC1 固定后续必须先做: +WAPC2 通过后,下一步不是直接进入 `wecom_attendance_approval_mirror_v1`。WPROC1/WAPC2F 固定后续必须先做: -1. `WAPC2F 自动生成 POC 样件`:默认申请人 `SMOKE002`、审批人 `SMOKE101`,两者必须 active、已绑定企微、可见、不是同一 OA 员工且不是同一企微 userid;若 `SMOKE101` 不可用,不自动换人,先问答确认。 -2. `WAPC2 自动复跑`:使用自动生成的脱敏模板 ID 和 `sp_no` 复跑只读 POC。 +1. `WAPC2F 自动生成 POC 样件`:dry-run 已通过;默认申请人 `SMOKE002`、审批人 `SMOKE101`,两者必须 active、已绑定企微、可见、不是同一 OA 员工且不是同一企微 userid;live 创建模板/审批单仍需另行问答批准。 +2. `WAPC2 自动复跑`:只有 live 生成真实模板 ID 和 `sp_no` 后,才使用脱敏模板 ID 和 `sp_no` 复跑只读 POC。 3. `WCBK1 回调只读 POC`:证明 URL 验证、验签、解密、幂等和重复投递。 4. `WACP3 补偿拉取 POC`:证明按时间窗口拿到审批单号并可拉取审批详情。 5. `WADM1 镜像数据模型设计`:先设计字段、状态机、幂等键、索引和隐私保留边界。 diff --git a/docs/文档事实源状态矩阵.md b/docs/文档事实源状态矩阵.md index 5df6932d..7a8ec0fc 100644 --- a/docs/文档事实源状态矩阵.md +++ b/docs/文档事实源状态矩阵.md @@ -80,6 +80,7 @@ | `WAPC2` | `issues/2026-05-19_09-34-16-wecom-approval-template-spno-poc-v1.csv` | `CURRENT` | 企微审批模板与低风险 `sp_no` 只读 POC 已部署并公网复跑:新增 `check:wecom-approval-poc` QA-only runner 和只读集成服务,范围请假/补卡,固定 `WECOM_AGENT_SECRET` 调用 `gettemplatedetail`/`getapprovaldetail`;远端报告 `/app/artifacts/qa-flow/wecom-approval-poc-20260519T020429Z/summary.json` 为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`,缺模板或 `sp_no` 不放行深度镜像 | | `WPROC1` | `issues/2026-05-19_10-43-50-wecom-process-route-engineering-v1.csv` | `CURRENT` | 企微路线与开发流程工程化修正:docs-only/contract-only 固化三层门禁、Definition of Ready、Definition of Done、自动化优先、非自审和高风险 POC 前置;修正 WAPC2F 默认链为 `SMOKE002 -> SMOKE101`,WAPC2 后不得直接进入深度镜像 | | `WPROC2` | `issues/2026-05-19_11-15-10-process-gate-automation-v1.csv` | `CURRENT` | 开发流程自动化门禁:新增 `scripts/qa/check-process-contract.mjs` process gate、WAPC2F profile 和最小门禁接入;WAPC2F 实施前必须先通过 baseline gate,不部署、不写 OA/企微数据 | +| `WAPC2F` | `issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv` | `CURRENT` | 企微审批 POC 样件自动生成 dry-run 已实现并部署验证:新增 `check:wecom-approval-fixtures` QA-only runner,固定 `WECOM_AGENT_SECRET` 构建 OA-POC 请假/补卡最小通用模板和低风险审批申请 payload;远端报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 为 `PASS_FIXTURE_DRY_RUN_READY`;未真实创建模板或审批单,live 写入仍需单独批准 | 旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。 @@ -87,28 +88,28 @@ | 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,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2` | `rg` | none | 2026-05-19 | `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,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2` | `audit-docs` | none | 2026-05-19 | `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,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | `rg` | none | 2026-05-19 | `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,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | `audit-docs` | none | 2026-05-19 | `audit-docs --check-registry` | medium | 判断文档权威性 | | `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WAPC1` | Issue CSV | `docs/requirements-specification.md` | 2026-05-19 | `rg` | high | WAPC1 后定义 HR 主数据镜像读取口径、运行优先、WOUA 差异收口、消息实发和审批配置准备边界;ERP 未来不接入 OA | | `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WAPC1` | `rg` | none | 2026-05-19 | `rg` | high | 记录已拍板产品边界、企微/OA 设计门禁、开发路线图、WCPC 准入结论、WHMS 部署验证、WAPI 运行优先、WRRA 差异收口和 WAPC1 审批配置准备口径 | | `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9,WFDG` | Playwright/视觉审查/DG8/DG9/WFDG | none | 2026-05-18 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型;WFDG 后区分 OA 主办、企微镜像和 OA 应急接管 | -| `ROADMAP-WECOM` | `docs/企微优先开发路线图.md` | 开发治理 | 企微优先开发路线图事实源 | `CURRENT` | product/dev | `WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2` | Issue CSV | none | 2026-05-19 | `rg` | high | WPROC2 后记录企微 HR/考勤路线阶段顺序、API/Secret 口径、运行优先、消息实发、审批配置、只读 POC、process gate、WAPC2F 自动样件、WCBK1 回调、WACP3 补偿拉取、WADM1 模型设计和禁止跳阶段 | -| `WECOM-API-CATALOG` | `docs/企微API能力目录.md` | 集成事实源 | 企微 API 能力目录和 Secret 矩阵事实源 | `CURRENT` | product/backend/qa | `WAPI,WRRA,WAPC1,WAPC2,WPROC1,WPROC2` | 官方企业微信开发者中心链接/Issue CSV | none | 2026-05-19 | `rg` | high | 企微开发先读;只保存摘要、链接和项目决策,不保存 Secret、token、完整 userid、手机号、邮箱或原始 payload;WPROC2 后高风险企微合同先跑 process gate | +| `ROADMAP-WECOM` | `docs/企微优先开发路线图.md` | 开发治理 | 企微优先开发路线图事实源 | `CURRENT` | product/dev | `WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | Issue CSV | none | 2026-05-19 | `rg` | high | WAPC2F 后记录企微 HR/考勤路线阶段顺序、API/Secret 口径、运行优先、消息实发、审批配置、只读 POC、process gate、WAPC2F dry-run 通过、WCBK1 回调、WACP3 补偿拉取、WADM1 模型设计和禁止跳阶段 | +| `WECOM-API-CATALOG` | `docs/企微API能力目录.md` | 集成事实源 | 企微 API 能力目录和 Secret 矩阵事实源 | `CURRENT` | product/backend/qa | `WAPI,WRRA,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | 官方企业微信开发者中心链接/Issue CSV | none | 2026-05-19 | `rg` | high | 企微开发先读;只保存摘要、链接和项目决策,不保存 Secret、token、完整 userid、手机号、邮箱或原始 payload;WAPC2F 后记录 create_template/applyevent dry-run ready 和 live 双开关边界 | | `DELIVERY` | `docs/开发交付与变更管理规约.md` | 开发治理 | 开发交付事实源 | `CURRENT` | dev/qa | `GOV8,WDRG,WRRA,WAPC1,WPROC1,WPROC2` | `rg` | none | 2026-05-19 | `rg` | high | Issue、review、commit、交付规则;WPROC2 后采用路线门禁、能力门禁、实现门禁、DoR/DoD、自动化优先和可执行 process gate | | `QA-GATE` | `docs/测试规约与回归门禁.md` | 测试验收 | 测试和回归门禁事实源 | `CURRENT` | qa | `QAF8,GOV8,WPROC1,WPROC2` | 回归脚本/Playwright | none | 2026-05-19 | `rg` | high | 测试计划、DONE 门禁、企微三层门禁、自动化优先、非自审测试要求和 check-process-contract | | `AGENT-PRACTICE` | `docs/Agent开发交付实践.md` | Agent 治理 | Agent 协作事实源 | `CURRENT` | dev | `GOV8,DG8` | Skill 校验 | none | 2026-05-13 | `rg` | medium | Agent 协作开发实践和设计主参考层级 | | `AGENTS` | `AGENTS.md` | 执行硬约束 | 项目硬规则 | `CURRENT` | lead | `GOV8` | 人工核对 | none | 2026-05-13 | `rg` | high | 不可违反约束 | -| `SKILL-HYB` | `.agents/skills/hyb-oa-delivery/SKILL.md` | Skill | 项目交付 Skill | `CURRENT` | dev | `GOV8,DG8,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1` | quick_validate | none | 2026-05-19 | `quick_validate.py` | medium | 每轮交付操作入口;企微/OA 后续先读 API 能力目录、Secret 矩阵、路线图和 WPROC1 状态,不得跳过 WAPC2F/WCBK1/WACP3/WADM1 直接做考勤审批镜像 | -| `REQ-WF` | `docs/需求-统一审批中心规约.md` | 需求事实源 | 审批模块规约 | `CURRENT` | product/backend | `AP6,WF8,WFOC,DBWFOC,WFDG,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1` | workflow 回归 | none | 2026-05-19 | `rg` | high | OA 主办审批、企微镜像、WRV1/WRA1/WMSG1/WAPC1/WAPC2/WPROC1 运行优先、审批配置、只读 POC、自动样件、回调/补偿/模型设计前置和 OA 应急接管边界 | +| `SKILL-HYB` | `.agents/skills/hyb-oa-delivery/SKILL.md` | Skill | 项目交付 Skill | `CURRENT` | dev | `GOV8,DG8,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | quick_validate | none | 2026-05-19 | `quick_validate.py` | medium | 每轮交付操作入口;企微/OA 后续先读 API 能力目录、Secret 矩阵、路线图和 WAPC2F dry-run 状态,不得跳过 live 样件、WCBK1/WACP3/WADM1 直接做考勤审批镜像 | +| `REQ-WF` | `docs/需求-统一审批中心规约.md` | 需求事实源 | 审批模块规约 | `CURRENT` | product/backend | `AP6,WF8,WFOC,DBWFOC,WFDG,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | workflow 回归 | none | 2026-05-19 | `rg` | high | OA 主办审批、企微镜像、WRV1/WRA1/WMSG1/WAPC1/WAPC2/WPROC1/WPROC2/WAPC2F 运行优先、审批配置、只读 POC、dry-run 样件、回调/补偿/模型设计前置和 OA 应急接管边界 | | `REQ-OCR` | `docs/需求-报销发票OCR规约.md` | 需求事实源 | OCR 模块规约 | `CURRENT` | product/backend | `RI6,OCR8,BIZ8` | OCR 回归 | none | 2026-05-13 | `rg` | high | OCR 和发票边界 | | `RULE-REIMB` | `docs/报销发票附件规则.md` | 需求事实源 | 报销发票附件规则 | `CURRENT` | product/finance | `RI6,BIZ8` | 报销回归 | none | 2026-05-13 | `rg` | high | 报销、发票、附件细则 | -| `REQ-WECOM` | `docs/需求-企业微信基础设施规约.md` | 需求事实源 | 企业微信边界规约 | `CURRENT` | product/backend | `R7E,ORG8,WF8,ATTV1,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1` | live smoke/真机反馈/Issue CSV | none | 2026-05-19 | `rg` | high | 企微优先承担 HR/考勤员工日常审批和人事/打卡能力;WPROC1 固定运行优先、审批配置、只读 POC、自动样件、回调/补偿/模型设计前置;采购、合同、项目、报销和财务继续 OA 主办 | +| `REQ-WECOM` | `docs/需求-企业微信基础设施规约.md` | 需求事实源 | 企业微信边界规约 | `CURRENT` | product/backend | `R7E,ORG8,WF8,ATTV1,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | live smoke/真机反馈/Issue CSV | none | 2026-05-19 | `rg` | high | 企微优先承担 HR/考勤员工日常审批和人事/打卡能力;WAPC2F 固定 dry-run 样件与 live 写入批准边界;采购、合同、项目、报销和财务继续 OA 主办 | | `RUNBOOK-TRIAL` | `docs/试运行与自动化验证手册.md` | 操作手册 | 试运行验证手册 | `CURRENT` | qa/ops | `R7E,QAOPS8` | live readiness | none | 2026-05-13 | `rg` | medium | 本地、公网、企业微信和真实 OCR 验证 | -| `MANUAL-ADMIN` | `docs/管理员手册.md` | 操作手册 | 管理员手册 | `CURRENT` | ops/admin | `ORG8,R7E,WFOC,DBWFOC,WFDG,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1` | 人工验收 | none | 2026-05-19 | `rg` | medium | 管理员上线、企微优先边界、Secret 矩阵、OA 主办数据边界、HR 主数据镜像、运行门禁、消息实发、审批配置、只读 POC、WPROC1 自动化优先和非自审链路 | +| `MANUAL-ADMIN` | `docs/管理员手册.md` | 操作手册 | 管理员手册 | `CURRENT` | ops/admin | `ORG8,R7E,WFOC,DBWFOC,WFDG,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F` | 人工验收 | none | 2026-05-19 | `rg` | medium | 管理员上线、企微优先边界、Secret 矩阵、OA 主办数据边界、HR 主数据镜像、运行门禁、消息实发、审批配置、只读 POC、WAPC2F dry-run 和非自审链路 | | `MANUAL-EMPLOYEE` | `docs/员工快速上手.md` | 操作手册 | 员工手册 | `CURRENT` | product | `R7E,WFOC,DBWFOC,WFDG` | 人工验收 | none | 2026-05-18 | `rg` | low | 员工侧使用说明,HR/考勤优先企业微信,采购/合同/财务仍在 OA;区分企微镜像和 OA 应急接管 | | `R7-DRAFT` | `docs/trialrun/R7扩大试运行草案.md` | 草案 | R7 准备材料 | `DRAFT` | product/qa | `R7E` | R7EP 报告 | none | 2026-05-13 | `rg` | medium | 不代表当前 R7E 执行状态 | | `OPS-RUNBOOK` | `docs/trialrun/OPS7运维交接Runbook.md` | 运维草案 | OPS 准备材料 | `DRAFT` | ops | `OPS8,QAOPS8` | 只读检查 | none | 2026-05-13 | `rg` | medium | 不代表恢复演练已执行 | -| `TRIAL-REPORT` | `docs/trialrun/人工试运行报告.md` | 证据材料 | 试运行证据入口 | `EVIDENCE` | qa | `R7EP,OPS8,QAF8,WOUA,WAPI,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2` | artifacts/qa-flow | none | 2026-05-19 | `rg` | medium | 只证明发生过;记录 WOUA-FIX 受控写入、WRV1/WRA1 运行优先门禁、WMSG1 消息实发真机确认、WAPC1 配置准备清单和 WAPC2 请假/补卡只读 POC,不直接放行深度镜像 | +| `TRIAL-REPORT` | `docs/trialrun/人工试运行报告.md` | 证据材料 | 试运行证据入口 | `EVIDENCE` | qa | `R7EP,OPS8,QAF8,WOUA,WAPI,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WAPC2F` | artifacts/qa-flow | none | 2026-05-19 | `rg` | medium | 只证明发生过;记录 WOUA-FIX 受控写入、WRV1/WRA1 运行优先门禁、WMSG1 消息实发真机确认、WAPC1 配置准备清单、WAPC2 只读 POC 和 WAPC2F dry-run,不直接放行深度镜像 | | `FBA-AUDIT` | `docs/trialrun/R7E后功能边界审计.md` | 证据材料 | R7E 后功能边界审计 | `EVIDENCE` | product/dev/qa | `FBA,R7E,QAOPS8,P2T` | Issue CSV | none | 2026-05-15 | `rg` | medium | 只作为功能边界、P2 候选和 Issue 草案证据,不直接派工 | | `HISTORY-MATRIX` | `docs/历史功能状态矩阵.md` | 历史材料 | 历史功能索引 | `HISTORICAL` | product | `DCL7` | git 历史 | none | 2026-05-13 | `rg` | low | 只追溯历史 | | `OLD-IMPL-PLAN` | `docs/implementation_plan.md` | 已移除历史 | 早期实施计划 | `REMOVED` | product | `DCL7` | git history | none | 2026-05-13 | manual | low | 从 git 历史恢复 | @@ -197,9 +198,10 @@ - `ME6-3` 代表性企业微信真机门禁已关闭。 - MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。 -- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核、ATTV1 考勤统一入口、AUTO-REMAINING-CLOSEOUT 剩余自动化收口、HRWD-CONTACT-LIVE-TEST 公网企微通讯录真实写入闭环、WFOC 需求重构、DBWFOC 数据边界修正、WFDG 设计门禁、WDRG 路线图门禁、WCPC 配置复跑、WHMSLIVE、WAPI、WRRA、WOUA-FIX、WRA1、WMSG1、WAPC1、WAPC2、WPROC1 和 WPROC2 均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现的公网 `payment_batch_v1=force_off` 漂移已由 AUTOREM 恢复为 `force_on`,正式自动巡检结论为 `GO_TO_OPERATE`;`erp_sync=force_off`,ERP 未来不接入 OA,正式支付接口、图片 OCR 和银行流水导入继续不进入当前路线。WCP1 已完成企业微信能力 QA-only 只读 POC,历史结论为 `PAUSE_FOR_CONFIG`;WCPC 使用既有在职测试号 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。WHMS 已实现并部署验证 `wecom_hr_master_sync_v1`、同步批次/逐员工记录、dry-run/apply、管理员 API、People 管理入口和 QA-only runner;默认和最终均为 `force_off`,公网 `SMOKE002` 因缺手机号、邮箱和部门按 `missing_required_field` 跳过,未执行生产真实员工批量 apply。WAPI 已将企微官方 API 能力目录、Secret 矩阵和运行优先路线落为当前事实源:通讯录读取使用 `WECOM_CONTACT_SECRET` 和 `user/list_id`,成员详情只作可选富化,人事助手/花名册、打卡、消息、OAuth 和审批读取默认使用 `WECOM_AGENT_SECRET`,`WECOM_CHECKIN_SECRET` 只作专用打卡 override,`WECOM_HR_ASSISTANT_SECRET` 不参与默认读取;后续企微能力不清楚时先查官方 API 和 [企微 API 能力目录](./企微API能力目录.md),不得靠运行时多个 Secret 试错。WOUA-FIX 已按用户确认执行受控写入并复检:7 个 OA active 缺企微测试账号软归档,`T0000` seed 复活问题修正,`SMOKE201` 测试主管和待办副作用已转交,最终 WOUA 报告 `/app/artifacts/qa-flow/wecom-oa-user-consistency-20260518T161716Z/summary.json` 为 `PASS_WITH_FINDINGS`,但 OA active 5、企微 active 5、匹配 5、OA active 缺企微 0;剩余 5 个企微缺字段先接受不补,`HRWDTEST-20260517214548` 归档绑定保留审计。WRV1 原始报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T173402Z/summary.json` 曾因同步开关误阻断;WRA1 已修正并公网复跑,报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T180506Z/summary.json` 为 `PASS_WITH_CONFIG_GAPS`,入口/OAuth、Secret 矩阵、OA active 绑定、通讯录、人事助手、消息 dry-run 和打卡只读链路均通过,打卡记录 `readable_sync_disabled`。WMSG1 已证明企业微信应用消息和 Deeplink 可用;WAPC1 已固定首批审批模板(请假、补卡、外出、加班)、低风险 `sp_no`、真实回调配置项、验签/解密/幂等/重试和补偿拉取准入清单;WAPC2 已新增请假/补卡模板详情和审批详情只读 POC runner;WPROC1 已固定后续自动化优先、非自审和三层门禁,WPROC2 已将其升级为 `check-process-contract` process gate。HR/考勤员工日常审批优先复用企业微信,OA 目标是按域镜像、人工接管、恢复回写和 RuleOps;采购、费用、合同、项目、报销、付款、AP、代账交接、审计和高敏附件继续 OA 主办。考勤/审批镜像、审批回调、OA 应急接管、恢复回写和 RuleOps 尚未实现;审批模板 ID、审批单号 `sp_no`、真实回调配置、补偿拉取、镜像数据模型和 HR 写接口仍需后续独立合同处理;不得跳过 WAPC2F、WCBK1、WACP3、WADM1 直接进入 `wecom_attendance_approval_mirror_v1`、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1` 或 `wecom_ruleops_v1`。HRWD 已完成 OA 驱动企业微信通讯录同步,并通过 `HRWDTEST-20260517214548` 真实公网 create/update/disable 验证;测试成员按审计要求保留离职/禁用状态。 +- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核、ATTV1 考勤统一入口、AUTO-REMAINING-CLOSEOUT 剩余自动化收口、HRWD-CONTACT-LIVE-TEST 公网企微通讯录真实写入闭环、WFOC 需求重构、DBWFOC 数据边界修正、WFDG 设计门禁、WDRG 路线图门禁、WCPC 配置复跑、WHMSLIVE、WAPI、WRRA、WOUA-FIX、WRA1、WMSG1、WAPC1、WAPC2、WPROC1、WPROC2 和 WAPC2F dry-run 均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现的公网 `payment_batch_v1=force_off` 漂移已由 AUTOREM 恢复为 `force_on`,正式自动巡检结论为 `GO_TO_OPERATE`;`erp_sync=force_off`,ERP 未来不接入 OA,正式支付接口、图片 OCR 和银行流水导入继续不进入当前路线。WCP1 已完成企业微信能力 QA-only 只读 POC,历史结论为 `PAUSE_FOR_CONFIG`;WCPC 使用既有在职测试号 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。WHMS 已实现并部署验证 `wecom_hr_master_sync_v1`、同步批次/逐员工记录、dry-run/apply、管理员 API、People 管理入口和 QA-only runner;默认和最终均为 `force_off`,公网 `SMOKE002` 因缺手机号、邮箱和部门按 `missing_required_field` 跳过,未执行生产真实员工批量 apply。WAPI 已将企微官方 API 能力目录、Secret 矩阵和运行优先路线落为当前事实源:通讯录读取使用 `WECOM_CONTACT_SECRET` 和 `user/list_id`,成员详情只作可选富化,人事助手/花名册、打卡、消息、OAuth 和审批读取默认使用 `WECOM_AGENT_SECRET`,`WECOM_CHECKIN_SECRET` 只作专用打卡 override,`WECOM_HR_ASSISTANT_SECRET` 不参与默认读取;后续企微能力不清楚时先查官方 API 和 [企微 API 能力目录](./企微API能力目录.md),不得靠运行时多个 Secret 试错。WOUA-FIX 已按用户确认执行受控写入并复检:7 个 OA active 缺企微测试账号软归档,`T0000` seed 复活问题修正,`SMOKE201` 测试主管和待办副作用已转交,最终 WOUA 报告 `/app/artifacts/qa-flow/wecom-oa-user-consistency-20260518T161716Z/summary.json` 为 `PASS_WITH_FINDINGS`,但 OA active 5、企微 active 5、匹配 5、OA active 缺企微 0;剩余 5 个企微缺字段先接受不补,`HRWDTEST-20260517214548` 归档绑定保留审计。WRV1 原始报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T173402Z/summary.json` 曾因同步开关误阻断;WRA1 已修正并公网复跑,报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T180506Z/summary.json` 为 `PASS_WITH_CONFIG_GAPS`,入口/OAuth、Secret 矩阵、OA active 绑定、通讯录、人事助手、消息 dry-run 和打卡只读链路均通过,打卡记录 `readable_sync_disabled`。WMSG1 已证明企业微信应用消息和 Deeplink 可用;WAPC1 已固定首批审批模板(请假、补卡、外出、加班)、低风险 `sp_no`、真实回调配置项、验签/解密/幂等/重试和补偿拉取准入清单;WAPC2 已新增请假/补卡模板详情和审批详情只读 POC runner;WPROC1 已固定后续自动化优先、非自审和三层门禁,WPROC2 已将其升级为 `check-process-contract` process gate;WAPC2F 已新增审批样件 runner 并公网 dry-run 通过,未真实创建企微模板或审批单。HR/考勤员工日常审批优先复用企业微信,OA 目标是按域镜像、人工接管、恢复回写和 RuleOps;采购、费用、合同、项目、报销、付款、AP、代账交接、审计和高敏附件继续 OA 主办。考勤/审批镜像、审批回调、OA 应急接管、恢复回写和 RuleOps 尚未实现;审批模板 live 创建、测试审批单 `sp_no`、真实回调配置、补偿拉取、镜像数据模型和 HR 写接口仍需后续独立合同处理;不得跳过 WAPC2F live 样件、WCBK1、WACP3、WADM1 直接进入 `wecom_attendance_approval_mirror_v1`、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1` 或 `wecom_ruleops_v1`。HRWD 已完成 OA 驱动企业微信通讯录同步,并通过 `HRWDTEST-20260517214548` 真实公网 create/update/disable 验证;测试成员按审计要求保留离职/禁用状态。 - WMSG1 已完成企微应用消息实发与 Deeplink 真机验证:远端报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T234431Z/summary.json` 显示 `checks.message.status=OK`、`errcode=0`、`recipientCount=1`,用户确认收到“OA 运行就绪检查”并可点击进入 OA 通知页面;本证据只证明消息和 Deeplink 可用,不放行审批模板、测试 `sp_no`、真实回调或深度镜像。 - WAPC1 已完成企微审批配置准备清单:首批只准备请假、补卡、外出、加班模板;每个模板需准备模板 ID、适用范围、字段项、审批人规则、可见范围和自建应用权限;测试 `sp_no` 必须低风险且不得含采购、合同、报销、发票、银行回单或高敏附件;真实回调 Token/EncodingAESKey 不写入仓库或文档。本证据只代表配置准备口径已固化,不代表真实回调、审批镜像、应急接管、恢复回写或 RuleOps 已实现。 - WAPC2 已完成企微审批模板与低风险 `sp_no` 只读 POC 本地实现、部署和公网只读复跑:范围只包括请假和补卡,固定 `WECOM_AGENT_SECRET`,远端报告为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`;本证据只代表模板详情/审批详情只读 runner 已具备且远端 Secret 可用,不代表模板 ID、测试 `sp_no`、真实回调或深度镜像已配置完成。 - WPROC1 已修正企微路线与开发流程工程化门禁:后续默认自动化优先,人工只处理批准、密钥/回调配置、企微后台不可 API 化配置、真机确认和风险拍板;WAPC2F 默认使用 `SMOKE002 -> SMOKE101` 非自审链,`SMOKE101` 不可用时停止问答确认;`wecom_attendance_approval_mirror_v1` 前必须完成 WAPC2F、WAPC2 自动复跑、WCBK1、WACP3 和 WADM1。 - WPROC2 已新增开发流程自动化门禁:`node scripts/qa/check-process-contract.mjs --baseline` 检查核心事实源和 WAPC2F 合同,`wecom-approval-fixture` profile 检查非自审、Secret 固定、live 双开关、缺配置分类和脱敏边界;该门禁已接入 `scripts/check-minimal.sh`。 +- WAPC2F 已实现企微审批 POC 样件自动生成 dry-run:`npm run check:wecom-approval-fixtures` 已部署公网并返回 `PASS_FIXTURE_DRY_RUN_READY`,报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json`;当前未设置 `WECOM_APPROVAL_FIXTURE_WRITE=1`,未创建模板或审批单,下一步 live 写入必须再次问答确认。 diff --git a/docs/文档索引.md b/docs/文档索引.md index 7f81b302..68897f3a 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 -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1 -> AUTO-REMAINING-CLOSEOUT -> HR-WECOM-DIRECTORY-V1 -> HRWD-CONTACT-LIVE-TEST -> WCFB -> WFOC -> DBWFOC -> WFDG -> WDRG -> WCP1 -> WCPC -> WHMS -> WHMSLIVE -> WOUA -> WAPI -> WRRA -> WOUA-FIX -> WRV1 -> WRA1 -> WMSG1 -> WAPC1 -> WAPC2 -> WPROC1 -> WPROC2。 +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 -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1 -> AUTO-REMAINING-CLOSEOUT -> HR-WECOM-DIRECTORY-V1 -> HRWD-CONTACT-LIVE-TEST -> WCFB -> WFOC -> DBWFOC -> WFDG -> WDRG -> WCP1 -> WCPC -> WHMS -> WHMSLIVE -> WOUA -> WAPI -> WRRA -> WOUA-FIX -> WRV1 -> WRA1 -> WMSG1 -> WAPC1 -> WAPC2 -> WPROC1 -> WPROC2 -> WAPC2F dry-run。 - R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。 - ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。 @@ -73,6 +73,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M - WAPC2 是 WAPC1 后的请假/补卡审批只读 POC 合同:新增 `server/src/scripts/check-wecom-approval-poc.js` 和 `npm run check:wecom-approval-poc`,固定 `WECOM_AGENT_SECRET` 读取 `gettemplatedetail`/`getapprovaldetail`;缺模板 ID 或低风险 `sp_no` 时报告 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG`,不放行深度镜像。 - WPROC1 是 WAPC2 后的企微路线与开发流程工程化修正合同:只更新合同和事实源,不写业务代码、不部署、不写 OA/企微数据;固定后续顺序为 `WAPC2F -> WAPC2 自动复跑 -> WCBK1 -> WACP3 -> WADM1 -> wecom_attendance_approval_mirror_v1`,并修正 WAPC2F 默认非自审链为 `SMOKE002 -> SMOKE101`。 - WPROC2 是 WPROC1 后的开发流程自动化门禁合同:新增 `scripts/qa/check-process-contract.mjs` process gate,并接入 `scripts/check-minimal.sh`;WAPC2F 实施前必须先通过 baseline gate 和 `wecom-approval-fixture` profile。本轮不部署、不写 OA/企微数据、不实现 WAPC2F 业务 runner。 +- WAPC2F 是 WPROC2 后的企微审批 POC 样件自动生成 dry-run 合同:新增 `server/src/scripts/check-wecom-approval-fixtures.js` 和 `npm run check:wecom-approval-fixtures`,公网报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 为 `PASS_FIXTURE_DRY_RUN_READY`;未设置 `WECOM_APPROVAL_FIXTURE_WRITE=1`,未真实创建企微模板或审批单,live 写入需另行批准。 ## 当前活跃治理流 @@ -133,6 +134,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M | `WAPC2` | `issues/2026-05-19_09-34-16-wecom-approval-template-spno-poc-v1.csv` | 已部署并公网只读复跑:企微审批模板与低风险 `sp_no` 只读 POC | 新增 QA-only runner、只读集成服务和 focused 单测;范围请假+补卡;固定 `WECOM_AGENT_SECRET` 调用 `gettemplatedetail` / `getapprovaldetail`;远端报告 `/app/artifacts/qa-flow/wecom-approval-poc-20260519T020429Z/summary.json` 为 `PARTIAL_APPROVAL_POC_NEEDS_CONFIG` | 不新增公开 API/schema/UI;不接真实回调;不调用企微写接口;不写 OA/企微数据;不进入深度镜像 | | `WPROC1` | `issues/2026-05-19_10-43-50-wecom-process-route-engineering-v1.csv` | 当前路线与流程治理:企微工程化门禁修正 | 固化三层门禁、Definition of Ready、Definition of Done、自动化优先、非自审和高风险能力先 POC;修正 WAPC2F 默认链为 `SMOKE002 -> SMOKE101` | docs-only/contract-only;不改业务代码、不部署、不写 OA/企微数据;WAPC2 后仍需 WAPC2F、WCBK1、WACP3、WADM1 后才能进入深度镜像 | | `WPROC2` | `issues/2026-05-19_11-15-10-process-gate-automation-v1.csv` | 当前流程自动化门禁:process gate 框架 | 新增 `check-process-contract` QA-only CLI、WAPC2F profile、最小门禁接入和事实源回写;WAPC2F 实施前先跑 baseline gate | 不部署、不改业务 API/schema/UI、不写 OA/企微数据、不实现 WAPC2F runner | +| `WAPC2F` | `issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv` | 当前审批样件 dry-run:企微审批 POC 样件自动生成 runner 已部署并公网 dry-run 通过 | 新增 `check:wecom-approval-fixtures`,固定 `WECOM_AGENT_SECRET`,默认 dry-run 构建 OA-POC 请假/补卡模板和审批申请 payload;`SMOKE002 -> SMOKE101` 非自审预检通过;远端报告 `PASS_FIXTURE_DRY_RUN_READY` | 不新增公开 API/schema/UI;不真实创建模板或审批单;不接回调;不进入深度镜像;live 写入需另行批准 | 旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。 @@ -155,8 +157,8 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M | [海音达OA-MVP需求规格说明书](./海音达OA-MVP需求规格说明书.md) | PRD 主事实源;WAPC1/WAPC2/WPROC1/WPROC2 后包含企微审批配置、只读 POC、工程化前置和 process gate 口径 | | [文档事实源状态矩阵](./文档事实源状态矩阵.md) | 文档分类、删除恢复和冲突规则 | | [产品设计规则](./产品设计规则.md) | PC、移动、企业微信 WebView 设计事实源;DG8 后飞书工作台/飞书审批为唯一主 UI/布局参考;DG9 后按页面分层和样板选型 | -| [企微优先开发路线图](./企微优先开发路线图.md) | WDRG/WCP1/WCPC/WHMS/WHMSLIVE/WOUA/WAPI/WRRA/WOUA-FIX/WRV1/WRA1/WMSG1/WAPC1/WAPC2/WPROC1/WPROC2 后企微优先 HR/考勤改造的阶段顺序、准入/退出门禁、并行规则、禁止跳阶段、API/Secret 口径、运行优先、消息实发、审批配置、只读 POC、process gate、自动样件、回调、补偿拉取和模型设计事实源 | -| [企微 API 能力目录](./企微API能力目录.md) | WAPI/WAPC1/WAPC2/WPROC1/WPROC2 后企微官方 API、Secret 矩阵、权限/可信 IP/回调模板、读写边界、运行优先、审批配置、只读 POC、WAPC2F/WCBK1/WACP3/WADM1 决策事实源 | +| [企微优先开发路线图](./企微优先开发路线图.md) | WDRG/WCP1/WCPC/WHMS/WHMSLIVE/WOUA/WAPI/WRRA/WOUA-FIX/WRV1/WRA1/WMSG1/WAPC1/WAPC2/WPROC1/WPROC2/WAPC2F 后企微优先 HR/考勤改造的阶段顺序、准入/退出门禁、并行规则、禁止跳阶段、API/Secret 口径、运行优先、消息实发、审批配置、只读 POC、process gate、dry-run 自动样件、回调、补偿拉取和模型设计事实源 | +| [企微 API 能力目录](./企微API能力目录.md) | WAPI/WAPC1/WAPC2/WPROC1/WPROC2/WAPC2F 后企微官方 API、Secret 矩阵、权限/可信 IP/回调模板、读写边界、运行优先、审批配置、只读 POC、WAPC2F dry-run 和 live 写入边界、WCBK1/WACP3/WADM1 决策事实源 | | [产品口径](./产品口径.md) | 已拍板产品边界和业务规则;WAPC1/WAPC2/WPROC1/WPROC2 后包含审批配置、只读 POC、工程化前置阶段顺序和 process gate | | [需求 - 统一审批中心规约](./需求-统一审批中心规约.md) | OA 主办审批、企微主办 HR/考勤员工日常审批镜像、催办、超时提醒、审批效率 | | [报销发票附件规则](./报销发票附件规则.md) | 报销、发票、OCR、附件细则 | diff --git a/docs/管理员手册.md b/docs/管理员手册.md index e0b8ecaf..d4518b63 100644 --- a/docs/管理员手册.md +++ b/docs/管理员手册.md @@ -1,6 +1,6 @@ # hyb-oa 管理员手册(10 人以下团队版) -**当前状态(2026-05-19 / WPROC1)**:R7E、PRELAUNCH、CASHIER-VOUCHER-V1、LAUNCH-DEPLOY-FINANCE、FIN-PROD-ENABLE、ACCOUNTING-HANDOFF-V1、ATTV1、HR-WECOM-DIRECTORY-V1、HRWD-CONTACT-LIVE-TEST、DBWFOC、WFDG、WDRG、WCPC、WHMSLIVE、WAPI、WRRA、WOUA-FIX、WRV1、WRA1、WMSG1、WAPC1、WAPC2 和 WPROC1 均已收口;SYSTEM-COMPLETE-GATE 结论为 `GO_TO_USE`。WCP1 历史结论为 `PAUSE_FOR_CONFIG`,WCPC 已使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)复跑通过,结论为 `GO_TO_HR_MASTER_SYNC`。WHMS 已实现并部署验证 `wecom_hr_master_sync_v1`:企微/人事助手 HR 主数据可 dry-run/apply 到 OA 员工镜像、账号、角色、绑定和审计;默认和最终均为 `force_off`,公网 `SMOKE002` 因企微侧缺手机号、邮箱和部门按 `missing_required_field` 跳过,未对真实员工执行生产批量 apply。WAPI 后固定企微 API 能力目录和 Secret 矩阵:通讯录使用 `WECOM_CONTACT_SECRET`,人事助手/打卡/消息/OAuth/审批读取使用 `WECOM_AGENT_SECRET`,运行时代码不得靠多个 Secret 试错;WOUA-FIX 已按用户确认软归档 7 个缺企微测试账号,复检 OA active 5、企微 active 5、匹配 5、OA active 缺企微 0,剩余 5 个企微缺字段先接受不补,1 个归档绑定保留审计。WRV1 新增并部署 `npm run check:wecom-runtime-ready` 运行优先门禁;WRA1 已修正 QA-only 打卡只读门禁并公网复跑,报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T180506Z/summary.json` 为 `PASS_WITH_CONFIG_GAPS`;WMSG1 已完成真实企业微信应用消息和 Deeplink 真机确认;WAPC1 已固化审批配置准备清单;WAPC2 已新增请假/补卡模板详情和低风险 `sp_no` 只读 POC runner。WPROC1 明确 WAPC2 后必须先做 WAPC2F 自动样件、WAPC2 自动复跑、WCBK1 回调只读 POC、WACP3 补偿拉取 POC 和 WADM1 镜像数据模型设计,不能直接进入 `wecom_attendance_approval_mirror_v1`。采购、费用、合同、项目、报销、付款、AP、代账交接和高敏附件继续 OA 主办,企业微信只做脱敏提醒和 OA deeplink。公网 `payment_batch_v1=force_on`,`erp_sync=force_off`。公司当前无 ERP/独立财务系统,且 ERP 未来不接入 OA;财务闭环以 OA 内审批、台账、付款批次、银行对公付款回单和月度代账交接为准。 +**当前状态(2026-05-19 / WAPC2F)**:R7E、PRELAUNCH、CASHIER-VOUCHER-V1、LAUNCH-DEPLOY-FINANCE、FIN-PROD-ENABLE、ACCOUNTING-HANDOFF-V1、ATTV1、HR-WECOM-DIRECTORY-V1、HRWD-CONTACT-LIVE-TEST、DBWFOC、WFDG、WDRG、WCPC、WHMSLIVE、WAPI、WRRA、WOUA-FIX、WRV1、WRA1、WMSG1、WAPC1、WAPC2、WPROC1、WPROC2 和 WAPC2F dry-run 均已收口;SYSTEM-COMPLETE-GATE 结论为 `GO_TO_USE`。WCP1 历史结论为 `PAUSE_FOR_CONFIG`,WCPC 已使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)复跑通过,结论为 `GO_TO_HR_MASTER_SYNC`。WHMS 已实现并部署验证 `wecom_hr_master_sync_v1`:企微/人事助手 HR 主数据可 dry-run/apply 到 OA 员工镜像、账号、角色、绑定和审计;默认和最终均为 `force_off`,公网 `SMOKE002` 因企微侧缺手机号、邮箱和部门按 `missing_required_field` 跳过,未对真实员工执行生产批量 apply。WAPI 后固定企微 API 能力目录和 Secret 矩阵:通讯录使用 `WECOM_CONTACT_SECRET`,人事助手/打卡/消息/OAuth/审批读取使用 `WECOM_AGENT_SECRET`,运行时代码不得靠多个 Secret 试错;WOUA-FIX 已按用户确认软归档 7 个缺企微测试账号,复检 OA active 5、企微 active 5、匹配 5、OA active 缺企微 0,剩余 5 个企微缺字段先接受不补,1 个归档绑定保留审计。WRV1 新增并部署 `npm run check:wecom-runtime-ready` 运行优先门禁;WRA1 已修正 QA-only 打卡只读门禁并公网复跑,报告 `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T180506Z/summary.json` 为 `PASS_WITH_CONFIG_GAPS`;WMSG1 已完成真实企业微信应用消息和 Deeplink 真机确认;WAPC1 已固化审批配置准备清单;WAPC2 已新增请假/补卡模板详情和低风险 `sp_no` 只读 POC runner;WPROC2 已新增 process gate;WAPC2F 已新增审批样件 runner 并公网 dry-run 通过,报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 为 `PASS_FIXTURE_DRY_RUN_READY`,但未真实创建企微模板或审批单。WPROC1 明确 WAPC2 后必须先做 WAPC2F 自动样件、WAPC2 自动复跑、WCBK1 回调只读 POC、WACP3 补偿拉取 POC 和 WADM1 镜像数据模型设计,不能直接进入 `wecom_attendance_approval_mirror_v1`。采购、费用、合同、项目、报销、付款、AP、代账交接和高敏附件继续 OA 主办,企业微信只做脱敏提醒和 OA deeplink。公网 `payment_batch_v1=force_on`,`erp_sync=force_off`。公司当前无 ERP/独立财务系统,且 ERP 未来不接入 OA;财务闭环以 OA 内审批、台账、付款批次、银行对公付款回单和月度代账交接为准。 ## 1. 适用范围 - 适用于 `small_team_internal` 工作区模式。 @@ -239,12 +239,13 @@ WAPC1 审批配置准备清单: WAPC2 请假/补卡只读 POC 操作口径: -1. 优先用 WAPC2F runner 自动生成 OA-POC 请假/补卡测试模板和低风险测试审批单;如果企微 API 或权限不支持,再记录 `NEEDS_CONFIG` 让管理员手工处理。 +1. 优先用 WAPC2F runner 自动生成 OA-POC 请假/补卡测试模板和低风险测试审批单;当前 dry-run 已通过,真实创建模板/审批单前仍需管理员批准单次 live 写入。 2. WAPC2F 默认申请人为 `SMOKE002`,审批人为 `SMOKE101`;两者必须 active、已绑定企微、可见、不是同一 OA 员工、不是同一企微 userid。`SMOKE101` 不可用时不自动换人,先问答确认。 -3. 测试单 `sp_no` 不得包含采购、合同、报销、发票、银行回单、高敏附件或真实敏感业务资料。 -4. QA 在服务器容器内执行 `npm run check:wecom-approval-poc -- --live --format=json`,并通过 CLI 或临时 env 传入请假/补卡模板 ID 和 `sp_no`。 -5. 报告结论 `PASS_APPROVAL_POC` 只代表模板详情和审批详情只读链路可用;`PARTIAL_APPROVAL_POC_NEEDS_CONFIG` 代表缺模板 ID 或 `sp_no`;`SKIP/BLOCKED_BY_EXTERNAL_CONFIG` 代表 Secret、权限、可信 IP、可见范围或企微 API 阻断。 -6. 报告只看脱敏模板 ID/`sp_no` 前缀、字段类型计数、审批节点计数和错误分类;不得截图或转存完整 `sp_no`、表单值、附件、评论、Secret、token 或原始 payload。 +3. WAPC2F live 写入只能在单次命令同时包含 `--live --create-templates --submit-test-approvals` 且设置 `WECOM_APPROVAL_FIXTURE_WRITE=1` 时执行;默认 dry-run 不获取写入 token、不在企微后台留下 OA-POC 记录。 +4. 测试单 `sp_no` 不得包含采购、合同、报销、发票、银行回单、高敏附件或真实敏感业务资料。 +5. QA 在服务器容器内执行 `npm run check:wecom-approval-poc -- --live --format=json`,并通过 CLI 或临时 env 传入请假/补卡模板 ID 和 `sp_no`。 +6. 报告结论 `PASS_APPROVAL_POC` 只代表模板详情和审批详情只读链路可用;`PARTIAL_APPROVAL_POC_NEEDS_CONFIG` 代表缺模板 ID 或 `sp_no`;`SKIP/BLOCKED_BY_EXTERNAL_CONFIG` 代表 Secret、权限、可信 IP、可见范围或企微 API 阻断。 +7. 报告只看脱敏模板 ID/`sp_no` 前缀、字段类型计数、审批节点计数和错误分类;不得截图或转存完整 `sp_no`、表单值、附件、评论、Secret、token 或原始 payload。 ### 3.3 正式财务闭环与代账交接 diff --git a/docs/需求-企业微信基础设施规约.md b/docs/需求-企业微信基础设施规约.md index d3095364..c27fd798 100644 --- a/docs/需求-企业微信基础设施规约.md +++ b/docs/需求-企业微信基础设施规约.md @@ -6,7 +6,7 @@ **上级事实源**:[海音达OA-MVP需求规格说明书](./海音达OA-MVP需求规格说明书.md) **相关事实源**:[需求 - 统一审批中心规约](./需求-统一审批中心规约.md)、[企微 API 能力目录](./企微API能力目录.md) -本规约细化企业微信在 `hyb-oa` 中的基础设施边界、入口、免登/绑定、通讯录身份、消息通知、deeplink、打卡、人事助手、原生审批、OA 镜像、人工接管和恢复回写要求。WFOC 起产品方向调整为“企微优先 + OA 无痛接管”;DBWFOC 后进一步固定为:HR/考勤员工日常审批优先使用企业微信,OA 做按域镜像、备份接管、财务闭环和审计中台;采购、合同、项目、财务和高敏附件继续由 OA 主办。WAPI 后,所有企微相关开发必须先读取本规约和 [企微 API 能力目录](./企微API能力目录.md),确认官方 API、Secret、权限、可信 IP、回调/模板和读写边界,再拆 Issue CSV。WMSG1 已证明企业微信应用消息和 Deeplink 真机回流可用;WAPC1 后审批进入配置准备清单,WAPC2 后先做请假/补卡模板详情与低风险 `sp_no` 只读 POC,WPROC1 后还必须先做自动样件、回调、补偿拉取和镜像数据模型门禁,不是直接进入审批镜像实现。 +本规约细化企业微信在 `hyb-oa` 中的基础设施边界、入口、免登/绑定、通讯录身份、消息通知、deeplink、打卡、人事助手、原生审批、OA 镜像、人工接管和恢复回写要求。WFOC 起产品方向调整为“企微优先 + OA 无痛接管”;DBWFOC 后进一步固定为:HR/考勤员工日常审批优先使用企业微信,OA 做按域镜像、备份接管、财务闭环和审计中台;采购、合同、项目、财务和高敏附件继续由 OA 主办。WAPI 后,所有企微相关开发必须先读取本规约和 [企微 API 能力目录](./企微API能力目录.md),确认官方 API、Secret、权限、可信 IP、回调/模板和读写边界,再拆 Issue CSV。WMSG1 已证明企业微信应用消息和 Deeplink 真机回流可用;WAPC1 后审批进入配置准备清单,WAPC2 后先做请假/补卡模板详情与低风险 `sp_no` 只读 POC;WAPC2F 已实现 OA-POC 样件 dry-run 并公网验证 `PASS_FIXTURE_DRY_RUN_READY`,但未真实创建模板或审批单。WPROC1 后仍必须先做 live 样件批准、WAPC2 自动复跑、回调、补偿拉取和镜像数据模型门禁,不是直接进入审批镜像实现。 --- @@ -35,7 +35,7 @@ | 通讯录 | 成员创建、读取、更新、禁用或删除 | 近期已验证 OA 写企微;后续改为企微主源同步 OA,OA 保留修复和审计 | `CAN_REUSE_NOW`:HCLT create/update/disable、部门读取和 WCPC 既有在职测试号 `contact.member_read` 均已通过 | | 人事助手 | 员工字段配置、花名册读取、花名册更新 | 作为员工主数据源,驱动 OA 员工、账号、角色、绑定和状态镜像 | `LOCAL_IMPLEMENTED_FOR_READ_SYNC`:字段配置和既有在职测试号花名册读取已通过;WHMS 已本地实现只读同步到 OA;资料更新写接口仍需单独 POC | | 打卡 | 打卡记录、员工规则、日报、月报、排班、补卡、规则管理 | 企微主办规则和采集,OA 同步按域镜像和应急对账 | `CAN_REUSE_NOW_FOR_READ`:WCPC 短窗口打卡记录和规则读取通过;日报/月报/排班等细项留到考勤镜像合同继续验证 | -| 审批 | 模板详情、提交审批、审批状态回调、审批详情、模板创建/更新、假期余额 | HR/考勤员工日常审批企微主办,OA 同步镜像;OA 主办财务、采购、合同、项目和报销审批不迁移 | `POC_RUNNER_READY_NEEDS_CONFIG`:假期余额读取通过;WAPC1 已固化配置准备清单,WAPC2 已新增请假/补卡模板详情和审批详情只读 POC runner;模板 ID、低风险测试审批单号、WAPC2F 自动样件、真实回调、补偿拉取和镜像数据模型仍需在考勤/审批镜像前补齐 | +| 审批 | 模板详情、提交审批、审批状态回调、审批详情、模板创建/更新、假期余额 | HR/考勤员工日常审批企微主办,OA 同步镜像;OA 主办财务、采购、合同、项目和报销审批不迁移 | `WAPC2F_DRY_RUN_READY`:假期余额读取通过;WAPC1 已固化配置准备清单,WAPC2 已新增请假/补卡模板详情和审批详情只读 POC runner;WAPC2F 已新增样件 runner 并公网 dry-run 通过。真实模板/测试审批单 live 写入、WAPC2 自动复跑、真实回调、补偿拉取和镜像数据模型仍需在考勤/审批镜像前补齐 | | 工作台和消息 | 自建应用入口、应用消息、deeplink | 继续作为 OA 移动入口和通知渠道 | `CAN_REUSE_NOW`;WRV1 新增 `check:wecom-runtime-ready` 对入口、OAuth、脱敏消息 dry-run 和 deeplink 做运行就绪门禁 | | 财务/报销 | 企微不提供本项目需要的 PDF OCR、付款批次、银行回单和代账闭环 | OA 主办 | `KEEP_OA_PRIMARY` | | 真实支付、ERP、图片 OCR、银行流水导入 | 非本路线目标 | 不进入当前路线 | `NOT_IN_SCOPE` | diff --git a/docs/需求-统一审批中心规约.md b/docs/需求-统一审批中心规约.md index b11e5821..98c4ec10 100644 --- a/docs/需求-统一审批中心规约.md +++ b/docs/需求-统一审批中心规约.md @@ -400,13 +400,13 @@ WAPC2 是 WAPC1 后的首个审批只读 POC,范围只包括请假和补卡, ### 9.3 WAPC2F 自动生成 POC 样件 -WAPC2F 是 WAPC2 后的自动化优先补缺阶段,目标是尽量用 QA-only runner 自动创建 OA-POC 请假/补卡测试模板和低风险测试审批单,再自动复跑 WAPC2。它不是正式模板配置、不是真实业务审批,也不是镜像实现。 +WAPC2F 是 WAPC2 后的自动化优先补缺阶段,目标是尽量用 QA-only runner 自动创建 OA-POC 请假/补卡测试模板和低风险测试审批单,再自动复跑 WAPC2。它不是正式模板配置、不是真实业务审批,也不是镜像实现。当前已新增 `npm run check:wecom-approval-fixtures` 并完成公网 dry-run,报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json` 为 `PASS_FIXTURE_DRY_RUN_READY`;本次未真实创建企微模板或审批单。 - 默认申请人固定 `SMOKE002`,审批人固定 `SMOKE101`。 - 预检两者必须 active、已绑定企微、可见、不是同一 OA 员工、不是同一企微 userid。 - 自审硬阻断,不因 `admin`、`manager`、`ceo`、`finance` 或其他角色放开。 - 如果 `SMOKE101` 不可用,不自动换成 `HYD000007` 或其他人员;必须返回配置缺口并用问答方式确认新链路。 -- live 写入必须有显式 CLI 参数和 `WECOM_APPROVAL_FIXTURE_WRITE=1`,默认 dry-run。 +- live 写入必须有显式 CLI 参数 `--live --create-templates --submit-test-approvals` 和单次进程环境变量 `WECOM_APPROVAL_FIXTURE_WRITE=1`,默认 dry-run 不获取写入 token、不调用企微写接口。 - 报告只保存脱敏模板 ID/`sp_no` 前缀、计数、状态和错误分类,不保存完整 `sp_no`、表单值、附件、评论、完整 userid、Secret、token 或原始 payload。 ### 9.4 WCBK1 / WACP3 / WADM1 前置门禁 diff --git a/issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv b/issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv index 1264e227..ec40ca41 100644 --- a/issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv +++ b/issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv @@ -1,10 +1,10 @@ ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes -"WAPC2F-0","合同建档与边界冻结","建立 WAPC2F 企微审批 POC 样件自动生成合同;承接 WAPC2 当前缺模板 ID 和低风险 sp_no 的配置缺口,本轮目标是尽量自动创建专用测试模板和测试审批单。","1) CSV 校验通过;2) 范围限定 OA-POC 前缀请假/补卡测试模板和低风险测试审批单;3) 固定 WECOM_AGENT_SECRET;4) 默认 dry-run;5) 默认测试链为申请人 SMOKE002 -> 审批人 SMOKE101,禁止自审;6) 不修改正式模板、不接真实回调、不写 OA 业务数据、不进入 wecom_attendance_approval_mirror_v1。","受影响流程:WAPC2 缺配置 -> 自动生成 POC 样件 -> 复跑模板/审批详情只读 POC;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv;node scripts/qa/check-process-contract.mjs --issue issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv --profile wecom-approval-fixture --json。","python3|node|manual","TODO","TODO","TODO","issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv","[]","WPROC2 baseline gate 通过后才能实施;本合同允许规划自动写企微测试样件,但真实 live 写入仍需后续实施前确认;回滚路径:撤回本轮代码/文档,企微后台中 OA-POC 测试模板和测试单如已生成需人工隐藏/停用或保留审计。" -"WAPC2F-1","路线自动化优先修正","更新路线图和事实源,把 WAPC2 后续阶段调整为自动化优先:WAPC2F 自动生成样件 -> WAPC2 复跑 -> WCBK1 回调 POC -> WACP3 补偿拉取 POC -> WADM1 镜像数据模型 -> 镜像实现。","1) 文档明确后续能自动化的动作优先用 QA-only runner 执行;2) 人工只负责批准写入、配置不可自动化的后台项和确认风险;3) 不再默认要求手工创建模板或测试单;4) 不把自动生成 POC 样件误读为正式模板配置完成。","受影响流程:企微审批路线治理;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;rg -n 'WAPC2F|自动生成|OA-POC|WCBK1|WACP3|WADM1|wecom_attendance_approval_mirror_v1' docs .agents/skills/hyb-oa-delivery/SKILL.md。","node|rg","TODO","TODO","TODO","docs/企微优先开发路线图.md|docs/企微API能力目录.md|docs/需求-统一审批中心规约.md|docs/需求-企业微信基础设施规约.md|docs/管理员手册.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|.agents/skills/hyb-oa-delivery/SKILL.md","[""WAPC2F-0""]","文档只记录自动化策略和边界,不保存 Secret、Token、完整 template_id、完整 sp_no 或原始 payload。" -"WAPC2F-2","企微审批写接口调研与安全门禁","核对企业微信创建审批模板和提交审批申请官方 API 的实际字段、权限、可见范围、可信 IP、审批人规则和限制;固化 live 写入双重或三重确认。","1) create_template/applyevent 的方法、路径、Secret 和字段结构明确;2) 若官方接口不支持所需模板或测试单自动生成,改为 SKIP/BLOCKED_BY_EXTERNAL_CONFIG;3) live 写入必须同时指定 CLI 写入参数和 WECOM_APPROVAL_FIXTURE_WRITE=1;4) 不尝试其他 Secret。","受影响流程:自动样件生成前置安全审查;验证步骤:rg -n '创建审批模板|提交审批申请|applyevent|create_template|WECOM_APPROVAL_FIXTURE_WRITE' docs server/src/scripts server/tests。","rg|manual","TODO","TODO","TODO","docs/企微API能力目录.md|server/src/scripts/check-wecom-approval-fixtures.js|server/src/services/integrations/wecomApproval.js","[""WAPC2F-1""]","若官方文档或远端 API 返回能力不支持,后续只记录不可自动化项,不做绕路试错;live 写入前必须明确清理/隐藏/停用或保留审计方式。" -"WAPC2F-3","QA-only 样件生成 runner","新增 QA-only runner check-wecom-approval-fixtures,用于 dry-run/live 创建 OA-POC 请假/补卡测试模板、提交低风险测试审批单,并输出脱敏 summary.json。","1) package.json 增加 check:wecom-approval-fixtures;2) 默认 dry-run 只构建脱敏 payload;3) live 写入必须显式 --live --create-templates --submit-test-approvals 且 WECOM_APPROVAL_FIXTURE_WRITE=1;4) 支持指定申请人/审批人员工号,默认申请人 SMOKE002、审批人 SMOKE101;5) 预检两者 active、已绑定企微、可见、不是同一 OA 员工且不是同一企微 userid;6) 命中自审时硬阻断;7) 报告不输出完整模板 ID、完整 sp_no、完整 userid、手机号、邮箱、token、secret、表单值、附件、评论或原始 payload。","受影响流程:自动创建测试模板和测试审批单;验证步骤:node scripts/qa/check-process-contract.mjs --baseline --json;cd server && node --check src/scripts/check-wecom-approval-fixtures.js;cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js --reporter=verbose。","node|vitest","TODO","TODO","TODO","server/src/scripts/check-wecom-approval-fixtures.js|server/package.json","[""WAPC2F-2""]","WPROC2 baseline gate 通过后才能实施;runner 只用于 QA 样件生成;不新增公开业务 API,不改 Prisma schema,不改前端;如果 SMOKE101 不可用,不自动切换为 HYD000007 或其他审批人,返回配置缺口并问答确认;live 写入留下的 OA-POC 测试模板/测试单需按清理、隐藏、停用或保留审计方式处理。" -"WAPC2F-4","审批模板创建与测试审批提交集成","扩展企微审批集成服务,固定 WECOM_AGENT_SECRET 调用创建审批模板和提交审批申请接口,并复用 WAPC2 脱敏、错误分类和无 fallback 规则。","1) 创建模板只创建 OA-POC 前缀测试模板;2) 提交审批只提交低风险无附件测试单;3) 申请人/审批人命中同一 OA 员工或同一企微 userid 时返回自审阻断;4) API 错误、权限、可见范围、可信 IP 或模板能力限制均返回 BLOCKED_BY_EXTERNAL_CONFIG;5) 不修改或覆盖正式模板。","受影响流程:企微审批测试样件写入;验证步骤:node scripts/qa/check-process-contract.mjs --issue issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv --profile wecom-approval-fixture --json;cd server && node --check src/services/integrations/wecomApproval.js;focused vitest 覆盖固定 Secret、无 fallback、dry-run/live 分支、自审阻断和 API 错误分类。","node|vitest","TODO","TODO","TODO","server/src/services/integrations/wecomApproval.js","[""WAPC2F-3""]","真实写企微测试模板可能在企微后台留下记录;回滚/清理口径为后续隐藏、停用、人工清理或保留审计,报告和最终说明必须提醒管理员。" -"WAPC2F-5","自动复跑 WAPC2 只读 POC","样件生成成功后,runner 自动把生成的模板 ID 和 sp_no 传给现有 check:wecom-approval-poc,验证 gettemplatedetail/getapprovaldetail 只读链路。","1) 样件生成成功后自动复用 WAPC2 runner 或等价内部函数复跑;2) 目标结论 PASS_APPROVAL_POC;3) 若只生成部分样件则返回 PARTIAL_APPROVAL_POC_NEEDS_CONFIG;4) 若企微阻断则返回 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。","受影响流程:自动生成样件 -> 自动只读验证 -> 输出一份可审计结论;验证步骤:cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js tests/unit/scripts/check-wecom-approval-poc.test.js --reporter=verbose。","vitest|node","TODO","TODO","TODO","server/src/scripts/check-wecom-approval-fixtures.js|server/src/scripts/check-wecom-approval-poc.js","[""WAPC2F-4""]","自动复跑不得把生成的完整 ID 或 sp_no 写入报告;只允许脱敏前缀和计数。" -"WAPC2F-6","Focused 单测与隐私回归","新增 focused vitest 覆盖样件生成 runner、写入保护、无 Secret fallback、脱敏报告、自动复跑和阻断分类。","1) 覆盖 dry-run 默认不写;2) 覆盖缺 WECOM_APPROVAL_FIXTURE_WRITE 时 live 不写;3) 覆盖 WECOM_AGENT_SECRET 固定选择;4) 覆盖创建模板、提交测试单、自动复跑;5) 覆盖申请人/审批人相同时自审硬阻断;6) 覆盖 SMOKE101 不可用时不自动换人;7) 覆盖隐私字段不泄露。","受影响流程:WAPC2F 本地质量门禁;验证步骤:cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js tests/unit/scripts/check-wecom-approval-poc.test.js --reporter=verbose。","vitest","TODO","TODO","TODO","server/tests/unit/scripts/check-wecom-approval-fixtures.test.js|server/tests/unit/scripts/check-wecom-approval-poc.test.js","[""WAPC2F-5""]","单测使用 mock fetch,不访问公网,不写企微。" -"WAPC2F-7","本地门禁与公网受控验证","本地门禁通过后,以 SYNC_ENV=0 受控部署;远端先 dry-run,再在单次进程内设置 WECOM_APPROVAL_FIXTURE_WRITE=1 执行 live 样件生成和 WAPC2 自动复跑。","1) WPROC2 baseline gate、CSV、node --check、prisma validate、focused vitest、文档审计、skill 校验和 git diff --check 通过;2) SYNC_ENV=0 部署,不同步本地 env;3) live 写入只创建 OA-POC 测试样件,申请链固定 SMOKE002 -> SMOKE101;4) 公网 /ready 和 runtime smoke 通过;5) 报告结论真实记录 PASS/PARTIAL/SKIP。","受影响流程:公网自动化 POC 样件生成和只读验证;验证步骤:node scripts/qa/check-process-contract.mjs --baseline --json;python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv;cd server && npm run prisma:validate;cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js tests/unit/scripts/check-wecom-approval-poc.test.js --reporter=verbose;node scripts/qa/audit-docs.mjs --check-registry;SYNC_ENV=0 bash deploy/trialrun/deploy.sh;远端 npm run check:wecom-approval-fixtures。","python3|node|npm|vitest|bash|ssh|curl","TODO","TODO","TODO","N/A","[""WAPC2F-6""]","WPROC2 baseline gate 是实施前置;公网 live 写入前最终说明必须再次提示会在企微后台留下 OA-POC 测试模板/审批记录;不得修改正式模板;若 SMOKE101 不可用,停止并问答确认,不自动换审批人;清理方式为隐藏、停用、人工清理或保留审计。" -"WAPC2F-8","状态回填、中文提交与下一阶段问答","根据验证结果回填状态并中文提交;最终用问答方式让用户决定是否进入 WCBK1 回调只读 POC、WACP3 补偿拉取 POC 或暂停。","1) Issue 状态按 Test_Method 通过情况回填;2) 中文提交引用 WAPC2F;3) 最终说明包含当前状态、改了什么、测试了什么、结果是什么、剩余风险和下一步问答;4) 不直接进入回调或镜像实现。","受影响流程:WAPC2F 交付闭环和后续路线决策;验证步骤:git status --short;git log --oneline -1;最终问答列出 WCBK1/WACP3/WADM1 选择。","git|manual","TODO","TODO","TODO","issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv|N/A","[""WAPC2F-7""]","后续动作默认自动化优先;人工只处理写入批准、企微后台无法 API 化的配置和真机确认;本行不写企微数据,若前序 live 生成样件则按隐藏、停用、人工清理或保留审计处理。" +WAPC2F-0,合同建档与边界冻结,建立 WAPC2F 企微审批 POC 样件自动生成合同;承接 WAPC2 当前缺模板 ID 和低风险 sp_no 的配置缺口,本轮目标是实现自动样件生成能力并完成本地/公网 dry-run,不真实创建企微模板或审批单。,1) CSV 校验通过;2) 范围限定 OA-POC 前缀请假/补卡测试模板和低风险测试审批单;3) 固定 WECOM_AGENT_SECRET;4) 默认 dry-run;5) 默认测试链为申请人 SMOKE002 -> 审批人 SMOKE101,禁止自审;6) 不修改正式模板、不接真实回调、不写 OA 业务数据、不进入 wecom_attendance_approval_mirror_v1。,受影响流程:WAPC2 缺配置 -> 自动生成 POC 样件 -> 复跑模板/审批详情只读 POC;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv;node scripts/qa/check-process-contract.mjs --issue issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv --profile wecom-approval-fixture --json。,python3|node|manual,DONE,DONE,DONE,issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv,[],WPROC2 baseline gate 通过后才能实施;本合同已完成 dry-run 能力和公网验证;真实 live 写入仍需后续实施前确认;回滚路径:撤回本轮代码/文档,若后续 live 生成 OA-POC 测试模板和测试单需人工隐藏/停用或保留审计。 +WAPC2F-1,路线自动化优先修正,更新路线图和事实源,把 WAPC2 后续阶段调整为自动化优先:WAPC2F 自动生成样件 -> WAPC2 复跑 -> WCBK1 回调 POC -> WACP3 补偿拉取 POC -> WADM1 镜像数据模型 -> 镜像实现。,1) 文档明确后续能自动化的动作优先用 QA-only runner 执行;2) 人工只负责批准写入、配置不可自动化的后台项和确认风险;3) 不再默认要求手工创建模板或测试单;4) 不把自动生成 POC 样件误读为正式模板配置完成。,受影响流程:企微审批路线治理;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;rg -n 'WAPC2F|自动生成|OA-POC|WCBK1|WACP3|WADM1|wecom_attendance_approval_mirror_v1' docs .agents/skills/hyb-oa-delivery/SKILL.md。,node|rg,DONE,DONE,DONE,docs/企微优先开发路线图.md|docs/企微API能力目录.md|docs/需求-统一审批中心规约.md|docs/需求-企业微信基础设施规约.md|docs/管理员手册.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|.agents/skills/hyb-oa-delivery/SKILL.md,"[""WAPC2F-0""]",文档只记录自动化策略和边界,不保存 Secret、Token、完整 template_id、完整 sp_no 或原始 payload。 +WAPC2F-2,企微审批写接口调研与安全门禁,核对企业微信创建审批模板和提交审批申请官方 API 的实际字段、权限、可见范围、可信 IP、审批人规则和限制;固化 live 写入双重或三重确认。,1) create_template/applyevent 的方法、路径、Secret 和字段结构明确;2) 若官方接口不支持所需模板或测试单自动生成,改为 SKIP/BLOCKED_BY_EXTERNAL_CONFIG;3) live 写入必须同时指定 CLI 写入参数和 WECOM_APPROVAL_FIXTURE_WRITE=1;4) 不尝试其他 Secret。,受影响流程:自动样件生成前置安全审查;验证步骤:rg -n '创建审批模板|提交审批申请|applyevent|create_template|WECOM_APPROVAL_FIXTURE_WRITE' docs server/src/scripts server/tests。,rg|manual,DONE,DONE,DONE,docs/企微API能力目录.md|server/src/scripts/check-wecom-approval-fixtures.js|server/src/services/integrations/wecomApproval.js,"[""WAPC2F-1""]",若官方文档或远端 API 返回能力不支持,后续只记录不可自动化项,不做绕路试错;live 写入前必须明确清理/隐藏/停用或保留审计方式。 +WAPC2F-3,QA-only 样件生成 runner,新增 QA-only runner check-wecom-approval-fixtures,用于 dry-run/live 创建 OA-POC 请假/补卡测试模板、提交低风险测试审批单,并输出脱敏 summary.json。,1) package.json 增加 check:wecom-approval-fixtures;2) 默认 dry-run 只构建脱敏 payload;3) live 写入必须显式 --live --create-templates --submit-test-approvals 且 WECOM_APPROVAL_FIXTURE_WRITE=1;4) 支持指定申请人/审批人员工号,默认申请人 SMOKE002、审批人 SMOKE101;5) 预检两者 active、已绑定企微、可见、不是同一 OA 员工且不是同一企微 userid;6) 命中自审时硬阻断;7) 报告不输出完整模板 ID、完整 sp_no、完整 userid、手机号、邮箱、token、secret、表单值、附件、评论或原始 payload。,受影响流程:自动创建测试模板和测试审批单;验证步骤:node scripts/qa/check-process-contract.mjs --baseline --json;cd server && node --check src/scripts/check-wecom-approval-fixtures.js;cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js --reporter=verbose。,node|vitest,DONE,DONE,DONE,server/src/scripts/check-wecom-approval-fixtures.js|server/package.json,"[""WAPC2F-2""]",WPROC2 baseline gate 通过后才能实施;runner 只用于 QA 样件生成;不新增公开业务 API,不改 Prisma schema,不改前端;如果 SMOKE101 不可用,不自动切换为 HYD000007 或其他审批人,返回配置缺口并问答确认;live 写入留下的 OA-POC 测试模板/测试单需按清理、隐藏、停用或保留审计方式处理。 +WAPC2F-4,审批模板创建与测试审批提交集成,扩展企微审批集成服务,固定 WECOM_AGENT_SECRET 调用创建审批模板和提交审批申请接口,并复用 WAPC2 脱敏、错误分类和无 fallback 规则。,1) 创建模板只创建 OA-POC 前缀测试模板;2) 提交审批只提交低风险无附件测试单;3) 申请人/审批人命中同一 OA 员工或同一企微 userid 时返回自审阻断;4) API 错误、权限、可见范围、可信 IP 或模板能力限制均返回 BLOCKED_BY_EXTERNAL_CONFIG;5) 不修改或覆盖正式模板。,受影响流程:企微审批测试样件写入;验证步骤:node scripts/qa/check-process-contract.mjs --issue issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv --profile wecom-approval-fixture --json;cd server && node --check src/services/integrations/wecomApproval.js;focused vitest 覆盖固定 Secret、无 fallback、dry-run/live 分支、自审阻断和 API 错误分类。,node|vitest,DONE,DONE,DONE,server/src/services/integrations/wecomApproval.js,"[""WAPC2F-3""]",真实写企微测试模板可能在企微后台留下记录;回滚/清理口径为后续隐藏、停用、人工清理或保留审计,报告和最终说明必须提醒管理员。 +WAPC2F-5,自动复跑 WAPC2 只读 POC,样件生成成功后,runner 自动把生成的模板 ID 和 sp_no 传给现有 check:wecom-approval-poc,验证 gettemplatedetail/getapprovaldetail 只读链路。,1) live 样件生成成功后自动复用 WAPC2 runner 或等价内部函数复跑;2) 目标结论 PASS_APPROVAL_POC;3) 若只生成部分样件则返回 PARTIAL_APPROVAL_POC_NEEDS_CONFIG;4) 若企微阻断则返回 SKIP/BLOCKED_BY_EXTERNAL_CONFIG;5) dry-run 场景必须清晰记录为未生成真实模板 ID 或 sp_no、自动复跑跳过。,受影响流程:自动生成样件 -> 自动只读验证 -> 输出一份可审计结论;验证步骤:cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js tests/unit/scripts/check-wecom-approval-poc.test.js --reporter=verbose。,vitest|node,DONE,DONE,DONE,server/src/scripts/check-wecom-approval-fixtures.js|server/src/scripts/check-wecom-approval-poc.js,"[""WAPC2F-4""]",自动复跑不得把生成的完整 ID 或 sp_no 写入报告;只允许脱敏前缀和计数。本轮仅验证自动复跑参数传递和 dry-run 跳过语义,真实复跑等待 live 样件批准。 +WAPC2F-6,Focused 单测与隐私回归,新增 focused vitest 覆盖样件生成 runner、写入保护、无 Secret fallback、脱敏报告、自动复跑和阻断分类。,1) 覆盖 dry-run 默认不写;2) 覆盖缺 WECOM_APPROVAL_FIXTURE_WRITE 时 live 不写;3) 覆盖 WECOM_AGENT_SECRET 固定选择;4) 覆盖创建模板、提交测试单、自动复跑;5) 覆盖申请人/审批人相同时自审硬阻断;6) 覆盖 SMOKE101 不可用时不自动换人;7) 覆盖隐私字段不泄露。,受影响流程:WAPC2F 本地质量门禁;验证步骤:cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js tests/unit/scripts/check-wecom-approval-poc.test.js --reporter=verbose。,vitest,DONE,DONE,DONE,server/tests/unit/scripts/check-wecom-approval-fixtures.test.js|server/tests/unit/scripts/check-wecom-approval-poc.test.js,"[""WAPC2F-5""]",单测使用 mock fetch,不访问公网,不写企微。 +WAPC2F-7,本地门禁与公网 dry-run 验证,本地门禁通过后,以 SYNC_ENV=0 受控部署;远端只执行 dry-run,不设置 WECOM_APPROVAL_FIXTURE_WRITE=1,不创建企微模板或审批单。,1) WPROC2 baseline gate、CSV、node --check、prisma validate、focused vitest、文档审计、skill 校验、git diff --check 和最小门禁通过;2) SYNC_ENV=0 部署,不同步本地 env;3) 公网 /ready、runtime smoke 和 public bundle scan 通过;4) 远端 dry-run 报告为 PASS_FIXTURE_DRY_RUN_READY 或真实 PARTIAL/SKIP;5) live 写入未执行,后续需另行问答批准。,受影响流程:公网自动化 POC 样件 dry-run 和后续 live 准入;验证步骤:node scripts/qa/check-process-contract.mjs --baseline --json;python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv;cd server && npm run prisma:validate;cd server && npx vitest run tests/unit/scripts/check-wecom-approval-fixtures.test.js tests/unit/scripts/check-wecom-approval-poc.test.js --reporter=verbose;node scripts/qa/audit-docs.mjs --check-registry;SYNC_ENV=0 bash deploy/trialrun/deploy.sh;远端 npm run check:wecom-approval-fixtures -- --format=json。,python3|node|npm|vitest|bash|ssh|curl,DONE,DONE,DONE,N/A,"[""WAPC2F-6""]",WPROC2 baseline gate 是实施前置;本轮公网报告 /app/artifacts/qa-flow/wecom-approval-fixtures-20260519T045001Z/summary.json 为 PASS_FIXTURE_DRY_RUN_READY;未设置 WECOM_APPROVAL_FIXTURE_WRITE=1,未创建企微模板或审批单;后续 live 写入前必须再次提示会在企微后台留下 OA-POC 测试模板/审批记录,不得修改正式模板。 +WAPC2F-8,状态回填、中文提交与下一阶段问答,根据验证结果回填状态并中文提交;最终用问答方式让用户决定是否允许单次 live 创建 OA-POC 测试模板和低风险测试审批单,之后再进入 WAPC2 自动复跑、WCBK1、WACP3 或暂停。,1) Issue 状态按 dry-run 实际结果回填;2) 中文提交引用 WAPC2F;3) 最终说明包含当前状态、改了什么、测试了什么、结果是什么、剩余风险和下一步问答;4) 不直接进入回调或镜像实现;5) 下一步优先问答确认是否允许单次 live 创建 OA-POC 测试模板和低风险测试审批单。,受影响流程:WAPC2F 交付闭环和后续路线决策;验证步骤:git status --short;git log --oneline -1;最终问答列出 WCBK1/WACP3/WADM1 选择。,git|manual,DONE,DONE,DONE,issues/2026-05-19_10-24-43-wecom-approval-fixture-auto-generate-v1.csv|N/A,"[""WAPC2F-7""]",后续动作默认自动化优先;人工只处理写入批准、企微后台无法 API 化的配置和真机确认;本轮未写企微数据,未生成 live 样件;若后续批准 live 生成样件,再按隐藏、停用、人工清理或保留审计处理。 diff --git a/server/package.json b/server/package.json index f91bebd5..34af7df6 100644 --- a/server/package.json +++ b/server/package.json @@ -29,6 +29,7 @@ "audit:smoke-data": "node src/scripts/audit-smoke-data.js", "audit:org-master-data": "node src/scripts/audit-org-master-data.js", "audit:wecom-oa-users": "node src/scripts/audit-wecom-oa-user-consistency.js", + "check:wecom-approval-fixtures": "node src/scripts/check-wecom-approval-fixtures.js", "check:wecom-approval-poc": "node src/scripts/check-wecom-approval-poc.js", "check:wecom-runtime-ready": "node src/scripts/check-wecom-runtime-ready.js", "resolve:wecom-oa-users": "node src/scripts/resolve-wecom-oa-user-consistency.js", diff --git a/server/src/scripts/check-wecom-approval-fixtures.js b/server/src/scripts/check-wecom-approval-fixtures.js new file mode 100644 index 00000000..89a3f329 --- /dev/null +++ b/server/src/scripts/check-wecom-approval-fixtures.js @@ -0,0 +1,772 @@ +#!/usr/bin/env node +require("dotenv/config"); + +const fs = require("node:fs"); +const path = require("node:path"); +const { Pool } = require("pg"); + +const { getConfig } = require("../config"); +const { + APPROVAL_APPLY_EVENT_ENDPOINT, + APPROVAL_CREATE_TEMPLATE_ENDPOINT, + APPROVAL_POC_TARGETS, + fetchWecomApprovalApplyEvent, + fetchWecomApprovalTemplateCreate, + maskApprovalIdentifier, +} = require("../services/integrations/wecomApproval"); +const { executeApprovalPoc } = require("./check-wecom-approval-poc"); + +const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow"); +const DEFAULT_APPLICANT_EMPLOYEE_NO = "SMOKE002"; +const DEFAULT_APPROVER_EMPLOYEE_NO = "SMOKE101"; +const WRITE_CONFIRM_ENV = "WECOM_APPROVAL_FIXTURE_WRITE"; + +const REPORT_PRIVACY = + "approval fixture counts/masked userid/template_id/sp_no prefixes only; no phone/email/token/secret/full userid/full template_id/full sp_no/form values/attachments/comments/raw payload"; + +function printHelp() { + process.stdout.write(`Usage: node src/scripts/check-wecom-approval-fixtures.js [options] + +QA-only 企业微信审批 POC 样件自动生成 runner。 +默认 dry-run:只构建脱敏 payload 和报告,不获取写入 token,不调用企微写接口,不创建模板,不提交审批单。 + +Options: + --live 启用 live 写入候选路径。 + --create-templates live 模式下允许创建 OA-POC 测试模板。 + --submit-test-approvals live 模式下允许提交低风险测试审批单。 + --applicant-employee-no= 申请人员工号,默认 SMOKE002。 + --approver-employee-no= 审批人员工号,默认 SMOKE101。 + --format=text|json 输出格式,默认 text。 + --report-dir= 报告目录,默认 artifacts/qa-flow/wecom-approval-fixtures-。 + --help 显示帮助。 + +live 写入必须同时满足: + 1) --live --create-templates --submit-test-approvals + 2) ${WRITE_CONFIRM_ENV}=1 + +Secret 固定规则: + 创建模板/提交审批:WECOM_AGENT_SECRET。 + 不使用 WECOM_CONTACT_SECRET、WECOM_CHECKIN_SECRET 或 WECOM_HR_ASSISTANT_SECRET 兜底。 + +隐私边界: + 报告只输出员工号、计数、状态、脱敏 userid/template_id/sp_no 前缀和错误分类。 + 不输出手机号、邮箱、token、secret、完整 userid、完整 template_id、完整 sp_no、表单值、附件、评论或企微原始 payload。 +`); +} + +function timestamp() { + return new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); +} + +function toSafeString(value, maxLength = 500) { + if (value == null) return ""; + return String(value).trim().slice(0, maxLength); +} + +function safeMessage(value) { + return toSafeString(value, 500) + .replace(/access_token=[^&\s]+/gi, "access_token=***") + .replace(/[A-Za-z0-9_-]{24,}/g, "***") + .slice(0, 240); +} + +function parseArgs(argv) { + const options = { + live: false, + createTemplates: false, + submitTestApprovals: false, + applicantEmployeeNo: DEFAULT_APPLICANT_EMPLOYEE_NO, + approverEmployeeNo: DEFAULT_APPROVER_EMPLOYEE_NO, + format: "text", + reportDir: path.join(DEFAULT_REPORT_ROOT, `wecom-approval-fixtures-${timestamp()}`), + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--help" || arg === "-h") { + options.help = true; + } else if (arg === "--live") { + options.live = true; + } else if (arg === "--no-live") { + options.live = false; + } else if (arg === "--create-templates") { + options.createTemplates = true; + } else if (arg === "--submit-test-approvals") { + options.submitTestApprovals = true; + } else if (arg === "--applicant-employee-no") { + options.applicantEmployeeNo = toSafeString(argv[index + 1], 80); + index += 1; + } else if (arg.startsWith("--applicant-employee-no=")) { + options.applicantEmployeeNo = toSafeString(arg.slice("--applicant-employee-no=".length), 80); + } else if (arg === "--approver-employee-no") { + options.approverEmployeeNo = toSafeString(argv[index + 1], 80); + index += 1; + } else if (arg.startsWith("--approver-employee-no=")) { + options.approverEmployeeNo = toSafeString(arg.slice("--approver-employee-no=".length), 80); + } else if (arg === "--format") { + options.format = argv[index + 1] || options.format; + index += 1; + } else if (arg.startsWith("--format=")) { + options.format = arg.slice("--format=".length); + } else if (arg === "--report-dir") { + options.reportDir = argv[index + 1] || options.reportDir; + index += 1; + } else if (arg.startsWith("--report-dir=")) { + options.reportDir = arg.slice("--report-dir=".length); + } else { + throw new Error(`Unsupported argument: ${arg}`); + } + } + + if (!["text", "json"].includes(options.format)) { + throw new Error(`Unsupported --format: ${options.format}`); + } + if (!options.applicantEmployeeNo) { + throw new Error("--applicant-employee-no requires non-empty value"); + } + if (!options.approverEmployeeNo) { + throw new Error("--approver-employee-no requires non-empty value"); + } + return options; +} + +function localized(text) { + return [{ text, lang: "zh_CN" }]; +} + +function targetLabel(targetKey) { + return APPROVAL_POC_TARGETS.find((item) => item.key === targetKey)?.label || targetKey; +} + +function fixtureNumber(targetKey, generatedAt) { + return `WAPC2F-${targetKey}-${generatedAt.slice(0, 10).replace(/-/g, "")}`; +} + +function buildTemplateFixturePayload(target, generatedAt) { + const label = targetLabel(target.key); + const number = fixtureNumber(target.key, generatedAt); + return { + template_names: localized(`OA-POC-${label}-${generatedAt.slice(0, 10)}`), + template_content: { + controls: [ + { + control: "Text", + id: "fixture_type", + title: localized("类型"), + require: 1, + }, + { + control: "Date", + id: "fixture_date", + title: localized("日期"), + require: 1, + }, + { + control: "Textarea", + id: "fixture_reason", + title: localized("说明"), + require: 0, + }, + ], + }, + fixture_marker: { + issue: "WAPC2F", + number, + target: target.key, + lowRisk: true, + }, + }; +} + +function buildApplyFixturePayload({ target, templateId, applicantUserId, approverUserId, generatedAt }) { + const label = targetLabel(target.key); + const number = fixtureNumber(target.key, generatedAt); + return { + creator_userid: applicantUserId, + template_id: templateId, + use_template_approver: 0, + approver: [ + { + attr: 2, + userid: [approverUserId], + }, + ], + notifyer: [ + { + userid: [approverUserId], + }, + ], + notify_type: 1, + apply_data: { + contents: [ + { + control: "Text", + id: "fixture_type", + title: localized("类型"), + value: { text: label }, + }, + { + control: "Date", + id: "fixture_date", + title: localized("日期"), + value: { date: generatedAt.slice(0, 10) }, + }, + { + control: "Textarea", + id: "fixture_reason", + title: localized("说明"), + value: { text: `${number} low-risk dry-run fixture` }, + }, + ], + }, + summary_list: [ + { + summary_info: localized(`OA-POC ${label} ${number}`), + }, + ], + }; +} + +function summarizeFixturePayload(payload, kind) { + const controls = kind === "template" + ? payload?.template_content?.controls || [] + : payload?.apply_data?.contents || []; + const controlTypes = {}; + for (const control of controls) { + const key = toSafeString(control?.control || "unknown", 80) || "unknown"; + controlTypes[key] = (controlTypes[key] || 0) + 1; + } + return { + kind, + fieldCount: controls.length, + fieldTypeCounts: controlTypes, + hasApprover: kind === "approval" ? Boolean(payload?.approver?.length) : false, + hasNotifyer: kind === "approval" ? Boolean(payload?.notifyer?.length) : false, + hasApplicant: kind === "approval" ? Boolean(payload?.creator_userid) : false, + }; +} + +function buildSecretCheck(config) { + const missing = []; + if (!config.wecomCorpId) missing.push("WECOM_CORP_ID"); + if (!config.wecomAgentSecret) missing.push("WECOM_AGENT_SECRET"); + return { + id: "approval_fixture_secret_matrix", + source: "WECOM_AGENT_SECRET", + endpoints: [ + `POST ${APPROVAL_CREATE_TEMPLATE_ENDPOINT}`, + `POST ${APPROVAL_APPLY_EVENT_ENDPOINT}`, + ], + status: missing.length ? "NEEDS_CONFIG" : "OK", + missing, + noFallback: [ + "WECOM_CONTACT_SECRET", + "WECOM_CHECKIN_SECRET", + "WECOM_HR_ASSISTANT_SECRET", + ], + }; +} + +function isOaActive(row) { + return row?.status === "active" && !row?.archived; +} + +function sanitizeActor(row) { + if (!row) return null; + return { + employeeNo: row.employeeNo, + status: row.status, + archived: Boolean(row.archived), + hasWecomBinding: Boolean(row.wecomExternalUserId), + wecomUserIdPrefix: row.wecomExternalUserId ? maskApprovalIdentifier(row.wecomExternalUserId) : null, + }; +} + +async function queryApprovalActorRows(client, employeeNos) { + const result = await client.query(` + SELECT + e.id::text, + e.employee_no, + e.status::text, + (e.archived_at IS NOT NULL) AS archived, + w.external_user_id AS wecom_external_user_id + FROM employees e + LEFT JOIN wecom_identity_bindings w ON w.employee_id = e.id + WHERE e.employee_no = ANY($1::text[]) + ORDER BY e.employee_no ASC + `, [employeeNos]); + + return result.rows.map((row) => ({ + id: row.id, + employeeNo: toSafeString(row.employee_no, 80), + status: toSafeString(row.status, 40), + archived: Boolean(row.archived), + wecomExternalUserId: toSafeString(row.wecom_external_user_id, 160), + })); +} + +async function queryApprovalActorRowsReadOnly(employeeNos) { + if (!process.env.DATABASE_URL) { + throw new Error("Missing DATABASE_URL"); + } + const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + const client = await pool.connect(); + try { + await client.query("BEGIN READ ONLY"); + const rows = await queryApprovalActorRows(client, employeeNos); + await client.query("COMMIT"); + return rows; + } catch (error) { + try { + await client.query("ROLLBACK"); + } catch { + // Preserve the original read-only error. + } + throw error; + } finally { + client.release(); + await pool.end(); + } +} + +function buildActorPrecheck(rows, options) { + const byEmployeeNo = new Map((rows || []).map((row) => [row.employeeNo, row])); + const applicant = byEmployeeNo.get(options.applicantEmployeeNo) || null; + const approver = byEmployeeNo.get(options.approverEmployeeNo) || null; + const gaps = []; + + if (!applicant) gaps.push({ id: "applicant_missing", employeeNo: options.applicantEmployeeNo }); + if (!approver) gaps.push({ id: "approver_missing", employeeNo: options.approverEmployeeNo }); + if (applicant && !isOaActive(applicant)) gaps.push({ id: "applicant_inactive", employeeNo: applicant.employeeNo }); + if (approver && !isOaActive(approver)) gaps.push({ id: "approver_inactive", employeeNo: approver.employeeNo }); + if (applicant && !applicant.wecomExternalUserId) gaps.push({ id: "applicant_missing_wecom_binding", employeeNo: applicant.employeeNo }); + if (approver && !approver.wecomExternalUserId) gaps.push({ id: "approver_missing_wecom_binding", employeeNo: approver.employeeNo }); + if (applicant && approver && applicant.id === approver.id) { + gaps.push({ id: "self_approval_same_oa_employee", employeeNo: applicant.employeeNo }); + } + if (applicant?.wecomExternalUserId && approver?.wecomExternalUserId + && applicant.wecomExternalUserId === approver.wecomExternalUserId) { + gaps.push({ id: "self_approval_same_wecom_userid", employeeNo: applicant.employeeNo }); + } + + return { + id: "approval_fixture_actor_precheck", + status: gaps.length ? "NEEDS_CONFIG" : "OK", + applicant: sanitizeActor(applicant), + approver: sanitizeActor(approver), + applicantEmployeeNo: options.applicantEmployeeNo, + approverEmployeeNo: options.approverEmployeeNo, + visibilityCheck: "deferred_to_live_wecom_api", + gaps, + internalActors: { applicant, approver }, + }; +} + +function buildFixturePlans(actorCheck, generatedAt) { + const applicantUserId = actorCheck.internalActors.applicant?.wecomExternalUserId || ""; + const approverUserId = actorCheck.internalActors.approver?.wecomExternalUserId || ""; + return APPROVAL_POC_TARGETS.map((target) => { + const templatePayload = buildTemplateFixturePayload(target, generatedAt); + const applyPayload = buildApplyFixturePayload({ + target, + templateId: `DRY_RUN_TEMPLATE_ID_${target.key}`, + applicantUserId, + approverUserId, + generatedAt, + }); + return { + target: target.key, + label: target.label, + template: { + endpoint: `POST ${APPROVAL_CREATE_TEMPLATE_ENDPOINT}`, + status: "DRY_RUN_READY", + payloadSummary: summarizeFixturePayload(templatePayload, "template"), + payload: templatePayload, + }, + approval: { + endpoint: `POST ${APPROVAL_APPLY_EVENT_ENDPOINT}`, + status: "DRY_RUN_READY", + payloadSummary: summarizeFixturePayload(applyPayload, "approval"), + payload: applyPayload, + }, + }; + }); +} + +function liveWriteRequested(options) { + return Boolean(options.live && options.createTemplates && options.submitTestApprovals); +} + +function liveWriteAllowed(options, env) { + return liveWriteRequested(options) && env[WRITE_CONFIRM_ENV] === "1"; +} + +function summarizeWriteResult(result, prefixKey) { + return { + status: result?.ok ? "OK" : "BLOCKED", + reason: result?.ok ? null : result?.code || "wecom_approval_fixture_api_error", + message: result?.message ? safeMessage(result.message) : null, + source: result?.tokenSource || "WECOM_AGENT_SECRET", + response: result?.response || null, + [prefixKey]: prefixKey === "templateIdPrefix" + ? maskApprovalIdentifier(result?.templateId) + : maskApprovalIdentifier(result?.spNo), + }; +} + +async function executeLiveWrite({ + config, + actorCheck, + fixturePlans, + generatedAt, + createTemplateImpl, + applyEventImpl, +}) { + const createdTemplates = {}; + const submittedApprovals = {}; + const targetResults = []; + + for (const plan of fixturePlans) { + const createResult = await createTemplateImpl({ + config, + payload: plan.template.payload, + }); + const templateSummary = summarizeWriteResult(createResult, "templateIdPrefix"); + let approvalSummary = { + status: "SKIPPED", + reason: "template_create_not_ok", + spNoPrefix: null, + }; + + if (createResult.ok && createResult.templateId) { + createdTemplates[plan.target] = createResult.templateId; + const approvalPayload = buildApplyFixturePayload({ + target: { key: plan.target, label: plan.label }, + templateId: createResult.templateId, + applicantUserId: actorCheck.internalActors.applicant.wecomExternalUserId, + approverUserId: actorCheck.internalActors.approver.wecomExternalUserId, + generatedAt, + }); + const applyResult = await applyEventImpl({ + config, + payload: approvalPayload, + }); + approvalSummary = summarizeWriteResult(applyResult, "spNoPrefix"); + if (applyResult.ok && applyResult.spNo) { + submittedApprovals[plan.target] = applyResult.spNo; + } + } + + targetResults.push({ + target: plan.target, + label: plan.label, + template: templateSummary, + approval: approvalSummary, + }); + } + + return { + status: targetResults.every((item) => item.template.status === "OK" && item.approval.status === "OK") ? "OK" : "BLOCKED", + targets: targetResults, + createdTemplates, + submittedApprovals, + }; +} + +async function executeAutoRerun({ liveWrite, executeApprovalPocImpl, generatedAt }) { + const templateEntries = Object.entries(liveWrite?.createdTemplates || {}); + const spNoEntries = Object.entries(liveWrite?.submittedApprovals || {}); + if (!templateEntries.length || !spNoEntries.length) { + return { + status: "DRY_RUN_SKIPPED", + reason: "no_live_generated_template_or_sp_no", + wouldRun: "check:wecom-approval-poc", + }; + } + + const templates = Object.fromEntries(templateEntries); + const spNos = Object.fromEntries(spNoEntries); + const report = await executeApprovalPocImpl({ + live: true, + format: "json", + reportDir: "", + templates, + spNos, + help: false, + }, { + generatedAt, + }); + + return { + status: report.status === "PASS_APPROVAL_POC" ? "OK" : "BLOCKED", + conclusion: report.conclusion, + targetCount: templateEntries.length, + templateIdPrefixes: Object.fromEntries(templateEntries.map(([key, value]) => [key, maskApprovalIdentifier(value)])), + spNoPrefixes: Object.fromEntries(spNoEntries.map(([key, value]) => [key, maskApprovalIdentifier(value)])), + }; +} + +function collectConfigGaps({ secretCheck, actorCheck, options, env }) { + const gaps = []; + if (secretCheck.status !== "OK") { + gaps.push({ + id: secretCheck.id, + status: "NEEDS_CONFIG", + missingEnv: secretCheck.missing, + }); + } + for (const gap of actorCheck.gaps || []) { + gaps.push({ + id: gap.id, + status: "NEEDS_CONFIG", + employeeNo: gap.employeeNo, + }); + } + if (options.live && !liveWriteAllowed(options, env)) { + gaps.push({ + id: "approval_fixture_write_guard", + status: "NEEDS_CONFIG", + missing: [ + ...(!options.createTemplates ? ["--create-templates"] : []), + ...(!options.submitTestApprovals ? ["--submit-test-approvals"] : []), + ...(env[WRITE_CONFIRM_ENV] !== "1" ? [`${WRITE_CONFIRM_ENV}=1`] : []), + ], + }); + } + return gaps; +} + +function collectMainBlockers(liveWrite) { + if (!liveWrite || liveWrite.status !== "BLOCKED") return []; + const blockers = []; + for (const item of liveWrite.targets || []) { + for (const check of [item.template, item.approval]) { + if (check?.status === "BLOCKED") { + blockers.push({ + id: item.target, + reason: check.reason || "wecom_approval_fixture_api_error", + }); + } + } + } + return blockers; +} + +function sanitizeLiveWrite(liveWrite) { + const templateIds = liveWrite?.createdTemplates || {}; + const spNos = liveWrite?.submittedApprovals || {}; + return { + ...liveWrite, + createdTemplates: undefined, + submittedApprovals: undefined, + createdTemplatePrefixes: Object.fromEntries( + Object.entries(templateIds).map(([key, value]) => [key, maskApprovalIdentifier(value)]), + ), + submittedApprovalPrefixes: Object.fromEntries( + Object.entries(spNos).map(([key, value]) => [key, maskApprovalIdentifier(value)]), + ), + }; +} + +function evaluateFixtureConclusion({ configGaps, mainBlockers, liveWrite, writeAllowed }) { + if (mainBlockers.length) { + return { + status: "SKIP", + conclusion: "BLOCKED_BY_EXTERNAL_CONFIG", + }; + } + if (configGaps.length) { + return { + status: "PARTIAL_FIXTURE_NEEDS_CONFIG", + conclusion: "PARTIAL_FIXTURE_NEEDS_CONFIG", + }; + } + if (!writeAllowed) { + return { + status: "PASS_FIXTURE_DRY_RUN_READY", + conclusion: "PASS_FIXTURE_DRY_RUN_READY", + }; + } + if (liveWrite?.status === "OK") { + return { + status: "PASS_FIXTURE_CREATED", + conclusion: "PASS_FIXTURE_CREATED", + }; + } + return { + status: "PARTIAL_FIXTURE_NEEDS_CONFIG", + conclusion: "PARTIAL_FIXTURE_NEEDS_CONFIG", + }; +} + +async function executeApprovalFixtures(options, deps = {}) { + const env = deps.env || process.env; + const generatedAt = deps.generatedAt || new Date().toISOString(); + const config = deps.config || getConfig(); + const secretCheck = buildSecretCheck(config); + let actorRows = deps.actorRows; + let actorReadError = null; + if (!actorRows) { + try { + actorRows = await (deps.queryActorRowsReadOnlyImpl || queryApprovalActorRowsReadOnly)([ + options.applicantEmployeeNo, + options.approverEmployeeNo, + ]); + } catch (error) { + actorReadError = safeMessage(error?.message || error); + actorRows = []; + } + } + + const actorCheck = buildActorPrecheck(actorRows, options); + if (actorReadError) { + actorCheck.status = "NEEDS_CONFIG"; + actorCheck.gaps.push({ id: "database_read_unavailable", employeeNo: null, message: actorReadError }); + } + + const fixturePlans = buildFixturePlans(actorCheck, generatedAt); + const writeRequested = liveWriteRequested(options); + const writeAllowed = liveWriteAllowed(options, env); + let liveWrite = { + status: "DRY_RUN_READY", + requested: writeRequested, + allowed: writeAllowed, + guard: writeAllowed ? "enabled" : "dry_run_or_missing_double_confirm", + targets: [], + }; + let autoRerun = { + status: "DRY_RUN_SKIPPED", + reason: "no_live_write_in_this_run", + wouldRun: "check:wecom-approval-poc", + }; + + const configGapsBeforeWrite = collectConfigGaps({ secretCheck, actorCheck, options, env }); + if (writeAllowed && !configGapsBeforeWrite.length) { + liveWrite = await executeLiveWrite({ + config, + actorCheck, + fixturePlans, + generatedAt, + createTemplateImpl: deps.createTemplateImpl || fetchWecomApprovalTemplateCreate, + applyEventImpl: deps.applyEventImpl || fetchWecomApprovalApplyEvent, + }); + autoRerun = await executeAutoRerun({ + liveWrite, + executeApprovalPocImpl: deps.executeApprovalPocImpl || executeApprovalPoc, + generatedAt, + }); + } + + const configGaps = collectConfigGaps({ secretCheck, actorCheck, options, env }); + const mainBlockers = collectMainBlockers(liveWrite); + const conclusion = evaluateFixtureConclusion({ + configGaps, + mainBlockers, + liveWrite, + writeAllowed, + }); + + return { + mode: "qa_only", + generatedAt, + status: conclusion.status, + conclusion: conclusion.conclusion, + privacy: REPORT_PRIVACY, + scope: { + issue: "WAPC2F", + targets: APPROVAL_POC_TARGETS.map((item) => item.key), + applicantEmployeeNo: options.applicantEmployeeNo, + approverEmployeeNo: options.approverEmployeeNo, + templateDepth: "minimal_generic", + writeBehavior: writeAllowed + ? "live write enabled by CLI and env" + : "dry-run only; no WeCom write token, no template creation, no approval submission", + deepMirror: "not_started", + callback: "not_configured", + }, + checks: { + secretMatrix: secretCheck, + actors: { + ...actorCheck, + internalActors: undefined, + }, + fixturePayloads: fixturePlans.map((item) => ({ + target: item.target, + label: item.label, + template: { + endpoint: item.template.endpoint, + status: item.template.status, + payloadSummary: item.template.payloadSummary, + }, + approval: { + endpoint: item.approval.endpoint, + status: item.approval.status, + payloadSummary: item.approval.payloadSummary, + }, + })), + liveWrite: sanitizeLiveWrite(liveWrite), + autoRerun, + }, + configGaps, + mainBlockers, + }; +} + +function ensureReportDir(reportDir) { + fs.mkdirSync(reportDir, { recursive: true }); +} + +function writeReport(reportDir, report) { + ensureReportDir(reportDir); + const filePath = path.join(reportDir, "summary.json"); + fs.writeFileSync(filePath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); + return filePath; +} + +function printText(report, reportPath) { + process.stdout.write(`[wecom-approval-fixtures] status=${report.status} conclusion=${report.conclusion}\n`); + process.stdout.write(`[wecom-approval-fixtures] report=${reportPath}\n`); + process.stdout.write(`[wecom-approval-fixtures] privacy=${report.privacy}\n`); + process.stdout.write(`[wecom-approval-fixtures] writeBehavior=${report.scope.writeBehavior}\n`); + if (report.configGaps?.length) { + process.stdout.write(`[wecom-approval-fixtures] configGaps=${JSON.stringify(report.configGaps)}\n`); + } + if (report.mainBlockers?.length) { + process.stdout.write(`[wecom-approval-fixtures] mainBlockers=${JSON.stringify(report.mainBlockers)}\n`); + } +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + if (options.help) { + printHelp(); + return; + } + const report = await executeApprovalFixtures(options); + const reportPath = writeReport(options.reportDir, report); + if (options.format === "json") { + process.stdout.write(`${JSON.stringify({ ...report, reportPath }, null, 2)}\n`); + } else { + printText(report, reportPath); + } +} + +if (require.main === module) { + main().catch((error) => { + process.stderr.write(`[wecom-approval-fixtures][FAIL] ${safeMessage(error?.message || error)}\n`); + process.exitCode = 1; + }); +} + +module.exports = { + buildActorPrecheck, + buildApplyFixturePayload, + buildSecretCheck, + buildTemplateFixturePayload, + evaluateFixtureConclusion, + executeApprovalFixtures, + liveWriteAllowed, + parseArgs, + summarizeFixturePayload, +}; diff --git a/server/src/services/integrations/wecomApproval.js b/server/src/services/integrations/wecomApproval.js index 7b74242f..6522e5ee 100644 --- a/server/src/services/integrations/wecomApproval.js +++ b/server/src/services/integrations/wecomApproval.js @@ -1,6 +1,8 @@ const { getWecomAppAccessToken, sanitizeWecomResponse } = require("./wecomAccessToken"); const WECOM_API_BASE = "https://qyapi.weixin.qq.com"; +const APPROVAL_CREATE_TEMPLATE_ENDPOINT = "/cgi-bin/oa/approval/create_template"; +const APPROVAL_APPLY_EVENT_ENDPOINT = "/cgi-bin/oa/applyevent"; const APPROVAL_POC_TARGETS = Object.freeze([ { key: "leave", label: "请假" }, @@ -107,6 +109,12 @@ function summarizeApprovalPayload(payload) { function sanitizeApprovalApiResponse(payload) { if (!payload || typeof payload !== "object") return null; const result = sanitizeWecomResponse(payload) ?? {}; + if (payload.template_id || payload.templateId) { + result.templateIdPrefix = maskApprovalIdentifier(payload.template_id ?? payload.templateId); + } + if (payload.sp_no || payload.spNo) { + result.spNoPrefix = maskApprovalIdentifier(payload.sp_no ?? payload.spNo); + } if (payload.template_content) { const summary = summarizeTemplatePayload(payload); result.fieldCount = summary.fieldCount; @@ -333,10 +341,142 @@ async function fetchWecomApprovalDetail({ } } +function extractTemplateId(payload) { + return toSafeString(payload?.template_id ?? payload?.templateId, 160); +} + +function extractSpNo(payload) { + return toSafeString(payload?.sp_no ?? payload?.spNo, 160); +} + +async function fetchWecomApprovalTemplateCreate({ + config, + payload, + fetchImpl = fetch, + timeoutMs = config?.wecomRequestTimeoutMs ?? 10000, +}) { + try { + const token = await getApprovalAccessToken({ config, fetchImpl, timeoutMs }); + const result = await fetchWecomApprovalJson(APPROVAL_CREATE_TEMPLATE_ENDPOINT, token.accessToken, payload, { + fetchImpl, + timeoutMs, + }); + + if (!result.ok) { + const error = classifyWecomApprovalResult(result, "wecom_approval_create_template_failed"); + return { + ok: false, + skipped: false, + retryable: false, + code: error.code, + message: error.message, + tokenSource: token.tokenSource, + endpoint: `POST ${APPROVAL_CREATE_TEMPLATE_ENDPOINT}`, + response: error.response, + templateId: null, + summary: null, + }; + } + + const templateId = extractTemplateId(result.payload); + return { + ok: true, + skipped: false, + retryable: false, + code: "wecom_approval_template_created", + message: null, + tokenSource: token.tokenSource, + endpoint: `POST ${APPROVAL_CREATE_TEMPLATE_ENDPOINT}`, + response: sanitizeApprovalApiResponse(result.payload), + templateId, + summary: { + templateIdPrefix: maskApprovalIdentifier(templateId), + }, + }; + } catch (error) { + return { + ok: false, + skipped: false, + retryable: Boolean(error?.details?.retryable), + code: error?.code || "wecom_approval_create_template_failed", + message: sanitizeErrorMessage(error?.message || error), + tokenSource: "WECOM_AGENT_SECRET", + endpoint: `POST ${APPROVAL_CREATE_TEMPLATE_ENDPOINT}`, + response: error?.details?.response || null, + templateId: null, + summary: null, + }; + } +} + +async function fetchWecomApprovalApplyEvent({ + config, + payload, + fetchImpl = fetch, + timeoutMs = config?.wecomRequestTimeoutMs ?? 10000, +}) { + try { + const token = await getApprovalAccessToken({ config, fetchImpl, timeoutMs }); + const result = await fetchWecomApprovalJson(APPROVAL_APPLY_EVENT_ENDPOINT, token.accessToken, payload, { + fetchImpl, + timeoutMs, + }); + + if (!result.ok) { + const error = classifyWecomApprovalResult(result, "wecom_approval_apply_event_failed"); + return { + ok: false, + skipped: false, + retryable: false, + code: error.code, + message: error.message, + tokenSource: token.tokenSource, + endpoint: `POST ${APPROVAL_APPLY_EVENT_ENDPOINT}`, + response: error.response, + spNo: null, + summary: null, + }; + } + + const spNo = extractSpNo(result.payload); + return { + ok: true, + skipped: false, + retryable: false, + code: "wecom_approval_apply_event_submitted", + message: null, + tokenSource: token.tokenSource, + endpoint: `POST ${APPROVAL_APPLY_EVENT_ENDPOINT}`, + response: sanitizeApprovalApiResponse(result.payload), + spNo, + summary: { + spNoPrefix: maskApprovalIdentifier(spNo), + }, + }; + } catch (error) { + return { + ok: false, + skipped: false, + retryable: Boolean(error?.details?.retryable), + code: error?.code || "wecom_approval_apply_event_failed", + message: sanitizeErrorMessage(error?.message || error), + tokenSource: "WECOM_AGENT_SECRET", + endpoint: `POST ${APPROVAL_APPLY_EVENT_ENDPOINT}`, + response: error?.details?.response || null, + spNo: null, + summary: null, + }; + } +} + module.exports = { APPROVAL_POC_TARGETS, + APPROVAL_APPLY_EVENT_ENDPOINT, + APPROVAL_CREATE_TEMPLATE_ENDPOINT, WecomApprovalPocError, + fetchWecomApprovalApplyEvent, fetchWecomApprovalDetail, + fetchWecomApprovalTemplateCreate, fetchWecomApprovalTemplateDetail, maskApprovalIdentifier, sanitizeApprovalApiResponse, diff --git a/server/tests/unit/scripts/check-wecom-approval-fixtures.test.js b/server/tests/unit/scripts/check-wecom-approval-fixtures.test.js new file mode 100644 index 00000000..46e9ef79 --- /dev/null +++ b/server/tests/unit/scripts/check-wecom-approval-fixtures.test.js @@ -0,0 +1,318 @@ +const { + buildActorPrecheck, + buildApplyFixturePayload, + buildSecretCheck, + buildTemplateFixturePayload, + executeApprovalFixtures, + liveWriteAllowed, + parseArgs, +} = require("../../../src/scripts/check-wecom-approval-fixtures"); +const { + fetchWecomApprovalApplyEvent, + fetchWecomApprovalTemplateCreate, +} = require("../../../src/services/integrations/wecomApproval"); +const { clearWecomAppAccessTokenCache } = require("../../../src/services/integrations/wecomAccessToken"); + +const SENSITIVE_AGENT_CREDENTIAL = ["fixture", "agent", "credential", "should", "never", "leak"].join("-"); +const SENSITIVE_CONTACT_CREDENTIAL = ["fixture", "contact", "credential", "unused"].join("-"); +const SENSITIVE_CHECKIN_CREDENTIAL = ["fixture", "checkin", "credential", "unused"].join("-"); +const SENSITIVE_HR_CREDENTIAL = ["fixture", "hr", "credential", "unused"].join("-"); +const SENSITIVE_APPLICANT_USERID = ["fixture", "applicant", "userid", "full", "secret"].join("_"); +const SENSITIVE_APPROVER_USERID = ["fixture", "approver", "userid", "full", "secret"].join("_"); +const SENSITIVE_TEMPLATE_ID = ["tpl", "fixture", "full", "identifier", "must", "not", "leak"].join("-"); +const SENSITIVE_SP_NO = ["sp", "fixture", "full", "identifier", "must", "not", "leak"].join("-"); + +function baseConfig(overrides = {}) { + return { + wecomCorpId: "corp-fixture", + wecomAgentId: "1000002", + wecomAgentSecret: SENSITIVE_AGENT_CREDENTIAL, + wecomContactSecret: SENSITIVE_CONTACT_CREDENTIAL, + wecomCheckinSecret: SENSITIVE_CHECKIN_CREDENTIAL, + wecomHrAssistantSecret: SENSITIVE_HR_CREDENTIAL, + wecomRequestTimeoutMs: 1000, + ...overrides, + }; +} + +function baseOptions(overrides = {}) { + return { + live: false, + createTemplates: false, + submitTestApprovals: false, + applicantEmployeeNo: "SMOKE002", + approverEmployeeNo: "SMOKE101", + format: "json", + reportDir: "/tmp/wecom-approval-fixtures-test", + help: false, + ...overrides, + }; +} + +function actorRows(overrides = {}) { + return [ + { + id: "applicant-id", + employeeNo: "SMOKE002", + status: "active", + archived: false, + wecomExternalUserId: SENSITIVE_APPLICANT_USERID, + }, + { + id: "approver-id", + employeeNo: "SMOKE101", + status: "active", + archived: false, + wecomExternalUserId: SENSITIVE_APPROVER_USERID, + }, + ].map((row) => ({ ...row, ...(overrides[row.employeeNo] || {}) })); +} + +function jsonResponse(payload, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + json: async () => payload, + }; +} + +describe("check-wecom-approval-fixtures", () => { + beforeEach(() => { + clearWecomAppAccessTokenCache(); + }); + + it("parses defaults and live write flags", () => { + expect(parseArgs(["--format=json"])).toMatchObject({ + live: false, + applicantEmployeeNo: "SMOKE002", + approverEmployeeNo: "SMOKE101", + format: "json", + }); + const live = parseArgs([ + "--live", + "--create-templates", + "--submit-test-approvals", + "--applicant-employee-no=SMOKE002", + "--approver-employee-no", + "SMOKE101", + ]); + expect(live).toMatchObject({ + live: true, + createTemplates: true, + submitTestApprovals: true, + applicantEmployeeNo: "SMOKE002", + approverEmployeeNo: "SMOKE101", + }); + }); + + it("passes dry-run without calling WeCom write APIs", async () => { + const createTemplateImpl = vi.fn(); + const applyEventImpl = vi.fn(); + const report = await executeApprovalFixtures(baseOptions(), { + config: baseConfig(), + env: {}, + actorRows: actorRows(), + createTemplateImpl, + applyEventImpl, + generatedAt: "2026-05-19T10:00:00.000Z", + }); + + expect(report.status).toBe("PASS_FIXTURE_DRY_RUN_READY"); + expect(report.scope.writeBehavior).toContain("dry-run only"); + expect(createTemplateImpl).not.toHaveBeenCalled(); + expect(applyEventImpl).not.toHaveBeenCalled(); + expect(report.checks.fixturePayloads).toHaveLength(2); + }); + + it("does not write in live mode without the explicit env switch", async () => { + const createTemplateImpl = vi.fn(); + const report = await executeApprovalFixtures(baseOptions({ + live: true, + createTemplates: true, + submitTestApprovals: true, + }), { + config: baseConfig(), + env: {}, + actorRows: actorRows(), + createTemplateImpl, + generatedAt: "2026-05-19T10:00:00.000Z", + }); + + expect(liveWriteAllowed(baseOptions({ + live: true, + createTemplates: true, + submitTestApprovals: true, + }), {})).toBe(false); + expect(report.status).toBe("PARTIAL_FIXTURE_NEEDS_CONFIG"); + expect(report.configGaps).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: "approval_fixture_write_guard" }), + ])); + expect(createTemplateImpl).not.toHaveBeenCalled(); + }); + + it("blocks self-approval by OA employee or WeCom userid", () => { + const sameEmployee = buildActorPrecheck(actorRows({ + SMOKE101: { id: "applicant-id" }, + }), baseOptions()); + expect(sameEmployee.status).toBe("NEEDS_CONFIG"); + expect(sameEmployee.gaps.map((item) => item.id)).toContain("self_approval_same_oa_employee"); + + const sameWecom = buildActorPrecheck(actorRows({ + SMOKE101: { wecomExternalUserId: SENSITIVE_APPLICANT_USERID }, + }), baseOptions()); + expect(sameWecom.gaps.map((item) => item.id)).toContain("self_approval_same_wecom_userid"); + }); + + it("keeps SMOKE101 unavailable as a config gap and does not auto-replace", async () => { + const report = await executeApprovalFixtures(baseOptions(), { + config: baseConfig(), + env: {}, + actorRows: actorRows({ + SMOKE101: { status: "inactive" }, + }), + generatedAt: "2026-05-19T10:00:00.000Z", + }); + + expect(report.status).toBe("PARTIAL_FIXTURE_NEEDS_CONFIG"); + expect(report.configGaps).toEqual(expect.arrayContaining([ + expect.objectContaining({ + id: "approver_inactive", + employeeNo: "SMOKE101", + }), + ])); + expect(JSON.stringify(report)).not.toContain("HYD000007"); + }); + + it("uses WECOM_AGENT_SECRET for create_template and applyevent without fallback", async () => { + const calls = []; + const fetchImpl = vi.fn(async (url, options = {}) => { + calls.push({ + url: String(url), + body: options.body ? JSON.parse(options.body) : null, + }); + if (String(url).includes("/cgi-bin/gettoken")) { + expect(String(url)).toContain(`corpsecret=${encodeURIComponent(SENSITIVE_AGENT_CREDENTIAL)}`); + expect(String(url)).not.toContain(SENSITIVE_CONTACT_CREDENTIAL); + expect(String(url)).not.toContain(SENSITIVE_CHECKIN_CREDENTIAL); + expect(String(url)).not.toContain(SENSITIVE_HR_CREDENTIAL); + return jsonResponse({ errcode: 0, access_token: "fixture-token", expires_in: 7200 }); + } + if (String(url).includes("/cgi-bin/oa/approval/create_template")) { + return jsonResponse({ errcode: 0, errmsg: "ok", template_id: SENSITIVE_TEMPLATE_ID }); + } + if (String(url).includes("/cgi-bin/oa/applyevent")) { + return jsonResponse({ errcode: 0, errmsg: "ok", sp_no: SENSITIVE_SP_NO }); + } + throw new Error(`unexpected url ${url}`); + }); + + const template = await fetchWecomApprovalTemplateCreate({ + config: baseConfig(), + payload: buildTemplateFixturePayload({ key: "leave", label: "请假" }, "2026-05-19T10:00:00.000Z"), + fetchImpl, + }); + const approval = await fetchWecomApprovalApplyEvent({ + config: baseConfig(), + payload: buildApplyFixturePayload({ + target: { key: "leave", label: "请假" }, + templateId: SENSITIVE_TEMPLATE_ID, + applicantUserId: SENSITIVE_APPLICANT_USERID, + approverUserId: SENSITIVE_APPROVER_USERID, + generatedAt: "2026-05-19T10:00:00.000Z", + }), + fetchImpl, + }); + + expect(template).toMatchObject({ + ok: true, + tokenSource: "WECOM_AGENT_SECRET", + templateId: SENSITIVE_TEMPLATE_ID, + }); + expect(approval).toMatchObject({ + ok: true, + tokenSource: "WECOM_AGENT_SECRET", + spNo: SENSITIVE_SP_NO, + }); + expect(calls.some((item) => item.url.includes("/cgi-bin/oa/approval/create_template"))).toBe(true); + expect(calls.some((item) => item.url.includes("/cgi-bin/oa/applyevent"))).toBe(true); + }); + + it("passes generated template IDs and sp_no into WAPC2 auto rerun without reporting full IDs", async () => { + const executeApprovalPocImpl = vi.fn(async (options) => { + expect(options.live).toBe(true); + expect(options.templates).toEqual({ + leave: `${SENSITIVE_TEMPLATE_ID}-leave`, + missed_punch: `${SENSITIVE_TEMPLATE_ID}-missed_punch`, + }); + expect(options.spNos).toEqual({ + leave: `${SENSITIVE_SP_NO}-leave`, + missed_punch: `${SENSITIVE_SP_NO}-missed_punch`, + }); + return { status: "PASS_APPROVAL_POC", conclusion: "PASS_APPROVAL_POC" }; + }); + + const report = await executeApprovalFixtures(baseOptions({ + live: true, + createTemplates: true, + submitTestApprovals: true, + }), { + config: baseConfig(), + env: { WECOM_APPROVAL_FIXTURE_WRITE: "1" }, + actorRows: actorRows(), + createTemplateImpl: vi.fn(async ({ payload }) => ({ + ok: true, + tokenSource: "WECOM_AGENT_SECRET", + templateId: `${SENSITIVE_TEMPLATE_ID}-${payload.fixture_marker.target}`, + response: { errcode: 0 }, + })), + applyEventImpl: vi.fn(async ({ payload }) => ({ + ok: true, + tokenSource: "WECOM_AGENT_SECRET", + spNo: `${SENSITIVE_SP_NO}-${payload.template_id.split("-").pop()}`, + response: { errcode: 0 }, + })), + executeApprovalPocImpl, + generatedAt: "2026-05-19T10:00:00.000Z", + }); + + const serialized = JSON.stringify(report); + expect(report.status).toBe("PASS_FIXTURE_CREATED"); + expect(executeApprovalPocImpl).toHaveBeenCalledTimes(1); + expect(serialized).not.toContain(`${SENSITIVE_TEMPLATE_ID}-leave`); + expect(serialized).not.toContain(`${SENSITIVE_SP_NO}-leave`); + expect(serialized).toContain("tpl-fixt***"); + expect(serialized).toContain("sp-fixtu***"); + }); + + it("keeps report output redacted", async () => { + const report = await executeApprovalFixtures(baseOptions(), { + config: baseConfig(), + env: {}, + actorRows: actorRows(), + generatedAt: "2026-05-19T10:00:00.000Z", + }); + const serialized = JSON.stringify(report); + + expect(serialized).not.toContain(SENSITIVE_AGENT_CREDENTIAL); + expect(serialized).not.toContain(SENSITIVE_CONTACT_CREDENTIAL); + expect(serialized).not.toContain(SENSITIVE_CHECKIN_CREDENTIAL); + expect(serialized).not.toContain(SENSITIVE_HR_CREDENTIAL); + expect(serialized).not.toContain(SENSITIVE_APPLICANT_USERID); + expect(serialized).not.toContain(SENSITIVE_APPROVER_USERID); + expect(serialized).not.toContain("low-risk dry-run fixture"); + expect(serialized).toContain("fixture_***"); + }); + + it("documents missing WECOM_AGENT_SECRET as a config gap", () => { + const check = buildSecretCheck(baseConfig({ wecomAgentSecret: "" })); + expect(check).toMatchObject({ + status: "NEEDS_CONFIG", + missing: ["WECOM_AGENT_SECRET"], + }); + expect(check.noFallback).toEqual([ + "WECOM_CONTACT_SECRET", + "WECOM_CHECKIN_SECRET", + "WECOM_HR_ASSISTANT_SECRET", + ]); + }); +});