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

796 lines
41 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.
# 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<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`:
```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<ForwardBotReady>` 成功表示 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<JoinRoomReady>`。作用域全部从受保护 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<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 管理器接口
```ts
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:
```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 的局部测试通过不自动代表端到端交付完成。