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/README.md +191 -0
- package/bin/evolclaw +10 -0
- package/data/evolclaw.sample.json +39 -0
- package/dist/channels/aun.js +28 -0
- package/dist/channels/feishu.js +452 -0
- package/dist/cli.js +759 -0
- package/dist/config.js +81 -0
- package/dist/core/agent-runner.js +326 -0
- package/dist/core/command-handler.js +823 -0
- package/dist/core/message-cache.js +56 -0
- package/dist/core/message-processor.js +516 -0
- package/dist/core/message-queue.js +110 -0
- package/dist/core/message-stream.js +59 -0
- package/dist/core/session-manager.js +803 -0
- package/dist/index.js +239 -0
- package/dist/paths.js +45 -0
- package/dist/types.js +1 -0
- package/dist/utils/error-utils.js +54 -0
- package/dist/utils/init.js +352 -0
- package/dist/utils/logger.js +47 -0
- package/dist/utils/markdown-to-feishu.js +38 -0
- package/dist/utils/permission.js +36 -0
- package/dist/utils/session-file-health.js +67 -0
- package/dist/utils/stream-flusher.js +151 -0
- package/dist/utils/stream-idle-monitor.js +103 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig } from './config.js';
|
|
2
|
+
import { SessionManager } from './core/session-manager.js';
|
|
3
|
+
import { AgentRunner } from './core/agent-runner.js';
|
|
4
|
+
import { FeishuChannel } from './channels/feishu.js';
|
|
5
|
+
import { AUNChannel } from './channels/aun.js';
|
|
6
|
+
import { MessageProcessor } from './core/message-processor.js';
|
|
7
|
+
import { MessageQueue } from './core/message-queue.js';
|
|
8
|
+
import { MessageCache } from './core/message-cache.js';
|
|
9
|
+
import { CommandHandler } from './core/command-handler.js';
|
|
10
|
+
import { logger } from './utils/logger.js';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
async function main() {
|
|
14
|
+
// 过滤飞书 SDK 的 info 日志
|
|
15
|
+
const originalLog = console.log;
|
|
16
|
+
const originalInfo = console.info;
|
|
17
|
+
const filter = (...args) => {
|
|
18
|
+
const firstArg = String(args[0] || '');
|
|
19
|
+
return firstArg.includes('[info]') || firstArg.includes('[ws]');
|
|
20
|
+
};
|
|
21
|
+
console.log = (...args) => {
|
|
22
|
+
if (filter(...args))
|
|
23
|
+
return;
|
|
24
|
+
originalLog(...args);
|
|
25
|
+
};
|
|
26
|
+
console.info = (...args) => {
|
|
27
|
+
if (filter(...args))
|
|
28
|
+
return;
|
|
29
|
+
originalInfo(...args);
|
|
30
|
+
};
|
|
31
|
+
logger.info('EvolClaw starting...');
|
|
32
|
+
// 确保数据目录存在
|
|
33
|
+
ensureDataDirs();
|
|
34
|
+
// 加载配置
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
const anthropic = resolveAnthropicConfig(config);
|
|
37
|
+
logger.info('✓ Config loaded (API keys hidden)');
|
|
38
|
+
if (anthropic.baseUrl) {
|
|
39
|
+
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
40
|
+
}
|
|
41
|
+
// 初始化数据库
|
|
42
|
+
const sessionManager = new SessionManager();
|
|
43
|
+
logger.info('✓ Database initialized');
|
|
44
|
+
// 初始化 Agent Runner(带持久化回调)
|
|
45
|
+
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, claudeSessionId) => {
|
|
46
|
+
await sessionManager.updateClaudeSessionIdBySessionId(sessionId, claudeSessionId);
|
|
47
|
+
}, anthropic.baseUrl, config);
|
|
48
|
+
logger.info('✓ Agent runner ready');
|
|
49
|
+
// 创建消息缓存
|
|
50
|
+
const messageCache = new MessageCache();
|
|
51
|
+
logger.info('✓ Message cache initialized');
|
|
52
|
+
// 定期清理过期消息(每小时)
|
|
53
|
+
setInterval(() => {
|
|
54
|
+
messageCache.cleanupExpired();
|
|
55
|
+
}, 60 * 60 * 1000);
|
|
56
|
+
// 飞书渠道
|
|
57
|
+
const feishu = new FeishuChannel({
|
|
58
|
+
appId: config.feishu.appId,
|
|
59
|
+
appSecret: config.feishu.appSecret,
|
|
60
|
+
db: sessionManager.getDatabase()
|
|
61
|
+
});
|
|
62
|
+
// 设置项目路径提供器
|
|
63
|
+
feishu.onProjectPathRequest(async (chatId) => {
|
|
64
|
+
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
|
|
65
|
+
return path.isAbsolute(session.projectPath)
|
|
66
|
+
? session.projectPath
|
|
67
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
68
|
+
});
|
|
69
|
+
// AUN 渠道
|
|
70
|
+
const aun = new AUNChannel({ domain: config.aun.domain, agentName: config.aun.agentName });
|
|
71
|
+
// 创建命令处理器
|
|
72
|
+
const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
|
|
73
|
+
// 创建消息处理器
|
|
74
|
+
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId) => {
|
|
75
|
+
const sendFn = async (id, text) => {
|
|
76
|
+
const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
|
|
77
|
+
if (channel === 'feishu') {
|
|
78
|
+
const fileMatches = [...text.matchAll(fileMarkerPattern)];
|
|
79
|
+
for (const match of fileMatches) {
|
|
80
|
+
const filePath = match[1].trim();
|
|
81
|
+
const session = await sessionManager.getActiveSession(channel, channelId);
|
|
82
|
+
const projectPath = session?.projectPath || process.cwd();
|
|
83
|
+
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
|
84
|
+
try {
|
|
85
|
+
await feishu.sendFile(id, absoluteFilePath);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
logger.error(`[Feishu] Failed to send file: ${absoluteFilePath}`, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
text = text.replace(fileMarkerPattern, '').trim();
|
|
92
|
+
}
|
|
93
|
+
if (text) {
|
|
94
|
+
if (channel === 'feishu')
|
|
95
|
+
await feishu.sendMessage(id, text);
|
|
96
|
+
else if (channel === 'aun')
|
|
97
|
+
await aun.sendMessage(id, text);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return cmdHandler.handle(content, channel, channelId, sendFn, userId);
|
|
101
|
+
});
|
|
102
|
+
// 回填 processor 和 messageQueue 的引用
|
|
103
|
+
cmdHandler.setProcessor(processor);
|
|
104
|
+
// 设置 compact 开始回调
|
|
105
|
+
agentRunner.setCompactStartCallback((sessionId) => {
|
|
106
|
+
processor.handleCompactStart();
|
|
107
|
+
});
|
|
108
|
+
// 创建消息队列
|
|
109
|
+
const messageQueue = new MessageQueue(async (message) => {
|
|
110
|
+
await processor.processMessage(message);
|
|
111
|
+
});
|
|
112
|
+
// 设置中断回调
|
|
113
|
+
messageQueue.setInterruptCallback(async (sessionKey) => {
|
|
114
|
+
await agentRunner.interrupt(sessionKey);
|
|
115
|
+
});
|
|
116
|
+
// 回填 messageQueue 引用
|
|
117
|
+
cmdHandler.setMessageQueue(messageQueue);
|
|
118
|
+
// 注册 Feishu 适配器
|
|
119
|
+
const feishuAdapter = {
|
|
120
|
+
name: 'feishu',
|
|
121
|
+
sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
|
|
122
|
+
sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
|
|
123
|
+
isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
|
|
124
|
+
};
|
|
125
|
+
const feishuOptions = {
|
|
126
|
+
systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:/path/to/file.txt] 系统会自动上传并发送。',
|
|
127
|
+
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
128
|
+
supportsImages: true,
|
|
129
|
+
};
|
|
130
|
+
processor.registerChannel(feishuAdapter, feishuOptions);
|
|
131
|
+
cmdHandler.registerAdapter(feishuAdapter);
|
|
132
|
+
// 注册 AUN 适配器
|
|
133
|
+
const aunAdapter = {
|
|
134
|
+
name: 'aun',
|
|
135
|
+
sendText: (channelId, text) => aun.sendMessage(channelId, text),
|
|
136
|
+
};
|
|
137
|
+
processor.registerChannel(aunAdapter);
|
|
138
|
+
cmdHandler.registerAdapter(aunAdapter);
|
|
139
|
+
// Feishu 消息处理
|
|
140
|
+
feishu.onMessage(async (chatId, content, images, userId, userName, messageId) => {
|
|
141
|
+
content = content.trim();
|
|
142
|
+
// 首次交互自动绑定主人
|
|
143
|
+
if (userId && !config.owners?.feishu) {
|
|
144
|
+
const { setOwner } = await import('./config.js');
|
|
145
|
+
setOwner(config, 'feishu', userId);
|
|
146
|
+
logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
|
|
147
|
+
}
|
|
148
|
+
// 命令立即处理,不进入队列
|
|
149
|
+
if (cmdHandler.isCommand(content)) {
|
|
150
|
+
const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId);
|
|
151
|
+
if (cmdResult !== null) {
|
|
152
|
+
if (cmdResult) {
|
|
153
|
+
try {
|
|
154
|
+
await feishu.sendMessage(chatId, cmdResult, { forceText: true });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger.error('[Feishu] Failed to send command response:', error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 获取当前项目路径
|
|
164
|
+
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
|
|
165
|
+
// 群聊消息添加用户名前缀
|
|
166
|
+
const chatMode = await feishu.getChatMode(chatId);
|
|
167
|
+
if (chatMode === 'group' && userName) {
|
|
168
|
+
content = `[${userName}] ${content}`;
|
|
169
|
+
}
|
|
170
|
+
// 普通消息进入队列
|
|
171
|
+
await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group' }, session.projectPath);
|
|
172
|
+
});
|
|
173
|
+
// AUN 消息处理
|
|
174
|
+
aun.onMessage(async (sessionId, content) => {
|
|
175
|
+
content = content.trim();
|
|
176
|
+
// 首次交互自动绑定主人
|
|
177
|
+
if (!config.owners?.aun) {
|
|
178
|
+
const { setOwner } = await import('./config.js');
|
|
179
|
+
setOwner(config, 'aun', sessionId);
|
|
180
|
+
logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
|
|
181
|
+
}
|
|
182
|
+
// 命令立即处理,不进入队列
|
|
183
|
+
if (cmdHandler.isCommand(content)) {
|
|
184
|
+
const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
|
|
185
|
+
if (cmdResult) {
|
|
186
|
+
await aun.sendMessage(sessionId, cmdResult);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// 获取当前项目路径
|
|
191
|
+
const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
|
|
192
|
+
// 普通消息进入队列
|
|
193
|
+
await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
|
|
194
|
+
});
|
|
195
|
+
// 连接渠道
|
|
196
|
+
const channels = [];
|
|
197
|
+
try {
|
|
198
|
+
await feishu.connect();
|
|
199
|
+
logger.info('✓ Feishu connected');
|
|
200
|
+
channels.push('Feishu');
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
logger.warn('⚠ Feishu connection failed (will continue without it)');
|
|
204
|
+
if (error instanceof Error) {
|
|
205
|
+
logger.warn(` Reason: ${error.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
await aun.connect();
|
|
210
|
+
logger.info('✓ AUN connected');
|
|
211
|
+
channels.push('AUN');
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logger.warn('⚠ AUN connection failed (will continue without it)');
|
|
215
|
+
if (error instanceof Error) {
|
|
216
|
+
logger.warn(` Reason: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
|
|
220
|
+
// 写入 ready 信号,供 restart-monitor 检测启动成功
|
|
221
|
+
const readySignalPath = resolvePaths().readySignal;
|
|
222
|
+
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
223
|
+
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
224
|
+
// 优雅关闭
|
|
225
|
+
const shutdown = async () => {
|
|
226
|
+
logger.info('\n\nShutting down gracefully...');
|
|
227
|
+
await feishu.disconnect();
|
|
228
|
+
await aun.disconnect();
|
|
229
|
+
sessionManager.close();
|
|
230
|
+
logger.info('✓ Shutdown complete');
|
|
231
|
+
process.exit(0);
|
|
232
|
+
};
|
|
233
|
+
process.on('SIGINT', shutdown);
|
|
234
|
+
process.on('SIGTERM', shutdown);
|
|
235
|
+
}
|
|
236
|
+
main().catch((error) => {
|
|
237
|
+
logger.error('Fatal error:', error);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
});
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
let _root = null;
|
|
5
|
+
export function resolveRoot() {
|
|
6
|
+
if (_root)
|
|
7
|
+
return _root;
|
|
8
|
+
if (process.env.EVOLCLAW_HOME) {
|
|
9
|
+
_root = process.env.EVOLCLAW_HOME;
|
|
10
|
+
}
|
|
11
|
+
else if (fs.existsSync(path.join(process.cwd(), 'data', 'evolclaw.json'))) {
|
|
12
|
+
_root = process.cwd();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
_root = path.join(os.homedir(), '.evolclaw');
|
|
16
|
+
}
|
|
17
|
+
return _root;
|
|
18
|
+
}
|
|
19
|
+
/** 重置缓存(仅供测试使用) */
|
|
20
|
+
export function _resetRoot() {
|
|
21
|
+
_root = null;
|
|
22
|
+
}
|
|
23
|
+
export function resolvePaths() {
|
|
24
|
+
const root = resolveRoot();
|
|
25
|
+
return {
|
|
26
|
+
root,
|
|
27
|
+
config: path.join(root, 'data', 'evolclaw.json'),
|
|
28
|
+
configSample: path.join(root, 'data', 'evolclaw.sample.json'),
|
|
29
|
+
db: path.join(root, 'data', 'sessions.db'),
|
|
30
|
+
pid: path.join(root, 'logs', 'evolclaw.pid'),
|
|
31
|
+
dataDir: path.join(root, 'data'),
|
|
32
|
+
logs: path.join(root, 'logs'),
|
|
33
|
+
lineStats: path.join(root, 'logs', 'line-stats.log'),
|
|
34
|
+
readySignal: path.join(root, 'logs', 'ready.signal'),
|
|
35
|
+
selfHealLog: path.join(root, 'logs', 'self-heal.md'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function ensureDataDirs() {
|
|
39
|
+
const p = resolvePaths();
|
|
40
|
+
fs.mkdirSync(p.dataDir, { recursive: true });
|
|
41
|
+
fs.mkdirSync(p.logs, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
export function getPackageRoot() {
|
|
44
|
+
return path.resolve(new URL('.', import.meta.url).pathname, '..');
|
|
45
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export var ErrorType;
|
|
2
|
+
(function (ErrorType) {
|
|
3
|
+
ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
|
|
4
|
+
ErrorType["API_ERROR"] = "api_error";
|
|
5
|
+
ErrorType["FILE_CORRUPT"] = "file_corrupt";
|
|
6
|
+
ErrorType["STREAM_ERROR"] = "stream_error";
|
|
7
|
+
ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
|
|
8
|
+
ErrorType["UNKNOWN"] = "unknown";
|
|
9
|
+
})(ErrorType || (ErrorType = {}));
|
|
10
|
+
export function classifyError(error) {
|
|
11
|
+
const msg = (error?.message || '').toLowerCase();
|
|
12
|
+
if (msg.includes('上下文过长') || msg.includes('context too long')
|
|
13
|
+
|| msg.includes('context_length_exceeded') || msg.includes('context_compact_failed')) {
|
|
14
|
+
return ErrorType.CONTEXT_TOO_LONG;
|
|
15
|
+
}
|
|
16
|
+
if (msg.includes('timeout') || msg.includes('etimedout')) {
|
|
17
|
+
return ErrorType.SDK_TIMEOUT;
|
|
18
|
+
}
|
|
19
|
+
if (msg.includes('5') && (msg.includes('00') || msg.includes('02') || msg.includes('03') || msg.includes('04'))) {
|
|
20
|
+
return ErrorType.API_ERROR;
|
|
21
|
+
}
|
|
22
|
+
if (msg.includes('enoent') || msg.includes('corrupt') || msg.includes('invalid json')) {
|
|
23
|
+
return ErrorType.FILE_CORRUPT;
|
|
24
|
+
}
|
|
25
|
+
if (msg.includes('stream') || msg.includes('aborted') || msg.includes('interrupted')) {
|
|
26
|
+
return ErrorType.STREAM_ERROR;
|
|
27
|
+
}
|
|
28
|
+
return ErrorType.UNKNOWN;
|
|
29
|
+
}
|
|
30
|
+
export function getErrorMessage(error) {
|
|
31
|
+
const msg = error?.message || String(error);
|
|
32
|
+
if (msg.includes('CONTEXT_COMPACT_FAILED')) {
|
|
33
|
+
return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
34
|
+
}
|
|
35
|
+
if (msg.includes('上下文过长') || msg.includes('context too long') || msg.includes('context_length_exceeded')) {
|
|
36
|
+
return '⚠️ 上下文过长,自动压缩重试失败,请手动输入 /compact 重试';
|
|
37
|
+
}
|
|
38
|
+
if (msg.includes('API Error: 400')) {
|
|
39
|
+
return '⚠️ 请求格式错误,请检查输入内容';
|
|
40
|
+
}
|
|
41
|
+
if (msg.includes('API Error: 500')) {
|
|
42
|
+
return '⚠️ API 服务暂时不可用,请稍后重试';
|
|
43
|
+
}
|
|
44
|
+
if (msg.includes('API Error: 429')) {
|
|
45
|
+
return '⚠️ 请求过于频繁,请稍后再试';
|
|
46
|
+
}
|
|
47
|
+
if (msg.includes('timeout')) {
|
|
48
|
+
return '⚠️ 请求超时,请重试';
|
|
49
|
+
}
|
|
50
|
+
if (msg.includes('permission') || msg.includes('im:resource')) {
|
|
51
|
+
return '⚠️ 权限不足,请联系管理员配置应用权限';
|
|
52
|
+
}
|
|
53
|
+
return '⚠️ 处理消息时出错,请稍后重试';
|
|
54
|
+
}
|