evolclaw 3.1.4 → 3.1.5

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 (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agents/claude-runner.js +348 -156
  3. package/dist/agents/kit-renderer.js +176 -21
  4. package/dist/aun/aid/agentmd.js +68 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/p2p.js +26 -2
  10. package/dist/aun/rpc/connection.js +23 -30
  11. package/dist/channels/aun.js +77 -88
  12. package/dist/channels/dingtalk.js +1 -0
  13. package/dist/channels/feishu.js +270 -190
  14. package/dist/channels/qqbot.js +1 -0
  15. package/dist/channels/wechat.js +1 -0
  16. package/dist/channels/wecom.js +1 -0
  17. package/dist/cli/agent.js +11 -5
  18. package/dist/cli/bench.js +40 -23
  19. package/dist/cli/index.js +170 -44
  20. package/dist/cli/init-channel.js +5 -1
  21. package/dist/cli/model.js +324 -0
  22. package/dist/cli/net-check.js +133 -50
  23. package/dist/cli/watch-msg.js +7 -7
  24. package/dist/cli/watch-web/debug-log.js +18 -0
  25. package/dist/cli/watch-web/server.js +306 -0
  26. package/dist/cli/watch-web/sources/aid.js +63 -0
  27. package/dist/cli/watch-web/sources/msg.js +70 -0
  28. package/dist/cli/watch-web/sources/session.js +638 -0
  29. package/dist/cli/watch-web/sources/types.js +10 -0
  30. package/dist/cli/watch-web/static/app.js +546 -0
  31. package/dist/cli/watch-web/static/index.html +54 -0
  32. package/dist/cli/watch-web/static/style.css +247 -0
  33. package/dist/core/channel-loader.js +7 -4
  34. package/dist/core/command-handler.js +81 -86
  35. package/dist/core/evolagent-registry.js +1 -1
  36. package/dist/core/evolagent.js +4 -4
  37. package/dist/core/interaction-router.js +59 -0
  38. package/dist/core/message/message-bridge.js +6 -6
  39. package/dist/core/message/message-log.js +2 -2
  40. package/dist/core/message/message-processor.js +86 -101
  41. package/dist/core/message/stream-idle-monitor.js +21 -0
  42. package/dist/core/model/model-catalog.js +215 -0
  43. package/dist/core/model/model-scope.js +250 -0
  44. package/dist/core/relation/peer-identity.js +40 -49
  45. package/dist/core/relation/peer-key.js +16 -0
  46. package/dist/core/session/session-fs-store.js +34 -55
  47. package/dist/core/session/session-key.js +24 -0
  48. package/dist/core/session/session-manager.js +308 -251
  49. package/dist/core/session/session-mapper.js +9 -4
  50. package/dist/core/trigger/manager.js +3 -3
  51. package/dist/core/trigger/scheduler.js +2 -1
  52. package/dist/index.js +6 -2
  53. package/dist/ipc.js +22 -0
  54. package/kits/docs/GUIDE.md +2 -2
  55. package/kits/docs/INDEX.md +11 -7
  56. package/kits/docs/channels/aun.md +56 -17
  57. package/kits/docs/channels/feishu.md +41 -12
  58. package/kits/docs/context-assembly.md +181 -0
  59. package/kits/docs/evolclaw/agent.md +49 -0
  60. package/kits/docs/evolclaw/aid.md +49 -0
  61. package/kits/docs/evolclaw/ctl.md +46 -0
  62. package/kits/docs/evolclaw/group.md +82 -0
  63. package/kits/docs/evolclaw/msg.md +86 -0
  64. package/kits/docs/evolclaw/rpc.md +35 -0
  65. package/kits/docs/evolclaw/storage.md +49 -0
  66. package/kits/docs/venues/aun-group.md +10 -0
  67. package/kits/docs/venues/aun-private.md +10 -0
  68. package/kits/docs/venues/client-desktop.md +10 -0
  69. package/kits/docs/venues/client-mobile.md +10 -0
  70. package/kits/docs/venues/feishu-group.md +13 -0
  71. package/kits/docs/venues/feishu-private.md +9 -0
  72. package/kits/docs/venues/group.md +11 -0
  73. package/kits/docs/venues/private.md +10 -0
  74. package/kits/eck_manifest.json +72 -36
  75. package/kits/rules/01-overview.md +20 -10
  76. package/kits/rules/06-channel.md +30 -27
  77. package/kits/templates/system-fragments/session.md +10 -3
  78. package/kits/templates/system-fragments/venue.md +9 -0
  79. package/package.json +11 -6
  80. package/dist/aun/aid/lifecycle-log.js +0 -33
  81. package/dist/utils/aid-lifecycle-log.js +0 -33
  82. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  83. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  84. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  85. package/kits/docs/evolclaw/tools.md +0 -25
@@ -7,6 +7,9 @@ const PARAM_DESCRIPTIONS = {
7
7
  EVOLCLAW_HOME: '用户数据根目录',
8
8
  PACKAGE_ROOT: 'evolclaw 包根目录',
9
9
  CURRENT_PROJECT: '当前项目完整路径',
10
+ PERSONAL_DIR: '当前 agent 个人数据目录',
11
+ RELATIONS_DIR: '当前 agent 关系数据目录',
12
+ VENUES_DIR: '当前 agent 环境数据目录',
10
13
  selfAid: '当前 agent 的 AID',
11
14
  selfName: '当前 agent 的显示名',
12
15
  hasPersona: '是否有 persona 内容',
@@ -20,10 +23,14 @@ const PARAM_DESCRIPTIONS = {
20
23
  chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
21
24
  channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
22
25
  venueUid: '场所唯一标识(预留)',
26
+ dispatch: '群分发模式(mention=被@才响应 / broadcast=所有消息都响应)',
27
+ clientType: '客户端类型(desktop/web/mobile)',
28
+ permissionMode: '权限模式(auto/bypass/request/edit/plan/noask/readonly)',
23
29
  capabilities: '当前渠道支持的能力列表',
24
30
  project: '当前项目目录名',
25
31
  sessionId: 'evolclaw 会话 ID',
26
32
  sessionName: '会话名称',
33
+ sessionKey: '会话路由键(channelType#urlEncode(channelId)#urlEncode(threadId))',
27
34
  sessionCreatedAt: '会话创建时间(ISO)',
28
35
  threadId: '话题 ID(多话题路由时)',
29
36
  chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
@@ -47,6 +54,9 @@ function buildPathMappings(vars) {
47
54
  { prefix: pkgRoot, alias: '$PACKAGE_ROOT' },
48
55
  ];
49
56
  if (selfAid) {
57
+ mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'personal'), alias: '$PERSONAL_DIR' });
58
+ mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'relations'), alias: '$RELATIONS_DIR' });
59
+ mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'venues'), alias: '$VENUES_DIR' });
50
60
  mappings.push({ prefix: path.join(evolHome, 'agents', selfAid), alias: '$AGENT_DIR' });
