evolclaw 2.5.3 → 2.5.5
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 +1 -1
- package/aun/pyproject.toml +20 -0
- package/data/evolclaw.sample.json +30 -8
- package/dist/agents/claude-runner.js +13 -1
- package/dist/channels/aun.js +2 -1
- package/dist/cli.js +9 -32
- package/dist/config.js +26 -1
- package/dist/core/command-handler.js +76 -14
- package/dist/core/message/message-processor.js +101 -83
- package/dist/core/message/stream-flusher.js +18 -2
- package/dist/core/session/session-manager.js +72 -21
- package/dist/index.js +25 -2
- package/dist/utils/init-channel.js +101 -51
- package/dist/utils/init.js +21 -12
- package/evolclaw-install-aun.md +113 -3
- package/package.json +4 -3
|
@@ -127,7 +127,7 @@ export class MessageProcessor {
|
|
|
127
127
|
static COMMAND_PREFIXES = [
|
|
128
128
|
'/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
|
|
129
129
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
130
|
-
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/
|
|
130
|
+
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
131
131
|
'/p ', '/s ', '/name ',
|
|
132
132
|
];
|
|
133
133
|
/** 判断消息内容是否为已知命令 */
|
|
@@ -299,6 +299,7 @@ export class MessageProcessor {
|
|
|
299
299
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
300
300
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
301
301
|
logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
|
|
302
|
+
logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
302
303
|
// 记录开始处理
|
|
303
304
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
304
305
|
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(message));
|
|
@@ -312,6 +313,7 @@ export class MessageProcessor {
|
|
|
312
313
|
// 创建 StreamFlusher,传入文件标记模式用于自动过滤
|
|
313
314
|
// 使用动态判断,确保切换项目后不会继续输出
|
|
314
315
|
let firstReply = true;
|
|
316
|
+
const isProactive = session.sessionMode === 'proactive';
|
|
315
317
|
const flusher = new StreamFlusher(async (text, isFinal, hasText) => {
|
|
316
318
|
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
317
319
|
if (!isCurrentlyBackground) {
|
|
@@ -333,7 +335,7 @@ export class MessageProcessor {
|
|
|
333
335
|
await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
|
|
334
336
|
}
|
|
335
337
|
// 后台任务:静默,不发送输出
|
|
336
|
-
}, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag);
|
|
338
|
+
}, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
|
|
337
339
|
// 保存当前 flusher,用于 compact 事件
|
|
338
340
|
this.currentFlusher = flusher;
|
|
339
341
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
@@ -396,24 +398,30 @@ export class MessageProcessor {
|
|
|
396
398
|
contextParts.push(`[当前环境] ${envParts.join(' | ')}`);
|
|
397
399
|
// 只读模式提示
|
|
398
400
|
if (session.metadata?.permissionMode === 'readonly') {
|
|
399
|
-
|
|
401
|
+
const sendHint = isProactive
|
|
402
|
+
? '使用 evolclaw ctl file 发送'
|
|
403
|
+
: '使用 [SEND_FILE:] 发送';
|
|
404
|
+
contextParts.push(`[只读模式] 禁止修改项目文件。如需生成文件供用户下载,请写入 .evolclaw/tmp/ 目录后${sendHint}`);
|
|
400
405
|
}
|
|
401
406
|
// 2. 文件发送能力(按 channelType 去重,提示词只展示第一级通道名)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
+
// proactive 模式:不推送 [SEND_FILE:] 提示,统一通过 evolclaw ctl file 显式发送(与 ctl send 契约一致)
|
|
408
|
+
if (!isProactive) {
|
|
409
|
+
const fileChannelTypes = new Set();
|
|
410
|
+
const currentCanSend = !!channelInfo.adapter.sendFile;
|
|
411
|
+
for (const [, info] of this.channels) {
|
|
412
|
+
if (info.adapter.sendFile) {
|
|
413
|
+
fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
|
|
417
|
+
if (currentCanSend || crossChannelTypes.length > 0) {
|
|
418
|
+
const hints = [];
|
|
419
|
+
if (currentCanSend)
|
|
420
|
+
hints.push(`[SEND_FILE:路径] 发送文件到当前通道`);
|
|
421
|
+
if (crossChannelTypes.length > 0)
|
|
422
|
+
hints.push(`[SEND_FILE:${crossChannelTypes[0]}:路径] 发送文件到指定通道(可用: ${crossChannelTypes.join('/')})`);
|
|
423
|
+
contextParts.push(hints.join(','));
|
|
407
424
|
}
|
|
408
|
-
}
|
|
409
|
-
const crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
|
|
410
|
-
if (currentCanSend || crossChannelTypes.length > 0) {
|
|
411
|
-
const hints = [];
|
|
412
|
-
if (currentCanSend)
|
|
413
|
-
hints.push(`[SEND_FILE:路径] 发送文件到当前通道`);
|
|
414
|
-
if (crossChannelTypes.length > 0)
|
|
415
|
-
hints.push(`[SEND_FILE:${crossChannelTypes[0]}:路径] 发送文件到指定通道(可用: ${crossChannelTypes.join('/')})`);
|
|
416
|
-
contextParts.push(hints.join(','));
|
|
417
425
|
}
|
|
418
426
|
// 3. 当前通道能力
|
|
419
427
|
const capParts = [];
|
|
@@ -439,6 +447,13 @@ export class MessageProcessor {
|
|
|
439
447
|
if (skillsHint) {
|
|
440
448
|
contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
|
|
441
449
|
}
|
|
450
|
+
// 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
|
|
451
|
+
if (isProactive) {
|
|
452
|
+
contextParts.push('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
|
|
453
|
+
'- 发送文本:evolclaw ctl send "<消息内容>"\n' +
|
|
454
|
+
'- 发送文件:evolclaw ctl file <路径>\n' +
|
|
455
|
+
'可多次调用。如不调用,用户将看不到任何回复。');
|
|
456
|
+
}
|
|
442
457
|
const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
443
458
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
444
459
|
const MAX_RETRIES = 3;
|
|
@@ -491,77 +506,80 @@ export class MessageProcessor {
|
|
|
491
506
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
492
507
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
493
508
|
// suppressed 模式下 flusher 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// 解析目标:按实例名匹配,再按 channelType 映射
|
|
509
|
-
let targetInfo = targetSpec ? this.channels.get(targetSpec) : channelInfo;
|
|
510
|
-
let targetLabel = targetSpec || message.channel;
|
|
511
|
-
if (targetSpec && !targetInfo) {
|
|
512
|
-
// 按 channelType 查找首个匹配的实例
|
|
513
|
-
const instanceName = this.channelTypeMap.get(targetSpec);
|
|
514
|
-
if (instanceName)
|
|
515
|
-
targetInfo = this.channels.get(instanceName);
|
|
516
|
-
}
|
|
517
|
-
const currentChannelType = channelInfo.options?.channelType || adapter.channelName;
|
|
518
|
-
const isCrossChannel = targetSpec && targetSpec !== message.channel
|
|
519
|
-
&& targetSpec !== currentChannelType;
|
|
520
|
-
// 跨通道仅限 owner
|
|
521
|
-
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
522
|
-
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, this.getReplyContext(message));
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
526
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
527
|
-
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
528
|
-
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, this.getReplyContext(message));
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
// 找目标 adapter
|
|
532
|
-
if (!targetInfo) {
|
|
533
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, this.getReplyContext(message));
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
if (!targetInfo.adapter.sendFile) {
|
|
537
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, this.getReplyContext(message));
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
// 找目标 channelId
|
|
541
|
-
let targetChannelId = message.channelId;
|
|
542
|
-
if (isCrossChannel) {
|
|
543
|
-
const targetAdapterName = targetInfo.adapter.channelName;
|
|
544
|
-
const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
|
|
545
|
-
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
546
|
-
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
547
|
-
if (!targetChannelId) {
|
|
548
|
-
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, this.getReplyContext(message));
|
|
509
|
+
// proactive 模式:agent 主动调用 ctl file 发送文件,跳过标记处理
|
|
510
|
+
if (!isProactive) {
|
|
511
|
+
const FILE_MARKER_RE = /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
|
|
512
|
+
const markerPattern = options?.fileMarkerPattern ?? FILE_MARKER_RE;
|
|
513
|
+
const flusherText = flusher.getFinalText();
|
|
514
|
+
const fullText = flusherText.length >= (streamResult.fullText?.length || 0) ? flusherText : streamResult.fullText;
|
|
515
|
+
const fileMatches = [...fullText.matchAll(markerPattern)];
|
|
516
|
+
for (const match of fileMatches) {
|
|
517
|
+
// 兼容旧格式 (1组) 和新格式 (2组)
|
|
518
|
+
const hasChannelGroup = match.length >= 3;
|
|
519
|
+
const targetSpec = hasChannelGroup ? (match[1] ?? undefined) : undefined;
|
|
520
|
+
const filePath = (hasChannelGroup ? match[2] : match[1]).trim();
|
|
521
|
+
if (this.isPlaceholderPath(filePath)) {
|
|
522
|
+
logger.info(`[${adapter.channelName}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
|
|
549
523
|
continue;
|
|
550
524
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
525
|
+
// 解析目标:按实例名匹配,再按 channelType 映射
|
|
526
|
+
let targetInfo = targetSpec ? this.channels.get(targetSpec) : channelInfo;
|
|
527
|
+
let targetLabel = targetSpec || message.channel;
|
|
528
|
+
if (targetSpec && !targetInfo) {
|
|
529
|
+
// 按 channelType 查找首个匹配的实例
|
|
530
|
+
const instanceName = this.channelTypeMap.get(targetSpec);
|
|
531
|
+
if (instanceName)
|
|
532
|
+
targetInfo = this.channels.get(instanceName);
|
|
533
|
+
}
|
|
534
|
+
const currentChannelType = channelInfo.options?.channelType || adapter.channelName;
|
|
535
|
+
const isCrossChannel = targetSpec && targetSpec !== message.channel
|
|
536
|
+
&& targetSpec !== currentChannelType;
|
|
537
|
+
// 跨通道仅限 owner
|
|
538
|
+
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
539
|
+
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, this.getReplyContext(message));
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
543
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
544
|
+
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
545
|
+
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, this.getReplyContext(message));
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
// 找目标 adapter
|
|
549
|
+
if (!targetInfo) {
|
|
550
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, this.getReplyContext(message));
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (!targetInfo.adapter.sendFile) {
|
|
554
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, this.getReplyContext(message));
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
// 找目标 channelId
|
|
558
|
+
let targetChannelId = message.channelId;
|
|
556
559
|
if (isCrossChannel) {
|
|
557
|
-
|
|
560
|
+
const targetAdapterName = targetInfo.adapter.channelName;
|
|
561
|
+
const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
|
|
562
|
+
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
563
|
+
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
564
|
+
if (!targetChannelId) {
|
|
565
|
+
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, this.getReplyContext(message));
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
570
|
+
try {
|
|
571
|
+
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, this.getReplyContext(message));
|
|
572
|
+
this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
573
|
+
if (isCrossChannel) {
|
|
574
|
+
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, this.getReplyContext(message));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
579
|
+
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, this.getReplyContext(message));
|
|
558
580
|
}
|
|
559
581
|
}
|
|
560
|
-
|
|
561
|
-
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
562
|
-
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, this.getReplyContext(message));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
582
|
+
} // end of !isProactive
|
|
565
583
|
// 最终回复文本添加到 flusher(统一在流结束后处理,避免多 complete 事件重复发送)
|
|
566
584
|
// suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
|
|
567
585
|
// 非 suppressed 且无流式文本:同上
|
|
@@ -32,6 +32,7 @@ let instanceCounter = 0;
|
|
|
32
32
|
export class StreamFlusher {
|
|
33
33
|
send;
|
|
34
34
|
interval;
|
|
35
|
+
silent;
|
|
35
36
|
buffer = '';
|
|
36
37
|
queue = []; // 按入队顺序记录 activity 和 text 段
|
|
37
38
|
timer;
|
|
@@ -45,14 +46,15 @@ export class StreamFlusher {
|
|
|
45
46
|
createTime = Date.now();
|
|
46
47
|
diagEnabled;
|
|
47
48
|
sendChain = Promise.resolve(); // 串行发送队列,保证消息按序到达
|
|
48
|
-
constructor(send, interval = 4000, fileMarkerPattern, diagEnabled = false) {
|
|
49
|
+
constructor(send, interval = 4000, fileMarkerPattern, diagEnabled = false, silent = false) {
|
|
49
50
|
this.send = send;
|
|
50
51
|
this.interval = interval;
|
|
52
|
+
this.silent = silent;
|
|
51
53
|
this.fileMarkerPattern = fileMarkerPattern;
|
|
52
54
|
this.diagEnabled = diagEnabled;
|
|
53
55
|
this.instanceId = `F${++instanceCounter}`;
|
|
54
56
|
if (this.diagEnabled)
|
|
55
|
-
diag(this.instanceId, 'created', { interval });
|
|
57
|
+
diag(this.instanceId, 'created', { interval, silent });
|
|
56
58
|
}
|
|
57
59
|
addText(text) {
|
|
58
60
|
if (this.buffer.length === 0 && text.length > 0) {
|
|
@@ -101,6 +103,8 @@ export class StreamFlusher {
|
|
|
101
103
|
this.buffer = this.buffer.replace(pattern, '').trim();
|
|
102
104
|
}
|
|
103
105
|
scheduleFlush() {
|
|
106
|
+
if (this.silent)
|
|
107
|
+
return; // proactive 模式:不调度发送
|
|
104
108
|
if (this.timer) {
|
|
105
109
|
if (this.diagEnabled)
|
|
106
110
|
diag(this.instanceId, 'scheduleFlush:skip', { reason: 'timer_exists' });
|
|
@@ -144,6 +148,8 @@ export class StreamFlusher {
|
|
|
144
148
|
* 用于 complete 事件前清空 pending activities,让最终文本留给 flush(true) 发送
|
|
145
149
|
*/
|
|
146
150
|
async flushActivitiesOnly() {
|
|
151
|
+
if (this.silent)
|
|
152
|
+
return;
|
|
147
153
|
const hasActivities = this.queue.some(e => e.kind === 'activity');
|
|
148
154
|
if (!hasActivities)
|
|
149
155
|
return;
|
|
@@ -173,6 +179,16 @@ export class StreamFlusher {
|
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
async flush(isFinal) {
|
|
182
|
+
if (this.silent) {
|
|
183
|
+
// 清理内部状态,避免后续误用
|
|
184
|
+
if (this.timer) {
|
|
185
|
+
clearTimeout(this.timer);
|
|
186
|
+
this.timer = undefined;
|
|
187
|
+
}
|
|
188
|
+
this.queue = [];
|
|
189
|
+
this.buffer = '';
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
176
192
|
if (this.timer) {
|
|
177
193
|
clearTimeout(this.timer);
|
|
178
194
|
this.timer = undefined;
|
|
@@ -10,6 +10,7 @@ export class SessionManager {
|
|
|
10
10
|
eventBus;
|
|
11
11
|
ownerResolver;
|
|
12
12
|
adminResolver;
|
|
13
|
+
sessionModeResolver;
|
|
13
14
|
fileAdapters = new Map();
|
|
14
15
|
constructor(dbPath = resolvePaths().db, eventBus, ownerResolver, adminResolver) {
|
|
15
16
|
ensureDir(path.dirname(dbPath));
|
|
@@ -25,6 +26,15 @@ export class SessionManager {
|
|
|
25
26
|
setAdminResolver(resolver) {
|
|
26
27
|
this.adminResolver = resolver;
|
|
27
28
|
}
|
|
29
|
+
setSessionModeResolver(resolver) {
|
|
30
|
+
this.sessionModeResolver = resolver;
|
|
31
|
+
}
|
|
32
|
+
/** 解析默认 sessionMode:通道配置锁定 > chatType 默认 > 'interactive' */
|
|
33
|
+
resolveDefaultSessionMode(channel, chatType) {
|
|
34
|
+
const ct = chatType || 'private';
|
|
35
|
+
const resolved = this.sessionModeResolver?.(channel, ct);
|
|
36
|
+
return resolved || 'interactive';
|
|
37
|
+
}
|
|
28
38
|
registerFileAdapter(adapter) {
|
|
29
39
|
this.fileAdapters.set(adapter.agentId, adapter);
|
|
30
40
|
logger.debug(`[SessionManager] Registered file adapter: ${adapter.agentId}`);
|
|
@@ -93,6 +103,15 @@ export class SessionManager {
|
|
|
93
103
|
`).run(JSON.stringify(metadata), Date.now(), row.id);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
106
|
+
/** 获取当前活跃 session 的 chatType(用于新建 session 时继承) */
|
|
107
|
+
getActiveChatType(channel, channelId) {
|
|
108
|
+
const row = this.db.prepare(`
|
|
109
|
+
SELECT chat_type FROM sessions
|
|
110
|
+
WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = '' AND deleted_at IS NULL
|
|
111
|
+
ORDER BY updated_at DESC LIMIT 1
|
|
112
|
+
`).get(channel, channelId);
|
|
113
|
+
return row?.chat_type || 'private';
|
|
114
|
+
}
|
|
96
115
|
validateSessionFile(row) {
|
|
97
116
|
const agentSessionId = row.agent_session_id;
|
|
98
117
|
if (!agentSessionId)
|
|
@@ -447,6 +466,13 @@ export class SessionManager {
|
|
|
447
466
|
const validSessionId = this.validateSessionFile(active);
|
|
448
467
|
const session = { ...this.rowToSession(active), agentSessionId: validSessionId };
|
|
449
468
|
session.identity = this.resolveIdentity(channel, userId);
|
|
469
|
+
// chatType 自动修正:入站 chatType 与存储值不一致时更新(修复历史 session 因错误识别留下的脏数据)
|
|
470
|
+
if (chatType && session.chatType !== chatType) {
|
|
471
|
+
logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
|
|
472
|
+
this.db.prepare(`UPDATE sessions SET chat_type = ?, updated_at = ? WHERE id = ?`)
|
|
473
|
+
.run(chatType, Date.now(), active.id);
|
|
474
|
+
session.chatType = chatType;
|
|
475
|
+
}
|
|
450
476
|
// 补写 peerId/peerName/channelName(旧 session 可能在这些字段引入前创建)
|
|
451
477
|
if (chatType === 'private' && userId) {
|
|
452
478
|
const activeMeta = active.metadata ? JSON.parse(active.metadata) : {};
|
|
@@ -492,6 +518,12 @@ export class SessionManager {
|
|
|
492
518
|
// 激活此会话
|
|
493
519
|
const existingMeta = existing.metadata ? JSON.parse(existing.metadata) : {};
|
|
494
520
|
existingMeta.isActive = true;
|
|
521
|
+
// chatType 自动修正(同 active 分支)
|
|
522
|
+
const shouldUpdateChatType = chatType !== undefined && existing.chat_type !== chatType;
|
|
523
|
+
if (shouldUpdateChatType) {
|
|
524
|
+
logger.info(`[SessionManager] Updating chatType for session ${existing.id}: ${existing.chat_type} -> ${chatType}`);
|
|
525
|
+
existing.chat_type = chatType;
|
|
526
|
+
}
|
|
495
527
|
// 补写 peerId/peerName
|
|
496
528
|
if (chatType === 'private' && userId && !existingMeta.peerId) {
|
|
497
529
|
existingMeta.peerId = userId;
|
|
@@ -499,8 +531,14 @@ export class SessionManager {
|
|
|
499
531
|
if (chatType === 'private' && metadata?.peerName && !existingMeta.peerName) {
|
|
500
532
|
existingMeta.peerName = metadata.peerName;
|
|
501
533
|
}
|
|
502
|
-
|
|
503
|
-
.
|
|
534
|
+
if (shouldUpdateChatType) {
|
|
535
|
+
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ?, chat_type = ? WHERE id = ?`)
|
|
536
|
+
.run(JSON.stringify(existingMeta), Date.now(), chatType, existing.id);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
540
|
+
.run(JSON.stringify(existingMeta), Date.now(), existing.id);
|
|
541
|
+
}
|
|
504
542
|
const session = { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: existingMeta };
|
|
505
543
|
session.identity = this.resolveIdentity(channel, userId);
|
|
506
544
|
return session;
|
|
@@ -515,7 +553,7 @@ export class SessionManager {
|
|
|
515
553
|
threadId: '',
|
|
516
554
|
agentId: agentId || 'claude',
|
|
517
555
|
chatType: chatType || 'private',
|
|
518
|
-
sessionMode: '
|
|
556
|
+
sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private'),
|
|
519
557
|
metadata: sessionMetadata,
|
|
520
558
|
name: name || '默认会话',
|
|
521
559
|
createdAt: Date.now(),
|
|
@@ -550,6 +588,10 @@ export class SessionManager {
|
|
|
550
588
|
sets.push('name = ?');
|
|
551
589
|
values.push(updates.name);
|
|
552
590
|
}
|
|
591
|
+
if (updates.sessionMode !== undefined) {
|
|
592
|
+
sets.push('session_mode = ?');
|
|
593
|
+
values.push(updates.sessionMode);
|
|
594
|
+
}
|
|
553
595
|
if (updates.metadata !== undefined) {
|
|
554
596
|
sets.push('metadata = ?');
|
|
555
597
|
values.push(updates.metadata ? JSON.stringify(updates.metadata) : null);
|
|
@@ -583,13 +625,14 @@ export class SessionManager {
|
|
|
583
625
|
}
|
|
584
626
|
return { ...this.rowToSession(existing), agentSessionId: validSessionId };
|
|
585
627
|
}
|
|
586
|
-
//
|
|
628
|
+
// 继承当前活跃主会话的项目路径和 chatType
|
|
587
629
|
const activeMain = this.db.prepare(`
|
|
588
|
-
SELECT project_path FROM sessions
|
|
630
|
+
SELECT project_path, chat_type FROM sessions
|
|
589
631
|
WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = ''
|
|
590
632
|
`).get(channel, channelId);
|
|
591
633
|
const projectPath = activeMain?.project_path || defaultProjectPath;
|
|
592
634
|
// 创建新话题会话
|
|
635
|
+
const inheritedChatType = activeMain?.chat_type || 'private';
|
|
593
636
|
const session = {
|
|
594
637
|
id: `${channel}-${channelId}-${Date.now()}`,
|
|
595
638
|
channel,
|
|
@@ -597,8 +640,8 @@ export class SessionManager {
|
|
|
597
640
|
projectPath,
|
|
598
641
|
threadId,
|
|
599
642
|
agentId: agentId || 'claude',
|
|
600
|
-
chatType:
|
|
601
|
-
sessionMode:
|
|
643
|
+
chatType: inheritedChatType,
|
|
644
|
+
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
602
645
|
metadata,
|
|
603
646
|
name: name || '话题会话',
|
|
604
647
|
createdAt: Date.now(),
|
|
@@ -618,9 +661,11 @@ export class SessionManager {
|
|
|
618
661
|
}
|
|
619
662
|
async switchProject(channel, channelId, newProjectPath, currentAgentId) {
|
|
620
663
|
const agentId = currentAgentId || 'claude';
|
|
621
|
-
// 1.
|
|
664
|
+
// 1. 继承当前 chatType(在 deactivate 之前读取)
|
|
665
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
666
|
+
// 2. 取消当前活跃会话
|
|
622
667
|
this.deactivateAllMetadata(channel, channelId);
|
|
623
|
-
//
|
|
668
|
+
// 3. 查找目标项目 + 当前 agent 的会话
|
|
624
669
|
const target = this.db.prepare(`
|
|
625
670
|
SELECT * FROM sessions
|
|
626
671
|
WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
|
|
@@ -635,7 +680,7 @@ export class SessionManager {
|
|
|
635
680
|
.run(JSON.stringify(metadata), Date.now(), target.id);
|
|
636
681
|
return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
|
|
637
682
|
}
|
|
638
|
-
//
|
|
683
|
+
// 4. 创建新会话
|
|
639
684
|
const session = {
|
|
640
685
|
id: `${channel}-${channelId}-${Date.now()}`,
|
|
641
686
|
channel,
|
|
@@ -643,8 +688,8 @@ export class SessionManager {
|
|
|
643
688
|
projectPath: newProjectPath,
|
|
644
689
|
threadId: '',
|
|
645
690
|
agentId,
|
|
646
|
-
chatType:
|
|
647
|
-
sessionMode:
|
|
691
|
+
chatType: inheritedChatType,
|
|
692
|
+
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
648
693
|
metadata: { isActive: true },
|
|
649
694
|
name: '默认会话',
|
|
650
695
|
createdAt: Date.now(),
|
|
@@ -680,9 +725,11 @@ export class SessionManager {
|
|
|
680
725
|
`).run(agentSessionId, Date.now(), sessionId);
|
|
681
726
|
}
|
|
682
727
|
async switchAgent(channel, channelId, projectPath, newAgentId) {
|
|
683
|
-
// 1.
|
|
728
|
+
// 1. 继承当前 chatType(在 deactivate 之前读取)
|
|
729
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
730
|
+
// 2. 取消当前活跃会话
|
|
684
731
|
this.deactivateAllMetadata(channel, channelId);
|
|
685
|
-
//
|
|
732
|
+
// 3. 查找目标 agent 在当前项目下的会话
|
|
686
733
|
const target = this.db.prepare(`
|
|
687
734
|
SELECT * FROM sessions
|
|
688
735
|
WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
|
|
@@ -697,7 +744,7 @@ export class SessionManager {
|
|
|
697
744
|
.run(JSON.stringify(metadata), Date.now(), target.id);
|
|
698
745
|
return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
|
|
699
746
|
}
|
|
700
|
-
//
|
|
747
|
+
// 4. 创建新会话(与 switchProject 保持一致)
|
|
701
748
|
const session = {
|
|
702
749
|
id: `${channel}-${channelId}-${Date.now()}`,
|
|
703
750
|
channel,
|
|
@@ -705,8 +752,8 @@ export class SessionManager {
|
|
|
705
752
|
projectPath,
|
|
706
753
|
threadId: '',
|
|
707
754
|
agentId: newAgentId,
|
|
708
|
-
chatType:
|
|
709
|
-
sessionMode:
|
|
755
|
+
chatType: inheritedChatType,
|
|
756
|
+
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
710
757
|
metadata: { isActive: true },
|
|
711
758
|
name: '默认会话',
|
|
712
759
|
createdAt: Date.now(),
|
|
@@ -838,6 +885,8 @@ export class SessionManager {
|
|
|
838
885
|
`).run(Date.now(), Date.now(), channelId);
|
|
839
886
|
}
|
|
840
887
|
async createNewSession(channel, channelId, projectPath, name, agentId) {
|
|
888
|
+
// 继承当前 chatType(在 deactivate 之前读取)
|
|
889
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
841
890
|
// 取消当前活跃会话
|
|
842
891
|
this.deactivateAllMetadata(channel, channelId);
|
|
843
892
|
// 创建新会话
|
|
@@ -848,8 +897,8 @@ export class SessionManager {
|
|
|
848
897
|
projectPath,
|
|
849
898
|
threadId: '',
|
|
850
899
|
agentId: agentId || 'claude',
|
|
851
|
-
chatType:
|
|
852
|
-
sessionMode:
|
|
900
|
+
chatType: inheritedChatType,
|
|
901
|
+
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
853
902
|
metadata: { isActive: true },
|
|
854
903
|
name: name || '默认会话',
|
|
855
904
|
createdAt: Date.now(),
|
|
@@ -955,6 +1004,8 @@ export class SessionManager {
|
|
|
955
1004
|
return this.rowToSession(rows[0]);
|
|
956
1005
|
}
|
|
957
1006
|
async importCliSession(channel, channelId, projectPath, agentSessionId, agentId = 'claude') {
|
|
1007
|
+
// 继承当前 chatType(在 deactivate 之前读取)
|
|
1008
|
+
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
958
1009
|
// 取消当前活跃会话
|
|
959
1010
|
this.deactivateAllMetadata(channel, channelId);
|
|
960
1011
|
// 从 CLI 会话文件读取标题
|
|
@@ -968,8 +1019,8 @@ export class SessionManager {
|
|
|
968
1019
|
projectPath,
|
|
969
1020
|
threadId: '',
|
|
970
1021
|
agentId,
|
|
971
|
-
chatType:
|
|
972
|
-
sessionMode:
|
|
1022
|
+
chatType: inheritedChatType,
|
|
1023
|
+
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
973
1024
|
agentSessionId,
|
|
974
1025
|
metadata: { isActive: true },
|
|
975
1026
|
name,
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
|
|
2
2
|
import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
|
|
3
3
|
import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
|
|
4
|
-
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner } from './config.js';
|
|
4
|
+
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getChannelSessionMode } from './config.js';
|
|
5
5
|
import { SessionManager } from './core/session/session-manager.js';
|
|
6
6
|
import { ClaudeAgentPlugin } from './agents/claude-runner.js';
|
|
7
7
|
import { CodexAgentPlugin } from './agents/codex-runner.js';
|
|
@@ -73,6 +73,29 @@ async function main() {
|
|
|
73
73
|
const statsCollector = new StatsCollector(eventBus);
|
|
74
74
|
// 初始化数据库(带 ownerResolver)
|
|
75
75
|
const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => isOwner(config, channel, userId), (channel, userId) => isAdmin(config, channel, userId));
|
|
76
|
+
// sessionMode 解析:通道配置锁定 > chatType 默认(AUN 群聊 → proactive,其余 → interactive)
|
|
77
|
+
sessionManager.setSessionModeResolver((channel, chatType) => {
|
|
78
|
+
const locked = getChannelSessionMode(config, channel);
|
|
79
|
+
if (locked)
|
|
80
|
+
return locked;
|
|
81
|
+
// chatType 默认值:仅 AUN 群聊默认为 proactive,其余通道默认 interactive
|
|
82
|
+
// channel 在多实例时为 instanceName,需要识别 AUN 系
|
|
83
|
+
// 简化:通过 ChannelOptions.channelType 在 MessageProcessor 注册时已知,但 SessionManager 不持有这个映射
|
|
84
|
+
// 这里回退到按 instanceName 反查 config.channels.aun
|
|
85
|
+
if (chatType === 'group') {
|
|
86
|
+
const aun = config.channels?.aun;
|
|
87
|
+
if (Array.isArray(aun)) {
|
|
88
|
+
if (aun.some((i) => i.name === channel))
|
|
89
|
+
return 'proactive';
|
|
90
|
+
}
|
|
91
|
+
else if (aun) {
|
|
92
|
+
const effectiveName = aun.name ?? 'aun';
|
|
93
|
+
if (effectiveName === channel)
|
|
94
|
+
return 'proactive';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
});
|
|
76
99
|
logger.info('✓ Database initialized');
|
|
77
100
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
78
101
|
sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
|
|
@@ -369,7 +392,7 @@ async function main() {
|
|
|
369
392
|
.map(([type, names]) => names.length === 1 ? names[0] : `${type}[${names.join(', ')}]`)
|
|
370
393
|
.join(', ');
|
|
371
394
|
const totalCount = connected.length;
|
|
372
|
-
logger.info(
|
|
395
|
+
logger.info(`🚀 EvolClaw is running with ${totalCount} channel(s): ${channelSummary}`);
|
|
373
396
|
eventBus.publish({
|
|
374
397
|
type: 'system:started',
|
|
375
398
|
channels: connected.map(c => c.toLowerCase()),
|