@agentunion/fastaun-browser 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/_packed_docs/CHANGELOG.md +74 -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/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/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
- package/dist/agent-md-schema.d.ts +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +229 -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 +26 -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 +29895 -21968
- 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 +33 -5
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +597 -220
- package/dist/client/delivery.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 +452 -151
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +17 -6
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +284 -115
- 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 +398 -114
- 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 +396 -104
- 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 +51 -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?.();
|
|
@@ -1173,14 +1189,102 @@ export class AUNClient {
|
|
|
1173
1189
|
return;
|
|
1174
1190
|
await this._handleTransportDisconnect(err);
|
|
1175
1191
|
}
|
|
1176
|
-
async
|
|
1192
|
+
async _recoverIdentityAdmission(err, _method = '', params = {}) {
|
|
1193
|
+
if (!(err instanceof AUNError) || err.code !== -32003)
|
|
1194
|
+
return false;
|
|
1195
|
+
const message = String(err.message ?? '').trim();
|
|
1196
|
+
const groupCertPrefix = 'valid group identity certificate is required for ';
|
|
1197
|
+
if (message.toLowerCase().startsWith(groupCertPrefix)) {
|
|
1198
|
+
const fallback = message.slice(groupCertPrefix.length).trim();
|
|
1199
|
+
const store = this._aidStore;
|
|
1200
|
+
const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
|
|
1201
|
+
if (!groupId || !store)
|
|
1202
|
+
return false;
|
|
1203
|
+
try {
|
|
1204
|
+
await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
|
|
1205
|
+
}
|
|
1206
|
+
catch {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
const groupMDPrefix = 'valid group agent.md is required for ';
|
|
1212
|
+
if (message.toLowerCase().startsWith(groupMDPrefix)) {
|
|
1213
|
+
const groupAid = message.slice(groupMDPrefix.length).trim();
|
|
1214
|
+
const store = this._aidStore;
|
|
1215
|
+
if (!groupAid || !store)
|
|
1216
|
+
return false;
|
|
1217
|
+
const loaded = await store.load(groupAid);
|
|
1218
|
+
if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
|
|
1219
|
+
return false;
|
|
1220
|
+
const ownerAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1221
|
+
if (!ownerAid)
|
|
1222
|
+
return false;
|
|
1223
|
+
await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}, ownerAid));
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
const agentPrefix = 'valid agent.md is required for ';
|
|
1227
|
+
if (!message.toLowerCase().startsWith(agentPrefix))
|
|
1228
|
+
return false;
|
|
1229
|
+
const target = message.slice(agentPrefix.length).trim();
|
|
1230
|
+
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1231
|
+
return false;
|
|
1232
|
+
await this._agentMdManager.upload(buildDefaultAgentMd(target));
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
async _runGroupIdentityOperation(groupId, operation) {
|
|
1236
|
+
const aid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1237
|
+
const dot = aid.indexOf('.');
|
|
1238
|
+
const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
|
|
1239
|
+
if (!key)
|
|
1240
|
+
return operation();
|
|
1241
|
+
let task = this._groupIdentityOperations.get(key);
|
|
1242
|
+
if (!task) {
|
|
1243
|
+
task = Promise.resolve().then(operation);
|
|
1244
|
+
this._groupIdentityOperations.set(key, task);
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
return await task;
|
|
1248
|
+
}
|
|
1249
|
+
finally {
|
|
1250
|
+
if (this._groupIdentityOperations.get(key) === task)
|
|
1251
|
+
this._groupIdentityOperations.delete(key);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async createGroup(params = {}, options = {}) {
|
|
1177
1255
|
if (!isJsonObject(params)) {
|
|
1178
1256
|
throw new ValidationError('createGroup params must be an object');
|
|
1179
1257
|
}
|
|
1258
|
+
const store = options.aidStore ?? this._aidStore;
|
|
1259
|
+
if (!store) {
|
|
1260
|
+
throw new ValidationError('createGroup requires aidStore');
|
|
1261
|
+
}
|
|
1262
|
+
this._aidStore = store;
|
|
1180
1263
|
const payload = { ...params };
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1264
|
+
const isNamed = Boolean(String(payload.group_name ?? '').trim());
|
|
1265
|
+
payload._defer_group_ready_postprocess = true;
|
|
1266
|
+
if (!isNamed) {
|
|
1267
|
+
delete payload.public_key;
|
|
1268
|
+
delete payload.curve;
|
|
1269
|
+
const created = await this.call('group.create', payload);
|
|
1270
|
+
const createdMap = isJsonObject(created) ? created : {};
|
|
1271
|
+
const createdGroup = isJsonObject(createdMap.group) ? createdMap.group : {};
|
|
1272
|
+
const groupId = String(createdGroup.group_id ?? createdGroup.group_aid ?? createdMap.group_id ?? createdMap.group_aid ?? '').trim();
|
|
1273
|
+
if (!groupId) {
|
|
1274
|
+
throw new ValidationError('group.create response missing group.group_id');
|
|
1275
|
+
}
|
|
1276
|
+
const bindParams = { group_id: groupId };
|
|
1277
|
+
for (const key of ['name', 'description', 'group_description', 'group_agent_md', 'groupAgentMd', 'content']) {
|
|
1278
|
+
if (key in payload)
|
|
1279
|
+
bindParams[key] = payload[key];
|
|
1280
|
+
}
|
|
1281
|
+
const bound = await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid(bindParams, { aidStore: store }));
|
|
1282
|
+
const boundMap = isJsonObject(bound) ? bound : {};
|
|
1283
|
+
const boundGroup = isJsonObject(boundMap.group) ? boundMap.group : {};
|
|
1284
|
+
const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
|
|
1285
|
+
const postprocessParams = { ...payload };
|
|
1286
|
+
delete postprocessParams._defer_group_ready_postprocess;
|
|
1287
|
+
return await this._groupState.postprocessResult('group.create', postprocessParams, result);
|
|
1184
1288
|
}
|
|
1185
1289
|
const keyPair = await new CryptoProvider().generateIdentity();
|
|
1186
1290
|
payload.public_key = keyPair.public_key_der_b64;
|
|
@@ -1192,30 +1296,63 @@ export class AUNClient {
|
|
|
1192
1296
|
const groupAid = String(group.group_aid ?? resultMap.group_aid ?? '').trim();
|
|
1193
1297
|
const certPem = String(aidCert.cert ?? aidCert.cert_pem ?? resultMap.cert ?? resultMap.cert_pem ?? '').trim();
|
|
1194
1298
|
if (!groupAid || !certPem) {
|
|
1195
|
-
throw new ValidationError('group.create
|
|
1299
|
+
throw new ValidationError('group.create response missing group.group_aid or aid_cert.cert');
|
|
1196
1300
|
}
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
verifySsl: this.configModel.verifySsl,
|
|
1301
|
+
const imported = await store.importGroupIdentity(groupAid, {
|
|
1302
|
+
private_key_pem: keyPair.private_key_pem,
|
|
1303
|
+
public_key_der_b64: keyPair.public_key_der_b64,
|
|
1304
|
+
curve: keyPair.curve,
|
|
1305
|
+
cert_pem: certPem,
|
|
1203
1306
|
});
|
|
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
|
-
}
|
|
1307
|
+
if (!imported.ok) {
|
|
1308
|
+
throw new ValidationError(imported.error.message);
|
|
1214
1309
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1310
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1311
|
+
if (!uploaderAid) {
|
|
1312
|
+
throw new ValidationError('createGroup requires current owner AID for group agent.md upload');
|
|
1313
|
+
}
|
|
1314
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
|
|
1315
|
+
const postprocessParams = { ...payload };
|
|
1316
|
+
delete postprocessParams._defer_group_ready_postprocess;
|
|
1317
|
+
return await this._groupState.postprocessResult('group.create', postprocessParams, result);
|
|
1318
|
+
}
|
|
1319
|
+
_groupAgentMdContent(groupAid, params = {}, group = {}) {
|
|
1320
|
+
const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
|
|
1321
|
+
if (explicit !== undefined && explicit !== null)
|
|
1322
|
+
return String(explicit);
|
|
1323
|
+
const value = (...keys) => {
|
|
1324
|
+
for (const source of [group, params]) {
|
|
1325
|
+
for (const key of keys) {
|
|
1326
|
+
const candidate = String(source[key] ?? '').trim();
|
|
1327
|
+
if (candidate)
|
|
1328
|
+
return candidate;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return '';
|
|
1332
|
+
};
|
|
1333
|
+
const name = value('name', 'group_name', 'title') || groupAid;
|
|
1334
|
+
const normalizedDescription = (value('description', 'group_description') || `AUN group ${name}`)
|
|
1335
|
+
.split(/\s+/).filter(Boolean).join(' ');
|
|
1336
|
+
const description = Array.from(normalizedDescription).slice(0, 100).join('');
|
|
1337
|
+
return [
|
|
1338
|
+
'---\n',
|
|
1339
|
+
`aid: ${JSON.stringify(groupAid)}\n`,
|
|
1340
|
+
`name: ${JSON.stringify(name)}\n`,
|
|
1341
|
+
'type: "group"\n',
|
|
1342
|
+
'version: "1.0.0"\n',
|
|
1343
|
+
`description: ${JSON.stringify(description)}\n`,
|
|
1344
|
+
'---\n\n',
|
|
1345
|
+
`# ${name}\n`,
|
|
1346
|
+
].join('');
|
|
1347
|
+
}
|
|
1348
|
+
async _uploadGroupAgentMd(store, groupAid, params, group, uploaderAid) {
|
|
1349
|
+
if (typeof store.uploadAgentMd !== 'function') {
|
|
1350
|
+
throw new ValidationError('group agent.md upload is unavailable');
|
|
1351
|
+
}
|
|
1352
|
+
const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
|
|
1353
|
+
if (!uploaded.ok) {
|
|
1354
|
+
throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
|
|
1217
1355
|
}
|
|
1218
|
-
return result;
|
|
1219
1356
|
}
|
|
1220
1357
|
async bindGroupAid(params = {}, options = {}) {
|
|
1221
1358
|
if (!isJsonObject(params)) {
|
|
@@ -1225,6 +1362,7 @@ export class AUNClient {
|
|
|
1225
1362
|
if (!store) {
|
|
1226
1363
|
throw new ValidationError('bindGroupAid requires aidStore');
|
|
1227
1364
|
}
|
|
1365
|
+
this._aidStore = store;
|
|
1228
1366
|
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1229
1367
|
const keystore = store._keystore;
|
|
1230
1368
|
let keyPair = null;
|
|
@@ -1272,6 +1410,11 @@ export class AUNClient {
|
|
|
1272
1410
|
if (!imported.ok) {
|
|
1273
1411
|
throw new ValidationError(imported.error.message);
|
|
1274
1412
|
}
|
|
1413
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1414
|
+
if (!uploaderAid) {
|
|
1415
|
+
throw new ValidationError('bindGroupAid requires current owner AID for group agent.md upload');
|
|
1416
|
+
}
|
|
1417
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group, uploaderAid);
|
|
1275
1418
|
// 落盘成功,清除 pending 槽位
|
|
1276
1419
|
if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
1277
1420
|
await keystore.clearPendingGroupBind(groupId);
|
|
@@ -1293,7 +1436,7 @@ export class AUNClient {
|
|
|
1293
1436
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1294
1437
|
if (!groupAid) {
|
|
1295
1438
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1296
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1439
|
+
groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
|
|
1297
1440
|
}
|
|
1298
1441
|
if (!groupAid) {
|
|
1299
1442
|
throw new ValidationError('renewGroupAid: unable to determine group_aid');
|
|
@@ -1361,6 +1504,11 @@ export class AUNClient {
|
|
|
1361
1504
|
if (!imported.ok) {
|
|
1362
1505
|
throw new ValidationError(`renewGroupAid failed to persist group identity: ${imported.error.message}`);
|
|
1363
1506
|
}
|
|
1507
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1508
|
+
if (!uploaderAid) {
|
|
1509
|
+
throw new ValidationError('renewGroupAid requires current owner AID for group agent.md upload');
|
|
1510
|
+
}
|
|
1511
|
+
await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
|
|
1364
1512
|
return result;
|
|
1365
1513
|
}
|
|
1366
1514
|
async startGroupTransfer(params = {}, options = {}) {
|
|
@@ -1379,7 +1527,7 @@ export class AUNClient {
|
|
|
1379
1527
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1380
1528
|
if (!groupAid) {
|
|
1381
1529
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1382
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1530
|
+
groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
|
|
1383
1531
|
}
|
|
1384
1532
|
if (!groupAid) {
|
|
1385
1533
|
throw new ValidationError('startGroupTransfer: unable to determine group_aid');
|
|
@@ -1427,7 +1575,7 @@ export class AUNClient {
|
|
|
1427
1575
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
1428
1576
|
if (!groupAid) {
|
|
1429
1577
|
const info = await this.call('group.get_info', { group_id: groupId, required: ['member'] });
|
|
1430
|
-
groupAid = String(info?.group_aid ?? '').trim();
|
|
1578
|
+
groupAid = String(info?.group_aid ?? info?.group?.group_aid ?? '').trim();
|
|
1431
1579
|
}
|
|
1432
1580
|
if (!groupAid) {
|
|
1433
1581
|
throw new ValidationError('completeGroupTransfer: unable to determine group_aid');
|
|
@@ -1475,6 +1623,11 @@ export class AUNClient {
|
|
|
1475
1623
|
if (!imported.ok) {
|
|
1476
1624
|
throw new ValidationError(imported.error.message);
|
|
1477
1625
|
}
|
|
1626
|
+
const uploaderAid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1627
|
+
if (!uploaderAid) {
|
|
1628
|
+
throw new ValidationError('completeGroupTransfer requires current owner AID for group agent.md upload');
|
|
1629
|
+
}
|
|
1630
|
+
await this._uploadGroupAgentMd(store, returnedGroupAid, payload, group, uploaderAid);
|
|
1478
1631
|
return result;
|
|
1479
1632
|
}
|
|
1480
1633
|
static _notifyParamsSizeOk(params) {
|
|
@@ -1692,14 +1845,14 @@ export class AUNClient {
|
|
|
1692
1845
|
const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
1693
1846
|
msg.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
1694
1847
|
}
|
|
1695
|
-
async _drainOrderedMessages(ns, beforeSeq) {
|
|
1696
|
-
await this._delivery.drainOrderedMessages(ns, beforeSeq);
|
|
1848
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
1849
|
+
await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
|
|
1697
1850
|
}
|
|
1698
1851
|
async _publishOrderedMessage(event, ns, seq, payload) {
|
|
1699
1852
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload);
|
|
1700
1853
|
}
|
|
1701
|
-
async _publishPulledMessage(event, ns, seq, payload) {
|
|
1702
|
-
return this._delivery.publishPulledMessage(event, ns, seq, payload);
|
|
1854
|
+
async _publishPulledMessage(event, ns, seq, payload, persist = true) {
|
|
1855
|
+
return this._delivery.publishPulledMessage(event, ns, seq, payload, persist);
|
|
1703
1856
|
}
|
|
1704
1857
|
_extractGroupIdFromResult(result) {
|
|
1705
1858
|
const group = isJsonObject(result.group) ? result.group : null;
|
|
@@ -1937,8 +2090,10 @@ export class AUNClient {
|
|
|
1937
2090
|
await this._rpcPipeline.signClientOperation(method, params);
|
|
1938
2091
|
}
|
|
1939
2092
|
// ── 内部:连接 ────────────────────────────────────
|
|
1940
|
-
async _connectOnce(params, allowReauth) {
|
|
2093
|
+
async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
|
|
1941
2094
|
const tStart = Date.now();
|
|
2095
|
+
connectionOwner ??= this._connectAbort ?? undefined;
|
|
2096
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1942
2097
|
this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
|
|
1943
2098
|
try {
|
|
1944
2099
|
const gatewayUrl = this._resolveGateway(params);
|
|
@@ -1951,8 +2106,12 @@ export class AUNClient {
|
|
|
1951
2106
|
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
1952
2107
|
this._refreshSeqTrackerContext();
|
|
1953
2108
|
await this._restoreSeqTrackerState();
|
|
2109
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2110
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1954
2111
|
try {
|
|
1955
2112
|
const challenge = await this._transport.connect(gatewayUrl);
|
|
2113
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2114
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1956
2115
|
this._state = 'authenticating';
|
|
1957
2116
|
if (allowReauth) {
|
|
1958
2117
|
const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
|
|
@@ -1964,6 +2123,8 @@ export class AUNClient {
|
|
|
1964
2123
|
shortTtlMs: Number(params.short_ttl_ms ?? 0),
|
|
1965
2124
|
extraInfo: params.extra_info,
|
|
1966
2125
|
});
|
|
2126
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2127
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1967
2128
|
if (isJsonObject(authContext)) {
|
|
1968
2129
|
const auth = authContext;
|
|
1969
2130
|
const identity = auth.identity;
|
|
@@ -1988,7 +2149,10 @@ export class AUNClient {
|
|
|
1988
2149
|
shortTtlMs: Number(params.short_ttl_ms ?? 0),
|
|
1989
2150
|
extraInfo: params.extra_info,
|
|
1990
2151
|
});
|
|
2152
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2153
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
1991
2154
|
await this._syncIdentityAfterConnect(String(params.access_token));
|
|
2155
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
1992
2156
|
if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
|
|
1993
2157
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
1994
2158
|
}
|
|
@@ -1996,13 +2160,19 @@ export class AUNClient {
|
|
|
1996
2160
|
}
|
|
1997
2161
|
catch (err) {
|
|
1998
2162
|
// P1-19: 首连失败时重置状态,避免半连接残留
|
|
1999
|
-
this.
|
|
2163
|
+
if ((!reconnectOwner || this._ownsReconnect(reconnectOwner))
|
|
2164
|
+
&& (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner))
|
|
2165
|
+
&& !this._closing) {
|
|
2166
|
+
this._state = 'standby';
|
|
2167
|
+
}
|
|
2000
2168
|
try {
|
|
2001
2169
|
await this._transport.close();
|
|
2002
2170
|
}
|
|
2003
2171
|
catch { /* 忽略关闭错误 */ }
|
|
2004
2172
|
throw err;
|
|
2005
2173
|
}
|
|
2174
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2175
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2006
2176
|
if (!this._transport.isConnected()) {
|
|
2007
2177
|
throw new ConnectionError('websocket disconnected during authentication');
|
|
2008
2178
|
}
|
|
@@ -2017,14 +2187,30 @@ export class AUNClient {
|
|
|
2017
2187
|
else if (gatewayUrl) {
|
|
2018
2188
|
this._gatewayCandidates = [gatewayUrl];
|
|
2019
2189
|
}
|
|
2020
|
-
|
|
2190
|
+
const statePayload = {
|
|
2021
2191
|
state: this._publicState(this._state),
|
|
2022
2192
|
gateway: gatewayUrl,
|
|
2023
|
-
}
|
|
2193
|
+
};
|
|
2194
|
+
if (reconnectOwner) {
|
|
2195
|
+
if (!await this._publishReconnectEvent(reconnectOwner, 'state_change', statePayload)) {
|
|
2196
|
+
throw new ConnectionError('reconnect attempt superseded');
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
else if (connectionOwner) {
|
|
2200
|
+
if (!await this._lifecycle.publishConnectionEvent(connectionOwner, 'state_change', statePayload)) {
|
|
2201
|
+
throw new StateError('connection attempt superseded');
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
else {
|
|
2205
|
+
await this._dispatcher.publish('state_change', statePayload);
|
|
2206
|
+
}
|
|
2207
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2208
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2024
2209
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
2025
2210
|
this._refreshSeqTrackerContext();
|
|
2026
2211
|
await this._restoreSeqTrackerState();
|
|
2027
2212
|
}
|
|
2213
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2028
2214
|
this._startBackgroundTasks();
|
|
2029
2215
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2030
2216
|
const isShortConnection = connectionKind === 'short';
|
|
@@ -2033,6 +2219,8 @@ export class AUNClient {
|
|
|
2033
2219
|
&& (!isShortConnection || hasExplicitBackgroundSync);
|
|
2034
2220
|
if (!isShortConnection) {
|
|
2035
2221
|
await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
|
|
2222
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
2223
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2036
2224
|
}
|
|
2037
2225
|
else {
|
|
2038
2226
|
this._clientLog.debug('V2 session init deferred for short connection');
|
|
@@ -2442,13 +2630,16 @@ export class AUNClient {
|
|
|
2442
2630
|
this._clientLog.debug(`publish gateway.disconnect failed: ${exc?.message ?? exc}`);
|
|
2443
2631
|
}
|
|
2444
2632
|
}
|
|
2445
|
-
async _handleTransportDisconnect(error, closeCode) {
|
|
2633
|
+
async _handleTransportDisconnect(error, closeCode, initialOptions) {
|
|
2446
2634
|
if (this._closing || this._state === 'closed')
|
|
2447
2635
|
return;
|
|
2448
2636
|
// 在首个 await 前占位,避免 close/error 与 token 刷新并发启动多条重连链。
|
|
2449
2637
|
if (this._reconnectActive)
|
|
2450
2638
|
return;
|
|
2451
2639
|
this._reconnectActive = true;
|
|
2640
|
+
// 在首个 await 前创建取消控制器,覆盖 disconnect 与清理动作的竞态窗口。
|
|
2641
|
+
const reconnectAbort = new AbortController();
|
|
2642
|
+
this._reconnectAbort = reconnectAbort;
|
|
2452
2643
|
// 不等待状态事件或 transport 清理,先隔离已在途的 inline Push 回调。
|
|
2453
2644
|
this._delivery.resetInlineAckState();
|
|
2454
2645
|
this._state = 'standby';
|
|
@@ -2464,8 +2655,18 @@ export class AUNClient {
|
|
|
2464
2655
|
state: this._publicState(this._state),
|
|
2465
2656
|
error,
|
|
2466
2657
|
});
|
|
2658
|
+
if (reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort || this._closing) {
|
|
2659
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2660
|
+
this._reconnectAbort = null;
|
|
2661
|
+
this._reconnectActive = false;
|
|
2662
|
+
}
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2467
2665
|
if (!this._sessionOptions.auto_reconnect) {
|
|
2468
|
-
this.
|
|
2666
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2667
|
+
this._reconnectAbort = null;
|
|
2668
|
+
this._reconnectActive = false;
|
|
2669
|
+
}
|
|
2469
2670
|
return;
|
|
2470
2671
|
}
|
|
2471
2672
|
// 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
|
|
@@ -2487,30 +2688,90 @@ export class AUNClient {
|
|
|
2487
2688
|
eventPayload.code = disconnectInfo.code;
|
|
2488
2689
|
}
|
|
2489
2690
|
await this._dispatcher.publish('state_change', eventPayload);
|
|
2490
|
-
this.
|
|
2691
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2692
|
+
this._reconnectAbort = null;
|
|
2693
|
+
this._reconnectActive = false;
|
|
2694
|
+
}
|
|
2491
2695
|
return;
|
|
2492
2696
|
}
|
|
2493
2697
|
// 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
|
|
2494
2698
|
const serverInitiated = closeCode !== undefined && closeCode !== 1000 && closeCode !== 1006;
|
|
2495
|
-
if (this._closing) {
|
|
2496
|
-
this.
|
|
2699
|
+
if (this._closing || reconnectAbort.signal.aborted || this._reconnectAbort !== reconnectAbort) {
|
|
2700
|
+
if (this._reconnectAbort === reconnectAbort) {
|
|
2701
|
+
this._reconnectAbort = null;
|
|
2702
|
+
this._reconnectActive = false;
|
|
2703
|
+
}
|
|
2497
2704
|
return;
|
|
2498
2705
|
}
|
|
2499
|
-
const
|
|
2500
|
-
this.
|
|
2501
|
-
this._safeAsync(
|
|
2706
|
+
const reconnectTask = Promise.resolve().then(() => this._reconnectLoop(serverInitiated, reconnectAbort, initialOptions));
|
|
2707
|
+
this._reconnectTask = reconnectTask;
|
|
2708
|
+
this._safeAsync(reconnectTask);
|
|
2709
|
+
}
|
|
2710
|
+
_ownsReconnect(owner) {
|
|
2711
|
+
return Boolean(owner
|
|
2712
|
+
&& !owner.signal.aborted
|
|
2713
|
+
&& !this._closing
|
|
2714
|
+
&& this._reconnectAbort === owner);
|
|
2715
|
+
}
|
|
2716
|
+
_assertReconnectOwner(owner) {
|
|
2717
|
+
if (owner && !this._ownsReconnect(owner)) {
|
|
2718
|
+
throw new ConnectionError('reconnect attempt superseded');
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
async _publishReconnectEvent(owner, event, payload) {
|
|
2722
|
+
if (!this._ownsReconnect(owner))
|
|
2723
|
+
return false;
|
|
2724
|
+
this._reconnectEventDispatchDepth += 1;
|
|
2725
|
+
try {
|
|
2726
|
+
await this._dispatcher.publish(event, payload);
|
|
2727
|
+
}
|
|
2728
|
+
finally {
|
|
2729
|
+
this._reconnectEventDispatchDepth -= 1;
|
|
2730
|
+
}
|
|
2731
|
+
return this._ownsReconnect(owner);
|
|
2732
|
+
}
|
|
2733
|
+
async _cancelReconnectAndWait() {
|
|
2734
|
+
const owner = this._reconnectAbort;
|
|
2735
|
+
const task = this._reconnectTask;
|
|
2736
|
+
if (!owner && !task && !this._reconnectActive)
|
|
2737
|
+
return;
|
|
2738
|
+
owner?.abort();
|
|
2739
|
+
try {
|
|
2740
|
+
await this._transport.close();
|
|
2741
|
+
}
|
|
2742
|
+
catch (exc) {
|
|
2743
|
+
this._clientLog.debug(`reconnect cancellation transport cleanup skipped: ${formatCaughtError(exc)}`);
|
|
2744
|
+
}
|
|
2745
|
+
// state_change handler 内等待其所属重连任务会自锁;该路径由事件返回后的
|
|
2746
|
+
// owner 复核负责收尾,且事件回调期间尚未进入下一次 transport 操作。
|
|
2747
|
+
if (task && this._reconnectEventDispatchDepth === 0) {
|
|
2748
|
+
try {
|
|
2749
|
+
await task;
|
|
2750
|
+
}
|
|
2751
|
+
catch (exc) {
|
|
2752
|
+
this._clientLog.debug(`cancelled reconnect task exited with error: ${formatCaughtError(exc)}`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
if (this._reconnectAbort === owner) {
|
|
2756
|
+
this._reconnectAbort = null;
|
|
2757
|
+
this._reconnectActive = false;
|
|
2758
|
+
this._nextRetryAt = null;
|
|
2759
|
+
}
|
|
2760
|
+
if (this._reconnectTask === task)
|
|
2761
|
+
this._reconnectTask = null;
|
|
2502
2762
|
}
|
|
2503
2763
|
/** 重连循环入口;无论正常退出还是意外异常,都只清理自己这一代控制器。 */
|
|
2504
|
-
async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort) {
|
|
2764
|
+
async _reconnectLoop(serverInitiated = false, reconnectAbort = this._reconnectAbort, initialOptions) {
|
|
2505
2765
|
let releasedOwnGeneration = false;
|
|
2506
2766
|
try {
|
|
2507
|
-
await this._runReconnectLoop(serverInitiated, reconnectAbort);
|
|
2767
|
+
await this._runReconnectLoop(serverInitiated, reconnectAbort, initialOptions);
|
|
2508
2768
|
}
|
|
2509
2769
|
finally {
|
|
2510
2770
|
if (this._reconnectAbort === reconnectAbort) {
|
|
2511
2771
|
this._nextRetryAt = null;
|
|
2512
2772
|
this._reconnectActive = false;
|
|
2513
2773
|
this._reconnectAbort = null;
|
|
2774
|
+
this._reconnectTask = null;
|
|
2514
2775
|
releasedOwnGeneration = true;
|
|
2515
2776
|
}
|
|
2516
2777
|
}
|
|
@@ -2519,7 +2780,7 @@ export class AUNClient {
|
|
|
2519
2780
|
}
|
|
2520
2781
|
}
|
|
2521
2782
|
/** 指数退避 + 当前基数抖动重连循环主体。 */
|
|
2522
|
-
async _runReconnectLoop(serverInitiated, reconnectAbort) {
|
|
2783
|
+
async _runReconnectLoop(serverInitiated, reconnectAbort, initialOptions) {
|
|
2523
2784
|
const retry = { ...this._sessionOptions.retry };
|
|
2524
2785
|
const maxBaseDelay = clampReconnectDelaySeconds(retry.max_delay, RECONNECT_MAX_BASE_DELAY_SECONDS);
|
|
2525
2786
|
// M25: max_attempts=0 表示无限重试(与 Go/Python 对齐)
|
|
@@ -2534,11 +2795,12 @@ export class AUNClient {
|
|
|
2534
2795
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
2535
2796
|
this._state = 'terminal_failed';
|
|
2536
2797
|
this._nextRetryAt = null;
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2798
|
+
if (reconnectAbort)
|
|
2799
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2800
|
+
state: this._publicState(this._state),
|
|
2801
|
+
attempt: attempt - 1,
|
|
2802
|
+
reason: 'max_attempts_exhausted',
|
|
2803
|
+
});
|
|
2542
2804
|
return;
|
|
2543
2805
|
}
|
|
2544
2806
|
// 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
|
|
@@ -2546,22 +2808,26 @@ export class AUNClient {
|
|
|
2546
2808
|
const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
|
|
2547
2809
|
this._nextRetryAt = new Date(Date.now() + sleepMs);
|
|
2548
2810
|
this._state = 'retry_backoff';
|
|
2549
|
-
await this.
|
|
2811
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2550
2812
|
state: this._publicState(this._state),
|
|
2551
2813
|
attempt,
|
|
2552
2814
|
next_retry_at: this._nextRetryAt.getTime() / 1000,
|
|
2553
|
-
})
|
|
2815
|
+
}))
|
|
2816
|
+
return;
|
|
2554
2817
|
try {
|
|
2555
2818
|
await this._sleep(sleepMs, reconnectAbort?.signal);
|
|
2556
2819
|
this._nextRetryAt = null;
|
|
2557
|
-
if (reconnectAbort
|
|
2820
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2558
2821
|
return;
|
|
2559
2822
|
// 退避结束,进入 reconnecting 状态
|
|
2560
2823
|
this._state = 'reconnecting';
|
|
2561
|
-
await this.
|
|
2824
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2562
2825
|
state: this._publicState(this._state),
|
|
2563
2826
|
attempt,
|
|
2564
|
-
})
|
|
2827
|
+
}))
|
|
2828
|
+
return;
|
|
2829
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2830
|
+
return;
|
|
2565
2831
|
// /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
|
|
2566
2832
|
if (this._gatewayUrl) {
|
|
2567
2833
|
void this._discovery.checkHealth(this._gatewayUrl, 5000)
|
|
@@ -2574,6 +2840,20 @@ export class AUNClient {
|
|
|
2574
2840
|
});
|
|
2575
2841
|
}
|
|
2576
2842
|
await this._transport.close();
|
|
2843
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2844
|
+
return;
|
|
2845
|
+
if (initialOptions) {
|
|
2846
|
+
// 首连失败时没有可复用的 session/gateway,必须重新 discovery + authenticate。
|
|
2847
|
+
await this._lifecycle.retryInitialConnect(initialOptions, reconnectAbort ?? undefined);
|
|
2848
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2849
|
+
return;
|
|
2850
|
+
if (!this._transport.isConnected()) {
|
|
2851
|
+
throw new ConnectionError('websocket disconnected while initial reconnect completed');
|
|
2852
|
+
}
|
|
2853
|
+
this._clientLog.debug(`initial reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
|
|
2854
|
+
this._nextRetryAt = null;
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2577
2857
|
if (!this._sessionParams) {
|
|
2578
2858
|
throw new StateError('missing connect params for reconnect');
|
|
2579
2859
|
}
|
|
@@ -2601,7 +2881,9 @@ export class AUNClient {
|
|
|
2601
2881
|
for (const gateway of candidates) {
|
|
2602
2882
|
const attemptParams = { ...this._sessionParams, gateway };
|
|
2603
2883
|
try {
|
|
2604
|
-
await this._connectOnce(attemptParams, true);
|
|
2884
|
+
await this._connectOnce(attemptParams, true, reconnectAbort ?? undefined);
|
|
2885
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2886
|
+
return;
|
|
2605
2887
|
if (!this._transport.isConnected()) {
|
|
2606
2888
|
throw new ConnectionError('websocket disconnected while reconnecting');
|
|
2607
2889
|
}
|
|
@@ -2627,20 +2909,24 @@ export class AUNClient {
|
|
|
2627
2909
|
return;
|
|
2628
2910
|
}
|
|
2629
2911
|
catch (exc) {
|
|
2912
|
+
if (!this._ownsReconnect(reconnectAbort))
|
|
2913
|
+
return;
|
|
2630
2914
|
this._lastError = exc instanceof Error ? exc : new Error(String(exc));
|
|
2631
2915
|
this._lastErrorCode = 'reconnect_failed';
|
|
2632
|
-
await this.
|
|
2916
|
+
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'connection.error', {
|
|
2633
2917
|
error: formatCaughtError(exc),
|
|
2634
2918
|
attempt,
|
|
2635
|
-
})
|
|
2919
|
+
}))
|
|
2920
|
+
return;
|
|
2636
2921
|
if (!this._shouldRetryReconnect(exc)) {
|
|
2637
2922
|
this._state = 'terminal_failed';
|
|
2638
2923
|
this._nextRetryAt = null;
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2924
|
+
if (reconnectAbort)
|
|
2925
|
+
await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
2926
|
+
state: this._publicState(this._state),
|
|
2927
|
+
error: formatCaughtError(exc),
|
|
2928
|
+
attempt,
|
|
2929
|
+
});
|
|
2644
2930
|
return;
|
|
2645
2931
|
}
|
|
2646
2932
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
@@ -2663,7 +2949,7 @@ export class AUNClient {
|
|
|
2663
2949
|
|| error instanceof ValidationError || error instanceof StateError) {
|
|
2664
2950
|
return false;
|
|
2665
2951
|
}
|
|
2666
|
-
if (error instanceof ConnectionError)
|
|
2952
|
+
if (error instanceof ConnectionError || error instanceof TimeoutError)
|
|
2667
2953
|
return true;
|
|
2668
2954
|
if (error instanceof AUNError)
|
|
2669
2955
|
return error.retryable;
|
|
@@ -2683,6 +2969,7 @@ export class AUNClient {
|
|
|
2683
2969
|
_resetSeqTrackingState() {
|
|
2684
2970
|
void this._saveSeqTrackerState();
|
|
2685
2971
|
this._resetV2IdentityRuntime();
|
|
2972
|
+
this._v2E2EE.resetForwardCursorState();
|
|
2686
2973
|
this._seqTracker = new SeqTracker();
|
|
2687
2974
|
this._seqTrackerContext = null;
|
|
2688
2975
|
this._gapFillDone.clear();
|
|
@@ -2692,18 +2979,13 @@ export class AUNClient {
|
|
|
2692
2979
|
this._v2SenderIKPending.clear();
|
|
2693
2980
|
this._v2SenderIKFetching.clear();
|
|
2694
2981
|
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
2982
|
}
|
|
2702
2983
|
_refreshSeqTrackerContext() {
|
|
2703
2984
|
const nextContext = this._currentSeqTrackerContext();
|
|
2704
2985
|
if (nextContext === this._seqTrackerContext)
|
|
2705
2986
|
return;
|
|
2706
2987
|
void this._saveSeqTrackerState();
|
|
2988
|
+
this._v2E2EE.resetForwardCursorState();
|
|
2707
2989
|
this._seqTracker = new SeqTracker();
|
|
2708
2990
|
this._delivery.resetInlineAckState();
|
|
2709
2991
|
this._gapFillDone.clear();
|
|
@@ -2713,12 +2995,6 @@ export class AUNClient {
|
|
|
2713
2995
|
this._v2SenderIKPending.clear();
|
|
2714
2996
|
this._v2SenderIKFetching.clear();
|
|
2715
2997
|
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
2998
|
this._seqTrackerContext = nextContext;
|
|
2723
2999
|
}
|
|
2724
3000
|
/** 将 SeqTracker 状态保存到 keystore */
|
|
@@ -2941,7 +3217,7 @@ export class AUNClient {
|
|
|
2941
3217
|
* undecryptable 事件、成功消费后触发 SPK 轮换。inline Push 在游标提交和
|
|
2942
3218
|
* Head/正文绑定校验前调用本入口时必须显式关闭这些副作用。
|
|
2943
3219
|
*/
|
|
2944
|
-
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true) {
|
|
3220
|
+
async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false) {
|
|
2945
3221
|
const session = this._v2Session;
|
|
2946
3222
|
if (!session)
|
|
2947
3223
|
return null;
|
|
@@ -3008,6 +3284,24 @@ export class AUNClient {
|
|
|
3008
3284
|
}
|
|
3009
3285
|
catch (exc) {
|
|
3010
3286
|
this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${String(exc)}`);
|
|
3287
|
+
if (allowPending || deferStatus) {
|
|
3288
|
+
const fromAid = String(msg.from_aid ?? '');
|
|
3289
|
+
const senderDeviceId = String(aad.from_device ?? '');
|
|
3290
|
+
if (deferStatus) {
|
|
3291
|
+
deferStatus.deferred = true;
|
|
3292
|
+
deferStatus.fromAid = fromAid;
|
|
3293
|
+
deferStatus.senderDeviceId = senderDeviceId;
|
|
3294
|
+
deferStatus.groupId = groupIdForKeys;
|
|
3295
|
+
}
|
|
3296
|
+
this._scheduleV2SenderIKPending({
|
|
3297
|
+
msg,
|
|
3298
|
+
fromAid,
|
|
3299
|
+
senderDeviceId,
|
|
3300
|
+
groupId: groupIdForKeys,
|
|
3301
|
+
startFetch: !deferKeyFetch,
|
|
3302
|
+
});
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3011
3305
|
if (emitUndecryptable) {
|
|
3012
3306
|
try {
|
|
3013
3307
|
const event = {
|
|
@@ -3035,13 +3329,19 @@ export class AUNClient {
|
|
|
3035
3329
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
3036
3330
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
3037
3331
|
let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
3038
|
-
if (!senderPubDer) {
|
|
3332
|
+
if (!senderPubDer && !deferKeyFetch) {
|
|
3039
3333
|
this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
3040
3334
|
senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
3041
3335
|
}
|
|
3042
3336
|
if (!senderPubDer) {
|
|
3043
|
-
if (allowPending) {
|
|
3044
|
-
|
|
3337
|
+
if (allowPending || deferStatus) {
|
|
3338
|
+
if (deferStatus) {
|
|
3339
|
+
deferStatus.deferred = true;
|
|
3340
|
+
deferStatus.fromAid = fromAid;
|
|
3341
|
+
deferStatus.senderDeviceId = senderDeviceId;
|
|
3342
|
+
deferStatus.groupId = groupIdForKeys;
|
|
3343
|
+
}
|
|
3344
|
+
this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys, startFetch: !deferKeyFetch });
|
|
3045
3345
|
return null;
|
|
3046
3346
|
}
|
|
3047
3347
|
if (emitUndecryptable) {
|
|
@@ -3069,9 +3369,13 @@ export class AUNClient {
|
|
|
3069
3369
|
let plaintext;
|
|
3070
3370
|
try {
|
|
3071
3371
|
plaintext = await decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
|
|
3372
|
+
if (plaintext === null)
|
|
3373
|
+
throw new Error('recipient_not_found_or_envelope_verification_failed');
|
|
3072
3374
|
}
|
|
3073
3375
|
catch (exc) {
|
|
3074
3376
|
this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${String(exc)}`);
|
|
3377
|
+
if (deferStatus)
|
|
3378
|
+
deferStatus.terminal = true;
|
|
3075
3379
|
if (emitUndecryptable) {
|
|
3076
3380
|
try {
|
|
3077
3381
|
const event = {
|
|
@@ -3321,18 +3625,6 @@ export class AUNClient {
|
|
|
3321
3625
|
this._clientLog.warn(`background task exception:${String(exc)}`);
|
|
3322
3626
|
});
|
|
3323
3627
|
}
|
|
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
3628
|
// ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
|
|
3337
3629
|
async _withBackgroundRpc(operation) {
|
|
3338
3630
|
this._backgroundRpcDepth += 1;
|