@agentunion/fastaun-browser 0.2.17 → 0.2.18

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 (56) hide show
  1. package/dist/auth.d.ts +3 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +293 -211
  4. package/dist/auth.js.map +1 -1
  5. package/dist/client.d.ts +11 -0
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +1080 -812
  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 +176 -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/indexeddb.d.ts +3 -0
  29. package/dist/keystore/indexeddb.d.ts.map +1 -1
  30. package/dist/keystore/indexeddb.js +153 -97
  31. package/dist/keystore/indexeddb.js.map +1 -1
  32. package/dist/logger.d.ts +37 -0
  33. package/dist/logger.d.ts.map +1 -0
  34. package/dist/logger.js +112 -0
  35. package/dist/logger.js.map +1 -0
  36. package/dist/namespaces/auth.d.ts +4 -0
  37. package/dist/namespaces/auth.d.ts.map +1 -1
  38. package/dist/namespaces/auth.js +214 -101
  39. package/dist/namespaces/auth.js.map +1 -1
  40. package/dist/namespaces/custody.d.ts +3 -0
  41. package/dist/namespaces/custody.d.ts.map +1 -1
  42. package/dist/namespaces/custody.js +147 -75
  43. package/dist/namespaces/custody.js.map +1 -1
  44. package/dist/namespaces/meta.d.ts +3 -0
  45. package/dist/namespaces/meta.d.ts.map +1 -1
  46. package/dist/namespaces/meta.js +94 -43
  47. package/dist/namespaces/meta.js.map +1 -1
  48. package/dist/secret-store/indexeddb-store.d.ts +3 -0
  49. package/dist/secret-store/indexeddb-store.d.ts.map +1 -1
  50. package/dist/secret-store/indexeddb-store.js +57 -29
  51. package/dist/secret-store/indexeddb-store.js.map +1 -1
  52. package/dist/transport.d.ts +3 -0
  53. package/dist/transport.d.ts.map +1 -1
  54. package/dist/transport.js +74 -4
  55. package/dist/transport.js.map +1 -1
  56. package/package.json +1 -1
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
  /**
@@ -377,14 +378,36 @@ export class AUNClient {
377
378
  _reconnectActive = false;
378
379
  _reconnectAbort = null;
379
380
  _serverKicked = false;
381
+ // Logger(per-client 单例 + 各模块子 logger)
382
+ _logger;
383
+ _clientLog;
384
+ _logE2;
385
+ _logEG;
386
+ _logAuth;
387
+ _logTransport;
388
+ _logKeystore;
389
+ _logDiscovery;
390
+ _logEvents;
380
391
  constructor(config, _debug = false) {
381
392
  const rawConfig = config ?? {};
382
393
  this.configModel = createConfig(rawConfig);
394
+ const initAid = String(rawConfig.aid ?? '').trim() || null;
383
395
  this.config = {
384
396
  aun_path: this.configModel.aunPath,
385
397
  root_ca_path: this.configModel.rootCaPem,
386
398
  seed_password: this.configModel.seedPassword,
387
399
  };
400
+ // Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
401
+ this._logger = new AUNLogger({ debug: _debug });
402
+ this._clientLog = this._logger.for('aun_core.client');
403
+ this._logE2 = this._logger.for('aun_core.e2ee');
404
+ this._logEG = this._logger.for('aun_core.e2ee-group');
405
+ this._logAuth = this._logger.for('aun_core.auth');
406
+ this._logTransport = this._logger.for('aun_core.transport');
407
+ this._logKeystore = this._logger.for('aun_core.keystore');
408
+ this._logDiscovery = this._logger.for('aun_core.discovery');
409
+ this._logEvents = this._logger.for('aun_core.events');
410
+ this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
388
411
  this._dispatcher = new EventDispatcher();
389
412
  this._discovery = new GatewayDiscovery();
390
413
  this._keystore = new IndexedDBKeyStore();
@@ -395,12 +418,13 @@ export class AUNClient {
395
418
  this._auth = new AuthFlow({
396
419
  keystore: this._keystore,
397
420
  crypto: new CryptoProvider(),
398
- aid: null,
421
+ aid: initAid,
399
422
  deviceId: this._deviceId,
400
423
  slotId: this._slotId,
401
424
  rootCaPem: this.configModel.rootCaPem,
402
425
  verifySsl: this.configModel.verifySsl,
403
426
  });
427
+ this._aid = initAid;
404
428
  this._transport = new RPCTransport({
405
429
  eventDispatcher: this._dispatcher,
406
430
  timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
@@ -422,6 +446,29 @@ export class AUNClient {
422
446
  this.auth = new AuthNamespace(this);
423
447
  this.custody = new CustodyNamespace(this);
424
448
  this.meta = new MetaNamespace(this);
449
+ // 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
450
+ this._auth.setLogger(this._logAuth);
451
+ this._transport.setLogger(this._logTransport);
452
+ this._dispatcher.setLogger(this._logEvents);
453
+ this._e2ee.setLogger(this._logE2);
454
+ this._groupE2ee.setLogger(this._logEG);
455
+ setE2eeModuleLogger(this._logE2);
456
+ setE2eeGroupModuleLogger(this._logEG);
457
+ if (typeof this._discovery.setLogger === 'function') {
458
+ this._discovery.setLogger(this._logger.for('aun_core.discovery'));
459
+ }
460
+ if (typeof this.auth.setLogger === 'function') {
461
+ this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
462
+ }
463
+ if (typeof this.custody.setLogger === 'function') {
464
+ this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
465
+ }
466
+ if (typeof this._keystore.setLogger === 'function') {
467
+ this._keystore.setLogger(this._logKeystore);
468
+ }
469
+ if (typeof this.meta.setLogger === 'function') {
470
+ this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
471
+ }
425
472
  // 内部订阅:推送消息自动解密后 re-publish 给用户
426
473
  this._dispatcher.subscribe('_raw.message.received', (data) => {
427
474
  this._onRawMessageReceived(data);
@@ -471,7 +518,17 @@ export class AUNClient {
471
518
  }
472
519
  /** 主动检查 gateway 可用性(GET /health) */
473
520
  async checkGatewayHealth(gatewayUrl, timeout = 5000) {
474
- return this._discovery.checkHealth(gatewayUrl, timeout);
521
+ const tStart = Date.now();
522
+ this._clientLog.debug(`checkGatewayHealth enter: gateway=${gatewayUrl} timeout=${timeout}`);
523
+ try {
524
+ const result = await this._discovery.checkHealth(gatewayUrl, timeout);
525
+ this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
526
+ return result;
527
+ }
528
+ catch (err) {
529
+ this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
530
+ throw err;
531
+ }
475
532
  }
476
533
  get e2ee() {
477
534
  return this._e2ee;
@@ -487,7 +544,10 @@ export class AUNClient {
487
544
  * @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
488
545
  */
489
546
  async connect(auth, options) {
547
+ const tStart = Date.now();
548
+ this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
490
549
  if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
550
+ this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
491
551
  throw new StateError(`connect not allowed in state ${this._state}`);
492
552
  }
493
553
  this._state = 'connecting';
@@ -499,18 +559,23 @@ export class AUNClient {
499
559
  this._closing = false;
500
560
  try {
501
561
  await this._connectOnce(normalized, false);
562
+ this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
502
563
  }
503
564
  catch (err) {
504
565
  // 连接失败时回退状态,允许重试
505
566
  if (this._state === 'connecting' || this._state === 'authenticating') {
506
567
  this._state = 'disconnected';
507
568
  }
569
+ this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
508
570
  throw err;
509
571
  }
510
572
  }
511
573
  /** 断开连接但保留本地状态,可再次 connect */
512
574
  async disconnect() {
575
+ const tStart = Date.now();
576
+ this._clientLog.debug(`disconnect enter: state=${this._state}`);
513
577
  if (this._state !== 'connected' && this._state !== 'reconnecting') {
578
+ this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms reason=not_connected`);
514
579
  return;
515
580
  }
516
581
  this._saveSeqTrackerState();
@@ -523,45 +588,59 @@ export class AUNClient {
523
588
  await this._transport.close();
524
589
  this._state = 'disconnected';
525
590
  await this._dispatcher.publish('connection.state', { state: this._state });
591
+ this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
526
592
  }
527
593
  /** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
528
594
  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;
595
+ const tStart = Date.now();
596
+ this._clientLog.debug('listIdentities enter');
597
+ try {
598
+ const listFn = this._keystore.listIdentities;
599
+ if (typeof listFn !== 'function') {
600
+ this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
601
+ return [];
602
+ }
603
+ const aids = await listFn.call(this._keystore);
604
+ const summaries = [];
605
+ for (const aid of [...aids].sort()) {
606
+ const identity = await this._keystore.loadIdentity(aid);
607
+ if (!identity || !identity.private_key_pem)
608
+ continue;
609
+ const summary = { aid };
610
+ // 优先从 loadMetadata 获取
611
+ const loadMeta = this._keystore.loadMetadata;
612
+ if (typeof loadMeta === 'function') {
613
+ const md = await loadMeta.call(this._keystore, aid);
614
+ if (md && Object.keys(md).length > 0) {
615
+ summary.metadata = md;
553
616
  }
554
617
  }
555
- if (Object.keys(metadata).length > 0) {
556
- summary.metadata = metadata;
618
+ // 回退:从 identity 中提取非核心字段
619
+ if (!summary.metadata) {
620
+ const metadata = {};
621
+ for (const [key, value] of Object.entries(identity)) {
622
+ if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
623
+ metadata[key] = value;
624
+ }
625
+ }
626
+ if (Object.keys(metadata).length > 0) {
627
+ summary.metadata = metadata;
628
+ }
557
629
  }
630
+ summaries.push(summary);
558
631
  }
559
- summaries.push(summary);
632
+ this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
633
+ return summaries;
634
+ }
635
+ catch (err) {
636
+ this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
637
+ throw err;
560
638
  }
561
- return summaries;
562
639
  }
563
640
  /** 关闭连接 */
564
641
  async close() {
642
+ const tStart = Date.now();
643
+ this._clientLog.debug(`close enter: state=${this._state}`);
565
644
  this._closing = true;
566
645
  this._saveSeqTrackerState();
567
646
  this._stopBackgroundTasks();
@@ -574,6 +653,7 @@ export class AUNClient {
574
653
  if (this._state === 'idle' || this._state === 'closed') {
575
654
  this._state = 'closed';
576
655
  this._resetSeqTrackingState();
656
+ this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms reason=already_idle`);
577
657
  return;
578
658
  }
579
659
  // 关闭前通知服务端主动退出(best-effort,失败不阻塞)
@@ -587,6 +667,7 @@ export class AUNClient {
587
667
  this._state = 'closed';
588
668
  await this._dispatcher.publish('connection.state', { state: this._state });
589
669
  this._resetSeqTrackingState();
670
+ this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
590
671
  }
591
672
  // ── RPC ───────────────────────────────────────────
592
673
  /**
@@ -596,6 +677,19 @@ export class AUNClient {
596
677
  * 自动解密 message.pull/group.pull、Group E2EE 生命周期编排。
597
678
  */
598
679
  async call(method, params) {
680
+ const tStart = Date.now();
681
+ this._clientLog.debug(`call enter: method=${method}`);
682
+ try {
683
+ const result = await this._callImpl(method, params);
684
+ this._clientLog.debug(`call exit: elapsed=${Date.now() - tStart}ms method=${method}`);
685
+ return result;
686
+ }
687
+ catch (err) {
688
+ this._clientLog.debug(`call exit (error): elapsed=${Date.now() - tStart}ms method=${method} err=${err instanceof Error ? err.message : String(err)}`);
689
+ throw err;
690
+ }
691
+ }
692
+ async _callImpl(method, params) {
599
693
  if (this._state !== 'connected') {
600
694
  throw new ConnectionError('client is not connected');
601
695
  }
@@ -605,10 +699,6 @@ export class AUNClient {
605
699
  const p = { ...(params ?? {}) };
606
700
  this._validateOutboundCall(method, p);
607
701
  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
702
  // group.* 方法注入 device_id(服务端用于多设备消息路由)
613
703
  if (method.startsWith('group.') && this._deviceId && p.device_id === undefined) {
614
704
  p.device_id = this._deviceId;
@@ -678,7 +768,7 @@ export class AUNClient {
678
768
  if (serverAck > 0) {
679
769
  const contig = this._seqTracker.getContiguousSeq(ns);
680
770
  if (contig < serverAck) {
681
- console.info('[aun_core] message.pull retention-floor 推进: ns=' + ns + ' contiguous=' + contig + ' -> server_ack_seq=' + serverAck);
771
+ this._clientLog.info('message.pull retention-floor advance: ns=' + ns + ' contiguous=' + contig + ' -> server_ack_seq=' + serverAck);
682
772
  this._seqTracker.forceContiguousSeq(ns, serverAck);
683
773
  }
684
774
  }
@@ -690,7 +780,7 @@ export class AUNClient {
690
780
  seq: contig,
691
781
  device_id: this._deviceId,
692
782
  slot_id: this._slotId,
693
- }).catch((e) => { console.warn('message.pull auto-ack 失败:', e); });
783
+ }).catch((e) => { this._clientLog.warn(`message.pull auto-ack failed:${String(e)}`); });
694
784
  }
695
785
  }
696
786
  }
@@ -719,7 +809,7 @@ export class AUNClient {
719
809
  if (serverAck > 0) {
720
810
  const contig = this._seqTracker.getContiguousSeq(ns);
721
811
  if (contig < serverAck) {
722
- console.info('[aun_core] group.pull retention-floor 推进: ns=' + ns + ' contiguous=' + contig + ' -> cursor.current_seq=' + serverAck);
812
+ this._clientLog.info('group.pull retention-floor advance: ns=' + ns + ' contiguous=' + contig + ' -> cursor.current_seq=' + serverAck);
723
813
  this._seqTracker.forceContiguousSeq(ns, serverAck);
724
814
  }
725
815
  }
@@ -734,7 +824,7 @@ export class AUNClient {
734
824
  msg_seq: contig,
735
825
  device_id: this._deviceId,
736
826
  slot_id: this._slotId,
737
- }).catch((e) => { console.warn('group.pull auto-ack 失败: group=' + gid, e); });
827
+ }).catch((e) => { this._clientLog.warn('group.pull auto-ack failed: group=' + gid, e); });
738
828
  }
