evolclaw 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js ADDED
@@ -0,0 +1,81 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { logger } from './utils/logger.js';
5
+ import { resolvePaths } from './paths.js';
6
+ // Re-export path utilities for backward compatibility
7
+ export { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
8
+ function loadClaudeSettings() {
9
+ try {
10
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
11
+ if (fs.existsSync(settingsPath)) {
12
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
13
+ }
14
+ }
15
+ catch { }
16
+ return {};
17
+ }
18
+ export function resolveAnthropicConfig(config) {
19
+ const settings = loadClaudeSettings();
20
+ const apiKey = config.anthropic?.apiKey
21
+ || process.env.ANTHROPIC_AUTH_TOKEN
22
+ || settings.env?.ANTHROPIC_AUTH_TOKEN;
23
+ if (!apiKey) {
24
+ throw new Error('No API key found. Set one of: config.anthropic.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
25
+ }
26
+ const baseUrl = config.anthropic?.baseUrl
27
+ || process.env.ANTHROPIC_BASE_URL
28
+ || settings.env?.ANTHROPIC_BASE_URL;
29
+ const model = config.anthropic?.model
30
+ || settings.model
31
+ || 'sonnet';
32
+ return { apiKey, baseUrl, model };
33
+ }
34
+ export function loadConfig(configPath = resolvePaths().config) {
35
+ if (!fs.existsSync(configPath)) {
36
+ throw new Error(`Config file not found: ${configPath}`);
37
+ }
38
+ const content = fs.readFileSync(configPath, 'utf-8');
39
+ const config = JSON.parse(content);
40
+ validateConfig(config);
41
+ return config;
42
+ }
43
+ export function saveConfig(config, configPath = resolvePaths().config) {
44
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
45
+ }
46
+ export function getOwner(config, channel) {
47
+ return config.owners?.[channel];
48
+ }
49
+ export function setOwner(config, channel, userId, configPath = resolvePaths().config) {
50
+ if (!config.owners) {
51
+ config.owners = {};
52
+ }
53
+ config.owners[channel] = userId;
54
+ saveConfig(config, configPath);
55
+ }
56
+ export function isOwner(config, channel, userId) {
57
+ return config.owners?.[channel] === userId;
58
+ }
59
+ function validateConfig(config) {
60
+ // anthropic 部分不再强制校验,由 resolveAnthropicConfig() 处理
61
+ // Feishu 配置可选,但如果配置了就要完整
62
+ if (config.feishu) {
63
+ if (!config.feishu.appId || config.feishu.appId.startsWith('YOUR_')) {
64
+ logger.warn('⚠ Feishu appId not configured (Feishu channel will be disabled)');
65
+ }
66
+ if (!config.feishu.appSecret || config.feishu.appSecret.startsWith('YOUR_')) {
67
+ logger.warn('⚠ Feishu appSecret not configured (Feishu channel will be disabled)');
68
+ }
69
+ }
70
+ if (!config.aun?.domain)
71
+ throw new Error('Missing aun.domain');
72
+ if (!config.aun?.agentName)
73
+ throw new Error('Missing aun.agentName');
74
+ if (!config.projects?.defaultPath)
75
+ throw new Error('Missing projects.defaultPath');
76
+ }
77
+ export function ensureDir(dirPath) {
78
+ if (!fs.existsSync(dirPath)) {
79
+ fs.mkdirSync(dirPath, { recursive: true });
80
+ }
81
+ }
@@ -0,0 +1,326 @@
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
+ export class AgentRunner {
10
+ apiKey;
11
+ model;
12
+ baseUrl;
13
+ config;
14
+ activeSessions = new Map();
15
+ activeStreams = new Map();
16
+ onSessionIdUpdate;
17
+ onCompactStart;
18
+ constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
19
+ this.apiKey = apiKey;
20
+ this.model = model || 'sonnet';
21
+ this.baseUrl = baseUrl;
22
+ this.config = config;
23
+ this.onSessionIdUpdate = onSessionIdUpdate;
24
+ }
25
+ setModel(model) {
26
+ this.model = model;
27
+ }
28
+ getModel() {
29
+ return this.model;
30
+ }
31
+ setCompactStartCallback(callback) {
32
+ this.onCompactStart = callback;
33
+ }
34
+ async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
35
+ ensureDir(projectPath);
36
+ ensureDir(path.join(projectPath, '.claude'));
37
+ // 优先使用传入的 claudeSessionId(从数据库恢复),否则使用内存中的
38
+ let claudeSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
39
+ // 检查是否在安全模式
40
+ let skipResume = false;
41
+ if (sessionManager) {
42
+ const health = await sessionManager.getHealthStatus(sessionId);
43
+ if (health.safeMode) {
44
+ // 安全模式:不使用 resume,每次都是新对话
45
+ claudeSessionId = undefined;
46
+ skipResume = true;
47
+ logger.warn(`[AgentRunner] Safe mode enabled for ${sessionId}, not resuming session`);
48
+ }
49
+ }
50
+ // 验证会话文件是否存在且有效(仅在非安全模式且有 claudeSessionId 时)
51
+ if (claudeSessionId && !skipResume) {
52
+ const homeDir = os.homedir();
53
+ const encodedPath = projectPath.replace(/\//g, '-');
54
+ const sessionFile = path.join(homeDir, '.claude', 'projects', encodedPath, `${claudeSessionId}.jsonl`);
55
+ let isValid = false;
56
+ if (fs.existsSync(sessionFile)) {
57
+ try {
58
+ const content = fs.readFileSync(sessionFile, 'utf-8');
59
+ const lines = content.split('\n').filter(l => l.trim());
60
+ // 查找第一个包含 sessionId 和 version 的行(跳过 queue-operation)
61
+ for (const line of lines) {
62
+ try {
63
+ const data = JSON.parse(line);
64
+ if (data.sessionId && data.version) {
65
+ isValid = true;
66
+ break;
67
+ }
68
+ }
69
+ catch { }
70
+ }
71
+ if (!isValid) {
72
+ logger.warn(`[AgentRunner] Session file missing session data: ${sessionFile}`);
73
+ }
74
+ }
75
+ catch (error) {
76
+ logger.warn(`[AgentRunner] Session file corrupted: ${sessionFile}`);
77
+ }
78
+ }
79
+ if (!isValid) {
80
+ logger.warn(`[AgentRunner] Invalid session file, starting new session`);
81
+ claudeSessionId = undefined;
82
+ this.activeSessions.delete(sessionId);
83
+ if (this.onSessionIdUpdate) {
84
+ this.onSessionIdUpdate(sessionId, '');
85
+ }
86
+ }
87
+ }
88
+ // PreCompact Hook - 在压缩开始时触发
89
+ const preCompactHook = async () => {
90
+ if (this.onCompactStart) {
91
+ this.onCompactStart(sessionId);
92
+ }
93
+ return {};
94
+ };
95
+ // PreToolUse Hook - 工具执行前安全检查
96
+ const preToolUseHook = async (input) => {
97
+ const result = await canUseTool(input.tool_name, input.tool_input || {});
98
+ if (result.behavior === 'deny') {
99
+ // 使用 decision: 'block' 来拒绝工具执行
100
+ return {
101
+ decision: 'block',
102
+ reason: result.message
103
+ };
104
+ }
105
+ return {};
106
+ };
107
+ const useSettingSources = this.config?.sdk?.useSettingSources !== false;
108
+ const enableSummaries = this.config?.sdk?.agentProgressSummaries !== false;
109
+ // 公共 options(新旧模式共用)
110
+ const commonOptions = {
111
+ cwd: projectPath,
112
+ model: this.model,
113
+ canUseTool,
114
+ permissionMode: 'default',
115
+ persistSession: true,
116
+ hooks: {
117
+ PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
118
+ PreToolUse: [{ matcher: '.*', hooks: [preToolUseHook] }]
119
+ },
120
+ ...(enableSummaries ? { agentProgressSummaries: true } : {}),
121
+ stderr: (msg) => {
122
+ if (msg.includes('[ERROR]') || msg.includes('[WARN]') || msg.includes('Stream started')) {
123
+ logger.info(`[Claude-stderr] ${msg.trim()}`);
124
+ }
125
+ else {
126
+ logger.debug(`[Claude-stderr] ${msg.trim()}`);
127
+ }
128
+ },
129
+ env: {
130
+ ...process.env,
131
+ ANTHROPIC_AUTH_TOKEN: this.apiKey,
132
+ PATH: process.env.PATH,
133
+ DISABLE_AUTOUPDATER: '1',
134
+ ...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
135
+ }
136
+ };
137
+ const createQuery = (promptInput, resumeSessionId) => {
138
+ if (useSettingSources) {
139
+ // 新方式:SDK 自动加载 CLAUDE.md 和 MCP 配置
140
+ return query({
141
+ prompt: promptInput,
142
+ options: {
143
+ ...commonOptions,
144
+ settingSources: ['project', 'user'],
145
+ systemPrompt: {
146
+ type: 'preset',
147
+ preset: 'claude_code',
148
+ ...(systemPromptAppend ? { append: systemPromptAppend } : {})
149
+ },
150
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
151
+ }
152
+ });
153
+ }
154
+ else {
155
+ // 旧方式:手动加载 CLAUDE.md 和 MCP 配置(保留用于回滚)
156
+ const globalClaudeMd = (() => {
157
+ try {
158
+ const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
159
+ if (fs.existsSync(globalPath)) {
160
+ return fs.readFileSync(globalPath, 'utf-8').trim();
161
+ }
162
+ }
163
+ catch { }
164
+ return '';
165
+ })();
166
+ const projectClaudeMds = [
167
+ path.join(projectPath, 'CLAUDE.md'),
168
+ path.join(projectPath, '.claude', 'CLAUDE.md'),
169
+ ].map(p => {
170
+ try {
171
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8').trim() : '';
172
+ }
173
+ catch {
174
+ return '';
175
+ }
176
+ }).filter(Boolean);
177
+ const globalMcpServers = (() => {
178
+ try {
179
+ const mcpPath = path.join(os.homedir(), '.claude', 'mcp.json');
180
+ if (fs.existsSync(mcpPath)) {
181
+ const config = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
182
+ return config.mcpServers || {};
183
+ }
184
+ }
185
+ catch { }
186
+ return {};
187
+ })();
188
+ const fullAppend = [...projectClaudeMds, globalClaudeMd, systemPromptAppend].filter(Boolean).join('\n\n');
189
+ return query({
190
+ prompt: promptInput,
191
+ options: {
192
+ ...commonOptions,
193
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
194
+ ...(Object.keys(globalMcpServers).length > 0 ? { mcpServers: globalMcpServers } : {}),
195
+ ...(fullAppend ? {
196
+ systemPrompt: {
197
+ type: 'preset',
198
+ preset: 'claude_code',
199
+ append: fullAppend
200
+ }
201
+ } : {}),
202
+ }
203
+ });
204
+ }
205
+ };
206
+ let lastError;
207
+ for (let attempt = 0; attempt < 3; attempt++) {
208
+ try {
209
+ let queryStream;
210
+ if (images && images.length > 0) {
211
+ logger.debug('[AgentRunner] Creating query with images, images:', images.length);
212
+ logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
213
+ const stream = new MessageStream();
214
+ stream.push(prompt, images);
215
+ stream.end();
216
+ queryStream = createQuery(stream);
217
+ }
218
+ else {
219
+ logger.debug('[AgentRunner] Creating query with text only, claudeSessionId:', initialClaudeSessionId);
220
+ queryStream = createQuery(prompt, claudeSessionId);
221
+ }
222
+ this.activeStreams.set(sessionId, queryStream);
223
+ return queryStream;
224
+ }
225
+ catch (error) {
226
+ lastError = error;
227
+ if (attempt < 2) {
228
+ await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000));
229
+ }
230
+ }
231
+ }
232
+ throw lastError;
233
+ }
234
+ async interrupt(sessionId) {
235
+ const stream = this.activeStreams.get(sessionId);
236
+ if (stream && 'interrupt' in stream && typeof stream.interrupt === 'function') {
237
+ await stream.interrupt();
238
+ this.activeStreams.delete(sessionId);
239
+ logger.info(`[AgentRunner] Interrupted session: ${sessionId}`);
240
+ }
241
+ }
242
+ registerStream(key, stream) {
243
+ this.activeStreams.set(key, stream);
244
+ }
245
+ cleanupStream(sessionId) {
246
+ this.activeStreams.delete(sessionId);
247
+ }
248
+ updateSessionId(sessionId, claudeSessionId) {
249
+ logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, claudeSessionId=${claudeSessionId}`);
250
+ this.activeSessions.set(sessionId, claudeSessionId);
251
+ if (this.onSessionIdUpdate) {
252
+ this.onSessionIdUpdate(sessionId, claudeSessionId);
253
+ }
254
+ }
255
+ /**
256
+ * 主动压缩会话上下文
257
+ */
258
+ async compactSession(sessionId, claudeSessionId, projectPath) {
259
+ try {
260
+ logger.info(`[AgentRunner] Compacting session: ${claudeSessionId}`);
261
+ const stream = query({
262
+ prompt: '/compact',
263
+ options: {
264
+ cwd: projectPath,
265
+ model: this.model,
266
+ resume: claudeSessionId,
267
+ maxTurns: 1,
268
+ permissionMode: 'default',
269
+ env: {
270
+ ...process.env,
271
+ ANTHROPIC_AUTH_TOKEN: this.apiKey,
272
+ DISABLE_AUTOUPDATER: '1',
273
+ ...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
274
+ }
275
+ }
276
+ });
277
+ for await (const event of stream) {
278
+ if (event.type === 'system' && event.subtype === 'compact_boundary') {
279
+ logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
280
+ return true;
281
+ }
282
+ }
283
+ return true; // 正常结束也算成功
284
+ }
285
+ catch (error) {
286
+ logger.error('[AgentRunner] Compact failed:', error);
287
+ return false;
288
+ }
289
+ }
290
+ /**
291
+ * 通过 SDK /clear 命令清空会话历史
292
+ */
293
+ async clearSession(claudeSessionId, projectPath) {
294
+ try {
295
+ logger.info(`[AgentRunner] Clearing session via SDK: ${claudeSessionId}`);
296
+ const stream = query({
297
+ prompt: '/clear',
298
+ options: {
299
+ cwd: projectPath,
300
+ model: this.model,
301
+ resume: claudeSessionId,
302
+ maxTurns: 1,
303
+ permissionMode: 'default',
304
+ env: {
305
+ ...process.env,
306
+ ANTHROPIC_AUTH_TOKEN: this.apiKey,
307
+ DISABLE_AUTOUPDATER: '1',
308
+ ...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
309
+ }
310
+ }
311
+ });
312
+ for await (const event of stream) {
313
+ logger.debug(`[AgentRunner] Clear event: type=${event.type}, subtype=${event.subtype || 'none'}`);
314
+ }
315
+ return true;
316
+ }
317
+ catch (error) {
318
+ logger.error('[AgentRunner] Clear session failed:', error);
319
+ return false;
320
+ }
321
+ }
322
+ async closeSession(sessionId) {
323
+ this.activeSessions.delete(sessionId);
324
+ this.activeStreams.delete(sessionId);
325
+ }
326
+ }