@f2a/network 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/.github/workflows/ci.yml +113 -0
  2. package/.github/workflows/publish.yml +60 -0
  3. package/LICENSE +21 -0
  4. package/MONOREPO.md +58 -0
  5. package/README.md +280 -0
  6. package/SKILL.md +137 -0
  7. package/dist/adapters/openclaw.d.ts +103 -0
  8. package/dist/adapters/openclaw.d.ts.map +1 -0
  9. package/dist/adapters/openclaw.js +297 -0
  10. package/dist/adapters/openclaw.js.map +1 -0
  11. package/dist/cli/commands.d.ts +17 -0
  12. package/dist/cli/commands.d.ts.map +1 -0
  13. package/dist/cli/commands.js +107 -0
  14. package/dist/cli/commands.js.map +1 -0
  15. package/dist/cli/index.d.ts +6 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +203 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/core/autonomous-economy.d.ts +136 -0
  20. package/dist/core/autonomous-economy.d.ts.map +1 -0
  21. package/dist/core/autonomous-economy.js +255 -0
  22. package/dist/core/autonomous-economy.js.map +1 -0
  23. package/dist/core/connection-manager.d.ts +80 -0
  24. package/dist/core/connection-manager.d.ts.map +1 -0
  25. package/dist/core/connection-manager.js +235 -0
  26. package/dist/core/connection-manager.js.map +1 -0
  27. package/dist/core/connection-manager.test.d.ts +2 -0
  28. package/dist/core/connection-manager.test.d.ts.map +1 -0
  29. package/dist/core/connection-manager.test.js +52 -0
  30. package/dist/core/connection-manager.test.js.map +1 -0
  31. package/dist/core/e2ee-crypto.d.ts +90 -0
  32. package/dist/core/e2ee-crypto.d.ts.map +1 -0
  33. package/dist/core/e2ee-crypto.js +190 -0
  34. package/dist/core/e2ee-crypto.js.map +1 -0
  35. package/dist/core/f2a.d.ts +126 -0
  36. package/dist/core/f2a.d.ts.map +1 -0
  37. package/dist/core/f2a.js +425 -0
  38. package/dist/core/f2a.js.map +1 -0
  39. package/dist/core/identity.d.ts +47 -0
  40. package/dist/core/identity.d.ts.map +1 -0
  41. package/dist/core/identity.js +130 -0
  42. package/dist/core/identity.js.map +1 -0
  43. package/dist/core/identity.test.d.ts +2 -0
  44. package/dist/core/identity.test.d.ts.map +1 -0
  45. package/dist/core/identity.test.js +43 -0
  46. package/dist/core/identity.test.js.map +1 -0
  47. package/dist/core/p2p-network.d.ts +242 -0
  48. package/dist/core/p2p-network.d.ts.map +1 -0
  49. package/dist/core/p2p-network.js +1182 -0
  50. package/dist/core/p2p-network.js.map +1 -0
  51. package/dist/core/reputation-security.d.ts +168 -0
  52. package/dist/core/reputation-security.d.ts.map +1 -0
  53. package/dist/core/reputation-security.js +369 -0
  54. package/dist/core/reputation-security.js.map +1 -0
  55. package/dist/core/reputation.d.ts +179 -0
  56. package/dist/core/reputation.d.ts.map +1 -0
  57. package/dist/core/reputation.js +472 -0
  58. package/dist/core/reputation.js.map +1 -0
  59. package/dist/core/review-committee.d.ts +130 -0
  60. package/dist/core/review-committee.d.ts.map +1 -0
  61. package/dist/core/review-committee.js +251 -0
  62. package/dist/core/review-committee.js.map +1 -0
  63. package/dist/core/serverless.d.ts +155 -0
  64. package/dist/core/serverless.d.ts.map +1 -0
  65. package/dist/core/serverless.js +615 -0
  66. package/dist/core/serverless.js.map +1 -0
  67. package/dist/core/token-manager.d.ts +42 -0
  68. package/dist/core/token-manager.d.ts.map +1 -0
  69. package/dist/core/token-manager.js +122 -0
  70. package/dist/core/token-manager.js.map +1 -0
  71. package/dist/daemon/control-server.d.ts +55 -0
  72. package/dist/daemon/control-server.d.ts.map +1 -0
  73. package/dist/daemon/control-server.js +262 -0
  74. package/dist/daemon/control-server.js.map +1 -0
  75. package/dist/daemon/index.d.ts +35 -0
  76. package/dist/daemon/index.d.ts.map +1 -0
  77. package/dist/daemon/index.js +69 -0
  78. package/dist/daemon/index.js.map +1 -0
  79. package/dist/daemon/main.d.ts +6 -0
  80. package/dist/daemon/main.d.ts.map +1 -0
  81. package/dist/daemon/main.js +38 -0
  82. package/dist/daemon/main.js.map +1 -0
  83. package/dist/daemon/start.d.ts +6 -0
  84. package/dist/daemon/start.d.ts.map +1 -0
  85. package/dist/daemon/start.js +25 -0
  86. package/dist/daemon/start.js.map +1 -0
  87. package/dist/daemon/webhook.d.ts +30 -0
  88. package/dist/daemon/webhook.d.ts.map +1 -0
  89. package/dist/daemon/webhook.js +86 -0
  90. package/dist/daemon/webhook.js.map +1 -0
  91. package/dist/daemon/webhook.test.d.ts +2 -0
  92. package/dist/daemon/webhook.test.d.ts.map +1 -0
  93. package/dist/daemon/webhook.test.js +24 -0
  94. package/dist/daemon/webhook.test.js.map +1 -0
  95. package/dist/index.d.ts +24 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +25 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/protocol/messages.d.ts +739 -0
  100. package/dist/protocol/messages.d.ts.map +1 -0
  101. package/dist/protocol/messages.js +188 -0
  102. package/dist/protocol/messages.js.map +1 -0
  103. package/dist/protocol/messages.test.d.ts +2 -0
  104. package/dist/protocol/messages.test.d.ts.map +1 -0
  105. package/dist/protocol/messages.test.js +55 -0
  106. package/dist/protocol/messages.test.js.map +1 -0
  107. package/dist/types/index.d.ts +247 -0
  108. package/dist/types/index.d.ts.map +1 -0
  109. package/dist/types/index.js +10 -0
  110. package/dist/types/index.js.map +1 -0
  111. package/dist/types/result.d.ts +28 -0
  112. package/dist/types/result.d.ts.map +1 -0
  113. package/dist/types/result.js +16 -0
  114. package/dist/types/result.js.map +1 -0
  115. package/dist/utils/benchmark.d.ts +67 -0
  116. package/dist/utils/benchmark.d.ts.map +1 -0
  117. package/dist/utils/benchmark.js +179 -0
  118. package/dist/utils/benchmark.js.map +1 -0
  119. package/dist/utils/logger.d.ts +105 -0
  120. package/dist/utils/logger.d.ts.map +1 -0
  121. package/dist/utils/logger.js +275 -0
  122. package/dist/utils/logger.js.map +1 -0
  123. package/dist/utils/middleware.d.ts +85 -0
  124. package/dist/utils/middleware.d.ts.map +1 -0
  125. package/dist/utils/middleware.js +173 -0
  126. package/dist/utils/middleware.js.map +1 -0
  127. package/dist/utils/rate-limiter.d.ts +71 -0
  128. package/dist/utils/rate-limiter.d.ts.map +1 -0
  129. package/dist/utils/rate-limiter.js +160 -0
  130. package/dist/utils/rate-limiter.js.map +1 -0
  131. package/dist/utils/signature.d.ts +57 -0
  132. package/dist/utils/signature.d.ts.map +1 -0
  133. package/dist/utils/signature.js +102 -0
  134. package/dist/utils/signature.js.map +1 -0
  135. package/dist/utils/validation.d.ts +504 -0
  136. package/dist/utils/validation.d.ts.map +1 -0
  137. package/dist/utils/validation.js +159 -0
  138. package/dist/utils/validation.js.map +1 -0
  139. package/docs/F2A-PROTOCOL.md +61 -0
  140. package/docs/MOBILE_BOOTSTRAP_DESIGN.md +126 -0
  141. package/docs/a2a-lessons.md +316 -0
  142. package/docs/middleware-guide.md +448 -0
  143. package/docs/readme-update-checklist.md +90 -0
  144. package/docs/reputation-guide.md +396 -0
  145. package/docs/rfcs/001-reputation-system.md +712 -0
  146. package/docs/security-design.md +247 -0
  147. package/install.sh +231 -0
  148. package/package.json +64 -0
  149. package/packages/openclaw-adapter/README.md +510 -0
  150. package/packages/openclaw-adapter/openclaw.plugin.json +106 -0
  151. package/packages/openclaw-adapter/package.json +40 -0
  152. package/packages/openclaw-adapter/src/announcement-queue.test.ts +449 -0
  153. package/packages/openclaw-adapter/src/announcement-queue.ts +403 -0
  154. package/packages/openclaw-adapter/src/capability-detector.test.ts +99 -0
  155. package/packages/openclaw-adapter/src/capability-detector.ts +183 -0
  156. package/packages/openclaw-adapter/src/claim-handlers.test.ts +974 -0
  157. package/packages/openclaw-adapter/src/claim-handlers.ts +482 -0
  158. package/packages/openclaw-adapter/src/connector.business.test.ts +583 -0
  159. package/packages/openclaw-adapter/src/connector.ts +795 -0
  160. package/packages/openclaw-adapter/src/index.test.ts +82 -0
  161. package/packages/openclaw-adapter/src/index.ts +18 -0
  162. package/packages/openclaw-adapter/src/integration.e2e.test.ts +829 -0
  163. package/packages/openclaw-adapter/src/logger.ts +51 -0
  164. package/packages/openclaw-adapter/src/network-client.test.ts +266 -0
  165. package/packages/openclaw-adapter/src/network-client.ts +251 -0
  166. package/packages/openclaw-adapter/src/network-recovery.test.ts +465 -0
  167. package/packages/openclaw-adapter/src/node-manager.test.ts +136 -0
  168. package/packages/openclaw-adapter/src/node-manager.ts +429 -0
  169. package/packages/openclaw-adapter/src/plugin.test.ts +439 -0
  170. package/packages/openclaw-adapter/src/plugin.ts +104 -0
  171. package/packages/openclaw-adapter/src/reputation.test.ts +221 -0
  172. package/packages/openclaw-adapter/src/reputation.ts +368 -0
  173. package/packages/openclaw-adapter/src/task-guard.test.ts +502 -0
  174. package/packages/openclaw-adapter/src/task-guard.ts +860 -0
  175. package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +462 -0
  176. package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +284 -0
  177. package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +408 -0
  178. package/packages/openclaw-adapter/src/task-queue.ts +668 -0
  179. package/packages/openclaw-adapter/src/tool-handlers.test.ts +906 -0
  180. package/packages/openclaw-adapter/src/tool-handlers.ts +574 -0
  181. package/packages/openclaw-adapter/src/types.ts +361 -0
  182. package/packages/openclaw-adapter/src/webhook-pusher.test.ts +188 -0
  183. package/packages/openclaw-adapter/src/webhook-pusher.ts +220 -0
  184. package/packages/openclaw-adapter/src/webhook-server.test.ts +580 -0
  185. package/packages/openclaw-adapter/src/webhook-server.ts +202 -0
  186. package/packages/openclaw-adapter/tsconfig.json +20 -0
  187. package/src/cli/commands.test.ts +157 -0
  188. package/src/cli/commands.ts +129 -0
  189. package/src/cli/index.test.ts +77 -0
  190. package/src/cli/index.ts +234 -0
  191. package/src/core/autonomous-economy.test.ts +291 -0
  192. package/src/core/autonomous-economy.ts +428 -0
  193. package/src/core/e2ee-crypto.test.ts +125 -0
  194. package/src/core/e2ee-crypto.ts +246 -0
  195. package/src/core/f2a.test.ts +269 -0
  196. package/src/core/f2a.ts +618 -0
  197. package/src/core/p2p-network.test.ts +199 -0
  198. package/src/core/p2p-network.ts +1432 -0
  199. package/src/core/reputation-security.test.ts +403 -0
  200. package/src/core/reputation-security.ts +562 -0
  201. package/src/core/reputation.test.ts +260 -0
  202. package/src/core/reputation.ts +576 -0
  203. package/src/core/review-committee.test.ts +380 -0
  204. package/src/core/review-committee.ts +401 -0
  205. package/src/core/token-manager.test.ts +133 -0
  206. package/src/core/token-manager.ts +140 -0
  207. package/src/daemon/control-server.test.ts +216 -0
  208. package/src/daemon/control-server.ts +292 -0
  209. package/src/daemon/index.test.ts +85 -0
  210. package/src/daemon/index.ts +89 -0
  211. package/src/daemon/main.ts +44 -0
  212. package/src/daemon/start.ts +29 -0
  213. package/src/daemon/webhook.test.ts +68 -0
  214. package/src/daemon/webhook.ts +105 -0
  215. package/src/index.test.ts +436 -0
  216. package/src/index.ts +72 -0
  217. package/src/types/index.test.ts +87 -0
  218. package/src/types/index.ts +341 -0
  219. package/src/types/result.ts +68 -0
  220. package/src/utils/benchmark.ts +237 -0
  221. package/src/utils/logger.ts +331 -0
  222. package/src/utils/middleware.ts +229 -0
  223. package/src/utils/rate-limiter.ts +207 -0
  224. package/src/utils/signature.ts +136 -0
  225. package/src/utils/validation.ts +186 -0
  226. package/tests/docker/Dockerfile.node +23 -0
  227. package/tests/docker/Dockerfile.runner +18 -0
  228. package/tests/docker/docker-compose.test.yml +73 -0
  229. package/tests/integration/message-passing.test.ts +109 -0
  230. package/tests/integration/multi-node.test.ts +92 -0
  231. package/tests/integration/p2p-connection.test.ts +83 -0
  232. package/tests/integration/test-config.ts +32 -0
  233. package/tsconfig.json +21 -0
  234. package/vitest.config.ts +26 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * F2A Node Manager
