← 返回首页

全链路演示脚本说明

一键 curl 演示(15 步):健康检查 → 诊断 → 租户登录 → 品牌/关键词/扫描/报告 → 发文 → step 11 知乎 OAuth + platform-publish-via-api(须 WM_GEO_CHANNEL_OAUTH_DEV_BYPASS=1 WM_GEO_PLATFORM_PUBLISH_API_E2E=1run-demo-full-business-path.sh 默认导出)→ 代发/运营/供应商/撤稿。 与 e2e/demo-script-smoke.spec.ts 前段 API 对齐;OAuth 见 e2e/demo-script-extension-oauth-steps-flow.spec.ts;platform publish 见 e2e/demo-script-platform-publish-steps-flow.spec.ts;扩展真发 checklist 见 e2e/demo-script-extension-real-publish-steps-flow.spec.ts; 脚本 echo + docs 锚点见 e2e/demo-script-doc-extension-real-publish-echo-flow.spec.ts;扩展连接步骤见 e2e/publish-extension-connection-diagnosis-flow.spec.ts;M3/支付/PDF-OSS 租户运维见 e2e/demo-script-tenant-ops-steps-flow.spec.ts;定时 Cron 运维见 e2e/demo-script-cron-ops-steps-flow.spec.ts;M5 引用监测 D+ Cron 见 e2e/m5-citation-scheduler-cron-mvp-flow.spec.ts

Settings 全链路 demo-script · 双 hub 总入口

Admin 六域 + M5 七子卡

全链路脚本在 step 7j(Admin 六域 hub)与 step 11d+11e merge(M5 七子卡 hub)及 step 7l+7o(公网部署五件套汇总)完成后调用 GET /api/v1/app/demo/full-chain-hub-steps,末尾 echo Settings「全链路演示 · 总入口」卡 / 本页锚点 / Admin 双 hub 子卡 URL。登录态实时状态见 账户设置 「全链路演示 · 总入口」卡;Admin 侧见 运营后台 「运维 demo-script · 汇总入口」与「M5 引用监测 · 汇总运维入口」双卡。

  1. 1.GET admin/ops-hub-status + GET demo/admin-ops-hub-steps(Admin 六域 hub · step 7j)GET /api/v1/admin/ops-hub-status · GET /api/v1/app/demo/admin-ops-hub-steps
  2. 2.GET admin/m5/citation-ops-hub-status + GET demo/m5-citation-ops-steps(M5 七子卡 hub · step 11d+11e merge)GET /api/v1/admin/m5/citation-ops-hub-status · GET /api/v1/app/demo/m5-citation-ops-steps
  3. 3.GET deploy/deploy-readiness-status(公网部署五件套汇总 · APP_URL+DNS+CDN+health API+真机 HTTPS · step 7l+7o merge)GET /api/v1/app/deploy/deploy-readiness-status
  4. 4.聚合 GET demo/full-chain-hub-steps(Settings 全链路总入口卡 · 双 hub + 四件套对齐 · step 11d+7l merge)GET /api/v1/app/demo/full-chain-hub-steps
  5. 5.GET admin/fc-env-post-write-acceptance-status(FC env 写入后运维验收 · aliyun-d1-checklist §0.5)GET /api/v1/admin/fc-env-post-write-acceptance-status
  6. 6.脚本 step 11d+7l merge 后 + 末尾 echo Settings 总入口卡 / deploy-readiness-status / docs#full-chain-hub-steps / Admin FC env 验收卡scripts/demo-full-business-path.sh

step 7b · M3/支付/PDF-OSS 租户运维 checklist

Cron/支付/PDF-OSS 闭环

全链路脚本在 step 7 PDF 导出后调用 GET /api/v1/app/m3/module-status GET /api/v1/app/billing/pay-checklist-status GET /api/v1/app/reports/export-oss-context 与聚合 GET /api/v1/app/demo/tenant-ops-steps,末尾 echo Settings / 本页锚点 URL。登录态实时状态见 账户设置 「全链路演示 · M3/支付/PDF-OSS 运维步骤」卡。

  1. 1.M3 建议模块 checklist(GET m3/module-status?brandId=)GET /api/v1/app/m3/module-status
  2. 2.国内支付就绪 checklist(GET billing/pay-checklist-status)GET /api/v1/app/billing/pay-checklist-status
  3. 3.PDF/OSS 报告导出 checklist(GET reports/export-oss-context?brandId=)GET /api/v1/app/reports/export-oss-context
  4. 4.step 7 报告 PDF 导出(GET …/export?format=pdf · %PDF- 魔数)scripts/demo-full-business-path.sh step 7
  5. 5.聚合 GET demo/tenant-ops-steps + 末尾 echo Settings / docs 锚点GET /api/v1/app/demo/tenant-ops-steps
账户设置 · 租户运维步骤卡·M3 建议模块·e2e/demo-script-tenant-ops-steps-flow.spec.ts

step 7j · Admin 运维 demo-script 总 hub

Admin 六域汇总

全链路脚本在 step 7d–7i 各子域 Admin 运维演示后调用 GET /api/v1/admin/ops-hub-status 与聚合 GET /api/v1/app/demo/admin-ops-hub-steps,末尾 echo Settings 总入口卡 / 本页锚点 / Admin 汇总入口卡 URL。登录态实时状态见 账户设置 「Admin 运维总入口」卡;Admin 运维见 运营后台 「运维 demo-script · 汇总入口」卡与 PRD 缺口路线图

  1. 1.GET admin/ops-hub-status(六域 Admin 运维汇总入口)GET /api/v1/admin/ops-hub-status
  2. 2.GET demo/cn-pay-ops-steps(step 7d · 国内支付/订阅 Admin 运维)GET /api/v1/app/demo/cn-pay-ops-steps
  3. 3.GET demo/pdf-oss-ops-steps(step 7e · PDF/OSS Admin 运维)GET /api/v1/app/demo/pdf-oss-ops-steps
  4. 4.GET demo/cron-admin-ops-steps(step 7f · 定时 Cron Admin 运维)GET /api/v1/app/demo/cron-admin-ops-steps
  5. 5.GET demo/m3-admin-ops-steps(step 7g · M3 Admin 运维)GET /api/v1/app/demo/m3-admin-ops-steps
  6. 6.GET demo/extension-admin-ops-steps(step 7h · extension Admin 六卡运维)GET /api/v1/app/demo/extension-admin-ops-steps
  7. 7.GET demo/platform-publish-admin-ops-steps(step 7i · platform publish Admin 运维)GET /api/v1/app/demo/platform-publish-admin-ops-steps
  8. 8.聚合 GET demo/admin-ops-hub-steps + 末尾 echo Settings / docs / /admin hub 锚点scripts/demo-full-business-path.sh

step 7k · FC 部署 readiness checklist

APP_URL / FC env

全链路脚本在 AOK–AOM 租户 APP_URL checklist 后调用 GET /api/v1/app/deploy/app-url-status GET /api/v1/admin/deploy-readiness-status GET /api/v1/app/demo/full-chain-hub-steps 与聚合 GET /api/v1/app/demo/deploy-readiness-ops-steps(step 7k+11d merge),末尾 echo Settings / 本页锚点 / 双 hub 总入口 / Admin FC 部署 readiness 运维卡 / aliyun-d1-checklist URL。登录态实时状态见 账户设置 「FC 部署 readiness 步骤」卡;Admin 运维见 运营后台 「FC 部署 readiness 运维」卡与 阿里云 D1 清单

  1. 1.GET deploy/app-url-status(租户 APP_URL / HTTPS / 双变量一致 checklist)GET /api/v1/app/deploy/app-url-status
  2. 2.GET deploy/dns-resolution-status(生产 hostname A/AAAA 解析 / CDN 回源 checklist)GET /api/v1/app/deploy/dns-resolution-status
  3. 3.GET deploy/cdn-https-status(CDN HTTPS / TLS 证书 · health API 443 探测 checklist)GET /api/v1/app/deploy/cdn-https-status
  4. 4.GET deploy/deploy-readiness-status(租户公网部署五件套汇总 · APP_URL+DNS+CDN+health API+真机 HTTPS · step 7l+7o merge)GET /api/v1/app/deploy/deploy-readiness-status
  5. 5.GET deploy/health-api-public-status(health API 公网 443 探测 · schemaVersion/summary/ok 契约 · step 7m merge)GET /api/v1/app/deploy/health-api-public-status
  6. 6.GET deploy/mobile-browser-https-status(wm-geo.com 真机 HTTPS · Mobile Safari UA 根路径探测 · step 7n merge)GET /api/v1/app/deploy/mobile-browser-https-status
  7. 7.GET admin/deploy-readiness-status(FC env + APP_URL 聚合 · DATABASE_URL/CRON/OSS)GET /api/v1/admin/deploy-readiness-status
  8. 8.GET demo/full-chain-hub-steps(Settings 双 hub 总入口 · Admin 六域 + M5 七子卡 · step 7k+11d merge)GET /api/v1/app/demo/full-chain-hub-steps
  9. 9.Admin 首页「FC 部署 readiness 运维」卡 checklist 对齐(readiness badge + 运维链)GET /api/v1/admin/deploy-readiness-status
  10. 10.聚合 GET demo/deploy-readiness-ops-steps + step 7k+7l+7m+7n+7o merge + 末尾 echo Settings / deploy/deploy-readiness-status(五件套) / deploy/health-api-public-status / deploy/mobile-browser-https-status / docs / full-chain-hub / /admin FC 部署卡 / aliyun-d1-checklistGET /api/v1/app/demo/deploy-readiness-ops-steps

step 7g · M3 Admin 运维 checklist

Admin M3

全链路脚本在 step 7b 租户 M3/支付/PDF-OSS checklist 后调用 GET /api/v1/app/m3/module-status GET /api/v1/admin/m3/module-status 与聚合 GET /api/v1/app/demo/m3-admin-ops-steps,末尾 echo Settings / 本页锚点 / Admin M3 建议模块运维卡 URL。登录态实时状态见 账户设置 「M3 Admin 运维步骤」卡;Admin 运维见 运营后台 「M3 建议模块运维」卡与 PRD 缺口路线图

  1. 1.GET m3/module-status(租户 M3 建议模块 checklist + hub/各页模块卡)GET /api/v1/app/m3/module-status
  2. 2.GET admin/m3/module-status(全库 brands/generations/scans 计数 + PRD 缺口)GET /api/v1/admin/m3/module-status
  3. 3.Admin 首页「M3 建议模块运维」卡 checklist 对齐(readiness badge + 运维链)GET /api/v1/admin/m3/module-status
  4. 4.聚合 GET demo/m3-admin-ops-steps + step 7g + 末尾 echo Settings / docs / /admin M3 运维卡GET /api/v1/app/demo/m3-admin-ops-steps

step 7h · extension Admin 运维 checklist

Admin 扩展六卡

全链路脚本在 step 11 扩展 OAuth/真发 checklist 后调用 GET /api/v1/app/extension/real-publish-checklist-summary、六张 Admin 扩展运维卡子 API 与聚合 GET /api/v1/app/demo/extension-admin-ops-steps,末尾 echo Settings / 本页锚点 / /admin 扩展六卡 URL。登录态实时状态见 账户设置 「extension Admin 运维步骤」卡;Admin 运维见 运营后台 扩展真发 / 连接 / DOM 真调 / fill-submit / live_url / platform publish 六卡与 PRD 缺口路线图

  1. 1.GET extension/real-publish-checklist-summary(租户扩展真发 checklist 汇总卡)GET /api/v1/app/extension/real-publish-checklist-summary
  2. 2.GET admin/extension-real-publish-status(扩展真发 checklist 运维卡)GET /api/v1/admin/extension-real-publish-status
  3. 3.GET admin/extension-connection-status(扩展连接运维卡)GET /api/v1/admin/extension-connection-status
  4. 4.GET admin/extension/dom-tuning-status(扩展 DOM 真调运维卡)GET /api/v1/admin/extension/dom-tuning-status
  5. 5.GET admin/extension/fill-submit-draft-status(fillDraft/submitDraft 运维卡)GET /api/v1/admin/extension/fill-submit-draft-status
  6. 6.GET admin/extension/live-url-backfill-status(live_url 写回运维卡)GET /api/v1/admin/extension/live-url-backfill-status
  7. 7.GET admin/platform-publish-status(platform publish 运维卡)GET /api/v1/admin/platform-publish-status
  8. 8.聚合 GET demo/extension-admin-ops-steps + step 7h + 末尾 echo Settings / docs / /admin 扩展六卡GET /api/v1/app/demo/extension-admin-ops-steps

step 7i · platform publish Admin 运维 checklist

Admin platform publish

全链路脚本在 step 11 租户 platform publish 演示后调用 GET /api/v1/app/demo/platform-publish-steps GET /api/v1/app/extension/platform-publish-status GET /api/v1/admin/platform-publish-status 与聚合 GET /api/v1/app/demo/platform-publish-admin-ops-steps,末尾 echo Settings / 本页锚点 / Admin platform publish 运维卡 URL。登录态实时状态见 账户设置 「platform publish Admin 运维步骤」卡;Admin 运维见 运营后台 「platform publish 运维」卡与 PRD 缺口路线图

  1. 1.GET demo/platform-publish-steps(租户 step 11 OAuth + platform-publish-via-api 演示)GET /api/v1/app/demo/platform-publish-steps
  2. 2.GET extension/platform-publish-status(租户四平台 publish API skeleton checklist)GET /api/v1/app/extension/platform-publish-status
  3. 3.GET admin/platform-publish-status(全平台 OAuth/live_url/platform API 写回计数)GET /api/v1/admin/platform-publish-status
  4. 4.Admin 首页「platform publish 运维」卡 checklist 对齐(readiness badge + 运维链)GET /api/v1/admin/platform-publish-status
  5. 5.聚合 GET demo/platform-publish-admin-ops-steps + step 7i + 末尾 echo Settings / docs / /admin platform publish 运维卡GET /api/v1/app/demo/platform-publish-admin-ops-steps

