# Platform UI Wiring Core Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** 完成启动、登录、大厅、进房的无引擎 UI 接线核心,并用真实 PlatformRuntime 回放验证,不修改 Cocos 序列化资源。 **Architecture:** Runtime 保持唯一状态源;presentation 投影 ViewModel、转发命令并实现同步 ScenePort。实际节点绑定留给单独批准的 3B 编辑器批次;3A 使用记录型 ViewPort 验证。 **Tech Stack:** 现有 TypeScript、node:test、tsx;不新增依赖,不导入 cc。 **Spec:** `docs/superpowers/specs/2026-09-05-platform-ui-wiring-design.md`,并继承 `2026-09-04-framework-subgame-zero-coupling-migration-design.md`。 ## Global Constraints - 服务器包、route/rpc、roomtype 与 deskinfo 全部沿用已验证 contracts。不得为 UI 新增字段。 - 远程配置与原生读取/WVJB 不在本批修改。 - 任何 `.scene/.prefab/.anim/.meta` 变更均使 3A 验收失败。 - 3A 测试夹具只留在 framework-tests,不导入 assets,不声明生产 GameEntry。 - 不扩充 SDK 出口,不依赖旧 PlatformSession/RoomRPCBus,不引入下游兜底。 - 每个 Task 独立执行 RED → GREEN → 规格/实现审查 → 代码质量审查 → 修复复验 → scoped commit。审查不通过不得推进下一 Task。 - 执行使用新的隔离 worktree;先按 using-git-worktrees 核实,不重建已删除的旧工作树来冒充继续旧任务。本文件不授权编辑器资源写入。 ## 执行环境与文件地图 所有代码路径在本计划中相对 `cocoscreator_projects/`;文档路径相对仓库根。所有命令在 `cocoscreator_projects` 执行,git 路径也据此指定。 先完整阅读仓库 AGENTS.md、上述两个设计及本计划。基线为 `8eae325`,执行时检查分支状态并记录新的 base SHA;保留无关修改。不需要重新执行已完成的 decoupled-runtime Tasks 1–11。 新增 `YouleNexus/assets/framework/presentation/`:`ui-contracts.ts` 管端口和模型;`page-model.ts` 管纯投影;`frame-renderer.ts` 管合帧与取消;`platform-ui-controller.ts` 管 ScenePort 和交互。不建第二个 Store 或 barrel SDK 出口。 修改 `platform/runtime.ts` 只增加只读观察及其终止清理;测试放在 `framework-tests/presentation/`,真实运行时联合回放追加到已有 `framework-tests/platform/runtime.test.ts`,复用其中 makeRuntime、RecordingWireClient、ManualClock、makeGameEntry、fixture,不把测试帮助函数搬进产品代码。 通用验证命令: ```powershell node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit node scripts/check-import-boundaries.mjs node --test framework-tests/architecture/import-boundaries.test.mjs git diff --check ``` npm 不可解析时使用 `C:\Users\Joywayer\AppData\Local\Author Software\nvm\.nodejs\npm.exe`。tsx 启动的 `uv_os_get_passwd` ENOMEM 属于待核实环境失败,不是有效 RED;申请在允许的环境运行同一只读测试命令,不改依赖、不写个人环境绕过脚本。不要运行含旧资源迁移生成器的全量 npm test:framework。 ## Task 1: 给 Runtime 增加有明确错误通道的只读观察 **Files:** Modify `YouleNexus/assets/framework/platform/runtime.ts`; Test `framework-tests/platform/runtime.test.ts`。 **Interfaces:** Consumes `PlatformStore.subscribe(listener: (state: PlatformState, previous: PlatformState) => void): () => void` 和 Runtime 既有清理路径。Produces `PlatformRuntime.subscribeState(listener: (state: PlatformState) => void, onError: (error: unknown) => void): () => void`,不立即通知,初值仍取 state。 - [x] 在 runtime.test.ts 现有 helpers 下增加实际观察测试: ```ts test('UI observes canonical commits and unsubscribe is idempotent', async () => { const { runtime, wire } = makeRuntime(); const seen: unknown[] = []; const off = runtime.subscribeState(s => seen.push(s), e => { throw e; }); assert.equal(seen.length, 0); await runtime.start(); wire.emit({ type: 'open', server: 'ws://agent' }); assert.equal(seen[seen.length - 1], runtime.state); off(); off(); const count = seen.length; runtime.login(ACCOUNT); wire.emit({ type: 'message', message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'), } }); assert.equal(seen.length, count); runtime.stop(); assert.throws(() => runtime.subscribeState(() => {}, () => {}), /stopped/); }); ``` - [x] Run `node --import tsx --test framework-tests/platform/runtime.test.ts`,确认因缺 subscribeState 失败。 - [x] 增加每观察者 active 标志、取消集合及终止清理;Store 回调交付提交值,不改为读取可能被重入更新的最新值。核心正常取消结构: ```ts let active = true; const off = (): void => { if (!active) return; active = false; unsubscribeStore(); this.stateObservers.delete(off); }; const unsubscribeStore = this.store.subscribe(state => { if (!active) return; try { listener(state); } catch (error) { off(); try { onError(error); } catch (callbackError) { this.showFatalOnce(this.failureError('UI observer', error, [callbackError])); } } }); this.stateObservers.add(off); return off; ``` `stateObservers` 是 Runtime 新增 `Set<() => void>`;现有 failureError 签名为 `(operation: string, primary: unknown, cleanupErrors: readonly unknown[]): Error`。入口拒绝 `kicked`、`fatal`、`stopped`,错误携带当前生命周期;不新增生命周期。stop 与 fatal 在外部清理回调前使观察者失效。不得将原始错误改成默认数据。 - [x] 追加 RED/GREEN:监听器抛错原对象只交付一次、其他监听继续;onError 抛错保留两错误;stop/fatal 后迟到 Store 回调无效;监听中 stop/取消其他监听/再订阅不会复活。测试使用现有 runtime helpers 和 Store 行为,不用私有字段强转写状态。 - [x] 重跑上述测试和通用验证。规格审查核对原引用、初值与取消语义,质量审查核对异常/重入清理。提交只含本 Task 两个文件,message `feat(platform): expose lifecycle-safe UI observation`。 ## Task 2: 定义 UI 端口并实现唯一快照投影 **Files:** Create `YouleNexus/assets/framework/presentation/ui-contracts.ts`, `YouleNexus/assets/framework/presentation/page-model.ts`, `framework-tests/presentation/page-model.test.ts`。 **Interfaces:** Consumes Task 1 的 PlatformRuntime 只读观察和既有 `PlatformState`、`selectSelfPlayer`、`selectRoomPlayers`。Produces 下列完整端口与 `selectPageModel(page: PageId, state: PlatformState, ready: boolean): PageModel`: ```ts import type { PlatformRuntime } from '../platform/runtime.ts'; import type { ServerDenial } from '../platform/runtime-session.ts'; export type UiRuntime = Pick; export type PageId = 'loading' | 'login' | 'lobby' | 'room'; export interface PlayerModel { readonly playerid: number; readonly nickname: string; readonly avatar: string; readonly bean: number; } export type SeatModel = | { readonly kind: 'empty'; readonly seat: number } | { readonly kind: 'occupied'; readonly seat: number; readonly player: PlayerModel; readonly ready: boolean; readonly offline: boolean }; export type PageModel = | { readonly page: 'loading' } | { readonly page: 'login'; readonly canLogin: boolean } | { readonly page: 'lobby'; readonly self: PlayerModel } | { readonly page: 'room'; readonly roomcode: string; readonly selfSeat: number; readonly seats: readonly SeatModel[]; readonly canPrepare: boolean; readonly canExit: boolean }; export type OverlayModel = | { readonly kind: 'none' } | { readonly kind: 'reconnect' } | { readonly kind: 'denial'; readonly outcome: ServerDenial } | { readonly kind: 'kicked'; readonly data: unknown } | { readonly kind: 'fatal'; readonly error: Error }; export interface FrameScheduler { request(callback: () => void): () => void; } export interface PlatformViewPort { render(model: PageModel): void; activate(page: PageId): void; showOverlay(model: OverlayModel): void; } ``` - [x] 编写投影 RED;测试文件按相对路径 `../../YouleNexus/assets/framework/...` 导入类型/函数,使用 node:test 和 strict assert: ```ts test('empty seats keep authoritative positions and count', () => { for (const count of [2, 4, 10]) { const state: PlatformState = { app: { phase: 'logged-in' }, players: { selfPlayerId: 7, entities: { 7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0, isprepare: 0, onstate: 0 }, } }, room: { kind: 'inside', roomcode: '000123', selfSeat: 0, seatPlayerIds: [7, ...Array(count - 1).fill(null)], readySeats: [], offlineSeats: [], roomtype: [], stage: 0, needprepare: 1, infinite: 0 }, }; const model = selectPageModel('room', state, true); assert.equal(model.page, 'room'); if (model.page !== 'room') throw new Error('expected room'); assert.equal(model.roomcode, '000123'); assert.equal(model.seats.length, count); assert.deepEqual(model.seats[1], { kind: 'empty', seat: 1 }); assert.equal(model.canPrepare, true); assert.equal(model.canExit, true); assert.ok(Object.isFrozen(model)); assert.ok(Object.isFrozen(model.seats)); } }); ``` - [x] Run `node --import tsx --test framework-tests/presentation/page-model.test.ts`,缺模块/函数为预期 RED。 - [x] 实现 switch(page),所有输出及嵌套模型 freeze;不直接泄露 PlatformPlayer 扩展字段。empty 来源仅允许 null。room occupied 的 onstate/isprepare 缺失抛带 seat/player 路径错误;selfSeat 必须指向 self。核心投影: ```ts const playerModel = (player: PlatformPlayer): PlayerModel => Object.freeze({ playerid: player.playerid, nickname: player.nickname, avatar: player.avatar, bean: player.bean, }); const canExit = state.app.phase === 'logged-in' && (room.stage === 0 || room.infinite === 1); ``` PlatformPlayer 从既有 platform-types type-only 导入;room 是 inside 判别收窄后的 state.room。完整 canPrepare 条件按设计第 5 节,不扩展服务器规则。 - [x] 追加逐项 RED/GREEN:lobby 缺 self、occupied 缺实体、selfSeat 不匹配必报错;bean=0/空 avatar 不变;已有准备/离线状态;战斗普通房禁退出、infinite 房可退出;reconnecting 禁命令;输入 roomtype 完全不解析。 - [x] 重跑本测试和通用验证;规格审查查空位及服务器原值,质量审查查冻结/判别联合/职责。scoped commit 三文件,message `feat(presentation): project immutable public page models`。 ## Task 3: 实现可取消、抗重入的合帧渲染 **Files:** Create `YouleNexus/assets/framework/presentation/frame-renderer.ts`, `framework-tests/presentation/frame-renderer.test.ts`。 **Interfaces:** Consumes Task 2 `FrameScheduler`。Produces `class FrameRenderer`,constructor `(scheduler: FrameScheduler, render: () => void, onFault: (error: unknown) => void)`;方法 `request(): void`, `flush(): void`, `dispose(): void`。 - [x] 写入可直接运行的 RED(导入 FrameRenderer、test、assert): ```ts test('coalesces frames and invalidates callbacks even if scheduler delivers late', () => { const callbacks: (() => void)[] = []; let cancelled = 0; let rendered = 0; const renderer = new FrameRenderer({ request(cb) { callbacks.push(cb); return () => { cancelled++; }; }, }, () => { rendered++; }, e => { throw e; }); renderer.request(); renderer.request(); assert.equal(callbacks.length, 1); renderer.flush(); assert.equal(rendered, 1); callbacks[0]!(); assert.equal(rendered, 1); renderer.request(); renderer.dispose(); renderer.dispose(); callbacks[1]!(); assert.equal(rendered, 1); assert.equal(cancelled, 2); }); ``` - [x] Run `node --import tsx --test framework-tests/presentation/frame-renderer.test.ts`,确认缺类 RED。 - [x] 实现 pending cancel、generation、disposed。flush 先失效旧 token、取消旧任务再同步 render;同步异常直接抛出。request 的异步回调先释放 pending,再 try render,失败 dispose 并调用 onFault 原错误。dispose 先置终止位再调用外部 cancel;即使 cancel 抛错,迟到任务仍无效。核心失效顺序: ```ts const cancel = this.pending; this.pending = null; this.generation += 1; if (cancel !== null) cancel(); ``` pending 初始为 null,generation 初始为 0。request/dispose 终止后的请求不排队;flush 终止后不渲染。取消错误和原 render 错误同时存在时用包含原对象的显式聚合错误,不吞掉任何一个。 - [x] 追加 RED/GREEN:render 内 request 只排下一帧;render 内 dispose 后不得重排;取消抛错仍失效;render 异步抛错只报一次;flush 同步抛错不吞。测试实现手动 scheduler,不用真实计时。 - [x] 重跑本测试与通用验证;规格审查查同帧最新/首帧,质量审查查 token 与回调重入。scoped commit 两文件,message `feat(presentation): add cancellable frame renderer`。 ## Task 4: 实现 ScenePort Presenter 和原样命令入口 **Files:** Create `YouleNexus/assets/framework/presentation/platform-ui-controller.ts`, `framework-tests/presentation/platform-ui-controller.test.ts`。 **Interfaces:** Consumes Tasks 1–3。Produces `class PlatformUiController implements ScenePort`;constructor `(view: PlatformViewPort, scheduler: FrameScheduler, onFault: (error: unknown) => void)`;`connect(runtime: UiRuntime): void`;全部既有 ScenePort 方法;`login(account: LoginAccountIdentity): void`, `joinRoom(command: JoinRoomCommand): void`, `prepare(): void`, `exitRoom(): void`, `dismissDenial(): void`, `dispose(): void`。账号/命令/ScenePort 类型均 type-only 导入既有定义。 - [x] 先写纯端口 RED,测试 helper 也留在本测试文件: ```ts test('login forwards the exact account only after the runtime opens login', () => { const sent: unknown[] = []; const runtime: UiRuntime = { state: { app: { phase: 'connected' }, players: { selfPlayerId: null, entities: {} }, room: { kind: 'outside' } }, ready: true, subscribeState: () => () => {}, login: account => { sent.push(account); }, joinRoom: () => {}, prepare: () => {}, exitRoom: () => {}, }; const events: string[] = []; const controller = new PlatformUiController({ render: model => { events.push(`render:${model.page}`); }, activate: page => { events.push(`activate:${page}`); }, showOverlay: () => {}, }, { request: () => () => {} }, error => { throw error; }); controller.connect(runtime); const account = { openid: 'o1', nickname: 'name', avatar: '', sex: 0, province: '', city: '', unionid: 'u1' }; assert.throws(() => controller.login(account), /login/); controller.showLogin(); controller.login(account); assert.equal(sent[0], account); assert.deepEqual(events.slice(-2), ['render:login', 'activate:login']); controller.dispose(); assert.throws(() => controller.login(account), /disposed/); }); ``` - [x] Run `node --import tsx --test framework-tests/presentation/platform-ui-controller.test.ts`,缺类为 RED。 - [x] 实现一次 connect;订阅之后读取 runtime.state。尚未收到 ScenePort 页面请求时不画业务页。切页保存 page、flush 最新模型、检查未被 render 重入终止后 activate;不调用 director/loadScene。核心模型读取: ```ts private renderCurrent(): void { if (this.page === null) return; const runtime = this.requireRuntime(); this.view.render(selectPageModel(this.page, runtime.state, runtime.ready)); } ``` page 初始 null;requireRuntime 对未 connect/已 dispose 显式抛错。用 FrameRenderer 承担合帧,controller 保留 terminal/disposed 生命周期及当前 OverlayModel,不保留玩家/房间镜像。每个外部 View 调用后检查终止位再继续。connect 重复拒绝;dispose 幂等,取消 frame 和 subscription 均尝试执行。 - [x] 实现命令 guards,直接 `runtime.login(account)`/`runtime.joinRoom(command)`,不 spread 或加工业务输入。prepare/exit 使用最新 page model 决定 UI 可用性。恢复/denial/kick/fatal 规则严格按设计第 6 节:terminal 禁命令但仍允许第一次 fatal 画 overlay,fatal 优先于 kicked。 - [x] 每个行为先加 RED 再实现:join 原引用;ready=false 禁登录;每帧最新 state;showRoom 同步 render→activate;denial 保留 data 身份且重试成功;connected/logged-in 清 reconnect 但不清 denial;kick/fatal 后迟到回调不绘制;render/activate/showOverlay 中 dispose 不再调用后续 View;订阅/frame 错误调用 onFault,同步错误向 Runtime 传播。 - [x] 重跑三个 presentation 测试和通用验证;规格审查查导航唯一来源/拒绝重试,质量审查查 controller 未引入第二状态源、循环清理。scoped commit 两文件,message `feat(presentation): wire platform scenes and UI commands`。 ## Task 5: 用真实 Runtime 验证整条接线并锁定边界 **Files:** Modify `framework-tests/platform/runtime.test.ts`; Create `framework-tests/architecture/presentation-boundaries.test.mjs`; Modify 本计划仅勾选已验证任务并记录结果。 **Interfaces:** Consumes `PlatformUiController`、真实 `PlatformRuntime` 及 runtime.test.ts 现有 helpers。Produces 无新增生产 API;只读集成回放与静态边界门禁。 - [x] 将下面集成 RED 加入 runtime.test.ts,导入 controller;使用现有 ACCOUNT、DEVICE、runtimeConfig、RecordingWireClient、ManualClock、fixture、makeGameEntry: ```ts test('real runtime renders the room before restoring canonical deskinfo', async () => { const events: string[] = []; const wire = new RecordingWireClient(); const controller = new PlatformUiController({ render: model => { events.push(`render:${model.page}`); }, activate: page => { events.push(`activate:${page}`); }, showOverlay: model => { events.push(`overlay:${model.kind}`); }, }, { request: () => () => {} }, error => { throw error; }); const deskinfo = { fixture: 'opaque' }; const entry = makeGameEntry({ createModule: () => ({ attach() { events.push('attach'); }, handlePlatformEvent() {}, handleGameMessage() {}, dispose() {}, restore(value) { assert.equal(value, deskinfo); events.push('restore'); }, }) }); const runtime = new PlatformRuntime({ gameEntry: entry, resolveRuntimeConfig: async () => runtimeConfig(), createWireClient: () => wire, scene: controller, loadResources: async () => {}, waitForMinimumDisplay: async () => {}, getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(), }); controller.connect(runtime); await runtime.start(); wire.emit({ type: 'open', server: 'ws://agent' }); controller.login(ACCOUNT); wire.emit({ type: 'message', message: { route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json') } }); controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' }); wire.emit({ type: 'message', message: { route: 'agent', rpc: 'self_join_room', data: { ...fixture('self-join-room.json'), deskinfo } } }); assert.ok(events.includes('render:lobby')); assert.ok(events.indexOf('attach') < events.indexOf('render:room')); assert.ok(events.indexOf('render:room') < events.indexOf('activate:room')); assert.ok(events.indexOf('activate:room') < events.indexOf('restore')); controller.dispose(); runtime.stop(); assert.equal(wire.stopCalls, 1); }); ``` - [x] Run `node --import tsx --test framework-tests/platform/runtime.test.ts`。联合测试若初次已绿,不制造伪失败;通过以下定向负例验证测试能抓回归:临时在 controller 中把 showRoom 首帧排到 scheduler,确认该用例因缺 room render/activate 失败,然后恢复正确代码。该受控变异只涉及本批 TS 并不得提交。 - [x] 追加真实 Runtime 回放:非零登录、非零进房保持原 Store root/原 data,重试能成功;断线重连 overlay 可恢复;kick/fatal 无后续 UI 命令;延迟 loadResources 时不得由 controller 登录;view 回调销毁 Runtime/Presenter 不得 restore;stop 后旧 scheduler 回调无效。夹具数据仅在测试中具备明确来源,不改服务器 fixture 文件来让错误行为通过。 - [x] 编写 architecture 测试,用 TypeScript AST 遍历 presentation 的 import/export/call expression,type-only 合同引用允许,值导入 protocol/net/cc 和动态导入同样禁止。扫描使用 `readFileSync/readdirSync`,不启动 Creator、不写资源。基础 RED 断言先覆盖禁止真实值导入: ```js import ts from 'typescript'; import assert from 'node:assert/strict'; import { test } from 'node:test'; function forbiddenSpecifier(specifier) { return specifier === 'cc' || /(?:^|\/)(?:net|protocol)(?:\/|$)/.test(specifier) || /(?:platform-session|room-rpc-bus)(?:\.|$)/.test(specifier); } test('presentation boundary recognizes forbidden production imports', () => { assert.equal(forbiddenSpecifier('../net/wire-client.ts'), true); assert.equal(forbiddenSpecifier('../protocol/contracts/index.ts'), true); assert.equal(forbiddenSpecifier('../platform/runtime.ts'), false); }); ``` 完成 AST 取值与 type-only 判别后,增加内联正反样本(普通/混合 type import、export from、import()、require()),再扫描真实目录;同文件核对 SDK index 仍仅导出 contracts。禁止整个 presentation 被添加到子游戏例外白名单。不要用单个字符串搜索冒充完整边界检查。 - [x] Run `node --test framework-tests/architecture/presentation-boundaries.test.mjs`;暂时注入禁止 import 到新增 presentation 文件验证真实扫描失败,立即恢复;GREEN 后运行所有纯 TS 测试: ```powershell $uiCoreTests = @(rg --files framework-tests -g '*.test.ts') if ($uiCoreTests.Count -eq 0) { throw 'No TypeScript tests selected' } node --import tsx --test --test-concurrency=1 @uiCoreTests node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit node scripts/check-import-boundaries.mjs git diff --check git diff --name-only HEAD git status --short ``` 运行前检查所选 .test.ts 没有生成资源副作用;不混入 legacy-layer-migration 的 .mjs 生成器。核对从执行 base SHA 到 HEAD 的 committed diff 以及工作区/untracked,确认没有 `.scene/.prefab/.anim/.meta`;不仅检查最后一个 commit。 - [x] 规格审查把设计第 8 节每项映射到测试;质量审查核对 UI 生命周期重入和静态检查漏口。只提交本 Task 测试和计划记录,message `test(presentation): verify runtime UI wiring and boundaries`。 ## 完成与后续门槛 ### 3A 执行验证记录(2026-09-05) - 执行分支 `codex/platform-ui-wiring-core`;实际 base `2d7655a5d7d2e56733d9622eac6d6e0fcb36254b`,Tasks 1–4 已经逐项规格与质量审查并提交,Task 4 head `d29a659541bcb0dcfe9037a6122ff03a25b9404f`。 - Task 5 实现后的独立验证:显式枚举 `.test.ts` 的回归 499/499;两个指定架构测试文件合计 49/49;上述命令退出码均为 0。`tsc --noEmit`、`check-import-boundaries.mjs`、`git diff --check` 通过。旧 EventBus/reactive/StoreBinder 异常路径测试有预期错误日志;新增测试输出干净。 - 真实 Runtime 63/63,新增回放验证原始 deskinfo 恢复顺序、非零拒绝原引用及重试、重连、终止、资源门槛及重入销毁。异步 showRoom 定向变异和真实禁止 import 变异均使对应测试失败,已立即恢复。 - 验证前扫描纯 TS 测试的资源写入风险;未运行生成器混入的全量 runner。分支 base 到当前提交及未提交/untracked 范围没有 `.scene/.prefab/.anim/.meta` 差异。执行早期误运行旧 runner 产生的两份 out prefab 变更已获用户明确授权、精确恢复并核验。 - UI 接线核心仅为 3A 无引擎回放;不是实际 Cocos 接管。Task 5 已完成规格与质量审查:SDK 本地导出漏检及精确声明数量问题经过两轮 RED/GREEN 修复和范围复审,全部关闭。最终整分支审查已通过,审查范围 `2d7655a..ec67fe6` 无 Critical/Important/代码类 Minor 问题;`ec67fe6` 上重新验证 TS 499/499、架构 49/49、类型与导入检查通过,工作区及整个提交范围无序列化资源差异。本次状态更新仅修改文档。 报告实际测试命令、数量、退出码、base/head SHA 与资源零变更检查;不沿用过去的 408/408 数字当本批结果。3A 完成可表述为“UI 接线核心已通过无引擎回放”。 不自动进入 3B。进入实际 Cocos 接管前,须另行确认允许修改的序列化文件、真实 GameEntry/目标宿主/账号来源,再通过 MCP 核实控件绑定并制定编辑器实施任务。主题扩展与真实子游戏继续各自独立计划。