@agentunion/fastaun-browser 0.3.6 → 0.4.0
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/_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 +1633 -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 +163 -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 +64 -0
- package/dist/aid-store.d.ts.map +1 -0
- package/dist/aid-store.js +855 -0
- package/dist/aid-store.js.map +1 -0
- package/dist/aid.d.ts +50 -0
- package/dist/aid.d.ts.map +1 -0
- package/dist/aid.js +106 -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 +1626 -1885
- 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 +558 -154
- package/dist/client.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 +1 -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,34 @@ 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, discoveryPort) {
|
|
258
|
+
const host = String(aid ?? '').trim();
|
|
259
|
+
if (!host)
|
|
260
|
+
return '';
|
|
261
|
+
if (discoveryPort && !host.includes(':'))
|
|
262
|
+
return `${host}:${discoveryPort}`;
|
|
263
|
+
return host;
|
|
264
|
+
}
|
|
265
|
+
async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
268
|
+
try {
|
|
269
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (controller.signal.aborted) {
|
|
273
|
+
throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
globalThis.clearTimeout(timer);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
248
281
|
/**
|
|
249
282
|
* 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
|
|
250
283
|
*
|
|
@@ -581,6 +614,22 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
|
|
|
581
614
|
affinity_ttl_ms: affinityTtlMs,
|
|
582
615
|
};
|
|
583
616
|
}
|
|
617
|
+
function assertClientOptions(value, label) {
|
|
618
|
+
if (value == null)
|
|
619
|
+
return;
|
|
620
|
+
if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
|
|
621
|
+
throw new ValidationError(`${label} must be an options object`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function clientOptionsConfig(options) {
|
|
625
|
+
const raw = { ...(options ?? {}) };
|
|
626
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
|
|
627
|
+
throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
|
|
628
|
+
}
|
|
629
|
+
delete raw.debug;
|
|
630
|
+
delete raw.protected_headers;
|
|
631
|
+
return raw;
|
|
632
|
+
}
|
|
584
633
|
/**
|
|
585
634
|
* AUN Core SDK 客户端 — 浏览器版本。
|
|
586
635
|
*
|
|
@@ -601,6 +650,8 @@ export class AUNClient {
|
|
|
601
650
|
_aid = null;
|
|
602
651
|
_identity = null;
|
|
603
652
|
_state = 'idle';
|
|
653
|
+
_currentAid = null;
|
|
654
|
+
_instanceProtectedHeaders = null;
|
|
604
655
|
_gatewayUrl = null;
|
|
605
656
|
_deviceId;
|
|
606
657
|
_slotId;
|
|
@@ -615,12 +666,6 @@ export class AUNClient {
|
|
|
615
666
|
_keystore;
|
|
616
667
|
_auth;
|
|
617
668
|
_transport;
|
|
618
|
-
/** 认证命名空间 */
|
|
619
|
-
auth;
|
|
620
|
-
/** AID 托管命名空间 */
|
|
621
|
-
custody;
|
|
622
|
-
/** 元数据命名空间(心跳、状态、信任根管理) */
|
|
623
|
-
meta;
|
|
624
669
|
// E2EE 编排状态(内存缓存)
|
|
625
670
|
_certCache = new Map();
|
|
626
671
|
// 后台任务 handle(浏览器 setInterval/setTimeout)
|
|
@@ -661,7 +706,7 @@ export class AUNClient {
|
|
|
661
706
|
_localAgentMdEtag = '';
|
|
662
707
|
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
663
708
|
_remoteAgentMdEtag = '';
|
|
664
|
-
/** 浏览器侧
|
|
709
|
+
/** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
665
710
|
_agentMdPath = '';
|
|
666
711
|
_agentMdCache = new Map();
|
|
667
712
|
_agentMdFetchInflight = new Set();
|
|
@@ -686,6 +731,14 @@ export class AUNClient {
|
|
|
686
731
|
_reconnectActive = false;
|
|
687
732
|
_reconnectAbort = null;
|
|
688
733
|
_serverKicked = false;
|
|
734
|
+
// 重连状态追踪(对齐 Python client.py)
|
|
735
|
+
_nextRetryAt = null;
|
|
736
|
+
_retryAttempt = 0;
|
|
737
|
+
_retryMaxAttempts = 0;
|
|
738
|
+
_lastError = null;
|
|
739
|
+
_lastErrorCode = null;
|
|
740
|
+
/** 对端 AID 缓存(aid string → AID 对象) */
|
|
741
|
+
_peerCache = new Map();
|
|
689
742
|
/**
|
|
690
743
|
* 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
|
|
691
744
|
* 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
|
|
@@ -699,17 +752,32 @@ export class AUNClient {
|
|
|
699
752
|
_logKeystore;
|
|
700
753
|
_logDiscovery;
|
|
701
754
|
_logEvents;
|
|
702
|
-
constructor(
|
|
703
|
-
|
|
755
|
+
constructor(first, second) {
|
|
756
|
+
if (typeof first === 'string') {
|
|
757
|
+
throw new ValidationError('AUNClient aid must be an AID object, not a string');
|
|
758
|
+
}
|
|
759
|
+
if (typeof second === 'boolean') {
|
|
760
|
+
throw new ValidationError('AUNClient debug must be passed as options.debug');
|
|
761
|
+
}
|
|
762
|
+
const inputAid = first instanceof AID ? first : null;
|
|
763
|
+
if (!inputAid && second !== undefined) {
|
|
764
|
+
throw new ValidationError('AUNClient options-only construction accepts a single options object');
|
|
765
|
+
}
|
|
766
|
+
const options = inputAid ? (second ?? {}) : (first ?? {});
|
|
767
|
+
assertClientOptions(options, 'AUNClient options');
|
|
768
|
+
const rawConfig = clientOptionsConfig(options);
|
|
769
|
+
if (inputAid)
|
|
770
|
+
rawConfig.aun_path = inputAid.aunPath;
|
|
771
|
+
const _debug = !!options?.debug;
|
|
704
772
|
this.configModel = createConfig(rawConfig);
|
|
705
|
-
const initAid =
|
|
773
|
+
const initAid = inputAid ? inputAid.aid : null;
|
|
706
774
|
this.config = {
|
|
707
775
|
aun_path: this.configModel.aunPath,
|
|
708
776
|
root_ca_path: this.configModel.rootCaPem,
|
|
709
777
|
seed_password: this.configModel.seedPassword,
|
|
710
778
|
};
|
|
711
779
|
this._agentMdPath = this._agentMdDefaultRoot();
|
|
712
|
-
this._deviceId = getDeviceId();
|
|
780
|
+
this._deviceId = (inputAid?.deviceId) || getDeviceId();
|
|
713
781
|
// Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
|
|
714
782
|
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
715
783
|
this._logger.bindDeviceId(this._deviceId);
|
|
@@ -723,7 +791,7 @@ export class AUNClient {
|
|
|
723
791
|
this._dispatcher = new EventDispatcher();
|
|
724
792
|
this._discovery = new GatewayDiscovery();
|
|
725
793
|
this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
|
|
726
|
-
this._slotId = '';
|
|
794
|
+
this._slotId = inputAid?.slotId || 'default';
|
|
727
795
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
728
796
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
729
797
|
this._auth = new AuthFlow({
|
|
@@ -746,9 +814,21 @@ export class AUNClient {
|
|
|
746
814
|
this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
|
|
747
815
|
});
|
|
748
816
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
817
|
+
if (inputAid) {
|
|
818
|
+
if (!inputAid.isPrivateKeyValid())
|
|
819
|
+
throw new StateError('AUNClient requires an AID with a valid private key');
|
|
820
|
+
this._currentAid = inputAid;
|
|
821
|
+
this._identity = {
|
|
822
|
+
aid: inputAid.aid,
|
|
823
|
+
private_key_pem: inputAid._privateKeyPem ?? '',
|
|
824
|
+
public_key_der_b64: inputAid.publicKey,
|
|
825
|
+
cert: inputAid.certPem,
|
|
826
|
+
};
|
|
827
|
+
this._state = 'disconnected';
|
|
828
|
+
}
|
|
829
|
+
if (options?.protected_headers !== undefined) {
|
|
830
|
+
this.setProtectedHeaders(options.protected_headers);
|
|
831
|
+
}
|
|
752
832
|
// 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
|
|
753
833
|
this._auth.setLogger(this._logAuth);
|
|
754
834
|
this._transport.setLogger(this._logTransport);
|
|
@@ -756,18 +836,9 @@ export class AUNClient {
|
|
|
756
836
|
if (typeof this._discovery.setLogger === 'function') {
|
|
757
837
|
this._discovery.setLogger(this._logger.for('aun_core.discovery'));
|
|
758
838
|
}
|
|
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
839
|
if (typeof this._keystore.setLogger === 'function') {
|
|
766
840
|
this._keystore.setLogger(this._logKeystore);
|
|
767
841
|
}
|
|
768
|
-
if (typeof this.meta.setLogger === 'function') {
|
|
769
|
-
this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
|
|
770
|
-
}
|
|
771
842
|
// 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
|
|
772
843
|
this._dispatcher.subscribe('_raw.message.received', (data) => {
|
|
773
844
|
this._onRawMessageReceived(data);
|
|
@@ -818,17 +889,182 @@ export class AUNClient {
|
|
|
818
889
|
get aid() {
|
|
819
890
|
return this._aid;
|
|
820
891
|
}
|
|
821
|
-
|
|
892
|
+
_setAgentMdRoot(root) {
|
|
822
893
|
const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
|
|
823
894
|
this._agentMdPath = next;
|
|
824
895
|
this._agentMdCache.clear();
|
|
825
896
|
return next;
|
|
826
897
|
}
|
|
827
|
-
|
|
828
|
-
|
|
898
|
+
async _resolveAgentMdUrl(aid) {
|
|
899
|
+
const target = String(aid ?? '').trim();
|
|
900
|
+
if (!target)
|
|
901
|
+
throw new ValidationError('agent.md requires non-empty aid');
|
|
902
|
+
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
903
|
+
if (!gatewayUrl) {
|
|
904
|
+
try {
|
|
905
|
+
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
gatewayUrl = '';
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const authority = agentMdAuthority(target, this.configModel.discoveryPort);
|
|
912
|
+
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
913
|
+
}
|
|
914
|
+
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
915
|
+
let identity = await this._auth.loadIdentityOrNone(aid);
|
|
916
|
+
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
917
|
+
identity = this._identity;
|
|
918
|
+
}
|
|
919
|
+
if (!identity) {
|
|
920
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
921
|
+
}
|
|
922
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
923
|
+
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
924
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
925
|
+
return cachedToken;
|
|
926
|
+
}
|
|
927
|
+
if (identity.refresh_token) {
|
|
928
|
+
try {
|
|
929
|
+
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
930
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
931
|
+
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
932
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
933
|
+
this._identity = refreshed;
|
|
934
|
+
return refreshedToken;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
// refresh 失败时回退到完整 authenticate。
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const result = await this._auth.authenticate(gatewayUrl, aid);
|
|
942
|
+
const token = String(result.access_token ?? '');
|
|
943
|
+
if (!token)
|
|
944
|
+
throw new StateError('authenticate did not return access_token');
|
|
945
|
+
const fallbackIdentity = {
|
|
946
|
+
...identity,
|
|
947
|
+
access_token: token,
|
|
948
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
949
|
+
};
|
|
950
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
951
|
+
if (Number.isFinite(fallbackExpiresAt))
|
|
952
|
+
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
953
|
+
this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
|
|
954
|
+
return token;
|
|
955
|
+
}
|
|
956
|
+
async _uploadAgentMd(content) {
|
|
957
|
+
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
958
|
+
if (!target)
|
|
959
|
+
throw new StateError('uploadAgentMd requires local AID');
|
|
960
|
+
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
961
|
+
this._gatewayUrl = gatewayUrl;
|
|
962
|
+
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
963
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
964
|
+
method: 'PUT',
|
|
965
|
+
headers: {
|
|
966
|
+
Authorization: `Bearer ${token}`,
|
|
967
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
968
|
+
},
|
|
969
|
+
body: content,
|
|
970
|
+
});
|
|
971
|
+
if (response.status === 404) {
|
|
972
|
+
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
973
|
+
}
|
|
974
|
+
if (!response.ok) {
|
|
975
|
+
const message = (await response.text()).trim();
|
|
976
|
+
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
977
|
+
}
|
|
978
|
+
const payload = await response.json();
|
|
979
|
+
if (!isJsonObject(payload))
|
|
980
|
+
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
981
|
+
return payload;
|
|
982
|
+
}
|
|
983
|
+
async _downloadAgentMd(aid) {
|
|
984
|
+
const target = String(aid ?? '').trim();
|
|
985
|
+
if (!target)
|
|
986
|
+
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
987
|
+
const cached = this._agentMdCache.get(target);
|
|
988
|
+
const url = await this._resolveAgentMdUrl(target);
|
|
989
|
+
const response = await fetchWithTimeout(url, {
|
|
990
|
+
method: 'GET',
|
|
991
|
+
headers: { Accept: 'text/markdown' },
|
|
992
|
+
redirect: 'follow',
|
|
993
|
+
});
|
|
994
|
+
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
995
|
+
return String(cached.text);
|
|
996
|
+
}
|
|
997
|
+
if (response.status === 404) {
|
|
998
|
+
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
999
|
+
}
|
|
1000
|
+
if (!response.ok) {
|
|
1001
|
+
const message = (await response.text()).trim();
|
|
1002
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
1003
|
+
}
|
|
1004
|
+
const text = await response.text();
|
|
1005
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1006
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1007
|
+
this._agentMdCache.set(target, {
|
|
1008
|
+
...(cached ?? {}),
|
|
1009
|
+
text,
|
|
1010
|
+
etag,
|
|
1011
|
+
lastModified,
|
|
1012
|
+
remote_etag: etag,
|
|
1013
|
+
last_modified: lastModified,
|
|
1014
|
+
});
|
|
1015
|
+
return text;
|
|
829
1016
|
}
|
|
830
|
-
|
|
831
|
-
|
|
1017
|
+
async _headAgentMd(aid) {
|
|
1018
|
+
const target = String(aid ?? '').trim();
|
|
1019
|
+
if (!target)
|
|
1020
|
+
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
1021
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
1022
|
+
method: 'HEAD',
|
|
1023
|
+
headers: { Accept: 'text/markdown' },
|
|
1024
|
+
});
|
|
1025
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1026
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1027
|
+
if (response.status === 404) {
|
|
1028
|
+
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
1029
|
+
}
|
|
1030
|
+
if (!response.ok) {
|
|
1031
|
+
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
1032
|
+
}
|
|
1033
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
1034
|
+
this._agentMdCache.set(target, {
|
|
1035
|
+
...cached,
|
|
1036
|
+
etag,
|
|
1037
|
+
lastModified,
|
|
1038
|
+
remote_etag: etag,
|
|
1039
|
+
last_modified: lastModified,
|
|
1040
|
+
});
|
|
1041
|
+
return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
|
|
1042
|
+
}
|
|
1043
|
+
async _verifyAgentMd(content, aid) {
|
|
1044
|
+
const target = String(aid ?? '').trim();
|
|
1045
|
+
if (!target)
|
|
1046
|
+
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
1047
|
+
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
1048
|
+
if (!peer) {
|
|
1049
|
+
let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
|
|
1050
|
+
if (!certPem) {
|
|
1051
|
+
certPem = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1052
|
+
}
|
|
1053
|
+
if (!certPem)
|
|
1054
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1055
|
+
peer = await AID.create({
|
|
1056
|
+
aid: target,
|
|
1057
|
+
aunPath: this.configModel.aunPath,
|
|
1058
|
+
certPem,
|
|
1059
|
+
privateKeyPem: null,
|
|
1060
|
+
certValid: true,
|
|
1061
|
+
privateKeyValid: false,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const result = await peer.verifyAgentMd(content);
|
|
1065
|
+
if (!result.ok)
|
|
1066
|
+
throw new AUNError(result.error.message);
|
|
1067
|
+
return { ...result.data, verified: result.data.status === 'verified' };
|
|
832
1068
|
}
|
|
833
1069
|
/**
|
|
834
1070
|
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
@@ -856,8 +1092,12 @@ export class AUNClient {
|
|
|
856
1092
|
if (localContent === null || localContent.length === 0) {
|
|
857
1093
|
throw new ValidationError('publishAgentMd requires local agent.md content');
|
|
858
1094
|
}
|
|
859
|
-
const
|
|
860
|
-
|
|
1095
|
+
const signedResult = await this._currentAid?.signAgentMd(localContent);
|
|
1096
|
+
if (!signedResult?.ok) {
|
|
1097
|
+
throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1098
|
+
}
|
|
1099
|
+
const signed = signedResult.data.signed;
|
|
1100
|
+
const result = await this._uploadAgentMd(signed);
|
|
861
1101
|
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
862
1102
|
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
863
1103
|
if (remoteEtag)
|
|
@@ -877,13 +1117,13 @@ export class AUNClient {
|
|
|
877
1117
|
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
878
1118
|
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
879
1119
|
*/
|
|
880
|
-
async
|
|
1120
|
+
async _fetchAgentMdCache(aid) {
|
|
881
1121
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
882
1122
|
if (!target) {
|
|
883
1123
|
throw new ValidationError('fetchAgentMd requires aid (or local AID)');
|
|
884
1124
|
}
|
|
885
|
-
const content = await this.
|
|
886
|
-
const signature = await this.
|
|
1125
|
+
const content = await this._downloadAgentMd(target);
|
|
1126
|
+
const signature = await this._verifyAgentMd(content, target);
|
|
887
1127
|
const isSelf = target === (this._aid ?? '');
|
|
888
1128
|
const localEtag = await this._agentMdContentEtag(content);
|
|
889
1129
|
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
@@ -932,7 +1172,7 @@ export class AUNClient {
|
|
|
932
1172
|
return String(this._aid ?? '').trim();
|
|
933
1173
|
}
|
|
934
1174
|
_agentMdDefaultRoot() {
|
|
935
|
-
return this._joinAgentMdPath(this.configModel.aunPath || '.', '
|
|
1175
|
+
return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
|
|
936
1176
|
}
|
|
937
1177
|
_joinAgentMdPath(base, name) {
|
|
938
1178
|
const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
|
|
@@ -1039,8 +1279,7 @@ export class AUNClient {
|
|
|
1039
1279
|
}
|
|
1040
1280
|
_agentMdAuthCacheMeta(aid) {
|
|
1041
1281
|
try {
|
|
1042
|
-
const
|
|
1043
|
-
const record = store?.get(String(aid ?? '').trim());
|
|
1282
|
+
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
1044
1283
|
return record && typeof record === 'object' ? { ...record } : {};
|
|
1045
1284
|
}
|
|
1046
1285
|
catch {
|
|
@@ -1165,7 +1404,7 @@ export class AUNClient {
|
|
|
1165
1404
|
return;
|
|
1166
1405
|
this._agentMdFetchInflight.add(target);
|
|
1167
1406
|
try {
|
|
1168
|
-
await this.
|
|
1407
|
+
await this._fetchAgentMdCache(target);
|
|
1169
1408
|
}
|
|
1170
1409
|
catch (err) {
|
|
1171
1410
|
await this._saveAgentMdRecord(target, {
|
|
@@ -1226,7 +1465,7 @@ export class AUNClient {
|
|
|
1226
1465
|
}
|
|
1227
1466
|
await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1228
1467
|
}
|
|
1229
|
-
async
|
|
1468
|
+
async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
|
|
1230
1469
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
1231
1470
|
if (!target)
|
|
1232
1471
|
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
@@ -1256,7 +1495,7 @@ export class AUNClient {
|
|
|
1256
1495
|
const now = Date.now();
|
|
1257
1496
|
let remote;
|
|
1258
1497
|
try {
|
|
1259
|
-
remote = await this.
|
|
1498
|
+
remote = await this._headAgentMd(target);
|
|
1260
1499
|
}
|
|
1261
1500
|
catch (err) {
|
|
1262
1501
|
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1310,7 +1549,116 @@ export class AUNClient {
|
|
|
1310
1549
|
}
|
|
1311
1550
|
}
|
|
1312
1551
|
get state() {
|
|
1313
|
-
return this._state;
|
|
1552
|
+
return this._publicState(this._state);
|
|
1553
|
+
}
|
|
1554
|
+
_publicState(state) {
|
|
1555
|
+
return STATE_TO_PUBLIC[state] ?? state;
|
|
1556
|
+
}
|
|
1557
|
+
get currentAid() {
|
|
1558
|
+
return this._currentAid;
|
|
1559
|
+
}
|
|
1560
|
+
get hasIdentity() {
|
|
1561
|
+
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
1562
|
+
}
|
|
1563
|
+
get canSign() {
|
|
1564
|
+
return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
|
|
1565
|
+
}
|
|
1566
|
+
get canConnect() {
|
|
1567
|
+
return this.hasIdentity && this.state !== ConnectionState.CLOSED;
|
|
1568
|
+
}
|
|
1569
|
+
get canSend() {
|
|
1570
|
+
return this.state === ConnectionState.READY;
|
|
1571
|
+
}
|
|
1572
|
+
get isReady() { return this.canSend; }
|
|
1573
|
+
get isOnline() {
|
|
1574
|
+
return this.state === ConnectionState.READY
|
|
1575
|
+
|| this.state === ConnectionState.RECONNECTING
|
|
1576
|
+
|| this.state === ConnectionState.RETRY_BACKOFF;
|
|
1577
|
+
}
|
|
1578
|
+
get isClosed() { return this.state === ConnectionState.CLOSED; }
|
|
1579
|
+
get aunPath() { return this.hasIdentity ? this._currentAid?.aunPath ?? this.configModel.aunPath : null; }
|
|
1580
|
+
/** 下次重连时间(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_at) */
|
|
1581
|
+
get nextRetryAt() {
|
|
1582
|
+
return this.state === ConnectionState.RETRY_BACKOFF ? this._nextRetryAt : null;
|
|
1583
|
+
}
|
|
1584
|
+
/** 距下次重连的剩余秒数(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_in_seconds) */
|
|
1585
|
+
get nextRetryInSeconds() {
|
|
1586
|
+
const t = this.nextRetryAt;
|
|
1587
|
+
if (t === null)
|
|
1588
|
+
return null;
|
|
1589
|
+
return Math.max(0, (t.getTime() - Date.now()) / 1000);
|
|
1590
|
+
}
|
|
1591
|
+
/** 当前重连尝试次数(对齐 Python retry_attempt) */
|
|
1592
|
+
get retryAttempt() { return this._retryAttempt; }
|
|
1593
|
+
/** 最大重连次数(0 = 无限,对齐 Python retry_max_attempts) */
|
|
1594
|
+
get retryMaxAttempts() { return this._retryMaxAttempts; }
|
|
1595
|
+
/** 最近一次错误(对齐 Python last_error) */
|
|
1596
|
+
get lastError() { return this._lastError; }
|
|
1597
|
+
/** 最近一次错误码(对齐 Python last_error_code) */
|
|
1598
|
+
get lastErrorCode() { return this._lastErrorCode; }
|
|
1599
|
+
loadIdentity(aid) {
|
|
1600
|
+
if (!aid?.isPrivateKeyValid())
|
|
1601
|
+
throw new StateError('loadIdentity requires an AID with a valid private key');
|
|
1602
|
+
const publicState = this.state;
|
|
1603
|
+
if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
|
|
1604
|
+
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
1605
|
+
}
|
|
1606
|
+
this._currentAid = aid;
|
|
1607
|
+
this._aid = aid.aid;
|
|
1608
|
+
this._identity = {
|
|
1609
|
+
aid: aid.aid,
|
|
1610
|
+
private_key_pem: aid._privateKeyPem ?? '',
|
|
1611
|
+
public_key_der_b64: aid.publicKey,
|
|
1612
|
+
cert: aid.certPem,
|
|
1613
|
+
};
|
|
1614
|
+
this._auth._aid = aid.aid;
|
|
1615
|
+
this._state = 'disconnected';
|
|
1616
|
+
this._closing = false;
|
|
1617
|
+
}
|
|
1618
|
+
setProtectedHeaders(headers) {
|
|
1619
|
+
if (!headers) {
|
|
1620
|
+
this._instanceProtectedHeaders = null;
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const cleaned = {};
|
|
1624
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1625
|
+
if (key === '_auth')
|
|
1626
|
+
continue;
|
|
1627
|
+
cleaned[String(key)] = String(value);
|
|
1628
|
+
}
|
|
1629
|
+
this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
|
|
1630
|
+
}
|
|
1631
|
+
getProtectedHeaders() {
|
|
1632
|
+
return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
|
|
1633
|
+
}
|
|
1634
|
+
cachePeer(aid) {
|
|
1635
|
+
if (!this.hasIdentity)
|
|
1636
|
+
throw new StateError('cachePeer requires a loaded identity');
|
|
1637
|
+
if (!aid.isCertValid())
|
|
1638
|
+
throw new ValidationError('cachePeer requires an AID with a valid certificate');
|
|
1639
|
+
this._peerCache.set(aid.aid, aid);
|
|
1640
|
+
return aid;
|
|
1641
|
+
}
|
|
1642
|
+
getPeer(aid) {
|
|
1643
|
+
if (!this.hasIdentity)
|
|
1644
|
+
throw new StateError('getPeer requires a loaded identity');
|
|
1645
|
+
return this._peerCache.get(String(aid ?? '').trim()) ?? null;
|
|
1646
|
+
}
|
|
1647
|
+
async lookupPeer(aid) {
|
|
1648
|
+
if (!this.hasIdentity)
|
|
1649
|
+
throw new StateError('lookupPeer requires a loaded identity');
|
|
1650
|
+
const target = String(aid ?? '').trim();
|
|
1651
|
+
if (!target)
|
|
1652
|
+
throw new ValidationError('lookupPeer requires non-empty aid');
|
|
1653
|
+
const cached = this._peerCache.get(target);
|
|
1654
|
+
if (cached)
|
|
1655
|
+
return cached;
|
|
1656
|
+
throw new NotFoundError(`peer not found in cache: ${target}`);
|
|
1657
|
+
}
|
|
1658
|
+
peers() {
|
|
1659
|
+
if (!this.hasIdentity)
|
|
1660
|
+
throw new StateError('peers requires a loaded identity');
|
|
1661
|
+
return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
1314
1662
|
}
|
|
1315
1663
|
get gatewayUrl() {
|
|
1316
1664
|
return this._gatewayUrl;
|
|
@@ -1325,36 +1673,65 @@ export class AUNClient {
|
|
|
1325
1673
|
get gatewayHealth() {
|
|
1326
1674
|
return this._discovery.lastHealthy;
|
|
1327
1675
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1676
|
+
// ── 生命周期 ──────────────────────────────────────
|
|
1677
|
+
/** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
|
|
1678
|
+
async authenticate(options = {}) {
|
|
1330
1679
|
const tStart = Date.now();
|
|
1331
|
-
this.
|
|
1680
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1681
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1682
|
+
throw new StateError('authenticate requires a loaded AID with a valid private key');
|
|
1683
|
+
}
|
|
1684
|
+
const publicState = this.state;
|
|
1685
|
+
if (publicState !== ConnectionState.STANDBY && publicState !== ConnectionState.AUTHENTICATED) {
|
|
1686
|
+
throw new StateError(`authenticate not allowed in state ${publicState}`);
|
|
1687
|
+
}
|
|
1688
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1689
|
+
throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
|
|
1690
|
+
}
|
|
1691
|
+
this._state = 'connecting';
|
|
1332
1692
|
try {
|
|
1333
|
-
const
|
|
1334
|
-
this.
|
|
1693
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1694
|
+
const result = await this._auth.authenticate(gateway, target);
|
|
1695
|
+
this._gatewayUrl = String(result.gateway ?? gateway);
|
|
1696
|
+
this._identity = await this._auth.loadIdentityOrNone(target);
|
|
1697
|
+
this._state = 'authenticated';
|
|
1698
|
+
this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
|
|
1335
1699
|
return result;
|
|
1336
1700
|
}
|
|
1337
1701
|
catch (err) {
|
|
1338
|
-
this.
|
|
1702
|
+
this._state = 'disconnected';
|
|
1703
|
+
this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1339
1704
|
throw err;
|
|
1340
1705
|
}
|
|
1341
1706
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
* 连接到 Gateway。
|
|
1345
|
-
*
|
|
1346
|
-
* @param auth - 认证参数,必须包含 access_token 和 gateway
|
|
1347
|
-
* @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
|
|
1348
|
-
*/
|
|
1349
|
-
async connect(auth, options) {
|
|
1707
|
+
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1708
|
+
async connect(options = {}) {
|
|
1350
1709
|
const tStart = Date.now();
|
|
1351
1710
|
this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
|
|
1352
|
-
if (
|
|
1711
|
+
if (arguments.length > 1) {
|
|
1712
|
+
throw new ValidationError('connect accepts a single options object');
|
|
1713
|
+
}
|
|
1714
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1715
|
+
throw new ValidationError('connect options must not include aid or token fields; load an AID object first');
|
|
1716
|
+
}
|
|
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}`);
|
|
1355
1731
|
}
|
|
1356
1732
|
this._state = 'connecting';
|
|
1357
|
-
const
|
|
1733
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1734
|
+
const params = { ...options, gateway };
|
|
1358
1735
|
const normalized = this._normalizeConnectParams(params);
|
|
1359
1736
|
this._sessionParams = normalized;
|
|
1360
1737
|
this._sessionOptions = this._buildSessionOptions(normalized);
|
|
@@ -1365,7 +1742,7 @@ export class AUNClient {
|
|
|
1365
1742
|
for (const gw of gateways) {
|
|
1366
1743
|
try {
|
|
1367
1744
|
const gwParams = { ...normalized, gateway: gw };
|
|
1368
|
-
await this._connectOnce(gwParams,
|
|
1745
|
+
await this._connectOnce(gwParams, true);
|
|
1369
1746
|
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
|
|
1370
1747
|
return;
|
|
1371
1748
|
}
|
|
@@ -1380,7 +1757,7 @@ export class AUNClient {
|
|
|
1380
1757
|
}
|
|
1381
1758
|
}
|
|
1382
1759
|
if (this._state === 'connecting' || this._state === 'authenticating') {
|
|
1383
|
-
this._state = '
|
|
1760
|
+
this._state = 'terminal_failed';
|
|
1384
1761
|
}
|
|
1385
1762
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
1386
1763
|
throw lastErr;
|
|
@@ -1402,56 +1779,9 @@ export class AUNClient {
|
|
|
1402
1779
|
}
|
|
1403
1780
|
await this._transport.close();
|
|
1404
1781
|
this._state = 'disconnected';
|
|
1405
|
-
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
1782
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
|
|
1406
1783
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
|
|
1407
1784
|
}
|
|
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
1785
|
/** 关闭连接 */
|
|
1456
1786
|
async close() {
|
|
1457
1787
|
const tStart = Date.now();
|
|
@@ -1480,7 +1810,7 @@ export class AUNClient {
|
|
|
1480
1810
|
}
|
|
1481
1811
|
await this._transport.close();
|
|
1482
1812
|
this._state = 'closed';
|
|
1483
|
-
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
1813
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
|
|
1484
1814
|
this._resetSeqTrackingState();
|
|
1485
1815
|
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
|
|
1486
1816
|
}
|
|
@@ -1515,6 +1845,10 @@ export class AUNClient {
|
|
|
1515
1845
|
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
1516
1846
|
}
|
|
1517
1847
|
const p = { ...(params ?? {}) };
|
|
1848
|
+
if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
|
|
1849
|
+
const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
|
|
1850
|
+
p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
|
|
1851
|
+
}
|
|
1518
1852
|
if (method === 'message.send' || method === 'group.send') {
|
|
1519
1853
|
this._normalizeOutboundMessagePayload(p, method);
|
|
1520
1854
|
}
|
|
@@ -1545,7 +1879,7 @@ export class AUNClient {
|
|
|
1545
1879
|
throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
1546
1880
|
}
|
|
1547
1881
|
this._clientLog.debug('call route: message.send → V2 encrypted send');
|
|
1548
|
-
return await this.
|
|
1882
|
+
return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
|
|
1549
1883
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1550
1884
|
timestamp: p.timestamp,
|
|
1551
1885
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1564,7 +1898,7 @@ export class AUNClient {
|
|
|
1564
1898
|
throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
1565
1899
|
}
|
|
1566
1900
|
this._clientLog.debug('call route: group.send → V2 encrypted send');
|
|
1567
|
-
return await this.
|
|
1901
|
+
return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
|
|
1568
1902
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1569
1903
|
timestamp: p.timestamp,
|
|
1570
1904
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1612,24 +1946,24 @@ export class AUNClient {
|
|
|
1612
1946
|
// message.pull:V2 就绪时走 V2 pull
|
|
1613
1947
|
if (method === 'message.pull' && this._v2Session) {
|
|
1614
1948
|
this._clientLog.debug('call route: message.pull → V2 pull');
|
|
1615
|
-
const messages = await this.
|
|
1949
|
+
const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
|
|
1616
1950
|
return { messages };
|
|
1617
1951
|
}
|
|
1618
1952
|
// message.ack:V2 就绪时走 V2 ack
|
|
1619
1953
|
if (method === 'message.ack' && this._v2Session) {
|
|
1620
1954
|
this._clientLog.debug('call route: message.ack → V2 ack');
|
|
1621
|
-
return await this.
|
|
1955
|
+
return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
|
|
1622
1956
|
}
|
|
1623
1957
|
// group.pull:V2 就绪时走 V2 pull
|
|
1624
1958
|
if (method === 'group.pull' && this._v2Session && p.group_id) {
|
|
1625
1959
|
this._clientLog.debug('call route: group.pull → V2 pull');
|
|
1626
|
-
const messages = await this.
|
|
1960
|
+
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
1961
|
return { messages };
|
|
1628
1962
|
}
|
|
1629
1963
|
// group.ack_messages:V2 就绪时走 V2 ack
|
|
1630
1964
|
if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
|
|
1631
1965
|
this._clientLog.debug('call route: group.ack_messages → V2 ack');
|
|
1632
|
-
return await this.
|
|
1966
|
+
return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
|
|
1633
1967
|
}
|
|
1634
1968
|
// 关键操作自动附加客户端签名
|
|
1635
1969
|
if (SIGNED_METHODS.has(method)) {
|
|
@@ -1754,16 +2088,6 @@ export class AUNClient {
|
|
|
1754
2088
|
}
|
|
1755
2089
|
return await this._transport.call(method, p);
|
|
1756
2090
|
}
|
|
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
2091
|
// ── 事件 ──────────────────────────────────────────
|
|
1768
2092
|
/**
|
|
1769
2093
|
* 订阅事件。
|
|
@@ -1901,7 +2225,7 @@ export class AUNClient {
|
|
|
1901
2225
|
this._gapFillDone.add(dedupKey);
|
|
1902
2226
|
try {
|
|
1903
2227
|
this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
1904
|
-
const messages = await this.
|
|
2228
|
+
const messages = await this._pullGroupV2(groupId, afterSeq, 50);
|
|
1905
2229
|
this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
1906
2230
|
}
|
|
1907
2231
|
finally {
|
|
@@ -3373,7 +3697,7 @@ export class AUNClient {
|
|
|
3373
3697
|
this._state = 'connected';
|
|
3374
3698
|
this._connectedAt = Date.now();
|
|
3375
3699
|
await this._dispatcher.publish('connection.state', {
|
|
3376
|
-
state: this._state,
|
|
3700
|
+
state: this._publicState(this._state),
|
|
3377
3701
|
gateway: gatewayUrl,
|
|
3378
3702
|
});
|
|
3379
3703
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
@@ -3383,7 +3707,7 @@ export class AUNClient {
|
|
|
3383
3707
|
this._startBackgroundTasks();
|
|
3384
3708
|
// V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
|
|
3385
3709
|
try {
|
|
3386
|
-
await this.
|
|
3710
|
+
await this._initV2Session();
|
|
3387
3711
|
}
|
|
3388
3712
|
catch (exc) {
|
|
3389
3713
|
this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
|
|
@@ -3402,6 +3726,57 @@ export class AUNClient {
|
|
|
3402
3726
|
const gateways = this._resolveGateways(params);
|
|
3403
3727
|
return gateways[0];
|
|
3404
3728
|
}
|
|
3729
|
+
async _resolveGatewayForAid(aid) {
|
|
3730
|
+
const target = String(aid ?? this._aid ?? '').trim();
|
|
3731
|
+
if (!target)
|
|
3732
|
+
throw new StateError('gateway discovery requires a loaded AID');
|
|
3733
|
+
if (this._gatewayUrl)
|
|
3734
|
+
return this._gatewayUrl;
|
|
3735
|
+
try {
|
|
3736
|
+
const getMetadata = this._keystore.getMetadata;
|
|
3737
|
+
const raw = typeof getMetadata === 'function'
|
|
3738
|
+
? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
|
|
3739
|
+
: '';
|
|
3740
|
+
if (raw) {
|
|
3741
|
+
const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
|
|
3742
|
+
if (gateway) {
|
|
3743
|
+
this._gatewayUrl = gateway;
|
|
3744
|
+
return gateway;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
catch {
|
|
3749
|
+
// 缓存读取失败不影响发现流程。
|
|
3750
|
+
}
|
|
3751
|
+
const dotIdx = target.indexOf('.');
|
|
3752
|
+
const issuerDomain = dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
|
|
3753
|
+
const portSuffix = this.configModel.discoveryPort ? `:${this.configModel.discoveryPort}` : '';
|
|
3754
|
+
const candidates = [
|
|
3755
|
+
`https://${target}${portSuffix}/.well-known/aun-gateway`,
|
|
3756
|
+
`https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`,
|
|
3757
|
+
];
|
|
3758
|
+
let lastError = null;
|
|
3759
|
+
for (const url of candidates) {
|
|
3760
|
+
try {
|
|
3761
|
+
const gateway = await this._discovery.discover(url);
|
|
3762
|
+
this._gatewayUrl = gateway;
|
|
3763
|
+
try {
|
|
3764
|
+
const setMetadata = this._keystore.setMetadata;
|
|
3765
|
+
if (typeof setMetadata === 'function') {
|
|
3766
|
+
await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
catch {
|
|
3770
|
+
// 缓存写入失败不影响连接。
|
|
3771
|
+
}
|
|
3772
|
+
return gateway;
|
|
3773
|
+
}
|
|
3774
|
+
catch (err) {
|
|
3775
|
+
lastError = err;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
throw lastError instanceof Error ? lastError : new ConnectionError(`gateway discovery failed for ${target}`);
|
|
3779
|
+
}
|
|
3405
3780
|
_resolveGateways(params) {
|
|
3406
3781
|
const topology = isJsonObject(params.topology) ? params.topology : null;
|
|
3407
3782
|
if (topology) {
|
|
@@ -3451,12 +3826,13 @@ export class AUNClient {
|
|
|
3451
3826
|
_normalizeConnectParams(params) {
|
|
3452
3827
|
const request = { ...params };
|
|
3453
3828
|
const accessToken = String(request.access_token ?? '');
|
|
3454
|
-
if (!accessToken)
|
|
3455
|
-
throw new StateError('connect requires non-empty access_token');
|
|
3456
3829
|
const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
|
|
3457
3830
|
if (!gateway)
|
|
3458
3831
|
throw new StateError('connect requires non-empty gateway');
|
|
3459
|
-
|
|
3832
|
+
if (accessToken)
|
|
3833
|
+
request.access_token = accessToken;
|
|
3834
|
+
else
|
|
3835
|
+
delete request.access_token;
|
|
3460
3836
|
request.gateway = gateway;
|
|
3461
3837
|
request.device_id = this._deviceId;
|
|
3462
3838
|
request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
|
|
@@ -3676,6 +4052,11 @@ export class AUNClient {
|
|
|
3676
4052
|
}
|
|
3677
4053
|
try {
|
|
3678
4054
|
identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
|
|
4055
|
+
// 刷新期间可能已断线,复检状态,避免写回 stale identity
|
|
4056
|
+
if (this._state !== 'connected') {
|
|
4057
|
+
scheduleRefresh();
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
3679
4060
|
this._identity = identity;
|
|
3680
4061
|
if (this._sessionParams && identity.access_token) {
|
|
3681
4062
|
this._sessionParams.access_token = identity.access_token;
|
|
@@ -3816,7 +4197,7 @@ export class AUNClient {
|
|
|
3816
4197
|
// 先停止后台任务,避免心跳/token刷新在重连期间继续触发
|
|
3817
4198
|
this._stopBackgroundTasks();
|
|
3818
4199
|
await this._dispatcher.publish('connection.state', {
|
|
3819
|
-
state: this._state,
|
|
4200
|
+
state: this._publicState(this._state),
|
|
3820
4201
|
error,
|
|
3821
4202
|
});
|
|
3822
4203
|
if (!this._sessionOptions.auto_reconnect)
|
|
@@ -3830,7 +4211,7 @@ export class AUNClient {
|
|
|
3830
4211
|
this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
|
|
3831
4212
|
const disconnectInfo = this._lastDisconnectInfo ?? {};
|
|
3832
4213
|
const eventPayload = {
|
|
3833
|
-
state: this._state, error, reason,
|
|
4214
|
+
state: this._publicState(this._state), error, reason,
|
|
3834
4215
|
};
|
|
3835
4216
|
// 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
|
|
3836
4217
|
const detail = disconnectInfo.detail;
|
|
@@ -3858,34 +4239,51 @@ export class AUNClient {
|
|
|
3858
4239
|
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
|
|
3859
4240
|
// 服务端主动关闭时从 16s 起跳,避免重连风暴;网络断开从 initial_delay 起跳
|
|
3860
4241
|
let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
|
|
4242
|
+
this._retryAttempt = 0;
|
|
4243
|
+
this._retryMaxAttempts = maxAttempts;
|
|
3861
4244
|
for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
|
|
3862
4245
|
// R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
|
|
3863
4246
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
3864
4247
|
this._state = 'terminal_failed';
|
|
4248
|
+
this._nextRetryAt = null;
|
|
3865
4249
|
this._reconnectActive = false;
|
|
3866
4250
|
this._reconnectAbort = null;
|
|
3867
4251
|
await this._dispatcher.publish('connection.state', {
|
|
3868
|
-
state: this._state,
|
|
4252
|
+
state: this._publicState(this._state),
|
|
3869
4253
|
attempt: attempt - 1,
|
|
3870
4254
|
reason: 'max_attempts_exhausted',
|
|
3871
4255
|
});
|
|
3872
4256
|
return;
|
|
3873
4257
|
}
|
|
3874
|
-
|
|
4258
|
+
// 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
|
|
4259
|
+
this._retryAttempt = attempt;
|
|
4260
|
+
const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
|
|
4261
|
+
this._nextRetryAt = new Date(Date.now() + sleepMs);
|
|
4262
|
+
this._state = 'retry_backoff';
|
|
3875
4263
|
await this._dispatcher.publish('connection.state', {
|
|
3876
|
-
state: this._state,
|
|
4264
|
+
state: this._publicState(this._state),
|
|
3877
4265
|
attempt,
|
|
4266
|
+
next_retry_at: this._nextRetryAt.getTime() / 1000,
|
|
3878
4267
|
});
|
|
3879
4268
|
try {
|
|
3880
|
-
await this._sleep(
|
|
4269
|
+
await this._sleep(sleepMs);
|
|
4270
|
+
this._nextRetryAt = null;
|
|
3881
4271
|
if (this._reconnectAbort?.signal.aborted) {
|
|
3882
4272
|
this._reconnectActive = false;
|
|
3883
4273
|
return;
|
|
3884
4274
|
}
|
|
4275
|
+
// 退避结束,进入 reconnecting 状态
|
|
4276
|
+
this._state = 'reconnecting';
|
|
4277
|
+
await this._dispatcher.publish('connection.state', {
|
|
4278
|
+
state: this._publicState(this._state),
|
|
4279
|
+
attempt,
|
|
4280
|
+
});
|
|
3885
4281
|
// 重连前先 GET /health 探测,不健康则跳过本轮
|
|
3886
4282
|
if (this._gatewayUrl) {
|
|
3887
4283
|
const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
|
|
3888
4284
|
if (!healthy) {
|
|
4285
|
+
this._lastError = new Error('gateway health check failed');
|
|
4286
|
+
this._lastErrorCode = 'gateway_unhealthy';
|
|
3889
4287
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
3890
4288
|
continue;
|
|
3891
4289
|
}
|
|
@@ -3895,21 +4293,27 @@ export class AUNClient {
|
|
|
3895
4293
|
throw new StateError('missing connect params for reconnect');
|
|
3896
4294
|
}
|
|
3897
4295
|
await this._connectOnce(this._sessionParams, true);
|
|
4296
|
+
this._lastError = null;
|
|
4297
|
+
this._lastErrorCode = null;
|
|
4298
|
+
this._nextRetryAt = null;
|
|
3898
4299
|
this._reconnectActive = false;
|
|
3899
4300
|
this._reconnectAbort = null;
|
|
3900
4301
|
return;
|
|
3901
4302
|
}
|
|
3902
4303
|
catch (exc) {
|
|
4304
|
+
this._lastError = exc instanceof Error ? exc : new Error(String(exc));
|
|
4305
|
+
this._lastErrorCode = 'reconnect_failed';
|
|
3903
4306
|
await this._dispatcher.publish('connection.error', {
|
|
3904
4307
|
error: formatCaughtError(exc),
|
|
3905
4308
|
attempt,
|
|
3906
4309
|
});
|
|
3907
4310
|
if (!this._shouldRetryReconnect(exc)) {
|
|
3908
4311
|
this._state = 'terminal_failed';
|
|
4312
|
+
this._nextRetryAt = null;
|
|
3909
4313
|
this._reconnectActive = false;
|
|
3910
4314
|
this._reconnectAbort = null;
|
|
3911
4315
|
await this._dispatcher.publish('connection.state', {
|
|
3912
|
-
state: this._state,
|
|
4316
|
+
state: this._publicState(this._state),
|
|
3913
4317
|
error: formatCaughtError(exc),
|
|
3914
4318
|
attempt,
|
|
3915
4319
|
});
|
|
@@ -4259,7 +4663,7 @@ export class AUNClient {
|
|
|
4259
4663
|
*
|
|
4260
4664
|
* connect 成功后自动调用,可幂等手动调用。
|
|
4261
4665
|
*/
|
|
4262
|
-
async
|
|
4666
|
+
async _initV2Session() {
|
|
4263
4667
|
if (!this._aid)
|
|
4264
4668
|
return;
|
|
4265
4669
|
let identity = this._identity;
|
|
@@ -4564,7 +4968,7 @@ export class AUNClient {
|
|
|
4564
4968
|
* @param opts 可选 messageId / timestamp(与 Python 行为一致)
|
|
4565
4969
|
* @returns 服务端响应
|
|
4566
4970
|
*/
|
|
4567
|
-
async
|
|
4971
|
+
async _sendV2(to, payload, opts) {
|
|
4568
4972
|
if (!this._v2Session) {
|
|
4569
4973
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4570
4974
|
}
|
|
@@ -4608,7 +5012,7 @@ export class AUNClient {
|
|
|
4608
5012
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
4609
5013
|
* @param limit 最多拉取条数
|
|
4610
5014
|
*/
|
|
4611
|
-
async
|
|
5015
|
+
async _pullV2(afterSeq = 0, limit = 50, opts) {
|
|
4612
5016
|
if (!this._v2Session) {
|
|
4613
5017
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4614
5018
|
}
|
|
@@ -4704,7 +5108,7 @@ export class AUNClient {
|
|
|
4704
5108
|
this._saveSeqTrackerState();
|
|
4705
5109
|
}
|
|
4706
5110
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
4707
|
-
this._safeAsync(this.
|
|
5111
|
+
this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
|
|
4708
5112
|
}
|
|
4709
5113
|
}
|
|
4710
5114
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
@@ -4722,7 +5126,7 @@ export class AUNClient {
|
|
|
4722
5126
|
*
|
|
4723
5127
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
4724
5128
|
*/
|
|
4725
|
-
async
|
|
5129
|
+
async _ackV2(upToSeq) {
|
|
4726
5130
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
4727
5131
|
let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
4728
5132
|
if (seq <= 0)
|
|
@@ -4959,7 +5363,7 @@ export class AUNClient {
|
|
|
4959
5363
|
* @param opts 可选 messageId / timestamp
|
|
4960
5364
|
* @returns 服务端响应
|
|
4961
5365
|
*/
|
|
4962
|
-
async
|
|
5366
|
+
async _sendGroupV2(groupId, payload, opts) {
|
|
4963
5367
|
if (!this._v2Session) {
|
|
4964
5368
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4965
5369
|
}
|
|
@@ -5018,7 +5422,7 @@ export class AUNClient {
|
|
|
5018
5422
|
}
|
|
5019
5423
|
}
|
|
5020
5424
|
async _pullGroupV2Internal(params) {
|
|
5021
|
-
await this.
|
|
5425
|
+
await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
|
|
5022
5426
|
}
|
|
5023
5427
|
/**
|
|
5024
5428
|
* 拉取并解密 V2 Group 消息。
|
|
@@ -5027,7 +5431,7 @@ export class AUNClient {
|
|
|
5027
5431
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
5028
5432
|
* @param limit 最多拉取条数
|
|
5029
5433
|
*/
|
|
5030
|
-
async
|
|
5434
|
+
async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
|
|
5031
5435
|
if (!this._v2Session) {
|
|
5032
5436
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
5033
5437
|
}
|
|
@@ -5126,7 +5530,7 @@ export class AUNClient {
|
|
|
5126
5530
|
this._saveSeqTrackerState();
|
|
5127
5531
|
}
|
|
5128
5532
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
5129
|
-
this._safeAsync(this.
|
|
5533
|
+
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
|
|
5130
5534
|
}
|
|
5131
5535
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
5132
5536
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
@@ -5144,7 +5548,7 @@ export class AUNClient {
|
|
|
5144
5548
|
* @param groupId 群 ID
|
|
5145
5549
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
5146
5550
|
*/
|
|
5147
|
-
async
|
|
5551
|
+
async _ackGroupV2(groupId, upToSeq) {
|
|
5148
5552
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
5149
5553
|
if (!gid)
|
|
5150
5554
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
@@ -6221,7 +6625,7 @@ export class AUNClient {
|
|
|
6221
6625
|
try {
|
|
6222
6626
|
do {
|
|
6223
6627
|
this._v2PullPending = false;
|
|
6224
|
-
await this.
|
|
6628
|
+
await this._pullV2();
|
|
6225
6629
|
const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
|
|
6226
6630
|
this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
|
|
6227
6631
|
} while (this._v2PullPending);
|