@agentunion/fastaun-browser 0.5.4 → 0.5.6

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 (151) hide show
  1. package/CHANGELOG.md +130 -94
  2. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/216/245/345/205/245/344/270/216/346/274/224/347/244/272/346/214/207/345/215/227.md +290 -0
  3. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/346/236/266/346/236/204/344/270/216/350/257/246/347/273/206/345/256/236/347/216/260/350/256/241/345/210/222-codex.md +994 -0
  4. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/350/277/220/347/273/264/344/270/216/345/217/221/345/270/203/346/214/207/345/215/227.md +144 -0
  5. package/_packed_docs/CHANGELOG.md +130 -94
  6. package/_packed_docs/INDEX.md +127 -69
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
  8. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +366 -0
  9. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +227 -0
  10. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1004 -0
  11. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +999 -0
  12. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
  13. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
  14. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
  15. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
  16. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
  17. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +165 -421
  18. package/_packed_docs/protocol/README.md +1 -0
  19. package/_packed_docs/protocol/aun-docs-guide.md +7 -4
  20. package/_packed_docs/protocol/index.md +25 -19
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +62 -17
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -78
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +50 -14
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +312 -282
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
  29. package/_packed_docs/sdk/09-payload-reference.md +1 -1
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
  32. package/_packed_docs/sdk/INDEX.md +42 -34
  33. package/_packed_docs/sdk/README.md +7 -6
  34. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/344/273/243/347/240/201/345/256/241/346/237/245/351/227/256/351/242/230/346/270/205/345/215/225.md +229 -0
  35. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/346/224/271/351/200/240/346/226/271/346/241/210.md +750 -0
  36. package/dist/agent-md.d.ts +4 -5
  37. package/dist/agent-md.d.ts.map +1 -1
  38. package/dist/agent-md.js +66 -43
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +2 -6
  41. package/dist/aid-store.d.ts.map +1 -1
  42. package/dist/aid-store.js +20 -14
  43. package/dist/aid-store.js.map +1 -1
  44. package/dist/auth.d.ts.map +1 -1
  45. package/dist/auth.js +30 -11
  46. package/dist/auth.js.map +1 -1
  47. package/dist/bundle.js +3297 -1023
  48. package/dist/client/delivery.d.ts +75 -8
  49. package/dist/client/delivery.d.ts.map +1 -1
  50. package/dist/client/delivery.js +1419 -245
  51. package/dist/client/delivery.js.map +1 -1
  52. package/dist/client/group-state.d.ts.map +1 -1
  53. package/dist/client/group-state.js +27 -24
  54. package/dist/client/group-state.js.map +1 -1
  55. package/dist/client/lifecycle.d.ts.map +1 -1
  56. package/dist/client/lifecycle.js +48 -4
  57. package/dist/client/lifecycle.js.map +1 -1
  58. package/dist/client/mention-mode.d.ts +7 -0
  59. package/dist/client/mention-mode.d.ts.map +1 -0
  60. package/dist/client/mention-mode.js +184 -0
  61. package/dist/client/mention-mode.js.map +1 -0
  62. package/dist/client/peers.d.ts +1 -1
  63. package/dist/client/peers.d.ts.map +1 -1
  64. package/dist/client/peers.js +26 -3
  65. package/dist/client/peers.js.map +1 -1
  66. package/dist/client/rpc-pipeline.d.ts +22 -1
  67. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  68. package/dist/client/rpc-pipeline.js +309 -74
  69. package/dist/client/rpc-pipeline.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +25 -2
  71. package/dist/client/v2-e2ee.d.ts.map +1 -1
  72. package/dist/client/v2-e2ee.js +606 -97
  73. package/dist/client/v2-e2ee.js.map +1 -1
  74. package/dist/client.d.ts +24 -5
  75. package/dist/client.d.ts.map +1 -1
  76. package/dist/client.js +404 -215
  77. package/dist/client.js.map +1 -1
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +4 -1
  80. package/dist/errors.js.map +1 -1
  81. package/dist/facades.d.ts +3 -0
  82. package/dist/facades.d.ts.map +1 -1
  83. package/dist/facades.js +91 -53
  84. package/dist/facades.js.map +1 -1
  85. package/dist/group-index.d.ts +6 -1
  86. package/dist/group-index.d.ts.map +1 -1
  87. package/dist/group-index.js +44 -25
  88. package/dist/group-index.js.map +1 -1
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/keystore/index.d.ts +10 -5
  94. package/dist/keystore/index.d.ts.map +1 -1
  95. package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
  96. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  97. package/dist/keystore/indexeddb-identity-store.js +0 -60
  98. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  99. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  100. package/dist/keystore/indexeddb-shared.js +9 -5
  101. package/dist/keystore/indexeddb-shared.js.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.d.ts +3 -1
  103. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  104. package/dist/keystore/indexeddb-token-store.js +47 -2
  105. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  106. package/dist/register-flow.d.ts.map +1 -1
  107. package/dist/register-flow.js +28 -3
  108. package/dist/register-flow.js.map +1 -1
  109. package/dist/seq-tracker.d.ts +28 -8
  110. package/dist/seq-tracker.d.ts.map +1 -1
  111. package/dist/seq-tracker.js +218 -61
  112. package/dist/seq-tracker.js.map +1 -1
  113. package/dist/storage/vfs.d.ts +1 -0
  114. package/dist/storage/vfs.d.ts.map +1 -1
  115. package/dist/storage/vfs.js +26 -2
  116. package/dist/storage/vfs.js.map +1 -1
  117. package/dist/tools/cross-sdk-agent.js +376 -10
  118. package/dist/tools/cross-sdk-agent.js.map +1 -1
  119. package/dist/transport.d.ts +12 -0
  120. package/dist/transport.d.ts.map +1 -1
  121. package/dist/transport.js +218 -101
  122. package/dist/transport.js.map +1 -1
  123. package/dist/v2/session/session.d.ts.map +1 -1
  124. package/dist/v2/session/session.js +3 -4
  125. package/dist/v2/session/session.js.map +1 -1
  126. package/dist/v2/state/commitment.d.ts.map +1 -1
  127. package/dist/v2/state/commitment.js +1 -2
  128. package/dist/v2/state/commitment.js.map +1 -1
  129. package/dist/version.d.ts +1 -1
  130. package/dist/version.js +1 -1
  131. package/package.json +6 -5
  132. package/dist/group-resources.d.ts +0 -98
  133. package/dist/group-resources.d.ts.map +0 -1
  134. package/dist/group-resources.js +0 -635
  135. package/dist/group-resources.js.map +0 -1
  136. package/dist/keystore/indexeddb.d.ts +0 -179
  137. package/dist/keystore/indexeddb.d.ts.map +0 -1
  138. package/dist/keystore/indexeddb.js +0 -2031
  139. package/dist/keystore/indexeddb.js.map +0 -1
  140. package/dist/namespaces/auth.d.ts +0 -98
  141. package/dist/namespaces/auth.d.ts.map +0 -1
  142. package/dist/namespaces/auth.js +0 -992
  143. package/dist/namespaces/auth.js.map +0 -1
  144. package/dist/namespaces/custody.d.ts +0 -51
  145. package/dist/namespaces/custody.d.ts.map +0 -1
  146. package/dist/namespaces/custody.js +0 -302
  147. package/dist/namespaces/custody.js.map +0 -1
  148. package/dist/namespaces/meta.d.ts +0 -109
  149. package/dist/namespaces/meta.d.ts.map +0 -1
  150. package/dist/namespaces/meta.js +0 -549
  151. package/dist/namespaces/meta.js.map +0 -1
package/dist/client.js CHANGED
@@ -6,7 +6,6 @@
6
6
  // - 后台任务使用 setTimeout/setInterval
7
7
  import { createConfig, getDeviceId, normalizeSlotId } from './config.js';