739
829
  }
740
830
  }
@@ -781,7 +871,7 @@ export class AUNClient {
781
871
  // P0-12: await rotation 完成(带超时兜底),确保后续 group.send 使用新 epoch
782
872
  const rotationPromise = this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, result), expectedEpoch, allowMember);
783
873
  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));
874
+ await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => this._clientLog.warn(`membership RPC epoch rotation fallback failed:${String(exc)}`));
785
875
  }
786
876
  }
787
877
  return result;
@@ -813,7 +903,9 @@ export class AUNClient {
813
903
  // ── 事件管道:消息解密 ────────────────────────────
814
904
  /** 处理 transport 层推送的原始消息:解密后 re-publish 给用户 */
815
905
  _onRawMessageReceived(data) {
906
+ this._clientLog.debug(`_onRawMessageReceived enter: from=${data?.from ?? '-'} mid=${data?.message_id ?? '-'} seq=${data?.seq ?? '-'}`);
816
907
  this._safeAsync(this._processAndPublishMessage(data));
908
+ this._clientLog.debug(`_onRawMessageReceived exit: elapsed=0ms (dispatched async)`);
817
909
  }
818
910
  /** 实际处理推送消息的异步任务 */
819
911
  async _processAndPublishMessage(data) {
@@ -847,7 +939,7 @@ export class AUNClient {
847
939
  seq: contig,
848
940
  device_id: this._deviceId,
849
941
  slot_id: this._slotId,
850
- }).catch((e) => { console.warn('P2P auto-ack 失败:', e); });
942
+ }).catch((e) => { this._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
851
943
  }
852
944
  // 即时持久化 cursor,异常断连后不回退
853
945
  this._saveSeqTrackerState();
@@ -862,7 +954,7 @@ export class AUNClient {
862
954
  }
863
955
  }
864
956
  catch (exc) {
865
- console.warn('消息解密失败:', exc);
957
+ this._clientLog.warn(`messagedecryptfailed:${String(exc)}`);
866
958
  // H26: 解密失败不再投递原始密文 payload(避免元数据泄漏 + 语义混淆),
867
959
  // 改为发布 message.undecryptable 事件,仅携带安全的 header 信息。
868
960
  if (isJsonObject(data)) {
@@ -881,7 +973,9 @@ export class AUNClient {
881
973
  }
882
974
  /** 处理群组消息推送:自动解密后 re-publish */
883
975
  _onRawGroupMessageCreated(data) {
976
+ this._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
884
977
  this._safeAsync(this._processAndPublishGroupMessage(data));
978
+ this._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)`);
885
979
  }
886
980
  /**
887
981
  * 处理群组推送消息的异步任务。
@@ -924,7 +1018,7 @@ export class AUNClient {
924
1018
  msg_seq: contig,
925
1019
  device_id: this._deviceId,
926
1020
  slot_id: this._slotId,
927
- }).catch((e) => { console.warn('群消息 auto-ack 失败: group=' + groupId, e); });
1021
+ }).catch((e) => { this._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
928
1022
  }
929
1023
  this._saveSeqTrackerState();
930
1024
  }
@@ -952,7 +1046,7 @@ export class AUNClient {
952
1046
  }
953
1047
  }
954
1048
  catch (exc) {
955
- console.warn('群消息解密失败:', exc);
1049
+ this._clientLog.warn(`group message decrypt failed:${String(exc)}`);
956
1050
  // H26: 解密失败改发 group.message_undecryptable 事件,不投递原始密文 payload。
957
1051
  if (isJsonObject(data)) {
958
1052
  const src = data;
@@ -1009,7 +1103,7 @@ export class AUNClient {
1009
1103
  }
1010
1104
  }
1011
1105
  catch (exc) {
1012
- console.warn('自动 pull 群消息失败:', exc);
1106
+ this._clientLog.warn(`auto pull group message failed:${String(exc)}`);
1013
1107
  }
1014
1108
  // pull 失败时仍透传原始通知
1015
1109
  await this._publishAppEvent('group.message_created', notification);
@@ -1059,7 +1153,7 @@ export class AUNClient {
1059
1153
  }
1060
1154
  }
1061
1155
  catch (exc) {
1062
- console.warn('[aun_core] 群消息补洞失败:', exc);
1156
+ this._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
1063
1157
  }
1064
1158
  finally {
1065
1159
  // S1: 成功 / 失败路径都必须清理飞行标记
@@ -1097,7 +1191,7 @@ export class AUNClient {
1097
1191
  if (serverAck > 0) {
1098
1192
  const contigBefore = this._seqTracker.getContiguousSeq(ns);
1099
1193
  if (contigBefore < serverAck) {
1100
- console.info('[aun_core] group.pull_events retention-floor 推进: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
1194
+ this._clientLog.info('group.pull_events retention-floor advance: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
1101
1195
  this._seqTracker.forceContiguousSeq(ns, serverAck);
1102
1196
  }
1103
1197
  }
@@ -1110,7 +1204,7 @@ export class AUNClient {
1110
1204
  event_seq: contig,
1111
1205
  device_id: this._deviceId,
1112
1206
  slot_id: this._slotId,
1113
- }).catch((e) => { console.warn('群事件 auto-ack 失败: group=' + groupId, e); });
1207
+ }).catch((e) => { this._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
1114
1208
  }
1115
1209
  for (const evt of events) {
1116
1210
  if (isJsonObject(evt)) {
@@ -1132,7 +1226,7 @@ export class AUNClient {
1132
1226
  }
1133
1227
  }
1134
1228
  catch (exc) {
1135
- console.warn('[aun_core] 群事件补洞失败:', exc);
1229
+ this._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
1136
1230
  }
1137
1231
  finally {
1138
1232
  // S1: 成功 / 失败路径都必须清理飞行标记
@@ -1185,7 +1279,7 @@ export class AUNClient {
1185
1279
  }
1186
1280
  }
1187
1281
  catch (exc) {
1188
- console.warn('[aun_core] P2P 消息补洞失败:', exc);
1282
+ this._clientLog.warn(`P2P message gap-fill failed:${String(exc)}`);
1189
1283
  }
1190
1284
  finally {
1191
1285
  // S1: 成功 / 失败路径都必须清理飞行标记
@@ -1330,10 +1424,10 @@ export class AUNClient {
1330
1424
  encrypt: true,
1331
1425
  persist_required: true,
1332
1426
  });
1333
- console.info(`[aun_core] 已向 ${targetAid} 请求群 ${groupId} 的密钥`);
1427
+ this._clientLog.info(`to ${targetAid} request group ${groupId} key`);
1334
1428
  }
1335
1429
  catch (exc) {
1336
- console.warn(`[aun_core] 向 ${targetAid} 请求群 ${groupId} 密钥失败:`, exc);
1430
+ this._clientLog.warn(`to ${targetAid} request group ${groupId} key failed: ${String(exc)}`);
1337
1431
  }
1338
1432
  }
1339
1433
  /**
@@ -1460,82 +1554,93 @@ export class AUNClient {
1460
1554
  return member ? String(member.group_id ?? '') : '';
1461
1555
  }
1462
1556
  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));
1557
+ const tStart = Date.now();
1558
+ const action = String(data?.action ?? '');
1559
+ const groupIdInit = String(data?.group_id ?? '');
1560
+ this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupIdInit} action=${action}`);
1561
+ try {
1562
+ if (isJsonObject(data)) {
1563
+ const d = data;
1564
+ // 验签:有 client_signature 就验,没有默认安全
1565
+ const cs = d.client_signature;
1566
+ if (cs && isJsonObject(cs)) {
1567
+ d._verified = await this._verifyEventSignature(d, cs);
1568
+ }
1569
+ await this._dispatcher.publish('group.changed', d);
1570
+ const groupId = (d.group_id ?? '');
1571
+ // event_seq 空洞检测:持久化后的 group.changed 会携带 event_seq。
1572
+ // onMessageSeq 返回值决定是否补拉,与 P2P / group.message 路径对齐。
1573
+ let needPull = false;
1574
+ const rawEventSeq = d.event_seq;
1575
+ if (rawEventSeq != null && groupId) {
1576
+ const es = Number(rawEventSeq);
1577
+ if (Number.isFinite(es) && es > 0) {
1578
+ needPull = this._seqTracker.onMessageSeq(`group_event:${groupId}`, es);
1494
1579
  }
1495
1580
  }
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));
1581
+ // 仅在真实 event gap 时才触发补拉(补洞回来的事件不再触发新补洞)
1582
+ if (needPull && groupId && !d._from_gap_fill) {
1583
+ this._safeAsync(this._fillGroupEventGap(groupId));
1584
+ }
1585
+ if (d.action === 'member_left' || d.action === 'member_removed') {
1586
+ if (groupId) {
1587
+ const expectedEpoch = this._membershipRotationExpectedEpoch(d);
1588
+ if (expectedEpoch === null) {
1589
+ 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
1590
  }
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));
1591
+ else {
1592
+ this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1516
1593
  }
1517
1594
  }
1518
- else {
1519
- if (expectedEpoch === null) {
1595
+ }
1596
+ // 成员加入:按 action 区分策略
1597
+ // - member_added / join_approved(私密群/审批群):admin 必然在线,立即轮换
1598
+ // - joined / invite_code_used(开放群/邀请码群):所有在线成员延迟轮换,新成员自己延迟更长
1599
+ if (['member_added', 'joined', 'join_approved', 'invite_code_used'].includes(String(d.action ?? ''))) {
1600
+ if (groupId) {
1601
+ const action = String(d.action ?? '');
1602
+ const expectedEpoch = this._membershipRotationExpectedEpoch(d);
1603
+ const joinedAids = this._joinedMemberAidsFromPayload(d);
1604
+ const isSelfJoining = joinedAids.includes(this._aid ?? '') && (action === 'joined' || action === 'invite_code_used');
1605
+ if (isSelfJoining || (action === 'joined' || action === 'invite_code_used')) {
1606
+ // open/invite_code 群:所有在线成员都参与延迟轮换
1607
+ // 新成员自己延迟更长,优先让其他在线成员先轮换
1520
1608
  const triggerId = this._membershipRotationTriggerId(groupId, d);
1521
- this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1609
+ if (!isSelfJoining) {
1610
+ this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1611
+ }
1612
+ if (expectedEpoch !== null) {
1613
+ const delay = isSelfJoining ? AUNClient._SELF_JOIN_ROTATION_DELAY_MS : undefined;
1614
+ this._safeAsync(this._delayedRotateAfterJoin(groupId, triggerId, expectedEpoch, true, delay));
1615
+ }
1522
1616
  }
1523
1617
  else {
1524
- this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1618
+ if (expectedEpoch === null) {
1619
+ const triggerId = this._membershipRotationTriggerId(groupId, d);
1620
+ this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
1621
+ }
1622
+ else {
1623
+ this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
1624
+ }
1525
1625
  }
1526
1626
  }
1527
1627
  }
1528
- }
1529
- // 群组解散 清理本地 epoch key、seq_tracker、补洞去重缓存
1530
- if (d.action === 'dissolved') {
1531
- if (groupId) {
1532
- this._cleanupDissolvedGroup(groupId);
1628
+ // 群组解散 → 清理本地 epoch key、seq_tracker、补洞去重缓存
1629
+ if (d.action === 'dissolved') {
1630
+ if (groupId) {
1631
+ this._cleanupDissolvedGroup(groupId);
1632
+ }
1533
1633
  }
1534
1634
  }
1635
+ else {
1636
+ // data 非对象也透传给用户(兼容旧版)
1637
+ await this._dispatcher.publish('group.changed', data);
1638
+ }
1639
+ this._clientLog.debug(`_onRawGroupChanged exit: elapsed=${Date.now() - tStart}ms group_id=${groupIdInit}`);
1535
1640
  }
