@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,51 @@
1
+ /**
2
+ * OpenClaw Adapter 统一日志模块
3
+ *
4
+ * 设计说明:
5
+ * - 提供统一的日志接口,便于维护和测试
6
+ * - 支持结构化日志输出
7
+ * - 默认使用 console,未来可扩展为其他日志系统
8
+ */
9
+
10
+ export interface Logger {
11
+ debug(message: string, ...args: unknown[]): void;
12
+ info(message: string, ...args: unknown[]): void;
13
+ warn(message: string, ...args: unknown[]): void;
14
+ error(message: string, ...args: unknown[]): void;
15
+ }
16
+
17
+ /** 默认日志前缀 */
18
+ const DEFAULT_PREFIX = '[F2A]';
19
+
20
+ /**
21
+ * 创建日志记录器
22
+ * @param component 组件名称(可选)
23
+ */
24
+ export function createLogger(component?: string): Logger {
25
+ const prefix = component ? `[F2A:${component}]` : DEFAULT_PREFIX;
26
+
27
+ return {
28
+ debug(message: string, ...args: unknown[]): void {
29
+ console.log(`${prefix} ${message}`, ...args);
30
+ },
31
+ info(message: string, ...args: unknown[]): void {
32
+ console.log(`${prefix} ${message}`, ...args);
33
+ },
34
+ warn(message: string, ...args: unknown[]): void {
35
+ console.warn(`${prefix} ${message}`, ...args);
36
+ },
37
+ error(message: string, ...args: unknown[]): void {
38
+ console.error(`${prefix} ${message}`, ...args);
39
+ }
40
+ };
41
+ }
42
+
43
+ /** 默认日志记录器 */
44
+ export const logger = createLogger();
45
+
46
+ /** 组件专用日志记录器 */
47
+ export const webhookLogger = createLogger('Webhook');
48
+ export const taskGuardLogger = createLogger('TaskGuard');
49
+ export const queueLogger = createLogger('Queue');
50
+ export const nodeLogger = createLogger('Node');
51
+ export const pluginLogger = createLogger('Plugin');
@@ -0,0 +1,266 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { F2ANetworkClient } from './network-client';
3
+ import { F2ANodeConfig } from './types';
4
+
5
+ // Mock global fetch
6
+ const mockFetch = vi.fn();
7
+ global.fetch = mockFetch;
8
+
9
+ describe('F2ANetworkClient', () => {
10
+ let client: F2ANetworkClient;
11
+ const mockConfig: F2ANodeConfig = {
12
+ nodePath: './F2A',
13
+ controlPort: 9001,
14
+ controlToken: 'test-token',
15
+ p2pPort: 9000,
16
+ enableMDNS: true,
17
+ };
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ client = new F2ANetworkClient(mockConfig);
22
+ });
23
+
24
+ describe('HTTP request handling', () => {
25
+ it('should include correct headers in requests', async () => {
26
+ mockFetch.mockResolvedValueOnce({
27
+ ok: true,
28
+ json: async () => ({ success: true, data: [] }),
29
+ });
30
+
31
+ await client.getConnectedPeers();
32
+
33
+ expect(mockFetch).toHaveBeenCalledWith(
34
+ 'http://localhost:9001/peers',
35
+ expect.objectContaining({
36
+ headers: {
37
+ 'Authorization': 'Bearer test-token',
38
+ 'Content-Type': 'application/json',
39
+ },
40
+ })
41
+ );
42
+ });
43
+
44
+ it('should handle HTTP errors correctly', async () => {
45
+ mockFetch.mockResolvedValueOnce({
46
+ ok: false,
47
+ status: 401,
48
+ text: async () => 'Unauthorized',
49
+ });
50
+
51
+ const result = await client.getConnectedPeers();
52
+
53
+ expect(result.success).toBe(false);
54
+ // 新的 Result 类型中 error 是 F2AError 对象
55
+ if (!result.success) {
56
+ expect(result.error.message).toContain('401');
57
+ expect(result.error.code).toBe('CONNECTION_FAILED');
58
+ }
59
+ });
60
+
61
+ it('should handle network errors', async () => {
62
+ mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
63
+
64
+ const result = await client.getConnectedPeers();
65
+
66
+ expect(result.success).toBe(false);
67
+ // 新的 Result 类型中 error 是 F2AError 对象
68
+ if (!result.success) {
69
+ expect(result.error.message).toBe('Connection refused');
70
+ expect(result.error.code).toBe('CONNECTION_FAILED');
71
+ }
72
+ });
73
+
74
+ it('should handle JSON parsing errors', async () => {
75
+ mockFetch.mockResolvedValueOnce({
76
+ ok: true,
77
+ json: async () => { throw new Error('Invalid JSON'); },
78
+ });
79
+
80
+ const result = await client.getConnectedPeers();
81
+
82
+ expect(result.success).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe('discoverAgents', () => {
87
+ it('should discover agents without capability filter', async () => {
88
+ const mockAgents = [
89
+ { peerId: 'peer-1', displayName: 'Agent 1', capabilities: [] },
90
+ ];
91
+ mockFetch.mockResolvedValueOnce({
92
+ ok: true,
93
+ json: async () => mockAgents,
94
+ });
95
+
96
+ const result = await client.discoverAgents();
97
+
98
+ expect(result.success).toBe(true);
99
+ expect(result.data).toEqual(mockAgents);
100
+ expect(mockFetch).toHaveBeenCalledWith(
101
+ 'http://localhost:9001/discover',
102
+ expect.objectContaining({
103
+ method: 'POST',
104
+ body: JSON.stringify({ capability: undefined }),
105
+ })
106
+ );
107
+ });
108
+
109
+ it('should discover agents with capability filter', async () => {
110
+ mockFetch.mockResolvedValueOnce({
111
+ ok: true,
112
+ json: async () => ({ success: true, data: [] }),
113
+ });
114
+
115
+ await client.discoverAgents('code-generation');
116
+
117
+ expect(mockFetch).toHaveBeenCalledWith(
118
+ 'http://localhost:9001/discover',
119
+ expect.objectContaining({
120
+ body: JSON.stringify({ capability: 'code-generation' }),
121
+ })
122
+ );
123
+ });
124
+ });
125
+
126
+ describe('delegateTask', () => {
127
+ it('should send task delegation request', async () => {
128
+ mockFetch.mockResolvedValueOnce({
129
+ ok: true,
130
+ json: async () => ({ success: true, data: { result: 'done' } }),
131
+ });
132
+
133
+ const result = await client.delegateTask({
134
+ peerId: 'peer-1',
135
+ taskType: 'test-task',
136
+ description: 'Test description',
137
+ parameters: { key: 'value' },
138
+ timeout: 30000,
139
+ });
140
+
141
+ expect(result.success).toBe(true);
142
+ expect(mockFetch).toHaveBeenCalledWith(
143
+ 'http://localhost:9001/delegate',
144
+ expect.objectContaining({
145
+ method: 'POST',
146
+ body: expect.stringContaining('peer-1'),
147
+ })
148
+ );
149
+ });
150
+ });
151
+
152
+ describe('sendTaskResponse', () => {
153
+ it('should send task response with all fields', async () => {
154
+ mockFetch.mockResolvedValueOnce({
155
+ ok: true,
156
+ json: async () => ({ success: true }),
157
+ });
158
+
159
+ const result = await client.sendTaskResponse('peer-1', {
160
+ taskId: 'task-123',
161
+ status: 'success',
162
+ result: { output: 'hello' },
163
+ latency: 100,
164
+ });
165
+
166
+ expect(result.success).toBe(true);
167
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
168
+ expect(callBody).toMatchObject({
169
+ peerId: 'peer-1',
170
+ taskId: 'task-123',
171
+ status: 'success',
172
+ });
173
+ });
174
+ });
175
+
176
+ describe('registerWebhook', () => {
177
+ it('should register webhook with default events', async () => {
178
+ mockFetch.mockResolvedValueOnce({
179
+ ok: true,
180
+ json: async () => ({ success: true }),
181
+ });
182
+
183
+ const result = await client.registerWebhook('http://localhost:9002/webhook');
184
+
185
+ expect(result.success).toBe(true);
186
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
187
+ expect(callBody).toEqual({
188
+ url: 'http://localhost:9002/webhook',
189
+ events: ['discover', 'delegate', 'status'],
190
+ });
191
+ });
192
+ });
193
+
194
+ describe('updateAgentInfo', () => {
195
+ it('should update agent info', async () => {
196
+ mockFetch.mockResolvedValueOnce({
197
+ ok: true,
198
+ json: async () => ({ success: true }),
199
+ });
200
+
201
+ const result = await client.updateAgentInfo({
202
+ displayName: 'Test Agent',
203
+ capabilities: [{ name: 'test', description: 'Test capability', tools: [] }],
204
+ });
205
+
206
+ expect(result.success).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe('getPendingTasks', () => {
211
+ it('should get pending tasks', async () => {
212
+ const mockTasks = [{ taskId: 'task-1', description: 'Test' }];
213
+ mockFetch.mockResolvedValueOnce({
214
+ ok: true,
215
+ json: async () => mockTasks,
216
+ });
217
+
218
+ const result = await client.getPendingTasks();
219
+
220
+ expect(result.success).toBe(true);
221
+ expect(result.data).toEqual(mockTasks);
222
+ });
223
+ });
224
+
225
+ describe('confirmConnection', () => {
226
+ it('should confirm connection', async () => {
227
+ mockFetch.mockResolvedValueOnce({
228
+ ok: true,
229
+ json: async () => ({ success: true }),
230
+ });
231
+
232
+ const result = await client.confirmConnection('peer-1');
233
+
234
+ expect(result.success).toBe(true);
235
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
236
+ expect(callBody).toEqual({ peerId: 'peer-1' });
237
+ });
238
+ });
239
+
240
+ describe('rejectConnection', () => {
241
+ it('should reject connection with reason', async () => {
242
+ mockFetch.mockResolvedValueOnce({
243
+ ok: true,
244
+ json: async () => ({ success: true }),
245
+ });
246
+
247
+ const result = await client.rejectConnection('peer-1', 'suspicious');
248
+
249
+ expect(result.success).toBe(true);
250
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
251
+ expect(callBody).toEqual({ peerId: 'peer-1', reason: 'suspicious' });
252
+ });
253
+
254
+ it('should reject connection without reason', async () => {
255
+ mockFetch.mockResolvedValueOnce({
256
+ ok: true,
257
+ json: async () => ({ success: true }),
258
+ });
259
+
260
+ await client.rejectConnection('peer-1');
261
+
262
+ const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
263
+ expect(callBody).toEqual({ peerId: 'peer-1', reason: undefined });
264
+ });
265
+ });
266
+ });
@@ -0,0 +1,251 @@
1
+ /**
2
+ * F2A Network Client
3
+ * 与 F2A Node 的 HTTP API 通信
4
+ */
5
+
6
+ import type {
7
+ F2ANodeConfig,
8
+ AgentInfo,
9
+ PeerInfo,
10
+ TaskRequest,
11
+ TaskResponse,
12
+ DelegateOptions
13
+ } from './types.js';
14
+ import { Result, failure, success, createError } from './types.js';
15
+ import { nodeLogger as logger } from './logger.js';
16
+
17
+ /** 默认请求超时(毫秒) */
18
+ const DEFAULT_TIMEOUT_MS = 30000;
19
+
20
+ /** 默认重试配置 */
21
+ const DEFAULT_MAX_RETRIES = 3;
22
+ const DEFAULT_BASE_DELAY_MS = 1000;
23
+
24
+ /** 可重试的错误码 */
25
+ const RETRYABLE_ERROR_CODES = [
26
+ 'ECONNRESET',
27
+ 'ENOTFOUND',
28
+ 'ETIMEDOUT',
29
+ 'ECONNREFUSED',
30
+ 'EHOSTUNREACH',
31
+ 'ENETUNREACH',
32
+ 'EAI_AGAIN'
33
+ ];
34
+
35
+ /** 可重试的 HTTP 状态码 */
36
+ const RETRYABLE_STATUS_CODES = [502, 503, 504, 429];
37
+
38
+ export class F2ANetworkClient {
39
+ private baseUrl: string;
40
+ private token: string;
41
+ private timeoutMs: number;
42
+ private maxRetries: number;
43
+ private baseDelayMs: number;
44
+
45
+ constructor(config: F2ANodeConfig) {
46
+ this.baseUrl = `http://localhost:${config.controlPort}`;
47
+ this.token = config.controlToken;
48
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
49
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
50
+ this.baseDelayMs = config.retryDelayMs ?? DEFAULT_BASE_DELAY_MS;
51
+ }
52
+
53
+ /**
54
+ * 判断错误是否可重试
55
+ */
56
+ private isRetryableError(error: unknown): boolean {
57
+ if (error instanceof Error) {
58
+ // 网络错误
59
+ if (RETRYABLE_ERROR_CODES.some(code => error.message.includes(code))) {
60
+ return true;
61
+ }
62
+ // 超时错误
63
+ if (error.name === 'AbortError' || error.message.includes('timeout')) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * 计算重试延迟(指数退避 + 抖动)
72
+ */
73
+ private calculateDelay(attempt: number): number {
74
+ const exponentialDelay = this.baseDelayMs * Math.pow(2, attempt);
75
+ const jitter = Math.random() * this.baseDelayMs * 0.5;
76
+ return Math.min(exponentialDelay + jitter, 30000); // 最大 30 秒
77
+ }
78
+
79
+ /**
80
+ * 延迟函数
81
+ */
82
+ private delay(ms: number): Promise<void> {
83
+ return new Promise(resolve => setTimeout(resolve, ms));
84
+ }
85
+
86
+ private async request<T>(
87
+ method: string,
88
+ path: string,
89
+ body?: unknown
90
+ ): Promise<Result<T>> {
91
+ // P1 修复:初始化 lastError 为有意义的默认值,避免 null 问题
92
+ let lastError: Error = new Error('Request failed before any attempt');
93
+
94
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
95
+ // 使用 AbortController 设置超时
96
+ const controller = new AbortController();
97
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
98
+
99
+ try {
100
+ const response = await fetch(`${this.baseUrl}${path}`, {
101
+ method,
102
+ headers: {
103
+ 'Authorization': `Bearer ${this.token}`,
104
+ 'Content-Type': 'application/json'
105
+ },
106
+ body: body ? JSON.stringify(body) : undefined,
107
+ signal: controller.signal
108
+ });
109
+
110
+ if (!response.ok) {
111
+ const errorText = await response.text();
112
+
113
+ // 检查是否是可重试的 HTTP 错误
114
+ if (RETRYABLE_STATUS_CODES.includes(response.status) && attempt < this.maxRetries) {
115
+ lastError = new Error(`HTTP ${response.status}: ${errorText}`);
116
+ const delayMs = this.calculateDelay(attempt);
117
+ logger.info(`Retrying request to ${path} after ${delayMs}ms (attempt ${attempt + 1}/${this.maxRetries})`);
118
+ clearTimeout(timeoutId);
119
+ await this.delay(delayMs);
120
+ continue;
121
+ }
122
+
123
+ return failure(createError(
124
+ 'CONNECTION_FAILED',
125
+ `HTTP ${response.status}: ${errorText}`
126
+ ));
127
+ }
128
+
129
+ const data = await response.json() as T;
130
+ return success(data);
131
+
132
+ } catch (error) {
133
+ // 处理超时错误
134
+ if (error instanceof Error && error.name === 'AbortError') {
135
+ if (attempt < this.maxRetries) {
136
+ const delayMs = this.calculateDelay(attempt);
137
+ logger.info(`Retrying request to ${path} after timeout (${delayMs}ms, attempt ${attempt + 1}/${this.maxRetries})`);
138
+ clearTimeout(timeoutId);
139
+ await this.delay(delayMs);
140
+ continue;
141
+ }
142
+ // 所有重试都因超时失败
143
+ return failure(createError(
144
+ 'CONNECTION_FAILED',
145
+ `Request timed out after ${this.timeoutMs}ms`
146
+ ));
147
+ }
148
+
149
+ // 检查是否可重试
150
+ if (this.isRetryableError(error) && attempt < this.maxRetries) {
151
+ lastError = error instanceof Error ? error : new Error(String(error));
152
+ const delayMs = this.calculateDelay(attempt);
153
+ logger.info(`Retrying request to ${path} after ${delayMs}ms (attempt ${attempt + 1}/${this.maxRetries})`);
154
+ clearTimeout(timeoutId);
155
+ await this.delay(delayMs);
156
+ continue;
157
+ }
158
+
159
+ // P1 修复:确保返回有意义的错误信息
160
+ const errorMessage = error instanceof Error ? error.message : String(error);
161
+ lastError = error instanceof Error ? error : new Error(errorMessage);
162
+
163
+ return failure(createError(
164
+ 'CONNECTION_FAILED',
165
+ errorMessage
166
+ ));
167
+ } finally {
168
+ clearTimeout(timeoutId);
169
+ }
170
+ }
171
+
172
+ // 所有重试都失败了
173
+ // P1 修复:lastError 此时一定有值,不再需要 optional chaining
174
+ return failure(createError(
175
+ 'CONNECTION_FAILED',
176
+ lastError.message
177
+ ));
178
+ }
179
+
180
+ /**
181
+ * 发现网络中的 Agents
182
+ */
183
+ async discoverAgents(capability?: string): Promise<Result<AgentInfo[]>> {
184
+ return this.request<AgentInfo[]>('POST', '/discover', { capability });
185
+ }
186
+
187
+ /**
188
+ * 获取已连接的 Peers
189
+ */
190
+ async getConnectedPeers(): Promise<Result<PeerInfo[]>> {
191
+ return this.request<PeerInfo[]>('GET', '/peers');
192
+ }
193
+
194
+ /**
195
+ * 委托任务给特定 Peer
196
+ */
197
+ async delegateTask(options: DelegateOptions): Promise<Result<unknown>> {
198
+ return this.request<unknown>('POST', '/delegate', options);
199
+ }
200
+
201
+ /**
202
+ * 发送任务响应
203
+ */
204
+ async sendTaskResponse(
205
+ peerId: string,
206
+ response: TaskResponse
207
+ ): Promise<Result<void>> {
208
+ return this.request<void>('POST', '/task/response', {
209
+ peerId,
210
+ ...response
211
+ });
212
+ }
213
+
214
+ /**
215
+ * 注册 Webhook
216
+ */
217
+ async registerWebhook(webhookUrl: string): Promise<Result<void>> {
218
+ return this.request<void>('POST', '/webhook/register', {
219
+ url: webhookUrl,
220
+ events: ['discover', 'delegate', 'status']
221
+ });
222
+ }
223
+
224
+ /**
225
+ * 更新 Agent 信息
226
+ */
227
+ async updateAgentInfo(agentInfo: Partial<AgentInfo>): Promise<Result<void>> {
228
+ return this.request<void>('POST', '/agent/update', agentInfo);
229
+ }
230
+
231
+ /**
232
+ * 获取待处理任务
233
+ */
234
+ async getPendingTasks(): Promise<Result<TaskRequest[]>> {
235
+ return this.request<TaskRequest[]>('GET', '/tasks/pending');
236
+ }
237
+
238
+ /**
239
+ * 确认连接请求
240
+ */
241
+ async confirmConnection(peerId: string): Promise<Result<void>> {
242
+ return this.request<void>('POST', '/connection/confirm', { peerId });
243
+ }
244
+
245
+ /**
246
+ * 拒绝连接请求
247
+ */
248
+ async rejectConnection(peerId: string, reason?: string): Promise<Result<void>> {
249
+ return this.request<void>('POST', '/connection/reject', { peerId, reason });
250
+ }
251
+ }