evolclaw 3.1.11 → 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 +41 -0
- package/README.md +27 -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/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +538 -325
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +98 -151
- 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 +44 -13
- package/dist/cli/index.js +207 -46
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +192 -85
- 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 +48 -11
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +754 -172
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/evolagent-registry.js +4 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +215 -0
- package/dist/core/message/create-status.js +67 -0
- 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 +52 -22
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +336 -68
- package/dist/core/message/message-queue.js +15 -8
- 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 +40 -7
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- 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}/kit-renderer.js +5 -1
- package/dist/{agents → eck}/manifest-engine.js +127 -35
- package/dist/{agents → eck}/message-renderer.js +26 -1
- package/dist/index.js +185 -8
- package/dist/ipc.js +22 -0
- package/dist/paths.js +7 -3
- 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/message-fragments/item.md +1 -1
- 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/utils/channel-helpers.js +0 -46
|
@@ -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,8 +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, loadEvolclawConfig } from '../config-store.js';
|
|
17
|
+
import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './message/command-handler-agent-control.js';
|
|
18
|
+
import { displaySessionTitle } from './session/session-title.js';
|
|
16
19
|
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
17
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('|');
|
|
18
23
|
// ── CLI 透传(menu.action name=cli action=exec)─────────────────────────
|
|
19
24
|
// 经消息通道的远程命令执行(RCE):仅 owner、白名单内只读+配置命令、无 shell、超时+截断。
|
|
20
25
|
// command → '*'(全部子命令放行) | Set(允许的子命令)。
|
|
@@ -23,6 +28,7 @@ const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
|
|
|
23
28
|
const CLI_EXEC_WHITELIST = {
|
|
24
29
|
status: '*',
|
|
25
30
|
model: '*',
|
|
31
|
+
stats: '*',
|
|
26
32
|
agent: new Set(['list', 'show', 'get']),
|
|
27
33
|
aid: new Set(['list', 'show', 'lookup']),
|
|
28
34
|
storage: new Set(['ls', 'quota']),
|
|
@@ -131,8 +137,12 @@ function formatIdleTime(ms) {
|
|
|
131
137
|
return `${minutes}分钟前`;
|
|
132
138
|
return '刚刚';
|
|
133
139
|
}
|
|
140
|
+
function isAdminRole(role) {
|
|
141
|
+
return role === 'owner' || role === 'admin';
|
|
142
|
+
}
|
|
134
143
|
// 支持的命令列表
|
|
135
|
-
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'];
|
|
136
146
|
// 命令别名映射
|
|
137
147
|
const aliases = {
|
|
138
148
|
'/s': '/session',
|
|
@@ -460,6 +470,43 @@ export class CommandHandler {
|
|
|
460
470
|
accumulateErrors: () => true,
|
|
461
471
|
};
|
|
462
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
|
+
}
|
|
463
510
|
/**
|
|
464
511
|
* 返回结构化命令菜单(供 menu.query 使用)
|
|
465
512
|
* owner 看到全部命令,admin 看到管理级命令(不含 owner-only),guest 仅看到用户级命令
|
|
@@ -467,9 +514,16 @@ export class CommandHandler {
|
|
|
467
514
|
getMenuItems(role, chatType = 'private') {
|
|
468
515
|
const isOwner = role === 'owner';
|
|
469
516
|
const isAdmin = role === 'owner' || role === 'admin';
|
|
517
|
+
const canReadTopic = role !== 'anonymous';
|
|
470
518
|
const items = [];
|
|
471
519
|
if (!isAdmin && chatType === 'group') {
|
|
472
520
|
return [
|
|
521
|
+
...(canReadTopic ? [{
|
|
522
|
+
group: '话题管理',
|
|
523
|
+
commands: [
|
|
524
|
+
{ cmd: '/topic', label: '话题管理', desc: '查看当前聊天的话题会话', next: { type: 'select', dynamic: true } },
|
|
525
|
+
]
|
|
526
|
+
}] : []),
|
|
473
527
|
{
|
|
474
528
|
group: '其他',
|
|
475
529
|
commands: [
|
|
@@ -485,6 +539,7 @@ export class CommandHandler {
|
|
|
485
539
|
commands: [
|
|
486
540
|
{ cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
|
|
487
541
|
{ cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
|
|
542
|
+
...(canReadTopic ? [{ cmd: '/topic', label: '话题管理', desc: '查看与管理当前聊天的话题会话', next: { type: 'select', dynamic: true } }] : []),
|
|
488
543
|
{ cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
|
|
489
544
|
{ cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
|
|
490
545
|
...(isAdmin ? [
|
|
@@ -523,6 +578,7 @@ export class CommandHandler {
|
|
|
523
578
|
...(isOwner ? [
|
|
524
579
|
{ value: 'auto', label: '自动模式', desc: '根据风险等级自动决定是否审批' },
|
|
525
580
|
{ value: 'bypass', label: '免审批模式', desc: '跳过所有工具审批确认' },
|
|
581
|
+
{ value: 'readonly', label: '只读模式', desc: '允许读取和临时目录写入,拒绝项目文件修改' },
|
|
526
582
|
{ value: 'plan', label: '计划模式', desc: '仅允许只读操作,写操作需审批' },
|
|
527
583
|
{ value: 'edit', label: '编辑模式', desc: '允许文件编辑,其他操作需审批' },
|
|
528
584
|
{ value: 'request', label: '请求模式', desc: '所有操作均需审批' },
|
|
@@ -573,18 +629,63 @@ export class CommandHandler {
|
|
|
573
629
|
return items;
|
|
574
630
|
}
|
|
575
631
|
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
576
|
-
async getSubMenuItems(cmd, channel, channelId, userId) {
|
|
632
|
+
async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, _explicitChatType) {
|
|
577
633
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
634
|
+
// ── 进程级 /agent list(owners 鉴权) ──
|
|
635
|
+
if (cmd === '/agent') {
|
|
636
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
637
|
+
throw { code: 'FORBIDDEN', message: '操作需要 owner 权限' };
|
|
638
|
+
}
|
|
639
|
+
const res = await execAgentOptions(args);
|
|
640
|
+
if ('error' in res)
|
|
641
|
+
throw { code: res.code, message: res.error };
|
|
642
|
+
return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
|
|
643
|
+
}
|
|
644
|
+
// ── 关系级 /trigger list(每个 trigger 一个 MenuItem) ──
|
|
645
|
+
if (cmd === '/trigger') {
|
|
646
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
647
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
648
|
+
if (!manager)
|
|
649
|
+
return [];
|
|
650
|
+
const scope = args?.options === 'all' ? 'all' : 'enabled';
|
|
651
|
+
const role = (overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId)).role;
|
|
652
|
+
const isAdmin = role === 'owner' || role === 'admin';
|
|
653
|
+
const all = manager.listAll();
|
|
654
|
+
const list = scope === 'all' ? all.active.concat(all.history) : manager.listActive();
|
|
655
|
+
const visible = isAdmin ? list
|
|
656
|
+
: list.filter((t) => t.createdByPeerId === (userId ?? '') && t.createdByChannel === channel);
|
|
657
|
+
return visible.map((t) => ({
|
|
658
|
+
// 透传完整 trigger 字段(ECWeb Triggers 表逐列渲染需要)
|
|
659
|
+
...t,
|
|
660
|
+
value: t.id,
|
|
661
|
+
label: t.name,
|
|
662
|
+
desc: `${t.scheduleType}${t.nextFireAt ? ` | 下次 ${new Date(t.nextFireAt).toLocaleString()}` : ''}`,
|
|
663
|
+
// 状态标识:history 条目带 doneReason(fired/cancelled/expired),active 条目恒为 'active'
|
|
664
|
+
status: t.doneReason ?? 'active',
|
|
665
|
+
}));
|
|
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
|
+
}
|
|
578
677
|
if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
|
|
579
678
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
580
679
|
const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
|
|
581
680
|
const currentSession = session;
|
|
582
681
|
const items = sessions
|
|
682
|
+
.filter(s => !s.threadId)
|
|
583
683
|
.filter(s => !active || s.id !== active.id)
|
|
584
684
|
.map(s => {
|
|
685
|
+
const displayName = displaySessionTitle(s.name, s.id.slice(0, 8));
|
|
585
686
|
const item = {
|
|
586
687
|
value: s.name || s.id.slice(0, 8),
|
|
587
|
-
label:
|
|
688
|
+
label: displayName,
|
|
588
689
|
selected: currentSession ? s.id === currentSession.id : false,
|
|
589
690
|
};
|
|
590
691
|
if (s.agentSessionId) {
|
|
@@ -675,7 +776,7 @@ export class CommandHandler {
|
|
|
675
776
|
const permAgent = this.getAgent(channel, session?.agentId);
|
|
676
777
|
const validModes = hasPermissionController(permAgent)
|
|
677
778
|
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
678
|
-
: [
|
|
779
|
+
: [...PERMISSION_MODE_KEYS];
|
|
679
780
|
return validModes.map(m => ({ value: m, label: m, selected: m === currentMode }));
|
|
680
781
|
}
|
|
681
782
|
return null;
|
|
@@ -698,12 +799,18 @@ export class CommandHandler {
|
|
|
698
799
|
return s ? null : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
|
|
699
800
|
}
|
|
700
801
|
/** menu.query — 查询当前值。 */
|
|
701
|
-
async execMenuQuery(cmd, channel, channelId, userId) {
|
|
702
|
-
void userId;
|
|
802
|
+
async execMenuQuery(cmd, channel, channelId, userId, args, _explicitChatType) {
|
|
703
803
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
704
804
|
if (!cmdBase)
|
|
705
805
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
706
806
|
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
807
|
+
// ── 进程级 /agent(owners 鉴权) ──
|
|
808
|
+
if (cmdBase === '/agent') {
|
|
809
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
810
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
811
|
+
}
|
|
812
|
+
return await execAgentQuery(args);
|
|
813
|
+
}
|
|
707
814
|
if (cmdBase === '/pwd') {
|
|
708
815
|
const sessPath = session?.projectPath;
|
|
709
816
|
const fallbackPath = evolagent?.config?.projects?.defaultPath;
|
|
@@ -752,6 +859,54 @@ export class CommandHandler {
|
|
|
752
859
|
data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
|
|
753
860
|
return { data };
|
|
754
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
|
+
}
|
|
755
910
|
if (cmdBase === '/baseagent') {
|
|
756
911
|
const value = session?.agentId ?? evolagent?.config?.active_baseagent ?? null;
|
|
757
912
|
return { data: { baseagent: value } };
|
|
@@ -792,6 +947,9 @@ export class CommandHandler {
|
|
|
792
947
|
const fallback = evolagent?.config?.dispatch;
|
|
793
948
|
return { data: { mode: sessionMode ?? fallback ?? null } };
|
|
794
949
|
}
|
|
950
|
+
if (cmdBase === '/observable') {
|
|
951
|
+
return { data: { observable: evolagent?.getObservable() ?? false } };
|
|
952
|
+
}
|
|
795
953
|
if (cmdBase === '/perm') {
|
|
796
954
|
const need = this.requireSession(session);
|
|
797
955
|
if (need)
|
|
@@ -804,6 +962,9 @@ export class CommandHandler {
|
|
|
804
962
|
return { data: { mode: currentMode } };
|
|
805
963
|
}
|
|
806
964
|
if (cmdBase === '/system') {
|
|
965
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
966
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
967
|
+
}
|
|
807
968
|
const owningAgent = this.getOwningAgent(channel);
|
|
808
969
|
const data = {
|
|
809
970
|
agent: owningAgent?.name ?? 'DefaultAgent',
|
|
@@ -819,6 +980,13 @@ export class CommandHandler {
|
|
|
819
980
|
data.version = pkg.version;
|
|
820
981
|
}
|
|
821
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 { }
|
|
822
990
|
const channels = owningAgent?.channelInstanceNames?.() ?? [];
|
|
823
991
|
if (channels.length)
|
|
824
992
|
data.channels = channels;
|
|
@@ -827,7 +995,7 @@ export class CommandHandler {
|
|
|
827
995
|
return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
828
996
|
}
|
|
829
997
|
/** menu.update — 写入新值。 */
|
|
830
|
-
async execMenuUpdate(cmd, value, channel, channelId, userId) {
|
|
998
|
+
async execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity) {
|
|
831
999
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
832
1000
|
if (!cmdBase)
|
|
833
1001
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
@@ -835,7 +1003,57 @@ export class CommandHandler {
|
|
|
835
1003
|
if (!arg)
|
|
836
1004
|
return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
|
|
837
1005
|
const { session, evolagent } = await this.loadMenuContext(channel, channelId);
|
|
838
|
-
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1006
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
1007
|
+
// ── 关系级 /trigger update(调度参数,value 为 JSON 字符串) ──
|
|
1008
|
+
if (cmdBase === '/trigger') {
|
|
1009
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
1010
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
1011
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
1012
|
+
if (!manager || !scheduler)
|
|
1013
|
+
return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
|
|
1014
|
+
let patch;
|
|
1015
|
+
try {
|
|
1016
|
+
patch = JSON.parse(arg);
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
return { error: 'value 需为 JSON', code: 'INVALID_ARGS' };
|
|
1020
|
+
}
|
|
1021
|
+
if (!patch?.nameOrId)
|
|
1022
|
+
return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
|
|
1023
|
+
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
1024
|
+
if (!isAdmin && !userId)
|
|
1025
|
+
return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
|
|
1026
|
+
const trigger = isAdmin
|
|
1027
|
+
? (manager.getByName(patch.nameOrId) ?? manager.getById(patch.nameOrId))
|
|
1028
|
+
: (manager.getByNameScoped(patch.nameOrId, userId ?? '', channel) ?? manager.getByIdScoped(patch.nameOrId, userId ?? '', channel));
|
|
1029
|
+
if (!trigger)
|
|
1030
|
+
return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
|
|
1031
|
+
const fields = {};
|
|
1032
|
+
if (patch.scheduleType !== undefined)
|
|
1033
|
+
fields.scheduleType = patch.scheduleType;
|
|
1034
|
+
if (patch.scheduleValue !== undefined)
|
|
1035
|
+
fields.scheduleValue = String(patch.scheduleValue);
|
|
1036
|
+
if (patch.prompt !== undefined)
|
|
1037
|
+
fields.prompt = String(patch.prompt);
|
|
1038
|
+
// 调度参数变化时重算 nextFireAt——先校验避免 NaN 污染 scheduler heap
|
|
1039
|
+
if (fields.scheduleType !== undefined || fields.scheduleValue !== undefined) {
|
|
1040
|
+
const effType = fields.scheduleType ?? trigger.scheduleType;
|
|
1041
|
+
const effValue = fields.scheduleValue ?? trigger.scheduleValue;
|
|
1042
|
+
const schedErr = validateScheduleParams(effType, effValue);
|
|
1043
|
+
if (schedErr)
|
|
1044
|
+
return { error: schedErr, code: 'INVALID_ARGS' };
|
|
1045
|
+
fields.nextFireAt = calcNextFireAt(effType, effValue, Date.now());
|
|
1046
|
+
}
|
|
1047
|
+
let updated;
|
|
1048
|
+
try {
|
|
1049
|
+
updated = manager.update(trigger.id, fields);
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
return { error: `更新失败:${err?.message || err}`, code: 'INVALID_ARGS' };
|
|
1053
|
+
}
|
|
1054
|
+
scheduler.update(updated);
|
|
1055
|
+
return { data: { id: updated.id, nextFireAt: updated.nextFireAt } };
|
|
1056
|
+
}
|
|
839
1057
|
if (cmdBase === '/baseagent') {
|
|
840
1058
|
const valid = this.getAvailableBaseagents(channel);
|
|
841
1059
|
if (valid.length && !valid.includes(arg)) {
|
|
@@ -926,7 +1144,7 @@ export class CommandHandler {
|
|
|
926
1144
|
const permAgent = this.getAgent(channel, session.agentId);
|
|
927
1145
|
const validModes = hasPermissionController(permAgent)
|
|
928
1146
|
? permAgent.listModes().filter(m => m.available).map(m => m.key)
|
|
929
|
-
: [
|
|
1147
|
+
: [...PERMISSION_MODE_KEYS];
|
|
930
1148
|
if (!validModes.includes(arg))
|
|
931
1149
|
return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
|
|
932
1150
|
const metadata = { ...(session.metadata || {}), permissionMode: arg };
|
|
@@ -945,17 +1163,133 @@ export class CommandHandler {
|
|
|
945
1163
|
this.agentRegistry.setShowActivities(channel, newMode);
|
|
946
1164
|
return { data: { mode: newMode } };
|
|
947
1165
|
}
|
|
1166
|
+
if (cmdBase === '/observable') {
|
|
1167
|
+
if (identity.role !== 'owner')
|
|
1168
|
+
return { error: '观察者模式仅限 owner 开关', code: 'NO_PERMISSION' };
|
|
1169
|
+
if (arg !== 'true' && arg !== 'false')
|
|
1170
|
+
return { error: `无效值: ${arg},可选: true / false`, code: 'INVALID_VALUE' };
|
|
1171
|
+
if (!evolagent)
|
|
1172
|
+
return { error: '找不到通道所属 agent,无法持久化', code: 'EXEC_FAILED' };
|
|
1173
|
+
evolagent.setObservable(arg === 'true');
|
|
1174
|
+
return { data: { observable: arg === 'true' } };
|
|
1175
|
+
}
|
|
948
1176
|
return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
949
1177
|
}
|
|
950
1178
|
/** menu.action — 触发动词。 */
|
|
951
|
-
async execMenuAction(cmd, action, args, channel, channelId, userId) {
|
|
1179
|
+
async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType) {
|
|
952
1180
|
const cmdBase = cmd.trim().split(' ')[0];
|
|
953
1181
|
if (!cmdBase)
|
|
954
1182
|
return { error: '缺少命令', code: 'MISSING_CMD' };
|
|
955
1183
|
if (!action)
|
|
956
1184
|
return { error: '缺少 action', code: 'MISSING_VALUE' };
|
|
957
1185
|
const { session } = await this.loadMenuContext(channel, channelId);
|
|
958
|
-
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1186
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
1187
|
+
// ── 进程级 /agent(owners 鉴权,不依赖 session/channel) ──
|
|
1188
|
+
// NOTE(D5): 本次进程级 /agent 仅按 evolclaw.json owners 鉴权,任意 evolagent 的 AUN
|
|
1189
|
+
// channel 均可作为入口。part1(daemon 控制 AID)落地后,应叠加 isControlChannel(channelId)
|
|
1190
|
+
// 闸:仅控制 AID channel 上的 /agent /system 生效。见 part1 计划。
|
|
1191
|
+
if (cmdBase === '/agent') {
|
|
1192
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
1193
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
1194
|
+
}
|
|
1195
|
+
const a = { ...(args ?? {}) };
|
|
1196
|
+
if (action === 'create') {
|
|
1197
|
+
a.project = resolveProjectPath(a.project, a.aid ?? '', loadDefaults());
|
|
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
|
+
}
|
|
1212
|
+
return await execAgentAction(action, a, userId ?? '');
|
|
1213
|
+
}
|
|
1214
|
+
// ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
|
|
1215
|
+
if (cmdBase === '/trigger') {
|
|
1216
|
+
const role = identity.role;
|
|
1217
|
+
const isAdmin = role === 'owner' || role === 'admin';
|
|
1218
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
1219
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
1220
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
1221
|
+
if (!manager || !scheduler)
|
|
1222
|
+
return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
|
|
1223
|
+
if (action === 'set') {
|
|
1224
|
+
// args 结构化 → 直接组装 ParsedTriggerSet(绕过 parseTriggerSet 文本解析,无注入风险)
|
|
1225
|
+
if (!args?.scheduleType || !args?.scheduleValue || !args?.prompt) {
|
|
1226
|
+
return { error: '缺少必填参数:scheduleType / scheduleValue / prompt', code: 'INVALID_ARGS' };
|
|
1227
|
+
}
|
|
1228
|
+
// menu 路径绕过了 parseTriggerSet 的校验,必须自行校验枚举/数值,
|
|
1229
|
+
// 否则非法值会传到 calcNextFireAt 产出 NaN nextFireAt,污染 scheduler heap。
|
|
1230
|
+
const schedErr = validateScheduleParams(args.scheduleType, String(args.scheduleValue));
|
|
1231
|
+
if (schedErr)
|
|
1232
|
+
return { error: schedErr, code: 'INVALID_ARGS' };
|
|
1233
|
+
const strategy = args.targetSessionStrategy ?? 'latest';
|
|
1234
|
+
if (!['latest', 'current', 'thread'].includes(strategy)) {
|
|
1235
|
+
return { error: `无效 targetSessionStrategy: ${strategy}`, code: 'INVALID_ARGS' };
|
|
1236
|
+
}
|
|
1237
|
+
const parsed = {
|
|
1238
|
+
scheduleType: args.scheduleType,
|
|
1239
|
+
scheduleValue: String(args.scheduleValue),
|
|
1240
|
+
prompt: String(args.prompt),
|
|
1241
|
+
name: args.name,
|
|
1242
|
+
targetChannel: args.targetChannel,
|
|
1243
|
+
targetChannelId: args.targetChannelId,
|
|
1244
|
+
targetThreadId: args.targetThreadId,
|
|
1245
|
+
targetSessionStrategy: strategy,
|
|
1246
|
+
agentId: args.agentId,
|
|
1247
|
+
};
|
|
1248
|
+
const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined);
|
|
1249
|
+
if (!r.ok)
|
|
1250
|
+
return { error: r.error, code: /已存在|exists|重复/.test(r.error) ? 'CONFLICT' : 'INVALID_ARGS' };
|
|
1251
|
+
return { data: { id: r.trigger.id, name: r.trigger.name, nextFireAt: r.trigger.nextFireAt } };
|
|
1252
|
+
}
|
|
1253
|
+
if (action === 'cancel') {
|
|
1254
|
+
const nameOrId = args?.nameOrId;
|
|
1255
|
+
if (!nameOrId)
|
|
1256
|
+
return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
|
|
1257
|
+
if (!isAdmin && !userId)
|
|
1258
|
+
return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
|
|
1259
|
+
const trigger = isAdmin
|
|
1260
|
+
? (manager.getByName(nameOrId) ?? manager.getById(nameOrId))
|
|
1261
|
+
: (manager.getByNameScoped(nameOrId, userId ?? '', channel) ?? manager.getByIdScoped(nameOrId, userId ?? '', channel));
|
|
1262
|
+
if (!trigger)
|
|
1263
|
+
return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
|
|
1264
|
+
manager.moveToDone(trigger.id, 'cancelled');
|
|
1265
|
+
scheduler.cancel(trigger.id);
|
|
1266
|
+
this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, name: trigger.name, by: userId ?? '' });
|
|
1267
|
+
return { data: { id: trigger.id, cancelled: true } };
|
|
1268
|
+
}
|
|
1269
|
+
return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
|
|
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
|
+
}
|
|
959
1293
|
// ── /session 系列 ──
|
|
960
1294
|
if (cmdBase === '/session' || cmdBase === '/s') {
|
|
961
1295
|
if (action === 'stop') {
|
|
@@ -1011,9 +1345,11 @@ export class CommandHandler {
|
|
|
1011
1345
|
}
|
|
1012
1346
|
// ── /system 系列 ──
|
|
1013
1347
|
if (cmdBase === '/system') {
|
|
1348
|
+
// D1 迁移:进程级鉴权统一查 evolclaw.json owners,替代各 action 内联的 identity.role 判断
|
|
1349
|
+
if (!isProcessLevelOwner(userId, loadEvolclawConfig().owners)) {
|
|
1350
|
+
return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
|
|
1351
|
+
}
|
|
1014
1352
|
if (action === 'restart') {
|
|
1015
|
-
if (identity.role !== 'owner')
|
|
1016
|
-
return { error: '无权限:服务重启仅限 owner 使用', code: 'NO_PERMISSION' };
|
|
1017
1353
|
const restartInfo = { channel, channelId, timestamp: Date.now() };
|
|
1018
1354
|
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
1019
1355
|
const { spawn } = await import('child_process');
|
|
@@ -1027,12 +1363,37 @@ export class CommandHandler {
|
|
|
1027
1363
|
return { data: { action: 'restart', success: true } };
|
|
1028
1364
|
}
|
|
1029
1365
|
if (action === 'check') {
|
|
1030
|
-
|
|
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;
|
|
1031
1371
|
}
|
|
1032
1372
|
if (action === 'upgrade') {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
+
};
|
|
1036
1397
|
}
|
|
1037
1398
|
return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
|
|
1038
1399
|
}
|
|
@@ -1057,6 +1418,79 @@ export class CommandHandler {
|
|
|
1057
1418
|
}
|
|
1058
1419
|
return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
|
|
1059
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
|
+
}
|
|
1060
1494
|
/**
|
|
1061
1495
|
* CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
|
|
1062
1496
|
* 不 in-process 调用(CLI handler 用 console.log + process.exit,spawn 行为与终端一致且隔离)。
|
|
@@ -1129,7 +1563,7 @@ export class CommandHandler {
|
|
|
1129
1563
|
/** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
|
|
1130
1564
|
async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
|
|
1131
1565
|
try {
|
|
1132
|
-
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);
|
|
1133
1567
|
if (result == null) {
|
|
1134
1568
|
// null / undefined: 命令未识别或前置守卫拦截(如 idle 检查),视为失败
|
|
1135
1569
|
return { error: '命令未执行(可能被前置守卫拦截)', code: 'EXEC_FAILED' };
|
|
@@ -1144,6 +1578,8 @@ export class CommandHandler {
|
|
|
1144
1578
|
const data = { action, success: true };
|
|
1145
1579
|
if (payload.text)
|
|
1146
1580
|
data.message = payload.text;
|
|
1581
|
+
if (payload.structured)
|
|
1582
|
+
data.structured = payload.structured;
|
|
1147
1583
|
// 对于切换/创建类动作,附加切换后的活跃 session 信息便于客户端继续操作
|
|
1148
1584
|
if (opts.enrichSession) {
|
|
1149
1585
|
const newSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
@@ -1169,13 +1605,13 @@ export class CommandHandler {
|
|
|
1169
1605
|
const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID);
|
|
1170
1606
|
return result;
|
|
1171
1607
|
}
|
|
1172
|
-
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) {
|
|
1173
1609
|
// 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
|
|
1174
1610
|
// 不应覆盖 session 中已有的正确值
|
|
1175
1611
|
if (source === 'card-trigger')
|
|
1176
1612
|
chatType = undefined;
|
|
1177
1613
|
// 解析身份(按实例名)
|
|
1178
|
-
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
1614
|
+
const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
|
|
1179
1615
|
const policy = this.getPolicy(channel);
|
|
1180
1616
|
// 按当前会话选择 agent 后端
|
|
1181
1617
|
const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
@@ -1228,12 +1664,12 @@ export class CommandHandler {
|
|
|
1228
1664
|
}
|
|
1229
1665
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
1230
1666
|
// 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
|
|
1231
|
-
// - 始终需要 idle(无参即写):/
|
|
1667
|
+
// - 始终需要 idle(无参即写):/compact /repair /fork /new
|
|
1232
1668
|
// - 仅带参时需要 idle(无参是列表/用法):/session /baseagent /rewind
|
|
1233
1669
|
// - /chatmode:在 handler 内部自行做写操作的 idle 检查
|
|
1234
1670
|
// - /dispatch:在 handler 内部自行做写操作的 idle 检查
|
|
1235
1671
|
// - /safe:已禁用 no-op,不再要求 idle
|
|
1236
|
-
const idleAlways = ['/
|
|
1672
|
+
const idleAlways = ['/compact', '/repair', '/fork', '/new'];
|
|
1237
1673
|
const idleWhenArg = ['/session', '/baseagent', '/rewind'];
|
|
1238
1674
|
const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
|
|
1239
1675
|
idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
|
|
@@ -1261,7 +1697,8 @@ export class CommandHandler {
|
|
|
1261
1697
|
// 检查是否以 / 开头(可能是命令)
|
|
1262
1698
|
if (normalizedContent.startsWith('/')) {
|
|
1263
1699
|
const inputCmd = normalizedContent.split(' ')[0];
|
|
1264
|
-
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 + ' '));
|
|
1265
1702
|
if (!isValidCommand) {
|
|
1266
1703
|
const similar = commands.find(cmd => {
|
|
1267
1704
|
const distance = levenshteinDistance(inputCmd, cmd);
|
|
@@ -1275,7 +1712,8 @@ export class CommandHandler {
|
|
|
1275
1712
|
}
|
|
1276
1713
|
}
|
|
1277
1714
|
}
|
|
1278
|
-
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 + ' '));
|
|
1279
1717
|
if (!isCmd)
|
|
1280
1718
|
return undefined;
|
|
1281
1719
|
// /help 命令不需要会话
|
|
@@ -1336,7 +1774,7 @@ export class CommandHandler {
|
|
|
1336
1774
|
'',
|
|
1337
1775
|
'🔐 权限管理:',
|
|
1338
1776
|
' /perm - 查看当前权限模式',
|
|
1339
|
-
...(isOwner ? [
|
|
1777
|
+
...(isOwner ? [` /perm <${PERMISSION_MODE_USAGE}> - 切换权限模式`] : []),
|
|
1340
1778
|
' /perm allow|always|deny - 审批权限请求',
|
|
1341
1779
|
'',
|
|
1342
1780
|
'🛠️ 运维:',
|
|
@@ -1383,7 +1821,7 @@ export class CommandHandler {
|
|
|
1383
1821
|
}
|
|
1384
1822
|
// 权限管理
|
|
1385
1823
|
if (isAdmin) {
|
|
1386
|
-
cmds.push({ command: '/perm', args: isOwner ?
|
|
1824
|
+
cmds.push({ command: '/perm', args: isOwner ? `<${PERMISSION_MODE_USAGE}>` : undefined, description: '查看当前权限模式', category: '权限管理', roles: ['admin', 'owner'] });
|
|
1387
1825
|
cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
|
|
1388
1826
|
}
|
|
1389
1827
|
// 运维
|
|
@@ -1515,11 +1953,11 @@ export class CommandHandler {
|
|
|
1515
1953
|
}
|
|
1516
1954
|
}
|
|
1517
1955
|
// 不是已知模式名也不是 allow/deny
|
|
1518
|
-
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;
|
|
1519
1957
|
return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny` };
|
|
1520
1958
|
}
|
|
1521
1959
|
// 双参数不再支持,提示正确用法
|
|
1522
|
-
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;
|
|
1523
1961
|
return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny` };
|
|
1524
1962
|
}
|
|
1525
1963
|
// /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
|
|
@@ -1689,7 +2127,7 @@ export class CommandHandler {
|
|
|
1689
2127
|
const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
|
|
1690
2128
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
1691
2129
|
const projectName = this.getProjectName(session.projectPath);
|
|
1692
|
-
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name
|
|
2130
|
+
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${displaySessionTitle(newSession.name, '(未命名)')}\n ${hasExistingSession}`;
|
|
1693
2131
|
if (source === 'card-trigger')
|
|
1694
2132
|
return null;
|
|
1695
2133
|
return { kind: 'command.result', text: agentSwitchResponse };
|
|
@@ -2177,7 +2615,8 @@ export class CommandHandler {
|
|
|
2177
2615
|
const sessionKey = stopSession.id;
|
|
2178
2616
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
2179
2617
|
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
2180
|
-
|
|
2618
|
+
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
2619
|
+
if (queueLength === 0 && !hasActive && !isProcessing) {
|
|
2181
2620
|
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
2182
2621
|
}
|
|
2183
2622
|
await stopAgent.interrupt(sessionKey);
|
|
@@ -2192,37 +2631,10 @@ export class CommandHandler {
|
|
|
2192
2631
|
this.sessionManager.clearProcessing(sessionKey);
|
|
2193
2632
|
return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
|
|
2194
2633
|
}
|
|
2195
|
-
// /clear
|
|
2634
|
+
// /clear 已移除:Claude/Codex/Gemini 对“清空当前 backend 历史”的语义不一致。
|
|
2635
|
+
// 统一使用 /new 创建新会话来开始全新上下文。
|
|
2196
2636
|
if (normalizedContent === '/clear') {
|
|
2197
|
-
|
|
2198
|
-
if ('error' in result)
|
|
2199
|
-
return { kind: 'command.error', text: result.error };
|
|
2200
|
-
const { session } = result;
|
|
2201
|
-
const sessionAgent = this.getAgent(channel, session.agentId);
|
|
2202
|
-
if (!sessionAgent.capabilities?.clear) {
|
|
2203
|
-
return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代` };
|
|
2204
|
-
}
|
|
2205
|
-
if (!session.agentSessionId) {
|
|
2206
|
-
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需清空' };
|
|
2207
|
-
}
|
|
2208
|
-
const projectPath = path.isAbsolute(session.projectPath)
|
|
2209
|
-
? session.projectPath
|
|
2210
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
2211
|
-
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
2212
|
-
try {
|
|
2213
|
-
const cleared = await sessionAgent.clearSession(session.id, session.agentSessionId, projectPath);
|
|
2214
|
-
if (cleared) {
|
|
2215
|
-
await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
|
|
2216
|
-
sessionAgent.updateSessionId(session.id, '');
|
|
2217
|
-
return { kind: 'command.result', text: '✅ 已清空当前会话的对话历史' };
|
|
2218
|
-
}
|
|
2219
|
-
else {
|
|
2220
|
-
return { kind: 'command.error', text: '❌ 清空会话失败,请稍后重试' };
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
finally {
|
|
2224
|
-
releaseLock();
|
|
2225
|
-
}
|
|
2637
|
+
return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
|
|
2226
2638
|
}
|
|
2227
2639
|
// /compact 命令:手动压缩会话上下文
|
|
2228
2640
|
if (normalizedContent === '/compact') {
|
|
@@ -2247,7 +2659,10 @@ export class CommandHandler {
|
|
|
2247
2659
|
}
|
|
2248
2660
|
const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
|
|
2249
2661
|
if (compacted) {
|
|
2250
|
-
return {
|
|
2662
|
+
return {
|
|
2663
|
+
kind: 'command.result',
|
|
2664
|
+
text: '✅ 会话压缩完成',
|
|
2665
|
+
};
|
|
2251
2666
|
}
|
|
2252
2667
|
else {
|
|
2253
2668
|
return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
|
|
@@ -2316,7 +2731,7 @@ export class CommandHandler {
|
|
|
2316
2731
|
const chatModeLine = `会话模式: ${sessionMode}`;
|
|
2317
2732
|
const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
|
|
2318
2733
|
if (isAdmin) {
|
|
2319
|
-
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}`);
|
|
2320
2735
|
if (health.consecutiveErrors > 0) {
|
|
2321
2736
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
2322
2737
|
}
|
|
@@ -2342,6 +2757,9 @@ export class CommandHandler {
|
|
|
2342
2757
|
}
|
|
2343
2758
|
}
|
|
2344
2759
|
const projectPath = this.getEffectiveDefaultPath(channel);
|
|
2760
|
+
if (sendMessage && session) {
|
|
2761
|
+
await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
|
|
2762
|
+
}
|
|
2345
2763
|
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.primaryRunnerKey);
|
|
2346
2764
|
this.eventBus.publish({
|
|
2347
2765
|
type: 'session:created',
|
|
@@ -2364,12 +2782,16 @@ export class CommandHandler {
|
|
|
2364
2782
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
2365
2783
|
const subCmd = normalizedContent.slice('/check'.length).trim();
|
|
2366
2784
|
// 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
|
|
2367
|
-
//
|
|
2785
|
+
// __ecweb__ 是 ECWeb 系统级入口,展示全量渠道
|
|
2368
2786
|
const checkOwningAgent = this.getOwningAgent(channel);
|
|
2369
2787
|
let allowedChannels;
|
|
2370
2788
|
if (checkOwningAgent) {
|
|
2371
2789
|
allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
|
|
2372
2790
|
}
|
|
2791
|
+
else if (channel === '__ecweb__') {
|
|
2792
|
+
// ECWeb 全局视图:展示所有渠道
|
|
2793
|
+
allowedChannels = new Set(this.adapters.keys());
|
|
2794
|
+
}
|
|
2373
2795
|
else {
|
|
2374
2796
|
// default 范围:不再有 default channel 概念,等价于"所有 channel"
|
|
2375
2797
|
const defaultNames = [];
|
|
@@ -2457,7 +2879,29 @@ export class CommandHandler {
|
|
|
2457
2879
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
2458
2880
|
}
|
|
2459
2881
|
}
|
|
2460
|
-
|
|
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 };
|
|
2461
2905
|
}
|
|
2462
2906
|
// /restart 命令:重启服务(owner only)
|
|
2463
2907
|
if (normalizedContent === '/restart') {
|
|
@@ -2698,7 +3142,7 @@ export class CommandHandler {
|
|
|
2698
3142
|
}
|
|
2699
3143
|
const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
|
|
2700
3144
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2701
|
-
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);
|
|
2702
3146
|
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
2703
3147
|
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
|
|
2704
3148
|
if (orphanCliSessions.length === 0) {
|
|
@@ -2748,7 +3192,7 @@ export class CommandHandler {
|
|
|
2748
3192
|
}
|
|
2749
3193
|
// /slist — 仅显示 EvolClaw 会话
|
|
2750
3194
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2751
|
-
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);
|
|
2752
3196
|
// 从 SDK 同步会话名称(发现 CLI 改名)
|
|
2753
3197
|
try {
|
|
2754
3198
|
const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
|
|
@@ -2766,20 +3210,16 @@ export class CommandHandler {
|
|
|
2766
3210
|
logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
|
|
2767
3211
|
}
|
|
2768
3212
|
// 构建可显示会话列表(复用于卡片和文本)
|
|
2769
|
-
const hideTopics = currentProjectSessions.length > 10;
|
|
2770
|
-
const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
|
|
2771
3213
|
const maxDisplay = 10;
|
|
2772
3214
|
const displaySessions = [];
|
|
2773
3215
|
let displayIndex = 0;
|
|
2774
3216
|
for (let i = 0; i < currentProjectSessions.length; i++) {
|
|
2775
3217
|
const s = currentProjectSessions[i];
|
|
2776
|
-
if (hideTopics && s.threadId)
|
|
2777
|
-
continue;
|
|
2778
3218
|
if (displayIndex >= maxDisplay)
|
|
2779
3219
|
break;
|
|
2780
3220
|
const isActive = s.metadata?.isActive === true;
|
|
2781
3221
|
displayIndex++;
|
|
2782
|
-
const name = s.name
|
|
3222
|
+
const name = displaySessionTitle(s.name, '(未命名)');
|
|
2783
3223
|
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
2784
3224
|
const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
|
|
2785
3225
|
let status = '[空闲]';
|
|
@@ -2798,10 +3238,9 @@ export class CommandHandler {
|
|
|
2798
3238
|
if (this.interactionRouter && displaySessions.length >= 1) {
|
|
2799
3239
|
const bodyLines = displaySessions.map(ds => {
|
|
2800
3240
|
const prefix = ds.isActive ? '✓' : '•';
|
|
2801
|
-
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
2802
3241
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
2803
3242
|
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
2804
|
-
return `${prefix} ${ds.index}. ${
|
|
3243
|
+
return `${prefix} ${ds.index}. ${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
|
|
2805
3244
|
});
|
|
2806
3245
|
const interaction = {
|
|
2807
3246
|
type: 'interaction',
|
|
@@ -2836,22 +3275,19 @@ export class CommandHandler {
|
|
|
2836
3275
|
for (const ds of displaySessions) {
|
|
2837
3276
|
const prefix = ds.isActive ? ' ✓' : ' ';
|
|
2838
3277
|
const num = `${ds.index}.`;
|
|
2839
|
-
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
2840
3278
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
2841
3279
|
if (ds.fileMissing) {
|
|
2842
|
-
lines.push(`${prefix} ${num}
|
|
3280
|
+
lines.push(`${prefix} ${num} ❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
2843
3281
|
}
|
|
2844
3282
|
else {
|
|
2845
|
-
lines.push(`${prefix} ${num} ${
|
|
3283
|
+
lines.push(`${prefix} ${num} ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
2846
3284
|
}
|
|
2847
3285
|
}
|
|
2848
|
-
const hiddenCount = currentProjectSessions.length - displayIndex
|
|
2849
|
-
if (
|
|
3286
|
+
const hiddenCount = currentProjectSessions.length - displayIndex;
|
|
3287
|
+
if (hiddenCount > 0) {
|
|
2850
3288
|
const parts = [];
|
|
2851
3289
|
if (hiddenCount > 0)
|
|
2852
3290
|
parts.push(`${hiddenCount} 个更早的会话`);
|
|
2853
|
-
if (topicCount > 0)
|
|
2854
|
-
parts.push(`${topicCount} 个话题会话`);
|
|
2855
3291
|
lines.push(`\n (已隐藏 ${parts.join('、')})`);
|
|
2856
3292
|
}
|
|
2857
3293
|
lines.push('');
|
|
@@ -2880,12 +3316,7 @@ export class CommandHandler {
|
|
|
2880
3316
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
2881
3317
|
const idx = parseInt(sessionName, 10);
|
|
2882
3318
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2883
|
-
const
|
|
2884
|
-
// 与 /slist 显示逻辑一致:超过10个时隐藏非活跃话题会话
|
|
2885
|
-
const hideTopics = projectSessions.length > 10;
|
|
2886
|
-
const visibleSessions = hideTopics
|
|
2887
|
-
? projectSessions.filter(s => !s.threadId)
|
|
2888
|
-
: projectSessions;
|
|
3319
|
+
const visibleSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
|
|
2889
3320
|
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
2890
3321
|
targetSession = visibleSessions[idx - 1];
|
|
2891
3322
|
}
|
|
@@ -2896,6 +3327,9 @@ export class CommandHandler {
|
|
|
2896
3327
|
if (!targetSession && sessionName.length >= 8) {
|
|
2897
3328
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
2898
3329
|
}
|
|
3330
|
+
if (targetSession?.threadId) {
|
|
3331
|
+
return { kind: 'command.error', text: `❌ 话题会话不支持通过 /s 切换\n请在对应话题内继续对话` };
|
|
3332
|
+
}
|
|
2899
3333
|
const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
|
|
2900
3334
|
if (!targetSession && sessionName.length >= 8 && canImport) {
|
|
2901
3335
|
const projectPaths = Object.values(this.projects);
|
|
@@ -2910,7 +3344,7 @@ export class CommandHandler {
|
|
|
2910
3344
|
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
|
|
2911
3345
|
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
2912
3346
|
const projectName = this.getProjectName(projectPath);
|
|
2913
|
-
return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史` };
|
|
3347
|
+
return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${displaySessionTitle(imported.name, '(未命名)')}\n 项目: ${projectName}\n 将继续之前的对话历史` };
|
|
2914
3348
|
}
|
|
2915
3349
|
}
|
|
2916
3350
|
}
|
|
@@ -2928,10 +3362,10 @@ export class CommandHandler {
|
|
|
2928
3362
|
}
|
|
2929
3363
|
if (source === 'card-trigger')
|
|
2930
3364
|
return null;
|
|
2931
|
-
return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name
|
|
3365
|
+
return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
|
|
2932
3366
|
}
|
|
2933
3367
|
if (targetSession.id === session.id) {
|
|
2934
|
-
return { kind: 'command.result', text: `当前已在会话: ${targetSession.name
|
|
3368
|
+
return { kind: 'command.result', text: `当前已在会话: ${displaySessionTitle(targetSession.name, sessionName)}` };
|
|
2935
3369
|
}
|
|
2936
3370
|
// 阻止从主会话切换到话题会话
|
|
2937
3371
|
if (!session.threadId && targetSession.threadId) {
|
|
@@ -2945,7 +3379,7 @@ export class CommandHandler {
|
|
|
2945
3379
|
const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
|
|
2946
3380
|
if (source === 'card-trigger')
|
|
2947
3381
|
return null;
|
|
2948
|
-
return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name
|
|
3382
|
+
return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}${continueHint}${lastInputLine}` };
|
|
2949
3383
|
}
|
|
2950
3384
|
// /rename 或 /name 命令:重命名当前会话
|
|
2951
3385
|
if (normalizedContent === '/rename' || normalizedContent === '/name') {
|
|
@@ -2967,8 +3401,14 @@ export class CommandHandler {
|
|
|
2967
3401
|
if (existing && existing.id !== session.id) {
|
|
2968
3402
|
return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
|
|
2969
3403
|
}
|
|
2970
|
-
const oldName = session.name
|
|
3404
|
+
const oldName = displaySessionTitle(session.name, '(未命名)');
|
|
2971
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
|
+
}
|
|
2972
3412
|
if (!success) {
|
|
2973
3413
|
return { kind: 'command.error', text: `❌ 重命名失败` };
|
|
2974
3414
|
}
|
|
@@ -2992,11 +3432,7 @@ export class CommandHandler {
|
|
|
2992
3432
|
if (!targetSession && /^\d+$/.test(sessionName)) {
|
|
2993
3433
|
const idx = parseInt(sessionName, 10);
|
|
2994
3434
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
2995
|
-
const
|
|
2996
|
-
const hideTopics = projectSessions.length > 10;
|
|
2997
|
-
const visibleSessions = hideTopics
|
|
2998
|
-
? projectSessions.filter(s => !s.threadId)
|
|
2999
|
-
: projectSessions;
|
|
3435
|
+
const visibleSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
|
|
3000
3436
|
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
3001
3437
|
targetSession = visibleSessions[idx - 1];
|
|
3002
3438
|
}
|
|
@@ -3007,6 +3443,9 @@ export class CommandHandler {
|
|
|
3007
3443
|
if (!targetSession && sessionName.length >= 8) {
|
|
3008
3444
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
3009
3445
|
}
|
|
3446
|
+
if (targetSession?.threadId) {
|
|
3447
|
+
return { kind: 'command.error', text: `❌ 请使用话题管理删除话题会话` };
|
|
3448
|
+
}
|
|
3010
3449
|
if (!targetSession) {
|
|
3011
3450
|
return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
|
|
3012
3451
|
}
|
|
@@ -3020,7 +3459,7 @@ export class CommandHandler {
|
|
|
3020
3459
|
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
3021
3460
|
const targetAgent = this.getAgent(channel, targetSession.agentId);
|
|
3022
3461
|
await targetAgent.closeSession(targetSession.id);
|
|
3023
|
-
return { kind: 'command.result', text: `✓ 已删除会话: ${targetSession.name
|
|
3462
|
+
return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
|
|
3024
3463
|
}
|
|
3025
3464
|
// /fork 命令:分支当前会话
|
|
3026
3465
|
if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
|
|
@@ -3038,8 +3477,19 @@ export class CommandHandler {
|
|
|
3038
3477
|
try {
|
|
3039
3478
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
3040
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
|
+
});
|
|
3041
3491
|
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
3042
|
-
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 <名称> 切换回原会话` };
|
|
3043
3493
|
}
|
|
3044
3494
|
catch (error) {
|
|
3045
3495
|
logger.error('[CommandHandler] Fork session failed:', error);
|
|
@@ -3053,14 +3503,11 @@ export class CommandHandler {
|
|
|
3053
3503
|
return { kind: 'command.error', text: result.error };
|
|
3054
3504
|
const { session } = result;
|
|
3055
3505
|
const rewindAgent = this.getAgent(channel, session.agentId);
|
|
3056
|
-
if (rewindAgent.name !== 'claude') {
|
|
3057
|
-
return { kind: 'command.error', text: '❌ /rewind 仅支持 Claude 后端' };
|
|
3058
|
-
}
|
|
3059
3506
|
if (!session.agentSessionId) {
|
|
3060
3507
|
return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
|
|
3061
3508
|
}
|
|
3062
3509
|
if (!rewindAgent.getSessionMessages) {
|
|
3063
|
-
return { kind: 'command.error', text:
|
|
3510
|
+
return { kind: 'command.error', text: `❌ 当前 Agent (${rewindAgent.name}) 不支持 /rewind` };
|
|
3064
3511
|
}
|
|
3065
3512
|
const args = normalizedContent.slice('/rewind'.length).trim();
|
|
3066
3513
|
if (!args) {
|
|
@@ -3204,7 +3651,7 @@ export class CommandHandler {
|
|
|
3204
3651
|
}
|
|
3205
3652
|
manager.moveToDone(trigger.id, 'cancelled');
|
|
3206
3653
|
scheduler.cancel(trigger.id);
|
|
3207
|
-
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 });
|
|
3208
3655
|
return `✅ 触发器已取消:**${trigger.name}**`;
|
|
3209
3656
|
}
|
|
3210
3657
|
// /trigger update <name|id> [--参数...]
|
|
@@ -3235,6 +3682,38 @@ export class CommandHandler {
|
|
|
3235
3682
|
const now = Date.now();
|
|
3236
3683
|
patch.nextFireAt = calcNextFireAt(patch.scheduleType, patch.scheduleValue, now);
|
|
3237
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
|
+
}
|
|
3238
3717
|
let updated;
|
|
3239
3718
|
try {
|
|
3240
3719
|
updated = manager.update(trigger.id, patch);
|
|
@@ -3254,70 +3733,98 @@ export class CommandHandler {
|
|
|
3254
3733
|
const result = parseTriggerSet(args);
|
|
3255
3734
|
if (!result.ok)
|
|
3256
3735
|
return `❌ ${result.error}`;
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3736
|
+
const reg = await this.registerTriggerFromParsed(result.value, channel, channelId, peerId, messageId);
|
|
3737
|
+
if (!reg.ok)
|
|
3738
|
+
return `❌ ${reg.error}`;
|
|
3739
|
+
const nextStr = new Date(reg.trigger.nextFireAt).toLocaleString();
|
|
3740
|
+
return `✅ 触发器已注册:**${reg.trigger.name}**\n下次触发:${nextStr}`;
|
|
3741
|
+
}
|
|
3742
|
+
return `❌ 未知子命令。用法:\n/trigger — 查看活跃触发器\n/trigger list — 查看所有触发器\n/trigger set <参数> — 注册触发器\n/trigger update <名称|ID> <参数> — 修改触发器\n/trigger cancel <名称> — 取消触发器`;
|
|
3743
|
+
}
|
|
3744
|
+
/** 从已解析的 trigger 参数组装 Trigger 并注册。文本路径(handleTrigger)与 menu 路径共用。
|
|
3745
|
+
* parsed 形状 = parseTriggerSet 的 result.value(ParsedTriggerSet)。
|
|
3746
|
+
* 失败 return { ok:false, error };成功 return { ok:true, trigger }。本方法不改变原文本路径行为。 */
|
|
3747
|
+
async registerTriggerFromParsed(parsed, channel, channelId, peerId, messageId) {
|
|
3748
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
3749
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
3750
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
3751
|
+
if (!manager || !scheduler)
|
|
3752
|
+
return { ok: false, error: '触发器功能未启用' };
|
|
3753
|
+
const now = Date.now();
|
|
3754
|
+
const nextFireAt = calcNextFireAt(parsed.scheduleType, parsed.scheduleValue, now);
|
|
3755
|
+
// Auto-generate name if not provided
|
|
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
|
+
}
|
|
3768
|
+
const trigger = {
|
|
3769
|
+
id: crypto.randomUUID(),
|
|
3770
|
+
name,
|
|
3771
|
+
scheduleType: parsed.scheduleType,
|
|
3772
|
+
scheduleValue: parsed.scheduleValue,
|
|
3773
|
+
nextFireAt,
|
|
3774
|
+
targetChannel: targetChannelName,
|
|
3775
|
+
targetChannelId,
|
|
3776
|
+
targetChannelType,
|
|
3777
|
+
targetThreadId: parsed.targetThreadId,
|
|
3778
|
+
targetSessionStrategy: parsed.targetSessionStrategy,
|
|
3779
|
+
agentId: parsed.agentId,
|
|
3780
|
+
prompt: parsed.prompt,
|
|
3781
|
+
createdByPeerId: peerId,
|
|
3782
|
+
createdByChannel: channel,
|
|
3783
|
+
fireCount: 0,
|
|
3784
|
+
failCount: 0,
|
|
3785
|
+
createdAt: now,
|
|
3786
|
+
updatedAt: now,
|
|
3787
|
+
};
|
|
3788
|
+
try {
|
|
3789
|
+
// Strategy-based session binding
|
|
3790
|
+
if (parsed.targetSessionStrategy === 'current') {
|
|
3791
|
+
if (parsed.targetChannel && parsed.targetChannel !== channel) {
|
|
3792
|
+
return { ok: false, error: '跨渠道不支持 --session current,请改用 latest 或 thread' };
|
|
3288
3793
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3794
|
+
const active = await this.sessionManager.getActiveSession(channel, channelId);
|
|
3795
|
+
if (!active)
|
|
3796
|
+
return { ok: false, error: '当前没有活跃会话,改用 --session latest 或 thread' };
|
|
3797
|
+
trigger.boundSessionId = active.id;
|
|
3798
|
+
}
|
|
3799
|
+
else if (parsed.targetSessionStrategy === 'thread') {
|
|
3800
|
+
const targetAdapterName = parsed.targetChannel ?? channel;
|
|
3801
|
+
const adapter = this.adapters.get(targetAdapterName);
|
|
3802
|
+
if (!adapter?.capabilities.thread)
|
|
3803
|
+
return { ok: false, error: '目标渠道不支持 thread 会话' };
|
|
3804
|
+
const channelType = adapter.channelKey.split('#')[0];
|
|
3805
|
+
trigger.targetChannelType = channelType;
|
|
3806
|
+
if (channelType === 'aun') {
|
|
3807
|
+
trigger.threadKind = 'aun';
|
|
3808
|
+
trigger.targetThreadId = `trigger-${trigger.id}`;
|
|
3809
|
+
}
|
|
3810
|
+
else {
|
|
3811
|
+
if (!messageId)
|
|
3812
|
+
return { ok: false, error: '飞书 thread 模式需要消息 ID,请重新发送命令' };
|
|
3813
|
+
trigger.threadKind = 'feishu';
|
|
3814
|
+
trigger.rootMessageId = messageId;
|
|
3815
|
+
trigger.pendingThread = true;
|
|
3307
3816
|
}
|
|
3308
|
-
// Validate name uniqueness before persisting (manager.register writes to disk)
|
|
3309
|
-
// scheduler.register is in-memory only and cannot fail, so order is safe here.
|
|
3310
|
-
// If manager.register throws (duplicate name/ID), nothing is persisted.
|
|
3311
|
-
manager.register(trigger);
|
|
3312
|
-
scheduler.register(trigger);
|
|
3313
|
-
}
|
|
3314
|
-
catch (err) {
|
|
3315
|
-
return `❌ 注册失败:${err.message}`;
|
|
3316
3817
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3818
|
+
// Validate name uniqueness before persisting (manager.register writes to disk)
|
|
3819
|
+
// scheduler.register is in-memory only and cannot fail, so order is safe here.
|
|
3820
|
+
// If manager.register throws (duplicate name/ID), nothing is persisted.
|
|
3821
|
+
manager.register(trigger);
|
|
3822
|
+
scheduler.register(trigger);
|
|
3319
3823
|
}
|
|
3320
|
-
|
|
3824
|
+
catch (err) {
|
|
3825
|
+
return { ok: false, error: `注册失败:${err.message}` };
|
|
3826
|
+
}
|
|
3827
|
+
return { ok: true, trigger };
|
|
3321
3828
|
}
|
|
3322
3829
|
// ── /rewind helpers ──
|
|
3323
3830
|
async handleRewindList(session, agent) {
|
|
@@ -3368,12 +3875,26 @@ export class CommandHandler {
|
|
|
3368
3875
|
const detail = fileResult.filesChanged
|
|
3369
3876
|
? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
|
|
3370
3877
|
: '';
|
|
3371
|
-
|
|
3878
|
+
if (agent.capabilities?.fileRewind === 'git-head') {
|
|
3879
|
+
results.push(`✅ 已按 Git HEAD 恢复文件${detail}(Codex 当前不提供逐轮文件快照)`);
|
|
3880
|
+
}
|
|
3881
|
+
else {
|
|
3882
|
+
results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
|
|
3883
|
+
}
|
|
3372
3884
|
}
|
|
3373
3885
|
}
|
|
3374
|
-
//
|
|
3886
|
+
// 对话回退:Codex app-server 可直接 rollback;Claude 走 resumeAt 延迟到下次消息生效。
|
|
3375
3887
|
if (mode === 'chat' || mode === 'all') {
|
|
3376
|
-
|
|
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) {
|
|
3377
3898
|
const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
|
|
3378
3899
|
await this.sessionManager.updateSession(session.id, { metadata: meta });
|
|
3379
3900
|
}
|
|
@@ -3386,10 +3907,6 @@ export class CommandHandler {
|
|
|
3386
3907
|
agentSessionId: null,
|
|
3387
3908
|
});
|
|
3388
3909
|
}
|
|
3389
|
-
const discarded = turns.length - turnNum + 1;
|
|
3390
|
-
const keepDesc = keepTarget
|
|
3391
|
-
? `回退到第 ${turnNum - 1} 轮:"${keepTarget.userContent}"`
|
|
3392
|
-
: '已清空全部对话历史';
|
|
3393
3910
|
results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
|
|
3394
3911
|
}
|
|
3395
3912
|
this.eventBus.publish({
|
|
@@ -3645,9 +4162,74 @@ export class CommandHandler {
|
|
|
3645
4162
|
else if (Array.isArray(m?.content)) {
|
|
3646
4163
|
text = m.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ');
|
|
3647
4164
|
}
|
|
3648
|
-
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();
|
|
3649
4186
|
if (!text)
|
|
3650
4187
|
return '';
|
|
3651
4188
|
return text.length > 50 ? text.substring(0, 50) + '…' : text;
|
|
3652
4189
|
}
|
|
3653
4190
|
}
|
|
4191
|
+
/** 进程级 menu 操作(/agent、/system)鉴权:发送方 AID 必须在 owners 名单中。
|
|
4192
|
+
* owners 来自 evolclaw.json 顶层(进程级控制面配置)。纯静态名单比对。 */
|
|
4193
|
+
export function isProcessLevelOwner(peerId, owners) {
|
|
4194
|
+
if (!peerId)
|
|
4195
|
+
return false;
|
|
4196
|
+
return (owners ?? []).includes(peerId);
|
|
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
|
+
}
|
|
4208
|
+
/** 校验 menu 路径直传的 trigger 调度参数(绕过 parseTriggerSet 文本解析后必须自校验)。
|
|
4209
|
+
* 返回错误字符串表示非法;返回 null 表示通过。
|
|
4210
|
+
* 防止非法 scheduleType/scheduleValue 传到 calcNextFireAt 产出 NaN/throw,污染 scheduler heap。 */
|
|
4211
|
+
export function validateScheduleParams(scheduleType, scheduleValue) {
|
|
4212
|
+
if (!['delay', 'at', 'cron'].includes(scheduleType)) {
|
|
4213
|
+
return `无效 scheduleType: ${scheduleType}(可选: delay / at / cron)`;
|
|
4214
|
+
}
|
|
4215
|
+
if (scheduleType === 'delay') {
|
|
4216
|
+
const ms = Number(scheduleValue);
|
|
4217
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
4218
|
+
return `delay 的 scheduleValue 需为正整数毫秒: ${scheduleValue}`;
|
|
4219
|
+
}
|
|
4220
|
+
else if (scheduleType === 'at') {
|
|
4221
|
+
const ts = new Date(scheduleValue).getTime();
|
|
4222
|
+
if (!Number.isFinite(ts))
|
|
4223
|
+
return `at 的 scheduleValue 需为合法时间: ${scheduleValue}`;
|
|
4224
|
+
}
|
|
4225
|
+
else {
|
|
4226
|
+
// cron:交给 calcNextFireAt 内部的 CronExpressionParser 校验(会 throw,被上层 catch)
|
|
4227
|
+
try {
|
|
4228
|
+
calcNextFireAt('cron', scheduleValue, Date.now());
|
|
4229
|
+
}
|
|
4230
|
+
catch {
|
|
4231
|
+
return `无效 cron 表达式: ${scheduleValue}`;
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
return null;
|
|
4235
|
+
}
|