Files
youle_cocos/docs/protocol/03-协议-room路由.md
T
joywayerandClaude Opus 4.8 08640ca89a docs(protocol): 交叉源码审计后重修全 8 篇协议文档
基于 00_Surface 平台层源码逐条交叉核对,修订 docs/protocol:
- 补全前后端双向数据包(请求+响应/推送字段),覆盖框架模板全部平台层 rpc
- 修正错误:重连触发条件(deskinfo 而非 isbattle)、小程序桥接(openminigamedata 而非 miniProData)、
  get_player_invitecode 字段、setGameServer/GameData.Server 来源、route 分发守卫等
- 补遗漏 rpc:send_phone_code_wechat、submit_error/submit_log、refresh_task_state、
  update_bean/update_charm/broadcast、connect_agentserver 字段、5 个成对推送常量
- 补 Desk 约 11 个字段、登录响应账号字段、10 个子游戏钩子
- 标注源码 bug(🐛 can_award 接收函数未定义、小程序 checkType=8 误接白名单)
- 校准全篇 file:line 引用;待后端确认项标 ⚠️
- 明确范围:仅框架模板协议,不含子游戏对局包

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:35:47 +08:00

307 lines
20 KiB
Markdown
Raw 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.
# 03 · 平台层协议 — room 路由(房间服务器)
> 信封:`{ app:"youle", route:"room", rpc:"<下列名称>", data:{...} }`
> 本篇覆盖 **route=room 的全部平台层数据包**(房间生命周期 / 解散投票 / 开局 / 房内社交 / 服务器切换)。不含子游戏对局内协议(见 05 章)。
> 房间内操作通用请求字段(除特别说明外,C→S 请求恒含这四项):`agentid`、`gameid`、`playerid`、`roomcode`。
> 「响应/推送」均指服务器返回内层 `data` 字段。座位号 `seat` 通常为 0 起整数;`Logic.ChangeToStatus(C_Player.seat, seat)` 把绝对座位转为以自己为基准的相对视角。
>
> **图例**:🐛 源码 bug;⚠️ 待后端确认;— 无特别说明。
>
> **条目格式**:
> `rpc名` · route=room · <方向> — 场景;请求字段(C→S);响应/推送字段(S→C);源码 发送/接收;备注。
>
> **路由说明(交叉引用)**:发送(C→S)走 `RouteList.room`,但少数推送虽在 room 场景内消费,其上行请求实际走 `agent` 路由——下文逐条标注;这类项归档于 [02 · agent 路由](./02-协议-agent路由.md),此处仅记录其在房内的接收语义。
---
## 房间生命周期
### self_break_room · route=room · C→S 请求 + S→C 响应
- 场景:房主在**未开局**前主动解散房间。
- 请求字段(C→S):仅通用四字段。
- 响应字段(S→C):`roomcode`(可选,用于从本地"我的房间"列表移除)。处理:清空牌桌、回大厅。
- 源码:发送 `09_Net.js:266`(`Send_self_break_room`)/接收 `09_Net.js:271` → `07_Desk.js:707`(`Desk.self_break_room`)。
- 备注:—
### other_break_room · route=room · S→C 推送
- 场景:他人(房主)解散房间,推送给房内其余玩家。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):无业务字段;触发 `Func.exitRoom()`、清桌、回大厅、提示"房主已解散房间!"。
- 源码:接收 `09_Net.js:277` → `07_Desk.js:721`(`Desk.other_break_room`)。
- 备注:常伴随 **connect_agentserver** 切服包(`data.opt == other_break_room`,见下文)。
### self_exit_room · route=room · C→S 请求 + S→C 响应
- 场景:自己在**未开局**前退出房间。
- 请求字段(C→S):仅通用四字段(发送处见 `08_Utl_Output.js:993`)。
- 响应字段(S→C):`isowner`(可选,==1 且非无限局时把房间加回本地列表), `seat`(可选,传给 `Game_Modify.myExitRoom`), `roomcode`(可选)。
- 源码:发送 `09_Net.js:289`(`Send_self_exit_room`)/接收 `09_Net.js:294` → `07_Desk.js:753`(`Desk.self_exit_room`)。
- 备注:—
### other_exit_room · route=room · S→C 推送
- 场景:其他玩家(未开局)退出房间。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):`seat`(离开者座位)。处理:清该座位、`playercnt--`;若 `seat==0` 且非无限局,提示房主已离开。
- 源码:接收 `09_Net.js:301` → `07_Desk.js:785`(`Desk.other_exit_room`)。
- 备注:—
### player_prepare · route=room · C→S 请求 + S→C 推送
- 场景:玩家点击准备(`needprepare==1` 的房间)。
- 请求字段(C→S):仅通用四字段。
- 响应/推送字段(S→C):`seat`(准备者座位), `deskwar`(可选;为真表示满足开战条件 → `HideStartScene` + `Game_Modify.StartWar`)。推送给房内所有人。
- 源码:发送 `09_Net.js:629`(`Send_player_prepare`)/接收 `09_Net.js:632` → `07_Desk.js:1126`(`Desk.player_prepare`)。
- 备注:—
### change_room · route=room · C→S 请求 → S→C 响应(change_seat) · 跨座换桌
- 场景:玩家请求换座 / 换桌。
- 请求字段(C→S):仅通用四字段(**不带目标座位**,由服务器决定换到哪个空位)。发送处 `11_GameUI.js:1945`、`08_Utl_Output.js:1006`。
- 响应字段(S→C):rpc 名改为 **`change_seat`**,`data`:`seat1`, `seat2`(两个互换的座位号)。处理:交换两座 Desk 信息,若自己在其中则 `C_Player.SetSeat` 更新。
- 源码:发送 `09_Net.js:678`(`Send_change_room`)/接收 `09_Net.js:682`(`Net.change_seat`) → `07_Desk.js:178`(`Desk.change_seat`)。
- 备注:上行 rpc=`change_room`,下行 rpc=`change_seat`,二者成对。
### share_room · route=room · C→S 请求 + S→C 响应
- 场景:把房间分享到世界房列表。
- 请求字段(C→S):通用四字段;可选 `roomlist`、`roomtype`、`shareType`(高级房分享时携带,见 `11_GameUI.js:1734`/`11_GameUI.js:1891`)。
- 响应字段(S→C):无业务字段;提示"已成功分享至平台!"。
- 源码:发送 `09_Net.js:635`(`Send_share_room`)/接收 `09_Net.js:638` → `07_Desk.js:1152`(`Desk.share_room`)。
- 备注:—
---
## 进房推送(他人视角)
### other_join_room · route=room · S→C 推送
- 场景:其他玩家加入当前房间。
- 请求字段(C→S):无(纯推送;自己进房用 `self_join_room`,走 agent 路由,见 02 章)。
- 响应/推送字段(S→C):`seat`(新玩家座位) + 该玩家完整对象(直接传给 `Player.SetDeskInfo`,见 [04 · Player 座位对象](./04-数据结构.md#player-座位对象)):
`playerid`, `nickname`, `avatar`, `sex`, `ip`, `onstate`, `bean`, `charm`, `sign` 等;另含 `needprepare`(可选), `deskwar`(可选;为真直接走 `Desk.makewar` 开战)。
- 源码:接收 `09_Net.js:281` → `07_Desk.js:729`(`Desk.other_join_room`);字段写入见 `06_Player.js:263`(`Player.SetDeskInfo`)。
- 备注:⚠️ `charm`/`sign` 是否每次必含、其余字段是否完整以 `Player.SetDeskInfo` 实际读取为准,详见 [04 章](./04-数据结构.md#player-座位对象)。
### other_offline · route=room · S→C 推送
- 场景:其他玩家离线。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):`seat`。处理:该座 `onstate=1`,刷新 UI。
- 源码:接收 `09_Net.js:390` → `07_Desk.js:887`(`Desk.other_offline`)。
- 备注:—
### other_online · route=room · S→C 推送
- 场景:其他玩家重新上线。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):`seat`, `ip`。处理:该座 `onstate=0`、更新 ip。
- 源码:接收 `09_Net.js:395` → `07_Desk.js:893`(`Desk.other_online`)。
- 备注:—
---
## 解散投票(开局后)
> 流程:某玩家 apply → 全员收到 `other_apply_free_room`(带各座状态与倒计时)→
> 各玩家 agree/refuse → 最终 `free_room` 广播结果。
### self_apply_free_room · route=room · C→S 请求 + S→C 响应
- 场景:自己申请解散房间。
- 请求字段(C→S):仅通用四字段。
- 响应字段(S→C):`agreefree` { `state`:[各座同意状态数组], `countdown`:倒计时 }。
- 源码:发送 `09_Net.js:308`/接收 `09_Net.js:313` → `07_Desk.js:804`(`Desk.self_apply_free_room`)。
- 备注:—
### other_apply_free_room · route=room · S→C 推送
- 场景:他人申请解散。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):`seat`(申请者), `agreefree` { `state`[], `countdown` }。
- 源码:接收 `09_Net.js:319` → `07_Desk.js:817`(`Desk.other_apply_free_room`)。
- 备注:—
### self_agree_free_room · route=room · C→S 请求 + S→C 响应
- 场景:自己同意解散。
- 请求字段(C→S):仅通用四字段。
- 响应字段(S→C):无业务字段;本地把自己加入同意列表、刷新投票 UI。
- 源码:发送 `09_Net.js:323`/接收 `09_Net.js:328` → `07_Desk.js:829`(`Desk.self_agree_free_room`)。
- 备注:—
### other_agree_free_room · route=room · S→C 推送
- 场景:他人同意解散。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):`seat`(同意者)。
- 源码:接收 `09_Net.js:333` → `07_Desk.js:836`(`Desk.other_agree_free_room`)。
- 备注:—
### self_refuse_free_room · route=room · C→S 请求 + S→C 响应
- 场景:自己拒绝解散。
- 请求字段(C→S):仅通用四字段。
- 响应字段(S→C):无业务字段;清空同意列表、投票结果置不通过、弹"已拒绝"结果。
- 源码:发送 `09_Net.js:338`/接收 `09_Net.js:343` → `07_Desk.js:842`(`Desk.self_refuse_free_room`)。
- 备注:—
### other_refuse_free_room · route=room · S→C 推送
- 场景:他人拒绝解散(任一人拒绝即否决本轮)。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):`seat`(拒绝者)。
- 源码:接收 `09_Net.js:348` → `07_Desk.js:853`(`Desk.other_refuse_free_room`)。
- 备注:—
### free_room · route=room · S→C 推送
- 场景:解散投票最终结果。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):
| 字段 | 说明 |
|------|------|
| `freeNow` | true=立即解散;false=投票通过待确认 |
| `deskfree` | 解散结算信息对象(可选) |
| `roomcard` | 解散后房卡数(可选,写回 `C_Player.setRoomcard`) |
| `tips` | 提示文本(`freeNow==true` 时) |
| `time` | 提示显示时长(`freeNow==true` 时) |
| `seats` | 投票通过的座位数组(`freeNow==false` 时传给 `OpenApplyResult`) |
- 源码:接收 `09_Net.js:353` → `07_Desk.js:864`(`Desk.free_room`)。
- 备注:可伴随 **connect_agentserver** 切服包(`data.opt == free_room`,见下文)。
### beanroom_surrender · route=room · C→S 请求 + S→C 响应
- 场景:豆豆房(金币房)投降。
- 请求字段(C→S):通用四字段 + `count`(投降数量 = `GameData.surrendCount`,发送处 `11_GameUI.js:1245`)。
- 响应字段(S→C):`state`(0=成功 → `Game_Modify.onSurrender(_msg)`); 失败时 `showerror`(==1 则弹) / `error`(错误文本)。
- 源码:发送 `09_Net.js:613`(`Send_beanroom_surrender`)/接收 `09_Net.js:617`(`Net.beanroom_surrender`,逻辑直接在 Net 内处理)。
- 备注:⚠️ 未见成对的 `other_xxx` 推送,是否向房内其他玩家广播他人投降待后端确认。
---
## 开局
### self_makewar · route=room · C→S 请求 → S→C 响应(self_makewar)
- 场景:房主主动开局。
- 请求字段(C→S):仅通用四字段。
- 响应字段(S→C):rpc=`self_makewar`,无业务字段;触发 `Desk.self_makewar` → `Game_Modify.StartWar(_msg)`。
- 源码:发送 `09_Net.js:470`(`Send_self_makewar`)/接收 `09_Net.js:475` → `07_Desk.js:987`(`Desk.self_makewar`)。
- 备注:—
### other_makewar · route=room · S→C 推送
- 场景:开局广播(房主开局或满员自动开局)给房内其他玩家。
- 请求字段(C→S):无(纯推送)。
- 响应字段(S→C):开局信息对象 → `Desk.makewar` → `Game_Modify.StartWar(msg)`。
- 源码:接收 `09_Net.js:480`(`Net.other_makewar`) → `07_Desk.js:1020`(`Desk.makewar`)。
- 备注:实际发牌等对局数据在此之后通过**子游戏内协议**下发(见 05 章)。
---
## 房间内社交
### send_text · route=room · C→S 请求 + S→C 推送 · 文字聊天
- 场景:文字聊天 / 全服公告 / 预定义常用语。
- 请求字段(C→S):通用四字段 + `text`(内容;点常用语时为 `Game_Config.Info.TextContent[spid-206]` 文本) + `type`(0=普通 / 1=全服公告;按是否勾选公告设 0/1) + `info`(可选;**有 info 时 `type` 改为 2**)。发送处 `11_GameUI.js:1170`(输入框)、`11_GameUI.js:2717`(常用语)。
- 推送字段(S→C):`type`(0=普通 / 1=全服公告 / 2=机器人 / 3=预定义文字), `text`(内容;**type==3 时为 `Game_Config.Info.TextContent` 的 1 基索引**,客户端按 `(idx-1)%len` 取文本), `seat`(发送者座位,type≠1 时使用), `info`(type==2 时附加)。
- 源码:发送 `09_Net.js:400`(`Send_send_text`)/接收 `09_Net.js:404` → `07_Desk.js:900`(`Desk.send_text`)。
- 备注:百人场(`vipInfinite`)只处理 type==1 公告分支。
### receive_chat · route=room · S→C 推送(⚠️)
- 场景:聊天推送的另一可能 rpc 名(与 `send_text` 成对)。
- 请求字段(C→S):无。
- 推送字段(S→C):未知(疑似同 `send_text` 推送结构)。
- 源码:常量 `02_Const.js:38`(`RpcList.receive_chat`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器聊天推送究竟用 `send_text` 还是 `receive_chat` 待后端确认。
### send_voice · route=room · C→S 请求 + S→C 推送 · 语音
- 场景:发送语音消息。
- 请求字段(C→S):通用四字段 + `voiceurl`(已上传音频地址) + `time`(时长) + `info`(可选) + `type`(可选,有 info 时为 2)。发送处 `05_Func.js:1714`、`05_Func.js:2984`。
> 语音需先上传得到 `voiceurl` 再随包发送,客户端不直接传音频二进制。
- 推送字段(S→C):`type`(0=普通 / 2=机器人), `seat`(发送座位), `voiceurl`, `time`, `info`(type==2 时)。
- 源码:发送 `09_Net.js:492`(`Send_send_voice`)/接收 `09_Net.js:495` → `07_Desk.js:1073`(`Desk.send_voice`)。
- 备注:—
### play_voice · route=room · S→C 推送(⚠️)
- 场景:与 `send_voice` 成对,疑为语音**播放**推送。
- 请求字段(C→S):无(`send_voice` 上行,`play_voice` 疑为下行播放推送)。
- 推送字段(S→C):未知。
- 源码:常量 `02_Const.js:36` 与 `02_Const.js:54`(重复定义 `RpcList.play_voice`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**(注:`GameUI.play_voice` 是本地播放方法,非网络处理器)。
- 备注:⚠️ `02_Const.js` 已定义但客户端无网络处理函数;服务器是否下发 `play_voice` 待后端确认。
### send_gift · route=room · C→S 请求 + S→C 推送 · 互动/送礼
- 场景:向指定座位送互动礼物。
- 请求字段(C→S):通用四字段 + `giftid`(=`spid_up - 255`,按钮精灵号算出,发送处 `11_GameUI.js:2725`) + `receiveseat`(=`GameData.InteractPlayer`) + `info`(可选) + `type`(有 info 时为 2)。
- 推送字段(S→C):`type`(0=普通 / 2=机器人), `giftid`(客户端做 `(giftid-1)%4+1` 归一为 1~4 动画), `sendseat`(发送座位), `receiveseat`(接收座位), `info`(type==2 时)。
- 源码:发送 `09_Net.js:484`(`Send_send_gift`)/接收 `09_Net.js:487` → `07_Desk.js:1060`(`Desk.send_gift`)。
- 备注:—
### other_send_gift · route=room · S→C 推送(⚠️)
- 场景:与 `send_gift` 成对,疑为他人送礼推送。
- 请求字段(C→S):无。
- 推送字段(S→C):未知(疑似同 `send_gift` 推送结构)。
- 源码:常量 `02_Const.js:52`(`RpcList.other_send_gift`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器送礼推送用 `send_gift` 还是 `other_send_gift` 待后端确认。
### send_phiz · route=room · C→S 请求 + S→C 推送 · 表情
- 场景:发送表情动画。
- 请求字段(C→S):通用四字段 + `text`(=`up_id`,表情按钮序号 1~`ConstVal.Emotion.count`,发送处 `11_GameUI.js:815`) + `info`(可选) + `type`(有 info 时为 2)。
- 推送字段(S→C):`type`(0=普通 / 2=机器人), `text`(表情 ID,客户端按 `(text-1)%ConstVal.Emotion.src_list.length+1` 归一), `seat`(发送座位), `info`(type==2 时)。
- 源码:发送 `09_Net.js:539`(`Send_send_phiz`)/接收 `09_Net.js:543` → `07_Desk.js:1086`(`Desk.send_phiz`)。
- 备注:—
### call_phone · route=room · C→S 请求 + S→C 推送 · 拨打电话
- 场景:拨打/接听电话,置玩家 `onstate=2`(通话中)。
- 请求字段(C→S):仅通用四字段(发送处 `06_Player.js:380` / `:388` / `:396`,对应接起/电话进来/去电三种触发)。
- 推送字段(S→C):rpc=`call_phone`,`seat`(拨打者座位 → 该玩家 `onstate=2`)。
- 源码:发送 `09_Net.js:452`(`Send_call_phone`)/接收 `09_Net.js:456` → `07_Desk.js:967`(`Desk.call_phone`)。
- 备注:—
### other_callphone · route=room · S→C 推送(⚠️)
- 场景:与 `call_phone` 成对,疑为他人拨打电话推送。
- 请求字段(C→S):无。
- 推送字段(S→C):未知(疑似含 `seat`)。
- 源码:常量 `02_Const.js:45`(`RpcList.other_callphone`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器是否用 `other_callphone` 单独推送他人拨号待后端确认(当前客户端用 `call_phone` 推送统一处理本人与他人)。
### hangup_phone · route=room · C→S 请求 + S→C 推送 · 挂断电话
- 场景:挂断电话,置玩家 `onstate=0`。
- 请求字段(C→S):仅通用四字段(发送处 `06_Player.js:372`)。
- 推送字段(S→C):rpc=`hangup_phone`,`seat`(挂断者座位 → 该玩家 `onstate=0`)。
- 源码:发送 `09_Net.js:461`(`Send_hangup_phone`)/接收 `09_Net.js:465` → `07_Desk.js:977`(`Desk.hangup_phone`)。
- 备注:—
### other_hangup · route=room · S→C 推送(⚠️)
- 场景:与 `hangup_phone` 成对,疑为他人挂断电话推送。
- 请求字段(C→S):无。
- 推送字段(S→C):未知(疑似含 `seat`)。
- 源码:常量 `02_Const.js:47`(`RpcList.other_hangup`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。
- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器是否用 `other_hangup` 单独推送待后端确认。
---
## 房内可收的其它推送(接收语义在 room 场景,发送走 agent 路由)
> 以下推送在房间内被消费,但其上行请求实际走 `agent` 路由,详细发送定义见 [02 · agent 路由](./02-协议-agent路由.md);此处仅记录房内接收语义,避免遗漏。
### update_bean · S→C 推送(房内他人充值场景)
- 场景:房间内**他人**充值豆豆/金币,刷新该座余额。
- 推送字段(S→C):`seat`(充值者座位;无 `seat` 则为自己充值,走 `C_Player.update_bean`), `bean`(新余额), `type`(==6 时播放金币音效 `Logic.playCoinMp3()`);另含 `change`(存在时为大厅购买分支,不在房内座位场景)。
- 源码:接收 `09_Net.js:598`(`Net.update_bean`) → `07_Desk.js:1201`(`Desk.update_bean`)。
- 备注:与 [02 章的 update_bean](./02-协议-agent路由.md) 为**同名推送**,此处侧重"房间内座位刷新"分支(`seat` 已定义时)。
### update_charm · S→C 推送(房内座位魅力更新)
- 场景:批量更新房内座位魅力值。
- 推送字段(S→C):`seatlist`:[ { `seat`, `charm` }, ... ],逐项写入对应座位 `Player.setCharm`。
- 源码:接收 `09_Net.js:731`(`Net.update_charm`) → `07_Desk.js:1260`(`Desk.update_charm`)。
- 备注:⚠️ 交叉引用——**发送走 agent 路由**(`09_Net.js:727` `Send_update_charm` → `RouteList.agent`),归档于 02 章;本篇仅记录房内接收。
### broadcast · S→C 推送(房内跑马灯 / 弹窗)
- 场景:房内可收到的全局广播(跑马灯或顶部弹窗)。
- 推送字段(S→C):`msgtype`(0=顶部即时弹窗 `ShowiMessage` / 1=跑马灯 `addBroadcast`), `msgcontent`(内容文本)。
- 源码:接收 `09_Net.js:534`(`Net.broadcast`) → `07_Desk.js:1112`(`Desk.broadcast`)。
- 备注:交叉引用——**发送走 agent 路由**(`09_Net.js:530` `Send_broadcast` → `RouteList.agent`),归档于 02 章;本篇仅记录房内接收。
---
## 服务器切换(room 侧)
### connect_roomserver · route=room · C→S 请求 + S→C 推送 · 切到房间服
- 场景:从大厅服切换到房间服务器。
- 请求字段(C→S):由 `Send_connect_roomserver` 发起(走 room 路由)。
- 推送字段(S→C):`data.roomserver`(新房间服地址)。处理:置 `GameData.ConnectType=true`、`GameData.ConnectRpc=connect_roomserver`、`GameData.Server=data.roomserver`,关闭当前连接并用新地址重连。
- 源码:发送 `09_Net.js:500`(`Send_connect_roomserver`)/接收 `09_Net.js:503`(`Net.connect_roomserver`)。
- 备注:—
### connect_agentserver · S→C 推送(切回大厅服;上行走 agent 路由)
- 场景:解散/退房后从房间服切回大厅服(agent)。
- 推送字段(S→C):`data.opt`(切服原因,值为 `other_break_room` 或 `free_room`;命中其一时 `GameUI.StartLoad()`), `data.agentserver`(新大厅服地址)。处理:置 `GameData.ConnectRpc=connect_agentserver`、`GameData.Server=data.agentserver`,关连接重连。
- 源码:接收 `09_Net.js:518`(`Net.connect_agentserver`);上行 `09_Net.js:515`(`Send_connect_agentserver` → `RouteList.agent`)。
- 备注:交叉引用——**上行请求走 agent 路由**,归档于 02 章;本篇记录其作为 `other_break_room`/`free_room` 后续切服推送的语义(原文档"服务器切换"仅写了 `connect_roomserver`,此处补全 `connect_agentserver`)。