|
| 1 | +# Node Management 功能设计文档 |
| 2 | + |
| 3 | +## 概述 |
| 4 | + |
| 5 | +在 frp-bridge 项目中新增节点管理功能,支持 server 端对多个连接的 client 节点进行管理,client 端需要主动收集基础信息并上报给 server。 |
| 6 | + |
| 7 | +**设计目标:** |
| 8 | +- Server 模式:被动接收并管理已连接 client 的节点信息,仅提供查询 API |
| 9 | +- Client 模式:连接时主动上报节点信息,周期性心跳更新 |
| 10 | +- 类型安全:完整的 TypeScript 类型定义 |
| 11 | +- 遵循架构:Runtime Command/Query 模式 |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## FRP 原生能力分析 |
| 16 | + |
| 17 | +根据 frp 官方文档调查,**frps 原生不提供获取已连接 frpc 系统信息的直接 API**。 |
| 18 | + |
| 19 | +### frps 提供的能力 |
| 20 | + |
| 21 | +| 功能 | 包含内容 | 限制 | |
| 22 | +|------|--------|------| |
| 23 | +| **Dashboard** | 代理流量、连接数统计 | 不含客户端系统信息;API 不规范 | |
| 24 | +| **Prometheus** | 性能指标 (/metrics) | 同上;仅指标导向 | |
| 25 | +| **客户端信息** | **无法提供** | 需 Client 端主动上报 | |
| 26 | + |
| 27 | +### 推荐方案:被动节点发现 + 主动心跳更新 |
| 28 | + |
| 29 | +``` |
| 30 | +Client 端初始化: |
| 31 | + ├─ 收集系统信息 (IP、CPU、内存、OS、版本) |
| 32 | + └─ 连接时上报 → node.register 命令 (创建节点) |
| 33 | +
|
| 34 | +Client 端运行中: |
| 35 | + └─ 定期心跳 → node.heartbeat 命令 (更新状态和信息) |
| 36 | +
|
| 37 | +Server 端: |
| 38 | + ├─ 被动接收 register/heartbeat → 自动创建/更新节点 |
| 39 | + ├─ 集成 frps Dashboard API → 代理统计 (流量、连接数) |
| 40 | + └─ 节点只能查询,不支持人工增删改 |
| 41 | +``` |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## 类型定义 (`@frp-bridge/types`) |
| 46 | + |
| 47 | +### 核心类型 |
| 48 | + |
| 49 | +```typescript |
| 50 | +export interface NodeInfo { |
| 51 | + id: string // UUID |
| 52 | + name: string |
| 53 | + ip: string |
| 54 | + port: number |
| 55 | + protocol: 'tcp' | 'udp' |
| 56 | + serverAddr: string |
| 57 | + serverPort: number |
| 58 | + hostname?: string |
| 59 | + osType?: string // 'linux' | 'darwin' | 'win32' |
| 60 | + osRelease?: string |
| 61 | + platform?: string // 'x64' | 'arm64' |
| 62 | + cpuCores?: number |
| 63 | + memTotal?: number |
| 64 | + frpVersion?: string |
| 65 | + bridgeVersion?: string |
| 66 | + status: 'online' | 'offline' | 'connecting' | 'error' |
| 67 | + lastHeartbeat?: number |
| 68 | + connectedAt?: number |
| 69 | + labels?: Record<string, string> |
| 70 | + metadata?: Record<string, unknown> |
| 71 | + token?: string |
| 72 | + createdAt: number |
| 73 | + updatedAt: number |
| 74 | +} |
| 75 | + |
| 76 | +export interface NodeRegisterPayload { |
| 77 | + ip: string |
| 78 | + port: number |
| 79 | + serverAddr: string |
| 80 | + serverPort: number |
| 81 | + protocol: 'tcp' | 'udp' |
| 82 | + hostname: string |
| 83 | + osType: string |
| 84 | + osRelease: string |
| 85 | + platform: string |
| 86 | + cpuCores: number |
| 87 | + memTotal: number |
| 88 | + frpVersion: string |
| 89 | + bridgeVersion: string |
| 90 | + token?: string |
| 91 | +} |
| 92 | + |
| 93 | +export interface NodeHeartbeatPayload { |
| 94 | + nodeId: string |
| 95 | + status: 'online' | 'error' |
| 96 | + lastHeartbeat: number |
| 97 | + cpuCores?: number |
| 98 | + memTotal?: number |
| 99 | +} |
| 100 | + |
| 101 | +export interface NodeListQuery { |
| 102 | + page?: number |
| 103 | + pageSize?: number |
| 104 | + status?: NodeInfo['status'] |
| 105 | + labels?: Record<string, string> |
| 106 | + search?: string |
| 107 | +} |
| 108 | + |
| 109 | +export interface NodeListResponse { |
| 110 | + items: NodeInfo[] |
| 111 | + total: number |
| 112 | + page: number |
| 113 | + pageSize: number |
| 114 | + hasMore: boolean |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## Runtime 命令和查询 |
| 119 | + |
| 120 | +### 客户端命令 (Server 自动处理) |
| 121 | + |
| 122 | +| 命令 | 功能 | 事件 | 触发时机 | |
| 123 | +|------|------|------|--------| |
| 124 | +| `node.register` | Client 初始化时上报节点信息,Server 生成 UUID | `node:registered` | Client 启动 | |
| 125 | +| `node.heartbeat` | Client 周期性上报心跳和最新系统信息 | `node:heartbeat` | 每 30-60s | |
| 126 | +| `node.unregister` | Client 优雅关闭时通知 Server | `node:unregistered` | Client 退出 | |
| 127 | + |
| 128 | +### Server 端查询 (外部 API) |
| 129 | + |
| 130 | +| 查询 | 功能 | 返回 | |
| 131 | +|------|------|------| |
| 132 | +| `node.list` | 获取节点列表 (分页/过滤/搜索) | NodeListResponse | |
| 133 | +| `node.get` | 获取单个节点详情 | NodeInfo | |
| 134 | +| `node.stats` | 获取全局统计 (总数、在线数、离线数) | NodeStatistics | |
| 135 | + |
| 136 | +**说明**: Server 端不提供任何修改节点的 API,节点生命周期由 Client 驱动。 |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## NodeManager 类设计 |
| 141 | + |
| 142 | +位置: `packages/core/src/node/node-manager.ts` |
| 143 | + |
| 144 | +```typescript |
| 145 | +export class NodeManager { |
| 146 | + // 内部命令 (由 Runtime handler 调用,不外部暴露) |
| 147 | + private async registerNode(payload: NodeRegisterPayload): Promise<NodeInfo> |
| 148 | + private async updateHeartbeat(payload: NodeHeartbeatPayload): Promise<void> |
| 149 | + private async unregisterNode(nodeId: string): Promise<void> |
| 150 | + |
| 151 | + // 公开查询方法 |
| 152 | + async listNodes(query?: NodeListQuery): Promise<NodeListResponse> |
| 153 | + async getNode(id: string): Promise<NodeInfo> |
| 154 | + async getStatistics(): Promise<{ total: number, online: number, offline: number }> |
| 155 | + |
| 156 | + // 工具方法 |
| 157 | + hasNode(id: string): boolean |
| 158 | + getOnlineNodes(): NodeInfo[] |
| 159 | + getOfflineNodes(): NodeInfo[] |
| 160 | + getNodesByStatus(status: NodeInfo['status']): NodeInfo[] |
| 161 | + |
| 162 | + // 生命周期 |
| 163 | + async initialize(): Promise<void> |
| 164 | + async dispose(): Promise<void> |
| 165 | +} |
| 166 | + |
| 167 | +export interface NodeStorage { |
| 168 | + save: (node: NodeInfo) => Awaitable<void> |
| 169 | + delete: (id: string) => Awaitable<void> |
| 170 | + load: (id: string) => Awaitable<NodeInfo | undefined> |
| 171 | + list: () => Awaitable<NodeInfo[]> |
| 172 | +} |
| 173 | + |
| 174 | +export type NodeEvent = 'node:registered' | 'node:heartbeat' | 'node:unregistered' | 'node:statusChanged' |
| 175 | +``` |
| 176 | +
|
| 177 | +--- |
| 178 | +
|
| 179 | +## Client 端信息收集 |
| 180 | +
|
| 181 | +### ClientNodeCollector |
| 182 | +
|
| 183 | +位置: `packages/core/src/node/client-collector.ts` |
| 184 | +
|
| 185 | +**关键点**: frps 原生无法获取 frpc 系统信息,必须由 Client 端主动收集和上报。 |
| 186 | +
|
| 187 | +```typescript |
| 188 | +export class ClientNodeCollector { |
| 189 | + async collectNodeInfo(): Promise<Partial<NodeInfo>> |
| 190 | + startHeartbeat(interval?: number): void |
| 191 | + stopHeartbeat(): void |
| 192 | + async reportToServer(info: Partial<NodeInfo>): Promise<void> |
| 193 | +} |
| 194 | + |
| 195 | +export interface ClientCollectorOptions { |
| 196 | + heartbeatInterval?: number |
| 197 | + logger?: RuntimeLogger |
| 198 | + serverUrl?: string |
| 199 | + autoStart?: boolean |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +**收集的信息**: IP、端口、主机名、OS、CPU核数、内存、FRP/Bridge版本 |
| 204 | + |
| 205 | +**行为**: |
| 206 | +- 初始化时收集并调用 `node.register` (由 Server 自动分配 nodeId) |
| 207 | +- 每 30-60s 调用 `node.heartbeat` (可选更新系统信息) |
| 208 | +- 优雅关闭时调用 `node.unregister` |
| 209 | +- 断网/异常关闭时,Server 自动标记为离线 (心跳超时) |
| 210 | + |
| 211 | +### Client 配置扩展 |
| 212 | + |
| 213 | +```typescript |
| 214 | +export interface ClientConfig { |
| 215 | + serverAddr: string |
| 216 | + serverPort?: number |
| 217 | + node?: { |
| 218 | + heartbeatInterval?: number |
| 219 | + enableAutoReport?: boolean |
| 220 | + } |
| 221 | +} |
| 222 | +``` |
| 223 | + |
| 224 | +## FrpBridge 集成 |
| 225 | + |
| 226 | +```typescript |
| 227 | +export class FrpBridge { |
| 228 | + private readonly nodeManager?: NodeManager |
| 229 | + private readonly clientCollector?: ClientNodeCollector |
| 230 | + |
| 231 | + constructor(options: FrpBridgeOptions) { |
| 232 | + if (this.isServerMode) { |
| 233 | + this.nodeManager = new NodeManager(nodeStorage, nodeManagerOptions) |
| 234 | + } |
| 235 | + if (this.isClientMode) { |
| 236 | + this.clientCollector = new ClientNodeCollector(clientCollectorOptions) |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + getNodeManager(): NodeManager | undefined { |
| 241 | + return this.nodeManager |
| 242 | + } |
| 243 | + |
| 244 | + getClientCollector(): ClientNodeCollector | undefined { |
| 245 | + return this.clientCollector |
| 246 | + } |
| 247 | +} |
| 248 | + |
| 249 | +export interface FrpBridgeOptions { |
| 250 | + nodeStorage?: NodeStorage |
| 251 | + process?: FrpBridgeProcessOptions & { |
| 252 | + nodeHeartbeatInterval?: number // Client 端心跳间隔,默认 30s |
| 253 | + } |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +## 数据持久化 |
| 258 | + |
| 259 | +**文件结构**: |
| 260 | +``` |
| 261 | +~/.frp-bridge/runtime/nodes/ |
| 262 | +├── nodes.json |
| 263 | +└── node-{id}.json |
| 264 | +``` |
| 265 | + |
| 266 | +**FileNodeStorage 实现**: |
| 267 | +```typescript |
| 268 | +export class FileNodeStorage implements NodeStorage { |
| 269 | + async save(node: NodeInfo): Promise<void> |
| 270 | + async delete(id: string): Promise<void> |
| 271 | + async load(id: string): Promise<NodeInfo | undefined> |
| 272 | + async list(): Promise<NodeInfo[]> |
| 273 | +} |
| 274 | +``` |
| 275 | + |
| 276 | +--- |
| 277 | + |
| 278 | +## 事件和错误处理 |
| 279 | +### 事件类型 |
| 280 | + |
| 281 | +| 事件 | 触发 | 用途 | |
| 282 | +|------|------|------| |
| 283 | +| `node:registered` | Client 初次连接 | 节点注册 | |
| 284 | +| `node:heartbeat` | Client 心跳上报 | 状态和信息更新 | |
| 285 | +| `node:unregistered` | Client 主动断开 | 节点注销 | |
| 286 | +| `node:statusChanged` | 心跳超时或异常 | 状态变化 (online→offline) | | 心跳确认 | |
| 287 | +| `node:statusChanged` | (内部) | 状态变化 | |
| 288 | + |
| 289 | +### 错误处理 |
| 290 | + |
| 291 | +```typescript |
| 292 | +export type NodeErrorCode = |
| 293 | + | 'NODE_NOT_FOUND' // 节点不存在 |
| 294 | + | 'NODE_ALREADY_EXISTS' // 重复注册 (nodeId 冲突) |
| 295 | + | 'INVALID_NODE_DATA' // 数据验证失败 |
| 296 | + | 'HEARTBEAT_TIMEOUT' // 心跳超时 |
| 297 | + | 'STORAGE_ERROR' // 持久化错误 |
| 298 | +``` |
| 299 | +
|
| 300 | +**说明**: Client 侧异常(网络故障、进程崩溃)由 Server 的心跳超时检测处理,自动标记为离线。 |
| 301 | +
|
| 302 | +--- |
| 303 | +
|
| 304 | +## 导出结构 |
| 305 | +
|
| 306 | +```typescript |
| 307 | +// packages/core/src/node/index.ts |
| 308 | +export { ClientNodeCollector, FileNodeStorage, NodeError, NodeManager } |
| 309 | +export type { ClientCollectorOptions, NodeEvent, NodeManagerOptions, NodeStorage } |
| 310 | + |
| 311 | +// packages/types/src/index.ts (追加) |
| 312 | +export type { |
| 313 | + CreateNodePayload, |
| 314 | + NodeInfo, |
| 315 | + NodeListQuery, |
| 316 | + NodeListResponse, |
| 317 | + UpdateNodePayload |
| 318 | +} from './node' |
| 319 | +``` |
| 320 | + |
| 321 | +--- |
| 322 | + |
| 323 | +## 实现时序 |
| 324 | + |
| 325 | +| Phase | 任务 | |
| 326 | +|-------|------| |
| 327 | +| 1 | 类型定义 (NodeInfo、CreateNodePayload 等) | |
| 328 | +| 2 | NodeManager 核心 (CRUD、事件、存储接口) | |
| 329 | +| 3 | 持久化 (FileNodeStorage) | |
| 330 | +| 4 | Client 集成 (ClientNodeCollector) | |
| 331 | +| 5 | Bridge 集成 (命令/查询注册) | |
| 332 | + |
| 333 | +--- |
| 334 | + |
| 335 | +## 性能和安全 |
| 336 | + |
| 337 | +### 性能 |
| 338 | +- Map 存储 O(1) 查询 |
| 339 | +- 分页查询避免全量加载 |
| 340 | +- 异步 I/O 不阻塞事件循环 |
| 341 | +- 心跳间隔 30-60s 可配置 |
| 342 | + |
| 343 | +### 安全 |
| 344 | +- 节点创建时验证令牌 |
| 345 | +- Server 端验证操作权限 |
| 346 | +- 所有输入数据验证 |
| 347 | +- 敏感信息 (token) 可选加密 |
| 348 | + |
| 349 | +--- |
| 350 | + |
| 351 | +## 扩展方向 |
| 352 | + |
| 353 | +1. **Dashboard 代理数据集成**: 定期从 frps 拉取代理统计,映射到 NodeInfo |
| 354 | +2. **Prometheus 监控**: 集成 `/metrics` 端点到时间序列数据库 |
| 355 | +3. **节点分组和标签**: 灵活分类和检索机制 |
| 356 | +4. **告警机制**: 节点离线/异常状态告警 |
| 357 | +5. **Web Dashboard**: 可视化管理界面 |
| 358 | +6. **自适应心跳**: 根据网络状况动态调整心跳间隔 |
0 commit comments