step 7e · PDF/OSS Admin 运维 checklist

Admin PDF/OSS

全链路脚本在 step 7 PDF 与 step 7b 租户 checklist 后调用 GET /api/v1/app/reports/export-oss-context GET /api/v1/admin/reports/export-oss-status 与聚合 GET /api/v1/app/demo/pdf-oss-ops-steps,末尾 echo Settings / 本页锚点 / Admin PDF/OSS 运维卡 URL。登录态实时状态见 账户设置 「PDF/OSS Admin 运维步骤」卡;Admin 运维见 运营后台 「PDF/OSS 报告导出运维」卡与 阿里云 OSS 清单

  1. 1.GET reports/export-oss-context(租户 PDF/OSS checklist · Settings/Reports 各页汇总卡)GET /api/v1/app/reports/export-oss-context
  2. 2.GET admin/reports/export-oss-status(PDF/OSS env + reports/scans 全库计数)GET /api/v1/admin/reports/export-oss-status
  3. 3.Admin 首页「PDF/OSS 报告导出运维」卡 checklist 对齐(readiness badge + 运维链)GET /api/v1/admin/reports/export-oss-status
  4. 4.step 7 报告 PDF 导出(GET …/export?format=pdf · %PDF- 魔数校验)scripts/demo-full-business-path.sh step 7
  5. 5.聚合 GET demo/pdf-oss-ops-steps + step 7e + 末尾 echo Settings / docs / /admin PDF/OSS 运维卡GET /api/v1/app/demo/pdf-oss-ops-steps

step 7d · 国内支付/订阅 Admin 运维 checklist

Admin 订阅列表

全链路脚本在 step 7b 租户支付 checklist 后调用 GET /api/v1/app/billing/pay-checklist-status GET /api/v1/admin/billing/cn-pay-production-status 与聚合 GET /api/v1/app/demo/cn-pay-ops-steps,末尾 echo Settings / 本页锚点 / Admin 国内支付运维卡 URL。登录态实时状态见 账户设置 「国内支付/订阅 Admin 运维步骤」卡;Admin 运维见 运营后台 「国内支付运维」卡与 订阅列表

  1. 1.GET billing/pay-checklist-status(微信/支付宝/Stripe 租户就绪)GET /api/v1/app/billing/pay-checklist-status
  2. 2.GET admin/billing/cn-pay-production-status(双通道 env + orders/subscriptions 全库计数)GET /api/v1/admin/billing/cn-pay-production-status
  3. 3.Admin 订阅列表 /admin/billing/subscriptions(wechat/alipay 活跃行核对)GET /api/v1/admin/billing/subscriptions
  4. 4.聚合 GET demo/cn-pay-ops-steps + step 7d + 末尾 echo Settings / docs / /admin 国内支付运维卡GET /api/v1/app/demo/cn-pay-ops-steps

step 7c · 定时 Cron 运维 checklist

Cron 产品化闭环

全链路脚本在 step 7b 租户运维后调用 GET /api/v1/app/monitor/schedule-status GET /api/v1/app/monitor/cron-productization-context 与聚合 GET /api/v1/app/demo/cron-ops-steps,末尾 echo Settings / 本页锚点 URL。登录态实时状态见 账户设置 「全链路演示 · 定时 Cron 运维步骤」卡;Admin 全平台摘要见 运营后台 「定时 Cron 产品化运维」卡。

  1. 1.定时重测 Cron checklist(GET monitor/schedule-status?brandId=)GET /api/v1/app/monitor/schedule-status
  2. 2.定时 Cron 产品化 checklist(GET monitor/cron-productization-context?brandId=)GET /api/v1/app/monitor/cron-productization-context
  3. 3.运维脚本 scripts/run-all-cron.sh(scans → async → publications)scripts/run-all-cron.sh
  4. 4.聚合 GET demo/cron-ops-steps + 末尾 echo Settings / docs 锚点GET /api/v1/app/demo/cron-ops-steps
账户设置 · Cron 运维步骤卡·Prompt 矩阵·e2e/demo-script-cron-ops-steps-flow.spec.ts

step 7f · 定时 Cron Admin 运维 checklist

Admin Cron

全链路脚本在 step 7c 租户 Cron checklist 后调用 GET /api/v1/app/monitor/schedule-status GET /api/v1/app/monitor/cron-productization-context GET /api/v1/admin/monitor/cron-productization-status 与聚合 GET /api/v1/app/demo/cron-admin-ops-steps,末尾 echo Settings / 本页锚点 / Admin Cron 产品化运维卡 URL。登录态实时状态见 账户设置 「定时 Cron Admin 运维步骤」卡;Admin 运维见 运营后台 「定时 Cron 产品化运维」卡与 Cron 运维说明

  1. 1.GET monitor/schedule-status(租户定时重测 Cron checklist)GET /api/v1/app/monitor/schedule-status
  2. 2.GET monitor/cron-productization-context(22 页租户汇总卡 + 三条 Cron)GET /api/v1/app/monitor/cron-productization-context
  3. 3.GET admin/monitor/cron-productization-status(全平台 Cron 产品化运维计数)GET /api/v1/admin/monitor/cron-productization-status
  4. 4.Admin 首页「定时 Cron 产品化运维」卡 checklist 对齐(readiness badge + 运维链)GET /api/v1/admin/monitor/cron-productization-status
  5. 5.聚合 GET demo/cron-admin-ops-steps + step 7f + 末尾 echo Settings / docs / /admin Cron 产品化运维卡GET /api/v1/app/demo/cron-admin-ops-steps

step 11d · M5 引用监测 checklist

Admin 七子卡 hub · embedding 合并

全链路脚本在 step 11 platform publish 写回 live_url 后调用 GET /api/v1/app/publications/citation-monitoring-status GET /api/v1/cron/citation-scheduler GET /api/v1/admin/m5/citation-ops-hub-status 与聚合 GET /api/v1/app/demo/m5-citation-ops-steps,末尾 echo Settings / 本页锚点 / Admin hub URL。登录态实时状态见 账户设置 「全链路演示 · M5 引用监测运维步骤」卡;Admin 七子卡汇总见 运营后台 「M5 引用监测 · 汇总运维入口」卡(monitoring/comparison/capture/CSV/L2-L3/embedding/证据趋势);step 11e embedding 已并入本页 checklist。

  1. 1.GET publications/citation-monitoring-status(L1 eligible + citation_hits 计数)GET /api/v1/app/publications/citation-monitoring-status?brandId=
  2. 2.POST record-citation-l1-scan(手动 L1 直引 · 演示/补扫)POST /api/v1/app/publications/:id/record-citation-l1-scan
  3. 3.GET citation-scheduler(D+1/3/7/14/30 自动调度 Cron MVP)GET /api/v1/cron/citation-scheduler
  4. 4.D+ Cron 自动 capture + 链 L2/L3(captured/chained 计数)GET /api/v1/cron/citation-scheduler
  5. 5.GET admin/m5/citation-ops-hub-status(七子卡汇总运维入口 · 含 embedding)GET /api/v1/admin/m5/citation-ops-hub-status
  6. 6.GET m5/embedding-status(step 11e 并入 citation ops hub · 租户 embedding checklist)GET /api/v1/app/m5/embedding-status
  7. 7.POST record-citation-l3-scan(L3 语义引 · 优先 embedding cosine)POST /api/v1/app/publications/:id/record-citation-l3-scan
  8. 8.GET admin/m5/embedding-ops-status(hub 七子卡 embedding 锚点)GET /api/v1/admin/m5/embedding-ops-status
  9. 9.租户证据抽屉 + 30 天引用趋势(evidence-context / citation-trend-context)GET …/citation-hits/:hitId/evidence-context · GET …/citation-trend-context
  10. 10.聚合 GET demo/m5-citation-ops-steps(step 11d+11e merge)+ 末尾 echo Settings / docs / /admin hubGET /api/v1/app/demo/m5-citation-ops-steps

step 11e · M5 embedding Admin 运维 checklist

M5 embedding

全链路脚本在 step 11d M5 引用监测 checklist 后调用 GET /api/v1/app/m5/embedding-status POST …/record-citation-l3-scan GET /api/v1/admin/m5/embedding-ops-status 与聚合 GET /api/v1/app/demo/m5-embedding-ops-steps,末尾 echo Settings / 本页锚点 / Admin M5 L3 embedding 运维卡 URL。登录态实时状态见 账户设置 「M5 embedding Admin 运维步骤」卡;Admin 运维见 运营后台 「M5 L3 embedding API 运维」卡与 PRD 缺口路线图

  1. 1.GET m5/embedding-status(租户 L3 embedding API checklist + PublicationDetail 卡)GET /api/v1/app/m5/embedding-status
  2. 2.POST record-citation-l3-scan(L3 语义引 · 优先 embedding cosine ≥ 0.85)POST /api/v1/app/publications/:id/record-citation-l3-scan
  3. 3.GET admin/m5/embedding-ops-status(全库 L3/semantic scan 计数 + PRD 缺口)GET /api/v1/admin/m5/embedding-ops-status
  4. 4.Admin 首页「M5 L3 embedding API 运维」卡 checklist 对齐 + hub 七子卡 embedding 锚点GET /api/v1/admin/m5/embedding-ops-status
  5. 5.聚合 GET demo/m5-embedding-ops-steps + step 11e + 末尾 echo Settings / docs / /admin embedding 运维卡GET /api/v1/app/demo/m5-embedding-ops-steps

扩展连接步骤(P0)

Chrome 扩展连接

安装扩展前可先调用 GET /api/v1/app/extension/connection-diagnosis 核对适配器、握手、OAuth 与 URL 回填骨架。登录态实时状态见 发布扩展引导 「扩展连接诊断」卡,或 账户设置 同名诊断卡。

  1. 1.Chrome 开发者模式加载 extension/ 目录extension/manifest.json + content.js postMessage 握手
  2. 2.控制台 postMessage 握手(wm-geo:extension-ping/pong)ExtensionPresenceDetector · /app/publish/extension
  3. 3.渠道 OAuth(POST /api/v1/channels/:channel/oauth/start → callback 存 token)GET …/extension/connection-diagnosis · Settings「已连接渠道」
  4. 4.OAuth exchange / refresh 骨架(HTTP 或 platform_skeleton)GET …/channels/:channel/oauth/exchange-status
  5. 5.GET extension/connection-diagnosis 连接诊断摘要GET …/extension/connection-diagnosis
  6. 6.Publish 扩展引导页「扩展连接诊断」卡 + checklistSummaryGET …/extension/connection-diagnosis + /app/publish/extension
  7. 7.脚本 step 11 + 末尾 echo demo/extension-connection-steps · connection-diagnosis · docs#extension-connection-stepsscripts/demo-full-business-path.sh
发布扩展引导 · 连接诊断卡·账户设置 · 扩展连接诊断·demo-full-business-path step 11 · e2e/demo-script-doc-extension-connection-echo-flow.spec.ts

step 11 · 扩展真发 checklist 步骤

P0 Chrome 扩展真发

全链路脚本在 OAuth + platform publish 后调用 GET /api/v1/app/demo/extension-real-publish-steps GET /api/v1/app/extension/real-publish-checklist-summary,末尾 echo Settings / 本页锚点 / 扩展引导 URL。登录态实时状态见 账户设置 「全链路演示 · 扩展真发 checklist 步骤」卡。

  1. 1.fillDraft + submitDraft + capturePublishedUrl DOM 骨架(四平台 adapter)GET …/extension/fill-draft-dom-skeleton · submit-draft-dom-skeleton · capture-published-url-dom-skeleton
  2. 2.AUTO_SUBMIT(NEXT_PUBLIC_WM_GEO_EXTENSION_AUTO_SUBMIT=1 或 extension/options)GET …/extension/auto-submit-status
  3. 3.AUTO_SUBMIT + platform publish API 联动GET …/extension/auto-submit-platform-linkage
  4. 4.GET extension/real-publish-checklist-summary 汇总(P0 扩展真发)GET …/extension/real-publish-checklist-summary
  5. 5.知乎 OAuth 已存(step 11 前置;dev bypass 或生产凭据)GET …/me/integrations
  6. 6.脚本 step 11 + 末尾 echo demo/extension-real-publish-steps · real-publish-checklist-summary · docs 锚点scripts/demo-full-business-path.sh
账户设置 · 扩展真发步骤卡·发布扩展引导·e2e/demo-script-doc-extension-real-publish-echo-flow.spec.ts

四平台 DOM 真调验收卡

P0 Chrome 扩展真发

各平台编辑页须在本机 Chrome 加载 extension/ 后按下方验收步骤真调 fillDraft → submitDraft(须 AUTO_SUBMIT)→ capturePublishedUrl。 每平台须同时满足 stable-dom-selectors stable-url-capture 注册表对齐(acceptanceDepthReady)。 登录态实时状态见 GET /api/v1/app/extension/dom-tuning-acceptance 发布扩展引导 「四平台 DOM 真调验收卡」。

小红书https://creator.xiaohongshu.com/publish/publishextension/platforms/xiaohongshu/README.md
  1. 1.fillDraft:标题/正文控件写入open-draft 后 DevTools 确认 title/body 已填入;找不到控件时 console.warn
  2. 2.submitDraft:发布按钮点击(须 AUTO_SUBMIT)开启 AUTO_SUBMIT 后观察是否点击「发布/发表」;默认仅半自动填表
  3. 3.capturePublishedUrl:发布后 URL 写回 live_url发布后检查 window.location.href 或 platform publish API fallback → background POST live-url-from-extension
