evolclaw 3.1.2 → 3.1.4

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 (48) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +2 -6
  3. package/assets/.env.template +4 -0
  4. package/assets/config.json.template +6 -0
  5. package/assets/wechat-group-qr.jpeg +0 -0
  6. package/dist/agents/claude-runner.js +1 -1
  7. package/dist/agents/codex-runner.js +75 -19
  8. package/dist/agents/gemini-runner.js +0 -2
  9. package/dist/agents/kit-renderer.js +85 -22
  10. package/dist/aun/aid/agentmd.js +67 -74
  11. package/dist/aun/aid/client.js +22 -7
  12. package/dist/aun/aid/identity.js +314 -28
  13. package/dist/aun/aid/index.js +2 -2
  14. package/dist/aun/rpc/connection.js +8 -10
  15. package/dist/channels/aun.js +53 -41
  16. package/dist/cli/agent.js +28 -28
  17. package/dist/cli/bench.js +8 -14
  18. package/dist/cli/help.js +23 -0
  19. package/dist/cli/index.js +398 -73
  20. package/dist/cli/init-channel.js +2 -3
  21. package/dist/cli/init.js +13 -6
  22. package/dist/cli/link-rules.js +2 -1
  23. package/dist/cli/net-check.js +10 -11
  24. package/dist/core/command-handler.js +621 -541
  25. package/dist/core/evolagent.js +31 -0
  26. package/dist/core/message/im-renderer.js +10 -0
  27. package/dist/core/message/message-bridge.js +123 -24
  28. package/dist/core/message/message-processor.js +61 -31
  29. package/dist/core/relation/peer-identity.js +64 -21
  30. package/dist/core/session/session-manager.js +191 -44
  31. package/dist/core/trigger/manager.js +37 -0
  32. package/dist/index.js +4 -1
  33. package/dist/paths.js +87 -16
  34. package/dist/utils/npm-ops.js +18 -11
  35. package/kits/eck_manifest.json +9 -9
  36. package/kits/rules/02-navigation.md +1 -0
  37. package/kits/rules/05-venue.md +2 -2
  38. package/kits/rules/06-channel.md +2 -18
  39. package/kits/templates/system-fragments/baseagent.md +8 -2
  40. package/kits/templates/system-fragments/channel.md +20 -8
  41. package/kits/templates/system-fragments/identity.md +5 -6
  42. package/kits/templates/system-fragments/relation.md +10 -5
  43. package/kits/templates/system-fragments/session.md +20 -0
  44. package/kits/templates/system-fragments/venue.md +5 -3
  45. package/package.json +4 -2
  46. package/dist/net-check.js +0 -640
  47. package/dist/watch-msg.js +0 -544
  48. package/kits/templates/system-fragments/runtime.md +0 -19
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.4 (2026-05-27)
4
+
5
+ ### New Features
6
+
7
+ - **CLI help 系统重构 + ECK 注入** — 全新 help 子命令体系,ECK(EvolClaw Kit)注入机制重构,AID identity 管理命令
8
+ - **Menu 协议五动词拆分** — menu 协议拆分为 list/query/options/update/action 五个动词,命令收敛至 baseagent/session
9
+
10
+ ### Improvements
11
+
12
+ - **npm-ops 去 CLI 依赖** — checkLatestVersion 改用 HTTP fetch 直接查 registry API,去除对 npm CLI 的运行时依赖
13
+ - **agent.md 适配 SDK 0.3.3** — 统一 aun_path 到 EVOLCLAW_HOME,适配最新 SDK 版本
14
+
15
+ ### Bug Fixes
16
+
17
+ - **Session channelType 缺失修复** — 修复 channelType 缺失导致 chat dir 无法解析 + isBackgroundSession 同步化
18
+ - **SessionManager 查询路径 NPE** — 修复缺失 channelType 时的空指针异常
19
+
20
+ ## v3.1.3 (2026-05-26)
21
+
22
+ ### New Features
23
+
24
+ - **Menu 协议拆分为 5 动词** — `menu.list` / `menu.query` / `menu.options` / `menu.update` / `menu.action`,response 统一 `error.code` 透传(`NO_ACTIVE_SESSION` / `MISSING_VALUE` / ...),`name → cmd` 由 bridge 内部映射,AUN 白名单接纳全部 `menu.*` 类型
25
+ - **xhigh effort 档位** — Claude/Codex 新增 `xhigh` 推理强度档位,Codex 通过 `codex debug models` 动态拉取模型目录与各模型支持的 effort 列表
26
+ - **结构化 menu options 字段** — session 子菜单返回 `preview`(首条消息预览)/ `lastActive` / `agentSessionId` / `turns` 扩展字段,所有可选项菜单返回 `selected` 标记当前值
27
+
28
+ ### Improvements
29
+
30
+ - **命令收敛** — 移除 `/plist` `/project` `/bind` `/agentmd` `/p` 别名;`/agent` 改为 EvolAgent 管理命令;baseagent 切换统一为 `/baseagent`(别名 `/base`);`/aid` `/rpc` `/storage` `/evolagent` 改为 ctl 专属
31
+ - **execMenu 拆分** — `command-handler` 拆出 `execMenuQuery` / `execMenuUpdate` / `execMenuAction` 三入口,无活跃会话时多项 fallback 到 evolagent config(`/pwd` / `/chatmode` / `/dispatch` 等)
32
+ - **EvolAgent 持久化方法** — 新增 `setActiveBaseagent` / `setChatmodePrivate` / `setDispatch`,配置变更直接落盘
33
+ - **Codex 实例每次重建** — 通过 env 注入 `EVOLCLAW_SESSION_ID`,仅首轮拼接 `systemPromptAppend`,避免 resume 时重复污染历史
34
+ - **Agent plugin 启用判定** — `isEnabled` 改为按 `baseagents.<name>` 配置启用,Codex 额外检测 `@openai/codex-sdk` 是否可用,`init` / `agent new` CLI 同步该判定
35
+ - **context-too-long 提示文案** — 按 agent 是否支持 compact 区分提示,retry 后仍超长时清理 renderer 中混入的错误文本(新增 `IMRenderer.stripContextError`)
36
+
37
+ ### Bug Fixes
38
+
39
+ - **Codex resume 历史污染** — systemPromptAppend 在 resume 轮次重复拼接到 prompt 头部,导致系统提示被反复写入对话历史
40
+
3
41
  ## v3.1.2 (2026-05-25)
