evolclaw 3.1.1 → 3.1.3
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 +428 -0
- package/README.md +3 -7
- package/SKILLS.md +311 -0
- package/dist/agents/claude-runner.js +1 -1
- package/dist/agents/codex-runner.js +75 -19
- package/dist/agents/gemini-runner.js +0 -2
- package/dist/agents/kit-renderer.js +59 -10
- package/dist/aun/aid/agentmd.js +50 -27
- package/dist/aun/aid/client.js +5 -11
- package/dist/aun/aid/identity.js +32 -13
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +1 -0
- package/dist/aun/msg/p2p.js +15 -2
- package/dist/aun/msg/upload.js +57 -18
- package/dist/aun/rpc/connection.js +3 -0
- package/dist/channels/aun.js +122 -48
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +5 -4
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +142 -40
- package/dist/cli/index.js +103 -58
- package/dist/cli/init-channel.js +4 -2
- package/dist/cli/init.js +55 -26
- package/dist/cli/watch-msg.js +3 -1
- package/dist/config-store.js +22 -1
- package/dist/core/channel-loader.js +4 -4
- package/dist/core/command-handler.js +626 -538
- package/dist/core/evolagent-registry.js +45 -9
- package/dist/core/evolagent.js +35 -4
- package/dist/core/message/im-renderer.js +14 -4
- package/dist/core/message/message-bridge.js +149 -25
- package/dist/core/message/message-processor.js +45 -38
- package/dist/core/session/session-fs-store.js +23 -0
- package/dist/core/session/session-manager.js +188 -42
- package/dist/index.js +15 -17
- package/dist/paths.js +35 -0
- package/dist/utils/cross-platform.js +2 -1
- package/kits/docs/INDEX.md +6 -0
- package/kits/eck_manifest.json +3 -3
- package/kits/rules/02-navigation.md +1 -0
- package/kits/rules/06-channel.md +2 -18
- package/kits/templates/system-fragments/baseagent.md +2 -2
- package/kits/templates/system-fragments/channel.md +18 -9
- package/kits/templates/system-fragments/eckruntime.md +14 -0
- package/kits/templates/system-fragments/identity.md +5 -6
- package/kits/templates/system-fragments/relation.md +7 -5
- package/kits/templates/system-fragments/venue.md +2 -3
- package/package.json +5 -2
- package/kits/templates/system-fragments/runtime.md +0 -19
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
2
2
|
import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
|
|
3
|
+
import { getCodexEfforts } from '../agents/codex-runner.js';
|
|
4
|
+
import { resolveAnthropicConfig, resolveOpenaiConfig } from '../agents/resolve.js';
|
|
3
5
|
import { renderCommandCardAsText } from './interaction-router.js';
|
|
4
6
|
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
5
7
|
import { resolvePaths, getPackageRoot } from '../paths.js';
|
|
@@ -11,22 +13,48 @@ import os from 'os';
|
|
|
11
13
|
import { parseTriggerSet, parseTriggerUpdate } from './trigger/parser.js';
|
|
12
14
|
import { calcNextFireAt } from './trigger/scheduler.js';
|
|
13
15
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
|
|
14
|
-
const allEfforts = ['low', 'medium', 'high', 'max'];
|
|
15
|
-
const nonMaxEfforts = allEfforts.filter(e => e !== 'max');
|
|
16
|
+
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
17
|
+
const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
|
|
16
18
|
function getAvailableEfforts(agent, model) {
|
|
17
19
|
if (agent.name === 'claude') {
|
|
18
|
-
|
|
19
|
-
return allEfforts;
|
|
20
|
-
return nonMaxEfforts;
|
|
20
|
+
return allEfforts;
|
|
21
21
|
}
|
|
22
22
|
if (agent.name === 'codex') {
|
|
23
|
-
return
|
|
23
|
+
return getCodexEfforts(model);
|
|
24
24
|
}
|
|
25
25
|
return [];
|
|
26
26
|
}
|
|
27
27
|
function formatModelUsage(_agent, _model) {
|
|
28
28
|
return '用法: /model <模型>';
|
|
29
29
|
}
|
|
30
|
+
function getModelListSource(owning, agent) {
|
|
31
|
+
const codexConfig = owning?.config?.baseagents?.codex;
|
|
32
|
+
const claudeConfig = owning?.config?.baseagents?.claude;
|
|
33
|
+
if (agent.name === 'codex') {
|
|
34
|
+
let resolved = {};
|
|
35
|
+
try {
|
|
36
|
+
resolved = resolveOpenaiConfig({ agents: { codex: codexConfig } }, codexConfig);
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
return {
|
|
40
|
+
apiBaseUrl: resolved.baseUrl,
|
|
41
|
+
apiKey: resolved.apiKey,
|
|
42
|
+
fallbackModels: agent.listModels?.() || [],
|
|
43
|
+
owner: 'openai',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let resolved = {};
|
|
47
|
+
try {
|
|
48
|
+
resolved = resolveAnthropicConfig({ agents: { claude: claudeConfig } }, claudeConfig);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
return {
|
|
52
|
+
apiBaseUrl: resolved.baseUrl,
|
|
53
|
+
apiKey: resolved.apiKey,
|
|
54
|
+
fallbackModels: ['claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6'],
|
|
55
|
+
owner: 'anthropic',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
30
58
|
/**
|
|
31
59
|
* 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
|
|
32
60
|
*/
|
|
@@ -100,16 +128,16 @@ function formatIdleTime(ms) {
|
|
|
100
128
|
return '刚刚';
|
|
101
129
|
}
|
|
102
130
|
// 支持的命令列表
|
|
103
|
-
const commands = ['/new', '/pwd', '/
|
|
131
|
+
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
104
132
|
// 命令别名映射
|
|
105
133
|
const aliases = {
|
|
106
|
-
'/p': '/project',
|
|
107
134
|
'/s': '/session',
|
|
108
135
|
'/name': '/rename',
|
|
109
|
-
'/rw': '/rewind'
|
|
136
|
+
'/rw': '/rewind',
|
|
137
|
+
'/base': '/baseagent',
|
|
110
138
|
};
|
|
111
139
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
112
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/
|
|
140
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
113
141
|
export class CommandHandler {
|
|
114
142
|
sessionManager;
|
|
115
143
|
messageCache;
|
|
@@ -465,16 +493,6 @@ export class CommandHandler {
|
|
|
465
493
|
}
|
|
466
494
|
];
|
|
467
495
|
}
|
|
468
|
-
if (isAdmin) {
|
|
469
|
-
items.push({
|
|
470
|
-
group: '项目管理',
|
|
471
|
-
commands: [
|
|
472
|
-
{ cmd: '/pwd', label: '显示当前项目路径', desc: '查看当前会话绑定的项目目录' },
|
|
473
|
-
{ cmd: '/p', label: '列出或切换项目', desc: '切换到其他已配置的项目', next: { type: 'select', dynamic: true } },
|
|
474
|
-
...(isOwner ? [{ cmd: '/bind', label: '绑定新项目目录', desc: '将当前会话绑定到指定项目路径', next: { type: 'text' } }] : []),
|
|
475
|
-
]
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
496
|
items.push({
|
|
479
497
|
group: '会话管理',
|
|
480
498
|
commands: [
|
|
@@ -493,7 +511,7 @@ export class CommandHandler {
|
|
|
493
511
|
items.push({
|
|
494
512
|
group: 'Agent 与模型',
|
|
495
513
|
commands: [
|
|
496
|
-
{ cmd: '/
|
|
514
|
+
{ cmd: '/baseagent', label: '切换 Agent 后端', desc: '切换当前会话使用的 AI 后端', next: { type: 'select', dynamic: true } },
|
|
497
515
|
{ cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } },
|
|
498
516
|
{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
|
|
499
517
|
{ value: 'low', label: 'Low' },
|
|
@@ -542,7 +560,7 @@ export class CommandHandler {
|
|
|
542
560
|
{ value: 'none', label: '不显示', desc: '关闭所有中间输出' },
|
|
543
561
|
] } },
|
|
544
562
|
...(isAdmin ? [
|
|
545
|
-
{ cmd: '/restart', label: '
|
|
563
|
+
{ cmd: '/restart', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' },
|
|
546
564
|
] : []),
|
|
547
565
|
...(isOwner ? [
|
|
548
566
|
{ cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
|
|
@@ -570,22 +588,32 @@ export class CommandHandler {
|
|
|
570
588
|
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
571
589
|
async getSubMenuItems(cmd, channel, channelId, userId) {
|
|
572
590
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
573
|
-
if (cmd === '/s' || cmd === '/del') {
|
|
591
|
+
if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
|
|
574
592
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
575
593
|
const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
|
|
594
|
+
const currentSession = session;
|
|
576
595
|
const items = sessions
|
|
577
596
|
.filter(s => !active || s.id !== active.id)
|
|
578
597
|
.map(s => {
|
|
579
|
-
const
|
|
580
|
-
const time = s.updatedAt ? formatIdleTime(Date.now() - s.updatedAt) : '';
|
|
581
|
-
const parts = [shortId, time].filter(Boolean).join(' · ');
|
|
582
|
-
return {
|
|
598
|
+
const item = {
|
|
583
599
|
value: s.name || s.id.slice(0, 8),
|
|
584
600
|
label: s.name || s.id.slice(0, 8),
|
|
585
|
-
|
|
601
|
+
selected: currentSession ? s.id === currentSession.id : false,
|
|
586
602
|
};
|
|
603
|
+
if (s.agentSessionId) {
|
|
604
|
+
item.agentSessionId = s.agentSessionId;
|
|
605
|
+
const fileInfo = this.sessionManager.getSessionFileInfo(s.projectPath, s.agentSessionId, s.agentId);
|
|
606
|
+
if (fileInfo.turns)
|
|
607
|
+
item.turns = fileInfo.turns;
|
|
608
|
+
const firstMsg = this.sessionManager.readSessionFirstMessage(s.projectPath, s.agentSessionId, s.agentId);
|
|
609
|
+
if (firstMsg)
|
|
610
|
+
item.preview = firstMsg.length > 80 ? firstMsg.slice(0, 80) + '…' : firstMsg;
|
|
611
|
+
}
|
|
612
|
+
if (s.updatedAt)
|
|
613
|
+
item.lastActive = s.updatedAt;
|
|
614
|
+
return item;
|
|
587
615
|
});
|
|
588
|
-
if (cmd === '/s') {
|
|
616
|
+
if (cmd === '/s' || cmd === '/session') {
|
|
589
617
|
items.push({ value: 'cli', label: '查看 CLI 会话', desc: '列出未导入的 CLI 本地会话' });
|
|
590
618
|
}
|
|
591
619
|
return items;
|
|
@@ -594,107 +622,474 @@ export class CommandHandler {
|
|
|
594
622
|
// Use agent-scoped project list: agent-owned channels see their agent.json's
|
|
595
623
|
// projects.list; default channel sees agent config's projects.list
|
|
596
624
|
const list = this.getEffectiveProjects(channel);
|
|
597
|
-
|
|
625
|
+
const currentPath = session?.projectPath;
|
|
626
|
+
return Object.entries(list).map(([name, p]) => ({ value: name, label: name, desc: p, selected: currentPath === p }));
|
|
598
627
|
}
|
|
599
|
-
if (cmd === '/
|
|
600
|
-
|
|
628
|
+
if (cmd === '/baseagent') {
|
|
629
|
+
const currentAgent = session?.agentId;
|
|
630
|
+
return this.getAvailableBaseagents(channel).map(name => ({ value: name, label: name, selected: name === currentAgent }));
|
|
601
631
|
}
|
|
602
632
|
if (cmd === '/model') {
|
|
603
633
|
const agent = this.getAgent(channel, session?.agentId);
|
|
604
634
|
if (hasModelSwitcher(agent) && agent.listModels) {
|
|
605
635
|
const models = await agent.listModels() ?? [];
|
|
636
|
+
const currentModel = agent.getModel();
|
|
606
637
|
if (models.length > 0)
|
|
607
|
-
return models.map((m) => ({ value: m, label: m }));
|
|
638
|
+
return models.map((m) => ({ value: m, label: m, selected: m === currentModel }));
|
|
608
639
|
}
|
|
609
640
|
return null;
|
|
610
641
|
}
|
|
611
|
-
if (cmd === '/restart') {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
642
|
+
// if (cmd === '/restart') {
|
|
643
|
+
// const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
|
|
644
|
+
// // 列出所有 channel type
|
|
645
|
+
// const visibleTypes = new Set<string>();
|
|
646
|
+
// for (const [name] of this.adapters) {
|
|
647
|
+
// const t = this.channelTypeMap.get(name);
|
|
648
|
+
// if (t) visibleTypes.add(t);
|
|
649
|
+
// }
|
|
650
|
+
// const channels = [...visibleTypes].map(type => ({ value: type, label: type, desc: '重连此类型所有渠道实例' }));
|
|
651
|
+
// if (isOwner) channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' });
|
|
652
|
+
// return channels;
|
|
653
|
+
// }
|
|
654
|
+
if (cmd === '/activity') {
|
|
655
|
+
const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
|
|
656
|
+
return [
|
|
657
|
+
{ value: 'all', label: '全部显示', selected: currentMode === 'all' },
|
|
658
|
+
{ value: 'dm', label: '仅私聊显示', selected: currentMode === 'dm-only' },
|
|
659
|
+
{ value: 'owner', label: '仅 owner 私聊显示', selected: currentMode === 'owner-dm-only' },
|
|
660
|
+
{ value: 'none', label: '全部静默', selected: currentMode === 'none' },
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
if (cmd === '/effort') {
|
|
664
|
+
const agent = this.getAgent(channel, session?.agentId);
|
|
665
|
+
const currentModel = hasModelSwitcher(agent) ? agent.getModel() : agent.name;
|
|
666
|
+
const efforts = getAvailableEfforts(agent, currentModel);
|
|
667
|
+
const currentEffort = agent.getEffort?.() || 'auto';
|
|
668
|
+
const allItems = [...efforts, 'auto'];
|
|
669
|
+
return allItems.map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e, selected: e === currentEffort }));
|
|
670
|
+
}
|
|
671
|
+
if (cmd === '/chatmode') {
|
|
672
|
+
// 无活跃会话时,selected 跟随 evolagent.config.chatmode.private 默认值
|
|
673
|
+
let currentMode;
|
|
674
|
+
if (session?.sessionMode) {
|
|
675
|
+
currentMode = session.sessionMode;
|
|
619
676
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
677
|
+
else {
|
|
678
|
+
const evolagent = this.agentRegistry?.resolveByChannel(channel);
|
|
679
|
+
currentMode = evolagent?.config?.chatmode?.private || 'interactive';
|
|
680
|
+
}
|
|
681
|
+
return [
|
|
682
|
+
{ value: 'interactive', label: '交互模式', selected: currentMode === 'interactive' },
|
|
683
|
+
{ value: 'proactive', label: '主动模式', selected: currentMode === 'proactive' },
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
if (cmd === '/dispatch') {
|
|
687
|
+
const currentMode = session?.metadata?.dispatchMode ?? null;
|
|
688
|
+
return [
|
|
689
|
+
{ value: 'mention', label: '@提及时响应', selected: currentMode === 'mention' },
|
|
690
|
+
{ value: 'broadcast', label: '所有消息响应', selected: currentMode === 'broadcast' },
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
if (cmd === '/perm') {
|
|
694
|
+
const currentMode = session?.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
|
|
695
|
+
const permAgent = this.getAgent(channel, session?.agentId);
|
|
696
|
+
const validModes = hasPermissionController(permAgent)
|
|
697
|
+
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
698
|
+
: ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
|
|
699
|
+
return validModes.map(m => ({ value: m, label: m, selected: m === currentMode }));
|
|
624
700
|
}
|
|
625
701
|
return null;
|
|
626
702
|
}
|
|
627
|
-
|
|
628
|
-
|
|
703
|
+
// ── Menu Protocol exec ────────────────────────────────────────────────
|
|
704
|
+
//
|
|
705
|
+
// 三个入口对应 menu.query / menu.update / menu.action:
|
|
706
|
+
// execMenuQuery — 查询某项当前值(无会话时多数 fallback 到 evolagent config)
|
|
707
|
+
// execMenuUpdate — 写入新值(持久化到 session 或 evolagent config)
|
|
708
|
+
// execMenuAction — 触发动词(stop/restart/new/delete/compact/fork/switch/check/upgrade)
|
|
709
|
+
//
|
|
710
|
+
// 所有方法返回 { data } 或 { error, code? }。code 是结构化错误码(NO_ACTIVE_SESSION 等),
|
|
711
|
+
// 客户端可据此决定降级策略。message-bridge 把 code 透传到 menu.response。
|
|
712
|
+
async loadMenuContext(channel, channelId) {
|
|
629
713
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
714
|
+
const evolagent = this.agentRegistry?.resolveByChannel(channel) ?? null;
|
|
715
|
+
return { session, evolagent };
|
|
716
|
+
}
|
|
717
|
+
requireSession(s) {
|
|
718
|
+
return s ? null : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
|
|
719
|
+
}
|
|
720
|
+
/** menu.query — 查询当前值。 */
|
|
721
|
+
async execMenuQuery(cmd, channel, channelId, userId) {
|
|
722
|
+
void userId;
|
|
723
|
+
const cmdBase = cmd.trim().split(' ')[0];
|
|
634
724
|
if (!cmdBase)
|
|
635
|
-
return { error: '缺少命令' };
|
|
636
|
-
const
|
|
725
|
+
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
726
|
+
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
727
|
+
if (cmdBase === '/pwd') {
|
|
728
|
+
const sessPath = session?.projectPath;
|
|
729
|
+
const fallbackPath = evolagent?.config?.projects?.defaultPath;
|
|
730
|
+
const path = sessPath ?? fallbackPath ?? null;
|
|
731
|
+
const name = path ? this.getProjectName(path) : null;
|
|
732
|
+
return { data: { name, path } };
|
|
733
|
+
}
|
|
734
|
+
if (cmdBase === '/session' || cmdBase === '/s') {
|
|
735
|
+
if (!session) {
|
|
736
|
+
return { data: { status: 'no-session' } };
|
|
737
|
+
}
|
|
738
|
+
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
739
|
+
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
740
|
+
const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
|
|
741
|
+
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
742
|
+
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
743
|
+
let processingDuration;
|
|
744
|
+
if (isProcessing && session.processingState) {
|
|
745
|
+
const elapsed = Date.now() - parseInt(session.processingState, 10);
|
|
746
|
+
if (!isNaN(elapsed) && elapsed > 0)
|
|
747
|
+
processingDuration = Math.floor(elapsed / 1000);
|
|
748
|
+
}
|
|
749
|
+
let turns = 0;
|
|
750
|
+
if (session.agentSessionId) {
|
|
751
|
+
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId, session.agentId);
|
|
752
|
+
turns = fileInfo.turns;
|
|
753
|
+
}
|
|
754
|
+
const data = {
|
|
755
|
+
name: session.name || null,
|
|
756
|
+
agentSessionId: session.agentSessionId || null,
|
|
757
|
+
status: isProcessing ? 'processing' : 'idle',
|
|
758
|
+
createdAt: session.createdAt,
|
|
759
|
+
updatedAt: session.updatedAt,
|
|
760
|
+
};
|
|
761
|
+
if (processingDuration !== undefined)
|
|
762
|
+
data.processingDuration = processingDuration;
|
|
763
|
+
if (queueLength > 0)
|
|
764
|
+
data.queueLength = queueLength;
|
|
765
|
+
if (turns > 0)
|
|
766
|
+
data.turns = turns;
|
|
767
|
+
if (health.lastSuccessTime)
|
|
768
|
+
data.lastSuccess = health.lastSuccessTime;
|
|
769
|
+
if (health.consecutiveErrors)
|
|
770
|
+
data.consecutiveErrors = health.consecutiveErrors;
|
|
771
|
+
if (health.lastError)
|
|
772
|
+
data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
|
|
773
|
+
return { data };
|
|
774
|
+
}
|
|
775
|
+
if (cmdBase === '/baseagent') {
|
|
776
|
+
const value = session?.agentId ?? evolagent?.config?.active_baseagent ?? null;
|
|
777
|
+
return { data: { baseagent: value } };
|
|
778
|
+
}
|
|
779
|
+
if (cmdBase === '/model') {
|
|
780
|
+
if (session) {
|
|
781
|
+
const agent = this.getAgent(channel, session.agentId);
|
|
782
|
+
if (hasModelSwitcher(agent))
|
|
783
|
+
return { data: { model: agent.getModel() ?? null } };
|
|
784
|
+
}
|
|
785
|
+
const ba = evolagent?.config?.active_baseagent;
|
|
786
|
+
const block = ba && evolagent ? evolagent.config.baseagents?.[ba] : undefined;
|
|
787
|
+
return { data: { model: block?.model ?? null } };
|
|
788
|
+
}
|
|
789
|
+
if (cmdBase === '/effort') {
|
|
790
|
+
if (session) {
|
|
791
|
+
const agent = this.getAgent(channel, session.agentId);
|
|
792
|
+
const e = agent.getEffort?.();
|
|
793
|
+
if (e !== undefined)
|
|
794
|
+
return { data: { effort: e } };
|
|
795
|
+
}
|
|
796
|
+
const ba = evolagent?.config?.active_baseagent;
|
|
797
|
+
const block = ba && evolagent ? evolagent.config.baseagents?.[ba] : undefined;
|
|
798
|
+
const fallbackField = ba === 'codex' ? (block?.effort ?? block?.reasoning) : block?.effort;
|
|
799
|
+
return { data: { effort: fallbackField ?? null } };
|
|
800
|
+
}
|
|
801
|
+
if (cmdBase === '/chatmode') {
|
|
802
|
+
const sessionMode = session?.sessionMode;
|
|
803
|
+
const fallback = evolagent?.config?.chatmode?.private;
|
|
804
|
+
return { data: { mode: sessionMode || fallback || 'interactive' } };
|
|
805
|
+
}
|
|
806
|
+
if (cmdBase === '/dispatch') {
|
|
807
|
+
const chatType = session?.chatType || 'private';
|
|
808
|
+
if (chatType !== 'group') {
|
|
809
|
+
return { error: 'dispatch 仅在群聊会话中有效', code: 'NOT_APPLICABLE' };
|
|
810
|
+
}
|
|
811
|
+
const sessionMode = session?.metadata?.dispatchMode;
|
|
812
|
+
const fallback = evolagent?.config?.dispatch;
|
|
813
|
+
return { data: { mode: sessionMode ?? fallback ?? null } };
|
|
814
|
+
}
|
|
637
815
|
if (cmdBase === '/perm') {
|
|
816
|
+
const need = this.requireSession(session);
|
|
817
|
+
if (need)
|
|
818
|
+
return need;
|
|
638
819
|
const currentMode = session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
|
|
639
|
-
|
|
640
|
-
|
|
820
|
+
return { data: { mode: currentMode } };
|
|
821
|
+
}
|
|
822
|
+
if (cmdBase === '/activity') {
|
|
823
|
+
const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
|
|
824
|
+
return { data: { mode: currentMode } };
|
|
825
|
+
}
|
|
826
|
+
if (cmdBase === '/system') {
|
|
827
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
828
|
+
const data = {
|
|
829
|
+
agent: owningAgent?.name ?? 'DefaultAgent',
|
|
830
|
+
channel: this.resolveChannelType(channel),
|
|
831
|
+
pid: process.pid,
|
|
832
|
+
node: process.version,
|
|
833
|
+
uptime: Math.floor(process.uptime()),
|
|
834
|
+
};
|
|
835
|
+
try {
|
|
836
|
+
const pkgPath = path.join(getPackageRoot(), 'package.json');
|
|
837
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
838
|
+
if (pkg?.version)
|
|
839
|
+
data.version = pkg.version;
|
|
840
|
+
}
|
|
841
|
+
catch { }
|
|
842
|
+
const channels = owningAgent?.channelInstanceNames?.() ?? [];
|
|
843
|
+
if (channels.length)
|
|
844
|
+
data.channels = channels;
|
|
845
|
+
return { data };
|
|
846
|
+
}
|
|
847
|
+
return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
848
|
+
}
|
|
849
|
+
/** menu.update — 写入新值。 */
|
|
850
|
+
async execMenuUpdate(cmd, value, channel, channelId, userId) {
|
|
851
|
+
const cmdBase = cmd.trim().split(' ')[0];
|
|
852
|
+
if (!cmdBase)
|
|
853
|
+
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
854
|
+
const arg = value.trim();
|
|
855
|
+
if (!arg)
|
|
856
|
+
return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
|
|
857
|
+
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
858
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
859
|
+
if (cmdBase === '/baseagent') {
|
|
860
|
+
const valid = this.getAvailableBaseagents(channel);
|
|
861
|
+
if (valid.length && !valid.includes(arg)) {
|
|
862
|
+
return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
|
|
863
|
+
}
|
|
864
|
+
// 当前会话切换走 slash 命令的完整逻辑(涉及 runner 状态、session.agentId 重新挂载等)
|
|
865
|
+
// 仅在 slash 命令成功后才持久化到 evolagent config,避免失败时配置已落盘
|
|
866
|
+
if (session && session.agentId !== arg) {
|
|
867
|
+
const result = await this._handleInternal(`/baseagent ${arg}`, channel, channelId, undefined, userId);
|
|
868
|
+
const payload = result;
|
|
869
|
+
if (payload?.kind === 'command.error') {
|
|
870
|
+
return { error: payload.text || '切换失败', code: 'EXEC_FAILED' };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// 持久化到 evolagent config(影响后续新会话)
|
|
874
|
+
if (evolagent)
|
|
875
|
+
evolagent.setActiveBaseagent(arg);
|
|
876
|
+
return { data: { baseagent: arg } };
|
|
877
|
+
}
|
|
878
|
+
if (cmdBase === '/model') {
|
|
879
|
+
const agent = this.getAgent(channel, session?.agentId);
|
|
880
|
+
if (hasModelSwitcher(agent)) {
|
|
881
|
+
const models = (await agent.listModels?.()) ?? [];
|
|
882
|
+
if (models.length && !models.includes(arg)) {
|
|
883
|
+
return { error: `无效模型: ${arg}`, code: 'INVALID_VALUE' };
|
|
884
|
+
}
|
|
885
|
+
agent.setModel(arg);
|
|
886
|
+
}
|
|
887
|
+
if (evolagent)
|
|
888
|
+
evolagent.setBaseagentModel(arg);
|
|
889
|
+
return { data: { model: arg } };
|
|
890
|
+
}
|
|
891
|
+
if (cmdBase === '/effort') {
|
|
892
|
+
const agent = this.getAgent(channel, session?.agentId);
|
|
893
|
+
const currentModel = hasModelSwitcher(agent) ? agent.getModel() : agent.name;
|
|
894
|
+
const validEfforts = getAvailableEfforts(agent, currentModel);
|
|
895
|
+
const allValid = [...validEfforts, 'auto'];
|
|
896
|
+
if (!allValid.includes(arg)) {
|
|
897
|
+
return { error: `无效推理强度: ${arg},可选: ${allValid.join(' / ')}`, code: 'INVALID_VALUE' };
|
|
898
|
+
}
|
|
899
|
+
if (typeof agent.setEffort === 'function') {
|
|
900
|
+
agent.setEffort(arg === 'auto' ? undefined : arg);
|
|
901
|
+
}
|
|
902
|
+
if (evolagent)
|
|
903
|
+
evolagent.setBaseagentEffort(arg === 'auto' ? undefined : arg);
|
|
904
|
+
return { data: { effort: arg } };
|
|
905
|
+
}
|
|
906
|
+
if (cmdBase === '/chatmode') {
|
|
907
|
+
if (arg !== 'interactive' && arg !== 'proactive') {
|
|
908
|
+
return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
|
|
909
|
+
}
|
|
910
|
+
if (session) {
|
|
911
|
+
const chatType = session.chatType || 'private';
|
|
912
|
+
if (chatType === 'group' && identity.role !== 'owner' && identity.role !== 'admin') {
|
|
913
|
+
return { error: '无权限:群聊中仅管理员可切换', code: 'NO_PERMISSION' };
|
|
914
|
+
}
|
|
915
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: arg });
|
|
916
|
+
this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
|
|
641
917
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
918
|
+
else {
|
|
919
|
+
if (evolagent)
|
|
920
|
+
evolagent.setChatmodePrivate(arg);
|
|
921
|
+
}
|
|
922
|
+
return { data: { mode: arg } };
|
|
923
|
+
}
|
|
924
|
+
if (cmdBase === '/dispatch') {
|
|
925
|
+
if (arg !== 'mention' && arg !== 'broadcast') {
|
|
926
|
+
return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
|
|
927
|
+
}
|
|
928
|
+
const chatType = session?.chatType;
|
|
929
|
+
if (!session || chatType !== 'group') {
|
|
930
|
+
return { error: 'dispatch 仅在群聊会话中有效', code: 'NOT_APPLICABLE' };
|
|
931
|
+
}
|
|
932
|
+
if (identity.role !== 'owner' && identity.role !== 'admin') {
|
|
933
|
+
return { error: '无权限:群聊中仅管理员可切换', code: 'NO_PERMISSION' };
|
|
934
|
+
}
|
|
935
|
+
const metadata = { ...(session.metadata || {}), dispatchMode: arg };
|
|
936
|
+
await this.sessionManager.updateSession(session.id, { metadata });
|
|
937
|
+
this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
|
|
938
|
+
return { data: { mode: arg } };
|
|
939
|
+
}
|
|
940
|
+
if (cmdBase === '/perm') {
|
|
941
|
+
const need = this.requireSession(session);
|
|
942
|
+
if (need)
|
|
943
|
+
return need;
|
|
646
944
|
if (identity.role !== 'owner')
|
|
647
|
-
return { error: '无权限' };
|
|
945
|
+
return { error: '无权限', code: 'NO_PERMISSION' };
|
|
648
946
|
const permAgent = this.getAgent(channel, session.agentId);
|
|
649
947
|
const validModes = hasPermissionController(permAgent)
|
|
650
948
|
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
651
949
|
: ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
|
|
652
950
|
if (!validModes.includes(arg))
|
|
653
|
-
return { error: `无效模式: ${arg}
|
|
951
|
+
return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
|
|
654
952
|
const metadata = { ...(session.metadata || {}), permissionMode: arg };
|
|
655
953
|
await this.sessionManager.updateSession(session.id, { metadata });
|
|
656
954
|
return { data: { mode: arg } };
|
|
657
955
|
}
|
|
658
|
-
if (cmdBase === '/
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const chatType = session.chatType || 'private';
|
|
670
|
-
if (chatType === 'group' && identity.role !== 'owner' && identity.role !== 'admin') {
|
|
671
|
-
return { error: '无权限:群聊中仅管理员可切换' };
|
|
672
|
-
}
|
|
673
|
-
await this.sessionManager.updateSession(session.id, { sessionMode: arg });
|
|
674
|
-
this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: session.id, mode: arg, timestamp: Date.now() });
|
|
675
|
-
return { data: { mode: arg } };
|
|
956
|
+
if (cmdBase === '/activity') {
|
|
957
|
+
const modeMap = { all: 'all', dm: 'dm-only', owner: 'owner-dm-only', none: 'none' };
|
|
958
|
+
const newMode = modeMap[arg];
|
|
959
|
+
if (!newMode)
|
|
960
|
+
return { error: `无效模式: ${arg},可选: all / dm / owner / none`, code: 'INVALID_VALUE' };
|
|
961
|
+
if (identity.role !== 'owner')
|
|
962
|
+
return { error: '中间输出模式切换仅限 owner', code: 'NO_PERMISSION' };
|
|
963
|
+
if (!this.agentRegistry?.setShowActivities)
|
|
964
|
+
return { error: '找不到通道所属 agent,无法持久化', code: 'EXEC_FAILED' };
|
|
965
|
+
this.agentRegistry.setShowActivities(channel, newMode);
|
|
966
|
+
return { data: { mode: newMode } };
|
|
676
967
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
968
|
+
return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
969
|
+
}
|
|
970
|
+
/** menu.action — 触发动词。 */
|
|
971
|
+
async execMenuAction(cmd, action, args, channel, channelId, userId) {
|
|
972
|
+
const cmdBase = cmd.trim().split(' ')[0];
|
|
973
|
+
if (!cmdBase)
|
|
974
|
+
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
975
|
+
if (!action)
|
|
976
|
+
return { error: '缺少 action', code: 'MISSING_VALUE' };
|
|
977
|
+
const { session } = await this.loadMenuContext(channel, channelId);
|
|
978
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
979
|
+
// ── /session 系列 ──
|
|
980
|
+
if (cmdBase === '/session' || cmdBase === '/s') {
|
|
981
|
+
if (action === 'stop') {
|
|
982
|
+
if (!session)
|
|
983
|
+
return { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
|
|
984
|
+
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
985
|
+
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
986
|
+
const hasActive = sessionAgent.hasActiveStream(sessionKey);
|
|
987
|
+
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
988
|
+
if (queueLength === 0 && !hasActive) {
|
|
989
|
+
return { error: '当前没有正在处理的任务', code: 'NO_ACTIVE_TASK' };
|
|
990
|
+
}
|
|
991
|
+
await sessionAgent.interrupt(sessionKey);
|
|
992
|
+
this.eventBus.publish({
|
|
993
|
+
type: 'task:interrupted',
|
|
994
|
+
sessionId: sessionKey,
|
|
995
|
+
reason: 'stop',
|
|
996
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
997
|
+
});
|
|
998
|
+
this.sessionManager.clearProcessing(sessionKey);
|
|
999
|
+
return { data: { action: 'stop', success: true } };
|
|
1000
|
+
}
|
|
1001
|
+
if (action === 'new') {
|
|
1002
|
+
const name = (args?.name ?? '').toString().trim();
|
|
1003
|
+
return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true });
|
|
1004
|
+
}
|
|
1005
|
+
if (action === 'delete') {
|
|
1006
|
+
const target = (args?.target ?? '').toString().trim();
|
|
1007
|
+
if (!target)
|
|
1008
|
+
return { error: '缺少 args.target', code: 'MISSING_VALUE' };
|
|
1009
|
+
return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId);
|
|
1010
|
+
}
|
|
1011
|
+
if (action === 'switch') {
|
|
1012
|
+
const target = (args?.target ?? '').toString().trim();
|
|
1013
|
+
if (!target)
|
|
1014
|
+
return { error: '缺少 args.target', code: 'MISSING_VALUE' };
|
|
1015
|
+
return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true });
|
|
1016
|
+
}
|
|
1017
|
+
if (action === 'compact') {
|
|
1018
|
+
const need = this.requireSession(session);
|
|
1019
|
+
if (need)
|
|
1020
|
+
return need;
|
|
1021
|
+
return await this.delegateAsAction(action, '/compact', channel, channelId, userId);
|
|
1022
|
+
}
|
|
1023
|
+
if (action === 'fork') {
|
|
1024
|
+
const need = this.requireSession(session);
|
|
1025
|
+
if (need)
|
|
1026
|
+
return need;
|
|
1027
|
+
const name = (args?.name ?? '').toString().trim();
|
|
1028
|
+
return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true });
|
|
1029
|
+
}
|
|
1030
|
+
return { error: `不支持的 session action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
1031
|
+
}
|
|
1032
|
+
// ── /system 系列 ──
|
|
1033
|
+
if (cmdBase === '/system') {
|
|
1034
|
+
if (action === 'restart') {
|
|
1035
|
+
if (identity.role !== 'owner')
|
|
1036
|
+
return { error: '无权限:服务重启仅限 owner 使用', code: 'NO_PERMISSION' };
|
|
1037
|
+
const restartInfo = { channel, channelId, timestamp: Date.now() };
|
|
1038
|
+
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
1039
|
+
const { spawn } = await import('child_process');
|
|
1040
|
+
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
|
|
1041
|
+
detached: true,
|
|
1042
|
+
stdio: 'ignore',
|
|
1043
|
+
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
1044
|
+
}).unref();
|
|
1045
|
+
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
1046
|
+
setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 500);
|
|
1047
|
+
return { data: { action: 'restart', success: true } };
|
|
691
1048
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1049
|
+
if (action === 'check') {
|
|
1050
|
+
return await this.delegateAsAction(action, '/check', channel, channelId, userId);
|
|
1051
|
+
}
|
|
1052
|
+
if (action === 'upgrade') {
|
|
1053
|
+
if (identity.role !== 'owner')
|
|
1054
|
+
return { error: '无权限:升级仅限 owner 使用', code: 'NO_PERMISSION' };
|
|
1055
|
+
return await this.delegateAsAction(action, '/upgrade', channel, channelId, userId);
|
|
1056
|
+
}
|
|
1057
|
+
return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
1058
|
+
}
|
|
1059
|
+
return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
1060
|
+
}
|
|
1061
|
+
/** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
|
|
1062
|
+
async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
|
|
1063
|
+
try {
|
|
1064
|
+
const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId);
|
|
1065
|
+
if (result == null) {
|
|
1066
|
+
// null / undefined: 命令未识别或前置守卫拦截(如 idle 检查),视为失败
|
|
1067
|
+
return { error: '命令未执行(可能被前置守卫拦截)', code: 'EXEC_FAILED' };
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof result !== 'object' || !('kind' in result)) {
|
|
1070
|
+
return { data: { action, success: true } };
|
|
1071
|
+
}
|
|
1072
|
+
const payload = result;
|
|
1073
|
+
if (payload.kind === 'command.error') {
|
|
1074
|
+
return { error: payload.text || '执行失败', code: 'EXEC_FAILED' };
|
|
1075
|
+
}
|
|
1076
|
+
const data = { action, success: true };
|
|
1077
|
+
if (payload.text)
|
|
1078
|
+
data.message = payload.text;
|
|
1079
|
+
// 对于切换/创建类动作,附加切换后的活跃 session 信息便于客户端继续操作
|
|
1080
|
+
if (opts.enrichSession) {
|
|
1081
|
+
const newSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
1082
|
+
if (newSession) {
|
|
1083
|
+
data.session = { id: newSession.id, name: newSession.name || null };
|
|
1084
|
+
if (newSession.agentSessionId)
|
|
1085
|
+
data.session.agentSessionId = newSession.agentSessionId;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return { data };
|
|
1089
|
+
}
|
|
1090
|
+
catch (e) {
|
|
1091
|
+
return { error: e?.message || String(e), code: 'INTERNAL' };
|
|
696
1092
|
}
|
|
697
|
-
return { error: `不支持 exec 模式: ${cmdBase}` };
|
|
698
1093
|
}
|
|
699
1094
|
isCommand(content) {
|
|
700
1095
|
return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
|
|
@@ -707,6 +1102,10 @@ export class CommandHandler {
|
|
|
707
1102
|
return result;
|
|
708
1103
|
}
|
|
709
1104
|
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
|
|
1105
|
+
// 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
|
|
1106
|
+
// 不应覆盖 session 中已有的正确值
|
|
1107
|
+
if (source === 'card-trigger')
|
|
1108
|
+
chatType = undefined;
|
|
710
1109
|
// 解析身份(按实例名)
|
|
711
1110
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
712
1111
|
const policy = this.getPolicy(channel);
|
|
@@ -727,26 +1126,13 @@ export class CommandHandler {
|
|
|
727
1126
|
logger.info(`[CommandHandler] handle: channel=${channel} channelId=${channelId} cmd="${normalizedContent.split(' ')[0]}" user=${userId ?? 'n/a'} role=${identity?.role ?? 'n/a'}`);
|
|
728
1127
|
// 话题内禁用部分命令
|
|
729
1128
|
if (threadId) {
|
|
730
|
-
const threadBlocked = ['/new', '/slist', '/
|
|
1129
|
+
const threadBlocked = ['/new', '/slist', '/s', '/session', '/fork', '/del', '/baseagent'];
|
|
731
1130
|
const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
|
|
732
1131
|
if (isBlocked) {
|
|
733
1132
|
return { kind: 'command.error', text: '⚠️ 话题中不支持此命令' };
|
|
734
1133
|
}
|
|
735
1134
|
}
|
|
736
1135
|
// Agent-owned 通道:禁止项目切换和 agent 切换
|
|
737
|
-
const owningAgent = this.getOwningAgent(channel);
|
|
738
|
-
if (owningAgent) {
|
|
739
|
-
const isProjectCmd = normalizedContent === '/project' || normalizedContent.startsWith('/project ') ||
|
|
740
|
-
normalizedContent === '/bind' || normalizedContent.startsWith('/bind ') ||
|
|
741
|
-
normalizedContent === '/plist' ||
|
|
742
|
-
normalizedContent === '/p' || normalizedContent.startsWith('/p ');
|
|
743
|
-
if (isProjectCmd) {
|
|
744
|
-
return { kind: 'command.error', text: `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}` };
|
|
745
|
-
}
|
|
746
|
-
if (normalizedContent.startsWith('/agent ')) {
|
|
747
|
-
return { kind: 'command.error', text: `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}` };
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
1136
|
// 权限检查:区分用户级命令和管理级命令
|
|
751
1137
|
const isOwner = identity.role === 'owner';
|
|
752
1138
|
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
@@ -755,7 +1141,7 @@ export class CommandHandler {
|
|
|
755
1141
|
// guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
|
|
756
1142
|
const guestGroupCommands = [
|
|
757
1143
|
'/status', '/help', '/evolhelp', '/check', '/chatmode', '/dispatch',
|
|
758
|
-
'/model', '/setmodel', '/effort', '/
|
|
1144
|
+
'/model', '/setmodel', '/effort', '/baseagent', '/perm', '/activity', '/safe', '/stop',
|
|
759
1145
|
'/resume', '/trigger',
|
|
760
1146
|
];
|
|
761
1147
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
@@ -775,12 +1161,12 @@ export class CommandHandler {
|
|
|
775
1161
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
776
1162
|
// 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
|
|
777
1163
|
// - 始终需要 idle(无参即写):/new /clear /compact /repair /fork
|
|
778
|
-
// - 仅带参时需要 idle(无参是列表/用法):/session /
|
|
1164
|
+
// - 仅带参时需要 idle(无参是列表/用法):/session /baseagent /rewind
|
|
779
1165
|
// - /chatmode:在 handler 内部自行做写操作的 idle 检查
|
|
780
1166
|
// - /dispatch:在 handler 内部自行做写操作的 idle 检查
|
|
781
1167
|
// - /safe:已禁用 no-op,不再要求 idle
|
|
782
1168
|
const idleAlways = ['/new', '/clear', '/compact', '/repair', '/fork'];
|
|
783
|
-
const idleWhenArg = ['/session', '/
|
|
1169
|
+
const idleWhenArg = ['/session', '/baseagent', '/rewind'];
|
|
784
1170
|
const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
|
|
785
1171
|
idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
|
|
786
1172
|
if (needsIdle) {
|
|
@@ -852,10 +1238,8 @@ export class CommandHandler {
|
|
|
852
1238
|
const lines = [
|
|
853
1239
|
'可用命令:',
|
|
854
1240
|
'',
|
|
855
|
-
'📁
|
|
1241
|
+
'📁 项目:',
|
|
856
1242
|
' /pwd - 显示当前项目路径',
|
|
857
|
-
' /p [name|path] - 列出或切换项目',
|
|
858
|
-
...(isOwner ? [' /bind <path> - 绑定新项目目录'] : []),
|
|
859
1243
|
'',
|
|
860
1244
|
'🔄 会话管理:',
|
|
861
1245
|
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
@@ -867,7 +1251,7 @@ export class CommandHandler {
|
|
|
867
1251
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
868
1252
|
'',
|
|
869
1253
|
'🤖 Agent 与模型:',
|
|
870
|
-
' /
|
|
1254
|
+
' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
|
|
871
1255
|
' /model [model] - 查看或切换模型',
|
|
872
1256
|
' /effort [level] - 查看或切换推理强度',
|
|
873
1257
|
'',
|
|
@@ -886,7 +1270,7 @@ export class CommandHandler {
|
|
|
886
1270
|
' /stop - 中断当前任务',
|
|
887
1271
|
' /check - 检查渠道状态',
|
|
888
1272
|
...(isAdmin ? [
|
|
889
|
-
' /restart
|
|
1273
|
+
' /restart - 重启服务(owner only)',
|
|
890
1274
|
] : []),
|
|
891
1275
|
...(isOwner ? [
|
|
892
1276
|
' /restart - 重启服务',
|
|
@@ -895,8 +1279,6 @@ export class CommandHandler {
|
|
|
895
1279
|
'',
|
|
896
1280
|
'🧰 工具:',
|
|
897
1281
|
' /file [channel] <path> - 发送项目内文件',
|
|
898
|
-
' /aid [list|show|new|delete|lookup|agentmd] - AID 身份管理',
|
|
899
|
-
' /storage [upload|download|ls|rm|quota] <aid> - 文件存储',
|
|
900
1282
|
] : []),
|
|
901
1283
|
'',
|
|
902
1284
|
'❓ 帮助:',
|
|
@@ -907,12 +1289,8 @@ export class CommandHandler {
|
|
|
907
1289
|
// /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
|
|
908
1290
|
if (normalizedContent === '/evolhelp') {
|
|
909
1291
|
const cmds = [];
|
|
910
|
-
//
|
|
911
|
-
cmds.push({ command: '/pwd', description: '显示当前项目路径', category: '
|
|
912
|
-
cmds.push({ command: '/p', aliases: ['/project', '/plist'], args: '[name|path]', description: '列出或切换项目', category: '项目管理', roles: ['admin', 'owner'] });
|
|
913
|
-
if (isOwner) {
|
|
914
|
-
cmds.push({ command: '/bind', args: '<path>', description: '绑定新项目目录', category: '项目管理', roles: ['owner'] });
|
|
915
|
-
}
|
|
1292
|
+
// 项目
|
|
1293
|
+
cmds.push({ command: '/pwd', description: '显示当前项目路径', category: '项目', roles: ['admin', 'owner'] });
|
|
916
1294
|
// 会话管理
|
|
917
1295
|
cmds.push({ command: '/new', args: '[名称]', description: '创建新会话(清空历史请用此命令,可选命名)', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
|
|
918
1296
|
cmds.push({ command: '/s', aliases: ['/session', '/slist'], args: '[cli|名称|序号|uuid]', description: '列出或切换会话', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
|
|
@@ -925,7 +1303,7 @@ export class CommandHandler {
|
|
|
925
1303
|
}
|
|
926
1304
|
// Agent 与模型
|
|
927
1305
|
if (isAdmin) {
|
|
928
|
-
cmds.push({ command: '/
|
|
1306
|
+
cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
929
1307
|
cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
930
1308
|
cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
931
1309
|
}
|
|
@@ -940,13 +1318,10 @@ export class CommandHandler {
|
|
|
940
1318
|
cmds.push({ command: '/check', description: '检查渠道状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
|
|
941
1319
|
if (isAdmin) {
|
|
942
1320
|
cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '聊天设置', roles: ['admin', 'owner'] });
|
|
943
|
-
cmds.push({ command: '/restart', args: '<channel>', description: '重连指定渠道', category: '运维', roles: ['admin', 'owner'] });
|
|
944
1321
|
}
|
|
945
1322
|
if (isOwner) {
|
|
946
1323
|
cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['owner'] });
|
|
947
1324
|
cmds.push({ command: '/file', args: '[channel] <path>', description: '发送项目内文件', category: '工具', roles: ['owner'] });
|
|
948
|
-
cmds.push({ command: '/aid', args: '[list|show|new|delete|lookup|agentmd]', description: 'AID 身份管理', category: '工具', roles: ['owner'] });
|
|
949
|
-
cmds.push({ command: '/storage', args: '[upload|download|ls|rm|quota] <aid>', description: '文件存储', category: '工具', roles: ['owner'] });
|
|
950
1325
|
}
|
|
951
1326
|
// 聊天设置
|
|
952
1327
|
if (isAdmin) {
|
|
@@ -1174,9 +1549,9 @@ export class CommandHandler {
|
|
|
1174
1549
|
return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
|
1175
1550
|
}
|
|
1176
1551
|
}
|
|
1177
|
-
// /
|
|
1178
|
-
if (normalizedContent === '/
|
|
1179
|
-
const args = normalizedContent.slice(
|
|
1552
|
+
// /baseagent 命令:查看或切换 Agent 后端
|
|
1553
|
+
if (normalizedContent === '/baseagent' || normalizedContent.startsWith('/baseagent ')) {
|
|
1554
|
+
const args = normalizedContent.slice(10).trim();
|
|
1180
1555
|
// 切换(带参)需权限:群聊 owner only,私聊 admin+;无参查询对所有人放开
|
|
1181
1556
|
if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
|
|
1182
1557
|
return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
|
|
@@ -1200,7 +1575,7 @@ export class CommandHandler {
|
|
|
1200
1575
|
title: '🔌 切换 Agent',
|
|
1201
1576
|
buttons: available.map(a => ({
|
|
1202
1577
|
label: a === currentAgent ? `✓ ${a}` : a,
|
|
1203
|
-
command: `/
|
|
1578
|
+
command: `/baseagent ${a}`,
|
|
1204
1579
|
style: (a === currentAgent ? 'primary' : 'default'),
|
|
1205
1580
|
disabled: a === currentAgent,
|
|
1206
1581
|
})),
|
|
@@ -1216,7 +1591,7 @@ export class CommandHandler {
|
|
|
1216
1591
|
const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
|
|
1217
1592
|
const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
|
|
1218
1593
|
if (canSwitchAgent) {
|
|
1219
|
-
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n用法: /
|
|
1594
|
+
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n用法: /baseagent <name>` };
|
|
1220
1595
|
}
|
|
1221
1596
|
return { kind: 'command.result', text: `当前 Agent: ${currentAgent}` };
|
|
1222
1597
|
}
|
|
@@ -1251,37 +1626,16 @@ export class CommandHandler {
|
|
|
1251
1626
|
const currentModel = hasModelSwitcher(setmodelAgent) ? setmodelAgent.getModel() : setmodelAgent.name;
|
|
1252
1627
|
const efforts = getAvailableEfforts(setmodelAgent, currentModel);
|
|
1253
1628
|
const currentEffort = setmodelAgent.getEffort?.() || 'auto';
|
|
1254
|
-
|
|
1255
|
-
let apiBaseUrl;
|
|
1256
|
-
try {
|
|
1257
|
-
const configBaseUrl = this.getOwningAgent(channel)?.config?.baseagents?.claude?.baseUrl;
|
|
1258
|
-
const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
|
|
1259
|
-
if (configBaseUrl && !isPlaceholderUrl) {
|
|
1260
|
-
apiBaseUrl = configBaseUrl;
|
|
1261
|
-
}
|
|
1262
|
-
else if (process.env.ANTHROPIC_BASE_URL) {
|
|
1263
|
-
apiBaseUrl = process.env.ANTHROPIC_BASE_URL;
|
|
1264
|
-
}
|
|
1265
|
-
else {
|
|
1266
|
-
const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
1267
|
-
if (fs.existsSync(claudeSettingsPath)) {
|
|
1268
|
-
const claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
|
|
1269
|
-
if (claudeSettings.env?.ANTHROPIC_BASE_URL) {
|
|
1270
|
-
apiBaseUrl = claudeSettings.env.ANTHROPIC_BASE_URL;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
catch { }
|
|
1629
|
+
const modelListSource = getModelListSource(this.getOwningAgent(channel), setmodelAgent);
|
|
1276
1630
|
let modelListData = null;
|
|
1277
|
-
if (apiBaseUrl) {
|
|
1631
|
+
if (modelListSource.apiBaseUrl) {
|
|
1278
1632
|
try {
|
|
1279
|
-
const modelsUrl = apiBaseUrl.replace(/\/+$/, '') + '/v1/models';
|
|
1633
|
+
const modelsUrl = modelListSource.apiBaseUrl.replace(/\/+$/, '') + '/v1/models';
|
|
1280
1634
|
const controller = new AbortController();
|
|
1281
1635
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1282
1636
|
const resp = await fetch(modelsUrl, {
|
|
1283
1637
|
signal: controller.signal,
|
|
1284
|
-
headers: { 'Authorization': `Bearer ${
|
|
1638
|
+
headers: { 'Authorization': `Bearer ${modelListSource.apiKey || ''}` },
|
|
1285
1639
|
});
|
|
1286
1640
|
clearTimeout(timeout);
|
|
1287
1641
|
if (resp.ok) {
|
|
@@ -1295,11 +1649,12 @@ export class CommandHandler {
|
|
|
1295
1649
|
const now = Math.floor(Date.now() / 1000);
|
|
1296
1650
|
modelListData = {
|
|
1297
1651
|
object: 'list',
|
|
1298
|
-
data:
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1652
|
+
data: modelListSource.fallbackModels.map(id => ({
|
|
1653
|
+
id,
|
|
1654
|
+
object: 'model',
|
|
1655
|
+
created: now,
|
|
1656
|
+
owned_by: modelListSource.owner,
|
|
1657
|
+
})),
|
|
1303
1658
|
};
|
|
1304
1659
|
}
|
|
1305
1660
|
return { kind: 'command.result', text: JSON.stringify({
|
|
@@ -1506,54 +1861,12 @@ export class CommandHandler {
|
|
|
1506
1861
|
return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
|
|
1507
1862
|
return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
|
|
1508
1863
|
}
|
|
1509
|
-
// /aid, /rpc, /storage —
|
|
1510
|
-
if (normalizedContent === '/
|
|
1864
|
+
// /agent, /aid, /rpc, /storage — 仅限 ctl 调用,slash 输入拒绝
|
|
1865
|
+
if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ') ||
|
|
1866
|
+
normalizedContent === '/aid' || normalizedContent.startsWith('/aid ') ||
|
|
1511
1867
|
normalizedContent === '/rpc' || normalizedContent.startsWith('/rpc ') ||
|
|
1512
1868
|
normalizedContent === '/storage' || normalizedContent.startsWith('/storage ')) {
|
|
1513
|
-
|
|
1514
|
-
return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
|
|
1515
|
-
// 无参数时返回用法说明
|
|
1516
|
-
if (normalizedContent === '/aid') {
|
|
1517
|
-
return { kind: 'command.result', text: `用法:
|
|
1518
|
-
/aid list 列出本地所有 AID
|
|
1519
|
-
/aid show <aid> 查看 AID 详情
|
|
1520
|
-
/aid new <aid> 创建新 AID
|
|
1521
|
-
/aid delete <aid> 删除本地 AID
|
|
1522
|
-
/aid lookup <aid> 远程探测 AID
|
|
1523
|
-
/aid agentmd put <aid> 签名并上传 agent.md
|
|
1524
|
-
/aid agentmd get <aid> 下载并验签 agent.md` };
|
|
1525
|
-
}
|
|
1526
|
-
if (normalizedContent === '/rpc') {
|
|
1527
|
-
return { kind: 'command.result', text: `用法: /rpc --as <aid> --params <json>
|
|
1528
|
-
示例: /rpc --as myaid.agentid.pub --params {"method":"meta.ping","params":{}}` };
|
|
1529
|
-
}
|
|
1530
|
-
if (normalizedContent === '/storage') {
|
|
1531
|
-
return { kind: 'command.result', text: `用法:
|
|
1532
|
-
/storage upload <aid> <file> <path> [--public]
|
|
1533
|
-
/storage download <aid> <url> [local-path]
|
|
1534
|
-
/storage ls <aid> [prefix]
|
|
1535
|
-
/storage rm <aid> <path>
|
|
1536
|
-
/storage quota <aid>` };
|
|
1537
|
-
}
|
|
1538
|
-
const cliArgs = normalizedContent.slice(1); // strip leading /
|
|
1539
|
-
try {
|
|
1540
|
-
const { execFile } = await import('node:child_process');
|
|
1541
|
-
const { promisify } = await import('node:util');
|
|
1542
|
-
const execFileAsync = promisify(execFile);
|
|
1543
|
-
const { stdout, stderr } = await execFileAsync('evolclaw', cliArgs.split(/\s+/), {
|
|
1544
|
-
timeout: 30000,
|
|
1545
|
-
encoding: 'utf-8',
|
|
1546
|
-
env: { ...process.env, AUN_LOG_INI_DISABLE: '1' },
|
|
1547
|
-
});
|
|
1548
|
-
const output = (stdout || '').trim();
|
|
1549
|
-
if (!output && stderr)
|
|
1550
|
-
return { kind: 'command.result', text: `⚠ ${stderr.trim().slice(0, 500)}` };
|
|
1551
|
-
return { kind: 'command.result', text: output || '(无输出)' };
|
|
1552
|
-
}
|
|
1553
|
-
catch (e) {
|
|
1554
|
-
const msg = e.stderr?.trim() || e.stdout?.trim() || String(e.message || e);
|
|
1555
|
-
return { kind: 'command.error', text: `❌ ${msg.slice(0, 500)}` };
|
|
1556
|
-
}
|
|
1869
|
+
return { kind: 'command.error', text: '❌ 此命令仅限 ctl 调用,不支持 slash 输入' };
|
|
1557
1870
|
}
|
|
1558
1871
|
if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
|
|
1559
1872
|
const activityArg = normalizedContent.slice(9).trim();
|
|
@@ -1878,10 +2191,7 @@ export class CommandHandler {
|
|
|
1878
2191
|
session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
1879
2192
|
}
|
|
1880
2193
|
// 对于需要会话的命令,如果没有会话则使用默认项目创建临时会话
|
|
1881
|
-
// 这样 /pwd、/status 等命令可以在没有活跃会话时返回默认项目信息
|
|
1882
2194
|
if (!session && (normalizedContent.startsWith('/new') ||
|
|
1883
|
-
normalizedContent.startsWith('/bind') ||
|
|
1884
|
-
normalizedContent.startsWith('/project') ||
|
|
1885
2195
|
normalizedContent === '/pwd' ||
|
|
1886
2196
|
normalizedContent === '/status')) {
|
|
1887
2197
|
session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel));
|
|
@@ -2075,44 +2385,8 @@ export class CommandHandler {
|
|
|
2075
2385
|
}
|
|
2076
2386
|
return { kind: 'command.result', text: lines.join('\n') };
|
|
2077
2387
|
}
|
|
2078
|
-
// /restart 命令:重启服务(owner only)
|
|
2079
|
-
if (normalizedContent === '/restart'
|
|
2080
|
-
const restartArg = normalizedContent.slice('/restart'.length).trim();
|
|
2081
|
-
// /restart <type> — 重连指定类型的所有渠道(admin only)
|
|
2082
|
-
if (restartArg) {
|
|
2083
|
-
if (!isAdmin)
|
|
2084
|
-
return { kind: 'command.error', text: '❌ 无权限:渠道重连仅限管理员使用' };
|
|
2085
|
-
const type = restartArg;
|
|
2086
|
-
// /restart 是服务级操作:重连该 type 下的所有实例(不分 agent)
|
|
2087
|
-
const scopedNames = [];
|
|
2088
|
-
for (const [name] of this.adapters) {
|
|
2089
|
-
if (this.channelTypeMap.get(name) === type)
|
|
2090
|
-
scopedNames.push(name);
|
|
2091
|
-
}
|
|
2092
|
-
if (scopedNames.length === 0) {
|
|
2093
|
-
return { kind: 'command.error', text: `❌ 没有类型为 "${type}" 的渠道` };
|
|
2094
|
-
}
|
|
2095
|
-
const results = [];
|
|
2096
|
-
for (const name of scopedNames) {
|
|
2097
|
-
const ch = this.channelObjects.get(name);
|
|
2098
|
-
if (!ch) {
|
|
2099
|
-
results.push(`${name}: 未找到渠道对象`);
|
|
2100
|
-
continue;
|
|
2101
|
-
}
|
|
2102
|
-
if (!ch.reconnect) {
|
|
2103
|
-
results.push(`${name}: 不支持重连`);
|
|
2104
|
-
continue;
|
|
2105
|
-
}
|
|
2106
|
-
try {
|
|
2107
|
-
const result = await ch.reconnect();
|
|
2108
|
-
results.push(`${name}: ${result}`);
|
|
2109
|
-
}
|
|
2110
|
-
catch (e) {
|
|
2111
|
-
results.push(`${name}: 重连失败 - ${e?.message || e}`);
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
return { kind: 'command.result', text: `🔄 重连 ${type}:\n ${results.join('\n ')}` };
|
|
2115
|
-
}
|
|
2388
|
+
// /restart 命令:重启服务(owner only)
|
|
2389
|
+
if (normalizedContent === '/restart') {
|
|
2116
2390
|
// /restart(无参数)— 重启整个服务(owner only)
|
|
2117
2391
|
if (!isOwner)
|
|
2118
2392
|
return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 owner 使用' };
|
|
@@ -2195,7 +2469,6 @@ export class CommandHandler {
|
|
|
2195
2469
|
}
|
|
2196
2470
|
// /pwd 命令:显示当前项目路径
|
|
2197
2471
|
if (normalizedContent === '/pwd') {
|
|
2198
|
-
// session 现在总是存在(上面已自动创建)
|
|
2199
2472
|
if (!session) {
|
|
2200
2473
|
return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
|
|
2201
2474
|
}
|
|
@@ -2309,241 +2582,6 @@ export class CommandHandler {
|
|
|
2309
2582
|
return { kind: 'command.error', text: `❌ 文件发送失败: ${error.message || error}` };
|
|
2310
2583
|
}
|
|
2311
2584
|
}
|
|
2312
|
-
// /plist 命令:列出所有项目
|
|
2313
|
-
if (normalizedContent === '/plist') {
|
|
2314
|
-
if (!policy.canListProjects(session?.chatType || 'private', identity.role)) {
|
|
2315
|
-
if (!session) {
|
|
2316
|
-
return { kind: 'command.error', text: `❌ 当前群聊未绑定项目
|
|
2317
|
-
|
|
2318
|
-
请使用 /bind <项目路径> 绑定项目` };
|
|
2319
|
-
}
|
|
2320
|
-
const projectName = this.getProjectName(session.projectPath);
|
|
2321
|
-
const isProcessing = !!session.processingState;
|
|
2322
|
-
const status = isProcessing ? '[处理中]' : '[空闲]';
|
|
2323
|
-
return { kind: 'command.result', text: `当前群聊绑定的项目:
|
|
2324
|
-
${projectName} (${session.projectPath}) - ${status}
|
|
2325
|
-
|
|
2326
|
-
提示:群聊不支持切换项目` };
|
|
2327
|
-
}
|
|
2328
|
-
// 收集项目信息并按最近活跃排序(唯一来源:agent config projects.list)
|
|
2329
|
-
const entries = [];
|
|
2330
|
-
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
2331
|
-
// 跳过不存在的路径
|
|
2332
|
-
if (!fs.existsSync(projectPath))
|
|
2333
|
-
continue;
|
|
2334
|
-
const isCurrent = session ? path.resolve(session.projectPath) === path.resolve(projectPath) : false;
|
|
2335
|
-
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
2336
|
-
entries.push({
|
|
2337
|
-
name, projectPath, projectSession, isCurrent,
|
|
2338
|
-
updatedAt: projectSession?.updatedAt ?? 0,
|
|
2339
|
-
});
|
|
2340
|
-
}
|
|
2341
|
-
// 当前活跃项目置顶,其余按 updatedAt 降序
|
|
2342
|
-
entries.sort((a, b) => {
|
|
2343
|
-
if (a.isCurrent !== b.isCurrent)
|
|
2344
|
-
return a.isCurrent ? -1 : 1;
|
|
2345
|
-
return b.updatedAt - a.updatedAt;
|
|
2346
|
-
});
|
|
2347
|
-
// 构建项目状态文本的辅助函数
|
|
2348
|
-
const buildStatusText = (entry) => {
|
|
2349
|
-
const { projectSession, isCurrent } = entry;
|
|
2350
|
-
if (!projectSession)
|
|
2351
|
-
return '无会话';
|
|
2352
|
-
const parts = [];
|
|
2353
|
-
if (isCurrent) {
|
|
2354
|
-
parts.push('活跃');
|
|
2355
|
-
}
|
|
2356
|
-
else {
|
|
2357
|
-
parts.push(formatIdleTime(Date.now() - projectSession.updatedAt));
|
|
2358
|
-
}
|
|
2359
|
-
const isProcessing = !!projectSession.processingState;
|
|
2360
|
-
if (isProcessing) {
|
|
2361
|
-
const qLen = this.messageQueue.getQueueLength(projectSession.id);
|
|
2362
|
-
parts.push(qLen > 0 ? `[处理中,队列${qLen}条]` : '[处理中]');
|
|
2363
|
-
}
|
|
2364
|
-
const unread = this.messageCache.getCount(projectSession.id);
|
|
2365
|
-
if (unread > 0) {
|
|
2366
|
-
parts.push(`[${unread}条新消息]`);
|
|
2367
|
-
}
|
|
2368
|
-
else if (!isProcessing && !isCurrent) {
|
|
2369
|
-
parts.push('[空闲]');
|
|
2370
|
-
}
|
|
2371
|
-
return parts.join(' ');
|
|
2372
|
-
};
|
|
2373
|
-
// 尝试发送 CommandCard 卡片(每个项目一个按钮,一键切换)
|
|
2374
|
-
if (entries.length > 0) {
|
|
2375
|
-
const bodyLines = entries.map(e => {
|
|
2376
|
-
const status = buildStatusText(e);
|
|
2377
|
-
const prefix = e.isCurrent ? '✓' : '•';
|
|
2378
|
-
return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
|
|
2379
|
-
});
|
|
2380
|
-
const interaction = {
|
|
2381
|
-
type: 'interaction',
|
|
2382
|
-
id: `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
|
2383
|
-
channelId,
|
|
2384
|
-
sessionId: activeSession?.id || '',
|
|
2385
|
-
initiatorId: userId,
|
|
2386
|
-
kind: {
|
|
2387
|
-
kind: 'command-card',
|
|
2388
|
-
title: '📂 项目列表',
|
|
2389
|
-
body: bodyLines.join('\n'),
|
|
2390
|
-
buttons: entries.map(e => ({
|
|
2391
|
-
label: e.isCurrent ? `✓ ${e.name}` : e.name,
|
|
2392
|
-
command: `/project ${e.name}`,
|
|
2393
|
-
style: (e.isCurrent ? 'primary' : 'default'),
|
|
2394
|
-
disabled: e.isCurrent,
|
|
2395
|
-
})),
|
|
2396
|
-
},
|
|
2397
|
-
};
|
|
2398
|
-
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
2399
|
-
const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
|
|
2400
|
-
if (cardResult === null)
|
|
2401
|
-
return null;
|
|
2402
|
-
return { kind: 'command.result', text: cardResult };
|
|
2403
|
-
}
|
|
2404
|
-
// 降级:文本列表
|
|
2405
|
-
const lines = ['可用项目:'];
|
|
2406
|
-
for (const entry of entries) {
|
|
2407
|
-
const prefix = entry.isCurrent ? ' ✓' : ' ';
|
|
2408
|
-
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
2409
|
-
}
|
|
2410
|
-
lines.push('', '提示: 使用 /p <名称> 切换项目');
|
|
2411
|
-
return { kind: 'command.result', text: lines.join('\n') };
|
|
2412
|
-
}
|
|
2413
|
-
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
2414
|
-
if (normalizedContent === '/project') {
|
|
2415
|
-
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
2416
|
-
// 群聊不能切换项目,交由 /plist 逻辑处理
|
|
2417
|
-
}
|
|
2418
|
-
const delegated = await this.handle('/plist', channel, channelId, undefined, userId, threadId);
|
|
2419
|
-
return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
|
|
2420
|
-
}
|
|
2421
|
-
// /project 命令:切换项目(支持名称或路径)
|
|
2422
|
-
if (normalizedContent.startsWith('/project ')) {
|
|
2423
|
-
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
2424
|
-
return { kind: 'command.error', text: `❌ 群聊不支持切换项目
|
|
2425
|
-
|
|
2426
|
-
群聊只能绑定一个项目。如需更换项目,请联系管理员重新配置。` };
|
|
2427
|
-
}
|
|
2428
|
-
let arg = normalizedContent.slice(9).trim();
|
|
2429
|
-
if (!arg)
|
|
2430
|
-
return { kind: 'command.result', text: '用法: /p <name|path> 或 /project <name|path>' };
|
|
2431
|
-
// 检查确认标志
|
|
2432
|
-
const hasConfirm = arg.endsWith(' --confirm');
|
|
2433
|
-
if (hasConfirm) {
|
|
2434
|
-
arg = arg.slice(0, -10).trim();
|
|
2435
|
-
}
|
|
2436
|
-
let projectPath;
|
|
2437
|
-
let projectName;
|
|
2438
|
-
if (arg.includes('/')) {
|
|
2439
|
-
if (!path.isAbsolute(arg)) {
|
|
2440
|
-
return { kind: 'command.error', text: '❌ 项目路径必须是绝对路径' };
|
|
2441
|
-
}
|
|
2442
|
-
if (!fs.existsSync(arg)) {
|
|
2443
|
-
return { kind: 'command.error', text: `❌ 路径不存在: ${arg}` };
|
|
2444
|
-
}
|
|
2445
|
-
projectPath = arg;
|
|
2446
|
-
projectName = path.basename(arg);
|
|
2447
|
-
}
|
|
2448
|
-
else {
|
|
2449
|
-
projectPath = this.projects[arg];
|
|
2450
|
-
if (!projectPath) {
|
|
2451
|
-
return { kind: 'command.error', text: `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目` };
|
|
2452
|
-
}
|
|
2453
|
-
projectName = arg;
|
|
2454
|
-
}
|
|
2455
|
-
if (session) {
|
|
2456
|
-
const normalizedSessionPath = path.resolve(session.projectPath);
|
|
2457
|
-
const normalizedProjectPath = path.resolve(projectPath);
|
|
2458
|
-
if (normalizedSessionPath === normalizedProjectPath) {
|
|
2459
|
-
return { kind: 'command.result', text: `当前已在项目: ${projectName}\n 路径: ${projectPath}` };
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
// 群聊切换项目需要确认
|
|
2463
|
-
const isGroupChat = session?.chatType === 'group';
|
|
2464
|
-
if (isGroupChat && !hasConfirm) {
|
|
2465
|
-
return { kind: 'command.error', text: `⚠️ 群聊切换项目风险提示:
|
|
2466
|
-
|
|
2467
|
-
切换项目将影响所有群成员的对话上下文,可能导致:
|
|
2468
|
-
• 当前项目的会话历史被切换
|
|
2469
|
-
• 正在处理的任务被中断
|
|
2470
|
-
• 其他成员的工作受到影响
|
|
2471
|
-
|
|
2472
|
-
确认切换请执行:
|
|
2473
|
-
/p ${projectName} --confirm` };
|
|
2474
|
-
}
|
|
2475
|
-
const currentAgentId = activeSession?.agentId || this.primaryRunnerKey;
|
|
2476
|
-
const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath, currentAgentId);
|
|
2477
|
-
this.eventBus.publish({
|
|
2478
|
-
type: 'project:switched',
|
|
2479
|
-
sessionId: newSession.id,
|
|
2480
|
-
channel,
|
|
2481
|
-
channelId,
|
|
2482
|
-
projectPath,
|
|
2483
|
-
timestamp: Date.now()
|
|
2484
|
-
});
|
|
2485
|
-
const cachedEvents = this.messageCache.getEvents(newSession.id);
|
|
2486
|
-
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
2487
|
-
const currentAgent = newSession.agentId || this.primaryRunnerKey;
|
|
2488
|
-
let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n Agent: ${currentAgent}\n ${hasExistingSession}`;
|
|
2489
|
-
if (cachedEvents.length > 0 && sendMessage) {
|
|
2490
|
-
for (const event of cachedEvents) {
|
|
2491
|
-
if (event.type === 'completed') {
|
|
2492
|
-
response += `\n\n后台任务完成`;
|
|
2493
|
-
if (event.metadata?.duration) {
|
|
2494
|
-
response += ` (耗时: ${Math.round(event.metadata.duration / 1000)}s)`;
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
else if (event.type === 'error') {
|
|
2498
|
-
response += `\n\n后台任务失败: ${event.metadata?.errorType || '未知错误'}`;
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
await sendMessage(channelId, response);
|
|
2502
|
-
for (const event of cachedEvents) {
|
|
2503
|
-
await sendMessage(channelId, event.message);
|
|
2504
|
-
}
|
|
2505
|
-
this.messageCache.clearEvents(newSession.id);
|
|
2506
|
-
return { kind: 'command.result', text: '' };
|
|
2507
|
-
}
|
|
2508
|
-
return { kind: 'command.result', text: response };
|
|
2509
|
-
}
|
|
2510
|
-
// /bind 命令:持久化项目到配置(不切换)(owner only)
|
|
2511
|
-
if (normalizedContent === '/bind')
|
|
2512
|
-
return { kind: 'command.result', text: '用法: /bind <路径>' };
|
|
2513
|
-
if (normalizedContent.startsWith('/bind ')) {
|
|
2514
|
-
if (!isOwner)
|
|
2515
|
-
return { kind: 'command.error', text: '❌ 无权限:此命令仅限 owner 使用' };
|
|
2516
|
-
const projectPath = normalizedContent.slice(6).trim();
|
|
2517
|
-
if (!projectPath)
|
|
2518
|
-
return { kind: 'command.result', text: '用法: /bind <路径>' };
|
|
2519
|
-
if (!path.isAbsolute(projectPath)) {
|
|
2520
|
-
return { kind: 'command.error', text: '❌ 项目路径必须是绝对路径' };
|
|
2521
|
-
}
|
|
2522
|
-
if (!fs.existsSync(projectPath)) {
|
|
2523
|
-
if (this.getOwningAgent(channel)?.config?.projects?.autoCreate) {
|
|
2524
|
-
fs.mkdirSync(projectPath, { recursive: true });
|
|
2525
|
-
}
|
|
2526
|
-
else {
|
|
2527
|
-
return { kind: 'command.error', text: `❌ 路径不存在: ${projectPath}` };
|
|
2528
|
-
}
|
|
2529
|
-
}
|
|
2530
|
-
// 生成项目名称(使用目录名)
|
|
2531
|
-
const projectName = path.basename(projectPath);
|
|
2532
|
-
// 检查在当前 scope 内是否已存在
|
|
2533
|
-
const scopeProjects = this.getEffectiveProjects(channel);
|
|
2534
|
-
const existing = scopeProjects[projectName];
|
|
2535
|
-
if (existing) {
|
|
2536
|
-
if (existing === projectPath) {
|
|
2537
|
-
return { kind: 'command.result', text: `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目` };
|
|
2538
|
-
}
|
|
2539
|
-
return { kind: 'command.error', text: `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件` };
|
|
2540
|
-
}
|
|
2541
|
-
// 写入:agent-owned channel → agent.json;default → agent config
|
|
2542
|
-
const err = await this.addProjectInScope(channel, projectName, projectPath);
|
|
2543
|
-
if (err)
|
|
2544
|
-
return { kind: 'command.result', text: err };
|
|
2545
|
-
return { kind: 'command.result', text: `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目` };
|
|
2546
|
-
}
|
|
2547
2585
|
// /slist 命令:列出当前项目的会话
|
|
2548
2586
|
// /slist — 仅 EvolClaw 会话
|
|
2549
2587
|
// /slist cli — 仅 CLI 会话(未导入的)
|
|
@@ -2553,8 +2591,7 @@ export class CommandHandler {
|
|
|
2553
2591
|
|
|
2554
2592
|
请先执行以下操作之一:
|
|
2555
2593
|
1. 发送任意消息 - 自动创建新会话
|
|
2556
|
-
2. /new [名称] -
|
|
2557
|
-
3. /p <项目> - 切换到指定项目` };
|
|
2594
|
+
2. /new [名称] - 创建命名会话` };
|
|
2558
2595
|
}
|
|
2559
2596
|
const showCliOnly = normalizedContent === '/slist cli';
|
|
2560
2597
|
// /slist cli — 仅显示 CLI 会话
|
|
@@ -3000,8 +3037,10 @@ export class CommandHandler {
|
|
|
3000
3037
|
return null;
|
|
3001
3038
|
}
|
|
3002
3039
|
handleTrigger(content, channel, channelId, peerId, isAdmin) {
|
|
3003
|
-
|
|
3004
|
-
const
|
|
3040
|
+
// Resolve trigger manager/scheduler from the owning agent of this channel
|
|
3041
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
3042
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
3043
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
3005
3044
|
// Bare /trigger → list active
|
|
3006
3045
|
if (content === '/trigger') {
|
|
3007
3046
|
if (!manager)
|
|
@@ -3268,13 +3307,13 @@ export class CommandHandler {
|
|
|
3268
3307
|
// ── Agent Ctl ──
|
|
3269
3308
|
static CTL_COMMANDS = [
|
|
3270
3309
|
'/help', '/status', '/check', '/pwd',
|
|
3271
|
-
'/model', '/effort', '/perm', '/agent',
|
|
3272
|
-
'/compact', '/file', '/send', '/restart', '/
|
|
3273
|
-
'/rename', '/name', '/
|
|
3310
|
+
'/model', '/effort', '/perm', '/agent', '/baseagent',
|
|
3311
|
+
'/compact', '/file', '/send', '/restart', '/aid', '/rpc', '/storage',
|
|
3312
|
+
'/rename', '/name', '/trigger',
|
|
3274
3313
|
'/chatmode', '/dispatch', '/activity',
|
|
3275
3314
|
];
|
|
3276
3315
|
/** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
|
|
3277
|
-
static CTL_READONLY = new Set(['/
|
|
3316
|
+
static CTL_READONLY = new Set(['/baseagent']);
|
|
3278
3317
|
/**
|
|
3279
3318
|
* 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
|
|
3280
3319
|
* - 群聊话题:metadata.replyContext.{threadId,peerId}
|
|
@@ -3328,47 +3367,45 @@ export class CommandHandler {
|
|
|
3328
3367
|
}
|
|
3329
3368
|
// 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
|
|
3330
3369
|
const userId = session.metadata?.peerId;
|
|
3331
|
-
// 3.1 /
|
|
3332
|
-
if (cmd === '/
|
|
3333
|
-
const arg = cmd.slice('/
|
|
3370
|
+
// 3.1 /agent: EvolAgent 管理(转发到 CLI)
|
|
3371
|
+
if (cmd === '/agent' || cmd.startsWith('/agent ')) {
|
|
3372
|
+
const arg = cmd.slice('/agent'.length).trim();
|
|
3373
|
+
// 无参数时返回用法
|
|
3334
3374
|
if (!arg) {
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3375
|
+
return { ok: true, result: `用法:\n /agent list 列出所有 agent\n /agent show [name] 查看 agent 详情\n /agent enable <name> 启用 agent\n /agent disable <name> 停用 agent\n /agent get <name> <key> 读取配置字段\n /agent set <name> <key> <val> 修改配置字段\n /agent rename <name> <newname> 修改名称\n /agent reload [name] 热重载配置` };
|
|
3376
|
+
}
|
|
3377
|
+
const parts = arg.split(/\s+/);
|
|
3378
|
+
const subCmd = parts[0];
|
|
3379
|
+
// ctl 禁止 new/delete(仅限 CLI 操作)
|
|
3380
|
+
if (subCmd === 'new' || subCmd === 'delete') {
|
|
3381
|
+
return { ok: false, error: `❌ /agent ${subCmd} 仅限 CLI 操作,请使用: evolclaw agent ${subCmd} ...` };
|
|
3382
|
+
}
|
|
3383
|
+
// 自我保护:不能 disable 自己所在的 agent
|
|
3384
|
+
const selfAgent = this.getOwningAgent(session.channel);
|
|
3385
|
+
const selfName = selfAgent?.name;
|
|
3386
|
+
if (selfName && subCmd === 'disable' && parts[1] === selfName) {
|
|
3387
|
+
return { ok: false, error: `❌ 不能 disable 自己所在的 agent: ${selfName}` };
|
|
3388
|
+
}
|
|
3389
|
+
// 转发到 CLI
|
|
3390
|
+
const cliArgs = ['agent', ...parts];
|
|
3391
|
+
try {
|
|
3392
|
+
const { execFile } = await import('node:child_process');
|
|
3393
|
+
const { promisify } = await import('node:util');
|
|
3394
|
+
const execFileAsync = promisify(execFile);
|
|
3395
|
+
const { stdout, stderr } = await execFileAsync('evolclaw', cliArgs, {
|
|
3396
|
+
timeout: 30000,
|
|
3397
|
+
encoding: 'utf-8',
|
|
3398
|
+
env: { ...process.env, AUN_LOG_INI_DISABLE: '1' },
|
|
3399
|
+
});
|
|
3400
|
+
const output = (stdout || '').trim();
|
|
3401
|
+
if (!output && stderr)
|
|
3402
|
+
return { ok: true, result: `⚠ ${stderr.trim().slice(0, 500)}` };
|
|
3403
|
+
return { ok: true, result: output || '(无输出)' };
|
|
3340
3404
|
}
|
|
3341
|
-
|
|
3342
|
-
const
|
|
3343
|
-
|
|
3344
|
-
return { ok: false, error: '用法: evolclaw ctl evolagent reload <name>' };
|
|
3345
|
-
// I8: reload is a structural op, require admin or owner
|
|
3346
|
-
if (!userId) {
|
|
3347
|
-
return { ok: false, error: '权限不足:evolagent reload 仅 owner/admin 可用' };
|
|
3348
|
-
}
|
|
3349
|
-
const identity = this.sessionManager.resolveIdentity(session.channel, userId);
|
|
3350
|
-
if (identity.role !== 'owner' && identity.role !== 'admin') {
|
|
3351
|
-
return { ok: false, error: '权限不足:evolagent reload 仅 owner/admin 可用' };
|
|
3352
|
-
}
|
|
3353
|
-
if (!this.agentRegistry)
|
|
3354
|
-
return { ok: false, error: 'EvolAgentRegistry not available' };
|
|
3355
|
-
const a = this.agentRegistry.get(name);
|
|
3356
|
-
if (!a)
|
|
3357
|
-
return { ok: false, error: `Agent "${name}" not found` };
|
|
3358
|
-
const hooks = globalThis.__evolclaw_reloadHooks;
|
|
3359
|
-
if (!hooks)
|
|
3360
|
-
return { ok: false, error: 'Reload hooks not initialized' };
|
|
3361
|
-
if (!this.agentRegistry.reload)
|
|
3362
|
-
return { ok: false, error: 'EvolAgentRegistry.reload not available' };
|
|
3363
|
-
try {
|
|
3364
|
-
await this.agentRegistry.reload(name, hooks);
|
|
3365
|
-
return { ok: true, result: `Agent "${name}" reloaded` };
|
|
3366
|
-
}
|
|
3367
|
-
catch (e) {
|
|
3368
|
-
return { ok: false, error: `Reload failed: ${e?.message || e}` };
|
|
3369
|
-
}
|
|
3405
|
+
catch (e) {
|
|
3406
|
+
const msg = e.stderr?.trim() || e.stdout?.trim() || String(e.message || e);
|
|
3407
|
+
return { ok: false, error: msg.slice(0, 500) };
|
|
3370
3408
|
}
|
|
3371
|
-
return { ok: false, error: '用法: evolclaw ctl evolagent [reload <name>]' };
|
|
3372
3409
|
}
|
|
3373
3410
|
// 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
|
|
3374
3411
|
if (cmd.startsWith('/send ') || cmd === '/send') {
|
|
@@ -3386,9 +3423,10 @@ export class CommandHandler {
|
|
|
3386
3423
|
const taskId = replyContext?.metadata?.taskId;
|
|
3387
3424
|
const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
|
|
3388
3425
|
// --encrypt 覆盖 session 加密状态
|
|
3426
|
+
// 添加 source: 'ctl' 标记(用于区分 ec ctl send)
|
|
3389
3427
|
const enrichedReplyContext = forceEncrypt
|
|
3390
|
-
? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true } }
|
|
3391
|
-
: replyContext;
|
|
3428
|
+
? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true, source: 'ctl' } }
|
|
3429
|
+
: { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), source: 'ctl' } };
|
|
3392
3430
|
await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext: enrichedReplyContext }), { kind: 'result.text', text, isFinal: true });
|
|
3393
3431
|
// 出方向 jsonl 写入已下沉到 aun.ts:deliverTextEntry,message.send 成功后统一写入。
|
|
3394
3432
|
return { ok: true, result: 'ok' };
|
|
@@ -3410,6 +3448,56 @@ export class CommandHandler {
|
|
|
3410
3448
|
}
|
|
3411
3449
|
}
|
|
3412
3450
|
}
|
|
3451
|
+
// 5.1 /aid, /rpc, /storage — ctl 专属,转发到 CLI 执行
|
|
3452
|
+
if (cmd === '/aid' || cmd.startsWith('/aid ') ||
|
|
3453
|
+
cmd === '/rpc' || cmd.startsWith('/rpc ') ||
|
|
3454
|
+
cmd === '/storage' || cmd.startsWith('/storage ')) {
|
|
3455
|
+
// 权限检查:仅 owner
|
|
3456
|
+
if (userId) {
|
|
3457
|
+
const identity = this.sessionManager.resolveIdentity(session.channel, userId);
|
|
3458
|
+
if (identity.role !== 'owner') {
|
|
3459
|
+
return { ok: false, error: '无权限:此命令仅限 owner 使用' };
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
// 无参数时返回用法说明
|
|
3463
|
+
if (cmd === '/aid') {
|
|
3464
|
+
return { ok: true, result: `用法:\n /aid list 列出本地所有 AID\n /aid show <aid> 查看 AID 详情\n /aid new <aid> 创建新 AID\n /aid delete <aid> 删除本地 AID\n /aid lookup <aid> 远程探测 AID\n /aid agentmd put <aid> 签名并上传 agent.md\n /aid agentmd get <aid> 下载并验签 agent.md` };
|
|
3465
|
+
}
|
|
3466
|
+
if (cmd === '/rpc') {
|
|
3467
|
+
return { ok: true, result: `用法: /rpc --as <aid> --params <json>\n示例: /rpc --as myaid.agentid.pub --params {"method":"meta.ping","params":{}}` };
|
|
3468
|
+
}
|
|
3469
|
+
if (cmd === '/storage') {
|
|
3470
|
+
return { ok: true, result: `用法:\n /storage upload <aid> <file> <path> [--public]\n /storage download <aid> <url> [local-path]\n /storage ls <aid> [prefix]\n /storage rm <aid> <path>\n /storage quota <aid>` };
|
|
3471
|
+
}
|
|
3472
|
+
// /aid 自我保护:不能删除当前 agent 所用的 AID
|
|
3473
|
+
if (cmd.startsWith('/aid delete ')) {
|
|
3474
|
+
const targetAid = cmd.slice('/aid delete '.length).trim();
|
|
3475
|
+
const selfAgent = this.getOwningAgent(session.channel);
|
|
3476
|
+
const selfAid = selfAgent?.config?.aid;
|
|
3477
|
+
if (selfAid && targetAid === selfAid) {
|
|
3478
|
+
return { ok: false, error: `❌ 不能删除当前 agent 所用的 AID: ${selfAid}` };
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
const cliArgs = cmd.slice(1); // strip leading /
|
|
3482
|
+
try {
|
|
3483
|
+
const { execFile } = await import('node:child_process');
|
|
3484
|
+
const { promisify } = await import('node:util');
|
|
3485
|
+
const execFileAsync = promisify(execFile);
|
|
3486
|
+
const { stdout, stderr } = await execFileAsync('evolclaw', cliArgs.split(/\s+/), {
|
|
3487
|
+
timeout: 30000,
|
|
3488
|
+
encoding: 'utf-8',
|
|
3489
|
+
env: { ...process.env, AUN_LOG_INI_DISABLE: '1' },
|
|
3490
|
+
});
|
|
3491
|
+
const output = (stdout || '').trim();
|
|
3492
|
+
if (!output && stderr)
|
|
3493
|
+
return { ok: true, result: `⚠ ${stderr.trim().slice(0, 500)}` };
|
|
3494
|
+
return { ok: true, result: output || '(无输出)' };
|
|
3495
|
+
}
|
|
3496
|
+
catch (e) {
|
|
3497
|
+
const msg = e.stderr?.trim() || e.stdout?.trim() || String(e.message || e);
|
|
3498
|
+
return { ok: false, error: msg.slice(0, 500) };
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3413
3501
|
// 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
|
|
3414
3502
|
try {
|
|
3415
3503
|
const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
|