Skip to content

Commit 749f819

Browse files
committed
feat(node-management): implement node management functionality for frp-bridge
- Add NodeManager class for server-side management of client nodes, handling registration, heartbeat updates, and queries. - Introduce ClientNodeCollector for client-side information collection and heartbeat reporting. - Implement FileNodeStorage for persistent storage of node information. - Define TypeScript types for node management, including NodeInfo, NodeRegisterPayload, NodeHeartbeatPayload, NodeListQuery, and NodeStatistics. - Enhance FrpBridge class to support node management commands and queries. - Update index files to export new classes and types related to node management.
1 parent 0c24c7d commit 749f819

9 files changed

Lines changed: 1322 additions & 7 deletions

File tree

ai-docs/node-management-design.md

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
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

Comments
 (0)