evolclaw 3.1.5 → 3.1.7

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 (51) hide show
  1. package/CHANGELOG.md +68 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +78 -321
  4. package/dist/agents/manifest-engine.js +243 -0
  5. package/dist/agents/message-renderer.js +112 -0
  6. package/dist/aun/aid/agentmd.js +10 -3
  7. package/dist/aun/msg/group.js +2 -2
  8. package/dist/channels/aun.js +154 -18
  9. package/dist/channels/dingtalk.js +1 -1
  10. package/dist/channels/feishu.js +31 -9
  11. package/dist/channels/qqbot.js +1 -1
  12. package/dist/channels/wechat.js +1 -1
  13. package/dist/channels/wecom.js +1 -1
  14. package/dist/cli/agent.js +10 -11
  15. package/dist/cli/bench.js +1 -5
  16. package/dist/cli/help.js +8 -0
  17. package/dist/cli/index.js +91 -128
  18. package/dist/cli/init.js +37 -21
  19. package/dist/cli/link-rules.js +1 -7
  20. package/dist/cli/model.js +231 -6
  21. package/dist/config-store.js +1 -22
  22. package/dist/core/command-handler.js +181 -48
  23. package/dist/core/evolagent.js +0 -18
  24. package/dist/core/message/im-renderer.js +9 -20
  25. package/dist/core/message/message-bridge.js +9 -10
  26. package/dist/core/message/message-processor.js +188 -39
  27. package/dist/core/message/message-queue.js +15 -1
  28. package/dist/core/relation/peer-identity.js +23 -11
  29. package/dist/core/trigger/parser.js +4 -4
  30. package/dist/core/trigger/scheduler.js +43 -13
  31. package/dist/index.js +102 -52
  32. package/dist/ipc.js +1 -1
  33. package/dist/utils/error-utils.js +6 -0
  34. package/dist/utils/process-introspect.js +7 -5
  35. package/kits/docs/INDEX.md +4 -8
  36. package/kits/docs/context-assembly.md +1 -0
  37. package/kits/docs/evolclaw/INDEX.md +43 -0
  38. package/kits/docs/evolclaw/group.md +13 -6
  39. package/kits/docs/evolclaw/model.md +51 -0
  40. package/kits/docs/evolclaw/msg.md +5 -0
  41. package/kits/docs/venues/group.md +13 -1
  42. package/kits/eck_manifest.json +9 -0
  43. package/kits/eck_message_manifest.json +14 -0
  44. package/kits/rules/06-channel.md +5 -1
  45. package/kits/templates/message-fragments/item.md +2 -0
  46. package/kits/templates/system-fragments/baseagent.md +7 -1
  47. package/kits/templates/system-fragments/channel.md +7 -5
  48. package/kits/templates/system-fragments/commands.md +19 -0
  49. package/kits/templates/system-fragments/session.md +12 -0
  50. package/kits/templates/system-fragments/venue.md +15 -0
  51. package/package.json +3 -3
@@ -12,8 +12,33 @@ import os from 'os';
12
12
  import { parseTriggerSet, parseTriggerUpdate } from './trigger/parser.js';
13
13
  import { calcNextFireAt } from './trigger/scheduler.js';
14
14
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
15
+ import { tryParseChannelKey } from './channel-loader.js';
15
16
  const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
16
17
  const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
