@agentunion/fastaun 0.4.5 → 0.4.6

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 (45) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/_packed_docs/CHANGELOG.md +26 -0
  3. package/_packed_docs/INDEX.md +2 -2
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +73 -84
  6. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +15 -14
  7. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +2 -2
  8. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -5
  9. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +42 -26
  10. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  11. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +61 -35
  12. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  13. package/_packed_docs/sdk/09-message-rpc-manual.md +6 -6
  14. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +6 -4
  15. package/_packed_docs/sdk/INDEX.md +2 -2
  16. package/_packed_docs/sdk/README.md +3 -3
  17. package/dist/agent-md.d.ts +101 -0
  18. package/dist/agent-md.js +778 -0
  19. package/dist/agent-md.js.map +1 -0
  20. package/dist/aid-store.d.ts +6 -39
  21. package/dist/aid-store.js +52 -142
  22. package/dist/aid-store.js.map +1 -1
  23. package/dist/auth.js +1 -1
  24. package/dist/auth.js.map +1 -1
  25. package/dist/client.d.ts +2 -62
  26. package/dist/client.js +140 -825
  27. package/dist/client.js.map +1 -1
  28. package/dist/crypto.d.ts +1 -1
  29. package/dist/crypto.js +1 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +2 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/keystore/index.d.ts +6 -2
  34. package/dist/keystore/local-identity-store.d.ts +70 -0
  35. package/dist/keystore/local-identity-store.js +525 -0
  36. package/dist/keystore/local-identity-store.js.map +1 -0
  37. package/dist/keystore/local-token-store.d.ts +68 -0
  38. package/dist/keystore/local-token-store.js +368 -0
  39. package/dist/keystore/local-token-store.js.map +1 -0
  40. package/dist/register-flow.d.ts +12 -4
  41. package/dist/register-flow.js +70 -3
  42. package/dist/register-flow.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -11,22 +11,21 @@
11
11
  * - 群组 E2EE 全自动编排(建群/加人/踢人/退出)
12
12
  */
13
13
  import * as crypto from 'node:crypto';
14
- import * as fs from 'node:fs';
15
14
  import * as http from 'node:http';
16
15
  import * as https from 'node:https';
17
- import * as path from 'node:path';
18
16
  import { URL } from 'node:url';
19
- import { configFromMap, getDeviceId, normalizeInstanceId, normalizeSlotId, slotIsolationKey } from './config.js';
17
+ import { configFromMap, getDeviceId, normalizeSlotId, slotIsolationKey } from './config.js';
20
18
  import { CryptoProvider } from './crypto.js';
21
19
  import { GatewayDiscovery } from './discovery.js';
22
20
  import { DnsResilientNet } from './net.js';
23
21
  import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
24
22
  import { EventDispatcher } from './events.js';
25
- import { FileKeyStore } from './keystore/file.js';
23
+ import { LocalTokenStore } from './keystore/local-token-store.js';
26
24
  import { AUNLogger } from './logger.js';
27
25
  import { normalizeGroupId } from './group-id.js';
28
26
  import { RPCTransport } from './transport.js';
29
27
  import { AuthFlow } from './auth.js';
28
+ import { AgentMdManager } from './agent-md.js';
30
29
  import { SeqTracker } from './seq-tracker.js';
31
30
  import { V2Session } from './v2/session/index.js';
32
31
  import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2ee/index.js';
@@ -226,7 +225,6 @@ const SIGNED_METHODS = new Set([
226
225
  ]);
227
226
  /** peer 证书缓存 TTL(1 小时) */
228
227
  const PEER_CERT_CACHE_TTL = 3600;
229
- const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
230
228
  function normalizeV2WrapPolicy(raw) {
231
229
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
232
230
  return { explicit: false, version: '', protocol: '', scope: 'device' };
@@ -404,33 +402,93 @@ function lengthPrefixedBytesKey(...parts) {
404
402
  }
405
403
  return Buffer.concat(chunks);
406
404
  }
407
- function agentMdHttpScheme(gatewayUrl) {
408
- const raw = String(gatewayUrl ?? '').trim().toLowerCase();
409
- return raw.startsWith('ws://') ? 'http' : 'https';
410
- }
411
- function agentMdAuthority(aid, discoveryPort) {
412
- const host = String(aid ?? '').trim();
413
- if (!host)
414
- return '';
415
- if (discoveryPort && !host.includes(':'))
416
- return `${host}:${discoveryPort}`;
417
- return host;
418
- }
419
- async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
420
- const controller = new AbortController();
421
- const timer = setTimeout(() => controller.abort(), timeoutMs);
422
- try {
423
- return await fetch(input, { ...init, signal: controller.signal });
424
- }
425
- catch (error) {
426
- if (controller.signal.aborted) {
427
- throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
428
- }
429
- throw error;
430
- }
431
- finally {
432
- clearTimeout(timer);
433
- }
405
+ function createAgentMdManagerForRuntime(opts) {
406
+ return new AgentMdManager({
407
+ aunPath: opts.config().aunPath,
408
+ verifySsl: opts.config().verifySsl,
409
+ discoveryPort: opts.config().discoveryPort,
410
+ logger: opts.logger(),
411
+ ownerAidGetter: opts.ownerAid,
412
+ currentAidGetter: opts.currentAid,
413
+ gatewayResolver: async (aid) => {
414
+ const gatewayUrl = await opts.gateway.resolve(aid);
415
+ opts.gateway.set(gatewayUrl);
416
+ return gatewayUrl;
417
+ },
418
+ accessTokenResolver: async (aid, gatewayUrl) => {
419
+ const target = String(aid ?? '').trim();
420
+ let identity = opts.identity.get();
421
+ if (!identity || String(identity.aid ?? '') !== target) {
422
+ throw new StateError('no local identity found, register or load an AID first');
423
+ }
424
+ const auth = opts.auth();
425
+ const cachedToken = String(identity.access_token ?? '');
426
+ const expiresAt = auth.getAccessTokenExpiry(identity);
427
+ if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
428
+ return cachedToken;
429
+ }
430
+ if (identity.refresh_token) {
431
+ try {
432
+ const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
433
+ const refreshedToken = String(refreshed.access_token ?? '');
434
+ const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
435
+ if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
436
+ opts.identity.set(refreshed);
437
+ return refreshedToken;
438
+ }
439
+ }
440
+ catch {
441
+ // refresh 失败时回退到完整 authenticate。
442
+ }
443
+ }
444
+ const result = await auth.authenticate(gatewayUrl, { aid: target });
445
+ const token = String(result.access_token ?? '');
446
+ if (!token)
447
+ throw new StateError('authenticate did not return access_token');
448
+ identity = auth.loadIdentityOrNone(target) ?? {
449
+ ...identity,
450
+ access_token: token,
451
+ refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
452
+ access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
453
+ token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
454
+ expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
455
+ };
456
+ opts.identity.set(identity);
457
+ return token;
458
+ },
459
+ peerResolver: async (aid) => {
460
+ const target = String(aid ?? '').trim();
461
+ const current = opts.currentAid();
462
+ if (current?.aid === target)
463
+ return current;
464
+ let certPem = '';
465
+ try {
466
+ certPem = String(opts.tokenStore().loadCert(target) ?? '').trim();
467
+ }
468
+ catch {
469
+ certPem = '';
470
+ }
471
+ if (!certPem) {
472
+ if (!opts.gateway.get()) {
473
+ try {
474
+ opts.gateway.set(await opts.gateway.resolve(target));
475
+ }
476
+ catch { /* best effort before cert fetch */ }
477
+ }
478
+ certPem = String(await opts.fetchPeerCert(target) ?? '').trim();
479
+ }
480
+ if (!certPem)
481
+ throw new NotFoundError(`certificate not found for aid: ${target}`);
482
+ return AID._create({
483
+ aid: target,
484
+ aunPath: opts.config().aunPath,
485
+ certPem,
486
+ privateKeyPem: null,
487
+ certValid: true,
488
+ privateKeyValid: false,
489
+ });
490
+ },
491
+ });
434
492
  }
