evolclaw 2.1.2 → 2.2.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 +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +567 -205
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /package/dist/{core → utils}/message-cache.js +0 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ EvolClaw 是一个轻量级 AI Agent 网关,基于 Claude Agent SDK 构建。
|
|
|
35
35
|
|
|
36
36
|
### 核心组件
|
|
37
37
|
|
|
38
|
-
1. **消息渠道层** (`src/channels/`) - Feishu WebSocket + WeChat HTTP 长轮询
|
|
38
|
+
1. **消息渠道层** (`src/channels/`) - Feishu WebSocket + WeChat HTTP 长轮询 + AUN Mesh 网络
|
|
39
39
|
2. **消息队列层** (`src/core/message-queue.ts`) - 会话级串行处理 + 中断支持
|
|
40
40
|
3. **命令处理层** (`src/core/command-handler.ts`) - 斜杠命令处理(CommandHandler 类)
|
|
41
41
|
4. **消息处理层** (`src/core/message-processor.ts`) - 统一事件处理引擎
|
|
@@ -106,6 +106,9 @@ evolclaw init feishu
|
|
|
106
106
|
|
|
107
107
|
# 单独配置微信(扫码登录)
|
|
108
108
|
evolclaw init wechat
|
|
109
|
+
|
|
110
|
+
# 单独配置 AUN(Mesh 网络通道)
|
|
111
|
+
evolclaw init aun
|
|
109
112
|
```
|
|
110
113
|
|
|
111
114
|
交互式引导完成以下配置:
|
|
@@ -173,7 +176,8 @@ evolclaw/
|
|
|
173
176
|
│ │ └── message-cache.ts # 消息缓存
|
|
174
177
|
│ ├── channels/
|
|
175
178
|
│ │ ├── feishu.ts # 飞书 WebSocket 渠道
|
|
176
|
-
│ │
|
|
179
|
+
│ │ ├── wechat.ts # 微信 ClawBot 渠道
|
|
180
|
+
│ │ └── aun.ts # AUN Mesh 网络渠道
|
|
177
181
|
│ ├── utils/ # 工具函数
|
|
178
182
|
│ ├── types.ts # 类型定义
|
|
179
183
|
│ ├── config.ts # 配置加载
|
|
@@ -209,6 +213,8 @@ evolclaw/
|
|
|
209
213
|
- `/clear` - 清空对话历史
|
|
210
214
|
- `/compact` - 压缩会话上下文
|
|
211
215
|
- `/stop` - 中断当前任务
|
|
216
|
+
- `/send <文件路径>` - 发送文件给用户
|
|
217
|
+
- `/check` - 系统健康检查面板
|
|
212
218
|
- `/restart` - 重启服务(自愈机制)
|
|
213
219
|
- `/repair` - 检查并修复会话
|
|
214
220
|
- `/safe` - 进入安全模式
|
|
@@ -224,7 +230,7 @@ evolclaw/
|
|
|
224
230
|
|
|
225
231
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
226
232
|
- **AI SDK**:@anthropic-ai/claude-agent-sdk >= 0.2.75
|
|
227
|
-
- **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API
|
|
233
|
+
- **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、AUN Mesh 网络
|
|
228
234
|
- **数据存储**:node:sqlite(内置模块)+ JSONL(CLI 共用)
|
|
229
235
|
- **测试框架**:Vitest
|
|
230
236
|
|
|
@@ -232,6 +238,7 @@ evolclaw/
|
|
|
232
238
|
|
|
233
239
|
- [x] Windows 系统 CLI 命令支持
|
|
234
240
|
- [x] 微信插件支持图片/文件的收发
|
|
241
|
+
- [x] AUN Mesh 网络通道接入
|
|
235
242
|
- [ ] 自动授权可配置(自动放行/自动拒绝)
|
|
236
243
|
- [ ] 手动授权支持(飞书卡片/文本回复)
|
|
237
244
|
- [ ] ACP 协议支持(接入 Codex / Gemini CLI)
|
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
"agents": {
|
|
3
3
|
"anthropic": {
|
|
4
4
|
"model": "sonnet"
|
|
5
|
-
}
|
|
5
|
+
},
|
|
6
|
+
"openai": {
|
|
7
|
+
"apiKey": "your-openai-api-key",
|
|
8
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
9
|
+
"model": "gpt-5.2-codex",
|
|
10
|
+
"effort": "medium"
|
|
11
|
+
},
|
|
12
|
+
"defaultAgent": "claude"
|
|
6
13
|
},
|
|
7
14
|
"channels": {
|
|
15
|
+
"defaultChannel": "feishu",
|
|
8
16
|
"feishu": {
|
|
9
17
|
"enabled": true,
|
|
10
18
|
"appId": "",
|
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { query, forkSession as sdkForkSession } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { ensureDir, resolveAnthropicConfig } from '../config.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { checkBlacklist, summarizeToolInput } from '../utils/permission-utils.js';
|
|
8
|
+
import { encodePath } from '../utils/cross-platform.js';
|
|
9
|
+
class MessageStream {
|
|
10
|
+
queue = [];
|
|
11
|
+
waiting = null;
|
|
12
|
+
done = false;
|
|
13
|
+
push(text, images) {
|
|
14
|
+
let content;
|
|
15
|
+
if (images && images.length > 0) {
|
|
16
|
+
logger.debug('[MessageStream] Creating multimodal message with', images.length, 'images');
|
|
17
|
+
content = [
|
|
18
|
+
{ type: 'text', text },
|
|
19
|
+
...images.map((img) => ({
|
|
20
|
+
type: 'image',
|
|
21
|
+
source: {
|
|
22
|
+
type: 'base64',
|
|
23
|
+
media_type: img.mimeType || 'image/png',
|
|
24
|
+
data: img.data,
|
|
25
|
+
},
|
|
26
|
+
})),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
content = text;
|
|
31
|
+
}
|
|
32
|
+
const message = {
|
|
33
|
+
type: 'user',
|
|
34
|
+
message: { role: 'user', content },
|
|
35
|
+
parent_tool_use_id: null,
|
|
36
|
+
session_id: '',
|
|
37
|
+
};
|
|
38
|
+
this.queue.push(message);
|
|
39
|
+
this.waiting?.();
|
|
40
|
+
}
|
|
41
|
+
end() {
|
|
42
|
+
this.done = true;
|
|
43
|
+
this.waiting?.();
|
|
44
|
+
}
|
|
45
|
+
async *[Symbol.asyncIterator]() {
|
|
46
|
+
while (true) {
|
|
47
|
+
while (this.queue.length > 0) {
|
|
48
|
+
yield this.queue.shift();
|
|
49
|
+
}
|
|
50
|
+
if (this.done)
|
|
51
|
+
return;
|
|
52
|
+
await new Promise((r) => {
|
|
53
|
+
this.waiting = r;
|
|
54
|
+
});
|
|
55
|
+
this.waiting = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── 类型守卫 ──
|
|
60
|
+
export function hasModelSwitcher(agent) {
|
|
61
|
+
return typeof agent.setModel === 'function' && typeof agent.listModels === 'function';
|
|
62
|
+
}
|
|
63
|
+
export function hasPermissionController(agent) {
|
|
64
|
+
return typeof agent.setMode === 'function' && typeof agent.listModes === 'function';
|
|
65
|
+
}
|
|
66
|
+
export function hasCompact(agent) {
|
|
67
|
+
return typeof agent.compact === 'function';
|
|
68
|
+
}
|
|
69
|
+
export class AgentRunner {
|
|
70
|
+
name = 'claude';
|
|
71
|
+
capabilities = { clear: true, compact: true, fork: true };
|
|
72
|
+
apiKey;
|
|
73
|
+
model;
|
|
74
|
+
effort;
|
|
75
|
+
permissionMode = 'default'; // default = 全部自动放行
|
|
76
|
+
baseUrl;
|
|
77
|
+
config;
|
|
78
|
+
activeSessions = new Map();
|
|
79
|
+
activeStreams = new Map();
|
|
80
|
+
interruptFns = new Map();
|
|
81
|
+
onSessionIdUpdate;
|
|
82
|
+
onCompactStart;
|
|
83
|
+
permissionGateway;
|
|
84
|
+
sendPromptFn;
|
|
85
|
+
constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
|
|
86
|
+
this.apiKey = apiKey;
|
|
87
|
+
this.model = model || 'sonnet';
|
|
88
|
+
this.effort = undefined;
|
|
89
|
+
this.baseUrl = baseUrl;
|
|
90
|
+
this.config = config;
|
|
91
|
+
this.onSessionIdUpdate = onSessionIdUpdate;
|
|
92
|
+
}
|
|
93
|
+
getAgentEnv() {
|
|
94
|
+
return {
|
|
95
|
+
...process.env,
|
|
96
|
+
ANTHROPIC_AUTH_TOKEN: this.apiKey,
|
|
97
|
+
PATH: process.env.PATH,
|
|
98
|
+
DISABLE_AUTOUPDATER: '1',
|
|
99
|
+
...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
setModel(model) {
|
|
103
|
+
this.model = model;
|
|
104
|
+
}
|
|
105
|
+
getModel() {
|
|
106
|
+
return this.model;
|
|
107
|
+
}
|
|
108
|
+
listModels() {
|
|
109
|
+
return ['opus', 'sonnet', 'haiku'];
|
|
110
|
+
}
|
|
111
|
+
setEffort(effort) {
|
|
112
|
+
this.effort = effort;
|
|
113
|
+
}
|
|
114
|
+
getEffort() {
|
|
115
|
+
return this.effort;
|
|
116
|
+
}
|
|
117
|
+
// ── PermissionController 接口 ──
|
|
118
|
+
setMode(mode) {
|
|
119
|
+
this.permissionMode = mode;
|
|
120
|
+
}
|
|
121
|
+
getMode() {
|
|
122
|
+
return this.permissionMode;
|
|
123
|
+
}
|
|
124
|
+
listModes() {
|
|
125
|
+
return [
|
|
126
|
+
{ key: 'default', nameZh: '默认', description: '全部自动放行', available: true },
|
|
127
|
+
{ key: 'request', nameZh: '审批', description: '部分自动,部分询问', available: true },
|
|
128
|
+
{ key: 'edit', nameZh: '编辑', description: '自动接受编辑,其他询问', available: true },
|
|
129
|
+
{ key: 'plan', nameZh: '规划', description: '只规划不执行', available: true },
|
|
130
|
+
{ key: 'noask', nameZh: '静默', description: '未批准则拒绝', available: true },
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
setPermissionGateway(gateway) {
|
|
134
|
+
this.permissionGateway = gateway;
|
|
135
|
+
}
|
|
136
|
+
setSendPrompt(fn) {
|
|
137
|
+
this.sendPromptFn = fn;
|
|
138
|
+
}
|
|
139
|
+
toSdkPermissionMode() {
|
|
140
|
+
const map = {
|
|
141
|
+
'default': 'default', // 全部自动放行,靠 canUseTool 一律 allow 实现
|
|
142
|
+
'request': 'default', // 部分自动,部分询问
|
|
143
|
+
'edit': 'acceptEdits',
|
|
144
|
+
'plan': 'plan',
|
|
145
|
+
'noask': 'dontAsk',
|
|
146
|
+
};
|
|
147
|
+
return map[this.permissionMode] || 'default';
|
|
148
|
+
}
|
|
149
|
+
// ── Compactable 接口 ──
|
|
150
|
+
async compact(sessionId, agentSessionId, projectPath) {
|
|
151
|
+
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
152
|
+
}
|
|
153
|
+
syncFromUserSettings() {
|
|
154
|
+
try {
|
|
155
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
156
|
+
if (!fs.existsSync(settingsPath))
|
|
157
|
+
return;
|
|
158
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
159
|
+
if (settings.model && settings.model !== this.model) {
|
|
160
|
+
logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
|
|
161
|
+
this.model = settings.model;
|
|
162
|
+
}
|
|
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;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
logger.debug(`[AgentRunner] Failed to sync from ~/.claude/settings.json:`, error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
setCompactStartCallback(callback) {
|
|
174
|
+
this.onCompactStart = callback;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
178
|
+
* 所有 SDK 特有的事件类型引用封装在此方法内
|
|
179
|
+
*/
|
|
180
|
+
async *transformStream(sdkStream, sessionId) {
|
|
181
|
+
let hasTextDelta = false;
|
|
182
|
+
let lastSessionId;
|
|
183
|
+
// tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
|
|
184
|
+
const toolUseNames = new Map();
|
|
185
|
+
for await (const event of sdkStream) {
|
|
186
|
+
// 提取 session_id(任意 SDK 事件都可能携带)
|
|
187
|
+
if (event.session_id && event.session_id !== lastSessionId) {
|
|
188
|
+
lastSessionId = event.session_id;
|
|
189
|
+
this.updateSessionId(sessionId, event.session_id);
|
|
190
|
+
yield { type: 'session_id', sessionId: event.session_id };
|
|
191
|
+
}
|
|
192
|
+
// text_delta → text
|
|
193
|
+
if (event.type === 'text_delta' && event.text) {
|
|
194
|
+
hasTextDelta = true;
|
|
195
|
+
yield { type: 'text', text: event.text };
|
|
196
|
+
}
|
|
197
|
+
// system: compact_boundary → compact
|
|
198
|
+
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
199
|
+
yield { type: 'compact', preTokens: event.compact_metadata?.pre_tokens || 0 };
|
|
200
|
+
}
|
|
201
|
+
// system: task_progress → task_progress
|
|
202
|
+
if (event.type === 'system' && event.subtype === 'task_progress') {
|
|
203
|
+
yield {
|
|
204
|
+
type: 'task_progress',
|
|
205
|
+
summary: event.summary,
|
|
206
|
+
toolUses: event.tool_uses,
|
|
207
|
+
durationMs: event.duration_ms,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
|
|
211
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
212
|
+
for (const content of event.message.content) {
|
|
213
|
+
if (content.type === 'tool_use') {
|
|
214
|
+
// 记录 id → name 映射,供后续 tool_result 使用
|
|
215
|
+
if (content.id)
|
|
216
|
+
toolUseNames.set(content.id, content.name);
|
|
217
|
+
yield { type: 'tool_use', name: content.name, input: content.input };
|
|
218
|
+
}
|
|
219
|
+
else if (content.type === 'text' && content.text && !hasTextDelta) {
|
|
220
|
+
yield { type: 'text', text: content.text };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// user: 提取 tool_result 块(SDK 将工具结果嵌套在 SDKUserMessage 中)
|
|
225
|
+
if (event.type === 'user' && event.message?.content) {
|
|
226
|
+
const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
|
|
227
|
+
for (const block of contentArray) {
|
|
228
|
+
if (typeof block === 'object' && block !== null && block.type === 'tool_result') {
|
|
229
|
+
const toolName = toolUseNames.get(block.tool_use_id) || '';
|
|
230
|
+
const resultContent = typeof block.content === 'string'
|
|
231
|
+
? block.content
|
|
232
|
+
: block.content != null ? JSON.stringify(block.content) : '';
|
|
233
|
+
yield {
|
|
234
|
+
type: 'tool_result',
|
|
235
|
+
name: toolName,
|
|
236
|
+
result: resultContent,
|
|
237
|
+
isError: block.is_error === true,
|
|
238
|
+
error: block.is_error === true ? resultContent : undefined,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// result → complete(含 permission_denials 提取)
|
|
244
|
+
if (event.type === 'result') {
|
|
245
|
+
// 先发出被拒绝的权限事件
|
|
246
|
+
if (Array.isArray(event.permission_denials)) {
|
|
247
|
+
for (const denial of event.permission_denials) {
|
|
248
|
+
yield {
|
|
249
|
+
type: 'tool_result',
|
|
250
|
+
name: denial.tool_name || '',
|
|
251
|
+
result: '',
|
|
252
|
+
isError: true,
|
|
253
|
+
error: `权限被拒绝: ${denial.tool_name}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
yield {
|
|
258
|
+
type: 'complete',
|
|
259
|
+
result: event.result,
|
|
260
|
+
subtype: event.subtype,
|
|
261
|
+
isError: event.is_error,
|
|
262
|
+
errors: event.errors,
|
|
263
|
+
durationMs: event.duration_ms,
|
|
264
|
+
costUsd: event.total_cost_usd,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
|
|
270
|
+
// 同步用户级配置到内存
|
|
271
|
+
this.syncFromUserSettings();
|
|
272
|
+
ensureDir(projectPath);
|
|
273
|
+
ensureDir(path.join(projectPath, '.claude'));
|
|
274
|
+
// 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
|
|
275
|
+
let agentSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
|
|
276
|
+
// 检查是否在安全模式
|
|
277
|
+
let skipResume = false;
|
|
278
|
+
if (sessionManager) {
|
|
279
|
+
const health = await sessionManager.getHealthStatus(sessionId);
|
|
280
|
+
if (health.safeMode) {
|
|
281
|
+
// 安全模式:不使用 resume,每次都是新对话
|
|
282
|
+
agentSessionId = undefined;
|
|
283
|
+
skipResume = true;
|
|
284
|
+
logger.warn(`[AgentRunner] Safe mode enabled for ${sessionId}, not resuming session`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// 验证会话文件是否存在且有效(仅在非安全模式且有 agentSessionId 时)
|
|
288
|
+
if (agentSessionId && !skipResume) {
|
|
289
|
+
const homeDir = os.homedir();
|
|
290
|
+
const encodedProjectPath = encodePath(projectPath);
|
|
291
|
+
const sessionFile = path.join(homeDir, '.claude', 'projects', encodedProjectPath, `${agentSessionId}.jsonl`);
|
|
292
|
+
let isValid = false;
|
|
293
|
+
if (fs.existsSync(sessionFile)) {
|
|
294
|
+
try {
|
|
295
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
296
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
297
|
+
// 查找第一个包含 sessionId 和 version 的行(跳过 queue-operation)
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
try {
|
|
300
|
+
const data = JSON.parse(line);
|
|
301
|
+
if (data.sessionId && data.version) {
|
|
302
|
+
isValid = true;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch { }
|
|
307
|
+
}
|
|
308
|
+
if (!isValid) {
|
|
309
|
+
logger.warn(`[AgentRunner] Session file missing session data: ${sessionFile}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
logger.warn(`[AgentRunner] Session file corrupted: ${sessionFile}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!isValid) {
|
|
317
|
+
logger.warn(`[AgentRunner] Invalid session file, starting new session`);
|
|
318
|
+
agentSessionId = undefined;
|
|
319
|
+
this.activeSessions.delete(sessionId);
|
|
320
|
+
if (this.onSessionIdUpdate) {
|
|
321
|
+
this.onSessionIdUpdate(sessionId, '');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// PreCompact Hook - 在压缩开始时触发
|
|
326
|
+
const preCompactHook = async () => {
|
|
327
|
+
if (this.onCompactStart) {
|
|
328
|
+
this.onCompactStart(sessionId);
|
|
329
|
+
}
|
|
330
|
+
return {};
|
|
331
|
+
};
|
|
332
|
+
// PreToolUse Hook - 黑名单检查(不可绕过,所有模式都走)
|
|
333
|
+
const preToolUseHook = async (input) => {
|
|
334
|
+
const result = await checkBlacklist(input.tool_name, input.tool_input || {});
|
|
335
|
+
if (result.behavior === 'deny') {
|
|
336
|
+
return {
|
|
337
|
+
decision: 'block',
|
|
338
|
+
reason: result.message
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {};
|
|
342
|
+
};
|
|
343
|
+
// SDK-level canUseTool 回调:接入 PermissionGateway 的用户审批入口
|
|
344
|
+
// 只在 SDK 认为此工具需要用户确认时触发(黑名单已在 PreToolUse hook 拦截)
|
|
345
|
+
const canUseToolCallback = async (toolName, input, options) => {
|
|
346
|
+
// default 模式:一律 allow(替代有缺陷的 bypassPermissions)
|
|
347
|
+
if (this.permissionMode === 'default') {
|
|
348
|
+
return { behavior: 'allow', updatedInput: input };
|
|
349
|
+
}
|
|
350
|
+
// 如果 PermissionGateway 未设置(如测试环境),回退到一律 allow
|
|
351
|
+
if (!this.permissionGateway || !this.sendPromptFn) {
|
|
352
|
+
return { behavior: 'allow', updatedInput: input };
|
|
353
|
+
}
|
|
354
|
+
const summary = options.title
|
|
355
|
+
|| options.description
|
|
356
|
+
|| 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: '用户拒绝或审批超时' };
|
|
361
|
+
};
|
|
362
|
+
const useSettingSources = this.config?.agents?.anthropic?.useSettingSources !== false;
|
|
363
|
+
const enableSummaries = this.config?.agents?.anthropic?.agentProgressSummaries !== false;
|
|
364
|
+
// 公共 options(新旧模式共用)
|
|
365
|
+
const sdkPermissionMode = this.toSdkPermissionMode();
|
|
366
|
+
logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
|
|
367
|
+
const commonOptions = {
|
|
368
|
+
cwd: projectPath,
|
|
369
|
+
model: this.model,
|
|
370
|
+
...(this.effort ? { effort: this.effort } : {}),
|
|
371
|
+
canUseTool: canUseToolCallback,
|
|
372
|
+
permissionMode: sdkPermissionMode,
|
|
373
|
+
persistSession: true,
|
|
374
|
+
hooks: {
|
|
375
|
+
PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
|
|
376
|
+
PreToolUse: [{ matcher: '.*', hooks: [preToolUseHook] }]
|
|
377
|
+
},
|
|
378
|
+
...(enableSummaries ? { agentProgressSummaries: true } : {}),
|
|
379
|
+
stderr: (msg) => {
|
|
380
|
+
if (msg.includes('[ERROR]') || msg.includes('[WARN]') || msg.includes('Stream started')) {
|
|
381
|
+
logger.info(`[Claude-stderr] ${msg.trim()}`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
logger.debug(`[Claude-stderr] ${msg.trim()}`);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
env: this.getAgentEnv()
|
|
388
|
+
};
|
|
389
|
+
const createQuery = (promptInput, resumeSessionId) => {
|
|
390
|
+
if (useSettingSources) {
|
|
391
|
+
// 新方式:SDK 自动加载 CLAUDE.md 和 MCP 配置
|
|
392
|
+
return query({
|
|
393
|
+
prompt: promptInput,
|
|
394
|
+
options: {
|
|
395
|
+
...commonOptions,
|
|
396
|
+
settingSources: ['project', 'user'],
|
|
397
|
+
systemPrompt: {
|
|
398
|
+
type: 'preset',
|
|
399
|
+
preset: 'claude_code',
|
|
400
|
+
...(systemPromptAppend ? { append: systemPromptAppend } : {})
|
|
401
|
+
},
|
|
402
|
+
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// 旧方式:手动加载 CLAUDE.md 和 MCP 配置(保留用于回滚)
|
|
408
|
+
const globalClaudeMd = (() => {
|
|
409
|
+
try {
|
|
410
|
+
const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
411
|
+
if (fs.existsSync(globalPath)) {
|
|
412
|
+
return fs.readFileSync(globalPath, 'utf-8').trim();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch { }
|
|
416
|
+
return '';
|
|
417
|
+
})();
|
|
418
|
+
const projectClaudeMds = [
|
|
419
|
+
path.join(projectPath, 'CLAUDE.md'),
|
|
420
|
+
path.join(projectPath, '.claude', 'CLAUDE.md'),
|
|
421
|
+
].map(p => {
|
|
422
|
+
try {
|
|
423
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8').trim() : '';
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
}).filter(Boolean);
|
|
429
|
+
const globalMcpServers = (() => {
|
|
430
|
+
try {
|
|
431
|
+
const mcpPath = path.join(os.homedir(), '.claude', 'mcp.json');
|
|
432
|
+
if (fs.existsSync(mcpPath)) {
|
|
433
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
434
|
+
return config.mcpServers || {};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch { }
|
|
438
|
+
return {};
|
|
439
|
+
})();
|
|
440
|
+
const fullAppend = [...projectClaudeMds, globalClaudeMd, systemPromptAppend].filter(Boolean).join('\n\n');
|
|
441
|
+
return query({
|
|
442
|
+
prompt: promptInput,
|
|
443
|
+
options: {
|
|
444
|
+
...commonOptions,
|
|
445
|
+
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
446
|
+
...(Object.keys(globalMcpServers).length > 0 ? { mcpServers: globalMcpServers } : {}),
|
|
447
|
+
...(fullAppend ? {
|
|
448
|
+
systemPrompt: {
|
|
449
|
+
type: 'preset',
|
|
450
|
+
preset: 'claude_code',
|
|
451
|
+
append: fullAppend
|
|
452
|
+
}
|
|
453
|
+
} : {}),
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
};
|
|
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
|
+
}
|
|
487
|
+
}
|
|
488
|
+
throw lastError;
|
|
489
|
+
}
|
|
490
|
+
async interrupt(sessionId) {
|
|
491
|
+
const fn = this.interruptFns.get(sessionId);
|
|
492
|
+
if (fn) {
|
|
493
|
+
try {
|
|
494
|
+
await fn();
|
|
495
|
+
logger.info(`[AgentRunner] Interrupted session: ${sessionId}`);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
logger.warn(`[AgentRunner] Interrupt failed (transport closed): ${sessionId}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this.interruptFns.delete(sessionId);
|
|
502
|
+
this.activeStreams.delete(sessionId);
|
|
503
|
+
}
|
|
504
|
+
hasActiveStream(sessionId) {
|
|
505
|
+
return this.activeStreams.has(sessionId);
|
|
506
|
+
}
|
|
507
|
+
registerStream(key, stream) {
|
|
508
|
+
this.activeStreams.set(key, stream);
|
|
509
|
+
}
|
|
510
|
+
cleanupStream(sessionId) {
|
|
511
|
+
this.activeStreams.delete(sessionId);
|
|
512
|
+
this.interruptFns.delete(sessionId);
|
|
513
|
+
}
|
|
514
|
+
updateSessionId(sessionId, agentSessionId) {
|
|
515
|
+
logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
516
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
517
|
+
if (this.onSessionIdUpdate) {
|
|
518
|
+
this.onSessionIdUpdate(sessionId, agentSessionId);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
runSessionCommand(prompt, agentSessionId, projectPath) {
|
|
522
|
+
return query({
|
|
523
|
+
prompt,
|
|
524
|
+
options: {
|
|
525
|
+
cwd: projectPath,
|
|
526
|
+
model: this.model,
|
|
527
|
+
resume: agentSessionId,
|
|
528
|
+
maxTurns: 1,
|
|
529
|
+
permissionMode: this.toSdkPermissionMode(),
|
|
530
|
+
env: this.getAgentEnv()
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* 主动压缩会话上下文
|
|
536
|
+
*/
|
|
537
|
+
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
538
|
+
try {
|
|
539
|
+
logger.info(`[AgentRunner] Compacting session: ${agentSessionId}`);
|
|
540
|
+
const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
|
|
541
|
+
this.activeStreams.set(sessionId, stream);
|
|
542
|
+
try {
|
|
543
|
+
for await (const event of stream) {
|
|
544
|
+
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
545
|
+
logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
this.activeStreams.delete(sessionId);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
logger.error('[AgentRunner] Compact failed:', error);
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* 通过 SDK /clear 命令清空会话历史
|
|
562
|
+
*/
|
|
563
|
+
async clearSession(sessionId, agentSessionId, projectPath) {
|
|
564
|
+
try {
|
|
565
|
+
logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
|
|
566
|
+
const stream = this.runSessionCommand('/clear', agentSessionId, projectPath);
|
|
567
|
+
this.activeStreams.set(sessionId, stream);
|
|
568
|
+
try {
|
|
569
|
+
for await (const event of stream) {
|
|
570
|
+
logger.debug(`[AgentRunner] Clear event: type=${event.type}, subtype=${event.subtype || 'none'}`);
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
this.activeStreams.delete(sessionId);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
logger.error('[AgentRunner] Clear session failed:', error);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async closeSession(sessionId) {
|
|
584
|
+
this.activeSessions.delete(sessionId);
|
|
585
|
+
this.activeStreams.delete(sessionId);
|
|
586
|
+
this.interruptFns.delete(sessionId);
|
|
587
|
+
}
|
|
588
|
+
resolveSessionFile(agentSessionId, projectPath) {
|
|
589
|
+
const encodedProjectPath = encodePath(projectPath);
|
|
590
|
+
const sessionFile = path.join(os.homedir(), '.claude', 'projects', encodedProjectPath, `${agentSessionId}.jsonl`);
|
|
591
|
+
return fs.existsSync(sessionFile) ? sessionFile : null;
|
|
592
|
+
}
|
|
593
|
+
async forkSession(agentSessionId, projectPath, title) {
|
|
594
|
+
const result = await sdkForkSession(agentSessionId, { dir: projectPath, title });
|
|
595
|
+
return result.sessionId;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Plugin implementation
|
|
599
|
+
export class ClaudeAgentPlugin {
|
|
600
|
+
name = 'claude';
|
|
601
|
+
isEnabled(config) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
createAgent(config, callbacks) {
|
|
605
|
+
const anthropic = resolveAnthropicConfig(config);
|
|
606
|
+
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, config);
|
|
607
|
+
if (anthropic.effort) {
|
|
608
|
+
agentRunner.setEffort(anthropic.effort);
|
|
609
|
+
}
|
|
610
|
+
return { agent: agentRunner };
|
|
611
|
+
}
|
|
612
|
+
}
|