4
42
 
5
43
  ### Improvements
package/README.md CHANGED
@@ -243,14 +243,11 @@ evolclaw/
243
243
 
244
244
  ### 管理员级命令(Admin+ 可用)
245
245
 
246
- **项目管理**:
246
+ **项目**:
247
247
  - `/pwd` - 显示当前项目路径
248
- - `/plist` - 列出所有项目(显示会话空闲时间)
249
- - `/p <name|path>` - 切换项目(保留会话历史)
250
- - `/bind <path>` - 绑定新项目目录
251
248
 
252
249
  **Agent 与模型**:
253
- - `/agent [name]` - 查看或切换 Agent 后端(claude / codex / gemini
250
+ - `/baseagent [name]` - 查看或切换 Agent 后端(claude / codex / gemini)(别名 `/base`)
254
251
  - `/model [model]` - 查看或切换模型
255
252
  - `/effort [level]` - 查看或切换推理强度(low / medium / high / max / auto)
256
253
  - `/perm [mode]` - 查看或切换权限模式(auto / edit / default / readonly)
@@ -272,7 +269,6 @@ evolclaw/
272
269
  - `/file <文件路径>` - 发送文件给用户
273
270
  - `/restart` - 重启服务(自愈机制)
274
271
  - `/repair` - 检查并修复会话
275
- - `/agentmd [put|set]` - 管理 AUN agent.md(仅 AUN 渠道)
276
272
 
277
273
  ## 技术栈
278
274
 
@@ -0,0 +1,4 @@
1
+ # AUN 环境变量
2
+ # 取消注释并填入值即可生效(进程已有的同名变量不会被覆盖)
3
+
4
+ # AUN_ENCRYPTION_SEED=your-seed-here
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema_version": 1,
3
+ "aun": {
4
+ "encryptionSeed": null
5
+ }
6
+ }
Binary file
@@ -1094,7 +1094,7 @@ export class AgentRunner {
1094
1094
  export class ClaudeAgentPlugin {
1095
1095
  name = 'claude';
1096
1096
  isEnabled(agent) {
1097
- return agent.baseagent === 'claude';
1097
+ return !!agent.config.baseagents?.claude;
1098
1098
  }
1099
1099
  createAgent(agent, callbacks) {
1100
1100
  const override = agent.config.baseagents?.claude;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { resolveOpenaiConfig } from './resolve.js';
9
9
  import { logger } from '../utils/logger.js';
10
+ import { execFileSync } from 'child_process';
10
11
  import fs from 'fs';
11
12
  import path from 'path';
12
13
  import os from 'os';
@@ -17,13 +18,58 @@ const MIME_EXT = {
17
18
  'image/gif': '.gif',
18
19
  'image/webp': '.webp',
19
20
  };
20
- // ── Codex 模型列表 ──
21
- const CODEX_MODELS = ['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5-codex', 'gpt-5.2', 'gpt-5.4'];
21
+ const CODEX_CATALOG_FALLBACK = [
22
+ { slug: 'gpt-5.5', efforts: ['low', 'medium', 'high', 'xhigh'] },
23
+ { slug: 'gpt-5.4', efforts: ['low', 'medium', 'high', 'xhigh'] },
24
+ { slug: 'gpt-5.4-mini', efforts: ['low', 'medium', 'high', 'xhigh'] },
25
+ { slug: 'gpt-5.3-codex', efforts: ['low', 'medium', 'high', 'xhigh'] },
26
+ { slug: 'gpt-5.2', efforts: ['low', 'medium', 'high', 'xhigh'] },
27
+ ];
28
+ let codexCatalogCache = null;
29
+ export function isCodexSdkAvailable() {
30
+ try {
31
+ import.meta.resolve('@openai/codex-sdk');
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ function fetchCodexCatalog() {
39
+ if (codexCatalogCache)
40
+ return codexCatalogCache;
41
+ try {
42
+ const output = execFileSync('codex', ['debug', 'models'], {
43
+ encoding: 'utf-8',
44
+ timeout: 5000,
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ });
47
+ const catalog = JSON.parse(output);
48
+ const models = catalog.models
49
+ .filter(m => m.visibility === 'list')
50
+ .map(m => ({
51
+ slug: m.slug,
52
+ efforts: (m.supported_reasoning_levels || []).map(l => l.effort),
53
+ }));
54
+ if (models.length > 0) {
55
+ codexCatalogCache = models;
56
+ return models;
57
+ }
58
+ }
59
+ catch (e) {
60
+ logger.debug(`[CodexRunner] Failed to fetch model catalog, using fallback: ${e}`);
61
+ }
62
+ return CODEX_CATALOG_FALLBACK;
63
+ }
64
+ export function getCodexEfforts(model) {
65
+ const catalog = fetchCodexCatalog();
66
+ const entry = catalog.find(m => m.slug === model);
67
+ return entry?.efforts ?? catalog[0]?.efforts ?? ['low', 'medium', 'high'];
68
+ }
22
69
  // ── Codex Runner ──
23
70
  export class CodexRunner {
24
71
  name = 'codex';
25
72
  capabilities = { clear: false, compact: false, fork: false };
26
- codex = null;
27
73
  codexModule = null;
28
74
  model;
29
75
  effort;
@@ -39,21 +85,25 @@ export class CodexRunner {
39
85
  this.effort = this.resolvedConfig.effort;
40
86
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
41
87
  }
42
- async ensureCodex() {
43
- if (!this.codex || !this.codexModule) {
88
+ async ensureCodex(sessionId) {
89
+ if (!this.codexModule) {
44
90
  const { requireOptional } = await import('../utils/npm-ops.js');
45
91
  this.codexModule = await requireOptional('@openai/codex-sdk');
46
- this.codex = new this.codexModule.Codex({
47
- apiKey: this.resolvedConfig.apiKey,
48
- baseUrl: this.resolvedConfig.baseUrl,
49
- });
50
92
  }
51
- return { codex: this.codex, mod: this.codexModule };
93
+ const codex = new this.codexModule.Codex({
94
+ apiKey: this.resolvedConfig.apiKey,
95
+ baseUrl: this.resolvedConfig.baseUrl,
96
+ env: {
97
+ ...process.env,
98
+ EVOLCLAW_SESSION_ID: sessionId,
99
+ },
100
+ });
101
+ return { codex, mod: this.codexModule };
52
102
  }
53
103
  // ── ModelSwitcher ──
54
104
  setModel(model) { this.model = model; }
55
105
  getModel() { return this.model; }
56
- listModels() { return CODEX_MODELS; }
106
+ listModels() { return fetchCodexCatalog().map(m => m.slug); }
57
107
  // ── Effort ──
58
108
  setEffort(effort) { this.effort = effort; }
59
109
  getEffort() { return this.effort; }
@@ -94,10 +144,14 @@ export class CodexRunner {
94
144
  }
95
145
  // ── Core: runQuery ──
96
146
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager) {
97
- // Agent ctl: 注入 EVOLCLAW_SESSION_ID 供子进程使用
98
- process.env.EVOLCLAW_SESSION_ID = sessionId;
99
- const { codex } = await this.ensureCodex();
147
+ const { codex } = await this.ensureCodex(sessionId);
100
148
  let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
149
+ let fullPrompt = prompt;
150
+ // Only inject system context on the first turn; resumed Codex threads already
151
+ // have that context in history and repeating it will pollute the conversation.
152
+ if (systemPromptAppend && !agentSessionId) {
153
+ fullPrompt = prompt + '\n\n--- [SYSTEM_PROMPT_END] ---\n' + systemPromptAppend;
154
+ }
101
155
  const threadOptions = {
102
156
  workingDirectory: projectPath,
103
157
  model: this.model,
@@ -116,7 +170,7 @@ export class CodexRunner {
116
170
  let input;
117
171
  if (images?.length) {
118
172
  const tmpDir = os.tmpdir();
119
- const parts = [{ type: 'text', text: prompt }];
173
+ const parts = [{ type: 'text', text: fullPrompt }];
120
174
  for (let i = 0; i < images.length; i++) {
121
175
  const img = images[i];
122
176
  const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
@@ -129,7 +183,7 @@ export class CodexRunner {
129
183
  logger.info(`[CodexRunner] Attached ${images.length} image(s) as local_image`);
130
184
  }
131
185
  else {
132
- input = prompt;
186
+ input = fullPrompt;
133
187
  }
134
188
  const { events } = await thread.runStreamed(input, { signal: controller.signal });
135
189
  // 包装为 AgentEvent 流
@@ -302,10 +356,10 @@ export class CodexRunner {
302
356
  export class CodexAgentPlugin {
303
357
  name = 'codex';
304
358
  isEnabled(agent) {
305
- if (agent.baseagent !== 'codex')
306
- return false;
307
359
  if (!agent.config.baseagents?.codex)
308
360
  return false;
361
+ if (!isCodexSdkAvailable())
362
+ return false;
309
363
  try {
310
364
  const override = agent.config.baseagents.codex;
311
365
  const syntheticConfig = { agents: { codex: override } };
@@ -317,8 +371,10 @@ export class CodexAgentPlugin {
317
371
  }
318
372
  }
319
373
  createAgent(agent, callbacks) {
374
+ if (!isCodexSdkAvailable()) {
375
+ throw new Error('Missing optional dependency @openai/codex-sdk');
376
+ }
320
377
  const override = agent.config.baseagents?.codex;
321
- const syntheticConfig = { agents: { codex: override } };
322
378
  const merged = {
323
379
  agents: { codex: { ...(override || {}) } },
324
380
  };
@@ -407,8 +407,6 @@ export class GeminiRunner {
407
407
  export class GeminiAgentPlugin {
408
408
  name = 'gemini';
409
409
  isEnabled(agent) {
410
- if (agent.baseagent !== 'gemini')
411
- return false;
412
410
  const geminiCfg = agent.config.baseagents?.gemini;
413
411
  if (!geminiCfg)
414
412
  return false;
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { kitsDir, eckDebugDir, resolveRoot } from '../paths.js';
3
+ import { kitsDir, eckDebugDir, resolveRoot, getPackageRoot } from '../paths.js';
4
4
  import { logger } from '../utils/logger.js';
5
5
  // ── Param descriptions (for debug output) ──
6
6
  const PARAM_DESCRIPTIONS = {
@@ -14,21 +14,60 @@ const PARAM_DESCRIPTIONS = {
14
14
  peerId: '对端在该渠道的原生 ID',
15
15
  peerKey: '对端跨渠道唯一标识(channel#urlEncode(peerId))',
16
16
  peerName: '对端显示名',
17
- peerRole: '对端角色',
17
+ peerRole: '对端角色(owner/admin/guest/anonymous)',
18
+ peerType: '对端类型(human/agent)',
18
19
  groupId: '群组 ID(群聊时)',
19
- scene: '场景类型',
20
- chatType: '聊天类型',
21
- channel: '当前渠道',
22
- venueUid: 'venue 唯一标识',
23
- project: '当前项目目录名(由 CURRENT_PROJECT 派生)',
20
+ chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
21
+ channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
22
+ venueUid: '场所唯一标识(预留)',
23
+ capabilities: '当前渠道支持的能力列表',
24
+ project: '当前项目目录名',
25
+ sessionId: 'evolclaw 会话 ID',
24
26
  sessionName: '会话名称',
25
- sessionMode: '会话模式',
27
+ sessionCreatedAt: '会话创建时间(ISO)',
28
+ threadId: '话题 ID(多话题路由时)',
29
+ chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
26
30
  readonly: '是否只读模式',
27
- canSendFile: '当前渠道是否支持发文件',
28
- capabilities: '渠道能力列表',
29
- baseAgent: '当前 base agent 规范值(claude/codex/gemini/hermes)',
30
- baseAgentName: '当前 base agent 显示名',
31
+ baseAgent: 'base agent 规范值(claude/codex/gemini/hermes)',
32
+ baseAgentName: 'base agent 显示名',
33
+ baseAgentModel: 'base agent 使用的模型',
34
+ agentSessionId: 'base agent 会话 ID',
31
35
  };
36
+ function buildPathMappings(vars) {
37
+ const pkgRoot = getPackageRoot();
38
+ const evolHome = String(vars['EVOLCLAW_HOME'] || resolveRoot());
39
+ const selfAid = vars['selfAid'] ? String(vars['selfAid']) : '';
40
+ const currentProject = vars['CURRENT_PROJECT'] ? String(vars['CURRENT_PROJECT']) : '';
41
+ const mappings = [
42
+ { prefix: path.join(pkgRoot, 'kits', 'rules'), alias: '$KITS_RULES' },
43
+ { prefix: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'), alias: '$KITS_FRAGMENTS' },
44
+ { prefix: path.join(pkgRoot, 'kits', 'templates'), alias: '$KITS_TEMPLATES' },
45
+ { prefix: path.join(pkgRoot, 'kits', 'docs'), alias: '$KITS_DOCS' },
46
+ { prefix: path.join(pkgRoot, 'kits'), alias: '$KITS' },
47
+ { prefix: pkgRoot, alias: '$PACKAGE_ROOT' },
48
+ ];
49
+ if (selfAid) {
50
+ mappings.push({ prefix: path.join(evolHome, 'agents', selfAid), alias: '$AGENT_DIR' });
51
+ }
52
+ mappings.push({ prefix: evolHome, alias: '$EVOLCLAW_HOME' });
53
+ if (currentProject) {
54
+ mappings.push({ prefix: currentProject, alias: '$CURRENT_PROJECT' });
55
+ }
56
+ // Sort by prefix length descending so longer (more specific) paths match first
57
+ mappings.sort((a, b) => b.prefix.length - a.prefix.length);
58
+ return mappings;
59
+ }
60
+ function shortenPath(filePath, mappings) {
61
+ const normalized = filePath.replace(/\\/g, '/');
62
+ for (const { prefix, alias } of mappings) {
63
+ const normalizedPrefix = prefix.replace(/\\/g, '/');
64
+ if (normalized.startsWith(normalizedPrefix)) {
65
+ const rest = normalized.slice(normalizedPrefix.length);
66
+ return alias + rest;
67
+ }
68
+ }
69
+ return filePath;
70
+ }
32
71
  // ── Cache ──
33
72
  let _manifestCache = null;
34
73
  const _sessionPathCache = new Map();
@@ -49,6 +88,8 @@ export function renderKitSections(ctx) {
49
88
  loadKitManifest();
50
89
  const sections = _manifestCache;
51
90
  const fileParts = [];
91
+ const fragmentParts = [];
92
+ const pathMappings = buildPathMappings(ctx.vars);
52
93
  for (const section of sections) {
53
94
  if (section.enabled === false)
54
95
  continue;
@@ -62,14 +103,20 @@ export function renderKitSections(ctx) {
62
103
  if (!content.trim())
63
104
  continue;
64
105
  const label = section.description ? `${section.id} — ${section.description}` : section.id;
65
- fileParts.push(`Contenu de ${filePath} (${label}):\n\n${content.trimEnd()}`);
106
+ const displayPath = shortenPath(filePath, pathMappings);
107
+ const part = `Contenu de ${displayPath} (${label}):\n\n${content.trimEnd()}`;
108
+ fileParts.push(part);
109
+ if (section.needsInjection) {
110
+ fragmentParts.push(part);
111
+ }
66
112
  }
67
113
  }
68
114
  if (fileParts.length === 0)
69
115
  return '';
70
116
  const body = fileParts.join('\n\n');
71
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>`;
72
- writeDebugFiles(ctx, output);
118
+ const fragmentsOutput = fragmentParts.length > 0 ? fragmentParts.join('\n\n') : '';
119
+ writeDebugFiles(ctx, output, fragmentsOutput);
73
120
  return output;
74
121
  }
75
122
  export function cleanEckDebug() {
@@ -233,14 +280,27 @@ function isTruthy(val) {
233
280
  }
234
281
  // CHUNK_CONTINUE_6
235
282
  // ── Template rendering ──
283
+ function resolveConditions(template, vars) {
284
+ // Find innermost {{?...}}...{{/}} block (no nested {{? inside) and resolve it.
285
+ // Repeat until no blocks remain.
286
+ const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}([^]*?)\{\{\/\}\}/;
287
+ let result = template;
288
+ let prev;
289
+ do {
290
+ prev = result;
291
+ result = result.replace(inner, (_match, key, op, value, body) => {
292
+ if (op === '=')
293
+ return String(vars[key]) === value ? body : '';
294
+ if (op === '!=')
295
+ return String(vars[key]) !== value ? body : '';
296
+ return isTruthy(vars[key]) ? body : '';
297
+ });
298
+ } while (result !== prev);
299
+ return result;
300
+ }
236
301
  function renderTemplate(template, vars) {
237
- // Pass 1: conditional sections {{?key=value}}...{{/}} and {{?key}}...{{/}}
238
- let result = template.replace(/\{\{\?(\w+)(?:=([^}]*))?\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, value, body) => {
239
- if (value !== undefined) {
240
- return String(vars[key]) === value ? body : '';
241
- }
242
- return isTruthy(vars[key]) ? body : '';
243
- });
302
+ // Pass 1: resolve nested conditionals inside-out
303
+ let result = resolveConditions(template, vars);
244
304
  // Pass 2: variable substitution {{key}}
245
305
  result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
246
306
  const val = vars[key];
@@ -261,7 +321,7 @@ function getSessionCache(sessionId) {
261
321
  return cache;
262
322
  }
263
323
  // ── Debug output ──
264
- function writeDebugFiles(ctx, output) {
324
+ function writeDebugFiles(ctx, output, fragmentsOutput) {
265
325
  const now = new Date();
266
326
  const ts = now.toISOString().replace(/[T:.]/g, '-').slice(0, 19);
267
327
  const dir = eckDebugDir();
@@ -278,4 +338,7 @@ function writeDebugFiles(ctx, output) {
278
338
  };
279
339
  fs.writeFile(path.join(dir, `vars-${ts}.json`), JSON.stringify(varsData, null, 2), () => { });
280
340
  fs.writeFile(path.join(dir, `context-${ts}.md`), output, () => { });
341
+ if (fragmentsOutput) {
342
+ fs.writeFile(path.join(dir, `fragments-${ts}.md`), fragmentsOutput, () => { });
343
+ }
281
344
  }
@@ -1,8 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
- import { getAunClient } from './client.js';
5
- import { agentMdPath, aidLocalDir } from '../../paths.js';
3
+ import { getAunClient, createAunClient } from './client.js';
4
+ import { agentMdPath, aidLocalDir, resolveRoot } from '../../paths.js';
6
5
  export function buildInitialAgentMd(opts) {
7
6
  const agentName = opts.aid.split('.')[0];
8
7
  const agentType = opts.type || 'ai';
@@ -83,71 +82,37 @@ async function verifyContent(content, aid, certPem, client) {
83
82
  * Create a bare AUNClient (no createAid) for read-only operations.
84
83
  */
85
84
  async function createBareClient(aunPath) {
86
- const { AUNClient } = await import('@agentunion/fastaun');
87
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
88
- const clientOpts = { aun_path: aunPath, debug: false };
89
- if (fs.existsSync(caCertPath))
90
- clientOpts.root_ca_path = caCertPath;
91
- return new AUNClient(clientOpts);
85
+ return createAunClient({ aunPath });
92
86
  }
93
87
  export async function agentmdGet(aid, opts) {
94
- const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
95
- const localPath = agentMdPath(aid);
96
- // === Path A: local agent.md exists ===
97
- if (fs.existsSync(localPath)) {
98
- const content = fs.readFileSync(localPath, 'utf-8');
99
- if (!opts?.withVerification)
100
- return content;
101
- // Verify local content
102
- const client = opts?.client ?? await createBareClient(aunPath);
103
- const ownClient = !opts?.client;
104
- try {
105
- const certPem = await obtainCertPem(aid, aunPath, client);
106
- const verification = await verifyContent(content, aid, certPem, client);
107
- if (verification.status !== 'invalid') {
108
- return { content, verification };
109
- }
110
- // Fallback: local invalid → try remote
111
- try {
112
- const remote = await client.auth.downloadAgentMd(aid);
113
- if (remote) {
114
- const remoteVerification = await verifyContent(remote, aid, certPem, client);
115
- if (remoteVerification.status === 'verified') {
116
- fs.writeFileSync(localPath, remote, 'utf-8');
117
- return { content: remote, verification: remoteVerification };
118
- }
119
- }
120
- }
121
- catch { /* remote fetch failed, return local invalid result */ }
122
- return { content, verification };
123
- }
124
- finally {
125
- if (ownClient)
126
- try {
127
- await client.close();
128
- }
129
- catch { /* ignore */ }
130
- }
131
- }
132
- // === Path B: no local agent.md → download from remote ===
88
+ const aunPath = opts?.aunPath ?? resolveRoot();
133
89
  const client = opts?.client ?? await createBareClient(aunPath);
134
90
  const ownClient = !opts?.client;
91
+ const localPath = agentMdPath(aid);
135
92
  try {
136
- const raw = await client.auth.downloadAgentMd(aid);
137
- if (!opts?.withVerification) {
138
- // Persist without verification
139
- const aidDir = aidLocalDir(aid);
140
- fs.mkdirSync(aidDir, { recursive: true });
141
- fs.writeFileSync(path.join(aidDir, 'agent.md'), raw, 'utf-8');
142
- return raw;
93
+ // Try SDK fetch (auto-saves locally + verifies signature)
94
+ let content;
95
+ 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) } : {}) };
102
+ }
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;
107
+ content = fs.readFileSync(localPath, 'utf-8');
108
+ if (opts?.withVerification) {
109
+ const certPem = await obtainCertPem(aid, aunPath, client);
110
+ verification = await verifyContent(content, aid, certPem, client);
111
+ }
143
112
  }
144
- const certPem = await obtainCertPem(aid, aunPath, client);
145
- const verification = await verifyContent(raw, aid, certPem, client);
146
- // Persist to local
147
- const aidDir = aidLocalDir(aid);
148
- fs.mkdirSync(aidDir, { recursive: true });
149
- fs.writeFileSync(path.join(aidDir, 'agent.md'), raw, 'utf-8');
150
- return { content: raw, verification };
113
+ if (!opts?.withVerification)
114
+ return content;
115
+ return { content: content, verification: verification ?? { status: 'unsigned' } };
151
116
  }
152
117
  finally {
153
118
  if (ownClient)
@@ -158,24 +123,52 @@ export async function agentmdGet(aid, opts) {
158
123
  }
159
124
  }
160
125
  /**
161
- * Upload agent.md: auto-sign + upload + sync to local file.
126
+ * Upload agent.md: write to local file → publishAgentMd (auto-sign + upload).
162
127
  */
163
128
  export async function agentmdPut(content, opts) {
164
- const aunPath = opts.aunPath ?? path.join(os.homedir(), '.aun');
129
+ const aunPath = opts.aunPath ?? resolveRoot();
165
130
  const client = opts.client ?? await getAunClient(opts.aid, { aunPath });
166
131
  const ownClient = !opts.client;
132
+ const dir = aidLocalDir(opts.aid);
133
+ const filePath = path.join(dir, 'agent.md');
134
+ const existed = fs.existsSync(filePath);
167
135
  try {
168
- let signed;
169
- try {
170
- signed = await client.auth.signAgentMd(content);
171
- }
172
- catch {
173
- signed = content;
174
- }
175
- await client.auth.uploadAgentMd(signed);
176
- const dir = aidLocalDir(opts.aid);
177
136
  fs.mkdirSync(dir, { recursive: true });
178
- fs.writeFileSync(path.join(dir, 'agent.md'), signed, 'utf-8');
137
+ fs.writeFileSync(filePath, content, 'utf-8');
138
+ await client.publishAgentMd();
139
+ }
140
+ catch (e) {
141
+ if (!existed)
142
+ try {
143
+ fs.unlinkSync(filePath);
144
+ }
145
+ catch { /* ignore */ }
146
+ throw e;
147
+ }
148
+ finally {
149
+ if (ownClient)
150
+ try {
151
+ await client.close();
152
+ }
153
+ catch { /* ignore */ }
154
+ }
155
+ }
156
+ /**
157
+ * Check if agent.md is up-to-date (30-day cache), fetch if changed.
158
+ * Returns changed=true + content when a new version was downloaded.
159
+ */
160
+ export async function agentmdSync(aid, opts) {
161
+ const client = opts?.client ?? await createBareClient();
162
+ const ownClient = !opts?.client;
163
+ 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 };
168
+ }
169
+ const localPath = agentMdPath(aid);
170
+ const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
171
+ return { changed: false, content };
179
172
  }
180
173
  finally {
181
174
  if (ownClient)
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import { fileURLToPath } from 'url';
5
4
  import { execFileSync } from 'child_process';
6
5
  import { isWindows } from '../../utils/cross-platform.js';
@@ -25,7 +24,7 @@ export function suppressSdkLogs() {
25
24
  return; process.stderr.write(args.map(String).join(' ') + '\n'); };
26
25
  }
27
26
  // ==================== Constants ====================
28
- export const MIN_AUN_CORE_SDK = [0, 2, 17];
27
+ export const MIN_AUN_CORE_SDK = [0, 3, 3];
29
28
  export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
30
29
  // ==================== SDK & Environment ====================
31
30
  function compareVersion(a, min) {
@@ -114,15 +113,31 @@ export async function downloadCaRoot(aunPath, gatewayUrl, indent = '') {
114
113
  return false;
115
114
  }
116
115
  }
117
- // ==================== AUNClient Factory ====================
118
- export async function getAunClient(aid, opts) {
119
- const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
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();
120
126
  const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
121
127
  const { AUNClient } = await import('@agentunion/fastaun');
122
- const clientOpts = { aun_path: aunPath, debug: false };
128
+ const clientOpts = { aun_path: aunPath, debug: opts.debug ?? false };
123
129
  if (fs.existsSync(caCertPath))
124
130
  clientOpts.root_ca_path = caCertPath;
125
- const client = new AUNClient(clientOpts);
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 });
126
141
  await client.auth.createAid({ aid });
127
142
  return client;
128
143
  }