承接目录治理:上轮只压缩内容未减文件数,闭线工作记录仍平铺致目录看着仍一堆(创始人指出)。本次执行 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>
5.3 KiB
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)
currentIframe()解包助手(兼容数组/元素两形态),attachBridge改用之;iframeRef类型如实改为HTMLIFrameElement | HTMLIFrameElement[] | null并在声明处注释钉死 v-for 数组语义。- 防御加装(本缺陷活过两波的教训):
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)
- v-for 内模板 ref 是数组——即使只渲染一个元素;按元素直用
.contentWindow/.value类属性得 undefined 且 TS 不报(类型声明可以撒谎)。取用必须显式解包。 - 跨边界出口的静默降级必须配一次性告警——
if (!win) return式静默丢弃让本缺陷穿过两个波次的五级门;fire-and-forget 出口哑火至少要在挂接时刻可观测。 - 「从未有人消费过的方向」不算被验证过——host→game 自建成起即断,所有门禁都只押了 game→host;新增首个反向消费者(storage)时应补双向探针(probe_bridge_channel.py 即此用途,留作常备件)。