evolclaw 3.1.4 → 3.1.6

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/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  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
@@ -1,7 +1,6 @@
1
1
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
2
2
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
3
3
  import { getCodexEfforts } from '../agents/codex-runner.js';
4
- import { resolveAnthropicConfig, resolveOpenaiConfig } from '../agents/resolve.js';
5
4
  import { renderCommandCardAsText } from './interaction-router.js';
6
5
  import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
7
6
  import { resolvePaths, getPackageRoot } from '../paths.js';
@@ -13,8 +12,33 @@ import os from 'os';
13
12
  import { parseTriggerSet, parseTriggerUpdate } from './trigger/parser.js';
14
13
  import { calcNextFireAt } from './trigger/scheduler.js';
15
14
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
15
+ import { tryParseChannelKey } from './channel-loader.js';
16
16
  const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
17
17
  const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
18
+ // ── CLI 透传(menu.action name=cli action=exec)─────────────────────────
19
+ // 经消息通道的远程命令执行(RCE):仅 owner、白名单内只读+配置命令、无 shell、超时+截断。
20
+ // command → '*'(全部子命令放行) | Set(允许的子命令)。
21
+ // 刻意排除破坏性/进程控制/数据面:restart stop start init dev mv rpc msg group net、
22
+ // agent set/new/delete/enable/disable/rename/reload、aid new/delete/agentmd、storage 写操作。
23
+ const CLI_EXEC_WHITELIST = {
24
+ status: '*',
25
+ model: '*',
26
+ agent: new Set(['list', 'show', 'get']),
27
+ aid: new Set(['list', 'show', 'lookup']),
28
+ storage: new Set(['ls', 'quota']),
29
+ };
30
+ const CLI_EXEC_TIMEOUT_MS = 15_000;
31
+ const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
32
+ /** 把命令行字符串分词为 argv,尊重单/双引号,不调用 shell。 */
33
+ function tokenizeArgv(line) {
34
+ const out = [];
35
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
36
+ let m;
37
+ while ((m = re.exec(line)) !== null) {
38
+ out.push(m[1] ?? m[2] ?? m[3] ?? '');
39
+ }
40
+ return out;
41
+ }
18
42
  function getAvailableEfforts(agent, model) {
19
43
  if (agent.name === 'claude') {
20
44
  return allEfforts;
@@ -27,33 +51,13 @@ function getAvailableEfforts(agent, model) {
27
51
  function formatModelUsage(_agent, _model) {
28
52
  return '用法: /model <模型>';
29
53
  }
30
- function getModelListSource(owning, agent) {
31
- const codexConfig = owning?.config?.baseagents?.codex;
32
- const claudeConfig = owning?.config?.baseagents?.claude;
33
- if (agent.name === 'codex') {
34
- let resolved = {};
35
- try {
36
- resolved = resolveOpenaiConfig({ agents: { codex: codexConfig } }, codexConfig);
37
- }
38
- catch { }
39
- return {
40
- apiBaseUrl: resolved.baseUrl,
41
- apiKey: resolved.apiKey,
42
- fallbackModels: agent.listModels?.() || [],
43
- owner: 'openai',
44
- };
45
- }
46
- let resolved = {};
47
- try {
48
- resolved = resolveAnthropicConfig({ agents: { claude: claudeConfig } }, claudeConfig);
49
- }
50
- catch { }
51
- return {
52
- apiBaseUrl: resolved.baseUrl,
53
- apiKey: resolved.apiKey,
54
- fallbackModels: ['claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6'],
55
- owner: 'anthropic',
56
- };
54
+ /**
55
+ * 模型展示标签:短别名 + 实际完整 ID(如 "opus (claude-opus-4-8)")。
56
+ * 仅用于展示;命令值/持久化仍使用短别名。完整 ID 不可用或与短名相同时只显示短名。
57
+ */
58
+ function modelDisplayLabel(agent, model) {
59
+ const full = agent.resolveModelId?.(model);
60
+ return full && full !== model ? `${model} (${full})` : model;
57
61
  }
58
62
  /**
59
63
  * 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
@@ -225,33 +229,6 @@ export class CommandHandler {
225
229
  return owning.projectPath;
226
230
  return process.cwd();
227
231
  }
228
- /**
229
- * 返回当前通道有效的 projects.list(从 owning agent 的 config 取)。
230
- * 都没配 list 时回退到 defaultPath 单项目。
231
- */
232
- getEffectiveProjects(channel) {
233
- const owning = this.getOwningAgent(channel);
234
- if (owning) {
235
- return owning.getProjects();
236
- }
237
- return this.projects;
238
- }
239
- /**
240
- * 添加项目到当前通道范围(写到 owning agent 的 config.json)。
241
- */
242
- async addProjectInScope(channel, name, projectPath) {
243
- const owning = this.getOwningAgent(channel);
244
- if (!owning) {
245
- return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
246
- }
247
- try {
248
- owning.addProject(name, projectPath);
249
- }
250
- catch (e) {
251
- return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
252
- }
253
- return undefined;
254
- }
255
232
  /**
256
233
  * 持久化 baseagent.model:写到 agent config.json;找不到 owning agent 时
257
234
  * 退到用户级 ~/.claude/settings.json(Claude 专用)。
@@ -400,7 +377,7 @@ export class CommandHandler {
400
377
  return { matched: true, result: '✓ 已回答' };
401
378
  }
402
379
  /** 获取活跃会话,无会话时自动创建(话题除外) */
403
- async ensureSession(channel, channelId, threadId, chatType) {
380
+ async ensureSession(channel, channelId, threadId, chatType, selfAID) {
404
381
  if (threadId) {
405
382
  // 话题会话:仅查询,不创建
406
383
  const session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
@@ -411,8 +388,9 @@ export class CommandHandler {
411
388
  }
412
389
  const ct = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
413
390
  const channelType = this.resolveChannelType(channel);
391
+ const sid = selfAID ?? this.resolveSelfAID(channel);
414
392
  const session = await this.sessionManager.getActiveSession(channel, channelId)
415
- ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct, undefined, undefined, channelType);
393
+ ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct, undefined, sid, channelType);
416
394
  // 如果 session 已存在但 chatType 跟传入的不一致,更新
417
395
  if (ct && session.chatType !== ct) {
418
396
  await this.sessionManager.updateSession(session.id, { chatType: ct });
@@ -447,6 +425,14 @@ export class CommandHandler {
447
425
  resolveChannelType(channelName) {
448
426
  return this.channelTypeMap.get(channelName) || channelName;
449
427
  }
428
+ /**
429
+ * 从 channel key(<type>#<selfAID>#<name>)解析本地身份 AID。
430
+ * 非 evolagent 通道(裸 channelType,如 'feishu')解析失败返回 undefined。
431
+ * aun 通道创建 session 时必须提供 selfAID,故所有 getOrCreateSession 调用都经此兜底。
432
+ */
433
+ resolveSelfAID(channel) {
434
+ return tryParseChannelKey(channel)?.selfAID;
435
+ }
450
436
  registerPolicy(channelName, policy) {
451
437
  this.policies.set(channelName, policy);
452
438
  }
@@ -619,13 +605,6 @@ export class CommandHandler {
619
605
  }
620
606
  return items;
621
607
  }
622
- if (cmd === '/p') {
623
- // Use agent-scoped project list: agent-owned channels see their agent.json's
624
- // projects.list; default channel sees agent config's projects.list
625
- const list = this.getEffectiveProjects(channel);
626
- const currentPath = session?.projectPath;
627
- return Object.entries(list).map(([name, p]) => ({ value: name, label: name, desc: p, selected: currentPath === p }));
628
- }
629
608
  if (cmd === '/baseagent') {
630
609
  const currentAgent = session?.agentId;
631
610
  return this.getAvailableBaseagents(channel).map(name => ({ value: name, label: name, selected: name === currentAgent }));
@@ -636,7 +615,7 @@ export class CommandHandler {
636
615
  const models = await agent.listModels() ?? [];
637
616
  const currentModel = agent.getModel();
638
617
  if (models.length > 0)
639
- return models.map((m) => ({ value: m, label: m, selected: m === currentModel }));
618
+ return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m), selected: m === currentModel }));
640
619
  }
641
620
  return null;
642
621
  }
@@ -1044,7 +1023,7 @@ export class CommandHandler {
1044
1023
  env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
1045
1024
  }).unref();
1046
1025
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
1047
- setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 500);
1026
+ setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
1048
1027
  return { data: { action: 'restart', success: true } };
