games-development-ai/docs/agent-specs/_archive/2026-06-11-回包链路专项修-收口报告.md
zizi 7f24a344d0 docs(agent-specs): B2 归档——64 个闭线工作记录移入 _archive/,热目录顶层 90→26
承接目录治理:上轮只压缩内容未减文件数,闭线工作记录仍平铺致目录看着仍一堆(创始人指出)。本次执行 B2 归档。

64 个 ≤06-15 闭线档(25 压缩桩 + 闭线 review/report/纪要/edit-plan)git mv 入 docs/agent-specs/_archive/(文件名不变、仍 git 跟踪可查)。热目录 ≤06-15 仅留 13 活档(决策/纲领/SoT/活spike)+13 个 06-16 在飞。

活资产 20 处旧路径引用(.agents/docs/memory/_index)同步改 _archive/,引用断裂复测=0;_index 活地图 + 治理档状态收口。约束:0 个 06-16 被移、orchestrator 等未跟踪在飞档零误纳。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:26:19 +00:00

5.3 KiB
Raw Blame History

host→iframe 回包链路专项修 · 收口报告

2026-06-11 | 批② 遗留债专项(mc-batch1-merge-shipped 登记 P1)| 主 agent 系统化调试(四阶段纪律全程) 症状:idle 离线补发 loadState 宿主回包从未到达 iframe;疑似同断 ad/pay。

1. 根因(一句话)

GamePlayer.vue 的模板 ref iframeRef 位于 v-for="slot in containers" 内部——Vue 3 对 v-for 内模板 ref 填充的是【数组】而非元素;attachBridge 按元素直取 (数组).contentWindow === undefined → HostBridge.targetWindow = undefined → post() 首行 if (!win) return 静默丢弃全部 host→game 消息(storage/ad/pay 回包与 init 全断)。game→host 方向监听全局 window 不受影响,游戏可玩、遥测可达,故两个波次未现形——storage 是首个真依赖回包的特性,才暴露。

2. 证据链(探针四锚点 + 判别实验,全程 CDP 注入零源码污染)

工具:orchestrator/probe_bridge_channel.py(新增,留作常备诊断件;复用 player_cdp.CdpSession)。 四锚点 = CALL@TOP(游戏发请求)/ RECV@TOP(宿主收到)/ CALL@SUB(宿主回包落到存活 iframe realm)/ RECV@SUB(iframe 收到)。

实验 结果 排除/锁定
v1 双边探针(idle 9108/93086) ①②✅ ③④❌ 断点锁定「宿主 post 出口→iframe 入口」之间;全量 console 无白名单 warn → 排除 key 拒收
v2 判别实验:经存活 contentWindow 手动注入 host_to_game 手动注入 ③④ 全通 通路本身健康 → 问题在 bridge 持有的引用/状态
v3 contentWindow 读取时间线(prototype getter 监听) 应用全程零读取(唯一 CWGET 来自手动实验);单 iframe 元素、srcdoc 两代文档 应用从未从 HTMLIFrameElement 上取过代理 → 唯一自洽解释 = ref 是数组、.contentWindow 读在数组上(不触发 getter、得 undefined)
静态对账 :59 单元素类型声明 ↔ :482 v-for 包裹 :508 ref 与 Vue 3 文档语义(v-for 内 ref 收集为数组)一致,根因成立

附带发现:宿主 sdk_ready→init 下发同被丢弃(修复后首次送达,iframe 侧设计即幂等忽略,无副作用)——host→game 方向此前从未工作过,被 iframe「自启动兜底」完全掩盖。

3. 修法(最小变更,game-studio/src/host/GamePlayer.vue)

  1. currentIframe() 解包助手(兼容数组/元素两形态),attachBridge 改用之;iframeRef 类型如实改为 HTMLIFrameElement | HTMLIFrameElement[] | null 并在声明处注释钉死 v-for 数组语义。
  2. 防御加装(本缺陷活过两波的教训):attachBridge 在 contentWindow 不可用时显式 console.warn——host→game 出口哑火必须可观测,post() 内静默降级维持不变(宿主稳定性优先)。

4. 验证(staging 实证,dist 重建 --mode staging 部署 :4173)

验证 结果
探针复跑(idle 9108) 四锚点全绿(应用自身 rid),init 也送达
离线补发往返(--roundtrip 剧本:点击启动→节流存档→刷新→二局读档) 闭合:localStorage 落真档 {v:1,resource:7353,autoYield:4,…};刷新后 loadState 回包 val=obj
clicker 回归(9011/93011) lifecycle 正常、零异常零告警;无 storage 请求属预期;init 送达无副作用
构建门 npm run build -- --mode staging 绿(vue-tsc -b + vite build),修复标记 grep 进 dist 实证

5. ad/pay 同断判定(遗留债第二问)

确认同断、同根因、随本修同愈(机制级):ad/pay 结果回包与 storage 回包走同一 bridge.post→iframe onHostMessage 分发链,此前一并被 targetWindow=undefined 丢弃(批①只验了请求方向+宿主弹层,回包从未通过)。修复后 init/storage 两类 host_to_game 均实证送达+消费。诚实边界:ad/pay 的业务级 UX 闭环(弹层确认→结果回游戏回调→发奖)未做端到端实测(现有生成游戏不调用 sdk.ad/pay),留 M4 真实广告接入波验收。

6. 影响面与部署状态

  • 改动 = GamePlayer.vue 单文件(解包+告警),所有模板共用宿主,clicker 回归已过;runtime/inject/bridge/契约零改动。
  • staging :4173 已部署修复版 dist(vite preview 按盘服务,探针实证新代码在线);idle 离线补发自本日起在 staging 真实可用。
  • 提交链:dee9dfe→…→修复 fix(runtime-host) + 探针工具五连(见 git log 2026-06-11)。

7. 蒸馏(已落 .agents/rules/engineering-conventions.md)

  1. v-for 内模板 ref 是数组——即使只渲染一个元素;按元素直用 .contentWindow/.value 类属性得 undefined 且 TS 不报(类型声明可以撒谎)。取用必须显式解包。
  2. 跨边界出口的静默降级必须配一次性告警——if (!win) return 式静默丢弃让本缺陷穿过两个波次的五级门;fire-and-forget 出口哑火至少要在挂接时刻可观测。
  3. 「从未有人消费过的方向」不算被验证过——host→game 自建成起即断,所有门禁都只押了 game→host;新增首个反向消费者(storage)时应补双向探针(probe_bridge_channel.py 即此用途,留作常备件)。