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.
- package/CHANGELOG.md +68 -3
- package/dist/agents/claude-runner.js +69 -24
- package/dist/agents/kit-renderer.js +78 -321
- package/dist/agents/manifest-engine.js +243 -0
- package/dist/agents/message-renderer.js +112 -0
- package/dist/aun/aid/agentmd.js +10 -3
- package/dist/aun/msg/group.js +2 -2
- package/dist/channels/aun.js +154 -18
- package/dist/channels/dingtalk.js +1 -1
- package/dist/channels/feishu.js +31 -9
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/channels/wecom.js +1 -1
- package/dist/cli/agent.js +10 -11
- package/dist/cli/bench.js +1 -5
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +91 -128
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +231 -6
- package/dist/config-store.js +1 -22
- package/dist/core/command-handler.js +181 -48
- package/dist/core/evolagent.js +0 -18
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +9 -10
- package/dist/core/message/message-processor.js +188 -39
- package/dist/core/message/message-queue.js +15 -1
- package/dist/core/relation/peer-identity.js +23 -11
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +43 -13
- package/dist/index.js +102 -52
- package/dist/ipc.js +1 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/INDEX.md +4 -8
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/group.md +13 -6
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +5 -0
- package/kits/docs/venues/group.md +13 -1
- package/kits/eck_manifest.json +9 -0
- package/kits/eck_message_manifest.json +14 -0
- package/kits/rules/06-channel.md +5 -1
- package/kits/templates/message-fragments/item.md +2 -0
- 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 +12 -0
- package/kits/templates/system-fragments/venue.md +15 -0
- 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,
|
|
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 === '/
|
|
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,
|
|
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,
|
|
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.
|
package/dist/core/evolagent.js
CHANGED
|
@@ -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.
|
|
139
|
-
|
|
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;
|