1049
1028
  }
1050
1029
  if (action === 'check') {
@@ -1057,8 +1036,96 @@ export class CommandHandler {
1057
1036
  }
1058
1037
  return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
1059
1038
  }
1039
+ // ── /cli 透传 ──
1040
+ if (cmdBase === '/cli') {
1041
+ if (action !== 'exec')
1042
+ return { error: `不支持的 cli action: ${action}`, code: 'NOT_SUPPORTED' };
1043
+ if (identity.role !== 'owner')
1044
+ return { error: '无权限:CLI 执行仅限 owner', code: 'NO_PERMISSION' };
1045
+ const argv = Array.isArray(args?.argv) ? args.argv.map((x) => String(x))
1046
+ : typeof args?.command === 'string' ? tokenizeArgv(args.command)
1047
+ : null;
1048
+ if (!argv || argv.length === 0)
1049
+ return { error: '缺少 argv 或 command', code: 'MISSING_VALUE' };
1050
+ const allowed = CLI_EXEC_WHITELIST[argv[0]];
1051
+ if (!allowed)
1052
+ return { error: `命令不在白名单: ${argv[0]}`, code: 'NOT_ALLOWED' };
1053
+ if (allowed !== '*' && !allowed.has(argv[1] ?? '')) {
1054
+ return { error: `子命令不在白名单: ${argv[0]} ${argv[1] ?? ''}`, code: 'NOT_ALLOWED' };
1055
+ }
1056
+ return await this.execCliPassthrough(argv);
1057
+ }
1060
1058
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
1061
1059
  }
