feat(cheap-worker): per-genre 脚手架路由接入 scaffold_template/scaffold_desc(扩模板数据点)

扩模板需让难品类 AI 从预置品类骨架起步、少写。接入(全缺省回落 _template、create 路零改):
- tools.mjs scaffoldSaa(gameId, templateName='_template'):clone 指定模板;CLI 加第 3 参;模板缺失 warn 回落。
- cheap_run.scaffold(game_id, template=None):shell-out 透传模板名。
- cheap_studio.run_studio(scaffold_template, scaffold_desc):线程到 prep + build_system_prompt。
- cheap_roles:system prompt 脚手架描述参数化(⟦SCAFFOLD_DESC⟧),让 AI 知道起点已是该品类完整游戏、换皮而非重写。

管线 routing 单测 27/27、prompt 参数化 8/8。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-29 17:01:06 -07:00
parent 9ddd173c06
commit 47d6394347
4 changed files with 43 additions and 17 deletions

View File

@ -25,7 +25,7 @@ _SYSTEM_PROMPT = """你是 A-model 游戏生成 agent。目标产物 = 一款**
这一档的定位钉死AI 参与深度低 做简单玩具轻量简单手感物理碰撞粒子调色这些复杂能力平台已用工程脚手架 + L2 插件库替你承担**少写多调插件**就能做出卖相与深度你的活是在脚手架上把 brief 做成一款**真能让人玩进去**的游戏**不是产个没人会玩的简单玩具**
工作目录已就绪
- 我已把克隆起点 _template 拷到 G/一个跑通的脚手架起点点圆得分menuplayover 骨架 + 五法齐全 + 编排插件示范你的活是据 brief **借插件能力把它改造成一款高质游戏**别把它当"这档就该这么简单"的天花板
- 我已把克隆起点拷到 G/SCAFFOLD_DESC你的活是据 brief **借插件能力把它改造成一款高质游戏**别把它当"这档就该这么简单"的天花板
- **L1 固定 plumbing 别动write 会被拒**G/index.htmlG/entry-bundle.jsG/src/game.js wrapper**G/src/host-config.js**host 装配已替你注入** 11 件能力插件** + viewport 固定 390×844 + 工厂 wiring你无需装配任何插件
- **你只写 L3 游戏本体****G/src/game-logic.js必写游戏核心** + 按需 core.js纯逻辑/ render.js画面/ balance.js数值/ assets.js资产结构简单可把逻辑/画面集中在 game-logic.js 多系统/多场景再拆 core/render把脚手架起点改造成 brief 要的**高质游戏**多借插件能力做卖相与深度**画面读 viewport.w/h390×844 竖屏**
- **入口契约钉死**game-logic.js 必须 `export function createGame({ plugins, bundle, viewport })` 返回 GameInstance 五法 init(boot)/update(dt)/render(g)/destroy()+必须 _forensicsView**插件已扁平注入 plugins**用到才解构sceneFsm/sessionScore/hudUi/timerScheduler/save/gamefeel/juice/palettePost/audioMusic/collision/physics
@ -63,17 +63,24 @@ A 读手册1+ 读你的起点 game-logic.js4→ B 按 brief 定玩
现在开始** read_file 读手册别直接写码核心玩法实现完check+build 绿了就立即 finish**"""
def build_system_prompt(game_id: str) -> str:
"""据 game_id 拼出本 run 的 system prompt把 ⟦G⟧ 占位替换成 repo 根相对的 game 目录)。
# 默认脚手架描述create 路 = 通用 _template「点圆得分」起点。扩模板per-genre 黄金骨架传各自描述覆盖,
# 让 AI 知道起点已是一款该品类的完整游戏、按 brief 换皮而非重写(与 scaffold_template 配对,见 run_studio
_DEFAULT_SCAFFOLD_DESC = "一个跑通的脚手架起点「点圆得分」menu→play→over 骨架 + 五法齐全 + 编排插件示范"
def build_system_prompt(game_id: str, scaffold_desc: str = None) -> str:
"""据 game_id 拼出本 run 的 system prompt把 ⟦SCAFFOLD_DESC⟧/⟦G⟧ 占位替换)。
Args:
game_id: run 的游戏 idcheap-worker 统一用 cheap-<xxx> 前缀 Node 旧路 node-<xxx> 隔离
scaffold_desc: 起点脚手架的一句话描述None=默认通用点圆得分create 路不变
扩模板传 per-genre 描述如经营=完整经营点客游戏 brief 换皮 AI 从品类骨架换皮少写
Returns:
system prompt 字符串 agent read skill不手抄
"""
g = f"game-runtime/games/amgen-{game_id}"
return _SYSTEM_PROMPT.replace("G⟧", g)
return _SYSTEM_PROMPT.replace("SCAFFOLD_DESC⟧", scaffold_desc or _DEFAULT_SCAFFOLD_DESC).replace("G⟧", g)
def _contract_block() -> str:

View File

@ -153,10 +153,18 @@ def _shell_env() -> dict:
return env
def scaffold(game_id: str) -> dict:
"""scaffoldshell-out `node tools.mjs scaffold-saa <id>`clone _template + SAA 信封 __GameBundle。返回 {ok, output}。"""
def scaffold(game_id: str, template: Optional[str] = None) -> dict:
"""scaffoldshell-out `node tools.mjs scaffold-saa <id> [template]`clone 模板 + SAA 信封 __GameBundle。返回 {ok, output}。
template=None默认 通用 _template点圆得分起点扩模板 per-genre 黄金骨架目录名
经营=_template-shop 让难品类 AI 从预置经营循环起步少写模板不存在则 node 侧回落 _template
"""
try:
r = _run_node_tools("scaffold-saa", game_id)
argv = ["node", str(_AMODEL_GEN / "tools.mjs"), "scaffold-saa", game_id]
if template:
argv.append(template)
r = subprocess.run(argv, cwd=str(_GAME_RUNTIME), capture_output=True, text=True,
timeout=90.0, check=False)
return {"ok": r.returncode == 0, "output": (r.stdout + r.stderr).strip()}
except Exception as e: # noqa: BLE001
return {"ok": False, "output": f"scaffold shell-out 异常:{e}"}

View File

@ -110,7 +110,8 @@ def build_trace_source(verdict, driver_type, attempts, stage, model) -> dict:
async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=16000,
port=4320, cdp_port=9222, run_gates=True,
system_prompt=None, initial_kick=None, write_whitelist=None, prepare=None):
system_prompt=None, initial_kick=None, write_whitelist=None, prepare=None,
scaffold_template=None, scaffold_desc=None):
"""跑便宜档一局生成scaffold → ReAct 写 src/ → done 门 → 收口 stage+smoke+九门)。返回 run-summary dict。
run_gates=True默认spike 单局收口跑全套 stagesmokeensure_play_spec九门 play
@ -129,7 +130,8 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
t0 = time.time()
_rec(f"model={_bootstrap.SPIKE_MODEL} id={game_id} brief=「{brief}」max_iters={max_iters} max_resumes={max_resumes}")
prep = prepare or cheap_run.scaffold # create=scaffold clone _templatemodify=上游已预备的 noop
# create=scaffold clone 模板(扩模板:scaffold_template 传 per-genre 黄金骨架名,如经营=_template-shop)modify=上游已预备的 noop
prep = prepare or (lambda gid: cheap_run.scaffold(gid, scaffold_template))
sc = prep(game_id)
if not sc["ok"]:
return {"ok": False, "gameId": game_id, "fail": "准备失败:" + sc["output"]}
@ -146,7 +148,7 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
writer = Agent(
name="cheap-writer",
system_prompt=system_prompt or build_system_prompt(game_id), # modify 传 build_modify_system_prompt
system_prompt=system_prompt or build_system_prompt(game_id, scaffold_desc), # modify 传 build_modify_system_prompt;扩模板传 scaffold_desc
model=model,
toolkit=toolkit,
middlewares=[tracer, breaker], # trace 外层、熔断内层(列表序=洋葱序)

View File

@ -378,16 +378,25 @@ export function saaGameDir(gameId) {
}
/**
* scaffoldSaa:clone _template amgen-<gameId>/, A-model SAA plumbing( review B2)
* scaffoldSaa:clone 模板 amgen-<gameId>/, A-model SAA plumbing( review B2)
* - entry-bundle.js = entry-bundle.amodel.template.js(导出 bootGameHost, --global-name=__GameBundle window.__GameBundle)
* - index.html = _wg1-gen/_shared/index.template.html( __GameBundle.bootGameHost, __genBooted)
* - spike 入口(entry.js/src/main.js;SAA 不用)+ dist/test
* agent 仍只写 src/{game,core,render,host-config,balance,assets}.js( spike ;信封差异全在 plumbing)
*
* @param {string} gameId run 游戏 id
* @param {string} [templateName='_template'] 起始模板目录名(扩模板:经营=_template-shop per-genre 黄金骨架;
* 缺省 _template=通用点圆得分起点)模板不存在则回落 _template(不静默失败, warn 可查)
*/
export function scaffoldSaa(gameId) {
export function scaffoldSaa(gameId, templateName = '_template') {
const gd = gameDir(gameId);
let tmplDir = join(GAMES_DIR, templateName || '_template');
if (!existsSync(tmplDir)) {
console.warn(`[scaffold-saa] 模板 ${templateName} 不存在,回落 _template`);
tmplDir = TEMPLATE_DIR;
}
if (existsSync(gd)) rmSync(gd, { recursive: true, force: true });
cpSync(TEMPLATE_DIR, gd, { recursive: true });
cpSync(tmplDir, gd, { recursive: true });
rmSync(join(gd, 'dist'), { recursive: true, force: true });
rmSync(join(gd, 'test'), { recursive: true, force: true });
// 换 SAA 信封 plumbing(覆盖 _template 的 spike 信封 entry.js/index.html)
@ -482,9 +491,9 @@ export function ensurePlaySpec(gameId, state) {
// ───────────────────────── 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;
const [, , cmd, id, arg3] = process.argv;
if (!cmd || !id) {
console.error('用法:node tools.mjs <scaffold|check|build|scaffold-saa|build-saa|stage> <id>');
console.error('用法:node tools.mjs <scaffold|check|build|scaffold-saa|build-saa|stage> <id> [templateName]');
process.exit(2);
}
if (cmd === 'scaffold') {
@ -500,8 +509,8 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
console.log('[build]', r.ok ? 'PASS' : 'FAIL');
if (!r.ok) { console.log(r.error); process.exit(1); }
} else if (cmd === 'scaffold-saa') {
const gd = scaffoldSaa(id);
console.log('[scaffold-saa] →', relative(REPO_ROOT, gd), '(SAA 信封 plumbing)');
const gd = scaffoldSaa(id, arg3);
console.log('[scaffold-saa] →', relative(REPO_ROOT, gd), `(SAA 信封 plumbing, 模板=${arg3 || '_template'})`);
} else if (cmd === 'build-saa') {
const r = buildSaa(id);
console.log('[build-saa]', r.ok ? 'PASS' : 'FAIL');