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