evolclaw 2.8.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +21 -12
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +108 -46
  5. package/dist/agents/codex-runner.js +13 -14
  6. package/dist/agents/gemini-runner.js +15 -17
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/agents/resolve.js +134 -0
  9. package/dist/aun/aid/agentmd.js +186 -0
  10. package/dist/aun/aid/client.js +134 -0
  11. package/dist/aun/aid/identity.js +159 -0
  12. package/dist/aun/aid/index.js +3 -0
  13. package/dist/aun/aid/lifecycle-log.js +33 -0
  14. package/dist/aun/aid/types.js +1 -0
  15. package/dist/aun/aid/validation.js +21 -0
  16. package/dist/aun/msg/group.js +293 -0
  17. package/dist/aun/msg/index.js +4 -0
  18. package/dist/aun/msg/p2p.js +147 -0
  19. package/dist/aun/msg/payload-type.js +27 -0
  20. package/dist/aun/msg/upload.js +98 -0
  21. package/dist/aun/outbox.js +138 -0
  22. package/dist/aun/rpc/caller.js +42 -0
  23. package/dist/aun/rpc/connection.js +34 -0
  24. package/dist/aun/rpc/index.js +2 -0
  25. package/dist/aun/storage/download.js +29 -0
  26. package/dist/aun/storage/index.js +3 -0
  27. package/dist/aun/storage/manage.js +10 -0
  28. package/dist/aun/storage/upload.js +35 -0
  29. package/dist/channels/aun.js +1340 -349
  30. package/dist/channels/dingtalk.js +59 -5
  31. package/dist/channels/feishu.js +381 -32
  32. package/dist/channels/qqbot.js +68 -12
  33. package/dist/channels/wechat.js +63 -4
  34. package/dist/channels/wecom.js +59 -5
  35. package/dist/cli/agent.js +800 -0
  36. package/dist/cli/bench.js +1219 -0
  37. package/dist/cli/index.js +4513 -0
  38. package/dist/{utils → cli}/init-channel.js +211 -621
  39. package/dist/cli/init.js +178 -0
  40. package/dist/cli/link-rules.js +245 -0
  41. package/dist/cli/net-check.js +640 -0
  42. package/dist/cli/watch-msg.js +589 -0
  43. package/dist/config-store.js +645 -0
  44. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  45. package/dist/core/channel-loader.js +176 -12
  46. package/dist/core/command-handler.js +883 -848
  47. package/dist/core/evolagent-registry.js +191 -371
  48. package/dist/core/evolagent.js +202 -238
  49. package/dist/core/interaction-router.js +52 -5
  50. package/dist/core/message/im-renderer.js +486 -0
  51. package/dist/core/message/items-formatter.js +68 -0
  52. package/dist/core/message/message-bridge.js +109 -56
  53. package/dist/core/message/message-log.js +93 -0
  54. package/dist/core/message/message-processor.js +430 -212
  55. package/dist/core/message/message-queue.js +13 -6
  56. package/dist/core/permission.js +116 -11
  57. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  58. package/dist/core/session/session-fs-store.js +230 -0
  59. package/dist/core/session/session-manager.js +740 -777
  60. package/dist/core/session/session-mapper.js +87 -0
  61. package/dist/core/trigger/manager.js +122 -0
  62. package/dist/core/trigger/parser.js +128 -0
  63. package/dist/core/trigger/scheduler.js +224 -0
  64. package/dist/data/error-dict.json +118 -0
  65. package/dist/eck/baseagent-caps.js +18 -0
  66. package/dist/eck/detect.js +47 -0
  67. package/dist/eck/init.js +77 -0
  68. package/dist/eck/rules-loader.js +28 -0
  69. package/dist/index.js +560 -283
  70. package/dist/ipc.js +49 -0
  71. package/dist/net-check.js +640 -0
  72. package/dist/paths.js +73 -9
  73. package/dist/types.js +8 -2
  74. package/dist/utils/aid-lifecycle-log.js +33 -0
  75. package/dist/utils/atomic-write.js +89 -0
  76. package/dist/utils/channel-helpers.js +46 -0
  77. package/dist/utils/cross-platform.js +17 -26
  78. package/dist/utils/error-utils.js +10 -2
  79. package/dist/utils/instance-registry.js +434 -0
  80. package/dist/utils/log-writer.js +217 -0
  81. package/dist/utils/logger.js +34 -77
  82. package/dist/utils/media-cache.js +23 -0
  83. package/dist/utils/npm-ops.js +163 -0
  84. package/dist/utils/process-introspect.js +122 -0
  85. package/dist/utils/stats.js +192 -0
  86. package/dist/watch-msg.js +544 -0
  87. package/evolclaw-install-aun.md +127 -47
  88. package/kits/docs/GUIDE.md +20 -0
  89. package/kits/docs/INDEX.md +52 -0
  90. package/kits/docs/aun/CHEATSHEET.md +17 -0
  91. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  92. package/kits/docs/channels/aun.md +25 -0
  93. package/kits/docs/channels/feishu.md +27 -0
  94. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  95. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  96. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  97. package/kits/docs/eck_templates/runtime.template.md +19 -0
  98. package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
  99. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  100. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  101. package/kits/docs/evolclaw/self-summary.md +29 -0
  102. package/kits/docs/evolclaw/tools.md +25 -0
  103. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  104. package/kits/docs/identity/PATH_OPS.md +16 -0
  105. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  106. package/kits/docs/identity/identity-tools.md +26 -0
  107. package/kits/docs/path-registry.md +43 -0
  108. package/kits/eck_manifest.json +95 -0
  109. package/kits/rules/01-overview.md +120 -0
  110. package/kits/rules/02-navigation.md +75 -0
  111. package/kits/rules/03-identity.md +34 -0
  112. package/kits/rules/04-relation.md +49 -0
  113. package/kits/rules/05-venue.md +45 -0
  114. package/kits/rules/06-channel.md +43 -0
  115. package/kits/templates/system-fragments/baseagent.md +2 -0
  116. package/kits/templates/system-fragments/channel.md +10 -0
  117. package/kits/templates/system-fragments/identity.md +12 -0
  118. package/kits/templates/system-fragments/relation.md +9 -0
  119. package/kits/templates/system-fragments/runtime.md +19 -0
  120. package/kits/templates/system-fragments/venue.md +5 -0
  121. package/package.json +10 -6
  122. package/data/evolclaw.sample.json +0 -60
  123. package/dist/agents/templates.js +0 -122
  124. package/dist/channels/aun-ops.js +0 -275
  125. package/dist/cli.js +0 -2178
  126. package/dist/config.js +0 -591
  127. package/dist/core/agent-registry.js +0 -450
  128. package/dist/core/evolagent-schema.js +0 -72
  129. package/dist/core/message/stream-flusher.js +0 -238
  130. package/dist/core/message/thought-emitter.js +0 -162
  131. package/dist/core/reload-hooks.js +0 -87
  132. package/dist/prompts/templates.js +0 -122
  133. package/dist/templates/prompts.md +0 -104
  134. package/dist/templates/skills.md +0 -66
  135. package/dist/utils/channel-fingerprint.js +0 -59
  136. package/dist/utils/error-dict.js +0 -63
  137. package/dist/utils/format.js +0 -32
  138. package/dist/utils/init.js +0 -645
  139. package/dist/utils/migrate-project.js +0 -122
  140. package/dist/utils/reload-hooks.js +0 -87
  141. package/dist/utils/stats-collector.js +0 -99
  142. package/dist/utils/upgrade.js +0 -100