抖音https://creator.douyin.com/creator-micro/content/uploadextension/platforms/douyin/README.md
  1. 1.fillDraft:标题/描述控件写入open-draft 后确认标题与正文/描述 textarea 或 contenteditable 已写入
  2. 2.submitDraft:发布按钮点击(须 AUTO_SUBMIT)开启 AUTO_SUBMIT 后观察「发布作品/立即发布」按钮是否被点击
  3. 3.capturePublishedUrl:发布后 URL 写回 live_url发布后 URL 须匹配 douyin.com 域名;无 URL 时走 platform publish API fallback
知乎https://zhuanlan.zhihu.com/writeextension/platforms/zhihu/README.md
  1. 1.fillDraft:Draft.js 标题/正文写入确认 .WriteIndex-titleInput 与 .public-DraftEditor-content 或等价节点已写入
  2. 2.submitDraft:发布按钮点击(须 AUTO_SUBMIT)开启 AUTO_SUBMIT 后观察「发布/发表」按钮;OAuth token 经 submitDraft 骨架传入
  3. 3.capturePublishedUrl:专栏 URL 写回 live_url发布后 URL 须匹配 zhuanlan.zhihu.com/p/ 或 www.zhihu.com 模式
微信公众号https://mp.weixin.qq.com/extension/platforms/wechat/README.md
  1. 1.fillDraft:#title / #js_editor 写入open-draft 后确认标题 input 与 ProseMirror/contenteditable 正文已写入
  2. 2.submitDraft:群发/发表按钮(须 AUTO_SUBMIT)开启 AUTO_SUBMIT 后观察「发表/群发/保存并群发」按钮;须已登录 mp 后台
  3. 3.capturePublishedUrl:mp 域名 URL 写回 live_url发布后 URL 须匹配 mp.weixin.qq.com;弹窗/审核延迟可能导致捕获失败
发布扩展引导 · 验收卡·账户设置·e2e/extension-dom-tuning-acceptance-deepening-flow.spec.ts

稳定 DOM 选择器注册表

P0 primary / fallback 分层

四平台 adapter 的 title/body/publishButton/publishedUrlPattern 选择器分为 primary(平台特化)与 fallback(启发式)。 只读摘要见 GET /api/v1/app/extension/stable-dom-selectors 发布扩展引导 「稳定 DOM 选择器注册表」卡。

小红书extension/platforms/xiaohongshu/adapter.jsprimary 4 · fallback 6
  • title · [data-testid="note-title"]笔记标题 data-testid
  • body · [data-testid="note-content"]笔记正文 data-testid
  • publishButton · button[data-testid*="publish"]发布按钮 data-testid
  • publishedUrlPattern · ^https://www\.xiaohongshu\.com/笔记 URL 模式
抖音extension/platforms/douyin/adapter.jsprimary 4 · fallback 5
  • title · input[class*="title"]标题 input class
  • body · textarea[class*="desc"]描述 textarea class
  • publishButton · button[class*="publish"]发布按钮 class
  • publishedUrlPattern · ^https://www\.douyin\.com/作品 URL 模式
知乎extension/platforms/zhihu/adapter.jsprimary 6 · fallback 4
  • title · .WriteIndex-titleInput input专栏标题 input
  • title · .WriteIndex-titleInput textarea专栏标题 textarea
  • body · .public-DraftEditor-contentDraft.js 正文
  • body · .DraftEditor-root [contenteditable="true"]DraftEditor contenteditable
  • publishButton · .PublishPanel-stepOneBtn发布面板按钮
  • publishedUrlPattern · ^https://zhuanlan\.zhihu\.com/p/专栏文章 URL
微信公众号extension/platforms/wechat/adapter.jsprimary 5 · fallback 3
  • title · #title标题 #title
  • body · #js_editor正文 #js_editor
  • body · .ProseMirrorProseMirror 编辑器
  • publishButton · #js_send群发按钮 #js_send
  • publishedUrlPattern · ^https://mp\.weixin\.qq\.com/mp 域名 URL
发布扩展引导 · 稳定选择器·账户设置·e2e/extension-stable-dom-selectors-flow.spec.ts

Extension · 发布后 URL 稳定捕获 MVP

P0 · pattern 校验 + live_url gate

四平台 submitDraft 成功后,content.js 按 publishedUrlPattern(primary/fallback)校验 URL, 通过才经 background POST /api/v1/app/publications/:id/live-url-from-extension 写回 live_url。只读摘要:GET /api/v1/app/extension/stable-url-capture

  • 小红书

    • [primary] ^https://www\.xiaohongshu\.com/笔记 URL 模式
    • [fallback] ^https://creator\.xiaohongshu\.com/创作者页 URL
  • 抖音

    • [primary] ^https://www\.douyin\.com/作品 URL 模式
    • [fallback] ^https://creator\.douyin\.com/创作者页 URL
  • 知乎

    • [primary] ^https://zhuanlan\.zhihu\.com/p/专栏文章 URL
    • [fallback] ^https://www\.zhihu\.com/知乎域名 URL
  • 微信公众号

    • [primary] ^https://mp\.weixin\.qq\.com/mp 域名 URL

E2E: e2e/extension-stable-url-capture-flow.spec.ts

scripts/demo-full-business-path.sh

#!/usr/bin/env bash
# 全链路业务演示:匿名诊断 → 租户登录 → 品牌/关键词/扫描/报告 → 报告 PDF 导出 → 公开报告 API →
# 内容生成 → 读取 output 转 publications(POST bodyPlain)→ 发文发布 →
# 代发下单/指派 → 供应商履约 → 运营撤稿。
#
# 前置:
#   1. `npm run dev` 或 `npm run start` 已监听 BASE_URL(默认 http://127.0.0.1:3000)
#   2. `DATABASE_URL` 已配置且 `npx prisma migrate deploy` 已执行
#   3. 环境变量(见仓库根 `.env.example`):
#      - JWT_SECRET(≥16 字符)
#      - INTERNAL_JOB_SECRET(≥16 字符,用于 process-async-jobs)
#      - 本地建议:ADMIN_DEV_BYPASS=1(运营指派/撤稿,无需单独 admin 会话)
#      - 可选:MOCK_LLM=1 或任意 LLM Key(见 OPENAI_API_KEY / DEEPSEEK_API_KEY 等)
#      - 可选:WECHAT_PAY_DEV_MOCK=1 + WECHAT_PAY_DEV_NOTIFY=1 → step 2b mock 下单+notify 断言 subscriptions
#      - 演示扩展 OAuth:WM_GEO_CHANNEL_OAUTH_DEV_BYPASS=1(run-demo 默认导出)→ step 11 知乎 OAuth start+callback
#      - 演示 platform publish:WM_GEO_PLATFORM_PUBLISH_API_E2E=1 + …_ZHIHU_PUBLISH_API_URL(run-demo 默认导出)→ step 11 platform-publish-via-api
#   4. 依赖:curl、jq
#
# 用法:
#   ./scripts/run-demo-full-business-path.sh   # 推荐:自动导出 INTERNAL_JOB_SECRET / ADMIN_DEV_BYPASS / WECHAT_PAY_DEV_*
#   或手写:
#   export INTERNAL_JOB_SECRET='your-local-secret-at-least-16-chars'
#   export ADMIN_DEV_BYPASS=1
#   ./scripts/demo-full-business-path.sh
#
# 首次:chmod +x scripts/demo-full-business-path.sh

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

# 加载本地 env(不覆盖已 export 的变量)
load_env_file() {
  local f="$1"
  [[ -f "$f" ]] || return 0
  set -a
  # shellcheck disable=SC1090
  source "$f"
  set +a
}
load_env_file ".env"
load_env_file ".env.local"

BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
BASE_URL="${BASE_URL%/}"
COOKIE_JAR="${TMPDIR:-/tmp}/wm-geo-demo-$$.jar"
STAMP="$(date +%s)"
DEV_VENDOR_SUBJECT="${DEV_VENDOR_SUBJECT:-00000000-0000-0000-0000-00000000e002}"

step() {
  echo ""
  echo "==> $*"
}

fail() {
  echo "demo-full-business-path: 失败 — $*" >&2
  exit 1
}

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1"
}

json_field() {
  local json="$1"
  local filter="$2"
  echo "$json" | jq -r "$filter // empty"
}

assert_http() {
  local code="$1"
  local expected="$2"
  local label="$3"
  local body="${4:-}"
  if [[ "$code" != "$expected" ]]; then
    echo "$body" >&2
    fail "${label}: 期望 HTTP ${expected},实际 ${code}"
  fi
}

curl_json() {
  local method="$1"
  local url="$2"
  shift 2
  curl -sS -w "\n%{http_code}" -X "$method" "$url" "$@"
}

require_cmd curl
require_cmd jq

if [[ -z "${INTERNAL_JOB_SECRET:-}" ]]; then
  fail "未设置 INTERNAL_JOB_SECRET(≥16 字符,见 .env.example)"
fi
if [[ "${#INTERNAL_JOB_SECRET}" -lt 16 ]]; then
  fail "INTERNAL_JOB_SECRET 长度须 ≥16"
fi

step "0/15 健康检查 GET /api/v1/health"
health_out="$(curl_json GET "${BASE_URL}/api/v1/health")"
health_code="$(echo "$health_out" | tail -n1)"
health_body="$(echo "$health_out" | sed '$d')"
assert_http "$health_code" "200" "health" "$health_body"
echo "$health_body" | jq -c '{ok,note}' 2>/dev/null || echo "$health_body"

# —— 匿名诊断 ——
step "1/15 匿名诊断 POST /api/v1/diagnose/anon"
diag_out="$(curl_json POST "${BASE_URL}/api/v1/diagnose/anon" \
  -H "Content-Type: application/json" \
  -d "{\"brand\":\"演示品牌-${STAMP}\",\"industrySlug\":\"generic\",\"keywords\":[\"国内 GEO 监测工具有哪些?\"],\"competitors\":[\"友商A\"],\"engines\":[\"deepseek\"]}")"
diag_code="$(echo "$diag_out" | tail -n1)"
diag_body="$(echo "$diag_out" | sed '$d')"
assert_http "$diag_code" "200" "diagnose/anon" "$diag_body"
ANON_SHARE_TOKEN="$(json_field "$diag_body" '.shareToken')"
echo "shareToken=${ANON_SHARE_TOKEN:-(无库或未返回)}"

# —— 租户会话 ——
step "2/15 开发登录 POST /api/v1/auth/dev-session"
rm -f "$COOKIE_JAR"
login_out="$(curl_json POST "${BASE_URL}/api/v1/auth/dev-session" \
  -c "$COOKIE_JAR" -b "$COOKIE_JAR")"
login_code="$(echo "$login_out" | tail -n1)"
login_body="$(echo "$login_out" | sed '$d')"
assert_http "$login_code" "200" "dev-session" "$login_body"
echo "$login_body" | jq -c '{requestId,note}' 2>/dev/null || true

# —— 品牌 + 关键词 ——
step "3/15 创建品牌 POST /api/v1/app/brands"
brand_out="$(curl_json POST "${BASE_URL}/api/v1/app/brands" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"name\":\"demo-brand-${STAMP}\"}")"
brand_code="$(echo "$brand_out" | tail -n1)"
brand_body="$(echo "$brand_out" | sed '$d')"
assert_http "$brand_code" "201" "app/brands" "$brand_body"
BRAND_ID="$(json_field "$brand_body" '.brand.id')"
[[ -n "$BRAND_ID" ]] || fail "未解析 brand.id"
echo "brandId=$BRAND_ID"

# —— 可选:国内支付 mock 下单 + notify 履约(创建品牌后带 brandId)——
if [[ "${WECHAT_PAY_DEV_MOCK:-}" == "1" ]]; then
  step "3b/14(可选)微信 mock 下单 + notify → billing/subscription(brandId)"
  if [[ "${WECHAT_PAY_DEV_NOTIFY:-}" != "1" ]]; then
    echo "跳过 notify:未设 WECHAT_PAY_DEV_NOTIFY=1"
  else
    wx_create_out="$(curl_json POST "${BASE_URL}/api/v1/app/billing/wechat-pay/create" \
      -b "$COOKIE_JAR" -H "Content-Type: application/json" \
      -d "{\"planId\":\"solo\",\"brandId\":\"${BRAND_ID}\"}")"
    wx_create_code="$(echo "$wx_create_out" | tail -n1)"
    wx_create_body="$(echo "$wx_create_out" | sed '$d')"
    if [[ "$wx_create_code" == "503" ]]; then
      echo "跳过:微信未配置(503)"
    else
      assert_http "$wx_create_code" "200" "wechat-pay/create" "$wx_create_body"
      WX_OUT_TRADE_NO="$(json_field "$wx_create_body" '.out_trade_no')"
      [[ -n "$WX_OUT_TRADE_NO" ]] || fail "未解析 out_trade_no"
      wx_notify_out="$(curl_json POST "${BASE_URL}/api/v1/webhooks/wechat-pay" \
        -H "Content-Type: application/json" \
        -d "{\"id\":\"evt-demo-${STAMP}\",\"event_type\":\"TRANSACTION.SUCCESS\",\"resource_type\":\"encrypt-resource\",\"resource\":{\"out_trade_no\":\"${WX_OUT_TRADE_NO}\",\"transaction_id\":\"wx_demo_${STAMP}\",\"trade_state\":\"SUCCESS\"}}")"
      wx_notify_code="$(echo "$wx_notify_out" | tail -n1)"
      wx_notify_body="$(echo "$wx_notify_out" | sed '$d')"
      assert_http "$wx_notify_code" "200" "webhooks/wechat-pay" "$wx_notify_body"
      sub_out="$(curl_json GET "${BASE_URL}/api/v1/app/billing/subscription" -b "$COOKIE_JAR")"
      sub_code="$(echo "$sub_out" | tail -n1)"
      sub_body="$(echo "$sub_out" | sed '$d')"
      assert_http "$sub_code" "200" "billing/subscription" "$sub_body"
      sub_provider="$(json_field "$sub_body" '.provider')"
      sub_plan="$(json_field "$sub_body" '.planSlug')"
      sub_status="$(json_field "$sub_body" '.status')"
      [[ "$sub_provider" == "wechat" ]] || fail "期望 provider=wechat,实际 ${sub_provider:-空}"
      [[ "$sub_plan" == "solo" ]] || fail "期望 planSlug=solo,实际 ${sub_plan:-空}"
      [[ "$sub_status" == "active" ]] || fail "期望 status=active,实际 ${sub_status:-空}"
      wx_order_brand="$(json_field "$sub_body" '.lastCnPayOrder.brandId')"
      [[ "$wx_order_brand" == "$BRAND_ID" ]] || fail "期望 lastCnPayOrder.brandId=${BRAND_ID},实际 ${wx_order_brand:-空}"
      echo "billing/subscription: provider=${sub_provider} planSlug=${sub_plan} status=${sub_status} brandId=${wx_order_brand}"
    fi
  fi
