@agentunion/fastaun-browser 0.5.4 → 0.5.8

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 (184) hide show
  1. package/CHANGELOG.md +177 -67
  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 +177 -67
  6. package/_packed_docs/INDEX.md +124 -65
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +65 -30
  8. package/_packed_docs/agent.md/SCHEMA.md +83 -58
  9. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
  10. package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
  11. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
  12. 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
  13. 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 +234 -0
  14. 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 +1095 -0
  15. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1199 -0
  16. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
  17. package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
  18. package/_packed_docs/message-online-push-alignment.md +572 -0
  19. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
  20. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
  21. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
  22. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
  23. 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
  24. package/_packed_docs/protocol/README.md +1 -0
  25. package/_packed_docs/protocol/aun-docs-guide.md +7 -4
  26. package/_packed_docs/protocol/index.md +25 -19
  27. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +63 -18
  28. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
  29. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +93 -84
  30. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
  31. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +73 -34
  32. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
  33. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +21 -9
  34. package/_packed_docs/sdk/09-group-rpc-manual.md +319 -285
  35. package/_packed_docs/sdk/09-message-rpc-manual.md +144 -103
  36. package/_packed_docs/sdk/09-payload-reference.md +1 -1
  37. package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
  38. package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
  39. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +34 -23
  40. package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
  41. package/_packed_docs/sdk/INDEX.md +45 -37
  42. package/_packed_docs/sdk/README.md +11 -9
  43. package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
  44. 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
  45. 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
  46. package/dist/agent-md-schema.d.ts +14 -0
  47. package/dist/agent-md-schema.d.ts.map +1 -0
  48. package/dist/agent-md-schema.js +103 -0
  49. package/dist/agent-md-schema.js.map +1 -0
  50. package/dist/agent-md.d.ts +11 -5
  51. package/dist/agent-md.d.ts.map +1 -1
  52. package/dist/agent-md.js +295 -53
  53. package/dist/agent-md.js.map +1 -1
  54. package/dist/aid-store.d.ts +7 -7
  55. package/dist/aid-store.d.ts.map +1 -1
  56. package/dist/aid-store.js +46 -18
  57. package/dist/aid-store.js.map +1 -1
  58. package/dist/aid.d.ts +2 -0
  59. package/dist/aid.d.ts.map +1 -1
  60. package/dist/aid.js +29 -7
  61. package/dist/aid.js.map +1 -1
  62. package/dist/auth.d.ts.map +1 -1
  63. package/dist/auth.js +31 -11
  64. package/dist/auth.js.map +1 -1
  65. package/dist/bundle.js +29920 -19719
  66. package/dist/cert-utils.d.ts +1 -0
  67. package/dist/cert-utils.d.ts.map +1 -1
  68. package/dist/cert-utils.js +36 -16
  69. package/dist/cert-utils.js.map +1 -1
  70. package/dist/client/delivery.d.ts +106 -11
  71. package/dist/client/delivery.d.ts.map +1 -1
  72. package/dist/client/delivery.js +1925 -374
  73. package/dist/client/delivery.js.map +1 -1
  74. package/dist/client/group-state.d.ts.map +1 -1
  75. package/dist/client/group-state.js +27 -24
  76. package/dist/client/group-state.js.map +1 -1
  77. package/dist/client/lifecycle.d.ts +15 -1
  78. package/dist/client/lifecycle.d.ts.map +1 -1
  79. package/dist/client/lifecycle.js +477 -132
  80. package/dist/client/lifecycle.js.map +1 -1
  81. package/dist/client/mention-mode.d.ts +7 -0
  82. package/dist/client/mention-mode.d.ts.map +1 -0
  83. package/dist/client/mention-mode.js +184 -0
  84. package/dist/client/mention-mode.js.map +1 -0
  85. package/dist/client/peers.d.ts +1 -1
  86. package/dist/client/peers.d.ts.map +1 -1
  87. package/dist/client/peers.js +26 -3
  88. package/dist/client/peers.js.map +1 -1
  89. package/dist/client/rpc-pipeline.d.ts +34 -2
  90. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  91. package/dist/client/rpc-pipeline.js +503 -99
  92. package/dist/client/rpc-pipeline.js.map +1 -1
  93. package/dist/client/runtime.d.ts +1 -3
  94. package/dist/client/runtime.d.ts.map +1 -1
  95. package/dist/client/runtime.js +4 -7
  96. package/dist/client/runtime.js.map +1 -1
  97. package/dist/client/v2-e2ee.d.ts +43 -2
  98. package/dist/client/v2-e2ee.d.ts.map +1 -1
  99. package/dist/client/v2-e2ee.js +931 -138
  100. package/dist/client/v2-e2ee.js.map +1 -1
  101. package/dist/client.d.ts +46 -15
  102. package/dist/client.d.ts.map +1 -1
  103. package/dist/client.js +785 -304
  104. package/dist/client.js.map +1 -1
  105. package/dist/errors.d.ts.map +1 -1
  106. package/dist/errors.js +4 -1
  107. package/dist/errors.js.map +1 -1
  108. package/dist/facades.d.ts +26 -1
  109. package/dist/facades.d.ts.map +1 -1
  110. package/dist/facades.js +142 -60
  111. package/dist/facades.js.map +1 -1
  112. package/dist/group-id.d.ts.map +1 -1
  113. package/dist/group-id.js +2 -17
  114. package/dist/group-id.js.map +1 -1
  115. package/dist/group-index.d.ts +6 -1
  116. package/dist/group-index.d.ts.map +1 -1
  117. package/dist/group-index.js +44 -25
  118. package/dist/group-index.js.map +1 -1
  119. package/dist/index.d.ts +5 -4
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +3 -2
  122. package/dist/index.js.map +1 -1
  123. package/dist/keystore/index.d.ts +10 -5
  124. package/dist/keystore/index.d.ts.map +1 -1
  125. package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
  126. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  127. package/dist/keystore/indexeddb-identity-store.js +0 -60
  128. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  129. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  130. package/dist/keystore/indexeddb-shared.js +9 -5
  131. package/dist/keystore/indexeddb-shared.js.map +1 -1
  132. package/dist/keystore/indexeddb-token-store.d.ts +3 -1
  133. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  134. package/dist/keystore/indexeddb-token-store.js +47 -2
  135. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  136. package/dist/register-flow.d.ts.map +1 -1
  137. package/dist/register-flow.js +28 -3
  138. package/dist/register-flow.js.map +1 -1
  139. package/dist/seq-tracker.d.ts +28 -8
  140. package/dist/seq-tracker.d.ts.map +1 -1
  141. package/dist/seq-tracker.js +224 -61
  142. package/dist/seq-tracker.js.map +1 -1
  143. package/dist/storage/vfs.d.ts +1 -0
  144. package/dist/storage/vfs.d.ts.map +1 -1
  145. package/dist/storage/vfs.js +26 -2
  146. package/dist/storage/vfs.js.map +1 -1
  147. package/dist/tools/cross-sdk-agent.js +399 -11
  148. package/dist/tools/cross-sdk-agent.js.map +1 -1
  149. package/dist/transport.d.ts +12 -0
  150. package/dist/transport.d.ts.map +1 -1
  151. package/dist/transport.js +226 -102
  152. package/dist/transport.js.map +1 -1
  153. package/dist/v2/session/session.d.ts.map +1 -1
  154. package/dist/v2/session/session.js +3 -4
  155. package/dist/v2/session/session.js.map +1 -1
  156. package/dist/v2/state/commitment.d.ts.map +1 -1
  157. package/dist/v2/state/commitment.js +1 -2
  158. package/dist/v2/state/commitment.js.map +1 -1
  159. package/dist/validators.d.ts.map +1 -1
  160. package/dist/validators.js +8 -14
  161. package/dist/validators.js.map +1 -1
  162. package/dist/version.d.ts +1 -1
  163. package/dist/version.js +1 -1
  164. package/package.json +11 -9
  165. package/dist/group-resources.d.ts +0 -98
  166. package/dist/group-resources.d.ts.map +0 -1
  167. package/dist/group-resources.js +0 -635
  168. package/dist/group-resources.js.map +0 -1
  169. package/dist/keystore/indexeddb.d.ts +0 -179
  170. package/dist/keystore/indexeddb.d.ts.map +0 -1
  171. package/dist/keystore/indexeddb.js +0 -2031
  172. package/dist/keystore/indexeddb.js.map +0 -1
  173. package/dist/namespaces/auth.d.ts +0 -98
  174. package/dist/namespaces/auth.d.ts.map +0 -1
  175. package/dist/namespaces/auth.js +0 -992
  176. package/dist/namespaces/auth.js.map +0 -1
  177. package/dist/namespaces/custody.d.ts +0 -51
  178. package/dist/namespaces/custody.d.ts.map +0 -1
  179. package/dist/namespaces/custody.js +0 -302
  180. package/dist/namespaces/custody.js.map +0 -1
  181. package/dist/namespaces/meta.d.ts +0 -109
  182. package/dist/namespaces/meta.d.ts.map +0 -1
  183. package/dist/namespaces/meta.js +0 -549
  184. package/dist/namespaces/meta.js.map +0 -1
package/dist/client.js CHANGED
@@ -9,17 +9,17 @@ import { EventDispatcher } from './events.js';
9
9
  import { normalizeGroupId } from './group-id.js';
10
10
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
11
11
  import { GatewayDiscovery } from './discovery.js';
12
- import { AIDStore } from './aid-store.js';
13
12
  import { RPCTransport } from './transport.js';
14
13
  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';
21
21
  import { RpcPipeline } from './client/rpc-pipeline.js';
22
- import { GroupFacade, MessageFacade, StreamFacade } from './facades.js';
22
+ import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
23
23
  import { StorageVFS } from './storage/vfs.js';
24
24
  import { CollabClient } from './collab/client.js';
25
25
  import { V2E2EECoordinator } from './client/v2-e2ee.js';
@@ -29,7 +29,7 @@ import { IndexedDBTokenStore } from './keystore/indexeddb-token-store.js';
29
29
  import { decryptMessage, } from './v2/e2ee/index.js';
30
30
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
31
31
  import { AUNLogger } from './logger.js';
32
- import { AgentMdManager } from './agent-md.js';
32
+ import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
33
33
  import { GroupIndexMetaCache } from './group-index.js';
34
34
  import { certMatchesFingerprint, normalizeFingerprintHex } from './cert-utils.js';