package/README.md CHANGED
@@ -20,6 +20,9 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
20
20
  - 💾 **会话持久化**:会话数据与 CLI 工具共享,不额外存储,服务重启不丢失
21
21
  - ⚡ **执行中插入**:任务执行中可发送新消息,自动中断当前任务并处理新请求
22
22
  - 🔕 **消息智能发送**:前台任务动态聚合批量发送,后台任务静默完成后通知
23
+ - 🧩 **EvolAgent 多实例**:一个 JSON 文件定义一个 Agent(channels + baseagent + project),多 Agent 并发运行,Agent 运行时隔离 + 热重载无需重启
24
+ - 🔔 **AI 自主触发器**:Agent 可设置延迟 / 定时 / 周期任务,cron 表达式支持,独立 silent session 执行
25
+ - 🎴 **交互卡片体系**:CommandCard(按钮直接触发 slash 命令)+ ActionInteraction(按钮回写交互),Feishu 与 AUN 统一支持
23
26
  - 🤖 **健壮性保障**:任务超时提醒、会话异常安全模式修复、重启失败自动自愈
24
27
 
25
28
  ## 适合场景
@@ -45,7 +48,7 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
45
48
  4. **消息处理层** (`src/core/message/message-processor.ts`) - 统一事件处理引擎
46
49
  5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
47
50
  6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
48
- 7. **会话存储层** - JSONL 文件(CLI 共用)+ SQLite 元数据
51
+ 7. **会话存储层** - JSONL 文件(CLI 共用)+ 文件系统(每 chat 一个目录,含 active.json / meta_*.jsonl / messages.jsonl / health.jsonl)
49
52
 
50
53
  ### 消息流转