1060
+ /**
1061
+ * CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
1062
+ * 不 in-process 调用(CLI handler 用 console.log + process.exit,spawn 行为与终端一致且隔离)。
1063
+ * 调用方已完成 owner 校验与白名单过滤。
1064
+ */
1065
+ async execCliPassthrough(argv) {
1066
+ const { spawn } = await import('child_process');
1067
+ const cliEntry = path.join(getPackageRoot(), 'dist', 'cli', 'index.js');
1068
+ const startedAt = Date.now();
1069
+ return await new Promise((resolve) => {
1070
+ let stdout = '';
1071
+ let stderr = '';
1072
+ let total = 0;
1073
+ let truncated = false;
1074
+ let settled = false;
1075
+ const child = spawn('node', [cliEntry, ...argv], {
1076
+ env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root },
1077
+ windowsHide: true,
1078
+ });
1079
+ const append = (buf, sink) => {
1080
+ if (truncated)
1081
+ return;
1082
+ const remaining = CLI_EXEC_MAX_OUTPUT - total;
1083
+ if (remaining <= 0) {
1084
+ truncated = true;
1085
+ return;
1086
+ }
1087
+ const chunk = buf.length > remaining ? buf.subarray(0, remaining) : buf;
1088
+ total += chunk.length;
1089
+ if (sink === 'out')
1090
+ stdout += chunk.toString('utf-8');
1091
+ else
1092
+ stderr += chunk.toString('utf-8');
1093
+ if (buf.length > remaining)
1094
+ truncated = true;
1095
+ };
1096
+ child.stdout?.on('data', (b) => append(b, 'out'));
1097
+ child.stderr?.on('data', (b) => append(b, 'err'));
1098
+ const timer = setTimeout(() => {
1099
+ if (settled)
1100
+ return;
1101
+ settled = true;
1102
+ try {
1103
+ child.kill('SIGKILL');
1104
+ }
1105
+ catch { }
1106
+ logger.warn(`[CommandHandler] cli exec timeout: ${argv.join(' ')}`);
1107
+ resolve({ error: `执行超时(${CLI_EXEC_TIMEOUT_MS / 1000}s):${argv[0]}`, code: 'TIMEOUT' });
1108
+ }, CLI_EXEC_TIMEOUT_MS);
1109
+ child.on('error', (e) => {
1110
+ if (settled)
1111
+ return;
1112
+ settled = true;
1113
+ clearTimeout(timer);
1114
+ resolve({ error: e?.message || String(e), code: 'INTERNAL' });
1115
+ });
1116
+ child.on('close', (exitCode) => {
1117
+ if (settled)
1118
+ return;
1119
+ settled = true;
1120
+ clearTimeout(timer);
1121
+ resolve({ data: {
1122
+ exitCode: exitCode ?? -1,
1123
+ stdout, stderr, truncated,
1124
+ durationMs: Date.now() - startedAt,
1125
+ } });
1126
+ });
1127
+ });
1128
+ }
1062
1129
  /** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
1063
1130
  async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
1064
1131
  try {
@@ -1093,16 +1160,16 @@ export class CommandHandler {
1093
1160
  }
1094
1161
  }
1095
1162
  isCommand(content) {
1096
- return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
1163
+ return content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
1097
1164
  }
1098
1165
  /**
1099
1166
  * 主命令处理入口
1100
1167
  */
