evolclaw 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -2
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -27
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1069 -141
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +28 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +406 -293
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +97 -150
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent.js +8 -5
- package/dist/cli/index.js +177 -44
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +473 -114
- package/dist/core/evolagent-registry.js +1 -0
- package/dist/core/evolagent.js +1 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +35 -13
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +49 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +295 -35
- package/dist/core/message/message-queue.js +2 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +56 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +27 -13
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +314 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +130 -8
- package/dist/ipc.js +17 -1
- package/dist/utils/cross-platform.js +23 -5
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/stats.js +14 -0
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/system-fragments/response-depth.md +16 -0
- package/package.json +4 -4
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
2
|
-
import { hasModelSwitcher, hasPermissionController } from '../agents/
|
|
2
|
+
import { hasModelSwitcher, hasPermissionController } from '../agents/runner-types.js';
|
|
3
3
|
import { getCodexEfforts } from '../agents/codex-runner.js';
|
|
4
4
|
import { renderCommandCardAsText } from './interaction-router.js';
|
|
5
5
|
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
@@ -13,11 +13,13 @@ 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
15
|
import { tryParseChannelKey } from './channel-loader.js';
|
|
16
|
-
import { loadDefaults } from '../config-store.js';
|
|
17
|
-
import { loadEvolclawConfig } from '../evolclaw-config.js';
|
|
16
|
+
import { loadDefaults, loadEvolclawConfig } from '../config-store.js';
|
|
18
17
|
import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './message/command-handler-agent-control.js';
|
|
18
|
+
import { displaySessionTitle } from './session/session-title.js';
|
|
19
19
|
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
20
20
|
const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
|
|
21
|
+
const PERMISSION_MODE_KEYS = ['auto', 'bypass', 'readonly', 'plan', 'edit', 'request', 'noask'];
|
|
22
|
+
const PERMISSION_MODE_USAGE = PERMISSION_MODE_KEYS.join('|');
|
|
21
23
|
// ── CLI 透传(menu.action name=cli action=exec)─────────────────────────
|
|
22
24
|
// 经消息通道的远程命令执行(RCE):仅 owner、白名单内只读+配置命令、无 shell、超时+截断。
|
|
23
25
|
// command → '*'(全部子命令放行) | Set(允许的子命令)。
|
|
@@ -26,6 +28,7 @@ const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
|
|
|
26
28
|
const CLI_EXEC_WHITELIST = {
|
|
27
29
|
status: '*',
|
|
28
30
|
model: '*',
|
|
31
|
+
stats: '*',
|
|
29
32
|
agent: new Set(['list', 'show', 'get']),
|
|
30
33
|
aid: new Set(['list', 'show', 'lookup']),
|
|
31
34
|
storage: new Set(['ls', 'quota']),
|
|
@@ -134,8 +137,12 @@ function formatIdleTime(ms) {
|
|
|
134
137
|
return `${minutes}分钟前`;
|
|
135
138
|
return '刚刚';
|
|
136
139
|
}
|
|
140
|
+
function isAdminRole(role) {
|
|
141
|
+
return role === 'owner' || role === 'admin';
|
|
142
|
+
}
|
|
137
143
|
// 支持的命令列表
|
|
138
|
-
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/
|
|
144
|
+
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
145
|
+
const deprecatedCommands = ['/clear'];
|
|
139
146
|
// 命令别名映射
|
|
140
147
|
const aliases = {
|
|
141
148
|
'/s': '/session',
|
|
@@ -463,6 +470,43 @@ export class CommandHandler {
|
|
|
463
470
|
accumulateErrors: () => true,
|
|
464
471
|
};
|
|
465
472
|
}
|
|
473
|
+
resolveMenuChatType(channel, channelId, explicit) {
|
|
474
|
+
if (explicit)
|
|
475
|
+
return explicit;
|
|
476
|
+
const active = this.sessionManager.getActiveSessionSync(channel, channelId);
|
|
477
|
+
return active?.chatType === 'group' ? 'group' : 'private';
|
|
478
|
+
}
|
|
479
|
+
canReadTopics(role) {
|
|
480
|
+
return role !== 'anonymous';
|
|
481
|
+
}
|
|
482
|
+
canDeleteTopic(role, chatType, topic, userId) {
|
|
483
|
+
if (role === 'anonymous')
|
|
484
|
+
return false;
|
|
485
|
+
if (isAdminRole(role))
|
|
486
|
+
return true;
|
|
487
|
+
if (chatType === 'group')
|
|
488
|
+
return false;
|
|
489
|
+
return !!userId && topic.metadata?.peerId === userId;
|
|
490
|
+
}
|
|
491
|
+
buildTopicMenuItem(s) {
|
|
492
|
+
const displayName = displaySessionTitle(s.name, s.threadId || s.id.slice(0, 8));
|
|
493
|
+
const item = {
|
|
494
|
+
value: s.threadId,
|
|
495
|
+
label: displayName,
|
|
496
|
+
};
|
|
497
|
+
if (s.agentSessionId) {
|
|
498
|
+
item.agentSessionId = s.agentSessionId;
|
|
499
|
+
const fileInfo = this.sessionManager.getSessionFileInfo(s.projectPath, s.agentSessionId, s.agentId);
|
|
500
|
+
if (fileInfo.turns)
|
|
501
|
+
item.turns = fileInfo.turns;
|
|
502
|
+
const firstMsg = this.sessionManager.readSessionFirstMessage(s.projectPath, s.agentSessionId, s.agentId);
|
|
503
|
+
if (firstMsg)
|
|
504
|
+
item.preview = firstMsg.length > 80 ? firstMsg.slice(0, 80) + '...' : firstMsg;
|
|
505
|
+
}
|
|
506
|
+
if (s.updatedAt)
|
|
507
|
+
item.lastActive = s.updatedAt;
|
|
508
|
+
return item;
|
|
509
|
+
}
|
|
466
510
|
/**
|
|
467
511
|
* 返回结构化命令菜单(供 menu.query 使用)
|
|
468
512
|
* owner 看到全部命令,admin 看到管理级命令(不含 owner-only),guest 仅看到用户级命令
|
|
@@ -470,9 +514,16 @@ export class CommandHandler {
|
|
|
470
514
|
getMenuItems(role, chatType = 'private') {
|
|
471
515
|
const isOwner = role === 'owner';
|
|
472
516
|
const isAdmin = role === 'owner' || role === 'admin';
|
|
517
|
+
const canReadTopic = role !== 'anonymous';
|
|
473
518
|
const items = [];
|
|
474
519
|
if (!isAdmin && chatType === 'group') {
|
|
475
520
|
return [
|
|
521
|
+
...(canReadTopic ? [{
|
|
522
|
+
group: '话题管理',
|
|
523
|
+
commands: [
|
|
524
|
+
{ cmd: '/topic', label: '话题管理', desc: '查看当前聊天的话题会话', next: { type: 'select', dynamic: true } },
|
|
525
|
+
]
|
|
526
|
+
}] : []),
|
|
476
527
|
{
|
|
477
528
|
group: '其他',
|
|
478
529
|
commands: [
|
|
@@ -488,6 +539,7 @@ export class CommandHandler {
|
|
|
488
539
|
commands: [
|
|
489
540
|
{ cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
|
|
490
541
|
{ cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
|
|
542
|
+
...(canReadTopic ? [{ cmd: '/topic', label: '话题管理', desc: '查看与管理当前聊天的话题会话', next: { type: 'select', dynamic: true } }] : []),
|
|
491
543
|
{ cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
|
|
492
544
|
{ cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
|
|
493
545
|
...(isAdmin ? [
|
|
@@ -526,6 +578,7 @@ export class CommandHandler {
|
|
|
526
578
|
...(isOwner ? [
|
|
527
579
|
{ value: 'auto', label: '自动模式', desc: '根据风险等级自动决定是否审批' },
|
|
528
580
|
{ value: 'bypass', label: '免审批模式', desc: '跳过所有工具审批确认' },
|
|
581
|
+
{ value: 'readonly', label: '只读模式', desc: '允许读取和临时目录写入,拒绝项目文件修改' },
|
|
529
582
|
{ value: 'plan', label: '计划模式', desc: '仅允许只读操作,写操作需审批' },
|
|
530
583
|
{ value: 'edit', label: '编辑模式', desc: '允许文件编辑,其他操作需审批' },
|
|
531
584
|
{ value: 'request', label: '请求模式', desc: '所有操作均需审批' },
|
|
@@ -576,7 +629,7 @@ export class CommandHandler {
|
|
|
576
629
|
return items;
|
|
577
630
|
}
|
|
578
631
|
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
579
|
-
async getSubMenuItems(cmd, channel, channelId, userId, args) {
|
|
632
|
+
async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, _explicitChatType) {
|
|
580
633
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
581
634
|
// ── 进程级 /agent list(owners 鉴权) ──
|
|
582
635
|
if (cmd === '/agent') {
|
|
@@ -595,28 +648,44 @@ export class CommandHandler {
|
|
|
595
648
|
if (!manager)
|
|
596
649
|
return [];
|
|
597
650
|
const scope = args?.options === 'all' ? 'all' : 'enabled';
|
|
598
|
-
const role = this.sessionManager.resolveIdentity(channel, userId).role;
|
|
651
|
+
const role = (overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId)).role;
|
|
599
652
|
const isAdmin = role === 'owner' || role === 'admin';
|
|
600
653
|
const all = manager.listAll();
|
|
601
654
|
const list = scope === 'all' ? all.active.concat(all.history) : manager.listActive();
|
|
602
655
|
const visible = isAdmin ? list
|
|
603
656
|
: list.filter((t) => t.createdByPeerId === (userId ?? '') && t.createdByChannel === channel);
|
|
604
657
|
return visible.map((t) => ({
|
|
658
|
+
// 透传完整 trigger 字段(ECWeb Triggers 表逐列渲染需要)
|
|
659
|
+
...t,
|
|
605
660
|
value: t.id,
|
|
606
661
|
label: t.name,
|
|
607
662
|
desc: `${t.scheduleType}${t.nextFireAt ? ` | 下次 ${new Date(t.nextFireAt).toLocaleString()}` : ''}`,
|
|
663
|
+
// 状态标识:history 条目带 doneReason(fired/cancelled/expired),active 条目恒为 'active'
|
|
664
|
+
status: t.doneReason ?? 'active',
|
|
608
665
|
}));
|
|
609
666
|
}
|
|
667
|
+
if (cmd === '/topic') {
|
|
668
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
669
|
+
if (!this.canReadTopics(identity.role)) {
|
|
670
|
+
throw { code: 'FORBIDDEN', message: '无权限查看话题' };
|
|
671
|
+
}
|
|
672
|
+
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
673
|
+
return sessions
|
|
674
|
+
.filter(s => !!s.threadId)
|
|
675
|
+
.map(s => this.buildTopicMenuItem(s));
|
|
676
|
+
}
|
|
610
677
|
if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
|
|
611
678
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
612
679
|
const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
|
|
613
680
|
const currentSession = session;
|
|
614
681
|
const items = sessions
|
|
682
|
+
.filter(s => !s.threadId)
|
|
615
683
|
.filter(s => !active || s.id !== active.id)
|
|
616
684
|
.map(s => {
|
|
685
|
+
const displayName = displaySessionTitle(s.name, s.id.slice(0, 8));
|
|
617
686
|
const item = {
|
|
618
687
|
value: s.name || s.id.slice(0, 8),
|
|
619
|
-
label:
|
|
688
|
+
label: displayName,
|
|
620
689
|
selected: currentSession ? s.id === currentSession.id : false,
|
|
621
690
|
};
|
|
622
691
|
if (s.agentSessionId) {
|
|
@@ -707,7 +776,7 @@ export class CommandHandler {
|
|
|
707
776
|
const permAgent = this.getAgent(channel, session?.agentId);
|
|
708
777
|
const validModes = hasPermissionController(permAgent)
|
|
709
778
|
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
710
|
-
: [
|
|
779
|
+
: [...PERMISSION_MODE_KEYS];
|
|
711
780
|
return validModes.map(m => ({ value: m, label: m, selected: m === currentMode }));
|
|
712
781
|
}
|
|
713
782
|
return null;
|
|
@@ -730,7 +799,7 @@ export class CommandHandler {
|
|
|
730
799
|
return s ? null : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
|
|
731
800
|
}
|
|
732
801
|
/** menu.query — 查询当前值。 */
|
|
733
|
-
async execMenuQuery(cmd, channel, channelId, userId, args) {
|
|
802
|
+
async execMenuQuery(cmd, channel, channelId, userId, args, _explicitChatType) {
|
|
734
803
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
735
804
|
if (!cmdBase)
|
|
736
805
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
@@ -790,6 +859,54 @@ export class CommandHandler {
|
|
|
790
859
|
data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
|
|
791
860
|
return { data };
|
|
792
861
|
}
|
|
862
|
+
if (cmdBase === '/topic') {
|
|
863
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
864
|
+
if (!this.canReadTopics(identity.role)) {
|
|
865
|
+
return { error: '无权限查看话题', code: 'FORBIDDEN' };
|
|
866
|
+
}
|
|
867
|
+
const target = (args?.target ?? '').toString().trim();
|
|
868
|
+
if (!target)
|
|
869
|
+
return { error: '缺少 args.target', code: 'MISSING_VALUE' };
|
|
870
|
+
const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
|
|
871
|
+
if (!topic)
|
|
872
|
+
return { error: '话题不存在', code: 'NOT_FOUND' };
|
|
873
|
+
const sessionKey = this.getQueueKey(topic, channel, channelId);
|
|
874
|
+
const sessionAgent = this.getAgent(channel, topic.agentId);
|
|
875
|
+
const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
|
|
876
|
+
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
877
|
+
const health = await this.sessionManager.getHealthStatus(topic.id);
|
|
878
|
+
let processingDuration;
|
|
879
|
+
if (isProcessing && topic.processingState) {
|
|
880
|
+
const elapsed = Date.now() - parseInt(topic.processingState, 10);
|
|
881
|
+
if (!isNaN(elapsed) && elapsed > 0)
|
|
882
|
+
processingDuration = Math.floor(elapsed / 1000);
|
|
883
|
+
}
|
|
884
|
+
let turns = 0;
|
|
885
|
+
if (topic.agentSessionId) {
|
|
886
|
+
turns = this.sessionManager.getSessionFileInfo(topic.projectPath, topic.agentSessionId, topic.agentId).turns;
|
|
887
|
+
}
|
|
888
|
+
const data = {
|
|
889
|
+
threadId: topic.threadId,
|
|
890
|
+
name: topic.name || null,
|
|
891
|
+
agentSessionId: topic.agentSessionId || null,
|
|
892
|
+
status: isProcessing ? 'processing' : 'idle',
|
|
893
|
+
createdAt: topic.createdAt,
|
|
894
|
+
updatedAt: topic.updatedAt,
|
|
895
|
+
};
|
|
896
|
+
if (processingDuration !== undefined)
|
|
897
|
+
data.processingDuration = processingDuration;
|
|
898
|
+
if (queueLength > 0)
|
|
899
|
+
data.queueLength = queueLength;
|
|
900
|
+
if (turns > 0)
|
|
901
|
+
data.turns = turns;
|
|
902
|
+
if (health.lastSuccessTime)
|
|
903
|
+
data.lastSuccess = health.lastSuccessTime;
|
|
904
|
+
if (health.consecutiveErrors)
|
|
905
|
+
data.consecutiveErrors = health.consecutiveErrors;
|
|
906
|
+
if (health.lastError)
|
|
907
|
+
data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
|
|
908
|
+
return { data };
|
|
909
|
+
}
|
|
793
910
|
if (cmdBase === '/baseagent') {
|
|
794
911
|
const value = session?.agentId ?? evolagent?.config?.active_baseagent ?? null;
|
|
795
912
|
return { data: { baseagent: value } };
|
|
@@ -863,6 +980,13 @@ export class CommandHandler {
|
|
|
863
980
|
data.version = pkg.version;
|
|
864
981
|
}
|
|
865
982
|
catch { }
|
|
983
|
+
try {
|
|
984
|
+
const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
|
|
985
|
+
const fp2 = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
986
|
+
if (fp2?.version)
|
|
987
|
+
data.fastaunVersion = fp2.version;
|
|
988
|
+
}
|
|
989
|
+
catch { }
|
|
866
990
|
const channels = owningAgent?.channelInstanceNames?.() ?? [];
|
|
867
991
|
if (channels.length)
|
|
868
992
|
data.channels = channels;
|
|
@@ -871,7 +995,7 @@ export class CommandHandler {
|
|
|
871
995
|
return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
872
996
|
}
|
|
873
997
|
/** menu.update — 写入新值。 */
|
|
874
|
-
async execMenuUpdate(cmd, value, channel, channelId, userId) {
|
|
998
|
+
async execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity) {
|
|
875
999
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
876
1000
|
if (!cmdBase)
|
|
877
1001
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
@@ -879,7 +1003,7 @@ export class CommandHandler {
|
|
|
879
1003
|
if (!arg)
|
|
880
1004
|
return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
|
|
881
1005
|
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
882
|
-
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1006
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
883
1007
|
// ── 关系级 /trigger update(调度参数,value 为 JSON 字符串) ──
|
|
884
1008
|
if (cmdBase === '/trigger') {
|
|
885
1009
|
const owningAgent = this.getOwningAgent(channel);
|
|
@@ -1020,7 +1144,7 @@ export class CommandHandler {
|
|
|
1020
1144
|
const permAgent = this.getAgent(channel, session.agentId);
|
|
1021
1145
|
const validModes = hasPermissionController(permAgent)
|
|
1022
1146
|
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
1023
|
-
: [
|
|
1147
|
+
: [...PERMISSION_MODE_KEYS];
|
|
1024
1148
|
if (!validModes.includes(arg))
|
|
1025
1149
|
return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
|
|
1026
1150
|
const metadata = { ...(session.metadata || {}), permissionMode: arg };
|
|
@@ -1052,14 +1176,14 @@ export class CommandHandler {
|
|
|
1052
1176
|
return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
1053
1177
|
}
|
|
1054
1178
|
/** menu.action — 触发动词。 */
|
|
1055
|
-
async execMenuAction(cmd, action, args, channel, channelId, userId) {
|
|
1179
|
+
async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType) {
|
|
1056
1180
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
1057
1181
|
if (!cmdBase)
|
|
1058
1182
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
1059
1183
|
if (!action)
|
|
1060
1184
|
return { error: '缺少 action', code: 'MISSING_VALUE' };
|
|
1061
1185
|
const { session } = await this.loadMenuContext(channel, channelId);
|
|
1062
|
-
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1186
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
1063
1187
|
// ── 进程级 /agent(owners 鉴权,不依赖 session/channel) ──
|
|
1064
1188
|
// NOTE(D5): 本次进程级 /agent 仅按 evolclaw.json owners 鉴权,任意 evolagent 的 AUN
|
|
1065
1189
|
// channel 均可作为入口。part1(daemon 控制 AID)落地后,应叠加 isControlChannel(channelId)
|
|
@@ -1072,6 +1196,19 @@ export class CommandHandler {
|
|
|
1072
1196
|
if (action === 'create') {
|
|
1073
1197
|
a.project = resolveProjectPath(a.project, a.aid ?? '', loadDefaults());
|
|
1074
1198
|
}
|
|
1199
|
+
// reload / disable / delete 会中断 agent 正在处理的任务,执行前检查是否繁忙。
|
|
1200
|
+
// 队列按 agent 名计数,故先用 registry 把 aid 解析成 name;force 跳过。
|
|
1201
|
+
if ((action === 'reload' || action === 'disable' || action === 'delete') && a.aid && !a.force) {
|
|
1202
|
+
const handle = this.agentRegistry?.get(a.aid) ?? null;
|
|
1203
|
+
const agentName = handle?.name;
|
|
1204
|
+
if (agentName) {
|
|
1205
|
+
const busy = this.messageQueue.getProcessingCountByAgent(agentName)
|
|
1206
|
+
+ this.messageQueue.getQueueLengthByAgent(agentName);
|
|
1207
|
+
if (busy > 0) {
|
|
1208
|
+
return { error: `该 Agent 有 ${busy} 个任务执行中`, code: 'BUSY' };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1075
1212
|
return await execAgentAction(action, a, userId ?? '');
|
|
1076
1213
|
}
|
|
1077
1214
|
// ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
|
|
@@ -1126,11 +1263,33 @@ export class CommandHandler {
|
|
|
1126
1263
|
return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
|
|
1127
1264
|
manager.moveToDone(trigger.id, 'cancelled');
|
|
1128
1265
|
scheduler.cancel(trigger.id);
|
|
1129
|
-
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: userId ?? '' });
|
|
1266
|
+
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, name: trigger.name, by: userId ?? '' });
|
|
1130
1267
|
return { data: { id: trigger.id, cancelled: true } };
|
|
1131
1268
|
}
|
|
1132
1269
|
return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
|
|
1133
1270
|
}
|
|
1271
|
+
if (cmdBase === '/topic') {
|
|
1272
|
+
if (action !== 'delete') {
|
|
1273
|
+
return { error: `不支持的 topic action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
1274
|
+
}
|
|
1275
|
+
const target = (args?.target ?? '').toString().trim();
|
|
1276
|
+
if (!target)
|
|
1277
|
+
return { error: '缺少 args.target', code: 'MISSING_VALUE' };
|
|
1278
|
+
const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
|
|
1279
|
+
if (!topic)
|
|
1280
|
+
return { error: '话题不存在', code: 'NOT_FOUND' };
|
|
1281
|
+
const chatType = this.resolveMenuChatType(channel, channelId, explicitChatType);
|
|
1282
|
+
if (!this.canDeleteTopic(identity.role, chatType, topic, userId)) {
|
|
1283
|
+
return { error: '无权限删除话题', code: 'FORBIDDEN' };
|
|
1284
|
+
}
|
|
1285
|
+
const success = await this.sessionManager.unbindSession(topic.id);
|
|
1286
|
+
if (!success)
|
|
1287
|
+
return { error: '删除失败', code: 'DELETE_FAILED' };
|
|
1288
|
+
this.eventBus.publish({ type: 'session:deleted', sessionId: topic.id });
|
|
1289
|
+
const targetAgent = this.getAgent(channel, topic.agentId);
|
|
1290
|
+
await targetAgent.closeSession?.(topic.id);
|
|
1291
|
+
return { data: { deleted: true } };
|
|
1292
|
+
}
|
|
1134
1293
|
// ── /session 系列 ──
|
|
1135
1294
|
if (cmdBase === '/session' || cmdBase === '/s') {
|
|
1136
1295
|
if (action === 'stop') {
|
|
@@ -1204,10 +1363,37 @@ export class CommandHandler {
|
|
|
1204
1363
|
return { data: { action: 'restart', success: true } };
|
|
1205
1364
|
}
|
|
1206
1365
|
if (action === 'check') {
|
|
1207
|
-
|
|
1366
|
+
const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity });
|
|
1367
|
+
const structured = r.data?.structured ?? null;
|
|
1368
|
+
if (structured)
|
|
1369
|
+
return { data: { ...r.data, ...structured } };
|
|
1370
|
+
return r;
|
|
1208
1371
|
}
|
|
1209
1372
|
if (action === 'upgrade') {
|
|
1210
|
-
|
|
1373
|
+
const devMode = isLinkedInstall();
|
|
1374
|
+
const localEvolclaw = getLocalVersion();
|
|
1375
|
+
// fastaun 本地版本:从 node_modules 读取(与 menu.query name=system 一致)
|
|
1376
|
+
let localFastaun = null;
|
|
1377
|
+
try {
|
|
1378
|
+
const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
|
|
1379
|
+
localFastaun = JSON.parse(fs.readFileSync(fp, 'utf-8'))?.version ?? null;
|
|
1380
|
+
}
|
|
1381
|
+
catch { }
|
|
1382
|
+
const [evolclawRemote, fastaunRemote, ecwebRemote] = await Promise.all([
|
|
1383
|
+
checkLatestVersion('evolclaw'),
|
|
1384
|
+
checkLatestVersion('@agentunion/fastaun'),
|
|
1385
|
+
checkLatestVersion('evolclaw-web'),
|
|
1386
|
+
]);
|
|
1387
|
+
const cmp = (local, remote) => !!(local && remote && compareVersions(local, remote) < 0);
|
|
1388
|
+
return {
|
|
1389
|
+
data: {
|
|
1390
|
+
devMode,
|
|
1391
|
+
evolclaw: { local: localEvolclaw, remote: evolclawRemote, hasUpdate: cmp(localEvolclaw, evolclawRemote) },
|
|
1392
|
+
fastaun: { local: localFastaun, remote: fastaunRemote, hasUpdate: cmp(localFastaun, fastaunRemote) },
|
|
1393
|
+
// ecweb 本地版本由 ECWeb 进程自身注入(data.ecwebVersion),此处仅给 remote
|
|
1394
|
+
ecweb: { remote: ecwebRemote },
|
|
1395
|
+
},
|
|
1396
|
+
};
|
|
1211
1397
|
}
|
|
1212
1398
|
return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
1213
1399
|
}
|
|
@@ -1232,6 +1418,79 @@ export class CommandHandler {
|
|
|
1232
1418
|
}
|
|
1233
1419
|
return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
1234
1420
|
}
|
|
1421
|
+
/** ECWeb 专用入口:注入 owner identity,进程级操作检查 owners 非空。不暴露 cli。 */
|
|
1422
|
+
async execMenuForEcweb(payload) {
|
|
1423
|
+
const id = payload?.id ?? '';
|
|
1424
|
+
const name = payload?.name;
|
|
1425
|
+
if (name === 'cli' || payload?.cmd === '/cli') {
|
|
1426
|
+
return { type: 'menu.response', id, name, error: { code: 'NOT_SUPPORTED', message: 'cli 不在 ECWeb 控制范围' } };
|
|
1427
|
+
}
|
|
1428
|
+
const isProcessLevel = name === 'system' || name === 'agent';
|
|
1429
|
+
const owners = loadEvolclawConfig().owners ?? [];
|
|
1430
|
+
if (isProcessLevel && owners.length === 0) {
|
|
1431
|
+
return { type: 'menu.response', id, name, error: { code: 'FORBIDDEN', message: '请在 evolclaw.json 配置 owners 后使用进程级操作' } };
|
|
1432
|
+
}
|
|
1433
|
+
const ECWEB_CHANNEL = '__ecweb__';
|
|
1434
|
+
// payload.agent(aid 或 name)时,用该 agent 的首个 channel 实例作为 channel 参数,
|
|
1435
|
+
// 让 execMenuQuery / execMenuUpdate 能按真实 agent 解析 model/effort/perm 等会话级配置。
|
|
1436
|
+
// system / agent 两个进程级 name 不走此路径,仍用 ECWEB_CHANNEL。
|
|
1437
|
+
const agentChannelKey = (() => {
|
|
1438
|
+
if (!payload?.agent || isProcessLevel)
|
|
1439
|
+
return ECWEB_CHANNEL;
|
|
1440
|
+
const handle = this.agentRegistry?.get(payload.agent) ?? null;
|
|
1441
|
+
return handle?.channelInstanceNames()?.[0] ?? ECWEB_CHANNEL;
|
|
1442
|
+
})();
|
|
1443
|
+
const ownerIdentity = { role: 'owner', mode: 'interactive' };
|
|
1444
|
+
// 进程级操作用 owners[0] 让 isProcessLevelOwner() 通过;其余传 undefined
|
|
1445
|
+
const userId = isProcessLevel ? (owners[0] ?? '') : undefined;
|
|
1446
|
+
const nameMap = {
|
|
1447
|
+
pwd: '/pwd', session: '/session', baseagent: '/baseagent', model: '/model',
|
|
1448
|
+
topic: '/topic',
|
|
1449
|
+
effort: '/effort', chatmode: '/chatmode', dispatch: '/dispatch',
|
|
1450
|
+
permission: '/perm', activity: '/activity', system: '/system',
|
|
1451
|
+
agent: '/agent', trigger: '/trigger',
|
|
1452
|
+
};
|
|
1453
|
+
const cmd = name ? (nameMap[name] ?? payload.cmd) : payload.cmd;
|
|
1454
|
+
try {
|
|
1455
|
+
switch (payload?.type) {
|
|
1456
|
+
case 'menu.list':
|
|
1457
|
+
return { type: 'menu.response', id, data: this.getMenuItems('owner', 'private') };
|
|
1458
|
+
case 'menu.query': {
|
|
1459
|
+
if (!cmd)
|
|
1460
|
+
return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
|
|
1461
|
+
const r = await this.execMenuQuery(cmd, agentChannelKey, agentChannelKey, userId, payload.args);
|
|
1462
|
+
return ecwebResp(id, name, r);
|
|
1463
|
+
}
|
|
1464
|
+
case 'menu.options': {
|
|
1465
|
+
if (!cmd)
|
|
1466
|
+
return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
|
|
1467
|
+
const data = await this.getSubMenuItems(cmd, agentChannelKey, agentChannelKey, userId, payload.args, ownerIdentity) ?? [];
|
|
1468
|
+
return { type: 'menu.response', id, name, data };
|
|
1469
|
+
}
|
|
1470
|
+
case 'menu.update': {
|
|
1471
|
+
if (!cmd)
|
|
1472
|
+
return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
|
|
1473
|
+
if (!payload.value)
|
|
1474
|
+
return ecwebErr(id, name, 'MISSING_VALUE', '缺少 value');
|
|
1475
|
+
const r = await this.execMenuUpdate(cmd, payload.value, agentChannelKey, agentChannelKey, userId, ownerIdentity);
|
|
1476
|
+
return ecwebResp(id, name, r);
|
|
1477
|
+
}
|
|
1478
|
+
case 'menu.action': {
|
|
1479
|
+
if (!cmd)
|
|
1480
|
+
return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
|
|
1481
|
+
if (!payload.action)
|
|
1482
|
+
return ecwebErr(id, name, 'MISSING_VALUE', '缺少 action');
|
|
1483
|
+
const r = await this.execMenuAction(cmd, payload.action, payload.args, agentChannelKey, agentChannelKey, userId, ownerIdentity);
|
|
1484
|
+
return ecwebResp(id, name, r);
|
|
1485
|
+
}
|
|
1486
|
+
default:
|
|
1487
|
+
return ecwebErr(id, name, 'NOT_SUPPORTED', `未知类型: ${payload?.type}`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
catch (e) {
|
|
1491
|
+
return ecwebErr(id, name, 'INTERNAL', e?.message ?? String(e));
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1235
1494
|
/**
|
|
1236
1495
|
* CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
|
|
1237
1496
|
* 不 in-process 调用(CLI handler 用 console.log + process.exit,spawn 行为与终端一致且隔离)。
|
|
@@ -1304,7 +1563,7 @@ export class CommandHandler {
|
|
|
1304
1563
|
/** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
|
|
1305
1564
|
async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
|
|
1306
1565
|
try {
|
|
1307
|
-
const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId);
|
|
1566
|
+
const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId, undefined, undefined, undefined, undefined, undefined, opts.overrideIdentity);
|
|
1308
1567
|
if (result == null) {
|
|
1309
1568
|
// null / undefined: 命令未识别或前置守卫拦截(如 idle 检查),视为失败
|
|
1310
1569
|
return { error: '命令未执行(可能被前置守卫拦截)', code: 'EXEC_FAILED' };
|
|
@@ -1319,6 +1578,8 @@ export class CommandHandler {
|
|
|
1319
1578
|
const data = { action, success: true };
|
|
1320
1579
|
if (payload.text)
|
|
1321
1580
|
data.message = payload.text;
|
|
1581
|
+
if (payload.structured)
|
|
1582
|
+
data.structured = payload.structured;
|
|
1322
1583
|
// 对于切换/创建类动作,附加切换后的活跃 session 信息便于客户端继续操作
|
|
1323
1584
|
if (opts.enrichSession) {
|
|
1324
1585
|
const newSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
@@ -1344,13 +1605,13 @@ export class CommandHandler {
|
|
|
1344
1605
|
const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID);
|
|
1345
1606
|
return result;
|
|
1346
1607
|
}
|
|
1347
|
-
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID) {
|
|
1608
|
+
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity) {
|
|
1348
1609
|
// 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
|
|
1349
1610
|
// 不应覆盖 session 中已有的正确值
|
|
1350
1611
|
if (source === 'card-trigger')
|
|
1351
1612
|
chatType = undefined;
|
|
1352
1613
|
// 解析身份(按实例名)
|
|
1353
|
-
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1614
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
1354
1615
|
const policy = this.getPolicy(channel);
|
|
1355
1616
|
// 按当前会话选择 agent 后端
|
|
1356
1617
|
const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
@@ -1403,12 +1664,12 @@ export class CommandHandler {
|
|
|
1403
1664
|
}
|
|
1404
1665
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
1405
1666
|
// 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
|
|
1406
|
-
// - 始终需要 idle(无参即写):/
|
|
1667
|
+
// - 始终需要 idle(无参即写):/compact /repair /fork /new
|
|
1407
1668
|
// - 仅带参时需要 idle(无参是列表/用法):/session /baseagent /rewind
|
|
1408
1669
|
// - /chatmode:在 handler 内部自行做写操作的 idle 检查
|
|
1409
1670
|
// - /dispatch:在 handler 内部自行做写操作的 idle 检查
|
|
1410
1671
|
// - /safe:已禁用 no-op,不再要求 idle
|
|
1411
|
-
const idleAlways = ['/
|
|
1672
|
+
const idleAlways = ['/compact', '/repair', '/fork', '/new'];
|
|
1412
1673
|
const idleWhenArg = ['/session', '/baseagent', '/rewind'];
|
|
1413
1674
|
const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
|
|
1414
1675
|
idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
|
|
@@ -1436,7 +1697,8 @@ export class CommandHandler {
|
|
|
1436
1697
|
// 检查是否以 / 开头(可能是命令)
|
|
1437
1698
|
if (normalizedContent.startsWith('/')) {
|
|
1438
1699
|
const inputCmd = normalizedContent.split(' ')[0];
|
|
1439
|
-
const isValidCommand = commands.some(cmd => normalizedContent.startsWith(cmd))
|
|
1700
|
+
const isValidCommand = commands.some(cmd => normalizedContent.startsWith(cmd)) ||
|
|
1701
|
+
deprecatedCommands.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '));
|
|
1440
1702
|
if (!isValidCommand) {
|
|
1441
1703
|
const similar = commands.find(cmd => {
|
|
1442
1704
|
const distance = levenshteinDistance(inputCmd, cmd);
|
|
@@ -1450,7 +1712,8 @@ export class CommandHandler {
|
|
|
1450
1712
|
}
|
|
1451
1713
|
}
|
|
1452
1714
|
}
|
|
1453
|
-
const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd))
|
|
1715
|
+
const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd)) ||
|
|
1716
|
+
deprecatedCommands.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '));
|
|
1454
1717
|
if (!isCmd)
|
|
1455
1718
|
return undefined;
|
|
1456
1719
|
// /help 命令不需要会话
|
|
@@ -1511,7 +1774,7 @@ export class CommandHandler {
|
|
|
1511
1774
|
'',
|
|
1512
1775
|
'🔐 权限管理:',
|
|
1513
1776
|
' /perm - 查看当前权限模式',
|
|
1514
|
-
...(isOwner ? [
|
|
1777
|
+
...(isOwner ? [` /perm <${PERMISSION_MODE_USAGE}> - 切换权限模式`] : []),
|
|
1515
1778
|
' /perm allow|always|deny - 审批权限请求',
|
|
1516
1779
|
'',
|
|
1517
1780
|
'🛠️ 运维:',
|
|
@@ -1558,7 +1821,7 @@ export class CommandHandler {
|
|
|
1558
1821
|
}
|
|
1559
1822
|
// 权限管理
|
|
1560
1823
|
if (isAdmin) {
|
|
1561
|
-
cmds.push({ command: '/perm', args: isOwner ?
|
|
1824
|
+
cmds.push({ command: '/perm', args: isOwner ? `<${PERMISSION_MODE_USAGE}>` : undefined, description: '查看当前权限模式', category: '权限管理', roles: ['admin', 'owner'] });
|
|
1562
1825
|
cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
|
|
1563
1826
|
}
|
|
1564
1827
|
// 运维
|
|
@@ -1690,11 +1953,11 @@ export class CommandHandler {
|
|
|
1690
1953
|
}
|
|
1691
1954
|
}
|
|
1692
1955
|
// 不是已知模式名也不是 allow/deny
|
|
1693
|
-
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') :
|
|
1956
|
+
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : PERMISSION_MODE_USAGE;
|
|
1694
1957
|
return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny` };
|
|
1695
1958
|
}
|
|
1696
1959
|
// 双参数不再支持,提示正确用法
|
|
1697
|
-
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') :
|
|
1960
|
+
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : PERMISSION_MODE_USAGE;
|
|
1698
1961
|
return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny` };
|
|
1699
1962
|
}
|
|
1700
1963
|
// /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
|
|
@@ -1864,7 +2127,7 @@ export class CommandHandler {
|
|
|
1864
2127
|
const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
|
|
1865
2128
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
1866
2129
|
const projectName = this.getProjectName(session.projectPath);
|
|
1867
|
-
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name
|
|
2130
|
+
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${displaySessionTitle(newSession.name, '(未命名)')}\n ${hasExistingSession}`;
|
|
1868
2131
|
if (source === 'card-trigger')
|
|
1869
2132
|
return null;
|
|
1870
2133
|
return { kind: 'command.result', text: agentSwitchResponse };
|
|
@@ -2352,7 +2615,8 @@ export class CommandHandler {
|
|
|
2352
2615
|
const sessionKey = stopSession.id;
|
|
2353
2616
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
2354
2617
|
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
2355
|
-
|
|
2618
|
+
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
2619
|
+
if (queueLength === 0 && !hasActive && !isProcessing) {
|
|
2356
2620
|
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
2357
2621
|
}
|
|
2358
2622
|
await stopAgent.interrupt(sessionKey);
|
|
@@ -2367,37 +2631,10 @@ export class CommandHandler {
|
|
|
2367
2631
|
this.sessionManager.clearProcessing(sessionKey);
|
|
2368
2632
|
return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
|
|
2369
2633
|
}
|
|
2370
|
-
// /clear
|
|
2634
|
+
// /clear 已移除:Claude/Codex/Gemini 对“清空当前 backend 历史”的语义不一致。
|
|
2635
|
+
// 统一使用 /new 创建新会话来开始全新上下文。
|
|
2371
2636
|
if (normalizedContent === '/clear') {
|
|
2372
|
-
|
|
2373
|
-
if ('error' in result)
|
|
2374
|
-
return { kind: 'command.error', text: result.error };
|
|
2375
|
-
const { session } = result;
|
|
2376
|
-
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
2377
|
-
if (!sessionAgent.capabilities?.clear) {
|
|
2378
|
-
return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代` };
|
|
2379
|
-
}
|
|
2380
|
-
if (!session.agentSessionId) {
|
|
2381
|
-
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需清空' };
|
|
2382
|
-
}
|
|
2383
|
-
const projectPath = path.isAbsolute(session.projectPath)
|
|
2384
|
-
? session.projectPath
|
|
2385
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
2386
|
-
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
2387
|
-
try {
|
|
2388
|
-
const cleared = await sessionAgent.clearSession(session.id, session.agentSessionId, projectPath);
|
|
2389
|
-
if (cleared) {
|
|
2390
|
-
await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
|
|
2391
|
-
sessionAgent.updateSessionId(session.id, '');
|
|
2392
|
-
return { kind: 'command.result', text: '✅ 已清空当前会话的对话历史' };
|
|
2393
|
-
}
|
|
2394
|
-
else {
|
|
2395
|
-
return { kind: 'command.error', text: '❌ 清空会话失败,请稍后重试' };
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
finally {
|
|
2399
|
-
releaseLock();
|
|
2400
|
-
}
|
|
2637
|
+
return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
|
|
2401
2638
|
}
|
|
2402
2639
|
// /compact 命令:手动压缩会话上下文
|
|
2403
2640
|
if (normalizedContent === '/compact') {
|
|
@@ -2422,7 +2659,10 @@ export class CommandHandler {
|
|
|
2422
2659
|
}
|
|
2423
2660
|
const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
|
|
2424
2661
|
if (compacted) {
|
|
2425
|
-
return {
|
|
2662
|
+
return {
|
|
2663
|
+
kind: 'command.result',
|
|
2664
|
+
text: '✅ 会话压缩完成',
|
|
2665
|
+
};
|
|
2426
2666
|
}
|
|
2427
2667
|
else {
|
|
2428
2668
|
return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
|
|
@@ -2491,7 +2731,7 @@ export class CommandHandler {
|
|
|
2491
2731
|
const chatModeLine = `会话模式: ${sessionMode}`;
|
|
2492
2732
|
const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
|
|
2493
2733
|
if (isAdmin) {
|
|
2494
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name
|
|
2734
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${displaySessionTitle(session.name, '(未命名)')}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`);
|
|
2495
2735
|
if (health.consecutiveErrors > 0) {
|
|
2496
2736
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
2497
2737
|
}
|
|
@@ -2517,6 +2757,9 @@ export class CommandHandler {
|
|
|
2517
2757
|
}
|
|
2518
2758
|
}
|
|
2519
2759
|
const projectPath = this.getEffectiveDefaultPath(channel);
|
|
2760
|
+
if (sendMessage && session) {
|
|
2761
|
+
await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
|
|
2762
|
+
}
|
|
2520
2763
|
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.primaryRunnerKey);
|
|
2521
2764
|
this.eventBus.publish({
|
|
2522
2765
|
type: 'session:created',
|
|
@@ -2539,12 +2782,16 @@ export class CommandHandler {
|
|
|
2539
2782
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
2540
2783
|
const subCmd = normalizedContent.slice('/check'.length).trim();
|
|
2541
2784
|
// 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
|
|
2542
|
-
//
|
|
2785
|
+
// __ecweb__ 是 ECWeb 系统级入口,展示全量渠道
|
|
2543
2786
|
const checkOwningAgent = this.getOwningAgent(channel);
|
|
2544
2787
|
let allowedChannels;
|
|
2545
2788
|
if (checkOwningAgent) {
|
|
2546
2789
|
allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
|
|
2547
2790
|
}
|
|
2791
|
+
else if (channel === '__ecweb__') {
|
|
2792
|
+
// ECWeb 全局视图:展示所有渠道
|
|
2793
|
+
allowedChannels = new Set(this.adapters.keys());
|
|
2794
|
+
}
|
|
2548
2795
|
else {
|
|
2549
2796
|
// default 范围:不再有 default channel 概念,等价于"所有 channel"
|
|
2550
2797
|
const defaultNames = [];
|
|
@@ -2632,7 +2879,29 @@ export class CommandHandler {
|
|
|
2632
2879
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
2633
2880
|
}
|
|
2634
2881
|
}
|
|
2635
|
-
|
|
2882
|
+
const checkSnap = this.statsCollector?.getSnapshot(currentAgentName);
|
|
2883
|
+
const structured = {
|
|
2884
|
+
channels: [...groups.entries()].map(([type, instances]) => ({ type, instances })),
|
|
2885
|
+
queue: {
|
|
2886
|
+
pending: this.messageQueue.getQueueLengthByAgent(currentAgentName),
|
|
2887
|
+
processing: this.messageQueue.getProcessingCountByAgent(currentAgentName),
|
|
2888
|
+
},
|
|
2889
|
+
uptimeMs,
|
|
2890
|
+
lastHour: checkSnap?.lastHour ?? null,
|
|
2891
|
+
evolagents: this.agentRegistry?.list().map((ag) => ({
|
|
2892
|
+
name: ag.name, aid: ag.aid ?? '', status: ag.status,
|
|
2893
|
+
baseagent: ag.baseagent ?? null,
|
|
2894
|
+
activeTasks: this.messageQueue.getProcessingCountByAgent(ag.name)
|
|
2895
|
+
+ this.messageQueue.getQueueLengthByAgent(ag.name),
|
|
2896
|
+
error: ag.error,
|
|
2897
|
+
})) ?? [],
|
|
2898
|
+
baseagents: [...this.agentMap.entries()].map(([key, runner]) => ({
|
|
2899
|
+
name: key.split('::')[1] ?? key,
|
|
2900
|
+
activeStreams: runner.activeStreamCount?.() ?? 0,
|
|
2901
|
+
healthy: true,
|
|
2902
|
+
})),
|
|
2903
|
+
};
|
|
2904
|
+
return { kind: 'command.result', text: lines.join('\n'), structured };
|
|
2636
2905
|
}
|
|
2637
2906
|
// /restart 命令:重启服务(owner only)
|
|
2638
2907
|
if (normalizedContent === '/restart') {
|
|
@@ -2873,7 +3142,7 @@ export class CommandHandler {
|
|
|
2873
3142
|
}
|
|
2874
3143
|
const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
|
|
2875
3144
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2876
|
-
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
3145
|
+
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
|
|
2877
3146
|
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
2878
3147
|
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
|
|
2879
3148
|
if (orphanCliSessions.length === 0) {
|
|
@@ -2923,7 +3192,7 @@ export class CommandHandler {
|
|
|
2923
3192
|
}
|
|
2924
3193
|
// /slist — 仅显示 EvolClaw 会话
|
|
2925
3194
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2926
|
-
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId
|
|
3195
|
+
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
|
|
2927
3196
|
// 从 SDK 同步会话名称(发现 CLI 改名)
|
|
2928
3197
|
try {
|
|
2929
3198
|
const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
|
|
@@ -2941,20 +3210,16 @@ export class CommandHandler {
|
|
|
2941
3210
|
logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
|
|
2942
3211
|
}
|
|
2943
3212
|
// 构建可显示会话列表(复用于卡片和文本)
|
|
2944
|
-
const hideTopics = currentProjectSessions.length > 10;
|
|
2945
|
-
const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
|
|
2946
3213
|
const maxDisplay = 10;
|
|
2947
3214
|
const displaySessions = [];
|
|
2948
3215
|
let displayIndex = 0;
|
|
2949
3216
|
for (let i = 0; i < currentProjectSessions.length; i++) {
|
|
2950
3217
|
const s = currentProjectSessions[i];
|
|
2951
|
-
if (hideTopics && s.threadId)
|
|
2952
|
-
continue;
|
|
2953
3218
|
if (displayIndex >= maxDisplay)
|
|
2954
3219
|
break;
|
|
2955
3220
|
const isActive = s.metadata?.isActive === true;
|
|
2956
3221
|
displayIndex++;
|
|
2957
|
-
const name = s.name
|
|
3222
|
+
const name = displaySessionTitle(s.name, '(未命名)');
|
|
2958
3223
|
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
2959
3224
|
const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
|
|
2960
3225
|
let status = '[空闲]';
|
|
@@ -2973,10 +3238,9 @@ export class CommandHandler {
|
|
|
2973
3238
|
if (this.interactionRouter && displaySessions.length >= 1) {
|
|
2974
3239
|
const bodyLines = displaySessions.map(ds => {
|
|
2975
3240
|
const prefix = ds.isActive ? '✓' : '•';
|
|
2976
|
-
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
2977
3241
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
2978
3242
|
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
2979
|
-
return `${prefix} ${ds.index}. ${
|
|
3243
|
+
return `${prefix} ${ds.index}. ${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
|
|
2980
3244
|
});
|
|
2981
3245
|
const interaction = {
|
|
2982
3246
|
type: 'interaction',
|
|
@@ -3011,22 +3275,19 @@ export class CommandHandler {
|
|
|
3011
3275
|
for (const ds of displaySessions) {
|
|
3012
3276
|
const prefix = ds.isActive ? ' ✓' : ' ';
|
|
3013
3277
|
const num = `${ds.index}.`;
|
|
3014
|
-
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
3015
3278
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
3016
3279
|
if (ds.fileMissing) {
|
|
3017
|
-
lines.push(`${prefix} ${num}
|
|
3280
|
+
lines.push(`${prefix} ${num} ❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
3018
3281
|
}
|
|
3019
3282
|
else {
|
|
3020
|
-
lines.push(`${prefix} ${num} ${
|
|
3283
|
+
lines.push(`${prefix} ${num} ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
3021
3284
|
}
|
|
3022
3285
|
}
|
|
3023
|
-
const hiddenCount = currentProjectSessions.length - displayIndex
|
|
3024
|
-
if (
|
|
3286
|
+
const hiddenCount = currentProjectSessions.length - displayIndex;
|
|
3287
|
+
if (hiddenCount > 0) {
|
|
3025
3288
|
const parts = [];
|
|
3026
3289
|
if (hiddenCount > 0)
|
|
3027
3290
|
parts.push(`${hiddenCount} 个更早的会话`);
|
|
3028
|
-
if (topicCount > 0)
|
|
3029
|
-
parts.push(`${topicCount} 个话题会话`);
|
|
3030
3291
|
lines.push(`\n (已隐藏 ${parts.join('、')})`);
|
|
3031
3292
|
}
|
|
3032
3293
|
lines.push('');
|
|
@@ -3055,12 +3316,7 @@ export class CommandHandler {
|
|
|
3055
3316
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
3056
3317
|
const idx = parseInt(sessionName, 10);
|
|
3057
3318
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
3058
|
-
const
|
|
3059
|
-
// 与 /slist 显示逻辑一致:超过10个时隐藏非活跃话题会话
|
|
3060
|
-
const hideTopics = projectSessions.length > 10;
|
|
3061
|
-
const visibleSessions = hideTopics
|
|
3062
|
-
? projectSessions.filter(s => !s.threadId)
|
|
3063
|
-
: projectSessions;
|
|
3319
|
+
const visibleSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
|
|
3064
3320
|
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
3065
3321
|
targetSession = visibleSessions[idx - 1];
|
|
3066
3322
|
}
|
|
@@ -3071,6 +3327,9 @@ export class CommandHandler {
|
|
|
3071
3327
|
if (!targetSession && sessionName.length >= 8) {
|
|
3072
3328
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
3073
3329
|
}
|
|
3330
|
+
if (targetSession?.threadId) {
|
|
3331
|
+
return { kind: 'command.error', text: `❌ 话题会话不支持通过 /s 切换\n请在对应话题内继续对话` };
|
|
3332
|
+
}
|
|
3074
3333
|
const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
|
|
3075
3334
|
if (!targetSession && sessionName.length >= 8 && canImport) {
|
|
3076
3335
|
const projectPaths = Object.values(this.projects);
|
|
@@ -3085,7 +3344,7 @@ export class CommandHandler {
|
|
|
3085
3344
|
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
|
|
3086
3345
|
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
3087
3346
|
const projectName = this.getProjectName(projectPath);
|
|
3088
|
-
return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史` };
|
|
3347
|
+
return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${displaySessionTitle(imported.name, '(未命名)')}\n 项目: ${projectName}\n 将继续之前的对话历史` };
|
|
3089
3348
|
}
|
|
3090
3349
|
}
|
|
3091
3350
|
}
|
|
@@ -3103,10 +3362,10 @@ export class CommandHandler {
|
|
|
3103
3362
|
}
|
|
3104
3363
|
if (source === 'card-trigger')
|
|
3105
3364
|
return null;
|
|
3106
|
-
return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name
|
|
3365
|
+
return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
|
|
3107
3366
|
}
|
|
3108
3367
|
if (targetSession.id === session.id) {
|
|
3109
|
-
return { kind: 'command.result', text: `当前已在会话: ${targetSession.name
|
|
3368
|
+
return { kind: 'command.result', text: `当前已在会话: ${displaySessionTitle(targetSession.name, sessionName)}` };
|
|
3110
3369
|
}
|
|
3111
3370
|
// 阻止从主会话切换到话题会话
|
|
3112
3371
|
if (!session.threadId && targetSession.threadId) {
|
|
@@ -3120,7 +3379,7 @@ export class CommandHandler {
|
|
|
3120
3379
|
const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
|
|
3121
3380
|
if (source === 'card-trigger')
|
|
3122
3381
|
return null;
|
|
3123
|
-
return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name
|
|
3382
|
+
return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}${continueHint}${lastInputLine}` };
|
|
3124
3383
|
}
|
|
3125
3384
|
// /rename 或 /name 命令:重命名当前会话
|
|
3126
3385
|
if (normalizedContent === '/rename' || normalizedContent === '/name') {
|
|
@@ -3142,8 +3401,14 @@ export class CommandHandler {
|
|
|
3142
3401
|
if (existing && existing.id !== session.id) {
|
|
3143
3402
|
return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
|
|
3144
3403
|
}
|
|
3145
|
-
const oldName = session.name
|
|
3404
|
+
const oldName = displaySessionTitle(session.name, '(未命名)');
|
|
3146
3405
|
const success = await this.sessionManager.renameSession(session.id, newName);
|
|
3406
|
+
if (success && session.agentSessionId) {
|
|
3407
|
+
const renameAgent = this.getAgent(channel, session.agentId);
|
|
3408
|
+
await renameAgent.setSessionName?.(session.agentSessionId, newName).catch(error => {
|
|
3409
|
+
logger.debug('[CommandHandler] Backend session rename sync failed:', error);
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3147
3412
|
if (!success) {
|
|
3148
3413
|
return { kind: 'command.error', text: `❌ 重命名失败` };
|
|
3149
3414
|
}
|
|
@@ -3167,11 +3432,7 @@ export class CommandHandler {
|
|
|
3167
3432
|
if (!targetSession && /^\d+$/.test(sessionName)) {
|
|
3168
3433
|
const idx = parseInt(sessionName, 10);
|
|
3169
3434
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
3170
|
-
const
|
|
3171
|
-
const hideTopics = projectSessions.length > 10;
|
|
3172
|
-
const visibleSessions = hideTopics
|
|
3173
|
-
? projectSessions.filter(s => !s.threadId)
|
|
3174
|
-
: projectSessions;
|
|
3435
|
+
const visibleSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
|
|
3175
3436
|
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
3176
3437
|
targetSession = visibleSessions[idx - 1];
|
|
3177
3438
|
}
|
|
@@ -3182,6 +3443,9 @@ export class CommandHandler {
|
|
|
3182
3443
|
if (!targetSession && sessionName.length >= 8) {
|
|
3183
3444
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
3184
3445
|
}
|
|
3446
|
+
if (targetSession?.threadId) {
|
|
3447
|
+
return { kind: 'command.error', text: `❌ 请使用话题管理删除话题会话` };
|
|
3448
|
+
}
|
|
3185
3449
|
if (!targetSession) {
|
|
3186
3450
|
return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
|
|
3187
3451
|
}
|
|
@@ -3195,7 +3459,7 @@ export class CommandHandler {
|
|
|
3195
3459
|
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
3196
3460
|
const targetAgent = this.getAgent(channel, targetSession.agentId);
|
|
3197
3461
|
await targetAgent.closeSession(targetSession.id);
|
|
3198
|
-
return { kind: 'command.result', text: `✓ 已删除会话: ${targetSession.name
|
|
3462
|
+
return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
|
|
3199
3463
|
}
|
|
3200
3464
|
// /fork 命令:分支当前会话
|
|
3201
3465
|
if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
|
|
@@ -3213,8 +3477,19 @@ export class CommandHandler {
|
|
|
3213
3477
|
try {
|
|
3214
3478
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
3215
3479
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
3480
|
+
await forkAgent.updateSessionMetadata?.(forkedSessionId, {
|
|
3481
|
+
gitInfo: {
|
|
3482
|
+
branch: null,
|
|
3483
|
+
commitHash: null,
|
|
3484
|
+
repositoryUrl: null,
|
|
3485
|
+
},
|
|
3486
|
+
evolclawSessionId: newSession.id,
|
|
3487
|
+
sourceSessionId: session.id,
|
|
3488
|
+
}).catch(error => {
|
|
3489
|
+
logger.debug('[CommandHandler] Backend fork metadata sync failed:', error);
|
|
3490
|
+
});
|
|
3216
3491
|
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
3217
|
-
return { kind: 'command.result', text: `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
|
|
3492
|
+
return { kind: 'command.result', text: `✅ 会话已分支: ${displaySessionTitle(newSession.name, '(未命名)')}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
|
|
3218
3493
|
}
|
|
3219
3494
|
catch (error) {
|
|
3220
3495
|
logger.error('[CommandHandler] Fork session failed:', error);
|
|
@@ -3228,14 +3503,11 @@ export class CommandHandler {
|
|
|
3228
3503
|
return { kind: 'command.error', text: result.error };
|
|
3229
3504
|
const { session } = result;
|
|
3230
3505
|
const rewindAgent = this.getAgent(channel, session.agentId);
|
|
3231
|
-
if (rewindAgent.name !== 'claude') {
|
|
3232
|
-
return { kind: 'command.error', text: '❌ /rewind 仅支持 Claude 后端' };
|
|
3233
|
-
}
|
|
3234
3506
|
if (!session.agentSessionId) {
|
|
3235
3507
|
return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
|
|
3236
3508
|
}
|
|
3237
3509
|
if (!rewindAgent.getSessionMessages) {
|
|
3238
|
-
return { kind: 'command.error', text:
|
|
3510
|
+
return { kind: 'command.error', text: `❌ 当前 Agent (${rewindAgent.name}) 不支持 /rewind` };
|
|
3239
3511
|
}
|
|
3240
3512
|
const args = normalizedContent.slice('/rewind'.length).trim();
|
|
3241
3513
|
if (!args) {
|
|
@@ -3379,7 +3651,7 @@ export class CommandHandler {
|
|
|
3379
3651
|
}
|
|
3380
3652
|
manager.moveToDone(trigger.id, 'cancelled');
|
|
3381
3653
|
scheduler.cancel(trigger.id);
|
|
3382
|
-
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: peerId });
|
|
3654
|
+
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, name: trigger.name, by: peerId });
|
|
3383
3655
|
return `✅ 触发器已取消:**${trigger.name}**`;
|
|
3384
3656
|
}
|
|
3385
3657
|
// /trigger update <name|id> [--参数...]
|
|
@@ -3410,6 +3682,38 @@ export class CommandHandler {
|
|
|
3410
3682
|
const now = Date.now();
|
|
3411
3683
|
patch.nextFireAt = calcNextFireAt(patch.scheduleType, patch.scheduleValue, now);
|
|
3412
3684
|
}
|
|
3685
|
+
// 跨渠道迁移:改了 targetChannel 时必须同步重算 targetChannelType,
|
|
3686
|
+
// 并按 session 策略重新绑定执行会话(与 set 路径保持一致,否则 trigger
|
|
3687
|
+
// 仍按旧 channelType 路由 / 仍绑在旧渠道的 boundSessionId 上)。
|
|
3688
|
+
const effectiveChannel = patch.targetChannel ?? trigger.targetChannel;
|
|
3689
|
+
const effectiveChannelId = patch.targetChannelId ?? trigger.targetChannelId;
|
|
3690
|
+
if (patch.targetChannel) {
|
|
3691
|
+
patch.targetChannelType = this.resolveChannelType(patch.targetChannel);
|
|
3692
|
+
}
|
|
3693
|
+
// 解析最终生效的 session 策略(patch 优先,否则沿用原 trigger)
|
|
3694
|
+
const effStrategy = patch.targetSessionStrategy ?? trigger.targetSessionStrategy;
|
|
3695
|
+
// 渠道或策略变化时,按策略重新绑定会话
|
|
3696
|
+
if (patch.targetChannel || patch.targetSessionStrategy) {
|
|
3697
|
+
if (effStrategy === 'current') {
|
|
3698
|
+
if (patch.targetChannel && patch.targetChannel !== trigger.targetChannel) {
|
|
3699
|
+
return '❌ 跨渠道不支持 --session current,请改用 latest 或 thread';
|
|
3700
|
+
}
|
|
3701
|
+
const active = await this.sessionManager.getActiveSession(effectiveChannel, effectiveChannelId);
|
|
3702
|
+
if (!active)
|
|
3703
|
+
return '❌ 目标渠道当前没有活跃会话,改用 --session latest 或先在该渠道发一条消息';
|
|
3704
|
+
patch.boundSessionId = active.id;
|
|
3705
|
+
}
|
|
3706
|
+
else if (effStrategy === 'thread') {
|
|
3707
|
+
const adapter = this.adapters.get(effectiveChannel);
|
|
3708
|
+
if (!adapter?.capabilities.thread)
|
|
3709
|
+
return '❌ 目标渠道不支持 thread 会话';
|
|
3710
|
+
}
|
|
3711
|
+
else {
|
|
3712
|
+
// latest 策略:清除旧的 boundSessionId(若有),按渠道动态取最新会话
|
|
3713
|
+
if (trigger.boundSessionId)
|
|
3714
|
+
patch.boundSessionId = undefined;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3413
3717
|
let updated;
|
|
3414
3718
|
try {
|
|
3415
3719
|
updated = manager.update(trigger.id, patch);
|
|
@@ -3450,15 +3754,26 @@ export class CommandHandler {
|
|
|
3450
3754
|
const nextFireAt = calcNextFireAt(parsed.scheduleType, parsed.scheduleValue, now);
|
|
3451
3755
|
// Auto-generate name if not provided
|
|
3452
3756
|
const name = parsed.name ?? `trigger-${Date.now().toString(36)}`;
|
|
3757
|
+
// Validate target channel exists
|
|
3758
|
+
const targetChannelName = parsed.targetChannel ?? channel;
|
|
3759
|
+
if (parsed.targetChannel && !this.adapters.has(parsed.targetChannel)) {
|
|
3760
|
+
return { ok: false, error: `目标渠道不存在或未启用:${parsed.targetChannel}` };
|
|
3761
|
+
}
|
|
3762
|
+
// Validate channelId format for AUN: must look like an AID (contains '.')
|
|
3763
|
+
const targetChannelType = this.resolveChannelType(targetChannelName);
|
|
3764
|
+
const targetChannelId = parsed.targetChannelId ?? channelId;
|
|
3765
|
+
if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChannelId.includes('.')) {
|
|
3766
|
+
return { ok: false, error: `AUN 渠道的 --channelid 必须是 AID 格式(如 user.agentid.pub),收到:"${parsed.targetChannelId}"` };
|
|
3767
|
+
}
|
|
3453
3768
|
const trigger = {
|
|
3454
3769
|
id: crypto.randomUUID(),
|
|
3455
3770
|
name,
|
|
3456
3771
|
scheduleType: parsed.scheduleType,
|
|
3457
3772
|
scheduleValue: parsed.scheduleValue,
|
|
3458
3773
|
nextFireAt,
|
|
3459
|
-
targetChannel:
|
|
3460
|
-
targetChannelId
|
|
3461
|
-
targetChannelType
|
|
3774
|
+
targetChannel: targetChannelName,
|
|
3775
|
+
targetChannelId,
|
|
3776
|
+
targetChannelType,
|
|
3462
3777
|
targetThreadId: parsed.targetThreadId,
|
|
3463
3778
|
targetSessionStrategy: parsed.targetSessionStrategy,
|
|
3464
3779
|
agentId: parsed.agentId,
|
|
@@ -3466,12 +3781,16 @@ export class CommandHandler {
|
|
|
3466
3781
|
createdByPeerId: peerId,
|
|
3467
3782
|
createdByChannel: channel,
|
|
3468
3783
|
fireCount: 0,
|
|
3784
|
+
failCount: 0,
|
|
3469
3785
|
createdAt: now,
|
|
3470
3786
|
updatedAt: now,
|
|
3471
3787
|
};
|
|
3472
3788
|
try {
|
|
3473
3789
|
// Strategy-based session binding
|
|
3474
3790
|
if (parsed.targetSessionStrategy === 'current') {
|
|
3791
|
+
if (parsed.targetChannel && parsed.targetChannel !== channel) {
|
|
3792
|
+
return { ok: false, error: '跨渠道不支持 --session current,请改用 latest 或 thread' };
|
|
3793
|
+
}
|
|
3475
3794
|
const active = await this.sessionManager.getActiveSession(channel, channelId);
|
|
3476
3795
|
if (!active)
|
|
3477
3796
|
return { ok: false, error: '当前没有活跃会话,改用 --session latest 或 thread' };
|
|
@@ -3556,12 +3875,26 @@ export class CommandHandler {
|
|
|
3556
3875
|
const detail = fileResult.filesChanged
|
|
3557
3876
|
? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
|
|
3558
3877
|
: '';
|
|
3559
|
-
|
|
3878
|
+
if (agent.capabilities?.fileRewind === 'git-head') {
|
|
3879
|
+
results.push(`✅ 已按 Git HEAD 恢复文件${detail}(Codex 当前不提供逐轮文件快照)`);
|
|
3880
|
+
}
|
|
3881
|
+
else {
|
|
3882
|
+
results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
|
|
3883
|
+
}
|
|
3560
3884
|
}
|
|
3561
3885
|
}
|
|
3562
|
-
//
|
|
3886
|
+
// 对话回退:Codex app-server 可直接 rollback;Claude 走 resumeAt 延迟到下次消息生效。
|
|
3563
3887
|
if (mode === 'chat' || mode === 'all') {
|
|
3564
|
-
|
|
3888
|
+
const discarded = turns.length - turnNum + 1;
|
|
3889
|
+
if (agent.rollbackSessionTurns) {
|
|
3890
|
+
const ok = await agent.rollbackSessionTurns(session.agentSessionId, session.projectPath, discarded);
|
|
3891
|
+
if (!ok)
|
|
3892
|
+
return '❌ 对话回退失败';
|
|
3893
|
+
const meta = { ...(session.metadata || {}) };
|
|
3894
|
+
delete meta.resumeAt;
|
|
3895
|
+
await this.sessionManager.updateSession(session.id, { metadata: meta });
|
|
3896
|
+
}
|
|
3897
|
+
else if (keepTarget) {
|
|
3565
3898
|
const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
|
|
3566
3899
|
await this.sessionManager.updateSession(session.id, { metadata: meta });
|
|
3567
3900
|
}
|
|
@@ -3574,10 +3907,6 @@ export class CommandHandler {
|
|
|
3574
3907
|
agentSessionId: null,
|
|
3575
3908
|
});
|
|
3576
3909
|
}
|
|
3577
|
-
const discarded = turns.length - turnNum + 1;
|
|
3578
|
-
const keepDesc = keepTarget
|
|
3579
|
-
? `回退到第 ${turnNum - 1} 轮:"${keepTarget.userContent}"`
|
|
3580
|
-
: '已清空全部对话历史';
|
|
3581
3910
|
results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
|
|
3582
3911
|
}
|
|
3583
3912
|
this.eventBus.publish({
|
|
@@ -3833,7 +4162,27 @@ export class CommandHandler {
|
|
|
3833
4162
|
else if (Array.isArray(m?.content)) {
|
|
3834
4163
|
text = m.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ');
|
|
3835
4164
|
}
|
|
3836
|
-
text = text.trim()
|
|
4165
|
+
text = text.trim();
|
|
4166
|
+
// Strip injection wrappers before previewing (outermost first):
|
|
4167
|
+
// 1. Interrupt wrapper: 【新消息插入】\n...\n【请无视之前中断继续处理】
|
|
4168
|
+
text = text.replace(/^【新消息插入】\s*/, '').replace(/\s*【请无视之前中断继续处理】$/, '').trim();
|
|
4169
|
+
// 2. Current format: ‹metadata›\ncontent (message-renderer item.md)
|
|
4170
|
+
if (text.startsWith('‹')) {
|
|
4171
|
+
const nl = text.indexOf('\n');
|
|
4172
|
+
if (nl !== -1)
|
|
4173
|
+
text = text.slice(nl + 1).trim();
|
|
4174
|
+
}
|
|
4175
|
+
// 3. Legacy XML format: <messages><message sender="..." time="...">content</message></messages>
|
|
4176
|
+
if (text.startsWith('<messages>')) {
|
|
4177
|
+
const parts = [];
|
|
4178
|
+
const re = /<message(?:\s[^>]*)?>([\s\S]*?)<\/message>/g;
|
|
4179
|
+
let match;
|
|
4180
|
+
while ((match = re.exec(text)) !== null)
|
|
4181
|
+
parts.push(match[1].trim());
|
|
4182
|
+
if (parts.length > 0)
|
|
4183
|
+
text = parts.join(' ');
|
|
4184
|
+
}
|
|
4185
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
3837
4186
|
if (!text)
|
|
3838
4187
|
return '';
|
|
3839
4188
|
return text.length > 50 ? text.substring(0, 50) + '…' : text;
|
|
@@ -3846,6 +4195,16 @@ export function isProcessLevelOwner(peerId, owners) {
|
|
|
3846
4195
|
return false;
|
|
3847
4196
|
return (owners ?? []).includes(peerId);
|
|
3848
4197
|
}
|
|
4198
|
+
/** ECWeb menu.response 错误信封。 */
|
|
4199
|
+
function ecwebErr(id, name, code, message) {
|
|
4200
|
+
return { type: 'menu.response', id, ...(name ? { name } : {}), error: { code, message } };
|
|
4201
|
+
}
|
|
4202
|
+
/** 把 execMenu* 的 {data}|{error} 结果转成 menu.response。 */
|
|
4203
|
+
function ecwebResp(id, name, result) {
|
|
4204
|
+
return 'error' in result
|
|
4205
|
+
? { type: 'menu.response', id, ...(name ? { name } : {}), error: { code: result.code ?? 'EXEC_FAILED', message: result.error } }
|
|
4206
|
+
: { type: 'menu.response', id, ...(name ? { name } : {}), data: result.data };
|
|
4207
|
+
}
|
|
3849
4208
|
/** 校验 menu 路径直传的 trigger 调度参数(绕过 parseTriggerSet 文本解析后必须自校验)。
|
|
3850
4209
|
* 返回错误字符串表示非法;返回 null 表示通过。
|
|
3851
4210
|
* 防止非法 scheduleType/scheduleValue 传到 calcNextFireAt 产出 NaN/throw,污染 scheduler heap。 */
|