Files
node_server/FORWARD_IMPLEMENTATION_PLAN.md
T
2026-10-22 17:07:08 +08:00

41 KiB
Raw Blame History

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. 推荐端到端时序

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

type ForwardAck<T> =
    | {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:

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<ForwardBotReady> 成功表示 Node 已入房、业务请求处理器已安装、上下文观察已建立;不要求存在 BotContext,也不代表 Minecraft 在线。

4.5 入房及续期

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<JoinRoomReady>。作用域全部从受保护 token 取得,不相信客户端另传的房间名、角色和 Bot ID。

事件 reauth 接收 {token: string},返回同一作用域的更新凭证结果。续期不得改变角色、Bot、Node、会话或 Web grant_id;变更这些字段必须重新建立授权。

4.6 Token 声明

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

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 快照及事件

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

建议暴露:

get_bot_context(bot_id: number): bot_instance_context<any> | 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 管理器接口

ensure_connected(command: ForwardBotCommand): Promise<ForwardBotReady>;
release_session(session_id: string, reason: string): Promise<void>;
on_main_registered(node_session_id: string): Promise<void>;
on_main_disconnected(): void;
close_all(): Promise<void>;

按 (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:

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. 执行依赖与分工顺序

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 的工作指令

以 FORWARD_IMPLEMENTATION_PLAN.md 为协议和任务边界来源。
只实现分配给你的任务及文件,不顺手重构其他模块。
先核对当前源码,保留已有用户修改。
共享 forward_server 路径可能为别名,先确认真实文件归属。
不自行更改事件名、字段、时间单位、角色、错误码和权限语义。
不新增根目录共享协议包,不跨项目引用协议源码;在自己负责的项目内定义必要类型和校验。
契约有问题时给出具体冲突,由协调 Agent 更新本文,再由受影响 owner 同步本地实现。
所有异步操作考虑超时、取消、旧会话和回收。
提供真实验收证据,不以 Socket connect 代替入房成功。
不得把转发重连实现成重新启动 Bot。
完成后报告:修改文件、接口、测试、依赖、未完成项和联调注意事项。
不要提交 Git commit,除非用户另有明确要求。

协调 Agent 负责协议冻结、文件所有权、集成顺序和最终验证;各 Agent 的局部测试通过不自动代表端到端交付完成。