evolclaw 3.0.0 → 3.1.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.
Files changed (99) hide show
  1. package/README.md +1 -1
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +7 -9
  5. package/dist/agents/codex-runner.js +2 -0
  6. package/dist/agents/gemini-runner.js +9 -9
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/aun/aid/identity.js +28 -0
  9. package/dist/aun/aid/index.js +1 -1
  10. package/dist/aun/aid/lifecycle-log.js +33 -0
  11. package/dist/aun/msg/group.js +3 -1
  12. package/dist/aun/msg/p2p.js +4 -1
  13. package/dist/channels/aun.js +353 -125
  14. package/dist/channels/dingtalk.js +2 -1
  15. package/dist/channels/feishu.js +118 -5
  16. package/dist/channels/qqbot.js +2 -1
  17. package/dist/channels/wechat.js +3 -1
  18. package/dist/channels/wecom.js +2 -1
  19. package/dist/cli/bench.js +1219 -0
  20. package/dist/cli/index.js +279 -19
  21. package/dist/cli/link-rules.js +245 -0
  22. package/dist/cli/net-check.js +640 -0
  23. package/dist/cli/watch-msg.js +589 -0
  24. package/dist/config-store.js +37 -5
  25. package/dist/core/channel-loader.js +23 -10
  26. package/dist/core/command-handler.js +46 -22
  27. package/dist/core/evolagent.js +5 -10
  28. package/dist/core/message/im-renderer.js +50 -44
  29. package/dist/core/message/items-formatter.js +11 -4
  30. package/dist/core/message/message-bridge.js +7 -2
  31. package/dist/core/message/message-log.js +2 -0
  32. package/dist/core/message/message-processor.js +150 -99
  33. package/dist/core/message/message-queue.js +10 -3
  34. package/dist/core/permission.js +95 -3
  35. package/dist/core/session/session-manager.js +98 -64
  36. package/dist/core/trigger/scheduler.js +1 -1
  37. package/dist/data/error-dict.json +118 -0
  38. package/dist/eck/baseagent-caps.js +18 -0
  39. package/dist/eck/detect.js +47 -0
  40. package/dist/eck/init.js +77 -0
  41. package/dist/eck/rules-loader.js +28 -0
  42. package/dist/index.js +137 -16
  43. package/dist/net-check.js +640 -0
  44. package/dist/paths.js +31 -40
  45. package/dist/utils/aid-lifecycle-log.js +33 -0
  46. package/dist/utils/atomic-write.js +10 -0
  47. package/dist/utils/cross-platform.js +17 -8
  48. package/dist/utils/error-utils.js +10 -2
  49. package/dist/utils/instance-registry.js +6 -5
  50. package/dist/utils/log-writer.js +2 -1
  51. package/dist/utils/logger.js +10 -0
  52. package/dist/utils/npm-ops.js +35 -3
  53. package/dist/utils/process-introspect.js +16 -38
  54. package/dist/watch-msg.js +26 -11
  55. package/evolclaw-install-aun.md +14 -2
  56. package/kits/docs/GUIDE.md +20 -0
  57. package/kits/docs/INDEX.md +52 -0
  58. package/kits/docs/aun/CHEATSHEET.md +17 -0
  59. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  60. package/kits/docs/channels/feishu.md +27 -0
  61. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  62. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  63. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  64. package/kits/docs/eck_templates/runtime.template.md +19 -0
  65. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  66. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  67. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  68. package/kits/docs/identity/PATH_OPS.md +16 -0
  69. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  70. package/kits/docs/path-registry.md +43 -0
  71. package/kits/eck_manifest.json +95 -0
  72. package/kits/rules/01-overview.md +120 -0
  73. package/kits/rules/02-navigation.md +75 -0
  74. package/kits/rules/03-identity.md +34 -0
  75. package/kits/rules/04-relation.md +49 -0
  76. package/kits/rules/05-venue.md +45 -0
  77. package/kits/rules/06-channel.md +43 -0
  78. package/kits/templates/system-fragments/baseagent.md +2 -0
  79. package/kits/templates/system-fragments/channel.md +10 -0
  80. package/kits/templates/system-fragments/identity.md +12 -0
  81. package/kits/templates/system-fragments/relation.md +9 -0
  82. package/kits/templates/system-fragments/runtime.md +19 -0
  83. package/kits/templates/system-fragments/venue.md +5 -0
  84. package/package.json +7 -5
  85. package/dist/agents/templates.js +0 -122
  86. package/dist/data/prompts.md +0 -137
  87. package/kits/aun/meta.md +0 -25
  88. package/kits/aun/role.md +0 -25
  89. package/kits/templates/group.md +0 -20
  90. package/kits/templates/private.md +0 -9
  91. package/kits/templates/system-fragments/personal-context.md +0 -3
  92. package/kits/templates/system-fragments/self-intro.md +0 -5
  93. package/kits/templates/system-fragments/speaker-intro.md +0 -5
  94. package/kits/templates/system-fragments/venue-intro.md +0 -5
  95. /package/kits/{channels → docs/channels}/aun.md +0 -0
  96. /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
  97. /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
  98. /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
  99. /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