1536
- else {
1537
- // data 非对象也透传给用户(兼容旧版)
1538
- await this._dispatcher.publish('group.changed', data);
1641
+ catch (err) {
1642
+ this._clientLog.debug(`_onRawGroupChanged exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1643
+ throw err;
1539
1644
  }
1540
1645
  }
1541
1646
  /**
@@ -1543,18 +1648,35 @@ export class AUNClient {
1543
1648
  * 当 prev_state_hash 与本地不连续时回源 group.get_state,并对回源数据做 hash 验证。
1544
1649
  */
1545
1650
  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;
1651
+ const tStart = Date.now();
1652
+ const groupIdInit = String(data?.group_id ?? '');
1653
+ this._clientLog.debug(`_onGroupStateCommitted enter: group_id=${groupIdInit} state_version=${String(data?.state_version ?? '-')}`);
1654
+ try {
1655
+ if (!isJsonObject(data)) {
1656
+ this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=non_object`);
1657
+ return;
1658
+ }
1659
+ const d = data;
1660
+ const groupId = String(d.group_id ?? '').trim();
1661
+ if (!groupId) {
1662
+ this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=no_group_id`);
1663
+ return;
1664
+ }
1665
+ await this._onGroupStateCommittedImpl(d, groupId);
1666
+ this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
1667
+ }
1668
+ catch (err) {
1669
+ this._clientLog.debug(`_onGroupStateCommitted exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1670
+ throw err;
1671
+ }
1672
+ }
1673
+ async _onGroupStateCommittedImpl(d, groupId) {
1552
1674
  // 提交者签名验证
1553
1675
  const cs = d.client_signature;
1554
1676
  if (cs && isJsonObject(cs)) {
1555
1677
  const verified = await this._verifyEventSignature(d, cs);
1556
1678
  if (verified === false) {
1557
- console.warn('[aun_core] state_committed 提交者签名验证失败 group=%s', groupId);
1679
+ this._clientLog.warn(`state_committed committer signature verify failed group=%s${String(groupId)}`);
1558
1680
  return;
1559
1681
  }
1560
1682
  d._verified = verified;
@@ -1571,7 +1693,7 @@ export class AUNClient {
1571
1693
  ? await loadFn.call(this._keystore, groupId)
1572
1694
  : null;
1573
1695
  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);
1696
+ this._clientLog.warn('[aun_core] state_hash 链不连续 group=%s local_sv=%d event_sv=%d', groupId, localState.state_version, stateVersion);
1575
1697
  // 回源同步
1576
1698
  try {
1577
1699
  const serverState = await this._transport.call('group.get_state', { group_id: groupId });
@@ -1591,7 +1713,7 @@ export class AUNClient {
1591
1713
  members: sMembers, policy: sPolicy, prevStateHash: sPrev,
1592
1714
  });
1593
1715
  if (computed !== sHash) {
1594
- console.warn('[aun_core] 回源 state_hash 验证失败 group=%s sv=%d expected=%s got=%s', groupId, sv, sHash, computed);
1716
+ this._clientLog.warn('[aun_core] 回源 state_hash 验证失败 group=%s sv=%d expected=%s got=%s', groupId, sv, sHash, computed);
1595
1717
  return;
1596
1718
  }
1597
1719
  }
@@ -1610,7 +1732,7 @@ export class AUNClient {
1610
1732
  }
1611
1733
  }
1612
1734
  catch (exc) {
1613
- console.warn('[aun_core] state 回源失败 group=%s:', groupId, exc);
1735
+ this._clientLog.warn(`state pull-back failed group=%s:${groupId} ${exc}`);
1614
1736
  }
1615
1737
  return;
1616
1738
  }
@@ -1622,7 +1744,7 @@ export class AUNClient {
1622
1744
  members, policy, prevStateHash,
1623
1745
  });
1624
1746
  if (computed !== stateHash) {
1625
- console.warn('[aun_core] state_hash 重算不匹配 group=%s sv=%d expected=%s got=%s', groupId, stateVersion, stateHash, computed);
1747
+ this._clientLog.warn('[aun_core] state_hash 重算不匹配 group=%s sv=%d expected=%s got=%s', groupId, stateVersion, stateHash, computed);
1626
1748
  return;
1627
1749
  }
1628
1750
  // 3. 更新本地存储
