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);
+ }
+}