一键 curl 演示(15 步):健康检查 → 诊断 → 租户登录 → 品牌/关键词/扫描/报告 → 发文 → step 11 知乎 OAuth + platform-publish-via-api(须 WM_GEO_CHANNEL_OAUTH_DEV_BYPASS=1、 WM_GEO_PLATFORM_PUBLISH_API_E2E=1,run-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。
全链路脚本在 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 引用监测 · 汇总运维入口」双卡。
全链路脚本在 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 运维步骤」卡。
全链路脚本在 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 缺口路线图。
全链路脚本在 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 清单。
全链路脚本在 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 缺口路线图。
全链路脚本在 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 缺口路线图。
全链路脚本在 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 缺口路线图。
全链路脚本在 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 清单。
全链路脚本在 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 运维见 运营后台 「国内支付运维」卡与 订阅列表。
全链路脚本在 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 产品化运维」卡。
全链路脚本在 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 运维说明。
全链路脚本在 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。
全链路脚本在 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 缺口路线图。
安装扩展前可先调用 GET /api/v1/app/extension/connection-diagnosis 核对适配器、握手、OAuth 与 URL 回填骨架。登录态实时状态见 发布扩展引导 「扩展连接诊断」卡,或 账户设置 同名诊断卡。
全链路脚本在 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 步骤」卡。
各平台编辑页须在本机 Chrome 加载 extension/ 后按下方验收步骤真调 fillDraft → submitDraft(须 AUTO_SUBMIT)→ capturePublishedUrl。 每平台须同时满足 stable-dom-selectors 与 stable-url-capture 注册表对齐(acceptanceDepthReady)。 登录态实时状态见 GET /api/v1/app/extension/dom-tuning-acceptance 与 发布扩展引导 「四平台 DOM 真调验收卡」。
四平台 adapter 的 title/body/publishButton/publishedUrlPattern 选择器分为 primary(平台特化)与 fallback(启发式)。 只读摘要见 GET /api/v1/app/extension/stable-dom-selectors 与 发布扩展引导 「稳定 DOM 选择器注册表」卡。
[data-testid="note-title"] — 笔记标题 data-testid[data-testid="note-content"] — 笔记正文 data-testidbutton[data-testid*="publish"] — 发布按钮 data-testid^https://www\.xiaohongshu\.com/ — 笔记 URL 模式input[class*="title"] — 标题 input classtextarea[class*="desc"] — 描述 textarea classbutton[class*="publish"] — 发布按钮 class^https://www\.douyin\.com/ — 作品 URL 模式.WriteIndex-titleInput input — 专栏标题 input.WriteIndex-titleInput textarea — 专栏标题 textarea.public-DraftEditor-content — Draft.js 正文.DraftEditor-root [contenteditable="true"] — DraftEditor contenteditable.PublishPanel-stepOneBtn — 发布面板按钮^https://zhuanlan\.zhihu\.com/p/ — 专栏文章 URL#title — 标题 #title#js_editor — 正文 #js_editor.ProseMirror — ProseMirror 编辑器#js_send — 群发按钮 #js_send^https://mp\.weixin\.qq\.com/ — mp 域名 URL四平台 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
小红书
抖音
知乎
微信公众号
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 列表见 业务跑通手册。