@agentunion/fastaun-browser 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 +104 -0
- package/_packed_docs/CHANGELOG.md +104 -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 +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +230 -10
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +5 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +24 -4
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +29 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +30253 -21955
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +36 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +40 -5
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +719 -221
- 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.d.ts.map +1 -1
- package/dist/client/lifecycle.js +463 -151
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +25 -6
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +407 -123
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.d.ts.map +1 -1
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +434 -112
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +22 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +500 -116
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +23 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +55 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +8 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +23 -1
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +8 -1
- package/dist/transport.js.map +1 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
package/dist/client.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// - 后台任务使用 setTimeout/setInterval
|
|
7
7
|
import { createConfig, getDeviceId, normalizeSlotId } from './config.js';
|
|
8
8
|
import { EventDispatcher } from './events.js';
|
|
9
|
+
import { normalizeGroupId } from './group-id.js';
|
|
9
10
|
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
10
11
|
import { GatewayDiscovery } from './discovery.js';
|
|
11
|
-
import { AIDStore } from './aid-store.js';
|
|
12
12
|
import { RPCTransport } from './transport.js';
|
|
13
13
|
import { AuthFlow } from './auth.js';
|
|
14
14
|
import { SeqTracker } from './seq-tracker.js';
|
|
@@ -19,7 +19,7 @@ import { IdentityRuntimeManager } from './client/identity.js';
|
|
|
19
19
|
import { LifecycleController } from './client/lifecycle.js';
|
|
20
20
|
import { PeerDirectory } from './client/peers.js';
|
|
21
21
|
import { RpcPipeline } from './client/rpc-pipeline.js';
|
|
22
|
-
import { GroupFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
22
|
+
import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
23
23
|
import { StorageVFS } from './storage/vfs.js';
|
|
24
24
|
import { CollabClient } from './collab/client.js';
|
|
25
25
|
import { V2E2EECoordinator } from './client/v2-e2ee.js';
|
|
@@ -29,7 +29,7 @@ import { IndexedDBTokenStore } from './keystore/indexeddb-token-store.js';
|
|
|
29
29
|
import { decryptMessage, } from './v2/e2ee/index.js';
|
|
30
30
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
31
31
|
import { AUNLogger } from './logger.js';
|
|
32
|
-
import { AgentMdManager } from './agent-md.js';
|
|
32
|
+
import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
|
|
33
33
|
import { GroupIndexMetaCache } from './group-index.js';
|
|
34
34
|
import { certMatchesFingerprint, normalizeFingerprintHex } from './cert-utils.js';
|
|
35
35
|
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
|
|
@@ -99,7 +99,7 @@ const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
|
|
|
99
99
|
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
100
100
|
const NON_IDEMPOTENT_TIMEOUT = 35;
|
|
101
101
|
const NON_IDEMPOTENT_METHODS = new Set([
|
|
102
|
-
'message.send', 'group.send', 'group.create', 'group.invite',
|
|
102
|
+
'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
|
|
103
103
|
'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
|
|
104
104
|
'group.set_settings',
|
|
105
105
|
'group.update_announcement', 'group.update_rules',
|
|
@@ -231,6 +231,7 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
231
231
|
return new AgentMdManager({
|
|
232
232
|
aunPath: opts.config().aunPath,
|
|
233
233
|
tokenStore: opts.tokenStore(),
|
|
234
|
+
uploadViaGatewayHost: true,
|
|
234
235
|
logger: opts.logger(),
|
|
235
236
|
ownerAidGetter: opts.ownerAid,
|
|
236
237
|
currentAidGetter: opts.currentAid,
|
|
@@ -561,6 +562,8 @@ export class AUNClient {
|
|
|
561
562
|
_v2LazyProposeTriggered = new Map();
|
|
562
563
|
/** agent.md 运行时管理器,负责上传、下载、缓存和 RPC 元数据观察。 */
|
|
563
564
|
_agentMdManager;
|
|
565
|
+
_aidStore;
|
|
566
|
+
_groupIdentityOperations = new Map();
|
|
564
567
|
_groupIndexMetaCache = new GroupIndexMetaCache();
|
|
565
568
|
_groupIndexCacheLoaded = new Set();
|
|
566
569
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
@@ -580,12 +583,6 @@ export class AUNClient {
|
|
|
580
583
|
_messageRecallSeen = new Map();
|
|
581
584
|
/** 群撤回去重:group_id|sorted(message_ids) -> 时间戳,保证应用层只回调一次 */
|
|
582
585
|
_groupRecallSeen = new Map();
|
|
583
|
-
/** 在线未读 hint 队列:同一 group 只保留最后一条,延迟 drain 降低登录瞬时拉取压力。 */
|
|
584
|
-
_onlineUnreadHintQueue = new Map();
|
|
585
|
-
_onlineUnreadHintTimer = null;
|
|
586
|
-
_onlineUnreadHintDrainActive = false;
|
|
587
|
-
_onlineUnreadHintInitialDelayMs = 750;
|
|
588
|
-
_onlineUnreadHintIntervalMs = 50;
|
|
589
586
|
/** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
|
|
590
587
|
_gapFillActive = false;
|
|
591
588
|
_backgroundRpcDepth = 0;
|
|
@@ -595,6 +592,8 @@ export class AUNClient {
|
|
|
595
592
|
// 重连相关
|
|
596
593
|
_reconnectActive = false;
|
|
597
594
|
_reconnectAbort = null;
|
|
595
|
+
_reconnectTask = null;
|
|
596
|
+
_reconnectEventDispatchDepth = 0;
|
|
598
597
|
_serverKicked = false;
|
|
599
598
|
// 重连状态追踪(对齐 Python client.py)
|
|
600
599
|
_nextRetryAt = null;
|
|
@@ -628,6 +627,7 @@ export class AUNClient {
|
|
|
628
627
|
_storage;
|
|
629
628
|
_collab;
|
|
630
629
|
_messageFacade;
|
|
630
|
+
_mailFacade;
|
|
631
631
|
_groupFacade;
|
|
632
632
|
_streamFacade;
|
|
633
633
|
constructor(aid) {
|
|
@@ -635,6 +635,7 @@ export class AUNClient {
|
|
|
635
635
|
throw new ValidationError('AUNClient only accepts an AID object or no argument');
|
|
636
636
|
}
|
|
637
637
|
const inputAid = aid ?? null;
|
|
638
|
+
this._aidStore = inputAid?._aidStore;
|
|
638
639
|
const rawConfig = {};
|
|
639
640
|
if (inputAid)
|
|
640
641
|
rawConfig.aun_path = inputAid.aunPath;
|
|
@@ -770,7 +771,7 @@ export class AUNClient {
|
|
|
770
771
|
this._safeAsync(this._onGroupStateCommitted(data));
|
|
771
772
|
});
|
|
772
773
|
// 其他事件直接透传
|
|
773
|
-
for (const evt of ['message.ack', 'storage.object_changed']) {
|
|
774
|
+
for (const evt of ['message.ack', 'storage.object_changed', 'stream/created', 'stream/closed']) {
|
|
774
775
|
this._dispatcher.subscribe(`_raw.${evt}`, (data) => {
|
|
775
776
|
this._dispatcher.publish(evt, data);
|
|
776
777
|
});
|
|
@@ -784,6 +785,14 @@ export class AUNClient {
|
|
|
784
785
|
get aid() {
|
|
785
786
|
return this._aid;
|
|
786
787
|
}
|
|
788
|
+
/** 返回后台刷新任务维护的最新 access token。 */
|
|
789
|
+
getAccessToken() {
|
|
790
|
+
const identityToken = String(this._identity?.access_token ?? '').trim();
|
|
791
|
+
if (identityToken)
|
|
792
|
+
return identityToken;
|
|
793
|
+
const sessionToken = String(this._sessionParams?.access_token ?? '').trim();
|
|
794
|
+
return sessionToken || null;
|
|
795
|
+
}
|
|
787
796
|
/** AUN Storage VFS 入口 */
|
|
788
797
|
get storage() {
|
|
789
798
|
if (!this._storage)
|
|
@@ -849,6 +858,12 @@ export class AUNClient {
|
|
|
849
858
|
}
|
|
850
859
|
void this._persistGroupIndexCache(groupAid, fields);
|
|
851
860
|
}
|
|
861
|
+
/** Mail bridge facade 入口 */
|
|
862
|
+
get mail() {
|
|
863
|
+
if (!this._mailFacade)
|
|
864
|
+
this._mailFacade = new MailFacade(this);
|
|
865
|
+
return this._mailFacade;
|
|
866
|
+
}
|
|
852
867
|
getGroupIndexRemoteMeta(groupAid) {
|
|
853
868
|
return this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid);
|
|
854
869
|
}
|
|
@@ -1008,6 +1023,7 @@ export class AUNClient {
|
|
|
1008
1023
|
this._v2PullPending = false;
|
|
1009
1024
|
}
|
|
1010
1025
|
_applyAidRuntimeContext(aid) {
|
|
1026
|
+
this._aidStore = aid._aidStore;
|
|
1011
1027
|
const oldTransport = this._transport;
|
|
1012
1028
|
try {
|
|
1013
1029
|
const closeResult = oldTransport?.close?.();
|
|
@@ -1033,6 +1049,7 @@ export class AUNClient {
|
|
|
1033
1049
|
this._peerCache.clear();
|
|
1034
1050
|
this._certCache.clear();
|
|
1035
1051
|
this._gatewayUrl = null;
|
|
1052
|
+
this._gatewayCandidates = [];
|
|
1036
1053
|
this._deviceId = aid.deviceId || getDeviceId();
|
|
1037
1054
|
this._slotId = aid.slotId || 'default';
|
|
1038
1055
|
this._logger = new AUNLogger({ debug: aid.debug, aunPath: nextConfig.aunPath });
|
|
@@ -1173,16 +1190,126 @@ export class AUNClient {
|
|
|
1173
1190
|
return;
|
|
1174
1191
|
await this._handleTransportDisconnect(err);
|
|
1175
1192
|
}
|
|
1176
|
-
async
|
|
1193
|
+
async _recoverIdentityAdmission(err, _method = '', params = {}) {
|
|
1194
|
+
if (!(err instanceof AUNError) || err.code !== -32003)
|
|
1195
|
+
return false;
|
|
1196
|
+
const message = String(err.message ?? '').trim();
|
|
1197
|
+
const groupCertPrefix = 'valid group identity certificate is required for ';
|
|
1198
|
+
if (message.toLowerCase().startsWith(groupCertPrefix)) {
|
|
1199
|
+
const fallback = message.slice(groupCertPrefix.length).trim();
|
|
1200
|
+
const store = this._aidStore;
|
|
1201
|
+
const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
|
|
1202
|
+
if (!groupId || !store)
|
|
1203
|
+
return false;
|
|
1204
|
+
try {
|
|
1205
|
+
await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
const groupMDPrefix = 'valid group agent.md is required for ';
|
|
1213
|
+
if (message.toLowerCase().startsWith(groupMDPrefix)) {
|
|
1214
|
+
const groupAid = message.slice(groupMDPrefix.length).trim();
|
|
1215
|
+
const store = this._aidStore;
|
|
1216
|
+
if (!groupAid || !store)
|
|
1217
|
+
return false;
|
|
1218
|
+
const loaded = await store.load(groupAid);
|
|
1219
|
+
if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
|
|
1220
|
+
return false;
|
|
1221
|
+
const ownerAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1222
|
+
if (!ownerAid)
|
|
1223
|
+
return false;
|
|
1224
|
+
await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}, ownerAid));
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
const agentPrefix = 'valid agent.md is required for ';
|
|
1228
|
+
if (!message.toLowerCase().startsWith(agentPrefix))
|
|
1229
|
+
return false;
|
|
1230
|
+
const target = message.slice(agentPrefix.length).trim();
|
|
1231
|
+
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1232
|
+
return false;
|
|
1233
|
+
await this._agentMdManager.upload(await this._agentMdManager.readContent(target) ?? buildDefaultAgentMd(target));
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
async _runGroupIdentityOperation(groupId, operation) {
|
|
1237
|
+
const aid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1238
|
+
const dot = aid.indexOf('.');
|
|
1239
|
+
const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
|
|
1240
|
+
if (!key)
|
|
1241
|
+
return operation();
|
|
1242
|
+
let task = this._groupIdentityOperations.get(key);
|
|
1243
|
+
if (!task) {
|
|
1244
|
+
task = Promise.resolve().then(operation);
|
|
1245
|
+
this._groupIdentityOperations.set(key, task);
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
return await task;
|
|
1249
|
+
}
|
|
1250
|
+
finally {
|
|
1251
|
+
if (this._groupIdentityOperations.get(key) === task)
|
|
1252
|
+
this._groupIdentityOperations.delete(key);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async createGroup(params = {}, options = {}) {
|
|
1177
1256
|
if (!isJsonObject(params)) {
|
|
1178
1257
|
throw new ValidationError('createGroup params must be an object');
|
|
1179
1258
|
}
|
|
1259
|
+
const store = options.aidStore ?? this._aidStore;
|
|
1260
|
+
if (!store) {
|
|
1261
|
+
throw new ValidationError('createGroup requires aidStore');
|
|
1262
|
+
}
|
|
1263
|
+
this._aidStore = store;
|
|
1180
1264
|
const payload = { ...params };
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1265
|
+
if (payload.group_name === undefined && payload.groupName !== undefined) {
|
|
1266
|
+
payload.group_name = payload.groupName;
|
|
1267
|
+
delete payload.groupName;
|
|
1268
|
+
}
|
|
1269
|
+
const isNamed = Boolean(String(payload.group_name ?? '').trim());
|
|
1270
|
+
payload._defer_group_ready_postprocess = true;
|
|
1271
|
+
if (!isNamed) {
|
|
1272
|
+
delete payload.public_key;
|
|
1273
|
+
delete payload.curve;
|
|
1274
|
+
const created = await this.call('group.create', payload);
|
|
1275
|
+
const createdMap = isJsonObject(created) ? created : {};
|
|
1276
|
+
const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
|
|
1277
|
+
const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
|
|
1278
|
+
if (!groupId) {
|
|
1279
|
+
throw new ValidationError('group.create response missing group.group_id');
|
|
1280
|
+
}
|
|
1281
|
+
const bindParams = { group_id: groupId };
|
|
1282
|
+
for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
|
|
1283
|
+
if (key in payload)
|
|
1284
|
+
bindParams[key] = payload[key];
|
|
1285
|
+
}
|
|
1286
|
+
const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
|
|
1287
|
+
const boundMap = isJsonObject(bound) ? bound : {};
|
|
1288
|
+
const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
|
|
1289
|
+
const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
|
|
1290
|
+
const postprocessParams = { ...payload };
|
|
1291
|
+
delete postprocessParams._defer_group_ready_postprocess;
|
|
1292
|
+
return await this._groupState.postprocessResult('group.create', postprocessParams, result);
|
|
1293
|
+
}
|
|
1294
|
+
const pendingKey = `create:${String(payload.group_name).trim().toLowerCase()}`;
|
|
1295
|
+
const keystore = store._keystore;
|
|
1296
|
+
let keyPair = null;
|
|
1297
|
+
if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1298
|
+
keyPair = await keystore.loadPendingGroupBind(pendingKey);
|
|
1299
|
+
}
|
|
1300
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1301
|
+
const generated = await new CryptoProvider().generateIdentity();
|
|
1302
|
+
keyPair = {
|
|
1303
|
+
private_key_pem: generated.private_key_pem,
|
|
1304
|
+
public_key_der_b64: generated.public_key_der_b64,
|
|
1305
|
+
curve: generated.curve,
|
|
1306
|
+
};
|
|
1307
|
+
if (keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1308
|
+
await keystore.savePendingGroupBind(pendingKey, keyPair);
|
|
1309
|
+
}
|
|
1184
1310
|
}
|
|
1185
|
-
|
|
1311
|
+
if (!keyPair)
|
|
1312
|
+
throw new ValidationError('createGroup: failed to generate or load key pair');
|
|
1186
1313
|
payload.public_key = keyPair.public_key_der_b64;
|
|
1187
1314
|
payload.curve = keyPair.curve;
|
|
1188
1315
|
const result = await this.call('group.create', payload);
|
|
@@ -1192,30 +1319,66 @@ export class AUNClient {
|
|
|
1192
1319
|
const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
|
|
1193
1320
|
const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
|
|
1194
1321
|
if (!groupAid || !certPem) {
|
|
1195
|
-
throw new ValidationError('group.create
|
|
1322
|
+
throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
|
|
1196
1323
|
}
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
verifySsl: this.configModel.verifySsl,
|
|
1324
|
+
const imported = await store.importGroupIdentity(groupAid, {
|
|
1325
|
+
private_key_pem: keyPair.private_key_pem,
|
|
1326
|
+
public_key_der_b64: keyPair.public_key_der_b64,
|
|
1327
|
+
curve: keyPair.curve,
|
|
1328
|
+
cert_pem: certPem,
|
|
1203
1329
|
});
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
private_key_pem: keyPair.private_key_pem,
|
|
1207
|
-
public_key_der_b64: keyPair.public_key_der_b64,
|
|
1208
|
-
curve: keyPair.curve,
|
|
1209
|
-
cert_pem: certPem,
|
|
1210
|
-
});
|
|
1211
|
-
if (!imported.ok) {
|
|
1212
|
-
throw new ValidationError(imported.error.message);
|
|
1213
|
-
}
|
|
1330
|
+
if (!imported.ok) {
|
|
1331
|
+
throw new ValidationError(imported.error.message);
|
|
1214
1332
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1333
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1334
|
+
if (!uploaderAid) {
|
|
1335
|
+
throw new ValidationError('createGroup requires current owner AID for group agent.md upload');
|
|
1336
|
+
}
|
|
1337
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
|
|
1338
|
+
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1339
|
+
await keystore.clearPendingGroupBind(pendingKey);
|
|
1340
|
+
}
|
|
1341
|
+
const postprocessParams = { ...payload };
|
|
1342
|
+
delete postprocessParams._defer_group_ready_postprocess;
|
|
1343
|
+
return await this._groupState.postprocessResult('group.create', postprocessParams, result);
|
|
1344
|
+
}
|
|
1345
|
+
_groupAgentMdContent(groupAid, params = {}, group = {}) {
|
|
1346
|
+
const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
|
|
1347
|
+
if (explicit !== undefined && explicit !== null)
|
|
1348
|
+
return String(explicit);
|
|
1349
|
+
const value = (...keys) => {
|
|
1350
|
+
for (const source of [group, params]) {
|
|
1351
|
+
for (const key of keys) {
|
|
1352
|
+
const candidate = String(source[key] ?? '').trim();
|
|
1353
|
+
if (candidate)
|
|
1354
|
+
return candidate;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return '';
|
|
1358
|
+
};
|
|
1359
|
+
const name = value('name', 'group_name', 'title') || groupAid;
|
|
1360
|
+
const normalizedDescription = (value('description', 'group_description') || `AUN group ${name}`)
|
|
1361
|
+
.split(/\s+/).filter(Boolean).join(' ');
|
|
1362
|
+
const description = Array.from(normalizedDescription).slice(0, 100).join('');
|
|
1363
|
+
return [
|
|
1364
|
+
'---\n',
|
|
1365
|
+
`aid: ${JSON.stringify(groupAid)}\n`,
|
|
1366
|
+
`name: ${JSON.stringify(name)}\n`,
|
|
1367
|
+
'type: "group"\n',
|
|
1368
|
+
'version: "1.0.0"\n',
|
|
1369
|
+
`description: ${JSON.stringify(description)}\n`,
|
|
1370
|
+
'---\n\n',
|
|
1371
|
+
`# ${name}\n`,
|
|
1372
|
+
].join('');
|
|
1373
|
+
}
|
|
1374
|
+
async _uploadGroupAgentMd(store, groupAid, params, group, uploaderAid) {
|
|
1375
|
+
if (typeof store.uploadAgentMd !== 'function') {
|
|
1376
|
+
throw new ValidationError('group agent.md upload is unavailable');
|
|
1377
|
+
}
|
|
1378
|
+
const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
|
|
1379
|
+
if (!uploaded.ok) {
|
|
1380
|
+
throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
|
|
1217
1381
|
}
|
|
1218
|
-
return result;
|
|
1219
1382
|
}
|
|
1220
1383
|
async bindGroupAid(params = {}, options = {}) {
|
|
1221
1384
|
if (!isJsonObject(params)) {
|
|
@@ -1225,12 +1388,17 @@ export class AUNClient {
|
|
|
1225
1388
|
if (!store) {
|
|
1226
1389
|
throw new ValidationError('bindGroupAid requires aidStore');
|
|
1227
1390
|
}
|
|
1391
|
+
this._aidStore = store;
|
|
1228
1392
|
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1393
|
+
const pendingKey = `bind:${groupId}`;
|
|
1229
1394
|
const keystore = store._keystore;
|
|
1230
1395
|
let keyPair = null;
|
|
1231
1396
|
// 优先复用 pending 槽位中已暂存的待绑定密钥(崩溃/重试幂等)
|
|
1232
1397
|
if (groupId && keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1233
|
-
keyPair = await keystore.loadPendingGroupBind(
|
|
1398
|
+
keyPair = await keystore.loadPendingGroupBind(pendingKey);
|
|
1399
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1400
|
+
keyPair = await keystore.loadPendingGroupBind(groupId);
|
|
1401
|
+
}
|
|
1234
1402
|
}
|
|
1235
1403
|
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1236
1404
|
// 未命中 pending,生成新密钥
|
|
@@ -1242,7 +1410,7 @@ export class AUNClient {
|
|
|
1242
1410
|
};
|
|
1243
1411
|
// 发 RPC 前先落盘暂存,确保崩溃后重试能复用同一密钥
|
|
1244
1412
|
if (groupId && keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1245
|
-
await keystore.savePendingGroupBind(
|
|
1413
|
+
await keystore.savePendingGroupBind(pendingKey, keyPair);
|
|
1246
1414
|
}
|
|
1247
1415
|
}
|
|
1248
1416
|
if (!keyPair) {
|
|
@@ -1272,8 +1440,14 @@ export class AUNClient {
|
|
|
1272
1440
|
if (!imported.ok) {
|
|
1273
1441
|
throw new ValidationError(imported.error.message);
|
|
1274
1442
|
}
|
|
1443
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1444
|
+
if (!uploaderAid) {
|
|
1445
|
+
throw new ValidationError('bindGroupAid requires current owner AID for group agent.md upload');
|
|
1446
|
+
}
|
|
1447
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
|
|
1275
1448
|
// 落盘成功,清除 pending 槽位
|
|
1276
1449
|
if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1450
|
+
await keystore.clearPendingGroupBind(pendingKey);
|
|
1277
1451
|
await keystore.clearPendingGroupBind(groupId);
|
|
1278
1452
|
}
|
|
1279
1453
|
return result;
|
|
@@ -1293,7 +1467,7 @@ export class AUNClient {
|
|
|
1293
1467
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1294
1468
|
if (!groupAid) {
|
|
1295
1469
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1296
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1470
|
+
groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
|
|
1297
1471
|
}
|
|
1298
1472
|
if (!groupAid) {
|
|
1299
1473
|
throw new ValidationError('renewGroupAid: unable to determine group_aid');
|
|
@@ -1310,7 +1484,25 @@ export class AUNClient {
|
|
|
1310
1484
|
if (!oldPublicKey) {
|
|
1311
1485
|
throw new ValidationError(`renewGroupAid: cannot determine old public key for ${groupAid}`);
|
|
1312
1486
|
}
|
|
1313
|
-
const
|
|
1487
|
+
const keystore = store._keystore;
|
|
1488
|
+
const pendingKey = `renew:${groupId}`;
|
|
1489
|
+
let newKeyPair = null;
|
|
1490
|
+
if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1491
|
+
newKeyPair = await keystore.loadPendingGroupBind(pendingKey);
|
|
1492
|
+
}
|
|
1493
|
+
if (!newKeyPair || !newKeyPair.public_key_der_b64 || !newKeyPair.private_key_pem) {
|
|
1494
|
+
const generated = await new CryptoProvider().generateIdentity();
|
|
1495
|
+
newKeyPair = {
|
|
1496
|
+
private_key_pem: generated.private_key_pem,
|
|
1497
|
+
public_key_der_b64: generated.public_key_der_b64,
|
|
1498
|
+
curve: generated.curve,
|
|
1499
|
+
};
|
|
1500
|
+
if (keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1501
|
+
await keystore.savePendingGroupBind(pendingKey, newKeyPair);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (!newKeyPair)
|
|
1505
|
+
throw new ValidationError('renewGroupAid: failed to generate or load key pair');
|
|
1314
1506
|
const newPublicKey = newKeyPair.public_key_der_b64;
|
|
1315
1507
|
const newPrivateKey = newKeyPair.private_key_pem;
|
|
1316
1508
|
const curve = newKeyPair.curve || 'P-256';
|
|
@@ -1361,6 +1553,14 @@ export class AUNClient {
|
|
|
1361
1553
|
if (!imported.ok) {
|
|
1362
1554
|
throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
|
|
1363
1555
|
}
|
|
1556
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1557
|
+
if (!uploaderAid) {
|
|
1558
|
+
throw new ValidationError('renewGroupAid requires current owner AID for group agent.md upload');
|
|
1559
|
+
}
|
|
1560
|
+
await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
|
|
1561
|
+
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1562
|
+
await keystore.clearPendingGroupBind(pendingKey);
|
|
1563
|
+
}
|
|
1364
1564
|
return result;
|
|
1365
1565
|
}
|
|
1366
1566
|
async startGroupTransfer(params = {}, options = {}) {
|
|
@@ -1379,7 +1579,7 @@ export class AUNClient {
|
|
|
1379
1579
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1380
1580
|
if (!groupAid) {
|
|
1381
1581
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1382
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1582
|
+
groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
|
|
1383
1583
|
}
|
|
1384
1584
|
if (!groupAid) {
|
|
1385
1585
|
throw new ValidationError('startGroupTransfer: unable to determine group_aid');
|
|
@@ -1418,20 +1618,38 @@ export class AUNClient {
|
|
|
1418
1618
|
if (!store) {
|
|
1419
1619
|
throw new ValidationError('completeGroupTransfer requires aidStore');
|
|
1420
1620
|
}
|
|
1421
|
-
const keyPair = await new CryptoProvider().generateIdentity();
|
|
1422
1621
|
const payload = { ...params };
|
|
1423
1622
|
const groupId = String(params.group_id ?? '').trim();
|
|
1424
1623
|
if (!groupId) {
|
|
1425
1624
|
throw new ValidationError('completeGroupTransfer requires group_id');
|
|
1426
1625
|
}
|
|
1626
|
+
const pendingKey = `complete:${groupId}`;
|
|
1427
1627
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1428
1628
|
if (!groupAid) {
|
|
1429
1629
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1430
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1630
|
+
groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
|
|
1431
1631
|
}
|
|
1432
1632
|
if (!groupAid) {
|
|
1433
1633
|
throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
|
|
1434
1634
|
}
|
|
1635
|
+
const keystore = store._keystore;
|
|
1636
|
+
let keyPair = null;
|
|
1637
|
+
if (keystore && typeof keystore.loadPendingGroupBind === 'function') {
|
|
1638
|
+
keyPair = await keystore.loadPendingGroupBind(pendingKey);
|
|
1639
|
+
}
|
|
1640
|
+
if (!keyPair || !keyPair.public_key_der_b64 || !keyPair.private_key_pem) {
|
|
1641
|
+
const generated = await new CryptoProvider().generateIdentity();
|
|
1642
|
+
keyPair = {
|
|
1643
|
+
private_key_pem: generated.private_key_pem,
|
|
1644
|
+
public_key_der_b64: generated.public_key_der_b64,
|
|
1645
|
+
curve: generated.curve,
|
|
1646
|
+
};
|
|
1647
|
+
if (keystore && typeof keystore.savePendingGroupBind === 'function') {
|
|
1648
|
+
await keystore.savePendingGroupBind(pendingKey, keyPair);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (!keyPair)
|
|
1652
|
+
throw new ValidationError('completeGroupTransfer: failed to generate or load key pair');
|
|
1435
1653
|
const current = this.currentAid;
|
|
1436
1654
|
const newOwner = String(current?.aid ?? '').trim();
|
|
1437
1655
|
if (!current || !newOwner || !current.isPrivateKeyValid()) {
|
|
@@ -1475,6 +1693,14 @@ export class AUNClient {
|
|
|
1475
1693
|
if (!imported.ok) {
|
|
1476
1694
|
throw new ValidationError(imported.error.message);
|
|
1477
1695
|
}
|
|
1696
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1697
|
+
if (!uploaderAid) {
|
|
1698
|
+
throw new ValidationError('completeGroupTransfer requires current owner AID for group agent.md upload');
|
|
1699
|
+
}
|
|
1700
|
+
await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
|
|
1701
|
+
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1702
|
+
await keystore.clearPendingGroupBind(pendingKey);
|
|
1703
|
+
}
|
|
1478
1704
|
return result;
|
|
1479
1705
|
}
|
|
1480
1706
|
static _notifyParamsSizeOk(params) {
|
|
@@ -1692,14 +1918,14 @@ export class AUNClient {
|
|
|
1692
1918
|
const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
1693
1919
|
msg.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
1694
1920
|
}
|
|
1695
|
-
async _drainOrderedMessages(ns, beforeSeq) {
|
|
1696
|
-
await this._delivery.drainOrderedMessages(ns, beforeSeq);
|
|
1921
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
1922
|
+
await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
|
|
1697
1923
|
}
|
|
1698
1924
|
async _publishOrderedMessage(event, ns, seq, payload) {
|
|
1699
1925
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload);
|
|
1700
1926
|
}
|
|
1701
|
-
async _publishPulledMessage(event, ns, seq, payload) {
|
|
1702
|
-
return this._delivery.publishPulledMessage(event, ns, seq, payload);
|
|
1927
|
+
async _publishPulledMessage(event, ns, seq, payload, persist = true) {
|
|
1928
|
+
return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
|
|
1703
1929
|
}
|
|
1704
1930
|
_extractGroupIdFromResult(result) {
|
|
1705
1931
|
const group = isJsonObject(result.group) ? result.group : null;
|
|
@@ -1937,8 +2163,10 @@ export class AUNClient {
|
|
|
1937
2163
|
await this._rpcPipeline.signClientOperation(method, params);
|
|
1938
2164
|
}
|
|
1939
2165
|
// ── 内部:连接 ────────────────────────────────────
|
|
1940
|
-
async _connectOnce(params, allowReauth) {
|
|
2166
|
+
async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
|
|
1941
2167
|
const tStart = Date.now();
|
|
2168
|
+
connectionOwner ??= this._connectAbort ?? undefined;
|
|
2169
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1942
2170
|
this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
|
|
1943
2171
|
try {
|
|
1944
2172
|
const gatewayUrl = this._resolveGateway(params);
|
|
@@ -1951,8 +2179,12 @@ export class AUNClient {
|
|
|
1951
2179
|
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
1952
2180
|
this._refreshSeqTrackerContext();
|
|
1953
2181
|
await this._restoreSeqTrackerState();
|
|
2182
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2183
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1954
2184
|
try {
|
|
1955
2185
|
const challenge = await this._transport.connect(gatewayUrl);
|
|
2186
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2187
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1956
2188
|
this._state = 'authenticating';
|
|
1957
2189
|
if (allowReauth) {
|
|
1958
2190
|
const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
|
|
@@ -1964,6 +2196,8 @@ export class AUNClient {
|
|
|
1964
2196
|
shortTtlMs: Number(params.short_ttl_ms ?? 0),
|
|
1965
2197
|
extraInfo: params.extra_info,
|
|
1966
2198
|
});
|
|
2199
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2200
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1967
2201
|
if (isJsonObject(authContext)) {
|
|
1968
2202
|
const auth = authContext;
|
|
1969
2203
|
const identity = auth.identity;
|
|
@@ -1988,7 +2222,10 @@ export class AUNClient {
|
|
|
1988
2222
|
shortTtlMs: Number(params.short_ttl_ms ?? 0),
|
|
1989
2223
|
extraInfo: params.extra_info,
|
|
1990
2224
|
});
|
|
2225
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2226
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1991
2227
|
await this._syncIdentityAfterConnect(String(params.access_token));
|
|
2228
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
1992
2229
|
if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
|
|
1993
2230
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
1994
2231
|
}
|
|
@@ -1996,13 +2233,19 @@ export class AUNClient {
|
|
|
1996
2233
|
}
|
|
1997
2234
|
catch (err) {
|
|
1998
2235
|
// P1-19: 首连失败时重置状态,避免半连接残留
|
|
1999
|
-
this.
|
|
2236
|
+
if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
|
|
2237
|
+
&& (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
|
|
2238
|
+
&& !this._closing) {
|
|
2239
|
+
this._state = 'standby';
|
|
2240
|
+
}
|
|
2000
2241
|
try {
|
|
2001
2242
|
await this._transport.close();
|
|
2002
2243
|
}
|
|
2003
2244
|
catch { /* 忽略关闭错误 */ }
|
|
2004
2245
|
throw err;
|
|
2005
2246
|
}
|
|
2247
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2248
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2006
2249
|
if (!this._transport.isConnected()) {
|
|
2007
2250
|
throw new ConnectionError('websocket disconnected during authentication');
|
|
2008
2251
|
}
|
|
@@ -2017,14 +2260,30 @@ export class AUNClient {
|
|
|
2017
2260
|
else if (gatewayUrl) {
|
|
2018
2261
|
this._gatewayCandidates = [gatewayUrl];
|
|
2019
2262
|
}
|
|
2020
|
-
|
|
2263
|
+
const statePayload = {
|
|
2021
2264
|
state: this._publicState(this._state),
|
|
2022
2265
|
gateway: gatewayUrl,
|
|
2023
|
-
}
|
|
2266
|
+
};
|
|
2267
|
+
if (reconnectOwner) {
|
|
2268
|
+
if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
|
|
2269
|
+
throw new ConnectionError('reconnect attempt superseded');
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
else if (connectionOwner) {
|
|
2273
|
+
if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
|
|
2274
|
+
throw new StateError('connection attempt superseded');
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
await this._dispatcher.publish('state_change', statePayload);
|
|
2279
|
+
}
|
|
2280
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2281
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2024
2282
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
2025
2283
|
this._refreshSeqTrackerContext();
|
|
2026
2284
|
await this._restoreSeqTrackerState();
|
|
2027
2285
|
}
|
|
2286
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2028
2287
|
this._startBackgroundTasks();
|
|
2029
2288
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2030
2289
|
const isShortConnection = connectionKind === 'short';
|
|
@@ -2033,6 +2292,8 @@ export class AUNClient {
|
|
|
2033
2292
|
&& (!isShortConnection || hasExplicitBackgroundSync);
|
|
2034
2293
|
if (!isShortConnection) {
|
|
2035
2294
|
await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
|
|
2295
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2296
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2036
2297
|
}
|
|
2037
2298
|
else {
|
|
2038
2299
|
this._clientLog.debug('V2 session init deferred for short connection');
|
|
@@ -2063,6 +2324,11 @@ export class AUNClient {
|
|
|
2063
2324
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
2064
2325
|
if (!target)
|
|
2065
2326
|
throw new StateError('gateway discovery requires a loaded AID');
|
|
2327
|
+
const discovery = this._discovery;
|
|
2328
|
+
const tokenStore = this._tokenStore;
|
|
2329
|
+
const discoveryIsCurrent = () => (this._discovery === discovery
|
|
2330
|
+
&& this._tokenStore === tokenStore
|
|
2331
|
+
&& !this._closing);
|
|
2066
2332
|
if (this._gatewayCandidates.length > 0)
|
|
2067
2333
|
return [...this._gatewayCandidates];
|
|
2068
2334
|
if (this._gatewayUrl) {
|
|
@@ -2070,14 +2336,18 @@ export class AUNClient {
|
|
|
2070
2336
|
return [...this._gatewayCandidates];
|
|
2071
2337
|
}
|
|
2072
2338
|
try {
|
|
2073
|
-
const getMetadata =
|
|
2339
|
+
const getMetadata = tokenStore.getMetadata;
|
|
2074
2340
|
const rawList = typeof getMetadata === 'function'
|
|
2075
|
-
? String(await getMetadata.call(
|
|
2341
|
+
? String(await getMetadata.call(tokenStore, target, 'gateway_urls') ?? '').trim()
|
|
2076
2342
|
: '';
|
|
2343
|
+
if (!discoveryIsCurrent())
|
|
2344
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2077
2345
|
const cachedList = rawList ? JSON.parse(rawList) : [];
|
|
2078
2346
|
const raw = typeof getMetadata === 'function'
|
|
2079
|
-
? String(await getMetadata.call(
|
|
2347
|
+
? String(await getMetadata.call(tokenStore, target, 'gateway_url') ?? '').trim()
|
|
2080
2348
|
: '';
|
|
2349
|
+
if (!discoveryIsCurrent())
|
|
2350
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2081
2351
|
const candidates = Array.isArray(cachedList)
|
|
2082
2352
|
? cachedList.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
2083
2353
|
: [];
|
|
@@ -2089,6 +2359,8 @@ export class AUNClient {
|
|
|
2089
2359
|
}
|
|
2090
2360
|
}
|
|
2091
2361
|
catch {
|
|
2362
|
+
if (!discoveryIsCurrent())
|
|
2363
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2092
2364
|
// 缓存读取失败不影响发现流程。
|
|
2093
2365
|
}
|
|
2094
2366
|
const dotIdx = target.indexOf('.');
|
|
@@ -2101,7 +2373,9 @@ export class AUNClient {
|
|
|
2101
2373
|
let lastError = null;
|
|
2102
2374
|
for (const url of candidates) {
|
|
2103
2375
|
try {
|
|
2104
|
-
const discovered = await
|
|
2376
|
+
const discovered = await discovery.discoverAll(url);
|
|
2377
|
+
if (!discoveryIsCurrent())
|
|
2378
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2105
2379
|
const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
|
|
2106
2380
|
if (gatewayUrls.length === 0)
|
|
2107
2381
|
throw new ValidationError('gateway discovery returned no candidates');
|
|
@@ -2109,10 +2383,14 @@ export class AUNClient {
|
|
|
2109
2383
|
this._gatewayCandidates = gatewayUrls;
|
|
2110
2384
|
this._gatewayUrl = gateway;
|
|
2111
2385
|
try {
|
|
2112
|
-
const setMetadata =
|
|
2386
|
+
const setMetadata = tokenStore.setMetadata;
|
|
2113
2387
|
if (typeof setMetadata === 'function') {
|
|
2114
|
-
await setMetadata.call(
|
|
2115
|
-
|
|
2388
|
+
await setMetadata.call(tokenStore, target, 'gateway_url', gateway);
|
|
2389
|
+
if (!discoveryIsCurrent())
|
|
2390
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2391
|
+
await setMetadata.call(tokenStore, target, 'gateway_urls', JSON.stringify(gatewayUrls));
|
|
2392
|
+
if (!discoveryIsCurrent())
|
|
2393
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2116
2394
|
}
|
|
2117
2395
|
}
|
|
2118
2396
|
catch {
|
|
@@ -2121,6 +2399,8 @@ export class AUNClient {
|
|
|
2121
2399
|
return [...gatewayUrls];
|
|
2122
2400
|
}
|
|
2123
2401
|
catch (err) {
|
|
2402
|
+
if (!discoveryIsCurrent())
|
|
2403
|
+
throw new StateError('gateway discovery superseded by identity change');
|
|
2124
2404
|
lastError = err;
|
|
2125
2405
|
}
|
|
2126
2406
|
}
|
|
@@ -2442,13 +2722,16 @@ export class AUNClient {
|
|
|
2442
2722
|
this._clientLog.debug(`publish gateway.disconnect failed: ${exc?.message ?? exc}`);
|
|
2443
2723
|
}
|
|
2444
2724
|
}
|
|
2445
|
-
async _handleTransportDisconnect(error, closeCode) {
|
|
2725
|
+
async _handleTransportDisconnect(error, closeCode, initialOptions) {
|
|
2446
2726
|
if (this._closing || this._state === 'closed')
|
|
2447
2727
|
return;
|
|
2448
2728
|
// 在首个 await 前占位,避免 close/error 与 token 刷新并发启动多条重连链。
|
|
2449
2729
|
if (this._reconnectActive)
|
|
2450
2730
|
return;
|
|
2451
2731
|
this._reconnectActive = true;
|
|
2732
|
+
// 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
|
|
2733
|
+
const reconnectAbort = new AbortController();
|
|
2734
|
+
this._reconnectAbort = reconnectAbort;
|
|
2452
2735
|
// 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
|
|
2453
2736
|
this._delivery.resetInlineAckState();
|
|
2454
2737
|
this._state = 'standby';
|
|
@@ -2464,8 +2747,18 @@ export class AUNClient {
|
|
|
2464
2747
|
state: this._publicState(this._state),
|
|
2465
2748
|
error,
|
|
2466
2749
|
});
|
|
2750
|
+
if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
|
|
2751
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2752
|
+
this._reconnectAbort = null;
|
|
2753
|
+
this._reconnectActive = false;
|
|
2754
|
+
}
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2467
2757
|
if (!this._sessionOptions.auto_reconnect) {
|
|
2468
|
-
this.
|
|
2758
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2759
|
+
this._reconnectAbort = null;
|
|
2760
|
+
this._reconnectActive = false;
|
|
2761
|
+
}
|
|
2469
2762
|
return;
|
|
2470
2763
|
}
|
|
2471
2764
|
// 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
|
|
@@ -2487,30 +2780,90 @@ export class AUNClient {
|
|
|
2487
2780
|
eventPayload.code = disconnectInfo.code;
|
|
2488
2781
|
}
|
|
2489
2782
|
await this._dispatcher.publish('state_change', eventPayload);
|
|
2490
|
-
this.
|
|
2783
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2784
|
+
this._reconnectAbort = null;
|
|
2785
|
+
this._reconnectActive = false;
|
|
2786
|
+
}
|
|
2491
2787
|
return;
|
|
2492
2788
|
}
|
|
2493
2789
|
// 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
|
|
2494
2790
|
const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
|
|
2495
|
-
if (this._closing) {
|
|
2496
|
-
this.
|
|
2791
|
+
if (this._closing || reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort) {
|
|
2792
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2793
|
+
this._reconnectAbort = null;
|
|
2794
|
+
this._reconnectActive = false;
|
|
2795
|
+
}
|
|
2497
2796
|
return;
|
|
2498
2797
|
}
|
|
2499
|
-
const
|
|
2500
|
-
this.
|
|
2501
|
-
this._safeAsync(
|
|
2798
|
+
const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
|
|
2799
|
+
this._reconnectTask = reconnectTask;
|
|
2800
|
+
this._safeAsync(reconnectTask);
|
|
2801
|
+
}
|
|
2802
|
+
_ownsReconnect(owner) {
|
|
2803
|
+
return Boolean(owner
|
|
2804
|
+
&& !owner.signal.aborted
|
|
2805
|
+
&& !this._closing
|
|
2806
|
+
&& this._reconnectAbort === owner);
|
|
2807
|
+
}
|
|
2808
|
+
_assertReconnectOwner(owner) {
|
|
2809
|
+
if (owner && !this._ownsReconnect(owner)) {
|
|
2810
|
+
throw new ConnectionError('reconnect attempt superseded');
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
async _publishReconnectEvent(owner, event, payload) {
|
|
2814
|
+
if (!this._ownsReconnect(owner))
|
|
2815
|
+
return false;
|
|
2816
|
+
this._reconnectEventDispatchDepth += 1;
|
|
2817
|
+
try {
|
|
2818
|
+
await this._dispatcher.publish(event, payload);
|
|
2819
|
+
}
|
|
2820
|
+
finally {
|
|
2821
|
+
this._reconnectEventDispatchDepth -= 1;
|
|
2822
|
+
}
|
|
2823
|
+
return this._ownsReconnect(owner);
|
|
2824
|
+
}
|
|
2825
|
+
async _cancelReconnectAndWait() {
|
|
2826
|
+
const owner = this._reconnectAbort;
|
|
2827
|
+
const task = this._reconnectTask;
|
|
2828
|
+
if (!owner && !task && !this._reconnectActive)
|
|
2829
|
+
return;
|
|
2830
|
+
owner?.abort();
|
|
2831
|
+
try {
|
|
2832
|
+
await this._transport.close();
|
|
2833
|
+
}
|
|
2834
|
+
catch (exc) {
|
|
2835
|
+
this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
|
|
2836
|
+
}
|
|
2837
|
+
// state_change handler 内等待其所属重连任务会自锁;该路径由事件返回后的
|
|
2838
|
+
// owner 复核负责收尾,且事件回调期间尚未进入下一次 transport 操作。
|
|
2839
|
+
if (task && this._reconnectEventDispatchDepth === 0) {
|
|
2840
|
+
try {
|
|
2841
|
+
await task;
|
|
2842
|
+
}
|
|
2843
|
+
catch (exc) {
|
|
2844
|
+
this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (this._reconnectAbort === owner) {
|
|
2848
|
+
this._reconnectAbort = null;
|
|
2849
|
+
this._reconnectActive = false;
|
|
2850
|
+
this._nextRetryAt = null;
|
|
2851
|
+
}
|
|
2852
|
+
if (this._reconnectTask === task)
|
|
2853
|
+
this._reconnectTask = null;
|
|
2502
2854
|
}
|
|
2503
2855
|
/** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
|
|
2504
|
-
async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort) {
|
|
2856
|
+
async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort, initialOptions) {
|
|
2505
2857
|
let releasedOwnGeneration = false;
|
|
2506
2858
|
try {
|
|
2507
|
-
await this._runReconnectLoop(serverInitiated, reconnectAbort);
|
|
2859
|
+
await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
|
|
2508
2860
|
}
|
|
2509
2861
|
finally {
|
|
2510
2862
|
if (this._reconnectAbort === reconnectAbort) {
|
|
2511
2863
|
this._nextRetryAt = null;
|
|
2512
2864
|
this._reconnectActive = false;
|
|
2513
2865
|
this._reconnectAbort = null;
|
|
2866
|
+
this._reconnectTask = null;
|
|
2514
2867
|
releasedOwnGeneration = true;
|
|
2515
2868
|
}
|
|
2516
2869
|
}
|
|
@@ -2519,7 +2872,7 @@ export class AUNClient {
|
|
|
2519
2872
|
}
|
|
2520
2873
|
}
|
|
2521
2874
|
/** 指数退避 + 当前基数抖动重连循环主体。 */
|
|
2522
|
-
async _runReconnectLoop(serverInitiated, reconnectAbort) {
|
|
2875
|
+
async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
|
|
2523
2876
|
const retry = { ...this._sessionOptions.retry };
|
|
2524
2877
|
const maxBaseDelay = clampReconnectDelaySeconds(retry.max_delay, RECONNECT_MAX_BASE_DELAY_SECONDS);
|
|
2525
2878
|
// M25: max_attempts=0 表示无限重试(与 Go/Python 对齐)
|
|
@@ -2534,11 +2887,12 @@ export class AUNClient {
|
|
|
2534
2887
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
2535
2888
|
this._state = 'terminal_failed';
|
|
2536
2889
|
this._nextRetryAt = null;
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2890
|
+
if (reconnectAbort)
|
|
2891
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2892
|
+
state: this._publicState(this._state),
|
|
2893
|
+
attempt: attempt - 1,
|
|
2894
|
+
reason: 'max_attempts_exhausted',
|
|
2895
|
+
});
|
|
2542
2896
|
return;
|
|
2543
2897
|
}
|
|
2544
2898
|
// 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
|
|
@@ -2546,22 +2900,26 @@ export class AUNClient {
|
|
|
2546
2900
|
const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
|
|
2547
2901
|
this._nextRetryAt = new Date(Date.now() + sleepMs);
|
|
2548
2902
|
this._state = 'retry_backoff';
|
|
2549
|
-
await this.
|
|
2903
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2550
2904
|
state: this._publicState(this._state),
|
|
2551
2905
|
attempt,
|
|
2552
2906
|
next_retry_at: this._nextRetryAt.getTime() / 1000,
|
|
2553
|
-
})
|
|
2907
|
+
}))
|
|
2908
|
+
return;
|
|
2554
2909
|
try {
|
|
2555
2910
|
await this._sleep(sleepMs, reconnectAbort?.signal);
|
|
2556
2911
|
this._nextRetryAt = null;
|
|
2557
|
-
if (reconnectAbort
|
|
2912
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2558
2913
|
return;
|
|
2559
2914
|
// 退避结束,进入 reconnecting 状态
|
|
2560
2915
|
this._state = 'reconnecting';
|
|
2561
|
-
await this.
|
|
2916
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2562
2917
|
state: this._publicState(this._state),
|
|
2563
2918
|
attempt,
|
|
2564
|
-
})
|
|
2919
|
+
}))
|
|
2920
|
+
return;
|
|
2921
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2922
|
+
return;
|
|
2565
2923
|
// /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
|
|
2566
2924
|
if (this._gatewayUrl) {
|
|
2567
2925
|
void this._discovery.checkHealth(this._gatewayUrl, 5000)
|
|
@@ -2574,6 +2932,20 @@ export class AUNClient {
|
|
|
2574
2932
|
});
|
|
2575
2933
|
}
|
|
2576
2934
|
await this._transport.close();
|
|
2935
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2936
|
+
return;
|
|
2937
|
+
if (initialOptions) {
|
|
2938
|
+
// 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
|
|
2939
|
+
await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
|
|
2940
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2941
|
+
return;
|
|
2942
|
+
if (!this._transport.isConnected()) {
|
|
2943
|
+
throw new ConnectionError('websocket disconnected while initial reconnect completed');
|
|
2944
|
+
}
|
|
2945
|
+
this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
|
|
2946
|
+
this._nextRetryAt = null;
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2577
2949
|
if (!this._sessionParams) {
|
|
2578
2950
|
throw new StateError('missing connect params for reconnect');
|
|
2579
2951
|
}
|
|
@@ -2601,7 +2973,9 @@ export class AUNClient {
|
|
|
2601
2973
|
for (const gateway of candidates) {
|
|
2602
2974
|
const attemptParams = { ...this._sessionParams, gateway };
|
|
2603
2975
|
try {
|
|
2604
|
-
await this._connectOnce(attemptParams, true);
|
|
2976
|
+
await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
|
|
2977
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2978
|
+
return;
|
|
2605
2979
|
if (!this._transport.isConnected()) {
|
|
2606
2980
|
throw new ConnectionError('websocket disconnected while reconnecting');
|
|
2607
2981
|
}
|
|
@@ -2627,20 +3001,24 @@ export class AUNClient {
|
|
|
2627
3001
|
return;
|
|
2628
3002
|
}
|
|
2629
3003
|
catch (exc) {
|
|
3004
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
3005
|
+
return;
|
|
2630
3006
|
this._lastError = exc instanceof Error ? exc : new Error(String(exc));
|
|
2631
3007
|
this._lastErrorCode = 'reconnect_failed';
|
|
2632
|
-
await this.
|
|
3008
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
|
|
2633
3009
|
error: formatCaughtError(exc),
|
|
2634
3010
|
attempt,
|
|
2635
|
-
})
|
|
3011
|
+
}))
|
|
3012
|
+
return;
|
|
2636
3013
|
if (!this._shouldRetryReconnect(exc)) {
|
|
2637
3014
|
this._state = 'terminal_failed';
|
|
2638
3015
|
this._nextRetryAt = null;
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3016
|
+
if (reconnectAbort)
|
|
3017
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
3018
|
+
state: this._publicState(this._state),
|
|
3019
|
+
error: formatCaughtError(exc),
|
|
3020
|
+
attempt,
|
|
3021
|
+
});
|
|
2644
3022
|
return;
|
|
2645
3023
|
}
|
|
2646
3024
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
@@ -2663,7 +3041,7 @@ export class AUNClient {
|
|
|
2663
3041
|
|| error instanceof ValidationError || error instanceof StateError) {
|
|
2664
3042
|
return false;
|
|
2665
3043
|
}
|
|
2666
|
-
if (error instanceof ConnectionError)
|
|
3044
|
+
if (error instanceof ConnectionError || error instanceof TimeoutError)
|
|
2667
3045
|
return true;
|
|
2668
3046
|
if (error instanceof AUNError)
|
|
2669
3047
|
return error.retryable;
|
|
@@ -2683,6 +3061,7 @@ export class AUNClient {
|
|
|
2683
3061
|
_resetSeqTrackingState() {
|
|
2684
3062
|
void this._saveSeqTrackerState();
|
|
2685
3063
|
this._resetV2IdentityRuntime();
|
|
3064
|
+
this._v2E2EE.resetForwardCursorState();
|
|
2686
3065
|
this._seqTracker = new SeqTracker();
|
|
2687
3066
|
this._seqTrackerContext = null;
|
|
2688
3067
|
this._gapFillDone.clear();
|
|
@@ -2692,18 +3071,13 @@ export class AUNClient {
|
|
|
2692
3071
|
this._v2SenderIKPending.clear();
|
|
2693
3072
|
this._v2SenderIKFetching.clear();
|
|
2694
3073
|
this._groupSynced.clear();
|
|
2695
|
-
this._onlineUnreadHintQueue.clear();
|
|
2696
|
-
if (this._onlineUnreadHintTimer) {
|
|
2697
|
-
clearTimeout(this._onlineUnreadHintTimer);
|
|
2698
|
-
this._onlineUnreadHintTimer = null;
|
|
2699
|
-
}
|
|
2700
|
-
this._onlineUnreadHintDrainActive = false;
|
|
2701
3074
|
}
|
|
2702
3075
|
_refreshSeqTrackerContext() {
|
|
2703
3076
|
const nextContext = this._currentSeqTrackerContext();
|
|
2704
3077
|
if (nextContext === this._seqTrackerContext)
|
|
2705
3078
|
return;
|
|
2706
3079
|
void this._saveSeqTrackerState();
|
|
3080
|
+
this._v2E2EE.resetForwardCursorState();
|
|
2707
3081
|
this._seqTracker = new SeqTracker();
|
|
2708
3082
|
this._delivery.resetInlineAckState();
|
|
2709
3083
|
this._gapFillDone.clear();
|
|
@@ -2713,12 +3087,6 @@ export class AUNClient {
|
|
|
2713
3087
|
this._v2SenderIKPending.clear();
|
|
2714
3088
|
this._v2SenderIKFetching.clear();
|
|
2715
3089
|
this._groupSynced.clear();
|
|
2716
|
-
this._onlineUnreadHintQueue.clear();
|
|
2717
|
-
if (this._onlineUnreadHintTimer) {
|
|
2718
|
-
clearTimeout(this._onlineUnreadHintTimer);
|
|
2719
|
-
this._onlineUnreadHintTimer = null;
|
|
2720
|
-
}
|
|
2721
|
-
this._onlineUnreadHintDrainActive = false;
|
|
2722
3090
|
this._seqTrackerContext = nextContext;
|
|
2723
3091
|
}
|
|
2724
3092
|
/** 将 SeqTracker 状态保存到 keystore */
|
|
@@ -2941,7 +3309,7 @@ export class AUNClient {
|
|
|
2941
3309
|
* undecryptable 事件、成功消费后触发 SPK 轮换。inline Push 在游标提交和
|
|
2942
3310
|
* Head/正文绑定校验前调用本入口时必须显式关闭这些副作用。
|
|
2943
3311
|
*/
|
|
2944
|
-
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
|
|
3312
|
+
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
|
|
2945
3313
|
const session = this._v2Session;
|
|
2946
3314
|
if (!session)
|
|
2947
3315
|
return null;
|
|
@@ -3008,6 +3376,24 @@ export class AUNClient {
|
|
|
3008
3376
|
}
|
|
3009
3377
|
catch (exc) {
|
|
3010
3378
|
this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${String(exc)}`);
|
|
3379
|
+
if (allowPending || deferStatus) {
|
|
3380
|
+
const fromAid = String(msg.from_aid ?? '');
|
|
3381
|
+
const senderDeviceId = String(aad.from_device ?? '');
|
|
3382
|
+
if (deferStatus) {
|
|
3383
|
+
deferStatus.deferred = true;
|
|
3384
|
+
deferStatus.fromAid = fromAid;
|
|
3385
|
+
deferStatus.senderDeviceId = senderDeviceId;
|
|
3386
|
+
deferStatus.groupId = groupIdForKeys;
|
|
3387
|
+
}
|
|
3388
|
+
this._scheduleV2SenderIKPending({
|
|
3389
|
+
msg,
|
|
3390
|
+
fromAid,
|
|
3391
|
+
senderDeviceId,
|
|
3392
|
+
groupId: groupIdForKeys,
|
|
3393
|
+
startFetch: !deferKeyFetch,
|
|
3394
|
+
});
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3011
3397
|
if (emitUndecryptable) {
|
|
3012
3398
|
try {
|
|
3013
3399
|
const event = {
|
|
@@ -3035,13 +3421,19 @@ export class AUNClient {
|
|
|
3035
3421
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
3036
3422
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
3037
3423
|
let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
3038
|
-
if (!senderPubDer) {
|
|
3424
|
+
if (!senderPubDer && !deferKeyFetch) {
|
|
3039
3425
|
this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
3040
3426
|
senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
3041
3427
|
}
|
|
3042
3428
|
if (!senderPubDer) {
|
|
3043
|
-
if (allowPending) {
|
|
3044
|
-
|
|
3429
|
+
if (allowPending || deferStatus) {
|
|
3430
|
+
if (deferStatus) {
|
|
3431
|
+
deferStatus.deferred = true;
|
|
3432
|
+
deferStatus.fromAid = fromAid;
|
|
3433
|
+
deferStatus.senderDeviceId = senderDeviceId;
|
|
3434
|
+
deferStatus.groupId = groupIdForKeys;
|
|
3435
|
+
}
|
|
3436
|
+
this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
|
|
3045
3437
|
return null;
|
|
3046
3438
|
}
|
|
3047
3439
|
if (emitUndecryptable) {
|
|
@@ -3069,9 +3461,13 @@ export class AUNClient {
|
|
|
3069
3461
|
let plaintext;
|
|
3070
3462
|
try {
|
|
3071
3463
|
plaintext = await decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
|
|
3464
|
+
if (plaintext === null)
|
|
3465
|
+
throw new Error('recipient_not_found_or_envelope_verification_failed');
|
|
3072
3466
|
}
|
|
3073
3467
|
catch (exc) {
|
|
3074
3468
|
this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${String(exc)}`);
|
|
3469
|
+
if (deferStatus)
|
|
3470
|
+
deferStatus.terminal = true;
|
|
3075
3471
|
if (emitUndecryptable) {
|
|
3076
3472
|
try {
|
|
3077
3473
|
const event = {
|
|
@@ -3321,18 +3717,6 @@ export class AUNClient {
|
|
|
3321
3717
|
this._clientLog.warn(`background task exception:${String(exc)}`);
|
|
3322
3718
|
});
|
|
3323
3719
|
}
|
|
3324
|
-
_explicitGroupCursorParams(params) {
|
|
3325
|
-
const value = params._group_cursor_params;
|
|
3326
|
-
if (!isJsonObject(value))
|
|
3327
|
-
return {};
|
|
3328
|
-
return { ...value };
|
|
3329
|
-
}
|
|
3330
|
-
_groupCursorTargetsCurrentInstance(params) {
|
|
3331
|
-
const deviceId = String(params.device_id ?? '').trim();
|
|
3332
|
-
const slotId = String(params.slot_id ?? '').trim();
|
|
3333
|
-
return (!deviceId || deviceId === (this._deviceId ?? ''))
|
|
3334
|
-
&& (!slotId || slotId === (this._slotId ?? ''));
|
|
3335
|
-
}
|
|
3336
3720
|
// ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
|
|
3337
3721
|
async _withBackgroundRpc(operation) {
|
|
3338
3722
|
this._backgroundRpcDepth += 1;
|