@@ -10,7 +10,8 @@ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError,
10
10
  import { summarizeToolInput } from '../permission.js';
11
11
  import { DEFAULT_PERMISSION_MODE } from '../../types.js';
12
12
  import { getPackageRoot, resolveRoot } from '../../paths.js';
13
- import { renderPromptSection } from '../../agents/templates.js';
13
+ import { renderKitSections } from '../../agents/kit-renderer.js';
14
+ import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
15
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
16
  /**
16
17
  * 构造 OutboundEnvelope —— 出站三件套的信封部分。
@@ -124,7 +125,8 @@ export class MessageProcessor {
124
125
  const agent = this.agentRegistry.resolveByChannel(channelName);
125
126
  if (!agent)
126
127
  return null;
127
- const globalCm = this.agentRegistry?.resolveByChannel(channelName)?.config?.chatmode;
128
+ // chatmode 解析优先级:agent.config.chatmode > globalSettings.chatmode
129
+ const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
128
130
  return agent.getContext(channelName, chatType, globalCm);
129
131
  }
130
132
  /**
@@ -182,7 +184,7 @@ export class MessageProcessor {
182
184
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
183
185
  '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
184
186
  '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
185
- '/aid', '/agentmd',
187
+ '/aid', '/agentmd', '/upgrade',
186
188
  ];
187
189
  /** 判断消息内容是否为已知命令 */