51
61
  }
52
62
  mappings.push({ prefix: evolHome, alias: '$EVOLCLAW_HOME' });
@@ -90,33 +100,75 @@ export function renderKitSections(ctx) {
90
100
  const fileParts = [];
91
101
  const fragmentParts = [];
92
102
  const pathMappings = buildPathMappings(ctx.vars);
103
+ const diagnostics = [];
93
104
  for (const section of sections) {
94
- if (section.enabled === false)
105
+ const rawPath = section.type === 'file' ? (section.file ?? '') : (section.path ?? '');
106
+ const diag = {
107
+ id: section.id,
108
+ description: section.description,
109
+ type: section.type,
110
+ rawPath,
111
+ pattern: section.pattern,
112
+ needsInjection: section.needsInjection,
113
+ when: section.when,
114
+ enabled: section.enabled !== false,
115
+ whenPassed: false,
116
+ resolvedPath: null,
117
+ resolveStatus: 'no-path',
118
+ fileCount: 0,
119
+ used: false,
120
+ injected: false,
121
+ };
122
+ if (section.enabled === false) {
123
+ diag.resolveStatus = 'skipped-disabled';
124
+ diagnostics.push(diag);
95
125
  continue;
96
- if (!evaluateWhen(section.when, ctx.vars))
126
+ }
127
+ diag.whenPassed = evaluateWhen(section.when, ctx.vars);
128
+ if (!diag.whenPassed) {
129
+ diag.resolveStatus = 'skipped-when';
130
+ diagnostics.push(diag);
97
131
  continue;
132
+ }
133
+ if (rawPath) {
134
+ const resolveResult = resolvePathWithDiag(rawPath, ctx);
135
+ diag.resolvedPath = resolveResult.resolved;
136
+ diag.resolveStatus = resolveResult.status;
137
+ if (resolveResult.unresolvedTokens.length > 0)
138
+ diag.unresolvedTokens = resolveResult.unresolvedTokens;
139
+ }
98
140
  const files = loadSectionFiles(section, ctx);
99
- if (files.length === 0)
141
+ diag.fileCount = files.length;
142
+ if (files.length === 0) {
143
+ diagnostics.push(diag);
100
144
  continue;
145
+ }
146
+ let anyUsed = false;
101
147
  for (const [filePath, rawContent] of files) {
102
148
  const content = section.needsInjection ? renderTemplate(rawContent, ctx.vars) : rawContent;
103
- if (!content.trim())
149
+ if (!content.trim()) {
150
+ diag.emptyContent = true;
104
151
  continue;
152
+ }
105
153
  const label = section.description ? `${section.id} — ${section.description}` : section.id;
106
154
  const displayPath = shortenPath(filePath, pathMappings);
107
155
  const part = `Contenu de ${displayPath} (${label}):\n\n${content.trimEnd()}`;
108
156
  fileParts.push(part);
157
+ anyUsed = true;
109
158
  if (section.needsInjection) {
110
159
  fragmentParts.push(part);
160
+ diag.injected = true;
111
161
  }
112
162
  }
163
+ diag.used = anyUsed;
164
+ diagnostics.push(diag);
113
165
  }
114
- if (fileParts.length === 0)
115
- return '';
116
166
  const body = fileParts.join('\n\n');
117
- const output = `<system-reminder>\nEvolClaw Context Kit documents are shown below.\n\n${body}\n\nIMPORTANT: Use this context when it affects the current interaction.\n</system-reminder>`;
167
+ const output = fileParts.length > 0
168
+ ? `<system-reminder>\nEvolClaw Context Kit documents are shown below.\n\n${body}\n\nIMPORTANT: Use this context when it affects the current interaction.\n</system-reminder>`
169
+ : '';
118
170
  const fragmentsOutput = fragmentParts.length > 0 ? fragmentParts.join('\n\n') : '';
119
- writeDebugFiles(ctx, output, fragmentsOutput);
171
+ writeDebugFiles(ctx, output, fragmentsOutput, diagnostics);
120
172
  return output;
121
173
  }