@@ -1649,7 +1771,7 @@ export class AUNClient {
1649
1771
  _cleanupDissolvedGroup(groupId) {
1650
1772
  // 1. 清理 GroupE2EEManager / keystore 中的 epoch 密钥
1651
1773
  this._safeAsync(this._groupE2ee.removeGroup(groupId).catch((exc) => {
1652
- console.warn(`[aun_core] 清理解散群组 ${groupId} epoch 密钥失败:`, exc);
1774
+ this._clientLog.warn(`cleanup dissolved group ${groupId} epoch key failed: ${String(exc)}`);
1653
1775
  }));
1654
1776
  // 2. 清理 seq_tracker 中的群消息和群事件命名空间
1655
1777
  this._seqTracker.removeNamespace(`group:${groupId}`);
@@ -1666,7 +1788,7 @@ export class AUNClient {
1666
1788
  this._pushedSeqs.delete(`group_event:${groupId}`);
1667
1789
  this._pendingOrderedMsgs.delete(`group:${groupId}`);
1668
1790
  this._pendingDecryptMsgs.delete(`group:${groupId}`);
1669
- console.info(`[aun_core] 已清理解散群组 ${groupId} 的本地状态`);
1791
+ this._clientLog.info(`cleanup dissolved group ${groupId} local state`);
1670
1792
  }
1671
1793
  async _verifyEventSignature(_event, cs) {
1672
1794
  const sigAid = String(cs.aid ?? '');
@@ -1683,7 +1805,7 @@ export class AUNClient {
1683
1805
  if (expectedFP) {
1684
1806
  const actualFP = await certificateSha256Fingerprint(cached.certPem);
1685
1807
  if (actualFP !== expectedFP) {
1686
- console.warn('[aun_core] 群事件验签失败:证书指纹不匹配 aid=%s', sigAid);
1808
+ this._clientLog.warn(`group event sig verify failed: cert fingerprint mismatch aid=%s${String(sigAid)}`);
1687
1809
  return false;
1688
1810
  }
1689
1811
  }
@@ -1698,7 +1820,7 @@ export class AUNClient {
1698
1820
  const sigBytes = base64ToUint8(sigB64);
1699
1821
  const ok = await ecdsaVerifyDer(pubKey, sigBytes, signData);
1700
1822
  if (!ok) {
1701
- console.warn('[aun_core] 群事件验签失败 aid=%s method=%s', sigAid, method);
1823
+ this._clientLog.warn(`group event sig verify failed aid=%s method=%s${sigAid} ${method}`);
1702
1824
  // P1-16: 签名失败统一发布事件
1703
1825
  this._dispatcher.publish('signature.verification_failed', {
1704
1826
  aid: sigAid, method, error: 'ECDSA verification failed',
@@ -1707,7 +1829,7 @@ export class AUNClient {
1707
1829
  return ok;
1708
1830
  }
1709
1831
  catch (exc) {
1710
- console.warn('[aun_core] 群事件验签异常:', exc);
1832
+ this._clientLog.warn(`group event sig verify exception:${String(exc)}`);
1711
1833
  // P1-16: 签名失败统一发布事件
1712
1834
  this._dispatcher.publish('signature.verification_failed', {
1713
1835
  aid: sigAid, method, error: String(exc),
@@ -1729,74 +1851,86 @@ export class AUNClient {
1729
1851
  }
1730
1852
  /** 自动加密并发送 P2P 消息 */
1731
1853
  async _sendEncrypted(params) {
1854
+ const tStart = Date.now();
1732
1855
  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({
1856
+ this._clientLog.debug(`_sendEncrypted enter: to=${toAid} mid=${String(params.message_id ?? '<auto>')}`);
1857
+ try {
1858
+ this._validateMessageRecipient(toAid);
1859
+ const payload = isJsonObject(params.payload) ? params.payload : null;
1860
+ const messageId = String(params.message_id ?? '') || _uuidV4();
1861
+ const timestamp = params.timestamp ?? Date.now();
1862
+ if (payload === null) {
1863
+ throw new ValidationError('message.send payload must be an object when encrypt=true');
1864
+ }
1865
+ const persistRequired = Boolean(params.persist_required || params.durable);
1866
+ const protectedHeaders = this._protectedHeadersFromParams(params);
1867
+ // Lazy P2P sync:首次发送前自动拉取历史,避免重连后 seq 空洞
1868
+ if (!this._p2pSynced) {
1869
+ await this._lazySyncP2p();
1870
+ }
1871
+ // 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
1872
+ const sendAttempt = async (refreshPeerMaterial = false) => {
1873
+ const recipientPrekeys = refreshPeerMaterial
1874
+ ? await this._refreshPeerPrekeys(toAid)
1875
+ : await this._fetchPeerPrekeys(toAid);
1876
+ const selfSyncCopies = await this._buildSelfSyncCopies({
1877
+ logicalToAid: toAid, payload, messageId, timestamp, protectedHeaders,
1878
+ });
1879
+ // 多设备过滤:只保留有有效 device_id 的可路由 prekey,
1880
+ // 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
1881
+ const routablePrekeys = recipientPrekeys.filter(pk => {
1882
+ const did = String(pk.device_id ?? '').trim();
1883
+ return did && did !== PREKEY_FALLBACK_DEVICE_ID;
1884
+ });
1885
+ const canUseMultiDevice = routablePrekeys.length > 0
1886
+ && (routablePrekeys.length > 1 || selfSyncCopies.length > 0);
1887
+ if (!canUseMultiDevice) {
1888
+ return await this._sendEncryptedSingle({
1889
+ toAid, payload, messageId, timestamp,
1890
+ prekey: routablePrekeys[0] ?? recipientPrekeys[0],
1891
+ persistRequired, protectedHeaders,
1892
+ });
1893
+ }
1894
+ const recipientCopies = await this._buildRecipientDeviceCopies({
1764
1895
  toAid, payload, messageId, timestamp,
1765
- prekey: routablePrekeys[0] ?? recipientPrekeys[0],
1766
- persistRequired, protectedHeaders,
1896
+ prekeys: routablePrekeys, protectedHeaders,
1767
1897
  });
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: {
1898
+ const sendParams = {
1899
+ to: toAid,
1900
+ payload: {
1901
+ type: 'e2ee.multi_device',
1902
+ logical_message_id: messageId,
1903
+ recipient_copies: recipientCopies,
1904
+ self_copies: selfSyncCopies,
1905
+ },
1776
1906
  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,
1907
+ encrypted: true,
1908
+ message_id: messageId,
1909
+ timestamp,
1910
+ };
1911
+ if (persistRequired)
1912
+ sendParams.persist_required = true;
1913
+ return this._transport.call('message.send', sendParams);
1785
1914
  };
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);
1915
+ // 首次尝试(使用缓存);若对端证书/prekey 过期导致指纹不匹配,清缓存后重试一次
1916
+ try {
1917
+ const result = await sendAttempt(false);
1918
+ this._clientLog.debug(`_sendEncrypted exit: elapsed=${Date.now() - tStart}ms to=${toAid} retry=false`);
1919
+ return result;
1920
+ }
1921
+ catch (exc) {
1922
+ if (!isRetryablePeerMaterialError(exc))
1923
+ throw exc;
1924
+ this._clientLog.warn(`peer cert/prekey mismatch for ${toAid}, refreshing and retrying once`);
1925
+ }
1926
+ const result = await sendAttempt(true);
1927
+ this._clientLog.debug(`_sendEncrypted exit: elapsed=${Date.now() - tStart}ms to=${toAid} retry=true`);
1928
+ return result;
1793
1929
  }
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`);
1930
+ catch (err) {
1931
+ this._clientLog.debug(`_sendEncrypted exit (error): elapsed=${Date.now() - tStart}ms to=${toAid} err=${err instanceof Error ? err.message : String(err)}`);
1932
+ throw err;
1798
1933
  }
1799
- return await sendAttempt(true);
1800
1934
  }
1801
1935
  /**
1802
1936
  * 首次发送 P2P 消息前懒拉取历史消息,同步 seqTracker 避免空洞。
@@ -1822,7 +1956,7 @@ export class AUNClient {
1822
1956
  }
1823
1957
  }
1824
1958
  catch (exc) {
1825
- console.warn('[aun_core] lazySyncP2p 失败:', exc);
1959
+ this._clientLog.warn(`lazySyncP2p failed:${String(exc)}`);
1826
1960
  }
1827
1961
  }
1828
1962
  async _sendEncryptedSingle(opts) {
@@ -1933,7 +2067,7 @@ export class AUNClient {
1933
2067
  }
1934
2068
  catch (e) {
1935
2069
  // 旧设备的 prekey 可能携带已轮换的证书指纹,跳过该设备的自同步副本
1936
- console.warn(`self-sync 跳过设备 ${deviceId}: 证书解析失败 (${e}),可能是旧 prekey`);
2070
+ this._clientLog.warn(`self-sync skip device ${deviceId}: cert parse failed (${e}), may be old prekey`);
1937
2071
  continue;
1938
2072
  }
1939
2073
  const [envelope, encryptResult] = await this._encryptCopyPayload({
@@ -1980,7 +2114,7 @@ export class AUNClient {
1980
2114
  });
1981
2115
  }
1982
2116
  catch (exc) {
1983
- console.warn('发布 e2ee.degraded 事件失败:', exc);
2117
+ this._clientLog.warn(`publish e2ee.degraded eventfailed:${String(exc)}`);
1984
2118
  }
1985
2119
  }
1986
2120
  }
@@ -2067,10 +2201,21 @@ export class AUNClient {
2067
2201
  }
2068
2202
  /** 自动加密并发送群组消息 */
2069
2203
  async _sendGroupEncrypted(params) {
2070
- return this._callGroupEncryptedRpc('group.send', params, {
2071
- idField: 'message_id',
2072
- idPrefix: 'gm',
2073
- });
2204
+ const tStart = Date.now();
2205
+ const groupId = String(params.group_id ?? '');
2206
+ this._clientLog.debug(`_sendGroupEncrypted enter: group_id=${groupId}`);
2207
+ try {
2208
+ const result = await this._callGroupEncryptedRpc('group.send', params, {
2209
+ idField: 'message_id',
2210
+ idPrefix: 'gm',
2211
+ });
2212
+ this._clientLog.debug(`_sendGroupEncrypted exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
2213
+ return result;
2214
+ }
2215
+ catch (err) {
2216
+ this._clientLog.debug(`_sendGroupEncrypted exit (error): elapsed=${Date.now() - tStart}ms group_id=${groupId} err=${err instanceof Error ? err.message : String(err)}`);
2217
+ throw err;
2218
+ }
2074
2219
  }
2075
2220
  async _putGroupThoughtEncrypted(params) {
2076
2221
  return this._callGroupEncryptedRpc('group.thought.put', params, {
@@ -2126,7 +2271,7 @@ export class AUNClient {
2126
2271
  }
2127
2272
  catch (exc) {
2128
2273
  if (attempt === 0 && this._isRecoverableGroupEpochError(exc)) {
2129
- console.warn(`[aun_core] 群 ${prepared.groupId} 调用 ${method} epoch 已过旧,恢复密钥后重加密重试一次: ${formatCaughtError(exc)}`);
2274
+ this._clientLog.warn(`group ${prepared.groupId} call ${method} epoch too old, retry encrypt after key recovery : ${formatCaughtError(exc)}`);
2130
2275
  prepared = await this._prepareGroupEncryptedRpcParams(method, params, options, true);
2131
2276
  continue;
2132
2277
  }
@@ -2217,7 +2362,7 @@ export class AUNClient {
2217
2362
  }
2218
2363
  }
2219
2364
  catch (exc) {
2220
- console.warn(`[aun_core] lazySyncGroup(${groupId}) 失败:`, exc);
2365
+ this._clientLog.warn(`lazySyncGroup(${groupId}) failed: ${String(exc)}`);
2221
2366
  }
2222
2367
  }
2223
2368
  _isGroupEpochTooOldError(exc) {
@@ -2275,7 +2420,7 @@ export class AUNClient {
2275
2420
  const secretData = await this._groupE2ee.loadSecret(groupId, 1);
2276
2421
  if (!secretData || secretData.pending_rotation_id)
2277
2422
  return epochResult;
2278
- console.warn(`[aun_core] 群 ${groupId} 检测到本地 epoch 1 已存在但服务端 epoch 仍为 0,尝试补同步初始 epoch`);
2423
+ this._clientLog.warn(`group ${groupId} local epoch=1 but server epoch=0, try sync initial epoch`);
2279
2424
  await this._syncEpochToServer(groupId);
2280
2425
  try {
2281
2426
  const refreshed = await this.call('group.e2ee.get_epoch', { group_id: groupId });
@@ -2283,7 +2428,7 @@ export class AUNClient {
2283
2428
  return refreshed;
2284
2429
  }
2285
2430
  catch (exc) {
2286
- console.warn(`[aun_core] 群 ${groupId} 初始 epoch 补同步后刷新服务端 epoch 失败: ${formatCaughtError(exc)}`);
2431
+ this._clientLog.warn(`group ${groupId} initial epoch sync then refresh server epoch failed: ${formatCaughtError(exc)}`);
2287
2432
  }
2288
2433
  return epochResult;
2289
2434
  }
@@ -2300,7 +2445,7 @@ export class AUNClient {
2300
2445
  catch (exc) {
2301
2446
  if (strict)
2302
2447
  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)}`);
2448
+ this._clientLog.warn(`group ${groupId} epoch precheck failed: ${formatCaughtError(exc)}`);
2304
2449
  return;
2305
2450
  }
2306
2451
  let serverEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
@@ -2348,7 +2493,7 @@ export class AUNClient {
2348
2493
  throw new StateError(`group ${groupId} epoch rotation has not completed`);
2349
2494
  }
2350
2495
  }
2351
- console.warn(`[aun_core] group ${groupId} local epoch=${effectiveLocalEpoch} < server epoch=${serverEpoch}; requesting key recovery`);
2496
+ this._clientLog.warn(`group ${groupId} local epoch=${effectiveLocalEpoch} < server epoch=${serverEpoch}; requesting key recovery`);
2352
2497
  await this._recoverGroupEpochKey(groupId, serverEpoch, '', 5000);
2353
2498
  const deadline = Date.now() + 5000;
2354
2499
  while (Date.now() < deadline) {
@@ -2372,7 +2517,7 @@ export class AUNClient {
2372
2517
  members = isJsonObject(membersResult) ? membersResult.members : null;
2373
2518
  }
2374
2519
  catch (exc) {
2375
- console.warn(`[aun_core] 群 ${groupId} 成员 epoch floor 预检跳过: ${formatCaughtError(exc)}`);
2520
+ this._clientLog.warn(`group ${groupId} member epoch floor pre-check skip: ${formatCaughtError(exc)}`);
2376
2521
  return;
2377
2522
  }
2378
2523
  let maxMinReadEpoch = 0;
@@ -2387,7 +2532,7 @@ export class AUNClient {
2387
2532
  }
2388
2533
  if (maxMinReadEpoch <= committedEpoch)
2389
2534
  return;
2390
- console.warn(`[aun_core] 群 ${groupId} 成员 min_read_epoch 高于 committed epoch,按 committed epoch 继续发送: committed=${committedEpoch} floor=${maxMinReadEpoch}`);
2535
+ this._clientLog.warn(`group ${groupId} min_read_epoch above committed epoch, send with committed epoch: committed=${committedEpoch} floor=${maxMinReadEpoch}`);
2391
2536
  return;
2392
2537
  }
2393
2538
  }
@@ -2398,7 +2543,7 @@ export class AUNClient {
2398
2543
  return epochResult;
2399
2544
  }
2400
2545
  catch (exc) {
2401
- console.warn(`[aun_core] 群 ${groupId} 查询 committed epoch 状态失败,回退本地 epoch: ${formatCaughtError(exc)}`);
2546
+ this._clientLog.warn(`group ${groupId} query committed epoch state failed, rollback local epoch: ${formatCaughtError(exc)}`);
2402
2547
  }
2403
2548
  const localEpoch = await this._groupE2ee.currentEpoch(groupId);
2404
2549
  return { epoch: localEpoch ?? 0, committed_epoch: localEpoch ?? 0 };
@@ -2426,7 +2571,7 @@ export class AUNClient {
2426
2571
  let committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
2427
2572
  if (await this._committedRotationMembershipGap(groupId, committedEpoch, committedRotation)) {
2428
2573
  const allowMember = await this._groupAllowsMemberEpochRotation(groupId);
2429
- console.warn(`[aun_core] 群 ${groupId} committed epoch ${committedEpoch} 的成员快照与当前成员不一致,触发成员变更轮换修复`);
2574
+ this._clientLog.warn(`group ${groupId} committed epoch ${committedEpoch} member snapshot mismatches current members, trigger rotation fix`);
2430
2575
  await this._maybeLeadRotateGroupEpoch(groupId, `${groupId}:committed_membership_gap:aid:${this._aid}:epoch:${committedEpoch}`, committedEpoch, allowMember);
2431
2576
  const refreshed = await this._committedGroupEpochState(groupId);
2432
2577
  const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
@@ -2443,7 +2588,7 @@ export class AUNClient {
2443
2588
  return committedEpoch;
2444
2589
  }
2445
2590
  const pendingRotationId = secretData ? String(secretData.pending_rotation_id ?? '') : '';
2446
- console.warn(`[aun_core] 群 ${groupId} epoch ${committedEpoch} 本地 pending key 未匹配服务端 committed rotation,先恢复密钥: local_rotation=${pendingRotationId || '-'}`);
2591
+ this._clientLog.warn(`group ${groupId} epoch ${committedEpoch} local pending key mismatches server committed rotation, recover key first: local_rotation=${pendingRotationId || '-'}`);
2447
2592
  await this._recoverGroupEpochKey(groupId, committedEpoch, '', 5000);
2448
2593
  let refreshed = await this._committedGroupEpochState(groupId);
2449
2594
  const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
@@ -2488,13 +2633,13 @@ export class AUNClient {
2488
2633
  if (activeMembers.join('\n') !== expectedMembers.join('\n')) {
2489
2634
  const missing = activeMembers.filter((aid) => !expectedMembers.includes(aid));
2490
2635
  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)}`);
2636
+ this._clientLog.info(`group ${groupId} committed membership gap: epoch=${committedEpoch} missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
2492
2637
  return true;
2493
2638
  }
2494
2639
  return false;
2495
2640
  }
2496
2641
  catch (exc) {
2497
- console.debug(`[aun_core] 查询当前成员失败,无法判断 committed membership gap: group=${groupId} err=${formatCaughtError(exc)}`);
2642
+ this._clientLog.debug(`query current members failed, cannot determine committed membership gap: group=${groupId} err=${formatCaughtError(exc)}`);
2498
2643
  return false;
2499
2644
  }
2500
2645
  }
@@ -2514,7 +2659,7 @@ export class AUNClient {
2514
2659
  if (fromAid) {
2515
2660
  const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
2516
2661
  if (!certReady) {
2517
- console.warn(`无法获取发送方 ${fromAid} 的证书,跳过解密`);
2662
+ this._clientLog.warn(`cannot fetch sender ${fromAid} cert, skip decrypt`);
2518
2663
  throw new Error(`发送方证书不可用: from=${fromAid}, mid=${message.message_id}`);
2519
2664
  }
2520
2665
  }
@@ -2528,47 +2673,56 @@ export class AUNClient {
2528
2673
  }
2529
2674
  /** 批量解密 P2P 消息(用于 message.pull) */
2530
2675
  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;
2676
+ const tStart = Date.now();
2677
+ this._clientLog.debug(`_decryptMessages enter: count=${messages.length}`);
2678
+ try {
2679
+ const seenInBatch = new Set();
2680
+ const result = [];
2681
+ for (const msg of messages) {
2682
+ const mid = (msg.message_id ?? '');
2683
+ if (mid && seenInBatch.has(mid))
2684
+ continue;
2685
+ if (mid)
2686
+ seenInBatch.add(mid);
2687
+ const payload = isJsonObject(msg.payload) ? msg.payload : null;
2688
+ if (payload !== null && await this._tryHandleGroupKeyMessage(msg)) {
2689
+ continue;
2690
+ }
2691
+ if (payload !== null
2692
+ && payload.type === 'e2ee.encrypted'
2693
+ && (msg.encrypted === true || !('encrypted' in msg))) {
2694
+ try {
2695
+ const fromAid = (msg.from ?? '');
2696
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
2697
+ if (fromAid) {
2698
+ const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
2699
+ if (!certReady) {
2700
+ this._clientLog.warn(`cannot fetch sender %s cert, skip decrypt${String(fromAid)}`);
2701
+ continue;
2702
+ }
2703
+ }
2704
+ // Pull 场景:跳过防重放和 timestamp 窗口检查(push 已处理过的消息仍需要能解密)
2705
+ const decrypted = await this._e2ee.decryptMessage(msg, { skipReplay: true });
2706
+ if (decrypted !== null) {
2707
+ result.push(decrypted);
2554
2708
  }
2555
2709
  }
2556
- // Pull 场景:跳过防重放和 timestamp 窗口检查(push 已处理过的消息仍需要能解密)
2557
- const decrypted = await this._e2ee.decryptMessage(msg, { skipReplay: true });
2558
- if (decrypted !== null) {
2559
- result.push(decrypted);
2710
+ catch (decryptExc) {
2711
+ this._clientLog.warn(`pull messagedecryptfailed, skip: from=${String(msg.from ?? '')} mid=${mid} err=${decryptExc instanceof Error ? decryptExc.message : String(decryptExc)}`);
2712
+ continue;
2560
2713
  }
2561
2714
  }
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;
2715
+ else {
2716
+ result.push(msg);
2565
2717
  }
2566
2718
  }
2567
- else {
2568
- result.push(msg);
2569
- }
2719
+ this._clientLog.debug(`_decryptMessages exit: elapsed=${Date.now() - tStart}ms in=${messages.length} out=${result.length}`);
2720
+ return result;
2721
+ }
2722
+ catch (err) {
2723
+ this._clientLog.debug(`_decryptMessages exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
2724
+ throw err;
2570
2725
  }
2571
- return result;
2572
2726
  }
2573
2727
  /** 解密单条群组消息。opts.skipReplay 用于 pull 场景跳过防重放。 */
2574
2728
  _enqueuePendingDecrypt(groupId, msg) {
@@ -2617,20 +2771,34 @@ export class AUNClient {
2617
2771
  this._safeAsync(this._retryPendingDecryptMsgs(groupId));
2618
2772
  }
2619
2773
  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;
2774
+ const tStart = Date.now();
2775
+ this._clientLog.debug(`_recoverGroupEpochKey enter: group_id=${groupId} epoch=${epoch} sender=${senderAid} timeout=${timeoutMs}`);
2776
+ try {
2777
+ const existing = await this._groupE2ee.loadSecret(groupId, epoch);
2778
+ if (await this._groupEpochSecretReadyForRecovery(groupId, epoch, existing)) {
2779
+ this._scheduleRetryPendingDecryptMsgs(groupId);
2780
+ this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=already_ready`);
2781
+ return true;
2782
+ }
2783
+ // inflight 去重:同 groupId:epoch 的并发恢复共享同一个 Promise
2784
+ const key = `${groupId}:${epoch}`;
2785
+ const inflight = this._groupEpochRecoveryInflight.get(key);
2786
+ if (inflight) {
2787
+ const r = await inflight;
2788
+ this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=${r ? 'ok' : 'failed'} source=inflight`);
2789
+ return r;
2790
+ }
2791
+ const promise = this._doRecoverGroupEpochKey(groupId, epoch, senderAid, timeoutMs)
2792
+ .finally(() => this._groupEpochRecoveryInflight.delete(key));
2793
+ this._groupEpochRecoveryInflight.set(key, promise);
2794
+ const r = await promise;
2795
+ this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=${r ? 'ok' : 'failed'}`);
2796
+ return r;
2797
+ }
2798
+ catch (err) {
2799
+ this._clientLog.debug(`_recoverGroupEpochKey exit (error): elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} err=${err instanceof Error ? err.message : String(err)}`);
2800
+ throw err;
2624
2801
  }
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
2802
  }
2635
2803
  static _extractGroupJoinMode(payload) {
2636
2804
  if (!isJsonObject(payload))
@@ -2976,46 +3144,66 @@ export class AUNClient {
2976
3144
  }
2977
3145
  }
2978
3146
  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);
3147
+ const tStart = Date.now();
3148
+ const groupIdInit = String(message.group_id ?? '');
3149
+ const midInit = String(message.message_id ?? '');
3150
+ this._clientLog.debug(`_decryptGroupMessage enter: group_id=${groupIdInit} mid=${midInit} skip_replay=${!!opts?.skipReplay}`);
3151
+ try {
3152
+ const payload = isJsonObject(message.payload) ? message.payload : null;
3153
+ if (payload === null || payload.type !== 'e2ee.group_encrypted') {
3154
+ const r = this._attachGroupDispatchModeToPayload(message);
3155
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_encrypted`);
3156
+ return r;
3157
+ }
3158
+ // 确保发送方证书已缓存(签名验证需要)
3159
+ const senderAid = String(message.from ?? message.sender_aid ?? '');
3160
+ if (senderAid) {
3161
+ const certOk = await this._ensureSenderCertCached(senderAid);
3162
+ if (!certOk) {
3163
+ this._clientLog.warn(`group message decrypt skip: sender ${senderAid} cert unavailable`);
3164
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=skip_no_sender_cert`);
3165
+ return message;
3012
3166
  }
3013
3167
  }
3014
- catch (exc) {
3015
- console.debug(`[aun_core] ${groupId} epoch ${epoch} 同步恢复失败: ${formatCaughtError(exc)}`);
3168
+ // 先尝试直接解密
3169
+ const result = await this._groupE2ee.decrypt(message, opts);
3170
+ if (result !== null && isJsonObject(result.e2ee)) {
3171
+ const r = this._attachGroupDispatchModeToPayload(result);
3172
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=ok group_id=${groupIdInit}`);
3173
+ return r;
3174
+ }
3175
+ // replay guard 命中:decrypt 返回了原消息(非 null)但无 e2ee 字段
3176
+ // 不是解密失败,不应触发 recover
3177
+ if (result !== null) {
3178
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=replay_skipped`);
3179
+ return result;
3180
+ }
3181
+ // 真正的解密失败(result === null),尝试密钥恢复后重试
3182
+ const groupId = String(message.group_id ?? '');
3183
+ const sender = String(message.from ?? message.sender_aid ?? '');
3184
+ const epoch = Number(payload.epoch ?? 0);
3185
+ if (epoch > 0 && groupId) {
3186
+ try {
3187
+ if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
3188
+ const retry = await this._groupE2ee.decrypt(message, opts);
3189
+ if (retry !== null && retry.e2ee) {
3190
+ const r = this._attachGroupDispatchModeToPayload(retry);
3191
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=ok_after_recover`);
3192
+ return r;
3193
+ }
3194
+ }
3195
+ }
3196
+ catch (exc) {
3197
+ this._clientLog.debug(`group ${groupId} epoch ${epoch} sync recover failed: ${formatCaughtError(exc)}`);
3198
+ }
3016
3199
  }
3200
+ this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=undecryptable group_id=${groupIdInit}`);
3201
+ return message;
3202
+ }
3203
+ catch (err) {
3204
+ this._clientLog.debug(`_decryptGroupMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
3205
+ throw err;
3017
3206
  }
3018
- return message;
3019
3207
  }
3020
3208
  _attachGroupDispatchModeToPayload(message) {
3021
3209
  const payload = message.payload;
@@ -3132,14 +3320,14 @@ export class AUNClient {
3132
3320
  if (fromAid) {
3133
3321
  const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
3134
3322
  if (!certReady) {
3135
- console.warn('[aun_core] p2p.thought.decrypt failed: 无法获取发送方证书 thought_id=' + thoughtId + ' from=' + fromAid);
3323
+ this._clientLog.warn('p2p.thought.decrypt failed: cannot fetch sendercert thought_id=' + thoughtId + ' from=' + fromAid);
3136
3324
  decryptFailed = true;
3137
3325
  }
3138
3326
  }
3139
3327
  if (!decryptFailed) {
3140
3328
  decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
3141
3329
  if (decrypted === null || (isJsonObject(decrypted.payload) && decrypted.payload.type === 'e2ee.encrypted')) {
3142
- console.warn('[aun_core] p2p.thought.decrypt failed thought_id=' + thoughtId);
3330
+ this._clientLog.warn('p2p.thought.decrypt failed thought_id=' + thoughtId);
3143
3331
  decryptFailed = true;
3144
3332
  decrypted = message;
3145
3333
  }
@@ -3188,7 +3376,7 @@ export class AUNClient {
3188
3376
  decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
3189
3377
  }
3190
3378
  catch (exc) {
3191
- console.warn('[aun_core] e2ee.encrypted 外壳解密抛异常,交给普通链路处理:', exc);
3379
+ this._clientLog.warn(`e2ee.encrypted outer decrypt exception, fallback to normal path:${String(exc)}`);
3192
3380
  return false;
3193
3381
  }
3194
3382
  if (!decrypted) {
@@ -3227,7 +3415,7 @@ export class AUNClient {
3227
3415
  }
3228
3416
  }
3229
3417
  catch (exc) {
3230
- console.warn('[aun_core] 群组密钥消息处理异常:', exc);
3418
+ this._clientLog.warn(`group key message handle exception:${String(exc)}`);
3231
3419
  // S14: 控制面消息处理异常也要抑制业务分发
3232
3420
  if (isGroupKeyCtrl)
3233
3421
  return true;
@@ -3255,7 +3443,7 @@ export class AUNClient {
3255
3443
  members = memberList.map(m => String(m.aid ?? ''));
3256
3444
  }
3257
3445
  catch (exc) {
3258
- console.warn(`群组 ${groupId} 成员列表回源失败:`, exc);
3446
+ this._clientLog.warn(`group ${groupId} member list pull-back failed: ${String(exc)}`);
3259
3447
  }
3260
3448
  }
3261
3449
  const response = await this._groupE2ee.handleKeyRequestMsg(actualPayload, members);
@@ -3269,7 +3457,7 @@ export class AUNClient {
3269
3457
  });
3270
3458
  }
3271
3459
  catch (exc) {
3272
- console.warn(`向 ${requester} 回复群组密钥失败:`, exc);
3460
+ this._clientLog.warn(`reply group key to ${requester} failed: ${String(exc)}`);
3273
3461
  }
3274
3462
  }
3275
3463
  }
@@ -3291,144 +3479,170 @@ export class AUNClient {
3291
3479
  * 跨域时自动将请求路由到 peer 所在域的 Gateway。
3292
3480
  */
3293
3481
  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;
3482
+ const tStart = Date.now();
3483
+ this._clientLog.debug(`_fetchPeerCert enter: aid=${aid} fingerprint=${certFingerprint ?? '<none>'}`);
3307
3484
  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);
3485
+ const cacheKey = certCacheKey(aid, certFingerprint);
3486
+ const cached = this._certCache.get(cacheKey);
3487
+ const now = Date.now() / 1000;
3488
+ if (cached && now < cached.refreshAfter) {
3489
+ this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} source=cache`);
3490
+ return cached.certPem;
3491
+ }
3492
+ const gatewayUrl = this._gatewayUrl;
3493
+ if (!gatewayUrl) {
3494
+ throw new ValidationError('gateway url unavailable for e2ee cert fetch');
3495
+ }
3496
+ // 跨域时用 peer 所在域的 Gateway URL
3497
+ const peerGatewayUrl = resolvePeerGatewayUrl(gatewayUrl, aid);
3498
+ let certPem;
3312
3499
  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();
3500
+ const certUrl = buildCertUrl(peerGatewayUrl, aid, certFingerprint);
3501
+ // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3502
+ const controller = new AbortController();
3503
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
3504
+ try {
3505
+ const resp = await fetch(certUrl, { signal: controller.signal });
3506
+ if (!resp.ok)
3507
+ throw new ValidationError(`failed to fetch peer cert for ${aid}: HTTP ${resp.status}`);
3508
+ certPem = await resp.text();
3509
+ }
3510
+ finally {
3511
+ clearTimeout(timeoutId);
3512
+ }
3317
3513
  }
3318
- finally {
3319
- clearTimeout(timeoutId);
3514
+ catch (exc) {
3515
+ if (!certFingerprint) {
3516
+ throw exc;
3517
+ }
3518
+ // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3519
+ const fallbackController = new AbortController();
3520
+ const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), 5000);
3521
+ try {
3522
+ const fallbackResp = await fetch(buildCertUrl(peerGatewayUrl, aid), { signal: fallbackController.signal });
3523
+ if (!fallbackResp.ok) {
3524
+ throw exc;
3525
+ }
3526
+ certPem = await fallbackResp.text();
3527
+ }
3528
+ finally {
3529
+ clearTimeout(fallbackTimeoutId);
3530
+ }
3320
3531
  }
3321
- }
3322
- catch (exc) {
3323
- if (!certFingerprint) {
3324
- throw exc;
3532
+ // H7: 严格校验指纹(DER SHA-256 或 SPKI SHA-256 任一匹配即可)
3533
+ if (certFingerprint) {
3534
+ const expectedFP = String(certFingerprint).trim().toLowerCase();
3535
+ if (!expectedFP.startsWith('sha256:')) {
3536
+ throw new ValidationError(`unsupported cert_fingerprint format for ${aid}: ${expectedFP.slice(0, 24)}`);
3537
+ }
3538
+ const derFP = await this._certFingerprint(certPem);
3539
+ if (derFP !== expectedFP) {
3540
+ const spkiFP = await this._spkiFingerprint(certPem);
3541
+ if (!spkiFP || spkiFP !== expectedFP) {
3542
+ throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
3543
+ }
3544
+ }
3325
3545
  }
3326
- // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
3327
- const fallbackController = new AbortController();
3328
- const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), 5000);
3546
+ // 完整 PKI 验证:链 + CRL + OCSP + AID 绑定
3329
3547
  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();
3548
+ await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
3335
3549
  }
3336
- finally {
3337
- clearTimeout(fallbackTimeoutId);
3550
+ catch (exc) {
3551
+ throw new ValidationError(`peer cert verification failed for ${aid}: ${exc}`);
3338
3552
  }
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)}`);
3553
+ this._certCache.set(cacheKey, {
3554
+ certPem,
3555
+ validatedAt: now,
3556
+ refreshAfter: now + PEER_CERT_CACHE_TTL,
3557
+ });
3558
+ try {
3559
+ // peer 证书只存版本目录,不覆盖 cert.pem
3560
+ await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3345
3561
  }
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
- }
3562
+ catch (exc) {
3563
+ this._clientLog.error(`write cert to keystore failed (aid=${aid}): ${String(exc)}`, exc instanceof Error ? exc : undefined);
3352
3564
  }
3565
+ this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} source=fetched`);
3566
+ return certPem;
3353
3567
  }
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}`);
3360
- }
3361
- this._certCache.set(cacheKey, {
3362
- certPem,
3363
- validatedAt: now,
3364
- refreshAfter: now + PEER_CERT_CACHE_TTL,
3365
- });
3366
- 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);
3568
+ catch (err) {
3569
+ this._clientLog.debug(`_fetchPeerCert exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
3570
+ throw err;
3372
3571
  }
3373
- return certPem;
3374
3572
  }
3375
3573
  /** 获取对方所有设备的 prekey(带缓存)。 */
3376
3574
  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;
3575
+ const tStart = Date.now();
3576
+ this._clientLog.debug(`_fetchPeerPrekeys enter: peer_aid=${peerAid}`);
3390
3577
  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;
3578
+ const cachedList = this._peerPrekeysCache.get(peerAid);
3579
+ if (cachedList && Date.now() / 1000 < cachedList.expireAt) {
3580
+ const normalized = normalizePeerPrekeys(cachedList.items);
3581
+ if (normalized.length > 0) {
3582
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=list_cache`);
3583
+ return normalized.map((item) => ({ ...item }));
3584
+ }
3412
3585
  }
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 }));
3586
+ const cached = this._e2ee.getCachedPrekey(peerAid);
3587
+ if (cached !== null) {
3588
+ const normalized = normalizePeerPrekeys([cached]);
3589
+ if (normalized.length > 0) {
3590
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=e2ee_cache`);
3591
+ return normalized.map((item) => ({ ...item }));
3592
+ }
3593
+ }
3594
+ let result;
3595
+ try {
3596
+ result = await this._transport.call('message.e2ee.get_prekey', { aid: peerAid });
3597
+ }
3598
+ catch (exc) {
3599
+ throw new ValidationError(`failed to fetch peer prekey for ${peerAid}: ${String(exc)}`);
3600
+ }
3601
+ if (!isJsonObject(result)) {
3602
+ throw new ValidationError(`invalid prekey response for ${peerAid}`);
3603
+ }
3604
+ if (result.found === false) {
3605
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=0 reason=not_found`);
3606
+ return [];
3607
+ }
3608
+ const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
3609
+ if (devicePrekeys) {
3610
+ const normalized = normalizePeerPrekeys(devicePrekeys);
3611
+ if (normalized.length > 0) {
3612
+ this._peerPrekeysCache.set(peerAid, {
3613
+ items: normalized.map((item) => ({ ...item })),
3614
+ expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
3615
+ });
3616
+ this._e2ee.cachePrekey(peerAid, normalized[0]);
3617
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=device_prekeys`);
3618
+ return normalized;
3619
+ }
3620
+ }
3621
+ if (!isPeerPrekeyResponse(result)) {
3622
+ throw new ValidationError(`invalid prekey response for ${peerAid}`);
3426
3623
  }
3624
+ if (result.prekey) {
3625
+ const normalized = normalizePeerPrekeys([result.prekey]);
3626
+ if (normalized.length > 0) {
3627
+ this._peerPrekeysCache.set(peerAid, {
3628
+ items: normalized.map((item) => ({ ...item })),
3629
+ expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
3630
+ });
3631
+ this._e2ee.cachePrekey(peerAid, normalized[0]);
3632
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=single_prekey`);
3633
+ return normalized.map((item) => ({ ...item }));
3634
+ }
3635
+ }
3636
+ if (result.found) {
3637
+ throw new ValidationError(`invalid prekey response for ${peerAid}`);
3638
+ }
3639
+ this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=0`);
3640
+ return [];
3427
3641
  }
3428
- if (result.found) {
3429
- throw new ValidationError(`invalid prekey response for ${peerAid}`);
3642
+ catch (err) {
3643
+ this._clientLog.debug(`_fetchPeerPrekeys exit (error): elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} err=${err instanceof Error ? err.message : String(err)}`);
3644
+ throw err;
3430
3645
  }
3431
- return [];
3432
3646
  }
3433
3647
  /** 清除对端 prekey 的双层缓存(_peerPrekeysCache + e2ee 内部缓存) */
3434
3648
  _invalidatePeerPrekeyCache(peerAid) {
@@ -3507,10 +3721,10 @@ export class AUNClient {
3507
3721
  catch (exc) {
3508
3722
  // 刷新失败时:若缓存有 PKI 验证过的证书(2 倍 TTL 内)则继续用
3509
3723
  if (cached && now < cached.validatedAt + PEER_CERT_CACHE_TTL * 2) {
3510
- console.warn(`刷新发送方 ${aid} 证书失败,继续使用已验证的内存缓存:`, exc);
3724
+ this._clientLog.warn(`refresh sender ${aid} cert failed, continue using verified memory cache: ${String(exc)}`);
3511
3725
  return true;
3512
3726
  }
3513
- console.warn(`获取发送方 ${aid} 证书失败且无已验证缓存,拒绝信任:`, exc);
3727
+ this._clientLog.warn(`fetch sender ${aid} cert failed and no verify cache, reject trust: ${String(exc)}`);
3514
3728
  return false;
3515
3729
  }
3516
3730
  }
@@ -3638,7 +3852,7 @@ export class AUNClient {
3638
3852
  }
3639
3853
  else {
3640
3854
  failed.push(String(dist.to));
3641
- console.warn('epoch 密钥分发失败 (to=%s):', dist.to, exc);
3855
+ this._clientLog.warn(`epoch keydistributefailed (to=%s):${dist.to} ${exc}`);
3642
3856
  }
3643
3857
  }
3644
3858
  }
@@ -3656,7 +3870,7 @@ export class AUNClient {
3656
3870
  return isJsonObject(result) && result.success === true;
3657
3871
  }
3658
3872
  catch (exc) {
3659
- console.warn(`[aun_core] 刷新 epoch rotation lease 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3873
+ this._clientLog.warn(`refresh epoch rotation lease failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3660
3874
  return false;
3661
3875
  }
3662
3876
  }
@@ -3672,7 +3886,7 @@ export class AUNClient {
3672
3886
  return isJsonObject(result) && result.success === true;
3673
3887
  }
3674
3888
  catch (exc) {
3675
- console.warn(`[aun_core] 提交 epoch key ack 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3889
+ this._clientLog.warn(`commit epoch key ack failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3676
3890
  return false;
3677
3891
  }
3678
3892
  }
@@ -3700,7 +3914,7 @@ export class AUNClient {
3700
3914
  ? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
3701
3915
  : [];
3702
3916
  if (this._aid && !expectedMembers.includes(this._aid)) {
3703
- console.debug(`[aun_core] 放行 group key 分发:新成员恢复 commitment 不匹配属正常 group=${groupId} epoch=${epoch}`);
3917
+ this._clientLog.debug(`allow group key distribute: new member recover commitment mismatch is normal group=${groupId} epoch=${epoch}`);
3704
3918
  }
3705
3919
  else {
3706
3920
  return false;
@@ -3709,7 +3923,7 @@ export class AUNClient {
3709
3923
  }
3710
3924
  return true;
3711
3925
  }
3712
- console.info(`[aun_core] 拒绝缺少 rotation_id 的未来 epoch key 分发: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
3926
+ this._clientLog.info(`reject missing rotation_id future epoch key distribute: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
3713
3927
  return false;
3714
3928
  }
3715
3929
  const pending = isJsonObject(epochResult.pending_rotation) ? epochResult.pending_rotation : null;
@@ -3730,10 +3944,10 @@ export class AUNClient {
3730
3944
  }
3731
3945
  }
3732
3946
  catch (exc) {
3733
- console.warn(`[aun_core] 拒绝无法校验 active rotation epoch key 分发: group=${groupId} rotation=${rotationId} err=${formatCaughtError(exc)}`);
3947
+ this._clientLog.warn(`reject cannot check active rotation epoch key distribute: group=${groupId} rotation=${rotationId} err=${formatCaughtError(exc)}`);
3734
3948
  return false;
3735
3949
  }
3736
- console.info(`[aun_core] 拒绝非 pending/committed 状态的 epoch key 分发: group=${groupId} rotation=${rotationId} epoch=${epoch}`);
3950
+ this._clientLog.info(`reject non-pending/committed state epoch key distribute: group=${groupId} rotation=${rotationId} epoch=${epoch}`);
3737
3951
  return false;
3738
3952
  }
3739
3953
  async _discardGroupDistributionIfStale(payload) {
@@ -3748,10 +3962,10 @@ export class AUNClient {
3748
3962
  return;
3749
3963
  try {
3750
3964
  await this._groupE2ee.discardPendingSecret(groupId, epoch, rotationId);
3751
- console.info('丢弃 verify 后变为 stale group epoch key: group=%s epoch=%s rotation=%s', groupId, epoch, rotationId);
3965
+ this._clientLog.info('discard stale group epoch key after verify: group=%s epoch=%s rotation=%s', groupId, epoch, rotationId);
3752
3966
  }
3753
3967
  catch (exc) {
3754
- console.debug('清理 stale group epoch key 失败: group=%s epoch=%s rotation=%s err=%s', groupId, epoch, rotationId, formatCaughtError(exc));
3968
+ this._clientLog.debug('cleanup stale group epoch key failed: group=%s epoch=%s rotation=%s err=%s', groupId, epoch, rotationId, formatCaughtError(exc));
3755
3969
  }
3756
3970
  }
3757
3971
  async _verifyGroupKeyResponseEpoch(payload) {
@@ -3768,7 +3982,7 @@ export class AUNClient {
3768
3982
  return false;
3769
3983
  const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
3770
3984
  if (epoch > committedEpoch) {
3771
- console.info(`[aun_core] 拒绝未提交 epoch group key response: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
3985
+ this._clientLog.info(`reject uncommitted epoch group key response: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
3772
3986
  return false;
3773
3987
  }
3774
3988
  const committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
@@ -3779,7 +3993,7 @@ export class AUNClient {
3779
3993
  ? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
3780
3994
  : [];
3781
3995
  if (this._aid && !expectedMembers.includes(this._aid)) {
3782
- console.debug(`[aun_core] 放行 group key response:新成员恢复 commitment 不匹配属正常 group=${groupId} epoch=${epoch}`);
3996
+ this._clientLog.debug(`allow group key response: new member recover commitment mismatch is normal group=${groupId} epoch=${epoch}`);
3783
3997
  }
3784
3998
  else {
3785
3999
  return false;
@@ -3789,7 +4003,7 @@ export class AUNClient {
3789
4003
  return true;
3790
4004
  }
3791
4005
  catch (exc) {
3792
- console.warn(`[aun_core] 拒绝无法校验 committed epoch group key response: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
4006
+ this._clientLog.warn(`reject cannot check committed epoch group key response: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
3793
4007
  return false;
3794
4008
  }
3795
4009
  }
@@ -3804,7 +4018,7 @@ export class AUNClient {
3804
4018
  return isJsonObject(result) && result.success === true;
3805
4019
  }
3806
4020
  catch (exc) {
3807
- console.warn(`[aun_core] 中止 epoch rotation 失败: rotation=${rotationId} err=${formatCaughtError(exc)}`);
4021
+ this._clientLog.warn(`abort epoch rotation failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
3808
4022
  return false;
3809
4023
  }
3810
4024
  }
@@ -3848,7 +4062,7 @@ export class AUNClient {
3848
4062
  if (this._closing || this._state !== 'connected')
3849
4063
  return;
3850
4064
  if (Date.now() - started > 20000) {
3851
- console.warn('group epoch create sync still in-flight; skip duplicate sync (group=%s)', groupId);
4065
+ this._clientLog.warn(`group epoch create sync still in-flight; skip duplicate sync (group=%s)${String(groupId)}`);
3852
4066
  return;
3853
4067
  }
3854
4068
  await new Promise((resolve) => setTimeout(resolve, 200));
@@ -3882,12 +4096,12 @@ export class AUNClient {
3882
4096
  const beginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
3883
4097
  const rotation = isJsonObject(beginResult) && isJsonObject(beginResult.rotation) ? beginResult.rotation : null;
3884
4098
  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));
4099
+ this._clientLog.warn('group epoch begin failed; stop key distribution (group=%s, returned=%s)', groupId, JSON.stringify(beginResult));
3886
4100
  return;
3887
4101
  }
3888
4102
  const activeRotationId = String(rotation.rotation_id ?? rotationId);
3889
4103
  if (!await this._ackGroupRotationKey(activeRotationId, secretData.commitment)) {
3890
- console.warn('group epoch self ack failed (group=%s, rotation=%s)', groupId, activeRotationId);
4104
+ this._clientLog.warn(`group epoch self ack failed (group=%s, rotation=%s)${groupId} ${activeRotationId}`);
3891
4105
  await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
3892
4106
  return;
3893
4107
  }
@@ -3904,17 +4118,17 @@ export class AUNClient {
3904
4118
  await storeGroupSecret(this._keystore, this._aid, groupId, 1, secretData.secret, secretData.commitment, secretData.member_aids, secretData.epoch_chain);
3905
4119
  return;
3906
4120
  }
3907
- console.warn('group epoch commit failed (group=%s, returned=%s)', groupId, JSON.stringify(commitResult));
4121
+ this._clientLog.warn('group epoch commit failed (group=%s, returned=%s)', groupId, JSON.stringify(commitResult));
3908
4122
  return;
3909
4123
  }
3910
4124
  catch (exc) {
3911
4125
  if (attempt < maxRetries) {
3912
4126
  const delay = 500 * Math.pow(2, attempt - 1);
3913
- console.warn(`同步 epoch 到服务端失败 (group=${groupId}, 第${attempt}/${maxRetries}), ${delay}ms后重试:`, exc);
4127
+ this._clientLog.warn(`sync epoch to server failed (group=${groupId}, #${attempt}/${maxRetries} ), ${delay}ms then retry: ${String(exc)}`);
3914
4128
  await new Promise(r => setTimeout(r, delay));
3915
4129
  }
3916
4130
  else {
3917
- console.error(`同步 epoch 到服务端最终失败 (group=${groupId}, 已重试${maxRetries}):`, exc);
4131
+ this._clientLog.error(`sync epoch to server final failed (group=${groupId}, retry${maxRetries} ): ${String(exc)}`, exc instanceof Error ? exc : undefined);
3918
4132
  }
3919
4133
  }
3920
4134
  }