18
+ // ── CLI 透传(menu.action name=cli action=exec)─────────────────────────
19
+ // 经消息通道的远程命令执行(RCE):仅 owner、白名单内只读+配置命令、无 shell、超时+截断。
20
+ // command → '*'(全部子命令放行) | Set(允许的子命令)。
21
+ // 刻意排除破坏性/进程控制/数据面:restart stop start init dev mv rpc msg group net、
22
+ // agent set/new/delete/enable/disable/rename/reload、aid new/delete/agentmd、storage 写操作。
23
+ const CLI_EXEC_WHITELIST = {
24
+ status: '*',
25
+ model: '*',
26
+ agent: new Set(['list', 'show', 'get']),
27
+ aid: new Set(['list', 'show', 'lookup']),
28
+ storage: new Set(['ls', 'quota']),
29
+ };
30
+ const CLI_EXEC_TIMEOUT_MS = 15_000;
31
+ const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
32
+ /** 把命令行字符串分词为 argv,尊重单/双引号,不调用 shell。 */
33
+ function tokenizeArgv(line) {
34
+ const out = [];
35
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
36
+ let m;
37
+ while ((m = re.exec(line)) !== null) {
38
+ out.push(m[1] ?? m[2] ?? m[3] ?? '');
39
+ }
40
+ return out;
41
+ }
17
42
  function getAvailableEfforts(agent, model) {
18
43
  if (agent.name === 'claude') {
19
44
  return allEfforts;
@@ -204,33 +229,6 @@ export class CommandHandler {
204
229
  return owning.projectPath;
205
230
  return process.cwd();
206
231
  }
207
- /**
208
- * 返回当前通道有效的 projects.list(从 owning agent 的 config 取)。
209
- * 都没配 list 时回退到 defaultPath 单项目。
210
- */
211
- getEffectiveProjects(channel) {
212
- const owning = this.getOwningAgent(channel);
213
- if (owning) {
214
- return owning.getProjects();
215
- }
216
- return this.projects;
217
- }
218
- /**
219
- * 添加项目到当前通道范围(写到 owning agent 的 config.json)。
220
- */
221
- async addProjectInScope(channel, name, projectPath) {
222
- const owning = this.getOwningAgent(channel);
223
- if (!owning) {
224
- return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
225
- }
226
- try {
227
- owning.addProject(name, projectPath);
228
- }
229
- catch (e) {
230
- return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
231
- }
232
- return undefined;
233
- }
234
232
  /**
235
233
  * 持久化 baseagent.model:写到 agent config.json;找不到 owning agent 时
236
234
  * 退到用户级 ~/.claude/settings.json(Claude 专用)。
@@ -379,7 +377,7 @@ export class CommandHandler {
379
377
  return { matched: true, result: '✓ 已回答' };
380
378
  }
381
379
  /** 获取活跃会话,无会话时自动创建(话题除外) */
382
- async ensureSession(channel, channelId, threadId, chatType) {
380
+ async ensureSession(channel, channelId, threadId, chatType, selfAID) {
383
381
  if (threadId) {
384
382
  // 话题会话:仅查询,不创建
385
383
  const session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
@@ -390,8 +388,9 @@ export class CommandHandler {
390
388
  }
391
389
  const ct = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
392
390
  const channelType = this.resolveChannelType(channel);
391
+ const sid = selfAID ?? this.resolveSelfAID(channel);
393
392
  const session = await this.sessionManager.getActiveSession(channel, channelId)
394
- ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct, undefined, undefined, channelType);
393
+ ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct, undefined, sid, channelType);
395
394
  // 如果 session 已存在但 chatType 跟传入的不一致,更新
396
395
  if (ct && session.chatType !== ct) {
397
396
  await this.sessionManager.updateSession(session.id, { chatType: ct });
@@ -426,6 +425,14 @@ export class CommandHandler {
426
425
  resolveChannelType(channelName) {
427
426
  return this.channelTypeMap.get(channelName) || channelName;
428
427
  }
428
+ /**
429
+ * 从 channel key(<type>#<selfAID>#<name>)解析本地身份 AID。
430
+ * 非 evolagent 通道(裸 channelType,如 'feishu')解析失败返回 undefined。
431
+ * aun 通道创建 session 时必须提供 selfAID,故所有 getOrCreateSession 调用都经此兜底。
432
+ */
433
+ resolveSelfAID(channel) {
434
+ return tryParseChannelKey(channel)?.selfAID;
435
+ }
429
436
  registerPolicy(channelName, policy) {
430
437
  this.policies.set(channelName, policy);
431
438
  }
@@ -598,13 +605,6 @@ export class CommandHandler {
598
605
  }
599
606
  return items;
600
607
  }
601
- if (cmd === '/p') {
602
- // Use agent-scoped project list: agent-owned channels see their agent.json's
603
- // projects.list; default channel sees agent config's projects.list
604
- const list = this.getEffectiveProjects(channel);
605
- const currentPath = session?.projectPath;
606
- return Object.entries(list).map(([name, p]) => ({ value: name, label: name, desc: p, selected: currentPath === p }));
607
- }
608
608
  if (cmd === '/baseagent') {
609
609
  const currentAgent = session?.agentId;
610
610
  return this.getAvailableBaseagents(channel).map(name => ({ value: name, label: name, selected: name === currentAgent }));
@@ -1036,8 +1036,96 @@ export class CommandHandler {
1036
1036
  }
1037
1037
  return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
1038
1038
  }
1039
+ // ── /cli 透传 ──
1040
+ if (cmdBase === '/cli') {
1041
+ if (action !== 'exec')
1042
+ return { error: `不支持的 cli action: ${action}`, code: 'NOT_SUPPORTED' };
1043
+ if (identity.role !== 'owner')
1044
+ return { error: '无权限:CLI 执行仅限 owner', code: 'NO_PERMISSION' };
1045
+ const argv = Array.isArray(args?.argv) ? args.argv.map((x) => String(x))
1046
+ : typeof args?.command === 'string' ? tokenizeArgv(args.command)
1047
+ : null;
1048
+ if (!argv || argv.length === 0)
1049
+ return { error: '缺少 argv 或 command', code: 'MISSING_VALUE' };
1050
+ const allowed = CLI_EXEC_WHITELIST[argv[0]];
1051
+ if (!allowed)
1052
+ return { error: `命令不在白名单: ${argv[0]}`, code: 'NOT_ALLOWED' };
1053
+ if (allowed !== '*' && !allowed.has(argv[1] ?? '')) {
1054
+ return { error: `子命令不在白名单: ${argv[0]} ${argv[1] ?? ''}`, code: 'NOT_ALLOWED' };
1055
+ }
1056
+ return await this.execCliPassthrough(argv);
1057
+ }
1039
1058
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
1040
1059
  }
1060
+ /**
1061
+ * CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
1062
+ * 不 in-process 调用(CLI handler 用 console.log + process.exit,spawn 行为与终端一致且隔离)。
1063
+ * 调用方已完成 owner 校验与白名单过滤。
1064
+ */
1065
+ async execCliPassthrough(argv) {
1066
+ const { spawn } = await import('child_process');
1067
+ const cliEntry = path.join(getPackageRoot(), 'dist', 'cli', 'index.js');
1068
+ const startedAt = Date.now();
1069
+ return await new Promise((resolve) => {
1070
+ let stdout = '';
1071
+ let stderr = '';
1072
+ let total = 0;
1073
+ let truncated = false;
1074
+ let settled = false;
1075
+ const child = spawn('node', [cliEntry, ...argv], {
1076
+ env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root },
1077
+ windowsHide: true,
1078
+ });
1079
+ const append = (buf, sink) => {
1080
+ if (truncated)
1081
+ return;
1082
+ const remaining = CLI_EXEC_MAX_OUTPUT - total;
1083
+ if (remaining <= 0) {
1084
+ truncated = true;
1085
+ return;
1086
+ }
1087
+ const chunk = buf.length > remaining ? buf.subarray(0, remaining) : buf;
1088
+ total += chunk.length;
1089
+ if (sink === 'out')
1090
+ stdout += chunk.toString('utf-8');
1091
+ else
1092
+ stderr += chunk.toString('utf-8');
1093
+ if (buf.length > remaining)
1094
+ truncated = true;
1095
+ };
1096
+ child.stdout?.on('data', (b) => append(b, 'out'));
1097
+ child.stderr?.on('data', (b) => append(b, 'err'));
1098
+ const timer = setTimeout(() => {
1099
+ if (settled)
1100
+ return;
1101
+ settled = true;
1102
+ try {
1103
+ child.kill('SIGKILL');
1104
+ }
1105
+ catch { }
1106
+ logger.warn(`[CommandHandler] cli exec timeout: ${argv.join(' ')}`);
1107
+ resolve({ error: `执行超时(${CLI_EXEC_TIMEOUT_MS / 1000}s):${argv[0]}`, code: 'TIMEOUT' });
1108
+ }, CLI_EXEC_TIMEOUT_MS);
1109
+ child.on('error', (e) => {
1110
+ if (settled)
1111
+ return;
1112
+ settled = true;
1113
+ clearTimeout(timer);
1114
+ resolve({ error: e?.message || String(e), code: 'INTERNAL' });
1115
+ });
1116
+ child.on('close', (exitCode) => {
1117
+ if (settled)
1118
+ return;
1119
+ settled = true;
1120
+ clearTimeout(timer);
1121
+ resolve({ data: {
1122
+ exitCode: exitCode ?? -1,
1123
+ stdout, stderr, truncated,
1124
+ durationMs: Date.now() - startedAt,
1125
+ } });
1126
+ });
1127
+ });
1128
+ }
1041
1129
  /** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
1042
1130
  async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
1043
1131
  try {
@@ -1072,16 +1160,16 @@ export class CommandHandler {
1072
1160
  }
1073
1161
  }
1074
1162
  isCommand(content) {
1075
- return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
1163
+ return content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
1076
1164
  }
1077
1165
  /**
1078
1166
  * 主命令处理入口
1079
1167
  */
1080
- async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
1081
- const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source);
1168
+ async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID) {
1169
+ const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID);
1082
1170
  return result;
