@agentunion/fastaun-browser 0.2.17 → 0.2.19

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.
Files changed (58) hide show
  1. package/dist/auth.d.ts +12 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +370 -215
  4. package/dist/auth.js.map +1 -1
  5. package/dist/client.d.ts +24 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +1307 -849
  8. package/dist/client.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +3 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/discovery.d.ts +3 -0
  13. package/dist/discovery.d.ts.map +1 -1
  14. package/dist/discovery.js +15 -1
  15. package/dist/discovery.js.map +1 -1
  16. package/dist/e2ee-group.d.ts +4 -0
  17. package/dist/e2ee-group.d.ts.map +1 -1
  18. package/dist/e2ee-group.js +327 -201
  19. package/dist/e2ee-group.js.map +1 -1
  20. package/dist/e2ee.d.ts +4 -0
  21. package/dist/e2ee.d.ts.map +1 -1
  22. package/dist/e2ee.js +196 -117
  23. package/dist/e2ee.js.map +1 -1
  24. package/dist/events.d.ts +3 -0
  25. package/dist/events.d.ts.map +1 -1
  26. package/dist/events.js +4 -1
  27. package/dist/events.js.map +1 -1
  28. package/dist/keystore/index.d.ts +11 -0
  29. package/dist/keystore/index.d.ts.map +1 -1
  30. package/dist/keystore/indexeddb.d.ts +38 -0
  31. package/dist/keystore/indexeddb.d.ts.map +1 -1
  32. package/dist/keystore/indexeddb.js +245 -98
  33. package/dist/keystore/indexeddb.js.map +1 -1
  34. package/dist/logger.d.ts +37 -0
  35. package/dist/logger.d.ts.map +1 -0
  36. package/dist/logger.js +112 -0
  37. package/dist/logger.js.map +1 -0
  38. package/dist/namespaces/auth.d.ts +13 -3
  39. package/dist/namespaces/auth.d.ts.map +1 -1
  40. package/dist/namespaces/auth.js +284 -106
  41. package/dist/namespaces/auth.js.map +1 -1
  42. package/dist/namespaces/custody.d.ts +3 -0
  43. package/dist/namespaces/custody.d.ts.map +1 -1
  44. package/dist/namespaces/custody.js +147 -75
  45. package/dist/namespaces/custody.js.map +1 -1
  46. package/dist/namespaces/meta.d.ts +3 -0
  47. package/dist/namespaces/meta.d.ts.map +1 -1
  48. package/dist/namespaces/meta.js +94 -43
  49. package/dist/namespaces/meta.js.map +1 -1
  50. package/dist/secret-store/indexeddb-store.d.ts +3 -0
  51. package/dist/secret-store/indexeddb-store.d.ts.map +1 -1
  52. package/dist/secret-store/indexeddb-store.js +57 -29
  53. package/dist/secret-store/indexeddb-store.js.map +1 -1
  54. package/dist/transport.d.ts +3 -0
  55. package/dist/transport.d.ts.map +1 -1
  56. package/dist/transport.js +74 -4
  57. package/dist/transport.js.map +1 -1
  58. package/package.json +37 -37
package/dist/client.js CHANGED
@@ -15,9 +15,10 @@ import { AuthNamespace } from './namespaces/auth.js';
15
15
  import { CustodyNamespace } from './namespaces/custody.js';
16
16
  import { MetaNamespace } from './namespaces/meta.js';
17
17
  import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, toBufferSource } from './crypto.js';
18
- import { E2EEManager, _certificateSha256Fingerprint as certificateSha256Fingerprint, _ecdsaVerifyDer as ecdsaVerifyDer, _importCertPublicKeyEcdsa as importCertPublicKeyEcdsa, } from './e2ee.js';
19
- import { GroupE2EEManager, computeMembershipCommitment, computeStateHash, storeGroupSecret, storeGroupSecretEpoch, buildKeyDistribution, buildKeyRequest, buildMembershipManifest, signMembershipManifest, verifyEpochChain, } from './e2ee-group.js';
18
+ import { E2EEManager, _certificateSha256Fingerprint as certificateSha256Fingerprint, _ecdsaVerifyDer as ecdsaVerifyDer, _importCertPublicKeyEcdsa as importCertPublicKeyEcdsa, setModuleLogger as setE2eeModuleLogger, } from './e2ee.js';
19
+ import { GroupE2EEManager, computeMembershipCommitment, computeStateHash, storeGroupSecret, storeGroupSecretEpoch, buildKeyDistribution, buildKeyRequest, buildMembershipManifest, signMembershipManifest, verifyEpochChain, setModuleLogger as setE2eeGroupModuleLogger, } from './e2ee-group.js';
20
20
  import { IndexedDBKeyStore } from './keystore/indexeddb.js';
21
+ import { AUNLogger } from './logger.js';
21
22
  import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, ValidationError, } from './errors.js';
22
23
  import { isJsonObject, } from './types.js';
