evolclaw 2.8.3 → 3.0.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 (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -1,11 +1,15 @@
1
1
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
2
2
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
3
- import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
3
+ import { renderCommandCardAsText } from './interaction-router.js';
4
+ import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
5
+ import { resolvePaths, getPackageRoot } from '../paths.js';
4
6
  import { logger } from '../utils/logger.js';
5
7
  import crypto from 'crypto';
6
8
  import path from 'path';
7
9
  import fs from 'fs';
8
10
  import os from 'os';
11
+ import { parseTriggerSet } from './trigger/parser.js';
12
+ import { calcNextFireAt } from './trigger/scheduler.js';
9
13
  const allEfforts = ['low', 'medium', 'high', 'max'];
10
14
  const nonMaxEfforts = allEfforts.filter(e => e !== 'max');
11
15
  function getAvailableEfforts(agent, model) {
@@ -104,7 +108,7 @@ function formatIdleTime(ms) {
104
108
  return '刚刚';
105
109
  }
106
110
  // 支持的命令列表
107
- 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', '/aid', '/agentmd', '/chatmode', '/ask', '/resume'];
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'];
108
112
  // 命令别名映射
109
113
  const aliases = {
110
114
  '/p': '/project',
@@ -113,10 +117,9 @@ const aliases = {
113
117
  '/rw': '/rewind'
114
118
  };
115
119
  // 命令快速路径前缀(所有命令都不进入消息队列)
116
- 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', '/aid', '/agentmd', '/ask', '/resume'];
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'];
117
121
  export class CommandHandler {
118
122
  sessionManager;
119
- config;
120
123
  messageCache;
121
124
  eventBus;
122
125
  adapters = new Map();
@@ -129,8 +132,10 @@ export class CommandHandler {
129
132
  interactionRouter;
130
133
  statsCollector;
131
134
  agentMap;
132
- defaultAgentId;
135
+ primaryRunnerKey;
133
136
  agentRegistry;
137
+ triggerScheduler;
138
+ triggerManager;
134
139
  /**
135
140
  * Get the runner for a (channel, baseagent) pair.
136
141
  *
@@ -139,18 +144,18 @@ export class CommandHandler {
139
144
  */
140
145
  getAgent(channel, baseagent) {
141
146
  if (channel && baseagent) {
142
- const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
147
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
143
148
  const key = `${evolName}::${baseagent}`;
144
149
  if (this.agentMap.has(key))
145
150
  return this.agentMap.get(key);
146
151
  }
147
- if (this.agentMap.has(this.defaultAgentId))
148
- return this.agentMap.get(this.defaultAgentId);
152
+ if (this.agentMap.has(this.primaryRunnerKey))
153
+ return this.agentMap.get(this.primaryRunnerKey);
149
154
  return this.agentMap.values().next().value;
150
155
  }
151
156
  /** Return the list of baseagents available to a given channel (per-EvolAgent isolation). */
152
157
  getAvailableBaseagents(channel) {
153
- const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
158
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
154
159
  const prefix = `${evolName}::`;
155
160
  const result = [];
156
161
  for (const key of this.agentMap.keys()) {
@@ -159,48 +164,49 @@ export class CommandHandler {
159
164
  }
160
165
  return result;
161
166
  }
162
- /** Extract the baseagent component from `defaultAgentId` (e.g. `[default]::claude` → `claude`). */
167
+ /** Extract the baseagent component from `primaryRunnerKey` (e.g. `aid::claude` → `claude`). */
163
168
  parseDefaultBaseagent() {
164
- const idx = this.defaultAgentId.indexOf('::');
165
- return idx >= 0 ? this.defaultAgentId.slice(idx + 2) : this.defaultAgentId;
169
+ const idx = this.primaryRunnerKey.indexOf('::');
170
+ return idx >= 0 ? this.primaryRunnerKey.slice(idx + 2) : this.primaryRunnerKey;
166
171
  }
167
- constructor(sessionManager, agentRunnerOrMap, config, messageCache, eventBus, defaultAgentId) {
172
+ constructor(sessionManager, agentRunnerOrMap, messageCache, eventBus, primaryRunnerKey) {
168
173
  this.sessionManager = sessionManager;
169
- this.config = config;
170
174
  this.messageCache = messageCache;
171
175
  this.eventBus = eventBus;
172
176
  if (agentRunnerOrMap instanceof Map) {
173
177
  this.agentMap = agentRunnerOrMap;
174
- this.defaultAgentId = defaultAgentId || '[default]::claude';
178
+ this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
175
179
  }
176
180
  else {
177
- // Backward-compat single-runner path: treat as DefaultAgent's claude.
178
- this.agentMap = new Map([[`[default]::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
179
- this.defaultAgentId = `[default]::${agentRunnerOrMap.name}`;
181
+ // 测试 / 单 runner 路径:占位 agent name '<unknown>'
182
+ this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
183
+ this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
180
184
  }
181
185
  }
182
186
  /** 注入 EvolAgentRegistry,用于判断通道是否被 EvolAgent 管理 */
183
187
  setAgentRegistry(registry) {
184
188
  this.agentRegistry = registry;
185
189
  }
186
- /** 返回管理当前通道的 EvolAgent(非 default),无则返回 null */
190
+ /** 注入触发器调度器(由 index.ts 在初始化后调用) */
191
+ setTriggerScheduler(scheduler, manager) {
192
+ this.triggerScheduler = scheduler;
193
+ this.triggerManager = manager;
194
+ }
195
+ /** 返回管理当前通道的 EvolAgent,无则返回 null */
187
196
  getOwningAgent(channel) {
188
197
  if (!this.agentRegistry)
189
198
  return null;
190
- const agent = this.agentRegistry.resolveByChannel(channel);
191
- if (!agent || agent.isDefault)
192
- return null;
193
- return agent;
199
+ return this.agentRegistry.resolveByChannel(channel);
194
200
  }
195
- /** 返回当前通道的有效项目路径:agent-owned agent.projectPath;否则用全局 defaultPath。*/
201
+ /** 返回当前通道的有效项目路径:从 owning agent 取。*/
196
202
  getEffectiveDefaultPath(channel) {
197
203
  const owning = this.getOwningAgent(channel);
198
204
  if (owning)
199
205
  return owning.projectPath;
200
- return this.config.projects?.defaultPath || process.cwd();
206
+ return process.cwd();
201
207
  }
202
208
  /**
203
- * 返回当前通道有效的 projects.list(agent-owned agent.json 的;否则全局 evolclaw.json 的)。
209
+ * 返回当前通道有效的 projects.list(从 owning agent config 取)。
204
210
  * 都没配 list 时回退到 defaultPath 单项目。
205
211
  */
206
212
  getEffectiveProjects(channel) {
@@ -211,39 +217,24 @@ export class CommandHandler {
211
217
  return this.projects;
212
218
  }
213
219
  /**
214
- * 添加项目到当前通道范围(agent-owned agent.json;default evolclaw.json)。
220
+ * 添加项目到当前通道范围(写到 owning agent config.json)。
215
221
  */
216
222
  async addProjectInScope(channel, name, projectPath) {
217
223
  const owning = this.getOwningAgent(channel);
218
- if (owning) {
219
- try {
220
- owning.addProject(name, projectPath);
221
- }
222
- catch (e) {
223
- return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
224
- }
225
- return undefined;
226
- }
227
- if (!this.config.projects) {
228
- this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
224
+ if (!owning) {
225
+ return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
229
226
  }
230
- if (!this.config.projects.list) {
231
- this.config.projects.list = {};
232
- }
233
- this.config.projects.list[name] = projectPath;
234
227
  try {
235
- const { saveConfig } = await import('../config.js');
236
- saveConfig(this.config);
228
+ owning.addProject(name, projectPath);
237
229
  }
238
230
  catch (e) {
239
- return `⚠️ 写入 evolclaw.json 失败: ${e?.message || e}`;
231
+ return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
240
232
  }
241
- // Refresh in-memory list cache (this.projects getter reads from this.config)
242
233
  return undefined;
243
234
  }
244
235
  /**
245
- * 持久化 baseagent.model:agent-owned 写到 agent.json;否则写 evolclaw.json ~/.claude/settings.json。
246
- * 返回错误信息或 undefined。
236
+ * 持久化 baseagent.model:写到 agent config.json;找不到 owning agent 时
237
+ * 退到用户级 ~/.claude/settings.json(Claude 专用)。
247
238
  */
248
239
  persistBaseagentModel(channel, baseagentName, newModel) {
249
240
  const owning = this.getOwningAgent(channel);
@@ -252,42 +243,14 @@ export class CommandHandler {
252
243
  owning.setBaseagentModel(newModel);
253
244
  }
254
245
  catch (e) {
255
- return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
256
- }
257
- return undefined;
258
- }
259
- // DefaultAgent / 无 owning agent:保留原"就近原则"
260
- if (!this.config.agents)
261
- this.config.agents = {};
262
- const isCodex = baseagentName === 'codex';
263
- if (isCodex) {
264
- if (!this.config.agents.codex)
265
- this.config.agents.codex = {};
266
- if (newModel)
267
- this.config.agents.codex.model = newModel;
268
- try {
269
- saveConfig(this.config);
270
- }
271
- catch (e) {
272
- return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
246
+ return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
273
247
  }
274
248
  return undefined;
275
249
  }
276
- const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
277
- if (configuredInEvolclaw) {
278
- if (!this.config.agents.claude)
279
- this.config.agents.claude = {};
280
- if (newModel)
281
- this.config.agents.claude.model = newModel;
282
- try {
283
- saveConfig(this.config);
284
- }
285
- catch (e) {
286
- return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
287
- }
288
- return undefined;
250
+ // owning agent(罕见,新结构下应当不会发生)→ Claude 走用户级 fallback
251
+ if (baseagentName !== 'claude') {
252
+ return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
289
253
  }
290
- // Fallback: ~/.claude/settings.json
291
254
  const updates = {};
292
255
  if (newModel)
293
256
  updates.model = newModel;
@@ -298,7 +261,7 @@ export class CommandHandler {
298
261
  return undefined;
299
262
  }
300
263
  /**
301
- * 持久化 baseagent.effortagent-owned 写到 agent.json;否则就近原则。
264
+ * 持久化 baseagent.effort:写到 agent config.json;找不到时退到用户级 settings。
302
265
  */
303
266
  persistBaseagentEffort(channel, baseagentName, newEffort) {
304
267
  const owning = this.getOwningAgent(channel);
@@ -307,57 +270,12 @@ export class CommandHandler {
307
270
  owning.setBaseagentEffort(newEffort);
308
271
  }
309
272
  catch (e) {
310
- return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
311
- }
312
- return undefined;
313
- }
314
- if (!this.config.agents)
315
- this.config.agents = {};
316
- const isCodex = baseagentName === 'codex';
317
- if (isCodex) {
318
- if (newEffort === undefined) {
319
- if (this.config.agents.codex?.reasoning) {
320
- delete this.config.agents.codex.reasoning;
321
- try {
322
- saveConfig(this.config);
323
- }
324
- catch { }
325
- }
326
- }
327
- else {
328
- if (!this.config.agents.codex)
329
- this.config.agents.codex = {};
330
- this.config.agents.codex.reasoning = newEffort;
331
- try {
332
- saveConfig(this.config);
333
- }
334
- catch (e) {
335
- return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
336
- }
273
+ return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
337
274
  }
338
275
  return undefined;
339
276
  }
340
- const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
341
- if (configuredInEvolclaw) {
342
- if (newEffort === undefined) {
343
- delete this.config.agents.claude.effort;
344
- try {
345
- saveConfig(this.config);
346
- }
347
- catch { }
348
- }
349
- else {
350
- if (!this.config.agents.claude)
351
- this.config.agents.claude = {};
352
- this.config.agents.claude.effort = newEffort;
353
- try {
354
- saveConfig(this.config);
355
- }
356
- catch (e) {
357
- return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
358
- }
359
- }
360
- return undefined;
277
+ if (baseagentName !== 'claude') {
278
+ return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
361
279
  }
362
280
  const updates = { effortLevel: newEffort ?? null };
363
281
  const writeResult = writeUserSettings(updates);
@@ -366,14 +284,8 @@ export class CommandHandler {
366
284
  }
367
285
  return undefined;
368
286
  }
369
- /** 项目列表快捷访问(list 缺失时用 defaultPath 作为唯一项目) */
287
+ /** 项目列表快捷访问(无 channel 上下文时的 fallback,尽量不用) */
370
288
  get projects() {
371
- const list = this.config.projects?.list;
372
- if (list && Object.keys(list).length > 0)
373
- return list;
374
- const dp = this.config.projects?.defaultPath;
375
- if (dp)
376
- return { [path.basename(dp)]: dp };
377
289
  return {};
378
290
  }
379
291
  /** 根据项目路径查找配置中的项目名称 */
@@ -412,72 +324,63 @@ export class CommandHandler {
412
324
  return session.metadata?.replyContext;
413
325
  }
414
326
  /**
415
- * 尝试通过渠道适配器发送交互卡片。
416
- * 返回 message_id 表示卡片已发送,false 表示降级为文本。
327
+ * 发送 CommandCard 卡片。卡片成功返回 null(调用方直接 return),失败返回降级文本。
328
+ * CommandCard 不进 InteractionRouter,按钮点击由 channel 直接构造伪命令入站消息。
329
+ *
330
+ * 走统一 adapter.send(envelope, { kind: 'interaction', ... }) 入口。
417
331
  */
418
- async trySendInteraction(channel, channelId, interaction, replyContext) {
419
- const adapter = this.adapters.get(channel);
420
- if (!adapter?.sendInteraction)
421
- return false;
332
+ async sendCommandCard(opts) {
333
+ const adapter = this.adapters.get(opts.channel);
334
+ if (opts.interaction.kind.kind !== 'command-card') {
335
+ logger.warn(`[CommandHandler] sendCommandCard called with non-CommandCard kind`);
336
+ return null;
337
+ }
338
+ const card = opts.interaction.kind;
339
+ if (opts.canWrite === false)
340
+ return renderCommandCardAsText(card);
341
+ if (!adapter?.send)
342
+ return renderCommandCardAsText(card);
343
+ // session 忙碌时降级到文本,避免并发触发带参写操作
344
+ if (this.isSessionBusy(opts.interaction.sessionId))
345
+ return renderCommandCardAsText(card);
422
346
  try {
423
- return await adapter.sendInteraction(channelId, interaction, replyContext);
347
+ const envelope = buildEnvelope({
348
+ channel: opts.channel,
349
+ channelId: opts.channelId,
350
+ agentName: this.agentRegistry?.resolveByChannel(opts.channel)?.name,
351
+ replyContext: opts.replyCtx,
352
+ });
353
+ const fallbackText = renderCommandCardAsText(card);
354
+ const messageId = await sendInteractionPayload(adapter, envelope, opts.interaction, fallbackText, opts.replyCtx);
355
+ if (messageId)
356
+ return null;
424
357
  }
425
358
  catch (e) {
426
- logger.warn(`[CommandHandler] sendInteraction failed: ${e}`);
427
- return false;
428
- }
429
- }
430
- /** 作废某 session 下所有 pending 交互卡片(PATCH 禁用 + cancel) */
431
- async invalidateOldCards(channel, sessionId) {
432
- if (!this.interactionRouter)
433
- return;
434
- const adapter = this.adapters.get(channel);
435
- const pending = this.interactionRouter.getPending(sessionId);
436
- if (pending.length === 0)
437
- return;
438
- const disabledCard = {
439
- config: { wide_screen_mode: true },
440
- header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
441
- elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
442
- };
443
- for (const id of pending) {
444
- const msgId = this.interactionRouter.getMessageId(id);
445
- if (msgId && adapter?.patchInteractionCard) {
446
- adapter.patchInteractionCard(msgId, disabledCard).catch(() => { });
447
- }
448
- this.interactionRouter.cancel(id);
359
+ logger.warn(`[CommandHandler] sendCommandCard failed: ${e}`);
449
360
  }
361
+ return renderCommandCardAsText(card);
450
362
  }
451
363
  /**
452
- * 发送交互卡片并注册回调。作废旧卡片 发送新卡片 注册到 interactionRouter。
453
- * 返回 true 表示卡片已发送(调用方应 return null),false 表示降级到文本。
364
+ * 通用降级应答入口:按 (sessionId, fallbackCommand) 查找 pending interaction 并路由。
365
+ * 返回 { matched: true } 表示已处理,调用方直接返回 result。
454
366
  */
455
- async sendInteractionCard(opts) {
367
+ async handleInteractionFallback(command, args, sessionId, userId) {
456
368
  if (!this.interactionRouter)
457
- return false;
458
- // 无写权限 走文本降级(由调用点 fall through 输出只读信息)
459
- if (opts.canWrite === false)
460
- return false;
461
- // 有写权限但此刻忙碌 也走文本降级(避免诱导用户在忙碌状态下触发带参写操作)
462
- if (this.isSessionBusy(opts.sessionId))
463
- return false;
464
- await this.invalidateOldCards(opts.channel, opts.sessionId);
465
- const messageId = await this.trySendInteraction(opts.channel, opts.channelId, opts.interaction, opts.replyCtx);
466
- if (!messageId)
467
- return false;
468
- const wrappedCallback = async (action, values, operatorId) => {
469
- // 点击回调时二次校验:若会话此刻忙碌,忽略本次点击(防止已弹卡片被用于带参切换)
470
- if (this.isSessionBusy(opts.sessionId)) {
471
- const adapter = this.adapters.get(opts.channel);
472
- adapter?.sendText(opts.channelId, '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试', opts.replyCtx);
473
- return;
474
- }
475
- await opts.callback(action, values, operatorId);
476
- // 已完成交互的卡片:保留原始内容,仅禁用按钮(不标记为"已过期")
477
- // "已过期"仅用于被新卡片替代的旧卡片(invalidateOldCards)
478
- };
479
- this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
480
- return true;
369
+ return { matched: false };
370
+ const pendingId = this.interactionRouter.findPendingByCommand(sessionId, command);
371
+ if (!pendingId)
372
+ return { matched: false };
373
+ const initiatorId = this.interactionRouter.getInitiator(pendingId);
374
+ if (initiatorId && userId && initiatorId !== userId) {
375
+ return { matched: true, result: '⚠️ 仅卡片发起者可应答' };
376
+ }
377
+ this.interactionRouter.handle({
378
+ type: 'interaction.response',
379
+ id: pendingId,
380
+ action: args,
381
+ operatorId: userId,
382
+ });
383
+ return { matched: true, result: '✓ 已回答' };
481
384
  }
482
385
  /** 判断指定 session 是否有活跃流(用于 idle 守卫和卡片降级) */
483
386
  isSessionBusy(sessionId) {
@@ -487,8 +390,8 @@ export class CommandHandler {
487
390
  }
488
391
  return false;
489
392
  }
490
- /** 获取活跃会话,无会话时返回统一错误提示 */
491
- async ensureSession(channel, channelId, threadId) {
393
+ /** 获取活跃会话,无会话时自动创建(话题除外) */
394
+ async ensureSession(channel, channelId, threadId, chatType) {
492
395
  if (threadId) {
493
396
  // 话题会话:仅查询,不创建
494
397
  const session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
@@ -497,9 +400,13 @@ export class CommandHandler {
497
400
  }
498
401
  return { session };
499
402
  }
500
- const session = await this.sessionManager.getActiveSession(channel, channelId);
501
- if (!session) {
502
- return { error: '❌ 当前没有活跃会话\n使用 /new 创建新会话' };
403
+ const ct = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
404
+ const session = await this.sessionManager.getActiveSession(channel, channelId)
405
+ ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct);
406
+ // 如果 session 已存在但 chatType 跟传入的不一致,更新
407
+ if (ct && session.chatType !== ct) {
408
+ await this.sessionManager.updateSession(session.id, { chatType: ct });
409
+ session.chatType = ct;
503
410
  }
504
411
  return { session };
505
412
  }
@@ -617,6 +524,10 @@ export class CommandHandler {
617
524
  { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
618
525
  { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
619
526
  ] } },
527
+ { cmd: '/dispatch', label: '切换分发模式', desc: '控制群聊消息过滤(仅@提及或广播响应)', next: { type: 'select', items: [
528
+ { value: 'mention', label: '@ 提及', desc: '仅在被 @ 提及时响应' },
529
+ { value: 'all', label: '广播', desc: '响应群内所有消息' },
530
+ ] } },
620
531
  ]
621
532
  });
622
533
  items.push({
@@ -654,14 +565,6 @@ export class CommandHandler {
654
565
  ] : []),
655
566
  ...(isOwner ? [
656
567
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
657
- { cmd: '/aid', label: 'AID 身份管理', desc: '管理本地 AID 身份(创建/列表)', next: { type: 'select', items: [
658
- { value: 'list', label: '列表', desc: '列出本地所有 AID' },
659
- { value: 'new', label: '创建', desc: '创建新 AID 身份', next: { type: 'text' } },
660
- ] } },
661
- { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
662
- { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
663
- { value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
664
- ] } },
665
568
  ] : []),
666
569
  ]
667
570
  });
@@ -708,7 +611,7 @@ export class CommandHandler {
708
611
  }
709
612
  if (cmd === '/p') {
710
613
  // Use agent-scoped project list: agent-owned channels see their agent.json's
711
- // projects.list; default channel sees evolclaw.json's projects.list
614
+ // projects.list; default channel sees agent config's projects.list
712
615
  const list = this.getEffectiveProjects(channel);
713
616
  return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
714
617
  }
@@ -725,10 +628,6 @@ export class CommandHandler {
725
628
  return null;
726
629
  }
727
630
  if (cmd === '/restart') {
728
- // /restart 是服务级操作(重连/重启进程),仅限 default 通道。
729
- // EvolAgent 通道返回空菜单(用户在 agent-owned 通道上无可选项)
730
- if (this.getOwningAgent(channel))
731
- return [];
732
631
  const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
733
632
  // 列出所有 channel type
734
633
  const visibleTypes = new Set();
@@ -791,6 +690,27 @@ export class CommandHandler {
791
690
  return { error: '无权限:群聊中仅管理员可切换' };
792
691
  }
793
692
  await this.sessionManager.updateSession(session.id, { sessionMode: arg });
693
+ this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
694
+ return { data: { mode: arg } };
695
+ }
696
+ if (cmdBase === '/dispatch') {
697
+ const currentMode = session.metadata?.dispatchMode || 'mention';
698
+ if (mode === 'query') {
699
+ return { data: { mode: currentMode } };
700
+ }
701
+ // update
702
+ if (!arg)
703
+ return { error: '缺少目标模式' };
704
+ if (arg !== 'mention' && arg !== 'all')
705
+ return { error: `无效模式: ${arg}` };
706
+ const identity = this.sessionManager.resolveIdentity(channel, userId);
707
+ const chatType = session.chatType || 'private';
708
+ if (chatType === 'group' && identity.role !== 'owner' && identity.role !== 'admin') {
709
+ return { error: '无权限:群聊中仅管理员可切换' };
710
+ }
711
+ const metadata = { ...(session.metadata || {}), dispatchMode: arg };
712
+ await this.sessionManager.updateSession(session.id, { metadata });
713
+ this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
794
714
  return { data: { mode: arg } };
795
715
  }
796
716
  return { error: `不支持 exec 模式: ${cmdBase}` };
@@ -801,7 +721,11 @@ export class CommandHandler {
801
721
  /**
802
722
  * 主命令处理入口
803
723
  */
804
- async handle(content, channel, channelId, sendMessage, userId, threadId) {
724
+ async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
725
+ const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source);
726
+ return result;
727
+ }
728
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
805
729
  // 解析身份(按实例名)
806
730
  const identity = this.sessionManager.resolveIdentity(channel, userId);
807
731
  const policy = this.getPolicy(channel);
@@ -825,7 +749,7 @@ export class CommandHandler {
825
749
  const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del', '/agent'];
826
750
  const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
827
751
  if (isBlocked) {
828
- return '⚠️ 话题中不支持此命令';
752
+ return { kind: 'command.error', text: '⚠️ 话题中不支持此命令' };
829
753
  }
830
754
  }
831
755
  // Agent-owned 通道:禁止项目切换和 agent 切换
@@ -836,10 +760,10 @@ export class CommandHandler {
836
760
  normalizedContent === '/plist' ||
837
761
  normalizedContent === '/p' || normalizedContent.startsWith('/p ');
838
762
  if (isProjectCmd) {
839
- return `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}`;
763
+ return { kind: 'command.error', text: `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}` };
840
764
  }
841
765
  if (normalizedContent.startsWith('/agent ')) {
842
- return `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}`;
766
+ return { kind: 'command.error', text: `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}` };
843
767
  }
844
768
  }
845
769
  // 权限检查:区分用户级命令和管理级命令
@@ -849,9 +773,9 @@ export class CommandHandler {
849
773
  if (normalizedContent.startsWith('/')) {
850
774
  // guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
851
775
  const guestGroupCommands = [
852
- '/status', '/help', '/evolhelp', '/check', '/chatmode',
776
+ '/status', '/help', '/evolhelp', '/check', '/chatmode', '/dispatch',
853
777
  '/model', '/setmodel', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
854
- '/resume',
778
+ '/resume', '/trigger',
855
779
  ];
856
780
  const userCommands = activeChatType === 'group' && !isAdmin
857
781
  ? guestGroupCommands
@@ -862,9 +786,9 @@ export class CommandHandler {
862
786
  ];
863
787
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
864
788
  if (!isUserCommand && !isAdmin) {
865
- return activeChatType === 'group'
866
- ? '❌ 无权限:当前群聊仅支持 /status 和 /help'
867
- : '❌ 无权限:此命令仅限管理员使用';
789
+ return { kind: 'command.error', text: activeChatType === 'group'
790
+ ? '❌ 无权限:当前群聊仅支持 /status 和 /help'
791
+ : '❌ 无权限:此命令仅限管理员使用' };
868
792
  }
869
793
  }
870
794
  // 空闲检查:某些命令需要等待当前会话空闲
@@ -872,6 +796,7 @@ export class CommandHandler {
872
796
  // - 始终需要 idle(无参即写):/new /clear /compact /repair /fork
873
797
  // - 仅带参时需要 idle(无参是列表/用法):/session /bind /project /agent /rewind
874
798
  // - /chatmode:在 handler 内部自行做写操作的 idle 检查
799
+ // - /dispatch:在 handler 内部自行做写操作的 idle 检查
875
800
  // - /safe:已禁用 no-op,不再要求 idle
876
801
  const idleAlways = ['/new', '/clear', '/compact', '/repair', '/fork'];
877
802
  const idleWhenArg = ['/session', '/bind', '/project', '/agent', '/rewind'];
@@ -884,12 +809,12 @@ export class CommandHandler {
884
809
  if (threadSession) {
885
810
  const threadAgent = this.getAgent(channel, threadSession.agentId);
886
811
  if (threadAgent.hasActiveStream(threadSession.id)) {
887
- return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
812
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
888
813
  }
889
814
  }
890
815
  }
891
816
  else if (activeSession && agent.hasActiveStream(activeSession.id)) {
892
- return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
817
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
893
818
  }
894
819
  }
895
820
  // 检查是否以 / 开头(可能是命令)
@@ -902,10 +827,10 @@ export class CommandHandler {
902
827
  return distance <= 2;
903
828
  });
904
829
  if (similar) {
905
- return `❌ 未知命令: ${inputCmd}\n💡 你是不是想输入: ${similar}\n\n输入 /help 查看所有可用命令`;
830
+ return { kind: 'command.error', text: `❌ 未知命令: ${inputCmd}\n💡 你是不是想输入: ${similar}\n\n输入 /help 查看所有可用命令` };
906
831
  }
907
832
  else {
908
- return `❌ 未知命令: ${inputCmd}\n\n输入 /help 查看所有可用命令`;
833
+ return { kind: 'command.error', text: `❌ 未知命令: ${inputCmd}\n\n输入 /help 查看所有可用命令` };
909
834
  }
910
835
  }
911
836
  }
@@ -923,7 +848,7 @@ export class CommandHandler {
923
848
  ' /check - 检查渠道健康',
924
849
  ' /help - 显示此帮助信息',
925
850
  ];
926
- return lines.join('\n');
851
+ return { kind: 'command.result', text: lines.join('\n') };
927
852
  }
928
853
  if (!isAdmin) {
929
854
  const lines = [
@@ -940,7 +865,7 @@ export class CommandHandler {
940
865
  '❓ 帮助:',
941
866
  ' /help - 显示此帮助信息',
942
867
  ];
943
- return lines.join('\n');
868
+ return { kind: 'command.result', text: lines.join('\n') };
944
869
  }
945
870
  // admin+ 基础命令
946
871
  const lines = [
@@ -965,6 +890,11 @@ export class CommandHandler {
965
890
  ' /model [model] - 查看或切换模型',
966
891
  ' /effort [level] - 查看或切换推理强度',
967
892
  '',
893
+ '💬 聊天设置:',
894
+ ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
895
+ ' /chatmode [interactive|proactive] - 查看/切换会话模式(被动响应或主动推进)',
896
+ ' /dispatch [mention|all] - 查看/切换群聊分发模式(仅@响应或广播响应,仅群聊)',
897
+ '',
968
898
  '🔐 权限管理:',
969
899
  ' /perm - 查看当前权限模式',
970
900
  ...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
@@ -974,21 +904,24 @@ export class CommandHandler {
974
904
  ' /status - 显示会话状态',
975
905
  ' /stop - 中断当前任务',
976
906
  ' /check - 检查渠道状态',
977
- ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
978
907
  ...(isAdmin ? [
979
908
  ' /restart <type> - 重连该类型所有渠道实例(服务级,admin+)',
980
909
  ] : []),
981
910
  ...(isOwner ? [
982
911
  ' /restart - 重启服务',
912
+ ] : []),
913
+ ...(isOwner ? [
914
+ '',
915
+ '🧰 工具:',
983
916
  ' /file [channel] <path> - 发送项目内文件',
984
- ' /aid [list|new <aid>] - AID 身份管理',
985
- ' /agentmd [put|set <内容>] - 管理 agent.md',
917
+ ' /aid [list|show|new|delete|lookup|agentmd] - AID 身份管理',
918
+ ' /storage [upload|download|ls|rm|quota] <aid> - 文件存储',
986
919
  ] : []),
987
920
  '',
988
921
  '❓ 帮助:',
989
922
  ' /help - 显示此帮助信息',
990
923
  ];
991
- return lines.join('\n');
924
+ return { kind: 'command.result', text: lines.join('\n') };
992
925
  }
993
926
  // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
994
927
  if (normalizedContent === '/evolhelp') {
@@ -1013,7 +946,6 @@ export class CommandHandler {
1013
946
  if (isAdmin) {
1014
947
  cmds.push({ command: '/agent', args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1015
948
  cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1016
- cmds.push({ command: '/setmodel', description: '返回 JSON 格式的模型列表(供程序解析)', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1017
949
  cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1018
950
  }
1019
951
  // 权限管理
@@ -1026,86 +958,69 @@ export class CommandHandler {
1026
958
  cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
1027
959
  cmds.push({ command: '/check', description: '检查渠道状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
1028
960
  if (isAdmin) {
1029
- cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '运维', roles: ['admin', 'owner'] });
961
+ cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '聊天设置', roles: ['admin', 'owner'] });
1030
962
  cmds.push({ command: '/restart', args: '<channel>', description: '重连指定渠道', category: '运维', roles: ['admin', 'owner'] });
1031
963
  }
1032
964
  if (isOwner) {
1033
965
  cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['owner'] });
1034
- cmds.push({ command: '/file', args: '[channel] <path>', description: '发送项目内文件', category: '运维', roles: ['owner'] });
1035
- cmds.push({ command: '/aid', args: '[list|new <aid>]', description: 'AID 管理', category: '运维', roles: ['owner'] });
1036
- cmds.push({ command: '/agentmd', args: '[put|set <内容>]', description: '管理 agent.md', category: '运维', roles: ['owner'] });
966
+ cmds.push({ command: '/file', args: '[channel] <path>', description: '发送项目内文件', category: '工具', roles: ['owner'] });
967
+ cmds.push({ command: '/aid', args: '[list|show|new|delete|lookup|agentmd]', description: 'AID 身份管理', category: '工具', roles: ['owner'] });
968
+ cmds.push({ command: '/storage', args: '[upload|download|ls|rm|quota] <aid>', description: '文件存储', category: '工具', roles: ['owner'] });
1037
969
  }
1038
- // 会话模式
970
+ // 聊天设置
1039
971
  if (isAdmin) {
1040
- cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '会话管理', roles: ['admin', 'owner'] });
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'] });
1041
974
  }
1042
975
  // 交互
1043
976
  cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['guest', 'admin', 'owner'] });
1044
- cmds.push({ command: '/resume', description: '查看当前项目的 Claude 会话记录', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
1045
977
  // 帮助
1046
978
  cmds.push({ command: '/help', description: '显示帮助信息', category: '帮助', roles: ['guest', 'admin', 'owner'] });
1047
- cmds.push({ command: '/evolhelp', description: '返回 JSON 格式命令列表', category: '帮助', roles: ['guest', 'admin', 'owner'] });
1048
979
  const categories = [...new Set(cmds.map(c => c.category))];
1049
- return JSON.stringify({ commands: cmds, categories });
980
+ return { kind: 'command.result', text: JSON.stringify({ commands: cmds, categories }) };
1050
981
  }
1051
982
  // /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
1052
983
  if (normalizedContent.startsWith('/perm')) {
1053
984
  const args = normalizedContent.slice(5).trim();
1054
985
  // 先获取正确的 session 和 agent(话题可能用不同 agent)
1055
- const permResult = await this.ensureSession(channel, channelId, threadId);
986
+ const permResult = await this.ensureSession(channel, channelId, threadId, chatType);
1056
987
  if ('error' in permResult)
1057
- return permResult.error;
988
+ return { kind: 'command.result', text: permResult.error };
1058
989
  const { session: permSession } = permResult;
1059
990
  const permAgent = this.getAgent(channel, permSession.agentId);
1060
991
  // /perm(无参数):显示当前模式和可选模式
1061
992
  if (!args) {
1062
993
  if (!hasPermissionController(permAgent)) {
1063
- return '❌ 权限控制不可用';
994
+ return { kind: 'command.error', text: '❌ 权限控制不可用' };
1064
995
  }
1065
996
  const currentMode = permSession.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
1066
997
  const modes = permAgent.listModes();
1067
- // 尝试发送交互卡片
1068
- if (this.interactionRouter) {
1069
- const requestId = `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
998
+ // 尝试发送 CommandCard 卡片
999
+ {
1070
1000
  const availableModes = modes.filter(m => m.available);
1071
1001
  const interaction = {
1072
1002
  type: 'interaction',
1073
- id: requestId,
1003
+ id: `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1074
1004
  channelId,
1075
1005
  sessionId: permSession.id,
1006
+ initiatorId: userId,
1076
1007
  kind: {
1077
- kind: 'action',
1008
+ kind: 'command-card',
1078
1009
  title: '🔐 权限模式',
1079
1010
  body: availableModes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
1080
1011
  buttons: availableModes.map(m => ({
1081
- key: m.key,
1082
1012
  label: m.key === currentMode ? `✓ ${m.key}` : m.key,
1083
- style: m.key === currentMode ? 'primary' : 'default',
1013
+ command: `/perm ${m.key}`,
1014
+ style: (m.key === currentMode ? 'primary' : 'default'),
1015
+ disabled: m.key === currentMode,
1084
1016
  })),
1085
1017
  },
1086
1018
  };
1087
1019
  const replyCtx = this.getReplyContext(permSession);
1088
- const cardSent = await this.sendInteractionCard({
1089
- channel, channelId, sessionId: permSession.id, requestId, interaction, replyCtx,
1090
- canWrite: isOwner,
1091
- callback: async (action, _values, operatorId) => {
1092
- if (action !== currentMode) {
1093
- if (userId && operatorId && operatorId !== userId)
1094
- return;
1095
- const result = await this.handle(`/perm ${action}`, channel, channelId, undefined, userId, threadId);
1096
- if (result) {
1097
- const adapter = this.adapters.get(channel);
1098
- adapter?.sendText(channelId, result, replyCtx);
1099
- }
1100
- else {
1101
- // 切换成功后重新发新卡片(会自动 invalidate 旧卡片)
1102
- await this.handle('/perm', channel, channelId, undefined, userId, threadId);
1103
- }
1104
- }
1105
- },
1106
- });
1107
- if (cardSent)
1020
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1021
+ if (cardResult === null)
1108
1022
  return null;
1023
+ return { kind: 'command.result', text: cardResult };
1109
1024
  }
1110
1025
  // 降级:文本
1111
1026
  const modeList = modes.map(m => {
@@ -1114,25 +1029,30 @@ export class CommandHandler {
1114
1029
  return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
1115
1030
  }).join('\n');
1116
1031
  if (isOwner) {
1117
- return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
1032
+ return { kind: 'command.result', text: `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求` };
1118
1033
  }
1119
- return `🔐 当前权限模式: ${currentMode}`;
1034
+ return { kind: 'command.result', text: `🔐 当前权限模式: ${currentMode}` };
1120
1035
  }
1121
1036
  const parts = args.split(/\s+/);
1122
1037
  // /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
1123
1038
  if (parts.length === 1) {
1124
1039
  const arg = parts[0];
1125
- // /perm allow|always|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
1040
+ // /perm allow|always|deny:快捷审批
1041
+ // 优先走 InteractionRouter fallback(统一降级路径)
1126
1042
  if (arg === 'allow' || arg === 'always' || arg === 'deny') {
1043
+ const fb = await this.handleInteractionFallback('perm', arg, permSession.id, userId);
1044
+ if (fb.matched)
1045
+ return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
1046
+ // fallback 不命中:走 permissionGateway 直接审批(兼容旧路径)
1127
1047
  if (!this.permissionGateway) {
1128
- return '❌ 权限审批未启用';
1048
+ return { kind: 'command.error', text: '❌ 权限审批未启用' };
1129
1049
  }
1130
1050
  const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
1131
1051
  if (pendingIds.length === 0) {
1132
- return '❌ 当前没有待审批的权限请求';
1052
+ return { kind: 'command.error', text: '❌ 当前没有待审批的权限请求' };
1133
1053
  }
1134
1054
  if (pendingIds.length > 1) {
1135
- return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}`;
1055
+ return { kind: 'command.error', text: `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}` };
1136
1056
  }
1137
1057
  const requestId = pendingIds[0];
1138
1058
  const decision = arg;
@@ -1142,7 +1062,7 @@ export class CommandHandler {
1142
1062
  always: '✓ 已授权(始终允许该工具),继续执行……',
1143
1063
  deny: '✓ 已拒绝'
1144
1064
  };
1145
- return labels[decision];
1065
+ return { kind: 'command.result', text: labels[decision] };
1146
1066
  }
1147
1067
  // /perm <mode>:切换权限模式
1148
1068
  if (hasPermissionController(permAgent)) {
@@ -1150,56 +1070,51 @@ export class CommandHandler {
1150
1070
  const matched = modes.find(m => m.key === arg);
1151
1071
  if (matched) {
1152
1072
  if (!matched.available) {
1153
- return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
1073
+ return { kind: 'command.error', text: `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}` };
1154
1074
  }
1155
1075
  // guest 和 admin 用户不能切换权限模式(仅 owner)
1156
1076
  if (!isOwner) {
1157
- return '❌ 权限模式切换仅限 owner';
1077
+ return { kind: 'command.error', text: '❌ 权限模式切换仅限 owner' };
1158
1078
  }
1159
1079
  const metadata = permSession.metadata || {};
1160
1080
  metadata.permissionMode = arg;
1161
1081
  await this.sessionManager.updateSession(permSession.id, { metadata });
1162
- return `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}`;
1082
+ return { kind: 'command.result', text: `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}` };
1163
1083
  }
1164
1084
  }
1165
1085
  // 不是已知模式名也不是 allow/deny
1166
1086
  const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
1167
- return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny`;
1087
+ return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny` };
1168
1088
  }
1169
1089
  // 双参数不再支持,提示正确用法
1170
1090
  const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
1171
- return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
1091
+ return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny` };
1172
1092
  }
1173
1093
  // /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
1174
1094
  if (normalizedContent.startsWith('/ask')) {
1175
1095
  const args = normalizedContent.slice(4).trim();
1176
1096
  if (!args) {
1177
- // 无参数:列出当前 pending 的交互请求
1178
- const askResult = await this.ensureSession(channel, channelId, threadId);
1097
+ const askResult = await this.ensureSession(channel, channelId, threadId, chatType);
1179
1098
  if ('error' in askResult)
1180
- return askResult.error;
1099
+ return { kind: 'command.result', text: askResult.error };
1181
1100
  const pendingIds = this.interactionRouter?.getPending(askResult.session.id) || [];
1182
1101
  if (pendingIds.length === 0)
1183
- return '当前没有待回答的问题';
1184
- return `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>`;
1102
+ return { kind: 'command.result', text: '当前没有待回答的问题' };
1103
+ return { kind: 'command.result', text: `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>` };
1185
1104
  }
1186
- const askResult = await this.ensureSession(channel, channelId, threadId);
1105
+ const askResult = await this.ensureSession(channel, channelId, threadId, chatType);
1187
1106
  if ('error' in askResult)
1188
- return askResult.error;
1189
- const { session: askSession } = askResult;
1190
- const pendingIds = this.interactionRouter?.getPending(askSession.id) || [];
1191
- if (pendingIds.length === 0)
1192
- return '❌ 当前没有待回答的问题';
1193
- // 路由到最早的 pending interaction
1194
- const targetId = pendingIds[0];
1195
- this.interactionRouter.handle({ type: 'interaction.response', id: targetId, action: args, operatorId: userId });
1196
- return `✓ 已回答`;
1107
+ return { kind: 'command.result', text: askResult.error };
1108
+ const fb = await this.handleInteractionFallback('ask', args, askResult.session.id, userId);
1109
+ if (fb.matched)
1110
+ return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
1111
+ return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
1197
1112
  }
1198
1113
  // /resume 命令:返回当前项目的 Claude 会话记录(JSON)
1199
1114
  if (normalizedContent === '/resume' || normalizedContent.startsWith('/resume ')) {
1200
- const resumeResult = await this.ensureSession(channel, channelId, threadId);
1115
+ const resumeResult = await this.ensureSession(channel, channelId, threadId, chatType);
1201
1116
  if ('error' in resumeResult)
1202
- return resumeResult.error;
1117
+ return { kind: 'command.result', text: resumeResult.error };
1203
1118
  const { session: resumeSession } = resumeResult;
1204
1119
  try {
1205
1120
  const { encodePath } = await import('../utils/cross-platform.js');
@@ -1207,11 +1122,11 @@ export class CommandHandler {
1207
1122
  const encodedPath = encodePath(resumeSession.projectPath);
1208
1123
  const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1209
1124
  if (!fs.existsSync(projectDir)) {
1210
- return '❌ 未找到 Claude 会话记录目录';
1125
+ return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
1211
1126
  }
1212
1127
  const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
1213
1128
  if (jsonlFiles.length === 0) {
1214
- return '❌ 当前项目没有 Claude 会话记录';
1129
+ return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
1215
1130
  }
1216
1131
  const sessions = [];
1217
1132
  for (const file of jsonlFiles) {
@@ -1271,11 +1186,11 @@ export class CommandHandler {
1271
1186
  });
1272
1187
  }
1273
1188
  sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
1274
- return JSON.stringify(sessions, null, 2);
1189
+ return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
1275
1190
  }
1276
1191
  catch (error) {
1277
1192
  logger.error('[CommandHandler] /resume failed:', error);
1278
- return `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}`;
1193
+ return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
1279
1194
  }
1280
1195
  }
1281
1196
  // /agent 命令:查看或切换 Agent 后端
@@ -1283,7 +1198,7 @@ export class CommandHandler {
1283
1198
  const args = normalizedContent.slice(6).trim();
1284
1199
  // 切换(带参)需权限:群聊 owner only,私聊 admin+;无参查询对所有人放开
1285
1200
  if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
1286
- return '❌ 无权限:此命令仅限管理员使用';
1201
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
1287
1202
  }
1288
1203
  const available = this.getAvailableBaseagents(channel);
1289
1204
  if (!args) {
@@ -1291,57 +1206,45 @@ export class CommandHandler {
1291
1206
  const currentAgent = activeSession?.agentId
1292
1207
  || this.agentRegistry?.resolveByChannel(channel)?.baseagent
1293
1208
  || this.parseDefaultBaseagent();
1294
- // 尝试发送交互卡片
1209
+ // 尝试发送 CommandCard 卡片
1295
1210
  if (this.interactionRouter && available.length > 1) {
1296
- const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1297
1211
  const interaction = {
1298
1212
  type: 'interaction',
1299
- id: requestId,
1213
+ id: `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1300
1214
  channelId,
1301
- sessionId: activeSession?.id || requestId,
1215
+ sessionId: activeSession?.id || `agent-${Date.now()}`,
1216
+ initiatorId: userId,
1302
1217
  kind: {
1303
- kind: 'action',
1218
+ kind: 'command-card',
1304
1219
  title: '🔌 切换 Agent',
1305
1220
  buttons: available.map(a => ({
1306
- key: a,
1307
1221
  label: a === currentAgent ? `✓ ${a}` : a,
1308
- style: a === currentAgent ? 'primary' : 'default',
1222
+ command: `/agent ${a}`,
1223
+ style: (a === currentAgent ? 'primary' : 'default'),
1224
+ disabled: a === currentAgent,
1309
1225
  })),
1310
1226
  },
1311
1227
  };
1312
1228
  const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1313
- const cardSent = await this.sendInteractionCard({
1314
- channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
1315
- canWrite: activeChatType === 'group' ? isOwner : isAdmin,
1316
- callback: async (action, _values, operatorId) => {
1317
- if (action !== currentAgent) {
1318
- if (userId && operatorId && operatorId !== userId)
1319
- return;
1320
- const result = await this.handle(`/agent ${action}`, channel, channelId, undefined, userId, threadId);
1321
- if (result) {
1322
- const adapter = this.adapters.get(channel);
1323
- adapter?.sendText(channelId, result, replyCtx);
1324
- }
1325
- }
1326
- },
1327
- });
1328
- if (cardSent)
1229
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: activeChatType === 'group' ? isOwner : isAdmin });
1230
+ if (cardResult === null)
1329
1231
  return null;
1232
+ return { kind: 'command.result', text: cardResult };
1330
1233
  }
1331
1234
  // 降级:文本
1332
1235
  const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
1333
1236
  const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
1334
1237
  if (canSwitchAgent) {
1335
- return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
1238
+ return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>` };
1336
1239
  }
1337
- return `当前 Agent: ${currentAgent}`;
1240
+ return { kind: 'command.result', text: `当前 Agent: ${currentAgent}` };
1338
1241
  }
1339
1242
  if (!available.includes(args)) {
1340
- return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
1243
+ return { kind: 'command.error', text: `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}` };
1341
1244
  }
1342
- const result = await this.ensureSession(channel, channelId, threadId);
1245
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
1343
1246
  if ('error' in result)
1344
- return result.error;
1247
+ return { kind: 'command.error', text: result.error };
1345
1248
  const { session } = result;
1346
1249
  // 取消原会话的 pending 权限请求和交互卡片
1347
1250
  if (this.permissionGateway) {
@@ -1355,13 +1258,13 @@ export class CommandHandler {
1355
1258
  const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
1356
1259
  const projectName = this.getProjectName(session.projectPath);
1357
1260
  let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
1358
- return agentSwitchResponse;
1261
+ return { kind: 'command.result', text: agentSwitchResponse };
1359
1262
  }
1360
1263
  // /setmodel 命令:返回 JSON 格式的模型列表(供程序解析)
1361
1264
  if (normalizedContent === '/setmodel' || normalizedContent.startsWith('/setmodel ')) {
1362
- const setmodelResult = await this.ensureSession(channel, channelId, threadId);
1265
+ const setmodelResult = await this.ensureSession(channel, channelId, threadId, chatType);
1363
1266
  if ('error' in setmodelResult)
1364
- return setmodelResult.error;
1267
+ return { kind: 'command.result', text: setmodelResult.error };
1365
1268
  const { session: setmodelSession } = setmodelResult;
1366
1269
  const setmodelAgent = this.getAgent(channel, setmodelSession.agentId);
1367
1270
  const currentModel = hasModelSwitcher(setmodelAgent) ? setmodelAgent.getModel() : setmodelAgent.name;
@@ -1370,7 +1273,7 @@ export class CommandHandler {
1370
1273
  // 获取 API URL 用于请求 /models
1371
1274
  let apiBaseUrl;
1372
1275
  try {
1373
- const configBaseUrl = this.config.agents?.claude?.baseUrl;
1276
+ const configBaseUrl = this.getOwningAgent(channel)?.config?.baseagents?.claude?.baseUrl;
1374
1277
  const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
1375
1278
  if (configBaseUrl && !isPlaceholderUrl) {
1376
1279
  apiBaseUrl = configBaseUrl;
@@ -1397,7 +1300,7 @@ export class CommandHandler {
1397
1300
  const timeout = setTimeout(() => controller.abort(), 5000);
1398
1301
  const resp = await fetch(modelsUrl, {
1399
1302
  signal: controller.signal,
1400
- headers: { 'Authorization': `Bearer ${this.config.agents?.claude?.apiKey || process.env.ANTHROPIC_AUTH_TOKEN || ''}` },
1303
+ headers: { 'Authorization': `Bearer ${this.getOwningAgent(channel)?.config?.baseagents?.claude?.apiKey || process.env.ANTHROPIC_AUTH_TOKEN || ''}` },
1401
1304
  });
1402
1305
  clearTimeout(timeout);
1403
1306
  if (resp.ok) {
@@ -1418,20 +1321,20 @@ export class CommandHandler {
1418
1321
  ],
1419
1322
  };
1420
1323
  }
1421
- return JSON.stringify({
1422
- current_model: currentModel,
1423
- current_effort: currentEffort,
1424
- available_efforts: efforts,
1425
- models: modelListData,
1426
- }, null, 2);
1324
+ return { kind: 'command.result', text: JSON.stringify({
1325
+ current_model: currentModel,
1326
+ current_effort: currentEffort,
1327
+ available_efforts: efforts,
1328
+ models: modelListData,
1329
+ }, null, 2) };
1427
1330
  }
1428
1331
  // /model 命令:查看或切换模型/推理强度
1429
1332
  if (normalizedContent.startsWith('/model')) {
1430
1333
  const args = normalizedContent.slice(6).trim();
1431
1334
  // 获取当前会话(话题会话可能绑定不同 agent)
1432
- const modelResult = await this.ensureSession(channel, channelId, threadId);
1335
+ const modelResult = await this.ensureSession(channel, channelId, threadId, chatType);
1433
1336
  if ('error' in modelResult)
1434
- return modelResult.error;
1337
+ return { kind: 'command.result', text: modelResult.error };
1435
1338
  const { session: modelSession } = modelResult;
1436
1339
  const modelAgent = this.getAgent(channel, modelSession.agentId);
1437
1340
  const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
@@ -1439,42 +1342,30 @@ export class CommandHandler {
1439
1342
  const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
1440
1343
  const efforts = getAvailableEfforts(modelAgent, currentModel);
1441
1344
  const currentEffort = modelAgent.getEffort?.() || 'auto';
1442
- // 尝试发送交互卡片
1345
+ // 尝试发送 CommandCard 卡片
1443
1346
  if (this.interactionRouter && models.length > 0) {
1444
- const requestId = `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1445
1347
  const interaction = {
1446
1348
  type: 'interaction',
1447
- id: requestId,
1349
+ id: `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1448
1350
  channelId,
1449
1351
  sessionId: modelSession.id,
1352
+ initiatorId: userId,
1450
1353
  kind: {
1451
- kind: 'action',
1354
+ kind: 'command-card',
1452
1355
  title: '🤖 切换模型',
1453
1356
  buttons: models.map((m) => ({
1454
- key: m,
1455
1357
  label: m === currentModel ? `✓ ${m}` : m,
1456
- style: m === currentModel ? 'primary' : 'default',
1358
+ command: `/model ${m}`,
1359
+ style: (m === currentModel ? 'primary' : 'default'),
1360
+ disabled: m === currentModel,
1457
1361
  })),
1458
1362
  },
1459
1363
  };
1460
1364
  const replyCtx = this.getReplyContext(modelSession);
1461
- const cardSent = await this.sendInteractionCard({
1462
- channel, channelId, sessionId: modelSession.id, requestId, interaction, replyCtx,
1463
- canWrite: isAdmin,
1464
- callback: async (action, _values, operatorId) => {
1465
- if (action !== currentModel) {
1466
- if (userId && operatorId && operatorId !== userId)
1467
- return;
1468
- const result = await this.handle(`/model ${action}`, channel, channelId, undefined, userId, threadId);
1469
- if (result) {
1470
- const adapter = this.adapters.get(channel);
1471
- adapter?.sendText(channelId, result, replyCtx);
1472
- }
1473
- }
1474
- },
1475
- });
1476
- if (cardSent)
1365
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1366
+ if (cardResult === null)
1477
1367
  return null;
1368
+ return { kind: 'command.result', text: cardResult };
1478
1369
  }
1479
1370
  // 降级:文本
1480
1371
  const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
@@ -1482,13 +1373,13 @@ export class CommandHandler {
1482
1373
  ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
1483
1374
  : '';
1484
1375
  if (isAdmin) {
1485
- return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
1376
+ return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}` };
1486
1377
  }
1487
- return `当前模型: ${currentModel}${effortHint}`;
1378
+ return { kind: 'command.result', text: `当前模型: ${currentModel}${effortHint}` };
1488
1379
  }
