@agentunion/fastaun 0.5.6 → 0.5.9
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 +106 -0
- package/_packed_docs/CHANGELOG.md +106 -0
- package/_packed_docs/INDEX.md +12 -11
- package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
- package/_packed_docs/agent.md/SCHEMA.md +83 -58
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
- 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 +3 -3
- 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 +7 -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 +422 -331
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
- package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
- package/_packed_docs/message-online-push-alignment.md +572 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
- package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
- package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
- package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
- package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
- package/_packed_docs/sdk/INDEX.md +22 -22
- package/_packed_docs/sdk/README.md +6 -5
- package/dist/agent-md-schema.d.ts +16 -0
- package/dist/agent-md-schema.js +109 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.js +91 -9
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +5 -1
- package/dist/aid-store.js +25 -6
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.js +32 -12
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +65 -13
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +2 -1
- package/dist/cert-utils.js +32 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +36 -7
- package/dist/client/delivery.js +701 -216
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +2 -2
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.d.ts +15 -1
- package/dist/client/lifecycle.js +475 -152
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +24 -6
- package/dist/client/rpc-pipeline.js +372 -131
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -3
- package/dist/client/v2-e2ee.js +376 -93
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -10
- package/dist/client.js +515 -150
- package/dist/client.js.map +1 -1
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -0
- package/dist/discovery.js.map +1 -1
- package/dist/facades.d.ts +23 -1
- package/dist/facades.js +59 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/net.d.ts +3 -0
- package/dist/net.js +13 -0
- package/dist/net.js.map +1 -1
- package/dist/seq-tracker.js +6 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +29 -3
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.js +8 -1
- package/dist/transport.js.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/dist/client.js
CHANGED
|
@@ -16,17 +16,17 @@ import * as https from 'node:https';
|
|
|
16
16
|
import { URL } from 'node:url';
|
|
17
17
|
import { configFromMap, getDeviceId, normalizeSlotId } from './config.js';
|
|
18
18
|
import { CryptoProvider } from './crypto.js';
|
|
19
|
-
import { AIDStore } from './aid-store.js';
|
|
20
19
|
import { GatewayDiscovery } from './discovery.js';
|
|
21
20
|
import { DnsResilientNet } from './net.js';
|
|
22
21
|
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
|
|
23
22
|
import { EventDispatcher } from './events.js';
|
|
24
23
|
import { LocalTokenStore } from './keystore/local-token-store.js';
|
|
25
24
|
import { AUNLogger } from './logger.js';
|
|
25
|
+
import { normalizeGroupId } from './group-id.js';
|
|
26
26
|
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
27
27
|
import { RPCTransport } from './transport.js';
|
|
28
28
|
import { AuthFlow } from './auth.js';
|
|
29
|
-
import { AgentMdManager } from './agent-md.js';
|
|
29
|
+
import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
|
|
30
30
|
import { GroupIndexMetaCache } from './group-index.js';
|
|
31
31
|
import { SeqTracker } from './seq-tracker.js';
|
|
32
32
|
import { ClientRuntime } from './client/runtime.js';
|
|
@@ -37,7 +37,7 @@ import { LifecycleController } from './client/lifecycle.js';
|
|
|
37
37
|
import { PeerDirectory } from './client/peers.js';
|
|
38
38
|
import { RpcPipeline } from './client/rpc-pipeline.js';
|
|
39
39
|
import { CollabClient } from './collab/client.js';
|
|
40
|
-
import { GroupFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
40
|
+
import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
41
41
|
import { StorageVFS } from './storage/vfs.js';
|
|
42
42
|
import { V2E2EECoordinator } from './client/v2-e2ee.js';
|
|
43
43
|
import { GroupStateCoordinator } from './client/group-state.js';
|
|
@@ -45,7 +45,7 @@ import { decryptMessage, } from './v2/e2ee/index.js';
|
|
|
45
45
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
46
46
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
47
47
|
import { AID } from './aid.js';
|
|
48
|
-
import { certMatchesFingerprint, normalizeFingerprintHex } from './cert-utils.js';
|
|
48
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
|
|
49
49
|
function isPromiseLike(value) {
|
|
50
50
|
return Boolean(value && typeof value.then === 'function');
|
|
51
51
|
}
|
|
@@ -124,7 +124,7 @@ const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
|
|
|
124
124
|
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
125
125
|
const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
|
|
126
126
|
const NON_IDEMPOTENT_METHODS = new Set([
|
|
127
|
-
'message.send', 'group.send', 'group.create', 'group.invite',
|
|
127
|
+
'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
|
|
128
128
|
'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
|
|
129
129
|
'group.set_settings',
|
|
130
130
|
'group.update_announcement', 'group.update_rules',
|
|
@@ -332,6 +332,7 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
332
332
|
aunPath: opts.config().aunPath,
|
|
333
333
|
verifySsl: opts.config().verifySsl,
|
|
334
334
|
discoveryPort: opts.config().discoveryPort,
|
|
335
|
+
uploadViaGatewayHost: true,
|
|
335
336
|
logger: opts.logger(),
|
|
336
337
|
ownerAidGetter: opts.ownerAid,
|
|
337
338
|
currentAidGetter: opts.currentAid,
|
|
@@ -435,6 +436,7 @@ export class AUNClient {
|
|
|
435
436
|
_gatewayCandidates = [];
|
|
436
437
|
/** 是否正在关闭 */
|
|
437
438
|
_closing = false;
|
|
439
|
+
_disconnecting = false;
|
|
438
440
|
/** 事件调度器 */
|
|
439
441
|
_dispatcher;
|
|
440
442
|
/** Gateway 发现 */
|
|
@@ -458,6 +460,8 @@ export class AUNClient {
|
|
|
458
460
|
/** peer 证书缓存 */
|
|
459
461
|
_certCache = new Map();
|
|
460
462
|
_agentMdManager;
|
|
463
|
+
_aidStore;
|
|
464
|
+
_groupIdentityOperations = new Map();
|
|
461
465
|
_groupIndexMetaCache = new GroupIndexMetaCache();
|
|
462
466
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
463
467
|
_seqTracker = new SeqTracker();
|
|
@@ -483,12 +487,6 @@ export class AUNClient {
|
|
|
483
487
|
_pushProcessQueues = new Map();
|
|
484
488
|
/** P2P pull 进行中到达的纯通知 push 上界;pull gate 释放后需要补拉一次。 */
|
|
485
489
|
_pendingP2pPullUpper = new Map();
|
|
486
|
-
/** 在线未读 hint 队列:同一 group 只保留最后一条,延迟 drain 降低登录瞬时拉取压力。 */
|
|
487
|
-
_onlineUnreadHintQueue = new Map();
|
|
488
|
-
_onlineUnreadHintTimer = null;
|
|
489
|
-
_onlineUnreadHintDrainActive = false;
|
|
490
|
-
_onlineUnreadHintInitialDelayMs = 750;
|
|
491
|
-
_onlineUnreadHintIntervalMs = 50;
|
|
492
490
|
/** 缺 sender IK 时暂存原始 V2 消息,后台补齐 IK 后重试解密。 */
|
|
493
491
|
_v2SenderIKPending = new Map();
|
|
494
492
|
/** sender IK 后台补齐任务去重。 */
|
|
@@ -520,6 +518,8 @@ export class AUNClient {
|
|
|
520
518
|
_peerCache = new Map();
|
|
521
519
|
_reconnectActive = false;
|
|
522
520
|
_reconnectAbort = null;
|
|
521
|
+
_reconnectTask = null;
|
|
522
|
+
_reconnectEventDispatchDepth = 0;
|
|
523
523
|
_serverKicked = false;
|
|
524
524
|
/** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
|
|
525
525
|
_lastDisconnectInfo = null;
|
|
@@ -536,6 +536,7 @@ export class AUNClient {
|
|
|
536
536
|
_storage;
|
|
537
537
|
_collab;
|
|
538
538
|
_message;
|
|
539
|
+
_mail;
|
|
539
540
|
_group;
|
|
540
541
|
_stream;
|
|
541
542
|
constructor(aid) {
|
|
@@ -543,6 +544,7 @@ export class AUNClient {
|
|
|
543
544
|
throw new ValidationError('AUNClient only accepts an AID object or no argument');
|
|
544
545
|
}
|
|
545
546
|
const inputAid = aid ?? null;
|
|
547
|
+
this._aidStore = inputAid?._aidStore;
|
|
546
548
|
const rawConfig = {};
|
|
547
549
|
if (inputAid) {
|
|
548
550
|
rawConfig.aun_path = inputAid.aunPath;
|
|
@@ -685,7 +687,7 @@ export class AUNClient {
|
|
|
685
687
|
// 群组状态提交事件:验证 state_hash 链并更新本地存储
|
|
686
688
|
this._dispatcher.subscribe('_raw.group.state_committed', (data) => this._onGroupStateCommitted(data));
|
|
687
689
|
// 其他事件直接透传
|
|
688
|
-
for (const evt of ['message.ack', 'storage.object_changed']) {
|
|
690
|
+
for (const evt of ['message.ack', 'storage.object_changed', 'stream/created', 'stream/closed']) {
|
|
689
691
|
this._dispatcher.subscribe(`_raw.${evt}`, (data) => this._dispatcher.publish(evt, data));
|
|
690
692
|
}
|
|
691
693
|
// 服务端主动断开通知:记录日志并标记不重连
|
|
@@ -696,6 +698,14 @@ export class AUNClient {
|
|
|
696
698
|
get aid() {
|
|
697
699
|
return this._aid;
|
|
698
700
|
}
|
|
701
|
+
/** 返回后台刷新任务维护的最新 access token。 */
|
|
702
|
+
getAccessToken() {
|
|
703
|
+
const identityToken = String(this._identity?.access_token ?? '').trim();
|
|
704
|
+
if (identityToken)
|
|
705
|
+
return identityToken;
|
|
706
|
+
const sessionToken = String(this._sessionParams?.access_token ?? '').trim();
|
|
707
|
+
return sessionToken || null;
|
|
708
|
+
}
|
|
699
709
|
/** AUN Storage VFS 入口 */
|
|
700
710
|
get storage() {
|
|
701
711
|
if (!this._storage)
|
|
@@ -804,6 +814,7 @@ export class AUNClient {
|
|
|
804
814
|
this._v2LazyProposeTriggered.clear();
|
|
805
815
|
}
|
|
806
816
|
_applyAidRuntimeContext(aid) {
|
|
817
|
+
this._aidStore = aid._aidStore;
|
|
807
818
|
const oldTransport = this._transport;
|
|
808
819
|
try {
|
|
809
820
|
const closeResult = oldTransport?.close?.();
|
|
@@ -841,6 +852,7 @@ export class AUNClient {
|
|
|
841
852
|
this._peerCache.clear();
|
|
842
853
|
this._certCache.clear();
|
|
843
854
|
this._gatewayUrl = null;
|
|
855
|
+
this._gatewayCandidates = [];
|
|
844
856
|
this._deviceId = aid.deviceId || getDeviceId(nextConfig.aunPath);
|
|
845
857
|
this._slotId = aid.slotId || 'default';
|
|
846
858
|
const debugFlag = nextConfig.debug;
|
|
@@ -1011,16 +1023,124 @@ export class AUNClient {
|
|
|
1011
1023
|
return;
|
|
1012
1024
|
await this._handleTransportDisconnect(err);
|
|
1013
1025
|
}
|
|
1014
|
-
async
|
|
1026
|
+
async _recoverIdentityAdmission(err, _method = '', params = {}) {
|
|
1027
|
+
if (!(err instanceof AUNError) || err.code !== -32003)
|
|
1028
|
+
return false;
|
|
1029
|
+
const message = String(err.message ?? '').trim();
|
|
1030
|
+
const groupCertPrefix = 'valid group identity certificate is required for ';
|
|
1031
|
+
if (message.toLowerCase().startsWith(groupCertPrefix)) {
|
|
1032
|
+
const fallback = message.slice(groupCertPrefix.length).trim();
|
|
1033
|
+
const store = this._aidStore;
|
|
1034
|
+
const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
|
|
1035
|
+
if (!groupId || !store)
|
|
1036
|
+
return false;
|
|
1037
|
+
try {
|
|
1038
|
+
await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
const groupMDPrefix = 'valid group agent.md is required for ';
|
|
1046
|
+
if (message.toLowerCase().startsWith(groupMDPrefix)) {
|
|
1047
|
+
const groupAid = message.slice(groupMDPrefix.length).trim();
|
|
1048
|
+
const store = this._aidStore;
|
|
1049
|
+
if (!groupAid || !store)
|
|
1050
|
+
return false;
|
|
1051
|
+
const loaded = store.load(groupAid);
|
|
1052
|
+
if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
|
|
1053
|
+
return false;
|
|
1054
|
+
await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}));
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
const agentPrefix = 'valid agent.md is required for ';
|
|
1058
|
+
if (!message.toLowerCase().startsWith(agentPrefix))
|
|
1059
|
+
return false;
|
|
1060
|
+
const target = message.slice(agentPrefix.length).trim();
|
|
1061
|
+
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1062
|
+
return false;
|
|
1063
|
+
let agentMd = buildDefaultAgentMd(target);
|
|
1064
|
+
try {
|
|
1065
|
+
agentMd = this._agentMdManager.readContent(target);
|
|
1066
|
+
}
|
|
1067
|
+
catch { }
|
|
1068
|
+
await this._agentMdManager.upload(agentMd);
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
async _runGroupIdentityOperation(groupId, operation) {
|
|
1072
|
+
const aid = String(this._aid ?? '').trim();
|
|
1073
|
+
const dot = aid.indexOf('.');
|
|
1074
|
+
const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
|
|
1075
|
+
if (!key)
|
|
1076
|
+
return operation();
|
|
1077
|
+
let task = this._groupIdentityOperations.get(key);
|
|
1078
|
+
if (!task) {
|
|
1079
|
+
task = Promise.resolve().then(operation);
|
|
1080
|
+
this._groupIdentityOperations.set(key, task);
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
return await task;
|
|
1084
|
+
}
|
|
1085
|
+
finally {
|
|
1086
|
+
if (this._groupIdentityOperations.get(key) === task)
|
|
1087
|
+
this._groupIdentityOperations.delete(key);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async createGroup(params = {}, options = {}) {
|
|
1015
1091
|
if (!isJsonObject(params)) {
|
|
1016
1092
|
throw new ValidationError('createGroup params must be an object');
|
|
1017
1093
|
}
|
|
1094
|
+
const store = options.aidStore;
|
|
1095
|
+
if (!store) {
|
|
1096
|
+
throw new ValidationError('createGroup requires aidStore');
|
|
1097
|
+
}
|
|
1098
|
+
this._aidStore = store;
|
|
1018
1099
|
const payload = { ...params };
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1100
|
+
if (payload.group_name === undefined && payload.groupName !== undefined) {
|
|
1101
|
+
payload.group_name = payload.groupName;
|
|
1102
|
+
delete payload.groupName;
|
|
1103
|
+
}
|
|
1104
|
+
const isNamed = Boolean(String(payload.group_name ?? '').trim());
|
|
1105
|
+
payload._defer_group_ready_postprocess = true;
|
|
1106
|
+
if (!isNamed) {
|
|
1107
|
+
delete payload.public_key;
|
|
1108
|
+
delete payload.curve;
|
|
1109
|
+
const created = await this.call('group.create', payload);
|
|
1110
|
+
const createdMap = isJsonObject(created) ? created : {};
|
|
1111
|
+
const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
|
|
1112
|
+
const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
|
|
1113
|
+
if (!groupId) {
|
|
1114
|
+
throw new ValidationError('group.create response missing group.group_id');
|
|
1115
|
+
}
|
|
1116
|
+
const bindParams = { group_id: groupId };
|
|
1117
|
+
for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
|
|
1118
|
+
if (key in payload)
|
|
1119
|
+
bindParams[key] = payload[key];
|
|
1120
|
+
}
|
|
1121
|
+
const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
|
|
1122
|
+
const boundMap = isJsonObject(bound) ? bound : {};
|
|
1123
|
+
const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
|
|
1124
|
+
const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
|
|
1125
|
+
const postprocessParams = { ...payload };
|
|
1126
|
+
delete postprocessParams._defer_group_ready_postprocess;
|
|
1127
|
+
return await this._groupState.postprocessResult('group.create', postprocessParams, result);
|
|
1128
|
+
}
|
|
1129
|
+
const pendingKey = `create:${String(payload.group_name).trim().toLowerCase()}`;
|
|
1130
|
+
const keystore = store._keystore;
|
|
1131
|
+
let keyPair = null;
|
|
1132
|
+
if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1133
|
+
keyPair = keystore.loadPendingGroupBind(pendingKey);
|
|
1134
|
+
}
|
|
1135
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1136
|
+
keyPair = new CryptoProvider().generateIdentity();
|
|
1137
|
+
if (keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1138
|
+
keystore.savePendingGroupBind(pendingKey, keyPair);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1142
|
+
throw new ValidationError('createGroup: failed to generate or load key pair');
|
|
1022
1143
|
}
|
|
1023
|
-
const keyPair = new CryptoProvider().generateIdentity();
|
|
1024
1144
|
payload.public_key = keyPair.public_key_der_b64;
|
|
1025
1145
|
payload.curve = keyPair.curve;
|
|
1026
1146
|
const result = await this.call('group.create', payload);
|
|
@@ -1030,31 +1150,65 @@ export class AUNClient {
|
|
|
1030
1150
|
const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
|
|
1031
1151
|
const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
|
|
1032
1152
|
if (!groupAid || !certPem) {
|
|
1033
|
-
throw new ValidationError('group.create
|
|
1153
|
+
throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
|
|
1034
1154
|
}
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
rootCaPath: this._configModel.rootCaPath,
|
|
1041
|
-
debug: this._configModel.debug,
|
|
1155
|
+
const imported = store.importGroupIdentity(groupAid, {
|
|
1156
|
+
private_key_pem: keyPair.private_key_pem,
|
|
1157
|
+
public_key_der_b64: keyPair.public_key_der_b64,
|
|
1158
|
+
curve: keyPair.curve,
|
|
1159
|
+
cert_pem: certPem,
|
|
1042
1160
|
});
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
private_key_pem: keyPair.private_key_pem,
|
|
1046
|
-
public_key_der_b64: keyPair.public_key_der_b64,
|
|
1047
|
-
curve: keyPair.curve,
|
|
1048
|
-
cert_pem: certPem,
|
|
1049
|
-
});
|
|
1050
|
-
if (!imported.ok) {
|
|
1051
|
-
throw new ValidationError(imported.error.message);
|
|
1052
|
-
}
|
|
1161
|
+
if (!imported.ok) {
|
|
1162
|
+
throw new ValidationError(imported.error.message);
|
|
1053
1163
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1164
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group);
|
|
1165
|
+
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1166
|
+
keystore.clearPendingGroupBind(pendingKey);
|
|
1167
|
+
}
|
|
1168
|
+
const postprocessParams = { ...payload };
|
|
1169
|
+
delete postprocessParams._defer_group_ready_postprocess;
|
|
1170
|
+
return await this._groupState.postprocessResult('group.create', postprocessParams, result);
|
|
1171
|
+
}
|
|
1172
|
+
_groupAgentMdContent(groupAid, params, group) {
|
|
1173
|
+
const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
|
|
1174
|
+
if (explicit !== undefined && explicit !== null)
|
|
1175
|
+
return String(explicit);
|
|
1176
|
+
const value = (...keys) => {
|
|
1177
|
+
for (const source of [group, params]) {
|
|
1178
|
+
for (const key of keys) {
|
|
1179
|
+
const item = String(source[key] ?? '').trim();
|
|
1180
|
+
if (item)
|
|
1181
|
+
return item;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return '';
|
|
1185
|
+
};
|
|
1186
|
+
const name = value('name', 'group_name', 'title') || groupAid;
|
|
1187
|
+
const description = (value('description', 'group_description') || `AUN group ${name}`)
|
|
1188
|
+
.replace(/\s+/g, ' ')
|
|
1189
|
+
.slice(0, 100);
|
|
1190
|
+
return [
|
|
1191
|
+
'---',
|
|
1192
|
+
`aid: ${JSON.stringify(groupAid)}`,
|
|
1193
|
+
`name: ${JSON.stringify(name)}`,
|
|
1194
|
+
'type: "group"',
|
|
1195
|
+
'version: "1.0.0"',
|
|
1196
|
+
`description: ${JSON.stringify(description)}`,
|
|
1197
|
+
'---',
|
|
1198
|
+
'',
|
|
1199
|
+
`# ${name}`,
|
|
1200
|
+
'',
|
|
1201
|
+
].join('\n');
|
|
1202
|
+
}
|
|
1203
|
+
async _uploadGroupAgentMd(store, groupAid, params, group) {
|
|
1204
|
+
const uploaderAid = String(this._aid ?? '').trim();
|
|
1205
|
+
if (!uploaderAid) {
|
|
1206
|
+
throw new ValidationError('group agent.md upload requires current owner AID');
|
|
1207
|
+
}
|
|
1208
|
+
const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
|
|
1209
|
+
if (!uploaded.ok) {
|
|
1210
|
+
throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
|
|
1056
1211
|
}
|
|
1057
|
-
return result;
|
|
1058
1212
|
}
|
|
1059
1213
|
async bindGroupAid(params = {}, options = {}) {
|
|
1060
1214
|
if (!isJsonObject(params)) {
|
|
@@ -1064,12 +1218,17 @@ export class AUNClient {
|
|
|
1064
1218
|
if (!store) {
|
|
1065
1219
|
throw new ValidationError('bindGroupAid requires aidStore');
|
|
1066
1220
|
}
|
|
1221
|
+
this._aidStore = store;
|
|
1067
1222
|
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1223
|
+
const pendingKey = `bind:${groupId}`;
|
|
1068
1224
|
const keystore = store._keystore; // AIDStore._keystore: LocalIdentityStore
|
|
1069
1225
|
let keyPair = null;
|
|
1070
1226
|
// 优先复用 pending 槽位中已暂存的待绑定密钥(崩溃/重试幂等)
|
|
1071
1227
|
if (groupId && keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1072
|
-
keyPair = keystore.loadPendingGroupBind(
|
|
1228
|
+
keyPair = keystore.loadPendingGroupBind(pendingKey);
|
|
1229
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1230
|
+
keyPair = keystore.loadPendingGroupBind(groupId);
|
|
1231
|
+
}
|
|
1073
1232
|
}
|
|
1074
1233
|
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1075
1234
|
// 未命中 pending,生成新密钥
|
|
@@ -1081,7 +1240,7 @@ export class AUNClient {
|
|
|
1081
1240
|
};
|
|
1082
1241
|
// 发 RPC 前先落盘暂存,确保崩溃后重试能复用同一密钥
|
|
1083
1242
|
if (groupId && keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1084
|
-
keystore.savePendingGroupBind(
|
|
1243
|
+
keystore.savePendingGroupBind(pendingKey, keyPair);
|
|
1085
1244
|
}
|
|
1086
1245
|
}
|
|
1087
1246
|
if (!keyPair) {
|
|
@@ -1113,8 +1272,10 @@ export class AUNClient {
|
|
|
1113
1272
|
if (!imported.ok) {
|
|
1114
1273
|
throw new ValidationError(imported.error.message);
|
|
1115
1274
|
}
|
|
1275
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group);
|
|
1116
1276
|
// 落盘成功,清除 pending 槽位
|
|
1117
1277
|
if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1278
|
+
keystore.clearPendingGroupBind(pendingKey);
|
|
1118
1279
|
keystore.clearPendingGroupBind(groupId);
|
|
1119
1280
|
}
|
|
1120
1281
|
return result;
|
|
@@ -1137,7 +1298,7 @@ export class AUNClient {
|
|
|
1137
1298
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1138
1299
|
const infoMap = isJsonObject(info) ? info : {};
|
|
1139
1300
|
const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
|
|
1140
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1301
|
+
groupAid = String(info?.group_aid ?? grp?.group_aid ?? '').trim();
|
|
1141
1302
|
}
|
|
1142
1303
|
if (!groupAid) {
|
|
1143
1304
|
throw new ValidationError('startGroupTransfer: unable to determine group_aid');
|
|
@@ -1185,7 +1346,7 @@ export class AUNClient {
|
|
|
1185
1346
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1186
1347
|
const infoMap = isJsonObject(info) ? info : {};
|
|
1187
1348
|
const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
|
|
1188
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1349
|
+
groupAid = String(info?.group_aid ?? grp?.group_aid ?? '').trim();
|
|
1189
1350
|
}
|
|
1190
1351
|
if (!groupAid) {
|
|
1191
1352
|
throw new ValidationError('renewGroupAid: unable to determine group_aid');
|
|
@@ -1204,7 +1365,21 @@ export class AUNClient {
|
|
|
1204
1365
|
throw new ValidationError(`renewGroupAid: cannot determine old public key for ${groupAid}`);
|
|
1205
1366
|
}
|
|
1206
1367
|
// 生成新密钥
|
|
1207
|
-
const
|
|
1368
|
+
const keystore = store._keystore;
|
|
1369
|
+
const pendingKey = `renew:${groupId}`;
|
|
1370
|
+
let newKeyPair = null;
|
|
1371
|
+
if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1372
|
+
newKeyPair = keystore.loadPendingGroupBind(pendingKey);
|
|
1373
|
+
}
|
|
1374
|
+
if (!newKeyPair || !newKeyPair.public_key_der_b64 || !newKeyPair.private_key_pem) {
|
|
1375
|
+
newKeyPair = new CryptoProvider().generateIdentity();
|
|
1376
|
+
if (keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1377
|
+
keystore.savePendingGroupBind(pendingKey, newKeyPair);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (!newKeyPair || !newKeyPair.public_key_der_b64 || !newKeyPair.private_key_pem) {
|
|
1381
|
+
throw new ValidationError('renewGroupAid: failed to generate or load key pair');
|
|
1382
|
+
}
|
|
1208
1383
|
const newPublicKey = newKeyPair.public_key_der_b64;
|
|
1209
1384
|
const newPrivateKey = newKeyPair.private_key_pem;
|
|
1210
1385
|
const curve = newKeyPair.curve || 'P-256';
|
|
@@ -1259,6 +1434,10 @@ export class AUNClient {
|
|
|
1259
1434
|
if (!imported.ok) {
|
|
1260
1435
|
throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
|
|
1261
1436
|
}
|
|
1437
|
+
await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group);
|
|
1438
|
+
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1439
|
+
keystore.clearPendingGroupBind(pendingKey);
|
|
1440
|
+
}
|
|
1262
1441
|
return result;
|
|
1263
1442
|
}
|
|
1264
1443
|
async completeGroupTransfer(params = {}, options = {}) {
|
|
@@ -1269,18 +1448,32 @@ export class AUNClient {
|
|
|
1269
1448
|
if (!store) {
|
|
1270
1449
|
throw new ValidationError('completeGroupTransfer requires aidStore');
|
|
1271
1450
|
}
|
|
1272
|
-
const keyPair = new CryptoProvider().generateIdentity();
|
|
1273
1451
|
const payload = { ...params };
|
|
1274
1452
|
const groupId = String(params.group_id ?? '').trim();
|
|
1275
1453
|
if (!groupId) {
|
|
1276
1454
|
throw new ValidationError('completeGroupTransfer requires group_id');
|
|
1277
1455
|
}
|
|
1456
|
+
const pendingKey = `complete:${groupId}`;
|
|
1457
|
+
const keystore = store._keystore;
|
|
1458
|
+
let keyPair = null;
|
|
1459
|
+
if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1460
|
+
keyPair = keystore.loadPendingGroupBind(pendingKey);
|
|
1461
|
+
}
|
|
1462
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1463
|
+
keyPair = new CryptoProvider().generateIdentity();
|
|
1464
|
+
if (keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1465
|
+
keystore.savePendingGroupBind(pendingKey, keyPair);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1469
|
+
throw new ValidationError('completeGroupTransfer: failed to generate or load key pair');
|
|
1470
|
+
}
|
|
1278
1471
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1279
1472
|
if (!groupAid) {
|
|
1280
1473
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1281
1474
|
const infoMap = isJsonObject(info) ? info : {};
|
|
1282
1475
|
const grp = isJsonObject(infoMap.group) ? infoMap.group : infoMap;
|
|
1283
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1476
|
+
groupAid = String(info?.group_aid ?? grp?.group_aid ?? '').trim();
|
|
1284
1477
|
}
|
|
1285
1478
|
if (!groupAid) {
|
|
1286
1479
|
throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
|
|
@@ -1330,31 +1523,25 @@ export class AUNClient {
|
|
|
1330
1523
|
if (!imported.ok) {
|
|
1331
1524
|
throw new ValidationError(imported.error.message);
|
|
1332
1525
|
}
|
|
1526
|
+
await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group);
|
|
1527
|
+
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1528
|
+
keystore.clearPendingGroupBind(pendingKey);
|
|
1529
|
+
}
|
|
1333
1530
|
return result;
|
|
1334
1531
|
}
|
|
1335
1532
|
/** 证书公钥校验:确保服务端返回的证书公钥 == 本地公钥(DER base64) */
|
|
1336
1533
|
async verifyCertPublicKeyMatch(certPem, localPublicKeyDerB64, operation) {
|
|
1337
|
-
// 简化实现:使用 Node.js crypto 或浏览器 Web Crypto API
|
|
1338
|
-
// 完整实现需要解析 X.509 证书提取 subjectPublicKeyInfo
|
|
1339
|
-
// 这里先做基本校验,后续可增强
|
|
1340
|
-
const crypto = globalThis.crypto;
|
|
1341
|
-
if (!crypto || !crypto.subtle) {
|
|
1342
|
-
// 浏览器环境或 Node < 15,跳过校验(非致命)
|
|
1343
|
-
console.warn(`${operation}: crypto.subtle not available, skipping cert public key verification`);
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
1534
|
try {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const certLines = certPem.split('\n').filter(l => l && !l.startsWith('-----'));
|
|
1350
|
-
const certDer = Buffer.from(certLines.join(''), 'base64');
|
|
1351
|
-
// 验证本地公钥可导入
|
|
1535
|
+
const cert = new crypto.X509Certificate(certPem);
|
|
1536
|
+
const certPubDer = cert.publicKey.export({ type: 'spki', format: 'der' });
|
|
1352
1537
|
const localPubDer = Buffer.from(localPublicKeyDerB64, 'base64');
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1538
|
+
if (!localPubDer.length || !certPubDer.equals(localPubDer)) {
|
|
1539
|
+
throw new AuthError(`${operation}: certificate public key mismatch`);
|
|
1540
|
+
}
|
|
1356
1541
|
}
|
|
1357
1542
|
catch (err) {
|
|
1543
|
+
if (err instanceof AuthError)
|
|
1544
|
+
throw err;
|
|
1358
1545
|
throw new AuthError(`${operation}: certificate or public key validation failed: ${err.message}`);
|
|
1359
1546
|
}
|
|
1360
1547
|
}
|
|
@@ -1566,9 +1753,11 @@ export class AUNClient {
|
|
|
1566
1753
|
this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
|
|
1567
1754
|
return false;
|
|
1568
1755
|
}
|
|
1756
|
+
if (this._rpcPipeline?.hasPullActivity?.(ns, false))
|
|
1757
|
+
return false;
|
|
1569
1758
|
this._pendingP2pPullUpper.delete(ns);
|
|
1570
1759
|
this._clientLog.info(`P2P pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
|
|
1571
|
-
|
|
1760
|
+
this._safeAsync(this._onV2PushNotification({ seq: upperSeq }, 'v2'));
|
|
1572
1761
|
return true;
|
|
1573
1762
|
}
|
|
1574
1763
|
_markPublishedSeq(ns, seq) {
|
|
@@ -1707,18 +1896,6 @@ export class AUNClient {
|
|
|
1707
1896
|
}
|
|
1708
1897
|
return Math.max(0, ...values.filter((value) => Number.isFinite(value)));
|
|
1709
1898
|
}
|
|
1710
|
-
_explicitGroupCursorParams(params) {
|
|
1711
|
-
const value = params._group_cursor_params;
|
|
1712
|
-
if (!isJsonObject(value))
|
|
1713
|
-
return {};
|
|
1714
|
-
return { ...value };
|
|
1715
|
-
}
|
|
1716
|
-
_groupCursorTargetsCurrentInstance(params) {
|
|
1717
|
-
const deviceId = String(params.device_id ?? '').trim();
|
|
1718
|
-
const slotId = String(params.slot_id ?? '').trim();
|
|
1719
|
-
return (!deviceId || deviceId === (this._deviceId ?? ''))
|
|
1720
|
-
&& (!slotId || slotId === (this._slotId ?? ''));
|
|
1721
|
-
}
|
|
1722
1899
|
async _withBackgroundRpc(operation) {
|
|
1723
1900
|
this._backgroundRpcDepth += 1;
|
|
1724
1901
|
try {
|
|
@@ -1731,6 +1908,12 @@ export class AUNClient {
|
|
|
1731
1908
|
async _runPullSerialized(key, operation, background = false) {
|
|
1732
1909
|
return await this._rpcPipeline.runPullSerialized(key, operation, background);
|
|
1733
1910
|
}
|
|
1911
|
+
/** Mail bridge facade 入口 */
|
|
1912
|
+
get mail() {
|
|
1913
|
+
if (!this._mail)
|
|
1914
|
+
this._mail = new MailFacade(this);
|
|
1915
|
+
return this._mail;
|
|
1916
|
+
}
|
|
1734
1917
|
async _tryRunInlineRealtime(namespace, seq, operation) {
|
|
1735
1918
|
return await this._rpcPipeline.tryRunInlineRealtime(namespace, seq, operation);
|
|
1736
1919
|
}
|
|
@@ -1760,14 +1943,14 @@ export class AUNClient {
|
|
|
1760
1943
|
}
|
|
1761
1944
|
return true;
|
|
1762
1945
|
}
|
|
1763
|
-
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
|
|
1764
|
-
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse);
|
|
1946
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
1947
|
+
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
|
|
1765
1948
|
}
|
|
1766
1949
|
async _publishOrderedMessage(event, ns, seq, payload) {
|
|
1767
1950
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload);
|
|
1768
1951
|
}
|
|
1769
|
-
async _publishPulledMessage(event, ns, seq, payload) {
|
|
1770
|
-
return this._delivery.publishPulledMessage(event, ns, seq, payload);
|
|
1952
|
+
async _publishPulledMessage(event, ns, seq, payload, persist = true) {
|
|
1953
|
+
return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
|
|
1771
1954
|
}
|
|
1772
1955
|
_markOrderedSeqDelivered(ns, seq) {
|
|
1773
1956
|
if (!ns || !Number.isFinite(seq) || !Number.isInteger(seq) || seq <= 0)
|
|
@@ -1836,7 +2019,7 @@ export class AUNClient {
|
|
|
1836
2019
|
this._seqTracker.removeNamespace(groupEventNs);
|
|
1837
2020
|
this._persistRepairedSeq(groupNs);
|
|
1838
2021
|
this._persistRepairedSeq(groupEventNs);
|
|
1839
|
-
this._saveSeqTrackerState();
|
|
2022
|
+
void this._saveSeqTrackerState();
|
|
1840
2023
|
for (const key of this._gapFillDone.keys()) {
|
|
1841
2024
|
if (key.includes(groupId)) {
|
|
1842
2025
|
this._gapFillDone.delete(key);
|
|
@@ -1929,7 +2112,8 @@ export class AUNClient {
|
|
|
1929
2112
|
if (!normalizeFingerprintHex(expectedFP)) {
|
|
1930
2113
|
throw new ValidationError(`unsupported cert_fingerprint format for ${aid}: ${expectedFP.slice(0, 24)}`);
|
|
1931
2114
|
}
|
|
1932
|
-
if (!certMatchesFingerprint(certPem, expectedFP)
|
|
2115
|
+
if (!certMatchesFingerprint(certPem, expectedFP)
|
|
2116
|
+
&& !publicKeyMatchesFingerprint(certPem, expectedFP)) {
|
|
1933
2117
|
throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
|
|
1934
2118
|
}
|
|
1935
2119
|
}
|
|
@@ -2103,8 +2287,9 @@ export class AUNClient {
|
|
|
2103
2287
|
return JSON.stringify([this._aid, this._deviceId, this._slotId]);
|
|
2104
2288
|
}
|
|
2105
2289
|
_resetSeqTrackingState() {
|
|
2106
|
-
this._saveSeqTrackerState();
|
|
2290
|
+
void this._saveSeqTrackerState();
|
|
2107
2291
|
this._resetV2IdentityRuntime();
|
|
2292
|
+
this._v2E2EE.resetForwardCursorState();
|
|
2108
2293
|
this._delivery.resetInlineAckState();
|
|
2109
2294
|
this._seqTracker = new SeqTracker();
|
|
2110
2295
|
this._seqTrackerContext = null;
|
|
@@ -2115,18 +2300,13 @@ export class AUNClient {
|
|
|
2115
2300
|
this._v2SenderIKPending.clear();
|
|
2116
2301
|
this._v2SenderIKFetching.clear();
|
|
2117
2302
|
this._groupSynced.clear();
|
|
2118
|
-
this._onlineUnreadHintQueue.clear();
|
|
2119
|
-
if (this._onlineUnreadHintTimer) {
|
|
2120
|
-
clearTimeout(this._onlineUnreadHintTimer);
|
|
2121
|
-
this._onlineUnreadHintTimer = null;
|
|
2122
|
-
}
|
|
2123
|
-
this._onlineUnreadHintDrainActive = false;
|
|
2124
2303
|
}
|
|
2125
2304
|
_refreshSeqTrackerContext() {
|
|
2126
2305
|
const nextContext = this._currentSeqTrackerContext();
|
|
2127
2306
|
if (nextContext === this._seqTrackerContext)
|
|
2128
2307
|
return;
|
|
2129
|
-
this._saveSeqTrackerState();
|
|
2308
|
+
void this._saveSeqTrackerState();
|
|
2309
|
+
this._v2E2EE.resetForwardCursorState();
|
|
2130
2310
|
this._delivery.resetInlineAckState();
|
|
2131
2311
|
this._seqTracker = new SeqTracker();
|
|
2132
2312
|
this._gapFillDone.clear();
|
|
@@ -2136,12 +2316,6 @@ export class AUNClient {
|
|
|
2136
2316
|
this._v2SenderIKPending.clear();
|
|
2137
2317
|
this._v2SenderIKFetching.clear();
|
|
2138
2318
|
this._groupSynced.clear();
|
|
2139
|
-
this._onlineUnreadHintQueue.clear();
|
|
2140
|
-
if (this._onlineUnreadHintTimer) {
|
|
2141
|
-
clearTimeout(this._onlineUnreadHintTimer);
|
|
2142
|
-
this._onlineUnreadHintTimer = null;
|
|
2143
|
-
}
|
|
2144
|
-
this._onlineUnreadHintDrainActive = false;
|
|
2145
2319
|
this._seqTrackerContext = nextContext;
|
|
2146
2320
|
}
|
|
2147
2321
|
/** 将 SeqTracker 状态保存到 keystore */
|
|
@@ -2210,8 +2384,10 @@ export class AUNClient {
|
|
|
2210
2384
|
}
|
|
2211
2385
|
// ── 内部:连接 ────────────────────────────────────────────
|
|
2212
2386
|
/** 执行一次连接流程 */
|
|
2213
|
-
async _connectOnce(params, allowReauth) {
|
|
2387
|
+
async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
|
|
2214
2388
|
const tStart = Date.now();
|
|
2389
|
+
connectionOwner ??= this._connectAbort ?? undefined;
|
|
2390
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2215
2391
|
const gatewayUrl = this._resolveGateway(params);
|
|
2216
2392
|
this._gatewayUrl = gatewayUrl;
|
|
2217
2393
|
this._slotId = String(params.slot_id ?? '');
|
|
@@ -2228,8 +2404,11 @@ export class AUNClient {
|
|
|
2228
2404
|
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
2229
2405
|
this._refreshSeqTrackerContext();
|
|
2230
2406
|
this._restoreSeqTrackerState();
|
|
2407
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2231
2408
|
try {
|
|
2232
2409
|
const challenge = await this._transport.connect(gatewayUrl);
|
|
2410
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2411
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2233
2412
|
this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
|
|
2234
2413
|
this._state = 'authenticating';
|
|
2235
2414
|
if (allowReauth) {
|
|
@@ -2242,6 +2421,8 @@ export class AUNClient {
|
|
|
2242
2421
|
shortTtlMs: Number(params.short_ttl_ms ?? 0),
|
|
2243
2422
|
extraInfo,
|
|
2244
2423
|
});
|
|
2424
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2425
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2245
2426
|
if (isJsonObject(authContext)) {
|
|
2246
2427
|
const auth = authContext;
|
|
2247
2428
|
const identity = auth.identity;
|
|
@@ -2268,11 +2449,15 @@ export class AUNClient {
|
|
|
2268
2449
|
shortTtlMs: Number(params.short_ttl_ms ?? 0),
|
|
2269
2450
|
extraInfo,
|
|
2270
2451
|
});
|
|
2452
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2453
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2271
2454
|
this._syncIdentityAfterConnect(String(params.access_token));
|
|
2272
2455
|
if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
|
|
2273
2456
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
2274
2457
|
}
|
|
2275
2458
|
}
|
|
2459
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2460
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2276
2461
|
if (!this._transport.isConnected()) {
|
|
2277
2462
|
throw new ConnectionError('websocket disconnected during authentication');
|
|
2278
2463
|
}
|
|
@@ -2288,13 +2473,29 @@ export class AUNClient {
|
|
|
2288
2473
|
this._gatewayCandidates = [gatewayUrl];
|
|
2289
2474
|
}
|
|
2290
2475
|
this._clientLog.debug(`auth complete, connection ready: aid=${this._aid ?? ''}, gateway=${gatewayUrl}`);
|
|
2291
|
-
|
|
2476
|
+
const statePayload = { state: this._publicState(this._state), gateway: gatewayUrl };
|
|
2477
|
+
if (reconnectOwner) {
|
|
2478
|
+
if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
|
|
2479
|
+
throw new ConnectionError('reconnect attempt superseded');
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
else if (connectionOwner) {
|
|
2483
|
+
if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
|
|
2484
|
+
throw new StateError('connection attempt superseded');
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
else {
|
|
2488
|
+
await this._dispatcher.publish('state_change', statePayload);
|
|
2489
|
+
}
|
|
2490
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2491
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2292
2492
|
// auth 阶段 aid 可能被 identity 覆盖(上方 this._aid = identity.aid);
|
|
2293
2493
|
// 若 context 发生变化,重新 refresh + restore,保持 tracker 与真实身份一致。
|
|
2294
2494
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
2295
2495
|
this._refreshSeqTrackerContext();
|
|
2296
2496
|
this._restoreSeqTrackerState();
|
|
2297
2497
|
}
|
|
2498
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2298
2499
|
this._startBackgroundTasks();
|
|
2299
2500
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2300
2501
|
const isShortConnection = connectionKind === 'short';
|
|
@@ -2303,6 +2504,8 @@ export class AUNClient {
|
|
|
2303
2504
|
&& (!isShortConnection || hasExplicitBackgroundSync);
|
|
2304
2505
|
if (!isShortConnection) {
|
|
2305
2506
|
await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
|
|
2507
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2508
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2306
2509
|
}
|
|
2307
2510
|
else {
|
|
2308
2511
|
this._clientLog.debug('V2 session init deferred for short connection');
|
|
@@ -2317,7 +2520,11 @@ export class AUNClient {
|
|
|
2317
2520
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
2318
2521
|
}
|
|
2319
2522
|
catch (err) {
|
|
2320
|
-
|
|
2523
|
+
if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
|
|
2524
|
+
&& (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
|
|
2525
|
+
&& !this._closing) {
|
|
2526
|
+
this._state = (prevState === 'connected' || prevState === 'ready') ? 'standby' : (this._currentAid ? 'standby' : 'no_identity');
|
|
2527
|
+
}
|
|
2321
2528
|
this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl} err=${err instanceof Error ? err.message : String(err)}`);
|
|
2322
2529
|
throw err;
|
|
2323
2530
|
}
|
|
@@ -2523,7 +2730,7 @@ export class AUNClient {
|
|
|
2523
2730
|
return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid);
|
|
2524
2731
|
}
|
|
2525
2732
|
/** 解密单条 V2 pull 消息。缺 sender IK 时先入 pending,后台补齐后重试。 */
|
|
2526
|
-
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
|
|
2733
|
+
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
|
|
2527
2734
|
const session = this._v2Session;
|
|
2528
2735
|
if (!session)
|
|
2529
2736
|
return null;
|
|
@@ -2585,6 +2792,24 @@ export class AUNClient {
|
|
|
2585
2792
|
}
|
|
2586
2793
|
catch (exc) {
|
|
2587
2794
|
this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${formatCaughtError(exc)}`);
|
|
2795
|
+
if (allowPending || deferStatus) {
|
|
2796
|
+
const fromAid = String(msg.from_aid ?? '');
|
|
2797
|
+
const senderDeviceId = String(aad.from_device ?? '');
|
|
2798
|
+
if (deferStatus) {
|
|
2799
|
+
deferStatus.deferred = true;
|
|
2800
|
+
deferStatus.fromAid = fromAid;
|
|
2801
|
+
deferStatus.senderDeviceId = senderDeviceId;
|
|
2802
|
+
deferStatus.groupId = groupIdForKeys;
|
|
2803
|
+
}
|
|
2804
|
+
this._scheduleV2SenderIKPending({
|
|
2805
|
+
msg,
|
|
2806
|
+
fromAid,
|
|
2807
|
+
senderDeviceId,
|
|
2808
|
+
groupId: groupIdForKeys,
|
|
2809
|
+
startFetch: !deferKeyFetch,
|
|
2810
|
+
});
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2588
2813
|
const event = {
|
|
2589
2814
|
message_id: String(msg.message_id ?? ''),
|
|
2590
2815
|
from: String(msg.from_aid ?? ''),
|
|
@@ -2610,13 +2835,19 @@ export class AUNClient {
|
|
|
2610
2835
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
2611
2836
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
2612
2837
|
let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
2613
|
-
if (!senderPubDer) {
|
|
2838
|
+
if (!senderPubDer && !deferKeyFetch) {
|
|
2614
2839
|
this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
2615
2840
|
senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
2616
2841
|
}
|
|
2617
2842
|
if (!senderPubDer) {
|
|
2618
|
-
if (allowPending) {
|
|
2619
|
-
|
|
2843
|
+
if (allowPending || deferStatus) {
|
|
2844
|
+
if (deferStatus) {
|
|
2845
|
+
deferStatus.deferred = true;
|
|
2846
|
+
deferStatus.fromAid = fromAid;
|
|
2847
|
+
deferStatus.senderDeviceId = senderDeviceId;
|
|
2848
|
+
deferStatus.groupId = groupIdForKeys;
|
|
2849
|
+
}
|
|
2850
|
+
this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
|
|
2620
2851
|
return null;
|
|
2621
2852
|
}
|
|
2622
2853
|
const event = {
|
|
@@ -2641,9 +2872,13 @@ export class AUNClient {
|
|
|
2641
2872
|
let plaintext;
|
|
2642
2873
|
try {
|
|
2643
2874
|
plaintext = decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
|
|
2875
|
+
if (plaintext === null)
|
|
2876
|
+
throw new Error('recipient_not_found_or_envelope_verification_failed');
|
|
2644
2877
|
}
|
|
2645
2878
|
catch (exc) {
|
|
2646
2879
|
this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${formatCaughtError(exc)}`);
|
|
2880
|
+
if (deferStatus)
|
|
2881
|
+
deferStatus.terminal = true;
|
|
2647
2882
|
const event = {
|
|
2648
2883
|
message_id: String(msg.message_id ?? ''),
|
|
2649
2884
|
from: fromAid,
|
|
@@ -2896,6 +3131,11 @@ export class AUNClient {
|
|
|
2896
3131
|
if (!resolvedAid) {
|
|
2897
3132
|
throw new StateError('gateway discovery requires a loaded AID');
|
|
2898
3133
|
}
|
|
3134
|
+
const discovery = this._discovery;
|
|
3135
|
+
const tokenStore = this._tokenStore;
|
|
3136
|
+
const discoveryIsCurrent = () => (this._discovery === discovery
|
|
3137
|
+
&& this._tokenStore === tokenStore
|
|
3138
|
+
&& !this._closing);
|
|
2899
3139
|
if (this._gatewayCandidates.length > 0)
|
|
2900
3140
|
return [...this._gatewayCandidates];
|
|
2901
3141
|
if (this._gatewayUrl) {
|
|
@@ -2903,9 +3143,9 @@ export class AUNClient {
|
|
|
2903
3143
|
return [...this._gatewayCandidates];
|
|
2904
3144
|
}
|
|
2905
3145
|
try {
|
|
2906
|
-
const loadMetadata =
|
|
3146
|
+
const loadMetadata = tokenStore.loadMetadata;
|
|
2907
3147
|
const metadata = typeof loadMetadata === 'function'
|
|
2908
|
-
? loadMetadata.call(
|
|
3148
|
+
? loadMetadata.call(tokenStore, resolvedAid)
|
|
2909
3149
|
: null;
|
|
2910
3150
|
const cachedList = isJsonObject(metadata) && Array.isArray(metadata.gateway_urls)
|
|
2911
3151
|
? metadata.gateway_urls.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
@@ -2932,12 +3172,13 @@ export class AUNClient {
|
|
|
2932
3172
|
try {
|
|
2933
3173
|
// discoverAll 是生产路径;若调用方注入了旧版/测试 discovery
|
|
2934
3174
|
// 替身而只提供 discover,则保留单候选兼容,不影响多候选发现。
|
|
2935
|
-
const discovery = this._discovery;
|
|
2936
3175
|
const hasInjectedDiscover = Object.prototype.hasOwnProperty.call(discovery, 'discover')
|
|
2937
3176
|
&& typeof discovery.discover === 'function';
|
|
2938
3177
|
const discovered = hasInjectedDiscover
|
|
2939
3178
|
? [await discovery.discover(url)]
|
|
2940
3179
|
: await discovery.discoverAll(url);
|
|
3180
|
+
if (!discoveryIsCurrent())
|
|
3181
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2941
3182
|
const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
|
|
2942
3183
|
if (gatewayUrls.length === 0)
|
|
2943
3184
|
throw new ValidationError('gateway discovery returned no candidates');
|
|
@@ -2945,9 +3186,9 @@ export class AUNClient {
|
|
|
2945
3186
|
const gateway = gatewayUrls[0];
|
|
2946
3187
|
this._gatewayUrl = gateway;
|
|
2947
3188
|
try {
|
|
2948
|
-
const saveMetadata =
|
|
3189
|
+
const saveMetadata = tokenStore.saveMetadata;
|
|
2949
3190
|
if (typeof saveMetadata === 'function') {
|
|
2950
|
-
saveMetadata.call(
|
|
3191
|
+
saveMetadata.call(tokenStore, resolvedAid, {
|
|
2951
3192
|
gateway_url: gateway,
|
|
2952
3193
|
gateway_urls: gatewayUrls,
|
|
2953
3194
|
gateway_cached_at: Date.now(),
|
|
@@ -2960,6 +3201,8 @@ export class AUNClient {
|
|
|
2960
3201
|
return [...gatewayUrls];
|
|
2961
3202
|
}
|
|
2962
3203
|
catch (err) {
|
|
3204
|
+
if (!discoveryIsCurrent())
|
|
3205
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2963
3206
|
lastErr = err;
|
|
2964
3207
|
this._clientLog.warn(`gateway discovery failed: aid=${resolvedAid} url=${url} err=${formatCaughtError(err)}`);
|
|
2965
3208
|
}
|
|
@@ -3311,8 +3554,8 @@ export class AUNClient {
|
|
|
3311
3554
|
}
|
|
3312
3555
|
}
|
|
3313
3556
|
/** 传输层断线回调 */
|
|
3314
|
-
async _handleTransportDisconnect(error, closeCode) {
|
|
3315
|
-
if (this._closing || this.state === ConnectionState.CLOSED)
|
|
3557
|
+
async _handleTransportDisconnect(error, closeCode, initialOptions) {
|
|
3558
|
+
if (this._closing || this._disconnecting || this.state === ConnectionState.CLOSED)
|
|
3316
3559
|
return;
|
|
3317
3560
|
// 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
|
|
3318
3561
|
if (this._reconnectActive)
|
|
@@ -3321,6 +3564,9 @@ export class AUNClient {
|
|
|
3321
3564
|
this._delivery.resetInlineAckState();
|
|
3322
3565
|
// 原子化标志,防止 await 期间并发调用再次进入
|
|
3323
3566
|
this._reconnectActive = true;
|
|
3567
|
+
// 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
|
|
3568
|
+
const reconnectAbort = new AbortController();
|
|
3569
|
+
this._reconnectAbort = reconnectAbort;
|
|
3324
3570
|
this._clientLog.warn(`transport disconnected: closeCode=${closeCode ?? 'none'}, error=${error ? formatCaughtError(error) : 'none'}`);
|
|
3325
3571
|
this._state = 'standby';
|
|
3326
3572
|
this._stopBackgroundTasks();
|
|
@@ -3330,9 +3576,28 @@ export class AUNClient {
|
|
|
3330
3576
|
catch (exc) {
|
|
3331
3577
|
this._clientLog.debug(`transport cleanup skipped: ${formatCaughtError(exc)}`);
|
|
3332
3578
|
}
|
|
3333
|
-
await this.
|
|
3579
|
+
if (!await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
3580
|
+
state: this._publicState(this._state),
|
|
3581
|
+
error,
|
|
3582
|
+
})) {
|
|
3583
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
3584
|
+
this._reconnectAbort = null;
|
|
3585
|
+
this._reconnectActive = false;
|
|
3586
|
+
}
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
|
|
3590
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
3591
|
+
this._reconnectAbort = null;
|
|
3592
|
+
this._reconnectActive = false;
|
|
3593
|
+
}
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3334
3596
|
if (!this._sessionOptions.auto_reconnect) {
|
|
3335
|
-
this.
|
|
3597
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
3598
|
+
this._reconnectAbort = null;
|
|
3599
|
+
this._reconnectActive = false;
|
|
3600
|
+
}
|
|
3336
3601
|
return;
|
|
3337
3602
|
}
|
|
3338
3603
|
// 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
|
|
@@ -3352,38 +3617,111 @@ export class AUNClient {
|
|
|
3352
3617
|
if (disconnectInfo.code !== undefined && disconnectInfo.code !== null) {
|
|
3353
3618
|
eventPayload.code = disconnectInfo.code;
|
|
3354
3619
|
}
|
|
3355
|
-
await this.
|
|
3356
|
-
this.
|
|
3620
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', eventPayload);
|
|
3621
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
3622
|
+
this._reconnectAbort = null;
|
|
3623
|
+
this._reconnectActive = false;
|
|
3624
|
+
}
|
|
3357
3625
|
return;
|
|
3358
3626
|
}
|
|
3359
3627
|
// 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
|
|
3360
3628
|
const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
|
|
3361
|
-
this.
|
|
3362
|
-
|
|
3629
|
+
if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
|
|
3630
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
3631
|
+
this._reconnectAbort = null;
|
|
3632
|
+
this._reconnectActive = false;
|
|
3633
|
+
}
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
this._startReconnect(serverInitiated, initialOptions, reconnectAbort);
|
|
3363
3637
|
}
|
|
3364
3638
|
/** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
|
|
3365
|
-
_startReconnect(serverInitiated = false) {
|
|
3366
|
-
if (
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3639
|
+
_startReconnect(serverInitiated = false, initialOptions, existingAbort) {
|
|
3640
|
+
if (existingAbort) {
|
|
3641
|
+
if (existingAbort.signal.aborted || this._reconnectAbort !== existingAbort)
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
else {
|
|
3645
|
+
if (this._reconnectActive)
|
|
3646
|
+
return;
|
|
3647
|
+
this._reconnectActive = true;
|
|
3648
|
+
this._reconnectAbort = new AbortController();
|
|
3649
|
+
}
|
|
3650
|
+
const reconnectAbort = existingAbort ?? this._reconnectAbort;
|
|
3371
3651
|
this._clientLog.debug(`reconnect loop started: serverInitiated=${String(serverInitiated)}, aid=${this._aid ?? ''}`);
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3652
|
+
if (!reconnectAbort)
|
|
3653
|
+
return;
|
|
3654
|
+
const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
|
|
3655
|
+
this._reconnectTask = reconnectTask;
|
|
3656
|
+
this._safeAsync(reconnectTask);
|
|
3657
|
+
}
|
|
3658
|
+
_ownsReconnect(owner) {
|
|
3659
|
+
return Boolean(owner
|
|
3660
|
+
&& !owner.signal.aborted
|
|
3661
|
+
&& !this._closing
|
|
3662
|
+
&& this._reconnectAbort === owner);
|
|
3663
|
+
}
|
|
3664
|
+
_assertReconnectOwner(owner) {
|
|
3665
|
+
if (owner && !this._ownsReconnect(owner)) {
|
|
3666
|
+
throw new ConnectionError('reconnect attempt superseded');
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
async _publishReconnectEvent(owner, event, payload) {
|
|
3670
|
+
if (!this._ownsReconnect(owner))
|
|
3671
|
+
return false;
|
|
3672
|
+
this._reconnectEventDispatchDepth += 1;
|
|
3673
|
+
try {
|
|
3674
|
+
await this._dispatcher.publish(event, payload);
|
|
3675
|
+
}
|
|
3676
|
+
finally {
|
|
3677
|
+
this._reconnectEventDispatchDepth -= 1;
|
|
3678
|
+
}
|
|
3679
|
+
return this._ownsReconnect(owner);
|
|
3680
|
+
}
|
|
3681
|
+
async _cancelReconnectAndWait() {
|
|
3682
|
+
const owner = this._reconnectAbort;
|
|
3683
|
+
const task = this._reconnectTask;
|
|
3684
|
+
if (!owner && !task && !this._reconnectActive)
|
|
3685
|
+
return;
|
|
3686
|
+
owner?.abort();
|
|
3687
|
+
this._discovery?._cancelPending?.();
|
|
3688
|
+
this._auth?._cancelPending?.();
|
|
3689
|
+
this._stopBackgroundTasks();
|
|
3690
|
+
try {
|
|
3691
|
+
await this._transport.close();
|
|
3692
|
+
}
|
|
3693
|
+
catch (exc) {
|
|
3694
|
+
this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
|
|
3695
|
+
}
|
|
3696
|
+
// 重连事件 handler 内等待所属任务会自锁;事件返回后的 owner 复核负责收尾。
|
|
3697
|
+
if (task && this._reconnectEventDispatchDepth === 0) {
|
|
3698
|
+
try {
|
|
3699
|
+
await task;
|
|
3700
|
+
}
|
|
3701
|
+
catch (exc) {
|
|
3702
|
+
this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
if (this._reconnectAbort === owner) {
|
|
3706
|
+
this._reconnectAbort = null;
|
|
3707
|
+
this._reconnectActive = false;
|
|
3708
|
+
this._nextRetryAt = null;
|
|
3709
|
+
}
|
|
3710
|
+
if (this._reconnectTask === task)
|
|
3711
|
+
this._reconnectTask = null;
|
|
3375
3712
|
}
|
|
3376
3713
|
/** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
|
|
3377
|
-
async _reconnectLoop(serverInitiated, reconnectAbort = this._reconnectAbort) {
|
|
3714
|
+
async _reconnectLoop(serverInitiated, reconnectAbort = this._reconnectAbort, initialOptions) {
|
|
3378
3715
|
let releasedOwnGeneration = false;
|
|
3379
3716
|
try {
|
|
3380
|
-
await this._runReconnectLoop(serverInitiated, reconnectAbort);
|
|
3717
|
+
await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
|
|
3381
3718
|
}
|
|
3382
3719
|
finally {
|
|
3383
3720
|
if (this._reconnectAbort === reconnectAbort) {
|
|
3384
3721
|
this._nextRetryAt = null;
|
|
3385
3722
|
this._reconnectActive = false;
|
|
3386
3723
|
this._reconnectAbort = null;
|
|
3724
|
+
this._reconnectTask = null;
|
|
3387
3725
|
releasedOwnGeneration = true;
|
|
3388
3726
|
}
|
|
3389
3727
|
}
|
|
@@ -3394,44 +3732,47 @@ export class AUNClient {
|
|
|
3394
3732
|
}
|
|
3395
3733
|
}
|
|
3396
3734
|
/** 重连循环主体(for 循环 + AbortController,与 JS/Python 对齐) */
|
|
3397
|
-
async _runReconnectLoop(serverInitiated, reconnectAbort) {
|
|
3735
|
+
async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
|
|
3398
3736
|
const retry = this._sessionOptions.retry;
|
|
3399
3737
|
const maxBaseDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64.0) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
|
|
3400
3738
|
const maxAttemptsRaw = Number(retry.max_attempts ?? 0);
|
|
3401
3739
|
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
|
|
3402
3740
|
this._retryMaxAttempts = maxAttempts;
|
|
3403
3741
|
let delay = clampReconnectDelayMs(serverInitiated ? 16_000 : Number(retry.initial_delay ?? 1.0) * 1000, serverInitiated ? 16_000 : RECONNECT_MIN_BASE_DELAY_MS, maxBaseDelay);
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3742
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3743
|
+
return;
|
|
3744
|
+
for (let attempt = 1; this._ownsReconnect(reconnectAbort); attempt++) {
|
|
3407
3745
|
// max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
|
|
3408
3746
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
3409
3747
|
this._state = 'connection_failed';
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3748
|
+
if (reconnectAbort)
|
|
3749
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
3750
|
+
state: this._publicState(this._state),
|
|
3751
|
+
attempt: attempt - 1,
|
|
3752
|
+
reason: 'max_attempts_exhausted',
|
|
3753
|
+
});
|
|
3415
3754
|
break;
|
|
3416
3755
|
}
|
|
3417
3756
|
this._retryAttempt = attempt;
|
|
3418
3757
|
this._nextRetryAt = Date.now() + reconnectSleepDelayMs(delay, maxBaseDelay);
|
|
3419
3758
|
this._state = 'retry_backoff';
|
|
3420
|
-
await this.
|
|
3759
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
3421
3760
|
state: this._publicState(this._state),
|
|
3422
3761
|
attempt,
|
|
3423
3762
|
next_retry_at: this._nextRetryAt,
|
|
3424
|
-
})
|
|
3763
|
+
}))
|
|
3764
|
+
return;
|
|
3425
3765
|
try {
|
|
3426
3766
|
// 抖动随当前 base 增长且不超过 max_base,避免首次重连被放大到几十秒。
|
|
3427
3767
|
await this._sleep(Math.max(0, this._nextRetryAt - Date.now()), reconnectAbort?.signal);
|
|
3428
|
-
if (
|
|
3429
|
-
|
|
3768
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3769
|
+
return;
|
|
3430
3770
|
this._state = 'reconnecting';
|
|
3431
|
-
await this.
|
|
3771
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
3432
3772
|
state: this._publicState(this._state),
|
|
3433
3773
|
attempt,
|
|
3434
|
-
})
|
|
3774
|
+
}))
|
|
3775
|
+
return;
|
|
3435
3776
|
// /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
|
|
3436
3777
|
if (this._gatewayUrl) {
|
|
3437
3778
|
void this._discovery.checkHealth(this._gatewayUrl, 5_000)
|
|
@@ -3444,6 +3785,20 @@ export class AUNClient {
|
|
|
3444
3785
|
});
|
|
3445
3786
|
}
|
|
3446
3787
|
await this._transport.close();
|
|
3788
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3789
|
+
return;
|
|
3790
|
+
if (initialOptions) {
|
|
3791
|
+
// 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
|
|
3792
|
+
await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
|
|
3793
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3794
|
+
return;
|
|
3795
|
+
if (!this._transport.isConnected()) {
|
|
3796
|
+
throw new ConnectionError('websocket disconnected while initial reconnect completed');
|
|
3797
|
+
}
|
|
3798
|
+
this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
|
|
3799
|
+
this._nextRetryAt = null;
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3447
3802
|
if (this._sessionParams === null) {
|
|
3448
3803
|
throw new StateError('missing connect params for reconnect');
|
|
3449
3804
|
}
|
|
@@ -3471,7 +3826,9 @@ export class AUNClient {
|
|
|
3471
3826
|
for (const gateway of candidates) {
|
|
3472
3827
|
const attemptParams = { ...this._sessionParams, gateway };
|
|
3473
3828
|
try {
|
|
3474
|
-
await this._connectOnce(attemptParams, true);
|
|
3829
|
+
await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
|
|
3830
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3831
|
+
return;
|
|
3475
3832
|
if (!this._transport.isConnected()) {
|
|
3476
3833
|
throw new ConnectionError('websocket disconnected while reconnecting');
|
|
3477
3834
|
}
|
|
@@ -3480,8 +3837,12 @@ export class AUNClient {
|
|
|
3480
3837
|
break;
|
|
3481
3838
|
}
|
|
3482
3839
|
catch (exc) {
|
|
3840
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3841
|
+
return;
|
|
3483
3842
|
lastGatewayError = exc;
|
|
3484
3843
|
await this._transport.close().catch(() => { });
|
|
3844
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3845
|
+
return;
|
|
3485
3846
|
if (candidates.length > 1) {
|
|
3486
3847
|
this._clientLog.warn(`reconnect gateway failed, trying next: gateway=${gateway}, error=${formatCaughtError(exc)}`);
|
|
3487
3848
|
}
|
|
@@ -3497,17 +3858,21 @@ export class AUNClient {
|
|
|
3497
3858
|
return;
|
|
3498
3859
|
}
|
|
3499
3860
|
catch (exc) {
|
|
3500
|
-
|
|
3861
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3862
|
+
return;
|
|
3863
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
|
|
3501
3864
|
error: formatCaughtError(exc),
|
|
3502
3865
|
attempt,
|
|
3503
|
-
})
|
|
3866
|
+
}))
|
|
3867
|
+
return;
|
|
3504
3868
|
if (!AUNClient._shouldRetryReconnect(exc)) {
|
|
3505
3869
|
this._state = 'connection_failed';
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3870
|
+
if (reconnectAbort)
|
|
3871
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
3872
|
+
state: this._publicState(this._state),
|
|
3873
|
+
error: formatCaughtError(exc),
|
|
3874
|
+
attempt,
|
|
3875
|
+
});
|
|
3511
3876
|
break;
|
|
3512
3877
|
}
|
|
3513
3878
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
@@ -3573,7 +3938,7 @@ export class AUNClient {
|
|
|
3573
3938
|
|| error instanceof ValidationError || error instanceof StateError) {
|
|
3574
3939
|
return false;
|
|
3575
3940
|
}
|
|
3576
|
-
if (error instanceof ConnectionError)
|
|
3941
|
+
if (error instanceof ConnectionError || error instanceof TimeoutError)
|
|
3577
3942
|
return true;
|
|
3578
3943
|
if (error instanceof AUNError)
|
|
3579
3944
|
return error.retryable;
|