#!/usr/bin/env python3 # -*- coding: utf-8 -*- """post_fallback_callback.py —— P3 W-G1 视觉终局·P2 兜底:直接 POST succeeded 回调(已验证样板 bundle)。 【缘由】主路两次一句话真生成(gen9306 whack / gen9307 tictactoe)均栽于 driver-coverage (design agent 给 driver:none、harness 盲打打不中目标→moves/score 恒 0→H_progress 挂,8/9 门过,非模型壁垒)。 兜底取【已验证过九门】样板 tictactoe(verdict.json A-I 全 pass)的 bundle.iife.js 作 engineBundle, 经派发面【真回调写链】(handleCallback·HMAC 验签·与真生成同代码路)落库入 feed。 【与 worker service.py 的 post_callback 逐字节对齐】payload 用 json.dumps(ensure_ascii=False).encode("utf-8") 一次算定既签既发;HMAC-SHA256(secret, data) hex 小写置头 X-Callback-Signature;Content-Type 带 charset=utf-8。 这是【真回调路】——后端 CallbackSignatureVerifier 验签 → handleCallback 建版本/组包(含 manifest+checksum)/落包。 【用法(mini-desktop,cwd=repo 根)】 CALLBACK_SECRET=<与后端 AIGC_CALLBACK_SECRET 同值> \\ python3 wg1/gen-worker/post_fallback_callback.py \\ --bundle game-runtime/games/_wg1-gen/tictactoe/bundle.iife.js \\ --trace-id wg1-3bB-gen9306-fb-001 \\ --title "井字棋(兜底)" \\ --callback http://localhost:48080/admin-api/aigc/dify/callback-internal """ import argparse import hashlib import hmac import json import os import sys import urllib.error import urllib.request def main(): ap = argparse.ArgumentParser() ap.add_argument("--bundle", required=True, help="已验证样板 bundle.iife.js 路径") ap.add_argument("--trace-id", required=True, help="须与 RUNNING 任务 trace_id 一致(handleCallback 据此定位)") ap.add_argument("--title", default="井字棋(兜底)", help="meta 标题") ap.add_argument("--template-id", default="generic") ap.add_argument("--callback", default="http://localhost:48080/admin-api/aigc/dify/callback-internal") args = ap.parse_args() secret = os.environ.get("CALLBACK_SECRET", "") if not secret: print("❌ CALLBACK_SECRET 未设(须与后端 AIGC_CALLBACK_SECRET 逐字节一致)", file=sys.stderr) sys.exit(2) # 读已验证样板 bundle 全文(engineBundle 真值) with open(args.bundle, "r", encoding="utf-8") as f: bundle_text = f.read() if "__GameBundle" not in bundle_text: print(f"❌ bundle 缺 __GameBundle 全局名:{args.bundle}", file=sys.stderr) sys.exit(3) print(f"[fallback] 读 bundle {len(bundle_text)} 字符(含 __GameBundle ✓)← {args.bundle}") # 组 DifyCallbackReqVO(succeeded 路,与 worker build_callback_payload 同形) payload = { "traceId": args.trace_id, "status": "succeeded", "templateId": args.template_id, "gameConfig": { "templateId": args.template_id, "title": args.title[:20], "theme": "generic", "engineDriven": True, }, "assets": [], "engineBundle": bundle_text, } # 逐字节对齐铁律:一次算定既签既发 data = json.dumps(payload, ensure_ascii=False).encode("utf-8") sig = hmac.new(secret.encode("utf-8"), data, hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json; charset=utf-8", "tenant-id": "0", "X-Callback-Signature": sig, } print(f"[fallback] HMAC 签名(前8)={sig[:8]}, payload 字节={len(data)}, POST → {args.callback}") req = urllib.request.Request(args.callback, data=data, method="POST", headers=headers) try: with urllib.request.urlopen(req, timeout=60) as resp: body = resp.read().decode("utf-8", "replace") print(f"[fallback] 回调完成 HTTP={resp.status} body={body[:300]}") except urllib.error.HTTPError as e: body = e.read().decode("utf-8", "replace") if e.fp else "" print(f"[fallback] 回调 HTTP={e.code} body={body[:300]}") # 401/4xx 视为失败 if e.code >= 400: sys.exit(4) except Exception as e: print(f"❌ 回调 POST 异常:{type(e).__name__}: {e}", file=sys.stderr) sys.exit(5) if __name__ == "__main__": main()