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