else
  echo "(跳过微信 mock 履约:未设 WECHAT_PAY_DEV_MOCK=1)"
fi

KW_TEXT="demo-kw-${STAMP}"
import_out="$(curl_json POST "${BASE_URL}/api/v1/app/keywords/import" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"text\":\"${KW_TEXT}\"}")"
import_code="$(echo "$import_out" | tail -n1)"
assert_http "$import_code" "202" "keywords/import"

draft_out="$(curl_json GET "${BASE_URL}/api/v1/app/keywords/draft" -b "$COOKIE_JAR")"
draft_code="$(echo "$draft_out" | tail -n1)"
draft_body="$(echo "$draft_out" | sed '$d')"
assert_http "$draft_code" "200" "keywords/draft"
DRAFT_ITEMS="$(echo "$draft_body" | jq -c '.items // []')"
[[ "$(echo "$DRAFT_ITEMS" | jq 'length')" -gt 0 ]] || fail "关键词草稿为空"

save_out="$(curl_json POST "${BASE_URL}/api/v1/app/keywords/save" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"items\":${DRAFT_ITEMS}}")"
save_code="$(echo "$save_out" | tail -n1)"
assert_http "$save_code" "200" "keywords/save"

# —— 扫描 + 报告 ——
step "4/15 监测扫描 POST /api/v1/scans"
scan_out="$(curl_json POST "${BASE_URL}/api/v1/scans" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"brandId\":\"${BRAND_ID}\",\"keywords\":[\"${KW_TEXT}\"],\"engines\":[\"deepseek\"]}")"
scan_code="$(echo "$scan_out" | tail -n1)"
scan_body="$(echo "$scan_out" | sed '$d')"
assert_http "$scan_code" "201" "scans" "$scan_body"
SCAN_ID="$(json_field "$scan_body" '.scanId')"
REPORT_ID="$(json_field "$scan_body" '.reportId')"
SHARE_TOKEN="$(json_field "$scan_body" '.shareToken')"
[[ -n "$SCAN_ID" ]] || fail "未解析 scanId"
[[ -n "$REPORT_ID" ]] || fail "未解析 reportId"
echo "scanId=$SCAN_ID reportId=$REPORT_ID shareToken=$SHARE_TOKEN"

step "4b/15 监测详情 GET /api/v1/app/scans/:id"
scan_detail_out="$(curl_json GET "${BASE_URL}/api/v1/app/scans/${SCAN_ID}" -b "$COOKIE_JAR")"
scan_detail_code="$(echo "$scan_detail_out" | tail -n1)"
scan_detail_body="$(echo "$scan_detail_out" | sed '$d')"
assert_http "$scan_detail_code" "200" "app/scans/:id" "$scan_detail_body"
scan_detail_brand="$(json_field "$scan_detail_body" '.brandId')"
scan_detail_report="$(json_field "$scan_detail_body" '.reportId')"
[[ "$scan_detail_brand" == "$BRAND_ID" ]] || fail "期望 scan detail brandId=${BRAND_ID},实际 ${scan_detail_brand:-空}"
[[ "$scan_detail_report" == "$REPORT_ID" ]] || fail "期望 scan detail reportId=${REPORT_ID},实际 ${scan_detail_report:-空}"
echo "$scan_detail_body" | jq -c '{id,brandId,reportId,status}' 2>/dev/null || true

step "4c/15 内嵌报告 GET /api/v1/app/reports/:id/export?inline=1"
INLINE_TMP="${TMPDIR:-/tmp}/wm-geo-demo-report-inline-${STAMP}.html"
inline_http="$(curl -sS -o "$INLINE_TMP" -w "%{http_code}" \
  -b "$COOKIE_JAR" \
  "${BASE_URL}/api/v1/app/reports/${REPORT_ID}/export?inline=1")"
assert_http "$inline_http" "200" "reports/export?inline=1"
if ! head -c 512 "$INLINE_TMP" | grep -qiE '<html|<!DOCTYPE'; then
  fail "内嵌报告 HTML 校验失败(期望含 html 或 DOCTYPE)"
fi
inline_bytes="$(wc -c < "$INLINE_TMP" | tr -d ' ')"
echo "report.inline.bytes=${inline_bytes} path=${INLINE_TMP}"
rm -f "$INLINE_TMP"

step "5/15 报告列表 GET /api/v1/app/reports"
reports_out="$(curl_json GET "${BASE_URL}/api/v1/app/reports" -b "$COOKIE_JAR")"
reports_code="$(echo "$reports_out" | tail -n1)"
reports_body="$(echo "$reports_out" | sed '$d')"
assert_http "$reports_code" "200" "app/reports"
report_count="$(echo "$reports_body" | jq '.items | length')"
[[ "$report_count" -gt 0 ]] || fail "报告列表为空"
report_scan_id="$(echo "$reports_body" | jq -r --arg rid "$REPORT_ID" '.items[] | select(.id == $rid) | .scanId // empty' | head -n1)"
[[ "$report_scan_id" == "$SCAN_ID" ]] || fail "期望 reports 列表项 scanId=${SCAN_ID},实际 ${report_scan_id:-空}"
echo "reports.count=$report_count scanId=${report_scan_id}"

# —— 公开报告(/r/[token] 对应 API)——
step "6/15 竞品刷新 POST /api/v1/app/competitors/refresh + 异步机(有竞品数据时)"
comp_out="$(curl_json GET "${BASE_URL}/api/v1/app/competitors" -b "$COOKIE_JAR")"
comp_code="$(echo "$comp_out" | tail -n1)"
comp_body="$(echo "$comp_out" | sed '$d')"
assert_http "$comp_code" "200" "app/competitors" "$comp_body"
comp_count="$(echo "$comp_body" | jq '.competitors | length // 0')"
if [[ "$comp_count" -gt 0 ]]; then
  refresh_out="$(curl_json POST "${BASE_URL}/api/v1/app/competitors/refresh" -b "$COOKIE_JAR")"
  refresh_code="$(echo "$refresh_out" | tail -n1)"
  refresh_body="$(echo "$refresh_out" | sed '$d')"
  assert_http "$refresh_code" "202" "competitors/refresh" "$refresh_body"
  echo "$refresh_body" | jq -c '{accepted,jobId,note}' 2>/dev/null || true
  comp_jobs_out="$(curl_json POST "${BASE_URL}/api/v1/internal/process-async-jobs" \
    -H "Authorization: Bearer ${INTERNAL_JOB_SECRET}" \
    -H "Content-Type: application/json" \
    -d '{}')"
  comp_jobs_code="$(echo "$comp_jobs_out" | tail -n1)"
  comp_jobs_body="$(echo "$comp_jobs_out" | sed '$d')"
  assert_http "$comp_jobs_code" "200" "process-async-jobs (competitors)" "$comp_jobs_body"
  echo "$comp_jobs_body" | jq -c '{processed,succeeded,failed}' 2>/dev/null || true
else
  echo "跳过:品牌库无竞品数据(competitors.count=0)"
fi

step "7/15 报告 PDF 导出 GET /api/v1/app/reports/:id/export?format=pdf"
PDF_TMP="${TMPDIR:-/tmp}/wm-geo-demo-report-${STAMP}.pdf"
pdf_http="$(curl -sS -o "$PDF_TMP" -w "%{http_code}" \
  -b "$COOKIE_JAR" \
  "${BASE_URL}/api/v1/app/reports/${REPORT_ID}/export?format=pdf")"
assert_http "$pdf_http" "200" "reports/export?format=pdf"
if ! head -c 5 "$PDF_TMP" | grep -q '%PDF-'; then
  fail "报告 PDF 魔数校验失败(期望 %PDF-)"
fi
pdf_bytes="$(wc -c < "$PDF_TMP" | tr -d ' ')"
echo "report.pdf.bytes=${pdf_bytes} path=${PDF_TMP}"
rm -f "$PDF_TMP"

step "7b/15 M3/支付/PDF-OSS 租户运维 checklist GET demo/tenant-ops-steps + 子 API"
m3_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/m3/module-status?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
m3_ops_code="$(echo "$m3_ops_out" | tail -n1)"
m3_ops_body="$(echo "$m3_ops_out" | sed '$d')"
assert_http "$m3_ops_code" "200" "m3/module-status" "$m3_ops_body"
echo "$m3_ops_body" | jq -c '{readiness,checklistSummary,latestScanId,checklist:[.checklist[]|select(.key=="demo_script_tenant_ops_steps")]}' 2>/dev/null || true

pay_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/billing/pay-checklist-status" -b "$COOKIE_JAR")"
pay_ops_code="$(echo "$pay_ops_out" | tail -n1)"
pay_ops_body="$(echo "$pay_ops_out" | sed '$d')"
assert_http "$pay_ops_code" "200" "billing/pay-checklist-status" "$pay_ops_body"
echo "$pay_ops_body" | jq -c '{aggregateReadiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_tenant_ops_steps")]}' 2>/dev/null || true

oss_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/reports/export-oss-context?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
oss_ops_code="$(echo "$oss_ops_out" | tail -n1)"
oss_ops_body="$(echo "$oss_ops_out" | sed '$d')"
assert_http "$oss_ops_code" "200" "reports/export-oss-context" "$oss_ops_body"
echo "$oss_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_tenant_ops_steps")]}' 2>/dev/null || true

demo_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/tenant-ops-steps?brandId=${BRAND_ID}&pdfExportVerified=1" -b "$COOKIE_JAR")"
demo_ops_code="$(echo "$demo_ops_out" | tail -n1)"
demo_ops_body="$(echo "$demo_ops_out" | sed '$d')"
assert_http "$demo_ops_code" "200" "demo/tenant-ops-steps" "$demo_ops_body"
echo "$demo_ops_body" | jq -c '{readiness,pdfExportVerified,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_tenant_ops_steps")]}' 2>/dev/null || true

step "7d/15 国内支付/订阅 Admin 运维 checklist GET demo/cn-pay-ops-steps + 子 API"
admin_cn_pay_out="$(curl_json GET "${BASE_URL}/api/v1/admin/billing/cn-pay-production-status" -b "$COOKIE_JAR")"
admin_cn_pay_code="$(echo "$admin_cn_pay_out" | tail -n1)"
admin_cn_pay_body="$(echo "$admin_cn_pay_out" | sed '$d')"
if [[ "$admin_cn_pay_code" == "200" ]]; then
  echo "$admin_cn_pay_body" | jq -c '{readiness,cnPayOrdersPaid,cnPaySubscriptionsActive,cnPaySubscriptionsTotal,checklist:[.checklist[]|select(.key|test("admin_billing_subscriptions|demo_script_admin_cn_pay"))]}' 2>/dev/null || true
else
  echo "提示: admin/billing/cn-pay-production-status 返回 ${admin_cn_pay_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

admin_subs_out="$(curl_json GET "${BASE_URL}/api/v1/admin/billing/subscriptions?limit=5" -b "$COOKIE_JAR")"
admin_subs_code="$(echo "$admin_subs_out" | tail -n1)"
admin_subs_body="$(echo "$admin_subs_out" | sed '$d')"
if [[ "$admin_subs_code" == "200" ]]; then
  echo "$admin_subs_body" | jq -c '{items:(.items|length),hasMore}' 2>/dev/null || true
else
  echo "提示: admin/billing/subscriptions 返回 ${admin_subs_code}(需 finance/auditor 或 ADMIN_DEV_BYPASS=1)"
fi

demo_cn_pay_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/cn-pay-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_cn_pay_ops_code="$(echo "$demo_cn_pay_ops_out" | tail -n1)"
demo_cn_pay_ops_body="$(echo "$demo_cn_pay_ops_out" | sed '$d')"
assert_http "$demo_cn_pay_ops_code" "200" "demo/cn-pay-ops-steps" "$demo_cn_pay_ops_body"
echo "$demo_cn_pay_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key|test("demo_script_cn_pay_ops_steps|demo_script_cn_pay_notify_mobile_reverify"))]}' 2>/dev/null || true