1489
1380
  // 带参(切换/调整)需 admin+;无参查询已在上方返回
1490
1381
  if (!isAdmin)
1491
- return '❌ 无权限:切换模型仅限管理员使用';
1382
+ return { kind: 'command.error', text: '❌ 无权限:切换模型仅限管理员使用' };
1492
1383
  const parts = args.split(/\s+/);
1493
1384
  let newModel;
1494
1385
  let newEffort;
@@ -1498,10 +1389,11 @@ export class CommandHandler {
1498
1389
  const efforts = getAvailableEfforts(modelAgent, currentModel);
1499
1390
  // effort 相关参数统一转发到 /effort
1500
1391
  if (efforts.includes(arg) || arg === 'auto') {
1501
- return this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
1392
+ const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
1393
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
1502
1394
  }
1503
1395
  else if (allEfforts.includes(arg)) {
1504
- return `⚠️ 请使用 /effort ${arg} 调整推理强度`;
1396
+ return { kind: 'command.error', text: `⚠️ 请使用 /effort ${arg} 调整推理强度` };
1505
1397
  }
1506
1398
  else if (models.includes(arg)) {
1507
1399
  newModel = arg;
@@ -1509,34 +1401,33 @@ export class CommandHandler {
1509
1401
  else {
1510
1402
  const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
1511
1403
  const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
1512
- return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
1404
+ return { kind: 'command.error', text: `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}` };
1513
1405
  }
1514
1406
  }
1515
1407
  else {
1516
1408
  // 双参数:model effort
1517
1409
  const [modelArg, effortArg] = parts;
1518
1410
  if (!models.includes(modelArg)) {
1519
- return `❌ 无效的模型ID: ${modelArg}`;
1411
+ return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArg}` };
1520
1412
  }
1521
1413
  const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
1522
1414
  if (targetEfforts.length === 0) {
1523
- return `⚠️ ${modelArg} 不支持推理强度设置`;
1415
+ return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
1524
1416
  }
1525
1417
  if (!targetEfforts.includes(effortArg)) {
1526
1418
  const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
1527
- return `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}`;
1419
+ return { kind: 'command.result', text: `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}` };
1528
1420
  }
1529
1421
  newModel = modelArg;
1530
1422
  newEffort = effortArg;
1531
1423
  }
1532
- if (!this.config.agents)
1533
- this.config.agents = {};
1424
+ // 运行时 model/effort 切换已通过 EvolAgent.setBaseagentModel/setBaseagentEffort 持久化
1534
1425
  const isCodexAgent = modelAgent.name === 'codex';
1535
1426
  const changes = [];
1536
1427
  if (newModel) {
1537
1428
  modelAgent.setModel?.(newModel);
1538
1429
  this.eventBus.publish({
1539
- type: 'agent:model-changed',
1430
+ type: 'runner:model-changed',
1540
1431
  sessionId: modelSession.id,
1541
1432
  model: newModel,
1542
1433
  timestamp: Date.now()
@@ -1551,231 +1442,160 @@ export class CommandHandler {
1551
1442
  if (newModel) {
1552
1443
  const err = this.persistBaseagentModel(channel, modelAgent.name, newModel);
1553
1444
  if (err)
1554
- return `${err}\n已更新运行时配置,但未持久化`;
1445
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1555
1446
  }
1556
1447
  if (newEffort) {
1557
1448
  const err = this.persistBaseagentEffort(channel, modelAgent.name, newEffort);
1558
1449
  if (err)
1559
- return `${err}\n已更新运行时配置,但未持久化`;
1450
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1560
1451
  }
1561
- return `✓ 已切换\n ${changes.join('\n ')}`;
1452
+ return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
1562
1453
  }
1563
1454
  // /effort 命令:查看或切换推理强度
1564
1455
  if (normalizedContent.startsWith('/effort')) {
1565
1456
  const args = normalizedContent.slice(7).trim();
1566
- const effortResult = await this.ensureSession(channel, channelId, threadId);
1457
+ const effortResult = await this.ensureSession(channel, channelId, threadId, chatType);
1567
1458
  if ('error' in effortResult)
1568
- return effortResult.error;
1459
+ return { kind: 'command.result', text: effortResult.error };
1569
1460
  const { session: effortSession } = effortResult;
1570
1461
  const effortAgent = this.getAgent(channel, effortSession.agentId);
1571
1462
  const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
1572
1463
  const efforts = getAvailableEfforts(effortAgent, currentModel);
1573
1464
  const currentEffort = effortAgent.getEffort?.() || 'auto';
1574
1465
  if (efforts.length === 0) {
1575
- return '⚠️ 当前模型不支持推理强度设置';
1466
+ return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
1576
1467
  }
1577
1468
  if (!args) {
1578
- // /effort(无参数):显示当前推理强度 + 发送 Action 卡片
1469
+ // /effort(无参数):显示当前推理强度 + 发送 CommandCard 卡片
1579
1470
  if (this.interactionRouter) {
1580
- const requestId = `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1581
- const buttons = [
1582
- ...efforts.map(e => ({
1583
- key: e,
1584
- label: e === currentEffort ? `✓ ${e}` : e,
1585
- style: e === currentEffort ? 'primary' : 'default',
1586
- })),
1587
- {
1588
- key: 'auto',
1589
- label: currentEffort === 'auto' ? '✓ auto' : 'auto',
1590
- style: currentEffort === 'auto' ? 'primary' : 'default',
1591
- },
1592
- ];
1471
+ const allItems = [...efforts, 'auto'];
1593
1472
  const interaction = {
1594
1473
  type: 'interaction',
1595
- id: requestId,
1474
+ id: `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1596
1475
  channelId,
1597
1476
  sessionId: effortSession.id,
1477
+ initiatorId: userId,
1598
1478
  kind: {
1599
- kind: 'action',
1479
+ kind: 'command-card',
1600
1480
  title: '⚡ 推理强度',
1601
- buttons,
1481
+ buttons: allItems.map(e => ({
1482
+ label: e === currentEffort ? `✓ ${e}` : e,
1483
+ command: `/effort ${e}`,
1484
+ style: (e === currentEffort ? 'primary' : 'default'),
1485
+ disabled: e === currentEffort,
1486
+ })),
1602
1487
  },
1603
1488
  };
1604
1489
  const replyCtx = this.getReplyContext(effortSession);
1605
- const cardSent = await this.sendInteractionCard({
1606
- channel, channelId, sessionId: effortSession.id, requestId, interaction, replyCtx,
1607
- canWrite: isAdmin,
1608
- callback: async (action, _values, operatorId) => {
1609
- if (action !== currentEffort) {
1610
- if (userId && operatorId && operatorId !== userId)
1611
- return;
1612
- const result = await this.handle(`/effort ${action}`, channel, channelId, undefined, userId, threadId);
1613
- if (result) {
1614
- const adapter = this.adapters.get(channel);
1615
- adapter?.sendText(channelId, result, replyCtx);
1616
- }
1617
- }
1618
- },
1619
- });
1620
- if (cardSent)
1490
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1491
+ if (cardResult === null)
1621
1492
  return null;
1493
+ return { kind: 'command.result', text: cardResult };
1622
1494
  }
1623
1495
  // 降级:文本
1624
1496
  const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
1625
1497
  const allItems = [...efforts, 'auto'];
1626
1498
  const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
1627
1499
  if (isAdmin) {
1628
- return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
1500
+ return { kind: 'command.result', text: `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>` };
1629
1501
  }
1630
- return `⚡ 推理强度: ${effortDisplay}`;
1502
+ return { kind: 'command.result', text: `⚡ 推理强度: ${effortDisplay}` };
1631
1503
  }
1632
1504
  // 带参(切换)需 admin+;无参查询已在上方返回
1633
1505
  if (!isAdmin)
1634
- return '❌ 无权限:切换推理强度仅限管理员使用';
1506
+ return { kind: 'command.error', text: '❌ 无权限:切换推理强度仅限管理员使用' };
1635
1507
  // /effort auto:恢复 SDK 默认
1636
1508
  if (args === 'auto') {
1637
1509
  effortAgent.setEffort?.(undefined);
1638
1510
  const err = this.persistBaseagentEffort(channel, effortAgent.name, undefined);
1639
1511
  if (err)
1640
- return `${err}\n已更新运行时配置,但未持久化`;
1641
- return '✓ 推理强度已恢复为 auto (SDK默认)';
1512
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1513
+ return { kind: 'command.result', text: '✓ 推理强度已恢复为 auto (SDK默认)' };
1642
1514
  }
1643
1515
  // /effort <level>:切换推理强度
1644
1516
  if (!efforts.includes(args)) {
1645
1517
  if (allEfforts.includes(args)) {
1646
- return `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}`;
1518
+ return { kind: 'command.error', text: `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}` };
1647
1519
  }
1648
- return `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto`;
1520
+ return { kind: 'command.error', text: `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto` };
1649
1521
  }
1650
1522
  const newEffort = args;
1651
1523
  effortAgent.setEffort?.(newEffort);
1652
1524
  const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
1653
1525
  if (err)
1654
- return `${err}\n已更新运行时配置,但未持久化`;
1655
- return `✓ 推理强度: ${newEffort}`;
1656
- }
1657
- // /aid 命令:AID 身份管理(list / new)
1658
- if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
1659
- if (!isOwner)
1660
- return '❌ 无权限:此命令仅限 owner 使用';
1661
- const arg = normalizedContent.slice(4).trim();
1662
- const { aidList, aidCreate, agentmdPut, buildInitialAgentMd, isValidAid } = await import('../channels/aun-ops.js');
1663
- // /aid 或 /aid list — 列出本地所有 AID
1664
- if (!arg || arg === 'list') {
1665
- const aids = aidList();
1666
- if (aids.length === 0)
1667
- return '本地无 AID';
1668
- const lines = ['本地 AID:'];
1669
- for (const a of aids) {
1670
- const icons = [
1671
- a.hasPrivateKey ? '🔑' : ' ',
1672
- a.hasAgentMd ? '📄' : ' ',
1673
- ].join('');
1674
- lines.push(` ${icons} ${a.aid}`);
1675
- }
1676
- lines.push('\n🔑=私钥 📄=agent.md');
1677
- return lines.join('\n');
1678
- }
1679
- // /aid new <aid> — 创建 AID(纯身份,不动 config)
1680
- if (arg.startsWith('new ')) {
1681
- const rawAid = arg.slice(4).trim();
1682
- if (!rawAid)
1683
- return '用法: /aid new <完整AID>\n例: /aid new reviewer.agentid.pub';
1684
- if (!isValidAid(rawAid))
1685
- return `❌ 无效 AID 格式: ${rawAid}`;
1686
- try {
1687
- const result = await aidCreate(rawAid);
1688
- if (!result.alreadyExisted) {
1689
- const content = buildInitialAgentMd({ aid: rawAid });
1690
- try {
1691
- await agentmdPut(content, { aid: rawAid, client: result.client });
1692
- }
1693
- catch { /* non-fatal */ }
1694
- }
1695
- try {
1696
- await result.client.close();
1697
- }
1698
- catch { /* ignore */ }
1699
- const verb = result.alreadyExisted ? '已存在' : '已创建';
1700
- return `✓ ${rawAid} ${verb}
1701
- 如需上线 AUN 通道,运行 evolclaw init aun`;
1702
- }
1703
- catch (e) {
1704
- return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
1705
- }
1706
- }
1707
- return '用法: /aid [list|new <aid>]';
1526
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1527
+ return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
1708
1528
  }
1709
- // /agentmd 命令:管理 agent.md 身份文件
1710
- if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
1529
+ // /aid, /rpc, /storage — 转发到 CLI 执行
1530
+ if (normalizedContent === '/aid' || normalizedContent.startsWith('/aid ') ||
1531
+ normalizedContent === '/rpc' || normalizedContent.startsWith('/rpc ') ||
1532
+ normalizedContent === '/storage' || normalizedContent.startsWith('/storage ')) {
1711
1533
  if (!isOwner)
1712
- return '❌ 无权限:此命令仅限 owner 使用';
1713
- const adapter = this.adapters.get(channel);
1714
- if (!adapter?.uploadAgentMd)
1715
- return '❌ 当前通道不支持 agent.md 操作';
1716
- const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1717
- const arg = normalizedContent.slice(9).trim();
1718
- const { agentmdGet, agentmdPut } = await import('../channels/aun-ops.js');
1719
- // put read local agent.md and upload to network
1720
- if (arg === 'put') {
1721
- if (!selfAid)
1722
- return '❌ 未连接,无法确定本地 AID';
1723
- try {
1724
- const { readFileSync } = await import('node:fs');
1725
- const { join } = await import('node:path');
1726
- const { homedir } = await import('node:os');
1727
- const localPath = join(homedir(), '.aun', 'AIDs', selfAid, 'agent.md');
1728
- if (!readFileSync)
1729
- return '❌ 读取失败';
1730
- const content = readFileSync(localPath, 'utf-8');
1731
- await agentmdPut(content, { aid: selfAid });
1732
- return '✅ agent.md 已发布';
1733
- }
1734
- catch (e) {
1735
- return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
1736
- }
1737
- }
1738
- // set <content> — upload inline content
1739
- if (arg.startsWith('set ')) {
1740
- const content = arg.slice(4).trim();
1741
- if (!content)
1742
- return '用法:/agentmd set <内容>';
1743
- if (!selfAid)
1744
- return '❌ 未连接,无法确定本地 AID';
1745
- try {
1746
- await agentmdPut(content, { aid: selfAid });
1747
- return '✅ agent.md 已更新并发布到AUN网络';
1748
- }
1749
- catch (e) {
1750
- return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
1751
- }
1534
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
1535
+ // 无参数时返回用法说明
1536
+ if (normalizedContent === '/aid') {
1537
+ return { kind: 'command.result', text: `🆔 AID 身份管理
1538
+
1539
+ 用法:
1540
+ /aid list 列出本地所有 AID
1541
+ /aid show <aid> 查看 AID 详情
1542
+ /aid new <aid> 创建新 AID
1543
+ /aid delete <aid> 删除本地 AID
1544
+ /aid lookup <aid> 远程探测 AID
1545
+ /aid agentmd put <aid> 签名并上传 agent.md
1546
+ /aid agentmd get <aid> 下载并验签 agent.md` };
1547
+ }
1548
+ if (normalizedContent === '/rpc') {
1549
+ return { kind: 'command.result', text: `📡 AUN RPC 调用
1550
+
1551
+ 用法:
1552
+ /rpc --as <aid> --params <json>
1553
+
1554
+ 参数格式:
1555
+ 单行 JSON 单次调用
1556
+ 多行 JSONL 逐行执行,失败即停
1557
+
1558
+ 示例:
1559
+ /rpc --as myaid.agentid.pub --params {"method":"meta.ping","params":{}}` };
1752
1560
  }
1753
- // view /agentmd or /agentmd <aid>
1754
- const aidToView = arg || selfAid;
1755
- if (!aidToView)
1756
- return '用法:/agentmd [<aid>] | put | set <内容>';
1561
+ if (normalizedContent === '/storage') {
1562
+ return { kind: 'command.result', text: `📦 文件存储
1563
+
1564
+ 用法:
1565
+ /storage upload <aid> <file> <path> [--public] 上传文件
1566
+ /storage download <aid> <url> [local-path] 下载文件
1567
+ /storage ls <aid> [prefix] 列文件
1568
+ /storage rm <aid> <path> 删文件
1569
+ /storage quota <aid> 查配额` };
1570
+ }
1571
+ const cliArgs = normalizedContent.slice(1); // strip leading /
1757
1572
  try {
1758
- const md = await agentmdGet(aidToView);
1759
- if (!md || !md.trim())
1760
- return `ℹ️ ${aidToView} 尚未设置 agent.md`;
1761
- return `\`\`\`\n${md.slice(0, 1500)}\n\`\`\``;
1573
+ const { execFile } = await import('node:child_process');
1574
+ const { promisify } = await import('node:util');
1575
+ const execFileAsync = promisify(execFile);
1576
+ const { stdout, stderr } = await execFileAsync('evolclaw', cliArgs.split(/\s+/), {
1577
+ timeout: 30000,
1578
+ encoding: 'utf-8',
1579
+ env: { ...process.env, AUN_LOG_INI_DISABLE: '1' },
1580
+ });
1581
+ const output = (stdout || '').trim();
1582
+ if (!output && stderr)
1583
+ return { kind: 'command.result', text: `⚠ ${stderr.trim().slice(0, 500)}` };
1584
+ return { kind: 'command.result', text: output || '(无输出)' };
1762
1585
  }
