feat(amodel-gen): agentic A-model 生成 harness(ReAct+M3)+ skill + 四层设计
spike 验证通过:M3 经 new-api(openai+tools)agentic 自闭环产出可玩 A-model 游戏。 - harness game-runtime/tools/amodel-gen/(tools/play/m3/prompt/gen.mjs,零三方依赖): scaffold(clone _template)→ ReAct(read/write/list/check/build/done;done 门=check+build 绿; compaction + loop-detection)→ 循环外 play(headless Chrome boot+截图+交互 = 能跑能玩)。 - 3/3 玩法干净自闭环实证:打地鼠 / 陨石躲避 / 反应点击(13-22 步,175-378K token,play PASS)。 - skill .agents/skills/agentic-amodel-generation.md(链路/配方/8 坑/M3 实证/主流定性)+ README/AGENTS 索引同步。 - 设计 docs/agent-specs/2026-06-21-agentic-amodel-generation-design.md(§0 整链路 current→new + 四层 + §0.8 主流对照;draft 待 §6.8 双评审)。 取代旧 gamedef/factory 单次产线(A-model 生产形态);production 删 gamedef 待双评审(D1=A 无退路)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c44681b580
commit
6e388d9bbb
@ -56,7 +56,8 @@
|
|||||||
| `skills/staging-ops.md` | staging 运维配方:机器分工/代码同步/后端重部署/前端构建门/冒烟门 |
|
| `skills/staging-ops.md` | staging 运维配方:机器分工/代码同步/后端重部署/前端构建门/冒烟门 |
|
||||||
| `skills/ui-walkthrough-cdp.md` | 真 UI 走查配方(CDP on mini-desktop):studio/admin 走查 + CDP 七坑 + 信道探针 |
|
| `skills/ui-walkthrough-cdp.md` | 真 UI 走查配方(CDP on mini-desktop):studio/admin 走查 + CDP 七坑 + 信道探针 |
|
||||||
| `skills/game-e2e-cdp-harness.md` | Canvas 游戏 e2e 证据 harness:编排形制/驱动器六律/出厂红线/四件套口径(T1b-α 实证;§7 = W-G1 生成游戏真玩九门 = 假绿守卫 G 门 + H 机制/latch + I 控制手感 + 自产 gatespec) |
|
| `skills/game-e2e-cdp-harness.md` | Canvas 游戏 e2e 证据 harness:编排形制/驱动器六律/出厂红线/四件套口径(T1b-α 实证;§7 = W-G1 生成游戏真玩九门 = 假绿守卫 G 门 + H 机制/latch + I 控制手感 + 自产 gatespec) |
|
||||||
| `skills/cheap-model-game-generation.md` | 便宜模型(new-api DeepSeek/MiniMax)直出可真玩轻游戏 L1 worker:链路/模型与成本/L1 纪律/坑红线/latch 套壳/design agent 自产 gatespec/质量三层(W-G1 HJ-GEN-001 实证) |
|
| `skills/cheap-model-game-generation.md` | 便宜模型(new-api DeepSeek/MiniMax)直出可真玩轻游戏 L1 worker:链路/模型与成本/L1 纪律/坑红线/latch 套壳/design agent 自产 gatespec/质量三层(W-G1 HJ-GEN-001 实证)。**注**:A-model 路改用下条 agentic harness,本条 gamedef/worker-loop 部分已退役 |
|
||||||
|
| `skills/agentic-amodel-generation.md` | **agentic A-model 生成 harness(ReAct+M3 已实证·生产形态)**:scaffold→工具循环(read/write/list/check/build/done)→循环外 play;链路/5 文件/运行配方/8 坑(write 前缀·compaction·停机纪律·防假绿·bundle.tick·boot envelope·路径深度)/M3 3-玩法自闭环实证/主流定性;取代旧 gamedef 单次产线(HJ-AGI-003) |
|
||||||
| `skills/saa-graph-orchestration.md` | SAA(Spring AI Alibaba)裸 StateGraph 生成编排:拓扑/加节点/接 new-api(剥 /v1 坑)/checkpoint(含 saved_at 无 tiebreaker 框架坑+显式 checkPointId 修法)/observation/最小依赖集/派发契约/验证门(HJ-AGI-002 实证) |
|
| `skills/saa-graph-orchestration.md` | SAA(Spring AI Alibaba)裸 StateGraph 生成编排:拓扑/加节点/接 new-api(剥 /v1 坑)/checkpoint(含 saved_at 无 tiebreaker 框架坑+显式 checkPointId 修法)/observation/最小依赖集/派发契约/验证门(HJ-AGI-002 实证) |
|
||||||
| `skills/doc-organizer.md` | 文档整理助手(创始人手动触发):增量(上次清理→现在)+**两阶段审批门**——发起分析 Workflow→编清理计划→评审→批准后才执行;三轴=过期档清理(归档/压缩)/核心设计档措辞对齐现行真相/主任务总账回填;底层 `tools/doc-organizer.{sh,-analyze.mjs,-state.json}`(检测自动·判断留人·脚本不自删·最新日在飞档不碰) |
|
| `skills/doc-organizer.md` | 文档整理助手(创始人手动触发):增量(上次清理→现在)+**两阶段审批门**——发起分析 Workflow→编清理计划→评审→批准后才执行;三轴=过期档清理(归档/压缩)/核心设计档措辞对齐现行真相/主任务总账回填;底层 `tools/doc-organizer.{sh,-analyze.mjs,-state.json}`(检测自动·判断留人·脚本不自删·最新日在飞档不碰) |
|
||||||
| `skills/drive-remote-claude-tmux.md` | 远程驱动交互式 Claude Code(ssh + tmux):双向通道 send-keys 派活 + capture-pane 读屏,不走 ACP/headless;每设备配置块 + onboarding 侦察配方 + 专用会话(独立 worktree 防撞树) + bypass-perms + 安全红线;**完成判据=git push 非读屏**(创始人 2026-06-18 拍板·多设备复用) |
|
| `skills/drive-remote-claude-tmux.md` | 远程驱动交互式 Claude Code(ssh + tmux):双向通道 send-keys 派活 + capture-pane 读屏,不走 ACP/headless;每设备配置块 + onboarding 侦察配方 + 专用会话(独立 worktree 防撞树) + bypass-perms + 安全红线;**完成判据=git push 非读屏**(创始人 2026-06-18 拍板·多设备复用) |
|
||||||
|
|||||||
82
.agents/skills/agentic-amodel-generation.md
Normal file
82
.agents/skills/agentic-amodel-generation.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# skill:agentic A-model 游戏生成 harness(ReAct + M3,已实证)
|
||||||
|
|
||||||
|
> **一句话**:给模型 read/write/list/check/build/done 工具,让它**自己读 skill→读插件 api.d.ts→读范例→写多文件 LittleJS `src/`→自查 check/build→done**,harness 再跑循环外 play 出"能跑能玩"判定。这是 A-model 生成的**生产形态**(取代旧 gamedef/factory 单次产线)。
|
||||||
|
>
|
||||||
|
> 配套:code 层手册 [`littlejs-game-dev.md`](littlejs-game-dev.md) · design 层 [`sim-business-game-design.md`](sim-business-game-design.md) · 设计权威 [`../../docs/agent-specs/2026-06-21-agentic-amodel-generation-design.md`](../../docs/agent-specs/2026-06-21-agentic-amodel-generation-design.md)。旧 [`cheap-model-game-generation.md`](cheap-model-game-generation.md) 的 gamedef/worker-loop 路对 A-model **已退役**,只看其通用纪律。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 何时用
|
||||||
|
|
||||||
|
- 要让一个模型(默认 **M3**,经 new-api)从一句 brief **自动生成一款可跑可玩的多文件 LittleJS 小游戏**;
|
||||||
|
- 验证某模型/某 brief 的生成可达性;
|
||||||
|
- 作为 SAA 生成图 `generate` 节点的进程外 spike(验通后端口进 SAA)。
|
||||||
|
|
||||||
|
## 2. 链路(agentic generate 内循环)
|
||||||
|
|
||||||
|
```
|
||||||
|
scaffold(clone _template → games/amgen-<id>/)
|
||||||
|
→ ReAct 循环(M3 @ new-api,openai 协议 + function-calling):
|
||||||
|
system+brief → M3 发 tool_calls → harness 执行 → 回喂 → 循环
|
||||||
|
工具:read_file/list_dir(repo 只读)· write_file(scoped)· check · build · done
|
||||||
|
done 门:check 与 build 都 PASS 才接受(harness 复核)
|
||||||
|
→ play(循环外·权威门:serve + headless Chrome boot + 帧推进 + 截图 + 交互 = 能跑能玩)
|
||||||
|
→ verdict.json + run-log/run-summary + 截图
|
||||||
|
```
|
||||||
|
|
||||||
|
四层(自上而下,设计文档详):**L1 env**(mini-desktop 沙箱)→ **L2 harness**(本 ReAct)→ **L3 context**(agent 经 read_file 按需自取 skill/api/范例,不预塞)→ **L4 prompt**(薄,指 agent 读真 skill,**绝不手抄**)。
|
||||||
|
|
||||||
|
## 3. harness 位置 + 文件
|
||||||
|
|
||||||
|
`game-runtime/tools/amodel-gen/`(node,零三方依赖——Node22 全局 fetch/WebSocket):
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|---|---|
|
||||||
|
| `tools.mjs` | scaffold(clone `_template`,删 dist/test)+ 5 工具 read/write/list/check/build;check=`node --check`+五法/导出名/红线 lint(去注释串再扫) |
|
||||||
|
| `m3.mjs` | new-api M3 客户端(openai+tools,超时/重试;key:env 优先、`docs/内网凭据与端点.md` 兜底) |
|
||||||
|
| `prompt.mjs` | L4 薄 prompt + 工具 schema(指 agent 读真 skill) |
|
||||||
|
| `play.mjs` | 循环外权威门:CDP(全局 WebSocket)boot+帧推进+截图+`do(start)`/tap+读 `__gameHost.state()` |
|
||||||
|
| `gen.mjs` | orchestrator:scaffold→ReAct(compaction+loop-detection+done 门)→play→落证据 |
|
||||||
|
|
||||||
|
## 4. 运行配方(mini-desktop)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 同步 harness(Mac→mini)
|
||||||
|
rsync -az -e "ssh -o ProxyCommand=none" game-runtime/tools/amodel-gen/ \
|
||||||
|
root@100.64.0.7:/root/game-staging/repo/game-runtime/tools/amodel-gen/
|
||||||
|
# 跑一款(端口两两错开,避开 live 48080)
|
||||||
|
ssh -o ProxyCommand=none root@100.64.0.7 'cd /root/game-staging/repo/game-runtime && \
|
||||||
|
NEWAPI_KEY=sk-... node tools/amodel-gen/gen.mjs --id <id> --brief "<玩法一句话>" \
|
||||||
|
--max-steps 45 --port 4321 --cdp 9223'
|
||||||
|
# 证据:games/amgen-<id>/evidence/{run-summary.json,run-log.txt,first-paint.png,after-play.png}
|
||||||
|
```
|
||||||
|
> 控制组(无 agent,验管线):`node tools/amodel-gen/tools.mjs scaffold ctrl1 && ... build ctrl1 && node tools/amodel-gen/play.mjs ctrl1 4320 9222`。
|
||||||
|
|
||||||
|
## 5. 关键坑 / 红线(踩过的,务必守)
|
||||||
|
|
||||||
|
1. **write 路径必须含 `game-runtime/` 前缀**(所有工具路径 = repo-根相对)。prompt 里 game 目录写 `game-runtime/games/amgen-<id>/`,与 read/write 同源——**少前缀 → 写全被拦 → agent 原地打转**(mole2 实证)。boundary 报错要回显正确允许路径。
|
||||||
|
2. **token compaction 必须做**:全历史每步重发会爆炸(2.3M token!)。`gen.compactHistory` 折叠旧的大 tool 结果 / 旧 write 内容为占位(保留最近 ~8 条)→ **降到 ~200K**。文件在磁盘,agent 需要可重新 read_file。
|
||||||
|
3. **停机纪律**:① scaffold **删 `test/`**(去掉"写测试"诱因)② prompt 明示"check+build 绿就**立即 done**、**没有 edit_file**(用 write_file 整覆盖)、别写测试/额外文件" ③ 双绿后 harness **一次性催 done**。缺它 → agent 绿了还乱写测试耗尽步数(mole1 实证,45 步未 done)。
|
||||||
|
4. **check 加"game.js≠`_template`"防假绿**:写路径错→没覆盖→check 会在**未改的模板**上 PASS,蒙混过关。
|
||||||
|
5. **`update(dt)` 必调 `opts.runtime.bundle.tick(dt)`**:否则插件 `onFrame` 不推进、timer 永不到期、一局不结束(check 已硬拦)。
|
||||||
|
6. **boot envelope(沿用 `_template`)**:全局名 `TemplateGame` + `buildTemplateHostConfig`(host-config 导出名固定,别改)+ 标记 `__gameBooted/__gameHost/__gameForensics`;play 用 `?engine=real`;build `--global-name=TemplateGame`。
|
||||||
|
7. **路径深度**:游戏落 `games/<id>/`(直接在 games/ 下,与 catch-fruit 同深度)→ `src/*.js` import 插件用 `../../../src/`(3-up)。嵌更深一层会变 4-up,esbuild 解析失败。
|
||||||
|
8. **固定 plumbing 不让 agent 碰**:`index.html`/`entry.js`/`src/main.js`(scaffold 自带);agent 只写 `src/{game,core,render,host-config,assets}.js`。
|
||||||
|
|
||||||
|
## 6. M3 实证(2026-06-21,HJ-AGI-003)
|
||||||
|
|
||||||
|
| run | 玩法 | steps | tokens | check/build/done/play |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| mole1/mole3 | 打地鼠 | 14 | 200K | 全 ✓(mole3 自闭环) |
|
||||||
|
| dodge1 | 陨石躲避(移动+碰撞+提速) | 13 | 175K | 全 ✓ |
|
||||||
|
| react1 | 反应点击(寿命靶+save-progress) | 22 | 378K | 全 ✓ |
|
||||||
|
|
||||||
|
**3/3 跨玩法干净自闭环**,截图实锤可玩。M3 tool-calling 经 new-api openai 协议快(~2s/步、不触慢思考)。bar=**能跑能玩**(暂丢九门;九门是旧管线的,A-model 验证另议)。
|
||||||
|
|
||||||
|
## 7. 主流定性(设计 §0.8)
|
||||||
|
|
||||||
|
整体 = Anthropic taxonomy 的 **orchestrator workflow + 内嵌 agentic(ReAct)+ evaluator-optimizer(repair)**,属"受约束 agentic"——主流且被推荐用于可预测生产管线。已采纳"循环内便宜快反馈(check/build)+ 循环外昂贵权威验证(play)"分层。**按需补的缺口**:agent scratchpad、更强的长循环压缩、play 失败→repair 回环(spike 暂线性 done→play)。
|
||||||
|
|
||||||
|
## 8. 下一步(production)
|
||||||
|
|
||||||
|
spike 过 → 把本 agentic generate 端口进 `SaaStudioGraph` 的 generate 节点(Java 侧重写,调用同形态 M3 tool-loop),按设计 §0.5 删 gamedef/factory,validate/play 换 A-model 版。**须过 §6.8 Codex+Opus 双评审后才执行删除(D1=A 无退路)。**
|
||||||
@ -146,6 +146,7 @@ games-development-ai/
|
|||||||
| [`.agents/skills/add-game-template.md`](.agents/skills/add-game-template.md) | 新玩法模板上线配方(contract→prompt→runtime→backend→orchestrator→五级验收门) |
|
| [`.agents/skills/add-game-template.md`](.agents/skills/add-game-template.md) | 新玩法模板上线配方(contract→prompt→runtime→backend→orchestrator→五级验收门) |
|
||||||
| [`.agents/skills/ai-generation-pipeline.md`](.agents/skills/ai-generation-pipeline.md) | AI 生成流水线(Dify + OpenGame + aigc 外壳)开发手册 |
|
| [`.agents/skills/ai-generation-pipeline.md`](.agents/skills/ai-generation-pipeline.md) | AI 生成流水线(Dify + OpenGame + aigc 外壳)开发手册 |
|
||||||
| [`.agents/skills/cheap-model-game-generation.md`](.agents/skills/cheap-model-game-generation.md) | 便宜模型造游戏(W-G1):worker loop · 九门真玩 harness · design-agent 自产 gatespec · 成本 / 模型选择 · 5 个坑(HJ-GEN-001 已验证) |
|
| [`.agents/skills/cheap-model-game-generation.md`](.agents/skills/cheap-model-game-generation.md) | 便宜模型造游戏(W-G1):worker loop · 九门真玩 harness · design-agent 自产 gatespec · 成本 / 模型选择 · 5 个坑(HJ-GEN-001 已验证) |
|
||||||
|
| [`.agents/skills/agentic-amodel-generation.md`](.agents/skills/agentic-amodel-generation.md) | **agentic A-model 生成 harness(ReAct+M3 已实证·生产形态)**:scaffold→工具循环(read/write/list/check/build/done)→循环外 play;5 文件 + 运行配方 + 8 坑(write 前缀/compaction/停机纪律/防假绿/bundle.tick/boot envelope/路径深度)+ M3 3-玩法自闭环实证;取代旧 gamedef 单次产线(HJ-AGI-003) |
|
||||||
| [`.agents/skills/saa-graph-orchestration.md`](.agents/skills/saa-graph-orchestration.md) | SAA 裸 StateGraph 生成编排:拓扑 / 加节点 / new-api(剥 /v1)/ checkpoint(含 saved_at 无 tiebreaker 的框架坑 + 显式 checkPointId 修法)/ 观测 / 最小依赖集 / dispatcher 契约 / 门(HJ-AGI-002 已验证) |
|
| [`.agents/skills/saa-graph-orchestration.md`](.agents/skills/saa-graph-orchestration.md) | SAA 裸 StateGraph 生成编排:拓扑 / 加节点 / new-api(剥 /v1)/ checkpoint(含 saved_at 无 tiebreaker 的框架坑 + 显式 checkPointId 修法)/ 观测 / 最小依赖集 / dispatcher 契约 / 门(HJ-AGI-002 已验证) |
|
||||||
| [`.agents/skills/prompt-governance.md`](.agents/skills/prompt-governance.md) | Prompt 作为第 8 契约:Registry / 加载-注入 / eval 门 / HITL 治理 |
|
| [`.agents/skills/prompt-governance.md`](.agents/skills/prompt-governance.md) | Prompt 作为第 8 契约:Registry / 加载-注入 / eval 门 / HITL 治理 |
|
||||||
| [`.agents/skills/littlejs-game-dev.md`](.agents/skills/littlejs-game-dev.md) | **AI 直接写 LittleJS 游戏代码作业手册(code 层)**:12 插件 API 速查(8 基元 + 4 编排=session-score/hud-ui/scene-fsm/timer-scheduler)+ 可导航代码结构 + 写 WHAT 调 HOW 边界 + 资产/mmx 流 + 工厂五法契约 + 受控面铁律(终态产物=src/ 多文件工程,取代废弃的 gameDefinition JSON 投影线) |
|
| [`.agents/skills/littlejs-game-dev.md`](.agents/skills/littlejs-game-dev.md) | **AI 直接写 LittleJS 游戏代码作业手册(code 层)**:12 插件 API 速查(8 基元 + 4 编排=session-score/hud-ui/scene-fsm/timer-scheduler)+ 可导航代码结构 + 写 WHAT 调 HOW 边界 + 资产/mmx 流 + 工厂五法契约 + 受控面铁律(终态产物=src/ 多文件工程,取代废弃的 gameDefinition JSON 投影线) |
|
||||||
|
|||||||
224
docs/agent-specs/2026-06-21-agentic-amodel-generation-design.md
Normal file
224
docs/agent-specs/2026-06-21-agentic-amodel-generation-design.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
topic: Agentic A-model 游戏生成 · 四层设计(自上而下)
|
||||||
|
type: design
|
||||||
|
status: draft(执行前过 §6.8 双评审)
|
||||||
|
date: 2026-06-21
|
||||||
|
owner: Mac 开发执行线
|
||||||
|
origin:
|
||||||
|
- docs/agent-specs/2026-06-21-saa-amodel-rewrite-review.md(SAA→A-model 改写评审版 v3,D1=A/D6=A)
|
||||||
|
- 审计结论(2026-06-21):上一轮 spike = 非测(harness path bug + skill 从未加载 + 单次塞 prompt)→ 改 agentic(方案 B)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agentic A-model 游戏生成 · 四层设计(自上而下)
|
||||||
|
|
||||||
|
> **为什么自上而下、prompt 最后**:每层是下一层的地基——**env**(在哪跑/怎么隔离)→ **harness**(agent 有什么工具/怎么循环)→ **context 拼装**(agent 能读到/读什么)→ **prompt**(只是指挥 agent 用好上面三层的薄指令)。上一轮失败正是**跳过前三层、直接手写 6.6KB prompt 硬塞**(skill 全文 + api.d.ts + 范例 0 进 prompt + harness path bug)= 地基没打就盖楼。
|
||||||
|
>
|
||||||
|
> **方案 = B(agentic)**:给模型 read/write-file 工具,让它**自己读 skill→读 api.d.ts→读范例→写多文件**(catch-fruit 已证有能力 agent 行)。**不**用 A(单次塞满 prompt)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0 · 整条链路架构(current → new · 删 gamedef 留概念) — 2026-06-21 founder 定调
|
||||||
|
|
||||||
|
> **founder 三条定调(2026-06-21)**:① 可删旧 gamedef 生成代码;② 保留整体设计**概念**、重写整条链路;③ **模型只用 M3**。
|
||||||
|
> **已验证前提(本日实测)**:M3 经 new-api(openai 协议)发 tool_calls **干净且快**(1.8s,reasoning=0,并行 2 调用,参数正确)→ 方案 B agentic 成立。M3-only 使旧的「模型档阶梯(stage1/stage2/fallback)」塌缩为单档。
|
||||||
|
|
||||||
|
### §0.1 现状 → 新架构 · 节点级映射(16 节点拓扑保留,generate 换芯)
|
||||||
|
|
||||||
|
| 节点 | 现状 | 新(D1=A 单一 A-model + M3 + agentic) | 动作 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| render | init role/tier/counters | 同;tier 简化(仅 M3) | 改·简化 |
|
||||||
|
| classify | 品类→archetype+三维profile | 同,M3 | 留 |
|
||||||
|
| design | 单次 M3→设计稿+自产 gatespec | M3(可 agentic 读 design-skill) | 留·可增强 |
|
||||||
|
| **generate** | **单次 completion → iife OR gamedef JSON** | **agentic ReAct:M3 + read/write/list → 多文件 `src/`** | **重写·核心** |
|
||||||
|
| **validate** | iife 静态契约 **OR** gamedef build-from-source.mjs | A-model 静态:`node --check` × src/*.js + 五法契约 + host-config 导出 + 插件键一致 | **重写** |
|
||||||
|
| scaffold | 落 generated-factory.js + 模板 | 装配固定 plumbing(entry-bundle/index/assets-stub)环绕 agent 的 `src/` | 改 |
|
||||||
|
| asset | 六类资产规格 | 同(spike 可 stub→标题底纹兜底) | 留 |
|
||||||
|
| build | esbuild build.mjs → bundle.iife.js | 同(`--global-name=__GameBundle`) | 留 |
|
||||||
|
| play | 九门 serve-and-play(A–E 客观门) | boot+截图+点击 = **能跑能玩**(spike 暂丢九门) | 改·简化 |
|
||||||
|
| player | VLM 视觉+文本顾问 | M3 文本顾问(视觉待 M3 能力确认) | 留·spike 暂缓 |
|
||||||
|
| nreview | 叙事质量门 | 同 | 留·spike 暂缓 |
|
||||||
|
| modify | 改源路 | 同(spike 只走 create) | 留·spike 外 |
|
||||||
|
| repair | count++,feedback→generate | 同 | 留 |
|
||||||
|
| **escalate** | 升档 stage2 强档**模型** | **M3-only 模型档塌缩** → 重定义为 M3+thinking预算/温度升档,或并入 repair | 改·塌缩 |
|
||||||
|
| emit | 读 bundle→engineBundle,成功 | 同(→ GamePackage 契约,**对下游不变的边界**) | 留 |
|
||||||
|
| giveup | 终态失败完整 dump | 同 | 留 |
|
||||||
|
|
||||||
|
### §0.2 新链路 · 系统架构(分层 ASCII)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ 后端 game-cloud · SAA StateGraph(进程内 dispatcher=saa,Java)──────────────────┐
|
||||||
|
│ │
|
||||||
|
│ START→render→classify→design→[generate]→validate→scaffold→asset→build→play │
|
||||||
|
│ ▲ │ │ │
|
||||||
|
│ repair 回环 ◄─────────┴── validate/build/play fail ──┘ │
|
||||||
|
│ │ play pass │
|
||||||
|
│ giveup(终态失败) {player | nreview} │
|
||||||
|
│ │ ok │
|
||||||
|
│ emit(终态成功) │
|
||||||
|
│ │ generate:HTTP+工具循环 │ validate/build/play:ProcessBuilder│ emit │
|
||||||
|
└─────────┼───────────────────────────┼───────────────────────────────────┼────────┘
|
||||||
|
▼ (M3 ReAct) ▼ → node 脚本 ▼ GamePackage
|
||||||
|
┌──────────────┐ ┌─────────────────────────────────────────┐ ┌──────────┐
|
||||||
|
│ new-api 网关 │ │ 运行时 game-runtime(node · mini-desktop)│ │ 契约边界 │
|
||||||
|
│ 100.64.0.8 │ │ ┌───────────────────────────────────┐ │ │ game- │
|
||||||
|
│ **M3 only** │◄────►│ │ games/<id>/ ← 本次产物 │ │ │ package │
|
||||||
|
│ tool-calls ✓ │ 工具 │ │ src/{core,render,balance,game, │ │ │ .schema │
|
||||||
|
└──────────────┘ 读写 │ │ host-config}.js ← M3 写 │ │ │ →落库→ │
|
||||||
|
│ │ entry-bundle.js(__GameBundle) │ │ │ studio/ │
|
||||||
|
│ │ + index.html + assets stub │ │ │ feed │
|
||||||
|
│ ├───────────────────────────────────┤ │ └──────────┘
|
||||||
|
│ │ src/plugins/*(12) · core/*.d.ts │ │
|
||||||
|
│ │ build.mjs(esbuild)· serve+play │ │
|
||||||
|
│ └───────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### §0.3 generate 节点 · agentic 内循环(四层 + **循环内 check/build 自检 / 循环外 play 权威门** —— 详见 §0.8)
|
||||||
|
|
||||||
|
> 下图 read/write/list 之外,工具集已加 **check/build**(循环内快反馈);**done 门 = check+build 必须已绿**;`play` 是 done 之后的**循环外权威门**(慢)。
|
||||||
|
|
||||||
|
```
|
||||||
|
╔═ L1 ENV · games/<id>/(scoped 写)· repo 只读 · M3@new-api · 无 shell ════════════╗
|
||||||
|
║ ║
|
||||||
|
║ ╔═ L2 HARNESS · ReAct 控制器(node,确定性)═══════════════════════════════════╗ ║
|
||||||
|
║ ║ ┌ L4 PROMPT(薄):角色+步骤+红线+完成判据;指 agent 读真 skill,不手抄 ┐ ║ ║
|
||||||
|
║ ║ │ system+brief ─► M3 ─► tool_calls? ─yes─► harness 执行 │ ║ ║
|
||||||
|
║ ║ │ ▲ │ read/write/list │ ║ ║
|
||||||
|
║ ║ │ │ 回喂结果/纠错 no(done) └ L3 CONTEXT:按需读 ┐│ ║ ║
|
||||||
|
║ ║ │ └───────────────────┼──── code-skill→api.d.ts→catch-fruit ──┘│ ║ ║
|
||||||
|
║ ║ └────────────────────────── ▼ done/max-steps ───────────────────────┘ ║ ║
|
||||||
|
║ ║ build.mjs ─► serve ─► play(boot+截图+点击)─► verdict(能跑能玩?) ║ ║
|
||||||
|
║ ╚═══════════════════════════════════════════════════════════════════════════╝ ║
|
||||||
|
╚═════════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
### §0.4 单次生成作业 · 流程图(repair/giveup 分支)
|
||||||
|
|
||||||
|
```
|
||||||
|
brief
|
||||||
|
└► render ─create─► classify(品类→archetype) ─► design(M3 读 design-skill → 玩法设计 + gatespec)
|
||||||
|
│
|
||||||
|
┌──────────────────────────────────────▼─────────────────────────────────┐
|
||||||
|
│ generate(agentic M3):读 code-skill + 用到插件 api.d.ts + catch-fruit 范例 │
|
||||||
|
│ → 多轮工具写 src/{core,render,balance,game,host-config}.js │
|
||||||
|
└──────────────────────────────────────┬─────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
validate(A-model 静态:node --check + 五法契约 + host-config + 插件键)
|
||||||
|
│ ok │ fail
|
||||||
|
▼ ▼
|
||||||
|
scaffold(固定 plumbing) ──► asset ──► build repair(count++,feedback→generate)
|
||||||
|
│ ok │ fail ▲ repairCount<max? ─是
|
||||||
|
▼ └────────┴── 否 ─► giveup(终态失败 dump)
|
||||||
|
play(boot+截图+点击 = 能跑能玩)
|
||||||
|
│ pass │ fail ─► repair
|
||||||
|
▼
|
||||||
|
[player / nreview 质量门 · 生产保留 / spike 暂缓]
|
||||||
|
▼
|
||||||
|
emit(读 bundle → engineBundle → GamePackage,成功)
|
||||||
|
```
|
||||||
|
|
||||||
|
### §0.5 删除候选清单(D1=A · 执行时须过 §6.8 双评审 + grep 复核,本设计不删)
|
||||||
|
|
||||||
|
- **gamedef 表示层(核心删除)**:`game-runtime/src/host/gd-runtime.js`(声明式 runtime + `new Function` behavior)、`build-from-source.mjs`(JSON→factory 装配器)、二者 `*.test.mjs` —— A-model 无 gameDefinition、无 `new Function`,这一层整体退役。
|
||||||
|
- **factory(iife)路分支**:`SaaPrompts` 的 `SYSTEM`(iife 形状)与 `GAMEDEF_SYSTEM` 二选一 → 收敛为**单一 A-model system**;`buildGameDefMessages` / factory `buildRegenerateMessages` 按需清。
|
||||||
|
- **Python worker 全套**(D6=A,SAA 进程内已接替):`wg1/gen-worker/worker/{service,run,validate,prompt}.py`、`agent_loop/{studio,roles}.py`、`gamedef_quickcheck.py`。
|
||||||
|
- **死契约**:`contracts/prompts/`(SaaPrompts 已为 SSOT)、`contracts/agent-loop/`(Dify workflow,SAA 图接替)。
|
||||||
|
- **配置开关**:`AigcExecutorProperties.saaSourceMode`(factory|gamedef 二选一)→ 删,单一 A-model 路;`saaModelProtocol` 视 M3 走 openai+tools 可简化(thinking 节点如需另议)。
|
||||||
|
|
||||||
|
### §0.6 保留概念清单(重写但不丢的设计骨架)
|
||||||
|
|
||||||
|
16 节点图拓扑(generate 换芯)· repair 回环 · checkpoint 续跑(MysqlSaver)· observation 可观测 · **feedback 回喂闭环**(validate/build/play 失败皆回喂)· token 拆账(总量+per-model+per-attempt)· play 客观门(九门概念,spike 暂以"能跑能玩"代替)· **五法工厂契约**(`game-host.d.ts`:init/update/render/destroy/_forensicsView)· **受控面铁律**(time/random/引擎/输入皆经 `ctx`)· emit→GamePackage 作为对下游(studio/feed)**不变的契约边界**。
|
||||||
|
|
||||||
|
### §0.7 建设次序(spike 先证,production 后删 —— D1=A 无退路铁律)
|
||||||
|
|
||||||
|
1. **spike(独立 node harness · mini-desktop)**:按 L1→L2→L3→L4 建 agentic generate + validate + build + play(能跑能玩),用 **M3** 端到端跑通 catch-fruit 级别游戏。**不碰** SAA Java、**不删** 任何 gamedef 代码。
|
||||||
|
2. **production rewrite**:spike 过 → 把 agentic generate 节点端口进 `SaaStudioGraph`,按 §0.5 删 gamedef/factory,validate/play 换 A-model 版。**此步过 §6.8 Codex+Opus 双评审后才执行删除。**
|
||||||
|
3. **不过则**:补 skill/工具/context,**绝不先删退路**。
|
||||||
|
|
||||||
|
### §0.8 主流 agentic 对照 + 本设计定性(2026-06-21 评审确认)
|
||||||
|
|
||||||
|
**基准**:Anthropic《Building Effective Agents》《Effective context engineering for AI agents》《Writing effective tools for agents》、SWE-agent 的 **ACI(Agent-Computer Interface)**、ReAct。
|
||||||
|
|
||||||
|
- **四支柱对照**:主流 = 环境/工具(ACI)· agent 循环(ReAct)· context 工程 · 记忆。本设计:L1 + L2 工具 = 环境/ACI(✅ 最小工具集、沙箱最小权限);L2 = ReAct 循环(✅,M3 已实测发 tool_calls);L3 + L4 = context 工程(⚠️ 主流把 prompt 视为 context 子项;本设计尚缺"长循环压缩");记忆 = ❌ 暂无(单局生成可弱化)。
|
||||||
|
- **定性(Anthropic taxonomy)**:整张 SAA 图 = **orchestrator workflow**;generate 节点 = **内嵌 agentic(ReAct)**;repair 回环 = **evaluator-optimizer**。这是 Anthropic 明确推荐用于**可预测生产管线**的组合("要可预测用 workflow,全自治留给真需要灵活性处")。本设计属**受约束 agentic 流派**——主流且被推荐,非缺陷。
|
||||||
|
- **已采纳的关键修正**:把验证从"循环外"补一半进"循环内"——给 agent `check` / `build` 工具 + **done 前自检门**(§0.3)。理由:便宜模型最常死在它**看不见的语法/API 错**;in-loop 编译反馈是主流让弱模型成功的**头号技巧**,直接服务 spike 目标;慢的真玩仍留循环外作权威门。
|
||||||
|
- **按需补的 3 缺口**(非 spike 阻塞):① agent scratchpad/todo(外化计划,弱模型受益);② 长循环 context 压缩(文件累积撑爆时:压缩/子代理/清旧 tool 结果;M3 上下文大可暂缓但要有上限保护);③ 停机:`max-steps` 已有,补 loop-detection + token/预算上限。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L1 · Environment(环境 / 沙箱)—— 最先建
|
||||||
|
|
||||||
|
**目标**:干净、可复现、隔离的运行沙箱;生成 + build + play 都在里面,跑崩不污染 repo/live。
|
||||||
|
|
||||||
|
| 关键决策 | 方案 |
|
||||||
|
|---|---|
|
||||||
|
| 在哪跑 | **mini-desktop(100.64.0.7)**:node22 + Chrome146 + java17 + new-api 内网可达(已验);ssh 直连 `ssh -o ProxyCommand=none root@100.64.0.7` + Bash `dangerouslyDisableSandbox`(别名→死代理 198.18.0.7) |
|
||||||
|
| 隔离单元 | 每 run 一个 **git worktree 或 Docker 容器**(repo 副本 + 锁分支 `feat/mac-amodel-foundation`);轻量起步可先「scoped 目录 + 端口隔离」 |
|
||||||
|
| 文件边界 | agent 的 **write 工具只许写本 run 的 game 目录**(`games/_spike-<id>/`);read = repo 只读;**不给 agent 任意 shell** |
|
||||||
|
| 命令边界 | build(esbuild)/ play(Chrome 九门/或 boot+截图)由 **harness 跑,非 agent**;端口错开 live :48080(4320/9222) |
|
||||||
|
| 凭据 | new-api key + base 经 env 注入(`docs/内网凭据与端点.md`) |
|
||||||
|
| 可观测 | 全程一个 run 日志(agent 每步工具调用 + build + play),founder 可 tail;SAA app 日志 = `/root/game-staging/app.log` |
|
||||||
|
|
||||||
|
**依赖**:无(最底层)。**先建好它,后面才有地方跑。**
|
||||||
|
|
||||||
|
**✅ L1 实测结论(2026-06-21,mini-desktop)**:用已知可玩的 **catch-fruit 当控制**——build(esbuild,220KB,引擎 inline)+ boot + play(真引擎、水果下落、HUD 活、截图确认)**全通** → build/play 基础设施对 A-model 游戏成立。recipe 已得。
|
||||||
|
|
||||||
|
**🔑 harness 形态决策(L1→L2 关键,基于 recipe)**:
|
||||||
|
- **目录**:run 落 `games/amgen-<id>/`(**直接在 `games/` 下**,与 catch-fruit 同深度 → 插件 import 用 `../../../src/`,**与 catch-fruit 范例逐字一致,根除路径深度坑**);gitignore。
|
||||||
|
- **形态 = catch-fruit shape(多文件 `src/`)**,**不用** `_wg1-gen` 旧的单文件 `generated-factory.js`(那是 factory 单文件路,与 A-model 多文件矛盾;且嵌进 `_wg1-gen/<id>/src` 会变 4-up 触发深度坑)。
|
||||||
|
- **固定 plumbing(harness 写,不让 agent 碰)**:`index.html` + `entry.js` + `src/main.js`,**固定全局名 `__AModelGame` + 固定 boot 标记**(`__gameBooted`/`__gameHost`/`__gameForensics`);**agent 只产** `src/{host-config,game,core,render,balance,assets}.js`,约定 `host-config.js` 导出**固定名 `buildHostConfig`**、`game.js` 默认导出工厂、`update(dt)` 内调 `opts.runtime.bundle.tick(dt)`。
|
||||||
|
- **play = 轻量 CDP**(serve + headless Chrome boot + 截图 + 一次 tap + 读 `__gameForensics` 状态 = **能跑能玩**),**不复用** `serve-and-play.sh` 九门(founder 已弃九门;且它硬编码 `_wg1-gen` 布局)。
|
||||||
|
- **build**(locked esbuild,不改):`node scripts/build.mjs games/amgen-<id>/entry.js games/amgen-<id>/dist/bundle.js --global-name=__AModelGame`(cwd=`game-runtime`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L2 · Harness(编排循环 + 工具框架)
|
||||||
|
|
||||||
|
**目标**:确定性控制器,驱动「agentic 生成 → 装配 → build → play → verdict」,并定义 agent 的工具。**这就是项目延期的 ReAct 生成节点的 spike 版**;验通后端口进 SAA `generate` 节点。
|
||||||
|
|
||||||
|
| 关键决策 | 方案 |
|
||||||
|
|---|---|
|
||||||
|
| **工具集(给 agent)** | `read_file`(repo 只读)、`write_file`(scoped run 目录)、`list_dir`、**`check`**(`node --check` × src/*.js + 五法/host-config/插件键静态 lint)、**`build`**(esbuild,秒级);**无任意 shell**。经 new-api function-calling 暴露(**M3 已实测支持**) |
|
||||||
|
| ✅ 可行性(已验证) | **M3 经 new-api(openai 协议)发 tool_calls 干净且快**(1.8s,reasoning=0,并行 2 调用,参数正确)——agentic ReAct 成立,无需回退伪 agentic |
|
||||||
|
| agent 循环 | system+brief → M3 发 tool_calls → harness 执行(读/写/check/build)→ 回结果 → 循环至 agent done **或** max-steps;**done 门:check + build 必须已绿**;越界/读不到/check 失败 → 回喂纠错 + loop-detection 防卡死 |
|
||||||
|
| 装配(固定 plumbing) | run 目录预置:`entry-bundle.js`(暴露 `__GameBundle.bootGameHost`,**用 per-game host-config 而非 generic 壳**)+ `index.html`(play 模板)+ `src/assets.js` stub;**深度 = `games/<id>/`(治路径 bug:import 插件用 `../../../src/plugins`)**;agent 只产 `src/{core,render,balance,game,host-config}.js` |
|
||||||
|
| 收口(循环外权威门) | agent done(已自 build 绿)→ **play**:serve + 无头 Chrome boot+截图+点击(bar=**能跑能玩**,暂丢九门)→ 存 verdict/截图/日志;fail → repair 回 generate |
|
||||||
|
| 多 run | 循环 briefs(模型固定 M3)→ 汇总过率/失败模式 |
|
||||||
|
|
||||||
|
**依赖**:L1(先有沙箱才能定工具边界)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L3 · Context 拼装(agent 读什么 / 怎么读)
|
||||||
|
|
||||||
|
**目标**:让 agent 拿到**充足但不过量**的 context(治上一轮"skill 没加载")。**agentic 让 skill 的指针生效**(agent 按需读文件)。
|
||||||
|
|
||||||
|
| 关键决策 | 方案 |
|
||||||
|
|---|---|
|
||||||
|
| 两阶段 | **design 阶段**(读 `sim-business-game-design.md` → brief 变具体玩法设计:核心循环/机制/解锁/数值/插件清单/反无趣自检)→ **code 阶段**(读 `littlejs-game-dev.md` + 选用插件 api.d.ts + catch-fruit 范例 → 写多文件实现该设计) |
|
||||||
|
| 按需读、别全读 | skill 指引"用 X 插件就读 `src/plugins/X/api.d.ts`";一份完整范例(catch-fruit)即可,不读全 12;控 token |
|
||||||
|
| 必读锚点(prompt 点名) | ① 两份 skill ② `game-host.d.ts`(工厂五法契约)③ `games/catch-fruit/`(唯一完整范例)④ 用到插件的 `api.d.ts` |
|
||||||
|
| skill 生成可用度 | 已是指针式,**agentic 下够用**(agent 能读到指向的文件);若 agent 反复踩同一坑(如某 API),再把那条 inline 进 skill |
|
||||||
|
|
||||||
|
**依赖**:L2(先有 read_file,agent 才能读)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L4 · Prompt(薄指令层)—— 最后写
|
||||||
|
|
||||||
|
**目标**:最薄的指令,指挥 agent 用好 L1-L3。**绝不手抄 skill**(上一轮的错)——指 agent 去读真 skill。
|
||||||
|
|
||||||
|
**system prompt 骨架**:
|
||||||
|
1. **角色**:你是 A-model 游戏生成 agent;产物 = 本 run 目录 `src/` 下多文件 LittleJS 游戏。
|
||||||
|
2. **步骤**:读 skill(你的 manual)→ design(读 design skill)→ 读用到插件 api.d.ts + catch-fruit 范例 → 写 `src/` → 自检。
|
||||||
|
3. **工具**:read_file / write_file(scoped)/ list_dir。
|
||||||
|
4. **红线(精炼)**:工厂默认导出 + 零引擎 import;零裸 `Date/Math/setTimeout`;render 画在 g;timer 经 timer-scheduler + update 内 `runtime.bundle.tick(dt)`;host-config 导出 `buildHostConfig` 返 `{factory,plugins,registerOrder,viewport,buildFactoryOpts}`;插件键名与 game.js 解构一致。
|
||||||
|
5. **完成判据**:harness build 过 + 能 boot + 能玩。
|
||||||
|
|
||||||
|
**依赖**:L1+L2+L3 全定好。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 建设顺序(= 创始人定)
|
||||||
|
|
||||||
|
**L1 环境 → L2 harness → L3 context 拼装 → L4 prompt**,每层验通再下一层。验证策略:先用**能力够的模型**把 agentic 端到端跑通(确认 harness/工具/context 对)→ 再逐级换便宜模型测经济性(bar=能跑能玩)。**过 = 才动 SAA 删 factory/gamedef(D1=A 无退路);不过 = 补 skill/工具,绝不先删退路。**
|
||||||
200
game-runtime/tools/amodel-gen/gen.mjs
Normal file
200
game-runtime/tools/amodel-gen/gen.mjs
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* gen.mjs — agentic A-model 生成 orchestrator(L2 ReAct 循环 · spike)
|
||||||
|
* owner:amodel-gen spike
|
||||||
|
*
|
||||||
|
* 流程:scaffold(_template→amgen-<id>) → ReAct(M3 + 5 工具,done 门=check+build 绿)
|
||||||
|
* → play(循环外·权威门) → 落 verdict / run-log / run-summary。
|
||||||
|
*
|
||||||
|
* 用法(cwd=game-runtime,mini-desktop):
|
||||||
|
* node tools/amodel-gen/gen.mjs --id run1 --brief "挡板接球打砖块,清屏即胜" [--max-steps 40] [--port 4320] [--cdp 9222]
|
||||||
|
*
|
||||||
|
* 注:本文件是 harness 侧,允许 Date.now/setTimeout(红线只约束游戏 src)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { scaffold, readFileTool, writeFileTool, listDirTool, check, build, gameDir } from './tools.mjs';
|
||||||
|
import { chat, MODEL } from './m3.mjs';
|
||||||
|
import { TOOLS, buildSystemPrompt } from './prompt.mjs';
|
||||||
|
import { play } from './play.mjs';
|
||||||
|
|
||||||
|
function arg(name, def) {
|
||||||
|
const i = process.argv.indexOf(`--${name}`);
|
||||||
|
return i >= 0 ? process.argv[i + 1] : def;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_RESULT_CAP = 30000; // 单次工具结果注入上限(够一份 skill/api.d.ts,控 context)
|
||||||
|
|
||||||
|
/** 历史压缩:折叠旧的大 tool 结果 / write 内容,控 token(主流 context 工程:循环越长越要压)。占位 <400 字符 → 幂等。 */
|
||||||
|
function compactHistory(messages, keepLast = 8) {
|
||||||
|
const head = 2; // system + 首个 user-brief 保全
|
||||||
|
const tailStart = Math.max(head, messages.length - keepLast);
|
||||||
|
for (let i = head; i < tailStart; i++) {
|
||||||
|
const m = messages[i];
|
||||||
|
if (!m) continue;
|
||||||
|
if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 400) {
|
||||||
|
m.content = '[旧工具结果已折叠省 token;需要可重新 read_file]';
|
||||||
|
}
|
||||||
|
if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
|
||||||
|
for (const c of m.tool_calls) {
|
||||||
|
if (c.function && c.function.name === 'write_file' && c.function.arguments && c.function.arguments.length > 400) {
|
||||||
|
try {
|
||||||
|
const a = JSON.parse(c.function.arguments);
|
||||||
|
c.function.arguments = JSON.stringify({ path: a.path, content: '[旧 write 内容已折叠;文件在磁盘,需要可重新 read_file]' });
|
||||||
|
} catch { /* 坏 JSON 略过 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const id = arg('id', 'run1');
|
||||||
|
const brief = arg('brief', '一个简单的点击得分小游戏');
|
||||||
|
const maxSteps = Number(arg('max-steps', '40'));
|
||||||
|
const port = Number(arg('port', '4320'));
|
||||||
|
const cdpPort = Number(arg('cdp', '9222'));
|
||||||
|
|
||||||
|
const log = [];
|
||||||
|
const t0 = Date.now();
|
||||||
|
const rec = (m) => {
|
||||||
|
const line = `[+${((Date.now() - t0) / 1000).toFixed(1)}s] ${m}`;
|
||||||
|
console.log(line);
|
||||||
|
log.push(line);
|
||||||
|
};
|
||||||
|
|
||||||
|
rec(`model=${MODEL} id=${id} brief=「${brief}」maxSteps=${maxSteps}`);
|
||||||
|
rec(`scaffold amgen-${id}(clone _template)`);
|
||||||
|
scaffold(id);
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{ role: 'system', content: buildSystemPrompt(id) },
|
||||||
|
{ role: 'user', content: `请按这个 brief 造一款游戏:「${brief}」。先读手册再动手。` },
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastCheck = null;
|
||||||
|
let lastBuild = null;
|
||||||
|
let doneSummary = null;
|
||||||
|
let finished = false;
|
||||||
|
let nudgedDone = false;
|
||||||
|
let steps = 0;
|
||||||
|
let totalTokens = 0;
|
||||||
|
const callSig = new Map(); // loop-detection:同一 (name,args) 计数
|
||||||
|
|
||||||
|
while (!finished && steps < maxSteps) {
|
||||||
|
steps++;
|
||||||
|
compactHistory(messages); // 每轮先压旧大块,控 token
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await chat(messages, TOOLS);
|
||||||
|
} catch (e) {
|
||||||
|
rec(`step${steps} M3 调用失败: ${e.message}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
totalTokens += (resp.usage && resp.usage.total_tokens) || 0;
|
||||||
|
const msg = resp.message;
|
||||||
|
messages.push(msg);
|
||||||
|
const calls = msg.tool_calls || [];
|
||||||
|
|
||||||
|
if (calls.length === 0) {
|
||||||
|
rec(`step${steps}: 无 tool_calls(finish=${resp.finishReason}; content="${(msg.content || '').slice(0, 80)}")`);
|
||||||
|
messages.push({ role: 'user', content: '请用工具继续(read_file/write_file/check/build);全绿后调 done。' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of calls) {
|
||||||
|
const name = c.function.name;
|
||||||
|
let a = {};
|
||||||
|
try { a = JSON.parse(c.function.arguments || '{}'); } catch { /* 容错:坏 JSON 当空参 */ }
|
||||||
|
const sig = name + ':' + (c.function.arguments || '');
|
||||||
|
callSig.set(sig, (callSig.get(sig) || 0) + 1);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (name === 'read_file') {
|
||||||
|
const r = readFileTool(a.path);
|
||||||
|
result = r.ok ? (r.truncated ? '[内容已截断]\n' : '') + r.content : 'ERROR: ' + r.error;
|
||||||
|
rec(`read ${a.path} ${r.ok ? 'ok' : 'ERR ' + r.error}`);
|
||||||
|
} else if (name === 'list_dir') {
|
||||||
|
const r = listDirTool(a.path);
|
||||||
|
result = r.ok ? r.entries.join('\n') : 'ERROR: ' + r.error;
|
||||||
|
rec(`list ${a.path} ${r.ok ? 'ok' : 'ERR'}`);
|
||||||
|
} else if (name === 'write_file') {
|
||||||
|
const r = writeFileTool(id, a.path, a.content || '');
|
||||||
|
result = r.ok ? `OK ${r.bytes}B 已写` : 'ERROR: ' + r.error;
|
||||||
|
rec(`write ${a.path} ${r.ok ? r.bytes + 'B' : 'ERR ' + r.error}`);
|
||||||
|
} else if (name === 'check') {
|
||||||
|
lastCheck = check(id);
|
||||||
|
result = lastCheck.ok ? 'PASS' : 'FAIL:\n' + lastCheck.errors.join('\n');
|
||||||
|
rec(`check ${lastCheck.ok ? 'PASS' : 'FAIL(' + lastCheck.errors.length + ')'}`);
|
||||||
|
} else if (name === 'build') {
|
||||||
|
lastBuild = build(id);
|
||||||
|
result = lastBuild.ok ? 'PASS' : 'FAIL:\n' + lastBuild.error;
|
||||||
|
rec(`build ${lastBuild.ok ? 'PASS' : 'FAIL'}`);
|
||||||
|
} else if (name === 'done') {
|
||||||
|
// done 门:复核 check+build 必须都绿,否则拒绝并回喂
|
||||||
|
const c2 = check(id);
|
||||||
|
const b2 = c2.ok ? build(id) : { ok: false, error: 'check 未过,先修 check' };
|
||||||
|
lastCheck = c2; if (c2.ok) lastBuild = b2;
|
||||||
|
if (c2.ok && b2.ok) {
|
||||||
|
doneSummary = a.summary || '';
|
||||||
|
result = 'DONE 已接受';
|
||||||
|
rec(`done 接受: ${doneSummary}`);
|
||||||
|
messages.push({ role: 'tool', tool_call_id: c.id, content: result });
|
||||||
|
finished = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result = `DONE 被拒:done 前 check+build 必须都绿。check=${c2.ok ? 'PASS' : 'FAIL: ' + c2.errors.join('; ')} build=${b2.ok ? 'PASS' : 'FAIL: ' + (b2.error || '')}`;
|
||||||
|
rec('done 被拒(check/build 未全绿)');
|
||||||
|
} else {
|
||||||
|
result = 'ERROR: 未知工具 ' + name;
|
||||||
|
}
|
||||||
|
messages.push({ role: 'tool', tool_call_id: c.id, content: String(result).slice(0, TOOL_RESULT_CAP) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// check+build 都绿 → 一次性强提醒 done(治"绿了还乱写测试/额外文件")
|
||||||
|
if (!finished && !nudgedDone && lastCheck && lastCheck.ok && lastBuild && lastBuild.ok) {
|
||||||
|
messages.push({ role: 'user', content: 'check 与 build 都已 PASS。若已实现 brief 的核心玩法,请立即调 done(summary 一句话);不要再写测试或额外文件。' });
|
||||||
|
nudgedDone = true;
|
||||||
|
rec('已提示 done(check+build 绿)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop-detection:同一调用重复过多 → 截停(防卡死烧 token)
|
||||||
|
if ([...callSig.values()].some((n) => n > 6)) {
|
||||||
|
rec('loop-detection:同一工具调用重复过多,截停');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 循环外·权威门:play(仅在 agent 合格 done 后跑)
|
||||||
|
let verdict = null;
|
||||||
|
if (finished) {
|
||||||
|
rec('play(循环外·权威门:boot+截图+交互 = 能跑能玩)...');
|
||||||
|
try {
|
||||||
|
verdict = await play(id, { port, cdpPort });
|
||||||
|
rec(`play ${verdict.pass ? 'PASS(能跑)' : 'FAIL'} — ${verdict.notes.join(' | ')}`);
|
||||||
|
} catch (e) {
|
||||||
|
rec('play 异常: ' + (e.message || e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rec('未合格 done,跳过 play(本 run 未产出可玩游戏)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
id, brief, model: MODEL, steps, finished, doneSummary,
|
||||||
|
check: lastCheck ? { ok: lastCheck.ok, errors: lastCheck.errors } : null,
|
||||||
|
build: lastBuild ? { ok: lastBuild.ok } : null,
|
||||||
|
verdict: verdict ? { pass: verdict.pass, notes: verdict.notes, error: verdict.error || null } : null,
|
||||||
|
approxTokens: totalTokens,
|
||||||
|
wallSec: Number(((Date.now() - t0) / 1000).toFixed(1)),
|
||||||
|
};
|
||||||
|
const evDir = join(gameDir(id), 'evidence');
|
||||||
|
mkdirSync(evDir, { recursive: true });
|
||||||
|
writeFileSync(join(evDir, 'run-log.txt'), log.join('\n'));
|
||||||
|
writeFileSync(join(evDir, 'run-summary.json'), JSON.stringify(summary, null, 2));
|
||||||
|
console.log('\n=== RUN SUMMARY ===');
|
||||||
|
console.log(JSON.stringify(summary, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error('gen.mjs 顶层异常:', e); process.exit(1); });
|
||||||
75
game-runtime/tools/amodel-gen/m3.mjs
Normal file
75
game-runtime/tools/amodel-gen/m3.mjs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* m3.mjs — new-api M3 客户端(openai 协议 + function-calling)
|
||||||
|
* owner:amodel-gen spike | 消费方:gen.mjs(ReAct 循环每轮调 chat)
|
||||||
|
*
|
||||||
|
* 单一职责:发一轮 chat/completions(带 tools)→ 返 {message, usage, finishReason}。
|
||||||
|
* 含超时 + 重试(429/5xx/IO 重试;4xx 不重试)。模型固定 MiniMax-M3(founder 定)。
|
||||||
|
* 密钥(§9):优先 env NEWAPI_KEY,否则从 docs/内网凭据与端点.md(内网允许入仓)解析——doc 是单一事实源。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { REPO_ROOT } from './tools.mjs';
|
||||||
|
|
||||||
|
const BASE = (process.env.NEWAPI_BASE_URL || 'http://100.64.0.8:3000').replace(/\/+$/, '');
|
||||||
|
const MODEL = process.env.AMGEN_MODEL || 'MiniMax-M3'; // founder:只用 M3
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
/** 取网关 token:env 优先,否则解析项目密钥文档(§9 单一事实源)。 */
|
||||||
|
function getKey() {
|
||||||
|
if (process.env.NEWAPI_KEY) return process.env.NEWAPI_KEY;
|
||||||
|
try {
|
||||||
|
const doc = readFileSync(join(REPO_ROOT, 'docs/内网凭据与端点.md'), 'utf8');
|
||||||
|
const m = doc.match(/NEWAPI_KEY[^\n]*?(sk-[A-Za-z0-9]+)/);
|
||||||
|
if (m) return m[1];
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
throw new Error('找不到 NEWAPI_KEY(请设 env 或确认 docs/内网凭据与端点.md 存在)');
|
||||||
|
}
|
||||||
|
const KEY = getKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发一轮带 tools 的 chat/completions。
|
||||||
|
* @param {Array<object>} messages openai 消息数组(含 system/user/assistant/tool)
|
||||||
|
* @param {Array<object>} tools function-calling 工具定义
|
||||||
|
* @param {{maxTokens?:number, temperature?:number, timeoutMs?:number}} [opt]
|
||||||
|
* @returns {Promise<{message:object, usage:object, finishReason:string}>}
|
||||||
|
*/
|
||||||
|
export async function chat(messages, tools, opt = {}) {
|
||||||
|
const { maxTokens = 16000, temperature = 0.3, timeoutMs = 180000 } = opt;
|
||||||
|
const body = { model: MODEL, messages, tools, tool_choice: 'auto', max_tokens: maxTokens, temperature };
|
||||||
|
let lastErr;
|
||||||
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${BASE}/v1/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: ctrl.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (!r.ok) {
|
||||||
|
const txt = await r.text().catch(() => '');
|
||||||
|
// 4xx(除 429)= 请求本身错,不重试,直接抛
|
||||||
|
if (r.status !== 429 && r.status < 500) throw new Error(`HTTP ${r.status}: ${txt.slice(0, 300)}`);
|
||||||
|
lastErr = new Error(`HTTP ${r.status}: ${txt.slice(0, 200)}`);
|
||||||
|
await sleep(1500 * (attempt + 1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const d = await r.json();
|
||||||
|
const ch = d.choices && d.choices[0];
|
||||||
|
if (!ch) throw new Error('响应无 choices: ' + JSON.stringify(d).slice(0, 300));
|
||||||
|
return { message: ch.message, usage: d.usage || {}, finishReason: ch.finish_reason };
|
||||||
|
} catch (e) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
lastErr = e;
|
||||||
|
await sleep(1500 * (attempt + 1)); // 超时/IO 重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr || new Error('M3 chat 失败(重试耗尽)');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MODEL, BASE };
|
||||||
189
game-runtime/tools/amodel-gen/play.mjs
Normal file
189
game-runtime/tools/amodel-gen/play.mjs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* play.mjs — 循环外·权威门:serve + headless Chrome boot + 帧推进 + 截图 + 一次交互 = 能跑能玩
|
||||||
|
* owner:amodel-gen spike | 消费方:gen.mjs(agent done 后跑) / CLI 独立验证
|
||||||
|
*
|
||||||
|
* 【bar = 能跑能玩(非九门)】
|
||||||
|
* 能跑 = booted(无 boot error)+ 真引擎掌帧推进(frameCount 前进);
|
||||||
|
* 能玩 = do(start)+tap 后状态有 phase(非 booting),且截图存证(供人眼最终确认);
|
||||||
|
* 截图落 games/amgen-<id>/evidence/{first-paint,after-play}.png + verdict.json。
|
||||||
|
*
|
||||||
|
* 【CDP】Node22 全局 WebSocket + fetch,无需 ws 包。Chrome --headless=new --remote-debugging-port。
|
||||||
|
* 注:本文件是 harness 侧(非游戏代码),允许用 Date.now/setTimeout(红线只约束游戏 src)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { GAME_RUNTIME, gameDir } from './tools.mjs';
|
||||||
|
|
||||||
|
const CHROME = process.env.CHROME_BIN || '/usr/bin/google-chrome';
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
/** 起静态服务器(game-runtime 根)。 */
|
||||||
|
function startServer(port) {
|
||||||
|
return spawn('node', ['test/harness/static-serve.cjs', '.', String(port)], {
|
||||||
|
cwd: GAME_RUNTIME, stdio: 'ignore',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 起 headless Chrome(端口避开 live;独立 user-data 防串)。 */
|
||||||
|
function startChrome(cdpPort, userDataDir) {
|
||||||
|
return spawn(CHROME, [
|
||||||
|
'--headless=new', '--no-sandbox', '--no-zygote', '--disable-gpu',
|
||||||
|
`--remote-debugging-port=${cdpPort}`, '--remote-allow-origins=*',
|
||||||
|
'--autoplay-policy=no-user-gesture-required',
|
||||||
|
`--user-data-dir=${userDataDir}`, 'about:blank',
|
||||||
|
], { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 轮询 CDP /json,拿一个 page target 的 ws 调试 url。 */
|
||||||
|
async function getWsUrl(cdpPort, timeoutMs) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`http://localhost:${cdpPort}/json`);
|
||||||
|
const list = await r.json();
|
||||||
|
const page = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl);
|
||||||
|
if (page) return page.webSocketDebuggerUrl;
|
||||||
|
} catch { /* chrome 还没起好,继续轮询 */ }
|
||||||
|
await sleep(300);
|
||||||
|
}
|
||||||
|
throw new Error('CDP 未就绪(chrome 起失败?)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 极简 CDP 客户端(全局 WebSocket)。 */
|
||||||
|
class CDP {
|
||||||
|
constructor(ws) {
|
||||||
|
this.ws = ws;
|
||||||
|
this.id = 0;
|
||||||
|
this.pending = new Map();
|
||||||
|
ws.addEventListener('message', (ev) => {
|
||||||
|
let msg;
|
||||||
|
try { msg = JSON.parse(ev.data); } catch { return; }
|
||||||
|
if (msg.id && this.pending.has(msg.id)) {
|
||||||
|
const { resolve: res, reject } = this.pending.get(msg.id);
|
||||||
|
this.pending.delete(msg.id);
|
||||||
|
msg.error ? reject(new Error(JSON.stringify(msg.error))) : res(msg.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
send(method, params) {
|
||||||
|
const id = ++this.id;
|
||||||
|
return new Promise((res, reject) => {
|
||||||
|
this.pending.set(id, { resolve: res, reject });
|
||||||
|
this.ws.send(JSON.stringify({ id, method, params: params || {} }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** 在页面里求值(returnByValue);异常透出。 */
|
||||||
|
async evalJs(expression, awaitPromise = false) {
|
||||||
|
const r = await this.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise });
|
||||||
|
if (r.exceptionDetails) {
|
||||||
|
throw new Error('页面 eval 异常: ' + (r.exceptionDetails.text || JSON.stringify(r.exceptionDetails)));
|
||||||
|
}
|
||||||
|
return r.result ? r.result.value : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectCDP(wsUrl) {
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
ws.addEventListener('open', res, { once: true });
|
||||||
|
ws.addEventListener('error', () => rej(new Error('ws 连接失败')), { once: true });
|
||||||
|
});
|
||||||
|
return new CDP(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真玩验证。返回 verdict;截图 + verdict.json 落 evidence/。
|
||||||
|
* @param {string} id
|
||||||
|
* @param {{port?:number, cdpPort?:number}} [opts]
|
||||||
|
*/
|
||||||
|
export async function play(id, opts = {}) {
|
||||||
|
const port = opts.port || 4320;
|
||||||
|
const cdpPort = opts.cdpPort || 9222;
|
||||||
|
const url = `http://localhost:${port}/games/amgen-${id}/index.html?engine=real&seed=12345`;
|
||||||
|
const evDir = join(gameDir(id), 'evidence');
|
||||||
|
mkdirSync(evDir, { recursive: true });
|
||||||
|
const userDataDir = `/tmp/amgen-chrome-${id}`;
|
||||||
|
|
||||||
|
const verdict = { id, url, pass: false, steps: {}, notes: [] };
|
||||||
|
const server = startServer(port);
|
||||||
|
const chrome = startChrome(cdpPort, userDataDir);
|
||||||
|
let cdp;
|
||||||
|
try {
|
||||||
|
await sleep(900); // 等 server + chrome 起
|
||||||
|
const wsUrl = await getWsUrl(cdpPort, 15000);
|
||||||
|
cdp = await connectCDP(wsUrl);
|
||||||
|
await cdp.send('Page.enable');
|
||||||
|
await cdp.send('Runtime.enable');
|
||||||
|
await cdp.send('Page.navigate', { url });
|
||||||
|
|
||||||
|
// 1) 等 boot(__gameBooted=true 或 __gameBootError)
|
||||||
|
let booted = false, bootErr = null;
|
||||||
|
const bootDeadline = Date.now() + 30000;
|
||||||
|
while (Date.now() < bootDeadline) {
|
||||||
|
const st = await cdp.evalJs(
|
||||||
|
'({b: window.__gameBooted, e: window.__gameBootError, ei: window.__gameHostEngineInitFired})',
|
||||||
|
).catch(() => null);
|
||||||
|
if (st && st.b === true) { booted = true; verdict.steps.engineInit = !!st.ei; break; }
|
||||||
|
if (st && st.e) { bootErr = st.e; break; }
|
||||||
|
await sleep(400);
|
||||||
|
}
|
||||||
|
verdict.steps.boot = { booted, bootErr };
|
||||||
|
if (!booted) { verdict.notes.push('boot 失败:' + (bootErr || '超时(30s)')); throw new Error('boot 失败'); }
|
||||||
|
|
||||||
|
// 2) 帧推进(真引擎掌帧)
|
||||||
|
const f0 = await cdp.evalJs('window.__gameHost ? window.__gameHost.frameCount() : -1');
|
||||||
|
await sleep(1000);
|
||||||
|
const f1 = await cdp.evalJs('window.__gameHost ? window.__gameHost.frameCount() : -1');
|
||||||
|
verdict.steps.frames = { f0, f1, advanced: typeof f1 === 'number' && f1 > f0 };
|
||||||
|
|
||||||
|
// 3) 首屏截图
|
||||||
|
const shot1 = await cdp.send('Page.captureScreenshot', { format: 'png' });
|
||||||
|
writeFileSync(join(evDir, 'first-paint.png'), Buffer.from(shot1.data, 'base64'));
|
||||||
|
|
||||||
|
// 4) do(start) 进 play + 一次 tap(do 形态两款游戏不一,两种都试)
|
||||||
|
const st0 = await cdp.evalJs('window.__gameHost ? window.__gameHost.state() : null');
|
||||||
|
await cdp.evalJs(`(()=>{const h=window.__gameHost; if(!h||!h.do) return; try{h.do({type:'start'})}catch(e){} try{h.do('start')}catch(e){}})()`).catch(() => {});
|
||||||
|
await sleep(300);
|
||||||
|
await cdp.evalJs('window.__gameHost && window.__gameHost.tap && window.__gameHost.tap(195, 500)').catch(() => {});
|
||||||
|
await sleep(1200);
|
||||||
|
const st1 = await cdp.evalJs('window.__gameHost ? window.__gameHost.state() : null');
|
||||||
|
verdict.steps.play = { st0, st1 };
|
||||||
|
|
||||||
|
// 5) 玩后截图
|
||||||
|
const shot2 = await cdp.send('Page.captureScreenshot', { format: 'png' });
|
||||||
|
writeFileSync(join(evDir, 'after-play.png'), Buffer.from(shot2.data, 'base64'));
|
||||||
|
|
||||||
|
// 判定:能跑 = booted + 帧推进;phase 进展供"能玩"参考(人眼看截图最终拍板)
|
||||||
|
const phase1 = st1 && st1.phase;
|
||||||
|
verdict.pass = booted && verdict.steps.frames.advanced;
|
||||||
|
verdict.notes.push(`booted=${booted} engineInit=${verdict.steps.engineInit} framesAdvanced=${verdict.steps.frames.advanced}(${f0}→${f1}) phase=${phase1}`);
|
||||||
|
} catch (e) {
|
||||||
|
verdict.error = String((e && e.message) || e);
|
||||||
|
} finally {
|
||||||
|
try { cdp && cdp.ws.close(); } catch {}
|
||||||
|
try { chrome.kill('SIGKILL'); } catch {}
|
||||||
|
try { server.kill('SIGKILL'); } catch {}
|
||||||
|
}
|
||||||
|
writeFileSync(join(evDir, 'verdict.json'), JSON.stringify(verdict, null, 2));
|
||||||
|
return verdict;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────── CLI ─────────────────────────
|
||||||
|
// 用法:node play.mjs <id> [port] [cdpPort]
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
const [, , id, port, cdp] = process.argv;
|
||||||
|
if (!id) { console.error('用法:node play.mjs <id> [port] [cdpPort]'); process.exit(2); }
|
||||||
|
play(id, { port: port ? Number(port) : undefined, cdpPort: cdp ? Number(cdp) : undefined })
|
||||||
|
.then((v) => {
|
||||||
|
console.log('[play]', v.pass ? 'PASS(能跑)' : 'FAIL', '—', v.notes.join(' | '));
|
||||||
|
if (v.error) console.log(' error:', v.error);
|
||||||
|
console.log(' evidence:', join(gameDir(id), 'evidence'));
|
||||||
|
process.exit(v.pass ? 0 : 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
94
game-runtime/tools/amodel-gen/prompt.mjs
Normal file
94
game-runtime/tools/amodel-gen/prompt.mjs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* prompt.mjs — L4 薄 system prompt + 工具定义(openai function-calling schema)
|
||||||
|
* owner:amodel-gen spike | 消费方:gen.mjs
|
||||||
|
*
|
||||||
|
* 铁律(上一轮翻车教训):**绝不手抄 skill** —— 只给薄指令 + 指 agent 自己 read_file 读真 skill/范例。
|
||||||
|
* L3 context = agent 经 read_file 按需自取(skill→插件 api.d.ts→catch-fruit 范例),不预塞。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** 给 agent 的工具(openai function-calling)。check/build 是循环内快反馈;done 有自检门。 */
|
||||||
|
export const TOOLS = [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'read_file',
|
||||||
|
description: '读 repo 内任意文件(只读)。用它读 skill、插件 api.d.ts、catch-fruit/_template 范例。先读手册再写码。',
|
||||||
|
parameters: { type: 'object', properties: { path: { type: 'string', description: 'repo 相对路径,如 .agents/skills/littlejs-game-dev.md' } }, required: ['path'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'list_dir',
|
||||||
|
description: '列目录(只读),用于发现文件。',
|
||||||
|
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'write_file',
|
||||||
|
description: '写文件到本 run 的 game 目录(仅许写 games/amgen-<id>/ 内)。整文件覆盖。',
|
||||||
|
parameters: { type: 'object', properties: { path: { type: 'string', description: '如 games/amgen-<id>/src/game.js' }, content: { type: 'string', description: '完整文件内容' } }, required: ['path', 'content'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'check',
|
||||||
|
description: '循环内快校验:node --check 各 src/*.js + 五法/导出名/红线 lint。返回 PASS 或错误清单。done 前必须 PASS。',
|
||||||
|
parameters: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'build',
|
||||||
|
description: '循环内 esbuild 打包(秒级)。返回 PASS 或编译错误。done 前必须 PASS。',
|
||||||
|
parameters: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'done',
|
||||||
|
description: '声明完成。仅当 check 与 build 都 PASS 才会被接受(harness 会复核)。',
|
||||||
|
parameters: { type: 'object', properties: { summary: { type: 'string', description: '一句话:你做了一款什么游戏、核心玩法' } }, required: ['summary'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** L4 薄 system prompt(指 agent 读真 skill,不手抄)。 */
|
||||||
|
export function buildSystemPrompt(id) {
|
||||||
|
const G = `game-runtime/games/amgen-${id}`; // 与 read/write 的 repo-根相对路径一致(必须含 game-runtime/ 前缀)
|
||||||
|
return `你是 A-model 游戏生成 agent。目标产物 = 一款**能跑能玩**的多文件 LittleJS 小游戏,落在 ${G}/。
|
||||||
|
|
||||||
|
【工作目录已就绪】
|
||||||
|
- 我已把克隆起点 _template 拷到 ${G}/(一个可玩的最小游戏「点圆得分」:menu→play→over,用了 4 件编排插件)。
|
||||||
|
- **固定 plumbing 别动**:${G}/index.html、${G}/entry.js、${G}/src/main.js。
|
||||||
|
- **你要改写游戏本体**:${G}/src/ 下的 game.js / core.js / render.js / host-config.js(按需也可改 balance.js / assets.js)。把「点圆得分」改造成 brief 要的游戏。
|
||||||
|
|
||||||
|
【先读手册(必须;别凭记忆猜 API)】
|
||||||
|
1. read_file('.agents/skills/littlejs-game-dev.md') —— code 层作业手册(结构/边界/红线/12 插件 API 速查)。
|
||||||
|
2. read_file('${G}/README.md') —— 这份克隆起点的文件职责 +「你写什么 vs 调什么」边界 + ⚠bundle.tick 坑。
|
||||||
|
3. 用到某插件就 read_file 它的 api.d.ts(如 game-runtime/src/plugins/scene-fsm/api.d.ts、.../session-score/api.d.ts)看精确签名。
|
||||||
|
4. 需要更完整范例时 read_file('game-runtime/games/catch-fruit/src/game.js')(catch-fruit 是真浏览器可玩的完整参照)。
|
||||||
|
|
||||||
|
【步骤】
|
||||||
|
A 读手册(1-2) → B 按 brief 定玩法(核心循环 / 胜负 / 计分 / 数值)→ C 读要用的插件 api.d.ts → D write_file 改写 src/ 实现玩法 → E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check+build 都绿后调 done。
|
||||||
|
|
||||||
|
【红线(check 会拦,违反则 done 被拒)】
|
||||||
|
- game.js 默认导出工厂、**零引擎 import**;host-config.js **保留导出名 buildTemplateHostConfig**(只改插件集/wiring,别改函数名)。
|
||||||
|
- 游戏源(game/core/render)**零裸** Math.random / Date.now / setTimeout / requestAnimationFrame / new AudioContext / addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,输入经 ctx.getInput。
|
||||||
|
- game.js 的 update(dt) 内**必须调 opts.runtime.bundle.tick(dt)**(否则插件 onFrame 不推进、timer 永不到期、一局不结束)。
|
||||||
|
- 插件由 host 拥有、经 opts.runtime.plugins 注入;游戏只**用**、不 new、不 register;插件键名与 host-config 的 plugins 对象一致。
|
||||||
|
|
||||||
|
【完成判据 + 停机纪律(重要)】
|
||||||
|
- 判据 = check PASS + build PASS(本 spike **不跑** README 里的 node --test)。**一旦 check 与 build 都 PASS,立即调 done**(summary 一句话)。
|
||||||
|
- **别做**:别写/改任何 test/ 文件、别写额外脚本、别追求完美、别加 brief/README 没要求的东西。这是 spike,**能跑能玩即可**。
|
||||||
|
- 你**只有** read_file / write_file / list_dir / check / build / done 六个工具;**没有 edit_file** —— 改文件用 write_file 整体覆盖。
|
||||||
|
|
||||||
|
现在开始:**先 read_file 读手册,别直接写码;核心玩法实现完、check+build 绿了就立即 done。**`;
|
||||||
|
}
|
||||||
244
game-runtime/tools/amodel-gen/tools.mjs
Normal file
244
game-runtime/tools/amodel-gen/tools.mjs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* tools.mjs — agentic A-model 生成 harness 的工具层
|
||||||
|
* owner:amodel-gen spike | 消费方:gen.mjs(ReAct 循环) / play.mjs / CLI
|
||||||
|
*
|
||||||
|
* 【两类东西】
|
||||||
|
* A) 给 agent 的 5 工具(L1 沙箱边界):
|
||||||
|
* - read_file : repo 根只读(agent 自读 skill / api.d.ts / catch-fruit 范例);拒绝逃出 repo 根;超大截断。
|
||||||
|
* - write_file : 仅许写本 run 的 game 目录 games/amgen-<id>/;拒绝越界。
|
||||||
|
* - list_dir : repo 根内列目录。
|
||||||
|
* - check : 循环内快反馈 = node --check 各 src/*.js + 五法/导出名/红线静态 lint。
|
||||||
|
* - build : esbuild(scripts/build.mjs 锁参)→ bundle;循环内快反馈。
|
||||||
|
* 不给 agent 任意 shell。
|
||||||
|
* B) 确定性 scaffold(克隆 _template → 本 run 目录)。
|
||||||
|
*
|
||||||
|
* 【路径基准】本文件在 game-runtime/tools/amodel-gen/ → GAME_RUNTIME=../.. → REPO_ROOT=../../..
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
readFileSync, writeFileSync, readdirSync, statSync,
|
||||||
|
mkdirSync, existsSync, rmSync, cpSync,
|
||||||
|
} from 'node:fs';
|
||||||
|
import { resolve, dirname, relative, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
// tools/amodel-gen → game-runtime → repo 根
|
||||||
|
export const GAME_RUNTIME = resolve(__dirname, '../..');
|
||||||
|
export const REPO_ROOT = resolve(__dirname, '../../..');
|
||||||
|
const GAMES_DIR = join(GAME_RUNTIME, 'games');
|
||||||
|
const TEMPLATE_DIR = join(GAMES_DIR, '_template');
|
||||||
|
|
||||||
|
const MAX_READ_BYTES = 200 * 1024; // 单文件读上限,防撑爆 agent context
|
||||||
|
|
||||||
|
/** 把 repo 相对路径解析为绝对路径,并校验不逃出 repo 根。 */
|
||||||
|
function resolveInRepo(p) {
|
||||||
|
const abs = resolve(REPO_ROOT, p);
|
||||||
|
if (abs !== REPO_ROOT && !abs.startsWith(REPO_ROOT + '/')) {
|
||||||
|
throw new Error(`路径越界(必须在 repo 根内):${p}`);
|
||||||
|
}
|
||||||
|
return abs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本 run 的 game 目录(= 写边界)。 */
|
||||||
|
export function gameDir(id) {
|
||||||
|
return join(GAMES_DIR, `amgen-${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验绝对路径落在本 run 的 game 目录内(写边界)。 */
|
||||||
|
function assertInGameDir(id, abs) {
|
||||||
|
const gd = gameDir(id);
|
||||||
|
if (abs !== gd && !abs.startsWith(gd + '/')) {
|
||||||
|
throw new Error(`write 越界:只许写 ${relative(REPO_ROOT, gd)}/ 内(你给的是 ${relative(REPO_ROOT, abs)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────── 给 agent 的 5 工具 ─────────────────────────
|
||||||
|
|
||||||
|
/** read_file:repo 根只读。返回 {ok, content?, truncated?, error?}。 */
|
||||||
|
export function readFileTool(path) {
|
||||||
|
try {
|
||||||
|
const abs = resolveInRepo(path);
|
||||||
|
const st = statSync(abs);
|
||||||
|
if (st.isDirectory()) return { ok: false, error: `是目录,不是文件:${path}(请用 list_dir)` };
|
||||||
|
let content = readFileSync(abs, 'utf8');
|
||||||
|
let truncated = false;
|
||||||
|
if (Buffer.byteLength(content, 'utf8') > MAX_READ_BYTES) {
|
||||||
|
content = content.slice(0, MAX_READ_BYTES);
|
||||||
|
truncated = true; // 截断:超大文件只给前 200KB,提示 agent 它被截了
|
||||||
|
}
|
||||||
|
return { ok: true, content, truncated };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: String((e && e.message) || e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** write_file:仅许写本 run game 目录。返回 {ok, bytes?, error?}。 */
|
||||||
|
export function writeFileTool(id, path, content) {
|
||||||
|
try {
|
||||||
|
const abs = resolveInRepo(path);
|
||||||
|
assertInGameDir(id, abs);
|
||||||
|
mkdirSync(dirname(abs), { recursive: true });
|
||||||
|
writeFileSync(abs, content, 'utf8');
|
||||||
|
return { ok: true, bytes: Buffer.byteLength(content, 'utf8') };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: String((e && e.message) || e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** list_dir:repo 根内列目录(目录名带尾 /)。返回 {ok, entries?, error?}。 */
|
||||||
|
export function listDirTool(path) {
|
||||||
|
try {
|
||||||
|
const abs = resolveInRepo(path);
|
||||||
|
const entries = readdirSync(abs, { withFileTypes: true })
|
||||||
|
.map((d) => (d.isDirectory() ? d.name + '/' : d.name))
|
||||||
|
.sort();
|
||||||
|
return { ok: true, entries };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: String((e && e.message) || e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────── scaffold / check / build ─────────────────────────
|
||||||
|
|
||||||
|
/** scaffold:克隆 _template → games/amgen-<id>/(去掉 dist;保留可玩骨架 + plumbing)。返回 game 目录。 */
|
||||||
|
export function scaffold(id) {
|
||||||
|
const gd = gameDir(id);
|
||||||
|
if (existsSync(gd)) rmSync(gd, { recursive: true, force: true });
|
||||||
|
cpSync(TEMPLATE_DIR, gd, { recursive: true });
|
||||||
|
rmSync(join(gd, 'dist'), { recursive: true, force: true }); // 旧产物(若有)清掉
|
||||||
|
rmSync(join(gd, 'test'), { recursive: true, force: true }); // 去 test/:spike 不让 agent 钻测试(gate=check+build+play),也去掉 README 的"跑测试"诱因
|
||||||
|
return gd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏源红线:零裸时间/随机/DOM(时间随机经 boot.ctx,定时经 timer-scheduler)。只匹配"调用形态"。
|
||||||
|
const BANNED = [
|
||||||
|
/\bMath\.random\s*\(/, /\bDate\.now\s*\(/, /\bperformance\.now\s*\(/,
|
||||||
|
/\bsetTimeout\s*\(/, /\bsetInterval\s*\(/, /\brequestAnimationFrame\s*\(/,
|
||||||
|
/\bnew\s+AudioContext\b/, /\baddEventListener\s*\(/,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 去注释 + 字符串字面量,避免红线/结构正则误伤文档注释里的示例(如注释里写「禁用 Math.random()」)。 */
|
||||||
|
function stripCode(s) {
|
||||||
|
return s
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, ' ') // 块注释
|
||||||
|
.replace(/\/\/[^\n]*/g, ' ') // 行注释
|
||||||
|
.replace(/'(?:[^'\\]|\\.)*'/g, "''") // 单引号串
|
||||||
|
.replace(/"(?:[^"\\]|\\.)*"/g, '""') // 双引号串
|
||||||
|
.replace(/`(?:[^`\\]|\\.)*`/g, '``'); // 模板串
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check:循环内快反馈门(便宜模型自纠语法/契约错的头号手段)。
|
||||||
|
* 1) node --check 各 src/*.js + entry.js(语法);
|
||||||
|
* 2) 结构契约:game.js 默认导出 + update 内调 bundle.tick;host-config.js 导出 buildTemplateHostConfig;
|
||||||
|
* 3) 红线:游戏源(game/core/render)零裸时间/随机/DOM。
|
||||||
|
* 返回 {ok, errors[]}。
|
||||||
|
*/
|
||||||
|
export function check(id) {
|
||||||
|
const gd = gameDir(id);
|
||||||
|
const errors = [];
|
||||||
|
const srcDir = join(gd, 'src');
|
||||||
|
let srcFiles = [];
|
||||||
|
try {
|
||||||
|
srcFiles = readdirSync(srcDir).filter((f) => f.endsWith('.js'));
|
||||||
|
} catch {
|
||||||
|
return { ok: false, errors: ['src/ 目录不存在或不可读'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 语法
|
||||||
|
for (const rel of [...srcFiles.map((f) => join('src', f)), 'entry.js']) {
|
||||||
|
const abs = join(gd, rel);
|
||||||
|
if (!existsSync(abs)) continue;
|
||||||
|
const r = spawnSync('node', ['--check', abs], { encoding: 'utf8' });
|
||||||
|
if (r.status !== 0) {
|
||||||
|
errors.push(`语法错误 ${rel}: ${(r.stderr || '').trim().split('\n').slice(0, 3).join(' ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 结构契约
|
||||||
|
const gameAbs = join(gd, 'src', 'game.js');
|
||||||
|
if (existsSync(gameAbs)) {
|
||||||
|
const g = stripCode(readFileSync(gameAbs, 'utf8'));
|
||||||
|
if (!/export\s+default/.test(g)) errors.push('game.js 必须 export default(工厂默认导出)');
|
||||||
|
if (!/\bbundle\.tick\s*\(/.test(g)) {
|
||||||
|
errors.push('game.js 的 update(dt) 内必须调 bundle.tick(dt)(否则插件 onFrame 不推进、timer 永不到期、一局不结束)');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push('缺 src/game.js');
|
||||||
|
}
|
||||||
|
const hcAbs = join(gd, 'src', 'host-config.js');
|
||||||
|
if (existsSync(hcAbs)) {
|
||||||
|
const hc = stripCode(readFileSync(hcAbs, 'utf8'));
|
||||||
|
if (!/export\s+function\s+buildTemplateHostConfig\b/.test(hc)) {
|
||||||
|
errors.push('host-config.js 必须保留导出名 buildTemplateHostConfig(固定 plumbing main.js 据此 import,勿改名)');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push('缺 src/host-config.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.5) game.js 必须真改写过(防 write 路径写错→没覆盖→拿 _template 原样蒙混过 check)
|
||||||
|
try {
|
||||||
|
const tmpl = readFileSync(join(TEMPLATE_DIR, 'src', 'game.js'), 'utf8');
|
||||||
|
if (existsSync(gameAbs) && readFileSync(gameAbs, 'utf8') === tmpl) {
|
||||||
|
errors.push(`game.js 与 _template 完全相同:还没真正改写游戏(确认 write 路径写进了 ${relative(REPO_ROOT, gd)}/src/)`);
|
||||||
|
}
|
||||||
|
} catch { /* 模板缺失则跳过 */ }
|
||||||
|
|
||||||
|
// 3) 红线(只查 agent 写的游戏文件)
|
||||||
|
for (const f of ['game.js', 'core.js', 'render.js']) {
|
||||||
|
const abs = join(gd, 'src', f);
|
||||||
|
if (!existsSync(abs)) continue;
|
||||||
|
const s = stripCode(readFileSync(abs, 'utf8'));
|
||||||
|
for (const re of BANNED) {
|
||||||
|
if (re.test(s)) errors.push(`红线 ${f}: 禁用 ${re.source}(时间/随机经 boot.ctx,定时经 timer-scheduler,输入经 ctx.getInput)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** build:esbuild 锁参打包(cwd=game-runtime)。返回 {ok, error?}。 */
|
||||||
|
export function build(id) {
|
||||||
|
const entry = `games/amgen-${id}/entry.js`;
|
||||||
|
const out = `games/amgen-${id}/dist/template-bundle.js`;
|
||||||
|
const r = spawnSync(
|
||||||
|
'node',
|
||||||
|
['scripts/build.mjs', entry, out, '--global-name=TemplateGame'],
|
||||||
|
{ cwd: GAME_RUNTIME, encoding: 'utf8', timeout: 60000 },
|
||||||
|
);
|
||||||
|
if (r.status !== 0) {
|
||||||
|
const err = ((r.stderr || '') + (r.stdout || '')).trim().split('\n').slice(-8).join('\n');
|
||||||
|
return { ok: false, error: err || `build 退出码 ${r.status}` };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────── CLI(独立验证用)─────────────────────────
|
||||||
|
// 用法:node tools.mjs scaffold <id> | check <id> | build <id>
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
const [, , cmd, id] = process.argv;
|
||||||
|
if (!cmd || !id) {
|
||||||
|
console.error('用法:node tools.mjs <scaffold|check|build> <id>');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
if (cmd === 'scaffold') {
|
||||||
|
const gd = scaffold(id);
|
||||||
|
console.log('[scaffold] →', relative(REPO_ROOT, gd));
|
||||||
|
} else if (cmd === 'check') {
|
||||||
|
const r = check(id);
|
||||||
|
console.log('[check]', r.ok ? 'PASS' : 'FAIL');
|
||||||
|
r.errors.forEach((e) => console.log(' -', e));
|
||||||
|
process.exit(r.ok ? 0 : 1);
|
||||||
|
} else if (cmd === 'build') {
|
||||||
|
const r = build(id);
|
||||||
|
console.log('[build]', r.ok ? 'PASS' : 'FAIL');
|
||||||
|
if (!r.ok) { console.log(r.error); process.exit(1); }
|
||||||
|
} else {
|
||||||
|
console.error('未知命令:', cmd);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user