evolclaw 2.1.0 → 2.1.1
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 +9 -4
- package/dist/channels/feishu.js +7 -1
- package/dist/cli.js +13 -7
- package/dist/config.js +4 -1
- package/dist/core/agent-runner.js +32 -0
- package/dist/core/command-handler.js +196 -33
- package/dist/core/message-processor.js +39 -15
- package/dist/core/session-manager.js +24 -46
- package/dist/index.js +5 -2
- package/dist/index.js.bak +340 -0
- package/dist/utils/stream-flusher.js +42 -32
- package/package.json +1 -1
|
@@ -19,10 +19,12 @@ export class MessageProcessor {
|
|
|
19
19
|
currentFlusher;
|
|
20
20
|
currentIsGroup = false;
|
|
21
21
|
shouldSuppressActivities = false;
|
|
22
|
-
/**
|
|
22
|
+
/** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
|
|
23
23
|
async isBackgroundSession(session, channel, channelId) {
|
|
24
|
+
// 话题会话独立运行,不是后台任务
|
|
24
25
|
if (session.threadId)
|
|
25
26
|
return false;
|
|
27
|
+
// 主会话:与当前活跃会话比对
|
|
26
28
|
const active = await this.sessionManager.getActiveSession(channel, channelId);
|
|
27
29
|
return active ? session.id !== active.id : false;
|
|
28
30
|
}
|
|
@@ -61,7 +63,7 @@ export class MessageProcessor {
|
|
|
61
63
|
const isOwnerUser = isOwner(this.config, message.channel, message.userId || '');
|
|
62
64
|
// 非主人(群聊或单聊):空闲监控静默/简短
|
|
63
65
|
const quietMode = isGroup || !isOwnerUser;
|
|
64
|
-
//
|
|
66
|
+
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
65
67
|
const shouldSuppress = () => {
|
|
66
68
|
const mode = this.config.showActivities ?? 'all';
|
|
67
69
|
if (mode === 'all')
|
|
@@ -70,6 +72,8 @@ export class MessageProcessor {
|
|
|
70
72
|
return isGroup;
|
|
71
73
|
if (mode === 'owner-dm-only')
|
|
72
74
|
return isGroup || !isOwnerUser;
|
|
75
|
+
if (mode === 'none')
|
|
76
|
+
return true;
|
|
73
77
|
return false;
|
|
74
78
|
};
|
|
75
79
|
this.shouldSuppressActivities = shouldSuppress();
|
|
@@ -265,7 +269,7 @@ ${suggestions}`, sendOpts);
|
|
|
265
269
|
await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
|
|
266
270
|
}
|
|
267
271
|
// 后台任务:静默,不发送输出
|
|
268
|
-
}, this.config.flushDelay
|
|
272
|
+
}, (this.config.flushDelay || 4) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag);
|
|
269
273
|
// 保存当前 flusher,用于 compact 事件
|
|
270
274
|
this.currentFlusher = flusher;
|
|
271
275
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
@@ -311,7 +315,7 @@ ${suggestions}`, sendOpts);
|
|
|
311
315
|
// 文件存在性检查:真实路径但文件不存在,告知用户
|
|
312
316
|
if (!fs.existsSync(resolvedPath)) {
|
|
313
317
|
logger.warn(`[${adapter.name}] File not found: ${resolvedPath}`);
|
|
314
|
-
await adapter.sendText(message.channelId, `⚠️ 文件未找到: ${filePath}
|
|
318
|
+
await adapter.sendText(message.channelId, `⚠️ 文件未找到: ${filePath}`, this.getThreadSendOpts(session));
|
|
315
319
|
continue;
|
|
316
320
|
}
|
|
317
321
|
logger.info(`[${adapter.name}] Sending file: ${resolvedPath}`);
|
|
@@ -320,7 +324,7 @@ ${suggestions}`, sendOpts);
|
|
|
320
324
|
}
|
|
321
325
|
catch (error) {
|
|
322
326
|
logger.error(`[${adapter.name}] Failed to send file: ${resolvedPath}`, error);
|
|
323
|
-
await adapter.sendText(message.channelId, `❌ 文件发送失败: ${filePath}
|
|
327
|
+
await adapter.sendText(message.channelId, `❌ 文件发送失败: ${filePath}`, this.getThreadSendOpts(session));
|
|
324
328
|
}
|
|
325
329
|
}
|
|
326
330
|
}
|
|
@@ -380,7 +384,14 @@ ${suggestions}`, sendOpts);
|
|
|
380
384
|
}
|
|
381
385
|
else {
|
|
382
386
|
const userMessage = getErrorMessage(error);
|
|
383
|
-
|
|
387
|
+
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
388
|
+
let sendOpts;
|
|
389
|
+
try {
|
|
390
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
|
|
391
|
+
sendOpts = this.getThreadSendOpts(session);
|
|
392
|
+
}
|
|
393
|
+
catch { }
|
|
394
|
+
await adapter.sendText(message.channelId, userMessage, sendOpts);
|
|
384
395
|
}
|
|
385
396
|
}
|
|
386
397
|
}
|
|
@@ -388,7 +399,11 @@ ${suggestions}`, sendOpts);
|
|
|
388
399
|
* 解析会话和项目路径
|
|
389
400
|
*/
|
|
390
401
|
async resolveSession(message) {
|
|
391
|
-
|
|
402
|
+
// 话题会话:传入 rootId metadata(首条消息的 messageId 作为 rootId)
|
|
403
|
+
const metadata = message.threadId && message.messageId
|
|
404
|
+
? { feishu: { rootId: message.messageId } }
|
|
405
|
+
: undefined;
|
|
406
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata);
|
|
392
407
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
393
408
|
? session.projectPath
|
|
394
409
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -419,11 +434,13 @@ ${suggestions}`, sendOpts);
|
|
|
419
434
|
const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
|
|
420
435
|
// === 前台任务:正常处理所有事件 ===
|
|
421
436
|
if (!isCurrentlyBackground) {
|
|
422
|
-
//
|
|
437
|
+
// 流式文本事件(抑制时跳过,只累积到 allText)
|
|
423
438
|
if (event.type === 'text_delta' && event.text) {
|
|
424
439
|
hasTextDelta = true;
|
|
425
440
|
hasReceivedText = true;
|
|
426
|
-
|
|
441
|
+
if (!shouldSuppress()) {
|
|
442
|
+
flusher.addText(event.text);
|
|
443
|
+
}
|
|
427
444
|
}
|
|
428
445
|
// 系统事件:compact_boundary(群聊时静默)
|
|
429
446
|
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
@@ -457,7 +474,9 @@ ${suggestions}`, sendOpts);
|
|
|
457
474
|
else if (content.type === 'text' && content.text && !hasTextDelta) {
|
|
458
475
|
// 仅在没有 text_delta 事件时从 assistant 事件提取文本,避免重复
|
|
459
476
|
hasReceivedText = true;
|
|
460
|
-
|
|
477
|
+
if (!shouldSuppress()) {
|
|
478
|
+
flusher.addTextBlock(content.text);
|
|
479
|
+
}
|
|
461
480
|
}
|
|
462
481
|
}
|
|
463
482
|
}
|
|
@@ -470,14 +489,19 @@ ${suggestions}`, sendOpts);
|
|
|
470
489
|
flusher.addActivity(`⚠️ ${toolName}: ${errorMsg}`);
|
|
471
490
|
}
|
|
472
491
|
}
|
|
473
|
-
// Result
|
|
492
|
+
// Result 事件:最终输出
|
|
474
493
|
if (event.type === 'result' && event.result) {
|
|
475
|
-
logger.debug(`[MessageProcessor] result event: hasReceivedText=${hasReceivedText}, result="${event.result}"`);
|
|
476
|
-
if (
|
|
477
|
-
//
|
|
494
|
+
logger.debug(`[MessageProcessor] result event: hasReceivedText=${hasReceivedText}, shouldSuppress=${shouldSuppress()}, result="${event.result}"`);
|
|
495
|
+
if (shouldSuppress()) {
|
|
496
|
+
// 抑制模式:直接发送 result(跳过中间输出)
|
|
497
|
+
flusher.addText(event.result);
|
|
498
|
+
}
|
|
499
|
+
else if (!hasReceivedText) {
|
|
500
|
+
// 非抑制模式 + 无流式文本:使用 result 作为兜底
|
|
478
501
|
flusher.addText(event.result);
|
|
479
502
|
}
|
|
480
|
-
|
|
503
|
+
// 非抑制模式 + 有流式文本:已通过 text_delta 累积,无需再添加
|
|
504
|
+
await flusher.flush(true); // isFinal=true 标记最终输出
|
|
481
505
|
}
|
|
482
506
|
continue;
|
|
483
507
|
}
|
|
@@ -170,13 +170,11 @@ export class SessionManager {
|
|
|
170
170
|
`);
|
|
171
171
|
}
|
|
172
172
|
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return this.getOrCreateThreadSession(channel, channelId, normalizedThreadId, defaultProjectPath, metadata, name);
|
|
173
|
+
// 话题会话:独立查找/创建,不参与 isActive 竞争
|
|
174
|
+
if (threadId) {
|
|
175
|
+
return this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name);
|
|
177
176
|
}
|
|
178
|
-
//
|
|
179
|
-
// 1. 查找该聊天的活跃会话
|
|
177
|
+
// 主会话:查找活跃会话
|
|
180
178
|
const active = this.db.prepare(`
|
|
181
179
|
SELECT * FROM sessions
|
|
182
180
|
WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
|
|
@@ -185,28 +183,24 @@ export class SessionManager {
|
|
|
185
183
|
const validSessionId = this.validateSessionFile(active);
|
|
186
184
|
return { ...this.rowToSession(active), agentSessionId: validSessionId };
|
|
187
185
|
}
|
|
188
|
-
//
|
|
186
|
+
// 查找默认项目的主会话
|
|
189
187
|
const existing = this.db.prepare(`
|
|
190
188
|
SELECT * FROM sessions
|
|
191
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id =
|
|
189
|
+
WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ''
|
|
192
190
|
ORDER BY updated_at DESC LIMIT 1
|
|
193
|
-
`).get(channel, channelId, defaultProjectPath
|
|
191
|
+
`).get(channel, channelId, defaultProjectPath);
|
|
194
192
|
if (existing) {
|
|
195
193
|
const validSessionId = this.validateSessionFile(existing);
|
|
196
|
-
|
|
197
|
-
this.db.prepare(`
|
|
198
|
-
UPDATE sessions SET is_active = 1, updated_at = ?
|
|
199
|
-
WHERE id = ?
|
|
200
|
-
`).run(Date.now(), existing.id);
|
|
194
|
+
this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), existing.id);
|
|
201
195
|
return { ...this.rowToSession(existing), agentSessionId: validSessionId, isActive: true };
|
|
202
196
|
}
|
|
203
|
-
//
|
|
197
|
+
// 创建新主会话
|
|
204
198
|
const session = {
|
|
205
199
|
id: `${channel}-${channelId}-${Date.now()}`,
|
|
206
200
|
channel,
|
|
207
201
|
channelId,
|
|
208
202
|
projectPath: defaultProjectPath,
|
|
209
|
-
threadId:
|
|
203
|
+
threadId: '',
|
|
210
204
|
agentType: 'claude',
|
|
211
205
|
metadata,
|
|
212
206
|
name: name || '默认会话',
|
|
@@ -214,22 +208,7 @@ export class SessionManager {
|
|
|
214
208
|
createdAt: Date.now(),
|
|
215
209
|
updatedAt: Date.now()
|
|
216
210
|
};
|
|
217
|
-
|
|
218
|
-
const result = this.db.prepare(`
|
|
219
|
-
INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
|
|
220
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
221
|
-
`).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId, session.agentType, session.agentSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
|
|
222
|
-
// 如果插入被忽略(已存在),重新查询
|
|
223
|
-
if (result.changes === 0) {
|
|
224
|
-
const recheck = this.db.prepare(`
|
|
225
|
-
SELECT * FROM sessions
|
|
226
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
|
|
227
|
-
`).get(channel, channelId, defaultProjectPath, normalizedThreadId);
|
|
228
|
-
if (recheck) {
|
|
229
|
-
this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), recheck.id);
|
|
230
|
-
return { ...this.rowToSession(recheck), isActive: true, updatedAt: Date.now() };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
211
|
+
this.insertSession(session);
|
|
233
212
|
return session;
|
|
234
213
|
}
|
|
235
214
|
async updateSession(sessionId, updates) {
|
|
@@ -247,15 +226,16 @@ export class SessionManager {
|
|
|
247
226
|
this.db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`).run(...values, Date.now(), sessionId);
|
|
248
227
|
}
|
|
249
228
|
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name) {
|
|
229
|
+
// 查找已有话题会话
|
|
250
230
|
const existing = this.db.prepare(`
|
|
251
231
|
SELECT * FROM sessions
|
|
252
232
|
WHERE channel = ? AND channel_id = ? AND thread_id = ?
|
|
253
|
-
LIMIT 1
|
|
254
233
|
`).get(channel, channelId, threadId);
|
|
255
234
|
if (existing) {
|
|
256
235
|
const validSessionId = this.validateSessionFile(existing);
|
|
257
|
-
|
|
236
|
+
// 合并 metadata(如果提供)
|
|
258
237
|
if (metadata) {
|
|
238
|
+
const existingMeta = this.rowToSession(existing).metadata;
|
|
259
239
|
const merged = existingMeta ? { ...existingMeta, ...metadata } : metadata;
|
|
260
240
|
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
261
241
|
.run(JSON.stringify(merged), Date.now(), existing.id);
|
|
@@ -263,12 +243,13 @@ export class SessionManager {
|
|
|
263
243
|
}
|
|
264
244
|
return { ...this.rowToSession(existing), agentSessionId: validSessionId };
|
|
265
245
|
}
|
|
266
|
-
|
|
246
|
+
// 继承当前活跃主会话的项目路径
|
|
247
|
+
const activeMain = this.db.prepare(`
|
|
267
248
|
SELECT project_path FROM sessions
|
|
268
249
|
WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
|
|
269
|
-
LIMIT 1
|
|
270
250
|
`).get(channel, channelId);
|
|
271
|
-
const projectPath =
|
|
251
|
+
const projectPath = activeMain?.project_path || defaultProjectPath;
|
|
252
|
+
// 创建新话题会话(isActive 固定为 false)
|
|
272
253
|
const session = {
|
|
273
254
|
id: `${channel}-${channelId}-${Date.now()}`,
|
|
274
255
|
channel,
|
|
@@ -283,15 +264,6 @@ export class SessionManager {
|
|
|
283
264
|
updatedAt: Date.now()
|
|
284
265
|
};
|
|
285
266
|
this.insertSession(session);
|
|
286
|
-
// Race condition 保护
|
|
287
|
-
const recheck = this.db.prepare(`
|
|
288
|
-
SELECT * FROM sessions
|
|
289
|
-
WHERE channel = ? AND channel_id = ? AND thread_id = ?
|
|
290
|
-
LIMIT 1
|
|
291
|
-
`).get(channel, channelId, threadId);
|
|
292
|
-
if (recheck && recheck.id !== session.id) {
|
|
293
|
-
return this.rowToSession(recheck);
|
|
294
|
-
}
|
|
295
267
|
return session;
|
|
296
268
|
}
|
|
297
269
|
async switchProject(channel, channelId, newProjectPath) {
|
|
@@ -411,6 +383,12 @@ export class SessionManager {
|
|
|
411
383
|
`).run(newName, Date.now(), sessionId);
|
|
412
384
|
return result.changes > 0;
|
|
413
385
|
}
|
|
386
|
+
async unbindSession(sessionId) {
|
|
387
|
+
const result = this.db.prepare(`
|
|
388
|
+
DELETE FROM sessions WHERE id = ?
|
|
389
|
+
`).run(sessionId);
|
|
390
|
+
return result.changes > 0;
|
|
391
|
+
}
|
|
414
392
|
async createNewSession(channel, channelId, projectPath, name) {
|
|
415
393
|
// 取消当前活跃会话
|
|
416
394
|
this.deactivateAll(channel, channelId);
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,9 @@ async function main() {
|
|
|
46
46
|
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
|
|
47
47
|
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
48
48
|
}, anthropic.baseUrl, config);
|
|
49
|
+
if (anthropic.effort) {
|
|
50
|
+
agentRunner.setEffort(anthropic.effort);
|
|
51
|
+
}
|
|
49
52
|
logger.info('✓ Agent runner ready');
|
|
50
53
|
// 创建消息缓存
|
|
51
54
|
const messageCache = new MessageCache();
|
|
@@ -79,7 +82,7 @@ async function main() {
|
|
|
79
82
|
const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
|
|
80
83
|
// 创建消息处理器
|
|
81
84
|
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
|
|
82
|
-
const sendFn = async (id, text) => {
|
|
85
|
+
const sendFn = async (id, text, opts) => {
|
|
83
86
|
const adapter = cmdHandler.getAdapter(channel);
|
|
84
87
|
if (!adapter)
|
|
85
88
|
return;
|
|
@@ -105,7 +108,7 @@ async function main() {
|
|
|
105
108
|
text = text.replace(fileMarkerPattern, '').trim();
|
|
106
109
|
}
|
|
107
110
|
if (text) {
|
|
108
|
-
await adapter.sendText(id, text);
|
|
111
|
+
await adapter.sendText(id, text, opts);
|
|
109
112
|
}
|
|
110
113
|
};
|
|
111
114
|
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
@@ -0,0 +1,340 @@
|
|
|
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 { WechatChannel } from './channels/wechat.js';
|
|
7
|
+
import { MessageProcessor } from './core/message-processor.js';
|
|
8
|
+
import { MessageQueue } from './core/message-queue.js';
|
|
9
|
+
import { MessageCache } from './core/message-cache.js';
|
|
10
|
+
import { CommandHandler } from './core/command-handler.js';
|
|
11
|
+
import { logger } from './utils/logger.js';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
async function main() {
|
|
15
|
+
// 过滤飞书 SDK 的 info 日志
|
|
16
|
+
const originalLog = console.log;
|
|
17
|
+
const originalInfo = console.info;
|
|
18
|
+
const filter = (...args) => {
|
|
19
|
+
const firstArg = String(args[0] || '');
|
|
20
|
+
return firstArg.includes('[info]') || firstArg.includes('[ws]');
|
|
21
|
+
};
|
|
22
|
+
console.log = (...args) => {
|
|
23
|
+
if (filter(...args))
|
|
24
|
+
return;
|
|
25
|
+
originalLog(...args);
|
|
26
|
+
};
|
|
27
|
+
console.info = (...args) => {
|
|
28
|
+
if (filter(...args))
|
|
29
|
+
return;
|
|
30
|
+
originalInfo(...args);
|
|
31
|
+
};
|
|
32
|
+
logger.info('EvolClaw starting...');
|
|
33
|
+
// 确保数据目录存在
|
|
34
|
+
ensureDataDirs();
|
|
35
|
+
// 加载配置
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
const anthropic = resolveAnthropicConfig(config);
|
|
38
|
+
logger.info('✓ Config loaded (API keys hidden)');
|
|
39
|
+
if (anthropic.baseUrl) {
|
|
40
|
+
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
41
|
+
}
|
|
42
|
+
// 初始化数据库
|
|
43
|
+
const sessionManager = new SessionManager();
|
|
44
|
+
logger.info('✓ Database initialized');
|
|
45
|
+
// 初始化 Agent Runner(带持久化回调)
|
|
46
|
+
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
|
|
47
|
+
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
48
|
+
}, anthropic.baseUrl, config);
|
|
49
|
+
if (anthropic.effort) {
|
|
50
|
+
agentRunner.setEffort(anthropic.effort);
|
|
51
|
+
}
|
|
52
|
+
logger.info('✓ Agent runner ready');
|
|
53
|
+
// 创建消息缓存
|
|
54
|
+
const messageCache = new MessageCache();
|
|
55
|
+
logger.info('✓ Message cache initialized');
|
|
56
|
+
// 定期清理过期消息(每小时)
|
|
57
|
+
setInterval(() => {
|
|
58
|
+
messageCache.cleanupExpired();
|
|
59
|
+
}, 60 * 60 * 1000);
|
|
60
|
+
// 飞书渠道(条件初始化)
|
|
61
|
+
let feishu = null;
|
|
62
|
+
if (config.channels?.feishu?.enabled !== false && config.channels?.feishu?.appId) {
|
|
63
|
+
feishu = new FeishuChannel({
|
|
64
|
+
appId: config.channels.feishu.appId,
|
|
65
|
+
appSecret: config.channels.feishu.appSecret,
|
|
66
|
+
db: sessionManager.getDatabase()
|
|
67
|
+
});
|
|
68
|
+
// 设置项目路径提供器
|
|
69
|
+
feishu.onProjectPathRequest(async (chatId) => {
|
|
70
|
+
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
|
|
71
|
+
return path.isAbsolute(session.projectPath)
|
|
72
|
+
? session.projectPath
|
|
73
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// AUN 渠道(条件初始化)
|
|
77
|
+
let aun = null;
|
|
78
|
+
if (config.channels?.aun?.enabled !== false && config.channels?.aun?.domain) {
|
|
79
|
+
aun = new AUNChannel({ domain: config.channels.aun.domain, agentName: config.channels.aun.agentName });
|
|
80
|
+
}
|
|
81
|
+
// 创建命令处理器
|
|
82
|
+
const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
|
|
83
|
+
// 创建消息处理器
|
|
84
|
+
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
|
|
85
|
+
const sendFn = async (id, text, opts) => {
|
|
86
|
+
const adapter = cmdHandler.getAdapter(channel);
|
|
87
|
+
if (!adapter)
|
|
88
|
+
return;
|
|
89
|
+
// 文件标记处理(通过 adapter.sendFile 能力判断,不按渠道名分支)
|
|
90
|
+
if (adapter.sendFile) {
|
|
91
|
+
const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
|
|
92
|
+
const fileMatches = [...text.matchAll(fileMarkerPattern)];
|
|
93
|
+
for (const match of fileMatches) {
|
|
94
|
+
const filePath = match[1].trim();
|
|
95
|
+
// 跳过占位符/代码片段中的伪路径
|
|
96
|
+
if (!filePath || /[\\[\]{}*+?|^$]/.test(filePath))
|
|
97
|
+
continue;
|
|
98
|
+
const session = await sessionManager.getActiveSession(channel, channelId);
|
|
99
|
+
const projectPath = session?.projectPath || process.cwd();
|
|
100
|
+
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
|
101
|
+
try {
|
|
102
|
+
await adapter.sendFile(id, absoluteFilePath);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.error(`[${channel}] Failed to send file: ${absoluteFilePath}`, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
text = text.replace(fileMarkerPattern, '').trim();
|
|
109
|
+
}
|
|
110
|
+
if (text) {
|
|
111
|
+
await adapter.sendText(id, text, opts);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
115
|
+
});
|
|
116
|
+
// 回填 processor 和 messageQueue 的引用
|
|
117
|
+
cmdHandler.setProcessor(processor);
|
|
118
|
+
// 设置 compact 开始回调
|
|
119
|
+
agentRunner.setCompactStartCallback((sessionId) => {
|
|
120
|
+
processor.handleCompactStart();
|
|
121
|
+
});
|
|
122
|
+
// 创建消息队列
|
|
123
|
+
const messageQueue = new MessageQueue(async (message) => {
|
|
124
|
+
await processor.processMessage(message);
|
|
125
|
+
});
|
|
126
|
+
// 设置中断回调
|
|
127
|
+
messageQueue.setInterruptCallback(async (sessionKey) => {
|
|
128
|
+
await agentRunner.interrupt(sessionKey);
|
|
129
|
+
});
|
|
130
|
+
// 回填 messageQueue 引用
|
|
131
|
+
cmdHandler.setMessageQueue(messageQueue);
|
|
132
|
+
// 注册 Feishu 适配器(如果已初始化)
|
|
133
|
+
if (feishu) {
|
|
134
|
+
const feishuAdapter = {
|
|
135
|
+
name: 'feishu',
|
|
136
|
+
sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
|
|
137
|
+
sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
|
|
138
|
+
isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
|
|
139
|
+
};
|
|
140
|
+
const feishuOptions = {
|
|
141
|
+
systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt] 路径支持相对路径(相对项目目录)或绝对路径。系统会自动上传并发送。',
|
|
142
|
+
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
143
|
+
supportsImages: true,
|
|
144
|
+
};
|
|
145
|
+
processor.registerChannel(feishuAdapter, feishuOptions);
|
|
146
|
+
cmdHandler.registerAdapter(feishuAdapter);
|
|
147
|
+
}
|
|
148
|
+
// 注册 AUN 适配器(如果已初始化)
|
|
149
|
+
if (aun) {
|
|
150
|
+
const aunAdapter = {
|
|
151
|
+
name: 'aun',
|
|
152
|
+
sendText: (channelId, text) => aun.sendMessage(channelId, text),
|
|
153
|
+
};
|
|
154
|
+
processor.registerChannel(aunAdapter);
|
|
155
|
+
cmdHandler.registerAdapter(aunAdapter);
|
|
156
|
+
}
|
|
157
|
+
// ── WeChat 渠道(条件初始化)──
|
|
158
|
+
let wechat = null;
|
|
159
|
+
if (config.channels?.wechat?.enabled && config.channels?.wechat?.token) {
|
|
160
|
+
wechat = new WechatChannel({
|
|
161
|
+
baseUrl: config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com',
|
|
162
|
+
token: config.channels.wechat.token,
|
|
163
|
+
});
|
|
164
|
+
// 设置项目路径提供器(用于接收文件保存)
|
|
165
|
+
wechat.onProjectPathRequest(async (channelId) => {
|
|
166
|
+
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
167
|
+
return path.isAbsolute(session.projectPath)
|
|
168
|
+
? session.projectPath
|
|
169
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
170
|
+
});
|
|
171
|
+
const wechatAdapter = {
|
|
172
|
+
name: 'wechat',
|
|
173
|
+
sendText: (channelId, text) => wechat.sendMessage(channelId, text),
|
|
174
|
+
sendFile: (channelId, filePath) => wechat.sendFile(channelId, filePath),
|
|
175
|
+
};
|
|
176
|
+
const wechatOptions = {
|
|
177
|
+
systemPromptAppend: '[系统功能] 你可以发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt]',
|
|
178
|
+
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
179
|
+
};
|
|
180
|
+
processor.registerChannel(wechatAdapter, wechatOptions);
|
|
181
|
+
cmdHandler.registerAdapter(wechatAdapter);
|
|
182
|
+
// Session 过期通知(通过 Feishu 等其他渠道告知用户)
|
|
183
|
+
wechat.onSessionExpiredNotify(async (message) => {
|
|
184
|
+
// 尝试通过已注册的 Feishu owner 通知
|
|
185
|
+
const feishuOwner = config.channels?.feishu?.owner;
|
|
186
|
+
if (feishuOwner) {
|
|
187
|
+
try {
|
|
188
|
+
// Feishu owner ID 是 open_id,但 sendMessage 需要 chat_id
|
|
189
|
+
// 这里只记日志,因为 owner 的 chat_id 需要从 session 中获取
|
|
190
|
+
logger.warn(`[WeChat] ${message}`);
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
logger.warn(`[WeChat] ${message}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
wechat.onMessage(async (channelId, content, userId, images) => {
|
|
199
|
+
content = content.trim();
|
|
200
|
+
// 首次交互自动绑定主人
|
|
201
|
+
if (userId && !config.channels?.wechat?.owner) {
|
|
202
|
+
const { setOwner } = await import('./config.js');
|
|
203
|
+
setOwner(config, 'wechat', userId);
|
|
204
|
+
logger.info(`[Owner] Auto-bound WeChat owner: ${userId}`);
|
|
205
|
+
}
|
|
206
|
+
// 命令快速路径
|
|
207
|
+
if (cmdHandler.isCommand(content)) {
|
|
208
|
+
const cmdResult = await cmdHandler.handle(content, 'wechat', channelId, undefined, userId);
|
|
209
|
+
if (cmdResult !== null) {
|
|
210
|
+
if (cmdResult) {
|
|
211
|
+
try {
|
|
212
|
+
await wechat.sendMessage(channelId, cmdResult);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
logger.error('[WeChat] Failed to send command response:', error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 获取当前项目路径
|
|
222
|
+
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
223
|
+
// 普通消息进入队列
|
|
224
|
+
await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, images, timestamp: Date.now(), userId }, session.projectPath);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// Feishu 消息处理
|
|
228
|
+
if (feishu) {
|
|
229
|
+
feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
|
|
230
|
+
let content = rawContent.trim();
|
|
231
|
+
// 首次交互自动绑定主人
|
|
232
|
+
if (userId && !config.channels?.feishu?.owner) {
|
|
233
|
+
const { setOwner } = await import('./config.js');
|
|
234
|
+
setOwner(config, 'feishu', userId);
|
|
235
|
+
logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
|
|
236
|
+
}
|
|
237
|
+
// 命令立即处理,不进入队列
|
|
238
|
+
if (cmdHandler.isCommand(content)) {
|
|
239
|
+
const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
|
|
240
|
+
if (cmdResult !== null) {
|
|
241
|
+
if (cmdResult) {
|
|
242
|
+
try {
|
|
243
|
+
await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
logger.error('[Feishu] Failed to send command response:', error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// 获取当前项目路径(话题会话自动创建,携带 metadata)
|
|
253
|
+
const metadata = rootId ? { feishu: { rootId } } : undefined;
|
|
254
|
+
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
|
|
255
|
+
// 群聊消息添加用户名前缀
|
|
256
|
+
const chatMode = await feishu.getChatMode(chatId);
|
|
257
|
+
if (chatMode === 'group' && userName) {
|
|
258
|
+
content = `[${userName}] ${content}`;
|
|
259
|
+
}
|
|
260
|
+
// 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
|
|
261
|
+
await messageQueue.enqueue(session.id, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions, threadId }, session.projectPath);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// AUN 消息处理
|
|
265
|
+
if (aun) {
|
|
266
|
+
aun.onMessage(async (sessionId, content) => {
|
|
267
|
+
content = content.trim();
|
|
268
|
+
// 首次交互自动绑定主人
|
|
269
|
+
if (!config.channels?.aun?.owner) {
|
|
270
|
+
const { setOwner } = await import('./config.js');
|
|
271
|
+
setOwner(config, 'aun', sessionId);
|
|
272
|
+
logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
|
|
273
|
+
}
|
|
274
|
+
// 命令立即处理,不进入队列
|
|
275
|
+
if (cmdHandler.isCommand(content)) {
|
|
276
|
+
const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
|
|
277
|
+
if (cmdResult) {
|
|
278
|
+
await aun.sendMessage(sessionId, cmdResult);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// 获取当前项目路径
|
|
283
|
+
const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
|
|
284
|
+
// 普通消息进入队列
|
|
285
|
+
await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// 连接渠道
|
|
289
|
+
const channels = [];
|
|
290
|
+
const channelInstances = [
|
|
291
|
+
...(feishu ? [{ name: 'Feishu', instance: feishu, timeout: 5000 }] : []),
|
|
292
|
+
...(aun ? [{ name: 'AUN', instance: aun }] : []),
|
|
293
|
+
...(wechat ? [{ name: 'WeChat', instance: wechat }] : []),
|
|
294
|
+
];
|
|
295
|
+
for (const { name, instance, timeout } of channelInstances) {
|
|
296
|
+
try {
|
|
297
|
+
if (timeout) {
|
|
298
|
+
await Promise.race([
|
|
299
|
+
instance.connect(),
|
|
300
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), timeout))
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
await instance.connect();
|
|
305
|
+
}
|
|
306
|
+
logger.info(`✓ ${name} connected`);
|
|
307
|
+
channels.push(name);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
logger.warn(`⚠ ${name} connection failed (will continue without it)`);
|
|
311
|
+
if (error instanceof Error) {
|
|
312
|
+
logger.warn(` Reason: ${error.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
|
|
317
|
+
// 写入 ready 信号,供 restart-monitor 检测启动成功
|
|
318
|
+
const readySignalPath = resolvePaths().readySignal;
|
|
319
|
+
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
320
|
+
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
321
|
+
// 优雅关闭
|
|
322
|
+
const shutdown = async () => {
|
|
323
|
+
logger.info('\n\nShutting down gracefully...');
|
|
324
|
+
if (feishu)
|
|
325
|
+
await feishu.disconnect();
|
|
326
|
+
if (aun)
|
|
327
|
+
await aun.disconnect();
|
|
328
|
+
if (wechat)
|
|
329
|
+
await wechat.disconnect();
|
|
330
|
+
sessionManager.close();
|
|
331
|
+
logger.info('✓ Shutdown complete');
|
|
332
|
+
process.exit(0);
|
|
333
|
+
};
|
|
334
|
+
process.on('SIGINT', shutdown);
|
|
335
|
+
process.on('SIGTERM', shutdown);
|
|
336
|
+
}
|
|
337
|
+
main().catch((error) => {
|
|
338
|
+
logger.error('Fatal error:', error);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
});
|