1083
1171
  }
1084
- async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
1172
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID) {
1085
1173
  // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
1086
1174
  // 不应覆盖 session 中已有的正确值
1087
1175
  if (source === 'card-trigger')
@@ -1421,6 +1509,8 @@ export class CommandHandler {
1421
1509
  const metadata = permSession.metadata || {};
1422
1510
  metadata.permissionMode = arg;
1423
1511
  await this.sessionManager.updateSession(permSession.id, { metadata });
1512
+ if (source === 'card-trigger')
1513
+ return null;
1424
1514
  return { kind: 'command.result', text: `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}` };
1425
1515
  }
1426
1516
  }
@@ -1600,6 +1690,8 @@ export class CommandHandler {
1600
1690
  const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
1601
1691
  const projectName = this.getProjectName(session.projectPath);
1602
1692
  let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
1693
+ if (source === 'card-trigger')
1694
+ return null;
1603
1695
  return { kind: 'command.result', text: agentSwitchResponse };
1604
1696
  }
1605
1697
  // /setmodel 命令:返回 JSON 格式的模型列表(供程序解析)
@@ -1613,7 +1705,7 @@ export class CommandHandler {
1613
1705
  const efforts = getAvailableEfforts(setmodelAgent, currentModel);
1614
1706
  const currentEffort = setmodelAgent.getEffort?.() || 'auto';
1615
1707
  const now = Math.floor(Date.now() / 1000);
1616
- const modelIds = hasModelSwitcher(setmodelAgent) ? setmodelAgent.listModels() : [];
1708
+ const modelIds = hasModelSwitcher(setmodelAgent) ? await setmodelAgent.listModels() : [];
1617
1709
  const modelListData = {
1618
1710
  object: 'list',
1619
1711
  data: modelIds.map(id => ({ id, object: 'model', created: now, owned_by: setmodelAgent.name === 'codex' ? 'openai' : 'anthropic' })),
@@ -1634,7 +1726,7 @@ export class CommandHandler {
1634
1726
  return { kind: 'command.result', text: modelResult.error };
1635
1727
  const { session: modelSession } = modelResult;
1636
1728
  const modelAgent = this.getAgent(channel, modelSession.agentId);
1637
- const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
1729
+ const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1638
1730
  if (!args) {
1639
1731
  const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
1640
1732
  const efforts = getAvailableEfforts(modelAgent, currentModel);
@@ -1758,6 +1850,8 @@ export class CommandHandler {
1758
1850
  if (err)
1759
1851
  return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1760
1852
  }
1853
+ if (source === 'card-trigger')
1854
+ return null;
1761
1855
  return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
1762
1856
  }
1763
1857
  // /effort 命令:查看或切换推理强度
@@ -1832,6 +1926,8 @@ export class CommandHandler {
1832
1926
  const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
1833
1927
  if (err)
1834
1928
  return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1929
+ if (source === 'card-trigger')
1930
+ return null;
1835
1931
  return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
1836
1932
  }
1837
1933
  // /agent, /aid, /rpc, /storage — 仅限 ctl 调用,slash 输入拒绝
@@ -1918,6 +2014,8 @@ export class CommandHandler {
1918
2014
  else {
1919
2015
  return { kind: 'command.error', text: `⚠️ 找不到通道 "${channel}" 所属的 self-agent,无法持久化` };
1920
2016
  }
2017
+ if (source === 'card-trigger')
2018
+ return null;
1921
2019
  return { kind: 'command.result', text: `✅ 中间输出模式: ${activityArg}(${label})` };
1922
2020
  }
1923
2021
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
@@ -1998,6 +2096,8 @@ export class CommandHandler {
1998
2096
  }
1999
2097
  await this.sessionManager.updateSession(chatmodeSession.id, { sessionMode: arg });
2000
2098
  this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: chatmodeSession.id, mode: arg, timestamp: Date.now() });
2099
+ if (source === 'card-trigger')
2100
+ return null;
2001
2101
  return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
2002
2102
  }