35
35
  import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.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,24 +96,10 @@ 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([
116
- 'message.send', 'group.send', 'group.create', 'group.invite',
102
+ 'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
117
103
  'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
118
104
  'group.set_settings',
119
105
  'group.update_announcement', 'group.update_rules',
@@ -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;
@@ -244,6 +231,7 @@ function createAgentMdManagerForRuntime(opts) {
244
231
  return new AgentMdManager({
245
232
  aunPath: opts.config().aunPath,
246
233
  tokenStore: opts.tokenStore(),
234
+ uploadViaGatewayHost: true,
247
235
  logger: opts.logger(),
248
236
  ownerAidGetter: opts.ownerAid,
249
237
  currentAidGetter: opts.currentAid,
@@ -303,7 +291,6 @@ function createAgentMdManagerForRuntime(opts) {
303
291
  if (current?.aid === target) {
304
292
  if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
305
293
  return current;
306
- throw new StateError(`current AID certificate fingerprint mismatch for ${target}`);
307
294
  }
308
295
  if (!opts.gateway.get()) {
309
296
  try {
@@ -529,6 +516,7 @@ export class AUNClient {
529
516
  _currentAid = null;
530
517
  _instanceProtectedHeaders = null;
531
518
  _gatewayUrl = null;
519
+ _gatewayCandidates = [];
532
520
  _deviceId;
533
521
  _slotId;
534
522
  _connectedAt = 0;
@@ -574,6 +562,8 @@ export class AUNClient {
574
562
  _v2LazyProposeTriggered = new Map();
575
563
  /** agent.md 运行时管理器,负责上传、下载、缓存和 RPC 元数据观察。 */
576
564
  _agentMdManager;
565
+ _aidStore;
566
+ _groupIdentityOperations = new Map();
577
567
  _groupIndexMetaCache = new GroupIndexMetaCache();
578
568
  _groupIndexCacheLoaded = new Set();
579
569
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
@@ -593,20 +583,17 @@ export class AUNClient {
593
583
  _messageRecallSeen = new Map();
594
584
  /** 群撤回去重:group_id|sorted(message_ids) -> 时间戳,保证应用层只回调一次 */
595
585
  _groupRecallSeen = new Map();
596
- /** 在线未读 hint 队列:同一 group 只保留最后一条,延迟 drain 降低登录瞬时拉取压力。 */
597
- _onlineUnreadHintQueue = new Map();
598
- _onlineUnreadHintTimer = null;
599
- _onlineUnreadHintDrainActive = false;
600
- _onlineUnreadHintInitialDelayMs = 750;
601
- _onlineUnreadHintIntervalMs = 50;
602
586
  /** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
603
587
  _gapFillActive = false;
604
588
  _backgroundRpcDepth = 0;
605
589
  // Pull Gate:序列化同一 key 的并发 pull 操作,防止重复拉取
606
590
  _pullGates = new Map();
591
+ _pullResponseKeys = new Map();
607
592
  // 重连相关
608
593
  _reconnectActive = false;
609
594
  _reconnectAbort = null;
595
+ _reconnectTask = null;
596
+ _reconnectEventDispatchDepth = 0;
610
597
  _serverKicked = false;
611
598
  // 重连状态追踪(对齐 Python client.py)
612
599
  _nextRetryAt = null;
@@ -640,6 +627,7 @@ export class AUNClient {
640
627
  _storage;
641
628
  _collab;
642
629
  _messageFacade;
630
+ _mailFacade;
643
631
  _groupFacade;
644
632
  _streamFacade;
645
633
  constructor(aid) {
@@ -647,6 +635,7 @@ export class AUNClient {
647
635
  throw new ValidationError('AUNClient only accepts an AID object or no argument');
648
636
  }
649
637
  const inputAid = aid ?? null;
638
+ this._aidStore = inputAid?._aidStore;
650
639
  const rawConfig = {};
651
640
  if (inputAid)
652
641
  rawConfig.aun_path = inputAid.aunPath;
@@ -782,7 +771,7 @@ export class AUNClient {
782
771
  this._safeAsync(this._onGroupStateCommitted(data));
783
772
  });
784
773
  // 其他事件直接透传
785
- for (const evt of ['message.ack', 'storage.object_changed']) {
774
+ for (const evt of ['message.ack', 'storage.object_changed', 'stream/created', 'stream/closed']) {
786
775
  this._dispatcher.subscribe(`_raw.${evt}`, (data) => {
787
776
  this._dispatcher.publish(evt, data);
788
777
  });
@@ -796,6 +785,14 @@ export class AUNClient {
796
785
  get aid() {
797
786
  return this._aid;
798
787
  }
788
+ /** 返回后台刷新任务维护的最新 access token。 */
789
+ getAccessToken() {
790
+ const identityToken = String(this._identity?.access_token ?? '').trim();
791
+ if (identityToken)
792
+ return identityToken;
793
+ const sessionToken = String(this._sessionParams?.access_token ?? '').trim();
794
+ return sessionToken || null;
795
+ }
799
796
  /** AUN Storage VFS 入口 */
800
797
  get storage() {
801
798
  if (!this._storage)
@@ -842,7 +839,6 @@ export class AUNClient {
842
839
  for (const groupAid of Object.keys(groupIndexes)) {
843
840
  await this._persistGroupIndexCache(groupAid, {
844
841
  remote_meta: this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid) ?? {},
845
- local_etag: this._groupIndexMetaCache.localEtag(this._aid ?? '', groupAid),
846
842
  });
847
843
  }
848
844
  }
@@ -850,8 +846,23 @@ export class AUNClient {
850
846
  return this._groupIndexMetaCache.isStale(this._aid ?? '', groupAid);
851
847
  }
852
848
  markGroupIndexFresh(groupAid, options) {
853
- this._groupIndexMetaCache.markFresh(this._aid ?? '', groupAid, options);
854
- void this._persistGroupIndexCache(groupAid, { local_etag: String(options.etag ?? '') });
849
+ const verificationVersion = this._groupIndexMetaCache.markFresh(this._aid ?? '', groupAid, options);
850
+ const fields = {
851
+ local_etag: String(options.etag ?? ''),
852
+ verification_version: verificationVersion,
853
+ };
854
+ if (verificationVersion !== 1) {
855
+ fields.index_jsonl = '';
856
+ fields.settings = {};
857
+ fields.entry_etags = {};
858
+ }
859
+ void this._persistGroupIndexCache(groupAid, fields);
860
+ }
861
+ /** Mail bridge facade 入口 */
862
+ get mail() {
863
+ if (!this._mailFacade)
864
+ this._mailFacade = new MailFacade(this);
865
+ return this._mailFacade;
855
866
  }
856
867
  getGroupIndexRemoteMeta(groupAid) {
857
868
  return this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid);
@@ -868,6 +879,9 @@ export class AUNClient {
868
879
  return this._groupIndexMetaCache.cachedSettingsByEntries(this._aid ?? '', groupAid, keys.map((item) => String(item)), entries);
869
880
  }
870
881
  cacheGroupIndexSettings(groupAid, settings, options) {
882
+ if (!Array.isArray(options?.entries) || !String(options?.etag ?? '').trim()) {
883
+ return Promise.resolve();
884
+ }
871
885
  this._groupIndexMetaCache.cacheSettings(this._aid ?? '', groupAid, settings, options);
872
886
  const entryEtags = {};
873
887
  for (const item of options?.entries ?? []) {
@@ -876,6 +890,7 @@ export class AUNClient {
876
890
  entryEtags[key] = String(item.etag ?? '');
877
891
  }
878
892
  const fields = {
893
+ verification_version: 1,
879
894
  settings,
880
895
  entry_etags: entryEtags,
881
896
  remote_meta: this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid) ?? {},
@@ -906,6 +921,7 @@ export class AUNClient {
906
921
  local_etag: record.local_etag,
907
922
  settings: record.settings,
908
923
  entry_etags: record.entry_etags,
924
+ verification_version: record.verification_version,
909
925
  });
910
926
  }
911
927
  async _persistGroupIndexCache(groupAid, fields) {
@@ -920,6 +936,7 @@ export class AUNClient {
920
936
  local_etag: record.local_etag,
921
937
  settings: record.settings,
922
938
  entry_etags: record.entry_etags,
939
+ verification_version: record.verification_version,
923
940
  });
924
941
  }
925
942
  get state() {
@@ -980,6 +997,8 @@ export class AUNClient {
980
997
  }
981
998
  _resetV2IdentityRuntime() {
982
999
  this._v2RuntimeGeneration += 1;
1000
+ // 身份或 V2 会话代次切换时,隔离仍在途的 inline 解密/应用回调。
1001
+ this._delivery?.resetInlineAckState();
983
1002
  const keyStore = this._v2KeyStore;
984
1003
  try {
985
1004
  keyStore?.close?.();
@@ -1004,6 +1023,7 @@ export class AUNClient {
1004
1023
  this._v2PullPending = false;
1005
1024
  }
1006
1025
  _applyAidRuntimeContext(aid) {
1026
+ this._aidStore = aid._aidStore;
1007
1027
  const oldTransport = this._transport;
1008
1028
  try {
1009
1029
  const closeResult = oldTransport?.close?.();
@@ -1111,8 +1131,8 @@ export class AUNClient {
1111
1131
  getPeer(aid) {
1112
1132
  return this._peerDirectory.getPeer(aid);
1113
1133
  }
1114
- async lookupPeer(aid) {
1115
- return this._peerDirectory.lookupPeer(aid);
1134
+ async lookupPeer(aid, certFingerprint) {
1135
+ return this._peerDirectory.lookupPeer(aid, certFingerprint);
1116
1136
  }
1117
1137
  peers() {
1118
1138
  return this._peerDirectory.peers();
@@ -1169,14 +1189,102 @@ export class AUNClient {
1169
1189
  return;
1170
1190
  await this._handleTransportDisconnect(err);
1171
1191
  }
1172
- async createGroup(params = {}) {
1192
+ async _recoverIdentityAdmission(err, _method = '', params = {}) {
1193
+ if (!(err instanceof AUNError) || err.code !== -32003)
1194
+ return false;
1195
+ const message = String(err.message ?? '').trim();
1196
+ const groupCertPrefix = 'valid group identity certificate is required for ';
1197
+ if (message.toLowerCase().startsWith(groupCertPrefix)) {
1198
+ const fallback = message.slice(groupCertPrefix.length).trim();
1199
+ const store = this._aidStore;
1200
+ const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
1201
+ if (!groupId || !store)
1202
+ return false;
1203
+ try {
1204
+ await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
1205
+ }
1206
+ catch {
1207
+ return false;
1208
+ }
1209
+ return true;
1210
+ }
1211
+ const groupMDPrefix = 'valid group agent.md is required for ';
1212
+ if (message.toLowerCase().startsWith(groupMDPrefix)) {
1213
+ const groupAid = message.slice(groupMDPrefix.length).trim();
1214
+ const store = this._aidStore;
1215
+ if (!groupAid || !store)
1216
+ return false;
1217
+ const loaded = await store.load(groupAid);
1218
+ if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
1219
+ return false;
1220
+ const ownerAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1221
+ if (!ownerAid)
1222
+ return false;
1223
+ await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}, ownerAid));
1224
+ return true;
1225
+ }
1226
+ const agentPrefix = 'valid agent.md is required for ';
1227
+ if (!message.toLowerCase().startsWith(agentPrefix))
1228
+ return false;
1229
+ const target = message.slice(agentPrefix.length).trim();
1230
+ if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
1231
+ return false;
1232
+ await this._agentMdManager.upload(buildDefaultAgentMd(target));
1233
+ return true;
1234
+ }
1235
+ async _runGroupIdentityOperation(groupId, operation) {
1236
+ const aid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1237
+ const dot = aid.indexOf('.');
1238
+ const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
1239
+ if (!key)
1240
+ return operation();
1241
+ let task = this._groupIdentityOperations.get(key);
1242
+ if (!task) {
1243
+ task = Promise.resolve().then(operation);
1244
+ this._groupIdentityOperations.set(key, task);
1245
+ }
1246
+ try {
1247
+ return await task;
1248
+ }
1249
+ finally {
1250
+ if (this._groupIdentityOperations.get(key) === task)
1251
+ this._groupIdentityOperations.delete(key);
1252
+ }
1253
+ }
1254
+ async createGroup(params = {}, options = {}) {
1173
1255
  if (!isJsonObject(params)) {
1174
1256
  throw new ValidationError('createGroup params must be an object');
1175
1257
  }
1258
+ const store = options.aidStore ?? this._aidStore;
1259
+ if (!store) {
1260
+ throw new ValidationError('createGroup requires aidStore');
1261
+ }
1262
+ this._aidStore = store;
1176
1263
  const payload = { ...params };
1177
- const groupName = String(payload.group_name ?? payload.groupName ?? '').trim();
1178
- if (!groupName) {
1179
- return await this.call('group.create', payload);
1264
+ const isNamed = Boolean(String(payload.group_name ?? '').trim());
1265
+ payload._defer_group_ready_postprocess = true;
1266
+ if (!isNamed) {
1267
+ delete payload.public_key;
1268
+ delete payload.curve;
1269
+ const created = await this.call('group.create', payload);
1270
+ const createdMap = isJsonObject(created) ? created : {};
1271
+ const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
1272
+ const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
1273
+ if (!groupId) {
1274
+ throw new ValidationError('group.create response missing group.group_id');
1275
+ }
1276
+ const bindParams = { group_id: groupId };
1277
+ for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
1278
+ if (key in payload)
1279
+ bindParams[key] = payload[key];
1280
+ }
1281
+ const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
1282
+ const boundMap = isJsonObject(bound) ? bound : {};
1283
+ const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
1284
+ const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
1285
+ const postprocessParams = { ...payload };
1286
+ delete postprocessParams._defer_group_ready_postprocess;
1287
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1180
1288
  }
1181
1289
  const keyPair = await new CryptoProvider().generateIdentity();
1182
1290
  payload.public_key = keyPair.public_key_der_b64;
@@ -1188,30 +1296,63 @@ export class AUNClient {
1188
1296
  const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
1189
1297
  const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
1190
1298
  if (!groupAid || !certPem) {
1191
- throw new ValidationError('group.create named group response missing group.group_aid or aid_cert.cert');
1299
+ throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
1192
1300
  }
1193
- const store = new AIDStore({
1194
- aunPath: this.configModel.aunPath,
1195
- encryptionSeed: this.configModel.seedPassword ?? '',
1196
- slotId: this._slotId,
1197
- rootCaPem: this.configModel.rootCaPem,
1198
- verifySsl: this.configModel.verifySsl,
1301
+ const imported = await store.importGroupIdentity(groupAid, {
1302
+ private_key_pem: keyPair.private_key_pem,
1303
+ public_key_der_b64: keyPair.public_key_der_b64,
1304
+ curve: keyPair.curve,
1305
+ cert_pem: certPem,
1199
1306
  });
1200
- try {
1201
- const imported = await store.importGroupIdentity(groupAid, {
1202
- private_key_pem: keyPair.private_key_pem,
1203
- public_key_der_b64: keyPair.public_key_der_b64,
1204
- curve: keyPair.curve,
1205
- cert_pem: certPem,
1206
- });
1207
- if (!imported.ok) {
1208
- throw new ValidationError(imported.error.message);
1209
- }
1307
+ if (!imported.ok) {
1308
+ throw new ValidationError(imported.error.message);
1210
1309
  }
1211
- finally {
1212
- store.close();
1310
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1311
+ if (!uploaderAid) {
1312
+ throw new ValidationError('createGroup requires current owner AID for group agent.md upload');
1313
+ }
1314
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
1315
+ const postprocessParams = { ...payload };
1316
+ delete postprocessParams._defer_group_ready_postprocess;
1317
+ return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1318
+ }
1319
+ _groupAgentMdContent(groupAid, params = {}, group = {}) {
1320
+ const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
1321
+ if (explicit !== undefined && explicit !== null)
1322
+ return String(explicit);
1323
+ const value = (...keys) => {
1324
+ for (const source of [group, params]) {
1325
+ for (const key of keys) {
1326
+ const candidate = String(source[key] ?? '').trim();
1327
+ if (candidate)
1328
+ return candidate;
1329
+ }
1330
+ }
1331
+ return '';
1332
+ };
1333
+ const name = value('name', 'group_name', 'title') || groupAid;
1334
+ const normalizedDescription = (value('description', 'group_description') || `AUN group ${name}`)
1335
+ .split(/\s+/).filter(Boolean).join(' ');
1336
+ const description = Array.from(normalizedDescription).slice(0, 100).join('');
1337
+ return [
1338
+ '---\n',
1339
+ `aid: ${JSON.stringify(groupAid)}\n`,
1340
+ `name: ${JSON.stringify(name)}\n`,
1341
+ 'type: "group"\n',
1342
+ 'version: "1.0.0"\n',
1343
+ `description: ${JSON.stringify(description)}\n`,
1344
+ '---\n\n',
1345
+ `# ${name}\n`,
1346
+ ].join('');
1347
+ }
1348
+ async _uploadGroupAgentMd(store, groupAid, params, group, uploaderAid) {
1349
+ if (typeof store.uploadAgentMd !== 'function') {
1350
+ throw new ValidationError('group agent.md upload is unavailable');
1351
+ }
1352
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
1353
+ if (!uploaded.ok) {
1354
+ throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
1213
1355
  }
1214
- return result;
1215
1356
  }
1216
1357
  async bindGroupAid(params = {}, options = {}) {
1217
1358
  if (!isJsonObject(params)) {
@@ -1221,6 +1362,7 @@ export class AUNClient {
1221
1362
  if (!store) {
1222
1363
  throw new ValidationError('bindGroupAid requires aidStore');
1223
1364
  }
1365
+ this._aidStore = store;
1224
1366
  const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1225
1367
  const keystore = store._keystore;
1226
1368
  let keyPair = null;
@@ -1268,6 +1410,11 @@ export class AUNClient {
1268
1410
  if (!imported.ok) {
1269
1411
  throw new ValidationError(imported.error.message);
1270
1412
  }
1413
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1414
+ if (!uploaderAid) {
1415
+ throw new ValidationError('bindGroupAid requires current owner AID for group agent.md upload');
1416
+ }
1417
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
1271
1418
  // 落盘成功,清除 pending 槽位
1272
1419
  if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
1273
1420
  await keystore.clearPendingGroupBind(groupId);
@@ -1289,7 +1436,7 @@ export class AUNClient {
1289
1436
  let groupAid = String(params.group_aid ?? '').trim();
1290
1437
  if (!groupAid) {
1291
1438
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1292
- groupAid = String(info?.group_aid ?? '').trim();
1439
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1293
1440
  }
1294
1441
  if (!groupAid) {
1295
1442
  throw new ValidationError('renewGroupAid: unable to determine group_aid');
@@ -1357,6 +1504,11 @@ export class AUNClient {
1357
1504
  if (!imported.ok) {
1358
1505
  throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
1359
1506
  }
1507
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1508
+ if (!uploaderAid) {
1509
+ throw new ValidationError('renewGroupAid requires current owner AID for group agent.md upload');
1510
+ }
1511
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
1360
1512
  return result;
1361
1513
  }
1362
1514
  async startGroupTransfer(params = {}, options = {}) {
@@ -1375,7 +1527,7 @@ export class AUNClient {
1375
1527
  let groupAid = String(params.group_aid ?? '').trim();
1376
1528
  if (!groupAid) {
1377
1529
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1378
- groupAid = String(info?.group_aid ?? '').trim();
1530
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1379
1531
  }
1380
1532
  if (!groupAid) {
1381
1533
  throw new ValidationError('startGroupTransfer: unable to determine group_aid');
@@ -1423,7 +1575,7 @@ export class AUNClient {
1423
1575
  let groupAid = String(params.group_aid ?? '').trim();
1424
1576
  if (!groupAid) {
1425
1577
  const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
1426
- groupAid = String(info?.group_aid ?? '').trim();
1578
+ groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
1427
1579
  }
1428
1580
  if (!groupAid) {
1429
1581
  throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
@@ -1471,6 +1623,11 @@ export class AUNClient {
1471
1623
  if (!imported.ok) {
1472
1624
  throw new ValidationError(imported.error.message);
1473
1625
  }
1626
+ const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
1627
+ if (!uploaderAid) {
1628
+ throw new ValidationError('completeGroupTransfer requires current owner AID for group agent.md upload');
1629
+ }
1630
+ await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
1474
1631
  return result;
1475
1632
  }
1476
1633
  static _notifyParamsSizeOk(params) {
@@ -1539,15 +1696,16 @@ export class AUNClient {
1539
1696
  const eventMethod = AUNClient._validateNotifyEventMethod(method);
1540
1697
  // 校验目标 Group ID 格式
1541
1698
  validateGroupIDFormat(targetGroupId, 'notify.group_id');
1542
- const normalizedGroupId = normalizeGroupId(targetGroupId);
1543
- if (!normalizedGroupId) {
1699
+ if (!targetGroupId) {
1544
1700
  throw new ValidationError('group_id is required for group notify');
1545
1701
  }
1546
1702
  const routeParams = {
1547
- group_aid: normalizedGroupId,
1548
- group_id: normalizedGroupId,
1703
+ group_id: targetGroupId,
1549
1704
  deliver: { method: eventMethod, params: payload },
1550
1705
  };
1706
+ const explicitGroupAid = String(options.group_aid ?? options.groupAid ?? '').trim();
1707
+ if (explicitGroupAid)
1708
+ routeParams.group_aid = explicitGroupAid;
1551
1709
  if (ttl !== undefined)
1552
1710
  routeParams.ttl_ms = ttl;
1553
1711
  await this._transport.notify('notification/group.route', routeParams);
@@ -1564,17 +1722,26 @@ export class AUNClient {
1564
1722
  }
1565
1723
  async _callRawV2Rpc(method, params) {
1566
1724
  const p = { ...(params ?? {}) };
1567
- const rpcBackground = Boolean(p._rpc_background) || this._backgroundRpcDepth > 0;
1725
+ const forceForeground = Boolean(p._rpc_foreground);
1726
+ const rpcBackground = !forceForeground
1727
+ && (Boolean(p._rpc_background) || this._backgroundRpcDepth > 0);
1728
+ delete p._rpc_foreground;
1568
1729
  delete p._rpc_background;
1569
1730
  delete p._pull_gate_locked;
1570
1731
  delete p._skip_auto_ack;
1571
1732
  delete p.skip_auto_ack;
1572
1733
  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);
1734
+ if (method.startsWith('group.')) {
1735
+ const rawGroupAid = String(p.group_aid ?? p.groupAid ?? '').trim();
1736
+ const rawGroupId = String(p.group_id ?? p.groupId ?? '').trim();
1737
+ if (rawGroupId)
1738
+ p.group_id = rawGroupId;
1739
+ else if (rawGroupAid)
1740
+ p.group_id = rawGroupAid;
1741
+ if (rawGroupAid)
1742
+ p.group_aid = rawGroupAid;
1743
+ delete p.groupAid;
1744
+ delete p.groupId;
1578
1745
  }
1579
1746
  if (method.startsWith('group.') && p.device_id === undefined) {
1580
1747
  p.device_id = this._deviceId;
@@ -1678,30 +1845,30 @@ export class AUNClient {
1678
1845
  const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
1679
1846
  msg.payload = { ...payload, text: payload.text + '\n' + trace };
1680
1847
  }
1681
- async _drainOrderedMessages(ns, beforeSeq) {
1682
- await this._delivery.drainOrderedMessages(ns, beforeSeq);
1848
+ async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
1849
+ await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
1683
1850
  }
1684
1851
  async _publishOrderedMessage(event, ns, seq, payload) {
1685
1852
  return this._delivery.publishOrderedMessage(event, ns, seq, payload);
1686
1853
  }
1687
- async _publishPulledMessage(event, ns, seq, payload) {
1688
- return this._delivery.publishPulledMessage(event, ns, seq, payload);
1854
+ async _publishPulledMessage(event, ns, seq, payload, persist = true) {
1855
+ return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
1689
1856
  }
1690
1857
  _extractGroupIdFromResult(result) {
1691
1858
  const group = isJsonObject(result.group) ? result.group : null;
1692
- const gid = group ? String(group.group_aid ?? group.group_id ?? '') : '';
1859
+ const gid = group ? String(group.group_id ?? group.groupId ?? group.group_aid ?? group.groupAid ?? '') : '';
1693
1860
  if (gid)
1694
1861
  return gid;
1695
- const directGid = String(result.group_aid ?? result.group_id ?? '');
1862
+ const directGid = String(result.group_id ?? result.groupId ?? result.group_aid ?? result.groupAid ?? '');
1696
1863
  if (directGid)
1697
1864
  return directGid;
1698
1865
  const member = isJsonObject(result.member) ? result.member : null;
1699
- return member ? String(member.group_aid ?? member.group_id ?? '') : '';
1866
+ return member ? String(member.group_id ?? member.groupId ?? member.group_aid ?? member.groupAid ?? '') : '';
1700
1867
  }
1701
1868
  async _onRawGroupChanged(data) {
1702
1869
  const tStart = Date.now();
1703
1870
  const action = String(data?.action ?? '');
1704
- const groupIdInit = String(data?.group_aid ?? data?.group_id ?? '');
1871
+ const groupIdInit = String(data?.group_id ?? data?.group_aid ?? '');
1705
1872
  this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupIdInit} action=${action}`);
1706
1873
  try {
1707
1874
  if (isJsonObject(data)) {
@@ -1717,7 +1884,7 @@ export class AUNClient {
1717
1884
  d._verified = this._isEventSignatureVerified(verified);
1718
1885
  }
1719
1886
  }
1720
- const groupId = (d.group_aid ?? d.group_id ?? '');
1887
+ const groupId = (d.group_id ?? d.group_aid ?? '');
1721
1888
  await this._delivery.handleGroupChangedEventSeq(d, groupId);
1722
1889
  }
1723
1890
  else {
@@ -1923,8 +2090,10 @@ export class AUNClient {
1923
2090
  await this._rpcPipeline.signClientOperation(method, params);
1924
2091
  }
1925
2092
  // ── 内部:连接 ────────────────────────────────────
1926
- async _connectOnce(params, allowReauth) {
2093
+ async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
1927
2094
  const tStart = Date.now();
2095
+ connectionOwner ??= this._connectAbort ?? undefined;
2096
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1928
2097
  this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
1929
2098
  try {
1930
2099
  const gatewayUrl = this._resolveGateway(params);
@@ -1937,8 +2106,12 @@ export class AUNClient {
1937
2106
  // 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
1938
2107
  this._refreshSeqTrackerContext();
1939
2108
  await this._restoreSeqTrackerState();
2109
+ this._assertReconnectOwner(reconnectOwner);
2110
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1940
2111
  try {
1941
2112
  const challenge = await this._transport.connect(gatewayUrl);
2113
+ this._assertReconnectOwner(reconnectOwner);
2114
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1942
2115
  this._state = 'authenticating';
1943
2116
  if (allowReauth) {
1944
2117
  const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
@@ -1950,6 +2123,8 @@ export class AUNClient {
1950
2123
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
1951
2124
  extraInfo: params.extra_info,
1952
2125
  });
2126
+ this._assertReconnectOwner(reconnectOwner);
2127
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1953
2128
  if (isJsonObject(authContext)) {
1954
2129
  const auth = authContext;
1955
2130
  const identity = auth.identity;
@@ -1974,7 +2149,10 @@ export class AUNClient {
1974
2149
  shortTtlMs: Number(params.short_ttl_ms ?? 0),
1975
2150
  extraInfo: params.extra_info,
1976
2151
  });
2152
+ this._assertReconnectOwner(reconnectOwner);
2153
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1977
2154
  await this._syncIdentityAfterConnect(String(params.access_token));
2155
+ this._assertReconnectOwner(reconnectOwner);
1978
2156
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
1979
2157
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
1980
2158
  }
@@ -1982,23 +2160,57 @@ export class AUNClient {
1982
2160
  }
1983
2161
  catch (err) {
1984
2162
  // P1-19: 首连失败时重置状态,避免半连接残留
1985
- this._state = 'standby';
2163
+ if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
2164
+ && (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
2165
+ && !this._closing) {
2166
+ this._state = 'standby';
2167
+ }
1986
2168
  try {
1987
2169
  await this._transport.close();
1988
2170
  }
1989
2171
  catch { /* 忽略关闭错误 */ }
1990
2172
  throw err;
1991
2173
  }
2174
+ this._assertReconnectOwner(reconnectOwner);
2175
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2176
+ if (!this._transport.isConnected()) {
2177
+ throw new ConnectionError('websocket disconnected during authentication');
2178
+ }
2179
+ this._serverKicked = false;
2180
+ this._lastDisconnectInfo = null;
1992
2181
  this._state = 'connected';
1993
2182
  this._connectedAt = Date.now();
1994
- await this._dispatcher.publish('state_change', {
2183
+ if (Array.isArray(params.gateways)) {
2184
+ this._gatewayCandidates = [...new Set(params.gateways
2185
+ .map((item) => String(item ?? '').trim()).filter(Boolean))];
2186
+ }
2187
+ else if (gatewayUrl) {
2188
+ this._gatewayCandidates = [gatewayUrl];
2189
+ }
2190
+ const statePayload = {
1995
2191
  state: this._publicState(this._state),
1996
2192
  gateway: gatewayUrl,
1997
- });
2193
+ };
2194
+ if (reconnectOwner) {
2195
+ if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
2196
+ throw new ConnectionError('reconnect attempt superseded');
2197
+ }
2198
+ }
2199
+ else if (connectionOwner) {
2200
+ if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
2201
+ throw new StateError('connection attempt superseded');
2202
+ }
2203
+ }
2204
+ else {
2205
+ await this._dispatcher.publish('state_change', statePayload);
2206
+ }
2207
+ this._assertReconnectOwner(reconnectOwner);
2208
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
1998
2209
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
1999
2210
  this._refreshSeqTrackerContext();
2000
2211
  await this._restoreSeqTrackerState();
2001
2212
  }
2213
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2002
2214
  this._startBackgroundTasks();
2003
2215
  const connectionKind = String(params.connection_kind ?? 'long');
2004
2216
  const isShortConnection = connectionKind === 'short';
@@ -2007,6 +2219,8 @@ export class AUNClient {
2007
2219
  && (!isShortConnection || hasExplicitBackgroundSync);
2008
2220
  if (!isShortConnection) {
2009
2221
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
2222
+ this._assertReconnectOwner(reconnectOwner);
2223
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2010
2224
  }
2011
2225
  else {
2012
2226
  this._clientLog.debug('V2 session init deferred for short connection');
@@ -2023,26 +2237,43 @@ export class AUNClient {
2023
2237
  }
2024
2238
  }
2025
2239
  _resolveGateway(params) {
2240
+ const explicitGateway = String(params.gateway ?? '').trim();
2241
+ if (explicitGateway)
2242
+ return explicitGateway;
2026
2243
  const gateways = this._resolveGateways(params);
2027
2244
  return gateways[0];
2028
2245
  }
2029
2246
  async _resolveGatewayForAid(aid) {
2247
+ const gateways = await this._resolveGatewayCandidatesForAid(aid);
2248
+ return gateways[0];
2249
+ }
2250
+ async _resolveGatewayCandidatesForAid(aid) {
2030
2251
  const target = String(aid ?? this._aid ?? '').trim();
2031
2252
  if (!target)
2032
2253
  throw new StateError('gateway discovery requires a loaded AID');
2033
- if (this._gatewayUrl)
2034
- return this._gatewayUrl;
2254
+ if (this._gatewayCandidates.length > 0)
2255
+ return [...this._gatewayCandidates];
2256
+ if (this._gatewayUrl) {
2257
+ this._gatewayCandidates = [this._gatewayUrl];
2258
+ return [...this._gatewayCandidates];
2259
+ }
2035
2260
  try {
2036
2261
  const getMetadata = this._tokenStore.getMetadata;
2262
+ const rawList = typeof getMetadata === 'function'
2263
+ ? String(await getMetadata.call(this._tokenStore, target, 'gateway_urls') ?? '').trim()
2264
+ : '';
2265
+ const cachedList = rawList ? JSON.parse(rawList) : [];
2037
2266
  const raw = typeof getMetadata === 'function'
2038
2267
  ? String(await getMetadata.call(this._tokenStore, target, 'gateway_url') ?? '').trim()
2039
2268
  : '';
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
- }
2269
+ const candidates = Array.isArray(cachedList)
2270
+ ? cachedList.map((item) => String(item ?? '').trim()).filter(Boolean)
2271
+ : [];
2272
+ const gateway = candidates[0] ?? (raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw);
2273
+ if (gateway) {
2274
+ this._gatewayCandidates = [...new Set([gateway, ...candidates])];
2275
+ this._gatewayUrl = this._gatewayCandidates[0];
2276
+ return [...this._gatewayCandidates];
2046
2277
  }
2047
2278
  }
2048
2279
  catch {
@@ -2058,18 +2289,24 @@ export class AUNClient {
2058
2289
  let lastError = null;
2059
2290
  for (const url of candidates) {
2060
2291
  try {
2061
- const gateway = await this._discovery.discover(url);
2292
+ const discovered = await this._discovery.discoverAll(url);
2293
+ const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
2294
+ if (gatewayUrls.length === 0)
2295
+ throw new ValidationError('gateway discovery returned no candidates');
2296
+ const gateway = gatewayUrls[0];
2297
+ this._gatewayCandidates = gatewayUrls;
2062
2298
  this._gatewayUrl = gateway;
2063
2299
  try {
2064
2300
  const setMetadata = this._tokenStore.setMetadata;
2065
2301
  if (typeof setMetadata === 'function') {
2066
2302
  await setMetadata.call(this._tokenStore, target, 'gateway_url', gateway);
2303
+ await setMetadata.call(this._tokenStore, target, 'gateway_urls', JSON.stringify(gatewayUrls));
2067
2304
  }
2068
2305
  }
2069
2306
  catch {
2070
2307
  // 缓存写入失败不影响连接。
2071
2308
  }
2072
- return gateway;
2309
+ return [...gatewayUrls];
2073
2310
  }
2074
2311
  catch (err) {
2075
2312
  lastError = err;
@@ -2088,7 +2325,9 @@ export class AUNClient {
2088
2325
  throw new ValidationError('relay topology is not implemented in the Browser SDK');
2089
2326
  }
2090
2327
  }
2091
- const gw = params.gateway ?? params.gateways;
2328
+ const gw = Array.isArray(params.gateways) && params.gateways.length > 0
2329
+ ? params.gateways
2330
+ : params.gateway;
2092
2331
  if (Array.isArray(gw)) {
2093
2332
  const urls = gw.map((g) => String(g ?? '')).filter((u) => u.length > 0);
2094
2333
  if (urls.length > 0)
@@ -2118,7 +2357,13 @@ export class AUNClient {
2118
2357
  _normalizeConnectParams(params) {
2119
2358
  const request = { ...params };
2120
2359
  const accessToken = String(request.access_token ?? '');
2121
- const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
2360
+ const rawGateways = request.gateways;
2361
+ const normalizedGateways = Array.isArray(rawGateways)
2362
+ ? [...new Set(rawGateways.map((item) => String(item ?? '').trim()).filter(Boolean))]
2363
+ : [];
2364
+ if (normalizedGateways.length > 0)
2365
+ request.gateways = normalizedGateways;
2366
+ const gateway = String(request.gateway ?? normalizedGateways[0] ?? this._gatewayUrl ?? '');
2122
2367
  if (!gateway)
2123
2368
  throw new StateError('connect requires non-empty gateway');
2124
2369
  if (accessToken)
@@ -2190,9 +2435,6 @@ export class AUNClient {
2190
2435
  if ('auto_reconnect' in params) {
2191
2436
  options.auto_reconnect = Boolean(params.auto_reconnect);
2192
2437
  }
2193
- if ('heartbeat_interval' in params) {
2194
- options.heartbeat_interval = Number(params.heartbeat_interval);
2195
- }
2196
2438
  if ('token_refresh_before' in params) {
2197
2439
  options.token_refresh_before = Number(params.token_refresh_before);
2198
2440
  }
@@ -2208,10 +2450,9 @@ export class AUNClient {
2208
2450
  }
2209
2451
  // ── 内部:后台任务 ────────────────────────────────
2210
2452
  _startBackgroundTasks() {
2211
- // 短连接不启动 heartbeat token 刷新(生命周期短,不需要长期会话维护);
2212
- // auto_reconnect 仍允许,由 _sessionOptions.auto_reconnect 决定
2453
+ // WebSocket Ping/Pong Gateway 与传输层处理;SDK 不发送 RPC 心跳。
2454
+ // 短连接生命周期短,不需要 token 刷新。
2213
2455
  if (this._sessionOptions?.connection_kind !== 'short') {
2214
- this._startHeartbeat();
2215
2456
  this._startTokenRefresh();
2216
2457
  }
2217
2458
  // V2 内存缓存定时清理(每小时扫描过期条目)
@@ -2252,56 +2493,19 @@ export class AUNClient {
2252
2493
  }
2253
2494
  /** 心跳定时器 */
2254
2495
  _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
2496
  if (this._heartbeatTimer !== null) {
2299
2497
  clearInterval(this._heartbeatTimer);
2300
2498
  this._heartbeatTimer = null;
2301
2499
  }
2302
- if (newInterval > 0 && this.state === ConnectionState.READY && !this._closing) {
2303
- this._startHeartbeat();
2500
+ this._sessionOptions.heartbeat_interval = 0;
2501
+ }
2502
+ /** 忽略旧 Gateway 的 RPC 心跳配置,固定使用 WebSocket Ping/Pong。 */
2503
+ _applyServerHeartbeatInterval(_raw, _source) {
2504
+ if (this._heartbeatTimer !== null) {
2505
+ clearInterval(this._heartbeatTimer);
2506
+ this._heartbeatTimer = null;
2304
2507
  }
2508
+ this._sessionOptions.heartbeat_interval = 0;
2305
2509
  }
2306
2510
  /** Token 刷新定时器 */
2307
2511
  _startTokenRefresh() {
@@ -2399,7 +2603,7 @@ export class AUNClient {
2399
2603
  }
2400
2604
  // ── 内部:断线重连 ────────────────────────────────
2401
2605
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
2402
- static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015]);
2606
+ static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
2403
2607
  /** 处理服务端主动断开通知 event/gateway.disconnect
2404
2608
  *
2405
2609
  * 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
@@ -2411,7 +2615,11 @@ export class AUNClient {
2411
2615
  const reason = obj.reason ?? '';
2412
2616
  const detail = (obj.detail && typeof obj.detail === 'object') ? obj.detail : {};
2413
2617
  this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
2414
- this._serverKicked = true;
2618
+ const reasonText = String(reason ?? '').trim().toLowerCase();
2619
+ const retryable = Number(code) === 4012
2620
+ || reasonText.includes('long_connection_already_exists')
2621
+ || reasonText.includes('long connection already exists');
2622
+ this._serverKicked = !retryable;
2415
2623
  // 缓存最近一次 disconnect 信息,让后续 connection.state(terminal_failed) 也能带 detail
2416
2624
  this._lastDisconnectInfo = { code, reason, detail };
2417
2625
  // 透传给应用层订阅者
@@ -2422,25 +2630,48 @@ export class AUNClient {
2422
2630
  this._clientLog.debug(`publish gateway.disconnect failed: ${exc?.message ?? exc}`);
2423
2631
  }
2424
2632
  }
2425
- async _handleTransportDisconnect(error, closeCode) {
2633
+ async _handleTransportDisconnect(error, closeCode, initialOptions) {
2426
2634
  if (this._closing || this._state === 'closed')
2427
2635
  return;
2636
+ // 在首个 await 前占位,避免 close/error 与 token 刷新并发启动多条重连链。
2637
+ if (this._reconnectActive)
2638
+ return;
2639
+ this._reconnectActive = true;
2640
+ // 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
2641
+ const reconnectAbort = new AbortController();
2642
+ this._reconnectAbort = reconnectAbort;
2643
+ // 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
2644
+ this._delivery.resetInlineAckState();
2428
2645
  this._state = 'standby';
2429
2646
  // 先停止后台任务,避免心跳/token刷新在重连期间继续触发
2430
2647
  this._stopBackgroundTasks();
2431
- void this._transport.close().catch((exc) => {
2648
+ try {
2649
+ await this._transport.close();
2650
+ }
2651
+ catch (exc) {
2432
2652
  this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError(exc)}`);
2433
- });
2653
+ }
2434
2654
  await this._dispatcher.publish('state_change', {
2435
2655
  state: this._publicState(this._state),
2436
2656
  error,
2437
2657
  });
2438
- if (!this._sessionOptions.auto_reconnect)
2658
+ if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
2659
+ if (this._reconnectAbort === reconnectAbort) {
2660
+ this._reconnectAbort = null;
2661
+ this._reconnectActive = false;
2662
+ }
2439
2663
  return;
2440
- if (this._reconnectActive)
2664
+ }
2665
+ if (!this._sessionOptions.auto_reconnect) {
2666
+ if (this._reconnectAbort === reconnectAbort) {
2667
+ this._reconnectAbort = null;
2668
+ this._reconnectActive = false;
2669
+ }
2441
2670
  return;
2671
+ }
2442
2672
  // 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
2443
- if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
2673
+ const retryableClose = closeCode === 4012;
2674
+ if ((this._serverKicked && !retryableClose) || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
2444
2675
  this._state = 'terminal_failed';
2445
2676
  const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
2446
2677
  this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
@@ -2457,16 +2688,99 @@ export class AUNClient {
2457
2688
  eventPayload.code = disconnectInfo.code;
2458
2689
  }
2459
2690
  await this._dispatcher.publish('state_change', eventPayload);
2691
+ if (this._reconnectAbort === reconnectAbort) {
2692
+ this._reconnectAbort = null;
2693
+ this._reconnectActive = false;
2694
+ }
2460
2695
  return;
2461
2696
  }
2462
2697
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
2463
2698
  const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
2464
- this._reconnectActive = true;
2465
- this._reconnectAbort = new AbortController();
2466
- this._safeAsync(this._reconnectLoop(serverInitiated));
2699
+ if (this._closing || reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort) {
2700
+ if (this._reconnectAbort === reconnectAbort) {
2701
+ this._reconnectAbort = null;
2702
+ this._reconnectActive = false;
2703
+ }
2704
+ return;
2705
+ }
2706
+ const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
2707
+ this._reconnectTask = reconnectTask;
2708
+ this._safeAsync(reconnectTask);
2709
+ }
2710
+ _ownsReconnect(owner) {
2711
+ return Boolean(owner
2712
+ && !owner.signal.aborted
2713
+ && !this._closing
2714
+ && this._reconnectAbort === owner);
2467
2715
  }
2468
- /** 指数退避 + 固定上限抖动重连循环(默认无限重试,仅在不可重试错误或 close() 或 max_attempts 耗尽时终止) */
2469
- async _reconnectLoop(serverInitiated = false) {
2716
+ _assertReconnectOwner(owner) {
2717
+ if (owner && !this._ownsReconnect(owner)) {
2718
+ throw new ConnectionError('reconnect attempt superseded');
2719
+ }
2720
+ }
2721
+ async _publishReconnectEvent(owner, event, payload) {
2722
+ if (!this._ownsReconnect(owner))
2723
+ return false;
2724
+ this._reconnectEventDispatchDepth += 1;
2725
+ try {
2726
+ await this._dispatcher.publish(event, payload);
2727
+ }
2728
+ finally {
2729
+ this._reconnectEventDispatchDepth -= 1;
2730
+ }
2731
+ return this._ownsReconnect(owner);
2732
+ }
2733
+ async _cancelReconnectAndWait() {
2734
+ const owner = this._reconnectAbort;
2735
+ const task = this._reconnectTask;
2736
+ if (!owner && !task && !this._reconnectActive)
2737
+ return;
2738
+ owner?.abort();
2739
+ try {
2740
+ await this._transport.close();
2741
+ }
2742
+ catch (exc) {
2743
+ this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
2744
+ }
2745
+ // state_change handler 内等待其所属重连任务会自锁;该路径由事件返回后的
2746
+ // owner 复核负责收尾,且事件回调期间尚未进入下一次 transport 操作。
2747
+ if (task && this._reconnectEventDispatchDepth === 0) {
2748
+ try {
2749
+ await task;
2750
+ }
2751
+ catch (exc) {
2752
+ this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
2753
+ }
2754
+ }
2755
+ if (this._reconnectAbort === owner) {
2756
+ this._reconnectAbort = null;
2757
+ this._reconnectActive = false;
2758
+ this._nextRetryAt = null;
2759
+ }
2760
+ if (this._reconnectTask === task)
2761
+ this._reconnectTask = null;
2762
+ }
2763
+ /** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
2764
+ async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort, initialOptions) {
2765
+ let releasedOwnGeneration = false;
2766
+ try {
2767
+ await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
2768
+ }
2769
+ finally {
2770
+ if (this._reconnectAbort === reconnectAbort) {
2771
+ this._nextRetryAt = null;
2772
+ this._reconnectActive = false;
2773
+ this._reconnectAbort = null;
2774
+ this._reconnectTask = null;
2775
+ releasedOwnGeneration = true;
2776
+ }
2777
+ }
2778
+ if (releasedOwnGeneration && !this._closing && this._state === 'connected' && !this._transport.isConnected()) {
2779
+ await this._handleTransportDisconnect(new ConnectionError('websocket disconnected while reconnect completed'));
2780
+ }
2781
+ }
2782
+ /** 指数退避 + 当前基数抖动重连循环主体。 */
2783
+ async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
2470
2784
  const retry = { ...this._sessionOptions.retry };
2471
2785
  const maxBaseDelay = clampReconnectDelaySeconds(retry.max_delay, RECONNECT_MAX_BASE_DELAY_SECONDS);
2472
2786
  // M25: max_attempts=0 表示无限重试(与 Go/Python 对齐)
@@ -2476,18 +2790,17 @@ export class AUNClient {
2476
2790
  let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
2477
2791
  this._retryAttempt = 0;
2478
2792
  this._retryMaxAttempts = maxAttempts;
2479
- for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
2793
+ for (let attempt = 1; !reconnectAbort?.signal.aborted; attempt++) {
2480
2794
  // R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
2481
2795
  if (maxAttempts > 0 && attempt > maxAttempts) {
2482
2796
  this._state = 'terminal_failed';
2483
2797
  this._nextRetryAt = null;
2484
- this._reconnectActive = false;
2485
- this._reconnectAbort = null;
2486
- await this._dispatcher.publish('state_change', {
2487
- state: this._publicState(this._state),
2488
- attempt: attempt - 1,
2489
- reason: 'max_attempts_exhausted',
2490
- });
2798
+ if (reconnectAbort)
2799
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2800
+ state: this._publicState(this._state),
2801
+ attempt: attempt - 1,
2802
+ reason: 'max_attempts_exhausted',
2803
+ });
2491
2804
  return;
2492
2805
  }
2493
2806
  // 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
@@ -2495,35 +2808,52 @@ export class AUNClient {
2495
2808
  const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
2496
2809
  this._nextRetryAt = new Date(Date.now() + sleepMs);
2497
2810
  this._state = 'retry_backoff';
2498
- await this._dispatcher.publish('state_change', {
2811
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2499
2812
  state: this._publicState(this._state),
2500
2813
  attempt,
2501
2814
  next_retry_at: this._nextRetryAt.getTime() / 1000,
2502
- });
2815
+ }))
2816
+ return;
2503
2817
  try {
2504
- await this._sleep(sleepMs);
2818
+ await this._sleep(sleepMs, reconnectAbort?.signal);
2505
2819
  this._nextRetryAt = null;
2506
- if (this._reconnectAbort?.signal.aborted) {
2507
- this._reconnectActive = false;
2820
+ if (!this._ownsReconnect(reconnectAbort))
2508
2821
  return;
2509
- }
2510
2822
  // 退避结束,进入 reconnecting 状态
2511
2823
  this._state = 'reconnecting';
2512
- await this._dispatcher.publish('state_change', {
2824
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2513
2825
  state: this._publicState(this._state),
2514
2826
  attempt,
2515
- });
2516
- // 重连前先 GET /health 探测,不健康则跳过本轮
2827
+ }))
2828
+ return;
2829
+ if (!this._ownsReconnect(reconnectAbort))
2830
+ return;
2831
+ // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
2517
2832
  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
- }
2833
+ void this._discovery.checkHealth(this._gatewayUrl, 5000)
2834
+ .then((healthy) => {
2835
+ if (!healthy)
2836
+ this._clientLog.warn(`reconnect health advisory failed: gateway=${this._gatewayUrl}`);
2837
+ })
2838
+ .catch((exc) => {
2839
+ this._clientLog.debug(`reconnect health advisory error: ${exc instanceof Error ? exc.message : String(exc)}`);
2840
+ });
2525
2841
  }
2526
2842
  await this._transport.close();
2843
+ if (!this._ownsReconnect(reconnectAbort))
2844
+ return;
2845
+ if (initialOptions) {
2846
+ // 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
2847
+ await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
2848
+ if (!this._ownsReconnect(reconnectAbort))
2849
+ return;
2850
+ if (!this._transport.isConnected()) {
2851
+ throw new ConnectionError('websocket disconnected while initial reconnect completed');
2852
+ }
2853
+ this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
2854
+ this._nextRetryAt = null;
2855
+ return;
2856
+ }
2527
2857
  if (!this._sessionParams) {
2528
2858
  throw new StateError('missing connect params for reconnect');
2529
2859
  }
@@ -2545,44 +2875,72 @@ export class AUNClient {
2545
2875
  this._sessionParams.access_token = '';
2546
2876
  }
2547
2877
  }
2548
- await this._connectOnce(this._sessionParams, true);
2878
+ const candidates = this._orderedReconnectGateways(this._sessionParams);
2879
+ let lastGatewayError = null;
2880
+ let connected = false;
2881
+ for (const gateway of candidates) {
2882
+ const attemptParams = { ...this._sessionParams, gateway };
2883
+ try {
2884
+ await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
2885
+ if (!this._ownsReconnect(reconnectAbort))
2886
+ return;
2887
+ if (!this._transport.isConnected()) {
2888
+ throw new ConnectionError('websocket disconnected while reconnecting');
2889
+ }
2890
+ this._sessionParams.gateway = gateway;
2891
+ connected = true;
2892
+ break;
2893
+ }
2894
+ catch (exc) {
2895
+ lastGatewayError = exc;
2896
+ await this._transport.close().catch(() => { });
2897
+ if (candidates.length > 1) {
2898
+ this._clientLog.warn(`reconnect gateway failed, trying next: gateway=${gateway}, error=${exc instanceof Error ? exc.message : String(exc)}`);
2899
+ }
2900
+ if (!this._shouldRetryReconnect(exc))
2901
+ throw exc;
2902
+ }
2903
+ }
2904
+ if (!connected)
2905
+ throw (lastGatewayError instanceof Error ? lastGatewayError : new ConnectionError('all gateway candidates failed'));
2549
2906
  this._lastError = null;
2550
2907
  this._lastErrorCode = null;
2551
2908
  this._nextRetryAt = null;
2552
- this._reconnectActive = false;
2553
- this._reconnectAbort = null;
2554
2909
  return;
2555
2910
  }
2556
2911
  catch (exc) {
2912
+ if (!this._ownsReconnect(reconnectAbort))
2913
+ return;
2557
2914
  this._lastError = exc instanceof Error ? exc : new Error(String(exc));
2558
2915
  this._lastErrorCode = 'reconnect_failed';
2559
- await this._dispatcher.publish('connection.error', {
2916
+ if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
2560
2917
  error: formatCaughtError(exc),
2561
2918
  attempt,
2562
- });
2919
+ }))
2920
+ return;
2563
2921
  if (!this._shouldRetryReconnect(exc)) {
2564
2922
  this._state = 'terminal_failed';
2565
2923
  this._nextRetryAt = null;
2566
- this._reconnectActive = false;
2567
- this._reconnectAbort = null;
2568
- await this._dispatcher.publish('state_change', {
2569
- state: this._publicState(this._state),
2570
- error: formatCaughtError(exc),
2571
- attempt,
2572
- });
2924
+ if (reconnectAbort)
2925
+ await this._publishReconnectEvent(reconnectAbort, 'state_change', {
2926
+ state: this._publicState(this._state),
2927
+ error: formatCaughtError(exc),
2928
+ attempt,
2929
+ });
2573
2930
  return;
2574
2931
  }
2575
2932
  delay = Math.min(delay * 2, maxBaseDelay);
2576
2933
  }
2577
2934
  }
2578
- this._reconnectActive = false;
2579
- this._reconnectAbort = null;
2580
2935
  }
2581
2936
  /** 判断是否应重试重连 */
2582
2937
  _shouldRetryReconnect(error) {
2583
2938
  if (error instanceof AuthError) {
2584
2939
  const message = String(error.message ?? '').toLowerCase();
2585
- if (message.includes('aid_login1_failed') || message.includes('aid_login2_failed')) {
2940
+ if (message.includes('long_connection_already_exists')
2941
+ || message.includes('long connection already exists')
2942
+ || message.includes('aid_login1_failed')
2943
+ || message.includes('aid_login2_failed')) {
2586
2944
  return true;
2587
2945
  }
2588
2946
  return false;
@@ -2591,7 +2949,7 @@ export class AUNClient {
2591
2949
  || error instanceof ValidationError || error instanceof StateError) {
2592
2950
  return false;
2593
2951
  }
2594
- if (error instanceof ConnectionError)
2952
+ if (error instanceof ConnectionError || error instanceof TimeoutError)
2595
2953
  return true;
2596
2954
  if (error instanceof AUNError)
2597
2955
  return error.retryable;
@@ -2611,39 +2969,32 @@ export class AUNClient {
2611
2969
  _resetSeqTrackingState() {
2612
2970
  void this._saveSeqTrackerState();
2613
2971
  this._resetV2IdentityRuntime();
2972
+ this._v2E2EE.resetForwardCursorState();
2614
2973
  this._seqTracker = new SeqTracker();
2615
2974
  this._seqTrackerContext = null;
2616
2975
  this._gapFillDone.clear();
2617
2976
  this._pushedSeqs.clear();
2977
+ this._pullResponseKeys.clear();
2618
2978
  this._pendingOrderedMsgs.clear();
2619
2979
  this._v2SenderIKPending.clear();
2620
2980
  this._v2SenderIKFetching.clear();
2621
2981
  this._groupSynced.clear();
2622
- this._onlineUnreadHintQueue.clear();
2623
- if (this._onlineUnreadHintTimer) {
2624
- clearTimeout(this._onlineUnreadHintTimer);
2625
- this._onlineUnreadHintTimer = null;
2626
- }
2627
- this._onlineUnreadHintDrainActive = false;
2628
2982
  }
2629
2983
  _refreshSeqTrackerContext() {
2630
2984
  const nextContext = this._currentSeqTrackerContext();
2631
2985
  if (nextContext === this._seqTrackerContext)
2632
2986
  return;
2633
2987
  void this._saveSeqTrackerState();
2988
+ this._v2E2EE.resetForwardCursorState();
2634
2989
  this._seqTracker = new SeqTracker();
2990
+ this._delivery.resetInlineAckState();
2635
2991
  this._gapFillDone.clear();
2636
2992
  this._pushedSeqs.clear();
2993
+ this._pullResponseKeys.clear();
2637
2994
  this._pendingOrderedMsgs.clear();
2638
2995
  this._v2SenderIKPending.clear();
2639
2996
  this._v2SenderIKFetching.clear();
2640
2997
  this._groupSynced.clear();
2641
- this._onlineUnreadHintQueue.clear();
2642
- if (this._onlineUnreadHintTimer) {
2643
- clearTimeout(this._onlineUnreadHintTimer);
2644
- this._onlineUnreadHintTimer = null;
2645
- }
2646
- this._onlineUnreadHintDrainActive = false;
2647
2998
  this._seqTrackerContext = nextContext;
2648
2999
  }
2649
3000
  /** 将 SeqTracker 状态保存到 keystore */
@@ -2653,6 +3004,9 @@ export class AUNClient {
2653
3004
  _persistSeq(ns, forceSeq) {
2654
3005
  return this._delivery.persistSeq(ns, forceSeq);
2655
3006
  }
3007
+ async _commitSeqTrackerState(ns) {
3008
+ await this._delivery.commitSeqTrackerState(ns);
3009
+ }
2656
3010
  _persistRepairedSeq(ns) {
2657
3011
  return this._delivery.persistRepairedSeq(ns);
2658
3012
  }
@@ -2805,6 +3159,9 @@ export class AUNClient {
2805
3159
  async _getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint) {
2806
3160
  return await this._v2E2EE.getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint);
2807
3161
  }
3162
+ async _fetchV2SenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint) {
3163
+ return await this._v2E2EE.fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint);
3164
+ }
2808
3165
  _cacheV2PeerIKFromDevice(dev, fallbackAid = '') {
2809
3166
  const session = this._v2Session;
2810
3167
  if (!session || !isJsonObject(dev))
@@ -2850,11 +3207,17 @@ export class AUNClient {
2850
3207
  *
2851
3208
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
2852
3209
  */
2853
- async _ackV2(upToSeq) {
2854
- return await this._v2E2EE.ackV2(upToSeq);
3210
+ async _ackV2(upToSeq, inlineGeneration) {
3211
+ return await this._v2E2EE.ackV2(upToSeq, inlineGeneration);
2855
3212
  }
2856
- /** 解密单条 V2 消息(与 Python `_decrypt_v2_message` 对齐)。缺 sender IK 时先入 pending,后台补齐后重试。 */
2857
- async _decryptV2Message(msg, allowPending = true) {
3213
+ /**
3214
+ * 解密单条 V2 消息(与 Python `_decrypt_v2_message` 对齐)。
3215
+ *
3216
+ * 默认保持 Pull / 普通 Push 的既有行为:缺 sender IK 时入 pending、失败发布
3217
+ * undecryptable 事件、成功消费后触发 SPK 轮换。inline Push 在游标提交和
3218
+ * Head/正文绑定校验前调用本入口时必须显式关闭这些副作用。
3219
+ */
3220
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
2858
3221
  const session = this._v2Session;
2859
3222
  if (!session)
2860
3223
  return null;
@@ -2870,7 +3233,8 @@ export class AUNClient {
2870
3233
  return null;
2871
3234
  }
2872
3235
  const e2eeMeta = v2E2eeMeta(envelope);
2873
- await this._observeAgentMdFromEnvelope(envelope);
3236
+ if (observeAgentMd)
3237
+ await this._observeAgentMdFromEnvelope(envelope);
2874
3238
  // 确定 spk_id 和 recipient_key_source
2875
3239
  let spkId = '';
2876
3240
  let recipientKeySource = '';
@@ -2920,100 +3284,127 @@ export class AUNClient {
2920
3284
  }
2921
3285
  catch (exc) {
2922
3286
  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);
3287
+ if (allowPending || deferStatus) {
3288
+ const fromAid = String(msg.from_aid ?? '');
3289
+ const senderDeviceId = String(aad.from_device ?? '');
3290
+ if (deferStatus) {
3291
+ deferStatus.deferred = true;
3292
+ deferStatus.fromAid = fromAid;
3293
+ deferStatus.senderDeviceId = senderDeviceId;
3294
+ deferStatus.groupId = groupIdForKeys;
3295
+ }
3296
+ this._scheduleV2SenderIKPending({
3297
+ msg,
3298
+ fromAid,
3299
+ senderDeviceId,
3300
+ groupId: groupIdForKeys,
3301
+ startFetch: !deferKeyFetch,
3302
+ });
3303
+ return null;
3304
+ }
3305
+ if (emitUndecryptable) {
3306
+ try {
3307
+ const event = {
3308
+ message_id: String(msg.message_id ?? ''),
3309
+ from: String(msg.from_aid ?? ''),
3310
+ to: String(msg.to ?? ''),
3311
+ seq: msg.seq,
3312
+ timestamp: (msg.t_server ?? msg.timestamp),
3313
+ device_id: String(msg.device_id ?? ''),
3314
+ slot_id: String(msg.slot_id ?? ''),
3315
+ _decrypt_error: String(exc),
3316
+ _decrypt_stage: 'spk_lookup',
3317
+ _envelope_type: String(envelope.type ?? ''),
3318
+ _suite: String(envelope.suite ?? ''),
3319
+ _spk_id: spkId,
3320
+ };
3321
+ attachV2EnvelopeMetadata(event, e2eeMeta);
3322
+ await this._dispatcher.publish(undecryptableEvent, event);
3323
+ }
3324
+ catch { /* publish 异常不影响主流程 */ }
2940
3325
  }
2941
- catch { /* publish 异常不影响主流程 */ }
2942
3326
  return null;
2943
3327
  }
2944
3328
  const fromAid = String(msg.from_aid ?? '');
2945
3329
  const senderDeviceId = String(aad.from_device ?? '');
2946
3330
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
2947
- const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
2948
- if (!senderPubDer) {
3331
+ let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
3332
+ if (!senderPubDer && !deferKeyFetch) {
2949
3333
  this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
2950
- if (allowPending) {
2951
- this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
3334
+ senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
3335
+ }
3336
+ if (!senderPubDer) {
3337
+ if (allowPending || deferStatus) {
3338
+ if (deferStatus) {
3339
+ deferStatus.deferred = true;
3340
+ deferStatus.fromAid = fromAid;
3341
+ deferStatus.senderDeviceId = senderDeviceId;
3342
+ deferStatus.groupId = groupIdForKeys;
3343
+ }
3344
+ this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
2952
3345
  return null;
2953
3346
  }
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);
3347
+ if (emitUndecryptable) {
3348
+ try {
3349
+ const event = {
3350
+ message_id: String(msg.message_id ?? ''),
3351
+ from: fromAid,
3352
+ to: String(msg.to ?? ''),
3353
+ seq: msg.seq,
3354
+ timestamp: (msg.t_server ?? msg.timestamp),
3355
+ device_id: String(msg.device_id ?? ''),
3356
+ slot_id: String(msg.slot_id ?? ''),
3357
+ _decrypt_error: 'sender_ik_not_found',
3358
+ _decrypt_stage: 'sender_ik',
3359
+ _envelope_type: String(envelope.type ?? ''),
3360
+ _suite: String(envelope.suite ?? ''),
3361
+ };
3362
+ attachV2EnvelopeMetadata(event, e2eeMeta);
3363
+ await this._dispatcher.publish(undecryptableEvent, event);
3364
+ }
3365
+ catch { /* publish 异常不影响主流程 */ }
2970
3366
  }
2971
- catch { /* publish 异常不影响主流程 */ }
2972
3367
  return null;
2973
3368
  }
2974
3369
  let plaintext;
2975
3370
  try {
2976
3371
  plaintext = await decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
3372
+ if (plaintext === null)
3373
+ throw new Error('recipient_not_found_or_envelope_verification_failed');
2977
3374
  }
2978
3375
  catch (exc) {
2979
3376
  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);
3377
+ if (deferStatus)
3378
+ deferStatus.terminal = true;
3379
+ if (emitUndecryptable) {
3380
+ try {
3381
+ const event = {
3382
+ message_id: String(msg.message_id ?? ''),
3383
+ from: fromAid,
3384
+ to: String(msg.to ?? ''),
3385
+ seq: msg.seq,
3386
+ timestamp: (msg.t_server ?? msg.timestamp),
3387
+ device_id: String(msg.device_id ?? ''),
3388
+ slot_id: String(msg.slot_id ?? ''),
3389
+ _decrypt_error: String(exc),
3390
+ _decrypt_stage: 'decrypt',
3391
+ _envelope_type: String(envelope.type ?? ''),
3392
+ _suite: String(envelope.suite ?? ''),
3393
+ };
3394
+ attachV2EnvelopeMetadata(event, e2eeMeta);
3395
+ await this._dispatcher.publish(undecryptableEvent, event);
3396
+ }
3397
+ catch { /* publish 异常不影响主流程 */ }
2996
3398
  }
2997
- catch { /* publish 异常不影响主流程 */ }
2998
3399
  return null;
2999
3400
  }
3000
3401
  if (plaintext == null)
3001
3402
  return null;
3002
3403
  // 消费触发 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
- }
3404
+ if (rotateKeys)
3405
+ this._scheduleV2KeyRotationForMessage(msg, envelope);
3015
3406
  const e2ee = v2E2eeMeta(envelope);
3016
- const result = {
3407
+ let result = {
3017
3408
  message_id: String(msg.message_id ?? ''),
3018
3409
  from: fromAid,
3019
3410
  to: this._aid ?? '',
@@ -3031,8 +3422,67 @@ export class AUNClient {
3031
3422
  result.slot_id = msg.slot_id;
3032
3423
  attachGatewayProximity(result, msg);
3033
3424
  attachV2EnvelopeMetadata(result, e2ee);
3425
+ if (groupIdForKeys)
3426
+ result = normalizeGroupMentionMode(result, msg, envelope);
3034
3427
  return result;
3035
3428
  }
3429
+ _scheduleV2KeyRotationForMessage(msg, envelope) {
3430
+ const session = this._v2Session;
3431
+ if (!session)
3432
+ return;
3433
+ let spkId = '';
3434
+ let recipientKeySource = '';
3435
+ const recipientObj = envelope.recipient;
3436
+ if (recipientObj && typeof recipientObj === 'object') {
3437
+ spkId = String(recipientObj.spk_id ?? '');
3438
+ recipientKeySource = String(recipientObj.key_source ?? '');
3439
+ }
3440
+ else if (Array.isArray(envelope.recipients)) {
3441
+ spkId = String(msg.spk_id ?? '');
3442
+ for (const row of envelope.recipients) {
3443
+ if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
3444
+ if (!spkId)
3445
+ spkId = String(row[5] ?? '');
3446
+ recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
3447
+ break;
3448
+ }
3449
+ }
3450
+ }
3451
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
3452
+ const groupId = String(msg.group_id ?? aad.group_id ?? envelope.group_id ?? '').trim();
3453
+ if (groupId && recipientKeySource === 'group_device_prekey' && session.isLastUploadedGroupSPK(groupId, spkId)) {
3454
+ this._v2E2EE.scheduleGroupSpkRotation(groupId, { reason: 'group_spk_consumed' });
3455
+ }
3456
+ else if (groupId && recipientKeySource === 'peer_device_prekey') {
3457
+ this._v2E2EE.scheduleGroupSpkRegistrationAfterPeerFallback(groupId);
3458
+ }
3459
+ else if (!groupId && session.isLastUploadedSPK(spkId)) {
3460
+ const callFn = async (method, params) => this.call(method, params);
3461
+ session.rotateSPK(callFn).catch(exc => {
3462
+ this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${exc}`);
3463
+ });
3464
+ }
3465
+ }
3466
+ /** inline 消息仅在应用提交且 ACK 成功后补做可变副作用。 */
3467
+ async _postInlineV2CommitAndAck(msg) {
3468
+ const envJson = msg.envelope_json;
3469
+ if (!envJson || typeof envJson !== 'string')
3470
+ return;
3471
+ let envelope;
3472
+ try {
3473
+ envelope = JSON.parse(envJson);
3474
+ }
3475
+ catch {
3476
+ return;
3477
+ }
3478
+ try {
3479
+ await this._observeAgentMdFromEnvelope(envelope);
3480
+ }
3481
+ catch (exc) {
3482
+ this._clientLog.debug(`inline V2 agent.md observe failed (non-fatal): ${String(exc)}`);
3483
+ }
3484
+ this._scheduleV2KeyRotationForMessage(msg, envelope);
3485
+ }
3036
3486
  /**
3037
3487
  * V2 Group 加密发送(推测性:用缓存 bootstrap 直接发,失败刷新重试一次)。
3038
3488
  *
@@ -3067,8 +3517,8 @@ export class AUNClient {
3067
3517
  * @param groupId 群 ID
3068
3518
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
3069
3519
  */
3070
- async _ackGroupV2(groupId, upToSeq) {
3071
- return await this._v2E2EE.ackGroupV2(groupId, upToSeq);
3520
+ async _ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
3521
+ return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration);
3072
3522
  }
3073
3523
  /**
3074
3524
  * V2 P2P 多设备 wrap envelope 构造(不发送)。
@@ -3175,18 +3625,6 @@ export class AUNClient {
3175
3625
  this._clientLog.warn(`background task exception:${String(exc)}`);
3176
3626
  });
3177
3627
  }
3178
- _explicitGroupCursorParams(params) {
3179
- const value = params._group_cursor_params;
3180
- if (!isJsonObject(value))
3181
- return {};
3182
- return { ...value };
3183
- }
3184
- _groupCursorTargetsCurrentInstance(params) {
3185
- const deviceId = String(params.device_id ?? '').trim();
3186
- const slotId = String(params.slot_id ?? '').trim();
3187
- return (!deviceId || deviceId === (this._deviceId ?? ''))
3188
- && (!slotId || slotId === (this._slotId ?? ''));
3189
- }
3190
3628
  // ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
3191
3629
  async _withBackgroundRpc(operation) {
3192
3630
  this._backgroundRpcDepth += 1;
@@ -3197,13 +3635,56 @@ export class AUNClient {
3197
3635
  this._backgroundRpcDepth = Math.max(0, this._backgroundRpcDepth - 1);
3198
3636
  }
3199
3637
  }
3200
- async _runPullSerialized(key, operation) {
3201
- return await this._rpcPipeline.runPullSerialized(key, operation);
3638
+ async _runPullSerialized(key, operation, background = false) {
3639
+ return await this._rpcPipeline.runPullSerialized(key, operation, background);
3202
3640
  }
3203
- /** 可取消的 sleep */
3204
- _sleep(ms) {
3641
+ async _withPullResponseProcessing(ns, operation) {
3642
+ if (!ns)
3643
+ return await operation();
3644
+ this._pullResponseKeys.set(ns, (this._pullResponseKeys.get(ns) ?? 0) + 1);
3645
+ try {
3646
+ return await operation();
3647
+ }
3648
+ finally {
3649
+ const remaining = (this._pullResponseKeys.get(ns) ?? 1) - 1;
3650
+ if (remaining <= 0)
3651
+ this._pullResponseKeys.delete(ns);
3652
+ else
3653
+ this._pullResponseKeys.set(ns, remaining);
3654
+ }
3655
+ }
3656
+ /** 每轮重连从当前地址之后开始轮转,避免永远黏在失效的首个 Gateway。 */
3657
+ _orderedReconnectGateways(params) {
3658
+ const raw = Array.isArray(params.gateways) && params.gateways.length > 0
3659
+ ? params.gateways
3660
+ : (this._gatewayCandidates.length > 0 ? this._gatewayCandidates : [params.gateway ?? this._gatewayUrl]);
3661
+ const candidates = [...new Set(raw.map((item) => String(item ?? '').trim()).filter(Boolean))];
3662
+ if (candidates.length <= 1)
3663
+ return candidates;
3664
+ const current = String(this._gatewayUrl ?? params.gateway ?? '').trim();
3665
+ const index = candidates.indexOf(current);
3666
+ const start = index >= 0 ? (index + 1) % candidates.length : 0;
3667
+ return [...candidates.slice(start), ...candidates.slice(0, start)];
3668
+ }
3669
+ /** 可取消的 sleep;重连终止时立即释放退避等待。 */
3670
+ _sleep(ms, signal) {
3205
3671
  return new Promise((resolve) => {
3206
- globalThis.setTimeout(resolve, ms);
3672
+ let settled = false;
3673
+ const finish = () => {
3674
+ if (settled)
3675
+ return;
3676
+ settled = true;
3677
+ globalThis.clearTimeout(timer);
3678
+ signal?.removeEventListener('abort', finish);
3679
+ resolve();
3680
+ };
3681
+ const timer = globalThis.setTimeout(finish, ms);
3682
+ if (signal) {
3683
+ if (signal.aborted)
3684
+ finish();
3685
+ else
3686
+ signal.addEventListener('abort', finish, { once: true });
3687
+ }
3207
3688
  });
3208
3689
  }
3209
3690
  }