122
174
  export function cleanEckDebug() {
@@ -212,23 +264,38 @@ function loadDirectorySection(dirPath, pattern, ctx) {
212
264
  }
213
265
  // ── Path resolution ──
214
266
  function resolvePath(rawPath, ctx) {
215
- let resolved = rawPath.replace(/\$([A-Z_]+)/g, (_, name) => {
267
+ const r = resolvePathWithDiag(rawPath, ctx);
268
+ return r.status === 'ok' ? r.resolved : null;
269
+ }
270
+ function resolvePathWithDiag(rawPath, ctx) {
271
+ const unresolved = [];
272
+ let resolved = rawPath.replace(/\$([A-Z_]+)/g, (_m, name) => {
216
273
  const val = ctx.vars[name];
217
- if (val === undefined || val === null || val === false || val === '')
274
+ if (val === undefined || val === null || val === false || val === '') {
275
+ unresolved.push(`$${name}`);
218
276
  return '';
277
+ }
219
278
  return String(val);
220
279
  });
221
- resolved = resolved.replace(/\{\{(\w+)\}\}/g, (_, key) => {
280
+ resolved = resolved.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
222
281
  const val = ctx.vars[key];
223
- if (val === undefined || val === null || val === false || val === '')
282
+ if (val === undefined || val === null || val === false || val === '') {
283
+ unresolved.push(`{{${key}}}`);
224
284
  return '';
285
+ }
225
286
  return String(val);
226
287
  });
227
- if (!resolved || resolved.includes('$') || resolved.includes('{{'))
228
- return null;
229
- if (!fs.existsSync(resolved))
230
- return null;
231
- return resolved;
288
+ if (!resolved || resolved.includes('$') || resolved.includes('{{')) {
289
+ return { resolved: resolved || null, status: 'unresolved-vars', unresolvedTokens: unresolved };
290
+ }
291
+ if (unresolved.length > 0) {
292
+ // 占位符是非必需变量,但有的解析为空——视为未解析
293
+ return { resolved, status: 'unresolved-vars', unresolvedTokens: unresolved };
294
+ }
295
+ if (!fs.existsSync(resolved)) {
296
+ return { resolved, status: 'not-exist', unresolvedTokens: unresolved };
297
+ }
298
+ return { resolved, status: 'ok', unresolvedTokens: unresolved };
232
299
  }
233
300
  // CHUNK_CONTINUE_5
234
301
  // ── Directory reading ──
@@ -260,10 +327,17 @@ function evaluateWhen(when, vars) {
260
327
  return true;
261
328
  if (when.var !== undefined) {
262
329
  const val = vars[when.var];
263
- if (when.eq !== undefined)
330
+ if (when.eq !== undefined) {
331
+ // 把 undefined 视作 null 的等价物,便于 manifest 用 eq:null/neq:null 表达"未注入"
332
+ if (when.eq === null)
333
+ return val === null || val === undefined;
264
334
  return val === when.eq;
265
- if (when.neq !== undefined)
335
+ }
336
+ if (when.neq !== undefined) {
337
+ if (when.neq === null)
338
+ return val !== null && val !== undefined;
266
339
  return val !== when.neq;
340
+ }
267
341
  if (when.in !== undefined)
268
342
  return when.in.includes(val);
269
343
  if (when.nin !== undefined)
@@ -321,7 +395,7 @@ function getSessionCache(sessionId) {
321
395
  return cache;
322
396
  }
323
397
  // ── Debug output ──
324
- function writeDebugFiles(ctx, output, fragmentsOutput) {
398
+ function writeDebugFiles(ctx, output, fragmentsOutput, diagnostics) {
325
399
  const now = new Date();
326
400
  const ts = now.toISOString().replace(/[T:.]/g, '-').slice(0, 19);
327
401
  const dir = eckDebugDir();
@@ -337,8 +411,89 @@ function writeDebugFiles(ctx, output, fragmentsOutput) {
337
411
  })),
338
412
  };
339
413
  fs.writeFile(path.join(dir, `vars-${ts}.json`), JSON.stringify(varsData, null, 2), () => { });
340
- fs.writeFile(path.join(dir, `context-${ts}.md`), output, () => { });
414
+ if (output)
415
+ fs.writeFile(path.join(dir, `context-${ts}.md`), output, () => { });
341
416
  if (fragmentsOutput) {
342
417
  fs.writeFile(path.join(dir, `fragments-${ts}.md`), fragmentsOutput, () => { });
343
418
  }
419
+ fs.writeFile(path.join(dir, `manifest-${ts}.md`), formatManifestDiagnostics(ctx, diagnostics), () => { });
420
+ }
421
+ function formatManifestDiagnostics(ctx, diagnostics) {
422
+ const STATUS_ICON = {
423
+ 'ok': 'OK',
424
+ 'unresolved-vars': 'UNRESOLVED-VARS',
425
+ 'not-exist': 'NOT-EXIST',
426
+ 'skipped-disabled': 'SKIPPED(disabled)',
427
+ 'skipped-when': 'SKIPPED(when)',
428
+ 'no-path': 'NO-PATH',
429
+ };
430
+ const used = diagnostics.filter(d => d.used).length;
431
+ const skippedWhen = diagnostics.filter(d => d.resolveStatus === 'skipped-when').length;
432
+ const errors = diagnostics.filter(d => d.resolveStatus === 'unresolved-vars' || d.resolveStatus === 'not-exist').length;
433
+ const lines = [];
434
+ lines.push(`# ECK Manifest Diagnostics`);
435
+ lines.push('');
436
+ lines.push(`- timestamp: ${new Date().toISOString()}`);
437
+ lines.push(`- sessionId: ${ctx.sessionId}`);
438
+ lines.push(`- sections total: ${diagnostics.length}`);
439
+ lines.push(`- sections used: ${used}`);
440
+ lines.push(`- sections skipped (when=false): ${skippedWhen}`);
441
+ lines.push(`- sections with errors (unresolved-vars/not-exist): ${errors}`);
442
+ lines.push('');
443
+ if (errors > 0) {
444
+ lines.push(`## Errors`);
445
+ lines.push('');
446
+ for (const d of diagnostics) {
447
+ if (d.resolveStatus !== 'unresolved-vars' && d.resolveStatus !== 'not-exist')
448
+ continue;
449
+ lines.push(`### ${d.id}${d.description ? ' — ' + d.description : ''}`);
450
+ lines.push(`- status: ${STATUS_ICON[d.resolveStatus]}`);
451
+ lines.push(`- type: ${d.type}`);
452
+ lines.push(`- raw path: \`${d.rawPath}\``);
453
+ lines.push(`- resolved: \`${d.resolvedPath ?? '(null)'}\``);
454
+ if (d.unresolvedTokens && d.unresolvedTokens.length > 0) {
455
+ lines.push(`- unresolved tokens: ${d.unresolvedTokens.map(t => '`' + t + '`').join(', ')}`);
456
+ }
457
+ lines.push('');
458
+ }
459
+ }
460
+ lines.push(`## All sections`);
461
+ lines.push('');
462
+ lines.push(`| order | id | status | type | raw path | resolved | files | used | injected |`);
463
+ lines.push(`|---|---|---|---|---|---|---|---|---|`);
464
+ diagnostics.forEach((d, idx) => {
465
+ const status = STATUS_ICON[d.resolveStatus];
466
+ const rawPath = d.rawPath ? '`' + d.rawPath + '`' : '—';
467
+ const resolvedShort = d.resolvedPath ? '`' + (d.resolvedPath.length > 60 ? '…' + d.resolvedPath.slice(-58) : d.resolvedPath) + '`' : '—';
468
+ lines.push(`| ${idx + 1} | ${d.id} | ${status} | ${d.type} | ${rawPath} | ${resolvedShort} | ${d.fileCount} | ${d.used ? 'Y' : '·'} | ${d.injected ? 'Y' : '·'} |`);
469
+ });
470
+ lines.push('');
471
+ // 详细列出每个 section
472
+ lines.push(`## Section details`);
473
+ lines.push('');
474
+ for (const d of diagnostics) {
475
+ lines.push(`### ${d.id}${d.description ? ' — ' + d.description : ''}`);
476
+ lines.push(`- status: ${STATUS_ICON[d.resolveStatus]}`);
477
+ lines.push(`- type: ${d.type}`);
478
+ if (d.rawPath)
479
+ lines.push(`- raw path: \`${d.rawPath}\``);
480
+ if (d.pattern)
481
+ lines.push(`- pattern: \`${d.pattern}\``);
482
+ if (d.when !== undefined)
483
+ lines.push(`- when: \`${JSON.stringify(d.when)}\` → ${d.whenPassed ? 'true' : 'false'}`);
484
+ lines.push(`- needsInjection: ${d.needsInjection ? 'true' : 'false'}`);
485
+ lines.push(`- enabled: ${d.enabled}`);
486
+ if (d.resolvedPath)
487
+ lines.push(`- resolved: \`${d.resolvedPath}\``);
488
+ if (d.unresolvedTokens && d.unresolvedTokens.length > 0) {
489
+ lines.push(`- unresolved tokens: ${d.unresolvedTokens.map(t => '`' + t + '`').join(', ')}`);
490
+ }
491
+ lines.push(`- file count: ${d.fileCount}`);
492
+ if (d.emptyContent)
493
+ lines.push(`- note: 文件存在但渲染后内容为空`);
494
+ lines.push(`- used in output: ${d.used ? 'yes' : 'no'}`);
495
+ lines.push(`- injected as fragment: ${d.injected ? 'yes' : 'no'}`);
496
+ lines.push('');
497
+ }
498
+ return lines.join('\n');
344
499
  }
@@ -1,143 +1,96 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { getAunClient, createAunClient } from './client.js';
3
+ import { getAidStore, loadAid, SLOT } from './store.js';
4
4
  import { agentMdPath, aidLocalDir, resolveRoot } from '../../paths.js';
5
5
  export function buildInitialAgentMd(opts) {
6
6
  const agentName = opts.aid.split('.')[0];
7
7
  const agentType = opts.type || 'ai';
8
8
  return `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\n---\n`;
9
9
  }
10
- /**
11
- * Resolve the gateway URL for an AID via .well-known discovery.
12
- */
13
- async function discoverGateway(aid) {
14
- try {
15
- const resp = await fetch(`https://${aid}/.well-known/aun-gateway`, { redirect: 'follow' });
16
- if (!resp.ok)
17
- return undefined;
18
- const text = await resp.text();
19
- try {
20
- return JSON.parse(text.trim()).gateways?.[0]?.url ?? text.trim();
21
- }
22
- catch {
23
- return text.trim();
24
- }
25
- }
26
- catch {
27
- return undefined;
28
- }
29
- }
30
- /**
31
- * Obtain cert PEM for an AID: local first, then network.
32
- * Persists fetched cert to local for future use.
33
- */
34
- async function obtainCertPem(aid, aunPath, client) {
35
- const localCert = path.join(aunPath, 'AIDs', aid, 'public', 'cert.pem');
36
- if (fs.existsSync(localCert)) {
37
- return fs.readFileSync(localCert, 'utf-8');
38
- }
39
- // Fetch from network via SDK's _fetchPeerCert (needs gateway)
40
- if (client) {
41
- try {
42
- if (!client._gatewayUrl) {
43
- client._gatewayUrl = await discoverGateway(aid);
44
- }
45
- if (client._gatewayUrl) {
46
- const certPem = await client._fetchPeerCert.call(client, aid);
47
- // Persist for future use
48
- if (certPem) {
49
- const certDir = path.join(aunPath, 'AIDs', aid, 'public');
50
- fs.mkdirSync(certDir, { recursive: true });
51
- fs.writeFileSync(localCert, certPem, 'utf-8');
52
- }
53
- return certPem;
54
- }
55
- }
56
- catch { /* fall through */ }
57
- }
58
- return undefined;
10
+ /** Normalize an SDK verification result to the local union type. */
11
+ function normalizeVerification(v) {
12
+ const status = v.status === 'verified' ? 'verified' : v.status === 'unsigned' ? 'unsigned' : 'invalid';
13
+ return { status, ...(v.reason ? { reason: String(v.reason) } : {}) };
59
14
  }
60
15
  /**
61
- * Verify agent.md content using SDK.
16
+ * Verify locally-cached agent.md content offline via the AID value object.
17
+ * Loads the peer cert through the store (no network, no private key required).
62
18
  */
63
- async function verifyContent(content, aid, certPem, client) {
19
+ function verifyLocal(store, aid, content) {
64
20
  if (!content.includes('AUN-SIGNATURE')) {
65
21
  return { status: 'unsigned' };
66
22
  }
67
- if (!certPem) {
68
- return { status: 'invalid', reason: 'certificate not available' };
69
- }
70
23
  try {
71
- const result = await client.auth.verifyAgentMd(content, { aid, certPem });
72
- if (result.status === 'verified' || result.verified) {
73
- return { status: 'verified' };
74
- }
75
- return { status: 'invalid', reason: result.reason };
24
+ const aidObj = loadAid(store, aid);
25
+ const r = aidObj.verifyAgentMd(content);
26
+ if (!r.ok)
27
+ return { status: 'invalid', reason: r.error.message };
28
+ return normalizeVerification(r.data);
76
29
  }
77
30
  catch (e) {
78
- return { status: 'invalid', reason: `verify error: ${String(e.message || e).slice(0, 100)}` };
31
+ // loadAid throws AidLoadError when the peer cert is missing/invalid.
32
+ return { status: 'invalid', reason: `certificate not available: ${String(e?.message || e).slice(0, 100)}` };
79
33
  }
80
34
  }
81
- /**
82
- * Create a bare AUNClient (no createAid) for read-only operations.
83
- */
84
- async function createBareClient(aunPath) {
85
- return createAunClient({ aunPath });
86
- }
87
35
  export async function agentmdGet(aid, opts) {
88
36
  const aunPath = opts?.aunPath ?? resolveRoot();
89
- const client = opts?.client ?? await createBareClient(aunPath);
90
- const ownClient = !opts?.client;
37
+ const store = opts?.store ?? await getAidStore({ slotId: SLOT.cli, aunPath });
38
+ const ownStore = !opts?.store;
91
39
  const localPath = agentMdPath(aid);
92
40
  try {
93
- // Try SDK fetch (auto-saves locally + verifies signature)
94
41
  let content;
95
42
  let verification;
96
- try {
97
- const info = await client.fetchAgentMd(aid);
98
- content = info.content;
99
- const sig = info.signature ?? {};
100
- const status = sig.status === 'verified' ? 'verified' : sig.status === 'unsigned' ? 'unsigned' : 'invalid';
101
- verification = { status, ...(sig.reason ? { reason: String(sig.reason) } : {}) };
43
+ const r = await store.downloadAgentMd(aid);
44
+ if (r.ok) {
45
+ content = r.data.content;
46
+ verification = normalizeVerification(r.data.verification);
102
47
  }
103
- catch (err) {
104
- // Network failed — fall back to local file (verify signature via SDK if requested)
105
- if (!fs.existsSync(localPath))
106
- throw err;
48
+ else {
49
+ // Network/fetch failed — fall back to local file.
50
+ if (!fs.existsSync(localPath)) {
51
+ throw new Error(`fetch agent.md failed for ${aid}: ${r.error.message}`);
52
+ }
107
53
  content = fs.readFileSync(localPath, 'utf-8');
108
54
  if (opts?.withVerification) {
109
- const certPem = await obtainCertPem(aid, aunPath, client);
110
- verification = await verifyContent(content, aid, certPem, client);
55
+ verification = verifyLocal(store, aid, content);
111
56
  }
112
57
  }
113
58
  if (!opts?.withVerification)
114
59
  return content;
115
- return { content: content, verification: verification ?? { status: 'unsigned' } };
60
+ return { content, verification: verification ?? { status: 'unsigned' } };
116
61
  }
117
62
  finally {
118
- if (ownClient)
63
+ if (ownStore)
119
64
  try {
120
- await client.close();
65
+ store.close();
121
66
  }
122
67
  catch { /* ignore */ }
123
68
  }
124
69
  }
125
70
  /**
126
- * Upload agent.md: write to local file → publishAgentMd (auto-sign + upload).
71
+ * Upload agent.md: write local file → store.uploadAgentMd (auto-auth + sign + upload).
72
+ *
73
+ * fastaun 0.4.7: uploadAgentMd moved from AUNClient to AIDStore; the store builds
74
+ * its own LocalTokenStore + AuthFlow internally, so no AUNClient/authenticate needed.
127
75
  */
128
76
  export async function agentmdPut(content, opts) {
129
77
  const aunPath = opts.aunPath ?? resolveRoot();
130
- const client = opts.client ?? await getAunClient(opts.aid, { aunPath });
131
- const ownClient = !opts.client;
78
+ const store = opts.store ?? await getAidStore({ slotId: SLOT.cli, aunPath });
79
+ const ownStore = !opts.store;
132
80
  const dir = aidLocalDir(opts.aid);
133
81
  const filePath = path.join(dir, 'agent.md');
134
82
  const existed = fs.existsSync(filePath);
83
+ // 先写本地文件(与旧行为一致:本地内容更新应在上传失败时仍保留)
84
+ fs.mkdirSync(dir, { recursive: true });
85
+ fs.writeFileSync(filePath, content, 'utf-8');
135
86
  try {
136
- fs.mkdirSync(dir, { recursive: true });
137
- fs.writeFileSync(filePath, content, 'utf-8');
138
- await client.publishAgentMd();
87
+ const r = await store.uploadAgentMd(opts.aid, content);
88
+ if (!r.ok)
89
+ throw new Error(`upload agent.md failed for ${opts.aid}: ${r.error.message}`);
139
90
  }
140
91
  catch (e) {
92
+ // 上传失败:仅当文件原本不存在(本次新建)时回滚,避免留下孤儿文件;
93
+ // 已存在的文件保留新内容(旧语义)。
141
94
  if (!existed)
142
95
  try {
143
96
  fs.unlinkSync(filePath);
@@ -146,34 +99,46 @@ export async function agentmdPut(content, opts) {
146
99
  throw e;
147
100
  }
148
101
  finally {
149
- if (ownClient)
102
+ if (ownStore)
150
103
  try {
151
- await client.close();
104
+ store.close();
152
105
  }
153
106
  catch { /* ignore */ }
154
107
  }
155
108
  }
156
109
  /**
157
- * Check if agent.md is up-to-date (30-day cache), fetch if changed.
110
+ * Check if agent.md is up-to-date (30-day TTL), fetch if changed.
158
111
  * Returns changed=true + content when a new version was downloaded.
112
+ *
113
+ * Note: store.checkAgentMd tracks freshness via the store's in-memory cache,
114
+ * so a freshly-built store reports local_found=false and will fetch.
159
115
  */
160
116
  export async function agentmdSync(aid, opts) {
161
- const client = opts?.client ?? await createBareClient();
162
- const ownClient = !opts?.client;
117
+ const aunPath = opts?.aunPath ?? resolveRoot();
118
+ const store = opts?.store ?? await getAidStore({ slotId: SLOT.cli, aunPath });
119
+ const ownStore = !opts?.store;
120
+ const localPath = agentMdPath(aid);
163
121
  try {
164
- const state = await client.checkAgentMd(aid, 30);
165
- if (!state.in_sync || !state.local_found) {
166
- const info = await client.fetchAgentMd(aid);
167
- return { changed: true, content: info.content };
122
+ const check = await store.checkAgentMd(aid, 30);
123
+ // In sync (cache fresh) — return local file content unchanged.
124
+ if (check.ok && !check.data.needs_update && check.data.local_found) {
125
+ const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
126
+ return { changed: false, content };
127
+ }
128
+ // Needs update (or check failed) — fetch fresh content.
129
+ // SDK's downloadAgentMd persists to disk internally (AgentMdManager.saveRecord → writeContent).
130
+ const fetched = await store.downloadAgentMd(aid);
131
+ if (fetched.ok) {
132
+ return { changed: true, content: fetched.data.content };
168
133
  }
169
- const localPath = agentMdPath(aid);
134
+ // Fetch failed (network) — fall back to local file if present.
170
135
  const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
171
136
  return { changed: false, content };
172
137
  }
173
138
  finally {
174
- if (ownClient)
139
+ if (ownStore)
175
140
  try {
176
- await client.close();
141
+ store.close();
177
142
  }
178
143
  catch { /* ignore */ }
179
144
  }
@@ -24,7 +24,7 @@ export function suppressSdkLogs() {
24
24
  return; process.stderr.write(args.map(String).join(' ') + '\n'); };
25
25
  }
26
26
  // ==================== Constants ====================
27
- export const MIN_AUN_CORE_SDK = [0, 3, 3];
27
+ export const MIN_AUN_CORE_SDK = [0, 4, 3];
28
28
  export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
29
29
  // ==================== SDK & Environment ====================
30
30
  function compareVersion(a, min) {
@@ -113,31 +113,3 @@ export async function downloadCaRoot(aunPath, gatewayUrl, indent = '') {
113
113
  return false;
114
114
  }
115
115
  }
116
- /**
117
- * 统一构造 AUNClient:自动绑 root_ca_path + setAgentMdPath(aidsDir())。
118
- * 不做 createAid / authenticate / connect,调用方按需续作。
119
- *
120
- * 所有 new AUNClient 调用都应走此工厂,避免 SDK 默认把 agent.md 写到
121
- * {aun_path}/AgentMDs(默认目录)。
122
- */
123
- export async function createAunClient(opts = {}) {
124
- const { aunPath: defaultAunPath, aidsDir } = await import('../../paths.js');
125
- const aunPath = opts.aunPath ?? defaultAunPath();
126
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
127
- const { AUNClient } = await import('@agentunion/fastaun');
128
- const clientOpts = { aun_path: aunPath, debug: opts.debug ?? false };
129
- if (fs.existsSync(caCertPath))
130
- clientOpts.root_ca_path = caCertPath;
131
- if (opts.encryptionSeed)
132
- clientOpts.encryption_seed = opts.encryptionSeed;
133
- const client = opts.aunSdkLog !== undefined
134
- ? new AUNClient(clientOpts, opts.aunSdkLog)
135
- : new AUNClient(clientOpts);
136
- client.setAgentMdPath(aidsDir());
137
- return client;
138
- }
139
- export async function getAunClient(aid, opts) {
140
- const client = await createAunClient({ aunPath: opts?.aunPath });
141
- await client.auth.createAid({ aid });
142
- return client;
143
- }