1763
1586
  catch (e) {
1764
- const msg = String(e.message || e);
1765
- if (msg.includes('not found') || msg.includes('404')) {
1766
- return `ℹ️ ${aidToView} 尚未设置 agent.md`;
1767
- }
1768
- return `❌ 获取失败: ${msg.slice(0, 100)}`;
1587
+ const msg = e.stderr?.trim() || e.stdout?.trim() || String(e.message || e);
1588
+ return { kind: 'command.error', text: `❌ ${msg.slice(0, 500)}` };
1769
1589
  }
1770
1590
  }
1771
1591
  if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
1772
1592
  const activityArg = normalizedContent.slice(9).trim();
1773
1593
  // 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
1774
1594
  if (activityArg && !isAdmin)
1775
- return '❌ 无权限:此命令仅限管理员使用';
1595
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
1776
1596
  // proactive 模式下流式输出全部静默,activity 配置无意义
1777
1597
  if (activeSession?.sessionMode === 'proactive') {
1778
- return '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)';
1598
+ return { kind: 'command.error', text: '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)' };
1779
1599
  }
1780
1600
  const modeMap = {
1781
1601
  all: 'all',
@@ -1783,7 +1603,7 @@ export class CommandHandler {
1783
1603
  owner: 'owner-dm-only',
1784
1604
  none: 'none',
1785
1605
  };
1786
- const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? getChannelShowActivities(this.config, channel);
1606
+ const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
1787
1607
  // 模式描述列表(用于 body 和文本降级)
1788
1608
  const modeDescriptions = [
1789
1609
  { key: 'all', configVal: 'all', label: '全部显示' },
@@ -1792,99 +1612,126 @@ export class CommandHandler {
1792
1612
  { key: 'none', configVal: 'none', label: '全部静默' },
1793
1613
  ];
1794
1614
  if (!activityArg) {
1795
- // 无参数:显示当前模式 + Action 卡片
1796
- if (this.interactionRouter) {
1797
- const requestId = `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1798
- const body = modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n');
1799
- const buttons = modeDescriptions.map(m => ({
1800
- key: m.key,
1801
- label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
1802
- style: m.configVal === currentMode ? 'primary' : 'default',
1803
- }));
1615
+ // 尝试发送 CommandCard 卡片
1616
+ {
1804
1617
  const interaction = {
1805
1618
  type: 'interaction',
1806
- id: requestId,
1619
+ id: `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1807
1620
  channelId,
1808
- sessionId: activeSession?.id || requestId,
1621
+ sessionId: activeSession?.id || '',
1622
+ initiatorId: userId,
1809
1623
  kind: {
1810
- kind: 'action',
1624
+ kind: 'command-card',
1811
1625
  title: '📋 中间输出模式',
1812
- body,
1813
- buttons,
1626
+ body: modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n'),
1627
+ buttons: modeDescriptions.map(m => ({
1628
+ label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
1629
+ command: `/activity ${m.key}`,
1630
+ style: (m.configVal === currentMode ? 'primary' : 'default'),
1631
+ disabled: m.configVal === currentMode,
1632
+ })),
1814
1633
  },
1815
1634
  };
1816
1635
  const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1817
- const cardSent = await this.sendInteractionCard({
1818
- channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
1819
- canWrite: isOwner,
1820
- callback: async (action, _values, operatorId) => {
1821
- const newMode = modeMap[action];
1822
- if (newMode && newMode !== currentMode) {
1823
- if (userId && operatorId && operatorId !== userId)
1824
- return;
1825
- const result = await this.handle(`/activity ${action}`, channel, channelId, undefined, userId, threadId);
1826
- if (result) {
1827
- const adapter = this.adapters.get(channel);
1828
- adapter?.sendText(channelId, result, replyCtx);
1829
- }
1830
- }
1831
- },
1832
- });
1833
- if (cardSent)
1636
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1637
+ if (cardResult === null)
1834
1638
  return null;
1639
+ // 卡片降级:fall through 到下方文本输出
1835
1640
  }
1836
1641
  // 降级:文本
1837
1642
  const modeList = modeDescriptions.map(m => {
1838
- const prefix = m.configVal === currentMode ? '✓' : ' ';
1839
- return ` ${prefix} ${m.key} (${m.label})`;
1643
+ const prefix = m.configVal === currentMode ? '✓' : '';
1644
+ return ` ${prefix} ${m.key} ${m.label}`;
1840
1645
  }).join('\n');
1841
1646
  if (isOwner) {
1842
- return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
1647
+ return { kind: 'command.result', text: [`📋 中间输出模式: ${currentMode}`, '', modeList, '', '用法: /activity <all|dm|owner|none>'].join('\n') };
1843
1648
  }
1844
- return `📋 中间输出模式: ${currentMode}`;
1649
+ return { kind: 'command.result', text: `📋 中间输出模式: ${currentMode}` };
1845
1650
  }
1846
1651
  const newMode = modeMap[activityArg];
1847
1652
  if (!newMode) {
1848
- return `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none`;
1653
+ return { kind: 'command.error', text: `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none` };
1849
1654
  }
1850
1655
  const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
1851
1656
  if (newMode === currentMode) {
1852
- return `📋 中间输出模式已是 ${activityArg}(${label})`;
1657
+ return { kind: 'command.result', text: `📋 中间输出模式已是 ${activityArg}(${label})` };
1853
1658
  }
1854
1659
  // 切换操作仅 owner
1855
1660
  if (!isOwner)
1856
- return '❌ 中间输出模式切换仅限 owner';
1661
+ return { kind: 'command.error', text: '❌ 中间输出模式切换仅限 owner' };
1857
1662
  if (this.agentRegistry?.setShowActivities) {
1858
1663
  this.agentRegistry.setShowActivities(channel, newMode);
1859
1664
  }
1860
1665
  else {
1861
- setChannelShowActivities(this.config, channel, newMode);
1666
+ return { kind: 'command.error', text: `⚠️ 找不到通道 "${channel}" 所属的 self-agent,无法持久化` };
1862
1667
  }
1863
- return `✅ 中间输出模式: ${activityArg}(${label})`;
1668
+ return { kind: 'command.result', text: `✅ 中间输出模式: ${activityArg}(${label})` };
1864
1669
  }
