evolclaw 2.1.0 → 2.1.2
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 +29 -65
- package/dist/index.js +5 -2
- package/dist/index.js.bak +340 -0
- package/dist/utils/error-utils.js +4 -2
- 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);
|
|
@@ -567,26 +545,12 @@ export class SessionManager {
|
|
|
567
545
|
return this.rowToSession(rows[0]);
|
|
568
546
|
}
|
|
569
547
|
async importCliSession(channel, channelId, projectPath, agentSessionId) {
|
|
570
|
-
// 检查是否已存在相同项目路径的会话
|
|
571
|
-
const existingByPath = this.db.prepare(`
|
|
572
|
-
SELECT * FROM sessions
|
|
573
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ?
|
|
574
|
-
`).get(channel, channelId, projectPath);
|
|
575
|
-
if (existingByPath) {
|
|
576
|
-
// 更新 agent_session_id 并激活
|
|
577
|
-
this.db.prepare(`
|
|
578
|
-
UPDATE sessions SET is_active = 0, updated_at = ?
|
|
579
|
-
WHERE channel = ? AND channel_id = ? AND is_active = 1 AND id != ?
|
|
580
|
-
`).run(Date.now(), channel, channelId, existingByPath.id);
|
|
581
|
-
this.db.prepare(`
|
|
582
|
-
UPDATE sessions SET agent_session_id = ?, is_active = 1, updated_at = ?
|
|
583
|
-
WHERE id = ?
|
|
584
|
-
`).run(agentSessionId, Date.now(), existingByPath.id);
|
|
585
|
-
return { ...this.rowToSession(existingByPath), agentSessionId, isActive: true, updatedAt: Date.now() };
|
|
586
|
-
}
|
|
587
548
|
// 取消当前活跃会话
|
|
588
549
|
this.deactivateAll(channel, channelId);
|
|
589
|
-
//
|
|
550
|
+
// 从 CLI 会话文件读取标题
|
|
551
|
+
const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId);
|
|
552
|
+
const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
|
553
|
+
// 创建新会话记录
|
|
590
554
|
const session = {
|
|
591
555
|
id: `${channel}-${channelId}-${Date.now()}`,
|
|
592
556
|
channel,
|
|
@@ -595,7 +559,7 @@ export class SessionManager {
|
|
|
595
559
|
threadId: '',
|
|
596
560
|
agentType: 'claude',
|
|
597
561
|
agentSessionId,
|
|
598
|
-
name
|
|
562
|
+
name,
|
|
599
563
|
isActive: true,
|
|
600
564
|
createdAt: Date.now(),
|
|
601
565
|
updatedAt: Date.now()
|
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);
|