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.
@@ -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 → readonly
388
+ // 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → auto
383
389
  const role = session.identity?.role;
384
- const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'readonly';
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
- const envParts = [
415
- `会话通道: ${currentChannelType}`,
416
- `当前项目: ${path.basename(absoluteProjectPath)}`,
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
- const currentCanSend = !!channelInfo.adapter.sendFile;
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
- const crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
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
- // 3. 当前通道能力
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
- if (capParts.length > 0) {
468
- contextParts.push(`[通道能力] ${capParts.join('、')}`);
469
- }
470
- // 4. 群聊 @ 规则:告知 agent 应该 @ 谁,由 agent 自行在回复中添加
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(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
462
+ contextParts.push(renderPromptSection('group', { peerId: message.peerId }));
473
463
  }
474
- // 5. Agent ctl 自管理指令提示 + SKILLS.md 生成
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('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
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 (shouldSuppress()) {
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
- logger.info(`[MessageProcessor] Event: type=${event.type}`);
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.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
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.info(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
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.info(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
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' : 'readonly';
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' : 'readonly';
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, getChannelSessionMode } from './config.js';
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 解析:通道配置锁定 > 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;
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
- replyContext: rootId ? { replyToMessageId: rootId, replyInThread: !!threadId } : undefined,
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
+ }