From fd5b245baaf727788028f25d32d10f8aa38eff60 Mon Sep 17 00:00:00 2001 From: lili Date: Thu, 18 Jun 2026 00:05:18 -0700 Subject: [PATCH] =?UTF-8?q?feat(saa):=20U2=20=E6=8E=A5=E7=BC=9D=20+=20U3?= =?UTF-8?q?=20=E7=9C=9F=E4=BA=A7=20gameDefinition=20+=20U4=20=E5=A4=9A?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=9B=9E=E9=80=80=EF=BC=88gamedef=20?= =?UTF-8?q?=E8=B7=AF=20additive=20=E7=81=B0=E5=BA=A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plan 2026-06-18-001 U2(后端接缝)/U3/U4 · 后端线 · 真结构化生成上产线(saaSourceMode=gamedef,默认 factory 字节不变)。 U3 真产 gameDefinition: - SaaPrompts 新增 GAMEDEF_SYSTEM(与现 SYSTEM 物理隔离,公平性铁律)+ buildGameDefMessages;内容=runtime-api-2d.md 约定提炼(schema/rt 面/命名约定/硬约束)+ 内联 scan-clean few-shot;硬约束由 build-from-source 静态扫描兜底。 - SaaStudioNodes.generateNode gamedef 分支:产结构化 gameDefinition(looseParse 脏 JSON 容错)落 K_SOURCE_PROJECT; factory 路(含 regenerate 消费 base 源)字节不变。 U2 后端接缝(gamedef→可玩,scaffold/build/play/harness/build.mjs 全零改): - build-from-source.mjs 加 CLI(node build-from-source.mjs / --validate;被 import 不执行)。 - SaaGenNodes.validateNode(gameRuntimeRoot,sourceMode) gamedef 分支:经 build-from-source.mjs 子进程校验+装配 sourceProject→factorySrc(下游 scaffold/build 零改消费);CLI 失败回喂 repair。 - 确定性点改解锁:modifyNode(sourceMode) gamedef 路真改源不 fail-loud;modifyRouter(sourceMode) gamedef deterministic→validate(装配 patched 源重验)/ factory→fail-loud;assetNode 幂等(已有 assets 跳过,不覆盖点改)。 U4 多模型回退链:generateNode 主模型(含 stage1/stage2)耗尽 180s×3 重试后,回退同 role 跨家档(M_CODE/FIX_FALLBACK =MiniMax-M3)再试;接现 callWithTimeoutAndRetry/looseParse/救场阶梯不另起限次。GenModels record 收敛入参。 线程:AigcExecutorProperties.saaSourceMode(默认 factory)→ SaaGraphDispatcher → SaaStudioGraph.build 9 参重载 → assemble → generate/validate/modify/modifyRouter;8/7/5 参 build 旧重载 delegate "factory" 不破现调用。 worker prompt.py 不动(gamedef 为 SAA 路专属、http worker 仍 iife,无 shared prompt 漂移=无 split-brain)。 验收(lili-mac 快走查):mvn test-compile 过(main+test);模型无关单测 SaaStudioGraphTopologyTest 30/30 (含新增 gamedef deterministic→validate 路由证)+ SaaStudioNodesRegressionTest 8/8,零失败;build-from-source CLI 实证(--validate exit0/2、assemble 产正确 import 路径工厂)。**gamedef 全链过门率≥60% cutover 门需 NEWAPI_KEY+ mini-desktop(authoritative,未在本机跑)**。默认 factory 路行为字节不变(灰度 opt-in)。 Co-Authored-By: Claude Opus 4.8 --- .../game/module/aigc/saa/SaaGenNodes.java | 107 +++++++++-- .../module/aigc/saa/SaaGraphDispatcher.java | 1 + .../game/module/aigc/saa/SaaPrompts.java | 88 +++++++++ .../game/module/aigc/saa/SaaStudioGraph.java | 57 ++++-- .../game/module/aigc/saa/SaaStudioNodes.java | 171 ++++++++++++------ .../executor/AigcExecutorProperties.java | 8 + .../aigc/saa/SaaStudioGraphTopologyTest.java | 74 +++++--- .../saa/SaaStudioNodesRegressionTest.java | 2 +- game-runtime/src/host/build-from-source.mjs | 47 +++++ 9 files changed, 441 insertions(+), 114 deletions(-) diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java index 2416e3fc..768fdeab 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java @@ -230,11 +230,23 @@ final class SaaGenNodes { } /** - * validate 节点(对齐 validate.py:validate):静态过了才跑 node --check(省时)。 - * 写 state:validateOk(boolean) / validateErrors(List) / feedback(失败回喂文字)。 + * validate 节点:按 sourceMode 二分(plan U3,additive)。 + *
    + *
  • factory(默认,字节不变,对齐 validate.py):静态契约扫描 {@code factorySrc} + node --check ESM 语法门;
  • + *
  • gamedef(plan U2 接缝):经 {@code build-from-source.mjs} 子进程校验 + 装配 {@code sourceProject} → + * 产 {@code factorySrc}(下游 scaffold/build 零改消费);CLI 失败(校验拒/超时)→ feedback 回喂 repair。
  • + *
+ * 写 state:validateOk(boolean) / validateErrors(List) / feedback(失败) / factorySrc(gamedef 成功时=装配产物)。 + * + * @param gameRuntimeRoot game-runtime 根(gamedef 路 build-from-source.mjs 子进程 cwd;factory 路不读)。 + * @param sourceMode 生成产物形态(factory|gamedef,AigcExecutorProperties.saaSourceMode)。 */ - static NodeAction validateNode() { + static NodeAction validateNode(Path gameRuntimeRoot, String sourceMode) { return (OverAllState s) -> { + if ("gamedef".equals(sourceMode)) { + return validateGameDef(s, gameRuntimeRoot); + } + // ── factory 路(现行 iife,字节不变):静态契约扫描 + node --check ── String src = s.value("factorySrc", String.class).orElse(""); List errs = staticCheck(src); if (errs.isEmpty()) { @@ -245,23 +257,84 @@ final class SaaGenNodes { errs.add("语法错误(node --check):" + trimmed); } } - Map out = new HashMap<>(); - boolean ok = errs.isEmpty(); - out.put("validateOk", ok); - out.put("validateErrors", errs); - if (!ok) { - StringBuilder fb = new StringBuilder("校验未过:\n"); - for (String e : errs) { - fb.append("- ").append(e).append("\n"); - } - out.put("feedback", fb.toString()); - clearPlayerFeedback(s, out); // A-4:硬门失败清空 player 软门反馈,杜绝串味(仅全图注册了该 key 时生效)。 - bumpFailCount(s, out); // P0-1:validate 硬门失败也计 failCount(救场阶梯对 validate/build/play 任一门统一成立,§6.3)。 - } - return out; + return validateOut(s, errs); }; } + /** + * gamedef 路校验+装配(plan U2 接缝):sourceProject JSON 经 {@code build-from-source.mjs } 子进程 + * 校验(schema/引用/静态扫描)+装配出 generated-factory 文本;成功落 {@code factorySrc}(下游 scaffold/build 零改), + * 失败把 CLI 错误(stderr,经 redirectErrorStream 并入 output)回喂 repair。harness/build.mjs 全程零改。 + */ + private static Map validateGameDef(OverAllState s, Path gameRuntimeRoot) { + String sp = s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse(""); + List errs = new ArrayList<>(); + if (sp.isEmpty()) { + errs.add("gamedef 路 sourceProject 为空(generate 未产出结构化源)"); + return validateOut(s, errs); + } + Path tmpSrc = null; + Path tmpOut = null; + try { + tmpSrc = Files.createTempFile("saa-gdsrc-", ".json"); + tmpOut = Files.createTempFile("saa-gdfac-", ".js"); + Files.write(tmpSrc, sp.getBytes(StandardCharsets.UTF_8)); + // 经唯一构建脚本同机的 build-from-source.mjs 装配(cwd=game-runtime 根,使产物 import 路径与 scaffold 落点一致)。 + ProcessBuilder pb = new ProcessBuilder("node", "src/host/build-from-source.mjs", + tmpSrc.toString(), tmpOut.toString()); + pb.directory(gameRuntimeRoot.toFile()); + ProcResult r = runProcess(pb, 30); + if (r.timedOut) { + errs.add("gamedef 装配超时(build-from-source.mjs 30s)"); + } else if (r.exitCode != 0) { + String out = r.output == null ? "" : r.output; + errs.add("gamedef 校验/装配未过:\n" + (out.length() > 800 ? out.substring(0, 800) : out)); + } else { + String factory = new String(Files.readAllBytes(tmpOut), StandardCharsets.UTF_8); + Map ok = new HashMap<>(); + ok.put("validateOk", true); + ok.put("validateErrors", new ArrayList()); + ok.put("factorySrc", factory); // 装配产物 → 下游 scaffold/build 零改消费(gamedef 接缝核心) + return ok; + } + } catch (IOException | InterruptedException e) { + errs.add("gamedef 装配子进程异常:" + e.getMessage()); + } finally { + deleteQuiet(tmpSrc); + deleteQuiet(tmpOut); + } + return validateOut(s, errs); + } + + /** 统一 validate 输出(成功=validateOk:true;失败=feedback + 清 player 软门 + 计 failCount)。 */ + private static Map validateOut(OverAllState s, List errs) { + Map out = new HashMap<>(); + boolean ok = errs.isEmpty(); + out.put("validateOk", ok); + out.put("validateErrors", errs); + if (!ok) { + StringBuilder fb = new StringBuilder("校验未过:\n"); + for (String e : errs) { + fb.append("- ").append(e).append("\n"); + } + out.put("feedback", fb.toString()); + clearPlayerFeedback(s, out); // A-4:硬门失败清空 player 软门反馈,杜绝串味(仅全图注册了该 key 时生效)。 + bumpFailCount(s, out); // P0-1:validate 硬门失败也计 failCount(救场阶梯对 validate/build/play 任一门统一成立,§6.3)。 + } + return out; + } + + /** 静默删临时文件(清理失败仅忽略)。 */ + private static void deleteQuiet(Path p) { + if (p != null) { + try { + Files.deleteIfExists(p); + } catch (IOException ignore) { + // 临时文件清理失败可忽略。 + } + } + } + // ====================== ② scaffold 节点 ====================== // 纯 Java 文件 IO(抄 run.py:33-42 语义)。 diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcher.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcher.java index aa32bf8f..ae01244b 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcher.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcher.java @@ -322,6 +322,7 @@ public class SaaGraphDispatcher implements GenerationDispatcher { compiledGraph = SaaStudioGraph.build(api, SaaStudioGraph.Models.stage1(), gameRuntimeRoot, properties.getSaaMaxRepairs(), properties.getSaaMaxPlayerRounds(), properties.getSaaStage2ExtraRepairs(), + properties.getSaaSourceMode(), // plan U3:factory(默认,iife) | gamedef(真结构化源) saverConfig, observationRegistry); log.info("[saa-dispatch] SAA 图构造完成(缓存复用):maxRepairs={}, stage2Extra={}, maxPlayerRounds={}, " + "newApiBase={}, checkpoint={}, observation={}", diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java index 1a095d27..0a1ef7e0 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java @@ -138,6 +138,72 @@ final class SaaPrompts { ``` """; + // ============================================================================ + // ①.1 GAMEDEF_SYSTEM —— plan 2026-06-18-001 U3:真结构化 gameDefinition 生成 SYSTEM。 + // 与 ① SYSTEM(iife factory)物理隔离·并存(公平性铁律:不改现 SYSTEM 一字); + // 仅 saaSourceMode=gamedef 路用。内容 = game-runtime/src/host/runtime-api-2d.md 约定的提炼 + 内联 few-shot。 + // 硬约束由 build-from-source.mjs 校验边界静态扫描兜底(违反→拒绝→repair),非靠模型自觉。 + // ============================================================================ + static final String GAMEDEF_SYSTEM = """ + 你是轻量 2D 小游戏的【结构化源生成器】。产出 = 一份声明式 gameDefinition(JSON)——游戏=可维护结构化源 + (实体/组件/场景/规则 + 各带逻辑 JS 的 behaviors),平台据它构建可玩包(改源不改包)。 + + 【输出格式·严格】只输出一个 JSON 对象(就是 gameDefinition 本身),不要 ```代码块标记、不要任何解释文字。顶层键: + {"entities":[...],"components":[...],"behaviors":[...],"scenes":[...],"rules":[...]} + + 【结构】 + - entities[]: {"id","transform":{"position":{"x","y"}},"components":[组件id...]}(可加 "vx","vy","tags":[...] 及任意数值字段如 idx) + - components[]: {"id","kind",...} kind ∈ render/physics/collision/custom + · render: {"kind":"render","shape":"rect|circle|fill","color":"#rrggbb"}; rect 加 "w","h"; circle 加 "r"; fill 铺满作背景 + · physics: {"kind":"physics","gravity":数}; 挂此组件的实体每帧自动 x+=vx*dt,y+=vy*dt(+重力) + - behaviors[]: {"id","trigger":"init|update|input|collision|timer","code":"<一段 JS 逻辑串>"} + code 编译为 function(rt,self,dt){...}: init 跑一次,其余每帧跑; self=本 behavior 持久态(跨帧存计时器/累加器) + - scenes[]: {"id","entityRefs":[实体id...]}(scenes[0] 决定初始实例化哪些实体) + - rules[]: {"id","condition":"","outcome":"win|lose|score|advance"} + + 【behavior/condition 唯一能用的 rt 面】 + - 实体: rt.getEntity(id) / rt.entities() / rt.query(tag) / rt.spawn({x,y,vx,vy,tags,components}) / rt.destroy(e); 实体含 .x.y.vx.vy.alive.tags + 可直接读写属性(e.hp=3) + - 输入: rt.input.isDown(key) / justPressed(key) / justTapped() / pointer{x,y,down} + - 时间随机(确定性): rt.time.now()(秒,从0) / rt.dt / rt.random() / rt.randRange(a,b) / rt.randInt(a,b) + - 分数胜负: rt.score / rt.addScore(n=1) / rt.setScore(n) / rt.win() / rt.lose()(置不可逆终态) + - 工具特效: rt.clamp(v,lo,hi) / rt.dist(ax,ay,bx,by) / rt.overlap(a,b)(AABB,{x,y,w,h}) / rt.fx.burst(x,y,color) / rt.fx.beep(kind) / rt.view{w:390,h:844} + + 【硬约束(违反=校验拒绝→打回重做,非靠自觉)】 + 1. 确定性: 随机一律 rt.random/randRange/randInt, 时间一律 rt.time.now()/dt。禁 Math.random、Date、performance(Math.sin/abs/floor/PI 等允许)。 + 2. 只用 rt: 禁 process/require/eval/.constructor/Function/document/window/fetch/addEventListener/import。要生成实体用 rt.spawn(别自造约定外 helper)。 + 3. 终态: 胜负用 rt.win()/rt.lose()(不可逆), 别用分数/flag 表达结束; 游戏必须"会输"(超时/碰撞/耗尽任一)。 + 4. 特效: 碰撞/得分等关键事件调 rt.fx.burst/beep(真接引擎=满真接线门)。 + 5. condition 是无副作用布尔表达式: 禁 ; / 赋值 / 调 win/lose/addScore/spawn/destroy。 + 6. 死循环: 禁 while(true)/for(;;)。 + + 【可玩 + 被九门驱动(关键命名约定,务必对齐)】 + - 实体须有 render 组件才可见; 放一个 shape:"fill" 实体铺满 390x844 作背景(否则判白屏)。 + - 九门据【命名路径】驱动你的游戏: + · 挡板接球类: 球实体 id 命名 "ball"、挡板命名 "paddle"(九门读 ball.x/paddle.x 真玩对齐)。 + · 离散点击类(井字棋/打地鼠/翻牌/扫雷): 每个可点目标实体打 tags:["target"], 设 idx(序号)/occupied(是否已占)(九门据 targets[] 逐个点; 不打 target 标→点不到→机制门必挂)。 + - HUD/进展可见: 分数/剩余看得到; 前 30 秒能上手、有正反馈、看得到分数增长。 + + 【参照(严格按此结构产 JSON, 但实现成题目要求的那款游戏; 这是个躲避类示例)】 + { + "components":[ + {"id":"bg","kind":"render","shape":"fill","color":"#101028"}, + {"id":"rp","kind":"render","shape":"rect","color":"#33dd66","w":34,"h":34}, + {"id":"re","kind":"render","shape":"circle","color":"#ff4444","r":14} + ], + "entities":[ + {"id":"world","transform":{"position":{"x":0,"y":0}},"components":["bg"]}, + {"id":"player","transform":{"position":{"x":195,"y":720}},"components":["rp"]} + ], + "behaviors":[ + {"id":"control","trigger":"update","code":"const p=rt.getEntity('player'); if(!p) return; if(rt.input.isDown('ArrowLeft')) p.x-=260*dt; if(rt.input.isDown('ArrowRight')) p.x+=260*dt; if(rt.input.pointer.down) p.x=rt.input.pointer.x; p.x=rt.clamp(p.x,20,370);"}, + {"id":"spawn","trigger":"update","code":"self.t=(self.t||0)+dt; self.s=(self.s||0)+dt; if(self.t>=0.4){ self.t=0; rt.spawn({x:rt.randRange(20,370),y:-20,vy:rt.randRange(220,360),tags:['enemy'],components:[{kind:'render',shape:'circle',color:'#ff4444',r:14}]}); } if(self.s>=1){ self.s-=1; rt.addScore(1); }"}, + {"id":"enemies","trigger":"update","code":"const p=rt.getEntity('player'); for(const e of rt.query('enemy')){ e.y+=e.vy*dt; if(e.y>880){ rt.destroy(e); } else if(p && rt.overlap({x:e.x,y:e.y,w:26,h:26},{x:p.x,y:p.y,w:34,h:34})){ rt.fx.burst(e.x,e.y,'#ff0000'); rt.fx.beep('lose'); rt.lose(); } }"} + ], + "scenes":[{"id":"main","entityRefs":["world","player"]}], + "rules":[{"id":"survive","condition":"rt.score >= 20","outcome":"win"}] + } + """; + // ============================================================================ // ② DESIGN_SYSTEM —— 逐字移植 roles.py:8-32(设计 agent + 末尾自产 gatespec)。 // ============================================================================ @@ -259,6 +325,28 @@ final class SaaPrompts { return new String[]{system, user}; } + /** + * 组装 gamedef 路 (system, user)(plan U3,saaSourceMode=gamedef)。与 {@link #buildMessages}(iife) 物理隔离: + * system=GAMEDEF_SYSTEM(含 schema/rt 面/命名约定/内联 few-shot),user=题面 + 品类提示 + 回喂上轮失败原因(全量重出)。 + * + * @param briefText 题面(含设计稿 enriched)。 + * @param retryFeedback 重试回喂(null/空=首轮)。 + * @param archetype 品类原型(K_ARCHETYPE,generic 不附提示)。 + * @return [0]=system(GAMEDEF_SYSTEM),[1]=user。 + */ + static String[] buildGameDefMessages(String briefText, String retryFeedback, String archetype) { + StringBuilder user = new StringBuilder("请为下面这款游戏产出一份 gameDefinition(只输出一个 JSON 对象,不要 ```代码块、不要解释):\n\n"); + user.append(briefText == null ? "" : briefText); + if (archetype != null && !archetype.isEmpty() && !"generic".equals(archetype)) { + user.append("\n\n(品类提示:").append(archetype).append(")"); + } + if (retryFeedback != null && !retryFeedback.isEmpty()) { + user.append("\n\n———\n上一次生成【未通过】,失败原因如下,请针对性修正后重新产出完整 gameDefinition JSON(不要只给片段):\n") + .append(retryFeedback); + } + return new String[]{GAMEDEF_SYSTEM, user.toString()}; + } + /** base 源注入上限(防超长 prompt 撑爆上下文/计费;便宜模型上下文窗口有限)。超出则截断并标注。 */ private static final int SOURCE_PROJECT_MAX_CHARS = 12000; diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraph.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraph.java index 0d837e2f..b3a44218 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraph.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraph.java @@ -83,6 +83,13 @@ public final class SaaStudioGraph { /** stage2 fix 角色模型名(强档;models.yaml stage2.fix)。 */ public static final String M_FIX_STAGE2 = "deepseek-v4-pro"; + // ===== U4 可靠性回退档模型名(plan 2026-06-18-001 U4)===== + // 主模型(含 stage1/stage2)耗尽 180s×3 重试后再试一档,跨家提高可用性(治 MiniMax 超时/M2.5 返空/deepseek JSON 截断)。 + /** code 角色回退模型名(U4;跨家,提高可用性)。 */ + public static final String M_CODE_FALLBACK = "MiniMax-M3"; + /** fix 角色回退模型名(U4)。 */ + public static final String M_FIX_FALLBACK = "MiniMax-M3"; + /** 图名(可观测/可视化标识)。 */ public static final String GRAPH_NAME = "saa-studio-full"; @@ -108,11 +115,12 @@ public final class SaaStudioGraph { * @param fixStage2 救场升档后 fix 角色模型名(P1-5;强档) */ public record Models(String design, String code, String fix, String playerText, String playerVision, - String classify, String narrative, String codeStage2, String fixStage2) { - /** models.yaml stage1 默认路由(+ 固定架构 classify/narrative 角色 + 救场 stage2 强档 code/fix)。 */ + String classify, String narrative, String codeStage2, String fixStage2, + String codeFallback, String fixFallback) { + /** models.yaml stage1 默认路由(+ 固定架构 classify/narrative + 救场 stage2 强档 + U4 回退档 code/fix)。 */ public static Models stage1() { return new Models(M_DESIGN, M_CODE, M_FIX, M_PLAYER_TEXT, M_PLAYER_VISION, M_CLASSIFY, M_NARRATIVE, - M_CODE_STAGE2, M_FIX_STAGE2); + M_CODE_STAGE2, M_FIX_STAGE2, M_CODE_FALLBACK, M_FIX_FALLBACK); } } @@ -203,7 +211,8 @@ public final class SaaStudioGraph { * @throws GraphStateException 图结构非法(节点/边布线错误,编译期暴露) */ private static StateGraph assemble(OpenAiApi api, Models models, Path gameRuntimeRoot, - int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs) + int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs, + String sourceMode) throws GraphStateException { // == per-role 采样面(temperature/maxTokens)逐字对齐 Python == // design:Python config.build_model 不传 temperature → 服务端默认 → temperature=null(不下发); @@ -216,6 +225,9 @@ public final class SaaStudioGraph { // P1-5:救场 stage2 强档 code/fix(同 16000/temperature=0,仅模型名换强档);generate 据 modelTier 真切,使升档生效。 OpenAiChatModel codeStage2Model = model(api, models.codeStage2(), 0.0, 16000); OpenAiChatModel fixStage2Model = model(api, models.fixStage2(), 0.0, 16000); + // U4 回退档(同采样面 0.0/16000,仅模型名换跨家;generateNode 主耗尽重试后再试一档)。 + OpenAiChatModel codeFallbackModel = model(api, models.codeFallback(), 0.0, 16000); + OpenAiChatModel fixFallbackModel = model(api, models.fixFallback(), 0.0, 16000); OpenAiChatModel playerTextModel = model(api, models.playerText(), 0.3, 4000); OpenAiChatModel playerVisionModel = model(api, models.playerVision(), 0.3, 900); // 固定架构新增两角色(B2):classify(物理优先分类,温度低求稳)、narrative-reviewer(叙事审查,温度低)。 @@ -236,17 +248,20 @@ public final class SaaStudioGraph { .addNode("render", node_async(SaaStudioNodes.renderNode())) .addNode("classify", node_async(SaaStudioNodes.classifyNode(classifyModel, models.classify()))) .addNode("design", node_async(SaaStudioNodes.designNode(designModel, models.design(), gameRuntimeRoot))) - .addNode("generate", node_async(SaaStudioNodes.generateNode(codeModel, fixModel, - codeStage2Model, fixStage2Model, models.code(), models.fix(), - models.codeStage2(), models.fixStage2(), gameRuntimeRoot))) - .addNode("validate", node_async(SaaGenNodes.validateNode())) + .addNode("generate", node_async(SaaStudioNodes.generateNode( + new SaaStudioNodes.GenModels(codeModel, fixModel, codeStage2Model, fixStage2Model, + codeFallbackModel, fixFallbackModel, + models.code(), models.fix(), models.codeStage2(), models.fixStage2(), + models.codeFallback(), models.fixFallback()), + gameRuntimeRoot, sourceMode))) + .addNode("validate", node_async(SaaGenNodes.validateNode(gameRuntimeRoot, sourceMode))) .addNode("scaffold", node_async(SaaGenNodes.scaffoldNode(gameRuntimeRoot))) .addNode("asset", node_async(SaaStudioNodes.assetNode())) .addNode("build", node_async(SaaGenNodes.buildNode(gameRuntimeRoot))) .addNode("play", node_async(SaaGenNodes.playNode(gameRuntimeRoot))) .addNode("player", node_async(SaaStudioNodes.playerNode(playerTextModel, playerVisionModel, models.playerText(), models.playerVision(), gameRuntimeRoot))) .addNode("nreview", node_async(SaaStudioNodes.nreviewNode(narrativeModel, models.narrative()))) - .addNode("modify", node_async(SaaStudioNodes.modifyNode())) + .addNode("modify", node_async(SaaStudioNodes.modifyNode(sourceMode))) .addNode("escalate", node_async(SaaStudioNodes.escalateNode())) .addNode("repair", node_async(SaaStudioNodes.repairNode())) .addNode("emit", node_async(SaaStudioNodes.emitNode(gameRuntimeRoot))) @@ -295,9 +310,10 @@ public final class SaaStudioGraph { // modify:regenerate-module→generate / deterministic→END(fail)(§6.2 + P0-4 guard)。 // P0-4:deterministic 路本期 guard fail-loud(modify 节点已置 failed 终态,直达 END), // 绝不走会忽略 sourceProject 的旧 build;TODO(B7/engine-E2) 接入从 sourceProject 真构建后改回 "build"→build。 + // modify:失败终态→END(fail) / regenerate-module→generate / deterministic(gamedef)→validate(装配 patched 源重验) / deterministic(factory)→fail。 .addConditionalEdges("modify", - edge_async(SaaStudioNodes.modifyRouter()), - Map.of("generate", "generate", "fail", END)); + edge_async(SaaStudioNodes.modifyRouter(sourceMode)), + Map.of("generate", "generate", "validate", "validate", "fail", END)); } /** @@ -383,7 +399,24 @@ public final class SaaStudioGraph { int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs, SaverConfig saverConfig, ObservationRegistry observationRegistry) throws GraphStateException { - StateGraph graph = assemble(api, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds, stage2ExtraRepairs); + // 默认 factory 形态(现行行为字节不变);gamedef 路走下方 9 参重载(dispatcher 据 saaSourceMode 选)。 + return build(api, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds, stage2ExtraRepairs, + "factory", saverConfig, observationRegistry); + } + + /** + * 组装并编译全图(plan 2026-06-18-001 U3 全量重载:显式 {@code sourceMode}=factory|gamedef)。 + * 布线唯一源 = {@link #assemble}(所有 build 重载共用)。 + * + * @param sourceMode 生成产物形态(factory=iife 现行 / gamedef=真结构化源;dispatcher 据 AigcExecutorProperties.saaSourceMode 传)。 + */ + public static CompiledGraph build(OpenAiApi api, Models models, Path gameRuntimeRoot, + int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs, + String sourceMode, + SaverConfig saverConfig, ObservationRegistry observationRegistry) + throws GraphStateException { + StateGraph graph = assemble(api, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds, + stage2ExtraRepairs, sourceMode); CompileConfig.Builder cc = CompileConfig.builder() .recursionLimit(recursionLimitFor(maxRepairs, stage2ExtraRepairs)); diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodes.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodes.java index 1dc127f4..169b117e 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodes.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodes.java @@ -431,71 +431,112 @@ final class SaaStudioNodes { * @param fixStage2Name fix stage2 模型名(per-model 拆账)。 * @param gameRuntimeRoot game-runtime 根(few-shot 探针)。 */ - static NodeAction generateNode(ChatModel codeModel, ChatModel fixModel, - ChatModel codeStage2Model, ChatModel fixStage2Model, - String codeModelName, String fixModelName, - String codeStage2Name, String fixStage2Name, Path gameRuntimeRoot) { + /** + * generate 节点的模型束(收敛入参,plan U4 加回退档):(role × tier) 选主模型 + 同 role 回退模型。 + * stage1/stage2 = P1-5 救场升档档;fallback = U4 可靠性回退档(主模型耗尽重试后再试,治超时/返空/JSON 截断)。 + */ + record GenModels(ChatModel code, ChatModel fix, ChatModel codeStage2, ChatModel fixStage2, + ChatModel codeFallback, ChatModel fixFallback, + String codeName, String fixName, String codeStage2Name, String fixStage2Name, + String codeFallbackName, String fixFallbackName) { + } + + static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode) { return (OverAllState s) -> { String role = s.value(K_ROLE, "code"); boolean isFix = "fix".equals(role); // P1-5:modelTier=stage2(escalate 升档后)→ 选 stage2 强档模型;否则用 stage1。模型对象与模型名选择同口径。 boolean isStage2 = "stage2".equals(s.value(K_MODEL_TIER, String.class).orElse("stage1")); - ChatModel model = isStage2 - ? (isFix ? fixStage2Model : codeStage2Model) - : (isFix ? fixModel : codeModel); - String mname = selectGenerateModelName(role, isStage2 ? "stage2" : "stage1", - codeModelName, fixModelName, codeStage2Name, fixStage2Name); + ChatModel primary = isStage2 + ? (isFix ? gm.fixStage2() : gm.codeStage2()) + : (isFix ? gm.fix() : gm.code()); + String pname = selectGenerateModelName(role, isStage2 ? "stage2" : "stage1", + gm.codeName(), gm.fixName(), gm.codeStage2Name(), gm.fixStage2Name()); + // U4 回退档(同 role;主模型耗尽 180s×3 重试后再试一档,治超时/返空/截断;接现救场体系不另起限次)。 + ChatModel fallback = isFix ? gm.fixFallback() : gm.codeFallback(); + String fname = isFix ? gm.fixFallbackName() : gm.codeFallbackName(); int attemptNo = ((Number) s.value(K_REPAIR_COUNT, 0)).intValue(); // 当前修复轮序(首轮=0)。 String enriched = s.value(K_ENRICHED, String.class) .orElse(s.value(K_BRIEF, String.class).orElse("")); String feedback = s.value(K_FEEDBACK, String.class).orElse(null); + boolean gamedef = "gamedef".equals(sourceMode); - // ── P0-2(regenerate-module 路真消费 base 源):仅当本轮是 regenerate-module 路(modifyMode=regenerate-module - // ∧ role=fix〔modify 节点置〕)∧ base 源非空时,把 base 源 + 目标 behavior + intent 注入 prompt - // (informed-by-base 重生成);否则(create / 普通 repair 失败回边)走原 buildMessages,prompt 字节零变。 - // target/intent 从 K_MODIFY_PATCH 复用 resolveBehaviorTarget 再取(单一事实源,与 modify 节点同口径; - // modify 节点已经 P0-1 守卫保证 regenerate 路 target 非空)。 + // ── 组 messages:gamedef 路用 GAMEDEF_SYSTEM(真结构化源);factory 路保现行(regenerate 消费 base 源 / 普通)。 String modifyMode = s.value(K_MODIFY_MODE, String.class).orElse(""); String sourceProject = s.value(K_SOURCE_PROJECT, String.class).orElse(""); - boolean regeneratePath = isFix && "regenerate-module".equals(modifyMode) && !sourceProject.isEmpty(); String[] msgs; - if (regeneratePath) { - JsonNode patch = SaaPrompts.looseParse(s.value(K_MODIFY_PATCH, String.class).orElse("")); - String target = resolveBehaviorTarget(patch); - String intent = patch != null ? patch.path("payload").path("intent").asText("") : ""; - msgs = SaaPrompts.buildRegenerateMessages(enriched, feedback, gameRuntimeRoot, - sourceProject, target, intent); + if (gamedef) { + // gamedef:题面 + 品类提示 + 回喂;regenerate-module 在 gamedef 路退化为带反馈整源重生成(确定性点改走 modify 节点)。 + String archetype = s.value(K_ARCHETYPE, String.class).orElse("generic"); + msgs = SaaPrompts.buildGameDefMessages(enriched, feedback, archetype); } else { - msgs = SaaPrompts.buildMessages(enriched, feedback, gameRuntimeRoot); + // P0-2(factory regenerate-module 路真消费 base 源·informed-by-base 重生成);否则普通 buildMessages,prompt 字节零变。 + boolean regeneratePath = isFix && "regenerate-module".equals(modifyMode) && !sourceProject.isEmpty(); + if (regeneratePath) { + JsonNode patch = SaaPrompts.looseParse(s.value(K_MODIFY_PATCH, String.class).orElse("")); + String target = resolveBehaviorTarget(patch); + String intent = patch != null ? patch.path("payload").path("intent").asText("") : ""; + msgs = SaaPrompts.buildRegenerateMessages(enriched, feedback, gameRuntimeRoot, + sourceProject, target, intent); + } else { + msgs = SaaPrompts.buildMessages(enriched, feedback, gameRuntimeRoot); + } } + Map out = new HashMap<>(); - // A-1:generate(含 repair 轮,role=fix 时)LLM 调用套 180s 超时 + ≤3 重试; - // 超限不裸退图——把错因写 feedback + factorySrc 留空(validate 判空过短 → 路由 repair/giveup,对齐 run.py:103-105)。 - Object[] r; - try { - r = callAndRecord(model, msgs[0], msgs[1]); - } catch (Exception e) { - out.put(K_FACTORY_SRC, ""); - out.put(K_FEEDBACK, "模型调用失败(已重试):" + trunc(e.getMessage(), 300)); - // B-8:LLM 失败也记一轮(stage_fail=generate,usage 0)。 + // ── U4:主→回退多模型链(每档内仍 180s×3 重试)。任一档成功即用;全失败不裸退图(写 feedback,下游路由 repair/giveup)。 + ChatModel[] chain = (fallback != null && fallback != primary) + ? new ChatModel[]{primary, fallback} : new ChatModel[]{primary}; + String[] chainNames = (fallback != null && fallback != primary) + ? new String[]{pname, fname} : new String[]{pname}; + Object[] r = null; + String usedModel = pname; + Exception lastErr = null; + for (int i = 0; i < chain.length; i++) { + try { + r = callAndRecord(chain[i], msgs[0], msgs[1]); + usedModel = chainNames[i]; + lastErr = null; + break; + } catch (Exception e) { + lastErr = e; // 本档耗尽重试 → 试下一回退档(最后一档仍失败落循环外)。 + } + } + if (r == null) { + String key = gamedef ? K_SOURCE_PROJECT : K_FACTORY_SRC; + out.put(key, ""); + out.put(K_FEEDBACK, "模型调用失败(主+回退档均已重试):" + trunc(lastErr == null ? "" : lastErr.getMessage(), 300)); appendAttempt(s, out, MAPPER.createObjectNode() - .put("attempt", attemptNo).put("role", role).put("model", mname) + .put("attempt", attemptNo).put("role", role).put("model", pname) .put("stage_fail", "generate") .set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0))); return out; } - String src = extractJsBlock((String) r[0]); - out.put(K_FACTORY_SRC, src); - // 抽不到 ```js → 写 feedback(对齐 studio.py:242-245 的 extract 失败桶),validate 会判空过短再兜。 - if (src.isEmpty()) { - out.put(K_FEEDBACK, "未能从输出中提取 ```js 代码块。请只输出一个完整的 ```js 代码块,块内是完整工厂源码。"); + + // ── 抽产物:gamedef → 结构化 gameDefinition(looseParse 脏 JSON 容错)落 K_SOURCE_PROJECT;factory → ```js 落 K_FACTORY_SRC。 + String stageFail = null; + if (gamedef) { + JsonNode gd = SaaPrompts.looseParse((String) r[0]); + if (gd == null || !gd.isObject()) { + out.put(K_SOURCE_PROJECT, ""); + out.put(K_FEEDBACK, "未能从输出解析出 gameDefinition JSON。请只输出一个 JSON 对象(顶层含 entities/components/behaviors/scenes/rules)。"); + stageFail = "extract"; + } else { + out.put(K_SOURCE_PROJECT, gd.toString()); // validate(gamedef) 经 build-from-source 校验+strip+装配 + } + } else { + String src = extractJsBlock((String) r[0]); + out.put(K_FACTORY_SRC, src); + if (src.isEmpty()) { + out.put(K_FEEDBACK, "未能从输出中提取 ```js 代码块。请只输出一个完整的 ```js 代码块,块内是完整工厂源码。"); + stageFail = "extract"; + } } long in = (Long) r[1], outTok = (Long) r[2]; - accumTokensByModel(s, out, mname, in, outTok); // B-8:per-model 拆账(含总量累加)。 - // B-8:记本生成轮(attempt 序号/角色/模型/usage;后续 validate/build/play 失败由各节点补 stage_fail 到尾记录)。 + accumTokensByModel(s, out, usedModel, in, outTok); // B-8:per-model 拆账(记真实所用档,含回退)。 appendAttempt(s, out, MAPPER.createObjectNode() - .put("attempt", attemptNo).put("role", role).put("model", mname) - .put("stage_fail", src.isEmpty() ? "extract" : (String) null) + .put("attempt", attemptNo).put("role", role).put("model", usedModel) + .put("stage_fail", stageFail) .set("usage", MAPPER.createObjectNode().put("in", in).put("out", outTok))); return out; }; @@ -970,6 +1011,13 @@ final class SaaStudioNodes { String spStr = s.value(K_SOURCE_PROJECT, String.class).orElse(""); Map out = new HashMap<>(); + // 幂等(plan U2):sourceProject 已有非空 assets[](如 deterministic modify 复用既有源、repair 回环重入)→ 跳过, + // 不覆盖既有资产(杜绝确定性点改的 assets 改动被本节点重产覆盖)。 + JsonNode pre = SaaPrompts.looseParse(spStr); + if (pre != null && pre.isObject() && pre.path("assets").isArray() && pre.path("assets").size() > 0) { + return out; // passthrough(已有资产,不重产) + } + // ① 产六类资产规格(v0 几何兜底:每类一条占位 spec,ref=逻辑名,provider=mmx-cli)。 com.fasterxml.jackson.databind.node.ArrayNode specs = MAPPER.createArrayNode(); for (String cat : new String[]{"sprite", "character", "effect", "scene", "ui", "music"}) { @@ -1019,7 +1067,7 @@ final class SaaStudioNodes { *

regenerate-module 路本期可达:它走 generate(LLM 在 sourceProject 基础上重出 behavior 模块), * 不依赖「从 sourceProject 构建」管线,故不受 P0-4 限制(B7 完整应用时按 target 精确定位模块为增强)。 */ - static NodeAction modifyNode() { + static NodeAction modifyNode(String sourceMode) { return (OverAllState s) -> { String mode = s.value(K_MODIFY_MODE, String.class).orElse(""); String patchStr = s.value(K_MODIFY_PATCH, String.class).orElse(""); @@ -1093,14 +1141,15 @@ final class SaaStudioNodes { applyDeterministicPatch(sp, patch.path("target"), patch.path("payload").path("value")); } out.put(K_SOURCE_PROJECT, sp.toString()); - out.put(K_MODIFY_MODE, "deterministic"); // 固化路由信号(modifyRouter 据此走 fail-loud 终态)。 - // P0-4 guard fail-loud:从 sourceProject 真构建的管线(B7+引擎线 E2)本期未实现 → 显式置失败终态, - // 绝不静默走会忽略 sourceProject 的旧 build(旧 build 从 factorySrc/gameDir 构建,确定性编辑改的 sourceProject 不会生效)。 - // TODO(B7/engine-E2):补「从 sourceProject 真构建」管线后,本路改为真路由 build(删除以下两行 + modifyRouter 改回 deterministic→build)。 - out.put(K_STATUS, "failed"); - out.put(K_FAILURE, "modify 源构建管线待 B7+引擎线 E2(从 sourceProject 构建)实现:" - + "deterministic 编辑已真改源项目,但当前 build 节点仅从 factorySrc/gameDir 构建、不消费 sourceProject," - + "为避免静默产出忽略编辑的错误产物,本路显式失败(非空壳:sourceProject 已真改、待下游构建管线接入)"); + out.put(K_MODIFY_MODE, "deterministic"); // 固化路由信号(modifyRouter 据 sourceMode 分流)。 + // plan U2 接缝:gamedef 路确定性点改已解锁——build-from-source(U2) 可从 sourceProject 真构建, + // 故 gamedef 路真改源后不置失败,由 modifyRouter→validate 装配 patched 源→build/play 重验。 + // factory 路仍 fail-loud:源即 iife factorySrc、无结构化源可从之确定性构建,避免静默产出忽略编辑的错误产物。 + if (!"gamedef".equals(sourceMode)) { + out.put(K_STATUS, "failed"); + out.put(K_FAILURE, "factory 形态无结构化源可确定性点改(源即 iife):deterministic 编辑无处生效," + + "本路显式失败(saaSourceMode=gamedef 才支持确定性点改:改 gameDefinition 一处→重装配重验)"); + } return out; }; } @@ -1419,17 +1468,27 @@ final class SaaStudioNodes { } /** - * modify 路由(execution §6.2):modify 节点后据 {@link #K_MODIFY_MODE} 分流—— + * modify 路由(execution §6.2 + plan U2 接缝):modify 节点后分流—— *

    - *
  • regenerate-module → generate(LLM 在 sourceProject 基础上重出 behavior 模块,本期可达);
  • - *
  • deterministic → {@code "fail"}(P0-4 guard fail-loud:modify 节点已置失败终态,路由直达 END, - * 绝不走会忽略 sourceProject 的旧 build;TODO(B7/engine-E2) 接入「从 sourceProject 真构建」后改回 deterministic→build)。
  • + *
  • {@link #K_STATUS}=failed(regenerate 守卫失败 / factory 确定性 fail-loud)→ {@code "fail"}(直达 END);
  • + *
  • regenerate-module(未失败)→ generate(LLM 在 sourceProject 基础上重出 behavior 模块);
  • + *
  • deterministic(未失败)→ gamedef 路 {@code "validate"}(U2 接缝:真改源 → validate 经 build-from-source + * 装配 patched 源 → scaffold/build/play 重验,解锁确定性点改);factory 路 {@code "fail"}(无结构化源可装配)。
  • *
+ * + * @param sourceMode 生成产物形态(factory|gamedef):决定 deterministic 路是否解锁(gamedef→validate / factory→fail)。 */ - static EdgeAction modifyRouter() { + static EdgeAction modifyRouter(String sourceMode) { return (OverAllState s) -> { + if ("failed".equals(s.value(K_STATUS, String.class).orElse(""))) { + return "fail"; // 失败终态(regenerate 守卫失败 / factory 确定性 fail-loud)→ END + } String mode = s.value(K_MODIFY_MODE, String.class).orElse("deterministic"); - return "regenerate-module".equals(mode) ? "generate" : "fail"; + if ("regenerate-module".equals(mode)) { + return "generate"; + } + // deterministic(未失败):gamedef 真改源 → validate 装配重验;factory 无结构化源 → fail-loud。 + return "gamedef".equals(sourceMode) ? "validate" : "fail"; }; } diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/executor/AigcExecutorProperties.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/executor/AigcExecutorProperties.java index e4bdad31..cb264184 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/executor/AigcExecutorProperties.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/executor/AigcExecutorProperties.java @@ -189,6 +189,14 @@ public class AigcExecutorProperties { */ private Boolean saaCheckpointEnabled = true; + /** + * SAA 生成产物形态(plan 2026-06-18-001 U3,additive 灰度):{@code factory}=现行 iife factorySrc(generate 产 ```js 工厂、 + * validate 走 12 禁词静态门,默认·行为字节不变);{@code gamedef}=真结构化 gameDefinition(generate 产声明式源落 + * {@code sourceProject}、validate 经 build-from-source.mjs 校验+装配出工厂——「改源不改包/可维护结构化源」范式上产线)。 + * cutover 量化门 = gamedef 路 SaaFullGraphE2eTest 过门率 ≥ 60%(iife 现基线);达标才切默认。仅 dispatcher=saa 生效。 + */ + private String saaSourceMode = "factory"; + /** * 阈值关系铁律校验(HJ-AGENT-LOOP-EXEC-002 §5.2 精确式;执行器装配时调用,误配置 fail-fast) * diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraphTopologyTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraphTopologyTest.java index 1ab4c65a..25585ce4 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraphTopologyTest.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioGraphTopologyTest.java @@ -178,18 +178,36 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "deterministic", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); JsonNode sp = MAPPER.readTree(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("{}")); assertEquals(9, sp.path("config").path("speed").asInt(), "确定性编辑应改中 /config/speed"); assertEquals(3, sp.path("config").path("size").asInt(), "未指定字段保持不变(只改一处)"); // P0-4:deterministic 路本期 guard fail-loud——sourceProject 真改后路由 "fail"(→END),不走会忽略 sourceProject 的旧 build。 - assertEquals("fail", SaaStudioNodes.modifyRouter().apply(s), + assertEquals("fail", SaaStudioNodes.modifyRouter("factory").apply(s), "P0-4:deterministic 路由 fail(→END),TODO(B7/engine-E2) 接入从 sourceProject 真构建后改回 build"); assertEquals("failed", s.value(SaaStudioNodes.K_STATUS, String.class).orElse(""), "P0-4:deterministic 显式失败终态(不静默产出忽略编辑的错产物)"); } + @Test + void modify_deterministic_gamedef_unlocks_validate_route() throws Exception { + // plan U2 接缝:gamedef 路确定性点改解锁——真改源 + 路由 validate(装配 patched 源重验),不再 fail-loud。 + String base = "{\"schemaVersion\":\"1.0\",\"config\":{\"speed\":5}}"; + String patch = "{\"mode\":\"deterministic\",\"target\":{\"path\":\"/config/speed\"},\"payload\":{\"value\":9}}"; + OverAllState s = newFullState(Map.of( + SaaStudioNodes.K_MODIFY_MODE, "deterministic", + SaaStudioNodes.K_MODIFY_PATCH, patch, + SaaStudioNodes.K_SOURCE_PROJECT, base)); + applyNode(s, SaaStudioNodes.modifyNode("gamedef")); + JsonNode sp = MAPPER.readTree(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("{}")); + assertEquals(9, sp.path("config").path("speed").asInt(), "gamedef 确定性改也真改源一处"); + assertTrue(!"failed".equals(s.value(SaaStudioNodes.K_STATUS, String.class).orElse("")), + "gamedef 路确定性改不再 fail-loud(已解锁)"); + assertEquals("validate", SaaStudioNodes.modifyRouter("gamedef").apply(s), + "U2 接缝:gamedef deterministic → validate(装配 patched 源 → build/play 重验)"); + } + @Test void modify_deterministic_top_level_segment_overwrite() throws Exception { // 顶层整段覆写 /config。 @@ -200,7 +218,7 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "deterministic", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); JsonNode sp = MAPPER.readTree(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("{}")); assertEquals(20, sp.path("config").path("speed").asInt()); assertTrue(sp.path("config").path("hard").asBoolean()); @@ -214,12 +232,12 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, "{\"schemaVersion\":\"1.0\"}")); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); assertEquals("fix", s.value(SaaStudioNodes.K_ROLE, String.class).orElse(""), "重生成模块路置 role=fix"); assertTrue(s.value(SaaStudioNodes.K_FEEDBACK, String.class).orElse("").contains("让跳跃更高更软"), "intent 应作为 feedback 喂 generate"); // 路由:modifyRouter → generate。 - assertEquals("generate", SaaStudioNodes.modifyRouter().apply(s)); + assertEquals("generate", SaaStudioNodes.modifyRouter("factory").apply(s)); } // ====================== ⑦ escalate 节点:升档 stage1→stage2 + 记事件 ====================== @@ -341,7 +359,7 @@ class SaaStudioGraphTopologyTest { OverAllState s = newFullState(Map.of( "factorySrc", "", // 空源码 → staticCheck「为空或过短」失败(纯内存,无子进程) SaaStudioNodes.K_FAIL_COUNT, 0)); - applyNode(s, SaaGenNodes.validateNode()); + applyNode(s, SaaGenNodes.validateNode(null, "factory")); assertFalse(s.value(SaaStudioNodes.K_VALIDATE_OK, Boolean.class).orElse(true), "空源码 validate 应失败"); assertEquals(1, ((Number) s.value(SaaStudioNodes.K_FAIL_COUNT, -1)).intValue(), "P0-1:validate 硬门失败必须 failCount++(旧实现漏此,阶梯永不触发)"); @@ -417,18 +435,18 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "deterministic", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); // ① sourceProject 已真改(非空壳:编辑生效、不丢)。 JsonNode sp = MAPPER.readTree(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("{}")); assertEquals(9, sp.path("config").path("speed").asInt(), "deterministic 应真改 sourceProject(不丢编辑)"); - // ② 显式失败终态(fail-loud),failureReason 指向 B7/引擎线 E2 待实现。 + // ② 显式失败终态(fail-loud),failureReason 指明 factory 形态不支持确定性点改、须用 gamedef(U2 已解锁 gamedef 路)。 assertEquals("failed", s.value(SaaStudioNodes.K_STATUS, String.class).orElse(""), - "P0-4:deterministic 路必须显式失败,绝不静默走会忽略 sourceProject 的旧 build"); - assertTrue(s.value(SaaStudioNodes.K_FAILURE, String.class).orElse("").contains("B7"), - "failureReason 应明确指向 B7+引擎线 E2 源构建管线待实现"); + "factory 路 deterministic 必须显式失败,绝不静默走会忽略 sourceProject 的旧 build"); + assertTrue(s.value(SaaStudioNodes.K_FAILURE, String.class).orElse("").contains("gamedef"), + "failureReason 应指明 factory 形态无结构化源可确定性点改、须用 saaSourceMode=gamedef(U2 已解锁)"); // ③ 路由:modifyRouter → "fail"(图里 → END,不走 build)。 - assertEquals("fail", SaaStudioNodes.modifyRouter().apply(s), + assertEquals("fail", SaaStudioNodes.modifyRouter("factory").apply(s), "deterministic 路由应为 fail(→END),不走 build"); // 复核:regenerate-module 路不受 P0-4 deterministic guard 影响,仍 → generate(本期可达)。 // 注(U6/B7):regenerate-module 现要求 base 源非空(取 base 源失败 fail-loud,非静默全量重生成),故此处补 K_SOURCE_PROJECT @@ -439,8 +457,8 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, "{\"mode\":\"regenerate-module\",\"target\":{\"id\":\"jump\"},\"payload\":{\"intent\":\"x\"}}", SaaStudioNodes.K_SOURCE_PROJECT, "{\"schemaVersion\":\"1.0\"}")); - applyNode(s2, SaaStudioNodes.modifyNode()); - assertEquals("generate", SaaStudioNodes.modifyRouter().apply(s2)); + applyNode(s2, SaaStudioNodes.modifyNode("factory")); + assertEquals("generate", SaaStudioNodes.modifyRouter("factory").apply(s2)); } /** @@ -457,7 +475,7 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "deterministic", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); JsonNode sp = MAPPER.readTree(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("{}")); assertTrue(sp.path("assets").isArray() && sp.path("assets").size() == 2, "数组长度不变(P1-6:下标不再被当对象键误写、不损坏数组)"); @@ -471,7 +489,7 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "deterministic", SaaStudioNodes.K_MODIFY_PATCH, patchAppend, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s2, SaaStudioNodes.modifyNode()); + applyNode(s2, SaaStudioNodes.modifyNode("factory")); JsonNode sp2 = MAPPER.readTree(s2.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("{}")); assertEquals(3, sp2.path("assets").size(), "/assets/- 应末尾追加一元"); assertEquals("a2", sp2.path("assets").get(2).path("id").asText()); @@ -596,7 +614,7 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); assertEquals("fix", s.value(SaaStudioNodes.K_ROLE, String.class).orElse(""), "regenerate 置 role=fix"); String fb = s.value(SaaStudioNodes.K_FEEDBACK, String.class).orElse(""); @@ -607,7 +625,7 @@ class SaaStudioGraphTopologyTest { assertTrue(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse("").contains("shoot"), "base 源应透传保留(generate 在其基础上重出 jump)"); // 路由 → generate(本期可达)。 - assertEquals("generate", SaaStudioNodes.modifyRouter().apply(s)); + assertEquals("generate", SaaStudioNodes.modifyRouter("factory").apply(s)); } /** @@ -621,13 +639,13 @@ class SaaStudioGraphTopologyTest { OverAllState s = newFullState(Map.of( SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch)); // 不给 K_SOURCE_PROJECT = base 源缺失 - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); assertEquals("failed", s.value(SaaStudioNodes.K_STATUS, String.class).orElse(""), "base 源缺失应 fail-loud(非静默全量重生成)"); assertTrue(s.value(SaaStudioNodes.K_FAILURE, String.class).orElse("").contains("base 源"), "failureReason 应明确指向 base 源缺失"); - assertEquals("fail", SaaStudioNodes.modifyRouter().apply(s), "base 缺 → 路由 fail(→END)"); + assertEquals("fail", SaaStudioNodes.modifyRouter("factory").apply(s), "base 缺 → 路由 fail(→END)"); } /** @@ -643,13 +661,13 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); assertEquals("failed", s.value(SaaStudioNodes.K_STATUS, String.class).orElse(""), "target 指向不存在 behavior 应 fail-loud"); assertTrue(s.value(SaaStudioNodes.K_FAILURE, String.class).orElse("").contains("不存在"), "failureReason 应明确 target behavior 不存在"); - assertEquals("fail", SaaStudioNodes.modifyRouter().apply(s)); + assertEquals("fail", SaaStudioNodes.modifyRouter("factory").apply(s)); } /** @@ -666,10 +684,10 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); // 不误杀:role=fix、路由 generate(target 存在性留 generate 处置)。 assertEquals("fix", s.value(SaaStudioNodes.K_ROLE, String.class).orElse("")); - assertEquals("generate", SaaStudioNodes.modifyRouter().apply(s), + assertEquals("generate", SaaStudioNodes.modifyRouter("factory").apply(s), "无 behaviors 容器(早期源骨架)不误杀,路由 generate"); } @@ -716,7 +734,7 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); assertEquals("failed", s.value(SaaStudioNodes.K_STATUS, String.class).orElse(""), "缺 target 必须 fail-loud(绝不静默全量重生成)"); @@ -724,7 +742,7 @@ class SaaStudioGraphTopologyTest { assertTrue(reason.contains("缺 target") || reason.contains("target"), "failureReason 应说明 regenerate 缺 target,实得=" + reason); // 未进入收窄路:role 不应被置 fix(不进入 generate)。 - assertEquals("fail", SaaStudioNodes.modifyRouter().apply(s), "缺 target → 路由 fail(→END),不可达 generate"); + assertEquals("fail", SaaStudioNodes.modifyRouter("factory").apply(s), "缺 target → 路由 fail(→END),不可达 generate"); } /** @@ -741,10 +759,10 @@ class SaaStudioGraphTopologyTest { SaaStudioNodes.K_MODIFY_MODE, "regenerate-module", SaaStudioNodes.K_MODIFY_PATCH, patch, SaaStudioNodes.K_SOURCE_PROJECT, base)); - applyNode(s, SaaStudioNodes.modifyNode()); + applyNode(s, SaaStudioNodes.modifyNode("factory")); assertEquals("fix", s.value(SaaStudioNodes.K_ROLE, String.class).orElse(""), "target.path 形态应视为有 target(不被缺-target 守卫误杀)"); - assertEquals("generate", SaaStudioNodes.modifyRouter().apply(s)); + assertEquals("generate", SaaStudioNodes.modifyRouter("factory").apply(s)); } /** diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodesRegressionTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodesRegressionTest.java index 95c7835c..a9996455 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodesRegressionTest.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaStudioNodesRegressionTest.java @@ -90,7 +90,7 @@ class SaaStudioNodesRegressionTest { assertEquals("", s.value(SaaStudioNodes.K_PLAYER_FEEDBACK, String.class).orElse("")); // ② validate 节点(硬门)失败 → 写 feedback=校验未过…,并(A-4)清空 pfb(本就已空,幂等)。 - applyNode(s, SaaGenNodes.validateNode()); + applyNode(s, SaaGenNodes.validateNode(null, "factory")); String fb = s.value(SaaStudioNodes.K_FEEDBACK, String.class).orElse(""); assertTrue(fb.contains("校验未过"), "A-4:硬门失败后回喂必须是 validate 失败原因,实得:" + fb); diff --git a/game-runtime/src/host/build-from-source.mjs b/game-runtime/src/host/build-from-source.mjs index 0df75c22..2726d855 100644 --- a/game-runtime/src/host/build-from-source.mjs +++ b/game-runtime/src/host/build-from-source.mjs @@ -210,3 +210,50 @@ export default function createGame(opts) { } export default assembleFactoryFromSource; + +/* ════════════════════════════════════════════════════════════════════════════ + * CLI(U2 Java 接缝调用点)——SaaGenNodes.validateNode(gamedef) 经 runProcess 子进程调本 CLI。 + * node build-from-source.mjs 校验+装配+写 generated-factory.js(失败 exit 2 + stderr 逐条错误) + * node build-from-source.mjs --validate 只校验(失败 exit 2 + stderr 错误) + * 被 import 时不执行(保单测/装配器作纯函数用);仅作脚本直跑时执行 main。 + * ════════════════════════════════════════════════════════════════════════════ */ +async function cliMain(argv) { + const { readFileSync, writeFileSync } = await import('node:fs'); + const args = argv.slice(2); + const validateOnly = args[0] === '--validate'; + const files = validateOnly ? args.slice(1) : args; + const srcFile = files[0]; + const outFile = files[1]; + if (!srcFile) { + process.stderr.write('usage: build-from-source.mjs [--validate] [outFactory]\n'); + process.exit(64); + } + let sp; + try { + sp = JSON.parse(readFileSync(srcFile, 'utf8')); + } catch (e) { + process.stderr.write('source JSON 解析失败: ' + e.message + '\n'); + process.exit(2); + } + if (validateOnly) { + const v = validateSourceProject(sp); + if (!v.ok) { process.stderr.write(v.errors.join('\n') + '\n'); process.exit(2); } + process.exit(0); + } + try { + const factory = assembleFactoryFromSource(sp); + if (outFile) { writeFileSync(outFile, factory, 'utf8'); } else { process.stdout.write(factory); } + process.exit(0); + } catch (err) { + process.stderr.write((err.validationErrors ? err.validationErrors.join('\n') : err.message) + '\n'); + process.exit(2); + } +} + +// 仅作为脚本直接运行时执行(import 时 process.argv[1] 非本文件 → 跳过)。 +if (typeof process !== 'undefined' && process.argv && process.argv[1]) { + const here = new URL(import.meta.url).pathname; + if (process.argv[1] === here || process.argv[1].endsWith('/build-from-source.mjs')) { + cliMain(process.argv); + } +}