1865
1670
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
1866
1671
  // - 查看:所有人可用
1867
1672
  // - 设置:单聊任何角色可设置;群聊仅管理员可设置
1868
1673
  if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1869
- if (!activeSession)
1870
- return ' 当前无活跃会话';
1674
+ const chatmodeResult = await this.ensureSession(channel, channelId, threadId, chatType);
1675
+ if ('error' in chatmodeResult)
1676
+ return { kind: 'command.result', text: chatmodeResult.error };
1677
+ const chatmodeSession = chatmodeResult.session;
1871
1678
  const arg = normalizedContent.slice(9).trim();
1872
- const currentMode = activeSession.sessionMode || 'interactive';
1679
+ const currentMode = chatmodeSession.sessionMode || 'interactive';
1680
+ const chatmodeChatType = chatmodeSession.chatType || activeChatType;
1681
+ const canSwitch = chatmodeChatType !== 'group' || isAdmin;
1873
1682
  if (!arg) {
1874
- const canSwitch = activeChatType !== 'group' || isAdmin;
1683
+ // 尝试发送 CommandCard 卡片
1875
1684
  if (canSwitch) {
1876
- return `📋 当前会话模式: ${currentMode}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1685
+ const modes = [
1686
+ { key: 'interactive', name: '交互模式', desc: '被动响应:收到消息时才回复,回复直接显示' },
1687
+ { key: 'proactive', name: '主动模式', desc: '主动推进:流式输出静默,由 Agent 自调 ctl send 发声' },
1688
+ ];
1689
+ const interaction = {
1690
+ type: 'interaction',
1691
+ id: `chatmode-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1692
+ channelId,
1693
+ sessionId: chatmodeSession.id,
1694
+ initiatorId: userId,
1695
+ kind: {
1696
+ kind: 'command-card',
1697
+ title: '🔄 会话模式',
1698
+ body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
1699
+ buttons: modes.map(m => ({
1700
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
1701
+ command: `/chatmode ${m.key}`,
1702
+ style: (m.key === currentMode ? 'primary' : 'default'),
1703
+ disabled: m.key === currentMode,
1704
+ })),
1705
+ },
1706
+ };
1707
+ const replyCtx = this.getReplyContext(chatmodeSession);
1708
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1709
+ if (cardResult === null)
1710
+ return null;
1711
+ // 卡片降级:fall through 到下方文本输出
1877
1712
  }
