@f2a/openclaw-f2a 0.2.25 → 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 +42 -0
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js +499 -0
- 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/package.json +1 -1
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* F2A 联系人管理器
|
|
4
|
+
*
|
|
5
|
+
* 管理通讯录、分组、标签和握手请求
|
|
6
|
+
* 支持持久化存储和导入/导出功能
|
|
7
|
+
*
|
|
8
|
+
* @module contact-manager
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ContactManager = void 0;
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const contact_types_js_1 = require("./contact-types.js");
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 常量定义
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/** 通讯录数据版本 */
|
|
19
|
+
const CONTACTS_DATA_VERSION = 1;
|
|
20
|
+
/** 默认数据文件名 */
|
|
21
|
+
const DEFAULT_CONTACTS_FILE = 'contacts.json';
|
|
22
|
+
/** 默认分组 */
|
|
23
|
+
const DEFAULT_GROUPS = [
|
|
24
|
+
{
|
|
25
|
+
id: 'default',
|
|
26
|
+
name: '默认分组',
|
|
27
|
+
description: '默认联系人分组',
|
|
28
|
+
createdAt: Date.now(),
|
|
29
|
+
updatedAt: Date.now(),
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// 工具函数
|
|
34
|
+
// ============================================================================
|
|
35
|
+
/**
|
|
36
|
+
* 生成唯一 ID
|
|
37
|
+
*/
|
|
38
|
+
function generateId() {
|
|
39
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 深拷贝对象
|
|
43
|
+
*/
|
|
44
|
+
function deepClone(obj) {
|
|
45
|
+
return JSON.parse(JSON.stringify(obj));
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// ContactManager 类
|
|
49
|
+
// ============================================================================
|
|
50
|
+
/**
|
|
51
|
+
* F2A 联系人管理器
|
|
52
|
+
*
|
|
53
|
+
* 提供通讯录的完整管理功能:
|
|
54
|
+
* - 联系人 CRUD 操作
|
|
55
|
+
* - 分组和标签管理
|
|
56
|
+
* - 握手请求处理
|
|
57
|
+
* - 数据导入/导出
|
|
58
|
+
* - 持久化存储
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const manager = new ContactManager('/path/to/data', logger);
|
|
63
|
+
*
|
|
64
|
+
* // 添加联系人
|
|
65
|
+
* await manager.addContact({
|
|
66
|
+
* name: 'Alice',
|
|
67
|
+
* peerId: '12D3KooW...',
|
|
68
|
+
* capabilities: [{ name: 'code-generation' }],
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // 发送好友请求
|
|
72
|
+
* const request = manager.createHandshakeRequest('12D3KooW...', 'Bob');
|
|
73
|
+
*
|
|
74
|
+
* // 获取好友列表
|
|
75
|
+
* const friends = manager.getContactsByStatus(FriendStatus.FRIEND);
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
class ContactManager {
|
|
79
|
+
dataDir;
|
|
80
|
+
dataPath;
|
|
81
|
+
data;
|
|
82
|
+
logger;
|
|
83
|
+
eventHandlers = new Set();
|
|
84
|
+
autoSave = true;
|
|
85
|
+
/**
|
|
86
|
+
* 创建联系人管理器
|
|
87
|
+
*
|
|
88
|
+
* @param dataDir - 数据存储目录
|
|
89
|
+
* @param logger - 日志记录器
|
|
90
|
+
* @param options - 配置选项
|
|
91
|
+
*/
|
|
92
|
+
constructor(dataDir, logger, options) {
|
|
93
|
+
this.dataDir = dataDir;
|
|
94
|
+
this.logger = logger;
|
|
95
|
+
this.autoSave = options?.autoSave ?? true;
|
|
96
|
+
this.dataPath = (0, path_1.join)(dataDir, DEFAULT_CONTACTS_FILE);
|
|
97
|
+
// 确保目录存在
|
|
98
|
+
if (!(0, fs_1.existsSync)(dataDir)) {
|
|
99
|
+
(0, fs_1.mkdirSync)(dataDir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
// 加载或初始化数据
|
|
102
|
+
this.data = this.loadData();
|
|
103
|
+
this.logger?.info('[ContactManager] 初始化完成');
|
|
104
|
+
this.logger?.info(`[ContactManager] 已加载 ${this.data.contacts.length} 个联系人`);
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// 数据持久化
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* 加载数据
|
|
111
|
+
*/
|
|
112
|
+
loadData() {
|
|
113
|
+
try {
|
|
114
|
+
if ((0, fs_1.existsSync)(this.dataPath)) {
|
|
115
|
+
const content = (0, fs_1.readFileSync)(this.dataPath, 'utf-8');
|
|
116
|
+
const data = JSON.parse(content);
|
|
117
|
+
// 验证版本兼容性
|
|
118
|
+
if (data.version !== CONTACTS_DATA_VERSION) {
|
|
119
|
+
this.logger?.warn(`[ContactManager] 数据版本不匹配 (${data.version} vs ${CONTACTS_DATA_VERSION}),将迁移数据`);
|
|
120
|
+
return this.migrateData(data);
|
|
121
|
+
}
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
this.logger?.error(`[ContactManager] 加载数据失败: ${err}`);
|
|
127
|
+
}
|
|
128
|
+
// 返回默认数据
|
|
129
|
+
return this.createDefaultData();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 创建默认数据结构
|
|
133
|
+
*/
|
|
134
|
+
createDefaultData() {
|
|
135
|
+
return {
|
|
136
|
+
version: CONTACTS_DATA_VERSION,
|
|
137
|
+
contacts: [],
|
|
138
|
+
groups: deepClone(DEFAULT_GROUPS),
|
|
139
|
+
pendingHandshakes: [],
|
|
140
|
+
blockedPeers: [],
|
|
141
|
+
lastUpdated: Date.now(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 迁移旧版本数据
|
|
146
|
+
*/
|
|
147
|
+
migrateData(data) {
|
|
148
|
+
// 未来版本迁移逻辑
|
|
149
|
+
// 目前只有 v1,直接返回
|
|
150
|
+
return {
|
|
151
|
+
...data,
|
|
152
|
+
version: CONTACTS_DATA_VERSION,
|
|
153
|
+
groups: data.groups?.length ? data.groups : deepClone(DEFAULT_GROUPS),
|
|
154
|
+
pendingHandshakes: data.pendingHandshakes || [],
|
|
155
|
+
blockedPeers: data.blockedPeers || [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 保存数据
|
|
160
|
+
*/
|
|
161
|
+
saveData() {
|
|
162
|
+
if (!this.autoSave)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
this.data.lastUpdated = Date.now();
|
|
166
|
+
const content = JSON.stringify(this.data, null, 2);
|
|
167
|
+
(0, fs_1.writeFileSync)(this.dataPath, content, 'utf-8');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
this.logger?.error(`[ContactManager] 保存数据失败: ${err}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 手动触发保存
|
|
175
|
+
*/
|
|
176
|
+
flush() {
|
|
177
|
+
this.saveData();
|
|
178
|
+
}
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// 联系人管理
|
|
181
|
+
// ============================================================================
|
|
182
|
+
/**
|
|
183
|
+
* 添加联系人
|
|
184
|
+
*
|
|
185
|
+
* @param params - 创建参数
|
|
186
|
+
* @returns 新创建的联系人
|
|
187
|
+
*/
|
|
188
|
+
addContact(params) {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
// 检查是否已存在
|
|
191
|
+
const existing = this.getContactByPeerId(params.peerId);
|
|
192
|
+
if (existing) {
|
|
193
|
+
this.logger?.warn(`[ContactManager] 联系人已存在: ${params.peerId}`);
|
|
194
|
+
return existing;
|
|
195
|
+
}
|
|
196
|
+
const contact = {
|
|
197
|
+
id: generateId(),
|
|
198
|
+
name: params.name,
|
|
199
|
+
peerId: params.peerId,
|
|
200
|
+
agentId: params.agentId,
|
|
201
|
+
status: contact_types_js_1.FriendStatus.STRANGER,
|
|
202
|
+
capabilities: params.capabilities || [],
|
|
203
|
+
reputation: params.reputation ?? 0,
|
|
204
|
+
groups: params.groups || ['default'],
|
|
205
|
+
tags: params.tags || [],
|
|
206
|
+
lastCommunicationTime: 0,
|
|
207
|
+
createdAt: now,
|
|
208
|
+
updatedAt: now,
|
|
209
|
+
notes: params.notes,
|
|
210
|
+
multiaddrs: params.multiaddrs,
|
|
211
|
+
metadata: params.metadata,
|
|
212
|
+
};
|
|
213
|
+
this.data.contacts.push(contact);
|
|
214
|
+
this.saveData();
|
|
215
|
+
this.emitEvent('contact:added', contact);
|
|
216
|
+
this.logger?.info(`[ContactManager] 添加联系人: ${contact.name} (${contact.peerId.slice(0, 16)})`);
|
|
217
|
+
return contact;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 更新联系人
|
|
221
|
+
*
|
|
222
|
+
* @param contactId - 联系人 ID
|
|
223
|
+
* @param params - 更新参数
|
|
224
|
+
* @returns 更新后的联系人,如果不存在返回 null
|
|
225
|
+
*/
|
|
226
|
+
updateContact(contactId, params) {
|
|
227
|
+
const index = this.data.contacts.findIndex(c => c.id === contactId);
|
|
228
|
+
if (index === -1) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const contact = this.data.contacts[index];
|
|
232
|
+
// 应用更新
|
|
233
|
+
if (params.name !== undefined)
|
|
234
|
+
contact.name = params.name;
|
|
235
|
+
if (params.capabilities !== undefined)
|
|
236
|
+
contact.capabilities = params.capabilities;
|
|
237
|
+
if (params.reputation !== undefined)
|
|
238
|
+
contact.reputation = params.reputation;
|
|
239
|
+
if (params.status !== undefined)
|
|
240
|
+
contact.status = params.status;
|
|
241
|
+
if (params.groups !== undefined)
|
|
242
|
+
contact.groups = params.groups;
|
|
243
|
+
if (params.tags !== undefined)
|
|
244
|
+
contact.tags = params.tags;
|
|
245
|
+
if (params.notes !== undefined)
|
|
246
|
+
contact.notes = params.notes;
|
|
247
|
+
if (params.multiaddrs !== undefined)
|
|
248
|
+
contact.multiaddrs = params.multiaddrs;
|
|
249
|
+
if (params.metadata !== undefined)
|
|
250
|
+
contact.metadata = params.metadata;
|
|
251
|
+
if (params.updateLastCommunication) {
|
|
252
|
+
contact.lastCommunicationTime = Date.now();
|
|
253
|
+
}
|
|
254
|
+
contact.updatedAt = Date.now();
|
|
255
|
+
this.data.contacts[index] = contact;
|
|
256
|
+
this.saveData();
|
|
257
|
+
this.emitEvent('contact:updated', contact);
|
|
258
|
+
return contact;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 删除联系人
|
|
262
|
+
*
|
|
263
|
+
* @param contactId - 联系人 ID
|
|
264
|
+
* @returns 是否删除成功
|
|
265
|
+
*/
|
|
266
|
+
removeContact(contactId) {
|
|
267
|
+
const index = this.data.contacts.findIndex(c => c.id === contactId);
|
|
268
|
+
if (index === -1) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const [removed] = this.data.contacts.splice(index, 1);
|
|
272
|
+
this.saveData();
|
|
273
|
+
this.emitEvent('contact:removed', removed);
|
|
274
|
+
this.logger?.info(`[ContactManager] 删除联系人: ${removed.name} (${removed.peerId.slice(0, 16)})`);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 获取联系人
|
|
279
|
+
*
|
|
280
|
+
* @param contactId - 联系人 ID
|
|
281
|
+
* @returns 联系人信息,如果不存在返回 null
|
|
282
|
+
*/
|
|
283
|
+
getContact(contactId) {
|
|
284
|
+
return this.data.contacts.find(c => c.id === contactId) || null;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 通过 Peer ID 获取联系人
|
|
288
|
+
*
|
|
289
|
+
* @param peerId - Peer ID
|
|
290
|
+
* @returns 联系人信息,如果不存在返回 null
|
|
291
|
+
*/
|
|
292
|
+
getContactByPeerId(peerId) {
|
|
293
|
+
return this.data.contacts.find(c => c.peerId === peerId) || null;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 获取所有联系人
|
|
297
|
+
*
|
|
298
|
+
* @param filter - 过滤条件
|
|
299
|
+
* @param sort - 排序选项
|
|
300
|
+
* @returns 联系人列表
|
|
301
|
+
*/
|
|
302
|
+
getContacts(filter, sort) {
|
|
303
|
+
let result = [...this.data.contacts];
|
|
304
|
+
// 应用过滤器
|
|
305
|
+
if (filter) {
|
|
306
|
+
result = result.filter(c => {
|
|
307
|
+
// 按名称过滤
|
|
308
|
+
if (filter.name && !c.name.toLowerCase().includes(filter.name.toLowerCase())) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
// 按状态过滤
|
|
312
|
+
if (filter.status) {
|
|
313
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
314
|
+
if (!statuses.includes(c.status)) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// 按分组过滤
|
|
319
|
+
if (filter.group && !c.groups.includes(filter.group)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
// 按标签过滤
|
|
323
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
324
|
+
if (!filter.tags.some(tag => c.tags.includes(tag))) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// 按信誉分数过滤
|
|
329
|
+
if (filter.minReputation !== undefined && c.reputation < filter.minReputation) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (filter.maxReputation !== undefined && c.reputation > filter.maxReputation) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
// 按能力过滤
|
|
336
|
+
if (filter.capability) {
|
|
337
|
+
if (!c.capabilities.some(cap => cap.name === filter.capability)) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// 应用排序
|
|
345
|
+
if (sort) {
|
|
346
|
+
result.sort((a, b) => {
|
|
347
|
+
let valueA;
|
|
348
|
+
let valueB;
|
|
349
|
+
switch (sort.field) {
|
|
350
|
+
case 'name':
|
|
351
|
+
valueA = a.name.toLowerCase();
|
|
352
|
+
valueB = b.name.toLowerCase();
|
|
353
|
+
break;
|
|
354
|
+
case 'reputation':
|
|
355
|
+
valueA = a.reputation;
|
|
356
|
+
valueB = b.reputation;
|
|
357
|
+
break;
|
|
358
|
+
case 'lastCommunicationTime':
|
|
359
|
+
valueA = a.lastCommunicationTime;
|
|
360
|
+
valueB = b.lastCommunicationTime;
|
|
361
|
+
break;
|
|
362
|
+
case 'createdAt':
|
|
363
|
+
default:
|
|
364
|
+
valueA = a.createdAt;
|
|
365
|
+
valueB = b.createdAt;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
if (typeof valueA === 'string') {
|
|
369
|
+
return sort.order === 'asc'
|
|
370
|
+
? valueA.localeCompare(valueB)
|
|
371
|
+
: valueB.localeCompare(valueA);
|
|
372
|
+
}
|
|
373
|
+
return sort.order === 'asc' ? valueA - valueB : valueB - valueA;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* 按好友状态获取联系人
|
|
380
|
+
*/
|
|
381
|
+
getContactsByStatus(status) {
|
|
382
|
+
return this.getContacts({ status });
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 获取好友列表
|
|
386
|
+
*/
|
|
387
|
+
getFriends() {
|
|
388
|
+
return this.getContactsByStatus(contact_types_js_1.FriendStatus.FRIEND);
|
|
389
|
+
}
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// 分组管理
|
|
392
|
+
// ============================================================================
|
|
393
|
+
/**
|
|
394
|
+
* 创建分组
|
|
395
|
+
*/
|
|
396
|
+
createGroup(params) {
|
|
397
|
+
const now = Date.now();
|
|
398
|
+
const group = {
|
|
399
|
+
id: generateId(),
|
|
400
|
+
name: params.name,
|
|
401
|
+
description: params.description,
|
|
402
|
+
color: params.color,
|
|
403
|
+
createdAt: now,
|
|
404
|
+
updatedAt: now,
|
|
405
|
+
};
|
|
406
|
+
this.data.groups.push(group);
|
|
407
|
+
this.saveData();
|
|
408
|
+
this.emitEvent('group:created', group);
|
|
409
|
+
return group;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* 更新分组
|
|
413
|
+
*/
|
|
414
|
+
updateGroup(groupId, params) {
|
|
415
|
+
const group = this.data.groups.find(g => g.id === groupId);
|
|
416
|
+
if (!group) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
if (params.name !== undefined)
|
|
420
|
+
group.name = params.name;
|
|
421
|
+
if (params.description !== undefined)
|
|
422
|
+
group.description = params.description;
|
|
423
|
+
if (params.color !== undefined)
|
|
424
|
+
group.color = params.color;
|
|
425
|
+
group.updatedAt = Date.now();
|
|
426
|
+
this.saveData();
|
|
427
|
+
this.emitEvent('group:updated', group);
|
|
428
|
+
return group;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 删除分组
|
|
432
|
+
*/
|
|
433
|
+
deleteGroup(groupId) {
|
|
434
|
+
// 不允许删除默认分组
|
|
435
|
+
if (groupId === 'default') {
|
|
436
|
+
this.logger?.warn('[ContactManager] 不能删除默认分组');
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const index = this.data.groups.findIndex(g => g.id === groupId);
|
|
440
|
+
if (index === -1) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
// 将该分组下的联系人移到默认分组
|
|
444
|
+
for (const contact of this.data.contacts) {
|
|
445
|
+
const groupIndex = contact.groups.indexOf(groupId);
|
|
446
|
+
if (groupIndex !== -1) {
|
|
447
|
+
contact.groups.splice(groupIndex, 1);
|
|
448
|
+
if (contact.groups.length === 0) {
|
|
449
|
+
contact.groups.push('default');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const [removed] = this.data.groups.splice(index, 1);
|
|
454
|
+
this.saveData();
|
|
455
|
+
this.emitEvent('group:deleted', removed);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 获取所有分组
|
|
460
|
+
*/
|
|
461
|
+
getGroups() {
|
|
462
|
+
return [...this.data.groups];
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 获取分组
|
|
466
|
+
*/
|
|
467
|
+
getGroup(groupId) {
|
|
468
|
+
return this.data.groups.find(g => g.id === groupId) || null;
|
|
469
|
+
}
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// 标签管理
|
|
472
|
+
// ============================================================================
|
|
473
|
+
/**
|
|
474
|
+
* 获取所有标签
|
|
475
|
+
*/
|
|
476
|
+
getAllTags() {
|
|
477
|
+
const tags = new Set();
|
|
478
|
+
for (const contact of this.data.contacts) {
|
|
479
|
+
for (const tag of contact.tags) {
|
|
480
|
+
tags.add(tag);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return Array.from(tags).sort();
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* 为联系人添加标签
|
|
487
|
+
*/
|
|
488
|
+
addTag(contactId, tag) {
|
|
489
|
+
const contact = this.getContact(contactId);
|
|
490
|
+
if (!contact)
|
|
491
|
+
return false;
|
|
492
|
+
if (!contact.tags.includes(tag)) {
|
|
493
|
+
contact.tags.push(tag);
|
|
494
|
+
contact.updatedAt = Date.now();
|
|
495
|
+
this.saveData();
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 移除联系人的标签
|
|
501
|
+
*/
|
|
502
|
+
removeTag(contactId, tag) {
|
|
503
|
+
const contact = this.getContact(contactId);
|
|
504
|
+
if (!contact)
|
|
505
|
+
return false;
|
|
506
|
+
const index = contact.tags.indexOf(tag);
|
|
507
|
+
if (index !== -1) {
|
|
508
|
+
contact.tags.splice(index, 1);
|
|
509
|
+
contact.updatedAt = Date.now();
|
|
510
|
+
this.saveData();
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
// ============================================================================
|
|
515
|
+
// 握手请求管理
|
|
516
|
+
// ============================================================================
|
|
517
|
+
/**
|
|
518
|
+
* 创建握手请求
|
|
519
|
+
*/
|
|
520
|
+
createHandshakeRequest(toPeerId, fromName, capabilities, message) {
|
|
521
|
+
return {
|
|
522
|
+
requestId: generateId(),
|
|
523
|
+
from: '', // 调用方需要填充自己的 Peer ID
|
|
524
|
+
fromName,
|
|
525
|
+
capabilities,
|
|
526
|
+
timestamp: Date.now(),
|
|
527
|
+
message,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 添加待处理的握手请求
|
|
532
|
+
*/
|
|
533
|
+
addPendingHandshake(request) {
|
|
534
|
+
const pending = {
|
|
535
|
+
requestId: request.requestId,
|
|
536
|
+
from: request.from,
|
|
537
|
+
fromName: request.fromName,
|
|
538
|
+
capabilities: request.capabilities,
|
|
539
|
+
receivedAt: Date.now(),
|
|
540
|
+
message: request.message,
|
|
541
|
+
};
|
|
542
|
+
// 检查是否已存在来自同一 Peer 的请求
|
|
543
|
+
const existingIndex = this.data.pendingHandshakes.findIndex(p => p.from === request.from);
|
|
544
|
+
if (existingIndex !== -1) {
|
|
545
|
+
// 替换旧请求
|
|
546
|
+
this.data.pendingHandshakes[existingIndex] = pending;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
this.data.pendingHandshakes.push(pending);
|
|
550
|
+
}
|
|
551
|
+
this.saveData();
|
|
552
|
+
this.emitEvent('handshake:request', pending);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* 获取待处理的握手请求列表
|
|
556
|
+
*/
|
|
557
|
+
getPendingHandshakes() {
|
|
558
|
+
return [...this.data.pendingHandshakes];
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* 获取特定 Peer 的待处理请求
|
|
562
|
+
*/
|
|
563
|
+
getPendingHandshakeFrom(peerId) {
|
|
564
|
+
return this.data.pendingHandshakes.find(p => p.from === peerId) || null;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* 接受握手请求
|
|
568
|
+
*
|
|
569
|
+
* 这会:
|
|
570
|
+
* 1. 将请求方添加为好友
|
|
571
|
+
* 2. 从待处理列表中移除
|
|
572
|
+
* 3. 触发事件
|
|
573
|
+
*/
|
|
574
|
+
acceptHandshake(requestId, myName, myCapabilities) {
|
|
575
|
+
const index = this.data.pendingHandshakes.findIndex(p => p.requestId === requestId);
|
|
576
|
+
if (index === -1) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const pending = this.data.pendingHandshakes[index];
|
|
580
|
+
// 添加为好友
|
|
581
|
+
let contact = this.getContactByPeerId(pending.from);
|
|
582
|
+
if (contact) {
|
|
583
|
+
// 更新现有联系人
|
|
584
|
+
this.updateContact(contact.id, {
|
|
585
|
+
status: contact_types_js_1.FriendStatus.FRIEND,
|
|
586
|
+
capabilities: pending.capabilities,
|
|
587
|
+
name: pending.fromName,
|
|
588
|
+
updateLastCommunication: true,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
// 创建新联系人
|
|
593
|
+
contact = this.addContact({
|
|
594
|
+
name: pending.fromName,
|
|
595
|
+
peerId: pending.from,
|
|
596
|
+
capabilities: pending.capabilities,
|
|
597
|
+
groups: ['default'],
|
|
598
|
+
});
|
|
599
|
+
this.updateContact(contact.id, { status: contact_types_js_1.FriendStatus.FRIEND });
|
|
600
|
+
}
|
|
601
|
+
// 移除待处理请求
|
|
602
|
+
this.data.pendingHandshakes.splice(index, 1);
|
|
603
|
+
this.saveData();
|
|
604
|
+
const response = {
|
|
605
|
+
requestId,
|
|
606
|
+
from: '', // 调用方填充
|
|
607
|
+
accepted: true,
|
|
608
|
+
fromName: myName,
|
|
609
|
+
capabilities: myCapabilities,
|
|
610
|
+
timestamp: Date.now(),
|
|
611
|
+
};
|
|
612
|
+
this.emitEvent('handshake:accepted', { pending, response });
|
|
613
|
+
return response;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* 拒绝握手请求
|
|
617
|
+
*/
|
|
618
|
+
rejectHandshake(requestId, reason) {
|
|
619
|
+
const index = this.data.pendingHandshakes.findIndex(p => p.requestId === requestId);
|
|
620
|
+
if (index === -1) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const [pending] = this.data.pendingHandshakes.splice(index, 1);
|
|
624
|
+
this.saveData();
|
|
625
|
+
const response = {
|
|
626
|
+
requestId,
|
|
627
|
+
from: '', // 调用方填充
|
|
628
|
+
accepted: false,
|
|
629
|
+
timestamp: Date.now(),
|
|
630
|
+
reason,
|
|
631
|
+
};
|
|
632
|
+
this.emitEvent('handshake:rejected', { pending, response });
|
|
633
|
+
return response;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* 处理收到的握手响应
|
|
637
|
+
*
|
|
638
|
+
* 当对方接受我们的好友请求时调用
|
|
639
|
+
*/
|
|
640
|
+
handleHandshakeResponse(response) {
|
|
641
|
+
if (!response.accepted) {
|
|
642
|
+
this.logger?.info(`[ContactManager] 好友请求被拒绝: ${response.reason || '无原因'}`);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
// 添加为好友
|
|
646
|
+
let contact = this.getContactByPeerId(response.from);
|
|
647
|
+
if (contact) {
|
|
648
|
+
this.updateContact(contact.id, {
|
|
649
|
+
status: contact_types_js_1.FriendStatus.FRIEND,
|
|
650
|
+
name: response.fromName || contact.name,
|
|
651
|
+
capabilities: response.capabilities,
|
|
652
|
+
updateLastCommunication: true,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
contact = this.addContact({
|
|
657
|
+
name: response.fromName || 'Unknown',
|
|
658
|
+
peerId: response.from,
|
|
659
|
+
capabilities: response.capabilities || [],
|
|
660
|
+
groups: ['default'],
|
|
661
|
+
});
|
|
662
|
+
this.updateContact(contact.id, { status: contact_types_js_1.FriendStatus.FRIEND });
|
|
663
|
+
}
|
|
664
|
+
this.logger?.info(`[ContactManager] 好友请求已接受: ${contact.name}`);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
// ============================================================================
|
|
668
|
+
// 黑名单管理
|
|
669
|
+
// ============================================================================
|
|
670
|
+
/**
|
|
671
|
+
* 拉黑联系人
|
|
672
|
+
*/
|
|
673
|
+
blockContact(contactId) {
|
|
674
|
+
const contact = this.getContact(contactId);
|
|
675
|
+
if (!contact)
|
|
676
|
+
return false;
|
|
677
|
+
contact.status = contact_types_js_1.FriendStatus.BLOCKED;
|
|
678
|
+
contact.updatedAt = Date.now();
|
|
679
|
+
if (!this.data.blockedPeers.includes(contact.peerId)) {
|
|
680
|
+
this.data.blockedPeers.push(contact.peerId);
|
|
681
|
+
}
|
|
682
|
+
this.saveData();
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* 解除拉黑
|
|
687
|
+
*/
|
|
688
|
+
unblockContact(contactId) {
|
|
689
|
+
const contact = this.getContact(contactId);
|
|
690
|
+
if (!contact)
|
|
691
|
+
return false;
|
|
692
|
+
contact.status = contact_types_js_1.FriendStatus.STRANGER;
|
|
693
|
+
contact.updatedAt = Date.now();
|
|
694
|
+
const index = this.data.blockedPeers.indexOf(contact.peerId);
|
|
695
|
+
if (index !== -1) {
|
|
696
|
+
this.data.blockedPeers.splice(index, 1);
|
|
697
|
+
}
|
|
698
|
+
this.saveData();
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* 检查是否被拉黑
|
|
703
|
+
*/
|
|
704
|
+
isBlocked(peerId) {
|
|
705
|
+
return this.data.blockedPeers.includes(peerId);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* 检查是否为好友
|
|
709
|
+
*/
|
|
710
|
+
isFriend(peerId) {
|
|
711
|
+
const contact = this.getContactByPeerId(peerId);
|
|
712
|
+
return contact?.status === contact_types_js_1.FriendStatus.FRIEND;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* 检查是否可以发送消息(好友或陌生人都可以,但被拉黑不行)
|
|
716
|
+
*/
|
|
717
|
+
canSendMessage(peerId) {
|
|
718
|
+
const contact = this.getContactByPeerId(peerId);
|
|
719
|
+
if (!contact)
|
|
720
|
+
return true; // 陌生人可以发送
|
|
721
|
+
return contact.status !== contact_types_js_1.FriendStatus.BLOCKED;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 检查是否可以发送任务消息(只有好友可以)
|
|
725
|
+
*/
|
|
726
|
+
canSendTask(peerId) {
|
|
727
|
+
return this.isFriend(peerId);
|
|
728
|
+
}
|
|
729
|
+
// ============================================================================
|
|
730
|
+
// 导入/导出
|
|
731
|
+
// ============================================================================
|
|
732
|
+
/**
|
|
733
|
+
* 导出通讯录
|
|
734
|
+
*/
|
|
735
|
+
exportContacts(nodeId) {
|
|
736
|
+
return {
|
|
737
|
+
...deepClone(this.data),
|
|
738
|
+
exportedAt: Date.now(),
|
|
739
|
+
exportedBy: nodeId,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* 导入通讯录
|
|
744
|
+
*
|
|
745
|
+
* @param data - 导入的数据
|
|
746
|
+
* @param merge - 是否合并(true)或覆盖(false)
|
|
747
|
+
*/
|
|
748
|
+
importContacts(data, merge = true) {
|
|
749
|
+
const result = {
|
|
750
|
+
success: true,
|
|
751
|
+
importedContacts: 0,
|
|
752
|
+
importedGroups: 0,
|
|
753
|
+
skippedContacts: 0,
|
|
754
|
+
errors: [],
|
|
755
|
+
};
|
|
756
|
+
try {
|
|
757
|
+
if (!merge) {
|
|
758
|
+
// 覆盖模式
|
|
759
|
+
this.data = {
|
|
760
|
+
version: CONTACTS_DATA_VERSION,
|
|
761
|
+
contacts: data.contacts,
|
|
762
|
+
groups: data.groups.length ? data.groups : deepClone(DEFAULT_GROUPS),
|
|
763
|
+
pendingHandshakes: data.pendingHandshakes || [],
|
|
764
|
+
blockedPeers: data.blockedPeers || [],
|
|
765
|
+
lastUpdated: Date.now(),
|
|
766
|
+
};
|
|
767
|
+
result.importedContacts = data.contacts.length;
|
|
768
|
+
result.importedGroups = data.groups.length;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
// 合并模式
|
|
772
|
+
const existingPeerIds = new Set(this.data.contacts.map(c => c.peerId));
|
|
773
|
+
for (const contact of data.contacts) {
|
|
774
|
+
if (existingPeerIds.has(contact.peerId)) {
|
|
775
|
+
result.skippedContacts++;
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
this.data.contacts.push(contact);
|
|
779
|
+
result.importedContacts++;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const existingGroupIds = new Set(this.data.groups.map(g => g.id));
|
|
783
|
+
for (const group of data.groups) {
|
|
784
|
+
if (!existingGroupIds.has(group.id)) {
|
|
785
|
+
this.data.groups.push(group);
|
|
786
|
+
result.importedGroups++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// 合并黑名单
|
|
790
|
+
for (const peerId of data.blockedPeers || []) {
|
|
791
|
+
if (!this.data.blockedPeers.includes(peerId)) {
|
|
792
|
+
this.data.blockedPeers.push(peerId);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
this.saveData();
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
result.success = false;
|
|
800
|
+
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
801
|
+
}
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
// ============================================================================
|
|
805
|
+
// 事件处理
|
|
806
|
+
// ============================================================================
|
|
807
|
+
/**
|
|
808
|
+
* 添加事件处理器
|
|
809
|
+
*/
|
|
810
|
+
on(handler) {
|
|
811
|
+
this.eventHandlers.add(handler);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* 移除事件处理器
|
|
815
|
+
*/
|
|
816
|
+
off(handler) {
|
|
817
|
+
this.eventHandlers.delete(handler);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* 触发事件
|
|
821
|
+
*/
|
|
822
|
+
emitEvent(event, data) {
|
|
823
|
+
for (const handler of this.eventHandlers) {
|
|
824
|
+
try {
|
|
825
|
+
handler(event, data);
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
this.logger?.error(`[ContactManager] 事件处理器错误: ${err}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// ============================================================================
|
|
833
|
+
// 统计信息
|
|
834
|
+
// ============================================================================
|
|
835
|
+
/**
|
|
836
|
+
* 获取统计信息
|
|
837
|
+
*/
|
|
838
|
+
getStats() {
|
|
839
|
+
return {
|
|
840
|
+
total: this.data.contacts.length,
|
|
841
|
+
friends: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.FRIEND).length,
|
|
842
|
+
strangers: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.STRANGER).length,
|
|
843
|
+
pending: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.PENDING).length,
|
|
844
|
+
blocked: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.BLOCKED).length,
|
|
845
|
+
groups: this.data.groups.length,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
// ============================================================================
|
|
849
|
+
// 清理
|
|
850
|
+
// ============================================================================
|
|
851
|
+
/**
|
|
852
|
+
* 清空通讯录
|
|
853
|
+
*/
|
|
854
|
+
clear() {
|
|
855
|
+
this.data = this.createDefaultData();
|
|
856
|
+
this.saveData();
|
|
857
|
+
this.logger?.info('[ContactManager] 通讯录已清空');
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* 删除数据文件
|
|
861
|
+
*/
|
|
862
|
+
deleteData() {
|
|
863
|
+
if ((0, fs_1.existsSync)(this.dataPath)) {
|
|
864
|
+
(0, fs_1.unlinkSync)(this.dataPath);
|
|
865
|
+
}
|
|
866
|
+
this.data = this.createDefaultData();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
exports.ContactManager = ContactManager;
|
|
870
|
+
// 默认导出
|
|
871
|
+
exports.default = ContactManager;
|
|
872
|
+
//# sourceMappingURL=contact-manager.js.map
|