# 平台纵向链路契约清单 > 本清单只覆盖 `2026-09-04-platform-vertical-slice-design.md` 的第一批范围。 > > 完整 84 项平台 RPC 的唯一清单仍是 `docs/protocol/README.md`,字段权威仍在 `docs/protocol/01-05`。本文不复制完整协议,只记录第一批实现门槛、源码锚点和当前偏差。 ## 1. 权威证据 | 领域 | 权威文件 | |---|---| | 传输、信封、心跳、登录门控、切服 | `docs/protocol/01-传输层与架构.md` | | agent RPC | `docs/protocol/02-协议-agent路由.md` | | room RPC | `docs/protocol/03-协议-room路由.md` | | 登录、玩家、房间、roomtype 数据 | `docs/protocol/04-数据结构.md` | | 游戏 route、deskwar、deskinfo | `docs/protocol/05-游戏内协议与桥接.md` | | 旧连接与启动流程 | `projects/Game_Surface_3/js/00_Surface/12_Logic.js` | | 旧发包与收包入口 | `projects/Game_Surface_3/js/00_Surface/09_Net.js` | | 旧房间状态机 | `projects/Game_Surface_3/js/00_Surface/07_Desk.js` | | 原生配置与 WVJB | `projects/Game_Surface_3/js/00_Surface/05_Func.js` | | 默认 gameserver | `projects/Game_Surface_3/js/01_SubGame/00_SubGame_Config.js:11` | ## 2. 启动与配置契约 | 编号 | 契约 | 原工程锚点 | 当前 YouleNexus 偏差 | 第一批门槛 | |---|---|---|---|---| | CFG-01 | 默认 `gameserver` 来自子游戏配置唯一常量 | `00_SubGame_Config.js:11` | `profiles.ts` 使用不同 URL | profile 与目标原工程值一致 | | CFG-02 | 原生 `gameconfig` 非空时:`-`→`/`、`#`→`:`,构造 `http://.txt` | `12_Logic.js:1268-1279` | 未完整复刻 | 黄金输入输出测试 | | CFG-03 | H5 渠道取 query `channelid`、`agentid`;原生渠道走原方法 | `12_Logic.js:1215-1246` | source 合并策略可能改变分支 | mode/userAgent 分支显式建模 | | CFG-04 | 原生 agent 使用 `window.settings.getothername('agent')`;仅 uAgent_3 使用 `window.app_agent` | `05_Func.js:2451-2468`, `12_Logic.js:1232-1246` | 捕获任意错误后都会尝试全局回退 | 只有来源明确定义的分支可回退 | | CFG-05 | `get_config(gameserver)` 成功后解析 JSON 并取 `_msg.data.urlserver` | `12_Logic.js:521,533-550` | 当前解析 `*_server_tcp` | WebSocket 地址只取 `urlserver` | | CFG-06 | `urlserver` 可为单地址或候选数组 | `12_Logic.js:826-899` | target 支持 servers 数组,但上游来源不同 | 保持顺序并逐项规范为 ws/wss URL | | CFG-07 | 配置失败不产生替代服务器 | 仓库第二准则 | `LoginFlow.ts` 回退 localhost | release/debug 都显式失败 | | CFG-08 | debug 直连是 profile 的显式语义 | `profiles.ts` 目标设计 | 已具备,但入口硬编码 local | 仅显式选择 profile 时生效 | 启动就绪条件固定为四项: ```text resourcesReady && configReady && socketOpen && minimumDisplayElapsed ``` 任一项失败都不能把其它项当作成功。`LaunchFlow.ts` 的固定一秒模拟不属于契约实现。 ## 3. 传输契约 | 编号 | 契约 | 精确要求 | |---|---|---| | NET-01 | 出站信封 | `{app:"youle", route, rpc, data}`,单层 JSON | | NET-02 | 入站载荷 | 浏览器 `MessageEvent.data` 解出协议单层信封;允许旧服务实际出现的字符串化形态,但不能虚构第二层业务包装 | | NET-03 | route 分界 | `platform/agent/room` 进入平台;其它 route 仅在等于 active game route 时进入子游戏 | | NET-04 | 登录门控 | 发出 login 后,只处理 `player_login` 与 `kick_server`,其它业务包丢弃 | | NET-05 | 登录守护 | 子游戏模式四秒未收到 login 响应时触发重连 | | NET-06 | 收包超时 | 有效帧重置 watchdog;超时进入 slow 并重连 | | NET-07 | 踢下线 | `kick_server` 停止自动重连并进入 kicked | | NET-08 | 切服 | `connect_roomserver.data.roomserver`、`connect_agentserver.data.agentserver` 更新当前服务器并重连重登 | | NET-09 | 发送前置条件 | transport 或完整 login identity 缺失时显式抛错,不静默 return | | NET-10 | 唯一消费者 | 每个业务信封只交给一个 Router 一次 | ## 4. 第一批 RPC 契约 ### 4.1 agent route | RPC | 方向 | 第一批用途 | 请求/响应权威 | 必须验证的分支 | |---|---|---|---|---| | `player_login` | 双向 | 首登、重登、恢复房间 | protocol/02 §登录;protocol/04 §登录响应 | state 失败、无房、大厅、在房、含 deskinfo | | `self_join_room` | 双向 | 房号加入房间 | protocol/02 §房间创建/进入;protocol/04 §公共字段 | state=0、state=99、一般失败、deskwar、deskinfo、普通进房 | | `connect_agentserver` | S→C | 切换 agent server | protocol/01 §7.4 | 地址存在、地址缺失报错 | | `connect_roomserver` | S→C | 切换 room server | protocol/01 §7.4 | 地址存在、地址缺失报错 | | `kick_server` | S→C | 强制下线 | protocol/01 §3.2 | 停止重连、显示踢下线态 | `player_login.data.version` 按已验证协议使用数字 versionCode。请求的必需字段是: ```text agentid, gameid, openid, nickname, avatar, sex, province, city, unionid, version, channelid, marketid, machineid, machineroom ``` 条件字段严格按来源条件加入:`ip`、`location`、`telphone`、`telphoneAuto`、缓存 `playerid`。条件不成立时字段应省略,不用空值伪装已提供。 `self_join_room` 基础请求字段是: ```text agentid, playerid, gameid, roomcode ``` `location` 与 `ip` 按原发送器注入;`vipMatch`、`match_id` 不属于第一批普通房号加入入口。 ### 4.2 room route | RPC | 方向 | 必需 data | Store/钩子结果 | |---|---|---|---| | `other_join_room` | S→C | `seat` + 完整玩家座位对象 | upsert 玩家实体,写 seatPlayerIds;`deskwar` 时开战 | | `self_exit_room` | 双向 | 响应可含 `seat/isowner/roomcode` | 成功后清房间;保留登录玩家实体 | | `other_exit_room` | S→C | `seat` | 清座位映射;玩家实体无其它引用时移除 | | `player_prepare` | 双向/推送 | `seat`,可选 `deskwar` | 更新 ready,调用 onPlayerReady;deskwar 时开战 | | `other_offline` | S→C | `seat` | 对应玩家 `onstate=1`,调用 onPlayerOffline | | `other_online` | S→C | `seat`, `ip` | 对应玩家 `onstate=0` 并更新 ip,调用 onPlayerOnline | 上表的座位值以服务器协议为准。转换为数组索引必须集中在一个座位转换函数中;控制器和 handler 不允许各自执行 `seat - 1`。 ## 5. 登录响应字段组 ### 5.1 A 组:玩家与代理状态 第一批 parser 至少显式声明并校验 protocol/04 已验证字段: ```text state, playerid, agentid, channelid, nickname, avatar, openid, sex, unionid, roomcard, bean, score, invitecode, advanced, taskstate, ip, bankpower, bank, sign, tel, initCard, initBean, bankpwd, agentname, agentmode, gameversion ``` 字段是否可选以 protocol/04 的条件说明为准。可选性必须写在 parser 类型中;不得用 `?? 0` 或 `?? ''` 把缺失变成有效数据。 ### 5.2 B 组:房间恢复状态 存在 `roomcode` 时,parser 至少处理: ```text roomcode, roomtype, asetcount, isbattle, makewar, seat, isowner, players, roommode, beanlimit, needprepare, infinite, rebateNumber, rebateMode, rebateType, sign, ownerNotice, videoConfig, shortcode, match, matchid, agreefree, isbet, deskinfo ``` 关键判定: - `roomcode` 存在决定是否恢复房间; - `isbattle` 只写房间 stage; - `deskinfo` 值为真决定是否调用 `onReconnect(deskinfo)`,严格复刻原工程 `if (deskinfo)`; - `deskinfo` 值不被平台复制、补字段或解析; - `roomtype` 保留完整嵌套数组结构。 ## 6. 状态所有权 | 数据 | 唯一来源 | 禁止的第二来源 | |---|---|---| | runtime mode | runtime-mode resolver | Cocos 控制器硬编码 | | gameserver | active profile,经原生 gameconfig 合法覆盖 | LoginFlow 常量 | | WebSocket servers | remote `data.urlserver` 或显式 debug direct profile | Store/NetClient 自行推导 | | channel identity | identity resolver | 发包器补默认 ID | | login identity | 登录/授权来源 | NetClient mock identity | | rpc→route | 生产 `RPC_ROUTE` | 控制器字符串 | | 玩家实体 | PlayerDirectory `entities` | RoomStore 中复制 PlayerState | | 自己身份 | `selfPlayerId` 指向玩家实体 | 独立 C_Player 镜像 | | 房间座位 | RoomState `seatPlayerIds` | UI 局部玩家数组 | | 当前场景 | AppState scene/phase | 反查某个节点 active | | roomtype | RoomState 原样值 | UI 重新生成或规范化 | | deskinfo | 当前入站响应中的原样值,完成一次性派发后由子游戏接管 | PlatformState 或平台拆解后的派生对象 | ## 7. 子游戏解耦契约 ### 7.1 公开边界 | 编号 | 契约 | 验收方式 | |---|---|---| | GAME-01 | 平台核心只依赖 `sdk/contracts`,不导入具体游戏 | import-boundary test | | GAME-02 | 具体游戏只依赖 `sdk/contracts` 与自身目录,不导入 platform/net/protocol/ui 内部 | import-boundary test | | GAME-03 | SDK contracts 不依赖 Store、Reactive、EventBus、WebSocket 或 `cc` | import-boundary test | | GAME-04 | 每个游戏以 `GameDescriptor` 注册,key/gameId/route 唯一 | registry unit test | | GAME-05 | `apiVersion` 必须与平台完全匹配 | mismatch failure test | | GAME-06 | descriptor factory 每局产生新 GameModule | identity assertion | | GAME-07 | 平台→游戏只走生命周期和 `PlatformToGameEvent` | fake module event log | | GAME-08 | 游戏 route 包只走 `handleGameMessage` | router integration test | | GAME-09 | deskinfo 只走 `restore`,平台不保存、不解析 | reference/value assertion | | GAME-10 | 游戏→平台只走受限 `GameHost` | public type and import scan | | GAME-11 | `sendGameMessage` 自动绑定本游戏 route | encoded envelope assertion | | GAME-12 | platform command 使用判别联合,未知命令显式失败 | exhaustive parser test | | GAME-13 | snapshot 是冻结 DTO,不暴露内部 Store 类型 | mutation failure/type test | | GAME-14 | dispose 后 Host 失效并释放全部订阅 | lifecycle/leak test | | GAME-15 | 不同游戏、不同房间会话之间无状态泄漏 | two-game isolation test | | GAME-16 | `resolveSeatCount(roomtype)` 是座位总数的唯一游戏侧解释入口,且必须返回正整数 | descriptor validation test | | GAME-17 | Host 座位映射绑定当前 self seat/seat count,子游戏不导入内部 seat 实现 | 2/4/10-seat round-trip test | 第一版 `GameHostCommand` 只包含: ```text room.prepare → 发送第一批 player_prepare room.exit → 仅未开局时发送 self_exit_room;已开局显式拒绝 ``` 平台先提交权威 Store,再串行通知子游戏 event。`subscribe` 初次立即发 snapshot,后续仅在公开 snapshot 变化时通知;snapshot 不能替代需要恰好处理一次的 event。 ### 7.2 现有 SDK 必须移除的耦合 | 现状 | 原因 | 目标 | |---|---|---| | `sdk/index.ts` import `platform/readonly.ts` 和 Store types | SDK 类型随平台内部重构而变化 | 使用独立 `PlatformGameSnapshot` DTO | | `GameContext.events` 暴露内部 EventBus | 子游戏依赖平台事件实现且可产生无约束事件 | 删除;子游戏自行管理内部事件 | | `GameNet.send(route,rpc,data)` | 子游戏可伪造平台 route | 改为自动绑定 route 的 `sendGameMessage` | | 大量可选 hook | 未实现时静默跳过,接口持续膨胀 | 改为判别联合 `PlatformToGameEvent` | | `IGameModule.serialize?()` | 无客户端上传快照的协议证据 | 删除;重连快照只接受服务器 deskinfo | | `ActiveGame.set/clear` | 无注册、版本和释放校验 | 用 GameRegistry + GameSessionHost 替代 | ### 7.3 子游戏接入步骤 接入一个新子游戏只允许以下步骤: 1. 在 `assets/games//` 创建游戏自身模块、状态、组件和资源; 2. 实现 `GameModule` 与唯一 `GameDescriptor`; 3. 使用 `game-contract-harness` 跑通标准契约测试; 4. 在应用组合根注册 descriptor; 5. 增加该游戏自己的 route/rpc、roomtype 和 deskinfo 测试。 上述步骤不包含修改 framework Router、NetClient、PlatformState 或平台 UI。若接入必须修改这些核心模块,说明 SDK 契约缺失,应先作为平台 API 变更单独设计和升级版本。 ## 8. 首批 UI 对应关系 | 行为 | prefab/Layer | 控制器职责 | |---|---|---| | 启动背景和资源进度 | Layer 1 | 只显示加载状态 | | 网络请求遮罩 | Layer 614 | 订阅 pending command 数量 | | 断线重连 | Layer 615 | 订阅 reconnecting/slow | | 强制下线 | Layer 616 | 订阅 kicked 并阻止继续操作 | | 登录/授权入口 | `Login_Layer.prefab` | 产生 login identity 或触发已有身份登录 | | 大厅 | Layer 4 | 展示 self player selector;打开 JoinRoom | | 输入房号 | Layer 15 | 本地六位输入;确认时调用 joinRoom command | | 房间壳 | Layer 50、411 | 展示房号、时钟、网络态和退出入口 | | 准备按钮 | Layer 403 | 调用 prepare command,状态由服务器推送确认 | | 玩家座位 | Layer 202、416 | 从 seat selector 渲染,不保存玩家副本 | 对 prefab 的节点路径、按钮绑定与组件挂载,以当前 Cocos 资源实际结构为准,并在实施时通过 funplay-cocos MCP 查询和修改。 ## 9. 原生接口范围 第一批只实现启动必需的同步读取: ```text window.settings.getothername('agent') window.settings.getothername('gameconfig') window.settings.getothername('servertype') window.settings.getchannelName() window.settings.getmarketname() ``` uAgent_3 的 `window['app_' + name]` 是原工程明确定义的独立分支,不是任意异常的通用回退。 完整 WVJB 作为后续独立子项目。现阶段已确认的审计事实: - 原工程注册 21 个唯一 handler 名; - 源码文本出现 41 个唯一 `callHandler` 名,其中 `getcompareCode`、`getothername`、`getmarketname` 仅出现在注释调用; - 入站与出站名称集合不同,且存在大小写敏感名称,例如 `getphoneinfo` 与 `getphoneInfo`; - 初始化必须保留 `window.WVJBCallbacks`、`WebViewJavascriptBridgeReady` 和 `wvjbscheme://__BRIDGE_LOADED__`。 因此现有 14 项共用白名单不能作为完整桥契约继续扩展。 ## 10. 黄金样本清单 实施前必须把以下样本固化到 `cocoscreator_projects/framework-tests/fixtures/protocol/`: 1. 远程配置:单 `urlserver`、数组 `urlserver`、缺失 `urlserver`; 2. `player_login` 请求:普通身份、带 ip/location、设备手机号、缓存 playerid; 3. `player_login` 响应:失败、成功无房、成功在房无 deskinfo、成功在房含 deskinfo; 4. `self_join_room`:成功普通、state=99、一般失败、deskwar、deskinfo; 5. `other_join_room`:完整玩家和准备条件; 6. `player_prepare`:普通准备与 deskwar; 7. `other_exit_room`、`other_offline`、`other_online`; 8. `connect_agentserver`、`connect_roomserver`、`kick_server`; 9. 一段纵向序列:登录大厅→进房→准备→掉线→重登恢复。 若仓库中没有真实样本,先从原客户端或测试服务器抓取并脱敏。没有证据的字段不能凭名称推断。 ## 11. 完成判据 每条契约须具备: - 一个权威源码/协议锚点; - 一个具名 TypeScript 类型; - 一个运行时 parser; - 至少一个成功黄金样本; - 必需字段缺失的失败测试; - 对应 Store action 或运行时状态迁移测试; - 涉及 UI 时的控制器测试; - 涉及服务器时的出站信封断言。 每个子游戏 descriptor 还必须通过 `game-contract-harness` 和 import-boundary 测试。 缺少上述任一项,该契约仍视为未迁移。