@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.
- package/CHANGELOG.md +177 -67
- 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
- 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
- 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
- package/_packed_docs/CHANGELOG.md +177 -67
- package/_packed_docs/INDEX.md +124 -65
- package/_packed_docs/KITE_DOCS_GUIDE.md +65 -30
- package/_packed_docs/agent.md/SCHEMA.md +83 -58
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
- 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
- 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
- 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
- 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
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
- package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
- package/_packed_docs/message-online-push-alignment.md +572 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
- 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
- package/_packed_docs/protocol/README.md +1 -0
- package/_packed_docs/protocol/aun-docs-guide.md +7 -4
- package/_packed_docs/protocol/index.md +25 -19
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +63 -18
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +93 -84
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +73 -34
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +21 -9
- package/_packed_docs/sdk/09-group-rpc-manual.md +319 -285
- package/_packed_docs/sdk/09-message-rpc-manual.md +144 -103
- package/_packed_docs/sdk/09-payload-reference.md +1 -1
- package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
- package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +34 -23
- package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
- package/_packed_docs/sdk/INDEX.md +45 -37
- package/_packed_docs/sdk/README.md +11 -9
- package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
- 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
- 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
- package/dist/agent-md-schema.d.ts +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +11 -5
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +295 -53
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +7 -7
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +46 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +29 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +31 -11
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +29920 -19719
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +36 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +106 -11
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1925 -374
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +27 -24
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.d.ts +15 -1
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +477 -132
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +7 -0
- package/dist/client/mention-mode.d.ts.map +1 -0
- package/dist/client/mention-mode.js +184 -0
- package/dist/client/mention-mode.js.map +1 -0
- package/dist/client/peers.d.ts +1 -1
- package/dist/client/peers.d.ts.map +1 -1
- package/dist/client/peers.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +34 -2
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +503 -99
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.d.ts.map +1 -1
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +43 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +931 -138
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +46 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +785 -304
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +26 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +142 -60
- package/dist/facades.js.map +1 -1
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.js.map +1 -1
- package/dist/group-index.d.ts +6 -1
- package/dist/group-index.d.ts.map +1 -1
- package/dist/group-index.js +44 -25
- package/dist/group-index.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +10 -5
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +0 -60
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +9 -5
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +3 -1
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +47 -2
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/register-flow.d.ts.map +1 -1
- package/dist/register-flow.js +28 -3
- package/dist/register-flow.js.map +1 -1
- package/dist/seq-tracker.d.ts +28 -8
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +224 -61
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +26 -2
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +399 -11
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +12 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +226 -102
- package/dist/transport.js.map +1 -1
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +3 -4
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +1 -2
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -9
- package/dist/group-resources.d.ts +0 -98
- package/dist/group-resources.d.ts.map +0 -1
- package/dist/group-resources.js +0 -635
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/indexeddb.d.ts +0 -179
- package/dist/keystore/indexeddb.d.ts.map +0 -1
- package/dist/keystore/indexeddb.js +0 -2031
- package/dist/keystore/indexeddb.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -98
- package/dist/namespaces/auth.d.ts.map +0 -1
- package/dist/namespaces/auth.js +0 -992
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -51
- package/dist/namespaces/custody.d.ts.map +0 -1
- package/dist/namespaces/custody.js +0 -302
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -109
- package/dist/namespaces/meta.d.ts.map +0 -1
- package/dist/namespaces/meta.js +0 -549
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
|
1299
|
+
throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
|
|
1192
1300
|
}
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.')
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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?.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
2034
|
-
return this.
|
|
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
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
2212
|
-
//
|
|
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
|
-
|
|
2303
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
-
|
|
2469
|
-
|
|
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; !
|
|
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
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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.
|
|
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.
|
|
2507
|
-
this._reconnectActive = false;
|
|
2820
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2508
2821
|
return;
|
|
2509
|
-
}
|
|
2510
2822
|
// 退避结束,进入 reconnecting 状态
|
|
2511
2823
|
this._state = 'reconnecting';
|
|
2512
|
-
await this.
|
|
2824
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2513
2825
|
state: this._publicState(this._state),
|
|
2514
2826
|
attempt,
|
|
2515
|
-
})
|
|
2516
|
-
|
|
2827
|
+
}))
|
|
2828
|
+
return;
|
|
2829
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2830
|
+
return;
|
|
2831
|
+
// /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
|
|
2517
2832
|
if (this._gatewayUrl) {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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('
|
|
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
|
-
/**
|
|
2857
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2924
|
-
const
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2951
|
-
|
|
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
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
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
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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 (
|
|
3004
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
3204
|
-
|
|
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
|
-
|
|
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
|
}
|