1101
- async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
1102
- const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source);
1168
+ async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID) {
1169
+ const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID);
1103
1170
  return result;
1104
1171
  }
1105
- async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
1172
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID) {
1106
1173
  // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
1107
1174
  // 不应覆盖 session 中已有的正确值
1108
1175
  if (source === 'card-trigger')
@@ -1161,12 +1228,12 @@ export class CommandHandler {
1161
1228
  }
1162
1229
  // 空闲检查:某些命令需要等待当前会话空闲
1163
1230
  // 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
1164
- // - 始终需要 idle(无参即写):/new /clear /compact /repair /fork
1231
+ // - 始终需要 idle(无参即写):/clear /compact /repair /fork /new
1165
1232
  // - 仅带参时需要 idle(无参是列表/用法):/session /baseagent /rewind
1166
1233
  // - /chatmode:在 handler 内部自行做写操作的 idle 检查
1167
1234
  // - /dispatch:在 handler 内部自行做写操作的 idle 检查
1168
1235
  // - /safe:已禁用 no-op,不再要求 idle
1169
- const idleAlways = ['/new', '/clear', '/compact', '/repair', '/fork'];
1236
+ const idleAlways = ['/clear', '/compact', '/repair', '/fork', '/new'];
1170
1237
  const idleWhenArg = ['/session', '/baseagent', '/rewind'];
1171
1238
  const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
1172
1239
  idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
@@ -1176,13 +1243,19 @@ export class CommandHandler {
1176
1243
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
1177
1244
  if (threadSession) {
1178
1245
  const threadAgent = this.getAgent(channel, threadSession.agentId);
1179
- if (threadAgent.hasActiveStream(threadSession.id)) {
1246
+ const isBusy = threadAgent.hasActiveStream(threadSession.id) ||
1247
+ this.messageQueue?.isProcessing(threadSession.id);
1248
+ if (isBusy) {
1180
1249
  return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1181
1250
  }
1182
1251
  }
1183
1252
  }
1184
- else if (activeSession && agent.hasActiveStream(activeSession.id)) {
1185
- return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1253
+ else if (activeSession) {
1254
+ const isBusy = agent.hasActiveStream(activeSession.id) ||
1255
+ this.messageQueue?.isProcessing(activeSession.id);
1256
+ if (isBusy) {
1257
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1258
+ }
1186
1259
  }
1187
1260
  }
1188
1261
  // 检查是否以 / 开头(可能是命令)
@@ -1436,6 +1509,8 @@ export class CommandHandler {
1436
1509
  const metadata = permSession.metadata || {};
1437
1510
  metadata.permissionMode = arg;
1438
1511
  await this.sessionManager.updateSession(permSession.id, { metadata });
1512
+ if (source === 'card-trigger')
1513
+ return null;
1439
1514
  return { kind: 'command.result', text: `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}` };
1440
1515
  }
1441
1516
  }
@@ -1615,6 +1690,8 @@ export class CommandHandler {
1615
1690
  const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
1616
1691
  const projectName = this.getProjectName(session.projectPath);
1617
1692
  let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
1693
+ if (source === 'card-trigger')
1694
+ return null;
1618
1695
  return { kind: 'command.result', text: agentSwitchResponse };
1619
1696
  }
1620
1697
  // /setmodel 命令:返回 JSON 格式的模型列表(供程序解析)
@@ -1627,37 +1704,12 @@ export class CommandHandler {
1627
1704
  const currentModel = hasModelSwitcher(setmodelAgent) ? setmodelAgent.getModel() : setmodelAgent.name;
1628
1705
  const efforts = getAvailableEfforts(setmodelAgent, currentModel);
1629
1706
  const currentEffort = setmodelAgent.getEffort?.() || 'auto';
1630
- const modelListSource = getModelListSource(this.getOwningAgent(channel), setmodelAgent);
1631
- let modelListData = null;
1632
- if (modelListSource.apiBaseUrl) {
1633
- try {
1634
- const modelsUrl = modelListSource.apiBaseUrl.replace(/\/+$/, '') + '/v1/models';
1635
- const controller = new AbortController();
1636
- const timeout = setTimeout(() => controller.abort(), 5000);
1637
- const resp = await fetch(modelsUrl, {
1638
- signal: controller.signal,
1639
- headers: { 'Authorization': `Bearer ${modelListSource.apiKey || ''}` },
1640
- });
1641
- clearTimeout(timeout);
1642
- if (resp.ok) {
1643
- modelListData = await resp.json();
1644
- }
1645
- }
1646
- catch { }
1647
- }
1648
- // 兜底模型列表
1649
- if (!modelListData || !modelListData.data || modelListData.data.length === 0) {
1650
- const now = Math.floor(Date.now() / 1000);
1651
- modelListData = {
1652
- object: 'list',
1653
- data: modelListSource.fallbackModels.map(id => ({
1654
- id,
1655
- object: 'model',
1656
- created: now,
1657
- owned_by: modelListSource.owner,
1658
- })),
1659
- };
1660
- }
1707
+ const now = Math.floor(Date.now() / 1000);
1708
+ const modelIds = hasModelSwitcher(setmodelAgent) ? await setmodelAgent.listModels() : [];
1709
+ const modelListData = {
1710
+ object: 'list',
1711
+ data: modelIds.map(id => ({ id, object: 'model', created: now, owned_by: setmodelAgent.name === 'codex' ? 'openai' : 'anthropic' })),
1712
+ };
1661
1713
  return { kind: 'command.result', text: JSON.stringify({
1662
1714
  current_model: currentModel,
1663
1715
  current_effort: currentEffort,
@@ -1674,7 +1726,7 @@ export class CommandHandler {
1674
1726
  return { kind: 'command.result', text: modelResult.error };
1675
1727
  const { session: modelSession } = modelResult;
1676
1728
  const modelAgent = this.getAgent(channel, modelSession.agentId);
1677
- const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
1729
+ const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1678
1730
  if (!args) {
1679
1731
  const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
1680
1732
  const efforts = getAvailableEfforts(modelAgent, currentModel);
@@ -1690,12 +1742,15 @@ export class CommandHandler {
1690
1742
  kind: {
1691
1743
  kind: 'command-card',
1692
1744
  title: '🤖 切换模型',
1693
- buttons: models.map((m) => ({
1694
- label: m === currentModel ? `✓ ${m}` : m,
1695
- command: `/model ${m}`,
1696
- style: (m === currentModel ? 'primary' : 'default'),
1697
- disabled: m === currentModel,
1698
- })),
1745
+ buttons: models.map((m) => {
1746
+ const display = modelDisplayLabel(modelAgent, m);
1747
+ return {
1748
+ label: m === currentModel ? `✓ ${display}` : display,
1749
+ command: `/model ${m}`,
1750
+ style: (m === currentModel ? 'primary' : 'default'),
1751
+ disabled: m === currentModel,
1752
+ };
1753
+ }),
1699
1754
  },
1700
1755
  };
1701
1756
  const replyCtx = this.getReplyContext(modelSession);
@@ -1705,14 +1760,14 @@ export class CommandHandler {
1705
1760
  return { kind: 'command.result', text: cardResult };
1706
1761
  }
1707
1762
  // 降级:文本
1708
- const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
1763
+ const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1709
1764
  const effortHint = efforts.length > 0
1710
1765
  ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
1711
1766
  : '';
1712
1767
  if (isAdmin) {
1713
- return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n用法: /model <模型>` };
1768
+ return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}\n\n可用模型:\n${modelList}\n\n用法: /model <模型>` };
1714
1769
  }
1715
- return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}` };
1770
+ return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}` };
1716
1771
  }
1717
1772
  // 带参(切换/调整)需 admin+;无参查询已在上方返回
1718
1773
  if (!isAdmin)
@@ -1732,20 +1787,29 @@ export class CommandHandler {
1732
1787
  else if (allEfforts.includes(arg)) {
1733
1788
  return { kind: 'command.error', text: `⚠️ 请使用 /effort ${arg} 调整推理强度` };
1734
1789
  }
1735
- else if (models.includes(arg)) {
1736
- newModel = arg;
1737
- }
1738
1790
  else {
1739
- const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
1740
- const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
1741
- return { kind: 'command.error', text: `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}` };
1791
+ const resolvedArg = hasModelSwitcher(modelAgent) ? (modelAgent.resolveModelId?.(arg) ?? arg) : arg;
1792
+ if (models.includes(resolvedArg)) {
1793
+ newModel = resolvedArg;
1794
+ }
1795
+ else if (models.includes(arg)) {
1796
+ newModel = arg;
1797
+ }
1798
+ else {
1799
+ const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1800
+ const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
1801
+ return { kind: 'command.error', text: `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}` };
1802
+ }
1742
1803
  }
1743
1804
  }
1744
1805
  else {
1745
1806
  // 双参数:model effort
1746
- const [modelArg, effortArg] = parts;
1807
+ const [modelArgRaw, effortArg] = parts;
1808
+ const modelArg = hasModelSwitcher(modelAgent)
1809
+ ? (models.includes(modelArgRaw) ? modelArgRaw : (modelAgent.resolveModelId?.(modelArgRaw) ?? modelArgRaw))
1810
+ : modelArgRaw;
1747
1811
  if (!models.includes(modelArg)) {
1748
- return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArg}` };
1812
+ return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArgRaw}` };
1749
1813
  }
1750
1814
  const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
1751
1815
  if (targetEfforts.length === 0) {
@@ -1786,6 +1850,8 @@ export class CommandHandler {
1786
1850
  if (err)
1787
1851
  return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1788
1852
  }
1853
+ if (source === 'card-trigger')
1854
+ return null;
1789
1855
  return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
1790
1856
  }
1791
1857
  // /effort 命令:查看或切换推理强度
@@ -1860,6 +1926,8 @@ export class CommandHandler {
1860
1926
  const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
1861
1927
  if (err)
1862
1928
  return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1929
+ if (source === 'card-trigger')
1930
+ return null;
1863
1931
  return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
1864
1932
  }
1865
1933
  // /agent, /aid, /rpc, /storage — 仅限 ctl 调用,slash 输入拒绝
@@ -1946,6 +2014,8 @@ export class CommandHandler {
1946
2014
  else {
1947
2015
  return { kind: 'command.error', text: `⚠️ 找不到通道 "${channel}" 所属的 self-agent,无法持久化` };
1948
2016
  }
2017
+ if (source === 'card-trigger')
2018
+ return null;
1949
2019
  return { kind: 'command.result', text: `✅ 中间输出模式: ${activityArg}(${label})` };
1950
2020
  }
1951
2021
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
@@ -2016,16 +2086,18 @@ export class CommandHandler {
2016
2086
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2017
2087
  if (threadSession) {
2018
2088
  const threadAgent = this.getAgent(channel, threadSession.agentId);
2019
- if (threadAgent.hasActiveStream(threadSession.id)) {
2089
+ if (threadAgent.hasActiveStream(threadSession.id) || this.messageQueue?.isProcessing(threadSession.id)) {
2020
2090
  return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
2021
2091
  }
2022
2092
  }
2023
2093
  }
2024
- else if (agent.hasActiveStream(chatmodeSession.id)) {
2094
+ else if (agent.hasActiveStream(chatmodeSession.id) || this.messageQueue?.isProcessing(chatmodeSession.id)) {
2025
2095
  return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
2026
2096
  }
2027
2097
  await this.sessionManager.updateSession(chatmodeSession.id, { sessionMode: arg });
2028
2098
  this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: chatmodeSession.id, mode: arg, timestamp: Date.now() });
2099
+ if (source === 'card-trigger')
2100
+ return null;
2029
2101
  return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
2030
2102
  }
2031
2103
  // /dispatch 命令:查看/切换群聊分发模式(mention | broadcast)
@@ -2091,6 +2163,8 @@ export class CommandHandler {
2091
2163
  const metadata = { ...(dispatchSession.metadata || {}), dispatchMode: arg };
2092
2164
  await this.sessionManager.updateSession(dispatchSession.id, { metadata });
2093
2165
  this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: arg, timestamp: Date.now() });