2003
2103
  // /dispatch 命令:查看/切换群聊分发模式(mention | broadcast)
@@ -2063,6 +2163,8 @@ export class CommandHandler {
2063
2163
  const metadata = { ...(dispatchSession.metadata || {}), dispatchMode: arg };
2064
2164
  await this.sessionManager.updateSession(dispatchSession.id, { metadata });
2065
2165
  this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: arg, timestamp: Date.now() });
2166
+ if (source === 'card-trigger')
2167
+ return null;
2066
2168
  return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
2067
2169
  }
2068
2170
  // /stop 命令:中断当前任务
@@ -2157,15 +2259,16 @@ export class CommandHandler {
2157
2259
  }
2158
2260
  // 尝试获取活跃会话(话题时直接查找话题 session)
2159
2261
  let session;
2262
+ const resolvedSelfAID = selfAID ?? this.resolveSelfAID(channel);
2160
2263
  if (threadId) {
2161
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, chatType, undefined, undefined, this.resolveChannelType(channel));
2264
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, chatType, undefined, resolvedSelfAID, this.resolveChannelType(channel));
2162
2265
  }
2163
2266
  else {
2164
2267
  session = await this.sessionManager.getActiveSession(channel, channelId);
2165
2268
  }
2166
2269
  // 如果没有会话,自动创建(所有后续命令都需要 session)
