@f2a/openclaw-f2a 0.3.0 → 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.
@@ -5,6 +5,13 @@
5
5
  * 管理通讯录、分组、标签和握手请求
6
6
  * 支持持久化存储和导入/导出功能
7
7
  *
8
+ * ⚠️ 并发安全说明
9
+ *
10
+ * ContactManager 不是线程安全的。在 Node.js 单线程事件循环环境下,
11
+ * 只要避免在同一个事件循环 tick 内发起多个修改操作,就是安全的。
12
+ *
13
+ * 如果需要在多进程/集群环境下使用,请使用外部锁服务(如 Redis)。
14
+ *
8
15
  * @module contact-manager
9
16
  */
10
17
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -19,6 +26,14 @@ const contact_types_js_1 = require("./contact-types.js");
19
26
  const CONTACTS_DATA_VERSION = 1;
20
27
  /** 默认数据文件名 */
21
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;
22
37
  /** 默认分组 */
23
38
  const DEFAULT_GROUPS = [
24
39
  {
@@ -33,15 +48,39 @@ const DEFAULT_GROUPS = [
33
48
  // 工具函数
34
49
  // ============================================================================
35
50
  /**
36
- * 生成唯一 ID
51
+ * 生成唯一 ID(UUID v4 格式)
52
+ * P2-1 修复:使用加密安全的随机数生成器
37
53
  */
38
54
  function generateId() {
39
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
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)}`;
40
64
  }
65
+ // 静态计数器
66
+ (function (generateId) {
67
+ generateId.counter = 0;
68
+ })(generateId || (generateId = {}));
41
69
  /**
42
70
  * 深拷贝对象
71
+ * P1-1 修复:使用 structuredClone 支持更多类型
43
72
  */
44
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、循环引用)
45
84
  return JSON.parse(JSON.stringify(obj));
46
85
  }
47
86
  // ============================================================================
@@ -82,6 +121,61 @@ class ContactManager {
82
121
  logger;
83
122
  eventHandlers = new Set();
84
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
+ }
85
179
  /**
86
180
  * 创建联系人管理器
87
181
  *
@@ -90,13 +184,31 @@ class ContactManager {
90
184
  * @param options - 配置选项
91
185
  */
92
186
  constructor(dataDir, logger, options) {
93
- this.dataDir = dataDir;
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;
94
206
  this.logger = logger;
95
207
  this.autoSave = options?.autoSave ?? true;
96
- this.dataPath = (0, path_1.join)(dataDir, DEFAULT_CONTACTS_FILE);
208
+ this.dataPath = (0, path_1.join)(normalizedDataDir, DEFAULT_CONTACTS_FILE);
97
209
  // 确保目录存在
98
- if (!(0, fs_1.existsSync)(dataDir)) {
99
- (0, fs_1.mkdirSync)(dataDir, { recursive: true });
210
+ if (!(0, fs_1.existsSync)(normalizedDataDir)) {
211
+ (0, fs_1.mkdirSync)(normalizedDataDir, { recursive: true });
100
212
  }
101
213
  // 加载或初始化数据
102
214
  this.data = this.loadData();
@@ -157,17 +269,21 @@ class ContactManager {
157
269
  }
158
270
  /**
159
271
  * 保存数据
272
+ * P1-2 修复:返回保存结果,不再静默忽略错误
273
+ * @returns 是否保存成功
160
274
  */
161
275
  saveData() {
162
276
  if (!this.autoSave)
163
- return;
277
+ return true;
164
278
  try {
165
279
  this.data.lastUpdated = Date.now();
166
280
  const content = JSON.stringify(this.data, null, 2);
167
281
  (0, fs_1.writeFileSync)(this.dataPath, content, 'utf-8');
282
+ return true;
168
283
  }
169
284
  catch (err) {
170
285
  this.logger?.error(`[ContactManager] 保存数据失败: ${err}`);
286
+ return false;
171
287
  }
172
288
  }
173
289
  /**
@@ -181,11 +297,28 @@ class ContactManager {
181
297
  // ============================================================================
182
298
  /**
183
299
  * 添加联系人
300
+ * P2-1 修复:添加输入验证
301
+ * P1 修复:支持传入 status 参数
184
302
  *
185
303
  * @param params - 创建参数
186
- * @returns 新创建的联系人
304
+ * @returns 新创建的联系人,如果保存失败返回 null
187
305
  */
188
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
+ }
189
322
  const now = Date.now();
190
323
  // 检查是否已存在
191
324
  const existing = this.getContactByPeerId(params.peerId);
@@ -198,7 +331,7 @@ class ContactManager {
198
331
  name: params.name,
199
332
  peerId: params.peerId,
200
333
  agentId: params.agentId,
201
- status: contact_types_js_1.FriendStatus.STRANGER,
334
+ status: params.status ?? contact_types_js_1.FriendStatus.STRANGER, // P1 修复:支持传入 status,默认为 STRANGER
202
335
  capabilities: params.capabilities || [],
203
336
  reputation: params.reputation ?? 0,
204
337
  groups: params.groups || ['default'],
@@ -211,17 +344,24 @@ class ContactManager {
211
344
  metadata: params.metadata,
212
345
  };
213
346
  this.data.contacts.push(contact);
214
- this.saveData();
347
+ // P1-2 修复:检查保存结果
348
+ if (!this.saveData()) {
349
+ // 保存失败,回滚
350
+ this.data.contacts.pop();
351
+ this.logger?.error('[ContactManager] 添加联系人失败:数据保存失败');
352
+ return null;
353
+ }
215
354
  this.emitEvent('contact:added', contact);
216
355
  this.logger?.info(`[ContactManager] 添加联系人: ${contact.name} (${contact.peerId.slice(0, 16)})`);
217
356
  return contact;
218
357
  }
219
358
  /**
220
359
  * 更新联系人
360
+ * P2-1 修复:添加输入验证
221
361
  *
222
362
  * @param contactId - 联系人 ID
223
363
  * @param params - 更新参数
224
- * @returns 更新后的联系人,如果不存在返回 null
364
+ * @returns 更新后的联系人,如果不存在或保存失败返回 null
225
365
  */
226
366
  updateContact(contactId, params) {
227
367
  const index = this.data.contacts.findIndex(c => c.id === contactId);
@@ -229,6 +369,12 @@ class ContactManager {
229
369
  return null;
230
370
  }
231
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
+ }
232
378
  // 应用更新
233
379
  if (params.name !== undefined)
234
380
  contact.name = params.name;
@@ -253,12 +399,19 @@ class ContactManager {
253
399
  }
254
400
  contact.updatedAt = Date.now();
255
401
  this.data.contacts[index] = contact;
256
- this.saveData();
402
+ // P1-2 修复:检查保存结果
403
+ if (!this.saveData()) {
404
+ // 保存失败,回滚
405
+ this.data.contacts[index] = originalContact;
406
+ this.logger?.error('[ContactManager] 更新联系人失败:数据保存失败');
407
+ return null;
408
+ }
257
409
  this.emitEvent('contact:updated', contact);
258
410
  return contact;
259
411
  }
260
412
  /**
261
413
  * 删除联系人
414
+ * P1-2 修复:添加保存检查和回滚
262
415
  *
263
416
  * @param contactId - 联系人 ID
264
417
  * @returns 是否删除成功
@@ -269,7 +422,13 @@ class ContactManager {
269
422
  return false;
270
423
  }
271
424
  const [removed] = this.data.contacts.splice(index, 1);
272
- this.saveData();
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
+ }
273
432
  this.emitEvent('contact:removed', removed);
274
433
  this.logger?.info(`[ContactManager] 删除联系人: ${removed.name} (${removed.peerId.slice(0, 16)})`);
275
434
  return true;
@@ -392,6 +551,7 @@ class ContactManager {
392
551
  // ============================================================================
393
552
  /**
394
553
  * 创建分组
554
+ * P1-2 修复:添加保存检查和回滚
395
555
  */
396
556
  createGroup(params) {
397
557
  const now = Date.now();
@@ -404,18 +564,27 @@ class ContactManager {
404
564
  updatedAt: now,
405
565
  };
406
566
  this.data.groups.push(group);
407
- this.saveData();
567
+ // P1-2 修复:检查保存结果,失败时恢复
568
+ if (!this.saveData()) {
569
+ // 保存失败,回滚
570
+ this.data.groups.pop();
571
+ this.logger?.error('[ContactManager] 创建分组失败:数据保存失败');
572
+ return null;
573
+ }
408
574
  this.emitEvent('group:created', group);
409
575
  return group;
410
576
  }
411
577
  /**
412
578
  * 更新分组
579
+ * P1-2 修复:添加保存检查和回滚
413
580
  */
414
581
  updateGroup(groupId, params) {
415
582
  const group = this.data.groups.find(g => g.id === groupId);
416
583
  if (!group) {
417
584
  return null;
418
585
  }
586
+ // 备份原始数据用于回滚
587
+ const originalGroup = { ...group };
419
588
  if (params.name !== undefined)
420
589
  group.name = params.name;
421
590
  if (params.description !== undefined)
@@ -423,12 +592,19 @@ class ContactManager {
423
592
  if (params.color !== undefined)
424
593
  group.color = params.color;
425
594
  group.updatedAt = Date.now();
426
- this.saveData();
595
+ // P1-2 修复:检查保存结果,失败时恢复
596
+ if (!this.saveData()) {
597
+ // 保存失败,回滚
598
+ Object.assign(group, originalGroup);
599
+ this.logger?.error('[ContactManager] 更新分组失败:数据保存失败');
600
+ return null;
601
+ }
427
602
  this.emitEvent('group:updated', group);
428
603
  return group;
429
604
  }
430
605
  /**
431
606
  * 删除分组
607
+ * P1-2 修复:添加保存检查和回滚
432
608
  */
433
609
  deleteGroup(groupId) {
434
610
  // 不允许删除默认分组
@@ -440,6 +616,10 @@ class ContactManager {
440
616
  if (index === -1) {
441
617
  return false;
442
618
  }
619
+ // 备份受影响的联系人分组信息用于回滚
620
+ const affectedContacts = this.data.contacts
621
+ .filter(c => c.groups.includes(groupId))
622
+ .map(c => ({ contact: c, originalGroups: [...c.groups] }));
443
623
  // 将该分组下的联系人移到默认分组
444
624
  for (const contact of this.data.contacts) {
445
625
  const groupIndex = contact.groups.indexOf(groupId);
@@ -451,7 +631,16 @@ class ContactManager {
451
631
  }
452
632
  }
453
633
  const [removed] = this.data.groups.splice(index, 1);
454
- this.saveData();
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
+ }
455
644
  this.emitEvent('group:deleted', removed);
456
645
  return true;
457
646
  }
@@ -565,11 +754,14 @@ class ContactManager {
565
754
  }
566
755
  /**
567
756
  * 接受握手请求
757
+ * P1-2 修复:在移除 pendingHandshakes 前检查 saveData
568
758
  *
569
759
  * 这会:
570
760
  * 1. 将请求方添加为好友
571
- * 2. 从待处理列表中移除
761
+ * 2. 从待处理列表中移除(仅在保存成功后)
572
762
  * 3. 触发事件
763
+ *
764
+ * @returns 包含响应和对方 Peer ID 的对象,或 null
573
765
  */
574
766
  acceptHandshake(requestId, myName, myCapabilities) {
575
767
  const index = this.data.pendingHandshakes.findIndex(p => p.requestId === requestId);
@@ -581,26 +773,49 @@ class ContactManager {
581
773
  let contact = this.getContactByPeerId(pending.from);
582
774
  if (contact) {
583
775
  // 更新现有联系人
584
- this.updateContact(contact.id, {
776
+ const updated = this.updateContact(contact.id, {
585
777
  status: contact_types_js_1.FriendStatus.FRIEND,
586
778
  capabilities: pending.capabilities,
587
779
  name: pending.fromName,
588
780
  updateLastCommunication: true,
589
781
  });
782
+ // P1-2 修复:检查更新是否成功
783
+ if (!updated) {
784
+ this.logger?.error('[ContactManager] 接受握手失败:更新联系人失败');
785
+ return null;
786
+ }
787
+ contact = updated;
590
788
  }
591
789
  else {
592
- // 创建新联系人
790
+ // P1 修复:创建新联系人时直接设置 status 为 FRIEND,避免状态不一致
593
791
  contact = this.addContact({
594
792
  name: pending.fromName,
595
793
  peerId: pending.from,
596
794
  capabilities: pending.capabilities,
597
795
  groups: ['default'],
796
+ status: contact_types_js_1.FriendStatus.FRIEND, // 直接设置为好友
598
797
  });
599
- this.updateContact(contact.id, { status: contact_types_js_1.FriendStatus.FRIEND });
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;
600
811
  }
601
- // 移除待处理请求
812
+ // 保存成功,移除待处理请求
602
813
  this.data.pendingHandshakes.splice(index, 1);
603
- this.saveData();
814
+ // 再次保存以记录 pendingHandshakes 的移除
815
+ if (!this.saveData()) {
816
+ this.logger?.warn('[ContactManager] 移除待处理请求后保存失败,但好友已添加');
817
+ // 继续执行,因为好友已经添加成功
818
+ }
604
819
  const response = {
605
820
  requestId,
606
821
  from: '', // 调用方填充
@@ -610,18 +825,32 @@ class ContactManager {
610
825
  timestamp: Date.now(),
611
826
  };
612
827
  this.emitEvent('handshake:accepted', { pending, response });
613
- return response;
828
+ return { response, fromPeerId: pending.from };
614
829
  }
615
830
  /**
616
831
  * 拒绝握手请求
832
+ * P1-2 修复:在移除 pendingHandshakes 前检查 saveData
833
+ *
834
+ * @returns 包含响应和对方 Peer ID 的对象,或 null
617
835
  */
618
836
  rejectHandshake(requestId, reason) {
619
837
  const index = this.data.pendingHandshakes.findIndex(p => p.requestId === requestId);
620
838
  if (index === -1) {
621
839
  return null;
622
840
  }
623
- const [pending] = this.data.pendingHandshakes.splice(index, 1);
624
- this.saveData();
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
+ }
625
854
  const response = {
626
855
  requestId,
627
856
  from: '', // 调用方填充
@@ -630,10 +859,11 @@ class ContactManager {
630
859
  reason,
631
860
  };
632
861
  this.emitEvent('handshake:rejected', { pending, response });
633
- return response;
862
+ return { response, fromPeerId: pending.from };
634
863
  }
635
864
  /**
636
865
  * 处理收到的握手响应
866
+ * P1 修复:添加 PeerID 验证,优化状态设置
637
867
  *
638
868
  * 当对方接受我们的好友请求时调用
639
869
  */
@@ -642,24 +872,39 @@ class ContactManager {
642
872
  this.logger?.info(`[ContactManager] 好友请求被拒绝: ${response.reason || '无原因'}`);
643
873
  return false;
644
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
+ }
645
880
  // 添加为好友
646
881
  let contact = this.getContactByPeerId(response.from);
647
882
  if (contact) {
648
- this.updateContact(contact.id, {
883
+ const updated = this.updateContact(contact.id, {
649
884
  status: contact_types_js_1.FriendStatus.FRIEND,
650
885
  name: response.fromName || contact.name,
651
886
  capabilities: response.capabilities,
652
887
  updateLastCommunication: true,
653
888
  });
889
+ if (!updated) {
890
+ this.logger?.error('[ContactManager] 处理握手响应失败:更新联系人失败');
891
+ return false;
892
+ }
893
+ contact = updated;
654
894
  }
655
895
  else {
896
+ // P1 修复:创建新联系人时直接设置 status 为 FRIEND
656
897
  contact = this.addContact({
657
898
  name: response.fromName || 'Unknown',
658
899
  peerId: response.from,
659
900
  capabilities: response.capabilities || [],
660
901
  groups: ['default'],
902
+ status: contact_types_js_1.FriendStatus.FRIEND, // 直接设置为好友
661
903
  });
662
- this.updateContact(contact.id, { status: contact_types_js_1.FriendStatus.FRIEND });
904
+ if (!contact) {
905
+ this.logger?.error('[ContactManager] 处理握手响应失败:添加联系人失败');
906
+ return false;
907
+ }
663
908
  }
664
909
  this.logger?.info(`[ContactManager] 好友请求已接受: ${contact.name}`);
665
910
  return true;
@@ -669,33 +914,63 @@ class ContactManager {
669
914
  // ============================================================================
670
915
  /**
671
916
  * 拉黑联系人
917
+ * P1-2 修复:添加保存检查和回滚
672
918
  */
673
919
  blockContact(contactId) {
674
920
  const contact = this.getContact(contactId);
675
921
  if (!contact)
676
922
  return false;
923
+ // 备份原始状态用于回滚
924
+ const originalStatus = contact.status;
925
+ const wasBlocked = this.data.blockedPeers.includes(contact.peerId);
677
926
  contact.status = contact_types_js_1.FriendStatus.BLOCKED;
678
927
  contact.updatedAt = Date.now();
679
928
  if (!this.data.blockedPeers.includes(contact.peerId)) {
680
929
  this.data.blockedPeers.push(contact.peerId);
681
930
  }
682
- this.saveData();
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
+ }
683
945
  return true;
684
946
  }
685
947
  /**
686
948
  * 解除拉黑
949
+ * P1-2 修复:添加保存检查和回滚
687
950
  */
688
951
  unblockContact(contactId) {
689
952
  const contact = this.getContact(contactId);
690
953
  if (!contact)
691
954
  return false;
955
+ // 备份原始状态用于回滚
956
+ const originalStatus = contact.status;
957
+ const blockedIndex = this.data.blockedPeers.indexOf(contact.peerId);
692
958
  contact.status = contact_types_js_1.FriendStatus.STRANGER;
693
959
  contact.updatedAt = Date.now();
694
- const index = this.data.blockedPeers.indexOf(contact.peerId);
695
- if (index !== -1) {
696
- this.data.blockedPeers.splice(index, 1);
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;
697
973
  }
698
- this.saveData();
699
974
  return true;
700
975
  }
701
976
  /**
@@ -741,6 +1016,8 @@ class ContactManager {
741
1016
  }
742
1017
  /**
743
1018
  * 导入通讯录
1019
+ * P1-4 修复:添加数据大小和联系人数量限制
1020
+ * P1-3 修复:验证每个联系人的字段
744
1021
  *
745
1022
  * @param data - 导入的数据
746
1023
  * @param merge - 是否合并(true)或覆盖(false)
@@ -754,23 +1031,60 @@ class ContactManager {
754
1031
  errors: [],
755
1032
  };
756
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
+ }
757
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
+ }
758
1059
  // 覆盖模式
759
1060
  this.data = {
760
1061
  version: CONTACTS_DATA_VERSION,
761
- contacts: data.contacts,
1062
+ contacts: validContacts,
762
1063
  groups: data.groups.length ? data.groups : deepClone(DEFAULT_GROUPS),
763
1064
  pendingHandshakes: data.pendingHandshakes || [],
764
1065
  blockedPeers: data.blockedPeers || [],
765
1066
  lastUpdated: Date.now(),
766
1067
  };
767
- result.importedContacts = data.contacts.length;
1068
+ result.importedContacts = validContacts.length;
768
1069
  result.importedGroups = data.groups.length;
769
1070
  }
770
1071
  else {
771
1072
  // 合并模式
772
1073
  const existingPeerIds = new Set(this.data.contacts.map(c => c.peerId));
773
- for (const contact of data.contacts) {
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) {
774
1088
  if (existingPeerIds.has(contact.peerId)) {
775
1089
  result.skippedContacts++;
776
1090
  }