@f2a/openclaw-f2a 0.3.3 → 0.3.4
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/dist/F2AComponentRegistry.d.ts +195 -0
- package/dist/F2AComponentRegistry.d.ts.map +1 -0
- package/dist/F2AComponentRegistry.js +385 -0
- package/dist/F2AComponentRegistry.js.map +1 -0
- package/dist/F2ACore.d.ts +227 -0
- package/dist/F2ACore.d.ts.map +1 -0
- package/dist/F2ACore.js +648 -0
- package/dist/F2ACore.js.map +1 -0
- package/dist/F2AMessageRouter.d.ts +108 -0
- package/dist/F2AMessageRouter.d.ts.map +1 -0
- package/dist/F2AMessageRouter.js +157 -0
- package/dist/F2AMessageRouter.js.map +1 -0
- package/dist/F2AToolRegistry.d.ts +45 -0
- package/dist/F2AToolRegistry.d.ts.map +1 -0
- package/dist/F2AToolRegistry.js +79 -0
- package/dist/F2AToolRegistry.js.map +1 -0
- package/dist/F2AWebhookManager.d.ts +79 -0
- package/dist/F2AWebhookManager.d.ts.map +1 -0
- package/dist/F2AWebhookManager.js +241 -0
- package/dist/F2AWebhookManager.js.map +1 -0
- package/dist/agent-manager.js +6 -6
- package/dist/agent-manager.js.map +1 -1
- package/dist/announcement-queue.d.ts +1 -8
- package/dist/announcement-queue.d.ts.map +1 -1
- package/dist/announcement-queue.js +19 -19
- package/dist/announcement-queue.js.map +1 -1
- package/dist/claim-handlers.d.ts +13 -4
- package/dist/claim-handlers.d.ts.map +1 -1
- package/dist/claim-handlers.js +42 -13
- package/dist/claim-handlers.js.map +1 -1
- package/dist/connector-helpers.d.ts +143 -0
- package/dist/connector-helpers.d.ts.map +1 -0
- package/dist/connector-helpers.js +398 -0
- package/dist/connector-helpers.js.map +1 -0
- package/dist/connector.d.ts +87 -265
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js +334 -2037
- package/dist/connector.js.map +1 -1
- package/dist/contact-manager.d.ts +1 -1
- package/dist/contact-manager.d.ts.map +1 -1
- package/dist/contact-manager.js +12 -5
- package/dist/contact-manager.js.map +1 -1
- package/dist/contact-tool-handlers.d.ts +72 -0
- package/dist/contact-tool-handlers.d.ts.map +1 -0
- package/dist/contact-tool-handlers.js +413 -0
- package/dist/contact-tool-handlers.js.map +1 -0
- package/dist/f2a-client.d.ts +168 -0
- package/dist/f2a-client.d.ts.map +1 -0
- package/dist/f2a-client.js +425 -0
- package/dist/f2a-client.js.map +1 -0
- package/dist/handshake-protocol.d.ts +3 -2
- package/dist/handshake-protocol.d.ts.map +1 -1
- package/dist/handshake-protocol.js +15 -6
- package/dist/handshake-protocol.js.map +1 -1
- package/dist/index.d.ts +12 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -45
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +31 -9
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +268 -12
- package/dist/logger.js.map +1 -1
- package/dist/network-client.d.ts +1 -8
- package/dist/network-client.d.ts.map +1 -1
- package/dist/network-client.js.map +1 -1
- package/dist/node-manager.d.ts +8 -8
- package/dist/node-manager.d.ts.map +1 -1
- package/dist/node-manager.js +100 -36
- package/dist/node-manager.js.map +1 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +43 -23
- package/dist/plugin.js.map +1 -1
- package/dist/task-guard.d.ts.map +1 -1
- package/dist/task-guard.js +44 -38
- package/dist/task-guard.js.map +1 -1
- package/dist/task-queue.d.ts +2 -8
- package/dist/task-queue.d.ts.map +1 -1
- package/dist/task-queue.js +144 -119
- package/dist/task-queue.js.map +1 -1
- package/dist/tool-handlers.d.ts +15 -3
- package/dist/tool-handlers.d.ts.map +1 -1
- package/dist/tool-handlers.js +85 -54
- package/dist/tool-handlers.js.map +1 -1
- package/dist/tools/contact-tools.d.ts +18 -0
- package/dist/tools/contact-tools.d.ts.map +1 -0
- package/dist/tools/contact-tools.js +176 -0
- package/dist/tools/contact-tools.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/network-tools.d.ts +17 -0
- package/dist/tools/network-tools.d.ts.map +1 -0
- package/dist/tools/network-tools.js +106 -0
- package/dist/tools/network-tools.js.map +1 -0
- package/dist/tools/task-tools.d.ts +25 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +275 -0
- package/dist/tools/task-tools.js.map +1 -0
- package/dist/types.d.ts +376 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/webhook-handlers.d.ts +50 -0
- package/dist/webhook-handlers.d.ts.map +1 -0
- package/dist/webhook-handlers.js +183 -0
- package/dist/webhook-handlers.js.map +1 -0
- package/dist/webhook-pusher.d.ts +1 -8
- package/dist/webhook-pusher.d.ts.map +1 -1
- package/dist/webhook-pusher.js.map +1 -1
- package/dist/webhook-server.d.ts +3 -8
- package/dist/webhook-server.d.ts.map +1 -1
- package/dist/webhook-server.js +21 -4
- package/dist/webhook-server.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -4
- package/dist/reputation.d.ts +0 -156
- package/dist/reputation.d.ts.map +0 -1
- package/dist/reputation.js +0 -432
- package/dist/reputation.js.map +0 -1
package/dist/connector.js
CHANGED
|
@@ -1,845 +1,202 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* F2A OpenClaw Connector Plugin
|
|
4
|
-
* 主插件类 -
|
|
4
|
+
* 主插件类 - 协调各模块完成 F2A 功能
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
6
|
+
* Issue #106 重构:拆分并解耦
|
|
7
|
+
* - F2ACore.ts - 核心生命周期管理
|
|
8
|
+
* - F2AComponentRegistry.ts - 组件懒加载管理器
|
|
9
|
+
* - F2AWebhookManager.ts - Webhook 处理逻辑
|
|
10
|
+
* - F2AToolRegistry.ts - 工具注册和路由
|
|
10
11
|
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
11
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
13
|
-
exports.isValidPeerId = isValidPeerId;
|
|
46
|
+
exports.F2APlugin = void 0;
|
|
14
47
|
const path_1 = require("path");
|
|
15
48
|
const os_1 = require("os");
|
|
16
|
-
const
|
|
17
|
-
const network_client_js_1 = require("./network-client.js");
|
|
18
|
-
const webhook_server_js_1 = require("./webhook-server.js");
|
|
19
|
-
const reputation_js_1 = require("./reputation.js");
|
|
20
|
-
const types_js_1 = require("./types.js");
|
|
21
|
-
const capability_detector_js_1 = require("./capability-detector.js");
|
|
22
|
-
const task_queue_js_1 = require("./task-queue.js");
|
|
23
|
-
const announcement_queue_js_1 = require("./announcement-queue.js");
|
|
24
|
-
const webhook_pusher_js_1 = require("./webhook-pusher.js");
|
|
25
|
-
const task_guard_js_1 = require("./task-guard.js");
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
26
50
|
const tool_handlers_js_1 = require("./tool-handlers.js");
|
|
27
51
|
const claim_handlers_js_1 = require("./claim-handlers.js");
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** P1-7: 消息内容最大长度限制 (1MB),防止内存耗尽 */
|
|
37
|
-
const MAX_MESSAGE_LENGTH = 1024 * 1024;
|
|
38
|
-
/** P1-6: PeerID 格式正则(libp2p 格式:12D3KooW...) */
|
|
39
|
-
const PEER_ID_REGEX = /^12D3KooW[A-Za-z1-9]{44}$/;
|
|
40
|
-
/** P1-4: URL 编码的路径遍历模式 */
|
|
41
|
-
const PATH_TRAVERSAL_PATTERNS = [
|
|
42
|
-
'%2e%2e', // URL 编码的 ..
|
|
43
|
-
'%2E%2E', // URL 编码的 .. (大写)
|
|
44
|
-
'%252e', // 双重 URL 编码
|
|
45
|
-
'%c0%ae', // UTF-8 overlong encoding
|
|
46
|
-
'%c1%9c', // UTF-8 overlong encoding
|
|
47
|
-
];
|
|
48
|
-
/**
|
|
49
|
-
* P1-6: 验证 PeerID 格式
|
|
50
|
-
* @param peerId - 待验证的 Peer ID
|
|
51
|
-
* @returns 是否为有效的 libp2p Peer ID 格式
|
|
52
|
-
*/
|
|
53
|
-
function isValidPeerId(peerId) {
|
|
54
|
-
return typeof peerId === 'string' && PEER_ID_REGEX.test(peerId);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* P0-1, P1-4: 验证路径安全性,防止路径遍历攻击
|
|
58
|
-
*
|
|
59
|
-
* 增强版本,处理:
|
|
60
|
-
* - 符号链接(通过 realpath 验证)
|
|
61
|
-
* - URL 编码绕过
|
|
62
|
-
* - 双重编码绕过
|
|
63
|
-
* - UTF-8 overlong encoding
|
|
64
|
-
*
|
|
65
|
-
* @param path - 待验证的路径
|
|
66
|
-
* @param options - 可选的额外验证选项
|
|
67
|
-
* @returns 如果路径安全返回 true,否则返回 false
|
|
68
|
-
*/
|
|
69
|
-
function isPathSafe(path, options) {
|
|
70
|
-
if (typeof path !== 'string' || path.length === 0) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
// 拒绝绝对路径
|
|
74
|
-
if ((0, path_1.isAbsolute)(path)) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
// 拒绝包含路径遍历字符
|
|
78
|
-
if (path.includes('..') || path.includes('\0')) {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
// 拒绝以 ~ 开头的路径(用户目录展开)
|
|
82
|
-
if (path.startsWith('~')) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
// P1-4: 检查 URL 编码的路径遍历模式
|
|
86
|
-
const lowerPath = path.toLowerCase();
|
|
87
|
-
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
88
|
-
if (lowerPath.includes(pattern.toLowerCase())) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// P1-4: 解码 URL 编码后再次检查
|
|
93
|
-
try {
|
|
94
|
-
const decodedPath = decodeURIComponent(path);
|
|
95
|
-
// 解码后再次检查路径遍历
|
|
96
|
-
if (decodedPath.includes('..') || decodedPath.includes('\0')) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
// 检查解码后是否变成绝对路径
|
|
100
|
-
if ((0, path_1.isAbsolute)(decodedPath)) {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// 解码失败可能是恶意构造,拒绝
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
// P1-4: 如果指定了允许的根目录,验证路径不会逃逸
|
|
109
|
-
if (options?.allowedRoot) {
|
|
110
|
-
try {
|
|
111
|
-
const resolvedPath = (0, path_1.join)(options.allowedRoot, path);
|
|
112
|
-
// 检查解析后的路径是否仍在允许的根目录下
|
|
113
|
-
if (!resolvedPath.startsWith(options.allowedRoot)) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* P1-2: 统一错误提取工具函数
|
|
125
|
-
* 从各种错误格式中提取错误消息,添加循环引用保护
|
|
126
|
-
* @param error - 任意错误对象
|
|
127
|
-
* @returns 错误消息字符串
|
|
128
|
-
*/
|
|
129
|
-
function extractErrorMessage(error) {
|
|
130
|
-
try {
|
|
131
|
-
if (error instanceof Error) {
|
|
132
|
-
return error.message;
|
|
133
|
-
}
|
|
134
|
-
if (typeof error === 'string') {
|
|
135
|
-
return error;
|
|
136
|
-
}
|
|
137
|
-
if (typeof error === 'object' && error !== null) {
|
|
138
|
-
// 尝试常见的错误属性
|
|
139
|
-
const err = error;
|
|
140
|
-
if (typeof err.message === 'string') {
|
|
141
|
-
return err.message;
|
|
142
|
-
}
|
|
143
|
-
if (typeof err.error === 'string') {
|
|
144
|
-
return err.error;
|
|
145
|
-
}
|
|
146
|
-
if (typeof err.msg === 'string') {
|
|
147
|
-
return err.msg;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// P1-2: 使用 try-catch 保护 String() 调用,防止循环引用异常
|
|
151
|
-
return String(error);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// 如果 String() 抛出异常(如循环引用),返回安全的默认消息
|
|
155
|
-
return '[Error: Unable to extract error message - possible circular reference]';
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Issue #96: 从 IDENTITY.md 读取 agent 名字
|
|
160
|
-
* @param workspace - agent workspace 目录
|
|
161
|
-
* @returns agent 名字,如果读取失败返回 null
|
|
162
|
-
*/
|
|
163
|
-
function readAgentNameFromIdentity(workspace) {
|
|
164
|
-
if (!workspace) {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
const identityPath = (0, path_1.join)(workspace, 'IDENTITY.md');
|
|
169
|
-
const fs = require('fs');
|
|
170
|
-
if (!fs.existsSync(identityPath)) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
const content = fs.readFileSync(identityPath, 'utf-8');
|
|
174
|
-
// 解析 IDENTITY.md 中的 Name 字段
|
|
175
|
-
// 格式: - **Name:** 猫咕噜 (Cat Guru)
|
|
176
|
-
const nameMatch = content.match(/-\s*\*\*Name:\*\*\s*(.+?)(?:\s*\([^)]*\))?$/m);
|
|
177
|
-
if (nameMatch && nameMatch[1]) {
|
|
178
|
-
const name = nameMatch[1].trim();
|
|
179
|
-
// 移除可能的英文别名(括号内的内容已在正则中处理)
|
|
180
|
-
return name;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
// 读取失败,返回 null
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
class F2AOpenClawAdapter {
|
|
52
|
+
const contact_tool_handlers_js_1 = require("./contact-tool-handlers.js");
|
|
53
|
+
const task_guard_js_1 = require("./task-guard.js");
|
|
54
|
+
const connector_helpers_js_1 = require("./connector-helpers.js");
|
|
55
|
+
const F2ACore_js_1 = require("./F2ACore.js");
|
|
56
|
+
const F2AComponentRegistry_js_1 = require("./F2AComponentRegistry.js");
|
|
57
|
+
const F2AWebhookManager_js_1 = require("./F2AWebhookManager.js");
|
|
58
|
+
const F2AToolRegistry_js_1 = require("./F2AToolRegistry.js");
|
|
59
|
+
class F2APlugin {
|
|
190
60
|
name = 'f2a-openclaw-f2a';
|
|
191
61
|
version = '0.3.0';
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
_reputationSystem;
|
|
197
|
-
_logger;
|
|
198
|
-
_capabilityDetector;
|
|
199
|
-
_taskQueue;
|
|
200
|
-
_announcementQueue;
|
|
201
|
-
_webhookPusher;
|
|
202
|
-
_reviewCommittee;
|
|
203
|
-
// F2A 实例(直接管理模式)
|
|
204
|
-
_f2a;
|
|
62
|
+
// 核心模块
|
|
63
|
+
core;
|
|
64
|
+
components;
|
|
65
|
+
webhookManager;
|
|
205
66
|
// 处理器实例(延迟初始化)
|
|
206
67
|
_toolHandlers;
|
|
207
68
|
_claimHandlers;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
_handshakeProtocol;
|
|
211
|
-
// P1-3: 消息哈希去重缓存,防止恶意节点绕过 echo 检测
|
|
212
|
-
// 存储最近处理过的消息哈希,避免重复处理
|
|
69
|
+
_contactToolHandlers;
|
|
70
|
+
// 消息哈希去重缓存
|
|
213
71
|
_processedMessageHashes = new Map();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
nodeConfig;
|
|
220
|
-
capabilities = [];
|
|
221
|
-
api;
|
|
222
|
-
pollTimer;
|
|
223
|
-
_initialized = false;
|
|
224
|
-
// ========== 懒加载 Getter ==========
|
|
225
|
-
/**
|
|
226
|
-
* 获取节点管理器(懒加载)
|
|
227
|
-
*/
|
|
228
|
-
get nodeManager() {
|
|
229
|
-
if (!this._nodeManager) {
|
|
230
|
-
this._nodeManager = new node_manager_js_1.F2ANodeManager(this.nodeConfig, this._logger);
|
|
231
|
-
}
|
|
232
|
-
return this._nodeManager;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* 获取默认的 F2A 数据目录
|
|
236
|
-
*
|
|
237
|
-
* 优先级:
|
|
238
|
-
* 1. config.dataDir(用户显式配置)
|
|
239
|
-
* 2. workspace/.f2a(agent workspace 目录)
|
|
240
|
-
* 3. ~/.f2a(兼容旧版本)
|
|
241
|
-
*
|
|
242
|
-
* 安全:
|
|
243
|
-
* - P0-1: 验证 workspace 路径,防止路径遍历攻击
|
|
244
|
-
* - P1-1: 优先检查 config.dataDir
|
|
245
|
-
*/
|
|
246
|
-
getDefaultDataDir() {
|
|
247
|
-
// P1-1: 优先使用用户配置的 dataDir
|
|
248
|
-
if (this.config?.dataDir) {
|
|
249
|
-
return this.config.dataDir;
|
|
250
|
-
}
|
|
251
|
-
// 默认:使用 agent workspace 目录
|
|
252
|
-
const workspace = this.api?.config?.agents?.defaults?.workspace;
|
|
253
|
-
// P0-1: 验证 workspace 路径安全性
|
|
254
|
-
if (isPathSafe(workspace)) {
|
|
255
|
-
return (0, path_1.join)(workspace, '.f2a');
|
|
256
|
-
}
|
|
257
|
-
// 兼容旧版本
|
|
258
|
-
return (0, path_1.join)((0, os_1.homedir)(), '.f2a');
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* P1-2, P1-5, P1-3: 检测是否为回声消息(避免循环)
|
|
262
|
-
*
|
|
263
|
-
* 使用多层验证策略:
|
|
264
|
-
* 1. 检查 metadata 中的特定标记(不仅仅是 type === 'reply')
|
|
265
|
-
* 2. 检查消息来源可信度
|
|
266
|
-
* 3. 检查消息内容的特殊标记
|
|
267
|
-
* 4. P1-3: 基于消息内容哈希的去重机制(防止恶意绕过)
|
|
268
|
-
*
|
|
269
|
-
* @param msg - 接收到的消息
|
|
270
|
-
* @returns 是否为应该跳过的回声消息
|
|
271
|
-
*/
|
|
272
|
-
isEchoMessage(msg) {
|
|
273
|
-
const { metadata, content, from } = msg;
|
|
274
|
-
// 层1: 检查 metadata 中的标记
|
|
275
|
-
// P1-5: 不能只依赖 metadata.type,恶意节点可以伪造
|
|
276
|
-
// 我们检查更具体的标记组合
|
|
277
|
-
if (metadata) {
|
|
278
|
-
// 检查是否是我们自己发出的回复标记
|
|
279
|
-
if (metadata.type === 'reply' && metadata.replyTo) {
|
|
280
|
-
// 进一步验证:检查是否来自可信源(我们自己发出的消息)
|
|
281
|
-
// 如果有 replyTo,说明这是一个回复消息
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
// 检查显式的跳过标记
|
|
285
|
-
if (metadata._f2a_skip_echo === true || metadata['x-openclaw-skip'] === true) {
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// 层2: 检查消息内容中的特殊标记
|
|
290
|
-
// P1-2: 使用更严格的匹配,避免误判正常消息
|
|
291
|
-
if (content) {
|
|
292
|
-
// 使用特殊的标记格式 [[F2A:REPLY:...]]
|
|
293
|
-
// 而不是简单的 "NO_REPLY" 字符串
|
|
294
|
-
if (content.includes('[[F2A:REPLY:') || content.includes('[[reply_to_current]]')) {
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
// 检查是否以 NO_REPLY 标记开头(更严格)
|
|
298
|
-
if (content.startsWith('NO_REPLY:') || content.startsWith('[NO_REPLY]')) {
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// 层3: 检查消息来源是否是我们自己的 peerId(防止自循环)
|
|
303
|
-
if (this._f2a && from === this._f2a.peerId) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
// 层4 (P1-3): 基于消息内容哈希的去重机制
|
|
307
|
-
// 防止恶意节点构造不含特定标记的消息绕过检测
|
|
308
|
-
if (content) {
|
|
309
|
-
const messageHash = this.computeMessageHash(from, content);
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
// 检查是否已处理过相同的消息内容
|
|
312
|
-
if (this._processedMessageHashes.has(messageHash)) {
|
|
313
|
-
const processedTime = this._processedMessageHashes.get(messageHash);
|
|
314
|
-
// 如果在 TTL 内,认为是重复消息
|
|
315
|
-
if (now - processedTime < F2AOpenClawAdapter.MESSAGE_HASH_TTL_MS) {
|
|
316
|
-
this._logger?.debug?.(`[F2A Adapter] 检测到重复消息(哈希去重): ${messageHash.slice(0, 16)}...`);
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// 记录此消息哈希
|
|
321
|
-
this._processedMessageHashes.set(messageHash, now);
|
|
322
|
-
// 清理过期的条目(防止内存泄漏)
|
|
323
|
-
if (this._processedMessageHashes.size > F2AOpenClawAdapter.MAX_MESSAGE_HASH_CACHE_SIZE) {
|
|
324
|
-
this.cleanupMessageHashCache(now);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* P1-3: 计算消息内容哈希
|
|
331
|
-
* 用于基于内容的去重
|
|
332
|
-
*/
|
|
333
|
-
computeMessageHash(from, content) {
|
|
334
|
-
// 使用简单的哈希算法,避免依赖 crypto 模块
|
|
335
|
-
const data = `${from}:${content}`;
|
|
336
|
-
let hash = 0;
|
|
337
|
-
for (let i = 0; i < data.length; i++) {
|
|
338
|
-
const char = data.charCodeAt(i);
|
|
339
|
-
hash = ((hash << 5) - hash) + char;
|
|
340
|
-
hash = hash & hash; // Convert to 32bit integer
|
|
341
|
-
}
|
|
342
|
-
return `msg-${hash.toString(16)}-${data.length}`;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* P1-3: 清理过期的消息哈希缓存
|
|
346
|
-
*/
|
|
347
|
-
cleanupMessageHashCache(now) {
|
|
348
|
-
const ttl = F2AOpenClawAdapter.MESSAGE_HASH_TTL_MS;
|
|
349
|
-
for (const [hash, timestamp] of this._processedMessageHashes.entries()) {
|
|
350
|
-
if (now - timestamp > ttl) {
|
|
351
|
-
this._processedMessageHashes.delete(hash);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* 获取网络客户端(懒加载)
|
|
357
|
-
* 新架构:直接使用 F2A 实例的方法,不再通过 HTTP
|
|
358
|
-
*/
|
|
359
|
-
get networkClient() {
|
|
360
|
-
if (!this._networkClient) {
|
|
361
|
-
this._networkClient = new network_client_js_1.F2ANetworkClient(this.nodeConfig, this._logger);
|
|
362
|
-
}
|
|
363
|
-
return this._networkClient;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* 获取任务队列(懒加载)
|
|
367
|
-
* 只有在真正需要处理任务时才初始化 SQLite 数据库
|
|
368
|
-
*/
|
|
369
|
-
get taskQueue() {
|
|
370
|
-
if (!this._taskQueue) {
|
|
371
|
-
const dataDir = this.getDefaultDataDir();
|
|
372
|
-
this._taskQueue = new task_queue_js_1.TaskQueue({
|
|
373
|
-
maxSize: this.config.maxQueuedTasks || 100,
|
|
374
|
-
maxAgeMs: 24 * 60 * 60 * 1000, // 24小时
|
|
375
|
-
persistDir: dataDir,
|
|
376
|
-
persistEnabled: true,
|
|
377
|
-
logger: this._logger
|
|
378
|
-
});
|
|
379
|
-
this._logger?.info('[F2A Adapter] TaskQueue 已初始化(懒加载)');
|
|
380
|
-
}
|
|
381
|
-
return this._taskQueue;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* 获取信誉系统(懒加载)
|
|
385
|
-
*/
|
|
386
|
-
get reputationSystem() {
|
|
387
|
-
if (!this._reputationSystem) {
|
|
388
|
-
this._reputationSystem = new reputation_js_1.ReputationSystem({
|
|
389
|
-
enabled: types_js_1.INTERNAL_REPUTATION_CONFIG.enabled,
|
|
390
|
-
initialScore: types_js_1.INTERNAL_REPUTATION_CONFIG.initialScore,
|
|
391
|
-
minScoreForService: types_js_1.INTERNAL_REPUTATION_CONFIG.minScoreForService,
|
|
392
|
-
decayRate: types_js_1.INTERNAL_REPUTATION_CONFIG.decayRate,
|
|
393
|
-
}, this.getDefaultDataDir());
|
|
394
|
-
}
|
|
395
|
-
return this._reputationSystem;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* 获取能力检测器(懒加载)
|
|
399
|
-
*/
|
|
400
|
-
get capabilityDetector() {
|
|
401
|
-
if (!this._capabilityDetector) {
|
|
402
|
-
this._capabilityDetector = new capability_detector_js_1.CapabilityDetector();
|
|
403
|
-
}
|
|
404
|
-
return this._capabilityDetector;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* 获取广播队列(懒加载)
|
|
408
|
-
*/
|
|
409
|
-
get announcementQueue() {
|
|
410
|
-
if (!this._announcementQueue) {
|
|
411
|
-
this._announcementQueue = new announcement_queue_js_1.AnnouncementQueue({
|
|
412
|
-
maxSize: 50,
|
|
413
|
-
maxAgeMs: 30 * 60 * 1000, // 30分钟
|
|
414
|
-
logger: this._logger
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
return this._announcementQueue;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* 获取评审委员会(懒加载)
|
|
421
|
-
*/
|
|
422
|
-
get reviewCommittee() {
|
|
423
|
-
if (!this._reviewCommittee) {
|
|
424
|
-
const reputationAdapter = new reputation_js_1.ReputationManagerAdapter(this.reputationSystem);
|
|
425
|
-
this._reviewCommittee = new network_1.ReviewCommittee(reputationAdapter, {
|
|
426
|
-
minReviewers: 1,
|
|
427
|
-
maxReviewers: 5,
|
|
428
|
-
minReputation: types_js_1.INTERNAL_REPUTATION_CONFIG.minScoreForReview,
|
|
429
|
-
reviewTimeout: 5 * 60 * 1000 // 5 分钟
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
return this._reviewCommittee;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* 获取工具处理器(延迟初始化,支持未初始化时调用getTools)
|
|
436
|
-
*/
|
|
72
|
+
// 配置
|
|
73
|
+
_pluginConfig;
|
|
74
|
+
_nodeConfig;
|
|
75
|
+
_pluginApi;
|
|
76
|
+
// ========== 处理器 Getter ==========
|
|
437
77
|
get toolHandlers() {
|
|
438
|
-
if (!this._toolHandlers)
|
|
78
|
+
if (!this._toolHandlers)
|
|
439
79
|
this._toolHandlers = new tool_handlers_js_1.ToolHandlers(this);
|
|
440
|
-
}
|
|
441
80
|
return this._toolHandlers;
|
|
442
81
|
}
|
|
443
|
-
/**
|
|
444
|
-
* 获取认领处理器(延迟初始化)
|
|
445
|
-
*/
|
|
446
82
|
get claimHandlers() {
|
|
447
|
-
if (!this._claimHandlers)
|
|
83
|
+
if (!this._claimHandlers)
|
|
448
84
|
this._claimHandlers = new claim_handlers_js_1.ClaimHandlers(this);
|
|
449
|
-
}
|
|
450
85
|
return this._claimHandlers;
|
|
451
86
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (!this._contactManager) {
|
|
457
|
-
const dataDir = this.getDefaultDataDir();
|
|
458
|
-
this._contactManager = new contact_manager_js_1.ContactManager(dataDir, this._logger);
|
|
459
|
-
this._logger?.info('[F2A Adapter] ContactManager 已初始化');
|
|
460
|
-
}
|
|
461
|
-
return this._contactManager;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Issue #99: 获取握手协议处理器(延迟初始化)
|
|
465
|
-
* 依赖 F2A 实例和 ContactManager
|
|
466
|
-
* P2-3 修复:传递握手配置
|
|
467
|
-
*/
|
|
468
|
-
get handshakeProtocol() {
|
|
469
|
-
if (!this._handshakeProtocol && this._f2a && this._contactManager) {
|
|
470
|
-
this._handshakeProtocol = new handshake_protocol_js_1.HandshakeProtocol(this._f2a, this._contactManager, this._logger, this.config.handshake // P2-3 修复:传递配置
|
|
471
|
-
);
|
|
472
|
-
this._logger?.info('[F2A Adapter] HandshakeProtocol 已初始化');
|
|
473
|
-
}
|
|
474
|
-
return this._handshakeProtocol;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* 检查是否已初始化(用于判断是否需要启动服务)
|
|
478
|
-
*/
|
|
479
|
-
isInitialized() {
|
|
480
|
-
return this._initialized;
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* 获取 F2A 状态(供 tool-handlers 使用)
|
|
484
|
-
*/
|
|
485
|
-
getF2AStatus() {
|
|
486
|
-
if (!this._f2a) {
|
|
487
|
-
return { running: false };
|
|
488
|
-
}
|
|
489
|
-
return {
|
|
490
|
-
running: true,
|
|
491
|
-
peerId: this._f2a.peerId,
|
|
492
|
-
uptime: this._f2a.startTime ? Date.now() - this._f2a.startTime : undefined
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* 获取 F2A Client(供 tool-handlers 使用)
|
|
497
|
-
* 直接访问 F2A 实例的方法
|
|
498
|
-
*/
|
|
499
|
-
get f2aClient() {
|
|
500
|
-
return {
|
|
501
|
-
discoverAgents: async (capability) => {
|
|
502
|
-
if (!this._f2a) {
|
|
503
|
-
return { success: false, error: { message: 'F2A 实例未初始化' } };
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
const agents = await this._f2a.discoverAgents(capability);
|
|
507
|
-
return { success: true, data: agents };
|
|
508
|
-
}
|
|
509
|
-
catch (err) {
|
|
510
|
-
return { success: false, error: { message: extractErrorMessage(err) } };
|
|
511
|
-
}
|
|
512
|
-
},
|
|
513
|
-
getConnectedPeers: async () => {
|
|
514
|
-
if (!this._f2a) {
|
|
515
|
-
return { success: false, error: { message: 'F2A 实例未初始化' } };
|
|
516
|
-
}
|
|
517
|
-
try {
|
|
518
|
-
// 从 F2A 实例获取连接的 peers
|
|
519
|
-
const peers = this._f2a.p2pNetwork?.getConnectedPeers?.() || [];
|
|
520
|
-
return { success: true, data: peers };
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
return { success: false, error: { message: extractErrorMessage(err) } };
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* 初始化插件
|
|
530
|
-
*
|
|
531
|
-
* 架构重构:延迟初始化策略
|
|
532
|
-
* - 构造函数/initialize 只保存配置,不打开任何资源
|
|
533
|
-
* - TaskQueue/WebhookServer 在首次访问时才初始化
|
|
534
|
-
* - 这允许 `openclaw gateway status` 等 CLI 命令能正常退出
|
|
535
|
-
*/
|
|
536
|
-
async initialize(config) {
|
|
537
|
-
// 保存 OpenClaw logger(统一日志格式)
|
|
538
|
-
this._logger = config._api?.logger;
|
|
539
|
-
this._logger?.info('[F2A Adapter] 初始化(延迟模式)...');
|
|
540
|
-
// 保存 API 引用(用于触发心跳等)
|
|
541
|
-
this.api = config._api;
|
|
542
|
-
// 合并配置(只保存,不初始化资源)
|
|
543
|
-
this.config = this.mergeConfig(config);
|
|
544
|
-
this.nodeConfig = {
|
|
545
|
-
nodePath: this.config.f2aPath || './F2A',
|
|
546
|
-
controlPort: this.config.controlPort || 9001,
|
|
547
|
-
controlToken: this.config.controlToken || this.generateToken(),
|
|
548
|
-
p2pPort: this.config.p2pPort || 9000,
|
|
549
|
-
enableMDNS: this.config.enableMDNS ?? true,
|
|
550
|
-
bootstrapPeers: this.config.bootstrapPeers || [],
|
|
551
|
-
dataDir: this.config.dataDir
|
|
552
|
-
};
|
|
553
|
-
// 初始化 Webhook 推送器(如果配置了)
|
|
554
|
-
if (this.config.webhookPush?.enabled !== false && this.config.webhookPush?.url) {
|
|
555
|
-
this._webhookPusher = new webhook_pusher_js_1.WebhookPusher(this.config.webhookPush, this._logger);
|
|
556
|
-
this._logger?.info('[F2A Adapter] Webhook 推送已配置');
|
|
557
|
-
}
|
|
558
|
-
// 检测能力(基于配置,不依赖 OpenClaw 会话)
|
|
559
|
-
// 这是轻量级操作,不需要延迟
|
|
560
|
-
this.capabilities = this.capabilityDetector.getDefaultCapabilities();
|
|
561
|
-
if (this.config.capabilities?.length) {
|
|
562
|
-
this.capabilities = this.capabilityDetector.mergeCustomCapabilities(this.capabilities, this.config.capabilities);
|
|
563
|
-
}
|
|
564
|
-
// 注意:registerCleanupHandlers() 移到 enable() 中调用
|
|
565
|
-
// 因为它注册 process.on 事件处理器,会阻止 CLI 进程退出
|
|
566
|
-
this._logger?.info('[F2A Adapter] 初始化完成(延迟模式)');
|
|
567
|
-
this._logger?.info(`[F2A Adapter] Agent 名称: ${this.config.agentName}`);
|
|
568
|
-
this._logger?.info(`[F2A Adapter] 能力数: ${this.capabilities.length}`);
|
|
569
|
-
this._logger?.info('[F2A Adapter] 资源将在首次使用时初始化(TaskQueue/WebhookServer 等)');
|
|
87
|
+
get contactToolHandlers() {
|
|
88
|
+
if (!this._contactToolHandlers)
|
|
89
|
+
this._contactToolHandlers = new contact_tool_handlers_js_1.ContactToolHandlers(this);
|
|
90
|
+
return this._contactToolHandlers;
|
|
570
91
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
// 直接创建 F2A 实例(新架构)
|
|
587
|
-
try {
|
|
588
|
-
// 使用统一的默认数据目录计算方法
|
|
589
|
-
const dataDir = this.getDefaultDataDir();
|
|
590
|
-
// 文件日志确保不被丢失
|
|
591
|
-
const debugLog = (msg) => {
|
|
592
|
-
const fs = require('fs');
|
|
593
|
-
const logPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw/logs/adapter-debug.log');
|
|
594
|
-
try {
|
|
595
|
-
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${msg}\n`);
|
|
596
|
-
}
|
|
597
|
-
catch { }
|
|
598
|
-
console.log(msg);
|
|
599
|
-
this._logger?.info(msg);
|
|
600
|
-
};
|
|
601
|
-
debugLog(`[F2A Adapter] 使用数据目录: ${dataDir}`);
|
|
602
|
-
debugLog(`[F2A Adapter] workspace: ${this.api?.config?.agents?.defaults?.workspace}`);
|
|
603
|
-
debugLog(`[F2A Adapter] config.dataDir: ${this.config.dataDir}`);
|
|
604
|
-
// Issue #96: 从 IDENTITY.md 读取 agent 名字
|
|
605
|
-
const workspace = this.api?.config?.agents?.defaults?.workspace;
|
|
606
|
-
const identityName = readAgentNameFromIdentity(workspace);
|
|
607
|
-
// 优先级:IDENTITY.md > config.agentName > 默认值
|
|
608
|
-
const displayName = identityName || this.config.agentName || 'OpenClaw Agent';
|
|
609
|
-
if (identityName) {
|
|
610
|
-
debugLog(`[F2A Adapter] 从 IDENTITY.md 读取 agent 名字: ${identityName}`);
|
|
611
|
-
}
|
|
612
|
-
this._f2a = await network_1.F2A.create({
|
|
613
|
-
displayName,
|
|
614
|
-
dataDir,
|
|
615
|
-
network: {
|
|
616
|
-
listenPort: this.config.p2pPort || 0,
|
|
617
|
-
bootstrapPeers: this.config.bootstrapPeers || [],
|
|
618
|
-
enableMDNS: this.config.enableMDNS ?? true,
|
|
619
|
-
enableDHT: false,
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
// 监听 P2P 消息,调用 OpenClaw Agent 生成回复
|
|
623
|
-
this._f2a.on('message', async (msg) => {
|
|
624
|
-
// P1-6: 验证 PeerID 格式
|
|
625
|
-
if (!isValidPeerId(msg.from)) {
|
|
626
|
-
this._logger?.warn(`[F2A Adapter] 拒绝来自无效 PeerID 的消息: ${String(msg.from).slice(0, 20)}`);
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
// P1-7: 检查消息长度限制
|
|
630
|
-
if (msg.content && msg.content.length > MAX_MESSAGE_LENGTH) {
|
|
631
|
-
this._logger?.warn(`[F2A Adapter] 消息过长 (${msg.content.length} bytes),拒绝处理`);
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
const logMsg = `[F2A Adapter] 收到 P2P 消息: from=${msg.from.slice(0, 16)}, content=${msg.content?.slice(0, 50)}`;
|
|
635
|
-
this._logger?.info(logMsg);
|
|
636
|
-
// 写入文件日志
|
|
637
|
-
try {
|
|
638
|
-
const fs = require('fs');
|
|
639
|
-
const logPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw/logs/adapter-debug.log');
|
|
640
|
-
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${logMsg}\n`);
|
|
641
|
-
}
|
|
642
|
-
catch { }
|
|
643
|
-
try {
|
|
644
|
-
// P1-2, P1-5: 改进的回声循环检测
|
|
645
|
-
// 使用更严格的检测逻辑,防止恶意绕过
|
|
646
|
-
const isReply = this.isEchoMessage(msg);
|
|
647
|
-
if (isReply) {
|
|
648
|
-
this._logger?.info('[F2A Adapter] 跳过 Agent 回复,避免回声循环');
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
// 调用 OpenClaw Agent 生成回复
|
|
652
|
-
const reply = await this.invokeOpenClawAgent(msg.from, msg.content, msg.messageId);
|
|
653
|
-
// 发送回复
|
|
654
|
-
if (reply && this._f2a) {
|
|
655
|
-
await this._f2a.sendMessage(msg.from, reply, { type: 'reply', replyTo: msg.messageId });
|
|
656
|
-
this._logger?.info('[F2A Adapter] 回复已发送', { to: msg.from.slice(0, 16) });
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
catch (err) {
|
|
660
|
-
this._logger?.error('[F2A Adapter] 处理消息失败', { error: extractErrorMessage(err) });
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
// 监听其他事件
|
|
664
|
-
this._f2a.on('peer:connected', (event) => {
|
|
665
|
-
this._logger?.info('[F2A Adapter] Peer 连接', { peerId: event.peerId.slice(0, 16) });
|
|
666
|
-
});
|
|
667
|
-
this._f2a.on('peer:disconnected', (event) => {
|
|
668
|
-
this._logger?.info('[F2A Adapter] Peer 断开', { peerId: event.peerId.slice(0, 16) });
|
|
669
|
-
});
|
|
670
|
-
// 启动 F2A(带超时保护,避免阻塞 Gateway)
|
|
671
|
-
const START_TIMEOUT_MS = 10000; // 10 秒超时
|
|
672
|
-
const startPromise = this._f2a.start();
|
|
673
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
674
|
-
const timer = setTimeout(() => {
|
|
675
|
-
reject(new Error('F2A 启动超时'));
|
|
676
|
-
}, START_TIMEOUT_MS);
|
|
677
|
-
timer.unref(); // 确保 Gateway 可以退出
|
|
678
|
-
});
|
|
679
|
-
const result = await Promise.race([startPromise, timeoutPromise]);
|
|
680
|
-
if (!result.success) {
|
|
681
|
-
throw new Error(`F2A 启动失败: ${result.error}`);
|
|
92
|
+
// ========== 消息处理 ==========
|
|
93
|
+
isEchoMessage(msg) {
|
|
94
|
+
const { metadata, content, from } = msg;
|
|
95
|
+
if ((0, connector_helpers_js_1.isEchoMessageByMetadata)(metadata))
|
|
96
|
+
return true;
|
|
97
|
+
if ((0, connector_helpers_js_1.isEchoMessageByContent)(content))
|
|
98
|
+
return true;
|
|
99
|
+
const f2a = this.core?.getF2A();
|
|
100
|
+
if (f2a && from === f2a.peerId)
|
|
101
|
+
return true;
|
|
102
|
+
if (content && content.length > connector_helpers_js_1.MESSAGE_HASH_THRESHOLD) {
|
|
103
|
+
const messageHash = (0, connector_helpers_js_1.computeMessageHash)(from, content);
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if ((0, connector_helpers_js_1.isDuplicateMessage)(this._processedMessageHashes, messageHash, now)) {
|
|
106
|
+
return true;
|
|
682
107
|
}
|
|
683
|
-
this.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
catch (err) {
|
|
689
|
-
const errorMsg = extractErrorMessage(err);
|
|
690
|
-
this._logger?.error(`[F2A Adapter] 创建 F2A 实例失败: ${errorMsg}`);
|
|
691
|
-
this._logger?.warn('[F2A Adapter] F2A Adapter 将以降级模式运行,P2P 功能不可用');
|
|
692
|
-
// 清理失败的实例
|
|
693
|
-
if (this._f2a) {
|
|
694
|
-
try {
|
|
695
|
-
await this._f2a.stop();
|
|
696
|
-
}
|
|
697
|
-
catch { }
|
|
698
|
-
this._f2a = undefined;
|
|
108
|
+
this._processedMessageHashes.set(messageHash, now);
|
|
109
|
+
if (this._processedMessageHashes.size > connector_helpers_js_1.MAX_MESSAGE_HASH_CACHE_SIZE) {
|
|
110
|
+
(0, connector_helpers_js_1.cleanupMessageHashCache)(this._processedMessageHashes, now);
|
|
699
111
|
}
|
|
700
112
|
}
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
this._webhookServer = new webhook_server_js_1.WebhookServer(this.config.webhookPort || 0, this.createWebhookHandler(), { logger: this._logger });
|
|
704
|
-
await this._webhookServer.start();
|
|
705
|
-
this._logger?.info(`[F2A Adapter] Webhook 服务器已启动: ${this._webhookServer.getUrl()}`);
|
|
706
|
-
}
|
|
707
|
-
catch (err) {
|
|
708
|
-
const errorMsg = extractErrorMessage(err);
|
|
709
|
-
this._logger?.warn(`[F2A Adapter] Webhook 服务器启动失败: ${errorMsg}`);
|
|
710
|
-
}
|
|
711
|
-
// 启动兜底轮询
|
|
712
|
-
this.startFallbackPolling();
|
|
713
|
-
if (this._f2a) {
|
|
714
|
-
this._logger?.info(`[F2A Adapter] P2P 已就绪,Peer ID: ${this._f2a.peerId?.slice(0, 20)}...`);
|
|
715
|
-
}
|
|
113
|
+
return false;
|
|
716
114
|
}
|
|
717
|
-
/**
|
|
718
|
-
* 调用 OpenClaw Agent 生成回复
|
|
719
|
-
* 使用 OpenClaw Plugin API 而不是 CLI
|
|
720
|
-
*/
|
|
721
|
-
/**
|
|
722
|
-
* 创建 F2A 回复 Dispatcher
|
|
723
|
-
* 参考 feishu 插件的 createFeishuReplyDispatcher
|
|
724
|
-
*
|
|
725
|
-
* Dispatcher 定义了如何将 Agent 的回复发送回 P2P 网络
|
|
726
|
-
*/
|
|
727
115
|
createF2AReplyDispatcher(fromPeerId, messageId) {
|
|
728
116
|
const sendReply = async (text) => {
|
|
729
|
-
|
|
117
|
+
const f2a = this.core?.getF2A();
|
|
118
|
+
if (!f2a || !text?.trim())
|
|
730
119
|
return;
|
|
731
|
-
}
|
|
732
120
|
try {
|
|
733
|
-
await
|
|
734
|
-
|
|
735
|
-
replyTo: messageId,
|
|
736
|
-
});
|
|
737
|
-
this._logger?.info('[F2A Adapter] 回复已发送', { to: fromPeerId.slice(0, 16) });
|
|
121
|
+
await f2a.sendMessage(fromPeerId, text, { type: 'reply', replyTo: messageId });
|
|
122
|
+
this.core?.getLogger()?.info('[F2A] 回复已发送', { to: fromPeerId.slice(0, 16) });
|
|
738
123
|
}
|
|
739
124
|
catch (err) {
|
|
740
|
-
this.
|
|
125
|
+
this.core?.getLogger()?.error('[F2A] 发送回复失败', { error: (0, connector_helpers_js_1.extractErrorMessage)(err) });
|
|
741
126
|
}
|
|
742
127
|
};
|
|
743
|
-
// 返回 dispatcher 对象,格式与 OpenClaw 兼容
|
|
744
128
|
return {
|
|
745
|
-
deliver: async (payload
|
|
129
|
+
deliver: async (payload) => {
|
|
746
130
|
const text = payload.text ?? '';
|
|
747
|
-
if (!text.trim())
|
|
131
|
+
if (!text.trim())
|
|
748
132
|
return;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const chunkLimit = 4000;
|
|
752
|
-
for (let i = 0; i < text.length; i += chunkLimit) {
|
|
753
|
-
const chunk = text.slice(i, i + chunkLimit);
|
|
754
|
-
await sendReply(chunk);
|
|
755
|
-
}
|
|
133
|
+
for (let i = 0; i < text.length; i += 4000)
|
|
134
|
+
await sendReply(text.slice(i, i + 4000));
|
|
756
135
|
},
|
|
757
136
|
};
|
|
758
137
|
}
|
|
759
|
-
/**
|
|
760
|
-
* 调用 OpenClaw Agent 生成回复
|
|
761
|
-
* 参考 feishu 插件实现,使用 api.channel.reply.dispatchReplyFromConfig
|
|
762
|
-
*/
|
|
763
138
|
async invokeOpenClawAgent(fromPeerId, message, replyToMessageId) {
|
|
764
|
-
|
|
139
|
+
// SessionKey: subagent:f2a:<peerId>
|
|
140
|
+
// 使用 subagent 前缀,禁用 MEMORY.md 加载(群聊记忆隔离)
|
|
141
|
+
// 每个 peer 独立 session,支持跨对话记忆
|
|
142
|
+
const sessionKey = `subagent:f2a:${fromPeerId}`;
|
|
143
|
+
const logger = this.core?.getLogger();
|
|
144
|
+
const f2aDispatcher = this.createF2AReplyDispatcher(fromPeerId, replyToMessageId);
|
|
145
|
+
// P1-5 修复:添加日志文件大小限制(与 F2ACore.ts 保持一致)
|
|
146
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
|
|
765
147
|
const debugLog = (msg) => {
|
|
766
148
|
try {
|
|
767
|
-
const
|
|
768
|
-
|
|
149
|
+
const logPath = (0, path_1.join)((0, os_1.homedir)(), '.openclaw/logs/f2a-debug.log');
|
|
150
|
+
// P1-5 修复:检查日志文件大小,超过限制则截断
|
|
151
|
+
try {
|
|
152
|
+
const stats = fs.statSync(logPath);
|
|
153
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
154
|
+
// 保留最后 1MB 的日志
|
|
155
|
+
const keepSize = 1024 * 1024;
|
|
156
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
const keepLines = lines.slice(-Math.floor(keepSize / 100)); // 估计每行 100 字节
|
|
159
|
+
fs.writeFileSync(logPath, keepLines.join('\n'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// 文件不存在或其他错误,忽略
|
|
164
|
+
}
|
|
769
165
|
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${msg}\n`);
|
|
770
166
|
}
|
|
771
167
|
catch { }
|
|
772
|
-
|
|
168
|
+
logger?.info(msg);
|
|
773
169
|
};
|
|
774
|
-
debugLog(`[F2A
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const f2aDispatcher = this.createF2AReplyDispatcher(fromPeerId, replyToMessageId);
|
|
778
|
-
// 使用 OpenClaw Channel API (参考飞书插件)
|
|
779
|
-
if (this.api?.channel?.reply?.dispatchReplyFromConfig) {
|
|
780
|
-
debugLog('[F2A Adapter] 使用 Channel API');
|
|
170
|
+
debugLog(`[F2A] invokeOpenClawAgent: sessionKey=${sessionKey}`);
|
|
171
|
+
// Channel API
|
|
172
|
+
if (this._pluginApi?.channel?.reply?.dispatchReplyFromConfig) {
|
|
781
173
|
try {
|
|
782
|
-
const route = this.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
SessionKey: route.sessionKey,
|
|
787
|
-
PeerId: sessionKey,
|
|
788
|
-
Sender: 'F2P P2P',
|
|
789
|
-
SenderId: fromPeerId.slice(0, 16),
|
|
790
|
-
ChannelType: 'p2p',
|
|
791
|
-
InboundId: fromPeerId.slice(0, 16),
|
|
174
|
+
const route = this._pluginApi.channel.routing.resolveAgentRoute({ peerId: sessionKey });
|
|
175
|
+
const ctx = this._pluginApi.channel.reply.finalizeInboundContext({
|
|
176
|
+
SessionKey: route.sessionKey, PeerId: sessionKey, Sender: 'F2P P2P',
|
|
177
|
+
SenderId: fromPeerId, ChannelType: 'p2p', InboundId: fromPeerId,
|
|
792
178
|
});
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
ctx,
|
|
796
|
-
cfg: this.config,
|
|
797
|
-
dispatcher: f2aDispatcher,
|
|
798
|
-
});
|
|
799
|
-
debugLog(`[F2A Adapter] Channel API 完成: ${JSON.stringify(result)}`);
|
|
800
|
-
return undefined; // dispatcher 会自动发送回复
|
|
179
|
+
await this._pluginApi.channel.reply.dispatchReplyFromConfig({ ctx, cfg: this._pluginConfig, dispatcher: f2aDispatcher });
|
|
180
|
+
return undefined;
|
|
801
181
|
}
|
|
802
182
|
catch (err) {
|
|
803
|
-
debugLog(`[F2A
|
|
183
|
+
debugLog(`[F2A] Channel API 失败: ${(0, connector_helpers_js_1.extractErrorMessage)(err)}`);
|
|
804
184
|
}
|
|
805
185
|
}
|
|
806
|
-
//
|
|
807
|
-
if (this.
|
|
808
|
-
debugLog('[F2A Adapter] 使用 Subagent API');
|
|
186
|
+
// Subagent API
|
|
187
|
+
if (this._pluginApi?.runtime?.subagent?.run) {
|
|
809
188
|
try {
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
const runResult = await this.api.runtime.subagent.run({
|
|
814
|
-
sessionKey,
|
|
815
|
-
message,
|
|
816
|
-
deliver: false,
|
|
817
|
-
idempotencyKey,
|
|
818
|
-
});
|
|
819
|
-
const waitResult = await this.api.runtime.subagent.waitForRun({
|
|
820
|
-
runId: runResult.runId,
|
|
821
|
-
timeoutMs: 60000,
|
|
822
|
-
});
|
|
189
|
+
const idempotencyKey = `subagent:f2a:${fromPeerId}-${Date.now()}`;
|
|
190
|
+
const runResult = await this._pluginApi.runtime.subagent.run({ sessionKey, message, deliver: false, idempotencyKey });
|
|
191
|
+
const waitResult = await this._pluginApi.runtime.subagent.waitForRun({ runId: runResult.runId, timeoutMs: 60000 });
|
|
823
192
|
if (waitResult.status === 'ok') {
|
|
824
|
-
const messagesResult = await this.
|
|
825
|
-
|
|
826
|
-
limit: 1,
|
|
827
|
-
});
|
|
828
|
-
if (messagesResult.messages && messagesResult.messages.length > 0) {
|
|
193
|
+
const messagesResult = await this._pluginApi.runtime.subagent.getSessionMessages({ sessionKey, limit: 1 });
|
|
194
|
+
if (messagesResult.messages?.length > 0) {
|
|
829
195
|
const lastMessage = messagesResult.messages[messagesResult.messages.length - 1];
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// 找到 type='text' 的元素
|
|
834
|
-
const textBlock = lastMessage.content.find((block) => block.type === 'text');
|
|
835
|
-
reply = textBlock?.text || '';
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
reply = lastMessage?.content || lastMessage?.text || '';
|
|
839
|
-
}
|
|
840
|
-
debugLog(`[F2A Adapter] Subagent 回复文本: ${reply?.slice(0, 100)}...`);
|
|
196
|
+
const reply = Array.isArray(lastMessage?.content)
|
|
197
|
+
? lastMessage.content.find((b) => b.type === 'text')?.text || ''
|
|
198
|
+
: lastMessage?.content || lastMessage?.text || '';
|
|
841
199
|
if (reply) {
|
|
842
|
-
// 手动发送回复
|
|
843
200
|
await f2aDispatcher.deliver({ text: reply });
|
|
844
201
|
return undefined;
|
|
845
202
|
}
|
|
@@ -847,1323 +204,263 @@ class F2AOpenClawAdapter {
|
|
|
847
204
|
}
|
|
848
205
|
}
|
|
849
206
|
catch (err) {
|
|
850
|
-
debugLog(`[F2A
|
|
207
|
+
debugLog(`[F2A] Subagent 失败: ${(0, connector_helpers_js_1.extractErrorMessage)(err)}`);
|
|
851
208
|
}
|
|
852
209
|
}
|
|
853
|
-
//
|
|
854
|
-
|
|
855
|
-
const fallbackReply = `收到你的消息:"${message.slice(0, 30)}"。我是 ${this.config.agentName || 'OpenClaw Agent'},很高兴与你交流!`;
|
|
210
|
+
// 降级回复
|
|
211
|
+
const fallbackReply = `收到你的消息:"${message.slice(0, 30)}"。我是 ${this._pluginConfig.agentName || 'OpenClaw Agent'},很高兴与你交流!`;
|
|
856
212
|
await f2aDispatcher.deliver({ text: fallbackReply });
|
|
857
213
|
return undefined;
|
|
858
214
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
catch { }
|
|
871
|
-
}
|
|
872
|
-
// 同步关闭其他资源
|
|
873
|
-
if (this.pollTimer) {
|
|
874
|
-
clearInterval(this.pollTimer);
|
|
875
|
-
this.pollTimer = undefined;
|
|
876
|
-
}
|
|
877
|
-
if (this._webhookServer) {
|
|
878
|
-
try {
|
|
879
|
-
this._webhookServer.server?.close();
|
|
880
|
-
}
|
|
881
|
-
catch { }
|
|
882
|
-
}
|
|
883
|
-
if (this._taskQueue) {
|
|
884
|
-
try {
|
|
885
|
-
this._taskQueue.close();
|
|
886
|
-
}
|
|
887
|
-
catch { }
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
process.once('beforeExit', autoCleanup);
|
|
891
|
-
process.once('SIGINT', autoCleanup);
|
|
892
|
-
process.once('SIGTERM', autoCleanup);
|
|
215
|
+
// ========== 插件生命周期 ==========
|
|
216
|
+
async initialize(config) {
|
|
217
|
+
this._pluginApi = config._api;
|
|
218
|
+
const logger = config._api?.logger;
|
|
219
|
+
this.core = new F2ACore_js_1.F2ACore({ pluginConfig: {}, nodeConfig: {}, api: this._pluginApi, logger });
|
|
220
|
+
await this.core.initialize(config);
|
|
221
|
+
this._pluginConfig = this.core.getConfig();
|
|
222
|
+
this._nodeConfig = this.core.getNodeConfig();
|
|
223
|
+
this.components = new F2AComponentRegistry_js_1.F2AComponentRegistry({ pluginConfig: this._pluginConfig, nodeConfig: this._nodeConfig, api: this._pluginApi, logger });
|
|
224
|
+
this.core.setComponentRegistry(this.components);
|
|
225
|
+
logger?.info('[F2A] 插件初始化完成');
|
|
893
226
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
if (!this._webhookPusher) {
|
|
907
|
-
// 没有配置 webhook,不轮询(保持原有轮询模式)
|
|
227
|
+
async enable() {
|
|
228
|
+
const logger = this.core?.getLogger();
|
|
229
|
+
this.webhookManager = new F2AWebhookManager_js_1.F2AWebhookManager({
|
|
230
|
+
config: this._pluginConfig, capabilities: this.core?.getCapabilities() || [], logger,
|
|
231
|
+
reputationSystem: this.components.getReputationSystem(), taskQueue: this.components.getTaskQueue(),
|
|
232
|
+
webhookPusher: this.core?.getWebhookPusher(), api: this._pluginApi,
|
|
233
|
+
invokeOpenClawAgent: (from, msg) => this.invokeOpenClawAgent(from, msg),
|
|
234
|
+
});
|
|
235
|
+
const onMessage = async (msg) => {
|
|
236
|
+
if (!(0, connector_helpers_js_1.isValidPeerId)(msg.from)) {
|
|
237
|
+
logger?.warn('[F2A] 拒绝无效 PeerID', { peerId: String(msg.from).slice(0, 20) });
|
|
908
238
|
return;
|
|
909
239
|
}
|
|
910
|
-
|
|
911
|
-
|
|
240
|
+
if (msg.content?.length > connector_helpers_js_1.MAX_MESSAGE_LENGTH) {
|
|
241
|
+
logger?.warn(`[F2A] 消息过长,拒绝处理`);
|
|
912
242
|
return;
|
|
913
243
|
}
|
|
244
|
+
logger?.info('[F2A] 收到 P2P 消息', { from: msg.from.slice(0, 16), content: msg.content?.slice(0, 50) });
|
|
914
245
|
try {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
246
|
+
if (this.isEchoMessage(msg)) {
|
|
247
|
+
logger?.info('[F2A] 跳过回声消息');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const reply = await this.invokeOpenClawAgent(msg.from, msg.content, msg.messageId);
|
|
251
|
+
const f2a = this.core?.getF2A();
|
|
252
|
+
if (reply && f2a) {
|
|
253
|
+
await f2a.sendMessage(msg.from, reply, { type: 'reply', replyTo: msg.messageId });
|
|
254
|
+
logger?.info('[F2A] 回复已发送', { to: msg.from.slice(0, 16) });
|
|
925
255
|
}
|
|
926
256
|
}
|
|
927
|
-
catch (
|
|
928
|
-
|
|
257
|
+
catch (err) {
|
|
258
|
+
logger?.error('[F2A] 处理消息失败', { error: (0, connector_helpers_js_1.extractErrorMessage)(err) });
|
|
929
259
|
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
260
|
+
};
|
|
261
|
+
await this.core.enable(this.webhookManager.createHandler(), onMessage);
|
|
262
|
+
this.components.getContactManager();
|
|
263
|
+
try {
|
|
264
|
+
this.components.getHandshakeProtocol();
|
|
934
265
|
}
|
|
266
|
+
catch { }
|
|
267
|
+
logger?.info('[F2A] 适配器已启用');
|
|
935
268
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const stats = this._taskQueue.getStats();
|
|
945
|
-
if (stats.processing === 0) {
|
|
946
|
-
return; // 没有处理中的任务,无需检查
|
|
947
|
-
}
|
|
948
|
-
const allTasks = this._taskQueue.getAll();
|
|
949
|
-
const now = Date.now();
|
|
950
|
-
const processingTimeout = this.config.processingTimeoutMs || 5 * 60 * 1000; // 默认 5 分钟
|
|
951
|
-
for (const task of allTasks) {
|
|
952
|
-
if (task.status === 'processing') {
|
|
953
|
-
const taskTimeout = task.timeout || 30000; // 使用任务自身的超时或默认 30 秒
|
|
954
|
-
const maxAllowedTime = Math.max(taskTimeout * 2, processingTimeout); // 至少 2 倍任务超时或 processingTimeout
|
|
955
|
-
const processingTime = now - (task.updatedAt || task.createdAt);
|
|
956
|
-
if (processingTime > maxAllowedTime) {
|
|
957
|
-
this._logger?.warn(`[F2A Adapter] 检测到僵尸任务 ${task.taskId.slice(0, 8)}... (processing ${Math.round(processingTime / 1000)}s),重置为 pending`);
|
|
958
|
-
// 将任务重置为 pending 状态
|
|
959
|
-
this._taskQueue.resetProcessingTask(task.taskId);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
269
|
+
async shutdown() {
|
|
270
|
+
await this.core?.shutdown();
|
|
271
|
+
this._toolHandlers = undefined;
|
|
272
|
+
this._claimHandlers = undefined;
|
|
273
|
+
this._contactToolHandlers = undefined;
|
|
274
|
+
this._processedMessageHashes.clear();
|
|
275
|
+
task_guard_js_1.taskGuard.shutdown();
|
|
276
|
+
this.core?.getLogger()?.info('[F2A] 插件已关闭');
|
|
963
277
|
}
|
|
964
|
-
|
|
965
|
-
* 获取插件提供的 Tools
|
|
966
|
-
*/
|
|
278
|
+
// ========== 工具注册 ==========
|
|
967
279
|
getTools() {
|
|
968
|
-
return
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
description: '发现 F2A 网络中的 Agents,可以按能力过滤',
|
|
972
|
-
parameters: {
|
|
973
|
-
capability: {
|
|
974
|
-
type: 'string',
|
|
975
|
-
description: '按能力过滤,如 code-generation, file-operation',
|
|
976
|
-
required: false
|
|
977
|
-
},
|
|
978
|
-
min_reputation: {
|
|
979
|
-
type: 'number',
|
|
980
|
-
description: '最低信誉分数 (0-100)',
|
|
981
|
-
required: false
|
|
982
|
-
}
|
|
983
|
-
},
|
|
984
|
-
handler: this.toolHandlers.handleDiscover.bind(this.toolHandlers)
|
|
985
|
-
},
|
|
986
|
-
{
|
|
987
|
-
name: 'f2a_delegate',
|
|
988
|
-
description: '委托任务给网络中的特定 Agent',
|
|
989
|
-
parameters: {
|
|
990
|
-
agent: {
|
|
991
|
-
type: 'string',
|
|
992
|
-
description: '目标 Agent ID、名称或 #索引 (如 #1)',
|
|
993
|
-
required: true
|
|
994
|
-
},
|
|
995
|
-
task: {
|
|
996
|
-
type: 'string',
|
|
997
|
-
description: '任务描述',
|
|
998
|
-
required: true
|
|
999
|
-
},
|
|
1000
|
-
context: {
|
|
1001
|
-
type: 'string',
|
|
1002
|
-
description: '任务上下文或附件',
|
|
1003
|
-
required: false
|
|
1004
|
-
},
|
|
1005
|
-
timeout: {
|
|
1006
|
-
type: 'number',
|
|
1007
|
-
description: '超时时间(毫秒)',
|
|
1008
|
-
required: false
|
|
1009
|
-
}
|
|
1010
|
-
},
|
|
1011
|
-
handler: this.toolHandlers.handleDelegate.bind(this.toolHandlers)
|
|
1012
|
-
},
|
|
1013
|
-
{
|
|
1014
|
-
name: 'f2a_broadcast',
|
|
1015
|
-
description: '广播任务给所有具备某能力的 Agents(并行执行)',
|
|
1016
|
-
parameters: {
|
|
1017
|
-
capability: {
|
|
1018
|
-
type: 'string',
|
|
1019
|
-
description: '所需能力',
|
|
1020
|
-
required: true
|
|
1021
|
-
},
|
|
1022
|
-
task: {
|
|
1023
|
-
type: 'string',
|
|
1024
|
-
description: '任务描述',
|
|
1025
|
-
required: true
|
|
1026
|
-
},
|
|
1027
|
-
min_responses: {
|
|
1028
|
-
type: 'number',
|
|
1029
|
-
description: '最少响应数',
|
|
1030
|
-
required: false
|
|
1031
|
-
}
|
|
1032
|
-
},
|
|
1033
|
-
handler: this.toolHandlers.handleBroadcast.bind(this.toolHandlers)
|
|
1034
|
-
},
|
|
1035
|
-
{
|
|
1036
|
-
name: 'f2a_status',
|
|
1037
|
-
description: '查看 F2A 网络状态和已连接 Peers',
|
|
1038
|
-
parameters: {},
|
|
1039
|
-
handler: this.toolHandlers.handleStatus.bind(this.toolHandlers)
|
|
1040
|
-
},
|
|
1041
|
-
{
|
|
1042
|
-
name: 'f2a_reputation',
|
|
1043
|
-
description: '查看或管理 Peer 信誉',
|
|
1044
|
-
parameters: {
|
|
1045
|
-
action: {
|
|
1046
|
-
type: 'string',
|
|
1047
|
-
description: '操作: list, view, block, unblock',
|
|
1048
|
-
required: true,
|
|
1049
|
-
enum: ['list', 'view', 'block', 'unblock']
|
|
1050
|
-
},
|
|
1051
|
-
peer_id: {
|
|
1052
|
-
type: 'string',
|
|
1053
|
-
description: 'Peer ID',
|
|
1054
|
-
required: false
|
|
1055
|
-
}
|
|
1056
|
-
},
|
|
1057
|
-
handler: this.toolHandlers.handleReputation.bind(this.toolHandlers)
|
|
1058
|
-
},
|
|
1059
|
-
// 新增:任务队列相关工具
|
|
1060
|
-
{
|
|
1061
|
-
name: 'f2a_poll_tasks',
|
|
1062
|
-
description: '查询本节点收到的远程任务队列(待 OpenClaw 执行)',
|
|
1063
|
-
parameters: {
|
|
1064
|
-
limit: {
|
|
1065
|
-
type: 'number',
|
|
1066
|
-
description: '最大返回任务数',
|
|
1067
|
-
required: false
|
|
1068
|
-
},
|
|
1069
|
-
status: {
|
|
1070
|
-
type: 'string',
|
|
1071
|
-
description: '任务状态过滤: pending, processing, completed, failed',
|
|
1072
|
-
required: false,
|
|
1073
|
-
enum: ['pending', 'processing', 'completed', 'failed']
|
|
1074
|
-
}
|
|
1075
|
-
},
|
|
1076
|
-
handler: this.toolHandlers.handlePollTasks.bind(this.toolHandlers)
|
|
1077
|
-
},
|
|
1078
|
-
{
|
|
1079
|
-
name: 'f2a_submit_result',
|
|
1080
|
-
description: '提交远程任务的执行结果,发送给原节点',
|
|
1081
|
-
parameters: {
|
|
1082
|
-
task_id: {
|
|
1083
|
-
type: 'string',
|
|
1084
|
-
description: '任务ID',
|
|
1085
|
-
required: true
|
|
1086
|
-
},
|
|
1087
|
-
result: {
|
|
1088
|
-
type: 'string',
|
|
1089
|
-
description: '任务执行结果',
|
|
1090
|
-
required: true
|
|
1091
|
-
},
|
|
1092
|
-
status: {
|
|
1093
|
-
type: 'string',
|
|
1094
|
-
description: '执行状态: success 或 error',
|
|
1095
|
-
required: true,
|
|
1096
|
-
enum: ['success', 'error']
|
|
1097
|
-
}
|
|
1098
|
-
},
|
|
1099
|
-
handler: this.toolHandlers.handleSubmitResult.bind(this.toolHandlers)
|
|
1100
|
-
},
|
|
1101
|
-
{
|
|
1102
|
-
name: 'f2a_task_stats',
|
|
1103
|
-
description: '查看任务队列统计信息',
|
|
1104
|
-
parameters: {},
|
|
1105
|
-
handler: this.toolHandlers.handleTaskStats.bind(this.toolHandlers)
|
|
1106
|
-
},
|
|
1107
|
-
// 认领模式工具
|
|
1108
|
-
{
|
|
1109
|
-
name: 'f2a_announce',
|
|
1110
|
-
description: '广播任务到 F2A 网络,等待其他 Agent 认领(认领模式)',
|
|
1111
|
-
parameters: {
|
|
1112
|
-
task_type: {
|
|
1113
|
-
type: 'string',
|
|
1114
|
-
description: '任务类型',
|
|
1115
|
-
required: true
|
|
1116
|
-
},
|
|
1117
|
-
description: {
|
|
1118
|
-
type: 'string',
|
|
1119
|
-
description: '任务描述',
|
|
1120
|
-
required: true
|
|
1121
|
-
},
|
|
1122
|
-
required_capabilities: {
|
|
1123
|
-
type: 'array',
|
|
1124
|
-
description: '所需能力列表',
|
|
1125
|
-
required: false
|
|
1126
|
-
},
|
|
1127
|
-
estimated_complexity: {
|
|
1128
|
-
type: 'number',
|
|
1129
|
-
description: '预估复杂度 (1-10)',
|
|
1130
|
-
required: false
|
|
1131
|
-
},
|
|
1132
|
-
reward: {
|
|
1133
|
-
type: 'number',
|
|
1134
|
-
description: '任务奖励',
|
|
1135
|
-
required: false
|
|
1136
|
-
},
|
|
1137
|
-
timeout: {
|
|
1138
|
-
type: 'number',
|
|
1139
|
-
description: '超时时间(毫秒)',
|
|
1140
|
-
required: false
|
|
1141
|
-
}
|
|
1142
|
-
},
|
|
1143
|
-
handler: this.claimHandlers.handleAnnounce.bind(this.claimHandlers)
|
|
1144
|
-
},
|
|
1145
|
-
{
|
|
1146
|
-
name: 'f2a_list_announcements',
|
|
1147
|
-
description: '查看当前开放的任务广播(可认领)',
|
|
1148
|
-
parameters: {
|
|
1149
|
-
capability: {
|
|
1150
|
-
type: 'string',
|
|
1151
|
-
description: '按能力过滤',
|
|
1152
|
-
required: false
|
|
1153
|
-
},
|
|
1154
|
-
limit: {
|
|
1155
|
-
type: 'number',
|
|
1156
|
-
description: '最大返回数量',
|
|
1157
|
-
required: false
|
|
1158
|
-
}
|
|
1159
|
-
},
|
|
1160
|
-
handler: this.claimHandlers.handleListAnnouncements.bind(this.claimHandlers)
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
name: 'f2a_claim',
|
|
1164
|
-
description: '认领一个开放的任务广播',
|
|
1165
|
-
parameters: {
|
|
1166
|
-
announcement_id: {
|
|
1167
|
-
type: 'string',
|
|
1168
|
-
description: '广播ID',
|
|
1169
|
-
required: true
|
|
1170
|
-
},
|
|
1171
|
-
estimated_time: {
|
|
1172
|
-
type: 'number',
|
|
1173
|
-
description: '预计完成时间(毫秒)',
|
|
1174
|
-
required: false
|
|
1175
|
-
},
|
|
1176
|
-
confidence: {
|
|
1177
|
-
type: 'number',
|
|
1178
|
-
description: '信心指数 (0-1)',
|
|
1179
|
-
required: false
|
|
1180
|
-
}
|
|
1181
|
-
},
|
|
1182
|
-
handler: this.claimHandlers.handleClaim.bind(this.claimHandlers)
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
name: 'f2a_manage_claims',
|
|
1186
|
-
description: '管理我的任务广播的认领(接受/拒绝)',
|
|
1187
|
-
parameters: {
|
|
1188
|
-
announcement_id: {
|
|
1189
|
-
type: 'string',
|
|
1190
|
-
description: '广播ID',
|
|
1191
|
-
required: true
|
|
1192
|
-
},
|
|
1193
|
-
action: {
|
|
1194
|
-
type: 'string',
|
|
1195
|
-
description: '操作: list, accept, reject',
|
|
1196
|
-
required: true,
|
|
1197
|
-
enum: ['list', 'accept', 'reject']
|
|
1198
|
-
},
|
|
1199
|
-
claim_id: {
|
|
1200
|
-
type: 'string',
|
|
1201
|
-
description: '认领ID(accept/reject 时需要)',
|
|
1202
|
-
required: false
|
|
1203
|
-
}
|
|
1204
|
-
},
|
|
1205
|
-
handler: this.claimHandlers.handleManageClaims.bind(this.claimHandlers)
|
|
1206
|
-
},
|
|
1207
|
-
{
|
|
1208
|
-
name: 'f2a_my_claims',
|
|
1209
|
-
description: '查看我提交的任务认领状态',
|
|
1210
|
-
parameters: {
|
|
1211
|
-
status: {
|
|
1212
|
-
type: 'string',
|
|
1213
|
-
description: '状态过滤: pending, accepted, rejected, all',
|
|
1214
|
-
required: false,
|
|
1215
|
-
enum: ['pending', 'accepted', 'rejected', 'all']
|
|
1216
|
-
}
|
|
1217
|
-
},
|
|
1218
|
-
handler: this.claimHandlers.handleMyClaims.bind(this.claimHandlers)
|
|
1219
|
-
},
|
|
1220
|
-
{
|
|
1221
|
-
name: 'f2a_announcement_stats',
|
|
1222
|
-
description: '查看任务广播统计',
|
|
1223
|
-
parameters: {},
|
|
1224
|
-
handler: this.claimHandlers.handleAnnouncementStats.bind(this.claimHandlers)
|
|
1225
|
-
},
|
|
1226
|
-
// 任务评估相关工具
|
|
1227
|
-
{
|
|
1228
|
-
name: 'f2a_estimate_task',
|
|
1229
|
-
description: '评估任务成本(工作量、复杂度、预估时间)',
|
|
1230
|
-
parameters: {
|
|
1231
|
-
task_type: {
|
|
1232
|
-
type: 'string',
|
|
1233
|
-
description: '任务类型',
|
|
1234
|
-
required: true
|
|
1235
|
-
},
|
|
1236
|
-
description: {
|
|
1237
|
-
type: 'string',
|
|
1238
|
-
description: '任务描述',
|
|
1239
|
-
required: true
|
|
1240
|
-
},
|
|
1241
|
-
required_capabilities: {
|
|
1242
|
-
type: 'array',
|
|
1243
|
-
description: '所需能力列表',
|
|
1244
|
-
required: false
|
|
1245
|
-
}
|
|
1246
|
-
},
|
|
1247
|
-
handler: this.toolHandlers.handleEstimateTask.bind(this.toolHandlers)
|
|
1248
|
-
},
|
|
1249
|
-
{
|
|
1250
|
-
name: 'f2a_review_task',
|
|
1251
|
-
description: '作为评审者评审任务的工作量和价值',
|
|
1252
|
-
parameters: {
|
|
1253
|
-
task_id: {
|
|
1254
|
-
type: 'string',
|
|
1255
|
-
description: '任务ID',
|
|
1256
|
-
required: true
|
|
1257
|
-
},
|
|
1258
|
-
workload: {
|
|
1259
|
-
type: 'number',
|
|
1260
|
-
description: '工作量评估 (0-100)',
|
|
1261
|
-
required: true
|
|
1262
|
-
},
|
|
1263
|
-
value: {
|
|
1264
|
-
type: 'number',
|
|
1265
|
-
description: '价值评估 (-100 ~ 100)',
|
|
1266
|
-
required: true
|
|
1267
|
-
},
|
|
1268
|
-
risk_flags: {
|
|
1269
|
-
type: 'array',
|
|
1270
|
-
description: '风险标记: dangerous, malicious, spam, invalid',
|
|
1271
|
-
required: false
|
|
1272
|
-
},
|
|
1273
|
-
comment: {
|
|
1274
|
-
type: 'string',
|
|
1275
|
-
description: '评审意见',
|
|
1276
|
-
required: false
|
|
1277
|
-
}
|
|
1278
|
-
},
|
|
1279
|
-
handler: this.toolHandlers.handleReviewTask.bind(this.toolHandlers)
|
|
1280
|
-
},
|
|
1281
|
-
{
|
|
1282
|
-
name: 'f2a_get_reviews',
|
|
1283
|
-
description: '获取任务的评审汇总结果',
|
|
1284
|
-
parameters: {
|
|
1285
|
-
task_id: {
|
|
1286
|
-
type: 'string',
|
|
1287
|
-
description: '任务ID',
|
|
1288
|
-
required: true
|
|
1289
|
-
}
|
|
1290
|
-
},
|
|
1291
|
-
handler: this.toolHandlers.handleGetReviews.bind(this.toolHandlers)
|
|
1292
|
-
},
|
|
1293
|
-
{
|
|
1294
|
-
name: 'f2a_get_capabilities',
|
|
1295
|
-
description: '获取指定 Agent 的能力列表',
|
|
1296
|
-
parameters: {
|
|
1297
|
-
peer_id: {
|
|
1298
|
-
type: 'string',
|
|
1299
|
-
description: 'Agent 的 Peer ID 或名称',
|
|
1300
|
-
required: false
|
|
1301
|
-
}
|
|
1302
|
-
},
|
|
1303
|
-
handler: this.toolHandlers.handleGetCapabilities.bind(this.toolHandlers)
|
|
1304
|
-
},
|
|
1305
|
-
// ========== Issue #98 & #99: 通讯录和握手机制工具 ==========
|
|
1306
|
-
{
|
|
1307
|
-
name: 'f2a_contacts',
|
|
1308
|
-
description: '管理通讯录联系人。Actions: list(列出联系人), get(获取详情), add(添加), remove(删除), update(更新), block(拉黑), unblock(解除拉黑)',
|
|
1309
|
-
parameters: {
|
|
1310
|
-
action: {
|
|
1311
|
-
type: 'string',
|
|
1312
|
-
description: '操作类型: list, get, add, remove, update, block, unblock',
|
|
1313
|
-
required: true,
|
|
1314
|
-
enum: ['list', 'get', 'add', 'remove', 'update', 'block', 'unblock']
|
|
1315
|
-
},
|
|
1316
|
-
contact_id: {
|
|
1317
|
-
type: 'string',
|
|
1318
|
-
description: '联系人 ID(get/remove/update/block/unblock 时需要)',
|
|
1319
|
-
required: false
|
|
1320
|
-
},
|
|
1321
|
-
peer_id: {
|
|
1322
|
-
type: 'string',
|
|
1323
|
-
description: 'Peer ID(add 时需要,get/remove 时可选)',
|
|
1324
|
-
required: false
|
|
1325
|
-
},
|
|
1326
|
-
name: {
|
|
1327
|
-
type: 'string',
|
|
1328
|
-
description: '联系人名称(add/update 时需要)',
|
|
1329
|
-
required: false
|
|
1330
|
-
},
|
|
1331
|
-
groups: {
|
|
1332
|
-
type: 'array',
|
|
1333
|
-
description: '分组列表',
|
|
1334
|
-
required: false
|
|
1335
|
-
},
|
|
1336
|
-
tags: {
|
|
1337
|
-
type: 'array',
|
|
1338
|
-
description: '标签列表',
|
|
1339
|
-
required: false
|
|
1340
|
-
},
|
|
1341
|
-
notes: {
|
|
1342
|
-
type: 'string',
|
|
1343
|
-
description: '备注信息',
|
|
1344
|
-
required: false
|
|
1345
|
-
},
|
|
1346
|
-
status: {
|
|
1347
|
-
type: 'string',
|
|
1348
|
-
description: '按状态过滤(list 时可选): stranger, pending, friend, blocked',
|
|
1349
|
-
required: false,
|
|
1350
|
-
enum: ['stranger', 'pending', 'friend', 'blocked']
|
|
1351
|
-
},
|
|
1352
|
-
group: {
|
|
1353
|
-
type: 'string',
|
|
1354
|
-
description: '按分组过滤(list 时可选)',
|
|
1355
|
-
required: false
|
|
1356
|
-
}
|
|
1357
|
-
},
|
|
1358
|
-
handler: this.handleContacts.bind(this)
|
|
1359
|
-
},
|
|
1360
|
-
{
|
|
1361
|
-
name: 'f2a_contact_groups',
|
|
1362
|
-
description: '管理联系人分组。Actions: list(列出分组), create(创建), update(更新), delete(删除)',
|
|
1363
|
-
parameters: {
|
|
1364
|
-
action: {
|
|
1365
|
-
type: 'string',
|
|
1366
|
-
description: '操作类型: list, create, update, delete',
|
|
1367
|
-
required: true,
|
|
1368
|
-
enum: ['list', 'create', 'update', 'delete']
|
|
1369
|
-
},
|
|
1370
|
-
group_id: {
|
|
1371
|
-
type: 'string',
|
|
1372
|
-
description: '分组 ID(update/delete 时需要)',
|
|
1373
|
-
required: false
|
|
1374
|
-
},
|
|
1375
|
-
name: {
|
|
1376
|
-
type: 'string',
|
|
1377
|
-
description: '分组名称(create/update 时需要)',
|
|
1378
|
-
required: false
|
|
1379
|
-
},
|
|
1380
|
-
description: {
|
|
1381
|
-
type: 'string',
|
|
1382
|
-
description: '分组描述',
|
|
1383
|
-
required: false
|
|
1384
|
-
},
|
|
1385
|
-
color: {
|
|
1386
|
-
type: 'string',
|
|
1387
|
-
description: '分组颜色(十六进制,如 #FF5733)',
|
|
1388
|
-
required: false
|
|
1389
|
-
}
|
|
1390
|
-
},
|
|
1391
|
-
handler: this.handleContactGroups.bind(this)
|
|
1392
|
-
},
|
|
1393
|
-
{
|
|
1394
|
-
name: 'f2a_friend_request',
|
|
1395
|
-
description: '发送好友请求给指定 Agent',
|
|
1396
|
-
parameters: {
|
|
1397
|
-
peer_id: {
|
|
1398
|
-
type: 'string',
|
|
1399
|
-
description: '目标 Agent 的 Peer ID',
|
|
1400
|
-
required: true
|
|
1401
|
-
},
|
|
1402
|
-
message: {
|
|
1403
|
-
type: 'string',
|
|
1404
|
-
description: '附加消息',
|
|
1405
|
-
required: false
|
|
1406
|
-
}
|
|
1407
|
-
},
|
|
1408
|
-
handler: this.handleFriendRequest.bind(this)
|
|
1409
|
-
},
|
|
1410
|
-
{
|
|
1411
|
-
name: 'f2a_pending_requests',
|
|
1412
|
-
description: '查看和处理待处理的好友请求。Actions: list(列出请求), accept(接受), reject(拒绝)',
|
|
1413
|
-
parameters: {
|
|
1414
|
-
action: {
|
|
1415
|
-
type: 'string',
|
|
1416
|
-
description: '操作类型: list, accept, reject',
|
|
1417
|
-
required: true,
|
|
1418
|
-
enum: ['list', 'accept', 'reject']
|
|
1419
|
-
},
|
|
1420
|
-
request_id: {
|
|
1421
|
-
type: 'string',
|
|
1422
|
-
description: '请求 ID(accept/reject 时需要)',
|
|
1423
|
-
required: false
|
|
1424
|
-
},
|
|
1425
|
-
reason: {
|
|
1426
|
-
type: 'string',
|
|
1427
|
-
description: '拒绝原因(reject 时可选)',
|
|
1428
|
-
required: false
|
|
1429
|
-
}
|
|
1430
|
-
},
|
|
1431
|
-
handler: this.handlePendingRequests.bind(this)
|
|
1432
|
-
},
|
|
1433
|
-
{
|
|
1434
|
-
name: 'f2a_contacts_export',
|
|
1435
|
-
description: '导出通讯录数据',
|
|
1436
|
-
parameters: {},
|
|
1437
|
-
handler: this.handleContactsExport.bind(this)
|
|
1438
|
-
},
|
|
1439
|
-
{
|
|
1440
|
-
name: 'f2a_contacts_import',
|
|
1441
|
-
description: '导入通讯录数据',
|
|
1442
|
-
parameters: {
|
|
1443
|
-
data: {
|
|
1444
|
-
type: 'object',
|
|
1445
|
-
description: '导入的通讯录数据(JSON 格式)',
|
|
1446
|
-
required: true
|
|
1447
|
-
},
|
|
1448
|
-
merge: {
|
|
1449
|
-
type: 'boolean',
|
|
1450
|
-
description: '是否合并(true)或覆盖(false),默认 true',
|
|
1451
|
-
required: false
|
|
1452
|
-
}
|
|
1453
|
-
},
|
|
1454
|
-
handler: this.handleContactsImport.bind(this)
|
|
1455
|
-
}
|
|
1456
|
-
];
|
|
280
|
+
return new F2AToolRegistry_js_1.F2AToolRegistry({
|
|
281
|
+
toolHandlers: this.toolHandlers, claimHandlers: this.claimHandlers, contactToolHandlers: this.contactToolHandlers,
|
|
282
|
+
}).getTools();
|
|
1457
283
|
}
|
|
284
|
+
// ========== Webhook 处理器(兼容性方法) ==========
|
|
1458
285
|
/**
|
|
1459
286
|
* 创建 Webhook 处理器
|
|
287
|
+
* 兼容性方法,委托给 F2AWebhookManager
|
|
1460
288
|
*/
|
|
1461
289
|
createWebhookHandler() {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
caps = caps.filter(c => c.name === payload.query.capability ||
|
|
1475
|
-
c.tools?.includes(payload.query.capability));
|
|
1476
|
-
}
|
|
1477
|
-
return {
|
|
1478
|
-
capabilities: caps,
|
|
1479
|
-
reputation: this.reputationSystem.getReputation(payload.requester).score
|
|
1480
|
-
};
|
|
1481
|
-
},
|
|
1482
|
-
onDelegate: async (payload) => {
|
|
1483
|
-
// 安全检查(懒加载触发 reputationSystem)
|
|
1484
|
-
if (!this.reputationSystem.isAllowed(payload.from)) {
|
|
1485
|
-
return {
|
|
1486
|
-
accepted: false,
|
|
1487
|
-
taskId: payload.taskId,
|
|
1488
|
-
reason: 'Reputation too low'
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
// 检查白名单/黑名单
|
|
1492
|
-
const whitelist = this.config.security?.whitelist || [];
|
|
1493
|
-
const blacklist = this.config.security?.blacklist || [];
|
|
1494
|
-
const isWhitelisted = whitelist.length > 0 && whitelist.includes(payload.from);
|
|
1495
|
-
const isBlacklisted = blacklist.includes(payload.from);
|
|
1496
|
-
if (whitelist.length > 0 && !isWhitelisted) {
|
|
1497
|
-
return {
|
|
1498
|
-
accepted: false,
|
|
1499
|
-
taskId: payload.taskId,
|
|
1500
|
-
reason: 'Not in whitelist'
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
if (isBlacklisted) {
|
|
1504
|
-
return {
|
|
1505
|
-
accepted: false,
|
|
1506
|
-
taskId: payload.taskId,
|
|
1507
|
-
reason: 'In blacklist'
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
// TaskGuard 安全检查
|
|
1511
|
-
const requesterReputation = this.reputationSystem.getReputation(payload.from);
|
|
1512
|
-
const taskGuardContext = {
|
|
1513
|
-
requesterReputation,
|
|
1514
|
-
isWhitelisted,
|
|
1515
|
-
isBlacklisted,
|
|
1516
|
-
recentTaskCount: 0 // Will be tracked internally by TaskGuard
|
|
1517
|
-
};
|
|
1518
|
-
const taskGuardReport = task_guard_js_1.taskGuard.check(payload, taskGuardContext);
|
|
1519
|
-
if (!taskGuardReport.passed) {
|
|
1520
|
-
// 任务被阻止
|
|
1521
|
-
const blockReasons = taskGuardReport.blocks.map(b => b.message).join('; ');
|
|
1522
|
-
this._logger?.warn(`[F2A Adapter] TaskGuard 阻止任务 ${payload.taskId}: ${blockReasons}`);
|
|
1523
|
-
return {
|
|
1524
|
-
accepted: false,
|
|
1525
|
-
taskId: payload.taskId,
|
|
1526
|
-
reason: `TaskGuard blocked: ${blockReasons}`
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
if (taskGuardReport.requiresConfirmation) {
|
|
1530
|
-
// 任务需要确认(警告但不阻止)
|
|
1531
|
-
const warnReasons = taskGuardReport.warnings.map(w => w.message).join('; ');
|
|
1532
|
-
this._logger?.warn(`[F2A Adapter] TaskGuard 警告 ${payload.taskId}: ${warnReasons}`);
|
|
1533
|
-
// 未来可以扩展为请求用户确认
|
|
1534
|
-
// 目前记录警告但继续处理任务
|
|
1535
|
-
}
|
|
1536
|
-
// 检查队列是否已满(懒加载触发 taskQueue)
|
|
1537
|
-
const stats = this.taskQueue.getStats();
|
|
1538
|
-
if (stats.pending >= (this.config.maxQueuedTasks || 100)) {
|
|
1539
|
-
return {
|
|
1540
|
-
accepted: false,
|
|
1541
|
-
taskId: payload.taskId,
|
|
1542
|
-
reason: 'Task queue is full'
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
// 添加任务到队列
|
|
1546
|
-
try {
|
|
1547
|
-
const task = this.taskQueue.add(payload);
|
|
1548
|
-
// 优先使用 webhook 推送
|
|
1549
|
-
if (this._webhookPusher) {
|
|
1550
|
-
const result = await this._webhookPusher.pushTask(task);
|
|
1551
|
-
if (result.success) {
|
|
1552
|
-
this.taskQueue.markWebhookPushed(task.taskId);
|
|
1553
|
-
this._logger?.info(`[F2A Adapter] 任务 ${task.taskId} 已通过 webhook 推送 (${result.latency}ms)`);
|
|
1554
|
-
}
|
|
1555
|
-
else {
|
|
1556
|
-
this._logger?.info(`[F2A Adapter] Webhook 推送失败: ${result.error},任务将在轮询时处理`);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
// 触发 OpenClaw 心跳,让它知道有新任务
|
|
1560
|
-
this.api?.runtime?.system?.requestHeartbeatNow?.();
|
|
1561
|
-
return {
|
|
1562
|
-
accepted: true,
|
|
1563
|
-
taskId: payload.taskId
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
catch (error) {
|
|
1567
|
-
return {
|
|
1568
|
-
accepted: false,
|
|
1569
|
-
taskId: payload.taskId,
|
|
1570
|
-
reason: error instanceof Error ? error.message : 'Failed to queue task'
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
},
|
|
1574
|
-
onMessage: async (payload) => {
|
|
1575
|
-
// P1-6: 验证 PeerID 格式
|
|
1576
|
-
if (!isValidPeerId(payload.from)) {
|
|
1577
|
-
this._logger?.warn(`[F2A Adapter] onMessage: 拒绝来自无效 PeerID 的消息: ${String(payload.from).slice(0, 20)}`);
|
|
1578
|
-
return { response: 'Invalid sender' };
|
|
1579
|
-
}
|
|
1580
|
-
// P1-7: 检查消息长度限制
|
|
1581
|
-
if (payload.content && payload.content.length > MAX_MESSAGE_LENGTH) {
|
|
1582
|
-
this._logger?.warn(`[F2A Adapter] onMessage: 消息过长 (${payload.content.length} bytes),拒绝处理`);
|
|
1583
|
-
return { response: 'Message too long' };
|
|
1584
|
-
}
|
|
1585
|
-
this._logger?.info('[F2A Adapter] 收到 P2P 消息', {
|
|
1586
|
-
from: payload.from.slice(0, 16),
|
|
1587
|
-
content: payload.content.slice(0, 50)
|
|
1588
|
-
});
|
|
1589
|
-
try {
|
|
1590
|
-
// 调用 OpenClaw Agent 处理消息
|
|
1591
|
-
// 使用 peerId 作为 session id,确保同一个对话者使用同一个 session
|
|
1592
|
-
const sessionId = `f2a-${payload.from.slice(0, 16)}`;
|
|
1593
|
-
// 构造消息,包含发送者信息
|
|
1594
|
-
const message = `[来自 ${payload.metadata?.from || payload.from.slice(0, 16)}] ${payload.content}`;
|
|
1595
|
-
// 调用 openclaw agent 命令
|
|
1596
|
-
const result = await this.invokeOpenClawAgent(payload.from, message);
|
|
1597
|
-
return { response: result || '收到消息,但我暂时无法生成回复。' };
|
|
1598
|
-
}
|
|
1599
|
-
catch (error) {
|
|
1600
|
-
this._logger?.error('[F2A Adapter] 处理消息失败', { error: extractErrorMessage(error) });
|
|
1601
|
-
return { response: '抱歉,我遇到了一些问题,无法处理你的消息。' };
|
|
1602
|
-
}
|
|
1603
|
-
},
|
|
1604
|
-
onStatus: async () => {
|
|
1605
|
-
// 如果 TaskQueue 未初始化,返回空闲状态
|
|
1606
|
-
if (!this._taskQueue) {
|
|
1607
|
-
return {
|
|
1608
|
-
status: 'available',
|
|
1609
|
-
load: 0,
|
|
1610
|
-
queued: 0,
|
|
1611
|
-
processing: 0
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
const stats = this._taskQueue.getStats();
|
|
1615
|
-
return {
|
|
1616
|
-
status: 'available',
|
|
1617
|
-
load: stats.pending + stats.processing,
|
|
1618
|
-
queued: stats.pending,
|
|
1619
|
-
processing: stats.processing
|
|
1620
|
-
};
|
|
1621
|
-
}
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* 注册到 F2A Node
|
|
1626
|
-
*/
|
|
1627
|
-
async registerToNode() {
|
|
1628
|
-
if (!this._webhookServer)
|
|
1629
|
-
return;
|
|
1630
|
-
await this.networkClient.registerWebhook(this._webhookServer.getUrl());
|
|
1631
|
-
await this.networkClient.updateAgentInfo({
|
|
1632
|
-
displayName: this.config.agentName,
|
|
1633
|
-
capabilities: this.capabilities
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
// ========== Helpers ==========
|
|
1637
|
-
/**
|
|
1638
|
-
* 合并配置(公开方法供处理器使用)
|
|
1639
|
-
*/
|
|
1640
|
-
mergeConfig(config) {
|
|
1641
|
-
return {
|
|
1642
|
-
autoStart: config.autoStart ?? true,
|
|
1643
|
-
webhookPort: config.webhookPort || 9002,
|
|
1644
|
-
agentName: config.agentName || 'OpenClaw Agent',
|
|
1645
|
-
capabilities: config.capabilities || [],
|
|
1646
|
-
f2aPath: config.f2aPath,
|
|
1647
|
-
controlPort: config.controlPort,
|
|
1648
|
-
controlToken: config.controlToken,
|
|
1649
|
-
p2pPort: config.p2pPort,
|
|
1650
|
-
enableMDNS: config.enableMDNS,
|
|
1651
|
-
bootstrapPeers: config.bootstrapPeers,
|
|
1652
|
-
// dataDir 只保存用户显式配置的值,默认值在 getDefaultDataDir() 中处理
|
|
1653
|
-
dataDir: config.dataDir,
|
|
1654
|
-
maxQueuedTasks: config.maxQueuedTasks || 100,
|
|
1655
|
-
pollInterval: config.pollInterval,
|
|
1656
|
-
// 保留 webhookPush 配置(修复:之前丢失导致 webhook 推送被禁用)
|
|
1657
|
-
webhookPush: config.webhookPush,
|
|
1658
|
-
reputation: {
|
|
1659
|
-
enabled: config.reputation?.enabled ?? types_js_1.INTERNAL_REPUTATION_CONFIG.enabled,
|
|
1660
|
-
initialScore: config.reputation?.initialScore || types_js_1.INTERNAL_REPUTATION_CONFIG.initialScore,
|
|
1661
|
-
minScoreForService: config.reputation?.minScoreForService || types_js_1.INTERNAL_REPUTATION_CONFIG.minScoreForService,
|
|
1662
|
-
decayRate: config.reputation?.decayRate || types_js_1.INTERNAL_REPUTATION_CONFIG.decayRate
|
|
1663
|
-
},
|
|
1664
|
-
security: {
|
|
1665
|
-
requireConfirmation: config.security?.requireConfirmation ?? false,
|
|
1666
|
-
whitelist: config.security?.whitelist || [],
|
|
1667
|
-
blacklist: config.security?.blacklist || [],
|
|
1668
|
-
maxTasksPerMinute: config.security?.maxTasksPerMinute || 10
|
|
1669
|
-
}
|
|
1670
|
-
};
|
|
1671
|
-
}
|
|
1672
|
-
generateToken() {
|
|
1673
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
1674
|
-
let token = 'f2a-';
|
|
1675
|
-
for (let i = 0; i < 32; i++) {
|
|
1676
|
-
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
290
|
+
// 如果 webhookManager 未初始化,创建一个临时的
|
|
291
|
+
if (!this.webhookManager) {
|
|
292
|
+
this.webhookManager = new F2AWebhookManager_js_1.F2AWebhookManager({
|
|
293
|
+
config: this._pluginConfig,
|
|
294
|
+
capabilities: this.core?.getCapabilities() || [],
|
|
295
|
+
logger: this.core?.getLogger(),
|
|
296
|
+
reputationSystem: this.components.getReputationSystem(),
|
|
297
|
+
taskQueue: this.components.getTaskQueue(),
|
|
298
|
+
webhookPusher: this.core?.getWebhookPusher(),
|
|
299
|
+
api: this._pluginApi,
|
|
300
|
+
invokeOpenClawAgent: (from, msg) => this.invokeOpenClawAgent(from, msg),
|
|
301
|
+
});
|
|
1677
302
|
}
|
|
1678
|
-
return
|
|
303
|
+
return this.webhookManager.createHandler();
|
|
1679
304
|
}
|
|
1680
305
|
/**
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
306
|
+
* 注册到 F2A Node(兼容性方法)
|
|
307
|
+
* @deprecated 已迁移到 F2ACore
|
|
1683
308
|
*/
|
|
1684
|
-
async
|
|
309
|
+
async registerToNode() {
|
|
310
|
+
// 已迁移到 F2ACore,此方法保留用于测试兼容性
|
|
311
|
+
// 无操作
|
|
312
|
+
}
|
|
313
|
+
// ========== 兼容性属性(测试用) ==========
|
|
314
|
+
// 测试用存储(允许测试覆盖)
|
|
315
|
+
__testTaskQueueOverride;
|
|
316
|
+
__testWebhookServerOverride;
|
|
317
|
+
__testReputationSystemOverride;
|
|
318
|
+
/** @deprecated 使用 getTaskQueue() 代替 */
|
|
319
|
+
get _taskQueue() {
|
|
320
|
+
if (this.__testTaskQueueOverride !== undefined)
|
|
321
|
+
return this.__testTaskQueueOverride;
|
|
322
|
+
if (!this.components)
|
|
323
|
+
return undefined;
|
|
1685
324
|
try {
|
|
1686
|
-
|
|
1687
|
-
const { promisify } = await import('util');
|
|
1688
|
-
const execAsync = promisify(exec);
|
|
1689
|
-
const { stdout } = await execAsync('f2a version', { timeout: 5000 });
|
|
1690
|
-
this._logger?.info(`[F2A Adapter] F2A CLI 已安装: ${stdout.trim()}`);
|
|
1691
|
-
return true;
|
|
325
|
+
return this.components.getTaskQueue();
|
|
1692
326
|
}
|
|
1693
|
-
catch
|
|
1694
|
-
|
|
1695
|
-
if (error instanceof Error && (error.message.includes('ENOENT') || error.message.includes('not found'))) {
|
|
1696
|
-
this._logger?.debug?.('[F2A Adapter] F2A CLI 未安装');
|
|
1697
|
-
return false;
|
|
1698
|
-
}
|
|
1699
|
-
// P2-3 修复:timeout 也视为 CLI 未安装(CLI 可能存在但响应慢)
|
|
1700
|
-
if (error instanceof Error && error.message.includes('ETIMEDOUT')) {
|
|
1701
|
-
this._logger?.debug?.('[F2A Adapter] F2A CLI 响应超时,视为未安装');
|
|
1702
|
-
return false;
|
|
1703
|
-
}
|
|
1704
|
-
// 其他错误,可能是 CLI 已安装但有问题
|
|
1705
|
-
this._logger?.debug?.(`[F2A Adapter] F2A CLI 检测异常: ${error instanceof Error ? error.message : String(error)}`);
|
|
1706
|
-
return false;
|
|
327
|
+
catch {
|
|
328
|
+
return undefined;
|
|
1707
329
|
}
|
|
1708
330
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
* 执行 `f2a daemon -d` 命令(后台模式)
|
|
1712
|
-
*/
|
|
1713
|
-
async startDaemonViaCLI() {
|
|
1714
|
-
try {
|
|
1715
|
-
const { spawn } = await import('child_process');
|
|
1716
|
-
return new Promise((resolve) => {
|
|
1717
|
-
this._logger?.info('[F2A Adapter] 执行: f2a daemon -d');
|
|
1718
|
-
// 设置 messageHandlerUrl 环境变量,让 daemon 将消息推送到 adapter 的 webhook
|
|
1719
|
-
const env = { ...process.env };
|
|
1720
|
-
if (this._webhookServer) {
|
|
1721
|
-
const webhookUrl = `${this._webhookServer.getUrl()}/message`;
|
|
1722
|
-
env.F2A_MESSAGE_HANDLER_URL = webhookUrl;
|
|
1723
|
-
this._logger?.info(`[F2A Adapter] 配置消息处理 URL: ${webhookUrl}`);
|
|
1724
|
-
}
|
|
1725
|
-
const proc = spawn('f2a', ['daemon', '-d'], {
|
|
1726
|
-
detached: true, // P1-1 修复:让 daemon 独立运行,不随父进程退出
|
|
1727
|
-
stdio: 'ignore', // P1-2 修复:ignore stdio 配合 detached 使用
|
|
1728
|
-
env // 传递环境变量
|
|
1729
|
-
});
|
|
1730
|
-
// P1-2 修复:detached + ignore stdio 后,需要 unref 让父进程可以独立退出
|
|
1731
|
-
proc.unref();
|
|
1732
|
-
proc.on('error', (err) => {
|
|
1733
|
-
this._logger?.error(`[F2A Adapter] 启动 daemon 失败: ${err.message}`);
|
|
1734
|
-
resolve(false);
|
|
1735
|
-
});
|
|
1736
|
-
// P2-1 修复:daemon 启动后 CLI 会自动等待服务就绪后退出
|
|
1737
|
-
// 我们只需要等待一段时间后检查 daemon 是否真的在运行
|
|
1738
|
-
setTimeout(async () => {
|
|
1739
|
-
const running = await this._nodeManager?.isRunning();
|
|
1740
|
-
if (running) {
|
|
1741
|
-
this._logger?.info('[F2A Adapter] F2A daemon 服务已就绪');
|
|
1742
|
-
resolve(true);
|
|
1743
|
-
}
|
|
1744
|
-
else {
|
|
1745
|
-
this._logger?.warn('[F2A Adapter] F2A daemon 启动超时,请检查日志: ~/.f2a/daemon.log');
|
|
1746
|
-
resolve(false);
|
|
1747
|
-
}
|
|
1748
|
-
}, 5000);
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1751
|
-
catch (error) {
|
|
1752
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1753
|
-
this._logger?.error(`[F2A Adapter] 启动 daemon 失败: ${errMsg}`);
|
|
1754
|
-
return false;
|
|
1755
|
-
}
|
|
331
|
+
set _taskQueue(value) {
|
|
332
|
+
this.__testTaskQueueOverride = value;
|
|
1756
333
|
}
|
|
1757
|
-
/**
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
return `${icon} ${r.agent}${latency}\n ${r.success ? '完成' : `失败: ${r.error}`}`;
|
|
1765
|
-
}).join('\n\n');
|
|
334
|
+
/** @deprecated 使用 core?.getWebhookUrl() 代替 */
|
|
335
|
+
get _webhookServer() {
|
|
336
|
+
if (this.__testWebhookServerOverride !== undefined)
|
|
337
|
+
return this.__testWebhookServerOverride;
|
|
338
|
+
if (!this.core?.getWebhookUrl())
|
|
339
|
+
return undefined;
|
|
340
|
+
return { getUrl: () => this.core.getWebhookUrl() };
|
|
1766
341
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
*/
|
|
1770
|
-
async resolveAgent(agentRef) {
|
|
1771
|
-
const result = await this.networkClient?.discoverAgents();
|
|
1772
|
-
if (!result?.success)
|
|
1773
|
-
return null;
|
|
1774
|
-
const agents = result.data || [];
|
|
1775
|
-
// #索引格式
|
|
1776
|
-
if (agentRef.startsWith('#')) {
|
|
1777
|
-
const index = parseInt(agentRef.slice(1)) - 1;
|
|
1778
|
-
return agents[index] || null;
|
|
1779
|
-
}
|
|
1780
|
-
// 精确匹配
|
|
1781
|
-
const exact = agents.find((a) => a.peerId === agentRef ||
|
|
1782
|
-
a.displayName === agentRef);
|
|
1783
|
-
if (exact)
|
|
1784
|
-
return exact;
|
|
1785
|
-
// 模糊匹配
|
|
1786
|
-
const fuzzy = agents.find((a) => a.peerId.startsWith(agentRef) ||
|
|
1787
|
-
(a.displayName?.toLowerCase().includes(agentRef.toLowerCase()) ?? false));
|
|
1788
|
-
return fuzzy || null;
|
|
342
|
+
set _webhookServer(value) {
|
|
343
|
+
this.__testWebhookServerOverride = value;
|
|
1789
344
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
async handleContacts(params, _context) {
|
|
345
|
+
/** @deprecated 使用 getReputationSystem() 代替 */
|
|
346
|
+
get _reputationSystem() {
|
|
347
|
+
if (this.__testReputationSystemOverride !== undefined)
|
|
348
|
+
return this.__testReputationSystemOverride;
|
|
349
|
+
if (!this.components)
|
|
350
|
+
return undefined;
|
|
1797
351
|
try {
|
|
1798
|
-
|
|
1799
|
-
switch (params.action) {
|
|
1800
|
-
case 'list': {
|
|
1801
|
-
const filter = {};
|
|
1802
|
-
if (params.status) {
|
|
1803
|
-
filter.status = params.status;
|
|
1804
|
-
}
|
|
1805
|
-
if (params.group) {
|
|
1806
|
-
filter.group = params.group;
|
|
1807
|
-
}
|
|
1808
|
-
const contacts = cm.getContacts(filter, { field: 'name', order: 'asc' });
|
|
1809
|
-
const stats = cm.getStats();
|
|
1810
|
-
return {
|
|
1811
|
-
content: `📋 **通讯录** (${stats.total} 个联系人)\n\n` +
|
|
1812
|
-
contacts.map(c => {
|
|
1813
|
-
const statusIcon = {
|
|
1814
|
-
[contact_types_js_1.FriendStatus.FRIEND]: '💚',
|
|
1815
|
-
[contact_types_js_1.FriendStatus.STRANGER]: '⚪',
|
|
1816
|
-
[contact_types_js_1.FriendStatus.PENDING]: '🟡',
|
|
1817
|
-
[contact_types_js_1.FriendStatus.BLOCKED]: '🔴',
|
|
1818
|
-
}[c.status];
|
|
1819
|
-
return `${statusIcon} **${c.name}**\n Peer: ${c.peerId.slice(0, 16)}...\n 信誉: ${c.reputation} | 状态: ${c.status}`;
|
|
1820
|
-
}).join('\n\n') || '暂无联系人'
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
case 'get': {
|
|
1824
|
-
let contact;
|
|
1825
|
-
if (params.contact_id) {
|
|
1826
|
-
contact = cm.getContact(params.contact_id);
|
|
1827
|
-
}
|
|
1828
|
-
else if (params.peer_id) {
|
|
1829
|
-
contact = cm.getContactByPeerId(params.peer_id);
|
|
1830
|
-
}
|
|
1831
|
-
else {
|
|
1832
|
-
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1833
|
-
}
|
|
1834
|
-
if (!contact) {
|
|
1835
|
-
return { content: '❌ 联系人不存在' };
|
|
1836
|
-
}
|
|
1837
|
-
return {
|
|
1838
|
-
content: `👤 **${contact.name}**\n` +
|
|
1839
|
-
` ID: ${contact.id}\n` +
|
|
1840
|
-
` Peer ID: ${contact.peerId}\n` +
|
|
1841
|
-
` 状态: ${contact.status}\n` +
|
|
1842
|
-
` 信誉: ${contact.reputation}\n` +
|
|
1843
|
-
` 分组: ${contact.groups.join(', ') || '无'}\n` +
|
|
1844
|
-
` 标签: ${contact.tags.join(', ') || '无'}\n` +
|
|
1845
|
-
` 最后通信: ${contact.lastCommunicationTime ? new Date(contact.lastCommunicationTime).toLocaleString() : '从未'}\n` +
|
|
1846
|
-
(contact.notes ? ` 备注: ${contact.notes}` : '')
|
|
1847
|
-
};
|
|
1848
|
-
}
|
|
1849
|
-
case 'add': {
|
|
1850
|
-
if (!params.peer_id || !params.name) {
|
|
1851
|
-
return { content: '❌ 需要提供 peer_id 和 name' };
|
|
1852
|
-
}
|
|
1853
|
-
const contact = cm.addContact({
|
|
1854
|
-
name: params.name,
|
|
1855
|
-
peerId: params.peer_id,
|
|
1856
|
-
groups: params.groups,
|
|
1857
|
-
tags: params.tags,
|
|
1858
|
-
notes: params.notes,
|
|
1859
|
-
});
|
|
1860
|
-
// P1-2 修复:检查添加是否成功
|
|
1861
|
-
if (!contact) {
|
|
1862
|
-
return { content: '❌ 添加联系人失败(可能保存失败或已存在)' };
|
|
1863
|
-
}
|
|
1864
|
-
return { content: `✅ 已添加联系人: ${contact.name} (${contact.peerId.slice(0, 16)})` };
|
|
1865
|
-
}
|
|
1866
|
-
case 'remove': {
|
|
1867
|
-
let contactId = params.contact_id;
|
|
1868
|
-
if (!contactId && params.peer_id) {
|
|
1869
|
-
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1870
|
-
contactId = contact?.id;
|
|
1871
|
-
}
|
|
1872
|
-
if (!contactId) {
|
|
1873
|
-
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1874
|
-
}
|
|
1875
|
-
const success = cm.removeContact(contactId);
|
|
1876
|
-
return { content: success ? '✅ 已删除联系人' : '❌ 联系人不存在' };
|
|
1877
|
-
}
|
|
1878
|
-
case 'update': {
|
|
1879
|
-
let contactId = params.contact_id;
|
|
1880
|
-
if (!contactId && params.peer_id) {
|
|
1881
|
-
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1882
|
-
contactId = contact?.id;
|
|
1883
|
-
}
|
|
1884
|
-
if (!contactId) {
|
|
1885
|
-
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1886
|
-
}
|
|
1887
|
-
const contact = cm.updateContact(contactId, {
|
|
1888
|
-
name: params.name,
|
|
1889
|
-
groups: params.groups,
|
|
1890
|
-
tags: params.tags,
|
|
1891
|
-
notes: params.notes,
|
|
1892
|
-
});
|
|
1893
|
-
return { content: contact ? `✅ 已更新联系人: ${contact.name}` : '❌ 联系人不存在' };
|
|
1894
|
-
}
|
|
1895
|
-
case 'block': {
|
|
1896
|
-
let contactId = params.contact_id;
|
|
1897
|
-
if (!contactId && params.peer_id) {
|
|
1898
|
-
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1899
|
-
contactId = contact?.id;
|
|
1900
|
-
}
|
|
1901
|
-
if (!contactId) {
|
|
1902
|
-
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1903
|
-
}
|
|
1904
|
-
const success = cm.blockContact(contactId);
|
|
1905
|
-
return { content: success ? '✅ 已拉黑联系人' : '❌ 联系人不存在' };
|
|
1906
|
-
}
|
|
1907
|
-
case 'unblock': {
|
|
1908
|
-
let contactId = params.contact_id;
|
|
1909
|
-
if (!contactId && params.peer_id) {
|
|
1910
|
-
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1911
|
-
contactId = contact?.id;
|
|
1912
|
-
}
|
|
1913
|
-
if (!contactId) {
|
|
1914
|
-
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1915
|
-
}
|
|
1916
|
-
const success = cm.unblockContact(contactId);
|
|
1917
|
-
return { content: success ? '✅ 已解除拉黑' : '❌ 联系人不存在' };
|
|
1918
|
-
}
|
|
1919
|
-
default:
|
|
1920
|
-
return { content: '❌ 未知操作' };
|
|
1921
|
-
}
|
|
352
|
+
return this.components.getReputationSystem();
|
|
1922
353
|
}
|
|
1923
|
-
catch
|
|
1924
|
-
return
|
|
354
|
+
catch {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
set _reputationSystem(value) {
|
|
359
|
+
this.__testReputationSystemOverride = value;
|
|
360
|
+
}
|
|
361
|
+
// ========== 公共接口实现 ==========
|
|
362
|
+
isInitialized() { return this.core?.isInitialized() ?? false; }
|
|
363
|
+
getF2AStatus() { return this.core?.getF2AStatus() ?? { running: false }; }
|
|
364
|
+
getF2A() { return this.core?.getF2A(); }
|
|
365
|
+
getConfig() { return this._pluginConfig; }
|
|
366
|
+
getApi() { return this._pluginApi; }
|
|
367
|
+
getNetworkClient() { if (!this.components)
|
|
368
|
+
throw new Error('Not initialized'); return this.components.getNetworkClient(); }
|
|
369
|
+
getReputationSystem() { if (!this.components)
|
|
370
|
+
throw new Error('Not initialized'); return this.components.getReputationSystem(); }
|
|
371
|
+
getNodeManager() { if (!this.components)
|
|
372
|
+
throw new Error('Not initialized'); return this.components.getNodeManager(); }
|
|
373
|
+
getTaskQueue() { if (!this.components)
|
|
374
|
+
throw new Error('Not initialized'); return this.components.getTaskQueue(); }
|
|
375
|
+
getAnnouncementQueue() { if (!this.components)
|
|
376
|
+
throw new Error('Not initialized'); return this.components.getAnnouncementQueue(); }
|
|
377
|
+
getReviewCommittee() { return this.components?.getReviewCommittee(); }
|
|
378
|
+
getContactManager() { if (!this.components)
|
|
379
|
+
throw new Error('Not initialized'); return this.components.getContactManager(); }
|
|
380
|
+
getHandshakeProtocol() {
|
|
381
|
+
if (!this.components)
|
|
382
|
+
return undefined;
|
|
383
|
+
try {
|
|
384
|
+
return this.components.getHandshakeProtocol();
|
|
1925
385
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
386
|
+
catch {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// ========== 兼容性属性(供 PluginInternalAccess 访问) ==========
|
|
391
|
+
/** @internal 用于 tool-handlers 直接访问 */
|
|
392
|
+
get taskQueue() { return this._taskQueue; }
|
|
393
|
+
get networkClient() { return this.components?.getNetworkClient(); }
|
|
394
|
+
get reputationSystem() { return this._reputationSystem; }
|
|
395
|
+
get reviewCommittee() { return this.components?.getReviewCommittee(); }
|
|
396
|
+
get config() { return this.getConfig(); }
|
|
397
|
+
get api() { return this.getApi(); }
|
|
398
|
+
async discoverAgents(capability) {
|
|
399
|
+
const f2a = this.core?.getF2A();
|
|
400
|
+
if (!f2a)
|
|
401
|
+
return { success: false, error: { message: 'F2A 实例未初始化' } };
|
|
1931
402
|
try {
|
|
1932
|
-
|
|
1933
|
-
switch (params.action) {
|
|
1934
|
-
case 'list': {
|
|
1935
|
-
const groups = cm.getGroups();
|
|
1936
|
-
return {
|
|
1937
|
-
content: `📁 **分组列表** (${groups.length} 个)\n\n` +
|
|
1938
|
-
groups.map(g => `• **${g.name}** (${g.id})\n ${g.description || '无描述'}`).join('\n\n')
|
|
1939
|
-
};
|
|
1940
|
-
}
|
|
1941
|
-
case 'create': {
|
|
1942
|
-
if (!params.name) {
|
|
1943
|
-
return { content: '❌ 需要提供分组名称' };
|
|
1944
|
-
}
|
|
1945
|
-
const group = cm.createGroup({
|
|
1946
|
-
name: params.name,
|
|
1947
|
-
description: params.description,
|
|
1948
|
-
color: params.color,
|
|
1949
|
-
});
|
|
1950
|
-
if (!group) {
|
|
1951
|
-
return { content: '❌ 创建分组失败' };
|
|
1952
|
-
}
|
|
1953
|
-
return { content: `✅ 已创建分组: ${group.name}` };
|
|
1954
|
-
}
|
|
1955
|
-
case 'update': {
|
|
1956
|
-
if (!params.group_id) {
|
|
1957
|
-
return { content: '❌ 需要提供 group_id' };
|
|
1958
|
-
}
|
|
1959
|
-
const group = cm.updateGroup(params.group_id, {
|
|
1960
|
-
name: params.name,
|
|
1961
|
-
description: params.description,
|
|
1962
|
-
color: params.color,
|
|
1963
|
-
});
|
|
1964
|
-
return { content: group ? `✅ 已更新分组: ${group.name}` : '❌ 分组不存在' };
|
|
1965
|
-
}
|
|
1966
|
-
case 'delete': {
|
|
1967
|
-
if (!params.group_id) {
|
|
1968
|
-
return { content: '❌ 需要提供 group_id' };
|
|
1969
|
-
}
|
|
1970
|
-
const success = cm.deleteGroup(params.group_id);
|
|
1971
|
-
return { content: success ? '✅ 已删除分组' : '❌ 无法删除(分组不存在或为默认分组)' };
|
|
1972
|
-
}
|
|
1973
|
-
default:
|
|
1974
|
-
return { content: '❌ 未知操作' };
|
|
1975
|
-
}
|
|
403
|
+
return { success: true, data: await f2a.discoverAgents(capability) };
|
|
1976
404
|
}
|
|
1977
405
|
catch (err) {
|
|
1978
|
-
return {
|
|
406
|
+
return { success: false, error: { message: (0, connector_helpers_js_1.extractErrorMessage)(err) } };
|
|
1979
407
|
}
|
|
1980
408
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
409
|
+
async getConnectedPeers() {
|
|
410
|
+
const f2a = this.core?.getF2A();
|
|
411
|
+
if (!f2a)
|
|
412
|
+
return { success: false, error: { message: 'F2A 实例未初始化' } };
|
|
1985
413
|
try {
|
|
1986
|
-
|
|
1987
|
-
return { content: '❌ F2A 实例未初始化' };
|
|
1988
|
-
}
|
|
1989
|
-
if (!this._contactManager) {
|
|
1990
|
-
// 确保联系人管理器已初始化
|
|
1991
|
-
this.contactManager;
|
|
1992
|
-
}
|
|
1993
|
-
if (!this._handshakeProtocol) {
|
|
1994
|
-
// 初始化握手协议
|
|
1995
|
-
this.handshakeProtocol;
|
|
1996
|
-
}
|
|
1997
|
-
if (!this._handshakeProtocol) {
|
|
1998
|
-
return { content: '❌ 握手协议未初始化' };
|
|
1999
|
-
}
|
|
2000
|
-
const requestId = await this._handshakeProtocol.sendFriendRequest(params.peer_id, params.message);
|
|
2001
|
-
if (requestId) {
|
|
2002
|
-
return { content: `✅ 好友请求已发送\n请求 ID: ${requestId}\n等待对方响应...` };
|
|
2003
|
-
}
|
|
2004
|
-
else {
|
|
2005
|
-
return { content: '❌ 发送好友请求失败' };
|
|
2006
|
-
}
|
|
414
|
+
return { success: true, data: f2a.p2pNetwork?.getConnectedPeers?.() || [] };
|
|
2007
415
|
}
|
|
2008
416
|
catch (err) {
|
|
2009
|
-
return {
|
|
417
|
+
return { success: false, error: { message: (0, connector_helpers_js_1.extractErrorMessage)(err) } };
|
|
2010
418
|
}
|
|
2011
419
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
420
|
+
async sendMessage(to, content, metadata) {
|
|
421
|
+
const f2a = this.core?.getF2A();
|
|
422
|
+
if (!f2a)
|
|
423
|
+
return { success: false, error: 'F2A 实例未初始化' };
|
|
2016
424
|
try {
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
case 'list': {
|
|
2020
|
-
const pending = cm.getPendingHandshakes();
|
|
2021
|
-
if (pending.length === 0) {
|
|
2022
|
-
return { content: '📭 暂无待处理的好友请求' };
|
|
2023
|
-
}
|
|
2024
|
-
return {
|
|
2025
|
-
content: `📬 **待处理的好友请求** (${pending.length} 个)\n\n` +
|
|
2026
|
-
pending.map(p => `• **${p.fromName}**\n Peer: ${p.from.slice(0, 16)}...\n 请求 ID: ${p.requestId}\n 收到: ${new Date(p.receivedAt).toLocaleString()}` +
|
|
2027
|
-
(p.message ? `\n 消息: ${p.message}` : '')).join('\n\n')
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
case 'accept': {
|
|
2031
|
-
if (!params.request_id) {
|
|
2032
|
-
return { content: '❌ 需要提供 request_id' };
|
|
2033
|
-
}
|
|
2034
|
-
if (!this._handshakeProtocol) {
|
|
2035
|
-
return { content: '❌ 握手协议未初始化' };
|
|
2036
|
-
}
|
|
2037
|
-
const success = await this._handshakeProtocol.acceptRequest(params.request_id);
|
|
2038
|
-
return { content: success ? '✅ 已接受好友请求,双方已成为好友' : '❌ 接受失败' };
|
|
2039
|
-
}
|
|
2040
|
-
case 'reject': {
|
|
2041
|
-
if (!params.request_id) {
|
|
2042
|
-
return { content: '❌ 需要提供 request_id' };
|
|
2043
|
-
}
|
|
2044
|
-
if (!this._handshakeProtocol) {
|
|
2045
|
-
return { content: '❌ 握手协议未初始化' };
|
|
2046
|
-
}
|
|
2047
|
-
const success = await this._handshakeProtocol.rejectRequest(params.request_id, params.reason);
|
|
2048
|
-
return { content: success ? '✅ 已拒绝好友请求' : '❌ 拒绝失败' };
|
|
2049
|
-
}
|
|
2050
|
-
default:
|
|
2051
|
-
return { content: '❌ 未知操作' };
|
|
2052
|
-
}
|
|
425
|
+
await f2a.sendMessage(to, content, metadata);
|
|
426
|
+
return { success: true };
|
|
2053
427
|
}
|
|
2054
428
|
catch (err) {
|
|
2055
|
-
return {
|
|
429
|
+
return { success: false, error: (0, connector_helpers_js_1.extractErrorMessage)(err) };
|
|
2056
430
|
}
|
|
2057
431
|
}
|
|
2058
|
-
|
|
2059
|
-
* 处理导出通讯录工具
|
|
2060
|
-
*/
|
|
2061
|
-
async handleContactsExport(_params, _context) {
|
|
432
|
+
async sendFriendRequest(peerId, message) {
|
|
2062
433
|
try {
|
|
2063
|
-
|
|
2064
|
-
const data = cm.exportContacts(this._f2a?.peerId);
|
|
2065
|
-
return {
|
|
2066
|
-
content: `📤 **通讯录导出成功**\n\n` +
|
|
2067
|
-
`联系人: ${data.contacts.length} 个\n` +
|
|
2068
|
-
`分组: ${data.groups.length} 个\n` +
|
|
2069
|
-
`导出时间: ${new Date(data.exportedAt).toLocaleString()}\n\n` +
|
|
2070
|
-
'```json\n' + JSON.stringify(data, null, 2) + '\n```',
|
|
2071
|
-
data,
|
|
2072
|
-
};
|
|
434
|
+
return this.components.getHandshakeProtocol().sendFriendRequest(peerId, message);
|
|
2073
435
|
}
|
|
2074
|
-
catch
|
|
2075
|
-
|
|
436
|
+
catch {
|
|
437
|
+
this.core?.getLogger()?.warn('[F2A] 握手协议未初始化');
|
|
438
|
+
return null;
|
|
2076
439
|
}
|
|
2077
440
|
}
|
|
2078
|
-
|
|
2079
|
-
* 处理导入通讯录工具
|
|
2080
|
-
*/
|
|
2081
|
-
async handleContactsImport(params, _context) {
|
|
441
|
+
async acceptFriendRequest(requestId) {
|
|
2082
442
|
try {
|
|
2083
|
-
|
|
2084
|
-
const result = cm.importContacts(params.data, params.merge ?? true);
|
|
2085
|
-
if (result.success) {
|
|
2086
|
-
return {
|
|
2087
|
-
content: `📥 **通讯录导入完成**\n\n` +
|
|
2088
|
-
`✅ 导入联系人: ${result.importedContacts} 个\n` +
|
|
2089
|
-
`✅ 导入分组: ${result.importedGroups} 个\n` +
|
|
2090
|
-
`⏭️ 跳过联系人: ${result.skippedContacts} 个` +
|
|
2091
|
-
(result.errors.length ? `\n\n⚠️ 错误:\n${result.errors.join('\n')}` : '')
|
|
2092
|
-
};
|
|
2093
|
-
}
|
|
2094
|
-
else {
|
|
2095
|
-
return {
|
|
2096
|
-
content: `❌ 导入失败\n\n错误:\n${result.errors.join('\n')}`
|
|
2097
|
-
};
|
|
2098
|
-
}
|
|
443
|
+
return this.components.getHandshakeProtocol().acceptRequest(requestId);
|
|
2099
444
|
}
|
|
2100
|
-
catch
|
|
2101
|
-
return
|
|
445
|
+
catch {
|
|
446
|
+
return false;
|
|
2102
447
|
}
|
|
2103
448
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
*/
|
|
2108
|
-
async shutdown() {
|
|
2109
|
-
this._logger?.info('[F2A Adapter] 正在关闭...');
|
|
2110
|
-
// 停止轮询定时器
|
|
2111
|
-
if (this.pollTimer) {
|
|
2112
|
-
clearInterval(this.pollTimer);
|
|
2113
|
-
this.pollTimer = undefined;
|
|
2114
|
-
}
|
|
2115
|
-
// Issue #99: 关闭握手协议
|
|
2116
|
-
if (this._handshakeProtocol) {
|
|
2117
|
-
this._handshakeProtocol.shutdown();
|
|
2118
|
-
this._logger?.info('[F2A Adapter] HandshakeProtocol 已关闭');
|
|
2119
|
-
this._handshakeProtocol = undefined;
|
|
2120
|
-
}
|
|
2121
|
-
// Issue #98: 刷新通讯录数据
|
|
2122
|
-
if (this._contactManager) {
|
|
2123
|
-
this._contactManager.flush();
|
|
2124
|
-
this._logger?.info('[F2A Adapter] ContactManager 数据已保存');
|
|
2125
|
-
this._contactManager = undefined;
|
|
2126
|
-
}
|
|
2127
|
-
// 停止 F2A 实例(新架构直接管理)
|
|
2128
|
-
if (this._f2a) {
|
|
2129
|
-
try {
|
|
2130
|
-
await this._f2a.stop();
|
|
2131
|
-
this._logger?.info('[F2A Adapter] F2A 实例已停止');
|
|
2132
|
-
}
|
|
2133
|
-
catch (err) {
|
|
2134
|
-
this._logger?.warn('[F2A Adapter] F2A 实例停止失败', { error: extractErrorMessage(err) });
|
|
2135
|
-
}
|
|
2136
|
-
this._f2a = undefined;
|
|
2137
|
-
}
|
|
2138
|
-
// 停止 Webhook 服务器(只有已启动时才关闭)
|
|
2139
|
-
if (this._webhookServer) {
|
|
2140
|
-
await this._webhookServer.stop?.();
|
|
2141
|
-
this._logger?.info('[F2A Adapter] Webhook 服务器已停止');
|
|
2142
|
-
}
|
|
2143
|
-
// P1 修复:关闭前刷新信誉系统数据,确保持久化
|
|
2144
|
-
if (this._reputationSystem) {
|
|
2145
|
-
this._reputationSystem.flush();
|
|
2146
|
-
this._logger?.info('[F2A Adapter] 信誉系统数据已保存');
|
|
2147
|
-
}
|
|
2148
|
-
// P1 修复:关闭 TaskGuard,停止持久化定时器并保存最终状态
|
|
2149
|
-
task_guard_js_1.taskGuard.shutdown();
|
|
2150
|
-
this._logger?.info('[F2A Adapter] TaskGuard 已关闭');
|
|
2151
|
-
// 停止 F2A Node(只有已启动时才关闭)
|
|
2152
|
-
if (this._nodeManager) {
|
|
2153
|
-
await this._nodeManager.stop();
|
|
2154
|
-
this._logger?.info('[F2A Adapter] F2A Node 管理器已停止');
|
|
449
|
+
async rejectFriendRequest(requestId, reason) {
|
|
450
|
+
try {
|
|
451
|
+
return this.components.getHandshakeProtocol().rejectRequest(requestId, reason);
|
|
2155
452
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
if (this._taskQueue) {
|
|
2159
|
-
this._taskQueue.close();
|
|
2160
|
-
this._logger?.info('[F2A Adapter] 任务队列已关闭,持久化数据已保留');
|
|
453
|
+
catch {
|
|
454
|
+
return false;
|
|
2161
455
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
456
|
+
}
|
|
457
|
+
get f2aClient() {
|
|
458
|
+
return {
|
|
459
|
+
discoverAgents: (capability) => this.discoverAgents(capability),
|
|
460
|
+
getConnectedPeers: () => this.getConnectedPeers(),
|
|
461
|
+
};
|
|
2164
462
|
}
|
|
2165
463
|
}
|
|
2166
|
-
exports.
|
|
2167
|
-
|
|
2168
|
-
exports.default = F2AOpenClawAdapter;
|
|
464
|
+
exports.F2APlugin = F2APlugin;
|
|
465
|
+
exports.default = F2APlugin;
|
|
2169
466
|
//# sourceMappingURL=connector.js.map
|