23
24
  /**
@@ -342,6 +343,8 @@ export class AUNClient {
342
343
  _certCache = new Map();
343
344
  _prekeyReplenishInflight = new Set();
344
345
  _prekeyReplenished = new Set();
346
+ // 当前活跃 prekey:只有这个 prekey 被消费时才触发 replenish 上传
347
+ _activePrekeyId = '';
345
348
  _peerPrekeysCache = new Map();
346
349
  // 后台任务 handle(浏览器 setInterval/setTimeout)
347
350
  _heartbeatTimer = null;
@@ -377,14 +380,41 @@ export class AUNClient {
377
380
  _reconnectActive = false;
378
381
  _reconnectAbort = null;
379
382
  _serverKicked = false;
383
+ /**
384
+ * 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
385
+ * 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
386
+ */
387
+ _lastDisconnectInfo = null;
388
+ // Logger(per-client 单例 + 各模块子 logger)
389
+ _logger;
390
+ _clientLog;
391
+ _logE2;
392
+ _logEG;
393
+ _logAuth;
394
+ _logTransport;
395
+ _logKeystore;
396
+ _logDiscovery;
397
+ _logEvents;
380
398
  constructor(config, _debug = false) {
381
399
  const rawConfig = config ?? {};
382
400
  this.configModel = createConfig(rawConfig);
401
+ const initAid = String(rawConfig.aid ?? '').trim() || null;
383
402
  this.config = {
384
403
  aun_path: this.configModel.aunPath,
385
404
  root_ca_path: this.configModel.rootCaPem,
386
405
  seed_password: this.configModel.seedPassword,
387
406
  };
407
+ // Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
408
+ this._logger = new AUNLogger({ debug: _debug });
409
+ this._clientLog = this._logger.for('aun_core.client');
410
+ this._logE2 = this._logger.for('aun_core.e2ee');
411
+ this._logEG = this._logger.for('aun_core.e2ee-group');
412
+ this._logAuth = this._logger.for('aun_core.auth');
413
+ this._logTransport = this._logger.for('aun_core.transport');
414
+ this._logKeystore = this._logger.for('aun_core.keystore');
415
+ this._logDiscovery = this._logger.for('aun_core.discovery');
416
+ this._logEvents = this._logger.for('aun_core.events');
417
+ this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
388
418
  this._dispatcher = new EventDispatcher();
389
419
  this._discovery = new GatewayDiscovery();
390
420
  this._keystore = new IndexedDBKeyStore();
@@ -395,12 +425,13 @@ export class AUNClient {
395
425
  this._auth = new AuthFlow({
396
426
  keystore: this._keystore,
397
427
  crypto: new CryptoProvider(),
398
- aid: null,
428
+ aid: initAid,
399
429
  deviceId: this._deviceId,
400
430
  slotId: this._slotId,
401
431
  rootCaPem: this.configModel.rootCaPem,
402
432
  verifySsl: this.configModel.verifySsl,
403
433
  });
434
+ this._aid = initAid;
404
435
  this._transport = new RPCTransport({
405
436
  eventDispatcher: this._dispatcher,
406
437
  timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
@@ -422,6 +453,29 @@ export class AUNClient {
422
453
  this.auth = new AuthNamespace(this);
423
454
  this.custody = new CustodyNamespace(this);
424
455
  this.meta = new MetaNamespace(this);
456
+ // 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
457
+ this._auth.setLogger(this._logAuth);
458
+ this._transport.setLogger(this._logTransport);
459
+ this._dispatcher.setLogger(this._logEvents);
460
+ this._e2ee.setLogger(this._logE2);
461
+ this._groupE2ee.setLogger(this._logEG);
462
+ setE2eeModuleLogger(this._logE2);
463
+ setE2eeGroupModuleLogger(this._logEG);
464
+ if (typeof this._discovery.setLogger === 'function') {
465
+ this._discovery.setLogger(this._logger.for('aun_core.discovery'));
466
+ }
467
+ if (typeof this.auth.setLogger === 'function') {
468
+ this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
469
+ }
470
+ if (typeof this.custody.setLogger === 'function') {
471
+ this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
472
+ }
473
+ if (typeof this._keystore.setLogger === 'function') {
474
+ this._keystore.setLogger(this._logKeystore);
475
+ }
476
+ if (typeof this.meta.setLogger === 'function') {
477
+ this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
478
+ }
425
479
  // 内部订阅:推送消息自动解密后 re-publish 给用户
426
480
  this._dispatcher.subscribe('_raw.message.received', (data) => {
427
481
  this._onRawMessageReceived(data);
@@ -445,8 +499,8 @@ export class AUNClient {
445
499
  });
446
500
  }
447
501
  // 服务端主动断开通知:记录日志并标记不重连
448
- this._dispatcher.subscribe('_raw.gateway.disconnect', (data) => {
449
- this._onGatewayDisconnect(data);
502
+ this._dispatcher.subscribe('_raw.gateway.disconnect', async (data) => {
503
+ await this._onGatewayDisconnect(data);
450
504
  });
451
505
  }
452
506
  // ── 属性 ──────────────────────────────────────────
@@ -471,7 +525,17 @@ export class AUNClient {
471
525
  }
472
526
  /** 主动检查 gateway 可用性(GET /health) */
473
527
  async checkGatewayHealth(gatewayUrl, timeout = 5000) {
474
- return this._discovery.checkHealth(gatewayUrl, timeout);
528
+ const tStart = Date.now();
529
+ this._clientLog.debug(`checkGatewayHealth enter: gateway=${gatewayUrl} timeout=${timeout}`);
530
+ try {
531
+ const result = await this._discovery.checkHealth(gatewayUrl, timeout);
532
+ this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
533
+ return result;
534
+ }
535
+ catch (err) {
536
+ this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
537
+ throw err;
538
+ }
475
539
  }
476
540
  get e2ee() {
477
541
  return this._e2ee;
@@ -487,7 +551,10 @@ export class AUNClient {
487
551
  * @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
488
552
  */
489
553
  async connect(auth, options) {
554
+ const tStart = Date.now();
555
+ this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
490
556
  if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
557
+ this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
491
558
  throw new StateError(`connect not allowed in state ${this._state}`);
492
559
  }
493
560
  this._state = 'connecting';
@@ -499,18 +566,23 @@ export class AUNClient {
499
566
  this._closing = false;
500
567
  try {
501
568
  await this._connectOnce(normalized, false);
569
+ this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
502
570
  }
503
571
  catch (err) {
504
572
  // 连接失败时回退状态,允许重试
505
573
  if (this._state === 'connecting' || this._state === 'authenticating') {
506
574
  this._state = 'disconnected';
507
575
  }
576
+ this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
508
577
  throw err;
509
578
  }
510
579
  }
511
580
  /** 断开连接但保留本地状态,可再次 connect */
512
581
  async disconnect() {
582
+ const tStart = Date.now();
583
+ this._clientLog.debug(`disconnect enter: state=${this._state}`);
513
584
  if (this._state !== 'connected' && this._state !== 'reconnecting') {
585
+ this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms reason=not_connected`);
514
586
  return;
515
587
  }
516
588
  this._saveSeqTrackerState();
@@ -523,45 +595,59 @@ export class AUNClient {
523
595
  await this._transport.close();
524
596
  this._state = 'disconnected';
525
597
  await this._dispatcher.publish('connection.state', { state: this._state });
598
+ this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
526
599
  }
527
600
  /** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
528
601
  async listIdentities() {
529
- const listFn = this._keystore.listIdentities;
530
- if (typeof listFn !== 'function')
531
- return [];
532
- const aids = await listFn.call(this._keystore);
533
- const summaries = [];
534
- for (const aid of [...aids].sort()) {
535
- const identity = await this._keystore.loadIdentity(aid);
536
- if (!identity || !identity.private_key_pem)
537
- continue;
538
- const summary = { aid };
539
- // 优先从 loadMetadata 获取
540
- const loadMeta = this._keystore.loadMetadata;
541
- if (typeof loadMeta === 'function') {
542
- const md = await loadMeta.call(this._keystore, aid);
543
- if (md && Object.keys(md).length > 0) {
544
- summary.metadata = md;
545
- }
546
- }
547
- // 回退:从 identity 中提取非核心字段
548
- if (!summary.metadata) {
549
- const metadata = {};
550
- for (const [key, value] of Object.entries(identity)) {
551
- if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
552
- metadata[key] = value;
602
+ const tStart = Date.now();
603
+ this._clientLog.debug('listIdentities enter');
604
+ try {
605
+ const listFn = this._keystore.listIdentities;
606
+ if (typeof listFn !== 'function') {
607
+ this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
608
+ return [];
609
+ }
610
+ const aids = await listFn.call(this._keystore);
611
+ const summaries = [];
612
+ for (const aid of [...aids].sort()) {
613
+ const identity = await this._keystore.loadIdentity(aid);
614
+ if (!identity || !identity.private_key_pem)
615
+ continue;
616
+ const summary = { aid };
617
+ // 优先从 loadMetadata 获取
618
+ const loadMeta = this._keystore.loadMetadata;
619
+ if (typeof loadMeta === 'function') {
620
+ const md = await loadMeta.call(this._keystore, aid);
621
+ if (md && Object.keys(md).length > 0) {
622
+ summary.metadata = md;
553
623
  }
554
624
  }
555
- if (Object.keys(metadata).length > 0) {
556
- summary.metadata = metadata;
625
+ // 回退:从 identity 中提取非核心字段
626
+ if (!summary.metadata) {
627
+ const metadata = {};
628
+ for (const [key, value] of Object.entries(identity)) {
629
+ if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
630
+ metadata[key] = value;
631
+ }
632
+ }
633
+ if (Object.keys(metadata).length > 0) {
634
+ summary.metadata = metadata;
635
+ }
557
636
  }
637
+ summaries.push(summary);
558
638
  }
559
- summaries.push(summary);
639
+ this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
640
+ return summaries;
641
+ }
642
+ catch (err) {
643
+ this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
644
+ throw err;
560
645
  }
561
- return summaries;
562
646
  }
563
647
  /** 关闭连接 */
564
648
  async close() {
649
+ const tStart = Date.now();
650
+ this._clientLog.debug(`close enter: state=${this._state}`);
565
651
  this._closing = true;
566
652
  this._saveSeqTrackerState();
567
653
  this._stopBackgroundTasks();
@@ -574,6 +660,7 @@ export class AUNClient {
574
660
  if (this._state === 'idle' || this._state === 'closed') {
575
661
  this._state = 'closed';
576
662
  this._resetSeqTrackingState();
663
+ this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms reason=already_idle`);
577
664
  return;
578
665
  }
579
666
  // 关闭前通知服务端主动退出(best-effort,失败不阻塞)
@@ -587,6 +674,7 @@ export class AUNClient {
587
674
  this._state = 'closed';
588
675
  await this._dispatcher.publish('connection.state', { state: this._state });
589
676
  this._resetSeqTrackingState();
677
+ this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
590
678
  }
591
679
  // ── RPC ───────────────────────────────────────────
592
680
  /**
@@ -596,6 +684,19 @@ export class AUNClient {
596
684
  * 自动解密 message.pull/group.pull、Group E2EE 生命周期编排。
597
685
  */
598
686
  async call(method, params) {
687
+ const tStart = Date.now();
688
+ this._clientLog.debug(`call enter: method=${method}`);
689
+ try {
690
+ const result = await this._callImpl(method, params);
691
+ this._clientLog.debug(`call exit: elapsed=${Date.now() - tStart}ms method=${method}`);
692
+ return result;
693
+ }
694
+ catch (err) {
695
+ this._clientLog.debug(`call exit (error): elapsed=${Date.now() - tStart}ms method=${method} err=${err instanceof Error ? err.message : String(err)}`);
696
+ throw err;
697
+ }
698
+ }
699
+ async _callImpl(method, params) {
599
700
  if (this._state !== 'connected') {
600
701
  throw new ConnectionError('client is not connected');
601
702
  }
@@ -605,10 +706,6 @@ export class AUNClient {
605
706
  const p = { ...(params ?? {}) };
606
707
  this._validateOutboundCall(method, p);
607
708
  this._injectMessageCursorContext(method, p);
608
- // group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
609
- if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null && p.group_id !== '') {
610
- p.group_id = normalizeGroupId(p.group_id);
611
- }
612
709
  // group.* 方法注入 device_id(服务端用于多设备消息路由)
613
710
  if (method.startsWith('group.') && this._deviceId && p.device_id === undefined) {
614
711
  p.device_id = this._deviceId;
@@ -678,7 +775,7 @@ export class AUNClient {
678
775
  if (serverAck > 0) {
679
776
  const contig = this._seqTracker.getContiguousSeq(ns);
680
777
  if (contig < serverAck) {
681
- console.info('[aun_core] message.pull retention-floor 推进: ns=' + ns + ' contiguous=' + contig + ' -> server_ack_seq=' + serverAck);
778
+ this._clientLog.info('message.pull retention-floor advance: ns=' + ns + ' contiguous=' + contig + ' -> server_ack_seq=' + serverAck);
682
779
  this._seqTracker.forceContiguousSeq(ns, serverAck);
683
780
  }
684
781
  }
@@ -690,7 +787,7 @@ export class AUNClient {
690
787
  seq: contig,
691
788
  device_id: this._deviceId,
692
789
  slot_id: this._slotId,
693
- }).catch((e) => { console.warn('message.pull auto-ack 失败:', e); });
790
+ }).catch((e) => { this._clientLog.warn(`message.pull auto-ack failed:${String(e)}`); });
694
791
  }
695
792
  }
696
793
  }
@@ -698,7 +795,6 @@ export class AUNClient {
698
795
  if (method === 'group.pull' && isJsonObject(result)) {
699
796
  const r = result;
700
797
  const messages = r.messages;
701
- // 先保存原始消息(解密前),用于喂 SeqTracker(与 P2P message.pull 路径对齐)
702
798
  const rawMessages = (Array.isArray(messages) ? messages : []).filter(isJsonObject);
703
799
  if (rawMessages.length) {
704
800
  r.messages = await this._decryptGroupMessages(rawMessages);
@@ -706,35 +802,54 @@ export class AUNClient {
706
802
  const gid = (p.group_id ?? '');
707
803
  if (gid) {
708
804
  const ns = `group:${gid}`;
709
- // ⚠️ 使用原始消息(rawMessages)喂 SeqTracker,与 P2P message.pull 路径一致
710
- if (rawMessages.length) {
711
- this._seqTracker.onPullResult(ns, rawMessages);
805
+ // 区分解密成功 / 失败:失败的 payload 仍是 e2ee.group_encrypted。
806
+ const decryptedOnly = [];
807
+ let failedCount = 0;
808
+ const decryptedMessages = Array.isArray(r.messages) ? r.messages : [];
809
+ for (const m of decryptedMessages) {
810
+ if (!isJsonObject(m))
811
+ continue;
812
+ const payload = isJsonObject(m.payload) ? m.payload : {};
813
+ const ptype = payload.type;
814
+ if (ptype === 'e2ee.group_encrypted') {
815
+ failedCount++;
816
+ this._enqueuePendingDecrypt(gid, m);
817
+ }
818
+ else {
819
+ decryptedOnly.push(m);
820
+ }
821
+ }
822
+ if (decryptedOnly.length) {
823
+ // 仅用解密成功的消息推进 contig;失败的等 retry 解密成功才推进。
824
+ this._seqTracker.onPullResult(ns, decryptedOnly);
712
825
  }
713
826
  // ⚠️ 逻辑边界 L4:group retention floor 通道 = cursor.current_seq
714
- // 群路径目前无独立 earliest_available_seq 字段;若未来引入 group retention,需新增字段并同步更新此处。
715
- // 与 S2 [1,seq-1] 历史 gap 配合使用,force_contiguous_seq 是跳过空洞的唯一手段。
716
827
  const cursor = isJsonObject(r.cursor) ? r.cursor : null;
717
828
  if (cursor) {
718
829
  const serverAck = Number(cursor.current_seq ?? 0);
719
830
  if (serverAck > 0) {
720
831
  const contig = this._seqTracker.getContiguousSeq(ns);
721
832
  if (contig < serverAck) {
722
- console.info('[aun_core] group.pull retention-floor 推进: ns=' + ns + ' contiguous=' + contig + ' -> cursor.current_seq=' + serverAck);
833
+ this._clientLog.info('group.pull retention-floor advance: ns=' + ns + ' contiguous=' + contig + ' -> cursor.current_seq=' + serverAck);
723
834
  this._seqTracker.forceContiguousSeq(ns, serverAck);
724
835
  }
725
836
  }
726
837
  }
727
838
  this._saveSeqTrackerState();
728
- // auto-ack contiguous_seq
839
+ // auto-ack:仅当没有解密失败时才 ack。失败时让服务端 cursor 留在原位等 retry。
729
840
  const contig = this._seqTracker.getContiguousSeq(ns);
730
- const shouldAck = rawMessages.length > 0 || (cursor !== null && Number(cursor.current_seq ?? 0) > 0);
841
+ const shouldAck = failedCount === 0 && (decryptedOnly.length > 0 || (cursor !== null && Number(cursor.current_seq ?? 0) > 0));
731
842
  if (contig > 0 && shouldAck) {
732
843
  this._transport.call('group.ack_messages', {
733
844
  group_id: gid,
734
845
  msg_seq: contig,
735
846
  device_id: this._deviceId,
736
847
  slot_id: this._slotId,
737
- }).catch((e) => { console.warn('group.pull auto-ack 失败: group=' + gid, e); });
848
+ }).catch((e) => { this._clientLog.warn('group.pull auto-ack failed: group=' + gid, e); });
849
+ }
850
+ // 有解密失败时调度 recovery 兜底定时
851
+ if (failedCount > 0) {
852
+ this._scheduleRecoveryTimeout(gid);
738
853
  }
739
854
  }
740
855
  }
@@ -781,7 +896,7 @@ export class AUNClient {
781
896
  // P0-12: await rotation 完成(带超时兜底),确保后续 group.send 使用新 epoch
782
897
  const rotationPromise = this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, result), expectedEpoch, allowMember);
783
898
  const timeoutPromise = new Promise((resolve) => globalThis.setTimeout(resolve, 5000));
784
- await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => console.warn('membership RPC epoch rotation fallback failed:', exc));
899
+ await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => this._clientLog.warn(`membership RPC epoch rotation fallback failed:${String(exc)}`));
785
900
  }
786
901
  }
787
902
  return result;
@@ -813,7 +928,9 @@ export class AUNClient {
813
928
  // ── 事件管道:消息解密 ────────────────────────────
814
929
  /** 处理 transport 层推送的原始消息:解密后 re-publish 给用户 */
815
930
  _onRawMessageReceived(data) {
931
+ this._clientLog.debug(`_onRawMessageReceived enter: from=${data?.from ?? '-'} mid=${data?.message_id ?? '-'} seq=${data?.seq ?? '-'}`);
816
932
  this._safeAsync(this._processAndPublishMessage(data));
933
+ this._clientLog.debug(`_onRawMessageReceived exit: elapsed=0ms (dispatched async)`);
817
934
  }
818
935
  /** 实际处理推送消息的异步任务 */
819
936
  async _processAndPublishMessage(data) {
@@ -828,6 +945,22 @@ export class AUNClient {
828
945
  }
829
946
  // 拦截 P2P 传输的群组密钥分发/请求/响应消息
830
947
  if (await this._tryHandleGroupKeyMessage(msg)) {
948
+ // group_key 控制消息也要推进 seq tracker + auto-ack,
949
+ // 否则 fillP2pGap 会因为 contig 卡在此 seq 之前而重复拉取同样的历史消息。
950
+ const seq = msg.seq;
951
+ if (seq !== undefined && seq !== null && this._aid) {
952
+ const ns = `p2p:${this._aid}`;
953
+ this._seqTracker.onMessageSeq(ns, seq);
954
+ this._saveSeqTrackerState();
955
+ const contig = this._seqTracker.getContiguousSeq(ns);
956
+ if (contig > 0) {
957
+ this._transport.call('message.ack', {
958
+ seq: contig,
959
+ device_id: this._deviceId,
960
+ slot_id: this._slotId,
961
+ }).catch(() => { });
962
+ }
963
+ }
831
964
  return;
832
965
  }
833
966
  // P2P 空洞检测
@@ -847,7 +980,7 @@ export class AUNClient {
847
980
  seq: contig,
848
981
  device_id: this._deviceId,
849
982
  slot_id: this._slotId,
850
- }).catch((e) => { console.warn('P2P auto-ack 失败:', e); });
983
+ }).catch((e) => { this._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
851
984
  }
852
985
  // 即时持久化 cursor,异常断连后不回退
853
986
  this._saveSeqTrackerState();
@@ -862,7 +995,7 @@ export class AUNClient {
862
995
  }
863
996
  }
864
997
  catch (exc) {
865
- console.warn('消息解密失败:', exc);
998
+ this._clientLog.warn(`messagedecryptfailed:${String(exc)}`);
866
999
  // H26: 解密失败不再投递原始密文 payload(避免元数据泄漏 + 语义混淆),
867
1000
  // 改为发布 message.undecryptable 事件,仅携带安全的 header 信息。
868
1001
  if (isJsonObject(data)) {
@@ -881,7 +1014,9 @@ export class AUNClient {
881
1014
  }
882
1015
  /** 处理群组消息推送:自动解密后 re-publish */
883
1016
  _onRawGroupMessageCreated(data) {
1017
+ this._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
884
1018
  this._safeAsync(this._processAndPublishGroupMessage(data));
1019
+ this._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)`);
885
1020
  }
886
1021
  /**
887
1022
  * 处理群组推送消息的异步任务。
@@ -910,8 +1045,12 @@ export class AUNClient {
910
1045
  return;
911
1046
  }
912
1047
  const decrypted = await this._decryptGroupMessage(msg);
913
- // 只有带 payload 的真实消息,在同步解密/恢复尝试结束后才推进游标。
914
- if (groupId && seq !== undefined && seq !== null) {
1048
+ // 解密失败时**不推进 seq tracker / 不 auto-ack**:让服务端 cursor 留在原位,
1049
+ // 等密钥恢复后 retry 解密成功才推进 + ack;recovery 真的失败时由
1050
+ // _retryPendingDecryptMsgs(forceAdvanceOnFail=true) 兜底强制推进。
1051
+ const payloadForCheck = isJsonObject(msg.payload) ? msg.payload : null;
1052
+ const isDecryptFail = payloadForCheck?.type === 'e2ee.group_encrypted' && !decrypted.e2ee;
1053
+ if (!isDecryptFail && groupId && seq !== undefined && seq !== null) {
915
1054
  const ns = `group:${groupId}`;
916
1055
  const needPull = this._seqTracker.onMessageSeq(ns, seq);
917
1056
  if (needPull) {
@@ -924,15 +1063,17 @@ export class AUNClient {
924
1063
  msg_seq: contig,
925
1064
  device_id: this._deviceId,
926
1065
  slot_id: this._slotId,
927
- }).catch((e) => { console.warn('群消息 auto-ack 失败: group=' + groupId, e); });
1066
+ }).catch((e) => { this._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
928
1067
  }
929
1068
  this._saveSeqTrackerState();
930
1069
  }
931
1070
  // R3: 解密失败 → 不 publish 密文给应用层,入 pending 队列 + 发 undecryptable 事件
932
- const payloadForCheck = isJsonObject(msg.payload) ? msg.payload : null;
933
- if (payloadForCheck?.type === 'e2ee.group_encrypted' && !decrypted.e2ee) {
934
- if (groupId)
1071
+ if (isDecryptFail) {
1072
+ if (groupId) {
935
1073
  this._enqueuePendingDecrypt(groupId, msg);
1074
+ // 触发 recovery 兜底定时(30s 后如果仍未解开,强制推进)
1075
+ this._scheduleRecoveryTimeout(groupId);
1076
+ }
936
1077
  await this._publishAppEvent('group.message_undecryptable', {
937
1078
  message_id: msg.message_id ?? null,
938
1079
  group_id: groupId,
@@ -952,7 +1093,7 @@ export class AUNClient {
952
1093
  }
953
1094
  }
954
1095
  catch (exc) {
955
- console.warn('群消息解密失败:', exc);
1096
+ this._clientLog.warn(`group message decrypt failed:${String(exc)}`);
956
1097
  // H26: 解密失败改发 group.message_undecryptable 事件,不投递原始密文 payload。
957
1098
  if (isJsonObject(data)) {
958
1099
  const src = data;
@@ -1009,7 +1150,7 @@ export class AUNClient {
1009
1150
  }
1010
1151
  }
1011
1152
  catch (exc) {
1012
- console.warn('自动 pull 群消息失败:', exc);
1153
+ this._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1013
1154
  }
1014
1155
  // pull 失败时仍透传原始通知
1015
1156
  await this._publishAppEvent('group.message_created', notification);
@@ -1059,7 +1200,7 @@ export class AUNClient {
1059
1200
  }
1060
1201
  }
1061
1202
  catch (exc) {
1062
- console.warn('[aun_core] 群消息补洞失败:', exc);
1203
+ this._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
1063
1204
  }
1064
1205
  finally {
1065
1206
  // S1: 成功 / 失败路径都必须清理飞行标记
@@ -1097,7 +1238,7 @@ export class AUNClient {
1097
1238
  if (serverAck > 0) {
1098
1239
  const contigBefore = this._seqTracker.getContiguousSeq(ns);
1099
1240
  if (contigBefore < serverAck) {
1100
- console.info('[aun_core] group.pull_events retention-floor 推进: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
1241
+ this._clientLog.info('group.pull_events retention-floor advance: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
1101
1242
  this._seqTracker.forceContiguousSeq(ns, serverAck);
1102
1243
  }
1103
1244
  }
@@ -1110,7 +1251,7 @@ export class AUNClient {
1110
1251
  event_seq: contig,
1111
1252
  device_id: this._deviceId,
1112
1253
  slot_id: this._slotId,
1113
- }).catch((e) => { console.warn('群事件 auto-ack 失败: group=' + groupId, e); });
1254
+ }).catch((e) => { this._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
1114
1255
  }
1115
1256
  for (const evt of events) {
1116
1257
  if (isJsonObject(evt)) {
@@ -1132,7 +1273,7 @@ export class AUNClient {
1132
1273
  }
1133
1274
  }
1134
1275
  catch (exc) {
1135
- console.warn('[aun_core] 群事件补洞失败:', exc);
1276
+ this._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
1136
1277
  }
1137
1278
  finally {
1138
1279
  // S1: 成功 / 失败路径都必须清理飞行标记
@@ -1185,7 +1326,7 @@ export class AUNClient {
1185
1326
  }
1186
1327
  }
1187
1328
  catch (exc) {
1188
- console.warn('[aun_core] P2P 消息补洞失败:', exc);
1329
+ this._clientLog.warn(`P2P message gap-fill failed:${String(exc)}`);
1189
1330
  }
1190
1331
  finally {
1191
1332
  // S1: 成功 / 失败路径都必须清理飞行标记
@@ -1330,10 +1471,10 @@ export class AUNClient {
1330
1471
  encrypt: true,
1331
1472
  persist_required: true,
1332
1473
  });
1333
- console.info(`[aun_core] 已向 ${targetAid} 请求群 ${groupId} 的密钥`);
1474
+ this._clientLog.info(`to ${targetAid} request group ${groupId} key`);
1334
1475
  }
1335
1476
  catch (exc) {
1336
- console.warn(`[aun_core] 向 ${targetAid} 请求群 ${groupId} 密钥失败:`, exc);
1477
+ this._clientLog.warn(`to ${targetAid} request group ${groupId} key failed: ${String(exc)}`);
1337
1478
  }
1338
1479
  }
1339
1480
  /**
@@ -1460,82 +1601,93 @@ export class AUNClient {
1460
1601
  return member ? String(member.group_id ?? '') : '';
1461
1602
  }
1462
1603
  async _onRawGroupChanged(data) {
1463
- if (isJsonObject(data)) {
1464
- const d = data;
1465
- // 验签:有 client_signature 就验,没有默认安全
1466
- const cs = d.client_signature;
1467
- if (cs && isJsonObject(cs)) {
1468
- d._verified = await this._verifyEventSignature(d, cs);
1469
- }
1470
- await this._dispatcher.publish('group.changed', d);
1471
- const groupId = (d.group_id ?? '');
1472
- // event_seq 空洞检测:持久化后的 group.changed 会携带 event_seq。
1473
- // onMessageSeq 返回值决定是否补拉,与 P2P / group.message 路径对齐。
1474
- let needPull = false;
1475
- const rawEventSeq = d.event_seq;
1476
- if (rawEventSeq != null && groupId) {
1477
- const es = Number(rawEventSeq);
1478
- if (Number.isFinite(es) && es > 0) {
1479
- needPull = this._seqTracker.onMessageSeq(`group_event:${groupId}`, es);
1480
- }
1481
- }
1482
- // 仅在真实 event gap 时才触发补拉(补洞回来的事件不再触发新补洞)
1483
- if (needPull && groupId && !d._from_gap_fill) {
1484
- this._safeAsync(this._fillGroupEventGap(groupId));
1485
- }
1486
- if (d.action === 'member_left' || d.action === 'member_removed') {
1487
- if (groupId) {
1488
- const expectedEpoch = this._membershipRotationExpectedEpoch(d);
1489
- if (expectedEpoch === null) {
1490
- console.debug('membership event without old_epoch skipped for epoch rotation: aid=%s group=%s action=%s event_seq=%s', this._aid ?? '', groupId, String(d.action ?? ''), String(d.event_seq ?? ''));
1491
- }
1492
- else {
1493
- this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1604
+ const tStart = Date.now();
1605
+ const action = String(data?.action ?? '');
1606
+ const groupIdInit = String(data?.group_id ?? '');
1607
+ this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupIdInit} action=${action}`);
1608
+ try {
1609
+ if (isJsonObject(data)) {
1610
+ const d = data;
1611
+ // 验签:有 client_signature 就验,没有默认安全
1612
+ const cs = d.client_signature;
1613
+ if (cs && isJsonObject(cs)) {
1614
+ d._verified = await this._verifyEventSignature(d, cs);
1615
+ }
1616
+ await this._dispatcher.publish('group.changed', d);
1617
+ const groupId = (d.group_id ?? '');
1618
+ // event_seq 空洞检测:持久化后的 group.changed 会携带 event_seq。
1619
+ // onMessageSeq 返回值决定是否补拉,与 P2P / group.message 路径对齐。
1620
+ let needPull = false;
1621
+ const rawEventSeq = d.event_seq;
1622
+ if (rawEventSeq != null && groupId) {
1623
+ const es = Number(rawEventSeq);
1624
+ if (Number.isFinite(es) && es > 0) {
1625
+ needPull = this._seqTracker.onMessageSeq(`group_event:${groupId}`, es);
1494
1626
  }
1495
1627
  }
1496
- }
1497
- // 成员加入:按 action 区分策略
1498
- // - member_added / join_approved(私密群/审批群):admin 必然在线,立即轮换
1499
- // - joined / invite_code_used(开放群/邀请码群):所有在线成员延迟轮换,新成员自己延迟更长
1500
- if (['member_added', 'joined', 'join_approved', 'invite_code_used'].includes(String(d.action ?? ''))) {
1501
- if (groupId) {
1502
- const action = String(d.action ?? '');
1503
- const expectedEpoch = this._membershipRotationExpectedEpoch(d);
1504
- const joinedAids = this._joinedMemberAidsFromPayload(d);
1505
- const isSelfJoining = joinedAids.includes(this._aid ?? '') && (action === 'joined' || action === 'invite_code_used');
1506
- if (isSelfJoining || (action === 'joined' || action === 'invite_code_used')) {
1507
- // open/invite_code 群:所有在线成员都参与延迟轮换
1508
- // 新成员自己延迟更长,优先让其他在线成员先轮换
1509
- const triggerId = this._membershipRotationTriggerId(groupId, d);
1510
- if (!isSelfJoining) {
1511
- this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1628
+ // 仅在真实 event gap 时才触发补拉(补洞回来的事件不再触发新补洞)
1629
+ if (needPull && groupId && !d._from_gap_fill) {
1630
+ this._safeAsync(this._fillGroupEventGap(groupId));
1631
+ }
1632
+ if (d.action === 'member_left' || d.action === 'member_removed') {
1633
+ if (groupId) {
1634
+ const expectedEpoch = this._membershipRotationExpectedEpoch(d);
1635
+ if (expectedEpoch === null) {
1636
+ 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 ?? '')}`);
1512
1637
  }
1513
- if (expectedEpoch !== null) {
1514
- const delay = isSelfJoining ? AUNClient._SELF_JOIN_ROTATION_DELAY_MS : undefined;
1515
- this._safeAsync(this._delayedRotateAfterJoin(groupId, triggerId, expectedEpoch, true, delay));
1638
+ else {
1639
+ this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1516
1640
  }
1517
1641
  }
1518
- else {
1519
- if (expectedEpoch === null) {
1642
+ }
1643
+ // 成员加入:按 action 区分策略
1644
+ // - member_added / join_approved(私密群/审批群):admin 必然在线,立即轮换
1645
+ // - joined / invite_code_used(开放群/邀请码群):所有在线成员延迟轮换,新成员自己延迟更长
1646
+ if (['member_added', 'joined', 'join_approved', 'invite_code_used'].includes(String(d.action ?? ''))) {
1647
+ if (groupId) {
1648
+ const action = String(d.action ?? '');
1649
+ const expectedEpoch = this._membershipRotationExpectedEpoch(d);
1650
+ const joinedAids = this._joinedMemberAidsFromPayload(d);
1651
+ const isSelfJoining = joinedAids.includes(this._aid ?? '') && (action === 'joined' || action === 'invite_code_used');
1652
+ if (isSelfJoining || (action === 'joined' || action === 'invite_code_used')) {
1653
+ // open/invite_code 群:所有在线成员都参与延迟轮换
1654
+ // 新成员自己延迟更长,优先让其他在线成员先轮换
1520
1655
  const triggerId = this._membershipRotationTriggerId(groupId, d);
1521
- this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1656
+ if (!isSelfJoining) {
1657
+ this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1658
+ }
1659
+ if (expectedEpoch !== null) {
1660
+ const delay = isSelfJoining ? AUNClient._SELF_JOIN_ROTATION_DELAY_MS : undefined;
1661
+ this._safeAsync(this._delayedRotateAfterJoin(groupId, triggerId, expectedEpoch, true, delay));
1662
+ }
1522
1663
  }
1523
1664
  else {
1524
- this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1665
+ if (expectedEpoch === null) {
1666
+ const triggerId = this._membershipRotationTriggerId(groupId, d);
1667
+ this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1668
+ }
1669
+ else {
1670
+ this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1671
+ }
1525
1672
  }
1526
1673
  }
1527
1674
  }
1528
- }
1529
- // 群组解散 清理本地 epoch key、seq_tracker、补洞去重缓存
1530
- if (d.action === 'dissolved') {
1531
- if (groupId) {
1532
- this._cleanupDissolvedGroup(groupId);
1675
+ // 群组解散 → 清理本地 epoch key、seq_tracker、补洞去重缓存
1676
+ if (d.action === 'dissolved') {
1677
+ if (groupId) {
1678
+ this._cleanupDissolvedGroup(groupId);
1679
+ }
1533
1680
  }
1534
1681
  }
1682
+ else {
1683
+ // data 非对象也透传给用户(兼容旧版)
1684
+ await this._dispatcher.publish('group.changed', data);
1685
+ }
1686
+ this._clientLog.debug(`_onRawGroupChanged exit: elapsed=${Date.now() - tStart}ms group_id=${groupIdInit}`);
1535
1687
  }
1536
- else {
1537
- // data 非对象也透传给用户(兼容旧版)
1538
- await this._dispatcher.publish('group.changed', data);
1688
+ catch (err) {
1689
+ this._clientLog.debug(`_onRawGroupChanged exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1690
+ throw err;
1539
1691
  }
1540
1692
  }
1541
1693
  /**
@@ -1543,18 +1695,35 @@ export class AUNClient {
1543
1695
  * 当 prev_state_hash 与本地不连续时回源 group.get_state,并对回源数据做 hash 验证。
1544
1696
  */
1545
1697
  async _onGroupStateCommitted(data) {
1546
- if (!isJsonObject(data))
1547
- return;
1548
- const d = data;
1549
- const groupId = String(d.group_id ?? '').trim();
1550
- if (!groupId)
1551
- return;
1698
+ const tStart = Date.now();
1699
+ const groupIdInit = String(data?.group_id ?? '');
1700
+ this._clientLog.debug(`_onGroupStateCommitted enter: group_id=${groupIdInit} state_version=${String(data?.state_version ?? '-')}`);
1701
+ try {
1702
+ if (!isJsonObject(data)) {
1703
+ this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=non_object`);
1704
+ return;
1705
+ }
1706
+ const d = data;
1707
+ const groupId = String(d.group_id ?? '').trim();
1708
+ if (!groupId) {
1709
+ this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=no_group_id`);
1710
+ return;
1711
+ }
1712
+ await this._onGroupStateCommittedImpl(d, groupId);
1713
+ this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
1714
+ }
1715
+ catch (err) {
1716
+ this._clientLog.debug(`_onGroupStateCommitted exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1717
+ throw err;
1718
+ }
1719
+ }
1720
+ async _onGroupStateCommittedImpl(d, groupId) {
1552
1721
  // 提交者签名验证
1553
1722
  const cs = d.client_signature;
1554
1723
  if (cs && isJsonObject(cs)) {
1555
1724
  const verified = await this._verifyEventSignature(d, cs);
1556
1725
  if (verified === false) {
1557
- console.warn('[aun_core] state_committed 提交者签名验证失败 group=%s', groupId);
1726
+ this._clientLog.warn(`state_committed committer signature verify failed group=%s${String(groupId)}`);
1558
1727
  return;
1559
1728
  }
1560
1729
  d._verified = verified;
@@ -1571,7 +1740,7 @@ export class AUNClient {
1571
1740
  ? await loadFn.call(this._keystore, groupId)
1572
1741
  : null;
1573
1742
  if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
1574
- console.warn('[aun_core] state_hash 链不连续 group=%s local_sv=%d event_sv=%d', groupId, localState.state_version, stateVersion);
1743
+ this._clientLog.warn('[aun_core] state_hash 链不连续 group=%s local_sv=%d event_sv=%d', groupId, localState.state_version, stateVersion);
1575
1744
  // 回源同步
1576
1745
  try {
1577
1746
  const serverState = await this._transport.call('group.get_state', { group_id: groupId });
@@ -1591,7 +1760,7 @@ export class AUNClient {
1591
1760
  members: sMembers, policy: sPolicy, prevStateHash: sPrev,
1592
1761
  });
1593
1762
  if (computed !== sHash) {
1594
- console.warn('[aun_core] 回源 state_hash 验证失败 group=%s sv=%d expected=%s got=%s', groupId, sv, sHash, computed);
1763
+ this._clientLog.warn('[aun_core] 回源 state_hash 验证失败 group=%s sv=%d expected=%s got=%s', groupId, sv, sHash, computed);
1595
1764
  return;
1596
1765
  }
1597
1766
  }
@@ -1610,7 +1779,7 @@ export class AUNClient {
1610
1779
  }
1611
1780
  }
1612
1781
  catch (exc) {
1613
- console.warn('[aun_core] state 回源失败 group=%s:', groupId, exc);
1782
+ this._clientLog.warn(`state pull-back failed group=%s:${groupId} ${exc}`);
1614
1783
  }
1615
1784
  return;
1616
1785
  }
@@ -1622,7 +1791,7 @@ export class AUNClient {
1622
1791
  members, policy, prevStateHash,
1623
1792
  });
1624
1793
  if (computed !== stateHash) {
1625
- console.warn('[aun_core] state_hash 重算不匹配 group=%s sv=%d expected=%s got=%s', groupId, stateVersion, stateHash, computed);
1794
+ this._clientLog.warn('[aun_core] state_hash 重算不匹配 group=%s sv=%d expected=%s got=%s', groupId, stateVersion, stateHash, computed);
1626
1795
  return;
1627
1796
  }
1628
1797
  // 3. 更新本地存储
@@ -1649,7 +1818,7 @@ export class AUNClient {
1649
1818
  _cleanupDissolvedGroup(groupId) {
1650
1819
  // 1. 清理 GroupE2EEManager / keystore 中的 epoch 密钥
1651
1820
  this._safeAsync(this._groupE2ee.removeGroup(groupId).catch((exc) => {
1652
- console.warn(`[aun_core] 清理解散群组 ${groupId} epoch 密钥失败:`, exc);
1821
+ this._clientLog.warn(`cleanup dissolved group ${groupId} epoch key failed: ${String(exc)}`);
1653
1822
  }));
1654
1823
  // 2. 清理 seq_tracker 中的群消息和群事件命名空间
1655
1824
  this._seqTracker.removeNamespace(`group:${groupId}`);
@@ -1666,7 +1835,7 @@ export class AUNClient {
1666
1835
  this._pushedSeqs.delete(`group_event:${groupId}`);
1667
1836
  this._pendingOrderedMsgs.delete(`group:${groupId}`);
1668
1837
  this._pendingDecryptMsgs.delete(`group:${groupId}`);
1669
- console.info(`[aun_core] 已清理解散群组 ${groupId} 的本地状态`);
1838
+ this._clientLog.info(`cleanup dissolved group ${groupId} local state`);
1670
1839
  }
1671
1840
  async _verifyEventSignature(_event, cs) {
1672
1841
  const sigAid = String(cs.aid ?? '');
@@ -1683,7 +1852,7 @@ export class AUNClient {
1683
1852
  if (expectedFP) {
1684
1853
  const actualFP = await certificateSha256Fingerprint(cached.certPem);
1685
1854
  if (actualFP !== expectedFP) {
1686
- console.warn('[aun_core] 群事件验签失败:证书指纹不匹配 aid=%s', sigAid);
1855
+ this._clientLog.warn(`group event sig verify failed: cert fingerprint mismatch aid=%s${String(sigAid)}`);
1687
1856
  return false;
1688
1857
  }
1689
1858
  }
@@ -1698,7 +1867,7 @@ export class AUNClient {
1698
1867
  const sigBytes = base64ToUint8(sigB64);
1699
1868
  const ok = await ecdsaVerifyDer(pubKey, sigBytes, signData);
1700
1869
  if (!ok) {
1701
- console.warn('[aun_core] 群事件验签失败 aid=%s method=%s', sigAid, method);
1870
+ this._clientLog.warn(`group event sig verify failed aid=%s method=%s${sigAid} ${method}`);
1702
1871
  // P1-16: 签名失败统一发布事件
1703
1872
  this._dispatcher.publish('signature.verification_failed', {
1704
1873
  aid: sigAid, method, error: 'ECDSA verification failed',
@@ -1707,7 +1876,7 @@ export class AUNClient {
1707
1876
  return ok;
1708
1877
  }
1709
1878
  catch (exc) {
1710
- console.warn('[aun_core] 群事件验签异常:', exc);
1879
+ this._clientLog.warn(`group event sig verify exception:${String(exc)}`);
1711
1880
  // P1-16: 签名失败统一发布事件
1712
1881
  this._dispatcher.publish('signature.verification_failed', {
1713
1882
  aid: sigAid, method, error: String(exc),
@@ -1729,74 +1898,88 @@ export class AUNClient {
1729
1898
  }
1730
1899
  /** 自动加密并发送 P2P 消息 */
1731
1900
  async _sendEncrypted(params) {
1901
+ const tStart = Date.now();
1732
1902
  const toAid = String(params.to ?? '');
1733
- this._validateMessageRecipient(toAid);
1734
- const payload = isJsonObject(params.payload) ? params.payload : null;
1735
- const messageId = String(params.message_id ?? '') || _uuidV4();
1736
- const timestamp = params.timestamp ?? Date.now();
1737
- if (payload === null) {
1738
- throw new ValidationError('message.send payload must be an object when encrypt=true');
1739
- }
1740
- const persistRequired = Boolean(params.persist_required || params.durable);
1741
- const protectedHeaders = this._protectedHeadersFromParams(params);
1742
- // Lazy P2P sync:首次发送前自动拉取历史,避免重连后 seq 空洞
1743
- if (!this._p2pSynced) {
1744
- await this._lazySyncP2p();
1745
- }
1746
- // 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
1747
- const sendAttempt = async (refreshPeerMaterial = false) => {
1748
- const recipientPrekeys = refreshPeerMaterial
1749
- ? await this._refreshPeerPrekeys(toAid)
1750
- : await this._fetchPeerPrekeys(toAid);
1751
- const selfSyncCopies = await this._buildSelfSyncCopies({
1752
- logicalToAid: toAid, payload, messageId, timestamp, protectedHeaders,
1753
- });
1754
- // 多设备过滤:只保留有有效 device_id 的可路由 prekey,
1755
- // 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
1756
- const routablePrekeys = recipientPrekeys.filter(pk => {
1757
- const did = String(pk.device_id ?? '').trim();
1758
- return did && did !== PREKEY_FALLBACK_DEVICE_ID;
1759
- });
1760
- const canUseMultiDevice = routablePrekeys.length > 0
1761
- && (routablePrekeys.length > 1 || selfSyncCopies.length > 0);
1762
- if (!canUseMultiDevice) {
1763
- return await this._sendEncryptedSingle({
1903
+ this._clientLog.debug(`_sendEncrypted enter: to=${toAid} mid=${String(params.message_id ?? '<auto>')}`);
1904
+ try {
1905
+ this._validateMessageRecipient(toAid);
1906
+ const payload = isJsonObject(params.payload) ? params.payload : null;
1907
+ const messageId = String(params.message_id ?? '') || _uuidV4();
1908
+ const timestamp = params.timestamp ?? Date.now();
1909
+ if (payload === null) {
1910
+ throw new ValidationError('message.send payload must be an object when encrypt=true');
1911
+ }
1912
+ const persistRequired = Boolean(params.persist_required || params.durable);
1913
+ const protectedHeaders = this._protectedHeadersFromParams(params);
1914
+ // Lazy P2P sync:首次发送前自动拉取历史,避免重连后 seq 空洞
1915
+ if (!this._p2pSynced) {
1916
+ await this._lazySyncP2p();
1917
+ }
1918
+ // 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
1919
+ const sendAttempt = async (refreshPeerMaterial = false) => {
1920
+ const recipientPrekeys = refreshPeerMaterial
1921
+ ? await this._refreshPeerPrekeys(toAid)
1922
+ : await this._fetchPeerPrekeys(toAid);
1923
+ const selfSyncCopies = await this._buildSelfSyncCopies({
1924
+ logicalToAid: toAid, payload, messageId, timestamp, protectedHeaders,
1925
+ });
1926
+ // 多设备过滤:只保留有有效 device_id 的可路由 prekey,
1927
+ // 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
1928
+ const routablePrekeys = recipientPrekeys.filter(pk => {
1929
+ const did = String(pk.device_id ?? '').trim();
1930
+ return did && did !== PREKEY_FALLBACK_DEVICE_ID;
1931
+ });
1932
+ // 只要有 routable prekey 就走 multi_device 路径(即使只有 1 个 recipient device + 0 self copies)。
1933
+ // 这确保服务端为每个已注册设备存储副本,离线设备重连后能 pull 到。
1934
+ // single 路径仅在完全没有 routable prekey 时使用(legacy 兼容)。
1935
+ const canUseMultiDevice = routablePrekeys.length > 0;
1936
+ if (!canUseMultiDevice) {
1937
+ return await this._sendEncryptedSingle({
1938
+ toAid, payload, messageId, timestamp,
1939
+ prekey: routablePrekeys[0] ?? recipientPrekeys[0],
1940
+ persistRequired, protectedHeaders,
1941
+ });
1942
+ }
1943
+ const recipientCopies = await this._buildRecipientDeviceCopies({
1764
1944
  toAid, payload, messageId, timestamp,
1765
- prekey: routablePrekeys[0] ?? recipientPrekeys[0],
1766
- persistRequired, protectedHeaders,
1945
+ prekeys: routablePrekeys, protectedHeaders,
1767
1946
  });
1768
- }
1769
- const recipientCopies = await this._buildRecipientDeviceCopies({
1770
- toAid, payload, messageId, timestamp,
1771
- prekeys: routablePrekeys, protectedHeaders,
1772
- });
1773
- const sendParams = {
1774
- to: toAid,
1775
- payload: {
1947
+ const sendParams = {
1948
+ to: toAid,
1949
+ payload: {
1950
+ type: 'e2ee.multi_device',
1951
+ logical_message_id: messageId,
1952
+ recipient_copies: recipientCopies,
1953
+ self_copies: selfSyncCopies,
1954
+ },
1776
1955
  type: 'e2ee.multi_device',
1777
- logical_message_id: messageId,
1778
- recipient_copies: recipientCopies,
1779
- self_copies: selfSyncCopies,
1780
- },
1781
- type: 'e2ee.multi_device',
1782
- encrypted: true,
1783
- message_id: messageId,
1784
- timestamp,
1956
+ encrypted: true,
1957
+ message_id: messageId,
1958
+ timestamp,
1959
+ };
1960
+ if (persistRequired)
1961
+ sendParams.persist_required = true;
1962
+ return this._transport.call('message.send', sendParams);
1785
1963
  };
1786
- if (persistRequired)
1787
- sendParams.persist_required = true;
1788
- return this._transport.call('message.send', sendParams);
1789
- };
1790
- // 首次尝试(使用缓存);若对端证书/prekey 过期导致指纹不匹配,清缓存后重试一次
1791
- try {
1792
- return await sendAttempt(false);
1964
+ // 首次尝试(使用缓存);若对端证书/prekey 过期导致指纹不匹配,清缓存后重试一次
1965
+ try {
1966
+ const result = await sendAttempt(false);
1967
+ this._clientLog.debug(`_sendEncrypted exit: elapsed=${Date.now() - tStart}ms to=${toAid} retry=false`);
1968
+ return result;
1969
+ }
1970
+ catch (exc) {
1971
+ if (!isRetryablePeerMaterialError(exc))
1972
+ throw exc;
1973
+ this._clientLog.warn(`peer cert/prekey mismatch for ${toAid}, refreshing and retrying once`);
1974
+ }
1975
+ const result = await sendAttempt(true);
1976
+ this._clientLog.debug(`_sendEncrypted exit: elapsed=${Date.now() - tStart}ms to=${toAid} retry=true`);
1977
+ return result;
1793
1978
  }
1794
- catch (exc) {
1795
- if (!isRetryablePeerMaterialError(exc))
1796
- throw exc;
1797
- console.warn(`[aun_core] peer cert/prekey mismatch for ${toAid}, refreshing and retrying once`);
1979
+ catch (err) {
1980
+ this._clientLog.debug(`_sendEncrypted exit (error): elapsed=${Date.now() - tStart}ms to=${toAid} err=${err instanceof Error ? err.message : String(err)}`);
1981
+ throw err;
1798
1982
  }
1799
- return await sendAttempt(true);
1800
1983
  }
1801
1984
  /**
1802
1985
  * 首次发送 P2P 消息前懒拉取历史消息,同步 seqTracker 避免空洞。
@@ -1822,7 +2005,7 @@ export class AUNClient {
1822
2005
  }
1823
2006
  }
1824
2007
  catch (exc) {
1825
- console.warn('[aun_core] lazySyncP2p 失败:', exc);
2008
+ this._clientLog.warn(`lazySyncP2p failed:${String(exc)}`);
1826
2009
  }
1827
2010
  }
1828
2011
  async _sendEncryptedSingle(opts) {
@@ -1933,7 +2116,7 @@ export class AUNClient {
1933
2116
  }
1934
2117
  catch (e) {
1935
2118
  // 旧设备的 prekey 可能携带已轮换的证书指纹,跳过该设备的自同步副本
1936
- console.warn(`self-sync 跳过设备 ${deviceId}: 证书解析失败 (${e}),可能是旧 prekey`);
2119
+ this._clientLog.warn(`self-sync skip device ${deviceId}: cert parse failed (${e}), may be old prekey`);
1937
2120
  continue;
1938
2121
  }
1939
2122
  const [envelope, encryptResult] = await this._encryptCopyPayload({
@@ -1980,7 +2163,7 @@ export class AUNClient {
1980
2163
  });
1981
2164
  }
1982
2165
  catch (exc) {
1983
- console.warn('发布 e2ee.degraded 事件失败:', exc);
2166
+ this._clientLog.warn(`publish e2ee.degraded eventfailed:${String(exc)}`);
1984
2167
  }
1985
2168
  }
1986
2169
  }
@@ -2067,10 +2250,21 @@ export class AUNClient {
2067
2250
  }
2068
2251
  /** 自动加密并发送群组消息 */
2069
2252
  async _sendGroupEncrypted(params) {
2070
- return this._callGroupEncryptedRpc('group.send', params, {
2071
- idField: 'message_id',
2072
- idPrefix: 'gm',
2073
- });
2253
+ const tStart = Date.now();
2254
+ const groupId = String(params.group_id ?? '');
2255
+ this._clientLog.debug(`_sendGroupEncrypted enter: group_id=${groupId}`);
2256
+ try {
2257
+ const result = await this._callGroupEncryptedRpc('group.send', params, {
2258
+ idField: 'message_id',
2259
+ idPrefix: 'gm',
2260
+ });
2261
+ this._clientLog.debug(`_sendGroupEncrypted exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
2262
+ return result;
2263
+ }
2264
+ catch (err) {
2265
+ this._clientLog.debug(`_sendGroupEncrypted exit (error): elapsed=${Date.now() - tStart}ms group_id=${groupId} err=${err instanceof Error ? err.message : String(err)}`);
2266
+ throw err;
2267
+ }
2074
2268
  }
2075
2269
  async _putGroupThoughtEncrypted(params) {
2076
2270
  return this._callGroupEncryptedRpc('group.thought.put', params, {
@@ -2126,7 +2320,7 @@ export class AUNClient {
2126
2320
  }
2127
2321
  catch (exc) {
2128
2322
  if (attempt === 0 && this._isRecoverableGroupEpochError(exc)) {
2129
- console.warn(`[aun_core] 群 ${prepared.groupId} 调用 ${method} epoch 已过旧,恢复密钥后重加密重试一次: ${formatCaughtError(exc)}`);
2323
+ this._clientLog.warn(`group ${prepared.groupId} call ${method} epoch too old, retry encrypt after key recovery : ${formatCaughtError(exc)}`);
2130
2324
  prepared = await this._prepareGroupEncryptedRpcParams(method, params, options, true);
2131
2325
  continue;
2132
2326
  }
@@ -2217,7 +2411,7 @@ export class AUNClient {
2217
2411
  }
2218
2412
  }
2219
2413
  catch (exc) {
2220
- console.warn(`[aun_core] lazySyncGroup(${groupId}) 失败:`, exc);
2414
+ this._clientLog.warn(`lazySyncGroup(${groupId}) failed: ${String(exc)}`);
2221
2415
  }
2222
2416
  }
2223
2417
  _isGroupEpochTooOldError(exc) {
@@ -2275,7 +2469,7 @@ export class AUNClient {
2275
2469
  const secretData = await this._groupE2ee.loadSecret(groupId, 1);
2276
2470
  if (!secretData || secretData.pending_rotation_id)
2277
2471
  return epochResult;
2278
- console.warn(`[aun_core] 群 ${groupId} 检测到本地 epoch 1 已存在但服务端 epoch 仍为 0,尝试补同步初始 epoch`);
2472
+ this._clientLog.warn(`group ${groupId} local epoch=1 but server epoch=0, try sync initial epoch`);
2279
2473
  await this._syncEpochToServer(groupId);
2280
2474
  try {
2281
2475
  const refreshed = await this.call('group.e2ee.get_epoch', { group_id: groupId });
@@ -2283,7 +2477,7 @@ export class AUNClient {
2283
2477
  return refreshed;
2284
2478
  }
2285
2479
  catch (exc) {
2286
- console.warn(`[aun_core] 群 ${groupId} 初始 epoch 补同步后刷新服务端 epoch 失败: ${formatCaughtError(exc)}`);
2480
+ this._clientLog.warn(`group ${groupId} initial epoch sync then refresh server epoch failed: ${formatCaughtError(exc)}`);
2287
2481
  }
2288
2482
  return epochResult;
2289
2483
  }
@@ -2300,7 +2494,7 @@ export class AUNClient {
2300
2494
  catch (exc) {
2301
2495
  if (strict)
2302
2496
  throw new StateError(`group ${groupId} failed to query server epoch before retry: ${formatCaughtError(exc)}`);
2303
- console.warn(`[aun_core] group ${groupId} epoch precheck failed: ${formatCaughtError(exc)}`);
2497
+ this._clientLog.warn(`group ${groupId} epoch precheck failed: ${formatCaughtError(exc)}`);
2304
2498
  return;
2305
2499
  }
2306
2500
  let serverEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
@@ -2348,7 +2542,7 @@ export class AUNClient {
2348
2542
  throw new StateError(`group ${groupId} epoch rotation has not completed`);
2349
2543
  }
2350
2544
  }
2351
- console.warn(`[aun_core] group ${groupId} local epoch=${effectiveLocalEpoch} < server epoch=${serverEpoch}; requesting key recovery`);
2545
+ this._clientLog.warn(`group ${groupId} local epoch=${effectiveLocalEpoch} < server epoch=${serverEpoch}; requesting key recovery`);
2352
2546
  await this._recoverGroupEpochKey(groupId, serverEpoch, '', 5000);
2353
2547
  const deadline = Date.now() + 5000;
2354
2548
  while (Date.now() < deadline) {
@@ -2372,7 +2566,7 @@ export class AUNClient {
2372
2566
  members = isJsonObject(membersResult) ? membersResult.members : null;
2373
2567
  }
2374
2568
  catch (exc) {
2375
- console.warn(`[aun_core] 群 ${groupId} 成员 epoch floor 预检跳过: ${formatCaughtError(exc)}`);
2569
+ this._clientLog.warn(`group ${groupId} member epoch floor pre-check skip: ${formatCaughtError(exc)}`);
2376
2570
  return;
2377
2571
  }
2378
2572
  let maxMinReadEpoch = 0;
@@ -2387,7 +2581,7 @@ export class AUNClient {
2387
2581
  }
2388
2582
  if (maxMinReadEpoch <= committedEpoch)
2389
2583
  return;
2390
- console.warn(`[aun_core] 群 ${groupId} 成员 min_read_epoch 高于 committed epoch,按 committed epoch 继续发送: committed=${committedEpoch} floor=${maxMinReadEpoch}`);
2584
+ this._clientLog.warn(`group ${groupId} min_read_epoch above committed epoch, send with committed epoch: committed=${committedEpoch} floor=${maxMinReadEpoch}`);
2391
2585
  return;
2392
2586
  }
2393
2587
  }
@@ -2398,7 +2592,7 @@ export class AUNClient {
2398
2592
  return epochResult;
2399
2593
  }
2400
2594
  catch (exc) {
2401
- console.warn(`[aun_core] 群 ${groupId} 查询 committed epoch 状态失败,回退本地 epoch: ${formatCaughtError(exc)}`);
2595
+ this._clientLog.warn(`group ${groupId} query committed epoch state failed, rollback local epoch: ${formatCaughtError(exc)}`);
2402
2596
  }
2403
2597
  const localEpoch = await this._groupE2ee.currentEpoch(groupId);
2404
2598
  return { epoch: localEpoch ?? 0, committed_epoch: localEpoch ?? 0 };
@@ -2426,7 +2620,7 @@ export class AUNClient {
2426
2620
  let committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
2427
2621
  if (await this._committedRotationMembershipGap(groupId, committedEpoch, committedRotation)) {
2428
2622
  const allowMember = await this._groupAllowsMemberEpochRotation(groupId);
2429
- console.warn(`[aun_core] 群 ${groupId} committed epoch ${committedEpoch} 的成员快照与当前成员不一致,触发成员变更轮换修复`);
2623
+ this._clientLog.warn(`group ${groupId} committed epoch ${committedEpoch} member snapshot mismatches current members, trigger rotation fix`);
2430
2624
  await this._maybeLeadRotateGroupEpoch(groupId, `${groupId}:committed_membership_gap:aid:${this._aid}:epoch:${committedEpoch}`, committedEpoch, allowMember);
2431
2625
  const refreshed = await this._committedGroupEpochState(groupId);
2432
2626
  const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
@@ -2443,7 +2637,7 @@ export class AUNClient {
2443
2637
  return committedEpoch;
2444
2638
  }
2445
2639
  const pendingRotationId = secretData ? String(secretData.pending_rotation_id ?? '') : '';
2446
- console.warn(`[aun_core] 群 ${groupId} epoch ${committedEpoch} 本地 pending key 未匹配服务端 committed rotation,先恢复密钥: local_rotation=${pendingRotationId || '-'}`);
2640
+ this._clientLog.warn(`group ${groupId} epoch ${committedEpoch} local pending key mismatches server committed rotation, recover key first: local_rotation=${pendingRotationId || '-'}`);
2447
2641
  await this._recoverGroupEpochKey(groupId, committedEpoch, '', 5000);
2448
2642
  let refreshed = await this._committedGroupEpochState(groupId);
2449
2643
  const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
@@ -2488,13 +2682,13 @@ export class AUNClient {
2488
2682
  if (activeMembers.join('\n') !== expectedMembers.join('\n')) {
2489
2683
  const missing = activeMembers.filter((aid) => !expectedMembers.includes(aid));
2490
2684
  const extra = expectedMembers.filter((aid) => !activeMembers.includes(aid));
2491
- console.info(`[aun_core] 群 ${groupId} committed membership gap: epoch=${committedEpoch} missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
2685
+ this._clientLog.info(`group ${groupId} committed membership gap: epoch=${committedEpoch} missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
2492
2686
  return true;
2493
2687
  }
2494
2688
  return false;
2495
2689
  }
2496
2690
  catch (exc) {
2497
- console.debug(`[aun_core] 查询当前成员失败,无法判断 committed membership gap: group=${groupId} err=${formatCaughtError(exc)}`);
2691
+ this._clientLog.debug(`query current members failed, cannot determine committed membership gap: group=${groupId} err=${formatCaughtError(exc)}`);
2498
2692
  return false;
2499
2693
  }
2500
2694
  }
@@ -2514,7 +2708,7 @@ export class AUNClient {
2514
2708
  if (fromAid) {
2515
2709
  const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
2516
2710
  if (!certReady) {
2517
- console.warn(`无法获取发送方 ${fromAid} 的证书,跳过解密`);
2711
+ this._clientLog.warn(`cannot fetch sender ${fromAid} cert, skip decrypt`);
2518
2712
  throw new Error(`发送方证书不可用: from=${fromAid}, mid=${message.message_id}`);
2519
2713
  }
2520
2714
  }
@@ -2528,47 +2722,56 @@ export class AUNClient {
2528
2722
  }
2529
2723
  /** 批量解密 P2P 消息(用于 message.pull) */
2530
2724
  async _decryptMessages(messages) {
2531
- const seenInBatch = new Set();
2532
- const result = [];
2533
- for (const msg of messages) {
2534
- const mid = (msg.message_id ?? '');
2535
- if (mid && seenInBatch.has(mid))
2536
- continue;
2537
- if (mid)
2538
- seenInBatch.add(mid);
2539
- const payload = isJsonObject(msg.payload) ? msg.payload : null;
2540
- if (payload !== null && await this._tryHandleGroupKeyMessage(msg)) {
2541
- continue;
2542
- }
2543
- if (payload !== null
2544
- && payload.type === 'e2ee.encrypted'
2545
- && (msg.encrypted === true || !('encrypted' in msg))) {
2546
- try {
2547
- const fromAid = (msg.from ?? '');
2548
- const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
2549
- if (fromAid) {
2550
- const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
2551
- if (!certReady) {
2552
- console.warn('[aun_core] 无法获取发送方 %s 的证书,跳过解密', fromAid);
2553
- continue;
2725
+ const tStart = Date.now();
2726
+ this._clientLog.debug(`_decryptMessages enter: count=${messages.length}`);
2727
+ try {
2728
+ const seenInBatch = new Set();
2729
+ const result = [];
2730
+ for (const msg of messages) {
2731
+ const mid = (msg.message_id ?? '');
2732
+ if (mid && seenInBatch.has(mid))
2733
+ continue;
2734
+ if (mid)
2735
+ seenInBatch.add(mid);
2736
+ const payload = isJsonObject(msg.payload) ? msg.payload : null;
2737
+ if (payload !== null && await this._tryHandleGroupKeyMessage(msg)) {
2738
+ continue;
2739
+ }
2740
+ if (payload !== null
2741
+ && payload.type === 'e2ee.encrypted'
2742
+ && (msg.encrypted === true || !('encrypted' in msg))) {
2743
+ try {
2744
+ const fromAid = (msg.from ?? '');
2745
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
2746
+ if (fromAid) {
2747
+ const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
2748
+ if (!certReady) {
2749
+ this._clientLog.warn(`cannot fetch sender %s cert, skip decrypt${String(fromAid)}`);
2750
+ continue;
2751
+ }
2752
+ }
2753
+ // Pull 场景:跳过防重放和 timestamp 窗口检查(push 已处理过的消息仍需要能解密)
2754
+ const decrypted = await this._e2ee.decryptMessage(msg, { skipReplay: true });
2755
+ if (decrypted !== null) {
2756
+ result.push(decrypted);
2554
2757
  }
2555
2758
  }
2556
- // Pull 场景:跳过防重放和 timestamp 窗口检查(push 已处理过的消息仍需要能解密)
2557
- const decrypted = await this._e2ee.decryptMessage(msg, { skipReplay: true });
2558
- if (decrypted !== null) {
2559
- result.push(decrypted);
2759
+ catch (decryptExc) {
2760
+ this._clientLog.warn(`pull messagedecryptfailed, skip: from=${String(msg.from ?? '')} mid=${mid} err=${decryptExc instanceof Error ? decryptExc.message : String(decryptExc)}`);
2761
+ continue;
2560
2762
  }
2561
2763
  }
2562
- catch (decryptExc) {
2563
- console.warn('[aun_core] pull 消息解密失败,跳过: from=%s mid=%s err=%s', (msg.from ?? ''), mid, decryptExc instanceof Error ? decryptExc.message : String(decryptExc));
2564
- continue;
2764
+ else {
2765
+ result.push(msg);
2565
2766
  }
2566
2767
  }
2567
- else {
2568
- result.push(msg);
2569
- }
2768
+ this._clientLog.debug(`_decryptMessages exit: elapsed=${Date.now() - tStart}ms in=${messages.length} out=${result.length}`);
2769
+ return result;
2770
+ }
2771
+ catch (err) {
2772
+ this._clientLog.debug(`_decryptMessages exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
2773
+ throw err;
2570
2774
  }
2571
- return result;
2572
2775
  }
2573
2776
  /** 解密单条群组消息。opts.skipReplay 用于 pull 场景跳过防重放。 */
2574
2777
  _enqueuePendingDecrypt(groupId, msg) {
@@ -2577,23 +2780,47 @@ export class AUNClient {
2577
2780
  queue.push(msg);
2578
2781
  this._pendingDecryptMsgs.set(ns, queue.slice(-PENDING_DECRYPT_LIMIT));
2579
2782
  }
2580
- async _retryPendingDecryptMsgs(groupId) {
2783
+ async _retryPendingDecryptMsgs(groupId, forceAdvanceOnFail = false) {
2581
2784
  const ns = `group:${groupId}`;
2582
2785
  const queue = this._pendingDecryptMsgs.get(ns);
2583
2786
  if (!queue || queue.length === 0)
2584
2787
  return;
2585
2788
  this._pendingDecryptMsgs.set(ns, []);
2586
2789
  const stillPending = [];
2790
+ let forceAdvancedAny = false;
2587
2791
  for (const msg of queue) {
2588
2792
  try {
2589
2793
  const decrypted = await this._decryptGroupMessage(msg);
2590
2794
  const payload = isJsonObject(msg.payload) ? msg.payload : null;
2591
2795
  if (payload?.type === 'e2ee.group_encrypted' && !decrypted.e2ee) {
2592
- stillPending.push(msg);
2796
+ if (forceAdvanceOnFail) {
2797
+ // recovery 真的失败:强制推进 + 发 undecryptable
2798
+ this._clientLog.info(`group recovery give up: group=${groupId} seq=${String(msg.seq ?? '')} → force advance + publish undecryptable`);
2799
+ const seq = msg.seq;
2800
+ if (seq !== undefined && seq !== null) {
2801
+ this._seqTracker.onMessageSeq(ns, seq);
2802
+ this._saveSeqTrackerState();
2803
+ forceAdvancedAny = true;
2804
+ }
2805
+ await this._publishAppEvent('group.message_undecryptable', {
2806
+ message_id: msg.message_id,
2807
+ group_id: groupId,
2808
+ from: msg.from,
2809
+ seq,
2810
+ timestamp: msg.timestamp,
2811
+ _decrypt_error: 'epoch recovery failed',
2812
+ });
2813
+ }
2814
+ else {
2815
+ stillPending.push(msg);
2816
+ }
2593
2817
  continue;
2594
2818
  }
2595
2819
  const seq = msg.seq;
2596
2820
  if (seq !== undefined && seq !== null) {
2821
+ // 推进 seq tracker(之前 push/pull 失败时没推进)
2822
+ this._seqTracker.onMessageSeq(ns, seq);
2823
+ this._saveSeqTrackerState();
2597
2824
  await this._publishOrderedMessage('group.message_created', ns, seq, decrypted);
2598
2825
  }
2599
2826
  else {
@@ -2604,6 +2831,17 @@ export class AUNClient {
2604
2831
  stillPending.push(msg);
2605
2832
  }
2606
2833
  }
2834
+ if (forceAdvancedAny) {
2835
+ const contig = this._seqTracker.getContiguousSeq(ns);
2836
+ if (contig > 0) {
2837
+ this._transport.call('group.ack_messages', {
2838
+ group_id: groupId,
2839
+ msg_seq: contig,
2840
+ device_id: this._deviceId,
2841
+ slot_id: this._slotId,
2842
+ }).catch((e) => { this._clientLog.warn('group recovery force-advance ack failed: group=' + groupId, e); });
2843
+ }
2844
+ }
2607
2845
  const queuedDuringRetry = this._pendingDecryptMsgs.get(ns) ?? [];
2608
2846
  const mergedPending = [...stillPending, ...queuedDuringRetry].slice(-PENDING_DECRYPT_LIMIT);
2609
2847
  if (mergedPending.length)
@@ -2611,26 +2849,59 @@ export class AUNClient {
2611
2849
  else
2612
2850
  this._pendingDecryptMsgs.delete(ns);
2613
2851
  }
2852
+ // recovery 兜底定时去重:每个 group 在 30s 内最多调度一次"超时强制推进"任务
2853
+ _recoveryTimeoutScheduled = new Map();
2854
+ _scheduleRecoveryTimeout(groupId, timeoutMs = 30000) {
2855
+ if (!groupId)
2856
+ return;
2857
+ const now = Date.now();
2858
+ const last = this._recoveryTimeoutScheduled.get(groupId) ?? 0;
2859
+ if (last && (last + timeoutMs) > now)
2860
+ return;
2861
+ this._recoveryTimeoutScheduled.set(groupId, now);
2862
+ setTimeout(() => {
2863
+ const ns = `group:${groupId}`;
2864
+ const queue = this._pendingDecryptMsgs.get(ns);
2865
+ if (!queue || queue.length === 0)
2866
+ return;
2867
+ this._clientLog.info(`group recovery timeout: group=${groupId} → force advance`);
2868
+ this._safeAsync(this._retryPendingDecryptMsgs(groupId, true));
2869
+ }, timeoutMs);
2870
+ }
2614
2871
  _scheduleRetryPendingDecryptMsgs(groupId) {
2615
2872
  if (!groupId || !this._pendingDecryptMsgs.has(`group:${groupId}`))
2616
2873
  return;
2617
2874
  this._safeAsync(this._retryPendingDecryptMsgs(groupId));
2618
2875
  }
2619
2876
  async _recoverGroupEpochKey(groupId, epoch, senderAid = '', timeoutMs = 5000) {
2620
- const existing = await this._groupE2ee.loadSecret(groupId, epoch);
2621
- if (await this._groupEpochSecretReadyForRecovery(groupId, epoch, existing)) {
2622
- this._scheduleRetryPendingDecryptMsgs(groupId);
2623
- return true;
2877
+ const tStart = Date.now();
2878
+ this._clientLog.debug(`_recoverGroupEpochKey enter: group_id=${groupId} epoch=${epoch} sender=${senderAid} timeout=${timeoutMs}`);
2879
+ try {
2880
+ const existing = await this._groupE2ee.loadSecret(groupId, epoch);
2881
+ if (await this._groupEpochSecretReadyForRecovery(groupId, epoch, existing)) {
2882
+ this._scheduleRetryPendingDecryptMsgs(groupId);
2883
+ this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=already_ready`);
2884
+ return true;
2885
+ }
2886
+ // inflight 去重:同 groupId:epoch 的并发恢复共享同一个 Promise
2887
+ const key = `${groupId}:${epoch}`;
2888
+ const inflight = this._groupEpochRecoveryInflight.get(key);
2889
+ if (inflight) {
2890
+ const r = await inflight;
2891
+ this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=${r ? 'ok' : 'failed'} source=inflight`);
2892
+ return r;
2893
+ }
2894
+ const promise = this._doRecoverGroupEpochKey(groupId, epoch, senderAid, timeoutMs)
2895
+ .finally(() => this._groupEpochRecoveryInflight.delete(key));
2896
+ this._groupEpochRecoveryInflight.set(key, promise);
2897
+ const r = await promise;
2898
+ this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=${r ? 'ok' : 'failed'}`);
2899
+ return r;
2900
+ }
2901
+ catch (err) {
2902
+ this._clientLog.debug(`_recoverGroupEpochKey exit (error): elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} err=${err instanceof Error ? err.message : String(err)}`);
2903
+ throw err;
2624
2904
  }
2625
- // inflight 去重:同 groupId:epoch 的并发恢复共享同一个 Promise
2626
- const key = `${groupId}:${epoch}`;
2627
- const inflight = this._groupEpochRecoveryInflight.get(key);
2628
- if (inflight)
2629
- return inflight;
2630
- const promise = this._doRecoverGroupEpochKey(groupId, epoch, senderAid, timeoutMs)
2631
- .finally(() => this._groupEpochRecoveryInflight.delete(key));
2632
- this._groupEpochRecoveryInflight.set(key, promise);
2633
- return promise;
2634
2905
  }
2635
2906
  static _extractGroupJoinMode(payload) {
2636
2907
  if (!isJsonObject(payload))
@@ -2976,46 +3247,66 @@ export class AUNClient {
2976
3247
  }
2977
3248
  }
2978
3249
  async _decryptGroupMessage(message, opts) {
2979
- const payload = isJsonObject(message.payload) ? message.payload : null;
2980
- if (payload === null || payload.type !== 'e2ee.group_encrypted') {
2981
- return this._attachGroupDispatchModeToPayload(message);
2982
- }
2983
- // 确保发送方证书已缓存(签名验证需要)
2984
- const senderAid = String(message.from ?? message.sender_aid ?? '');
2985
- if (senderAid) {
2986
- const certOk = await this._ensureSenderCertCached(senderAid);
2987
- if (!certOk) {
2988
- console.warn(`群消息解密跳过:发送方 ${senderAid} 证书不可用`);
2989
- return message;
2990
- }
2991
- }
2992
- // 先尝试直接解密
2993
- const result = await this._groupE2ee.decrypt(message, opts);
2994
- if (result !== null && isJsonObject(result.e2ee)) {
2995
- return this._attachGroupDispatchModeToPayload(result);
2996
- }
2997
- // replay guard 命中:decrypt 返回了原消息(非 null)但无 e2ee 字段
2998
- // 不是解密失败,不应触发 recover
2999
- if (result !== null) {
3000
- return result;
3001
- }
3002
- // 真正的解密失败(result === null),尝试密钥恢复后重试
3003
- const groupId = String(message.group_id ?? '');
3004
- const sender = String(message.from ?? message.sender_aid ?? '');
3005
- const epoch = Number(payload.epoch ?? 0);
3006
- if (epoch > 0 && groupId) {
3007
- try {
3008
- if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
3009
- const retry = await this._groupE2ee.decrypt(message, opts);
3010
- if (retry !== null && retry.e2ee)
3011
- return this._attachGroupDispatchModeToPayload(retry);
3250
+ const tStart = Date.now();
3251
+ const groupIdInit = String(message.group_id ?? '');
3252
+ const midInit = String(message.message_id ?? '');
3253
+ this._clientLog.debug(`_decryptGroupMessage enter: group_id=${groupIdInit} mid=${midInit} skip_replay=${!!opts?.skipReplay}`);
3254
+ try {
3255
+ const payload = isJsonObject(message.payload) ? message.payload : null;
3256
+ if (payload === null || payload.type !== 'e2ee.group_encrypted') {
3257
+ const r = this._attachGroupDispatchModeToPayload(message);
3258
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_encrypted`);
3259
+ return r;
3260
+ }
3261
+ // 确保发送方证书已缓存(签名验证需要)
3262
+ const senderAid = String(message.from ?? message.sender_aid ?? '');
3263
+ if (senderAid) {
3264
+ const certOk = await this._ensureSenderCertCached(senderAid);
3265
+ if (!certOk) {
3266
+ this._clientLog.warn(`group message decrypt skip: sender ${senderAid} cert unavailable`);
3267
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=skip_no_sender_cert`);
3268
+ return message;
3012
3269
  }
3013
3270
  }
3014
- catch (exc) {
3015
- console.debug(`[aun_core] ${groupId} epoch ${epoch} 同步恢复失败: ${formatCaughtError(exc)}`);
3271
+ // 先尝试直接解密
3272
+ const result = await this._groupE2ee.decrypt(message, opts);
3273
+ if (result !== null && isJsonObject(result.e2ee)) {
3274
+ const r = this._attachGroupDispatchModeToPayload(result);
3275
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=ok group_id=${groupIdInit}`);
3276
+ return r;
3277
+ }
3278
+ // replay guard 命中:decrypt 返回了原消息(非 null)但无 e2ee 字段
3279
+ // 不是解密失败,不应触发 recover
3280
+ if (result !== null) {
3281
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=replay_skipped`);
3282
+ return result;
3283
+ }
3284
+ // 真正的解密失败(result === null),尝试密钥恢复后重试
3285
+ const groupId = String(message.group_id ?? '');
3286
+ const sender = String(message.from ?? message.sender_aid ?? '');
3287
+ const epoch = Number(payload.epoch ?? 0);
3288
+ if (epoch > 0 && groupId) {
3289
+ try {
3290
+ if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
3291
+ const retry = await this._groupE2ee.decrypt(message, opts);
3292
+ if (retry !== null && retry.e2ee) {
3293
+ const r = this._attachGroupDispatchModeToPayload(retry);
3294
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=ok_after_recover`);
3295
+ return r;
3296
+ }
3297
+ }
3298
+ }
3299
+ catch (exc) {
3300
+ this._clientLog.debug(`group ${groupId} epoch ${epoch} sync recover failed: ${formatCaughtError(exc)}`);
3301
+ }
3016
3302
  }
3303
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=undecryptable group_id=${groupIdInit}`);
3304
+ return message;
3305
+ }
3306
+ catch (err) {
3307
+ this._clientLog.debug(`_decryptGroupMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
3308
+ throw err;
3017
3309
  }
3018
- return message;
3019
3310
  }
3020
3311
  _attachGroupDispatchModeToPayload(message) {
3021
3312
  const payload = message.payload;
@@ -3132,14 +3423,14 @@ export class AUNClient {
3132
3423
  if (fromAid) {
3133
3424
  const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
3134
3425
  if (!certReady) {
3135
- console.warn('[aun_core] p2p.thought.decrypt failed: 无法获取发送方证书 thought_id=' + thoughtId + ' from=' + fromAid);
3426
+ this._clientLog.warn('p2p.thought.decrypt failed: cannot fetch sendercert thought_id=' + thoughtId + ' from=' + fromAid);
3136
3427
  decryptFailed = true;
3137
3428
  }
3138
3429
  }
3139
3430
  if (!decryptFailed) {
3140
3431
  decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
3141
3432
  if (decrypted === null || (isJsonObject(decrypted.payload) && decrypted.payload.type === 'e2ee.encrypted')) {
3142
- console.warn('[aun_core] p2p.thought.decrypt failed thought_id=' + thoughtId);
3433
+ this._clientLog.warn('p2p.thought.decrypt failed thought_id=' + thoughtId);
3143
3434
  decryptFailed = true;
3144
3435
  decrypted = message;
3145
3436
  }
@@ -3188,7 +3479,7 @@ export class AUNClient {
3188
3479
  decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
3189
3480
  }
3190
3481
  catch (exc) {
3191
- console.warn('[aun_core] e2ee.encrypted 外壳解密抛异常,交给普通链路处理:', exc);
3482
+ this._clientLog.warn(`e2ee.encrypted outer decrypt exception, fallback to normal path:${String(exc)}`);
3192
3483
  return false;
3193
3484
  }
3194
3485
  if (!decrypted) {
@@ -3207,11 +3498,31 @@ export class AUNClient {
3207
3498
  let result = null;
3208
3499
  try {
3209
3500
  if (actualPayload.type === 'e2ee.group_key_distribution') {
3501
+ // 快速跳过已过期的历史 epoch 分发:本地已有更高 epoch 时不发任何 RPC,
3502
+ // 避免 fillP2pGap 拉到大量历史群密钥消息时触发 epoch 编排风暴。
3503
+ const distGroupId = String(actualPayload.group_id ?? '');
3504
+ const distEpoch = Number(actualPayload.epoch ?? 0);
3505
+ if (distGroupId && distEpoch > 0) {
3506
+ const localEpoch = await this._groupE2ee.currentEpoch(distGroupId) ?? 0;
3507
+ if (localEpoch >= distEpoch) {
3508
+ this._clientLog.debug(`skip stale group_key_distribution: group=${distGroupId} msg_epoch=${distEpoch} local_epoch=${localEpoch}`);
3509
+ return true;
3510
+ }
3511
+ }
3210
3512
  if (!await this._verifyActiveGroupRotationDistribution(actualPayload)) {
3211
3513
  return true;
3212
3514
  }
3213
3515
  }
3214
3516
  else if (actualPayload.type === 'e2ee.group_key_response') {
3517
+ const respGroupId = String(actualPayload.group_id ?? '');
3518
+ const respEpoch = Number(actualPayload.epoch ?? 0);
3519
+ if (respGroupId && respEpoch > 0) {
3520
+ const localEpoch = await this._groupE2ee.currentEpoch(respGroupId) ?? 0;
3521
+ if (localEpoch >= respEpoch) {
3522
+ this._clientLog.debug(`skip stale group_key_response: group=${respGroupId} msg_epoch=${respEpoch} local_epoch=${localEpoch}`);
3523
+ return true;
3524
+ }
3525
+ }
3215
3526
  if (!await this._verifyGroupKeyResponseEpoch(actualPayload)) {
3216
3527
  return true;
3217
3528
  }
@@ -3227,7 +3538,7 @@ export class AUNClient {
3227
3538
  }
3228
3539
  }
3229
3540
  catch (exc) {
3230
- console.warn('[aun_core] 群组密钥消息处理异常:', exc);
3541
+ this._clientLog.warn(`group key message handle exception:${String(exc)}`);
3231
3542
  // S14: 控制面消息处理异常也要抑制业务分发
3232
3543
  if (isGroupKeyCtrl)
3233
3544
  return true;
@@ -3255,7 +3566,7 @@ export class AUNClient {
3255
3566
  members = memberList.map(m => String(m.aid ?? ''));
3256
3567
  }
3257
3568
  catch (exc) {
3258
- console.warn(`群组 ${groupId} 成员列表回源失败:`, exc);
3569
+ this._clientLog.warn(`group ${groupId} member list pull-back failed: ${String(exc)}`);
3259
3570
  }
3260
3571
  }
3261
3572
  const response = await this._groupE2ee.handleKeyRequestMsg(actualPayload, members);
@@ -3269,7 +3580,7 @@ export class AUNClient {
3269
3580
  });
3270
3581
  }
3271
3582
  catch (exc) {
3272
- console.warn(`向 ${requester} 回复群组密钥失败:`, exc);
3583
+ this._clientLog.warn(`reply group key to ${requester} failed: ${String(exc)}`);
3273
3584
  }
3274
3585
  }
3275
3586
  }
@@ -3291,144 +3602,170 @@ export class AUNClient {
3291
3602
  * 跨域时自动将请求路由到 peer 所在域的 Gateway。
3292
3603
  */
3293
3604
  async _fetchPeerCert(aid, certFingerprint) {
3294
- const cacheKey = certCacheKey(aid, certFingerprint);
3295
- const cached = this._certCache.get(cacheKey);
3296
- const now = Date.now() / 1000;
3297
- if (cached && now < cached.refreshAfter) {
3298
- return cached.certPem;
3299
- }
3300
- const gatewayUrl = this._gatewayUrl;
3301
- if (!gatewayUrl) {
3302
- throw new ValidationError('gateway url unavailable for e2ee cert fetch');
3303
- }
3304
- // 跨域时用 peer 所在域的 Gateway URL
3305
- const peerGatewayUrl = resolvePeerGatewayUrl(gatewayUrl, aid);
3306
- let certPem;
3605
+ const tStart = Date.now();
3606
+ this._clientLog.debug(`_fetchPeerCert enter: aid=${aid} fingerprint=${certFingerprint ?? '<none>'}`);
3307
3607
  try {
3308
- const certUrl = buildCertUrl(peerGatewayUrl, aid, certFingerprint);
3309
- // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3310
- const controller = new AbortController();
3311
- const timeoutId = setTimeout(() => controller.abort(), 5000);
3608
+ const cacheKey = certCacheKey(aid, certFingerprint);
3609
+ const cached = this._certCache.get(cacheKey);
3610
+ const now = Date.now() / 1000;
3611
+ if (cached && now < cached.refreshAfter) {
3612
+ this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} source=cache`);
3613
+ return cached.certPem;
3614
+ }
3615
+ const gatewayUrl = this._gatewayUrl;
3616
+ if (!gatewayUrl) {
3617
+ throw new ValidationError('gateway url unavailable for e2ee cert fetch');
3618
+ }
3619
+ // 跨域时用 peer 所在域的 Gateway URL
3620
+ const peerGatewayUrl = resolvePeerGatewayUrl(gatewayUrl, aid);
3621
+ let certPem;
3312
3622
  try {
3313
- const resp = await fetch(certUrl, { signal: controller.signal });
3314
- if (!resp.ok)
3315
- throw new ValidationError(`failed to fetch peer cert for ${aid}: HTTP ${resp.status}`);
3316
- certPem = await resp.text();
3623
+ const certUrl = buildCertUrl(peerGatewayUrl, aid, certFingerprint);
3624
+ // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3625
+ const controller = new AbortController();
3626
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
3627
+ try {
3628
+ const resp = await fetch(certUrl, { signal: controller.signal });
3629
+ if (!resp.ok)
3630
+ throw new ValidationError(`failed to fetch peer cert for ${aid}: HTTP ${resp.status}`);
3631
+ certPem = await resp.text();
3632
+ }
3633
+ finally {
3634
+ clearTimeout(timeoutId);
3635
+ }
3317
3636
  }
3318
- finally {
3319
- clearTimeout(timeoutId);
3637
+ catch (exc) {
3638
+ if (!certFingerprint) {
3639
+ throw exc;
3640
+ }
3641
+ // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3642
+ const fallbackController = new AbortController();
3643
+ const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), 5000);
3644
+ try {
3645
+ const fallbackResp = await fetch(buildCertUrl(peerGatewayUrl, aid), { signal: fallbackController.signal });
3646
+ if (!fallbackResp.ok) {
3647
+ throw exc;
3648
+ }
3649
+ certPem = await fallbackResp.text();
3650
+ }
3651
+ finally {
3652
+ clearTimeout(fallbackTimeoutId);
3653
+ }
3320
3654
  }
3321
- }
3322
- catch (exc) {
3323
- if (!certFingerprint) {
3324
- throw exc;
3655
+ // H7: 严格校验指纹(DER SHA-256 或 SPKI SHA-256 任一匹配即可)
3656
+ if (certFingerprint) {
3657
+ const expectedFP = String(certFingerprint).trim().toLowerCase();
3658
+ if (!expectedFP.startsWith('sha256:')) {
3659
+ throw new ValidationError(`unsupported cert_fingerprint format for ${aid}: ${expectedFP.slice(0, 24)}`);
3660
+ }
3661
+ const derFP = await this._certFingerprint(certPem);
3662
+ if (derFP !== expectedFP) {
3663
+ const spkiFP = await this._spkiFingerprint(certPem);
3664
+ if (!spkiFP || spkiFP !== expectedFP) {
3665
+ throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
3666
+ }
3667
+ }
3325
3668
  }
3326
- // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3327
- const fallbackController = new AbortController();
3328
- const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), 5000);
3669
+ // 完整 PKI 验证:链 + CRL + OCSP + AID 绑定
3329
3670
  try {
3330
- const fallbackResp = await fetch(buildCertUrl(peerGatewayUrl, aid), { signal: fallbackController.signal });
3331
- if (!fallbackResp.ok) {
3332
- throw exc;
3333
- }
3334
- certPem = await fallbackResp.text();
3671
+ await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
3335
3672
  }
3336
- finally {
3337
- clearTimeout(fallbackTimeoutId);
3673
+ catch (exc) {
3674
+ throw new ValidationError(`peer cert verification failed for ${aid}: ${exc}`);
3338
3675
  }
3339
- }
3340
- // H7: 严格校验指纹(DER SHA-256 或 SPKI SHA-256 任一匹配即可)
3341
- if (certFingerprint) {
3342
- const expectedFP = String(certFingerprint).trim().toLowerCase();
3343
- if (!expectedFP.startsWith('sha256:')) {
3344
- throw new ValidationError(`unsupported cert_fingerprint format for ${aid}: ${expectedFP.slice(0, 24)}`);
3676
+ this._certCache.set(cacheKey, {
3677
+ certPem,
3678
+ validatedAt: now,
3679
+ refreshAfter: now + PEER_CERT_CACHE_TTL,
3680
+ });
3681
+ try {
3682
+ // peer 证书只存版本目录,不覆盖 cert.pem
3683
+ await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3345
3684
  }
3346
- const derFP = await this._certFingerprint(certPem);
3347
- if (derFP !== expectedFP) {
3348
- const spkiFP = await this._spkiFingerprint(certPem);
3349
- if (!spkiFP || spkiFP !== expectedFP) {
3350
- throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
3351
- }
3685
+ catch (exc) {
3686
+ this._clientLog.error(`write cert to keystore failed (aid=${aid}): ${String(exc)}`, exc instanceof Error ? exc : undefined);
3352
3687
  }
3688
+ this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} source=fetched`);
3689
+ return certPem;
3353
3690
  }
3354
- // 完整 PKI 验证:链 + CRL + OCSP + AID 绑定
3355
- try {
3356
- await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
3357
- }
3358
- catch (exc) {
3359
- throw new ValidationError(`peer cert verification failed for ${aid}: ${exc}`);
3691
+ catch (err) {
3692
+ this._clientLog.debug(`_fetchPeerCert exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
3693
+ throw err;
3360
3694
  }
3361
- this._certCache.set(cacheKey, {
3362
- certPem,
3363
- validatedAt: now,
3364
- refreshAfter: now + PEER_CERT_CACHE_TTL,
3365
- });
3695
+ }
3696
+ /** 获取对方所有设备的 prekey(带缓存)。 */
3697
+ async _fetchPeerPrekeys(peerAid) {
3698
+ const tStart = Date.now();
3699
+ this._clientLog.debug(`_fetchPeerPrekeys enter: peer_aid=${peerAid}`);
3366
3700
  try {
3367
- // peer 证书只存版本目录,不覆盖 cert.pem
3368
- await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3369
- }
3370
- catch (exc) {
3371
- console.error(`写入证书到 keystore 失败 (aid=${aid}):`, exc);
3372
- }
3373
- return certPem;
3374
- }
3375
- /** 获取对方所有设备的 prekey(带缓存)。 */
3376
- async _fetchPeerPrekeys(peerAid) {
3377
- const cachedList = this._peerPrekeysCache.get(peerAid);
3378
- if (cachedList && Date.now() / 1000 < cachedList.expireAt) {
3379
- const normalized = normalizePeerPrekeys(cachedList.items);
3380
- if (normalized.length > 0)
3381
- return normalized.map((item) => ({ ...item }));
3382
- }
3383
- const cached = this._e2ee.getCachedPrekey(peerAid);
3384
- if (cached !== null) {
3385
- const normalized = normalizePeerPrekeys([cached]);
3386
- if (normalized.length > 0)
3387
- return normalized.map((item) => ({ ...item }));
3388
- }
3389
- let result;
3390
- try {
3391
- result = await this._transport.call('message.e2ee.get_prekey', { aid: peerAid });
3392
- }
3393
- catch (exc) {
3394
- throw new ValidationError(`failed to fetch peer prekey for ${peerAid}: ${String(exc)}`);
3395
- }
3396
- if (!isJsonObject(result)) {
3397
- throw new ValidationError(`invalid prekey response for ${peerAid}`);
3398
- }
3399
- if (result.found === false) {
3400
- return [];
3401
- }
3402
- const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
3403
- if (devicePrekeys) {
3404
- const normalized = normalizePeerPrekeys(devicePrekeys);
3405
- if (normalized.length > 0) {
3406
- this._peerPrekeysCache.set(peerAid, {
3407
- items: normalized.map((item) => ({ ...item })),
3408
- expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
3409
- });
3410
- this._e2ee.cachePrekey(peerAid, normalized[0]);
3411
- return normalized;
3701
+ const cachedList = this._peerPrekeysCache.get(peerAid);
3702
+ if (cachedList && Date.now() / 1000 < cachedList.expireAt) {
3703
+ const normalized = normalizePeerPrekeys(cachedList.items);
3704
+ if (normalized.length > 0) {
3705
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=list_cache`);
3706
+ return normalized.map((item) => ({ ...item }));
3707
+ }
3412
3708
  }
3413
- }
3414
- if (!isPeerPrekeyResponse(result)) {
3415
- throw new ValidationError(`invalid prekey response for ${peerAid}`);
3416
- }
3417
- if (result.prekey) {
3418
- const normalized = normalizePeerPrekeys([result.prekey]);
3419
- if (normalized.length > 0) {
3420
- this._peerPrekeysCache.set(peerAid, {
3421
- items: normalized.map((item) => ({ ...item })),
3422
- expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
3423
- });
3424
- this._e2ee.cachePrekey(peerAid, normalized[0]);
3425
- return normalized.map((item) => ({ ...item }));
3709
+ const cached = this._e2ee.getCachedPrekey(peerAid);
3710
+ if (cached !== null) {
3711
+ const normalized = normalizePeerPrekeys([cached]);
3712
+ if (normalized.length > 0) {
3713
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=e2ee_cache`);
3714
+ return normalized.map((item) => ({ ...item }));
3715
+ }
3716
+ }
3717
+ let result;
3718
+ try {
3719
+ result = await this._transport.call('message.e2ee.get_prekey', { aid: peerAid });
3720
+ }
3721
+ catch (exc) {
3722
+ throw new ValidationError(`failed to fetch peer prekey for ${peerAid}: ${String(exc)}`);
3723
+ }
3724
+ if (!isJsonObject(result)) {
3725
+ throw new ValidationError(`invalid prekey response for ${peerAid}`);
3726
+ }
3727
+ if (result.found === false) {
3728
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=0 reason=not_found`);
3729
+ return [];
3730
+ }
3731
+ const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
3732
+ if (devicePrekeys) {
3733
+ const normalized = normalizePeerPrekeys(devicePrekeys);
3734
+ if (normalized.length > 0) {
3735
+ this._peerPrekeysCache.set(peerAid, {
3736
+ items: normalized.map((item) => ({ ...item })),
3737
+ expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
3738
+ });
3739
+ this._e2ee.cachePrekey(peerAid, normalized[0]);
3740
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=device_prekeys`);
3741
+ return normalized;
3742
+ }
3743
+ }
3744
+ if (!isPeerPrekeyResponse(result)) {
3745
+ throw new ValidationError(`invalid prekey response for ${peerAid}`);
3746
+ }
3747
+ if (result.prekey) {
3748
+ const normalized = normalizePeerPrekeys([result.prekey]);
3749
+ if (normalized.length > 0) {
3750
+ this._peerPrekeysCache.set(peerAid, {
3751
+ items: normalized.map((item) => ({ ...item })),
3752
+ expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
3753
+ });
3754
+ this._e2ee.cachePrekey(peerAid, normalized[0]);
3755
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=single_prekey`);
3756
+ return normalized.map((item) => ({ ...item }));
3757
+ }
3758
+ }
3759
+ if (result.found) {
3760
+ throw new ValidationError(`invalid prekey response for ${peerAid}`);
3426
3761
  }
3762
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=0`);
3763
+ return [];
3427
3764
  }
3428
- if (result.found) {
3429
- throw new ValidationError(`invalid prekey response for ${peerAid}`);
3765
+ catch (err) {
3766
+ this._clientLog.debug(`_fetchPeerPrekeys exit (error): elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} err=${err instanceof Error ? err.message : String(err)}`);
3767
+ throw err;
3430
3768
  }
3431
- return [];
3432
3769
  }
3433
3770
  /** 清除对端 prekey 的双层缓存(_peerPrekeysCache + e2ee 内部缓存) */
3434
3771
  _invalidatePeerPrekeyCache(peerAid) {
@@ -3467,6 +3804,8 @@ export class AUNClient {
3467
3804
  async _uploadPrekey() {
3468
3805
  const prekeyMaterial = await this._e2ee.generatePrekey();
3469
3806
  const result = await this._transport.call('message.e2ee.put_prekey', prekeyMaterial);
3807
+ // 上传成功后记录为活跃 prekey
3808
+ this._activePrekeyId = String(prekeyMaterial.prekey_id ?? '');
3470
3809
  return isJsonObject(result) ? { ...result } : { ok: true };
3471
3810
  }
3472
3811
  /** 确保发送方证书在本地可用且未过期 */
@@ -3507,10 +3846,10 @@ export class AUNClient {
3507
3846
  catch (exc) {
3508
3847
  // 刷新失败时:若缓存有 PKI 验证过的证书(2 倍 TTL 内)则继续用
3509
3848
  if (cached && now < cached.validatedAt + PEER_CERT_CACHE_TTL * 2) {
3510
- console.warn(`刷新发送方 ${aid} 证书失败,继续使用已验证的内存缓存:`, exc);
3849
+ this._clientLog.warn(`refresh sender ${aid} cert failed, continue using verified memory cache: ${String(exc)}`);
3511
3850
  return true;
3512
3851
  }
3513
- console.warn(`获取发送方 ${aid} 证书失败且无已验证缓存,拒绝信任:`, exc);
3852
+ this._clientLog.warn(`fetch sender ${aid} cert failed and no verify cache, reject trust: ${String(exc)}`);
3514
3853
  return false;
3515
3854
  }
3516
3855
  }
@@ -3638,7 +3977,7 @@ export class AUNClient {
3638
3977
  }
3639
3978
  else {
3640
3979
  failed.push(String(dist.to));
3641
- console.warn('epoch 密钥分发失败 (to=%s):', dist.to, exc);
3980
+ this._clientLog.warn(`epoch keydistributefailed (to=%s):${dist.to} ${exc}`);
3642
3981
  }
3643
3982
  }
3644
3983
  }
@@ -3656,7 +3995,7 @@ export class AUNClient {
3656
3995
  return isJsonObject(result) && result.success === true;
3657
3996
  }
3658
3997
  catch (exc) {
3659
- console.warn(`[aun_core] 刷新 epoch rotation lease 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3998
+ this._clientLog.warn(`refresh epoch rotation lease failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3660
3999
  return false;
3661
4000
  }
3662
4001
  }
@@ -3672,7 +4011,7 @@ export class AUNClient {
3672
4011
  return isJsonObject(result) && result.success === true;
3673
4012
  }
3674
4013
  catch (exc) {
3675
- console.warn(`[aun_core] 提交 epoch key ack 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
4014
+ this._clientLog.warn(`commit epoch key ack failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3676
4015
  return false;
3677
4016
  }
3678
4017
  }
@@ -3681,6 +4020,11 @@ export class AUNClient {
3681
4020
  const groupId = String(payload.group_id ?? '').trim();
3682
4021
  if (!groupId)
3683
4022
  return false;
4023
+ // 历史群(不在当前 session 活跃列表):跳过 RPC 验证,只做本地 handle_incoming
4024
+ if (!this._groupSynced.has(groupId)) {
4025
+ this._clientLog.debug(`skip RPC verify for inactive group: group=${groupId} rotation=${rotationId}`);
4026
+ return true;
4027
+ }
3684
4028
  const epoch = Number(payload.epoch ?? 0);
3685
4029
  if (!Number.isFinite(epoch) || epoch <= 0)
3686
4030
  return false;
@@ -3700,7 +4044,7 @@ export class AUNClient {
3700
4044
  ? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
3701
4045
  : [];
3702
4046
  if (this._aid && !expectedMembers.includes(this._aid)) {
3703
- console.debug(`[aun_core] 放行 group key 分发:新成员恢复 commitment 不匹配属正常 group=${groupId} epoch=${epoch}`);
4047
+ this._clientLog.debug(`allow group key distribute: new member recover commitment mismatch is normal group=${groupId} epoch=${epoch}`);
3704
4048
  }
3705
4049
  else {
3706
4050
  return false;
@@ -3709,7 +4053,7 @@ export class AUNClient {
3709
4053
  }
3710
4054
  return true;
3711
4055
  }
3712
- console.info(`[aun_core] 拒绝缺少 rotation_id 的未来 epoch key 分发: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
4056
+ this._clientLog.info(`reject missing rotation_id future epoch key distribute: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
3713
4057
  return false;
3714
4058
  }
3715
4059
  const pending = isJsonObject(epochResult.pending_rotation) ? epochResult.pending_rotation : null;
@@ -3730,10 +4074,10 @@ export class AUNClient {
3730
4074
  }
3731
4075
  }
3732
4076
  catch (exc) {
3733
- console.warn(`[aun_core] 拒绝无法校验 active rotation epoch key 分发: group=${groupId} rotation=${rotationId} err=${formatCaughtError(exc)}`);
4077
+ this._clientLog.warn(`reject cannot check active rotation epoch key distribute: group=${groupId} rotation=${rotationId} err=${formatCaughtError(exc)}`);
3734
4078
  return false;
3735
4079
  }
3736
- console.info(`[aun_core] 拒绝非 pending/committed 状态的 epoch key 分发: group=${groupId} rotation=${rotationId} epoch=${epoch}`);
4080
+ this._clientLog.info(`reject non-pending/committed state epoch key distribute: group=${groupId} rotation=${rotationId} epoch=${epoch}`);
3737
4081
  return false;
3738
4082
  }
3739
4083
  async _discardGroupDistributionIfStale(payload) {
@@ -3748,10 +4092,10 @@ export class AUNClient {
3748
4092
  return;
3749
4093
  try {
3750
4094
  await this._groupE2ee.discardPendingSecret(groupId, epoch, rotationId);
3751
- console.info('丢弃 verify 后变为 stale group epoch key: group=%s epoch=%s rotation=%s', groupId, epoch, rotationId);
4095
+ this._clientLog.info('discard stale group epoch key after verify: group=%s epoch=%s rotation=%s', groupId, epoch, rotationId);
3752
4096
  }
3753
4097
  catch (exc) {
3754
- console.debug('清理 stale group epoch key 失败: group=%s epoch=%s rotation=%s err=%s', groupId, epoch, rotationId, formatCaughtError(exc));
4098
+ this._clientLog.debug('cleanup stale group epoch key failed: group=%s epoch=%s rotation=%s err=%s', groupId, epoch, rotationId, formatCaughtError(exc));
3755
4099
  }
3756
4100
  }
3757
4101
  async _verifyGroupKeyResponseEpoch(payload) {
@@ -3768,7 +4112,7 @@ export class AUNClient {
3768
4112
  return false;
3769
4113
  const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
3770
4114
  if (epoch > committedEpoch) {
3771
- console.info(`[aun_core] 拒绝未提交 epoch group key response: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
4115
+ this._clientLog.info(`reject uncommitted epoch group key response: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
3772
4116
  return false;
3773
4117
  }
3774
4118
  const committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
@@ -3779,7 +4123,7 @@ export class AUNClient {
3779
4123
  ? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
3780
4124
  : [];
3781
4125
  if (this._aid && !expectedMembers.includes(this._aid)) {
3782
- console.debug(`[aun_core] 放行 group key response:新成员恢复 commitment 不匹配属正常 group=${groupId} epoch=${epoch}`);
4126
+ this._clientLog.debug(`allow group key response: new member recover commitment mismatch is normal group=${groupId} epoch=${epoch}`);
3783
4127
  }
3784
4128
  else {
3785
4129
  return false;
@@ -3789,7 +4133,7 @@ export class AUNClient {
3789
4133
  return true;
3790
4134
  }
3791
4135
  catch (exc) {
3792
- console.warn(`[aun_core] 拒绝无法校验 committed epoch group key response: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
4136
+ this._clientLog.warn(`reject cannot check committed epoch group key response: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
3793
4137
  return false;
3794
4138
  }
3795
4139
  }
@@ -3804,7 +4148,7 @@ export class AUNClient {
3804
4148
  return isJsonObject(result) && result.success === true;
3805
4149
  }
3806
4150
  catch (exc) {
3807
- console.warn(`[aun_core] 中止 epoch rotation 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
4151
+ this._clientLog.warn(`abort epoch rotation failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3808
4152
  return false;
3809
4153
  }
3810
4154
  }
@@ -3848,7 +4192,7 @@ export class AUNClient {
3848
4192
  if (this._closing || this._state !== 'connected')
3849
4193
  return;
3850
4194
  if (Date.now() - started > 20000) {
3851
- console.warn('group epoch create sync still in-flight; skip duplicate sync (group=%s)', groupId);
4195
+ this._clientLog.warn(`group epoch create sync still in-flight; skip duplicate sync (group=%s)${String(groupId)}`);
3852
4196
  return;
3853
4197
  }
3854
4198
  await new Promise((resolve) => setTimeout(resolve, 200));
@@ -3882,12 +4226,12 @@ export class AUNClient {
3882
4226
  const beginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
3883
4227
  const rotation = isJsonObject(beginResult) && isJsonObject(beginResult.rotation) ? beginResult.rotation : null;
3884
4228
  if (!isJsonObject(beginResult) || beginResult.success !== true || !rotation) {
3885
- console.warn('group epoch begin failed; stop key distribution (group=%s, returned=%s)', groupId, JSON.stringify(beginResult));
4229
+ this._clientLog.warn('group epoch begin failed; stop key distribution (group=%s, returned=%s)', groupId, JSON.stringify(beginResult));
3886
4230
  return;
3887
4231
  }
3888
4232
  const activeRotationId = String(rotation.rotation_id ?? rotationId);
3889
4233
  if (!await this._ackGroupRotationKey(activeRotationId, secretData.commitment)) {
3890
- console.warn('group epoch self ack failed (group=%s, rotation=%s)', groupId, activeRotationId);
4234
+ this._clientLog.warn(`group epoch self ack failed (group=%s, rotation=%s)${groupId} ${activeRotationId}`);
3891
4235
  await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
3892
4236
  return;
3893
4237
  }
@@ -3904,17 +4248,17 @@ export class AUNClient {
3904
4248
  await storeGroupSecret(this._keystore, this._aid, groupId, 1, secretData.secret, secretData.commitment, secretData.member_aids, secretData.epoch_chain);
3905
4249
  return;
3906
4250
  }
3907
- console.warn('group epoch commit failed (group=%s, returned=%s)', groupId, JSON.stringify(commitResult));
4251
+ this._clientLog.warn('group epoch commit failed (group=%s, returned=%s)', groupId, JSON.stringify(commitResult));
3908
4252
  return;
3909
4253
  }
3910
4254
  catch (exc) {
3911
4255
  if (attempt < maxRetries) {
3912
4256
  const delay = 500 * Math.pow(2, attempt - 1);
3913
- console.warn(`同步 epoch 到服务端失败 (group=${groupId}, 第${attempt}/${maxRetries}), ${delay}ms后重试:`, exc);
4257
+ this._clientLog.warn(`sync epoch to server failed (group=${groupId}, #${attempt}/${maxRetries} ), ${delay}ms then retry: ${String(exc)}`);
3914
4258
  await new Promise(r => setTimeout(r, delay));
3915
4259
  }
3916
4260
  else {
3917
- console.error(`同步 epoch 到服务端最终失败 (group=${groupId}, 已重试${maxRetries}):`, exc);
4261
+ this._clientLog.error(`sync epoch to server final failed (group=${groupId}, retry${maxRetries} ): ${String(exc)}`, exc instanceof Error ? exc : undefined);
3918
4262
  }
3919
4263
  }
3920
4264
  }
@@ -3928,118 +4272,136 @@ export class AUNClient {
3928
4272
  * 避免所有剩余 admin 同时触发 `_rotateGroupEpoch` 造成 CAS 风暴。
3929
4273
  */
3930
4274
  async _maybeLeadRotateGroupEpoch(groupId, triggerId = '', expectedEpoch = null, allowMember = false) {
3931
- const myAid = this._aid;
3932
- if (!myAid || this._closing || this._state !== 'connected')
3933
- return;
3934
- const started = Date.now();
3935
- while (this._groupEpochRotationInflight.has(groupId)) {
3936
- if (triggerId && this._groupMembershipRotationDone.has(triggerId))
3937
- return;
3938
- if (this._closing || this._state !== 'connected')
3939
- return;
3940
- if (Date.now() - started > 20000) {
3941
- console.warn('group epoch rotation still in-flight; skip pending trigger (group=%s trigger=%s)', groupId, triggerId || '-');
3942
- return;
3943
- }
3944
- await new Promise((resolve) => setTimeout(resolve, 200));
3945
- }
3946
- if (this._closing || this._state !== 'connected')
3947
- return;
3948
- this._groupEpochRotationInflight.add(groupId);
4275
+ const tStart = Date.now();
4276
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch enter: group_id=${groupId} trigger=${triggerId || '-'} expected_epoch=${expectedEpoch ?? '-'} allow_member=${allowMember}`);
3949
4277
  try {
3950
- if (this._closing || this._state !== 'connected')
3951
- return;
3952
- const membersResp = await this.call('group.get_members', { group_id: groupId });
3953
- if (!isJsonObject(membersResp))
3954
- return;
3955
- const rawList = membersResp.members ?? membersResp.items;
3956
- if (!Array.isArray(rawList))
4278
+ const myAid = this._aid;
4279
+ if (!myAid || this._closing || this._state !== 'connected') {
4280
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=not_ready`);
3957
4281
  return;
3958
- const admins = [];
3959
- const members = [];
3960
- for (const m of rawList) {
3961
- if (!isJsonObject(m))
3962
- continue;
3963
- const role = String(m.role ?? '');
3964
- const aid = String(m.aid ?? '');
3965
- if (!aid)
3966
- continue;
3967
- if (role === 'admin' || role === 'owner') {
3968
- admins.push(aid);
4282
+ }
4283
+ const started = Date.now();
4284
+ while (this._groupEpochRotationInflight.has(groupId)) {
4285
+ if (triggerId && this._groupMembershipRotationDone.has(triggerId)) {
4286
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=trigger_done`);
4287
+ return;
4288
+ }
4289
+ if (this._closing || this._state !== 'connected') {
4290
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=disconnected_during_wait`);
4291
+ return;
3969
4292
  }
3970
- else if (allowMember && role === 'member') {
3971
- members.push(aid);
4293
+ if (Date.now() - started > 20000) {
4294
+ this._clientLog.warn(`group epoch rotation still in-flight; skip pending trigger (group=%s trigger=%s)${groupId} ${triggerId || '-'}`);
4295
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=inflight_timeout`);
4296
+ return;
3972
4297
  }
4298
+ await new Promise((resolve) => setTimeout(resolve, 200));
3973
4299
  }
3974
- // 候选列表:admin/owner 排序在前,member 排序在后
3975
- let candidates = [...admins.sort(), ...members.sort()];
3976
- if (candidates.length === 0)
4300
+ if (this._closing || this._state !== 'connected') {
4301
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=disconnected`);
3977
4302
  return;
3978
- // 没有当前 epoch key 的成员不参与 leader 选举
3979
- if (expectedEpoch !== null && expectedEpoch > 0) {
3980
- const localSecret = await this._groupE2ee.loadSecret(groupId, expectedEpoch);
3981
- if (!localSecret) {
3982
- const filtered = candidates.filter(c => c !== myAid);
3983
- if (filtered.length > 0) {
3984
- candidates = filtered;
4303
+ }
4304
+ this._groupEpochRotationInflight.add(groupId);
4305
+ try {
4306
+ if (this._closing || this._state !== 'connected')
4307
+ return;
4308
+ const membersResp = await this.call('group.get_members', { group_id: groupId });
4309
+ if (!isJsonObject(membersResp))
4310
+ return;
4311
+ const rawList = membersResp.members ?? membersResp.items;
4312
+ if (!Array.isArray(rawList))
4313
+ return;
4314
+ const admins = [];
4315
+ const members = [];
4316
+ for (const m of rawList) {
4317
+ if (!isJsonObject(m))
4318
+ continue;
4319
+ const role = String(m.role ?? '');
4320
+ const aid = String(m.aid ?? '');
4321
+ if (!aid)
4322
+ continue;
4323
+ if (role === 'admin' || role === 'owner') {
4324
+ admins.push(aid);
3985
4325
  }
3986
- else if (!allowMember) {
3987
- return;
4326
+ else if (allowMember && role === 'member') {
4327
+ members.push(aid);
3988
4328
  }
3989
4329
  }
3990
- }
3991
- const leader = candidates[0];
3992
- if (leader === myAid) {
3993
- // 我是 leader,直接发起
4330
+ // 候选列表:admin/owner 排序在前,member 排序在后
4331
+ let candidates = [...admins.sort(), ...members.sort()];
4332
+ if (candidates.length === 0)
4333
+ return;
4334
+ // 没有当前 epoch key 的成员不参与 leader 选举
4335
+ if (expectedEpoch !== null && expectedEpoch > 0) {
4336
+ const localSecret = await this._groupE2ee.loadSecret(groupId, expectedEpoch);
4337
+ if (!localSecret) {
4338
+ const filtered = candidates.filter(c => c !== myAid);
4339
+ if (filtered.length > 0) {
4340
+ candidates = filtered;
4341
+ }
4342
+ else if (!allowMember) {
4343
+ return;
4344
+ }
4345
+ }
4346
+ }
4347
+ const leader = candidates[0];
4348
+ if (leader === myAid) {
4349
+ // 我是 leader,直接发起
4350
+ await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
4351
+ return;
4352
+ }
4353
+ if (!candidates.includes(myAid))
4354
+ return;
4355
+ // 非 leader:随机 jitter(2~6s)后查询服务端 epoch 是否已被 leader 推进
4356
+ const jitterMs = 2000 + Math.floor(Math.random() * 4000);
4357
+ let beforeEpoch = 0;
4358
+ try {
4359
+ const resp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4360
+ if (isJsonObject(resp))
4361
+ beforeEpoch = Number(resp.epoch ?? 0);
4362
+ }
4363
+ catch {
4364
+ beforeEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4365
+ }
4366
+ await new Promise((r) => setTimeout(r, jitterMs));
4367
+ if (this._closing || this._state !== 'connected')
4368
+ return;
4369
+ let afterEpoch = 0;
4370
+ let afterResp = {};
4371
+ try {
4372
+ afterResp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4373
+ if (isJsonObject(afterResp))
4374
+ afterEpoch = Number(afterResp.epoch ?? 0);
4375
+ }
4376
+ catch {
4377
+ afterEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4378
+ }
4379
+ if (afterEpoch > beforeEpoch)
4380
+ return; // leader 已完成
4381
+ const pending = isJsonObject(afterResp) && isJsonObject(afterResp.pending_rotation) ? afterResp.pending_rotation : null;
4382
+ if (pending && !pending.expired) {
4383
+ this._scheduleGroupRotationRetry(groupId, {
4384
+ reason: 'membership_changed',
4385
+ triggerId,
4386
+ expectedEpoch,
4387
+ pending,
4388
+ });
4389
+ return;
4390
+ }
4391
+ this._clientLog.info(`[H21] leader did not complete epoch rotation, non-leader fallback: group=%s myAid=%s${groupId} ${myAid}`);
3994
4392
  await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
3995
- return;
3996
- }
3997
- if (!candidates.includes(myAid))
3998
- return;
3999
- // 非 leader:随机 jitter(2~6s)后查询服务端 epoch 是否已被 leader 推进
4000
- const jitterMs = 2000 + Math.floor(Math.random() * 4000);
4001
- let beforeEpoch = 0;
4002
- try {
4003
- const resp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4004
- if (isJsonObject(resp))
4005
- beforeEpoch = Number(resp.epoch ?? 0);
4006
- }
4007
- catch {
4008
- beforeEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4009
- }
4010
- await new Promise((r) => setTimeout(r, jitterMs));
4011
- if (this._closing || this._state !== 'connected')
4012
- return;
4013
- let afterEpoch = 0;
4014
- let afterResp = {};
4015
- try {
4016
- afterResp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4017
- if (isJsonObject(afterResp))
4018
- afterEpoch = Number(afterResp.epoch ?? 0);
4019
4393
  }
4020
- catch {
4021
- afterEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4394
+ catch (exc) {
4395
+ this._clientLog.warn(`_maybeLeadRotateGroupEpoch failed: %s${String(exc)}`);
4022
4396
  }
4023
- if (afterEpoch > beforeEpoch)
4024
- return; // leader 已完成
4025
- const pending = isJsonObject(afterResp) && isJsonObject(afterResp.pending_rotation) ? afterResp.pending_rotation : null;
4026
- if (pending && !pending.expired) {
4027
- this._scheduleGroupRotationRetry(groupId, {
4028
- reason: 'membership_changed',
4029
- triggerId,
4030
- expectedEpoch,
4031
- pending,
4032
- });
4033
- return;
4397
+ finally {
4398
+ this._groupEpochRotationInflight.delete(groupId);
4034
4399
  }
4035
- console.info('[H21] leader 未完成 epoch 轮换,非 leader 兜底: group=%s myAid=%s', groupId, myAid);
4036
- await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
4037
- }
4038
- catch (exc) {
4039
- console.warn('_maybeLeadRotateGroupEpoch 失败: %s', exc);
4400
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
4040
4401
  }
4041
- finally {
4042
- this._groupEpochRotationInflight.delete(groupId);
4402
+ catch (err) {
4403
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
4404
+ throw err;
4043
4405
  }
4044
4406
  }
4045
4407
  /**
@@ -4047,196 +4409,205 @@ export class AUNClient {
4047
4409
  * 使用服务端 CAS 保证只有一方成功。
4048
4410
  */
4049
4411
  async _rotateGroupEpoch(groupId, triggerId = '', expectedEpoch = null) {
4412
+ const tStart = Date.now();
4413
+ this._clientLog.debug(`_rotateGroupEpoch enter: group_id=${groupId} trigger=${triggerId || '-'} expected_epoch=${expectedEpoch ?? '-'}`);
4050
4414
  try {
4051
- if (!this._aid)
4052
- return;
4053
- const memberAids = await this._getGroupMemberAids(groupId);
4054
- if (triggerId && this._groupMembershipRotationDone.has(triggerId))
4055
- return;
4056
- const epochResult = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4057
- const serverEpoch = isJsonObject(epochResult) ? Number(epochResult.epoch ?? 0) : 0;
4058
- const pendingRotation = isJsonObject(epochResult) && isJsonObject(epochResult.pending_rotation)
4059
- ? epochResult.pending_rotation
4060
- : null;
4061
- if (pendingRotation && !pendingRotation.expired) {
4062
- const pendingRotationId = String(pendingRotation.rotation_id ?? '');
4063
- const stalePending = (expectedEpoch !== null
4064
- && serverEpoch === expectedEpoch
4065
- && this._rotationExpectedMembersStale(pendingRotation, memberAids));
4066
- if (stalePending && await this._abortGroupRotation(pendingRotationId, 'membership_changed_during_rotation')) {
4067
- console.info('aborted stale pending group epoch rotation: group=%s rotation=%s', groupId, pendingRotationId || '-');
4415
+ try {
4416
+ if (!this._aid)
4417
+ return;
4418
+ const memberAids = await this._getGroupMemberAids(groupId);
4419
+ if (triggerId && this._groupMembershipRotationDone.has(triggerId))
4420
+ return;
4421
+ const epochResult = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4422
+ const serverEpoch = isJsonObject(epochResult) ? Number(epochResult.epoch ?? 0) : 0;
4423
+ const pendingRotation = isJsonObject(epochResult) && isJsonObject(epochResult.pending_rotation)
4424
+ ? epochResult.pending_rotation
4425
+ : null;
4426
+ if (pendingRotation && !pendingRotation.expired) {
4427
+ const pendingRotationId = String(pendingRotation.rotation_id ?? '');
4428
+ const stalePending = (expectedEpoch !== null
4429
+ && serverEpoch === expectedEpoch
4430
+ && this._rotationExpectedMembersStale(pendingRotation, memberAids));
4431
+ if (stalePending && await this._abortGroupRotation(pendingRotationId, 'membership_changed_during_rotation')) {
4432
+ this._clientLog.info(`aborted stale pending group epoch rotation: group=%s rotation=%s${groupId} ${pendingRotationId || '-'}`);
4433
+ }
4434
+ else {
4435
+ this._scheduleGroupRotationRetry(groupId, {
4436
+ reason: 'membership_changed',
4437
+ triggerId,
4438
+ expectedEpoch,
4439
+ pending: pendingRotation,
4440
+ });
4441
+ return;
4442
+ }
4068
4443
  }
4069
- else {
4070
- this._scheduleGroupRotationRetry(groupId, {
4071
- reason: 'membership_changed',
4072
- triggerId,
4073
- expectedEpoch,
4074
- pending: pendingRotation,
4075
- });
4444
+ if (expectedEpoch !== null && serverEpoch !== expectedEpoch) {
4445
+ if (triggerId)
4446
+ this._groupMembershipRotationDone.add(triggerId);
4447
+ this._clientLog.info(`skip membership epoch rotation: group=%s expected_epoch=%d server_epoch=%d trigger=%s${groupId} ${expectedEpoch} ${serverEpoch} ${triggerId || '-'}`);
4076
4448
  return;
4077
4449
  }
4078
- }
4079
- if (expectedEpoch !== null && serverEpoch !== expectedEpoch) {
4080
- if (triggerId)
4081
- this._groupMembershipRotationDone.add(triggerId);
4082
- console.info('skip membership epoch rotation: group=%s expected_epoch=%d server_epoch=%d trigger=%s', groupId, expectedEpoch, serverEpoch, triggerId || '-');
4083
- return;
4084
- }
4085
- const currentEpoch = expectedEpoch ?? serverEpoch;
4086
- const targetEpoch = currentEpoch + 1;
4087
- let prevChainHint = null;
4088
- const localPrev = await this._groupE2ee.loadSecret(groupId, currentEpoch);
4089
- const localPrevChain = String(localPrev?.epoch_chain ?? '').trim();
4090
- if (!localPrevChain && isJsonObject(epochResult)) {
4091
- const cr = epochResult.committed_rotation;
4092
- if (isJsonObject(cr)) {
4093
- const rawChain = String(cr.epoch_chain ?? '').trim();
4094
- if (rawChain) {
4095
- prevChainHint = rawChain;
4096
- console.info(`[aun_core] 轮换补充 prev epoch chain from server: group=${groupId} epoch=${currentEpoch}`);
4450
+ const currentEpoch = expectedEpoch ?? serverEpoch;
4451
+ const targetEpoch = currentEpoch + 1;
4452
+ let prevChainHint = null;
4453
+ const localPrev = await this._groupE2ee.loadSecret(groupId, currentEpoch);
4454
+ const localPrevChain = String(localPrev?.epoch_chain ?? '').trim();
4455
+ if (!localPrevChain && isJsonObject(epochResult)) {
4456
+ const cr = epochResult.committed_rotation;
4457
+ if (isJsonObject(cr)) {
4458
+ const rawChain = String(cr.epoch_chain ?? '').trim();
4459
+ if (rawChain) {
4460
+ prevChainHint = rawChain;
4461
+ this._clientLog.info(`rotation supplement prev epoch chain from server: group=${groupId} epoch=${currentEpoch}`);
4462
+ }
4097
4463
  }
4098
4464
  }
4099
- }
4100
- const rotationId = `rot-${_uuidV4().replace(/-/g, '')}`;
4101
- const info = await this._groupE2ee.rotateEpochTo(groupId, targetEpoch, memberAids, { rotationId, prevChainHint });
4102
- this._attachRotationId(info, rotationId);
4103
- const discardGeneratedPending = async () => {
4465
+ const rotationId = `rot-${_uuidV4().replace(/-/g, '')}`;
4466
+ const info = await this._groupE2ee.rotateEpochTo(groupId, targetEpoch, memberAids, { rotationId, prevChainHint });
4467
+ this._attachRotationId(info, rotationId);
4468
+ const discardGeneratedPending = async () => {
4469
+ try {
4470
+ await this._groupE2ee.discardPendingSecret(groupId, targetEpoch, rotationId);
4471
+ }
4472
+ catch (cleanupExc) {
4473
+ this._clientLog.debug('cleanup local pending group key failed: group=%s epoch=%d rotation=%s err=%s', groupId, targetEpoch, rotationId, formatCaughtError(cleanupExc));
4474
+ }
4475
+ };
4476
+ const rotateParams = {
4477
+ group_id: groupId,
4478
+ base_epoch: currentEpoch,
4479
+ target_epoch: targetEpoch,
4480
+ rotation_id: rotationId,
4481
+ reason: triggerId || expectedEpoch !== null ? 'membership_changed' : 'manual',
4482
+ key_commitment: String(info.commitment ?? ''),
4483
+ expected_members: memberAids,
4484
+ required_acks: [this._aid],
4485
+ lease_ms: GROUP_ROTATION_LEASE_MS,
4486
+ };
4487
+ const sigParams = await this._buildRotationSignature(groupId, currentEpoch, targetEpoch, rotateParams);
4488
+ Object.assign(rotateParams, sigParams);
4489
+ let rawBeginResult;
4104
4490
  try {
4105
- await this._groupE2ee.discardPendingSecret(groupId, targetEpoch, rotationId);
4491
+ rawBeginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
4106
4492
  }
4107
- catch (cleanupExc) {
4108
- console.debug('清理本地 pending group key 失败: group=%s epoch=%d rotation=%s err=%s', groupId, targetEpoch, rotationId, formatCaughtError(cleanupExc));
4493
+ catch (exc) {
4494
+ await discardGeneratedPending();
4495
+ throw exc;
4109
4496
  }
4110
- };
4111
- const rotateParams = {
4112
- group_id: groupId,
4113
- base_epoch: currentEpoch,
4114
- target_epoch: targetEpoch,
4115
- rotation_id: rotationId,
4116
- reason: triggerId || expectedEpoch !== null ? 'membership_changed' : 'manual',
4117
- key_commitment: String(info.commitment ?? ''),
4118
- expected_members: memberAids,
4119
- required_acks: [this._aid],
4120
- lease_ms: GROUP_ROTATION_LEASE_MS,
4121
- };
4122
- const sigParams = await this._buildRotationSignature(groupId, currentEpoch, targetEpoch, rotateParams);
4123
- Object.assign(rotateParams, sigParams);
4124
- let rawBeginResult;
4125
- try {
4126
- rawBeginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
4127
- }
4128
- catch (exc) {
4129
- await discardGeneratedPending();
4130
- throw exc;
4131
- }
4132
- const beginResult = isJsonObject(rawBeginResult) ? rawBeginResult : null;
4133
- const beginRotationRaw = beginResult ? beginResult.rotation : null;
4134
- const rotation = isJsonObject(beginRotationRaw) ? beginRotationRaw : null;
4135
- if (!beginResult || beginResult.success !== true || !rotation) {
4136
- if (rotation && !rotation.expired) {
4137
- if (this._rotationExpectedMembersStale(rotation, memberAids)
4138
- && await this._abortGroupRotation(String(rotation.rotation_id ?? ''), 'membership_changed_during_rotation')) {
4139
- this._scheduleGroupRotationRetry(groupId, {
4140
- reason: 'membership_changed',
4141
- triggerId,
4142
- expectedEpoch,
4143
- pending: null,
4144
- });
4497
+ const beginResult = isJsonObject(rawBeginResult) ? rawBeginResult : null;
4498
+ const beginRotationRaw = beginResult ? beginResult.rotation : null;
4499
+ const rotation = isJsonObject(beginRotationRaw) ? beginRotationRaw : null;
4500
+ if (!beginResult || beginResult.success !== true || !rotation) {
4501
+ if (rotation && !rotation.expired) {
4502
+ if (this._rotationExpectedMembersStale(rotation, memberAids)
4503
+ && await this._abortGroupRotation(String(rotation.rotation_id ?? ''), 'membership_changed_during_rotation')) {
4504
+ this._scheduleGroupRotationRetry(groupId, {
4505
+ reason: 'membership_changed',
4506
+ triggerId,
4507
+ expectedEpoch,
4508
+ pending: null,
4509
+ });
4510
+ }
4511
+ else {
4512
+ this._scheduleGroupRotationRetry(groupId, {
4513
+ reason: 'membership_changed',
4514
+ triggerId,
4515
+ expectedEpoch,
4516
+ pending: rotation,
4517
+ });
4518
+ }
4145
4519
  }
4146
- else {
4520
+ else if (beginResult && beginResult.reason === 'expected_members_mismatch') {
4147
4521
  this._scheduleGroupRotationRetry(groupId, {
4148
4522
  reason: 'membership_changed',
4149
4523
  triggerId,
4150
4524
  expectedEpoch,
4151
- pending: rotation,
4525
+ pending: null,
4152
4526
  });
4153
4527
  }
4528
+ this._clientLog.warn('group epoch begin failed; stop key distribution (group=%s, current_epoch=%d, returned=%s)', groupId, currentEpoch, JSON.stringify(beginResult));
4529
+ await discardGeneratedPending();
4530
+ return;
4154
4531
  }
4155
- else if (beginResult && beginResult.reason === 'expected_members_mismatch') {
4532
+ const activeRotationId = String(rotation.rotation_id ?? rotationId);
4533
+ const distributeResult = await this._distributeGroupEpochKey(info, activeRotationId);
4534
+ if (distributeResult.failed.length > 0) {
4535
+ this._clientLog.warn('group epoch key distribution incomplete; abort rotation before retry (group=%s rotation=%s failed=%s)', groupId, activeRotationId, distributeResult.failed.join(','));
4536
+ await this._abortGroupRotation(activeRotationId, 'distribution_failed');
4156
4537
  this._scheduleGroupRotationRetry(groupId, {
4157
4538
  reason: 'membership_changed',
4158
4539
  triggerId,
4159
4540
  expectedEpoch,
4160
4541
  pending: null,
4161
4542
  });
4543
+ await discardGeneratedPending();
4544
+ return;
4162
4545
  }
4163
- console.warn('group epoch begin failed; stop key distribution (group=%s, current_epoch=%d, returned=%s)', groupId, currentEpoch, JSON.stringify(beginResult));
4164
- await discardGeneratedPending();
4165
- return;
4166
- }
4167
- const activeRotationId = String(rotation.rotation_id ?? rotationId);
4168
- const distributeResult = await this._distributeGroupEpochKey(info, activeRotationId);
4169
- if (distributeResult.failed.length > 0) {
4170
- console.warn('group epoch key distribution incomplete; abort rotation before retry (group=%s rotation=%s failed=%s)', groupId, activeRotationId, distributeResult.failed.join(','));
4171
- await this._abortGroupRotation(activeRotationId, 'distribution_failed');
4172
- this._scheduleGroupRotationRetry(groupId, {
4173
- reason: 'membership_changed',
4174
- triggerId,
4175
- expectedEpoch,
4176
- pending: null,
4177
- });
4178
- await discardGeneratedPending();
4179
- return;
4180
- }
4181
- await this._heartbeatGroupRotation(activeRotationId);
4182
- if (!await this._ackGroupRotationKey(activeRotationId, String(info.commitment ?? ''))) {
4183
- console.warn('group epoch self ack failed; abort rotation before retry (group=%s rotation=%s)', groupId, activeRotationId);
4184
- await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
4185
- this._scheduleGroupRotationRetry(groupId, {
4186
- reason: 'membership_changed',
4187
- triggerId,
4188
- expectedEpoch,
4189
- pending: null,
4190
- });
4191
- await discardGeneratedPending();
4192
- return;
4193
- }
4194
- const commitParams = { rotation_id: activeRotationId };
4195
- // 构建 per-member ECIES 加密的 epoch key 上传到服务端
4196
- if (await this._groupAllowsMemberEpochRotation(groupId)) {
4197
- const encryptedKeys = await this._buildEpochEncryptedKeys(info, memberAids, targetEpoch, groupId);
4198
- if (encryptedKeys && Object.keys(encryptedKeys).length > 0) {
4199
- commitParams.encrypted_keys = encryptedKeys;
4200
- }
4201
- }
4202
- const commitResult = await this.call('group.e2ee.commit_rotation', commitParams);
4203
- if (!isJsonObject(commitResult) || commitResult.success !== true) {
4204
- console.warn('group epoch commit failed (group=%s, rotation=%s, returned=%s)', groupId, activeRotationId, JSON.stringify(commitResult));
4205
- this._scheduleGroupRotationRetry(groupId, {
4206
- reason: 'membership_changed',
4207
- triggerId,
4208
- expectedEpoch,
4209
- pending: isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : rotation,
4210
- });
4211
- const returnedRotation = isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : null;
4212
- if (!(returnedRotation
4213
- && String(returnedRotation.rotation_id ?? '') === activeRotationId
4214
- && String(returnedRotation.status ?? '') === 'distributing')) {
4546
+ await this._heartbeatGroupRotation(activeRotationId);
4547
+ if (!await this._ackGroupRotationKey(activeRotationId, String(info.commitment ?? ''))) {
4548
+ this._clientLog.warn(`group epoch self ack failed; abort rotation before retry (group=%s rotation=%s)${groupId} ${activeRotationId}`);
4549
+ await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
4550
+ this._scheduleGroupRotationRetry(groupId, {
4551
+ reason: 'membership_changed',
4552
+ triggerId,
4553
+ expectedEpoch,
4554
+ pending: null,
4555
+ });
4215
4556
  await discardGeneratedPending();
4557
+ return;
4216
4558
  }
4217
- return;
4218
- }
4219
- const committedSecret = await this._groupE2ee.loadSecret(groupId, targetEpoch);
4220
- if (committedSecret && this._aid) {
4221
- const committedRotation = isJsonObject(commitResult.rotation)
4222
- ? commitResult.rotation
4223
- : { rotation_id: activeRotationId, key_commitment: String(info.commitment ?? '') };
4224
- if (this._groupSecretMatchesCommittedRotation(committedSecret, committedRotation)) {
4225
- await storeGroupSecret(this._keystore, this._aid, groupId, targetEpoch, committedSecret.secret, committedSecret.commitment, committedSecret.member_aids.length > 0 ? committedSecret.member_aids : memberAids, committedSecret.epoch_chain);
4559
+ const commitParams = { rotation_id: activeRotationId };
4560
+ // 构建 per-member ECIES 加密的 epoch key 上传到服务端
4561
+ if (await this._groupAllowsMemberEpochRotation(groupId)) {
4562
+ const encryptedKeys = await this._buildEpochEncryptedKeys(info, memberAids, targetEpoch, groupId);
4563
+ if (encryptedKeys && Object.keys(encryptedKeys).length > 0) {
4564
+ commitParams.encrypted_keys = encryptedKeys;
4565
+ }
4226
4566
  }
4227
- else {
4228
- console.warn('group epoch commit succeeded but local target key does not match committed rotation; keep pending blocked (group=%s rotation=%s epoch=%d)', groupId, activeRotationId, targetEpoch);
4567
+ const commitResult = await this.call('group.e2ee.commit_rotation', commitParams);
4568
+ if (!isJsonObject(commitResult) || commitResult.success !== true) {
4569
+ this._clientLog.warn('group epoch commit failed (group=%s, rotation=%s, returned=%s)', groupId, activeRotationId, JSON.stringify(commitResult));
4570
+ this._scheduleGroupRotationRetry(groupId, {
4571
+ reason: 'membership_changed',
4572
+ triggerId,
4573
+ expectedEpoch,
4574
+ pending: isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : rotation,
4575
+ });
4576
+ const returnedRotation = isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : null;
4577
+ if (!(returnedRotation
4578
+ && String(returnedRotation.rotation_id ?? '') === activeRotationId
4579
+ && String(returnedRotation.status ?? '') === 'distributing')) {
4580
+ await discardGeneratedPending();
4581
+ }
4582
+ return;
4229
4583
  }
4230
- }
4231
- if (triggerId) {
4232
- this._groupMembershipRotationDone.add(triggerId);
4233
- if (this._groupMembershipRotationDone.size > 2000) {
4234
- this._groupMembershipRotationDone = new Set(Array.from(this._groupMembershipRotationDone).slice(-1000));
4584
+ const committedSecret = await this._groupE2ee.loadSecret(groupId, targetEpoch);
4585
+ if (committedSecret && this._aid) {
4586
+ const committedRotation = isJsonObject(commitResult.rotation)
4587
+ ? commitResult.rotation
4588
+ : { rotation_id: activeRotationId, key_commitment: String(info.commitment ?? '') };
4589
+ if (this._groupSecretMatchesCommittedRotation(committedSecret, committedRotation)) {
4590
+ await storeGroupSecret(this._keystore, this._aid, groupId, targetEpoch, committedSecret.secret, committedSecret.commitment, committedSecret.member_aids.length > 0 ? committedSecret.member_aids : memberAids, committedSecret.epoch_chain);
4591
+ }
4592
+ else {
4593
+ this._clientLog.warn(`group epoch commit succeeded but local target key does not match committed rotation; keep pending blocked (group=%s rotation=%s epoch=%d)${groupId} ${activeRotationId} ${targetEpoch}`);
4594
+ }
4595
+ }
4596
+ if (triggerId) {
4597
+ this._groupMembershipRotationDone.add(triggerId);
4598
+ if (this._groupMembershipRotationDone.size > 2000) {
4599
+ this._groupMembershipRotationDone = new Set(Array.from(this._groupMembershipRotationDone).slice(-1000));
4600
+ }
4235
4601
  }
4236
4602
  }
4603
+ catch (exc) {
4604
+ this._logE2eeError('rotate_epoch', groupId, '', exc);
4605
+ }
4606
+ this._clientLog.debug(`_rotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
4237
4607
  }
4238
- catch (exc) {
4239
- this._logE2eeError('rotate_epoch', groupId, '', exc);
4608
+ catch (err) {
4609
+ this._clientLog.debug(`_rotateGroupEpoch exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
4610
+ throw err;
4240
4611
  }
4241
4612
  }
4242
4613
  /** 从成员加入事件 payload 中提取新加入的成员 AID 列表。 */
@@ -4416,73 +4787,91 @@ export class AUNClient {
4416
4787
  }
4417
4788
  // ── 内部:连接 ────────────────────────────────────
4418
4789
  async _connectOnce(params, allowReauth) {
4419
- const gatewayUrl = this._resolveGateway(params);
4420
- this._gatewayUrl = gatewayUrl;
4421
- this._slotId = String(params.slot_id ?? '');
4422
- this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
4423
- this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
4424
- this._state = 'connecting';
4425
- // 前置 restore:在 _transport.connect 启动 reader 之前完成,
4426
- // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
4427
- this._refreshSeqTrackerContext();
4428
- await this._restoreSeqTrackerState();
4790
+ const tStart = Date.now();
4791
+ this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
4429
4792
  try {
4430
- const challenge = await this._transport.connect(gatewayUrl);
4431
- this._state = 'authenticating';
4432
- if (allowReauth) {
4433
- const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
4434
- accessToken: params.access_token,
4435
- deviceId: this._deviceId,
4436
- slotId: this._slotId,
4437
- deliveryMode: this._connectDeliveryMode,
4438
- });
4439
- if (isJsonObject(authContext)) {
4440
- const auth = authContext;
4441
- const identity = auth.identity;
4442
- if (identity && isJsonObject(identity)) {
4443
- this._identity = identity;
4444
- this._aid = String(identity.aid ?? this._aid ?? '');
4445
- if (this._sessionParams) {
4446
- this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
4793
+ const gatewayUrl = this._resolveGateway(params);
4794
+ this._gatewayUrl = gatewayUrl;
4795
+ this._slotId = String(params.slot_id ?? '');
4796
+ this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
4797
+ this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
4798
+ this._state = 'connecting';
4799
+ // 前置 restore:在 _transport.connect 启动 reader 之前完成,
4800
+ // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
4801
+ this._refreshSeqTrackerContext();
4802
+ await this._restoreSeqTrackerState();
4803
+ try {
4804
+ const challenge = await this._transport.connect(gatewayUrl);
4805
+ this._state = 'authenticating';
4806
+ if (allowReauth) {
4807
+ const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
4808
+ accessToken: params.access_token,
4809
+ deviceId: this._deviceId,
4810
+ slotId: this._slotId,
4811
+ deliveryMode: this._connectDeliveryMode,
4812
+ connectionKind: String(params.connection_kind ?? 'long'),
4813
+ shortTtlMs: Number(params.short_ttl_ms ?? 0),
4814
+ extraInfo: params.extra_info,
4815
+ });
4816
+ if (isJsonObject(authContext)) {
4817
+ const auth = authContext;
4818
+ const identity = auth.identity;
4819
+ if (identity && isJsonObject(identity)) {
4820
+ this._identity = identity;
4821
+ this._aid = String(identity.aid ?? this._aid ?? '');
4822
+ if (this._sessionParams) {
4823
+ this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
4824
+ }
4447
4825
  }
4448
4826
  }
4449
4827
  }
4828
+ else {
4829
+ await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4830
+ deviceId: this._deviceId,
4831
+ slotId: this._slotId,
4832
+ deliveryMode: this._connectDeliveryMode,
4833
+ connectionKind: String(params.connection_kind ?? 'long'),
4834
+ shortTtlMs: Number(params.short_ttl_ms ?? 0),
4835
+ extraInfo: params.extra_info,
4836
+ });
4837
+ await this._syncIdentityAfterConnect(String(params.access_token));
4838
+ }
4450
4839
  }
4451
- else {
4452
- await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4453
- deviceId: this._deviceId,
4454
- slotId: this._slotId,
4455
- deliveryMode: this._connectDeliveryMode,
4456
- });
4457
- await this._syncIdentityAfterConnect(String(params.access_token));
4840
+ catch (err) {
4841
+ // P1-19: 首连失败时重置状态,避免半连接残留
4842
+ this._state = 'disconnected';
4843
+ try {
4844
+ await this._transport.close();
4845
+ }
4846
+ catch { /* 忽略关闭错误 */ }
4847
+ throw err;
4458
4848
  }
4459
- }
4460
- catch (err) {
4461
- // P1-19: 首连失败时重置状态,避免半连接残留
4462
- this._state = 'disconnected';
4849
+ this._state = 'connected';
4850
+ await this._dispatcher.publish('connection.state', {
4851
+ state: this._state,
4852
+ gateway: gatewayUrl,
4853
+ });
4854
+ // auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
4855
+ if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
4856
+ this._refreshSeqTrackerContext();
4857
+ await this._restoreSeqTrackerState();
4858
+ }
4859
+ this._startBackgroundTasks();
4860
+ // 上线后自动上传 prekey
4463
4861
  try {
4464
- await this._transport.close();
4862
+ await this._uploadPrekey();
4465
4863
  }
4466
- catch { /* 忽略关闭错误 */ }
4467
- throw err;
4468
- }
4469
- this._state = 'connected';
4470
- await this._dispatcher.publish('connection.state', {
4471
- state: this._state,
4472
- gateway: gatewayUrl,
4473
- });
4474
- // auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
4475
- if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
4476
- this._refreshSeqTrackerContext();
4477
- await this._restoreSeqTrackerState();
4478
- }
4479
- this._startBackgroundTasks();
4480
- // 上线后自动上传 prekey
4481
- try {
4482
- await this._uploadPrekey();
4864
+ catch (exc) {
4865
+ this._clientLog.warn(`prekey upload failed:${String(exc)}`);
4866
+ }
4867
+ // connect/reconnect 成功后自动触发一次 P2P message.pull,补齐离线期间积压
4868
+ // 群消息按惰性触发,不在此处主动 pull
4869
+ this._safeAsync(this._fillP2pGap());
4870
+ this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? '-'}`);
4483
4871
  }
4484
- catch (exc) {
4485
- console.warn('prekey 上传失败:', exc);
4872
+ catch (err) {
4873
+ this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
4874
+ throw err;
4486
4875
  }
4487
4876
  }
4488
4877
  _resolveGateway(params) {
@@ -4563,15 +4952,42 @@ export class AUNClient {
4563
4952
  if (request.timeouts !== undefined && !isJsonObject(request.timeouts)) {
4564
4953
  throw new ValidationError('timeouts must be an object');
4565
4954
  }
4955
+ // 长短连接选项:默认 long,向后兼容
4956
+ const kindRaw = request.connection_kind;
4957
+ if (kindRaw == null) {
4958
+ request.connection_kind = 'long';
4959
+ }
4960
+ else {
4961
+ request.connection_kind = String(kindRaw).trim().toLowerCase();
4962
+ }
4963
+ if (request.connection_kind !== 'long' && request.connection_kind !== 'short') {
4964
+ throw new ValidationError("connection_kind must be 'long' or 'short'");
4965
+ }
4966
+ try {
4967
+ request.short_ttl_ms = Math.max(0, Math.floor(Number(request.short_ttl_ms) || 0));
4968
+ }
4969
+ catch {
4970
+ throw new ValidationError('short_ttl_ms must be a non-negative integer');
4971
+ }
4972
+ if (request.connection_kind !== 'short') {
4973
+ request.short_ttl_ms = 0;
4974
+ }
4566
4975
  return request;
4567
4976
  }
4568
4977
  _buildSessionOptions(params) {
4978
+ const connectionKind = String(params.connection_kind ?? 'long');
4979
+ // 短连接默认禁用 auto_reconnect:短连接生命周期短,自动重连无意义
4980
+ const defaultAutoReconnect = connectionKind === 'short'
4981
+ ? false
4982
+ : DEFAULT_SESSION_OPTIONS.auto_reconnect;
4569
4983
  const options = {
4570
- auto_reconnect: DEFAULT_SESSION_OPTIONS.auto_reconnect,
4984
+ auto_reconnect: defaultAutoReconnect,
4571
4985
  heartbeat_interval: DEFAULT_SESSION_OPTIONS.heartbeat_interval,
4572
4986
  token_refresh_before: DEFAULT_SESSION_OPTIONS.token_refresh_before,
4573
4987
  retry: { ...DEFAULT_SESSION_OPTIONS.retry },
4574
4988
  timeouts: { ...DEFAULT_SESSION_OPTIONS.timeouts },
4989
+ connection_kind: connectionKind,
4990
+ short_ttl_ms: Number(params.short_ttl_ms ?? 0),
4575
4991
  };
4576
4992
  if ('auto_reconnect' in params) {
4577
4993
  options.auto_reconnect = Boolean(params.auto_reconnect);
@@ -4592,6 +5008,10 @@ export class AUNClient {
4592
5008
  }
4593
5009
  // ── 内部:后台任务 ────────────────────────────────
4594
5010
  _startBackgroundTasks() {
5011
+ // 短连接生命周期短,禁用心跳与 token 刷新(不接收推送、不需要长期会话维护)
5012
+ if (this._sessionOptions?.connection_kind === 'short') {
5013
+ return;
5014
+ }
4595
5015
  this._startHeartbeat();
4596
5016
  this._startTokenRefresh();
4597
5017
  this._startPrekeyRefresh();
@@ -4650,10 +5070,10 @@ export class AUNClient {
4650
5070
  }
4651
5071
  catch (exc) {
4652
5072
  consecutiveFailures++;
4653
- console.warn(`心跳失败 (${consecutiveFailures}/${maxFailures}):`, exc);
5073
+ this._clientLog.warn(`heartbeat failed (${consecutiveFailures}/${maxFailures}): ${String(exc)}`);
4654
5074
  this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
4655
5075
  if (consecutiveFailures >= maxFailures) {
4656
- console.warn(`连续 ${maxFailures} 次心跳失败,触发断线重连`);
5076
+ this._clientLog.warn(`consecutive ${maxFailures} heartbeat failed, trigger disconnect reconnect`);
4657
5077
  this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
4658
5078
  }
4659
5079
  }
@@ -4712,7 +5132,7 @@ export class AUNClient {
4712
5132
  if (exc instanceof AuthError) {
4713
5133
  this._tokenRefreshFailures++;
4714
5134
  if (this._tokenRefreshFailures >= 3) {
4715
- console.warn(`token 刷新连续失败 ${this._tokenRefreshFailures} 次,停止刷新循环并触发重连`);
5135
+ this._clientLog.warn(`token refreshconsecutivefailed ${this._tokenRefreshFailures} , stop refresh loop and trigger reconnect`);
4716
5136
  this._dispatcher.publish('token.refresh_exhausted', {
4717
5137
  aid: this._identity?.aid ?? null,
4718
5138
  consecutive_failures: this._tokenRefreshFailures,
@@ -4722,7 +5142,7 @@ export class AUNClient {
4722
5142
  this._handleTransportDisconnect(new Error('token refresh exhausted, triggering reconnect'));
4723
5143
  return;
4724
5144
  }
4725
- console.warn(`token 刷新失败 (${this._tokenRefreshFailures}/3),下次重试:`, exc);
5145
+ this._clientLog.warn(`token refresh failed (${this._tokenRefreshFailures}/3), next retry: ${String(exc)}`);
4726
5146
  }
4727
5147
  else {
4728
5148
  this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
@@ -4770,7 +5190,7 @@ export class AUNClient {
4770
5190
  }
4771
5191
  }
4772
5192
  catch (exc) {
4773
- console.warn('[aun_core] prekey 定时刷新失败:', exc);
5193
+ this._clientLog.warn(`prekey scheduled refresh failed:${String(exc)}`);
4774
5194
  }
4775
5195
  // 仍处于连接状态时安排下一次检查
4776
5196
  if (this._state === 'connected') {
@@ -4857,22 +5277,17 @@ export class AUNClient {
4857
5277
  const prekeyId = this._extractConsumedPrekeyId(message);
4858
5278
  if (!prekeyId || this._state !== 'connected')
4859
5279
  return;
4860
- if (this._prekeyReplenished.has(prekeyId))
4861
- return;
4862
- // 同一时刻只允许一个 put_prekey inflight
4863
- if (this._prekeyReplenishInflight.size > 0)
5280
+ // 只有活跃 prekey 被消费时才触发上传。历史 prekey 被消费不触发,避免上传风暴。
5281
+ if (!this._activePrekeyId || prekeyId !== this._activePrekeyId)
4864
5282
  return;
4865
- this._prekeyReplenishInflight.add(prekeyId);
5283
+ // 清空活跃标记,防止重复触发(新上传完成后会设新的 active)
5284
+ this._activePrekeyId = '';
4866
5285
  this._safeAsync((async () => {
4867
5286
  try {
4868
5287
  await this._uploadPrekey();
4869
- this._prekeyReplenished.add(prekeyId);
4870
5288
  }
4871
5289
  catch (exc) {
4872
- console.warn(`消费 prekey ${prekeyId} 后补充 current prekey 失败:`, exc);
4873
- }
4874
- finally {
4875
- this._prekeyReplenishInflight.delete(prekeyId);
5290
+ this._clientLog.warn(`replenish current prekey after consuming ${prekeyId} failed: ${String(exc)}`);
4876
5291
  }
4877
5292
  })());
4878
5293
  }
@@ -4894,7 +5309,7 @@ export class AUNClient {
4894
5309
  }
4895
5310
  }
4896
5311
  catch (exc) {
4897
- console.warn('epoch 清理失败:', exc);
5312
+ this._clientLog.warn(`epoch cleanup failed:${String(exc)}`);
4898
5313
  }
4899
5314
  }, 3600 * 1000);
4900
5315
  }
@@ -4913,7 +5328,7 @@ export class AUNClient {
4913
5328
  }
4914
5329
  }
4915
5330
  catch (exc) {
4916
- console.warn('epoch 定时轮换失败:', exc);
5331
+ this._clientLog.warn(`epoch scheduled rotation failed:${String(exc)}`);
4917
5332
  }
4918
5333
  }, rotateInterval * 1000);
4919
5334
  }
@@ -4941,13 +5356,28 @@ export class AUNClient {
4941
5356
  }
4942
5357
  // ── 内部:断线重连 ────────────────────────────────
4943
5358
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
4944
- static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011]);
4945
- /** 处理服务端主动断开通知 event/gateway.disconnect */
4946
- _onGatewayDisconnect(data) {
4947
- const code = data?.code;
4948
- const reason = data?.reason ?? '';
4949
- console.warn(`[aun_core] 服务端主动断开: code=${code}, reason=${reason}`);
5359
+ static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015]);
5360
+ /** 处理服务端主动断开通知 event/gateway.disconnect
5361
+ *
5362
+ * 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
5363
+ * 透传到应用层可订阅事件 'gateway.disconnect',方便业务定位被踢原因。
5364
+ */
5365
+ async _onGatewayDisconnect(data) {
5366
+ const obj = (data && typeof data === 'object') ? data : {};
5367
+ const code = obj.code;
5368
+ const reason = obj.reason ?? '';
5369
+ const detail = (obj.detail && typeof obj.detail === 'object') ? obj.detail : {};
5370
+ this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
4950
5371
  this._serverKicked = true;
5372
+ // 缓存最近一次 disconnect 信息,让后续 connection.state(terminal_failed) 也能带 detail
5373
+ this._lastDisconnectInfo = { code, reason, detail };
5374
+ // 透传给应用层订阅者
5375
+ try {
5376
+ await this._dispatcher.publish('gateway.disconnect', { code, reason, detail });
5377
+ }
5378
+ catch (exc) {
5379
+ this._clientLog.debug(`publish gateway.disconnect failed: ${exc?.message ?? exc}`);
5380
+ }
4951
5381
  }
4952
5382
  async _handleTransportDisconnect(error, closeCode) {
4953
5383
  if (this._closing || this._state === 'closed')
@@ -4967,10 +5397,20 @@ export class AUNClient {
4967
5397
  if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
4968
5398
  this._state = 'terminal_failed';
4969
5399
  const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
4970
- console.warn(`[aun_core] 抑制自动重连: ${reason}`);
4971
- await this._dispatcher.publish('connection.state', {
5400
+ this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
5401
+ const disconnectInfo = this._lastDisconnectInfo ?? {};
5402
+ const eventPayload = {
4972
5403
  state: this._state, error, reason,
4973
- });
5404
+ };
5405
+ // 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
5406
+ const detail = disconnectInfo.detail;
5407
+ if (detail && typeof detail === 'object' && Object.keys(detail).length > 0) {
5408
+ eventPayload.detail = detail;
5409
+ }
5410
+ if (disconnectInfo.code !== undefined && disconnectInfo.code !== null) {
5411
+ eventPayload.code = disconnectInfo.code;
5412
+ }
5413
+ await this._dispatcher.publish('connection.state', eventPayload);
4974
5414
  return;
4975
5415
  }
4976
5416
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
@@ -5057,62 +5497,80 @@ export class AUNClient {
5057
5497
  * 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
5058
5498
  */
5059
5499
  async createNamedGroup(groupName, opts = {}) {
5060
- const cp = new CryptoProvider();
5061
- const identity = await cp.generateIdentity();
5062
- const params = {};
5063
- for (const [k, v] of Object.entries(opts)) {
5064
- params[k] = v;
5065
- }
5066
- params.group_name = groupName;
5067
- params.public_key = identity.public_key_der_b64;
5068
- params.curve = 'P-256';
5069
- const result = await this.call('group.create', params);
5070
- const groupInfo = result?.group;
5071
- const aidCert = result?.aid_cert;
5072
- const groupAid = String(groupInfo?.group_aid ?? '');
5073
- if (groupAid && aidCert) {
5074
- await this._keystore.saveIdentity(groupAid, {
5075
- private_key_pem: identity.private_key_pem,
5076
- public_key: identity.public_key_der_b64,
5077
- curve: 'P-256',
5078
- type: 'group_identity',
5079
- });
5080
- const certPem = String(aidCert.cert ?? '');
5081
- if (certPem) {
5082
- await this._keystore.saveCert(groupAid, certPem);
5500
+ const tStart = Date.now();
5501
+ this._clientLog.debug(`createNamedGroup enter: name=${groupName}`);
5502
+ try {
5503
+ const cp = new CryptoProvider();
5504
+ const identity = await cp.generateIdentity();
5505
+ const params = {};
5506
+ for (const [k, v] of Object.entries(opts)) {
5507
+ params[k] = v;
5508
+ }
5509
+ params.group_name = groupName;
5510
+ params.public_key = identity.public_key_der_b64;
5511
+ params.curve = 'P-256';
5512
+ const result = await this.call('group.create', params);
5513
+ const groupInfo = result?.group;
5514
+ const aidCert = result?.aid_cert;
5515
+ const groupAid = String(groupInfo?.group_aid ?? '');
5516
+ if (groupAid && aidCert) {
5517
+ await this._keystore.saveIdentity(groupAid, {
5518
+ private_key_pem: identity.private_key_pem,
5519
+ public_key: identity.public_key_der_b64,
5520
+ curve: 'P-256',
5521
+ type: 'group_identity',
5522
+ });
5523
+ const certPem = String(aidCert.cert ?? '');
5524
+ if (certPem) {
5525
+ await this._keystore.saveCert(groupAid, certPem);
5526
+ }
5083
5527
  }
5528
+ this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
5529
+ return result;
5530
+ }
5531
+ catch (err) {
5532
+ this._clientLog.debug(`createNamedGroup exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
5533
+ throw err;
5084
5534
  }
5085
- return result;
5086
5535
  }
5087
5536
  /**
5088
5537
  * 为已有普通群绑定命名 AID(升级为命名群)。
5089
5538
  */
5090
5539
  async bindGroupAid(groupId, groupName) {
5091
- const cp = new CryptoProvider();
5092
- const identity = await cp.generateIdentity();
5093
- const params = {
5094
- group_id: groupId,
5095
- group_name: groupName,
5096
- public_key: identity.public_key_der_b64,
5097
- curve: 'P-256',
5098
- };
5099
- const result = await this.call('group.bind_aid', params);
5100
- const groupInfo = result?.group;
5101
- const aidCert = result?.aid_cert;
5102
- const groupAid = String(groupInfo?.group_aid ?? '');
5103
- if (groupAid && aidCert) {
5104
- await this._keystore.saveIdentity(groupAid, {
5105
- private_key_pem: identity.private_key_pem,
5540
+ const tStart = Date.now();
5541
+ this._clientLog.debug(`bindGroupAid enter: group_id=${groupId} name=${groupName}`);
5542
+ try {
5543
+ const cp = new CryptoProvider();
5544
+ const identity = await cp.generateIdentity();
5545
+ const params = {
5546
+ group_id: groupId,
5547
+ group_name: groupName,
5106
5548
  public_key: identity.public_key_der_b64,
5107
5549
  curve: 'P-256',
5108
- type: 'group_identity',
5109
- });
5110
- const certPem = String(aidCert.cert ?? '');
5111
- if (certPem) {
5112
- await this._keystore.saveCert(groupAid, certPem);
5550
+ };
5551
+ const result = await this.call('group.bind_aid', params);
5552
+ const groupInfo = result?.group;
5553
+ const aidCert = result?.aid_cert;
5554
+ const groupAid = String(groupInfo?.group_aid ?? '');
5555
+ if (groupAid && aidCert) {
5556
+ await this._keystore.saveIdentity(groupAid, {
5557
+ private_key_pem: identity.private_key_pem,
5558
+ public_key: identity.public_key_der_b64,
5559
+ curve: 'P-256',
5560
+ type: 'group_identity',
5561
+ });
5562
+ const certPem = String(aidCert.cert ?? '');
5563
+ if (certPem) {
5564
+ await this._keystore.saveCert(groupAid, certPem);
5565
+ }
5113
5566
  }
5567
+ this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
5568
+ return result;
5569
+ }
5570
+ catch (err) {
5571
+ this._clientLog.debug(`bindGroupAid exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
5572
+ throw err;
5114
5573
  }
5115
- return result;
5116
5574
  }
5117
5575
  /** 判断是否应重试重连 */
5118
5576
  _shouldRetryReconnect(error) {
@@ -5338,7 +5796,7 @@ export class AUNClient {
5338
5796
  /** 安全执行异步操作(不阻塞调用方,错误打 warning 便于排障) */
5339
5797
  _safeAsync(promise) {
5340
5798
  promise.catch((exc) => {
5341
- console.warn('后台任务异常:', exc);
5799
+ this._clientLog.warn(`background task exception:${String(exc)}`);
5342
5800
  });
5343
5801
  }
5344
5802
  /** 可取消的 sleep */