@f2a/openclaw-f2a 0.2.25 → 0.3.1

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.
@@ -0,0 +1,1186 @@
1
+ "use strict";
2
+ /**
3
+ * F2A 联系人管理器
4
+ *
5
+ * 管理通讯录、分组、标签和握手请求
6
+ * 支持持久化存储和导入/导出功能
7
+ *
8
+ * ⚠️ 并发安全说明
9
+ *
10
+ * ContactManager 不是线程安全的。在 Node.js 单线程事件循环环境下,
11
+ * 只要避免在同一个事件循环 tick 内发起多个修改操作,就是安全的。
12
+ *
13
+ * 如果需要在多进程/集群环境下使用,请使用外部锁服务(如 Redis)。
14
+ *
15
+ * @module contact-manager
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ContactManager = void 0;
19
+ const path_1 = require("path");
20
+ const fs_1 = require("fs");
21
+ const contact_types_js_1 = require("./contact-types.js");
22
+ // ============================================================================
23
+ // 常量定义
24
+ // ============================================================================
25
+ /** 通讯录数据版本 */
26
+ const CONTACTS_DATA_VERSION = 1;
27
+ /** 默认数据文件名 */
28
+ const DEFAULT_CONTACTS_FILE = 'contacts.json';
29
+ /** P1-4 修复:最大联系人数量限制 */
30
+ const MAX_CONTACTS = 10000;
31
+ /** P1-4 修复:导入数据最大大小(字节) */
32
+ const MAX_IMPORT_SIZE = 10 * 1024 * 1024; // 10MB
33
+ /** P2-1 修复:PeerID 格式正则(libp2p 格式:12D3KooW...) */
34
+ const PEER_ID_REGEX = /^12D3KooW[A-Za-z0-9]{44}$/;
35
+ /** P2-1 修复:名称最大长度 */
36
+ const MAX_NAME_LENGTH = 100;
37
+ /** 默认分组 */
38
+ const DEFAULT_GROUPS = [
39
+ {
40
+ id: 'default',
41
+ name: '默认分组',
42
+ description: '默认联系人分组',
43
+ createdAt: Date.now(),
44
+ updatedAt: Date.now(),
45
+ },
46
+ ];
47
+ // ============================================================================
48
+ // 工具函数
49
+ // ============================================================================
50
+ /**
51
+ * 生成唯一 ID(UUID v4 格式)
52
+ * P2-1 修复:使用加密安全的随机数生成器
53
+ */
54
+ function generateId() {
55
+ // 使用 crypto.randomUUID() 如果可用,否则回退到自定义实现
56
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
57
+ return crypto.randomUUID();
58
+ }
59
+ // 回退实现:基于时间戳 + 随机数 + 计数器
60
+ const timestamp = Date.now().toString(36);
61
+ const randomPart = Math.random().toString(36).slice(2, 11);
62
+ const counter = (generateId.counter = (generateId.counter || 0) + 1);
63
+ return `${timestamp}-${randomPart}-${counter.toString(36)}`;
64
+ }
65
+ // 静态计数器
66
+ (function (generateId) {
67
+ generateId.counter = 0;
68
+ })(generateId || (generateId = {}));
69
+ /**
70
+ * 深拷贝对象
71
+ * P1-1 修复:使用 structuredClone 支持更多类型
72
+ */
73
+ function deepClone(obj) {
74
+ // 优先使用 structuredClone(支持 Date、Map、Set 等)
75
+ if (typeof structuredClone === 'function') {
76
+ try {
77
+ return structuredClone(obj);
78
+ }
79
+ catch {
80
+ // 回退到 JSON 方法
81
+ }
82
+ }
83
+ // 回退:JSON 序列化(不支持 Date、undefined、循环引用)
84
+ return JSON.parse(JSON.stringify(obj));
85
+ }
86
+ // ============================================================================
87
+ // ContactManager 类
88
+ // ============================================================================
89
+ /**
90
+ * F2A 联系人管理器
91
+ *
92
+ * 提供通讯录的完整管理功能:
93
+ * - 联系人 CRUD 操作
94
+ * - 分组和标签管理
95
+ * - 握手请求处理
96
+ * - 数据导入/导出
97
+ * - 持久化存储
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const manager = new ContactManager('/path/to/data', logger);
102
+ *
103
+ * // 添加联系人
104
+ * await manager.addContact({
105
+ * name: 'Alice',
106
+ * peerId: '12D3KooW...',
107
+ * capabilities: [{ name: 'code-generation' }],
108
+ * });
109
+ *
110
+ * // 发送好友请求
111
+ * const request = manager.createHandshakeRequest('12D3KooW...', 'Bob');
112
+ *
113
+ * // 获取好友列表
114
+ * const friends = manager.getContactsByStatus(FriendStatus.FRIEND);
115
+ * ```
116
+ */
117
+ class ContactManager {
118
+ dataDir;
119
+ dataPath;
120
+ data;
121
+ logger;
122
+ eventHandlers = new Set();
123
+ autoSave = true;
124
+ /**
125
+ * P2-1 修复:验证 PeerID 格式
126
+ * libp2p 格式:12D3KooW + 44 个 base58 字符
127
+ */
128
+ validatePeerId(peerId) {
129
+ return PEER_ID_REGEX.test(peerId);
130
+ }
131
+ /**
132
+ * P2-1 修复:验证名称
133
+ * 限制长度,防止过长的名称
134
+ */
135
+ validateName(name) {
136
+ return typeof name === 'string' && name.length > 0 && name.length <= MAX_NAME_LENGTH;
137
+ }
138
+ /**
139
+ * P1-3 修复:验证联系人字段完整性
140
+ * 用于导入数据验证
141
+ */
142
+ validateContactFields(contact) {
143
+ if (!contact || typeof contact !== 'object')
144
+ return false;
145
+ const c = contact;
146
+ // 必须字段
147
+ if (typeof c.id !== 'string' || c.id.length === 0)
148
+ return false;
149
+ if (typeof c.name !== 'string' || !this.validateName(c.name))
150
+ return false;
151
+ if (typeof c.peerId !== 'string' || c.peerId.length === 0)
152
+ return false;
153
+ // 可选字段类型检查
154
+ if (c.agentId !== undefined && typeof c.agentId !== 'string')
155
+ return false;
156
+ if (c.status !== undefined && typeof c.status !== 'string')
157
+ return false;
158
+ if (c.reputation !== undefined && typeof c.reputation !== 'number')
159
+ return false;
160
+ if (c.notes !== undefined && typeof c.notes !== 'string')
161
+ return false;
162
+ if (c.createdAt !== undefined && typeof c.createdAt !== 'number')
163
+ return false;
164
+ if (c.updatedAt !== undefined && typeof c.updatedAt !== 'number')
165
+ return false;
166
+ if (c.lastCommunicationTime !== undefined && typeof c.lastCommunicationTime !== 'number')
167
+ return false;
168
+ // 数组字段检查
169
+ if (c.capabilities !== undefined && !Array.isArray(c.capabilities))
170
+ return false;
171
+ if (c.groups !== undefined && !Array.isArray(c.groups))
172
+ return false;
173
+ if (c.tags !== undefined && !Array.isArray(c.tags))
174
+ return false;
175
+ if (c.multiaddrs !== undefined && !Array.isArray(c.multiaddrs))
176
+ return false;
177
+ return true;
178
+ }
179
+ /**
180
+ * 创建联系人管理器
181
+ *
182
+ * @param dataDir - 数据存储目录
183
+ * @param logger - 日志记录器
184
+ * @param options - 配置选项
185
+ */
186
+ constructor(dataDir, logger, options) {
187
+ // P1-3 修复:验证 dataDir,防止路径遍历攻击
188
+ if (!dataDir || typeof dataDir !== 'string') {
189
+ throw new Error('[ContactManager] dataDir 必须是非空字符串');
190
+ }
191
+ // P1-3 修复:使用 path.resolve 和 path.normalize 规范化路径
192
+ // 解析为绝对路径,消除 .. 和 . 符号
193
+ const normalizedDataDir = (0, path_1.resolve)((0, path_1.normalize)(dataDir));
194
+ // 检查规范化后的路径是否仍在预期范围内
195
+ // 如果路径包含 ..,规范化后应该被消除
196
+ // 如果结果路径与原始路径差异过大,可能存在问题
197
+ const originalNormalized = (0, path_1.normalize)(dataDir);
198
+ if (originalNormalized !== normalizedDataDir && !dataDir.startsWith('/')) {
199
+ logger?.warn(`[ContactManager] 路径被规范化: ${dataDir} -> ${normalizedDataDir}`);
200
+ }
201
+ // 检查路径遍历(规范化后的路径不应包含 ..)
202
+ if (normalizedDataDir.includes('..')) {
203
+ throw new Error('[ContactManager] dataDir 路径无效(路径遍历风险)');
204
+ }
205
+ this.dataDir = normalizedDataDir;
206
+ this.logger = logger;
207
+ this.autoSave = options?.autoSave ?? true;
208
+ this.dataPath = (0, path_1.join)(normalizedDataDir, DEFAULT_CONTACTS_FILE);
209
+ // 确保目录存在
210
+ if (!(0, fs_1.existsSync)(normalizedDataDir)) {
211
+ (0, fs_1.mkdirSync)(normalizedDataDir, { recursive: true });
212
+ }
213
+ // 加载或初始化数据
214
+ this.data = this.loadData();
215
+ this.logger?.info('[ContactManager] 初始化完成');
216
+ this.logger?.info(`[ContactManager] 已加载 ${this.data.contacts.length} 个联系人`);
217
+ }
218
+ // ============================================================================
219
+ // 数据持久化
220
+ // ============================================================================
221
+ /**
222
+ * 加载数据
223
+ */
224
+ loadData() {
225
+ try {
226
+ if ((0, fs_1.existsSync)(this.dataPath)) {
227
+ const content = (0, fs_1.readFileSync)(this.dataPath, 'utf-8');
228
+ const data = JSON.parse(content);
229
+ // 验证版本兼容性
230
+ if (data.version !== CONTACTS_DATA_VERSION) {
231
+ this.logger?.warn(`[ContactManager] 数据版本不匹配 (${data.version} vs ${CONTACTS_DATA_VERSION}),将迁移数据`);
232
+ return this.migrateData(data);
233
+ }
234
+ return data;
235
+ }
236
+ }
237
+ catch (err) {
238
+ this.logger?.error(`[ContactManager] 加载数据失败: ${err}`);
239
+ }
240
+ // 返回默认数据
241
+ return this.createDefaultData();
242
+ }
243
+ /**
244
+ * 创建默认数据结构
245
+ */
246
+ createDefaultData() {
247
+ return {
248
+ version: CONTACTS_DATA_VERSION,
249
+ contacts: [],
250
+ groups: deepClone(DEFAULT_GROUPS),
251
+ pendingHandshakes: [],
252
+ blockedPeers: [],
253
+ lastUpdated: Date.now(),
254
+ };
255
+ }
256
+ /**
257
+ * 迁移旧版本数据
258
+ */
259
+ migrateData(data) {
260
+ // 未来版本迁移逻辑
261
+ // 目前只有 v1,直接返回
262
+ return {
263
+ ...data,
264
+ version: CONTACTS_DATA_VERSION,
265
+ groups: data.groups?.length ? data.groups : deepClone(DEFAULT_GROUPS),
266
+ pendingHandshakes: data.pendingHandshakes || [],
267
+ blockedPeers: data.blockedPeers || [],
268
+ };
269
+ }
270
+ /**
271
+ * 保存数据
272
+ * P1-2 修复:返回保存结果,不再静默忽略错误
273
+ * @returns 是否保存成功
274
+ */
275
+ saveData() {
276
+ if (!this.autoSave)
277
+ return true;
278
+ try {
279
+ this.data.lastUpdated = Date.now();
280
+ const content = JSON.stringify(this.data, null, 2);
281
+ (0, fs_1.writeFileSync)(this.dataPath, content, 'utf-8');
282
+ return true;
283
+ }
284
+ catch (err) {
285
+ this.logger?.error(`[ContactManager] 保存数据失败: ${err}`);
286
+ return false;
287
+ }
288
+ }
289
+ /**
290
+ * 手动触发保存
291
+ */
292
+ flush() {
293
+ this.saveData();
294
+ }
295
+ // ============================================================================
296
+ // 联系人管理
297
+ // ============================================================================
298
+ /**
299
+ * 添加联系人
300
+ * P2-1 修复:添加输入验证
301
+ * P1 修复:支持传入 status 参数
302
+ *
303
+ * @param params - 创建参数
304
+ * @returns 新创建的联系人,如果保存失败返回 null
305
+ */
306
+ addContact(params) {
307
+ // P2-1 修复:验证输入
308
+ if (!this.validateName(params.name)) {
309
+ this.logger?.error('[ContactManager] 添加联系人失败:名称无效或过长');
310
+ return null;
311
+ }
312
+ // P1-4 修复:PeerID 验证失败时拒绝添加联系人
313
+ if (!this.validatePeerId(params.peerId)) {
314
+ this.logger?.error(`[ContactManager] 添加联系人失败:PeerID 格式无效: ${params.peerId.slice(0, 16)}...`);
315
+ return null;
316
+ }
317
+ // P1-1 修复:检查联系人数量限制
318
+ if (this.data.contacts.length >= MAX_CONTACTS) {
319
+ this.logger?.error(`[ContactManager] 联系人数量已达上限 (${MAX_CONTACTS})`);
320
+ return null;
321
+ }
322
+ const now = Date.now();
323
+ // 检查是否已存在
324
+ const existing = this.getContactByPeerId(params.peerId);
325
+ if (existing) {
326
+ this.logger?.warn(`[ContactManager] 联系人已存在: ${params.peerId}`);
327
+ return existing;
328
+ }
329
+ const contact = {
330
+ id: generateId(),
331
+ name: params.name,
332
+ peerId: params.peerId,
333
+ agentId: params.agentId,
334
+ status: params.status ?? contact_types_js_1.FriendStatus.STRANGER, // P1 修复:支持传入 status,默认为 STRANGER
335
+ capabilities: params.capabilities || [],
336
+ reputation: params.reputation ?? 0,
337
+ groups: params.groups || ['default'],
338
+ tags: params.tags || [],
339
+ lastCommunicationTime: 0,
340
+ createdAt: now,
341
+ updatedAt: now,
342
+ notes: params.notes,
343
+ multiaddrs: params.multiaddrs,
344
+ metadata: params.metadata,
345
+ };
346
+ this.data.contacts.push(contact);
347
+ // P1-2 修复:检查保存结果
348
+ if (!this.saveData()) {
349
+ // 保存失败,回滚
350
+ this.data.contacts.pop();
351
+ this.logger?.error('[ContactManager] 添加联系人失败:数据保存失败');
352
+ return null;
353
+ }
354
+ this.emitEvent('contact:added', contact);
355
+ this.logger?.info(`[ContactManager] 添加联系人: ${contact.name} (${contact.peerId.slice(0, 16)})`);
356
+ return contact;
357
+ }
358
+ /**
359
+ * 更新联系人
360
+ * P2-1 修复:添加输入验证
361
+ *
362
+ * @param contactId - 联系人 ID
363
+ * @param params - 更新参数
364
+ * @returns 更新后的联系人,如果不存在或保存失败返回 null
365
+ */
366
+ updateContact(contactId, params) {
367
+ const index = this.data.contacts.findIndex(c => c.id === contactId);
368
+ if (index === -1) {
369
+ return null;
370
+ }
371
+ const contact = this.data.contacts[index];
372
+ const originalContact = deepClone(contact); // P1 修复:使用深拷贝备份用于回滚
373
+ // P2-1 修复:验证名称
374
+ if (params.name !== undefined && !this.validateName(params.name)) {
375
+ this.logger?.error('[ContactManager] 更新联系人失败:名称无效或过长');
376
+ return null;
377
+ }
378
+ // 应用更新
379
+ if (params.name !== undefined)
380
+ contact.name = params.name;
381
+ if (params.capabilities !== undefined)
382
+ contact.capabilities = params.capabilities;
383
+ if (params.reputation !== undefined)
384
+ contact.reputation = params.reputation;
385
+ if (params.status !== undefined)
386
+ contact.status = params.status;
387
+ if (params.groups !== undefined)
388
+ contact.groups = params.groups;
389
+ if (params.tags !== undefined)
390
+ contact.tags = params.tags;
391
+ if (params.notes !== undefined)
392
+ contact.notes = params.notes;
393
+ if (params.multiaddrs !== undefined)
394
+ contact.multiaddrs = params.multiaddrs;
395
+ if (params.metadata !== undefined)
396
+ contact.metadata = params.metadata;
397
+ if (params.updateLastCommunication) {
398
+ contact.lastCommunicationTime = Date.now();
399
+ }
400
+ contact.updatedAt = Date.now();
401
+ this.data.contacts[index] = contact;
402
+ // P1-2 修复:检查保存结果
403
+ if (!this.saveData()) {
404
+ // 保存失败,回滚
405
+ this.data.contacts[index] = originalContact;
406
+ this.logger?.error('[ContactManager] 更新联系人失败:数据保存失败');
407
+ return null;
408
+ }
409
+ this.emitEvent('contact:updated', contact);
410
+ return contact;
411
+ }
412
+ /**
413
+ * 删除联系人
414
+ * P1-2 修复:添加保存检查和回滚
415
+ *
416
+ * @param contactId - 联系人 ID
417
+ * @returns 是否删除成功
418
+ */
419
+ removeContact(contactId) {
420
+ const index = this.data.contacts.findIndex(c => c.id === contactId);
421
+ if (index === -1) {
422
+ return false;
423
+ }
424
+ const [removed] = this.data.contacts.splice(index, 1);
425
+ // P1-2 修复:检查保存结果,失败时恢复联系人
426
+ if (!this.saveData()) {
427
+ // 保存失败,恢复联系人
428
+ this.data.contacts.splice(index, 0, removed);
429
+ this.logger?.error('[ContactManager] 删除联系人失败:数据保存失败');
430
+ return false;
431
+ }
432
+ this.emitEvent('contact:removed', removed);
433
+ this.logger?.info(`[ContactManager] 删除联系人: ${removed.name} (${removed.peerId.slice(0, 16)})`);
434
+ return true;
435
+ }
436
+ /**
437
+ * 获取联系人
438
+ *
439
+ * @param contactId - 联系人 ID
440
+ * @returns 联系人信息,如果不存在返回 null
441
+ */
442
+ getContact(contactId) {
443
+ return this.data.contacts.find(c => c.id === contactId) || null;
444
+ }
445
+ /**
446
+ * 通过 Peer ID 获取联系人
447
+ *
448
+ * @param peerId - Peer ID
449
+ * @returns 联系人信息,如果不存在返回 null
450
+ */
451
+ getContactByPeerId(peerId) {
452
+ return this.data.contacts.find(c => c.peerId === peerId) || null;
453
+ }
454
+ /**
455
+ * 获取所有联系人
456
+ *
457
+ * @param filter - 过滤条件
458
+ * @param sort - 排序选项
459
+ * @returns 联系人列表
460
+ */
461
+ getContacts(filter, sort) {
462
+ let result = [...this.data.contacts];
463
+ // 应用过滤器
464
+ if (filter) {
465
+ result = result.filter(c => {
466
+ // 按名称过滤
467
+ if (filter.name && !c.name.toLowerCase().includes(filter.name.toLowerCase())) {
468
+ return false;
469
+ }
470
+ // 按状态过滤
471
+ if (filter.status) {
472
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
473
+ if (!statuses.includes(c.status)) {
474
+ return false;
475
+ }
476
+ }
477
+ // 按分组过滤
478
+ if (filter.group && !c.groups.includes(filter.group)) {
479
+ return false;
480
+ }
481
+ // 按标签过滤
482
+ if (filter.tags && filter.tags.length > 0) {
483
+ if (!filter.tags.some(tag => c.tags.includes(tag))) {
484
+ return false;
485
+ }
486
+ }
487
+ // 按信誉分数过滤
488
+ if (filter.minReputation !== undefined && c.reputation < filter.minReputation) {
489
+ return false;
490
+ }
491
+ if (filter.maxReputation !== undefined && c.reputation > filter.maxReputation) {
492
+ return false;
493
+ }
494
+ // 按能力过滤
495
+ if (filter.capability) {
496
+ if (!c.capabilities.some(cap => cap.name === filter.capability)) {
497
+ return false;
498
+ }
499
+ }
500
+ return true;
501
+ });
502
+ }
503
+ // 应用排序
504
+ if (sort) {
505
+ result.sort((a, b) => {
506
+ let valueA;
507
+ let valueB;
508
+ switch (sort.field) {
509
+ case 'name':
510
+ valueA = a.name.toLowerCase();
511
+ valueB = b.name.toLowerCase();
512
+ break;
513
+ case 'reputation':
514
+ valueA = a.reputation;
515
+ valueB = b.reputation;
516
+ break;
517
+ case 'lastCommunicationTime':
518
+ valueA = a.lastCommunicationTime;
519
+ valueB = b.lastCommunicationTime;
520
+ break;
521
+ case 'createdAt':
522
+ default:
523
+ valueA = a.createdAt;
524
+ valueB = b.createdAt;
525
+ break;
526
+ }
527
+ if (typeof valueA === 'string') {
528
+ return sort.order === 'asc'
529
+ ? valueA.localeCompare(valueB)
530
+ : valueB.localeCompare(valueA);
531
+ }
532
+ return sort.order === 'asc' ? valueA - valueB : valueB - valueA;
533
+ });
534
+ }
535
+ return result;
536
+ }
537
+ /**
538
+ * 按好友状态获取联系人
539
+ */
540
+ getContactsByStatus(status) {
541
+ return this.getContacts({ status });
542
+ }
543
+ /**
544
+ * 获取好友列表
545
+ */
546
+ getFriends() {
547
+ return this.getContactsByStatus(contact_types_js_1.FriendStatus.FRIEND);
548
+ }
549
+ // ============================================================================
550
+ // 分组管理
551
+ // ============================================================================
552
+ /**
553
+ * 创建分组
554
+ * P1-2 修复:添加保存检查和回滚
555
+ */
556
+ createGroup(params) {
557
+ const now = Date.now();
558
+ const group = {
559
+ id: generateId(),
560
+ name: params.name,
561
+ description: params.description,
562
+ color: params.color,
563
+ createdAt: now,
564
+ updatedAt: now,
565
+ };
566
+ this.data.groups.push(group);
567
+ // P1-2 修复:检查保存结果,失败时恢复
568
+ if (!this.saveData()) {
569
+ // 保存失败,回滚
570
+ this.data.groups.pop();
571
+ this.logger?.error('[ContactManager] 创建分组失败:数据保存失败');
572
+ return null;
573
+ }
574
+ this.emitEvent('group:created', group);
575
+ return group;
576
+ }
577
+ /**
578
+ * 更新分组
579
+ * P1-2 修复:添加保存检查和回滚
580
+ */
581
+ updateGroup(groupId, params) {
582
+ const group = this.data.groups.find(g => g.id === groupId);
583
+ if (!group) {
584
+ return null;
585
+ }
586
+ // 备份原始数据用于回滚
587
+ const originalGroup = { ...group };
588
+ if (params.name !== undefined)
589
+ group.name = params.name;
590
+ if (params.description !== undefined)
591
+ group.description = params.description;
592
+ if (params.color !== undefined)
593
+ group.color = params.color;
594
+ group.updatedAt = Date.now();
595
+ // P1-2 修复:检查保存结果,失败时恢复
596
+ if (!this.saveData()) {
597
+ // 保存失败,回滚
598
+ Object.assign(group, originalGroup);
599
+ this.logger?.error('[ContactManager] 更新分组失败:数据保存失败');
600
+ return null;
601
+ }
602
+ this.emitEvent('group:updated', group);
603
+ return group;
604
+ }
605
+ /**
606
+ * 删除分组
607
+ * P1-2 修复:添加保存检查和回滚
608
+ */
609
+ deleteGroup(groupId) {
610
+ // 不允许删除默认分组
611
+ if (groupId === 'default') {
612
+ this.logger?.warn('[ContactManager] 不能删除默认分组');
613
+ return false;
614
+ }
615
+ const index = this.data.groups.findIndex(g => g.id === groupId);
616
+ if (index === -1) {
617
+ return false;
618
+ }
619
+ // 备份受影响的联系人分组信息用于回滚
620
+ const affectedContacts = this.data.contacts
621
+ .filter(c => c.groups.includes(groupId))
622
+ .map(c => ({ contact: c, originalGroups: [...c.groups] }));
623
+ // 将该分组下的联系人移到默认分组
624
+ for (const contact of this.data.contacts) {
625
+ const groupIndex = contact.groups.indexOf(groupId);
626
+ if (groupIndex !== -1) {
627
+ contact.groups.splice(groupIndex, 1);
628
+ if (contact.groups.length === 0) {
629
+ contact.groups.push('default');
630
+ }
631
+ }
632
+ }
633
+ const [removed] = this.data.groups.splice(index, 1);
634
+ // P1-2 修复:检查保存结果,失败时恢复
635
+ if (!this.saveData()) {
636
+ // 保存失败,回滚
637
+ this.data.groups.splice(index, 0, removed);
638
+ for (const { contact, originalGroups } of affectedContacts) {
639
+ contact.groups = originalGroups;
640
+ }
641
+ this.logger?.error('[ContactManager] 删除分组失败:数据保存失败');
642
+ return false;
643
+ }
644
+ this.emitEvent('group:deleted', removed);
645
+ return true;
646
+ }
647
+ /**
648
+ * 获取所有分组
649
+ */
650
+ getGroups() {
651
+ return [...this.data.groups];
652
+ }
653
+ /**
654
+ * 获取分组
655
+ */
656
+ getGroup(groupId) {
657
+ return this.data.groups.find(g => g.id === groupId) || null;
658
+ }
659
+ // ============================================================================
660
+ // 标签管理
661
+ // ============================================================================
662
+ /**
663
+ * 获取所有标签
664
+ */
665
+ getAllTags() {
666
+ const tags = new Set();
667
+ for (const contact of this.data.contacts) {
668
+ for (const tag of contact.tags) {
669
+ tags.add(tag);
670
+ }
671
+ }
672
+ return Array.from(tags).sort();
673
+ }
674
+ /**
675
+ * 为联系人添加标签
676
+ */
677
+ addTag(contactId, tag) {
678
+ const contact = this.getContact(contactId);
679
+ if (!contact)
680
+ return false;
681
+ if (!contact.tags.includes(tag)) {
682
+ contact.tags.push(tag);
683
+ contact.updatedAt = Date.now();
684
+ this.saveData();
685
+ }
686
+ return true;
687
+ }
688
+ /**
689
+ * 移除联系人的标签
690
+ */
691
+ removeTag(contactId, tag) {
692
+ const contact = this.getContact(contactId);
693
+ if (!contact)
694
+ return false;
695
+ const index = contact.tags.indexOf(tag);
696
+ if (index !== -1) {
697
+ contact.tags.splice(index, 1);
698
+ contact.updatedAt = Date.now();
699
+ this.saveData();
700
+ }
701
+ return true;
702
+ }
703
+ // ============================================================================
704
+ // 握手请求管理
705
+ // ============================================================================
706
+ /**
707
+ * 创建握手请求
708
+ */
709
+ createHandshakeRequest(toPeerId, fromName, capabilities, message) {
710
+ return {
711
+ requestId: generateId(),
712
+ from: '', // 调用方需要填充自己的 Peer ID
713
+ fromName,
714
+ capabilities,
715
+ timestamp: Date.now(),
716
+ message,
717
+ };
718
+ }
719
+ /**
720
+ * 添加待处理的握手请求
721
+ */
722
+ addPendingHandshake(request) {
723
+ const pending = {
724
+ requestId: request.requestId,
725
+ from: request.from,
726
+ fromName: request.fromName,
727
+ capabilities: request.capabilities,
728
+ receivedAt: Date.now(),
729
+ message: request.message,
730
+ };
731
+ // 检查是否已存在来自同一 Peer 的请求
732
+ const existingIndex = this.data.pendingHandshakes.findIndex(p => p.from === request.from);
733
+ if (existingIndex !== -1) {
734
+ // 替换旧请求
735
+ this.data.pendingHandshakes[existingIndex] = pending;
736
+ }
737
+ else {
738
+ this.data.pendingHandshakes.push(pending);
739
+ }
740
+ this.saveData();
741
+ this.emitEvent('handshake:request', pending);
742
+ }
743
+ /**
744
+ * 获取待处理的握手请求列表
745
+ */
746
+ getPendingHandshakes() {
747
+ return [...this.data.pendingHandshakes];
748
+ }
749
+ /**
750
+ * 获取特定 Peer 的待处理请求
751
+ */
752
+ getPendingHandshakeFrom(peerId) {
753
+ return this.data.pendingHandshakes.find(p => p.from === peerId) || null;
754
+ }
755
+ /**
756
+ * 接受握手请求
757
+ * P1-2 修复:在移除 pendingHandshakes 前检查 saveData
758
+ *
759
+ * 这会:
760
+ * 1. 将请求方添加为好友
761
+ * 2. 从待处理列表中移除(仅在保存成功后)
762
+ * 3. 触发事件
763
+ *
764
+ * @returns 包含响应和对方 Peer ID 的对象,或 null
765
+ */
766
+ acceptHandshake(requestId, myName, myCapabilities) {
767
+ const index = this.data.pendingHandshakes.findIndex(p => p.requestId === requestId);
768
+ if (index === -1) {
769
+ return null;
770
+ }
771
+ const pending = this.data.pendingHandshakes[index];
772
+ // 添加为好友
773
+ let contact = this.getContactByPeerId(pending.from);
774
+ if (contact) {
775
+ // 更新现有联系人
776
+ const updated = this.updateContact(contact.id, {
777
+ status: contact_types_js_1.FriendStatus.FRIEND,
778
+ capabilities: pending.capabilities,
779
+ name: pending.fromName,
780
+ updateLastCommunication: true,
781
+ });
782
+ // P1-2 修复:检查更新是否成功
783
+ if (!updated) {
784
+ this.logger?.error('[ContactManager] 接受握手失败:更新联系人失败');
785
+ return null;
786
+ }
787
+ contact = updated;
788
+ }
789
+ else {
790
+ // P1 修复:创建新联系人时直接设置 status 为 FRIEND,避免状态不一致
791
+ contact = this.addContact({
792
+ name: pending.fromName,
793
+ peerId: pending.from,
794
+ capabilities: pending.capabilities,
795
+ groups: ['default'],
796
+ status: contact_types_js_1.FriendStatus.FRIEND, // 直接设置为好友
797
+ });
798
+ // 检查添加是否成功
799
+ if (!contact) {
800
+ this.logger?.error('[ContactManager] 接受握手失败:添加联系人失败');
801
+ return null;
802
+ }
803
+ // 不再需要额外调用 updateContact 设置状态
804
+ }
805
+ // P1-2 修复:先保存数据,成功后再移除待处理请求
806
+ // 这样如果保存失败,待处理请求仍然存在,可以重试
807
+ if (!this.saveData()) {
808
+ this.logger?.error('[ContactManager] 接受握手失败:保存数据失败');
809
+ // 不移除 pendingHandshakes,允许重试
810
+ return null;
811
+ }
812
+ // 保存成功,移除待处理请求
813
+ this.data.pendingHandshakes.splice(index, 1);
814
+ // 再次保存以记录 pendingHandshakes 的移除
815
+ if (!this.saveData()) {
816
+ this.logger?.warn('[ContactManager] 移除待处理请求后保存失败,但好友已添加');
817
+ // 继续执行,因为好友已经添加成功
818
+ }
819
+ const response = {
820
+ requestId,
821
+ from: '', // 调用方填充
822
+ accepted: true,
823
+ fromName: myName,
824
+ capabilities: myCapabilities,
825
+ timestamp: Date.now(),
826
+ };
827
+ this.emitEvent('handshake:accepted', { pending, response });
828
+ return { response, fromPeerId: pending.from };
829
+ }
830
+ /**
831
+ * 拒绝握手请求
832
+ * P1-2 修复:在移除 pendingHandshakes 前检查 saveData
833
+ *
834
+ * @returns 包含响应和对方 Peer ID 的对象,或 null
835
+ */
836
+ rejectHandshake(requestId, reason) {
837
+ const index = this.data.pendingHandshakes.findIndex(p => p.requestId === requestId);
838
+ if (index === -1) {
839
+ return null;
840
+ }
841
+ const pending = this.data.pendingHandshakes[index];
842
+ // P1-2 修复:先保存数据,确保状态持久化
843
+ if (!this.saveData()) {
844
+ this.logger?.error('[ContactManager] 拒绝握手失败:保存数据失败');
845
+ return null;
846
+ }
847
+ // 保存成功后,移除待处理请求
848
+ this.data.pendingHandshakes.splice(index, 1);
849
+ // 再次保存以记录 pendingHandshakes 的移除
850
+ if (!this.saveData()) {
851
+ this.logger?.warn('[ContactManager] 移除待处理请求后保存失败');
852
+ // 继续执行,因为主要操作已完成
853
+ }
854
+ const response = {
855
+ requestId,
856
+ from: '', // 调用方填充
857
+ accepted: false,
858
+ timestamp: Date.now(),
859
+ reason,
860
+ };
861
+ this.emitEvent('handshake:rejected', { pending, response });
862
+ return { response, fromPeerId: pending.from };
863
+ }
864
+ /**
865
+ * 处理收到的握手响应
866
+ * P1 修复:添加 PeerID 验证,优化状态设置
867
+ *
868
+ * 当对方接受我们的好友请求时调用
869
+ */
870
+ handleHandshakeResponse(response) {
871
+ if (!response.accepted) {
872
+ this.logger?.info(`[ContactManager] 好友请求被拒绝: ${response.reason || '无原因'}`);
873
+ return false;
874
+ }
875
+ // P1 修复:验证 PeerID 格式
876
+ if (!this.validatePeerId(response.from)) {
877
+ this.logger?.warn(`[ContactManager] 无效的 PeerID 格式: ${response.from.slice(0, 16)}...`);
878
+ return false;
879
+ }
880
+ // 添加为好友
881
+ let contact = this.getContactByPeerId(response.from);
882
+ if (contact) {
883
+ const updated = this.updateContact(contact.id, {
884
+ status: contact_types_js_1.FriendStatus.FRIEND,
885
+ name: response.fromName || contact.name,
886
+ capabilities: response.capabilities,
887
+ updateLastCommunication: true,
888
+ });
889
+ if (!updated) {
890
+ this.logger?.error('[ContactManager] 处理握手响应失败:更新联系人失败');
891
+ return false;
892
+ }
893
+ contact = updated;
894
+ }
895
+ else {
896
+ // P1 修复:创建新联系人时直接设置 status 为 FRIEND
897
+ contact = this.addContact({
898
+ name: response.fromName || 'Unknown',
899
+ peerId: response.from,
900
+ capabilities: response.capabilities || [],
901
+ groups: ['default'],
902
+ status: contact_types_js_1.FriendStatus.FRIEND, // 直接设置为好友
903
+ });
904
+ if (!contact) {
905
+ this.logger?.error('[ContactManager] 处理握手响应失败:添加联系人失败');
906
+ return false;
907
+ }
908
+ }
909
+ this.logger?.info(`[ContactManager] 好友请求已接受: ${contact.name}`);
910
+ return true;
911
+ }
912
+ // ============================================================================
913
+ // 黑名单管理
914
+ // ============================================================================
915
+ /**
916
+ * 拉黑联系人
917
+ * P1-2 修复:添加保存检查和回滚
918
+ */
919
+ blockContact(contactId) {
920
+ const contact = this.getContact(contactId);
921
+ if (!contact)
922
+ return false;
923
+ // 备份原始状态用于回滚
924
+ const originalStatus = contact.status;
925
+ const wasBlocked = this.data.blockedPeers.includes(contact.peerId);
926
+ contact.status = contact_types_js_1.FriendStatus.BLOCKED;
927
+ contact.updatedAt = Date.now();
928
+ if (!this.data.blockedPeers.includes(contact.peerId)) {
929
+ this.data.blockedPeers.push(contact.peerId);
930
+ }
931
+ // P1-2 修复:检查保存结果,失败时恢复
932
+ if (!this.saveData()) {
933
+ // 保存失败,回滚
934
+ contact.status = originalStatus;
935
+ contact.updatedAt = Date.now();
936
+ if (!wasBlocked) {
937
+ const index = this.data.blockedPeers.indexOf(contact.peerId);
938
+ if (index !== -1) {
939
+ this.data.blockedPeers.splice(index, 1);
940
+ }
941
+ }
942
+ this.logger?.error('[ContactManager] 拉黑联系人失败:数据保存失败');
943
+ return false;
944
+ }
945
+ return true;
946
+ }
947
+ /**
948
+ * 解除拉黑
949
+ * P1-2 修复:添加保存检查和回滚
950
+ */
951
+ unblockContact(contactId) {
952
+ const contact = this.getContact(contactId);
953
+ if (!contact)
954
+ return false;
955
+ // 备份原始状态用于回滚
956
+ const originalStatus = contact.status;
957
+ const blockedIndex = this.data.blockedPeers.indexOf(contact.peerId);
958
+ contact.status = contact_types_js_1.FriendStatus.STRANGER;
959
+ contact.updatedAt = Date.now();
960
+ if (blockedIndex !== -1) {
961
+ this.data.blockedPeers.splice(blockedIndex, 1);
962
+ }
963
+ // P1-2 修复:检查保存结果,失败时恢复
964
+ if (!this.saveData()) {
965
+ // 保存失败,回滚
966
+ contact.status = originalStatus;
967
+ contact.updatedAt = Date.now();
968
+ if (blockedIndex !== -1) {
969
+ this.data.blockedPeers.splice(blockedIndex, 0, contact.peerId);
970
+ }
971
+ this.logger?.error('[ContactManager] 解除拉黑失败:数据保存失败');
972
+ return false;
973
+ }
974
+ return true;
975
+ }
976
+ /**
977
+ * 检查是否被拉黑
978
+ */
979
+ isBlocked(peerId) {
980
+ return this.data.blockedPeers.includes(peerId);
981
+ }
982
+ /**
983
+ * 检查是否为好友
984
+ */
985
+ isFriend(peerId) {
986
+ const contact = this.getContactByPeerId(peerId);
987
+ return contact?.status === contact_types_js_1.FriendStatus.FRIEND;
988
+ }
989
+ /**
990
+ * 检查是否可以发送消息(好友或陌生人都可以,但被拉黑不行)
991
+ */
992
+ canSendMessage(peerId) {
993
+ const contact = this.getContactByPeerId(peerId);
994
+ if (!contact)
995
+ return true; // 陌生人可以发送
996
+ return contact.status !== contact_types_js_1.FriendStatus.BLOCKED;
997
+ }
998
+ /**
999
+ * 检查是否可以发送任务消息(只有好友可以)
1000
+ */
1001
+ canSendTask(peerId) {
1002
+ return this.isFriend(peerId);
1003
+ }
1004
+ // ============================================================================
1005
+ // 导入/导出
1006
+ // ============================================================================
1007
+ /**
1008
+ * 导出通讯录
1009
+ */
1010
+ exportContacts(nodeId) {
1011
+ return {
1012
+ ...deepClone(this.data),
1013
+ exportedAt: Date.now(),
1014
+ exportedBy: nodeId,
1015
+ };
1016
+ }
1017
+ /**
1018
+ * 导入通讯录
1019
+ * P1-4 修复:添加数据大小和联系人数量限制
1020
+ * P1-3 修复:验证每个联系人的字段
1021
+ *
1022
+ * @param data - 导入的数据
1023
+ * @param merge - 是否合并(true)或覆盖(false)
1024
+ */
1025
+ importContacts(data, merge = true) {
1026
+ const result = {
1027
+ success: true,
1028
+ importedContacts: 0,
1029
+ importedGroups: 0,
1030
+ skippedContacts: 0,
1031
+ errors: [],
1032
+ };
1033
+ try {
1034
+ // P1-4 修复:检查数据大小
1035
+ const dataSize = JSON.stringify(data).length;
1036
+ if (dataSize > MAX_IMPORT_SIZE) {
1037
+ result.success = false;
1038
+ result.errors.push(`数据大小超出限制: ${dataSize} > ${MAX_IMPORT_SIZE} 字节`);
1039
+ return result;
1040
+ }
1041
+ // P1-4 修复:检查联系人数量
1042
+ if (data.contacts && data.contacts.length > MAX_CONTACTS) {
1043
+ result.success = false;
1044
+ result.errors.push(`联系人数量超出限制: ${data.contacts.length} > ${MAX_CONTACTS}`);
1045
+ return result;
1046
+ }
1047
+ if (!merge) {
1048
+ // P1-3 修复:覆盖模式也要验证数据结构
1049
+ const validContacts = [];
1050
+ for (let i = 0; i < data.contacts.length; i++) {
1051
+ if (this.validateContactFields(data.contacts[i])) {
1052
+ validContacts.push(data.contacts[i]);
1053
+ }
1054
+ else {
1055
+ result.errors.push(`联系人 #${i + 1} 字段验证失败,已跳过`);
1056
+ result.skippedContacts++;
1057
+ }
1058
+ }
1059
+ // 覆盖模式
1060
+ this.data = {
1061
+ version: CONTACTS_DATA_VERSION,
1062
+ contacts: validContacts,
1063
+ groups: data.groups.length ? data.groups : deepClone(DEFAULT_GROUPS),
1064
+ pendingHandshakes: data.pendingHandshakes || [],
1065
+ blockedPeers: data.blockedPeers || [],
1066
+ lastUpdated: Date.now(),
1067
+ };
1068
+ result.importedContacts = validContacts.length;
1069
+ result.importedGroups = data.groups.length;
1070
+ }
1071
+ else {
1072
+ // 合并模式
1073
+ const existingPeerIds = new Set(this.data.contacts.map(c => c.peerId));
1074
+ // P1-4 修复:检查合并后的总数(使用验证后的有效联系人)
1075
+ const validContacts = data.contacts.filter(c => this.validateContactFields(c));
1076
+ const invalidCount = data.contacts.length - validContacts.length;
1077
+ if (invalidCount > 0) {
1078
+ result.errors.push(`${invalidCount} 个联系人字段验证失败,已跳过`);
1079
+ result.skippedContacts += invalidCount;
1080
+ }
1081
+ const totalAfterMerge = this.data.contacts.length + validContacts.filter(c => !existingPeerIds.has(c.peerId)).length;
1082
+ if (totalAfterMerge > MAX_CONTACTS) {
1083
+ result.success = false;
1084
+ result.errors.push(`合并后联系人数量超出限制: ${totalAfterMerge} > ${MAX_CONTACTS}`);
1085
+ return result;
1086
+ }
1087
+ for (const contact of validContacts) {
1088
+ if (existingPeerIds.has(contact.peerId)) {
1089
+ result.skippedContacts++;
1090
+ }
1091
+ else {
1092
+ this.data.contacts.push(contact);
1093
+ result.importedContacts++;
1094
+ }
1095
+ }
1096
+ const existingGroupIds = new Set(this.data.groups.map(g => g.id));
1097
+ for (const group of data.groups) {
1098
+ if (!existingGroupIds.has(group.id)) {
1099
+ this.data.groups.push(group);
1100
+ result.importedGroups++;
1101
+ }
1102
+ }
1103
+ // 合并黑名单
1104
+ for (const peerId of data.blockedPeers || []) {
1105
+ if (!this.data.blockedPeers.includes(peerId)) {
1106
+ this.data.blockedPeers.push(peerId);
1107
+ }
1108
+ }
1109
+ }
1110
+ this.saveData();
1111
+ }
1112
+ catch (err) {
1113
+ result.success = false;
1114
+ result.errors.push(err instanceof Error ? err.message : String(err));
1115
+ }
1116
+ return result;
1117
+ }
1118
+ // ============================================================================
1119
+ // 事件处理
1120
+ // ============================================================================
1121
+ /**
1122
+ * 添加事件处理器
1123
+ */
1124
+ on(handler) {
1125
+ this.eventHandlers.add(handler);
1126
+ }
1127
+ /**
1128
+ * 移除事件处理器
1129
+ */
1130
+ off(handler) {
1131
+ this.eventHandlers.delete(handler);
1132
+ }
1133
+ /**
1134
+ * 触发事件
1135
+ */
1136
+ emitEvent(event, data) {
1137
+ for (const handler of this.eventHandlers) {
1138
+ try {
1139
+ handler(event, data);
1140
+ }
1141
+ catch (err) {
1142
+ this.logger?.error(`[ContactManager] 事件处理器错误: ${err}`);
1143
+ }
1144
+ }
1145
+ }
1146
+ // ============================================================================
1147
+ // 统计信息
1148
+ // ============================================================================
1149
+ /**
1150
+ * 获取统计信息
1151
+ */
1152
+ getStats() {
1153
+ return {
1154
+ total: this.data.contacts.length,
1155
+ friends: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.FRIEND).length,
1156
+ strangers: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.STRANGER).length,
1157
+ pending: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.PENDING).length,
1158
+ blocked: this.data.contacts.filter(c => c.status === contact_types_js_1.FriendStatus.BLOCKED).length,
1159
+ groups: this.data.groups.length,
1160
+ };
1161
+ }
1162
+ // ============================================================================
1163
+ // 清理
1164
+ // ============================================================================
1165
+ /**
1166
+ * 清空通讯录
1167
+ */
1168
+ clear() {
1169
+ this.data = this.createDefaultData();
1170
+ this.saveData();
1171
+ this.logger?.info('[ContactManager] 通讯录已清空');
1172
+ }
1173
+ /**
1174
+ * 删除数据文件
1175
+ */
1176
+ deleteData() {
1177
+ if ((0, fs_1.existsSync)(this.dataPath)) {
1178
+ (0, fs_1.unlinkSync)(this.dataPath);
1179
+ }
1180
+ this.data = this.createDefaultData();
1181
+ }
1182
+ }
1183
+ exports.ContactManager = ContactManager;
1184
+ // 默认导出
1185
+ exports.default = ContactManager;
1186
+ //# sourceMappingURL=contact-manager.js.map