@@ -3928,118 +4142,136 @@ export class AUNClient {
3928
4142
  * 避免所有剩余 admin 同时触发 `_rotateGroupEpoch` 造成 CAS 风暴。
3929
4143
  */
3930
4144
  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);
4145
+ const tStart = Date.now();
4146
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch enter: group_id=${groupId} trigger=${triggerId || '-'} expected_epoch=${expectedEpoch ?? '-'} allow_member=${allowMember}`);
3949
4147
  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))
4148
+ const myAid = this._aid;
4149
+ if (!myAid || this._closing || this._state !== 'connected') {
4150
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=not_ready`);
3954
4151
  return;
3955
- const rawList = membersResp.members ?? membersResp.items;
3956
- if (!Array.isArray(rawList))
3957
- 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);
4152
+ }
4153
+ const started = Date.now();
4154
+ while (this._groupEpochRotationInflight.has(groupId)) {
4155
+ if (triggerId && this._groupMembershipRotationDone.has(triggerId)) {
4156
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=trigger_done`);
4157
+ return;
3969
4158
  }
3970
- else if (allowMember && role === 'member') {
3971
- members.push(aid);
4159
+ if (this._closing || this._state !== 'connected') {
4160
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=disconnected_during_wait`);
4161
+ return;
3972
4162
  }