pay_notify_mobile_reverify_out="$(curl_json GET "${BASE_URL}/api/v1/app/billing/payment-notify-mobile-reverify-status" -b "$COOKIE_JAR")"
pay_notify_mobile_reverify_code="$(echo "$pay_notify_mobile_reverify_out" | tail -n1)"
pay_notify_mobile_reverify_body="$(echo "$pay_notify_mobile_reverify_out" | sed '$d')"
assert_http "$pay_notify_mobile_reverify_code" "200" "billing/payment-notify-mobile-reverify-status" "$pay_notify_mobile_reverify_body"
echo "$pay_notify_mobile_reverify_body" | jq -c '{readiness,wechatNotifyUrl,alipayNotifyUrl,checklistSummary,checklist:[.checklist[]|select(.key|test("settings_payment_notify|wechat_notify_probe|alipay_notify_probe"))]}' 2>/dev/null || true

step "7e/15 PDF/OSS Admin 运维 checklist GET demo/pdf-oss-ops-steps + 子 API"
admin_pdf_oss_out="$(curl_json GET "${BASE_URL}/api/v1/admin/reports/export-oss-status" -b "$COOKIE_JAR")"
admin_pdf_oss_code="$(echo "$admin_pdf_oss_out" | tail -n1)"
admin_pdf_oss_body="$(echo "$admin_pdf_oss_out" | sed '$d')"
if [[ "$admin_pdf_oss_code" == "200" ]]; then
  echo "$admin_pdf_oss_body" | jq -c '{readiness,reportCount,completedScanCount,checklist:[.checklist[]|select(.key|test("admin_report_export_oss|demo_script_admin_pdf_oss"))]}' 2>/dev/null || true
else
  echo "提示: admin/reports/export-oss-status 返回 ${admin_pdf_oss_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_pdf_oss_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/pdf-oss-ops-steps?brandId=${BRAND_ID}&pdfExportVerified=1" -b "$COOKIE_JAR")"
demo_pdf_oss_ops_code="$(echo "$demo_pdf_oss_ops_out" | tail -n1)"
demo_pdf_oss_ops_body="$(echo "$demo_pdf_oss_ops_out" | sed '$d')"
assert_http "$demo_pdf_oss_ops_code" "200" "demo/pdf-oss-ops-steps" "$demo_pdf_oss_ops_body"
echo "$demo_pdf_oss_ops_body" | jq -c '{readiness,pdfExportVerified,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_pdf_oss_ops_steps")]}' 2>/dev/null || true

step "7c/15 定时 Cron 运维 checklist GET demo/cron-ops-steps + 子 API"
sched_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/monitor/schedule-status?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
sched_ops_code="$(echo "$sched_ops_out" | tail -n1)"
sched_ops_body="$(echo "$sched_ops_out" | sed '$d')"
assert_http "$sched_ops_code" "200" "monitor/schedule-status" "$sched_ops_body"
echo "$sched_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_cron_ops_steps")]}' 2>/dev/null || true

cron_ctx_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/monitor/cron-productization-context?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
cron_ctx_ops_code="$(echo "$cron_ctx_ops_out" | tail -n1)"
cron_ctx_ops_body="$(echo "$cron_ctx_ops_out" | sed '$d')"
assert_http "$cron_ctx_ops_code" "200" "monitor/cron-productization-context" "$cron_ctx_ops_body"
echo "$cron_ctx_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_cron_ops_steps")]}' 2>/dev/null || true

demo_cron_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/cron-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_cron_ops_code="$(echo "$demo_cron_ops_out" | tail -n1)"
demo_cron_ops_body="$(echo "$demo_cron_ops_out" | sed '$d')"
assert_http "$demo_cron_ops_code" "200" "demo/cron-ops-steps" "$demo_cron_ops_body"
echo "$demo_cron_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_cron_ops_steps")]}' 2>/dev/null || true

step "7f/15 定时 Cron Admin 运维 checklist GET demo/cron-admin-ops-steps + 子 API"
admin_cron_ops_out="$(curl_json GET "${BASE_URL}/api/v1/admin/monitor/cron-productization-status" -b "$COOKIE_JAR")"
admin_cron_ops_code="$(echo "$admin_cron_ops_out" | tail -n1)"
admin_cron_ops_body="$(echo "$admin_cron_ops_out" | sed '$d')"
if [[ "$admin_cron_ops_code" == "200" ]]; then
  echo "$admin_cron_ops_body" | jq -c '{readiness,tenantProductizationPageCount,scheduledPublicationCount,checklist:[.checklist[]|select(.key|test("admin_cron_productization|demo_script_admin_cron"))]}' 2>/dev/null || true
else
  echo "提示: admin/monitor/cron-productization-status 返回 ${admin_cron_ops_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_cron_admin_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/cron-admin-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_cron_admin_ops_code="$(echo "$demo_cron_admin_ops_out" | tail -n1)"
demo_cron_admin_ops_body="$(echo "$demo_cron_admin_ops_out" | sed '$d')"
assert_http "$demo_cron_admin_ops_code" "200" "demo/cron-admin-ops-steps" "$demo_cron_admin_ops_body"
echo "$demo_cron_admin_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_cron_admin_ops_steps")]}' 2>/dev/null || true

step "7g/15 M3 Admin 运维 checklist GET demo/m3-admin-ops-steps + 子 API"
admin_m3_ops_out="$(curl_json GET "${BASE_URL}/api/v1/admin/m3/module-status" -b "$COOKIE_JAR")"
admin_m3_ops_code="$(echo "$admin_m3_ops_out" | tail -n1)"
admin_m3_ops_body="$(echo "$admin_m3_ops_out" | sed '$d')"
if [[ "$admin_m3_ops_code" == "200" ]]; then
  echo "$admin_m3_ops_body" | jq -c '{readiness,brandCount,generationCount,checklist:[.checklist[]|select(.key|test("admin_m3_module|demo_script_admin_m3"))]}' 2>/dev/null || true
else
  echo "提示: admin/m3/module-status 返回 ${admin_m3_ops_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_m3_admin_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/m3-admin-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_m3_admin_ops_code="$(echo "$demo_m3_admin_ops_out" | tail -n1)"
demo_m3_admin_ops_body="$(echo "$demo_m3_admin_ops_out" | sed '$d')"
assert_http "$demo_m3_admin_ops_code" "200" "demo/m3-admin-ops-steps" "$demo_m3_admin_ops_body"
echo "$demo_m3_admin_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_m3_admin_ops_steps")]}' 2>/dev/null || true

step "7h/15 extension Admin 运维 checklist GET demo/extension-admin-ops-steps + 子 API"
admin_ext_rp_out="$(curl_json GET "${BASE_URL}/api/v1/admin/extension-real-publish-status" -b "$COOKIE_JAR")"
admin_ext_rp_code="$(echo "$admin_ext_rp_out" | tail -n1)"
admin_ext_rp_body="$(echo "$admin_ext_rp_out" | sed '$d')"
if [[ "$admin_ext_rp_code" == "200" ]]; then
  echo "$admin_ext_rp_body" | jq -c '{readiness,liveUrlCount,oauthUserCount,checklist:[.checklist[]|select(.key|test("admin_extension_real_publish|demo_script_admin_extension"))]}' 2>/dev/null || true
else
  echo "提示: admin/extension-real-publish-status 返回 ${admin_ext_rp_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_ext_admin_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/extension-admin-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_ext_admin_ops_code="$(echo "$demo_ext_admin_ops_out" | tail -n1)"
demo_ext_admin_ops_body="$(echo "$demo_ext_admin_ops_out" | sed '$d')"
assert_http "$demo_ext_admin_ops_code" "200" "demo/extension-admin-ops-steps" "$demo_ext_admin_ops_body"
echo "$demo_ext_admin_ops_body" | jq -c '{readiness,adminExtensionCardCount,adminExtensionCardsReadyCount,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_extension_admin_ops_steps")]}' 2>/dev/null || true

step "7i/15 platform publish Admin 运维 checklist GET demo/platform-publish-admin-ops-steps + 子 API"
admin_pp_ops_out="$(curl_json GET "${BASE_URL}/api/v1/admin/platform-publish-status" -b "$COOKIE_JAR")"
admin_pp_ops_code="$(echo "$admin_pp_ops_out" | tail -n1)"
admin_pp_ops_body="$(echo "$admin_pp_ops_out" | sed '$d')"
if [[ "$admin_pp_ops_code" == "200" ]]; then
  echo "$admin_pp_ops_body" | jq -c '{readiness,httpReadyChannelCount,oauthUserCount,liveUrlCount,platformPublishViaApiCount,checklist:[.checklist[]|select(.key|test("admin_platform_publish|demo_script_admin_platform"))]}' 2>/dev/null || true
else
  echo "提示: admin/platform-publish-status 返回 ${admin_pp_ops_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_pp_admin_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/platform-publish-admin-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_pp_admin_ops_code="$(echo "$demo_pp_admin_ops_out" | tail -n1)"
demo_pp_admin_ops_body="$(echo "$demo_pp_admin_ops_out" | sed '$d')"
assert_http "$demo_pp_admin_ops_code" "200" "demo/platform-publish-admin-ops-steps" "$demo_pp_admin_ops_body"
echo "$demo_pp_admin_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_platform_publish_admin_ops_steps")]}' 2>/dev/null || true

step "7j/15 Admin 运维总 hub checklist GET demo/admin-ops-hub-steps + 子 API"
admin_ops_hub_out="$(curl_json GET "${BASE_URL}/api/v1/admin/ops-hub-status" -b "$COOKIE_JAR")"
admin_ops_hub_code="$(echo "$admin_ops_hub_out" | tail -n1)"
admin_ops_hub_body="$(echo "$admin_ops_hub_out" | sed '$d')"
if [[ "$admin_ops_hub_code" == "200" ]]; then
  echo "$admin_ops_hub_body" | jq -c '{readiness,subAreaCount,subAreasReadyCount,checklist:[.checklist[]|select(.key|test("admin_ops_hub|demo_script_admin_ops_hub"))]}' 2>/dev/null || true
else
  echo "提示: admin/ops-hub-status 返回 ${admin_ops_hub_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_admin_ops_hub_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/admin-ops-hub-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_admin_ops_hub_code="$(echo "$demo_admin_ops_hub_out" | tail -n1)"
demo_admin_ops_hub_body="$(echo "$demo_admin_ops_hub_out" | sed '$d')"
assert_http "$demo_admin_ops_hub_code" "200" "demo/admin-ops-hub-steps" "$demo_admin_ops_hub_body"
echo "$demo_admin_ops_hub_body" | jq -c '{readiness,adminOpsAreaCount,adminOpsAreasReadyCount,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_admin_ops_hub")]}' 2>/dev/null || true

step "7k/16 FC 部署 readiness + full-chain-hub merge GET deploy/app-url-status + admin/deploy-readiness-status + demo/deploy-readiness-ops-steps + demo/full-chain-hub-steps"
app_url_status_out="$(curl_json GET "${BASE_URL}/api/v1/app/deploy/app-url-status" -b "$COOKIE_JAR")"
app_url_status_code="$(echo "$app_url_status_out" | tail -n1)"
app_url_status_body="$(echo "$app_url_status_out" | sed '$d')"
assert_http "$app_url_status_code" "200" "deploy/app-url-status" "$app_url_status_body"
echo "$app_url_status_body" | jq -c '{readiness,resolvedOrigin,checklistSummary,checklist:[.checklist[]|select(.key|test("settings_deploy|demo_script_deploy"))]}' 2>/dev/null || true

admin_deploy_out="$(curl_json GET "${BASE_URL}/api/v1/admin/deploy-readiness-status" -b "$COOKIE_JAR")"
admin_deploy_code="$(echo "$admin_deploy_out" | tail -n1)"
admin_deploy_body="$(echo "$admin_deploy_out" | sed '$d')"
if [[ "$admin_deploy_code" == "200" ]]; then
  echo "$admin_deploy_body" | jq -c '{readiness,appUrlReadiness,databaseConfigured,cronSecretConfigured,ossStorageConfigured,checklist:[.checklist[]|select(.key|test("admin_deploy|demo_script_deploy|fc_"))]}' 2>/dev/null || true
else
  echo "提示: admin/deploy-readiness-status 返回 ${admin_deploy_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
fi

demo_deploy_ops_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/deploy-readiness-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_deploy_ops_code="$(echo "$demo_deploy_ops_out" | tail -n1)"
demo_deploy_ops_body="$(echo "$demo_deploy_ops_out" | sed '$d')"
assert_http "$demo_deploy_ops_code" "200" "demo/deploy-readiness-ops-steps" "$demo_deploy_ops_body"
echo "$demo_deploy_ops_body" | jq -c '{readiness,checklistSummary,checklist:[.checklist[]|select(.key|test("demo_script_deploy_readiness"))]}' 2>/dev/null || true

demo_full_chain_hub_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/full-chain-hub-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
demo_full_chain_hub_code="$(echo "$demo_full_chain_hub_out" | tail -n1)"
demo_full_chain_hub_body="$(echo "$demo_full_chain_hub_out" | sed '$d')"
assert_http "$demo_full_chain_hub_code" "200" "demo/full-chain-hub-steps" "$demo_full_chain_hub_body"
echo "$demo_full_chain_hub_body" | jq -c '{readiness,dualHubsReadyCount,checklist:[.checklist[]|select(.key|test("demo_script_full_chain_hub|demo_script_deploy_readiness_full_chain"))]}' 2>/dev/null || true

