WRULE1 实现企微审批链校验器 POC

这个提交包含在:
2026-05-21 00:35:10 +08:00
父节点 1440cfe9a2
当前提交 c119183da6
共修改 14 个文件,包含 1225 行新增和 29 行删除
+4 -4
查看文件
@@ -19,8 +19,8 @@ Read only what is relevant:
- 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/WPROC2 fixed the engineering process gate; WEGOV1 is the route gate, and WACP3R has now rerun the compensation-pull POC by automatically discovering OA-POC leave/missed-punch template IDs from `SMOKE002` creator. Public report `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T115944Z/summary.json` is `PASS_WITH_FINDINGS`: compensation pull and detail read work, with finding that callback evidence files are not persisted across deploys. WREAD1/WREAD1F have now completed real-device readability confirmation for leave and missed-punch: public report `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` is `PASS_READABLE`; the user confirmed cards/lists/details are readable, reject has second confirmation, and approve is single-tap. WADM1 has completed the self-owned archive/mirror data-model design. WARCH1 has implemented and publicly verified the minimum leave/missed-punch archive slice: Prisma archive/callback/compensation/finding tables, callback DB persistence with non-200 retry on DB failure, compensation persistence behind `--persist-to-db` + `WECOM_ARCHIVE_WRITE=1`, and offline `npm run check:wecom-approval-archive`; public reports `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T160306Z/summary.json` and `/app/artifacts/qa-flow/wecom-approval-archive-20260519T160330Z/summary.json` are `PASS_WITH_FINDINGS`, with 2 archived histories readable and only a historical pre-WARCH1 callback gap finding. WARCH2 has now added the formal OA read-only archive API/page: `/api/wecom/approval-archive`, `/wecom-approval-archive`, and `/mobile/wecom-approval-archive` read OA DB only, do not call WeCom API or read WeCom Secrets, and enforce self/direct-report/HR-admin visibility with unmapped applicant records HR/admin-only. WROAD2+OPS is the controlled launch gate: use test accounts `SMOKE002 -> SMOKE101`, observe/admin-alert via employee_no `HYD000006` (the employee id for `wangshunli`), run daily checks for 3 days, allow only compensation archive writes to OA DB via `WECOM_ARCHIVE_WRITE=1`, and do not create WeCom templates/approvals, write WeCom, or expand real users. Day 0 public report `/app/artifacts/qa-flow/wroad2-ops-20260520-041941/summary.json` is `PASS_WITH_FINDINGS`: WeCom runtime, compensation archive persistence, offline archive read, and privacy smoke pass; findings are missing live-smoke login credentials/pending OCR subcheck and historical callback gap. It still does not implement full mirror, outing/overtime live, emergency mode, writeback, or RuleOps. WAUTO1 adds `scripts/qa/collect-wecom-controlled-trial-evidence.sh` for read-only controlled-trial evidence aggregation; it must not set `WECOM_ARCHIVE_WRITE=1`, `WECOM_CHECKIN_ARCHIVE_WRITE=1`, or `WECOM_APPROVAL_FIXTURE_WRITE=1`. WEXT1LD is the outing/overtime live-before dry-run gate and does not authorize live fixture creation. WFULLSMOKE1 adds `scripts/qa/run-three-day-full-goal-smoke.sh` as the three-day full-goal smoke controller; it may create only OA test business data marked `TEST/SMOKE/WFULL`, and must not write WeCom, create outing/overtime fixtures, enable real employees, make real payments, connect real payment providers, write ERP, or import bank statements. 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 WROAD2-CLOSEOUT, WROAD2-BIZ1, WAPPR1, WAUTO1/WEXT1LD, WFULLSMOKE1 evidence, WEXT1-LIVE, WREAL1 question gates or explicit approval, and the next stage's own Issue CSV and acceptance gates.
- WARCH3/WCHECK1/WEXT1 update the route after WROAD2 Day 0: WARCH3 adds `scripts/qa/run-warch3-ops-daily.sh`, `deploy/trialrun/install-warch3-ops-timer.sh`, and `npm run send:wecom-ops-alert` for 07:30 systemd巡检和脱敏企微告警;public report `/app/artifacts/qa-flow/warch3-ops-20260520-054841/summary.json` is `PASS_WITH_FINDINGS`, and WOPS1 now fixes alert noise: default recipient is `HYD000006` (the employee_no for `wangshunli`), repeated known findings are skipped via `artifacts/qa-flow/warch3-ops-alert-state.json`, and only blocked/failure/new-finding/status-worsening/force sends a message. WOPS1 public rerun `/app/artifacts/qa-flow/warch3-ops-20260520-070731/summary.json` is `PASS_WITH_FINDINGS`, and alert report `/app/artifacts/qa-flow/wecom-ops-alert-20260520T070740Z/summary.json` is `PASS_ALERT_SKIPPED_NO_FINDINGS` with `no_new_findings_or_status_worsening`. WCHECK1 adds `npm run check:wecom-checkin-summary-archive`, daily/monthly check-in summaries, and rule snapshots with `--persist-to-db` + `WECOM_CHECKIN_ARCHIVE_WRITE=1` as the only OA DB write gate; public reports `/app/artifacts/qa-flow/wecom-checkin-summary-archive-20260520T054907Z/summary.json` and `/app/artifacts/qa-flow/wecom-checkin-summary-archive-20260520T054926Z/summary.json` are `PASS_CHECKIN_ARCHIVE_READY` and `PASS_CHECKIN_ARCHIVE_OFFLINE_READY`. WEXT1 only extends approval fixtures for `--target=outing --target=overtime` dry-run; public report `/app/artifacts/qa-flow/wecom-approval-fixtures-20260520T054940Z/summary.json` is `PASS_FIXTURE_DRY_RUN_READY`. WAUTO1/WEXT1LD now add a read-only evidence aggregation script and a live-before dry-run contract; WFULLSMOKE1 adds full-domain main-path smoke orchestration across WeCom HR/attendance, OA-primary reimbursement/procurement/contract-project/payment-batch flows, mobile/WeCom entry, and ops checks. These do not create outing/overtime WeCom templates/approvals, expand real users, or implement full mirror/emergency/writeback/RuleOps. WEXT1-LIVE needs WROAD2-CLOSEOUT/WROAD2-BIZ1/WAPPR1/WAUTO1/WEXT1LD/WFULLSMOKE1, a new Issue CSV, and explicit approval; WREAL1 real users are asked only after leave, missed punch, outing, and overtime test-account evidence plus approver-rule gate and WFULLSMOKE1 are passable/explainable.
- WROAD2-BIZ1/WAPPR1 adjust the post-WOPS1 route: during the 3-day window, add manual business smoke on test accounts without replacing WARCH3 automation. `SMOKE002` should try WeCom check-in and submit low-risk leave/missed-punch, `SMOKE101` must open details before approving, and `HYD000006` checks OA archived approval/check-in summaries. WAPPR1 treats "applicant must manually choose approver" as a blocking rule finding: test templates remain `SMOKE002 -> SMOKE101`, formal leave/missed-punch/outing/overtime templates must auto-resolve approvers from direct manager, department owner, or configured approval role, and applicant-chosen arbitrary approvers/self-approval must block WREAL1. Official WeCom APIs do not expose a read-only native-template approval-rule configuration surface: `gettemplatedetail` returns controls, `getapprovaldetail` returns actual submitted flow, and `applyevent use_template_approver=1` is a write-style check, so do not design WAPPR1 as pure API automation. WAUTO1 can automate evidence collection, and WFULLSMOKE1 can aggregate full-domain smoke evidence, but WAPPR1 choices A/B/C/D and real-device business actions remain manual gates. Do not start WEXT1-LIVE, WREAL1, deep mirror, emergency, writeback, or RuleOps until WROAD2-CLOSEOUT, WROAD2-BIZ1, WAPPR1, WAUTO1/WEXT1LD, WFULLSMOKE1, WEXT1-LIVE, and the relevant question gates or explicit approval are complete.
- WROUTE2 locks the post-WFULLSMOKE1 route: first close out test-account evidence, then ask for WEXT1-LIVE outing+overtime test-account live POC, then ask WREAL1 with exactly 1 real employee + 1 approver/admin observer, then prioritize `wecom_attendance_approval_mirror_v1` minimum complete mirror. WTCLOSE1 adds the small-team execution rule: because the company has fewer than 10 people, WAPPR1 targets option A for leave/missed-punch/outing/overtime; formal HR/attendance approvals use one direct manager or configured approval role with admin fallback/blocking when the approver chain is missing, unavailable, invisible, unbound, or self-approval. WTCLOSE1 only authorizes WAUTO1/WFULLSMOKE1 aggregation and WEXT1-LIVE pre-dry-run; it does not authorize WeCom writes, real employees, outing/overtime fixture creation, emergency mode, writeback, or RuleOps.
- WROAD2-BIZ1/WAPPR1/WRULE1 adjust the post-WOPS1 route: during the 3-day window, add manual business smoke on test accounts without replacing WARCH3 automation. `SMOKE002` should try WeCom check-in and submit low-risk leave/missed-punch, `SMOKE101` must open details before approving, and `HYD000006` checks OA archived approval/check-in summaries. WAPPR1 treats "applicant must manually choose approver" as a blocking rule finding: test templates remain `SMOKE002 -> SMOKE101`, formal leave/missed-punch/outing/overtime templates must auto-resolve approvers from direct manager, department owner, or configured approval role, and applicant-chosen arbitrary approvers/self-approval must block WREAL1. Official WeCom APIs do not expose a read-only native-template approval-rule configuration surface: `gettemplatedetail` returns controls, `getapprovaldetail` returns actual submitted flow, and `applyevent use_template_approver=1` is a write-style check, so do not design WAPPR1 as pure API automation. WRULE1 adds `npm run check:wecom-approval-rule-validator` to compare OA theoretical approvers with archived WeCom actual approval chains; it is read-only, does not read WeCom backend rules, and does not replace WAPPR1 A/B/C/D manual gates. WAUTO1 can automate evidence collection, and WFULLSMOKE1 can aggregate full-domain smoke evidence, but WAPPR1 choices A/B/C/D and real-device business actions remain manual gates. Do not start WEXT1-LIVE, WREAL1, deep mirror, emergency, writeback, or RuleOps until WROAD2-CLOSEOUT, WROAD2-BIZ1, WAPPR1, WRULE1, WAUTO1/WEXT1LD, WFULLSMOKE1, WEXT1-LIVE, and the relevant question gates or explicit approval are complete.
- WROUTE2 locks the post-WFULLSMOKE1 route: first close out test-account evidence, then ask for WEXT1-LIVE outing+overtime test-account live POC, then ask WREAL1 with exactly 1 real employee + 1 approver/admin observer, then prioritize `wecom_attendance_approval_mirror_v1` minimum complete mirror. WTCLOSE1 adds the small-team execution rule: because the company has fewer than 10 people, WAPPR1 targets option A for leave/missed-punch/outing/overtime; formal HR/attendance approvals use one direct manager or configured approval role with admin fallback/blocking when the approver chain is missing, unavailable, invisible, unbound, or self-approval. WRULE1 enforces this as an OA-side validator over archived WeCom approval details; `PASS_RULE_MATCH` or explainable `PASS_WITH_ADMIN_FALLBACK` can support test-account closeout, while `FAIL_APPLICANT_CAN_CHOOSE`, `FAIL_SELF_APPROVAL`, `FAIL_APPROVER_MISMATCH`, missing manual gates, or external config blockers stop WEXT1-LIVE/WREAL1. WTCLOSE1 only authorizes WAUTO1/WFULLSMOKE1 aggregation and WEXT1-LIVE pre-dry-run; it does not authorize WeCom writes, real employees, outing/overtime fixture creation, emergency mode, writeback, or RuleOps.
- WAPC2FLIVE supersedes the WAPC2F dry-run gap for OA-POC samples: after user approval, the runner was corrected to the official `template_name` + `property` controls create-template payload, deployed with `SYNC_ENV=0`, and the public live report `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T053400Z/summary.json` reached `PASS_FIXTURE_CREATED`; leave/missed-punch OA-POC templates and low-risk test approvals were created and retained for audit, with WAPC2 auto-rerun `PASS_APPROVAL_POC`. WCBK1 has added and deployed read-only `GET/POST /api/wecom/approval/callback`, `wecomCallbackCrypto`, `npm run check:wecom-approval-callback-poc`, and the `wecom-callback` process gate. After remote callback config was set, public report `/app/artifacts/qa-flow/wecom-approval-callback-poc-20260519T083422Z/summary.json` reached `PASS_CALLBACK_URL_VERIFIED`; after `SMOKE101` approved an existing OA-POC missed-punch test approval, `/app/artifacts/qa-flow/wecom-approval-callback-poc-20260519T092648Z/summary.json` reached `PASS_CALLBACK_EVENT_CONFIRMED` with one redacted event. WACP3/WACP3R has added and deployed `npm run check:wecom-approval-compensation-poc`, `POST /cgi-bin/oa/getapprovalinfo` read support, and automatic template discovery by `SMOKE002` creator; WREAD1 deploy reran WACP3R at `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T125515Z/summary.json` and then generated `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T125532Z/summary.json` as `PAUSED_MANUAL_CONFIRMATION`. WREAD1F then fixed duplicate OA-POC template names by adding timestamp uniqueness and created fresh leave/missed-punch approvals at `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T131958Z/summary.json`; final WREAD1 manual report `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` is `PASS_READABLE`. This clears leave/missed-punch readability only; mirror data model, minimum archive, emergency mode, writeback, RuleOps, and outing/overtime sample decisions remain later gates.
- WEGOV1 changes the current WeCom route to "Enterprise WeChat owns mature HR/attendance flows; OA stores self-owned history, audit, entry, messages, and emergency fallback." Treat four domains explicitly: WeCom-owned domain is check-in, leave, missed punch, outing, overtime, leave balance, and HR assistant; OA-owned domain remains reimbursement, procurement, contracts, projects, payments, invoices, bank receipts, sensitive attachments, and operating audit; OA archive domain stores WeCom business history, full external keys, and necessary snapshots; OA emergency domain is manually enabled only when WeCom is unavailable. Do not assume a full isolated WeCom sandbox; use the controlled test domain `SMOKE002 -> SMOKE101`, `OA-POC` templates, low-risk approvals, synthetic tests, public live runners, and limited real-device confirmation. WARCH1 now supplies the minimum leave/missed-punch archive implementation, WARCH2 supplies the formal read-only history query surface, and WROAD2 supplies controlled ops daily checks. Do not start deep mirror until WROAD2-CLOSEOUT, WROAD2-BIZ1, WAPPR1, WAUTO1/WEXT1LD, WFULLSMOKE1, WEXT1-LIVE, WREAL1 question gates or explicit approval, and the mirror stage's own contract are complete.
- For reimbursement, invoice, OCR, duplicate invoice, or attachment work, read `docs/报销发票附件规则.md` and `references/reimbursement.md`.
@@ -111,8 +111,8 @@ Final responses should include:
- R6 scope defaults to reimbursement, leave, procurement, approval, Enterprise WeChat entry/notification, finance payment/export.
- Do not expand to budget, ERP, AI, full travel, or complex BPM during R6; record those as follow-up Issues.
- Enterprise WeChat personal app message is the formal approval notification channel after R5; mock and group bots are not acceptance evidence for R6 business notification.
- After WAPI, ERP is not a future OA integration target. HR/attendance employee daily approvals (leave, missed punch, outing, overtime) should reuse mature Enterprise WeChat capabilities first, while OA provides self-owned archive, domain-based mirror, manual takeover, recovery writeback, finance closeout, and audit. Reimbursement, payment batches, AP, accounting handoff, procurement, expenses, contracts, projects, sensitive attachments, invoices, bank receipts, and accounting handoff materials remain OA-primary unless a later Issue changes that domain explicitly. OA-primary approvals may send Enterprise WeChat personal-app notifications, but messages must contain only dehydrated title/type/no/status/deeplink; do not upload procurement forms, contract bodies, quotes, supplier files, invoices, bank receipts, or accounting materials to WeCom native approvals or WeCom attachments. Current WAPI rule: contacts read with `WECOM_CONTACT_SECRET`; HR assistant/roster/check-in/approval default to `WECOM_AGENT_SECRET`, with `WECOM_CHECKIN_SECRET` only as explicit check-in override; no runtime Secret guessing. Current stage order is `WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WEXT1-LIVE 外出/加班测试号 live POC -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像 -> oa_hr_emergency_mode_v1 -> wecom_recovery_writeback_v1 -> wecom_ruleops_v1`. UI and API work must distinguish `OA 主办`, `企微主办`, `OA 自建留存`, and `OA 应急接管`. Do not treat WEXT1 dry-run, WEXT1LD, WFULLSMOKE1, or WROUTE2 as permission to live-create outing/overtime templates or approvals; live needs explicit user approval.
- Current post-WROAD2 route wording is: `WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WEXT1-LIVE 外出/加班测试号 live POC -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像`. WREAL1-PREP only prepared the checklist and does not enable real employees. Do not treat WEXT1 dry-run, WEXT1LD, WFULLSMOKE1, or WROUTE2 as permission to live-create outing/overtime templates or approvals; live needs explicit user approval.
- After WAPI, ERP is not a future OA integration target. HR/attendance employee daily approvals (leave, missed punch, outing, overtime) should reuse mature Enterprise WeChat capabilities first, while OA provides self-owned archive, domain-based mirror, manual takeover, recovery writeback, finance closeout, and audit. Reimbursement, payment batches, AP, accounting handoff, procurement, expenses, contracts, projects, sensitive attachments, invoices, bank receipts, and accounting handoff materials remain OA-primary unless a later Issue changes that domain explicitly. OA-primary approvals may send Enterprise WeChat personal-app notifications, but messages must contain only dehydrated title/type/no/status/deeplink; do not upload procurement forms, contract bodies, quotes, supplier files, invoices, bank receipts, or accounting materials to WeCom native approvals or WeCom attachments. Current WAPI rule: contacts read with `WECOM_CONTACT_SECRET`; HR assistant/roster/check-in/approval default to `WECOM_AGENT_SECRET`, with `WECOM_CHECKIN_SECRET` only as explicit check-in override; no runtime Secret guessing. Current stage order is `WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WRULE1 审批链校验器 POC -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WEXT1-LIVE 外出/加班测试号 live POC -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像 -> oa_hr_emergency_mode_v1 -> wecom_recovery_writeback_v1 -> wecom_ruleops_v1`. UI and API work must distinguish `OA 主办`, `企微主办`, `OA 自建留存`, and `OA 应急接管`. Do not treat WEXT1 dry-run, WEXT1LD, WFULLSMOKE1, or WROUTE2 as permission to live-create outing/overtime templates or approvals; live needs explicit user approval.
- Current post-WROAD2 route wording is: `WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WRULE1 审批链校验器 POC -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WEXT1-LIVE 外出/加班测试号 live POC -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像`. WREAL1-PREP only prepared the checklist and does not enable real employees. Do not treat WEXT1 dry-run, WEXT1LD, WFULLSMOKE1, or WROUTE2 as permission to live-create outing/overtime templates or approvals; live needs explicit user approval.
- WMSG1 has verified real Enterprise WeChat personal-app delivery and Deeplink return: report `/app/artifacts/qa-flow/wecom-runtime-ready-20260518T234431Z/summary.json` has `checks.message.status=OK`, `errcode=0`, and `recipientCount=1`; the user confirmed receiving "OA 运行就绪检查" and opening the OA notifications page from the message. This only clears message/deeplink runtime evidence; template/detail, callback evidence, compensation pull, readability, self-owned archive model design, and minimum archive implementation are covered by WAPC2FLIVE/WCBK1/WACP3R/WREAD1/WADM1/WARCH1 in the OA-POC leave/missed-punch scope, while deep mirror work remains a separate later gate.
- 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:<id>` / `--template=missed_punch:<id>` and low-risk test approval numbers with `--sp-no=leave:<sp_no>` / `--sp-no=missed_punch:<sp_no>`. 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.
+16 -3
查看文件
@@ -477,10 +477,11 @@ Go 结论边界:
| WROAD2-CLOSEOUT 三天验收 | 待 Day 1/2/3 自动采集 | Day 0 报告 `/app/artifacts/qa-flow/wroad2-ops-20260520-041941/summary.json` 为 `PASS_WITH_FINDINGS`。已知 observation 为 live smoke 凭据、pending OCR 子检查和历史 callback gap,不阻断受控试用;企微 runtime、审批补偿归档、OA 离线读取、打卡摘要 offline 可读和隐私扫描仍是主链路。三天内正常或已知 finding 不打扰,仅新增异常、状态恶化、阻断或脚本失败时通知。 |
| WROAD2-BIZ1 三天业务冒烟 | 已建档,待人工执行 | 三天内补充真实业务动作:`SMOKE002` 每天按企微规则尝试上/下班打卡,提交低风险请假和补卡;`SMOKE101` 每条审批必须先打开详情再审批;`HYD000006` 每天查看 OA 企微审批归档历史和打卡摘要。人工业务冒烟不替代 WARCH3 自动巡检;无法打卡、无法审批或仍需手选审批人时,真实记录为 finding。 |
| WAPPR1 审批规则规范化 | 已建档,作为真实试用前门禁 | 用户发现补卡申请需要申请人手动选择审批人,该现象作为 blocking finding 治理。测试模板固定 `SMOKE002 -> SMOKE101`;正式请假、补卡、外出、加班模板必须按直属主管、部门负责人或指定审批角色自动带出审批人;申请人不得任意选择审批人,审批人缺失、停用、未绑定、不可见或自审时阻断。 |
| WREAL1-PREP 真实试用准备 | 仅规划,不启用真实员工 | 固化候选员工 active/已绑定/可见、审批人非本人、回滚联系人、允许流程、暂停条件和验收标准。当前继续测试账号;真实试用必须等请假、补卡、外出、加班四类测试号证据和 WAPPR1 审批规则门禁通过后再问答确认,默认仍从 1 名真实员工 + 1 名审批人/管理员观察开始。 |
| WRULE1 审批链校验器 | 已建档并进入本地 POC | 固定“单一组织规则,两个执行域”:OA 维护主管链、业务审批角色和管理员兜底,企微执行 HR/考勤审批,OA 只读已归档审批详情并校验理论审批人是否匹配实际审批链。WRULE1 不读取企微后台规则、不写企微、不替代 WAPPR1 人工门禁。 |
| WREAL1-PREP 真实试用准备 | 仅规划,不启用真实员工 | 固化候选员工 active/已绑定/可见、审批人非本人、回滚联系人、允许流程、暂停条件和验收标准。当前继续测试账号;真实试用必须等请假、补卡、外出、加班四类测试号证据和 WAPPR1/WRULE1 审批规则门禁通过后再问答确认,默认仍从 1 名真实员工 + 1 名审批人/管理员观察开始。 |
| 外出/加班 | live POC 待三天验收后另建合同 | WEXT1 dry-run 证据保留;当前不创建企微外出/加班模板或审批单。已锁定 WEXT1-LIVE 方向:外出+加班两类一起创建 OA-POC 测试模板和低风险审批单,固定 `SMOKE002 -> SMOKE101`、`WECOM_AGENT_SECRET` 和双开关,样件保留审计,由 `SMOKE101` 真机确认。 |
三天验收结束后的人工问答选项固定为:进入 WEXT1-LIVE、先处理指定 finding、或暂停。外出/加班 live 写入前必须再次说明当前状态、改了什么、测了什么、结果是什么和会留下什么企微记录;四类测试号证据和 WAPPR1 审批规则门禁通过后,再问是否进入真实小范围试用。
三天验收结束后的人工问答选项固定为:进入 WEXT1-LIVE、先处理指定 finding、或暂停。外出/加班 live 写入前必须再次说明当前状态、改了什么、测了什么、结果是什么和会留下什么企微记录;四类测试号证据和 WAPPR1/WRULE1 审批规则门禁通过后,再问是否进入真实小范围试用。
## WAUTO1 / WEXT1LD 自动证据汇总与 live 前 dry-run
@@ -512,7 +513,7 @@ Go 结论边界:
| 项 | 当前结果 | 证据和边界 |
| --- | --- | --- |
| 路线合同 | 已建档 | 新增 `issues/2026-05-20_20-15-29-wecom-test-account-closeout-to-minimal-mirror-route-v1.csv`,作为 WFULLSMOKE1 后的路线锁定合同。 |
| 测试号收口 | 已锁定 | 当前优先完成 WROAD2-CLOSEOUT、WROAD2-BIZ1、WAPPR1、WAUTO1、WEXT1LD 和 WFULLSMOKE1;WAPPR1 四类模板必须为 A,否则不进入 WEXT1-LIVE 或 WREAL1。 |
| 测试号收口 | 已锁定 | 当前优先完成 WROAD2-CLOSEOUT、WROAD2-BIZ1、WAPPR1、WRULE1、WAUTO1、WEXT1LD 和 WFULLSMOKE1;WAPPR1 四类模板必须为 A,WRULE1 必须为 `PASS_RULE_MATCH` 或可解释 `PASS_WITH_ADMIN_FALLBACK`,否则不进入 WEXT1-LIVE 或 WREAL1。 |
| 外出/加班 live | 待后续问答 | 只有三天验收、全域主路径、业务冒烟、规则门禁和 dry-run 均可解释通过后,才另建 WEXT1-LIVE 合同并再次问答是否一次性 live 创建外出+加班测试号样件。 |
| 真实试用与镜像 | 后置 | WREAL1 首批固定为 1 名真实员工 + 1 名审批人/管理员观察;WREAL1 通过后优先规划 `wecom_attendance_approval_mirror_v1` 最小完整镜像,应急接管、恢复回写和 RuleOps 均后置。 |
@@ -531,6 +532,18 @@ Go 结论边界:
| 外出/加班 live 前 dry-run | `PASS_FIXTURE_DRY_RUN_READY` | 公网报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260520T125941Z/summary.json`;`secretMatrix=OK`、`actors=OK`、`liveWrite=DRY_RUN_READY`、`autoRerun=DRY_RUN_SKIPPED`。本轮没有设置 `WECOM_APPROVAL_FIXTURE_WRITE=1`,没有带 `--live --create-templates --submit-test-approvals`,没有创建企微模板或审批单。 |
| 后续问答 | 待人工门禁 | dry-run 已具备进入问答的自动证据,但必须先由 `HYD000006` 确认请假、补卡、外出、加班四类模板均为 A;若出现 B/C/D,先修审批规则。WAPPR1 四类为 A 且三天业务动作可解释通过后,才问是否进入 WEXT1-LIVE;真实员工试用仍固定后置为 1 名真实员工 + 1 名审批人/管理员观察。 |
## WRULE1 企微 HR/考勤审批链校验器 POC
执行时间:2026-05-20。本轮只做本地只读 POC,不部署、不写 OA 业务数据、不写企业微信、不启用真实员工。
| 项目 | 状态 | 证据与边界 |
| --- | --- | --- |
| 执行合同 | 已建档 | 新增 `issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv`,ID 为 `WRULE1-0..WRULE1-7`。 |
| 校验器 | 已实现 | 新增 `server/src/services/integrations/wecomApprovalRuleValidator.js` 和 `server/src/scripts/check-wecom-approval-rule-validator.js`;按 OA 直属主管、业务审批角色和管理员兜底计算理论审批人,再与 OA 已归档企微审批参与人/动作对比。 |
| 结论分类 | 已固定 | `PASS_RULE_MATCH`、`PASS_WITH_ADMIN_FALLBACK`、`FAIL_APPLICANT_CAN_CHOOSE`、`FAIL_SELF_APPROVAL`、`FAIL_APPROVER_MISMATCH`、`PAUSED_MANUAL_CONFIRMATION`、`SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 |
| 人工门禁关系 | 不替代 WAPPR1 | `--manual-gate=<template>:A/B/C/D` 只登记管理员后台和真机提交页结论;缺门禁时输出 `PAUSED_MANUAL_CONFIRMATION`。WRULE1 只能证明已提交审批链是否匹配 OA 理论规则,不能证明申请页是否允许改审批人。 |
| 隐私边界 | 已固化 | 报告只输出脱敏 `sp_no`、template、employee 前缀、计数和 finding 类型;不输出完整 userid、完整 template_id、完整审批单号、手机号、邮箱、token、Secret、表单值、附件、评论或原始 payload。 |
## 历史恢复
需要追溯 R70-R91 的详细流水时,使用:
+6 -4
查看文件
@@ -2,8 +2,8 @@
**文档状态**:当前企微 API 调研与本项目使用事实源。
**分类**:集成事实源 / API 能力目录。
**适用时间**:WAPI / WAPC1 / WAPC2 / WPROC1 / WPROC2 / WAPC2F / WAPC2FLIVE / WCBK1 / WEGOV1 / WACP3 / WACP3R / WREAD1 / WADM1 / WARCH1 / WARCH2 / WROAD2 / WARCH3 / WCHECK1 / WEXT1 / WOPS1 / WROAD2-CLOSEOUT / WROAD2-BIZ1 / WAPPR1 / WAUTO1 / WEXT1LD / WFULLSMOKE1 / WROUTE2 / WREAL1-PREP 之后。
**关联 Issue**:`WAPI,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WROUTE2,WREAL1P`。
**适用时间**:WAPI / WAPC1 / WAPC2 / WPROC1 / WPROC2 / WAPC2F / WAPC2FLIVE / WCBK1 / WEGOV1 / WACP3 / WACP3R / WREAD1 / WADM1 / WARCH1 / WARCH2 / WROAD2 / WARCH3 / WCHECK1 / WEXT1 / WOPS1 / WROAD2-CLOSEOUT / WROAD2-BIZ1 / WAPPR1 / WAUTO1 / WEXT1LD / WFULLSMOKE1 / WROUTE2 / WRULE1 / WREAL1-PREP 之后。
**关联 Issue**:`WAPI,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WROUTE2,WRULE1,WREAL1P`。
**最近核对**:2026-05-20。
**不得误读**:本文只记录本项目会用到的企业微信官方 API 摘要、链接和项目决策,不复制官方文档全文,不保存 Secret、access token、完整 userid、手机号、邮箱或企微原始 payload。官方文档和企业微信后台权限配置始终是最终依据。
@@ -65,6 +65,7 @@
| 批量获取审批单号 | [批量获取审批单号](https://developer.work.weixin.qq.com/document/path/91816) | `/cgi-bin/oa/getapprovalinfo` | `POST` | `WECOM_AGENT_SECRET` | `CAN_REUSE_NOW_FOR_ARCHIVE` | WACP3/WACP3R 已新增 `check:wecom-approval-compensation-poc`,支持按 `creator` 自动发现 OA-POC 模板,再按 `template_id` filters 拉审批单号,并用 `getapprovaldetail` 拉详情;WARCH1 增加 OA DB 持久化双开关、补偿 run/item 和对账 finding,用于解决回调证据文件跨部署丢失问题。WROAD2+OPS 每日巡检复用该 runner;只允许以 `WECOM_ARCHIVE_WRITE=1` + `--persist-to-db` 写 OA 留存库,不写企微、不无过滤拉取高风险业务单据。 |
| 提交审批申请 | [提交审批申请](https://developer.work.weixin.qq.com/document/path/91853) | `/cgi-bin/oa/applyevent` | `POST` | `WECOM_AGENT_SECRET` | `DONE_LIVE_VERIFIED_FOR_OA_POC_FIXTURES` | 已接入 WAPC2F QA-only runner;请假/补卡已完成一次 live 创建并保留审计。`use_template_approver=1` 可使用管理后台设置的模板流程,但这是写入型验证,会创建审批单;且官方文档限制部分模板不能通过接口提交,不能作为 WAPPR1 通用只读门禁。WEXT1 扩展外出/加班 payload,但本轮只 dry-run;live 写入仍需显式 CLI、`WECOM_APPROVAL_FIXTURE_WRITE=1`、非自审预检和人工批准。OA 主办报销、采购、合同、项目不迁到企微原生审批。 |
| 审批规则配置读取 | 审批模板详情、审批申请详情、提交审批申请、[审批流程引擎](https://developer.work.weixin.qq.com/document/path/90269) | 无原生审批规则只读接口 | N/A | N/A | `NOT_SUPPORTED_BY_API / NEEDS_MANUAL_GATE` | 当前未发现官方只读 API 可读取企微原生审批模板的“申请人自选节点”“是否允许改审批人”或后台审批规则配置。WAPPR1 必须用管理后台人工确认 + 真机提交页确认 + `getapprovaldetail` 实际审批链校验;审批流程引擎面向自建/第三方应用流程,不等同于读取或修改企业微信原生审批应用规则。 |
| 审批链规则校验 | OA 归档审批详情 + OA 员工主管链/角色 | `npm run check:wecom-approval-rule-validator` | QA-only 本地/远端脚本 | 上游审批详情固定 `WECOM_AGENT_SECRET`;脚本默认只读 OA DB | `CAN_REUSE_NOW_FOR_RULE_POC` | WRULE1 采用“单一组织规则,两个执行域”:OA 维护员工直属主管、业务审批角色和管理员兜底事实,企业微信执行 HR/考勤审批。脚本读取已归档企微审批详情,对比理论审批人和实际审批链,输出 `PASS_RULE_MATCH`、`PASS_WITH_ADMIN_FALLBACK`、`FAIL_APPLICANT_CAN_CHOOSE`、`FAIL_SELF_APPROVAL`、`FAIL_APPROVER_MISMATCH`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。脚本不读取企微后台规则、不写企微、不替代 WAPPR1 人工门禁。 |
| 假期余额 | [获取成员假期余额](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) | `/cgi-bin/oa/approval/create_template` 等官方模板写接口 | `POST` | `WECOM_AGENT_SECRET` | `DONE_LIVE_VERIFIED_FOR_OA_POC_FIXTURES` | WAPC2FLIVE 已用 `template_name` + `property` controls 结构创建 OA-POC 请假/补卡测试模板并保留审计。WEXT1 只为外出/加班构建 dry-run payload;正式模板更新、外出/加班 live 创建和 RuleOps 写入仍留后期合同。 |
@@ -98,7 +99,7 @@ WAPC2 不代表真实回调、审批镜像、应急接管、恢复回写或 Rule
WPROC1 将 WAPC2 之后的审批路线固定为:
`WAPC2 -> WAPC2F 自动生成 POC 样件 -> WAPC2FLIVE live 创建与 WAPC2 自动复跑 -> WCBK1 回调只读 POC -> WEGOV1 企微主办治理与自建留存路线修正 -> WACP3/WACP3R 补偿拉取 POC -> WREAD1 审批可读性与误审批防护门禁 -> WADM1 自建留存/镜像数据模型设计 -> WARCH1 最小自建历史留存实现 -> WARCH2 正式只读历史查询 -> WROAD2+OPS 受控上线与每日巡检 -> WARCH3 归档稳定化自动巡检 -> WCHECK1 打卡摘要留存 -> WEXT1 外出/加班 dry-run -> WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WEXT1-LIVE 外出/加班测试号 live POC -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像`
`WAPC2 -> WAPC2F 自动生成 POC 样件 -> WAPC2FLIVE live 创建与 WAPC2 自动复跑 -> WCBK1 回调只读 POC -> WEGOV1 企微主办治理与自建留存路线修正 -> WACP3/WACP3R 补偿拉取 POC -> WREAD1 审批可读性与误审批防护门禁 -> WADM1 自建留存/镜像数据模型设计 -> WARCH1 最小自建历史留存实现 -> WARCH2 正式只读历史查询 -> WROAD2+OPS 受控上线与每日巡检 -> WARCH3 归档稳定化自动巡检 -> WCHECK1 打卡摘要留存 -> WEXT1 外出/加班 dry-run -> WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WRULE1 审批链校验器 POC -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WEXT1-LIVE 外出/加班测试号 live POC -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像`
门禁要求:
@@ -121,6 +122,7 @@ WPROC1 将 WAPC2 之后的审批路线固定为:
- `WEXT1` 扩展 `npm run check:wecom-approval-fixtures` 支持 `--target=outing --target=overtime`,本轮只 dry-run,不 live 创建外出/加班模板或审批单。
- `WROAD2-BIZ1` 补充测试账号人工业务冒烟:打卡、低风险请假/补卡、审批详情查看和 OA 离线历史查看;它不新增企微 API,不替代 WARCH3 自动巡检。
- `WAPPR1` 不新增写接口;它是审批模板配置门禁,要求申请人不得任意选择审批人,正式模板必须自动带出审批人,规则未通过不得进入真实员工试用。由于官方 API 不支持只读读取原生审批规则,WAPPR1 结论必须由后台人工确认、真机提交页确认和 `getapprovaldetail` 提交后实际审批链校验共同组成。
- `WRULE1` 不新增企微 API;它复用 OA 已归档的企微审批详情和 OA 组织事实源,自动校验理论审批人是否匹配实际审批链。WRULE1 可发现自审、实际审批人不符和管理员兜底,但不能证明申请页是否允许手选审批人,仍必须保留 WAPPR1 人工 A/B/C/D 门禁。
### 6.1 WADM1 数据留存与 API 结果边界
@@ -136,7 +138,7 @@ WADM1/WARCH1 将已验证的审批能力固定为“可读可留存”的实现
## 7. 运行优先结论
- 近期优先利用:企业微信工作台入口、OAuth/免登、绑定、个人应用消息、OA deeplink、通讯录 userid 清单、可选成员详情、人事助手花名册读取、打卡记录和打卡规则读取,以及 WROAD2/WARCH3 测试账号受控上线巡检。
- 暂缓深度镜像:WREAD1、WADM1、WARCH1、WARCH2 已完成请假/补卡可读性、模型设计、最小历史留存和正式只读查询;WARCH3/WCHECK1/WEXT1/WOPS1/WROAD2-BIZ1/WAPPR1/WAUTO1/WEXT1LD/WFULLSMOKE1/WROUTE2 本轮只解决巡检稳定、打卡摘要留存、外出/加班 dry-run、告警降噪、测试账号业务冒烟、审批规则门禁、自动证据汇总、全域测试和路线锁定;`wecom_attendance_approval_mirror_v1` 最小完整镜像仍需等 WEXT1-LIVE、四类测试号证据和 WREAL1 小范围真实试用通过后再优先规划;`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1` 后置,或你明确批准后再拆。
- 暂缓深度镜像:WREAD1、WADM1、WARCH1、WARCH2 已完成请假/补卡可读性、模型设计、最小历史留存和正式只读查询;WARCH3/WCHECK1/WEXT1/WOPS1/WROAD2-BIZ1/WAPPR1/WRULE1/WAUTO1/WEXT1LD/WFULLSMOKE1/WROUTE2 本轮只解决巡检稳定、打卡摘要留存、外出/加班 dry-run、告警降噪、测试账号业务冒烟、审批规则门禁、理论审批人自动校验、自动证据汇总、全域测试和路线锁定;`wecom_attendance_approval_mirror_v1` 最小完整镜像仍需等 WEXT1-LIVE、四类测试号证据和 WREAL1 小范围真实试用通过后再优先规划;`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1` 后置,或你明确批准后再拆。
- OA 主办边界不变:报销、采购、费用、合同、项目、付款批次、AP、代账交接、审计和高敏附件继续在 OA 内完成,只向企业微信发送脱敏提醒和 OA 链接。
## 8. 后续使用要求
+15 -12
查看文件
@@ -2,12 +2,13 @@
**文档状态**:当前开发路线图事实源。
**分类**:开发治理 / 路线图。
**适用时间**:WDRG / WCP1 / WHMSLIVE / WOUA / WAPI / WRRA / WOUA-FIX / WRV1 / WRA1 / WMSG1 / WAPC1 / WAPC2 / WPROC1 / WPROC2 / WAPC2F / WAPC2FLIVE / WCBK1 / WEGOV1 / WACP3 / WACP3R / WREAD1 / WREAD1F / WADM1 / WARCH1 / WARCH2 / WROAD2 / WARCH3 / WCHECK1 / WEXT1 / WOPS1 / WROAD2-CLOSEOUT / WROAD2-BIZ1 / WAPPR1 / WAUTO1 / WEXT1LD / WFULLSMOKE1 / WROUTE2 / WTCLOSE1 / WREAL1-PREP / WEXT1-LIVE 路线选择之后。
**关联 Issue**:`WDRG,WCP1,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WREAD1F,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WROUTE2,WTCLOSE1,WREAL1P`。
**适用时间**:WDRG / WCP1 / WHMSLIVE / WOUA / WAPI / WRRA / WOUA-FIX / WRV1 / WRA1 / WMSG1 / WAPC1 / WAPC2 / WPROC1 / WPROC2 / WAPC2F / WAPC2FLIVE / WCBK1 / WEGOV1 / WACP3 / WACP3R / WREAD1 / WREAD1F / WADM1 / WARCH1 / WARCH2 / WROAD2 / WARCH3 / WCHECK1 / WEXT1 / WOPS1 / WROAD2-CLOSEOUT / WROAD2-BIZ1 / WAPPR1 / WAUTO1 / WEXT1LD / WFULLSMOKE1 / WROUTE2 / WTCLOSE1 / WRULE1 / WREAL1-PREP / WEXT1-LIVE 路线选择之后。
**关联 Issue**:`WDRG,WCP1,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WOUAFIX,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WREAD1F,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WROUTE2,WTCLOSE1,WRULE1,WREAL1P`。
**最近审计**:2026-05-20。
**不得误读**:本文只控制后续开发节奏、阶段顺序和门禁。WHMSLIVE 后只代表企微 HR 主数据镜像已部署并受控验证;WAPI 后进一步固定企微 API/Secret 口径并转为运行优先;WRRA 后明确 `PASS_WITH_FINDINGS` 只代表只读对账成功且存在差异,不代表企微考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps 已完成运行时实现。WMSG1 只证明企业微信应用消息可达和 Deeplink 可回到 OA;WAPC1 只固化审批配置准备清单;WAPC2 只新增请假/补卡模板详情和低风险 `sp_no` 审批详情只读 POC,不代表真实回调或镜像能力已完成。WPROC1 后,WAPC2 不直接放行深度镜像,必须先完成自动样件、回调、补偿拉取和镜像数据模型设计门禁。WPROC2 只新增可执行 process gate。WAPC2FLIVE 已创建 OA-POC 请假/补卡测试模板和低风险测试审批单,并自动复跑 WAPC2 得到 `PASS_APPROVAL_POC`。WCBK1 已完成真实回调 URL 验证和一条现有 OA-POC 测试审批单事件确认,公网报告为 `PASS_CALLBACK_EVENT_CONFIRMED`;WEGOV1 后路线明确为“企微成熟功能优先上线,OA 做自建留存、审计、入口、消息和应急兜底”。WACP3R 已将补偿拉取复跑修正为自动按 `SMOKE002` creator 发现 OA-POC 请假/补卡模板,公网报告 `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T115944Z/summary.json` 为 `PASS_WITH_FINDINGS`。WREAD1F 新样例后,审批人真机确认请假和补卡消息卡片、列表、详情页均可看清;远端人工结果报告 `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` 为 `PASS_READABLE`。但同意按钮单击即通过仍是操作风险,需在 WARCH1/WARCH2 的留存、审计、提示或操作规程中继续处理。WADM1 只完成自建留存/镜像数据模型设计;WARCH1 只实现请假/补卡最小审批历史留存、回调事件入库、补偿拉取入库和 QA-only 离线读取证明;WARCH2 只提供 OA DB 只读查询 API 和 PC/移动历史页,不调用企微 API、不读取企微 Secret、不代表完整镜像、外出/加班、应急接管、恢复回写或 RuleOps 已完成。WROAD2 后当前策略是测试账号受控上线和每日脚本化巡检,允许补偿归档写 OA 自建留存库,但不写企业微信、不扩大真实员工范围;WARCH3 将每日巡检升级为服务器 `systemd timer` 与脱敏企微告警,WCHECK1 增加打卡规范化明细、日/月摘要和规则快照留存,WEXT1 只完成外出/加班 dry-run 支持。WOPS1 已公网验证巡检告警降噪:`HYD000006` 是管理员 `wangshunli` 的 employee_no,脚本不再把姓名字符串当员工号;已知 finding 不重复发送,只有阻断、脚本失败、新增 finding 或状态恶化才告警。WROAD2-BIZ1 在三天内补充测试账号人工业务冒烟;WAPPR1 将申请人任意选择审批人列为真实试用前阻断 finding。WAUTO1 只做只读证据汇总,WEXT1LD 只做外出/加班 live 前 dry-run,均不授权 live 创建。WFULLSMOKE1 是三天全域最终目标测试总控,只聚合企微 HR/考勤、OA 主办域、移动端和运维巡检证据;它允许在公网试运行环境创建带 `TEST/SMOKE/WFULL` 标记的 OA 测试业务数据,但不写企业微信、不创建外出/加班样件、不做真实付款、不接真实支付、不写 ERP、不导入银行流水、不启用真实员工。WREAL1-PREP 只准备真实小范围试用清单,不启用真实员工;后续顺序收紧为 WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WAUTO1 自动证据汇总 -> WEXT1LD 外出/加班 live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WEXT1-LIVE 外出/加班测试号 live POC -> 请假/补卡/外出/加班四类测试号证据和审批规则门禁通过后再问真实小范围试用。上述阶段仍不代表完整深度镜像、应急接管、恢复回写或 RuleOps 已完成。
**WROUTE2 补充口径**:WFULLSMOKE1 之后先锁定测试号全域收口,再进入 WEXT1-LIVE 问答;WREAL1 首批固定为 1 名真实员工 + 1 名审批人/管理员观察;WREAL1 通过后优先规划 `wecom_attendance_approval_mirror_v1` 最小完整镜像,应急接管、恢复回写和 RuleOps 后置。
**WTCLOSE1 执行口径**:公司不到 10 人,WAPPR1 四类模板目标按 A 执行;正式 HR/考勤审批采用一层直属主管或指定审批角色自动带出审批人,主管链缺失、审批人不可见/停用/未绑定或命中自审时由管理员兜底或阻断。本轮自动执行到 WAUTO1/WFULLSMOKE1 汇总和 WEXT1-LIVE 前 dry-run,不授权任何企微写入。
**WRULE1 执行口径**:采用“单一组织规则,两个执行域”。OA 维护员工、直属主管、业务角色和管理员兜底事实;企业微信只执行 HR/考勤审批;OA 通过 `check:wecom-approval-rule-validator` 读取已归档企微审批详情,计算理论审批人并对比实际审批链。WRULE1 是只读校验器,不新增 HR/考勤审批引擎,不写企微,不替代 WAPPR1 的后台人工确认和真机提交页确认。
## 1. 路线图原则
@@ -24,6 +25,7 @@
- WFULLSMOKE1 把三天目标从“企微受控巡检”扩展为全域主路径闭环:企微 HR/考勤、OA 报销、采购、合同/项目、付款批次、移动端和运维巡检一起汇总;只允许 `TEST/SMOKE/WFULL` 标记的 OA 测试业务数据,不写企业微信、不真实付款、不写 ERP、不导入银行流水。
- WROUTE2 后锁定“测试号全域收口 -> 外出/加班测试号 live POC -> 真实小试用 -> 最小完整镜像”:当前不启用真实员工;WREAL1 首批固定为 1 名真实员工 + 1 名审批人/管理员观察;WREAL1 通过后优先规划 `wecom_attendance_approval_mirror_v1` 最小完整镜像,不先做应急接管、恢复回写或 RuleOps。
- WTCLOSE1 后小团队审批规则按一层主管/指定审批角色 + 管理员兜底执行;四类模板未达到 A 时,不进入 WEXT1-LIVE 或 WREAL1。
- WRULE1 后,WAPPR1 仍是人工规则门禁;WRULE1 只把已提交审批链和 OA 理论审批人自动对账。`PASS_RULE_MATCH` 或可解释 `PASS_WITH_ADMIN_FALLBACK` 才能继续测试号收口;`FAIL_APPLICANT_CAN_CHOOSE`、`FAIL_SELF_APPROVAL`、`FAIL_APPROVER_MISMATCH` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG` 均阻断 WEXT1-LIVE、WREAL1 和最小完整镜像。
- 企业微信自建应用不按完整隔离 sandbox 处理;后续默认使用真实企业内受控测试域、本地 synthetic 自动测试、公网 live runner 和少量真机确认。
- 阶段未满足退出条件前不得跳阶段。线上缺陷、安全风险、正式使用阻断或真实反馈可以另开独立 Issue CSV,但不得混入企微 HR/考勤主路线。
- ERP 未来不接入 OA;真实支付、图片 OCR、银行流水导入、复杂 BPM、审批设计器、重预算和其他 P2 大功能继续冻结,试用期只处理阻断和真实反馈。
@@ -97,16 +99,17 @@
| 19 | `WROAD2-CLOSEOUT 三天验收` | 汇总 Day 0/1/2/3 巡检报告,决定是否进入外出/加班 live POC。 | WOPS1 本地和公网验证通过;WARCH3 timer 正常。 | 输出 `PASS_CONTROLLED_OPS_READY`、`PASS_WITH_FINDINGS`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`,并结合 WROAD2-BIZ1 人工业务冒烟结果问答确认。 |
| 20 | `WROAD2-BIZ1 三天业务冒烟` | 在测试账号范围补充真实业务动作:打卡、请假、补卡、OA 离线查看,并为外出/加班 live POC 准备人工验收清单。 | 用户选择三天内测试更多业务;仍限定测试账号。 | 输出 `PASS_BUSINESS_SMOKE_READY`、`PASS_WITH_FINDINGS`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`;不写企微、不创建外出/加班样件。 |
| 21 | `WAPPR1 审批规则规范化门禁` | 统一企微 HR/考勤审批人规则,禁止申请人任意选择审批人。 | 发现补卡申请需要手动选择审批人;用户要求审批规则按规范自动化;官方 API 不支持只读读取原生模板审批规则。 | 四类模板按模板给出 `PASS_APPROVER_RULES_READY`、`PASS_WITH_TEMPLATE_ADJUSTMENTS`、`FAIL_KEEP_OA_PRIMARY_FOR_THIS_TEMPLATE` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`;规则不通过不得进入真实试用。WAPPR1 必须保留后台人工确认和真机提交页确认,API 只校验提交后的实际审批链。 |
| 22 | `WAUTO1 受控试用证据自动汇总` | 自动汇总三天受控试用证据,准备 WAPPR1 人工确认和 WEXT1-LIVE 问答。 | WROAD2-BIZ1/WAPPR1 已建档,用户要求能自动执行的全部脚本化。 | 新脚本默认只读,不触发补偿归档写库,不写企微;输出脱敏 summary 和 WAPPR1 人工清单。 |
| 23 | `WEXT1LD 外出/加班 live 前 dry-run gate` | 在 live 创建前远端只执行外出/加班 dry-run。 | WEXT1 dry-run runner 已存在;用户要求 live 前先自动 dry-run。 | 不带 live 写入参数;通过后只进入问答,不自动创建模板或审批单。 |
| 24 | `WFULLSMOKE1 三天全域最终目标测试` | 汇总企微 HR/考勤、OA 报销、采购、合同/项目、付款批次、移动/企微入口和运维巡检主路径。 | 用户要求三天内测试整个项目最终目标;WROAD2/WARCH3/WCHECK1/WROAD2-BIZ1/WAPPR1/WAUTO1/WEXT1LD 已建立。 | 输出 `PASS_FULL_GOAL_SMOKE_READY`、`PASS_WITH_FINDINGS`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`;只允许 `TEST/SMOKE/WFULL` OA 测试数据,不写企微、不真实付款、不写 ERP、不导入银行流水。 |
| 25 | `WREAL1-PREP / WROUTE2 真实试用准备和路线锁定` | 固化候选员工、审批人、允许流程、暂停条件、回滚联系人和验收标准,不启用真实员工,并锁定测试号收口到最小完整镜像的后续顺序。 | 用户选择先收口测试账号,不提前启用真实员工。 | 文档和合同明确真实试用需等四类测试号证据、WAPPR1 审批规则门禁和 WFULLSMOKE1 可解释通过后再问;当前继续测试账号,首批真实试用固定为 1 名真实员工 + 1 名审批人/管理员观察。 |
| 26 | `WEXT1-LIVE 外出/加班测试号 live POC` | 单次创建外出+加班 OA-POC 模板和低风险测试审批单,并做真机可读性和审批规则确认。 | WROAD2-CLOSEOUT/WROAD2-BIZ1/WAPPR1/WAUTO1/WEXT1LD/WFULLSMOKE1 可解释通过;新建独立 Issue CSV;再次说明会留下的企微记录并取得人工确认。 | 两类样件保留审计;固定 `SMOKE002 -> SMOKE101`、`WECOM_AGENT_SECRET` 和双开关;只保存脱敏 template/`sp_no`/userid 前缀;`SMOKE101` 真机确认消息、列表、详情、按钮风险和审批人是否自动带出。 |
| 27 | `WREAL1 小范围真实试用` | 在 1 名真实员工 + 1 名审批人/管理员观察范围验证企微主办 HR/考勤和 OA 留存。 | 请假、补卡、外出、加班四类测试号证据通过;WAPPR1 规则门禁通过;WFULLSMOKE1 可解释通过;用户通过问答确认真实员工、审批人、试用周期、暂停条件和回滚联系人。 | 试用报告可解释通过或暂停;仍不直接全员推广。 |
| 28 | `wecom_attendance_approval_mirror_v1 最小完整镜像` | 企微打卡、请假、补卡、外出、加班、假期余额和审批历史镜像到 OA。 | WREAL1 通过后优先规划;不得仅凭测试号证据进入,也不先做应急接管、恢复回写或 RuleOps。 | OA 可查询企微 HR/考勤镜像、附件摘要、审批历史、考勤解释和审计;不把 OA 主办高敏审批迁到企微。 |
| 29 | `oa_hr_emergency_mode_v1` | 企业微信不可用时,由管理员人工开启 OA HR/考勤应急接管。 | 自建历史留存和镜像数据可支撑对账;OA 应急入口、权限和审计方案明确。 | 管理员可填写原因、范围、预计结束时间和责任人后开启/关闭;应急单据带 `EMERGENCY` 标记。 |
| 30 | `wecom_recovery_writeback_v1` | 企微恢复后,OA 应急数据先 dry-run,再回写企微。 | 应急接管模式完成;企微写回接口、冲突规则和回滚策略已单独批准。 | 系统生成成功、失败、跳过和冲突清单;无冲突项可自动回写,冲突进入人工确认。 |
| 31 | `wecom_ruleops_v1` | AI 草拟企微规则、生成差异报告和 dry-run,人工确认后写入企微。 | 前述同步/回写链路稳定;企微规则 API 能力和权限已验证。 | 每次写入保存前后快照、确认人、API `errcode`、失败原因、回滚建议和审计记录。 |
| 22 | `WRULE1 企微 HR/考勤审批链校验器 POC` | OA 按单一组织规则计算理论审批人,对比企微已提交审批链,防止两套规则或自审。 | WAPPR1 已建档;已存在 OA 自建审批归档和员工主管链/角色数据;用户选择单规则双域。 | 新增 `check:wecom-approval-rule-validator`,默认只读 OA DB、不调用企微、不写 OA/企微。结论限定 `PASS_RULE_MATCH`、`PASS_WITH_ADMIN_FALLBACK`、`FAIL_APPLICANT_CAN_CHOOSE`、`FAIL_SELF_APPROVAL`、`FAIL_APPROVER_MISMATCH`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 |
| 23 | `WAUTO1 受控试用证据自动汇总` | 自动汇总三天受控试用证据,准备 WAPPR1 人工确认和 WEXT1-LIVE 问答。 | WROAD2-BIZ1/WAPPR1/WRULE1 已建档,用户要求能自动执行的全部脚本化。 | 新脚本默认只读,不触发补偿归档写库,不写企微;输出脱敏 summary 和 WAPPR1 人工清单。 |
| 24 | `WEXT1LD 外出/加班 live 前 dry-run gate` | 在 live 创建前远端只执行外出/加班 dry-run。 | WEXT1 dry-run runner 已存在;用户要求 live 前先自动 dry-run。 | 不带 live 写入参数;通过后只进入问答,不自动创建模板或审批单。 |
| 25 | `WFULLSMOKE1 三天全域最终目标测试` | 汇总企微 HR/考勤、OA 报销、采购、合同/项目、付款批次、移动/企微入口和运维巡检主路径。 | 用户要求三天内测试整个项目最终目标;WROAD2/WARCH3/WCHECK1/WROAD2-BIZ1/WAPPR1/WRULE1/WAUTO1/WEXT1LD 已建立。 | 输出 `PASS_FULL_GOAL_SMOKE_READY`、`PASS_WITH_FINDINGS`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`;只允许 `TEST/SMOKE/WFULL` OA 测试数据,不写企微、不真实付款、不写 ERP、不导入银行流水。 |
| 26 | `WREAL1-PREP / WROUTE2 真实试用准备和路线锁定` | 固化候选员工、审批人、允许流程、暂停条件、回滚联系人和验收标准,不启用真实员工,并锁定测试号收口到最小完整镜像的后续顺序。 | 用户选择先收口测试账号,不提前启用真实员工。 | 文档和合同明确真实试用需等四类测试号证据、WAPPR1/WRULE1 审批规则门禁和 WFULLSMOKE1 可解释通过后再问;当前继续测试账号,首批真实试用固定为 1 名真实员工 + 1 名审批人/管理员观察。 |
| 27 | `WEXT1-LIVE 外出/加班测试号 live POC` | 单次创建外出+加班 OA-POC 模板和低风险测试审批单,并做真机可读性和审批规则确认。 | WROAD2-CLOSEOUT/WROAD2-BIZ1/WAPPR1/WRULE1/WAUTO1/WEXT1LD/WFULLSMOKE1 可解释通过;新建独立 Issue CSV;再次说明会留下的企微记录并取得人工确认。 | 两类样件保留审计;固定 `SMOKE002 -> SMOKE101`、`WECOM_AGENT_SECRET` 和双开关;只保存脱敏 template/`sp_no`/userid 前缀;`SMOKE101` 真机确认消息、列表、详情、按钮风险和审批人是否自动带出。 |
| 28 | `WREAL1 小范围真实试用` | 在 1 名真实员工 + 1 名审批人/管理员观察范围验证企微主办 HR/考勤和 OA 留存。 | 请假、补卡、外出、加班四类测试号证据通过;WAPPR1/WRULE1 规则门禁通过;WFULLSMOKE1 可解释通过;用户通过问答确认真实员工、审批人、试用周期、暂停条件和回滚联系人。 | 试用报告可解释通过或暂停;仍不直接全员推广。 |
| 29 | `wecom_attendance_approval_mirror_v1 最小完整镜像` | 企微打卡、请假、补卡、外出、加班、假期余额和审批历史镜像到 OA。 | WREAL1 通过后优先规划;不得仅凭测试号证据进入,也不先做应急接管、恢复回写或 RuleOps。 | OA 可查询企微 HR/考勤镜像、附件摘要、审批历史、考勤解释和审计;不把 OA 主办高敏审批迁到企微。 |
| 30 | `oa_hr_emergency_mode_v1` | 企业微信不可用时,由管理员人工开启 OA HR/考勤应急接管。 | 自建历史留存和镜像数据可支撑对账;OA 应急入口、权限和审计方案明确。 | 管理员可填写原因、范围、预计结束时间和责任人后开启/关闭;应急单据带 `EMERGENCY` 标记。 |
| 31 | `wecom_recovery_writeback_v1` | 企微恢复后,OA 应急数据先 dry-run,再回写企微。 | 应急接管模式完成;企微写回接口、冲突规则和回滚策略已单独批准。 | 系统生成成功、失败、跳过和冲突清单;无冲突项可自动回写,冲突进入人工确认。 |
| 32 | `wecom_ruleops_v1` | AI 草拟企微规则、生成差异报告和 dry-run,人工确认后写入企微。 | 前述同步/回写链路稳定;企微规则 API 能力和权限已验证。 | 每次写入保存前后快照、确认人、API `errcode`、失败原因、回滚建议和审计记录。 |
## 4. 每阶段固定门禁
+4 -3
查看文件
@@ -98,6 +98,7 @@
| `WROAD2-CLOSEOUT` | `issues/2026-05-20_14-35-42-wroad2-controlled-ops-closeout-v1.csv` | `CURRENT` | 三天受控巡检验收合同:Day 1/2/3 由 WARCH3 timer 采集后汇总,已知 live smoke 凭据、pending OCR 和历史 callback gap 作为 observation 不阻断;结束后需结合 WROAD2-BIZ1 和 WAPPR1 结论,再问答确认进入 WEXT1-LIVE、先处理 finding 或暂停,不直接启用真实员工 |
| `WROAD2-BIZ1` | `issues/2026-05-20_17-13-57-wroad2-business-smoke-v1.csv` | `CURRENT` | 三天测试账号业务冒烟合同:不替代 WARCH3 自动巡检;`SMOKE002` 每天按企微规则尝试打卡并提交低风险请假/补卡,`SMOKE101` 先看详情再审批,`HYD000006` 查看 OA 企微审批归档和打卡摘要;无法打卡、无法审批或仍需手选审批人时记录 finding;不写企业微信、不启用真实员工 |
| `WAPPR1` | `issues/2026-05-20_17-13-57-wecom-approval-rule-governance-v1.csv` | `CURRENT` | 企微审批规则规范化门禁合同:补卡手动选择审批人现象作为 blocking finding;测试模板固定 `SMOKE002 -> SMOKE101`;正式请假、补卡、外出、加班模板必须按直属主管、部门负责人或指定审批角色自动带出审批人;申请人不得任意选择审批人;审批人缺失、停用、未绑定、不可见或自审时阻断;官方 API 不支持只读读取原生模板审批规则,WAPPR1 需后台人工确认 + 真机提交页确认 + API 实际审批链校验;规则未通过不得进入真实试用 |
| `WRULE1` | `issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv` | `CURRENT` | 企微 HR/考勤审批链校验器 POC:采用“单一组织规则,两个执行域”,OA 维护员工直属主管、业务审批角色和管理员兜底事实,企微执行 HR/考勤审批,OA 只读已归档企微审批详情并对比理论审批人和实际审批链;新增 `check:wecom-approval-rule-validator`,不读取企微后台规则、不写 OA/企微、不替代 WAPPR1 人工门禁;自审、实际审批人不符、申请人可手选审批人或缺人工门禁均阻断后续 live/真实试用 |
| `WAUTO1` | `issues/2026-05-20_18-04-16-wecom-controlled-trial-auto-evidence-v1.csv` | `CURRENT` | 受控试用证据自动汇总合同已实现:新增 `scripts/qa/collect-wecom-controlled-trial-evidence.sh`,只读汇总 WROAD2/WARCH3/WCHECK1/WROAD2-BIZ1/WAPPR1/WEXT1 dry-run 证据,复跑 `/ready`、WARCH3 summary-only、审批补偿只读、审批归档离线、打卡摘要 offline、外出/加班 dry-run 和隐私扫描;默认不写 OA DB、不写企业微信、不创建模板或审批单 |
| `WEXT1LD` | `issues/2026-05-20_18-04-16-wecom-outing-overtime-live-dryrun-gate-v1.csv` | `CURRENT` | 外出/加班 live 前 dry-run gate 已实现:远端只执行 `npm run check:wecom-approval-fixtures -- --format=json --target=outing --target=overtime`,固定 `SMOKE002 -> SMOKE101` 与 `WECOM_AGENT_SECRET`;不设置写入环境变量、不带 live 写入参数,dry-run 通过后仍需人工问答批准或暂停 |
| `WFULLSMOKE1` | `issues/2026-05-20_19-01-12-three-day-full-goal-smoke-v1.csv` | `CURRENT` | 三天全域最终目标测试总控合同已建立:新增 `scripts/qa/run-three-day-full-goal-smoke.sh` 聚合企微 HR/考勤、OA 报销、采购、合同/项目、付款批次、移动/企微入口和运维巡检;允许带 `TEST/SMOKE/WFULL` 标记的 OA 测试业务数据,不写企业微信、不创建外出/加班样件、不启用真实员工、不真实付款、不接真实支付、不写 ERP、不导入银行流水 |
@@ -111,19 +112,19 @@
| 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,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WREAL1P` | `rg` | none | 2026-05-20 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么;WAUTO1/WEXT1LD/WFULLSMOKE1 后优先读取企微主办 + OA 自建留存路线、三天业务冒烟、审批规则规范化、自动证据汇总、全域最终目标测试、受控上线巡检、告警降噪、打卡摘要、外出/加班 dry-run、WEXT1-LIVE 前置和真实试用准备 |
| `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,WAPC2FLIVE,WCBK1,WEGOV1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WRULE1,WAUTO1,WEXT1LD,WFULLSMOKE1,WREAL1P` | `rg` | none | 2026-05-20 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么;WAUTO1/WEXT1LD/WFULLSMOKE1 后优先读取企微主办 + OA 自建留存路线、三天业务冒烟、审批规则规范化、审批链校验器、自动证据汇总、全域最终目标测试、受控上线巡检、告警降噪、打卡摘要、外出/加班 dry-run、WEXT1-LIVE 前置和真实试用准备 |
| `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,WAPC2FLIVE,WCBK1,WEGOV1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WREAL1P` | `audit-docs` | none | 2026-05-20 | `audit-docs --check-registry` | medium | 判断文档权威性;登记当前受控上线、三天业务冒烟、审批规则规范化、自动证据汇总、全域最终目标测试、自动巡检、告警降噪、打卡摘要留存、外出/加班 dry-run、WEXT1-LIVE 前置和真实试用准备事实 |
| `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,WAPC2,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1` | Issue CSV | `docs/requirements-specification.md` | 2026-05-20 | `rg` | high | WARCH3/WCHECK1/WEXT1/WOPS1 后定义 HR/考勤企微主办、OA 自建留存、受控测试域、只读历史查询、自动巡检、打卡摘要、外出/加班 dry-run、WEXT1-LIVE、WREAL1 前置和深度镜像边界;ERP 未来不接入 OA |
| `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T,WFDG,WDRG,WCP1,WCPC,WHMS,WHMSLIVE,WOUA,WAPI,WRRA,WAPC1,WAPC2FLIVE,WCBK1,WEGOV1` | `rg` | none | 2026-05-19 | `rg` | high | 记录已拍板产品边界、企微/OA 设计门禁、开发路线图、WCBK1 真实回调事件证据、WEGOV1 混合主办边界和 OA 自建留存策略 |
| `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,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WREAD1F,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WROUTE2,WTCLOSE1,WREAL1P` | Issue CSV | none | 2026-05-20 | `rg` | high | WAUTO1/WEXT1LD/WFULLSMOKE1/WTCLOSE1 后固定最小审批历史留存、正式只读历史查询、受控上线自动巡检、三天业务冒烟、审批规则规范化、自动证据汇总、全域最终目标测试、小团队一层审批 + 管理员兜底、告警降噪、打卡摘要、外出/加班 dry-run、live 前 dry-run gate、WEXT1-LIVE 前置和真实试用准备;仍禁止直接进入深度镜像 |
| `WECOM-API-CATALOG` | `docs/企微API能力目录.md` | 集成事实源 | 企微 API 能力目录和 Secret 矩阵事实源 | `CURRENT` | product/backend/qa | `WAPI,WRRA,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WAPPR1,WTCLOSE1` | 官方企业微信开发者中心链接/Issue CSV | none | 2026-05-20 | `rg` | high | 企微开发先读;只保存摘要、链接和项目决策,不保存 Secret、token、完整 userid、手机号、邮箱或原始 payload;记录审批补偿、打卡摘要、巡检只写 OA 留存、不写企业微信,以及企微原生审批规则不可只读 API 化、需人工门禁的边界 |
| `WECOM-API-CATALOG` | `docs/企微API能力目录.md` | 集成事实源 | 企微 API 能力目录和 Secret 矩阵事实源 | `CURRENT` | product/backend/qa | `WAPI,WRRA,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WAPPR1,WTCLOSE1,WRULE1` | 官方企业微信开发者中心链接/Issue CSV | none | 2026-05-20 | `rg` | high | 企微开发先读;只保存摘要、链接和项目决策,不保存 Secret、token、完整 userid、手机号、邮箱或原始 payload;记录审批补偿、打卡摘要、巡检只写 OA 留存、不写企业微信、企微原生审批规则不可只读 API 化、需人工门禁,以及 WRULE1 只读审批链校验边界 |
| `DELIVERY` | `docs/开发交付与变更管理规约.md` | 开发治理 | 开发交付事实源 | `CURRENT` | dev/qa | `GOV8,WDRG,WRRA,WAPC1,WPROC1,WPROC2,WCBK1,WEGOV1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1` | `rg` | none | 2026-05-20 | `rg` | high | Issue、review、commit、交付规则;WFULLSMOKE1 后固定 WAUTO1、WEXT1LD、全域最终目标测试和 WEXT1-LIVE/WREAL1 前置门禁 |
| `QA-GATE` | `docs/测试规约与回归门禁.md` | 测试验收 | 测试和回归门禁事实源 | `CURRENT` | qa | `QAF8,GOV8,WPROC1,WPROC2,WAPC2FLIVE,WCBK1,WEGOV1` | 回归脚本/Playwright | none | 2026-05-19 | `rg` | high | 测试计划、DONE 门禁、企微三层门禁、自动化优先、非自审、无完整 sandbox 假设、WREAD1 真机可读性和 WARCH1 留存验收门禁 |
| `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,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WREAD1F,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WOPS1,WROAD2C,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WROUTE2,WTCLOSE1,WREAL1P` | quick_validate | none | 2026-05-20 | `quick_validate.py` | medium | 每轮交付操作入口;企微/OA 后续先读 API 能力目录、Secret 矩阵、路线图、WEGOV1/WADM1/WARCH1/WARCH2/WROAD2/WARCH3/WCHECK1/WEXT1/WOPS1/WROAD2B/WAPPR1/WAUTO1/WEXT1LD/WFULLSMOKE1/WTCLOSE1 自建留存、受控巡检、业务冒烟、审批规则、自动证据汇总、全域收口 dry-run 和告警降噪边界;不得绕过 WROAD2C/WROAD2B/WAPPR1/WAUTO1/WEXT1LD/WFULLSMOKE1/WTCLOSE1/WEXT1-LIVE/WREAL1 或后续独立合同直接做完整考勤审批镜像 |
| `REQ-WF` | `docs/需求-统一审批中心规约.md` | 需求事实源 | 审批模块规约 | `CURRENT` | product/backend | `AP6,WF8,WFOC,DBWFOC,WFDG,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WREAD1F,WADM1,WARCH1,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WTCLOSE1` | workflow 回归 | none | 2026-05-20 | `rg` | high | OA 主办审批、企微主办 HR/考勤审批、OA 自建留存、真实回调事件证据、WACP3R 补偿拉取 findings、WREAD1 请假/补卡真机可读性结论、WARCH1 最小历史留存、WAPPR1 审批规则规范化和 API 不支持读取规则边界、WTCLOSE1 小团队一层审批 + 管理员兜底目标 A、WAUTO1/WEXT1LD/WFULLSMOKE1 前置和 OA 应急接管边界 |
| `REQ-WF` | `docs/需求-统一审批中心规约.md` | 需求事实源 | 审批模块规约 | `CURRENT` | product/backend | `AP6,WF8,WFOC,DBWFOC,WFDG,WOUA,WAPI,WRRA,WRV1,WRA1,WMSG1,WAPC1,WAPC2,WPROC1,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WREAD1F,WADM1,WARCH1,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WTCLOSE1,WRULE1` | workflow 回归 | none | 2026-05-20 | `rg` | high | OA 主办审批、企微主办 HR/考勤审批、OA 自建留存、真实回调事件证据、WACP3R 补偿拉取 findings、WREAD1 请假/补卡真机可读性结论、WARCH1 最小历史留存、WAPPR1 审批规则规范化和 API 不支持读取规则边界、WTCLOSE1 小团队一层审批 + 管理员兜底目标 A、WRULE1 理论审批人与实际审批链校验、WAUTO1/WEXT1LD/WFULLSMOKE1 前置和 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,WPROC2,WAPC2F,WAPC2FLIVE,WCBK1,WEGOV1,WACP3,WACP3R,WREAD1,WADM1,WARCH1,WARCH2,WROAD2,WARCH3,WCHECK1,WEXT1,WROAD2B,WAPPR1,WAUTO1,WEXT1LD,WFULLSMOKE1,WTCLOSE1` | live smoke/真机反馈/Issue CSV | none | 2026-05-20 | `rg` | high | 企微优先承担 HR/考勤员工日常审批和人事/打卡能力;WARCH2 已提供请假/补卡正式只读历史查询,WARCH3 固定受控上线自动巡检,WCHECK1 固定打卡摘要留存,WEXT1 固定外出/加班 dry-run,WROAD2B 固定三天人工业务冒烟,WAPPR1/WTCLOSE1 固定审批规则规范化和小团队目标 A,WFULLSMOKE1 固定全域主路径测试边界,WEXT1-LIVE 需三天验收后另建合同;采购、合同、项目、报销和财务继续 OA 主办 |
+2 -1
查看文件
@@ -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 -> WAPC2F dry-run -> WAPC2FLIVE -> WCBK1 -> WEGOV1 -> WACP3/WACP3R -> WREAD1 自动汇总 -> WREAD1F 新样例 -> WREAD1 真机确认 -> WADM1 自建留存模型设计 -> WARCH1 最小审批历史留存 -> WARCH2 正式只读历史查询 -> WROAD2+OPS 受控上线与每日巡检 -> WARCH3 归档稳定化自动巡检 -> WCHECK1 打卡摘要留存 -> WEXT1 外出/加班 dry-run -> WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WAUTO1 自动证据汇总 -> WEXT1LD live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WTCLOSE1 小团队自动审批规则与测试号全域收口 dry-run -> WEXT1-LIVE 路线选择(待三天验收、全域主路径、业务冒烟和规则门禁后另建合同) -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像。
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 -> WAPC2FLIVE -> WCBK1 -> WEGOV1 -> WACP3/WACP3R -> WREAD1 自动汇总 -> WREAD1F 新样例 -> WREAD1 真机确认 -> WADM1 自建留存模型设计 -> WARCH1 最小审批历史留存 -> WARCH2 正式只读历史查询 -> WROAD2+OPS 受控上线与每日巡检 -> WARCH3 归档稳定化自动巡检 -> WCHECK1 打卡摘要留存 -> WEXT1 外出/加班 dry-run -> WOPS1 巡检告警降噪 -> WROAD2-CLOSEOUT 三天验收 -> WROAD2-BIZ1 三天业务冒烟 -> WAPPR1 审批规则规范化 -> WRULE1 审批链校验器 POC -> WAUTO1 自动证据汇总 -> WEXT1LD live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WTCLOSE1 小团队自动审批规则与测试号全域收口 dry-run -> WEXT1-LIVE 路线选择(待三天验收、全域主路径、业务冒烟和规则门禁后另建合同) -> 四类测试号证据和审批规则门禁通过后问答 WREAL1 小范围真实试用(首批 1 名真实员工 + 1 名审批人/管理员观察) -> wecom_attendance_approval_mirror_v1 最小完整镜像。
- R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。
- ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。
@@ -159,6 +159,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
| `WROAD2-CLOSEOUT` | `issues/2026-05-20_14-35-42-wroad2-controlled-ops-closeout-v1.csv` | 当前三天受控巡检验收:Day 1/2/3 采集后人工确认 | 已知 observation 为 live smoke 凭据、pending OCR 和历史 callback gap,不阻断;主链必须保持企微 runtime、审批补偿归档、OA 离线读取、打卡摘要 offline 可读和隐私扫描通过 | 三天结束前不启用真实员工;结论需结合 WROAD2-BIZ1/WAPPR1 后问答确认是否进入 WEXT1-LIVE、先处理 finding 或暂停 |
| `WROAD2-BIZ1` | `issues/2026-05-20_17-13-57-wroad2-business-smoke-v1.csv` | 当前三天业务冒烟:补充测试账号人工业务动作 | `SMOKE002` 每天按企微规则尝试打卡并提交低风险请假/补卡,`SMOKE101` 先看详情再审批,`HYD000006` 查看 OA 归档历史和打卡摘要 | 不替代 WARCH3 自动巡检;不写企业微信;无法打卡、无法审批或仍需手选审批人时记录 finding |
| `WAPPR1` | `issues/2026-05-20_17-13-57-wecom-approval-rule-governance-v1.csv` | 当前审批规则规范化门禁:真实试用前必须解决审批人规则 | 测试模板固定 `SMOKE002 -> SMOKE101`;正式请假、补卡、外出、加班模板必须自动带出审批人;申请人不得任意选择审批人 | 审批人缺失、停用、未绑定、不可见或自审时阻断;规则未通过的模板不得进入真实试用 |
| `WRULE1` | `issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv` | 当前审批链校验器 POC:单一组织规则 + 企微执行 + OA 校验留存 | 新增 `npm run check:wecom-approval-rule-validator`,按 OA 直属主管、业务审批角色和管理员兜底计算理论审批人,并对比 OA 已归档企微审批详情中的实际审批链 | 只读 OA DB;不读取企微后台规则、不调用企微 API、不写 OA/企微;不替代 WAPPR1 人工 A/B/C/D 门禁 |
| `WAUTO1` | `issues/2026-05-20_18-04-16-wecom-controlled-trial-auto-evidence-v1.csv` | 当前受控试用证据自动汇总:把三天自动报告和只读 runner 汇总成脱敏证据 | 新增 `scripts/qa/collect-wecom-controlled-trial-evidence.sh`,复跑 `/ready`、WARCH3 summary-only、审批补偿只读、审批归档离线、打卡摘要 offline、外出/加班 dry-run 和隐私扫描 | 默认不写 OA DB、不写企业微信、不创建模板或审批单;WAPPR1 和三天业务仍需人工确认 |
| `WEXT1LD` | `issues/2026-05-20_18-04-16-wecom-outing-overtime-live-dryrun-gate-v1.csv` | 当前外出/加班 live 前 dry-run gate:只做远端 dry-run,不授权 live 写入 | 远端执行 `npm run check:wecom-approval-fixtures -- --format=json --target=outing --target=overtime`;固定 `SMOKE002 -> SMOKE101` 和 `WECOM_AGENT_SECRET` | 不设置 `WECOM_APPROVAL_FIXTURE_WRITE=1`,不带 live 写入参数;通过后仍需问答是否 live 创建样件 |
| `WFULLSMOKE1` | `issues/2026-05-20_19-01-12-three-day-full-goal-smoke-v1.csv` | 当前三天全域最终目标测试总控:覆盖企微 HR/考勤、OA 主办域、移动端和运维巡检 | 新增 `scripts/qa/run-three-day-full-goal-smoke.sh`,聚合 `/ready`、feature flag、WARCH3/WAUTO1、审批归档、打卡摘要、OA 报销/采购/付款批次主路径、移动/企微入口和隐私扫描 | 只允许 `TEST/SMOKE/WFULL` OA 测试业务数据;不写企业微信、不创建外出/加班样件、不启用真实员工、不真实付款、不接真实支付、不写 ERP、不导入银行流水 |
+2 -1
查看文件
@@ -1,9 +1,10 @@
# hyb-oa 管理员手册(10 人以下团队版)
**当前状态(2026-05-20 / WOPS1/WROAD2-CLOSEOUT/WROAD2-BIZ1/WAPPR1/WREAL1-PREP 后)**:R7E、PRELAUNCH、财务闭环、ATTV1、HR-WECOM-DIRECTORY-V1、WAPI、WOUA-FIX、WRV1/WRA1、WMSG1、WAPC1/WAPC2、WPROC1/WPROC2、WAPC2F dry-run、WAPC2FLIVE、WCBK1、WEGOV1、WACP3/WACP3R、WREAD1/WREAD1F、WADM1、WARCH1 和 WARCH2 均已收口到当前开发阶段;WROAD2+OPS 已完成测试账号受控上线 Day 0 巡检,报告 `/app/artifacts/qa-flow/wroad2-ops-20260520-041941/summary.json` 为 `PASS_WITH_FINDINGS`;SYSTEM-COMPLETE-GATE 结论为 `GO_TO_USE`。WAPI 后固定企微 API 能力目录和 Secret 矩阵:通讯录使用 `WECOM_CONTACT_SECRET`,人事助手/打卡/消息/OAuth/审批读取使用 `WECOM_AGENT_SECRET`,运行时代码不得靠多个 Secret 试错。WAPC2FLIVE 已创建 OA-POC 请假/补卡测试模板和低风险测试审批单,报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T053400Z/summary.json` 为 `PASS_FIXTURE_CREATED`,自动复跑 WAPC2 为 `PASS_APPROVAL_POC`;测试模板和审批记录保留审计,不自动删除。WCBK1 已部署只读回调端点和 runner,远端完成回调 URL 验证并收到一条由 `SMOKE101` 同意现有 OA-POC 补卡测试单触发的真实审批状态事件,报告 `/app/artifacts/qa-flow/wecom-approval-callback-poc-20260519T092648Z/summary.json` 为 `PASS_CALLBACK_EVENT_CONFIRMED`。WACP3R 已新增自动发现模板 ID 路径并公网只读复跑,报告 `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T115944Z/summary.json` 为 `PASS_WITH_FINDINGS`;WREAD1F 新样例后,用户确认请假/补卡在企业微信消息卡片、列表和详情页均可看清,远端报告 `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` 为 `PASS_READABLE`。WARCH1 已新增并部署请假/补卡最小审批历史留存:回调事件默认入 OA DB,补偿拉取可在 `--persist-to-db` + `WECOM_ARCHIVE_WRITE=1` 双开关下写 OA 留存,离线 runner 可不调用企微 API 读取已保存历史摘要;公网报告 `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T160306Z/summary.json` 和 `/app/artifacts/qa-flow/wecom-approval-archive-20260519T160330Z/summary.json` 均为 `PASS_WITH_FINDINGS`,2 条请假/补卡历史可离线读取,finding 是旧回调未入库历史 gap。WARCH2 已新增只读历史查询 API 和 PC/移动历史页:页面入口为“企微审批历史”,文案固定“企业微信主办,OA 自建留存”和“仅已归档记录”,只读 OA DB,不调用企微 API,不提供审批动作;权限按本人相关、直属下属和 HR/admin/* 分层,申请人未映射或 callback-only 记录仅 HR/admin/* 可见。WARCH3 已新增 07:30 systemd timer 巡检和脱敏企微告警;WOPS1 将默认告警收件员工号修正为 `HYD000006`,也就是管理员 `wangshunli` 的员工号,并把策略改为只在阻断、脚本失败、新增 finding 或状态恶化时发送,已知 observation 不重复告警。WCHECK1 已新增打卡规范化明细、日/月摘要和规则快照留存;WEXT1 已支持外出/加班 dry-run,但未 live 创建模板或审批单。WROAD2-BIZ1 固定三天内补充测试账号人工业务冒烟:`SMOKE002` 打卡、请假、补卡,`SMOKE101` 先看详情再审批,`HYD000006` 查看 OA 归档历史和打卡摘要;WAPPR1 固定审批规则门禁,申请人不得任意选择审批人,正式模板必须自动带出审批人,缺审批人或自审时阻断。巡检只允许补偿归档和打卡摘要写 OA 留存库,不写企业微信、不扩大真实员工。WREAL1-PREP 只准备真实小范围试用清单,当前不启用真实员工;三天巡检、业务冒烟和审批规则门禁后先问答是否进入 WEXT1-LIVE 外出/加班测试号 live POC,四类测试号证据和 WAPPR1 通过后才问是否启动真实小范围试用。同意单击即通过风险已进入归档审计事实,后续正式历史页和操作规程继续突出动作来源与补偿一致性。采购、费用、合同、项目、报销、付款、AP、代账交接和高敏附件继续 OA 主办,企业微信只做脱敏提醒和 OA deeplink。公网 `payment_batch_v1=force_on`,`erp_sync=force_off`。公司当前无 ERP/独立财务系统,且 ERP 未来不接入 OA;财务闭环以 OA 内审批、台账、付款批次、银行对公付款回单和月度代账交接为准。
**当前状态(2026-05-20 / WOPS1/WROAD2-CLOSEOUT/WROAD2-BIZ1/WAPPR1/WRULE1/WREAL1-PREP 后)**:R7E、PRELAUNCH、财务闭环、ATTV1、HR-WECOM-DIRECTORY-V1、WAPI、WOUA-FIX、WRV1/WRA1、WMSG1、WAPC1/WAPC2、WPROC1/WPROC2、WAPC2F dry-run、WAPC2FLIVE、WCBK1、WEGOV1、WACP3/WACP3R、WREAD1/WREAD1F、WADM1、WARCH1 和 WARCH2 均已收口到当前开发阶段;WROAD2+OPS 已完成测试账号受控上线 Day 0 巡检,报告 `/app/artifacts/qa-flow/wroad2-ops-20260520-041941/summary.json` 为 `PASS_WITH_FINDINGS`;SYSTEM-COMPLETE-GATE 结论为 `GO_TO_USE`。WAPI 后固定企微 API 能力目录和 Secret 矩阵:通讯录使用 `WECOM_CONTACT_SECRET`,人事助手/打卡/消息/OAuth/审批读取使用 `WECOM_AGENT_SECRET`,运行时代码不得靠多个 Secret 试错。WAPC2FLIVE 已创建 OA-POC 请假/补卡测试模板和低风险测试审批单,报告 `/app/artifacts/qa-flow/wecom-approval-fixtures-20260519T053400Z/summary.json` 为 `PASS_FIXTURE_CREATED`,自动复跑 WAPC2 为 `PASS_APPROVAL_POC`;测试模板和审批记录保留审计,不自动删除。WCBK1 已部署只读回调端点和 runner,远端完成回调 URL 验证并收到一条由 `SMOKE101` 同意现有 OA-POC 补卡测试单触发的真实审批状态事件,报告 `/app/artifacts/qa-flow/wecom-approval-callback-poc-20260519T092648Z/summary.json` 为 `PASS_CALLBACK_EVENT_CONFIRMED`。WACP3R 已新增自动发现模板 ID 路径并公网只读复跑,报告 `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T115944Z/summary.json` 为 `PASS_WITH_FINDINGS`;WREAD1F 新样例后,用户确认请假/补卡在企业微信消息卡片、列表和详情页均可看清,远端报告 `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` 为 `PASS_READABLE`。WARCH1 已新增并部署请假/补卡最小审批历史留存:回调事件默认入 OA DB,补偿拉取可在 `--persist-to-db` + `WECOM_ARCHIVE_WRITE=1` 双开关下写 OA 留存,离线 runner 可不调用企微 API 读取已保存历史摘要;公网报告 `/app/artifacts/qa-flow/wecom-approval-compensation-poc-20260519T160306Z/summary.json` 和 `/app/artifacts/qa-flow/wecom-approval-archive-20260519T160330Z/summary.json` 均为 `PASS_WITH_FINDINGS`,2 条请假/补卡历史可离线读取,finding 是旧回调未入库历史 gap。WARCH2 已新增只读历史查询 API 和 PC/移动历史页:页面入口为“企微审批历史”,文案固定“企业微信主办,OA 自建留存”和“仅已归档记录”,只读 OA DB,不调用企微 API,不提供审批动作;权限按本人相关、直属下属和 HR/admin/* 分层,申请人未映射或 callback-only 记录仅 HR/admin/* 可见。WARCH3 已新增 07:30 systemd timer 巡检和脱敏企微告警;WOPS1 将默认告警收件员工号修正为 `HYD000006`,也就是管理员 `wangshunli` 的员工号,并把策略改为只在阻断、脚本失败、新增 finding 或状态恶化时发送,已知 observation 不重复告警。WCHECK1 已新增打卡规范化明细、日/月摘要和规则快照留存;WEXT1 已支持外出/加班 dry-run,但未 live 创建模板或审批单。WROAD2-BIZ1 固定三天内补充测试账号人工业务冒烟:`SMOKE002` 打卡、请假、补卡,`SMOKE101` 先看详情再审批,`HYD000006` 查看 OA 归档历史和打卡摘要;WAPPR1 固定审批规则门禁,申请人不得任意选择审批人,正式模板必须自动带出审批人,缺审批人或自审时阻断。WRULE1 新增只读审批链校验器:OA 按员工直属主管、指定审批角色和管理员兜底计算理论审批人,再与 OA 已归档的企微实际审批链对比;它不读取企微后台规则、不写企业微信、不替代 WAPPR1 人工 A/B/C/D 门禁。巡检只允许补偿归档和打卡摘要写 OA 留存库,不写企业微信、不扩大真实员工。WREAL1-PREP 只准备真实小范围试用清单,当前不启用真实员工;三天巡检、业务冒烟和审批规则门禁后先问答是否进入 WEXT1-LIVE 外出/加班测试号 live POC,四类测试号证据和 WAPPR1/WRULE1 通过后才问是否启动真实小范围试用。同意单击即通过风险已进入归档审计事实,后续正式历史页和操作规程继续突出动作来源与补偿一致性。采购、费用、合同、项目、报销、付款、AP、代账交接和高敏附件继续 OA 主办,企业微信只做脱敏提醒和 OA deeplink。公网 `payment_batch_v1=force_on`,`erp_sync=force_off`。公司当前无 ERP/独立财务系统,且 ERP 未来不接入 OA;财务闭环以 OA 内审批、台账、付款批次、银行对公付款回单和月度代账交接为准。
**WROUTE2 后续路线锁定**:继续先收口测试账号全域证据,不提前启用真实员工。WAPPR1 四类模板必须达到 A(审批人自动带出,申请人不能任意选择)后,才允许另建 WEXT1-LIVE 合同并再次问答是否一次性 live 创建外出+加班测试号样件。请假、补卡、外出、加班四类测试号证据通过后,WREAL1 首批固定为 1 名真实员工 + 1 名审批人/管理员观察;WREAL1 通过后优先规划 `wecom_attendance_approval_mirror_v1` 最小完整镜像,应急接管、恢复回写和 RuleOps 均后置。
**WTCLOSE1 小团队规则**:公司不到 10 人时,HR/考勤审批先按一层直属主管或指定审批角色自动带出审批人,申请人不能任意改人;主管链缺失、审批人不可见/停用/未绑定或命中自审时,管理员兜底或阻断。当前目标是请假、补卡、外出、加班四类模板均达到 A;本轮自动工作只到 WAUTO1/WFULLSMOKE1 汇总和 WEXT1-LIVE 前 dry-run,不创建企微样件。
**WRULE1 审批链校验**:管理员后台规则和真机提交页仍由 WAPPR1 人工确认;系统只能用已归档审批详情校验实际审批链。校验命令为 `npm run check:wecom-approval-rule-validator -- --template=<类型> --manual-gate=<类型>:A`,缺人工门禁时结论为 `PAUSED_MANUAL_CONFIRMATION`;自审、实际审批人不符或申请人可手选审批人均阻断 WEXT1-LIVE、WREAL1 和最小镜像。
## 1. 适用范围
- 适用于 `small_team_internal` 工作区模式。
+15 -1
查看文件
@@ -5,7 +5,7 @@
**状态**:模块级开发规约
**上级事实源**:[海音达OA-MVP需求规格说明书](./海音达OA-MVP需求规格说明书.md)
本规约细化 `hyb-oa` 审批中心和企业微信原生审批镜像的产品边界、角色权限、状态模型、异常规则、留痕要求和验收场景。WFOC 起,审批按主办方分层;DBWFOC 后边界进一步收窄:报销、财务、采购、合同和项目类审批继续由 OA 主办;请假、补卡、外出、加班等 HR/考勤员工日常审批优先由企业微信主办,OA 同步按域镜像并保留自建历史留存、人工接管和恢复回写能力。WMSG1 已验证企业微信应用消息和 Deeplink 回流,WAPC1 后审批镜像前置工作进入配置准备清单阶段,WAPC2 已提供请假/补卡只读 POC runner,WAPC2FLIVE 已自动生成请假/补卡 OA-POC 样件并复跑通过。WCBK1 已完成只读回调 URL 验证和真实审批事件确认,结论为 `PASS_CALLBACK_EVENT_CONFIRMED`。WEGOV1 后明确先利用企微成熟审批和考勤能力,同时由 OA 保存关键历史和外部关联键。WACP3R 已自动发现 OA-POC 请假/补卡模板并公网复跑到 `PASS_WITH_FINDINGS`:补偿拉取和详情读取可用,finding 是回调证据文件未持久化。WREAD1F 后已完成请假/补卡真机可读性确认:消息卡片、列表、详情页均可看清,远端报告 `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` 为 `PASS_READABLE`;但同意按钮单击即通过,需在归档审计、提示或操作规程中处理。WADM1 已完成自建留存/镜像数据模型设计;WARCH1 已实现请假/补卡最小历史留存;WARCH2 已新增正式只读历史查询 API 和 PC/移动历史页,但完整镜像、应急接管、恢复回写和 RuleOps 仍不能直接放行。后续审批相关开发必须先从本规约拆分 Issue CSV,再进入技术方案和代码实现。
本规约细化 `hyb-oa` 审批中心和企业微信原生审批镜像的产品边界、角色权限、状态模型、异常规则、留痕要求和验收场景。WFOC 起,审批按主办方分层;DBWFOC 后边界进一步收窄:报销、财务、采购、合同和项目类审批继续由 OA 主办;请假、补卡、外出、加班等 HR/考勤员工日常审批优先由企业微信主办,OA 同步按域镜像并保留自建历史留存、人工接管和恢复回写能力。WMSG1 已验证企业微信应用消息和 Deeplink 回流,WAPC1 后审批镜像前置工作进入配置准备清单阶段,WAPC2 已提供请假/补卡只读 POC runner,WAPC2FLIVE 已自动生成请假/补卡 OA-POC 样件并复跑通过。WCBK1 已完成只读回调 URL 验证和真实审批事件确认,结论为 `PASS_CALLBACK_EVENT_CONFIRMED`。WEGOV1 后明确先利用企微成熟审批和考勤能力,同时由 OA 保存关键历史和外部关联键。WACP3R 已自动发现 OA-POC 请假/补卡模板并公网复跑到 `PASS_WITH_FINDINGS`:补偿拉取和详情读取可用,finding 是回调证据文件未持久化。WREAD1F 后已完成请假/补卡真机可读性确认:消息卡片、列表、详情页均可看清,远端报告 `/app/artifacts/qa-flow/wecom-approval-readability-gate-20260519T133628Z/summary.json` 为 `PASS_READABLE`;但同意按钮单击即通过,需在归档审计、提示或操作规程中处理。WADM1 已完成自建留存/镜像数据模型设计;WARCH1 已实现请假/补卡最小历史留存;WARCH2 已新增正式只读历史查询 API 和 PC/移动历史页。WAPPR1/WRULE1 后固定“单一组织规则,两个执行域”:OA 维护员工主管链、业务审批角色和管理员兜底事实,企业微信执行 HR/考勤审批,OA 只做归档和理论审批人校验;完整镜像、应急接管、恢复回写和 RuleOps 仍不能直接放行。后续审批相关开发必须先从本规约拆分 Issue CSV,再进入技术方案和代码实现。
---
@@ -423,6 +423,20 @@ WAPPR1 承接 WROAD2 期间发现的补卡申请仍需申请人手动选择审
- A/B/C/D 人工门禁口径不变:只有 A 可放行;B、C、D 均阻断 WEXT1-LIVE、WREAL1 和后续 `wecom_attendance_approval_mirror_v1`。
- 如果企微模板能力或后台配置暂时无法做到自动审批人规则,对应模板继续保持测试账号 POC 或 OA 主办,不进入 WREAL1 真实员工试用。
### 9.3.2 WRULE1 企微 HR/考勤审批链校验器 POC
WRULE1 不新造 HR/考勤审批引擎,也不读取企微后台规则配置。它只在 WAPPR1 人工门禁基础上,用 OA 已归档的企微审批详情和 OA 组织事实源自动校验“理论审批人”和“实际审批链”是否一致,防止单规则双域变成两套规则。
- runner 为 `npm run check:wecom-approval-rule-validator`;默认只读 OA DB,不调用企业微信 API,不读取 Secret,不写 OA 或企业微信业务数据。
- 上游审批详情仍由 WARCH1/WROAD2 补偿归档链路提供,固定使用 `WECOM_AGENT_SECRET`,不得靠通讯录、打卡或历史 HR assistant Secret fallback。
- 理论审批人计算顺序为:申请人的有效直属主管、配置的业务审批角色、管理员兜底。直属主管或角色审批人必须 active、已绑定企微且不是申请人本人。
- 管理员兜底只允许作为可解释异常,输出 `PASS_WITH_ADMIN_FALLBACK`;它不能把 `admin` 权限解释成普通业务审批权。
- 实际审批人为申请人本人时输出 `FAIL_SELF_APPROVAL`;实际审批人不在 OA 理论候选内时输出 `FAIL_APPROVER_MISMATCH`;WAPPR1 手工门禁 B/C 时输出 `FAIL_APPLICANT_CAN_CHOOSE`。
- 未传 `--manual-gate=<template>:A/B/C/D` 时输出 `PAUSED_MANUAL_CONFIRMATION`,表示缺少后台人工确认或真机提交页证据,不得把实际审批链匹配误读为模板规则已合规。
- 结论限定为 `PASS_RULE_MATCH`、`PASS_WITH_ADMIN_FALLBACK`、`FAIL_APPLICANT_CAN_CHOOSE`、`FAIL_SELF_APPROVAL`、`FAIL_APPROVER_MISMATCH`、`PAUSED_MANUAL_CONFIRMATION` 或 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。
- 报告只输出审批类型、计数、脱敏 `sp_no`/template/employee 前缀和 finding 类型,不输出完整 userid、完整 template_id、完整 `sp_no`、手机号、邮箱、token、Secret、表单值、附件、评论或原始 payload。
- `PASS_RULE_MATCH` 或可解释 `PASS_WITH_ADMIN_FALLBACK` 只是进入测试号收口的自动证据之一;四类模板仍必须先由 WAPPR1 人工确认 A,才能进入 WEXT1-LIVE、WREAL1 和最小完整镜像。
### 9.4 WEGOV1 / WACP3 / WREAD1 / WADM1 / WARCH1 前置门禁
在 `wecom_attendance_approval_mirror_v1` 前必须补齐治理、POC、真机和数据模型门禁:
@@ -0,0 +1,9 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"WRULE1-0","审批链校验器合同建档","建立 WRULE1 企微 HR/考勤审批链校验器 POC 合同,承接单一组织规则 + 企微 HR/考勤执行 + OA 校验留存路线。OA 按员工直属主管、指定审批角色和管理员兜底计算理论审批人,再对比 OA 已归档的企微审批详情实际审批链。企微审批详情上游 Secret 来源固定 WECOM_AGENT_SECRET,不尝试其他 Secret、无 fallback、不允许运行时猜 Secret;缺 Secret、权限、可信 IP、模板、可见范围或数据缺口时记录 NEEDS_CONFIG 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。","1) 新合同使用 WRULE1-0..WRULE1-7;2) 默认只读 OA DB,不写 OA、不写企业微信、不创建模板或审批单;3) 不启用真实员工;4) 不替代 WAPPR1 人工 A/B/C/D 门禁;5) 输出结论限定 PASS_RULE_MATCH、PASS_WITH_ADMIN_FALLBACK、FAIL_APPLICANT_CAN_CHOOSE、FAIL_SELF_APPROVAL、FAIL_APPROVER_MISMATCH、PAUSED_MANUAL_CONFIRMATION 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG;6) 报告不泄露手机号、邮箱、token、Secret、EncodingAESKey、完整 userid、完整 template_id、完整 sp_no、表单值、附件、评论或原始 payload。","受影响流程:OA 组织规则 -> 企微 HR/考勤审批归档 -> 理论审批人和实际审批链校验 -> WEXT1-LIVE/WREAL1 准入;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv;node scripts/qa/check-process-contract.mjs --issue issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv --profile wecom --json。","python3|node","DONE","DONE","DONE","issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv","[]","回滚/清理:删除本 CSV 或恢复事实源即可;本合同不授权企微写入、不部署真实员工、不修改 Prisma schema。"
"WRULE1-1","只读审批链校验服务","新增校验逻辑:读取归档审批的申请人、实际审批人、OA 直属主管、业务审批角色和管理员兜底账号,计算理论审批人并输出逐单 finding。","1) 直属主管 active、已绑定企微且非本人时优先匹配直属主管;2) 无有效直属主管时允许配置的业务审批角色;3) 仍无候选时仅允许管理员兜底并标记 PASS_WITH_ADMIN_FALLBACK;4) 实际审批人为申请人时输出 FAIL_SELF_APPROVAL;5) 实际审批人不在理论审批候选中输出 FAIL_APPROVER_MISMATCH;6) WAPPR1 人工门禁 B/C 输出 FAIL_APPLICANT_CAN_CHOOSE,D 输出 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。","受影响流程:员工主管链/角色 -> 企微审批实际审批人 -> 规则一致性判定;验证步骤:cd server && npx vitest run tests/unit/services/wecomApprovalRuleValidator.test.js --reporter=verbose。","node|vitest","DONE","DONE","DONE","server/src/services/integrations/wecomApprovalRuleValidator.js|server/tests/unit/services/wecomApprovalRuleValidator.test.js","[""WRULE1-0""]","回滚/清理:删除新增服务和测试;不涉及数据库迁移或运行态写入。"
"WRULE1-2","QA-only runner 和脚本入口","新增 QA-only runner check-wecom-approval-rule-validator,默认读取 OA DB 已归档企微审批详情和组织规则,生成脱敏 summary.json;增加 npm 脚本入口。","1) 支持 --template=leave/missed_punch/outing/overtime;2) 支持 --manual-gate=<template|all>:A/B/C/D;3) 支持 --approval-role 和 --fallback-employee-no;4) 默认不调用企微 API,不读取 Secret,不写 OA/企微;5) 报告目录为 artifacts/qa-flow/wecom-approval-rule-validator-*;6) package 脚本为 check:wecom-approval-rule-validator。","受影响流程:WAPPR1 人工门禁 -> OA 归档审批读取 -> 审批链校验报告 -> 后续准入问答;验证步骤:cd server && node --check src/scripts/check-wecom-approval-rule-validator.js;cd server && npx vitest run tests/unit/scripts/check-wecom-approval-rule-validator.test.js --reporter=verbose。","node|vitest","DONE","DONE","DONE","server/src/scripts/check-wecom-approval-rule-validator.js|server/package.json|server/tests/unit/scripts/check-wecom-approval-rule-validator.test.js","[""WRULE1-1""]","回滚/清理:删除新增 runner、测试和 package 脚本;已生成 artifacts 报告可保留审计或删除本地目录。"
"WRULE1-3","WAPPR1 人工门禁闭环口径","继续沿用现有 WAPPR1 合同,明确请假、补卡、外出、加班四类模板必须人工确认 A;本 runner 只能校验已提交审批链,不能读取企微后台是否允许申请人手选审批人。","1) 四类模板 A 才放行;2) B/C/D 均阻断 WEXT1-LIVE、WREAL1 和最小完整镜像;3) runner 缺少 --manual-gate 时输出 PAUSED_MANUAL_CONFIRMATION;4) API 边界继续登记为 NOT_SUPPORTED_BY_API / NEEDS_MANUAL_GATE。","受影响流程:企微后台模板配置 -> 真机提交页确认 -> 提交后审批详情校验 -> 准入门禁;验证步骤:rg -n ""WAPPR1|A/B/C/D|申请人不能任意选择|PAUSED_MANUAL_CONFIRMATION|NOT_SUPPORTED_BY_API|NEEDS_MANUAL_GATE"" docs .agents/skills/hyb-oa-delivery/SKILL.md server/src/scripts/check-wecom-approval-rule-validator.js。","rg|manual","DONE","DONE","DONE","docs/企微API能力目录.md|docs/需求-统一审批中心规约.md|docs/管理员手册.md|server/src/scripts/check-wecom-approval-rule-validator.js","[""WRULE1-2""]","回滚/清理:恢复事实源文档;不修改企微后台、不写业务数据。"
"WRULE1-4","路线事实源回写","更新路线图、API 能力目录、统一审批规约、管理员手册、试运行报告、文档索引、事实源矩阵和项目 skill,固化单一组织规则、企微执行、OA 校验留存和 WRULE1 准入位置。","1) 路线改为 WAPPR1 -> WRULE1 -> WTCLOSE1/WAUTO1/WFULLSMOKE1 -> WEXT1-LIVE -> WREAL1 -> 最小完整镜像;2) 明确 OA 不维护另一套 HR/考勤审批规则;3) 明确 OA 主办域仍为报销、采购、合同、项目、付款、发票、高敏附件和经营审计;4) 应急接管、恢复写回、RuleOps 后置;5) 文档审计和 skill 校验通过。","受影响流程:事实源读取 -> 后续合同规划 -> 自动/人工门禁;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;python3 /home/shunli/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/hyb-oa-delivery;rg -n ""WRULE1|单一组织规则|理论审批人|实际审批链|PASS_RULE_MATCH"" docs .agents/skills/hyb-oa-delivery/SKILL.md。","node|python3|rg","DONE","DONE","DONE","docs/企微优先开发路线图.md|docs/企微API能力目录.md|docs/需求-统一审批中心规约.md|docs/管理员手册.md|docs/trialrun/人工试运行报告.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|.agents/skills/hyb-oa-delivery/SKILL.md","[""WRULE1-3""]","回滚/清理:恢复本轮文档和 skill 改动;不修改历史 Issue 快照。"
"WRULE1-5","本地门禁和隐私核对","执行合同、语法、单测、文档、最小门禁和隐私核对,确认报告不泄露完整外部键或敏感字段。","1) validate_issues_csv 通过;2) process baseline 和 WRULE1 wecom profile 通过;3) node --check 通过;4) focused vitest 通过;5) prisma validate 通过;6) audit-docs、quick_validate、git diff --check、scripts/check-minimal.sh 通过;7) 报告隐私边界通过。","受影响流程:本地实现 -> 自动验证 -> 交付准入;验证步骤:node scripts/qa/check-process-contract.mjs --baseline --json;cd server && npm run prisma:validate;cd server && npx vitest run tests/unit/services/wecomApprovalRuleValidator.test.js tests/unit/scripts/check-wecom-approval-rule-validator.test.js --reporter=verbose;git diff --check;bash scripts/check-minimal.sh。","python3|node|npm|git|bash","DONE","DONE","DONE","server/src/services/integrations/wecomApprovalRuleValidator.js|server/src/scripts/check-wecom-approval-rule-validator.js|server/tests/unit","[""WRULE1-4""]","回滚/清理:测试失败时不提交;本轮不部署、不写远端 env、不写企微。"
"WRULE1-6","只读 reviewer subagent 审查","审批规则属于高风险治理项,实施后另开只读 reviewer subagent 审查,不允许修改代码,重点检查双规则源、权限泄漏、准入误判和漏测项。","1) reviewer 只读;2) 审查单一组织规则 + 企微执行 + OA 校验留存是否会形成双事实源;3) 审查 B/C/D、self approval、mismatch 是否阻断真实试用;4) 审查 findings 和隐私边界;5) 如有 blocker 必须修复或记录。","受影响流程:实现完成 -> 独立评审 -> 状态回填;验证步骤:评审 subagent 输出 review 结论;人工核对无 blocker 或 blocker 已处理。","subagent|manual","DONE","DONE","DONE","N/A","[""WRULE1-5""]","回滚/清理:reviewer 不修改文件;如发现高风险,暂停 WEXT1-LIVE/WREAL1。"
"WRULE1-7","状态回填和中文提交","按测试和 reviewer 结果回填 WRULE1 状态,完成中文提交。","1) 所有 WRULE1 行按通过结果回填 DONE;2) git status 仅包含本轮相关改动;3) 中文提交包含 WRULE1;4) final 说明改动、验证、未部署状态和下一步人工门禁。","受影响流程:合同 -> 实施 -> 验证 -> review -> 提交 -> 后续问答;验证步骤:git status --short;git log -1 --oneline。","git|manual","DONE","DONE","DONE","issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv|N/A","[""WRULE1-6""]","回滚/清理:如提交前发现问题,恢复本轮新增文件和文档;本轮不部署、不写企微、不修改远端环境。提交建议:WRULE1 实现企微审批链校验器 POC。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 WRULE1-0 审批链校验器合同建档 建立 WRULE1 企微 HR/考勤审批链校验器 POC 合同,承接单一组织规则 + 企微 HR/考勤执行 + OA 校验留存路线。OA 按员工直属主管、指定审批角色和管理员兜底计算理论审批人,再对比 OA 已归档的企微审批详情实际审批链。企微审批详情上游 Secret 来源固定 WECOM_AGENT_SECRET,不尝试其他 Secret、无 fallback、不允许运行时猜 Secret;缺 Secret、权限、可信 IP、模板、可见范围或数据缺口时记录 NEEDS_CONFIG 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。 1) 新合同使用 WRULE1-0..WRULE1-7;2) 默认只读 OA DB,不写 OA、不写企业微信、不创建模板或审批单;3) 不启用真实员工;4) 不替代 WAPPR1 人工 A/B/C/D 门禁;5) 输出结论限定 PASS_RULE_MATCH、PASS_WITH_ADMIN_FALLBACK、FAIL_APPLICANT_CAN_CHOOSE、FAIL_SELF_APPROVAL、FAIL_APPROVER_MISMATCH、PAUSED_MANUAL_CONFIRMATION 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG;6) 报告不泄露手机号、邮箱、token、Secret、EncodingAESKey、完整 userid、完整 template_id、完整 sp_no、表单值、附件、评论或原始 payload。 受影响流程:OA 组织规则 -> 企微 HR/考勤审批归档 -> 理论审批人和实际审批链校验 -> WEXT1-LIVE/WREAL1 准入;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv;node scripts/qa/check-process-contract.mjs --issue issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv --profile wecom --json。 python3|node DONE DONE DONE issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv [] 回滚/清理:删除本 CSV 或恢复事实源即可;本合同不授权企微写入、不部署真实员工、不修改 Prisma schema。
3 WRULE1-1 只读审批链校验服务 新增校验逻辑:读取归档审批的申请人、实际审批人、OA 直属主管、业务审批角色和管理员兜底账号,计算理论审批人并输出逐单 finding。 1) 直属主管 active、已绑定企微且非本人时优先匹配直属主管;2) 无有效直属主管时允许配置的业务审批角色;3) 仍无候选时仅允许管理员兜底并标记 PASS_WITH_ADMIN_FALLBACK;4) 实际审批人为申请人时输出 FAIL_SELF_APPROVAL;5) 实际审批人不在理论审批候选中输出 FAIL_APPROVER_MISMATCH;6) WAPPR1 人工门禁 B/C 输出 FAIL_APPLICANT_CAN_CHOOSE,D 输出 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。 受影响流程:员工主管链/角色 -> 企微审批实际审批人 -> 规则一致性判定;验证步骤:cd server && npx vitest run tests/unit/services/wecomApprovalRuleValidator.test.js --reporter=verbose。 node|vitest DONE DONE DONE server/src/services/integrations/wecomApprovalRuleValidator.js|server/tests/unit/services/wecomApprovalRuleValidator.test.js ["WRULE1-0"] 回滚/清理:删除新增服务和测试;不涉及数据库迁移或运行态写入。
4 WRULE1-2 QA-only runner 和脚本入口 新增 QA-only runner check-wecom-approval-rule-validator,默认读取 OA DB 已归档企微审批详情和组织规则,生成脱敏 summary.json;增加 npm 脚本入口。 1) 支持 --template=leave/missed_punch/outing/overtime;2) 支持 --manual-gate=<template|all>:A/B/C/D;3) 支持 --approval-role 和 --fallback-employee-no;4) 默认不调用企微 API,不读取 Secret,不写 OA/企微;5) 报告目录为 artifacts/qa-flow/wecom-approval-rule-validator-*;6) package 脚本为 check:wecom-approval-rule-validator。 受影响流程:WAPPR1 人工门禁 -> OA 归档审批读取 -> 审批链校验报告 -> 后续准入问答;验证步骤:cd server && node --check src/scripts/check-wecom-approval-rule-validator.js;cd server && npx vitest run tests/unit/scripts/check-wecom-approval-rule-validator.test.js --reporter=verbose。 node|vitest DONE DONE DONE server/src/scripts/check-wecom-approval-rule-validator.js|server/package.json|server/tests/unit/scripts/check-wecom-approval-rule-validator.test.js ["WRULE1-1"] 回滚/清理:删除新增 runner、测试和 package 脚本;已生成 artifacts 报告可保留审计或删除本地目录。
5 WRULE1-3 WAPPR1 人工门禁闭环口径 继续沿用现有 WAPPR1 合同,明确请假、补卡、外出、加班四类模板必须人工确认 A;本 runner 只能校验已提交审批链,不能读取企微后台是否允许申请人手选审批人。 1) 四类模板 A 才放行;2) B/C/D 均阻断 WEXT1-LIVE、WREAL1 和最小完整镜像;3) runner 缺少 --manual-gate 时输出 PAUSED_MANUAL_CONFIRMATION;4) API 边界继续登记为 NOT_SUPPORTED_BY_API / NEEDS_MANUAL_GATE。 受影响流程:企微后台模板配置 -> 真机提交页确认 -> 提交后审批详情校验 -> 准入门禁;验证步骤:rg -n "WAPPR1|A/B/C/D|申请人不能任意选择|PAUSED_MANUAL_CONFIRMATION|NOT_SUPPORTED_BY_API|NEEDS_MANUAL_GATE" docs .agents/skills/hyb-oa-delivery/SKILL.md server/src/scripts/check-wecom-approval-rule-validator.js。 rg|manual DONE DONE DONE docs/企微API能力目录.md|docs/需求-统一审批中心规约.md|docs/管理员手册.md|server/src/scripts/check-wecom-approval-rule-validator.js ["WRULE1-2"] 回滚/清理:恢复事实源文档;不修改企微后台、不写业务数据。
6 WRULE1-4 路线事实源回写 更新路线图、API 能力目录、统一审批规约、管理员手册、试运行报告、文档索引、事实源矩阵和项目 skill,固化单一组织规则、企微执行、OA 校验留存和 WRULE1 准入位置。 1) 路线改为 WAPPR1 -> WRULE1 -> WTCLOSE1/WAUTO1/WFULLSMOKE1 -> WEXT1-LIVE -> WREAL1 -> 最小完整镜像;2) 明确 OA 不维护另一套 HR/考勤审批规则;3) 明确 OA 主办域仍为报销、采购、合同、项目、付款、发票、高敏附件和经营审计;4) 应急接管、恢复写回、RuleOps 后置;5) 文档审计和 skill 校验通过。 受影响流程:事实源读取 -> 后续合同规划 -> 自动/人工门禁;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;python3 /home/shunli/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/hyb-oa-delivery;rg -n "WRULE1|单一组织规则|理论审批人|实际审批链|PASS_RULE_MATCH" docs .agents/skills/hyb-oa-delivery/SKILL.md。 node|python3|rg DONE DONE DONE docs/企微优先开发路线图.md|docs/企微API能力目录.md|docs/需求-统一审批中心规约.md|docs/管理员手册.md|docs/trialrun/人工试运行报告.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|.agents/skills/hyb-oa-delivery/SKILL.md ["WRULE1-3"] 回滚/清理:恢复本轮文档和 skill 改动;不修改历史 Issue 快照。
7 WRULE1-5 本地门禁和隐私核对 执行合同、语法、单测、文档、最小门禁和隐私核对,确认报告不泄露完整外部键或敏感字段。 1) validate_issues_csv 通过;2) process baseline 和 WRULE1 wecom profile 通过;3) node --check 通过;4) focused vitest 通过;5) prisma validate 通过;6) audit-docs、quick_validate、git diff --check、scripts/check-minimal.sh 通过;7) 报告隐私边界通过。 受影响流程:本地实现 -> 自动验证 -> 交付准入;验证步骤:node scripts/qa/check-process-contract.mjs --baseline --json;cd server && npm run prisma:validate;cd server && npx vitest run tests/unit/services/wecomApprovalRuleValidator.test.js tests/unit/scripts/check-wecom-approval-rule-validator.test.js --reporter=verbose;git diff --check;bash scripts/check-minimal.sh。 python3|node|npm|git|bash DONE DONE DONE server/src/services/integrations/wecomApprovalRuleValidator.js|server/src/scripts/check-wecom-approval-rule-validator.js|server/tests/unit ["WRULE1-4"] 回滚/清理:测试失败时不提交;本轮不部署、不写远端 env、不写企微。
8 WRULE1-6 只读 reviewer subagent 审查 审批规则属于高风险治理项,实施后另开只读 reviewer subagent 审查,不允许修改代码,重点检查双规则源、权限泄漏、准入误判和漏测项。 1) reviewer 只读;2) 审查单一组织规则 + 企微执行 + OA 校验留存是否会形成双事实源;3) 审查 B/C/D、self approval、mismatch 是否阻断真实试用;4) 审查 findings 和隐私边界;5) 如有 blocker 必须修复或记录。 受影响流程:实现完成 -> 独立评审 -> 状态回填;验证步骤:评审 subagent 输出 review 结论;人工核对无 blocker 或 blocker 已处理。 subagent|manual DONE DONE DONE N/A ["WRULE1-5"] 回滚/清理:reviewer 不修改文件;如发现高风险,暂停 WEXT1-LIVE/WREAL1。
9 WRULE1-7 状态回填和中文提交 按测试和 reviewer 结果回填 WRULE1 状态,完成中文提交。 1) 所有 WRULE1 行按通过结果回填 DONE;2) git status 仅包含本轮相关改动;3) 中文提交包含 WRULE1;4) final 说明改动、验证、未部署状态和下一步人工门禁。 受影响流程:合同 -> 实施 -> 验证 -> review -> 提交 -> 后续问答;验证步骤:git status --short;git log -1 --oneline。 git|manual DONE DONE DONE issues/2026-05-20_23-56-15-wecom-hr-approval-rule-validator-v1.csv|N/A ["WRULE1-6"] 回滚/清理:如提交前发现问题,恢复本轮新增文件和文档;本轮不部署、不写企微、不修改远端环境。提交建议:WRULE1 实现企微审批链校验器 POC。
+1
查看文件
@@ -34,6 +34,7 @@
"check:wecom-approval-compensation-poc": "node src/scripts/check-wecom-approval-compensation-poc.js",
"check:wecom-approval-archive": "node src/scripts/check-wecom-approval-archive.js",
"check:wecom-approval-readability-gate": "node src/scripts/check-wecom-approval-readability-gate.js",
"check:wecom-approval-rule-validator": "node src/scripts/check-wecom-approval-rule-validator.js",
"check:wecom-approval-poc": "node src/scripts/check-wecom-approval-poc.js",
"check:wecom-checkin-summary-archive": "node src/scripts/check-wecom-checkin-summary-archive.js",
"check:wecom-runtime-ready": "node src/scripts/check-wecom-runtime-ready.js",
@@ -0,0 +1,454 @@
#!/usr/bin/env node
require("dotenv/config");
const fs = require("node:fs");
const path = require("node:path");
const { createPrismaClient } = require("../prismaClient");
const {
APPROVAL_RULE_TARGETS,
countBy,
evaluateRuleReport,
normalizeManualGates,
validateApprovalRuleArchive,
} = require("../services/integrations/wecomApprovalRuleValidator");
const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow");
const DEFAULT_FALLBACK_EMPLOYEE_NO = "HYD000006";
const DEFAULT_APPROVAL_ROLES = Object.freeze(["manager"]);
const REPORT_PRIVACY =
"rule status/counts/masked sp_no/template/employee 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-rule-validator.js [options]
QA-only 企微 HR/考勤审批链校验器 POC。
默认只读取 OA DB 已归档的企微审批详情,不调用企业微信 API,不写 OA/企微数据。
Options:
--format=text|json 输出格式,默认 text。
--report-dir=<path> 报告目录,默认 artifacts/qa-flow/wecom-approval-rule-validator-<timestamp>。
--template=leave 检查请假。
--template=missed_punch 检查补卡。
--template=outing 检查外出。
--template=overtime 检查加班。
--manual-gate=leave:A WAPPR1 人工门禁,A/B/C/D;支持 --manual-gate=all:A。
--approval-role=manager 无有效直属主管时允许的业务审批角色,默认 manager;可重复。
--fallback-employee-no=<no> 管理员兜底员工号,默认 HYD000006。
--status=<status> 按企微归档规范化状态过滤。
--limit=<1..100> 最大读取条数,默认 50。
--help 显示帮助。
Secret 固定规则:
本脚本默认只读 OA DB,不读取 Secret、不调用企微。
上游企微审批详情读取仍固定 WECOM_AGENT_SECRET,不使用通讯录、打卡或历史 HR assistant Secret 兜底。
隐私边界:
报告只输出结论、计数、脱敏 sp_no/template/employee 前缀和 finding 类型。
不输出手机号、邮箱、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 parseManualGate(value) {
const raw = toSafeString(value, 80);
const separatorIndex = raw.indexOf(":");
if (separatorIndex <= 0) {
throw new Error("--manual-gate requires <template|all>:<A|B|C|D>");
}
const target = raw.slice(0, separatorIndex).trim();
const gate = raw.slice(separatorIndex + 1).trim().toUpperCase();
if (![...APPROVAL_RULE_TARGETS, "all"].includes(target)) {
throw new Error(`Unsupported manual gate target: ${target}`);
}
if (!["A", "B", "C", "D"].includes(gate)) {
throw new Error(`Unsupported manual gate value: ${gate}`);
}
return { target, gate };
}
function parseArgs(argv) {
const options = {
format: "text",
reportDir: path.join(DEFAULT_REPORT_ROOT, `wecom-approval-rule-validator-${timestamp()}`),
templates: [],
manualGates: {},
approvalRoles: [],
fallbackEmployeeNo: DEFAULT_FALLBACK_EMPLOYEE_NO,
status: "",
limit: 50,
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 === "--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 if (arg === "--template") {
options.templates.push(toSafeString(argv[index + 1], 40));
index += 1;
} else if (arg.startsWith("--template=")) {
options.templates.push(toSafeString(arg.slice("--template=".length), 40));
} else if (arg === "--manual-gate") {
const { target, gate } = parseManualGate(argv[index + 1]);
if (target === "all") {
for (const template of APPROVAL_RULE_TARGETS) options.manualGates[template] = gate;
} else {
options.manualGates[target] = gate;
}
index += 1;
} else if (arg.startsWith("--manual-gate=")) {
const { target, gate } = parseManualGate(arg.slice("--manual-gate=".length));
if (target === "all") {
for (const template of APPROVAL_RULE_TARGETS) options.manualGates[template] = gate;
} else {
options.manualGates[target] = gate;
}
} else if (arg === "--approval-role") {
options.approvalRoles.push(toSafeString(argv[index + 1], 40));
index += 1;
} else if (arg.startsWith("--approval-role=")) {
options.approvalRoles.push(toSafeString(arg.slice("--approval-role=".length), 40));
} else if (arg === "--fallback-employee-no") {
options.fallbackEmployeeNo = toSafeString(argv[index + 1], 80);
index += 1;
} else if (arg.startsWith("--fallback-employee-no=")) {
options.fallbackEmployeeNo = toSafeString(arg.slice("--fallback-employee-no=".length), 80);
} else if (arg === "--status") {
options.status = toSafeString(argv[index + 1], 40);
index += 1;
} else if (arg.startsWith("--status=")) {
options.status = toSafeString(arg.slice("--status=".length), 40);
} else if (arg === "--limit") {
options.limit = Number(argv[index + 1] || options.limit);
index += 1;
} else if (arg.startsWith("--limit=")) {
options.limit = Number(arg.slice("--limit=".length));
} else {
throw new Error(`Unsupported argument: ${arg}`);
}
}
if (!["text", "json"].includes(options.format)) {
throw new Error(`Unsupported --format: ${options.format}`);
}
const uniqueTemplates = Array.from(new Set(options.templates.filter(Boolean)));
for (const template of uniqueTemplates) {
if (!APPROVAL_RULE_TARGETS.includes(template)) {
throw new Error(`Unsupported --template: ${template}`);
}
}
options.templates = uniqueTemplates.length ? uniqueTemplates : [...APPROVAL_RULE_TARGETS];
const uniqueRoles = Array.from(new Set(options.approvalRoles.filter(Boolean)));
options.approvalRoles = uniqueRoles.length ? uniqueRoles : [...DEFAULT_APPROVAL_ROLES];
if (!options.fallbackEmployeeNo) {
throw new Error("--fallback-employee-no requires non-empty value");
}
if (!Number.isFinite(options.limit) || options.limit < 1 || options.limit > 100) {
throw new Error("--limit must be between 1 and 100");
}
options.limit = Math.floor(options.limit);
options.manualGates = normalizeManualGates(options.manualGates);
return options;
}
function buildArchiveWhere(options) {
const where = { template_type: { in: options.templates } };
if (options.status) where.normalized_status = options.status;
return where;
}
const EMPLOYEE_SELECT = {
id: true,
employee_no: true,
status: true,
roles: { select: { role_code: true } },
wecom_identity_binding: { select: { external_user_id: true } },
};
async function readApprovalRuleInputs(prisma, options) {
const [archives, fallbackEmployee, approvalRoleEmployees] = await Promise.all([
prisma.wecomApprovalArchive.findMany({
where: buildArchiveWhere(options),
orderBy: [{ status_observed_at: "desc" }, { updated_at: "desc" }],
take: options.limit,
select: {
id: true,
sp_no: true,
template_id: true,
template_type: true,
normalized_status: true,
status_observed_at: true,
applicant: {
select: {
...EMPLOYEE_SELECT,
direct_manager_id: true,
direct_manager: { select: EMPLOYEE_SELECT },
},
},
participants: {
where: { role: "approver" },
orderBy: [{ node_index: "asc" }, { created_at: "asc" }],
select: {
role: true,
node_index: true,
employee: { select: EMPLOYEE_SELECT },
},
},
actions: {
orderBy: [{ action_time: "asc" }, { created_at: "asc" }],
select: {
action_type: true,
node_index: true,
operator: { select: EMPLOYEE_SELECT },
},
},
},
}),
prisma.employee.findUnique({
where: { employee_no: options.fallbackEmployeeNo },
select: EMPLOYEE_SELECT,
}).catch(() => null),
prisma.employee.findMany({
where: {
status: "active",
roles: { some: { role_code: { in: options.approvalRoles } } },
},
select: EMPLOYEE_SELECT,
}),
]);
return { archives, fallbackEmployee, approvalRoleEmployees };
}
function manualGateGapsForTemplates(options) {
return manualGateFindingsForTemplates(options)
.filter((item) => item.status === "PAUSED_MANUAL_CONFIRMATION");
}
function manualGateFindingsForTemplates(options) {
return options.templates
.map((template) => {
const gate = options.manualGates[template];
if (!gate) {
return {
templateType: template,
status: "PAUSED_MANUAL_CONFIRMATION",
reason: "manual_wappr1_gate_missing",
};
}
if (gate === "D") {
return {
templateType: template,
status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
reason: "manual_wappr1_template_not_visible_or_not_configured",
};
}
if (gate === "B" || gate === "C") {
return {
templateType: template,
status: "FAIL_APPLICANT_CAN_CHOOSE",
reason: gate === "B"
? "manual_wappr1_approver_can_be_changed"
: "manual_wappr1_approver_must_be_chosen_by_applicant",
};
}
return null;
})
.filter(Boolean);
}
function templateDataGapsForTemplates(options, archives) {
const presentTemplates = new Set((archives || []).map((archive) => archive?.template_type).filter(Boolean));
return options.templates
.filter((template) => !presentTemplates.has(template))
.map((template) => ({
templateType: template,
status: options.manualGates[template] ? "SKIP/BLOCKED_BY_EXTERNAL_CONFIG" : "PAUSED_MANUAL_CONFIRMATION",
reason: "no_wecom_approval_archive_rows_for_template_scope",
}));
}
async function executeApprovalRuleValidator(options, deps = {}) {
const generatedAt = deps.generatedAt || new Date().toISOString();
const env = deps.env || process.env;
let ownedPrisma = null;
let ownedPool = null;
let readError = null;
let rawInputs = { archives: [], fallbackEmployee: null, approvalRoleEmployees: [] };
const mainBlockers = [];
try {
let prisma = deps.prisma;
if (!prisma) {
if (!env.DATABASE_URL) throw new Error("DATABASE_URL is required for approval rule validation");
const created = deps.createPrismaClientImpl ? deps.createPrismaClientImpl() : createPrismaClient();
ownedPrisma = created.prisma;
ownedPool = created.pool;
prisma = ownedPrisma;
}
rawInputs = await (deps.readApprovalRuleInputsImpl || readApprovalRuleInputs)(prisma, options);
} catch (error) {
readError = error;
mainBlockers.push({
id: "approval_rule_db_read",
reason: "oa_archive_or_org_rule_read_failed",
errorCode: toSafeString(error?.code || "unknown", 40) || "unknown",
message: safeMessage(error?.message || error),
});
} finally {
if (ownedPrisma?.$disconnect) await ownedPrisma.$disconnect().catch(() => null);
if (ownedPool?.end) await ownedPool.end().catch(() => null);
}
const manualGateFindings = manualGateFindingsForTemplates(options);
const manualGateGaps = manualGateFindings.filter((item) => item.status === "PAUSED_MANUAL_CONFIRMATION");
const templateDataGaps = templateDataGapsForTemplates(options, rawInputs.archives || []);
const items = (rawInputs.archives || []).map((archive) => validateApprovalRuleArchive(archive, {
manualGates: options.manualGates,
fallbackEmployee: rawInputs.fallbackEmployee,
approvalRoleEmployees: rawInputs.approvalRoleEmployees,
}));
const noData = !readError && !items.length;
if (noData) {
mainBlockers.push({
id: "approval_archive_data",
reason: "no_wecom_approval_archive_rows_for_rule_scope",
});
}
const status = evaluateRuleReport(items, {
readError,
manualGateFindings,
manualGateGaps,
templateDataGaps,
noData,
});
return {
mode: "qa_only",
generatedAt,
status,
conclusion: status,
privacy: REPORT_PRIVACY,
scope: {
issue: "WRULE1",
source: "OA_ARCHIVED_WECOM_APPROVAL_DETAIL",
writeBehavior: "read-only OA DB; no OA writes, no WeCom writes",
wecomApiCalls: false,
upstreamWecomSecret: "WECOM_AGENT_SECRET",
noSecretFallback: true,
targets: options.templates,
realEmployees: "not_enabled",
outingOvertimeLive: "not_authorized",
nextStage: "WAPPR1_A_then_WEXT1_LIVE_then_WREAL1",
},
filters: {
templates: options.templates,
status: options.status || null,
limit: options.limit,
approvalRoles: options.approvalRoles,
fallbackEmployeeNoPrefix: maskSimple(options.fallbackEmployeeNo),
manualGates: options.manualGates,
},
checks: {
ruleValidation: {
status,
count: items.length,
statusCounts: countBy(items, (item) => item.status),
templateCounts: countBy(items, (item) => item.templateType),
ruleModeCounts: countBy(items, (item) => item.ruleMode),
},
manualGateFindings,
manualGateGaps,
templateDataGaps,
items,
},
mainBlockers,
};
}
function maskSimple(value) {
const raw = toSafeString(value, 80);
if (!raw) return null;
if (raw.length <= 4) return `${raw.slice(0, 1)}***`;
return `${raw.slice(0, 3)}***${raw.slice(-2)}`;
}
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-rule-validator] status=${report.status} conclusion=${report.conclusion}\n`);
process.stdout.write(`[wecom-approval-rule-validator] report=${reportPath}\n`);
process.stdout.write(`[wecom-approval-rule-validator] privacy=${report.privacy}\n`);
process.stdout.write(`[wecom-approval-rule-validator] targets=${report.scope.targets.join(",")} count=${report.checks.ruleValidation.count}\n`);
if (report.checks.manualGateGaps?.length) {
process.stdout.write(`[wecom-approval-rule-validator] manualGateGaps=${JSON.stringify(report.checks.manualGateGaps)}\n`);
}
if (report.mainBlockers?.length) {
process.stdout.write(`[wecom-approval-rule-validator] mainBlockers=${JSON.stringify(report.mainBlockers)}\n`);
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
const report = await executeApprovalRuleValidator(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-rule-validator][FAIL] ${safeMessage(error?.message || error)}\n`);
process.exitCode = 1;
});
}
module.exports = {
buildArchiveWhere,
executeApprovalRuleValidator,
manualGateFindingsForTemplates,
parseArgs,
readApprovalRuleInputs,
templateDataGapsForTemplates,
};
@@ -0,0 +1,331 @@
const { maskApprovalIdentifier } = require("./wecomApproval");
const APPROVAL_RULE_TARGETS = Object.freeze(["leave", "missed_punch", "outing", "overtime"]);
const MANUAL_GATE_VALUES = Object.freeze(["A", "B", "C", "D"]);
function toSafeString(value, maxLength = 500) {
if (value == null) return "";
return String(value).trim().slice(0, maxLength);
}
function toArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function roleCodes(employee) {
return toArray(employee?.roles)
.map((item) => (typeof item === "string" ? item : item?.role_code))
.filter(Boolean);
}
function hasWecomBinding(employee) {
return Boolean(employee?.wecom_identity_binding?.external_user_id);
}
function isActiveEmployee(employee) {
return Boolean(employee && employee.status === "active");
}
function employeeSafeSummary(employee) {
if (!employee) return null;
return {
employeeNoPrefix: maskEmployeeIdentifier(employee.employee_no),
status: employee.status || "unknown",
roleCodes: roleCodes(employee),
hasWecomBinding: hasWecomBinding(employee),
};
}
function maskEmployeeIdentifier(value) {
const raw = toSafeString(value, 80);
if (!raw) return null;
if (raw.length <= 4) return `${raw.slice(0, 1)}***`;
return `${raw.slice(0, 3)}***${raw.slice(-2)}`;
}
function uniqueById(items) {
const seen = new Set();
const result = [];
for (const item of items || []) {
if (!item?.id || seen.has(item.id)) continue;
seen.add(item.id);
result.push(item);
}
return result;
}
function normalizeManualGates(rawManualGates = {}) {
const gates = {};
for (const [target, value] of Object.entries(rawManualGates || {})) {
const normalizedTarget = toSafeString(target, 40);
const gate = toSafeString(value, 4).toUpperCase();
if (!APPROVAL_RULE_TARGETS.includes(normalizedTarget) || !MANUAL_GATE_VALUES.includes(gate)) continue;
gates[normalizedTarget] = gate;
}
return gates;
}
function buildExpectedApprovers({ applicant, fallbackEmployee, approvalRoleEmployees = [] }) {
const findings = [];
const directManager = applicant?.direct_manager || null;
if (directManager && directManager.id === applicant?.id) {
findings.push({
type: "FAIL_SELF_APPROVAL",
severity: "blocker",
reason: "direct_manager_resolves_to_applicant",
});
}
if (
directManager
&& directManager.id !== applicant?.id
&& isActiveEmployee(directManager)
&& hasWecomBinding(directManager)
) {
return {
mode: "direct_manager",
expectedEmployees: [directManager],
findings,
};
}
const validRoleEmployees = uniqueById(approvalRoleEmployees.filter((employee) => (
employee.id !== applicant?.id
&& isActiveEmployee(employee)
&& hasWecomBinding(employee)
)));
if (validRoleEmployees.length) {
return {
mode: "approval_role",
expectedEmployees: validRoleEmployees,
findings,
};
}
if (
fallbackEmployee
&& fallbackEmployee.id !== applicant?.id
&& isActiveEmployee(fallbackEmployee)
&& hasWecomBinding(fallbackEmployee)
) {
return {
mode: "admin_fallback",
expectedEmployees: [fallbackEmployee],
findings: [
...findings,
{
type: "PASS_WITH_ADMIN_FALLBACK",
severity: "warning",
reason: "direct_manager_or_role_approver_missing_or_unavailable",
},
],
};
}
return {
mode: "no_candidate",
expectedEmployees: [],
findings: [
...findings,
{
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "no_valid_direct_manager_role_or_admin_fallback",
},
],
};
}
function actualApproverEmployees(archive) {
const fromParticipants = toArray(archive?.participants)
.filter((item) => item?.role === "approver" && item.employee)
.sort((a, b) => (a.node_index || 0) - (b.node_index || 0))
.map((item) => item.employee);
if (fromParticipants.length) return uniqueById(fromParticipants);
const fromActions = toArray(archive?.actions)
.filter((item) => item?.operator && ["approve", "reject", "pending"].includes(item.action_type))
.sort((a, b) => (a.node_index || 0) - (b.node_index || 0))
.map((item) => item.operator);
return uniqueById(fromActions);
}
function manualGateFinding(templateType, gate) {
if (!gate) {
return {
type: "PAUSED_MANUAL_CONFIRMATION",
severity: "warning",
reason: "manual_wappr1_gate_missing",
templateType,
};
}
if (gate === "A") return null;
if (gate === "D") {
return {
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "manual_wappr1_template_not_visible_or_not_configured",
templateType,
};
}
return {
type: "FAIL_APPLICANT_CAN_CHOOSE",
severity: "blocker",
reason: gate === "B" ? "manual_wappr1_approver_can_be_changed" : "manual_wappr1_approver_must_be_chosen_by_applicant",
templateType,
};
}
function validateApprovalRuleArchive(archive, context = {}) {
const templateType = archive?.template_type || "unknown";
const manualGates = normalizeManualGates(context.manualGates);
const applicant = archive?.applicant || null;
const findings = [];
const manualFinding = manualGateFinding(templateType, manualGates[templateType]);
if (manualFinding) findings.push(manualFinding);
if (!applicant) {
findings.push({
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "applicant_not_mapped_to_oa_employee",
templateType,
});
}
const expected = applicant
? buildExpectedApprovers({
applicant,
fallbackEmployee: context.fallbackEmployee || null,
approvalRoleEmployees: context.approvalRoleEmployees || [],
})
: { mode: "no_applicant", expectedEmployees: [], findings: [] };
findings.push(...expected.findings);
const actualEmployees = actualApproverEmployees(archive);
if (!actualEmployees.length) {
findings.push({
type: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
severity: "blocker",
reason: "actual_approver_missing_or_unmapped",
templateType,
});
}
if (applicant && actualEmployees.some((employee) => employee.id === applicant.id)) {
findings.push({
type: "FAIL_SELF_APPROVAL",
severity: "blocker",
reason: "actual_approver_is_applicant",
templateType,
});
}
const expectedIds = new Set(expected.expectedEmployees.map((employee) => employee.id));
const actualIds = new Set(actualEmployees.map((employee) => employee.id));
const matched = [...actualIds].some((id) => expectedIds.has(id));
if (actualEmployees.length && expected.expectedEmployees.length && !matched) {
findings.push({
type: "FAIL_APPROVER_MISMATCH",
severity: "blocker",
reason: "actual_approver_not_in_oa_theoretical_approvers",
templateType,
});
}
const blockerTypes = new Set(findings.filter((item) => item.severity === "blocker").map((item) => item.type));
let status = "PASS_RULE_MATCH";
if (blockerTypes.has("FAIL_SELF_APPROVAL")) {
status = "FAIL_SELF_APPROVAL";
} else if (blockerTypes.has("FAIL_APPLICANT_CAN_CHOOSE")) {
status = "FAIL_APPLICANT_CAN_CHOOSE";
} else if (blockerTypes.has("FAIL_APPROVER_MISMATCH")) {
status = "FAIL_APPROVER_MISMATCH";
} else if (blockerTypes.has("SKIP/BLOCKED_BY_EXTERNAL_CONFIG")) {
status = "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
} else if (findings.some((item) => item.type === "PAUSED_MANUAL_CONFIRMATION")) {
status = "PAUSED_MANUAL_CONFIRMATION";
} else if (findings.some((item) => item.type === "PASS_WITH_ADMIN_FALLBACK")) {
status = "PASS_WITH_ADMIN_FALLBACK";
}
return {
archiveId: archive?.id || null,
spNoPrefix: maskApprovalIdentifier(archive?.sp_no),
templateIdPrefix: maskApprovalIdentifier(archive?.template_id),
templateType,
normalizedStatus: archive?.normalized_status || "unknown",
ruleMode: expected.mode,
status,
manualGate: manualGates[templateType] || null,
applicant: employeeSafeSummary(applicant),
expectedApprovers: expected.expectedEmployees.map(employeeSafeSummary),
actualApprovers: actualEmployees.map(employeeSafeSummary),
findings: findings.map((item) => ({
type: item.type,
severity: item.severity,
reason: item.reason,
templateType: item.templateType || templateType,
})),
};
}
function countBy(items, selector) {
const counts = {};
for (const item of items || []) {
const key = toSafeString(selector(item), 100) || "unknown";
counts[key] = (counts[key] || 0) + 1;
}
return counts;
}
function evaluateRuleReport(items, {
readError = null,
manualGateFindings = [],
manualGateGaps = [],
templateDataGaps = [],
noData = false,
} = {}) {
if (readError) return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
if (items.some((item) => item.status === "FAIL_SELF_APPROVAL")) return "FAIL_SELF_APPROVAL";
if (
items.some((item) => item.status === "FAIL_APPLICANT_CAN_CHOOSE")
|| manualGateFindings.some((item) => item.status === "FAIL_APPLICANT_CAN_CHOOSE")
) {
return "FAIL_APPLICANT_CAN_CHOOSE";
}
if (items.some((item) => item.status === "FAIL_APPROVER_MISMATCH")) return "FAIL_APPROVER_MISMATCH";
if (
items.some((item) => item.status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG")
|| manualGateFindings.some((item) => item.status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG")
) {
return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
}
if (
manualGateGaps.length
|| items.some((item) => item.status === "PAUSED_MANUAL_CONFIRMATION")
|| templateDataGaps.some((item) => item.status === "PAUSED_MANUAL_CONFIRMATION")
) {
return "PAUSED_MANUAL_CONFIRMATION";
}
if (templateDataGaps.some((item) => item.status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG")) {
return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
}
if (noData) return "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
if (items.some((item) => item.status === "PASS_WITH_ADMIN_FALLBACK")) return "PASS_WITH_ADMIN_FALLBACK";
return "PASS_RULE_MATCH";
}
module.exports = {
APPROVAL_RULE_TARGETS,
MANUAL_GATE_VALUES,
actualApproverEmployees,
buildExpectedApprovers,
countBy,
employeeSafeSummary,
evaluateRuleReport,
maskEmployeeIdentifier,
normalizeManualGates,
validateApprovalRuleArchive,
};
@@ -0,0 +1,188 @@
const {
executeApprovalRuleValidator,
parseArgs,
} = require("../../../src/scripts/check-wecom-approval-rule-validator");
function employee(id, overrides = {}) {
const { roles = ["employee"], ...rest } = overrides;
return {
id,
employee_no: rest.employee_no || `EMP-${id}`,
status: rest.status || "active",
roles: roles.map((role_code) => ({ role_code })),
wecom_identity_binding: rest.bound === false ? null : { external_user_id: `wecom-${id}` },
direct_manager: rest.direct_manager || null,
...rest,
};
}
function archiveRow() {
const manager = employee("manager", { employee_no: "SMOKE101", roles: ["manager"] });
return {
id: "archive-1",
sp_no: "SP_SCRIPT_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK",
template_id: "TPL_SCRIPT_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK",
template_type: "leave",
normalized_status: "approved",
applicant: employee("applicant", {
employee_no: "SMOKE002",
direct_manager: manager,
}),
participants: [{
role: "approver",
node_index: 1,
employee: manager,
}],
actions: [],
};
}
describe("check-wecom-approval-rule-validator", () => {
it("parses templates, manual gates and rule options", () => {
const options = parseArgs([
"--format=json",
"--template=leave",
"--manual-gate=all:A",
"--approval-role=hr",
"--fallback-employee-no=HYD000006",
"--limit=20",
]);
expect(options).toMatchObject({
format: "json",
templates: ["leave"],
manualGates: {
leave: "A",
missed_punch: "A",
outing: "A",
overtime: "A",
},
approvalRoles: ["hr"],
fallbackEmployeeNo: "HYD000006",
limit: 20,
});
expect(() => parseArgs(["--template=expense"])).toThrow(/Unsupported --template/);
expect(() => parseArgs(["--manual-gate=leave:E"])).toThrow(/Unsupported manual gate value/);
});
it("returns PASS_RULE_MATCH when archived WeCom approver matches OA rule", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--manual-gate=leave:A",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("PASS_RULE_MATCH");
expect(report.scope.wecomApiCalls).toBe(false);
expect(report.checks.ruleValidation.count).toBe(1);
});
it("pauses when WAPPR1 manual gates are missing", async () => {
const report = await executeApprovalRuleValidator(parseArgs(["--template=leave"]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("PAUSED_MANUAL_CONFIRMATION");
expect(report.checks.manualGateGaps).toHaveLength(1);
});
it("blocks when a selected template has no archived approval rows", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--template=outing",
"--manual-gate=leave:A",
"--manual-gate=outing:A",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(report.checks.templateDataGaps).toEqual([{
templateType: "outing",
status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
reason: "no_wecom_approval_archive_rows_for_template_scope",
}]);
});
it("blocks applicant-chosen manual gates even when that template has no archive rows", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--template=outing",
"--manual-gate=leave:A",
"--manual-gate=outing:B",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("FAIL_APPLICANT_CAN_CHOOSE");
expect(report.checks.manualGateFindings).toContainEqual({
templateType: "outing",
status: "FAIL_APPLICANT_CAN_CHOOSE",
reason: "manual_wappr1_approver_can_be_changed",
});
});
it("classifies manual D gates as external config blockers", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--manual-gate=leave:D",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(report.checks.manualGateFindings[0]).toMatchObject({
templateType: "leave",
status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
});
});
it("classifies DB read failures as external blockers without leaking long secrets", async () => {
const report = await executeApprovalRuleValidator(parseArgs(["--template=leave", "--manual-gate=leave:A"]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => {
throw new Error("database down secret-like-token-123456789012345678901234567890");
}),
});
expect(report.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(report.mainBlockers[0]).toMatchObject({
id: "approval_rule_db_read",
reason: "oa_archive_or_org_rule_read_failed",
errorCode: "unknown",
});
expect(JSON.stringify(report)).not.toContain("secret-like-token-123456789012345678901234567890");
});
});
@@ -0,0 +1,178 @@
const {
evaluateRuleReport,
validateApprovalRuleArchive,
} = require("../../../src/services/integrations/wecomApprovalRuleValidator");
const SENSITIVE_SP_NO = "SP_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK";
const SENSITIVE_TEMPLATE_ID = "TPL_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK";
function employee(id, overrides = {}) {
const { roles = ["employee"], ...rest } = overrides;
return {
id,
employee_no: rest.employee_no || `EMP-${id}`,
status: rest.status || "active",
roles: roles.map((role_code) => ({ role_code })),
wecom_identity_binding: rest.bound === false ? null : { external_user_id: `wecom-${id}-secret-userid` },
direct_manager: rest.direct_manager || null,
...rest,
};
}
function archive(overrides = {}) {
const manager = employee("manager", { employee_no: "SMOKE101", roles: ["manager"] });
const applicant = employee("applicant", {
employee_no: "SMOKE002",
direct_manager: manager,
});
return {
id: "archive-1",
sp_no: SENSITIVE_SP_NO,
template_id: SENSITIVE_TEMPLATE_ID,
template_type: "leave",
normalized_status: "approved",
applicant,
participants: [{
role: "approver",
node_index: 1,
employee: manager,
}],
actions: [],
...overrides,
};
}
describe("wecomApprovalRuleValidator", () => {
it("passes when actual approver matches OA direct manager", () => {
const result = validateApprovalRuleArchive(archive(), { manualGates: { leave: "A" } });
expect(result.status).toBe("PASS_RULE_MATCH");
expect(result.ruleMode).toBe("direct_manager");
expect(result.findings).toEqual([]);
});
it("fails when applicant is the actual approver", () => {
const row = archive();
row.participants = [{ role: "approver", node_index: 1, employee: row.applicant }];
const result = validateApprovalRuleArchive(row, { manualGates: { leave: "A" } });
expect(result.status).toBe("FAIL_SELF_APPROVAL");
expect(result.findings.map((item) => item.type)).toContain("FAIL_SELF_APPROVAL");
});
it("fails when manual WAPPR1 gate says applicant can choose approver", () => {
const result = validateApprovalRuleArchive(archive(), { manualGates: { leave: "B" } });
expect(result.status).toBe("FAIL_APPLICANT_CAN_CHOOSE");
expect(result.findings[0]).toMatchObject({
type: "FAIL_APPLICANT_CAN_CHOOSE",
reason: "manual_wappr1_approver_can_be_changed",
});
});
it("fails when WeCom actual approver does not match OA theoretical approver", () => {
const result = validateApprovalRuleArchive(archive({
participants: [{
role: "approver",
node_index: 1,
employee: employee("other", { employee_no: "OTHER001", roles: ["employee"] }),
}],
}), { manualGates: { leave: "A" } });
expect(result.status).toBe("FAIL_APPROVER_MISMATCH");
});
it("allows explainable admin fallback when no manager or approval role candidate exists", () => {
const fallback = employee("admin", { employee_no: "HYD000006", roles: ["admin"] });
const applicant = employee("applicant", {
employee_no: "SMOKE002",
direct_manager: null,
});
const result = validateApprovalRuleArchive(archive({
applicant,
participants: [{ role: "approver", node_index: 1, employee: fallback }],
}), {
manualGates: { leave: "A" },
fallbackEmployee: fallback,
approvalRoleEmployees: [],
});
expect(result.status).toBe("PASS_WITH_ADMIN_FALLBACK");
expect(result.ruleMode).toBe("admin_fallback");
});
it("pauses item status before admin fallback when manual gate is missing", () => {
const fallback = employee("admin", { employee_no: "HYD000006", roles: ["admin"] });
const applicant = employee("applicant", {
employee_no: "SMOKE002",
direct_manager: null,
});
const result = validateApprovalRuleArchive(archive({
applicant,
participants: [{ role: "approver", node_index: 1, employee: fallback }],
}), {
fallbackEmployee: fallback,
approvalRoleEmployees: [],
});
expect(result.status).toBe("PAUSED_MANUAL_CONFIRMATION");
expect(result.findings.map((item) => item.type)).toContain("PASS_WITH_ADMIN_FALLBACK");
});
it("uses approval-role approver when direct manager is unavailable", () => {
const roleApprover = employee("hr", { employee_no: "HR001", roles: ["hr"] });
const applicant = employee("applicant", {
employee_no: "SMOKE002",
direct_manager: employee("manager", { employee_no: "SMOKE101", status: "inactive", roles: ["manager"] }),
});
const result = validateApprovalRuleArchive(archive({
applicant,
participants: [{ role: "approver", node_index: 1, employee: roleApprover }],
}), {
manualGates: { leave: "A" },
approvalRoleEmployees: [roleApprover],
});
expect(result.status).toBe("PASS_RULE_MATCH");
expect(result.ruleMode).toBe("approval_role");
});
it("blocks when no approver candidate or fallback exists", () => {
const applicant = employee("applicant", {
employee_no: "SMOKE002",
direct_manager: null,
});
const result = validateApprovalRuleArchive(archive({
applicant,
participants: [{ role: "approver", node_index: 1, employee: employee("other") }],
}), { manualGates: { leave: "A" } });
expect(result.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(result.findings.map((item) => item.reason)).toContain("no_valid_direct_manager_role_or_admin_fallback");
});
it("redacts full external identifiers from public result", () => {
const result = validateApprovalRuleArchive(archive(), { manualGates: { leave: "A" } });
const serialized = JSON.stringify(result);
expect(serialized).not.toContain(SENSITIVE_SP_NO);
expect(serialized).not.toContain(SENSITIVE_TEMPLATE_ID);
expect(serialized).not.toContain("wecom-manager-secret-userid");
expect(serialized).not.toContain("SMOKE101");
});
it("prioritizes blocker conclusions in report evaluation", () => {
expect(evaluateRuleReport([{ status: "PASS_RULE_MATCH" }])).toBe("PASS_RULE_MATCH");
expect(evaluateRuleReport([{ status: "PASS_WITH_ADMIN_FALLBACK" }])).toBe("PASS_WITH_ADMIN_FALLBACK");
expect(evaluateRuleReport([{ status: "FAIL_APPROVER_MISMATCH" }, { status: "PASS_RULE_MATCH" }]))
.toBe("FAIL_APPROVER_MISMATCH");
expect(evaluateRuleReport([{ status: "PASS_RULE_MATCH" }], {
manualGateFindings: [{ status: "FAIL_APPLICANT_CAN_CHOOSE" }],
})).toBe("FAIL_APPLICANT_CAN_CHOOSE");
expect(evaluateRuleReport([{ status: "PASS_RULE_MATCH" }], {
templateDataGaps: [{ status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG" }],
})).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(evaluateRuleReport([], { noData: true })).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
});
});