4163
+ if (Date.now() - started > 20000) {
4164
+ this._clientLog.warn(`group epoch rotation still in-flight; skip pending trigger (group=%s trigger=%s)${groupId} ${triggerId || '-'}`);
4165
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=inflight_timeout`);
4166
+ return;
4167
+ }
4168
+ await new Promise((resolve) => setTimeout(resolve, 200));
3973
4169
  }
3974
- // 候选列表:admin/owner 排序在前,member 排序在后
3975
- let candidates = [...admins.sort(), ...members.sort()];
3976
- if (candidates.length === 0)
4170
+ if (this._closing || this._state !== 'connected') {
4171
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=disconnected`);
3977
4172
  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;
4173
+ }
4174
+ this._groupEpochRotationInflight.add(groupId);
4175
+ try {
4176
+ if (this._closing || this._state !== 'connected')
4177
+ return;
4178
+ const membersResp = await this.call('group.get_members', { group_id: groupId });
4179
+ if (!isJsonObject(membersResp))
4180
+ return;
4181
+ const rawList = membersResp.members ?? membersResp.items;
4182
+ if (!Array.isArray(rawList))
4183
+ return;
4184
+ const admins = [];
4185
+ const members = [];
4186
+ for (const m of rawList) {
4187
+ if (!isJsonObject(m))
4188
+ continue;
4189
+ const role = String(m.role ?? '');
4190
+ const aid = String(m.aid ?? '');
4191
+ if (!aid)
4192
+ continue;
4193
+ if (role === 'admin' || role === 'owner') {
4194
+ admins.push(aid);
3985
4195
  }
3986
- else if (!allowMember) {
3987
- return;
4196
+ else if (allowMember && role === 'member') {
4197
+ members.push(aid);
3988
4198
  }
3989
4199
  }
3990
- }
3991
- const leader = candidates[0];
3992
- if (leader === myAid) {
3993
- // 我是 leader,直接发起
4200
+ // 候选列表:admin/owner 排序在前,member 排序在后
4201
+ let candidates = [...admins.sort(), ...members.sort()];
4202
+ if (candidates.length === 0)
4203
+ return;
4204
+ // 没有当前 epoch key 的成员不参与 leader 选举
4205
+ if (expectedEpoch !== null && expectedEpoch > 0) {
4206
+ const localSecret = await this._groupE2ee.loadSecret(groupId, expectedEpoch);
4207
+ if (!localSecret) {
4208
+ const filtered = candidates.filter(c => c !== myAid);
4209
+ if (filtered.length > 0) {
4210
+ candidates = filtered;
4211
+ }
4212
+ else if (!allowMember) {
4213
+ return;
4214
+ }
4215
+ }
4216
+ }
4217
+ const leader = candidates[0];
4218
+ if (leader === myAid) {
4219
+ // 我是 leader,直接发起
4220
+ await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
4221
+ return;
4222
+ }
4223
+ if (!candidates.includes(myAid))
4224
+ return;
4225
+ // 非 leader:随机 jitter(2~6s)后查询服务端 epoch 是否已被 leader 推进
4226
+ const jitterMs = 2000 + Math.floor(Math.random() * 4000);
4227
+ let beforeEpoch = 0;
4228
+ try {
4229
+ const resp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4230
+ if (isJsonObject(resp))
4231
+ beforeEpoch = Number(resp.epoch ?? 0);
4232
+ }
4233
+ catch {
4234
+ beforeEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4235
+ }
4236
+ await new Promise((r) => setTimeout(r, jitterMs));
4237
+ if (this._closing || this._state !== 'connected')
4238
+ return;
4239
+ let afterEpoch = 0;
4240
+ let afterResp = {};
4241
+ try {
4242
+ afterResp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4243
+ if (isJsonObject(afterResp))
4244
+ afterEpoch = Number(afterResp.epoch ?? 0);
4245
+ }
4246
+ catch {
4247
+ afterEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4248
+ }
4249
+ if (afterEpoch > beforeEpoch)
4250
+ return; // leader 已完成
4251
+ const pending = isJsonObject(afterResp) && isJsonObject(afterResp.pending_rotation) ? afterResp.pending_rotation : null;
4252
+ if (pending && !pending.expired) {
4253
+ this._scheduleGroupRotationRetry(groupId, {
4254
+ reason: 'membership_changed',
4255
+ triggerId,
4256
+ expectedEpoch,
4257
+ pending,
4258
+ });
4259
+ return;
4260
+ }
4261
+ this._clientLog.info(`[H21] leader did not complete epoch rotation, non-leader fallback: group=%s myAid=%s${groupId} ${myAid}`);
3994
4262
  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
4263
  }
4020
- catch {
4021
- afterEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
4264
+ catch (exc) {
4265
+ this._clientLog.warn(`_maybeLeadRotateGroupEpoch failed: %s${String(exc)}`);
4022
4266
  }
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;
4267
+ finally {
4268
+ this._groupEpochRotationInflight.delete(groupId);
4034
4269
  }
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);
4270
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
4040
4271
  }
