@agentunion/fastaun 0.5.34 → 0.5.36

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/_packed_docs/CHANGELOG.md +56 -1
  3. package/_packed_docs/INDEX.md +18 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +9 -9
  5. package/_packed_docs/agent.md/SCHEMA.md +1 -1
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +2 -2
  7. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
  8. package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
  9. package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
  10. package/_packed_docs/sdk/0.5.35-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +189 -0
  11. package/_packed_docs/sdk/0.5.35-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +204 -0
  12. package/_packed_docs/sdk/0.5.36-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +130 -0
  13. package/_packed_docs/sdk/0.5.36-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +176 -0
  14. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +2 -2
  15. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +32 -6
  16. package/_packed_docs/sdk/09-group-rpc-manual.md +7 -7
  17. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -13
  18. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +73 -3
  19. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +50 -8
  20. package/_packed_docs/sdk/INDEX.md +23 -18
  21. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
  22. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +5 -5
  23. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +5 -5
  24. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
  25. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/345/256/236/346/226/275/350/256/241/345/210/222.md +400 -0
  26. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/346/226/271/346/241/210.md +485 -0
  27. package/dist/agent-md.d.ts +2 -1
  28. package/dist/agent-md.js +33 -6
  29. package/dist/agent-md.js.map +1 -1
  30. package/dist/aid-store.js +5 -1
  31. package/dist/aid-store.js.map +1 -1
  32. package/dist/auth.js +36 -45
  33. package/dist/auth.js.map +1 -1
  34. package/dist/client/delivery.d.ts +8 -1
  35. package/dist/client/delivery.js +203 -50
  36. package/dist/client/delivery.js.map +1 -1
  37. package/dist/client/lifecycle.js +10 -1
  38. package/dist/client/lifecycle.js.map +1 -1
  39. package/dist/client/rpc-pipeline.d.ts +4 -0
  40. package/dist/client/rpc-pipeline.js +93 -10
  41. package/dist/client/rpc-pipeline.js.map +1 -1
  42. package/dist/client/sync-channels.d.ts +2 -1
  43. package/dist/client/sync-channels.js +33 -14
  44. package/dist/client/sync-channels.js.map +1 -1
  45. package/dist/client/v2-e2ee.d.ts +1 -0
  46. package/dist/client/v2-e2ee.js +24 -6
  47. package/dist/client/v2-e2ee.js.map +1 -1
  48. package/dist/client.d.ts +15 -0
  49. package/dist/client.js +329 -56
  50. package/dist/client.js.map +1 -1
  51. package/dist/events.d.ts +1 -0
  52. package/dist/events.js +5 -0
  53. package/dist/events.js.map +1 -1
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +1 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/mail-client.d.ts +101 -0
  58. package/dist/mail-client.js +369 -0
  59. package/dist/mail-client.js.map +1 -0
  60. package/dist/ownership-v21.d.ts +2 -1
  61. package/dist/ownership-v21.js +19 -9
  62. package/dist/ownership-v21.js.map +1 -1
  63. package/dist/pubsub/binding.js +9 -2
  64. package/dist/pubsub/binding.js.map +1 -1
  65. package/dist/pubsub/channel.d.ts +11 -3
  66. package/dist/pubsub/channel.js +91 -10
  67. package/dist/pubsub/channel.js.map +1 -1
  68. package/dist/pubsub/client.d.ts +8 -2
  69. package/dist/pubsub/client.js +62 -21
  70. package/dist/pubsub/client.js.map +1 -1
  71. package/dist/pubsub/index.d.ts +2 -2
  72. package/dist/pubsub/index.js +1 -1
  73. package/dist/pubsub/index.js.map +1 -1
  74. package/dist/pubsub/service.d.ts +6 -0
  75. package/dist/pubsub/service.js +101 -10
  76. package/dist/pubsub/service.js.map +1 -1
  77. package/dist/pubsub/types.d.ts +11 -1
  78. package/dist/tools/cross-sdk-agent.d.ts +1 -1
  79. package/dist/transport.d.ts +4 -0
  80. package/dist/transport.js +39 -2
  81. package/dist/transport.js.map +1 -1
  82. package/dist/udp-standby.d.ts +88 -0
  83. package/dist/udp-standby.js +483 -0
  84. package/dist/udp-standby.js.map +1 -0
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/package.json +5 -1
  88. package/scripts/patch-imapflow-external.mjs +69 -0
package/dist/client.js CHANGED
@@ -26,6 +26,7 @@ import { AUNLogger } from './logger.js';
26
26
  import { normalizeGroupId } from './group-id.js';
27
27
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
28
28
  import { RPCTransport } from './transport.js';
29
+ import { UdpStandbyController } from './udp-standby.js';
29
30
  import { SyncChannels } from './client/sync-channels.js';
30
31
  import { AuthFlow } from './auth.js';
31
32
  import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
