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
@@ -89,23 +89,36 @@ export class ChannelLoader {
89
89
  }
90
90
  return instances;
91
91
  }
92
- async connectAll(instances, delayMs = 150) {
92
+ async connectAll(instances, { concurrency = 3, intervalMs = 50 } = {}) {
93
93
  const connected = [];
94
94
  const failed = [];
95
+ const inflight = new Set();
95
96
  for (const inst of instances) {
96
- try {
97
- await inst.connect();
98
- connected.push(inst.adapter.channelName);
99
- }
100
- catch (e) {
101
- failed.push(e);
97
+ // 等待并发数降到 concurrency 以下
98
+ while (inflight.size >= concurrency) {
99
+ await Promise.race(inflight);
102
100
  }
103
- if (delayMs > 0 && inst !== instances[instances.length - 1]) {
104
- await new Promise(r => setTimeout(r, delayMs));
101
+ const task = (async () => {
102
+ try {
103
+ await inst.connect();
104
+ connected.push(inst.adapter.channelName);
105
+ }
106
+ catch (e) {
107
+ failed.push({ name: inst.adapter.channelName, error: e });
108
+ logger.warn(`[connectAll] ${inst.adapter.channelName} connect failed: ${e}`);
109
+ }
110
+ })();
111
+ const tracked = task.then(() => { inflight.delete(tracked); });
112
+ inflight.add(tracked);
113
+ // 间隔发起,避免瞬间并发冲击网关
114
+ if (intervalMs > 0) {
115
+ await new Promise(r => setTimeout(r, intervalMs));
105
116
  }
106
117
  }
118
+ // 等待所有剩余任务完成
119
+ await Promise.allSettled(inflight);
107
120
  if (failed.length > 0) {
108
- logger.warn(`Some channels failed to connect:`, failed);
121
+ logger.warn(`[connectAll] ${failed.length} channel(s) failed initial connect (will retry in background): ${failed.map(f => f.name).join(', ')}`);
109
122
  }
110
123
  return connected;
111
124
  }
@@ -10,6 +10,7 @@ import fs from 'fs';
10
10
  import os from 'os';
11
11
  import { parseTriggerSet } from './trigger/parser.js';
12
12
  import { calcNextFireAt } from './trigger/scheduler.js';
13
+ import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
13
14
  const allEfforts = ['low', 'medium', 'high', 'max'];
14
15
  const nonMaxEfforts = allEfforts.filter(e => e !== 'max');
15
16
  function getAvailableEfforts(agent, model) {
@@ -108,7 +109,7 @@ function formatIdleTime(ms) {
108
109
  return '刚刚';
109
110
  }
110
111
  // 支持的命令列表
111
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/trigger'];
112
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/trigger', '/upgrade'];
112
113
  // 命令别名映射
113
114
  const aliases = {
114
115
  '/p': '/project',
@@ -117,7 +118,7 @@ const aliases = {
117
118
  '/rw': '/rewind'
118
119
  };
119
120
  // 命令快速路径前缀(所有命令都不进入消息队列)
120
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/trigger'];
121
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/trigger', '/upgrade'];
121
122
  export class CommandHandler {
122
123
  sessionManager;
123
124
  messageCache;
@@ -340,7 +341,6 @@ export class CommandHandler {
340
341
  return renderCommandCardAsText(card);
341
342
  if (!adapter?.send)
342
343
  return renderCommandCardAsText(card);
343
- // session 忙碌时降级到文本,避免并发触发带参写操作
344
344
  if (this.isSessionBusy(opts.interaction.sessionId))
345
345
  return renderCommandCardAsText(card);
346
346
  try {
@@ -526,7 +526,7 @@ export class CommandHandler {
526
526
  ] } },
527
527
  { cmd: '/dispatch', label: '切换分发模式', desc: '控制群聊消息过滤(仅@提及或广播响应)', next: { type: 'select', items: [
528
528
  { value: 'mention', label: '@ 提及', desc: '仅在被 @ 提及时响应' },
529
- { value: 'all', label: '广播', desc: '响应群内所有消息' },
529
+ { value: 'broadcast', label: '广播', desc: '响应群内所有消息' },
530
530
  ] } },
531
531
  ]
532
532
  });
@@ -694,14 +694,14 @@ export class CommandHandler {
694
694
  return { data: { mode: arg } };
695
695
  }
696
696
  if (cmdBase === '/dispatch') {
697
- const currentMode = session.metadata?.dispatchMode || 'mention';
697
+ const currentMode = session.metadata?.dispatchMode;
698
698
  if (mode === 'query') {
699
- return { data: { mode: currentMode } };
699
+ return { data: { mode: currentMode ?? null } };
700
700
  }
701
701
  // update
702
702
  if (!arg)
703
703
  return { error: '缺少目标模式' };
704
- if (arg !== 'mention' && arg !== 'all')
704
+ if (arg !== 'mention' && arg !== 'broadcast')
705
705
  return { error: `无效模式: ${arg}` };
706
706
  const identity = this.sessionManager.resolveIdentity(channel, userId);
707
707
  const chatType = session.chatType || 'private';
@@ -893,7 +893,7 @@ export class CommandHandler {
893
893
  '💬 聊天设置:',
894
894
  ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
895
895
  ' /chatmode [interactive|proactive] - 查看/切换会话模式(被动响应或主动推进)',
896
- ' /dispatch [mention|all] - 查看/切换群聊分发模式(仅@响应或广播响应,仅群聊)',
896
+ ' /dispatch [mention|broadcast] - 查看/切换群聊分发模式(仅@响应或广播响应,仅群聊)',
897
897
  '',
898
898
  '🔐 权限管理:',
899
899
  ' /perm - 查看当前权限模式',
@@ -970,7 +970,7 @@ export class CommandHandler {
970
970
  // 聊天设置
971
971
  if (isAdmin) {
972
972
  cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '聊天设置', roles: ['admin', 'owner'] });
973
- cmds.push({ command: '/dispatch', args: '[mention|all]', description: '查看/切换群聊分发模式(仅@响应或广播响应)', category: '聊天设置', roles: ['admin', 'owner'] });
973
+ cmds.push({ command: '/dispatch', args: '[mention|broadcast]', description: '查看/切换群聊分发模式(仅@响应或广播响应)', category: '聊天设置', roles: ['admin', 'owner'] });
974
974
  }
975
975
  // 交互
976
976
  cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['guest', 'admin', 'owner'] });
@@ -1750,7 +1750,7 @@ export class CommandHandler {
1750
1750
  this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: chatmodeSession.id, mode: arg, timestamp: Date.now() });
1751
1751
  return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
1752
1752
  }
