@agentunion/fastaun 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/_packed_docs/CHANGELOG.md +39 -0
- package/_packed_docs/INDEX.md +2 -2
- package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
- 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
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +15 -14
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +2 -2
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -5
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +44 -26
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +5 -5
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +63 -35
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-message-rpc-manual.md +6 -6
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +6 -4
- package/_packed_docs/sdk/INDEX.md +2 -2
- package/_packed_docs/sdk/README.md +3 -3
- package/dist/agent-md.d.ts +101 -0
- package/dist/agent-md.js +778 -0
- package/dist/agent-md.js.map +1 -0
- package/dist/aid-store.d.ts +12 -39
- package/dist/aid-store.js +114 -138
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +1 -62
- package/dist/client.js +138 -826
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +2 -0
- package/dist/keystore/aid-db.js +12 -2
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +6 -2
- package/dist/keystore/local-identity-store.d.ts +70 -0
- package/dist/keystore/local-identity-store.js +525 -0
- package/dist/keystore/local-identity-store.js.map +1 -0
- package/dist/keystore/local-token-store.d.ts +68 -0
- package/dist/keystore/local-token-store.js +368 -0
- package/dist/keystore/local-token-store.js.map +1 -0
- package/dist/register-flow.d.ts +12 -4
- package/dist/register-flow.js +70 -3
- package/dist/register-flow.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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,
|
|
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 {
|
|
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
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,9 @@ 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
|
-
};
|
|
1594
|
-
}
|
|
1595
927
|
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
1596
928
|
_observeRpcMeta(meta) {
|
|
1597
|
-
|
|
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
|
-
}
|
|
929
|
+
this._agentMdManager.observeRpcMeta(meta, this._aid);
|
|
1614
930
|
}
|
|
1615
931
|
/** 连接状态 */
|
|
1616
932
|
get state() {
|
|
@@ -2535,15 +1851,11 @@ export class AUNClient {
|
|
|
2535
1851
|
// 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
|
|
2536
1852
|
if (isJsonObject(payload)) {
|
|
2537
1853
|
try {
|
|
2538
|
-
const
|
|
2539
|
-
|
|
2540
|
-
if (localEtag || remoteEtag) {
|
|
1854
|
+
const snapshot = this._agentMdManager.eventSnapshot();
|
|
1855
|
+
if (snapshot) {
|
|
2541
1856
|
const obj = payload;
|
|
2542
1857
|
if (!('_agent_md' in obj)) {
|
|
2543
|
-
obj._agent_md =
|
|
2544
|
-
local_etag: localEtag,
|
|
2545
|
-
remote_etag: remoteEtag,
|
|
2546
|
-
};
|
|
1858
|
+
obj._agent_md = snapshot;
|
|
2547
1859
|
}
|
|
2548
1860
|
}
|
|
2549
1861
|
}
|
|
@@ -4234,7 +3546,7 @@ export class AUNClient {
|
|
|
4234
3546
|
this._v2BootstrapCache.clear();
|
|
4235
3547
|
}
|
|
4236
3548
|
let identity = this._identity;
|
|
4237
|
-
//
|
|
3549
|
+
// 私钥来自当前 AID 值对象,AUNClient 不从持久化存储读取私钥。
|
|
4238
3550
|
const currentAid = this._currentAid;
|
|
4239
3551
|
if (!currentAid?.privateKeyPem) {
|
|
4240
3552
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
@@ -5262,7 +4574,7 @@ export class AUNClient {
|
|
|
5262
4574
|
return null;
|
|
5263
4575
|
}
|
|
5264
4576
|
const e2eeMeta = this._v2E2eeMeta(envelope);
|
|
5265
|
-
this.
|
|
4577
|
+
this._agentMdManager.observeEnvelope(envelope);
|
|
5266
4578
|
let spkId = '';
|
|
5267
4579
|
let recipientKeySource = '';
|
|
5268
4580
|
if (isJsonObject(envelope.recipient)) {
|
|
@@ -5467,7 +4779,7 @@ export class AUNClient {
|
|
|
5467
4779
|
_attachV2EnvelopeMetadataFromSource(message, source) {
|
|
5468
4780
|
const envelope = this._extractV2EnvelopeFromSource(source);
|
|
5469
4781
|
if (envelope) {
|
|
5470
|
-
this.
|
|
4782
|
+
this._agentMdManager.observeEnvelope(envelope);
|
|
5471
4783
|
this._attachV2EnvelopeMetadata(message, this._v2E2eeMeta(envelope));
|
|
5472
4784
|
}
|
|
5473
4785
|
}
|
|
@@ -6950,8 +6262,8 @@ export class AUNClient {
|
|
|
6950
6262
|
if ('device_id' in params && String(params.device_id ?? '').trim() !== this._deviceId) {
|
|
6951
6263
|
throw new ValidationError('message.pull/message.ack device_id must match the current client instance');
|
|
6952
6264
|
}
|
|
6953
|
-
const slotId =
|
|
6954
|
-
if (slotId !== this._slotId) {
|
|
6265
|
+
const slotId = normalizeSlotId(params.slot_id ?? this._slotId, this._slotId);
|
|
6266
|
+
if (slotIsolationKey(slotId) !== slotIsolationKey(this._slotId)) {
|
|
6955
6267
|
throw new ValidationError('message.pull/message.ack slot_id must match the current client instance');
|
|
6956
6268
|
}
|
|
6957
6269
|
params.device_id = this._deviceId;
|