step "7l/17 公网部署五件套汇总 GET deploy/deploy-readiness-status + health API 443 探测"
deploy_readiness_summary_out="$(curl_json GET "${BASE_URL}/api/v1/app/deploy/deploy-readiness-status" -b "$COOKIE_JAR")"
deploy_readiness_summary_code="$(echo "$deploy_readiness_summary_out" | tail -n1)"
deploy_readiness_summary_body="$(echo "$deploy_readiness_summary_out" | sed '$d')"
assert_http "$deploy_readiness_summary_code" "200" "deploy/deploy-readiness-status" "$deploy_readiness_summary_body"
echo "$deploy_readiness_summary_body" | jq -c '{readiness,subCardsReadyCount,subCardCount,healthApiProbePath,healthApiProbeSuccess,checklistSummary,checklist:[.checklist[]|select(.key|test("deploy_readiness|health_api|tenant_deploy"))]}' 2>/dev/null || true

step "7m/17 health API 公网验收 GET deploy/health-api-public-status + schemaVersion/summary/ok 契约"
health_api_public_out="$(curl_json GET "${BASE_URL}/api/v1/app/deploy/health-api-public-status" -b "$COOKIE_JAR")"
health_api_public_code="$(echo "$health_api_public_out" | tail -n1)"
health_api_public_body="$(echo "$health_api_public_out" | sed '$d')"
assert_http "$health_api_public_code" "200" "deploy/health-api-public-status" "$health_api_public_body"
echo "$health_api_public_body" | jq -c '{readiness,healthApiProbePath,httpStatus,healthSchemaVersion,healthOk,healthSummary,checklistSummary,checklist:[.checklist[]|select(.key|test("health_|e2e_|aliyun_d1_health"))]}' 2>/dev/null || true

step "7n/17 wm-geo.com 真机 HTTPS GET deploy/mobile-browser-https-status + Mobile Safari UA 根路径探测"
mobile_browser_https_out="$(curl_json GET "${BASE_URL}/api/v1/app/deploy/mobile-browser-https-status" -b "$COOKIE_JAR")"
mobile_browser_https_code="$(echo "$mobile_browser_https_out" | tail -n1)"
mobile_browser_https_body="$(echo "$mobile_browser_https_out" | sed '$d')"
assert_http "$mobile_browser_https_code" "200" "deploy/mobile-browser-https-status" "$mobile_browser_https_body"
echo "$mobile_browser_https_body" | jq -c '{readiness,homepageProbePath,httpStatus,wmGeoProductionHost,probeSuccess,checklistSummary,checklist:[.checklist[]|select(.key|test("mobile_|aliyun_d1_|settings_mobile"))]}' 2>/dev/null || true

step "7o/17 deploy 五件套纳入真机 HTTPS 子卡 merge(deploy-readiness-status subCardCount=5)"
deploy_readiness_five_piece_out="$(curl_json GET "${BASE_URL}/api/v1/app/deploy/deploy-readiness-status" -b "$COOKIE_JAR")"
deploy_readiness_five_piece_code="$(echo "$deploy_readiness_five_piece_out" | tail -n1)"
deploy_readiness_five_piece_body="$(echo "$deploy_readiness_five_piece_out" | sed '$d')"
assert_http "$deploy_readiness_five_piece_code" "200" "deploy/deploy-readiness-status five-piece" "$deploy_readiness_five_piece_body"
echo "$deploy_readiness_five_piece_body" | jq -c '{readiness,subCardCount,subCardsReadyCount,subCards:[.subCards[]|{key,readiness}],checklist:[.checklist[]|select(.key|test("five_piece|mobile_browser|tenant_deploy_readiness_sub_mobile"))]}' 2>/dev/null || true
five_piece_count="$(echo "$deploy_readiness_five_piece_body" | jq -r '.subCardCount // 0' 2>/dev/null || echo 0)"
if [[ "$five_piece_count" != "5" ]]; then
  echo "WARN: deploy-readiness-status subCardCount 期望 5,实际 ${five_piece_count}"
fi

step "8/17 公开报告 GET /api/v1/public-reports/:shareToken"
if [[ -n "$SHARE_TOKEN" ]]; then
  pub_out="$(curl_json GET "${BASE_URL}/api/v1/public-reports/${SHARE_TOKEN}")"
  pub_code="$(echo "$pub_out" | tail -n1)"
  pub_body="$(echo "$pub_out" | sed '$d')"
  assert_http "$pub_code" "200" "public-reports" "$pub_body"
  echo "$pub_body" | jq -c '{shareToken,brandName,overallScore,engines:(.engines|length),competitors:(.competitors|length)}'
else
  echo "跳过:扫描未返回 shareToken"
fi

# —— 内容生成 ——
step "9/15 内容生成 POST /api/v1/app/generate/run + 异步机"
gen_out="$(curl_json POST "${BASE_URL}/api/v1/app/generate/run" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"brandId\":\"${BRAND_ID}\",\"templateId\":\"faq\"}")"
gen_code="$(echo "$gen_out" | tail -n1)"
gen_body="$(echo "$gen_out" | sed '$d')"
assert_http "$gen_code" "202" "generate/run" "$gen_body"
JOB_ID="$(json_field "$gen_body" '.jobId')"
echo "asyncJobId=$JOB_ID"