8
8
  import { EventDispatcher } from './events.js';
9
- import { normalizeGroupId } from './group-id.js';
10
9
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
11
10
  import { GatewayDiscovery } from './discovery.js';
12
11
  import { AIDStore } from './aid-store.js';
@@ -15,6 +14,7 @@ import { AuthFlow } from './auth.js';
15
14
  import { SeqTracker } from './seq-tracker.js';
16
15
  import { ClientRuntime } from './client/runtime.js';
17
16
  import { MessageDeliveryEngine } from './client/delivery.js';
17
+ import { normalizeGroupMentionMode } from './client/mention-mode.js';
18
18
  import { IdentityRuntimeManager } from './client/identity.js';
19
19
  import { LifecycleController } from './client/lifecycle.js';
20
20
  import { PeerDirectory } from './client/peers.js';
@@ -78,7 +78,7 @@ function groupIndexBodyText(value) {
78
78
  }
79
79
  const DEFAULT_SESSION_OPTIONS = {
80
80
  auto_reconnect: true,
81
- heartbeat_interval: 30.0,
81
+ heartbeat_interval: 0.0,
82
82
  token_refresh_before: 1800.0,
83
83
  retry: {
84
84
  initial_delay: 1.0,
@@ -96,20 +96,6 @@ const RECONNECT_MIN_BASE_DELAY_SECONDS = 1.0;
96
96
  const RECONNECT_MAX_BASE_DELAY_SECONDS = 64.0;
97
97
  const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
98
98
  const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
99
- // 心跳间隔下/上限(秒)。0 = 关闭心跳;负值视为 0;其余值 clamp 到 [10, 600]。
100
- // 服务端通过 hello.heartbeat_interval 与 meta.ping pong 中的同名字段下发。
101
- const HEARTBEAT_MIN_INTERVAL_SECONDS = 10;
102
- const HEARTBEAT_MAX_INTERVAL_SECONDS = 600;
103
- function clampHeartbeatInterval(value) {
104
- const n = Number(value);
105
- if (!Number.isFinite(n) || n <= 0)
106
- return 0;
107
- if (n < HEARTBEAT_MIN_INTERVAL_SECONDS)
108
- return HEARTBEAT_MIN_INTERVAL_SECONDS;
109
- if (n > HEARTBEAT_MAX_INTERVAL_SECONDS)
110
- return HEARTBEAT_MAX_INTERVAL_SECONDS;
111
- return n;
112
- }
113
99
  // P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
114
100
  const NON_IDEMPOTENT_TIMEOUT = 35;
115
101
  const NON_IDEMPOTENT_METHODS = new Set([
@@ -204,7 +190,8 @@ function clampReconnectDelaySeconds(value, fallback, upper = RECONNECT_MAX_BASE_
204
190
  return Math.min(Math.max(seconds, RECONNECT_MIN_BASE_DELAY_SECONDS), upper);
205
191
  }
206
192
  function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
207
- return baseDelay + Math.random() * maxBaseDelay;
193
+ const jitterLimit = Math.min(Math.max(baseDelay, 0), Math.max(maxBaseDelay - baseDelay, 0));
194
+ return baseDelay + Math.random() * jitterLimit;
208
195
  }
209
196
  /** 对端证书缓存 TTL(秒) */
210
197
  const PEER_CERT_CACHE_TTL = 3600;
@@ -303,7 +290,6 @@ function createAgentMdManagerForRuntime(opts) {
303
290
  if (current?.aid === target) {
304
291
  if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
305
292
  return current;
306
- throw new StateError(`current AID certificate fingerprint mismatch for ${target}`);
307
293
  }
308
294
  if (!opts.gateway.get()) {
309
295
  try {
@@ -529,6 +515,7 @@ export class AUNClient {
529
515
  _currentAid = null;
530
516
  _instanceProtectedHeaders = null;
531
517
  _gatewayUrl = null;
518
+ _gatewayCandidates = [];
532
519
  _deviceId;
533
520
  _slotId;
534
521
  _connectedAt = 0;
@@ -604,6 +591,7 @@ export class AUNClient {
604
591
  _backgroundRpcDepth = 0;
605
592
  // Pull Gate:序列化同一 key 的并发 pull 操作,防止重复拉取
606
593
  _pullGates = new Map();
594
+ _pullResponseKeys = new Map();
607
595
  // 重连相关
608
596
  _reconnectActive = false;
609
597
  _reconnectAbort = null;
@@ -842,7 +830,6 @@ export class AUNClient {
842
830
  for (const groupAid of Object.keys(groupIndexes)) {
843
831
  await this._persistGroupIndexCache(groupAid, {
844
832
  remote_meta: this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid) ?? {},
845
- local_etag: this._groupIndexMetaCache.localEtag(this._aid ?? '', groupAid),
846
833
  });
847
834
  }
848
835
  }
@@ -850,8 +837,17 @@ export class AUNClient {
850
837
  return this._groupIndexMetaCache.isStale(this._aid ?? '', groupAid);
851
838
  }
852
839
  markGroupIndexFresh(groupAid, options) {
853
- this._groupIndexMetaCache.markFresh(this._aid ?? '', groupAid, options);
854
- void this._persistGroupIndexCache(groupAid, { local_etag: String(options.etag ?? '') });
840
+ const verificationVersion = this._groupIndexMetaCache.markFresh(this._aid ?? '', groupAid, options);
841
+ const fields = {
842
+ local_etag: String(options.etag ?? ''),
843
+ verification_version: verificationVersion,
844
+ };
845
+ if (verificationVersion !== 1) {
846
+ fields.index_jsonl = '';
847
+ fields.settings = {};
848
+ fields.entry_etags = {};
849
+ }
850
+ void this._persistGroupIndexCache(groupAid, fields);
855
851
  }
856
852
  getGroupIndexRemoteMeta(groupAid) {
857
853
  return this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid);
@@ -868,6 +864,9 @@ export class AUNClient {
868
864
  return this._groupIndexMetaCache.cachedSettingsByEntries(this._aid ?? '', groupAid, keys.map((item) => String(item)), entries);
869
865
  }
870
866
  cacheGroupIndexSettings(groupAid, settings, options) {
867
+ if (!Array.isArray(options?.entries) || !String(options?.etag ?? '').trim()) {
868
+ return Promise.resolve();
869
+ }
871
870
  this._groupIndexMetaCache.cacheSettings(this._aid ?? '', groupAid, settings, options);
872
871
  const entryEtags = {};
873
872
  for (const item of options?.entries ?? []) {
@@ -876,6 +875,7 @@ export class AUNClient {
876
875
  entryEtags[key] = String(item.etag ?? '');
877
876
  }
878
877
  const fields = {
878
+ verification_version: 1,
879
879
  settings,
880
880
  entry_etags: entryEtags,
881
881
  remote_meta: this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid) ?? {},
@@ -906,6 +906,7 @@ export class AUNClient {
906
906
  local_etag: record.local_etag,
907
907
  settings: record.settings,
908
908
  entry_etags: record.entry_etags,
909
+ verification_version: record.verification_version,
909
910
  });
910
911
  }
911
912
  async _persistGroupIndexCache(groupAid, fields) {
@@ -920,6 +921,7 @@ export class AUNClient {
920
921
  local_etag: record.local_etag,
921
922
  settings: record.settings,
922
923
  entry_etags: record.entry_etags,
924
+ verification_version: record.verification_version,
923
925
  });
924
926
  }
925
927
  get state() {
@@ -980,6 +982,8 @@ export class AUNClient {
980
982
  }
981
983
  _resetV2IdentityRuntime() {
982
984
  this._v2RuntimeGeneration += 1;
985
+ // 身份或 V2 会话代次切换时,隔离仍在途的 inline 解密/应用回调。
986
+ this._delivery?.resetInlineAckState();
983
987
  const keyStore = this._v2KeyStore;
984
988
  try {
985
989
  keyStore?.close?.();
@@ -1111,8 +1115,8 @@ export class AUNClient {
1111
1115
  getPeer(aid) {
1112
1116
  return this._peerDirectory.getPeer(aid);
1113
1117
  }
1114
- async lookupPeer(aid) {
1115
- return this._peerDirectory.lookupPeer(aid);
1118
+ async lookupPeer(aid, certFingerprint) {
1119
+ return this._peerDirectory.lookupPeer(aid, certFingerprint);
1116
1120
  }
1117
1121
  peers() {
1118
1122
  return this._peerDirectory.peers();
@@ -1539,15 +1543,16 @@ export class AUNClient {
1539
1543
  const eventMethod = AUNClient._validateNotifyEventMethod(method);
1540
1544
  // 校验目标 Group ID 格式
1541
1545
  validateGroupIDFormat(targetGroupId, 'notify.group_id');
1542
- const normalizedGroupId = normalizeGroupId(targetGroupId);
1543
- if (!normalizedGroupId) {
1546
+ if (!targetGroupId) {
1544
1547
  throw new ValidationError('group_id is required for group notify');
1545
1548
  }
1546
1549
  const routeParams = {
1547
- group_aid: normalizedGroupId,
1548
- group_id: normalizedGroupId,
1550
+ group_id: targetGroupId,
1549
1551
  deliver: { method: eventMethod, params: payload },
1550
1552
  };
1553
+ const explicitGroupAid = String(options.group_aid ?? options.groupAid ?? '').trim();
1554
+ if (explicitGroupAid)
1555
+ routeParams.group_aid = explicitGroupAid;
1551
1556
  if (ttl !== undefined)
1552
1557
  routeParams.ttl_ms = ttl;
1553
1558
  await this._transport.notify('notification/group.route', routeParams);
@@ -1564,17 +1569,26 @@ export class AUNClient {
1564
1569
  }
1565
1570
  async _callRawV2Rpc(method, params) {
1566
1571
  const p = { ...(params ?? {}) };
1567
- const rpcBackground = Boolean(p._rpc_background) || this._backgroundRpcDepth > 0;
1572
+ const forceForeground = Boolean(p._rpc_foreground);
1573
+ const rpcBackground = !forceForeground
1574
+ && (Boolean(p._rpc_background) || this._backgroundRpcDepth > 0);
1575
+ delete p._rpc_foreground;
1568
1576
  delete p._rpc_background;
1569
1577
  delete p._pull_gate_locked;
1570
1578
  delete p._skip_auto_ack;
1571
1579
  delete p.skip_auto_ack;
1572
1580
  delete p._group_cursor_params;
1573
- if (method.startsWith('group.') && p.group_aid !== undefined && p.group_aid !== null) {
1574
- p.group_aid = normalizeGroupId(String(p.group_aid)) || String(p.group_aid);
1575
- }
1576
- else if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
1577
- p.group_aid = normalizeGroupId(String(p.group_id)) || String(p.group_id);
1581
+ if (method.startsWith('group.')) {
1582
+ const rawGroupAid = String(p.group_aid ?? p.groupAid ?? '').trim();
1583
+ const rawGroupId = String(p.group_id ?? p.groupId ?? '').trim();
1584
+ if (rawGroupId)
1585
+ p.group_id = rawGroupId;
1586
+ else if (rawGroupAid)
1587
+ p.group_id = rawGroupAid;
1588
+ if (rawGroupAid)
1589
+ p.group_aid = rawGroupAid;
1590
+ delete p.groupAid;
1591
+ delete p.groupId;
1578
1592
  }
1579
1593
  if (method.startsWith('group.') && p.device_id === undefined) {
1580
1594
  p.device_id = this._deviceId;
@@ -1689,19 +1703,19 @@ export class AUNClient {
1689
1703
  }
1690
1704
  _extractGroupIdFromResult(result) {
1691
1705
  const group = isJsonObject(result.group) ? result.group : null;
1692
- const gid = group ? String(group.group_aid ?? group.group_id ?? '') : '';
1706
+ const gid = group ? String(group.group_id ?? group.groupId ?? group.group_aid ?? group.groupAid ?? '') : '';
1693
1707
  if (gid)
1694
1708
  return gid;
1695
- const directGid = String(result.group_aid ?? result.group_id ?? '');
1709
+ const directGid = String(result.group_id ?? result.groupId ?? result.group_aid ?? result.groupAid ?? '');
1696
1710
  if (directGid)
1697
1711
  return directGid;
1698
1712
  const member = isJsonObject(result.member) ? result.member : null;
1699
- return member ? String(member.group_aid ?? member.group_id ?? '') : '';
1713
+ return member ? String(member.group_id ?? member.groupId ?? member.group_aid ?? member.groupAid ?? '') : '';
1700
1714
  }
1701
1715
  async _onRawGroupChanged(data) {
1702
1716
  const tStart = Date.now();
1703
1717
  const action = String(data?.action ?? '');
1704
- const groupIdInit = String(data?.group_aid ?? data?.group_id ?? '');
1718
+ const groupIdInit = String(data?.group_id ?? data?.group_aid ?? '');
1705
1719
  this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupIdInit} action=${action}`);
1706
1720
  try {
1707
1721
  if (isJsonObject(data)) {
@@ -1717,7 +1731,7 @@ export class AUNClient {
1717
1731
  d._verified = this._isEventSignatureVerified(verified);
1718
1732
  }
1719
1733
  }
1720
- const groupId = (d.group_aid ?? d.group_id ?? '');
1734
+ const groupId = (d.group_id ?? d.group_aid ?? '');
1721
1735
  await this._delivery.handleGroupChangedEventSeq(d, groupId);
1722
1736
  }
1723
1737
  else {
@@ -1989,8 +2003,20 @@ export class AUNClient {
1989
2003
  catch { /* 忽略关闭错误 */ }
1990
2004
  throw err;
1991
2005
  }
2006
+ if (!this._transport.isConnected()) {
2007
+ throw new ConnectionError('websocket disconnected during authentication');
2008
+ }
2009
+ this._serverKicked = false;
2010
+ this._lastDisconnectInfo = null;
1992
2011
  this._state = 'connected';
1993
2012
  this._connectedAt = Date.now();
2013
+ if (Array.isArray(params.gateways)) {
2014
+ this._gatewayCandidates = [...new Set(params.gateways
2015
+ .map((item) => String(item ?? '').trim()).filter(Boolean))];
2016
+ }
2017
+ else if (gatewayUrl) {
2018
+ this._gatewayCandidates = [gatewayUrl];
2019
+ }
1994
2020
  await this._dispatcher.publish('state_change', {
1995
2021
  state: this._publicState(this._state),
1996
2022
  gateway: gatewayUrl,
@@ -2023,26 +2049,43 @@ export class AUNClient {
2023
2049
  }
2024
2050
  }
2025
2051
  _resolveGateway(params) {
2052
+ const explicitGateway = String(params.gateway ?? '').trim();
2053
+ if (explicitGateway)
2054
+ return explicitGateway;
2026
2055
  const gateways = this._resolveGateways(params);
2027
2056
  return gateways[0];
2028
2057
  }
2029
2058
  async _resolveGatewayForAid(aid) {
2059
+ const gateways = await this._resolveGatewayCandidatesForAid(aid);
2060
+ return gateways[0];
2061
+ }
2062
+ async _resolveGatewayCandidatesForAid(aid) {
2030
2063
  const target = String(aid ?? this._aid ?? '').trim();
2031
2064
  if (!target)
2032
2065
  throw new StateError('gateway discovery requires a loaded AID');
2033
- if (this._gatewayUrl)
2034
- return this._gatewayUrl;
2066
+ if (this._gatewayCandidates.length > 0)
2067
+ return [...this._gatewayCandidates];
2068
+ if (this._gatewayUrl) {
2069
+ this._gatewayCandidates = [this._gatewayUrl];
2070
+ return [...this._gatewayCandidates];
2071
+ }
2035
2072
  try {
2036
2073
  const getMetadata = this._tokenStore.getMetadata;
2074
+ const rawList = typeof getMetadata === 'function'
2075
+ ? String(await getMetadata.call(this._tokenStore, target, 'gateway_urls') ?? '').trim()
2076
+ : '';
2077
+ const cachedList = rawList ? JSON.parse(rawList) : [];
2037
2078
  const raw = typeof getMetadata === 'function'
2038
2079
  ? String(await getMetadata.call(this._tokenStore, target, 'gateway_url') ?? '').trim()
2039
2080
  : '';
2040
- if (raw) {
2041
- const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
2042
- if (gateway) {
2043
- this._gatewayUrl = gateway;
2044
- return gateway;
2045
- }
2081
+ const candidates = Array.isArray(cachedList)
2082
+ ? cachedList.map((item) => String(item ?? '').trim()).filter(Boolean)
2083
+ : [];
2084
+ const gateway = candidates[0] ?? (raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw);
2085
+ if (gateway) {
2086
+ this._gatewayCandidates = [...new Set([gateway, ...candidates])];
2087
+ this._gatewayUrl = this._gatewayCandidates[0];
2088
+ return [...this._gatewayCandidates];
2046
2089
  }
2047
2090
  }
2048
2091
  catch {
@@ -2058,18 +2101,24 @@ export class AUNClient {
2058
2101
  let lastError = null;
2059
2102
  for (const url of candidates) {
2060
2103
  try {
2061
- const gateway = await this._discovery.discover(url);
2104
+ const discovered = await this._discovery.discoverAll(url);
2105
+ const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
2106
+ if (gatewayUrls.length === 0)
2107
+ throw new ValidationError('gateway discovery returned no candidates');
2108
+ const gateway = gatewayUrls[0];
2109
+ this._gatewayCandidates = gatewayUrls;
2062
2110
  this._gatewayUrl = gateway;
2063
2111
  try {
2064
2112
  const setMetadata = this._tokenStore.setMetadata;
2065
2113
  if (typeof setMetadata === 'function') {
2066
2114
  await setMetadata.call(this._tokenStore, target, 'gateway_url', gateway);
2115
+ await setMetadata.call(this._tokenStore, target, 'gateway_urls', JSON.stringify(gatewayUrls));
2067
2116
  }
2068
2117
  }
2069
2118
  catch {
2070
2119
  // 缓存写入失败不影响连接。
2071
2120
  }
2072
- return gateway;
2121
+ return [...gatewayUrls];
2073
2122
  }
2074
2123
  catch (err) {
2075
2124
  lastError = err;
@@ -2088,7 +2137,9 @@ export class AUNClient {
2088
2137
  throw new ValidationError('relay topology is not implemented in the Browser SDK');
2089
2138
  }
2090
2139
  }
2091
- const gw = params.gateway ?? params.gateways;
2140
+ const gw = Array.isArray(params.gateways) && params.gateways.length > 0
2141
+ ? params.gateways
2142
+ : params.gateway;
2092
2143
  if (Array.isArray(gw)) {
2093
2144
  const urls = gw.map((g) => String(g ?? '')).filter((u) => u.length > 0);
2094
2145
  if (urls.length > 0)
@@ -2118,7 +2169,13 @@ export class AUNClient {
2118
2169
  _normalizeConnectParams(params) {
2119
2170
  const request = { ...params };
2120
2171
  const accessToken = String(request.access_token ?? '');
2121
- const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
2172
+ const rawGateways = request.gateways;
2173
+ const normalizedGateways = Array.isArray(rawGateways)
2174
+ ? [...new Set(rawGateways.map((item) => String(item ?? '').trim()).filter(Boolean))]
2175
+ : [];
2176
+ if (normalizedGateways.length > 0)
2177
+ request.gateways = normalizedGateways;
2178
+ const gateway = String(request.gateway ?? normalizedGateways[0] ?? this._gatewayUrl ?? '');
2122
2179
  if (!gateway)
2123
2180
  throw new StateError('connect requires non-empty gateway');
2124
2181
  if (accessToken)
@@ -2190,9 +2247,6 @@ export class AUNClient {
2190
2247
  if ('auto_reconnect' in params) {
2191
2248
  options.auto_reconnect = Boolean(params.auto_reconnect);
2192
2249
  }
2193
- if ('heartbeat_interval' in params) {
2194
- options.heartbeat_interval = Number(params.heartbeat_interval);
2195
- }
2196
2250
  if ('token_refresh_before' in params) {
2197
2251
  options.token_refresh_before = Number(params.token_refresh_before);
2198
2252
  }
@@ -2208,10 +2262,9 @@ export class AUNClient {
2208
2262
  }
2209
2263
  // ── 内部:后台任务 ────────────────────────────────
2210
2264
  _startBackgroundTasks() {
2211
- // 短连接不启动 heartbeat token 刷新(生命周期短,不需要长期会话维护);
2212
- // auto_reconnect 仍允许,由 _sessionOptions.auto_reconnect 决定
2265
+ // WebSocket Ping/Pong Gateway 与传输层处理;SDK 不发送 RPC 心跳。
2266
+ // 短连接生命周期短,不需要 token 刷新。
2213
2267
  if (this._sessionOptions?.connection_kind !== 'short') {
2214
- this._startHeartbeat();
2215
2268
  this._startTokenRefresh();
2216
2269
  }
2217
2270
  // V2 内存缓存定时清理(每小时扫描过期条目)
@@ -2252,56 +2305,19 @@ export class AUNClient {
2252
2305
  }
2253
2306
  /** 心跳定时器 */
2254
2307
  _startHeartbeat() {
2255
- if (this._heartbeatTimer !== null)
2256
- return;
2257
- const interval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval ?? DEFAULT_SESSION_OPTIONS.heartbeat_interval);
2258
- if (interval <= 0)
2259
- return;
2260
- // M25: 把连续失败阈值从 3 次收窄到 2 次。既能容忍一次网络抖动/GC 暂停,
2261
- // 又把半开连接的检测延迟从 3 个心跳周期降到 2 个,避免 RPC 长时间挂起。
2262
- // 真正的 socket 死亡由 ws.on('close') 立即触发 _handleTransportDisconnect,
2263
- // 不依赖此心跳路径。
2264
- let consecutiveFailures = 0;
2265
- const maxFailures = 2;
2266
- this._heartbeatTimer = setInterval(async () => {
2267
- if (this.state !== ConnectionState.READY || this._closing)
2268
- return;
2269
- try {
2270
- const pong = await this._transport.call('meta.ping', {});
2271
- this._heartbeatCount++;
2272
- consecutiveFailures = 0;
2273
- // 服务端可在 pong 中下发新的 heartbeat_interval(秒,0=关闭)
2274
- if (isJsonObject(pong) && 'heartbeat_interval' in pong) {
2275
- this._applyServerHeartbeatInterval(pong.heartbeat_interval, 'pong');
2276
- }
2277
- }
2278
- catch (exc) {
2279
- consecutiveFailures++;
2280
- this._clientLog.warn(`heartbeat failed (${consecutiveFailures}/${maxFailures}): ${String(exc)}`);
2281
- this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
2282
- if (consecutiveFailures >= maxFailures) {
2283
- this._clientLog.warn(`consecutive ${maxFailures} heartbeat failed, trigger disconnect reconnect`);
2284
- this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
2285
- }
2286
- }
2287
- }, interval * 1000);
2288
- }
2289
- /** 服务端通过 hello/pong 下发 heartbeat_interval;clamp 后写入 session_options 并按需重启心跳。 */
2290
- _applyServerHeartbeatInterval(raw, source) {
2291
- const newInterval = clampHeartbeatInterval(raw);
2292
- const oldInterval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval);
2293
- if (newInterval === oldInterval)
2294
- return;
2295
- this._sessionOptions.heartbeat_interval = newInterval;
2296
- this._clientLog.debug(`heartbeat_interval updated by ${source}: ${oldInterval} -> ${newInterval}`);
2297
- // 重启定时器以应用新间隔(关闭/启动通过定时器有/无区分)
2298
2308
  if (this._heartbeatTimer !== null) {
2299
2309
  clearInterval(this._heartbeatTimer);
2300
2310
  this._heartbeatTimer = null;
2301
2311
  }
2302
- if (newInterval > 0 && this.state === ConnectionState.READY && !this._closing) {
2303
- this._startHeartbeat();
2312
+ this._sessionOptions.heartbeat_interval = 0;
2313
+ }
2314
+ /** 忽略旧 Gateway 的 RPC 心跳配置,固定使用 WebSocket Ping/Pong。 */
2315
+ _applyServerHeartbeatInterval(_raw, _source) {
2316
+ if (this._heartbeatTimer !== null) {
2317
+ clearInterval(this._heartbeatTimer);
2318
+ this._heartbeatTimer = null;
2304
2319
  }
2320
+ this._sessionOptions.heartbeat_interval = 0;
2305
2321
  }
2306
2322
  /** Token 刷新定时器 */
2307
2323
  _startTokenRefresh() {
@@ -2399,7 +2415,7 @@ export class AUNClient {
2399
2415
  }
2400
2416
  // ── 内部:断线重连 ────────────────────────────────
2401
2417
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
2402
- static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015]);
2418
+ static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
2403
2419
  /** 处理服务端主动断开通知 event/gateway.disconnect
2404
2420
  *
2405
2421
  * 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
@@ -2411,7 +2427,11 @@ export class AUNClient {
2411
2427
  const reason = obj.reason ?? '';
2412
2428
  const detail = (obj.detail && typeof obj.detail === 'object') ? obj.detail : {};
2413
2429
  this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
2414
- this._serverKicked = true;
2430
+ const reasonText = String(reason ?? '').trim().toLowerCase();
2431
+ const retryable = Number(code) === 4012
2432
+ || reasonText.includes('long_connection_already_exists')
2433
+ || reasonText.includes('long connection already exists');
2434
+ this._serverKicked = !retryable;
2415
2435
  // 缓存最近一次 disconnect 信息,让后续 connection.state(terminal_failed) 也能带 detail
2416
2436
  this._lastDisconnectInfo = { code, reason, detail };
2417
2437
  // 透传给应用层订阅者
@@ -2425,22 +2445,32 @@ export class AUNClient {
2425
2445
  async _handleTransportDisconnect(error, closeCode) {
2426
2446
  if (this._closing || this._state === 'closed')
2427
2447
  return;
2448
+ // 在首个 await 前占位,避免 close/error 与 token 刷新并发启动多条重连链。
2449
+ if (this._reconnectActive)
2450
+ return;
2451
+ this._reconnectActive = true;
2452
+ // 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
2453
+ this._delivery.resetInlineAckState();
2428
2454
  this._state = 'standby';
2429
2455
  // 先停止后台任务,避免心跳/token刷新在重连期间继续触发
2430
2456
  this._stopBackgroundTasks();
2431
- void this._transport.close().catch((exc) => {
2457
+ try {
2458
+ await this._transport.close();
2459
+ }
2460
+ catch (exc) {
2432
2461
  this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError(exc)}`);
2433
- });
2462
+ }
2434
2463
  await this._dispatcher.publish('state_change', {
2435
2464
  state: this._publicState(this._state),
2436
2465
  error,
2437
2466
  });
2438
- if (!this._sessionOptions.auto_reconnect)
2439
- return;
2440
- if (this._reconnectActive)
2467
+ if (!this._sessionOptions.auto_reconnect) {
2468
+ this._reconnectActive = false;
2441
2469
  return;
2470
+ }
2442
2471
  // 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
2443
- if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
2472
+ const retryableClose = closeCode === 4012;
2473
+ if ((this._serverKicked && !retryableClose) || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
2444
2474
  this._state = 'terminal_failed';
2445
2475
  const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
2446
2476
  this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
@@ -2457,16 +2487,39 @@ export class AUNClient {
2457
2487
  eventPayload.code = disconnectInfo.code;
2458
2488
  }
2459
2489
  await this._dispatcher.publish('state_change', eventPayload);
2490
+ this._reconnectActive = false;
2460
2491
  return;
2461
2492
  }
2462
2493
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
2463
2494
  const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
2464
- this._reconnectActive = true;
2465
- this._reconnectAbort = new AbortController();
2466
- this._safeAsync(this._reconnectLoop(serverInitiated));
2495
+ if (this._closing) {
2496
+ this._reconnectActive = false;
2497
+ return;
2498
+ }
2499
+ const reconnectAbort = new AbortController();
2500
+ this._reconnectAbort = reconnectAbort;
2501
+ this._safeAsync(this._reconnectLoop(serverInitiated, reconnectAbort));
2502
+ }
2503
+ /** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
2504
+ async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort) {
2505
+ let releasedOwnGeneration = false;
2506
+ try {
2507
+ await this._runReconnectLoop(serverInitiated, reconnectAbort);
2508
+ }
2509
+ finally {
2510
+ if (this._reconnectAbort === reconnectAbort) {
2511
+ this._nextRetryAt = null;
2512
+ this._reconnectActive = false;
2513
+ this._reconnectAbort = null;
2514
+ releasedOwnGeneration = true;
2515
+ }
2516
+ }
2517
+ if (releasedOwnGeneration && !this._closing && this._state === 'connected' && !this._transport.isConnected()) {
2518
+ await this._handleTransportDisconnect(new ConnectionError('websocket disconnected while reconnect completed'));
2519
+ }
2467
2520
  }
2468
- /** 指数退避 + 固定上限抖动重连循环(默认无限重试,仅在不可重试错误或 close() 或 max_attempts 耗尽时终止) */
2469
- async _reconnectLoop(serverInitiated = false) {
2521
+ /** 指数退避 + 当前基数抖动重连循环主体。 */
2522
+ async _runReconnectLoop(serverInitiated, reconnectAbort) {
2470
2523
  const retry = { ...this._sessionOptions.retry };
2471
2524
  const maxBaseDelay = clampReconnectDelaySeconds(retry.max_delay, RECONNECT_MAX_BASE_DELAY_SECONDS);
2472
2525
  // M25: max_attempts=0 表示无限重试(与 Go/Python 对齐)
@@ -2476,13 +2529,11 @@ export class AUNClient {
2476
2529
  let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
2477
2530
  this._retryAttempt = 0;
2478
2531
  this._retryMaxAttempts = maxAttempts;
2479
- for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
2532
+ for (let attempt = 1; !reconnectAbort?.signal.aborted; attempt++) {
2480
2533
  // R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
2481
2534
  if (maxAttempts > 0 && attempt > maxAttempts) {
2482
2535
  this._state = 'terminal_failed';
2483
2536
  this._nextRetryAt = null;
2484
- this._reconnectActive = false;
2485
- this._reconnectAbort = null;
2486
2537
  await this._dispatcher.publish('state_change', {
2487
2538
  state: this._publicState(this._state),
2488
2539
  attempt: attempt - 1,
@@ -2501,27 +2552,26 @@ export class AUNClient {
2501
2552
  next_retry_at: this._nextRetryAt.getTime() / 1000,
2502
2553
  });
2503
2554
  try {
2504
- await this._sleep(sleepMs);
2555
+ await this._sleep(sleepMs, reconnectAbort?.signal);
2505
2556
  this._nextRetryAt = null;
2506
- if (this._reconnectAbort?.signal.aborted) {
2507
- this._reconnectActive = false;
2557
+ if (reconnectAbort?.signal.aborted)
2508
2558
  return;
2509
- }
2510
2559
  // 退避结束,进入 reconnecting 状态
2511
2560
  this._state = 'reconnecting';
2512
2561
  await this._dispatcher.publish('state_change', {
2513
2562
  state: this._publicState(this._state),
2514
2563
  attempt,
2515
2564
  });
2516
- // 重连前先 GET /health 探测,不健康则跳过本轮
2565
+ // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
2517
2566
  if (this._gatewayUrl) {
2518
- const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
2519
- if (!healthy) {
2520
- this._lastError = new Error('gateway health check failed');
2521
- this._lastErrorCode = 'gateway_unhealthy';
2522
- delay = Math.min(delay * 2, maxBaseDelay);
2523
- continue;
2524
- }
2567
+ void this._discovery.checkHealth(this._gatewayUrl, 5000)
2568
+ .then((healthy) => {
2569
+ if (!healthy)
2570
+ this._clientLog.warn(`reconnect health advisory failed: gateway=${this._gatewayUrl}`);
2571
+ })
2572
+ .catch((exc) => {
2573
+ this._clientLog.debug(`reconnect health advisory error: ${exc instanceof Error ? exc.message : String(exc)}`);
2574
+ });
2525
2575
  }
2526
2576
  await this._transport.close();
2527
2577
  if (!this._sessionParams) {
@@ -2545,12 +2595,35 @@ export class AUNClient {
2545
2595
  this._sessionParams.access_token = '';
2546
2596
  }
2547
2597
  }
2548
- await this._connectOnce(this._sessionParams, true);
2598
+ const candidates = this._orderedReconnectGateways(this._sessionParams);
2599
+ let lastGatewayError = null;
2600
+ let connected = false;
2601
+ for (const gateway of candidates) {
2602
+ const attemptParams = { ...this._sessionParams, gateway };
2603
+ try {
2604
+ await this._connectOnce(attemptParams, true);
2605
+ if (!this._transport.isConnected()) {
2606
+ throw new ConnectionError('websocket disconnected while reconnecting');
2607
+ }
2608
+ this._sessionParams.gateway = gateway;
2609
+ connected = true;
2610
+ break;
2611
+ }
2612
+ catch (exc) {
2613
+ lastGatewayError = exc;
2614
+ await this._transport.close().catch(() => { });
2615
+ if (candidates.length > 1) {
2616
+ this._clientLog.warn(`reconnect gateway failed, trying next: gateway=${gateway}, error=${exc instanceof Error ? exc.message : String(exc)}`);
2617
+ }
2618
+ if (!this._shouldRetryReconnect(exc))
2619
+ throw exc;
2620
+ }
2621
+ }
2622
+ if (!connected)
2623
+ throw (lastGatewayError instanceof Error ? lastGatewayError : new ConnectionError('all gateway candidates failed'));
2549
2624
  this._lastError = null;
2550
2625
  this._lastErrorCode = null;
2551
2626
  this._nextRetryAt = null;
2552
- this._reconnectActive = false;
2553
- this._reconnectAbort = null;
2554
2627
  return;
2555
2628
  }
2556
2629
  catch (exc) {
@@ -2563,8 +2636,6 @@ export class AUNClient {
2563
2636
  if (!this._shouldRetryReconnect(exc)) {
2564
2637
  this._state = 'terminal_failed';
2565
2638
  this._nextRetryAt = null;
2566
- this._reconnectActive = false;
2567
- this._reconnectAbort = null;
2568
2639
  await this._dispatcher.publish('state_change', {
2569
2640
  state: this._publicState(this._state),
2570
2641
  error: formatCaughtError(exc),
@@ -2575,14 +2646,15 @@ export class AUNClient {
2575
2646
  delay = Math.min(delay * 2, maxBaseDelay);
2576
2647
  }
2577
2648
  }
2578
- this._reconnectActive = false;
2579
- this._reconnectAbort = null;
2580
2649
  }
2581
2650
  /** 判断是否应重试重连 */
2582
2651
  _shouldRetryReconnect(error) {
2583
2652
  if (error instanceof AuthError) {
2584
2653
  const message = String(error.message ?? '').toLowerCase();
2585
- if (message.includes('aid_login1_failed') || message.includes('aid_login2_failed')) {
2654
+ if (message.includes('long_connection_already_exists')
2655
+ || message.includes('long connection already exists')
2656
+ || message.includes('aid_login1_failed')
2657
+ || message.includes('aid_login2_failed')) {
2586
2658
  return true;
2587
2659
  }
2588
2660
  return false;
@@ -2615,6 +2687,7 @@ export class AUNClient {
2615
2687
  this._seqTrackerContext = null;
2616
2688
  this._gapFillDone.clear();
2617
2689
  this._pushedSeqs.clear();
2690
+ this._pullResponseKeys.clear();
2618
2691
  this._pendingOrderedMsgs.clear();
2619
2692
  this._v2SenderIKPending.clear();
2620
2693
  this._v2SenderIKFetching.clear();
@@ -2632,8 +2705,10 @@ export class AUNClient {
2632
2705
  return;
2633
2706
  void this._saveSeqTrackerState();
2634
2707
  this._seqTracker = new SeqTracker();
2708
+ this._delivery.resetInlineAckState();
2635
2709
  this._gapFillDone.clear();
2636
2710
  this._pushedSeqs.clear();
2711
+ this._pullResponseKeys.clear();
2637
2712
  this._pendingOrderedMsgs.clear();
2638
2713
  this._v2SenderIKPending.clear();
2639
2714
  this._v2SenderIKFetching.clear();
@@ -2653,6 +2728,9 @@ export class AUNClient {
2653
2728
  _persistSeq(ns, forceSeq) {
2654
2729
  return this._delivery.persistSeq(ns, forceSeq);
2655
2730
  }
2731
+ async _commitSeqTrackerState(ns) {
2732
+ await this._delivery.commitSeqTrackerState(ns);
2733
+ }
2656
2734
  _persistRepairedSeq(ns) {
2657
2735
  return this._delivery.persistRepairedSeq(ns);
2658
2736
  }
@@ -2805,6 +2883,9 @@ export class AUNClient {
2805
2883
  async _getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint) {
2806
2884
  return await this._v2E2EE.getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint);
2807
2885
  }
2886
+ async _fetchV2SenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint) {
2887
+ return await this._v2E2EE.fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint);
2888
+ }
2808
2889
  _cacheV2PeerIKFromDevice(dev, fallbackAid = '') {
2809
2890
  const session = this._v2Session;
2810
2891
  if (!session || !isJsonObject(dev))
@@ -2850,11 +2931,17 @@ export class AUNClient {
2850
2931
  *
2851
2932
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
2852
2933
  */
2853
- async _ackV2(upToSeq) {
2854
- return await this._v2E2EE.ackV2(upToSeq);
2934
+ async _ackV2(upToSeq, inlineGeneration) {
2935
+ return await this._v2E2EE.ackV2(upToSeq, inlineGeneration);
2855
2936
  }
2856
- /** 解密单条 V2 消息(与 Python `_decrypt_v2_message` 对齐)。缺 sender IK 时先入 pending,后台补齐后重试。 */
2857
- async _decryptV2Message(msg, allowPending = true) {
2937
+ /**
2938
+ * 解密单条 V2 消息(与 Python `_decrypt_v2_message` 对齐)。
2939
+ *
2940
+ * 默认保持 Pull / 普通 Push 的既有行为:缺 sender IK 时入 pending、失败发布
2941
+ * undecryptable 事件、成功消费后触发 SPK 轮换。inline Push 在游标提交和
2942
+ * Head/正文绑定校验前调用本入口时必须显式关闭这些副作用。
2943
+ */
2944
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
2858
2945
  const session = this._v2Session;
2859
2946
  if (!session)
2860
2947
  return null;
@@ -2870,7 +2957,8 @@ export class AUNClient {
2870
2957
  return null;
2871
2958
  }
2872
2959
  const e2eeMeta = v2E2eeMeta(envelope);
2873
- await this._observeAgentMdFromEnvelope(envelope);
2960
+ if (observeAgentMd)
2961
+ await this._observeAgentMdFromEnvelope(envelope);
2874
2962
  // 确定 spk_id 和 recipient_key_source
2875
2963
  let spkId = '';
2876
2964
  let recipientKeySource = '';
@@ -2920,55 +3008,62 @@ export class AUNClient {
2920
3008
  }
2921
3009
  catch (exc) {
2922
3010
  this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${String(exc)}`);
2923
- try {
2924
- const event = {
2925
- message_id: String(msg.message_id ?? ''),
2926
- from: String(msg.from_aid ?? ''),
2927
- to: String(msg.to ?? ''),
2928
- seq: msg.seq,
2929
- timestamp: (msg.t_server ?? msg.timestamp),
2930
- device_id: String(msg.device_id ?? ''),
2931
- slot_id: String(msg.slot_id ?? ''),
2932
- _decrypt_error: String(exc),
2933
- _decrypt_stage: 'spk_lookup',
2934
- _envelope_type: String(envelope.type ?? ''),
2935
- _suite: String(envelope.suite ?? ''),
2936
- _spk_id: spkId,
2937
- };
2938
- attachV2EnvelopeMetadata(event, e2eeMeta);
2939
- await this._dispatcher.publish(undecryptableEvent, event);
3011
+ if (emitUndecryptable) {
3012
+ try {
3013
+ const event = {
3014
+ message_id: String(msg.message_id ?? ''),
3015
+ from: String(msg.from_aid ?? ''),
3016
+ to: String(msg.to ?? ''),
3017
+ seq: msg.seq,
3018
+ timestamp: (msg.t_server ?? msg.timestamp),
3019
+ device_id: String(msg.device_id ?? ''),
3020
+ slot_id: String(msg.slot_id ?? ''),
3021
+ _decrypt_error: String(exc),
3022
+ _decrypt_stage: 'spk_lookup',
3023
+ _envelope_type: String(envelope.type ?? ''),
3024
+ _suite: String(envelope.suite ?? ''),
3025
+ _spk_id: spkId,
3026
+ };
3027
+ attachV2EnvelopeMetadata(event, e2eeMeta);
3028
+ await this._dispatcher.publish(undecryptableEvent, event);
3029
+ }
3030
+ catch { /* publish 异常不影响主流程 */ }
2940
3031
  }
2941
- catch { /* publish 异常不影响主流程 */ }
2942
3032
  return null;
2943
3033
  }
2944
3034
  const fromAid = String(msg.from_aid ?? '');
2945
3035
  const senderDeviceId = String(aad.from_device ?? '');
2946
3036
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
2947
- const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
3037
+ let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2948
3038
  if (!senderPubDer) {
2949
3039
  this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
3040
+ senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
3041
+ }
3042
+ if (!senderPubDer) {
2950
3043
  if (allowPending) {
2951
3044
  this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
2952
3045
  return null;
2953
3046
  }
2954
- try {
2955
- const event = {
2956
- message_id: String(msg.message_id ?? ''),
2957
- from: fromAid,
2958
- to: String(msg.to ?? ''),
2959
- seq: msg.seq,
2960
- timestamp: (msg.t_server ?? msg.timestamp),
2961
- device_id: String(msg.device_id ?? ''),
2962
- slot_id: String(msg.slot_id ?? ''),
2963
- _decrypt_error: 'sender_ik_not_found',
2964
- _decrypt_stage: 'sender_ik',
2965
- _envelope_type: String(envelope.type ?? ''),
2966
- _suite: String(envelope.suite ?? ''),
2967
- };
2968
- attachV2EnvelopeMetadata(event, e2eeMeta);
2969
- await this._dispatcher.publish(undecryptableEvent, event);
3047
+ if (emitUndecryptable) {
3048
+ try {
3049
+ const event = {
3050
+ message_id: String(msg.message_id ?? ''),
3051
+ from: fromAid,
3052
+ to: String(msg.to ?? ''),
3053
+ seq: msg.seq,
3054
+ timestamp: (msg.t_server ?? msg.timestamp),
3055
+ device_id: String(msg.device_id ?? ''),
3056
+ slot_id: String(msg.slot_id ?? ''),
3057
+ _decrypt_error: 'sender_ik_not_found',
3058
+ _decrypt_stage: 'sender_ik',
3059
+ _envelope_type: String(envelope.type ?? ''),
3060
+ _suite: String(envelope.suite ?? ''),
3061
+ };
3062
+ attachV2EnvelopeMetadata(event, e2eeMeta);
3063
+ await this._dispatcher.publish(undecryptableEvent, event);
3064
+ }
3065
+ catch { /* publish 异常不影响主流程 */ }
2970
3066
  }
2971
- catch { /* publish 异常不影响主流程 */ }
2972
3067
  return null;
2973
3068
  }
2974
3069
  let plaintext;
@@ -2977,43 +3072,35 @@ export class AUNClient {
2977
3072
  }
2978
3073
  catch (exc) {
2979
3074
  this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${String(exc)}`);
2980
- try {
2981
- const event = {
2982
- message_id: String(msg.message_id ?? ''),
2983
- from: fromAid,
2984
- to: String(msg.to ?? ''),
2985
- seq: msg.seq,
2986
- timestamp: (msg.t_server ?? msg.timestamp),
2987
- device_id: String(msg.device_id ?? ''),
2988
- slot_id: String(msg.slot_id ?? ''),
2989
- _decrypt_error: String(exc),
2990
- _decrypt_stage: 'decrypt',
2991
- _envelope_type: String(envelope.type ?? ''),
2992
- _suite: String(envelope.suite ?? ''),
2993
- };
2994
- attachV2EnvelopeMetadata(event, e2eeMeta);
2995
- await this._dispatcher.publish(undecryptableEvent, event);
3075
+ if (emitUndecryptable) {
3076
+ try {
3077
+ const event = {
3078
+ message_id: String(msg.message_id ?? ''),
3079
+ from: fromAid,
3080
+ to: String(msg.to ?? ''),
3081
+ seq: msg.seq,
3082
+ timestamp: (msg.t_server ?? msg.timestamp),
3083
+ device_id: String(msg.device_id ?? ''),
3084
+ slot_id: String(msg.slot_id ?? ''),
3085
+ _decrypt_error: String(exc),
3086
+ _decrypt_stage: 'decrypt',
3087
+ _envelope_type: String(envelope.type ?? ''),
3088
+ _suite: String(envelope.suite ?? ''),
3089
+ };
3090
+ attachV2EnvelopeMetadata(event, e2eeMeta);
3091
+ await this._dispatcher.publish(undecryptableEvent, event);
3092
+ }
3093
+ catch { /* publish 异常不影响主流程 */ }
2996
3094
  }
2997
- catch { /* publish 异常不影响主流程 */ }
2998
3095
  return null;
2999
3096
  }
3000
3097
  if (plaintext == null)
3001
3098
  return null;
3002
3099
  // 消费触发 SPK 轮换(fire-and-forget,不阻塞消息处理)
3003
- if (groupIdForKeys && recipientKeySource === 'group_device_prekey' && session.isLastUploadedGroupSPK(groupIdForKeys, spkId)) {
3004
- this._v2E2EE.scheduleGroupSpkRotation(groupIdForKeys, { reason: 'group_spk_consumed' });
3005
- }
3006
- else if (groupIdForKeys && recipientKeySource === 'peer_device_prekey') {
3007
- this._v2E2EE.scheduleGroupSpkRegistrationAfterPeerFallback(groupIdForKeys);
3008
- }
3009
- else if (!groupIdForKeys && session.isLastUploadedSPK(spkId)) {
3010
- const callFn = async (method, params) => this.call(method, params);
3011
- session.rotateSPK(callFn).catch(exc => {
3012
- this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${exc}`);
3013
- });
3014
- }
3100
+ if (rotateKeys)
3101
+ this._scheduleV2KeyRotationForMessage(msg, envelope);
3015
3102
  const e2ee = v2E2eeMeta(envelope);
3016
- const result = {
3103
+ let result = {
3017
3104
  message_id: String(msg.message_id ?? ''),
3018
3105
  from: fromAid,
3019
3106
  to: this._aid ?? '',
@@ -3031,8 +3118,67 @@ export class AUNClient {
3031
3118
  result.slot_id = msg.slot_id;
3032
3119
  attachGatewayProximity(result, msg);
3033
3120
  attachV2EnvelopeMetadata(result, e2ee);
3121
+ if (groupIdForKeys)
3122
+ result = normalizeGroupMentionMode(result, msg, envelope);
3034
3123
  return result;
3035
3124
  }
3125
+ _scheduleV2KeyRotationForMessage(msg, envelope) {
3126
+ const session = this._v2Session;
3127
+ if (!session)
3128
+ return;
3129
+ let spkId = '';
3130
+ let recipientKeySource = '';
3131
+ const recipientObj = envelope.recipient;
3132
+ if (recipientObj && typeof recipientObj === 'object') {
3133
+ spkId = String(recipientObj.spk_id ?? '');
3134
+ recipientKeySource = String(recipientObj.key_source ?? '');
3135
+ }
3136
+ else if (Array.isArray(envelope.recipients)) {
3137
+ spkId = String(msg.spk_id ?? '');
3138
+ for (const row of envelope.recipients) {
3139
+ if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
3140
+ if (!spkId)
3141
+ spkId = String(row[5] ?? '');
3142
+ recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
3143
+ break;
3144
+ }
3145
+ }
3146
+ }
3147
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
3148
+ const groupId = String(msg.group_id ?? aad.group_id ?? envelope.group_id ?? '').trim();
3149
+ if (groupId && recipientKeySource === 'group_device_prekey' && session.isLastUploadedGroupSPK(groupId, spkId)) {
3150
+ this._v2E2EE.scheduleGroupSpkRotation(groupId, { reason: 'group_spk_consumed' });
3151
+ }
3152
+ else if (groupId && recipientKeySource === 'peer_device_prekey') {
3153
+ this._v2E2EE.scheduleGroupSpkRegistrationAfterPeerFallback(groupId);
3154
+ }
3155
+ else if (!groupId && session.isLastUploadedSPK(spkId)) {
3156
+ const callFn = async (method, params) => this.call(method, params);
3157
+ session.rotateSPK(callFn).catch(exc => {
3158
+ this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${exc}`);
3159
+ });
3160
+ }
3161
+ }
3162
+ /** inline 消息仅在应用提交且 ACK 成功后补做可变副作用。 */
3163
+ async _postInlineV2CommitAndAck(msg) {
3164
+ const envJson = msg.envelope_json;
3165
+ if (!envJson || typeof envJson !== 'string')
3166
+ return;
3167
+ let envelope;
3168
+ try {
3169
+ envelope = JSON.parse(envJson);
3170
+ }
3171
+ catch {
3172
+ return;
3173
+ }
3174
+ try {
3175
+ await this._observeAgentMdFromEnvelope(envelope);
3176
+ }
3177
+ catch (exc) {
3178
+ this._clientLog.debug(`inline V2 agent.md observe failed (non-fatal): ${String(exc)}`);
3179
+ }
3180
+ this._scheduleV2KeyRotationForMessage(msg, envelope);
3181
+ }
3036
3182
  /**
3037
3183
  * V2 Group 加密发送(推测性:用缓存 bootstrap 直接发,失败刷新重试一次)。
3038
3184
  *
@@ -3067,8 +3213,8 @@ export class AUNClient {
3067
3213
  * @param groupId 群 ID
3068
3214
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
3069
3215
  */
3070
- async _ackGroupV2(groupId, upToSeq) {
3071
- return await this._v2E2EE.ackGroupV2(groupId, upToSeq);
3216
+ async _ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
3217
+ return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration);
3072
3218
  }
3073
3219
  /**
3074
3220
  * V2 P2P 多设备 wrap envelope 构造(不发送)。
@@ -3197,13 +3343,56 @@ export class AUNClient {
3197
3343
  this._backgroundRpcDepth = Math.max(0, this._backgroundRpcDepth - 1);
3198
3344
  }
3199
3345
  }
3200
- async _runPullSerialized(key, operation) {
3201
- return await this._rpcPipeline.runPullSerialized(key, operation);
3346
+ async _runPullSerialized(key, operation, background = false) {
3347
+ return await this._rpcPipeline.runPullSerialized(key, operation, background);
3202
3348
  }
3203
- /** 可取消的 sleep */
3204
- _sleep(ms) {
3349
+ async _withPullResponseProcessing(ns, operation) {
3350
+ if (!ns)
3351
+ return await operation();
3352
+ this._pullResponseKeys.set(ns, (this._pullResponseKeys.get(ns) ?? 0) + 1);
3353
+ try {
3354
+ return await operation();
3355
+ }
3356
+ finally {
3357
+ const remaining = (this._pullResponseKeys.get(ns) ?? 1) - 1;
3358
+ if (remaining <= 0)
3359
+ this._pullResponseKeys.delete(ns);
3360
+ else
3361
+ this._pullResponseKeys.set(ns, remaining);
3362
+ }
3363
+ }
3364
+ /** 每轮重连从当前地址之后开始轮转,避免永远黏在失效的首个 Gateway。 */
3365
+ _orderedReconnectGateways(params) {
3366
+ const raw = Array.isArray(params.gateways) && params.gateways.length > 0
3367
+ ? params.gateways
3368
+ : (this._gatewayCandidates.length > 0 ? this._gatewayCandidates : [params.gateway ?? this._gatewayUrl]);
3369
+ const candidates = [...new Set(raw.map((item) => String(item ?? '').trim()).filter(Boolean))];
3370
+ if (candidates.length <= 1)
3371
+ return candidates;
3372
+ const current = String(this._gatewayUrl ?? params.gateway ?? '').trim();
3373
+ const index = candidates.indexOf(current);
3374
+ const start = index >= 0 ? (index + 1) % candidates.length : 0;
3375
+ return [...candidates.slice(start), ...candidates.slice(0, start)];
3376
+ }
3377
+ /** 可取消的 sleep;重连终止时立即释放退避等待。 */
3378
+ _sleep(ms, signal) {
3205
3379
  return new Promise((resolve) => {
3206
- globalThis.setTimeout(resolve, ms);
3380
+ let settled = false;
3381
+ const finish = () => {
3382
+ if (settled)
3383
+ return;
3384
+ settled = true;
3385
+ globalThis.clearTimeout(timer);
3386
+ signal?.removeEventListener('abort', finish);
3387
+ resolve();
3388
+ };
3389
+ const timer = globalThis.setTimeout(finish, ms);
3390
+ if (signal) {
3391
+ if (signal.aborted)
3392
+ finish();
3393
+ else
3394
+ signal.addEventListener('abort', finish, { once: true });
3395
+ }
3207
3396
  });
3208
3397
  }
3209
3398
  }