@agentunion/fastaun-browser 0.3.5 → 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.
Files changed (74) hide show
  1. package/CHANGELOG.md +14 -0
  2. 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
  3. 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
  4. package/_packed_docs/CHANGELOG.md +14 -0
  5. package/_packed_docs/INDEX.md +17 -11
  6. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -10
  7. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +134 -158
  8. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +11 -7
  9. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +98 -119
  10. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +147 -374
  11. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +153 -153
  12. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -1364
  13. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +71 -91
  14. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +76 -63
  15. package/_packed_docs/sdk/09-custody-api-manual.md +7 -6
  16. package/_packed_docs/sdk/09-meta-rpc-manual.md +13 -14
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +89 -0
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +37 -49
  19. package/_packed_docs/sdk/INDEX.md +72 -98
  20. package/_packed_docs/sdk/README.md +85 -266
  21. package/dist/aid-store.d.ts +64 -0
  22. package/dist/aid-store.d.ts.map +1 -0
  23. package/dist/aid-store.js +855 -0
  24. package/dist/aid-store.js.map +1 -0
  25. package/dist/aid.d.ts +50 -0
  26. package/dist/aid.d.ts.map +1 -0
  27. package/dist/aid.js +106 -0
  28. package/dist/aid.js.map +1 -0
  29. package/dist/auth.d.ts +17 -1
  30. package/dist/auth.d.ts.map +1 -1
  31. package/dist/auth.js +27 -4
  32. package/dist/auth.js.map +1 -1
  33. package/dist/bundle.js +1981 -2048
  34. package/dist/cert-utils.d.ts +26 -0
  35. package/dist/cert-utils.d.ts.map +1 -0
  36. package/dist/cert-utils.js +221 -0
  37. package/dist/cert-utils.js.map +1 -0
  38. package/dist/client.d.ts +93 -58
  39. package/dist/client.d.ts.map +1 -1
  40. package/dist/client.js +775 -170
  41. package/dist/client.js.map +1 -1
  42. package/dist/error-codes.d.ts +25 -0
  43. package/dist/error-codes.d.ts.map +1 -0
  44. package/dist/error-codes.js +31 -0
  45. package/dist/error-codes.js.map +1 -0
  46. package/dist/errors.d.ts +4 -0
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js +4 -0
  49. package/dist/errors.js.map +1 -1
  50. package/dist/index.d.ts +6 -6
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -5
  53. package/dist/index.js.map +1 -1
  54. package/dist/keystore/index.d.ts +1 -1
  55. package/dist/keystore/index.d.ts.map +1 -1
  56. package/dist/result.d.ts +19 -0
  57. package/dist/result.d.ts.map +1 -0
  58. package/dist/result.js +10 -0
  59. package/dist/result.js.map +1 -0
  60. package/dist/transport.d.ts +3 -0
  61. package/dist/transport.d.ts.map +1 -1
  62. package/dist/transport.js +17 -2
  63. package/dist/transport.js.map +1 -1
  64. package/dist/types.d.ts +13 -2
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js +22 -0
  67. package/dist/types.js.map +1 -1
  68. package/dist/v2/e2ee/encrypt-p2p.js +1 -1
  69. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  70. package/dist/version.d.ts +2 -0
  71. package/dist/version.d.ts.map +1 -0
  72. package/dist/version.js +5 -0
  73. package/dist/version.js.map +1 -0
  74. 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
  *
@@ -453,6 +486,85 @@ function extractV2EnvelopeFromSource(source) {
453
486
  }
454
487
  return null;
455
488
  }
489
+ function truthyBool(value) {
490
+ if (value === true || value === 1)
491
+ return true;
492
+ if (typeof value === 'string') {
493
+ const normalized = value.trim().toLowerCase();
494
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
495
+ }
496
+ return false;
497
+ }
498
+ function isEncryptedEnvelopePayload(payload) {
499
+ if (!isJsonObject(payload))
500
+ return false;
501
+ const payloadType = String(payload.type ?? '').trim();
502
+ if (payloadType.startsWith('e2ee.'))
503
+ return true;
504
+ if (!String(payload.ciphertext ?? '').trim())
505
+ return false;
506
+ return payload.nonce !== undefined
507
+ || payload.tag !== undefined
508
+ || payload.recipient !== undefined
509
+ || payload.recipients !== undefined
510
+ || payload.wrapped_key !== undefined
511
+ || payload.recipients_digest !== undefined;
512
+ }
513
+ function encryptedPushEnvelope(msg) {
514
+ if (isEncryptedEnvelopePayload(msg.payload))
515
+ return msg.payload;
516
+ if (typeof msg.envelope_json === 'string' && msg.envelope_json.trim()) {
517
+ try {
518
+ const parsed = JSON.parse(msg.envelope_json);
519
+ if (isEncryptedEnvelopePayload(parsed))
520
+ return parsed;
521
+ }
522
+ catch {
523
+ return null;
524
+ }
525
+ }
526
+ return null;
527
+ }
528
+ function isEncryptedPushMessage(msg) {
529
+ if (truthyBool(msg.encrypted))
530
+ return true;
531
+ return encryptedPushEnvelope(msg) !== null;
532
+ }
533
+ function isV2EncryptedEnvelopePayload(envelope) {
534
+ if (!envelope)
535
+ return false;
536
+ const payloadType = String(envelope.type ?? '').trim();
537
+ if (payloadType === 'e2ee.p2p_encrypted' || payloadType === 'e2ee.group_encrypted')
538
+ return true;
539
+ return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
540
+ }
541
+ function safeUndecryptablePushEvent(msg, group) {
542
+ const event = {
543
+ message_id: msg.message_id ?? null,
544
+ from: msg.from ?? null,
545
+ seq: msg.seq ?? null,
546
+ timestamp: msg.timestamp ?? msg.t_server ?? null,
547
+ device_id: msg.device_id ?? null,
548
+ slot_id: msg.slot_id ?? null,
549
+ _decrypt_error: 'encrypted push payload is not decryptable on raw push path',
550
+ _decrypt_stage: 'push_envelope',
551
+ };
552
+ if (group) {
553
+ event.group_id = msg.group_id ?? null;
554
+ }
555
+ else {
556
+ event.to = msg.to ?? null;
557
+ }
558
+ const envelope = encryptedPushEnvelope(msg);
559
+ if (envelope) {
560
+ event._envelope_type = String(envelope.type ?? '');
561
+ event._suite = String(envelope.suite ?? '');
562
+ if (isV2EncryptedEnvelopePayload(envelope)) {
563
+ attachV2EnvelopeMetadata(event, v2E2eeMeta(envelope));
564
+ }
565
+ }
566
+ return event;
567
+ }
456
568
  function metadataWithoutAuth(value) {
457
569
  if (!isJsonObject(value))
458
570
  return null;
@@ -502,6 +614,22 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
502
614
  affinity_ttl_ms: affinityTtlMs,
503
615
  };
