evolclaw 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/assets/.env.template +4 -0
  3. package/assets/config.json.template +6 -0
  4. package/assets/wechat-group-qr.jpeg +0 -0
  5. package/dist/agents/claude-runner.js +348 -156
  6. package/dist/agents/kit-renderer.js +211 -42
  7. package/dist/aun/aid/agentmd.js +75 -139
  8. package/dist/aun/aid/client.js +1 -14
  9. package/dist/aun/aid/identity.js +381 -54
  10. package/dist/aun/aid/index.js +3 -2
  11. package/dist/aun/aid/store.js +74 -0
  12. package/dist/aun/msg/p2p.js +26 -2
  13. package/dist/aun/rpc/connection.js +23 -35
  14. package/dist/channels/aun.js +92 -144
  15. package/dist/channels/dingtalk.js +1 -0
  16. package/dist/channels/feishu.js +270 -190
  17. package/dist/channels/qqbot.js +1 -0
  18. package/dist/channels/wechat.js +1 -0
  19. package/dist/channels/wecom.js +1 -0
  20. package/dist/cli/agent.js +26 -27
  21. package/dist/cli/bench.js +45 -34
  22. package/dist/cli/help.js +23 -0
  23. package/dist/cli/index.js +538 -77
  24. package/dist/cli/init-channel.js +7 -4
  25. package/dist/cli/link-rules.js +2 -1
  26. package/dist/cli/model.js +324 -0
  27. package/dist/cli/net-check.js +138 -56
  28. package/dist/cli/watch-msg.js +7 -7
  29. package/dist/cli/watch-web/debug-log.js +18 -0
  30. package/dist/cli/watch-web/server.js +306 -0
  31. package/dist/cli/watch-web/sources/aid.js +63 -0
  32. package/dist/cli/watch-web/sources/msg.js +70 -0
  33. package/dist/cli/watch-web/sources/session.js +638 -0
  34. package/dist/cli/watch-web/sources/types.js +10 -0
  35. package/dist/cli/watch-web/static/app.js +546 -0
  36. package/dist/cli/watch-web/static/index.html +54 -0
  37. package/dist/cli/watch-web/static/style.css +247 -0
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +87 -93
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -4
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/message-bridge.js +6 -6
  44. package/dist/core/message/message-log.js +2 -2
  45. package/dist/core/message/message-processor.js +104 -118
  46. package/dist/core/message/stream-idle-monitor.js +21 -0
  47. package/dist/core/model/model-catalog.js +215 -0
  48. package/dist/core/model/model-scope.js +250 -0
  49. package/dist/core/relation/peer-identity.js +78 -44
  50. package/dist/core/relation/peer-key.js +16 -0
  51. package/dist/core/session/session-fs-store.js +34 -55
  52. package/dist/core/session/session-key.js +24 -0
  53. package/dist/core/session/session-manager.js +312 -251
  54. package/dist/core/session/session-mapper.js +9 -4
  55. package/dist/core/trigger/manager.js +37 -0
  56. package/dist/core/trigger/scheduler.js +2 -1
  57. package/dist/index.js +10 -3
  58. package/dist/ipc.js +22 -0
  59. package/dist/paths.js +87 -16
  60. package/dist/utils/npm-ops.js +18 -11
  61. package/kits/docs/GUIDE.md +2 -2
  62. package/kits/docs/INDEX.md +11 -7
  63. package/kits/docs/channels/aun.md +56 -17
  64. package/kits/docs/channels/feishu.md +41 -12
  65. package/kits/docs/context-assembly.md +181 -0
  66. package/kits/docs/evolclaw/agent.md +49 -0
  67. package/kits/docs/evolclaw/aid.md +49 -0
  68. package/kits/docs/evolclaw/ctl.md +46 -0
  69. package/kits/docs/evolclaw/group.md +82 -0
  70. package/kits/docs/evolclaw/msg.md +86 -0
  71. package/kits/docs/evolclaw/rpc.md +35 -0
  72. package/kits/docs/evolclaw/storage.md +49 -0
  73. package/kits/docs/venues/aun-group.md +10 -0
  74. package/kits/docs/venues/aun-private.md +10 -0
  75. package/kits/docs/venues/client-desktop.md +10 -0
  76. package/kits/docs/venues/client-mobile.md +10 -0
  77. package/kits/docs/venues/feishu-group.md +13 -0
  78. package/kits/docs/venues/feishu-private.md +9 -0
  79. package/kits/docs/venues/group.md +11 -0
  80. package/kits/docs/venues/private.md +10 -0
  81. package/kits/eck_manifest.json +75 -39
  82. package/kits/rules/01-overview.md +20 -10
  83. package/kits/rules/05-venue.md +2 -2
  84. package/kits/rules/06-channel.md +30 -27
  85. package/kits/templates/system-fragments/baseagent.md +7 -1
  86. package/kits/templates/system-fragments/channel.md +4 -1
  87. package/kits/templates/system-fragments/identity.md +4 -4
  88. package/kits/templates/system-fragments/relation.md +8 -5
  89. package/kits/templates/system-fragments/session.md +27 -0
  90. package/kits/templates/system-fragments/venue.md +13 -1
  91. package/package.json +13 -6
  92. package/dist/aun/aid/lifecycle-log.js +0 -33
  93. package/dist/net-check.js +0 -640
  94. package/dist/utils/aid-lifecycle-log.js +0 -33
  95. package/dist/watch-msg.js +0 -544
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
  100. package/kits/templates/system-fragments/eckruntime.md +0 -14