1878
- return `📋 当前会话模式: ${currentMode}`;
1713
+ // 降级:文本
1714
+ if (canSwitch) {
1715
+ return { kind: 'command.result', text: [
1716
+ `📋 会话模式: ${currentMode}`,
1717
+ '',
1718
+ '模式说明:',
1719
+ ' • interactive — 交互模式:收到消息时才回复,回复直接显示',
1720
+ ' • proactive — 主动模式:流式输出静默,由 Agent 自调 ctl send 发声',
1721
+ '',
1722
+ '用法: /chatmode <interactive|proactive>',
1723
+ ].join('\n') };
1724
+ }
1725
+ return { kind: 'command.result', text: `📋 会话模式: ${currentMode}` };
1879
1726
  }
1880
1727
  if (arg !== 'interactive' && arg !== 'proactive') {
1881
- return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
1728
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: interactive / proactive` };
1882
1729
  }
1883
- if (activeChatType === 'group' && !isAdmin) {
1884
- return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
1730
+ if ((chatmodeSession.chatType || activeChatType) === 'group' && !isAdmin) {
1731
+ return { kind: 'command.error', text: '❌ 无权限:群聊中切换会话模式仅限管理员使用' };
1885
1732
  }
1886
1733
  if (arg === currentMode) {
1887
- return `📋 当前会话模式已是 ${arg}`;
1734
+ return { kind: 'command.result', text: `📋 当前会话模式已是 ${arg}` };
1888
1735
  }
1889
1736
  // 仅在真正需要切换时才要求会话空闲
1890
1737
  if (threadId) {
@@ -1892,53 +1739,125 @@ export class CommandHandler {
1892
1739
  if (threadSession) {
1893
1740
  const threadAgent = this.getAgent(channel, threadSession.agentId);
1894
1741
  if (threadAgent.hasActiveStream(threadSession.id)) {
1895
- return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
1742
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1896
1743
  }
1897
1744
  }
1898
1745
  }
1899
- else if (agent.hasActiveStream(activeSession.id)) {
1900
- return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
1746
+ else if (agent.hasActiveStream(chatmodeSession.id)) {
1747
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1901
1748
  }
1902
- await this.sessionManager.updateSession(activeSession.id, { sessionMode: arg });
1903
- return `✅ 会话模式已切换: ${arg}`;
1749
+ await this.sessionManager.updateSession(chatmodeSession.id, { sessionMode: arg });
1750
+ this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: chatmodeSession.id, mode: arg, timestamp: Date.now() });
1751
+ return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
1752
+ }
1753
+ // /dispatch 命令:查看/切换群聊分发模式(mention | all)
1754
+ // 仅群聊可用;群聊中设置需管理员权限
1755
+ if (normalizedContent === '/dispatch' || normalizedContent.startsWith('/dispatch ')) {
1756
+ const dispatchResult = await this.ensureSession(channel, channelId, threadId, chatType);
1757
+ if ('error' in dispatchResult)
1758
+ return { kind: 'command.result', text: dispatchResult.error };
1759
+ const dispatchSession = dispatchResult.session;
1760
+ const dispatchChatType = dispatchSession.chatType || activeChatType;
1761
+ if (dispatchChatType !== 'group') {
1762
+ return { kind: 'command.error', text: '❌ /dispatch 仅在群聊中可用' };
1763
+ }
1764
+ const arg = normalizedContent.slice(9).trim();
1765
+ const currentMode = dispatchSession.metadata?.dispatchMode || 'mention';
1766
+ if (!arg) {
1767
+ // 尝试发送 CommandCard 卡片
1768
+ if (isAdmin) {
1769
+ const modes = [
1770
+ { key: 'mention', name: '提及模式', desc: '仅当被 @ 提及(含 @all)时响应群消息' },
1771
+ { key: 'all', name: '广播模式', desc: '群内所有消息都触发响应' },
1772
+ ];
1773
+ const interaction = {
1774
+ type: 'interaction',
1775
+ id: `dispatch-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1776
+ channelId,
1777
+ sessionId: dispatchSession.id,
1778
+ initiatorId: userId,
1779
+ kind: {
1780
+ kind: 'command-card',
1781
+ title: '📡 分发模式',
1782
+ body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
1783
+ buttons: modes.map(m => ({
1784
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
1785
+ command: `/dispatch ${m.key}`,
1786
+ style: (m.key === currentMode ? 'primary' : 'default'),
1787
+ disabled: m.key === currentMode,
1788
+ })),
1789
+ },
1790
+ };
1791
+ const replyCtx = this.getReplyContext(dispatchSession);
1792
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1793
+ if (cardResult === null)
1794
+ return null;
1795
+ // 卡片降级:fall through 到下方文本输出
1796
+ }
1797
+ // 降级:文本
1798
+ const lines = [];
1799
+ lines.push(`📋 分发模式: ${currentMode}`);
1800
+ lines.push('');
1801
+ lines.push('模式说明:');
1802
+ lines.push(' • mention — 提及模式:仅当被@提及时响应群消息(含@all)');
1803
+ lines.push(' • all — 广播模式:群内所有消息都触发响应');
1804
+ if (isAdmin) {
1805
+ lines.push('');
1806
+ lines.push('用法: /dispatch <mention|all>');
1807
+ }
1808
+ return { kind: 'command.result', text: lines.join('\n') };
1809
+ }
1810
+ if (arg !== 'mention' && arg !== 'all') {
1811
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention / all\n用法: /dispatch <模式>` };
1812
+ }
1813
+ if (!isAdmin) {
1814
+ return { kind: 'command.error', text: '❌ 无权限:群聊中切换分发模式仅限管理员使用' };
1815
+ }
1816
+ if (arg === currentMode) {
1817
+ return { kind: 'command.result', text: `📋 当前已是 ${arg}` };
1818
+ }
1819
+ const metadata = { ...(dispatchSession.metadata || {}), dispatchMode: arg };
1820
+ await this.sessionManager.updateSession(dispatchSession.id, { metadata });
1821
+ this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: arg, timestamp: Date.now() });
1822
+ return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode} → ${arg}` };
1904
1823
  }
1905
1824
  // /stop 命令:中断当前任务
1906
1825
  if (normalizedContent === '/stop') {
1907
- const stopResult = await this.ensureSession(channel, channelId, threadId);
1826
+ const stopResult = await this.ensureSession(channel, channelId, threadId, chatType);
1908
1827
  if ('error' in stopResult)
1909
- return '当前没有正在处理的任务';
1828
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
1910
1829
  const { session: stopSession } = stopResult;
1911
1830
  const stopAgent = this.getAgent(channel, stopSession.agentId);
1912
1831
  const sessionKey = stopSession.id;
1913
1832
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
1914
1833
  const hasActive = stopAgent.hasActiveStream(sessionKey);
1915
1834
  if (queueLength === 0 && !hasActive) {
1916
- return '当前没有正在处理的任务';
1835
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
1917
1836
  }
1918
1837
  await stopAgent.interrupt(sessionKey);
1919
1838
  // 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
1920
1839
  this.eventBus.publish({
1921
- type: 'message:interrupted',
1840
+ type: 'task:interrupted',
1922
1841
  sessionId: sessionKey,
1923
1842
  reason: 'stop',
1924
- agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '[default]',
1843
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
1925
1844
  });
1926
1845
  // 强制清除 processing_state
1927
1846
  this.sessionManager.clearProcessing(sessionKey);
1928
- return '✓ 已发送中断信号,任务将尽快停止';
1847
+ return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
1929
1848
  }
1930
1849
  // /clear 命令:通过 SDK /clear 清空会话历史
1931
1850
  if (normalizedContent === '/clear') {
1932
- const result = await this.ensureSession(channel, channelId, threadId);
1851
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
1933
1852
  if ('error' in result)
1934
- return result.error;
1853
+ return { kind: 'command.error', text: result.error };
1935
1854
  const { session } = result;
1936
1855
  const sessionAgent = this.getAgent(channel, session.agentId);
1937
1856
  if (!sessionAgent.capabilities?.clear) {
1938
- return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代`;
1857
+ return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代` };
1939
1858
  }
1940
1859
  if (!session.agentSessionId) {
1941
- return '❌ 当前会话没有历史记录,无需清空';
1860
+ return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需清空' };
1942
1861
  }
1943
1862
  const projectPath = path.isAbsolute(session.projectPath)
1944
1863
  ? session.projectPath
@@ -1949,10 +1868,10 @@ export class CommandHandler {
1949
1868
  if (cleared) {
1950
1869
  await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
1951
1870
  sessionAgent.updateSessionId(session.id, '');
1952
- return '✅ 已清空当前会话的对话历史';
1871
+ return { kind: 'command.result', text: '✅ 已清空当前会话的对话历史' };
1953
1872
  }
1954
1873
  else {
1955
- return '❌ 清空会话失败,请稍后重试';
1874
+ return { kind: 'command.error', text: '❌ 清空会话失败,请稍后重试' };
1956
1875
  }
1957
1876
  }
1958
1877
  finally {
@@ -1961,16 +1880,16 @@ export class CommandHandler {
1961
1880
  }
1962
1881
  // /compact 命令:手动压缩会话上下文
1963
1882
  if (normalizedContent === '/compact') {
1964
- const result = await this.ensureSession(channel, channelId, threadId);
1883
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
1965
1884
  if ('error' in result)
1966
- return result.error;
1885
+ return { kind: 'command.error', text: result.error };
1967
1886
  const { session } = result;
1968
1887
  const sessionAgent = this.getAgent(channel, session.agentId);
1969
1888
  if (!sessionAgent.capabilities?.compact) {
1970
- return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact`;
1889
+ return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact` };
1971
1890
  }
1972
1891
  if (!session.agentSessionId) {
1973
- return '❌ 当前会话没有历史记录,无需压缩';
1892
+ return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
1974
1893
  }
1975
1894
  const projectPath = path.isAbsolute(session.projectPath)
1976
1895
  ? session.projectPath
@@ -1982,10 +1901,10 @@ export class CommandHandler {
1982
1901
  }
1983
1902
  const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
1984
1903
  if (compacted) {
1985
- return '✅ 会话上下文已压缩';
1904
+ return { kind: 'command.result', text: '✅ 会话上下文已压缩' };
1986
1905
  }
1987
1906
  else {
1988
- return '❌ 会话压缩失败,请稍后重试';
1907
+ return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
1989
1908
  }
1990
1909
  }
