@f2a/openclaw-f2a 0.2.20
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/README.md +510 -0
- package/dist/agent-manager.d.ts +78 -0
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +206 -0
- package/dist/agent-manager.js.map +1 -0
- package/dist/announcement-queue.d.ts +152 -0
- package/dist/announcement-queue.d.ts.map +1 -0
- package/dist/announcement-queue.js +307 -0
- package/dist/announcement-queue.js.map +1 -0
- package/dist/capability-detector.d.ts +21 -0
- package/dist/capability-detector.d.ts.map +1 -0
- package/dist/capability-detector.js +178 -0
- package/dist/capability-detector.js.map +1 -0
- package/dist/claim-handlers.d.ts +75 -0
- package/dist/claim-handlers.d.ts.map +1 -0
- package/dist/claim-handlers.js +368 -0
- package/dist/claim-handlers.js.map +1 -0
- package/dist/connector.d.ts +174 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +1284 -0
- package/dist/connector.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +44 -0
- package/dist/logger.js.map +1 -0
- package/dist/network-client.d.ts +73 -0
- package/dist/network-client.d.ts.map +1 -0
- package/dist/network-client.js +202 -0
- package/dist/network-client.js.map +1 -0
- package/dist/node-manager.d.ts +79 -0
- package/dist/node-manager.d.ts.map +1 -0
- package/dist/node-manager.js +374 -0
- package/dist/node-manager.js.map +1 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +148 -0
- package/dist/plugin.js.map +1 -0
- package/dist/reputation.d.ts +156 -0
- package/dist/reputation.d.ts.map +1 -0
- package/dist/reputation.js +432 -0
- package/dist/reputation.js.map +1 -0
- package/dist/task-guard.d.ts +159 -0
- package/dist/task-guard.d.ts.map +1 -0
- package/dist/task-guard.js +763 -0
- package/dist/task-guard.js.map +1 -0
- package/dist/task-queue.d.ts +130 -0
- package/dist/task-queue.d.ts.map +1 -0
- package/dist/task-queue.js +592 -0
- package/dist/task-queue.js.map +1 -0
- package/dist/tool-handlers.d.ts +158 -0
- package/dist/tool-handlers.d.ts.map +1 -0
- package/dist/tool-handlers.js +727 -0
- package/dist/tool-handlers.js.map +1 -0
- package/dist/types.d.ts +417 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook-pusher.d.ts +71 -0
- package/dist/webhook-pusher.d.ts.map +1 -0
- package/dist/webhook-pusher.js +175 -0
- package/dist/webhook-pusher.js.map +1 -0
- package/dist/webhook-server.d.ts +70 -0
- package/dist/webhook-server.d.ts.map +1 -0
- package/dist/webhook-server.js +191 -0
- package/dist/webhook-server.js.map +1 -0
- package/openclaw.plugin.json +107 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* F2A OpenClaw Connector Plugin
|
|
4
|
+
* 主插件类 - 直接管理 F2A 实例
|
|
5
|
+
*
|
|
6
|
+
* 架构说明:
|
|
7
|
+
* - Adapter 直接创建和管理 F2A 实例(不需要独立的 daemon 进程)
|
|
8
|
+
* - 收到 P2P 消息时,直接调用 OpenClaw Agent API 生成回复
|
|
9
|
+
* - 这种方式更简洁,避免了 HTTP + CLI 的复杂性
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.F2AOpenClawAdapter = void 0;
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const os_1 = require("os");
|
|
15
|
+
const node_manager_js_1 = require("./node-manager.js");
|
|
16
|
+
const network_client_js_1 = require("./network-client.js");
|
|
17
|
+
const webhook_server_js_1 = require("./webhook-server.js");
|
|
18
|
+
const reputation_js_1 = require("./reputation.js");
|
|
19
|
+
const types_js_1 = require("./types.js");
|
|
20
|
+
const capability_detector_js_1 = require("./capability-detector.js");
|
|
21
|
+
const task_queue_js_1 = require("./task-queue.js");
|
|
22
|
+
const announcement_queue_js_1 = require("./announcement-queue.js");
|
|
23
|
+
const webhook_pusher_js_1 = require("./webhook-pusher.js");
|
|
24
|
+
const task_guard_js_1 = require("./task-guard.js");
|
|
25
|
+
const tool_handlers_js_1 = require("./tool-handlers.js");
|
|
26
|
+
const claim_handlers_js_1 = require("./claim-handlers.js");
|
|
27
|
+
const network_1 = require("@f2a/network");
|
|
28
|
+
class F2AOpenClawAdapter {
|
|
29
|
+
name = 'f2a-openclaw-f2a';
|
|
30
|
+
version = '0.3.0';
|
|
31
|
+
// 核心组件(延迟初始化)
|
|
32
|
+
_nodeManager;
|
|
33
|
+
_networkClient;
|
|
34
|
+
_webhookServer;
|
|
35
|
+
_reputationSystem;
|
|
36
|
+
_logger;
|
|
37
|
+
_capabilityDetector;
|
|
38
|
+
_taskQueue;
|
|
39
|
+
_announcementQueue;
|
|
40
|
+
_webhookPusher;
|
|
41
|
+
_reviewCommittee;
|
|
42
|
+
// F2A 实例(直接管理模式)
|
|
43
|
+
_f2a;
|
|
44
|
+
// 处理器实例(延迟初始化)
|
|
45
|
+
_toolHandlers;
|
|
46
|
+
_claimHandlers;
|
|
47
|
+
config;
|
|
48
|
+
nodeConfig;
|
|
49
|
+
capabilities = [];
|
|
50
|
+
api;
|
|
51
|
+
pollTimer;
|
|
52
|
+
_initialized = false;
|
|
53
|
+
// ========== 懒加载 Getter ==========
|
|
54
|
+
/**
|
|
55
|
+
* 获取节点管理器(懒加载)
|
|
56
|
+
*/
|
|
57
|
+
get nodeManager() {
|
|
58
|
+
if (!this._nodeManager) {
|
|
59
|
+
this._nodeManager = new node_manager_js_1.F2ANodeManager(this.nodeConfig, this._logger);
|
|
60
|
+
}
|
|
61
|
+
return this._nodeManager;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 获取网络客户端(懒加载)
|
|
65
|
+
*/
|
|
66
|
+
get networkClient() {
|
|
67
|
+
if (!this._networkClient) {
|
|
68
|
+
this._networkClient = new network_client_js_1.F2ANetworkClient(this.nodeConfig, this._logger);
|
|
69
|
+
}
|
|
70
|
+
return this._networkClient;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 获取任务队列(懒加载)
|
|
74
|
+
* 只有在真正需要处理任务时才初始化 SQLite 数据库
|
|
75
|
+
*/
|
|
76
|
+
get taskQueue() {
|
|
77
|
+
if (!this._taskQueue) {
|
|
78
|
+
const dataDir = this.config.dataDir || './f2a-data';
|
|
79
|
+
this._taskQueue = new task_queue_js_1.TaskQueue({
|
|
80
|
+
maxSize: this.config.maxQueuedTasks || 100,
|
|
81
|
+
maxAgeMs: 24 * 60 * 60 * 1000, // 24小时
|
|
82
|
+
persistDir: dataDir,
|
|
83
|
+
persistEnabled: true,
|
|
84
|
+
logger: this._logger
|
|
85
|
+
});
|
|
86
|
+
this._logger?.info('[F2A Adapter] TaskQueue 已初始化(懒加载)');
|
|
87
|
+
}
|
|
88
|
+
return this._taskQueue;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 获取信誉系统(懒加载)
|
|
92
|
+
*/
|
|
93
|
+
get reputationSystem() {
|
|
94
|
+
if (!this._reputationSystem) {
|
|
95
|
+
this._reputationSystem = new reputation_js_1.ReputationSystem({
|
|
96
|
+
enabled: types_js_1.INTERNAL_REPUTATION_CONFIG.enabled,
|
|
97
|
+
initialScore: types_js_1.INTERNAL_REPUTATION_CONFIG.initialScore,
|
|
98
|
+
minScoreForService: types_js_1.INTERNAL_REPUTATION_CONFIG.minScoreForService,
|
|
99
|
+
decayRate: types_js_1.INTERNAL_REPUTATION_CONFIG.decayRate,
|
|
100
|
+
}, this.config.dataDir || './f2a-data');
|
|
101
|
+
}
|
|
102
|
+
return this._reputationSystem;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 获取能力检测器(懒加载)
|
|
106
|
+
*/
|
|
107
|
+
get capabilityDetector() {
|
|
108
|
+
if (!this._capabilityDetector) {
|
|
109
|
+
this._capabilityDetector = new capability_detector_js_1.CapabilityDetector();
|
|
110
|
+
}
|
|
111
|
+
return this._capabilityDetector;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 获取广播队列(懒加载)
|
|
115
|
+
*/
|
|
116
|
+
get announcementQueue() {
|
|
117
|
+
if (!this._announcementQueue) {
|
|
118
|
+
this._announcementQueue = new announcement_queue_js_1.AnnouncementQueue({
|
|
119
|
+
maxSize: 50,
|
|
120
|
+
maxAgeMs: 30 * 60 * 1000, // 30分钟
|
|
121
|
+
logger: this._logger
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return this._announcementQueue;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 获取评审委员会(懒加载)
|
|
128
|
+
*/
|
|
129
|
+
get reviewCommittee() {
|
|
130
|
+
if (!this._reviewCommittee) {
|
|
131
|
+
const reputationAdapter = new reputation_js_1.ReputationManagerAdapter(this.reputationSystem);
|
|
132
|
+
this._reviewCommittee = new network_1.ReviewCommittee(reputationAdapter, {
|
|
133
|
+
minReviewers: 1,
|
|
134
|
+
maxReviewers: 5,
|
|
135
|
+
minReputation: types_js_1.INTERNAL_REPUTATION_CONFIG.minScoreForReview,
|
|
136
|
+
reviewTimeout: 5 * 60 * 1000 // 5 分钟
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return this._reviewCommittee;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 获取工具处理器(延迟初始化,支持未初始化时调用getTools)
|
|
143
|
+
*/
|
|
144
|
+
get toolHandlers() {
|
|
145
|
+
if (!this._toolHandlers) {
|
|
146
|
+
this._toolHandlers = new tool_handlers_js_1.ToolHandlers(this);
|
|
147
|
+
}
|
|
148
|
+
return this._toolHandlers;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 获取认领处理器(延迟初始化)
|
|
152
|
+
*/
|
|
153
|
+
get claimHandlers() {
|
|
154
|
+
if (!this._claimHandlers) {
|
|
155
|
+
this._claimHandlers = new claim_handlers_js_1.ClaimHandlers(this);
|
|
156
|
+
}
|
|
157
|
+
return this._claimHandlers;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 检查是否已初始化(用于判断是否需要启动服务)
|
|
161
|
+
*/
|
|
162
|
+
isInitialized() {
|
|
163
|
+
return this._initialized;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 初始化插件
|
|
167
|
+
*
|
|
168
|
+
* 架构重构:延迟初始化策略
|
|
169
|
+
* - 构造函数/initialize 只保存配置,不打开任何资源
|
|
170
|
+
* - TaskQueue/WebhookServer 在首次访问时才初始化
|
|
171
|
+
* - 这允许 `openclaw gateway status` 等 CLI 命令能正常退出
|
|
172
|
+
*/
|
|
173
|
+
async initialize(config) {
|
|
174
|
+
// 保存 OpenClaw logger(统一日志格式)
|
|
175
|
+
this._logger = config._api?.logger;
|
|
176
|
+
this._logger?.info('[F2A Adapter] 初始化(延迟模式)...');
|
|
177
|
+
// 保存 API 引用(用于触发心跳等)
|
|
178
|
+
this.api = config._api;
|
|
179
|
+
// 合并配置(只保存,不初始化资源)
|
|
180
|
+
this.config = this.mergeConfig(config);
|
|
181
|
+
this.nodeConfig = {
|
|
182
|
+
nodePath: this.config.f2aPath || './F2A',
|
|
183
|
+
controlPort: this.config.controlPort || 9001,
|
|
184
|
+
controlToken: this.config.controlToken || this.generateToken(),
|
|
185
|
+
p2pPort: this.config.p2pPort || 9000,
|
|
186
|
+
enableMDNS: this.config.enableMDNS ?? true,
|
|
187
|
+
bootstrapPeers: this.config.bootstrapPeers || [],
|
|
188
|
+
dataDir: this.config.dataDir
|
|
189
|
+
};
|
|
190
|
+
// 初始化 Webhook 推送器(如果配置了)
|
|
191
|
+
if (this.config.webhookPush?.enabled !== false && this.config.webhookPush?.url) {
|
|
192
|
+
this._webhookPusher = new webhook_pusher_js_1.WebhookPusher(this.config.webhookPush, this._logger);
|
|
193
|
+
this._logger?.info('[F2A Adapter] Webhook 推送已配置');
|
|
194
|
+
}
|
|
195
|
+
// 检测能力(基于配置,不依赖 OpenClaw 会话)
|
|
196
|
+
// 这是轻量级操作,不需要延迟
|
|
197
|
+
this.capabilities = this.capabilityDetector.getDefaultCapabilities();
|
|
198
|
+
if (this.config.capabilities?.length) {
|
|
199
|
+
this.capabilities = this.capabilityDetector.mergeCustomCapabilities(this.capabilities, this.config.capabilities);
|
|
200
|
+
}
|
|
201
|
+
// 注意:registerCleanupHandlers() 移到 enable() 中调用
|
|
202
|
+
// 因为它注册 process.on 事件处理器,会阻止 CLI 进程退出
|
|
203
|
+
this._logger?.info('[F2A Adapter] 初始化完成(延迟模式)');
|
|
204
|
+
this._logger?.info(`[F2A Adapter] Agent 名称: ${this.config.agentName}`);
|
|
205
|
+
this._logger?.info(`[F2A Adapter] 能力数: ${this.capabilities.length}`);
|
|
206
|
+
this._logger?.info('[F2A Adapter] 资源将在首次使用时初始化(TaskQueue/WebhookServer 等)');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 启用适配器(直接创建 F2A 实例)
|
|
210
|
+
*
|
|
211
|
+
* 新架构:Adapter 直接管理 F2A 实例,不需要独立的 daemon 进程。
|
|
212
|
+
* 这样消息处理可以直接调用 OpenClaw API,避免 HTTP + CLI 的复杂性。
|
|
213
|
+
*/
|
|
214
|
+
async enable() {
|
|
215
|
+
if (this._initialized) {
|
|
216
|
+
this._logger?.info('[F2A Adapter] 适配器已启用,跳过');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this._logger?.info('[F2A Adapter] 启用适配器(直接管理模式)...');
|
|
220
|
+
this._initialized = true;
|
|
221
|
+
// 注册清理处理器
|
|
222
|
+
this.registerCleanupHandlers();
|
|
223
|
+
// 直接创建 F2A 实例(新架构)
|
|
224
|
+
try {
|
|
225
|
+
// 使用绝对路径,避免相对路径问题
|
|
226
|
+
// 默认使用 ~/.f2a 以复用已有的 identity
|
|
227
|
+
const dataDir = this.nodeConfig.dataDir
|
|
228
|
+
? (this.nodeConfig.dataDir.startsWith('/') || this.nodeConfig.dataDir.startsWith('~')
|
|
229
|
+
? this.nodeConfig.dataDir
|
|
230
|
+
: (0, path_1.join)((0, os_1.homedir)(), this.nodeConfig.dataDir.replace(/^\.\/?/, '')))
|
|
231
|
+
: (0, path_1.join)((0, os_1.homedir)(), '.f2a');
|
|
232
|
+
// 文件日志确保不被丢失
|
|
233
|
+
const debugLog = (msg) => {
|
|
234
|
+
const fs = require('fs');
|
|
235
|
+
const logPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw/logs/adapter-debug.log');
|
|
236
|
+
try {
|
|
237
|
+
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${msg}\n`);
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
console.log(msg);
|
|
241
|
+
this._logger?.info(msg);
|
|
242
|
+
};
|
|
243
|
+
debugLog(`[F2A Adapter] 使用数据目录: ${dataDir}`);
|
|
244
|
+
debugLog(`[F2A Adapter] nodeConfig.dataDir: ${this.nodeConfig.dataDir}`);
|
|
245
|
+
debugLog(`[F2A Adapter] config.dataDir: ${this.config.dataDir}`);
|
|
246
|
+
this._f2a = await network_1.F2A.create({
|
|
247
|
+
displayName: this.config.agentName || 'OpenClaw Agent',
|
|
248
|
+
dataDir,
|
|
249
|
+
network: {
|
|
250
|
+
listenPort: this.config.p2pPort || 0,
|
|
251
|
+
bootstrapPeers: this.config.bootstrapPeers || [],
|
|
252
|
+
enableMDNS: this.config.enableMDNS ?? true,
|
|
253
|
+
enableDHT: false,
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// 监听 P2P 消息,调用 OpenClaw Agent 生成回复
|
|
257
|
+
this._f2a.on('message', async (msg) => {
|
|
258
|
+
const logMsg = `[F2A Adapter] 收到 P2P 消息: from=${msg.from?.slice(0, 16)}, content=${msg.content?.slice(0, 50)}`;
|
|
259
|
+
this._logger?.info(logMsg);
|
|
260
|
+
// 写入文件日志
|
|
261
|
+
try {
|
|
262
|
+
const fs = require('fs');
|
|
263
|
+
const logPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw/logs/adapter-debug.log');
|
|
264
|
+
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${logMsg}\n`);
|
|
265
|
+
}
|
|
266
|
+
catch { }
|
|
267
|
+
try {
|
|
268
|
+
// 调用 OpenClaw Agent 生成回复
|
|
269
|
+
const reply = await this.invokeOpenClawAgent(msg.from, msg.content, msg.messageId);
|
|
270
|
+
// 发送回复
|
|
271
|
+
if (reply && this._f2a) {
|
|
272
|
+
await this._f2a.sendMessage(msg.from, reply, { type: 'reply', replyTo: msg.messageId });
|
|
273
|
+
this._logger?.info('[F2A Adapter] 回复已发送', { to: msg.from.slice(0, 16) });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
this._logger?.error('[F2A Adapter] 处理消息失败', { error: err instanceof Error ? err.message : String(err) });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
// 监听其他事件
|
|
281
|
+
this._f2a.on('peer:connected', (event) => {
|
|
282
|
+
this._logger?.info('[F2A Adapter] Peer 连接', { peerId: event.peerId.slice(0, 16) });
|
|
283
|
+
});
|
|
284
|
+
this._f2a.on('peer:disconnected', (event) => {
|
|
285
|
+
this._logger?.info('[F2A Adapter] Peer 断开', { peerId: event.peerId.slice(0, 16) });
|
|
286
|
+
});
|
|
287
|
+
// 启动 F2A(带超时保护,避免阻塞 Gateway)
|
|
288
|
+
const START_TIMEOUT_MS = 10000; // 10 秒超时
|
|
289
|
+
const startPromise = this._f2a.start();
|
|
290
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
291
|
+
const timer = setTimeout(() => {
|
|
292
|
+
reject(new Error('F2A 启动超时'));
|
|
293
|
+
}, START_TIMEOUT_MS);
|
|
294
|
+
timer.unref(); // 确保 Gateway 可以退出
|
|
295
|
+
});
|
|
296
|
+
const result = await Promise.race([startPromise, timeoutPromise]);
|
|
297
|
+
if (!result.success) {
|
|
298
|
+
throw new Error(`F2A 启动失败: ${result.error}`);
|
|
299
|
+
}
|
|
300
|
+
this._logger?.info('[F2A Adapter] F2A 实例已启动', {
|
|
301
|
+
peerId: this._f2a.peerId?.slice(0, 16),
|
|
302
|
+
multiaddrs: this._f2a.agentInfo?.multiaddrs?.length || 0
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
307
|
+
this._logger?.error(`[F2A Adapter] 创建 F2A 实例失败: ${errorMsg}`);
|
|
308
|
+
this._logger?.warn('[F2A Adapter] F2A Adapter 将以降级模式运行,P2P 功能不可用');
|
|
309
|
+
// 清理失败的实例
|
|
310
|
+
if (this._f2a) {
|
|
311
|
+
try {
|
|
312
|
+
await this._f2a.stop();
|
|
313
|
+
}
|
|
314
|
+
catch { }
|
|
315
|
+
this._f2a = undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// 仍然启动 Webhook 服务器(用于任务委托等场景)
|
|
319
|
+
try {
|
|
320
|
+
this._webhookServer = new webhook_server_js_1.WebhookServer(this.config.webhookPort || 0, this.createWebhookHandler(), { logger: this._logger });
|
|
321
|
+
await this._webhookServer.start();
|
|
322
|
+
this._logger?.info(`[F2A Adapter] Webhook 服务器已启动: ${this._webhookServer.getUrl()}`);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
326
|
+
this._logger?.warn(`[F2A Adapter] Webhook 服务器启动失败: ${errorMsg}`);
|
|
327
|
+
}
|
|
328
|
+
// 启动兜底轮询
|
|
329
|
+
this.startFallbackPolling();
|
|
330
|
+
if (this._f2a) {
|
|
331
|
+
this._logger?.info(`[F2A Adapter] P2P 已就绪,Peer ID: ${this._f2a.peerId?.slice(0, 20)}...`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 调用 OpenClaw Agent 生成回复
|
|
336
|
+
* 使用 OpenClaw Plugin API 而不是 CLI
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* 创建 F2A 回复 Dispatcher
|
|
340
|
+
* 参考 feishu 插件的 createFeishuReplyDispatcher
|
|
341
|
+
*
|
|
342
|
+
* Dispatcher 定义了如何将 Agent 的回复发送回 P2P 网络
|
|
343
|
+
*/
|
|
344
|
+
createF2AReplyDispatcher(fromPeerId, messageId) {
|
|
345
|
+
const sendReply = async (text) => {
|
|
346
|
+
if (!this._f2a || !text?.trim()) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
await this._f2a.sendMessage(fromPeerId, text, {
|
|
351
|
+
type: 'reply',
|
|
352
|
+
replyTo: messageId,
|
|
353
|
+
});
|
|
354
|
+
this._logger?.info('[F2A Adapter] 回复已发送', { to: fromPeerId.slice(0, 16) });
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
this._logger?.error('[F2A Adapter] 发送回复失败', { error: err instanceof Error ? err.message : String(err) });
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
// 返回 dispatcher 对象,格式与 OpenClaw 兼容
|
|
361
|
+
return {
|
|
362
|
+
deliver: async (payload, _info) => {
|
|
363
|
+
const text = payload.text ?? '';
|
|
364
|
+
if (!text.trim()) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// 分块发送(如果文本太长)
|
|
368
|
+
const chunkLimit = 4000;
|
|
369
|
+
for (let i = 0; i < text.length; i += chunkLimit) {
|
|
370
|
+
const chunk = text.slice(i, i + chunkLimit);
|
|
371
|
+
await sendReply(chunk);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* 调用 OpenClaw Agent 生成回复
|
|
378
|
+
* 参考 feishu 插件实现,使用 api.channel.reply.dispatchReplyFromConfig
|
|
379
|
+
*/
|
|
380
|
+
async invokeOpenClawAgent(fromPeerId, message, replyToMessageId) {
|
|
381
|
+
const sessionKey = `f2a-${fromPeerId.slice(0, 16)}`;
|
|
382
|
+
const debugLog = (msg) => {
|
|
383
|
+
try {
|
|
384
|
+
const fs = require('fs');
|
|
385
|
+
const logPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw/logs/adapter-debug.log');
|
|
386
|
+
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${msg}\n`);
|
|
387
|
+
}
|
|
388
|
+
catch { }
|
|
389
|
+
this._logger?.info(msg);
|
|
390
|
+
};
|
|
391
|
+
debugLog(`[F2A Adapter] invokeOpenClawAgent: sessionKey=${sessionKey}`);
|
|
392
|
+
debugLog(`[F2A Adapter] API: hasApi=${!!this.api}, hasChannel=${!!this.api?.channel}`);
|
|
393
|
+
// 创建 F2A 回复 dispatcher
|
|
394
|
+
const f2aDispatcher = this.createF2AReplyDispatcher(fromPeerId, replyToMessageId);
|
|
395
|
+
// 使用 OpenClaw Channel API (参考飞书插件)
|
|
396
|
+
if (this.api?.channel?.reply?.dispatchReplyFromConfig) {
|
|
397
|
+
debugLog('[F2A Adapter] 使用 Channel API');
|
|
398
|
+
try {
|
|
399
|
+
const route = this.api.channel.routing.resolveAgentRoute({
|
|
400
|
+
peerId: sessionKey,
|
|
401
|
+
});
|
|
402
|
+
const ctx = this.api.channel.reply.finalizeInboundContext({
|
|
403
|
+
SessionKey: route.sessionKey,
|
|
404
|
+
PeerId: sessionKey,
|
|
405
|
+
Sender: 'F2P P2P',
|
|
406
|
+
SenderId: fromPeerId.slice(0, 16),
|
|
407
|
+
ChannelType: 'p2p',
|
|
408
|
+
InboundId: fromPeerId.slice(0, 16),
|
|
409
|
+
});
|
|
410
|
+
// 使用 F2A dispatcher 发送回复
|
|
411
|
+
const result = await this.api.channel.reply.dispatchReplyFromConfig({
|
|
412
|
+
ctx,
|
|
413
|
+
cfg: this.config,
|
|
414
|
+
dispatcher: f2aDispatcher,
|
|
415
|
+
});
|
|
416
|
+
debugLog(`[F2A Adapter] Channel API 完成: ${JSON.stringify(result)}`);
|
|
417
|
+
return undefined; // dispatcher 会自动发送回复
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
debugLog(`[F2A Adapter] Channel API 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// 降级:使用 subagent API
|
|
424
|
+
if (this.api?.runtime?.subagent?.run) {
|
|
425
|
+
debugLog('[F2A Adapter] 使用 Subagent API');
|
|
426
|
+
try {
|
|
427
|
+
const runResult = await this.api.runtime.subagent.run({
|
|
428
|
+
sessionKey,
|
|
429
|
+
message,
|
|
430
|
+
deliver: false,
|
|
431
|
+
});
|
|
432
|
+
const waitResult = await this.api.runtime.subagent.waitForRun({
|
|
433
|
+
runId: runResult.runId,
|
|
434
|
+
timeoutMs: 60000,
|
|
435
|
+
});
|
|
436
|
+
if (waitResult.status === 'ok') {
|
|
437
|
+
const messagesResult = await this.api.runtime.subagent.getSessionMessages({
|
|
438
|
+
sessionKey,
|
|
439
|
+
limit: 1,
|
|
440
|
+
});
|
|
441
|
+
if (messagesResult.messages && messagesResult.messages.length > 0) {
|
|
442
|
+
const lastMessage = messagesResult.messages[messagesResult.messages.length - 1];
|
|
443
|
+
const reply = lastMessage?.content || lastMessage?.text;
|
|
444
|
+
if (reply) {
|
|
445
|
+
// 手动发送回复
|
|
446
|
+
await f2aDispatcher.deliver({ text: reply });
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
debugLog(`[F2A Adapter] Subagent 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// 最终降级
|
|
457
|
+
debugLog('[F2A Adapter] 使用降级回复');
|
|
458
|
+
const fallbackReply = `收到你的消息:"${message.slice(0, 30)}"。我是 ${this.config.agentName || 'OpenClaw Agent'},很高兴与你交流!`;
|
|
459
|
+
await f2aDispatcher.deliver({ text: fallbackReply });
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 注册清理处理器
|
|
464
|
+
*/
|
|
465
|
+
registerCleanupHandlers() {
|
|
466
|
+
const autoCleanup = async () => {
|
|
467
|
+
// 关闭 F2A 实例
|
|
468
|
+
if (this._f2a) {
|
|
469
|
+
try {
|
|
470
|
+
await this._f2a.stop();
|
|
471
|
+
this._logger?.info('[F2A Adapter] F2A 实例已停止');
|
|
472
|
+
}
|
|
473
|
+
catch { }
|
|
474
|
+
}
|
|
475
|
+
// 同步关闭其他资源
|
|
476
|
+
if (this.pollTimer) {
|
|
477
|
+
clearInterval(this.pollTimer);
|
|
478
|
+
this.pollTimer = undefined;
|
|
479
|
+
}
|
|
480
|
+
if (this._webhookServer) {
|
|
481
|
+
try {
|
|
482
|
+
this._webhookServer.server?.close();
|
|
483
|
+
}
|
|
484
|
+
catch { }
|
|
485
|
+
}
|
|
486
|
+
if (this._taskQueue) {
|
|
487
|
+
try {
|
|
488
|
+
this._taskQueue.close();
|
|
489
|
+
}
|
|
490
|
+
catch { }
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
process.once('beforeExit', autoCleanup);
|
|
494
|
+
process.once('SIGINT', autoCleanup);
|
|
495
|
+
process.once('SIGTERM', autoCleanup);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 兜底轮询
|
|
499
|
+
* 当 webhook 推送失败时,轮询确保任务不会丢失
|
|
500
|
+
*/
|
|
501
|
+
startFallbackPolling() {
|
|
502
|
+
const interval = this.config.pollInterval || 60000; // 默认 60 秒
|
|
503
|
+
this.pollTimer = setInterval(async () => {
|
|
504
|
+
// P1 修复:定期检查并重置超时的 processing 任务,防止僵尸任务
|
|
505
|
+
// 只有在 TaskQueue 已初始化时才检查
|
|
506
|
+
if (this._taskQueue) {
|
|
507
|
+
this.resetTimedOutProcessingTasks();
|
|
508
|
+
}
|
|
509
|
+
if (!this._webhookPusher) {
|
|
510
|
+
// 没有配置 webhook,不轮询(保持原有轮询模式)
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// 只有在 TaskQueue 已初始化时才处理
|
|
514
|
+
if (!this._taskQueue) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
// 获取未推送的任务
|
|
519
|
+
const pending = this._taskQueue.getWebhookPending();
|
|
520
|
+
if (pending.length > 0) {
|
|
521
|
+
this._logger?.info(`[F2A Adapter] 兜底轮询: ${pending.length} 个待推送任务`);
|
|
522
|
+
for (const task of pending) {
|
|
523
|
+
const result = await this._webhookPusher.pushTask(task);
|
|
524
|
+
if (result.success) {
|
|
525
|
+
this._taskQueue.markWebhookPushed(task.taskId);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
this._logger?.error('[F2A Adapter] 兜底轮询失败:', error);
|
|
532
|
+
}
|
|
533
|
+
}, interval);
|
|
534
|
+
// 防止定时器阻止进程退出
|
|
535
|
+
if (this.pollTimer.unref) {
|
|
536
|
+
this.pollTimer.unref();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* P1 修复:重置超时的 processing 任务
|
|
541
|
+
* 如果任务在 processing 状态停留超过超时时间,将其重置为 pending
|
|
542
|
+
* 防止因处理失败导致的僵尸任务
|
|
543
|
+
*/
|
|
544
|
+
resetTimedOutProcessingTasks() {
|
|
545
|
+
if (!this._taskQueue)
|
|
546
|
+
return;
|
|
547
|
+
const stats = this._taskQueue.getStats();
|
|
548
|
+
if (stats.processing === 0) {
|
|
549
|
+
return; // 没有处理中的任务,无需检查
|
|
550
|
+
}
|
|
551
|
+
const allTasks = this._taskQueue.getAll();
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
const processingTimeout = this.config.processingTimeoutMs || 5 * 60 * 1000; // 默认 5 分钟
|
|
554
|
+
for (const task of allTasks) {
|
|
555
|
+
if (task.status === 'processing') {
|
|
556
|
+
const taskTimeout = task.timeout || 30000; // 使用任务自身的超时或默认 30 秒
|
|
557
|
+
const maxAllowedTime = Math.max(taskTimeout * 2, processingTimeout); // 至少 2 倍任务超时或 processingTimeout
|
|
558
|
+
const processingTime = now - (task.updatedAt || task.createdAt);
|
|
559
|
+
if (processingTime > maxAllowedTime) {
|
|
560
|
+
this._logger?.warn(`[F2A Adapter] 检测到僵尸任务 ${task.taskId.slice(0, 8)}... (processing ${Math.round(processingTime / 1000)}s),重置为 pending`);
|
|
561
|
+
// 将任务重置为 pending 状态
|
|
562
|
+
this._taskQueue.resetProcessingTask(task.taskId);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* 获取插件提供的 Tools
|
|
569
|
+
*/
|
|
570
|
+
getTools() {
|
|
571
|
+
return [
|
|
572
|
+
{
|
|
573
|
+
name: 'f2a_discover',
|
|
574
|
+
description: '发现 F2A 网络中的 Agents,可以按能力过滤',
|
|
575
|
+
parameters: {
|
|
576
|
+
capability: {
|
|
577
|
+
type: 'string',
|
|
578
|
+
description: '按能力过滤,如 code-generation, file-operation',
|
|
579
|
+
required: false
|
|
580
|
+
},
|
|
581
|
+
min_reputation: {
|
|
582
|
+
type: 'number',
|
|
583
|
+
description: '最低信誉分数 (0-100)',
|
|
584
|
+
required: false
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
handler: this.toolHandlers.handleDiscover.bind(this.toolHandlers)
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: 'f2a_delegate',
|
|
591
|
+
description: '委托任务给网络中的特定 Agent',
|
|
592
|
+
parameters: {
|
|
593
|
+
agent: {
|
|
594
|
+
type: 'string',
|
|
595
|
+
description: '目标 Agent ID、名称或 #索引 (如 #1)',
|
|
596
|
+
required: true
|
|
597
|
+
},
|
|
598
|
+
task: {
|
|
599
|
+
type: 'string',
|
|
600
|
+
description: '任务描述',
|
|
601
|
+
required: true
|
|
602
|
+
},
|
|
603
|
+
context: {
|
|
604
|
+
type: 'string',
|
|
605
|
+
description: '任务上下文或附件',
|
|
606
|
+
required: false
|
|
607
|
+
},
|
|
608
|
+
timeout: {
|
|
609
|
+
type: 'number',
|
|
610
|
+
description: '超时时间(毫秒)',
|
|
611
|
+
required: false
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
handler: this.toolHandlers.handleDelegate.bind(this.toolHandlers)
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: 'f2a_broadcast',
|
|
618
|
+
description: '广播任务给所有具备某能力的 Agents(并行执行)',
|
|
619
|
+
parameters: {
|
|
620
|
+
capability: {
|
|
621
|
+
type: 'string',
|
|
622
|
+
description: '所需能力',
|
|
623
|
+
required: true
|
|
624
|
+
},
|
|
625
|
+
task: {
|
|
626
|
+
type: 'string',
|
|
627
|
+
description: '任务描述',
|
|
628
|
+
required: true
|
|
629
|
+
},
|
|
630
|
+
min_responses: {
|
|
631
|
+
type: 'number',
|
|
632
|
+
description: '最少响应数',
|
|
633
|
+
required: false
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
handler: this.toolHandlers.handleBroadcast.bind(this.toolHandlers)
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: 'f2a_status',
|
|
640
|
+
description: '查看 F2A 网络状态和已连接 Peers',
|
|
641
|
+
parameters: {},
|
|
642
|
+
handler: this.toolHandlers.handleStatus.bind(this.toolHandlers)
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: 'f2a_reputation',
|
|
646
|
+
description: '查看或管理 Peer 信誉',
|
|
647
|
+
parameters: {
|
|
648
|
+
action: {
|
|
649
|
+
type: 'string',
|
|
650
|
+
description: '操作: list, view, block, unblock',
|
|
651
|
+
required: true,
|
|
652
|
+
enum: ['list', 'view', 'block', 'unblock']
|
|
653
|
+
},
|
|
654
|
+
peer_id: {
|
|
655
|
+
type: 'string',
|
|
656
|
+
description: 'Peer ID',
|
|
657
|
+
required: false
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
handler: this.toolHandlers.handleReputation.bind(this.toolHandlers)
|
|
661
|
+
},
|
|
662
|
+
// 新增:任务队列相关工具
|
|
663
|
+
{
|
|
664
|
+
name: 'f2a_poll_tasks',
|
|
665
|
+
description: '查询本节点收到的远程任务队列(待 OpenClaw 执行)',
|
|
666
|
+
parameters: {
|
|
667
|
+
limit: {
|
|
668
|
+
type: 'number',
|
|
669
|
+
description: '最大返回任务数',
|
|
670
|
+
required: false
|
|
671
|
+
},
|
|
672
|
+
status: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
description: '任务状态过滤: pending, processing, completed, failed',
|
|
675
|
+
required: false,
|
|
676
|
+
enum: ['pending', 'processing', 'completed', 'failed']
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
handler: this.toolHandlers.handlePollTasks.bind(this.toolHandlers)
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: 'f2a_submit_result',
|
|
683
|
+
description: '提交远程任务的执行结果,发送给原节点',
|
|
684
|
+
parameters: {
|
|
685
|
+
task_id: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: '任务ID',
|
|
688
|
+
required: true
|
|
689
|
+
},
|
|
690
|
+
result: {
|
|
691
|
+
type: 'string',
|
|
692
|
+
description: '任务执行结果',
|
|
693
|
+
required: true
|
|
694
|
+
},
|
|
695
|
+
status: {
|
|
696
|
+
type: 'string',
|
|
697
|
+
description: '执行状态: success 或 error',
|
|
698
|
+
required: true,
|
|
699
|
+
enum: ['success', 'error']
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
handler: this.toolHandlers.handleSubmitResult.bind(this.toolHandlers)
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: 'f2a_task_stats',
|
|
706
|
+
description: '查看任务队列统计信息',
|
|
707
|
+
parameters: {},
|
|
708
|
+
handler: this.toolHandlers.handleTaskStats.bind(this.toolHandlers)
|
|
709
|
+
},
|
|
710
|
+
// 认领模式工具
|
|
711
|
+
{
|
|
712
|
+
name: 'f2a_announce',
|
|
713
|
+
description: '广播任务到 F2A 网络,等待其他 Agent 认领(认领模式)',
|
|
714
|
+
parameters: {
|
|
715
|
+
task_type: {
|
|
716
|
+
type: 'string',
|
|
717
|
+
description: '任务类型',
|
|
718
|
+
required: true
|
|
719
|
+
},
|
|
720
|
+
description: {
|
|
721
|
+
type: 'string',
|
|
722
|
+
description: '任务描述',
|
|
723
|
+
required: true
|
|
724
|
+
},
|
|
725
|
+
required_capabilities: {
|
|
726
|
+
type: 'array',
|
|
727
|
+
description: '所需能力列表',
|
|
728
|
+
required: false
|
|
729
|
+
},
|
|
730
|
+
estimated_complexity: {
|
|
731
|
+
type: 'number',
|
|
732
|
+
description: '预估复杂度 (1-10)',
|
|
733
|
+
required: false
|
|
734
|
+
},
|
|
735
|
+
reward: {
|
|
736
|
+
type: 'number',
|
|
737
|
+
description: '任务奖励',
|
|
738
|
+
required: false
|
|
739
|
+
},
|
|
740
|
+
timeout: {
|
|
741
|
+
type: 'number',
|
|
742
|
+
description: '超时时间(毫秒)',
|
|
743
|
+
required: false
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
handler: this.claimHandlers.handleAnnounce.bind(this.claimHandlers)
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: 'f2a_list_announcements',
|
|
750
|
+
description: '查看当前开放的任务广播(可认领)',
|
|
751
|
+
parameters: {
|
|
752
|
+
capability: {
|
|
753
|
+
type: 'string',
|
|
754
|
+
description: '按能力过滤',
|
|
755
|
+
required: false
|
|
756
|
+
},
|
|
757
|
+
limit: {
|
|
758
|
+
type: 'number',
|
|
759
|
+
description: '最大返回数量',
|
|
760
|
+
required: false
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
handler: this.claimHandlers.handleListAnnouncements.bind(this.claimHandlers)
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
name: 'f2a_claim',
|
|
767
|
+
description: '认领一个开放的任务广播',
|
|
768
|
+
parameters: {
|
|
769
|
+
announcement_id: {
|
|
770
|
+
type: 'string',
|
|
771
|
+
description: '广播ID',
|
|
772
|
+
required: true
|
|
773
|
+
},
|
|
774
|
+
estimated_time: {
|
|
775
|
+
type: 'number',
|
|
776
|
+
description: '预计完成时间(毫秒)',
|
|
777
|
+
required: false
|
|
778
|
+
},
|
|
779
|
+
confidence: {
|
|
780
|
+
type: 'number',
|
|
781
|
+
description: '信心指数 (0-1)',
|
|
782
|
+
required: false
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
handler: this.claimHandlers.handleClaim.bind(this.claimHandlers)
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
name: 'f2a_manage_claims',
|
|
789
|
+
description: '管理我的任务广播的认领(接受/拒绝)',
|
|
790
|
+
parameters: {
|
|
791
|
+
announcement_id: {
|
|
792
|
+
type: 'string',
|
|
793
|
+
description: '广播ID',
|
|
794
|
+
required: true
|
|
795
|
+
},
|
|
796
|
+
action: {
|
|
797
|
+
type: 'string',
|
|
798
|
+
description: '操作: list, accept, reject',
|
|
799
|
+
required: true,
|
|
800
|
+
enum: ['list', 'accept', 'reject']
|
|
801
|
+
},
|
|
802
|
+
claim_id: {
|
|
803
|
+
type: 'string',
|
|
804
|
+
description: '认领ID(accept/reject 时需要)',
|
|
805
|
+
required: false
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
handler: this.claimHandlers.handleManageClaims.bind(this.claimHandlers)
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'f2a_my_claims',
|
|
812
|
+
description: '查看我提交的任务认领状态',
|
|
813
|
+
parameters: {
|
|
814
|
+
status: {
|
|
815
|
+
type: 'string',
|
|
816
|
+
description: '状态过滤: pending, accepted, rejected, all',
|
|
817
|
+
required: false,
|
|
818
|
+
enum: ['pending', 'accepted', 'rejected', 'all']
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
handler: this.claimHandlers.handleMyClaims.bind(this.claimHandlers)
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: 'f2a_announcement_stats',
|
|
825
|
+
description: '查看任务广播统计',
|
|
826
|
+
parameters: {},
|
|
827
|
+
handler: this.claimHandlers.handleAnnouncementStats.bind(this.claimHandlers)
|
|
828
|
+
},
|
|
829
|
+
// 任务评估相关工具
|
|
830
|
+
{
|
|
831
|
+
name: 'f2a_estimate_task',
|
|
832
|
+
description: '评估任务成本(工作量、复杂度、预估时间)',
|
|
833
|
+
parameters: {
|
|
834
|
+
task_type: {
|
|
835
|
+
type: 'string',
|
|
836
|
+
description: '任务类型',
|
|
837
|
+
required: true
|
|
838
|
+
},
|
|
839
|
+
description: {
|
|
840
|
+
type: 'string',
|
|
841
|
+
description: '任务描述',
|
|
842
|
+
required: true
|
|
843
|
+
},
|
|
844
|
+
required_capabilities: {
|
|
845
|
+
type: 'array',
|
|
846
|
+
description: '所需能力列表',
|
|
847
|
+
required: false
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
handler: this.toolHandlers.handleEstimateTask.bind(this.toolHandlers)
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: 'f2a_review_task',
|
|
854
|
+
description: '作为评审者评审任务的工作量和价值',
|
|
855
|
+
parameters: {
|
|
856
|
+
task_id: {
|
|
857
|
+
type: 'string',
|
|
858
|
+
description: '任务ID',
|
|
859
|
+
required: true
|
|
860
|
+
},
|
|
861
|
+
workload: {
|
|
862
|
+
type: 'number',
|
|
863
|
+
description: '工作量评估 (0-100)',
|
|
864
|
+
required: true
|
|
865
|
+
},
|
|
866
|
+
value: {
|
|
867
|
+
type: 'number',
|
|
868
|
+
description: '价值评估 (-100 ~ 100)',
|
|
869
|
+
required: true
|
|
870
|
+
},
|
|
871
|
+
risk_flags: {
|
|
872
|
+
type: 'array',
|
|
873
|
+
description: '风险标记: dangerous, malicious, spam, invalid',
|
|
874
|
+
required: false
|
|
875
|
+
},
|
|
876
|
+
comment: {
|
|
877
|
+
type: 'string',
|
|
878
|
+
description: '评审意见',
|
|
879
|
+
required: false
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
handler: this.toolHandlers.handleReviewTask.bind(this.toolHandlers)
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
name: 'f2a_get_reviews',
|
|
886
|
+
description: '获取任务的评审汇总结果',
|
|
887
|
+
parameters: {
|
|
888
|
+
task_id: {
|
|
889
|
+
type: 'string',
|
|
890
|
+
description: '任务ID',
|
|
891
|
+
required: true
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
handler: this.toolHandlers.handleGetReviews.bind(this.toolHandlers)
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
name: 'f2a_get_capabilities',
|
|
898
|
+
description: '获取指定 Agent 的能力列表',
|
|
899
|
+
parameters: {
|
|
900
|
+
peer_id: {
|
|
901
|
+
type: 'string',
|
|
902
|
+
description: 'Agent 的 Peer ID 或名称',
|
|
903
|
+
required: false
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
handler: this.toolHandlers.handleGetCapabilities.bind(this.toolHandlers)
|
|
907
|
+
}
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* 创建 Webhook 处理器
|
|
912
|
+
*/
|
|
913
|
+
createWebhookHandler() {
|
|
914
|
+
return {
|
|
915
|
+
onDiscover: async (payload) => {
|
|
916
|
+
// 检查请求者信誉(懒加载触发)
|
|
917
|
+
if (!this.reputationSystem.isAllowed(payload.requester)) {
|
|
918
|
+
return {
|
|
919
|
+
capabilities: [],
|
|
920
|
+
reputation: this.reputationSystem.getReputation(payload.requester).score
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
// 过滤能力
|
|
924
|
+
let caps = this.capabilities;
|
|
925
|
+
if (payload.query.capability) {
|
|
926
|
+
caps = caps.filter(c => c.name === payload.query.capability ||
|
|
927
|
+
c.tools?.includes(payload.query.capability));
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
capabilities: caps,
|
|
931
|
+
reputation: this.reputationSystem.getReputation(payload.requester).score
|
|
932
|
+
};
|
|
933
|
+
},
|
|
934
|
+
onDelegate: async (payload) => {
|
|
935
|
+
// 安全检查(懒加载触发 reputationSystem)
|
|
936
|
+
if (!this.reputationSystem.isAllowed(payload.from)) {
|
|
937
|
+
return {
|
|
938
|
+
accepted: false,
|
|
939
|
+
taskId: payload.taskId,
|
|
940
|
+
reason: 'Reputation too low'
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// 检查白名单/黑名单
|
|
944
|
+
const whitelist = this.config.security?.whitelist || [];
|
|
945
|
+
const blacklist = this.config.security?.blacklist || [];
|
|
946
|
+
const isWhitelisted = whitelist.length > 0 && whitelist.includes(payload.from);
|
|
947
|
+
const isBlacklisted = blacklist.includes(payload.from);
|
|
948
|
+
if (whitelist.length > 0 && !isWhitelisted) {
|
|
949
|
+
return {
|
|
950
|
+
accepted: false,
|
|
951
|
+
taskId: payload.taskId,
|
|
952
|
+
reason: 'Not in whitelist'
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (isBlacklisted) {
|
|
956
|
+
return {
|
|
957
|
+
accepted: false,
|
|
958
|
+
taskId: payload.taskId,
|
|
959
|
+
reason: 'In blacklist'
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
// TaskGuard 安全检查
|
|
963
|
+
const requesterReputation = this.reputationSystem.getReputation(payload.from);
|
|
964
|
+
const taskGuardContext = {
|
|
965
|
+
requesterReputation,
|
|
966
|
+
isWhitelisted,
|
|
967
|
+
isBlacklisted,
|
|
968
|
+
recentTaskCount: 0 // Will be tracked internally by TaskGuard
|
|
969
|
+
};
|
|
970
|
+
const taskGuardReport = task_guard_js_1.taskGuard.check(payload, taskGuardContext);
|
|
971
|
+
if (!taskGuardReport.passed) {
|
|
972
|
+
// 任务被阻止
|
|
973
|
+
const blockReasons = taskGuardReport.blocks.map(b => b.message).join('; ');
|
|
974
|
+
this._logger?.warn(`[F2A Adapter] TaskGuard 阻止任务 ${payload.taskId}: ${blockReasons}`);
|
|
975
|
+
return {
|
|
976
|
+
accepted: false,
|
|
977
|
+
taskId: payload.taskId,
|
|
978
|
+
reason: `TaskGuard blocked: ${blockReasons}`
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (taskGuardReport.requiresConfirmation) {
|
|
982
|
+
// 任务需要确认(警告但不阻止)
|
|
983
|
+
const warnReasons = taskGuardReport.warnings.map(w => w.message).join('; ');
|
|
984
|
+
this._logger?.warn(`[F2A Adapter] TaskGuard 警告 ${payload.taskId}: ${warnReasons}`);
|
|
985
|
+
// 未来可以扩展为请求用户确认
|
|
986
|
+
// 目前记录警告但继续处理任务
|
|
987
|
+
}
|
|
988
|
+
// 检查队列是否已满(懒加载触发 taskQueue)
|
|
989
|
+
const stats = this.taskQueue.getStats();
|
|
990
|
+
if (stats.pending >= (this.config.maxQueuedTasks || 100)) {
|
|
991
|
+
return {
|
|
992
|
+
accepted: false,
|
|
993
|
+
taskId: payload.taskId,
|
|
994
|
+
reason: 'Task queue is full'
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
// 添加任务到队列
|
|
998
|
+
try {
|
|
999
|
+
const task = this.taskQueue.add(payload);
|
|
1000
|
+
// 优先使用 webhook 推送
|
|
1001
|
+
if (this._webhookPusher) {
|
|
1002
|
+
const result = await this._webhookPusher.pushTask(task);
|
|
1003
|
+
if (result.success) {
|
|
1004
|
+
this.taskQueue.markWebhookPushed(task.taskId);
|
|
1005
|
+
this._logger?.info(`[F2A Adapter] 任务 ${task.taskId} 已通过 webhook 推送 (${result.latency}ms)`);
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
this._logger?.info(`[F2A Adapter] Webhook 推送失败: ${result.error},任务将在轮询时处理`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// 触发 OpenClaw 心跳,让它知道有新任务
|
|
1012
|
+
this.api?.runtime?.system?.requestHeartbeatNow?.();
|
|
1013
|
+
return {
|
|
1014
|
+
accepted: true,
|
|
1015
|
+
taskId: payload.taskId
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
return {
|
|
1020
|
+
accepted: false,
|
|
1021
|
+
taskId: payload.taskId,
|
|
1022
|
+
reason: error instanceof Error ? error.message : 'Failed to queue task'
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
onMessage: async (payload) => {
|
|
1027
|
+
this._logger?.info('[F2A Adapter] 收到 P2P 消息', {
|
|
1028
|
+
from: payload.from.slice(0, 16),
|
|
1029
|
+
content: payload.content.slice(0, 50)
|
|
1030
|
+
});
|
|
1031
|
+
try {
|
|
1032
|
+
// 调用 OpenClaw Agent 处理消息
|
|
1033
|
+
// 使用 peerId 作为 session id,确保同一个对话者使用同一个 session
|
|
1034
|
+
const sessionId = `f2a-${payload.from.slice(0, 16)}`;
|
|
1035
|
+
// 构造消息,包含发送者信息
|
|
1036
|
+
const message = `[来自 ${payload.metadata?.from || payload.from.slice(0, 16)}] ${payload.content}`;
|
|
1037
|
+
// 调用 openclaw agent 命令
|
|
1038
|
+
const result = await this.invokeOpenClawAgent(payload.from, message);
|
|
1039
|
+
return { response: result || '收到消息,但我暂时无法生成回复。' };
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
this._logger?.error('[F2A Adapter] 处理消息失败', { error: error instanceof Error ? error.message : String(error) });
|
|
1043
|
+
return { response: '抱歉,我遇到了一些问题,无法处理你的消息。' };
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
onStatus: async () => {
|
|
1047
|
+
// 如果 TaskQueue 未初始化,返回空闲状态
|
|
1048
|
+
if (!this._taskQueue) {
|
|
1049
|
+
return {
|
|
1050
|
+
status: 'available',
|
|
1051
|
+
load: 0,
|
|
1052
|
+
queued: 0,
|
|
1053
|
+
processing: 0
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
const stats = this._taskQueue.getStats();
|
|
1057
|
+
return {
|
|
1058
|
+
status: 'available',
|
|
1059
|
+
load: stats.pending + stats.processing,
|
|
1060
|
+
queued: stats.pending,
|
|
1061
|
+
processing: stats.processing
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* 注册到 F2A Node
|
|
1068
|
+
*/
|
|
1069
|
+
async registerToNode() {
|
|
1070
|
+
if (!this._webhookServer)
|
|
1071
|
+
return;
|
|
1072
|
+
await this.networkClient.registerWebhook(this._webhookServer.getUrl());
|
|
1073
|
+
await this.networkClient.updateAgentInfo({
|
|
1074
|
+
displayName: this.config.agentName,
|
|
1075
|
+
capabilities: this.capabilities
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
// ========== Helpers ==========
|
|
1079
|
+
/**
|
|
1080
|
+
* 合并配置(公开方法供处理器使用)
|
|
1081
|
+
*/
|
|
1082
|
+
mergeConfig(config) {
|
|
1083
|
+
return {
|
|
1084
|
+
autoStart: config.autoStart ?? true,
|
|
1085
|
+
webhookPort: config.webhookPort || 9002,
|
|
1086
|
+
agentName: config.agentName || 'OpenClaw Agent',
|
|
1087
|
+
capabilities: config.capabilities || [],
|
|
1088
|
+
f2aPath: config.f2aPath,
|
|
1089
|
+
controlPort: config.controlPort,
|
|
1090
|
+
controlToken: config.controlToken,
|
|
1091
|
+
p2pPort: config.p2pPort,
|
|
1092
|
+
enableMDNS: config.enableMDNS,
|
|
1093
|
+
bootstrapPeers: config.bootstrapPeers,
|
|
1094
|
+
dataDir: config.dataDir || './f2a-data',
|
|
1095
|
+
maxQueuedTasks: config.maxQueuedTasks || 100,
|
|
1096
|
+
pollInterval: config.pollInterval,
|
|
1097
|
+
// 保留 webhookPush 配置(修复:之前丢失导致 webhook 推送被禁用)
|
|
1098
|
+
webhookPush: config.webhookPush,
|
|
1099
|
+
reputation: {
|
|
1100
|
+
enabled: config.reputation?.enabled ?? types_js_1.INTERNAL_REPUTATION_CONFIG.enabled,
|
|
1101
|
+
initialScore: config.reputation?.initialScore || types_js_1.INTERNAL_REPUTATION_CONFIG.initialScore,
|
|
1102
|
+
minScoreForService: config.reputation?.minScoreForService || types_js_1.INTERNAL_REPUTATION_CONFIG.minScoreForService,
|
|
1103
|
+
decayRate: config.reputation?.decayRate || types_js_1.INTERNAL_REPUTATION_CONFIG.decayRate
|
|
1104
|
+
},
|
|
1105
|
+
security: {
|
|
1106
|
+
requireConfirmation: config.security?.requireConfirmation ?? false,
|
|
1107
|
+
whitelist: config.security?.whitelist || [],
|
|
1108
|
+
blacklist: config.security?.blacklist || [],
|
|
1109
|
+
maxTasksPerMinute: config.security?.maxTasksPerMinute || 10
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
generateToken() {
|
|
1114
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
1115
|
+
let token = 'f2a-';
|
|
1116
|
+
for (let i = 0; i < 32; i++) {
|
|
1117
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1118
|
+
}
|
|
1119
|
+
return token;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* 检查 F2A CLI 是否已安装
|
|
1123
|
+
* 通过执行 `f2a version` 命令来检测
|
|
1124
|
+
*/
|
|
1125
|
+
async checkF2AInstalled() {
|
|
1126
|
+
try {
|
|
1127
|
+
const { exec } = await import('child_process');
|
|
1128
|
+
const { promisify } = await import('util');
|
|
1129
|
+
const execAsync = promisify(exec);
|
|
1130
|
+
const { stdout } = await execAsync('f2a version', { timeout: 5000 });
|
|
1131
|
+
this._logger?.info(`[F2A Adapter] F2A CLI 已安装: ${stdout.trim()}`);
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
// 如果是命令不存在,说明 CLI 未安装
|
|
1136
|
+
if (error instanceof Error && (error.message.includes('ENOENT') || error.message.includes('not found'))) {
|
|
1137
|
+
this._logger?.debug?.('[F2A Adapter] F2A CLI 未安装');
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
// P2-3 修复:timeout 也视为 CLI 未安装(CLI 可能存在但响应慢)
|
|
1141
|
+
if (error instanceof Error && error.message.includes('ETIMEDOUT')) {
|
|
1142
|
+
this._logger?.debug?.('[F2A Adapter] F2A CLI 响应超时,视为未安装');
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
// 其他错误,可能是 CLI 已安装但有问题
|
|
1146
|
+
this._logger?.debug?.(`[F2A Adapter] F2A CLI 检测异常: ${error instanceof Error ? error.message : String(error)}`);
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* 通过 CLI 启动 F2A daemon
|
|
1152
|
+
* 执行 `f2a daemon -d` 命令(后台模式)
|
|
1153
|
+
*/
|
|
1154
|
+
async startDaemonViaCLI() {
|
|
1155
|
+
try {
|
|
1156
|
+
const { spawn } = await import('child_process');
|
|
1157
|
+
return new Promise((resolve) => {
|
|
1158
|
+
this._logger?.info('[F2A Adapter] 执行: f2a daemon -d');
|
|
1159
|
+
// 设置 messageHandlerUrl 环境变量,让 daemon 将消息推送到 adapter 的 webhook
|
|
1160
|
+
const env = { ...process.env };
|
|
1161
|
+
if (this._webhookServer) {
|
|
1162
|
+
const webhookUrl = `${this._webhookServer.getUrl()}/message`;
|
|
1163
|
+
env.F2A_MESSAGE_HANDLER_URL = webhookUrl;
|
|
1164
|
+
this._logger?.info(`[F2A Adapter] 配置消息处理 URL: ${webhookUrl}`);
|
|
1165
|
+
}
|
|
1166
|
+
const proc = spawn('f2a', ['daemon', '-d'], {
|
|
1167
|
+
detached: true, // P1-1 修复:让 daemon 独立运行,不随父进程退出
|
|
1168
|
+
stdio: 'ignore', // P1-2 修复:ignore stdio 配合 detached 使用
|
|
1169
|
+
env // 传递环境变量
|
|
1170
|
+
});
|
|
1171
|
+
// P1-2 修复:detached + ignore stdio 后,需要 unref 让父进程可以独立退出
|
|
1172
|
+
proc.unref();
|
|
1173
|
+
proc.on('error', (err) => {
|
|
1174
|
+
this._logger?.error(`[F2A Adapter] 启动 daemon 失败: ${err.message}`);
|
|
1175
|
+
resolve(false);
|
|
1176
|
+
});
|
|
1177
|
+
// P2-1 修复:daemon 启动后 CLI 会自动等待服务就绪后退出
|
|
1178
|
+
// 我们只需要等待一段时间后检查 daemon 是否真的在运行
|
|
1179
|
+
setTimeout(async () => {
|
|
1180
|
+
const running = await this._nodeManager?.isRunning();
|
|
1181
|
+
if (running) {
|
|
1182
|
+
this._logger?.info('[F2A Adapter] F2A daemon 服务已就绪');
|
|
1183
|
+
resolve(true);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
this._logger?.warn('[F2A Adapter] F2A daemon 启动超时,请检查日志: ~/.f2a/daemon.log');
|
|
1187
|
+
resolve(false);
|
|
1188
|
+
}
|
|
1189
|
+
}, 5000);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1194
|
+
this._logger?.error(`[F2A Adapter] 启动 daemon 失败: ${errMsg}`);
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 格式化广播结果(公共方法,供测试和外部调用)
|
|
1200
|
+
*/
|
|
1201
|
+
formatBroadcastResults(results) {
|
|
1202
|
+
return results.map(r => {
|
|
1203
|
+
const icon = r.success ? '✅' : '❌';
|
|
1204
|
+
const latency = r.latency ? ` (${r.latency}ms)` : '';
|
|
1205
|
+
return `${icon} ${r.agent}${latency}\n ${r.success ? '完成' : `失败: ${r.error}`}`;
|
|
1206
|
+
}).join('\n\n');
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* 解析 Agent 引用(公共方法,供测试和外部调用)
|
|
1210
|
+
*/
|
|
1211
|
+
async resolveAgent(agentRef) {
|
|
1212
|
+
const result = await this.networkClient?.discoverAgents();
|
|
1213
|
+
if (!result?.success)
|
|
1214
|
+
return null;
|
|
1215
|
+
const agents = result.data || [];
|
|
1216
|
+
// #索引格式
|
|
1217
|
+
if (agentRef.startsWith('#')) {
|
|
1218
|
+
const index = parseInt(agentRef.slice(1)) - 1;
|
|
1219
|
+
return agents[index] || null;
|
|
1220
|
+
}
|
|
1221
|
+
// 精确匹配
|
|
1222
|
+
const exact = agents.find((a) => a.peerId === agentRef ||
|
|
1223
|
+
a.displayName === agentRef);
|
|
1224
|
+
if (exact)
|
|
1225
|
+
return exact;
|
|
1226
|
+
// 模糊匹配
|
|
1227
|
+
const fuzzy = agents.find((a) => a.peerId.startsWith(agentRef) ||
|
|
1228
|
+
(a.displayName?.toLowerCase().includes(agentRef.toLowerCase()) ?? false));
|
|
1229
|
+
return fuzzy || null;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* 关闭插件,清理资源
|
|
1233
|
+
* 只清理已初始化的资源
|
|
1234
|
+
*/
|
|
1235
|
+
async shutdown() {
|
|
1236
|
+
this._logger?.info('[F2A Adapter] 正在关闭...');
|
|
1237
|
+
// 停止轮询定时器
|
|
1238
|
+
if (this.pollTimer) {
|
|
1239
|
+
clearInterval(this.pollTimer);
|
|
1240
|
+
this.pollTimer = undefined;
|
|
1241
|
+
}
|
|
1242
|
+
// 停止 F2A 实例(新架构直接管理)
|
|
1243
|
+
if (this._f2a) {
|
|
1244
|
+
try {
|
|
1245
|
+
await this._f2a.stop();
|
|
1246
|
+
this._logger?.info('[F2A Adapter] F2A 实例已停止');
|
|
1247
|
+
}
|
|
1248
|
+
catch (err) {
|
|
1249
|
+
this._logger?.warn('[F2A Adapter] F2A 实例停止失败', { error: err instanceof Error ? err.message : String(err) });
|
|
1250
|
+
}
|
|
1251
|
+
this._f2a = undefined;
|
|
1252
|
+
}
|
|
1253
|
+
// 停止 Webhook 服务器(只有已启动时才关闭)
|
|
1254
|
+
if (this._webhookServer) {
|
|
1255
|
+
await this._webhookServer.stop?.();
|
|
1256
|
+
this._logger?.info('[F2A Adapter] Webhook 服务器已停止');
|
|
1257
|
+
}
|
|
1258
|
+
// P1 修复:关闭前刷新信誉系统数据,确保持久化
|
|
1259
|
+
if (this._reputationSystem) {
|
|
1260
|
+
this._reputationSystem.flush();
|
|
1261
|
+
this._logger?.info('[F2A Adapter] 信誉系统数据已保存');
|
|
1262
|
+
}
|
|
1263
|
+
// P1 修复:关闭 TaskGuard,停止持久化定时器并保存最终状态
|
|
1264
|
+
task_guard_js_1.taskGuard.shutdown();
|
|
1265
|
+
this._logger?.info('[F2A Adapter] TaskGuard 已关闭');
|
|
1266
|
+
// 停止 F2A Node(只有已启动时才关闭)
|
|
1267
|
+
if (this._nodeManager) {
|
|
1268
|
+
await this._nodeManager.stop();
|
|
1269
|
+
this._logger?.info('[F2A Adapter] F2A Node 管理器已停止');
|
|
1270
|
+
}
|
|
1271
|
+
// 关闭任务队列连接(只有已初始化时才关闭)
|
|
1272
|
+
// 保留持久化数据,不删除任务,这样重启后可以恢复未完成的任务
|
|
1273
|
+
if (this._taskQueue) {
|
|
1274
|
+
this._taskQueue.close();
|
|
1275
|
+
this._logger?.info('[F2A Adapter] 任务队列已关闭,持久化数据已保留');
|
|
1276
|
+
}
|
|
1277
|
+
this._initialized = false;
|
|
1278
|
+
this._logger?.info('[F2A Adapter] 已关闭');
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
exports.F2AOpenClawAdapter = F2AOpenClawAdapter;
|
|
1282
|
+
// 默认导出
|
|
1283
|
+
exports.default = F2AOpenClawAdapter;
|
|
1284
|
+
//# sourceMappingURL=connector.js.map
|