evolclaw 2.2.0 → 2.3.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 +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
- package/dist/index.js +138 -54
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# EvolClaw
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> AI Agent 统一网关 —— 连接 IM、终端、Agent 网络
|
|
4
4
|
|
|
5
|
-
EvolClaw 是一个轻量级 AI Agent
|
|
5
|
+
EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex 等 AI Agent 提供统一接入层,支持飞书、微信、AUN Mesh 网络和终端 TUI 四种通道。人类可以通过手机 IM 随时接力开发,其他 Agent 也可以通过 AUN 网络直接调用你的 Agent —— 不只是人机交互,也是 Agent 间协作的基础设施。
|
|
6
6
|
|
|
7
7
|
## 核心特性
|
|
8
8
|
|
|
@@ -11,9 +11,11 @@ EvolClaw 是一个轻量级 AI Agent 网关,基于 Claude Agent SDK 构建。
|
|
|
11
11
|
- 🚀 **轻量化设计**:进程模式运行,CLI 命令行管理,无端口开放,无容器依赖,无 UI 界面
|
|
12
12
|
- 📁 **多项目支持**:每个项目独立会话,支持动态切换
|
|
13
13
|
- 👥 **双模式会话**:多用户私聊会话隔离,群聊会话共享,满足不同协作场景
|
|
14
|
-
- 🌐 **多渠道接入**:Channel Adapter 模式,飞书 +
|
|
14
|
+
- 🌐 **多渠道接入**:Channel Adapter 模式,飞书 + 微信 + AUN Mesh 网络,扫码一键接入
|
|
15
|
+
- 🤖 **Agent 间互联**:通过 AUN 网络,你的 Agent 可被其他 Agent 发现和调用
|
|
16
|
+
- 🖥️ **终端 TUI 客户端**:`evolclaw tui` 直接在终端与远程 Agent 对话,无需 IM
|
|
15
17
|
- 🔐 **分层权限**:用户级/管理员级命令分离,多用户安全隔离
|
|
16
|
-
-
|
|
18
|
+
- 📦 **项目搬家**:`evolclaw mv` 一键迁移项目目录,保留 Claude/Codex/EvolClaw 全部会话历史
|
|
17
19
|
- 💾 **会话持久化**:会话数据与 CLI 工具共享,不额外存储,服务重启不丢失
|
|
18
20
|
- ⚡ **执行中插入**:任务执行中可发送新消息,自动中断当前任务并处理新请求
|
|
19
21
|
- 🔕 **消息智能发送**:前台任务动态聚合批量发送,后台任务静默完成后通知
|
|
@@ -23,7 +25,8 @@ EvolClaw 是一个轻量级 AI Agent 网关,基于 Claude Agent SDK 构建。
|
|
|
23
25
|
|
|
24
26
|
- **通勤路上**:手机打开飞书,继续昨晚的代码 review,到公司无缝切回终端
|
|
25
27
|
- **会议间隙**:微信快速问一句「这个接口的返回格式是什么」,Agent 直接查代码回复
|
|
26
|
-
-
|
|
28
|
+
- **终端直连**:`evolclaw tui` 在任意终端直接与远程 Agent 对话,无需打开 IM
|
|
29
|
+
- **Agent 协作**:通过 AUN 网络,让你的 Agent 被其他 Agent 调用,组成分布式协作
|
|
27
30
|
- **外出离开工位**:不带电脑也能通过 IM 给 Agent 下达任务,回来看结果
|
|
28
31
|
- **团队协作**:拉个飞书群,成员共享同一个 Agent 会话,一起讨论和调试
|
|
29
32
|
|
|
@@ -36,11 +39,12 @@ EvolClaw 是一个轻量级 AI Agent 网关,基于 Claude Agent SDK 构建。
|
|
|
36
39
|
### 核心组件
|
|
37
40
|
|
|
38
41
|
1. **消息渠道层** (`src/channels/`) - Feishu WebSocket + WeChat HTTP 长轮询 + AUN Mesh 网络
|
|
39
|
-
2. **消息队列层** (`src/core/message-queue.ts`) - 会话级串行处理 + 中断支持
|
|
42
|
+
2. **消息队列层** (`src/core/message/message-queue.ts`) - 会话级串行处理 + 中断支持
|
|
40
43
|
3. **命令处理层** (`src/core/command-handler.ts`) - 斜杠命令处理(CommandHandler 类)
|
|
41
|
-
4. **消息处理层** (`src/core/message-processor.ts`) - 统一事件处理引擎
|
|
42
|
-
5. **会话管理层** (`src/core/session-manager.ts`) - 多项目会话管理
|
|
43
|
-
6.
|
|
44
|
+
4. **消息处理层** (`src/core/message/message-processor.ts`) - 统一事件处理引擎
|
|
45
|
+
5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
|
|
46
|
+
6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
|
|
47
|
+
7. **会话存储层** - JSONL 文件(CLI 共用)+ SQLite 元数据
|
|
44
48
|
|
|
45
49
|
### 消息流转
|
|
46
50
|
|
|
@@ -153,6 +157,9 @@ evolclaw stop # 停止服务
|
|
|
153
157
|
evolclaw restart # 重启服务
|
|
154
158
|
evolclaw status # 查看状态
|
|
155
159
|
evolclaw logs # 查看日志(tail -f)
|
|
160
|
+
evolclaw tui # 启动 AUN TUI 终端客户端
|
|
161
|
+
evolclaw mv <old> <new> # 项目搬家(保留全部会话)
|
|
162
|
+
evolclaw diagnose # 诊断启动环境
|
|
156
163
|
|
|
157
164
|
# 开发模式(热重载)
|
|
158
165
|
npm run dev
|
|
@@ -166,14 +173,23 @@ npm test
|
|
|
166
173
|
```
|
|
167
174
|
evolclaw/
|
|
168
175
|
├── src/
|
|
176
|
+
│ ├── agents/
|
|
177
|
+
│ │ ├── claude-runner.ts # Claude Agent SDK 封装
|
|
178
|
+
│ │ ├── codex-runner.ts # Codex Agent 封装
|
|
179
|
+
│ │ └── gemini-runner.ts # Gemini CLI 封装
|
|
169
180
|
│ ├── core/
|
|
170
|
-
│ │ ├──
|
|
171
|
-
│ │ ├──
|
|
172
|
-
│ │ ├── message-
|
|
173
|
-
│ │ ├── message-
|
|
174
|
-
│ │ ├──
|
|
175
|
-
│ │
|
|
176
|
-
│ │
|
|
181
|
+
│ │ ├── message/
|
|
182
|
+
│ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
|
|
183
|
+
│ │ │ ├── message-processor.ts # 统一消息处理引擎
|
|
184
|
+
│ │ │ ├── message-queue.ts # 消息队列(串行+中断)
|
|
185
|
+
│ │ │ ├── message-cache.ts # 消息缓存
|
|
186
|
+
│ │ │ └── stream-flusher.ts # 批量发送(3秒窗口)
|
|
187
|
+
│ │ ├── session/
|
|
188
|
+
│ │ │ ├── adapters/ # 各后端会话文件适配器
|
|
189
|
+
│ │ │ └── session-manager.ts # 会话管理(多项目支持)
|
|
190
|
+
│ │ ├── command-handler.ts # 斜杠命令处理
|
|
191
|
+
│ │ ├── interaction-router.ts # 卡片交互回调路由
|
|
192
|
+
│ │ └── permission.ts # 权限网关
|
|
177
193
|
│ ├── channels/
|
|
178
194
|
│ │ ├── feishu.ts # 飞书 WebSocket 渠道
|
|
179
195
|
│ │ ├── wechat.ts # 微信 ClawBot 渠道
|
|
@@ -182,8 +198,11 @@ evolclaw/
|
|
|
182
198
|
│ ├── types.ts # 类型定义
|
|
183
199
|
│ ├── config.ts # 配置加载
|
|
184
200
|
│ ├── paths.ts # 路径解析
|
|
185
|
-
│ ├── cli.ts # CLI 命令(init/start/stop/...)
|
|
201
|
+
│ ├── cli.ts # CLI 命令(init/start/stop/tui/mv/...)
|
|
186
202
|
│ └── index.ts # 主入口
|
|
203
|
+
├── aun/
|
|
204
|
+
│ ├── aun_cli.py # AUN TUI 客户端(Python)
|
|
205
|
+
│ └── pyproject.toml # AUN CLI 依赖声明
|
|
187
206
|
└── data/
|
|
188
207
|
└── evolclaw.sample.json # 配置模板
|
|
189
208
|
```
|
|
@@ -195,7 +214,8 @@ evolclaw/
|
|
|
195
214
|
**会话管理**:
|
|
196
215
|
- `/new [名称]` - 创建新会话
|
|
197
216
|
- `/slist` - 列出当前项目的所有会话
|
|
198
|
-
- `/
|
|
217
|
+
- `/slist cli` - 列出未导入的 CLI 会话
|
|
218
|
+
- `/s <名称|序号|uuid>` - 切换到指定会话
|
|
199
219
|
- `/name <新名称>` - 重命名当前会话
|
|
200
220
|
- `/del <名称>` - 删除指定会话(仅解绑,不删除文件)
|
|
201
221
|
- `/status` - 显示会话状态
|
|
@@ -209,6 +229,12 @@ evolclaw/
|
|
|
209
229
|
- `/p <name|path>` - 切换项目(保留会话历史)
|
|
210
230
|
- `/bind <path>` - 绑定新项目目录
|
|
211
231
|
|
|
232
|
+
**Agent 与模型**:
|
|
233
|
+
- `/agent [name]` - 查看或切换 Agent 后端(claude / codex / gemini)
|
|
234
|
+
- `/model [model]` - 查看或切换模型
|
|
235
|
+
- `/effort [level]` - 查看或切换推理强度(low / medium / high / max / auto)
|
|
236
|
+
- `/perm [mode]` - 查看或切换权限模式(auto / edit / default / readonly)
|
|
237
|
+
|
|
212
238
|
**系统管理**:
|
|
213
239
|
- `/clear` - 清空对话历史
|
|
214
240
|
- `/compact` - 压缩会话上下文
|
|
@@ -219,17 +245,10 @@ evolclaw/
|
|
|
219
245
|
- `/repair` - 检查并修复会话
|
|
220
246
|
- `/safe` - 进入安全模式
|
|
221
247
|
|
|
222
|
-
**模型管理**:
|
|
223
|
-
- `/model` - 显示当前模型和推理强度
|
|
224
|
-
- `/model <model>` - 切换模型
|
|
225
|
-
- `/model <effort>` - 切换推理强度(low / medium / high / max)
|
|
226
|
-
- `/model <model> <effort>` - 同时切换模型和推理强度
|
|
227
|
-
- `/model auto` - 恢复 SDK 默认推理强度
|
|
228
|
-
|
|
229
248
|
## 技术栈
|
|
230
249
|
|
|
231
250
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
232
|
-
- **AI SDK**:@anthropic-ai/claude-agent-sdk >= 0.2.75
|
|
251
|
+
- **AI SDK**:@anthropic-ai/claude-agent-sdk >= 0.2.75、@openai/codex-sdk、Gemini CLI
|
|
233
252
|
- **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、AUN Mesh 网络
|
|
234
253
|
- **数据存储**:node:sqlite(内置模块)+ JSONL(CLI 共用)
|
|
235
254
|
- **测试框架**:Vitest
|
|
@@ -239,8 +258,11 @@ evolclaw/
|
|
|
239
258
|
- [x] Windows 系统 CLI 命令支持
|
|
240
259
|
- [x] 微信插件支持图片/文件的收发
|
|
241
260
|
- [x] AUN Mesh 网络通道接入
|
|
261
|
+
- [x] TUI 终端客户端(`evolclaw tui`)
|
|
262
|
+
- [x] 项目搬家工具(`evolclaw mv`)
|
|
242
263
|
- [ ] 自动授权可配置(自动放行/自动拒绝)
|
|
243
|
-
- [
|
|
264
|
+
- [x] 手动授权支持(文本回复)
|
|
265
|
+
- [x] 手动授权支持(飞书卡片)
|
|
244
266
|
- [ ] ACP 协议支持(接入 Codex / Gemini CLI)
|
|
245
267
|
|
|
246
268
|
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"model": "gpt-5.2-codex",
|
|
10
10
|
"effort": "medium"
|
|
11
11
|
},
|
|
12
|
+
"google": {
|
|
13
|
+
"model": "gemini-2.5-flash"
|
|
14
|
+
},
|
|
12
15
|
"defaultAgent": "claude"
|
|
13
16
|
},
|
|
14
17
|
"channels": {
|
|
@@ -24,9 +27,9 @@
|
|
|
24
27
|
"token": ""
|
|
25
28
|
},
|
|
26
29
|
"aun": {
|
|
27
|
-
"enabled":
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
+
"enabled": false,
|
|
31
|
+
"aid": "your-agent.agentid.pub",
|
|
32
|
+
"gatewayPort": 443
|
|
30
33
|
}
|
|
31
34
|
},
|
|
32
35
|
"projects": {
|
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
|
-
import { checkBlacklist, summarizeToolInput } from '../
|
|
7
|
+
import { checkBlacklist, checkReadonly, summarizeToolInput } from '../core/permission.js';
|
|
8
8
|
import { encodePath } from '../utils/cross-platform.js';
|
|
9
9
|
class MessageStream {
|
|
10
10
|
queue = [];
|
|
@@ -72,7 +72,7 @@ export class AgentRunner {
|
|
|
72
72
|
apiKey;
|
|
73
73
|
model;
|
|
74
74
|
effort;
|
|
75
|
-
permissionMode = '
|
|
75
|
+
permissionMode = 'auto';
|
|
76
76
|
baseUrl;
|
|
77
77
|
config;
|
|
78
78
|
activeSessions = new Map();
|
|
@@ -82,6 +82,7 @@ export class AgentRunner {
|
|
|
82
82
|
onCompactStart;
|
|
83
83
|
permissionGateway;
|
|
84
84
|
sendPromptFn;
|
|
85
|
+
permissionContext;
|
|
85
86
|
constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
|
|
86
87
|
this.apiKey = apiKey;
|
|
87
88
|
this.model = model || 'sonnet';
|
|
@@ -123,11 +124,13 @@ export class AgentRunner {
|
|
|
123
124
|
}
|
|
124
125
|
listModes() {
|
|
125
126
|
return [
|
|
126
|
-
{ key: '
|
|
127
|
+
{ key: 'auto', nameZh: '自动', description: 'AI 分类器自动判断', available: true },
|
|
128
|
+
{ key: 'bypass', nameZh: '放行', description: '全部自动放行', available: true },
|
|
127
129
|
{ key: 'request', nameZh: '审批', description: '部分自动,部分询问', available: true },
|
|
128
130
|
{ key: 'edit', nameZh: '编辑', description: '自动接受编辑,其他询问', available: true },
|
|
129
131
|
{ key: 'plan', nameZh: '规划', description: '只规划不执行', available: true },
|
|
130
132
|
{ key: 'noask', nameZh: '静默', description: '未批准则拒绝', available: true },
|
|
133
|
+
{ key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
|
|
131
134
|
];
|
|
132
135
|
}
|
|
133
136
|
setPermissionGateway(gateway) {
|
|
@@ -136,15 +139,20 @@ export class AgentRunner {
|
|
|
136
139
|
setSendPrompt(fn) {
|
|
137
140
|
this.sendPromptFn = fn;
|
|
138
141
|
}
|
|
142
|
+
setPermissionContext(context) {
|
|
143
|
+
this.permissionContext = context;
|
|
144
|
+
}
|
|
139
145
|
toSdkPermissionMode() {
|
|
140
146
|
const map = {
|
|
141
|
-
'
|
|
147
|
+
'auto': 'auto', // AI 分类器自动判断
|
|
148
|
+
'bypass': 'default', // 全部自动放行(通过 canUseTool 一律 allow,保留 hook 安全检查)
|
|
142
149
|
'request': 'default', // 部分自动,部分询问
|
|
143
150
|
'edit': 'acceptEdits',
|
|
144
151
|
'plan': 'plan',
|
|
145
152
|
'noask': 'dontAsk',
|
|
153
|
+
'readonly': 'default',
|
|
146
154
|
};
|
|
147
|
-
return map[this.permissionMode] || '
|
|
155
|
+
return map[this.permissionMode] || 'auto';
|
|
148
156
|
}
|
|
149
157
|
// ── Compactable 接口 ──
|
|
150
158
|
async compact(sessionId, agentSessionId, projectPath) {
|
|
@@ -156,14 +164,19 @@ export class AgentRunner {
|
|
|
156
164
|
if (!fs.existsSync(settingsPath))
|
|
157
165
|
return;
|
|
158
166
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
159
|
-
|
|
167
|
+
// evolclaw.json 显式配置优先,不被 settings.json 覆盖
|
|
168
|
+
const configModel = this.config?.agents?.anthropic?.model;
|
|
169
|
+
if (!configModel && settings.model && settings.model !== this.model) {
|
|
160
170
|
logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
|
|
161
171
|
this.model = settings.model;
|
|
162
172
|
}
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
this.effort
|
|
173
|
+
const configEffort = this.config?.agents?.anthropic?.effort;
|
|
174
|
+
if (!configEffort) {
|
|
175
|
+
const newEffort = settings.effortLevel || undefined;
|
|
176
|
+
if (newEffort !== this.effort) {
|
|
177
|
+
logger.info(`[AgentRunner] Synced effort from ~/.claude/settings.json: ${newEffort ?? 'auto'}`);
|
|
178
|
+
this.effort = newEffort;
|
|
179
|
+
}
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
182
|
catch (error) {
|
|
@@ -207,6 +220,10 @@ export class AgentRunner {
|
|
|
207
220
|
durationMs: event.duration_ms,
|
|
208
221
|
};
|
|
209
222
|
}
|
|
223
|
+
// system: session_state_changed → state_changed
|
|
224
|
+
if (event.type === 'system' && event.subtype === 'session_state_changed') {
|
|
225
|
+
yield { type: 'state_changed', state: event.state };
|
|
226
|
+
}
|
|
210
227
|
// assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
|
|
211
228
|
if (event.type === 'assistant' && event.message?.content) {
|
|
212
229
|
for (const content of event.message.content) {
|
|
@@ -262,6 +279,8 @@ export class AgentRunner {
|
|
|
262
279
|
errors: event.errors,
|
|
263
280
|
durationMs: event.duration_ms,
|
|
264
281
|
costUsd: event.total_cost_usd,
|
|
282
|
+
terminalReason: event.terminal_reason,
|
|
283
|
+
sessionTitle: event.session_title,
|
|
265
284
|
};
|
|
266
285
|
}
|
|
267
286
|
}
|
|
@@ -329,38 +348,100 @@ export class AgentRunner {
|
|
|
329
348
|
}
|
|
330
349
|
return {};
|
|
331
350
|
};
|
|
332
|
-
// PreToolUse Hook -
|
|
351
|
+
// PreToolUse Hook - 黑名单检查 + input 修正(不可绕过,所有模式都走)
|
|
333
352
|
const preToolUseHook = async (input) => {
|
|
334
353
|
const result = await checkBlacklist(input.tool_name, input.tool_input || {});
|
|
335
354
|
if (result.behavior === 'deny') {
|
|
355
|
+
return { decision: 'block', reason: result.message };
|
|
356
|
+
}
|
|
357
|
+
if (this.permissionMode === 'readonly') {
|
|
358
|
+
const roResult = checkReadonly(input.tool_name, input.tool_input || {}, projectPath);
|
|
359
|
+
if (roResult.behavior === 'deny') {
|
|
360
|
+
return { decision: 'block', reason: roResult.message };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 修正 SDK schema 不兼容问题:部分工具被 system prompt 或 skills 指示传入
|
|
364
|
+
// SDK 未定义的参数(如 EnterPlanMode 的 reason),导致 InputValidationError
|
|
365
|
+
const toolInput = input.tool_input || {};
|
|
366
|
+
const sanitizeRules = {
|
|
367
|
+
'EnterPlanMode': ['reason'],
|
|
368
|
+
'ExitPlanMode': ['reason'],
|
|
369
|
+
'ExitWorktree': ['reason'],
|
|
370
|
+
};
|
|
371
|
+
const fieldsToRemove = sanitizeRules[input.tool_name];
|
|
372
|
+
if (fieldsToRemove && fieldsToRemove.some((f) => f in toolInput)) {
|
|
373
|
+
const cleaned = { ...toolInput };
|
|
374
|
+
for (const f of fieldsToRemove)
|
|
375
|
+
delete cleaned[f];
|
|
336
376
|
return {
|
|
337
|
-
|
|
338
|
-
|
|
377
|
+
hookSpecificOutput: {
|
|
378
|
+
hookEventName: 'PreToolUse',
|
|
379
|
+
permissionDecision: 'allow',
|
|
380
|
+
updatedInput: cleaned
|
|
381
|
+
}
|
|
339
382
|
};
|
|
340
383
|
}
|
|
341
384
|
return {};
|
|
342
385
|
};
|
|
386
|
+
// PermissionDenied Hook - auto 模式下 SDK 拒绝操作时通知用户
|
|
387
|
+
const permissionDeniedHook = async (input) => {
|
|
388
|
+
if (this.permissionMode === 'auto' && this.sendPromptFn) {
|
|
389
|
+
const toolName = input.tool_name || '未知工具';
|
|
390
|
+
const reason = input.reason || 'AI 判断此操作有风险';
|
|
391
|
+
const message = `⚠️ 操作已自动拦截\n工具: ${toolName}\n原因: ${reason}`;
|
|
392
|
+
try {
|
|
393
|
+
await this.sendPromptFn(message);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
logger.error('[PermissionDenied] Failed to send notification:', err);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {};
|
|
400
|
+
};
|
|
343
401
|
// SDK-level canUseTool 回调:接入 PermissionGateway 的用户审批入口
|
|
344
402
|
// 只在 SDK 认为此工具需要用户确认时触发(黑名单已在 PreToolUse hook 拦截)
|
|
345
403
|
const canUseToolCallback = async (toolName, input, options) => {
|
|
346
|
-
//
|
|
347
|
-
if (this.permissionMode === '
|
|
348
|
-
return { behavior: 'allow', updatedInput: input };
|
|
404
|
+
// bypass 模式:一律 allow
|
|
405
|
+
if (this.permissionMode === 'bypass') {
|
|
406
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
|
407
|
+
}
|
|
408
|
+
// readonly 模式:二次拦截(belt-and-suspenders)
|
|
409
|
+
if (this.permissionMode === 'readonly') {
|
|
410
|
+
const roResult = checkReadonly(toolName, input, projectPath);
|
|
411
|
+
if (roResult.behavior === 'deny') {
|
|
412
|
+
return { behavior: 'deny', message: roResult.message, decisionClassification: 'user_reject' };
|
|
413
|
+
}
|
|
414
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
|
415
|
+
}
|
|
416
|
+
// auto 模式:SDK 内置分类器自动判断,正常情况下不会触发 canUseTool 回调。
|
|
417
|
+
// 防御性兜底:确保即使 SDK 边界场景或版本变化意外调用了此回调,也不会阻塞流程。
|
|
418
|
+
if (this.permissionMode === 'auto') {
|
|
419
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
|
349
420
|
}
|
|
350
421
|
// 如果 PermissionGateway 未设置(如测试环境),回退到一律 allow
|
|
351
422
|
if (!this.permissionGateway || !this.sendPromptFn) {
|
|
352
|
-
return { behavior: 'allow', updatedInput: input };
|
|
423
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
|
424
|
+
}
|
|
425
|
+
// always-allow 缓存命中:直接放行
|
|
426
|
+
if (this.permissionGateway.isAlwaysAllowed(toolName)) {
|
|
427
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
|
353
428
|
}
|
|
354
429
|
const summary = options.title
|
|
355
430
|
|| options.description
|
|
356
431
|
|| summarizeToolInput(toolName, input);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
432
|
+
const decision = await this.permissionGateway.requestPermission(sessionId, toolName, input, this.sendPromptFn, this.permissionContext, summary, options.decisionReason);
|
|
433
|
+
if (decision === 'deny') {
|
|
434
|
+
return { behavior: 'deny', message: '用户拒绝或审批超时', decisionClassification: 'user_reject' };
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
behavior: 'allow',
|
|
438
|
+
updatedInput: input,
|
|
439
|
+
decisionClassification: decision === 'always' ? 'user_permanent' : 'user_temporary'
|
|
440
|
+
};
|
|
361
441
|
};
|
|
362
442
|
const useSettingSources = this.config?.agents?.anthropic?.useSettingSources !== false;
|
|
363
443
|
const enableSummaries = this.config?.agents?.anthropic?.agentProgressSummaries !== false;
|
|
444
|
+
const excludeDynamic = this.config?.agents?.anthropic?.excludeDynamicSections === true;
|
|
364
445
|
// 公共 options(新旧模式共用)
|
|
365
446
|
const sdkPermissionMode = this.toSdkPermissionMode();
|
|
366
447
|
logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
|
|
@@ -368,12 +449,15 @@ export class AgentRunner {
|
|
|
368
449
|
cwd: projectPath,
|
|
369
450
|
model: this.model,
|
|
370
451
|
...(this.effort ? { effort: this.effort } : {}),
|
|
452
|
+
autoCompactWindow: 200000,
|
|
453
|
+
advisorModel: 'haiku',
|
|
371
454
|
canUseTool: canUseToolCallback,
|
|
372
455
|
permissionMode: sdkPermissionMode,
|
|
373
456
|
persistSession: true,
|
|
374
457
|
hooks: {
|
|
375
458
|
PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
|
|
376
|
-
PreToolUse: [{ matcher: '.*', hooks: [preToolUseHook] }]
|
|
459
|
+
PreToolUse: [{ matcher: '.*', hooks: [preToolUseHook] }],
|
|
460
|
+
PermissionDenied: [{ matcher: '.*', hooks: [permissionDeniedHook] }]
|
|
377
461
|
},
|
|
378
462
|
...(enableSummaries ? { agentProgressSummaries: true } : {}),
|
|
379
463
|
stderr: (msg) => {
|
|
@@ -397,6 +481,7 @@ export class AgentRunner {
|
|
|
397
481
|
systemPrompt: {
|
|
398
482
|
type: 'preset',
|
|
399
483
|
preset: 'claude_code',
|
|
484
|
+
...(excludeDynamic ? { excludeDynamicSections: true } : {}),
|
|
400
485
|
...(systemPromptAppend ? { append: systemPromptAppend } : {})
|
|
401
486
|
},
|
|
402
487
|
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
@@ -455,37 +540,25 @@ export class AgentRunner {
|
|
|
455
540
|
});
|
|
456
541
|
}
|
|
457
542
|
};
|
|
458
|
-
let
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// 保存 interrupt 能力(不写 activeStreams,由 registerStream 管理活跃状态)
|
|
475
|
-
if ('interrupt' in sdkStream && typeof sdkStream.interrupt === 'function') {
|
|
476
|
-
this.interruptFns.set(sessionId, () => sdkStream.interrupt());
|
|
477
|
-
}
|
|
478
|
-
// 返回标准 AgentEvent 流
|
|
479
|
-
return this.transformStream(sdkStream, sessionId);
|
|
480
|
-
}
|
|
481
|
-
catch (error) {
|
|
482
|
-
lastError = error;
|
|
483
|
-
if (attempt < 2) {
|
|
484
|
-
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000));
|
|
485
|
-
}
|
|
486
|
-
}
|
|
543
|
+
let sdkStream;
|
|
544
|
+
if (images && images.length > 0) {
|
|
545
|
+
logger.debug('[AgentRunner] Creating query with images, images:', images.length);
|
|
546
|
+
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
547
|
+
const stream = new MessageStream();
|
|
548
|
+
stream.push(prompt, images);
|
|
549
|
+
stream.end();
|
|
550
|
+
sdkStream = createQuery(stream);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
|
|
554
|
+
sdkStream = createQuery(prompt, agentSessionId);
|
|
555
|
+
}
|
|
556
|
+
// 保存 interrupt 能力(不写 activeStreams,由 registerStream 管理活跃状态)
|
|
557
|
+
if ('interrupt' in sdkStream && typeof sdkStream.interrupt === 'function') {
|
|
558
|
+
this.interruptFns.set(sessionId, () => sdkStream.interrupt());
|
|
487
559
|
}
|
|
488
|
-
|
|
560
|
+
// 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
|
|
561
|
+
return this.transformStream(sdkStream, sessionId);
|
|
489
562
|
}
|
|
490
563
|
async interrupt(sessionId) {
|
|
491
564
|
const fn = this.interruptFns.get(sessionId);
|
|
@@ -38,8 +38,8 @@ export class CodexRunner {
|
|
|
38
38
|
baseUrl: resolved.baseUrl,
|
|
39
39
|
});
|
|
40
40
|
this.model = resolved.model;
|
|
41
|
-
if (resolved.
|
|
42
|
-
this.effort = resolved.
|
|
41
|
+
if (resolved.effort)
|
|
42
|
+
this.effort = resolved.effort;
|
|
43
43
|
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
44
44
|
}
|
|
45
45
|
// ── ModelSwitcher ──
|
|
@@ -50,11 +50,12 @@ export class CodexRunner {
|
|
|
50
50
|
setEffort(effort) { this.effort = effort; }
|
|
51
51
|
getEffort() { return this.effort; }
|
|
52
52
|
// ── Permission ──
|
|
53
|
-
currentMode = '
|
|
53
|
+
currentMode = 'auto';
|
|
54
54
|
approvalPolicy = 'never';
|
|
55
55
|
setMode(mode) {
|
|
56
56
|
const map = {
|
|
57
|
-
'
|
|
57
|
+
'auto': 'never',
|
|
58
|
+
'bypass': 'never',
|
|
58
59
|
'request': 'on-request',
|
|
59
60
|
'noask': 'untrusted',
|
|
60
61
|
};
|
|
@@ -64,9 +65,11 @@ export class CodexRunner {
|
|
|
64
65
|
getMode() { return this.currentMode; }
|
|
65
66
|
listModes() {
|
|
66
67
|
return [
|
|
67
|
-
{ key: '
|
|
68
|
+
{ key: 'auto', nameZh: '自动', description: '全部自动(受 sandbox 约束)', available: true },
|
|
69
|
+
{ key: 'bypass', nameZh: '放行', description: '全部自动(受 sandbox 约束)', available: true },
|
|
68
70
|
{ key: 'request', nameZh: '审批', description: '需要审批时询问', available: true },
|
|
69
71
|
{ key: 'noask', nameZh: '静默', description: '只执行已知安全操作', available: true },
|
|
72
|
+
{ key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
|
|
70
73
|
];
|
|
71
74
|
}
|
|
72
75
|
setSendPrompt(_fn) { }
|
|
@@ -193,6 +196,8 @@ export class CodexRunner {
|
|
|
193
196
|
async *transformStream(events, sessionId, thread, tempFiles) {
|
|
194
197
|
try {
|
|
195
198
|
for await (const event of events) {
|
|
199
|
+
if (!this.activeAbortControllers.has(sessionId))
|
|
200
|
+
break;
|
|
196
201
|
yield* this.mapEvent(event, sessionId, thread);
|
|
197
202
|
}
|
|
198
203
|
}
|