feat(amodel-gen): Phase 2 批B — SAA 图 amodel 路集成(集成方式C)+ Fix A(validate 尊重 harness ok)
集成方式C:SAA generate 节点 shell-out 已验证 harness `gen.mjs --mode saa`(amodel flag 旁挂)。 - AigcExecutorProperties: saaSourceMode += amodel(Javadoc;默认仍 factory)。 - SaaGenNodes: 新增 runProcessSplit/SplitProcResult(stdout/stderr 分流,末行 JSON 不被 stderr 污染);validateNode amodel 分支 + validateAmodelStaged。 - SaaStudioNodes: generateNode 6 参重载(amodel 短路 generateViaAmodelHarness:argv 透传 brief+key、解析末行 JSON);okElseRepairOrEscalateOrGiveupAmodelAware(validate-ok→play 短路)。 - SaaStudioGraph: assemble/build apiKey/llmBase 透传链 + validate 条件边 amodel-aware + "play"边。 - SaaGraphDispatcher: ensureGraph 传 apiKey/llmBase。 Fix A(本次发现并修的真集成 bug):gen.mjs done-gate 顺序 stage→smoke,smoke 失败 done 被拒但 stage 已落盘陈旧坏 bundle(harness 返 ok:false/stagedDir:null 却留文件)。原 validateAmodelStaged 只查产物存在=误过坏 bundle→路由 play→九门崩+白耗~10min。 修:generate 置 K_AMODEL_GEN_OK(成功 true/失败 false,不再自增 failCount);validate 先尊重该标记(false→救场,不读陈旧产物),failCount 由 validate 统一计一次(与 factory/gamedef「generate 产出、validate 计数」同口径)。 验证(严格): - mini 权威 mvn test-compile BUILD SUCCESS(main+test)。 - 83/83 SAA 确定性回归单测全绿(门路由18/拓扑30/节点回归14/history重载4/dispatcher9 等)——factory/gamedef 零回归。 - amodel e2e dispatch→generate→validate→路由 实跑通;控制测试 pristine _template scaffold-saa→build→stage→smoke PASS(env/信封/插件正常)。 - Fix A 行为确认:修后坏 bundle 被 validate 正确拒(未到 play);修前 breakout 坏 bundle 到了 play。 未达(解耦·非本集成范围):amodel e2e 仍 0/1——M3 生成质量 boot 崩 `Cannot read properties of undefined (reading 'w')`,跨 breakout+whack 复现;pristine 模板 boot 干净→证 M3 代码问题,非集成/env/harness。属 Phase 3/4 生成质量门(D1=A:factory 默认直到 amodel≥60%)。 零回归:amodel 纯旁挂 flag,saaSourceMode 默认 factory;amodel=false 时全部新分支跳过,新"play"边对 factory/gamedef 不可达。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cd2903baab
commit
3fbb3197f4
@ -131,6 +131,106 @@ final class SaaGenNodes {
|
||||
return new ProcResult(!done, done ? p.exitValue() : -1, output.trim());
|
||||
}
|
||||
|
||||
// ====================== 子进程运行器(stdout/stderr 分流版,amodel 路专用) ======================
|
||||
|
||||
/**
|
||||
* 分流子进程运行结果({@link #runProcessSplit},amodel 路专用):stdout / stderr <b>不合流</b>,分别保留。
|
||||
* <p>amodel 的 node harness(gen.mjs --mode saa)契约是「stdout <b>末行</b>为单行 JSON 结果」(其它日志走 stderr/前序 stdout)。
|
||||
* 若沿用 {@link #runProcess} 的 {@code redirectErrorStream(true)} 合流,stderr 日志会与 stdout 末行交织、污染 JSON 解析。
|
||||
* 故本版双守护线程各自 drain stdout/stderr 到独立 buffer(镜像 {@link #runProcess} 的并发 drain + 超时强杀范式)。
|
||||
*/
|
||||
static final class SplitProcResult {
|
||||
final boolean timedOut; // true=超时被强杀(未在限时内退出)
|
||||
final int exitCode; // 进程退出码(timedOut=true 时无意义,置 -1)
|
||||
final String stdout; // 标准输出(不含 stderr)
|
||||
final String stderr; // 标准错误(独立)
|
||||
|
||||
SplitProcResult(boolean timedOut, int exitCode, String stdout, String stderr) {
|
||||
this.timedOut = timedOut;
|
||||
this.exitCode = exitCode;
|
||||
this.stdout = stdout;
|
||||
this.stderr = stderr;
|
||||
}
|
||||
|
||||
/** 取 stdout 末行非空行(amodel harness 契约:末行为单行 JSON 结果)。无则返空串。 */
|
||||
String lastStdoutLine() {
|
||||
if (stdout == null || stdout.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String[] lines = stdout.split("\\r?\\n");
|
||||
for (int i = lines.length - 1; i >= 0; i--) {
|
||||
String ln = lines[i].trim();
|
||||
if (!ln.isEmpty()) {
|
||||
return ln;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分流子进程运行器(amodel 路专用):stdout / stderr <b>不合流</b>(不调 redirectErrorStream),双守护线程各 drain
|
||||
* 一路防管道死锁,限时 {@code waitFor},超时 destroy→destroyForcibly→二次 waitFor 兜底强杀(镜像 {@link #runProcess} 范式)。
|
||||
*
|
||||
* @param pb 已配好 command/directory 的 ProcessBuilder(本方法<b>不</b> redirectErrorStream)。
|
||||
* @param timeoutSecs 超时秒数。
|
||||
* @return SplitProcResult(timedOut / exitCode / stdout / stderr 分流)。
|
||||
* @throws IOException 进程启动失败。
|
||||
* @throws InterruptedException 等待被中断(向上抛,由调用方按节点语义处理)。
|
||||
*/
|
||||
static SplitProcResult runProcessSplit(ProcessBuilder pb, long timeoutSecs) throws IOException, InterruptedException {
|
||||
// 不调 redirectErrorStream:stdout/stderr 分流(amodel harness 末行 JSON 不被 stderr 日志污染)。
|
||||
Process p = pb.start();
|
||||
ByteArrayOutputStream outBuf = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream errBuf = new ByteArrayOutputStream();
|
||||
// 双并发 drain:各自把一路输出抽干到独立 buffer,确保子进程写任一管道永不阻塞(修复 readAll/waitFor 死锁)。
|
||||
Thread outDrain = drainTo(p.getInputStream(), outBuf, "saa-proc-drain-out");
|
||||
Thread errDrain = drainTo(p.getErrorStream(), errBuf, "saa-proc-drain-err");
|
||||
outDrain.start();
|
||||
errDrain.start();
|
||||
|
||||
boolean done = p.waitFor(timeoutSecs, TimeUnit.SECONDS);
|
||||
if (!done) {
|
||||
// 超时:先温和 destroy,再强杀,二次短等确保回收(与 runProcess 同款)。
|
||||
p.destroy();
|
||||
if (!p.waitFor(2, TimeUnit.SECONDS)) {
|
||||
p.destroyForcibly();
|
||||
p.waitFor(2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
// 等两路 drain 线程收尾(各最多 2s),取出已抽到的输出。
|
||||
outDrain.join(2000);
|
||||
errDrain.join(2000);
|
||||
String stdout;
|
||||
synchronized (outBuf) {
|
||||
stdout = outBuf.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
String stderr;
|
||||
synchronized (errBuf) {
|
||||
stderr = errBuf.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
return new SplitProcResult(!done, done ? p.exitValue() : -1, stdout.trim(), stderr.trim());
|
||||
}
|
||||
|
||||
/** 建一个守护 drain 线程:把 {@code in} 抽干到 {@code buf}(同步写,防并发交织)。供 {@link #runProcessSplit} 双路各起一条。 */
|
||||
private static Thread drainTo(InputStream in, ByteArrayOutputStream buf, String name) {
|
||||
Thread t = new Thread(() -> {
|
||||
try (InputStream src = in) {
|
||||
byte[] chunk = new byte[8192];
|
||||
int n;
|
||||
while ((n = src.read(chunk)) != -1) {
|
||||
synchronized (buf) {
|
||||
buf.write(chunk, 0, n);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignore) {
|
||||
// 进程结束/管道关闭引发的读异常可忽略(已尽量抽干)。
|
||||
}
|
||||
}, name);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
|
||||
// ====================== ① validate 节点 ======================
|
||||
// 静态契约扫描(抄 validate.py:55-77 语义)+ node --check ESM 门(抄 validate.py:80-100)。
|
||||
|
||||
@ -243,6 +343,11 @@ final class SaaGenNodes {
|
||||
*/
|
||||
static NodeAction validateNode(Path gameRuntimeRoot, String sourceMode) {
|
||||
return (OverAllState s) -> {
|
||||
// ── amodel 路(集成方式 C):generate 已 shell-out harness 自跑 scaffold/build/stage/smoke;
|
||||
// validate 只轻断言 staged 产物存在 + 含 __GameBundle(不要求 source-project schema 工件,留 follow-up)。
|
||||
if ("amodel".equals(sourceMode)) {
|
||||
return validateAmodelStaged(s, gameRuntimeRoot);
|
||||
}
|
||||
if ("gamedef".equals(sourceMode)) {
|
||||
return validateGameDef(s, gameRuntimeRoot);
|
||||
}
|
||||
@ -306,6 +411,47 @@ final class SaaGenNodes {
|
||||
return validateOut(s, errs);
|
||||
}
|
||||
|
||||
/**
|
||||
* amodel 路 validate(集成方式 C·精简):harness(gen.mjs --mode saa)已在子进程内 scaffold/build/stage/smoke-boot 完毕。
|
||||
* 两步门:<b>①先尊重 generate 的 harness ok 信号</b>({@link SaaStudioNodes#K_AMODEL_GEN_OK});
|
||||
* <b>②harness 成功才做产物轻断言</b>——staged {@code games/_wg1-gen/<gameId>/bundle.iife.js} 存在且文本含 {@code __GameBundle}。
|
||||
* 全过 → validateOk:true(图侧路由短路 scaffold/asset/build 直奔 play);任一不过 → errs 回喂 repair(复用 {@link #validateOut},计 failCount 一次)。
|
||||
*
|
||||
* <p><b>为何①不可省(2026-06-22 验收门修)</b>:gen.mjs done-gate 顺序是 stage→smoke,smoke 失败 done 被拒但 <b>stage 已落盘陈旧坏 bundle</b>
|
||||
* (harness 返 ok:false/stagedDir:null 却违约留文件)。若只做②(查产物存在)会误过该坏 bundle → 路由 play → 九门 A_boot 必崩 + 白耗一次约 10min 真玩。
|
||||
* 实证:e2e「打砖块」harness ok:false 但 staged 坏 bundle 被②放行→九门 A_boot「reading 'w'」崩(harness 自身 smoke 同款崩,证非误判)。
|
||||
*
|
||||
* <p><b>本期边界(follow-up)</b>:不要求 source-project schema 工件(A-model 终态是 src/ 多文件工程,非结构化 JSON 源);
|
||||
* 该校验留 follow-up,与 plan 决策 3「validate amodel:精」一致。
|
||||
*/
|
||||
private static Map<String, Object> validateAmodelStaged(OverAllState s, Path gameRuntimeRoot) {
|
||||
String gameId = s.value(SaaStudioNodes.K_GAME_ID, String.class).orElse("");
|
||||
List<String> errs = new ArrayList<>();
|
||||
// ① 先尊重 generate 的 harness ok 信号:harness ok=false 时可能留陈旧坏 bundle(gen.mjs stage→smoke,smoke 失败仍落了盘),
|
||||
// 只查产物存在会误过;故 generate 失败直接判 fail→救场(不读陈旧产物),failCount 经 validateOut 统一计一次。
|
||||
boolean genOk = s.value(SaaStudioNodes.K_AMODEL_GEN_OK, Boolean.class).orElse(false);
|
||||
if (!genOk) {
|
||||
String fb = s.value(SaaStudioNodes.K_FEEDBACK, String.class).orElse("amodel harness 生成失败(ok=false)");
|
||||
errs.add("amodel generate 未合格(harness ok=false,跳过 staged 产物校验直接救场):" + fb);
|
||||
return validateOut(s, errs);
|
||||
}
|
||||
// ② harness 成功:做产物轻断言。staged 产物路径与 harness 落点对齐:games/_wg1-gen/<gameId>/bundle.iife.js(gen.mjs stage 后的信封产物)。
|
||||
Path bundle = gameRuntimeRoot.resolve("games").resolve("_wg1-gen").resolve(gameId).resolve("bundle.iife.js");
|
||||
if (!Files.exists(bundle)) {
|
||||
errs.add("amodel staged 产物缺失:未见 " + bundle + "(harness 未成功 stage bundle.iife.js)");
|
||||
return validateOut(s, errs);
|
||||
}
|
||||
try {
|
||||
String text = new String(Files.readAllBytes(bundle), StandardCharsets.UTF_8);
|
||||
if (!text.contains(GLOBAL_NAME)) {
|
||||
errs.add("amodel staged 产物不含全局名 " + GLOBAL_NAME + "(疑似信封未注入/产物损坏)");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
errs.add("amodel staged 产物读取失败:" + e.getMessage());
|
||||
}
|
||||
return validateOut(s, errs);
|
||||
}
|
||||
|
||||
/** 统一 validate 输出(成功=validateOk:true;失败=feedback + 清 player 软门 + 计 failCount)。 */
|
||||
private static Map<String, Object> validateOut(OverAllState s, List<String> errs) {
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
|
||||
@ -414,11 +414,13 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
}
|
||||
// 固定架构 B2:透传救场阶梯 stage2 额外救场轮(saaStage2ExtraRepairs),走 factory-based build 重载(recursionLimit 按 5+3 重算)。
|
||||
// Plan A/U2:historyEnabled=anthropic——anthropic 路 generate 跨轮保留 thinking 历史 + 连续对话救场;openai 路关(字节零变)。
|
||||
// amodel 路:透传 apiKey/llmBase 给 generate 节点(落 shell-out harness 子进程 env NEWAPI_KEY/NEWAPI_BASE_URL);factory/gamedef 路忽略。
|
||||
compiledGraph = SaaStudioGraph.build(mf, models, gameRuntimeRoot,
|
||||
properties.getSaaMaxRepairs(), properties.getSaaMaxPlayerRounds(),
|
||||
properties.getSaaStage2ExtraRepairs(),
|
||||
properties.getSaaSourceMode(), // plan U3:factory(默认,iife) | gamedef(真结构化源)
|
||||
properties.getSaaSourceMode(), // plan U3:factory(默认,iife) | gamedef(真结构化源) | amodel(shell-out harness)
|
||||
anthropic, // plan U2:historyEnabled(仅 anthropic 路开连续对话历史+thinking 保留)
|
||||
properties.getApiKey(), properties.getLlmBase(), // amodel 路 harness 用(new-api 密钥/host 根;factory/gamedef 忽略)
|
||||
saverConfig, observationRegistry);
|
||||
log.info("[saa-dispatch] SAA 图构造完成(缓存复用):protocol={}, maxRepairs={}, stage2Extra={}, maxPlayerRounds={}, "
|
||||
+ "upstream={}, checkpoint={}, observation={}",
|
||||
|
||||
@ -424,7 +424,8 @@ public final class SaaStudioGraph {
|
||||
*/
|
||||
private static StateGraph assemble(RoleModelFactory mf, Models models, Path gameRuntimeRoot,
|
||||
int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs,
|
||||
String sourceMode, boolean historyEnabled)
|
||||
String sourceMode, boolean historyEnabled,
|
||||
String apiKey, String llmBase)
|
||||
throws GraphStateException {
|
||||
// == per-role 采样面(temperature/maxTokens)逐字对齐 Python(经 mf 工厂建 ChatModel:openai 默认路字节等价;anthropic flag 旁挂)==
|
||||
// design:Python config.build_model 不传 temperature → 服务端默认 → temperature=null(不下发);
|
||||
@ -465,7 +466,8 @@ public final class SaaStudioGraph {
|
||||
codeFallbackModel, fixFallbackModel,
|
||||
models.code(), models.fix(), models.codeStage2(), models.fixStage2(),
|
||||
models.codeFallback(), models.fixFallback()),
|
||||
gameRuntimeRoot, sourceMode, historyEnabled))) // U2:anthropic 路 history 开(连续对话救场);openai 路关(字节零变)
|
||||
gameRuntimeRoot, sourceMode, historyEnabled,
|
||||
apiKey, llmBase))) // U2:anthropic 路 history 开(连续对话救场);openai 路关(字节零变)。apiKey/llmBase 仅 amodel 路 shell-out harness 用
|
||||
.addNode("validate", node_async(SaaGenNodes.validateNode(gameRuntimeRoot, sourceMode)))
|
||||
.addNode("scaffold", node_async(SaaGenNodes.scaffoldNode(gameRuntimeRoot)))
|
||||
.addNode("asset", node_async(SaaStudioNodes.assetNode()))
|
||||
@ -497,10 +499,13 @@ public final class SaaStudioGraph {
|
||||
edge_async(SaaStudioNodes.renderRouter()),
|
||||
Map.of("classify", "classify", "modify", "modify"))
|
||||
// validate 失败回边:救场阶梯(ok→scaffold / repair / escalate / giveup)。
|
||||
// amodel-aware:amodel 路 harness 已自跑 scaffold/build/stage/smoke → validate-ok 短路到 play(跳 scaffold/asset/build);
|
||||
// factory/gamedef 路 ok 仍→scaffold(运行时零变,"play" 边对其不可达)。
|
||||
.addConditionalEdges("validate",
|
||||
edge_async(SaaStudioNodes.okElseRepairOrEscalateOrGiveup(
|
||||
SaaStudioNodes.K_VALIDATE_OK, maxRepairs, stage2ExtraRepairs)),
|
||||
Map.of("ok", "scaffold", "repair", "repair", "escalate", "escalate", "giveup", "giveup"))
|
||||
edge_async(SaaStudioNodes.okElseRepairOrEscalateOrGiveupAmodelAware(
|
||||
SaaStudioNodes.K_VALIDATE_OK, maxRepairs, stage2ExtraRepairs, sourceMode)),
|
||||
Map.of("ok", "scaffold", "play", "play",
|
||||
"repair", "repair", "escalate", "escalate", "giveup", "giveup"))
|
||||
// build 失败回边:救场阶梯(ok→play / repair / escalate / giveup)。
|
||||
.addConditionalEdges("build",
|
||||
edge_async(SaaStudioNodes.okElseRepairOrEscalateOrGiveup(
|
||||
@ -675,8 +680,29 @@ public final class SaaStudioGraph {
|
||||
String sourceMode, boolean historyEnabled,
|
||||
SaverConfig saverConfig, ObservationRegistry observationRegistry)
|
||||
throws GraphStateException {
|
||||
// apiKey/llmBase 默认空(factory/gamedef 路忽略;测试/OpenAiApi 入口走此)。amodel 路经下方 12 参重载显式带 key/base。
|
||||
return build(mf, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds, stage2ExtraRepairs,
|
||||
sourceMode, historyEnabled, "", "", saverConfig, observationRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装并编译全图(amodel 路 apiKey/llmBase 透传全量重载)。<b>布线唯一源仍是 {@link #assemble}</b>。
|
||||
*
|
||||
* <p>apiKey/llmBase 仅 {@code sourceMode=amodel} 路用——经 {@link #assemble}→{@link SaaStudioNodes#generateNode}
|
||||
* 透传,落 shell-out harness 子进程 env {@code NEWAPI_KEY}/{@code NEWAPI_BASE_URL}(factory/gamedef 路忽略,字节零变)。
|
||||
* dispatcher 据 {@code saaSourceMode=amodel} 调本重载传 {@code properties.getApiKey()}/{@code properties.getLlmBase()}。
|
||||
*
|
||||
* @param apiKey new-api 密钥(amodel harness 子进程 env NEWAPI_KEY;factory/gamedef 路传空即可)。
|
||||
* @param llmBase new-api host 根(amodel harness 子进程 env NEWAPI_BASE_URL;factory/gamedef 路传空即可)。
|
||||
*/
|
||||
public static CompiledGraph build(RoleModelFactory mf, Models models, Path gameRuntimeRoot,
|
||||
int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs,
|
||||
String sourceMode, boolean historyEnabled,
|
||||
String apiKey, String llmBase,
|
||||
SaverConfig saverConfig, ObservationRegistry observationRegistry)
|
||||
throws GraphStateException {
|
||||
StateGraph graph = assemble(mf, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds,
|
||||
stage2ExtraRepairs, sourceMode, historyEnabled);
|
||||
stage2ExtraRepairs, sourceMode, historyEnabled, apiKey, llmBase);
|
||||
|
||||
CompileConfig.Builder cc = CompileConfig.builder()
|
||||
.recursionLimit(recursionLimitFor(maxRepairs, stage2ExtraRepairs));
|
||||
|
||||
@ -19,6 +19,7 @@ import org.springframework.ai.content.Media;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@ -84,6 +85,7 @@ final class SaaStudioNodes {
|
||||
static final String K_FEEDBACK = "feedback"; // 上轮失败回喂(generate 注入 prompt;失败路由写)
|
||||
static final String K_PLAYER_FEEDBACK = "playerFeedback"; // player 软门体验问题(与失败 feedback 解耦,避免串味)
|
||||
static final String K_VALIDATE_OK = "validateOk"; // validate 节点产出(SaaGenNodes 写)
|
||||
static final String K_AMODEL_GEN_OK = "amodelGenOk"; // amodel 路 generate(shell-out harness) 成功标记(validate 据此放行/救场,集成方式C 修陈旧坏 bundle 漏判)
|
||||
static final String K_BUILD_OK = "buildOk"; // build 节点产出
|
||||
static final String K_PLAY_PASS = "playPass"; // play 九门是否全过
|
||||
static final String K_VERDICT = "verdict"; // 九门 verdict(JSON 串)
|
||||
@ -126,7 +128,7 @@ final class SaaStudioNodes {
|
||||
/** state key 全集(KeyStrategyFactory 注册用,全 ReplaceStrategy 覆盖语义)。 */
|
||||
static final String[] ALL_KEYS = {
|
||||
K_BRIEF, K_ENRICHED, K_DESIGN_TEXT, K_GATESPEC, K_GATESPEC_ERROR, K_PLAY_SPEC, K_FACTORY_SRC, K_GAME_ID,
|
||||
K_ROLE, K_REPAIR_COUNT, K_PLAYER_ROUND, K_FEEDBACK, K_VALIDATE_OK, K_BUILD_OK,
|
||||
K_ROLE, K_REPAIR_COUNT, K_PLAYER_ROUND, K_FEEDBACK, K_VALIDATE_OK, K_AMODEL_GEN_OK, K_BUILD_OK,
|
||||
K_PLAY_PASS, K_VERDICT, K_PLAYER_PANEL, K_PLAYER_FEEDBACK, K_TOKENS_IN, K_TOKENS_OUT,
|
||||
K_TOKENS_BY_MODEL, K_ATTEMPTS,
|
||||
K_STATUS, K_FAILURE, K_ENGINE_BUNDLE,
|
||||
@ -718,7 +720,8 @@ final class SaaStudioNodes {
|
||||
|
||||
static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode) {
|
||||
// 默认 openai 路(history 关):现行行为字节不变。anthropic 路经 4 参重载传 historyEnabled=true。
|
||||
return generateNode(gm, gameRuntimeRoot, sourceMode, false);
|
||||
// apiKey/llmBase 仅 amodel 路 shell-out harness 用(factory/gamedef 路忽略);此重载给空串(amodel 路不经此入口)。
|
||||
return generateNode(gm, gameRuntimeRoot, sourceMode, false, "", "");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -733,7 +736,27 @@ final class SaaStudioNodes {
|
||||
* @param historyEnabled true=anthropic 路(连续对话历史+thinking 保留);false=openai 路(每轮 [system,user],字节零变)。
|
||||
*/
|
||||
static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode, boolean historyEnabled) {
|
||||
// 4 参重载(既有 factory/gamedef 调用方 + 历史测试用):apiKey/llmBase 给空串(amodel 路不经此入口)。
|
||||
return generateNode(gm, gameRuntimeRoot, sourceMode, historyEnabled, "", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* generate 节点(amodel 路 apiKey/llmBase 透传全量重载):{@code sourceMode=amodel} 时闭包最前短路为 shell-out harness
|
||||
* ({@link #generateViaAmodelHarness}),apiKey/llmBase 落子进程 env;factory/gamedef 路原码原样在其后(<b>运行时零侵入</b>,apiKey/llmBase 忽略)。
|
||||
*
|
||||
* @param historyEnabled true=anthropic 路(连续对话历史+thinking 保留);false=openai 路(每轮 [system,user],字节零变)。
|
||||
* @param apiKey amodel 路 shell-out harness 用:透传为子进程 env {@code NEWAPI_KEY}(factory/gamedef 路忽略)。
|
||||
* @param llmBase amodel 路 shell-out harness 用:透传为子进程 env {@code NEWAPI_BASE_URL}(factory/gamedef 路忽略)。
|
||||
*/
|
||||
static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode, boolean historyEnabled,
|
||||
String apiKey, String llmBase) {
|
||||
// amodel 旁挂分支(集成方式 C):闭包外先判,命中则整条 generate 短路为 shell-out harness(factory/gamedef 原码零侵入)。
|
||||
boolean amodel = "amodel".equals(sourceMode);
|
||||
return (OverAllState s) -> {
|
||||
// amodel 路:generate 短路 → shell-out 已验证 node harness(harness 内自跑 scaffold/build/stage/smoke),不走下方 factory/gamedef LLM 编排。
|
||||
if (amodel) {
|
||||
return generateViaAmodelHarness(s, gameRuntimeRoot, apiKey, llmBase);
|
||||
}
|
||||
String role = s.value(K_ROLE, "code");
|
||||
boolean isFix = "fix".equals(role);
|
||||
// P1-5:modelTier=stage2(escalate 升档后)→ 选 stage2 强档模型;否则用 stage1。模型对象与模型名选择同口径。
|
||||
@ -860,6 +883,97 @@ final class SaaStudioNodes {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* amodel 路 generate(集成方式 C):shell-out 已验证 node harness {@code tools/amodel-gen/gen.mjs --mode saa}——
|
||||
* harness 在子进程内自跑 scaffold/build/stage/smoke-boot(A-model 手写 LittleJS src/ 多文件工程产线),
|
||||
* 图侧本方法只<b>透传 brief+key 起进程、解析 stdout 末行 JSON、按结果写 state</b>,不接 LLM 编排/历史/救场升档(amodel 不走 design assetSpec/asset 节点,已确认)。
|
||||
*
|
||||
* <p><b>透传链(plan 决策 1)</b>:apiKey/llmBase 经 build→assemble→generateNode 显式带入,落子进程 env
|
||||
* {@code NEWAPI_KEY}/{@code NEWAPI_BASE_URL}(gen.mjs→m3.mjs 据此连 new-api;NEWAPI_BASE_URL=host 根,m3.mjs 自拼 /v1)。
|
||||
*
|
||||
* <p><b>超时(plan 决策 2·最小)</b>:{@code runProcessSplit} 超时 900s;不做 deadline-入-state(TODO(follow-up):把单局剩余预算传入 harness)。
|
||||
*
|
||||
* <p><b>失败兜底(对齐现 generate 全档失败路)</b>:harness 失败/超时<b>不裸退图</b>——写 {@link #K_FEEDBACK}(含 fail 摘要 + stderr 末段)
|
||||
* + 置 {@link #K_AMODEL_GEN_OK}=false(成功则 true)。<b>验收门修(2026-06-22)</b>:validate 据此标记判 fail/放行——
|
||||
* gen.mjs done-gate 顺序是 stage→smoke,smoke 失败 done 被拒但 <b>stage 已落盘陈旧坏 bundle</b>(harness 返 ok:false/stagedDir:null,
|
||||
* 却违约留下文件);若 validate 只查产物存在会误过该坏 bundle、浪费一次九门 play。failCount <b>不在此自增</b>——
|
||||
* 由 validate 失败统一计一次(与 factory/gamedef「generate 产出、validate 计数」同口径,杜绝双重计数提前 escalate/giveup)。
|
||||
*
|
||||
* <p><b>shell 注入安全</b>:argv 独立逐项传(非拼 shell 字符串),brief 内任意字符无需转义。
|
||||
*
|
||||
* @param s 当前 state(读 gameId/enriched/brief/feedback)。
|
||||
* @param gameRuntimeRoot game-runtime 根(子进程 cwd;gen.mjs/tools/node_modules/games 在此)。
|
||||
* @param apiKey new-api 密钥(落子进程 env NEWAPI_KEY)。
|
||||
* @param llmBase new-api host 根(落子进程 env NEWAPI_BASE_URL)。
|
||||
*/
|
||||
private static Map<String, Object> generateViaAmodelHarness(OverAllState s, Path gameRuntimeRoot,
|
||||
String apiKey, String llmBase) {
|
||||
Map<String, Object> out = new HashMap<>();
|
||||
String gameId = s.value(K_GAME_ID, String.class).orElse("");
|
||||
// brief 优先取 enriched(design 富化稿);amodel 路通常无 design,缺则退原始 brief。
|
||||
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);
|
||||
// 上轮失败反馈拼进 brief 末尾(救场重生成时让 harness/模型看到失败原因;无反馈则原样)。
|
||||
String briefText = (feedback != null && !feedback.isEmpty())
|
||||
? enriched + "\n\n## 上轮失败反馈(请修复)\n" + feedback
|
||||
: enriched;
|
||||
try {
|
||||
// argv 独立逐项传(shell 注入安全:brief 任意字符无需转义)。cwd=game-runtime 根。
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"node", "tools/amodel-gen/gen.mjs", "--mode", "saa",
|
||||
"--game-id", gameId, "--brief", briefText);
|
||||
pb.directory(gameRuntimeRoot.toFile());
|
||||
// 透传 new-api 凭据/端点到子进程 env(gen.mjs→m3.mjs 消费)。
|
||||
pb.environment().put("NEWAPI_KEY", apiKey == null ? "" : apiKey);
|
||||
pb.environment().put("NEWAPI_BASE_URL", llmBase == null ? "" : llmBase);
|
||||
// 决策 2(最小):超时 900s;deadline-入-state 留 follow-up。
|
||||
// TODO(follow-up·deadline):把单局剩余预算(perBriefSec - 已耗)算成 timeoutSecs 传入,而非恒 900。
|
||||
SaaGenNodes.SplitProcResult r = SaaGenNodes.runProcessSplit(pb, 900);
|
||||
|
||||
// 解析 stdout 末行 JSON(amodel harness CLI 契约:末行单行 {ok,gameId,stagedDir,fail})。
|
||||
JsonNode env = SaaPrompts.looseParse(r.lastStdoutLine());
|
||||
boolean ok = !r.timedOut && env != null && env.path("ok").asBoolean(false);
|
||||
if (ok) {
|
||||
// 成功:harness 已 stage bundle.iife.js + smoke 绿(validate 仅轻断言其在场);记一条 attempt(role=amodel)。
|
||||
out.put(K_AMODEL_GEN_OK, true); // 放行信号:generate 真成功,validate 才认 staged 产物(防陈旧坏 bundle 漏到 play)。
|
||||
appendAttempt(s, out, MAPPER.createObjectNode()
|
||||
.put("role", "amodel").put("model", "amodel-harness")
|
||||
.set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0))); // token best-effort 略(harness 内部消耗未回传)
|
||||
return out;
|
||||
}
|
||||
// 失败:harness 未合格 done / 超时。
|
||||
String fail = r.timedOut
|
||||
? "harness 超时(900s)"
|
||||
: (env != null ? env.path("fail").asText("harness 未合格 done") : "harness 无可解析 stdout 末行 JSON");
|
||||
out.put(K_FEEDBACK, "amodel harness 失败:" + fail + " | " + tailStr(r.stderr, 1500));
|
||||
// generate 失败:置 amodelGenOk=false(validate 据此判 fail→救场)。failCount 由 validate 失败统一计一次,勿在此重计。
|
||||
out.put(K_AMODEL_GEN_OK, false);
|
||||
appendAttempt(s, out, MAPPER.createObjectNode()
|
||||
.put("role", "amodel").put("model", "amodel-harness").put("stage_fail", "amodel")
|
||||
.set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0)));
|
||||
return out;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// 子进程启动/等待异常:同样不裸退图(写 feedback + failCount,下游救场)。
|
||||
if (e instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
out.put(K_FEEDBACK, "amodel harness 子进程异常:" + trunc(e.getMessage(), 300));
|
||||
out.put(K_AMODEL_GEN_OK, false); // 同上:标记 generate 失败,failCount 留 validate 统一计一次。
|
||||
appendAttempt(s, out, MAPPER.createObjectNode()
|
||||
.put("role", "amodel").put("model", "amodel-harness").put("stage_fail", "amodel")
|
||||
.set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0)));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/** 取字符串尾部 n 字(amodel stderr 摘要:错误通常在末段,故取 tail 而非 {@link #trunc} 的 head)。 */
|
||||
private static String tailStr(String s, int n) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.length() <= n ? s : s.substring(s.length() - n);
|
||||
}
|
||||
|
||||
/**
|
||||
* P1-5:按 {@code (role, modelTier)} 二维选 generate 实际所用模型名(与 {@link #generateNode} 内模型对象选择同口径)。
|
||||
* <p>抽成纯静态方法便于<b>无网络</b>断言「升档后真切到 stage2 强档模型」(topology 测试调本方法即可,无需构造 ChatModel)。
|
||||
@ -1882,6 +1996,28 @@ final class SaaStudioNodes {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* validate 失败回边(amodel-aware 变体,集成方式 C):包 {@link #okElseRepairOrEscalateOrGiveup}——
|
||||
* 仅当 {@code sourceMode=amodel} 且基判据返回 {@code "ok"} 时改返 {@code "play"}(amodel 路 harness 已自跑
|
||||
* scaffold/build/stage/smoke,validate-ok 后<b>直奔 play 九门</b>,跳过 scaffold/asset/build);其余一律原样透传基判据
|
||||
* (factory/gamedef 路 validate-ok→scaffold,<b>运行时零变</b>;fail 分支 repair/escalate/giveup 不受影响)。
|
||||
*
|
||||
* @param okKey 成功标志 state key(此处恒 {@link #K_VALIDATE_OK})。
|
||||
* @param maxRepairs stage1 失败阈。
|
||||
* @param stage2ExtraRepairs stage2 额外失败阈。
|
||||
* @param sourceMode 生成产物形态(factory|gamedef|amodel):仅 amodel 把 ok 改路由到 play。
|
||||
*/
|
||||
static EdgeAction okElseRepairOrEscalateOrGiveupAmodelAware(String okKey, int maxRepairs,
|
||||
int stage2ExtraRepairs, String sourceMode) {
|
||||
EdgeAction base = okElseRepairOrEscalateOrGiveup(okKey, maxRepairs, stage2ExtraRepairs);
|
||||
boolean amodel = "amodel".equals(sourceMode);
|
||||
return (OverAllState s) -> {
|
||||
String r = base.apply(s);
|
||||
// amodel 路 validate-ok → 短路到 play(跳 scaffold/asset/build);非 amodel 或非 ok 一律原样(factory 不变)。
|
||||
return (amodel && "ok".equals(r)) ? "play" : r;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* validate/build 失败回边共用判据(对齐蓝图 §3.1):ok? 下一步 : repairCount>=N? giveup : repair。
|
||||
*
|
||||
|
||||
@ -192,7 +192,11 @@ public class AigcExecutorProperties {
|
||||
/**
|
||||
* SAA 生成产物形态(plan 2026-06-18-001 U3,additive 灰度):{@code factory}=现行 iife factorySrc(generate 产 ```js 工厂、
|
||||
* validate 走 12 禁词静态门,<b>默认·行为字节不变</b>);{@code gamedef}=真结构化 gameDefinition(generate 产声明式源落
|
||||
* {@code sourceProject}、validate 经 build-from-source.mjs 校验+装配出工厂——「改源不改包/可维护结构化源」范式上产线)。
|
||||
* {@code sourceProject}、validate 经 build-from-source.mjs 校验+装配出工厂——「改源不改包/可维护结构化源」范式上产线);
|
||||
* {@code amodel}=A-model 手写 LittleJS src/ 多文件工程(generate 短路为 <b>shell-out 已验证 node harness</b>
|
||||
* {@code game-runtime/tools/amodel-gen/gen.mjs --mode saa}:harness 内自跑 scaffold/build/stage/smoke-boot,
|
||||
* 图侧 generate 只透传 brief+key 起子进程、validate 只轻断言 staged {@code bundle.iife.js} 含 __GameBundle,
|
||||
* 路由短路 scaffold/asset/build 直奔 play——纯旁挂 flag 分支,factory/gamedef 路运行时零侵入)。
|
||||
* <b>cutover 量化门 = gamedef 路 SaaFullGraphE2eTest 过门率 ≥ 60%(iife 现基线)</b>;达标才切默认。仅 dispatcher=saa 生效。
|
||||
*/
|
||||
private String saaSourceMode = "factory";
|
||||
|
||||
@ -60,8 +60,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
* <p><b>003-U1 量测 flags</b>:
|
||||
* <ul>
|
||||
* <li>{@code -Dsaa.e2e.sourceMode}(默认 {@code factory}):量哪条生成路——{@code factory}=iife 工厂旧路(现行字节零变基线);
|
||||
* {@code gamedef}=真结构化 gameDefinition 路(cutover ≥60% 量测对象)。<b>修发现②</b>:旧版未 setSaaSourceMode→量的是 iife 旧路非 gamedef,
|
||||
* 故量 gamedef 真基线<b>必须</b> {@code -Dsaa.e2e.sourceMode=gamedef};</li>
|
||||
* {@code gamedef}=真结构化 gameDefinition 路(cutover ≥60% 量测对象);{@code amodel}=A-model 手写 LittleJS src/ 多文件工程路
|
||||
* (generate 短路 shell-out node harness {@code tools/amodel-gen/gen.mjs --mode saa}:harness 内自跑 scaffold/build/stage/smoke,
|
||||
* 图侧 validate 轻断言 staged bundle 后短路 scaffold/asset/build 直奔 play 九门;<b>amodel 变体无结构改动</b>,
|
||||
* 经 {@code setSaaSourceMode} 透传、{@code -Dsaa.e2e.sourceMode=amodel} 即可驱动,需 game-runtime 有 tools/amodel-gen + node)。
|
||||
* <b>修发现②</b>:旧版未 setSaaSourceMode→量的是 iife 旧路非 gamedef,故量 gamedef/amodel 真基线<b>必须</b>显式 {@code -Dsaa.e2e.sourceMode=<gamedef|amodel>};</li>
|
||||
* <li>{@code -Dsaa.e2e.concurrency}(默认 1):后台并发度 K——1=串行(生产字节零变基线);>1 多 job 并行真玩(各占错开端口对,
|
||||
* K=1 vs K=N verdict 对等校准延 003-U5 真跑期);</li>
|
||||
* <li>{@code -Dsaa.e2e.briefFile}(可选):外部 brief 文件(行式,去空白/跳空行与 {@code #} 注释);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user