@agentunion/fastaun 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 +126 -90
- 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 +126 -90
- 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.js +8 -2
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -0
- package/dist/aid-store.js +43 -7
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +97 -50
- package/dist/auth.js.map +1 -1
- package/dist/client/delivery.d.ts +70 -7
- package/dist/client/delivery.js +1332 -236
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +25 -22
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +46 -3
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +6 -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.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +20 -1
- package/dist/client/rpc-pipeline.js +332 -73
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -1
- package/dist/client/v2-e2ee.js +625 -105
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.js +332 -167
- package/dist/client.js.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.js +109 -53
- package/dist/facades.js.map +1 -1
- package/dist/group-index.d.ts +5 -0
- package/dist/group-index.js +65 -30
- package/dist/group-index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +12 -0
- package/dist/keystore/aid-db.js +68 -4
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +12 -0
- package/dist/keystore/local-identity-store.d.ts +10 -0
- package/dist/keystore/local-identity-store.js +8 -2
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +10 -0
- package/dist/keystore/local-token-store.js +8 -2
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/register-flow.d.ts +0 -1
- package/dist/register-flow.js +96 -38
- package/dist/register-flow.js.map +1 -1
- package/dist/secret-store/file-store.d.ts +3 -14
- package/dist/secret-store/file-store.js +7 -98
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/secret-store/index.d.ts +1 -4
- package/dist/secret-store/index.js +2 -2
- package/dist/secret-store/index.js.map +1 -1
- package/dist/seq-tracker.d.ts +29 -9
- package/dist/seq-tracker.js +233 -64
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.js +28 -4
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +360 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +19 -0
- package/dist/transport.js +197 -14
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.js +26 -23
- package/dist/v2/e2ee/decrypt.js.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.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 +5 -4
- package/dist/e2ee-group.d.ts +0 -249
- package/dist/e2ee-group.js +0 -1567
- package/dist/e2ee-group.js.map +0 -1
- package/dist/group-resources.d.ts +0 -31
- package/dist/group-resources.js +0 -84
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/file.d.ts +0 -109
- package/dist/keystore/file.js +0 -772
- package/dist/keystore/file.js.map +0 -1
- package/dist/keystore/sqlite-backup.d.ts +0 -44
- package/dist/keystore/sqlite-backup.js +0 -382
- package/dist/keystore/sqlite-backup.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -118
- package/dist/namespaces/auth.js +0 -968
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -48
- package/dist/namespaces/custody.js +0 -313
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -76
- package/dist/namespaces/meta.js +0 -490
- package/dist/namespaces/meta.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -23,7 +23,6 @@ import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, Permiss
|
|
|
23
23
|
import { EventDispatcher } from './events.js';
|
|
24
24
|
import { LocalTokenStore } from './keystore/local-token-store.js';
|
|
25
25
|
import { AUNLogger } from './logger.js';
|
|
26
|
-
import { normalizeGroupId } from './group-id.js';
|
|
27
26
|
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
28
27
|
import { RPCTransport } from './transport.js';
|
|
29
28
|
import { AuthFlow } from './auth.js';
|
|
@@ -32,6 +31,7 @@ import { GroupIndexMetaCache } from './group-index.js';
|
|
|
32
31
|
import { SeqTracker } from './seq-tracker.js';
|
|
33
32
|
import { ClientRuntime } from './client/runtime.js';
|
|
34
33
|
import { MessageDeliveryEngine } from './client/delivery.js';
|
|
34
|
+
import { normalizeGroupMentionMode } from './client/mention-mode.js';
|
|
35
35
|
import { IdentityRuntimeManager } from './client/identity.js';
|
|
36
36
|
import { LifecycleController } from './client/lifecycle.js';
|
|
37
37
|
import { PeerDirectory } from './client/peers.js';
|
|
@@ -103,7 +103,7 @@ function getV2DeviceId(dev) {
|
|
|
103
103
|
}
|
|
104
104
|
const DEFAULT_SESSION_OPTIONS = {
|
|
105
105
|
auto_reconnect: true,
|
|
106
|
-
heartbeat_interval:
|
|
106
|
+
heartbeat_interval: 0.0,
|
|
107
107
|
token_refresh_before: 1800.0,
|
|
108
108
|
retry: {
|
|
109
109
|
initial_delay: 1.0,
|
|
@@ -121,20 +121,6 @@ const RECONNECT_MIN_BASE_DELAY_MS = 1_000;
|
|
|
121
121
|
const RECONNECT_MAX_BASE_DELAY_MS = 64_000;
|
|
122
122
|
const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
|
|
123
123
|
const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
|
|
124
|
-
// 心跳间隔下/上限(秒)。0 = 关闭心跳;负值视为 0;其余值 clamp 到 [10, 600]。
|
|
125
|
-
// 服务端通过 hello.heartbeat_interval 与 meta.ping pong 中的同名字段下发。
|
|
126
|
-
const HEARTBEAT_MIN_INTERVAL_SECONDS = 10;
|
|
127
|
-
const HEARTBEAT_MAX_INTERVAL_SECONDS = 600;
|
|
128
|
-
function clampHeartbeatInterval(value) {
|
|
129
|
-
const n = Number(value);
|
|
130
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
131
|
-
return 0;
|
|
132
|
-
if (n < HEARTBEAT_MIN_INTERVAL_SECONDS)
|
|
133
|
-
return HEARTBEAT_MIN_INTERVAL_SECONDS;
|
|
134
|
-
if (n > HEARTBEAT_MAX_INTERVAL_SECONDS)
|
|
135
|
-
return HEARTBEAT_MAX_INTERVAL_SECONDS;
|
|
136
|
-
return n;
|
|
137
|
-
}
|
|
138
124
|
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
139
125
|
const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
|
|
140
126
|
const NON_IDEMPOTENT_METHODS = new Set([
|
|
@@ -227,7 +213,9 @@ function clampReconnectDelayMs(value, fallback, upper = RECONNECT_MAX_BASE_DELAY
|
|
|
227
213
|
return Math.min(Math.max(ms, RECONNECT_MIN_BASE_DELAY_MS), upper);
|
|
228
214
|
}
|
|
229
215
|
function reconnectSleepDelayMs(baseDelay, maxBaseDelay) {
|
|
230
|
-
|
|
216
|
+
// 抖动只随当前退避基数增长,并限制在 maxBaseDelay 内。
|
|
217
|
+
const jitterLimit = Math.min(Math.max(baseDelay, 0), Math.max(maxBaseDelay - baseDelay, 0));
|
|
218
|
+
return baseDelay + Math.random() * jitterLimit;
|
|
231
219
|
}
|
|
232
220
|
/** peer 证书缓存 TTL(1 小时) */
|
|
233
221
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
@@ -400,7 +388,6 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
400
388
|
if (current?.aid === target) {
|
|
401
389
|
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp))
|
|
402
390
|
return current;
|
|
403
|
-
throw new StateError(`current AID certificate fingerprint mismatch for ${target}`);
|
|
404
391
|
}
|
|
405
392
|
if (!opts.gateway.get()) {
|
|
406
393
|
try {
|
|
@@ -445,6 +432,7 @@ export class AUNClient {
|
|
|
445
432
|
_lastErrorCode = null;
|
|
446
433
|
/** Gateway URL */
|
|
447
434
|
_gatewayUrl = null;
|
|
435
|
+
_gatewayCandidates = [];
|
|
448
436
|
/** 是否正在关闭 */
|
|
449
437
|
_closing = false;
|
|
450
438
|
/** 事件调度器 */
|
|
@@ -662,9 +650,28 @@ export class AUNClient {
|
|
|
662
650
|
// P2P 消息撤回推送:在线 push 与 pull tombstone 去重后再发布给应用层
|
|
663
651
|
this._dispatcher.subscribe('_raw.message.recalled', (data) => this._safeAsync(this._onRawMessageRecalled(data)));
|
|
664
652
|
// V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
|
|
665
|
-
this._dispatcher.subscribe('_raw.peer.v2.message_received', (data) =>
|
|
653
|
+
this._dispatcher.subscribe('_raw.peer.v2.message_received', (data) => {
|
|
654
|
+
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
655
|
+
const hasInline = isJsonObject(data)
|
|
656
|
+
&& isJsonObject(data.inline_message);
|
|
657
|
+
const inlineGeneration = hasInline ? this._delivery.captureInlineAckGeneration() : undefined;
|
|
658
|
+
this._safeAsync(hasInline
|
|
659
|
+
? this._delivery.runPushProcessSerialized(ns, () => this._onV2PushNotification(data, 'v2', inlineGeneration), inlineGeneration)
|
|
660
|
+
: this._onV2PushNotification(data, 'v2'));
|
|
661
|
+
});
|
|
666
662
|
// V2 群组消息推送通知:收到通知后自动走 group.v2.pull 拉取并解密
|
|
667
|
-
this._dispatcher.subscribe('_raw.group.v2.message_created', (data) =>
|
|
663
|
+
this._dispatcher.subscribe('_raw.group.v2.message_created', (data) => {
|
|
664
|
+
const groupId = isJsonObject(data)
|
|
665
|
+
? String(data.group_id ?? '').trim()
|
|
666
|
+
: '';
|
|
667
|
+
const hasInline = isJsonObject(data)
|
|
668
|
+
&& String(data.kind ?? '').trim() !== 'group.online_unread_hint'
|
|
669
|
+
&& isJsonObject(data.inline_message);
|
|
670
|
+
const inlineGeneration = hasInline ? this._delivery.captureInlineAckGeneration() : undefined;
|
|
671
|
+
this._safeAsync(hasInline
|
|
672
|
+
? this._delivery.runPushProcessSerialized(groupId ? `group:${groupId}` : '', () => this._onRawGroupV2MessageCreated(data, 'v2', inlineGeneration), inlineGeneration)
|
|
673
|
+
: this._onRawGroupV2MessageCreated(data, 'v2'));
|
|
674
|
+
});
|
|
668
675
|
// 群组消息推送:自动解密后 re-publish
|
|
669
676
|
this._dispatcher.subscribe('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
|
|
670
677
|
// 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
|
|
@@ -918,8 +925,8 @@ export class AUNClient {
|
|
|
918
925
|
getPeer(aid) {
|
|
919
926
|
return this._peerDirectory.getPeer(aid);
|
|
920
927
|
}
|
|
921
|
-
async lookupPeer(aid) {
|
|
922
|
-
return this._peerDirectory.lookupPeer(aid);
|
|
928
|
+
async lookupPeer(aid, certFingerprint) {
|
|
929
|
+
return this._peerDirectory.lookupPeer(aid, certFingerprint);
|
|
923
930
|
}
|
|
924
931
|
peers() {
|
|
925
932
|
return this._peerDirectory.peers();
|
|
@@ -1420,15 +1427,16 @@ export class AUNClient {
|
|
|
1420
1427
|
}
|
|
1421
1428
|
if (targetGroupId) {
|
|
1422
1429
|
const eventMethod = AUNClient._validateNotifyEventMethod(method);
|
|
1423
|
-
|
|
1424
|
-
if (!normalizedGroupId) {
|
|
1430
|
+
if (!targetGroupId) {
|
|
1425
1431
|
throw new ValidationError('group_id is required for group notify');
|
|
1426
1432
|
}
|
|
1427
1433
|
const routeParams = {
|
|
1428
|
-
|
|
1429
|
-
group_id: normalizedGroupId,
|
|
1434
|
+
group_id: targetGroupId,
|
|
1430
1435
|
deliver: { method: eventMethod, params: payload },
|
|
1431
1436
|
};
|
|
1437
|
+
const explicitGroupAid = String(options.group_aid ?? options.groupAid ?? '').trim();
|
|
1438
|
+
if (explicitGroupAid)
|
|
1439
|
+
routeParams.group_aid = explicitGroupAid;
|
|
1432
1440
|
if (ttl !== undefined)
|
|
1433
1441
|
routeParams.ttl_ms = ttl;
|
|
1434
1442
|
await this._transport.notify('notification/group.route', routeParams);
|
|
@@ -1454,17 +1462,26 @@ export class AUNClient {
|
|
|
1454
1462
|
}
|
|
1455
1463
|
async _callRawV2Rpc(method, params) {
|
|
1456
1464
|
const p = { ...(params ?? {}) };
|
|
1457
|
-
const
|
|
1465
|
+
const forceForeground = Boolean(p._rpc_foreground);
|
|
1466
|
+
const rpcBackground = !forceForeground
|
|
1467
|
+
&& (Boolean(p._rpc_background) || this._backgroundRpcDepth > 0);
|
|
1468
|
+
delete p._rpc_foreground;
|
|
1458
1469
|
delete p._rpc_background;
|
|
1459
1470
|
delete p._pull_gate_locked;
|
|
1460
1471
|
delete p._skip_auto_ack;
|
|
1461
1472
|
delete p.skip_auto_ack;
|
|
1462
1473
|
delete p._group_cursor_params;
|
|
1463
|
-
if (method.startsWith('group.')
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1474
|
+
if (method.startsWith('group.')) {
|
|
1475
|
+
const rawGroupAid = String(p.group_aid ?? p.groupAid ?? '').trim();
|
|
1476
|
+
const rawGroupId = String(p.group_id ?? p.groupId ?? '').trim();
|
|
1477
|
+
if (rawGroupId)
|
|
1478
|
+
p.group_id = rawGroupId;
|
|
1479
|
+
else if (rawGroupAid)
|
|
1480
|
+
p.group_id = rawGroupAid;
|
|
1481
|
+
if (rawGroupAid)
|
|
1482
|
+
p.group_aid = rawGroupAid;
|
|
1483
|
+
delete p.groupAid;
|
|
1484
|
+
delete p.groupId;
|
|
1468
1485
|
}
|
|
1469
1486
|
if (method.startsWith('group.') && p.device_id === undefined) {
|
|
1470
1487
|
p.device_id = this._deviceId;
|
|
@@ -1711,8 +1728,11 @@ export class AUNClient {
|
|
|
1711
1728
|
this._backgroundRpcDepth = Math.max(0, this._backgroundRpcDepth - 1);
|
|
1712
1729
|
}
|
|
1713
1730
|
}
|
|
1714
|
-
async _runPullSerialized(key, operation) {
|
|
1715
|
-
return await this._rpcPipeline.runPullSerialized(key, operation);
|
|
1731
|
+
async _runPullSerialized(key, operation, background = false) {
|
|
1732
|
+
return await this._rpcPipeline.runPullSerialized(key, operation, background);
|
|
1733
|
+
}
|
|
1734
|
+
async _tryRunInlineRealtime(namespace, seq, operation) {
|
|
1735
|
+
return await this._rpcPipeline.tryRunInlineRealtime(namespace, seq, operation);
|
|
1716
1736
|
}
|
|
1717
1737
|
async _tryRunBackgroundPull(key, operation, followupOnMessages = false, onBusy) {
|
|
1718
1738
|
if (key && this._isPullResponseProcessing(key)) {
|
|
@@ -1749,16 +1769,6 @@ export class AUNClient {
|
|
|
1749
1769
|
async _publishPulledMessage(event, ns, seq, payload) {
|
|
1750
1770
|
return this._delivery.publishPulledMessage(event, ns, seq, payload);
|
|
1751
1771
|
}
|
|
1752
|
-
_markPulledSeqDelivered(ns, seq) {
|
|
1753
|
-
// Pull 批次是 after_seq 之后服务端当前可用的结果集,可能跨过永久空洞。
|
|
1754
|
-
// 这里仅在应用层发布返回后推进已交付游标,不能改成 push 的相邻 seq 门控。
|
|
1755
|
-
const seqNum = Number(seq);
|
|
1756
|
-
if (!ns || !Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0)
|
|
1757
|
-
return false;
|
|
1758
|
-
const before = this._seqTracker.getContiguousSeq(ns);
|
|
1759
|
-
this._seqTracker.forceContiguousSeq(ns, seqNum);
|
|
1760
|
-
return this._seqTracker.getContiguousSeq(ns) !== before;
|
|
1761
|
-
}
|
|
1762
1772
|
_markOrderedSeqDelivered(ns, seq) {
|
|
1763
1773
|
if (!ns || !Number.isFinite(seq) || !Number.isInteger(seq) || seq <= 0)
|
|
1764
1774
|
return false;
|
|
@@ -1768,21 +1778,21 @@ export class AUNClient {
|
|
|
1768
1778
|
}
|
|
1769
1779
|
_extractGroupIdFromResult(result) {
|
|
1770
1780
|
const group = isJsonObject(result.group) ? result.group : null;
|
|
1771
|
-
const gid = group ? String(group.group_aid ?? group.
|
|
1781
|
+
const gid = group ? String(group.group_id ?? group.groupId ?? group.group_aid ?? group.groupAid ?? '') : '';
|
|
1772
1782
|
if (gid)
|
|
1773
1783
|
return gid;
|
|
1774
|
-
const directGid = String(result.group_aid ?? result.
|
|
1784
|
+
const directGid = String(result.group_id ?? result.groupId ?? result.group_aid ?? result.groupAid ?? '');
|
|
1775
1785
|
if (directGid)
|
|
1776
1786
|
return directGid;
|
|
1777
1787
|
const member = isJsonObject(result.member) ? result.member : null;
|
|
1778
|
-
return member ? String(member.group_aid ?? member.
|
|
1788
|
+
return member ? String(member.group_id ?? member.groupId ?? member.group_aid ?? member.groupAid ?? '') : '';
|
|
1779
1789
|
}
|
|
1780
1790
|
async _onRawGroupChanged(data) {
|
|
1781
1791
|
const tStart = Date.now();
|
|
1782
1792
|
try {
|
|
1783
1793
|
if (isJsonObject(data)) {
|
|
1784
1794
|
const d = data;
|
|
1785
|
-
const groupId = String(d.
|
|
1795
|
+
const groupId = String(d.group_id ?? d.group_aid ?? '');
|
|
1786
1796
|
const action = String(d.action ?? '');
|
|
1787
1797
|
this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupId}, action=${action}, event_seq=${String(d.event_seq ?? '')}`);
|
|
1788
1798
|
// 验签:有 client_signature 就验,没有默认安全(H20: 严格 boolean)
|
|
@@ -2095,6 +2105,7 @@ export class AUNClient {
|
|
|
2095
2105
|
_resetSeqTrackingState() {
|
|
2096
2106
|
this._saveSeqTrackerState();
|
|
2097
2107
|
this._resetV2IdentityRuntime();
|
|
2108
|
+
this._delivery.resetInlineAckState();
|
|
2098
2109
|
this._seqTracker = new SeqTracker();
|
|
2099
2110
|
this._seqTrackerContext = null;
|
|
2100
2111
|
this._gapFillDone.clear();
|
|
@@ -2116,6 +2127,7 @@ export class AUNClient {
|
|
|
2116
2127
|
if (nextContext === this._seqTrackerContext)
|
|
2117
2128
|
return;
|
|
2118
2129
|
this._saveSeqTrackerState();
|
|
2130
|
+
this._delivery.resetInlineAckState();
|
|
2119
2131
|
this._seqTracker = new SeqTracker();
|
|
2120
2132
|
this._gapFillDone.clear();
|
|
2121
2133
|
this._pushedSeqs.clear();
|
|
@@ -2139,6 +2151,9 @@ export class AUNClient {
|
|
|
2139
2151
|
_persistSeq(ns, forceSeq) {
|
|
2140
2152
|
return this._delivery.persistSeq(ns, forceSeq);
|
|
2141
2153
|
}
|
|
2154
|
+
_commitSeqTrackerState(ns) {
|
|
2155
|
+
return this._delivery.commitSeqTrackerState(ns);
|
|
2156
|
+
}
|
|
2142
2157
|
_persistRepairedSeq(ns) {
|
|
2143
2158
|
return this._delivery.persistRepairedSeq(ns);
|
|
2144
2159
|
}
|
|
@@ -2258,8 +2273,20 @@ export class AUNClient {
|
|
|
2258
2273
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
2259
2274
|
}
|
|
2260
2275
|
}
|
|
2276
|
+
if (!this._transport.isConnected()) {
|
|
2277
|
+
throw new ConnectionError('websocket disconnected during authentication');
|
|
2278
|
+
}
|
|
2279
|
+
this._serverKicked = false;
|
|
2280
|
+
this._lastDisconnectInfo = null;
|
|
2261
2281
|
this._state = 'ready';
|
|
2262
2282
|
this._connectedAt = Date.now();
|
|
2283
|
+
if (Array.isArray(params.gateways)) {
|
|
2284
|
+
this._gatewayCandidates = [...new Set(params.gateways
|
|
2285
|
+
.map((item) => String(item ?? '').trim()).filter(Boolean))];
|
|
2286
|
+
}
|
|
2287
|
+
else if (gatewayUrl) {
|
|
2288
|
+
this._gatewayCandidates = [gatewayUrl];
|
|
2289
|
+
}
|
|
2263
2290
|
this._clientLog.debug(`auth complete, connection ready: aid=${this._aid ?? ''}, gateway=${gatewayUrl}`);
|
|
2264
2291
|
await this._dispatcher.publish('state_change', { state: this._publicState(this._state), gateway: gatewayUrl });
|
|
2265
2292
|
// auth 阶段 aid 可能被 identity 覆盖(上方 this._aid = identity.aid);
|
|
@@ -2434,6 +2461,9 @@ export class AUNClient {
|
|
|
2434
2461
|
async _getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint) {
|
|
2435
2462
|
return await this._v2E2EE.getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint);
|
|
2436
2463
|
}
|
|
2464
|
+
async _fetchV2SenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint) {
|
|
2465
|
+
return await this._v2E2EE.fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint);
|
|
2466
|
+
}
|
|
2437
2467
|
_cacheV2PeerIKFromDevice(dev, fallbackAid = '') {
|
|
2438
2468
|
const session = this._v2Session;
|
|
2439
2469
|
if (!session || !isJsonObject(dev))
|
|
@@ -2489,11 +2519,11 @@ export class AUNClient {
|
|
|
2489
2519
|
return await this._callRawV2Rpc('group.ack_messages', p);
|
|
2490
2520
|
}
|
|
2491
2521
|
/** V2 Group ack。 */
|
|
2492
|
-
async _ackGroupV2(groupId, upToSeq) {
|
|
2493
|
-
return await this._v2E2EE.ackGroupV2(groupId, upToSeq);
|
|
2522
|
+
async _ackGroupV2(groupId, upToSeq, groupAid) {
|
|
2523
|
+
return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid);
|
|
2494
2524
|
}
|
|
2495
2525
|
/** 解密单条 V2 pull 消息。缺 sender IK 时先入 pending,后台补齐后重试。 */
|
|
2496
|
-
async _decryptV2Message(msg, allowPending = true) {
|
|
2526
|
+
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
|
|
2497
2527
|
const session = this._v2Session;
|
|
2498
2528
|
if (!session)
|
|
2499
2529
|
return null;
|
|
@@ -2509,7 +2539,8 @@ export class AUNClient {
|
|
|
2509
2539
|
return null;
|
|
2510
2540
|
}
|
|
2511
2541
|
const e2eeMeta = this._v2E2eeMeta(envelope);
|
|
2512
|
-
|
|
2542
|
+
if (observeAgentMd)
|
|
2543
|
+
this._agentMdManager.observeEnvelope(envelope);
|
|
2513
2544
|
let spkId = '';
|
|
2514
2545
|
let recipientKeySource = '';
|
|
2515
2546
|
if (isJsonObject(envelope.recipient)) {
|
|
@@ -2570,16 +2601,20 @@ export class AUNClient {
|
|
|
2570
2601
|
};
|
|
2571
2602
|
this._attachV2EnvelopeMetadata(event, e2eeMeta);
|
|
2572
2603
|
this._logMessageDebug('decrypt-fail', 'v2.decrypt', undecryptableEvent, event);
|
|
2573
|
-
|
|
2604
|
+
if (emitUndecryptable)
|
|
2605
|
+
await this._dispatcher.publish(undecryptableEvent, event);
|
|
2574
2606
|
return null;
|
|
2575
2607
|
}
|
|
2576
2608
|
this._clientLog.debug(`V2 decrypt key lookup ok: seq=${String(msg.seq ?? '')}, group=${groupIdForKeys || '<p2p>'}, ik_len=${ikPriv.byteLength}, spk_len=${spkPriv?.byteLength ?? 0}`);
|
|
2577
2609
|
const fromAid = String(msg.from_aid ?? '');
|
|
2578
2610
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
2579
2611
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
2580
|
-
|
|
2612
|
+
let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
2581
2613
|
if (!senderPubDer) {
|
|
2582
2614
|
this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
2615
|
+
senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
2616
|
+
}
|
|
2617
|
+
if (!senderPubDer) {
|
|
2583
2618
|
if (allowPending) {
|
|
2584
2619
|
this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
|
|
2585
2620
|
return null;
|
|
@@ -2599,7 +2634,8 @@ export class AUNClient {
|
|
|
2599
2634
|
};
|
|
2600
2635
|
this._attachV2EnvelopeMetadata(event, e2eeMeta);
|
|
2601
2636
|
this._logMessageDebug('decrypt-fail', 'v2.decrypt', undecryptableEvent, event);
|
|
2602
|
-
|
|
2637
|
+
if (emitUndecryptable)
|
|
2638
|
+
await this._dispatcher.publish(undecryptableEvent, event);
|
|
2603
2639
|
return null;
|
|
2604
2640
|
}
|
|
2605
2641
|
let plaintext;
|
|
@@ -2623,29 +2659,18 @@ export class AUNClient {
|
|
|
2623
2659
|
};
|
|
2624
2660
|
this._attachV2EnvelopeMetadata(event, e2eeMeta);
|
|
2625
2661
|
this._logMessageDebug('decrypt-fail', 'v2.decrypt', undecryptableEvent, event);
|
|
2626
|
-
|
|
2662
|
+
if (emitUndecryptable)
|
|
2663
|
+
await this._dispatcher.publish(undecryptableEvent, event);
|
|
2627
2664
|
return null;
|
|
2628
2665
|
}
|
|
2629
2666
|
if (plaintext === null) {
|
|
2630
2667
|
this._clientLog.debug(`V2 decrypt returned null plaintext: seq=${String(msg.seq ?? '')}, group=${groupIdForKeys || '<p2p>'}`);
|
|
2631
2668
|
return null;
|
|
2632
2669
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
this._v2E2EE.scheduleGroupSpkRotation(groupIdForKeys, { reason: 'group_spk_consumed' });
|
|
2636
|
-
}
|
|
2637
|
-
else if (groupIdForKeys && recipientKeySource === 'peer_device_prekey') {
|
|
2638
|
-
this._v2E2EE.scheduleGroupSpkRegistrationAfterPeerFallback(groupIdForKeys);
|
|
2639
|
-
}
|
|
2640
|
-
else if (!groupIdForKeys && session.isLastUploadedSPK(spkId)) {
|
|
2641
|
-
// P2P SPK 消费触发轮换
|
|
2642
|
-
const callFn = async (method, params) => this.call(method, params);
|
|
2643
|
-
session.rotateSPK(callFn).catch(exc => {
|
|
2644
|
-
this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${formatCaughtError(exc)}`);
|
|
2645
|
-
});
|
|
2646
|
-
}
|
|
2670
|
+
if (rotateKeys)
|
|
2671
|
+
this._scheduleV2KeyRotationForMessage(msg, envelope);
|
|
2647
2672
|
const e2ee = this._v2E2eeMeta(envelope);
|
|
2648
|
-
|
|
2673
|
+
let result = {
|
|
2649
2674
|
message_id: String(msg.message_id ?? ''),
|
|
2650
2675
|
from: fromAid,
|
|
2651
2676
|
to: this._aid ?? '',
|
|
@@ -2663,9 +2688,65 @@ export class AUNClient {
|
|
|
2663
2688
|
result.slot_id = msg.slot_id;
|
|
2664
2689
|
attachGatewayProximity(result, msg);
|
|
2665
2690
|
this._attachV2EnvelopeMetadata(result, e2ee);
|
|
2691
|
+
if (groupIdForKeys)
|
|
2692
|
+
result = normalizeGroupMentionMode(result, msg, envelope);
|
|
2666
2693
|
this._logMessageDebug('decrypt-ok', 'v2.decrypt', groupIdForKeys ? 'group.message_created' : 'message.received', result);
|
|
2667
2694
|
return result;
|
|
2668
2695
|
}
|
|
2696
|
+
/** inline 在绑定、游标提交和 ACK 都成功后才调用,避免伪造 Head 过早消耗密钥。 */
|
|
2697
|
+
_postInlineV2CommitAndAck(msg) {
|
|
2698
|
+
const envelopeJson = msg.envelope_json;
|
|
2699
|
+
if (typeof envelopeJson !== 'string' || !envelopeJson)
|
|
2700
|
+
return;
|
|
2701
|
+
try {
|
|
2702
|
+
const envelope = JSON.parse(envelopeJson);
|
|
2703
|
+
this._agentMdManager.observeEnvelope(envelope);
|
|
2704
|
+
this._scheduleV2KeyRotationForMessage(msg, envelope);
|
|
2705
|
+
}
|
|
2706
|
+
catch (exc) {
|
|
2707
|
+
this._clientLog.debug(`inline V2 post-ack rotation skipped: invalid envelope: ${formatCaughtError(exc)}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
_scheduleV2KeyRotationForMessage(msg, envelope) {
|
|
2711
|
+
const session = this._v2Session;
|
|
2712
|
+
if (!session)
|
|
2713
|
+
return;
|
|
2714
|
+
let spkId = '';
|
|
2715
|
+
let recipientKeySource = '';
|
|
2716
|
+
if (isJsonObject(envelope.recipient)) {
|
|
2717
|
+
const recipient = envelope.recipient;
|
|
2718
|
+
spkId = String(recipient.spk_id ?? '');
|
|
2719
|
+
recipientKeySource = String(recipient.key_source ?? '');
|
|
2720
|
+
}
|
|
2721
|
+
else if (Array.isArray(envelope.recipients)) {
|
|
2722
|
+
spkId = String(msg.spk_id ?? '');
|
|
2723
|
+
for (const row of envelope.recipients) {
|
|
2724
|
+
if (!Array.isArray(row) || row.length < 6
|
|
2725
|
+
|| String(row[0] ?? '') !== this._aid
|
|
2726
|
+
|| (String(row[1] ?? '') !== this._deviceId && String(row[1] ?? '') !== ''))
|
|
2727
|
+
continue;
|
|
2728
|
+
if (!spkId)
|
|
2729
|
+
spkId = String(row[5] ?? '');
|
|
2730
|
+
if (row.length > 3)
|
|
2731
|
+
recipientKeySource = String(row[3] ?? '');
|
|
2732
|
+
break;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
2736
|
+
const groupIdForKeys = String(aad.group_id ?? msg.group_id ?? '').trim();
|
|
2737
|
+
if (groupIdForKeys && recipientKeySource === 'group_device_prekey' && session.isLastUploadedGroupSPK(groupIdForKeys, spkId)) {
|
|
2738
|
+
this._v2E2EE.scheduleGroupSpkRotation(groupIdForKeys, { reason: 'group_spk_consumed' });
|
|
2739
|
+
}
|
|
2740
|
+
else if (groupIdForKeys && recipientKeySource === 'peer_device_prekey') {
|
|
2741
|
+
this._v2E2EE.scheduleGroupSpkRegistrationAfterPeerFallback(groupIdForKeys);
|
|
2742
|
+
}
|
|
2743
|
+
else if (!groupIdForKeys && session.isLastUploadedSPK(spkId)) {
|
|
2744
|
+
const callFn = async (method, params) => this.call(method, params);
|
|
2745
|
+
session.rotateSPK(callFn).catch(exc => {
|
|
2746
|
+
this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${formatCaughtError(exc)}`);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2669
2750
|
_v2E2eeMeta(envelope) {
|
|
2670
2751
|
const suite = String(envelope.suite ?? '');
|
|
2671
2752
|
const meta = {
|
|
@@ -2785,8 +2866,8 @@ export class AUNClient {
|
|
|
2785
2866
|
async _v2AutoConfirmPendingProposals() {
|
|
2786
2867
|
return this._groupState.autoConfirmPendingProposals();
|
|
2787
2868
|
}
|
|
2788
|
-
async _onV2PushNotification(data) {
|
|
2789
|
-
return this._delivery.onV2PushNotification(data);
|
|
2869
|
+
async _onV2PushNotification(data, expectedVersion, inlineGeneration) {
|
|
2870
|
+
return this._delivery.onV2PushNotification(data, expectedVersion, inlineGeneration);
|
|
2790
2871
|
}
|
|
2791
2872
|
async _onV2StateProposed(data) {
|
|
2792
2873
|
return this._groupState.onV2StateProposed(data);
|
|
@@ -2797,8 +2878,8 @@ export class AUNClient {
|
|
|
2797
2878
|
async _onV2StateConfirmed(data) {
|
|
2798
2879
|
return this._groupState.onV2StateConfirmed(data);
|
|
2799
2880
|
}
|
|
2800
|
-
async _onRawGroupV2MessageCreated(data) {
|
|
2801
|
-
return this._delivery.onRawGroupV2MessageCreated(data);
|
|
2881
|
+
async _onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
|
|
2882
|
+
return this._delivery.onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration);
|
|
2802
2883
|
}
|
|
2803
2884
|
/** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
|
|
2804
2885
|
async _decryptV2PushMessage(data) {
|
|
@@ -2806,20 +2887,35 @@ export class AUNClient {
|
|
|
2806
2887
|
}
|
|
2807
2888
|
/** 按当前 AID 发现 Gateway;用于 authenticate()/connect() 的新入口。 */
|
|
2808
2889
|
async _resolveGatewayForAid(aid) {
|
|
2890
|
+
const gateways = await this._resolveGatewayCandidatesForAid(aid);
|
|
2891
|
+
return gateways[0];
|
|
2892
|
+
}
|
|
2893
|
+
/** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
|
|
2894
|
+
async _resolveGatewayCandidatesForAid(aid) {
|
|
2809
2895
|
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|
|
2810
2896
|
if (!resolvedAid) {
|
|
2811
2897
|
throw new StateError('gateway discovery requires a loaded AID');
|
|
2812
2898
|
}
|
|
2813
|
-
if (this.
|
|
2814
|
-
return this.
|
|
2899
|
+
if (this._gatewayCandidates.length > 0)
|
|
2900
|
+
return [...this._gatewayCandidates];
|
|
2901
|
+
if (this._gatewayUrl) {
|
|
2902
|
+
this._gatewayCandidates = [this._gatewayUrl];
|
|
2903
|
+
return [...this._gatewayCandidates];
|
|
2904
|
+
}
|
|
2815
2905
|
try {
|
|
2816
2906
|
const loadMetadata = this._tokenStore.loadMetadata;
|
|
2817
|
-
const
|
|
2818
|
-
?
|
|
2819
|
-
:
|
|
2907
|
+
const metadata = typeof loadMetadata === 'function'
|
|
2908
|
+
? loadMetadata.call(this._tokenStore, resolvedAid)
|
|
2909
|
+
: null;
|
|
2910
|
+
const cachedList = isJsonObject(metadata) && Array.isArray(metadata.gateway_urls)
|
|
2911
|
+
? metadata.gateway_urls.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
2912
|
+
: [];
|
|
2913
|
+
const cachedGateway = cachedList[0]
|
|
2914
|
+
?? (isJsonObject(metadata) ? String(metadata.gateway_url ?? '').trim() : '');
|
|
2820
2915
|
if (cachedGateway) {
|
|
2821
|
-
this.
|
|
2822
|
-
|
|
2916
|
+
this._gatewayCandidates = [...new Set([cachedGateway, ...cachedList])];
|
|
2917
|
+
this._gatewayUrl = this._gatewayCandidates[0];
|
|
2918
|
+
return [...this._gatewayCandidates];
|
|
2823
2919
|
}
|
|
2824
2920
|
}
|
|
2825
2921
|
catch {
|
|
@@ -2830,22 +2926,38 @@ export class AUNClient {
|
|
|
2830
2926
|
const portSuffix = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
|
|
2831
2927
|
const aidUrl = `https://${resolvedAid}${portSuffix}/.well-known/aun-gateway`;
|
|
2832
2928
|
const gatewayDomainUrl = `https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`;
|
|
2833
|
-
const candidates =
|
|
2929
|
+
const candidates = [aidUrl, gatewayDomainUrl];
|
|
2834
2930
|
let lastErr = null;
|
|
2835
2931
|
for (const url of candidates) {
|
|
2836
2932
|
try {
|
|
2837
|
-
|
|
2933
|
+
// discoverAll 是生产路径;若调用方注入了旧版/测试 discovery
|
|
2934
|
+
// 替身而只提供 discover,则保留单候选兼容,不影响多候选发现。
|
|
2935
|
+
const discovery = this._discovery;
|
|
2936
|
+
const hasInjectedDiscover = Object.prototype.hasOwnProperty.call(discovery, 'discover')
|
|
2937
|
+
&& typeof discovery.discover === 'function';
|
|
2938
|
+
const discovered = hasInjectedDiscover
|
|
2939
|
+
? [await discovery.discover(url)]
|
|
2940
|
+
: await discovery.discoverAll(url);
|
|
2941
|
+
const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
|
|
2942
|
+
if (gatewayUrls.length === 0)
|
|
2943
|
+
throw new ValidationError('gateway discovery returned no candidates');
|
|
2944
|
+
this._gatewayCandidates = gatewayUrls;
|
|
2945
|
+
const gateway = gatewayUrls[0];
|
|
2838
2946
|
this._gatewayUrl = gateway;
|
|
2839
2947
|
try {
|
|
2840
2948
|
const saveMetadata = this._tokenStore.saveMetadata;
|
|
2841
2949
|
if (typeof saveMetadata === 'function') {
|
|
2842
|
-
saveMetadata.call(this._tokenStore, resolvedAid, {
|
|
2950
|
+
saveMetadata.call(this._tokenStore, resolvedAid, {
|
|
2951
|
+
gateway_url: gateway,
|
|
2952
|
+
gateway_urls: gatewayUrls,
|
|
2953
|
+
gateway_cached_at: Date.now(),
|
|
2954
|
+
});
|
|
2843
2955
|
}
|
|
2844
2956
|
}
|
|
2845
2957
|
catch {
|
|
2846
2958
|
// 缓存写入失败不影响连接。
|
|
2847
2959
|
}
|
|
2848
|
-
return
|
|
2960
|
+
return [...gatewayUrls];
|
|
2849
2961
|
}
|
|
2850
2962
|
catch (err) {
|
|
2851
2963
|
lastErr = err;
|
|
@@ -2856,6 +2968,9 @@ export class AUNClient {
|
|
|
2856
2968
|
}
|
|
2857
2969
|
/** 从参数中解析 Gateway URL */
|
|
2858
2970
|
_resolveGateway(params) {
|
|
2971
|
+
const explicitGateway = String(params.gateway ?? '').trim();
|
|
2972
|
+
if (explicitGateway)
|
|
2973
|
+
return explicitGateway;
|
|
2859
2974
|
const gateways = this._resolveGateways(params);
|
|
2860
2975
|
return gateways[0];
|
|
2861
2976
|
}
|
|
@@ -2872,7 +2987,9 @@ export class AUNClient {
|
|
|
2872
2987
|
throw new ValidationError('relay topology is not implemented in the TypeScript SDK');
|
|
2873
2988
|
}
|
|
2874
2989
|
}
|
|
2875
|
-
const gw = params.
|
|
2990
|
+
const gw = Array.isArray(params.gateways) && params.gateways.length > 0
|
|
2991
|
+
? params.gateways
|
|
2992
|
+
: params.gateway;
|
|
2876
2993
|
if (Array.isArray(gw)) {
|
|
2877
2994
|
const urls = gw.map(g => String(g ?? '')).filter(u => u.length > 0);
|
|
2878
2995
|
if (urls.length > 0)
|
|
@@ -2907,7 +3024,13 @@ export class AUNClient {
|
|
|
2907
3024
|
if (!accessToken && opts.requireAccessToken === true) {
|
|
2908
3025
|
throw new StateError('connect requires non-empty access_token');
|
|
2909
3026
|
}
|
|
2910
|
-
const
|
|
3027
|
+
const rawGateways = request.gateways;
|
|
3028
|
+
const normalizedGateways = Array.isArray(rawGateways)
|
|
3029
|
+
? [...new Set(rawGateways.map((item) => String(item ?? '').trim()).filter(Boolean))]
|
|
3030
|
+
: [];
|
|
3031
|
+
if (normalizedGateways.length > 0)
|
|
3032
|
+
request.gateways = normalizedGateways;
|
|
3033
|
+
const gateway = String(request.gateway ?? normalizedGateways[0] ?? this._gatewayUrl ?? '');
|
|
2911
3034
|
if (!gateway)
|
|
2912
3035
|
throw new StateError('connect requires non-empty gateway');
|
|
2913
3036
|
if (accessToken)
|
|
@@ -2969,8 +3092,6 @@ export class AUNClient {
|
|
|
2969
3092
|
};
|
|
2970
3093
|
if ('auto_reconnect' in params)
|
|
2971
3094
|
options.auto_reconnect = Boolean(params.auto_reconnect);
|
|
2972
|
-
if ('heartbeat_interval' in params)
|
|
2973
|
-
options.heartbeat_interval = Number(params.heartbeat_interval);
|
|
2974
3095
|
if ('token_refresh_before' in params)
|
|
2975
3096
|
options.token_refresh_before = Number(params.token_refresh_before);
|
|
2976
3097
|
if ('retry' in params && isJsonObject(params.retry)) {
|
|
@@ -2986,10 +3107,9 @@ export class AUNClient {
|
|
|
2986
3107
|
// ── 内部:后台任务 ────────────────────────────────────────
|
|
2987
3108
|
/** 启动所有后台任务 */
|
|
2988
3109
|
_startBackgroundTasks() {
|
|
2989
|
-
//
|
|
2990
|
-
//
|
|
3110
|
+
// WebSocket Ping/Pong 由 Gateway 与传输层处理;SDK 不发送 RPC 心跳。
|
|
3111
|
+
// 短连接生命周期短,不需要 token 刷新。
|
|
2991
3112
|
if (this._sessionOptions.connection_kind !== 'short') {
|
|
2992
|
-
this._startHeartbeatTask();
|
|
2993
3113
|
this._startTokenRefreshTask();
|
|
2994
3114
|
}
|
|
2995
3115
|
this._startV2MaintenanceTasks();
|
|
@@ -3011,56 +3131,19 @@ export class AUNClient {
|
|
|
3011
3131
|
}
|
|
3012
3132
|
/** 启动心跳任务 */
|
|
3013
3133
|
_startHeartbeatTask() {
|
|
3014
|
-
if (this._heartbeatTimer !== null)
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
if (interval <= 0)
|
|
3018
|
-
return;
|
|
3019
|
-
// M25: 把连续失败阈值从 3 次收窄到 2 次。既能容忍一次网络抖动/GC 暂停,
|
|
3020
|
-
// 又把半开连接的检测延迟从 3 个心跳周期降到 2 个。
|
|
3021
|
-
// 真正的 socket 死亡由 ws.on('close') 立即触发 _handleTransportDisconnect,
|
|
3022
|
-
// 不依赖此心跳路径。
|
|
3023
|
-
let consecutiveFailures = 0;
|
|
3024
|
-
const maxFailures = 2;
|
|
3025
|
-
this._heartbeatTimer = setInterval(() => {
|
|
3026
|
-
if (this._closing || this.state !== ConnectionState.READY)
|
|
3027
|
-
return;
|
|
3028
|
-
this._transport.call('meta.ping', {}).then((pong) => {
|
|
3029
|
-
consecutiveFailures = 0;
|
|
3030
|
-
// 服务端可在 pong 中下发新的 heartbeat_interval(秒,0=关闭)
|
|
3031
|
-
if (isJsonObject(pong) && 'heartbeat_interval' in pong) {
|
|
3032
|
-
this._applyServerHeartbeatInterval(pong.heartbeat_interval, 'pong');
|
|
3033
|
-
}
|
|
3034
|
-
}).catch((exc) => {
|
|
3035
|
-
consecutiveFailures++;
|
|
3036
|
-
this._clientLog.warn(`heartbeat failed (${consecutiveFailures}/${maxFailures}): ${formatCaughtError(exc)}`);
|
|
3037
|
-
this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) }).catch(() => { });
|
|
3038
|
-
if (consecutiveFailures >= maxFailures) {
|
|
3039
|
-
this._clientLog.warn(`${maxFailures} consecutive heartbeat failures, triggering reconnect`);
|
|
3040
|
-
this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
|
|
3041
|
-
}
|
|
3042
|
-
});
|
|
3043
|
-
}, interval * 1000);
|
|
3044
|
-
// 允许 Node.js 进程在只剩定时器时退出
|
|
3045
|
-
if (this._heartbeatTimer && typeof this._heartbeatTimer === 'object' && 'unref' in this._heartbeatTimer) {
|
|
3046
|
-
this._heartbeatTimer.unref();
|
|
3134
|
+
if (this._heartbeatTimer !== null) {
|
|
3135
|
+
clearInterval(this._heartbeatTimer);
|
|
3136
|
+
this._heartbeatTimer = null;
|
|
3047
3137
|
}
|
|
3138
|
+
this._sessionOptions.heartbeat_interval = 0;
|
|
3048
3139
|
}
|
|
3049
|
-
/**
|
|
3050
|
-
_applyServerHeartbeatInterval(
|
|
3051
|
-
const newInterval = clampHeartbeatInterval(raw);
|
|
3052
|
-
const oldInterval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval);
|
|
3053
|
-
if (newInterval === oldInterval)
|
|
3054
|
-
return;
|
|
3055
|
-
this._sessionOptions.heartbeat_interval = newInterval;
|
|
3056
|
-
this._clientLog.debug(`heartbeat_interval updated by ${source}: ${oldInterval} -> ${newInterval}`);
|
|
3140
|
+
/** 忽略旧 Gateway 的 RPC 心跳配置,固定使用 WebSocket Ping/Pong。 */
|
|
3141
|
+
_applyServerHeartbeatInterval(_raw, _source) {
|
|
3057
3142
|
if (this._heartbeatTimer !== null) {
|
|
3058
3143
|
clearInterval(this._heartbeatTimer);
|
|
3059
3144
|
this._heartbeatTimer = null;
|
|
3060
3145
|
}
|
|
3061
|
-
|
|
3062
|
-
this._startHeartbeatTask();
|
|
3063
|
-
}
|
|
3146
|
+
this._sessionOptions.heartbeat_interval = 0;
|
|
3064
3147
|
}
|
|
3065
3148
|
/** 启动 token 刷新任务 */
|
|
3066
3149
|
_startTokenRefreshTask() {
|
|
@@ -3194,7 +3277,7 @@ export class AUNClient {
|
|
|
3194
3277
|
}
|
|
3195
3278
|
// ── 内部:断线重连 ────────────────────────────────────────
|
|
3196
3279
|
/** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
|
|
3197
|
-
static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011,
|
|
3280
|
+
static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
|
|
3198
3281
|
/** 处理服务端主动断开通知 event/gateway.disconnect。
|
|
3199
3282
|
*
|
|
3200
3283
|
* 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
|
|
@@ -3208,7 +3291,11 @@ export class AUNClient {
|
|
|
3208
3291
|
? payload.detail
|
|
3209
3292
|
: {};
|
|
3210
3293
|
this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
|
|
3211
|
-
|
|
3294
|
+
const reasonText = String(reason ?? '').trim().toLowerCase();
|
|
3295
|
+
const retryable = Number(code) === 4012
|
|
3296
|
+
|| reasonText.includes('long_connection_already_exists')
|
|
3297
|
+
|| reasonText.includes('long connection already exists');
|
|
3298
|
+
this._serverKicked = !retryable;
|
|
3212
3299
|
// 缓存最近一次 disconnect 信息,让后续 connection.state(connection_failed) 也能带 detail
|
|
3213
3300
|
this._lastDisconnectInfo = { code, reason, detail };
|
|
3214
3301
|
// 透传给应用层订阅者
|
|
@@ -3230,21 +3317,27 @@ export class AUNClient {
|
|
|
3230
3317
|
// 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
|
|
3231
3318
|
if (this._reconnectActive)
|
|
3232
3319
|
return;
|
|
3320
|
+
// 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
|
|
3321
|
+
this._delivery.resetInlineAckState();
|
|
3233
3322
|
// 原子化标志,防止 await 期间并发调用再次进入
|
|
3234
3323
|
this._reconnectActive = true;
|
|
3235
3324
|
this._clientLog.warn(`transport disconnected: closeCode=${closeCode ?? 'none'}, error=${error ? formatCaughtError(error) : 'none'}`);
|
|
3236
3325
|
this._state = 'standby';
|
|
3237
3326
|
this._stopBackgroundTasks();
|
|
3238
|
-
|
|
3327
|
+
try {
|
|
3328
|
+
await this._transport.close();
|
|
3329
|
+
}
|
|
3330
|
+
catch (exc) {
|
|
3239
3331
|
this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError(exc)}`);
|
|
3240
|
-
}
|
|
3332
|
+
}
|
|
3241
3333
|
await this._dispatcher.publish('state_change', { state: this._publicState(this._state), error });
|
|
3242
3334
|
if (!this._sessionOptions.auto_reconnect) {
|
|
3243
3335
|
this._reconnectActive = false;
|
|
3244
3336
|
return;
|
|
3245
3337
|
}
|
|
3246
3338
|
// 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
|
|
3247
|
-
|
|
3339
|
+
const retryableClose = closeCode === 4012;
|
|
3340
|
+
if ((this._serverKicked && !retryableClose) || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
|
|
3248
3341
|
this._state = 'connection_failed';
|
|
3249
3342
|
const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
|
|
3250
3343
|
this._clientLog.warn(`suppressing auto-reconnect: ${reason}`);
|
|
@@ -3268,26 +3361,47 @@ export class AUNClient {
|
|
|
3268
3361
|
this._reconnectActive = false;
|
|
3269
3362
|
this._startReconnect(serverInitiated);
|
|
3270
3363
|
}
|
|
3271
|
-
/** 启动重连循环(默认无限重试 +
|
|
3364
|
+
/** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
|
|
3272
3365
|
_startReconnect(serverInitiated = false) {
|
|
3273
3366
|
if (this._reconnectActive)
|
|
3274
3367
|
return;
|
|
3275
3368
|
this._reconnectActive = true;
|
|
3276
|
-
|
|
3369
|
+
const reconnectAbort = new AbortController();
|
|
3370
|
+
this._reconnectAbort = reconnectAbort;
|
|
3277
3371
|
this._clientLog.debug(`reconnect loop started: serverInitiated=${String(serverInitiated)}, aid=${this._aid ?? ''}`);
|
|
3278
|
-
this._reconnectLoop(serverInitiated).catch((exc) => {
|
|
3372
|
+
this._reconnectLoop(serverInitiated, reconnectAbort).catch((exc) => {
|
|
3279
3373
|
this._clientLog.warn(`reconnect loop error: ${formatCaughtError(exc)}`);
|
|
3280
3374
|
});
|
|
3281
3375
|
}
|
|
3282
|
-
/**
|
|
3283
|
-
async _reconnectLoop(serverInitiated) {
|
|
3376
|
+
/** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
|
|
3377
|
+
async _reconnectLoop(serverInitiated, reconnectAbort = this._reconnectAbort) {
|
|
3378
|
+
let releasedOwnGeneration = false;
|
|
3379
|
+
try {
|
|
3380
|
+
await this._runReconnectLoop(serverInitiated, reconnectAbort);
|
|
3381
|
+
}
|
|
3382
|
+
finally {
|
|
3383
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
3384
|
+
this._nextRetryAt = null;
|
|
3385
|
+
this._reconnectActive = false;
|
|
3386
|
+
this._reconnectAbort = null;
|
|
3387
|
+
releasedOwnGeneration = true;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
// 认证成功到重连代际释放之间若再次断线,断线回调会因 _reconnectActive
|
|
3391
|
+
// 被合并;释放后必须复核底层连接,避免留下“状态 ready、实际离线”。
|
|
3392
|
+
if (releasedOwnGeneration && !this._closing && this._state === 'ready' && !this._transport.isConnected()) {
|
|
3393
|
+
await this._handleTransportDisconnect(new ConnectionError('websocket disconnected while reconnect completed'));
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
/** 重连循环主体(for 循环 + AbortController,与 JS/Python 对齐) */
|
|
3397
|
+
async _runReconnectLoop(serverInitiated, reconnectAbort) {
|
|
3284
3398
|
const retry = this._sessionOptions.retry;
|
|
3285
3399
|
const maxBaseDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64.0) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
|
|
3286
3400
|
const maxAttemptsRaw = Number(retry.max_attempts ?? 0);
|
|
3287
3401
|
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
|
|
3288
3402
|
this._retryMaxAttempts = maxAttempts;
|
|
3289
3403
|
let delay = clampReconnectDelayMs(serverInitiated ? 16_000 : Number(retry.initial_delay ?? 1.0) * 1000, serverInitiated ? 16_000 : RECONNECT_MIN_BASE_DELAY_MS, maxBaseDelay);
|
|
3290
|
-
for (let attempt = 1; !
|
|
3404
|
+
for (let attempt = 1; !reconnectAbort?.signal.aborted; attempt++) {
|
|
3291
3405
|
if (this._closing)
|
|
3292
3406
|
break;
|
|
3293
3407
|
// max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
|
|
@@ -3309,22 +3423,25 @@ export class AUNClient {
|
|
|
3309
3423
|
next_retry_at: this._nextRetryAt,
|
|
3310
3424
|
});
|
|
3311
3425
|
try {
|
|
3312
|
-
//
|
|
3313
|
-
await this._sleep(Math.max(0, this._nextRetryAt - Date.now()));
|
|
3314
|
-
if (
|
|
3426
|
+
// 抖动随当前 base 增长且不超过 max_base,避免首次重连被放大到几十秒。
|
|
3427
|
+
await this._sleep(Math.max(0, this._nextRetryAt - Date.now()), reconnectAbort?.signal);
|
|
3428
|
+
if (reconnectAbort?.signal.aborted || this._closing)
|
|
3315
3429
|
break;
|
|
3316
3430
|
this._state = 'reconnecting';
|
|
3317
3431
|
await this._dispatcher.publish('state_change', {
|
|
3318
3432
|
state: this._publicState(this._state),
|
|
3319
3433
|
attempt,
|
|
3320
3434
|
});
|
|
3321
|
-
//
|
|
3435
|
+
// /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
|
|
3322
3436
|
if (this._gatewayUrl) {
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
}
|
|
3437
|
+
void this._discovery.checkHealth(this._gatewayUrl, 5_000)
|
|
3438
|
+
.then((healthy) => {
|
|
3439
|
+
if (!healthy)
|
|
3440
|
+
this._clientLog.warn(`reconnect health advisory failed: gateway=${this._gatewayUrl}`);
|
|
3441
|
+
})
|
|
3442
|
+
.catch((exc) => {
|
|
3443
|
+
this._clientLog.debug(`reconnect health advisory error: ${formatCaughtError(exc)}`);
|
|
3444
|
+
});
|
|
3328
3445
|
}
|
|
3329
3446
|
await this._transport.close();
|
|
3330
3447
|
if (this._sessionParams === null) {
|
|
@@ -3348,12 +3465,35 @@ export class AUNClient {
|
|
|
3348
3465
|
this._sessionParams.access_token = '';
|
|
3349
3466
|
}
|
|
3350
3467
|
}
|
|
3351
|
-
|
|
3468
|
+
const candidates = this._orderedReconnectGateways(this._sessionParams);
|
|
3469
|
+
let lastGatewayError = null;
|
|
3470
|
+
let connected = false;
|
|
3471
|
+
for (const gateway of candidates) {
|
|
3472
|
+
const attemptParams = { ...this._sessionParams, gateway };
|
|
3473
|
+
try {
|
|
3474
|
+
await this._connectOnce(attemptParams, true);
|
|
3475
|
+
if (!this._transport.isConnected()) {
|
|
3476
|
+
throw new ConnectionError('websocket disconnected while reconnecting');
|
|
3477
|
+
}
|
|
3478
|
+
this._sessionParams.gateway = gateway;
|
|
3479
|
+
connected = true;
|
|
3480
|
+
break;
|
|
3481
|
+
}
|
|
3482
|
+
catch (exc) {
|
|
3483
|
+
lastGatewayError = exc;
|
|
3484
|
+
await this._transport.close().catch(() => { });
|
|
3485
|
+
if (candidates.length > 1) {
|
|
3486
|
+
this._clientLog.warn(`reconnect gateway failed, trying next: gateway=${gateway}, error=${formatCaughtError(exc)}`);
|
|
3487
|
+
}
|
|
3488
|
+
if (!AUNClient._shouldRetryReconnect(exc))
|
|
3489
|
+
throw exc;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (!connected)
|
|
3493
|
+
throw (lastGatewayError instanceof Error ? lastGatewayError : new ConnectionError('all gateway candidates failed'));
|
|
3352
3494
|
// 重连成功,退出循环
|
|
3353
3495
|
this._clientLog.debug(`reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
|
|
3354
3496
|
this._nextRetryAt = null;
|
|
3355
|
-
this._reconnectActive = false;
|
|
3356
|
-
this._reconnectAbort = null;
|
|
3357
3497
|
return;
|
|
3358
3498
|
}
|
|
3359
3499
|
catch (exc) {
|
|
@@ -3373,17 +3513,39 @@ export class AUNClient {
|
|
|
3373
3513
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
3374
3514
|
}
|
|
3375
3515
|
}
|
|
3376
|
-
|
|
3377
|
-
|
|
3516
|
+
}
|
|
3517
|
+
/** 每轮重连从当前地址之后开始轮转,避免永远黏在失效的首个 Gateway。 */
|
|
3518
|
+
_orderedReconnectGateways(params) {
|
|
3519
|
+
const raw = Array.isArray(params.gateways) && params.gateways.length > 0
|
|
3520
|
+
? params.gateways
|
|
3521
|
+
: (this._gatewayCandidates.length > 0 ? this._gatewayCandidates : [params.gateway ?? this._gatewayUrl]);
|
|
3522
|
+
const candidates = [...new Set(raw.map((item) => String(item ?? '').trim()).filter(Boolean))];
|
|
3523
|
+
if (candidates.length <= 1)
|
|
3524
|
+
return candidates;
|
|
3525
|
+
const current = String(this._gatewayUrl ?? params.gateway ?? '').trim();
|
|
3526
|
+
const index = candidates.indexOf(current);
|
|
3527
|
+
const start = index >= 0 ? (index + 1) % candidates.length : 0;
|
|
3528
|
+
return [...candidates.slice(start), ...candidates.slice(0, start)];
|
|
3378
3529
|
}
|
|
3379
3530
|
/** 可取消的 sleep,监听 _reconnectAbort.signal,abort 时提前 resolve */
|
|
3380
|
-
_sleep(ms) {
|
|
3531
|
+
_sleep(ms, signal) {
|
|
3381
3532
|
return new Promise((resolve) => {
|
|
3382
|
-
|
|
3533
|
+
let settled = false;
|
|
3534
|
+
const finish = () => {
|
|
3535
|
+
if (settled)
|
|
3536
|
+
return;
|
|
3537
|
+
settled = true;
|
|
3538
|
+
clearTimeout(timer);
|
|
3539
|
+
signal?.removeEventListener('abort', finish);
|
|
3540
|
+
resolve();
|
|
3541
|
+
};
|
|
3542
|
+
const timer = setTimeout(finish, ms);
|
|
3383
3543
|
this._unrefTimer(timer);
|
|
3384
|
-
const signal = this._reconnectAbort?.signal;
|
|
3385
3544
|
if (signal) {
|
|
3386
|
-
signal.
|
|
3545
|
+
if (signal.aborted)
|
|
3546
|
+
finish();
|
|
3547
|
+
else
|
|
3548
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
3387
3549
|
}
|
|
3388
3550
|
});
|
|
3389
3551
|
}
|
|
@@ -3399,7 +3561,10 @@ export class AUNClient {
|
|
|
3399
3561
|
static _shouldRetryReconnect(error) {
|
|
3400
3562
|
if (error instanceof AuthError) {
|
|
3401
3563
|
const message = String(error.message ?? '').toLowerCase();
|
|
3402
|
-
if (message.includes('
|
|
3564
|
+
if (message.includes('long_connection_already_exists')
|
|
3565
|
+
|| message.includes('long connection already exists')
|
|
3566
|
+
|| message.includes('aid_login1_failed')
|
|
3567
|
+
|| message.includes('aid_login2_failed')) {
|
|
3403
3568
|
return true;
|
|
3404
3569
|
}
|
|
3405
3570
|
return false;
|