evolclaw 2.8.2 → 3.0.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/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -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 +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- 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 +704 -775
- 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/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- 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/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/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|all]` - 群聊分发模式(仅 @ 响应或广播)
|
|
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
|
|
|
@@ -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
|
|
@@ -465,7 +534,7 @@ export class AgentRunner {
|
|
|
465
534
|
// 记录 id → name 映射,供后续 tool_result 使用
|
|
466
535
|
if (content.id)
|
|
467
536
|
toolUseNames.set(content.id, content.name);
|
|
468
|
-
yield { type: 'tool_use', name: content.name, input: content.input };
|
|
537
|
+
yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id };
|
|
469
538
|
}
|
|
470
539
|
else if (content.type === 'text' && content.text && !hasTextDelta) {
|
|
471
540
|
yield { type: 'text', text: content.text };
|
|
@@ -487,6 +556,7 @@ export class AgentRunner {
|
|
|
487
556
|
result: resultContent,
|
|
488
557
|
isError: block.is_error === true,
|
|
489
558
|
error: block.is_error === true ? resultContent : undefined,
|
|
559
|
+
callId: block.tool_use_id,
|
|
490
560
|
};
|
|
491
561
|
}
|
|
492
562
|
}
|
|
@@ -988,15 +1058,20 @@ export class AgentRunner {
|
|
|
988
1058
|
// Plugin implementation
|
|
989
1059
|
export class ClaudeAgentPlugin {
|
|
990
1060
|
name = 'claude';
|
|
991
|
-
isEnabled(
|
|
992
|
-
return
|
|
1061
|
+
isEnabled(agent) {
|
|
1062
|
+
return !!agent.config.baseagents?.claude;
|
|
993
1063
|
}
|
|
994
|
-
createAgent(
|
|
995
|
-
const
|
|
996
|
-
const
|
|
1064
|
+
createAgent(agent, callbacks) {
|
|
1065
|
+
const override = agent.config.baseagents?.claude;
|
|
1066
|
+
const syntheticConfig = { agents: { claude: override } };
|
|
1067
|
+
const anthropic = resolveAnthropicConfig(syntheticConfig, override);
|
|
1068
|
+
const merged = {
|
|
1069
|
+
agents: { claude: { ...(override || {}) } },
|
|
1070
|
+
};
|
|
1071
|
+
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
|
|
997
1072
|
if (anthropic.effort) {
|
|
998
1073
|
agentRunner.setEffort(anthropic.effort);
|
|
999
1074
|
}
|
|
1000
|
-
return { agent: agentRunner };
|
|
1075
|
+
return { evolagentName: agent.name, baseagent: 'claude', agent: agentRunner };
|
|
1001
1076
|
}
|
|
1002
1077
|
}
|
|
@@ -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,17 +301,25 @@ export class CodexRunner {
|
|
|
301
301
|
// ── Plugin ──
|
|
302
302
|
export class CodexAgentPlugin {
|
|
303
303
|
name = 'codex';
|
|
304
|
-
isEnabled(
|
|
304
|
+
isEnabled(agent) {
|
|
305
|
+
if (!agent.config.baseagents?.codex)
|
|
306
|
+
return false;
|
|
305
307
|
try {
|
|
306
|
-
const
|
|
308
|
+
const override = agent.config.baseagents.codex;
|
|
309
|
+
const syntheticConfig = { agents: { codex: override } };
|
|
310
|
+
const resolved = resolveOpenaiConfig(syntheticConfig, override);
|
|
307
311
|
return !!resolved.apiKey;
|
|
308
312
|
}
|
|
309
313
|
catch {
|
|
310
314
|
return false;
|
|
311
315
|
}
|
|
312
316
|
}
|
|
313
|
-
createAgent(
|
|
314
|
-
const
|
|
315
|
-
|
|
317
|
+
createAgent(agent, callbacks) {
|
|
318
|
+
const override = agent.config.baseagents?.codex;
|
|
319
|
+
const syntheticConfig = { agents: { codex: override } };
|
|
320
|
+
const merged = {
|
|
321
|
+
agents: { codex: { ...(override || {}) } },
|
|
322
|
+
};
|
|
323
|
+
return { evolagentName: agent.name, baseagent: 'codex', agent: new CodexRunner(merged, callbacks) };
|
|
316
324
|
}
|
|
317
325
|
}
|
|
@@ -13,7 +13,7 @@ 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
17
|
import { GeminiSessionFileAdapter } from '../core/session/adapters/gemini-session-file-adapter.js';
|
|
18
18
|
import { logger } from '../utils/logger.js';
|
|
19
19
|
// Strip ANSI escape codes from Gemini CLI text output.
|
|
@@ -405,16 +405,25 @@ export class GeminiRunner {
|
|
|
405
405
|
// ── Plugin ──
|
|
406
406
|
export class GeminiAgentPlugin {
|
|
407
407
|
name = 'gemini';
|
|
408
|
-
isEnabled(
|
|
408
|
+
isEnabled(agent) {
|
|
409
|
+
if (!agent.config.baseagents?.gemini)
|
|
410
|
+
return false;
|
|
409
411
|
try {
|
|
410
|
-
const
|
|
412
|
+
const override = agent.config.baseagents.gemini;
|
|
413
|
+
const syntheticConfig = { agents: { gemini: override } };
|
|
414
|
+
const resolved = resolveGoogleConfig(syntheticConfig, override);
|
|
411
415
|
return !!resolved.cliPath;
|
|
412
416
|
}
|
|
413
417
|
catch {
|
|
414
418
|
return false;
|
|
415
419
|
}
|
|
416
420
|
}
|
|
417
|
-
createAgent(
|
|
418
|
-
|
|
421
|
+
createAgent(agent, callbacks) {
|
|
422
|
+
const override = agent.config.baseagents?.gemini;
|
|
423
|
+
const syntheticConfig = { agents: { gemini: override } };
|
|
424
|
+
const merged = {
|
|
425
|
+
agents: { gemini: { ...(override || {}) } },
|
|
426
|
+
};
|
|
427
|
+
return { evolagentName: agent.name, baseagent: 'gemini', agent: new GeminiRunner(merged, callbacks) };
|
|
419
428
|
}
|
|
420
429
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseagent credential resolvers.
|
|
3
|
+
*
|
|
4
|
+
* 输入是 Config 形态(`config.agents.<baseagent>` + override)。启动期由 index.ts
|
|
5
|
+
* 从 primaryAgent.config.baseagents 构造一个 syntheticConfig 喂入;各 plugin 的
|
|
6
|
+
* createAgent 也各自构造 syntheticConfig。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { commandExists } from '../utils/cross-platform.js';
|
|
12
|
+
function loadClaudeSettings() {
|
|
13
|
+
try {
|
|
14
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
15
|
+
if (fs.existsSync(settingsPath)) {
|
|
16
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
export function resolveAnthropicConfig(config, override) {
|
|
23
|
+
const settings = loadClaudeSettings();
|
|
24
|
+
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
25
|
+
const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
|
|
26
|
+
const globalApiKey = isPlaceholder(config.agents?.claude?.apiKey) ? undefined : config.agents?.claude?.apiKey;
|
|
27
|
+
const apiKey = overrideApiKey
|
|
28
|
+
|| globalApiKey
|
|
29
|
+
|| process.env.ANTHROPIC_AUTH_TOKEN
|
|
30
|
+
|| settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new Error('No API key found. Set one of: baseagents.claude.apiKey (per-agent or defaults), env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
|
|
33
|
+
}
|
|
34
|
+
const isPlaceholderUrl = (v) => !v || v.includes('api.anthropic.com');
|
|
35
|
+
const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
|
|
36
|
+
const globalBaseUrl = isPlaceholderUrl(config.agents?.claude?.baseUrl) ? undefined : config.agents?.claude?.baseUrl;
|
|
37
|
+
const baseUrl = overrideBaseUrl
|
|
38
|
+
|| globalBaseUrl
|
|
39
|
+
|| process.env.ANTHROPIC_BASE_URL
|
|
40
|
+
|| settings.env?.ANTHROPIC_BASE_URL;
|
|
41
|
+
const model = override?.model
|
|
42
|
+
|| config.agents?.claude?.model
|
|
43
|
+
|| settings.model
|
|
44
|
+
|| 'sonnet';
|
|
45
|
+
const effort = override?.effort
|
|
46
|
+
|| config.agents?.claude?.effort
|
|
47
|
+
|| settings.effortLevel
|
|
48
|
+
|| undefined;
|
|
49
|
+
const pickExec = (v) => (!v || v.includes('your-') || v.includes('placeholder')) ? undefined : v;
|
|
50
|
+
const pathToClaudeCodeExecutable = pickExec(override?.pathToClaudeCodeExecutable)
|
|
51
|
+
|| pickExec(config.agents?.claude?.pathToClaudeCodeExecutable);
|
|
52
|
+
return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
|
|
53
|
+
}
|
|
54
|
+
function loadCodexSettings() {
|
|
55
|
+
try {
|
|
56
|
+
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
57
|
+
let apiKey;
|
|
58
|
+
if (fs.existsSync(authPath)) {
|
|
59
|
+
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
60
|
+
apiKey = auth.OPENAI_API_KEY;
|
|
61
|
+
}
|
|
62
|
+
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
63
|
+
let model;
|
|
64
|
+
let baseUrl;
|
|
65
|
+
if (fs.existsSync(configPath)) {
|
|
66
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
67
|
+
const modelMatch = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
68
|
+
if (modelMatch)
|
|
69
|
+
model = modelMatch[1];
|
|
70
|
+
const providerMatch = content.match(/^model_provider\s*=\s*"([^"]+)"/m);
|
|
71
|
+
if (providerMatch) {
|
|
72
|
+
const provider = providerMatch[1];
|
|
73
|
+
const baseUrlMatch = content.match(new RegExp(`\\[model_providers\\.${provider}\\][\\s\\S]*?base_url\\s*=\\s*"([^"]+)"`, 'm'));
|
|
74
|
+
if (baseUrlMatch)
|
|
75
|
+
baseUrl = baseUrlMatch[1];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { apiKey, baseUrl, model };
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
export function resolveOpenaiConfig(config, override) {
|
|
84
|
+
const codexSettings = loadCodexSettings();
|
|
85
|
+
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
86
|
+
const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
|
|
87
|
+
const globalApiKey = isPlaceholder(config.agents?.codex?.apiKey) ? undefined : config.agents?.codex?.apiKey;
|
|
88
|
+
const apiKey = overrideApiKey
|
|
89
|
+
|| globalApiKey
|
|
90
|
+
|| process.env.OPENAI_API_KEY
|
|
91
|
+
|| codexSettings.apiKey;
|
|
92
|
+
if (!apiKey) {
|
|
93
|
+
throw new Error('No OpenAI API key found. Set one of: baseagents.codex.apiKey (per-agent or defaults), env OPENAI_API_KEY, or ~/.codex/auth.json');
|
|
94
|
+
}
|
|
95
|
+
const isPlaceholderUrl = (v) => !v || v.includes('api.openai.com');
|
|
96
|
+
const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
|
|
97
|
+
const globalBaseUrl = isPlaceholderUrl(config.agents?.codex?.baseUrl) ? undefined : config.agents?.codex?.baseUrl;
|
|
98
|
+
const baseUrl = overrideBaseUrl
|
|
99
|
+
|| globalBaseUrl
|
|
100
|
+
|| process.env.OPENAI_BASE_URL
|
|
101
|
+
|| codexSettings.baseUrl
|
|
102
|
+
|| undefined;
|
|
103
|
+
const model = override?.model
|
|
104
|
+
|| config.agents?.codex?.model
|
|
105
|
+
|| codexSettings.model
|
|
106
|
+
|| 'gpt-5.2-codex';
|
|
107
|
+
const effort = override?.effort
|
|
108
|
+
|| override?.reasoning
|
|
109
|
+
|| config.agents?.codex?.effort
|
|
110
|
+
|| config.agents?.codex?.reasoning
|
|
111
|
+
|| undefined;
|
|
112
|
+
return { apiKey, baseUrl, model, effort };
|
|
113
|
+
}
|
|
114
|
+
export function resolveGoogleConfig(config, override) {
|
|
115
|
+
const googleCfg = config.agents?.gemini;
|
|
116
|
+
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
117
|
+
let cliPath = override?.cliPath || googleCfg?.cliPath || '';
|
|
118
|
+
if (!cliPath) {
|
|
119
|
+
cliPath = commandExists('gemini') ? 'gemini' : '';
|
|
120
|
+
}
|
|
121
|
+
const model = override?.model || googleCfg?.model || 'gemini-2.5-flash';
|
|
122
|
+
const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
|
|
123
|
+
const globalApiKey = isPlaceholder(googleCfg?.apiKey) ? undefined : googleCfg?.apiKey;
|
|
124
|
+
const apiKey = overrideApiKey
|
|
125
|
+
|| globalApiKey
|
|
126
|
+
|| process.env.GEMINI_API_KEY
|
|
127
|
+
|| process.env.GOOGLE_API_KEY
|
|
128
|
+
|| undefined;
|
|
129
|
+
const mode = override?.mode || googleCfg?.mode || 'cli';
|
|
130
|
+
const useVertex = override?.useVertex ?? googleCfg?.useVertex ?? false;
|
|
131
|
+
const project = override?.project || googleCfg?.project || process.env.GOOGLE_CLOUD_PROJECT || undefined;
|
|
132
|
+
const location = override?.location || googleCfg?.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
|
|
133
|
+
return { cliPath, model, apiKey, mode, useVertex, project, location };
|
|
134
|
+
}
|
package/dist/agents/templates.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getPackageRoot, resolveRoot } from '../paths.js';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
|
-
const KNOWN_SECTIONS = new Set(['runtime', 'group', 'proactive']);
|
|
5
|
+
const KNOWN_SECTIONS = new Set(['runtime', 'group', 'proactive', 'trigger']);
|
|
6
6
|
const SECTION_RE = /^##\s+(\w+)\s*$/;
|
|
7
7
|
let sections = null;
|
|
8
8
|
let builtinSections = null;
|
|
@@ -43,8 +43,8 @@ function parseTemplate(content) {
|
|
|
43
43
|
return result;
|
|
44
44
|
}
|
|
45
45
|
function loadBuiltinTemplate() {
|
|
46
|
-
const builtinPath = path.join(getPackageRoot(), 'dist', '
|
|
47
|
-
const srcPath = path.join(getPackageRoot(), 'src', '
|
|
46
|
+
const builtinPath = path.join(getPackageRoot(), 'dist', 'data', 'prompts.md');
|
|
47
|
+
const srcPath = path.join(getPackageRoot(), 'src', 'data', 'prompts.md');
|
|
48
48
|
const filePath = fs.existsSync(builtinPath) ? builtinPath : srcPath;
|
|
49
49
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
50
50
|
return parseTemplate(content);
|