1753
- // /dispatch 命令:查看/切换群聊分发模式(mention | all
1753
+ // /dispatch 命令:查看/切换群聊分发模式(mention | broadcast
1754
1754
  // 仅群聊可用;群聊中设置需管理员权限
1755
1755
  if (normalizedContent === '/dispatch' || normalizedContent.startsWith('/dispatch ')) {
1756
1756
  const dispatchResult = await this.ensureSession(channel, channelId, threadId, chatType);
@@ -1762,13 +1762,14 @@ export class CommandHandler {
1762
1762
  return { kind: 'command.error', text: '❌ /dispatch 仅在群聊中可用' };
1763
1763
  }
1764
1764
  const arg = normalizedContent.slice(9).trim();
1765
- const currentMode = dispatchSession.metadata?.dispatchMode || 'mention';
1765
+ const currentMode = dispatchSession.metadata?.dispatchMode;
1766
1766
  if (!arg) {
1767
+ const displayMode = currentMode ?? '未设置(跟随群设置)';
1767
1768
  // 尝试发送 CommandCard 卡片
1768
1769
  if (isAdmin) {
1769
1770
  const modes = [
1770
1771
  { key: 'mention', name: '提及模式', desc: '仅当被 @ 提及(含 @all)时响应群消息' },
1771
- { key: 'all', name: '广播模式', desc: '群内所有消息都触发响应' },
1772
+ { key: 'broadcast', name: '广播模式', desc: '群内所有消息都触发响应' },
1772
1773
  ];
1773
1774
  const interaction = {
1774
1775
  type: 'interaction',
@@ -1796,19 +1797,19 @@ export class CommandHandler {
1796
1797
  }
1797
1798
  // 降级:文本
1798
1799
  const lines = [];
1799
- lines.push(`📋 分发模式: ${currentMode}`);
1800
+ lines.push(`📋 分发模式: ${displayMode}`);
1800
1801
  lines.push('');
1801
1802
  lines.push('模式说明:');
1802
- lines.push(' • mention — 提及模式:仅当被@提及时响应群消息(含@all)');
1803
- lines.push(' • all — 广播模式:群内所有消息都触发响应');
1803
+ lines.push(' • mention — 提及模式:仅当被@提及时响应群消息(含@all)');
1804
+ lines.push(' • broadcast — 广播模式:群内所有消息都触发响应');
1804
1805
  if (isAdmin) {
1805
1806
  lines.push('');
1806
- lines.push('用法: /dispatch <mention|all>');
1807
+ lines.push('用法: /dispatch <mention|broadcast>');
1807
1808
  }
1808
1809
  return { kind: 'command.result', text: lines.join('\n') };
1809
1810
  }
1810
- if (arg !== 'mention' && arg !== 'all') {
1811
- return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention / all\n用法: /dispatch <模式>` };
1811
+ if (arg !== 'mention' && arg !== 'broadcast') {
1812
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention / broadcast\n用法: /dispatch <模式>` };
1812
1813
  }
1813
1814
  if (!isAdmin) {
1814
1815
  return { kind: 'command.error', text: '❌ 无权限:群聊中切换分发模式仅限管理员使用' };
@@ -1819,7 +1820,7 @@ export class CommandHandler {
1819
1820
  const metadata = { ...(dispatchSession.metadata || {}), dispatchMode: arg };
1820
1821
  await this.sessionManager.updateSession(dispatchSession.id, { metadata });
1821
1822
  this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: arg, timestamp: Date.now() });
1822
- return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode} → ${arg}` };
1823
+ return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
1823
1824
  }
1824
1825
  // /stop 命令:中断当前任务
1825
1826
  if (normalizedContent === '/stop') {
@@ -1970,7 +1971,7 @@ export class CommandHandler {
1970
1971
  }
1971
1972
  const lines = [];
1972
1973
  const sessionMode = session.sessionMode || 'interactive';
1973
- const dispatchMode = session.metadata?.dispatchMode || 'mention';
1974
+ const dispatchMode = session.metadata?.dispatchMode ?? '未设置(跟随群设置)';
1974
1975
  const chatModeLine = `会话模式: ${sessionMode}`;
1975
1976
  const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
1976
1977
  if (isAdmin) {
@@ -2218,6 +2219,23 @@ export class CommandHandler {
2218
2219
  await executeRestart();
2219
2220
  return { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' };
2220
2221
  }
2222
+ // /upgrade 命令:检查版本更新,提示用户手动重启
2223
+ if (normalizedContent === '/upgrade') {
2224
+ if (!isAdmin)
2225
+ return { kind: 'command.error', text: '❌ 无权限:升级检查仅限管理员使用' };
2226
+ if (isLinkedInstall()) {
2227
+ return { kind: 'command.result', text: '⏭ 开发模式,跳过升级检查' };
2228
+ }
2229
+ const localVer = getLocalVersion();
2230
+ const remoteVer = await checkLatestVersion();
2231
+ if (!remoteVer) {
2232
+ return { kind: 'command.result', text: `⚠️ 无法连接 npm registry(当前版本 ${localVer})` };
2233
+ }
2234
+ if (compareVersions(localVer, remoteVer) >= 0) {
2235
+ return { kind: 'command.result', text: `✓ 已是最新版本 (${localVer})` };
2236
+ }
2237
+ return { kind: 'command.result', text: `📦 发现新版本 ${localVer} → ${remoteVer}\n执行 /restart 升级` };
2238
+ }
2221
2239
  // /pwd 命令:显示当前项目路径
2222
2240
  if (normalizedContent === '/pwd') {
2223
2241
  // session 现在总是存在(上面已自动创建)
@@ -3256,7 +3274,8 @@ export class CommandHandler {
3256
3274
  '/help', '/status', '/check', '/pwd',
3257
3275
  '/model', '/effort', '/perm', '/agent',
3258
3276
  '/compact', '/file', '/send', '/restart', '/bind', '/aid', '/rpc', '/storage',
3259
- '/rename', '/name', '/evolagent',
3277
+ '/rename', '/name', '/evolagent', '/trigger',
3278
+ '/chatmode', '/dispatch', '/activity',
3260
3279
  ];
3261
3280
  /** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
3262
3281
  static CTL_READONLY = new Set(['/agent']);
@@ -3278,6 +3297,8 @@ export class CommandHandler {
3278
3297
  const taskId = this.sessionManager.getActiveTaskId(session.id);
3279
3298
  const chatmode = session.sessionMode || 'interactive';
3280
3299
  const encrypted = this.sessionManager.getSessionEncrypt(session.id);
3300
+ // 诊断日志:记录 task_id 解析结果
3301
+ logger.info(`[CommandHandler] buildCtlReplyContext: sessionId=${session.id} taskId=${taskId ?? 'none'} chatmode=${chatmode} threadId=${ctx.threadId ?? 'none'}`);
3281
3302
  if (taskId || chatmode !== 'interactive' || encrypted != null) {
3282
3303
  ctx.metadata = {};
3283
3304
  if (taskId)
@@ -3294,6 +3315,7 @@ export class CommandHandler {
3294
3315
  * 复用现有 slash cmd 逻辑,权限继承 session 用户角色
3295
3316
  */
3296
3317
  async handleCtl(cmd, sessionId) {
3318
+ logger.info(`[ctl] cmd="${cmd}" sessionId=${sessionId}`);
3297
3319
  // 1. 白名单检查
3298
3320
  const inputCmd = cmd.split(' ')[0];
3299
3321
  if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
@@ -3362,7 +3384,9 @@ export class CommandHandler {
3362
3384
  return { ok: false, error: `adapter 未找到: ${session.channel}` };
3363
3385
  try {
3364
3386
  const replyContext = this.buildCtlReplyContext(session);
3365
- await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: session.channelId, replyContext: replyContext }), { kind: 'result.text', text, isFinal: true });
3387
+ const taskId = replyContext?.metadata?.taskId;
3388
+ const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
3389
+ await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext }), { kind: 'result.text', text, isFinal: true });
3366
3390
  return { ok: true, result: '已发送' };
3367
3391
  }
3368
3392
  catch (err) {
@@ -150,17 +150,12 @@ export class EvolAgent {
150
150
  this.persist();
151
151
  }
152
152
  // ── ShowActivities ────────────────────────────────────────────────────
153
- getShowActivities(channelKey) {
154
- const inst = this.findChannelInstance(channelKey);
155
- return inst?.showActivities ?? this.merged.show_activities ?? 'all';
153
+ getShowActivities(_channelKey) {
154
+ return this.merged.show_activities ?? 'all';
156
155
  }
157
- setShowActivities(channelKey, mode) {
158
- const inst = this.findRawChannelInstance(channelKey);
159
- if (!inst) {
160
- logger.warn(`[EvolAgent ${this.aid}] setShowActivities: channel "${channelKey}" not found`);
161
- return;
162
- }
163
- inst.showActivities = mode;
156
+ setShowActivities(_channelKey, mode) {
157
+ this.rawAgent.show_activities = mode;
158
+ this.merged.show_activities = mode;
164
159
  this.persist();
165
160
  }
166
161
  // ── Baseagent 字段写入 ────────────────────────────────────────────────
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../../utils/logger.js';
2
+ import { summarizeToolInput } from '../permission.js';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
4
5
  import { resolvePaths } from '../../paths.js';
@@ -29,8 +30,8 @@ export class IMRenderer {
29
30
  diagEnabled;
30
31
  /** 串行发送队列:保证消息按序到达 */
31
32
  sendChain = Promise.resolve();
32
- /** proactive:是否已发过 thinking 文本(用于去重 complete.result) */
33
- hasEmittedThinking = false;
33
+ /** proactive:是否已发过 text 文本(用于去重 complete.result) */
34
+ hasEmittedText = false;
34
35
  /** 自增 callId 兜底(runner 没提供时用) */
35
36
  syntheticCallSeq = 0;
36
37
  constructor(opts) {
@@ -75,7 +76,34 @@ export class IMRenderer {
75
76
  }
76
77
  /** 是否有 pending 内容 */
77
78
  hasContent() {
78
- return this.textBuffer.length > 0 || this.itemsQueue.some(it => it.kind !== 'thinking');
79
+ return this.textBuffer.length > 0 || this.itemsQueue.some(it => it.kind !== 'text');
80
+ }
81
+ /** 是否有待发送的文本 */
82
+ hasTextPending() {
83
+ return this.textBuffer.length > 0;
84
+ }
85
+ /** flush 当前 textBuffer 作为独立的 result.text(非 final),然后清空 buffer */
86
+ async flushText() {
87
+ if (this.opts.envelope.chatmode === 'proactive')
88
+ return;
89
+ if (this.textBuffer.length === 0)
90
+ return;
91
+ if (this.timer) {
92
+ clearTimeout(this.timer);
93
+ this.timer = undefined;
94
+ }
95
+ const text = this.textBuffer;
96
+ this.textBuffer = '';
97
+ // 清掉 itemsQueue 中的 text items(已发出)
98
+ this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
99
+ const payload = { kind: 'result.text', text, isFinal: false };
100
+ this.sentContent = true;
101
+ this.sendChain = this.sendChain
102
+ .then(() => this.opts.send(payload))
103
+ .catch(e => logger.warn('[IMRenderer] flushText send failed:', e));
104
+ await this.sendChain;
105
+ this.lastFlush = Date.now();
106
+ this.flushCount++;
79
107
  }
80
108
  /** 是否已发送过内容(用于决定最终 flush 是否带 isFinal 标题) */
81
109
  hasSentContent() {
@@ -92,9 +120,9 @@ export class IMRenderer {
92
120
  /** 从 buffer 中移除指定 pattern(用于文件标记预处理) */
93
121
  stripFromBuffer(pattern) {
94
122
  this.textBuffer = this.textBuffer.replace(pattern, '').trim();
95
- // itemsQueue 中的 thinking items 也同步过滤
123
+ // itemsQueue 中的 text items 也同步过滤
96
124
  for (const item of this.itemsQueue) {
97
- if (item.kind === 'thinking') {
125
+ if (item.kind === 'text') {
98
126
  item.text = item.text.replace(pattern, '');
99
127
  }
100
128
  }
@@ -106,13 +134,13 @@ export class IMRenderer {
106
134
  return;
107
135
  if (!text)
108
136
  return;
109
- // 同一窗口内连续 text delta 合并到最后一个 thinking item
137
+ // 同一窗口内连续 text delta 合并到最后一个 text item
110
138
  const last = this.itemsQueue[this.itemsQueue.length - 1];
111
- if (last && last.kind === 'thinking') {
139
+ if (last && last.kind === 'text') {
112
140
  last.text += text;
113
141
  }
114
142
  else {
115
- this.itemsQueue.push({ kind: 'thinking', text });
143
+ this.itemsQueue.push({ kind: 'text', text });
116
144
  }
117
145
  this.textBuffer += text;
118
146
  this.allText += text;
@@ -241,17 +269,17 @@ export class IMRenderer {
241
269
  const maxDelay = interval * 2.5;
242
270
  return Math.max(minDelay, Math.min(maxDelay, dynamicDelay));
243
271
  }
244
- /** 仅 flush 非 thinking items(保留 textBuffer 用于后续 final flush) */
272
+ /** 仅 flush 非 text items(text items 和 textBuffer 保留,等待下次完整 flush) */
245
273
  async flushActivitiesInternal() {
246
- const nonThinking = this.itemsQueue.filter(it => it.kind !== 'thinking');
274
+ const nonThinking = this.itemsQueue.filter(it => it.kind !== 'text');
247
275
  if (nonThinking.length === 0)
248
276
  return;
249
277
  if (this.timer) {
250
278
  clearTimeout(this.timer);
251
279
  this.timer = undefined;
252
280
  }
253
- // 移除已 flush 的 non-thinking items,保留 thinking items
254
- this.itemsQueue = this.itemsQueue.filter(it => it.kind === 'thinking');
281
+ // 移除已 flush 的 non-text items,保留 text items
282
+ this.itemsQueue = this.itemsQueue.filter(it => it.kind === 'text');
255
283
  const payload = { kind: 'activity.batch', items: nonThinking };
256
284
  if (this.diagEnabled)
257
285
  diag(this.instanceId, 'flushActivitiesOnly', { itemCount: nonThinking.length });
@@ -276,13 +304,13 @@ export class IMRenderer {
276
304
  if (this.opts.fileMarkerPattern) {
277
305
  this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
278
306
  for (const item of this.itemsQueue) {
279
- if (item.kind === 'thinking')
307
+ if (item.kind === 'text')
280
308
  item.text = item.text.replace(this.opts.fileMarkerPattern, '');
281
309
  }
282
310
  }
283
- // 清掉空 thinking items
311
+ // 清掉空 text items
284
312
  const items = this.itemsQueue.filter(it => {
285
- if (it.kind === 'thinking')
313
+ if (it.kind === 'text')
286
314
  return it.text.length > 0;
287
315
  return true;
288
316
  });
@@ -290,7 +318,6 @@ export class IMRenderer {
290
318
  const finalText = isFinal ? this.textBuffer : '';
291
319
  if (isFinal)
292
320
  this.textBuffer = '';
293
- // 非 final flush 保留 textBuffer,供最终 result.text 使用
294
321
  if (this.diagEnabled) {
295
322
  diag(this.instanceId, 'flush', {
296
323
  isFinal,
@@ -300,12 +327,8 @@ export class IMRenderer {
300
327
  sinceLastFlush: Date.now() - this.lastFlush,
301
328
  });
302
329
  }
303
- // 1. interactive 模式下:isFinal=true 时不发 thinking-only batch
304
- // (避免和最终 result.text 重复——最终回复已在 textBuffer 里)
305
- let itemsForBatch = items;
306
- if (isFinal) {
307
- itemsForBatch = items.filter(it => it.kind !== 'thinking');
308
- }
330
+ // 1. interactive 模式下:不发 text items(由 result.text 统一发送最终文本)
331
+ let itemsForBatch = items.filter(it => it.kind !== 'text');
309
332
  if (itemsForBatch.length > 0) {
310
333
  const payload = { kind: 'activity.batch', items: itemsForBatch };
311
334
  this.sentContent = true;
@@ -334,14 +357,14 @@ export class IMRenderer {
334
357
  if (event.type === 'complete' &&
335
358
  !event.isError &&
336
359
  event.result &&
337
- this.hasEmittedThinking) {
360
+ this.hasEmittedText) {
338
361
  return;
339
362
  }
340
363
  const item = this.mapEventToItem(event);
341
364
  if (!item)
342
365
  return;
343
- if (item.kind === 'thinking') {
344
- this.hasEmittedThinking = true;
366
+ if (item.kind === 'text') {
367
+ this.hasEmittedText = true;
345
368
  this.allText += item.text;
346
369
  }
347
370
  const payload = { kind: 'activity.batch', items: [item] };
@@ -355,9 +378,9 @@ export class IMRenderer {
355
378
  case 'text':
356
379
  if (!event.text)
357
380
  return null;
358
- return { kind: 'thinking', text: event.text };
381
+ return { kind: 'text', text: event.text };
359
382
  case 'tool_use': {
360
- const desc = this.summarizeInput(event.input, event.name);
383
+ const desc = summarizeToolInput(event.name, event.input || {});
361
384
  return {
362
385
  kind: 'tool_call',
363
386
  call_id: event.callId || this.synthCallId(),
@@ -437,23 +460,6 @@ export class IMRenderer {
437
460
  return null;
438
461
  }
439
462
  }
440
- summarizeInput(input, toolName) {
441
- if (!input || typeof input !== 'object')
442
- return '';
443
- if (toolName === 'Bash' && typeof input.command === 'string') {
444
- const cmd = input.command;
445
- if (cmd.includes('evolclaw ctl send') || cmd.includes('evolclaw ctl file')) {
446
- return cmd;
447
- }
448
- }
449
- return (input.description ||
450
- input.file_path ||
451
- input.pattern ||
452
- (typeof input.command === 'string' ? input.command.substring(0, 80) : '') ||
453
- (typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : '') ||
454
- (typeof input.query === 'string' ? input.query.substring(0, 80) : '') ||
455
- '');
456
- }
457
463
  stringifyResult(result) {
458
464
  if (result === null || result === undefined)
459
465
  return '';
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export function formatItemsAsText(items) {
7
7
  if (!items || items.length === 0)
8
- return '';
8
+ return ''; // early exit
9
9
  const lines = [];
10
10
  for (const item of items) {
11
11
  const line = formatItem(item);
@@ -16,20 +16,27 @@ export function formatItemsAsText(items) {
16
16
  }
17
17
  function formatItem(item) {
18
18
  switch (item.kind) {
19
- case 'thinking':
19
+ case 'text':
20
20
  return item.text;
21
21
  case 'reasoning':
22
22
  return `💭 ${item.text}`;
23
23
  case 'tool_call': {
24
24
  const desc = item.text || summarizeArgs(item.arguments);
25
- return desc ? `🔧 ${item.name}: ${desc}` : `🔧 ${item.name}`;
25
+ if (!desc)
26
+ return `🔧 ${item.name}`;
27
+ // 多行 desc(如 Edit diff):第一行跟工具名同行,代码块从新行开始
28
+ if (desc.includes('\n')) {
29
+ const nlIdx = desc.indexOf('\n');
30
+ return `🔧 ${item.name} ${desc.slice(0, nlIdx)}\n${desc.slice(nlIdx + 1)}`;
31
+ }
32
+ return `🔧 ${item.name}: ${desc}`;
26
33
  }
27
34
  case 'tool_result': {
28
35
  if (!item.ok) {
29
36
  const errMsg = item.error || (typeof item.result === 'string' ? item.result : '执行失败');
30
37
  return `⚠️ ${item.name}: ${errMsg}`;
31
38
  }
32
- return item.text ? `✅ ${item.name}: ${item.text}` : `✅ ${item.name}`;
39
+ return item.text ? `✓ ${item.name}: ${item.text}` : `✓ ${item.name}`;
33
40
  }
34
41
  case 'progress':
35
42
  return `⏳ ${item.text}`;
@@ -63,6 +63,8 @@ export class MessageBridge {
63
63
  onMessage(async (msg) => {
64
64
  try {
65
65
  let content = msg.content.trim();
66
+ // 渠道入站日志
67
+ logger.channelIn({ channel: channelName, channelId: msg.channelId, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
66
68
  // 0. 自定义消息快速路径(menu.query 等)
67
69
  if (await this.handleCustomPayload(content, channelName, msg, sendReply, adapter))
68
70
  return;
@@ -75,7 +77,10 @@ export class MessageBridge {
75
77
  if (this.cmdHandler.isCommand(cmdContent)) {
76
78
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
77
79
  }
78
- if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => sendReply(msg.channelId, text, msg.replyContext), msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext))
80
+ if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
81
+ logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
82
+ return sendReply(msg.channelId, text, msg.replyContext);
83
+ }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext))
79
84
  return;
80
85
  // 3. session 解析(使用 Channel 层填充的 chatType)
81
86
  const chatType = msg.chatType || 'private';
@@ -104,7 +109,7 @@ export class MessageBridge {
104
109
  const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
105
110
  const effectiveProjectPath = owningAgent?.projectPath
106
111
  ?? this.defaultProjectPath;
107
- const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.selfId, msg.channelType || effectiveChannelType);
112
+ const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.selfId, msg.channelType || effectiveChannelType, msg.peerType);
108
113
  // 4. 消息前缀(由 policy 决定)
109
114
  const channelInfo = this.processor.getChannelInfo?.(channelName);
110
115
  if (channelInfo?.policy) {
@@ -87,5 +87,7 @@ export function buildOutboundEntry(opts) {
87
87
  permMode: null,
88
88
  cmdParsed: null,
89
89
  durationMs: opts.durationMs ?? null,
90
+ numTurns: opts.numTurns ?? null,
91
+ usage: opts.usage ?? null,
90
92
  };
91
93
  }