435
493
  export class AUNClient {
436
494
  /** 原始配置 */
@@ -479,17 +537,7 @@ export class AUNClient {
479
537
  _defaultConnectDeliveryMode;
480
538
  /** peer 证书缓存 */
481
539
  _certCache = new Map();
482
- // AIDs 目录:{agentMdPath}/{aid}/agentmd.json 保存元数据,{agentMdPath}/{aid}/agent.md 保存正文。
483
- _agentMdPath = '';
484
- _localAgentMdPath = '';
485
- _localAgentMdEtag = '';
486
- // gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。
487
- _remoteAgentMdEtag = '';
488
- _agentMdCache = new Map();
489
- _agentMdFetchInflight = new Map();
490
- _agentMdDownloadInflight = new Map();
491
- _agentMdDownloadActive = 0;
492
- _agentMdDownloadWaiters = [];
540
+ _agentMdManager;
493
541
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
494
542
  _seqTracker = new SeqTracker();
495
543
  _seqTrackerContext = null;
@@ -541,7 +589,6 @@ export class AUNClient {
541
589
  _peerCache = new Map();
542
590
  static V2_SIG_CACHE_TTL_MS = 60 * 60 * 1000;
543
591
  static V2_SIG_CACHE_MAX = 16_384;
544
- static AGENT_MD_DOWNLOAD_CONCURRENCY = 8;
545
592
  _reconnectActive = false;
546
593
  _reconnectAbort = null;
547
594
  _serverKicked = false;
@@ -564,7 +611,6 @@ export class AUNClient {
564
611
  }
565
612
  this._configModel = configFromMap(rawConfig);
566
613
  const initAid = (inputAid && inputAid.isPrivateKeyValid()) ? inputAid.aid : null;
567
- this._agentMdPath = path.join(this._configModel.aunPath, 'AIDs');
568
614
  this.config = {
569
615
  aun_path: this._configModel.aunPath,
570
616
  root_ca_path: this._configModel.rootCaPath,
@@ -588,9 +634,8 @@ export class AUNClient {
588
634
  logger: this._clientLog,
589
635
  });
590
636
  this._discovery = new GatewayDiscovery({ verifySsl: this._configModel.verifySsl, logger: this._clientLog, net: dnsNet });
591
- const tokenStore = new FileKeyStore(this._configModel.aunPath, {
637
+ const tokenStore = new LocalTokenStore(this._configModel.aunPath, {
592
638
  logger: this._logger.for('aun_core.keystore'),
593
- secretStoreLogger: this._logger.for('aun_core.secret-store'),
594
639
  });
595
640
  this._tokenStore = tokenStore;
596
641
  this._slotId = inputAid?.slotId || 'default';
@@ -608,6 +653,24 @@ export class AUNClient {
608
653
  net: dnsNet,
609
654
  });
610
655
  this._aid = initAid;
656
+ this._agentMdManager = createAgentMdManagerForRuntime({
657
+ config: () => this._configModel,
658
+ logger: () => this._logger.for('aun_core.agent_md'),
659
+ ownerAid: () => this._aid,
660
+ currentAid: () => this._currentAid,
661
+ gateway: {
662
+ resolve: (target) => this._resolveGatewayForAid(target),
663
+ get: () => this._gatewayUrl,
664
+ set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
665
+ },
666
+ identity: {
667
+ get: () => this._identity,
668
+ set: (identity) => { this._identity = identity; },
669
+ },
670
+ auth: () => this._auth,
671
+ tokenStore: () => this._tokenStore,
672
+ fetchPeerCert: (target) => this._fetchPeerCert(target),
673
+ });
611
674
  this._transport = new RPCTransport({
612
675
  eventDispatcher: this._dispatcher,
613
676
  timeout: 10_000,
@@ -728,10 +791,6 @@ export class AUNClient {
728
791
  this.config.aun_path = nextConfig.aunPath;
729
792
  this.config.root_ca_path = nextConfig.rootCaPath;
730
793
  this.config.seed_password = nextConfig.seedPassword;
731
- this._agentMdPath = path.join(nextConfig.aunPath, 'AIDs');
732
- this._agentMdCache.clear();
733
- this._agentMdFetchInflight.clear();
734
- this._agentMdDownloadInflight.clear();
735
794
  this._peerCache.clear();
736
795
  this._certCache.clear();
737
796
  this._gatewayUrl = null;
@@ -746,9 +805,8 @@ export class AUNClient {
746
805
  logger: this._clientLog,
747
806
  });
748
807
  this._discovery = new GatewayDiscovery({ verifySsl: nextConfig.verifySsl, logger: this._clientLog, net: dnsNet });
749
- const tokenStore = new FileKeyStore(nextConfig.aunPath, {
808
+ const tokenStore = new LocalTokenStore(nextConfig.aunPath, {
750
809
  logger: this._logger.for('aun_core.keystore'),
751
- secretStoreLogger: this._logger.for('aun_core.secret-store'),
752
810
  });
753
811
  this._tokenStore = tokenStore;
754
812
  this._auth = new AuthFlow({
@@ -762,6 +820,24 @@ export class AUNClient {
762
820
  logger: this._logger.for('aun_core.auth'),
763
821
  net: dnsNet,
764
822
  });
823
+ this._agentMdManager = createAgentMdManagerForRuntime({
824
+ config: () => this._configModel,
825
+ logger: () => this._logger.for('aun_core.agent_md'),
826
+ ownerAid: () => this._aid,
827
+ currentAid: () => this._currentAid,
828
+ gateway: {
829
+ resolve: (target) => this._resolveGatewayForAid(target),
830
+ get: () => this._gatewayUrl,
831
+ set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
832
+ },
833
+ identity: {
834
+ get: () => this._identity,
835
+ set: (identity) => { this._identity = identity; },
836
+ },
837
+ auth: () => this._auth,
838
+ tokenStore: () => this._tokenStore,
839
+ fetchPeerCert: (target) => this._fetchPeerCert(target),
840
+ });
765
841
  this._transport = new RPCTransport({
766
842
  eventDispatcher: this._dispatcher,
767
843
  timeout: 10_000,
@@ -848,769 +924,12 @@ export class AUNClient {
848
924
  throw new StateError('peers requires a loaded identity');
849
925
  return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
850
926
  }
851
- async _resolveAgentMdUrl(aid) {
852
- const target = String(aid ?? '').trim();
853
- if (!target)
854
- throw new ValidationError('agent.md requires non-empty aid');
855
- let gatewayUrl = String(this._gatewayUrl ?? '').trim();
856
- if (!gatewayUrl) {
857
- try {
858
- gatewayUrl = await this._resolveGatewayForAid(target);
859
- }
860
- catch {
861
- gatewayUrl = '';
862
- }
863
- }
864
- return `${agentMdHttpScheme(gatewayUrl)}://${agentMdAuthority(target, this._configModel.discoveryPort)}/agent.md`;
865
- }
866
- async _ensureAgentMdUploadToken(aid, gatewayUrl) {
867
- let identity = this._identity && String(this._identity.aid ?? '') === aid ? this._identity : null;
868
- if (!identity) {
869
- throw new StateError('no local identity found, register or load an AID first');
870
- }
871
- const cachedToken = String(identity.access_token ?? '');
872
- const expiresAt = this._auth.getAccessTokenExpiry(identity);
873
- if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
874
- return cachedToken;
875
- }
876
- if (identity.refresh_token) {
877
- try {
878
- const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
879
- const refreshedToken = String(refreshed.access_token ?? '');
880
- const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
881
- if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
882
- this._identity = refreshed;
883
- return refreshedToken;
884
- }
885
- }
886
- catch {
887
- // refresh 失败时回退到完整 authenticate。
888
- }
889
- }
890
- const result = await this._auth.authenticate(gatewayUrl, { aid });
891
- const token = String(result.access_token ?? '');
892
- if (!token)
893
- throw new StateError('authenticate did not return access_token');
894
- this._identity = this._auth.loadIdentityOrNone(aid) ?? {
895
- ...identity,
896
- access_token: token,
897
- refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
898
- access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
899
- token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
900
- expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
901
- };
902
- return token;
903
- }
904
- async _uploadAgentMd(content) {
905
- const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
906
- if (!target)
907
- throw new StateError('uploadAgentMd requires local AID');
908
- const gatewayUrl = await this._resolveGatewayForAid(target);
909
- this._gatewayUrl = gatewayUrl;
910
- const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
911
- const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
912
- method: 'PUT',
913
- headers: {
914
- Authorization: `Bearer ${token}`,
915
- 'Content-Type': 'text/markdown; charset=utf-8',
916
- },
917
- body: content,
918
- });
919
- if (response.status === 404) {
920
- throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
921
- }
922
- if (!response.ok) {
923
- const message = (await response.text()).trim();
924
- throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
925
- }
926
- const payload = await response.json();
927
- if (!isJsonObject(payload))
928
- throw new AUNError('upload agent.md returned invalid JSON payload');
929
- return payload;
930
- }
931
- async _acquireAgentMdDownloadSlot() {
932
- if (this._agentMdDownloadActive < AUNClient.AGENT_MD_DOWNLOAD_CONCURRENCY) {
933
- this._agentMdDownloadActive += 1;
934
- return () => this._releaseAgentMdDownloadSlot();
935
- }
936
- await new Promise((resolve) => {
937
- this._agentMdDownloadWaiters.push(resolve);
938
- });
939
- return () => this._releaseAgentMdDownloadSlot();
940
- }
941
- _releaseAgentMdDownloadSlot() {
942
- const next = this._agentMdDownloadWaiters.shift();
943
- if (next) {
944
- next();
945
- return;
946
- }
947
- if (this._agentMdDownloadActive > 0)
948
- this._agentMdDownloadActive -= 1;
949
- }
950
- async _downloadAgentMd(aid) {
951
- const target = String(aid ?? '').trim();
952
- if (!target)
953
- throw new ValidationError('downloadAgentMd requires non-empty aid');
954
- const existing = this._agentMdDownloadInflight.get(target);
955
- if (existing)
956
- return await existing;
957
- const task = (async () => {
958
- const release = await this._acquireAgentMdDownloadSlot();
959
- try {
960
- return await this._downloadAgentMdOnce(target);
961
- }
962
- finally {
963
- release();
964
- }
965
- })();
966
- this._agentMdDownloadInflight.set(target, task);
967
- task.finally(() => {
968
- if (this._agentMdDownloadInflight.get(target) === task) {
969
- this._agentMdDownloadInflight.delete(target);
970
- }
971
- }).catch(() => undefined);
972
- return await task;
973
- }
974
- async _downloadAgentMdOnce(target) {
975
- const cached = this._agentMdCache.get(target);
976
- const url = await this._resolveAgentMdUrl(target);
977
- let response = await fetchWithTimeout(url, {
978
- method: 'GET',
979
- headers: { Accept: 'text/markdown' },
980
- redirect: 'follow',
981
- });
982
- if (response.status === 304 && typeof cached?.text === 'string') {
983
- return String(cached.text);
984
- }
985
- if (response.status === 304) {
986
- response = await fetchWithTimeout(url, {
987
- method: 'GET',
988
- headers: { Accept: 'text/markdown' },
989
- cache: 'reload',
990
- redirect: 'follow',
991
- });
992
- }
993
- if (response.status === 404) {
994
- throw new NotFoundError(`agent.md not found for aid: ${target}`);
995
- }
996
- if (!response.ok) {
997
- const message = (await response.text()).trim();
998
- throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
999
- }
1000
- const text = await response.text();
1001
- const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
1002
- const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
1003
- this._agentMdCache.set(target, {
1004
- ...(cached ?? {}),
1005
- text,
1006
- etag,
1007
- lastModified,
1008
- remote_etag: etag,
1009
- last_modified: lastModified,
1010
- });
1011
- return text;
1012
- }
1013
- async _headAgentMd(aid) {
1014
- const target = String(aid ?? '').trim();
1015
- if (!target)
1016
- throw new ValidationError('headAgentMd requires non-empty aid');
1017
- const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
1018
- method: 'HEAD',
1019
- headers: { Accept: 'text/markdown' },
1020
- redirect: 'follow',
1021
- }, 15_000);
1022
- const cached = this._agentMdCache.get(target) ?? {};
1023
- const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
1024
- const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
1025
- if (response.status === 404) {
1026
- return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
1027
- }
1028
- const resultEtag = response.status === 304 ? (etag || String(cached.etag ?? cached.remote_etag ?? '')) : etag;
1029
- const resultLastModified = response.status === 304 ? (lastModified || String(cached.lastModified ?? cached.last_modified ?? '')) : lastModified;
1030
- if (response.status < 200 || (response.status >= 300 && response.status !== 304)) {
1031
- throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
1032
- }
1033
- this._agentMdCache.set(target, {
1034
- ...cached,
1035
- etag: resultEtag,
1036
- lastModified: resultLastModified,
1037
- remote_etag: resultEtag,
1038
- last_modified: resultLastModified,
1039
- });
1040
- return { aid: target, found: true, etag: resultEtag, last_modified: resultLastModified, status: response.status };
1041
- }
1042
- async _verifyAgentMd(content, aid, certPem) {
1043
- const target = String(aid ?? '').trim();
1044
- if (!target)
1045
- throw new ValidationError('verifyAgentMd requires non-empty aid');
1046
- let peer = target === this._currentAid?.aid ? this._currentAid : null;
1047
- if (!peer) {
1048
- let resolvedCert = String(certPem ?? '').trim();
1049
- if (!resolvedCert) {
1050
- try {
1051
- resolvedCert = String(this._tokenStore.loadCert(target) ?? '').trim();
1052
- }
1053
- catch {
1054
- resolvedCert = '';
1055
- }
1056
- }
1057
- if (!resolvedCert) {
1058
- if (!this._gatewayUrl) {
1059
- try {
1060
- this._gatewayUrl = await this._resolveGatewayForAid(target);
1061
- }
1062
- catch { /* best-effort before cert fetch */ }
1063
- }
1064
- resolvedCert = String(await this._fetchPeerCert(target) ?? '').trim();
1065
- }
1066
- if (!resolvedCert)
1067
- throw new NotFoundError(`certificate not found for aid: ${target}`);
1068
- peer = AID._create({
1069
- aid: target,
1070
- aunPath: this._configModel.aunPath,
1071
- certPem: resolvedCert,
1072
- privateKeyPem: null,
1073
- certValid: true,
1074
- privateKeyValid: false,
1075
- });
1076
- }
1077
- const result = peer.verifyAgentMd(content);
1078
- if (!result.ok)
1079
- throw new AUNError(result.error.message);
1080
- const vd = result.data;
1081
- return { ...vd, verified: vd.status === 'verified' };
1082
- }
1083
- /**
1084
- * 读取 {agentMdPath}/{self_aid}/agent.md,签名后上传,并把签名结果原子写回本地。
1085
- */
1086
- async publishAgentMd() {
1087
- const target = this._agentMdOwnerAid();
1088
- if (!target || !this._currentAid) {
1089
- throw new ValidationError('publishAgentMd requires local AID');
1090
- }
1091
- const content = this._readAgentMdContent(target);
1092
- const signed = this._currentAid?.signAgentMd(content);
1093
- if (!signed?.ok) {
1094
- throw new StateError(signed?.error.message ?? 'publishAgentMd requires a valid local AID private key');
1095
- }
1096
- const signedContent = signed.data.signed;
1097
- const result = await this._uploadAgentMd(signedContent);
1098
- this._localAgentMdEtag = this._agentMdContentEtag(signedContent);
1099
- const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
1100
- if (remoteEtag)
1101
- this._remoteAgentMdEtag = remoteEtag;
1102
- this._saveAgentMdRecord(target, {
1103
- content: signedContent,
1104
- local_etag: this._localAgentMdEtag,
1105
- remote_etag: remoteEtag || undefined,
1106
- last_modified: isJsonObject(result) ? String(result.last_modified ?? '').trim() : '',
1107
- fetched_at: Date.now(),
1108
- remote_status: remoteEtag ? 'found' : 'unknown',
1109
- last_error: '',
1110
- });
1111
- return result;
1112
- }
1113
- async _startAgentMdFetchTask(target) {
1114
- const existing = this._agentMdFetchInflight.get(target);
1115
- if (existing) {
1116
- return await existing;
1117
- }
1118
- const task = this._fetchAgentMdOnce(target);
1119
- this._agentMdFetchInflight.set(target, task);
1120
- task.finally(() => {
1121
- if (this._agentMdFetchInflight.get(target) === task) {
1122
- this._agentMdFetchInflight.delete(target);
1123
- }
1124
- }).catch(() => undefined);
1125
- return await task;
1126
- }
1127
- async _fetchAgentMdOnce(target) {
1128
- const content = await this._downloadAgentMd(target);
1129
- const signature = await this._verifyAgentMd(content, target);
1130
- const isSelf = target === (this._aid ?? '');
1131
- const localEtag = this._agentMdContentEtag(content);
1132
- const cacheMeta = this._agentMdAuthCacheMeta(target);
1133
- const remoteEtag = String(cacheMeta.etag ?? '').trim();
1134
- const lastModified = String(cacheMeta.lastModified ?? cacheMeta.last_modified ?? '').trim();
1135
- if (isSelf) {
1136
- this._localAgentMdEtag = localEtag;
1137
- if (remoteEtag)
1138
- this._remoteAgentMdEtag = remoteEtag;
1139
- }
1140
- const saved = this._saveAgentMdRecord(target, {
1141
- content,
1142
- local_etag: localEtag,
1143
- remote_etag: remoteEtag || undefined,
1144
- last_modified: lastModified || undefined,
1145
- fetched_at: Date.now(),
1146
- remote_status: 'found',
1147
- verify_status: isJsonObject(signature) ? String(signature.status ?? '') : '',
1148
- verify_error: isJsonObject(signature) ? String(signature.reason ?? '') : '',
1149
- last_error: '',
1150
- });
1151
- let inSync = null;
1152
- if (isSelf) {
1153
- const remote = remoteEtag || this._remoteAgentMdEtag || '';
1154
- inSync = localEtag && remote ? localEtag === remote : false;
1155
- }
1156
- return {
1157
- aid: target,
1158
- content,
1159
- signature: signature,
1160
- in_sync: inSync,
1161
- saved_to: String(saved.saved_to ?? this._agentMdFilePath(target)),
1162
- save_error: null,
1163
- };
1164
- }
1165
- /**
1166
- * 设置 agent.md 本地存储根目录;为空时恢复默认 {aun_path}/AIDs。
1167
- */
1168
- _setAgentMdRoot(root) {
1169
- const raw = String(root ?? '').trim();
1170
- const next = raw || path.join(this._configModel.aunPath, 'AIDs');
1171
- fs.mkdirSync(next, { recursive: true });
1172
- this._agentMdPath = next;
1173
- this._agentMdCache.clear();
1174
- return this._agentMdPath;
1175
- }
1176
- /** 返回本地 agent.md 文件的 etag;未设置或读取失败时返回空串。 */
1177
- getLocalAgentMdEtag() {
1178
- return this._localAgentMdEtag;
1179
- }
1180
- /**
1181
- * 返回 gateway 在最近一次 RPC envelope._meta 注入的服务端 agent.md etag。
1182
- *
1183
- * 未收到过则为空串;不阻塞调用,纯内存读。
1184
- */
1185
- getRemoteAgentMdEtag() {
1186
- return this._remoteAgentMdEtag;
1187
- }
1188
- _agentMdContentEtag(content) {
1189
- return `"${crypto.createHash('sha256').update(String(content ?? ''), 'utf-8').digest('hex')}"`;
1190
- }
1191
- _agentMdOwnerAid() {
1192
- return String(this._aid ?? '').trim();
1193
- }
1194
- _agentMdSafeAid(aid) {
1195
- const target = String(aid ?? '').trim();
1196
- if (!target || target.includes('/') || target.includes('\\') || target.includes('\0')) {
1197
- throw new ValidationError('agent.md aid is empty or contains path separators');
1198
- }
1199
- return target;
1200
- }
1201
- _agentMdRoot() {
1202
- const root = this._agentMdPath || path.join(this._configModel.aunPath, 'AIDs');
1203
- fs.mkdirSync(root, { recursive: true });
1204
- return root;
1205
- }
1206
- _agentMdFilePath(aid) {
1207
- return path.join(this._agentMdRoot(), this._agentMdSafeAid(aid), 'agent.md');
1208
- }
1209
- _agentMdMetaPath(aid) {
1210
- return path.join(this._agentMdRoot(), this._agentMdSafeAid(aid), 'agentmd.json');
1211
- }
1212
- _atomicWriteText(filePath, content) {
1213
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1214
- const tmp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
1215
- let fd = null;
1216
- try {
1217
- fd = fs.openSync(tmp, 'w');
1218
- fs.writeFileSync(fd, content, 'utf-8');
1219
- fs.fsyncSync(fd);
1220
- fs.closeSync(fd);
1221
- fd = null;
1222
- fs.renameSync(tmp, filePath);
1223
- try {
1224
- const dirFd = fs.openSync(path.dirname(filePath), 'r');
1225
- try {
1226
- fs.fsyncSync(dirFd);
1227
- }
1228
- finally {
1229
- fs.closeSync(dirFd);
1230
- }
1231
- }
1232
- catch { /* best effort */ }
1233
- }
1234
- finally {
1235
- if (fd !== null) {
1236
- try {
1237
- fs.closeSync(fd);
1238
- }
1239
- catch { /* ignore */ }
1240
- }
1241
- if (fs.existsSync(tmp)) {
1242
- try {
1243
- fs.unlinkSync(tmp);
1244
- }
1245
- catch { /* ignore */ }
1246
- }
1247
- }
1248
- }
1249
- _sleepSync(ms) {
1250
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1251
- }
1252
- _withAgentMdRecordLock(aid, fn) {
1253
- const lockPath = path.join(path.dirname(this._agentMdMetaPath(aid)), 'agentmd.json.lock');
1254
- fs.mkdirSync(path.dirname(lockPath), { recursive: true });
1255
- const deadline = Date.now() + 5000;
1256
- let fd = null;
1257
- while (fd === null) {
1258
- try {
1259
- fd = fs.openSync(lockPath, 'wx');
1260
- fs.writeFileSync(fd, `${process.pid}\n`, 'utf-8');
1261
- }
1262
- catch (err) {
1263
- if (err?.code !== 'EEXIST' || Date.now() >= deadline)
1264
- throw err;
1265
- try {
1266
- const st = fs.statSync(lockPath);
1267
- if (Date.now() - st.mtimeMs > 30000)
1268
- fs.unlinkSync(lockPath);
1269
- }
1270
- catch { /* ignore */ }
1271
- this._sleepSync(25);
1272
- }
1273
- }
1274
- try {
1275
- return fn();
1276
- }
1277
- finally {
1278
- if (fd !== null) {
1279
- try {
1280
- fs.closeSync(fd);
1281
- }
1282
- catch { /* ignore */ }
1283
- }
1284
- try {
1285
- fs.unlinkSync(lockPath);
1286
- }
1287
- catch { /* ignore */ }
1288
- }
1289
- }
1290
- _writeAgentMdRecordUnlocked(aid, record) {
1291
- const payload = {};
1292
- for (const [key, value] of Object.entries(record)) {
1293
- if (key !== 'content' && value !== undefined && value !== null)
1294
- payload[key] = value;
1295
- }
1296
- payload.aid = this._agentMdSafeAid(aid);
1297
- this._atomicWriteText(this._agentMdMetaPath(aid), `${JSON.stringify(payload, null, 2)}\n`);
1298
- }
1299
- _normalizeAgentMdRecord(aid, data) {
1300
- if (!isJsonObject(data))
1301
- return {};
1302
- const record = {};
1303
- for (const [key, value] of Object.entries(data)) {
1304
- if (key !== 'content')
1305
- record[key] = value;
1306
- }
1307
- record.aid = this._agentMdSafeAid(String(record.aid ?? aid));
1308
- for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
1309
- record[key] = Number(record[key] ?? 0) || 0;
1310
- }
1311
- return record;
1312
- }
1313
- _readAgentMdRecordUnlocked(aid) {
1314
- const filePath = this._agentMdMetaPath(aid);
1315
- if (!fs.existsSync(filePath))
1316
- return {};
1317
- try {
1318
- return this._normalizeAgentMdRecord(aid, JSON.parse(fs.readFileSync(filePath, 'utf-8')));
1319
- }
1320
- catch (err) {
1321
- this._clientLog.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
1322
- return {};
1323
- }
1324
- }
1325
- _readAgentMdContent(aid) {
1326
- return fs.readFileSync(this._agentMdFilePath(aid), 'utf-8');
1327
- }
1328
- _writeAgentMdContent(aid, content) {
1329
- const filePath = this._agentMdFilePath(aid);
1330
- this._atomicWriteText(filePath, String(content ?? ''));
1331
- return filePath;
1332
- }
1333
- _agentMdAuthCacheMeta(aid) {
1334
- try {
1335
- const record = this._agentMdCache.get(String(aid ?? '').trim());
1336
- return record && typeof record === 'object' ? { ...record } : {};
1337
- }
1338
- catch {
1339
- return {};
1340
- }
1341
- }
1342
- _loadAgentMdRecord(aid) {
1343
- const target = String(aid ?? '').trim();
1344
- if (!target)
1345
- return null;
1346
- try {
1347
- const loaded = this._withAgentMdRecordLock(target, () => {
1348
- const record = this._readAgentMdRecordUnlocked(target);
1349
- const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
1350
- try {
1351
- const content = this._readAgentMdContent(target);
1352
- next.content = content;
1353
- next.local_etag = this._agentMdContentEtag(content);
1354
- }
1355
- catch (err) {
1356
- if (fs.existsSync(this._agentMdMetaPath(target))) {
1357
- this._clientLog.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
1358
- }
1359
- }
1360
- return next;
1361
- });
1362
- if (Object.keys(loaded).length <= 1)
1363
- return null;
1364
- this._agentMdCache.set(target, { ...loaded });
1365
- return { ...loaded };
1366
- }
1367
- catch (err) {
1368
- this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
1369
- }
1370
- return null;
1371
- }
1372
- _saveAgentMdRecord(aid, fields) {
1373
- const target = String(aid ?? '').trim();
1374
- if (!target)
1375
- return {};
1376
- try {
1377
- const inputFields = { ...fields };
1378
- const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
1379
- let savedTo = '';
1380
- const record = this._withAgentMdRecordLock(target, () => {
1381
- if (hasContent) {
1382
- const content = String(inputFields.content ?? '');
1383
- savedTo = this._writeAgentMdContent(target, content);
1384
- if (!inputFields.local_etag)
1385
- inputFields.local_etag = this._agentMdContentEtag(content);
1386
- if (!inputFields.fetched_at)
1387
- inputFields.fetched_at = Date.now();
1388
- }
1389
- delete inputFields.content;
1390
- const next = { ...this._readAgentMdRecordUnlocked(target), aid: target };
1391
- for (const [key, value] of Object.entries(inputFields)) {
1392
- if (value !== undefined && value !== null)
1393
- next[key] = value;
1394
- }
1395
- next.updated_at = Date.now();
1396
- this._writeAgentMdRecordUnlocked(target, next);
1397
- return next;
1398
- });
1399
- const loaded = { ...record };
1400
- if (hasContent) {
1401
- loaded.content = String(fields.content ?? '');
1402
- if (savedTo)
1403
- loaded.saved_to = savedTo;
1404
- }
1405
- this._agentMdCache.set(target, { ...loaded });
1406
- const owner = this._agentMdOwnerAid();
1407
- if (target === owner) {
1408
- const localEtag = String(loaded.local_etag ?? '').trim();
1409
- const remoteEtag = String(loaded.remote_etag ?? '').trim();
1410
- if (localEtag)
1411
- this._localAgentMdEtag = localEtag;
1412
- if (remoteEtag)
1413
- this._remoteAgentMdEtag = remoteEtag;
1414
- }
1415
- return { ...loaded };
1416
- }
1417
- catch (err) {
1418
- this._clientLog.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
1419
- }
1420
- return {};
1421
- }
1422
- _agentMdHasLocalContent(aid, record) {
1423
- if (record && typeof record.content === 'string' && record.content.length > 0)
1424
- return true;
1425
- try {
1426
- return fs.existsSync(this._agentMdFilePath(aid));
1427
- }
1428
- catch {
1429
- return false;
1430
- }
1431
- }
1432
- _agentMdCheckedAtFresh(checkedAtMs, maxUnsyncedDays) {
1433
- const days = Number(maxUnsyncedDays || 0);
1434
- if (!Number.isFinite(days) || days <= 0)
1435
- return false;
1436
- if (!Number.isFinite(checkedAtMs) || checkedAtMs <= 0)
1437
- return false;
1438
- return Date.now() - checkedAtMs <= days * 86400000;
1439
- }
1440
- _agentMdLastModifiedFresh(lastModified, maxUnsyncedDays) {
1441
- const days = Number(maxUnsyncedDays || 0);
1442
- if (!Number.isFinite(days) || days <= 0)
1443
- return false;
1444
- const ts = Date.parse(String(lastModified ?? '').trim());
1445
- if (!Number.isFinite(ts))
1446
- return false;
1447
- return Date.now() <= ts + days * 86400000;
1448
- }
1449
- _scheduleAgentMdFetchIfMissing(aid, record, source = '') {
1450
- const target = String(aid ?? '').trim();
1451
- if (!target || this._agentMdHasLocalContent(target, record))
1452
- return;
1453
- if (this._agentMdFetchInflight.has(target))
1454
- return;
1455
- void this._startAgentMdFetchTask(target).catch((err) => {
1456
- this._saveAgentMdRecord(target, {
1457
- last_error: err instanceof Error ? err.message : String(err),
1458
- remote_status: 'found',
1459
- });
1460
- this._clientLog.debug(`agent.md auto fetch failed: aid=${target} source=${source || '-'} err=${err instanceof Error ? err.message : String(err)}`);
1461
- });
1462
- }
1463
- _observeAgentMdMeta(aid, etag = '', lastModified = '', source = '') {
1464
- const target = String(aid ?? '').trim();
1465
- const remoteEtag = String(etag ?? '').trim();
1466
- const remoteLastModified = String(lastModified ?? '').trim();
1467
- if (!target || (!remoteEtag && !remoteLastModified))
1468
- return;
1469
- let before = this._agentMdCache.get(target);
1470
- if (!before || typeof before !== 'object')
1471
- before = this._loadAgentMdRecord(target) ?? {};
1472
- const same = (!remoteEtag || String(before.remote_etag ?? '').trim() === remoteEtag) &&
1473
- (!remoteLastModified || String(before.last_modified ?? '').trim() === remoteLastModified);
1474
- let record = { ...before };
1475
- if (!same || Object.keys(before).length === 0) {
1476
- const fields = {
1477
- observed_at: Date.now(),
1478
- remote_status: 'found',
1479
- };
1480
- if (remoteEtag)
1481
- fields.remote_etag = remoteEtag;
1482
- if (remoteLastModified)
1483
- fields.last_modified = remoteLastModified;
1484
- record = this._saveAgentMdRecord(target, fields) || record;
1485
- }
1486
- if (target === this._agentMdOwnerAid() && remoteEtag)
1487
- this._remoteAgentMdEtag = remoteEtag;
1488
- this._scheduleAgentMdFetchIfMissing(target, record, source);
1489
- this._clientLog.debug(`agent.md meta observed: aid=${target} etag=${remoteEtag || '-'} last_modified=${remoteLastModified || '-'} source=${source || '-'}`);
1490
- }
1491
- _observeAgentMdEtag(aid, etag, source = '') {
1492
- this._observeAgentMdMeta(aid, etag, '', source);
1493
- }
1494
- _observeAgentMdFromEnvelope(envelope) {
1495
- if (!isJsonObject(envelope))
1496
- return;
1497
- const env = envelope;
1498
- if (!isJsonObject(env.agent_md))
1499
- return;
1500
- const agentMd = env.agent_md;
1501
- if (!isJsonObject(agentMd.sender))
1502
- return;
1503
- const sender = agentMd.sender;
1504
- let senderAid = String(sender.aid ?? '').trim();
1505
- if (!senderAid) {
1506
- const aad = isJsonObject(env.aad) ? env.aad : {};
1507
- senderAid = String(aad.from ?? env.from ?? '').trim();
1508
- }
1509
- this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
1510
- }
1511
- async _checkAgentMdCache(aid, maxUnsyncedDays = 1) {
1512
- const target = String(aid ?? this._aid ?? '').trim();
1513
- if (!target)
1514
- throw new ValidationError('checkAgentMd requires aid (or local AID)');
1515
- const before = this._loadAgentMdRecord(target) ?? {};
1516
- const localEtag = String(before.local_etag ?? '').trim();
1517
- const localFound = !!(Object.keys(before).length > 0 && (String(before.content ?? '') || localEtag));
1518
- const remoteEtagCached = String(before.remote_etag ?? '').trim();
1519
- const lastModifiedCached = String(before.last_modified ?? '').trim();
1520
- const checkedAt = Number(before.checked_at ?? 0);
1521
- const fetchedAt = Number(before.fetched_at ?? 0);
1522
- const checkedAtCached = checkedAt > 0 ? checkedAt : fetchedAt;
1523
- const cachedInSync = !!(localFound && localEtag && remoteEtagCached && localEtag === remoteEtagCached);
1524
- // max_unsynced_days > 0 且距上次 HEAD 在窗口内 → 直接返回缓存;否则强制 HEAD。
1525
- if (cachedInSync && this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
1526
- return {
1527
- aid: target,
1528
- local_found: true,
1529
- remote_found: true,
1530
- local_etag: localEtag,
1531
- remote_etag: remoteEtagCached,
1532
- in_sync: true,
1533
- last_modified: lastModifiedCached,
1534
- status: 200,
1535
- cached: true,
1536
- verify_status: String(before.verify_status ?? ''),
1537
- verify_error: String(before.verify_error ?? ''),
1538
- };
1539
- }
1540
- const remoteFoundCached = !!(remoteEtagCached || String(before.remote_status ?? '') === 'found');
1541
- if (!localFound &&
1542
- !remoteFoundCached &&
1543
- String(before.remote_status ?? '') === 'missing' &&
1544
- this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
1545
- return {
1546
- aid: target,
1547
- local_found: false,
1548
- remote_found: false,
1549
- local_etag: '',
1550
- remote_etag: '',
1551
- in_sync: false,
1552
- last_modified: '',
1553
- status: 404,
1554
- cached: true,
1555
- verify_status: '',
1556
- verify_error: '',
1557
- };
1558
- }
1559
- const now = Date.now();
1560
- let remote;
1561
- try {
1562
- remote = await this._headAgentMd(target);
1563
- }
1564
- catch (err) {
1565
- this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
1566
- throw err;
1567
- }
1568
- const remoteFound = !!remote.found;
1569
- const remoteEtag = String(remote.etag ?? '').trim();
1570
- const lastModified = String(remote.last_modified ?? remote.lastModified ?? '').trim();
1571
- const saved = this._saveAgentMdRecord(target, {
1572
- remote_etag: remoteFound ? remoteEtag : '',
1573
- last_modified: lastModified,
1574
- checked_at: now,
1575
- remote_status: remoteFound ? 'found' : 'missing',
1576
- last_error: '',
1577
- });
1578
- if (target === this._agentMdOwnerAid() && remoteEtag)
1579
- this._remoteAgentMdEtag = remoteEtag;
1580
- const inSync = !!(localFound && remoteFound && localEtag && remoteEtag && localEtag === remoteEtag);
1581
- return {
1582
- aid: target,
1583
- local_found: localFound,
1584
- remote_found: remoteFound,
1585
- local_etag: localEtag,
1586
- remote_etag: remoteEtag,
1587
- in_sync: inSync,
1588
- last_modified: lastModified,
1589
- status: Number(remote.status ?? (remoteFound ? 200 : 404)),
1590
- cached: false,
1591
- verify_status: String(saved.verify_status ?? before.verify_status ?? ''),
1592
- verify_error: String(saved.verify_error ?? before.verify_error ?? ''),
1593
- };
927
+ async uploadAgentMd(content) {
928
+ return await this._agentMdManager.upload(content);
1594
929
  }
1595
930
  /** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
1596
931
  _observeRpcMeta(meta) {
1597
- if (!meta || typeof meta !== 'object')
1598
- return;
1599
- const etag = String(meta.agent_md_etag ?? '').trim();
1600
- if (etag) {
1601
- this._remoteAgentMdEtag = etag;
1602
- this._observeAgentMdMeta(this._aid ?? '', etag, '', 'rpc.self');
1603
- }
1604
- const etags = meta.agent_md_etags;
1605
- if (isJsonObject(etags)) {
1606
- // role key 优先级:requester / peer 是新规范,其余是兼容旧 SDK 的别名。
1607
- for (const key of ['requester', 'peer', 'receiver', 'target', 'to', 'sender', 'from']) {
1608
- const item = etags[key];
1609
- if (!isJsonObject(item))
1610
- continue;
1611
- this._observeAgentMdMeta(String(item.aid ?? ''), String(item.etag ?? ''), String(item.last_modified ?? item.lastModified ?? ''), `rpc.${key}`);
1612
- }
1613
- }
932
+ this._agentMdManager.observeRpcMeta(meta, this._aid);
1614
933
  }
1615
934
  /** 连接状态 */
1616
935
  get state() {
@@ -2535,15 +1854,11 @@ export class AUNClient {
2535
1854
  // 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
2536
1855
  if (isJsonObject(payload)) {
2537
1856
  try {
2538
- const localEtag = this._localAgentMdEtag || '';
2539
- const remoteEtag = this._remoteAgentMdEtag || '';
2540
- if (localEtag || remoteEtag) {
1857
+ const snapshot = this._agentMdManager.eventSnapshot();
1858
+ if (snapshot) {
2541
1859
  const obj = payload;
2542
1860
  if (!('_agent_md' in obj)) {
2543
- obj._agent_md = {
2544
- local_etag: localEtag,
2545
- remote_etag: remoteEtag,
2546
- };
1861
+ obj._agent_md = snapshot;
2547
1862
  }
2548
1863
  }
2549
1864
  }
@@ -4234,7 +3549,7 @@ export class AUNClient {
4234
3549
  this._v2BootstrapCache.clear();
4235
3550
  }
4236
3551
  let identity = this._identity;
4237
- // 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
3552
+ // 私钥来自当前 AID 值对象,AUNClient 不从持久化存储读取私钥。
4238
3553
  const currentAid = this._currentAid;
4239
3554
  if (!currentAid?.privateKeyPem) {
4240
3555
  this._clientLog.warn('V2 session init skipped: no AID private key');
@@ -5262,7 +4577,7 @@ export class AUNClient {
5262
4577
  return null;
5263
4578
  }
5264
4579
  const e2eeMeta = this._v2E2eeMeta(envelope);
5265
- this._observeAgentMdFromEnvelope(envelope);
4580
+ this._agentMdManager.observeEnvelope(envelope);
5266
4581
  let spkId = '';
5267
4582
  let recipientKeySource = '';
5268
4583
  if (isJsonObject(envelope.recipient)) {
@@ -5467,7 +4782,7 @@ export class AUNClient {
5467
4782
  _attachV2EnvelopeMetadataFromSource(message, source) {
5468
4783
  const envelope = this._extractV2EnvelopeFromSource(source);
5469
4784
  if (envelope) {
5470
- this._observeAgentMdFromEnvelope(envelope);
4785
+ this._agentMdManager.observeEnvelope(envelope);
5471
4786
  this._attachV2EnvelopeMetadata(message, this._v2E2eeMeta(envelope));
5472
4787
  }
5473
4788
  }
@@ -6950,8 +6265,8 @@ export class AUNClient {
6950
6265
  if ('device_id' in params && String(params.device_id ?? '').trim() !== this._deviceId) {
6951
6266
  throw new ValidationError('message.pull/message.ack device_id must match the current client instance');
6952
6267
  }
6953
- const slotId = normalizeInstanceId(params.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
6954
- if (slotId !== this._slotId) {
6268
+ const slotId = normalizeSlotId(params.slot_id ?? this._slotId, this._slotId);
6269
+ if (slotIsolationKey(slotId) !== slotIsolationKey(this._slotId)) {
6955
6270
  throw new ValidationError('message.pull/message.ack slot_id must match the current client instance');
6956
6271
  }
6957
6272
  params.device_id = this._deviceId;