188
190
  isKnownCommand(content) {
@@ -345,6 +347,8 @@ export class MessageProcessor {
345
347
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
346
348
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
347
349
  const chatmode = session.sessionMode ?? 'interactive';
350
+ // 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
351
+ logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}`);
348
352
  // 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
349
353
  const taskReplyContext = () => {
350
354
  const base = this.getReplyContext(message);
@@ -416,6 +420,10 @@ export class MessageProcessor {
416
420
  send: async (payload) => {
417
421
  if (isAutonomous)
418
422
  return; // autonomous session: never send to channel
423
+ // proactive 模式:activity.batch 是 thought 协议内容,只发给支持 thought 的 channel
424
+ // (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
425
+ if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
426
+ return;
419
427
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
420
428
  if (isCurrentlyBackground)
421
429
  return;
@@ -431,7 +439,7 @@ export class MessageProcessor {
431
439
  }
432
440
  }
433
441
  if (payload.kind === 'result.text' && payload.isFinal) {
434
- opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
442
+ opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
435
443
  }
436
444
  opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
437
445
  const enrichedEnvelope = { ...envelope, replyContext: opts };
@@ -488,32 +496,15 @@ export class MessageProcessor {
488
496
  // 动态构建运行时上下文提示
489
497
  const contextParts = [];
490
498
  const currentChannelType = options?.channelType || message.channel;
491
- // 1. 构建模板变量并渲染 runtime 段
492
- const peerName = message.peerName || session.metadata?.peerName;
493
- const peerType = message.peerType;
494
- const peerId = message.peerId;
499
+ // 提取 self 信息
495
500
  const adapterAny = channelInfo.adapter;
496
501
  const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
497
502
  const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
498
- const formatIdentity = (name, id) => {
499
- if (name && id)
500
- return `${name} (${id})`;
501
- return name || id || undefined;
502
- };
503
- const selfIdentity = formatIdentity(selfName, selfAid);
504
- const peerIdentity = formatIdentity(peerName, peerId);
505
- // 文件发送能力(按 channelType 去重)
506
- let crossChannelTypes = [];
503
+ const peerName = message.peerName || session.metadata?.peerName;
504
+ // 文件发送能力
507
505
  let currentCanSend = false;
508
506
  if (!isProactive) {
509
- const fileChannelTypes = new Set();
510
507
  currentCanSend = !!(channelInfo.adapter.capabilities?.file);
511
- for (const [, info] of this.channels) {
512
- if (info.adapter.capabilities?.file) {
513
- fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
514
- }
515
- }
516
- crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
517
508
  }
518
509
  // 通道能力
519
510
  const capParts = [];
@@ -523,49 +514,53 @@ export class MessageProcessor {
523
514
  capParts.push('图片输出');
524
515
  if (channelInfo.adapter.capabilities?.file)
525
516
  capParts.push('文件发送');
526
- // Personal layer: persona.md + working memory 注入
517
+ // Personal layer
527
518
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
528
- if (owningAgent) {
529
- const persona = owningAgent.getPersona?.();
530
- if (persona)
531
- contextParts.push(persona);
532
- const working = owningAgent.getWorkingMemory?.();
533
- if (working)
534
- contextParts.push(`[当前关注]\n${working}`);
535
- }
536
- contextParts.push(renderPromptSection('runtime', {
537
- channel: currentChannelType,
538
- project: path.basename(absoluteProjectPath),
539
- sessionName: session.name || '',
540
- selfIdentity: selfIdentity || '',
541
- peerRole: session.identity?.role || 'unknown',
542
- peerIdentity: peerIdentity || '',
543
- peerType: peerType && peerType !== 'unknown' ? peerType : '',
544
- chatType: session.chatType || '',
545
- agent: session.agentId && session.agentId !== 'claude' ? session.agentId : '',
546
- readonly: session.metadata?.permissionMode === 'readonly',
547
- readonlySendHint: isProactive ? '使用 evolclaw ctl file 发送' : '使用 [SEND_FILE:] 发送',
548
- fileSendCurrent: !isProactive && currentCanSend,
549
- fileSendCross: !isProactive && crossChannelTypes.length > 0,
550
- crossPrimary: crossChannelTypes[0] || '',
551
- crossTypes: crossChannelTypes.join('/'),
552
- capability: capParts.length > 0,
553
- capabilities: capParts.join(''),
554
- }));
555
- // 2. 群聊 @ 规则
556
- if (message.chatType === 'group' && message.peerId) {
557
- contextParts.push(renderPromptSection('group', { peerId: message.peerId }));
558
- }
559
- // 3. Proactive 模式提示词
560
- if (isProactive) {
561
- contextParts.push(renderPromptSection('proactive', {}));
562
- }
563
- // 4. 触发器功能提示词(非触发器消息时注入,让 AI 知道可以使用触发器)
564
- if (message.source !== 'trigger') {
565
- const triggerSection = renderPromptSection('trigger', {});
566
- if (triggerSection)
567
- contextParts.push(triggerSection);
568
- }
519
+ const persona = owningAgent?.getPersona?.() || undefined;
520
+ const working = owningAgent?.getWorkingMemory?.() || undefined;
521
+ if (persona)
522
+ contextParts.push(persona);
523
+ if (working)
524
+ contextParts.push(`[当前关注]\n${working}`);
525
+ // 计算 peerKey: <channel>#<urlEncode(peerId)>
526
+ const peerIdRaw = message.peerId;
527
+ const peerKey = (currentChannelType && peerIdRaw)
528
+ ? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
529
+ : undefined;
530
+ const normalizedBaseagent = normalizeBaseagent(agent.name);
531
+ // Kit renderer: 组装上下文
532
+ const kitCtx = {
533
+ vars: {
534
+ EVOLCLAW_HOME: resolveRoot(),
535
+ PACKAGE_ROOT: getPackageRoot(),
536
+ CURRENT_PROJECT: absoluteProjectPath,
537
+ selfAid: selfAid || undefined,
538
+ selfName: selfName || undefined,
539
+ hasPersona: !!persona,
540
+ hasWorkingMemory: !!working,
541
+ peerId: peerIdRaw || undefined,
542
+ peerKey,
543
+ peerName: peerName || undefined,
544
+ peerRole: session.identity?.role || 'unknown',
545
+ groupId: session.metadata?.groupId || undefined,
546
+ scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
547
+ chatType: session.chatType || null,
548
+ channel: currentChannelType || null,
549
+ venueUid: undefined,
550
+ project: path.basename(absoluteProjectPath),
551
+ sessionName: session.name || undefined,
552
+ sessionMode: isProactive ? 'proactive' : 'interactive',
553
+ readonly: session.metadata?.permissionMode === 'readonly',
554
+ canSendFile: !isProactive && currentCanSend,
555
+ capabilities: capParts.length > 0 ? capParts.join('') : undefined,
556
+ baseAgent: normalizedBaseagent.canonical,
557
+ baseAgentName: normalizedBaseagent.displayName,
558
+ },
559
+ sessionId: session.id,
560
+ };
561
+ const kitContext = renderKitSections(kitCtx);
562
+ if (kitContext)
563
+ contextParts.push(kitContext);
569
564
  effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
570
565
  // 可重试错误(403/429/5xx)指数退避重试,最多 3 次
571
566
  const MAX_RETRIES = 3;
@@ -616,6 +611,35 @@ export class MessageProcessor {
616
611
  throw error;
617
612
  }
618
613
  }
614
+ // prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
615
+ // 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
616
+ const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
617
+ const errorsText = streamResult.errors?.join(' ') || '';
618
+ const isPromptTooLong = streamResult.isError && session.agentSessionId && hasCompact(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
619
+ contextTooLongPattern.test(streamResult.lastReplyText) ||
620
+ contextTooLongPattern.test(errorsText) ||
621
+ contextTooLongPattern.test(streamResult.fullText));
622
+ if (isPromptTooLong) {
623
+ renderer.addNotice('⚠️ 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
624
+ await renderer.flush();
625
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
626
+ if (compacted) {
627
+ renderer.addNotice('✅ 压缩完成,正在重试...', 'info', 'compact-retry', true);
628
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
629
+ agent.registerStream(streamKey, retryStream);
630
+ streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
631
+ }
632
+ else {
633
+ throw new Error('CONTEXT_COMPACT_FAILED');
634
+ }
635
+ }
636
+ else if (streamResult.isError && !isPromptTooLong && (streamResult.terminalReason === 'prompt_too_long' ||
637
+ contextTooLongPattern.test(streamResult.lastReplyText) ||
638
+ contextTooLongPattern.test(errorsText) ||
639
+ contextTooLongPattern.test(streamResult.fullText))) {
640
+ // 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
641
+ renderer.addNotice('⚠️ 上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
642
+ }
619
643
  // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
620
644
  // 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
621
645
  // suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
@@ -698,43 +722,35 @@ export class MessageProcessor {
698
722
  }
699
723
  }
700
724
  } // end of !isProactive
701
- // 最终回复文本添加到 renderer(统一在流结束后处理,避免多 complete 事件重复发送)
702
- // suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
703
- // 非 suppressed 且无流式文本:同上
704
- // 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
705
- // 但如果 renderer 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
725
+ // 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
706
726
  const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
707
- // 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
708
- // 特征:无流式 text + complete.result 匹配已知模式
709
- // 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
710
- // Proactive 模式下 renderer silent,需要兜底发出以告知用户,否则用户完全无反馈。
711
- const isSdkFallbackMessage = !!finalReplyText
712
- && !streamResult.hasReceivedText
713
- && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
714
727
  if (finalReplyText) {
715
- if (isProactive && isSdkFallbackMessage) {
716
- // Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent renderer
728
+ if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
729
+ // Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
717
730
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
718
731
  if (!isCurrentlyBackground) {
719
732
  await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
720
733
  logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
721
734
  }
722
735
  }
723
- else if (shouldSuppress()) {
724
- renderer.addText(finalReplyText);
725
- }
726
- else if (!streamResult.hasReceivedText || (!renderer.hasSentContent() && !renderer.hasContent())) {
736
+ else if (shouldSuppress() || !streamResult.hasReceivedText) {
727
737
  renderer.addText(finalReplyText);
728
738
  }
729
739
  }
730
- // Flush 剩余内容(文件标记已在 flush 时自动移除)
731
- await renderer.flush(true);
732
- // 清理 activeStreams(正常完成)
740
+ // 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
733
741
  agent.cleanupStream(streamKey);
734
742
  logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
735
- // 清除处理中状态
736
743
  this.sessionManager.clearProcessing(session.id);
737
744
  logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
745
+ // 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
746
+ const preFlushInterrupt = this.interruptedSessions.get(session.id);
747
+ if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
748
+ logger.info(`[MessageProcessor] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
749
+ }
750
+ else {
751
+ // Flush 剩余内容(文件标记已在 flush 时自动移除)
752
+ await renderer.flush(true);
753
+ }
738
754
  // 更新 EvolAgent.lastActivity
