@agentunion/fastaun 0.5.33 → 0.5.35
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 +59 -2
- package/README.md +28 -26
- package/_packed_docs/CHANGELOG.md +59 -2
- package/_packed_docs/INDEX.md +26 -12
- package/_packed_docs/KITE_DOCS_GUIDE.md +17 -11
- package/_packed_docs/agent.md/SCHEMA.md +1 -1
- 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
- package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +58 -8
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
- package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
- package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
- package/_packed_docs/sdk/0.5.33-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +464 -316
- package/_packed_docs/sdk/0.5.33-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +499 -304
- package/_packed_docs/sdk/0.5.34-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +133 -0
- package/_packed_docs/sdk/0.5.34-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +107 -0
- 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
- 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
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -13
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +13 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +19 -9
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -20
- package/_packed_docs/sdk/09-collab-rpc-manual.md +48 -30
- package/_packed_docs/sdk/09-custody-api-manual.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +60 -13
- package/_packed_docs/sdk/09-message-rpc-manual.md +22 -0
- package/_packed_docs/sdk/09-meta-rpc-manual.md +2 -1
- package/_packed_docs/sdk/09-payload-reference.md +9 -8
- package/_packed_docs/sdk/09-proxy-rpc-manual.md +20 -14
- package/_packed_docs/sdk/09-storage-rpc-manual.md +1698 -1659
- package/_packed_docs/sdk/09-stream-rpc-manual.md +33 -20
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
- 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 +125 -30
- 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 +109 -33
- package/_packed_docs/sdk/INDEX.md +30 -18
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
- 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
- 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
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
- package/_packed_docs/sdk/README.md +11 -6
- 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
- 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
- package/dist/auth.js +39 -45
- package/dist/auth.js.map +1 -1
- package/dist/client/control-messages.js +2 -2
- package/dist/client/control-messages.js.map +1 -1
- package/dist/client/delivery.d.ts +12 -1
- package/dist/client/delivery.js +281 -46
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/lifecycle.js +13 -1
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-budget.d.ts +21 -0
- package/dist/client/rpc-budget.js +82 -0
- package/dist/client/rpc-budget.js.map +1 -0
- package/dist/client/rpc-pipeline.d.ts +5 -0
- package/dist/client/rpc-pipeline.js +227 -63
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/sync-channels.d.ts +90 -0
- package/dist/client/sync-channels.js +508 -0
- package/dist/client/sync-channels.js.map +1 -0
- package/dist/client/v2-e2ee.d.ts +5 -1
- package/dist/client/v2-e2ee.js +420 -303
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +17 -0
- package/dist/client.js +373 -50
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +1 -0
- package/dist/events.js +5 -0
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mail-client.d.ts +101 -0
- package/dist/mail-client.js +369 -0
- package/dist/mail-client.js.map +1 -0
- package/dist/pubsub/binding.js +23 -3
- package/dist/pubsub/binding.js.map +1 -1
- package/dist/pubsub/channel.d.ts +15 -3
- package/dist/pubsub/channel.js +114 -7
- package/dist/pubsub/channel.js.map +1 -1
- package/dist/pubsub/client.d.ts +17 -2
- package/dist/pubsub/client.js +205 -40
- package/dist/pubsub/client.js.map +1 -1
- package/dist/pubsub/credentials.d.ts +3 -0
- package/dist/pubsub/credentials.js +57 -20
- package/dist/pubsub/credentials.js.map +1 -1
- package/dist/pubsub/index.d.ts +5 -2
- package/dist/pubsub/index.js +70 -54
- package/dist/pubsub/index.js.map +1 -1
- package/dist/pubsub/service.d.ts +9 -0
- package/dist/pubsub/service.js +266 -20
- package/dist/pubsub/service.js.map +1 -1
- package/dist/pubsub/types.d.ts +13 -1
- package/dist/storage/lowlevel.d.ts +3 -1
- package/dist/storage/lowlevel.js +13 -0
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/types.d.ts +10 -0
- package/dist/storage/types.js.map +1 -1
- package/dist/storage/vfs.d.ts +3 -1
- package/dist/storage/vfs.js +2 -2
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +55 -5
- package/dist/tools/cross-sdk-agent.js +88 -21
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +16 -0
- package/dist/transport.js +129 -7
- package/dist/transport.js.map +1 -1
- package/dist/udp-standby.d.ts +89 -0
- package/dist/udp-standby.js +487 -0
- package/dist/udp-standby.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/scripts/patch-imapflow-external.mjs +69 -0
package/dist/client.js
CHANGED
|
@@ -26,6 +26,8 @@ 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';
|
|
30
|
+
import { SyncChannels } from './client/sync-channels.js';
|
|
29
31
|
import { AuthFlow } from './auth.js';
|
|
30
32
|
import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
|
|
31
33
|
import { AgentMdChangeCoordinator } from './agent-md-changes.js';
|
|
@@ -908,6 +910,9 @@ export class AUNClient {
|
|
|
908
910
|
_discovery;
|
|
909
911
|
/** 传输层 */
|
|
910
912
|
_transport;
|
|
913
|
+
_udpStandby = null;
|
|
914
|
+
_udpStandbyPolicy;
|
|
915
|
+
_udpStandbyConnectOptions;
|
|
911
916
|
/** 认证流程 */
|
|
912
917
|
_auth;
|
|
913
918
|
resolveCredentialService;
|
|
@@ -931,6 +936,8 @@ export class AUNClient {
|
|
|
931
936
|
_groupIdentityOperations = new Map();
|
|
932
937
|
_entityOwnershipConnection = 0;
|
|
933
938
|
_entityOwnershipChecks = new Map();
|
|
939
|
+
_postConnectRecoveryTimer = null;
|
|
940
|
+
_postConnectRecoveryAbort = null;
|
|
934
941
|
_groupIndexMetaCache = new GroupIndexMetaCache();
|
|
935
942
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
936
943
|
_p2pPeerPullEnabled = true;
|
|
@@ -1050,6 +1057,7 @@ export class AUNClient {
|
|
|
1050
1057
|
_peerDirectory;
|
|
1051
1058
|
_lifecycle;
|
|
1052
1059
|
_rpcPipeline;
|
|
1060
|
+
_syncChannelCoordinator = null;
|
|
1053
1061
|
_delivery;
|
|
1054
1062
|
_v2E2EE;
|
|
1055
1063
|
_groupState;
|
|
@@ -1161,6 +1169,7 @@ export class AUNClient {
|
|
|
1161
1169
|
this._installRiskHooks();
|
|
1162
1170
|
this._transport.setClassMessageHandler((event, payload) => this._controlMessages().onPush(payload, event.includes('group.')));
|
|
1163
1171
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1172
|
+
this._transport.setSyncControlObserver((meta, method, params) => this._historyChannels().observe(meta, method, params));
|
|
1164
1173
|
this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
1165
1174
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
1166
1175
|
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
@@ -1351,6 +1360,11 @@ export class AUNClient {
|
|
|
1351
1360
|
const client = new AUNClient(AID._create({ aid: aid.aid, aunPath: aid.aunPath, certPem: aid.certPem, privateKeyPem: aid.privateKeyPem,
|
|
1352
1361
|
certValid: aid.isCertValid(), privateKeyValid: aid.isPrivateKeyValid(), deviceId: this._deviceId, slotId,
|
|
1353
1362
|
verifySsl: aid.verifySsl, rootCaPath: aid.rootCaPath, debug: aid.debug, aidStore: aid._aidStore }));
|
|
1363
|
+
const identity = this._auth.loadIdentityOrNone(), token = String(identity?.access_token ?? ''), expiresAt = identity?.access_token_expires_at;
|
|
1364
|
+
if (identity?.aid === aid.aid && token.startsWith('eyJ') && token.split('.').length === 3
|
|
1365
|
+
&& typeof expiresAt === 'number' && expiresAt > Date.now() / 1000 + 30) {
|
|
1366
|
+
Object.assign(client._identity, { access_token: token, access_token_expires_at: expiresAt });
|
|
1367
|
+
}
|
|
1354
1368
|
client._ownershipOperation = true;
|
|
1355
1369
|
client._ownershipType = this.getType();
|
|
1356
1370
|
return client;
|
|
@@ -1590,6 +1604,10 @@ export class AUNClient {
|
|
|
1590
1604
|
}
|
|
1591
1605
|
_applyAidRuntimeContext(aid) {
|
|
1592
1606
|
this._aidStore = aid._aidStore;
|
|
1607
|
+
this._udpStandby?.close();
|
|
1608
|
+
this._udpStandby = null;
|
|
1609
|
+
this._udpStandbyPolicy = undefined;
|
|
1610
|
+
this._udpStandbyConnectOptions = undefined;
|
|
1593
1611
|
const oldTransport = this._transport;
|
|
1594
1612
|
try {
|
|
1595
1613
|
const closeResult = oldTransport?.close?.();
|
|
@@ -1693,6 +1711,7 @@ export class AUNClient {
|
|
|
1693
1711
|
this._installRiskHooks();
|
|
1694
1712
|
this._transport.setClassMessageHandler((event, payload) => this._controlMessages().onPush(payload, event.includes('group.')));
|
|
1695
1713
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1714
|
+
this._transport.setSyncControlObserver((meta, method, params) => this._historyChannels().observe(meta, method, params));
|
|
1696
1715
|
this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
1697
1716
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
1698
1717
|
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
@@ -1748,6 +1767,56 @@ export class AUNClient {
|
|
|
1748
1767
|
return this._peerDirectory.peers();
|
|
1749
1768
|
}
|
|
1750
1769
|
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
1770
|
+
_historyChannels() {
|
|
1771
|
+
return this._syncChannelCoordinator ??= new SyncChannels({
|
|
1772
|
+
main: () => this._transport,
|
|
1773
|
+
current: () => this._state === 'ready' && !this._closing && this._transport.isConnected(),
|
|
1774
|
+
wake: () => this._delivery?.requestBackgroundPullRecovery(),
|
|
1775
|
+
owner: () => String(this._aid ?? ''),
|
|
1776
|
+
namespace: (method, params) => this._rpcPipeline.pullGateKeyForCall(method, params).split('|')[0],
|
|
1777
|
+
pendingNamespaces: () => [...this._delivery.pendingHistoryNamespaces(), ...this._rpcPipeline.pendingHistoryNamespaces()],
|
|
1778
|
+
fail: (method, params, error) => this._dispatcher.enqueue('sync.error', { namespace: params.sender_aid ? this._delivery.peerNamespace(String(params.sender_aid)) : `group:${String(params.group_id ?? params.group_aid ?? '')}`, method, code: 'OVERSIZED_MESSAGE', error: error instanceof Error ? error.message : String(error) }),
|
|
1779
|
+
create: async (parent, timeout = 35000, signal) => {
|
|
1780
|
+
const deadline = Date.now() + timeout;
|
|
1781
|
+
const dispatcher = new EventDispatcher(this._logger.for('aun_core.events'));
|
|
1782
|
+
const transport = new RPCTransport({ eventDispatcher: dispatcher, rpcBudget: this._transport.rpcBudget, syncAuxiliary: true,
|
|
1783
|
+
timeout, verifySsl: this._configModel.verifySsl, logger: this._logger.for('aun_core.transport') });
|
|
1784
|
+
transport.setSyncControlObserver((meta, method, params) => this._historyChannels().observe(meta, method, params));
|
|
1785
|
+
transport.setRequestAdmission(this._transport._requestAdmission);
|
|
1786
|
+
transport.setRequestCheck(this._transport._requestCheck);
|
|
1787
|
+
transport.setProtocolDisconnectCallback(() => this._historyChannels().auxiliaryDisconnected(transport));
|
|
1788
|
+
const abort = () => { void transport.close(); };
|
|
1789
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
1790
|
+
try {
|
|
1791
|
+
if (signal?.aborted)
|
|
1792
|
+
throw new TimeoutError('history sync cancelled', { retryable: true });
|
|
1793
|
+
const challenge = await transport.connect(this._gatewayUrl);
|
|
1794
|
+
if (Date.now() >= deadline || signal?.aborted)
|
|
1795
|
+
throw new TimeoutError('history sync deadline exceeded', { retryable: true });
|
|
1796
|
+
transport.setTimeout(Math.max(1, deadline - Date.now()));
|
|
1797
|
+
const token = AuthFlow._getCachedAccessToken(this._identity ?? {})
|
|
1798
|
+
|| AuthFlow._getCachedAccessToken(this._sessionParams ?? {});
|
|
1799
|
+
if (!token)
|
|
1800
|
+
throw new ConnectionError('auxiliary sync connection has no valid access token');
|
|
1801
|
+
const hello = await this._auth.initializeWithToken(transport, challenge, token, { deviceId: this._deviceId,
|
|
1802
|
+
slotId: this._slotId, connectionKind: 'short', shortTtlMs: 0, extraInfo: { _sync_parent: parent } });
|
|
1803
|
+
if (!isJsonObject(hello.connection) || !hello.connection.id)
|
|
1804
|
+
throw new ConnectionError('auxiliary sync connection has no identity');
|
|
1805
|
+
if (!isJsonObject(hello.negotiated_capabilities) || hello.negotiated_capabilities.elastic_connections_v1 !== true || hello.negotiated_capabilities.sync_channels_v1 !== true)
|
|
1806
|
+
throw new ConnectionError('auxiliary sync capabilities were not negotiated');
|
|
1807
|
+
return { id: String(hello.connection.id), transport, dispose: () => dispatcher.close() };
|
|
1808
|
+
}
|
|
1809
|
+
catch (error) {
|
|
1810
|
+
await transport.close();
|
|
1811
|
+
await dispatcher.close();
|
|
1812
|
+
throw error;
|
|
1813
|
+
}
|
|
1814
|
+
finally {
|
|
1815
|
+
signal?.removeEventListener('abort', abort);
|
|
1816
|
+
}
|
|
1817
|
+
},
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1751
1820
|
_observeRpcMeta(meta) {
|
|
1752
1821
|
this._agentMdManager.observeRpcMeta(meta, this._aid);
|
|
1753
1822
|
this._groupIndexMetaCache.observeRpcMeta(meta, { localAid: this._aid ?? '' });
|
|
@@ -1791,10 +1860,14 @@ export class AUNClient {
|
|
|
1791
1860
|
}
|
|
1792
1861
|
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1793
1862
|
async connect(opts) {
|
|
1863
|
+
if (this._udpStandby && ['standby', 'preparing', 'waking'].includes(this._udpStandby.state))
|
|
1864
|
+
return this._udpStandby.ensureActive();
|
|
1865
|
+
this._udpStandbyConnectOptions = opts ? { ...opts } : undefined;
|
|
1794
1866
|
await this._lifecycle.connect(opts);
|
|
1795
1867
|
}
|
|
1796
1868
|
/** 关闭连接 */
|
|
1797
1869
|
async close() {
|
|
1870
|
+
this._udpStandby?.close();
|
|
1798
1871
|
this._risk.close();
|
|
1799
1872
|
this._peerGatewayCache.clear();
|
|
1800
1873
|
return this._lifecycle.close();
|
|
@@ -1804,6 +1877,7 @@ export class AUNClient {
|
|
|
1804
1877
|
* disconnect 是可恢复的:停止心跳、关闭 WebSocket,但不清理 keystore 等状态。
|
|
1805
1878
|
*/
|
|
1806
1879
|
async disconnect() {
|
|
1880
|
+
this._udpStandby?.close();
|
|
1807
1881
|
return this._lifecycle.disconnect();
|
|
1808
1882
|
}
|
|
1809
1883
|
get riskState() { return Object.freeze(structuredClone(this._risk.state)); }
|
|
@@ -1961,27 +2035,47 @@ export class AUNClient {
|
|
|
1961
2035
|
* 自动处理内部方法限制、E2EE 加解密、客户端签名等。
|
|
1962
2036
|
*/
|
|
1963
2037
|
async call(method, params, options) {
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2038
|
+
const echoStarted = performance.now();
|
|
2039
|
+
if ((method === 'message.send' || method === 'group.send') && params && this._isEchoPayload(params.payload ?? params.content)) {
|
|
2040
|
+
params = { ...params };
|
|
2041
|
+
this._echoStarts.set(params, echoStarted);
|
|
2042
|
+
}
|
|
2043
|
+
const lifecycle = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method);
|
|
2044
|
+
let standbyRelease;
|
|
2045
|
+
if (lifecycle && this._udpStandby) {
|
|
2046
|
+
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);
|
|
2047
|
+
standbyRelease = await this._udpStandby.acquire(method, deadline);
|
|
2048
|
+
options = { ...options, deadline };
|
|
1975
2049
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2050
|
+
try {
|
|
2051
|
+
const assertCurrent = lifecycle ? this._entityOwnershipGuard() : undefined;
|
|
2052
|
+
const recoverOwnership = lifecycle ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
|
|
2053
|
+
await this._checkEntityOwnershipOnAccess(method, params ?? {});
|
|
2054
|
+
const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
|
|
2055
|
+
? this._ensureReadStateIdentity() : null;
|
|
2056
|
+
try {
|
|
2057
|
+
const result = await (options === undefined ? this._rpcPipeline.call(method, params, assertCurrent) : this._rpcPipeline.call(method, params, assertCurrent, options));
|
|
2058
|
+
await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
|
|
2059
|
+
if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
|
|
2060
|
+
this._observeReadStateResult(method, result, params ?? {});
|
|
2061
|
+
return result;
|
|
2062
|
+
}
|
|
2063
|
+
catch (err) {
|
|
2064
|
+
if (recoverOwnership) {
|
|
2065
|
+
try {
|
|
2066
|
+
await recoverOwnership(err);
|
|
2067
|
+
}
|
|
2068
|
+
catch { }
|
|
1980
2069
|
}
|
|
1981
|
-
|
|
2070
|
+
await this._maybeHandleHalfOpenRpcFailure(err);
|
|
2071
|
+
const returned = await this._echoFailure(method, params, err);
|
|
2072
|
+
if (returned)
|
|
2073
|
+
return returned;
|
|
2074
|
+
throw err;
|
|
1982
2075
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
2076
|
+
}
|
|
2077
|
+
finally {
|
|
2078
|
+
standbyRelease?.();
|
|
1985
2079
|
}
|
|
1986
2080
|
}
|
|
1987
2081
|
_entityOwnershipGuard() {
|
|
@@ -2081,7 +2175,7 @@ export class AUNClient {
|
|
|
2081
2175
|
return await existing;
|
|
2082
2176
|
this._entityOwnershipChecks.set(`${identity}:${aid}`, task);
|
|
2083
2177
|
}
|
|
2084
|
-
const consensus = await this.checkOwnershipConsensus(aid);
|
|
2178
|
+
const consensus = await this.checkOwnershipConsensus(aid, true);
|
|
2085
2179
|
if (consensus.status === 'consensus' && consensus.owner_aid === this._aid)
|
|
2086
2180
|
return true;
|
|
2087
2181
|
await this._completeEntityOwnership(aid, type, undefined, () => {
|
|
@@ -2209,9 +2303,27 @@ export class AUNClient {
|
|
|
2209
2303
|
const groupId = String(params.group_aid ?? params.groupAid ?? params.group_id ?? params.groupId ?? message.slice(prefix.length)).trim();
|
|
2210
2304
|
const target = normalizeGroupId(groupId.toLowerCase(), { localIssuer: this._aid?.split('.').slice(1).join('.') });
|
|
2211
2305
|
const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2306
|
+
const assertCurrent = this._entityOwnershipGuard();
|
|
2307
|
+
try {
|
|
2308
|
+
assertCurrent();
|
|
2309
|
+
const pending = this._groupIdentityOperations.get(key);
|
|
2310
|
+
if (pending && !['group.create', 'group.bind_group_aid'].includes(_method)) {
|
|
2311
|
+
const local = this._aidStore?.load(target);
|
|
2312
|
+
if (!local?.ok || !local.data.aid.isPrivateKeyValid()) {
|
|
2313
|
+
await pending.task;
|
|
2314
|
+
assertCurrent();
|
|
2315
|
+
return true;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
if (!this._entityOwnershipChecks.has(key))
|
|
2319
|
+
await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
|
|
2320
|
+
const recovered = await this._entityOwnershipChecks.get(key) ?? false;
|
|
2321
|
+
assertCurrent();
|
|
2322
|
+
return recovered;
|
|
2323
|
+
}
|
|
2324
|
+
catch {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2215
2327
|
}
|
|
2216
2328
|
}
|
|
2217
2329
|
const agentPrefix = 'valid agent.md is required for ';
|
|
@@ -2260,17 +2372,14 @@ export class AUNClient {
|
|
|
2260
2372
|
catch (exc) {
|
|
2261
2373
|
this._clientLog.warn(`post-connect agent.md check failed: ${formatCaughtError(exc)}`);
|
|
2262
2374
|
}
|
|
2263
|
-
const store = this._aidStore;
|
|
2264
|
-
if (!store)
|
|
2265
|
-
return;
|
|
2266
|
-
await this._resumeOrgIdentityAfterConnect(store);
|
|
2267
2375
|
}
|
|
2268
2376
|
async _runGroupIdentityOperation(groupId, operation, context = '') {
|
|
2269
2377
|
const aid = String(this._aid ?? '').trim();
|
|
2270
2378
|
const dot = aid.indexOf('.');
|
|
2271
|
-
const
|
|
2272
|
-
if (!
|
|
2379
|
+
const target = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
|
|
2380
|
+
if (!target)
|
|
2273
2381
|
return operation();
|
|
2382
|
+
const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
|
|
2274
2383
|
const pending = this._groupIdentityOperations.get(key);
|
|
2275
2384
|
if (pending && pending.context !== context)
|
|
2276
2385
|
throw ownershipFailure('conflict', 'pending Org creation context changed');
|
|
@@ -2354,12 +2463,42 @@ export class AUNClient {
|
|
|
2354
2463
|
tokens.saveMetadata(orgAid, { org_identity_creation: null });
|
|
2355
2464
|
return checkpoint.result;
|
|
2356
2465
|
}
|
|
2357
|
-
|
|
2466
|
+
_schedulePostConnectRecovery() {
|
|
2467
|
+
this._postConnectRecoveryAbort?.abort();
|
|
2468
|
+
if (this._postConnectRecoveryTimer !== null)
|
|
2469
|
+
clearTimeout(this._postConnectRecoveryTimer);
|
|
2470
|
+
this._postConnectRecoveryTimer = null;
|
|
2471
|
+
const store = this._aidStore;
|
|
2472
|
+
if (this._sessionOptions.background_sync === false)
|
|
2473
|
+
return;
|
|
2474
|
+
const abort = new AbortController();
|
|
2475
|
+
this._postConnectRecoveryAbort = abort;
|
|
2476
|
+
const aid = this._aid, version = this._serviceEventIdentityVersion, connection = this._entityOwnershipConnection;
|
|
2477
|
+
const current = () => !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && connection === this._entityOwnershipConnection && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
|
|
2478
|
+
this._postConnectRecoveryTimer = setTimeout(() => {
|
|
2479
|
+
this._postConnectRecoveryTimer = null;
|
|
2480
|
+
if (!current())
|
|
2481
|
+
return;
|
|
2482
|
+
this._safeAsync(this._delivery.restoreMessageClassPulls());
|
|
2483
|
+
void this._fillP2pGap().catch((exc) => {
|
|
2484
|
+
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
2485
|
+
});
|
|
2486
|
+
if (store && !this._ownershipOperation)
|
|
2487
|
+
this._safeAsync(this._resumeOrgIdentityAfterConnect(store, current));
|
|
2488
|
+
}, 30_000 + Math.random() * 30_000);
|
|
2489
|
+
this._postConnectRecoveryTimer.unref?.();
|
|
2490
|
+
}
|
|
2491
|
+
async _resumeOrgIdentityAfterConnect(store, current = () => true) {
|
|
2492
|
+
if (!current())
|
|
2493
|
+
return;
|
|
2358
2494
|
const keyStore = store._keystore;
|
|
2359
2495
|
const tokens = store._tokenStore;
|
|
2360
2496
|
if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
|
|
2361
2497
|
return;
|
|
2362
2498
|
for (const aid of keyStore.listIdentities()) {
|
|
2499
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2500
|
+
if (!current())
|
|
2501
|
+
return;
|
|
2363
2502
|
const metadata = typeof tokens._loadMetadataStrict === 'function' ? tokens._loadMetadataStrict(aid) : tokens.loadMetadata(aid);
|
|
2364
2503
|
const creation = metadata?.org_identity_creation;
|
|
2365
2504
|
const transfer = metadata?.org_identity_transfer;
|
|
@@ -3534,6 +3673,8 @@ export class AUNClient {
|
|
|
3534
3673
|
// ── 事件自动解密管线 ──────────────────────────────────────
|
|
3535
3674
|
/** 处理 transport 层推送的原始 P2P 消息 */
|
|
3536
3675
|
async _onRawMessageReceived(data) {
|
|
3676
|
+
if (isJsonObject(data))
|
|
3677
|
+
this._echoStarts.set(data, performance.now());
|
|
3537
3678
|
if (this._controlMessages().onPush(data))
|
|
3538
3679
|
return;
|
|
3539
3680
|
return this._delivery.onRawMessageReceived(data);
|
|
@@ -3543,6 +3684,8 @@ export class AUNClient {
|
|
|
3543
3684
|
}
|
|
3544
3685
|
/** 处理群组消息推送:自动解密后 re-publish */
|
|
3545
3686
|
async _onRawGroupMessageCreated(data) {
|
|
3687
|
+
if (isJsonObject(data))
|
|
3688
|
+
this._echoStarts.set(data, performance.now());
|
|
3546
3689
|
if (this._controlMessages().onPush(data, true))
|
|
3547
3690
|
return;
|
|
3548
3691
|
return this._delivery.onRawGroupMessageCreated(data);
|
|
@@ -3657,21 +3800,70 @@ export class AUNClient {
|
|
|
3657
3800
|
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
3658
3801
|
return `${hh}:${mm}:${ss}.${ms}`;
|
|
3659
3802
|
}
|
|
3803
|
+
_echoStarts = new WeakMap();
|
|
3660
3804
|
_isEchoPayload(payload) {
|
|
3661
3805
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
3662
3806
|
return false;
|
|
3663
3807
|
const text = payload.text;
|
|
3664
|
-
if (typeof text !== 'string'
|
|
3808
|
+
if (typeof text !== 'string')
|
|
3665
3809
|
return false;
|
|
3666
|
-
|
|
3810
|
+
const first = text.split('\n', 1)[0].trimStart();
|
|
3811
|
+
return /^(?:echo\.(?:ecdiag|ask)(?:\s|$)|echo(?:\s|\[|$))/i.test(first);
|
|
3812
|
+
}
|
|
3813
|
+
_echoHop(payload) {
|
|
3814
|
+
if (Number.isInteger(payload.hop)) {
|
|
3815
|
+
if (!Number.isSafeInteger(payload.hop))
|
|
3816
|
+
throw new ValidationError('echo hop must be a JSON safe integer');
|
|
3817
|
+
return Number(payload.hop);
|
|
3818
|
+
}
|
|
3819
|
+
const limit = /(?:^|\s)hop=([+-]?\d+)(?=\s|$)/i.exec(payload.text.split('\n', 1)[0]);
|
|
3820
|
+
const hop = limit ? Number(limit[1]) : 100;
|
|
3821
|
+
if (!Number.isSafeInteger(hop))
|
|
3822
|
+
throw new ValidationError('echo hop must be a JSON safe integer');
|
|
3823
|
+
return hop;
|
|
3824
|
+
}
|
|
3825
|
+
_isEchoReturn(payload) {
|
|
3826
|
+
return this._echoHop(payload) < 0 || payload.dir === 'ret' || payload.dir !== 'fwd' && payload.text.split('\n').some(line => line.trim() === '[AUN-Echo.return]');
|
|
3827
|
+
}
|
|
3828
|
+
_appendEchoTrace(payload, stage, started, endpoint = false) {
|
|
3829
|
+
const previousHop = this._echoHop(payload);
|
|
3830
|
+
const returning = endpoint || this._isEchoReturn(payload);
|
|
3831
|
+
const hop = returning ? previousHop - 1 : Math.max(previousHop - 1, 0);
|
|
3832
|
+
if (!Number.isSafeInteger(hop))
|
|
3833
|
+
throw new ValidationError('echo hop decrement exceeds the JSON safe integer range');
|
|
3834
|
+
const now = new Date();
|
|
3835
|
+
const offset = -now.getTimezoneOffset();
|
|
3836
|
+
const tz = `${offset < 0 ? '-' : '+'}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0')}:${String(Math.abs(offset) % 60).padStart(2, '0')}`;
|
|
3837
|
+
const uptime = this._connectedAt ? Math.floor((now.getTime() - this._connectedAt) / 1000) : 0;
|
|
3838
|
+
const duration = Math.max(0, Math.round((performance.now() - started) * 1000) / 1000);
|
|
3839
|
+
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`;
|
|
3840
|
+
const result = { ...payload, type: 'text', hop, dir: returning ? 'ret' : 'fwd', text: payload.text.trimStart() + '\n' + trace };
|
|
3841
|
+
delete result.max_hop;
|
|
3842
|
+
delete result.rhop;
|
|
3843
|
+
return result;
|
|
3667
3844
|
}
|
|
3668
3845
|
_maybeAppendEchoTraceSend(params) {
|
|
3846
|
+
const reflected = params._echo_reflection === true;
|
|
3847
|
+
delete params._echo_reflection;
|
|
3848
|
+
for (const key of ['hop', 'max_hop', 'rhop', 'dir'])
|
|
3849
|
+
delete params[key];
|
|
3669
3850
|
const payload = params.payload;
|
|
3670
|
-
if (!this._isEchoPayload(payload))
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3851
|
+
if (!this._isEchoPayload(payload)) {
|
|
3852
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
3853
|
+
const clean = { ...payload };
|
|
3854
|
+
for (const key of ['hop', 'max_hop', 'rhop', 'dir'])
|
|
3855
|
+
delete clean[key];
|
|
3856
|
+
params.payload = clean;
|
|
3857
|
+
}
|
|
3858
|
+
return false;
|
|
3859
|
+
}
|
|
3860
|
+
if (reflected)
|
|
3861
|
+
return false;
|
|
3862
|
+
const started = this._echoStarts.get(params) ?? performance.now();
|
|
3863
|
+
this._echoStarts.set(params, started);
|
|
3864
|
+
const next = this._appendEchoTrace(payload, 'send', started);
|
|
3865
|
+
params.payload = next;
|
|
3866
|
+
return next.dir === 'fwd' && next.hop === 0;
|
|
3675
3867
|
}
|
|
3676
3868
|
_shouldSkipEventSignature(event) {
|
|
3677
3869
|
if (event.encrypted || event.encrypt)
|
|
@@ -3679,14 +3871,79 @@ export class AUNClient {
|
|
|
3679
3871
|
return this._isEchoPayload(event.payload);
|
|
3680
3872
|
}
|
|
3681
3873
|
_maybeAppendEchoTraceReceive(msg) {
|
|
3682
|
-
if (msg.encrypted)
|
|
3683
|
-
return;
|
|
3874
|
+
if (msg.encrypted || msg.encrypt)
|
|
3875
|
+
return false;
|
|
3684
3876
|
const payload = msg.payload;
|
|
3685
|
-
if (!this._isEchoPayload(payload))
|
|
3877
|
+
if (!this._isEchoPayload(payload) || String(payload.type ?? '').startsWith('e2ee.') || payload.ciphertext)
|
|
3878
|
+
return false;
|
|
3879
|
+
const started = this._echoStarts.get(msg) ?? performance.now();
|
|
3880
|
+
this._echoStarts.set(msg, started);
|
|
3881
|
+
const next = this._appendEchoTrace(payload, 'receive', started);
|
|
3882
|
+
msg.payload = next;
|
|
3883
|
+
return next.dir === 'fwd' && next.hop === 0;
|
|
3884
|
+
}
|
|
3885
|
+
async _returnReceivedEcho(msg) {
|
|
3886
|
+
const payload = msg.payload;
|
|
3887
|
+
const sender = String(msg.from ?? msg.sender_aid ?? msg.from_aid ?? '');
|
|
3888
|
+
const returned = this._appendEchoTrace(payload, 'receive', this._echoStarts.get(msg) ?? performance.now(), true);
|
|
3889
|
+
returned.text = String(returned.text) + '\n[AUN-Echo.return]';
|
|
3890
|
+
const groupId = String(msg.group_id ?? '');
|
|
3891
|
+
await this._rpcPipeline.call('message.send', {
|
|
3892
|
+
to: sender, encrypt: false, _echo_reflection: true,
|
|
3893
|
+
...(groupId ? { group_id: groupId } : {}),
|
|
3894
|
+
payload: returned,
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
async _echoFailure(method, params, error, traced = false) {
|
|
3898
|
+
if (!['message.send', 'group.send'].includes(method) || !params || !this._isEchoPayload(params.payload) || !(error instanceof AUNError) || error instanceof AuthError || error._echoDeliveryAttempted)
|
|
3899
|
+
return;
|
|
3900
|
+
const payload = params.payload;
|
|
3901
|
+
if (String(payload.type ?? '').toLowerCase().startsWith('e2ee') || payload.ciphertext)
|
|
3686
3902
|
return;
|
|
3687
|
-
const
|
|
3688
|
-
|
|
3689
|
-
|
|
3903
|
+
const started = this._echoStarts.get(params) ?? performance.now();
|
|
3904
|
+
let sent;
|
|
3905
|
+
try {
|
|
3906
|
+
sent = traced ? payload : this._appendEchoTrace(payload, 'send', started);
|
|
3907
|
+
}
|
|
3908
|
+
catch {
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
const reason = error instanceof TimeoutError || error.code === -32011 ? 'timeout' : 'send_failed';
|
|
3912
|
+
const state = error instanceof ValidationError || error instanceof ConnectionError && /not connected|closing|closed/i.test(error.message) ? 'not_delivered' : 'unknown';
|
|
3913
|
+
const detail = { code: error.code, message: error.message, data: error.data };
|
|
3914
|
+
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 };
|
|
3915
|
+
if (method === 'group.send')
|
|
3916
|
+
result.group_id = params.group_id;
|
|
3917
|
+
await this._publishEchoReturn(method, params, result);
|
|
3918
|
+
return result;
|
|
3919
|
+
}
|
|
3920
|
+
async _publishEchoReturn(method, params, result) {
|
|
3921
|
+
if (!this._isEchoPayload(result.payload))
|
|
3922
|
+
return;
|
|
3923
|
+
const payload = result.payload;
|
|
3924
|
+
const returnedPayload = this._appendEchoTrace(payload, 'receive', this._echoStarts.get(params) ?? performance.now(), !this._isEchoReturn(payload));
|
|
3925
|
+
if (result.echo_error)
|
|
3926
|
+
returnedPayload.text += ` reason=${result.reason} delivery_state=${result.delivery_state} error=${JSON.stringify(result.echo_error)}`;
|
|
3927
|
+
if (!String(returnedPayload.text).split('\n').some(line => line.trim() === '[AUN-Echo.return]'))
|
|
3928
|
+
returnedPayload.text += '\n[AUN-Echo.return]';
|
|
3929
|
+
result.payload = returnedPayload;
|
|
3930
|
+
if (this._state === 'closed')
|
|
3931
|
+
return;
|
|
3932
|
+
const groupId = String(result.group_id ?? params.group_id ?? params.group_aid ?? '');
|
|
3933
|
+
const sender = String(result.from ?? result.sender_aid ?? params.to ?? this._aid ?? '');
|
|
3934
|
+
try {
|
|
3935
|
+
await this._publishAppEvent(method === 'group.send' ? 'group.message_created' : 'message.received', {
|
|
3936
|
+
...result,
|
|
3937
|
+
message_id: String(result.message_id ?? params.message_id ?? ''),
|
|
3938
|
+
from: sender, sender_aid: sender, to: this._aid ?? '', encrypted: false,
|
|
3939
|
+
...(groupId ? { group_id: groupId } : {}), payload: returnedPayload,
|
|
3940
|
+
}, 'echo_return');
|
|
3941
|
+
}
|
|
3942
|
+
catch (error) {
|
|
3943
|
+
if (error instanceof Error)
|
|
3944
|
+
error._echoDeliveryAttempted = true;
|
|
3945
|
+
throw error;
|
|
3946
|
+
}
|
|
3690
3947
|
}
|
|
3691
3948
|
_debugJson(value) {
|
|
3692
3949
|
const seen = new WeakSet();
|
|
@@ -4560,6 +4817,7 @@ export class AUNClient {
|
|
|
4560
4817
|
// ── 内部:连接 ────────────────────────────────────────────
|
|
4561
4818
|
/** 执行一次连接流程 */
|
|
4562
4819
|
async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
|
|
4820
|
+
this._syncChannelCoordinator?.reset();
|
|
4563
4821
|
await this._restoreRiskState();
|
|
4564
4822
|
this._assertRiskReconnect();
|
|
4565
4823
|
this._riskNegotiated = false;
|
|
@@ -4577,6 +4835,10 @@ export class AUNClient {
|
|
|
4577
4835
|
: {};
|
|
4578
4836
|
const capabilities = isJsonObject(extraInfo._capabilities) ? { ...extraInfo._capabilities } : {};
|
|
4579
4837
|
delete capabilities.message_classes_v1;
|
|
4838
|
+
if (params.udp_standby !== false && params.connection_kind !== 'short')
|
|
4839
|
+
capabilities.udp_standby_v1 = true;
|
|
4840
|
+
else
|
|
4841
|
+
delete capabilities.udp_standby_v1;
|
|
4580
4842
|
if (typeof this._tokenStore.updateInstanceState === 'function') {
|
|
4581
4843
|
capabilities.message_classes_v2 = { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control'] };
|
|
4582
4844
|
capabilities.sdk_control_protocols = { 'aun.agent_md.changed': [1], 'aun.pubsub': [1] };
|
|
@@ -4599,12 +4861,21 @@ export class AUNClient {
|
|
|
4599
4861
|
const challenge = await this._transport.connect(gatewayUrl);
|
|
4600
4862
|
this._assertReconnectOwner(reconnectOwner);
|
|
4601
4863
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
4602
|
-
const authTransport =
|
|
4864
|
+
const authTransport = {
|
|
4603
4865
|
call: (...args) => {
|
|
4604
4866
|
this._assertReconnectOwner(reconnectOwner);
|
|
4867
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
4605
4868
|
return this._transport.call(...args);
|
|
4606
4869
|
},
|
|
4607
|
-
|
|
4870
|
+
connect: async (url) => {
|
|
4871
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
4872
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
4873
|
+
const nextChallenge = await this._transport.connect(url);
|
|
4874
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
4875
|
+
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
4876
|
+
return nextChallenge;
|
|
4877
|
+
},
|
|
4878
|
+
};
|
|
4608
4879
|
this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
|
|
4609
4880
|
this._state = 'authenticating';
|
|
4610
4881
|
if (allowReauth) {
|
|
@@ -4637,6 +4908,7 @@ export class AUNClient {
|
|
|
4637
4908
|
this._bindGatewayRuntimeIdentity(auth.hello, params);
|
|
4638
4909
|
this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
|
|
4639
4910
|
this._negotiatedCapabilities = isJsonObject(auth.hello) && isJsonObject(auth.hello.negotiated_capabilities) ? auth.hello.negotiated_capabilities : {};
|
|
4911
|
+
this._udpStandbyPolicy = isJsonObject(auth.hello) && isJsonObject(auth.hello.udp_standby) ? auth.hello.udp_standby : undefined;
|
|
4640
4912
|
if (isJsonObject(auth.hello))
|
|
4641
4913
|
await this._applyRiskHello(auth.hello);
|
|
4642
4914
|
}
|
|
@@ -4659,6 +4931,7 @@ export class AUNClient {
|
|
|
4659
4931
|
this._bindGatewayRuntimeIdentity(hello, params);
|
|
4660
4932
|
this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
|
|
4661
4933
|
this._negotiatedCapabilities = isJsonObject(hello) && isJsonObject(hello.negotiated_capabilities) ? hello.negotiated_capabilities : {};
|
|
4934
|
+
this._udpStandbyPolicy = isJsonObject(hello) && isJsonObject(hello.udp_standby) ? hello.udp_standby : undefined;
|
|
4662
4935
|
if (isJsonObject(hello))
|
|
4663
4936
|
await this._applyRiskHello(hello);
|
|
4664
4937
|
}
|
|
@@ -4671,6 +4944,7 @@ export class AUNClient {
|
|
|
4671
4944
|
this._migrationPending = false;
|
|
4672
4945
|
this._lastDisconnectInfo = null;
|
|
4673
4946
|
this._state = 'ready';
|
|
4947
|
+
this._historyChannels().bind(this._gatewayConnectionId, this._negotiatedCapabilities.elastic_connections_v1 === true && this._negotiatedCapabilities.sync_channels_v1 === true && params.connection_kind !== 'short');
|
|
4674
4948
|
this._connectedAt = Date.now();
|
|
4675
4949
|
if (Array.isArray(params.gateways)) {
|
|
4676
4950
|
this._gatewayCandidates = [...new Set(params.gateways
|
|
@@ -4728,19 +5002,16 @@ export class AUNClient {
|
|
|
4728
5002
|
this._agentMdChanges = new AgentMdChangeCoordinator(this);
|
|
4729
5003
|
// connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
|
|
4730
5004
|
// 群消息按惰性触发,不在此处主动 pull
|
|
4731
|
-
if (backgroundSyncEnabled) {
|
|
4732
|
-
void this._fillP2pGap().catch((exc) => {
|
|
4733
|
-
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
4734
|
-
});
|
|
4735
|
-
}
|
|
4736
5005
|
this._entityOwnershipConnection++;
|
|
4737
5006
|
this._entityOwnershipChecks.clear();
|
|
4738
5007
|
if (!this._ownershipOperation)
|
|
4739
5008
|
this._safeAsync(this._checkIdentityAdmissionAfterConnect());
|
|
4740
|
-
|
|
5009
|
+
if (backgroundSyncEnabled)
|
|
5010
|
+
this._schedulePostConnectRecovery();
|
|
4741
5011
|
if (!isShortConnection && backgroundSyncEnabled && this._v2Session) {
|
|
4742
5012
|
this._safeAsync(this._v2AutoConfirmPendingProposals());
|
|
4743
5013
|
}
|
|
5014
|
+
this._activateUdpStandby();
|
|
4744
5015
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
4745
5016
|
}
|
|
4746
5017
|
catch (err) {
|
|
@@ -5663,6 +5934,12 @@ export class AUNClient {
|
|
|
5663
5934
|
}
|
|
5664
5935
|
/** 停止所有后台任务 */
|
|
5665
5936
|
_stopBackgroundTasks() {
|
|
5937
|
+
this._postConnectRecoveryAbort?.abort();
|
|
5938
|
+
this._postConnectRecoveryAbort = null;
|
|
5939
|
+
if (this._postConnectRecoveryTimer !== null)
|
|
5940
|
+
clearTimeout(this._postConnectRecoveryTimer);
|
|
5941
|
+
this._postConnectRecoveryTimer = null;
|
|
5942
|
+
this._syncChannelCoordinator?.reset();
|
|
5666
5943
|
this._stopCredentialRenewal();
|
|
5667
5944
|
this._auth.invalidateStrictPkiCache();
|
|
5668
5945
|
this._certCache.clear();
|
|
@@ -6056,6 +6333,8 @@ export class AUNClient {
|
|
|
6056
6333
|
}
|
|
6057
6334
|
/** 传输层断线回调 */
|
|
6058
6335
|
async _handleTransportDisconnect(error, closeCode, initialOptions) {
|
|
6336
|
+
if (this._udpStandby?.handlesDisconnect())
|
|
6337
|
+
return;
|
|
6059
6338
|
if (this._closing || this._disconnecting || this.state === ConnectionState.CLOSED)
|
|
6060
6339
|
return;
|
|
6061
6340
|
// 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
|
|
@@ -6136,6 +6415,50 @@ export class AUNClient {
|
|
|
6136
6415
|
}
|
|
6137
6416
|
this._startReconnect(serverInitiated, initialOptions, reconnectAbort);
|
|
6138
6417
|
}
|
|
6418
|
+
_activateUdpStandby() {
|
|
6419
|
+
if (this._negotiatedCapabilities.udp_standby_v1 !== true || this._udpStandbyPolicy?.enabled !== true) {
|
|
6420
|
+
this._udpStandby?.deactivate();
|
|
6421
|
+
this._transport.setStandbyGate(null);
|
|
6422
|
+
return;
|
|
6423
|
+
}
|
|
6424
|
+
this._udpStandby ??= new UdpStandbyController({
|
|
6425
|
+
call: (method, params, timeout) => this._transport.call(method, params, timeout),
|
|
6426
|
+
canPrepare: async (timeout) => {
|
|
6427
|
+
if (!this._aid)
|
|
6428
|
+
return true;
|
|
6429
|
+
try {
|
|
6430
|
+
if (!this._aidStore) {
|
|
6431
|
+
const record = this._agentMdManager?._loadRecord?.(this._aid, true) ?? this._agentMdManager?.loadRecord(this._aid);
|
|
6432
|
+
const content = String(record?.local_draft_content ?? '') || String(record?.content ?? '');
|
|
6433
|
+
return !content || validateAgentMdReadDocument(content).frontmatter.type !== 'human';
|
|
6434
|
+
}
|
|
6435
|
+
const result = await this._aidStore.resolve(this._aid, { timeout });
|
|
6436
|
+
return !result.ok || result.data?.agent_md?.frontmatter.type !== 'human';
|
|
6437
|
+
}
|
|
6438
|
+
catch {
|
|
6439
|
+
return true;
|
|
6440
|
+
}
|
|
6441
|
+
},
|
|
6442
|
+
canStandby: () => this._state === 'ready' && !this._closing && !this._disconnecting && !this._reconnectActive
|
|
6443
|
+
&& this._sessionOptions?.background_sync !== false
|
|
6444
|
+
&& this._groupIdentityOperations.size === 0
|
|
6445
|
+
&& this._transport.isConnected() && this._transport.rpcBudget.counts.total === 0
|
|
6446
|
+
&& this._transport.foregroundWaiting === 0 && this._transport.rpcBudget.backgroundWaiting === 0
|
|
6447
|
+
&& this._dispatcher.isIdle && !this._delivery?.hasAnyPendingPull()
|
|
6448
|
+
&& (this._syncChannelCoordinator?.isIdle ?? true),
|
|
6449
|
+
prepareParams: () => ({ connection_id: this._gatewayConnectionId }),
|
|
6450
|
+
detach: () => this._transport.close(),
|
|
6451
|
+
reconnect: async () => {
|
|
6452
|
+
if (this._closing || this._disconnecting)
|
|
6453
|
+
throw new ConnectionError('client closed');
|
|
6454
|
+
this._state = 'standby';
|
|
6455
|
+
await this._lifecycle.connect(this._udpStandbyConnectOptions);
|
|
6456
|
+
},
|
|
6457
|
+
});
|
|
6458
|
+
this._udpStandby.activate(this._udpStandbyPolicy);
|
|
6459
|
+
const standby = this._udpStandby;
|
|
6460
|
+
this._transport.setStandbyGate((method, deadline, signal) => standby.acquire(method, deadline, signal), () => standby.touch(), params => standby.authenticationParams(params));
|
|
6461
|
+
}
|
|
6139
6462
|
/** 启动重连循环(默认无限重试 + 当前基数抖动,仅在不可重试错误、close() 或 max_attempts 耗尽时终止) */
|
|
6140
6463
|
_startReconnect(serverInitiated = false, initialOptions, existingAbort) {
|
|
6141
6464
|
if (existingAbort) {
|