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.
- package/README.md +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- 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 共用)+
|
|
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** >=
|
|
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
|
-
│ │ │
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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?.
|
|
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
|
|
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
|
-
|
|
394
|
-
|
|
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: '
|
|
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
|
-
|
|
406
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
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
|
-
...
|
|
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 '
|
|
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/
|
|
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(
|
|
305
|
-
if (
|
|
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.
|
|
309
|
-
const
|
|
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(
|
|
317
|
-
const override = agent.config.
|
|
318
|
-
|
|
319
|
+
createAgent(agent, callbacks) {
|
|
320
|
+
const override = agent.config.baseagents?.codex;
|
|
321
|
+
const syntheticConfig = { agents: { codex: override } };
|
|
319
322
|
const merged = {
|
|
320
|
-
...
|
|
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 '
|
|
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(
|
|
409
|
-
if (
|
|
409
|
+
isEnabled(agent) {
|
|
410
|
+
if (agent.baseagent !== 'gemini')
|
|
410
411
|
return false;
|
|
411
|
-
|
|
412
|
-
|
|
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(
|
|
421
|
-
const override = agent.config.
|
|
421
|
+
createAgent(agent, callbacks) {
|
|
422
|
+
const override = agent.config.baseagents?.gemini;
|
|
423
|
+
const syntheticConfig = { agents: { gemini: override } };
|
|
422
424
|
const merged = {
|
|
423
|
-
...
|
|
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
|
}
|