docs(runner-v2): P2 双 spec 定稿——通用宿主泛化(对抗复审4 mustFix全纳+4阻断项拍定)

P2 = 把 P1 装载契约从「装载王蓝莓一款」泛化为「装载任意 GamePackage」入 feed 真玩。
4 路只读侦察→草拟→opus 文档级对抗复审→综合落 review(411行)+execution(303行)。

对抗门逮 4 mustFix 全纳入(主 agent grep 独立复核属实):
- 门②跨层命名:telemetry game_play_end→lifecycle game_end(completed===true)(母spec L66 早警告)
- 体积口径:540KB(开工前 inferred 误值)→实测 259KB raw/88KB gz(SIZES.md:33)
- O2 范围:Phase C 纳入标为扩张请求(待创始人追认)
- ref bundle 产线不在仓:固化补法落实

4 阻断项全拍定→P2 解锁:O2(b)=追认纳入 Phase C(创始人)/O-P2-2=① DB-manifest 内嵌
(创始人·安全红线,bundle 文本进 engineBundle 字段+iframe 内联 script,CSP 不放外域)/
O-P2-1=ref 脚本灌库过渡桩(真产线留 P3)/O-P2-3=globalName __GameBundle。

架构形状:manifest 端点是 application/json 吐不了 .js→bundle 内嵌 GamePackage JSON 的
engineBundle 字段(落既有 package_json LONGTEXT,sha256 照覆盖),后端唯一改动=buildGamePackage
加一行+契约#4 加一可选字段;掏空壳线上零可玩=替换零兼容包袱;受控面6项+game-host.d.ts 零改。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-13 19:14:06 +00:00
parent 767eaf2e18
commit 4caaea8aab
2 changed files with 719 additions and 0 deletions

View File