51
54
 
@@ -77,7 +80,7 @@ MessageProcessor.processMessage()
77
80
  ### 环境要求
78
81
 
79
82
  - **操作系统**:macOS / Linux / Windows
80
- - **Node.js** >= 22(需要 node:sqlite 内置模块支持)
83
+ - **Node.js** >= 18
81
84
  - **Claude Code** >= 2.1.32(`npm install -g @anthropic-ai/claude-code`)
82
85
 
83
86
  ### 1. 安装
@@ -168,6 +171,7 @@ evolclaw restart # 重启服务
168
171
  evolclaw status # 查看状态
169
172
  evolclaw logs # 查看日志(tail -f)
170
173
  evolclaw tui # 启动 AUN TUI 终端客户端
174
+ evolclaw agent # 管理 EvolAgent(list / show / new / reload)
171
175
  evolclaw mv <old> <new> # 项目搬家(保留全部会话)
172
176
  evolclaw diagnose # 诊断启动环境
173
177
 
@@ -187,17 +191,23 @@ evolclaw/
187
191
  │ │ ├── claude-runner.ts # Claude Agent SDK 封装
188
192
  │ │ ├── codex-runner.ts # Codex Agent 封装
189
193
  │ │ └── gemini-runner.ts # Gemini CLI 封装
194
+ │ ├── aun/ # AUN 协议工具
190
195
  │ ├── core/
191
196
  │ │ ├── message/
192
197
  │ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
193
198
  │ │ │ ├── message-processor.ts # 统一消息处理引擎
194
199
  │ │ │ ├── message-queue.ts # 消息队列(串行+中断)
195
200
  │ │ │ ├── message-cache.ts # 消息缓存
196
- │ │ │ └── stream-flusher.ts # 批量发送(3秒窗口)
201
+ │ │ │ ├── message-log.ts # 每 chat 的 messages.jsonl
202
+ │ │ │ └── im-renderer.ts # IM 渲染 + 批量发送
197
203
  │ │ ├── session/
198
204
  │ │ │ ├── adapters/ # 各后端会话文件适配器
205
+ │ │ │ ├── session-fs-store.ts # 文件系统存储原语
199
206
  │ │ │ └── session-manager.ts # 会话管理(多项目支持)
207
+ │ │ ├── trigger/ # 触发器引擎
200
208
  │ │ ├── command-handler.ts # 斜杠命令处理
209
+ │ │ ├── evolagent.ts # EvolAgent 实体
210
+ │ │ ├── evolagent-registry.ts # Agent 注册表(扫描/路由/热重载)
201
211
  │ │ ├── interaction-router.ts # 卡片交互回调路由
202
212
  │ │ └── permission.ts # 权限网关
203
213
  │ ├── channels/
@@ -207,17 +217,13 @@ evolclaw/
207
217
  │ │ ├── qqbot.ts # QQ 频道渠道
208
218
  │ │ ├── wecom.ts # 企业微信 AI Bot 渠道
209
219
  │ │ └── aun.ts # AUN Mesh 网络渠道
220
+ │ ├── cli/ # CLI 命令
210
221
  │ ├── utils/ # 工具函数
211
222
  │ ├── types.ts # 类型定义
212
- │ ├── config.ts # 配置加载
223
+ │ ├── config-store.ts # 配置加载
213
224
  │ ├── paths.ts # 路径解析
214
- │ ├── cli.ts # CLI 命令(init/start/stop/tui/mv/...)
215
225
  │ └── index.ts # 主入口
