evolclaw 2.1.1 → 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 +571 -223
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/error-utils.js +4 -2
- 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
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import { ensureDir } from '../config.js';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import os from 'os';
|
|
6
|
-
import { MessageStream } from './message-stream.js';
|
|
7
|
-
import { logger } from '../utils/logger.js';
|
|
8
|
-
import { canUseTool } from '../utils/permission.js';
|
|
9
|
-
import { encodePath } from '../utils/platform.js';
|
|
10
|
-
export class AgentRunner {
|
|
11
|
-
apiKey;
|
|
12
|
-
model;
|
|
13
|
-
effort;
|
|
14
|
-
baseUrl;
|
|
15
|
-
config;
|
|
16
|
-
activeSessions = new Map();
|
|
17
|
-
activeStreams = new Map();
|
|
18
|
-
onSessionIdUpdate;
|
|
19
|
-
onCompactStart;
|
|
20
|
-
constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
|
|
21
|
-
this.apiKey = apiKey;
|
|
22
|
-
this.model = model || 'sonnet';
|
|
23
|
-
this.effort = undefined;
|
|
24
|
-
this.baseUrl = baseUrl;
|
|
25
|
-
this.config = config;
|
|
26
|
-
this.onSessionIdUpdate = onSessionIdUpdate;
|
|
27
|
-
}
|
|
28
|
-
getAgentEnv() {
|
|
29
|
-
return {
|
|
30
|
-
...process.env,
|
|
31
|
-
ANTHROPIC_AUTH_TOKEN: this.apiKey,
|
|
32
|
-
PATH: process.env.PATH,
|
|
33
|
-
DISABLE_AUTOUPDATER: '1',
|
|
34
|
-
...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
setModel(model) {
|
|
38
|
-
this.model = model;
|
|
39
|
-
}
|
|
40
|
-
getModel() {
|
|
41
|
-
return this.model;
|
|
42
|
-
}
|
|
43
|
-
setEffort(effort) {
|
|
44
|
-
this.effort = effort;
|
|
45
|
-
}
|
|
46
|
-
getEffort() {
|
|
47
|
-
return this.effort;
|
|
48
|
-
}
|
|
49
|
-
syncFromUserSettings() {
|
|
50
|
-
try {
|
|
51
|
-
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
52
|
-
if (!fs.existsSync(settingsPath))
|
|
53
|
-
return;
|
|
54
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
55
|
-
if (settings.model && settings.model !== this.model) {
|
|
56
|
-
logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
|
|
57
|
-
this.model = settings.model;
|
|
58
|
-
}
|
|
59
|
-
const newEffort = settings.effortLevel || undefined;
|
|
60
|
-
if (newEffort !== this.effort) {
|
|
61
|
-
logger.info(`[AgentRunner] Synced effort from ~/.claude/settings.json: ${newEffort ?? 'auto'}`);
|
|
62
|
-
this.effort = newEffort;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
logger.debug(`[AgentRunner] Failed to sync from ~/.claude/settings.json:`, error);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
setCompactStartCallback(callback) {
|
|
70
|
-
this.onCompactStart = callback;
|
|
71
|
-
}
|
|
72
|
-
async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
|
|
73
|
-
// 同步用户级配置到内存
|
|
74
|
-
this.syncFromUserSettings();
|
|
75
|
-
ensureDir(projectPath);
|
|
76
|
-
ensureDir(path.join(projectPath, '.claude'));
|
|
77
|
-
// 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
|
|
78
|
-
let agentSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
|
|
79
|
-
// 检查是否在安全模式
|
|
80
|
-
let skipResume = false;
|
|
81
|
-
if (sessionManager) {
|
|
82
|
-
const health = await sessionManager.getHealthStatus(sessionId);
|
|
83
|
-
if (health.safeMode) {
|
|
84
|
-
// 安全模式:不使用 resume,每次都是新对话
|
|
85
|
-
agentSessionId = undefined;
|
|
86
|
-
skipResume = true;
|
|
87
|
-
logger.warn(`[AgentRunner] Safe mode enabled for ${sessionId}, not resuming session`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// 验证会话文件是否存在且有效(仅在非安全模式且有 agentSessionId 时)
|
|
91
|
-
if (agentSessionId && !skipResume) {
|
|
92
|
-
const homeDir = os.homedir();
|
|
93
|
-
const encodedProjectPath = encodePath(projectPath);
|
|
94
|
-
const sessionFile = path.join(homeDir, '.claude', 'projects', encodedProjectPath, `${agentSessionId}.jsonl`);
|
|
95
|
-
let isValid = false;
|
|
96
|
-
if (fs.existsSync(sessionFile)) {
|
|
97
|
-
try {
|
|
98
|
-
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
99
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
100
|
-
// 查找第一个包含 sessionId 和 version 的行(跳过 queue-operation)
|
|
101
|
-
for (const line of lines) {
|
|
102
|
-
try {
|
|
103
|
-
const data = JSON.parse(line);
|
|
104
|
-
if (data.sessionId && data.version) {
|
|
105
|
-
isValid = true;
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch { }
|
|
110
|
-
}
|
|
111
|
-
if (!isValid) {
|
|
112
|
-
logger.warn(`[AgentRunner] Session file missing session data: ${sessionFile}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
logger.warn(`[AgentRunner] Session file corrupted: ${sessionFile}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (!isValid) {
|
|
120
|
-
logger.warn(`[AgentRunner] Invalid session file, starting new session`);
|
|
121
|
-
agentSessionId = undefined;
|
|
122
|
-
this.activeSessions.delete(sessionId);
|
|
123
|
-
if (this.onSessionIdUpdate) {
|
|
124
|
-
this.onSessionIdUpdate(sessionId, '');
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// PreCompact Hook - 在压缩开始时触发
|
|
129
|
-
const preCompactHook = async () => {
|
|
130
|
-
if (this.onCompactStart) {
|
|
131
|
-
this.onCompactStart(sessionId);
|
|
132
|
-
}
|
|
133
|
-
return {};
|
|
134
|
-
};
|
|
135
|
-
// PreToolUse Hook - 工具执行前安全检查
|
|
136
|
-
const preToolUseHook = async (input) => {
|
|
137
|
-
const result = await canUseTool(input.tool_name, input.tool_input || {});
|
|
138
|
-
if (result.behavior === 'deny') {
|
|
139
|
-
// 使用 decision: 'block' 来拒绝工具执行
|
|
140
|
-
return {
|
|
141
|
-
decision: 'block',
|
|
142
|
-
reason: result.message
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
return {};
|
|
146
|
-
};
|
|
147
|
-
const useSettingSources = this.config?.agents?.anthropic?.useSettingSources !== false;
|
|
148
|
-
const enableSummaries = this.config?.agents?.anthropic?.agentProgressSummaries !== false;
|
|
149
|
-
// 公共 options(新旧模式共用)
|
|
150
|
-
logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'}`);
|
|
151
|
-
const commonOptions = {
|
|
152
|
-
cwd: projectPath,
|
|
153
|
-
model: this.model,
|
|
154
|
-
...(this.effort ? { effort: this.effort } : {}),
|
|
155
|
-
canUseTool,
|
|
156
|
-
permissionMode: 'default',
|
|
157
|
-
persistSession: true,
|
|
158
|
-
hooks: {
|
|
159
|
-
PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
|
|
160
|
-
PreToolUse: [{ matcher: '.*', hooks: [preToolUseHook] }]
|
|
161
|
-
},
|
|
162
|
-
...(enableSummaries ? { agentProgressSummaries: true } : {}),
|
|
163
|
-
stderr: (msg) => {
|
|
164
|
-
if (msg.includes('[ERROR]') || msg.includes('[WARN]') || msg.includes('Stream started')) {
|
|
165
|
-
logger.info(`[Claude-stderr] ${msg.trim()}`);
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
logger.debug(`[Claude-stderr] ${msg.trim()}`);
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
env: this.getAgentEnv()
|
|
172
|
-
};
|
|
173
|
-
const createQuery = (promptInput, resumeSessionId) => {
|
|
174
|
-
if (useSettingSources) {
|
|
175
|
-
// 新方式:SDK 自动加载 CLAUDE.md 和 MCP 配置
|
|
176
|
-
return query({
|
|
177
|
-
prompt: promptInput,
|
|
178
|
-
options: {
|
|
179
|
-
...commonOptions,
|
|
180
|
-
settingSources: ['project', 'user'],
|
|
181
|
-
systemPrompt: {
|
|
182
|
-
type: 'preset',
|
|
183
|
-
preset: 'claude_code',
|
|
184
|
-
...(systemPromptAppend ? { append: systemPromptAppend } : {})
|
|
185
|
-
},
|
|
186
|
-
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
// 旧方式:手动加载 CLAUDE.md 和 MCP 配置(保留用于回滚)
|
|
192
|
-
const globalClaudeMd = (() => {
|
|
193
|
-
try {
|
|
194
|
-
const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
195
|
-
if (fs.existsSync(globalPath)) {
|
|
196
|
-
return fs.readFileSync(globalPath, 'utf-8').trim();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch { }
|
|
200
|
-
return '';
|
|
201
|
-
})();
|
|
202
|
-
const projectClaudeMds = [
|
|
203
|
-
path.join(projectPath, 'CLAUDE.md'),
|
|
204
|
-
path.join(projectPath, '.claude', 'CLAUDE.md'),
|
|
205
|
-
].map(p => {
|
|
206
|
-
try {
|
|
207
|
-
return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8').trim() : '';
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return '';
|
|
211
|
-
}
|
|
212
|
-
}).filter(Boolean);
|
|
213
|
-
const globalMcpServers = (() => {
|
|
214
|
-
try {
|
|
215
|
-
const mcpPath = path.join(os.homedir(), '.claude', 'mcp.json');
|
|
216
|
-
if (fs.existsSync(mcpPath)) {
|
|
217
|
-
const config = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
218
|
-
return config.mcpServers || {};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
catch { }
|
|
222
|
-
return {};
|
|
223
|
-
})();
|
|
224
|
-
const fullAppend = [...projectClaudeMds, globalClaudeMd, systemPromptAppend].filter(Boolean).join('\n\n');
|
|
225
|
-
return query({
|
|
226
|
-
prompt: promptInput,
|
|
227
|
-
options: {
|
|
228
|
-
...commonOptions,
|
|
229
|
-
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
230
|
-
...(Object.keys(globalMcpServers).length > 0 ? { mcpServers: globalMcpServers } : {}),
|
|
231
|
-
...(fullAppend ? {
|
|
232
|
-
systemPrompt: {
|
|
233
|
-
type: 'preset',
|
|
234
|
-
preset: 'claude_code',
|
|
235
|
-
append: fullAppend
|
|
236
|
-
}
|
|
237
|
-
} : {}),
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
let lastError;
|
|
243
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
244
|
-
try {
|
|
245
|
-
let queryStream;
|
|
246
|
-
if (images && images.length > 0) {
|
|
247
|
-
logger.debug('[AgentRunner] Creating query with images, images:', images.length);
|
|
248
|
-
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
249
|
-
const stream = new MessageStream();
|
|
250
|
-
stream.push(prompt, images);
|
|
251
|
-
stream.end();
|
|
252
|
-
queryStream = createQuery(stream);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
|
|
256
|
-
queryStream = createQuery(prompt, agentSessionId);
|
|
257
|
-
}
|
|
258
|
-
this.activeStreams.set(sessionId, queryStream);
|
|
259
|
-
return queryStream;
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
lastError = error;
|
|
263
|
-
if (attempt < 2) {
|
|
264
|
-
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
throw lastError;
|
|
269
|
-
}
|
|
270
|
-
async interrupt(sessionId) {
|
|
271
|
-
const stream = this.activeStreams.get(sessionId);
|
|
272
|
-
if (stream && 'interrupt' in stream && typeof stream.interrupt === 'function') {
|
|
273
|
-
await stream.interrupt();
|
|
274
|
-
this.activeStreams.delete(sessionId);
|
|
275
|
-
logger.info(`[AgentRunner] Interrupted session: ${sessionId}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
hasActiveStream(sessionId) {
|
|
279
|
-
return this.activeStreams.has(sessionId);
|
|
280
|
-
}
|
|
281
|
-
registerStream(key, stream) {
|
|
282
|
-
this.activeStreams.set(key, stream);
|
|
283
|
-
}
|
|
284
|
-
cleanupStream(sessionId) {
|
|
285
|
-
this.activeStreams.delete(sessionId);
|
|
286
|
-
}
|
|
287
|
-
updateSessionId(sessionId, agentSessionId) {
|
|
288
|
-
logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
289
|
-
this.activeSessions.set(sessionId, agentSessionId);
|
|
290
|
-
if (this.onSessionIdUpdate) {
|
|
291
|
-
this.onSessionIdUpdate(sessionId, agentSessionId);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
runSessionCommand(prompt, agentSessionId, projectPath) {
|
|
295
|
-
return query({
|
|
296
|
-
prompt,
|
|
297
|
-
options: {
|
|
298
|
-
cwd: projectPath,
|
|
299
|
-
model: this.model,
|
|
300
|
-
resume: agentSessionId,
|
|
301
|
-
maxTurns: 1,
|
|
302
|
-
permissionMode: 'default',
|
|
303
|
-
env: this.getAgentEnv()
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* 主动压缩会话上下文
|
|
309
|
-
*/
|
|
310
|
-
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
311
|
-
try {
|
|
312
|
-
logger.info(`[AgentRunner] Compacting session: ${agentSessionId}`);
|
|
313
|
-
const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
|
|
314
|
-
for await (const event of stream) {
|
|
315
|
-
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
316
|
-
logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return true; // 正常结束也算成功
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
logger.error('[AgentRunner] Compact failed:', error);
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* 通过 SDK /clear 命令清空会话历史
|
|
329
|
-
*/
|
|
330
|
-
async clearSession(agentSessionId, projectPath) {
|
|
331
|
-
try {
|
|
332
|
-
logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
|
|
333
|
-
const stream = this.runSessionCommand('/clear', agentSessionId, projectPath);
|
|
334
|
-
for await (const event of stream) {
|
|
335
|
-
logger.debug(`[AgentRunner] Clear event: type=${event.type}, subtype=${event.subtype || 'none'}`);
|
|
336
|
-
}
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
logger.error('[AgentRunner] Clear session failed:', error);
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
async closeSession(sessionId) {
|
|
345
|
-
this.activeSessions.delete(sessionId);
|
|
346
|
-
this.activeStreams.delete(sessionId);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MessageStream - Push-based async iterable for streaming user messages to the SDK.
|
|
3
|
-
* Based on HappyClaw's implementation.
|
|
4
|
-
*/
|
|
5
|
-
import { logger } from '../utils/logger.js';
|
|
6
|
-
export class MessageStream {
|
|
7
|
-
queue = [];
|
|
8
|
-
waiting = null;
|
|
9
|
-
done = false;
|
|
10
|
-
push(text, images) {
|
|
11
|
-
let content;
|
|
12
|
-
if (images && images.length > 0) {
|
|
13
|
-
logger.debug('[MessageStream] Creating multimodal message with', images.length, 'images');
|
|
14
|
-
logger.debug('[MessageStream] Image sizes:', images.map(img => img.data.length).join(', '));
|
|
15
|
-
// 多模态消息:text + images
|
|
16
|
-
content = [
|
|
17
|
-
{ type: 'text', text },
|
|
18
|
-
...images.map((img) => ({
|
|
19
|
-
type: 'image',
|
|
20
|
-
source: {
|
|
21
|
-
type: 'base64',
|
|
22
|
-
media_type: img.mimeType || 'image/png',
|
|
23
|
-
data: img.data,
|
|
24
|
-
},
|
|
25
|
-
})),
|
|
26
|
-
];
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
// 纯文本消息
|
|
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
|
-
logger.debug('[MessageStream] Pushing message, content type:', Array.isArray(content) ? 'array' : 'string');
|
|
39
|
-
this.queue.push(message);
|
|
40
|
-
this.waiting?.();
|
|
41
|
-
}
|
|
42
|
-
end() {
|
|
43
|
-
this.done = true;
|
|
44
|
-
this.waiting?.();
|
|
45
|
-
}
|
|
46
|
-
async *[Symbol.asyncIterator]() {
|
|
47
|
-
while (true) {
|
|
48
|
-
while (this.queue.length > 0) {
|
|
49
|
-
yield this.queue.shift();
|
|
50
|
-
}
|
|
51
|
-
if (this.done)
|
|
52
|
-
return;
|
|
53
|
-
await new Promise((r) => {
|
|
54
|
-
this.waiting = r;
|
|
55
|
-
});
|
|
56
|
-
this.waiting = null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|