739
755
  if (this.agentRegistry) {
740
756
  const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
@@ -788,7 +804,7 @@ export class MessageProcessor {
788
804
  adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
789
805
  }
790
806
  else {
791
- adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs } }).catch(() => { });
807
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
792
808
  }
793
809
  }
794
810
  if (message.triggerMeta) {
@@ -837,6 +853,8 @@ export class MessageProcessor {
837
853
  agent: session.agentId || null,
838
854
  model: agent.getModel?.() || null,
839
855
  durationMs: Date.now() - startTime,
856
+ numTurns: streamResult.numTurns,
857
+ usage: streamResult.usage,
840
858
  }));
841
859
  }
842
860
  }
@@ -961,7 +979,14 @@ export class MessageProcessor {
961
979
  : path.resolve(process.cwd(), session.projectPath);
962
980
  return { session, absoluteProjectPath };
963
981
  }
964
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId);
982
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
983
+ // 兜底纠正:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
984
+ // 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
985
+ if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
986
+ logger.info(`[MessageProcessor] proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive (peerType=${message.peerType})`);
987
+ session.sessionMode = 'proactive';
988
+ await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
989
+ }
965
990
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
966
991
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
967
992
  ? session.projectPath
@@ -982,6 +1007,8 @@ export class MessageProcessor {
982
1007
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
983
1008
  // 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
984
1009
  let lastReplyText = '';
1010
+ // callId → description 映射,用于 tool_result 回显描述
1011
+ const toolDescByCallId = new Map();
985
1012
  try {
986
1013
  for await (const event of stream) {
987
1014
  // 每收到事件重置空闲超时
@@ -1073,7 +1100,11 @@ export class MessageProcessor {
1073
1100
  }
1074
1101
  // 工具调用
1075
1102
  if (event.type === 'tool_use') {
1076
- // 工具调用意味着当前文本是中间轮,重置最后回复追踪
1103
+ // 工具调用意味着当前 turn 结束,flush 已累积的文本作为独立消息
1104
+ if (renderer.hasTextPending()) {
1105
+ await renderer.flushText();
1106
+ }
1107
+ // 重置最后回复追踪
1077
1108
  lastReplyText = '';
1078
1109
  this.eventBus.publish({
1079
1110
  type: 'tool:use',
@@ -1084,6 +1115,9 @@ export class MessageProcessor {
1084
1115
  });
1085
1116
  if (!shouldSuppress()) {
1086
1117
  const desc = summarizeToolInput(event.name, event.input || {});
1118
+ if (event.callId) {
1119
+ toolDescByCallId.set(event.callId, desc);
1120
+ }
1087
1121
  renderer.addToolCall(event.name, event.input, event.callId, desc);
1088
1122
  }
1089
1123
  }
@@ -1098,21 +1132,27 @@ export class MessageProcessor {
1098
1132
  agentName: agentNameForStats,
1099
1133
  timestamp: Date.now()
1100
1134
  });
1135
+ // 从 tool_use 阶段缓存的描述中回溯
1136
+ const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
1101
1137
  if (event.isError && !shouldSuppress()) {
1102
1138
  hasErrorResult = true;
1103
1139
  let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
1104
1140
  // 移除 XML 风格的错误标签
1105
1141
  errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
1106
- renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId);
1142
+ renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, undefined, cachedDesc);
1107
1143
  }
1108
1144
  else if (!event.isError && !shouldSuppress()) {
1109
- renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId);
1145
+ renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, undefined, cachedDesc);
1110
1146
  }
