Files
youle_cocos/docs/superpowers/specs/2026-09-04-platform-vertical-slice-contract-inventory.md

285 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 平台纵向链路契约清单
> 状态:部分废止。旧源码锚点和外部契约证据仍可用于取证;其中的目标架构、GameRegistry、`assets/games` 目录和实施建议不得继续采用。
>
> 替代规范:`docs/superpowers/specs/2026-09-04-framework-subgame-zero-coupling-migration-design.md`。
>
> 本清单只覆盖 `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://<value>.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/<game-key>/` 创建游戏自身模块、状态、组件和资源;
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 测试。
缺少上述任一项,该契约仍视为未迁移。