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.
Files changed (43) hide show
  1. package/README.md +49 -27
  2. package/data/evolclaw.sample.json +6 -3
  3. package/dist/agents/claude-runner.js +125 -52
  4. package/dist/agents/codex-runner.js +10 -5
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +247 -84
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +132 -50
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +750 -209
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +216 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
  16. package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
  17. package/dist/{utils → core/message}/stream-debouncer.js +1 -1
  18. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  19. package/dist/core/permission.js +212 -11
  20. package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
  21. package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
  22. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  23. package/dist/{utils → core/session}/session-file-health.js +1 -1
  24. package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
  25. package/dist/index.js +138 -54
  26. package/dist/{core/ipc-server.js → ipc.js} +36 -1
  27. package/dist/types.js +3 -0
  28. package/dist/utils/cross-platform.js +38 -1
  29. package/dist/utils/error-utils.js +130 -5
  30. package/dist/utils/init-channel.js +649 -0
  31. package/dist/utils/init.js +55 -150
  32. package/dist/utils/logger.js +8 -3
  33. package/dist/utils/media-cache.js +207 -0
  34. package/dist/{core → utils}/stats-collector.js +16 -0
  35. package/package.json +3 -3
  36. package/dist/core/message-bridge.js +0 -187
  37. package/dist/utils/init-feishu.js +0 -263
  38. package/dist/utils/init-wechat.js +0 -172
  39. package/dist/utils/ipc-client.js +0 -36
  40. package/dist/utils/permission-utils.js +0 -71
  41. /package/dist/{utils → core/message}/message-cache.js +0 -0
  42. /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
  43. /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
- > Claude Code 装进飞书和微信 —— 随时随地,接力开发
3
+ > AI Agent 统一网关 —— 连接 IM、终端、Agent 网络
4
4
 
5
- EvolClaw 是一个轻量级 AI Agent 网关,基于 Claude Agent SDK 构建。它将终端中的 Claude Code 能力延伸到飞书、微信等即时通讯工具,让你在手机上也能review 代码、调试问题、管理项目,真正实现多终端无缝接力开发。
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
- - 📊 **统一消息处理**:消息处理与渠道解耦,新增渠道仅需 ~15 行代码
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
- - **下班之后**:躺在沙发上用手机跟进 CI 报错,让 Agent 定位问题并修复
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. **会话存储层** - JSONL 文件(CLI 共用)+ SQLite 元数据
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
- │ │ ├── command-handler.ts # 斜杠命令处理
171
- │ │ ├── session-manager.ts # 会话管理(多项目支持)
172
- │ │ ├── message-queue.ts # 消息队列(串行+中断)
173
- │ │ ├── message-processor.ts # 统一消息处理引擎
174
- │ │ ├── stream-flusher.ts # 批量发送(3秒窗口)
175
- │ │ ├── agent-runner.ts # Claude Agent SDK 封装
176
- │ │ └── message-cache.ts # 消息缓存
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
- - `/s <名称>` - 切换到指定会话
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": true,
28
- "domain": "your-aun-domain",
29
- "agentName": "your-agent-name"
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 '../utils/permission-utils.js';
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 = 'default'; // default = 全部自动放行
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: 'default', nameZh: '默认', description: '全部自动放行', available: true },
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
- 'default': 'default', // 全部自动放行,靠 canUseTool 一律 allow 实现
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] || 'default';
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
- if (settings.model && settings.model !== this.model) {
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 newEffort = settings.effortLevel || undefined;
164
- if (newEffort !== this.effort) {
165
- logger.info(`[AgentRunner] Synced effort from ~/.claude/settings.json: ${newEffort ?? 'auto'}`);
166
- this.effort = newEffort;
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
- decision: 'block',
338
- reason: result.message
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
- // default 模式:一律 allow(替代有缺陷的 bypassPermissions)
347
- if (this.permissionMode === 'default') {
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 approved = await this.permissionGateway.requestPermission(sessionId, toolName, input, this.sendPromptFn, summary, options.decisionReason);
358
- return approved
359
- ? { behavior: 'allow', updatedInput: input }
360
- : { behavior: 'deny', message: '用户拒绝或审批超时' };
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 lastError;
459
- for (let attempt = 0; attempt < 3; attempt++) {
460
- try {
461
- let sdkStream;
462
- if (images && images.length > 0) {
463
- logger.debug('[AgentRunner] Creating query with images, images:', images.length);
464
- logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
465
- const stream = new MessageStream();
466
- stream.push(prompt, images);
467
- stream.end();
468
- sdkStream = createQuery(stream);
469
- }
470
- else {
471
- logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
472
- sdkStream = createQuery(prompt, agentSessionId);
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
- throw lastError;
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.reasoning)
42
- this.effort = resolved.reasoning;
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 = 'default';
53
+ currentMode = 'auto';
54
54
  approvalPolicy = 'never';
55
55
  setMode(mode) {
56
56
  const map = {
57
- 'default': 'never',
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: 'default', nameZh: '默认', description: '全部自动(受 sandbox 约束)', available: true },
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
  }