@agentunion/fastaun-browser 0.3.6 → 0.4.1
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 +24 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +596 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +1697 -0
- package/_packed_docs/CHANGELOG.md +24 -0
- package/_packed_docs/INDEX.md +17 -11
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -10
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +134 -158
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +11 -7
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +98 -119
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +147 -374
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +153 -153
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +168 -1383
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +71 -91
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +76 -63
- package/_packed_docs/sdk/09-custody-api-manual.md +7 -6
- package/_packed_docs/sdk/09-meta-rpc-manual.md +13 -14
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +37 -49
- package/_packed_docs/sdk/INDEX.md +72 -98
- package/_packed_docs/sdk/README.md +85 -266
- package/dist/aid-store.d.ts +125 -0
- package/dist/aid-store.d.ts.map +1 -0
- package/dist/aid-store.js +841 -0
- package/dist/aid-store.js.map +1 -0
- package/dist/aid.d.ts +56 -0
- package/dist/aid.d.ts.map +1 -0
- package/dist/aid.js +112 -0
- package/dist/aid.js.map +1 -0
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1630 -1901
- package/dist/cert-utils.d.ts +26 -0
- package/dist/cert-utils.d.ts.map +1 -0
- package/dist/cert-utils.js +221 -0
- package/dist/cert-utils.js.map +1 -0
- package/dist/client.d.ts +89 -60
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +568 -160
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/error-codes.d.ts +25 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +31 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +1 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/result.d.ts +19 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +10 -0
- package/dist/result.js.map +1 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +16 -1
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -1
package/dist/client.js
CHANGED
|
@@ -11,9 +11,6 @@ import { GatewayDiscovery } from './discovery.js';
|
|
|
11
11
|
import { RPCTransport } from './transport.js';
|
|
12
12
|
import { AuthFlow } from './auth.js';
|
|
13
13
|
import { SeqTracker } from './seq-tracker.js';
|
|
14
|
-
import { AuthNamespace } from './namespaces/auth.js';
|
|
15
|
-
import { CustodyNamespace } from './namespaces/custody.js';
|
|
16
|
-
import { MetaNamespace } from './namespaces/meta.js';
|
|
17
14
|
import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, certificateSha256Fingerprint, ecdsaSignDer, ecdsaVerifyDer, importCertPublicKeyEcdsa, importPrivateKeyEcdsa, } from './crypto.js';
|
|
18
15
|
import { IndexedDBKeyStore } from './keystore/indexeddb.js';
|
|
19
16
|
import { V2Session, V2KeyStore } from './v2/session/index.js';
|
|
@@ -21,8 +18,9 @@ import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2
|
|
|
21
18
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
22
19
|
import { computeStateCommitment } from './v2/state/index.js';
|
|
23
20
|
import { AUNLogger } from './logger.js';
|
|
24
|
-
import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
25
|
-
import { isJsonObject, } from './types.js';
|
|
21
|
+
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
22
|
+
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
23
|
+
import { AID } from './aid.js';
|
|
26
24
|
/**
|
|
27
25
|
* 递归排序键的 JSON 序列化(Canonical JSON for AUN)
|
|
28
26
|
* 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
|
|
@@ -171,6 +169,12 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
171
169
|
http: 30.0,
|
|
172
170
|
},
|
|
173
171
|
};
|
|
172
|
+
const PROTECTED_HEADERS_METHODS = new Set([
|
|
173
|
+
'message.send',
|
|
174
|
+
'group.send',
|
|
175
|
+
'message.thought.put',
|
|
176
|
+
'group.thought.put',
|
|
177
|
+
]);
|
|
174
178
|
const RECONNECT_MIN_BASE_DELAY_SECONDS = 1.0;
|
|
175
179
|
const RECONNECT_MAX_BASE_DELAY_SECONDS = 64.0;
|
|
176
180
|
const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
|
|
@@ -213,6 +217,7 @@ function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
|
|
|
213
217
|
/** 对端证书缓存 TTL(秒) */
|
|
214
218
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
215
219
|
const PEER_PREKEYS_CACHE_TTL = 3600;
|
|
220
|
+
const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
|
|
216
221
|
/**
|
|
217
222
|
* 将 WebSocket URL 转为对应的 HTTP URL
|
|
218
223
|
*/
|
|
@@ -245,6 +250,29 @@ function buildCertUrl(gatewayUrl, aid, certFingerprint) {
|
|
|
245
250
|
}
|
|
246
251
|
return url.toString();
|
|
247
252
|
}
|
|
253
|
+
function agentMdHttpScheme(gatewayUrl) {
|
|
254
|
+
const raw = String(gatewayUrl ?? '').trim().toLowerCase();
|
|
255
|
+
return raw.startsWith('ws://') ? 'http' : 'https';
|
|
256
|
+
}
|
|
257
|
+
function agentMdAuthority(aid) {
|
|
258
|
+
return String(aid ?? '').trim();
|
|
259
|
+
}
|
|
260
|
+
async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
263
|
+
try {
|
|
264
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (controller.signal.aborted) {
|
|
268
|
+
throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
globalThis.clearTimeout(timer);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
248
276
|
/**
|
|
249
277
|
* 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
|
|
250
278
|
*
|
|
@@ -581,6 +609,22 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
|
|
|
581
609
|
affinity_ttl_ms: affinityTtlMs,
|
|
582
610
|
};
|
|
583
611
|
}
|
|
612
|
+
function assertClientOptions(value, label) {
|
|
613
|
+
if (value == null)
|
|
614
|
+
return;
|
|
615
|
+
if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
|
|
616
|
+
throw new ValidationError(`${label} must be an options object`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function clientOptionsConfig(options) {
|
|
620
|
+
const raw = { ...(options ?? {}) };
|
|
621
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
|
|
622
|
+
throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
|
|
623
|
+
}
|
|
624
|
+
delete raw.debug;
|
|
625
|
+
delete raw.protected_headers;
|
|
626
|
+
return raw;
|
|
627
|
+
}
|
|
584
628
|
/**
|
|
585
629
|
* AUN Core SDK 客户端 — 浏览器版本。
|
|
586
630
|
*
|
|
@@ -601,6 +645,8 @@ export class AUNClient {
|
|
|
601
645
|
_aid = null;
|
|
602
646
|
_identity = null;
|
|
603
647
|
_state = 'idle';
|
|
648
|
+
_currentAid = null;
|
|
649
|
+
_instanceProtectedHeaders = null;
|
|
604
650
|
_gatewayUrl = null;
|
|
605
651
|
_deviceId;
|
|
606
652
|
_slotId;
|
|
@@ -615,12 +661,6 @@ export class AUNClient {
|
|
|
615
661
|
_keystore;
|
|
616
662
|
_auth;
|
|
617
663
|
_transport;
|
|
618
|
-
/** 认证命名空间 */
|
|
619
|
-
auth;
|
|
620
|
-
/** AID 托管命名空间 */
|
|
621
|
-
custody;
|
|
622
|
-
/** 元数据命名空间(心跳、状态、信任根管理) */
|
|
623
|
-
meta;
|
|
624
664
|
// E2EE 编排状态(内存缓存)
|
|
625
665
|
_certCache = new Map();
|
|
626
666
|
// 后台任务 handle(浏览器 setInterval/setTimeout)
|
|
@@ -661,7 +701,7 @@ export class AUNClient {
|
|
|
661
701
|
_localAgentMdEtag = '';
|
|
662
702
|
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
663
703
|
_remoteAgentMdEtag = '';
|
|
664
|
-
/** 浏览器侧
|
|
704
|
+
/** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
665
705
|
_agentMdPath = '';
|
|
666
706
|
_agentMdCache = new Map();
|
|
667
707
|
_agentMdFetchInflight = new Set();
|
|
@@ -686,6 +726,14 @@ export class AUNClient {
|
|
|
686
726
|
_reconnectActive = false;
|
|
687
727
|
_reconnectAbort = null;
|
|
688
728
|
_serverKicked = false;
|
|
729
|
+
// 重连状态追踪(对齐 Python client.py)
|
|
730
|
+
_nextRetryAt = null;
|
|
731
|
+
_retryAttempt = 0;
|
|
732
|
+
_retryMaxAttempts = 0;
|
|
733
|
+
_lastError = null;
|
|
734
|
+
_lastErrorCode = null;
|
|
735
|
+
/** 对端 AID 缓存(aid string → AID 对象) */
|
|
736
|
+
_peerCache = new Map();
|
|
689
737
|
/**
|
|
690
738
|
* 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
|
|
691
739
|
* 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
|
|
@@ -699,17 +747,25 @@ export class AUNClient {
|
|
|
699
747
|
_logKeystore;
|
|
700
748
|
_logDiscovery;
|
|
701
749
|
_logEvents;
|
|
702
|
-
constructor(
|
|
703
|
-
const
|
|
750
|
+
constructor(aid) {
|
|
751
|
+
const inputAid = aid instanceof AID ? aid : null;
|
|
752
|
+
if (typeof aid === 'string') {
|
|
753
|
+
throw new ValidationError('AUNClient aid must be an AID object, not a string');
|
|
754
|
+
}
|
|
755
|
+
const options = {};
|
|
756
|
+
const rawConfig = clientOptionsConfig(options);
|
|
757
|
+
if (inputAid)
|
|
758
|
+
rawConfig.aun_path = inputAid.aunPath;
|
|
759
|
+
const _debug = false;
|
|
704
760
|
this.configModel = createConfig(rawConfig);
|
|
705
|
-
const initAid =
|
|
761
|
+
const initAid = inputAid ? inputAid.aid : null;
|
|
706
762
|
this.config = {
|
|
707
763
|
aun_path: this.configModel.aunPath,
|
|
708
764
|
root_ca_path: this.configModel.rootCaPem,
|
|
709
765
|
seed_password: this.configModel.seedPassword,
|
|
710
766
|
};
|
|
711
767
|
this._agentMdPath = this._agentMdDefaultRoot();
|
|
712
|
-
this._deviceId = getDeviceId();
|
|
768
|
+
this._deviceId = (inputAid?.deviceId) || getDeviceId();
|
|
713
769
|
// Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
|
|
714
770
|
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
715
771
|
this._logger.bindDeviceId(this._deviceId);
|
|
@@ -723,7 +779,7 @@ export class AUNClient {
|
|
|
723
779
|
this._dispatcher = new EventDispatcher();
|
|
724
780
|
this._discovery = new GatewayDiscovery();
|
|
725
781
|
this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
|
|
726
|
-
this._slotId = '';
|
|
782
|
+
this._slotId = inputAid?.slotId || 'default';
|
|
727
783
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
728
784
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
729
785
|
this._auth = new AuthFlow({
|
|
@@ -746,9 +802,21 @@ export class AUNClient {
|
|
|
746
802
|
this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
|
|
747
803
|
});
|
|
748
804
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
805
|
+
if (inputAid) {
|
|
806
|
+
if (!inputAid.isPrivateKeyValid())
|
|
807
|
+
throw new StateError('AUNClient requires an AID with a valid private key');
|
|
808
|
+
this._currentAid = inputAid;
|
|
809
|
+
this._identity = {
|
|
810
|
+
aid: inputAid.aid,
|
|
811
|
+
private_key_pem: inputAid._privateKeyPem ?? '',
|
|
812
|
+
public_key_der_b64: inputAid.publicKey,
|
|
813
|
+
cert: inputAid.certPem,
|
|
814
|
+
};
|
|
815
|
+
this._state = 'disconnected';
|
|
816
|
+
}
|
|
817
|
+
if (options?.protected_headers !== undefined) {
|
|
818
|
+
this.setProtectedHeaders(options.protected_headers);
|
|
819
|
+
}
|
|
752
820
|
// 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
|
|
753
821
|
this._auth.setLogger(this._logAuth);
|
|
754
822
|
this._transport.setLogger(this._logTransport);
|
|
@@ -756,18 +824,9 @@ export class AUNClient {
|
|
|
756
824
|
if (typeof this._discovery.setLogger === 'function') {
|
|
757
825
|
this._discovery.setLogger(this._logger.for('aun_core.discovery'));
|
|
758
826
|
}
|
|
759
|
-
if (typeof this.auth.setLogger === 'function') {
|
|
760
|
-
this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
|
|
761
|
-
}
|
|
762
|
-
if (typeof this.custody.setLogger === 'function') {
|
|
763
|
-
this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
|
|
764
|
-
}
|
|
765
827
|
if (typeof this._keystore.setLogger === 'function') {
|
|
766
828
|
this._keystore.setLogger(this._logKeystore);
|
|
767
829
|
}
|
|
768
|
-
if (typeof this.meta.setLogger === 'function') {
|
|
769
|
-
this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
|
|
770
|
-
}
|
|
771
830
|
// 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
|
|
772
831
|
this._dispatcher.subscribe('_raw.message.received', (data) => {
|
|
773
832
|
this._onRawMessageReceived(data);
|
|
@@ -818,17 +877,182 @@ export class AUNClient {
|
|
|
818
877
|
get aid() {
|
|
819
878
|
return this._aid;
|
|
820
879
|
}
|
|
821
|
-
|
|
880
|
+
_setAgentMdRoot(root) {
|
|
822
881
|
const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
|
|
823
882
|
this._agentMdPath = next;
|
|
824
883
|
this._agentMdCache.clear();
|
|
825
884
|
return next;
|
|
826
885
|
}
|
|
827
|
-
|
|
828
|
-
|
|
886
|
+
async _resolveAgentMdUrl(aid) {
|
|
887
|
+
const target = String(aid ?? '').trim();
|
|
888
|
+
if (!target)
|
|
889
|
+
throw new ValidationError('agent.md requires non-empty aid');
|
|
890
|
+
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
891
|
+
if (!gatewayUrl) {
|
|
892
|
+
try {
|
|
893
|
+
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
gatewayUrl = '';
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const authority = agentMdAuthority(target);
|
|
900
|
+
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
901
|
+
}
|
|
902
|
+
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
903
|
+
let identity = await this._auth.loadIdentityOrNone(aid);
|
|
904
|
+
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
905
|
+
identity = this._identity;
|
|
906
|
+
}
|
|
907
|
+
if (!identity) {
|
|
908
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
909
|
+
}
|
|
910
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
911
|
+
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
912
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
913
|
+
return cachedToken;
|
|
914
|
+
}
|
|
915
|
+
if (identity.refresh_token) {
|
|
916
|
+
try {
|
|
917
|
+
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
918
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
919
|
+
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
920
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
921
|
+
this._identity = refreshed;
|
|
922
|
+
return refreshedToken;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// refresh 失败时回退到完整 authenticate。
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const result = await this._auth.authenticate(gatewayUrl, aid);
|
|
930
|
+
const token = String(result.access_token ?? '');
|
|
931
|
+
if (!token)
|
|
932
|
+
throw new StateError('authenticate did not return access_token');
|
|
933
|
+
const fallbackIdentity = {
|
|
934
|
+
...identity,
|
|
935
|
+
access_token: token,
|
|
936
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
937
|
+
};
|
|
938
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
939
|
+
if (Number.isFinite(fallbackExpiresAt))
|
|
940
|
+
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
941
|
+
this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
|
|
942
|
+
return token;
|
|
943
|
+
}
|
|
944
|
+
async _uploadAgentMd(content) {
|
|
945
|
+
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
946
|
+
if (!target)
|
|
947
|
+
throw new StateError('uploadAgentMd requires local AID');
|
|
948
|
+
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
949
|
+
this._gatewayUrl = gatewayUrl;
|
|
950
|
+
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
951
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
952
|
+
method: 'PUT',
|
|
953
|
+
headers: {
|
|
954
|
+
Authorization: `Bearer ${token}`,
|
|
955
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
956
|
+
},
|
|
957
|
+
body: content,
|
|
958
|
+
});
|
|
959
|
+
if (response.status === 404) {
|
|
960
|
+
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
961
|
+
}
|
|
962
|
+
if (!response.ok) {
|
|
963
|
+
const message = (await response.text()).trim();
|
|
964
|
+
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
965
|
+
}
|
|
966
|
+
const payload = await response.json();
|
|
967
|
+
if (!isJsonObject(payload))
|
|
968
|
+
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
969
|
+
return payload;
|
|
970
|
+
}
|
|
971
|
+
async _downloadAgentMd(aid) {
|
|
972
|
+
const target = String(aid ?? '').trim();
|
|
973
|
+
if (!target)
|
|
974
|
+
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
975
|
+
const cached = this._agentMdCache.get(target);
|
|
976
|
+
const url = await this._resolveAgentMdUrl(target);
|
|
977
|
+
const response = await fetchWithTimeout(url, {
|
|
978
|
+
method: 'GET',
|
|
979
|
+
headers: { Accept: 'text/markdown' },
|
|
980
|
+
redirect: 'follow',
|
|
981
|
+
});
|
|
982
|
+
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
983
|
+
return String(cached.text);
|
|
984
|
+
}
|
|
985
|
+
if (response.status === 404) {
|
|
986
|
+
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
987
|
+
}
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
const message = (await response.text()).trim();
|
|
990
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
991
|
+
}
|
|
992
|
+
const text = await response.text();
|
|
993
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
994
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
995
|
+
this._agentMdCache.set(target, {
|
|
996
|
+
...(cached ?? {}),
|
|
997
|
+
text,
|
|
998
|
+
etag,
|
|
999
|
+
lastModified,
|
|
1000
|
+
remote_etag: etag,
|
|
1001
|
+
last_modified: lastModified,
|
|
1002
|
+
});
|
|
1003
|
+
return text;
|
|
1004
|
+
}
|
|
1005
|
+
async _headAgentMd(aid) {
|
|
1006
|
+
const target = String(aid ?? '').trim();
|
|
1007
|
+
if (!target)
|
|
1008
|
+
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
1009
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
1010
|
+
method: 'HEAD',
|
|
1011
|
+
headers: { Accept: 'text/markdown' },
|
|
1012
|
+
});
|
|
1013
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1014
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1015
|
+
if (response.status === 404) {
|
|
1016
|
+
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
1017
|
+
}
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
1020
|
+
}
|
|
1021
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
1022
|
+
this._agentMdCache.set(target, {
|
|
1023
|
+
...cached,
|
|
1024
|
+
etag,
|
|
1025
|
+
lastModified,
|
|
1026
|
+
remote_etag: etag,
|
|
1027
|
+
last_modified: lastModified,
|
|
1028
|
+
});
|
|
1029
|
+
return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
|
|
829
1030
|
}
|
|
830
|
-
|
|
831
|
-
|
|
1031
|
+
async _verifyAgentMd(content, aid) {
|
|
1032
|
+
const target = String(aid ?? '').trim();
|
|
1033
|
+
if (!target)
|
|
1034
|
+
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
1035
|
+
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
1036
|
+
if (!peer) {
|
|
1037
|
+
let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
|
|
1038
|
+
if (!certPem) {
|
|
1039
|
+
certPem = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1040
|
+
}
|
|
1041
|
+
if (!certPem)
|
|
1042
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1043
|
+
peer = await AID.create({
|
|
1044
|
+
aid: target,
|
|
1045
|
+
aunPath: this.configModel.aunPath,
|
|
1046
|
+
certPem,
|
|
1047
|
+
privateKeyPem: null,
|
|
1048
|
+
certValid: true,
|
|
1049
|
+
privateKeyValid: false,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
const result = await peer.verifyAgentMd(content);
|
|
1053
|
+
if (!result.ok)
|
|
1054
|
+
throw new AUNError(result.error.message);
|
|
1055
|
+
return { ...result.data, verified: result.data.status === 'verified' };
|
|
832
1056
|
}
|
|
833
1057
|
/**
|
|
834
1058
|
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
@@ -856,8 +1080,12 @@ export class AUNClient {
|
|
|
856
1080
|
if (localContent === null || localContent.length === 0) {
|
|
857
1081
|
throw new ValidationError('publishAgentMd requires local agent.md content');
|
|
858
1082
|
}
|
|
859
|
-
const
|
|
860
|
-
|
|
1083
|
+
const signedResult = await this._currentAid?.signAgentMd(localContent);
|
|
1084
|
+
if (!signedResult?.ok) {
|
|
1085
|
+
throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1086
|
+
}
|
|
1087
|
+
const signed = signedResult.data.signed;
|
|
1088
|
+
const result = await this._uploadAgentMd(signed);
|
|
861
1089
|
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
862
1090
|
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
863
1091
|
if (remoteEtag)
|
|
@@ -877,13 +1105,13 @@ export class AUNClient {
|
|
|
877
1105
|
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
878
1106
|
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
879
1107
|
*/
|
|
880
|
-
async
|
|
1108
|
+
async _fetchAgentMdCache(aid) {
|
|
881
1109
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
882
1110
|
if (!target) {
|
|
883
1111
|
throw new ValidationError('fetchAgentMd requires aid (or local AID)');
|
|
884
1112
|
}
|
|
885
|
-
const content = await this.
|
|
886
|
-
const signature = await this.
|
|
1113
|
+
const content = await this._downloadAgentMd(target);
|
|
1114
|
+
const signature = await this._verifyAgentMd(content, target);
|
|
887
1115
|
const isSelf = target === (this._aid ?? '');
|
|
888
1116
|
const localEtag = await this._agentMdContentEtag(content);
|
|
889
1117
|
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
@@ -932,7 +1160,7 @@ export class AUNClient {
|
|
|
932
1160
|
return String(this._aid ?? '').trim();
|
|
933
1161
|
}
|
|
934
1162
|
_agentMdDefaultRoot() {
|
|
935
|
-
return this._joinAgentMdPath(this.configModel.aunPath || '.', '
|
|
1163
|
+
return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
|
|
936
1164
|
}
|
|
937
1165
|
_joinAgentMdPath(base, name) {
|
|
938
1166
|
const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
|
|
@@ -1039,8 +1267,7 @@ export class AUNClient {
|
|
|
1039
1267
|
}
|
|
1040
1268
|
_agentMdAuthCacheMeta(aid) {
|
|
1041
1269
|
try {
|
|
1042
|
-
const
|
|
1043
|
-
const record = store?.get(String(aid ?? '').trim());
|
|
1270
|
+
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
1044
1271
|
return record && typeof record === 'object' ? { ...record } : {};
|
|
1045
1272
|
}
|
|
1046
1273
|
catch {
|
|
@@ -1165,7 +1392,7 @@ export class AUNClient {
|
|
|
1165
1392
|
return;
|
|
1166
1393
|
this._agentMdFetchInflight.add(target);
|
|
1167
1394
|
try {
|
|
1168
|
-
await this.
|
|
1395
|
+
await this._fetchAgentMdCache(target);
|
|
1169
1396
|
}
|
|
1170
1397
|
catch (err) {
|
|
1171
1398
|
await this._saveAgentMdRecord(target, {
|
|
@@ -1226,7 +1453,7 @@ export class AUNClient {
|
|
|
1226
1453
|
}
|
|
1227
1454
|
await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1228
1455
|
}
|
|
1229
|
-
async
|
|
1456
|
+
async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
|
|
1230
1457
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
1231
1458
|
if (!target)
|
|
1232
1459
|
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
@@ -1256,7 +1483,7 @@ export class AUNClient {
|
|
|
1256
1483
|
const now = Date.now();
|
|
1257
1484
|
let remote;
|
|
1258
1485
|
try {
|
|
1259
|
-
remote = await this.
|
|
1486
|
+
remote = await this._headAgentMd(target);
|
|
1260
1487
|
}
|
|
1261
1488
|
catch (err) {
|
|
1262
1489
|
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1310,7 +1537,116 @@ export class AUNClient {
|
|
|
1310
1537
|
}
|
|
1311
1538
|
}
|
|
1312
1539
|
get state() {
|
|
1313
|
-
return this._state;
|
|
1540
|
+
return this._publicState(this._state);
|
|
1541
|
+
}
|
|
1542
|
+
_publicState(state) {
|
|
1543
|
+
return STATE_TO_PUBLIC[state] ?? state;
|
|
1544
|
+
}
|
|
1545
|
+
get currentAid() {
|
|
1546
|
+
return this._currentAid;
|
|
1547
|
+
}
|
|
1548
|
+
get hasIdentity() {
|
|
1549
|
+
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
1550
|
+
}
|
|
1551
|
+
get canSign() {
|
|
1552
|
+
return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
|
|
1553
|
+
}
|
|
1554
|
+
get canConnect() {
|
|
1555
|
+
return this.hasIdentity && this.state !== ConnectionState.CLOSED;
|
|
1556
|
+
}
|
|
1557
|
+
get canSend() {
|
|
1558
|
+
return this.state === ConnectionState.READY;
|
|
1559
|
+
}
|
|
1560
|
+
get isReady() { return this.canSend; }
|
|
1561
|
+
get isOnline() {
|
|
1562
|
+
return this.state === ConnectionState.READY
|
|
1563
|
+
|| this.state === ConnectionState.RECONNECTING
|
|
1564
|
+
|| this.state === ConnectionState.RETRY_BACKOFF;
|
|
1565
|
+
}
|
|
1566
|
+
get isClosed() { return this.state === ConnectionState.CLOSED; }
|
|
1567
|
+
get aunPath() { return this.hasIdentity ? this._currentAid?.aunPath ?? this.configModel.aunPath : null; }
|
|
1568
|
+
/** 下次重连时间(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_at) */
|
|
1569
|
+
get nextRetryAt() {
|
|
1570
|
+
return this.state === ConnectionState.RETRY_BACKOFF ? this._nextRetryAt : null;
|
|
1571
|
+
}
|
|
1572
|
+
/** 距下次重连的剩余秒数(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_in_seconds) */
|
|
1573
|
+
get nextRetryInSeconds() {
|
|
1574
|
+
const t = this.nextRetryAt;
|
|
1575
|
+
if (t === null)
|
|
1576
|
+
return null;
|
|
1577
|
+
return Math.max(0, (t.getTime() - Date.now()) / 1000);
|
|
1578
|
+
}
|
|
1579
|
+
/** 当前重连尝试次数(对齐 Python retry_attempt) */
|
|
1580
|
+
get retryAttempt() { return this._retryAttempt; }
|
|
1581
|
+
/** 最大重连次数(0 = 无限,对齐 Python retry_max_attempts) */
|
|
1582
|
+
get retryMaxAttempts() { return this._retryMaxAttempts; }
|
|
1583
|
+
/** 最近一次错误(对齐 Python last_error) */
|
|
1584
|
+
get lastError() { return this._lastError; }
|
|
1585
|
+
/** 最近一次错误码(对齐 Python last_error_code) */
|
|
1586
|
+
get lastErrorCode() { return this._lastErrorCode; }
|
|
1587
|
+
loadIdentity(aid) {
|
|
1588
|
+
if (!aid?.isPrivateKeyValid())
|
|
1589
|
+
throw new StateError('loadIdentity requires an AID with a valid private key');
|
|
1590
|
+
const publicState = this.state;
|
|
1591
|
+
if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
|
|
1592
|
+
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
1593
|
+
}
|
|
1594
|
+
this._currentAid = aid;
|
|
1595
|
+
this._aid = aid.aid;
|
|
1596
|
+
this._identity = {
|
|
1597
|
+
aid: aid.aid,
|
|
1598
|
+
private_key_pem: aid._privateKeyPem ?? '',
|
|
1599
|
+
public_key_der_b64: aid.publicKey,
|
|
1600
|
+
cert: aid.certPem,
|
|
1601
|
+
};
|
|
1602
|
+
this._auth._aid = aid.aid;
|
|
1603
|
+
this._state = 'disconnected';
|
|
1604
|
+
this._closing = false;
|
|
1605
|
+
}
|
|
1606
|
+
setProtectedHeaders(headers) {
|
|
1607
|
+
if (!headers) {
|
|
1608
|
+
this._instanceProtectedHeaders = null;
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const cleaned = {};
|
|
1612
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1613
|
+
if (key === '_auth')
|
|
1614
|
+
continue;
|
|
1615
|
+
cleaned[String(key)] = String(value);
|
|
1616
|
+
}
|
|
1617
|
+
this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
|
|
1618
|
+
}
|
|
1619
|
+
getProtectedHeaders() {
|
|
1620
|
+
return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
|
|
1621
|
+
}
|
|
1622
|
+
cachePeer(aid) {
|
|
1623
|
+
if (!this.hasIdentity)
|
|
1624
|
+
throw new StateError('cachePeer requires a loaded identity');
|
|
1625
|
+
if (!aid.isCertValid())
|
|
1626
|
+
throw new ValidationError('cachePeer requires an AID with a valid certificate');
|
|
1627
|
+
this._peerCache.set(aid.aid, aid);
|
|
1628
|
+
return aid;
|
|
1629
|
+
}
|
|
1630
|
+
getPeer(aid) {
|
|
1631
|
+
if (!this.hasIdentity)
|
|
1632
|
+
throw new StateError('getPeer requires a loaded identity');
|
|
1633
|
+
return this._peerCache.get(String(aid ?? '').trim()) ?? null;
|
|
1634
|
+
}
|
|
1635
|
+
async lookupPeer(aid) {
|
|
1636
|
+
if (!this.hasIdentity)
|
|
1637
|
+
throw new StateError('lookupPeer requires a loaded identity');
|
|
1638
|
+
const target = String(aid ?? '').trim();
|
|
1639
|
+
if (!target)
|
|
1640
|
+
throw new ValidationError('lookupPeer requires non-empty aid');
|
|
1641
|
+
const cached = this._peerCache.get(target);
|
|
1642
|
+
if (cached)
|
|
1643
|
+
return cached;
|
|
1644
|
+
throw new NotFoundError(`peer not found in cache: ${target}`);
|
|
1645
|
+
}
|
|
1646
|
+
peers() {
|
|
1647
|
+
if (!this.hasIdentity)
|
|
1648
|
+
throw new StateError('peers requires a loaded identity');
|
|
1649
|
+
return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
1314
1650
|
}
|
|
1315
1651
|
get gatewayUrl() {
|
|
1316
1652
|
return this._gatewayUrl;
|
|
@@ -1325,36 +1661,81 @@ export class AUNClient {
|
|
|
1325
1661
|
get gatewayHealth() {
|
|
1326
1662
|
return this._discovery.lastHealthy;
|
|
1327
1663
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1664
|
+
// ── 生命周期 ──────────────────────────────────────
|
|
1665
|
+
/** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
|
|
1666
|
+
async authenticate(options = {}) {
|
|
1330
1667
|
const tStart = Date.now();
|
|
1331
|
-
this.
|
|
1668
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1669
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1670
|
+
throw new StateError('authenticate requires a loaded AID with a valid private key');
|
|
1671
|
+
}
|
|
1672
|
+
const publicState = this.state;
|
|
1673
|
+
if (publicState !== ConnectionState.STANDBY) {
|
|
1674
|
+
throw new StateError(`authenticate not allowed in state ${publicState}`);
|
|
1675
|
+
}
|
|
1676
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1677
|
+
throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
|
|
1678
|
+
}
|
|
1679
|
+
this._state = 'connecting';
|
|
1332
1680
|
try {
|
|
1333
|
-
const
|
|
1334
|
-
this.
|
|
1681
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1682
|
+
const result = await this._auth.authenticate(gateway, target);
|
|
1683
|
+
this._gatewayUrl = String(result.gateway ?? gateway);
|
|
1684
|
+
this._identity = await this._auth.loadIdentityOrNone(target);
|
|
1685
|
+
this._state = 'authenticated';
|
|
1686
|
+
this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
|
|
1335
1687
|
return result;
|
|
1336
1688
|
}
|
|
1337
1689
|
catch (err) {
|
|
1338
|
-
this.
|
|
1690
|
+
this._state = 'disconnected';
|
|
1691
|
+
this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1339
1692
|
throw err;
|
|
1340
1693
|
}
|
|
1341
1694
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
* 连接到 Gateway。
|
|
1345
|
-
*
|
|
1346
|
-
* @param auth - 认证参数,必须包含 access_token 和 gateway
|
|
1347
|
-
* @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
|
|
1348
|
-
*/
|
|
1349
|
-
async connect(auth, options) {
|
|
1695
|
+
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1696
|
+
async connect(opts) {
|
|
1350
1697
|
const tStart = Date.now();
|
|
1698
|
+
const options = {};
|
|
1699
|
+
if (opts?.auto_reconnect !== undefined)
|
|
1700
|
+
options.auto_reconnect = opts.auto_reconnect;
|
|
1701
|
+
if (opts?.heartbeat_interval !== undefined)
|
|
1702
|
+
options.heartbeat_interval = opts.heartbeat_interval;
|
|
1703
|
+
if (opts?.connect_timeout !== undefined || opts?.call_timeout !== undefined) {
|
|
1704
|
+
options.timeouts = {
|
|
1705
|
+
...(opts.connect_timeout !== undefined ? { connect: opts.connect_timeout } : {}),
|
|
1706
|
+
...(opts.call_timeout !== undefined ? { call: opts.call_timeout } : {}),
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
if (opts?.retry_initial_delay !== undefined || opts?.retry_max_delay !== undefined || opts?.retry_max_attempts !== undefined) {
|
|
1710
|
+
options.retry = {
|
|
1711
|
+
initial_delay: opts.retry_initial_delay ?? 1,
|
|
1712
|
+
max_delay: opts.retry_max_delay ?? 64,
|
|
1713
|
+
max_attempts: opts.retry_max_attempts ?? 0,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1351
1716
|
this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
|
|
1352
|
-
|
|
1717
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1718
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1719
|
+
throw new StateError('connect requires a loaded AID with a valid private key');
|
|
1720
|
+
}
|
|
1721
|
+
const publicState = this.state;
|
|
1722
|
+
const allowed = new Set([
|
|
1723
|
+
ConnectionState.STANDBY,
|
|
1724
|
+
ConnectionState.AUTHENTICATED,
|
|
1725
|
+
ConnectionState.RETRY_BACKOFF,
|
|
1726
|
+
ConnectionState.CONNECTION_FAILED,
|
|
1727
|
+
]);
|
|
1728
|
+
if (!allowed.has(publicState)) {
|
|
1353
1729
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
|
|
1354
|
-
throw new StateError(`connect not allowed in state ${
|
|
1730
|
+
throw new StateError(`connect not allowed in state ${publicState}`);
|
|
1731
|
+
}
|
|
1732
|
+
// gateway 来自 authenticate() 缓存的 this._gatewayUrl;未认证则自动 authenticate()
|
|
1733
|
+
if (!this._gatewayUrl) {
|
|
1734
|
+
await this.authenticate();
|
|
1355
1735
|
}
|
|
1356
1736
|
this._state = 'connecting';
|
|
1357
|
-
const
|
|
1737
|
+
const gateway = String(this._gatewayUrl ?? '').trim();
|
|
1738
|
+
const params = { ...options, gateway };
|
|
1358
1739
|
const normalized = this._normalizeConnectParams(params);
|
|
1359
1740
|
this._sessionParams = normalized;
|
|
1360
1741
|
this._sessionOptions = this._buildSessionOptions(normalized);
|
|
@@ -1365,7 +1746,7 @@ export class AUNClient {
|
|
|
1365
1746
|
for (const gw of gateways) {
|
|
1366
1747
|
try {
|
|
1367
1748
|
const gwParams = { ...normalized, gateway: gw };
|
|
1368
|
-
await this._connectOnce(gwParams,
|
|
1749
|
+
await this._connectOnce(gwParams, true);
|
|
1369
1750
|
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
|
|
1370
1751
|
return;
|
|
1371
1752
|
}
|
|
@@ -1380,7 +1761,7 @@ export class AUNClient {
|
|
|
1380
1761
|
}
|
|
1381
1762
|
}
|
|
1382
1763
|
if (this._state === 'connecting' || this._state === 'authenticating') {
|
|
1383
|
-
this._state = '
|
|
1764
|
+
this._state = 'terminal_failed';
|
|
1384
1765
|
}
|
|
1385
1766
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
1386
1767
|
throw lastErr;
|
|
@@ -1402,56 +1783,9 @@ export class AUNClient {
|
|
|
1402
1783
|
}
|
|
1403
1784
|
await this._transport.close();
|
|
1404
1785
|
this._state = 'disconnected';
|
|
1405
|
-
await this._dispatcher.publish('
|
|
1786
|
+
await this._dispatcher.publish('state_change', { state: this._publicState(this._state) });
|
|
1406
1787
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
|
|
1407
1788
|
}
|
|
1408
|
-
/** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
|
|
1409
|
-
async listIdentities() {
|
|
1410
|
-
const tStart = Date.now();
|
|
1411
|
-
this._clientLog.debug('listIdentities enter');
|
|
1412
|
-
try {
|
|
1413
|
-
const listFn = this._keystore.listIdentities;
|
|
1414
|
-
if (typeof listFn !== 'function') {
|
|
1415
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
|
|
1416
|
-
return [];
|
|
1417
|
-
}
|
|
1418
|
-
const aids = await listFn.call(this._keystore);
|
|
1419
|
-
const summaries = [];
|
|
1420
|
-
for (const aid of [...aids].sort()) {
|
|
1421
|
-
const identity = await this._keystore.loadIdentity(aid);
|
|
1422
|
-
if (!identity || !identity.private_key_pem)
|
|
1423
|
-
continue;
|
|
1424
|
-
const summary = { aid };
|
|
1425
|
-
// 优先从 loadMetadata 获取
|
|
1426
|
-
const loadMeta = this._keystore.loadMetadata;
|
|
1427
|
-
if (typeof loadMeta === 'function') {
|
|
1428
|
-
const md = await loadMeta.call(this._keystore, aid);
|
|
1429
|
-
if (md && Object.keys(md).length > 0) {
|
|
1430
|
-
summary.metadata = md;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
// 回退:从 identity 中提取非核心字段
|
|
1434
|
-
if (!summary.metadata) {
|
|
1435
|
-
const metadata = {};
|
|
1436
|
-
for (const [key, value] of Object.entries(identity)) {
|
|
1437
|
-
if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
|
|
1438
|
-
metadata[key] = value;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (Object.keys(metadata).length > 0) {
|
|
1442
|
-
summary.metadata = metadata;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
summaries.push(summary);
|
|
1446
|
-
}
|
|
1447
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
|
|
1448
|
-
return summaries;
|
|
1449
|
-
}
|
|
1450
|
-
catch (err) {
|
|
1451
|
-
this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1452
|
-
throw err;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
1789
|
/** 关闭连接 */
|
|
1456
1790
|
async close() {
|
|
1457
1791
|
const tStart = Date.now();
|
|
@@ -1480,7 +1814,7 @@ export class AUNClient {
|
|
|
1480
1814
|
}
|
|
1481
1815
|
await this._transport.close();
|
|
1482
1816
|
this._state = 'closed';
|
|
1483
|
-
await this._dispatcher.publish('
|
|
1817
|
+
await this._dispatcher.publish('state_change', { state: this._publicState(this._state) });
|
|
1484
1818
|
this._resetSeqTrackingState();
|
|
1485
1819
|
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
|
|
1486
1820
|
}
|
|
@@ -1515,6 +1849,10 @@ export class AUNClient {
|
|
|
1515
1849
|
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
1516
1850
|
}
|
|
1517
1851
|
const p = { ...(params ?? {}) };
|
|
1852
|
+
if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
|
|
1853
|
+
const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
|
|
1854
|
+
p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
|
|
1855
|
+
}
|
|
1518
1856
|
if (method === 'message.send' || method === 'group.send') {
|
|
1519
1857
|
this._normalizeOutboundMessagePayload(p, method);
|
|
1520
1858
|
}
|
|
@@ -1545,7 +1883,7 @@ export class AUNClient {
|
|
|
1545
1883
|
throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
1546
1884
|
}
|
|
1547
1885
|
this._clientLog.debug('call route: message.send → V2 encrypted send');
|
|
1548
|
-
return await this.
|
|
1886
|
+
return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
|
|
1549
1887
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1550
1888
|
timestamp: p.timestamp,
|
|
1551
1889
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1564,7 +1902,7 @@ export class AUNClient {
|
|
|
1564
1902
|
throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
1565
1903
|
}
|
|
1566
1904
|
this._clientLog.debug('call route: group.send → V2 encrypted send');
|
|
1567
|
-
return await this.
|
|
1905
|
+
return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
|
|
1568
1906
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1569
1907
|
timestamp: p.timestamp,
|
|
1570
1908
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1612,24 +1950,24 @@ export class AUNClient {
|
|
|
1612
1950
|
// message.pull:V2 就绪时走 V2 pull
|
|
1613
1951
|
if (method === 'message.pull' && this._v2Session) {
|
|
1614
1952
|
this._clientLog.debug('call route: message.pull → V2 pull');
|
|
1615
|
-
const messages = await this.
|
|
1953
|
+
const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
|
|
1616
1954
|
return { messages };
|
|
1617
1955
|
}
|
|
1618
1956
|
// message.ack:V2 就绪时走 V2 ack
|
|
1619
1957
|
if (method === 'message.ack' && this._v2Session) {
|
|
1620
1958
|
this._clientLog.debug('call route: message.ack → V2 ack');
|
|
1621
|
-
return await this.
|
|
1959
|
+
return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
|
|
1622
1960
|
}
|
|
1623
1961
|
// group.pull:V2 就绪时走 V2 pull
|
|
1624
1962
|
if (method === 'group.pull' && this._v2Session && p.group_id) {
|
|
1625
1963
|
this._clientLog.debug('call route: group.pull → V2 pull');
|
|
1626
|
-
const messages = await this.
|
|
1964
|
+
const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
|
|
1627
1965
|
return { messages };
|
|
1628
1966
|
}
|
|
1629
1967
|
// group.ack_messages:V2 就绪时走 V2 ack
|
|
1630
1968
|
if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
|
|
1631
1969
|
this._clientLog.debug('call route: group.ack_messages → V2 ack');
|
|
1632
|
-
return await this.
|
|
1970
|
+
return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
|
|
1633
1971
|
}
|
|
1634
1972
|
// 关键操作自动附加客户端签名
|
|
1635
1973
|
if (SIGNED_METHODS.has(method)) {
|
|
@@ -1754,16 +2092,6 @@ export class AUNClient {
|
|
|
1754
2092
|
}
|
|
1755
2093
|
return await this._transport.call(method, p);
|
|
1756
2094
|
}
|
|
1757
|
-
// ── 便利方法 ──────────────────────────────────────
|
|
1758
|
-
async ping(params) {
|
|
1759
|
-
return this.meta.ping(params);
|
|
1760
|
-
}
|
|
1761
|
-
async status(params) {
|
|
1762
|
-
return this.meta.status(params);
|
|
1763
|
-
}
|
|
1764
|
-
async trustRoots(params) {
|
|
1765
|
-
return this.meta.trustRoots(params);
|
|
1766
|
-
}
|
|
1767
2095
|
// ── 事件 ──────────────────────────────────────────
|
|
1768
2096
|
/**
|
|
1769
2097
|
* 订阅事件。
|
|
@@ -1901,7 +2229,7 @@ export class AUNClient {
|
|
|
1901
2229
|
this._gapFillDone.add(dedupKey);
|
|
1902
2230
|
try {
|
|
1903
2231
|
this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
1904
|
-
const messages = await this.
|
|
2232
|
+
const messages = await this._pullGroupV2(groupId, afterSeq, 50);
|
|
1905
2233
|
this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
1906
2234
|
}
|
|
1907
2235
|
finally {
|
|
@@ -3372,8 +3700,8 @@ export class AUNClient {
|
|
|
3372
3700
|
}
|
|
3373
3701
|
this._state = 'connected';
|
|
3374
3702
|
this._connectedAt = Date.now();
|
|
3375
|
-
await this._dispatcher.publish('
|
|
3376
|
-
state: this._state,
|
|
3703
|
+
await this._dispatcher.publish('state_change', {
|
|
3704
|
+
state: this._publicState(this._state),
|
|
3377
3705
|
gateway: gatewayUrl,
|
|
3378
3706
|
});
|
|
3379
3707
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
@@ -3383,7 +3711,7 @@ export class AUNClient {
|
|
|
3383
3711
|
this._startBackgroundTasks();
|
|
3384
3712
|
// V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
|
|
3385
3713
|
try {
|
|
3386
|
-
await this.
|
|
3714
|
+
await this._initV2Session();
|
|
3387
3715
|
}
|
|
3388
3716
|
catch (exc) {
|
|
3389
3717
|
this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
|
|
@@ -3402,6 +3730,57 @@ export class AUNClient {
|
|
|
3402
3730
|
const gateways = this._resolveGateways(params);
|
|
3403
3731
|
return gateways[0];
|
|
3404
3732
|
}
|
|
3733
|
+
async _resolveGatewayForAid(aid) {
|
|
3734
|
+
const target = String(aid ?? this._aid ?? '').trim();
|
|
3735
|
+
if (!target)
|
|
3736
|
+
throw new StateError('gateway discovery requires a loaded AID');
|
|
3737
|
+
if (this._gatewayUrl)
|
|
3738
|
+
return this._gatewayUrl;
|
|
3739
|
+
try {
|
|
3740
|
+
const getMetadata = this._keystore.getMetadata;
|
|
3741
|
+
const raw = typeof getMetadata === 'function'
|
|
3742
|
+
? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
|
|
3743
|
+
: '';
|
|
3744
|
+
if (raw) {
|
|
3745
|
+
const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
|
|
3746
|
+
if (gateway) {
|
|
3747
|
+
this._gatewayUrl = gateway;
|
|
3748
|
+
return gateway;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
catch {
|
|
3753
|
+
// 缓存读取失败不影响发现流程。
|
|
3754
|
+
}
|
|
3755
|
+
const dotIdx = target.indexOf('.');
|
|
3756
|
+
const issuerDomain = dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
|
|
3757
|
+
const portSuffix = '';
|
|
3758
|
+
const candidates = [
|
|
3759
|
+
`https://${target}${portSuffix}/.well-known/aun-gateway`,
|
|
3760
|
+
`https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`,
|
|
3761
|
+
];
|
|
3762
|
+
let lastError = null;
|
|
3763
|
+
for (const url of candidates) {
|
|
3764
|
+
try {
|
|
3765
|
+
const gateway = await this._discovery.discover(url);
|
|
3766
|
+
this._gatewayUrl = gateway;
|
|
3767
|
+
try {
|
|
3768
|
+
const setMetadata = this._keystore.setMetadata;
|
|
3769
|
+
if (typeof setMetadata === 'function') {
|
|
3770
|
+
await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
catch {
|
|
3774
|
+
// 缓存写入失败不影响连接。
|
|
3775
|
+
}
|
|
3776
|
+
return gateway;
|
|
3777
|
+
}
|
|
3778
|
+
catch (err) {
|
|
3779
|
+
lastError = err;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
throw lastError instanceof Error ? lastError : new ConnectionError(`gateway discovery failed for ${target}`);
|
|
3783
|
+
}
|
|
3405
3784
|
_resolveGateways(params) {
|
|
3406
3785
|
const topology = isJsonObject(params.topology) ? params.topology : null;
|
|
3407
3786
|
if (topology) {
|
|
@@ -3451,12 +3830,13 @@ export class AUNClient {
|
|
|
3451
3830
|
_normalizeConnectParams(params) {
|
|
3452
3831
|
const request = { ...params };
|
|
3453
3832
|
const accessToken = String(request.access_token ?? '');
|
|
3454
|
-
if (!accessToken)
|
|
3455
|
-
throw new StateError('connect requires non-empty access_token');
|
|
3456
3833
|
const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
|
|
3457
3834
|
if (!gateway)
|
|
3458
3835
|
throw new StateError('connect requires non-empty gateway');
|
|
3459
|
-
|
|
3836
|
+
if (accessToken)
|
|
3837
|
+
request.access_token = accessToken;
|
|
3838
|
+
else
|
|
3839
|
+
delete request.access_token;
|
|
3460
3840
|
request.gateway = gateway;
|
|
3461
3841
|
request.device_id = this._deviceId;
|
|
3462
3842
|
request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
|
|
@@ -3676,6 +4056,11 @@ export class AUNClient {
|
|
|
3676
4056
|
}
|
|
3677
4057
|
try {
|
|
3678
4058
|
identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
|
|
4059
|
+
// 刷新期间可能已断线,复检状态,避免写回 stale identity
|
|
4060
|
+
if (this._state !== 'connected') {
|
|
4061
|
+
scheduleRefresh();
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
3679
4064
|
this._identity = identity;
|
|
3680
4065
|
if (this._sessionParams && identity.access_token) {
|
|
3681
4066
|
this._sessionParams.access_token = identity.access_token;
|
|
@@ -3815,8 +4200,8 @@ export class AUNClient {
|
|
|
3815
4200
|
this._state = 'disconnected';
|
|
3816
4201
|
// 先停止后台任务,避免心跳/token刷新在重连期间继续触发
|
|
3817
4202
|
this._stopBackgroundTasks();
|
|
3818
|
-
await this._dispatcher.publish('
|
|
3819
|
-
state: this._state,
|
|
4203
|
+
await this._dispatcher.publish('state_change', {
|
|
4204
|
+
state: this._publicState(this._state),
|
|
3820
4205
|
error,
|
|
3821
4206
|
});
|
|
3822
4207
|
if (!this._sessionOptions.auto_reconnect)
|
|
@@ -3830,7 +4215,7 @@ export class AUNClient {
|
|
|
3830
4215
|
this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
|
|
3831
4216
|
const disconnectInfo = this._lastDisconnectInfo ?? {};
|
|
3832
4217
|
const eventPayload = {
|
|
3833
|
-
state: this._state, error, reason,
|
|
4218
|
+
state: this._publicState(this._state), error, reason,
|
|
3834
4219
|
};
|
|
3835
4220
|
// 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
|
|
3836
4221
|
const detail = disconnectInfo.detail;
|
|
@@ -3840,7 +4225,7 @@ export class AUNClient {
|
|
|
3840
4225
|
if (disconnectInfo.code !== undefined && disconnectInfo.code !== null) {
|
|
3841
4226
|
eventPayload.code = disconnectInfo.code;
|
|
3842
4227
|
}
|
|
3843
|
-
await this._dispatcher.publish('
|
|
4228
|
+
await this._dispatcher.publish('state_change', eventPayload);
|
|
3844
4229
|
return;
|
|
3845
4230
|
}
|
|
3846
4231
|
// 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
|
|
@@ -3858,34 +4243,51 @@ export class AUNClient {
|
|
|
3858
4243
|
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
|
|
3859
4244
|
// 服务端主动关闭时从 16s 起跳,避免重连风暴;网络断开从 initial_delay 起跳
|
|
3860
4245
|
let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
|
|
4246
|
+
this._retryAttempt = 0;
|
|
4247
|
+
this._retryMaxAttempts = maxAttempts;
|
|
3861
4248
|
for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
|
|
3862
4249
|
// R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
|
|
3863
4250
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
3864
4251
|
this._state = 'terminal_failed';
|
|
4252
|
+
this._nextRetryAt = null;
|
|
3865
4253
|
this._reconnectActive = false;
|
|
3866
4254
|
this._reconnectAbort = null;
|
|
3867
|
-
await this._dispatcher.publish('
|
|
3868
|
-
state: this._state,
|
|
4255
|
+
await this._dispatcher.publish('state_change', {
|
|
4256
|
+
state: this._publicState(this._state),
|
|
3869
4257
|
attempt: attempt - 1,
|
|
3870
4258
|
reason: 'max_attempts_exhausted',
|
|
3871
4259
|
});
|
|
3872
4260
|
return;
|
|
3873
4261
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
4262
|
+
// 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
|
|
4263
|
+
this._retryAttempt = attempt;
|
|
4264
|
+
const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
|
|
4265
|
+
this._nextRetryAt = new Date(Date.now() + sleepMs);
|
|
4266
|
+
this._state = 'retry_backoff';
|
|
4267
|
+
await this._dispatcher.publish('state_change', {
|
|
4268
|
+
state: this._publicState(this._state),
|
|
3877
4269
|
attempt,
|
|
4270
|
+
next_retry_at: this._nextRetryAt.getTime() / 1000,
|
|
3878
4271
|
});
|
|
3879
4272
|
try {
|
|
3880
|
-
await this._sleep(
|
|
4273
|
+
await this._sleep(sleepMs);
|
|
4274
|
+
this._nextRetryAt = null;
|
|
3881
4275
|
if (this._reconnectAbort?.signal.aborted) {
|
|
3882
4276
|
this._reconnectActive = false;
|
|
3883
4277
|
return;
|
|
3884
4278
|
}
|
|
4279
|
+
// 退避结束,进入 reconnecting 状态
|
|
4280
|
+
this._state = 'reconnecting';
|
|
4281
|
+
await this._dispatcher.publish('state_change', {
|
|
4282
|
+
state: this._publicState(this._state),
|
|
4283
|
+
attempt,
|
|
4284
|
+
});
|
|
3885
4285
|
// 重连前先 GET /health 探测,不健康则跳过本轮
|
|
3886
4286
|
if (this._gatewayUrl) {
|
|
3887
4287
|
const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
|
|
3888
4288
|
if (!healthy) {
|
|
4289
|
+
this._lastError = new Error('gateway health check failed');
|
|
4290
|
+
this._lastErrorCode = 'gateway_unhealthy';
|
|
3889
4291
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
3890
4292
|
continue;
|
|
3891
4293
|
}
|
|
@@ -3895,21 +4297,27 @@ export class AUNClient {
|
|
|
3895
4297
|
throw new StateError('missing connect params for reconnect');
|
|
3896
4298
|
}
|
|
3897
4299
|
await this._connectOnce(this._sessionParams, true);
|
|
4300
|
+
this._lastError = null;
|
|
4301
|
+
this._lastErrorCode = null;
|
|
4302
|
+
this._nextRetryAt = null;
|
|
3898
4303
|
this._reconnectActive = false;
|
|
3899
4304
|
this._reconnectAbort = null;
|
|
3900
4305
|
return;
|
|
3901
4306
|
}
|
|
3902
4307
|
catch (exc) {
|
|
4308
|
+
this._lastError = exc instanceof Error ? exc : new Error(String(exc));
|
|
4309
|
+
this._lastErrorCode = 'reconnect_failed';
|
|
3903
4310
|
await this._dispatcher.publish('connection.error', {
|
|
3904
4311
|
error: formatCaughtError(exc),
|
|
3905
4312
|
attempt,
|
|
3906
4313
|
});
|
|
3907
4314
|
if (!this._shouldRetryReconnect(exc)) {
|
|
3908
4315
|
this._state = 'terminal_failed';
|
|
4316
|
+
this._nextRetryAt = null;
|
|
3909
4317
|
this._reconnectActive = false;
|
|
3910
4318
|
this._reconnectAbort = null;
|
|
3911
|
-
await this._dispatcher.publish('
|
|
3912
|
-
state: this._state,
|
|
4319
|
+
await this._dispatcher.publish('state_change', {
|
|
4320
|
+
state: this._publicState(this._state),
|
|
3913
4321
|
error: formatCaughtError(exc),
|
|
3914
4322
|
attempt,
|
|
3915
4323
|
});
|
|
@@ -4259,7 +4667,7 @@ export class AUNClient {
|
|
|
4259
4667
|
*
|
|
4260
4668
|
* connect 成功后自动调用,可幂等手动调用。
|
|
4261
4669
|
*/
|
|
4262
|
-
async
|
|
4670
|
+
async _initV2Session() {
|
|
4263
4671
|
if (!this._aid)
|
|
4264
4672
|
return;
|
|
4265
4673
|
let identity = this._identity;
|
|
@@ -4564,7 +4972,7 @@ export class AUNClient {
|
|
|
4564
4972
|
* @param opts 可选 messageId / timestamp(与 Python 行为一致)
|
|
4565
4973
|
* @returns 服务端响应
|
|
4566
4974
|
*/
|
|
4567
|
-
async
|
|
4975
|
+
async _sendV2(to, payload, opts) {
|
|
4568
4976
|
if (!this._v2Session) {
|
|
4569
4977
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4570
4978
|
}
|
|
@@ -4608,7 +5016,7 @@ export class AUNClient {
|
|
|
4608
5016
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
4609
5017
|
* @param limit 最多拉取条数
|
|
4610
5018
|
*/
|
|
4611
|
-
async
|
|
5019
|
+
async _pullV2(afterSeq = 0, limit = 50, opts) {
|
|
4612
5020
|
if (!this._v2Session) {
|
|
4613
5021
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4614
5022
|
}
|
|
@@ -4704,7 +5112,7 @@ export class AUNClient {
|
|
|
4704
5112
|
this._saveSeqTrackerState();
|
|
4705
5113
|
}
|
|
4706
5114
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
4707
|
-
this._safeAsync(this.
|
|
5115
|
+
this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
|
|
4708
5116
|
}
|
|
4709
5117
|
}
|
|
4710
5118
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
@@ -4722,7 +5130,7 @@ export class AUNClient {
|
|
|
4722
5130
|
*
|
|
4723
5131
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
4724
5132
|
*/
|
|
4725
|
-
async
|
|
5133
|
+
async _ackV2(upToSeq) {
|
|
4726
5134
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
4727
5135
|
let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
4728
5136
|
if (seq <= 0)
|
|
@@ -4959,7 +5367,7 @@ export class AUNClient {
|
|
|
4959
5367
|
* @param opts 可选 messageId / timestamp
|
|
4960
5368
|
* @returns 服务端响应
|
|
4961
5369
|
*/
|
|
4962
|
-
async
|
|
5370
|
+
async _sendGroupV2(groupId, payload, opts) {
|
|
4963
5371
|
if (!this._v2Session) {
|
|
4964
5372
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4965
5373
|
}
|
|
@@ -5018,7 +5426,7 @@ export class AUNClient {
|
|
|
5018
5426
|
}
|
|
5019
5427
|
}
|
|
5020
5428
|
async _pullGroupV2Internal(params) {
|
|
5021
|
-
await this.
|
|
5429
|
+
await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
|
|
5022
5430
|
}
|
|
5023
5431
|
/**
|
|
5024
5432
|
* 拉取并解密 V2 Group 消息。
|
|
@@ -5027,7 +5435,7 @@ export class AUNClient {
|
|
|
5027
5435
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
5028
5436
|
* @param limit 最多拉取条数
|
|
5029
5437
|
*/
|
|
5030
|
-
async
|
|
5438
|
+
async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
|
|
5031
5439
|
if (!this._v2Session) {
|
|
5032
5440
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
5033
5441
|
}
|
|
@@ -5126,7 +5534,7 @@ export class AUNClient {
|
|
|
5126
5534
|
this._saveSeqTrackerState();
|
|
5127
5535
|
}
|
|
5128
5536
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
5129
|
-
this._safeAsync(this.
|
|
5537
|
+
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
|
|
5130
5538
|
}
|
|
5131
5539
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
5132
5540
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
@@ -5144,7 +5552,7 @@ export class AUNClient {
|
|
|
5144
5552
|
* @param groupId 群 ID
|
|
5145
5553
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
5146
5554
|
*/
|
|
5147
|
-
async
|
|
5555
|
+
async _ackGroupV2(groupId, upToSeq) {
|
|
5148
5556
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
5149
5557
|
if (!gid)
|
|
5150
5558
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
@@ -6221,7 +6629,7 @@ export class AUNClient {
|
|
|
6221
6629
|
try {
|
|
6222
6630
|
do {
|
|
6223
6631
|
this._v2PullPending = false;
|
|
6224
|
-
await this.
|
|
6632
|
+
await this._pullV2();
|
|
6225
6633
|
const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
|
|
6226
6634
|
this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
|
|
6227
6635
|
} while (this._v2PullPending);
|