2167
2270
  if (!session) {
2168
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, chatType, undefined, undefined, this.resolveChannelType(channel));
2271
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, chatType, undefined, resolvedSelfAID, this.resolveChannelType(channel));
2169
2272
  }
2170
2273
  // /status 命令:显示会话状态
2171
2274
  if (normalizedContent === '/status') {
@@ -2372,7 +2475,7 @@ export class CommandHandler {
2372
2475
  const executeRestart = async () => {
2373
2476
  let replyContext;
2374
2477
  if (threadId) {
2375
- const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
2478
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, undefined, undefined, selfAID ?? this.resolveSelfAID(channel), this.resolveChannelType(channel));
2376
2479
  replyContext = this.getReplyContext(threadSession);
2377
2480
  }
2378
2481
  const restartInfo = {
@@ -2645,7 +2748,7 @@ export class CommandHandler {
2645
2748
  }
2646
2749
  // /slist — 仅显示 EvolClaw 会话
2647
2750
  const sessions = await this.sessionManager.listSessions(channel, channelId);
2648
- const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
2751
+ const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId?.startsWith('trigger-'));
2649
2752
  // 从 SDK 同步会话名称(发现 CLI 改名)
2650
2753
  try {
2651
2754
  const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
@@ -2823,6 +2926,8 @@ export class CommandHandler {
2823
2926
  if (!switched) {
2824
2927
  return { kind: 'command.error', text: `❌ 切换会话失败` };
2825
2928
  }
2929
+ if (source === 'card-trigger')
2930
+ return null;
2826
2931
  return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
2827
2932
  }
2828
2933
  if (targetSession.id === session.id) {
@@ -2838,6 +2943,8 @@ export class CommandHandler {
2838
2943
  }
2839
2944
  this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
2840
2945
  const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
2946
+ if (source === 'card-trigger')
2947
+ return null;
2841
2948
  return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}` };
2842
2949
  }
2843
2950
  // /rename 或 /name 命令:重命名当前会话
@@ -3024,12 +3131,12 @@ export class CommandHandler {
3024
3131
  }
3025
3132
  // /trigger 命令
3026
3133
  if (normalizedContent === '/trigger' || normalizedContent.startsWith('/trigger ')) {
3027
- const text = this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin);
3134
+ const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId);
3028
3135
  return { kind: 'command.result', text };
3029
3136
  }
3030
3137
  return null;
3031
3138
  }
3032
- handleTrigger(content, channel, channelId, peerId, isAdmin) {
3139
+ async handleTrigger(content, channel, channelId, peerId, isAdmin, messageId) {
3033
3140
  // Resolve trigger manager/scheduler from the owning agent of this channel
3034
3141
  const owningAgent = this.getOwningAgent(channel);
3035
3142
  const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
@@ -3172,6 +3279,32 @@ export class CommandHandler {
3172
3279
  updatedAt: now,
3173
3280
  };
3174
3281
  try {
3282
+ // Strategy-based session binding
3283
+ if (parsed.targetSessionStrategy === 'current') {
3284
+ const active = await this.sessionManager.getActiveSession(channel, channelId);
3285
+ if (!active)
3286
+ return '❌ 当前没有活跃会话,改用 --session latest 或 thread';
3287
+ trigger.boundSessionId = active.id;
3288
+ }
3289
+ else if (parsed.targetSessionStrategy === 'thread') {
3290
+ const targetAdapterName = parsed.targetChannel ?? channel;
3291
+ const adapter = this.adapters.get(targetAdapterName);
3292
+ if (!adapter?.capabilities.thread)
3293
+ return '❌ 目标渠道不支持 thread 会话';
3294
+ const channelType = adapter.channelKey.split('#')[0];
3295
+ trigger.targetChannelType = channelType;
3296
+ if (channelType === 'aun') {
3297
+ trigger.threadKind = 'aun';
3298
+ trigger.targetThreadId = `trigger-${trigger.id}`;
3299
+ }
3300
+ else {
3301
+ if (!messageId)
3302
+ return '❌ 飞书 thread 模式需要消息 ID,请重新发送命令';
3303
+ trigger.threadKind = 'feishu';
3304
+ trigger.rootMessageId = messageId;
3305
+ trigger.pendingThread = true;
3306
+ }
3307
+ }
3175
3308
  // Validate name uniqueness before persisting (manager.register writes to disk)
3176
3309
  // scheduler.register is in-memory only and cannot fail, so order is safe here.
3177
3310
  // If manager.register throws (duplicate name/ID), nothing is persisted.
@@ -213,24 +213,6 @@ export class EvolAgent {
213
213
  this.merged.dispatch = value;
214
214
  this.persist();
215
215
  }
216
- // ── Projects ──────────────────────────────────────────────────────────
217
- getProjects() {
218
- const list = this.merged.projects?.list;
219
- if (list && Object.keys(list).length > 0)
220
- return { ...list };
221
- const dp = this.merged.projects?.defaultPath;
222
- if (dp)
223
- return { [path.basename(dp)]: dp };
224
- return {};
225
- }
226
- addProject(name, projectPath) {
227
- if (!this.rawAgent.projects)
228
- this.rawAgent.projects = { defaultPath: projectPath, list: {} };
229
- if (!this.rawAgent.projects.list)
230
- this.rawAgent.projects.list = {};
231
- this.rawAgent.projects.list[name] = projectPath;
232
- this.persist();
233
- }
234
216
  // ── Personal layer ────────────────────────────────────────────────────
235
217
  _personaCache = undefined;
236
218
  /**
@@ -132,26 +132,6 @@ export class IMRenderer {
132
132
  getRemainingText() {
133
133
  return this.textBuffer;
134
134
  }
135
- /** 从 buffer 中移除指定 pattern(用于文件标记预处理) */
136
- stripFromBuffer(pattern) {
137
- this.textBuffer = this.textBuffer.replace(pattern, '').trim();
138
- // itemsQueue 中的 text items 也同步过滤
139
- for (const item of this.itemsQueue) {
140
- if (item.kind === 'text') {
141
- item.text = item.text.replace(pattern, '');
142
- }
143
- }
144
- }
145
- /** 清除上下文过长错误文本(从 buffer + allText 中移除) */
146
- stripContextError(pattern) {
147
- this.textBuffer = this.textBuffer.replace(pattern, '').trim();
148
- this.allText = this.allText.replace(pattern, '').trim();
149
- for (const item of this.itemsQueue) {
150
- if (item.kind === 'text') {
151
- item.text = item.text.replace(pattern, '');
152
- }
153
- }
154
- }
155
135
  // ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
156
136
  /** 添加文本片段(流式 text) */
157
137
  addText(text, outputTokens, turn) {
@@ -328,6 +308,15 @@ export class IMRenderer {
328
308
  clearTimeout(this.timer);
329
309
  this.timer = undefined;
330
310
  }
311
+ // 上下文错误短语过滤:剔除错误关键词本身,保留前后内容
312
+ const ctxErrPattern = /prompt is too long|input is too long|context too long|context limit|context_length_exceeded|上下文过长/gi;
313
+ const stripCtxErr = (s) => s.replace(ctxErrPattern, '').trim();
314
+ this.textBuffer = stripCtxErr(this.textBuffer);
315
+ this.allText = stripCtxErr(this.allText);
316
+ for (const item of this.itemsQueue) {
317
+ if (item.kind === 'text')
318
+ item.text = stripCtxErr(item.text);
319
+ }
331
320
  // 文件标记过滤
332
321
  if (this.opts.fileMarkerPattern) {
333
322
  this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
@@ -105,7 +105,7 @@ export class MessageBridge {
105
105
  if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
106
106
  logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
107
107
  return sendReply(msg.channelId, text, msg.replyContext);
108
- }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext))
108
+ }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID))
109
109
  return;
110
110
  // 3. session 解析(使用 Channel 层填充的 chatType)
111
111
  const chatType = msg.chatType || 'private';
@@ -135,13 +135,8 @@ export class MessageBridge {
135
135
  const effectiveProjectPath = owningAgent?.projectPath
136
136
  ?? this.defaultProjectPath;
137
137
  const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType);
138
- // 4. 消息前缀(由 policy 决定)
139
- const channelInfo = this.processor.getChannelInfo?.(channelName);
140
- if (channelInfo?.policy) {
141
- const prefix = channelInfo.policy.messagePrefix(chatType, msg.peerName);
142
- if (prefix)
143
- content = prefix + content;
144
- }
138
+ // 4. 群聊发送者标注由消息渲染层(message-renderer)逐条承担,不再在此硬编码前缀,
139
+ // 消息日志因此保存干净原文。policy.messagePrefix 暂保留(未来清理)。
145
140
  // 5. 构造完整消息(channel 字段存实例名,用于 session 精确匹配)
146
141
  const fullMessage = {
147
142
  channel: channelName,
@@ -152,6 +147,9 @@ export class MessageBridge {
152
147
  images: msg.images, timestamp: Date.now(),
153
148
  peerId: msg.peerId, peerName: msg.peerName,
154
149
  peerType: msg.peerType,
150
+ sameDevice: msg.sameDevice,
151
+ sameNetwork: msg.sameNetwork,
152
+ sameEgressIp: msg.sameEgressIp,
155
153
  messageId: msg.messageId,
156
154
  mentions: msg.mentions, threadId: msg.threadId,
157
155
  replyContext: msg.replyContext,
@@ -219,6 +217,7 @@ export class MessageBridge {
219
217
  permission: '/perm',
220
218
  activity: '/activity',
221
219
  system: '/system',
220
+ cli: '/cli',
222
221
  };
223
222
  resolveCmd(name, cmd) {
224
223
  if (cmd)
@@ -374,11 +373,11 @@ export class MessageBridge {
374
373
  }
375
374
  }
376
375
  /** 命令快速路径:返回 true 表示已处理 */
377
- async handleCommand(content, channel, channelId, sendReply, userId, threadId, chatType, source, replyContext) {
376
+ async handleCommand(content, channel, channelId, sendReply, userId, threadId, chatType, source, replyContext, messageId, selfAID) {
378
377
  if (!this.cmdHandler.isCommand(content))
379
378
  return false;
380
379
  logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
381
- const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source);
380
+ const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source, messageId, selfAID);
382
381
  logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
383
382
  if (cmdResult === undefined)
384
383
  return false;