@@ -909,6 +910,9 @@ export class AUNClient {
909
910
  _discovery;
910
911
  /** 传输层 */
911
912
  _transport;
913
+ _udpStandby = null;
914
+ _udpStandbyPolicy;
915
+ _udpStandbyConnectOptions;
912
916
  /** 认证流程 */
913
917
  _auth;
914
918
  resolveCredentialService;
@@ -932,6 +936,8 @@ export class AUNClient {
932
936
  _groupIdentityOperations = new Map();
933
937
  _entityOwnershipConnection = 0;
934
938
  _entityOwnershipChecks = new Map();
939
+ _postConnectRecoveryTimer = null;
940
+ _postConnectRecoveryAbort = null;
935
941
  _groupIndexMetaCache = new GroupIndexMetaCache();
936
942
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
937
943
  _p2pPeerPullEnabled = true;
@@ -1409,25 +1415,30 @@ export class AUNClient {
1409
1415
  const observation = { root: manager.root, etag: observedEtag, version: Number(record?.observation_version ?? 0), content_hash: typeof record?.content === 'string' ? ownershipSha256(record.content) : '' };
1410
1416
  const cache = this._ownershipConsensusCache ??= new Map();
1411
1417
  const cached = cache.get(target);
1412
- if (!force && cached?.result.status === 'consensus' && record?.remote_status !== 'missing'
1418
+ if (cached?.result.status === 'consensus' && cached.observedVersion === observation.version && record?.remote_status !== 'missing'
1419
+ && (!record?.remote_owner_md_sha256 || record.remote_owner_md_sha256 === cached.result.owner_md_sha256)
1413
1420
  && (observedEtag === cached.observedEtag || observedEtag === cached.result.agent_md_etag)
1414
1421
  && (typeof record?.content !== 'string' || ownershipSha256(record.content) === cached.result.agent_md_sha256)) {
1415
1422
  cached.observedEtag = observedEtag;
1416
1423
  return { ...cached.result };
1417
1424
  }
1418
- if (!force && !cached) {
1425
+ if (!cached) {
1419
1426
  const local = manager.localOwnershipConsensus(target, record);
1420
1427
  if (local) {
1421
- cache.set(target, { observedEtag, result: local });
1428
+ cache.set(target, { observedEtag, observedVersion: observation.version, result: local });
1422
1429
  return { ...local };
1423
1430
  }
1424
1431
  }
1425
1432
  cache.delete(target);
1426
1433
  const result = { status: 'unavailable', sub_aid: target, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '' };
1427
- const entry = { observedEtag, result: { ...result } };
1434
+ const entry = { observedEtag, observedVersion: observation.version, result: { ...result } };
1428
1435
  cache.set(target, entry);
1436
+ let consensusContent;
1429
1437
  try {
1430
1438
  const snapshot = await manager.readOwnershipFresh(target);
1439
+ consensusContent = snapshot.content;
1440
+ if (snapshot.owner_md_sha256)
1441
+ result.owner_md_sha256 = snapshot.owner_md_sha256;
1431
1442
  Object.assign(result, { relation_version: snapshot.relation_version, lock: snapshot.lock, agent_md_etag: snapshot.etag,
1432
1443
  agent_md_sha256: snapshot.exists ? ownershipSha256(snapshot.content) : '' });
1433
1444
  if (snapshot.ownership_status)
@@ -1448,11 +1459,15 @@ export class AUNClient {
1448
1459
  result.retryable = result.status === 'unavailable';
1449
1460
  }
1450
1461
  if (this._agentMdManager === manager && this._ownershipConsensusCache === cache && cache.get(target) === entry) {
1451
- entry.result = { ...result };
1452
1462
  try {
1453
- manager.cacheOwnershipSnapshot(target, result, observation);
1463
+ if (manager.cacheOwnershipSnapshot(target, result, observation, consensusContent))
1464
+ entry.result = { ...result };
1465
+ else
1466
+ cache.delete(target);
1467
+ }
1468
+ catch {
1469
+ cache.delete(target);
1454
1470
  }
1455
- catch { }
1456
1471
  }
1457
1472
  return result;
1458
1473
  }
@@ -1598,6 +1613,10 @@ export class AUNClient {
1598
1613
  }
1599
1614
  _applyAidRuntimeContext(aid) {
1600
1615
  this._aidStore = aid._aidStore;
1616
+ this._udpStandby?.close();
1617
+ this._udpStandby = null;
1618
+ this._udpStandbyPolicy = undefined;
1619
+ this._udpStandbyConnectOptions = undefined;
1601
1620
  const oldTransport = this._transport;
1602
1621
  try {
1603
1622
  const closeResult = oldTransport?.close?.();
@@ -1850,10 +1869,14 @@ export class AUNClient {
1850
1869
  }
1851
1870
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1852
1871
  async connect(opts) {
1872
+ if (this._udpStandby && ['standby', 'preparing', 'waking'].includes(this._udpStandby.state))
1873
+ return this._udpStandby.ensureActive();
1874
+ this._udpStandbyConnectOptions = opts ? { ...opts } : undefined;
1853
1875
  await this._lifecycle.connect(opts);
1854
1876
  }
1855
1877
  /** 关闭连接 */
1856
1878
  async close() {
1879
+ this._udpStandby?.close();
1857
1880
  this._risk.close();
1858
1881
  this._peerGatewayCache.clear();
1859
1882
  return this._lifecycle.close();
@@ -1863,6 +1886,7 @@ export class AUNClient {
1863
1886
  * disconnect 是可恢复的:停止心跳、关闭 WebSocket,但不清理 keystore 等状态。
1864
1887
  */
1865
1888
  async disconnect() {
1889
+ this._udpStandby?.close();
1866
1890
  return this._lifecycle.disconnect();
1867
1891
  }
1868
1892
  get riskState() { return Object.freeze(structuredClone(this._risk.state)); }
@@ -2020,27 +2044,47 @@ export class AUNClient {
2020
2044
  * 自动处理内部方法限制、E2EE 加解密、客户端签名等。
2021
2045
  */
2022
2046
  async call(method, params, options) {
2023
- const assertCurrent = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? this._entityOwnershipGuard() : undefined;
2024
- const recoverOwnership = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
2025
- await this._checkEntityOwnershipOnAccess(method, params ?? {});
2026
- const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
2027
- ? this._ensureReadStateIdentity() : null;
2028
- try {
2029
- const result = await (options === undefined ? this._rpcPipeline.call(method, params, assertCurrent) : this._rpcPipeline.call(method, params, assertCurrent, options));
2030
- await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
2031
- if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
2032
- this._observeReadStateResult(method, result, params ?? {});
2033
- return result;
2047
+ const echoStarted = performance.now();
2048
+ if ((method === 'message.send' || method === 'group.send') && params && this._isEchoPayload(params.payload ?? params.content)) {
2049
+ params = { ...params };
2050
+ this._echoStarts.set(params, echoStarted);
2051
+ }
2052
+ const lifecycle = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method);
2053
+ let standbyRelease;
2054
+ if (lifecycle && this._udpStandby) {
2055
+ const deadline = Math.min(options?.deadline ?? Infinity, typeof params?._rpc_deadline === 'number' ? params._rpc_deadline : Infinity, Date.now() + Number(this._sessionOptions?.timeouts?.call ?? 35) * 1000);
2056
+ standbyRelease = await this._udpStandby.acquire(method, deadline);
2057
+ options = { ...options, deadline };
2034
2058
  }
2035
- catch (err) {
2036
- if (recoverOwnership) {
2037
- try {
2038
- await recoverOwnership(err);
2059
+ try {
2060
+ const assertCurrent = lifecycle ? this._entityOwnershipGuard() : undefined;
2061
+ const recoverOwnership = lifecycle ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
2062
+ await this._checkEntityOwnershipOnAccess(method, params ?? {});
2063
+ const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
2064
+ ? this._ensureReadStateIdentity() : null;
2065
+ try {
2066
+ const result = await (options === undefined ? this._rpcPipeline.call(method, params, assertCurrent) : this._rpcPipeline.call(method, params, assertCurrent, options));
2067
+ await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
2068
+ if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
2069
+ this._observeReadStateResult(method, result, params ?? {});
2070
+ return result;
2071
+ }
2072
+ catch (err) {
2073
+ if (recoverOwnership) {
2074
+ try {
2075
+ await recoverOwnership(err);
2076
+ }
2077
+ catch { }
2039
2078
  }
2040
- catch { }
2079
+ await this._maybeHandleHalfOpenRpcFailure(err);
2080
+ const returned = await this._echoFailure(method, params, err);
2081
+ if (returned)
2082
+ return returned;
2083
+ throw err;
2041
2084
  }
2042
- await this._maybeHandleHalfOpenRpcFailure(err);
2043
- throw err;
2085
+ }
2086
+ finally {
2087
+ standbyRelease?.();
2044
2088
  }
2045
2089
  }
2046
2090
  _entityOwnershipGuard() {
@@ -2083,6 +2127,7 @@ export class AUNClient {
2083
2127
  throw ownershipFailure('conflict', 'entity lifecycle ownership action changed');
2084
2128
  const ownership = { statement: declaration.statement, action, ...(transfer ? { transfer: { new_owner_aid: ownershipAid(newOwner, 'new_owner_aid'), target_relation_version: declaration.statement.relation_version + 1, expires_at: Number(params.expires_at ?? Math.floor(Date.now() / 1000) + 3600) } } : {}) };
2085
2129
  const raw = signOwnershipFile(owner, `---\nowner_aid: ${owner.aid}\nsub_aid: ${target}\nownership: ${JSON.stringify(ownership)}\n---\n`, 'owner');
2130
+ await this._getOwnershipFiles().verifyFile(raw, 'owner', target, owner.aid, true);
2086
2131
  await storage.writeOwnerMd(owner.aid, target, raw, original, assertCurrent);
2087
2132
  return async (error) => {
2088
2133
  if (!(error instanceof AUNError) || error instanceof TimeoutError || error instanceof ConnectionError || error.retryable || isJsonObject(error.data) && error.data.committed === true || ![-32602, 4000, 4030, 403].includes(error.code) && !['INVALID_ARGUMENT', 'ROLE_ACCESS_DENIED', 'MEMBER_NOT_FOUND', 'permission_denied', 'member_banned', 'member_blocked'].includes(error.stringCode))
@@ -2228,12 +2273,14 @@ export class AUNClient {
2228
2273
  else {
2229
2274
  const payload = `---\nowner_aid: ${owner.aid}\nsub_aid: ${aid}\nownership: ${JSON.stringify({ statement, action: 'declare' })}\n---\n`;
2230
2275
  const raw = signOwnershipFile(owner, payload, 'owner');
2276
+ await this._getOwnershipFiles().verifyFile(raw, 'owner', aid, owner.aid, true);
2231
2277
  assertCurrent();
2232
2278
  await storage.writeOwnerMd(owner.aid, aid, raw, original, assertCurrent);
2233
2279
  }
2234
2280
  const source = content ?? (observed.exists ? observed.content : buildDefaultAgentMd(aid, { type, name: String(entity.name ?? aid) }));
2235
2281
  const updated = updateOwnershipAgentMd(source, { owner: owner.aid, ownership: { statement } }).replace(/^entity_cert_fingerprint:.*(?:\r?\n|$)/m, '');
2236
2282
  const signed = signOwnershipFile(local.data.aid, updated, 'agent');
2283
+ await this._getOwnershipFiles().verifyFile(signed, 'agent', aid, undefined, true);
2237
2284
  if (content === undefined) {
2238
2285
  if (!previous || previous.owner_aid !== owner.aid || previous.relation_version !== version) {
2239
2286
  assertCurrent();
@@ -2268,9 +2315,27 @@ export class AUNClient {
2268
2315
  const groupId = String(params.group_aid ?? params.groupAid ?? params.group_id ?? params.groupId ?? message.slice(prefix.length)).trim();
2269
2316
  const target = normalizeGroupId(groupId.toLowerCase(), { localIssuer: this._aid?.split('.').slice(1).join('.') });
2270
2317
  const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
2271
- if (!this._entityOwnershipChecks.has(key))
2272
- await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
2273
- return await this._entityOwnershipChecks.get(key) ?? false;
2318
+ const assertCurrent = this._entityOwnershipGuard();
2319
+ try {
2320
+ assertCurrent();
2321
+ const pending = this._groupIdentityOperations.get(key);
2322
+ if (pending && !['group.create', 'group.bind_group_aid'].includes(_method)) {
2323
+ const local = this._aidStore?.load(target);
2324
+ if (!local?.ok || !local.data.aid.isPrivateKeyValid()) {
2325
+ await pending.task;
2326
+ assertCurrent();
2327
+ return true;
2328
+ }
2329
+ }
2330
+ if (!this._entityOwnershipChecks.has(key))
2331
+ await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
2332
+ const recovered = await this._entityOwnershipChecks.get(key) ?? false;
2333
+ assertCurrent();
2334
+ return recovered;
2335
+ }
2336
+ catch {
2337
+ return false;
2338
+ }
2274
2339
  }
2275
2340
  }
2276
2341
  const agentPrefix = 'valid agent.md is required for ';
@@ -2319,17 +2384,14 @@ export class AUNClient {
2319
2384
  catch (exc) {
2320
2385
  this._clientLog.warn(`post-connect agent.md check failed: ${formatCaughtError(exc)}`);
2321
2386
  }
2322
- const store = this._aidStore;
2323
- if (!store)
2324
- return;
2325
- await this._resumeOrgIdentityAfterConnect(store);
2326
2387
  }
2327
2388
  async _runGroupIdentityOperation(groupId, operation, context = '') {
2328
2389
  const aid = String(this._aid ?? '').trim();
2329
2390
  const dot = aid.indexOf('.');
2330
- const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
2331
- if (!key)
2391
+ const target = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
2392
+ if (!target)
2332
2393
  return operation();
2394
+ const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
2333
2395
  const pending = this._groupIdentityOperations.get(key);
2334
2396
  if (pending && pending.context !== context)
2335
2397
  throw ownershipFailure('conflict', 'pending Org creation context changed');
@@ -2413,12 +2475,42 @@ export class AUNClient {
2413
2475
  tokens.saveMetadata(orgAid, { org_identity_creation: null });
2414
2476
  return checkpoint.result;
2415
2477
  }
2416
- async _resumeOrgIdentityAfterConnect(store) {
2478
+ _schedulePostConnectRecovery() {
2479
+ this._postConnectRecoveryAbort?.abort();
2480
+ if (this._postConnectRecoveryTimer !== null)
2481
+ clearTimeout(this._postConnectRecoveryTimer);
2482
+ this._postConnectRecoveryTimer = null;
2483
+ const store = this._aidStore;
2484
+ if (this._sessionOptions.background_sync === false)
2485
+ return;
2486
+ const abort = new AbortController();
2487
+ this._postConnectRecoveryAbort = abort;
2488
+ const aid = this._aid, version = this._serviceEventIdentityVersion, connection = this._entityOwnershipConnection;
2489
+ const current = () => !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && connection === this._entityOwnershipConnection && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
2490
+ this._postConnectRecoveryTimer = setTimeout(() => {
2491
+ this._postConnectRecoveryTimer = null;
2492
+ if (!current())
2493
+ return;
2494
+ this._safeAsync(this._delivery.restoreMessageClassPulls());
2495
+ void this._fillP2pGap().catch((exc) => {
2496
+ this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
2497
+ });
2498
+ if (store && !this._ownershipOperation)
2499
+ this._safeAsync(this._resumeOrgIdentityAfterConnect(store, current));
2500
+ }, 30_000 + Math.random() * 30_000);
2501
+ this._postConnectRecoveryTimer.unref?.();
2502
+ }
2503
+ async _resumeOrgIdentityAfterConnect(store, current = () => true) {
2504
+ if (!current())
2505
+ return;
2417
2506
  const keyStore = store._keystore;
2418
2507
  const tokens = store._tokenStore;
2419
2508
  if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
2420
2509
  return;
2421
2510
  for (const aid of keyStore.listIdentities()) {
2511
+ await new Promise((resolve) => setTimeout(resolve, 0));
2512
+ if (!current())
2513
+ return;
2422
2514
  const metadata = typeof tokens._loadMetadataStrict === 'function' ? tokens._loadMetadataStrict(aid) : tokens.loadMetadata(aid);
2423
2515
  const creation = metadata?.org_identity_creation;
2424
2516
  const transfer = metadata?.org_identity_transfer;
@@ -3593,6 +3685,8 @@ export class AUNClient {
3593
3685
  // ── 事件自动解密管线 ──────────────────────────────────────
3594
3686
  /** 处理 transport 层推送的原始 P2P 消息 */
3595
3687
  async _onRawMessageReceived(data) {
3688
+ if (isJsonObject(data))
3689
+ this._echoStarts.set(data, performance.now());
3596
3690
  if (this._controlMessages().onPush(data))
3597
3691
  return;
3598
3692
  return this._delivery.onRawMessageReceived(data);
@@ -3602,6 +3696,8 @@ export class AUNClient {
3602
3696
  }
3603
3697
  /** 处理群组消息推送:自动解密后 re-publish */
3604
3698
  async _onRawGroupMessageCreated(data) {
3699
+ if (isJsonObject(data))
3700
+ this._echoStarts.set(data, performance.now());
3605
3701
  if (this._controlMessages().onPush(data, true))
3606
3702
  return;
3607
3703
  return this._delivery.onRawGroupMessageCreated(data);
@@ -3716,21 +3812,70 @@ export class AUNClient {
3716
3812
  const ms = String(now.getMilliseconds()).padStart(3, '0');
3717
3813
  return `${hh}:${mm}:${ss}.${ms}`;
3718
3814
  }
3815
+ _echoStarts = new WeakMap();
3719
3816
  _isEchoPayload(payload) {
3720
3817
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3721
3818
  return false;
3722
3819
  const text = payload.text;
3723
- if (typeof text !== 'string' || text.length > 4096)
3820
+ if (typeof text !== 'string')
3724
3821
  return false;
3725
- return text.split('\n', 1)[0].toLowerCase().includes('echo');
3822
+ const first = text.split('\n', 1)[0].trimStart();
3823
+ return /^(?:echo\.(?:ecdiag|ask)(?:\s|$)|echo(?:\s|\[|$))/i.test(first);
3824
+ }
3825
+ _echoHop(payload) {
3826
+ if (Number.isInteger(payload.hop)) {
3827
+ if (!Number.isSafeInteger(payload.hop))
3828
+ throw new ValidationError('echo hop must be a JSON safe integer');
3829
+ return Number(payload.hop);
3830
+ }
3831
+ const limit = /(?:^|\s)hop=([+-]?\d+)(?=\s|$)/i.exec(payload.text.split('\n', 1)[0]);
3832
+ const hop = limit ? Number(limit[1]) : 100;
3833
+ if (!Number.isSafeInteger(hop))
3834
+ throw new ValidationError('echo hop must be a JSON safe integer');
3835
+ return hop;
3836
+ }
3837
+ _isEchoReturn(payload) {
3838
+ return this._echoHop(payload) < 0 || payload.dir === 'ret' || payload.dir !== 'fwd' && payload.text.split('\n').some(line => line.trim() === '[AUN-Echo.return]');
3839
+ }
3840
+ _appendEchoTrace(payload, stage, started, endpoint = false) {
3841
+ const previousHop = this._echoHop(payload);
3842
+ const returning = endpoint || this._isEchoReturn(payload);
3843
+ const hop = returning ? previousHop - 1 : Math.max(previousHop - 1, 0);
3844
+ if (!Number.isSafeInteger(hop))
3845
+ throw new ValidationError('echo hop decrement exceeds the JSON safe integer range');
3846
+ const now = new Date();
3847
+ const offset = -now.getTimezoneOffset();
3848
+ const tz = `${offset < 0 ? '-' : '+'}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0')}:${String(Math.abs(offset) % 60).padStart(2, '0')}`;
3849
+ const uptime = this._connectedAt ? Math.floor((now.getTime() - this._connectedAt) / 1000) : 0;
3850
+ const duration = Math.max(0, Math.round((performance.now() - started) * 1000) / 1000);
3851
+ const trace = `${this._echoTimestamp()} [AUN-SDK.${stage}] dir=${returning ? 'ret' : 'fwd'} hop=${hop} enc=0 ver=${VERSION} epoch=${now.getTime()} tz=${tz}${returning ? ` duration_ms=${duration}` : ''} aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
3852
+ const result = { ...payload, type: 'text', hop, dir: returning ? 'ret' : 'fwd', text: payload.text.trimStart() + '\n' + trace };
3853
+ delete result.max_hop;
3854
+ delete result.rhop;
3855
+ return result;
3726
3856
  }
3727
3857
  _maybeAppendEchoTraceSend(params) {
3858
+ const reflected = params._echo_reflection === true;
3859
+ delete params._echo_reflection;
3860
+ for (const key of ['hop', 'max_hop', 'rhop', 'dir'])
3861
+ delete params[key];
3728
3862
  const payload = params.payload;
3729
- if (!this._isEchoPayload(payload))
3730
- return;
3731
- const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
3732
- const trace = `${this._echoTimestamp()} [AUN-SDK.send] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
3733
- params.payload = { ...payload, text: payload.text + '\n' + trace };
3863
+ if (!this._isEchoPayload(payload)) {
3864
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
3865
+ const clean = { ...payload };
3866
+ for (const key of ['hop', 'max_hop', 'rhop', 'dir'])
3867
+ delete clean[key];
3868
+ params.payload = clean;
3869
+ }
3870
+ return false;
3871
+ }
3872
+ if (reflected)
3873
+ return false;
3874
+ const started = this._echoStarts.get(params) ?? performance.now();
3875
+ this._echoStarts.set(params, started);
3876
+ const next = this._appendEchoTrace(payload, 'send', started);
3877
+ params.payload = next;
3878
+ return next.dir === 'fwd' && next.hop === 0;
3734
3879
  }
3735
3880
  _shouldSkipEventSignature(event) {
3736
3881
  if (event.encrypted || event.encrypt)
@@ -3738,14 +3883,79 @@ export class AUNClient {
3738
3883
  return this._isEchoPayload(event.payload);
3739
3884
  }
3740
3885
  _maybeAppendEchoTraceReceive(msg) {
3741
- if (msg.encrypted)
3742
- return;
3886
+ if (msg.encrypted || msg.encrypt)
3887
+ return false;
3888
+ const payload = msg.payload;
3889
+ if (!this._isEchoPayload(payload) || String(payload.type ?? '').startsWith('e2ee.') || payload.ciphertext)
3890
+ return false;
3891
+ const started = this._echoStarts.get(msg) ?? performance.now();
3892
+ this._echoStarts.set(msg, started);
3893
+ const next = this._appendEchoTrace(payload, 'receive', started);
3894
+ msg.payload = next;
3895
+ return next.dir === 'fwd' && next.hop === 0;
3896
+ }
3897
+ async _returnReceivedEcho(msg) {
3743
3898
  const payload = msg.payload;
3744
- if (!this._isEchoPayload(payload))
3899
+ const sender = String(msg.from ?? msg.sender_aid ?? msg.from_aid ?? '');
3900
+ const returned = this._appendEchoTrace(payload, 'receive', this._echoStarts.get(msg) ?? performance.now(), true);
3901
+ returned.text = String(returned.text) + '\n[AUN-Echo.return]';
3902
+ const groupId = String(msg.group_id ?? '');
3903
+ await this._rpcPipeline.call('message.send', {
3904
+ to: sender, encrypt: false, _echo_reflection: true,
3905
+ ...(groupId ? { group_id: groupId } : {}),
3906
+ payload: returned,
3907
+ });
3908
+ }
3909
+ async _echoFailure(method, params, error, traced = false) {
3910
+ if (!['message.send', 'group.send'].includes(method) || !params || !this._isEchoPayload(params.payload) || !(error instanceof AUNError) || error instanceof AuthError || error._echoDeliveryAttempted)
3911
+ return;
3912
+ const payload = params.payload;
3913
+ if (String(payload.type ?? '').toLowerCase().startsWith('e2ee') || payload.ciphertext)
3914
+ return;
3915
+ const started = this._echoStarts.get(params) ?? performance.now();
3916
+ let sent;
3917
+ try {
3918
+ sent = traced ? payload : this._appendEchoTrace(payload, 'send', started);
3919
+ }
3920
+ catch {
3921
+ return;
3922
+ }
3923
+ const reason = error instanceof TimeoutError || error.code === -32011 ? 'timeout' : 'send_failed';
3924
+ const state = error instanceof ValidationError || error instanceof ConnectionError && /not connected|closing|closed/i.test(error.message) ? 'not_delivered' : 'unknown';
3925
+ const detail = { code: error.code, message: error.message, data: error.data };
3926
+ const result = { status: 'echo_return', message_id: params.message_id ?? `m-${crypto.randomUUID()}`, from: params.to ?? params.group_id, to: this._aid, payload: sent, reason, delivery_state: state, echo_error: detail };
3927
+ if (method === 'group.send')
3928
+ result.group_id = params.group_id;
3929
+ await this._publishEchoReturn(method, params, result);
3930
+ return result;
3931
+ }
3932
+ async _publishEchoReturn(method, params, result) {
3933
+ if (!this._isEchoPayload(result.payload))
3745
3934
  return;
3746
- const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
3747
- const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
3748
- msg.payload = { ...payload, text: payload.text + '\n' + trace };
3935
+ const payload = result.payload;
3936
+ const returnedPayload = this._appendEchoTrace(payload, 'receive', this._echoStarts.get(params) ?? performance.now(), !this._isEchoReturn(payload));
3937
+ if (result.echo_error)
3938
+ returnedPayload.text += ` reason=${result.reason} delivery_state=${result.delivery_state} error=${JSON.stringify(result.echo_error)}`;
3939
+ if (!String(returnedPayload.text).split('\n').some(line => line.trim() === '[AUN-Echo.return]'))
3940
+ returnedPayload.text += '\n[AUN-Echo.return]';
3941
+ result.payload = returnedPayload;
3942
+ if (this._state === 'closed')
3943
+ return;
3944
+ const groupId = String(result.group_id ?? params.group_id ?? params.group_aid ?? '');
3945
+ const sender = String(result.from ?? result.sender_aid ?? params.to ?? this._aid ?? '');
3946
+ try {
3947
+ await this._publishAppEvent(method === 'group.send' ? 'group.message_created' : 'message.received', {
3948
+ ...result,
3949
+ message_id: String(result.message_id ?? params.message_id ?? ''),
3950
+ from: sender, sender_aid: sender, to: this._aid ?? '', encrypted: false,
3951
+ ...(groupId ? { group_id: groupId } : {}), payload: returnedPayload,
3952
+ }, 'echo_return');
3953
+ }
3954
+ catch (error) {
3955
+ if (error instanceof Error)
3956
+ error._echoDeliveryAttempted = true;
3957
+ throw error;
3958
+ }
3749
3959
  }
3750
3960
  _debugJson(value) {
3751
3961
  const seen = new WeakSet();
@@ -4637,6 +4847,10 @@ export class AUNClient {
4637
4847
  : {};
4638
4848
  const capabilities = isJsonObject(extraInfo._capabilities) ? { ...extraInfo._capabilities } : {};
4639
4849
  delete capabilities.message_classes_v1;
4850
+ if (params.udp_standby !== false && params.connection_kind !== 'short')
4851
+ capabilities.udp_standby_v1 = true;
4852
+ else
4853
+ delete capabilities.udp_standby_v1;
4640
4854
  if (typeof this._tokenStore.updateInstanceState === 'function') {
4641
4855
  capabilities.message_classes_v2 = { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control'] };
4642
4856
  capabilities.sdk_control_protocols = { 'aun.agent_md.changed': [1], 'aun.pubsub': [1] };
@@ -4659,12 +4873,21 @@ export class AUNClient {
4659
4873
  const challenge = await this._transport.connect(gatewayUrl);
4660
4874
  this._assertReconnectOwner(reconnectOwner);
4661
4875
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4662
- const authTransport = reconnectOwner ? {
4876
+ const authTransport = {
4663
4877
  call: (...args) => {
4664
4878
  this._assertReconnectOwner(reconnectOwner);
4879
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4665
4880
  return this._transport.call(...args);
4666
4881
  },
4667
- } : this._transport;
4882
+ connect: async (url) => {
4883
+ this._assertReconnectOwner(reconnectOwner);
4884
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4885
+ const nextChallenge = await this._transport.connect(url);
4886
+ this._assertReconnectOwner(reconnectOwner);
4887
+ this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4888
+ return nextChallenge;
4889
+ },
4890
+ };
4668
4891
  this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
4669
4892
  this._state = 'authenticating';
4670
4893
  if (allowReauth) {
@@ -4697,6 +4920,7 @@ export class AUNClient {
4697
4920
  this._bindGatewayRuntimeIdentity(auth.hello, params);
4698
4921
  this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
4699
4922
  this._negotiatedCapabilities = isJsonObject(auth.hello) && isJsonObject(auth.hello.negotiated_capabilities) ? auth.hello.negotiated_capabilities : {};
4923
+ this._udpStandbyPolicy = isJsonObject(auth.hello) && isJsonObject(auth.hello.udp_standby) ? auth.hello.udp_standby : undefined;
4700
4924
  if (isJsonObject(auth.hello))
4701
4925
  await this._applyRiskHello(auth.hello);
4702
4926
  }
@@ -4719,6 +4943,7 @@ export class AUNClient {
4719
4943
  this._bindGatewayRuntimeIdentity(hello, params);
4720
4944
  this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
4721
4945
  this._negotiatedCapabilities = isJsonObject(hello) && isJsonObject(hello.negotiated_capabilities) ? hello.negotiated_capabilities : {};
4946
+ this._udpStandbyPolicy = isJsonObject(hello) && isJsonObject(hello.udp_standby) ? hello.udp_standby : undefined;
4722
4947
  if (isJsonObject(hello))
4723
4948
  await this._applyRiskHello(hello);
4724
4949
  }
@@ -4789,19 +5014,16 @@ export class AUNClient {
4789
5014
  this._agentMdChanges = new AgentMdChangeCoordinator(this);
4790
5015
  // connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
4791
5016
  // 群消息按惰性触发,不在此处主动 pull
4792
- if (backgroundSyncEnabled) {
4793
- void this._fillP2pGap().catch((exc) => {
4794
- this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
4795
- });
4796
- }
4797
5017
  this._entityOwnershipConnection++;
4798
5018
  this._entityOwnershipChecks.clear();
4799
5019
  if (!this._ownershipOperation)
4800
5020
  this._safeAsync(this._checkIdentityAdmissionAfterConnect());
4801
- this._safeAsync(this._delivery.restoreMessageClassPulls());
5021
+ if (backgroundSyncEnabled)
5022
+ this._schedulePostConnectRecovery();
4802
5023
  if (!isShortConnection && backgroundSyncEnabled && this._v2Session) {
4803
5024
  this._safeAsync(this._v2AutoConfirmPendingProposals());
4804
5025
  }
5026
+ this._activateUdpStandby();
4805
5027
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
4806
5028
  }
4807
5029
  catch (err) {
@@ -5724,6 +5946,11 @@ export class AUNClient {
5724
5946
  }
5725
5947
  /** 停止所有后台任务 */
5726
5948
  _stopBackgroundTasks() {
5949
+ this._postConnectRecoveryAbort?.abort();
5950
+ this._postConnectRecoveryAbort = null;
5951
+ if (this._postConnectRecoveryTimer !== null)
5952
+ clearTimeout(this._postConnectRecoveryTimer);
5953
+ this._postConnectRecoveryTimer = null;
5727
5954
  this._syncChannelCoordinator?.reset();
5728
5955
  this._stopCredentialRenewal();
5729
5956
  this._auth.invalidateStrictPkiCache();
@@ -6118,6 +6345,8 @@ export class AUNClient {
6118
6345
  }
6119
6346
  /** 传输层断线回调 */
6120
6347
  async _handleTransportDisconnect(error, closeCode, initialOptions) {
6348
+ if (this._udpStandby?.handlesDisconnect())
6349
+ return;
6121
6350
  if (this._closing || this._disconnecting || this.state === ConnectionState.CLOSED)
6122
6351
  return;
6123
6352
  // 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
@@ -6198,6 +6427,50 @@ export class AUNClient {
6198
6427
  }
6199
6428
  this._startReconnect(serverInitiated, initialOptions, reconnectAbort);
6200
6429
  }
6430
+ _activateUdpStandby() {
6431
+ if (this._negotiatedCapabilities.udp_standby_v1 !== true || this._udpStandbyPolicy?.enabled !== true) {
6432
+ this._udpStandby?.deactivate();
6433
+ this._transport.setStandbyGate(null);
6434
+ return;
6435
+ }
6436
+ this._udpStandby ??= new UdpStandbyController({
6437
+ call: (method, params, timeout) => this._transport.call(method, params, timeout),
6438
+ canPrepare: async (timeout) => {
6439
+ if (!this._aid)
6440
+ return true;
6441
+ try {
6442
+ if (!this._aidStore) {
6443
+ const record = this._agentMdManager?._loadRecord?.(this._aid, true) ?? this._agentMdManager?.loadRecord(this._aid);
6444
+ const content = String(record?.local_draft_content ?? '') || String(record?.content ?? '');
6445
+ return !content || validateAgentMdReadDocument(content).frontmatter.type !== 'human';
6446
+ }
6447
+ const result = await this._aidStore.resolve(this._aid, { timeout });
6448
+ return !result.ok || result.data?.agent_md?.frontmatter.type !== 'human';
6449
+ }
6450
+ catch {
6451
+ return true;
6452
+ }
6453
+ },
6454
+ canStandby: () => this._state === 'ready' && !this._closing && !this._disconnecting && !this._reconnectActive
6455
+ && this._sessionOptions?.background_sync !== false
6456
+ && this._groupIdentityOperations.size === 0
6457
+ && this._transport.isConnected() && this._transport.rpcBudget.counts.total === 0
6458
+ && this._transport.foregroundWaiting === 0 && this._transport.rpcBudget.backgroundWaiting === 0
6459
+ && this._dispatcher.isIdle && !this._delivery?.hasAnyPendingPull()
6460
+ && (this._syncChannelCoordinator?.isIdle ?? true),
6461
+ prepareParams: () => ({ connection_id: this._gatewayConnectionId }),
6462
+ detach: () => this._transport.close(),
6463
+ reconnect: async () => {
6464
+ if (this._closing || this._disconnecting)
6465
+ throw new ConnectionError('client closed');
6466
+ this._state = 'standby';
6467
+ await this._lifecycle.connect(this._udpStandbyConnectOptions);
6468
+ },
6469
+ });
6470
+ this._udpStandby.activate(this._udpStandbyPolicy);
6471
+ const standby = this._udpStandby;
6472
+ this._transport.setStandbyGate((method, deadline, signal) => standby.acquire(method, deadline, signal), () => standby.touch(), params => standby.authenticationParams(params));
6473
+ }
6201
6474
  /** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
6202
6475
  _startReconnect(serverInitiated = false, initialOptions, existingAbort) {
6203
6476
  if (existingAbort) {