@f2a/openclaw-f2a 0.3.0 → 0.3.3
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 +1 -0
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js +10 -1
- package/dist/connector.js.map +1 -1
- package/dist/contact-manager.d.ts +55 -7
- package/dist/contact-manager.d.ts.map +1 -1
- package/dist/contact-manager.js +350 -36
- package/dist/contact-manager.js.map +1 -1
- package/dist/contact-types.d.ts +2 -0
- package/dist/contact-types.d.ts.map +1 -1
- package/dist/handshake-protocol.d.ts +17 -1
- package/dist/handshake-protocol.d.ts.map +1 -1
- package/dist/handshake-protocol.js +124 -41
- package/dist/handshake-protocol.js.map +1 -1
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/contact-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)(
|
|
208
|
+
this.dataPath = (0, path_1.join)(normalizedDataDir, DEFAULT_CONTACTS_FILE);
|
|
97
209
|
// 确保目录存在
|
|
98
|
-
if (!(0, fs_1.existsSync)(
|
|
99
|
-
(0, fs_1.mkdirSync)(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|