# Bot 转发通道完整实现计划 本文用于把工作分配给多个 Agent 分步实现。本文是设计及任务约束,不代表功能已经实现。 工作区根目录:`/home/wupeng/Project/mf_v3`。 ## 1. 目标与边界 用户在 Web 请求连接某个 Bot 的转发通道,主服务器鉴权并确认 Node 在线后,命令 Node 连接转发服务器。Node 成功入房并准备好请求处理后,主服务器返回 Web 连接地址与 Web 专用凭证。Web 经转发服务器查询状态、订阅事件及执行白名单交互。 必须满足: - 任意 `bot_status` 均允许申请转发通道,包括 `offline`、`ready`、`connecting`、`online`、`disconnecting`、`reconnecting`。 - 不存在 BotContext 也允许建立通道,查询结果为 `offline`、`context_exists: false`。 - Node 必须是主服务器当前已认证、在线且能接收命令的节点会话。 - 建立或恢复转发连接不得调用 `start_bot_context()`,不得重置 Minecraft 自动重连次数。 - `stop_bot` 只销毁 BotContext,转发通道保持,前端收到 `offline`。 - 再次启动 Bot 后,通道自动绑定新 BotContext,无需重新连接 Web Socket。 - Node 和 Web 使用不同角色凭证;Web 不能伪装 Node,不能向其他 Web 广播。 - 命令响应只返回请求方;只允许白名单事件和业务方法。 - 每一个连接、入房、请求及关闭过程都有超时、取消及身份检查。 - 本次实现统一升级协议,不增加旧协议兼容分支。 - Bot 游戏状态、Node 在线状态、转发连接状态分别维护,禁止互相覆盖。 首版转发支持:快照、日志读取、事件订阅、取消订阅、在线聊天。启动、停止、配置修改继续走主服务器现有接口,不在转发层复制一套生命周期入口。 ## 2. 当前源码问题与处理归属 以下是设计时确认的源码问题,实施前应重新核对文件现状和行号。 | 问题 | 位置 | 归属任务 | | --- | --- | --- | | handler 参数 `socket` 与局部变量重复,缺少 `io` 导入,动态加载可能导致 Node 启动失败 | `node_server/event_handler/bot/forward_bot.ts` | D | | 分发器忽略所有 `forward_` 开头事件,导致 `forward_bot` 被静默丢弃 | `node_server/event_handler/init.ts` | D | | HTTP 转发路由缺少开头 `/`,校验后没有响应及命令流程 | `main_server/router/node/action.ts` | E | | 转发房间不区分 Node/Web,任意事件广播给所有其他成员 | `forward_server/index.ts` | B | | 将原始 ACK 函数传给房间广播,实际变成广播 ACK,语义和超时错误 | `forward_server/index.ts` | B | | token 只有 `node_id`、`bot_id`,没有角色、有效期及会话作用域 | `forward_server/types.ts` 和 token 工具 | A、B、E | | 把转发服务启动 ID 当作执行 Node ID,内置服务 `-1` 与实际节点不匹配 | `forward_server/index.ts`、`main_server/index.ts` | B、G | | 重复加入新房间后没有离开旧房间 | `forward_server/index.ts` | B | | 服务单例并发初始化、监听端口错误、关闭清理和单例复位不完整 | `forward_server/index.ts` | G | | 转发服务注册表不清理死连接,重复注册可能断开共享 Node 控制 Socket | `main_server/runtime/forward_server/socket.ts` | G | | BotContext 注册表缺少查询、创建、移除通知;直接绑定一个 Bot 实例无法跨重建保持 | `node_server/bot/instance/index.ts` | C | | `BotContext.onAny()` 没有对应解绑入口;原始事件可能包含完整 Bot 对象 | `node_server/bot/instance/bot_instance_init.ts` | C、D | | 跨域、公开地址、TLS 和反向代理未形成明确配置 | 转发服务启动配置 | G、F | AES-GCM 解密已有完整性校验,不应把问题误写成“token 没有验签”。问题是缺少声明验证、角色隔离、到期和撤销机制。没有证据说明当前公开 HTTP DTO 已泄漏转发密钥,新接口仍必须显式投影公开字段。 ## 3. 推荐端到端时序 ```mermaid sequenceDiagram participant W as Web participant M as Main participant N as Node participant F as Forward W->>M: POST /api/user/node/forward_bot {bot_id} M->>M: 用户/Bot归属/节点与转发权限/当前Node会话 M->>F: forward_open_session F-->>M: 控制ACK M->>N: forward_bot(作用域、地址、Node凭证、截止时间) N->>F: Socket.IO connect + join_room(node) F-->>N: join ACK N->>N: 注册请求处理及Bot上下文订阅 N-->>M: joined:true ACK M->>M: 再次核对当前Node会话及Bot归属/分配 M-->>W: 公开URL/path/namespace + Web凭证 W->>F: connect + join_room(web) F-->>W: join ACK + node_ready W->>F: relay_request(bot.snapshot) F->>N: relay_request(内部关联ID) N-->>F: 定向ACK F-->>W: 定向ACK N-->>F: bot_event F-->>W: 已授权且已订阅的Web ``` `forward_open_session` 由已经认证的主服务器与转发服务控制通道发送。不能由 Web Socket 调用。 ## 4. 任务 A:协议约定与各项目本地实现清单 ### 4.1 文件所有权 不新增根目录共享协议目录或公共包,也不要求各项目跨目录引用类型、校验器或 token 工具。每个项目使用自己的类型、工具和测试目录,沿用现有结构。 本文件是多 Agent 协作时的通信协议约定,不是运行时依赖。相同协议类型允许在各项目本地定义,但事件名、字段、错误码、时间单位和验证行为必须一致。 建议落点及文件所有权: | 项目 | 本地类型与校验 | 凭证工具 | 实现 owner | | --- | --- | --- | --- | | `forward_server` | 沿用 `types.ts`,校验与常量放本项目工具目录 | 沿用 `utils/token_utils.ts`,负责验证 | B;启动校验由 G 接入 | | `node_server` | 在现有 `types/`、`utils/` 中定义节点命令、ACK 和事件 DTO | 节点转发客户端只使用 Main 下发的 token,不为远程转发服务签发凭证 | D;C 提供观察事件接口 | | `main_server` | 在现有 `types/`、`utils/` 中定义控制协议和 HTTP DTO | 沿用 `utils/token_utils.ts`,负责签发 | E;控制接入由 G 负责 | | `main_server_web` | 在现有类型/API目录中定义浏览器所需类型和响应检查 | 不包含密钥、签发或服务端解密工具 | F | A 只整理并冻结文档中的协议、字段归属、校验规则及测试向量,不提前创建或占用 B/D/E/F 负责的生产文件。各 Agent 只实现本项目实际使用的部分,不必复制无关服务端类型。 不得为此引入跨项目源码别名、新 workspace 包、代码生成或改变项目构建结构。协议校验不依赖数据库、BotContext 或 UI 框架;服务端密钥工具禁止进入前端 bundle。 协议变更先更新本文件并通知受影响的 owner,再分别同步各项目本地实现。H 通过共同的合法/非法消息样例、token 测试向量及跨服务联调验证一致性,不能仅凭本地 TypeScript 类型检查判断网络协议兼容。 ### 4.2 身份与时间约定 | 字段 | 定义 | | --- | --- | | `forward_id` | 转发服务 ID,非负安全整数,内置服务为 `0` | | `node_id`、`bot_id`、`user_id` | 正安全整数 | | `node_session_id` | Main 当前认证的 Node 会话标识,不由浏览器提供 | | `forward_session_id` | Main 分配的转发通道 UUID,独立于 Socket.IO `socket.id` | | `grant_id` | 单个浏览器授权的 UUID,不同标签页独立 | | `request_id` | 一次业务请求的 UUID | | `context_generation` | 每次 BotContext 创建的新 UUID;无上下文时为 `null` | | `stream_id` | 节点事件流 UUID;Node 重启或流重建时改变 | | `seq` | 同一 `stream_id` 内递增的安全整数 | | `*_at`、`*_before` | Unix 毫秒时间戳 | Bot 状态类型不包含 `disposed`。销毁是内部终止标记,公开状态为 `offline`。 ### 4.3 Socket ACK ```ts type ForwardAck = | {status: 'success'; data: T} | {status: 'error'; data: { code: ForwardErrorCode; message: string; retryable: boolean; }}; type ForwardErrorCode = | 'INVALID_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NODE_OFFLINE' | 'RELAY_UNAVAILABLE' | 'SESSION_STALE' | 'TOKEN_EXPIRED' | 'TIMEOUT' | 'CANCELLED' | 'NO_CONTEXT' | 'BOT_NOT_ONLINE' | 'RATE_LIMITED' | 'RESYNC_REQUIRED' | 'INTERNAL_ERROR'; ``` 转发业务统一采用这个 ACK;现有 `register_node` 等控制协议不在 A 中整体重写。HTTP 层沿用项目响应包装,并显式映射错误,不调用只适配旧数字 `data` 的辅助函数来解析新 ACK。 ### 4.4 Main → Node 命令 事件 `forward_bot`: ```ts interface ForwardBotCommand { request_id: string; forward_session_id: string; node_session_id: string; forward_id: number; node_id: number; bot_id: number; user_id: number; deadline_at: number; relay: { url: string; path: string; namespace: string; token: string; }; } interface ForwardBotReady { request_id: string; forward_session_id: string; node_session_id: string; forward_id: number; node_id: number; bot_id: number; joined: true; } ``` `ForwardAck` 成功表示 Node 已入房、业务请求处理器已安装、上下文观察已建立;不要求存在 BotContext,也不代表 Minecraft 在线。 ### 4.5 入房及续期 ```ts interface JoinRoomRequest {token: string} interface JoinRoomReady { forward_session_id: string; forward_id: number; node_id: number; bot_id: number; role: 'node' | 'web'; node_ready: boolean; expires_at: number; } ``` 事件 `join_room` 使用 `ForwardAck`。作用域全部从受保护 token 取得,不相信客户端另传的房间名、角色和 Bot ID。 事件 `reauth` 接收 `{token: string}`,返回同一作用域的更新凭证结果。续期不得改变角色、Bot、Node、会话或 Web `grant_id`;变更这些字段必须重新建立授权。 ### 4.6 Token 声明 ```ts interface RelayClaims { v: 1; forward_id: number; forward_session_id: string; node_session_id: string; node_id: number; bot_id: number; role: 'node' | 'web'; user_id?: number; grant_id?: string; permissions: RelayMethod[]; issued_at: number; join_before: number; expires_at: number; jti: string; } ``` 默认策略:新 token 在 60 秒内可用于入房或 reauth,连接授权最长 5 分钟,到期前 60 秒续期。时间参数集中定义,可配置;所有端使用相同毫秒单位,允许有限时钟偏差。 验证顺序:大小与格式 → GCM 完整性 → JSON 字段 → 版本/服务 audience → 时间 → 控制通道登记的有效会话 → 角色/权限/浏览器 grant。 Node token 不带 Web grant;Web token 必须带 `user_id`、`grant_id`。Main 与指定转发服务共享服务端密钥,密钥不得进入 Node 命令、公开响应或日志;Node 只获得受限 token,不获得转发密钥。Node 自己提供内置转发服务时,只能持有它本来配置的本地服务密钥,不能借此取得其他转发服务密钥。Node 不能仅通过解码把 token 当成已验证身份,最终有效性由转发服务 ACK 确认。 ### 4.7 业务 RPC ```ts type RelayMethod = | 'bot.snapshot' | 'bot.logs' | 'bot.subscribe' | 'bot.unsubscribe' | 'bot.chat'; interface RelayRequest { request_id: string; method: RelayMethod; payload: unknown; } ``` Web 发送 `relay_request` 并等待 ACK。转发服务为它分配内部关联 ID,只对当前 Node Socket 发单目标 `emitWithAck`;Node 的响应由转发服务校验后返回原 Web ACK。禁止把 Web 的 ACK 函数传进房间广播。 每个内部关联记录绑定 Web Socket、grant、Node Socket 版本及超时。Web 的 `request_id` 仅在对应 grant 内唯一,不直接作为全局 Map 键。 `bot.subscribe` 参数为 `{events: BotEventName[]}`,整体替换当前 Web 的订阅集;重复请求幂等。`bot.unsubscribe` 删除给定事件,空数组不影响其他事件。订阅默认空,Web 入房后显式设置。 `bot.logs` 参数为 `{stream_id?: string, after_seq?: number, limit: number}`,限制 1 到 200;传 after_seq 时必须携带 stream_id。流已更换或缓存不覆盖请求区间时返回 `RESYNC_REQUIRED` 或明确 `truncated: true`,不得伪装完整记录。响应包含本次 stream ID、读取水位、记录及是否截断。 `bot.chat` 参数为 `{message: string}`,限制为非空字符串和可配置长度。Node 再次检查上下文及当前实例在线;不自动重放。 ### 4.8 快照及事件 ```ts interface BotSnapshot { bot_id: number; bot_status: BotStatus; context_exists: boolean; context_generation: string | null; stream_id: string; seq: number; captured_at: number; } type BotEventName = 'status_update' | 'log' | 'message' | 'bot_error'; interface BotEvent { forward_session_id: string; bot_id: number; context_generation: string | null; stream_id: string; seq: number; emitted_at: number; name: BotEventName; payload: unknown; } ``` `bot_event` 只允许从 Node 输入,转发给已授权、订阅相应事件的 Web。每种 `payload` 定义具体 DTO,不保留最终的任意 `unknown` 实现。 - 状态:`{status: BotStatus, context_exists: boolean}`。 - 日志:保留项目日志 ID、时间戳和样式 DTO,限制批次数及总字节数。 - 消息:文本、位置及必要的有界 JSON,禁止直接发送 ChatMessage 类实例。 - 错误:`{code?: string, message: string}`,生产环境不传完整堆栈和敏感配置。 `relay_status` 由转发服务发给 Web,包含 `node_ready` 和原因,不改变 `bot_status`。节点通道未准备好时可允许 Web 入房,但业务请求返回 `NODE_OFFLINE`,不能返回虚假成功。 ### 4.9 控制通道事件 Main 与转发服务器的已认证控制通道支持: - `forward_open_session`:登记/更新作用域、当前 Node 会话及租约,严格幂等。 - `forward_authorize_grant`:登记/延长浏览器 grant、用户身份、权限和有效期,必须在签发 Web token 并返回前完成。 - `forward_revoke_session`:关闭指定转发会话,撤销 Node/Web,失败所有等待请求。 - `forward_revoke_grant`:撤销指定浏览器授权,不关闭其他浏览器。 - `forward_session_status`:转发服务上报会话就绪/失联信息,绑定当前控制会话。 控制 payload 和 ACK 类型在 A 中一并冻结。open/renew 不得覆盖不同 Node 会话的旧作用域而不先撤销旧会话。 会话租约上限不得由数据面自行延长;Main 根据仍有效的 grant 续租。转发服务重启丢失内存授权后,Main 先重新登记 session/grant,再允许 Node/Web 恢复入房。 ### 4.10 A 的验收 - 文档明确非法 ID、错误角色、过期 token、错误 audience、缺字段及超大输入的预期结果。 - 提供无生产密钥的固定 token 测试向量和篡改/跨作用域失败样例,由 B/E 在各项目测试中分别验证。 - 浏览器 bundle 不包含密钥及 Node token 工具。 - 所有 Agent 获得事件名、payload、错误码、时间单位与版本的固定清单。 - 不新增根目录共享协议包,不引入跨项目源码依赖;本地类型和校验的 owner 已明确。 ## 5. 任务 B:实现转发服务器的数据路由与鉴权 ### 5.1 文件所有权与依赖 依赖 A;与 G 在接口冻结后并行。 B 独占 `forward_server` 新增的 room/session/router/auth 模块及相关测试。B 不直接改启动入口;向 G 提供 `install_relay_routes(io, session_store)` 和对应清理函数,由 G 接入。 已有 `types.ts` 的旧声明由 B 按 A 冻结的文档约定更新为本项目本地类型;不得保留两套含义不同的 token 类型,也不从其他项目导入协议实现。 ### 5.2 会话存储 按 `forward_session_id` 存储: - 固定 `forward_id / node_id / bot_id / node_session_id`。 - 当前有效 Node Socket 及本地连接 generation。 - Web Socket 与 grant 的映射、订阅集。 - 会话及每个 Socket 的授权截止时间。 - 有界 pending RPC 表。 - 首个 Web 到达前、最后 Web 离开后的回收定时器。 房间名由服务端生成,建议包含 Node、Bot、会话;禁止客户端任意指定 room。可以使用独立 Node/Web 子房间,但 RPC 必须仍按单 Socket 定向。 ### 5.3 入房 1. 连接后启动 10 秒未鉴权截止定时器。 2. 要求有效 ACK 回调,校验 token 和登记会话。 3. 同一 Socket 只能绑定一个不可变作用域;相同入房请求幂等返回,不累积其他房间。 4. Node 同作用域最多一条有效上行连接。新连接入房成功后才替换旧连接,并递增 generation。 5. 旧连接断开回调只能清理自己拥有的记录,不能清空新连接。 6. Web grant 和权限写入服务端 `socket.data`,不相信每次消息中的身份字段。 7. `await socket.join()` 成功后才 ACK。 8. 成功或断开时清理未鉴权定时器。 Node 尚未 ready 时 Web 可入房并收到 `node_ready: false`;HTTP 初次建立仍必须先等 Node ready。此设计处理 HTTP 返回后到 Web 入房之间 Node 又掉线的竞态。 ### 5.4 RPC 路由 1. Web 必须已鉴权,检查 method 权限和对应 payload。 2. `bot.subscribe/unsubscribe` 在转发服务处理,仅修改本 Web 订阅集。 3. 其他 method 选择该会话唯一当前 Node Socket。 4. 分配内部关联 ID,保存请求方和 Node generation。 5. 对 Node 单目标调用,设置明确 timeout;Node 必须返回完整 `ForwardAck`。 6. 校验当前 Node 所有权仍一致后,把结果回给原 Web,ACK 只调用一次。 7. 超时、Web 离开、Node 替换、会话撤销时清理记录,拒绝迟到结果。 默认 RPC timeout 5 秒;限制每个 Web 的 pending 数量及速率。请求 ID 去重缓存只保留有界窗口,不能宣称跨进程“恰好一次”。有副作用请求断线后不得自动重发。 ### 5.5 事件推送 - 只接受当前 Node Socket 的 `bot_event`。 - 校验 Bot、会话、事件白名单、大小和单流递增序号。 - 发给匹配订阅及权限的 Web,不广播给 Node 或其他房间。 - 非当前 Node、Web 伪造 `bot_event`、任意未知事件全部拒绝。 - 不使用 `error` 作为 Bot 业务事件名,避免与 Socket.IO 控制错误混淆。 ### 5.6 授权与回收 - token 到期计时器真正断开已有 Socket;不能只在入房时检查。 - `reauth` 仅允许延长同一作用域与 grant 的授权,替换到期计时器。 - 会话撤销及 grant 撤销分别清理对应资源。 - 初次 Node ready 后 60 秒没有任何 Web 加入,回收未使用会话。 - 最后一个 Web 离开后保留 30 秒宽限期,再向 Node 通知释放并清理会话。 - 回收操作核对 generation,不能回收并发建立的新会话。 ### 5.7 B 的验收 - Web A 请求不会被 Web B 接收,响应只给 A。 - Web 无法伪造 Node、越权换 Bot、调用控制事件。 - 两个 Web 使用相同 request ID 不冲突。 - Node 替换后旧 ACK、旧事件和旧 disconnect 不影响新连接。 - 重复入房不累积房间;过期、断开及回收后无遗留 timer/pending。 - 默认同步 adapter 和模拟异步 `join` 均在真实入房后 ACK。 ## 6. 任务 C:节点 BotContext 注册表观察接口 ### 6.1 文件所有权与依赖 依赖 A,可与 B、G 并行。 C 独占: - `node_server/bot/instance/index.ts`。 - `node_server/bot/instance/bot_instance_init.ts` 中注册表观察所需的最小改动。 - 新增节点 Bot 快照/事件序列化模块。 - 对应测试。 不重写现有 Bot 生命周期、不修改 Minecraft 自动重连插件,不让 D 在这些文件重复实现另一套观察机制。 ### 6.2 注册表 API 建议暴露: ```ts get_bot_context(bot_id: number): bot_instance_context | undefined; observe_bot_context(bot_id: number, listener: ContextObserver): () => void; get_bot_snapshot(bot_id: number): BotSnapshot; ``` `observe_bot_context` 注册时应提供当前上下文快照,之后提供创建、替换、状态更新及移除通知;每次通知包含 generation。 明确线性化点:创建通知在上下文可查询且状态上报绑定完成后、实际连接前发出;移除通知在该代次最终 `offline` 可观察后发出。初始化失败的短暂上下文也必须解绑,不留下幽灵订阅。 可以返回一个订阅对象同时含初始快照和 dispose,避免“先查询后注册”之间漏事件。具体 API 由 C 提前冻结给 D,不得让调用方自己轮询私有 Map。 ### 6.3 事件桥 - 对稳定的 BotContext 订阅 `status_update/log/message/error`,不对某个 mineflayer Bot 长期持有引用。 - 使用明确事件名及 `off` 解绑;若使用 `onAny`,必须增加匹配的 `offAny` 或 disposer。 - 不把 `instance_created`、完整 Bot、插件状态对象、配置密钥发往转发层。 - 新上下文使用新 generation;旧代次回调立即失效。 - Node 级事件流维护 stream ID、seq 和有界日志缓存,BotContext 销毁不丢失最后 offline 事件。 - 旧日志明确标注历史 generation;状态事件不得用旧代次覆盖当前状态。 - 无上下文快照为 `offline/context_exists:false/context_generation:null`,不创建 Bot 来获取日志。 ### 6.4 C 的验收 - 先订阅后启动、先启动后订阅均收到一致快照和后续事件。 - stop/start 跨上下文替换后订阅自动转移。 - Minecraft 实例重建不重复绑定、不丢失稳定上下文事件。 - 解绑后监听器计数恢复;旧回调不修改新 generation。 - JSON 序列化不会遍历 Bot、循环对象或泄漏代理密码。 - snapshot 的 seq 与事件水位一致,可供 F 解决快照竞态。 ## 7. 任务 D:节点转发连接管理器与 forward_bot handler ### 7.1 文件所有权与依赖 依赖 A、C 的接口,以及 B 的入房/RPC 协议;允许先对协议桩开发。 D 独占: - `node_server/event_handler/bot/forward_bot.ts`。 - `node_server/event_handler/init.ts` 的精确分发规则。 - 新增 `node_server/runtime/forward/` 或等效独立管理目录。 - 节点转发 RPC 处理器与测试。 `node_server/index.ts` 由 G 独占,D 提供控制连接状态 hook,由 G 调用。不要在 handler 每次执行时创建不受管理的 Socket。 ### 7.2 管理器接口 ```ts ensure_connected(command: ForwardBotCommand): Promise; release_session(session_id: string, reason: string): Promise; on_main_registered(node_session_id: string): Promise; on_main_disconnected(): void; close_all(): Promise; ``` 按 `(forward_id,node_id,bot_id)` 去重,记录当前 forward session、连接代次、pending ensure、Socket、context disposer、定时器和租约。 会话状态:`idle / connecting / joining / ready / retry_wait / closed`,不得写入 `bot_status`。 ### 7.3 ensure_connected 1. 校验命令、URL scheme/path、当前 Node ID/会话和截止时间。 2. 来源必须是主服务器当前已注册控制连接;拒绝旧连接迟到命令。 3. 同作用域同会话 ready 时幂等返回;若 token 更新,先 reauth 成功再 ACK。 4. 同会话正在初始化时共享同一个建立任务,各请求保留自己的等待截止时间。 5. 新会话替代旧会话时取消旧任务、解绑旧观察、关闭旧 Socket。 6. 创建 Socket 使用 `autoConnect:false`,先安装所有监听器,再 connect。 7. 每次 `connect` 都发送 `join_room` 并等待 ACK。 8. 校验 ACK 作用域后安装/确认 RPC 和 C 的观察桥,标记 ready。 9. 向 Main 返回对应 request ID 的成功 ACK。 10. 失败路径标准化错误,只执行一次 ACK,清理仅由本会话拥有的资源。 多个并发申请共享通道,某一个调用者超时不能直接关闭仍被其他有效申请/授权使用的通道。请求截止与会话租约分别管理,控制租约过期才能回收孤立结果。 ### 7.4 Node RPC - `bot.snapshot`:从 C 获取一致快照,Bot 离线也成功。 - `bot.logs`:读取 C 的有界日志缓存;没有上下文时仍可返回已有历史或空列表。 - `bot.chat`:校验权限和字符串,临执行前核对当前上下文 generation、实例身份和 `online`;否则返回 `NO_CONTEXT/BOT_NOT_ONLINE`。 - 未知 method 拒绝,禁止根据字符串任意调用 context 方法。 - 业务事件通过 serializer 后使用 `bot_event` 发送。 - 主服务器断开期间不接受新的有副作用操作;已有只读订阅可在租约内继续。 ### 7.5 重连 - 转发连接重连独立于主服务器重连和 Minecraft 自动重连插件。 - 默认指数退避 1 秒到 30 秒,加入随机抖动,最多 8 次;参数集中配置。 - 只有入房 ACK 成功才重置次数,不以 TCP/Socket.IO connect 为准。 - 意外 `io server disconnect` 可重试;通过明确的撤销/过期/旧会话错误停止旧凭证重试。 - 每次重连必须重新入房;不依赖服务端恢复自动恢复权限。 - token 过期时等待 Main 下发新 token,不能使用旧凭证无限重试。 - 不缓存并重放 chat;状态恢复靠快照,日志按序补偿。 - 关闭/替换时先递增连接代次,使排队回调失效,再清理 timer、监听器、观察桥和 Socket。 ### 7.6 handler 与分发修复 - 主控制 Socket 命名为 `main_socket`,转发客户端为 `relay_socket`。 - 修复重复声明并导入正确的 `io`。 - 将广泛的 `startsWith('forward_')` 忽略改为明确的控制事件路由,确保 `forward_bot` 可执行。 - 转发数据 Socket 使用独立白名单 handler,不把它接进主服务器命令的全量 dispatcher。 - 保持 Node 启动时所有 handler 可正常导入。 ### 7.7 D 的验收 - 所有 Bot 状态及无上下文均能建立转发通道,不产生 Minecraft 连接动作。 - 两个并发 ensure 只创建一个 Socket,超时调用者不误关闭共享会话。 - 重连时重新入房但不重复绑定上下文事件。 - stop/start 只改变 Bot 快照,转发连接保持。 - 旧命令、旧 ACK、旧重试回调均不能污染新连接。 - 超时、租约到期、手动释放和进程关闭无监听器或定时器泄漏。 ## 8. 任务 E:主服务器 HTTP 编排、授权、续期与撤销 ### 8.1 文件所有权与依赖 依赖 A;B、D 协议冻结后可并行开发,最终集成依赖 G 的健康注册表。 E 独占: - `main_server/router/node/action.ts`。 - 新增主服务器转发业务 service、会话与 grant 存储。 - 相关权限 helper 的必要提取及测试。 - HTTP 续期和释放接口。 E 不直接修改 G 所有的转发注册表及启动入口,使用 G 暴露的健康连接及控制命令 API。 ### 8.2 初次申请 接口:`POST /api/user/node/forward_bot`,body 严格为 `{bot_id}`。 1. 检查用户会话、账号有效性和请求数据。 2. 用 `query_bot_info(user_id,bot_id)` 或等效逻辑检查 Bot 归属。 3. 检查对应 Node/团队/转发资源权限,但不检查 Bot 游戏状态、版本、代理可用性或启动队列。 4. 使用 `get_node_server_socket()` 获取当前可命令 Node,不仅看缓存 ONLINE。 5. 按 `(forward_id,node_id,bot_id,node_session_id)` 查找/创建通道,合并建立任务。 6. 分配浏览器独立 grant,不让不同标签页互相覆盖。 7. 经 G 的控制通道调用 `forward_open_session`。 8. 签发 Node token,发送 `forward_bot`,等待完整 ready ACK。 9. ACK 前后核对相同 Node 当前会话;再次检查 Bot 归属、节点/转发分配及配置版本。 10. 经控制通道登记该浏览器 grant,成功后签发 Web token,构造公开 DTO 返回。 不得把资源授权 helper 返回的完整实体序列化给浏览器,其中可能包含转发密钥。 公开 DTO: ```ts interface ForwardAccess { bot_id: number; node_id: number; forward_id: number; forward_session_id: string; grant_id: string; url: string; // 浏览器可访问的公开地址 path: string; namespace: string; token: string; // 仅 Web 角色 expires_at: number; } ``` ### 8.3 续期与释放 - `POST /api/user/node/forward_bot/renew`,body `{bot_id,forward_session_id,grant_id}`。 - `POST /api/user/node/forward_bot/release`,body `{bot_id,forward_session_id,grant_id}`。 - 两者都验证登录会话、Bot 归属、grant 所有权及当前资源分配,不能仅凭 grant ID 操作。 - renew 先延长控制会话,再对 Node ensure/reauth 成功,登记更新后的 grant,最后签发 Web 新 token。 - renew 若发现 Node 会话/转发分配变化,返回 `SESSION_STALE`,前端重新初次申请。 - release 幂等撤销本 grant,立即通知转发层;不关闭其他 grant 使用的通道。 - 主服务器可依据有效 grant 延长 Node 会话租约,但不得绕过账号/资源复核。 - 浏览器异常关闭无法保证 release 到达,仍由租约和 B 的空闲回收兜底。 用户退出、权限撤销、Bot 删除、节点迁移、Node 控制会话替换时撤销对应 grant 或通道。首次实现只要求覆盖项目已有这些操作的入口,避免创建一个永不调用的撤销 API。 ### 8.4 超时、幂等与失败 - Node 建立总预算 8 秒,Main 等待 10 秒,HTTP 总预算 12 秒,前端约 15 秒。 - 所有等待包含排队时间,不逐阶段无限叠加 timeout。 - 在 HTTP 超时后不返回凭证。迟到成功只能保留在有效租约/其他申请仍使用的共享通道内。 - 不把转发建立加入持久化的离线 Bot 停止队列。 - Node 离线与转发不可用使用不同错误。 - 沿用当前 HTTP 响应 envelope,错误码映射由 E 明确写测试并向 F 发布。 - 进程重启不假装恢复内存会话:旧租约过期/撤销,Web 重新申请。 ### 8.5 E 的验收 - foreign Bot、无权限 Node、离线 Node 不能获取凭证。 - offline Bot 和无运行上下文不阻止申请。 - 非当前 Node 的成功 ACK 不被采纳。 - 等待期间 Bot 被迁移/删除或权限撤销不会返回可用旧凭证。 - 并发标签页共用上行但各有独立 grant,释放一个不影响另一个。 - 公开响应没有 Node token、密钥、内部地址或数据库实体泄漏。 ## 9. 任务 F:前端转发客户端与页面接入 ### 9.1 文件所有权与依赖 依赖 A、E 的 HTTP 约定及 B 的 Socket 协议。F 独占 `main_server_web` 中新增转发 API、连接管理模块、状态展示和相关测试。 不要把现有 Main Socket 和转发 Socket 混成同一个全局变量,也不要修改 Minecraft Bot 状态来表达转发断线。 ### 9.2 客户端生命周期 1. 用户打开 Bot 交互页或显式连接时申请 ForwardAccess。 2. 保存页面/账号/Bot 的请求 generation,丢弃迟到的旧响应。 3. 用返回的 URL/path/namespace 创建 Socket,先安装监听器。 4. connect 后 join_room,成功后才启用交互操作。 5. 设置事件订阅,再请求 snapshot,同步完成前缓冲实时事件。 6. 账号退出、切换 Bot、离开页面时取消 pending、移除监听器、关闭 Socket,尽力 release grant。 连接状态建议:`idle / requesting / connecting / joining / syncing / ready / retrying / expired / error`。 同页多个组件共用一个会话客户端,组件卸载采用引用计数,不各建一条 Socket。不同标签页使用独立 grant。 ### 9.3 快照与事件顺序 - 订阅后暂存事件,再请求快照,防止入房前首次状态丢失。 - 快照携带 stream ID 和 seq 水位,先应用快照,再应用同流中 seq 更大的缓冲事件。 - 流改变时丢弃旧流缓冲,重新同步,不能跨流比较 seq。 - context generation 改变时重置当前上下文视图,历史日志仍可按 generation 显示。 - seq 用于去重与快照水位,按事件订阅过滤后不保证连续,不能把自然跳号误判成丢包。由服务端显式 resync 通知、日志缓存截断或 `RESYNC_REQUIRED` 触发快照/日志重新同步。 - UI 不把 `node_ready:false` 显示成 Bot 已 offline,分别展示通道状态和 Bot 状态。 ### 9.4 重连与凭证 - 网络断线采用有限退避,connect 后必须重新 join。 - 凭证在 expires_at 前 60 秒 renew,成功后调用 reauth,不必断开整个 Socket。 - 过期、SESSION_STALE 时重新走 HTTP 初次申请,不使用旧 token 无限重试。 - 服务端明确撤销授权时停止自动重试并提示重新授权,不能无条件重连绕过撤销。 - snapshot、日志读取可显式重试;chat 不能在 ACK 超时后自动重发。 - 切换 Bot 后旧 ACK、旧事件、旧 HTTP 响应不能更新新页面。 ### 9.5 F 的验收 - Bot 离线页面可连接并展示 `context_exists:false`。 - 多标签页互不收到对方命令内容或私有响应。 - 停止/再次启动 Bot 后无需重连页面,即可显示新状态。 - 短断线、token 续期、主服务器重新分配转发地址均可恢复。 - 页面快速切换、重复挂载卸载不增加 Socket/监听器数量。 - 保持现有设计系统,桌面和移动端可用,不为此重做无关页面。 ## 10. 任务 G:服务启动、控制注册表与部署配置 ### 10.1 文件所有权与依赖 依赖 A,与 B/C 并行。G 是以下共享入口的唯一修改者: - `forward_server/index.ts` 和现有控制 runtime 入口。 - `main_server/index.ts`。 - `main_server/runtime/forward_server/socket.ts` 及控制注册 handler。 - `node_server/index.ts` 中与控制认证会话及进程关闭相关的集成。 - 所需配置、配置校验和生命周期测试。 注意:`node_server/forward_server`、`main_server/forward_server` 可能引用同一份共享源码。实施前确认实际路径,不把别名当成独立文件让不同 Agent 同时编辑。 ### 10.2 转发服务启动/关闭 - 使用共享初始化 Promise,避免 await 前检查单例造成并发启动。 - 等待 owned HTTP server 的 listening,处理端口占用与启动失败。 - 明确 HTTP server 所有权:内置服务借用 Main 的 HTTP server,停止转发功能不能意外关闭主站。 - 显式 stop API 清理 B 的会话、pending、监听器、定时器,再释放自有服务。 - 停止后复位单例和初始化状态,允许受控重启。 - 不依赖未保证存在的 Engine.IO server `close` 事件作为唯一清理入口。 - 启动参数明确为 forward ID,不继续使用 `-1` 充当执行 Node 身份。 - 密钥缺失时启动失败,取消已知字符串兜底。 ### 10.3 Main 注册表 - 同一当前连接重复注册同一转发服务幂等成功。 - 注册准备完成后才加入健康表并 ACK,初始化失败必须返回失败。 - 保存 cleanup disposer;断开时仅删除仍由该连接持有的记录。 - 健康查询不返回已断开的 Socket。 - origin 1 借用 Node 控制 Socket,移除转发登记不能直接 disconnect 整个 Node。 - origin 0 的 EventEmitter 不能调用不存在的 disconnect。 - 替代时间戳猜测,使用明确注册 generation/节点会话所有权。 - 暴露供 E 使用的当前转发健康查询、控制命令及撤销方法。 ### 10.4 控制链路恢复 - Main/Node 控制连接成功认证后,通知 D 当前 node_session_id。 - 主服务器短断线时,已有转发只读通道可存活到租约到期,禁止新有副作用请求。 - Main 重新认证产生新 Node 会话时,旧转发会话被替换/撤销,Web 重新申请。 - 内置转发使用进程内 EventEmitter 也必须遵守相同控制协议、ACK 和清理契约。 - 外部控制连接必须先认证,数据 Socket 不得调用控制事件。 Node 自带转发服务时,共用的 Main-Node 控制 Socket 需要同时路由 Node 命令和本地转发控制事件。G 与 D 采用明确事件表及独立 disposer,不得让修复 `forward_` 前缀过滤后,同一个控制事件被两套 handler 重复执行。Main 内置 EventEmitter 使用相同 request/ACK 适配,但不依赖 Socket.IO 才有的 timeout/disconnect 方法。 ### 10.5 地址和部署 每个转发服务配置分别包含: - `node_url`:Node 可访问地址。 - `public_url`:浏览器可访问地址。 - `path`、`namespace`。 - 明确允许的 Web Origin。 - TLS/反向代理配置及载荷限制。 HTTPS 页面必须使用可访问的 HTTPS/WSS 入口;Socket.IO 客户端传 HTTP(S) URL,不把 path 拼成错误 namespace。默认 polling 与 WebSocket 都应通过代理,不能以强制 WebSocket 掩盖未配置的跨域问题。 首版限定每个转发实例单进程。未来多进程需要共享唯一 Node 所有权、会话、pending 路由及 adapter;只加 Redis 房间广播不等于完成横向扩展。 ### 10.6 G 的验收 - 并发启动只创建一个服务,端口占用不返回成功。 - 关闭再启动可用,借用 Main HTTP server 时不误关主站。 - 重复注册不踢掉 Node;旧 disconnect 不删除新注册。 - 内置与外部转发使用相同作用域规则。 - Node 初始化过程中 `forward_bot` handler 可正常导入。 - 浏览器默认连接方式能通过配置的公开域名和代理连接。 ## 11. 任务 H:集成测试、故障注入与交付核对 ### 11.1 文件所有权 H 独占跨服务集成测试目录及测试运行说明。生产文件修改归原 owner,H 发现问题后给证据和复现,不跨范围修复同一文件。 建议使用可控 Main、真实 Socket.IO relay 和模拟 BotContext,避免依赖公网 Minecraft 服务。时间相关使用可控时钟,额外保留至少一组真实网络连接测试。 ### 11.2 必测矩阵 | 类别 | 场景 | 期望 | | --- | --- | --- | | Bot 状态 | 六种状态及无上下文 | 均允许申请,状态真实,不触发 start | | 权限 | 未登录、他人 Bot、无 Node/转发资源权限 | 拒绝,不签发有效凭证 | | Node | 离线、会话替换、等待期间断开 | 不返回旧连接成功 | | 角色 | Web 伪造 Node、伪造事件、控制消息 | 拒绝 | | 作用域 | 跨 Bot、跨 Node、跨 relay、旧 session token | 拒绝 | | 入房 | 重复、不同 room、异步 join | 幂等或明确拒绝,ACK 时已入房 | | RPC | 两 Web 相同请求 ID、超时、迟到 ACK | 不串响应,不泄漏 pending | | RPC | Node 在执行期间替换 | 旧响应失效,有副作用请求不自动重放 | | 生命周期 | Bot stop/start、实例 Minecraft 重连 | 通道保持,订阅跟随新上下文 | | 事件 | 首次状态、快照期间事件、序号缺口 | 一致同步,不让旧状态覆盖新状态 | | 重连 | Node-relay、Web-relay、Main-Node 分别断开 | 独立恢复,重新入房,无重复监听 | | 凭证 | 到期前续期、已连接后到期、撤销 | 正确续期或断开,不能永久授权 | | 并发 | 两标签页同时申请、一个超时/关闭 | 一条上行,各自授权,不误关共享连接 | | 回收 | 浏览器从未到达、最后浏览器离开、进程退出 | timer/Socket/listener/pending 回收 | | 部署 | 内置 relay、外部 relay、HTTPS、跨域 polling | 全流程可用 | | 容量 | 日志洪峰、慢客户端、恶意 pending | 限流/截断明确,内存有界 | ### 11.3 检查与交付 - 各服务类型检查通过,前端构建通过。 - 单元测试、Socket.IO 集成测试通过。 - 每个 Agent 列出修改文件、协议变化、执行的命令及未覆盖风险。 - 明确区分“transport connected”“joined”“node ready”“Bot online”。 - 不把 mock 通过描述成真实生产部署已验证。 - 验证全过程不输出密钥、token、代理密码或完整用户配置。 ## 12. 执行依赖与分工顺序 ```mermaid flowchart TD A[A 协议约定冻结] --> B[B 转发数据面] A --> C[C Bot注册表观察] A --> G[G 启动与控制注册] A --> E[E Main业务编排] C --> D[D Node转发管理] B --> D G --> E D --> E E --> F[F Web接入] B --> F B --> H[H 集成验收] C --> H D --> H E --> H F --> H G --> H ``` 推荐批次: 1. A 先冻结协议,明确全部类型、错误和事件;B/C/G 提前检查接口是否可实现。 2. B、C、G 并行实现;E/F 可按已冻结协议开发本地桩,不自行变更契约。 3. C 的观察 API 完成后 D 实现 Node 管理器;B/G 提供真实 relay 联调。 4. E 接通授权、会话及 HTTP 全流程;F 接入真实 API。 5. H 执行故障注入和完整验收,原 owner 修复各自模块。 不要让 E 和 G 同时修改主服务器启动/注册表,也不要让 C 和 D 同时修改 BotContext 生命周期。 ## 13. 可直接附给各 Agent 的工作指令 ```text 以 FORWARD_IMPLEMENTATION_PLAN.md 为协议和任务边界来源。 只实现分配给你的任务及文件,不顺手重构其他模块。 先核对当前源码,保留已有用户修改。 共享 forward_server 路径可能为别名,先确认真实文件归属。 不自行更改事件名、字段、时间单位、角色、错误码和权限语义。 不新增根目录共享协议包,不跨项目引用协议源码;在自己负责的项目内定义必要类型和校验。 契约有问题时给出具体冲突,由协调 Agent 更新本文,再由受影响 owner 同步本地实现。 所有异步操作考虑超时、取消、旧会话和回收。 提供真实验收证据,不以 Socket connect 代替入房成功。 不得把转发重连实现成重新启动 Bot。 完成后报告:修改文件、接口、测试、依赖、未完成项和联调注意事项。 不要提交 Git commit,除非用户另有明确要求。 ``` 协调 Agent 负责协议冻结、文件所有权、集成顺序和最终验证;各 Agent 的局部测试通过不自动代表端到端交付完成。