@@ -12,6 +12,8 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
12
12
  import { renderKitSections } from '../../agents/kit-renderer.js';
13
13
  import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
14
14
  import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
15
+ import { formatPeerKey } from '../relation/peer-key.js';
16
+ import { resolveEffectiveModel } from '../model/model-scope.js';
15
17
  function getContextTooLongHint(agent) {
16
18
  if (canCompactAgent(agent)) {
17
19
  return '上下文过长,请精简提问或使用 /compact 压缩上下文';
@@ -69,7 +71,8 @@ export class MessageProcessor {
69
71
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
70
72
  interactionRouter;
71
73
  messageQueue;
72
- skillsEnsured = false; // 全局 SKILLS.md 是否已确保
74
+ /** sessionId 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
75
+ activeMonitors = new Map();
73
76
  /**
74
77
  * Get the runner for a given (channel, baseagent) pair.
75
78
  *
@@ -95,10 +98,11 @@ export class MessageProcessor {
95
98
  return [...this.agentMap.keys()];
96
99
  }
97
100
  /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
98
- async isBackgroundSession(session, channel, channelId) {
101
+ isBackgroundSession(session, _channel, _channelId) {
99
102
  if (session.threadId)
100
103
  return false;
101
- const active = await this.sessionManager.getActiveSession(channel, channelId);
104
+ // 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
105
+ const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
102
106
  return active ? session.id !== active.id : false;
103
107
  }
104
108
  constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
@@ -125,6 +129,16 @@ export class MessageProcessor {
125
129
  }
126
130
  setInteractionRouter(router) {
127
131
  this.interactionRouter = router;
132
+ // 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
133
+ // 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
134
+ router.setWaitHooks({
135
+ onWaitStart: (sessionId) => {
136
+ this.activeMonitors.get(sessionId)?.pause();
137
+ },
138
+ onWaitEnd: (sessionId) => {
139
+ this.activeMonitors.get(sessionId)?.resume();
140
+ },
141
+ });
128
142
  }
129
143
  setMessageQueue(queue) {
130
144
  this.messageQueue = queue;
@@ -210,10 +224,10 @@ export class MessageProcessor {
210
224
  */
211
225
  async processMessage(message) {
212
226
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
213
- // 先解析会话,再优先用 session.metadata.channelName 精确定位实例级 adapter
227
+ // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
214
228
  // message.channel 现在存实例名(channelName),可直接用于精确路由
215
229
  const { session, absoluteProjectPath } = await this.resolveSession(message);
216
- const channelKey = session.metadata?.channelName || message.channel;
230
+ const channelKey = session.metadata?.channelKey || message.channel;
217
231
  const channelInfo = this.resolveChannelInfo(channelKey);
218
232
  if (!channelInfo) {
219
233
  logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
@@ -245,12 +259,13 @@ export class MessageProcessor {
245
259
  monitor?.recordEvent(eventType || 'unknown', toolName);
246
260
  };
247
261
  // Cache background status to avoid async call inside setInterval
248
- const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
262
+ const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
249
263
  const timeoutPromise = new Promise((_, reject) => {
250
264
  rejectFn = reject;
251
265
  if (!monitorEnabled)
252
266
  return;
253
267
  monitor = new StreamIdleMonitor(idleMs);
268
+ this.activeMonitors.set(streamKey, monitor);
254
269
  monitorInterval = setInterval(() => {
255
270
  // Drain all pending levels in one tick
256
271
  let result = monitor.check();
@@ -331,6 +346,7 @@ export class MessageProcessor {
331
346
  finally {
332
347
  if (monitorInterval)
333
348
  clearInterval(monitorInterval);
349
+ this.activeMonitors.delete(streamKey);
334
350
  }
335
351
  }
336
352
  /** 获取回复上下文(跟着任务走) */
@@ -340,7 +356,7 @@ export class MessageProcessor {
340
356
  /** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
341
357
  async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
342
358
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
343
- const channelKey = session.metadata?.channelName || message.channel;
359
+ const channelKey = session.metadata?.channelKey || message.channel;
344
360
  const channelInfo = this.resolveChannelInfo(channelKey);
345
361
  // Per-method agent name for stats bucketing (agent.name or '<unknown>')
346
362
  const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
@@ -382,7 +398,7 @@ export class MessageProcessor {
382
398
  replyContext: taskReplyContext(),
383
399
  });
384
400
  try {
385
- const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
401
+ const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
386
402
  // 记录收到消息
387
403
  logger.message({
388
404
  msgId: messageId,
@@ -439,7 +455,7 @@ export class MessageProcessor {
439
455
  // (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
440
456
  if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
441
457
  return;
442
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
458
+ const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
443
459
  if (isCurrentlyBackground)
444
460
  return;
445
461
  const opts = {};
@@ -447,7 +463,7 @@ export class MessageProcessor {
447
463
  if (baseReplyCtx) {
448
464
  Object.assign(opts, baseReplyCtx);
449
465
  }
450
- else if (firstReply && message.messageId) {
466
+ else if (firstReply && message.messageId && message.source !== 'trigger') {
451
467
  if (payload.kind === 'result.text' && payload.text) {
452
468
  opts.replyToMessageId = message.messageId;
453
469
  firstReply = false;
@@ -507,6 +523,7 @@ export class MessageProcessor {
507
523
  : message.content;
508
524
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
509
525
  let effectiveSystemPrompt;
526
+ let modelOverride;
510
527
  try {
511
528
  // 动态构建运行时上下文提示
512
529
  const contextParts = [];
@@ -516,18 +533,13 @@ export class MessageProcessor {
516
533
  const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
517
534
  const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
518
535
  const peerName = message.peerName || session.metadata?.peerName;
519
- // 文件发送能力
520
- let currentCanSend = false;
521
- if (!isProactive) {
522
- currentCanSend = !!(channelInfo.adapter.capabilities?.file);
523
- }
524
536
  // 通道能力
525
537
  const capParts = [];
526
538
  if (options?.supportsImages)
527
539
  capParts.push('图片输入');
528
540
  if (channelInfo.adapter.capabilities?.image)
529
541
  capParts.push('图片输出');
530
- if (channelInfo.adapter.capabilities?.file)
542
+ if (!isProactive && channelInfo.adapter.capabilities?.file)
531
543
  capParts.push('文件发送');
532
544
  // Personal layer
533
545
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
@@ -537,18 +549,41 @@ export class MessageProcessor {
537
549
  contextParts.push(persona);
538
550
  if (working)
539
551
  contextParts.push(`[当前关注]\n${working}`);
540
- // 计算 peerKey: <channel>#<urlEncode(peerId)>
552
+ // 计算 peerKey: <channelType>#<urlEncode(peerId)>
541
553
  const peerIdRaw = message.peerId;
542
554
  const peerKey = (currentChannelType && peerIdRaw)
543
- ? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
555
+ ? formatPeerKey(currentChannelType, peerIdRaw)
544
556
  : undefined;
557
+ // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
558
+ // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
559
+ // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
560
+ try {
561
+ const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
562
+ if (resolved.model)
563
+ modelOverride = { model: resolved.model, effort: resolved.effort };
564
+ }
565
+ catch (e) {
566
+ logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
567
+ }
545
568
  const normalizedBaseagent = normalizeBaseagent(agent.name);
569
+ const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
546
570
  // Kit renderer: 组装上下文
571
+ const pkgRoot = getPackageRoot();
547
572
  const kitCtx = {
548
573
  vars: {
549
574
  EVOLCLAW_HOME: resolveRoot(),
550
- PACKAGE_ROOT: getPackageRoot(),
575
+ PACKAGE_ROOT: pkgRoot,
551
576
  CURRENT_PROJECT: absoluteProjectPath,
577
+ // ECK 派生路径(manifest 引用时需要展开)
578
+ KITS: path.join(pkgRoot, 'kits'),
579
+ KITS_RULES: path.join(pkgRoot, 'kits', 'rules'),
580
+ KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
581
+ KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
582
+ KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
583
+ // 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
584
+ PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
585
+ RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
586
+ VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
552
587
  selfAid: selfAid || undefined,
553
588
  selfName: selfName || undefined,
554
589
  hasPersona: !!persona,
@@ -556,20 +591,30 @@ export class MessageProcessor {
556
591
  peerId: peerIdRaw || undefined,
557
592
  peerKey,
558
593
  peerName: peerName || undefined,
559
- peerRole: session.identity?.role || 'unknown',
594
+ peerRole: session.identity?.role || 'anonymous',
595
+ peerType: message.peerType || undefined,
560
596
  groupId: session.metadata?.groupId || undefined,
561
- scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
562
597
  chatType: session.chatType || null,
563
598
  channel: currentChannelType || null,
564
599
  venueUid: undefined,
600
+ // 群分发模式 / 客户端类型 / 权限模式
601
+ dispatch: session.metadata?.dispatchMode || undefined,
602
+ clientType: message.clientType || undefined,
603
+ permissionMode: session.metadata?.permissionMode || 'auto',
604
+ capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
565
605
  project: path.basename(absoluteProjectPath),
606
+ sessionId: session.id,
566
607
  sessionName: session.name || undefined,
567
- chatmode: isProactive ? 'proactive' : 'interactive',
608
+ sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
609
+ threadId: session.threadId || undefined,
610
+ // Stage 3: sessionKey 持久化字段
611
+ sessionKey: session.sessionKey,
612
+ chatMode: isProactive ? 'proactive' : 'interactive',
568
613
  readonly: session.metadata?.permissionMode === 'readonly',
569
- canSendFile: !isProactive && currentCanSend,
570
- capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
571
614
  baseAgent: normalizedBaseagent.canonical,
572
615
  baseAgentName: normalizedBaseagent.displayName,
616
+ baseAgentModel: agentModel || undefined,
617
+ agentSessionId: session.agentSessionId || undefined,
573
618
  },
574
619
  sessionId: session.id,
575
620
  };
@@ -583,7 +628,7 @@ export class MessageProcessor {
583
628
  let streamRegistered = false;
584
629
  try {
585
630
  logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
586
- const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
631
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
587
632
  agent.registerStream(streamKey, stream);
588
633
  streamRegistered = true;
589
634
  streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
@@ -612,9 +657,11 @@ export class MessageProcessor {
612
657
  await renderer.flush();
613
658
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
614
659
  if (compacted) {
615
- // compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
660
+ // compact 成功,清除第一次流中混入的错误文本,再重试
661
+ const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
662
+ renderer.stripContextError(ctxErrPattern);
616
663
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
617
- const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
664
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
618
665
  agent.registerStream(streamKey, retryStream);
619
666
  streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
620
667
  }
@@ -635,12 +682,13 @@ export class MessageProcessor {
635
682
  contextTooLongPattern.test(errorsText) ||
636
683
  contextTooLongPattern.test(streamResult.fullText));
637
684
  if (isPromptTooLong) {
685
+ renderer.stripContextError(contextTooLongPattern);
638
686
  renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
639
687
  await renderer.flush();
640
688
  const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
641
689
  if (compacted) {
642
690
  renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
643
- const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
691
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
644
692
  agent.registerStream(streamKey, retryStream);
645
693
  streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
646
694
  // 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
@@ -752,7 +800,7 @@ export class MessageProcessor {
752
800
  if (finalReplyText) {
753
801
  if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
754
802
  // Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
755
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
803
+ const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
756
804
  if (!isCurrentlyBackground) {
757
805
  await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
758
806
  logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
@@ -829,7 +877,7 @@ export class MessageProcessor {
829
877
  adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
830
878
  }
831
879
  else {
832
- adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
880
+ adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
833
881
  }
834
882
  }
835
883
  if (message.triggerMeta) {
@@ -868,7 +916,7 @@ export class MessageProcessor {
868
916
  // 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
869
917
  // 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
870
918
  }
871
- const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
919
+ const isFinallyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
872
920
  if (isFinallyBackground && session.sessionMode !== 'autonomous') {
873
921
  const projectName = path.basename(session.projectPath);
874
922
  const count = this.messageCache.getCount(session.id);
@@ -952,7 +1000,7 @@ export class MessageProcessor {
952
1000
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
953
1001
  let sendOpts;
954
1002
  try {
955
- await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
1003
+ await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
956
1004
  sendOpts = this.getReplyContext(message);
957
1005
  }
958
1006
  catch { }
@@ -989,7 +1037,7 @@ export class MessageProcessor {
989
1037
  : path.resolve(process.cwd(), session.projectPath);
990
1038
  return { session, absoluteProjectPath };
991
1039
  }
992
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
1040
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
993
1041
  // 兜底纠正1:群聊强制 proactive
994
1042
  if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
995
1043
  logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
@@ -1003,6 +1051,13 @@ export class MessageProcessor {
1003
1051
  session.sessionMode = 'proactive';
1004
1052
  await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
1005
1053
  }
1054
+ // Proactive→Interactive 模式切换提示:上一轮 proactive 使用了标志位,本轮已切换为 interactive
1055
+ if (session.sessionMode === 'interactive' && session.metadata?.lastProactiveFlag) {
1056
+ message.content = '本轮会话已切换为 interactive 模式,无需调用工具发送消息。\n\n' + message.content;
1057
+ delete session.metadata.lastProactiveFlag;
1058
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
1059
+ logger.info(`[MessageProcessor] Injected interactive mode hint for session ${session.id}`);
1060
+ }
1006
1061
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
1007
1062
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
1008
1063
  ? session.projectPath
@@ -1017,7 +1072,7 @@ export class MessageProcessor {
1017
1072
  */
1018
1073
  async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
1019
1074
  // Per-session agent name for stats bucketing
1020
- const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
1075
+ const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelKey || session.channel)?.name ?? '<unknown>';
1021
1076
  let hasReceivedText = false;
1022
1077
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
1023
1078
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
@@ -1083,7 +1138,7 @@ export class MessageProcessor {
1083
1138
  });
1084
1139
  continue;
1085
1140
  }
1086
- const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
1141
+ const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
1087
1142
  // === 前台任务:正常处理所有事件 ===
1088
1143
  if (!isCurrentlyBackground) {
1089
1144
  // 流式文本
@@ -1099,7 +1154,7 @@ export class MessageProcessor {
1099
1154
  if (event.type === 'compact') {
1100
1155
  this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
1101
1156
  if (!shouldSuppress()) {
1102
- renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`, 'info', 'compact');
1157
+ renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...)`, 'info', 'compact');
1103
1158
  }
1104
1159
  }
1105
1160
  // 子任务进度
@@ -1177,14 +1232,15 @@ export class MessageProcessor {
1177
1232
  // SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
1178
1233
  // 仅记录状态,最终 flush(true) 在流结束后统一执行
1179
1234
  if (event.type === 'complete') {
1180
- logger.info(`[MessageProcessor] complete event: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
1235
+ const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
1236
+ logger.info(`[MessageProcessor] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
1181
1237
  // 自动回填会话名称
1182
1238
  if (event.sessionTitle && session.name === '默认会话') {
1183
1239
  await this.sessionManager.renameSession(session.id, event.sessionTitle);
1184
1240
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1185
1241
  }
1186
1242
  // 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
1187
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1243
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
1188
1244
  // thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
1189
1245
  // 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
1190
1246
  // 失败且无前置错误输出:显示 errors 摘要
@@ -1212,6 +1268,15 @@ export class MessageProcessor {
1212
1268
  if (renderer.hasContent()) {
1213
1269
  await renderer.flushActivitiesOnly();
1214
1270
  }
1271
+ // 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
1272
+ if (session.sessionMode === 'proactive' && lastReplyText) {
1273
+ if (/\[PROACTIVE:REPLY_CONFIRMED_(SENT|NONE)\]/.test(lastReplyText)) {
1274
+ session.metadata = session.metadata || {};
1275
+ session.metadata.lastProactiveFlag = true;
1276
+ await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
1277
+ logger.debug(`[MessageProcessor] Set lastProactiveFlag for session ${session.id}`);
1278
+ }
1279
+ }
1215
1280
  }
1216
1281
  continue;
1217
1282
  }
@@ -1231,7 +1296,7 @@ export class MessageProcessor {
1231
1296
  logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
1232
1297
  }
1233
1298
  // 记录完成状态
1234
- completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
1299
+ completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, usage: event.usage };
1235
1300
  if (event.subtype === 'success') {
1236
1301
  this.messageCache.addEvent(session.id, {
1237
1302
  type: 'completed',
@@ -1344,85 +1409,6 @@ export class MessageProcessor {
1344
1409
  // 都找不到,返回项目根目录路径
1345
1410
  return rootPath;
1346
1411
  }
1347
- /**
1348
- * 确保全局数据目录下有最新版本的 SKILLS.md
1349
- * 目标:{EVOLCLAW_HOME}/data/SKILLS.md
1350
- */
1351
- ensureSkillsFile() {
1352
- try {
1353
- const targetDir = path.join(resolveRoot(), 'data');
1354
- const targetPath = path.join(targetDir, 'SKILLS.md');
1355
- const templatePath = path.join(getPackageRoot(), 'src', 'templates', 'skills.md');
1356
- // 模板不存在则跳过(构建环境可能没有 src/)
1357
- if (!fs.existsSync(templatePath)) {
1358
- // 尝试 dist/templates/skills.md
1359
- const distTemplatePath = path.join(getPackageRoot(), 'dist', 'templates', 'skills.md');
1360
- if (!fs.existsSync(distTemplatePath))
1361
- return;
1362
- this.copySkillsIfNeeded(distTemplatePath, targetDir, targetPath);
1363
- return;
1364
- }
1365
- this.copySkillsIfNeeded(templatePath, targetDir, targetPath);
1366
- }
1367
- catch {
1368
- // 静默失败,不影响正常消息处理
1369
- }
1370
- }
1371
- copySkillsIfNeeded(templatePath, targetDir, targetPath) {
1372
- const templateContent = fs.readFileSync(templatePath, 'utf-8');
1373
- const templateVersion = templateContent.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
1374
- if (fs.existsSync(targetPath)) {
1375
- const existing = fs.readFileSync(targetPath, 'utf-8');
1376
- const existingVersion = existing.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
1377
- if (this.compareSemver(existingVersion, templateVersion) >= 0)
1378
- return; // 已是最新
1379
- }
1380
- if (!fs.existsSync(targetDir)) {
1381
- fs.mkdirSync(targetDir, { recursive: true });
1382
- }
1383
- fs.writeFileSync(targetPath, templateContent, 'utf-8');
1384
- }
1385
- /** 简易 semver 比较:支持 "1", "1.0", "1.0.0" 等格式,返回 -1/0/1 */
1386
- compareSemver(a, b) {
1387
- const pa = a.split('.').map(Number);
1388
- const pb = b.split('.').map(Number);
1389
- const len = Math.max(pa.length, pb.length);
1390
- for (let i = 0; i < len; i++) {
1391
- const na = pa[i] || 0;
1392
- const nb = pb[i] || 0;
1393
- if (na !== nb)
1394
- return na > nb ? 1 : -1;
1395
- }
1396
- return 0;
1397
- }
1398
- /**
1399
- * 从 data/SKILLS.md 读取 frontmatter 并生成提示。
1400
- * 不缓存:每次读取保证用户编辑立即生效。
1401
- * 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
1402
- */
1403
- getSkillsHint() {
1404
- try {
1405
- const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
1406
- if (!fs.existsSync(skillsPath))
1407
- return null;
1408
- const content = fs.readFileSync(skillsPath, 'utf-8');
1409
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1410
- if (!frontmatterMatch)
1411
- return null;
1412
- const fm = frontmatterMatch[1];
1413
- const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
1414
- const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
1415
- const parts = [
1416
- `可通过 Bash 指令管理运行时,${desc}。`,
1417
- trigger ? `触发时机:${trigger}。` : '',
1418
- `完整文档见 ${skillsPath}`,
1419
- ];
1420
- return parts.filter(Boolean).join('');
1421
- }
1422
- catch {
1423
- return null;
1424
- }
1425
- }
1426
1412
  /**
1427
1413
  * 判断文件路径是否为占位符/示例文本
1428
1414
  * 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
@@ -10,6 +10,7 @@ export class StreamIdleMonitor {
10
10
  state;
11
11
  triggeredLevels = new Set();
12
12
  idleMs;
13
+ paused = false;
13
14
  constructor(idleMs) {
14
15
  this.idleMs = idleMs;
15
16
  this.state = {
@@ -22,6 +23,24 @@ export class StreamIdleMonitor {
22
23
  hasReceivedText: false,
23
24
  };
24
25
  }
26
+ /**
27
+ * 暂停空闲计时(等待用户交互期间调用,如权限确认 / AskUserQuestion / PlanMode)。
28
+ * 暂停期间 check() 始终返回 null,等待时长不计入 idle。
29
+ */
30
+ pause() {
31
+ this.paused = true;
32
+ }
33
+ /**
34
+ * 恢复空闲计时。从恢复时刻重新起算 idle,并清空已触发级别——
35
+ * 用户应答后是一次全新的执行周期,不应继承等待前的 idle 状态。
36
+ */
37
+ resume() {
38
+ if (!this.paused)
39
+ return;
40
+ this.paused = false;
41
+ this.state.lastEventTime = Date.now();
42
+ this.triggeredLevels.clear();
43
+ }
25
44
  /**
26
45
  * 记录 SDK 事件,更新状态并重置空闲计时
27
46
  */
@@ -44,6 +63,8 @@ export class StreamIdleMonitor {
44
63
  * 检查空闲状态,返回 null(未空闲)或分级结果
45
64
  */
46
65
  check() {
66
+ if (this.paused)
67
+ return null;
47
68
  const now = Date.now();
48
69
  const idleDuration = now - this.state.lastEventTime;
49
70
  const notifyThreshold = this.idleMs * NOTIFY_MULTIPLIER;