1991
1910
  finally {
@@ -2013,7 +1932,7 @@ export class CommandHandler {
2013
1932
  if (normalizedContent === '/status') {
2014
1933
  // session 现在总是存在(上面已自动创建)
2015
1934
  if (!session) {
2016
- return `❌ 无法创建会话,请检查配置`;
1935
+ return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
2017
1936
  }
2018
1937
  const sessionKey = this.getQueueKey(session, channel, channelId);
2019
1938
  const sessionAgent = this.getAgent(channel, session.agentId);
@@ -2032,6 +1951,8 @@ export class CommandHandler {
2032
1951
  }
2033
1952
  }
2034
1953
  const projectName = this.getProjectName(session.projectPath);
1954
+ const owningAgent = this.getOwningAgent(channel);
1955
+ const agentName = owningAgent?.name ?? 'DefaultAgent';
2035
1956
  const health = await this.sessionManager.getHealthStatus(session.id);
2036
1957
  const timeSinceSuccess = Date.now() - health.lastSuccessTime;
2037
1958
  const timeStr = timeSinceSuccess < 60000 ? '刚刚' :
@@ -2049,23 +1970,25 @@ export class CommandHandler {
2049
1970
  }
2050
1971
  const lines = [];
2051
1972
  const sessionMode = session.sessionMode || 'interactive';
1973
+ const dispatchMode = session.metadata?.dispatchMode || 'mention';
2052
1974
  const chatModeLine = `会话模式: ${sessionMode}`;
1975
+ const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
2053
1976
  if (isAdmin) {
2054
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
1977
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`);
2055
1978
  if (health.consecutiveErrors > 0) {
2056
1979
  lines.push(`异常计数: ${health.consecutiveErrors}`);
2057
1980
  }
2058
1981
  lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
2059
1982
  }
2060
1983
  else {
2061
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1984
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
2062
1985
  }
2063
1986
  if (health.lastError) {
2064
1987
  lines.push('');
2065
1988
  lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
2066
1989
  lines.push(`错误信息: ${health.lastError.substring(0, 100)}`);
2067
1990
  }
2068
- return lines.join('\n');
1991
+ return { kind: 'command.result', text: lines.join('\n') };
2069
1992
  }
2070
1993
  // /new 命令:创建新会话(支持命名)
2071
1994
  if (normalizedContent.startsWith('/new')) {
@@ -2073,11 +1996,11 @@ export class CommandHandler {
2073
1996
  if (sessionName) {
2074
1997
  const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2075
1998
  if (existing) {
2076
- return `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称`;
1999
+ return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
2077
2000
  }
2078
2001
  }
2079
- const projectPath = session?.projectPath || this.getEffectiveDefaultPath(channel);
2080
- const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.defaultAgentId);
2002
+ const projectPath = this.getEffectiveDefaultPath(channel);
2003
+ const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.primaryRunnerKey);
2081
2004
  this.eventBus.publish({
2082
2005
  type: 'session:created',
2083
2006
  sessionId: newSession.id,
@@ -2093,7 +2016,7 @@ export class CommandHandler {
2093
2016
  await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
2094
2017
  await agent.closeSession(session.id);
2095
2018
  }
2096
- return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
2019
+ return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 之前的对话历史已保留,可通过 /s 查看` };
2097
2020
  }
2098
2021
  // /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
2099
2022
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
@@ -2106,17 +2029,18 @@ export class CommandHandler {
2106
2029
  allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
2107
2030
  }
2108
2031
  else {
2109
- // default 范围:所有 channel 中,不属于任何 evolagent 的
2032
+ // default 范围:不再有 default channel 概念,等价于"所有 channel"
2110
2033
  const defaultNames = [];
2111
2034
  for (const [name] of this.adapters) {
2112
2035
  const owner = this.agentRegistry?.resolveByChannel(name);
2113
- if (!owner || owner.isDefault)
2036
+ if (!owner)
2114
2037
  defaultNames.push(name);
2115
2038
  }
2116
2039
  allowedChannels = new Set(defaultNames);
2117
2040
  }
2118
2041
  // Default: show system health check (non-admin 仅看摘要)
2119
- const lines = ['📡 渠道状态:'];
2042
+ const checkAgentName = checkOwningAgent?.name ?? 'DefaultAgent';
2043
+ const lines = [`📡 渠道状态 (Agent: ${checkAgentName}):`];
2120
2044
  // Group by channelType
2121
2045
  const groups = new Map();
2122
2046
  for (const [name] of this.adapters) {
@@ -2127,7 +2051,7 @@ export class CommandHandler {
2127
2051
  let status;
2128
2052
  if (ch?.getStatus) {
2129
2053
  const s = ch.getStatus();
2130
- status = s.connected ? '✓ 已连接' : s.reconnectAttempt > 0 ? `⏳ 重连中 (${s.reconnectAttempt}/${s.maxAttempts})` : '✗ 断开';
2054
+ status = s.connected ? '✓ 已连接' : '⏳ 重连中';
2131
2055
  }
2132
2056
  else {
2133
2057
  status = '✓ 已注册';
@@ -2141,19 +2065,23 @@ export class CommandHandler {
2141
2065
  const total = [...groups.values()].flat().length;
2142
2066
  const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
2143
2067
  lines.push(` ${healthy}/${total} 渠道正常`);
2144
- return lines.join('\n');
2068
+ return { kind: 'command.result', text: lines.join('\n') };
2145
2069
  }
2146
2070
  for (const [type, instances] of groups) {
2147
2071
  if (instances.length === 1) {
2148
- lines.push(` ${instances[0].name}: ${instances[0].status}`);
2072
+ lines.push(` ${type}: ${instances[0].status}`);
2149
2073
  }
2150
2074
  else {
2151
- const parts = instances.map(i => `${i.name} ${i.status}`);
2152
- lines.push(` ${type}: [${parts.join(', ')}]`);
2075
+ const parts = instances.map(i => {
2076
+ const seg = i.name.split('#');
2077
+ const instName = seg.length >= 3 ? seg.slice(2).join('#') : i.name;
2078
+ return `${i.status.includes('✓') ? '✓' : '⏳'} ${instName}`;
2079
+ });
2080
+ lines.push(` ${type}: ${parts.join(', ')}`);
2153
2081
  }
2154
2082
  }
2155
2083
  // 当前 agent 名(用于 agent 维度 stats / queue 查询)
2156
- const currentAgentName = checkOwningAgent?.name ?? '[default]';
2084
+ const currentAgentName = checkOwningAgent?.name ?? '<unknown>';
2157
2085
  // 队列状态(按当前 agent 维度)
2158
2086
  lines.push('', '📬 队列状态:');
2159
2087
  lines.push(` 待处理消息: ${this.messageQueue.getQueueLengthByAgent(currentAgentName)}`);
@@ -2187,19 +2115,15 @@ export class CommandHandler {
2187
2115
  lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
2188
2116
  }
2189
2117
  }
2190
- return lines.join('\n');
2118
+ return { kind: 'command.result', text: lines.join('\n') };
2191
2119
  }
2192
2120
  // /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
2193
2121
  if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
2194
2122
  const restartArg = normalizedContent.slice('/restart'.length).trim();
2195
- // /restart <type> — 重连指定类型的所有渠道(admin only,evolclaw 服务级操作)
2196
- // 服务级操作仅可从 default 通道发起,避免 evolagent owner/admin 越权
2123
+ // /restart <type> — 重连指定类型的所有渠道(admin only
2197
2124
  if (restartArg) {
2198
- if (this.getOwningAgent(channel)) {
2199
- return '❌ 渠道重连只能从 DefaultAgent 通道发起(服务级操作)';
2200
- }
2201
2125
  if (!isAdmin)
2202
- return '❌ 无权限:渠道重连仅限管理员使用';
2126
+ return { kind: 'command.error', text: '❌ 无权限:渠道重连仅限管理员使用' };
2203
2127
  const type = restartArg;
2204
2128
  // /restart 是服务级操作:重连该 type 下的所有实例(不分 agent)
2205
2129
  const scopedNames = [];
@@ -2208,7 +2132,7 @@ export class CommandHandler {
2208
2132
  scopedNames.push(name);
2209
2133
  }
2210
2134
  if (scopedNames.length === 0) {
2211
- return `❌ 没有类型为 "${type}" 的渠道`;
2135
+ return { kind: 'command.error', text: `❌ 没有类型为 "${type}" 的渠道` };
2212
2136
  }
2213
2137
  const results = [];
2214
2138
  for (const name of scopedNames) {
@@ -2229,15 +2153,11 @@ export class CommandHandler {
2229
2153
  results.push(`${name}: 重连失败 - ${e?.message || e}`);
2230
2154
  }
2231
2155
  }
2232
- return `🔄 重连 ${type}:\n ${results.join('\n ')}`;
2233
- }
2234
- // /restart(无参数)— 重启整个服务(owner only,且仅可从 default 通道触发)
2235
- // 防止 evolagent 通道的 owner 越权杀整个 evolclaw 进程(影响所有租户)
2236
- if (this.getOwningAgent(channel)) {
2237
- return '❌ 服务重启只能从 DefaultAgent 通道发起。EvolAgent 通道仅可执行 /restart <type> 重连特定类型渠道';
2156
+ return { kind: 'command.result', text: `🔄 重连 ${type}:\n ${results.join('\n ')}` };
2238
2157
  }
2158
+ // /restart(无参数)— 重启整个服务(owner only)
2239
2159
  if (!isOwner)
2240
- return '❌ 无权限:服务重启仅限 owner 使用';
2160
+ return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 owner 使用' };
2241
2161
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
2242
2162
  const sessionsWithMessages = allSessions
2243
2163
  .filter(s => this.messageCache.hasMessages(s.id))
@@ -2260,16 +2180,20 @@ export class CommandHandler {
2260
2180
  };
2261
2181
  fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2262
2182
  const { spawn } = await import('child_process');
2263
- spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
2183
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2264
2184
  detached: true,
2265
2185
  stdio: 'ignore',
2266
2186
  env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
2267
2187
  }).unref();
2268
2188
  this.eventBus.publish({ type: 'system:restart', channel, channelId });
2189
+ // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
2190
+ // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
2191
+ // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
2269
2192
  setTimeout(() => {
2270
2193
  logger.info('[System] Restarting by user command...');
2271
- process.exit(0);
2194
+ process.kill(process.pid, 'SIGTERM');
2272
2195
  }, 1000);
2196
+ return true;
2273
2197
  };
2274
2198
  // 文本确认流程
2275
2199
  if (sessionsWithMessages.length > 0) {
@@ -2283,38 +2207,38 @@ export class CommandHandler {
2283
2207
  }
2284
2208
  else {
2285
2209
  fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: now }));
2286
- return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
2210
+ return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
2287
2211
  }
2288
2212
  }
2289
2213
  else {
2290
2214
  fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: Date.now() }));
2291
- return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
2215
+ return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
2292
2216
  }
2293
2217
  }
2294
2218
  await executeRestart();
2295
- return '🔄 服务正在重启,请稍候...(约 5 秒后恢复)';
2219
+ return { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' };
2296
2220
  }
2297
2221
  // /pwd 命令:显示当前项目路径
2298
2222
  if (normalizedContent === '/pwd') {
2299
2223
  // session 现在总是存在(上面已自动创建)
2300
2224
  if (!session) {
2301
- return `❌ 无法创建会话,请检查配置`;
2225
+ return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
2302
2226
  }
2303
2227
  const configName = this.getConfiguredProjectName(session.projectPath);
2304
2228
  if (configName) {
2305
- return `当前项目: ${configName}\n路径: ${session.projectPath}`;
2229
+ return { kind: 'command.result', text: `当前项目: ${configName}\n路径: ${session.projectPath}` };
2306
2230
  }
2307
- return `当前项目: ${session.projectPath}`;
2231
+ return { kind: 'command.result', text: `当前项目: ${session.projectPath}` };
2308
2232
  }
2309
2233
  // /file 命令:发送项目内文件,支持 /file path 和 /file channel path(owner only)
2310
2234
  if (normalizedContent.startsWith('/file')) {
2311
2235
  if (!isOwner)
2312
- return '❌ 无权限:此命令仅限 owner 使用';
2236
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
2313
2237
  // 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
2314
2238
  // 还原: 将 [text](url) 替换为 text
2315
2239
  const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
2316
2240
  if (!rawArg) {
2317
- return '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md';
2241
+ return { kind: 'command.result', text: '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md' };
2318
2242
  }
2319
2243
  // 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
2320
2244
  const tokens = rawArg.split(/\s+/);
@@ -2344,89 +2268,89 @@ export class CommandHandler {
2344
2268
  const isCrossChannel = targetChannel !== channel;
2345
2269
  // 跨通道仅限 owner
2346
2270
  if (isCrossChannel && identity.role !== 'owner') {
2347
- return '❌ 跨通道发送仅限管理员';
2271
+ return { kind: 'command.error', text: '❌ 跨通道发送仅限管理员' };
2348
2272
  }
2349
2273
  // 找目标 adapter
2350
2274
  const targetAdapter = this.adapters.get(targetChannel);
2351
2275
  if (!targetAdapter) {
2352
- return `❌ 通道 ${targetLabel} 未启用或不存在`;
2276
+ return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 未启用或不存在` };
2353
2277
  }
2354
- if (!targetAdapter.sendFile) {
2355
- return `❌ 通道 ${targetLabel} 不支持文件发送`;
2278
+ if (!targetAdapter.capabilities?.file) {
2279
+ return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 不支持文件发送` };
2356
2280
  }
2357
2281
  // 获取 session(需要 projectPath)
2358
- const sendResult = await this.ensureSession(channel, channelId, threadId);
2282
+ const sendResult = await this.ensureSession(channel, channelId, threadId, chatType);
2359
2283
  if ('error' in sendResult)
2360
- return sendResult.error;
2284
+ return { kind: 'command.result', text: sendResult.error };
2361
2285
  const sendSession = sendResult.session;
2362
2286
  // 路径安全校验
2363
2287
  if (path.isAbsolute(filePath)) {
2364
- return '❌ 不支持绝对路径\n请使用项目内的相对路径';
2288
+ return { kind: 'command.error', text: '❌ 不支持绝对路径\n请使用项目内的相对路径' };
2365
2289
  }
2366
2290
  if (filePath.split(path.sep).includes('..') || filePath.split('/').includes('..')) {
2367
- return '❌ 不支持 .. 路径穿越';
2291
+ return { kind: 'command.error', text: '❌ 不支持 .. 路径穿越' };
2368
2292
  }
2369
2293
  const resolvedPath = path.resolve(sendSession.projectPath, filePath);
2370
2294
  // 存在性检查
2371
2295
  if (!fs.existsSync(resolvedPath)) {
2372
- return `❌ 文件不存在: ${filePath}`;
2296
+ return { kind: 'command.error', text: `❌ 文件不存在: ${filePath}` };
2373
2297
  }
2374
2298
  // 符号链接安全:realpath 后验证仍在项目目录内
2375
2299
  const realPath = fs.realpathSync(resolvedPath);
2376
2300
  const realProjectPath = fs.realpathSync(sendSession.projectPath);
2377
2301
  if (!realPath.startsWith(realProjectPath + path.sep) && realPath !== realProjectPath) {
2378
- return '❌ 路径不允许: 文件不在项目目录内';
2302
+ return { kind: 'command.error', text: '❌ 路径不允许: 文件不在项目目录内' };
2379
2303
  }
2380
2304
  const stat = fs.statSync(resolvedPath);
2381
2305
  if (stat.isDirectory()) {
2382
- return '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持';
2306
+ return { kind: 'command.error', text: '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持' };
2383
2307
  }
2384
2308
  const MAX_SIZE = 10 * 1024 * 1024;
2385
2309
  if (stat.size > MAX_SIZE) {
2386
- return `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)`;
2310
+ return { kind: 'command.error', text: `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)` };
2387
2311
  }
2388
2312
  // 找目标 channelId
2389
2313
  let targetChannelId = channelId;
2390
2314
  if (isCrossChannel) {
2391
- const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel) ?? getOwner(this.config, targetChannel);
2315
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel);
2392
2316
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
2393
2317
  if (!targetChannelId) {
2394
- return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
2318
+ return { kind: 'command.error', text: `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息` };
2395
2319
  }
2396
2320
  }
2397
2321
  // 发送文件
