# hyb-oa Server Runbook (P0/P1) ## 1. 环境准备 - Node.js: `>=22 <23` - Docker + Docker Compose - 本地工具:`curl`、`jq` - 配置文件:从 `server/.env.example` 复制为 `server/.env` 并按需修改 ```bash cd server cp .env.example .env ``` ## 2. Gate 基线命令(必须顺序执行) ### 2.1 启动数据库 ```bash cd .. export POSTGRES_PASSWORD="" export JWT_SECRET="" docker compose up -d db ``` 根级 Compose 不再内置数据库口令或 JWT 密钥;请仅在本地 shell 或未提交的环境文件中设置,并让 `server/.env` 的 `DATABASE_URL` 使用同一数据库口令。 ### 2.2 应用迁移 ```bash cd server npm run prisma:migrate:deploy ``` ### 2.3 执行 seed(幂等) ```bash cd server SEED_DEV_ALLOW=1 SEED_DEV_DATABASE_CONFIRM=hyb_oa npm run seed:dev ``` 说明: - `seed:dev` 永久拒绝 production,只接受 loopback PostgreSQL,并要求确认 URL 中的数据库名。 - 开发账号资料和口令可通过 `SEED_*` 环境变量覆盖;脚本不会打印口令。 - 默认保留已有账号密码。新建账号以及主动设置 `SEED_DEV_RESET_PASSWORDS=1` 后轮换的账号,首次登录都必须改密。 - 开发账号配置不得复用于试运行或生产。 试运行管理员引导(E20): ```bash cd server TRIALRUN_ADMIN_EMAIL="" \ TRIALRUN_ADMIN_PHONE="" \ TRIALRUN_ADMIN_PASSWORD="" \ npm run seed:trialrun ``` 说明: - `TRIALRUN_ADMIN_PASSWORD` 为空时会生成并打印一次性口令(请立刻保存)。 - 若管理员已存在,脚本只确保其为 active 且具备 `admin+employee` 角色,不会改动密码。 ### 2.3.1 认证安全说明(P5) - `POST /api/auth/login` 成功响应包含 `mustChangePassword`: - `true`:表示该账号处于“首次登录/管理员重置密码后”状态;前端应强制引导改密后再进入业务页。 - 改密接口:`POST /api/auth/change-password`(需要 `authorization: Bearer ` + `x-device-fingerprint`)。 - 登录失败锁定(默认): - 15 分钟内失败 5 次会锁定 30 分钟,返回 `423 AUTH_002`(含 `lockedUntil`)。 - 解锁:`POST /api/users/:id/unlock`(权限:`system.users`)。 - 弱密码拦截: - 采用 `zxcvbn` 评分,默认 `score < 2` 拒绝(返回 `VALID_001.details.password`)。 - 可通过 `WEAK_PASSWORD_CHECK_ENABLED=false` 关闭(不建议生产关闭)。 ### 2.3.2 邮件通知(可选,F15) - 默认关闭:`EMAIL_ENABLED=false` - 开启后支持触发邮件(失败降级不阻塞主流程): - 新待办分配(workflow.task_assigned) - 审批结果(approved/rejected/returned) - 付款完成(reimbursement.paid) - 密码重置(forgot-password / reset-password) - 需要配置 SMTP: - `SMTP_HOST`、`SMTP_PORT`(默认 587)、`SMTP_SECURE`(465 通常为 true) - `SMTP_USER`、`SMTP_PASS`(按 SMTP 服务要求) - `EMAIL_FROM`(发件人地址) - `EMAIL_BASE_URL`(可选,邮件内“查看详情”链接的站点前缀;为空则尝试使用 `APP_ORIGIN` 的第一个值) ### 2.3.3 作业系统(cron vs pg-boss)(F19~F19d) - 默认:`JOB_DRIVER=cron`(同进程 node-cron 调度) - 可选升级:`JOB_DRIVER=pgboss`(Postgres 队列,适配单机/多进程;避免引入 Redis) - `PG_BOSS_SCHEMA`(默认 `pgboss`) - `JOBS_ENABLED=false` 可整体关闭作业(仅用于排障/紧急回滚) - 目前迁移的作业: - `ocr.tick`(受 `OCR_PROVIDER` 控制,off 时不调度) - `delegation.revoke-expired` - `invoice.archive-unused` - 回滚策略: - 将 `JOB_DRIVER` 切回 `cron`(无需 schema 迁移回滚,风险低) ### 2.3.4 Worker 进程拆分(可选,F20) > 目的:单机也能用“API 进程 + Worker 进程”模式运行,便于未来横向扩展;不改变对外 API。 - API 进程:只提供 HTTP(可选托管 `client/dist`),**不跑作业** - Worker 进程:只跑作业(OCR/委派回收/超期归档),**不监听端口** 本地双进程示例: ```bash # 终端 A:API(不跑作业) cd /server JOBS_ENABLED=false npm run dev # 终端 B:Worker(只跑作业) cd /server JOBS_ENABLED=true npm run worker ``` 试运行 compose(推荐,见 `deploy/trialrun/runbook.md`): - 启用 worker profile:`--profile worker` - 同时将 server 的 `JOBS_ENABLED=false`(worker 进程内强制为 true) ### 2.3.5 存储驱动(local vs S3/MinIO)(F21) > 目标:把“票据文件存储”从本地目录可选演进到 S3 兼容(单机优先 MinIO),保持签名 URL 契约不变。 - 默认:`STORAGE_DRIVER=local`(写入 `STORAGE_DIR`) - 可选:`STORAGE_DRIVER=s3`(写入 S3/MinIO) - 需要配置:`S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY/S3_FORCE_PATH_STYLE` - 为兼容混合存储,`invoice_pool.storage_path` 对 S3 会写成 `s3:`(无 schema 变更) 签名 URL 兼容策略: - `/api/invoice-pool/:id/signed-url` 仍返回本系统 URL(带 token) - `/api/invoice-pool/:id/file` 内部按 `storage_path` 自动从本地或 S3 读取并返回文件流 ### 2.4 启动服务 ```bash cd /server npm run dev ``` ### 2.5 冒烟验证 ```bash cd /server ./scripts/smoke-test.sh ``` ### 2.6 本地最小门禁(P1-b) ```bash cd ./scripts/check-minimal.sh ``` 说明: - 该脚本只做静态/构建级门禁,不启动服务与数据库。 - 全量回归仍通过 `server/scripts/regression-all.sh` 执行。 ## 3. 1 分钟快速验证 ```bash cd /server ./scripts/smoke-test.sh ``` 也可直接探活: ```bash curl -fsS http://127.0.0.1:3000/health curl -fsS http://127.0.0.1:3000/ready curl -fsS http://127.0.0.1:3000/metrics | head ``` 说明: - `/health`:liveness(不查 DB) - `/ready`:readiness(`SELECT 1` 探活 DB,失败返回 503) - `/metrics`:Prometheus 指标(可通过 `METRICS_ENABLED=false` 关闭) --- ## 4. 回归入口 ### 4.1 脚本回归(推荐) ```bash cd /server BASE_URL=http://127.0.0.1:3000 ./scripts/smoke-test.sh ``` ### 4.2 判重专项回归(dedup) ```bash cd /server DATABASE_URL='' \ BASE_URL=http://127.0.0.1:3000 ./scripts/regression-dedup.sh ``` ### 4.3 工作流动作回归 ```bash cd /server ./scripts/regression-workflow-actions.sh ``` 可选并发校验: ```bash cd /server RUN_DELEGATION_CONCURRENCY_CHECK=1 ./scripts/regression-workflow-actions.sh ``` 覆盖场景: - `reject`:实例=`rejected`,报销=`rejected` - `return`:实例=`terminated`,报销=`draft` - `withdraw`:审批未处理前可撤回 - `return` 后二次 `submit`:创建新实例 - `PERM_002`:非 assignee 无法操作 - `no_actionable_approver`:全链无可执行审批人阻断 - `negative_not_allowed`:负金额防御 - `manager skip`:直属上级缺失时跳过并记录 - `transfer`:转审成功(任务改派 + 任务/转审记录/history 三处证据) - `transfer` 参数校验:reason<10 返回 `VALID_002` - `transfer` 同人转审:返回 `VALID_001`(`same_assignee_not_allowed`) - `transfer` 链路上限:同 task 第 4 次转审阻断(`chain_limit_exceeded`) - `transfer` 目标校验:目标审批人须 active 且具备 `expense.approve` - `pay`:财务付款成功(`approved -> paid`,发票 `bound -> archived(used_complete)`) - `pay` 权限:非财务返回 `PERM_001` - `pay` 状态校验:非 `approved/paid` 返回 `VALID_001` - `pay` 幂等:重复付款返回 200(`archivedInvoiceCount` 可为 0) - `pay` 幂等冲突:已付款后提交冲突凭证字段返回 `409 VALID_001`(`already_paid_conflict`) - `delegation`:有效期内可代办并在 history 写入 delegation 证据 - `delegation` 过期:代理人返回 `PERM_002`,自动回收任务可撤销过期委派 - `delegation` 并发回收(可选):`RUN_DELEGATION_CONCURRENCY_CHECK=1` 时双并发只回收一次并仅写一条自动回收审计 ### 4.4 一键全量回归 ```bash cd /server ./scripts/regression-all.sh ``` 串联 `smoke-test.sh + regression-dedup.sh + regression-workflow-actions.sh + regression-audit.sh + regression-ocr.sh`,适用于 CI 或发版前检查。 可选关闭审计/OCR: ```bash cd /server RUN_AUDIT_REGRESSION=0 RUN_OCR_REGRESSION=0 ./scripts/regression-all.sh ``` 可选打开组织/配置回归(org + settings): ```bash cd /server RUN_ORG_SETTINGS_REGRESSION=1 ./scripts/regression-all.sh ``` 可选打开认证安全回归(auth security): ```bash cd /server RUN_AUTH_SECURITY_REGRESSION=1 ./scripts/regression-all.sh ``` 独立执行: ```bash cd /server ./scripts/regression-audit.sh OCR_PROVIDER=mock ./scripts/regression-ocr.sh ./scripts/regression-org-settings.sh ./scripts/regression-auth-security.sh ``` ### 4.5 Postman 回归 - Collection: `server/postman/hyb-oa.postman_collection.json` - Environment: `server/postman/hyb-oa.postman_environment.json` - 核心链路: 1. Login Employee 2. Create Reimbursement 3. Upload Invoice 4. Bind Invoice 5. Submit Reimbursement 6. Login CEO 7. Approve Workflow Task 8. Get Signed URL ### 4.6 前端壳联调(P1-5) ```bash cd /client npm install npm run dev ``` 说明: - 前端开发地址默认 `http://127.0.0.1:5173`。 - 通过 Vite 代理将 `/api` 转发到 `http://127.0.0.1:3000`。 - Access Token 仅保存在内存,Refresh Token 通过 HttpOnly Cookie 续签。 ### 4.7 单域托管 client/dist(可选,试运行建议) ```bash cd /client npm run build cd /server SERVE_CLIENT_DIST=true npm run dev ``` 说明: - 访问 `http://127.0.0.1:3000` 即可打开前端(同域 cookie,减少 CORS 复杂度)。 - SPA 路由(如 `/dashboard`)会回退到 `index.html`;`/api/*` 不参与回退。 ### 4.8 试运行部署包(Docker Compose) 试运行/上线演练建议使用仓库内置部署包: - `deploy/trialrun/runbook.md` - `deploy/trialrun/docker-compose.yml` - `deploy/trialrun/.env.example` - `deploy/trialrun/checklist.md` 备份/恢复脚本(E14): ```bash cd ./scripts/backup-db.sh ./scripts/restore-db.sh deploy/trialrun/backups/ ./scripts/backup-storage.sh ./scripts/restore-storage.sh deploy/trialrun/backups/ deploy/trialrun/data/storage ``` 优雅停机(E21): - `docker stop` / `Ctrl+C` 会触发 `SIGTERM/SIGINT`,server 会调用 `app.close()` 完成收敛关闭(定时任务/HTTP/DB)。 --- ## 5. P2 API 摘要 ### 5.1 付款标记(pay) - `POST /api/reimbursements/:id/pay` - 权限:`expense.finance.pay` - 可选请求体:`paymentVoucherNo`、`paymentDate(YYYY-MM-DD)`、`paymentChannel(bank/alipay/wechat/cash/other)` - 语义: - `approved -> paid`:返回 200,并归档关联 `bound` 发票 - 已 `paid` 重复同值:返回 200(`payment.idempotent=true`) - 已 `paid` 重复冲突值:返回 `409 VALID_001`,`details.payment=already_paid_conflict` ### 5.2 委派管理 - `POST /api/delegations`:创建委派(scope + validFrom/validTo) - `GET /api/delegations/mine`:查看我发起/我接收的委派 - `POST /api/delegations/:id/revoke`:撤销委派 - 委派仅作用于 `assignee_id` 任务,不改写 `assignee_role` 语义。 - 自动回收实现采用 `FOR UPDATE SKIP LOCKED + UPDATE ... RETURNING`,多实例并发下确保同一委派仅回收一次。 ### 5.3 OCR 异步状态查询 - 上传发票在 OCR 开启且 `ocr_type=vat` 时会进入 `pending_ocr`。 - `GET /api/invoice-pool/:id` 可轮询 OCR 处理状态(`pending_ocr/available/ocr_failed`)。 ### 5.4 审计日志查询 - `GET /api/audit-logs` - 权限:`system.audit.view`(finance_mgr/admin) - 支持分页与筛选(actor/action/targetType/date range)。 ### 5.5 工作流查询增强(P3) - `GET /api/workflow/tasks`:支持 `status/page/pageSize`,返回 `page/pageSize/total/items`。 - `GET /api/workflow/tasks/:taskId`:任务详情(实例、报销摘要、可执行动作、处理历史)。 - `GET /api/workflow/approvers`:转审候选人列表(active + 具备 `expense.approve`)。 ### 5.6 部门管理(P5 蓝图 F26) - `GET /api/departments`:部门列表 + 树(登录即可;默认不含归档部门) - 可选:`GET /api/departments?includeArchived=1`(包含归档部门) - `POST /api/departments`:创建部门(`system.users`) - `PATCH /api/departments/:id`:更新部门(`system.users`;已归档禁止修改;禁止形成父子环) - `POST /api/departments/:id/archive`:归档部门(`system.users`) - 归档约束:有子部门/有在职员工则禁止归档 - 幂等:已归档重复调用返回 `outcome=already_archived` ### 5.7 列表查询接口(P3) - `GET /api/reimbursements`:支持 `page/pageSize/status/scope`。 - `GET /api/invoice-pool`:支持 `page/pageSize/status/ownerScope`。 ### 5.8 定时任务(可手动执行) ```bash cd /server npm run job:revoke-expired-delegations npm run job:ocr-once ``` ### 5.9 账号与角色管理(E17,试运行运营) - `GET /api/users`:员工列表(分页/搜索) - `POST /api/users`:创建员工(含初始密码) - `GET /api/users/:id`:员工详情(管理员可看全部;员工可看自己;敏感字段仅返回脱敏) - `POST /api/users/:id/disable`:禁用员工(tokenVersion+1 + 撤销 refresh) - `POST /api/users/:id/enable`:启用员工(tokenVersion+1 + 撤销 refresh) - `POST /api/users/:id/reset-password`:重置密码(返回一次性口令,tokenVersion+1) - `POST /api/users/:id/roles`:设置角色(tokenVersion+1 + 撤销 refresh) - `GET /api/users/:id/sensitive`:敏感字段明文查询(F25) - `PATCH /api/users/:id/sensitive`:敏感字段更新(F25) 权限: - `system.users`(hr/admin;admin 具备 `*`) - `system.users_sensitive`(仅特权账号;用于解密查看/更新银行卡、身份证等) --- ## 6. 关键环境变量 - `DATABASE_URL`: PostgreSQL 连接串 - `JWT_SECRET`: JWT & signed-url token 签名密钥 - `FIELD_ENCRYPTION_KEY`: 敏感字段加密密钥(AES-256-GCM;base64 32 bytes;用于银行卡/身份证等) - `APP_TIMEZONE`: 默认 `Asia/Shanghai` - `STORAGE_DIR`: 默认 `generated/storage`(相对 `server/`) - `SERVE_CLIENT_DIST`: 是否由 server 托管 `client/dist`(默认 `false`) - `CLIENT_DIST_DIR`: 前端构建产物目录(默认 `../client/dist`,相对 `server/`) - `SIGNED_URL_TTL_SECONDS`: 默认 `600` - `MAX_UPLOAD_SIZE_BYTES`: 默认 `10485760`(10MB) - `RATE_LIMIT_ENABLED`: 是否启用关键接口限流(默认:production=true,其它环境=false) - `RATE_LIMIT_LOGIN_MAX`: `/api/auth/login` 等接口限流次数(默认 `10`) - `RATE_LIMIT_LOGIN_WINDOW`: 限流时间窗(默认 `1 minute`,支持 ms 格式如 `30s`/`1m`) - `DELEGATION_REVOKE_CRON`: 委派自动回收定时表达式(默认 `0 * * * *`) - `OIDC_ENABLED`: 是否启用 OIDC 登录(可选;默认 `false`) - `OIDC_ISSUER_URL`: OIDC issuer URL(发现地址) - `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET`: OIDC client 配置 - `OIDC_REDIRECT_URI`: 回调地址(例如 `http://127.0.0.1:3000/api/auth/oidc/callback`) - `OIDC_SCOPES`: 默认 `openid profile email` - `OCR_PROVIDER`: `off` / `mock` / `tencent`(默认 `off`) - `OCR_BATCH_SIZE`: 每轮 OCR 认领数量(默认 `10`) - `OCR_LOCK_TTL_SECONDS`: OCR 锁超时秒数(默认 `300`) - `OCR_POLL_CRON`: OCR 轮询 cron(默认 `* * * * *`) - `OCR_MAX_ATTEMPTS`: OCR 最大重试次数(默认 `3`) - `OCR_TIMEOUT_MS`: 腾讯 OCR SDK 超时(默认 `5000`) - `TENCENT_SECRET_ID` / `TENCENT_SECRET_KEY` / `TENCENT_OCR_REGION`: 腾讯 OCR 配置 安全基线(P4): - 安全头:默认启用 `@fastify/helmet`(CSP 默认关闭,避免误伤 SPA 本地调试)。 - 限流:`/api/auth/login`、`/api/auth/forgot-password`、`/api/auth/reset-password` 在 `RATE_LIMIT_ENABLED=true` 时启用限流。 - 日志脱敏:`authorization/cookie/password/code/newPassword` 等敏感字段会被 redaction。 回滚策略: - 若限流误伤,可先设置 `RATE_LIMIT_ENABLED=false`(无需改代码)。 - 若安全头造成兼容性问题,可回退 `server/src/app.js` 中 helmet 注册逻辑。 > 生产提示:大表加索引需评估 `CREATE INDEX CONCURRENTLY`,避免锁表影响。 --- ## 7. 常见失败排查 | 现象 | 可能原因 | 解决方案 | |---|---|---| | `prisma migrate deploy` 失败 | `DATABASE_URL` 未配置/不可达 | 检查 `server/.env` 与数据库容器状态 | | `npm run seed:dev` 重复执行报错 | seed 脚本被改坏,未保持幂等 | 检查 `seed-config.js`/`seed-dev.js` 的 `upsert` / `skipDuplicates` | | signed-url 接口返回 403 | `JWT_SECRET` 不一致、token 过期、访问人无权限 | 统一服务密钥、确认 URL 在 TTL 内、检查账号角色 | | 上传返回 `file_too_large` | 文件超 `MAX_UPLOAD_SIZE_BYTES` | 调整环境变量或使用更小文件 | | 上传失败且报目录/权限错误 | `STORAGE_DIR` 不存在或不可写 | 创建目录并修正权限;Docker 需确认 volume 挂载 | | OCR 长期停留 `pending_ocr` | `OCR_PROVIDER=off`、mock未开启或腾讯密钥无效 | 检查 OCR 环境变量,优先用 `OCR_PROVIDER=mock npm run job:ocr-once` 验证链路 | | 委派过期后仍可操作 | 自动回收任务未运行 | 检查 `DELEGATION_REVOKE_CRON`,手动执行 `npm run job:revoke-expired-delegations` | --- ## 8. 手工故障注入(非阻塞) ### 8.1 `STORAGE_DIR` 不可写路径 - 临时将 `STORAGE_DIR` 指向只读目录,执行上传接口,确认错误可观测(日志+返回码)。 ### 8.2 磁盘空间不足(ENOSPC) - 在受控环境中制造磁盘满条件后重试上传。 - 预期:错误路径可观测(日志中能定位到 I/O 错误),不要求纳入日常 CI 冒烟。 --- ## 9. P6-P10 增量能力(验真 / 看板 / 预算 / 集成 / 生产硬化) ### 9.1 特性开关(默认值) - `INVOICE_VERIFICATION_ENABLED=false` - `VERIFICATION_PROVIDER=mock` - `BUDGET_CONTROL_MODE=warn` - `ERP_SYNC_ENABLED=false` - `ERP_SYNC_PROVIDER=mock` - `ERP_SYNC_ENDPOINT=` - `ERP_SYNC_TIMEOUT_MS=5000` - `ERP_SYNC_AUTH_TOKEN=` - `TRAVEL_SYNC_ENABLED=false` - `IM_NOTIFIER_ENABLED=false` ### 9.1.1 Feature Flag 治理(P11) - 新增统一管理 API: - `GET /api/system/feature-flags` - `PUT /api/system/feature-flags/:key` - 首期纳管键:`erp_sync`、`invoice_verification`。 - 优先级规则:`Env硬闸` > `DB在线灰度`。即使 DB 放开,若 Env 为 false 仍会关闭。 - 部门灰度模式:`rolloutType=department_allowlist` 时,仅白名单部门生效。 建议:先在测试组织灰度开启,再逐步扩大范围。 ### 9.2 新增 API 摘要 - 发票验真: - `POST /api/invoice-pool/:id/verify` - `GET /api/invoice-pool/:id/verification` - `POST /api/invoice-pool/:id/verification/retry` - 看板聚合: - `GET /api/dashboard/expense-overview` - `GET /api/dashboard/trends` - `GET /api/dashboard/top-categories` - `GET /api/dashboard/approval-efficiency` - 预算管理: - `GET /api/budgets` - `POST /api/budgets` - `PATCH /api/budgets/:id` - `POST /api/budgets/:id/archive` - 外部集成: - `POST /api/integrations/erp/sync/:reimbursementId` - `POST /api/travel/orders/import` - `GET /api/notifications/channels` - `POST /api/notifications/channels/test` ### 9.3 新增回归脚本 ```bash cd /server ./scripts/regression-verification.sh ./scripts/regression-dashboard.sh ./scripts/regression-budget.sh ./scripts/regression-integrations.sh ./scripts/regression-feature-flags.sh ``` 可通过总回归开关启用: ```bash cd /server RUN_VERIFICATION_REGRESSION=1 \ RUN_DASHBOARD_REGRESSION=1 \ RUN_BUDGET_REGRESSION=1 \ RUN_INTEGRATIONS_REGRESSION=1 \ RUN_FEATURE_FLAGS_REGRESSION=1 \ ./scripts/regression-all.sh ``` ### 9.4 单次任务脚本(调试) ```bash cd /server VERIFICATION_RECORD_ID= npm run job:verification-once npm run job:integration-outbox-once ``` ### 9.5 回滚建议 1. 先关闭特性开关(验真/ERP/差旅/IM)。 2. 再停止相关任务调度(verification / integration outbox)。 3. 不回滚审计与历史台账数据,保持可追溯。 ### 9.6 P9/P10 维护性与硬化说明 - 票据路由拆分:验真端点已拆到 `src/routes/invoicePool/verification.js`,读路径端点已拆到 `src/routes/invoicePool/read.js`,主入口仍为 `src/routes/invoicePool.js`,API 路径不变。 - Shared 抽象:`src/routes/invoicePool/shared/{selects,mappers,schemas,helpers}.js` 统一验真映射与票据查询 `select`,避免字段漂移。 - 看板聚合:`/api/dashboard/trends` 与 `/api/dashboard/top-categories` 已改为数据库聚合(groupBy),降低大数据量下内存聚合开销。 - 看板回归:`./scripts/regression-dashboard.sh` 新增重复请求一致性断言;可通过 `DASHBOARD_PERF_CHECK=1` 打印趋势接口耗时探针。 - ERP硬化回归:`./scripts/regression-integrations.sh` 内嵌最小 Node Mock ERP 服务,覆盖 success / 4xx(dead_letter) / 5xx(retry) / timeout(retry)。 - 矩阵门禁:`scripts/validate-feature-matrix.py` 默认校验 P5 + P6-P8 + P9 + P10 四事实源;`./scripts/check-minimal.sh` 会自动执行该校验。