jobs_out="$(curl_json POST "${BASE_URL}/api/v1/internal/process-async-jobs" \
  -H "Authorization: Bearer ${INTERNAL_JOB_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{}')"
jobs_code="$(echo "$jobs_out" | tail -n1)"
jobs_body="$(echo "$jobs_out" | sed '$d')"
assert_http "$jobs_code" "200" "process-async-jobs" "$jobs_body"
echo "$jobs_body" | jq -c '{processed,succeeded,failed,note}'

gen_list_out="$(curl_json GET "${BASE_URL}/api/v1/app/generate/list" -b "$COOKIE_JAR")"
gen_list_code="$(echo "$gen_list_out" | tail -n1)"
gen_list_body="$(echo "$gen_list_out" | sed '$d')"
assert_http "$gen_list_code" "200" "generate/list"
GEN_ID="$(echo "$gen_list_body" | jq -r '.items[0].id // empty')"
echo "latestGenerationId=${GEN_ID:-(无)}"

GEN_BODY_PLAIN=""
if [[ -n "$GEN_ID" ]]; then
  gen_detail_out="$(curl_json GET "${BASE_URL}/api/v1/app/generate/${GEN_ID}" -b "$COOKIE_JAR")"
  gen_detail_code="$(echo "$gen_detail_out" | tail -n1)"
  gen_detail_body="$(echo "$gen_detail_out" | sed '$d')"
  if [[ "$gen_detail_code" == "200" ]]; then
    GEN_BODY_PLAIN="$(json_field "$gen_detail_body" '.output')"
    echo "generation.output.len=${#GEN_BODY_PLAIN}"
  else
    echo "提示: GET generate/${GEN_ID} 返回 ${gen_detail_code},发文将不带 bodyPlain"
  fi
fi
if [[ -z "$GEN_BODY_PLAIN" ]]; then
  GEN_BODY_PLAIN="演示正文(demo-full-business-path ${STAMP})"
  echo "使用占位 bodyPlain(无 generation 输出)"
fi

# —— 发文发布(generate → publications,bodyPlain 写入 metrics_json)——
step "10/15 发文 POST /api/v1/app/publications(bodyPlain 来自生成)+ publish"
pub_create_payload="$(jq -nc \
  --arg title "demo-pub-${STAMP}" \
  --arg bodyPlain "$GEN_BODY_PLAIN" \
  --arg brandId "$BRAND_ID" \
  '{title: $title, bodyPlain: $bodyPlain, brandId: $brandId, channel: "知乎"}')"
pub_create_out="$(curl_json POST "${BASE_URL}/api/v1/app/publications" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "$pub_create_payload")"
pub_create_code="$(echo "$pub_create_out" | tail -n1)"
pub_create_body="$(echo "$pub_create_out" | sed '$d')"
assert_http "$pub_create_code" "201" "publications" "$pub_create_body"
PUB_ID="$(json_field "$pub_create_body" '.publication.id')"
[[ -n "$PUB_ID" ]] || fail "未解析 publication.id"

pub_run_out="$(curl_json POST "${BASE_URL}/api/v1/app/publications/${PUB_ID}/publish" \
  -b "$COOKIE_JAR")"
pub_run_code="$(echo "$pub_run_out" | tail -n1)"
pub_run_body="$(echo "$pub_run_out" | sed '$d')"
assert_http "$pub_run_code" "200" "publications/publish" "$pub_run_body"
echo "$pub_run_body" | jq -c '{status,jobId,message}' 2>/dev/null || true

# 再跑一次异步机(publication_publish)
curl -fsS -X POST "${BASE_URL}/api/v1/internal/process-async-jobs" \
  -H "Authorization: Bearer ${INTERNAL_JOB_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{}' >/dev/null

# —— 扩展 OAuth + platform publish(知乎 dev bypass + E2E mock,对齐 extension open-draft / platform-publish-via-api)——
step "11/15 扩展 OAuth + platform publish POST channels/zhihu/oauth/start + platform-publish-via-api"
if [[ "${WM_GEO_CHANNEL_OAUTH_DEV_BYPASS:-}" != "1" ]]; then
  echo "提示: 未设 WM_GEO_CHANNEL_OAUTH_DEV_BYPASS=1,跳过 OAuth/platform publish 竖切(推荐 ./scripts/run-demo-full-business-path.sh)"
else
  oauth_start_out="$(curl_json POST "${BASE_URL}/api/v1/channels/zhihu/oauth/start" \
    -b "$COOKIE_JAR" -H "Content-Type: application/json" \
    -d '{"returnTo":"/app/settings"}')"
  oauth_start_code="$(echo "$oauth_start_out" | tail -n1)"
  oauth_start_body="$(echo "$oauth_start_out" | sed '$d')"
  assert_http "$oauth_start_code" "200" "channels/zhihu/oauth/start" "$oauth_start_body"
  OAUTH_AUTHORIZE_URL="$(json_field "$oauth_start_body" '.authorizeUrl')"
  [[ -n "$OAUTH_AUTHORIZE_URL" ]] || fail "未解析 authorizeUrl"
  echo "$oauth_start_body" | jq -c '{channel,readiness,devBypass}' 2>/dev/null || true

  oauth_cb_code="$(curl -sS -o /dev/null -w "%{http_code}" -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
    "${OAUTH_AUTHORIZE_URL}")"
  if [[ "$oauth_cb_code" != "200" && "$oauth_cb_code" != "302" && "$oauth_cb_code" != "307" ]]; then
    fail "oauth callback: 期望 HTTP 200/302/307,实际 ${oauth_cb_code}"
  fi
  echo "oauth.callback.http=${oauth_cb_code}"

  int_out="$(curl_json GET "${BASE_URL}/api/v1/me/integrations" -b "$COOKIE_JAR")"
  int_code="$(echo "$int_out" | tail -n1)"
  int_body="$(echo "$int_out" | sed '$d')"
  assert_http "$int_code" "200" "me/integrations" "$int_body"
  OAUTH_CONNECTED="$(json_field "$int_body" '.extension.oauth.connectedCount')"
  echo "extension.oauth.connectedCount=${OAUTH_CONNECTED:-0}"
  if [[ "${OAUTH_CONNECTED:-0}" -lt 1 ]]; then
    fail "OAuth callback 后 connectedCount 应 ≥ 1"
  fi

  demo_oauth_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/extension-oauth-steps" -b "$COOKIE_JAR")"
  demo_oauth_code="$(echo "$demo_oauth_out" | tail -n1)"
  demo_oauth_body="$(echo "$demo_oauth_out" | sed '$d')"
  assert_http "$demo_oauth_code" "200" "demo/extension-oauth-steps" "$demo_oauth_body"
  echo "$demo_oauth_body" | jq -c '{readiness,connectedCount,checklist:[.checklist[]|select(.key=="demo_script_extension_oauth_steps")]}' 2>/dev/null || true

  if [[ "${WM_GEO_PLATFORM_PUBLISH_API_E2E:-}" == "1" && -n "${PUB_ID:-}" ]]; then
    pub_title="$(json_field "$pub_create_body" '.publication.title')"
    pub_pp_out="$(curl_json POST "${BASE_URL}/api/v1/app/publications/${PUB_ID}/platform-publish-via-api" \
      -b "$COOKIE_JAR" -H "Content-Type: application/json" \
      -d "$(jq -nc --arg title "${pub_title:-demo-pub-${STAMP}}" --arg bodyPlain "$GEN_BODY_PLAIN" '{title:$title,bodyPlain:$bodyPlain}')")"
    pub_pp_code="$(echo "$pub_pp_out" | tail -n1)"
    pub_pp_body="$(echo "$pub_pp_out" | sed '$d')"
    assert_http "$pub_pp_code" "200" "publications/platform-publish-via-api" "$pub_pp_body"
    PUB_LIVE_URL="$(json_field "$pub_pp_body" '.publication.liveUrl // .platformPublish.publishedUrl // empty')"
    echo "$pub_pp_body" | jq -c '{platformPublish:{ok,httpCalled,publishedUrl},publication:{liveUrl,status}}' 2>/dev/null || true

    demo_pp_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/platform-publish-steps" -b "$COOKIE_JAR")"
    demo_pp_code="$(echo "$demo_pp_out" | tail -n1)"
    demo_pp_body="$(echo "$demo_pp_out" | sed '$d')"
    assert_http "$demo_pp_code" "200" "demo/platform-publish-steps" "$demo_pp_body"
    echo "$demo_pp_body" | jq -c '{readiness,platformPublishOk,publishedUrl,checklist:[.checklist[]|select(.key=="demo_script_platform_publish_steps")]}' 2>/dev/null || true
  else
    echo "提示: 未设 WM_GEO_PLATFORM_PUBLISH_API_E2E=1,跳过 platform-publish-via-api(推荐 ./scripts/run-demo-full-business-path.sh)"
  fi

  demo_erp_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/extension-real-publish-steps" -b "$COOKIE_JAR")"
  demo_erp_code="$(echo "$demo_erp_out" | tail -n1)"
  demo_erp_body="$(echo "$demo_erp_out" | sed '$d')"
  assert_http "$demo_erp_code" "200" "demo/extension-real-publish-steps" "$demo_erp_body"
  echo "$demo_erp_body" | jq -c '{readiness,domSkeletonReady,connectedCount,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_extension_real_publish_steps")]}' 2>/dev/null || true

  rp_sum_out="$(curl_json GET "${BASE_URL}/api/v1/app/extension/real-publish-checklist-summary" -b "$COOKIE_JAR")"
  rp_sum_code="$(echo "$rp_sum_out" | tail -n1)"
  rp_sum_body="$(echo "$rp_sum_out" | sed '$d')"
  assert_http "$rp_sum_code" "200" "extension/real-publish-checklist-summary" "$rp_sum_body"
  echo "$rp_sum_body" | jq -c '{readiness,checklistSummary}' 2>/dev/null || true

  demo_ec_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/extension-connection-steps" -b "$COOKIE_JAR")"
  demo_ec_code="$(echo "$demo_ec_out" | tail -n1)"
  demo_ec_body="$(echo "$demo_ec_out" | sed '$d')"
  assert_http "$demo_ec_code" "200" "demo/extension-connection-steps" "$demo_ec_body"
  echo "$demo_ec_body" | jq -c '{readiness,diagnosisReadiness,connectedCount,checklistSummary,checklist:[.checklist[]|select(.key=="demo_script_extension_connection_steps")]}' 2>/dev/null || true

  conn_diag_out="$(curl_json GET "${BASE_URL}/api/v1/app/extension/connection-diagnosis" -b "$COOKIE_JAR")"
  conn_diag_code="$(echo "$conn_diag_out" | tail -n1)"
  conn_diag_body="$(echo "$conn_diag_out" | sed '$d')"
  assert_http "$conn_diag_code" "200" "extension/connection-diagnosis" "$conn_diag_body"
  echo "$conn_diag_body" | jq -c '{readiness,connectedCount,checklistSummary}' 2>/dev/null || true

  step "11d/15 M5 引用监测 + embedding merge checklist GET citation-monitoring-status + citation-scheduler + demo/m5-citation-ops-steps"
  if [[ -n "${PUB_ID:-}" ]]; then
    cite_mon_out="$(curl_json GET "${BASE_URL}/api/v1/app/publications/citation-monitoring-status?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
    cite_mon_code="$(echo "$cite_mon_out" | tail -n1)"
    cite_mon_body="$(echo "$cite_mon_out" | sed '$d')"
    assert_http "$cite_mon_code" "200" "publications/citation-monitoring-status" "$cite_mon_body"
    echo "$cite_mon_body" | jq -c '{readiness,citationHitCount,citationScanCount,checklist:[.checklist[]|select(.key|test("citation_scheduler|demo_script_m5"))]}' 2>/dev/null || true

    cite_cron_out="$(curl_json GET "${BASE_URL}/api/v1/cron/citation-scheduler" \
      -H "Authorization: Bearer ${INTERNAL_JOB_SECRET}")"
    cite_cron_code="$(echo "$cite_cron_out" | tail -n1)"
    cite_cron_body="$(echo "$cite_cron_out" | sed '$d')"
    assert_http "$cite_cron_code" "200" "cron/citation-scheduler" "$cite_cron_body"
    echo "$cite_cron_body" | jq -c '{polledPublications,scanned,captured,chained,duePublications,note}' 2>/dev/null || true

    cite_rec_out="$(curl_json POST "${BASE_URL}/api/v1/app/publications/${PUB_ID}/record-citation-l1-scan" \
      -b "$COOKIE_JAR" -H "Content-Type: application/json" \
      -d '{"promptUsed":"demo-full-business-path step 11d"}')"
    cite_rec_code="$(echo "$cite_rec_out" | tail -n1)"
    cite_rec_body="$(echo "$cite_rec_out" | sed '$d')"
    assert_http "$cite_rec_code" "200" "publications/record-citation-l1-scan" "$cite_rec_body"
    echo "$cite_rec_body" | jq -c '{scanId,hitCount,checklist:[.checklist[]|select(.key=="m5_citation_hits_table")]}' 2>/dev/null || true

    demo_m5_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/m5-citation-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
    demo_m5_code="$(echo "$demo_m5_out" | tail -n1)"
    demo_m5_body="$(echo "$demo_m5_out" | sed '$d')"
    assert_http "$demo_m5_code" "200" "demo/m5-citation-ops-steps" "$demo_m5_body"
    echo "$demo_m5_body" | jq -c '{readiness,adminHubSubCardCount,citationHitCount,citationL3HitCount,embeddingApiConfigured,checklistSummary,checklist:[.checklist[]|select(.key|test("demo_script_m5"))],steps:[.steps[]|select(.key|test("embedding|l3_scan|admin_m5_embedding"))]}' 2>/dev/null || true

    step "11d/15 (cont.) M5 embedding merge within citation ops hub GET m5/embedding-status + record-citation-l3-scan"

    admin_m5_hub_out="$(curl_json GET "${BASE_URL}/api/v1/admin/m5/citation-ops-hub-status" -b "$COOKIE_JAR")"
    admin_m5_hub_code="$(echo "$admin_m5_hub_out" | tail -n1)"
    admin_m5_hub_body="$(echo "$admin_m5_hub_out" | sed '$d')"
    if [[ "$admin_m5_hub_code" == "200" ]]; then
      echo "$admin_m5_hub_body" | jq -c '{readiness,subCardCount,citationHitCount,checklist:[.checklist[]|select(.key|test("admin_m5_citation_ops_hub|demo_script_m5_admin"))]}' 2>/dev/null || true
      admin_m5_embed_out="$(curl_json GET "${BASE_URL}/api/v1/admin/m5/embedding-ops-status" -b "$COOKIE_JAR")"
      admin_m5_embed_code="$(echo "$admin_m5_embed_out" | tail -n1)"
      admin_m5_embed_body="$(echo "$admin_m5_embed_out" | sed '$d')"
      if [[ "$admin_m5_embed_code" == "200" ]]; then
        echo "$admin_m5_embed_body" | jq -c '{readiness,embeddingApiConfigured,citationL3HitCount,checklist:[.checklist[]|select(.key|test("admin_m5_embedding|m5_embedding"))]}' 2>/dev/null || true
      else
        echo "提示: admin/m5/embedding-ops-status 返回 ${admin_m5_embed_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
      fi
    else
      echo "提示: admin/m5/citation-ops-hub-status 返回 ${admin_m5_hub_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
    fi

    step "11d/15 (cont.) demo/m5-embedding-ops-steps cross-check + admin/m5/embedding-ops-status"
    embed_status_out="$(curl_json GET "${BASE_URL}/api/v1/app/m5/embedding-status" -b "$COOKIE_JAR")"
    embed_status_code="$(echo "$embed_status_out" | tail -n1)"
    embed_status_body="$(echo "$embed_status_out" | sed '$d')"
    assert_http "$embed_status_code" "200" "m5/embedding-status" "$embed_status_body"
    echo "$embed_status_body" | jq -c '{readiness,embeddingApiConfigured,embeddingApiMocked,citationL3HitCount,checklist:[.checklist[]|select(.key|test("m5_embedding|demo_script_m5_embedding"))]}' 2>/dev/null || true

    cite_l3_out="$(curl_json POST "${BASE_URL}/api/v1/app/publications/${PUB_ID}/record-citation-l3-scan" \
      -b "$COOKIE_JAR" -H "Content-Type: application/json" \
      -d '{"aiAnswerMvp":"品牌增长的核心策略是内容营销与GEO优化,必须长期持续投入才能建立可持续竞争力。"}')"
    cite_l3_code="$(echo "$cite_l3_out" | tail -n1)"
    cite_l3_body="$(echo "$cite_l3_out" | sed '$d')"
    assert_http "$cite_l3_code" "200" "publications/record-citation-l3-scan" "$cite_l3_body"
    echo "$cite_l3_body" | jq -c '{hitCount,embeddingUsed,embeddingMocked,checklist:[.checklist[]|select(.key|test("m5_l3|semantic"))]}' 2>/dev/null || true

    demo_m5_embed_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/m5-embedding-ops-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
    demo_m5_embed_code="$(echo "$demo_m5_embed_out" | tail -n1)"
    demo_m5_embed_body="$(echo "$demo_m5_embed_out" | sed '$d')"
    assert_http "$demo_m5_embed_code" "200" "demo/m5-embedding-ops-steps" "$demo_m5_embed_body"
    echo "$demo_m5_embed_body" | jq -c '{readiness,embeddingApiConfigured,citationL3HitCount,checklistSummary,checklist:[.checklist[]|select(.key|test("demo_script_m5_embedding"))]}' 2>/dev/null || true

    admin_m5_embed_out="$(curl_json GET "${BASE_URL}/api/v1/admin/m5/embedding-ops-status" -b "$COOKIE_JAR")"
    admin_m5_embed_code="$(echo "$admin_m5_embed_out" | tail -n1)"
    admin_m5_embed_body="$(echo "$admin_m5_embed_out" | sed '$d')"
    if [[ "$admin_m5_embed_code" == "200" ]]; then
      echo "$admin_m5_embed_body" | jq -c '{readiness,embeddingApiConfigured,citationL3HitCount,checklist:[.checklist[]|select(.key|test("admin_m5_embedding|demo_script_m5_embedding"))]}' 2>/dev/null || true
    else
      echo "提示: admin/m5/embedding-ops-status 返回 ${admin_m5_embed_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
    fi

    step "11d/15 (cont.) Settings 双 hub 总入口 GET demo/full-chain-hub-steps"
    demo_full_chain_hub_out="$(curl_json GET "${BASE_URL}/api/v1/app/demo/full-chain-hub-steps?brandId=${BRAND_ID}" -b "$COOKIE_JAR")"
    demo_full_chain_hub_code="$(echo "$demo_full_chain_hub_out" | tail -n1)"
    demo_full_chain_hub_body="$(echo "$demo_full_chain_hub_out" | sed '$d')"
    assert_http "$demo_full_chain_hub_code" "200" "demo/full-chain-hub-steps" "$demo_full_chain_hub_body"
    echo "$demo_full_chain_hub_body" | jq -c '{readiness,dualHubsReadyCount,dualHubCount,tenantDeploySubCardsReadyCount,tenantDeploySubCardCount,checklistSummary,checklist:[.checklist[]|select(.key|test("demo_script_full_chain_hub|deploy_four_piece"))],subHubs:[.subHubs[]|{key,readiness}],steps:[.steps[]|select(.key|test("deploy_tenant|admin_ops|m5_citation"))]}' 2>/dev/null || true

    admin_fc_env_out="$(curl_json GET "${BASE_URL}/api/v1/admin/fc-env-post-write-acceptance-status" -b "$COOKIE_JAR")"
    admin_fc_env_code="$(echo "$admin_fc_env_out" | tail -n1)"
    admin_fc_env_body="$(echo "$admin_fc_env_out" | sed '$d')"
    if [[ "$admin_fc_env_code" == "200" ]]; then
      echo "$admin_fc_env_body" | jq -c '{readiness,fcCoreEnvReadyCount,fcCoreEnvTotal,checklistSummary,checklist:[.checklist[]|select(.key|test("fc_post_write|admin_fc_env"))]}' 2>/dev/null || true
    else
      echo "提示: admin/fc-env-post-write-acceptance-status 返回 ${admin_fc_env_code}(需 admin 权限或 ADMIN_DEV_BYPASS=1)"
    fi
  else
    echo "提示: 无 PUB_ID,跳过 M5 引用监测 checklist(须 step 10 创建发文)"
  fi
fi

# —— 代发 + 供应商 ——
step "12/15 代发下单 POST /api/v1/app/sourcing/orders/create"
order_out="$(curl_json POST "${BASE_URL}/api/v1/app/sourcing/orders/create" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"sourceId\":\"demo-source-${STAMP}\",\"note\":\"demo-full-business-path\",\"brandId\":\"${BRAND_ID}\"}")"
order_code="$(echo "$order_out" | tail -n1)"
order_body="$(echo "$order_out" | sed '$d')"
assert_http "$order_code" "202" "sourcing/orders/create" "$order_body"
ORDER_ID="$(json_field "$order_body" '.orderId')"
[[ -n "$ORDER_ID" ]] || fail "未解析 orderId"
echo "orderId=$ORDER_ID"

step "13/15 运营指派 PATCH /api/v1/admin/source-orders/:id"
if [[ "${ADMIN_DEV_BYPASS:-}" != "1" ]]; then
  echo "提示: 未设 ADMIN_DEV_BYPASS=1,将使用租户 Cookie 调 admin(需 dev 用户 is_admin)"
fi
assign_out="$(curl_json PATCH "${BASE_URL}/api/v1/admin/source-orders/${ORDER_ID}" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d "{\"assignedVendorSubject\":\"${DEV_VENDOR_SUBJECT}\"}")"
assign_code="$(echo "$assign_out" | tail -n1)"
assign_body="$(echo "$assign_out" | sed '$d')"
assert_http "$assign_code" "200" "admin/source-orders" "$assign_body"
echo "$assign_body" | jq -c '{fulfillmentStatus,assignedVendorSubject}' 2>/dev/null || true

step "14/15 供应商履约 accept → deliver → complete"
VENDOR_JAR="${TMPDIR:-/tmp}/wm-geo-vendor-$$.jar"
vendor_login_out="$(curl_json POST "${BASE_URL}/api/v1/auth/vendor-dev-session" \
  -c "$VENDOR_JAR" -b "$VENDOR_JAR")"