1111
1147
  }
1112
1148
  // 运行时错误(Codex: turn.failed / item error)
1113
1149
  if (event.type === 'error') {
1114
1150
  logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
1115
- if (!hasErrorResult && !shouldSuppress()) {
1151
+ // 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
1152
+ lastReplyText += event.error || '';
1153
+ // 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
1154
+ const isContextError = /prompt is too long|input is too long|上下文过长/i.test(event.error || '');
1155
+ if (!isContextError && !hasErrorResult && !shouldSuppress()) {
1116
1156
  hasErrorResult = true;
1117
1157
  renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
1118
1158
  }
@@ -1121,19 +1161,23 @@ export class MessageProcessor {
1121
1161
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
1122
1162
  // 仅记录状态,最终 flush(true) 在流结束后统一执行
1123
1163
  if (event.type === 'complete') {
1124
- logger.debug(`[MessageProcessor] complete event: hasReceivedText=${hasReceivedText}, isError=${event.isError}, shouldSuppress=${shouldSuppress()}`);
1164
+ logger.info(`[MessageProcessor] complete event: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
1125
1165
  // 自动回填会话名称
1126
1166
  if (event.sessionTitle && session.name === '默认会话') {
1127
1167
  await this.sessionManager.renameSession(session.id, event.sessionTitle);
1128
1168
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1129
1169
  }
1130
1170
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1131
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
1171
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1132
1172
  // 失败且无前置错误输出:显示 errors 摘要
1133
1173
  // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
1174
+ // 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
1134
1175
  const interruptReason = this.interruptedSessions.get(session.id);
1135
1176
  const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
1136
- if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt) {
1177
+ const isContextTooLong = event.terminalReason === 'prompt_too_long'
1178
+ || /prompt is too long|input is too long|上下文过长/i.test(event.errors?.join(' ') || '')
1179
+ || /prompt is too long|input is too long|上下文过长/i.test(lastReplyText);
1180
+ if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
1137
1181
  const errorSummary = event.errors?.join('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
1138
1182
  // 使用 terminalReason 提供更友好的错误提示
1139
1183
  const userFriendlyMessage = event.terminalReason
@@ -1165,7 +1209,7 @@ export class MessageProcessor {
1165
1209
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1166
1210
  }
1167
1211
  // 记录完成状态
1168
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
1212
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1169
1213
  if (event.subtype === 'success') {
1170
1214
  this.messageCache.addEvent(session.id, {
1171
1215
  type: 'completed',
@@ -1212,9 +1256,16 @@ export class MessageProcessor {
1212
1256
  catch (error) {
1213
1257
  // User interrupt (AbortError) is expected, log at info level
1214
1258
  const catchInterruptReason = this.interruptedSessions.get(session.id);
1215
- const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop';
1259
+ const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop' || catchInterruptReason === 'recalled';
1216
1260
  if (error instanceof Error && error.name === 'AbortError') {
1217
1261
  logger.info('[MessageProcessor] Stream interrupted (AbortError)');
1262
+ // User-initiated interrupt: skip flush — new task takes over the channel,
1263
+ // flushing here would send a spurious "最终回复" before the new task's output
1264
+ if (catchIsUserInterrupt) {
1265
+ completeResult.isError = false;
1266
+ completeResult.hasReceivedText = hasReceivedText;
1267
+ return completeResult;
1268
+ }
1218
1269
  }
1219
1270
  else if (catchIsUserInterrupt) {
1220
1271
  // SDK telemetry noise after user-initiated interrupt — not a real error
@@ -80,7 +80,8 @@ export class MessageQueue {
80
80
  }
81
81
  const queueKey = this.getQueueKey(sessionKey, projectPath);
82
82
  const agentName = options?.agentName || DEFAULT_AGENT_NAME;
83
- logger.debug(`[Queue] Enqueuing message for ${queueKey} (agent=${agentName})`);
83
+ const isProcessing = this.processing.has(queueKey);
84
+ logger.info(`[Queue] enqueue: key=${queueKey} processing=${isProcessing} queueLen=${this.queues.get(queueKey)?.length ?? 0} agent=${agentName}`);
84
85
  return new Promise((resolve, reject) => {
85
86
  if (!this.queues.has(queueKey)) {
86
87
  this.queues.set(queueKey, []);
@@ -104,6 +105,12 @@ export class MessageQueue {
104
105
  else {
105
106
  // 群聊:FIFO,不打断
106
107
  logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
108
+ this.eventBus?.publish({
109
+ type: 'task:queued',
110
+ channel: message.channel,
111
+ channelId: message.channelId,
112
+ replyContext: message.replyContext,
113
+ });
107
114
  }
108
115
  }
109
116
  else {
@@ -114,7 +121,7 @@ export class MessageQueue {
114
121
  }
115
122
  async processNext(queueKey) {
116
123
  this.processing.add(queueKey);
117
- logger.debug(`[Queue] Processing queue ${queueKey}`);
124
+ logger.info(`[Queue] processNext: start key=${queueKey}`);
118
125
  while (true) {
119
126
  // 等待外部锁释放(/compact, /clear 等快速命令)
120
127
  const lock = this.getExternalLock(queueKey);
@@ -124,7 +131,7 @@ export class MessageQueue {
124
131
  }
125
132
  const queue = this.queues.get(queueKey);
126
133
  if (!queue || queue.length === 0) {
127
- logger.debug(`[Queue] Queue ${queueKey} is empty, stopping`);
134
+ logger.info(`[Queue] processNext: queue empty, releasing key=${queueKey}`);
128
135
  this.processing.delete(queueKey);
129
136
  this.processingAgent.delete(queueKey);
130
137
  this.currentSessionKey = undefined;
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import fs from 'fs';
2
3
  import { renderActionAsText } from './interaction-router.js';
3
4
  import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
4
5
  // 危险命令黑名单(正则表达式)
@@ -89,9 +90,15 @@ export function summarizeToolInput(toolName, input) {
89
90
  return '';
90
91
  const extractors = {
91
92
  'Read': (i) => i.file_path,
92
- 'Edit': (i) => i.file_path,
93
+ 'Edit': (i) => formatEditSummary(i),
93
94
  'Write': (i) => i.file_path,
94
- 'Bash': (i) => i.command?.substring(0, 80),
95
+ 'Bash': (i) => {
96
+ const cmd = i.command?.substring(0, 80) || '';
97
+ const desc = i.description;
98
+ if (desc && cmd)
99
+ return `${cmd} | ${desc}`;
100
+ return cmd || desc;
101
+ },
95
102
  'Grep': (i) => `pattern: ${i.pattern}`,
96
103
  'Glob': (i) => `pattern: ${i.pattern}`,
97
104
  'Agent': (i) => i.description || i.prompt?.substring(0, 80),
@@ -110,6 +117,8 @@ export function summarizeToolInput(toolName, input) {
110
117
  },
111
118
  'TaskCreate': (i) => i.subject || i.description?.substring(0, 80),
112
119
  'TaskUpdate': (i) => i.status ? `${i.taskId} → ${i.status}` : i.taskId,
120
+ 'TaskOutput': (i) => `${i.task_id || '?'}${i.block === false ? ' (non-blocking)' : ''}${i.timeout ? ` timeout=${i.timeout}ms` : ''}`,
121
+ 'TaskStop': (i) => i.task_id || i.shell_id || '?',
113
122
  'NotebookEdit': (i) => i.notebook_path,
114
123
  'WebFetch': (i) => i.url,
115
124
  'WebSearch': (i) => i.query?.substring(0, 80),
@@ -131,6 +140,81 @@ export function summarizeToolInput(toolName, input) {
131
140
  || input.url
132
141
  || '';
133
142
  }
143
+ /** 为 Edit 工具生成 diff 风格摘要 */
144
+ function formatEditSummary(input) {
145
+ const filePath = input.file_path || '';
146
+ const oldStr = typeof input.old_string === 'string' ? input.old_string : '';
147
+ const newStr = typeof input.new_string === 'string' ? input.new_string : '';
148
+ if (!oldStr && !newStr)
149
+ return filePath;
150
+ const MAX_DIFF_LINES = 14;
151
+ const oldLines = oldStr.split('\n');
152
+ const newLines = newStr.split('\n');
153
+ // 尝试从文件中定位 old_string 的起始行号
154
+ let startLine = 0; // 0-based; 0 means unknown
155
+ if (filePath && oldStr) {
156
+ try {
157
+ const content = fs.readFileSync(filePath, 'utf-8');
158
+ const idx = content.indexOf(oldStr);
159
+ if (idx >= 0) {
160
+ startLine = content.slice(0, idx).split('\n').length; // 1-based
161
+ }
162
+ }
163
+ catch {
164
+ // 文件不可读,行号留空
165
+ }
166
+ }
167
+ const diffLines = [];
168
+ // 找公共前缀行数
169
+ let prefixLen = 0;
170
+ while (prefixLen < oldLines.length && prefixLen < newLines.length && oldLines[prefixLen] === newLines[prefixLen]) {
171
+ prefixLen++;
172
+ }
173
+ // 找公共后缀行数
174
+ let suffixLen = 0;
175
+ while (suffixLen < oldLines.length - prefixLen &&
176
+ suffixLen < newLines.length - prefixLen &&
177
+ oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
178
+ suffixLen++;
179
+ }
180
+ const CONTEXT = 2;
181
+ // 计算行号宽度(用于对齐)
182
+ const maxLineNo = startLine > 0 ? startLine + oldLines.length - 1 : 0;
183
+ const newMaxLineNo = startLine > 0 ? startLine + prefixLen + (newLines.length - suffixLen - prefixLen) - 1 : 0;
184
+ const padWidth = startLine > 0 ? Math.max(maxLineNo, newMaxLineNo).toString().length : 0;
185
+ // 格式化一行:行号 + 标记 + 内容
186
+ // 使用 Unicode 符号避免飞书 Markdown 将 "- " 解析为列表
187
+ const fmtLine = (lineNo, marker, text) => {
188
+ if (startLine > 0) {
189
+ return `${lineNo.toString().padStart(padWidth)} ${marker} ${text}`;
190
+ }
191
+ return `${marker} ${text}`;
192
+ };
193
+ // 上下文前缀(最多 CONTEXT 行)
194
+ const ctxStart = Math.max(0, prefixLen - CONTEXT);
195
+ for (let i = ctxStart; i < prefixLen; i++) {
196
+ diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
197
+ }
198
+ // 删除行
199
+ const removedEnd = oldLines.length - suffixLen;
200
+ for (let i = prefixLen; i < removedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
201
+ diffLines.push(fmtLine(startLine + i, '−', oldLines[i]));
202
+ }
203
+ // 新增行(行号从 prefixLen 位置开始递增)
204
+ const addedEnd = newLines.length - suffixLen;
205
+ for (let i = prefixLen; i < addedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
206
+ diffLines.push(fmtLine(startLine + i, '+', newLines[i]));
207
+ }
208
+ // 上下文后缀(最多 CONTEXT 行)
209
+ const ctxEnd = Math.min(oldLines.length, removedEnd + CONTEXT);
210
+ for (let i = removedEnd; i < ctxEnd && diffLines.length < MAX_DIFF_LINES + 2; i++) {
211
+ diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
212
+ }
213
+ if (diffLines.length > MAX_DIFF_LINES + 2) {
214
+ diffLines.splice(MAX_DIFF_LINES, diffLines.length, ' ...');
215
+ }
216
+ return `${filePath}\n\`\`\`\n${diffLines.join('\n')}\n\`\`\``;
217
+ }
134
218
  export class PermissionGateway {
135
219
  pending = new Map();
136
220
  timeout = 5 * 60 * 1000;
@@ -222,7 +306,15 @@ export class PermissionGateway {
222
306
  await sendPrompt(renderActionAsText(interaction));
223
307
  }
224
308
  return new Promise((resolve) => {
225
- this.pending.set(requestId, { sessionId, toolName, resolve, timer: setTimeout(() => { }, 0) });
309
+ const timer = setTimeout(() => {
310
+ const pending = this.pending.get(requestId);
311
+ if (!pending)
312
+ return;
313
+ this.pending.delete(requestId);
314
+ this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId, toolName });
315
+ pending.resolve('deny');
316
+ }, this.timeout);
317
+ this.pending.set(requestId, { sessionId, toolName, resolve, timer });
226
318
  // 注册到 InteractionRouter(卡片和文本降级都注册,统一路由)
227
319
  if (context?.interactionRouter) {
228
320
  context.interactionRouter.register(requestId, sessionId, (action) => {