@agentunion/fastaun 0.2.16 → 0.2.17
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/auth.d.ts +3 -0
- package/dist/auth.js +24 -24
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +7 -0
- package/dist/client.js +219 -159
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/e2ee-group.d.ts +5 -1
- package/dist/e2ee-group.js +14 -10
- package/dist/e2ee-group.js.map +1 -1
- package/dist/e2ee.d.ts +3 -0
- package/dist/e2ee.js +12 -4
- package/dist/e2ee.js.map +1 -1
- package/dist/events.d.ts +3 -0
- package/dist/events.js +11 -1
- package/dist/events.js.map +1 -1
- package/dist/group-id.d.ts +23 -0
- package/dist/group-id.js +94 -0
- package/dist/group-id.js.map +1 -0
- package/dist/keystore/aid-db.d.ts +1 -0
- package/dist/keystore/aid-db.js +4 -0
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/file.d.ts +5 -0
- package/dist/keystore/file.js +12 -6
- package/dist/keystore/file.js.map +1 -1
- package/dist/keystore/index.d.ts +2 -0
- package/dist/keystore/sqlite-backup.d.ts +5 -1
- package/dist/keystore/sqlite-backup.js +9 -6
- package/dist/keystore/sqlite-backup.js.map +1 -1
- package/dist/logger.d.ts +26 -3
- package/dist/logger.js +117 -40
- package/dist/logger.js.map +1 -1
- package/dist/secret-store/file-store.d.ts +4 -0
- package/dist/secret-store/file-store.js +5 -2
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/secret-store/index.d.ts +3 -0
- package/dist/secret-store/index.js +2 -2
- package/dist/secret-store/index.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.js +9 -1
- package/dist/transport.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -25,6 +25,7 @@ import { EventDispatcher } from './events.js';
|
|
|
25
25
|
import { FileKeyStore } from './keystore/file.js';
|
|
26
26
|
import { AUNLogger } from './logger.js';
|
|
27
27
|
import { SQLiteBackup } from './keystore/sqlite-backup.js';
|
|
28
|
+
import { normalizeGroupId } from './group-id.js';
|
|
28
29
|
import { AuthNamespace } from './namespaces/auth.js';
|
|
29
30
|
import { CustodyNamespace } from './namespaces/custody.js';
|
|
30
31
|
import { MetaNamespace } from './namespaces/meta.js';
|
|
@@ -32,18 +33,6 @@ import { RPCTransport } from './transport.js';
|
|
|
32
33
|
import { AuthFlow } from './auth.js';
|
|
33
34
|
import { SeqTracker } from './seq-tracker.js';
|
|
34
35
|
import { isJsonObject, } from './types.js';
|
|
35
|
-
// ── 日志辅助 ──────────────────────────────────────────────────
|
|
36
|
-
/** 文件日志(模块级单例) */
|
|
37
|
-
let _debugLogger = null;
|
|
38
|
-
/** 简易日志:前缀 [aun_core.client] */
|
|
39
|
-
function _clientLog(level, msg, ...args) {
|
|
40
|
-
const ts = new Date().toISOString();
|
|
41
|
-
const formatted = args.reduce((s, a) => s.replace('%s', String(a)), msg);
|
|
42
|
-
// eslint-disable-next-line no-console
|
|
43
|
-
console.log(`[${ts}] [aun_core.client] ${level}: ${formatted}`);
|
|
44
|
-
if (_debugLogger)
|
|
45
|
-
_debugLogger.log(`${level}: ${formatted}`);
|
|
46
|
-
}
|
|
47
36
|
/**
|
|
48
37
|
* 递归排序键的 JSON 序列化(Canonical JSON for AUN)
|
|
49
38
|
* 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
|
|
@@ -386,6 +375,8 @@ export class AUNClient {
|
|
|
386
375
|
_reconnectActive = false;
|
|
387
376
|
_reconnectAbort = null;
|
|
388
377
|
_serverKicked = false;
|
|
378
|
+
_logger;
|
|
379
|
+
_clientLog;
|
|
389
380
|
constructor(config, debug = false) {
|
|
390
381
|
const rawConfig = { ...(config ?? {}) };
|
|
391
382
|
this._configModel = configFromMap(rawConfig);
|
|
@@ -394,20 +385,27 @@ export class AUNClient {
|
|
|
394
385
|
root_ca_path: this._configModel.rootCaPath,
|
|
395
386
|
seed_password: this._configModel.seedPassword,
|
|
396
387
|
};
|
|
397
|
-
|
|
388
|
+
// 初始化 Logger(per-client 单例,必须最早创建)
|
|
389
|
+
const debugFlag = this._configModel.debug || debug;
|
|
390
|
+
this._logger = new AUNLogger({
|
|
391
|
+
debug: debugFlag,
|
|
392
|
+
aunPath: this._configModel.aunPath,
|
|
393
|
+
});
|
|
394
|
+
this._clientLog = this._logger.for('aun_core.client');
|
|
395
|
+
if (debugFlag) {
|
|
396
|
+
this._clientLog.info(`AUNClient 初始化完成 (debug=true, aunPath=${this._configModel.aunPath})`);
|
|
397
|
+
}
|
|
398
|
+
this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'));
|
|
398
399
|
this._discovery = new GatewayDiscovery({ verifySsl: this._configModel.verifySsl });
|
|
399
|
-
const defaultSQLiteBackup = new SQLiteBackup(join(this._configModel.aunPath, '.aun_backup', 'aun_backup.db'));
|
|
400
|
+
const defaultSQLiteBackup = new SQLiteBackup(join(this._configModel.aunPath, '.aun_backup', 'aun_backup.db'), { logger: this._logger.for('aun_core.keystore') });
|
|
400
401
|
const keystore = new FileKeyStore(this._configModel.aunPath, {
|
|
401
402
|
encryptionSeed: this._configModel.seedPassword ?? undefined,
|
|
402
403
|
sqliteBackup: defaultSQLiteBackup,
|
|
404
|
+
logger: this._logger.for('aun_core.keystore'),
|
|
405
|
+
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
403
406
|
});
|
|
404
407
|
this._keystore = keystore;
|
|
405
408
|
this._deviceId = getDeviceId(this._configModel.aunPath);
|
|
406
|
-
// 初始化文件日志(仅 debug 模式)
|
|
407
|
-
if (debug) {
|
|
408
|
-
_debugLogger = new AUNLogger();
|
|
409
|
-
_clientLog('info', 'AUNClient 初始化完成 (debug=true, aunPath=%s)', this._configModel.aunPath);
|
|
410
|
-
}
|
|
411
409
|
this._slotId = '';
|
|
412
410
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
413
411
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
@@ -419,24 +417,28 @@ export class AUNClient {
|
|
|
419
417
|
slotId: this._slotId,
|
|
420
418
|
rootCaPath: this._configModel.rootCaPath ?? undefined,
|
|
421
419
|
verifySsl: this._configModel.verifySsl,
|
|
420
|
+
logger: this._logger.for('aun_core.auth'),
|
|
422
421
|
});
|
|
423
422
|
this._transport = new RPCTransport({
|
|
424
423
|
eventDispatcher: this._dispatcher,
|
|
425
424
|
timeout: 10_000,
|
|
426
425
|
onDisconnect: (err, closeCode) => this._handleTransportDisconnect(err, closeCode),
|
|
427
426
|
verifySsl: this._configModel.verifySsl,
|
|
427
|
+
logger: this._logger.for('aun_core.transport'),
|
|
428
428
|
});
|
|
429
429
|
this._e2ee = new E2EEManager({
|
|
430
430
|
identityFn: () => this._identity ?? {},
|
|
431
431
|
deviceIdFn: () => this._deviceId,
|
|
432
432
|
keystore,
|
|
433
433
|
replayWindowSeconds: this._configModel.replayWindowSeconds,
|
|
434
|
+
logger: this._logger.for('aun_core.e2ee'),
|
|
434
435
|
});
|
|
435
436
|
this._groupE2ee = new GroupE2EEManager({
|
|
436
437
|
identityFn: () => this._identity ?? {},
|
|
437
438
|
keystore,
|
|
438
439
|
senderCertResolver: (aid) => this._getVerifiedPeerCert(aid),
|
|
439
440
|
initiatorCertResolver: (aid) => this._getVerifiedPeerCert(aid),
|
|
441
|
+
logger: this._logger.for('aun_core.e2ee-group'),
|
|
440
442
|
});
|
|
441
443
|
this.auth = new AuthNamespace(this);
|
|
442
444
|
this.custody = new CustodyNamespace(this);
|
|
@@ -523,6 +525,7 @@ export class AUNClient {
|
|
|
523
525
|
const closableKeyStore = this._keystore;
|
|
524
526
|
closableKeyStore.close?.();
|
|
525
527
|
this._state = 'closed';
|
|
528
|
+
this._logger.close();
|
|
526
529
|
this._resetSeqTrackingState();
|
|
527
530
|
return;
|
|
528
531
|
}
|
|
@@ -530,6 +533,7 @@ export class AUNClient {
|
|
|
530
533
|
const closableKeyStore = this._keystore;
|
|
531
534
|
closableKeyStore.close?.();
|
|
532
535
|
this._state = 'closed';
|
|
536
|
+
this._logger.close();
|
|
533
537
|
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
534
538
|
this._resetSeqTrackingState();
|
|
535
539
|
}
|
|
@@ -589,6 +593,11 @@ export class AUNClient {
|
|
|
589
593
|
const p = { ...(params ?? {}) };
|
|
590
594
|
this._validateOutboundCall(method, p);
|
|
591
595
|
this._injectMessageCursorContext(method, p);
|
|
596
|
+
// group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
|
|
597
|
+
// 不对无域输入补本域——保持与历史行为兼容,交给服务端归一化
|
|
598
|
+
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null && p.group_id !== '') {
|
|
599
|
+
p.group_id = normalizeGroupId(p.group_id);
|
|
600
|
+
}
|
|
592
601
|
// group.* 方法注入 device_id(服务端用于多设备消息路由)
|
|
593
602
|
if (method.startsWith('group.') && this._deviceId && p.device_id === undefined) {
|
|
594
603
|
p.device_id = this._deviceId;
|
|
@@ -603,8 +612,7 @@ export class AUNClient {
|
|
|
603
612
|
if (encrypt) {
|
|
604
613
|
return await this._sendEncrypted(p);
|
|
605
614
|
}
|
|
606
|
-
|
|
607
|
-
delete p.headers;
|
|
615
|
+
// encrypt=false:明文走通用 RPC 路径;protected_headers/headers 是信封元数据,加密与否都保留
|
|
608
616
|
}
|
|
609
617
|
// 自动加密:group.send 默认加密(encrypt 默认 True)
|
|
610
618
|
if (method === 'group.send') {
|
|
@@ -613,24 +621,20 @@ export class AUNClient {
|
|
|
613
621
|
if (encrypt) {
|
|
614
622
|
return await this._sendGroupEncrypted(p);
|
|
615
623
|
}
|
|
616
|
-
delete p.protected_headers;
|
|
617
|
-
delete p.headers;
|
|
618
624
|
}
|
|
619
625
|
if (method === 'group.thought.put') {
|
|
620
626
|
const encrypt = p.encrypt ?? true;
|
|
621
627
|
delete p.encrypt;
|
|
622
|
-
if (
|
|
623
|
-
|
|
628
|
+
if (encrypt) {
|
|
629
|
+
return await this._putGroupThoughtEncrypted(p);
|
|
624
630
|
}
|
|
625
|
-
return await this._putGroupThoughtEncrypted(p);
|
|
626
631
|
}
|
|
627
632
|
if (method === 'message.thought.put') {
|
|
628
633
|
const encrypt = p.encrypt ?? true;
|
|
629
634
|
delete p.encrypt;
|
|
630
|
-
if (
|
|
631
|
-
|
|
635
|
+
if (encrypt) {
|
|
636
|
+
return await this._putMessageThoughtEncrypted(p);
|
|
632
637
|
}
|
|
633
|
-
return await this._putMessageThoughtEncrypted(p);
|
|
634
638
|
}
|
|
635
639
|
// 关键操作自动附加客户端签名
|
|
636
640
|
if (SIGNED_METHODS.has(method)) {
|
|
@@ -662,7 +666,7 @@ export class AUNClient {
|
|
|
662
666
|
if (serverAck > 0) {
|
|
663
667
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
664
668
|
if (contig < serverAck) {
|
|
665
|
-
_clientLog(
|
|
669
|
+
this._clientLog.info(`message.pull retention-floor 推进: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAck}`);
|
|
666
670
|
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
667
671
|
}
|
|
668
672
|
}
|
|
@@ -674,7 +678,7 @@ export class AUNClient {
|
|
|
674
678
|
seq: contig,
|
|
675
679
|
device_id: this._deviceId,
|
|
676
680
|
slot_id: this._slotId,
|
|
677
|
-
}).catch((e) => { _clientLog(
|
|
681
|
+
}).catch((e) => { this._clientLog.debug(`message.pull auto-ack 失败: ${formatCaughtError(e)}`); });
|
|
678
682
|
}
|
|
679
683
|
}
|
|
680
684
|
}
|
|
@@ -701,7 +705,7 @@ export class AUNClient {
|
|
|
701
705
|
if (serverAck > 0) {
|
|
702
706
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
703
707
|
if (contig < serverAck) {
|
|
704
|
-
_clientLog(
|
|
708
|
+
this._clientLog.info(`group.pull retention-floor 推进: ns=${ns} contiguous=${contig} -> cursor.current_seq=${serverAck}`);
|
|
705
709
|
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
706
710
|
}
|
|
707
711
|
}
|
|
@@ -716,7 +720,7 @@ export class AUNClient {
|
|
|
716
720
|
msg_seq: contig,
|
|
717
721
|
device_id: this._deviceId,
|
|
718
722
|
slot_id: this._slotId,
|
|
719
|
-
}).catch((e) => { _clientLog(
|
|
723
|
+
}).catch((e) => { this._clientLog.debug(`group.pull auto-ack 失败: group=${gid} ${formatCaughtError(e)}`); });
|
|
720
724
|
}
|
|
721
725
|
}
|
|
722
726
|
}
|
|
@@ -762,7 +766,7 @@ export class AUNClient {
|
|
|
762
766
|
// P0-12: await rotation 完成(带超时兜底),确保后续 group.send 使用新 epoch
|
|
763
767
|
const rotationPromise = this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, result), expectedEpoch, allowMember);
|
|
764
768
|
const timeoutPromise = new Promise((resolve) => globalThis.setTimeout(resolve, 5000));
|
|
765
|
-
await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => _clientLog(
|
|
769
|
+
await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => this._clientLog.warn(`membership RPC epoch rotation fallback failed: ${formatCaughtError(exc)}`));
|
|
766
770
|
}
|
|
767
771
|
}
|
|
768
772
|
return result;
|
|
@@ -881,7 +885,7 @@ export class AUNClient {
|
|
|
881
885
|
catch (exc) {
|
|
882
886
|
if (!isRetryablePeerMaterialError(exc))
|
|
883
887
|
throw exc;
|
|
884
|
-
_clientLog(
|
|
888
|
+
this._clientLog.warn(`peer cert/prekey mismatch for ${toAid}, refreshing and retrying once`);
|
|
885
889
|
}
|
|
886
890
|
return await sendAttempt(true);
|
|
887
891
|
}
|
|
@@ -993,7 +997,7 @@ export class AUNClient {
|
|
|
993
997
|
}
|
|
994
998
|
catch (e) {
|
|
995
999
|
// 旧设备的 prekey 可能携带已轮换的证书指纹,跳过该设备的自同步副本
|
|
996
|
-
_clientLog(
|
|
1000
|
+
this._clientLog.warn(`self-sync 跳过设备 ${deviceId}: 证书解析失败 (${e}),可能是旧 prekey`);
|
|
997
1001
|
continue;
|
|
998
1002
|
}
|
|
999
1003
|
const [envelope, encryptResult] = this._encryptCopyPayload({
|
|
@@ -1031,7 +1035,7 @@ export class AUNClient {
|
|
|
1031
1035
|
mode: encryptResult.mode,
|
|
1032
1036
|
reason: encryptResult.degradation_reason,
|
|
1033
1037
|
}).catch((exc) => {
|
|
1034
|
-
_clientLog(
|
|
1038
|
+
this._clientLog.warn(`发布 e2ee.degraded 事件失败: ${formatCaughtError(exc)}`);
|
|
1035
1039
|
});
|
|
1036
1040
|
}
|
|
1037
1041
|
}
|
|
@@ -1096,7 +1100,7 @@ export class AUNClient {
|
|
|
1096
1100
|
}
|
|
1097
1101
|
catch (exc) {
|
|
1098
1102
|
if (attempt === 0 && this._isRecoverableGroupEpochError(exc)) {
|
|
1099
|
-
_clientLog(
|
|
1103
|
+
this._clientLog.warn(`群 ${groupId} 调用 ${method} 时 epoch 已过旧,恢复密钥后重加密重试一次: ${formatCaughtError(exc)}`);
|
|
1100
1104
|
({ sendParams, groupId } = await this._prepareGroupEncryptedRpcParams(method, params, options, true));
|
|
1101
1105
|
continue;
|
|
1102
1106
|
}
|
|
@@ -1182,11 +1186,11 @@ export class AUNClient {
|
|
|
1182
1186
|
}
|
|
1183
1187
|
if (messages.length > 0) {
|
|
1184
1188
|
this._saveSeqTrackerState();
|
|
1185
|
-
_clientLog(
|
|
1189
|
+
this._clientLog.info(`惰性同步群 ${groupId}: pull ${messages.length} 条消息, after_seq=${afterSeq}`);
|
|
1186
1190
|
}
|
|
1187
1191
|
}
|
|
1188
1192
|
catch (exc) {
|
|
1189
|
-
_clientLog(
|
|
1193
|
+
this._clientLog.warn(`惰性同步群 ${groupId} 失败: ${formatCaughtError(exc)}`);
|
|
1190
1194
|
}
|
|
1191
1195
|
}
|
|
1192
1196
|
/** 惰性同步:首次激活 P2P 通道时 pull 最近消息,建立 seq 基线 */
|
|
@@ -1209,11 +1213,11 @@ export class AUNClient {
|
|
|
1209
1213
|
}
|
|
1210
1214
|
if (messages.length > 0) {
|
|
1211
1215
|
this._saveSeqTrackerState();
|
|
1212
|
-
_clientLog(
|
|
1216
|
+
this._clientLog.info(`惰性同步 P2P: pull ${messages.length} 条消息, after_seq=${afterSeq}`);
|
|
1213
1217
|
}
|
|
1214
1218
|
}
|
|
1215
1219
|
catch (exc) {
|
|
1216
|
-
_clientLog(
|
|
1220
|
+
this._clientLog.warn(`惰性同步 P2P 失败: ${formatCaughtError(exc)}`);
|
|
1217
1221
|
}
|
|
1218
1222
|
}
|
|
1219
1223
|
_isGroupEpochTooOldError(exc) {
|
|
@@ -1269,10 +1273,10 @@ export class AUNClient {
|
|
|
1269
1273
|
encrypt: true,
|
|
1270
1274
|
persist_required: true,
|
|
1271
1275
|
});
|
|
1272
|
-
_clientLog(
|
|
1276
|
+
this._clientLog.info(`已向 ${targetAid} 请求群 ${groupId} 的 epoch ${epoch} 密钥`);
|
|
1273
1277
|
}
|
|
1274
1278
|
catch (exc) {
|
|
1275
|
-
_clientLog(
|
|
1279
|
+
this._clientLog.warn(`向 ${targetAid} 请求群 ${groupId} 密钥失败: ${formatCaughtError(exc)}`);
|
|
1276
1280
|
}
|
|
1277
1281
|
}
|
|
1278
1282
|
async _requestGroupKeyFromCandidates(groupId, serverEpoch, epochResult) {
|
|
@@ -1287,7 +1291,7 @@ export class AUNClient {
|
|
|
1287
1291
|
const secretData = this._groupE2ee.loadSecret(groupId, 1);
|
|
1288
1292
|
if (!secretData || secretData.pending_rotation_id)
|
|
1289
1293
|
return epochResult;
|
|
1290
|
-
_clientLog(
|
|
1294
|
+
this._clientLog.warn(`群 ${groupId} 检测到本地 epoch 1 已存在但服务端 epoch 仍为 0,尝试补同步初始 epoch`);
|
|
1291
1295
|
await this._syncEpochToServer(groupId);
|
|
1292
1296
|
try {
|
|
1293
1297
|
const refreshed = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
@@ -1295,7 +1299,7 @@ export class AUNClient {
|
|
|
1295
1299
|
return refreshed;
|
|
1296
1300
|
}
|
|
1297
1301
|
catch (exc) {
|
|
1298
|
-
_clientLog(
|
|
1302
|
+
this._clientLog.warn(`群 ${groupId} 初始 epoch 补同步后刷新服务端 epoch 失败: ${formatCaughtError(exc)}`);
|
|
1299
1303
|
}
|
|
1300
1304
|
return epochResult;
|
|
1301
1305
|
}
|
|
@@ -1312,7 +1316,7 @@ export class AUNClient {
|
|
|
1312
1316
|
catch (exc) {
|
|
1313
1317
|
if (strict)
|
|
1314
1318
|
throw new StateError(`group ${groupId} failed to query server epoch before retry: ${formatCaughtError(exc)}`);
|
|
1315
|
-
_clientLog(
|
|
1319
|
+
this._clientLog.warn(`group ${groupId} epoch precheck failed: ${formatCaughtError(exc)}`);
|
|
1316
1320
|
return;
|
|
1317
1321
|
}
|
|
1318
1322
|
let serverEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
@@ -1360,7 +1364,7 @@ export class AUNClient {
|
|
|
1360
1364
|
throw new StateError(`group ${groupId} epoch rotation has not completed`);
|
|
1361
1365
|
}
|
|
1362
1366
|
}
|
|
1363
|
-
_clientLog(
|
|
1367
|
+
this._clientLog.warn(`group ${groupId} local epoch=${effectiveLocalEpoch} < server epoch=${serverEpoch}; requesting key recovery`);
|
|
1364
1368
|
await this._recoverGroupEpochKey(groupId, serverEpoch, '', 5000);
|
|
1365
1369
|
const deadline = Date.now() + 5000;
|
|
1366
1370
|
while (Date.now() < deadline) {
|
|
@@ -1384,7 +1388,7 @@ export class AUNClient {
|
|
|
1384
1388
|
members = isJsonObject(membersResult) ? membersResult.members : null;
|
|
1385
1389
|
}
|
|
1386
1390
|
catch (exc) {
|
|
1387
|
-
_clientLog(
|
|
1391
|
+
this._clientLog.debug(`群 ${groupId} 成员 epoch floor 预检跳过: ${formatCaughtError(exc)}`);
|
|
1388
1392
|
return;
|
|
1389
1393
|
}
|
|
1390
1394
|
let maxMinReadEpoch = 0;
|
|
@@ -1399,7 +1403,7 @@ export class AUNClient {
|
|
|
1399
1403
|
}
|
|
1400
1404
|
if (maxMinReadEpoch <= committedEpoch)
|
|
1401
1405
|
return;
|
|
1402
|
-
_clientLog(
|
|
1406
|
+
this._clientLog.warn(`群 ${groupId} 成员 min_read_epoch 高于 committed epoch,按 committed epoch 继续发送: committed=${committedEpoch} floor=${maxMinReadEpoch}`);
|
|
1403
1407
|
return;
|
|
1404
1408
|
}
|
|
1405
1409
|
}
|
|
@@ -1410,7 +1414,7 @@ export class AUNClient {
|
|
|
1410
1414
|
return epochResult;
|
|
1411
1415
|
}
|
|
1412
1416
|
catch (exc) {
|
|
1413
|
-
_clientLog(
|
|
1417
|
+
this._clientLog.warn(`群 ${groupId} 查询 committed epoch 状态失败,回退本地 epoch: ${formatCaughtError(exc)}`);
|
|
1414
1418
|
}
|
|
1415
1419
|
const localEpoch = await this._groupE2ee.currentEpoch(groupId);
|
|
1416
1420
|
return { epoch: localEpoch ?? 0, committed_epoch: localEpoch ?? 0 };
|
|
@@ -1438,7 +1442,7 @@ export class AUNClient {
|
|
|
1438
1442
|
let committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
|
|
1439
1443
|
if (await this._committedRotationMembershipGap(groupId, committedEpoch, committedRotation)) {
|
|
1440
1444
|
const allowMember = await this._groupAllowsMemberEpochRotation(groupId);
|
|
1441
|
-
_clientLog(
|
|
1445
|
+
this._clientLog.warn(`群 ${groupId} committed epoch ${committedEpoch} 的成员快照与当前成员不一致,触发成员变更轮换修复`);
|
|
1442
1446
|
await this._maybeLeadRotateGroupEpoch(groupId, `${groupId}:committed_membership_gap:aid:${this._aid}:epoch:${committedEpoch}`, committedEpoch, allowMember);
|
|
1443
1447
|
const refreshed = await this._committedGroupEpochState(groupId);
|
|
1444
1448
|
const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
|
|
@@ -1455,7 +1459,7 @@ export class AUNClient {
|
|
|
1455
1459
|
return committedEpoch;
|
|
1456
1460
|
}
|
|
1457
1461
|
const pendingRotationId = secretData ? String(secretData.pending_rotation_id ?? '') : '';
|
|
1458
|
-
_clientLog(
|
|
1462
|
+
this._clientLog.warn(`群 ${groupId} epoch ${committedEpoch} 本地 pending key 未匹配服务端 committed rotation,先恢复密钥: local_rotation=${pendingRotationId || '-'}`);
|
|
1459
1463
|
await this._recoverGroupEpochKey(groupId, committedEpoch, '', 5000);
|
|
1460
1464
|
let refreshed = await this._committedGroupEpochState(groupId);
|
|
1461
1465
|
const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
|
|
@@ -1500,13 +1504,13 @@ export class AUNClient {
|
|
|
1500
1504
|
if (activeMembers.join('\n') !== expectedMembers.join('\n')) {
|
|
1501
1505
|
const missing = activeMembers.filter((aid) => !expectedMembers.includes(aid));
|
|
1502
1506
|
const extra = expectedMembers.filter((aid) => !activeMembers.includes(aid));
|
|
1503
|
-
_clientLog(
|
|
1507
|
+
this._clientLog.info(`群 ${groupId} committed membership gap: epoch=${committedEpoch} missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
|
|
1504
1508
|
return true;
|
|
1505
1509
|
}
|
|
1506
1510
|
return false;
|
|
1507
1511
|
}
|
|
1508
1512
|
catch (exc) {
|
|
1509
|
-
_clientLog(
|
|
1513
|
+
this._clientLog.debug(`查询当前成员失败,无法判断 committed membership gap: group=${groupId} err=${formatCaughtError(exc)}`);
|
|
1510
1514
|
return false;
|
|
1511
1515
|
}
|
|
1512
1516
|
}
|
|
@@ -1558,7 +1562,7 @@ export class AUNClient {
|
|
|
1558
1562
|
async _onRawMessageReceived(data) {
|
|
1559
1563
|
// 异步处理,不阻塞事件调度
|
|
1560
1564
|
this._processAndPublishMessage(data).catch((exc) => {
|
|
1561
|
-
_clientLog(
|
|
1565
|
+
this._clientLog.warn(`P2P 消息解密失败: ${formatCaughtError(exc)}`);
|
|
1562
1566
|
// H26: 不再投递原始密文 payload;改发 message.undecryptable 事件,仅携带安全 header
|
|
1563
1567
|
if (isJsonObject(data)) {
|
|
1564
1568
|
const safeEvent = {
|
|
@@ -1594,7 +1598,7 @@ export class AUNClient {
|
|
|
1594
1598
|
const ns = `p2p:${this._aid}`;
|
|
1595
1599
|
const needPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
1596
1600
|
if (needPull) {
|
|
1597
|
-
this._fillP2pGap().catch(exc => _clientLog(
|
|
1601
|
+
this._fillP2pGap().catch(exc => this._clientLog.warn(`后台补洞触发失败: ${formatCaughtError(exc)}`));
|
|
1598
1602
|
}
|
|
1599
1603
|
// auto-ack contiguous_seq
|
|
1600
1604
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
@@ -1603,7 +1607,7 @@ export class AUNClient {
|
|
|
1603
1607
|
seq: contig,
|
|
1604
1608
|
device_id: this._deviceId,
|
|
1605
1609
|
slot_id: this._slotId,
|
|
1606
|
-
}).catch((e) => { _clientLog(
|
|
1610
|
+
}).catch((e) => { this._clientLog.debug(`P2P auto-ack 失败: ${formatCaughtError(e)}`); });
|
|
1607
1611
|
}
|
|
1608
1612
|
// 即时持久化 cursor,异常断连后不回退
|
|
1609
1613
|
this._saveSeqTrackerState();
|
|
@@ -1620,7 +1624,7 @@ export class AUNClient {
|
|
|
1620
1624
|
/** 处理群组消息推送:自动解密后 re-publish */
|
|
1621
1625
|
async _onRawGroupMessageCreated(data) {
|
|
1622
1626
|
this._processAndPublishGroupMessage(data).catch((exc) => {
|
|
1623
|
-
_clientLog(
|
|
1627
|
+
this._clientLog.warn(`群消息解密失败: ${formatCaughtError(exc)}`);
|
|
1624
1628
|
// H26: 不再投递原始密文 payload;改发 group.message_undecryptable 事件
|
|
1625
1629
|
if (isJsonObject(data)) {
|
|
1626
1630
|
const safeEvent = {
|
|
@@ -1665,7 +1669,7 @@ export class AUNClient {
|
|
|
1665
1669
|
const ns = `group:${groupId}`;
|
|
1666
1670
|
const needPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
1667
1671
|
if (needPull) {
|
|
1668
|
-
this._fillGroupGap(groupId).catch(exc => _clientLog(
|
|
1672
|
+
this._fillGroupGap(groupId).catch(exc => this._clientLog.warn(`后台补洞触发失败: ${formatCaughtError(exc)}`));
|
|
1669
1673
|
}
|
|
1670
1674
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1671
1675
|
if (contig > 0) {
|
|
@@ -1674,7 +1678,7 @@ export class AUNClient {
|
|
|
1674
1678
|
msg_seq: contig,
|
|
1675
1679
|
device_id: this._deviceId,
|
|
1676
1680
|
slot_id: this._slotId,
|
|
1677
|
-
}).catch((e) => { _clientLog(
|
|
1681
|
+
}).catch((e) => { this._clientLog.debug(`群消息 auto-ack 失败: group=${groupId} ${formatCaughtError(e)}`); });
|
|
1678
1682
|
}
|
|
1679
1683
|
this._saveSeqTrackerState();
|
|
1680
1684
|
}
|
|
@@ -1742,7 +1746,7 @@ export class AUNClient {
|
|
|
1742
1746
|
}
|
|
1743
1747
|
}
|
|
1744
1748
|
catch (exc) {
|
|
1745
|
-
_clientLog(
|
|
1749
|
+
this._clientLog.debug(`自动 pull 群消息失败: ${formatCaughtError(exc)}`);
|
|
1746
1750
|
}
|
|
1747
1751
|
await this._publishAppEvent('group.message_created', notification);
|
|
1748
1752
|
}
|
|
@@ -1785,7 +1789,7 @@ export class AUNClient {
|
|
|
1785
1789
|
}
|
|
1786
1790
|
}
|
|
1787
1791
|
catch (exc) {
|
|
1788
|
-
_clientLog(
|
|
1792
|
+
this._clientLog.warn(`群消息补洞失败: ${formatCaughtError(exc)}`);
|
|
1789
1793
|
}
|
|
1790
1794
|
finally {
|
|
1791
1795
|
this._gapFillDone.delete(dedupKey);
|
|
@@ -1830,7 +1834,7 @@ export class AUNClient {
|
|
|
1830
1834
|
}
|
|
1831
1835
|
}
|
|
1832
1836
|
catch (exc) {
|
|
1833
|
-
_clientLog(
|
|
1837
|
+
this._clientLog.warn(`P2P 消息补洞失败: ${formatCaughtError(exc)}`);
|
|
1834
1838
|
}
|
|
1835
1839
|
finally {
|
|
1836
1840
|
this._gapFillDone.delete(dedupKey);
|
|
@@ -1987,7 +1991,7 @@ export class AUNClient {
|
|
|
1987
1991
|
if (serverAck > 0) {
|
|
1988
1992
|
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
1989
1993
|
if (contigBefore < serverAck) {
|
|
1990
|
-
_clientLog(
|
|
1994
|
+
this._clientLog.info(`group.pull_events retention-floor 推进: ns=${ns} contiguous=${contigBefore} -> cursor.current_seq=${serverAck}`);
|
|
1991
1995
|
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
1992
1996
|
}
|
|
1993
1997
|
}
|
|
@@ -2000,7 +2004,7 @@ export class AUNClient {
|
|
|
2000
2004
|
event_seq: contig,
|
|
2001
2005
|
device_id: this._deviceId,
|
|
2002
2006
|
slot_id: this._slotId,
|
|
2003
|
-
}).catch((e) => { _clientLog(
|
|
2007
|
+
}).catch((e) => { this._clientLog.debug(`群事件 auto-ack 失败: group=${groupId} ${formatCaughtError(e)}`); });
|
|
2004
2008
|
}
|
|
2005
2009
|
for (const evt of events) {
|
|
2006
2010
|
if (isJsonObject(evt)) {
|
|
@@ -2022,7 +2026,7 @@ export class AUNClient {
|
|
|
2022
2026
|
}
|
|
2023
2027
|
}
|
|
2024
2028
|
catch (exc) {
|
|
2025
|
-
_clientLog(
|
|
2029
|
+
this._clientLog.warn(`群事件补洞失败: ${formatCaughtError(exc)}`);
|
|
2026
2030
|
}
|
|
2027
2031
|
finally {
|
|
2028
2032
|
this._gapFillDone.delete(dedupKey);
|
|
@@ -2179,12 +2183,12 @@ export class AUNClient {
|
|
|
2179
2183
|
event_seq: contig,
|
|
2180
2184
|
device_id: this._deviceId,
|
|
2181
2185
|
slot_id: this._slotId,
|
|
2182
|
-
}).catch((e) => { _clientLog(
|
|
2186
|
+
}).catch((e) => { this._clientLog.debug(`群事件推送 auto-ack 失败: group=${groupId} ${formatCaughtError(e)}`); });
|
|
2183
2187
|
}
|
|
2184
2188
|
}
|
|
2185
2189
|
// 仅在真实 event gap 时才触发补拉(补洞回来的事件不再触发新补洞)
|
|
2186
2190
|
if (needPull && groupId && !d._from_gap_fill) {
|
|
2187
|
-
this._fillGroupEventGap(groupId).catch(exc => _clientLog(
|
|
2191
|
+
this._fillGroupEventGap(groupId).catch(exc => this._clientLog.warn(`后台补洞触发失败: ${formatCaughtError(exc)}`));
|
|
2188
2192
|
}
|
|
2189
2193
|
// 成员退出或被踢 → 剩余 admin/owner 自动补位轮换
|
|
2190
2194
|
// H21: 避免 epoch 轮换风暴——所有剩余 admin 同时收到事件不能都发起轮换,
|
|
@@ -2195,7 +2199,7 @@ export class AUNClient {
|
|
|
2195
2199
|
{
|
|
2196
2200
|
const expectedEpoch = this._membershipRotationExpectedEpoch(d);
|
|
2197
2201
|
if (expectedEpoch === null) {
|
|
2198
|
-
_clientLog(
|
|
2202
|
+
this._clientLog.debug(`membership event without old_epoch skipped for epoch rotation: aid=${this._aid ?? ''} group=${groupId} action=${String(d.action ?? '')} event_seq=${String(d.event_seq ?? '')}`);
|
|
2199
2203
|
}
|
|
2200
2204
|
else {
|
|
2201
2205
|
this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch).catch((exc) => this._logE2eeError('rotate_epoch', groupId, '', exc));
|
|
@@ -2213,7 +2217,7 @@ export class AUNClient {
|
|
|
2213
2217
|
const expectedEpoch = this._membershipRotationExpectedEpoch(d);
|
|
2214
2218
|
const joinedAids = this._joinedMemberAidsFromPayload(d);
|
|
2215
2219
|
const isSelfJoining = joinedAids.includes(this._aid ?? '') && (action === 'joined' || action === 'invite_code_used');
|
|
2216
|
-
_clientLog(
|
|
2220
|
+
this._clientLog.warn(`DEBUG: group.changed action=${action} groupId=${groupId} joinedAids=${JSON.stringify(joinedAids)} myAid=${this._aid} isSelfJoining=${String(isSelfJoining)} expectedEpoch=${String(expectedEpoch)}`);
|
|
2217
2221
|
if (isSelfJoining || (action === 'joined' || action === 'invite_code_used')) {
|
|
2218
2222
|
// open/invite_code 群:所有在线成员都参与延迟轮换
|
|
2219
2223
|
// 新成员自己延迟更长,优先让其他在线成员先轮换
|
|
@@ -2267,7 +2271,7 @@ export class AUNClient {
|
|
|
2267
2271
|
if (cs && isJsonObject(cs)) {
|
|
2268
2272
|
const verified = await this._verifyEventSignatureAsync(d, cs);
|
|
2269
2273
|
if (verified === false) {
|
|
2270
|
-
_clientLog(
|
|
2274
|
+
this._clientLog.warn(`state_committed 提交者签名验证失败 group=${groupId}`);
|
|
2271
2275
|
return;
|
|
2272
2276
|
}
|
|
2273
2277
|
d._verified = verified;
|
|
@@ -2282,7 +2286,7 @@ export class AUNClient {
|
|
|
2282
2286
|
const loadFn = this._keystore.loadGroupState;
|
|
2283
2287
|
const localState = loadFn ? loadFn.call(this._keystore, groupId) : null;
|
|
2284
2288
|
if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
|
|
2285
|
-
_clientLog(
|
|
2289
|
+
this._clientLog.warn(`state_hash 链不连续 group=${groupId} local_sv=${localState.state_version} event_sv=${stateVersion}`);
|
|
2286
2290
|
// 回源同步
|
|
2287
2291
|
try {
|
|
2288
2292
|
const serverState = await this._transport.call('group.get_state', { group_id: groupId });
|
|
@@ -2302,7 +2306,7 @@ export class AUNClient {
|
|
|
2302
2306
|
members: sMembers, policy: sPolicy, prevStateHash: sPrev,
|
|
2303
2307
|
});
|
|
2304
2308
|
if (computed !== sHash) {
|
|
2305
|
-
_clientLog(
|
|
2309
|
+
this._clientLog.warn(`回源 state_hash 验证失败 group=${groupId} sv=${sv} expected=${sHash} got=${computed}`);
|
|
2306
2310
|
return;
|
|
2307
2311
|
}
|
|
2308
2312
|
}
|
|
@@ -2313,7 +2317,7 @@ export class AUNClient {
|
|
|
2313
2317
|
}
|
|
2314
2318
|
}
|
|
2315
2319
|
catch (exc) {
|
|
2316
|
-
_clientLog(
|
|
2320
|
+
this._clientLog.warn(`state 回源失败 group=${groupId}: ${formatCaughtError(exc)}`);
|
|
2317
2321
|
}
|
|
2318
2322
|
return;
|
|
2319
2323
|
}
|
|
@@ -2325,7 +2329,7 @@ export class AUNClient {
|
|
|
2325
2329
|
members, policy, prevStateHash,
|
|
2326
2330
|
});
|
|
2327
2331
|
if (computed !== stateHash) {
|
|
2328
|
-
_clientLog(
|
|
2332
|
+
this._clientLog.warn(`state_hash 重算不匹配 group=${groupId} sv=${stateVersion} expected=${stateHash} got=${computed}`);
|
|
2329
2333
|
return;
|
|
2330
2334
|
}
|
|
2331
2335
|
// 3. 更新本地存储
|
|
@@ -2349,7 +2353,7 @@ export class AUNClient {
|
|
|
2349
2353
|
if (this._closing || this._state !== 'connected')
|
|
2350
2354
|
return;
|
|
2351
2355
|
if (Date.now() - started > 20000) {
|
|
2352
|
-
_clientLog(
|
|
2356
|
+
this._clientLog.warn(`group epoch rotation still in-flight; skip pending trigger (group=${groupId} trigger=${triggerId || '-'})`);
|
|
2353
2357
|
return;
|
|
2354
2358
|
}
|
|
2355
2359
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
@@ -2444,11 +2448,11 @@ export class AUNClient {
|
|
|
2444
2448
|
});
|
|
2445
2449
|
return;
|
|
2446
2450
|
}
|
|
2447
|
-
_clientLog(
|
|
2451
|
+
this._clientLog.info(`[H21] leader 未完成 epoch 轮换,非 leader 兜底: group=${groupId} myAid=${myAid}`);
|
|
2448
2452
|
await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
|
|
2449
2453
|
}
|
|
2450
2454
|
catch (exc) {
|
|
2451
|
-
_clientLog(
|
|
2455
|
+
this._clientLog.warn(`_maybeLeadRotateGroupEpoch 失败: ${formatCaughtError(exc)}`);
|
|
2452
2456
|
}
|
|
2453
2457
|
finally {
|
|
2454
2458
|
this._groupEpochRotationInflight.delete(groupId);
|
|
@@ -2466,7 +2470,7 @@ export class AUNClient {
|
|
|
2466
2470
|
this._groupE2ee.removeGroup(groupId);
|
|
2467
2471
|
}
|
|
2468
2472
|
catch (exc) {
|
|
2469
|
-
_clientLog(
|
|
2473
|
+
this._clientLog.warn(`清理解散群组 ${groupId} epoch 密钥失败: ${formatCaughtError(exc)}`);
|
|
2470
2474
|
}
|
|
2471
2475
|
// 2. 清理 seq_tracker 中的群消息和群事件命名空间
|
|
2472
2476
|
this._seqTracker.removeNamespace(`group:${groupId}`);
|
|
@@ -2483,7 +2487,7 @@ export class AUNClient {
|
|
|
2483
2487
|
this._pushedSeqs.delete(`group_event:${groupId}`);
|
|
2484
2488
|
this._pendingOrderedMsgs.delete(`group:${groupId}`);
|
|
2485
2489
|
this._pendingDecryptMsgs.delete(`group:${groupId}`);
|
|
2486
|
-
_clientLog(
|
|
2490
|
+
this._clientLog.info(`已清理解散群组 ${groupId} 的本地状态`);
|
|
2487
2491
|
}
|
|
2488
2492
|
/** 同步验签群事件 client_signature。返回 true/false/"pending"。 */
|
|
2489
2493
|
/**
|
|
@@ -2518,7 +2522,7 @@ export class AUNClient {
|
|
|
2518
2522
|
if (expectedFP) {
|
|
2519
2523
|
const actualFP = 'sha256:' + certObj.fingerprint256.replace(/:/g, '').toLowerCase();
|
|
2520
2524
|
if (actualFP !== expectedFP) {
|
|
2521
|
-
_clientLog(
|
|
2525
|
+
this._clientLog.warn(`验签失败:证书指纹不匹配 aid=${sigAid}`);
|
|
2522
2526
|
return false;
|
|
2523
2527
|
}
|
|
2524
2528
|
}
|
|
@@ -2529,7 +2533,7 @@ export class AUNClient {
|
|
|
2529
2533
|
const pubKey = certObj.publicKey;
|
|
2530
2534
|
const ok = crypto.verify('SHA256', signData, pubKey, Buffer.from(sigB64, 'base64'));
|
|
2531
2535
|
if (!ok) {
|
|
2532
|
-
_clientLog(
|
|
2536
|
+
this._clientLog.warn(`群事件验签失败 aid=${sigAid} method=${method}`);
|
|
2533
2537
|
// P1-16: 签名失败统一发布事件
|
|
2534
2538
|
this._dispatcher.publish('signature.verification_failed', {
|
|
2535
2539
|
aid: sigAid, method, error: 'ECDSA verification failed',
|
|
@@ -2538,7 +2542,7 @@ export class AUNClient {
|
|
|
2538
2542
|
return ok;
|
|
2539
2543
|
}
|
|
2540
2544
|
catch (exc) {
|
|
2541
|
-
_clientLog(
|
|
2545
|
+
this._clientLog.warn(`群事件验签异常: ${formatCaughtError(exc)}`);
|
|
2542
2546
|
// P1-16: 签名失败统一发布事件
|
|
2543
2547
|
this._dispatcher.publish('signature.verification_failed', {
|
|
2544
2548
|
aid: String(cs.aid ?? ''), method: String(cs._method ?? ''),
|
|
@@ -2622,7 +2626,7 @@ export class AUNClient {
|
|
|
2622
2626
|
members = memberList.map((m) => String(m.aid));
|
|
2623
2627
|
}
|
|
2624
2628
|
catch (exc) {
|
|
2625
|
-
_clientLog(
|
|
2629
|
+
this._clientLog.warn(`群组 ${groupId} 成员列表回源失败: ${formatCaughtError(exc)}`);
|
|
2626
2630
|
}
|
|
2627
2631
|
}
|
|
2628
2632
|
const response = this._groupE2ee.handleKeyRequestMsg(actualPayload, members);
|
|
@@ -2636,7 +2640,7 @@ export class AUNClient {
|
|
|
2636
2640
|
});
|
|
2637
2641
|
}
|
|
2638
2642
|
catch (exc) {
|
|
2639
|
-
_clientLog(
|
|
2643
|
+
this._clientLog.warn(`向 ${requester} 回复群组密钥失败: ${formatCaughtError(exc)}`);
|
|
2640
2644
|
}
|
|
2641
2645
|
}
|
|
2642
2646
|
}
|
|
@@ -2647,7 +2651,7 @@ export class AUNClient {
|
|
|
2647
2651
|
const keyCommitment = String(actualPayload.commitment ?? '');
|
|
2648
2652
|
if (rotationId && keyCommitment) {
|
|
2649
2653
|
this._ackGroupRotationKey(rotationId, keyCommitment)
|
|
2650
|
-
.catch((exc) => _clientLog(
|
|
2654
|
+
.catch((exc) => this._clientLog.warn(`提交 epoch key ack 失败: ${formatCaughtError(exc)}`));
|
|
2651
2655
|
}
|
|
2652
2656
|
this._scheduleRetryPendingDecryptMsgs(groupId);
|
|
2653
2657
|
}
|
|
@@ -2722,7 +2726,7 @@ export class AUNClient {
|
|
|
2722
2726
|
this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
2723
2727
|
}
|
|
2724
2728
|
catch (exc) {
|
|
2725
|
-
_clientLog(
|
|
2729
|
+
this._clientLog.error(`写入证书到 keystore 失败 (aid=${aid}, fp=${certFingerprint ?? ''}): ${formatCaughtError(exc)}`, exc instanceof Error ? exc : undefined);
|
|
2726
2730
|
}
|
|
2727
2731
|
return certPem;
|
|
2728
2732
|
}
|
|
@@ -2873,10 +2877,10 @@ export class AUNClient {
|
|
|
2873
2877
|
catch (exc) {
|
|
2874
2878
|
// 刷新失败时:若内存缓存有 PKI 验证过的证书(未过期 x2 倍 TTL)则继续用
|
|
2875
2879
|
if (cached && now < cached.validatedAt + PEER_CERT_CACHE_TTL * 2) {
|
|
2876
|
-
_clientLog(
|
|
2880
|
+
this._clientLog.debug(`刷新发送方 ${aid} 证书失败,继续使用已验证的内存缓存: ${formatCaughtError(exc)}`);
|
|
2877
2881
|
return true;
|
|
2878
2882
|
}
|
|
2879
|
-
_clientLog(
|
|
2883
|
+
this._clientLog.warn(`获取发送方 ${aid} 证书失败且无已验证缓存,拒绝信任: ${formatCaughtError(exc)}`);
|
|
2880
2884
|
return false;
|
|
2881
2885
|
}
|
|
2882
2886
|
}
|
|
@@ -2912,7 +2916,7 @@ export class AUNClient {
|
|
|
2912
2916
|
if (fromAid) {
|
|
2913
2917
|
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
2914
2918
|
if (!certReady) {
|
|
2915
|
-
_clientLog(
|
|
2919
|
+
this._clientLog.warn(`无法获取发送方 ${fromAid} 的证书,跳过解密`);
|
|
2916
2920
|
throw new Error(`发送方证书不可用: from=${fromAid}, mid=${message.message_id}`);
|
|
2917
2921
|
}
|
|
2918
2922
|
}
|
|
@@ -2948,7 +2952,7 @@ export class AUNClient {
|
|
|
2948
2952
|
if (fromAid) {
|
|
2949
2953
|
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
2950
2954
|
if (!certReady) {
|
|
2951
|
-
_clientLog(
|
|
2955
|
+
this._clientLog.warn(`无法获取发送方 ${fromAid} 的证书,跳过解密`);
|
|
2952
2956
|
continue;
|
|
2953
2957
|
}
|
|
2954
2958
|
}
|
|
@@ -2959,7 +2963,7 @@ export class AUNClient {
|
|
|
2959
2963
|
}
|
|
2960
2964
|
else {
|
|
2961
2965
|
// TS-015: 解密失败不回退到密文,跳过该消息并记录
|
|
2962
|
-
_clientLog(
|
|
2966
|
+
this._clientLog.warn(`pull 消息解密失败,跳过: from=${msg.from} mid=${msg.message_id}`);
|
|
2963
2967
|
}
|
|
2964
2968
|
}
|
|
2965
2969
|
else {
|
|
@@ -3012,7 +3016,7 @@ export class AUNClient {
|
|
|
3012
3016
|
_scheduleRetryPendingDecryptMsgs(groupId) {
|
|
3013
3017
|
if (!groupId || !this._pendingDecryptMsgs.has(`group:${groupId}`))
|
|
3014
3018
|
return;
|
|
3015
|
-
this._retryPendingDecryptMsgs(groupId).catch((exc) => _clientLog(
|
|
3019
|
+
this._retryPendingDecryptMsgs(groupId).catch((exc) => this._clientLog.warn(`群 ${groupId} pending 消息重试失败: ${formatCaughtError(exc)}`));
|
|
3016
3020
|
}
|
|
3017
3021
|
async _recoverGroupEpochKey(groupId, epoch, senderAid = '', timeoutMs = 5000) {
|
|
3018
3022
|
const existing = await this._groupE2ee.loadSecret(groupId, epoch);
|
|
@@ -3121,13 +3125,13 @@ export class AUNClient {
|
|
|
3121
3125
|
const myAid = this._aid || '';
|
|
3122
3126
|
const keyPair = this._keystore.loadKeyPair(myAid);
|
|
3123
3127
|
if (!keyPair?.private_key_pem) {
|
|
3124
|
-
_clientLog(
|
|
3128
|
+
this._clientLog.warn(`无法加载 AID 私钥用于 ECIES 解密: aid=${myAid}`);
|
|
3125
3129
|
return false;
|
|
3126
3130
|
}
|
|
3127
3131
|
const { eciesDecrypt } = await import('./e2ee-group.js');
|
|
3128
3132
|
const groupSecret = eciesDecrypt(keyPair.private_key_pem, encryptedBytes);
|
|
3129
3133
|
if (!groupSecret || groupSecret.length !== 32) {
|
|
3130
|
-
_clientLog(
|
|
3134
|
+
this._clientLog.warn(`服务端 epoch key ECIES 解密结果长度异常: group=${groupId} epoch=${serverEpoch} len=${groupSecret?.length ?? 0}`);
|
|
3131
3135
|
return false;
|
|
3132
3136
|
}
|
|
3133
3137
|
// 获取成员列表和 committed_rotation 用于 commitment / epoch_chain 验证
|
|
@@ -3164,7 +3168,7 @@ export class AUNClient {
|
|
|
3164
3168
|
}
|
|
3165
3169
|
catch { /* best effort */ }
|
|
3166
3170
|
if (memberAids.length === 0) {
|
|
3167
|
-
_clientLog(
|
|
3171
|
+
this._clientLog.warn(`服务端 epoch key 恢复缺少成员快照: group=${groupId} epoch=${serverEpoch}`);
|
|
3168
3172
|
return false;
|
|
3169
3173
|
}
|
|
3170
3174
|
const commitment = computeMembershipCommitment(memberAids, serverEpoch, groupId, groupSecret);
|
|
@@ -3175,7 +3179,7 @@ export class AUNClient {
|
|
|
3175
3179
|
const committedEpoch = Number(committedRotation.target_epoch ?? serverEpoch);
|
|
3176
3180
|
const committedCommitment = String(committedRotation.key_commitment ?? '').trim();
|
|
3177
3181
|
if (committedEpoch === serverEpoch && committedCommitment && committedCommitment !== commitment) {
|
|
3178
|
-
_clientLog(
|
|
3182
|
+
this._clientLog.warn(`服务端 epoch key 恢复 commitment 不匹配: group=${groupId} epoch=${serverEpoch}`);
|
|
3179
3183
|
return false;
|
|
3180
3184
|
}
|
|
3181
3185
|
if (epochChain && committedEpoch === serverEpoch) {
|
|
@@ -3191,7 +3195,7 @@ export class AUNClient {
|
|
|
3191
3195
|
const prevChain = String(prevData?.epoch_chain ?? '').trim();
|
|
3192
3196
|
if (prevChain && rotatorAid) {
|
|
3193
3197
|
if (!verifyEpochChain(epochChain, prevChain, serverEpoch, commitment, rotatorAid)) {
|
|
3194
|
-
_clientLog(
|
|
3198
|
+
this._clientLog.warn(`服务端 epoch key 恢复 epoch_chain 验证失败: group=${groupId} epoch=${serverEpoch} rotator=${rotatorAid}`);
|
|
3195
3199
|
return false;
|
|
3196
3200
|
}
|
|
3197
3201
|
epochChainUnverified = false;
|
|
@@ -3204,14 +3208,14 @@ export class AUNClient {
|
|
|
3204
3208
|
}
|
|
3205
3209
|
const stored = storeGroupSecretEpoch(this._keystore, myAid, groupId, serverEpoch, groupSecret, commitment, memberAids, epochChain || undefined, '', epochChainUnverified, epochChainUnverifiedReason);
|
|
3206
3210
|
if (!stored) {
|
|
3207
|
-
_clientLog(
|
|
3211
|
+
this._clientLog.warn(`服务端 epoch key 恢复存储失败: group=${groupId} epoch=${serverEpoch}`);
|
|
3208
3212
|
return false;
|
|
3209
3213
|
}
|
|
3210
|
-
_clientLog(
|
|
3214
|
+
this._clientLog.info(`从服务端恢复 epoch key 成功: group=${groupId} epoch=${serverEpoch}`);
|
|
3211
3215
|
return true;
|
|
3212
3216
|
}
|
|
3213
3217
|
catch (exc) {
|
|
3214
|
-
_clientLog(
|
|
3218
|
+
this._clientLog.debug(`从服务端恢复 epoch key 失败: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
|
|
3215
3219
|
return false;
|
|
3216
3220
|
}
|
|
3217
3221
|
}
|
|
@@ -3238,7 +3242,7 @@ export class AUNClient {
|
|
|
3238
3242
|
groupSecretBytes = loaded.secret;
|
|
3239
3243
|
}
|
|
3240
3244
|
else {
|
|
3241
|
-
_clientLog(
|
|
3245
|
+
this._clientLog.debug(`无法获取 group_secret 用于 ECIES 加密: group=${groupId} epoch=${targetEpoch}`);
|
|
3242
3246
|
return {};
|
|
3243
3247
|
}
|
|
3244
3248
|
}
|
|
@@ -3259,14 +3263,14 @@ export class AUNClient {
|
|
|
3259
3263
|
encryptedKeys[aid] = ciphertext.toString('base64');
|
|
3260
3264
|
}
|
|
3261
3265
|
catch (exc) {
|
|
3262
|
-
_clientLog(
|
|
3266
|
+
this._clientLog.debug(`为成员 ${aid} 构建 ECIES epoch key 失败: ${formatCaughtError(exc)}`);
|
|
3263
3267
|
continue;
|
|
3264
3268
|
}
|
|
3265
3269
|
}
|
|
3266
3270
|
return encryptedKeys;
|
|
3267
3271
|
}
|
|
3268
3272
|
catch (exc) {
|
|
3269
|
-
_clientLog(
|
|
3273
|
+
this._clientLog.debug(`构建 encrypted_keys 失败: group=${groupId} err=${formatCaughtError(exc)}`);
|
|
3270
3274
|
return {};
|
|
3271
3275
|
}
|
|
3272
3276
|
}
|
|
@@ -3304,11 +3308,11 @@ export class AUNClient {
|
|
|
3304
3308
|
}
|
|
3305
3309
|
}
|
|
3306
3310
|
catch {
|
|
3307
|
-
_clientLog(
|
|
3311
|
+
this._clientLog.debug(`群 ${groupId} 查询在线成员失败,回退全量候选`);
|
|
3308
3312
|
}
|
|
3309
3313
|
if (onlineAids !== null) {
|
|
3310
3314
|
if (onlineAids.length === 0) {
|
|
3311
|
-
_clientLog(
|
|
3315
|
+
this._clientLog.info(`群 ${groupId} epoch ${String(epoch)} 恢复失败:无在线成员可请求密钥`);
|
|
3312
3316
|
return false;
|
|
3313
3317
|
}
|
|
3314
3318
|
await this._requestGroupKeyFromOnline(groupId, epoch, onlineAids, epochResult);
|
|
@@ -3389,7 +3393,7 @@ export class AUNClient {
|
|
|
3389
3393
|
if (senderAid) {
|
|
3390
3394
|
const certOk = await this._ensureSenderCertCached(senderAid);
|
|
3391
3395
|
if (!certOk) {
|
|
3392
|
-
_clientLog(
|
|
3396
|
+
this._clientLog.warn(`群消息解密跳过:发送方 ${senderAid} 证书不可用`);
|
|
3393
3397
|
return message;
|
|
3394
3398
|
}
|
|
3395
3399
|
}
|
|
@@ -3416,7 +3420,7 @@ export class AUNClient {
|
|
|
3416
3420
|
}
|
|
3417
3421
|
}
|
|
3418
3422
|
catch (exc) {
|
|
3419
|
-
_clientLog(
|
|
3423
|
+
this._clientLog.debug(`群 ${groupId} epoch ${epoch} 同步恢复失败: ${formatCaughtError(exc)}`);
|
|
3420
3424
|
}
|
|
3421
3425
|
}
|
|
3422
3426
|
return message;
|
|
@@ -3537,7 +3541,7 @@ export class AUNClient {
|
|
|
3537
3541
|
if (fromAid) {
|
|
3538
3542
|
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
3539
3543
|
if (!certReady) {
|
|
3540
|
-
_clientLog(
|
|
3544
|
+
this._clientLog.warn(`无法获取发送方 ${fromAid} 的证书,跳过 message.thought.get 解密`);
|
|
3541
3545
|
decryptFailed = true;
|
|
3542
3546
|
}
|
|
3543
3547
|
}
|
|
@@ -3622,7 +3626,7 @@ export class AUNClient {
|
|
|
3622
3626
|
}
|
|
3623
3627
|
else {
|
|
3624
3628
|
failed.push(String(dist.to));
|
|
3625
|
-
_clientLog(
|
|
3629
|
+
this._clientLog.warn(`epoch 密钥分发失败 (to=${dist.to}): ${formatCaughtError(exc)}`);
|
|
3626
3630
|
}
|
|
3627
3631
|
}
|
|
3628
3632
|
}
|
|
@@ -3640,7 +3644,7 @@ export class AUNClient {
|
|
|
3640
3644
|
return isJsonObject(result) && result.success === true;
|
|
3641
3645
|
}
|
|
3642
3646
|
catch (exc) {
|
|
3643
|
-
_clientLog(
|
|
3647
|
+
this._clientLog.warn(`刷新 epoch rotation lease 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3644
3648
|
return false;
|
|
3645
3649
|
}
|
|
3646
3650
|
}
|
|
@@ -3656,7 +3660,7 @@ export class AUNClient {
|
|
|
3656
3660
|
return isJsonObject(result) && result.success === true;
|
|
3657
3661
|
}
|
|
3658
3662
|
catch (exc) {
|
|
3659
|
-
_clientLog(
|
|
3663
|
+
this._clientLog.warn(`提交 epoch key ack 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3660
3664
|
return false;
|
|
3661
3665
|
}
|
|
3662
3666
|
}
|
|
@@ -3684,7 +3688,7 @@ export class AUNClient {
|
|
|
3684
3688
|
? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
3685
3689
|
: [];
|
|
3686
3690
|
if (this._aid && !expectedMembers.includes(this._aid)) {
|
|
3687
|
-
_clientLog(
|
|
3691
|
+
this._clientLog.debug(`放行 group key 分发:新成员恢复 commitment 不匹配属正常 group=${groupId} epoch=${epoch}`);
|
|
3688
3692
|
}
|
|
3689
3693
|
else {
|
|
3690
3694
|
return false;
|
|
@@ -3693,7 +3697,7 @@ export class AUNClient {
|
|
|
3693
3697
|
}
|
|
3694
3698
|
return true;
|
|
3695
3699
|
}
|
|
3696
|
-
_clientLog(
|
|
3700
|
+
this._clientLog.info(`拒绝缺少 rotation_id 的未来 epoch key 分发: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
|
|
3697
3701
|
return false;
|
|
3698
3702
|
}
|
|
3699
3703
|
const pending = isJsonObject(epochResult.pending_rotation) ? epochResult.pending_rotation : null;
|
|
@@ -3714,10 +3718,10 @@ export class AUNClient {
|
|
|
3714
3718
|
}
|
|
3715
3719
|
}
|
|
3716
3720
|
catch (exc) {
|
|
3717
|
-
_clientLog(
|
|
3721
|
+
this._clientLog.warn(`拒绝无法校验 active rotation 的 epoch key 分发: group=${groupId} rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3718
3722
|
return false;
|
|
3719
3723
|
}
|
|
3720
|
-
_clientLog(
|
|
3724
|
+
this._clientLog.info(`拒绝非 pending/committed 状态的 epoch key 分发: group=${groupId} rotation=${rotationId} epoch=${epoch}`);
|
|
3721
3725
|
return false;
|
|
3722
3726
|
}
|
|
3723
3727
|
async _discardGroupDistributionIfStale(payload) {
|
|
@@ -3732,10 +3736,10 @@ export class AUNClient {
|
|
|
3732
3736
|
return;
|
|
3733
3737
|
try {
|
|
3734
3738
|
this._groupE2ee.discardPendingSecret(groupId, epoch, rotationId);
|
|
3735
|
-
_clientLog(
|
|
3739
|
+
this._clientLog.info(`丢弃 verify 后变为 stale 的 group epoch key: group=${groupId} epoch=${epoch} rotation=${rotationId}`);
|
|
3736
3740
|
}
|
|
3737
3741
|
catch (exc) {
|
|
3738
|
-
_clientLog(
|
|
3742
|
+
this._clientLog.debug(`清理 stale group epoch key 失败: group=${groupId} epoch=${epoch} rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3739
3743
|
}
|
|
3740
3744
|
}
|
|
3741
3745
|
async _verifyGroupKeyResponseEpoch(payload) {
|
|
@@ -3752,7 +3756,7 @@ export class AUNClient {
|
|
|
3752
3756
|
return false;
|
|
3753
3757
|
const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
3754
3758
|
if (epoch > committedEpoch) {
|
|
3755
|
-
_clientLog(
|
|
3759
|
+
this._clientLog.info(`拒绝未提交 epoch 的 group key response: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
|
|
3756
3760
|
return false;
|
|
3757
3761
|
}
|
|
3758
3762
|
const committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
|
|
@@ -3763,7 +3767,7 @@ export class AUNClient {
|
|
|
3763
3767
|
? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
3764
3768
|
: [];
|
|
3765
3769
|
if (this._aid && !expectedMembers.includes(this._aid)) {
|
|
3766
|
-
_clientLog(
|
|
3770
|
+
this._clientLog.debug(`放行 group key response:新成员恢复 commitment 不匹配属正常 group=${groupId} epoch=${epoch}`);
|
|
3767
3771
|
}
|
|
3768
3772
|
else {
|
|
3769
3773
|
return false;
|
|
@@ -3773,7 +3777,7 @@ export class AUNClient {
|
|
|
3773
3777
|
return true;
|
|
3774
3778
|
}
|
|
3775
3779
|
catch (exc) {
|
|
3776
|
-
_clientLog(
|
|
3780
|
+
this._clientLog.warn(`拒绝无法校验 committed epoch 的 group key response: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
|
|
3777
3781
|
return false;
|
|
3778
3782
|
}
|
|
3779
3783
|
}
|
|
@@ -3788,7 +3792,7 @@ export class AUNClient {
|
|
|
3788
3792
|
return isJsonObject(result) && result.success === true;
|
|
3789
3793
|
}
|
|
3790
3794
|
catch (exc) {
|
|
3791
|
-
_clientLog(
|
|
3795
|
+
this._clientLog.warn(`中止 epoch rotation 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3792
3796
|
return false;
|
|
3793
3797
|
}
|
|
3794
3798
|
}
|
|
@@ -3822,7 +3826,7 @@ export class AUNClient {
|
|
|
3822
3826
|
if (this._closing || this._state !== 'connected')
|
|
3823
3827
|
return;
|
|
3824
3828
|
this._maybeLeadRotateGroupEpoch(groupId, opts.triggerId, opts.expectedEpoch)
|
|
3825
|
-
.catch((exc) => _clientLog(
|
|
3829
|
+
.catch((exc) => this._clientLog.warn(`group epoch rotation retry failed: ${formatCaughtError(exc)}`));
|
|
3826
3830
|
}, this._rotationRetryDelayMs(opts.pending));
|
|
3827
3831
|
this._groupEpochRotationRetryTimers.set(retryKey, timer);
|
|
3828
3832
|
this._unrefTimer(timer);
|
|
@@ -3834,7 +3838,7 @@ export class AUNClient {
|
|
|
3834
3838
|
if (this._closing || this._state !== 'connected')
|
|
3835
3839
|
return;
|
|
3836
3840
|
if (Date.now() - started > 20000) {
|
|
3837
|
-
_clientLog(
|
|
3841
|
+
this._clientLog.warn(`group epoch create sync still in-flight; skip duplicate sync (group=${groupId})`);
|
|
3838
3842
|
return;
|
|
3839
3843
|
}
|
|
3840
3844
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
@@ -3867,12 +3871,12 @@ export class AUNClient {
|
|
|
3867
3871
|
const beginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
|
|
3868
3872
|
const rotation = isJsonObject(beginResult) && isJsonObject(beginResult.rotation) ? beginResult.rotation : null;
|
|
3869
3873
|
if (!isJsonObject(beginResult) || beginResult.success !== true || !rotation) {
|
|
3870
|
-
_clientLog(
|
|
3874
|
+
this._clientLog.warn(`group epoch begin failed; stop key distribution (group=${groupId}, returned=${JSON.stringify(beginResult)})`);
|
|
3871
3875
|
return;
|
|
3872
3876
|
}
|
|
3873
3877
|
const activeRotationId = String(rotation.rotation_id ?? rotationId);
|
|
3874
3878
|
if (!await this._ackGroupRotationKey(activeRotationId, secretData.commitment)) {
|
|
3875
|
-
_clientLog(
|
|
3879
|
+
this._clientLog.warn(`group epoch self ack failed (group=${groupId}, rotation=${activeRotationId})`);
|
|
3876
3880
|
await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
|
|
3877
3881
|
return;
|
|
3878
3882
|
}
|
|
@@ -3889,17 +3893,17 @@ export class AUNClient {
|
|
|
3889
3893
|
storeGroupSecret(this._keystore, this._aid, groupId, 1, secretData.secret, secretData.commitment, secretData.member_aids, secretData.epoch_chain);
|
|
3890
3894
|
return;
|
|
3891
3895
|
}
|
|
3892
|
-
_clientLog(
|
|
3896
|
+
this._clientLog.warn(`group epoch commit failed (group=${groupId}, returned=${JSON.stringify(commitResult)})`);
|
|
3893
3897
|
return;
|
|
3894
3898
|
}
|
|
3895
3899
|
catch (exc) {
|
|
3896
3900
|
if (attempt < maxRetries) {
|
|
3897
3901
|
const delay = 500 * Math.pow(2, attempt - 1);
|
|
3898
|
-
_clientLog(
|
|
3902
|
+
this._clientLog.warn(`同步 epoch 到服务端失败 (group=${groupId}, 第${attempt}/${maxRetries}次): ${formatCaughtError(exc)}, ${delay}ms后重试`);
|
|
3899
3903
|
await new Promise(r => setTimeout(r, delay));
|
|
3900
3904
|
}
|
|
3901
3905
|
else {
|
|
3902
|
-
_clientLog(
|
|
3906
|
+
this._clientLog.error(`同步 epoch 到服务端最终失败 (group=${groupId}, 已重试${maxRetries}次): ${formatCaughtError(exc)}`, exc instanceof Error ? exc : undefined);
|
|
3903
3907
|
}
|
|
3904
3908
|
}
|
|
3905
3909
|
}
|
|
@@ -3930,7 +3934,7 @@ export class AUNClient {
|
|
|
3930
3934
|
&& serverEpoch === expectedEpoch
|
|
3931
3935
|
&& this._rotationExpectedMembersStale(pendingRotation, memberAids));
|
|
3932
3936
|
if (stalePending && await this._abortGroupRotation(pendingRotationId, 'membership_changed_during_rotation')) {
|
|
3933
|
-
_clientLog(
|
|
3937
|
+
this._clientLog.info(`aborted stale pending group epoch rotation: group=${groupId} rotation=${pendingRotationId || '-'}`);
|
|
3934
3938
|
}
|
|
3935
3939
|
else {
|
|
3936
3940
|
this._scheduleGroupRotationRetry(groupId, {
|
|
@@ -3945,7 +3949,7 @@ export class AUNClient {
|
|
|
3945
3949
|
if (expectedEpoch !== null && serverEpoch !== expectedEpoch) {
|
|
3946
3950
|
if (triggerId)
|
|
3947
3951
|
this._groupMembershipRotationDone.add(triggerId);
|
|
3948
|
-
_clientLog(
|
|
3952
|
+
this._clientLog.info(`skip membership epoch rotation: group=${groupId} expected_epoch=${expectedEpoch} server_epoch=${serverEpoch} trigger=${triggerId || '-'}`);
|
|
3949
3953
|
return;
|
|
3950
3954
|
}
|
|
3951
3955
|
const currentEpoch = expectedEpoch ?? serverEpoch;
|
|
@@ -3961,7 +3965,7 @@ export class AUNClient {
|
|
|
3961
3965
|
const rawChain = String(cr.epoch_chain ?? '').trim();
|
|
3962
3966
|
if (rawChain) {
|
|
3963
3967
|
prevChainHint = rawChain;
|
|
3964
|
-
_clientLog(
|
|
3968
|
+
this._clientLog.info(`新成员轮换补充 prev epoch chain from server: group=${groupId} epoch=${currentEpoch}`);
|
|
3965
3969
|
}
|
|
3966
3970
|
}
|
|
3967
3971
|
}
|
|
@@ -3973,7 +3977,7 @@ export class AUNClient {
|
|
|
3973
3977
|
this._groupE2ee.discardPendingSecret(groupId, targetEpoch, rotationId);
|
|
3974
3978
|
}
|
|
3975
3979
|
catch (cleanupExc) {
|
|
3976
|
-
_clientLog(
|
|
3980
|
+
this._clientLog.debug(`清理本地 pending group key 失败: group=${groupId} epoch=${targetEpoch} rotation=${rotationId} err=${formatCaughtError(cleanupExc)}`);
|
|
3977
3981
|
}
|
|
3978
3982
|
};
|
|
3979
3983
|
const rotateParams = {
|
|
@@ -4027,14 +4031,14 @@ export class AUNClient {
|
|
|
4027
4031
|
pending: null,
|
|
4028
4032
|
});
|
|
4029
4033
|
}
|
|
4030
|
-
_clientLog(
|
|
4034
|
+
this._clientLog.warn(`group epoch begin failed; stop key distribution (group=${groupId}, current_epoch=${currentEpoch}, returned=${JSON.stringify(beginResult)})`);
|
|
4031
4035
|
discardGeneratedPending();
|
|
4032
4036
|
return;
|
|
4033
4037
|
}
|
|
4034
4038
|
const activeRotationId = String(rotation.rotation_id ?? rotationId);
|
|
4035
4039
|
const distributeResult = await this._distributeGroupEpochKey(info, activeRotationId);
|
|
4036
4040
|
if (distributeResult.failed.length > 0) {
|
|
4037
|
-
_clientLog(
|
|
4041
|
+
this._clientLog.warn(`group epoch key distribution incomplete; abort rotation before retry (group=${groupId} rotation=${activeRotationId} failed=${distributeResult.failed.join(',')})`);
|
|
4038
4042
|
await this._abortGroupRotation(activeRotationId, 'distribution_failed');
|
|
4039
4043
|
this._scheduleGroupRotationRetry(groupId, {
|
|
4040
4044
|
reason: 'membership_changed',
|
|
@@ -4047,7 +4051,7 @@ export class AUNClient {
|
|
|
4047
4051
|
}
|
|
4048
4052
|
await this._heartbeatGroupRotation(activeRotationId);
|
|
4049
4053
|
if (!await this._ackGroupRotationKey(activeRotationId, String(info.commitment ?? ''))) {
|
|
4050
|
-
_clientLog(
|
|
4054
|
+
this._clientLog.warn(`group epoch self ack failed; abort rotation before retry (group=${groupId} rotation=${activeRotationId})`);
|
|
4051
4055
|
await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
|
|
4052
4056
|
this._scheduleGroupRotationRetry(groupId, {
|
|
4053
4057
|
reason: 'membership_changed',
|
|
@@ -4068,7 +4072,7 @@ export class AUNClient {
|
|
|
4068
4072
|
}
|
|
4069
4073
|
const commitResult = await this.call('group.e2ee.commit_rotation', commitParams);
|
|
4070
4074
|
if (!isJsonObject(commitResult) || commitResult.success !== true) {
|
|
4071
|
-
_clientLog(
|
|
4075
|
+
this._clientLog.warn(`group epoch commit failed (group=${groupId}, rotation=${activeRotationId}, returned=${JSON.stringify(commitResult)})`);
|
|
4072
4076
|
this._scheduleGroupRotationRetry(groupId, {
|
|
4073
4077
|
reason: 'membership_changed',
|
|
4074
4078
|
triggerId,
|
|
@@ -4092,7 +4096,7 @@ export class AUNClient {
|
|
|
4092
4096
|
storeGroupSecret(this._keystore, this._aid, groupId, targetEpoch, committedSecret.secret, committedSecret.commitment, committedSecret.member_aids.length > 0 ? committedSecret.member_aids : memberAids, committedSecret.epoch_chain);
|
|
4093
4097
|
}
|
|
4094
4098
|
else {
|
|
4095
|
-
_clientLog(
|
|
4099
|
+
this._clientLog.warn(`group epoch commit succeeded but local target key does not match committed rotation; keep pending blocked (group=${groupId} rotation=${activeRotationId} epoch=${targetEpoch})`);
|
|
4096
4100
|
}
|
|
4097
4101
|
}
|
|
4098
4102
|
if (triggerId) {
|
|
@@ -4285,8 +4289,9 @@ export class AUNClient {
|
|
|
4285
4289
|
// 优先从 seq_tracker 表按行读取
|
|
4286
4290
|
const loadAll = this._keystore.loadAllSeqs;
|
|
4287
4291
|
if (typeof loadAll === 'function') {
|
|
4288
|
-
|
|
4292
|
+
let state = loadAll.call(this._keystore, this._aid, this._deviceId, this._slotId);
|
|
4289
4293
|
if (state && Object.keys(state).length > 0) {
|
|
4294
|
+
state = this._migrateSeqStateGroupIds(state);
|
|
4290
4295
|
this._seqTracker.restoreState(state);
|
|
4291
4296
|
return;
|
|
4292
4297
|
}
|
|
@@ -4296,12 +4301,14 @@ export class AUNClient {
|
|
|
4296
4301
|
if (typeof loader === 'function') {
|
|
4297
4302
|
const instanceState = loader.call(this._keystore, this._aid, this._deviceId, this._slotId);
|
|
4298
4303
|
if (instanceState && typeof instanceState.seq_tracker_state === 'object') {
|
|
4299
|
-
|
|
4304
|
+
let state = instanceState.seq_tracker_state;
|
|
4305
|
+
state = this._migrateSeqStateGroupIds(state);
|
|
4306
|
+
this._seqTracker.restoreState(state);
|
|
4300
4307
|
}
|
|
4301
4308
|
}
|
|
4302
4309
|
}
|
|
4303
4310
|
catch (exc) {
|
|
4304
|
-
_clientLog(
|
|
4311
|
+
this._clientLog.warn(`恢复 SeqTracker 状态失败: ${formatCaughtError(exc)}`);
|
|
4305
4312
|
// 通过内部 dispatcher 发布可观测事件,便于上层监控
|
|
4306
4313
|
this._dispatcher.publish('seq_tracker.persist_error', {
|
|
4307
4314
|
phase: 'restore',
|
|
@@ -4312,6 +4319,59 @@ export class AUNClient {
|
|
|
4312
4319
|
}).catch(() => { });
|
|
4313
4320
|
}
|
|
4314
4321
|
}
|
|
4322
|
+
/**
|
|
4323
|
+
* 把 seq_tracker state 里 group_event:/group_msg: 前缀的老/污染 group_id 归一化为 canonical。
|
|
4324
|
+
* 冲突取 max。同时落盘删除老 ns、写入新 ns,避免下次启动重复迁移。
|
|
4325
|
+
*/
|
|
4326
|
+
_migrateSeqStateGroupIds(state) {
|
|
4327
|
+
if (!state || Object.keys(state).length === 0)
|
|
4328
|
+
return state;
|
|
4329
|
+
const renameMap = {};
|
|
4330
|
+
for (const ns of Object.keys(state)) {
|
|
4331
|
+
for (const prefix of ['group_event:', 'group_msg:']) {
|
|
4332
|
+
if (ns.startsWith(prefix)) {
|
|
4333
|
+
const oldGid = ns.slice(prefix.length);
|
|
4334
|
+
const newGid = normalizeGroupId(oldGid);
|
|
4335
|
+
if (newGid && newGid !== oldGid) {
|
|
4336
|
+
renameMap[ns] = `${prefix}${newGid}`;
|
|
4337
|
+
}
|
|
4338
|
+
break;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
if (Object.keys(renameMap).length === 0)
|
|
4343
|
+
return state;
|
|
4344
|
+
const newState = { ...state };
|
|
4345
|
+
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
4346
|
+
const oldVal = Number(newState[oldNs] ?? 0);
|
|
4347
|
+
const curVal = Number(newState[newNs] ?? 0);
|
|
4348
|
+
delete newState[oldNs];
|
|
4349
|
+
newState[newNs] = Math.max(oldVal, curVal);
|
|
4350
|
+
}
|
|
4351
|
+
this._clientLog.info(`SeqTracker group_id 迁移:${Object.keys(renameMap).length} 个命名空间重写`);
|
|
4352
|
+
// 落盘
|
|
4353
|
+
const saver = this._keystore.saveSeq;
|
|
4354
|
+
const deleter = this._keystore.deleteSeq;
|
|
4355
|
+
if (typeof saver === 'function' && this._aid) {
|
|
4356
|
+
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
4357
|
+
if (typeof deleter === 'function') {
|
|
4358
|
+
try {
|
|
4359
|
+
deleter.call(this._keystore, this._aid, this._deviceId, this._slotId, oldNs);
|
|
4360
|
+
}
|
|
4361
|
+
catch (e) {
|
|
4362
|
+
this._clientLog.debug(`删除旧 seq ns 失败: ns=${oldNs} err=${formatCaughtError(e)}`);
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
try {
|
|
4366
|
+
saver.call(this._keystore, this._aid, this._deviceId, this._slotId, newNs, newState[newNs]);
|
|
4367
|
+
}
|
|
4368
|
+
catch (e) {
|
|
4369
|
+
this._clientLog.debug(`写入新 seq ns 失败: ns=${newNs} err=${formatCaughtError(e)}`);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
return newState;
|
|
4374
|
+
}
|
|
4315
4375
|
_currentSeqTrackerContext() {
|
|
4316
4376
|
if (!this._aid)
|
|
4317
4377
|
return null;
|
|
@@ -4366,7 +4426,7 @@ export class AUNClient {
|
|
|
4366
4426
|
}
|
|
4367
4427
|
}
|
|
4368
4428
|
catch (exc) {
|
|
4369
|
-
_clientLog(
|
|
4429
|
+
this._clientLog.warn(`保存 SeqTracker 状态失败: ${formatCaughtError(exc)}`);
|
|
4370
4430
|
// 通过内部 dispatcher 发布可观测事件,便于上层监控
|
|
4371
4431
|
this._dispatcher.publish('seq_tracker.persist_error', {
|
|
4372
4432
|
phase: 'save',
|
|
@@ -4448,8 +4508,8 @@ export class AUNClient {
|
|
|
4448
4508
|
if (identity && isJsonObject(identity)) {
|
|
4449
4509
|
this._identity = identity;
|
|
4450
4510
|
this._aid = String(identity.aid ?? this._aid ?? '');
|
|
4451
|
-
if (
|
|
4452
|
-
|
|
4511
|
+
if (this._aid)
|
|
4512
|
+
this._logger.bindAid(this._aid);
|
|
4453
4513
|
if (this._sessionParams !== null) {
|
|
4454
4514
|
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
4455
4515
|
}
|
|
@@ -4478,7 +4538,7 @@ export class AUNClient {
|
|
|
4478
4538
|
await this._uploadPrekey();
|
|
4479
4539
|
}
|
|
4480
4540
|
catch (exc) {
|
|
4481
|
-
_clientLog(
|
|
4541
|
+
this._clientLog.warn(`prekey 上传失败: ${formatCaughtError(exc)}`);
|
|
4482
4542
|
}
|
|
4483
4543
|
}
|
|
4484
4544
|
catch (err) {
|
|
@@ -4515,8 +4575,8 @@ export class AUNClient {
|
|
|
4515
4575
|
identity.access_token = accessToken;
|
|
4516
4576
|
this._identity = identity;
|
|
4517
4577
|
this._aid = String(identity.aid ?? this._aid ?? '');
|
|
4518
|
-
if (
|
|
4519
|
-
|
|
4578
|
+
if (this._aid)
|
|
4579
|
+
this._logger.bindAid(this._aid);
|
|
4520
4580
|
const persistIdentity = this._auth._persistIdentity;
|
|
4521
4581
|
if (typeof persistIdentity === 'function') {
|
|
4522
4582
|
persistIdentity.call(this._auth, identity);
|
|
@@ -4648,10 +4708,10 @@ export class AUNClient {
|
|
|
4648
4708
|
consecutiveFailures = 0;
|
|
4649
4709
|
}).catch((exc) => {
|
|
4650
4710
|
consecutiveFailures++;
|
|
4651
|
-
_clientLog(
|
|
4711
|
+
this._clientLog.warn(`心跳失败 (${consecutiveFailures}/${maxFailures}): ${formatCaughtError(exc)}`);
|
|
4652
4712
|
this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) }).catch(() => { });
|
|
4653
4713
|
if (consecutiveFailures >= maxFailures) {
|
|
4654
|
-
_clientLog(
|
|
4714
|
+
this._clientLog.warn(`连续 ${maxFailures} 次心跳失败,触发断线重连`);
|
|
4655
4715
|
this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
|
|
4656
4716
|
}
|
|
4657
4717
|
});
|
|
@@ -4715,7 +4775,7 @@ export class AUNClient {
|
|
|
4715
4775
|
if (exc instanceof AuthError) {
|
|
4716
4776
|
this._tokenRefreshFailures++;
|
|
4717
4777
|
if (this._tokenRefreshFailures >= 3) {
|
|
4718
|
-
_clientLog(
|
|
4778
|
+
this._clientLog.warn(`token 刷新连续失败 ${this._tokenRefreshFailures} 次,停止刷新循环并触发重连`);
|
|
4719
4779
|
await this._dispatcher.publish('token.refresh_exhausted', {
|
|
4720
4780
|
aid: this._identity?.aid ?? null,
|
|
4721
4781
|
consecutive_failures: this._tokenRefreshFailures,
|
|
@@ -4725,7 +4785,7 @@ export class AUNClient {
|
|
|
4725
4785
|
this._handleTransportDisconnect(new Error('token refresh exhausted, triggering reconnect'));
|
|
4726
4786
|
return;
|
|
4727
4787
|
}
|
|
4728
|
-
_clientLog(
|
|
4788
|
+
this._clientLog.debug(`token 刷新失败 (${this._tokenRefreshFailures}/3),下次重试: ${exc}`);
|
|
4729
4789
|
}
|
|
4730
4790
|
else {
|
|
4731
4791
|
await this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
|
|
@@ -4827,7 +4887,7 @@ export class AUNClient {
|
|
|
4827
4887
|
this._prekeyReplenished.add(prekeyId);
|
|
4828
4888
|
}
|
|
4829
4889
|
catch (exc) {
|
|
4830
|
-
_clientLog(
|
|
4890
|
+
this._clientLog.warn(`消费 prekey ${prekeyId} 后补充 current prekey 失败: ${formatCaughtError(exc)}`);
|
|
4831
4891
|
}
|
|
4832
4892
|
finally {
|
|
4833
4893
|
this._prekeyReplenishInflight.delete(prekeyId);
|
|
@@ -4852,7 +4912,7 @@ export class AUNClient {
|
|
|
4852
4912
|
}
|
|
4853
4913
|
}
|
|
4854
4914
|
catch (exc) {
|
|
4855
|
-
_clientLog(
|
|
4915
|
+
this._clientLog.warn(`epoch 清理失败: ${formatCaughtError(exc)}`);
|
|
4856
4916
|
}
|
|
4857
4917
|
}, 3600_000);
|
|
4858
4918
|
this._unrefTimer(this._groupEpochCleanupTimer);
|
|
@@ -4868,11 +4928,11 @@ export class AUNClient {
|
|
|
4868
4928
|
? this._keystore.listGroupSecretIds(this._aid)
|
|
4869
4929
|
: [];
|
|
4870
4930
|
for (const gid of groupIds) {
|
|
4871
|
-
this._maybeLeadRotateGroupEpoch(gid).catch((exc) => _clientLog(
|
|
4931
|
+
this._maybeLeadRotateGroupEpoch(gid).catch((exc) => this._clientLog.warn(`epoch 轮换失败: ${formatCaughtError(exc)}`));
|
|
4872
4932
|
}
|
|
4873
4933
|
}
|
|
4874
4934
|
catch (exc) {
|
|
4875
|
-
_clientLog(
|
|
4935
|
+
this._clientLog.warn(`epoch 轮换失败: ${formatCaughtError(exc)}`);
|
|
4876
4936
|
}
|
|
4877
4937
|
}, rotateInterval * 1000);
|
|
4878
4938
|
this._unrefTimer(this._groupEpochRotateTimer);
|
|
@@ -4920,7 +4980,7 @@ export class AUNClient {
|
|
|
4920
4980
|
_onGatewayDisconnect(data) {
|
|
4921
4981
|
const code = data?.code;
|
|
4922
4982
|
const reason = data?.reason ?? '';
|
|
4923
|
-
_clientLog(
|
|
4983
|
+
this._clientLog.warn(`服务端主动断开: code=${code}, reason=${reason}`);
|
|
4924
4984
|
this._serverKicked = true;
|
|
4925
4985
|
}
|
|
4926
4986
|
/** 传输层断线回调 */
|
|
@@ -4941,7 +5001,7 @@ export class AUNClient {
|
|
|
4941
5001
|
if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
|
|
4942
5002
|
this._state = 'terminal_failed';
|
|
4943
5003
|
const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
|
|
4944
|
-
_clientLog(
|
|
5004
|
+
this._clientLog.warn(`抑制自动重连: ${reason}`);
|
|
4945
5005
|
await this._dispatcher.publish('connection.state', {
|
|
4946
5006
|
state: this._state, error, reason,
|
|
4947
5007
|
});
|
|
@@ -4958,7 +5018,7 @@ export class AUNClient {
|
|
|
4958
5018
|
this._reconnectActive = true;
|
|
4959
5019
|
this._reconnectAbort = new AbortController();
|
|
4960
5020
|
this._reconnectLoop(serverInitiated).catch((exc) => {
|
|
4961
|
-
_clientLog(
|
|
5021
|
+
this._clientLog.warn(`重连循环异常: ${formatCaughtError(exc)}`);
|
|
4962
5022
|
});
|
|
4963
5023
|
}
|
|
4964
5024
|
/** 重连循环(for 循环 + AbortController,与 JS/Python 对齐) */
|