216
- ├── aun/
217
- │ ├── aun_cli.py # AUN TUI 客户端(Python)
218
- │ └── pyproject.toml # AUN CLI 依赖声明
219
- └── data/
220
- └── evolclaw.sample.json # 配置模板
226
+ └── kits/ # 共享上下文模板
221
227
  ```
222
228
 
223
229
  ## 斜杠命令
@@ -256,6 +262,9 @@ evolclaw/
256
262
  - `/stop` - 中断当前任务
257
263
  - `/check` - 系统健康检查(详情)
258
264
  - `/activity [all|dm|owner|none]` - 查看/控制中间输出显示模式
265
+ - `/chatmode [interactive|proactive]` - 查看/切换会话模式
266
+ - `/dispatch [mention|broadcast]` - 群聊分发模式(仅 @ 响应或广播)
267
+ - `/trigger <动作> ...` - 设置/查看 AI 自主触发器(延迟/定时/周期)
259
268
  - `/restart <channel>` - 重连指定渠道
260
269
 
261
270
  ### Owner 专属命令
@@ -270,7 +279,7 @@ evolclaw/
270
279
  - **运行时**:Node.js >= 22 + TypeScript(ES modules)
271
280
  - **AI SDK**:@anthropic-ai/claude-agent-sdk >= 0.2.75、@openai/codex-sdk、Gemini CLI
272
281
  - **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
273
- - **数据存储**:node:sqlite(内置模块)+ JSONL(CLI 共用)
282
+ - **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
274
283
  - **测试框架**:Vitest
275
284
 
276
285
  ## TODO
@@ -280,8 +289,8 @@ evolclaw/
280
289
  - [x] 项目搬家工具(`evolclaw mv`)
281
290
  - [x] 手动授权支持(文本回复 + 飞书卡片)
282
291
  - [x] 自动授权可配置(自动放行/自动拒绝)
292
+ - [x] 触发器支持
283
293
  - [ ] AUN 群组扩展功能支持
284
- - [ ] 触发器支持
285
294
  - [ ] 统计/状态监控 WebHook
286
295
 
287
296
 
package/bin/ec.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawnSync } from 'child_process';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import { createRequire } from 'module';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const here = path.dirname(fileURLToPath(import.meta.url));
10
+ const repoRoot = path.resolve(here, '..');
11
+ const srcEntry = path.join(repoRoot, 'src', 'cli', 'index.ts');
12
+ const distEntry = path.join(repoRoot, 'dist', 'cli', 'index.js');
13
+ const args = process.argv.slice(2);
14
+
15
+ if (fs.existsSync(srcEntry)) {
16
+ try {
17
+ const tsxImport = pathToFileURL(require.resolve('tsx')).href;
18
+ const result = spawnSync(process.execPath, ['--import', tsxImport, srcEntry, ...args], { stdio: 'inherit' });
19
+ process.exit(result.status ?? (result.error ? 1 : 0));
20
+ } catch {}
21
+ }
22
+
23
+ if (fs.existsSync(distEntry)) {
24
+ const result = spawnSync(process.execPath, [distEntry, ...args], { stdio: 'inherit' });
25
+ process.exit(result.status ?? (result.error ? 1 : 0));
26
+ }
27
+
28
+ console.error('ec: missing CLI entrypoint');
29
+ process.exit(1);
@@ -0,0 +1,19 @@
1
+ const BASEAGENT_ALIASES = {
2
+ claude: { canonical: 'claude', displayName: 'Claude Code' },
3
+ cc: { canonical: 'claude', displayName: 'Claude Code' },
4
+ 'claude-code': { canonical: 'claude', displayName: 'Claude Code' },
5
+ 'claude code': { canonical: 'claude', displayName: 'Claude Code' },
6
+ claudecode: { canonical: 'claude', displayName: 'Claude Code' },
7
+ codex: { canonical: 'codex', displayName: 'Codex' },
8
+ 'codex-cli': { canonical: 'codex', displayName: 'Codex' },
9
+ 'codex cli': { canonical: 'codex', displayName: 'Codex' },
10
+ gemini: { canonical: 'gemini', displayName: 'Gemini CLI' },
11
+ 'gemini-cli': { canonical: 'gemini', displayName: 'Gemini CLI' },
12
+ 'gemini cli': { canonical: 'gemini', displayName: 'Gemini CLI' },
13
+ geminicli: { canonical: 'gemini', displayName: 'Gemini CLI' },
14
+ hermes: { canonical: 'hermes', displayName: 'Hermes' },
15
+ };
16
+ export function normalizeBaseagent(input) {
17
+ const key = String(input || '').trim().toLowerCase().replace(/_/g, '-');
18
+ return BASEAGENT_ALIASES[key] || { canonical: 'unknown', displayName: input ? String(input) : 'Unknown' };
19
+ }
@@ -1,6 +1,9 @@
1
1
  import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages } from '@anthropic-ai/claude-agent-sdk';
2
- import { ensureDir, resolveAnthropicConfig } from '../config.js';
2
+ import { ensureDir } from '../utils/atomic-write.js';
3
+ import { resolveAnthropicConfig } from './resolve.js';
3
4
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
5
+ import { renderActionAsText } from '../core/interaction-router.js';
6
+ import { buildEnvelope, sendInteractionPayload } from '../core/message/message-processor.js';
4
7
  import path from 'path';
5
8
  import fs from 'fs';
6
9
  import os from 'os';
@@ -173,7 +176,7 @@ export class AgentRunner {
173
176
  if (!fs.existsSync(settingsPath))
174
177
  return;
175
178
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
176
- // evolclaw.json 显式配置优先,不被 settings.json 覆盖
179
+ // agent config 显式配置优先,不被 settings.json 覆盖
177
180
  const configModel = this.config?.agents?.claude?.model;
178
181
  if (!configModel && settings.model && settings.model !== this.model) {
179
182
  logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
@@ -203,14 +206,18 @@ export class AgentRunner {
203
206
  const questions = input.questions;
204
207
  // 没有交互上下文(无渠道适配器),回退到纯文本
205
208
  const permCtx = this.permissionContexts.get(sessionId);
206
- if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId) {
209
+ if (!permCtx?.adapter || !permCtx?.channelId) {
210
+ return this.handleAskUserQuestionFallback(sessionId, input, questions);
211
+ }
212
+ const adapterHasInteractionPath = !!permCtx.adapter.send;
213
+ if (!adapterHasInteractionPath) {
207
214
  return this.handleAskUserQuestionFallback(sessionId, input, questions);
208
215
  }
209
216
  const answers = {};
210
217
  // 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
211
218
  // 注意:sendPromptFn 是全局单例,多 channel 并发时会被覆盖,导致提示发到错误 channel
212
219
  const sendPrompt = permCtx.adapter && permCtx.channelId
213
- ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
220
+ ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
214
221
  : this.sendPromptFn;
215
222
  // 逐个 question 发送卡片并等待用户选择
216
223
  for (let i = 0; i < questions.length; i++) {
@@ -251,7 +258,17 @@ export class AgentRunner {
251
258
  };
252
259
  let cardSent = false;
253
260
  try {
254
- const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
261
+ const envelope = buildEnvelope({
262
+ taskId: permCtx.taskId,
263
+ channel: permCtx.channel ?? permCtx.adapter.channelName,
264
+ channelId: permCtx.channelId,
265
+ agentName: permCtx.agentName,
266
+ chatmode: permCtx.chatmode,
267
+ replyContext: permCtx.replyContext,
268
+ });
269
+ const optionLines = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
270
+ const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}`;
271
+ const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
255
272
  cardSent = !!result;