504
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
+ }
505
633
  /**
506
634
  * AUN Core SDK 客户端 — 浏览器版本。
507
635
  *
@@ -522,6 +650,8 @@ export class AUNClient {
522
650
  _aid = null;
523
651
  _identity = null;
524
652
  _state = 'idle';
653
+ _currentAid = null;
654
+ _instanceProtectedHeaders = null;
525
655
  _gatewayUrl = null;
526
656
  _deviceId;
527
657
  _slotId;
@@ -536,12 +666,6 @@ export class AUNClient {
536
666
  _keystore;
537
667
  _auth;
538
668
  _transport;
539
- /** 认证命名空间 */
540
- auth;
541
- /** AID 托管命名空间 */
542
- custody;
543
- /** 元数据命名空间(心跳、状态、信任根管理) */
544
- meta;
545
669
  // E2EE 编排状态(内存缓存)
546
670
  _certCache = new Map();
547
671
  // 后台任务 handle(浏览器 setInterval/setTimeout)
@@ -582,7 +706,7 @@ export class AUNClient {
582
706
  _localAgentMdEtag = '';
583
707
  /** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
584
708
  _remoteAgentMdEtag = '';
585
- /** 浏览器侧 AgentMDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
709
+ /** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
586
710
  _agentMdPath = '';
587
711
  _agentMdCache = new Map();
588
712
  _agentMdFetchInflight = new Set();
@@ -607,6 +731,14 @@ export class AUNClient {
607
731
  _reconnectActive = false;
608
732
  _reconnectAbort = null;
609
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();
610
742
  /**
611
743
  * 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
612
744
  * 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
@@ -620,17 +752,32 @@ export class AUNClient {
620
752
  _logKeystore;
621
753
  _logDiscovery;
622
754
  _logEvents;
623
- constructor(config, _debug = false) {
624
- const rawConfig = config ?? {};
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;
625
772
  this.configModel = createConfig(rawConfig);
626
- const initAid = String(rawConfig.aid ?? '').trim() || null;
773
+ const initAid = inputAid ? inputAid.aid : null;
627
774
  this.config = {
628
775
  aun_path: this.configModel.aunPath,
629
776
  root_ca_path: this.configModel.rootCaPem,
630
777
  seed_password: this.configModel.seedPassword,
631
778
  };
632
779
  this._agentMdPath = this._agentMdDefaultRoot();
633
- this._deviceId = getDeviceId();
780
+ this._deviceId = (inputAid?.deviceId) || getDeviceId();
634
781
  // Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
635
782
  this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
636
783
  this._logger.bindDeviceId(this._deviceId);
@@ -644,7 +791,7 @@ export class AUNClient {
644
791
  this._dispatcher = new EventDispatcher();
645
792
  this._discovery = new GatewayDiscovery();
646
793
  this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
647
- this._slotId = '';
794
+ this._slotId = inputAid?.slotId || 'default';
648
795
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
649
796
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
650
797
  this._auth = new AuthFlow({
@@ -667,9 +814,21 @@ export class AUNClient {
667
814
  this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
668
815
  });
669
816
  });
670
- this.auth = new AuthNamespace(this);
671
- this.custody = new CustodyNamespace(this);
672
- this.meta = new MetaNamespace(this);
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
+ }
673
832
  // 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
674
833
  this._auth.setLogger(this._logAuth);
675
834
  this._transport.setLogger(this._logTransport);
@@ -677,18 +836,9 @@ export class AUNClient {
677
836
  if (typeof this._discovery.setLogger === 'function') {
678
837
  this._discovery.setLogger(this._logger.for('aun_core.discovery'));
679
838
  }
680
- if (typeof this.auth.setLogger === 'function') {
681
- this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
682
- }
683
- if (typeof this.custody.setLogger === 'function') {
684
- this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
685
- }
686
839
  if (typeof this._keystore.setLogger === 'function') {
687
840
  this._keystore.setLogger(this._logKeystore);
688
841
  }
689
- if (typeof this.meta.setLogger === 'function') {
690
- this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
691
- }
692
842
  // 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
693
843
  this._dispatcher.subscribe('_raw.message.received', (data) => {
694
844
  this._onRawMessageReceived(data);
@@ -739,17 +889,182 @@ export class AUNClient {
739
889
  get aid() {
740
890
  return this._aid;
741
891
  }
742
- setAgentMdPath(root) {
892
+ _setAgentMdRoot(root) {
743
893
  const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
744
894
  this._agentMdPath = next;
745
895
  this._agentMdCache.clear();
746
896
  return next;
747
897
  }
748
- setAgentMDPath(root) {
749
- return this.setAgentMdPath(root);
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`;
750
913
  }
751
- SetAgentMDPath(root) {
752
- return this.setAgentMdPath(root);
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;
1016
+ }
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' };
753
1068
  }
754
1069
  /**
755
1070
  * 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
@@ -777,8 +1092,12 @@ export class AUNClient {
777
1092
  if (localContent === null || localContent.length === 0) {
778
1093
  throw new ValidationError('publishAgentMd requires local agent.md content');
779
1094
  }
780
- const signed = await this.auth.signAgentMd(localContent);
781
- const result = await this.auth.uploadAgentMd(signed);
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);
782
1101
  this._localAgentMdEtag = await this._agentMdContentEtag(signed);
783
1102
  const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
784
1103
  if (remoteEtag)
@@ -798,13 +1117,13 @@ export class AUNClient {
798
1117
  * 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
799
1118
  * {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
800
1119
  */
801
- async fetchAgentMd(aid) {
1120
+ async _fetchAgentMdCache(aid) {
802
1121
  const target = String(aid ?? this._aid ?? '').trim();
803
1122
  if (!target) {
804
1123
  throw new ValidationError('fetchAgentMd requires aid (or local AID)');
805
1124
  }
806
- const content = await this.auth.downloadAgentMd(target);
807
- const signature = await this.auth.verifyAgentMd(content, { aid: target });
1125
+ const content = await this._downloadAgentMd(target);
1126
+ const signature = await this._verifyAgentMd(content, target);
808
1127
  const isSelf = target === (this._aid ?? '');
809
1128
  const localEtag = await this._agentMdContentEtag(content);
810
1129
  const cacheMeta = this._agentMdAuthCacheMeta(target);
@@ -853,7 +1172,7 @@ export class AUNClient {
853
1172
  return String(this._aid ?? '').trim();
854
1173
  }
855
1174
  _agentMdDefaultRoot() {
856
- return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AgentMDs');
1175
+ return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
857
1176
  }
858
1177
  _joinAgentMdPath(base, name) {
859
1178
  const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
@@ -960,8 +1279,7 @@ export class AUNClient {
960
1279
  }
961
1280
  _agentMdAuthCacheMeta(aid) {
962
1281
  try {
963
- const store = this.auth._agentMdCache;
964
- const record = store?.get(String(aid ?? '').trim());
1282
+ const record = this._agentMdCache.get(String(aid ?? '').trim());
965
1283
  return record && typeof record === 'object' ? { ...record } : {};
966
1284
  }
967
1285
  catch {
@@ -1086,7 +1404,7 @@ export class AUNClient {
1086
1404
  return;
1087
1405
  this._agentMdFetchInflight.add(target);
1088
1406
  try {
1089
- await this.fetchAgentMd(target);
1407
+ await this._fetchAgentMdCache(target);
1090
1408
  }
1091
1409
  catch (err) {
1092
1410
  await this._saveAgentMdRecord(target, {
@@ -1147,7 +1465,7 @@ export class AUNClient {
1147
1465
  }
1148
1466
  await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
1149
1467
  }
1150
- async checkAgentMd(aid, maxUnsyncedDays = 0) {
1468
+ async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
1151
1469
  const target = String(aid ?? this._aid ?? '').trim();
1152
1470
  if (!target)
1153
1471
  throw new ValidationError('checkAgentMd requires aid (or local AID)');
@@ -1177,7 +1495,7 @@ export class AUNClient {
1177
1495
  const now = Date.now();
1178
1496
  let remote;
1179
1497
  try {
1180
- remote = await this.auth.headAgentMd(target);
1498
+ remote = await this._headAgentMd(target);
1181
1499
  }
1182
1500
  catch (err) {
1183
1501
  await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
@@ -1231,7 +1549,116 @@ export class AUNClient {
1231
1549
  }
1232
1550
  }
1233
1551
  get state() {
1234
- 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);
1235
1662
  }
1236
1663
  get gatewayUrl() {
1237
1664
  return this._gatewayUrl;
@@ -1246,36 +1673,65 @@ export class AUNClient {
1246
1673
  get gatewayHealth() {
1247
1674
  return this._discovery.lastHealthy;
1248
1675
  }
1249
- /** 主动检查 gateway 可用性(GET /health) */
1250
- async checkGatewayHealth(gatewayUrl, timeout = 5000) {
1676
+ // ── 生命周期 ──────────────────────────────────────
1677
+ /** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
1678
+ async authenticate(options = {}) {
1251
1679
  const tStart = Date.now();
1252
- this._clientLog.debug(`checkGatewayHealth enter: gateway=${gatewayUrl} timeout=${timeout}`);
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';
1253
1692
  try {
1254
- const result = await this._discovery.checkHealth(gatewayUrl, timeout);
1255
- this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
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}`);
1256
1699
  return result;
1257
1700
  }
1258
1701
  catch (err) {
1259
- this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1702
+ this._state = 'disconnected';
1703
+ this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1260
1704
  throw err;
1261
1705
  }
1262
1706
  }
1263
- // ── 生命周期 ──────────────────────────────────────
1264
- /**
1265
- * 连接到 Gateway。
1266
- *
1267
- * @param auth - 认证参数,必须包含 access_token 和 gateway
1268
- * @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
1269
- */
1270
- async connect(auth, options) {
1707
+ /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1708
+ async connect(options = {}) {
1271
1709
  const tStart = Date.now();
1272
1710
  this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
1273
- if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
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)) {
1274
1729
  this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
1275
- throw new StateError(`connect not allowed in state ${this._state}`);
1730
+ throw new StateError(`connect not allowed in state ${publicState}`);
1276
1731
  }
1277
1732
  this._state = 'connecting';
1278
- const params = { ...auth, ...options };
1733
+ const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
1734
+ const params = { ...options, gateway };
1279
1735
  const normalized = this._normalizeConnectParams(params);
1280
1736
  this._sessionParams = normalized;
1281
1737
  this._sessionOptions = this._buildSessionOptions(normalized);
@@ -1286,7 +1742,7 @@ export class AUNClient {
1286
1742
  for (const gw of gateways) {
1287
1743
  try {
1288
1744
  const gwParams = { ...normalized, gateway: gw };
1289
- await this._connectOnce(gwParams, false);
1745
+ await this._connectOnce(gwParams, true);
1290
1746
  this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
1291
1747
  return;
1292
1748
  }
@@ -1301,7 +1757,7 @@ export class AUNClient {
1301
1757
  }
1302
1758
  }
1303
1759
  if (this._state === 'connecting' || this._state === 'authenticating') {
1304
- this._state = 'disconnected';
1760
+ this._state = 'terminal_failed';
1305
1761
  }
1306
1762
  this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
1307
1763
  throw lastErr;
@@ -1323,56 +1779,9 @@ export class AUNClient {
1323
1779
  }
1324
1780
  await this._transport.close();
1325
1781
  this._state = 'disconnected';
1326
- await this._dispatcher.publish('connection.state', { state: this._state });
1782
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
1327
1783
  this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
1328
1784
  }
1329
- /** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
1330
- async listIdentities() {
1331
- const tStart = Date.now();
1332
- this._clientLog.debug('listIdentities enter');
1333
- try {
1334
- const listFn = this._keystore.listIdentities;
1335
- if (typeof listFn !== 'function') {
1336
- this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
1337
- return [];
1338
- }
1339
- const aids = await listFn.call(this._keystore);
1340
- const summaries = [];
1341
- for (const aid of [...aids].sort()) {
1342
- const identity = await this._keystore.loadIdentity(aid);
1343
- if (!identity || !identity.private_key_pem)
1344
- continue;
1345
- const summary = { aid };
1346
- // 优先从 loadMetadata 获取
1347
- const loadMeta = this._keystore.loadMetadata;
1348
- if (typeof loadMeta === 'function') {
1349
- const md = await loadMeta.call(this._keystore, aid);
1350
- if (md && Object.keys(md).length > 0) {
1351
- summary.metadata = md;
1352
- }
1353
- }
1354
- // 回退:从 identity 中提取非核心字段
1355
- if (!summary.metadata) {
1356
- const metadata = {};
1357
- for (const [key, value] of Object.entries(identity)) {
1358
- if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
1359
- metadata[key] = value;
1360
- }
1361
- }
1362
- if (Object.keys(metadata).length > 0) {
1363
- summary.metadata = metadata;
1364
- }
1365
- }
1366
- summaries.push(summary);
1367
- }
1368
- this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
1369
- return summaries;
1370
- }
1371
- catch (err) {
1372
- this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1373
- throw err;
1374
- }
1375
- }
1376
1785
  /** 关闭连接 */
1377
1786
  async close() {
1378
1787
  const tStart = Date.now();
@@ -1401,7 +1810,7 @@ export class AUNClient {
1401
1810
  }
1402
1811
  await this._transport.close();
1403
1812
  this._state = 'closed';
1404
- await this._dispatcher.publish('connection.state', { state: this._state });
1813
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
1405
1814
  this._resetSeqTrackingState();
1406
1815
  this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
1407
1816
  }
@@ -1436,6 +1845,10 @@ export class AUNClient {
1436
1845
  throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
1437
1846
  }
1438
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
+ }
1439
1852
  if (method === 'message.send' || method === 'group.send') {
1440
1853
  this._normalizeOutboundMessagePayload(p, method);
1441
1854
  }
@@ -1466,7 +1879,7 @@ export class AUNClient {
1466
1879
  throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1467
1880
  }
1468
1881
  this._clientLog.debug('call route: message.send → V2 encrypted send');
1469
- return await this.sendV2(String(p.to ?? ''), p.payload ?? {}, {
1882
+ return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
1470
1883
  messageId: String(p.message_id ?? '') || undefined,
1471
1884
  timestamp: p.timestamp,
1472
1885
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1485,7 +1898,7 @@ export class AUNClient {
1485
1898
  throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1486
1899
  }
1487
1900
  this._clientLog.debug('call route: group.send → V2 encrypted send');
1488
- return await this.sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1901
+ return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1489
1902
  messageId: String(p.message_id ?? '') || undefined,
1490
1903
  timestamp: p.timestamp,
1491
1904
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1533,24 +1946,24 @@ export class AUNClient {
1533
1946
  // message.pull:V2 就绪时走 V2 pull
1534
1947
  if (method === 'message.pull' && this._v2Session) {
1535
1948
  this._clientLog.debug('call route: message.pull → V2 pull');
1536
- const messages = await this.pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
1949
+ const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1537
1950
  return { messages };
1538
1951
  }
1539
1952
  // message.ack:V2 就绪时走 V2 ack
1540
1953
  if (method === 'message.ack' && this._v2Session) {
1541
1954
  this._clientLog.debug('call route: message.ack → V2 ack');
1542
- return await this.ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1955
+ return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1543
1956
  }
1544
1957
  // group.pull:V2 就绪时走 V2 pull
1545
1958
  if (method === 'group.pull' && this._v2Session && p.group_id) {
1546
1959
  this._clientLog.debug('call route: group.pull → V2 pull');
1547
- const messages = await this.pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
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);
1548
1961
  return { messages };
1549
1962
  }
1550
1963
  // group.ack_messages:V2 就绪时走 V2 ack
1551
1964
  if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
1552
1965
  this._clientLog.debug('call route: group.ack_messages → V2 ack');
1553
- return await this.ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1966
+ return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1554
1967
  }
1555
1968
  // 关键操作自动附加客户端签名
1556
1969
  if (SIGNED_METHODS.has(method)) {
@@ -1651,15 +2064,29 @@ export class AUNClient {
1651
2064
  // ── Group E2EE 自动编排已移除(V2-only:由 group.v2.bootstrap 驱动)────────
1652
2065
  return result;
1653
2066
  }
1654
- // ── 便利方法 ──────────────────────────────────────
1655
- async ping(params) {
1656
- return this.meta.ping(params);
1657
- }
1658
- async status(params) {
1659
- return this.meta.status(params);
1660
- }
1661
- async trustRoots(params) {
1662
- return this.meta.trustRoots(params);
2067
+ async _callRawV2Rpc(method, params) {
2068
+ const p = { ...(params ?? {}) };
2069
+ delete p._pull_gate_locked;
2070
+ delete p._skip_auto_ack;
2071
+ delete p.skip_auto_ack;
2072
+ if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
2073
+ p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
2074
+ }
2075
+ if (method.startsWith('group.') && p.device_id === undefined) {
2076
+ p.device_id = this._deviceId;
2077
+ }
2078
+ if (method.startsWith('group.') && p.slot_id === undefined) {
2079
+ p.slot_id = this._slotId;
2080
+ }
2081
+ if (SIGNED_METHODS.has(method)) {
2082
+ if (this._shouldSkipClientSignature(method, p)) {
2083
+ delete p.client_signature;
2084
+ }
2085
+ else {
2086
+ await this._signClientOperation(method, p);
2087
+ }
2088
+ }
2089
+ return await this._transport.call(method, p);
1663
2090
  }
1664
2091
  // ── 事件 ──────────────────────────────────────────
1665
2092
  /**
@@ -1695,12 +2122,19 @@ export class AUNClient {
1695
2122
  }
1696
2123
  // P2P 空洞检测
1697
2124
  const seq = msg.seq;
2125
+ const encryptedPush = isEncryptedPushMessage(msg);
1698
2126
  if (seq !== undefined && seq !== null && this._aid) {
1699
2127
  const ns = `p2p:${this._aid}`;
1700
2128
  // Push 修上界:先更新 maxSeenSeq
1701
2129
  if (seq > 0)
1702
2130
  this._seqTracker.updateMaxSeen(ns, seq);
1703
- const needPull = this._seqTracker.onMessageSeq(ns, seq);
2131
+ const contigBefore = this._seqTracker.getContiguousSeq(ns);
2132
+ const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
2133
+ const published = encryptedPush
2134
+ ? await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
2135
+ : await this._publishOrderedMessage('message.received', ns, seq, msg);
2136
+ const contigAfter = this._seqTracker.getContiguousSeq(ns);
2137
+ const needPull = seqNeedsPull && !published;
1704
2138
  if (needPull) {
1705
2139
  this._safeAsync(this._fillP2pGap());
1706
2140
  }
@@ -1716,14 +2150,16 @@ export class AUNClient {
1716
2150
  }).catch((e) => { this._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
1717
2151
  }
1718
2152
  // 即时持久化 cursor,异常断连后不回退
1719
- this._saveSeqTrackerState();
1720
- }
1721
- // 明文消息直接透传
1722
- if (seq !== undefined && seq !== null && this._aid) {
1723
- const ns = `p2p:${this._aid}`;
1724
- await this._publishOrderedMessage('message.received', ns, seq, msg);
2153
+ if (contigAfter !== contigBefore)
2154
+ this._saveSeqTrackerState();
2155
+ if (encryptedPush)
2156
+ return;
1725
2157
  }
1726
2158
  else {
2159
+ if (encryptedPush) {
2160
+ await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
2161
+ return;
2162
+ }
1727
2163
  await this._publishAppEvent('message.received', msg);
1728
2164
  }
1729
2165
  }
@@ -1789,7 +2225,7 @@ export class AUNClient {
1789
2225
  this._gapFillDone.add(dedupKey);
1790
2226
  try {
1791
2227
  this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
1792
- const messages = await this.pullGroupV2(groupId, afterSeq, 50);
2228
+ const messages = await this._pullGroupV2(groupId, afterSeq, 50);
1793
2229
  this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
1794
2230
  }
1795
2231
  finally {
@@ -1830,12 +2266,19 @@ export class AUNClient {
1830
2266
  return;
1831
2267
  }
1832
2268
  // seq 跟踪 + auto-ack
2269
+ const encryptedPush = isEncryptedPushMessage(msg);
1833
2270
  if (groupId && seq !== undefined && seq !== null) {
1834
2271
  const ns = `group:${groupId}`;
1835
2272
  // Push 修上界:先更新 maxSeenSeq
1836
2273
  if (seq > 0)
1837
2274
  this._seqTracker.updateMaxSeen(ns, seq);
1838
- const needPull = this._seqTracker.onMessageSeq(ns, seq);
2275
+ const contigBefore = this._seqTracker.getContiguousSeq(ns);
2276
+ const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
2277
+ const published = encryptedPush
2278
+ ? await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
2279
+ : await this._publishOrderedMessage('group.message_created', ns, seq, msg);
2280
+ const contigAfter = this._seqTracker.getContiguousSeq(ns);
2281
+ const needPull = seqNeedsPull && !published;
1839
2282
  if (needPull) {
1840
2283
  this._safeAsync(this._fillGroupGap(groupId));
1841
2284
  }
@@ -1850,14 +2293,16 @@ export class AUNClient {
1850
2293
  slot_id: this._slotId,
1851
2294
  }).catch((e) => { this._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
1852
2295
  }
1853
- this._saveSeqTrackerState();
1854
- }
1855
- // 明文消息直接透传
1856
- if (groupId && seq !== undefined && seq !== null) {
1857
- const nsKey = `group:${groupId}`;
1858
- await this._publishOrderedMessage('group.message_created', nsKey, seq, msg);
2296
+ if (contigAfter !== contigBefore)
2297
+ this._saveSeqTrackerState();
2298
+ if (encryptedPush)
2299
+ return;
1859
2300
  }
1860
2301
  else {
2302
+ if (encryptedPush) {
2303
+ await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
2304
+ return;
2305
+ }
1861
2306
  await this._publishAppEvent('group.message_created', msg);
1862
2307
  }
1863
2308
  }
@@ -1878,6 +2323,59 @@ export class AUNClient {
1878
2323
  }
1879
2324
  }
1880
2325
  }
2326
+ async _decryptEncryptedPushPayload(msg, group) {
2327
+ const envelope = encryptedPushEnvelope(msg);
2328
+ if (!isV2EncryptedEnvelopePayload(envelope))
2329
+ return null;
2330
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
2331
+ const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? '').trim();
2332
+ const plaintext = await this._decryptV2EnvelopeForThought({ envelope, fromAid });
2333
+ if (!plaintext)
2334
+ return null;
2335
+ const e2eeMeta = v2E2eeMeta(envelope);
2336
+ const result = {
2337
+ message_id: String(msg.message_id ?? ''),
2338
+ from: fromAid,
2339
+ seq: msg.seq ?? null,
2340
+ timestamp: msg.t_server ?? msg.timestamp ?? null,
2341
+ payload: plaintext,
2342
+ encrypted: true,
2343
+ e2ee: e2eeMeta,
2344
+ };
2345
+ result.direction = fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound';
2346
+ if (msg.t_server !== undefined)
2347
+ result.t_server = msg.t_server;
2348
+ if (msg.device_id !== undefined)
2349
+ result.device_id = msg.device_id;
2350
+ if (msg.slot_id !== undefined)
2351
+ result.slot_id = msg.slot_id;
2352
+ if (group) {
2353
+ result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
2354
+ }
2355
+ else {
2356
+ result.to = msg.to ?? this._aid ?? '';
2357
+ }
2358
+ attachV2EnvelopeMetadata(result, e2eeMeta);
2359
+ return result;
2360
+ }
2361
+ async _publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
2362
+ const safeEvent = safeUndecryptablePushEvent(msg, group);
2363
+ if (ns) {
2364
+ return this._publishOrderedMessage(event, ns, seq, safeEvent);
2365
+ }
2366
+ await this._publishAppEvent(event, safeEvent);
2367
+ return true;
2368
+ }
2369
+ async _publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
2370
+ const decrypted = await this._decryptEncryptedPushPayload(msg, group);
2371
+ if (decrypted) {
2372
+ if (ns)
2373
+ return this._publishOrderedMessage(normalEvent, ns, seq, decrypted);
2374
+ await this._publishAppEvent(normalEvent, decrypted);
2375
+ return true;
2376
+ }
2377
+ return this._publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
2378
+ }
1881
2379
  /** 收到不带 payload 的 group.message_created 通知后,自动 pull 最新消息 */
1882
2380
  async _autoPullGroupMessages(notification) {
1883
2381
  const groupId = (notification.group_id ?? '');
@@ -3199,7 +3697,7 @@ export class AUNClient {
3199
3697
  this._state = 'connected';
3200
3698
  this._connectedAt = Date.now();
3201
3699
  await this._dispatcher.publish('connection.state', {
3202
- state: this._state,
3700
+ state: this._publicState(this._state),
3203
3701
  gateway: gatewayUrl,
3204
3702
  });
3205
3703
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
@@ -3209,7 +3707,7 @@ export class AUNClient {
3209
3707
  this._startBackgroundTasks();
3210
3708
  // V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3211
3709
  try {
3212
- await this.initV2Session();
3710
+ await this._initV2Session();
3213
3711
  }
3214
3712
  catch (exc) {
3215
3713
  this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
@@ -3228,6 +3726,57 @@ export class AUNClient {
3228
3726
  const gateways = this._resolveGateways(params);
3229
3727
  return gateways[0];
3230
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
+ }
3231
3780
  _resolveGateways(params) {
3232
3781
  const topology = isJsonObject(params.topology) ? params.topology : null;
3233
3782
  if (topology) {
@@ -3277,12 +3826,13 @@ export class AUNClient {
3277
3826
  _normalizeConnectParams(params) {
3278
3827
  const request = { ...params };
3279
3828
  const accessToken = String(request.access_token ?? '');
3280
- if (!accessToken)
3281
- throw new StateError('connect requires non-empty access_token');
3282
3829
  const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
3283
3830
  if (!gateway)
3284
3831
  throw new StateError('connect requires non-empty gateway');
3285
- request.access_token = accessToken;
3832
+ if (accessToken)
3833
+ request.access_token = accessToken;
3834
+ else
3835
+ delete request.access_token;
3286
3836
  request.gateway = gateway;
3287
3837
  request.device_id = this._deviceId;
3288
3838
  request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
@@ -3502,6 +4052,11 @@ export class AUNClient {
3502
4052
  }
3503
4053
  try {
3504
4054
  identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
4055
+ // 刷新期间可能已断线,复检状态,避免写回 stale identity
4056
+ if (this._state !== 'connected') {
4057
+ scheduleRefresh();
4058
+ return;
4059
+ }
3505
4060
  this._identity = identity;
3506
4061
  if (this._sessionParams && identity.access_token) {
3507
4062
  this._sessionParams.access_token = identity.access_token;
@@ -3642,7 +4197,7 @@ export class AUNClient {
3642
4197
  // 先停止后台任务,避免心跳/token刷新在重连期间继续触发
3643
4198
  this._stopBackgroundTasks();
3644
4199
  await this._dispatcher.publish('connection.state', {
3645
- state: this._state,
4200
+ state: this._publicState(this._state),
3646
4201
  error,
3647
4202
  });
3648
4203
  if (!this._sessionOptions.auto_reconnect)
@@ -3656,7 +4211,7 @@ export class AUNClient {
3656
4211
  this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
3657
4212
  const disconnectInfo = this._lastDisconnectInfo ?? {};
3658
4213
  const eventPayload = {
3659
- state: this._state, error, reason,
4214
+ state: this._publicState(this._state), error, reason,
3660
4215
  };
3661
4216
  // 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
3662
4217
  const detail = disconnectInfo.detail;
@@ -3684,34 +4239,51 @@ export class AUNClient {
3684
4239
  const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
3685
4240
  // 服务端主动关闭时从 16s 起跳,避免重连风暴;网络断开从 initial_delay 起跳
3686
4241
  let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
4242
+ this._retryAttempt = 0;
4243
+ this._retryMaxAttempts = maxAttempts;
3687
4244
  for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
3688
4245
  // R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
3689
4246
  if (maxAttempts > 0 && attempt > maxAttempts) {
3690
4247
  this._state = 'terminal_failed';
4248
+ this._nextRetryAt = null;
3691
4249
  this._reconnectActive = false;
3692
4250
  this._reconnectAbort = null;
3693
4251
  await this._dispatcher.publish('connection.state', {
3694
- state: this._state,
4252
+ state: this._publicState(this._state),
3695
4253
  attempt: attempt - 1,
3696
4254
  reason: 'max_attempts_exhausted',
3697
4255
  });
3698
4256
  return;
3699
4257
  }
3700
- this._state = 'reconnecting';
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';
3701
4263
  await this._dispatcher.publish('connection.state', {
3702
- state: this._state,
4264
+ state: this._publicState(this._state),
3703
4265
  attempt,
4266
+ next_retry_at: this._nextRetryAt.getTime() / 1000,
3704
4267
  });
3705
4268
  try {
3706
- await this._sleep(reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000);
4269
+ await this._sleep(sleepMs);
4270
+ this._nextRetryAt = null;
3707
4271
  if (this._reconnectAbort?.signal.aborted) {
3708
4272
  this._reconnectActive = false;
3709
4273
  return;
3710
4274
  }
4275
+ // 退避结束,进入 reconnecting 状态
4276
+ this._state = 'reconnecting';
4277
+ await this._dispatcher.publish('connection.state', {
4278
+ state: this._publicState(this._state),
4279
+ attempt,
4280
+ });
3711
4281
  // 重连前先 GET /health 探测,不健康则跳过本轮
3712
4282
  if (this._gatewayUrl) {
3713
4283
  const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
3714
4284
  if (!healthy) {
4285
+ this._lastError = new Error('gateway health check failed');
4286
+ this._lastErrorCode = 'gateway_unhealthy';
3715
4287
  delay = Math.min(delay * 2, maxBaseDelay);
3716
4288
  continue;
3717
4289
  }
@@ -3721,21 +4293,27 @@ export class AUNClient {
3721
4293
  throw new StateError('missing connect params for reconnect');
3722
4294
  }
3723
4295
  await this._connectOnce(this._sessionParams, true);
4296
+ this._lastError = null;
4297
+ this._lastErrorCode = null;
4298
+ this._nextRetryAt = null;
3724
4299
  this._reconnectActive = false;
3725
4300
  this._reconnectAbort = null;
3726
4301
  return;
3727
4302
  }
3728
4303
  catch (exc) {
4304
+ this._lastError = exc instanceof Error ? exc : new Error(String(exc));
4305
+ this._lastErrorCode = 'reconnect_failed';
3729
4306
  await this._dispatcher.publish('connection.error', {
3730
4307
  error: formatCaughtError(exc),
3731
4308
  attempt,
3732
4309
  });
3733
4310
  if (!this._shouldRetryReconnect(exc)) {
3734
4311
  this._state = 'terminal_failed';
4312
+ this._nextRetryAt = null;
3735
4313
  this._reconnectActive = false;
3736
4314
  this._reconnectAbort = null;
3737
4315
  await this._dispatcher.publish('connection.state', {
3738
- state: this._state,
4316
+ state: this._publicState(this._state),
3739
4317
  error: formatCaughtError(exc),
3740
4318
  attempt,
3741
4319
  });
@@ -4085,10 +4663,30 @@ export class AUNClient {
4085
4663
  *
4086
4664
  * connect 成功后自动调用,可幂等手动调用。
4087
4665
  */
4088
- async initV2Session() {
4666
+ async _initV2Session() {
4089
4667
  if (!this._aid)
4090
4668
  return;
4091
- const identity = this._identity;
4669
+ let identity = this._identity;
4670
+ if (!identity?.private_key_pem) {
4671
+ // fallback:缓存的 identity 可能被 instance_state 污染,重新从 keystore 加载
4672
+ try {
4673
+ const reloaded = await this._keystore.loadIdentity(this._aid);
4674
+ if (reloaded?.private_key_pem) {
4675
+ this._identity = reloaded;
4676
+ identity = reloaded;
4677
+ this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
4678
+ // 自愈:重新持久化,清理 instance_state 中的脏数据
4679
+ try {
4680
+ const persistIdentity = this._auth._persistIdentity;
4681
+ if (typeof persistIdentity === 'function') {
4682
+ await persistIdentity.call(this._auth, reloaded);
4683
+ }
4684
+ }
4685
+ catch { /* best-effort */ }
4686
+ }
4687
+ }
4688
+ catch { /* ignore */ }
4689
+ }
4092
4690
  if (!identity?.private_key_pem) {
4093
4691
  this._clientLog.warn('V2 session init skipped: no AID private key');
4094
4692
  return;
@@ -4370,7 +4968,7 @@ export class AUNClient {
4370
4968
  * @param opts 可选 messageId / timestamp(与 Python 行为一致)
4371
4969
  * @returns 服务端响应
4372
4970
  */
4373
- async sendV2(to, payload, opts) {
4971
+ async _sendV2(to, payload, opts) {
4374
4972
  if (!this._v2Session) {
4375
4973
  throw new StateError('V2 session not initialized (not connected?)');
4376
4974
  }
@@ -4414,20 +5012,21 @@ export class AUNClient {
4414
5012
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
4415
5013
  * @param limit 最多拉取条数
4416
5014
  */
4417
- async pullV2(afterSeq = 0, limit = 50) {
5015
+ async _pullV2(afterSeq = 0, limit = 50, opts) {
4418
5016
  if (!this._v2Session) {
4419
5017
  throw new StateError('V2 session not initialized (not connected?)');
4420
5018
  }
4421
5019
  const ns = this._aid ? `p2p:${this._aid}` : '';
4422
5020
  const decrypted = [];
4423
- let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
5021
+ let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
4424
5022
  let pageCount = 0;
4425
5023
  const maxPages = 100;
4426
5024
  while (pageCount < maxPages) {
4427
5025
  pageCount += 1;
4428
- const result = await this.call('message.v2.pull', {
5026
+ const result = await this._callRawV2Rpc('message.v2.pull', {
4429
5027
  after_seq: nextAfterSeq,
4430
5028
  limit,
5029
+ ...(opts?.force ? { force: true } : {}),
4431
5030
  });
4432
5031
  const messages = (Array.isArray(result?.messages) ? result.messages : []);
4433
5032
  const seqs = messages
@@ -4509,7 +5108,7 @@ export class AUNClient {
4509
5108
  this._saveSeqTrackerState();
4510
5109
  }
4511
5110
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
4512
- this._safeAsync(this.ackV2(ackSeq).then(() => undefined));
5111
+ this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
4513
5112
  }
4514
5113
  }
4515
5114
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
@@ -4527,7 +5126,7 @@ export class AUNClient {
4527
5126
  *
4528
5127
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
4529
5128
  */
4530
- async ackV2(upToSeq) {
5129
+ async _ackV2(upToSeq) {
4531
5130
  const ns = this._aid ? `p2p:${this._aid}` : '';
4532
5131
  let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
4533
5132
  if (seq <= 0)
@@ -4747,6 +5346,12 @@ export class AUNClient {
4747
5346
  encrypted: true,
4748
5347
  e2ee: e2ee,
4749
5348
  };
5349
+ const explicitDirection = String(msg.direction ?? '').trim();
5350
+ result.direction = explicitDirection || (fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound');
5351
+ if (msg.device_id !== undefined)
5352
+ result.device_id = msg.device_id;
5353
+ if (msg.slot_id !== undefined)
5354
+ result.slot_id = msg.slot_id;
4750
5355
  attachV2EnvelopeMetadata(result, e2ee);
4751
5356
  return result;
4752
5357
  }
@@ -4758,7 +5363,7 @@ export class AUNClient {
4758
5363
  * @param opts 可选 messageId / timestamp
4759
5364
  * @returns 服务端响应
4760
5365
  */
4761
- async sendGroupV2(groupId, payload, opts) {
5366
+ async _sendGroupV2(groupId, payload, opts) {
4762
5367
  if (!this._v2Session) {
4763
5368
  throw new StateError('V2 session not initialized (not connected?)');
4764
5369
  }
@@ -4817,7 +5422,7 @@ export class AUNClient {
4817
5422
  }
4818
5423
  }
4819
5424
  async _pullGroupV2Internal(params) {
4820
- await this.pullGroupV2(params.group_id, params.after_seq, params.limit);
5425
+ await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
4821
5426
  }
4822
5427
  /**
4823
5428
  * 拉取并解密 V2 Group 消息。
@@ -4826,7 +5431,7 @@ export class AUNClient {
4826
5431
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
4827
5432
  * @param limit 最多拉取条数
4828
5433
  */
4829
- async pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5434
+ async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
4830
5435
  if (!this._v2Session) {
4831
5436
  throw new StateError('V2 session not initialized (not connected?)');
4832
5437
  }
@@ -4925,7 +5530,7 @@ export class AUNClient {
4925
5530
  this._saveSeqTrackerState();
4926
5531
  }
4927
5532
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
4928
- this._safeAsync(this.ackGroupV2(gid, ackSeq).then(() => undefined));
5533
+ this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
4929
5534
  }
4930
5535
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
4931
5536
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
@@ -4943,7 +5548,7 @@ export class AUNClient {
4943
5548
  * @param groupId 群 ID
4944
5549
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
4945
5550
  */
4946
- async ackGroupV2(groupId, upToSeq) {
5551
+ async _ackGroupV2(groupId, upToSeq) {
4947
5552
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
4948
5553
  if (!gid)
4949
5554
  throw new ValidationError('group.ack_messages requires group_id');
@@ -6020,7 +6625,7 @@ export class AUNClient {
6020
6625
  try {
6021
6626
  do {
6022
6627
  this._v2PullPending = false;
6023
- await this.pullV2();
6628
+ await this._pullV2();
6024
6629
  const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
6025
6630
  this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
6026
6631
  } while (this._v2PullPending);