3
+ * 管理 F2A Network 服务的生命周期
4
+ */
5
+
6
+ import { spawn, ChildProcess } from 'child_process';
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { promisify } from 'util';
10
+ import type { F2ANodeConfig, Result } from './types.js';
11
+ import { nodeLogger as logger } from './logger.js';
12
+
13
+ const sleep = promisify(setTimeout);
14
+
15
+ // PID 文件路径
16
+ const PID_FILE_NAME = 'f2a-node.pid';
17
+
18
+ /** 健康检查重启配置 */
19
+ interface HealthCheckRestartConfig {
20
+ /** 最大连续重启次数 */
21
+ maxRestarts: number;
22
+ /** 重启计数重置时间窗口(毫秒) */
23
+ resetWindowMs: number;
24
+ /** 冷却期基础时间(毫秒) */
25
+ cooldownBaseMs: number;
26
+ /** 冷却期最大时间(毫秒) */
27
+ cooldownMaxMs: number;
28
+ }
29
+
30
+ /** 默认重启配置 */
31
+ const DEFAULT_RESTART_CONFIG: HealthCheckRestartConfig = {
32
+ maxRestarts: 3, // 最多连续重启 3 次
33
+ resetWindowMs: 60000, // 1 分钟内重置计数
34
+ cooldownBaseMs: 5000, // 冷却期基础 5 秒
35
+ cooldownMaxMs: 60000 // 冷却期最大 60 秒
36
+ };
37
+
38
+ export class F2ANodeManager {
39
+ private process: ChildProcess | null = null;
40
+ private config: F2ANodeConfig;
41
+ private healthCheckInterval?: NodeJS.Timeout;
42
+ private pidFilePath: string;
43
+
44
+ // P1 修复:健康检查重启限制
45
+ private restartConfig: HealthCheckRestartConfig;
46
+ private consecutiveRestarts: number = 0;
47
+ private lastRestartTime: number = 0;
48
+ private isRestarting: boolean = false;
49
+
50
+ constructor(config: Partial<F2ANodeConfig>) {
51
+ this.config = {
52
+ nodePath: config.nodePath || './F2A',
53
+ controlPort: config.controlPort || 9001,
54
+ controlToken: config.controlToken || this.generateToken(),
55
+ p2pPort: config.p2pPort || 9000,
56
+ enableMDNS: config.enableMDNS ?? true,
57
+ bootstrapPeers: config.bootstrapPeers || []
58
+ };
59
+ this.pidFilePath = join(this.config.nodePath, PID_FILE_NAME);
60
+ this.restartConfig = { ...DEFAULT_RESTART_CONFIG };
61
+
62
+ // 启动时清理孤儿进程
63
+ this.cleanupOrphanProcesses();
64
+ }
65
+
66
+ /**
67
+ * 清理孤儿进程
68
+ * 检查 PID 文件中记录的进程是否仍在运行,如果是则尝试清理
69
+ */
70
+ private cleanupOrphanProcesses(): void {
71
+ if (!existsSync(this.pidFilePath)) {
72
+ return;
73
+ }
74
+
75
+ try {
76
+ const pidStr = readFileSync(this.pidFilePath, 'utf-8').trim();
77
+ const pid = parseInt(pidStr, 10);
78
+
79
+ if (isNaN(pid)) {
80
+ // PID 文件无效,删除
81
+ unlinkSync(this.pidFilePath);
82
+ return;
83
+ }
84
+
85
+ // 检查进程是否存在
86
+ try {
87
+ process.kill(pid, 0); // 不实际发送信号,只检查进程是否存在
88
+ // 进程存在,尝试终止
89
+ logger.info('发现孤儿进程,尝试终止: pid=%d', pid);
90
+ try {
91
+ process.kill(pid, 'SIGTERM');
92
+ // 等待进程终止
93
+ setTimeout(() => {
94
+ try {
95
+ process.kill(pid, 0); // 检查是否还在运行
96
+ process.kill(pid, 'SIGKILL'); // 强制终止
97
+ } catch {
98
+ // 进程已终止
99
+ }
100
+ }, 3000);
101
+ } catch (killError) {
102
+ // 无法终止,可能是权限问题
103
+ logger.warn('无法终止孤儿进程: pid=%d, error=%s', pid, killError);
104
+ }
105
+ } catch {
106
+ // 进程不存在,只删除 PID 文件
107
+ }
108
+
109
+ // 删除 PID 文件
110
+ unlinkSync(this.pidFilePath);
111
+ logger.info('孤儿进程清理完成');
112
+ } catch (error) {
113
+ logger.warn('清理孤儿进程失败: error=%s', error);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 保存 PID 到文件
119
+ */
120
+ private savePid(pid: number): void {
121
+ try {
122
+ writeFileSync(this.pidFilePath, String(pid), { mode: 0o644 });
123
+ logger.info('PID 文件已保存: path=%s', this.pidFilePath);
124
+ } catch (error) {
125
+ logger.warn('保存 PID 文件失败: error=%s', error);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 删除 PID 文件
131
+ */
132
+ private removePidFile(): void {
133
+ try {
134
+ if (existsSync(this.pidFilePath)) {
135
+ unlinkSync(this.pidFilePath);
136
+ }
137
+ } catch (error) {
138
+ logger.warn('删除 PID 文件失败: error=%s', error);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 确保 F2A Node 在运行
144
+ */
145
+ async ensureRunning(): Promise<Result<void>> {
146
+ if (await this.isRunning()) {
147
+ logger.info('Node 已在运行');
148
+ return { success: true };
149
+ }
150
+
151
+ return this.start();
152
+ }
153
+
154
+ /**
155
+ * 启动 F2A Node
156
+ */
157
+ async start(): Promise<Result<void>> {
158
+ const daemonPath = join(this.config.nodePath, 'dist/daemon/index.js');
159
+
160
+ if (!existsSync(daemonPath)) {
161
+ return {
162
+ success: false,
163
+ error: `F2A Node 未找到: ${daemonPath}\n请先运行: cd ${this.config.nodePath} && npm install && npm run build`
164
+ };
165
+ }
166
+
167
+ logger.info('启动 Node...');
168
+ logger.info('Control Port: %d', this.config.controlPort);
169
+ logger.info('P2P Port: %d', this.config.p2pPort);
170
+
171
+ try {
172
+ this.process = spawn('node', [daemonPath], {
173
+ cwd: this.config.nodePath,
174
+ env: {
175
+ ...process.env,
176
+ F2A_CONTROL_PORT: String(this.config.controlPort),
177
+ F2A_CONTROL_TOKEN: this.config.controlToken,
178
+ F2A_P2P_PORT: String(this.config.p2pPort),
179
+ F2A_ENABLE_MDNS: String(this.config.enableMDNS),
180
+ F2A_BOOTSTRAP_PEERS: JSON.stringify(this.config.bootstrapPeers)
181
+ },
182
+ detached: true,
183
+ stdio: ['ignore', 'pipe', 'pipe']
184
+ });
185
+
186
+ // 记录子进程 PID
187
+ const pid = this.process.pid;
188
+ if (pid) {
189
+ this.savePid(pid);
190
+ }
191
+
192
+ // 监听进程退出事件
193
+ this.process.on('exit', (code, signal) => {
194
+ logger.info('Node 进程退出: code=%s, signal=%s', code, signal);
195
+ this.removePidFile();
196
+ this.process = null;
197
+ });
198
+
199
+ this.process.on('error', (err) => {
200
+ logger.error('Node 进程错误: error=%s', err);
201
+ this.removePidFile();
202
+ });
203
+
204
+ this.process.unref();
205
+
206
+ // 记录日志
207
+ this.process.stdout?.on('data', (data) => {
208
+ logger.info('Node stdout: %s', data.toString().trim());
209
+ });
210
+
211
+ this.process.stderr?.on('data', (data) => {
212
+ logger.error('Node stderr: %s', data.toString().trim());
213
+ });
214
+
215
+ // 等待启动完成
216
+ await this.waitForReady(30000);
217
+
218
+ // 启动健康检查
219
+ this.startHealthCheck();
220
+
221
+ // P1 修复:成功启动后重置重启计数器
222
+ this.consecutiveRestarts = 0;
223
+
224
+ logger.info('Node 启动成功');
225
+ return { success: true };
226
+
227
+ } catch (error) {
228
+ const errorMsg = error instanceof Error ? error.message : String(error);
229
+ this.removePidFile();
230
+ return { success: false, error: errorMsg };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 停止 F2A Node
236
+ */
237
+ async stop(): Promise<void> {
238
+ if (this.healthCheckInterval) {
239
+ clearInterval(this.healthCheckInterval);
240
+ }
241
+
242
+ if (this.process) {
243
+ logger.info('停止 Node...');
244
+
245
+ // 尝试优雅关闭
246
+ this.process.kill('SIGTERM');
247
+
248
+ // 等待 5 秒
249
+ await sleep(5000);
250
+
251
+ // 如果还在运行,强制关闭
252
+ if (this.process.exitCode === null) {
253
+ this.process.kill('SIGKILL');
254
+ }
255
+
256
+ this.process = null;
257
+ } else {
258
+ // 没有当前进程引用,但可能存在孤儿进程
259
+ // 尝试从 PID 文件读取并终止
260
+ if (existsSync(this.pidFilePath)) {
261
+ try {
262
+ const pidStr = readFileSync(this.pidFilePath, 'utf-8').trim();
263
+ const pid = parseInt(pidStr, 10);
264
+ if (!isNaN(pid)) {
265
+ logger.info('尝试终止残留进程: pid=%d', pid);
266
+ try {
267
+ process.kill(pid, 'SIGTERM');
268
+ await sleep(3000);
269
+ try {
270
+ process.kill(pid, 0);
271
+ process.kill(pid, 'SIGKILL');
272
+ } catch {
273
+ // 进程已终止
274
+ }
275
+ } catch {
276
+ // 进程不存在或无权限
277
+ }
278
+ }
279
+ } catch (error) {
280
+ logger.warn('清理残留进程失败: error=%s', error);
281
+ }
282
+ }
283
+ }
284
+
285
+ // 清理 PID 文件
286
+ this.removePidFile();
287
+ }
288
+
289
+ /**
290
+ * 检查 Node 是否运行中
291
+ */
292
+ async isRunning(): Promise<boolean> {
293
+ try {
294
+ const response = await fetch(`http://localhost:${this.config.controlPort}/health`, {
295
+ headers: {
296
+ 'Authorization': `Bearer ${this.config.controlToken}`
297
+ }
298
+ });
299
+ return response.ok;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * 获取 Node 状态
307
+ */
308
+ async getStatus(): Promise<Result<{
309
+ running: boolean;
310
+ peerId?: string;
311
+ connectedPeers?: number;
312
+ uptime?: number;
313
+ }>> {
314
+ try {
315
+ const response = await fetch(`http://localhost:${this.config.controlPort}/status`, {
316
+ headers: {
317
+ 'Authorization': `Bearer ${this.config.controlToken}`
318
+ }
319
+ });
320
+
321
+ if (!response.ok) {
322
+ return { success: false, error: 'Node 未响应' };
323
+ }
324
+
325
+ const data = await response.json() as { running: boolean; peerId?: string; connectedPeers?: number; uptime?: number };
326
+ return { success: true, data };
327
+
328
+ } catch (error) {
329
+ return {
330
+ success: false,
331
+ error: error instanceof Error ? error.message : String(error)
332
+ };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * 等待 Node 就绪
338
+ */
339
+ private async waitForReady(timeout: number): Promise<void> {
340
+ const start = Date.now();
341
+
342
+ while (Date.now() - start < timeout) {
343
+ if (await this.isRunning()) {
344
+ return;
345
+ }
346
+ await sleep(500);
347
+ }
348
+
349
+ throw new Error('Node 启动超时');
350
+ }
351
+
352
+ /**
353
+ * 启动健康检查
354
+ *
355
+ * P1 修复:添加重启次数限制和冷却期,防止无限重启循环
356
+ */
357
+ private startHealthCheck(): void {
358
+ this.healthCheckInterval = setInterval(async () => {
359
+ // 如果正在重启,跳过本次检查
360
+ if (this.isRestarting) {
361
+ return;
362
+ }
363
+
364
+ const isHealthy = await this.isRunning();
365
+ if (!isHealthy && this.process) {
366
+ // 检查是否达到重启限制
367
+ const now = Date.now();
368
+
369
+ // 如果距离上次重启超过重置窗口,重置计数
370
+ if (now - this.lastRestartTime > this.restartConfig.resetWindowMs) {
371
+ this.consecutiveRestarts = 0;
372
+ }
373
+
374
+ // 检查是否达到最大重启次数
375
+ if (this.consecutiveRestarts >= this.restartConfig.maxRestarts) {
376
+ logger.error(
377
+ 'Node 健康检查失败,已达到最大重启次数: maxRestarts=%d, resetWindowMs=%d',
378
+ this.restartConfig.maxRestarts,
379
+ Math.round(this.restartConfig.resetWindowMs / 1000)
380
+ );
381
+ return;
382
+ }
383
+
384
+ // 计算冷却期(指数退避)
385
+ const cooldownMs = Math.min(
386
+ this.restartConfig.cooldownBaseMs * Math.pow(2, this.consecutiveRestarts),
387
+ this.restartConfig.cooldownMaxMs
388
+ );
389
+
390
+ logger.warn(
391
+ 'Node 健康检查失败,尝试重启: attempt=%d/%d, cooldownMs=%d',
392
+ this.consecutiveRestarts + 1,
393
+ this.restartConfig.maxRestarts,
394
+ Math.round(cooldownMs / 1000)
395
+ );
396
+
397
+ this.isRestarting = true;
398
+ this.consecutiveRestarts++;
399
+ this.lastRestartTime = now;
400
+
401
+ try {
402
+ await this.stop();
403
+ await sleep(cooldownMs);
404
+ await this.start();
405
+ } catch (error) {
406
+ logger.error('重启失败: error=%s', error);
407
+ } finally {
408
+ this.isRestarting = false;
409
+ }
410
+ }
411
+ }, 30000); // 每 30 秒检查一次
412
+ }
413
+
414
+ /**
415
+ * 生成随机 Token
416
+ */
417
+ private generateToken(): string {
418
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
419
+ let token = 'f2a-';
420
+ for (let i = 0; i < 32; i++) {
421
+ token += chars.charAt(Math.floor(Math.random() * chars.length));
422
+ }
423
+ return token;
424
+ }
425
+
426
+ getConfig(): F2ANodeConfig {
427
+ return { ...this.config };
428
+ }
429
+ }