#!/usr/bin/env bash # ============================================================================= # 造梦/绘境AI · staging 五链路冒烟门(deploy smoke gate) # ----------------------------------------------------------------------------- # 用途:每次 staging 部署后跑一遍,**秒级**确认 5 条端到端闭环的关键 API 在岗、 # 鉴权/CORS/质量分回路结构正常。这是「部署后是否健康」的就绪检查(liveness/ # readiness),**不是**深度 e2e(深度 e2e 走 agent-loop orchestrator 批跑)。 # # 设计原则: # - 默认 **以读为主**(health/列表/feed/钱包/包体),可随时重复跑;唯一写=studio/draft 建一条 # 草稿(status 0,不发布、无下游副作用、可忽略),用于验证创作入口+鉴权+gameId 分配在岗; # - `--deep` 才走重写路径(generate 入队触发真实生成 + 遥测事件 + 互动),会落库,按需开启; # - 每条链路逐项 PASS/FAIL,末尾总判 + 退出码(0=全过,1=有 FAIL)——可挂 CI/部署脚本。 # # 用法: # bash deploy/smoke-test.sh # 默认 staging 只读冒烟 # BASE=http://100.64.0.7:48080 TOKEN=test1 bash deploy/smoke-test.sh # bash deploy/smoke-test.sh --deep # 含写路径(生成入队+遥测事件) # # 约定:内网地址/测试 token 可入仓(内网非密);**真实密钥严禁写入本文件**。 # ============================================================================= set -uo pipefail BASE="${BASE:-http://100.64.0.7:48080}" # staging 后端(mini-desktop) TOKEN="${TOKEN:-test1}" # staging 批跑/冒烟测试态 token(种子创作者) TENANT="${TENANT:-1}" ORIGIN="${ORIGIN:-http://100.64.0.7:4173}" # 前端 origin(验 CORS 同源放行) DEEP=0 [ "${1:-}" = "--deep" ] && DEEP=1 PASS=0; FAIL=0; FAILED_ITEMS=() # 颜色(非 TTY 自动降级为纯文本) if [ -t 1 ]; then G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; B='\033[0;34m'; N='\033[0m'; else G=''; R=''; Y=''; B=''; N=''; fi _log() { printf "%b\n" "$*"; } _head() { printf "\n%b== %s ==%b\n" "$B" "$1" "$N"; } # api METHOD PATH [DATA] —— 返回 "HTTP_CODE\nBODY";鉴权头正确引用(防 'Bearer test1' 空格被拆) api() { local method="$1" path="$2" data="${3:-}" local url="$path"; [[ "$path" == http* ]] || url="$BASE$path" if [ -n "$data" ]; then curl -s -w $'\n%{http_code}' -X "$method" "$url" \ -H "Authorization: Bearer $TOKEN" -H "tenant-id: $TENANT" \ -H "Origin: $ORIGIN" -H "Content-Type: application/json" -d "$data" 2>/dev/null else curl -s -w $'\n%{http_code}' -X "$method" "$url" \ -H "Authorization: Bearer $TOKEN" -H "tenant-id: $TENANT" \ -H "Origin: $ORIGIN" 2>/dev/null fi } # check NAME METHOD PATH MODE [EXTRA] [DATA] # MODE=health → HTTP 200 即过(actuator) # MODE=envelope → HTTP 200 ∧ huijing body code==0 即过 # MODE=envelope_nonempty → 再要求 data 非空(list 长度≥1 或 data 非 null) # EXTRA:envelope_nonempty 下可传 jq 风格 python 取数表达式(默认探 data.list 或 data) check() { local name="$1" method="$2" path="$3" mode="$4" data="${5:-}" local out code body out="$(api "$method" "$path" "$data")" code="$(printf '%s' "$out" | tail -n1)" body="$(printf '%s' "$out" | sed '$d')" local ok=0 detail="" case "$mode" in health) [ "$code" = "200" ] && ok=1; detail="http=$code" ;; envelope|envelope_nonempty) if [ "$code" = "200" ]; then # 解析 huijing 信封 {code,data};envelope_nonempty 再判 data 是否非空 local pyout pyout="$(printf '%s' "$body" | python3 -c " import sys,json try: d=json.load(sys.stdin) except Exception as e: print('ERR|parse'); sys.exit() c=d.get('code') data=d.get('data') if isinstance(data,dict): n=len(data.get('list')) if isinstance(data.get('list'),list) else (1 if data else 0) elif isinstance(data,list): n=len(data) elif data is None: n=0 else: n=1 print('%s|%s'%(c,n)) " 2>/dev/null)" local ecode="${pyout%%|*}" ecount="${pyout##*|}" if [ "$ecode" = "0" ]; then if [ "$mode" = "envelope_nonempty" ]; then [ "${ecount:-0}" -ge 1 ] 2>/dev/null && ok=1 || ok=0 detail="code=0 count=$ecount" else ok=1; detail="code=0"; fi else detail="http=$code code=$ecode"; fi else detail="http=$code"; fi ;; esac if [ "$ok" = "1" ]; then PASS=$((PASS+1)); _log " ${G}✓${N} $name ($detail)"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("$name [$detail]"); _log " ${R}✗${N} $name ($detail)"; fi } _log "${B}绘境AI staging 五链路冒烟门${N} BASE=$BASE TOKEN=${TOKEN:0:2}*** deep=$DEEP" # ---------------- 基础设施 + 鉴权 ---------------- _head "基础设施 / 鉴权" check "infra: actuator/health" GET /actuator/health health check "auth: passport/me(token 有效)" GET /app-api/passport/me envelope # ---------------- 链路① 创作 → 生成 → 预览试玩 ---------------- _head "链路① 创作→生成→预览" # 四模板就绪(执行器 banner 同源:getTemplateList 应返 clicker/merge/idle/tycoon) out="$(api GET /app-api/aigc/template/list)" tcount="$(printf '%s' "$out" | sed '$d' | python3 -c "import sys,json try: d=json.load(sys.stdin); data=d.get('data') or []; print(len(data) if isinstance(data,list) else 0) except: print(-1)" 2>/dev/null)" if [ "${tcount:-0}" -ge 4 ] 2>/dev/null; then PASS=$((PASS+1)); _log " ${G}✓${N} aigc/template/list(四模板就绪 count=${tcount})"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("template/list count=$tcount"); _log " ${R}✗${N} aigc/template/list(期望≥4,实=${tcount})"; fi check "studio/draft(创建草稿拿 gameId)" POST /app-api/studio/draft envelope_nonempty '{"title":"冒烟门-clicker","templateId":"clicker"}' # ---------------- 链路② 发布 → 审核 → 游戏流 ---------------- _head "链路② 发布→审核→游戏流" check "feed/stream(内容池非空)" GET "/app-api/feed/stream?size=5" envelope_nonempty check "feed/zones(分区结构)" GET /app-api/feed/zones envelope check "project/my(创作者项目区)" GET "/app-api/project/my?pageNo=1&pageSize=5" envelope # ---------------- 链路③ 游戏流 → 试玩 → 互动 → 分享 ---------------- _head "链路③ 试玩→互动→分享" # 真实取包走 GET /app-api/runtime/package/{versionId}(descriptor 含 entry/manifest/sandbox); # 注意:feed item 的 packageUrl 字段恒为 null,**不是**取包入口(runtime.ts:25 实证)。 VID="$(api GET "/app-api/feed/stream?size=1" | sed '$d' | python3 -c " import sys,json try: d=json.load(sys.stdin); lst=(d.get('data') or {}).get('list') or [] print(lst[0].get('versionId','') if lst else '') except: print('')" 2>/dev/null)" if [ -n "$VID" ] && [ "$VID" != "None" ]; then check "runtime/package/{versionId}(试玩取包 vid=${VID})" GET "/app-api/runtime/package/$VID" envelope else FAIL=$((FAIL+1)); FAILED_ITEMS+=("feed 首条无 versionId"); _log " ${R}✗${N} feed 首条无 versionId(无法验取包)"; fi # ---------------- 链路④ 广告 → 收益 → 钱包 ---------------- _head "链路④ 广告→收益→钱包" check "ad/slot/list-enabled(广告位)" GET /app-api/ad/slot/list-enabled envelope check "trade/account/mine(钱包)" GET /app-api/trade/account/mine envelope check "trade/income/page(收益流水)" GET "/app-api/trade/income/page?pageNo=1&pageSize=5" envelope # ---------------- 链路⑤ 数据回路(遥测→质量分→feed 重排)---------------- _head "链路⑤ 数据回路(质量分回灌)" # 结构断言:feed 首条携带 qualityScore(feed 排序由质量分驱动 = 回路已回灌) QSCORE="$(api GET "/app-api/feed/stream?size=1" | sed '$d' | python3 -c " import sys,json try: d=json.load(sys.stdin); lst=(d.get('data') or {}).get('list') or [] print(lst[0].get('qualityScore') if lst and lst[0].get('qualityScore') is not None else 'NONE') except: print('NONE')" 2>/dev/null)" if [ "$QSCORE" != "NONE" ] && [ -n "$QSCORE" ]; then PASS=$((PASS+1)); _log " ${G}✓${N} feed 携带 qualityScore=${QSCORE}(质量分回路已回灌)"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("feed 无 qualityScore"); _log " ${R}✗${N} feed 首条无 qualityScore"; fi # ---------------- --deep:写路径(生成入队 + 遥测事件)---------------- if [ "$DEEP" = "1" ]; then _head "深度(--deep)写路径:创作→生成入队" # 真实创作链写路径:create_draft → studio_generate(契约 backend_gw.py:123/130)。 # generate body = {sessionId: draft.id, prompt}(sessionId=draft 的 id 非 gameId)。 # draft 不设 templateId:便宜档真实流「一句话不选模板」→ 后端归一 generic → 分派闸(templateId=='generic')投外置 worker :9501; # 旧四模板名(idle/clicker 等)会撞硬相等分派闸、fall-through 到进程内 LLM 旧路、到不了 worker(见 staging-ops §8)。 # 入队触发 executor 派发 worker(异步产包经回调落版本),本冒烟**只验入队 200/code=0**,不等生成完成。 # 不验 telemetry/interact 写:① 遥测回路产出已由链路⑤(feed 携 qualityScore)结构性验证; # ② 完整深度 e2e(play→judge→publish)是 agent-loop orchestrator 批跑的职责,不与冒烟门重叠。 SID="$(api POST /app-api/studio/draft '{"title":"冒烟门-deep-generic"}' | sed '$d' | python3 -c "import sys,json try: print((json.load(sys.stdin).get('data') or {}).get('id','')) except: print('')" 2>/dev/null)" if [ -n "$SID" ] && [ "$SID" != "None" ]; then check "studio/generate(入队 sessionId=${SID})" POST /app-api/studio/generate envelope \ "{\"sessionId\":$SID,\"prompt\":\"一个挂机种树攒果实的放置小游戏\"}" else FAIL=$((FAIL+1)); FAILED_ITEMS+=("deep: draft 无 id"); _log " ${R}✗${N} deep: draft 未拿到 sessionId(id)"; fi fi # ---------------- 总判 ---------------- _head "总判" TOTAL=$((PASS+FAIL)) if [ "$FAIL" = "0" ]; then _log "${G}SMOKE_PASS${N} $PASS/$TOTAL 全过" echo "SMOKE_VERDICT=PASS" exit 0 else _log "${R}SMOKE_FAIL${N} $PASS/$TOTAL 过,$FAIL 项 FAIL:" for it in "${FAILED_ITEMS[@]}"; do _log " ${R}-${N} $it"; done echo "SMOKE_VERDICT=FAIL" exit 1 fi