4041
- finally {
4042
- this._groupEpochRotationInflight.delete(groupId);
4272
+ catch (err) {
4273
+ this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
4274
+ throw err;
4043
4275
  }
4044
4276
  }
4045
4277
  /**
@@ -4047,196 +4279,205 @@ export class AUNClient {
4047
4279
  * 使用服务端 CAS 保证只有一方成功。
4048
4280
  */
4049
4281
  async _rotateGroupEpoch(groupId, triggerId = '', expectedEpoch = null) {
4282
+ const tStart = Date.now();
4283
+ this._clientLog.debug(`_rotateGroupEpoch enter: group_id=${groupId} trigger=${triggerId || '-'} expected_epoch=${expectedEpoch ?? '-'}`);
4050
4284
  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 || '-');
4285
+ try {
4286
+ if (!this._aid)
4287
+ return;
4288
+ const memberAids = await this._getGroupMemberAids(groupId);
4289
+ if (triggerId && this._groupMembershipRotationDone.has(triggerId))
4290
+ return;
4291
+ const epochResult = await this.call('group.e2ee.get_epoch', { group_id: groupId });
4292
+ const serverEpoch = isJsonObject(epochResult) ? Number(epochResult.epoch ?? 0) : 0;
4293
+ const pendingRotation = isJsonObject(epochResult) && isJsonObject(epochResult.pending_rotation)
4294
+ ? epochResult.pending_rotation
4295
+ : null;
4296
+ if (pendingRotation && !pendingRotation.expired) {
4297
+ const pendingRotationId = String(pendingRotation.rotation_id ?? '');
4298
+ const stalePending = (expectedEpoch !== null
4299
+ && serverEpoch === expectedEpoch
4300
+ && this._rotationExpectedMembersStale(pendingRotation, memberAids));
4301
+ if (stalePending && await this._abortGroupRotation(pendingRotationId, 'membership_changed_during_rotation')) {
4302
+ this._clientLog.info(`aborted stale pending group epoch rotation: group=%s rotation=%s${groupId} ${pendingRotationId || '-'}`);
4303
+ }
4304
+ else {
4305
+ this._scheduleGroupRotationRetry(groupId, {
4306
+ reason: 'membership_changed',
4307
+ triggerId,
4308
+ expectedEpoch,
4309
+ pending: pendingRotation,
4310
+ });
4311
+ return;
4312
+ }
4068
4313
  }
4069
- else {
4070
- this._scheduleGroupRotationRetry(groupId, {
4071
- reason: 'membership_changed',
4072
- triggerId,
4073
- expectedEpoch,
4074
- pending: pendingRotation,
4075
- });
4314
+ if (expectedEpoch !== null && serverEpoch !== expectedEpoch) {
4315
+ if (triggerId)
4316
+ this._groupMembershipRotationDone.add(triggerId);
4317
+ this._clientLog.info(`skip membership epoch rotation: group=%s expected_epoch=%d server_epoch=%d trigger=%s${groupId} ${expectedEpoch} ${serverEpoch} ${triggerId || '-'}`);
4076
4318
  return;
4077
4319
  }
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}`);
4320
+ const currentEpoch = expectedEpoch ?? serverEpoch;
4321
+ const targetEpoch = currentEpoch + 1;
4322
+ let prevChainHint = null;
4323
+ const localPrev = await this._groupE2ee.loadSecret(groupId, currentEpoch);
4324
+ const localPrevChain = String(localPrev?.epoch_chain ?? '').trim();
4325
+ if (!localPrevChain && isJsonObject(epochResult)) {
4326
+ const cr = epochResult.committed_rotation;
4327
+ if (isJsonObject(cr)) {
4328
+ const rawChain = String(cr.epoch_chain ?? '').trim();
4329
+ if (rawChain) {
4330
+ prevChainHint = rawChain;
4331
+ this._clientLog.info(`rotation supplement prev epoch chain from server: group=${groupId} epoch=${currentEpoch}`);
4332
+ }
4097
4333
  }
4098
4334
  }
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 () => {
4335
+ const rotationId = `rot-${_uuidV4().replace(/-/g, '')}`;
4336
+ const info = await this._groupE2ee.rotateEpochTo(groupId, targetEpoch, memberAids, { rotationId, prevChainHint });
4337
+ this._attachRotationId(info, rotationId);
4338
+ const discardGeneratedPending = async () => {
4339
+ try {
4340
+ await this._groupE2ee.discardPendingSecret(groupId, targetEpoch, rotationId);
4341
+ }
4342
+ catch (cleanupExc) {
4343
+ this._clientLog.debug('cleanup local pending group key failed: group=%s epoch=%d rotation=%s err=%s', groupId, targetEpoch, rotationId, formatCaughtError(cleanupExc));
4344
+ }
4345
+ };
4346
+ const rotateParams = {
4347
+ group_id: groupId,
4348
+ base_epoch: currentEpoch,
4349
+ target_epoch: targetEpoch,
4350
+ rotation_id: rotationId,
4351
+ reason: triggerId || expectedEpoch !== null ? 'membership_changed' : 'manual',
4352
+ key_commitment: String(info.commitment ?? ''),
4353
+ expected_members: memberAids,
4354
+ required_acks: [this._aid],
4355
+ lease_ms: GROUP_ROTATION_LEASE_MS,
4356
+ };
4357
+ const sigParams = await this._buildRotationSignature(groupId, currentEpoch, targetEpoch, rotateParams);
4358
+ Object.assign(rotateParams, sigParams);
4359
+ let rawBeginResult;
4104
4360
  try {
4105
- await this._groupE2ee.discardPendingSecret(groupId, targetEpoch, rotationId);
4361
+ rawBeginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
4106
4362
  }
4107
- catch (cleanupExc) {
4108
- console.debug('清理本地 pending group key 失败: group=%s epoch=%d rotation=%s err=%s', groupId, targetEpoch, rotationId, formatCaughtError(cleanupExc));
4363
+ catch (exc) {
4364
+ await discardGeneratedPending();
4365
+ throw exc;
4109
4366
  }
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
- });
4367
+ const beginResult = isJsonObject(rawBeginResult) ? rawBeginResult : null;
4368
+ const beginRotationRaw = beginResult ? beginResult.rotation : null;
4369
+ const rotation = isJsonObject(beginRotationRaw) ? beginRotationRaw : null;
4370
+ if (!beginResult || beginResult.success !== true || !rotation) {
4371
+ if (rotation && !rotation.expired) {
4372
+ if (this._rotationExpectedMembersStale(rotation, memberAids)
4373
+ && await this._abortGroupRotation(String(rotation.rotation_id ?? ''), 'membership_changed_during_rotation')) {
4374
+ this._scheduleGroupRotationRetry(groupId, {
4375
+ reason: 'membership_changed',
4376
+ triggerId,
4377
+ expectedEpoch,
4378
+ pending: null,
4379
+ });
4380
+ }
4381
+ else {
4382
+ this._scheduleGroupRotationRetry(groupId, {
4383
+ reason: 'membership_changed',
4384
+ triggerId,
4385
+ expectedEpoch,
4386
+ pending: rotation,
4387
+ });
4388
+ }
4145
4389
  }
4146
- else {
4390
+ else if (beginResult && beginResult.reason === 'expected_members_mismatch') {
4147
4391
  this._scheduleGroupRotationRetry(groupId, {
4148
4392
  reason: 'membership_changed',
4149
4393
  triggerId,
4150
4394
  expectedEpoch,
4151
- pending: rotation,
4395
+ pending: null,
4152
4396
  });
4153
4397
  }
4398
+ this._clientLog.warn('group epoch begin failed; stop key distribution (group=%s, current_epoch=%d, returned=%s)', groupId, currentEpoch, JSON.stringify(beginResult));
4399
+ await discardGeneratedPending();
4400
+ return;
4154
4401
  }
4155
- else if (beginResult && beginResult.reason === 'expected_members_mismatch') {
4402
+ const activeRotationId = String(rotation.rotation_id ?? rotationId);
4403
+ const distributeResult = await this._distributeGroupEpochKey(info, activeRotationId);
4404
+ if (distributeResult.failed.length > 0) {
4405
+ this._clientLog.warn('group epoch key distribution incomplete; abort rotation before retry (group=%s rotation=%s failed=%s)', groupId, activeRotationId, distributeResult.failed.join(','));
4406
+ await this._abortGroupRotation(activeRotationId, 'distribution_failed');
4156
4407
  this._scheduleGroupRotationRetry(groupId, {
4157
4408
  reason: 'membership_changed',
4158
4409
  triggerId,
4159
4410
  expectedEpoch,
4160
4411
  pending: null,
4161
4412
  });
4413
+ await discardGeneratedPending();
4414
+ return;
4162
4415
  }
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')) {
4416
+ await this._heartbeatGroupRotation(activeRotationId);
4417
+ if (!await this._ackGroupRotationKey(activeRotationId, String(info.commitment ?? ''))) {
4418
+ this._clientLog.warn(`group epoch self ack failed; abort rotation before retry (group=%s rotation=%s)${groupId} ${activeRotationId}`);
4419
+ await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
4420
+ this._scheduleGroupRotationRetry(groupId, {
4421
+ reason: 'membership_changed',
4422
+ triggerId,
4423
+ expectedEpoch,
4424
+ pending: null,
4425
+ });
4215
4426
  await discardGeneratedPending();
4427
+ return;
4216
4428
  }
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);
4429
+ const commitParams = { rotation_id: activeRotationId };
4430
+ // 构建 per-member ECIES 加密的 epoch key 上传到服务端
4431
+ if (await this._groupAllowsMemberEpochRotation(groupId)) {
4432
+ const encryptedKeys = await this._buildEpochEncryptedKeys(info, memberAids, targetEpoch, groupId);
4433
+ if (encryptedKeys && Object.keys(encryptedKeys).length > 0) {
4434
+ commitParams.encrypted_keys = encryptedKeys;
4435
+ }
4226
4436
  }
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);
4437
+ const commitResult = await this.call('group.e2ee.commit_rotation', commitParams);
4438
+ if (!isJsonObject(commitResult) || commitResult.success !== true) {
4439
+ this._clientLog.warn('group epoch commit failed (group=%s, rotation=%s, returned=%s)', groupId, activeRotationId, JSON.stringify(commitResult));
4440
+ this._scheduleGroupRotationRetry(groupId, {
4441
+ reason: 'membership_changed',
4442
+ triggerId,
4443
+ expectedEpoch,
4444
+ pending: isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : rotation,
4445
+ });
4446
+ const returnedRotation = isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : null;
4447
+ if (!(returnedRotation
4448
+ && String(returnedRotation.rotation_id ?? '') === activeRotationId
4449
+ && String(returnedRotation.status ?? '') === 'distributing')) {
4450
+ await discardGeneratedPending();
4451
+ }
4452
+ return;
4229
4453
  }
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));
4454
+ const committedSecret = await this._groupE2ee.loadSecret(groupId, targetEpoch);
4455
+ if (committedSecret && this._aid) {
4456
+ const committedRotation = isJsonObject(commitResult.rotation)
4457
+ ? commitResult.rotation
4458
+ : { rotation_id: activeRotationId, key_commitment: String(info.commitment ?? '') };
4459
+ if (this._groupSecretMatchesCommittedRotation(committedSecret, committedRotation)) {
4460
+ await storeGroupSecret(this._keystore, this._aid, groupId, targetEpoch, committedSecret.secret, committedSecret.commitment, committedSecret.member_aids.length > 0 ? committedSecret.member_aids : memberAids, committedSecret.epoch_chain);
4461
+ }
4462
+ else {
4463
+ 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}`);
4464
+ }
4465
+ }
4466
+ if (triggerId) {
4467
+ this._groupMembershipRotationDone.add(triggerId);
4468
+ if (this._groupMembershipRotationDone.size > 2000) {
4469
+ this._groupMembershipRotationDone = new Set(Array.from(this._groupMembershipRotationDone).slice(-1000));
4470
+ }
4235
4471
  }
