games-development-ai/docs/agent-specs/_archive/2026-06-15-python-to-SAA-migration-design.md
zizi 7f24a344d0 docs(agent-specs): B2 归档——64 个闭线工作记录移入 _archive/,热目录顶层 90→26
承接目录治理:上轮只压缩内容未减文件数,闭线工作记录仍平铺致目录看着仍一堆(创始人指出)。本次执行 B2 归档。

64 个 ≤06-15 闭线档(25 压缩桩 + 闭线 review/report/纪要/edit-plan)git mv 入 docs/agent-specs/_archive/(文件名不变、仍 git 跟踪可查)。热目录 ≤06-15 仅留 13 活档(决策/纲领/SoT/活spike)+13 个 06-16 在飞。

活资产 20 处旧路径引用(.agents/docs/memory/_index)同步改 _archive/,引用断裂复测=0;_index 活地图 + 治理档状态收口。约束:0 个 06-16 被移、orchestrator 等未跟踪在飞档零误纳。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:26:19 +00:00

33 KiB
Raw Blame History

Python agentic 生成系统 → SAA(Spring AI Alibaba / Java)迁移设计

类型:迁移设计(只读分析 + 目标图设计,不含完整实现代码)。 范围:把「已跑通的 Python agentic 生成系统」(wg1/gen-worker)迁移并充分发挥 SAA 强项地适配到 SAA 裸图编排,随后做「Python 生成游戏 vs SAA 生成游戏」对比测试。 基线证据:Python 侧 wg1/gen-worker/worker/**(逐行已读);Java 契约 game-cloud/game-module-aigc/**(dispatcher 接缝已读);SAA 源码 /root/oss/spring-ai-alibaba @ v1.1.2.2(核心类文件位已核);SAA 用法/坑见 2026-06-15-SAA-能力API接入-dossier.md。


0. 结论先行(决策级)

  1. 迁移本质 = 换「worker 躯体」,不换「契约骨架」。 Java 执行器侧已有干净接缝:AigcGenerateExecutor.dispatchGeneric 组 §6.1 job-in → WorkerDispatchClient.dispatch 投递 → worker 异步产 engineBundle → 回调 DifyCallbackReqVO 到 /dify/callback-internal(AigcGenerateExecutor.java:435-494、WorkerDispatchClient.java:69-107、DifyCallbackReqVO.java:25-67)。SAA 图只替换 job→engineBundle 这段躯体,两端契约逐字段不动。
  2. 躯体对应 = Python 的 L2 studio 闭环 → SAA「裸 StateGraph + 自定义 NodeAction」。 Python 已自证:design→[code|fix 循环]→九门真玩→player 顾问轮(agent_loop/studio.py:197-314)。这是确定性多步编排,不是「LLM+工具自治」——故用裸图 + 条件边回环 + state 计数器,不用 ReactAgent/LoopAgent(dossier §0-1)。
  3. build + 九门 harness 子进程整体复用,禁重写。 serve-and-play.sh→static-serve+headless Chrome→play.cdp.cjs(九门 A–I)+build.mjs(esbuild)是模型无关、已沉淀的确定性地板(game-runtime/games/_wg1-gen/_shared/play.cdp.cjs:10-26)。SAA 在 build/play 节点内 ProcessBuilder 调同一套 shell(dossier §4-3),不进 SAA 图、不用 sandbox 模块。
  4. SAA 比 Python 手写编排净增 5 项强项:MySQL checkpoint 可恢复/续跑、Hook+state 双重限次、节点级流式进度(SSE)、子图封装(player 顾问轮)、图可视化与可观测埋点。逐条映射见 §5。
  5. 部署两形态二选一:① 进程内 SaaGraphDispatcher(消灭 HTTP+HMAC 回调跳,dossier §0-5 首选);② Java worker 进程顶替 Python service.py,沿用 /generate HTTP 端点(drop-in,零改 Java 执行器)。建议对比测试期走形态②(隔离变量、与 Python 并排),生产收口走形态①。

1. 现有 Python agentic 系统精确画像(带 文件:行)

1.1 两层闭环:L1 裸路 vs L2 agentic studio

维度 L1 裸路(worker/run.py) L2 agentic studio(worker/agent_loop/studio.py)
角色 单模型直出(无 design/无 player) design → code/fix 多角色 + player 顾问
框架 裸 openai 客户端,零 AgentScope(_client.py:1-5 纪律) AgentScope 2.0.1 进程内多 agent(studio.py:31-32)
管线 generate→validate→scaffold→build→play→retry(run.py:97-136) design→[code|fix 环]→九门→player 顾问轮(studio.py:213-280)
重试上限 max_retries(默认 2,run.py:88) max_repairs(默认 5)+ player_rounds(默认 1,studio.py:197)
产出落盘 results/<id>.json(run.py:154) results/<id>.<stage>.json(studio.py:312)

现行生产入口 = L2:service.py:173 在 HTTP job 处理里调 run_studio(...)(L1 是早期裸路/冒烟,studio 复用其 scaffold/build/play,studio.py:8)。迁移目标锚定 L2。

1.2 L2 管线步骤(逐节点,studio.py)

  1. design(设计 agent)studio.py:214 → roles.DESIGN_SYSTEM(roles.py:8-32)。一句话 brief → 设计稿 + 末尾自产 ```gatespec JSON 块(gate-H 推广:让确定性门适用任意游戏)。
  2. gatespec 抽取与归一studio.py:218 → _extract_gatespec(studio.py:72-102):正则抠 ```gatespec 块,json_repair 兜脏 JSON,expectLatch 缺省补 True,paddle-intercept.ballPath 的 .y→.x 安全归一。抽出的 exportState/driver/controlCheck/assertAfterPlay/expectLatch 合入 play_spec(studio.py:219-223)。
  3. code / fix 循环studio.py:231-265:首轮 role=code,后续 role=fix;prompt.build_messages(enriched, retry_feedback=feedback)(prompt.py:110-119)把上轮失败原因回喂。每轮:
    • validate.extract_code(validate.py:13-28)→ 抠 ```js 块;
    • validate.validate(validate.py:103-111)= 静态契约扫描(static_check,validate.py:55-77:默认导出/init·update·render·destroy 四方法/ctx 用法/getEngine 必现/12 条禁用词)+ node --check ESM 语法门(validate.py:80-100);
    • run.scaffold(run.py:33-42)落 generated-factory.js + 拷 entry-bundle/index 模板 + 写 play-spec.json;
    • run.build(run.py:45-53)ProcessBuilder 调 node scripts/build.mjs ... --global-name=__GameBundle;
    • run.play(run.py:56-72)ProcessBuilder 调 serve-and-play.sh,读回 evidence/verdict.json。
  4. 九门判定(确定性硬地板)studio.py:259 → seven_ok = rc==0 and verdict.pass。未过 → run._verdict_feedback(verdict)(run.py:75-85)摘失败守卫回喂 → 继续 fix 环。
  5. player 顾问轮(七门过后才跑,软门)studio.py:267-280 → run_player_panel(studio.py:171-193):M3 视觉位(_judge_vision 读 first-paint/after-play 截图,studio.py:148-162)+ flash 文本位(_judge_text,studio.py:165-168)多人格评判 → 若 verdict=fix 且 player_round_used<player_rounds,把体验问题回喂再修一轮(七门仍是硬地板)。
  6. 成本结算studio.py:282-295 → cost.py 按 new-api /api/pricing+/api/status quota 公式逐模型拆账(cost.py:54-66)。

1.3 模型路由

  • 配置即旋钮:models.yaml 三阶段(stage1 便宜 DeepSeek-flash / stage1_mmx 跨家 MiniMax-M2.7 / stage2 强档 DeepSeek-v4-pro),每阶段 5 角色(design/code/fix/player_text/player_vision)+ _default;视觉位恒 MiniMax-M3(models.yaml:1-40)。
  • 取值:config.model_name_for(role, stage)(config.py:51-56)。
  • 客户端:config.build_model(config.py:59-68)建 AgentScope OpenAIChatModel,base_url 落 OpenAICredential(指 new-api);子类 RecordingChatModel(config.py:26-42)记每次调用 usage 供成本/对比。
  • 唯一出口 = new-api OpenAI 兼容网关_client.py:31 BASE_URL=http://100.64.0.8:3000/v1;代理旁路铁律:import openai 前把网关 host 并入 NO_PROXY(_client.py:35-44),否则本机 clash 代理转走→502(spike 头号坑)。
  • AGENT_MODEL_NAME 不是选模型机制(dossier §4-7);选模型靠每节点指定 model 名。

1.4 重试/修复逻辑(迁移的核心循环语义)

  • 失败即回喂重出全量(非 diff):prompt.py:114-118 明示「不要只给 diff」。
  • 失败分桶 → 不同回喂文案:extract 失败 / validate 失败(静态错误列表)/ build 失败(esbuild 日志前 600 字)/ 九门失败(_verdict_feedback 摘未过守卫)/ player fix(体验问题列表)——studio.py:243-279。
  • 计数器双层:repair 环 max_repairs + player 顾问 player_rounds,均 state 内自增(studio.py:231、275-276)。
  • 网关瞬时错误重试:_client.chat 内 tries=3(_client.py:60,94-97)。

1.5 harness(build + 九门)如何被调

  • 入口:run.play(game_id, port, cdp_port)(run.py:56-72)subprocess.run(["bash", "serve-and-play.sh", game_id, port, cdp_port], timeout=180)。
  • serve-and-play.sh(game-runtime/games/_wg1-gen/_shared/serve-and-play.sh:1-68):净场杀端口 → 起 static-serve.cjs(serve game-runtime 根)→ 起 headless Chrome(--remote-debugging-port + --remote-allow-origins=* + --autoplay-policy)→ 就绪轮询 → node play.cdp.cjs <gameId> --base --cdp → trap 清理,返 play 退出码。
  • build:run.build(run.py:45-53)node scripts/build.mjs <entry> <out> --global-name=__GameBundle;build.mjs:23-31 锁参 esbuild(bundle+minify+iife+es2019+无 sourcemap),输出 bundle.iife.js + .meta.json。
  • 九门 A–I(play.cdp.cjs:10-20,全过才 PASS,判定汇总 play.cdp.cjs:543-667):
    • A 装载 __genBooted ∧ __gameHostEngineInitFired ∧ 无 bootError
    • B 未捕获 __genInternals.uncaught 空
    • C 掌帧 500ms 内 frame 增量 ∈ [20,45]
    • D 真渲染 #game-engine 有色像素 bright>50 ∧ maxCh>80
    • E 活性 真玩前后帧哈希集 distinct≥2
    • F 真接线 __engineCalls 命中 spec.expectedEngineCallPrefixes 任一前缀(证经 getEngine() 真调引擎,非自绘)
    • G 输入有效 同 seed 起无输入对照实例推进到同帧号,整帧哈希不同
    • H 机制进展 + latch spec.assertAfterPlay[] 全过 + expectLatch 时真玩到 phase='gameover' 且驻留600ms;依赖游戏 _forensicsView() 导出 state(未声明=SKIP 向后兼容)
    • I 控制跟手 spec.controlCheck 声明时连点验控制体平滑逼近(未声明=SKIP)
  • 适配性真玩驱动:spec.driver 存在=读 state 自动出招(runDriver,play.cdp.cjs:204-212 分发 8 类:paddle-intercept/tap-targets/flap-to-gap/seek-x/tap-pairs/key-cycle/drag-aiming/aim-fire),解「盲打固定坐标打不动技巧游戏」假阴性;否则走 spec.inputs 固定序列。

1.6 job / callback 契约(迁移的 I/O 边界)

  • job-in(§6.1):Java dispatchGeneric 组(AigcGenerateExecutor.java:464-481):job_id, tier="L1", kind="generate", brief, model, budget{maxYuan,maxLlmCalls}, gameId, templateId, renderCtx{template_schema,banned_list,findings}, idempotency_key, deadline_ms, callback{type,target}, traceId。Python service.do_POST 收(service.py:239-262)。
  • 握手:worker 立即回 202(service.py:262),后台线程异步跑(service.py:261);忙时 409(串行锁,service.py:254-257,serve 4320/CDP 9222 不可并发)。
  • callback-out:build_callback_payload(service.py:91-123)组 DifyCallbackReqVO 形态:traceId, status(succeeded|failed), templateId, gameConfig{templateId,title,theme,engineDriven}, assets:[], engineBundle(成功路=bundle.iife.js 全文) | failureReason(失败路);HMAC-SHA256 签名(service.py:135-143,逐字节对账 Java CallbackSignatureVerifier)POST 回 /dify/callback-internal。Java 侧 DifyCallbackReqVO.engineBundle(DifyCallbackReqVO.java:53-66)= P3 additive 字段,resolveEngineBundleText 取值写 GamePackage.engineBundle 落包入 feed。
  • 出厂自检:bundle 必含 __GameBundle 全局名,否则判失败不落坏包(service.py:184-195)。

1.7 prompt 与证据口径

  • prompt:prompt.SYSTEM(prompt.py:24-107)= 模块形状(默认导出 createGame,五方法 + _forensicsView)+ 能力面(ctx.getEngine/getInput/time/random、boot.mainContext)+ 12 条硬禁止(禁 import/Math.random/Date.now/addEventListener/rAF...)+ 硬规则(闭包态/受控时钟/getEngine 容错/destroy 幂等/必调一次引擎能力/每帧可见变化)+ P0 latch 终态硬约束(gameover 驻留不自动重开)+ few-shot 参照(读 _goldenpath-probe/factory.js 防漂移,prompt.py:9-20)。
  • 证据四件套:verdict.json(九门 guards + pass)+ first-paint.png + after-play.png(play.cdp.cjs:26),落 games/_wg1-gen/<id>/evidence/;结果 JSON 含 attempts[](每轮 usage/stage_fail/guards)、seven_gate_verdict、player.panel、tokens_by_model、cost(studio.py:297-311)。

2. Python → SAA 映射表

# Python 构件(文件:行) SAA 构件 性质
1 L2 主编排 run_studio 循环 studio.py:197-314 StateGraph(name, KeyStrategyFactory) + CompiledGraph 需适配(命令式循环→声明式图)
2 design agent 调用 studio.py:214 render/design 节点 = node_async(NodeAction) 调 design model 直接对应
3 _extract_gatespec studio.py:72-102 design 节点内纯 Java 逻辑(正则 + Jackson 容错解析;json_repair→自写宽松解析或 JsonNode 容错) 需适配(无 json_repair 等价;做最小宽松解析)
4 code/fix 循环体 studio.py:231-265 generate 节点(首轮 code)+ repair 节点(fix)+ addConditionalEdges 回边 需适配(用 state role/repair_count 区分)
5 validate.validate 静态+node --check validate.py:103-111 validate 节点(静态契约 = 纯 Java 正则;node --check = ProcessBuilder) 直接对应(子进程复用)
6 run.scaffold run.py:33-42 scaffold 节点 = 纯 Java 文件 IO(写 factory + 拷模板 + 写 play-spec) 直接对应
7 run.build(esbuild 子进程)run.py:45-53 build 节点 = NodeAction 内 ProcessBuilder("node","scripts/build.mjs",...) 复用·禁重写(骨架抄 LocalFilesystemBackend.java:457-507,dossier §4-3)
8 run.play(九门 CDP 子进程)run.py:56-72 play 节点 = ProcessBuilder("bash","serve-and-play.sh",...) + 读 verdict.json 复用·禁重写
9 九门 verdict.pass 判定 play.cdp.cjs:666 条件边 addConditionalEdges("play", s→pass?"emit":cnt>=N?"giveup":"repair") SAA 更强(确定性门作条件边,图天然支持)
10 repair 计数 max_repairs studio.py:231 state repair_count 计数器 + recursionLimit 硬刹车(dossier §4-2) SAA 更强(Hook 可选叠加限次)
11 player 顾问轮 run_player_panel studio.py:171-193 子图 player-panel(vision+text 两节点)或单 player 节点 + 回边 SAA 更强(子图封装/可并行 vision|text)
12 失败回喂 prompt.build_messages(retry_feedback) prompt.py:110 repair 节点读 state feedback 注入 user prompt(state 即传参) 直接对应
13 模型路由 config.model_name_for + models.yaml config.py:51-56 N 个 OpenAiChatModel bean(共用 OpenAiApi,仅 defaultOptions.model 异),节点 @Qualifier 取(dossier §1-24) 需适配(YAML 阶段→Spring 配置/Map<role,model>)
14 _client.chat(裸 openai,base_url=new-api)_client.py:60-98 上游 OpenAiApi.builder().baseUrl(NEW_API) + OpenAiChatModel(dossier §1-24) 直接对应(new-api OpenAI 兼容同构)
15 RecordingChatModel usage 记录 config.py:26-42 ChatModel 调用后读 ChatResponse.metadata().usage(),写入 state token 累计 需适配
16 cost.py quota 折算 cost.py:54-66 复用 Python cost.py 子进程或 Java 重写 HttpClient 调 /api/pricing+/api/status 需适配(测而不闸,建议先复用脚本)
17 网关代理旁路 _client.py:35-44 不适用(JVM 不经 clash;直连 new-api,无此坑) 直接对应(坑消失)
18 service.py HTTP 壳 + HMAC 回调 service.py:155-262 形态①去掉(进程内 SaaGraphDispatcher 直调 handleCallback);形态②Spring @RestController /generate + 后台线程池跑图 需适配(见 §3.4)
19 串行锁 service.py:80 形态②Semaphore(1) 守 serve 4320/CDP 9222(图执行在后台 ExecutorService,dossier §3-45) 直接对应
20 结果落盘 results/*.json studio.py:312 节点写 state;emit 节点从 state 组 DifyCallbackReqVO(+ 诊断落盘可选) 直接对应
— 新增(Python 无) MysqlSaver checkpoint(同 threadId 续跑/time-travel,dossier §4-5) SAA 净增
— 新增(Python 无) 节点级 streaming 进度(stream()→Flux<NodeOutput>,SSE 推前端,dossier §4-4) SAA 净增

3. SAA 目标图设计

3.1 节点 + 边组织

flowchart TD
  START([START]) --> render[render<br/>取 job→state:brief/model/gameId/budget]
  render --> design[design 节点<br/>设计稿+自产 gatespec→merge play_spec]
  design --> generate[generate 节点<br/>code 模型出工厂源码]
  generate --> validate[validate 节点<br/>静态契约+node --check 子进程]
  validate -->|ok| scaffold[scaffold 节点<br/>落 factory+模板+play-spec]
  validate -->|fail & cnt<N| repair[repair 节点<br/>fix 模型回喂失败原因]
  scaffold --> build[build 节点<br/>ProcessBuilder esbuild]
  build -->|ok| play[play 节点<br/>ProcessBuilder serve-and-play 九门]
  build -->|fail & cnt<N| repair
  play -->|九门 pass| player{{player-panel 子图<br/>vision+text 顾问·软门}}
  play -->|fail & cnt<N| repair
  play -->|fail & cnt>=N| giveup[giveup 节点<br/>status=failed]
  repair --> generate
  player -->|无体验问题 或 player轮用尽| emit[emit 节点<br/>读 bundle.iife.js→组 DifyCallbackReqVO]
  player -->|有问题 & playerRound<M| repair
  giveup --> END([END])
  emit --> END

关键边语义(条件边靠 EdgeAction→String + mappings Map,dossier §1-21):

  • validate/build/play 三处失败回边共用同一判据:ok?下一步 : repair_count>=maxRepairs?"giveup" : "repair";失败原因写 state feedback,repair 节点读取注入 prompt。
  • play 成功边 → player;player 输出 verdict=fix 且 player_round<maxPlayerRounds → 回 repair(九门仍是硬地板,player 是软门,与 studio.py:267-280 同语义)。
  • repair 节点统一自增 repair_count 后回 generate(fix 与 code 同节点、靠 state role 区分模型;或拆两节点,建议同节点 + role 标志省图复杂度)。

3.2 用到的 SAA 强项(逐项对应本管线)

SAA 强项 本管线落点 证据
图编排(条件/回环/子图) 九门作条件边、repair 回环、player 子图 dossier §0-1、§4-2
MySQL checkpoint 恢复 单 job 多步昂贵(design+多轮 LLM+九门 ~数十秒);崩溃后同 threadId 续跑 dossier §4-5(MysqlSaver 唯一权威 saver,releaseThread(false))
Hook 限次 + state 计数器双保险 repair_count 主限次 + recursionLimit 硬刹车(优雅终止非异常) dossier §4-2、§5-3
streaming 进度 节点 apply 返回塞 Flux → stream() 展开 → SSE 推前端「设计中/写码中/真玩中」 dossier §4-4(无 streamEvents,用 stream()+StreamingOutput)
确定性九门作条件边 verdict.json pass→边路由,零猜测 play.cdp.cjs:666
子图封装 player-panel(vision|text 可并行)封为子图,主图一个节点引用 dossier §2(嵌套子图本期可用)
模型节点指 new-api N 个 OpenAiChatModel 共用 OpenAiApi(baseUrl=new-api) dossier §1-24
可观测埋点 节点级埋点/图可视化,替代 Python 散落 print dossier §2

3.3 关键节点骨架草图(伪代码级,非实现)

state schema(KeyStrategyFactory,dossier §1-22 用 ReplaceStrategy/AppendStrategy)

keys: brief, model(role→modelName map), gameId, templateId, budget,
      designText, playSpec(JSON), factorySrc, verdict(JSON),
      feedback, role(code|fix), repairCount, playerRound,
      tokensIn/Out, status(succeeded|failed), failureReason, engineBundle

generate / repair 节点(NodeAction,dossier §1-22 写靠 return Map)

// 伪代码:读 state 拼 prompt(首轮 code/重出 fix 回喂 feedback),调对应模型,抽 ```js
Map apply(OverAllState s){
  String role = s.value("role","code");                 // code|fix
  ChatModel m = role.equals("fix") ? fixModel : codeModel; // @Qualifier 取,全指 new-api
  String user = buildUser(s.value("brief"), s.value("designText"), s.value("feedback")); // 回喂全量重出
  String text = m.call(systemPrompt, user);             // systemPrompt = prompt.SYSTEM 等价
  String src = extractJsBlock(text);                    // 等价 validate.extract_code
  recordUsage(m, s);                                    // 累计 token 入 state
  return Map.of("factorySrc", src);
}

validate 节点(静态 = 纯 Java,node --check = 子进程)

Map apply(OverAllState s){
  List<String> errs = staticCheck(s.value("factorySrc")); // 默认导出/四方法/getEngine/12 禁用词
  if(errs.isEmpty()) errs = nodeCheck(src);               // ProcessBuilder("node","--check",tmp.mjs)
  return errs.isEmpty()? Map.of("validateOk",true)
                       : Map.of("validateOk",false,"feedback","校验未过:\n"+join(errs));
}

build / play 节点(ProcessBuilder,禁重写 harness;骨架抄 LocalFilesystemBackend.java:457-507)

Map apply(OverAllState s){                  // play 节点
  Process p = new ProcessBuilder("bash",
      "games/_wg1-gen/_shared/serve-and-play.sh", gameDir, "4320","9222")
      .directory(gameRuntimeRoot).start();
  boolean done = p.waitFor(180, SECONDS);   // 超时 destroyForcibly(对齐 timeout=180)
  if(!done){ p.destroyForcibly(); return Map.of("playPass",false,"feedback","真玩超时"); }
  JsonNode verdict = mapper.readTree(Path.of(gameDir,"evidence","verdict.json").toFile());
  boolean pass = p.exitValue()==0 && verdict.path("pass").asBoolean(false);
  return Map.of("verdict",verdict,"playPass",pass,
                "feedback", pass? "" : summarizeFailedGuards(verdict)); // 等价 _verdict_feedback
}

条件边(EdgeAction,dossier §1-21 mappings 不可空)

graph.addConditionalEdges("play",
  edge_async(s -> {
     if (Boolean.TRUE.equals(s.value("playPass"))) return "ok";
     return s.value("repairCount",0) >= MAX_REPAIRS ? "giveup" : "repair";
  }),
  Map.of("ok","player", "repair","repair", "giveup","giveup"));

emit 节点(组回调,复用现有写入路径)

Map apply(OverAllState s){
  String bundle = readFile(gameDir+"/bundle.iife.js");      // 读 __GameBundle iife 全文
  if(!bundle.contains("__GameBundle")) return Map.of("status","failed","failureReason","bundle_missing_global_name");
  return Map.of("status","succeeded","engineBundle",bundle); // → DifyCallbackReqVO.engineBundle
}

3.4 与 Java 契约的对接(两形态)

  • 形态①进程内 SaaGraphDispatcher(生产收口首选,dossier §0-5/§3-45):实现 GenerationDispatcher(与 WorkerDispatchClient 同接口位),dispatch(job) 把 job 转 state,在后台 ExecutorService compiledGraph.invoke(state, RunnableConfig.threadId(traceId))(自带阻塞,禁 Tomcat/Reactor 线程),出口直接进程内调 difyCallbackService.handleCallback(reqVO)——消灭 HTTP 跳 + HMAC(同信任域)。图执行不包大 @Transactional,跑完单独短事务落库。
  • 形态②Java worker 进程(对比测试期 drop-in):Spring @RestController 暴露 POST /generate(顶替 Python service.py),收 §6.1 job→202 握手→后台线程跑图→组 DifyCallbackReqVO+HMAC POST 回 /dify/callback-internal。Java 执行器零改(aigc.executor.worker-url 指向新 Java worker 即可),与 Python worker 并排,隔离变量做对比。

4. 复用 vs 重写边界(硬约束)

组件 决策 理由
serve-and-play.sh / play.cdp.cjs(九门 A–I)/ static-serve.cjs 子进程复用·禁重写 模型无关的确定性地板;重写 = 引入新假阴性/假绿风险,且 Java 无 CDP+esbuild 等价生态现货
scripts/build.mjs(esbuild 锁参) 子进程复用·禁重写 锁参影响 SIZES 口径;Java 无 esbuild 等价
entry-bundle/index 模板、_goldenpath-probe/factory.js few-shot 复用(读文件) 装载契约/few-shot 防漂移
cost.py 成本折算 建议子进程复用(或后续 Java 重写) 测而不闸;Python 脚本已沉淀,先复用省事
run.scaffold 文件 IO / validate.static_check 静态扫描 / extract_code 重写进 SAA 节点(纯 Java) 轻逻辑,无生态依赖,进图后受 checkpoint/可观测管辖
run_studio 命令式循环 / code-fix-player 编排 重写为 SAA 裸图 迁移主体;命令式→声明式图换取可恢复/限次/流式
_client.chat / config.build_model 模型调用 重写为 SAA OpenAiChatModel 上游 OpenAI 兼容指 new-api,同构
service.py HTTP 壳 + HMAC 形态①去掉 / 形态②Java 重写 见 §3.4
_extract_gatespec 的 json_repair 兜脏 重写为最小宽松解析 Java 无 json_repair;做「strip ```gatespec→Jackson 容错→缺省补 expectLatch/ballPath 归一」最小等价

5. 「充分发挥 SAA 强项」清单(相比 Python 手写编排)

  1. 可恢复(checkpoint/resume):Python run_studio 一旦进程崩,整 job 从零重跑(results/*.json 仅事后产物)。SAA MysqlSaver + threadId(traceId) → 崩溃后同 threadId 二次 invoke 即从最后节点续跑(dossier §4-5);与现有 watchdog 收尸(AigcGenerateExecutor.java:620-644)互补:watchdog 兜超时、checkpoint 兜续跑。
  2. 限次双保险:Python 靠裸 for attempt in range(max_repairs+1) 计数(studio.py:231)。SAA = state repair_count(业务语义主限次)+ recursionLimit(引擎级硬刹车,优雅终止非异常,dossier §5-3);ReAct 那种无限循环坑(dossier §5-3)在裸图下天然不触发。
  3. 节点级流式进度:Python 仅 stdout print(run.py:176、studio.py:338),前端无实时进度。SAA stream()→Flux<NodeOutput> + StreamingOutput(dossier §4-4,注意无 streamEvents)→ 出口 @PostMapping(produces=TEXT_EVENT_STREAM) 把「设计中→写码中→打包中→真玩中→第 k 次修复」推前端,直接对接 studio 创作页进度条。
  4. 子图封装:Python run_player_panel 是内联函数(studio.py:171)。SAA 把 player-panel(vision|text)封为子图(dossier §2 嵌套子图本期可用),主图一节点引用;vision/text 两位可并行(Python 当前串行 studio.py:180-192)→ 顾问轮提速。
  5. 可观测 + 可视化:Python 编排是黑盒命令式流。SAA 节点级埋点 + 图结构可导出(dossier §2 可观测埋点),排障/审计/给非工程同事讲清「九门卡在哪个节点」远优于读 print 日志。
  6. 确定性门即一等公民边:九门 verdict.pass 在 Python 是 if 分支(studio.py:262);在 SAA 是 addConditionalEdges 的路由谓词——把「能玩才算 pass」这条铁律固化进图拓扑,杜绝顺手改 if 绕过门。

6. 风险与待定 + 迁移分步

6.1 风险与待定

级别 项 说明 / 缓解
高 AgentScope 多角色语义损失 Python design/code/fix/player 是 AgentScope Agent(studio.py:127-134,max_iters=1 单轮)。SAA 裸图下每角色=一个节点调一次模型,单轮语义天然等价(无工具自治),低风险;但 player 的「人格 + JSON 评判」需在节点内复刻 roles.player_system(roles.py:38-57),按字段对齐。
高 build/play 子进程环境耦合 mini-desktop serve-and-play.sh 依赖 Chrome+node+esbuild+lsof(serve-and-play.sh:11-44),只在 mini-desktop(x86 e2e 机)跑。SAA worker(无论形态①②)必须与 harness 同机部署(all localhost),cwd=game-runtime 根。形态①把图嵌进 game-cloud → game-cloud 须能在 mini-desktop 起且能 fork 子进程(确认权限/PATH)。
中 MysqlSaver 注入 yudao Druid + 一图一 saver dossier §3-13/§5-5:MysqlSaver 收 DataSource(注 Druid),但一图只能注册一个 saver,注册俩抛异常;Redis 走业务旁路或自写双写装饰器。首次接需验 CREATE_IF_NOT_EXISTS 建表与 yudao Flyway 不冲突。
中 依赖冲突(Boot 3.5.14 / Redisson 4.4.0) dossier §3:不引 SAA Boot BOM(让 3.5.14 胜)、不用 RedisSaver(避 Redisson 3.x↔4.x)。形态②独立 worker 可用独立 Boot 版避此坑——对比期形态②额外降风险。
中 json_repair 无 Java 等价 便宜模型 gatespec/player JSON 常脏(studio.py:80-90 靠 json_repair 兜)。Java 需最小宽松解析(截块→Jackson 容错→字段缺省);脏到 Jackson 也解不动时按「gatespec 缺失→退 brief 默认 play_spec」降级(与 _extract_gatespec 返 None 同语义)。
低 代理旁路坑消失 _client.py:35-44 的 NO_PROXY 坑是 Python+clash 本机特有,JVM 不经 clash,迁移后此坑不存在(净简化)。
待定 对比测试口径 「Python vs SAA 生成游戏」对比需固定:同 brief 集 + 同 model(stage1)+ 同九门 harness + 同 maxRepairs;比较维度 = 九门通过率/平均修复轮数/wall/¥/player 评分。建议形态②并排跑同一批 briefs(wg1/gen-worker/briefs/*.json 16 款现货)。

6.2 迁移分步(可执行,供后续实现)

  1. 门 E(最便宜命题,dossier §3-47):aigc-server pom 加 3 BOM + graph-core(当前 pom 尚无 SAA 依赖,已核 game-module-aigc-server/pom.xml)→ dependency:tree 断言(Boot 全 3.5.14 / Redisson 4.4.0 / 无 3.22.0 泄漏)→ 纯 Java hello StateGraph 节点 → mvn package + 单体启动门(SAA 与 yudao 自动配置共存无 BeanCreation 失败)→ invoke 跑一节点。先不接模型/saver/job 链。
  2. 模型节点接 new-api:建 N 个 OpenAiChatModel bean(共用 OpenAiApi baseUrl=new-api,per-role model 名),一个 generate 节点真出 ```js,断言能拿到工厂源码 + usage。
  3. validate/scaffold/build/play 四节点接子进程:build/play 用 ProcessBuilder 调既有 harness(mini-desktop,cwd=game-runtime),读回 verdict.json;单测验九门 verdict 能解析、退出码正确传递。
  4. 组装裸图 + 条件边:render→design→generate→validate→[repair 环]→scaffold→build→play→player→emit/giveup,state repair_count/feedback 回喂;跑通一款 brief(如 breakout)端到端过九门。
  5. 接 MysqlSaver checkpoint:注 Druid DataSource,releaseThread(false),验同 threadId 续跑 + time-travel。
  6. 接 streaming 进度(可选,对比期可缓):节点塞 Flux,SSE 出口验前端收到逐节点进度。
  7. 形态②Java worker /generate 上线:Spring @RestController + 后台线程池 + Semaphore(1) 串行 + HMAC 回调;aigc.executor.worker-url 切到 Java worker,并排 Python worker。
  8. 对比测试:同批 briefs 各跑 Python worker / SAA worker,按 §6.1 待定口径出对比表(九门通过率/修复轮数/wall/¥/player 分)。
  9. 形态①收口(生产):对比验证 SAA 不残废后,实现进程内 SaaGraphDispatcher(去 HTTP+HMAC),灰度切 HttpWorkerDispatcher | SaaGraphDispatcher。

验证状态:本设计为只读分析 + 目标图设计,未写实现代码、未跑构建。Python 侧画像与 Java 契约边界均带 文件:行 证据(已逐行读);SAA 核心类文件位已核(spring-ai-alibaba-graph-core);SAA 用法/坑引自 dossier(四路源码取证)。下一步可验证项 = 迁移分步①门 E(最便宜、隔离「SAA 库能进 game-cloud」单命题)。


7. 实测进度与坑(增量更新)

step 2 已过(2026-06-15)— SAA→new-api→生成源码 最小链

  • 证据:game-module-aigc-server 内 SaaNewApiGenerateSmokeTest 绿(1 passed);deepseek-v4-flash@100.64.0.8:3000 真出 breakout 工厂源码(5779 chars,export default createGame);usage prompt=336 / completion=5717。
  • 文件:.../aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaNewApiGenerateSmokeTest.java

实测迁移坑(后续节点必看)

  1. 【高频坑】Spring AI OpenAiApi baseUrl 不要带 /v1:Spring AI 自拼 completionsPath=/v1/chat/completions,baseUrl 带 /v1 → 404 /v1/v1/...。而 openai-python 的 base_url 要带 /v1。同一 .env(NEWAPI_BASE_URL=.../v1)两侧通用须 Java 侧 stripV1Suffix() 剥末尾 /v1 → baseUrl 用 host 根。
  2. -am reactor 跑单测:上游模块无匹配测试会 "No tests matching" 失败 → 加 -Dsurefire.failIfNoSpecifiedTests=false。
  3. key:NEWAPI_KEY 在 mini-desktop /root/games-development-ai/wg1/gen-worker/.env(gitignored,worktree 内无);跑测试经环境变量传入。
  4. SAA model API(v1.1.2.2 javap 实证):OpenAiApi.builder().baseUrl(host根).apiKey() → OpenAiChatModel.builder().openAiApi().defaultOptions(OpenAiChatOptions.builder().model().temperature().maxTokens()) → model.call(new Prompt(List.of(SystemMessage,UserMessage))).getResult().getOutput().getText();usage=resp.getMetadata().getUsage().getPromptTokens()。