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 ff07a0ec..a0a315b4 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
@@ -538,6 +538,15 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
// player:K_PLAYER_PANEL(player 软门评判,诊断用);缺/非法则省略。
putPlayerIfPresent(trace, s);
+ // ── U3/B5 救场阶梯扩展键(additive,与 worker _extract_trace 同口径;缺则省略键,存量 trace_json 字节兼容)──
+ // 严格逐键 putIfPresent:modelTier 仅「真升档态」(stage2 或 escalationEvents 非空)才落,默认 stage1 省略
+ // (P0-1:保「无扩展键时键集与扩前一致」);K_ESCALATION_EVENTS/K_GIVEUP_DUMP_PATH 未发生救场时缺 → 省略。
+ // 任一键解析失败不连坐(外层 try 已兜底)。
+ putEscalationKeysIfPresent(trace, s);
+ // cacheHit:SAA 路 callAndRecord 现只读 prompt/completion token(Spring AI Usage 接口无 cache 字段),
+ // 无命中数据源 → 不伪造、整段省略(与 cost 省略同因:没有就不挂,避免 D11/审核台误读)。
+ // SAA 缓存命中捕获 = follow-up(须改 callAndRecord 读 native usage + 加 token state 键,超本单元范围)。
+
return trace.isEmpty() ? null : trace;
} catch (Exception e) {
// 命门:抽取异常一律吞 + warn,返 null(→ reqVO.trace=null → persistTraceQuietly 守卫旁路 → trace_json 留 NULL),
@@ -692,6 +701,46 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
}
}
+ /**
+ * U3/B5 救场阶梯扩展键 additive 抽取(modelTier / escalationEvents / giveupDumpPath)。
+ *
+ *
口径对齐 worker {@code _extract_trace}(camelCase 键名一致;worker 同段亦逐键 present-then-put):
+ *
+ * - {@code modelTier}:{@link SaaStudioNodes#K_MODEL_TIER}(render 初 stage1、escalate 升 stage2)。仅「真升档态」才塞
+ * ——{@code modelTier=="stage2"} 或 escalationEvents 非空数组时落;未升档(默认 stage1、无救场事件)→ 省略该键
+ * (P0-1 修复:render 默认落 stage1 是初值噪声,正常路落它会破「无扩展键时键集与扩前一致」的字节兼容;与 worker 同口径)。
+ * - {@code escalationEvents}:{@link SaaStudioNodes#K_ESCALATION_EVENTS} JSON 数组串(escalate append
+ * {tierBefore,tierAfter,atFailCount,ts})。解析为非空数组则原样塞;未升档(缺/空 "[]"/非数组)→ 省略键。
+ * - {@code giveupDumpPath}:{@link SaaStudioNodes#K_GIVEUP_DUMP_PATH}(giveup 节点产;U5 真落盘)。非空则塞;
+ * 未放弃(缺)→ 省略。
+ *
+ *
+ * 字节兼容:全 additive——未升档/未救场时三键全省略(modelTier 默认 stage1 不再落),
+ * 存量及正常路 trace_json 键集与扩前一致;{@code ReadinessScorer} 不读这三键(仅诊断/成本台账展示),两路同读不破。
+ * 单键解析失败不连坐(escalationEvents 脏 JSON → 该键省略,modelTier/giveupDumpPath 照常)。
+ */
+ private static void putEscalationKeysIfPresent(Map trace, OverAllState s) {
+ // escalationEvents:JSON 数组串 → 非空数组则原样塞(List);缺/空/非数组/脏 JSON 则省略键(单键失败不连坐)。
+ // 先解析它,因为「是否真升档」要据它(或 stage2)判定,决定 modelTier 是否落。
+ JsonNode events = parseTrace(s.value(SaaStudioNodes.K_ESCALATION_EVENTS, String.class).orElse(""));
+ boolean hasEvents = events != null && events.isArray() && events.size() > 0;
+ if (hasEvents) {
+ trace.put("escalationEvents", TRACE_MAPPER.convertValue(events, java.util.List.class));
+ }
+ // modelTier:终态模型档(stage1|stage2)。仅「真升档态」才塞——stage2 或 escalationEvents 非空;
+ // 默认 stage1(未升档,render 初值噪声)则省略(P0-1:保「无扩展键时键集与扩前一致」字节兼容)。
+ String modelTier = s.value(SaaStudioNodes.K_MODEL_TIER, String.class).filter(v -> !v.isBlank()).orElse(null);
+ boolean escalated = "stage2".equals(modelTier) || hasEvents;
+ if (modelTier != null && escalated) {
+ trace.put("modelTier", modelTier);
+ }
+ // giveupDumpPath:放弃 dump 落盘路径。非空才塞;未放弃(缺)则省略。
+ String giveupPath = s.value(SaaStudioNodes.K_GIVEUP_DUMP_PATH, String.class).filter(v -> !v.isBlank()).orElse(null);
+ if (giveupPath != null) {
+ trace.put("giveupDumpPath", giveupPath);
+ }
+ }
+
/** 兜底失败回调(图执行线程异常时调;handleCallback 再异常也只留痕不抛,避免拖垮后台线程)。 */
private void safeFailedCallback(Map job, String traceId, String cause) {
try {
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcherTraceTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcherTraceTest.java
index 47068d26..518f9f00 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcherTraceTest.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGraphDispatcherTraceTest.java
@@ -337,4 +337,127 @@ class SaaGraphDispatcherTraceTest {
Integer score = new ReadinessScorer().score(trace);
assertNotNull(score, "cost 省略不应让算分报错(efficiency 中性 0.5)");
}
+
+ // ============================== 用例8:U3/B5 救场阶梯扩展键 additive + 字节兼容 ==============================
+
+ /**
+ * U3/B5 字节兼容:succeededStub 未设 救场阶梯键(K_MODEL_TIER/K_ESCALATION_EVENTS/K_GIVEUP_DUMP_PATH,
+ * 均无初值)→ 抽出 trace 不含 modelTier/escalationEvents/giveupDumpPath/cacheHit 四键。
+ * 守门:未升档/未救场的存量回放,trace_json 键集与扩前一致(additive 不破存量)。
+ */
+ @Test
+ void extract_noEscalationKeys_omitsExtensionKeys() throws Exception {
+ Map trace = invokeExtract(succeededStub(), 8000L);
+ assertFalse(trace.containsKey("modelTier"), "未设 K_MODEL_TIER → modelTier 省略(字节兼容)");
+ assertFalse(trace.containsKey("escalationEvents"), "未升档 → escalationEvents 省略(字节兼容)");
+ assertFalse(trace.containsKey("giveupDumpPath"), "未放弃 → giveupDumpPath 省略(字节兼容)");
+ // SAA 路无缓存命中数据源(callAndRecord 只读 prompt/completion token)→ cacheHit 整段省略,不伪造。
+ assertFalse(trace.containsKey("cacheHit"), "SAA 路无命中数据源 → cacheHit 省略(不伪造)");
+ }
+
+ /**
+ * P0-1 字节兼容(核心守门):render 节点默认落 {@code K_MODEL_TIER="stage1"}(初值,未升档)且无救场事件
+ * → 抽出 trace 不含 modelTier 键。
+ *
+ * 守门:正常未升档路(render 已落 stage1 初值)的 trace_json 键集必须与扩前一致——modelTier=stage1 是
+ * 初值噪声,落它会平白给每条正常 trace 多一个 modelTier 键,破「无扩展键时键集与扩前一致」。本用例坐实
+ * 「默认 stage1 → modelTier 省略」(仅 stage2 或 escalationEvents 非空才落),与 worker {@code _extract_trace} 同口径。
+ */
+ @Test
+ void extract_renderDefaultStage1_omitsModelTier() throws Exception {
+ // 模拟 render 节点真实初态:K_MODEL_TIER="stage1"(render out.put 落的初值),无救场事件/无放弃。
+ Map init = new HashMap<>();
+ init.put(SaaStudioNodes.K_STATUS, "succeeded");
+ init.put(SaaStudioNodes.K_REPAIR_COUNT, 0);
+ init.put(SaaStudioNodes.K_GAME_ID, "saa-render-default");
+ init.put(SaaStudioNodes.K_MODEL_TIER, "stage1"); // render 默认初值(未升档)
+ OverAllState s = newStubState(init);
+
+ Map trace = invokeExtract(s, 5000L);
+ assertNotNull(trace);
+ assertFalse(trace.containsKey("modelTier"),
+ "render 默认 stage1(未升档)→ modelTier 省略(字节兼容:键集与扩前一致)");
+ assertFalse(trace.containsKey("escalationEvents"), "无救场事件 → escalationEvents 省略");
+ assertFalse(trace.containsKey("giveupDumpPath"), "未放弃 → giveupDumpPath 省略");
+ }
+
+ /**
+ * P0-1 边界:stage1 但 escalationEvents 非空(极端态:事件已 append 但 modelTier 尚未翻 stage2)
+ * → 仍判为「真升档态」,modelTier=stage1 也落(与 escalationEvents 同读,诚实反映轨迹)。
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ void extract_stage1WithEvents_keepsModelTier() throws Exception {
+ Map init = new HashMap<>();
+ init.put(SaaStudioNodes.K_STATUS, "failed");
+ init.put(SaaStudioNodes.K_REPAIR_COUNT, 5);
+ init.put(SaaStudioNodes.K_MODEL_TIER, "stage1"); // 尚未翻档,但已有救场事件
+ init.put(SaaStudioNodes.K_ESCALATION_EVENTS,
+ "[{\"tierBefore\":\"stage1\",\"tierAfter\":\"stage2\",\"atFailCount\":5,\"ts\":\"2026-06-17T00:00:00Z\"}]");
+ OverAllState s = newStubState(init);
+
+ Map trace = invokeExtract(s, 6000L);
+ assertEquals("stage1", trace.get("modelTier"), "escalationEvents 非空 → 判真升档态,modelTier 照落");
+ List