fix(saa): 包级 assets 接通源 sourceProject.assets[]——闭合产消断链(兑现 engine-E2/E3 消费侧 TODO)

gen-done 出包历史上把包级 assets 硬置空(setAssets(emptyList)),丢掉源
gameDefinition 已落库的六类源资产(game_source_project.source_json 顶层
assets[6]),造成产(asset 节点写源)→消(组包)断链。

改 SaaGraphDispatcher.buildCallbackReqVO(in-JVM SAA 路,即 generic/engine
任务真实落包路):新增 extractSourceAssets(s,traceId) 从图终态 K_SOURCE_PROJECT
抽顶层 assets[] 忠实映射进 reqVO.assets;DifyCallbackTxService.buildGamePackage
读 reqVO.getAssets() 即自然带进 GamePackage.assets。

- 形态:源 assetSpec={id,category,ref,provider}(source-project.schema.json
  #/$defs/assetSpec),运行时 gd-runtime 经 gdef.assets 同形消费;geom:<cat> ref
  系几何占位资产引用,原样透传,不伪造包契约 strict AssetSpec 的
  type/url/hash/bytes/mime(无真图/真 sha256,伪造即脏数据)。
- best-effort 非阻断(严格 additive):源缺失/空白/非 JSON/无 assets 键/非数组/
  异常一律回退空集,绝不外抛、绝不打断 SAA 生成;存量无源回调字节零变化。
- 复用既有 parseTrace + TRACE_MAPPER(只读,无共享可变状态),不引新依赖。

不动 AigcGenerateExecutor:440:该处为进程内 clicker/merge 旧路(generic/engine
经 dispatchGeneric 早返,不达此行;worker 回调由独立 reqVO 驱动),作用域无
sourceProject,改它即凭空伪造,故按现状保留(对齐 run_batch.py assets:[])。

验证:mvn -o clean install -pl game-module-aigc-server -am 编译 BUILD SUCCESS;
DifyCallbackServiceImplTest+AigcGenerateExecutorTest 41 tests 0 fail(含
in-process 路 getAssets().isEmpty() 断言仍过,确认未回归)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-21 10:50:40 +08:00
parent 9d463f0873
commit 490dc6293d

View File

@ -609,7 +609,14 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
DifyCallbackReqVO reqVO = new DifyCallbackReqVO(); DifyCallbackReqVO reqVO = new DifyCallbackReqVO();
reqVO.setTraceId(traceId); reqVO.setTraceId(traceId);
reqVO.setTemplateId(templateId); reqVO.setTemplateId(templateId);
reqVO.setAssets(java.util.Collections.emptyList()); // ── U2/B8 消费侧(§5.2 产消接通):忠实取已落库源 assets[](六类源资产)填充包级 assets,替代历史硬置空。 ──
// 兑现 SaaStudioNodes 处自标的「engine-E2/E3 消费侧本期不做」TODO:源 asset 节点已产 assets[6]
// (schemaVersion+gameDefinition+顶层 assets,落 game_source_project.source_json),此处把同批源 assets
// 忠实带进 GamePackage.assets(DifyCallbackTxService.buildGamePackage 读 reqVO.getAssets()),闭合产消断链。
// 形态注记:源 assetSpec={id,category,ref,provider}(source-project.schema.json #/$defs/assetSpec,运行时
// gd-runtime 经 gdef.assets 同形消费);geom:<cat> ref 为几何占位资产引用,原样透传不伪造 type/hash/bytes/mime。
// 空/缺/非数组源 assets → 回退空集(不抛):严格非破坏,存量无源回调字节零变。
reqVO.setAssets(extractSourceAssets(s, traceId));
// gameConfig 最小合法占位(engine 路游戏逻辑全在 engineBundle,gameConfig 仍不可空:GamePackage required)。 // gameConfig 最小合法占位(engine 路游戏逻辑全在 engineBundle,gameConfig 仍不可空:GamePackage required)。
Map<String, Object> gameConfig = new LinkedHashMap<>(); Map<String, Object> gameConfig = new LinkedHashMap<>();
gameConfig.put("templateId", templateId); // 须与包级 templateId 一致(一致性校验) gameConfig.put("templateId", templateId); // 须与包级 templateId 一致(一致性校验)
@ -763,6 +770,55 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
} }
} }
/**
* U2/B8 消费侧(§5.2 产消接通):从图终态 {@code K_SOURCE_PROJECT} 抽顶层 {@code assets[]} 映射为包级 assets。
*
* <p><b>兑现 TODO</b>:源 asset 节点已把六类源资产写入 {@code sourceProject} 顶层 {@code assets}
* ({@code source-project.schema.json #/$defs/assetSpec},形态 {@code {id,category,ref,provider}}),历史上组包侧
* 硬置空集导致产消断链;本方法把<b>同一批</b>源 assets 忠实搬进 {@link DifyCallbackReqVO#getAssets()},
* 由 {@code DifyCallbackTxService.buildGamePackage} 落进 {@code GamePackage.assets}。运行时 {@code gd-runtime}
* 经 {@code gdef.assets}(同 source assetSpec 形态)消费,故<b>原样透传不重塑</b>:{@code geom:<cat>} 为几何占位
* 资产引用,不伪造包契约 strict AssetSpec 的 {@code type/url/hash/bytes/mime}(无真图、无真 sha256,伪造即脏数据)。
*
* <p><b>best-effort 非阻断(严格 additive)</b>:源缺失/空白/非 JSON/无 assets 键/assets 非数组/任意异常 →
* 一律返回<b>空集</b>(绝不外抛、绝不打断 SAA 生成),与历史「恒空集」对存量无源回调字节零变化。
*
* @param s 图终态(可能 null)
* @param traceId 贯穿键(仅日志)
* @return 源 assets 映射出的 {@code List<Map<String,Object>>}(忠实透传字段);无/异常时空集(非 null)
*/
private static java.util.List<Map<String, Object>> extractSourceAssets(OverAllState s, String traceId) {
if (s == null) {
return java.util.Collections.emptyList();
}
try {
// 复用 best-effort 宽松解析(null/空白/非法 JSON → null);源串即 §5.2 落库的 source_json。
JsonNode root = parseTrace(s.value(SaaStudioNodes.K_SOURCE_PROJECT, String.class).orElse(""));
if (root == null) {
return java.util.Collections.emptyList();
}
JsonNode assets = root.path("assets");
if (!assets.isArray() || assets.isEmpty()) {
// 无 assets 键 / 非数组 / 空数组 → 回退空集(不抛,存量无源回调字节零变)。
return java.util.Collections.emptyList();
}
// 逐项忠实映射为 Map(保 {id,category,ref,provider} 等全部源字段;geom: ref 原样);非对象项跳过(防御脏数据)。
java.util.List<Map<String, Object>> out = new java.util.ArrayList<>(assets.size());
for (JsonNode a : assets) {
if (a != null && a.isObject()) {
out.add(TRACE_MAPPER.convertValue(a, new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {}));
}
}
// 可追溯日志:接通了几个源 assets(0 表示源里确无对象项,等价历史空集,不告警)。
log.info("[saa-dispatch] 包级 assets 接通源 assets[] 共 {} 个(产消接通,兑现 engine-E2/E3 消费侧 TODO)traceId={}", out.size(), traceId);
return out;
} catch (Exception e) {
// 命门:抽取/映射异常一律吞 + warn 返空集,绝不打断 SAA 生成(生成成败由 status/engineBundle 既有逻辑决定)。
log.warn("[saa-dispatch] 源 assets 抽取失败,包级 assets 回退空集(不阻断生成)traceId={}", traceId, e);
return java.util.Collections.emptyList();
}
}
/** /**
* 派生 models 并集:{@code K_TOKENS_BY_MODEL} 的 model 名键集 ∪ {@code K_ATTEMPTS[].{role:model}},组同形 {role:model} map。 * 派生 models 并集:{@code K_TOKENS_BY_MODEL} 的 model 名键集 ∪ {@code K_ATTEMPTS[].{role:model}},组同形 {role:model} map。
* *