@ -0,0 +1,307 @@
# P2 · 通用宿主泛化Runner v2 装载任意 GamePackage· 执行版
> **状态**execution 版 · 2026-06-13 · 给执行 agent 实现。配套 review 版 [`2026-06-13-P2-runner-v2泛化宿主-review.md`](./2026-06-13-P2-runner-v2泛化宿主-review.md);母梁 [`2026-06-13-runner-v2-arc-review.md`](./2026-06-13-runner-v2-arc-review.md) §5.2;母 spec [`2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md`](./2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md)Phase B/C/D + 门4/门6
> **修订**2026-06-13 对抗复审 → 纳入 4 mustFix门② lifecycle game_end·非 telemetry / 体积 259KB·非 540KB / O2 范围 Phase C 标扩张 / ref bundle 产线落实)+ 3 shouldFix门③后端 status=1 门禁叠加 / CSP img-src 补全 / bridge.ts 行号校准)。逐条裁决见 §13。
> **模型分流**:一律 opus创始人拍本环境 fable 子代理不可用)。
> **铁律**:下层受控面 6 项契约(`getContext2d/getInput/getAudioContext/getEngine``api.d.ts:269/281/286/303`+ 上层装载契约(`game-host.d.ts:25-59`**零改**;引擎 import 只活 entry/hostQ4区分 verified/inferred/assumption凡引代码事实带 file:line。
> **前置裁决4 阻断项全拍定 · 2026-06-13 创始人 + 主 agent→ P2 全面解锁可直接落地**
> - **O2(b)=追认纳入 Phase C**(创始人拍):掏空壳 `runtime/index.ts:56` 直接改「引擎包加载分支」、**跳过复活 1.x**;母 spec Phase C 条目由 P2 supersede改动记账归 P2。
> - **O-P2-2=① DB-manifest 内嵌**创始人拍·安全红线bundle 文本进 GamePackage JSON `engineBundle` 字段、iframe 内**内联 `<script>` 注入****CSP 保持不放外域、`connect-src 'none'` 不动**(攻击面最小)。首屏 P75 受压再评外域 `<script src>`
> - **O-P2-1=ref 脚本灌库过渡桩**(主 agent 定P2 只跑 ref 一款,用脚本一次性把 `ref-bundle.js` 文本灌进某 version 的 `engineBundle`(过渡桩,须显式标注);**真产线(生成原生出 bundle留 P3**。
> - **O-P2-3=globalName `__GameBundle`**(主 agent 定iife bundle 经 `--global-name=__GameBundle` 导出,宿主从 `window.__GameBundle``GameHostFactory`
---
## 1. 目标 / 范围边界
### 1.1 目标
把 P1 已 spike 实的装载约定从「装载王蓝莓一款」泛化为「装载任意 GamePackage」经发布主链入 feed、真人试玩① 后端把引擎 bundle 随 manifest 下发GamePackage 增 `engineBundle` additive 字段);② studio host 新增「引擎包分支」取代掏空 `startRuntime`,从包里取出 `GameHostFactory``bootGameHost` boot③ 通用宿主 `bootGameHost` = P1 `bootRef` 引擎骨架参数化 +「packageUrl 取包 + 动态取 factory」两步新增④ 一款 engine-aware 游戏P2 阶段=ref 级)经发布主链入 feed、真人浏览器真玩。
### 1.2 范围内
- **后端轨**`DifyCallbackTxService.buildGamePackage``:268-329`)增 `pkg.put("engineBundle", <文本>)`(唯一必要后端代码改动);契约 `game-package.schema.json``engineBundle` 可选字段(唯一契约面 additive 新增);**ref bundle 产线固化 + 跨仓产物取得路径落实**mustFix#4)。
- **前端轨**`game-studio/src/host/inject.ts` 新增引擎包分支(按 `pkg.engineBundle` 分流)+ CSP/sandbox/origin 迁移;`GamePlayer.vue` 取包/超时对齐;`contract.ts` GamePackage 前端副本加 `engineBundle`studio `--mode staging` 重建消 mock。
- **宿主轨**:把 P1 `bootRef` 泛化为 `bootGameHost(opts)`(引擎骨架照搬 + 参数化游戏工厂来源/插件清单/视口);`bootRef` 退化为薄包装保 ref 取证。
- **验收**feed 出 PUBLISHED + 引擎真渲 + lifecycle `game_end`(completed) + 防 demo 假阳性 + 链路③⑤ CDP 真走查。
### 1.3 范围外(非目标)
- 下层受控面 6 项契约 + 上层装载契约(零改)。
- OSS/MinIO沿 DB-manifest 内嵌降级;切 OSS 留 M3
- P3 生成主线agent 写码出 bundle、20 基准 bake-off、`SUPPORTED_TEMPLATE_IDS` 解封)→ W-G1 自身 spec。**但 P2 阶段 ref bundle 产线须先固化§7 后端轨步骤 B0。**
- feed 引擎选型不动HJ-CH-001
- 不重做 M-b 执行器。
- **母 spec Phase C 正文改写**O2(b) 未追认前不动正文studio host 物理改动按 P2 单口径但记账归 P2
---
## 2. 前置条件(含 O2 裁决 + supersede 清单)
| # | 前置 | 状态 / 证据 |
|---|---|---|
| P-1 | P1 整相收口(装载契约立 + engine-caps 模块化 + ref real 通道 + game-shop.js 独立工厂) | verified`game-host.d.ts:25-59``main.js:48` `import createShopGame from './game-shop.js'`P1 收口 `99b6c82` |
| P-2 | 下层交付锚 packageUrl/immutable additive 留位 | verified`game-package.schema.json:66-74` |
| P-3 | 后端 DB-manifest 派生分支活packageUrl 空→ manifest 端点) | verified`RuntimeConvert.java:79-92` |
| P-4 | 后端写包链可挂 bundlebuildGamePackage→storeForVersion→putManifest→package_json | verified`DifyCallbackTxService:268-329`(组包)/`:187-198`storeForVersion 步骤⑥)/`DbPackageStore:27`putManifest 写 package_json 原始 JSON 不重序列化) |
| P-5 | 后端 manifest 端点 play 场景强制 status=1 | verified`RuntimeConvert.java:83-84``AppRuntimeController:75/85-87` |
| P-6 | studio host 掏空壳唯一定义+唯一调用点(改面收敛) | verified唯一定义 `runtime/index.ts:56`,唯一运行调用 `inject.ts:184`grep 全仓) |
| P-7 | mini-desktop 具备 esbuild + chrome6c6g 禁 chrome | verifiedP1 runbook 同口径) |
| **P-8✅ 已解 · 2026-06-13** | O2(b)/O-P2-1/O-P2-2/O-P2-3 四项全拍定 | **已决**O2(b)=追认 Phase CO-P2-2=① DB-manifest 内嵌O-P2-1=ref 脚本灌库过渡桩(真产线 P3)O-P2-3=`__GameBundle`。详见文首「前置裁决」banner。**P2 解锁** |
### 2.1 O2 裁决结果 + 对母 spec 的 supersede 清单
**裁决**:合并 — P2 承接升级母 spec **Phase D + Phase C**O2(b) 2026-06-13 创始人**已追认** Phase C 纳入Phase B 后端侧已落无需重做。
**被 P2 supersede改口径/作废)**
| 母 spec 条目 | 行号 | 处置 | 范围归属 |
|---|---|---|---|
| Phase D 产物=stub-ref 入 feed | `L16`/`L81` | 升级为 engine-aware ref 入 feed | **已拍 Phase D** |
| 门6「CDP feed 卡片→/play 加载真包」止于加载 | `L133` | 抬门=并入 P2 门②(引擎真渲 + `game_loaded` + lifecycle `game_end`(completed) | **已拍 Phase D** |
| Phase C `runtime/index.ts:56` 掏空壳→「URL 加载 1.x runtime」 | `L80` | supersede=跳过复活 1.x直接改「引擎包加载分支」 | **✅ 已追认 O2(b)** |
| Phase C 四项兼容矩阵 + 门5 | `L119`/`L132` | 并入 P2 单口径一次改写CSP/sandbox/origin/CORS | **✅ 已追认 O2(b)** |
**仍有效P2 复用·不改)**Phase B 后端 packageUrl→DB-manifest 链(`RuntimeConvert.java:79-92`已落门4 防 demo 判据(`L131`,含 lifecycle `game_end(completed===true)`**mustFix#1 复用此口径**Phase D 发布主链五步翻态 + 门6 DB 四态断言(`L58`/`L133`,只换过链的包为引擎包);后端发布主链三服务被动验证(`L58`)。
---
## 3. 涉及模块与文件路径
### 3.1 后端 game-cloud最小改 + 产线)
| 文件 | 动作 | 关键行锚 |
|---|---|---|
| `…/aigc/service/callback/DifyCallbackTxService.java` | 改(唯一必要后端代码改动) | `buildGamePackage:268-329``pkg.put("engineBundle", <文本>)`;落在 checksum 占位回填前(使 sha256 覆盖 bundle`storeForVersion``:187-198`/`DbPackageStore.putManifest``:27`**零改**——bundle 随 JSON 自动落 package_json |
| `contracts/game-package.schema.json` | 改(唯一契约面 additive 新增) | `properties``engineBundle``type:string`,可选,描述见 §5不进 `required` |
| `game-runtime` ref bundle 产线 | **新增mustFix#4** | 固化 npm script`package.json` scripts 加 `build:ref-bundle`)产出确定路径 `games/wanglanmei-ref/dist/ref-bundle.js` + 钉死 `--global-name=<O-P2-3 定的固定名>`(现状:`scripts/build.mjs:40` `build(entry,outfile,globalName)` 存在但无 ref 调用方grep 实证) |
| 跨仓产物取得game-cloud ← game-runtime | **新增mustFix#4** | 明确 game-cloud 后端如何拿 game-runtime 构建产物:打包期拷贝 / 构建产物入库 / CI 产物传递三选一O-P2-1 拍)。**不假设后端直读前端仓 dist** |
> **后端被动验证(不改)**:发布主链三服务 `ProjectServiceImpl` / `PublishOrchestrationServiceImpl` / `FeedServiceImpl``FeedServiceImpl:119-159` 二次过滤 PUBLISHED 出流)。
### 3.2 前端 game-studio host主改面
| 文件 | 动作 | 关键行锚 |
|---|---|---|
| `src/host/inject.ts` | 改(引擎包分支 + CSP | `boot():174``pkg.engineBundle?` 分叉(有→引擎包分支,无→`startRuntime:184` 旧路兜底);`CSP:52-53` 按 O-P2-2 三选一迁移DB-manifest 内嵌路 CSP 可不放外域,见 §8`buildIframeSrcdoc:31` 注入 bundle script`buildDemoPackage:296` 兜底路径处置O-P2-5 |
| `src/host/GamePlayer.vue` | 改(取包/超时/sandbox | `resolvePackage:124` 取包(含 `fetchAndVerifyManifest:147` 真包路径);`launch:172``buildIframeSrcdoc:195``loadTimeout setTimeout 5000``:198/:204`)按引擎冷启动对齐;`sandbox:524/532`bundle 加载不改);`case 'game_end':300`(就绪闭环·勿改 telemetry |
| `src/host/bridge.ts` | 改(仅 origin 漂移时) | `buildOriginAllowlist(includeNull):269-283``set.add('null')` `:283`/ `targetOrigin '*':203-204/:220`DB-manifest 内嵌路 origin 仍 'null',不漂移则零改) |
| `src/host/contract.ts` | 改(前端契约副本) | GamePackage 加 `engineBundle?` 与后端 schema 对齐 |
| `src/host/runtime/index.ts` | 改(掏空壳→分支或保留兜底) | `startRuntime:56-113` 掏空壳:引擎包分支接管后,此壳保留作 demo 兜底O-P2-5或彻底替换按 O-P2-5 拍) |
| studio staging | 改(构建消 mock | `--mode staging` 重建,消 `:4173` mock 假数据R3/arc a3 |
### 3.3 宿主轨bootGameHost·P1 bootRef 泛化)
| 文件 | 动作 | 关键行锚 |
|---|---|---|
| `game-runtime/games/wanglanmei-ref/src/main.js`(或抽出独立 host 模块) | 改bootRef→bootGameHost | P1 `bootRef` setupReal 内引擎骨架照搬,抽参数化游戏工厂来源(从 packageUrl 取,替 `main.js:48` 静态 import+ 参数化插件清单D3+ 视口D4`bootRef` 退化为薄包装(传王蓝莓固定工厂/插件清单,保 ref 取证) |
| `game-runtime/SIZES.md` | 改 | 游戏产物口径按 **259KB raw / 88KB gz** 校正mustFix#2`SIZES.md:33` 已坐实,确认现值无误即可) |
> **P1 复用零改**`game-runtime/src/core/game-host.d.ts`(装载契约)、`host-dev/engine-caps.js`共享模块、bootRef 引擎五回调骨架(参数化非重写)。
---
## 4. 数据流与依赖
```mermaid
sequenceDiagram
participant CB as 生成回调(M-b)
participant TX as DifyCallbackTxService
participant DB as game_runtime_package.package_json
participant EP as AppRuntimeController/manifest
participant GP as GamePlayer.vue
participant IJ as inject.ts(srcdoc)
participant BH as bootGameHost
participant LJS as littlejsengine
CB->>TX: handleCallbackTx
TX->>TX: buildGamePackage:268 +pkg.put(engineBundle,<文本>)
TX->>DB: storeForVersion:198 → putManifest 写 package_json(含 engineBundle)
Note over GP: 玩家点 feed 卡 → /play
GP->>EP: GET /package/{vid}/manifest (scene=play·后端强制 status=1)
EP-->>GP: 裸 JSON(含 engineBundle)·字节一致
GP->>GP: fetchAndVerifyManifest:147 sha256==checksum 校验
GP->>IJ: buildIframeSrcdoc:195(pkg 含 engineBundle)
IJ->>IJ: boot():174 分叉 pkg.engineBundle?
alt 有 engineBundle(引擎包分支)
IJ->>IJ: 注入内联 <script>engineBundle</script> → window.<globalName>
IJ->>BH: bootGameHost({factory:取自全局名, pkg, canvas})
BH->>LJS: setGLEnable(false)+engineInit 五回调
LJS-->>BH: .then → mainContext 就绪
BH->>BH: game.init({ctx,mainContext,canvas,seed})
loop 引擎内部 RAF
LJS->>BH: gameUpdate→game.update / gameRender→game.render(mainContext)
end
BH->>GP: sdk.__emit('game_loaded')→bridge→GamePlayer:292
BH->>GP: 结算 sdk.__emit('game_end',{completed:true})→bridge→GamePlayer:300
else 无 engineBundle(向后兼容/demo 兜底)
IJ->>IJ: startRuntime:184 掏空壳 → game_error「重构中」
end
```
**关键数据流不变量**
- bundle 载体经 `package_json` LONGTEXT 随 manifest 一跳下发sha256(整包) 覆盖 bundle 完整性(无第二校验面)。
- 取包一律 **play 场景**(不拼 `?scene=preview`),后端 manifest 端点强制 status=1shouldFix#1 第一道门禁)。
- 就绪闭环唯一信号=lifecycle `game_end``GamePlayer.vue:300`**非** telemetry `game_play_end`mustFix#1)。
- 引擎掌帧/绘制面/能力面三不变量沿 P1掌帧唯一源=引擎 / 绘制面=mainContext / 能力面=ctx.getEngine())。
---
## 5. 接口 / 契约(复用 + additive 增量逐字写清)
### 5.1 复用(零改)
- **上层装载契约**`game-runtime/src/core/game-host.d.ts:25-59`verified`GameHostBootContext{ctx,mainContext,canvas,seed}` / `GameInstance{init,update,render,onInput?,destroy}` / `GameHostFactory=(opts?)=>GameInstance`。P2 通用宿主 import 它、取 factory、按五生命周期驱动。**零改。**
- **下层交付锚**`contracts/game-package.schema.json:66-74`verified`packageUrl`additive 可选MVP 降级=DB-manifest 端点)+ `immutable`。**零改。**
- **下层受控面 6 项**`api.d.ts:269/281/286/303``getContext2d/getInput/getAudioContext/getEngine`。**零改。**
- **宿主↔runtime 稳定边界**(保不破):`RuntimeSDKView.__emit/track/reportError/saveState/loadState` + `RuntimeHandle.destroy` + GamePlayer lifecycle emit 映射(`game_loaded:292`/`game_end:300`)。
### 5.2 additive 增量(唯一契约面新增,逐字)
**`contracts/game-package.schema.json` `properties` 增**(与 `packageUrl`/`immutable` 同级,不进 `required`
```jsonc
"engineBundle": {
"description": "引擎+包装层+游戏代码的可执行 iife bundle 文本T1b-β P2 通用宿主载体additive 可选,存量消费者无此字段时走 startRuntime 旧路/demo 兜底。MVP 默认=随 manifest 内嵌经 DB-manifest 端点下发(落 game_runtime_package.package_json LONGTEXTsha256 整包 checksum 覆盖其完整性,不接 OSS、不增第二校验面切 OSS 后改为 packageUrl 外链。iife 顶层全局名固定为 <O-P2-3 定的固定名>(宿主据此从 window 取 GameHostFactory。后端写权=DifyCallbackTxService.buildGamePackage put。",
"type": "string"
}
```
> **[verified]** 现 schema 无 `engineBundle`/`engineCode`/`engineUrl`grep contracts/+game-studio/+game-cloud/+game-runtime/ 零命中)。本字段是 P2 唯一契约面新增。`game-studio/src/host/contract.ts` GamePackage 前端副本同步加 `engineBundle?: string`
> **[verified]** bundle 随 JSON 落 `package_json` 后,`buildGamePackage` 末段 `bundleSize = T_base.getBytes(UTF_8).length` 自然涵盖 bundle 字节门禁语义成立checksum=sha256(整包文本) 覆盖 bundle 完整性。
---
## 6. 关键实施步骤(分三轨·有序)
> 三轨可部分并行(后端轨/宿主轨弱耦合),但**前端引擎包分支依赖宿主轨 `bootGameHost` 签名 + 后端轨 `engineBundle` 字段**,故前端轨末做。开工前 P-8 待拍项O2(b)/O-P2-1/2/3必须先拍。
### 6.1 后端轨game-cloud + 产线)
- **B0mustFix#4·产线先固化**
- `game-runtime/package.json` scripts 加 `build:ref-bundle`,内部 `node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=<O-P2-3 固定名>`(钉死路径+globalName
- 落实 game-cloud 后端取 ref bundle 文本的路径O-P2-1 三选一):打包期拷贝 / 构建产物入库 / CI 产物传递。**P2 仅跑 ref 一款时,可显式降级为「脚本一次性把 ref-bundle.js 文本灌入 DB 某 versionId 的 package_json.engineBundle」过渡桩**(须在代码注释+ §11 标过渡桩P3 接生成主线改随回调入参)。
- **B1唯一必要后端代码改动**`DifyCallbackTxService.buildGamePackage``:268-329`)在 checksum 占位回填前增 `pkg.put("engineBundle", <bundle 文本>)`(来源=B0 落实的取得路径)。`storeForVersion``:198`/`DbPackageStore.putManifest``:27`/表/端点零改。
- **B2契约**`game-package.schema.json``engineBundle`§5.2 逐字);前端 `contract.ts` 副本同步。
- **B3验证·后端只读**:发布主链三服务不改,被动验证(门①)。
### 6.2 宿主轨bootGameHost·P1 bootRef 泛化)
- **H1**:把 P1 `bootRef` setupReal 内引擎骨架(`setGLEnable(false)``engineInit` 五回调→`.then(setupReal)``createHostDevContext(context2d=mainContext)``createEngineCaps``game.init``__refBootedReal`)抽成 `bootGameHost(opts)`,其中:
- **游戏工厂来源参数化D1/D2**:替 P1 `main.js:48` 静态 `import createShopGame`——`bootGameHost``opts.factory`(宿主已从 bundle 全局名取出的 `GameHostFactory`)建实例。
- **插件清单参数化D3·O-P2-4**P2 阶段先用王蓝莓 7 件作 `opts.plugins` 默认标准集。
- **视口参数化D4**`opts.viewport`P2 阶段先用王蓝莓 390×844后续从 GamePackage 取)。
- **H2**`bootRef` 退化为薄包装:`bootGameHost({factory:createShopGame, plugins:王蓝莓7件, viewport:390×844, recHook})`,保 ref 取证全局名(`__refForensics/__refHost/__refBootedReal`)。**三者共用同一引擎骨架与 engine-caps 模块,不产生第三套。**
- **H3**SIZES.md 确认游戏产物口径=259KB raw / 88KB gz`SIZES.md:33` 已坐实校对无误即可mustFix#2)。
### 6.3 前端轨studio host 引擎包分支)
- **F1分流·依赖宿主轨 H1 + 后端轨 B1**`inject.ts boot()``:174`)加 `pkg.engineBundle?` 分叉:
- **有 `engineBundle`**iframe 内注入内联 `<script>${pkg.engineBundle}</script>`DB-manifest 内嵌路·非外域 `<script src>`)→ 取 `window.<globalName>``GameHostFactory``bootGameHost({factory, pkg, canvas})`(异步)。
- **无 `engineBundle`(向后兼容/demo 兜底)**:走旧 `startRuntime(canvas, CTX.pkg, runtimeView)``:184`)显示「重构中」。
- **F2CSP 迁移·O-P2-2**:按三选一拍定迁移 `inject.ts:52-53`DB-manifest 内嵌路:`script-src 'unsafe-inline'` 不放外域、`connect-src 'none'` 不放——bundle 经宿主层 fetch 非 iframe 内 fetch`img-src data: blob: https:` 已够现有资产通道,**仅当引擎运行时 fetch 资产才须放 connect-src**,见 §8 失败路径)。
- **F3取包/超时)**`GamePlayer.vue resolvePackage:124` 取包一律 play 场景;`fetchAndVerifyManifest:147` sha256 校验复用;`loadTimeout``:198/:204` 5s按引擎冷启动 4.8~5.3× 对齐DB-manifest 内嵌路靠 HTTP 缓存兜跨游戏复用;首屏 P75 CDP timing 实测,必要时放宽阈值——须 review 确认放宽不破 game_load_failed 降权语义)。
- **F4兜底处置·O-P2-5**demo 兜底(`buildDemoPackage:296`)默认保留走掏空壳显示「重构中」;门③显式断言「兜底触发即 fail」。
- **F5staging 消 mock**studio `--mode staging` 重建,消 `:4173` mock 假数据。
---
## 7. 边界失败路径
| 失败 | 触发 | 处置 |
|---|---|---|
| **CSP 死结(外部 bundle** | `connect-src 'none'`+`script-src 'unsafe-inline'` 封死外域加载(`inject.ts:52-53`verified | 走 DB-manifest 内嵌路bundle 文本经宿主层 fetch、iframe 内注入内联 `<script>`CSP 不放外域);回滚=保留 srcdoc 旧分支(`startRuntime:184` |
| **引擎运行时 fetch 资产被 connect-src 拦** | 引擎 bundle 试图 iframe 内 fetch `assets[].url`(非 `<img>`/`drawImage` | 沿现有「资产 URL 进 GamePackage、iframe 内 `<img>`/`drawImage` 加载」通道(`img-src data: blob: https:` 已放行shouldFix#2);若引擎必须 fetch 则纳入 CSP `connect-src` 决策O-P2-2 |
| **取包失败 / 非发布包** | versionId 未就绪 / status≠1 / 越权 | 后端 play 场景 manifest 端点强制 status=1`RuntimeConvert.java:83-84`,第一道);前端 demo 兜底触发→门③判 fail第二道 |
| **demo 兜底假阳性混入真发布** | `buildDemoPackage:296` 取包失败时注入 | 门③断言checksum≠占位 0^64 ∧ gameId/versionId 与请求一致 ∧ 非 demo 包;兜底触发即 fail |
| **装载失败(取不到 factory** | bundle 全局名不匹配 / iife 未导出 | 宿主 try-catch 取 `window.<globalName>`,缺失→发 `game_error`(经 sdk→ GamePlayer 错误态;门②等不到 `game_loaded`=fail |
| **就绪信号跨层误判** | 误用 telemetry `game_play_end` 判就绪 | **门② 锁 lifecycle `game_end`(completed===true)**`GamePlayer.vue:300`telemetry 仅埋点mustFix#1 |
| **加载超时** | 引擎冷启动逼近 5s | `loadTimeout``:198`)→ `game_load_failed` 降权占位DB-manifest 内嵌路靠 immutable HTTP 缓存跨游戏复用引擎缓解;首屏 P75 CDP 实测坐实 |
| **掏空壳替换破契约面** | 误改 `RuntimeSDKView`/`RuntimeHandle` 签名 | code review + 契约 diff 卡死宿主↔runtime 稳定边界硬约束) |
| **ref bundle 产线游离** | 后端读不到/读到过时产物 | B0 固化 npm script + 跨仓取得路径过渡桩须标注mustFix#4 |
---
## 8. 验证方法(本机可验项 + mini-desktop 真机门)
> 6c6g 仅只读侦察 + 写文档(禁 chrome/重进程/整树构建);真机门全在 mini-desktop。部署沿现行 staging 配方(按 studio/后端联调既有 runbook勿在 6c6g 整树构建)。
### 8.1 本机可验项只读·6c6g
- 契约 diff`game-package.schema.json` 仅 additive 增 `engineBundle``packageUrl`/`immutable`/受控面零改diff 证)。
- 后端改面收敛:`buildGamePackage` 仅增一 `pkg.put``storeForVersion`/`DbPackageStore`/端点零改diff 证)。
- 装载契约零改:`game-host.d.ts` diff 空。
- bundle 唯一性grep 确认 studio host 引擎包分支只一处取 `window.<globalName>``startRuntime` 旧分支保留作兜底。
- 就绪信号正确grep 门②/driver 用 `game_end``game_play_end`
### 8.2 mini-desktop 真机门(防假绿)
| 门 | 判据(可执行阈值) | 口径 / 锚 |
|---|---|---|
| **① feed 出 PUBLISHED** | 后端 `feed/stream` 出该游戏 `status=PUBLISHED(4)`DB 四态 `project.status==4 ∧ game_version.status==3 ∧ runtime_package.status==1 ∧ feed_rank 行存在` | 复用母 spec 门6 `L133` DB 四态feed 二次过滤 `FeedServiceImpl:119-159` |
| **② 引擎真渲非「重构中」** | 真人浏览器开 feed→点卡→/play→**引擎 bundle 真渲非「模板体系重构中」** + `game_loaded` + **lifecycle `game_end``completed===true`**console 零未捕获 | **mustFix#1**:就绪经 `sdk.__emit('game_end')`→bridge→`GamePlayer.vue:300``sdk-interface.d.ts:24` lifecycle**非** telemetry `game_play_end``events.schema.json:62`)。防掏空壳假绿:掏空壳 `runtime/index.ts:~103``game_error` 不发 `game_loaded`→ 走它则永等不到 `game_loaded`=fail |
| **③ 防 demo 假阳性** | (后端第一道) play 场景 manifest 端点强制 status=1`RuntimeConvert.java:83-84`(前端第二道) checksum=`game_runtime_package.checksum`(权威)∧ 包内 gameId/versionId 与请求一致 ∧ 非 `buildDemoPackage` 注入 ∧ **兜底触发即 fail** | **shouldFix#1 两道叠加**。门③读 `game_runtime_package.checksum`(经 RespVO.checksum**勿读 `game_version.checksum/bundle_size`**鸡蛋环恒空R2 verified |
| **④ 链路③⑤ CDP 真走查** | CDP 真触摸 feed 卡→/play 加载真包→引擎真玩studio `--mode staging` 重建消 mock`:4173` 阻断) | 防 mock 假数据假绿R3/arc a3走真后端 feed |
| **⑤ 首屏 P75性能** | feed 首屏 P75<3sCDP timing 实测引擎冷启动逼近 5s `game_loaded` 超时须坐实 | `GamePlayer.vue:198/204` 超时DB-manifest 内嵌路 immutable HTTP 缓存跨游戏复用 |
| **⑥ 体积坐实** | `size.mjs` 实测 P2 游戏产物(引擎+包装+游戏代码gz ≤ B1 入场券 gz≤350KB | **mustFix#2**ref 实测 259KB raw / 88KB gz`SIZES.md:33`**勿传 540KB**,开工 size.mjs 复测 |
---
## 9. 完成条件
- 契约:`game-package.schema.json` additive 增 `engineBundle`§5.2 逐字)+ 前端 `contract.ts` 副本同步;`packageUrl`/`immutable`/受控面 6 项/装载契约**零改**diff 证)。
- 后端:`buildGamePackage:268-329` 仅增 `pkg.put("engineBundle",…)``storeForVersion`/`DbPackageStore`/端点零改diff 证ref bundle 产线固化(`build:ref-bundle` npm script + 钉 globalName+ 跨仓取得路径落实或过渡桩标注mustFix#4)。
- 宿主:`bootGameHost(opts)` 由 P1 bootRef 引擎骨架参数化(游戏工厂来源/插件清单/视口);`bootRef` 退化为薄包装保 ref 取证不产生第三套host-dev/bootRef/bootGameHost 共用引擎骨架+engine-caps
- 前端:`inject.ts boot()` 引擎包分支(`pkg.engineBundle?` 分流落地CSP 按 O-P2-2 迁移demo 兜底处置O-P2-5staging 重建消 mock。
- 真机门全绿门①feed PUBLISHED ∧ 门②引擎真渲+`game_loaded`+lifecycle `game_end`(completed) ∧ 门③防 demo 假阳性(后端 status=1 + 前端三锚)∧ 门④CDP 真走查 ∧ 门⑤首屏 P75<3s 体积 gz350KB
- 一款 engine-aware 游戏P2=ref 级)经发布主链入 feed、真人浏览器真玩一局截图归档。
- O2(b) 范围若追认studio host 改动记账归 P2母 spec Phase C/D 受影响条目按 §2.1 supersede 清单更新。
---
## 10. 回滚策略
- **前端**`pkg.engineBundle` 缺失自动走 `startRuntime:184` 旧分支(向后兼容内建);引擎包分支若崩→移除分叉、退回掏空壳(线上零可玩路径,回滚不破存量)。
- **CSP**:若外域加载路 CSP 放宽出问题→退 DB-manifest 内嵌路CSP 不放外域)。
- **后端**`engineBundle` 字段 additive存量消费者无此字段回退原链路`buildGamePackage` 增的一行可单独 revert不影响组包其余
- **契约**`engineBundle` 移除不破存量additive
- **宿主**`bootGameHost` 泛化若回归→ ref 退回 P1 `bootRef` 直 importgit revert 泛化提交P2 不连累 P1 既有交付。
- **产线**ref bundle 过渡桩可随时切回脚本灌库。
---
## 11. 待创始人拍 Open Items开工前·阻断项标注
| # | 待拍项 | 阻断? | 默认建议 |
|---|---|---|---|
| **O2(a)** | Phase D 合并(已拍·确认) | 否 | 确认stub-ref 无产品价值) |
| **O2(b)** | **Phase C 纳入**(扩张·待追认) | **是(定范围记账)** | 追认纳入Phase C 与 P2 在 `runtime/index.ts:56` 唯一掏空壳互斥改法) |
| **O-P2-1** | **bundle 载体来源 + ref 产线 + 跨仓取得路径**mustFix#4 | **是** | P2(ref)阶段固化 `build:ref-bundle` + 跨仓产物传递(不假设直读前端仓 dist仅跑 ref 时可脚本灌库过渡桩P3 改随回调入参 |
| **O-P2-2** | **bundle 装 iframe 三选一**(安全红线·定 CSP | **是** | 先 ① DB-manifest 内嵌CSP 不放外域、改动最小、259KB 解析可接受);首屏 P75 受压再评 ② 外域 |
| **O-P2-3** | **固定 globalName** | **是(定宿主取 factory** | 钉 `__GameBundle``WanxiangGameBundle`(与 O-P2-1 产线一并固化) |
| **O-P2-4** | 插件清单归属(固定标准集 vs 随包给) | 否 | P2 先用王蓝莓 7 件标准集P3 多品类再拍 |
| **O-P2-5** | demo 兜底是否走引擎 | 否 | 保留 demo 走掏空壳「重构中」;门③兜底即 fail |
> 阻断三项:**O-P2-2定 CSP 改法)/O-P2-3定宿主取 factory/O-P2-1定 bundle 从哪来+产线)** 不拍无法落地;**O2(b)** 定范围记账须追认。建议优先拍这四项。
---
## 12. 风险与假设(诚实标注)
- **[verified]** P1 既成:`game-host.d.ts:25-59`(装载契约)/`main.js:48`game-shop.js 独立工厂)/engine-caps 模块化;下层 `packageUrl`/`immutable``game-package.schema.json:66-74`);后端 DB-manifest 派生(`RuntimeConvert.java:79-92`);后端写包链(`DifyCallbackTxService:268-329/187-198``DbPackageStore:27`manifest 端点 play 场景 status=1`RuntimeConvert.java:83-84``AppRuntimeController:75/85-87`);掏空壳唯一定义+调用(`runtime/index.ts:56`/`inject.ts:184`CSP 完整(`inject.ts:52-53`,含 img-src/style-srclifecycle `game_end``sdk-interface.d.ts:24`/`GamePlayer.vue:300`)≠ telemetry `game_play_end``events.schema.json:62`GamePlayer 流(`resolvePackage:124`/`fetchAndVerifyManifest:147`/`launch:172`/`buildIframeSrcdoc:195`/`loadTimeout:198`/`sandbox:524-532`inject 流(`buildIframeSrcdoc:31`/`boot:174`bridge origin`buildOriginAllowlist:269-283`/`targetOrigin:203-204/220`);契约无 engineBundlegrep 零命中ref bundle 实测 259KB raw / 88KB gz`SIZES.md:33``ref-bundle.js`=265762Bref 产线游离(`scripts/build.mjs:40` 有 build()无 ref 调用方)。
- **[inferred]** DB-manifest 内嵌是 CSP 改动面最小路(基于 `inject.ts:52-53` CSP 语义 + manifest 端点裸 String 机制;须开工实测 iframe 内联 259KB 解析/首屏代价)。
- **[inferred]** 引擎冷启动 4.8~5.3× 可能逼近 5s `game_loaded` 超时(记忆 + `GamePlayer.vue:198`);首屏 P75 须 CDP timing 实测坐实。
- **[assumption]** P2 阶段「ref 级固定产物 + 王蓝莓 7 件标准集 + 390×844 视口」是过渡口径P3 接生成主线后重定。
- **[assumption]** O-P2-1 若取「脚本灌库过渡桩」,则后端「读固定产物」是过渡能力,非生产链路,须代码注释+本节标注。
- **[未决]** O2(b)/O-P2-1/O-P2-2/O-P2-3 阻断O2(a)/O-P2-4/O-P2-5 非阻断。
---
## 13. 对抗复审裁决依据2026-06-13 · 逐条)
| 编号 | 复审条目 | 裁决 | 落地位置 / 依据file:line |
|---|---|---|---|
| **mustFix#1** | 门②用 telemetry `game_play_end` 作就绪信号·跨层错 | **纳入**:门②/§4/§8 锁 lifecycle `game_end`(completed===true)`GamePlayer.vue:300`telemetry 仅埋点 | §4 不变量 + §8 门② + §7 失败路径;依据 `sdk-interface.d.ts:24` vs `events.schema.json:62`、母 spec `L66`/`L131` |
| **mustFix#2** | 体积 540KB 全错·与实测矛盾 | **纳入**:全稿 259KB raw / 88KB gz门⑥/H3/§12 校正,标勿传 540KB | §3.3 H3 + §8 门⑥ + §12依据 `SIZES.md:33``ref-bundle.js`=265762B |
| **mustFix#3** | O2 从 Phase D 单方面扩到 Phase C+D 陈述为既定 | **纳入**O2 拆 (a) Phase D 确认 + (b) Phase C 扩张待追认O2(b) 未追认前不改母 spec Phase C 正文 | 头部前置裁决 + §2/§2.1 + §11依据 P1 review `L151`/arc O2 `L279`(只 Phase D |
| **mustFix#4** | O-P2-1 隐含未落实产线(产线不在仓/跨仓路径未定/globalName 未钉) | **纳入**B0 固化 `build:ref-bundle`+钉 globalName + 跨仓取得路径(或过渡桩标注) | §3.1 + §6.1 B0 + §7 + §11 O-P2-1依据 `scripts/build.mjs:40`(有 build()无 ref 调用方grep 实证) |
| **shouldFix#1** | 门③未用后端 play 场景 status=1 门禁 | **纳入**:门③两道叠加(后端 status=1 + 前端三锚),取包一律 play 场景 | §4 不变量 + §8 门③;依据 `RuntimeConvert.java:83-84``AppRuntimeController:75/85-87` |
| **shouldFix#2** | CSP 漏 img-src | **纳入**CSP 补全img-src/style-src+ 资产通道注(现有 `<img>`/`drawImage` 已够,引擎运行时 fetch 才触 connect-src 死结) | §6.3 F2 + §7 失败路径;依据 `inject.ts:52-53` 真源 |
| **shouldFix#3** | bridge.ts 行号失准 | **纳入**:校准 `bridge.ts:269-283`buildOriginAllowlist/set.add('null')+ `:203-204/220`targetOrigin | §3.2 文件表;依据 `bridge.ts:203-204/220/269-283` |
> 7 条全纳入,无反驳、无静默忽略。

View File

@ -0,0 +1,412 @@
# P2 · 通用宿主泛化Runner v2 装载任意 GamePackage· 评审版
> **状态**review 版 · 2026-06-13 · 给创始人决策(结论先行、不写代码级执行细节、多用 Mermaid。基于 4 路只读侦察R1 studio 宿主 / R2 后端发布 / R3 母 spec 对账 / R4 P1 泛化点)+ **一轮对抗复审整改**4 mustFix 全纳入 / 3 shouldFix 纳入),全程读真源码,证据带 file:line。
> **定位**Runner v2 全弧 **P2 相**(第二相)。母梁见 [`2026-06-13-runner-v2-arc-review.md`](./2026-06-13-runner-v2-arc-review.md) §5.2P1 见 [`2026-06-13-P1-王蓝莓上引擎-review.md`](./2026-06-13-P1-王蓝莓上引擎-review.md)。
> **P1 已落事实(改变本稿前提)**P1 整相收口(`99b6c82`2026-06-13——装载契约 `game-runtime/src/core/game-host.d.ts` 已立(`game-host.d.ts:25-59`verified`engine-caps.js` 已提升为可 import 模块、王蓝莓 `?engine=real` 同屏 + mini-desktop real 引擎 5 门全过、`game-shop.js` 已拆为独立 game 工厂(`main.js:48` `import createShopGame from './game-shop.js'`verified。**故 arc O1 已被 P1 自答P2 装载契约是既成事实,不再等拍。**
> **前提(创始人已拍)**:① 推进 P1→P2→P3 全弧、连续驱动;② **O2arc/P1 原口径)= P2 合并进母 spec Phase D**(仅 Phase D见 §2.1 范围澄清)。本稿把 O2 裁决精确化为第一结论,并显式标注「把 Phase C 一并纳入」属对已拍范围的扩张请求(待追认)。
> **✅ 4 阻断项全拍定2026-06-13→ P2 解锁****O2(b)=追认纳入 Phase C**(创始人);**O-P2-2=① DB-manifest 内嵌**创始人·安全红线CSP 不放外域);**O-P2-1=ref 脚本灌库过渡桩**(主 agent真产线留 P3**O-P2-3=globalName `__GameBundle`**(主 agent。execution 版文首「前置裁决」banner 为权威记录。
---
## 0. 本稿要回答的六件事(导航)
| § | 问题 | 一句话结论 |
|---|---|---|
| **1** | 两层复用论证 | P2 **零新造跨仓契约**:下层 `packageUrl`+`getEngine()` 已冻、上层 `game-host.d.ts` P1 已 spikeP2 用 R4 泛化 delta 证明是「泛化既有 bootRef」非「新建」。唯一契约面新增=GamePackage 上一个 additive `engineBundle` 可选字段。 |
| **2** | O2 裁决 | **合并**P2 承接并升级母 spec **Phase D**(已拍);**并请求把 Phase C 一并纳入**(扩张·待追认,依据=与 P2 在 `runtime/index.ts:56` 唯一掏空壳上互斥改法。supersede 清单见 §2.3。 |
| **3** | 三段设计 | ① 后端写包+manifest 交付 bundleDB-manifest 内嵌,零新端点);② studio 引擎包分支取代 srcdoc 内联CSP/sandbox 迁移点标实);③ 通用宿主从 bootRef→装载任意包R4 三步 delta。 |
| **4** | 验收门(防假绿) | feed 出 PUBLISHED + 真渲非「重构中」+ **lifecycle `game_end`(completed===true)**(非 telemetry `game_play_end`mustFix#1+ checksum/gameId/versionId 三锚 + 兜底触发即 fail + 链路③⑤真走查。 |
| **5** | 爆炸半径 + 风险/兼容 | srcdoc→外部 bundle 的 CSP、掏空壳替换零向后兼容包袱、与母 spec 双写、**~259KB raw / ~88KB gz 体积口径**mustFix#2 已纠,非 540KB。 |
| **6** | 非目标 + Open Items | 承 arc O2/O3/O5补 P2 自身 5 新待拍项bundle 载体/CSP 边界/分流点等)+ Phase C 范围扩张追认。 |
---
## 1. 结论先行两层全复用P2 不另造跨仓契约
**P2 = 把 P1 已 spike 实的「同一套装载约定」从「装载王蓝莓这一款」泛化为「装载任意 GamePackage」经发布主链入 feed、真人试玩。它不引入任何新跨仓契约——下层、上层都已冻结P2 只补「取包 + 装进 iframe + 取出 factory」这条产品化管线外加 GamePackage 上一个 additive `engineBundle` 可选字段(唯一契约面新增)。**
```mermaid
graph TB
subgraph 下层["下层契约【已冻·复用·零改】"]
PU["packageUrl + immutable<br/>game-package.schema.json:66-74<br/>(交付/加载锚·additive 已留位)"]
GE["PluginContext.getEngine()<br/>(插件↔引擎受控面·api.d.ts:303)"]
end
subgraph 上层["上层装载契约【P1 已 spike·复用·零改】"]
GH["game-host.d.ts:25-59<br/>GameHostBootContext(4字段)<br/>+GameInstance(5生命周期)<br/>+GameHostFactory"]
end
下层 -.托底.-> 上层
上层 ==P2 泛化(非新建)==> P2
subgraph P2["P2 通用宿主(本稿)"]
direction LR
B1["①后端写包<br/>bundle 随 manifest 下发<br/>+engineBundle 字段(唯一 additive)"]
B2["②studio 引擎包分支<br/>取代掏空 startRuntime"]
B3["③bootRef→装载任意包<br/>R4 三步 delta"]
B1 --> B2 --> B3
end
```
### 1.1 下层packageUrl + getEngine为何 P2 直接复用
**[verified]** 交付锚已就位:`contracts/game-package.schema.json:66-74``packageUrl` + `immutable`**additive 已留位字段**描述里明写「MVP 默认降级=复用后端 `game_version.package_url` 走 DB-manifest 端点 `/app-api/runtime/package/{versionId}/manifest`,天然含 versionId 版本化,不接 OSS、不改写链」。后端派生分支 `RuntimeConvert.resolveManifestUrl` 实体存在且活R2 验,`RuntimeConvert.java:79-92``packageUrl` 空 → 派生 `/app-api/runtime/package/{versionId}/manifest`)。**P2 沿用此端点,后端 packageUrl 写链零改。**
**[verified]** 引擎能力面 `getEngine()` 已冻(`api.d.ts:303`P1 装载契约头注 `game-host.d.ts:18-19` 明示游戏经 `ctx.getEngine()` 拿插件能力。P2 装进 iframe 的引擎包,插件能力面复用同一份 `engine-caps.js`P1 已模块化),**不重建**。
### 1.2 上层game-host.d.ts为何 P2 直接复用
**[verified]** P1 已把上层装载契约 spike 实并落盘:`game-host.d.ts:25-30``GameHostBootContext``ctx`/`mainContext`/`canvas`/`seed` 4 字段)、`:42-48``GameInstance``init`/`update`/`render`/`onInput?`/`destroy` 5 件)、`:59``GameHostFactory``(opts?) => GameInstance` 纯模块默认导出、零引擎 import。文件头注 `:2` 已明写消费方=「集成段 hostboot 游戏)+ 生成 agent产出靶」——**P2 通用宿主正是「集成段 host」消费方P1 设计时已为 P2 预留**。
P2 对此契约**零改**:通用宿主 import `game-host.d.ts`,从取回的 bundle 里取出一个 `GameHostFactory`,按 `GameInstance` 五生命周期经 `engineInit` 五回调驱动(与 P1 bootRef 同范式)。
### 1.3 用 R4 泛化 delta 证明「泛化既有」非「新建」
**[verified · R4]** P1 的 `bootRef`real 通道)已完成「装载一款游戏跑引擎」的 **(c) engineInit 五回调骨架 100% + (b) 取 factory 的一半(静态 import `game-shop.js``main.js:48`**,缺的只有 **(a) packageUrl 取包(全新)+ (b) 从动态 URL 取 factory另一半+ 把王蓝莓专属项参数化**。R4 泛化 delta王蓝莓专属 → 通用)清单:
| # | 王蓝莓专属file:line | 泛化动作 | 程度 |
|---|---|---|---|
| D1 | `import createShopGame from './game-shop.js'``main.js:48`,静态绑死) | 从 packageUrl 取 bundle → 取固定全局名的 `GameHostFactory` | **全新**P1 编译期 import |
| D2 | 无 packageUrl 取包逻辑 | 新增 `packageUrl→bundle`(走后端 DB-manifest 端点) | **全新** |
| D3 | 7 件插件清单+调参写死(`main.js` plugins 构造) | 插件清单参数化(宿主固定标准集 vs 随包给) | 改造(结构复用) |
| D4 | 视口 `VIEW_W/H` 来自王蓝莓 render.js`main.js:51` | 视口从 GamePackage 取 | 改造(小) |
| D5 | 资产路径 `loadAssets('./assets/')`game-shop 工厂内) | 资产基址从 `assets[].url` 取 | 改造(在游戏工厂侧) |
| D6 | 取证全局名 `__refForensics/__refHost`main.js setupReal 内) | 降为可选P2 不强制每款实现取证) | 改造(降可选) |
| **复用** | engineInit 五回调骨架、`createHostDevContext`+`createEngineCaps` 受控面背书、引擎设置、DPR 补偿、输入接线 | 整块照搬,只把 D1/D4 引用点参数化 | **直接复用,不重写** |
| **复用** | `host-dev/engine-caps.js`(纯工厂,零引擎 import零王蓝莓专属 | 零改动 import | **直接复用** |
**结论verified**P2 的「新增」物理上只有 **D1+D2 两步(取包 + 动态取 factory**,其余是参数化既有 bootRef + 复用 P1 模块。**这不是新建契约,是把已 spike 实的装载约定接上一条取包管线。** 在哪个宿主上泛化R4 已裁 **应在 `bootRef`real 通道)上泛化,不是 `bootHostDev`**——后者「有掌帧无游戏」从未消费装载契约,在它上改会得到「插件演示 host 强塞装载契约」=重写 + 取证脚手架二次分裂(造第三套)。
---
## 2. O2 裁决(本稿第一结论):合并 Phase D已拍+ 请求纳入 Phase C扩张·待追认
> **mustFix#3 整改**P1 review L151+L5③ + arc O2L279创始人已拍的 O2 原话 = 「P2 合并进母 spec **Phase D**」,**只点 Phase D**。本稿把 Phase C1.x 退役)也纳入 supersede论证双写 80%、同一掏空壳互斥改法)经复证站得住,**但这是对已拍范围的扩张,不是 O2 已涵盖的内容**——故下文显式标注§6 O2 待拍项拆为「(a) Phase D 合并(确认)+ (b) Phase C 纳入(请求扩张,待追认)」两子项,**不把 Phase C 合并陈述为既定**。
### 2.1 裁决:合并 Phase D已拍口径+ 请求把 Phase C 一并纳入(范围扩张)
**[verified · R3 对账矩阵]** P2arc §5.2)与母 spec Phase B/C/D 逐项重叠arc 自己在 §5.2 L211 已标「与母 spec Phase B/C/D 高度重叠……须避双写」):
| # | 工作项 | 母 spec | P2 | 关系 | 依据file:line |
|---|---|---|---|---|---|
| 1 | 后端 packageUrl→DB-manifest 交付链 | Phase B/§5、门4 | §5.2① | **双写(同件)·已落** | `RuntimeConvert.java:79-92`B1 遗留P2 明写沿用 |
| 2 | studio host 取代掏空 startRuntime | Phase C `L80``runtime/index.ts:56` | §5.2② | **P2 升级·直接冲突** | Phase C 改「URL 加载 1.x runtime」、P2 改「引擎包加载」,**同一掏空壳 `runtime/index.ts:56-113` 互斥改法** |
| 3 | srcdoc→URL + CSP/sandbox/origin 矩阵 | Phase C 四项矩阵 `L119`、门5 `L132` | §5.2③ | **双写(同件)** | 同改 `inject.ts:52-53` CSP + sandbox + origin改动面 100% 重合 |
| 4 | GamePlayer 引擎/包加载分支 | Phase C `GamePlayer.vue` | §5.2② | **双写(同文件同段)** | 同改 `GamePlayer.vue:128-168` |
| 5 | 一款游戏经发布主链入 feed | Phase D `L16`/`L81`、门6 `L133` | §5.2 目标 | **P2 升级** | 母 spec Phase D=ref 跑 **stub** 入 feedarc L65 明示P2=engine-aware 入 feed |
| 6 | studio 改 staging 重建(消 mock | (未列) | §5.2④ | **P2 纯新增** | mock `:4173` 阻断,母 spec 无此项 |
| 7 | 引擎 bundle 真渲验收 | 门6 止于加载真包 `L133` | §5.2 门② | **P2 升级** | P2 抬到引擎真渲 |
| 8 | 防 demo 兜底假阳性 | 门4 `L131` | §5.2 门③ | **双写(同件)** | 判据逐字复用(含 lifecycle `game_end` |
| 9 | 后端发布主链(三服务) | Phase D `L58`(被动验证) | §5.2 爆炸半径 | **双写(只读·低冲突)** | 同被动验证不改 |
**矩阵小结verified**9 项里 **5 项纯双写**#1/#3/#4/#8/#9,同文件同判据)、**3 项 P2 升级**#2/#5/#7stub→引擎、**1 项纯新增**#6 staging 重建)。**P2 与 Phase C/D 改动面 ≈80% 物理重合**(同 `GamePlayer.vue`/`inject.ts`/`runtime/index.ts`/CSP 矩阵/发布主链)。
```mermaid
graph LR
subgraph M["母 spec T1b-β"]
PB["Phase B 后端侧<br/>✅ 已落(RuntimeConvert:79-92)"]
PBf["Phase B 前端 URL 化<br/>❌ 未动"]
PC["Phase C 1.x 退役<br/>❌ 未动·改 1.x runtime URL<br/>【扩张·待追认】"]
PD["Phase D ref 入 feed<br/>❌ 未动·产物=stub-ref<br/>【已拍合并】"]
end
subgraph P2["P2 合并后口径"]
K0["复用 Phase B 后端链(零改)"]
K1["host 一次改到位:<br/>掏空壳→引擎包分支<br/>(跳过复活 1.x runtime)"]
K2["产物口径 stub-ref→engine-ref"]
K3["门6 抬到引擎真渲"]
end
PB ==复用==> K0
PC ==supersede(待追认)==> K1
PD ==升级(已拍)==> K2
PD ==抬门==> K3
PBf -.前端 URL 化并入 K1.-> K1
style PB fill:#d4edda
style PC fill:#ffe0b2
style PD fill:#fff3cd
```
### 2.2 合并三层依据
**① Phase D「ref 跑 stub 入 feed」本身无产品价值。** 母 spec Phase D 产物=「跑在 stub 插件上的 ref」arc L65`main.js:15` 不 import 引擎。P1 已把 ref 的 `?engine=real` 同屏 spike 实(`99b6c82`),让一个已知跑不出引擎效果的 stub 版先入 feed、P2 再推倒重做=纯返工。合并后 Phase D 产物口径直接抬到 engine-ref。**此条支撑 Phase D 合并=已拍口径。**
**② 双写密度 80% → 不合并必撞车(此条支撑请求纳入 Phase C。** 矩阵 #2`runtime/index.ts` 掏空壳的同一段Phase C 改「URL 加载 1.x runtime」、P2 改「加载引擎 bundle」**两改法在掏空壳 `runtime/index.ts:56` 上直接互斥**。若 Phase C 独立先跑,它先把 host 改成消费 1.x runtime URLP2 再推翻成引擎分支=host 改两遍,且中间这步「复活 W-CLEAN 已停用的 1.x runtime 再退役它」毫无意义。**正因此互斥,本稿请求把 Phase C 一并纳入 P2 单口径——但这是 P1 已拍 Phase D 范围之外的扩张须创始人追认§6 O2-(b))。**
**③ P1 已落使母 spec 旧前提失效。** 母 spec 的 Phase 链建立在「无装载契约、ref 跑 stub」前提P1 落地后(装载契约 + engine-caps 模块 + ref real 通道)前提已迁移,继续按旧前提跑 Phase C/D = 在过时基线上施工。
### 2.3 合并后母 spec 受影响条目清单
**被 P2 supersede改口径/作废):**
| 母 spec 条目 | 行号 | 处置 | 范围归属 |
|---|---|---|---|
| Phase D 产物=stub-ref 入 feed | `L16`/`L81`arc L65 | **改口径**:升级为 engine-aware refP1 `?engine=real` 包) | **已拍 Phase D** |
| Phase C `runtime/index.ts` 掏空壳→「URL 加载 1.x runtime」 | `L80``runtime/index.ts:56` | **supersede**:跳过复活 1.x直接改「引擎包加载分支`game-host.d.ts` boot」 | **扩张·待追认** |
| 门6「CDP feed 卡片→/play 加载真包」止于加载 | `L133` | **抬门**:并入 P2 门②「引擎 bundle 真渲非'重构中'+`game_loaded`+lifecycle `game_end`(completed)」 | **已拍 Phase D** |
| Phase C 四项兼容矩阵 / 门5 | `L119`/`L132` | **并入 P2 单口径一次改写**CSP/sandbox/origin/CORS | **扩张·待追认** |
**仍有效P2 直接复用,不改):**
| 母 spec 条目 | 行号 | 复用理由 |
|---|---|---|
| §5 默认降级=DB-manifest 带版本+immutablePhase B 后端侧) | `L62`/`L79` | **已落地**`RuntimeConvert.java:79-92`P2 不碰后端 packageUrl 写链 |
| 门4 防 demo 兜底假阳性判据(含 lifecycle `game_end` | `L131` | P2 门③逐字复用mustFix#1:用 `game_loaded+game_end(completed===true)`**非** telemetry `game_play_end` |
| §8 四项兼容矩阵sandbox/origin/CSP/CORS | `L119` | P2 兼容矩阵框架复用,改的是 CSP 放行项内容 |
| Phase D 发布主链五步翻态 + 门6 DB 四态断言 | `L58`/`L133` | 发布主链不改只换「过链的包」为引擎包DB 四态断言原样作 P2 验收 |
| 后端发布主链三服务被动验证 | `L58` | P2 同样只读验证 |
| 串行验收铁律(同一参考包 CDP 联验) | `L58-59` | 合并后参考包=engine-ref联验口径不变 |
> **O2 与 arc O1 联动verified**P1 落地时已自答 arc O1——`game-host.d.ts:1-12` 新立第 9 类 additive 契约=O1 建议①;生命周期 `init/update/render/onInput/destroy``:42-48`=O1 建议②;`mainContext` boot 注入(`:25-30`=O1 建议③。故 **O2 合并时装载契约已是既成事实,无前置依赖,可直接自裁。**
---
## 3. 三段设计
> 总览P2 = 一条「后端把 bundle 装进包 → studio 引擎包分支取出并 boot → bootRef 泛化成装载任意包」的产品化管线。三段各给 Mermaid + 关键 file:line。
```mermaid
flowchart LR
subgraph BE["①后端写包(R2)"]
G["DifyCallbackTxService<br/>.buildGamePackage:268-329<br/>+pkg.put(engineBundle)"]
S["storeForVersion → package_json<br/>LONGTEXT(零改)"]
EP["GET .../manifest:75-96<br/>裸 String 原样返回"]
end
subgraph FE["②studio 引擎包分支(R1)"]
FV["fetchAndVerifyManifest<br/>GamePlayer.vue:147(sha256 校验)"]
BR["引擎包分支取代<br/>startRuntime(掏空):184"]
CSP["CSP/sandbox 迁移点"]
end
subgraph HOST["③通用宿主(R4)"]
BH["bootGameHost(opts)<br/>=bootRef 引擎骨架参数化"]
FAC["从 bundle 取固定全局名<br/>GameHostFactory"]
end
G --> S --> EP --> FV --> BR --> BH --> FAC
CSP -.放行.-> BR
```
### 3.1 ① 后端写包 + manifest 交付 bundleR2
**[verified]** 当前渲染模型 = 「函数序列化注入」:硬编码 `startRuntime``.toString()` 在**前端构建期**烤进 iframe srcdoc`inject.ts:39``:104`GamePackage 只供数据(`gameConfig`+`assets`**无任何可执行引擎代码**)。要换 P1 产出的真引擎 bundlebundle 必须**随包从后端下发**。
```mermaid
flowchart TB
CB["生成回调(M-b 闭环)"] --> TX["DifyCallbackTxService.handleCallbackTx<br/>(三表同事务)"]
TX --> BP["buildGamePackage:268-329<br/>组包·字节可复现"]
BP -.P2 增一行.-> EB["pkg.put('engineBundle', bundle文本)<br/>(checksum 占位回填前)"]
BP --> SV["storeForVersion → package_json LONGTEXT<br/>(DbPackageStore 零改)"]
SV --> EP["GET /app-api/runtime/package/{vid}/manifest<br/>AppRuntimeController:75-96<br/>裸 String 原样返回·字节一致保 sha256"]
EP --> FE["前端 fetchAndVerifyManifest<br/>取 engineBundle 注入 iframe"]
```
**关键缺口与最小路verified · R2**
- **bundle 存哪**:今天 `package_json` LONGTEXT 只存 manifest JSON`gameConfig`+资源 URL**无任何字段/表存 JS bundle**,也无 OSS/MinIO。
- **URL 怎么出**:今天**无「按 versionId 取 bundle 字节」的端点**——manifest 端点 `AppRuntimeController:75` `produces = MediaType.APPLICATION_JSON_VALUE`,固定吐 `application/json`,不能直接吐 ~259KB 的 `application/javascript`
- **推荐最小路 = DB-manifest 内嵌**:在 GamePackage JSON 内增一字段承载 bundle 文本(`engineBundle`),落进既有 `package_json` LONGTEXTMySQL 上限 4GB259KB 无压力。manifest 端点「裸 String 原样返回 + 字节一致」机制原样复用(`AppRuntimeController:88-90` StringHttpMessageConverter 不重序列化checksum 仍 = sha256(整包文本),覆盖 bundle 完整性。**唯一必要后端改动 = `DifyCallbackTxService.buildGamePackage``:268-329`)增一个 `pkg.put("engineBundle", <文本>)` + 契约 #4 GamePackage schema 加该可选字段。** `storeForVersion`/`DbPackageStore`/表结构/端点**一行不改**`bundleSize` 取 UTF-8 字节数自然涵盖 bundle`buildGamePackage` 末段 `getBytes(UTF_8).length`verified
> **[verified]** 现契约 `game-package.schema.json` 仅有 `packageUrl`/`immutable` 交付锚,**无 `engineBundle`/`engineCode`/`engineUrl` 载体字段**grep contracts/ + game-studio/ + game-cloud/ + game-runtime/ 零命中。bundle 载体是 additive 新增字段——**这是 P2 唯一的契约面新增**(交付锚已就位,只缺承载位)。
> **[inferred]** 不推荐为 bundle 新增专用 `GET .../bundle` 端点吐 `application/javascript`会破坏「manifest 单跳 + 单 checksum 覆盖整包」完整性模型,引入第二个 sha256 校验面改动面远大于内嵌。OSS/MinIO 是 M3 退场契约P2 不应提前接。
> **[Open · O-P2-1]** bundle 文本从哪来 + 产线mustFix#4 强化,见 §6P2ref 级)阶段须**先固化 ref bundle 产线**(产出确定路径 + 钉死 globalName+ **明确 game-cloud 后端如何拿到 game-runtime 构建产物**(跨仓产物传递),不可隐含「后端能直读前端仓 dist」。
### 3.2 ② studio 引擎包分支取代 srcdoc 内联R1标 CSP/sandbox 迁移点)
**[verified]** 当前唯一 `startRuntime` 运行调用点 = `inject.ts:184`(全仓 grep 确认:`:8/:21/:39/:104/:161` 是注释/import/`.toString()`/srcdoc var 声明,非运行调用),无条件调掏空壳 `startRuntime(canvas, CTX.pkg, runtimeView)`。**没有任何按 pkg 字段分流引擎 vs 内联的逻辑**——这是 P2 必须新增的分叉。掏空壳 `runtime/index.ts:56-113` 只画「模板体系重构中」+发 `game_error``runtime/index.ts:~103`),完全非 engine-aware。
```mermaid
flowchart TB
L["GamePlayer.vue launch()<br/>→ resolvePackage(取包三分支)"] --> P["pkg(含 engineBundle?)"]
P --> SD["buildIframeSrcdoc<br/>构 srcdoc"]
SD --> BT["iframe boot()"]
BT -.P2 新增分叉.-> BR{"pkg.engineBundle?"}
BR -->|有| ENG["引擎包分支<br/>注入 bundle script<br/>→ 取 GameHostFactory<br/>→ bootGameHost 五回调"]
BR -->|无·向后兼容| OLD["旧路:startRuntime(掏空):184<br/>(demo 兜底仍走此=显示重构中)"]
ENG --> LC["生命周期回流(复用·不动)<br/>sdk.__emit→bridge→GamePlayer:300 game_end"]
```
**CSP/sandbox/origin 迁移点(全 verified · 这是 P2 安全面的硬改点)**
| 迁移点 | 现状file:line·读真源码确认 | P2 迁移 | 风险 |
|---|---|---|---|
| **CSP最硬** | `inject.ts:52-53` 完整=`default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob: https:; connect-src 'none';` | bundle 外部加载必改 `script-src`(放行引擎域白名单)+ 若拉资源放行 `connect-src` | 放宽=扩攻击面,需精确域白名单 |
| **加载时序** | 内联同步注入(`:104/:184``boot()` 无异步 | 外部 bundle 走异步 `<script onload>``boot()` 重构为异步 | 与 `GamePlayer.vue:198/204` 5s `game_loaded` 超时对齐(引擎冷启动 4.8~5.3× 见记忆可能逼近P75<3s 首屏受压 |
| **sandbox** | `GamePlayer.vue:524/532` `allow-scripts allow-same-origin` | bundle 加载不改 sandbox只要不引 top-navigation/popups | 低 |
| **origin** | srcdoc origin='null'`buildOriginAllowlist(includeNull)` `bridge.ts:269-283`/`GamePlayer.vue:224` 白名单含 `'null'` | 若 bundle 改 blob/子域origin 漂移,白名单同步 | `bridge.ts:269-270` 注明「生产用独立子域时应置 false」 |
> **img-src 资产通道shouldFix#2**`inject.ts:52-53` 已放行 `img-src data: blob: https:`,故 P2 引擎真渲所需游戏资产(`assets[].url` 指向的 https 图片/图集)若沿用现有「资产 URL 进 GamePackage、iframe 内 `<img>`/`drawImage` 加载」通道则**已够**;但若引擎试图**运行时 fetch 资产**则触 `connect-src 'none'` 死结,须一并纳入 CSP 决策(见 O-P2-2
> **[inferred]** 三选一blob / `<script src>` 外域 / DB-manifest 内嵌)直接决定攻击面:
> - **`.toString()` 内联不适用**引擎是第三方库LittleJS**不满足「无外部符号」约束**`inject.ts:8``.toString()` 模式对引擎失效。
> - **bundle 走 DB-manifest 内嵌 + iframe 内 `<script>` 注入文本**R1/R2 推荐路——bundle 文本随 manifest 下发(已过 sha256 校验iframe 内从解析出的 GamePackage 取 `engineBundle` 注入内联 `<script>`(非外域 `<script src>`)。**此路 CSP 仍可保持 `script-src 'unsafe-inline'` 不放外域**bundle 是内联文本不是外部 URL`connect-src 'none'` 也无需放bundle 经 manifest 端点取、走宿主层 fetch 非 iframe 内 fetch——**这是 CSP 改动面最小的一条**。代价:**~259KB 文本**mustFix#2 已纠,原误作 540KB进 srcdoc解析代价存在但约原估值一半失去 CDN 强缓存。
> - **bundle 走外域 `<script src=packageUrl>`**:享 HTTP 缓存feed 连续翻页复用引擎),但必须放宽 CSP `script-src` 含 packageUrl 域 + origin 可能漂移。
> **[Open · O-P2-2]** 三选一是 P2 安全红线主决策(见 §6
**iife factory 取出形态verified · R4**P1 esbuild iife 全局导出名 = **顶层 `var <globalName>`**`dist/ref-bundle.js` 首字节 = `"use strict";var WanglanmeiRef=(()=>{...})();`,已读真源核对),`<script>` 顶层 var 在浏览器全局即 `window.WanglanmeiRef`。**P1 每包不同名(`WanglanmeiRef`)→ P2 必须钉一个固定 globalName**(否则宿主不知取哪个全局 var。**[Open · O-P2-3]** 固定 globalName 命名(如 `__GameBundle`)。
### 3.3 ③ 通用宿主泛化:从 bootRef → 装载任意包R4
**[verified · R4]** bootRef → 通用宿主差 3 步engineInit 五回调骨架c与 engine-caps 受控面背书**整块复用、不重写**
```mermaid
flowchart TB
subgraph REUSE["P1 bootRef 可直接复用(整块照搬)"]
C["(c) engineInit 五回调骨架<br/>setGLEnable(false)→engineInit→.then(setupReal)"]
EC["createHostDevContext+createEngineCaps<br/>(受控面+引擎背书)"]
ECJ["engine-caps.js(零改动 import)"]
end
subgraph NEW["P2 新增/参数化(R4 delta)"]
A["(a) packageUrl→bundle 取包<br/>D2 全新"]
B["(b) 从 bundle 取 GameHostFactory<br/>D1 全新(P1 是静态 import)"]
PARAM["参数化 D3 插件清单/D4 视口/D5 资产"]
end
A --> B --> C
C --> BOOT["game.init(boot)<br/>boot={ctx,mainContext,canvas,seed}<br/>=game-host.d.ts:25-30 契约脊柱"]
EC --> BOOT
ECJ --> EC
PARAM --> BOOT
BOOT --> DRIVE["五回调真驱动 GameInstance<br/>game-host.d.ts:42-48"]
```
**落地形态inferred · R4 建议)**:把 `bootRef` 里「装载 game-shop」的专属项D1/D3/D4抽成参数得通用 `bootGameHost(opts)` = `bootRef` 引擎骨架 + 参数化游戏工厂来源(从 packageUrl 取)+ 参数化插件清单。`bootRef` 自身退化为「`bootGameHost` 传入王蓝莓固定工厂/插件清单」的薄包装(保 ref 取证)。**这样不产生第三套**host-dev纯插件演示留作引擎能力回归门/ bootRefref 取证,薄包装)/ bootGameHostP2 通用)三者共用同一引擎骨架与同一 engine-caps 模块。
> **[Open · O-P2-4]** 插件清单归属:宿主固定一套标准插件集 vs 随 GamePackage/templateId 给。P2先用 ref 级)阶段可先用王蓝莓那 7 件作标准集P3 多品类时再拍是否随包给。
---
## 4. 验收门(防假绿)
> 总纲门必须堵死「demo 兜底假阳性」与「重构中假绿」两类。母 spec 门4/门6 框架整套继承P2 抬到引擎层。**mustFix#1:就绪闭环用 lifecycle `game_end`,不用 telemetry `game_play_end`。**
```mermaid
flowchart LR
G1["门① feed 出 PUBLISHED<br/>feed/stream 出该游戏 status=4"] --> G2["门② 真渲非重构中<br/>引擎 bundle 真渲+game_loaded+lifecycle game_end(completed)"]
G2 --> G3["门③ 防 demo 假阳性<br/>checksum∧gameId∧versionId∧兜底即fail"]
G3 --> G4["门④ 链路③⑤真走查<br/>CDP feed 卡片→/play 引擎真玩"]
```
| 门 | 判据 | 防假绿要点verified 锚) |
|---|---|---|
| **① feed 出 PUBLISHED** | 后端 `feed/stream` 出现该游戏 `status=PUBLISHED(4)`DB 四态:`project.status==4 ∧ game_version.status==3 ∧ runtime_package.status==1 ∧ feed_rank 行存在` | feed 读侧二次过滤仅 PUBLISHED 出流(`FeedServiceImpl:119-159`);母 spec 门6 `L133` 原样作 P2 DB 四态断言 |
| **② 真渲非「重构中」** | 真人浏览器开 feed→点卡→**引擎 bundle 真渲非「模板体系重构中」** + `game_loaded` + **lifecycle `game_end``completed===true`**console 零未捕获错误 | **mustFix#1**:就绪信号经 SDK lifecycle 层 `game_end``sdk-interface.d.ts:24` 「本局结束」,经 `sdk.__emit('game_end')`→bridge→`GamePlayer.vue:300 case 'game_end': emit('end')`**不用 telemetry `game_play_end`**`events.schema.json:62`,那是埋点流、非就绪信号)。防「掏空壳假绿」:掏空壳 `runtime/index.ts:~103``game_error` 不发 `game_loaded`——若仍走掏空壳则永远等不到 `game_loaded`=fail |
| **③ 防 demo 兜底假阳性** | checksum=发布版本 ∧ 包内 gameId/versionId 与请求匹配 ∧ versionId 走真发布包(非 mock DEMO 占位) ∧ **兜底触发即 fail****取包一律用 play 场景**shouldFix#1,后端门禁兜 status=1 | **后端字段齐备**verified`game_runtime_package.checksum`(=sha256 整包,权威)、包内 `gameId`/`versionId``buildGamePackage:272-273`)、`status`。**缺的是消费侧断言**:宿主现仅校验「响应文本 sha256==RespVO.checksum」`GamePlayer.vue:147`)。**两道叠加**shouldFix#1(后端第一道) play 场景 manifest 端点已强制 status=1`RuntimeConvert.java:83-84` 「manifest 端点 scene 缺省为 play仅放行 status=1」(前端第二道) checksum≠占位 0^64 ∧ gameId/versionId 与请求一致 ∧ 非 `buildDemoPackage``inject.ts:296`)注入。**关键防线**demo 兜底触发即判 fail |
| **④ 链路③⑤真走查** | CDP 真触摸 feed 卡片→/play 加载真包→引擎真玩studio `--mode staging` 重建消 mock`:4173` 阻断 B | 防「mock 假数据假绿」:`:4173` 现取 mock「霓虹躲避」假数据未接真 stagingR3/arc a3必须 staging 重建后走真后端 feed |
> **[verified]** 门③的「后端字段齐备、缺消费侧断言」是 R2 的核心结论:`game_version.checksum`/`bundle_size` 恒空是鸡蛋环设计JSON 含 versionId → sha256 须先有 versionId故 version 行这两字段恒空),**门③别读 game_version 这两字段,要读 `game_runtime_package.checksum`**(经 RespVO.checksum 下发)。
> **[verified]** ~259KB bundle 进 manifest JSON 使响应体增大、`bundleSize` 变大(`buildGamePackage` 末段取 UTF-8 字节数自然涵盖),门禁语义仍成立(可接受)。
---
## 5. 爆炸半径 + 风险/兼容性
### 5.1 爆炸半径
```mermaid
graph TB
subgraph FE["前端 game-studio(主改面)"]
F1["host/runtime/index.ts:56<br/>掏空壳→引擎包分支(或新增分流)"]
F2["host/inject.ts:52-53 CSP<br/>+:184 分流点+boot 异步化"]
F3["host/GamePlayer.vue:128-168<br/>取包+:524/532 sandbox+:198 超时对齐"]
F4["host/contract.ts<br/>GamePackage 加 engineBundle 字段(前端副本)"]
F5["staging 重建(--mode staging)"]
end
subgraph BE["后端 game-cloud(最小改)"]
B1["DifyCallbackTxService.buildGamePackage:268-329<br/>+pkg.put(engineBundle)·唯一必要改动"]
B2["发布主链三服务(被动验证·不改)<br/>ProjectService/PublishOrchestration/FeedService"]
end
subgraph CT["契约(唯一新增)"]
C1["game-package.schema.json<br/>+engineBundle 可选字段(additive)"]
end
subgraph REUSE["P1 复用(零改)"]
R1["game-host.d.ts(装载契约)"]
R2["engine-caps.js(共享模块)"]
R3["bootRef 引擎骨架(参数化)"]
end
```
| 触及 | 文件 | 性质 |
|---|---|---|
| **studio host 主改** | `game-studio/src/host/{runtime/index.ts:56(掏空壳→引擎分支), inject.ts:52-53/184, GamePlayer.vue:128-168/198/524/532, contract.ts}` | 引擎包分支 + CSP 矩阵 + 异步 boot |
| **studio staging** | `--mode staging` 重建 | 消 mock纯新增 |
| **后端唯一必要改** | `DifyCallbackTxService.buildGamePackage:268-329``pkg.put("engineBundle",…)` | 一行级;`storeForVersion`/`DbPackageStore`/表/端点零改 |
| **契约唯一新增** | `contracts/game-package.schema.json``engineBundle` 可选字段 + 前端副本同步 | additive |
| **后端被动验证** | 发布主链三服务 | 只读不改 |
| **P1 复用** | `game-host.d.ts` / `engine-caps.js` / bootRef 骨架 | 零改/参数化 |
### 5.2 风险/兼容
| 风险 | 等级 | 缓解 / 回滚 |
|---|---|---|
| **CSP 死结srcdoc→外部 bundle** | **P0** | `inject.ts:52-53` `connect-src 'none'`+`script-src 'unsafe-inline'` 同时封死 `fetch(engineUrl)` 和外域 `<script src>`verified。**缓解=走 DB-manifest 内嵌路**bundle 文本随 manifest 经宿主层 fetchiframe 内注入内联 `<script>`CSP 可不放外域改动面最小§3.2)。若选外域加载则须精确域白名单。**回滚=保留 srcdoc 内联分支(向后兼容,见下)** |
| **掏空壳替换的向后兼容** | **低verified** | **零向后兼容包袱**:全仓唯一 `startRuntime` 运行调用点=`inject.ts:184`,唯一定义=`runtime/index.ts:56`当前所有游戏路径Play/Preview全汇入同一掏空壳画「重构中」=**线上零可玩游戏**W-CLEAN 已清空。P2 重写不破坏任何现存可玩路径(根本无可玩路径)。**唯一要保的是契约面**`RuntimeSDKView``__emit`/`track`/`reportError`/`saveState`/`loadState` 签名 + `RuntimeHandle.destroy` + GamePlayer lifecycle emit 映射宿主↔runtime 稳定边界,引擎接入复用非另起)。**demo 兜底路径(`inject.ts:296`)需决定是否也走引擎**,否则 demo 永久白屏文案(见 §6 |
| **加载时序逼近超时 / P75<3s 受压** | **P0** | 引擎冷启动实测 4.8~5.3×(记忆),外部 bundle 异步加载可能逼近 `GamePlayer.vue:198/204` 5s `game_loaded` 超时。缓解=DB-manifest 内嵌走 HTTP 缓存immutable跨游戏复用引擎 + 必要时放宽超时阈值;首屏 P75 须 CDP timing 实测坐实 |
| **与母 spec 双写** | **P1** | 见 §2合并消解。**最大双写点=`runtime/index.ts:56` 掏空壳 + `GamePlayer.vue:128-168` + CSP 矩阵三处**,合并后由 P2 单口径一次改写。须创始人拍 O2(a) Phase D 合并确认 + (b) Phase C 纳入追认(见 §6 |
| **~259KB raw / ~88KB gz 体积口径**mustFix#2 已纠) | **P2** | **实测:`ref-bundle.js`=265762 字节259KB rawSIZES.md:33 已坐实「ref real 真入引擎=264KB raw / 88KB gz」**2026-06-13 build 凭据 raw=264476 + metafile。gz 88KB ≪ B1 入场券 **gz≤350KB**`tiered-engine` 记忆)——**不触红线**。原草案 540KB 系开工前 inferred 估值误把 SIZES「+example 544」插件增量行当 entry 实测,**实际差近一倍**,已全稿改正。开工前用 `size.mjs` 对 P2 实际产物(引擎+包装+游戏代码)再实测坐实,**勿再传 540KB**亦勿引「13KB/55KB」旧口径已废 |
| **origin 漂移** | P2 | bundle 若改 blob/子域origin 从 'null' 变,`bridge.ts:269-283`/`GamePlayer.vue:224` 白名单同步。DB-manifest 内嵌路 origin 仍='null',无此风险 |
| **历史游戏兼容** | 低 | 插件公开 API 冻结、装载契约 additive、`startRuntime` 旧分支可保留作兜底分流 → 回滚=移除引擎分支不破存量 |
| **ref bundle 产线未固化**mustFix#4 | **P1** | ref-bundle 产线游离(无 npm script 产它/钉 globalName`scripts/build.mjs:40` `build(entry,outfile,globalName)` 存在但无 ref 调用方grep 实证);后端「读固定产物」依赖一个产线不在仓、跨仓取得路径未定的文件。缓解=O-P2-1 落实产线 + 跨仓产物传递路径(见 §6P2 自身要靠它出 bundle绕不过去 |
---
## 6. 非目标 + 待创始人拍 Open Items
### 6.1 非目标
- **不接 OSS/MinIO**:沿母 spec 默认降级 = DB-manifest 内嵌 + immutable切 OSS 留 M3 上线前(`PackageStore` 换 impladditive 可逆)。
- **不改下层受控面 6 项契约 + 上层装载契约**`getEngine()` 等已冻、`game-host.d.ts` P1 已定P2 零改(任何改触发架构复议)。
- **不动 feed 终裁引擎选型**LittleJS feed 侧不动渠道线小游戏引擎另行竞标HJ-CH-001
- **不做 P3 生成主线**P2 装载链上游包从哪来P2 阶段用 ref 级固定产物兜;「谁用什么把 GameHostFactory 源码 esbuild 成 bundle」这条产线是 P3 与 P2 的衔接缺口,归 P3但 P2 阶段 ref bundle 产线须先固化,见 O-P2-1
- **不重做 M-b 执行器**`SUPPORTED_TEMPLATE_IDS = List.of()`W-CLEAN 停用)的解封归 P3。
### 6.2 待创始人拍 Open Items
> 承 arc O2本稿 §2 裁=合并 Phase D + 请求纳入 Phase C/O3P3 白名单口径,归 P3/O5N 款入 feed归 P3补 P2 自身 5 新待拍项。
| # | 待拍项 | 选项 / 默认建议 |
|---|---|---|
| **O2(a)(已拍·确认)** | **Phase D 合并**P2 把母 spec Phase D「ref 跑 stub 入 feed」升级为「engine-aware ref 入 feed」。P1 已拍此合并。 | 建议:**确认**(依据 §2.2①stub-ref 无产品价值)。 |
| **O2(b)(扩张·待追认)** | **Phase C 一并纳入**:把母 spec Phase C1.x 退役 srcdoc→URL + 兼容矩阵 + 门5也并入 P2 单口径。**这是 P1 已拍 Phase D 范围之外的扩张**,非 O2 原口径所含。 | 建议:**追认纳入**(依据 §2.2②Phase C 与 P2 在 `runtime/index.ts:56` 唯一掏空壳上互斥改法,独立跑=host 改两遍+复活再退役 1.x 无意义)。**待追认是否扩母 spec 改动范围。** |
| **O-P2-1落地缺口·mustFix#4** | **bundle 载体来源 + 产线**engine-aware bundle 文本从哪来(随生成回调入参 vs 后端按 templateId 读固定引擎产物);且 **ref bundle 产线未固化进仓**(无 npm script 产它、跨仓取得路径未定)。 | 建议:**P2(ref 级)阶段**=(a) 固化一个 npm script`build:ref-bundle`)产出确定路径+钉死 globalName并入 O-P2-3(b) 明确 game-cloud 后端如何拿 game-runtime 构建产物(打包期拷贝/构建产物入库/CI 产物传递),**不假设后端直读前端仓 dist**(c) 若 P2 仅跑 ref 一款可显式降级为「bundle 文本由脚本一次性灌入 DB 某版本」并标注过渡桩。P3 接生成主线后改随回调入参。**待拍。** |
| **O-P2-2安全红线** | **bundle 装进 iframe 的三选一**:① DB-manifest 内嵌CSP 改动最小) / ② 外域 `<script src=packageUrl>`(享 HTTP 缓存,须放宽 CSP+origin 漂移) / ③ `.toString()` 内联(对引擎不适用,已排除)。**注**:若引擎需运行时 fetch 资产则触 `connect-src 'none'` 死结,须一并纳入。 | 建议:**先 ① DB-manifest 内嵌**CSP 可不放外域、改动面最小、零基建;~259KB 内联解析代价约原估一半可接受feed 翻页缓存收益靠 manifest 端点 HTTP 缓存兜;若实测首屏 P75 受压再评 ②。**待拍(直接决定攻击面与 CSP 放宽边界)。** |
| **O-P2-3** | **固定 globalName**P1 每包不同名(`WanglanmeiRef`P2 通用宿主须钉一个固定导出名才能取出 factory。 | 建议:钉 `__GameBundle`(或 `WanxiangGameBundle`P2 后端打包/P1 ref bundle 统一此名(与 O-P2-1 产线一并固化)。**待拍命名。** |
| **O-P2-4** | **插件清单归属**:宿主固定一套标准插件集 vs 随 GamePackage/templateId 给。 | 建议:**P2 阶段先用王蓝莓 7 件作标准集**P3 多品类时再拍是否随包给。**待拍。** |
| **O-P2-5** | **demo 兜底路径处置**`buildDemoPackage``inject.ts:296`templateId='clicker')取包失败时注入,其包喂掏空壳=永久白屏「重构中」。是否让 demo 也走引擎。 | 建议:**保留 demo 走掏空壳显示「重构中」**(取包失败本就该清晰降级);门③显式断言「兜底触发即 fail」防 demo 假阳性混入真发布。**待拍是否给 demo 一个最小引擎样例。** |
---
## 对抗整改清单mustFix 逐条 + shouldFix
| 级别 | 原问题 | 整改 | 证据file:line·复证 |
|---|---|---|---|
| **mustFix#1** | 门②判据用 telemetry `game_play_end` 作就绪信号,跨层错——它是埋点事件,真正驱动就绪闭环的是 lifecycle `game_end` | 全稿门②/§4 表/Mermaid 改用 **lifecycle `game_end`completed===true**(经 `sdk.__emit('game_end')`→bridge→`GamePlayer.vue:300`);与母 spec 门4 同口径telemetry `game_play_end` 仅作埋点不入就绪门 | `sdk-interface.d.ts:24``game_end`=lifecycle「本局结束」vs `events.schema.json:62``game_play_end`=telemetry「试玩结束。props: duration_ms, completed」`GamePlayer.vue:292/300``case 'game_loaded'`/`case 'game_end': emit('end')`);母 spec `L66` R4-P2 警告 + `L131` 门4 用 `game_end(completed===true)` |
| **mustFix#2** | 体积口径 540KB 全错且与已落实测矛盾 | 全稿 540KB→**259KB raw / 88KB gz**SIZES.md:33 实测);门/三选一/风险等级按真实值重估(内联解析代价约原估一半);标注勿再传 540KB开工前 `size.mjs` 复测 | `ref-bundle.js`=**265762 字节**(实测 `wc -c``SIZES.md:33`「ref real 真入引擎=264KB raw / 88KB gz」+ build 凭据 raw=264476540KB 溯源=P1/arc 开工前 inferred误把 `SIZES.md:11` +example 544 插件增量列当 entry 实测,`SIZES.md` 口径声明在增量法表头) |
| **mustFix#3** | O2 边界被从 Phase D 单方面扩到 Phase C+D 却陈述为既定 | §2.1 标题+首结论显式标注「Phase C 纳入属对已拍 Phase D 范围的**扩张请求**」§6 O2 拆为 **(a) Phase D 合并(确认)+ (b) Phase C 纳入(待追认)**;不把 Phase C 合并写成既定 | P1 review `L151`+`L5③`「P2 合并进母 spec Phase D」只 Phase Darc O2 `L279`(只 Phase D草案原 §2.1「Phase C+D」 |
| **mustFix#4** | O-P2-1「后端读 ref 固定产物」隐含未落实的产线假设产线不在仓、跨仓取得路径未定、globalName 未钉) | §3.1/§5.2/§6 O-P2-1 落实:(a) 固化 npm script 产 ref bundle+钉 globalName(b) 明确 game-cloud 如何拿 game-runtime 构建产物(不假设直读前端仓 dist(c) 仅跑 ref 时可显式降级为脚本灌库过渡桩;新增 §5.2「产线未固化」P1 风险行 | `scripts/build.mjs:40` `build(entry,outfile,globalName)` 存在但 grep scripts/ 无产 ref-bundle/传 `WanglanmeiRef` 的调用脚本(只 `size.mjs:64``__RT_SIZE_PROBE``dist/ref-bundle.js` 存在265762B今天 17:51 生成)但产线游离 |
| **shouldFix#1** | 门③把防 demo 重担全压前端,未利用后端既有 play-场景 status=1 门禁 | 门③补「两道叠加」:后端 play 场景 manifest 端点已强制 status=1第一道前端 checksum/gameId/versionId/非 demo第二道P2 取包一律用 play 场景 | `RuntimeConvert.java:83-84`「manifest 端点 scene 缺省为 play仅放行 status=1」+ `:44-46``AppRuntimeController:75/85-87` `getPackageManifest(versionId, scene, userId)` play 缺省 |
| **shouldFix#2** | CSP 描述漏 img-src影响资产加载判断 | §3.2 CSP 表补完整 CSP`img-src data: blob: https:; style-src 'unsafe-inline'`加「img-src 资产通道」注:现有 `<img>`/`drawImage` 通道已够,引擎运行时 fetch 资产才触 connect-src 死结 | `inject.ts:52-53` 真源=`default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob: https:; connect-src 'none';`(读真源码确认) |
| **shouldFix#3** | bridge.ts 引用行号失准 | §3.2/§5.2 origin 引用校准为 `bridge.ts:269-283``buildOriginAllowlist(includeNull)`+`set.add('null')` `:283`+ `:203-204/:220`targetOrigin '*' 注释「生产换精确 origin」 | `bridge.ts:203-204`targetOrigin '*' + 注释)、`:220``win.postMessage(envelope,'*')`)、`:269-283``buildOriginAllowlist`/`includeNull`/`set.add('null')``GamePlayer.vue:224` |
> **无反驳项**4 mustFix + 3 shouldFix 全部经独立读真源码复证成立,全部纳入,无静默忽略、无反驳。
---
## 风险与假设(诚实标注)
- **[verified]** 装载契约 `game-host.d.ts:25-59` 实体存在4 字段 boot + 5 生命周期 + factory消费方已含「集成段 host」=P2P1 已拆独立 game 工厂(`main.js:48` `import createShopGame from './game-shop.js'`);下层 `packageUrl`/`immutable` 已 additive 留位(`game-package.schema.json:66-74`);后端 DB-manifest 派生分支活(`RuntimeConvert.java:79-92`)。均读真源码确认。
- **[verified]** 掏空壳 `runtime/index.ts:56-113` 只画「重构中」+发 `game_error` 不发 `game_loaded`CSP `inject.ts:52-53` 完整含 img-src/style-src唯一 `startRuntime` 运行调用点=`inject.ts:184`、唯一定义=`runtime/index.ts:56`grep 全仓 + 读真源码复证)。
- **[verified]** 契约无 `engineBundle`/`engineCode`/`engineUrl` 载体字段grep contracts/+game-studio/+game-cloud/+game-runtime/ 零命中)→ bundle 载体是 P2 唯一契约面 additive 新增后端字段checksum/包内 gameId/versionId/status门③齐备缺消费侧断言R2
- **[verified · mustFix#1]** lifecycle `game_end``sdk-interface.d.ts:24``GamePlayer.vue:300`)≠ telemetry `game_play_end``events.schema.json:62`);母 spec `L66`/`L131` 已警告此跨层坑。门②口径已纠。
- **[verified · mustFix#2]** ref-bundle 实测 259KB raw / 88KB gz`SIZES.md:33` 坐实),非 540KB。
- **[verified · mustFix#3]** P1 review/arc O2 已拍范围只 Phase DPhase C 纳入是扩张,已显式标注待追认。
- **[verified · shouldFix#1]** 后端 play 场景 manifest 端点强制 status=1`RuntimeConvert.java:83-84`)。
- **[verified · 母 spec 对账]** P2 与 Phase C/D 改动面 ≈80% 物理重合5 双写/3 升级/1 新增Phase B 后端侧已落、前端 URL 化/C/D 未动。
- **[verified · mustFix#4]** ref bundle 产线游离未固化(`scripts/build.mjs:40``build(...)` 但无 ref 调用方)。
- **[inferred]** bundle 三选一中 DB-manifest 内嵌是 CSP 改动面最小路(基于 `inject.ts:52-53` CSP 语义 + manifest 端点裸 String 机制推断;须 P2 开工实测 iframe 内联 ~259KB 的解析/首屏代价坐实)。
- **[inferred]** 引擎冷启动 4.8~5.3× 可能逼近 5s `game_loaded` 超时(记忆 + `GamePlayer.vue:198/204`);首屏 P75<3s CDP timing 实测
- **[assumption]** O-P2-1/O-P2-4 的「P2 阶段用 ref 级固定产物 + 王蓝莓 7 件标准集」是过渡口径P3 接生成主线后须重定。
- **[未决]** O2(a) 确认 + O2(b) 追认 + O-P2-1~5 共七项待创始人拍;其中 O-P2-2 是安全红线主决策、O2(b) 是范围裁决主决策、O-P2-1 是落地缺口主决策,建议优先拍这三项再开 P2 execution。
**关键文件路径(绝对)**
- 本稿母梁:`/root/games-development-ai/docs/agent-specs/2026-06-13-runner-v2-arc-review.md`§5.2
- P1 review/execution`/root/games-development-ai/docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-{review,execution}.md`
- 母 specO2 标的):`/root/games-development-ai/docs/agent-specs/2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md`Phase B/C/D + 门4 `L131`/门6 `L133`
- 上层装载契约P1 既成·复用):`/root/games-development-ai/game-runtime/src/core/game-host.d.ts`
- 下层交付锚(复用):`/root/games-development-ai/contracts/game-package.schema.json:66-74`
- 后端 DB-manifest 派生(已落·零改):`/root/games-development-ai/game-cloud/game-module-runtime/game-module-runtime-server/src/main/java/cn/wanxiang/game/module/runtime/convert/runtime/RuntimeConvert.java:79-92`
- 后端 manifest 端点play 场景 status=1 门禁):`…/runtime/controller/app/runtime/AppRuntimeController.java:75-96`
- 后端写包唯一必要改点:`/root/games-development-ai/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/service/callback/DifyCallbackTxService.java:268-329`
- studio 冲突零件:`/root/games-development-ai/game-studio/src/host/runtime/index.ts:56-113`(掏空壳)、`…/inject.ts:52-53`CSP/`:184`(分流点)、`…/GamePlayer.vue:128-168`(取包)/`:300`game_end`…/bridge.ts:269-283`origin 白名单)
- 体积实测:`/root/games-development-ai/game-runtime/SIZES.md:33`259KB raw / 88KB gz