4236
4472
  }
4473
+ catch (exc) {
4474
+ this._logE2eeError('rotate_epoch', groupId, '', exc);
4475
+ }
4476
+ this._clientLog.debug(`_rotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
4237
4477
  }
4238
- catch (exc) {
4239
- this._logE2eeError('rotate_epoch', groupId, '', exc);
4478
+ catch (err) {
4479
+ this._clientLog.debug(`_rotateGroupEpoch exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
4480
+ throw err;
4240
4481
  }
4241
4482
  }
4242
4483
  /** 从成员加入事件 payload 中提取新加入的成员 AID 列表。 */
@@ -4416,73 +4657,82 @@ export class AUNClient {
4416
4657
  }
4417
4658
  // ── 内部:连接 ────────────────────────────────────
4418
4659
  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();
4660
+ const tStart = Date.now();
4661
+ this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
4429
4662
  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 ?? '');
4663
+ const gatewayUrl = this._resolveGateway(params);
4664
+ this._gatewayUrl = gatewayUrl;
4665
+ this._slotId = String(params.slot_id ?? '');
4666
+ this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
4667
+ this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
4668
+ this._state = 'connecting';
4669
+ // 前置 restore:在 _transport.connect 启动 reader 之前完成,
4670
+ // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
4671
+ this._refreshSeqTrackerContext();
4672
+ await this._restoreSeqTrackerState();
4673
+ try {
4674
+ const challenge = await this._transport.connect(gatewayUrl);
4675
+ this._state = 'authenticating';
4676
+ if (allowReauth) {
4677
+ const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
4678
+ accessToken: params.access_token,
4679
+ deviceId: this._deviceId,
4680
+ slotId: this._slotId,
4681
+ deliveryMode: this._connectDeliveryMode,
4682
+ });
4683
+ if (isJsonObject(authContext)) {
4684
+ const auth = authContext;
4685
+ const identity = auth.identity;
4686
+ if (identity && isJsonObject(identity)) {
4687
+ this._identity = identity;
4688
+ this._aid = String(identity.aid ?? this._aid ?? '');
4689
+ if (this._sessionParams) {
4690
+ this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
4691
+ }
4447
4692
  }
4448
4693
  }
4449
4694
  }
4695
+ else {
4696
+ await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4697
+ deviceId: this._deviceId,
4698
+ slotId: this._slotId,
4699
+ deliveryMode: this._connectDeliveryMode,
4700
+ });
4701
+ await this._syncIdentityAfterConnect(String(params.access_token));
4702
+ }
4450
4703
  }
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));
4704
+ catch (err) {
4705
+ // P1-19: 首连失败时重置状态,避免半连接残留
4706
+ this._state = 'disconnected';
4707
+ try {
4708
+ await this._transport.close();
4709
+ }
4710
+ catch { /* 忽略关闭错误 */ }
4711
+ throw err;
4458
4712
  }
4459
- }
4460
- catch (err) {
4461
- // P1-19: 首连失败时重置状态,避免半连接残留
4462
- this._state = 'disconnected';
4713
+ this._state = 'connected';
4714
+ await this._dispatcher.publish('connection.state', {
4715
+ state: this._state,
4716
+ gateway: gatewayUrl,
4717
+ });
4718
+ // auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
4719
+ if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
4720
+ this._refreshSeqTrackerContext();
4721
+ await this._restoreSeqTrackerState();
4722
+ }
4723
+ this._startBackgroundTasks();
4724
+ // 上线后自动上传 prekey
4463
4725
  try {
4464
- await this._transport.close();
4726
+ await this._uploadPrekey();
4465
4727
  }
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();
4728
+ catch (exc) {
4729
+ this._clientLog.warn(`prekey upload failed:${String(exc)}`);
4730
+ }
4731
+ this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? '-'}`);
4483
4732
  }
4484
- catch (exc) {
4485
- console.warn('prekey 上传失败:', exc);
4733
+ catch (err) {
4734
+ this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
4735
+ throw err;
4486
4736
  }
4487
4737
  }
4488
4738
  _resolveGateway(params) {
@@ -4650,10 +4900,10 @@ export class AUNClient {
4650
4900
  }
4651
4901
  catch (exc) {
4652
4902
  consecutiveFailures++;
4653
- console.warn(`心跳失败 (${consecutiveFailures}/${maxFailures}):`, exc);
4903
+ this._clientLog.warn(`heartbeat failed (${consecutiveFailures}/${maxFailures}): ${String(exc)}`);
4654
4904
  this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
4655
4905
  if (consecutiveFailures >= maxFailures) {
4656
- console.warn(`连续 ${maxFailures} 次心跳失败,触发断线重连`);
4906
+ this._clientLog.warn(`consecutive ${maxFailures} heartbeat failed, trigger disconnect reconnect`);
4657
4907
  this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
4658
4908
  }
4659
4909
  }
@@ -4712,7 +4962,7 @@ export class AUNClient {
4712
4962
  if (exc instanceof AuthError) {
4713
4963
  this._tokenRefreshFailures++;
4714
4964
  if (this._tokenRefreshFailures >= 3) {
4715
- console.warn(`token 刷新连续失败 ${this._tokenRefreshFailures} 次,停止刷新循环并触发重连`);
4965
+ this._clientLog.warn(`token refreshconsecutivefailed ${this._tokenRefreshFailures} , stop refresh loop and trigger reconnect`);
4716
4966
  this._dispatcher.publish('token.refresh_exhausted', {
4717
4967
  aid: this._identity?.aid ?? null,
4718
4968
  consecutive_failures: this._tokenRefreshFailures,
@@ -4722,7 +4972,7 @@ export class AUNClient {
4722
4972
  this._handleTransportDisconnect(new Error('token refresh exhausted, triggering reconnect'));
4723
4973
  return;
4724
4974
  }
4725
- console.warn(`token 刷新失败 (${this._tokenRefreshFailures}/3),下次重试:`, exc);
4975
+ this._clientLog.warn(`token refresh failed (${this._tokenRefreshFailures}/3), next retry: ${String(exc)}`);
4726
4976
  }
4727
4977
  else {
4728
4978
  this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
@@ -4770,7 +5020,7 @@ export class AUNClient {
4770
5020
  }
4771
5021
  }
4772
5022
  catch (exc) {
4773
- console.warn('[aun_core] prekey 定时刷新失败:', exc);
5023
+ this._clientLog.warn(`prekey scheduled refresh failed:${String(exc)}`);
4774
5024
  }
4775
5025
  // 仍处于连接状态时安排下一次检查
4776
5026
  if (this._state === 'connected') {
@@ -4869,7 +5119,7 @@ export class AUNClient {
4869
5119
  this._prekeyReplenished.add(prekeyId);
4870
5120
  }
4871
5121
  catch (exc) {
4872
- console.warn(`消费 prekey ${prekeyId} 后补充 current prekey 失败:`, exc);
5122
+ this._clientLog.warn(`consume prekey ${prekeyId} then replenish current prekey failed: ${String(exc)}`);
4873
5123
  }
4874
5124
  finally {
4875
5125
  this._prekeyReplenishInflight.delete(prekeyId);
@@ -4894,7 +5144,7 @@ export class AUNClient {
4894
5144
  }
4895
5145
  }
4896
5146
  catch (exc) {
4897
- console.warn('epoch 清理失败:', exc);
5147
+ this._clientLog.warn(`epoch cleanup failed:${String(exc)}`);
4898
5148
  }
4899
5149
  }, 3600 * 1000);
4900
5150
  }
@@ -4913,7 +5163,7 @@ export class AUNClient {
4913
5163
  }
4914
5164
  }
4915
5165
  catch (exc) {
4916
- console.warn('epoch 定时轮换失败:', exc);
5166
+ this._clientLog.warn(`epoch scheduled rotation failed:${String(exc)}`);
4917
5167
  }
4918
5168
  }, rotateInterval * 1000);
4919
5169
  }
@@ -4946,7 +5196,7 @@ export class AUNClient {
4946
5196
  _onGatewayDisconnect(data) {
4947
5197
  const code = data?.code;
4948
5198
  const reason = data?.reason ?? '';
4949
- console.warn(`[aun_core] 服务端主动断开: code=${code}, reason=${reason}`);
5199
+ this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}`);
4950
5200
  this._serverKicked = true;
4951
5201
  }
4952
5202
  async _handleTransportDisconnect(error, closeCode) {
@@ -4967,7 +5217,7 @@ export class AUNClient {
4967
5217
  if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
4968
5218
  this._state = 'terminal_failed';
4969
5219
  const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
4970
- console.warn(`[aun_core] 抑制自动重连: ${reason}`);
5220
+ this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
4971
5221
  await this._dispatcher.publish('connection.state', {
4972
5222
  state: this._state, error, reason,
4973
5223
  });
@@ -5057,62 +5307,80 @@ export class AUNClient {
5057
5307
  * 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
5058
5308
  */
5059
5309
  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);
5310
+ const tStart = Date.now();
5311
+ this._clientLog.debug(`createNamedGroup enter: name=${groupName}`);
5312
+ try {
5313
+ const cp = new CryptoProvider();
5314
+ const identity = await cp.generateIdentity();
5315
+ const params = {};
5316
+ for (const [k, v] of Object.entries(opts)) {
5317
+ params[k] = v;
5318
+ }
5319
+ params.group_name = groupName;
5320
+ params.public_key = identity.public_key_der_b64;
5321
+ params.curve = 'P-256';
5322
+ const result = await this.call('group.create', params);
5323
+ const groupInfo = result?.group;
5324
+ const aidCert = result?.aid_cert;
5325
+ const groupAid = String(groupInfo?.group_aid ?? '');
5326
+ if (groupAid && aidCert) {
5327
+ await this._keystore.saveIdentity(groupAid, {
5328
+ private_key_pem: identity.private_key_pem,
5329
+ public_key: identity.public_key_der_b64,
5330
+ curve: 'P-256',
5331
+ type: 'group_identity',
5332
+ });
5333
+ const certPem = String(aidCert.cert ?? '');
5334
+ if (certPem) {
5335
+ await this._keystore.saveCert(groupAid, certPem);
5336
+ }
5083
5337
  }
5338
+ this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
5339
+ return result;
5340
+ }
5341
+ catch (err) {
5342
+ this._clientLog.debug(`createNamedGroup exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
5343
+ throw err;
5084
5344
  }
5085
- return result;
5086
5345
  }
5087
5346
  /**
5088
5347
  * 为已有普通群绑定命名 AID(升级为命名群)。
5089
5348
  */
5090
5349
  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,
5350
+ const tStart = Date.now();
5351
+ this._clientLog.debug(`bindGroupAid enter: group_id=${groupId} name=${groupName}`);
5352
+ try {
5353
+ const cp = new CryptoProvider();
5354
+ const identity = await cp.generateIdentity();
5355
+ const params = {
5356
+ group_id: groupId,
5357
+ group_name: groupName,
5106
5358
  public_key: identity.public_key_der_b64,
5107
5359
  curve: 'P-256',
5108
- type: 'group_identity',
5109
- });
5110
- const certPem = String(aidCert.cert ?? '');
5111
- if (certPem) {
5112
- await this._keystore.saveCert(groupAid, certPem);
5360
+ };
5361
+ const result = await this.call('group.bind_aid', params);
5362
+ const groupInfo = result?.group;
5363
+ const aidCert = result?.aid_cert;
5364
+ const groupAid = String(groupInfo?.group_aid ?? '');
5365
+ if (groupAid && aidCert) {
5366
+ await this._keystore.saveIdentity(groupAid, {
5367
+ private_key_pem: identity.private_key_pem,
5368
+ public_key: identity.public_key_der_b64,
5369
+ curve: 'P-256',
5370
+ type: 'group_identity',
5371
+ });
5372
+ const certPem = String(aidCert.cert ?? '');
5373
+ if (certPem) {
5374
+ await this._keystore.saveCert(groupAid, certPem);
5375
+ }
5113
5376
  }
5377
+ this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
5378
+ return result;
5379
+ }
5380
+ catch (err) {
5381
+ this._clientLog.debug(`bindGroupAid exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
5382
+ throw err;
5114
5383
  }
5115
- return result;
5116
5384
  }
5117
5385
  /** 判断是否应重试重连 */
5118
5386
  _shouldRetryReconnect(error) {
@@ -5338,7 +5606,7 @@ export class AUNClient {
5338
5606
  /** 安全执行异步操作(不阻塞调用方,错误打 warning 便于排障) */
5339
5607
  _safeAsync(promise) {
5340
5608
  promise.catch((exc) => {
5341
- console.warn('后台任务异常:', exc);
5609
+ this._clientLog.warn(`background task exception:${String(exc)}`);
5342
5610
  });
5343
5611
  }
5344
5612
  /** 可取消的 sleep */