2166
+ if (source === 'card-trigger')
2167
+ return null;
2094
2168
  return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
2095
2169
  }
2096
2170
  // /stop 命令:中断当前任务
@@ -2185,15 +2259,16 @@ export class CommandHandler {
2185
2259
  }
2186
2260
  // 尝试获取活跃会话(话题时直接查找话题 session)
2187
2261
  let session;
2262
+ const resolvedSelfAID = selfAID ?? this.resolveSelfAID(channel);
2188
2263
  if (threadId) {
2189
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, chatType, undefined, undefined, this.resolveChannelType(channel));
2264
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, chatType, undefined, resolvedSelfAID, this.resolveChannelType(channel));
2190
2265
  }
2191
2266
  else {
2192
2267
  session = await this.sessionManager.getActiveSession(channel, channelId);
2193
2268
  }
2194
2269
  // 如果没有会话,自动创建(所有后续命令都需要 session)
2195
2270
  if (!session) {
2196
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, chatType, undefined, undefined, this.resolveChannelType(channel));
2271
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, chatType, undefined, resolvedSelfAID, this.resolveChannelType(channel));
2197
2272
  }
2198
2273
  // /status 命令:显示会话状态
2199
2274
  if (normalizedContent === '/status') {
@@ -2400,7 +2475,7 @@ export class CommandHandler {
2400
2475
  const executeRestart = async () => {
2401
2476
  let replyContext;
2402
2477
  if (threadId) {
2403
- const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
2478
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, undefined, undefined, selfAID ?? this.resolveSelfAID(channel), this.resolveChannelType(channel));
2404
2479
  replyContext = this.getReplyContext(threadSession);
2405
2480
  }
2406
2481
  const restartInfo = {
@@ -2417,6 +2492,27 @@ export class CommandHandler {
2417
2492
  env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
2418
2493
  }).unref();
2419
2494
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
2495
+ // 先发送重启反馈消息,等待发送完成后再 kill 进程
2496
+ // 避免消息还没发出去进程就退出了
2497
+ const adapter = this.adapters.get(channel);
2498
+ if (adapter) {
2499
+ try {
2500
+ const envelope = buildEnvelope({
2501
+ taskId: `restart-${Date.now()}`,
2502
+ channel,
2503
+ channelId,
2504
+ agentName: 'system',
2505
+ chatmode: 'interactive',
2506
+ replyContext,
2507
+ });
2508
+ await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
2509
+ // 等待消息发送完成后再延迟 kill
2510
+ await new Promise(resolve => setTimeout(resolve, 500));
2511
+ }
2512
+ catch (err) {
2513
+ logger.error('[System] Failed to send restart notification:', err);
2514
+ }
2515
+ }
2420
2516
  // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
2421
2517
  // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
2422
2518
  // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
@@ -2447,7 +2543,8 @@ export class CommandHandler {
2447
2543
  }
2448
2544
  }
2449
2545
  await executeRestart();
2450
- return { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' };
2546
+ // executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
2547
+ return null;
2451
2548
  }
2452
2549
  // /upgrade 命令:检查版本更新,提示用户手动重启
2453
2550
  if (normalizedContent === '/upgrade') {
@@ -2651,7 +2748,7 @@ export class CommandHandler {
2651
2748
  }
2652
2749
  // /slist — 仅显示 EvolClaw 会话
2653
2750
  const sessions = await this.sessionManager.listSessions(channel, channelId);
2654
- const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
2751
+ const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId?.startsWith('trigger-'));
2655
2752
  // 从 SDK 同步会话名称(发现 CLI 改名)
2656
2753
  try {
2657
2754
  const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
@@ -2829,6 +2926,8 @@ export class CommandHandler {
2829
2926
  if (!switched) {
2830
2927
  return { kind: 'command.error', text: `❌ 切换会话失败` };
2831
2928
  }
2929
+ if (source === 'card-trigger')
2930
+ return null;
2832
2931
  return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
2833
2932
  }
2834
2933
  if (targetSession.id === session.id) {
@@ -2844,6 +2943,8 @@ export class CommandHandler {
2844
2943
  }
2845
2944
  this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
2846
2945
  const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
2946
+ if (source === 'card-trigger')
2947
+ return null;
2847
2948
  return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}` };
2848
2949
  }
2849
2950
  // /rename 或 /name 命令:重命名当前会话
@@ -3030,12 +3131,12 @@ export class CommandHandler {
3030
3131
  }
3031
3132
  // /trigger 命令
3032
3133
  if (normalizedContent === '/trigger' || normalizedContent.startsWith('/trigger ')) {
3033
- const text = this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin);
3134
+ const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId);
3034
3135
  return { kind: 'command.result', text };
3035
3136
  }
3036
3137
  return null;
3037
3138
  }
3038
- handleTrigger(content, channel, channelId, peerId, isAdmin) {
3139
+ async handleTrigger(content, channel, channelId, peerId, isAdmin, messageId) {
3039
3140
  // Resolve trigger manager/scheduler from the owning agent of this channel
3040
3141
  const owningAgent = this.getOwningAgent(channel);
3041
3142
  const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
@@ -3166,6 +3267,7 @@ export class CommandHandler {
3166
3267
  nextFireAt,
3167
3268
  targetChannel: parsed.targetChannel ?? channel,
3168
3269
  targetChannelId: parsed.targetChannelId ?? channelId,
3270
+ targetChannelType: this.resolveChannelType(parsed.targetChannel ?? channel),
3169
3271
  targetThreadId: parsed.targetThreadId,
3170
3272
  targetSessionStrategy: parsed.targetSessionStrategy,
3171
3273
  agentId: parsed.agentId,
@@ -3177,6 +3279,32 @@ export class CommandHandler {
3177
3279
  updatedAt: now,
3178
3280
  };
3179
3281
  try {
3282
+ // Strategy-based session binding
3283
+ if (parsed.targetSessionStrategy === 'current') {
3284
+ const active = await this.sessionManager.getActiveSession(channel, channelId);
3285
+ if (!active)
3286
+ return '❌ 当前没有活跃会话,改用 --session latest 或 thread';
3287
+ trigger.boundSessionId = active.id;
3288
+ }
3289
+ else if (parsed.targetSessionStrategy === 'thread') {
3290
+ const targetAdapterName = parsed.targetChannel ?? channel;
3291
+ const adapter = this.adapters.get(targetAdapterName);
3292
+ if (!adapter?.capabilities.thread)
3293
+ return '❌ 目标渠道不支持 thread 会话';
3294
+ const channelType = adapter.channelKey.split('#')[0];
3295
+ trigger.targetChannelType = channelType;
3296
+ if (channelType === 'aun') {
3297
+ trigger.threadKind = 'aun';
3298
+ trigger.targetThreadId = `trigger-${trigger.id}`;
3299
+ }
3300
+ else {
3301
+ if (!messageId)
3302
+ return '❌ 飞书 thread 模式需要消息 ID,请重新发送命令';
3303
+ trigger.threadKind = 'feishu';
3304
+ trigger.rootMessageId = messageId;
3305
+ trigger.pendingThread = true;
3306
+ }
3307
+ }
3180
3308
  // Validate name uniqueness before persisting (manager.register writes to disk)
3181
3309
  // scheduler.register is in-memory only and cannot fail, so order is safe here.
3182
3310
  // If manager.register throws (duplicate name/ID), nothing is persisted.