@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.
- package/.github/workflows/ci.yml +113 -0
- package/.github/workflows/publish.yml +60 -0
- package/LICENSE +21 -0
- package/MONOREPO.md +58 -0
- package/README.md +280 -0
- package/SKILL.md +137 -0
- package/dist/adapters/openclaw.d.ts +103 -0
- package/dist/adapters/openclaw.d.ts.map +1 -0
- package/dist/adapters/openclaw.js +297 -0
- package/dist/adapters/openclaw.js.map +1 -0
- package/dist/cli/commands.d.ts +17 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +107 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +203 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/autonomous-economy.d.ts +136 -0
- package/dist/core/autonomous-economy.d.ts.map +1 -0
- package/dist/core/autonomous-economy.js +255 -0
- package/dist/core/autonomous-economy.js.map +1 -0
- package/dist/core/connection-manager.d.ts +80 -0
- package/dist/core/connection-manager.d.ts.map +1 -0
- package/dist/core/connection-manager.js +235 -0
- package/dist/core/connection-manager.js.map +1 -0
- package/dist/core/connection-manager.test.d.ts +2 -0
- package/dist/core/connection-manager.test.d.ts.map +1 -0
- package/dist/core/connection-manager.test.js +52 -0
- package/dist/core/connection-manager.test.js.map +1 -0
- package/dist/core/e2ee-crypto.d.ts +90 -0
- package/dist/core/e2ee-crypto.d.ts.map +1 -0
- package/dist/core/e2ee-crypto.js +190 -0
- package/dist/core/e2ee-crypto.js.map +1 -0
- package/dist/core/f2a.d.ts +126 -0
- package/dist/core/f2a.d.ts.map +1 -0
- package/dist/core/f2a.js +425 -0
- package/dist/core/f2a.js.map +1 -0
- package/dist/core/identity.d.ts +47 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +130 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/identity.test.d.ts +2 -0
- package/dist/core/identity.test.d.ts.map +1 -0
- package/dist/core/identity.test.js +43 -0
- package/dist/core/identity.test.js.map +1 -0
- package/dist/core/p2p-network.d.ts +242 -0
- package/dist/core/p2p-network.d.ts.map +1 -0
- package/dist/core/p2p-network.js +1182 -0
- package/dist/core/p2p-network.js.map +1 -0
- package/dist/core/reputation-security.d.ts +168 -0
- package/dist/core/reputation-security.d.ts.map +1 -0
- package/dist/core/reputation-security.js +369 -0
- package/dist/core/reputation-security.js.map +1 -0
- package/dist/core/reputation.d.ts +179 -0
- package/dist/core/reputation.d.ts.map +1 -0
- package/dist/core/reputation.js +472 -0
- package/dist/core/reputation.js.map +1 -0
- package/dist/core/review-committee.d.ts +130 -0
- package/dist/core/review-committee.d.ts.map +1 -0
- package/dist/core/review-committee.js +251 -0
- package/dist/core/review-committee.js.map +1 -0
- package/dist/core/serverless.d.ts +155 -0
- package/dist/core/serverless.d.ts.map +1 -0
- package/dist/core/serverless.js +615 -0
- package/dist/core/serverless.js.map +1 -0
- package/dist/core/token-manager.d.ts +42 -0
- package/dist/core/token-manager.d.ts.map +1 -0
- package/dist/core/token-manager.js +122 -0
- package/dist/core/token-manager.js.map +1 -0
- package/dist/daemon/control-server.d.ts +55 -0
- package/dist/daemon/control-server.d.ts.map +1 -0
- package/dist/daemon/control-server.js +262 -0
- package/dist/daemon/control-server.js.map +1 -0
- package/dist/daemon/index.d.ts +35 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +69 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/main.d.ts +6 -0
- package/dist/daemon/main.d.ts.map +1 -0
- package/dist/daemon/main.js +38 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/start.d.ts +6 -0
- package/dist/daemon/start.d.ts.map +1 -0
- package/dist/daemon/start.js +25 -0
- package/dist/daemon/start.js.map +1 -0
- package/dist/daemon/webhook.d.ts +30 -0
- package/dist/daemon/webhook.d.ts.map +1 -0
- package/dist/daemon/webhook.js +86 -0
- package/dist/daemon/webhook.js.map +1 -0
- package/dist/daemon/webhook.test.d.ts +2 -0
- package/dist/daemon/webhook.test.d.ts.map +1 -0
- package/dist/daemon/webhook.test.js +24 -0
- package/dist/daemon/webhook.test.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/messages.d.ts +739 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +188 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol/messages.test.d.ts +2 -0
- package/dist/protocol/messages.test.d.ts.map +1 -0
- package/dist/protocol/messages.test.js +55 -0
- package/dist/protocol/messages.test.js.map +1 -0
- package/dist/types/index.d.ts +247 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/result.d.ts +28 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +16 -0
- package/dist/types/result.js.map +1 -0
- package/dist/utils/benchmark.d.ts +67 -0
- package/dist/utils/benchmark.d.ts.map +1 -0
- package/dist/utils/benchmark.js +179 -0
- package/dist/utils/benchmark.js.map +1 -0
- package/dist/utils/logger.d.ts +105 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +275 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/middleware.d.ts +85 -0
- package/dist/utils/middleware.d.ts.map +1 -0
- package/dist/utils/middleware.js +173 -0
- package/dist/utils/middleware.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +71 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +160 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/signature.d.ts +57 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +102 -0
- package/dist/utils/signature.js.map +1 -0
- package/dist/utils/validation.d.ts +504 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +159 -0
- package/dist/utils/validation.js.map +1 -0
- package/docs/F2A-PROTOCOL.md +61 -0
- package/docs/MOBILE_BOOTSTRAP_DESIGN.md +126 -0
- package/docs/a2a-lessons.md +316 -0
- package/docs/middleware-guide.md +448 -0
- package/docs/readme-update-checklist.md +90 -0
- package/docs/reputation-guide.md +396 -0
- package/docs/rfcs/001-reputation-system.md +712 -0
- package/docs/security-design.md +247 -0
- package/install.sh +231 -0
- package/package.json +64 -0
- package/packages/openclaw-adapter/README.md +510 -0
- package/packages/openclaw-adapter/openclaw.plugin.json +106 -0
- package/packages/openclaw-adapter/package.json +40 -0
- package/packages/openclaw-adapter/src/announcement-queue.test.ts +449 -0
- package/packages/openclaw-adapter/src/announcement-queue.ts +403 -0
- package/packages/openclaw-adapter/src/capability-detector.test.ts +99 -0
- package/packages/openclaw-adapter/src/capability-detector.ts +183 -0
- package/packages/openclaw-adapter/src/claim-handlers.test.ts +974 -0
- package/packages/openclaw-adapter/src/claim-handlers.ts +482 -0
- package/packages/openclaw-adapter/src/connector.business.test.ts +583 -0
- package/packages/openclaw-adapter/src/connector.ts +795 -0
- package/packages/openclaw-adapter/src/index.test.ts +82 -0
- package/packages/openclaw-adapter/src/index.ts +18 -0
- package/packages/openclaw-adapter/src/integration.e2e.test.ts +829 -0
- package/packages/openclaw-adapter/src/logger.ts +51 -0
- package/packages/openclaw-adapter/src/network-client.test.ts +266 -0
- package/packages/openclaw-adapter/src/network-client.ts +251 -0
- package/packages/openclaw-adapter/src/network-recovery.test.ts +465 -0
- package/packages/openclaw-adapter/src/node-manager.test.ts +136 -0
- package/packages/openclaw-adapter/src/node-manager.ts +429 -0
- package/packages/openclaw-adapter/src/plugin.test.ts +439 -0
- package/packages/openclaw-adapter/src/plugin.ts +104 -0
- package/packages/openclaw-adapter/src/reputation.test.ts +221 -0
- package/packages/openclaw-adapter/src/reputation.ts +368 -0
- package/packages/openclaw-adapter/src/task-guard.test.ts +502 -0
- package/packages/openclaw-adapter/src/task-guard.ts +860 -0
- package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +462 -0
- package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +284 -0
- package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +408 -0
- package/packages/openclaw-adapter/src/task-queue.ts +668 -0
- package/packages/openclaw-adapter/src/tool-handlers.test.ts +906 -0
- package/packages/openclaw-adapter/src/tool-handlers.ts +574 -0
- package/packages/openclaw-adapter/src/types.ts +361 -0
- package/packages/openclaw-adapter/src/webhook-pusher.test.ts +188 -0
- package/packages/openclaw-adapter/src/webhook-pusher.ts +220 -0
- package/packages/openclaw-adapter/src/webhook-server.test.ts +580 -0
- package/packages/openclaw-adapter/src/webhook-server.ts +202 -0
- package/packages/openclaw-adapter/tsconfig.json +20 -0
- package/src/cli/commands.test.ts +157 -0
- package/src/cli/commands.ts +129 -0
- package/src/cli/index.test.ts +77 -0
- package/src/cli/index.ts +234 -0
- package/src/core/autonomous-economy.test.ts +291 -0
- package/src/core/autonomous-economy.ts +428 -0
- package/src/core/e2ee-crypto.test.ts +125 -0
- package/src/core/e2ee-crypto.ts +246 -0
- package/src/core/f2a.test.ts +269 -0
- package/src/core/f2a.ts +618 -0
- package/src/core/p2p-network.test.ts +199 -0
- package/src/core/p2p-network.ts +1432 -0
- package/src/core/reputation-security.test.ts +403 -0
- package/src/core/reputation-security.ts +562 -0
- package/src/core/reputation.test.ts +260 -0
- package/src/core/reputation.ts +576 -0
- package/src/core/review-committee.test.ts +380 -0
- package/src/core/review-committee.ts +401 -0
- package/src/core/token-manager.test.ts +133 -0
- package/src/core/token-manager.ts +140 -0
- package/src/daemon/control-server.test.ts +216 -0
- package/src/daemon/control-server.ts +292 -0
- package/src/daemon/index.test.ts +85 -0
- package/src/daemon/index.ts +89 -0
- package/src/daemon/main.ts +44 -0
- package/src/daemon/start.ts +29 -0
- package/src/daemon/webhook.test.ts +68 -0
- package/src/daemon/webhook.ts +105 -0
- package/src/index.test.ts +436 -0
- package/src/index.ts +72 -0
- package/src/types/index.test.ts +87 -0
- package/src/types/index.ts +341 -0
- package/src/types/result.ts +68 -0
- package/src/utils/benchmark.ts +237 -0
- package/src/utils/logger.ts +331 -0
- package/src/utils/middleware.ts +229 -0
- package/src/utils/rate-limiter.ts +207 -0
- package/src/utils/signature.ts +136 -0
- package/src/utils/validation.ts +186 -0
- package/tests/docker/Dockerfile.node +23 -0
- package/tests/docker/Dockerfile.runner +18 -0
- package/tests/docker/docker-compose.test.yml +73 -0
- package/tests/integration/message-passing.test.ts +109 -0
- package/tests/integration/multi-node.test.ts +92 -0
- package/tests/integration/p2p-connection.test.ts +83 -0
- package/tests/integration/test-config.ts +32 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F2A OpenClaw Plugin - 单元测试
|
|
3
|
+
* 测试插件注册和工具执行逻辑
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import type { OpenClawPluginApi } from './types.js';
|
|
8
|
+
|
|
9
|
+
// 使用 vi.hoisted 在 mock 之前初始化所有需要的变量
|
|
10
|
+
const { mockTools, mockInitialize, mockShutdown, createMockTools } = vi.hoisted(() => {
|
|
11
|
+
// 在 hoisted 作用域内定义工具创建函数
|
|
12
|
+
const createMockTools = () => {
|
|
13
|
+
const tools = [
|
|
14
|
+
{
|
|
15
|
+
name: 'f2a_discover',
|
|
16
|
+
description: '发现 F2A 网络中的 Agents',
|
|
17
|
+
parameters: {
|
|
18
|
+
capability: { type: 'string', description: '按能力过滤' }
|
|
19
|
+
},
|
|
20
|
+
handler: vi.fn(async (params: any) => {
|
|
21
|
+
return { content: '发现 2 个 Agents', data: { count: 2 } };
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'f2a_delegate',
|
|
26
|
+
description: '委托任务给网络中的特定 Agent',
|
|
27
|
+
parameters: {
|
|
28
|
+
agent: { type: 'string', description: '目标 Agent', required: true },
|
|
29
|
+
task: { type: 'string', description: '任务描述', required: true }
|
|
30
|
+
},
|
|
31
|
+
handler: vi.fn(async (params: any) => {
|
|
32
|
+
return { content: '任务已完成', data: { taskId: 'task-123' } };
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'f2a_status',
|
|
37
|
+
description: '查看 F2A 网络状态',
|
|
38
|
+
parameters: {},
|
|
39
|
+
handler: vi.fn(async () => {
|
|
40
|
+
return { content: 'F2A 状态: 运行中' };
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
return tools;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mockTools = createMockTools();
|
|
48
|
+
const mockInitialize = vi.fn();
|
|
49
|
+
const mockShutdown = vi.fn();
|
|
50
|
+
|
|
51
|
+
return { mockTools, mockInitialize, mockShutdown, createMockTools };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
vi.mock('./connector.js', () => {
|
|
55
|
+
const adapter = {
|
|
56
|
+
initialize: mockInitialize,
|
|
57
|
+
shutdown: mockShutdown,
|
|
58
|
+
getTools: vi.fn(() => mockTools),
|
|
59
|
+
_getMockTools: () => mockTools,
|
|
60
|
+
_resetMockTools: () => {
|
|
61
|
+
const newTools = createMockTools();
|
|
62
|
+
// 复制新工具到 mockTools 数组
|
|
63
|
+
mockTools.length = 0;
|
|
64
|
+
mockTools.push(...newTools);
|
|
65
|
+
return mockTools;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
F2AOpenClawAdapter: vi.fn(() => adapter)
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 导入 mock 后的模块
|
|
75
|
+
import { F2AOpenClawAdapter } from './connector.js';
|
|
76
|
+
import register from './plugin.js';
|
|
77
|
+
|
|
78
|
+
describe('Plugin register 函数', () => {
|
|
79
|
+
let mockApi: OpenClawPluginApi;
|
|
80
|
+
let mockAdapter: any;
|
|
81
|
+
let registeredTools: any[];
|
|
82
|
+
let registeredServices: any[];
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
vi.clearAllMocks();
|
|
86
|
+
registeredTools = [];
|
|
87
|
+
registeredServices = [];
|
|
88
|
+
|
|
89
|
+
// 重置 mock tools 到初始状态
|
|
90
|
+
mockTools.length = 0;
|
|
91
|
+
const freshTools = createMockTools();
|
|
92
|
+
mockTools.push(...freshTools);
|
|
93
|
+
|
|
94
|
+
// 重置 mock adapter
|
|
95
|
+
mockAdapter = new F2AOpenClawAdapter();
|
|
96
|
+
|
|
97
|
+
// 创建 mock API
|
|
98
|
+
mockApi = {
|
|
99
|
+
id: 'test-api-id',
|
|
100
|
+
name: 'test-plugin',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
description: 'Test plugin',
|
|
103
|
+
source: 'test',
|
|
104
|
+
config: {
|
|
105
|
+
plugins: {
|
|
106
|
+
entries: {
|
|
107
|
+
'f2a-openclaw-adapter': {
|
|
108
|
+
config: {
|
|
109
|
+
agentName: 'Test Agent',
|
|
110
|
+
autoStart: false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
agents: {
|
|
116
|
+
defaults: {
|
|
117
|
+
workspace: '/tmp/test-workspace'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
runtime: {
|
|
122
|
+
version: '1.0.0',
|
|
123
|
+
config: {
|
|
124
|
+
loadConfig: vi.fn(),
|
|
125
|
+
writeConfigFile: vi.fn()
|
|
126
|
+
},
|
|
127
|
+
system: {
|
|
128
|
+
enqueueSystemEvent: vi.fn(),
|
|
129
|
+
requestHeartbeatNow: vi.fn(),
|
|
130
|
+
runCommandWithTimeout: vi.fn()
|
|
131
|
+
},
|
|
132
|
+
media: {
|
|
133
|
+
loadWebMedia: vi.fn(),
|
|
134
|
+
detectMime: vi.fn()
|
|
135
|
+
},
|
|
136
|
+
tts: {
|
|
137
|
+
textToSpeechTelephony: vi.fn()
|
|
138
|
+
},
|
|
139
|
+
stt: {
|
|
140
|
+
transcribeAudioFile: vi.fn()
|
|
141
|
+
},
|
|
142
|
+
logging: {
|
|
143
|
+
shouldLogVerbose: vi.fn(),
|
|
144
|
+
getChildLogger: vi.fn()
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
logger: {
|
|
148
|
+
debug: vi.fn(),
|
|
149
|
+
info: vi.fn(),
|
|
150
|
+
warn: vi.fn(),
|
|
151
|
+
error: vi.fn()
|
|
152
|
+
},
|
|
153
|
+
registerTool: vi.fn((tool) => {
|
|
154
|
+
registeredTools.push(tool);
|
|
155
|
+
}),
|
|
156
|
+
registerService: vi.fn((service) => {
|
|
157
|
+
registeredServices.push(service);
|
|
158
|
+
})
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
vi.restoreAllMocks();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('初始化成功场景', () => {
|
|
167
|
+
it('应该成功初始化插件并记录日志', async () => {
|
|
168
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
169
|
+
|
|
170
|
+
await register(mockApi);
|
|
171
|
+
|
|
172
|
+
expect(mockInitialize).toHaveBeenCalled();
|
|
173
|
+
expect(mockApi.logger?.info).toHaveBeenCalledWith('[F2A Adapter] 初始化完成');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('应该从配置中提取插件配置并传递给 adapter', async () => {
|
|
177
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
178
|
+
|
|
179
|
+
await register(mockApi);
|
|
180
|
+
|
|
181
|
+
// 验证传递给 initialize 的配置包含 _api
|
|
182
|
+
const initConfig = mockInitialize.mock.calls[0][0];
|
|
183
|
+
expect(initConfig._api).toBe(mockApi);
|
|
184
|
+
expect(initConfig.agentName).toBe('Test Agent');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('应该处理空配置(使用默认值)', async () => {
|
|
188
|
+
mockApi.config = {};
|
|
189
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
190
|
+
|
|
191
|
+
await register(mockApi);
|
|
192
|
+
|
|
193
|
+
expect(mockInitialize).toHaveBeenCalled();
|
|
194
|
+
const initConfig = mockInitialize.mock.calls[0][0];
|
|
195
|
+
expect(initConfig._api).toBe(mockApi);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('初始化失败时的资源清理', () => {
|
|
200
|
+
it('应该在初始化失败时调用 shutdown 清理资源', async () => {
|
|
201
|
+
const initError = new Error('初始化失败');
|
|
202
|
+
mockInitialize.mockRejectedValue(initError);
|
|
203
|
+
mockShutdown.mockResolvedValue(undefined);
|
|
204
|
+
|
|
205
|
+
await expect(register(mockApi)).rejects.toThrow('F2A Adapter 初始化失败: 初始化失败');
|
|
206
|
+
|
|
207
|
+
expect(mockShutdown).toHaveBeenCalled();
|
|
208
|
+
expect(mockApi.logger?.error).toHaveBeenCalledWith('[F2A Adapter] 初始化失败: 初始化失败');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('应该在清理资源失败时记录警告', async () => {
|
|
212
|
+
const initError = new Error('初始化失败');
|
|
213
|
+
const shutdownError = new Error('清理失败');
|
|
214
|
+
mockInitialize.mockRejectedValue(initError);
|
|
215
|
+
mockShutdown.mockRejectedValue(shutdownError);
|
|
216
|
+
|
|
217
|
+
await expect(register(mockApi)).rejects.toThrow('F2A Adapter 初始化失败: 初始化失败');
|
|
218
|
+
|
|
219
|
+
expect(mockApi.logger?.warn).toHaveBeenCalledWith('[F2A Adapter] 清理资源时出错: 清理失败');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('初始化失败时不应该注册任何工具或服务', async () => {
|
|
223
|
+
mockInitialize.mockRejectedValue(new Error('初始化失败'));
|
|
224
|
+
mockShutdown.mockResolvedValue(undefined);
|
|
225
|
+
|
|
226
|
+
await expect(register(mockApi)).rejects.toThrow();
|
|
227
|
+
|
|
228
|
+
expect(registeredTools.length).toBe(0);
|
|
229
|
+
expect(registeredServices.length).toBe(0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('工具注册', () => {
|
|
234
|
+
it('应该注册所有工具', async () => {
|
|
235
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
236
|
+
|
|
237
|
+
await register(mockApi);
|
|
238
|
+
|
|
239
|
+
expect(mockApi.registerTool).toHaveBeenCalled();
|
|
240
|
+
// getTools 返回 3 个工具
|
|
241
|
+
expect(registeredTools.length).toBe(3);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('工具应该包含正确的属性', async () => {
|
|
245
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
246
|
+
|
|
247
|
+
await register(mockApi);
|
|
248
|
+
|
|
249
|
+
const tool = registeredTools[0];
|
|
250
|
+
expect(tool.name).toBe('f2a_discover');
|
|
251
|
+
expect(tool.description).toBe('发现 F2A 网络中的 Agents');
|
|
252
|
+
expect(tool.parameters).toBeDefined();
|
|
253
|
+
expect(typeof tool.execute).toBe('function');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('应该记录注册的工具数量', async () => {
|
|
257
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
258
|
+
|
|
259
|
+
await register(mockApi);
|
|
260
|
+
|
|
261
|
+
expect(mockApi.logger?.info).toHaveBeenCalledWith('[F2A Adapter] 已注册 3 个工具');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('服务注册', () => {
|
|
266
|
+
it('应该注册后台服务', async () => {
|
|
267
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
268
|
+
|
|
269
|
+
await register(mockApi);
|
|
270
|
+
|
|
271
|
+
expect(mockApi.registerService).toHaveBeenCalled();
|
|
272
|
+
expect(registeredServices.length).toBe(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('服务应该包含正确的 id', async () => {
|
|
276
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
277
|
+
|
|
278
|
+
await register(mockApi);
|
|
279
|
+
|
|
280
|
+
const service = registeredServices[0];
|
|
281
|
+
expect(service.id).toBe('f2a-adapter-service');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('服务 start 应该记录日志', async () => {
|
|
285
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
286
|
+
|
|
287
|
+
await register(mockApi);
|
|
288
|
+
|
|
289
|
+
const service = registeredServices[0];
|
|
290
|
+
service.start();
|
|
291
|
+
|
|
292
|
+
expect(mockApi.logger?.info).toHaveBeenCalledWith('[F2A Adapter] 服务已启动');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('服务 stop 应该调用 shutdown', async () => {
|
|
296
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
297
|
+
mockShutdown.mockResolvedValue(undefined);
|
|
298
|
+
|
|
299
|
+
await register(mockApi);
|
|
300
|
+
|
|
301
|
+
const service = registeredServices[0];
|
|
302
|
+
await service.stop();
|
|
303
|
+
|
|
304
|
+
expect(mockShutdown).toHaveBeenCalled();
|
|
305
|
+
expect(mockApi.logger?.info).toHaveBeenCalledWith('[F2A Adapter] 正在停止服务...');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('工具执行测试', () => {
|
|
310
|
+
it('execute 应该调用 tool.handler 并返回正确格式', async () => {
|
|
311
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
312
|
+
|
|
313
|
+
await register(mockApi);
|
|
314
|
+
|
|
315
|
+
const tool = registeredTools[0]; // f2a_discover
|
|
316
|
+
|
|
317
|
+
// 执行工具
|
|
318
|
+
const result = await tool.execute('session-123', { capability: 'code-generation' });
|
|
319
|
+
|
|
320
|
+
// 验证返回格式
|
|
321
|
+
expect(result).toHaveProperty('content');
|
|
322
|
+
expect(result.content).toBeInstanceOf(Array);
|
|
323
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
324
|
+
expect(result.content[0]).toHaveProperty('text');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('execute 应该正确处理字符串返回值', async () => {
|
|
328
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
329
|
+
|
|
330
|
+
await register(mockApi);
|
|
331
|
+
|
|
332
|
+
const tool = registeredTools[2]; // f2a_status (返回字符串)
|
|
333
|
+
|
|
334
|
+
const result = await tool.execute('session-123', {});
|
|
335
|
+
|
|
336
|
+
expect(result.content[0].type).toBe('text');
|
|
337
|
+
expect(result.content[0].text).toContain('F2A 状态');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('execute 应该正确处理带 content 的对象返回值', async () => {
|
|
341
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
342
|
+
|
|
343
|
+
await register(mockApi);
|
|
344
|
+
|
|
345
|
+
const tool = registeredTools[0]; // f2a_discover
|
|
346
|
+
|
|
347
|
+
const result = await tool.execute('session-123', {});
|
|
348
|
+
|
|
349
|
+
// handler 返回 { content: '发现 2 个 Agents', data: { count: 2 } }
|
|
350
|
+
expect(result.content[0].text).toBe('发现 2 个 Agents');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('execute 应该正确处理其他对象返回值(JSON 序列化)', async () => {
|
|
354
|
+
// 重置工具并使用自定义 handler
|
|
355
|
+
mockAdapter._resetMockTools();
|
|
356
|
+
mockTools[0].handler = vi.fn(async () => ({ data: { count: 5 }, status: 'ok' }));
|
|
357
|
+
|
|
358
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
359
|
+
|
|
360
|
+
await register(mockApi);
|
|
361
|
+
|
|
362
|
+
const tool = registeredTools[0];
|
|
363
|
+
const result = await tool.execute('session-123', {});
|
|
364
|
+
|
|
365
|
+
// JSON.stringify 不保留空格
|
|
366
|
+
expect(result.content[0].text).toContain('"count":5');
|
|
367
|
+
expect(result.content[0].text).toContain('"status":"ok"');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('execute 应该传递正确的 mockContext 给 handler', async () => {
|
|
371
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
372
|
+
|
|
373
|
+
// 获取 handler 的引用(在 register 之前)
|
|
374
|
+
const handlerSpy = mockTools[0].handler;
|
|
375
|
+
|
|
376
|
+
await register(mockApi);
|
|
377
|
+
|
|
378
|
+
const tool = registeredTools[0];
|
|
379
|
+
await tool.execute('my-session-id', { capability: 'test' });
|
|
380
|
+
|
|
381
|
+
// 验证 handler 被调用
|
|
382
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
383
|
+
|
|
384
|
+
// 验证 context 参数
|
|
385
|
+
const context = handlerSpy.mock.calls[0][1];
|
|
386
|
+
expect(context.sessionId).toBe('my-session-id');
|
|
387
|
+
expect(context.workspace).toBe('/tmp/test-workspace');
|
|
388
|
+
expect(typeof context.toJSON).toBe('function');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('execute 错误处理:应该记录错误并重新抛出', async () => {
|
|
392
|
+
const testError = new Error('工具执行失败');
|
|
393
|
+
|
|
394
|
+
// 重置工具并使用会抛出错误的 handler
|
|
395
|
+
mockAdapter._resetMockTools();
|
|
396
|
+
mockTools[0].name = 'failing_tool';
|
|
397
|
+
mockTools[0].handler = vi.fn(async () => {
|
|
398
|
+
throw testError;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
402
|
+
|
|
403
|
+
await register(mockApi);
|
|
404
|
+
|
|
405
|
+
const tool = registeredTools[0];
|
|
406
|
+
|
|
407
|
+
await expect(tool.execute('session-123', {})).rejects.toThrow('工具执行失败');
|
|
408
|
+
|
|
409
|
+
expect(mockApi.logger?.error).toHaveBeenCalledWith(
|
|
410
|
+
'[F2A Adapter] 工具 failing_tool 执行失败: 工具执行失败'
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('execute 应该使用默认 workspace 当配置中没有指定时', async () => {
|
|
415
|
+
// 清空 workspace 配置
|
|
416
|
+
mockApi.config = {
|
|
417
|
+
plugins: {
|
|
418
|
+
entries: {
|
|
419
|
+
'f2a-openclaw-adapter': { config: { autoStart: false } }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
mockInitialize.mockResolvedValue(undefined);
|
|
424
|
+
|
|
425
|
+
// 获取 handler 的引用(在 register 之前)
|
|
426
|
+
const handlerSpy = mockTools[0].handler;
|
|
427
|
+
|
|
428
|
+
await register(mockApi);
|
|
429
|
+
|
|
430
|
+
const tool = registeredTools[0];
|
|
431
|
+
await tool.execute('session-123', {});
|
|
432
|
+
|
|
433
|
+
// 验证 handler 被调用且 workspace 默认为 '.'
|
|
434
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
435
|
+
const context = handlerSpy.mock.calls[0][1];
|
|
436
|
+
expect(context.workspace).toBe('.');
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F2A OpenClaw Adapter Plugin
|
|
3
|
+
* OpenClaw 插件标准入口
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OpenClawPluginApi } from './types.js';
|
|
7
|
+
import { F2AOpenClawAdapter } from './connector.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* OpenClaw 插件注册函数
|
|
11
|
+
* 这是 OpenClaw 加载插件时调用的入口
|
|
12
|
+
*/
|
|
13
|
+
export default async function register(api: OpenClawPluginApi) {
|
|
14
|
+
const plugin = new F2AOpenClawAdapter();
|
|
15
|
+
|
|
16
|
+
// 从 OpenClaw 配置中获取插件配置
|
|
17
|
+
const pluginsConfig = api.config.plugins;
|
|
18
|
+
const config = pluginsConfig?.entries?.['f2a-openclaw-adapter']?.config || {};
|
|
19
|
+
|
|
20
|
+
// 将 API 引用传递给插件(用于触发心跳等操作)
|
|
21
|
+
const fullConfig = {
|
|
22
|
+
...config,
|
|
23
|
+
_api: api
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// 初始化插件 - 等待完成后再注册工具
|
|
27
|
+
try {
|
|
28
|
+
await plugin.initialize(fullConfig);
|
|
29
|
+
api.logger?.info('[F2A Adapter] 初始化完成');
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
api.logger?.error(`[F2A Adapter] 初始化失败: ${error.message}`);
|
|
32
|
+
|
|
33
|
+
// 清理已分配的资源,避免孤儿进程和端口占用
|
|
34
|
+
try {
|
|
35
|
+
api.logger?.info('[F2A Adapter] 正在清理资源...');
|
|
36
|
+
await plugin.shutdown?.();
|
|
37
|
+
} catch (shutdownError: any) {
|
|
38
|
+
api.logger?.warn(`[F2A Adapter] 清理资源时出错: ${shutdownError.message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 抛出错误让 OpenClaw 知道插件加载失败
|
|
42
|
+
throw new Error(`F2A Adapter 初始化失败: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 初始化完成后注册所有工具
|
|
46
|
+
// OpenClaw 的 registerTool 需要 execute 方法
|
|
47
|
+
const tools = plugin.getTools();
|
|
48
|
+
for (const tool of tools) {
|
|
49
|
+
api.registerTool?.({
|
|
50
|
+
name: tool.name,
|
|
51
|
+
description: tool.description,
|
|
52
|
+
parameters: tool.parameters,
|
|
53
|
+
// OpenClaw 使用 execute 而不是 handler
|
|
54
|
+
async execute(_id: string, params: unknown) {
|
|
55
|
+
try {
|
|
56
|
+
// 构造一个模拟的 SessionContext
|
|
57
|
+
const workspace = api.config.agents?.defaults?.workspace || '.';
|
|
58
|
+
const mockContext = {
|
|
59
|
+
sessionId: _id,
|
|
60
|
+
workspace,
|
|
61
|
+
toJSON: () => ({})
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = await tool.handler(params as Record<string, unknown>, mockContext);
|
|
65
|
+
|
|
66
|
+
// 将 ToolResult 转换为 OpenClaw 期望的格式
|
|
67
|
+
if (typeof result === 'string') {
|
|
68
|
+
return { content: [{ type: 'text', text: result }] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result?.content) {
|
|
72
|
+
return { content: [{ type: 'text', text: result.content }] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
api.logger?.error(`[F2A Adapter] 工具 ${tool.name} 执行失败: ${error.message}`);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 注册后台服务(用于清理资源)
|
|
85
|
+
api.registerService?.({
|
|
86
|
+
id: 'f2a-adapter-service',
|
|
87
|
+
start: () => {
|
|
88
|
+
api.logger?.info('[F2A Adapter] 服务已启动');
|
|
89
|
+
},
|
|
90
|
+
stop: async () => {
|
|
91
|
+
api.logger?.info('[F2A Adapter] 正在停止服务...');
|
|
92
|
+
await plugin.shutdown?.();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
api.logger?.info(`[F2A Adapter] 已注册 ${tools.length} 个工具`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 重新导出主要类,供外部使用
|
|
100
|
+
export { F2AOpenClawAdapter } from './connector.js';
|
|
101
|
+
export * from './types.js';
|
|
102
|
+
export { TaskQueue, QueuedTask, TaskQueueStats } from './task-queue.js';
|
|
103
|
+
export { AnnouncementQueue, AnnouncementQueueStats } from './announcement-queue.js';
|
|
104
|
+
export { TaskGuard, TaskGuardReport, TaskGuardRule, TaskGuardConfig, taskGuard } from './task-guard.js';
|