vendor_login_code="$(echo "$vendor_login_out" | tail -n1)"
vendor_login_body="$(echo "$vendor_login_out" | sed '$d')"
assert_http "$vendor_login_code" "200" "vendor-dev-session" "$vendor_login_body"

DELIVER_URL="https://example.com/demo-${STAMP}"
for action in accept deliver complete; do
  if [[ "$action" == "deliver" ]]; then
    act_out="$(curl_json POST "${BASE_URL}/api/v1/vendor/tasks/${ORDER_ID}/${action}" \
      -b "$VENDOR_JAR" -H "Content-Type: application/json" \
      -d "{\"publishedUrl\":\"${DELIVER_URL}\"}")"
  else
    act_out="$(curl_json POST "${BASE_URL}/api/v1/vendor/tasks/${ORDER_ID}/${action}" \
      -b "$VENDOR_JAR")"
  fi
  act_code="$(echo "$act_out" | tail -n1)"
  act_body="$(echo "$act_out" | sed '$d')"
  assert_http "$act_code" "202" "vendor/${action}" "$act_body"
  echo "vendor.${action}: $(echo "$act_body" | jq -c '.' 2>/dev/null || echo "$act_body")"
done

# —— 撤稿 ——
step "15/15 撤稿申请 + 运营批准"
wd_req_out="$(curl_json POST "${BASE_URL}/api/v1/app/publications/${PUB_ID}/withdraw-request" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d '{"reason":"demo-full-business-path"}')"
wd_req_code="$(echo "$wd_req_out" | tail -n1)"
wd_req_body="$(echo "$wd_req_out" | sed '$d')"
assert_http "$wd_req_code" "200" "withdraw-request" "$wd_req_body"
echo "$wd_req_body" | jq -c '{status,message}' 2>/dev/null || true

wd_appr_out="$(curl_json POST "${BASE_URL}/api/v1/admin/publications/${PUB_ID}/withdraw-approve" \
  -b "$COOKIE_JAR" -H "Content-Type: application/json" \
  -d '{"targetStatus":"withdrawn"}')"
wd_appr_code="$(echo "$wd_appr_out" | tail -n1)"
wd_appr_body="$(echo "$wd_appr_out" | sed '$d')"
# ADMIN_DEV_BYPASS=1 时可不带 Cookie
if [[ "$wd_appr_code" == "401" || "$wd_appr_code" == "403" ]] && [[ "${ADMIN_DEV_BYPASS:-}" == "1" ]]; then
  wd_appr_out="$(curl_json POST "${BASE_URL}/api/v1/admin/publications/${PUB_ID}/withdraw-approve" \
    -H "Content-Type: application/json" \
    -d '{"targetStatus":"withdrawn"}')"
  wd_appr_code="$(echo "$wd_appr_out" | tail -n1)"
  wd_appr_body="$(echo "$wd_appr_out" | sed '$d')"
fi
assert_http "$wd_appr_code" "200" "withdraw-approve" "$wd_appr_body"
echo "$wd_appr_body" | jq -c '{ok,status,note}' 2>/dev/null || true

echo ""
echo "✓ 全链路演示完成(stamp=${STAMP})"
echo "  公开页: ${BASE_URL}/r/${SHARE_TOKEN:-demo}"
echo "  品牌详情: ${BASE_URL}/app/brands/${BRAND_ID}"
echo "  报告筛选: ${BASE_URL}/app/reports?brandId=${BRAND_ID}"
if [[ -n "${SCAN_ID:-}" ]]; then
  echo "  监测详情: ${BASE_URL}/app/scans/${SCAN_ID}"
fi
if [[ -n "${REPORT_ID:-}" ]]; then
  echo "  内嵌报告: ${BASE_URL}/api/v1/app/reports/${REPORT_ID}/export?inline=1"
fi
echo "  M3/支付/PDF-OSS 运维(Settings): ${BASE_URL}/app/settings"
echo "  租户运维步骤说明(docs): ${BASE_URL}/docs/demo-script#tenant-ops-steps"
echo "  demo/tenant-ops-steps API: ${BASE_URL}/api/v1/app/demo/tenant-ops-steps?brandId=${BRAND_ID}"
echo "  m3/module-status API: ${BASE_URL}/api/v1/app/m3/module-status?brandId=${BRAND_ID}"
echo "  pay-checklist-status API: ${BASE_URL}/api/v1/app/billing/pay-checklist-status"
echo "  export-oss-context API: ${BASE_URL}/api/v1/app/reports/export-oss-context?brandId=${BRAND_ID}"
echo "  PDF/OSS Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  PDF/OSS Admin 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#pdf-oss-ops-steps"
echo "  demo/pdf-oss-ops-steps API: ${BASE_URL}/api/v1/app/demo/pdf-oss-ops-steps?brandId=${BRAND_ID}&pdfExportVerified=1"
echo "  admin/reports/export-oss-status API: ${BASE_URL}/api/v1/admin/reports/export-oss-status"
echo "  国内支付/订阅 Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  国内支付 Admin 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#cn-pay-ops-steps"
echo "  demo/cn-pay-ops-steps API: ${BASE_URL}/api/v1/app/demo/cn-pay-ops-steps?brandId=${BRAND_ID}"
echo "  admin/cn-pay-production-status API: ${BASE_URL}/api/v1/admin/billing/cn-pay-production-status"
echo "  billing/payment-notify-mobile-reverify-status API: ${BASE_URL}/api/v1/app/billing/payment-notify-mobile-reverify-status"
echo "  Admin 订阅列表: ${BASE_URL}/admin/billing/subscriptions"
echo "  定时 Cron 运维(Settings): ${BASE_URL}/app/settings"
echo "  Cron 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#cron-ops-steps"
echo "  demo/cron-ops-steps API: ${BASE_URL}/api/v1/app/demo/cron-ops-steps?brandId=${BRAND_ID}"
echo "  monitor/schedule-status API: ${BASE_URL}/api/v1/app/monitor/schedule-status?brandId=${BRAND_ID}"
echo "  cron-productization-context API: ${BASE_URL}/api/v1/app/monitor/cron-productization-context?brandId=${BRAND_ID}"
echo "  定时 Cron Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  Cron Admin 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#cron-admin-ops-steps"
echo "  demo/cron-admin-ops-steps API: ${BASE_URL}/api/v1/app/demo/cron-admin-ops-steps?brandId=${BRAND_ID}"
echo "  admin/monitor/cron-productization-status API: ${BASE_URL}/api/v1/admin/monitor/cron-productization-status"
echo "  M3 Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  M3 Admin 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#m3-admin-ops-steps"
echo "  demo/m3-admin-ops-steps API: ${BASE_URL}/api/v1/app/demo/m3-admin-ops-steps?brandId=${BRAND_ID}"
echo "  admin/m3/module-status API: ${BASE_URL}/api/v1/admin/m3/module-status"
echo "  extension Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  extension Admin 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#extension-admin-ops-steps"
echo "  demo/extension-admin-ops-steps API: ${BASE_URL}/api/v1/app/demo/extension-admin-ops-steps?brandId=${BRAND_ID}"
echo "  admin/extension-real-publish-status API: ${BASE_URL}/api/v1/admin/extension-real-publish-status"
echo "  platform publish Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  platform publish Admin 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#platform-publish-admin-ops-steps"
echo "  demo/platform-publish-admin-ops-steps API: ${BASE_URL}/api/v1/app/demo/platform-publish-admin-ops-steps?brandId=${BRAND_ID}"
echo "  admin/platform-publish-status API: ${BASE_URL}/api/v1/admin/platform-publish-status"
echo "  Admin 运维总 hub(Settings): ${BASE_URL}/app/settings"
echo "  Admin 运维总 hub 步骤说明(docs): ${BASE_URL}/docs/demo-script#admin-ops-hub-steps"
echo "  demo/admin-ops-hub-steps API: ${BASE_URL}/api/v1/app/demo/admin-ops-hub-steps?brandId=${BRAND_ID}"
echo "  admin/ops-hub-status API: ${BASE_URL}/api/v1/admin/ops-hub-status"
echo "  FC 部署 readiness(Settings · step 7k): ${BASE_URL}/app/settings"
echo "  FC 部署 readiness 步骤说明(docs): ${BASE_URL}/docs/demo-script#deploy-readiness-ops-steps"
echo "  demo/deploy-readiness-ops-steps API: ${BASE_URL}/api/v1/app/demo/deploy-readiness-ops-steps?brandId=${BRAND_ID}"
echo "  admin/deploy-readiness-status API: ${BASE_URL}/api/v1/admin/deploy-readiness-status"
echo "  deploy/app-url-status API: ${BASE_URL}/api/v1/app/deploy/app-url-status"
echo "  deploy/deploy-readiness-status API(五件套汇总 · step 7l+7o): ${BASE_URL}/api/v1/app/deploy/deploy-readiness-status"
echo "  deploy/health-api-public-status API(health API 公网验收 · step 7m): ${BASE_URL}/api/v1/app/deploy/health-api-public-status"
echo "  deploy/mobile-browser-https-status API(wm-geo.com 真机 HTTPS · step 7n): ${BASE_URL}/api/v1/app/deploy/mobile-browser-https-status"
echo "  公网/域名部署指引(Settings): ${BASE_URL}/app/settings"
echo "  上线配置清单(docs): ${BASE_URL}/docs/deploy-config"
echo "  阿里云 D1 清单(docs): ${BASE_URL}/docs/aliyun-d1-checklist"
echo "  Settings 全链路 demo-script 双 hub 总入口: ${BASE_URL}/app/settings"
echo "  双 hub 总入口步骤说明(docs): ${BASE_URL}/docs/demo-script#full-chain-hub-steps"
echo "  demo/full-chain-hub-steps API: ${BASE_URL}/api/v1/app/demo/full-chain-hub-steps?brandId=${BRAND_ID}"
echo "  FC env 写入后运维验收(Admin): ${BASE_URL}/admin"
echo "  admin/fc-env-post-write-acceptance-status API: ${BASE_URL}/api/v1/admin/fc-env-post-write-acceptance-status"
if [[ -n "${GEN_ID:-}" ]]; then
  echo "  生成详情: ${BASE_URL}/app/generate/${GEN_ID}"
fi
if [[ -n "${ORDER_ID:-}" ]]; then
  echo "  代发订单: ${BASE_URL}/app/sourcing/orders?brandId=${BRAND_ID}"
  echo "  代发订单详情: ${BASE_URL}/app/sourcing/orders/${ORDER_ID}"
fi
if [[ -n "${PUB_ID:-}" ]]; then
  echo "  发文详情: ${BASE_URL}/app/publications/${PUB_ID}"
  if [[ -n "${PUB_LIVE_URL:-}" ]]; then
    echo "  platform publish live_url: ${PUB_LIVE_URL}"
  fi
fi
echo "  扩展 OAuth(Settings): ${BASE_URL}/app/settings"
echo "  platform publish 演示(Settings): ${BASE_URL}/app/settings"
echo "  扩展真发 checklist(Settings): ${BASE_URL}/app/settings"
echo "  扩展真发步骤说明(docs): ${BASE_URL}/docs/demo-script#extension-real-publish-steps"
echo "  real-publish-checklist-summary: ${BASE_URL}/api/v1/app/extension/real-publish-checklist-summary"
echo "  扩展连接诊断(Settings): ${BASE_URL}/app/settings"
echo "  扩展连接步骤说明(docs): ${BASE_URL}/docs/demo-script#extension-connection-steps"
echo "  connection-diagnosis API: ${BASE_URL}/api/v1/app/extension/connection-diagnosis"
echo "  demo/extension-connection-steps API: ${BASE_URL}/api/v1/app/demo/extension-connection-steps"
echo "  M5 引用监测运维(Settings · step 11d+11e merge): ${BASE_URL}/app/settings"
echo "  M5 引用监测步骤说明(docs · 含 embedding): ${BASE_URL}/docs/demo-script#m5-citation-ops-steps"
echo "  demo/m5-citation-ops-steps API(含 embedding step): ${BASE_URL}/api/v1/app/demo/m5-citation-ops-steps?brandId=${BRAND_ID}"
echo "  admin/m5/citation-ops-hub-status API: ${BASE_URL}/api/v1/admin/m5/citation-ops-hub-status"
echo "  admin/m5/embedding-ops-status API: ${BASE_URL}/api/v1/admin/m5/embedding-ops-status"
echo "  M5 embedding Admin 运维(Settings): ${BASE_URL}/app/settings"
echo "  M5 embedding 运维步骤说明(docs): ${BASE_URL}/docs/demo-script#m5-embedding-ops-steps"
echo "  demo/m5-embedding-ops-steps API: ${BASE_URL}/api/v1/app/demo/m5-embedding-ops-steps?brandId=${BRAND_ID}"
echo "  m5/embedding-status API: ${BASE_URL}/api/v1/app/m5/embedding-status"
echo "  Admin M5 汇总运维入口: ${BASE_URL}/admin"
echo "  citation-monitoring-status API: ${BASE_URL}/api/v1/app/publications/citation-monitoring-status?brandId=${BRAND_ID}"
echo "  citation-scheduler Cron API: ${BASE_URL}/api/v1/cron/citation-scheduler"
echo "  发布扩展引导: ${BASE_URL}/app/publish/extension"
echo "  演示脚本说明: ${BASE_URL}/docs/demo-script"
rm -f "$COOKIE_JAR" "$VENDOR_JAR" 2>/dev/null || true

更完整的页面路径与 API 列表见 业务跑通手册