@f2a/openclaw-f2a 0.2.24 → 0.3.0
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/connector.d.ts +84 -2
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js +818 -20
- package/dist/connector.js.map +1 -1
- package/dist/contact-manager.d.ts +263 -0
- package/dist/contact-manager.d.ts.map +1 -0
- package/dist/contact-manager.js +872 -0
- package/dist/contact-manager.js.map +1 -0
- package/dist/contact-types.d.ts +287 -0
- package/dist/contact-types.d.ts.map +1 -0
- package/dist/contact-types.js +38 -0
- package/dist/contact-types.js.map +1 -0
- package/dist/handshake-protocol.d.ts +158 -0
- package/dist/handshake-protocol.d.ts.map +1 -0
- package/dist/handshake-protocol.js +441 -0
- package/dist/handshake-protocol.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/tool-handlers.d.ts.map +1 -1
- package/dist/tool-handlers.js +15 -0
- package/dist/tool-handlers.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/connector.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.F2AOpenClawAdapter = void 0;
|
|
13
|
+
exports.isValidPeerId = isValidPeerId;
|
|
13
14
|
const path_1 = require("path");
|
|
14
15
|
const os_1 = require("os");
|
|
15
16
|
const node_manager_js_1 = require("./node-manager.js");
|
|
@@ -25,6 +26,166 @@ const task_guard_js_1 = require("./task-guard.js");
|
|
|
25
26
|
const tool_handlers_js_1 = require("./tool-handlers.js");
|
|
26
27
|
const claim_handlers_js_1 = require("./claim-handlers.js");
|
|
27
28
|
const network_1 = require("@f2a/network");
|
|
29
|
+
// Issue #98 & #99: 通讯录和握手机制
|
|
30
|
+
const contact_manager_js_1 = require("./contact-manager.js");
|
|
31
|
+
const handshake_protocol_js_1 = require("./handshake-protocol.js");
|
|
32
|
+
const contact_types_js_1 = require("./contact-types.js");
|
|
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
|
+
}
|
|
28
189
|
class F2AOpenClawAdapter {
|
|
29
190
|
name = 'f2a-openclaw-f2a';
|
|
30
191
|
version = '0.3.0';
|
|
@@ -44,6 +205,16 @@ class F2AOpenClawAdapter {
|
|
|
44
205
|
// 处理器实例(延迟初始化)
|
|
45
206
|
_toolHandlers;
|
|
46
207
|
_claimHandlers;
|
|
208
|
+
// Issue #98 & #99: 通讯录和握手机制
|
|
209
|
+
_contactManager;
|
|
210
|
+
_handshakeProtocol;
|
|
211
|
+
// P1-3: 消息哈希去重缓存,防止恶意节点绕过 echo 检测
|
|
212
|
+
// 存储最近处理过的消息哈希,避免重复处理
|
|
213
|
+
_processedMessageHashes = new Map();
|
|
214
|
+
/** P1-3: 消息去重缓存最大条目数 */
|
|
215
|
+
static MAX_MESSAGE_HASH_CACHE_SIZE = 10000;
|
|
216
|
+
/** P1-3: 消息去重缓存条目最大存活时间(毫秒) */
|
|
217
|
+
static MESSAGE_HASH_TTL_MS = 5 * 60 * 1000; // 5 分钟
|
|
47
218
|
config;
|
|
48
219
|
nodeConfig;
|
|
49
220
|
capabilities = [];
|
|
@@ -62,18 +233,125 @@ class F2AOpenClawAdapter {
|
|
|
62
233
|
}
|
|
63
234
|
/**
|
|
64
235
|
* 获取默认的 F2A 数据目录
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
|
67
245
|
*/
|
|
68
246
|
getDefaultDataDir() {
|
|
247
|
+
// P1-1: 优先使用用户配置的 dataDir
|
|
248
|
+
if (this.config?.dataDir) {
|
|
249
|
+
return this.config.dataDir;
|
|
250
|
+
}
|
|
69
251
|
// 默认:使用 agent workspace 目录
|
|
70
252
|
const workspace = this.api?.config?.agents?.defaults?.workspace;
|
|
71
|
-
|
|
253
|
+
// P0-1: 验证 workspace 路径安全性
|
|
254
|
+
if (isPathSafe(workspace)) {
|
|
72
255
|
return (0, path_1.join)(workspace, '.f2a');
|
|
73
256
|
}
|
|
74
257
|
// 兼容旧版本
|
|
75
258
|
return (0, path_1.join)((0, os_1.homedir)(), '.f2a');
|
|
76
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
|
+
}
|
|
77
355
|
/**
|
|
78
356
|
* 获取网络客户端(懒加载)
|
|
79
357
|
* 新架构:直接使用 F2A 实例的方法,不再通过 HTTP
|
|
@@ -171,6 +449,28 @@ class F2AOpenClawAdapter {
|
|
|
171
449
|
}
|
|
172
450
|
return this._claimHandlers;
|
|
173
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Issue #98: 获取联系人管理器(延迟初始化)
|
|
454
|
+
*/
|
|
455
|
+
get contactManager() {
|
|
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
|
+
*/
|
|
467
|
+
get handshakeProtocol() {
|
|
468
|
+
if (!this._handshakeProtocol && this._f2a && this._contactManager) {
|
|
469
|
+
this._handshakeProtocol = new handshake_protocol_js_1.HandshakeProtocol(this._f2a, this._contactManager, this._logger);
|
|
470
|
+
this._logger?.info('[F2A Adapter] HandshakeProtocol 已初始化');
|
|
471
|
+
}
|
|
472
|
+
return this._handshakeProtocol;
|
|
473
|
+
}
|
|
174
474
|
/**
|
|
175
475
|
* 检查是否已初始化(用于判断是否需要启动服务)
|
|
176
476
|
*/
|
|
@@ -205,7 +505,7 @@ class F2AOpenClawAdapter {
|
|
|
205
505
|
return { success: true, data: agents };
|
|
206
506
|
}
|
|
207
507
|
catch (err) {
|
|
208
|
-
return { success: false, error: { message:
|
|
508
|
+
return { success: false, error: { message: extractErrorMessage(err) } };
|
|
209
509
|
}
|
|
210
510
|
},
|
|
211
511
|
getConnectedPeers: async () => {
|
|
@@ -218,7 +518,7 @@ class F2AOpenClawAdapter {
|
|
|
218
518
|
return { success: true, data: peers };
|
|
219
519
|
}
|
|
220
520
|
catch (err) {
|
|
221
|
-
return { success: false, error: { message:
|
|
521
|
+
return { success: false, error: { message: extractErrorMessage(err) } };
|
|
222
522
|
}
|
|
223
523
|
}
|
|
224
524
|
};
|
|
@@ -299,8 +599,16 @@ class F2AOpenClawAdapter {
|
|
|
299
599
|
debugLog(`[F2A Adapter] 使用数据目录: ${dataDir}`);
|
|
300
600
|
debugLog(`[F2A Adapter] workspace: ${this.api?.config?.agents?.defaults?.workspace}`);
|
|
301
601
|
debugLog(`[F2A Adapter] config.dataDir: ${this.config.dataDir}`);
|
|
602
|
+
// Issue #96: 从 IDENTITY.md 读取 agent 名字
|
|
603
|
+
const workspace = this.api?.config?.agents?.defaults?.workspace;
|
|
604
|
+
const identityName = readAgentNameFromIdentity(workspace);
|
|
605
|
+
// 优先级:IDENTITY.md > config.agentName > 默认值
|
|
606
|
+
const displayName = identityName || this.config.agentName || 'OpenClaw Agent';
|
|
607
|
+
if (identityName) {
|
|
608
|
+
debugLog(`[F2A Adapter] 从 IDENTITY.md 读取 agent 名字: ${identityName}`);
|
|
609
|
+
}
|
|
302
610
|
this._f2a = await network_1.F2A.create({
|
|
303
|
-
displayName
|
|
611
|
+
displayName,
|
|
304
612
|
dataDir,
|
|
305
613
|
network: {
|
|
306
614
|
listenPort: this.config.p2pPort || 0,
|
|
@@ -311,7 +619,17 @@ class F2AOpenClawAdapter {
|
|
|
311
619
|
});
|
|
312
620
|
// 监听 P2P 消息,调用 OpenClaw Agent 生成回复
|
|
313
621
|
this._f2a.on('message', async (msg) => {
|
|
314
|
-
|
|
622
|
+
// P1-6: 验证 PeerID 格式
|
|
623
|
+
if (!isValidPeerId(msg.from)) {
|
|
624
|
+
this._logger?.warn(`[F2A Adapter] 拒绝来自无效 PeerID 的消息: ${String(msg.from).slice(0, 20)}`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// P1-7: 检查消息长度限制
|
|
628
|
+
if (msg.content && msg.content.length > MAX_MESSAGE_LENGTH) {
|
|
629
|
+
this._logger?.warn(`[F2A Adapter] 消息过长 (${msg.content.length} bytes),拒绝处理`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const logMsg = `[F2A Adapter] 收到 P2P 消息: from=${msg.from.slice(0, 16)}, content=${msg.content?.slice(0, 50)}`;
|
|
315
633
|
this._logger?.info(logMsg);
|
|
316
634
|
// 写入文件日志
|
|
317
635
|
try {
|
|
@@ -321,10 +639,9 @@ class F2AOpenClawAdapter {
|
|
|
321
639
|
}
|
|
322
640
|
catch { }
|
|
323
641
|
try {
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
msg.content?.includes('NO_REPLY');
|
|
642
|
+
// P1-2, P1-5: 改进的回声循环检测
|
|
643
|
+
// 使用更严格的检测逻辑,防止恶意绕过
|
|
644
|
+
const isReply = this.isEchoMessage(msg);
|
|
328
645
|
if (isReply) {
|
|
329
646
|
this._logger?.info('[F2A Adapter] 跳过 Agent 回复,避免回声循环');
|
|
330
647
|
return;
|
|
@@ -338,7 +655,7 @@ class F2AOpenClawAdapter {
|
|
|
338
655
|
}
|
|
339
656
|
}
|
|
340
657
|
catch (err) {
|
|
341
|
-
this._logger?.error('[F2A Adapter] 处理消息失败', { error:
|
|
658
|
+
this._logger?.error('[F2A Adapter] 处理消息失败', { error: extractErrorMessage(err) });
|
|
342
659
|
}
|
|
343
660
|
});
|
|
344
661
|
// 监听其他事件
|
|
@@ -367,7 +684,7 @@ class F2AOpenClawAdapter {
|
|
|
367
684
|
});
|
|
368
685
|
}
|
|
369
686
|
catch (err) {
|
|
370
|
-
const errorMsg =
|
|
687
|
+
const errorMsg = extractErrorMessage(err);
|
|
371
688
|
this._logger?.error(`[F2A Adapter] 创建 F2A 实例失败: ${errorMsg}`);
|
|
372
689
|
this._logger?.warn('[F2A Adapter] F2A Adapter 将以降级模式运行,P2P 功能不可用');
|
|
373
690
|
// 清理失败的实例
|
|
@@ -386,7 +703,7 @@ class F2AOpenClawAdapter {
|
|
|
386
703
|
this._logger?.info(`[F2A Adapter] Webhook 服务器已启动: ${this._webhookServer.getUrl()}`);
|
|
387
704
|
}
|
|
388
705
|
catch (err) {
|
|
389
|
-
const errorMsg =
|
|
706
|
+
const errorMsg = extractErrorMessage(err);
|
|
390
707
|
this._logger?.warn(`[F2A Adapter] Webhook 服务器启动失败: ${errorMsg}`);
|
|
391
708
|
}
|
|
392
709
|
// 启动兜底轮询
|
|
@@ -418,7 +735,7 @@ class F2AOpenClawAdapter {
|
|
|
418
735
|
this._logger?.info('[F2A Adapter] 回复已发送', { to: fromPeerId.slice(0, 16) });
|
|
419
736
|
}
|
|
420
737
|
catch (err) {
|
|
421
|
-
this._logger?.error('[F2A Adapter] 发送回复失败', { error:
|
|
738
|
+
this._logger?.error('[F2A Adapter] 发送回复失败', { error: extractErrorMessage(err) });
|
|
422
739
|
}
|
|
423
740
|
};
|
|
424
741
|
// 返回 dispatcher 对象,格式与 OpenClaw 兼容
|
|
@@ -481,7 +798,7 @@ class F2AOpenClawAdapter {
|
|
|
481
798
|
return undefined; // dispatcher 会自动发送回复
|
|
482
799
|
}
|
|
483
800
|
catch (err) {
|
|
484
|
-
debugLog(`[F2A Adapter] Channel API 失败: ${
|
|
801
|
+
debugLog(`[F2A Adapter] Channel API 失败: ${extractErrorMessage(err)}`);
|
|
485
802
|
}
|
|
486
803
|
}
|
|
487
804
|
// 降级:使用 subagent API
|
|
@@ -490,12 +807,13 @@ class F2AOpenClawAdapter {
|
|
|
490
807
|
try {
|
|
491
808
|
// 生成 idempotencyKey(必需参数)
|
|
492
809
|
const idempotencyKey = `f2a-${fromPeerId.slice(0, 16)}-${Date.now()}`;
|
|
810
|
+
// P1-3: 使用正确的类型,移除 as any
|
|
493
811
|
const runResult = await this.api.runtime.subagent.run({
|
|
494
812
|
sessionKey,
|
|
495
813
|
message,
|
|
496
814
|
deliver: false,
|
|
497
815
|
idempotencyKey,
|
|
498
|
-
});
|
|
816
|
+
});
|
|
499
817
|
const waitResult = await this.api.runtime.subagent.waitForRun({
|
|
500
818
|
runId: runResult.runId,
|
|
501
819
|
timeoutMs: 60000,
|
|
@@ -527,7 +845,7 @@ class F2AOpenClawAdapter {
|
|
|
527
845
|
}
|
|
528
846
|
}
|
|
529
847
|
catch (err) {
|
|
530
|
-
debugLog(`[F2A Adapter] Subagent 失败: ${
|
|
848
|
+
debugLog(`[F2A Adapter] Subagent 失败: ${extractErrorMessage(err)}`);
|
|
531
849
|
}
|
|
532
850
|
}
|
|
533
851
|
// 最终降级
|
|
@@ -981,6 +1299,157 @@ class F2AOpenClawAdapter {
|
|
|
981
1299
|
}
|
|
982
1300
|
},
|
|
983
1301
|
handler: this.toolHandlers.handleGetCapabilities.bind(this.toolHandlers)
|
|
1302
|
+
},
|
|
1303
|
+
// ========== Issue #98 & #99: 通讯录和握手机制工具 ==========
|
|
1304
|
+
{
|
|
1305
|
+
name: 'f2a_contacts',
|
|
1306
|
+
description: '管理通讯录联系人。Actions: list(列出联系人), get(获取详情), add(添加), remove(删除), update(更新), block(拉黑), unblock(解除拉黑)',
|
|
1307
|
+
parameters: {
|
|
1308
|
+
action: {
|
|
1309
|
+
type: 'string',
|
|
1310
|
+
description: '操作类型: list, get, add, remove, update, block, unblock',
|
|
1311
|
+
required: true,
|
|
1312
|
+
enum: ['list', 'get', 'add', 'remove', 'update', 'block', 'unblock']
|
|
1313
|
+
},
|
|
1314
|
+
contact_id: {
|
|
1315
|
+
type: 'string',
|
|
1316
|
+
description: '联系人 ID(get/remove/update/block/unblock 时需要)',
|
|
1317
|
+
required: false
|
|
1318
|
+
},
|
|
1319
|
+
peer_id: {
|
|
1320
|
+
type: 'string',
|
|
1321
|
+
description: 'Peer ID(add 时需要,get/remove 时可选)',
|
|
1322
|
+
required: false
|
|
1323
|
+
},
|
|
1324
|
+
name: {
|
|
1325
|
+
type: 'string',
|
|
1326
|
+
description: '联系人名称(add/update 时需要)',
|
|
1327
|
+
required: false
|
|
1328
|
+
},
|
|
1329
|
+
groups: {
|
|
1330
|
+
type: 'array',
|
|
1331
|
+
description: '分组列表',
|
|
1332
|
+
required: false
|
|
1333
|
+
},
|
|
1334
|
+
tags: {
|
|
1335
|
+
type: 'array',
|
|
1336
|
+
description: '标签列表',
|
|
1337
|
+
required: false
|
|
1338
|
+
},
|
|
1339
|
+
notes: {
|
|
1340
|
+
type: 'string',
|
|
1341
|
+
description: '备注信息',
|
|
1342
|
+
required: false
|
|
1343
|
+
},
|
|
1344
|
+
status: {
|
|
1345
|
+
type: 'string',
|
|
1346
|
+
description: '按状态过滤(list 时可选): stranger, pending, friend, blocked',
|
|
1347
|
+
required: false,
|
|
1348
|
+
enum: ['stranger', 'pending', 'friend', 'blocked']
|
|
1349
|
+
},
|
|
1350
|
+
group: {
|
|
1351
|
+
type: 'string',
|
|
1352
|
+
description: '按分组过滤(list 时可选)',
|
|
1353
|
+
required: false
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
handler: this.handleContacts.bind(this)
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
name: 'f2a_contact_groups',
|
|
1360
|
+
description: '管理联系人分组。Actions: list(列出分组), create(创建), update(更新), delete(删除)',
|
|
1361
|
+
parameters: {
|
|
1362
|
+
action: {
|
|
1363
|
+
type: 'string',
|
|
1364
|
+
description: '操作类型: list, create, update, delete',
|
|
1365
|
+
required: true,
|
|
1366
|
+
enum: ['list', 'create', 'update', 'delete']
|
|
1367
|
+
},
|
|
1368
|
+
group_id: {
|
|
1369
|
+
type: 'string',
|
|
1370
|
+
description: '分组 ID(update/delete 时需要)',
|
|
1371
|
+
required: false
|
|
1372
|
+
},
|
|
1373
|
+
name: {
|
|
1374
|
+
type: 'string',
|
|
1375
|
+
description: '分组名称(create/update 时需要)',
|
|
1376
|
+
required: false
|
|
1377
|
+
},
|
|
1378
|
+
description: {
|
|
1379
|
+
type: 'string',
|
|
1380
|
+
description: '分组描述',
|
|
1381
|
+
required: false
|
|
1382
|
+
},
|
|
1383
|
+
color: {
|
|
1384
|
+
type: 'string',
|
|
1385
|
+
description: '分组颜色(十六进制,如 #FF5733)',
|
|
1386
|
+
required: false
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
handler: this.handleContactGroups.bind(this)
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
name: 'f2a_friend_request',
|
|
1393
|
+
description: '发送好友请求给指定 Agent',
|
|
1394
|
+
parameters: {
|
|
1395
|
+
peer_id: {
|
|
1396
|
+
type: 'string',
|
|
1397
|
+
description: '目标 Agent 的 Peer ID',
|
|
1398
|
+
required: true
|
|
1399
|
+
},
|
|
1400
|
+
message: {
|
|
1401
|
+
type: 'string',
|
|
1402
|
+
description: '附加消息',
|
|
1403
|
+
required: false
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
handler: this.handleFriendRequest.bind(this)
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: 'f2a_pending_requests',
|
|
1410
|
+
description: '查看和处理待处理的好友请求。Actions: list(列出请求), accept(接受), reject(拒绝)',
|
|
1411
|
+
parameters: {
|
|
1412
|
+
action: {
|
|
1413
|
+
type: 'string',
|
|
1414
|
+
description: '操作类型: list, accept, reject',
|
|
1415
|
+
required: true,
|
|
1416
|
+
enum: ['list', 'accept', 'reject']
|
|
1417
|
+
},
|
|
1418
|
+
request_id: {
|
|
1419
|
+
type: 'string',
|
|
1420
|
+
description: '请求 ID(accept/reject 时需要)',
|
|
1421
|
+
required: false
|
|
1422
|
+
},
|
|
1423
|
+
reason: {
|
|
1424
|
+
type: 'string',
|
|
1425
|
+
description: '拒绝原因(reject 时可选)',
|
|
1426
|
+
required: false
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
handler: this.handlePendingRequests.bind(this)
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
name: 'f2a_contacts_export',
|
|
1433
|
+
description: '导出通讯录数据',
|
|
1434
|
+
parameters: {},
|
|
1435
|
+
handler: this.handleContactsExport.bind(this)
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
name: 'f2a_contacts_import',
|
|
1439
|
+
description: '导入通讯录数据',
|
|
1440
|
+
parameters: {
|
|
1441
|
+
data: {
|
|
1442
|
+
type: 'object',
|
|
1443
|
+
description: '导入的通讯录数据(JSON 格式)',
|
|
1444
|
+
required: true
|
|
1445
|
+
},
|
|
1446
|
+
merge: {
|
|
1447
|
+
type: 'boolean',
|
|
1448
|
+
description: '是否合并(true)或覆盖(false),默认 true',
|
|
1449
|
+
required: false
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
handler: this.handleContactsImport.bind(this)
|
|
984
1453
|
}
|
|
985
1454
|
];
|
|
986
1455
|
}
|
|
@@ -1101,6 +1570,16 @@ class F2AOpenClawAdapter {
|
|
|
1101
1570
|
}
|
|
1102
1571
|
},
|
|
1103
1572
|
onMessage: async (payload) => {
|
|
1573
|
+
// P1-6: 验证 PeerID 格式
|
|
1574
|
+
if (!isValidPeerId(payload.from)) {
|
|
1575
|
+
this._logger?.warn(`[F2A Adapter] onMessage: 拒绝来自无效 PeerID 的消息: ${String(payload.from).slice(0, 20)}`);
|
|
1576
|
+
return { response: 'Invalid sender' };
|
|
1577
|
+
}
|
|
1578
|
+
// P1-7: 检查消息长度限制
|
|
1579
|
+
if (payload.content && payload.content.length > MAX_MESSAGE_LENGTH) {
|
|
1580
|
+
this._logger?.warn(`[F2A Adapter] onMessage: 消息过长 (${payload.content.length} bytes),拒绝处理`);
|
|
1581
|
+
return { response: 'Message too long' };
|
|
1582
|
+
}
|
|
1104
1583
|
this._logger?.info('[F2A Adapter] 收到 P2P 消息', {
|
|
1105
1584
|
from: payload.from.slice(0, 16),
|
|
1106
1585
|
content: payload.content.slice(0, 50)
|
|
@@ -1116,7 +1595,7 @@ class F2AOpenClawAdapter {
|
|
|
1116
1595
|
return { response: result || '收到消息,但我暂时无法生成回复。' };
|
|
1117
1596
|
}
|
|
1118
1597
|
catch (error) {
|
|
1119
|
-
this._logger?.error('[F2A Adapter] 处理消息失败', { error:
|
|
1598
|
+
this._logger?.error('[F2A Adapter] 处理消息失败', { error: extractErrorMessage(error) });
|
|
1120
1599
|
return { response: '抱歉,我遇到了一些问题,无法处理你的消息。' };
|
|
1121
1600
|
}
|
|
1122
1601
|
},
|
|
@@ -1306,6 +1785,313 @@ class F2AOpenClawAdapter {
|
|
|
1306
1785
|
(a.displayName?.toLowerCase().includes(agentRef.toLowerCase()) ?? false));
|
|
1307
1786
|
return fuzzy || null;
|
|
1308
1787
|
}
|
|
1788
|
+
// ============================================================================
|
|
1789
|
+
// Issue #98 & #99: 通讯录和握手机制工具处理方法
|
|
1790
|
+
// ============================================================================
|
|
1791
|
+
/**
|
|
1792
|
+
* 处理通讯录工具
|
|
1793
|
+
*/
|
|
1794
|
+
async handleContacts(params, _context) {
|
|
1795
|
+
try {
|
|
1796
|
+
const cm = this.contactManager;
|
|
1797
|
+
switch (params.action) {
|
|
1798
|
+
case 'list': {
|
|
1799
|
+
const filter = {};
|
|
1800
|
+
if (params.status) {
|
|
1801
|
+
filter.status = params.status;
|
|
1802
|
+
}
|
|
1803
|
+
if (params.group) {
|
|
1804
|
+
filter.group = params.group;
|
|
1805
|
+
}
|
|
1806
|
+
const contacts = cm.getContacts(filter, { field: 'name', order: 'asc' });
|
|
1807
|
+
const stats = cm.getStats();
|
|
1808
|
+
return {
|
|
1809
|
+
content: `📋 **通讯录** (${stats.total} 个联系人)\n\n` +
|
|
1810
|
+
contacts.map(c => {
|
|
1811
|
+
const statusIcon = {
|
|
1812
|
+
[contact_types_js_1.FriendStatus.FRIEND]: '💚',
|
|
1813
|
+
[contact_types_js_1.FriendStatus.STRANGER]: '⚪',
|
|
1814
|
+
[contact_types_js_1.FriendStatus.PENDING]: '🟡',
|
|
1815
|
+
[contact_types_js_1.FriendStatus.BLOCKED]: '🔴',
|
|
1816
|
+
}[c.status];
|
|
1817
|
+
return `${statusIcon} **${c.name}**\n Peer: ${c.peerId.slice(0, 16)}...\n 信誉: ${c.reputation} | 状态: ${c.status}`;
|
|
1818
|
+
}).join('\n\n') || '暂无联系人'
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
case 'get': {
|
|
1822
|
+
let contact;
|
|
1823
|
+
if (params.contact_id) {
|
|
1824
|
+
contact = cm.getContact(params.contact_id);
|
|
1825
|
+
}
|
|
1826
|
+
else if (params.peer_id) {
|
|
1827
|
+
contact = cm.getContactByPeerId(params.peer_id);
|
|
1828
|
+
}
|
|
1829
|
+
else {
|
|
1830
|
+
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1831
|
+
}
|
|
1832
|
+
if (!contact) {
|
|
1833
|
+
return { content: '❌ 联系人不存在' };
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
content: `👤 **${contact.name}**\n` +
|
|
1837
|
+
` ID: ${contact.id}\n` +
|
|
1838
|
+
` Peer ID: ${contact.peerId}\n` +
|
|
1839
|
+
` 状态: ${contact.status}\n` +
|
|
1840
|
+
` 信誉: ${contact.reputation}\n` +
|
|
1841
|
+
` 分组: ${contact.groups.join(', ') || '无'}\n` +
|
|
1842
|
+
` 标签: ${contact.tags.join(', ') || '无'}\n` +
|
|
1843
|
+
` 最后通信: ${contact.lastCommunicationTime ? new Date(contact.lastCommunicationTime).toLocaleString() : '从未'}\n` +
|
|
1844
|
+
(contact.notes ? ` 备注: ${contact.notes}` : '')
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
case 'add': {
|
|
1848
|
+
if (!params.peer_id || !params.name) {
|
|
1849
|
+
return { content: '❌ 需要提供 peer_id 和 name' };
|
|
1850
|
+
}
|
|
1851
|
+
const contact = cm.addContact({
|
|
1852
|
+
name: params.name,
|
|
1853
|
+
peerId: params.peer_id,
|
|
1854
|
+
groups: params.groups,
|
|
1855
|
+
tags: params.tags,
|
|
1856
|
+
notes: params.notes,
|
|
1857
|
+
});
|
|
1858
|
+
return { content: `✅ 已添加联系人: ${contact.name} (${contact.peerId.slice(0, 16)})` };
|
|
1859
|
+
}
|
|
1860
|
+
case 'remove': {
|
|
1861
|
+
let contactId = params.contact_id;
|
|
1862
|
+
if (!contactId && params.peer_id) {
|
|
1863
|
+
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1864
|
+
contactId = contact?.id;
|
|
1865
|
+
}
|
|
1866
|
+
if (!contactId) {
|
|
1867
|
+
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1868
|
+
}
|
|
1869
|
+
const success = cm.removeContact(contactId);
|
|
1870
|
+
return { content: success ? '✅ 已删除联系人' : '❌ 联系人不存在' };
|
|
1871
|
+
}
|
|
1872
|
+
case 'update': {
|
|
1873
|
+
let contactId = params.contact_id;
|
|
1874
|
+
if (!contactId && params.peer_id) {
|
|
1875
|
+
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1876
|
+
contactId = contact?.id;
|
|
1877
|
+
}
|
|
1878
|
+
if (!contactId) {
|
|
1879
|
+
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1880
|
+
}
|
|
1881
|
+
const contact = cm.updateContact(contactId, {
|
|
1882
|
+
name: params.name,
|
|
1883
|
+
groups: params.groups,
|
|
1884
|
+
tags: params.tags,
|
|
1885
|
+
notes: params.notes,
|
|
1886
|
+
});
|
|
1887
|
+
return { content: contact ? `✅ 已更新联系人: ${contact.name}` : '❌ 联系人不存在' };
|
|
1888
|
+
}
|
|
1889
|
+
case 'block': {
|
|
1890
|
+
let contactId = params.contact_id;
|
|
1891
|
+
if (!contactId && params.peer_id) {
|
|
1892
|
+
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1893
|
+
contactId = contact?.id;
|
|
1894
|
+
}
|
|
1895
|
+
if (!contactId) {
|
|
1896
|
+
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1897
|
+
}
|
|
1898
|
+
const success = cm.blockContact(contactId);
|
|
1899
|
+
return { content: success ? '✅ 已拉黑联系人' : '❌ 联系人不存在' };
|
|
1900
|
+
}
|
|
1901
|
+
case 'unblock': {
|
|
1902
|
+
let contactId = params.contact_id;
|
|
1903
|
+
if (!contactId && params.peer_id) {
|
|
1904
|
+
const contact = cm.getContactByPeerId(params.peer_id);
|
|
1905
|
+
contactId = contact?.id;
|
|
1906
|
+
}
|
|
1907
|
+
if (!contactId) {
|
|
1908
|
+
return { content: '❌ 需要提供 contact_id 或 peer_id' };
|
|
1909
|
+
}
|
|
1910
|
+
const success = cm.unblockContact(contactId);
|
|
1911
|
+
return { content: success ? '✅ 已解除拉黑' : '❌ 联系人不存在' };
|
|
1912
|
+
}
|
|
1913
|
+
default:
|
|
1914
|
+
return { content: '❌ 未知操作' };
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
catch (err) {
|
|
1918
|
+
return { content: `❌ 操作失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* 处理分组管理工具
|
|
1923
|
+
*/
|
|
1924
|
+
async handleContactGroups(params, _context) {
|
|
1925
|
+
try {
|
|
1926
|
+
const cm = this.contactManager;
|
|
1927
|
+
switch (params.action) {
|
|
1928
|
+
case 'list': {
|
|
1929
|
+
const groups = cm.getGroups();
|
|
1930
|
+
return {
|
|
1931
|
+
content: `📁 **分组列表** (${groups.length} 个)\n\n` +
|
|
1932
|
+
groups.map(g => `• **${g.name}** (${g.id})\n ${g.description || '无描述'}`).join('\n\n')
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
case 'create': {
|
|
1936
|
+
if (!params.name) {
|
|
1937
|
+
return { content: '❌ 需要提供分组名称' };
|
|
1938
|
+
}
|
|
1939
|
+
const group = cm.createGroup({
|
|
1940
|
+
name: params.name,
|
|
1941
|
+
description: params.description,
|
|
1942
|
+
color: params.color,
|
|
1943
|
+
});
|
|
1944
|
+
return { content: `✅ 已创建分组: ${group.name}` };
|
|
1945
|
+
}
|
|
1946
|
+
case 'update': {
|
|
1947
|
+
if (!params.group_id) {
|
|
1948
|
+
return { content: '❌ 需要提供 group_id' };
|
|
1949
|
+
}
|
|
1950
|
+
const group = cm.updateGroup(params.group_id, {
|
|
1951
|
+
name: params.name,
|
|
1952
|
+
description: params.description,
|
|
1953
|
+
color: params.color,
|
|
1954
|
+
});
|
|
1955
|
+
return { content: group ? `✅ 已更新分组: ${group.name}` : '❌ 分组不存在' };
|
|
1956
|
+
}
|
|
1957
|
+
case 'delete': {
|
|
1958
|
+
if (!params.group_id) {
|
|
1959
|
+
return { content: '❌ 需要提供 group_id' };
|
|
1960
|
+
}
|
|
1961
|
+
const success = cm.deleteGroup(params.group_id);
|
|
1962
|
+
return { content: success ? '✅ 已删除分组' : '❌ 无法删除(分组不存在或为默认分组)' };
|
|
1963
|
+
}
|
|
1964
|
+
default:
|
|
1965
|
+
return { content: '❌ 未知操作' };
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
catch (err) {
|
|
1969
|
+
return { content: `❌ 操作失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* 处理好友请求工具
|
|
1974
|
+
*/
|
|
1975
|
+
async handleFriendRequest(params, _context) {
|
|
1976
|
+
try {
|
|
1977
|
+
if (!this._f2a) {
|
|
1978
|
+
return { content: '❌ F2A 实例未初始化' };
|
|
1979
|
+
}
|
|
1980
|
+
if (!this._contactManager) {
|
|
1981
|
+
// 确保联系人管理器已初始化
|
|
1982
|
+
this.contactManager;
|
|
1983
|
+
}
|
|
1984
|
+
if (!this._handshakeProtocol) {
|
|
1985
|
+
// 初始化握手协议
|
|
1986
|
+
this.handshakeProtocol;
|
|
1987
|
+
}
|
|
1988
|
+
if (!this._handshakeProtocol) {
|
|
1989
|
+
return { content: '❌ 握手协议未初始化' };
|
|
1990
|
+
}
|
|
1991
|
+
const requestId = await this._handshakeProtocol.sendFriendRequest(params.peer_id, params.message);
|
|
1992
|
+
if (requestId) {
|
|
1993
|
+
return { content: `✅ 好友请求已发送\n请求 ID: ${requestId}\n等待对方响应...` };
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
return { content: '❌ 发送好友请求失败' };
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
catch (err) {
|
|
2000
|
+
return { content: `❌ 发送失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* 处理待处理请求工具
|
|
2005
|
+
*/
|
|
2006
|
+
async handlePendingRequests(params, _context) {
|
|
2007
|
+
try {
|
|
2008
|
+
const cm = this.contactManager;
|
|
2009
|
+
switch (params.action) {
|
|
2010
|
+
case 'list': {
|
|
2011
|
+
const pending = cm.getPendingHandshakes();
|
|
2012
|
+
if (pending.length === 0) {
|
|
2013
|
+
return { content: '📭 暂无待处理的好友请求' };
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
content: `📬 **待处理的好友请求** (${pending.length} 个)\n\n` +
|
|
2017
|
+
pending.map(p => `• **${p.fromName}**\n Peer: ${p.from.slice(0, 16)}...\n 请求 ID: ${p.requestId}\n 收到: ${new Date(p.receivedAt).toLocaleString()}` +
|
|
2018
|
+
(p.message ? `\n 消息: ${p.message}` : '')).join('\n\n')
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
case 'accept': {
|
|
2022
|
+
if (!params.request_id) {
|
|
2023
|
+
return { content: '❌ 需要提供 request_id' };
|
|
2024
|
+
}
|
|
2025
|
+
if (!this._handshakeProtocol) {
|
|
2026
|
+
return { content: '❌ 握手协议未初始化' };
|
|
2027
|
+
}
|
|
2028
|
+
const success = await this._handshakeProtocol.acceptRequest(params.request_id);
|
|
2029
|
+
return { content: success ? '✅ 已接受好友请求,双方已成为好友' : '❌ 接受失败' };
|
|
2030
|
+
}
|
|
2031
|
+
case 'reject': {
|
|
2032
|
+
if (!params.request_id) {
|
|
2033
|
+
return { content: '❌ 需要提供 request_id' };
|
|
2034
|
+
}
|
|
2035
|
+
if (!this._handshakeProtocol) {
|
|
2036
|
+
return { content: '❌ 握手协议未初始化' };
|
|
2037
|
+
}
|
|
2038
|
+
const success = await this._handshakeProtocol.rejectRequest(params.request_id, params.reason);
|
|
2039
|
+
return { content: success ? '✅ 已拒绝好友请求' : '❌ 拒绝失败' };
|
|
2040
|
+
}
|
|
2041
|
+
default:
|
|
2042
|
+
return { content: '❌ 未知操作' };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
catch (err) {
|
|
2046
|
+
return { content: `❌ 操作失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* 处理导出通讯录工具
|
|
2051
|
+
*/
|
|
2052
|
+
async handleContactsExport(_params, _context) {
|
|
2053
|
+
try {
|
|
2054
|
+
const cm = this.contactManager;
|
|
2055
|
+
const data = cm.exportContacts(this._f2a?.peerId);
|
|
2056
|
+
return {
|
|
2057
|
+
content: `📤 **通讯录导出成功**\n\n` +
|
|
2058
|
+
`联系人: ${data.contacts.length} 个\n` +
|
|
2059
|
+
`分组: ${data.groups.length} 个\n` +
|
|
2060
|
+
`导出时间: ${new Date(data.exportedAt).toLocaleString()}\n\n` +
|
|
2061
|
+
'```json\n' + JSON.stringify(data, null, 2) + '\n```',
|
|
2062
|
+
data,
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
catch (err) {
|
|
2066
|
+
return { content: `❌ 导出失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* 处理导入通讯录工具
|
|
2071
|
+
*/
|
|
2072
|
+
async handleContactsImport(params, _context) {
|
|
2073
|
+
try {
|
|
2074
|
+
const cm = this.contactManager;
|
|
2075
|
+
const result = cm.importContacts(params.data, params.merge ?? true);
|
|
2076
|
+
if (result.success) {
|
|
2077
|
+
return {
|
|
2078
|
+
content: `📥 **通讯录导入完成**\n\n` +
|
|
2079
|
+
`✅ 导入联系人: ${result.importedContacts} 个\n` +
|
|
2080
|
+
`✅ 导入分组: ${result.importedGroups} 个\n` +
|
|
2081
|
+
`⏭️ 跳过联系人: ${result.skippedContacts} 个` +
|
|
2082
|
+
(result.errors.length ? `\n\n⚠️ 错误:\n${result.errors.join('\n')}` : '')
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
return {
|
|
2087
|
+
content: `❌ 导入失败\n\n错误:\n${result.errors.join('\n')}`
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
catch (err) {
|
|
2092
|
+
return { content: `❌ 导入失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
1309
2095
|
/**
|
|
1310
2096
|
* 关闭插件,清理资源
|
|
1311
2097
|
* 只清理已初始化的资源
|
|
@@ -1317,6 +2103,18 @@ class F2AOpenClawAdapter {
|
|
|
1317
2103
|
clearInterval(this.pollTimer);
|
|
1318
2104
|
this.pollTimer = undefined;
|
|
1319
2105
|
}
|
|
2106
|
+
// Issue #99: 关闭握手协议
|
|
2107
|
+
if (this._handshakeProtocol) {
|
|
2108
|
+
this._handshakeProtocol.shutdown();
|
|
2109
|
+
this._logger?.info('[F2A Adapter] HandshakeProtocol 已关闭');
|
|
2110
|
+
this._handshakeProtocol = undefined;
|
|
2111
|
+
}
|
|
2112
|
+
// Issue #98: 刷新通讯录数据
|
|
2113
|
+
if (this._contactManager) {
|
|
2114
|
+
this._contactManager.flush();
|
|
2115
|
+
this._logger?.info('[F2A Adapter] ContactManager 数据已保存');
|
|
2116
|
+
this._contactManager = undefined;
|
|
2117
|
+
}
|
|
1320
2118
|
// 停止 F2A 实例(新架构直接管理)
|
|
1321
2119
|
if (this._f2a) {
|
|
1322
2120
|
try {
|
|
@@ -1324,7 +2122,7 @@ class F2AOpenClawAdapter {
|
|
|
1324
2122
|
this._logger?.info('[F2A Adapter] F2A 实例已停止');
|
|
1325
2123
|
}
|
|
1326
2124
|
catch (err) {
|
|
1327
|
-
this._logger?.warn('[F2A Adapter] F2A 实例停止失败', { error:
|
|
2125
|
+
this._logger?.warn('[F2A Adapter] F2A 实例停止失败', { error: extractErrorMessage(err) });
|
|
1328
2126
|
}
|
|
1329
2127
|
this._f2a = undefined;
|
|
1330
2128
|
}
|