evolclaw 2.6.3 → 2.7.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 +1 -1
- package/data/evolclaw.sample.json +3 -4
- package/dist/agents/claude-runner.js +15 -6
- package/dist/agents/codex-runner.js +0 -1
- package/dist/agents/gemini-runner.js +3 -1
- package/dist/channels/aun.js +99 -28
- package/dist/channels/feishu.js +2 -0
- package/dist/cli.js +29 -1
- package/dist/config.js +66 -40
- package/dist/core/command-handler.js +51 -41
- package/dist/core/message/message-processor.js +93 -72
- package/dist/core/message/thought-emitter.js +1 -0
- package/dist/core/session/session-manager.js +23 -2
- package/dist/index.js +24 -25
- package/dist/prompts/templates.js +122 -0
- package/dist/templates/prompts.md +103 -0
- package/dist/utils/channel-fingerprint.js +59 -0
- package/dist/utils/init.js +1 -1
- package/dist/utils/logger.js +15 -3
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError,
|
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
11
|
import { getOwner } from '../../config.js';
|
|
12
12
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
|
+
import { renderPromptSection } from '../../prompts/templates.js';
|
|
13
14
|
/**
|
|
14
15
|
* 统一消息处理器
|
|
15
16
|
* 负责处理来自不同渠道的消息,协调事件流处理
|
|
@@ -129,7 +130,8 @@ export class MessageProcessor {
|
|
|
129
130
|
'/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart',
|
|
130
131
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
131
132
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
132
|
-
'/p ', '/s ', '/name ',
|
|
133
|
+
'/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
134
|
+
'/aid', '/agentmd',
|
|
133
135
|
];
|
|
134
136
|
/** 判断消息内容是否为已知命令 */
|
|
135
137
|
isKnownCommand(content) {
|
|
@@ -192,6 +194,7 @@ export class MessageProcessor {
|
|
|
192
194
|
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
193
195
|
});
|
|
194
196
|
}
|
|
197
|
+
logger.info(`[MessageProcessor] agent.interrupt invoked (idle-kill) stream=${streamKey}`);
|
|
195
198
|
agent.interrupt(streamKey).catch(e => {
|
|
196
199
|
logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
|
|
197
200
|
});
|
|
@@ -313,7 +316,7 @@ export class MessageProcessor {
|
|
|
313
316
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
314
317
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
315
318
|
logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
|
|
316
|
-
logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
319
|
+
logger.info(`[MessageProcessor] session=${session.id} task=${taskId} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
317
320
|
// 记录开始处理
|
|
318
321
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
319
322
|
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
|
|
@@ -353,6 +356,9 @@ export class MessageProcessor {
|
|
|
353
356
|
}, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
|
|
354
357
|
// 保存当前 flusher,用于 compact 事件
|
|
355
358
|
this.currentFlusher = flusher;
|
|
359
|
+
if (isProactive) {
|
|
360
|
+
logger.info(`[MessageProcessor] proactive mode: flusher silent, outputs via thought.put task=${taskId}`);
|
|
361
|
+
}
|
|
356
362
|
// Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
|
|
357
363
|
// selector: context = { type: 'task', id: taskId }
|
|
358
364
|
if (isProactive && adapter.putThought) {
|
|
@@ -379,12 +385,13 @@ export class MessageProcessor {
|
|
|
379
385
|
? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
|
|
380
386
|
: undefined,
|
|
381
387
|
});
|
|
382
|
-
// 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest →
|
|
388
|
+
// 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → auto)
|
|
383
389
|
const role = session.identity?.role;
|
|
384
|
-
const defaultPermMode = role === 'owner' ? 'bypass' :
|
|
390
|
+
const defaultPermMode = role === 'owner' ? 'bypass' : 'auto';
|
|
385
391
|
agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
|
|
386
392
|
// 标记会话为处理中(实时持久化,重启后可恢复)
|
|
387
393
|
this.sessionManager.markProcessing(session.id);
|
|
394
|
+
logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
|
|
388
395
|
// 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
|
|
389
396
|
const prevInterruptReason = this.interruptedSessions.get(session.id);
|
|
390
397
|
this.interruptedSessions.delete(session.id);
|
|
@@ -396,8 +403,7 @@ export class MessageProcessor {
|
|
|
396
403
|
// 动态构建运行时上下文提示
|
|
397
404
|
const contextParts = [];
|
|
398
405
|
const currentChannelType = options?.channelType || message.channel;
|
|
399
|
-
// 1.
|
|
400
|
-
const peerLabel = session.identity?.role || 'unknown';
|
|
406
|
+
// 1. 构建模板变量并渲染 runtime 段
|
|
401
407
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
402
408
|
const peerType = message.peerType;
|
|
403
409
|
const peerId = message.peerId;
|
|
@@ -411,52 +417,20 @@ export class MessageProcessor {
|
|
|
411
417
|
};
|
|
412
418
|
const selfIdentity = formatIdentity(selfName, selfAid);
|
|
413
419
|
const peerIdentity = formatIdentity(peerName, peerId);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
];
|
|
418
|
-
if (session.name)
|
|
419
|
-
envParts.push(`会话名称: ${session.name}`);
|
|
420
|
-
if (selfIdentity)
|
|
421
|
-
envParts.push(`当前名称: ${selfIdentity}`);
|
|
422
|
-
envParts.push(`对端身份: ${peerLabel}`);
|
|
423
|
-
if (peerIdentity)
|
|
424
|
-
envParts.push(`对端名称: ${peerIdentity}`);
|
|
425
|
-
if (peerType && peerType !== 'unknown')
|
|
426
|
-
envParts.push(`对端类型: ${peerType}`);
|
|
427
|
-
if (session.chatType)
|
|
428
|
-
envParts.push(`聊天类型: ${session.chatType}`);
|
|
429
|
-
if (session.agentId && session.agentId !== 'claude')
|
|
430
|
-
envParts.push(`当前Agent: ${session.agentId}`);
|
|
431
|
-
contextParts.push(`[当前环境] ${envParts.join(' | ')}`);
|
|
432
|
-
// 只读模式提示
|
|
433
|
-
if (session.metadata?.permissionMode === 'readonly') {
|
|
434
|
-
const sendHint = isProactive
|
|
435
|
-
? '使用 evolclaw ctl file 发送'
|
|
436
|
-
: '使用 [SEND_FILE:] 发送';
|
|
437
|
-
contextParts.push(`[只读模式] 禁止修改项目文件。如需生成文件供用户下载,请写入 .evolclaw/tmp/ 目录后${sendHint}`);
|
|
438
|
-
}
|
|
439
|
-
// 2. 文件发送能力(按 channelType 去重,提示词只展示第一级通道名)
|
|
440
|
-
// proactive 模式:不推送 [SEND_FILE:] 提示,统一通过 evolclaw ctl file 显式发送(与 ctl send 契约一致)
|
|
420
|
+
// 文件发送能力(按 channelType 去重)
|
|
421
|
+
let crossChannelTypes = [];
|
|
422
|
+
let currentCanSend = false;
|
|
441
423
|
if (!isProactive) {
|
|
442
424
|
const fileChannelTypes = new Set();
|
|
443
|
-
|
|
425
|
+
currentCanSend = !!channelInfo.adapter.sendFile;
|
|
444
426
|
for (const [, info] of this.channels) {
|
|
445
427
|
if (info.adapter.sendFile) {
|
|
446
428
|
fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
|
|
447
429
|
}
|
|
448
430
|
}
|
|
449
|
-
|
|
450
|
-
if (currentCanSend || crossChannelTypes.length > 0) {
|
|
451
|
-
const hints = [];
|
|
452
|
-
if (currentCanSend)
|
|
453
|
-
hints.push(`[SEND_FILE:路径] 发送文件到当前通道`);
|
|
454
|
-
if (crossChannelTypes.length > 0)
|
|
455
|
-
hints.push(`[SEND_FILE:${crossChannelTypes[0]}:路径] 发送文件到指定通道(可用: ${crossChannelTypes.join('/')})`);
|
|
456
|
-
contextParts.push(hints.join(','));
|
|
457
|
-
}
|
|
431
|
+
crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
|
|
458
432
|
}
|
|
459
|
-
//
|
|
433
|
+
// 通道能力
|
|
460
434
|
const capParts = [];
|
|
461
435
|
if (options?.supportsImages)
|
|
462
436
|
capParts.push('图片输入');
|
|
@@ -464,30 +438,32 @@ export class MessageProcessor {
|
|
|
464
438
|
capParts.push('图片输出');
|
|
465
439
|
if (channelInfo.adapter.sendFile)
|
|
466
440
|
capParts.push('文件发送');
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
441
|
+
contextParts.push(renderPromptSection('runtime', {
|
|
442
|
+
channel: currentChannelType,
|
|
443
|
+
project: path.basename(absoluteProjectPath),
|
|
444
|
+
sessionName: session.name || '',
|
|
445
|
+
selfIdentity: selfIdentity || '',
|
|
446
|
+
peerRole: session.identity?.role || 'unknown',
|
|
447
|
+
peerIdentity: peerIdentity || '',
|
|
448
|
+
peerType: peerType && peerType !== 'unknown' ? peerType : '',
|
|
449
|
+
chatType: session.chatType || '',
|
|
450
|
+
agent: session.agentId && session.agentId !== 'claude' ? session.agentId : '',
|
|
451
|
+
readonly: session.metadata?.permissionMode === 'readonly',
|
|
452
|
+
readonlySendHint: isProactive ? '使用 evolclaw ctl file 发送' : '使用 [SEND_FILE:] 发送',
|
|
453
|
+
fileSendCurrent: !isProactive && currentCanSend,
|
|
454
|
+
fileSendCross: !isProactive && crossChannelTypes.length > 0,
|
|
455
|
+
crossPrimary: crossChannelTypes[0] || '',
|
|
456
|
+
crossTypes: crossChannelTypes.join('/'),
|
|
457
|
+
capability: capParts.length > 0,
|
|
458
|
+
capabilities: capParts.join('、'),
|
|
459
|
+
}));
|
|
460
|
+
// 2. 群聊 @ 规则
|
|
471
461
|
if (message.chatType === 'group' && message.peerId) {
|
|
472
|
-
contextParts.push(
|
|
462
|
+
contextParts.push(renderPromptSection('group', { peerId: message.peerId }));
|
|
473
463
|
}
|
|
474
|
-
//
|
|
475
|
-
// 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
|
|
476
|
-
// 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
|
|
477
|
-
// if (!this.skillsEnsured) {
|
|
478
|
-
// this.ensureSkillsFile();
|
|
479
|
-
// this.skillsEnsured = true;
|
|
480
|
-
// }
|
|
481
|
-
// const skillsHint = this.getSkillsHint();
|
|
482
|
-
// if (skillsHint) {
|
|
483
|
-
// contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
|
|
484
|
-
// }
|
|
485
|
-
// 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
|
|
464
|
+
// 3. Proactive 模式提示词
|
|
486
465
|
if (isProactive) {
|
|
487
|
-
contextParts.push('
|
|
488
|
-
'- 发送文本:evolclaw ctl send "<消息内容>"\n' +
|
|
489
|
-
'- 发送文件:evolclaw ctl file <路径>\n' +
|
|
490
|
-
'可多次调用。如不调用,用户将看不到任何回复。');
|
|
466
|
+
contextParts.push(renderPromptSection('proactive', {}));
|
|
491
467
|
}
|
|
492
468
|
const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
493
469
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
@@ -495,6 +471,7 @@ export class MessageProcessor {
|
|
|
495
471
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
496
472
|
let streamRegistered = false;
|
|
497
473
|
try {
|
|
474
|
+
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
498
475
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
499
476
|
agent.registerStream(streamKey, stream);
|
|
500
477
|
streamRegistered = true;
|
|
@@ -621,8 +598,23 @@ export class MessageProcessor {
|
|
|
621
598
|
// 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
|
|
622
599
|
// 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
|
|
623
600
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
601
|
+
// 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
|
|
602
|
+
// 特征:无流式 text + complete.result 匹配已知模式
|
|
603
|
+
// 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
|
|
604
|
+
// Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
|
|
605
|
+
const isSdkFallbackMessage = !!finalReplyText
|
|
606
|
+
&& !streamResult.hasReceivedText
|
|
607
|
+
&& /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
|
|
624
608
|
if (finalReplyText) {
|
|
625
|
-
if (
|
|
609
|
+
if (isProactive && isSdkFallbackMessage) {
|
|
610
|
+
// Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent flusher
|
|
611
|
+
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
612
|
+
if (!isCurrentlyBackground) {
|
|
613
|
+
await adapter.sendText(message.channelId, finalReplyText, capturedReplyContext);
|
|
614
|
+
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (shouldSuppress()) {
|
|
626
618
|
flusher.addText(finalReplyText);
|
|
627
619
|
}
|
|
628
620
|
else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
|
|
@@ -633,8 +625,10 @@ export class MessageProcessor {
|
|
|
633
625
|
await flusher.flush(true);
|
|
634
626
|
// 清理 activeStreams(正常完成)
|
|
635
627
|
agent.cleanupStream(streamKey);
|
|
628
|
+
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
636
629
|
// 清除处理中状态
|
|
637
630
|
this.sessionManager.clearProcessing(session.id);
|
|
631
|
+
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
638
632
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
639
633
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
640
634
|
if (streamResult.isError) {
|
|
@@ -708,8 +702,10 @@ export class MessageProcessor {
|
|
|
708
702
|
catch (error) {
|
|
709
703
|
// 清理流和处理中状态(异常时也要清除)
|
|
710
704
|
agent.cleanupStream(streamKey);
|
|
705
|
+
logger.info(`[MessageProcessor] agent.cleanupStream ok (on error): session=${session.id} task=${taskId}`);
|
|
711
706
|
try {
|
|
712
707
|
this.sessionManager.clearProcessing(session.id);
|
|
708
|
+
logger.info(`[MessageProcessor] session ${session.id} processing cleared (on error) task=${taskId}`);
|
|
713
709
|
}
|
|
714
710
|
catch { }
|
|
715
711
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
@@ -823,26 +819,52 @@ export class MessageProcessor {
|
|
|
823
819
|
// 每收到事件重置空闲超时
|
|
824
820
|
const toolName = event.type === 'tool_use' ? event.name : undefined;
|
|
825
821
|
resetTimer(event.type, toolName);
|
|
826
|
-
//
|
|
827
|
-
|
|
822
|
+
// 记录事件类型:高价值事件(text/tool_use/tool_result/complete/error/compact/task_progress)INFO,
|
|
823
|
+
// 框架事件(session_id/state_changed/status)DEBUG
|
|
824
|
+
let eventDetail = '';
|
|
825
|
+
if (event.type === 'text' && event.text) {
|
|
826
|
+
const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
|
|
827
|
+
eventDetail = ` text="${preview}${event.text.length > 80 ? '…' : ''}"`;
|
|
828
|
+
}
|
|
829
|
+
else if (event.type === 'tool_use') {
|
|
830
|
+
const input = event.input;
|
|
831
|
+
const desc = input?.description
|
|
832
|
+
|| input?.file_path
|
|
833
|
+
|| input?.pattern
|
|
834
|
+
|| (typeof input?.command === 'string' ? input.command.slice(0, 80) : '')
|
|
835
|
+
|| (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
|
|
836
|
+
|| (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
|
|
837
|
+
|| '';
|
|
838
|
+
eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
|
|
839
|
+
}
|
|
840
|
+
else if (event.type === 'tool_result') {
|
|
841
|
+
eventDetail = ` tool=${event.name} ok=${!event.isError}`;
|
|
842
|
+
}
|
|
843
|
+
const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
|
|
844
|
+
if (frameworkEvents.has(event.type)) {
|
|
845
|
+
logger.debug(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
849
|
+
}
|
|
828
850
|
// Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
|
|
829
851
|
if (thoughtEmitter) {
|
|
830
852
|
thoughtEmitter.emit(event).catch(() => { });
|
|
831
853
|
}
|
|
832
854
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
833
855
|
if (event.type === 'session_id') {
|
|
834
|
-
logger.
|
|
856
|
+
logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
835
857
|
continue;
|
|
836
858
|
}
|
|
837
859
|
// session 状态变更(idle/running/requires_action)
|
|
838
860
|
if (event.type === 'state_changed') {
|
|
839
|
-
logger.
|
|
861
|
+
logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
|
|
840
862
|
this.eventBus.publish({ type: 'agent:state-changed', sessionId: session.id, state: event.state });
|
|
841
863
|
continue;
|
|
842
864
|
}
|
|
843
865
|
// agent 状态通知(仅事件,不直出给用户)
|
|
844
866
|
if (event.type === 'status') {
|
|
845
|
-
logger.
|
|
867
|
+
logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
|
|
846
868
|
this.eventBus.publish({
|
|
847
869
|
type: 'agent:status',
|
|
848
870
|
sessionId: session.id,
|
|
@@ -901,7 +923,6 @@ export class MessageProcessor {
|
|
|
901
923
|
}
|
|
902
924
|
// 工具结果
|
|
903
925
|
if (event.type === 'tool_result') {
|
|
904
|
-
logger.debug(`[MessageProcessor] tool_result: name=${event.name}, is_error=${event.isError}`);
|
|
905
926
|
this.eventBus.publish({
|
|
906
927
|
type: 'tool:result',
|
|
907
928
|
sessionId: session.id,
|
|
@@ -23,6 +23,7 @@ export class ThoughtEmitter {
|
|
|
23
23
|
this.channelId = channelId;
|
|
24
24
|
this.taskId = taskId;
|
|
25
25
|
this.chatmode = chatmode;
|
|
26
|
+
logger.info(`[ThoughtEmitter] created channel=${channelId} task=${taskId} chatmode=${chatmode}`);
|
|
26
27
|
}
|
|
27
28
|
async emit(event) {
|
|
28
29
|
// 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
|
|
@@ -315,6 +315,26 @@ export class SessionManager {
|
|
|
315
315
|
logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
+
// Migration: readonly 模式已禁用,历史会话统一转为 auto
|
|
319
|
+
if (hasMetadata && tableInfo.length > 0) {
|
|
320
|
+
const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
|
|
321
|
+
let migratedPerm = 0;
|
|
322
|
+
for (const row of rows) {
|
|
323
|
+
try {
|
|
324
|
+
const meta = JSON.parse(row.metadata);
|
|
325
|
+
if (meta.permissionMode === 'readonly' || meta.permissionMode === 'noask') {
|
|
326
|
+
meta.permissionMode = 'auto';
|
|
327
|
+
this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
|
|
328
|
+
.run(JSON.stringify(meta), row.id);
|
|
329
|
+
migratedPerm++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch { /* skip malformed JSON */ }
|
|
333
|
+
}
|
|
334
|
+
if (migratedPerm > 0) {
|
|
335
|
+
logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode → auto`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
318
338
|
// 创建新表(首次初始化)
|
|
319
339
|
this.db.exec(`
|
|
320
340
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -451,7 +471,7 @@ export class SessionManager {
|
|
|
451
471
|
session.identity = this.resolveIdentity(channel, userId);
|
|
452
472
|
// 新话题会话补写默认权限模式
|
|
453
473
|
if (session.metadata && !session.metadata.permissionMode) {
|
|
454
|
-
session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : '
|
|
474
|
+
session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'auto';
|
|
455
475
|
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
456
476
|
.run(JSON.stringify(session.metadata), Date.now(), session.id);
|
|
457
477
|
}
|
|
@@ -562,7 +582,7 @@ export class SessionManager {
|
|
|
562
582
|
session.identity = this.resolveIdentity(channel, userId);
|
|
563
583
|
// 写入默认权限模式(基于角色,只在首次创建时设置)
|
|
564
584
|
if (!sessionMetadata.permissionMode) {
|
|
565
|
-
sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : '
|
|
585
|
+
sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'auto';
|
|
566
586
|
}
|
|
567
587
|
this.insertSession(session);
|
|
568
588
|
this.eventBus.publish({
|
|
@@ -661,6 +681,7 @@ export class SessionManager {
|
|
|
661
681
|
}
|
|
662
682
|
async switchProject(channel, channelId, newProjectPath, currentAgentId) {
|
|
663
683
|
const agentId = currentAgentId || 'claude';
|
|
684
|
+
logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
|
|
664
685
|
// 1. 继承当前 chatType(在 deactivate 之前读取)
|
|
665
686
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
666
687
|
// 2. 取消当前活跃会话
|
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,
|
|
4
|
+
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode } 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';
|
|
@@ -24,7 +24,9 @@ import { InteractionRouter } from './core/interaction-router.js';
|
|
|
24
24
|
import { ChannelLoader } from './core/channel-loader.js';
|
|
25
25
|
import { AgentLoader } from './core/agent-loader.js';
|
|
26
26
|
import { IpcServer } from './ipc.js';
|
|
27
|
-
import { logger } from './utils/logger.js';
|
|
27
|
+
import { logger, setLogLevel } from './utils/logger.js';
|
|
28
|
+
import { detectDuplicates } from './utils/channel-fingerprint.js';
|
|
29
|
+
import { loadPromptTemplates } from './prompts/templates.js';
|
|
28
30
|
import path from 'path';
|
|
29
31
|
import fs from 'fs';
|
|
30
32
|
async function main() {
|
|
@@ -48,8 +50,14 @@ async function main() {
|
|
|
48
50
|
logger.info('EvolClaw starting...');
|
|
49
51
|
// 确保数据目录存在
|
|
50
52
|
ensureDataDirs();
|
|
53
|
+
// 加载提示词模板
|
|
54
|
+
loadPromptTemplates();
|
|
51
55
|
// 加载配置
|
|
52
56
|
const config = loadConfig();
|
|
57
|
+
// 应用配置中的日志级别(优先于环境变量)
|
|
58
|
+
if (config.debug?.logLevel) {
|
|
59
|
+
setLogLevel(config.debug.logLevel);
|
|
60
|
+
}
|
|
53
61
|
const paths = resolvePaths();
|
|
54
62
|
// 配置完整性校验
|
|
55
63
|
const integrity = validateConfigIntegrity(config);
|
|
@@ -63,6 +71,14 @@ async function main() {
|
|
|
63
71
|
logger.info('✓ Config loaded (API keys hidden)');
|
|
64
72
|
// Channel instance name uniqueness check
|
|
65
73
|
validateChannelInstanceNames(config);
|
|
74
|
+
// Detect duplicate channel credentials
|
|
75
|
+
const duplicates = detectDuplicates(config);
|
|
76
|
+
if (duplicates.length > 0) {
|
|
77
|
+
for (const d of duplicates) {
|
|
78
|
+
logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} is used by instances [${d.instances.join(', ')}]. ` +
|
|
79
|
+
`Only the first instance will be active.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
66
82
|
if (anthropic.baseUrl) {
|
|
67
83
|
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
68
84
|
}
|
|
@@ -73,28 +89,9 @@ async function main() {
|
|
|
73
89
|
const statsCollector = new StatsCollector(eventBus);
|
|
74
90
|
// 初始化数据库(带 ownerResolver)
|
|
75
91
|
const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => isOwner(config, channel, userId), (channel, userId) => isAdmin(config, channel, userId));
|
|
76
|
-
// sessionMode
|
|
77
|
-
sessionManager.setSessionModeResolver((
|
|
78
|
-
|
|
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;
|
|
92
|
+
// sessionMode 解析:全局 chatmode 配置 > 默认 'interactive'
|
|
93
|
+
sessionManager.setSessionModeResolver((_channel, chatType) => {
|
|
94
|
+
return getDefaultSessionMode(config, chatType);
|
|
98
95
|
});
|
|
99
96
|
logger.info('✓ Database initialized');
|
|
100
97
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
@@ -240,7 +237,9 @@ async function main() {
|
|
|
240
237
|
await handler({
|
|
241
238
|
channel: channelType, channelId: chatId, content, images, chatType,
|
|
242
239
|
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
243
|
-
|
|
240
|
+
// 只在话题场景(threadId 有值)才设置 replyContext;
|
|
241
|
+
// 纯引用回复(rootId 有值但无 threadId)不设置,避免所有回复都带引用头
|
|
242
|
+
replyContext: threadId ? { replyToMessageId: rootId, replyInThread: true } : undefined,
|
|
244
243
|
});
|
|
245
244
|
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
|
|
246
245
|
replyToMessageId: replyContext?.replyToMessageId,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getPackageRoot, resolveRoot } from '../paths.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const KNOWN_SECTIONS = new Set(['runtime', 'group', 'proactive']);
|
|
6
|
+
const SECTION_RE = /^##\s+(\w+)\s*$/;
|
|
7
|
+
let sections = null;
|
|
8
|
+
let builtinSections = null;
|
|
9
|
+
function parseTemplate(content) {
|
|
10
|
+
const result = new Map();
|
|
11
|
+
let currentSection = null;
|
|
12
|
+
let currentLines = [];
|
|
13
|
+
for (const line of content.split('\n')) {
|
|
14
|
+
// Stop parsing at horizontal rule separator (documentation follows)
|
|
15
|
+
if (/^---\s*$/.test(line)) {
|
|
16
|
+
if (currentSection) {
|
|
17
|
+
result.set(currentSection, currentLines.join('\n').trim());
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
const m = line.match(SECTION_RE);
|
|
22
|
+
if (m) {
|
|
23
|
+
if (currentSection) {
|
|
24
|
+
result.set(currentSection, currentLines.join('\n').trim());
|
|
25
|
+
}
|
|
26
|
+
const name = m[1];
|
|
27
|
+
if (KNOWN_SECTIONS.has(name)) {
|
|
28
|
+
currentSection = name;
|
|
29
|
+
currentLines = [];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
currentSection = null;
|
|
33
|
+
currentLines = [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (currentSection) {
|
|
37
|
+
currentLines.push(line);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (currentSection) {
|
|
41
|
+
result.set(currentSection, currentLines.join('\n').trim());
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function loadBuiltinTemplate() {
|
|
46
|
+
const builtinPath = path.join(getPackageRoot(), 'dist', 'templates', 'prompts.md');
|
|
47
|
+
const srcPath = path.join(getPackageRoot(), 'src', 'templates', 'prompts.md');
|
|
48
|
+
const filePath = fs.existsSync(builtinPath) ? builtinPath : srcPath;
|
|
49
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
+
return parseTemplate(content);
|
|
51
|
+
}
|
|
52
|
+
export function loadPromptTemplates() {
|
|
53
|
+
builtinSections = loadBuiltinTemplate();
|
|
54
|
+
const userPath = path.join(resolveRoot(), 'data', 'prompts.md');
|
|
55
|
+
if (fs.existsSync(userPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(userPath, 'utf-8');
|
|
58
|
+
const parsed = parseTemplate(content);
|
|
59
|
+
sections = new Map(builtinSections);
|
|
60
|
+
for (const [key, value] of parsed) {
|
|
61
|
+
sections.set(key, value);
|
|
62
|
+
}
|
|
63
|
+
logger.info(`[PromptTemplates] Loaded user override: ${userPath}`);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger.warn(`[PromptTemplates] Failed to load user override (${userPath}), using builtin:`, err);
|
|
67
|
+
sections = builtinSections;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
sections = builtinSections;
|
|
72
|
+
logger.info(`[PromptTemplates] Using builtin templates`);
|
|
73
|
+
}
|
|
74
|
+
for (const name of KNOWN_SECTIONS) {
|
|
75
|
+
if (!sections.has(name)) {
|
|
76
|
+
logger.warn(`[PromptTemplates] Section "${name}" missing, using builtin fallback`);
|
|
77
|
+
const fallback = builtinSections.get(name);
|
|
78
|
+
if (fallback)
|
|
79
|
+
sections.set(name, fallback);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isTruthy(val) {
|
|
84
|
+
if (val === undefined || val === null || val === false || val === '' || val === 0)
|
|
85
|
+
return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function renderTemplate(template, vars) {
|
|
89
|
+
// Pass 1: conditional sections {{?key}}...{{/}}
|
|
90
|
+
let result = template.replace(/\{\{\?(\w+)\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, body) => {
|
|
91
|
+
return isTruthy(vars[key]) ? body : '';
|
|
92
|
+
});
|
|
93
|
+
// Pass 2: variable substitution {{key}}
|
|
94
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
95
|
+
const val = vars[key];
|
|
96
|
+
if (!isTruthy(val))
|
|
97
|
+
return '';
|
|
98
|
+
return String(val);
|
|
99
|
+
});
|
|
100
|
+
// Pass 3: remove blank lines
|
|
101
|
+
return result.split('\n').filter(line => line.trim() !== '').join('\n');
|
|
102
|
+
}
|
|
103
|
+
export function renderPromptSection(section, vars) {
|
|
104
|
+
if (!sections)
|
|
105
|
+
loadPromptTemplates();
|
|
106
|
+
const template = sections.get(section);
|
|
107
|
+
if (!template) {
|
|
108
|
+
logger.warn(`[PromptTemplates] Section "${section}" not found`);
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
return renderTemplate(template, vars);
|
|
112
|
+
}
|
|
113
|
+
/** Reset loaded templates (for testing) */
|
|
114
|
+
export function _resetTemplates() {
|
|
115
|
+
sections = null;
|
|
116
|
+
builtinSections = null;
|
|
117
|
+
}
|
|
118
|
+
/** Load templates from a raw string (for testing) */
|
|
119
|
+
export function _loadFromString(content) {
|
|
120
|
+
builtinSections = parseTemplate(content);
|
|
121
|
+
sections = builtinSections;
|
|
122
|
+
}
|