game-runtime / game-studio / tier2 / wg1 四个非后端源码模块的跨模块代码评审: 4 个并行后台 workflow × 三阶段(静态分析在 mini-desktop / 接口协议+大厂规范+项目铁律深审 / 逐条 file:line 对抗证伪)。 结果 0 P0、9 P1、7 P2、9 P3(另 1 条候选证伪)。核心代码语言规范达大厂基线、无 error 级问题; 真风险 = 生成 worker 成本与幂等闸、活层 3 处旧品牌+品牌门覆盖洞、契约真空/v1→v2 漂移、game-studio 构建门。 逐模块明细嵌于评审档 §七,_index §6 已登记。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 KiB
topic, type, date, status, owner, related
| topic | type | date | status | owner | related | ||||
|---|---|---|---|---|---|---|---|---|---|
| 非后端四模块(game-runtime / game-studio / tier2 / wg1)代码评审 | review | 2026-06-24 | review-closed(4 模块并行评审已收口;25 条真发现逐条 file:line 核实,0 P0,1 条候选被证伪) | 创始人 + 6c6g 文档/设计线 |
|
非后端四模块代码评审 · 跨模块总汇
一、范围与方法
本次评审覆盖绘境AI 仓内除 game-cloud(Java 后端)、game-admin(Vue 管理后台)之外、真正带语言规范的四个源码模块:game-runtime(LittleJS 增强发行版,JS/ESM,约 36k LOC)、game-studio(Vue3 + Vant 产品前端,TS,约 18.7k LOC)、tier2(AgentScope 自治富游戏线,Python + Phaser glue,约 10.4k LOC)、wg1(W-G1 廉价模型 worker,Python,约 3k LOC)。contracts/(契约 SoT)没有单独成审,而是作为各模块"接口协议"维度的对齐锚点;deploy/(两个脚本)与 spikes/ 不计入。
评审用四个并行后台 workflow 各跑一个模块,每个模块串行三阶段:先在 mini-desktop 上跑静态分析工具(ruff / oxlint / vue-tsc——因 6c6g 当时仅余约 2Gi 可用内存且 swap 将满,跑不动 vue-tsc 这类重型工具,全部经 rsync 到 mini-desktop 执行),再做接口协议、语言规范与大厂代码质量、项目硬约束三个维度的深度评审,最后对每条候选发现逐条对抗式证伪、只保留能落到 file:line 证据的真问题。四模块共得 25 条真发现,另有 1 条候选(tier2 的 _file_cache 并发隐患)经复核证伪剔除。
二、总体结论
四个模块没有一个 P0,也没有一处功能性硬伤。就"代码是否符合语言规范、是否达到大厂质量要求"这个问题本身而言,答案是肯定的:核心代码的静态分析 error 级命中为零(oxlint 核心 src 0 error、ruff 0 个 F 类未定义/未用变量、vue-tsc 唯一的阻断来自缺依赖而非代码缺陷),game-runtime、game-studio 与 tier2 核心在中文注释完整度、引擎/插件与受信边界纪律、错误处理一致性上明显高于一般水平。
真正的风险不在"写得对不对",而在两个方向。其一,两条生成线(廉价线 wg1、富游戏线 tier2)的成本与幂等闸还没焊死,会在生产真烧 token;其二,生成线正处在 v1→v2 演进期,契约与代码标注落在了实现后面。这两类问题加起来贡献了 9 条 P1 里的大部分。
| 模块 | 语言 | P1 | P2 | P3 | 健康判断 |
|---|---|---|---|---|---|
| game-runtime | JS/ESM | 1 | 1 | 3 | 健康,本仓质量最高的模块之一;唯一须先修的是活层品牌违规 |
| game-studio | TS + Vue3 | 2 | 0 | 2 | 健康、可发布;两个 P1(CSP 注释失真、vue-i18n 缺失卡类型门)合入前清 |
| tier2 | Python + JS | 2 | 3 | 2 | 契约与质量达上线水准,但整线尚未端到端真跑闭环 |
| wg1 | Python | 4 | 3 | 2 | 底子扎实、注释/可观测达标;切默认产线前需补幂等、成本、契约 |
| 合计(真) | — | 9 | 7 | 9 | 0 个 P0;另 tier2 有 1 条候选被证伪 |
三、四条跨模块主线
主线一 · 生成 worker 的成本与幂等闸(最高优先,唯一带真金白银风险)。 这一类直接关系"便宜模型造游戏"的单位经济,应在 W-G1 切默认产线、tier2 批量开闸之前焊死:
- wg1
POST /generate只有threading.Lock串行化、无幂等去重,执行器重投会对同一 traceId 重复真生成 + 重复 HMAC 回调,后端可能重复建版本/组包(wg1/gen-worker/worker/service.py:378)。 - wg1
run_studio声明了max_tokens形参却没透传到底层调用,便宜模型在复杂局会被默认上限截断 → parse 失败重试,把 token 白烧在无谓重试上(wg1/gen-worker/worker/agent_loop/studio.py:231)。 - tier2 的熔断与 ¥ 成本硬闸每个 chat 回合重建、跨 resume 回合不累计,单局
rmb_hard_limit=3.0在多回合 resume(默认 6 回合)下形同虚设,一款游戏实际可烧到约 7 倍单回合上限而硬闸永不触发(tier2/gen-worker/service/app.py:192)。
主线二 · 品牌红线,且机器门的扫描面有洞。 活层共三处残留已退役旧品牌「造梦AI」:game-runtime/package.json:6、game-studio/index.html:7(玩家在浏览器标签页直接看到的标题)、wg1/gen-worker/worker/service.py:34。除了把这三处一行改掉,更要紧的是把 AGENTS §6 条款10 的 brand-invariant 机器门的扫描面扩到各模块根的 package.json / index.html 与 wg1/ 目录——否则门只扫 src/ 就会继续放过这类根文件(详见 §四)。
主线三 · 契约真空与 v1→v2 方向漂移,代码跑在了契约前面。 这对应"生成引擎 🚧 演进中"的现状,修法是补 schema 或在代码里加演进状态标注,别让维护者把冻结的 v1 当终态继续加码:
- wg1 产出的 engineBundle、九门 verdict、9 维 trace 没有任何 contracts/ schema 背书,而评审锚点的 verdict/game-design schema 还是 v1 clicker 时代的契约,字段世代不符(
wg1/gen-worker/worker/service.py:91)。 - tier2 服务态生成主链等多处对外能力只有 schema 形状、缺端到端真跑证据,代码里甚至留着 2026-06-24 的实证注释自承"扁平探测全空 → 服务态生成零产出",属孤儿风险(
tier2/gen-worker/service/app.py:105)。 - game-runtime 的 build-from-source / gd-runtime 仍整建在 v1 声明式 gameDefinition 上,而 source-project 契约已升级为
oneOf(v1|v2)、把 v1 标注为冻结只读、v2 多文件src/定为终态,代码层却无任何标注指向该判别式(game-runtime/src/host/build-from-source.mjs)。
主线四 · game-studio 的构建门与 host 安全注释失真。
vue-i18n声明^10.0.0却没装,导致 26 条 TS2307 卡死vue-tsc类型门与 CI;同时vue-tsc ^3.2.8声明与实装6.0.3、typescript ~6.0.2不在同一兼容线,npm ci行为不可预期(game-studio/package.json)。host/inject.ts的 CSP 注释援引了本模块根本不存在的gamedef/scanLogic运行时,把一个不存在的静态防护当作既成安全保障陈述(违反"不得把未验证声明当事实"硬约束),同时给 iframe 沙箱多授了当前代码并不需要的unsafe-eval(game-studio/src/host/inject.ts:69)。
P2/P3 多为表述/一致性/低优先韧性债:tier2 的 validate_play_scene 用文本子串校验平台命脉接线(易假绿假红)、screenshot docstring 与像素回读数据流不符、bootstrap 鉴权头是占位口子;wg1 的 callback 孤儿字段、_judge_vision 绕过重试封装、基建子进程无 check;game-runtime 的 zaomeng 持久化键、m3 重试末轮多睡一次。各模块明细见 §六。
四、跨模块交叉核对补正
逐模块评审之外,我对四个模块做了一次跨模块交叉核对,补出一条单模块评审漏掉的发现,并据此指出一个机器门缺陷。
game-studio 的评审结论写"没有旧品牌造梦AI 残留",但那个结论是只扫了 src/ 得出的。模块根的 game-studio/index.html:7 有 <title>造梦AI</title>,它不在 src/ 下,被这次评审漏掉了——而它恰恰是玩家在浏览器标签页上直接看到的标题,是三处旧品牌残留里最显眼的一处。把它和 game-runtime、wg1 两处合起来,活层共有三处旧品牌。
这条补正本身就是结论:品牌不变量机器门当前的扫描面有洞。如果门只扫 src/ 或只扫部分目录,就会放过 index.html、package.json 这类模块根文件,以及 wg1/ 这类尚未纳入扫描范围的目录。所以这一项的修复不止是改三处字符串,而是把门的扫描面扩到各模块根的 package.json / index.html 与 wg1/——这正印证 AGENTS §6 条款10"规则必须编译成机器门、否则等于没门",而门的覆盖面本身也要被审。
五、静态分析口径与语言规范结论
四个模块的静态分析都在 mini-desktop 上跑(已预装 ruff 0.15.19、oxlint 1.71.0,并复用 game-studio 既有 node_modules 里的 vue-tsc 6.0.3),6c6g 只做只读读码:
- game-runtime:oxlint 1.71.0 扫核心
src/53 个文件,17 warning、0 error,逐条核对基本是有意为之的防御性写法(catch 占位_、刻意省参的 ZzFX sparse array),非真缺陷;games/下生成产物另触发约 1.8 万 warning,主体是 minified bundle,不属核心代码、不计入。 - game-studio:vue-tsc 6.0.3 报 26 条 error,但全部是 TS2307、根因单一(vue-i18n 未装),装包即清零;oxlint 扫 109 文件 0 error、仅 1 条 cosmetic warning。
- tier2:ruff 0.11.19 扫 Python 共 2328 条,其中 94% 是 E501 行过长噪声,实质约 135 条均为格式/接口占位级、无 error;oxlint 扫 JS(engine + harness)0 error。复核剔除两处误报:三处
subprocess.run实已在紧邻行显式判returncode、_file_cache实已被threading.Lock保护。 - wg1:ruff 0.15.19 扫 Python 共 553 条,85% 是 E501,有语义的少数命中(ARG001 正好印证
max_tokens未用、PLW1510 基建子进程无 check)已并入问题清单。
结论:四模块核心代码均无 error 级问题,语言规范层面达到大厂基线;Python 侧主要噪声是行长(可一把 ruff --fix 清掉),不构成质量风险。换句话说,"是否符合语言规范"的答案是符合,问题都在接口协议、成本韧性与项目硬约束三个维度,而非基础语言规范。
六、附录 · 评审方法与可复用配方
本次评审用一份参数化 workflow 脚本(scratchpad/review/module-review.workflow.mjs)按模块名启动四个独立后台 workflow 并行跑,每个 workflow 三阶段串行:
- 静态分析(sonnet):rsync 模块到 mini-desktop 的
/root/review-2026-06-24/<module>,按语言跑 ruff / oxlint / vue-tsc,结构化采集违规与统计; - 深度评审(opus):在 6c6g 本地只读读码,按接口协议 / 语言规范与大厂质量 / 项目硬约束三维,产出按 P0–P3 排序、每条带 file:line 证据的候选发现;
- 对抗验证与成文(opus):对每条候选都当作"可能是误报"去验证,只保留能落到 file:line 证据的真问题,写成中文散文报告。
内存路由是这次的关键工程决策:重型静态分析(尤其 vue-tsc)一律在 mini-desktop(15Gi 可用)执行,6c6g(约 2Gi 可用、swap 将满)只做只读读码,全程未触发 OOM。该配方可复用于后续任意模块的批量评审——把模块配置追加进脚本的 MODULES 表、再启动一个 workflow 即可。
七、各模块明细评审(逐模块原始输出)
以下为四个 workflow 各自第三阶段(对抗验证 + 成文)产出的逐模块完整报告原文,内部小标题已统一降一级。每条发现均经该模块逐条 file:line 核实。game-studio 报告中"无旧品牌残留"一句已被 §四交叉核对补正(漏扫了模块根
index.html)。
7.1 game-runtime
game-runtime 模块评审报告
1. 模块概述与健康判断
game-runtime 是绘境AI 的 Tier0/1 廉价线运行时:一套围绕 LittleJS 引擎的增强发行版,对外暴露三层 API——插件受控面、装载契约、通用宿主——并承载把后端线产出的结构化 gameDefinition 装配成可玩工厂源文本的 build-from-source 链路。从本次评审看,这是本仓质量最高的模块之一:对外接口设计成熟、单一职责清晰、边界纪律严明,简体中文注释完整覆盖外部交互、核心实现与错误路径,错误处理哲学一致(单点抛错隔离 + 留痕、不连坐),确定性纪律(禁 Date.now/Math.random)与资源安全(订阅 scope 化、dispose 定向回收、ENTITY_CAP 防失控 spawn)都落实到位。
健康判断:健康。逐条证伪后保留 5 个真问题,全部为 P1 及以下,无 P0,无功能性缺陷。其中 1 个 P1 是活层品牌违规(会被文档治理机器门红线拦截,必须先修),其余为契约方向标注缺失与若干低优先一致性/韧性债。模块功能正确性、边界设计、代码质量均无硬伤。
2. 接口协议评估
对外 API 面分三层,各司其职且正交,边界纪律是本模块最值得肯定处。
第一层是 src/core/plugin.js 的插件受控面:PluginContext 只暴露 6 项受控能力(YAGNI 最小集),PluginRegistry 负责注册、init 正序加错误隔离、dispose 逆序加幂等。贯穿其中的铁律是「绝不直透 littlejsengine 裸对象」,正是这条纪律让「引擎可换」从口号变成可成立的设计。
第二层是 src/core/game-host.d.ts 装载契约:GameHostFactory 工厂产出 GameInstance(五生命周期 + boot 脊柱 {ctx,mainContext,canvas,seed,assets?}),与 api.d.ts 的 PluginContext 划清边界——前者管「游戏整体 ↔ 引擎宿主」,后者管「插件 ↔ 引擎」。d.ts 注释还明确解释了为何不把它折进 api.d.ts、为何落在 src 而非 contracts/,没有孤儿设计也没有残缺入口。
第三层是 src/host/boot-game-host.js:把 P1 wanglanmei 专属骨架泛化为 bootGameHost(opts) 通用宿主,stub/real 双通道、参数化视口/工厂/插件集,消费方明确(取证钩子 + studio host)。
引擎↔插件边界铁律(创始人 2026-06-13)落实得尤其干净。particles-juice 把粒子模拟整块删除、改为经 ctx.getEngine().particles 薄包装引擎的 ParticleEmitter,toEngineSpec 是纯映射可单测;juice 套件因引擎无对应能力而自研补层;null-engine 时粒子优雅 no-op 且明确「禁回退 sim」。collision 则在文件头用红线论证了「引擎 boolean 原语不覆盖 MTV/法线语义 = 真缺件」,故自研补层合法而非误判为待包装。两者都做到了「同一职不留两条并行路」。
与契约的对齐基本到位:gd-runtime/build-from-source 消费 contracts/agent-loop/source-project.schema.json 的 v1 gameDefinition,顶部白名单与 schema 的 additionalProperties:false 对齐,注释还点出了 assets 位于 sourceProject 顶层而非 gameDefinition 属性这一易错点。
接口层唯一的实质风险是方向漂移:source-project 契约已在 2026-06-21 升级为 oneOf(v1|v2),明确把 v1 gamedef 声明式表示标注为「1.0 legacy·冻结只读·A-model 退役后不再产出」,把 v2 多文件 src/ 定为终态;但 src/host 的活实现主路(build-from-source.mjs + gd-runtime.js + generic-host-config.js 装配链)仍整建在 v1 之上并被消费。这是已知的演进漂移而非缺陷(创始人 06-20 已定 src/ 多文件为终态、gameDefinition 只是通往 src/ 的脚手架),但代码层无任何标注指向该契约判别式与冻结状态,后续维护者容易把 v1 当作当前唯一/终态路线继续加码。详见问题 GR-02。
3. 语言规范与大厂代码质量评估
语言规范与代码质量整体优秀。全文件简体中文注释完整,外部交互、核心实现、错误路径都有可追溯说明。错误处理显式且一致:注册器、输入桥、帧回调、插件 dispose 全部走「单点抛错隔离 + logError 留痕、不连坐」哲学。确定性纪律严格:受控 time/random,禁 Date.now/Math.random,gd-runtime 内还有 build-from-source 静态扫描在编译前拦截不可信模型逻辑。资源安全到位:输入订阅 scope 化、dispose 定向回收防泄漏、ENTITY_CAP 防失控 spawn。函数普遍小而单一,魔法数多已具名化(DEFAULT_SPRITE_SIZE、ENTITY_CAP、DPR、FIXED_DT)。
build-from-source 的信任模型尤其值得肯定:文件头明确论证了模型产出的 behavior.code / rule.condition 是不可信代码,只在浏览器(经九门 CDP harness)执行,Node 在生产链上永不执行不可信模型代码,同步 JS 无法进程内硬中断故在校验边界静态拦截危险模式。这是对 new Function 不沙箱现实的合理缓解,且对齐了双评审的边界化建议。
amodel-gen 是 spike/harness 工具(文件头自述,明确「红线只约束游戏 src,harness 允许 Date.now」),其 LLM 调用韧性已具备超时(AbortController 300s)、重试(429/5xx/IO 重试,4xx 不重试)与 loop-detection,达标。唯一小瑕疵是重试循环在末轮失败后仍 sleep 一次才抛(GR-03)。
唯一真正跨硬约束的问题是品牌:package.json 的 description 仍含已退役旧名「造梦AI」(GR-01,活层违规)。另有若干 zaomeng 内部标识符(save 默认 namespace、generic namespace、core Symbol),它们是持久化存储键与内部符号而非用户可见品牌展示,改动会破坏存量存档兼容,属较低优先级的命名一致性债(GR-05)。
4. 静态分析结论
本次对 game-runtime 跑了 oxlint@1.71.0(npx --yes oxlint@latest,95 条规则,4 线程)。平台核心代码 src/ 共 53 个文件,命中 17 个 warning、0 个 error,全部为 warning 级。三类规则:no-unused-vars(11 条,均为 catch 参数 _ 或 e)、no-sparse-arrays(5 条,均在 gd-runtime.js 的 BEEP_PARAMS 音效参数数组里刻意省略 ZzFX 位置参数)、no-unused-expressions(1 条,destroy 循环里的短路 cancel 链)。
逐条核对后,这 17 个 warning 基本都是有意为之、可降噪而非真缺陷:no-unused-vars 多为防御性 catch 的 _/e(均带中文注释说明失败不影响玩法),no-sparse-arrays 是刻意省参,no-unused-expressions 是短路调用模式。唯一与团队约定不一致的是 save-progress impl.js:232/252 两处坏档容错用了 catch(e) 而非全仓约定的 catch(_),导致 oxlint 误报(GR-04),建议统一为 _ 消噪。
games/ 目录下 AI 生成产物另触发约 17954 个 warning,主体是 _wg1-gen 下 minified 的 bundle.iife.js(oxlint 自身提示该文件过长建议跳过),不属平台核心代码、不计入质量结论。
5. 问题清单
| 编号 | 严重度 | 维度 | 位置 | 问题 / 影响 / 根因 / 修复 |
|---|---|---|---|---|
| GR-01 | P1 | 项目硬约束·品牌 | package.json:6 | 问题:description 写「造梦AI 平台维护的 LittleJS 增强发行版…」,造梦AI 是已退役旧品牌。影响:package.json 属活层源、非白名单,违反品牌不变量门,wave-close 的 brand-invariant 机器门会红线拦截。根因:改名时漏改残留(同模块 all-plugins.js:45 已用现行「绘境AI」佐证)。修复:把「造梦AI」改为「绘境AI」,其余文字不动;改后 rg '造梦AI' game-runtime/ 确认全活层返零(已核:仅此一处)。 |
| GR-02 | P2 | 接口协议·契约对齐 | src/host/build-from-source.mjs(及 gd-runtime.js)文件头 | 问题:source-project 契约已 oneOf 升级、v1 gamedef 标注「冻结只读·不再产出」、v2 多文件 src/ 为终态,但 build-from-source/gd-runtime 活实现仍整建在 v1 上且无任何标注指向该判别式与冻结状态。影响:维护者易把 v1 当作当前唯一/终态路线继续加码。根因:契约 06-21 升级,代码层未同步演进标注。修复:在两文件头各加一行演进状态注释,指明本路线对应 schemaVersion=1.0(legacy·冻结只读)、A-model v2 为契约终态、本实现为存量/脚手架路线,并链到 schema 的 oneOf 判别式。仅加注释、不改逻辑。 |
| GR-03 | P3 | 语言规范·调用韧性 | tools/amodel-gen/m3.mjs:71(及 61) | 问题:chat() 重试循环在末轮(attempt=2)失败后仍执行 await sleep(1500*(attempt+1)) 才抛,白等 4.5s;429/5xx 分支(行 61)同样模式。影响:在 timeoutMs=300s 的慢调用语境下拖长失败反馈。根因:sleep 未判断是否最后一轮。修复:if (attempt < 2) await sleep(...),catch 与 429 两处都加。spike 工具低优先,下次触碰顺手修。 |
| GR-04 | P3 | 大厂代码质量·一致性 | src/plugins/save-progress/impl.js:232,252 | 问题:两处坏档容错 catch(e) 声明 e 却未使用,与全仓 catch(_) 忽略约定不一致,触发 oxlint no-unused-vars 误报。影响:静态分析噪声 + 一致性债,无功能问题。根因:变量名用 e 而非约定的 _。修复:改为 catch(_)(行为零变更),消误报;或在确需日志时 catch(e){ logWarn(...e.message...) } 真正消费 e。优先前者。 |
| GR-05 | P3 | 项目硬约束·命名 | impl.js:39 / generic-host-config.js:80-81 / plugin.js:585 | 问题:多处内部标识符仍含旧代号 zaomeng——save DEFAULT_NAMESPACE='zaomeng.save'、generic namespace='zaomeng-generic'、core Symbol('zaomeng.coreProtocol.contextBundle')。影响:非用户可见品牌展示(不触发展示门),但 namespace 是落盘 key 前缀,裸改会让存量存档读不到旧档(破坏兼容)。根因:历史代号沿用。修复:Symbol 描述无外部依赖可直接改为 huijing/绘境 口径;namespace 默认值须走存档迁移(读旧前缀回填新前缀)或保留旧值 + 注释标注「历史代号,改动需配存档迁移,勿裸改」。建议先只改 Symbol。 |
6. 收尾建议
最该先做的是 GR-01:改一行 description 把「造梦AI」改成「绘境AI」。这是唯一会被文档治理机器门红线拦截的活层违规,成本极低、零风险,应在本模块收口前立即修掉,并随手确认全 game-runtime 活层无其它旧名残留(已核仅此一处)。
其次是 GR-02 的契约方向标注。它本身不是功能缺陷,但属于已知的「契约终态 vs 实现现状」漂移,在生成引擎仍在演进的当下,给 build-from-source.mjs 与 gd-runtime.js 文件头各补一行演进状态注释、链到 source-project schema 的 oneOf 判别式,能避免后续维护者误把冻结的 v1 路线当终态继续加码。只加注释、不动逻辑,适合在收口时一并落档。
GR-03/04/05 都是 P3 低优先债,不必专门排期。建议在下次触碰对应文件时顺手处理:GR-04 把两处 catch(e) 改 catch(_) 消静态分析噪声(行为零变更),GR-05 先只改无兼容风险的 Symbol 描述、namespace 保留旧值加注释,GR-03 在 sleep 前加末轮判断。它们都不影响功能正确性,可以等触碰窗口自然消化。
7.2 game-studio
game-studio 模块评审报告
1. 模块概述与健康判断
game-studio 是绘境AI 的产品前端仓,承载创作者与玩家两类用户的全部 Web 界面,技术栈为 Vue3 + Vant + LittleJS 增强发行版,经统一的 axios 边界(request.ts)对接后端 13 个业务模块,经 host 层(inject.ts / GamePlayer.vue / bridge.ts)把生成出来的游戏装进受控 iframe 真玩。
这个模块整体是健康的。对外 API 面与 contracts/api-schemas/ 逐端点对齐,没有路径或方法漂移;统一请求边界把超时、CommonResult 解包、401 并发去重、网络错误兜底、租户与鉴权头注入这些横切关注点都收口在一处;host 层对 postMessage 这一不可信输入做了 origin 白名单加 schema 双校验加默认拒绝,沙箱受信边界设计正确。静态分析也支持这个判断:oxlint 扫 109 个文件只报 1 条 cosmetic warning、0 error,全仓没有 : any / eval / innerHTML / v-html,中文注释覆盖完整并带设计意图与踩坑追溯。
真正需要动手的问题集中在两处,且都不是业务逻辑缺陷:一是 host 注入层的 CSP 注释援引了本模块根本不存在的 gamedef / scanLogic 运行时,把一个不存在的安全防护当成既成事实陈述,同时给沙箱授予了当前代码并不需要的 unsafe-eval;二是 vue-i18n 声明了却没装,直接卡死类型门与 CI 构建。前者违反项目"不得把未验证声明当事实"的硬约束,后者属依赖治理问题。两者都是 P1,但都好修。
总体判断:模块健康,可发布,但 host 层 CSP 注释失真与 vue-i18n 缺失这两个 P1 需在合入前清掉。
2. 接口协议评估
对外 API 面是这个模块最扎实的部分。src/api/ 下的 14 个端点,其路径、HTTP 方法、路径参数都与 contracts/api-schemas/*.yaml 一一对应,aigc / feed / runtime / telemetry / passport / project / studio / biz / community / trade 各命名空间均无漂移。各 API 以命名空间聚合导出(feedApi / aigcApi 等),避免了同名函数冲突,职责切分清晰。
request.ts 作为唯一的网络边界做得很到位:10 秒超时、CommonResult 统一解包、401 时做并发去重并清态跳登录、网络错误统一 Toast 不让界面白屏、tenant-id 与 Authorization 注入、用动态 import 规避循环依赖,这些都覆盖到位。每个 API 都有对应的视图入口和失败兜底,没有孤儿接口。
host 层的两条受信边界也闭合良好。bridge.ts 对 iframe 经 postMessage 回传的消息做 origin 白名单加 schema 校验加 requestId 配对超时清理;GamePlayer.vue 的 storage 受信边界做了 key 白名单、4KB 上限、JSON 校验、命名空间隔离与异常静默吞。埋点侧 telemetry 用常量枚举约束事件名、fire-and-forget,卸载时用 fetch(keepalive) 带鉴权头兜底,设计合理。i18n 采用全局 feed/create 命名空间加页内 local-scope 注入其余命名空间的一致模式,locale 文件均被消费、无孤儿。
唯一的接口层瑕疵在 host iframe 注入:fetchAndVerifyManifest(inject.ts:457)拉取 manifest 的 fetch 没有自己的 AbortController,慢响应时底层请求会泄漏(详见问题清单 GS-03),但有上层 load 超时兜底,影响有限。
3. 语言规范与大厂代码质量评估
语言规范层面这个模块达到了大厂质量基线。全仓零 : any 与 as any、零 eval / innerHTML / v-html / document.write、零 console.log(仅 GamePlayer.vue 有 10 处 console.warn 用于错误路径,可接受)、零 TODO / FIXME。命名一致,company 命名空间统一用 wanxiang_*,与 wanxiang-game-sdk channel 对齐,没有旧品牌"造梦AI"残留。
中文注释覆盖完整且带设计意图,符合项目硬约束;函数小而单一职责,魔法数大多由具名常量承载,例如 FLUSH_SIZE / POLL_INTERVAL / REQUEST_TIMEOUT_MS / LEGACY_LOAD_TIMEOUT_MS / ENGINE_LOAD_TIMEOUT_MS / TERMINAL。store 与 composable 的入参出参语义清晰,有并发守卫与计时器清理。
需要修的有两类。第一类是依赖与构建可重现性:package.json 声明 vue-i18n ^10.0.0 但 node_modules 里根本没装(已核实 vue-i18n 与 @intlify 目录均不存在),导致 16 个 import 了 useI18n 的文件加 i18n/index.ts 全部 TS2307,vue-tsc -b --noEmit 报 26 条 error,类型门与 CI 构建被整体卡死;同时 package.json 声明 vue-tsc ^3.2.8 而 staging 实装 6.0.3、typescript ~6.0.2 与 vue-tsc ^3.x 不在同一兼容线,存在声明与实装的漂移,npm ci 行为不可预期。第二类是一处文档失真:inject.ts 的 unsafe-eval 注释把一个本模块不存在的静态防护(scanLogic)当作安全保障陈述,违反"不得把未验证声明当事实"。其余都是可观测性与韧性的低优化点。
4. 静态分析结论
跑了两个工具。vue-tsc 6.0.3(node_modules/.bin/vue-tsc -b --noEmit)报 26 条 error,全部是 TS2307,且全部指向同一根因——vue-i18n 未安装,所有 import { useI18n } from 'vue-i18n' 找不到类型声明。换句话说,这 26 条不是 26 个独立缺陷,而是一个依赖缺失的放大,装上包即清零。oxlint 1.71.0(95 条规则、4 线程)扫 109 个核心文件,报 0 error、1 条 warning(Play.vue:163 对象展开里多余的 ?? {} 兜底,纯 cosmetic)。
客观结论:逻辑质量健康(oxlint 几乎全绿),类型门当前不可过、但根因单一(依赖未装),修复成本极低。值得注意的附带事实是版本声明漂移——package.json 与实装的 vue-tsc 主版本号不一致,这会让 npm ci 在不同机器上拉出不同结果,属可重现性隐患,应一并对账。
5. 问题清单
| 编号 | 严重度 | 维度 | 位置 | 问题 / 影响 / 根因 / 修复 |
|---|---|---|---|---|
| GS-01 | P1 | 安全 / 文档完整性 | src/host/inject.ts:69-81 |
问题:iframe CSP 授予 unsafe-eval,注释(69-71 行)称其为 gamedef 运行时所需、由 scanLogic 静态拦截兜底。但全模块 grep 证实 src/host/ 下无任何 new Function / eval / scanLogic / gd-runtime / tickBehaviors 代码,这些名字只出现在该注释自身里。现行 boot()(243-281 行)仅两条活路径:bootGameHost(引擎 bundle 经内联 <script> 注入,走 unsafe-inline 而非 eval)与 demo 兜底 startRuntime(纯工厂、亦无 eval)。影响:(a) 沙箱被授予当前代码不需要的 eval 能力,违反最小权限、无谓扩大 iframe 攻击面;(b) 注释把不存在的 scanLogic 防护当作既成安全保障,违反"不得把未验证声明当事实"硬约束,误导后续维护者。根因:gamedef + new Function 这条运行时已于 2026-06-21 被 engineBundle 路替代,但 CSP 的 unsafe-eval 授权与其安全承接注释未随之清理。修复:二选一并使注释与代码一致——① 若引擎 bundle 确不需 eval(LittleJS 增强发行版一般不依赖 eval),从 CSP 移除 'unsafe-eval' 只留 'unsafe-inline';② 若 bundle 内部确需 eval,则把注释据实改成"unsafe-eval 为内联引擎 bundle 运行时所需",删掉 gamedef / scanLogic 表述,改述真实边界(connect-src 'none' + iframe sandbox + checksum 完整性)。无论哪种都必须删掉对不存在的 scanLogic 的引用。 |
| GS-02 | P1 | 依赖 / 构建可重现性 | package.json |
问题:vue-i18n 声明 ^10.0.0 但 node_modules 未安装,致 26 条 TS2307,vue-tsc -b --noEmit 与含 vue-tsc 的 build 脚本整体阻断;另 vue-tsc 声明 ^3.2.8 但实装 6.0.3、typescript ~6.0.2 与 vue-tsc ^3.x 不兼容,存在声明↔实装漂移。影响:类型门与 CI 构建过不去,npm ci 行为不可预期。根因:依赖未 npm install / lockfile 未含 vue-i18n,且版本声明久未对账实装。修复:在权威构建机(mini-desktop)npm install vue-i18n@^10 并提交更新后的 lockfile;校准 package.json,把 vue-tsc 声明对齐实装(如 ^6.0.3)、确认 typescript ~6.0.2 与所选 vue-tsc 主版本兼容;CI 用 npm ci 并把 vue-tsc -b --noEmit 设为门。 |
| GS-03 | P3 | 外部调用韧性 | src/host/inject.ts:457 |
问题:fetchAndVerifyManifest 内的 fetch 无 AbortController / 超时,manifest 服务长时间不响应时该请求一直挂起。影响:上层 GamePlayer.launch 已设 loadTimeout(引擎路 12s / 旧路 5s,见 GamePlayer.vue:209-216)会让 UI 进 error 态并 emit load_timeout,用户侧能恢复;但底层 fetch 本身不取消,挂起请求与其后续 .text() / sha256 仍占资源到浏览器默认超时,且与 axios 层 10s 超时基线不一致。属低优化点,不影响功能正确性。根因:底层 fetch 未与上层超时联动取消。修复:给该 fetch 加 AbortController + setTimeout(如 8s,小于引擎 load 超时)或用 AbortSignal.timeout(8000) 作 signal,使底层请求随上层超时一并取消。 |
| GS-04 | P3 | 静态告警 | src/views/play/Play.vue:163 |
问题:track('game_impression', { forwarded: data.event, ...(data.props ?? {}) }, ctx) 中,展开 falsy 值不会添加属性,?? {} 冗余(oxlint unicorn/no-useless-fallback-in-spread 命中)。纯 cosmetic。影响:无功能影响,仅 1 条 lint warning。根因:展开语法下空对象兜底多余。修复:第 163 行改为 ...data.props(展开 undefined / null 在对象字面量中本就安全);第 161 行的 data.props ?? {} 是整参传入、不在本规则范围,保留不动。 |
6. 收尾建议
先装 vue-i18n 把类型门救活(GS-02)。 这是当前唯一卡死 CI 与类型校验的硬阻塞,且 26 条 error 根因单一、修复成本极低。在 mini-desktop 上 npm install vue-i18n@^10、提交 lockfile,顺手把 vue-tsc / typescript 的声明对齐实装版本,让 npm ci 可重现。这一步做完,模块的类型门就能转绿,后续才谈得上把 vue-tsc -b --noEmit 设为常态门。
清掉 host CSP 注释里的失真陈述(GS-01)。 这条虽是 P1,但更多是文档与最小权限层面的问题:确认 LittleJS 引擎 bundle 是否真的需要 eval——若不需要,从 CSP 删掉 'unsafe-eval';无论删不删,都必须把注释里 gamedef / scanLogic / tickBehaviors 这套已不存在的运行时描述改写成真实边界,不能让一个不存在的 scanLogic 防护继续以"既成安全保障"的口吻留在代码里误导后人。
两条 P3 可顺带处理。 GS-03 给 fetchAndVerifyManifest 的 fetch 加 AbortSignal.timeout,让底层请求随上层 load 超时一并取消,与 axios 10s 基线对齐;GS-04 把 Play.vue:163 的 ...(data.props ?? {}) 改成 ...data.props 消掉最后一条 lint warning。这两条不阻塞发布,但能让模块的韧性与静态告警都干净收口。
7.3 tier2
tier2 富游戏线 · 模块评审报告
1. 模块概述与健康判断
tier2 富游戏线是绘境AI生成体系里的高质量分支:一个跑在 AgentScope(Python 自治 agent 框架,v2.0.2)上的 ReAct 写码 agent,造现有廉价生成线(SAA + LittleJS)做不出来的多系统富游戏,产物是真 Phaser 源工程。它由两半组成:Python 侧的 gen-worker(九工具 toolkit、四道熔断中间件、有界 resume 控制面、Agent Service 服务壳、三份契约 schema),以及 JS 侧的 engine / harness / fixtures(Phaser 装载宿主、CDP 真玩 harness、富游戏门、金标样本)。
这个模块的整体工程水准明显高于本仓平均。它把"契约先行"贯彻得很彻底:source-project / verdict / trace-event 三份 schema 都交代了为什么 fork 而非复用旧契约;finish 工具的产出形状和 source-project schema 严格共用一份定义,从源头杜绝"交付的形状"和"落库认的形状"漂移;九工具职责单一、失败语义清晰;finish 门把"未过验收门不许收尾"焊进了接口,等于把"验收零自评"做成了不可绕过的硬约束。中文注释完整到近乎过载,每个失败路径、每处 best-effort、每个框架接缝坑都带源码行号引用,外部交互和落库都有 flush=True 的可追溯日志。错误处理纪律统一:LLM、网关、落库、trace 全线 best-effort 降级,¥ 累进硬闸 fail-closed,熔断和双层超时齐备,httpx 调用都带显式 timeout。
健康判断:结构与纪律达到大厂可上线水准,但整条线尚未端到端真跑闭环——真正的风险不在代码质量,而在"声明的能力还没被真实调用链走通"和"成本硬闸跨回合失效"这两处,需在 0 号 spike 真驱后逐项补证并修复。
逐条对抗式复核后,8 条候选发现里 7 条成立、1 条证伪(T2-05 并发缓存隐患被证伪——缓存读写实际已被 threading.Lock 保护)。成立的里有 2 条 P1、4 条 P2、2 条 P3(其中 T2-07 含一处误报已剔除)。
2. 接口协议评估
对外接口面是这个模块最强的部分。三份核心契约定义清楚,finish 与 source-project schema 共用定义这一手(F3 防漂移)做得干净;persist / store / control_plane 三处重建 source_project 时,role 推断、contentHash 口径、buildProfile 字段都对齐,没有 split-brain。装载契约 boot-phaser-host.d.ts 与 engine 实现一一对应,latch 终态轮询语义在契约、d.ts、host.js、verdict schema 四处一致。
接口面真正的问题集中在两处。
其一是孤儿/未兑现风险(T2-01,P1)。整条线自述"待 0 号 spike、Phaser 一行未落、尚未真跑"。app.py 的 _extract_function_tools 里留着一条 2026-06-24 的实证修复注释:此前按 tools/_tools 扁平探测全空,导致每个 chat run 起手即抛、服务态生成零产出,直到 B1 控制面真驱才暴露根本跑不通——这正是"声明了接口但使用路径未走通"的典型。同类未走通的还有 BackendStore 落库、MCP 挂点、L2/L3 段、富游戏三门:都有 schema 形状,但缺端到端被真实调用链验证的证据。这违反项目"不留孤儿/残缺设计"与证据规则。
其二是 screenshot 工具 docstring 的措辞与真实数据流冲突(T2-04,P2)。toolkit.py:265 的 docstring 写"L1/L2 判定零截图参与",但 verdict 的 humanPlayability.renderReflectsState / renderSanity 实际消费的正是 harness 对 canvas 的像素回读结果——play-phaser.cdp.cjs:772 用 captureImageData 抓 canvas 像素、evalRenderReflectsState 按格子比对,studio.py:527 的 derive_l3_egregious 再消费它。需要说清楚的是:纪律本身没破——这些字段全程严格 advisory,egregiousRenderFailure 反复声明不改 decision / L1.passed,只产升门信号。问题纯粹在措辞:"零截图/像素参与判定"这句话会让契约消费方误以为像素从不被读取,而事实是像素被读、只是不进 accept/fix/kill 的 decision。
此外服务态 bootstrap._post 的鉴权头(X-User-Id)自述"留口子待对齐真实 deps"(T2-06,P2),属残缺接口,须在 smoke 时按 app/deps.py 的 get_current_user_id 实现定死。
3. 语言规范与大厂代码质量评估
语言规范和工程质量整体达到大厂可上线水准。中文注释完整、日志可追溯、错误降级统一,符合项目硬约束。真正值得修的质量问题有限且集中。
最突出的是 validate_play_scene 用文本扫描校验平台命脉接线(T2-03,P2)。这道预检决定是否拦 build,并把精确修复指引回喂自纠循环。复核发现它比候选发现描述的稍微稳一点:第①步用了正则 function\s+createPlayScene\s*\(([^)]*)\) 判工厂签名带不带参,不是纯子串。但其余判据仍是裸子串扫描——'bindInput(' in src、'tick(' in src、'handleNormalizedInput' not in src、'play-runtime' in src、class extends Phaser.Scene + 'init(' in src。这套对源码文本脆弱:agent 把 bindInput/tick 写进注释或字符串即可骗过(假绿,门反被绕过);agent 用解构别名、跨行调用、或合法的 init( 出现在别处即被误杀(假红,冤拦 build、误导自纠方向)。相邻的 validate_datatable 走 JSON 解析是结构判定、稳;唯独 play-scene 这道退化成文本扫描。中期应换 AST(node --parse / acorn)。
其次是服务态熔断按回合重建导致 ¥ 硬闸跨 resume 不累计(T2-02,P1)。这条既是成本韧性问题也是质量问题:_tier2_middlewares_factory 每个 chat 回合 new 一组 CircuitBreakerMiddleware(),其 spent_rmb / 计数全在实例上、从 0 起。而控制面 drive_generation 靠多回合 POST /chat 做有界 resume(for attempt in range(max_resumes+1),默认 6,每回合内层 ReAct 又可达 40 轮)。于是 rmb_hard_limit=3.0 的单局 ¥ 硬闸只在单回合内有效,一款游戏跨 6+ 回合实际可烧到约 7 倍单回合上限而硬闸永不触发。app.py:191 注释已自承这是 followup,但它是真实的 fail-closed 成本保护缺口,与 C3"强制硬闸规模化前必须落地"相悖。
异常类 Tier2CircuitBreak 缺 Error 后缀(T2-07,P3,N818),是接口面一部分的小瑕。finish 组装的 addressing 不带 sourceUrl、buildProfile.target 硬编码 es2019 与 schema 示例 es2020 不一致(T2-08,P3),均为表述/一致性小项、无功能风险——复核确认 sourceUrl 确实由 BackendStore.save 在 store.py:527 二次补齐,finish 直接交付时缺省。
被证伪的一条:T2-05 称 prompts.py 的模块级 _file_cache 在多 agent 并发下无锁。复核 prompts.py 发现模块在 line 66 定义了 _lock = threading.Lock(),且 registry 缓存读写(line 105)、file 缓存读写(line 235-256)、reload(line 335)全部包在 with _lock 里。缓存访问实际是同步的,不存在竞态。ruff 的 PLW0603 命中的是 global _file_cache 语句本身(写 global 即触发该规则),但它没看到访问被锁保护。该并发隐患不成立。
4. 静态分析结论
在 mini-desktop 上跑了两个工具,均正常跑通、零崩溃:
- ruff 0.11.19 扫 gen-worker(Python),共 2328 条,其中 2193 条(94%)是 E501 行过长(配置限 88 字符)噪声。排除后实质 135 条:未排序 import(I001·31)、旧式 Optional 注解(UP045·40)、废弃 typing 导入(UP035·5)、未用 import/变量(F401·7 / F841·1)、未用参数(ARG·16,基本是中间件 hook 的
agent参数和工厂占位brief/cfg等接口占位)、subprocess 无 check(PLW1510·3)、若干 SIM/RET 简化项。 - oxlint 1.71.0 扫 engine + harness + fixtures(JS),34 warnings / 0 errors。核心平台代码(engine/ + harness/)零 error。主力是 catch 吞错参数未用(no-unused-vars·18,多为有意静默)和 harness 短路副作用被报 no-unused-expressions(2)。fixtures 是样本生成产物,优先级低于核心代码。
客观结论:没有 error 级问题,绝大多数是格式噪声和接口占位,不构成质量风险。 两处需澄清的误判:① PLW1510 的三处 subprocess.run(run.py:215/246/813)实为误报——三处都在紧邻的 218/248/815 行显式判了 r.returncode,失败不会静默,改 check=True 反而会改变现有的 returncode 处理路径,应在 ruff 配置忽略或加 noqa;② PLW0603(prompts.py:334)如第 3 节所述不构成真隐患,缓存已被锁保护。
5. 问题清单
| 编号 | 严重度 | 维度 | 文件:行 | 问题 | 影响 | 根因 | 修复 |
|---|---|---|---|---|---|---|---|
| T2-01 | P1 | 接口协议 | service/app.py:105 | 服务态生成主链及多处对外能力(BackendStore 落库 / MCP / L2/L3 / 富游戏三门)只有形状、无端到端真跑证据 | 接口声明了但使用路径未走通,B1 真驱才暴露根本跑不通(已有一次实证:九工具抠取全空致零产出) | 整条线"待 spike、尚未真跑";验证只到"服务能启",没真跑过一回合 chat | spike 后把端到端验证列成显式验收清单挂进 .agent:真起 service 跑通 chat→九工具→build→run_gates→finish→落库留四件套证据;BackendStore 真往返一次;富游戏三门对金标/坏样本真判。每补完一项去掉"待 spike"表述并附证据路径 |
| T2-02 | P1 | 外部调用韧性/成本 | service/app.py:192 | 服务态熔断/¥ 硬闸每 chat 回合重建,跨 resume 回合不累计,成本硬闸形同虚设 | 一款游戏跨 6+ resume 回合实际可烧到约 7× 单回合上限而 rmb_hard_limit=3.0 永不触发;fail-closed 成本保护被绕过 | _tier2_middlewares_factory 每回合 new 一组 CircuitBreakerMiddleware,计数在实例上从 0 起;有界 resume 是回合外循环 |
把 spent_rmb 与计数提到 session 维度跨回合累计:经 AgentState/tasks_context 持久化后在 factory 里 reload 注入,或在 drive_generation 层维护跨回合 ¥ 台账每回合前判。规模化(批量生成)前必须修 |
| T2-03 | P2 | 语言规范与质量 | worker/run.py:401 | validate_play_scene 用文本子串/正则启发式校验平台命脉接线,易假绿/假红 | 决定是否拦 build、并回喂自纠指引;agent 把 bindInput/tick 写进注释即骗过(假绿),用别名/换行即误杀(假红、误导自纠) | play-scene 这道预检退化成 substring 扫描(相邻 validate_datatable 走 JSON 解析是结构判定、稳) | 中期改 AST 结构判定(node --parse / acorn 解析,查 createPlayScene 是否带参、是否调 bindInput/tick、是否导出 handleNormalizedInput);短期至少剥注释/字符串后再扫,并在 errors 标注"启发式判定可能误报" |
| T2-04 | P2 | 接口协议 | worker/toolkit.py:262 | screenshot docstring"L1/L2 判定零截图参与"与实际像素回读数据流不符 | 纪律没破(全程 advisory、不进 decision),但措辞会误导契约消费方以为像素从不被读取 | renderReflectsState/renderSanity 与 derive_l3_egregious 消费的正是 harness 对 canvas 的像素回读;docstring 措辞与数据流冲突 | 改成精确表述:"截图与像素回读只产 advisory 观测(renderSanity/renderReflectsState/L3 score),绝不进 L1/L2 的 accept/fix/kill decision、不计入 runnableOk/L1.passed"。语义不变,只校准措辞 |
| T2-06 | P2 | 外部调用韧性 | service/bootstrap.py:99 | 服务态 REST 客户端鉴权头是占位口子,且四步串行启动无重试/幂等 | 鉴权契约未定;new-api 偶发 502 会让启动失败,重跑可能重复注册凭据/建 agent | _post/_patch 用 X-User-Id 留口子待 smoke 对齐;start_new_game 四步 REST 任一步 5xx 即整体抛 | smoke 时按真实 app/deps.py 定死鉴权头并去掉"留口子";给四步加幂等(凭据/agent 确定性名查重复用)+ 对 502/超时有限重试退避 |
| T2-07 | P3 | 语言规范 | worker/middleware.py:149 | 异常类 Tier2CircuitBreak 缺 Error 后缀(N818) | 该异常跨 studio/control_plane 被 catch,命名是接口面一部分,缺后缀降可读性 | 命名未遵 PEP8/阿里规约异常以 Error 结尾 | 若不担心改名半径,一次 git 替换为 Tier2CircuitBreakError(连同所有 import/except)。注:同条提及的 PLW1510 三处 subprocess.run 是误报(已显式判 returncode),应 ruff 忽略或加 noqa,勿改 check= |
| T2-08 | P3 | 接口协议 | worker/toolkit.py:356 | finish 的 addressing 未带 sourceUrl(落库后端补),buildProfile.target 硬编码 es2019 与 schema 示例 es2020 阅读歧义 | 无功能风险;F3"finish 形状=落库形状"在 sourceUrl 这项由 BackendStore 二次补齐、口径略有缝 | sourceUrl 由 store.py:527 落库时补;target 两处硬编码 es2019 而 schema description 举例 es2020(directional 开放、非冲突) | 在 finish/control_plane 的 addressing 注释点明"sourceUrl 由落库后端回填,finish 缺省",与 versionId 后端回填口径对齐;target 值统一或改 schema 示例消除歧义 |
证伪条目(不计入清单):T2-05(prompts.py 模块级 _file_cache 并发无锁)——复核证伪。模块 line 66 定义 _lock = threading.Lock(),缓存读写(105/235-256)与 reload(335)全包在 with _lock 内,访问已同步,无竞态。ruff PLW0603 命中的是 global 语句本身,未识别锁保护。
6. 收尾建议
最该先做的三件,按优先级:
第一,把"spike 后的端到端验证"做成显式可勾选的验收清单,挂进 tier2/.agent(对应 T2-01)。 这是整条线最大的不确定性来源——代码质量再高,只要核心调用链没被真实跑通就还是孤儿。清单至少覆盖:service 真起后跑通一回合完整生成闭环并留四件套证据、BackendStore 真往返一次、富游戏三门对金标 fixture 真判 ACCEPT 对坏样本真判挂。每兑现一项就去掉对应文件里的"待 spike"表述并附上证据路径,让"声称"逐步变成"已验证"。
第二,在规模化(批量生成)开闸前修掉 ¥ 硬闸跨回合失效(T2-02)。 这是唯一一条会直接转成真金白银失控的缺口:成本保护当前只在单回合内有效,而生成天然是多回合 resume 的。把 spent_rmb 和步数计数提到 session 维度跨回合累计——优先走 AgentState 持久化在 factory 里 reload 注入,控制面台账是退路。这条不修,批量跑一旦遇到反复自纠的难题面,单款成本可能数倍于预期上限。
第三,把 validate_play_scene 从文本扫描升级成 AST 判定(T2-03),并顺手校准 screenshot docstring 措辞(T2-04)。 前者关系到自纠循环的正确性——一道既能假绿放水、又能假红误导的预检,会让 agent 朝错误方向修、浪费 resume 预算。后者是零成本的措辞校准,把"零截图参与"改成"截图/像素回读只产 advisory、绝不进 decision",消除契约消费方的误读。两件都属于"现在改便宜、spike 真跑后再发现就贵"的预防性收尾。
T2-06 的鉴权头和幂等可以并到第一件的 smoke 环节一起定;T2-07 / T2-08 是可延后的格式与表述小项,不阻塞开闸。
7.4 wg1
模块评审报告 · wg1 廉价模型 worker(gen-worker)
一、模块概述与健康判断
gen-worker 是 W-G1 廉价模型造游戏线的执行体:用便宜模型(deepseek-v4-flash / MiniMax 系)跑一条「设计 agent 展开一句话 → 自产 gatespec → 代码 agent 写 GameHostFactory → esbuild 打 __GameBundle → CDP 九门真玩 harness → 失败回喂重试」的闭环,最终把过门的 bundle.iife.js 全文作 engineBundle,经 HMAC 签名回调后端落包入 feed。代码分三条接缝:run.py 提供 scaffold/build/play 的底层编排,agent_loop/studio.py 在其上叠 AgentScope 多 agent 闭环,service.py 把 studio 包成 HTTP 服务壳。
模块的工程素养整体偏高。中文注释完整且能追溯到决策出处,外部交互、错误路径、降级口径都写清了「为什么这么做」;日志带 trace_id 贯穿全链并 flush 落盘,便于在 nohup/systemd 下排障;成本、缓存、查重三条旁路都遵循 best-effort 不阻断主链的纪律;HMAC 逐字节对齐、串行锁 finally 幂等释放、临时文件 finally 清理这些资源安全细节也在线。框架适配(AgentScope)被隔离在 agent_loop 内,符合「换框架只改一处」的设计意图,这是值得肯定的分层。
但接口面和外部调用面存在几处真问题,集中在三类:worker 产出的 verdict/trace 没有任何 contracts/ schema 背书(契约真空)、派发入口缺幂等去重、以及 run_studio 的 max_tokens 形参声明了却没接线。再加上一处已退役旧品牌名残留触碰品牌不变量门。
健康判断:底子扎实、注释与可观测性达标,但派发入口的幂等缺口与 max_tokens 未透传是会在生产真烧 token 的实问题,且回调契约处于真空状态,需在切默认产线前补齐。
二、接口协议评估
worker 对外三条接缝职责分层清晰、复用充分——studio 复用 run.py 的 scaffold/build/play、prompt.build_messages、validate、cost,没有重造轮子。问题出在三处。
契约真空(WG1-01)。 任务要求对照的 contracts/agent-loop/verdict.schema.json 与 game-design.schema.json,实为 agent-loop-v1 clicker 模板时代的契约——其字段是 designId/taskId/decision/structureOk/playReport,templateId 枚举锁死 clicker/dodge/runner/match。而 gen-worker 既不产 GameDesign 也不产该形态 Verdict:它产出的是 CDP 九门 verdict.json(guards 逐门布尔)+ engineBundle 回调 + _extract_trace 抽出的 9 维 camelCase trace。两者是不同世代的设计。contracts/ 下虽已有 tier2-verdict.schema.json / tier2-source-project.schema.json,但那是 tier2 富游戏线的,engine 廉价线的 verdict/trace 形态没有任何 schema 约束。这不是「契约漂移」而是「契约真空」——worker↔后端 DifyCallbackReqVO.trace 的子集口径只散落在 service.py 注释里,无机器可校验的单一事实源。
回调入口的孤儿字段(WG1-05)。 service.py 文件头文档(§6.1 job-in)声明 job 携带 callback 字段,但全代码搜不到任何 job.get("callback")——handle_job / build_callback_payload / post_callback 自始至终只用进程级 state.callback_url(来自 --callback 或 DEFAULT_CALLBACK)。这是「声明了入参但无使用路径」的残缺接口:执行器若按文档传 per-job 回调地址,会被无声丢弃,回调发往进程启动时硬绑的地址。
派发入口缺幂等(WG1-02)。 POST /generate 仅靠 threading.Lock 串行化(忙时回 409),对同一 job_id/traceId 的重复投递没有任何去重:WorkerState 里没有 seen 集、没有落盘登记,do_POST 直接起后台线程真生成。执行器侧重试(超时或网络抖动重投)会让 worker 对同一 traceId 再烧一遍便宜模型并再次 HMAC 回调,后端 handleCallback 据 trace_id 定位任务可能被重复建版本/组包。这违反「外部异步交互须幂等」的硬约束。
命名上还有一处不一致(WG1-08):run.py:130 落 play_pass 键,studio.py:296 落 seven_gate_pass,而 service.py:124 的 _extract_trace 只读 seven_gate_pass。当前 service 只编排 studio 路,所以没有实际丢数据,但同一语义两个键名是隐患——若 run.py 路被其他消费方接入,service 抽 trace 会拿不到 sevenGatePass。
三、语言规范与大厂代码质量评估
代码质量整体接近大厂脚本层的合格线。注释密度和可追溯性是亮点,降级路径的纪律性也好——取价失败走 tokens-only 降级并打 costFallback=true 标记、查重异常只 log 不阻断、player panel 单位失败吞成 panel[tag]={error:...},都没有让旁路故障污染主链。
真正有语义影响、值得修的是 max_tokens 接线断裂(WG1-03)。run_studio 签名声明了 max_tokens=16000(studio.py:231),但函数体内从未使用:design/code/fix 三个 agent 经 _agent_reply 走 Agent(react_config=ReActConfig(max_iters=1)),而 _agent_reply 根本不接 max_tokens 形参;底层 config.build_model 也只配了 max_retries=2,没有任何 token 上限透传。对照 run.py:102 那条路——它把 max_tokens 真传进了 _client.chat——可以确认根因是从 run.py 抽 studio 时形参被保留但接线断了。后果是复杂局便宜模型输出触 AgentScope/SDK 默认上限被截断,extract_code 取不到完整 ```js 块,表现为 parse 失败重试,而这正是 _client.py:111 注释亲述、worker 反复踩的那个坑。player 文本位用写死的 4000、视觉位用写死的 900,也是同类魔法数。
视觉位还绕过了重试封装(WG1-06)。_judge_vision(studio.py:190)直接调 client.chat.completions.create,而 _client.chat(_client.py:99)封装了 tries=3 + retry_delay 的 new-api 502 突发重试。需要说明的是,get_client() 给底层 openai 客户端配了 max_retries=2(_client.py:57),所以视觉位并非完全没有重试韧性——它有 SDK 级的 2 次重试,只是少了 _client.chat 那层额外的 3 次带延迟突发循环。new-api 偶发 502 是项目已知坑,视觉位的韧性与同模块其他调用口径不一致,遇网关瞬时抖动更容易直接抛、被 run_player_panel 吞成 panel error、player 顾问位无声失效。
类型注解整体缺失(仅 roles.player_system 有),与大厂 typing 要求有距离,但属脚本层可接受的风格。品牌红线命中一处(WG1-04):service.py:34 的 @author 造梦AI 是已退役旧名,对外品牌现为「绘境AI」,活层代码出现旧名违反品牌不变量门,须改。
四、静态分析结论
在 mini-desktop 上用 ruff 0.15.19 扫 gen-worker/,共报 553 条,其中 19 条可自动修复。目录全为手写源文件,无生成产物。85%(471 条)是 E501 行过长,多为内联 prompt 字符串——roles.py:32 单行 1540 字符是最大命中,不影响运行只是可读性差;其次是 E702 分号多语句(32 条)和 I001 import 未排序(10 条)。这些都是噪声,可一把 ruff --fix + 手动整理超长 prompt。
有语义影响的少数命中值得分别对待:PLW1510(subprocess.run 无 check,4 处)里,validate.py:86 显式判了 r.returncode 故功能正确,但 run.py 的 build/play 两处若子进程因环境问题(node 缺失、脚本路径错)非零退出,会被当成普通失败回喂模型重试,掩盖真正的基建故障;ARG001(studio.py:231 的 max_tokens 未用)正是 WG1-03 的静态信号;SIM105(service.py:350、validate.py:97 的 try-except-pass)和 SIM115(validate.py:82 文件未用 with)属低风险,前者吞异常处都有兜底语义、后者有 finally unlink。无 F 类(未定义/未使用变量)命中,说明基本逻辑完整。
五、问题清单
经逐条对抗式证伪(打开 file:line 核对证据),9 条候选发现全部成立,无误报。其中 WG1-06 的「无重试韧性」表述略有夸大——视觉位仍有 openai SDK 级 max_retries=2,仅少了 wrapper 的额外突发循环——故在下表注明。
| 编号 | 级别 | 维度 | 位置 | 问题 / 影响 / 根因 / 修复 |
|---|---|---|---|---|
| WG1-01 | P1 | 接口协议 | service.py:91-218 | 问题:worker 产 engineBundle + 九门 verdict.json + 9d camelCase trace,无任何 contracts/ schema 背书;要求对照的 verdict/game-design schema 是 v1 clicker 时代契约,字段世代不符。影响:worker↔后端 DifyCallbackReqVO.trace 子集口径只在注释里,无机器可校验单一事实源,键名漂移无门可拦。根因:engine 路是 v1 之后的新世代,沿用旧契约名导致评审锚点对不上。修复:为 worker trace 与九门 verdict 各补一份 contracts/agent-loop schema(借 tier2-source-project.schema.json 范式),或在设计文档显式声明 engine 路不消费 v1 契约并指向新 schema。 |
| WG1-02 | P1 | 外部调用韧性/幂等 | service.py:378-401 | 问题:POST /generate 仅 threading.Lock 串行化,对同一 job_id/traceId 重投无去重(无 seen 集、无落盘登记)。影响:执行器重试触发对同一 traceId 重复真烧便宜模型 + 重复 HMAC 回调,后端可能重复建版本/组包,违反「外部异步交互须幂等」硬约束。根因:服务壳只解决了「不可并发」(serve 4320/CDP 9222 独占),没解决「不可重复处理」。修复:WorkerState 维护 trace_id→状态轻量登记(内存 set 起步,崩溃恢复用 results/ jsonl),命中已 RUNNING/DONE 直接回 202+已存在标记;或与后端约定幂等键归属并文档化。 |
| WG1-03 | P1 | 语言规范/接口残缺 | studio.py:231 | 问题:run_studio(...max_tokens=16000) 形参全函数未使用,design/code/fix 经 _agent_reply→ReActConfig(max_iters=1) 调用,build_model 也未透传 token 上限。影响:复杂局便宜模型输出触默认上限被截断 → extract_code 取不到完整 ```js → 表现为 parse 失败重试,浪费 max_repairs 次真烧 token,正是 _client.py:111 注释亲述的坑。根因:从 run.py(其 _client.chat 真用 max_tokens)抽 studio 时形参保留但接线断开。修复:把 max_tokens 真接进 _agent_reply(AgentScope generate 参数或 build_model 的 generate_kwargs),或删掉误导性形参;同时把 4000/900 魔法数提为命名常量。 |
| WG1-04 | P1 | 项目硬约束/品牌 | service.py:34 | 问题:文件头 @author 造梦AI 是已退役旧名(现为绘境AI)。影响:活层代码出现旧名违反品牌不变量门,wave-close 红线拦截。根因:早期文件头未随改名更新。修复:改为绘境AI 或去掉署名只留模块职责;把 wg1/ 纳入 wave-close 品牌门 rg 扫描范围防回流。 |
| WG1-05 | P2 | 接口协议/孤儿字段 | service.py:8 | 问题:文档声明 job 携带 callback 字段,但全程只用进程级 state.callback_url,job.get("callback") 从未被读取。影响:执行器若按文档传 per-job 回调地址会被无声丢弃。根因:文档列了字段但实现走进程级单一回调。修复:二选一——删掉文档里 job.callback 声明(若设计就是进程级单一地址),或在 handle_job 里 callback_url = job.get("callback") or state.callback_url 并对地址做白名单校验防 SSRF。 |
| WG1-06 | P2 | 外部调用韧性 | studio.py:190 | 问题:_judge_vision 直接调 client.chat.completions.create,绕过 _client.chat 的 tries=3 突发重试封装。影响:视觉位仍有 openai SDK 级 max_retries=2,但少了 wrapper 额外重试,遇网关瞬时 502 更易抛 → 被 run_player_panel 吞成 panel error → 顾问位无声失效。根因:多模态 content(含 image_url)走裸 create,未复用文本路重试。修复:给 _client 增一个支持多模态 content 的 chat 变体(复用同款 tries/retry_delay),_judge_vision 改走它。 |
| WG1-07 | P2 | 语言规范/可观测 | validate.py:86, run.py:48/62 | 问题:subprocess.run 均未传 check(PLW1510)。validate 处显式判 returncode 故正确,但 run.py build/play 处环境类非零退出被当普通失败回喂重试。影响:node 缺失/脚本路径错等基建故障被掩盖,浪费 max_retries 次真烧 token 才暴露。根因:统一捕获 stderr 回喂的写法没区分「模型可修」与「基建必须成功」。修复:对基建类子进程区分 returncode(如 127 command not found)→ 快速失败标 infra_fail 不进回喂循环;并注释为何不用 check=True(要捕获 stderr)。 |
| WG1-08 | P3 | 语言规范/命名一致 | run.py:130 | 问题:run.py 用 play_pass,studio.py:296 用 seven_gate_pass,service.py 只读 seven_gate_pass。影响:当前 service 只编排 studio 路无实际丢数据,但 run.py 路若被接入,抽 trace 拿不到 sevenGatePass。根因:两条路独立演化键名未对齐。修复:统一为 seven_gate_pass,过渡期 _extract_trace 兼容读 a.get("seven_gate_pass") or a.get("play_pass")。 |
| WG1-09 | P3 | 语言规范 | validate.py:82 | 问题:NamedTemporaryFile(delete=False) 后手动 write/close + finally unlink(SIM115)。影响:功能正确(finally 兜底 unlink,且 close 在 subprocess 前),但 with 写法更安全。根因:跨进程读需 delete=False,故未用 with。修复:改 with NamedTemporaryFile(...) as tmp 写入取 tmp.name,退出后 subprocess + finally unlink;或确认现状可接受后加注释说明。 |
六、收尾建议
先做 WG1-02 与 WG1-03,这两条会在生产真烧 token。 派发入口幂等去重是切默认产线前的硬门——执行器重试是必然会发生的,没有 trace_id 登记就会重复烧便宜模型并重复回调污染后端落包。max_tokens 接线则直接关系生成成功率:worker 反复踩的 parse 失败重试,相当一部分就是输出被默认上限截断造成的,接好这根线能省下大量无谓的重试 token,对「成功率冲 80% 门」是实打实的帮助。
其次清掉 WG1-04 品牌红线 + WG1-01 契约真空。 品牌名一行就能改,但它是 wave-close 会红线拦截的硬门,越早清越省事,顺手把 wg1/ 纳入品牌门扫描范围防回流。契约真空不必现在就补完整 schema,但至少要在设计文档里显式声明「engine 路走 engineBundle、不消费 v1 契约,trace 形态见 X」,把 worker↔后端的 trace 子集口径从注释提升为有锚点的约定——否则键名漂移无门可拦,后端 ReadinessScorer 这类消费方会在静默中被破坏。
ruff 噪声一把过即可。 ruff --fix 清掉 19 条自动可修的,再手动把 roles.py:32 等超长 prompt 字符串拆行、整理 import 排序。这类不影响运行,但留着会淹没真正有语义的命中(如下次的 ARG001/PLW1510),值得一次性清干净。