2398
2322
  try {
2399
2323
  const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
2400
- await targetAdapter.sendFile(targetChannelId, realPath, replyCtx);
2324
+ await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
2401
2325
  const sizeStr = stat.size < 1024 ? `${stat.size} B`
2402
2326
  : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
2403
2327
  : `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
2404
- return isCrossChannel
2405
- ? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
2406
- : `✅ 已发送: ${filePath} (${sizeStr})`;
2328
+ return { kind: 'command.result', text: isCrossChannel
2329
+ ? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
2330
+ : `✅ 已发送: ${filePath} (${sizeStr})` };
2407
2331
  }
2408
2332
  catch (error) {
2409
2333
  logger.error('[CommandHandler] /file failed:', error);
2410
- return `❌ 文件发送失败: ${error.message || error}`;
2334
+ return { kind: 'command.error', text: `❌ 文件发送失败: ${error.message || error}` };
2411
2335
  }
2412
2336
  }
2413
2337
  // /plist 命令:列出所有项目
2414
2338
  if (normalizedContent === '/plist') {
2415
2339
  if (!policy.canListProjects(session?.chatType || 'private', identity.role)) {
2416
2340
  if (!session) {
2417
- return `❌ 当前群聊未绑定项目
2341
+ return { kind: 'command.error', text: `❌ 当前群聊未绑定项目
2418
2342
 
2419
- 请使用 /bind <项目路径> 绑定项目`;
2343
+ 请使用 /bind <项目路径> 绑定项目` };
2420
2344
  }
2421
2345
  const projectName = this.getProjectName(session.projectPath);
2422
2346
  const isProcessing = !!session.processingState;
2423
2347
  const status = isProcessing ? '[处理中]' : '[空闲]';
2424
- return `当前群聊绑定的项目:
2348
+ return { kind: 'command.result', text: `当前群聊绑定的项目:
2425
2349
  ${projectName} (${session.projectPath}) - ${status}
2426
2350
 
2427
- 提示:群聊不支持切换项目`;
2351
+ 提示:群聊不支持切换项目` };
2428
2352
  }
2429
- // 收集项目信息并按最近活跃排序(唯一来源:evolclaw.json projects.list)
2353
+ // 收集项目信息并按最近活跃排序(唯一来源:agent config projects.list)
2430
2354
  const entries = [];
2431
2355
  for (const [name, projectPath] of Object.entries(this.projects)) {
2432
2356
  // 跳过不存在的路径
@@ -2471,14 +2395,8 @@ export class CommandHandler {
2471
2395
  }
2472
2396
  return parts.join(' ');
2473
2397
  };
2474
- // 尝试发送 ActionInteraction 卡片(每个项目一个按钮,一键切换)
2475
- if (this.interactionRouter && entries.length > 0) {
2476
- const requestId = `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
2477
- const buttons = entries.map(e => ({
2478
- key: e.name,
2479
- label: e.isCurrent ? `✓ ${e.name}` : e.name,
2480
- style: e.isCurrent ? 'primary' : 'default',
2481
- }));
2398
+ // 尝试发送 CommandCard 卡片(每个项目一个按钮,一键切换)
2399
+ if (entries.length > 0) {
2482
2400
  const bodyLines = entries.map(e => {
2483
2401
  const status = buildStatusText(e);
2484
2402
  const prefix = e.isCurrent ? '✓' : '•';
@@ -2486,35 +2404,27 @@ export class CommandHandler {
2486
2404
  });
2487
2405
  const interaction = {
2488
2406
  type: 'interaction',
2489
- id: requestId,
2407
+ id: `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
2490
2408
  channelId,
2491
- sessionId: activeSession?.id || requestId,
2409
+ sessionId: activeSession?.id || '',
2410
+ initiatorId: userId,
2492
2411
  kind: {
2493
- kind: 'action',
2412
+ kind: 'command-card',
2494
2413
  title: '📂 项目列表',
2495
2414
  body: bodyLines.join('\n'),
2496
- buttons,
2415
+ buttons: entries.map(e => ({
2416
+ label: e.isCurrent ? `✓ ${e.name}` : e.name,
2417
+ command: `/project ${e.name}`,
2418
+ style: (e.isCurrent ? 'primary' : 'default'),
2419
+ disabled: e.isCurrent,
2420
+ })),
2497
2421
  },
2498
2422
  };
2499
2423
  const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
2500
- const cardSent = await this.sendInteractionCard({
2501
- channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
2502
- canWrite: isAdmin,
2503
- callback: async (action, _values, operatorId) => {
2504
- if (userId && operatorId && operatorId !== userId)
2505
- return;
2506
- const selectedEntry = entries.find(e => e.name === action);
2507
- if (selectedEntry && !selectedEntry.isCurrent) {
2508
- const result = await this.handle(`/project ${action}`, channel, channelId, undefined, userId, threadId);
2509
- if (result) {
2510
- const adapter = this.adapters.get(channel);
2511
- adapter?.sendText(channelId, result, replyCtx);
2512
- }
2513
- }
2514
- },
2515
- });
2516
- if (cardSent)
2424
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
2425
+ if (cardResult === null)
2517
2426
  return null;
2427
+ return { kind: 'command.result', text: cardResult };
2518
2428
  }
2519
2429
  // 降级:文本列表
2520
2430
  const lines = ['可用项目:'];
@@ -2523,25 +2433,26 @@ export class CommandHandler {
2523
2433
  lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
2524
2434
  }
2525
2435
  lines.push('', '提示: 使用 /p <名称> 切换项目');
2526
- return lines.join('\n');
2436
+ return { kind: 'command.result', text: lines.join('\n') };
2527
2437
  }
2528
2438
  // /project(无参数):直接复用 /plist 逻辑(含卡片交互)
2529
2439
  if (normalizedContent === '/project') {
2530
2440
  if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
2531
2441
  // 群聊不能切换项目,交由 /plist 逻辑处理
2532
2442
  }
2533
- return this.handle('/plist', channel, channelId, undefined, userId, threadId);
2443
+ const delegated = await this.handle('/plist', channel, channelId, undefined, userId, threadId);
2444
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
2534
2445
  }
2535
2446
  // /project 命令:切换项目(支持名称或路径)
2536
2447
  if (normalizedContent.startsWith('/project ')) {
2537
2448
  if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
2538
- return `❌ 群聊不支持切换项目
2449
+ return { kind: 'command.error', text: `❌ 群聊不支持切换项目
2539
2450
 
2540
- 群聊只能绑定一个项目。如需更换项目,请联系管理员重新配置。`;
2451
+ 群聊只能绑定一个项目。如需更换项目,请联系管理员重新配置。` };
2541
2452
  }
2542
2453
  let arg = normalizedContent.slice(9).trim();
2543
2454
  if (!arg)
2544
- return '用法: /p <name|path> 或 /project <name|path>';
2455
+ return { kind: 'command.result', text: '用法: /p <name|path> 或 /project <name|path>' };
2545
2456
  // 检查确认标志
2546
2457
  const hasConfirm = arg.endsWith(' --confirm');
2547
2458
  if (hasConfirm) {
@@ -2551,10 +2462,10 @@ export class CommandHandler {
2551
2462
  let projectName;
2552
2463
  if (arg.includes('/')) {
2553
2464
  if (!path.isAbsolute(arg)) {
2554
- return '❌ 项目路径必须是绝对路径';
2465
+ return { kind: 'command.error', text: '❌ 项目路径必须是绝对路径' };
2555
2466
  }
2556
2467
  if (!fs.existsSync(arg)) {
2557
- return `❌ 路径不存在: ${arg}`;
2468
+ return { kind: 'command.error', text: `❌ 路径不存在: ${arg}` };
2558
2469
  }
2559
2470
  projectPath = arg;
2560
2471
  projectName = path.basename(arg);
@@ -2562,7 +2473,7 @@ export class CommandHandler {
2562
2473
  else {
2563
2474
  projectPath = this.projects[arg];
2564
2475
  if (!projectPath) {
2565
- return `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目`;
2476
+ return { kind: 'command.error', text: `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目` };
2566
2477
  }
2567
2478
  projectName = arg;
2568
2479
  }
@@ -2570,13 +2481,13 @@ export class CommandHandler {
2570
2481
  const normalizedSessionPath = path.resolve(session.projectPath);
2571
2482
  const normalizedProjectPath = path.resolve(projectPath);
2572
2483
  if (normalizedSessionPath === normalizedProjectPath) {
2573
- return `当前已在项目: ${projectName}\n 路径: ${projectPath}`;
2484
+ return { kind: 'command.result', text: `当前已在项目: ${projectName}\n 路径: ${projectPath}` };
2574
2485
  }
2575
2486
  }
2576
2487
  // 群聊切换项目需要确认
2577
2488
  const isGroupChat = session?.chatType === 'group';
2578
2489
  if (isGroupChat && !hasConfirm) {
2579
- return `⚠️ 群聊切换项目风险提示:
2490
+ return { kind: 'command.error', text: `⚠️ 群聊切换项目风险提示:
2580
2491
 
2581
2492
  切换项目将影响所有群成员的对话上下文,可能导致:
2582
2493
  • 当前项目的会话历史被切换
@@ -2584,9 +2495,9 @@ export class CommandHandler {
2584
2495
  • 其他成员的工作受到影响
2585
2496
 
2586
2497
  确认切换请执行:
2587
- /p ${projectName} --confirm`;
2498
+ /p ${projectName} --confirm` };
2588
2499
  }
2589
- const currentAgentId = activeSession?.agentId || this.defaultAgentId;
2500
+ const currentAgentId = activeSession?.agentId || this.primaryRunnerKey;
2590
2501
  const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath, currentAgentId);
2591
2502
  this.eventBus.publish({
2592
2503
  type: 'project:switched',
@@ -2598,7 +2509,7 @@ export class CommandHandler {
2598
2509
  });
2599
2510
  const cachedEvents = this.messageCache.getEvents(newSession.id);
2600
2511
  const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
2601
- const currentAgent = newSession.agentId || this.defaultAgentId;
2512
+ const currentAgent = newSession.agentId || this.primaryRunnerKey;
2602
2513
  let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n Agent: ${currentAgent}\n ${hasExistingSession}`;
2603
2514
  if (cachedEvents.length > 0 && sendMessage) {
2604
2515
  for (const event of cachedEvents) {
@@ -2617,28 +2528,28 @@ export class CommandHandler {
2617
2528
  await sendMessage(channelId, event.message);
2618
2529
  }
2619
2530
  this.messageCache.clearEvents(newSession.id);
2620
- return '';
2531
+ return { kind: 'command.result', text: '' };
2621
2532
  }
2622
- return response;
2533
+ return { kind: 'command.result', text: response };
2623
2534
  }
2624
2535
  // /bind 命令:持久化项目到配置(不切换)(owner only)
2625
2536
  if (normalizedContent === '/bind')
2626
- return '用法: /bind <路径>';
2537
+ return { kind: 'command.result', text: '用法: /bind <路径>' };
2627
2538
  if (normalizedContent.startsWith('/bind ')) {
2628
2539
  if (!isOwner)
2629
- return '❌ 无权限:此命令仅限 owner 使用';
2540
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
2630
2541
  const projectPath = normalizedContent.slice(6).trim();
2631
2542
  if (!projectPath)
2632
- return '用法: /bind <路径>';
2543
+ return { kind: 'command.result', text: '用法: /bind <路径>' };
2633
2544
  if (!path.isAbsolute(projectPath)) {
2634
- return '❌ 项目路径必须是绝对路径';
2545
+ return { kind: 'command.error', text: '❌ 项目路径必须是绝对路径' };
2635
2546
  }
2636
2547
  if (!fs.existsSync(projectPath)) {
2637
- if (this.config.projects?.autoCreate) {
2548
+ if (this.getOwningAgent(channel)?.config?.projects?.autoCreate) {
2638
2549
  fs.mkdirSync(projectPath, { recursive: true });
2639
2550
  }
2640
2551
  else {
2641
- return `❌ 路径不存在: ${projectPath}`;
2552
+ return { kind: 'command.error', text: `❌ 路径不存在: ${projectPath}` };
2642
2553
  }
2643
2554
  }
2644
2555
  // 生成项目名称(使用目录名)
@@ -2648,34 +2559,34 @@ export class CommandHandler {
2648
2559
  const existing = scopeProjects[projectName];
2649
2560
  if (existing) {
2650
2561
  if (existing === projectPath) {
2651
- return `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
2562
+ return { kind: 'command.result', text: `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目` };
2652
2563
  }
2653
- return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
2564
+ return { kind: 'command.error', text: `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件` };
2654
2565
  }
2655
- // 写入:agent-owned channel → agent.json;default → evolclaw.json
2566
+ // 写入:agent-owned channel → agent.json;default → agent config
2656
2567
  const err = await this.addProjectInScope(channel, projectName, projectPath);
2657
2568
  if (err)
2658
- return err;
2659
- return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
2569
+ return { kind: 'command.result', text: err };
2570
+ return { kind: 'command.result', text: `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目` };
2660
2571
  }
2661
2572
  // /slist 命令:列出当前项目的会话
2662
2573
  // /slist — 仅 EvolClaw 会话
2663
2574
  // /slist cli — 仅 CLI 会话(未导入的)
2664
2575
  if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
2665
2576
  if (!session) {
2666
- return `❌ 当前没有活跃会话
2577
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
2667
2578
 
2668
2579
  请先执行以下操作之一:
2669
2580
  1. 发送任意消息 - 自动创建新会话
2670
2581
  2. /new [名称] - 创建命名会话
2671
- 3. /p <项目> - 切换到指定项目`;
2582
+ 3. /p <项目> - 切换到指定项目` };
2672
2583
  }
2673
2584
  const showCliOnly = normalizedContent === '/slist cli';
2674
2585
  // /slist cli — 仅显示 CLI 会话
2675
2586
  if (showCliOnly) {
2676
2587
  const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
2677
2588
  if (!canImportCli) {
2678
- return '❌ 当前无权查看 CLI 会话';
2589
+ return { kind: 'command.error', text: '❌ 当前无权查看 CLI 会话' };
2679
2590
  }
2680
2591
  const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
2681
2592
  const sessions = await this.sessionManager.listSessions(channel, channelId);
@@ -2683,7 +2594,7 @@ export class CommandHandler {
2683
2594
  const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
2684
2595
  const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
2685
2596
  if (orphanCliSessions.length === 0) {
2686
- return `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话`;
2597
+ return { kind: 'command.result', text: `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话` };
2687
2598
  }
2688
2599
  // 构建显示数据(复用于卡片和文本)
2689
2600
  const cliDisplayItems = orphanCliSessions.map(c => {
@@ -2692,42 +2603,31 @@ export class CommandHandler {
2692
2603
  const uuid = c.uuid.substring(0, 8);
2693
2604
  return { uuid, fullUuid: c.uuid, time, message };
2694
2605
  });
2695
- // 尝试发送 ActionInteraction 卡片
2606
+ // 尝试发送 CommandCard 卡片
2696
2607
  if (this.interactionRouter && cliDisplayItems.length > 0) {
2697
- const requestId = `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
2698
- const buttons = cliDisplayItems.map(item => ({
2699
- key: item.uuid,
2700
- label: item.uuid,
2701
- style: 'default',
2702
- }));
2703
2608
  const bodyLines = cliDisplayItems.map(item => `• ${item.time} (${item.uuid}) "${item.message}"`);
2704
2609
  const interaction = {
2705
2610
  type: 'interaction',
2706
- id: requestId,
2611
+ id: `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
2707
2612
  channelId,
2708
2613
  sessionId: session.id,
2614
+ initiatorId: userId,
2709
2615
  kind: {
2710
- kind: 'action',
2616
+ kind: 'command-card',
2711
2617
  title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
2712
2618
  body: bodyLines.join('\n'),
2713
- buttons,
2619
+ buttons: cliDisplayItems.map(item => ({
2620
+ label: item.uuid,
2621
+ command: `/session ${item.uuid}`,
2622
+ style: 'default',
2623
+ })),
2714
2624
  },
2715
2625
  };
2716
2626
  const replyCtx = this.getReplyContext(session);
2717
- const cardSent = await this.sendInteractionCard({
2718
- channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
2719
- callback: async (action, _values, operatorId) => {
2720
- if (userId && operatorId && operatorId !== userId)
2721
- return;
2722
- const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
2723
- if (result) {
2724
- const adapter = this.adapters.get(channel);
2725
- adapter?.sendText(channelId, result, replyCtx);
2726
- }
2727
- },
2728
- });
2729
- if (cardSent)
2627
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
2628
+ if (cardResult === null)
2730
2629
  return null;
2630
+ return { kind: 'command.result', text: cardResult };
2731
2631
  }
2732
2632
  // 降级:文本列表
2733
2633
  const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
@@ -2736,7 +2636,7 @@ export class CommandHandler {
2736
2636
  }
2737
2637
  lines.push('');
2738
2638
  lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
2739
- return lines.join('\n');
2639
+ return { kind: 'command.result', text: lines.join('\n') };
2740
2640
  }
2741
2641
  // /slist — 仅显示 EvolClaw 会话
2742
2642
  const sessions = await this.sessionManager.listSessions(channel, channelId);
@@ -2786,17 +2686,8 @@ export class CommandHandler {
2786
2686
  }
2787
2687
  displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
2788
2688
  }
2789
- // 尝试发送 ActionInteraction 卡片(每个会话一个按钮,一键切换)
2689
+ // 尝试发送 CommandCard 卡片(每个会话一个按钮,一键切换)
2790
2690
  if (this.interactionRouter && displaySessions.length >= 1) {
2791
- const requestId = `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
2792
- const buttons = displaySessions.map(ds => {
2793
- const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
2794
- return {
2795
- key: String(ds.index),
2796
- label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
2797
- style: ds.isActive ? 'primary' : 'default',
2798
- };
2799
- });
2800
2691
  const bodyLines = displaySessions.map(ds => {
2801
2692
  const prefix = ds.isActive ? '✓' : '•';
2802
2693
  const threadTag = ds.session.threadId ? '[话题] ' : '';
@@ -2806,34 +2697,30 @@ export class CommandHandler {
2806
2697
  });
2807
2698
  const interaction = {
2808
2699
  type: 'interaction',
2809
- id: requestId,
2700
+ id: `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
2810
2701
  channelId,
2811
2702
  sessionId: session.id,
2703
+ initiatorId: userId,
2812
2704
  kind: {
2813
- kind: 'action',
2705
+ kind: 'command-card',
2814
2706
  title: `📋 ${path.basename(session.projectPath)} 会话列表`,
2815
2707
  body: bodyLines.join('\n'),
2816
- buttons,
2708
+ buttons: displaySessions.map(ds => {
2709
+ const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
2710
+ return {
2711
+ label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
2712
+ command: `/session ${ds.index}`,
2713
+ style: (ds.isActive ? 'primary' : 'default'),
2714
+ disabled: ds.isActive,
2715
+ };
2716
+ }),
2817
2717
  },
2818
2718
  };
2819
2719
  const replyCtx = this.getReplyContext(session);
2820
- const cardSent = await this.sendInteractionCard({
2821
- channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
2822
- callback: async (action, _values, operatorId) => {
2823
- if (userId && operatorId && operatorId !== userId)
2824
- return;
2825
- const target = displaySessions.find(ds => String(ds.index) === action);
2826
- if (target && !target.isActive) {
2827
- const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
2828
- if (result) {
2829
- const adapter = this.adapters.get(channel);
2830
- adapter?.sendText(channelId, result, replyCtx);
2831
- }
2832
- }
2833
- },
2834
- });
2835
- if (cardSent)
2720
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
2721
+ if (cardResult === null)
2836
2722
  return null;
2723
+ return { kind: 'command.result', text: cardResult };
2837
2724
  }
2838
2725
  // 降级:文本列表
2839
2726
  const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
@@ -2863,21 +2750,23 @@ export class CommandHandler {
2863
2750
  }
2864
2751
  lines.push('使用 /s <序号、name或8位uuid> 切换会话');
2865
2752
  lines.push('使用 /s cli 查看 CLI 会话');
2866
- return lines.join('\n');
2753
+ return { kind: 'command.result', text: lines.join('\n') };
2867
2754
  }
2868
2755
  // /session(无参数):直接复用 /slist 逻辑(含卡片交互)
2869
2756
  if (normalizedContent === '/session') {
2870
- return this.handle('/slist', channel, channelId, undefined, userId, threadId);
2757
+ const delegated = await this.handle('/slist', channel, channelId, undefined, userId, threadId);
2758
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
2871
2759
  }
2872
2760
  // /session cli(= /s cli):列出未导入的 CLI 会话
2873
2761
  if (normalizedContent === '/session cli') {
2874
- return this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
2762
+ const delegated = await this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
2763
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
2875
2764
  }
2876
2765
  // /session 或 /s 命令:切换会话
2877
2766
  if (normalizedContent.startsWith('/session ')) {
2878
2767
  const sessionName = normalizedContent.slice(9).trim();
2879
2768
  if (!sessionName)
2880
- return '用法: /s <序号、会话名称或前8位UUID>';
2769
+ return { kind: 'command.result', text: '用法: /s <序号、会话名称或前8位UUID>' };
2881
2770
  let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2882
2771
  // 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
2883
2772
  if (!targetSession && /^\d+$/.test(sessionName) && session) {
@@ -2893,7 +2782,7 @@ export class CommandHandler {
2893
2782
  targetSession = visibleSessions[idx - 1];
2894
2783
  }
2895
2784
  else {
2896
- return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2785
+ return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
2897
2786
  }
2898
2787
  }
2899
2788
  if (!targetSession && sessionName.length >= 8) {
@@ -2906,19 +2795,19 @@ export class CommandHandler {
2906
2795
  projectPaths.unshift(session.projectPath);
2907
2796
  }
2908
2797
  for (const projectPath of projectPaths) {
2909
- const currentAgentId = session?.agentId || this.defaultAgentId;
2798
+ const currentAgentId = session?.agentId || this.primaryRunnerKey;
2910
2799
  const cliSessions = await this.sessionManager.scanCliSessions(projectPath, currentAgentId);
2911
2800
  const cliSession = cliSessions.find(c => c.uuid.startsWith(sessionName));
2912
2801
  if (cliSession) {
2913
2802
  const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
2914
2803
  this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
2915
2804
  const projectName = this.getProjectName(projectPath);
2916
- return `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史`;
2805
+ return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史` };
2917
2806
  }
2918
2807
  }
2919
2808
  }
2920
2809
  if (!targetSession) {
2921
- return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
2810
+ return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
2922
2811
  }
2923
2812
  const lastInput = targetSession.agentSessionId
2924
2813
  ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
@@ -2927,64 +2816,64 @@ export class CommandHandler {
2927
2816
  if (!session) {
2928
2817
  const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
2929
2818
  if (!switched) {
2930
- return `❌ 切换会话失败`;
2819
+ return { kind: 'command.error', text: `❌ 切换会话失败` };
2931
2820
  }
2932
- return `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}`;
2821
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
2933
2822
  }
2934
2823
  if (targetSession.id === session.id) {
2935
- return `当前已在会话: ${targetSession.name || sessionName}`;
2824
+ return { kind: 'command.result', text: `当前已在会话: ${targetSession.name || sessionName}` };
2936
2825
  }
2937
2826
  // 阻止从主会话切换到话题会话
2938
2827
  if (!session.threadId && targetSession.threadId) {
2939
- return `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用`;
2828
+ return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
2940
2829
  }
2941
2830
  const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
2942
2831
  if (!switched) {
2943
- return `❌ 切换会话失败`;
2832
+ return { kind: 'command.error', text: `❌ 切换会话失败` };
2944
2833
  }
2945
2834
  this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
2946
2835
  const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
2947
- return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
2836
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}` };
2948
2837
  }
2949
2838
  // /rename 或 /name 命令:重命名当前会话
2950
2839
  if (normalizedContent === '/rename' || normalizedContent === '/name') {
2951
- return '用法: /name <新名称> 或 /rename <新名称>';
2840
+ return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
2952
2841
  }
2953
2842
  if (normalizedContent.startsWith('/rename ')) {
2954
2843
  const newName = normalizedContent.slice(8).trim();
2955
2844
  if (!newName)
2956
- return '用法: /name <新名称> 或 /rename <新名称>';
2845
+ return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
2957
2846
  if (!session) {
2958
- return `❌ 当前没有活跃会话
2847
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
2959
2848
 
2960
2849
  请先执行以下操作之一:
2961
2850
  1. 发送任意消息 - 自动创建新会话
2962
2851
  2. /new [名称] - 创建命名会话
2963
- 3. /session <名称> - 切换到已有会话`;
2852
+ 3. /session <名称> - 切换到已有会话` };
2964
2853
  }
2965
2854
  const existing = await this.sessionManager.getSessionByName(channel, channelId, newName);
2966
2855
  if (existing && existing.id !== session.id) {
2967
- return `❌ 会话名称 "${newName}" 已存在,请使用其他名称`;
2856
+ return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
2968
2857
  }
2969
2858
  const oldName = session.name || '(未命名)';
2970
2859
  const success = await this.sessionManager.renameSession(session.id, newName);
2971
2860
  if (!success) {
2972
- return `❌ 重命名失败`;
2861
+ return { kind: 'command.error', text: `❌ 重命名失败` };
2973
2862
  }
2974
2863
  this.eventBus.publish({ type: 'session:renamed', sessionId: session.id, oldName, newName });
2975
- return `✓ 已将当前会话重命名为: ${newName}`;
2864
+ return { kind: 'command.result', text: `✓ 已将当前会话重命名为: ${newName}` };
2976
2865
  }
2977
2866
  // /del 命令:删除指定会话(仅解绑,不删除文件)
2978
2867
  if (normalizedContent.startsWith('/del ')) {
2979
2868
  const sessionName = normalizedContent.slice(5).trim();
2980
2869
  if (!sessionName)
2981
- return '用法: /del <序号、会话名称或前8位UUID>';
2870
+ return { kind: 'command.result', text: '用法: /del <序号、会话名称或前8位UUID>' };
2982
2871
  if (!session) {
2983
- return `❌ 当前没有活跃会话`;
2872
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话` };
2984
2873
  }
2985
2874
  // 权限检查:policy 控制谁可以删除会话
2986
2875
  if (!policy.canDeleteSession(session.chatType || 'private', identity.role)) {
2987
- return `❌ 无权限:群聊中仅管理员可删除会话`;
2876
+ return { kind: 'command.error', text: `❌ 无权限:群聊中仅管理员可删除会话` };
2988
2877
  }
2989
2878
  let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2990
2879
  // 序号删除(与 /slist 显示序号一致)
@@ -3000,107 +2889,107 @@ export class CommandHandler {
3000
2889
  targetSession = visibleSessions[idx - 1];
3001
2890
  }
3002
2891
  else {
3003
- return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2892
+ return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
3004
2893
  }
3005
2894
  }
3006
2895
  if (!targetSession && sessionName.length >= 8) {
3007
2896
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
3008
2897
  }
3009
2898
  if (!targetSession) {
3010
- return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
2899
+ return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
3011
2900
  }
3012
2901
  if (targetSession.id === session.id) {
3013
- return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
2902
+ return { kind: 'command.error', text: `❌ 无法删除当前活跃会话\n请先切换到其他会话` };
3014
2903
  }
3015
2904
  const success = await this.sessionManager.unbindSession(targetSession.id);
3016
2905
  if (!success) {
3017
- return `❌ 删除失败`;
2906
+ return { kind: 'command.error', text: `❌ 删除失败` };
3018
2907
  }
3019
2908
  this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
3020
2909
  const targetAgent = this.getAgent(channel, targetSession.agentId);
3021
2910
  await targetAgent.closeSession(targetSession.id);
3022
- return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
2911
+ return { kind: 'command.result', text: `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问` };
3023
2912
  }
3024
2913
  // /fork 命令:分支当前会话
3025
2914
  if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
3026
2915
  const forkName = normalizedContent.slice(5).trim() || undefined;
3027
2916
  if (!session) {
3028
- return `❌ 当前没有活跃会话,无法分支`;
2917
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话,无法分支` };
3029
2918
  }
3030
2919
  if (!session.agentSessionId) {
3031
- return `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
2920
+ return { kind: 'command.error', text: `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork` };
3032
2921
  }
3033
2922
  const forkAgent = this.getAgent(channel, session.agentId);
3034
2923
  if (!forkAgent.capabilities?.fork) {
3035
- return `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代`;
2924
+ return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
3036
2925
  }
3037
2926
  try {
3038
2927
  const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
3039
2928
  const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
3040
2929
  this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
3041
- return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话`;
2930
+ return { kind: 'command.result', text: `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
3042
2931
  }
3043
2932
  catch (error) {
3044
2933
  logger.error('[CommandHandler] Fork session failed:', error);
3045
- return `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}`;
2934
+ return { kind: 'command.error', text: `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}` };
3046
2935
  }
3047
2936
  }
3048
2937
  // /rewind 命令:查看历史 / 回退会话
3049
2938
  if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
3050
- const result = await this.ensureSession(channel, channelId, threadId);
2939
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
3051
2940
  if ('error' in result)
3052
- return result.error;
2941
+ return { kind: 'command.error', text: result.error };
3053
2942
  const { session } = result;
3054
2943
  const rewindAgent = this.getAgent(channel, session.agentId);
3055
2944
  if (rewindAgent.name !== 'claude') {
3056
- return '❌ /rewind 仅支持 Claude 后端';
2945
+ return { kind: 'command.error', text: '❌ /rewind 仅支持 Claude 后端' };
3057
2946
  }
3058
2947
  if (!session.agentSessionId) {
3059
- return '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind';
2948
+ return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
3060
2949
  }
3061
2950
  if (!rewindAgent.getSessionMessages) {
3062
- return '❌ 当前 Agent 不支持 /rewind';
2951
+ return { kind: 'command.error', text: '❌ 当前 Agent 不支持 /rewind' };
3063
2952
  }
3064
2953
  const args = normalizedContent.slice('/rewind'.length).trim();
3065
2954
  if (!args) {
3066
- return await this.handleRewindList(session, rewindAgent);
2955
+ return { kind: 'command.result', text: await this.handleRewindList(session, rewindAgent) };
3067
2956
  }
3068
2957
  // 带参(执行回退,会删除文件/改对话)需 admin+
3069
2958
  if (!isAdmin)
3070
- return '❌ 无权限:回退操作仅限管理员使用';
2959
+ return { kind: 'command.error', text: '❌ 无权限:回退操作仅限管理员使用' };
3071
2960
  const parts = args.split(/\s+/);
3072
2961
  const turnNum = parseInt(parts[0], 10);
3073
2962
  if (isNaN(turnNum) || turnNum < 1) {
3074
- return '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)';
2963
+ return { kind: 'command.error', text: '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)' };
3075
2964
  }
3076
2965
  const mode = parts[1]?.toLowerCase();
3077
2966
  if (!mode) {
3078
- return `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)`;
2967
+ return { kind: 'command.error', text: `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)` };
3079
2968
  }
3080
2969
  if (!['chat', 'file', 'all'].includes(mode)) {
3081
- return `❌ 无效模式 "${mode}",可选:chat | file | all`;
2970
+ return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
3082
2971
  }
3083
- return await this.handleRewind(session, rewindAgent, turnNum, mode);
2972
+ return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
3084
2973
  }
3085
2974
  // /repair 命令:检查并修复会话文件
3086
2975
  if (normalizedContent === '/repair') {
3087
- const repairResult = await this.ensureSession(channel, channelId, threadId);
2976
+ const repairResult = await this.ensureSession(channel, channelId, threadId, chatType);
3088
2977
  if ('error' in repairResult)
3089
- return repairResult.error;
2978
+ return { kind: 'command.result', text: repairResult.error };
3090
2979
  const { session: repairSession } = repairResult;
3091
2980
  const repairAgent = this.getAgent(channel, repairSession.agentId);
3092
2981
  const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
3093
2982
  try {
3094
2983
  if (!repairSession.agentSessionId) {
3095
2984
  await this.sessionManager.resetHealthStatus(repairSession.id);
3096
- return `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器`;
2985
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器` };
3097
2986
  }
3098
2987
  // 通过 agent 定位 session 文件
3099
2988
  const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
3100
2989
  if (!sessionFile) {
3101
2990
  // 文件不存在(已被删除或从未创建),直接重置
3102
2991
  await this.sessionManager.resetHealthStatus(repairSession.id);
3103
- return `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器`;
2992
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器` };
3104
2993
  }
3105
2994
  const healthCheck = await checkSessionFile(sessionFile);
3106
2995
  if (healthCheck.corrupt) {
@@ -3110,26 +2999,146 @@ export class CommandHandler {
3110
2999
  await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
3111
3000
  repairAgent.updateSessionId(repairSession.id, '');
3112
3001
  await this.sessionManager.resetHealthStatus(repairSession.id);
3113
- return `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
3002
+ return { kind: 'command.result', text: `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}` };
3114
3003
  }
3115
3004
  if (healthCheck.issues.length > 0) {
3116
3005
  await this.sessionManager.resetHealthStatus(repairSession.id);
3117
- return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
3006
+ return { kind: 'command.error', text: `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。` };
3118
3007
  }
3119
3008
  await this.sessionManager.resetHealthStatus(repairSession.id);
3120
- return `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器`;
3009
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器` };
3121
3010
  }
3122
3011
  catch (error) {
3123
3012
  logger.error('[Repair] Failed:', error);
3124
- return `❌ 修复失败: ${error.message}`;
3013
+ return { kind: 'command.error', text: `❌ 修复失败: ${error.message}` };
3125
3014
  }
3126
3015
  }
3127
3016
  // /safe 命令:安全模式已禁用
3128
3017
  if (normalizedContent === '/safe') {
3129
- return `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。`;
3018
+ return { kind: 'command.result', text: `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。` };
3019
+ }
3020
+ // /trigger 命令
3021
+ if (normalizedContent === '/trigger' || normalizedContent.startsWith('/trigger ')) {
3022
+ const text = this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin);
3023
+ return { kind: 'command.result', text };
3130
3024
  }
3131
3025
  return null;
3132
3026
  }
3027
+ handleTrigger(content, channel, channelId, peerId, isAdmin) {
3028
+ const scheduler = this.triggerScheduler;
3029
+ const manager = this.triggerManager;
3030
+ // Bare /trigger → list active
3031
+ if (content === '/trigger') {
3032
+ if (!manager)
3033
+ return '⚠️ 触发器功能未启用';
3034
+ const active = manager.listActive();
3035
+ if (active.length === 0)
3036
+ return '📭 当前没有活跃的触发器';
3037
+ const lines = active.map(t => {
3038
+ const next = new Date(t.nextFireAt).toLocaleString();
3039
+ const fired = t.fireCount > 0 ? ` | 已触发 ${t.fireCount} 次` : '';
3040
+ return `• **${t.name}** [${t.scheduleType}] 下次: ${next}${fired}`;
3041
+ });
3042
+ return `📋 活跃触发器(${active.length} 个):\n\n${lines.join('\n')}`;
3043
+ }
3044
+ const sub = content.slice('/trigger '.length).trim();
3045
+ // /trigger list → list all (active + history)
3046
+ if (sub === 'list' || sub.startsWith('list ')) {
3047
+ if (!manager)
3048
+ return '⚠️ 触发器功能未启用';
3049
+ const { active, history } = manager.listAll();
3050
+ const lines = [];
3051
+ if (active.length > 0) {
3052
+ lines.push(`**活跃 (${active.length})**`);
3053
+ for (const t of active) {
3054
+ const next = new Date(t.nextFireAt).toLocaleString();
3055
+ lines.push(`• ${t.name} [${t.scheduleType}] 下次: ${next} | 触发 ${t.fireCount} 次`);
3056
+ }
3057
+ }
3058
+ if (history.length > 0) {
3059
+ lines.push(`\n**历史 (${history.length})**`);
3060
+ for (const h of history.slice(-10)) {
3061
+ const done = new Date(h.doneAt).toLocaleString();
3062
+ lines.push(`• ${h.name} [${h.doneReason}] ${done}`);
3063
+ }
3064
+ }
3065
+ if (lines.length === 0)
3066
+ return '📭 没有触发器记录';
3067
+ return lines.join('\n');
3068
+ }
3069
+ // /trigger cancel <name|id>
3070
+ if (sub.startsWith('cancel ')) {
3071
+ if (!manager || !scheduler)
3072
+ return '⚠️ 触发器功能未启用';
3073
+ const nameOrId = sub.slice('cancel '.length).trim();
3074
+ if (!nameOrId)
3075
+ return '❌ 用法:/trigger cancel <名称>';
3076
+ // Find trigger: non-admin lookup is scoped to (peerId, channel) to avoid info disclosure
3077
+ // Non-admins can cancel by name or by their own trigger's UUID
3078
+ let trigger;
3079
+ if (isAdmin) {
3080
+ trigger = manager.getByName(nameOrId) ?? manager.getById(nameOrId);
3081
+ }
3082
+ else {
3083
+ trigger = manager.getByNameScoped(nameOrId, peerId, channel)
3084
+ ?? manager.getByIdScoped(nameOrId, peerId, channel);
3085
+ }
3086
+ if (!trigger) {
3087
+ return isAdmin
3088
+ ? `❌ 未找到触发器:${nameOrId}`
3089
+ : `❌ 未找到触发器 "${nameOrId}",或无权限取消`;
3090
+ }
3091
+ manager.moveToDone(trigger.id, 'cancelled');
3092
+ scheduler.cancel(trigger.id);
3093
+ this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: peerId });
3094
+ return `✅ 触发器已取消:**${trigger.name}**`;
3095
+ }
3096
+ // /trigger set ...
3097
+ if (sub.startsWith('set ')) {
3098
+ if (!manager || !scheduler)
3099
+ return '⚠️ 触发器功能未启用';
3100
+ const args = sub.slice('set '.length);
3101
+ const result = parseTriggerSet(args);
3102
+ if (!result.ok)
3103
+ return `❌ ${result.error}`;
3104
+ const parsed = result.value;
3105
+ const now = Date.now();
3106
+ const nextFireAt = calcNextFireAt(parsed.scheduleType, parsed.scheduleValue, now);
3107
+ // Auto-generate name if not provided
3108
+ const name = parsed.name ?? `trigger-${Date.now().toString(36)}`;
3109
+ const trigger = {
3110
+ id: crypto.randomUUID(),
3111
+ name,
3112
+ scheduleType: parsed.scheduleType,
3113
+ scheduleValue: parsed.scheduleValue,
3114
+ nextFireAt,
3115
+ targetChannel: parsed.targetChannel ?? channel,
3116
+ targetChannelId: parsed.targetChannelId ?? channelId,
3117
+ targetThreadId: parsed.targetThreadId,
3118
+ targetSessionStrategy: parsed.targetSessionStrategy,
3119
+ agentId: parsed.agentId,
3120
+ prompt: parsed.prompt,
3121
+ createdByPeerId: peerId,
3122
+ createdByChannel: channel,
3123
+ fireCount: 0,
3124
+ createdAt: now,
3125
+ updatedAt: now,
3126
+ };
3127
+ try {
3128
+ // Validate name uniqueness before persisting (manager.register writes to disk)
3129
+ // scheduler.register is in-memory only and cannot fail, so order is safe here.
3130
+ // If manager.register throws (duplicate name/ID), nothing is persisted.
3131
+ manager.register(trigger);
3132
+ scheduler.register(trigger);
3133
+ }
3134
+ catch (err) {
3135
+ return `❌ 注册失败:${err.message}`;
3136
+ }
3137
+ const nextStr = new Date(nextFireAt).toLocaleString();
3138
+ return `✅ 触发器已注册:**${name}**\n下次触发:${nextStr}`;
3139
+ }
3140
+ return `❌ 未知子命令。用法:\n/trigger — 查看活跃触发器\n/trigger list — 查看所有触发器\n/trigger set <参数> — 注册触发器\n/trigger cancel <名称> — 取消触发器`;
3141
+ }
3133
3142
  // ── /rewind helpers ──
3134
3143
  async handleRewindList(session, agent) {
3135
3144
  try {
@@ -3246,7 +3255,7 @@ export class CommandHandler {
3246
3255
  static CTL_COMMANDS = [
3247
3256
  '/help', '/status', '/check', '/pwd',
3248
3257
  '/model', '/effort', '/perm', '/agent',
3249
- '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
3258
+ '/compact', '/file', '/send', '/restart', '/bind', '/aid', '/rpc', '/storage',
3250
3259
  '/rename', '/name', '/evolagent',
3251
3260
  ];
3252
3261
  /** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
@@ -3353,7 +3362,7 @@ export class CommandHandler {
3353
3362
  return { ok: false, error: `adapter 未找到: ${session.channel}` };
3354
3363
  try {
3355
3364
  const replyContext = this.buildCtlReplyContext(session);
3356
- await adapter.sendText(session.channelId, text, replyContext);
3365
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: session.channelId, replyContext: replyContext }), { kind: 'result.text', text, isFinal: true });
3357
3366
  return { ok: true, result: '已发送' };
3358
3367
  }
3359
3368
  catch (err) {
@@ -3366,8 +3375,9 @@ export class CommandHandler {
3366
3375
  const parts = sendArgs.split(/\s+/);
3367
3376
  const filePath = parts[parts.length - 1];
3368
3377
  if (filePath) {
3369
- const resolved = path.resolve(session.projectPath, filePath);
3370
- if (!resolved.startsWith(session.projectPath)) {
3378
+ const resolved = path.resolve(session.projectPath, filePath).replace(/\\/g, '/');
3379
+ const projectPath = session.projectPath.replace(/\\/g, '/');
3380
+ if (!resolved.startsWith(projectPath)) {
3371
3381
  return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
3372
3382
  }
3373
3383
  }
@@ -3376,7 +3386,8 @@ export class CommandHandler {
3376
3386
  try {
3377
3387
  const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
3378
3388
  userId);
3379
- return { ok: true, result: result ?? '(无输出)' };
3389
+ const text = typeof result === 'string' ? result : (result && 'text' in result ? result.text : '(无输出)');
3390
+ return { ok: true, result: text || '(无输出)' };
3380
3391
  }
3381
3392
  catch (err) {
3382
3393
  return { ok: false, error: err.message };