256
273
  }
257
274
  catch (err) {
@@ -313,16 +330,33 @@ export class AgentRunner {
313
330
  async handleAskUserQuestionFallback(sessionId, input, questions) {
314
331
  const permCtx = this.permissionContexts.get(sessionId);
315
332
  const sendPrompt = permCtx?.adapter && permCtx?.channelId
316
- ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
333
+ ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
317
334
  : this.sendPromptFn;
318
335
  const answers = {};
319
336
  if (questions?.length) {
320
337
  for (const q of questions) {
321
- const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
322
- const prompt = `💬 ${q.question}\n${optText}\n\n回复 /ask <数字> 选择,或 /ask <自定义内容>`;
323
338
  if (sendPrompt && permCtx?.interactionRouter) {
324
- await sendPrompt(prompt);
325
339
  const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
340
+ const interaction = {
341
+ type: 'interaction',
342
+ id: requestId,
343
+ channelId: permCtx.channelId || '',
344
+ sessionId,
345
+ initiatorId: permCtx.userId,
346
+ kind: {
347
+ kind: 'action',
348
+ title: `💬 ${q.question}`,
349
+ body: q.options.map((o, i) => `${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n'),
350
+ buttons: q.options.map((o, i) => ({ key: `opt-${i}`, label: o.label })),
351
+ },
352
+ fallback: {
353
+ command: 'ask',
354
+ buttonArgMap: Object.fromEntries(q.options.map((_, i) => [`opt-${i}`, String(i + 1)])),
355
+ acceptFreeText: true,
356
+ freeTextHint: '或回复 /ask <自定义内容>',
357
+ },
358
+ };
359
+ await sendPrompt(renderActionAsText(interaction));
326
360
  const answer = await new Promise((resolve) => {
327
361
  permCtx.interactionRouter.register(requestId, sessionId, (action) => {
328
362
  const num = parseInt(action.trim(), 10);
@@ -332,16 +366,16 @@ export class AgentRunner {
332
366
  else {
333
367
  resolve(action.trim());
334
368
  }
335
- }, { timeoutMs: 120_000, onTimeout: () => resolve(q.options[0]?.label || '') });
369
+ }, { timeoutMs: 120_000, onTimeout: () => resolve(q.options[0]?.label || ''), initiatorId: permCtx.userId, fallbackCommand: 'ask' });
336
370
  });
337
371
  answers[q.question] = answer;
338
372
  }
339
373
  else {
340
- // 无交互能力,自动选第一项
341
374
  const firstLabel = q.options[0]?.label || '';
342
375
  answers[q.question] = firstLabel;
343
376
  if (sendPrompt) {
344
- await sendPrompt(`${prompt}\n → 自动选择:${firstLabel}`);
377
+ const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
378
+ await sendPrompt(`💬 ${q.question}\n${optText}\n\n → 自动选择:${firstLabel}`);
345
379
  }
346
380
  }
347
381
  }
@@ -355,7 +389,7 @@ export class AgentRunner {
355
389
  async handleExitPlanMode(sessionId, input, options) {
356
390
  const permCtx = this.permissionContexts.get(sessionId);
357
391
  const sendPrompt = permCtx?.adapter && permCtx?.channelId
358
- ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
392
+ ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
359
393
  : this.sendPromptFn;
360
394
  // 无任何交互能力,直接 allow
361
395
  if (!permCtx?.channelId || !sendPrompt) {
@@ -363,7 +397,7 @@ export class AgentRunner {
363
397
  }
364
398
  // 尝试发送交互卡片
365
399
  let cardSent = false;
366
- if (permCtx.adapter?.sendInteraction) {
400
+ if (permCtx.adapter?.send) {
367
401
  const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
368
402
  const interaction = {
369
403
  type: 'interaction',
@@ -379,9 +413,23 @@ export class AgentRunner {
379
413
  },
380
414
  channelId: permCtx.channelId,
381
415
  sessionId,
416
+ initiatorId: permCtx.userId,
417
+ fallback: {
418
+ command: 'ask',
419
+ buttonArgMap: { approve: '1', reject: '2' },
420
+ },
382
421
  };
383
422
  try {
384
- const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
423
+ const envelope = buildEnvelope({
424
+ taskId: permCtx.taskId,
425
+ channel: permCtx.channel ?? permCtx.adapter.channelName,
426
+ channelId: permCtx.channelId,
427
+ agentName: permCtx.agentName,
428
+ chatmode: permCtx.chatmode,
429
+ replyContext: permCtx.replyContext,
430
+ });
431
+ const fallbackText = '📋 计划审批:AI 已完成规划,等待审批。\n回复 /ask 1 批准 / /ask 2 拒绝';
432
+ const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
385
433
  cardSent = !!result;
386
434
  }
387
435
  catch (err) {
@@ -390,22 +438,43 @@ export class AgentRunner {
390
438
  if (cardSent) {
391
439
  return new Promise((resolve) => {
392
440
  permCtx.interactionRouter?.register(requestId, sessionId, (action) => {
393
- if (action === 'approve') {
394
- resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
441
+ const trimmed = action.trim();
442
+ if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝' || trimmed === 'reject') {
443
+ resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
395
444
  }
396
445
  else {
397
- resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
446
+ resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
398
447
  }
399
- });
448
+ }, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
400
449
  });
401
450
  }
402
451
  }
403
452
  // 文本 fallback:注册到 interactionRouter,等待用户 /ask 回复
404
453
  if (permCtx.interactionRouter) {
405
- await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n\n 1. 批准执行\n 2. 拒绝\n\n回复 /ask 1 批准,/ask 2 拒绝:');
406
- const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
454
+ const fallbackRequestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
455
+ const fallbackInteraction = {
456
+ type: 'interaction',
457
+ id: fallbackRequestId,
458
+ channelId: permCtx.channelId || '',
459
+ sessionId,
460
+ initiatorId: permCtx.userId,
461
+ kind: {
462
+ kind: 'action',
463
+ title: '📋 计划审批',
464
+ body: 'AI 已完成规划,等待审批。',
465
+ buttons: [
466
+ { key: 'approve', label: '✅ 批准执行', style: 'primary' },
467
+ { key: 'reject', label: '❌ 拒绝', style: 'danger' },
468
+ ],
469
+ },
470
+ fallback: {
471
+ command: 'ask',
472
+ buttonArgMap: { approve: '1', reject: '2' },
473
+ },
474
+ };
475
+ await sendPrompt(renderActionAsText(fallbackInteraction));
407
476
  return new Promise((resolve) => {
408
- permCtx.interactionRouter.register(requestId, sessionId, (action) => {
477
+ permCtx.interactionRouter.register(fallbackRequestId, sessionId, (action) => {
409
478
  const trimmed = action.trim();
410
479
  if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
411
480
  resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
@@ -413,7 +482,7 @@ export class AgentRunner {
413
482
  else {
414
483
  resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
415
484
  }
416
- }, { timeoutMs: 300_000, onTimeout: () => resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' }) });
485
+ }, { timeoutMs: 300_000, onTimeout: () => resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' }), initiatorId: permCtx.userId, fallbackCommand: 'ask' });
417
486
  });
418
487
  }
419
488
  // 无交互能力,发提示后直接 allow
@@ -425,7 +494,6 @@ export class AgentRunner {
425
494
  * 所有 SDK 特有的事件类型引用封装在此方法内
426
495
  */
427
496
  async *transformStream(sdkStream, sessionId) {
428
- let hasTextDelta = false;
429
497
  let lastSessionId;
430
498
  // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
431
499
  const toolUseNames = new Map();
@@ -436,11 +504,6 @@ export class AgentRunner {
436
504
  this.updateSessionId(sessionId, event.session_id);
437
505
  yield { type: 'session_id', sessionId: event.session_id };
438
506
  }
439
- // text_delta → text
440
- if (event.type === 'text_delta' && event.text) {
441
- hasTextDelta = true;
442
- yield { type: 'text', text: event.text };
443
- }
444
507
  // system: compact_boundary → compact
445
508
  if (event.type === 'system' && event.subtype === 'compact_boundary') {
446
509
  yield { type: 'compact', preTokens: event.compact_metadata?.pre_tokens || 0 };
@@ -465,9 +528,9 @@ export class AgentRunner {
465
528
  // 记录 id → name 映射,供后续 tool_result 使用
466
529
  if (content.id)
467
530
  toolUseNames.set(content.id, content.name);
468
- yield { type: 'tool_use', name: content.name, input: content.input };
531
+ yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id };
469
532
  }
470
- else if (content.type === 'text' && content.text && !hasTextDelta) {
533
+ else if (content.type === 'text' && content.text) {
471
534
  yield { type: 'text', text: content.text };
472
535
  }
473
536
  }
@@ -487,6 +550,7 @@ export class AgentRunner {
487
550
  result: resultContent,
488
551
  isError: block.is_error === true,
489
552
  error: block.is_error === true ? resultContent : undefined,
553
+ callId: block.tool_use_id,
490
554
  };
491
555
  }
492
556
  }
@@ -519,7 +583,11 @@ export class AgentRunner {
519
583
  costUsd: event.total_cost_usd,
520
584
  terminalReason: event.terminal_reason,
521
585
  sessionTitle: event.session_title,
586
+ numTurns: event.num_turns,
587
+ usage: event.usage,
522
588
  };
589
+ // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
590
+ return;
523
591
  }
524
592
  }
525
593
  }
@@ -692,7 +760,7 @@ export class AgentRunner {
692
760
  const sdkPermissionMode = this.toSdkPermissionMode();
693
761
  logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
694
762
  if (systemPromptAppend) {
695
- logger.info(`[AgentRunner] systemPromptAppend (full):\n${systemPromptAppend}`);
763
+ logger.info(`[AgentRunner] systemPromptAppend: ${systemPromptAppend.length} chars`);
696
764
  }
697
765
  else {
698
766
  logger.info(`[AgentRunner] systemPromptAppend: none`);
@@ -988,21 +1056,15 @@ export class AgentRunner {
988
1056
  // Plugin implementation
989
1057
  export class ClaudeAgentPlugin {
990
1058
  name = 'claude';
991
- isEnabled(_globalConfig, agent) {
992
- // Only instantiate this baseagent for agents that declare it.
993
- return !!agent.config.agents?.claude;
994
- }
995
- createAgent(globalConfig, agent, callbacks) {
996
- // Per-agent override: read from agent.json's agents.claude block first.
997
- const override = agent.config.agents?.claude;
998
- const anthropic = resolveAnthropicConfig(globalConfig, override);
999
- // Merge per-agent claude block into config so runner reads useSettingSources etc.
1059
+ isEnabled(agent) {
1060
+ return agent.baseagent === 'claude';
1061
+ }
1062
+ createAgent(agent, callbacks) {
1063
+ const override = agent.config.baseagents?.claude;
1064
+ const syntheticConfig = { agents: { claude: override } };
1065
+ const anthropic = resolveAnthropicConfig(syntheticConfig, override);
1000
1066
  const merged = {
1001
- ...globalConfig,
1002
- agents: {
1003
- ...(globalConfig.agents || {}),
1004
- claude: { ...(globalConfig.agents?.claude || {}), ...(override || {}) },
1005
- },
1067
+ agents: { claude: { ...(override || {}) } },
1006
1068
  };
1007
1069
  const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
1008
1070
  if (anthropic.effort) {
@@ -5,7 +5,7 @@
5
5
  * Implements the same interface surface as AgentRunner (claude-runner.ts)
6
6
  * so MessageProcessor and CommandHandler can work with it transparently.
7
7
  */
8
- import { resolveOpenaiConfig } from '../config.js';
8
+ import { resolveOpenaiConfig } from './resolve.js';
9
9
  import { logger } from '../utils/logger.js';
10
10
  import fs from 'fs';
11
11
  import path from 'path';
@@ -41,7 +41,7 @@ export class CodexRunner {
41
41
  }
42
42
  async ensureCodex() {
43
43
  if (!this.codex || !this.codexModule) {
44
- const { requireOptional } = await import('../utils/init-channel.js');
44
+ const { requireOptional } = await import('../utils/npm-ops.js');
45
45
  this.codexModule = await requireOptional('@openai/codex-sdk');
46
46
  this.codex = new this.codexModule.Codex({
47
47
  apiKey: this.resolvedConfig.apiKey,
@@ -301,27 +301,26 @@ export class CodexRunner {
301
301
  // ── Plugin ──
302
302
  export class CodexAgentPlugin {
303
303
  name = 'codex';
304
- isEnabled(globalConfig, agent) {
305
- if (!agent.config.agents?.codex)
304
+ isEnabled(agent) {
305
+ if (agent.baseagent !== 'codex')
306
+ return false;
307
+ if (!agent.config.baseagents?.codex)
306
308
  return false;
307
309
  try {
308
- const override = agent.config.agents.codex;
309
- const resolved = resolveOpenaiConfig(globalConfig, override);
310
+ const override = agent.config.baseagents.codex;
311
+ const syntheticConfig = { agents: { codex: override } };
312
+ const resolved = resolveOpenaiConfig(syntheticConfig, override);
310
313
  return !!resolved.apiKey;
311
314
  }
312
315
  catch {
313
316
  return false;
314
317
  }
315
318
  }
316
- createAgent(globalConfig, agent, callbacks) {
317
- const override = agent.config.agents?.codex;
318
- // Synthesize a per-agent config view so CodexRunner sees its own credentials.
319
+ createAgent(agent, callbacks) {
320
+ const override = agent.config.baseagents?.codex;
321
+ const syntheticConfig = { agents: { codex: override } };
319
322
  const merged = {
320
- ...globalConfig,
321
- agents: {
322
- ...(globalConfig.agents || {}),
323
- codex: { ...(globalConfig.agents?.codex || {}), ...(override || {}) },
324
- },
323
+ agents: { codex: { ...(override || {}) } },
325
324
  };
326
325
  return { evolagentName: agent.name, baseagent: 'codex', agent: new CodexRunner(merged, callbacks) };
327
326
  }
@@ -13,7 +13,8 @@ import { createInterface } from 'readline';
13
13
  import fs from 'fs';
14
14
  import path from 'path';
15
15
  import os from 'os';
16
- import { resolveGoogleConfig } from '../config.js';
16
+ import { resolveGoogleConfig } from './resolve.js';
17
+ import { commandExists } from '../utils/cross-platform.js';
17
18
  import { GeminiSessionFileAdapter } from '../core/session/adapters/gemini-session-file-adapter.js';
18
19
  import { logger } from '../utils/logger.js';
19
20
  // Strip ANSI escape codes from Gemini CLI text output.
@@ -405,26 +406,23 @@ export class GeminiRunner {
405
406
  // ── Plugin ──
406
407
  export class GeminiAgentPlugin {
407
408
  name = 'gemini';
408
- isEnabled(globalConfig, agent) {
409
- if (!agent.config.agents?.gemini)
409
+ isEnabled(agent) {
410
+ if (agent.baseagent !== 'gemini')
410
411
  return false;
411
- try {
412
- const override = agent.config.agents.gemini;
413
- const resolved = resolveGoogleConfig(globalConfig, override);
414
- return !!resolved.cliPath;
415
- }
416
- catch {
412
+ const geminiCfg = agent.config.baseagents?.gemini;
413
+ if (!geminiCfg)
417
414
  return false;
418
- }
415
+ if (geminiCfg.cliPath)
416
+ return true;
417
+ if (geminiCfg.apiKey && !geminiCfg.apiKey.includes('your-') && !geminiCfg.apiKey.includes('placeholder'))
418
+ return true;
419
+ return commandExists('gemini');
419
420
  }
420
- createAgent(globalConfig, agent, callbacks) {
421
- const override = agent.config.agents?.gemini;
421
+ createAgent(agent, callbacks) {
422
+ const override = agent.config.baseagents?.gemini;
423
+ const syntheticConfig = { agents: { gemini: override } };
422
424
  const merged = {
423
- ...globalConfig,
424
- agents: {
425
- ...(globalConfig.agents || {}),
426
- gemini: { ...(globalConfig.agents?.gemini || {}), ...(override || {}) },
427
- },
425
+ agents: { gemini: { ...(override || {}) } },
428
426
  };
429
427
  return { evolagentName: agent.name, baseagent: 'gemini', agent: new GeminiRunner(merged, callbacks) };
430
428
  }