OAOPS1 建立小团队真实使用稳定化合同
这个提交包含在:
@@ -24,6 +24,7 @@ Read only what is relevant:
|
||||
- WAPPR1 2026-05-21 manual gate result: the user set the test employee's direct manager to the test supervisor and confirmed on a real device that leave, missed punch, outing, and overtime all auto-fill the test supervisor and the applicant cannot delete/change/add approvers. Treat all four templates as A / `PASS_APPROVER_RULES_READY`, but still require WRULE1 actual-chain validation, WROAD2-BIZ1 business evidence, WTCLOSE2 final test-account closeout, and explicit WEXT1-FORMAL/WREAL1 question gates before any WeCom write or real-employee trial.
|
||||
- WTCLOSE2/WTCLOSE2F adds `scripts/qa/run-wecom-test-account-final-closeout.sh` as the final automatic test-account closeout before WEXT1-FORMAL. Enterprise WeChat API checks must run from the deployment server trusted IP, so use `bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote`; local direct execution is blocked by default and may only be used for syntax/static gates. The remote run reruns `/ready`, WAUTO1, WFULLSMOKE1 with `WFULLSMOKE_ALLOW_PUBLIC_WRITES=0`, outing/overtime dry-run without live flags, and WRULE1 with `--manual-gate=all:A`; it must not set WeCom write switches, create templates/approvals, enable real employees, or treat WAPPR1=A as live approval. Do not treat previous local missing-env/DB WTCLOSE2 output as a WeCom business blocker.
|
||||
- WREMOTE1/WFULLSMOKE2/WEXT1-FORMAL supersedes the earlier fixture-creation route for the next step: first deploy/sync and run WTCLOSE2 from the deployment server trusted IP, then run WFULLSMOKE2 from the deployment server with `WFULLSMOKE_ALLOW_PUBLIC_WRITES=1` when OA `TEST-SMOKE-WFULL` public test data is approved, then use existing formal WeCom outing/overtime templates by having `SMOKE002` manually submit and `SMOKE101` review details before approving. WEXT1F adds `WEXT1F_MANUAL_CONFIRMATION=SMOKE002_SUBMITTED_SMOKE101_REVIEWED bash scripts/qa/run-wecom-formal-outing-overtime-evidence.sh --remote` for the post-manual evidence run: it explicitly pulls `--target=outing --target=overtime` with `--filter-creator-employee-no=SMOKE002`, may persist only to OA archive via `WECOM_ARCHIVE_WRITE=1` + `--persist-to-db`, and then runs archive offline read + WRULE1. Missing manual confirmation must produce `PAUSED_MANUAL_CONFIRMATION` and no WeCom API calls. Public WEXT1F rerun `/app/artifacts/qa-flow/wecom-formal-outing-overtime-evidence-20260522-104521/summary.json` is `PASS_WITH_FINDINGS`: outing/overtime read/archive/rule validation work and privacy smoke pass, and both outing and overtime are archived as approved; remaining callback duplicate/history-window observations do not block the WREAL1 question gate, but do not auto-start real users without explicit approval. Do not auto-create outing/overtime OA-POC templates, auto-submit approvals, write WeCom, broadly pull formal-template records from other employees, or run WeCom API evidence locally in this route. WREAL1 uses `/tmp/hyb-oa-wreal1.env` for real employee/approver values and must not write real employee identifiers to repo docs, Issue CSV, or chat.
|
||||
- OAOPS1 is the current post-WeCom lane for OA-primary maturation: WeCom check-in, missed punch, outing, leave, and overtime are frozen for now as WeCom-owned HR/attendance with OA archive/audit. The active route returns to OA-primary real use for a 7-day small-team stabilization window. Public OA may hold real business data for reimbursement/PDF OCR, approval, procurement/expense, contract registry, projects, payment batches, bank receipt attachments, AP/accounting handoff, mobile/WeCom entry, and audit. Use `scripts/qa/run-oa-real-use-daily.sh` for daily read-only checks; real business writes are manual in public OA. Do not upload confidential contract bodies, sensitive quotes, cost detail, bank statement originals, raw invoice images, or secrets to repo docs, chat, or reports. Keep image OCR, complex BPM, budgets, ERP, bank-statement import, real payment-provider integration, WeCom HR deep mirror, emergency takeover, recovery writeback, and RuleOps frozen unless a new Issue CSV explicitly approves them.
|
||||
- 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, WRULE1, WAUTO1/WEXT1LD, WFULLSMOKE1, WTCLOSE2, WEXT1-FORMAL, 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`.
|
||||
|
||||
@@ -8,6 +8,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
|
||||
## 当前结论
|
||||
|
||||
- R6 首轮小范围试运行已通过;范围是报销、请假、采购、审批、企业微信入口/通知、财务付款/导出。
|
||||
- 2026-05-22 OAOPS1 已切换为当前主线:企微打卡、补卡、外出、请假等 HR/考勤先冻结在企业微信主办 + OA 留存/审计完成态;OA 系统进入 7 天小团队全员真实使用稳定化。公网 OA 允许录入真实业务数据,首批覆盖报销/PDF OCR、审批、采购/费用、合同台账、项目、付款批次、银行回单、AP 台账、代账交接、移动/企微入口和审计;新增 `scripts/qa/run-oa-real-use-daily.sh` 做每日只读巡检。阻断即修,非阻断进入反馈池;图片 OCR、复杂 BPM、预算、ERP、银行流水导入、真实支付直连、企微 HR 深镜像、应急接管、恢复回写和 RuleOps 继续冻结。
|
||||
- PC6、PA6、FL6、AP6、RI6、LP6、ME6 自动化、QG6、DG7/MRF7、MIA7 已完成相应收口。
|
||||
- 2026-05-13 用户完成 iPhone 12、iPhone 14、Redmi K60 企业微信代表性真机主流程复测,`ME6-3` 已按主流程门禁关闭;iPhone 12 图文上下间距偏紧按非阻断观察项记录。
|
||||
- MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检;2026-05-13 已将包含 `cf2bba0` 的 MIF7T 代码部署到 `https://oa.hyinda.com`。
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
# hyb-oa 员工快速上手(10 人以下团队版)
|
||||
|
||||
**当前状态(2026-05-18 / WCP1)**:公网正式地址为 `https://oa.hyinda.com`。系统开发完成门禁结论为 `GO_TO_USE`。WFOC 已冻结新的产品方向,DBWFOC 已修正数据边界,WFDG 已固定设计门禁,WDRG 已固定后续开发路线图;WCP1 已完成企业微信能力只读 POC,当前因企业微信配置/权限缺口结论为 `PAUSE_FOR_CONFIG`,不代表页面入口或 HR/考勤流程已全部迁移完成。能用企业微信成熟能力的 HR/考勤员工日常审批优先使用企业微信,OA 做按域镜像、人工接管、恢复回写、财务闭环和审计中台;采购、合同、项目、报销和财务资料仍由 OA 主办。
|
||||
**当前状态(2026-05-22 / OAOPS1)**:公网正式地址为 `https://oa.hyinda.com`。系统开发完成门禁结论为 `GO_TO_USE`,当前进入 7 天小团队全员真实使用稳定化。企微打卡、补卡、外出、请假先按企业微信主办,OA 做留存、历史、审计和后续兜底;采购、合同、项目、报销、付款和财务资料仍由 OA 主办。OAOPS1 允许真实业务数据进入公网 OA 并按审计保留,但涉密合同正文、敏感报价、成本细节、银行流水原件和未脱敏票面不要上传到 OA、企业微信、仓库或聊天。
|
||||
|
||||
## 1. 你每周主要会用到什么
|
||||
- `Portal`:看公告、我的待办、我的资产、本周周报入口。
|
||||
@@ -43,6 +43,7 @@
|
||||
- 报销状态仍可在系统内查看,不需要额外问行政。
|
||||
- 发票/OCR 正式入口保持 PDF-only;图片发票不作为当前正式 OCR 能力,图片只按普通附件处理。
|
||||
- 如果报销已付款,系统会显示付款状态;银行实际付款由出纳在线下银行渠道完成。
|
||||
- OAOPS1 期间请用真实业务数据完成报销、采购/费用、合同台账和项目关联;遇到提交、审批、付款、导出或权限阻断时,记录页面、步骤和影响等级,让管理员当天建修复合同或暂停对应流程。
|
||||
|
||||
## 7. 入职 / 离职时你会看到什么
|
||||
- 入职时 HR 会优先在企业微信/人事助手维护人员资料,OA 后续同步生成账号、角色和绑定。
|
||||
@@ -71,11 +72,13 @@
|
||||
- 如果你是项目成员,优先在 `项目` 页面查看自己的里程碑、任务、风险和交付记录。
|
||||
- 差旅、报销、周报、待办都尽量挂到项目,这样老板不用再到多个平台拼状态。
|
||||
- 周报里可以按项目分别写本周进展、风险和下一步。
|
||||
- 采购/费用、合同台账和报销能关联项目时,优先关联项目;这样项目页能汇总真实费用、采购、合同和交付风险。
|
||||
|
||||
## 11. 涉密内容注意事项
|
||||
- 不要把涉密合同正文、涉密附件、敏感报价和成本细节上传到海音达工作台或企业微信通知里。
|
||||
- 如果系统里只要求你填合同编号、归档位置、离线档案号,这是正常设计,不是少功能。
|
||||
- 涉密资料正文继续按公司要求放在内网文件盘、NAS 或离线档案中。
|
||||
- 普通合同摘要、合同编号、负责人、项目、金额、状态和线下归档位置可以录入 OA;非涉密盖章页、验收单或归档证明可按管理员要求上传。
|
||||
|
||||
## 12. 客户沟通回填规则(R23 试点期)
|
||||
- 如果你通过私人微信、电话、会议、企业微信或现场沟通拿到了会影响项目推进的结果,请在 `24h` 内回填到海音达工作台。
|
||||
|
||||
+3
-2
@@ -108,6 +108,7 @@
|
||||
| `WTCLOSE2F` | `issues/2026-05-22_11-10-06-wecom-test-account-closeout-remote-only-v1.csv` | `CURRENT` | WTCLOSE2 远端可信 IP 执行口径修正:企微 API 只能由部署服务器访问,WTCLOSE2 真实运行必须使用 `--remote` 在 server 容器内执行;本地直接执行默认阻断,只保留静态门禁用途 |
|
||||
| `WREMOTE1` | `issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv` | `CURRENT` | 远端可信 IP 收口、WFULLSMOKE2 全域 smoke 和 WEXT1-FORMAL 正式模板外出/加班测试合同:先远端 WTCLOSE2,再远端全域 smoke;外出/加班不自动建模板,改为 `SMOKE002` 正式模板人工提交、`SMOKE101` 审批后系统只读补偿/归档/WRULE1 |
|
||||
| `WEXT1F` | `issues/2026-05-22_14-23-19-wecom-formal-outing-overtime-evidence-v1.csv` | `CURRENT` | 正式模板外出/加班证据脚本合同:`SMOKE002` 人工提交、`SMOKE101` 先看详情再审批后,使用 `WEXT1F_MANUAL_CONFIRMATION=SMOKE002_SUBMITTED_SMOKE101_REVIEWED bash scripts/qa/run-wecom-formal-outing-overtime-evidence.sh --remote` 从部署服务器只读拉取 outing/overtime;补偿拉取必须按 `creator=SMOKE002` 过滤,只写 OA 留存、离线读取和 WRULE1;不创建模板、不自动提交审批、不写企业微信 |
|
||||
| `OAOPS1` | `issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv` | `CURRENT` | 当前 OA 主办域真实使用稳定化合同:企微 HR/考勤先冻结在企微主办 + OA 留存/审计完成态;公网 OA 进入 7 天小团队全员真实使用,允许真实业务数据,覆盖报销/PDF OCR、审批、采购/费用、合同台账、项目、付款批次、银行回单、AP 台账、代账交接、移动/企微入口和审计;新增 `scripts/qa/run-oa-real-use-daily.sh` 默认只读巡检,真实业务写入由人工完成;P2 冻结项继续冻结 |
|
||||
| `WREAL1-PREP` | `issues/2026-05-20_14-35-42-wecom-real-trial-prep-v1.csv` | `CURRENT` | 真实小范围试用准备合同:只固化候选员工、非自审审批人、回滚联系人、允许流程、暂停条件和验收标准;当前不启用真实员工,真实试用必须等请假/补卡/外出/加班四类测试号证据、WAPPR1 审批规则门禁和 WFULLSMOKE1 全域主路径可解释通过后再问 |
|
||||
|
||||
旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。
|
||||
@@ -116,8 +117,8 @@
|
||||
|
||||
| Doc_ID | Path | Category | Authority | Status | Owner | Related_Issues | Evidence | Supersedes | Last_Audited | Audit_Method | Risk | Notes |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,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,WTCLOSE2,WREAL1P` | `rg` | none | 2026-05-22 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么;WAUTO1/WEXT1LD/WFULLSMOKE1/WTCLOSE2 后优先读取企微主办 + OA 自建留存路线、三天业务冒烟、审批规则规范化、审批链校验器、自动证据汇总、全域最终目标测试、测试号最终收口、受控上线巡检、告警降噪、打卡摘要、外出/加班 dry-run、WEXT1-FORMAL 前置和真实试用准备 |
|
||||
| `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,WRULE1,WAUTO1,WEXT1LD,WFULLSMOKE1,WTCLOSE2,WREAL1P` | `audit-docs` | none | 2026-05-22 | `audit-docs --check-registry` | medium | 判断文档权威性;登记当前受控上线、三天业务冒烟、审批规则规范化、审批链校验、自动证据汇总、全域最终目标测试、测试号最终收口、自动巡检、告警降噪、打卡摘要留存、外出/加班 dry-run、WEXT1-FORMAL 前置和真实试用准备事实 |
|
||||
| `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,WTCLOSE2,WREAL1P,OAOPS1` | `rg` | none | 2026-05-22 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么;OAOPS1 后优先读取 OA 主办域真实使用稳定化、合同台账、阻断即修、每日巡检和 P2 冻结口径 |
|
||||
| `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,WRULE1,WAUTO1,WEXT1LD,WFULLSMOKE1,WTCLOSE2,WREAL1P,OAOPS1` | `audit-docs` | none | 2026-05-22 | `audit-docs --check-registry` | medium | 判断文档权威性;登记当前 OAOPS1 小团队全员真实使用稳定化、合同台账、每日巡检、阻断即修和冻结项事实 |
|
||||
| `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-FORMAL、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 应急接管 |
|
||||
|
||||
+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 审批规则规范化 -> WRULE1 审批链校验器 POC -> WAUTO1 自动证据汇总 -> WEXT1LD live 前 dry-run -> WFULLSMOKE1 三天全域最终目标测试 -> WROUTE2 测试号收口到最小镜像路线锁定 -> WTCLOSE1 小团队自动审批规则与测试号全域收口 dry-run -> WTCLOSE2 测试号最终收口与证据复核 -> WTCLOSE2F 远端可信 IP 执行口径修正 -> WREMOTE1 远端可信 IP 收口 -> WFULLSMOKE2 全域 smoke -> WEXT1-FORMAL/WEXT1F 正式模板外出/加班测试 -> 四类测试号证据和审批规则门禁通过后问答 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 -> WTCLOSE2 测试号最终收口与证据复核 -> WTCLOSE2F 远端可信 IP 执行口径修正 -> WREMOTE1 远端可信 IP 收口 -> WFULLSMOKE2 全域 smoke -> WEXT1-FORMAL/WEXT1F 正式模板外出/加班测试 -> OAOPS1 小团队全员真实使用稳定化(当前主线)-> 后续再按需问答 WREAL1/最小镜像。
|
||||
|
||||
- R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。
|
||||
- ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。
|
||||
@@ -169,6 +169,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
|
||||
| `WTCLOSE2F` | `issues/2026-05-22_11-10-06-wecom-test-account-closeout-remote-only-v1.csv` | 当前 WTCLOSE2 远端可信 IP 执行口径修正 | WTCLOSE2 真实运行命令改为 `bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote`,由部署服务器 server 容器执行;本地直接执行默认拒绝跑企微链路 | 本地只做合同、语法、文档和静态门禁;此前本地缺 env/DB 的 WTCLOSE2 输出不作为企微业务阻断证据 |
|
||||
| `WREMOTE1` | `issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv` | 当前远端收口、全域 smoke 和正式模板外出/加班测试路线 | WFULLSMOKE2 必须使用 `--remote` 在部署服务器执行;外出/加班使用正式模板由 `SMOKE002` 人工提交、`SMOKE101` 审批,系统只读补偿、归档和 WRULE1 | 允许 OA `TEST-SMOKE-WFULL` 测试数据;不写企业微信、不自动创建外出/加班模板或审批单;真实员工信息只通过 `/tmp/hyb-oa-wreal1.env` 临时文件 |
|
||||
| `WEXT1F` | `issues/2026-05-22_14-23-19-wecom-formal-outing-overtime-evidence-v1.csv` | 当前正式模板外出/加班证据脚本:人工提交后从部署服务器只读收证据 | 新增 `WEXT1F_MANUAL_CONFIRMATION=SMOKE002_SUBMITTED_SMOKE101_REVIEWED bash scripts/qa/run-wecom-formal-outing-overtime-evidence.sh --remote`;显式拉取 `outing/overtime` 且按 `creator=SMOKE002` 过滤,写 OA 留存仅限 `WECOM_ARCHIVE_WRITE=1` + `--persist-to-db`;随后做离线归档读取和 WRULE1 | 不创建模板、不自动提交审批、不写企业微信、不启用真实员工;未有人工作业或缺确认标记时只可得到人工缺口结论 |
|
||||
| `OAOPS1` | `issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv` | 当前 OA 主办域真实使用稳定化:7 天小团队全员边用边修 | 新增 `scripts/qa/run-oa-real-use-daily.sh`,每日只读核验公网 ready、live smoke、feature flag、报销/PDF OCR、审批、采购/费用、合同台账、项目、付款批次、AP、移动/企微入口和隐私边界 | 允许真实业务数据进入公网 OA 并按审计保留;真实写入由人工完成;涉密合同正文、敏感报价、成本细节、银行流水原件、未脱敏票面不进仓库/聊天/报告;图片 OCR、复杂 BPM、预算、ERP、银行流水导入、真实支付直连、企微 HR 深镜像、应急接管、恢复回写、RuleOps 继续冻结 |
|
||||
| `WREAL1-PREP` | `issues/2026-05-20_14-35-42-wecom-real-trial-prep-v1.csv` | 当前真实试用准备:不启用真实员工 | 固化候选员工、非自审审批人、回滚联系人、允许流程、暂停条件和验收标准;真实试用必须等请假、补卡、外出、加班四类测试号证据、WAPPR1 和 WFULLSMOKE1 通过后再问 | 当前不创建外出/加班 live 样件;真实员工名单不写入 Issue 或文档,后续通过问答确认 |
|
||||
|
||||
旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。
|
||||
|
||||
+34
@@ -8,6 +8,7 @@
|
||||
**WRULE1 审批链校验**:管理员后台规则和真机提交页仍由 WAPPR1 人工确认;系统只能用已归档审批详情校验实际审批链。校验命令为 `npm run check:wecom-approval-rule-validator -- --template=<类型> --manual-gate=<类型>:A`,缺人工门禁时结论为 `PAUSED_MANUAL_CONFIRMATION`;自审、实际审批人不符或申请人可手选审批人均阻断 WEXT1-FORMAL、WREAL1 和最小镜像。
|
||||
**WTCLOSE2/WTCLOSE2F 测试号最终收口**:企业微信 API 已限制只能由部署服务器可信 IP 访问,因此命令必须使用 `bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote`。本地直接执行脚本会拒绝跑企微链路,只能用于 `bash -n`、合同和文档静态门禁;不得把本地缺 env/DB 的结果当作业务阻断。远端脚本复跑 WAUTO1、WFULLSMOKE1、外出/加班 dry-run 和 WRULE1,并汇总 WROAD2/WARCH3 既有报告;WFULLSMOKE1 在该脚本中关闭公网业务写流程,不新增 OA 测试业务数据;如子 runner 触发既有归档写库,只能走 WARCH3/WCHECK1 已批准的 OA 自建留存路径;脚本不写企业微信、不创建审批样件、不启用真实员工。
|
||||
**WEXT1F 正式模板外出/加班证据脚本**:管理员或 QA 只在人工测试完成后执行 `WEXT1F_MANUAL_CONFIRMATION=SMOKE002_SUBMITTED_SMOKE101_REVIEWED bash scripts/qa/run-wecom-formal-outing-overtime-evidence.sh --remote`。人工步骤固定为:`SMOKE002` 在企业微信正式外出模板提交低风险测试单,确认审批人自动带出测试主管且不能改人;再在正式加班模板提交低风险测试单并做同样确认;`SMOKE101` 必须先打开详情页再同意或驳回,不能直接点快捷同意。缺少该人工确认标记时脚本只输出 `PAUSED_MANUAL_CONFIRMATION`,不跑企微 API。脚本从部署服务器可信 IP 读取企微审批 API,不创建模板、不自动提交审批、不写企业微信;补偿拉取必须同时按 `template_id` 和 `creator=SMOKE002` 过滤,避免读取同一正式模板下其他员工单据;仅在既有 `WECOM_ARCHIVE_WRITE=1` + `--persist-to-db` 双开关下把拉到的外出/加班详情写入 OA 自建留存,然后做离线归档读取和 WRULE1 审批链校验。
|
||||
**OAOPS1 小团队真实使用稳定化**:企微打卡、补卡、外出、请假先冻结在当前完成态,日常继续由企业微信主办,OA 做留存、审计和后续兜底。当前主线回到 OA 主办域,按 7 天小团队全员真实使用稳定化执行;允许公网 OA 录入真实报销、采购/费用、合同台账、项目、付款批次和银行回单等业务数据,数据按审计保留不自动清理。新增 `scripts/qa/run-oa-real-use-daily.sh` 作为每日只读巡检;真实业务写入只由人工在公网 OA 内完成。图片 OCR、复杂 BPM、预算、ERP、银行流水导入、真实支付直连、企微 HR 深镜像、应急接管、恢复回写和 RuleOps 继续冻结。
|
||||
|
||||
## 1. 适用范围
|
||||
- 适用于 `small_team_internal` 工作区模式。
|
||||
@@ -379,6 +380,39 @@ WROAD2/WARCH3 企微受控上线每日巡检:
|
||||
|
||||
首周不主动新增可选功能。UX8 只接真实反馈或自动化确定性失败;移动采购创建、通知扩展、合同/供应商/资产、图片 OCR、真实支付、银行流水导入、复杂 BPM、审批设计器和重预算都必须另建独立合同;ERP 不再作为后续合同候选。
|
||||
|
||||
### 3.5 OAOPS1 七天真实使用稳定化
|
||||
|
||||
OAOPS1 是当前 OA 主办域稳定化主线,周期 7 天,目标是让小团队把真实业务在 OA 中跑起来,而不是继续用测试单替代日常流程。
|
||||
|
||||
每日只读巡检:
|
||||
|
||||
```bash
|
||||
bash scripts/qa/run-oa-real-use-daily.sh
|
||||
```
|
||||
|
||||
巡检默认只读,输出 `artifacts/qa-flow/oa-real-use-daily-<timestamp>/summary.json`,覆盖公网健康、live smoke、功能开关、报销/PDF OCR、审批待办、采购/费用、合同台账、项目、付款批次、AP 台账、移动/企微入口和隐私扫描。若缺少 live smoke 凭据,按 finding 或 SKIP 记录,不伪造成通过。
|
||||
|
||||
每日人工业务动作:
|
||||
|
||||
| 角色 | 动作 | 注意事项 |
|
||||
| --- | --- | --- |
|
||||
| 员工 | 提交真实报销、PDF 发票、采购/费用申请,必要时关联项目或合同 | 图片发票仍作为普通附件;不要上传密码、密钥、完整银行流水或未脱敏敏感材料 |
|
||||
| 审批人 | 从 OA 待办打开详情后审批 | 退回时写清补充材料,不用口头说明替代系统记录 |
|
||||
| 出纳/财务 | 创建付款批次、线下银行付款、上传银行回单、标记已付款、导出 AP/报销/票据资料 | OA 只记录付款闭环和证据;不接真实支付接口,不写 ERP |
|
||||
| 管理员 | 检查权限、审计日志、合同台账、项目关联和每日巡检报告 | 阻断当天建修复合同或暂停对应流程;非阻断体验问题进入反馈池 |
|
||||
|
||||
合同台账纳入首批真实使用:合同负责人可录入合同编号、合同名称、主体、金额、负责人、项目、状态和线下归档位置;非涉密盖章页、验收单、归档证明等可按权限上传并留下下载审计。涉密合同正文、敏感报价、成本细节、客户敏感资料不进入公网 OA;只记录线下归档位置或外部归档编号。
|
||||
|
||||
阻断判定:
|
||||
|
||||
- 无法登录、无法提交或审批报销/采购/合同相关事项。
|
||||
- PDF 发票 OCR、查重、绑定或状态长期卡住且无法解释。
|
||||
- 付款批次、银行回单、已付款标记、AP 导出或审计日志异常。
|
||||
- 普通员工越权查看财务、合同附件、他人审批或管理入口。
|
||||
- `payment_batch_v1` 意外关闭,或 `erp_sync`、图片 OCR、预算、银行流水导入、真实支付直连被误开启。
|
||||
|
||||
7 天结束后按自动报告和人工问题登记选择:接受 `PASS_OA_REAL_USE_READY` 进入体验/效率优化;接受 `PASS_WITH_FINDINGS` 但先处理指定 finding;或暂停真实使用扩展先整改。
|
||||
|
||||
## 4. 人员生命周期管理
|
||||
### 4.1 入职
|
||||
1. 在 `人员管理` 创建员工账号。
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
|
||||
OAOPS1-0,建立小团队全员真实使用稳定化合同,"以公网 OA 作为准生产真实使用环境,固定 7 天稳定化窗口、真实业务数据边界、阻断即修策略和冻结项。","Issue CSV 通过校验;路线明确企微 HR/考勤冻结,OA 主办报销、采购、合同、项目、付款和审计。","受影响流程:管理员确认 7 天真实使用窗口、真实数据进入 OA、P2 冻结;验证步骤:执行 validate_issues_csv.py 本文件和 check-process-contract general 门禁。","manual",DONE,DONE,DONE,"issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv","[]","不写企业微信;不启用 ERP、真实支付直连、银行流水导入、图片 OCR 或复杂 BPM。"
|
||||
OAOPS1-1,新增 OA 真实使用每日巡检脚本,"新增 QA-only 脚本聚合公网 /ready、live smoke、feature flag、报销/PDF OCR、审批、采购、合同/项目、付款批次、移动/企微入口和隐私扫描。","脚本输出 artifacts/qa-flow/oa-real-use-daily-<timestamp>/summary.json;默认只读,不创建真实业务数据。","受影响流程:每日巡检管理员先看报告再处理人工业务问题;验证步骤:执行 bash -n scripts/qa/run-oa-real-use-daily.sh;验证步骤:必要时以 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com 只读运行。","bash,node,playwright",DONE,DONE,DONE,"scripts/qa/run-oa-real-use-daily.sh","[""OAOPS1-0""]","脚本不得设置企业微信写入开关;不得自动上传真实票据、合同附件或银行回单。"
|
||||
OAOPS1-2,公网正式状态与财务开关每日核验,"每日核验公网 ready、live smoke readiness、payment_batch_v1=force_on、erp_sync=force_off,防止正式付款闭环或冻结项漂移。","巡检报告能看到 ready、live-smoke-env、feature flags 和 drift finding;payment_batch_v1 预期 force_on,erp_sync 预期 force_off。","受影响流程:管理员每日检查正式开关;验证步骤:执行 run-oa-real-use-daily.sh 并确认 feature-flags 子报告。","node,curl",TODO,TODO,TODO,"scripts/qa/run-oa-real-use-daily.sh","[""OAOPS1-1""]","若 payment_batch_v1 意外关闭或 erp_sync 意外开启,记 blocking finding 并当天建修复合同。"
|
||||
OAOPS1-3,报销 PDF OCR 与付款闭环真实使用门禁,"员工真实提交报销和 PDF 发票,审批后由出纳创建付款批次、上传银行回单并标记已付款;实际付款仍在线下银行渠道完成。","管理员手册和试运行报告写清真实数据保留、PDF-only OCR、银行回单附件、AP 导出和代账交接口径。","受影响流程:员工提交报销 -> 审批人处理 -> 出纳创建付款批次 -> 上传回单 -> 标记已付款 -> AP/报销/票据导出;验证步骤:每日巡检记录可解释状态。","manual,playwright",TODO,TODO,TODO,"docs/管理员手册.md;docs/trialrun/人工试运行报告.md","[""OAOPS1-2""]","真实票据和回单不进入仓库、聊天或自动截图报告;OCR 仍 PDF-only。"
|
||||
OAOPS1-4,采购费用与项目关联真实使用门禁,"首批真实使用覆盖采购/费用申请、审批、项目关联和必要合同关联,继续使用 OA 主办审批和脱敏企微通知。","员工快速上手和管理员手册说明采购/费用在 OA 内提交审批,项目相关事项应尽量关联项目或合同。","受影响流程:员工提交采购/费用 -> 审批通过或退回 -> 查看项目摘要和合同关联;验证步骤:人工提交一条真实采购/费用或用巡检读取列表,确认审计记录可追溯。","manual,playwright",TODO,TODO,TODO,"docs/管理员手册.md;docs/员工快速上手.md","[""OAOPS1-2""]","不做预算占用、招采、供应商生命周期或 ERP 入账。"
|
||||
OAOPS1-5,合同台账与合同归档真实使用门禁,"将合同台账、合同归档位置、合同与项目/采购/付款关联纳入 OAOPS1;涉密合同正文、敏感报价和成本细节不上传公网 OA。","事实源明确合同属于 OA 主办域;真实使用允许录入合同摘要、编号、负责人、项目、金额、线下归档位置和非涉密附件。","受影响流程:负责人建立合同台账 -> 关联项目/采购 -> 记录线下归档位置 -> 非涉密附件下载写审计;验证步骤:管理员抽查一条合同台账和权限边界。","manual",TODO,TODO,TODO,"docs/管理员手册.md;docs/员工快速上手.md","[""OAOPS1-4""]","合同正文是否上传按敏感级别判断;涉密不上传,只登记线下归档位置。"
|
||||
OAOPS1-6,每日问题登记与阻断即修机制,"7 天稳定期内,提交、审批、付款、导出、权限、登录、合同权限和移动入口阻断当天建修复合同;非阻断体验问题进入反馈池。","管理员手册包含每日收口表、阻断/非阻断分类、暂停条件和人工绕行记录要求。","受影响流程:管理员每日查看巡检报告和人工问题 -> 分类 blocking/finding/feedback;验证步骤:blocking 当天建 Issue CSV 或暂停对应流程,非阻断进入反馈池。","manual",TODO,TODO,TODO,"docs/管理员手册.md;docs/trialrun/人工试运行报告.md","[""OAOPS1-1""]","修复合同需单独 Issue CSV;不得顺手修非本轮范围问题。"
|
||||
OAOPS1-7,回写事实源与项目 skill,"回写文档索引、事实源矩阵、管理员手册、员工快速上手、人工试运行报告和 hyb-oa-delivery skill,固定 OAOPS1 当前路线。","rg 能检索到 OAOPS1、真实使用稳定化、合同台账、阻断即修、payment_batch_v1=force_on 和 P2 冻结口径。","受影响流程:后来者从文档索引和 skill 能判断当前已从企微 HR/考勤转回 OA 主办真实使用稳定化;验证步骤:执行 audit-docs 和 quick_validate。","node,python",DONE,DONE,DONE,"docs/文档索引.md;docs/文档事实源状态矩阵.md;docs/管理员手册.md;docs/员工快速上手.md;docs/trialrun/人工试运行报告.md;.agents/skills/hyb-oa-delivery/SKILL.md","[""OAOPS1-0""]","优先更新现有事实源,不新建孤立文档。"
|
||||
OAOPS1-8,OAOPS1 本地门禁与收口,"完成合同校验、脚本语法、文档审计、skill 校验、最小回归和 git diff 检查;如可用则执行公网只读巡检。","所有列出的本地门禁通过;公网只读巡检若缺 live 凭据按 SKIP/FINDING 记录,不作为代码阻断。","受影响流程:管理员可按报告开始 7 天真实使用;验证步骤:执行 validate_issues_csv.py、check-process-contract、bash -n、audit-docs、quick_validate、git diff --check、bash scripts/check-minimal.sh。","bash,node,python",DONE,DONE,DONE,"issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv;scripts/qa/run-oa-real-use-daily.sh","[""OAOPS1-1"",""OAOPS1-7""]","本轮不部署业务代码;若后续需要远端安装定时任务或公网部署,另建 OPS 合同。"
|
||||
|
+512
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u -o pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CLIENT_DIR="$ROOT_DIR/client"
|
||||
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
|
||||
REPORT_DIR="${OAOPS1_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/oa-real-use-daily-$(date +%Y%m%d-%H%M%S)}"
|
||||
SUMMARY_TXT="$REPORT_DIR/summary.txt"
|
||||
STATUS=0
|
||||
FINDING_COUNT=0
|
||||
|
||||
mkdir -p "$REPORT_DIR"
|
||||
: >"$SUMMARY_TXT"
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*" | tee -a "$SUMMARY_TXT"
|
||||
}
|
||||
|
||||
write_step_json() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local kind="$3"
|
||||
local code="${4:-0}"
|
||||
local detail="${5:-}"
|
||||
node - "$REPORT_DIR" "$name" "$status" "$kind" "$code" "$detail" <<'NODE' || true
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const [reportDir, name, status, kind, code, detail] = process.argv.slice(2);
|
||||
fs.writeFileSync(path.join(reportDir, `${name}.step.json`), `${JSON.stringify({
|
||||
name,
|
||||
status,
|
||||
kind,
|
||||
code: Number(code || 0),
|
||||
detail: String(detail || "").slice(0, 600),
|
||||
logPath: path.resolve(path.join(reportDir, `${name}.log`)),
|
||||
checkedAt: new Date().toISOString(),
|
||||
}, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local log_file="$REPORT_DIR/${name}.log"
|
||||
|
||||
log ">>> [oaops1] $name"
|
||||
if "$@" > >(tee "$log_file") 2>&1; then
|
||||
log "[oaops1][PASS] $name"
|
||||
write_step_json "$name" "PASS" "required" 0 "ok"
|
||||
else
|
||||
local code="$?"
|
||||
log "[oaops1][FAIL] $name exit=$code"
|
||||
STATUS=1
|
||||
write_step_json "$name" "FAIL" "required" "$code" "required step failed"
|
||||
fi
|
||||
}
|
||||
|
||||
run_finding_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local log_file="$REPORT_DIR/${name}.log"
|
||||
|
||||
log ">>> [oaops1] $name"
|
||||
if "$@" > >(tee "$log_file") 2>&1; then
|
||||
log "[oaops1][PASS] $name"
|
||||
write_step_json "$name" "PASS" "finding_allowed" 0 "ok"
|
||||
else
|
||||
local code="$?"
|
||||
log "[oaops1][FINDING] $name exit=$code"
|
||||
FINDING_COUNT=$((FINDING_COUNT + 1))
|
||||
write_step_json "$name" "FINDING" "finding_allowed" "$code" "finding or missing runtime credential"
|
||||
fi
|
||||
}
|
||||
|
||||
run_client_finding_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local log_file="$REPORT_DIR/${name}.log"
|
||||
|
||||
log ">>> [oaops1] $name"
|
||||
if (cd "$CLIENT_DIR" && env "$@") > >(tee "$log_file") 2>&1; then
|
||||
log "[oaops1][PASS] $name"
|
||||
write_step_json "$name" "PASS" "finding_allowed" 0 "ok"
|
||||
else
|
||||
local code="$?"
|
||||
log "[oaops1][FINDING] $name exit=$code"
|
||||
FINDING_COUNT=$((FINDING_COUNT + 1))
|
||||
write_step_json "$name" "FINDING" "finding_allowed" "$code" "finding or missing runtime credential"
|
||||
fi
|
||||
}
|
||||
|
||||
run_client_spec_if_present() {
|
||||
local name="$1"
|
||||
local spec="$2"
|
||||
shift 2
|
||||
|
||||
if [[ ! -f "$CLIENT_DIR/$spec" ]]; then
|
||||
log "[oaops1][FINDING] $name missing client spec: $spec"
|
||||
FINDING_COUNT=$((FINDING_COUNT + 1))
|
||||
write_step_json "$name" "FINDING" "finding_allowed" 0 "client spec missing: $spec"
|
||||
return 0
|
||||
fi
|
||||
|
||||
run_client_finding_step "$name" "$@" npx playwright test "$spec" --workers=1 --reporter=list
|
||||
}
|
||||
|
||||
load_default_env() {
|
||||
local env_file="${OAOPS1_ENV_FILE:-$HOME/.hyb-oa/live-smoke.env}"
|
||||
if [[ "${OAOPS1_SKIP_ENV_FILE:-0}" == "1" || ! -f "$env_file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
set +a
|
||||
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-$PUBLIC_BASE_URL}"
|
||||
log "[oaops1] loaded masked live smoke env file"
|
||||
}
|
||||
|
||||
run_ready_and_runtime() {
|
||||
run_step "public-ready" curl -fsS "$PUBLIC_BASE_URL/ready"
|
||||
run_finding_step "live-smoke-env" env LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" node "$ROOT_DIR/scripts/qa/check-live-smoke-env.mjs" --json
|
||||
run_finding_step "live-readiness" env QA_ISSUE_ID=OAOPS1 LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" LIVE_SMOKE_READINESS_DIR="$REPORT_DIR/live-smoke-readiness" bash "$ROOT_DIR/scripts/qa/run-live-smoke-readiness.sh"
|
||||
run_finding_step "live-feature-flags" env LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" node "$ROOT_DIR/scripts/qa/check-live-feature-flags.mjs" \
|
||||
--expect payment_batch_v1=force_on \
|
||||
--expect erp_sync=force_off \
|
||||
--expect attendance_v1=disabled \
|
||||
--expect attendance_unified_v1=enabled \
|
||||
--json
|
||||
}
|
||||
|
||||
run_authenticated_readonly_probe() {
|
||||
node - "$PUBLIC_BASE_URL" "$REPORT_DIR" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const [baseUrlRaw, reportDir] = process.argv.slice(2);
|
||||
const baseUrl = String(baseUrlRaw || "https://oa.hyinda.com").replace(/\/$/, "");
|
||||
|
||||
const credentials = {
|
||||
admin: {
|
||||
identifier: process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim(),
|
||||
password: process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim(),
|
||||
},
|
||||
employee: {
|
||||
identifier: process.env.LIVE_SMOKE_EMPLOYEE_IDENTIFIER?.trim(),
|
||||
password: process.env.LIVE_SMOKE_EMPLOYEE_PASSWORD?.trim(),
|
||||
},
|
||||
finance: {
|
||||
identifier: process.env.LIVE_SMOKE_FINANCE_IDENTIFIER?.trim(),
|
||||
password: process.env.LIVE_SMOKE_FINANCE_PASSWORD?.trim(),
|
||||
},
|
||||
};
|
||||
|
||||
const probes = [
|
||||
{ id: "portal", role: "admin", path: "/api/portal/home", domain: "portal" },
|
||||
{ id: "workflow-tasks", role: "admin", path: "/api/workflow/tasks?page=1&pageSize=5", domain: "approval" },
|
||||
{ id: "reimbursements", role: "admin", path: "/api/reimbursements?page=1&pageSize=5", domain: "reimbursement" },
|
||||
{ id: "reimbursement-summary", role: "admin", path: "/api/reimbursements/stats/summary", domain: "reimbursement" },
|
||||
{ id: "invoice-pool", role: "admin", path: "/api/invoice-pool?page=1&pageSize=5", domain: "invoice_ocr" },
|
||||
{ id: "purchase-requests", role: "admin", path: "/api/purchase-requests?page=1&pageSize=5", domain: "procurement" },
|
||||
{ id: "contracts-registry", role: "admin", path: "/api/contracts-registry?page=1&pageSize=5", domain: "contracts" },
|
||||
{ id: "projects", role: "admin", path: "/api/projects?page=1&pageSize=5", domain: "projects" },
|
||||
{ id: "payment-batches", role: "finance", path: "/api/payment-batches?page=1&pageSize=5", domain: "payment_batch" },
|
||||
{ id: "ap-ledger", role: "finance", path: "/api/ap-operations/reimbursement-ledger?page=1&pageSize=5", domain: "accounting_handoff" },
|
||||
{ id: "audit-logs", role: "admin", path: "/api/audit-logs?page=1&pageSize=5", domain: "audit" },
|
||||
{ id: "employee-reimbursements", role: "employee", path: "/api/reimbursements?page=1&pageSize=5&mine=1", domain: "employee_scope" },
|
||||
];
|
||||
|
||||
function maskIdentifier(value) {
|
||||
if (!value) return null;
|
||||
const text = String(value);
|
||||
if (text.length <= 4) return "***";
|
||||
return `${text.slice(0, 2)}***${text.slice(-2)}`;
|
||||
}
|
||||
|
||||
async function fetchJson(url, init = {}) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, json, text: text.slice(0, 200) };
|
||||
}
|
||||
|
||||
function summarizePayload(json) {
|
||||
const data = json?.data;
|
||||
if (Array.isArray(data)) return { itemCount: data.length };
|
||||
if (Array.isArray(data?.items)) return { itemCount: data.items.length, total: data.total ?? null };
|
||||
if (typeof data === "object" && data) {
|
||||
const summary = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (/token|secret|password|phone|email|payload|attachment|comment|form/i.test(key)) continue;
|
||||
if (typeof value === "number" || typeof value === "boolean") summary[key] = value;
|
||||
if (typeof value === "string" && value.length <= 60) summary[key] = value;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function login(role) {
|
||||
const credential = credentials[role];
|
||||
if (!credential?.identifier || !credential?.password) {
|
||||
return { status: "SKIP", reason: `missing_${role}_credentials` };
|
||||
}
|
||||
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-device-fingerprint": `oaops1-${role}-readonly`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier: credential.identifier,
|
||||
password: credential.password,
|
||||
}),
|
||||
});
|
||||
const accessToken = result.json?.data?.accessToken;
|
||||
if (!result.ok || typeof accessToken !== "string" || !accessToken) {
|
||||
return {
|
||||
status: "FAIL",
|
||||
reason: `login_http_${result.status}`,
|
||||
code: result.json?.error?.code ?? null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "PASS",
|
||||
accessToken,
|
||||
maskedIdentifier: maskIdentifier(credential.identifier),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const roles = {};
|
||||
for (const role of Object.keys(credentials)) {
|
||||
roles[role] = await login(role);
|
||||
}
|
||||
|
||||
const checks = [];
|
||||
for (const probe of probes) {
|
||||
const roleState = roles[probe.role];
|
||||
if (roleState.status !== "PASS") {
|
||||
checks.push({
|
||||
...probe,
|
||||
status: "SKIP",
|
||||
reason: roleState.reason || `${probe.role}_login_not_available`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const result = await fetchJson(`${baseUrl}${probe.path}`, {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
authorization: `Bearer ${roleState.accessToken}`,
|
||||
},
|
||||
});
|
||||
checks.push({
|
||||
...probe,
|
||||
status: result.ok ? "PASS" : "FAIL",
|
||||
httpStatus: result.status,
|
||||
summary: result.ok ? summarizePayload(result.json) : {
|
||||
code: result.json?.error?.code ?? null,
|
||||
message: typeof result.json?.error?.message === "string" ? result.json.error.message.slice(0, 100) : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const blocking = checks.filter((item) => item.status === "FAIL");
|
||||
const skipped = checks.filter((item) => item.status === "SKIP");
|
||||
const status = blocking.length ? "FAIL" : skipped.length ? "PASS_WITH_FINDINGS" : "PASS";
|
||||
const summary = {
|
||||
mode: "oaops1-authenticated-readonly-probe",
|
||||
status,
|
||||
baseUrl,
|
||||
checkedAt: new Date().toISOString(),
|
||||
readOnly: true,
|
||||
roles: Object.fromEntries(Object.entries(roles).map(([role, state]) => [role, {
|
||||
status: state.status,
|
||||
reason: state.reason || null,
|
||||
maskedIdentifier: state.maskedIdentifier || null,
|
||||
}])),
|
||||
checks,
|
||||
boundaries: {
|
||||
noBusinessWrites: true,
|
||||
noExportsByDefault: true,
|
||||
noFileUploads: true,
|
||||
noWeComWrites: true,
|
||||
noRealPaymentExecution: true,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(path.join(reportDir, "authenticated-readonly-probe.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
if (blocking.length) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[oaops1-probe][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
run_mobile_and_entry_matrix() {
|
||||
run_client_spec_if_present "mobile-entry-matrix" "tests/e2e/r124-mobile-entry-matrix.spec.ts" E2E_BASE_URL="$PUBLIC_BASE_URL"
|
||||
run_client_spec_if_present "wecom-rendering-matrix" "tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts" E2E_BASE_URL="$PUBLIC_BASE_URL"
|
||||
run_client_spec_if_present "wecom-entry" "tests/e2e/wecom-entry.spec.ts" E2E_BASE_URL="$PUBLIC_BASE_URL"
|
||||
}
|
||||
|
||||
write_manual_checklist() {
|
||||
cat >"$REPORT_DIR/manual-gates.md" <<'EOF'
|
||||
# OAOPS1 每日人工收口清单
|
||||
|
||||
## 员工
|
||||
- 真实报销:上传 PDF 发票,补充必要附件,提交 OA 审批。
|
||||
- 采购/费用:在 OA 创建申请,关联项目;如涉及合同,关联或补录合同台账。
|
||||
- 合同:只录入合同摘要、编号、负责人、项目、金额、线下归档位置;涉密合同正文、敏感报价和成本细节不上传公网 OA。
|
||||
|
||||
## 审批人
|
||||
- 从 OA 待办进入详情,确认报销、采购、合同或项目上下文后再审批。
|
||||
- 退回时写清需要补充的材料,避免线下口头说明替代系统记录。
|
||||
|
||||
## 出纳/财务
|
||||
- 审批通过后创建付款批次,线下银行渠道付款。
|
||||
- 上传银行回单附件后再在 OA 标记已付款。
|
||||
- 每日抽查 AP 台账、报销导出、票据状态和付款批次状态。
|
||||
|
||||
## 管理员
|
||||
- 查看本报告 summary.json 和人工问题登记。
|
||||
- 阻断项当天建修复 Issue CSV 或暂停对应流程;非阻断体验问题进入反馈池。
|
||||
- 确认 payment_batch_v1=force_on,erp_sync=force_off;图片 OCR、预算、ERP、银行流水导入、真实支付直连继续冻结。
|
||||
EOF
|
||||
}
|
||||
|
||||
run_privacy_smoke() {
|
||||
local sensitive_pattern='(["'\''"]?(WECOM_[A-Z0-9_]*(SECRET|TOKEN)|[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|ENCODING_AES_KEY|ENCODINGAESKEY|CORP_SECRET|ACCESS_TOKEN)[A-Z0-9_]*)["'\''"]?[[:space:]]*[:=][[:space:]]*["'\''"]?[A-Za-z0-9._:/+=-]{8,}["'\''"]?)|Authorization:[[:space:]]*Bearer[[:space:]]+[A-Za-z0-9._-]{8,}|access_token=[A-Za-z0-9._-]{8,}|1[3-9][0-9]{9}|[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}|-----BEGIN|BEGIN [A-Z ]*KEY|"((token|secret|password|EncodingAESKey|encodingAESKey|sp_no|spNo|template_id|templateId|userid|userId|external_userid|externalUserId))"[[:space:]]*:[[:space:]]*"[^"*]{8,}"|\b20[0-9]{12,}\b|"(raw|rawPayload|raw_payload|payload|xml|ciphertext|Encrypt|formValue|formValues|attachments|comments)"[[:space:]]*:'
|
||||
local matches
|
||||
matches="$(grep -RInE "$sensitive_pattern" "$REPORT_DIR" || true)"
|
||||
if [[ -n "$matches" ]]; then
|
||||
printf '%s\n' "$matches"
|
||||
log "[oaops1][FAIL] report-privacy-smoke matched sensitive pattern"
|
||||
STATUS=1
|
||||
else
|
||||
log "[oaops1][PASS] report-privacy-smoke"
|
||||
fi
|
||||
}
|
||||
|
||||
write_summary_json() {
|
||||
node - "$ROOT_DIR" "$REPORT_DIR" "$PUBLIC_BASE_URL" "$STATUS" "$FINDING_COUNT" <<'NODE' || return 1
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execSync } = require("node:child_process");
|
||||
|
||||
const [rootDir, reportDir, baseURL, statusInput, findingCountInput] = process.argv.slice(2);
|
||||
|
||||
function readJson(file) {
|
||||
if (!file || !fs.existsSync(file)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function step(name) {
|
||||
return readJson(path.join(reportDir, `${name}.step.json`)) || {
|
||||
name,
|
||||
status: "MISSING",
|
||||
kind: "unknown",
|
||||
logPath: path.resolve(path.join(reportDir, `${name}.log`)),
|
||||
};
|
||||
}
|
||||
|
||||
function child(relativePath) {
|
||||
const file = path.join(reportDir, relativePath, "summary.json");
|
||||
const json = readJson(file);
|
||||
return {
|
||||
status: json?.status || json?.conclusion || "MISSING",
|
||||
reportPath: fs.existsSync(file) ? path.resolve(file) : null,
|
||||
};
|
||||
}
|
||||
|
||||
let commit = "unknown";
|
||||
try {
|
||||
commit = execSync("git rev-parse --short HEAD", { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
||||
} catch {}
|
||||
|
||||
const stepNames = [
|
||||
"public-ready",
|
||||
"live-smoke-env",
|
||||
"live-readiness",
|
||||
"live-feature-flags",
|
||||
"authenticated-readonly-probe",
|
||||
"mobile-entry-matrix",
|
||||
"wecom-rendering-matrix",
|
||||
"wecom-entry",
|
||||
];
|
||||
const steps = Object.fromEntries(stepNames.map((name) => [name, step(name)]));
|
||||
const readonlyProbe = readJson(path.join(reportDir, "authenticated-readonly-probe.json"));
|
||||
|
||||
const findings = [];
|
||||
for (const [name, item] of Object.entries(steps)) {
|
||||
const status = String(item.status || "UNKNOWN");
|
||||
if (status === "PASS") continue;
|
||||
if (status === "FINDING") {
|
||||
findings.push({ source: name, status, severity: "finding" });
|
||||
} else if (/FAIL|ERROR|BLOCKED/i.test(status)) {
|
||||
findings.push({ source: name, status, severity: item.kind === "required" ? "blocker" : "finding" });
|
||||
} else if (/SKIP|MISSING|PAUSED|PARTIAL/i.test(status)) {
|
||||
findings.push({ source: name, status, severity: "manual_or_config" });
|
||||
}
|
||||
}
|
||||
if (readonlyProbe?.status && readonlyProbe.status !== "PASS") {
|
||||
findings.push({ source: "authenticated-readonly-probe", status: readonlyProbe.status, severity: readonlyProbe.status === "FAIL" ? "finding" : "manual_or_config" });
|
||||
}
|
||||
|
||||
let status = "PASS_OA_REAL_USE_DAILY_READY";
|
||||
if (Number(statusInput || 0) !== 0 || findings.some((item) => item.severity === "blocker")) {
|
||||
status = "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
|
||||
} else if (findings.length || Number(findingCountInput || 0) > 0) {
|
||||
status = "PASS_WITH_FINDINGS";
|
||||
}
|
||||
|
||||
const summary = {
|
||||
issueId: "OAOPS1",
|
||||
mode: "oa-real-use-daily",
|
||||
status,
|
||||
conclusion: status,
|
||||
baseURL,
|
||||
commit,
|
||||
checkedAt: new Date().toISOString(),
|
||||
reportDir: path.resolve(reportDir),
|
||||
window: {
|
||||
durationDays: 7,
|
||||
scope: "small-team-all-employees",
|
||||
realBusinessDataAllowed: true,
|
||||
fixPolicy: "blocking_same_day_issue_contract_nonblocking_feedback_pool",
|
||||
},
|
||||
oaPrimaryDomains: [
|
||||
"reimbursement_pdf_ocr",
|
||||
"approval_tasks",
|
||||
"procurement_expense",
|
||||
"contracts_registry",
|
||||
"projects",
|
||||
"payment_batches",
|
||||
"bank_receipt_attachments",
|
||||
"ap_accounting_handoff",
|
||||
"mobile_wecom_entry",
|
||||
"audit",
|
||||
],
|
||||
frozenItems: [
|
||||
"image_ocr",
|
||||
"complex_bpm",
|
||||
"budget_control",
|
||||
"erp_write",
|
||||
"bank_statement_import",
|
||||
"real_payment_provider",
|
||||
"wecom_hr_deep_mirror",
|
||||
"emergency_takeover",
|
||||
"recovery_writeback",
|
||||
"ruleops",
|
||||
],
|
||||
boundaries: {
|
||||
defaultReadOnlyAutomation: true,
|
||||
manualRealBusinessWritesOnly: true,
|
||||
paymentBatchExpected: "force_on",
|
||||
erpExpected: "force_off",
|
||||
actualPaymentChannel: "offline_bank_channel",
|
||||
contractSensitiveFiles: "do_not_upload_confidential_body_sensitive_quote_cost_detail_to_public_oa",
|
||||
},
|
||||
childReports: {
|
||||
liveReadiness: child("live-smoke-readiness"),
|
||||
},
|
||||
steps,
|
||||
readonlyProbe,
|
||||
findingCount: findings.length,
|
||||
findings,
|
||||
manualGates: {
|
||||
dailyCloseout: "管理员每日查看 summary.json 和 manual-gates.md,阻断即修,非阻断入反馈池。",
|
||||
day7Closeout: [
|
||||
"A:接受 PASS_OA_REAL_USE_READY,进入下一轮体验/效率优化。",
|
||||
"B:接受 PASS_WITH_FINDINGS,但先处理指定 finding。",
|
||||
"C:暂停真实使用扩展,先做整改。",
|
||||
],
|
||||
},
|
||||
privacy: "status/count/report-path summaries only; no phone/email/token/Secret/EncodingAESKey/full external ids/full invoice image/full bank receipt/full contract body/raw payload",
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
||||
console.log(`[oaops1] summary=${path.join(reportDir, "summary.json")}`);
|
||||
NODE
|
||||
}
|
||||
|
||||
finish() {
|
||||
write_manual_checklist
|
||||
write_summary_json || STATUS=1
|
||||
run_privacy_smoke
|
||||
log "[oaops1] report=$REPORT_DIR"
|
||||
exit "$STATUS"
|
||||
}
|
||||
|
||||
trap finish EXIT
|
||||
|
||||
load_default_env
|
||||
run_ready_and_runtime
|
||||
run_finding_step "authenticated-readonly-probe" run_authenticated_readonly_probe
|
||||
run_mobile_and_entry_matrix
|
||||
在新工单中引用
屏蔽一个用户