evolclaw 2.2.0 → 2.4.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/README.md +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +283 -95
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +232 -57
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +803 -247
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +217 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
- package/dist/index.js +140 -57
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
|
|
2
2
|
import { saveConfig, resolvePaths, getPackageRoot, getOwner } from '../config.js';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
|
+
import crypto from 'crypto';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import fs from 'fs';
|
|
6
7
|
import os from 'os';
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const allEfforts = ['low', 'medium', 'high', 'max'];
|
|
9
|
+
const nonMaxEfforts = allEfforts.filter(e => e !== 'max');
|
|
10
|
+
function getAvailableEfforts(agent, model) {
|
|
11
|
+
if (agent.name === 'claude') {
|
|
12
|
+
if (model.includes('opus'))
|
|
13
|
+
return allEfforts;
|
|
14
|
+
return nonMaxEfforts;
|
|
15
|
+
}
|
|
16
|
+
if (agent.name === 'codex') {
|
|
17
|
+
return nonMaxEfforts;
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
function formatModelUsage(agent, model) {
|
|
22
|
+
const efforts = getAvailableEfforts(agent, model);
|
|
23
|
+
const lines = [
|
|
24
|
+
'用法:',
|
|
25
|
+
' /model <model> 切换模型',
|
|
26
|
+
];
|
|
27
|
+
if (efforts.length > 0) {
|
|
28
|
+
lines.push(' /model <model> <effort> 切换模型+推理强度');
|
|
29
|
+
lines.push(' /effort [level] 查看或切换推理强度');
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
13
32
|
}
|
|
14
33
|
/**
|
|
15
34
|
* 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
|
|
@@ -84,7 +103,7 @@ function formatIdleTime(ms) {
|
|
|
84
103
|
return '刚刚';
|
|
85
104
|
}
|
|
86
105
|
// 支持的命令列表
|
|
87
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check'];
|
|
106
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check'];
|
|
88
107
|
// 命令别名映射
|
|
89
108
|
const aliases = {
|
|
90
109
|
'/p': '/project',
|
|
@@ -92,7 +111,7 @@ const aliases = {
|
|
|
92
111
|
'/name': '/rename'
|
|
93
112
|
};
|
|
94
113
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
95
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name '];
|
|
114
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name '];
|
|
96
115
|
export class CommandHandler {
|
|
97
116
|
sessionManager;
|
|
98
117
|
config;
|
|
@@ -101,9 +120,11 @@ export class CommandHandler {
|
|
|
101
120
|
adapters = new Map();
|
|
102
121
|
policies = new Map();
|
|
103
122
|
channelObjects = new Map(); // name → actual channel instance (for /check)
|
|
123
|
+
channelTypeMap = new Map(); // name → channelType (for grouping)
|
|
104
124
|
processor;
|
|
105
125
|
messageQueue;
|
|
106
126
|
permissionGateway;
|
|
127
|
+
interactionRouter;
|
|
107
128
|
statsCollector;
|
|
108
129
|
agentMap;
|
|
109
130
|
defaultAgentId;
|
|
@@ -145,12 +166,16 @@ export class CommandHandler {
|
|
|
145
166
|
const d = Math.floor(sec / 86400);
|
|
146
167
|
const h = Math.floor((sec % 86400) / 3600);
|
|
147
168
|
const m = Math.floor((sec % 3600) / 60);
|
|
169
|
+
const s = sec % 60;
|
|
148
170
|
const parts = [];
|
|
149
171
|
if (d > 0)
|
|
150
172
|
parts.push(`${d}天`);
|
|
151
173
|
if (h > 0)
|
|
152
174
|
parts.push(`${h}时`);
|
|
153
|
-
|
|
175
|
+
if (m > 0)
|
|
176
|
+
parts.push(`${m}分`);
|
|
177
|
+
if (parts.length === 0)
|
|
178
|
+
parts.push(`${s}秒`);
|
|
154
179
|
return parts.join('');
|
|
155
180
|
}
|
|
156
181
|
/** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
|
|
@@ -162,6 +187,69 @@ export class CommandHandler {
|
|
|
162
187
|
getReplyContext(session) {
|
|
163
188
|
return session.metadata?.replyContext;
|
|
164
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* 尝试通过渠道适配器发送交互卡片。
|
|
192
|
+
* 返回 message_id 表示卡片已发送,false 表示降级为文本。
|
|
193
|
+
*/
|
|
194
|
+
async trySendInteraction(channel, channelId, interaction, replyContext) {
|
|
195
|
+
const adapter = this.adapters.get(channel);
|
|
196
|
+
if (!adapter?.sendInteraction)
|
|
197
|
+
return false;
|
|
198
|
+
try {
|
|
199
|
+
return await adapter.sendInteraction(channelId, interaction, replyContext);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
logger.warn(`[CommandHandler] sendInteraction failed: ${e}`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** 作废某 session 下所有 pending 交互卡片(PATCH 禁用 + cancel) */
|
|
207
|
+
async invalidateOldCards(channel, sessionId) {
|
|
208
|
+
if (!this.interactionRouter)
|
|
209
|
+
return;
|
|
210
|
+
const adapter = this.adapters.get(channel);
|
|
211
|
+
const pending = this.interactionRouter.getPending(sessionId);
|
|
212
|
+
if (pending.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
const disabledCard = {
|
|
215
|
+
config: { wide_screen_mode: true },
|
|
216
|
+
header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
|
|
217
|
+
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
218
|
+
};
|
|
219
|
+
for (const id of pending) {
|
|
220
|
+
const msgId = this.interactionRouter.getMessageId(id);
|
|
221
|
+
if (msgId && adapter?.patchInteractionCard) {
|
|
222
|
+
adapter.patchInteractionCard(msgId, disabledCard).catch(() => { });
|
|
223
|
+
}
|
|
224
|
+
this.interactionRouter.cancel(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 发送交互卡片并注册回调。作废旧卡片 → 发送新卡片 → 注册到 interactionRouter。
|
|
229
|
+
* 返回 true 表示卡片已发送(调用方应 return null),false 表示降级到文本。
|
|
230
|
+
*/
|
|
231
|
+
async sendInteractionCard(opts) {
|
|
232
|
+
if (!this.interactionRouter)
|
|
233
|
+
return false;
|
|
234
|
+
await this.invalidateOldCards(opts.channel, opts.sessionId);
|
|
235
|
+
const messageId = await this.trySendInteraction(opts.channel, opts.channelId, opts.interaction, opts.replyCtx);
|
|
236
|
+
if (!messageId)
|
|
237
|
+
return false;
|
|
238
|
+
const wrappedCallback = async (action, values, operatorId) => {
|
|
239
|
+
await opts.callback(action, values, operatorId);
|
|
240
|
+
const adapter = this.adapters.get(opts.channel);
|
|
241
|
+
if (adapter?.patchInteractionCard) {
|
|
242
|
+
const disabledCard = {
|
|
243
|
+
config: { wide_screen_mode: true },
|
|
244
|
+
header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
|
|
245
|
+
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
246
|
+
};
|
|
247
|
+
adapter.patchInteractionCard(messageId, disabledCard).catch(() => { });
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
165
253
|
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
166
254
|
async ensureSession(channel, channelId, threadId) {
|
|
167
255
|
if (threadId) {
|
|
@@ -187,20 +275,37 @@ export class CommandHandler {
|
|
|
187
275
|
setPermissionGateway(gateway) {
|
|
188
276
|
this.permissionGateway = gateway;
|
|
189
277
|
}
|
|
278
|
+
setInteractionRouter(router) {
|
|
279
|
+
this.interactionRouter = router;
|
|
280
|
+
}
|
|
190
281
|
setStatsCollector(collector) {
|
|
191
282
|
this.statsCollector = collector;
|
|
192
283
|
}
|
|
193
284
|
registerAdapter(adapter) {
|
|
194
|
-
this.adapters.set(adapter.
|
|
285
|
+
this.adapters.set(adapter.channelName, adapter);
|
|
195
286
|
}
|
|
196
|
-
registerChannel(name, channel) {
|
|
287
|
+
registerChannel(name, channel, channelType) {
|
|
197
288
|
this.channelObjects.set(name, channel);
|
|
289
|
+
if (channelType)
|
|
290
|
+
this.channelTypeMap.set(name, channelType);
|
|
291
|
+
}
|
|
292
|
+
/** 将实例名解析为渠道类型(用于 session 查询) */
|
|
293
|
+
resolveChannelType(channelName) {
|
|
294
|
+
return this.channelTypeMap.get(channelName) || channelName;
|
|
198
295
|
}
|
|
199
296
|
registerPolicy(channelName, policy) {
|
|
200
297
|
this.policies.set(channelName, policy);
|
|
201
298
|
}
|
|
202
299
|
getAdapter(channelName) {
|
|
203
|
-
|
|
300
|
+
// 先按实例名查找,再按 channelType 查找
|
|
301
|
+
let adapter = this.adapters.get(channelName);
|
|
302
|
+
if (adapter)
|
|
303
|
+
return adapter;
|
|
304
|
+
for (const [name, a] of this.adapters) {
|
|
305
|
+
if ((this.channelTypeMap.get(name) || name) === channelName)
|
|
306
|
+
return a;
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
204
309
|
}
|
|
205
310
|
getPolicy(channel) {
|
|
206
311
|
return this.policies.get(channel) || {
|
|
@@ -219,15 +324,25 @@ export class CommandHandler {
|
|
|
219
324
|
* 返回结构化命令菜单(供 menu.query 使用)
|
|
220
325
|
* admin 看到全部命令分组,guest 仅看到用户级命令
|
|
221
326
|
*/
|
|
222
|
-
getMenuItems(isAdmin) {
|
|
327
|
+
getMenuItems(isAdmin, chatType = 'private') {
|
|
223
328
|
const items = [];
|
|
329
|
+
if (!isAdmin && chatType === 'group') {
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
group: '其他',
|
|
333
|
+
commands: [
|
|
334
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
335
|
+
{ cmd: '/help', label: '显示帮助信息' },
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
];
|
|
339
|
+
}
|
|
224
340
|
if (isAdmin) {
|
|
225
341
|
items.push({
|
|
226
342
|
group: '项目管理',
|
|
227
343
|
commands: [
|
|
228
344
|
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
229
|
-
{ cmd: '/
|
|
230
|
-
{ cmd: '/p', args: '<name|path>', label: '切换项目' },
|
|
345
|
+
{ cmd: '/p', args: '[name|path]', label: '列出或切换项目' },
|
|
231
346
|
{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
|
|
232
347
|
]
|
|
233
348
|
});
|
|
@@ -235,14 +350,12 @@ export class CommandHandler {
|
|
|
235
350
|
items.push({
|
|
236
351
|
group: '会话管理',
|
|
237
352
|
commands: [
|
|
238
|
-
{ cmd: '/new', args: '[name]', label: '
|
|
239
|
-
{ cmd: '/
|
|
240
|
-
{ cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
|
|
353
|
+
{ cmd: '/new', args: '[name]', label: '创建新会话(清空历史请用此命令)' },
|
|
354
|
+
{ cmd: '/s', args: '[cli|name|index|uuid]', label: '列出或切换会话(cli 查看未导入的 CLI 会话)' },
|
|
241
355
|
{ cmd: '/name', args: '<name>', label: '重命名当前会话' },
|
|
242
356
|
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
243
357
|
...(isAdmin ? [
|
|
244
358
|
{ cmd: '/fork', args: '[name]', label: '分支当前会话' },
|
|
245
|
-
{ cmd: '/clear', label: '清空会话对话历史' },
|
|
246
359
|
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
247
360
|
] : []),
|
|
248
361
|
]
|
|
@@ -252,13 +365,14 @@ export class CommandHandler {
|
|
|
252
365
|
group: 'Agent 与模型',
|
|
253
366
|
commands: [
|
|
254
367
|
{ cmd: '/agent', args: '[name]', label: '查看或切换 Agent 后端' },
|
|
255
|
-
{ cmd: '/model', args: '[model]
|
|
368
|
+
{ cmd: '/model', args: '[model]', label: '查看或切换模型' },
|
|
369
|
+
{ cmd: '/effort', args: '[level]', label: '查看或切换推理强度' },
|
|
256
370
|
]
|
|
257
371
|
});
|
|
258
372
|
items.push({
|
|
259
373
|
group: '权限管理',
|
|
260
374
|
commands: [
|
|
261
|
-
{ cmd: '/perm', args: '[mode|allow|deny]', label: '权限模式管理' },
|
|
375
|
+
{ cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
|
|
262
376
|
]
|
|
263
377
|
});
|
|
264
378
|
items.push({
|
|
@@ -267,9 +381,7 @@ export class CommandHandler {
|
|
|
267
381
|
{ cmd: '/status', label: '显示会话状态' },
|
|
268
382
|
{ cmd: '/stop', label: '中断当前任务' },
|
|
269
383
|
{ cmd: '/restart', label: '重启服务' },
|
|
270
|
-
{ cmd: '/
|
|
271
|
-
{ cmd: '/safe', label: '进入安全模式' },
|
|
272
|
-
{ cmd: '/send', args: '[渠道] <path>', label: '发送项目内文件' },
|
|
384
|
+
{ cmd: '/send', args: '[channel] <path>', label: '发送项目内文件' },
|
|
273
385
|
{ cmd: '/check', args: '[rty <channel>]', label: '检查渠道状态或重连指定渠道' },
|
|
274
386
|
]
|
|
275
387
|
});
|
|
@@ -300,7 +412,7 @@ export class CommandHandler {
|
|
|
300
412
|
* 主命令处理入口
|
|
301
413
|
*/
|
|
302
414
|
async handle(content, channel, channelId, sendMessage, userId, threadId) {
|
|
303
|
-
//
|
|
415
|
+
// 解析身份(按实例名)
|
|
304
416
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
305
417
|
const policy = this.getPolicy(channel);
|
|
306
418
|
// 按当前会话选择 agent 后端
|
|
@@ -327,15 +439,19 @@ export class CommandHandler {
|
|
|
327
439
|
}
|
|
328
440
|
// 权限检查:区分用户级命令和管理级命令
|
|
329
441
|
const isAdmin = identity.role === 'owner';
|
|
442
|
+
const activeChatType = activeSession?.chatType || 'private';
|
|
330
443
|
if (normalizedContent.startsWith('/')) {
|
|
331
|
-
const
|
|
444
|
+
const guestGroupCommands = ['/status', '/help'];
|
|
445
|
+
const userCommands = activeChatType === 'group' && !isAdmin
|
|
446
|
+
? guestGroupCommands
|
|
447
|
+
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
332
448
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
333
449
|
if (!isUserCommand && !isAdmin) {
|
|
334
|
-
return '❌
|
|
450
|
+
return '❌ 无权限:当前群聊仅支持 /status 和 /help';
|
|
335
451
|
}
|
|
336
452
|
}
|
|
337
453
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
338
|
-
const requiresIdle = ['/new', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind'];
|
|
454
|
+
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
|
|
339
455
|
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
340
456
|
if (threadId) {
|
|
341
457
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
@@ -370,18 +486,27 @@ export class CommandHandler {
|
|
|
370
486
|
}
|
|
371
487
|
const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd));
|
|
372
488
|
if (!isCmd)
|
|
373
|
-
return
|
|
489
|
+
return undefined;
|
|
374
490
|
// /help 命令不需要会话
|
|
375
491
|
if (normalizedContent === '/help') {
|
|
492
|
+
if (!isAdmin && activeChatType === 'group') {
|
|
493
|
+
const lines = [
|
|
494
|
+
'可用命令:',
|
|
495
|
+
'',
|
|
496
|
+
'其他:',
|
|
497
|
+
' /status - 显示会话状态',
|
|
498
|
+
' /help - 显示此帮助信息',
|
|
499
|
+
];
|
|
500
|
+
return lines.join('\n');
|
|
501
|
+
}
|
|
376
502
|
if (!isAdmin) {
|
|
377
503
|
const lines = [
|
|
378
504
|
'可用命令:',
|
|
379
505
|
'',
|
|
380
506
|
'🔄 会话管理:',
|
|
381
|
-
' /new [名称] -
|
|
382
|
-
' /
|
|
383
|
-
' /
|
|
384
|
-
' /name, /rename <新名称> - 重命名当前会话',
|
|
507
|
+
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
508
|
+
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
509
|
+
' /name <新名称> - 重命名当前会话',
|
|
385
510
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
386
511
|
' /status - 显示会话状态',
|
|
387
512
|
'',
|
|
@@ -395,36 +520,32 @@ export class CommandHandler {
|
|
|
395
520
|
'',
|
|
396
521
|
'📁 项目管理:',
|
|
397
522
|
' /pwd - 显示当前项目路径',
|
|
398
|
-
' /
|
|
399
|
-
' /p, /project <name|path> - 切换项目',
|
|
523
|
+
' /p [name|path] - 列出或切换项目',
|
|
400
524
|
' /bind <path> - 绑定新项目目录',
|
|
401
525
|
'',
|
|
402
526
|
'🔄 会话管理:',
|
|
403
|
-
' /new [名称] -
|
|
404
|
-
' /
|
|
405
|
-
' /
|
|
406
|
-
' /name, /rename <新名称> - 重命名当前会话',
|
|
527
|
+
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
528
|
+
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
529
|
+
' /name <新名称> - 重命名当前会话',
|
|
407
530
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
408
531
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
409
|
-
' /clear - 清空当前会话的对话历史',
|
|
410
532
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
411
533
|
'',
|
|
412
534
|
'🤖 Agent 与模型:',
|
|
413
535
|
' /agent [name] - 查看或切换 Agent 后端',
|
|
414
|
-
' /model [model]
|
|
536
|
+
' /model [model] - 查看或切换模型',
|
|
537
|
+
' /effort [level] - 查看或切换推理强度',
|
|
415
538
|
'',
|
|
416
539
|
'🔐 权限管理:',
|
|
417
540
|
' /perm - 查看当前权限模式',
|
|
418
|
-
' /perm <
|
|
419
|
-
' /perm allow|deny - 审批权限请求',
|
|
541
|
+
' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
|
|
542
|
+
' /perm allow|always|deny - 审批权限请求',
|
|
420
543
|
'',
|
|
421
544
|
'🛠️ 运维:',
|
|
422
545
|
' /status - 显示会话状态',
|
|
423
546
|
' /stop - 中断当前任务',
|
|
424
547
|
' /restart - 重启服务',
|
|
425
|
-
' /
|
|
426
|
-
' /safe - 进入安全模式',
|
|
427
|
-
' /send [渠道] <路径> - 发送项目内文件',
|
|
548
|
+
' /send [channel] <path> - 发送项目内文件',
|
|
428
549
|
'',
|
|
429
550
|
'❓ 帮助:',
|
|
430
551
|
' /help - 显示此帮助信息',
|
|
@@ -445,21 +566,61 @@ export class CommandHandler {
|
|
|
445
566
|
if (!hasPermissionController(permAgent)) {
|
|
446
567
|
return '❌ 权限控制不可用';
|
|
447
568
|
}
|
|
448
|
-
const
|
|
569
|
+
const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'readonly';
|
|
570
|
+
const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
|
|
449
571
|
const modes = permAgent.listModes();
|
|
572
|
+
// 尝试发送交互卡片
|
|
573
|
+
if (this.interactionRouter) {
|
|
574
|
+
const requestId = `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
575
|
+
const availableModes = modes.filter(m => m.available);
|
|
576
|
+
const interaction = {
|
|
577
|
+
type: 'interaction',
|
|
578
|
+
id: requestId,
|
|
579
|
+
channelId,
|
|
580
|
+
sessionId: permSession.id,
|
|
581
|
+
kind: {
|
|
582
|
+
kind: 'action',
|
|
583
|
+
title: '🔐 权限模式',
|
|
584
|
+
body: availableModes.map(m => `${m.key === currentMode ? '▶' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
|
|
585
|
+
buttons: availableModes.map(m => ({
|
|
586
|
+
key: m.key,
|
|
587
|
+
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
588
|
+
style: m.key === currentMode ? 'primary' : 'default',
|
|
589
|
+
})),
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
const replyCtx = this.getReplyContext(permSession);
|
|
593
|
+
const cardSent = await this.sendInteractionCard({
|
|
594
|
+
channel, channelId, sessionId: permSession.id, requestId, interaction, replyCtx,
|
|
595
|
+
callback: async (action, _values, operatorId) => {
|
|
596
|
+
if (action !== currentMode) {
|
|
597
|
+
if (userId && operatorId && operatorId !== userId)
|
|
598
|
+
return;
|
|
599
|
+
const result = await this.handle(`/perm ${action}`, channel, channelId, undefined, userId, threadId);
|
|
600
|
+
if (result) {
|
|
601
|
+
const adapter = this.adapters.get(channel);
|
|
602
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
if (cardSent)
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
// 降级:文本
|
|
450
611
|
const modeList = modes.map(m => {
|
|
451
612
|
const prefix = m.key === currentMode ? '▶' : ' ';
|
|
452
613
|
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
453
614
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
454
615
|
}).join('\n');
|
|
455
|
-
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式>
|
|
616
|
+
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
|
|
456
617
|
}
|
|
457
618
|
const parts = args.split(/\s+/);
|
|
458
|
-
// /perm <mode> 或 /perm allow|deny:切换模式 / 快捷审批
|
|
619
|
+
// /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
|
|
459
620
|
if (parts.length === 1) {
|
|
460
621
|
const arg = parts[0];
|
|
461
|
-
// /perm allow|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
|
|
462
|
-
if (arg === 'allow' || arg === 'deny') {
|
|
622
|
+
// /perm allow|always|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
|
|
623
|
+
if (arg === 'allow' || arg === 'always' || arg === 'deny') {
|
|
463
624
|
if (!this.permissionGateway) {
|
|
464
625
|
return '❌ 权限审批未启用';
|
|
465
626
|
}
|
|
@@ -471,8 +632,14 @@ export class CommandHandler {
|
|
|
471
632
|
return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}`;
|
|
472
633
|
}
|
|
473
634
|
const requestId = pendingIds[0];
|
|
474
|
-
|
|
475
|
-
|
|
635
|
+
const decision = arg;
|
|
636
|
+
this.permissionGateway.resolvePermission(permSession.id, requestId, decision);
|
|
637
|
+
const labels = {
|
|
638
|
+
allow: '✓ 已授权(本次),继续执行……',
|
|
639
|
+
always: '✓ 已授权(始终允许该工具),继续执行……',
|
|
640
|
+
deny: '✓ 已拒绝'
|
|
641
|
+
};
|
|
642
|
+
return labels[decision];
|
|
476
643
|
}
|
|
477
644
|
// /perm <mode>:切换权限模式
|
|
478
645
|
if (hasPermissionController(permAgent)) {
|
|
@@ -482,6 +649,10 @@ export class CommandHandler {
|
|
|
482
649
|
if (!matched.available) {
|
|
483
650
|
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
484
651
|
}
|
|
652
|
+
// guest 用户只能保持 readonly 模式
|
|
653
|
+
if (identity.role !== 'owner' && arg !== 'readonly') {
|
|
654
|
+
return '❌ 当前身份无法切换权限模式';
|
|
655
|
+
}
|
|
485
656
|
const metadata = permSession.metadata || {};
|
|
486
657
|
metadata.permissionMode = arg;
|
|
487
658
|
await this.sessionManager.updateSession(permSession.id, { metadata });
|
|
@@ -489,12 +660,12 @@ export class CommandHandler {
|
|
|
489
660
|
}
|
|
490
661
|
}
|
|
491
662
|
// 不是已知模式名也不是 allow/deny
|
|
492
|
-
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : '
|
|
493
|
-
return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|deny`;
|
|
663
|
+
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
664
|
+
return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny`;
|
|
494
665
|
}
|
|
495
666
|
// 双参数不再支持,提示正确用法
|
|
496
|
-
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : '
|
|
497
|
-
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|deny`;
|
|
667
|
+
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
668
|
+
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
|
|
498
669
|
}
|
|
499
670
|
// /agent 命令:查看或切换 Agent 后端
|
|
500
671
|
if (normalizedContent.startsWith('/agent')) {
|
|
@@ -504,7 +675,44 @@ export class CommandHandler {
|
|
|
504
675
|
const available = [...this.agentMap.keys()];
|
|
505
676
|
if (!args) {
|
|
506
677
|
const currentAgent = activeSession?.agentId || this.defaultAgentId;
|
|
507
|
-
|
|
678
|
+
// 尝试发送交互卡片
|
|
679
|
+
if (this.interactionRouter && available.length > 1) {
|
|
680
|
+
const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
681
|
+
const interaction = {
|
|
682
|
+
type: 'interaction',
|
|
683
|
+
id: requestId,
|
|
684
|
+
channelId,
|
|
685
|
+
sessionId: activeSession?.id || requestId,
|
|
686
|
+
kind: {
|
|
687
|
+
kind: 'action',
|
|
688
|
+
title: '🔌 切换 Agent',
|
|
689
|
+
buttons: available.map(a => ({
|
|
690
|
+
key: a,
|
|
691
|
+
label: a === currentAgent ? `✓ ${a}` : a,
|
|
692
|
+
style: a === currentAgent ? 'primary' : 'default',
|
|
693
|
+
})),
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
697
|
+
const cardSent = await this.sendInteractionCard({
|
|
698
|
+
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
699
|
+
callback: async (action, _values, operatorId) => {
|
|
700
|
+
if (action !== currentAgent) {
|
|
701
|
+
if (userId && operatorId && operatorId !== userId)
|
|
702
|
+
return;
|
|
703
|
+
const result = await this.handle(`/agent ${action}`, channel, channelId, undefined, userId, threadId);
|
|
704
|
+
if (result) {
|
|
705
|
+
const adapter = this.adapters.get(channel);
|
|
706
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
if (cardSent)
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
// 降级:文本
|
|
715
|
+
const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
|
|
508
716
|
return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
|
|
509
717
|
}
|
|
510
718
|
if (!this.agentMap.has(args)) {
|
|
@@ -514,15 +722,19 @@ export class CommandHandler {
|
|
|
514
722
|
if ('error' in result)
|
|
515
723
|
return result.error;
|
|
516
724
|
const { session } = result;
|
|
517
|
-
// 取消原会话的 pending
|
|
725
|
+
// 取消原会话的 pending 权限请求和交互卡片
|
|
518
726
|
if (this.permissionGateway) {
|
|
519
727
|
this.permissionGateway.cancelAll(session.id);
|
|
520
728
|
}
|
|
729
|
+
if (this.interactionRouter) {
|
|
730
|
+
this.interactionRouter.cancelAll(session.id);
|
|
731
|
+
}
|
|
521
732
|
// 切换到目标 agent(恢复已有会话或创建新会话)
|
|
522
733
|
const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
|
|
523
734
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
524
735
|
const projectName = this.getProjectName(session.projectPath);
|
|
525
|
-
|
|
736
|
+
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
|
|
737
|
+
return agentSwitchResponse;
|
|
526
738
|
}
|
|
527
739
|
// /model 命令:查看或切换模型/推理强度
|
|
528
740
|
if (normalizedContent.startsWith('/model')) {
|
|
@@ -536,54 +748,72 @@ export class CommandHandler {
|
|
|
536
748
|
const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
|
|
537
749
|
if (!args) {
|
|
538
750
|
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
751
|
+
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
539
752
|
const currentEffort = modelAgent.getEffort?.() || 'auto';
|
|
540
|
-
|
|
753
|
+
// 尝试发送交互卡片
|
|
754
|
+
if (this.interactionRouter && models.length > 0) {
|
|
755
|
+
const requestId = `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
756
|
+
const interaction = {
|
|
757
|
+
type: 'interaction',
|
|
758
|
+
id: requestId,
|
|
759
|
+
channelId,
|
|
760
|
+
sessionId: modelSession.id,
|
|
761
|
+
kind: {
|
|
762
|
+
kind: 'action',
|
|
763
|
+
title: '🤖 切换模型',
|
|
764
|
+
buttons: models.map((m) => ({
|
|
765
|
+
key: m,
|
|
766
|
+
label: m === currentModel ? `✓ ${m}` : m,
|
|
767
|
+
style: m === currentModel ? 'primary' : 'default',
|
|
768
|
+
})),
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
const replyCtx = this.getReplyContext(modelSession);
|
|
772
|
+
const cardSent = await this.sendInteractionCard({
|
|
773
|
+
channel, channelId, sessionId: modelSession.id, requestId, interaction, replyCtx,
|
|
774
|
+
callback: async (action, _values, operatorId) => {
|
|
775
|
+
if (action !== currentModel) {
|
|
776
|
+
if (userId && operatorId && operatorId !== userId)
|
|
777
|
+
return;
|
|
778
|
+
const result = await this.handle(`/model ${action}`, channel, channelId, undefined, userId, threadId);
|
|
779
|
+
if (result) {
|
|
780
|
+
const adapter = this.adapters.get(channel);
|
|
781
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
if (cardSent)
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
// 降级:文本
|
|
541
790
|
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
542
|
-
|
|
791
|
+
const effortHint = efforts.length > 0
|
|
792
|
+
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
793
|
+
: '';
|
|
794
|
+
return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
|
|
543
795
|
}
|
|
544
796
|
const parts = args.split(/\s+/);
|
|
545
797
|
let newModel;
|
|
546
798
|
let newEffort;
|
|
547
799
|
if (parts.length === 1) {
|
|
548
800
|
const arg = parts[0];
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (this.config.agents?.openai?.reasoning) {
|
|
555
|
-
delete this.config.agents.openai.reasoning;
|
|
556
|
-
try {
|
|
557
|
-
saveConfig(this.config);
|
|
558
|
-
}
|
|
559
|
-
catch { }
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
else {
|
|
563
|
-
const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
|
|
564
|
-
if (configuredInEvolclaw) {
|
|
565
|
-
delete this.config.agents.anthropic.effort;
|
|
566
|
-
try {
|
|
567
|
-
saveConfig(this.config);
|
|
568
|
-
}
|
|
569
|
-
catch { }
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
572
|
-
writeUserSettings({ effortLevel: null });
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return '✓ 推理强度已恢复为 auto (SDK默认)';
|
|
801
|
+
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
802
|
+
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
803
|
+
// effort 相关参数统一转发到 /effort
|
|
804
|
+
if (efforts.includes(arg) || arg === 'auto') {
|
|
805
|
+
return this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
|
|
576
806
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
newEffort = arg;
|
|
807
|
+
else if (allEfforts.includes(arg)) {
|
|
808
|
+
return `⚠️ 请使用 /effort ${arg} 调整推理强度`;
|
|
580
809
|
}
|
|
581
810
|
else if (models.includes(arg)) {
|
|
582
811
|
newModel = arg;
|
|
583
812
|
}
|
|
584
813
|
else {
|
|
585
814
|
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
586
|
-
|
|
815
|
+
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
816
|
+
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
|
|
587
817
|
}
|
|
588
818
|
}
|
|
589
819
|
else {
|
|
@@ -592,8 +822,13 @@ export class CommandHandler {
|
|
|
592
822
|
if (!models.includes(modelArg)) {
|
|
593
823
|
return `❌ 无效的模型ID: ${modelArg}`;
|
|
594
824
|
}
|
|
595
|
-
|
|
596
|
-
|
|
825
|
+
const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
|
|
826
|
+
if (targetEfforts.length === 0) {
|
|
827
|
+
return `⚠️ ${modelArg} 不支持推理强度设置`;
|
|
828
|
+
}
|
|
829
|
+
if (!targetEfforts.includes(effortArg)) {
|
|
830
|
+
const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
|
|
831
|
+
return `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}`;
|
|
597
832
|
}
|
|
598
833
|
newModel = modelArg;
|
|
599
834
|
newEffort = effortArg;
|
|
@@ -613,12 +848,8 @@ export class CommandHandler {
|
|
|
613
848
|
changes.push(`模型: ${newModel}`);
|
|
614
849
|
}
|
|
615
850
|
if (newEffort) {
|
|
616
|
-
const modelAfterSwitch = newModel ?? (hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name);
|
|
617
|
-
if (newEffort === 'max' && !modelAfterSwitch.includes('opus')) {
|
|
618
|
-
return '⚠️ max 推理强度仅 Opus 模型支持(opus / claude-opus-4-6)';
|
|
619
|
-
}
|
|
620
851
|
modelAgent.setEffort?.(newEffort);
|
|
621
|
-
changes.push(`推理强度: ${newEffort}
|
|
852
|
+
changes.push(`推理强度: ${newEffort}`);
|
|
622
853
|
}
|
|
623
854
|
// 持久化:写回来源(就近原则)
|
|
624
855
|
// evolclaw.json 配了 → 写 evolclaw.json
|
|
@@ -685,6 +916,137 @@ export class CommandHandler {
|
|
|
685
916
|
}
|
|
686
917
|
return `✓ 已切换\n ${changes.join('\n ')}`;
|
|
687
918
|
}
|
|
919
|
+
// /effort 命令:查看或切换推理强度
|
|
920
|
+
if (normalizedContent.startsWith('/effort')) {
|
|
921
|
+
const args = normalizedContent.slice(7).trim();
|
|
922
|
+
const effortResult = await this.ensureSession(channel, channelId, threadId);
|
|
923
|
+
if ('error' in effortResult)
|
|
924
|
+
return effortResult.error;
|
|
925
|
+
const { session: effortSession } = effortResult;
|
|
926
|
+
const effortAgent = this.getAgent(effortSession.agentId);
|
|
927
|
+
const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
|
|
928
|
+
const efforts = getAvailableEfforts(effortAgent, currentModel);
|
|
929
|
+
const currentEffort = effortAgent.getEffort?.() || 'auto';
|
|
930
|
+
if (efforts.length === 0) {
|
|
931
|
+
return '⚠️ 当前模型不支持推理强度设置';
|
|
932
|
+
}
|
|
933
|
+
if (!args) {
|
|
934
|
+
// /effort(无参数):显示当前推理强度 + 发送 Action 卡片
|
|
935
|
+
if (this.interactionRouter) {
|
|
936
|
+
const requestId = `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
937
|
+
const buttons = [
|
|
938
|
+
...efforts.map(e => ({
|
|
939
|
+
key: e,
|
|
940
|
+
label: e === currentEffort ? `✓ ${e}` : e,
|
|
941
|
+
style: e === currentEffort ? 'primary' : 'default',
|
|
942
|
+
})),
|
|
943
|
+
{
|
|
944
|
+
key: 'auto',
|
|
945
|
+
label: currentEffort === 'auto' ? '✓ auto' : 'auto',
|
|
946
|
+
style: currentEffort === 'auto' ? 'primary' : 'default',
|
|
947
|
+
},
|
|
948
|
+
];
|
|
949
|
+
const interaction = {
|
|
950
|
+
type: 'interaction',
|
|
951
|
+
id: requestId,
|
|
952
|
+
channelId,
|
|
953
|
+
sessionId: effortSession.id,
|
|
954
|
+
kind: {
|
|
955
|
+
kind: 'action',
|
|
956
|
+
title: '⚡ 推理强度',
|
|
957
|
+
buttons,
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
const replyCtx = this.getReplyContext(effortSession);
|
|
961
|
+
const cardSent = await this.sendInteractionCard({
|
|
962
|
+
channel, channelId, sessionId: effortSession.id, requestId, interaction, replyCtx,
|
|
963
|
+
callback: async (action, _values, operatorId) => {
|
|
964
|
+
if (action !== currentEffort) {
|
|
965
|
+
if (userId && operatorId && operatorId !== userId)
|
|
966
|
+
return;
|
|
967
|
+
const result = await this.handle(`/effort ${action}`, channel, channelId, undefined, userId, threadId);
|
|
968
|
+
if (result) {
|
|
969
|
+
const adapter = this.adapters.get(channel);
|
|
970
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
if (cardSent)
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
// 降级:文本
|
|
979
|
+
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
980
|
+
const effortList = efforts.map(e => `${e === currentEffort ? ' ✓' : ' '} ${e}`).join('\n');
|
|
981
|
+
return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n ${currentEffort === 'auto' ? ' ✓' : ' '} auto\n\n用法: /effort <level>`;
|
|
982
|
+
}
|
|
983
|
+
// /effort auto:恢复 SDK 默认
|
|
984
|
+
if (args === 'auto') {
|
|
985
|
+
effortAgent.setEffort?.(undefined);
|
|
986
|
+
const isCodex = effortAgent.name === 'codex';
|
|
987
|
+
if (isCodex) {
|
|
988
|
+
if (this.config.agents?.openai?.reasoning) {
|
|
989
|
+
delete this.config.agents.openai.reasoning;
|
|
990
|
+
try {
|
|
991
|
+
saveConfig(this.config);
|
|
992
|
+
}
|
|
993
|
+
catch { }
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
|
|
998
|
+
if (configuredInEvolclaw) {
|
|
999
|
+
delete this.config.agents.anthropic.effort;
|
|
1000
|
+
try {
|
|
1001
|
+
saveConfig(this.config);
|
|
1002
|
+
}
|
|
1003
|
+
catch { }
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
writeUserSettings({ effortLevel: null });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return '✓ 推理强度已恢复为 auto (SDK默认)';
|
|
1010
|
+
}
|
|
1011
|
+
// /effort <level>:切换推理强度
|
|
1012
|
+
if (!efforts.includes(args)) {
|
|
1013
|
+
if (allEfforts.includes(args)) {
|
|
1014
|
+
return `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}`;
|
|
1015
|
+
}
|
|
1016
|
+
return `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto`;
|
|
1017
|
+
}
|
|
1018
|
+
const newEffort = args;
|
|
1019
|
+
effortAgent.setEffort?.(newEffort);
|
|
1020
|
+
// 持久化
|
|
1021
|
+
if (!this.config.agents)
|
|
1022
|
+
this.config.agents = {};
|
|
1023
|
+
const isCodex = effortAgent.name === 'codex';
|
|
1024
|
+
if (isCodex) {
|
|
1025
|
+
if (!this.config.agents.openai)
|
|
1026
|
+
this.config.agents.openai = {};
|
|
1027
|
+
this.config.agents.openai.reasoning = newEffort;
|
|
1028
|
+
try {
|
|
1029
|
+
saveConfig(this.config);
|
|
1030
|
+
}
|
|
1031
|
+
catch { }
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
|
|
1035
|
+
if (configuredInEvolclaw) {
|
|
1036
|
+
if (!this.config.agents.anthropic)
|
|
1037
|
+
this.config.agents.anthropic = {};
|
|
1038
|
+
this.config.agents.anthropic.effort = newEffort;
|
|
1039
|
+
try {
|
|
1040
|
+
saveConfig(this.config);
|
|
1041
|
+
}
|
|
1042
|
+
catch { }
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
writeUserSettings({ effortLevel: newEffort });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return `✓ 推理强度: ${newEffort}`;
|
|
1049
|
+
}
|
|
688
1050
|
// /stop 命令:中断当前任务
|
|
689
1051
|
if (normalizedContent === '/stop') {
|
|
690
1052
|
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -827,7 +1189,7 @@ export class CommandHandler {
|
|
|
827
1189
|
}
|
|
828
1190
|
const lines = [];
|
|
829
1191
|
if (isAdmin) {
|
|
830
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
1192
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
831
1193
|
if (health.consecutiveErrors > 0) {
|
|
832
1194
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
833
1195
|
}
|
|
@@ -844,8 +1206,8 @@ export class CommandHandler {
|
|
|
844
1206
|
lines.push('⚠️ 当前处于安全模式(历史上下文已禁用)');
|
|
845
1207
|
lines.push('');
|
|
846
1208
|
lines.push('退出方式:');
|
|
847
|
-
lines.push('1. /
|
|
848
|
-
lines.push('2. /
|
|
1209
|
+
lines.push('1. /new [名称] - 创建新会话(清空历史)');
|
|
1210
|
+
lines.push('2. 联系管理员使用 /repair 检查并修复会话');
|
|
849
1211
|
}
|
|
850
1212
|
if (health.lastError) {
|
|
851
1213
|
lines.push('');
|
|
@@ -875,9 +1237,12 @@ export class CommandHandler {
|
|
|
875
1237
|
timestamp: Date.now()
|
|
876
1238
|
});
|
|
877
1239
|
if (session) {
|
|
1240
|
+
// Reset agent backend state so the new
|
|
1241
|
+
// session starts with a fresh conversation history
|
|
1242
|
+
await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
878
1243
|
await agent.closeSession(session.id);
|
|
879
1244
|
}
|
|
880
|
-
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /
|
|
1245
|
+
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
|
|
881
1246
|
}
|
|
882
1247
|
// /check 命令:检查渠道状态 / 手动重连指定渠道
|
|
883
1248
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
@@ -903,15 +1268,30 @@ export class CommandHandler {
|
|
|
903
1268
|
}
|
|
904
1269
|
// Default: show full system health check
|
|
905
1270
|
const lines = ['📡 渠道状态:'];
|
|
1271
|
+
// Group by channelType
|
|
1272
|
+
const groups = new Map();
|
|
906
1273
|
for (const [name] of this.adapters) {
|
|
1274
|
+
const type = this.channelTypeMap.get(name) || name;
|
|
907
1275
|
const ch = this.channelObjects.get(name);
|
|
1276
|
+
let status;
|
|
908
1277
|
if (ch?.getStatus) {
|
|
909
1278
|
const s = ch.getStatus();
|
|
910
|
-
|
|
911
|
-
|
|
1279
|
+
status = s.connected ? '✓ 已连接' : s.reconnectAttempt > 0 ? `⏳ 重连中 (${s.reconnectAttempt}/${s.maxAttempts})` : '✗ 断开';
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
status = '✓ 已注册';
|
|
1283
|
+
}
|
|
1284
|
+
if (!groups.has(type))
|
|
1285
|
+
groups.set(type, []);
|
|
1286
|
+
groups.get(type).push({ name, status });
|
|
1287
|
+
}
|
|
1288
|
+
for (const [type, instances] of groups) {
|
|
1289
|
+
if (instances.length === 1) {
|
|
1290
|
+
lines.push(` ${instances[0].name}: ${instances[0].status}`);
|
|
912
1291
|
}
|
|
913
1292
|
else {
|
|
914
|
-
|
|
1293
|
+
const parts = instances.map(i => `${i.name} ${i.status}`);
|
|
1294
|
+
lines.push(` ${type}: [${parts.join(', ')}]`);
|
|
915
1295
|
}
|
|
916
1296
|
}
|
|
917
1297
|
// 队列状态
|
|
@@ -924,7 +1304,6 @@ export class CommandHandler {
|
|
|
924
1304
|
? this.statsCollector.getSnapshot().uptimeMs
|
|
925
1305
|
: process.uptime() * 1000;
|
|
926
1306
|
lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
|
|
927
|
-
lines.push(` 当前安全模式会话: ${this.sessionManager.getSafeModeSessionCount()}`);
|
|
928
1307
|
// 近 1 小时统计
|
|
929
1308
|
if (this.statsCollector) {
|
|
930
1309
|
const snap = this.statsCollector.getSnapshot();
|
|
@@ -939,8 +1318,11 @@ export class CommandHandler {
|
|
|
939
1318
|
else {
|
|
940
1319
|
lines.push(` 处理出错: 0`);
|
|
941
1320
|
}
|
|
1321
|
+
if (h.toolErrors > 0) {
|
|
1322
|
+
const toolBreakdown = Object.entries(h.toolErrorsByName).map(([t, c]) => `${t}: ${c}`).join(', ');
|
|
1323
|
+
lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
|
|
1324
|
+
}
|
|
942
1325
|
lines.push(` 被中断: ${h.interrupts}`);
|
|
943
|
-
lines.push(` 进入安全模式: ${h.safeModeEntries}`);
|
|
944
1326
|
if (h.completed > 0) {
|
|
945
1327
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
946
1328
|
}
|
|
@@ -957,6 +1339,33 @@ export class CommandHandler {
|
|
|
957
1339
|
const count = this.messageCache.getCount(s.id);
|
|
958
1340
|
return `${s.projectPath} 有 ${count} 条新消息`;
|
|
959
1341
|
});
|
|
1342
|
+
// 执行重启逻辑(共用于卡片回调和文本确认)
|
|
1343
|
+
const executeRestart = async () => {
|
|
1344
|
+
let replyContext;
|
|
1345
|
+
if (threadId) {
|
|
1346
|
+
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
1347
|
+
replyContext = this.getReplyContext(threadSession);
|
|
1348
|
+
}
|
|
1349
|
+
const restartInfo = {
|
|
1350
|
+
channel,
|
|
1351
|
+
channelId,
|
|
1352
|
+
timestamp: Date.now(),
|
|
1353
|
+
...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
|
|
1354
|
+
};
|
|
1355
|
+
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
1356
|
+
const { spawn } = await import('child_process');
|
|
1357
|
+
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
|
|
1358
|
+
detached: true,
|
|
1359
|
+
stdio: 'ignore',
|
|
1360
|
+
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
1361
|
+
}).unref();
|
|
1362
|
+
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
1363
|
+
setTimeout(() => {
|
|
1364
|
+
logger.info('[System] Restarting by user command...');
|
|
1365
|
+
process.exit(0);
|
|
1366
|
+
}, 1000);
|
|
1367
|
+
};
|
|
1368
|
+
// 文本确认流程
|
|
960
1369
|
if (sessionsWithMessages.length > 0) {
|
|
961
1370
|
const restartKey = `${channel}-${channelId}`;
|
|
962
1371
|
const restartConfirmFile = path.join(resolvePaths().dataDir, `restart-confirm-${restartKey}.json`);
|
|
@@ -976,30 +1385,7 @@ export class CommandHandler {
|
|
|
976
1385
|
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
977
1386
|
}
|
|
978
1387
|
}
|
|
979
|
-
|
|
980
|
-
let replyContext;
|
|
981
|
-
if (threadId) {
|
|
982
|
-
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
983
|
-
replyContext = this.getReplyContext(threadSession);
|
|
984
|
-
}
|
|
985
|
-
const restartInfo = {
|
|
986
|
-
channel,
|
|
987
|
-
channelId,
|
|
988
|
-
timestamp: Date.now(),
|
|
989
|
-
...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
|
|
990
|
-
};
|
|
991
|
-
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
992
|
-
const { spawn } = await import('child_process');
|
|
993
|
-
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
|
|
994
|
-
detached: true,
|
|
995
|
-
stdio: 'ignore',
|
|
996
|
-
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
997
|
-
}).unref();
|
|
998
|
-
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
999
|
-
setTimeout(() => {
|
|
1000
|
-
logger.info('[System] Restarting by user command...');
|
|
1001
|
-
process.exit(0);
|
|
1002
|
-
}, 1000);
|
|
1388
|
+
await executeRestart();
|
|
1003
1389
|
return '🔄 服务正在重启,请稍候...(约 5 秒后恢复)';
|
|
1004
1390
|
}
|
|
1005
1391
|
// /pwd 命令:显示当前项目路径
|
|
@@ -1022,26 +1408,43 @@ export class CommandHandler {
|
|
|
1022
1408
|
if (!rawArg) {
|
|
1023
1409
|
return '用法: /send <相对路径> 或 /send <渠道> <相对路径>\n示例: /send src/index.ts\n示例: /send feishu report.md';
|
|
1024
1410
|
}
|
|
1025
|
-
// 解析目标通道:第一个 token
|
|
1411
|
+
// 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
|
|
1026
1412
|
const tokens = rawArg.split(/\s+/);
|
|
1027
|
-
const knownChannels = [...this.adapters.keys()];
|
|
1028
1413
|
let targetChannel = channel;
|
|
1414
|
+
let targetLabel = channel;
|
|
1029
1415
|
let filePath = rawArg;
|
|
1030
|
-
if (tokens.length >= 2
|
|
1031
|
-
|
|
1032
|
-
|
|
1416
|
+
if (tokens.length >= 2) {
|
|
1417
|
+
const spec = tokens[0];
|
|
1418
|
+
if (this.adapters.has(spec)) {
|
|
1419
|
+
// 精确实例名
|
|
1420
|
+
targetChannel = spec;
|
|
1421
|
+
targetLabel = spec;
|
|
1422
|
+
filePath = tokens.slice(1).join(' ');
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
// 按 channelType 查找第一个匹配的实例
|
|
1426
|
+
for (const [name] of this.adapters) {
|
|
1427
|
+
if ((this.channelTypeMap.get(name) || name) === spec) {
|
|
1428
|
+
targetChannel = name;
|
|
1429
|
+
targetLabel = spec;
|
|
1430
|
+
filePath = tokens.slice(1).join(' ');
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1033
1435
|
}
|
|
1436
|
+
const isCrossChannel = targetChannel !== channel;
|
|
1034
1437
|
// 跨通道仅限 owner
|
|
1035
|
-
if (
|
|
1438
|
+
if (isCrossChannel && identity.role !== 'owner') {
|
|
1036
1439
|
return '❌ 跨通道发送仅限管理员';
|
|
1037
1440
|
}
|
|
1038
1441
|
// 找目标 adapter
|
|
1039
1442
|
const targetAdapter = this.adapters.get(targetChannel);
|
|
1040
1443
|
if (!targetAdapter) {
|
|
1041
|
-
return `❌ 通道 ${
|
|
1444
|
+
return `❌ 通道 ${targetLabel} 未启用或不存在`;
|
|
1042
1445
|
}
|
|
1043
1446
|
if (!targetAdapter.sendFile) {
|
|
1044
|
-
return `❌ 通道 ${
|
|
1447
|
+
return `❌ 通道 ${targetLabel} 不支持文件发送`;
|
|
1045
1448
|
}
|
|
1046
1449
|
// 获取 session(需要 projectPath)
|
|
1047
1450
|
const sendResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -1076,22 +1479,22 @@ export class CommandHandler {
|
|
|
1076
1479
|
}
|
|
1077
1480
|
// 找目标 channelId
|
|
1078
1481
|
let targetChannelId = channelId;
|
|
1079
|
-
if (
|
|
1482
|
+
if (isCrossChannel) {
|
|
1080
1483
|
const ownerPeerId = getOwner(this.config, targetChannel);
|
|
1081
1484
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
|
|
1082
1485
|
if (!targetChannelId) {
|
|
1083
|
-
return `❌ 未找到 ${
|
|
1486
|
+
return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
|
|
1084
1487
|
}
|
|
1085
1488
|
}
|
|
1086
1489
|
// 发送文件
|
|
1087
1490
|
try {
|
|
1088
|
-
const replyCtx =
|
|
1491
|
+
const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
|
|
1089
1492
|
await targetAdapter.sendFile(targetChannelId, realPath, replyCtx);
|
|
1090
1493
|
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
1091
1494
|
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
1092
1495
|
: `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
|
|
1093
|
-
return
|
|
1094
|
-
? `📎 文件已通过 ${
|
|
1496
|
+
return isCrossChannel
|
|
1497
|
+
? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
|
|
1095
1498
|
: `✅ 已发送: ${filePath} (${sizeStr})`;
|
|
1096
1499
|
}
|
|
1097
1500
|
catch (error) {
|
|
@@ -1115,73 +1518,110 @@ export class CommandHandler {
|
|
|
1115
1518
|
|
|
1116
1519
|
提示:群聊不支持切换项目`;
|
|
1117
1520
|
}
|
|
1118
|
-
|
|
1119
|
-
// 收集项目信息并按最近活跃排序
|
|
1521
|
+
// 收集项目信息并按最近活跃排序(唯一来源:evolclaw.json projects.list)
|
|
1120
1522
|
const entries = [];
|
|
1121
|
-
const configuredPaths = new Set();
|
|
1122
1523
|
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1524
|
+
// 跳过不存在的路径
|
|
1525
|
+
if (!fs.existsSync(projectPath))
|
|
1526
|
+
continue;
|
|
1527
|
+
const isCurrent = session ? path.resolve(session.projectPath) === path.resolve(projectPath) : false;
|
|
1125
1528
|
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
1126
1529
|
entries.push({
|
|
1127
1530
|
name, projectPath, projectSession, isCurrent,
|
|
1128
1531
|
updatedAt: projectSession?.updatedAt ?? 0,
|
|
1129
1532
|
});
|
|
1130
1533
|
}
|
|
1131
|
-
// Include bound projects not in config (created via /bind)
|
|
1132
|
-
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1133
|
-
for (const s of allSessions) {
|
|
1134
|
-
if (!configuredPaths.has(s.projectPath)) {
|
|
1135
|
-
configuredPaths.add(s.projectPath);
|
|
1136
|
-
const isCurrent = session?.projectPath === s.projectPath;
|
|
1137
|
-
entries.push({
|
|
1138
|
-
name: path.basename(s.projectPath), projectPath: s.projectPath, projectSession: s, isCurrent,
|
|
1139
|
-
updatedAt: s.updatedAt ?? 0,
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
1534
|
// 当前活跃项目置顶,其余按 updatedAt 降序
|
|
1144
1535
|
entries.sort((a, b) => {
|
|
1145
1536
|
if (a.isCurrent !== b.isCurrent)
|
|
1146
1537
|
return a.isCurrent ? -1 : 1;
|
|
1147
1538
|
return b.updatedAt - a.updatedAt;
|
|
1148
1539
|
});
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const statusParts = [];
|
|
1540
|
+
// 构建项目状态文本的辅助函数
|
|
1541
|
+
const buildStatusText = (entry) => {
|
|
1542
|
+
const { projectSession, isCurrent } = entry;
|
|
1543
|
+
if (!projectSession)
|
|
1544
|
+
return '无会话';
|
|
1545
|
+
const parts = [];
|
|
1156
1546
|
if (isCurrent) {
|
|
1157
|
-
|
|
1547
|
+
parts.push('活跃');
|
|
1158
1548
|
}
|
|
1159
1549
|
else {
|
|
1160
|
-
|
|
1161
|
-
statusParts.push(formatIdleTime(idleMs));
|
|
1550
|
+
parts.push(formatIdleTime(Date.now() - projectSession.updatedAt));
|
|
1162
1551
|
}
|
|
1163
|
-
// 用 DB processingState 判断处理状态
|
|
1164
1552
|
const isProcessing = !!projectSession.processingState;
|
|
1165
1553
|
if (isProcessing) {
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
statusParts.push(`[处理中,队列${queueLength}条]`);
|
|
1169
|
-
}
|
|
1170
|
-
else {
|
|
1171
|
-
statusParts.push('[处理中]');
|
|
1172
|
-
}
|
|
1554
|
+
const qLen = this.messageQueue.getQueueLength(projectSession.id);
|
|
1555
|
+
parts.push(qLen > 0 ? `[处理中,队列${qLen}条]` : '[处理中]');
|
|
1173
1556
|
}
|
|
1174
|
-
const
|
|
1175
|
-
if (
|
|
1176
|
-
|
|
1557
|
+
const unread = this.messageCache.getCount(projectSession.id);
|
|
1558
|
+
if (unread > 0) {
|
|
1559
|
+
parts.push(`[${unread}条新消息]`);
|
|
1177
1560
|
}
|
|
1178
1561
|
else if (!isProcessing && !isCurrent) {
|
|
1179
|
-
|
|
1562
|
+
parts.push('[空闲]');
|
|
1180
1563
|
}
|
|
1181
|
-
|
|
1564
|
+
return parts.join(' ');
|
|
1565
|
+
};
|
|
1566
|
+
// 尝试发送 ActionInteraction 卡片(每个项目一个按钮,一键切换)
|
|
1567
|
+
if (this.interactionRouter && entries.length > 0) {
|
|
1568
|
+
const requestId = `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1569
|
+
const buttons = entries.map(e => ({
|
|
1570
|
+
key: e.name,
|
|
1571
|
+
label: e.isCurrent ? `✓ ${e.name}` : e.name,
|
|
1572
|
+
style: e.isCurrent ? 'primary' : 'default',
|
|
1573
|
+
}));
|
|
1574
|
+
const bodyLines = entries.map(e => {
|
|
1575
|
+
const status = buildStatusText(e);
|
|
1576
|
+
const prefix = e.isCurrent ? '▶' : '•';
|
|
1577
|
+
return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
|
|
1578
|
+
});
|
|
1579
|
+
const interaction = {
|
|
1580
|
+
type: 'interaction',
|
|
1581
|
+
id: requestId,
|
|
1582
|
+
channelId,
|
|
1583
|
+
sessionId: activeSession?.id || requestId,
|
|
1584
|
+
kind: {
|
|
1585
|
+
kind: 'action',
|
|
1586
|
+
title: '📂 项目列表',
|
|
1587
|
+
body: bodyLines.join('\n'),
|
|
1588
|
+
buttons,
|
|
1589
|
+
},
|
|
1590
|
+
};
|
|
1591
|
+
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1592
|
+
const cardSent = await this.sendInteractionCard({
|
|
1593
|
+
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
1594
|
+
callback: async (action, _values, operatorId) => {
|
|
1595
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1596
|
+
return;
|
|
1597
|
+
const selectedEntry = entries.find(e => e.name === action);
|
|
1598
|
+
if (selectedEntry && !selectedEntry.isCurrent) {
|
|
1599
|
+
const result = await this.handle(`/project ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1600
|
+
if (result) {
|
|
1601
|
+
const adapter = this.adapters.get(channel);
|
|
1602
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
});
|
|
1607
|
+
if (cardSent)
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
// 降级:文本列表
|
|
1611
|
+
const lines = ['可用项目:'];
|
|
1612
|
+
for (const entry of entries) {
|
|
1613
|
+
const prefix = entry.isCurrent ? ' ✓' : ' ';
|
|
1614
|
+
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
1182
1615
|
}
|
|
1183
1616
|
return lines.join('\n');
|
|
1184
1617
|
}
|
|
1618
|
+
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
1619
|
+
if (normalizedContent === '/project') {
|
|
1620
|
+
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
1621
|
+
// 群聊不能切换项目,交由 /plist 逻辑处理
|
|
1622
|
+
}
|
|
1623
|
+
return this.handle('/plist', channel, channelId, undefined, userId, threadId);
|
|
1624
|
+
}
|
|
1185
1625
|
// /project 命令:切换项目(支持名称或路径)
|
|
1186
1626
|
if (normalizedContent.startsWith('/project ')) {
|
|
1187
1627
|
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
@@ -1212,7 +1652,7 @@ export class CommandHandler {
|
|
|
1212
1652
|
else {
|
|
1213
1653
|
projectPath = this.projects[arg];
|
|
1214
1654
|
if (!projectPath) {
|
|
1215
|
-
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /
|
|
1655
|
+
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目`;
|
|
1216
1656
|
}
|
|
1217
1657
|
projectName = arg;
|
|
1218
1658
|
}
|
|
@@ -1307,16 +1747,87 @@ export class CommandHandler {
|
|
|
1307
1747
|
this.projects[projectName] = projectPath;
|
|
1308
1748
|
return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
|
|
1309
1749
|
}
|
|
1310
|
-
// /slist
|
|
1311
|
-
|
|
1750
|
+
// /slist 命令:列出当前项目的会话
|
|
1751
|
+
// /slist — 仅 EvolClaw 会话
|
|
1752
|
+
// /slist cli — 仅 CLI 会话(未导入的)
|
|
1753
|
+
if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
|
|
1312
1754
|
if (!session) {
|
|
1313
1755
|
return `❌ 当前没有活跃会话
|
|
1314
1756
|
|
|
1315
1757
|
请先执行以下操作之一:
|
|
1316
1758
|
1. 发送任意消息 - 自动创建新会话
|
|
1317
1759
|
2. /new [名称] - 创建命名会话
|
|
1318
|
-
3. /
|
|
1760
|
+
3. /p <项目> - 切换到指定项目`;
|
|
1761
|
+
}
|
|
1762
|
+
const showCliOnly = normalizedContent === '/slist cli';
|
|
1763
|
+
// /slist cli — 仅显示 CLI 会话
|
|
1764
|
+
if (showCliOnly) {
|
|
1765
|
+
const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
|
|
1766
|
+
if (!canImportCli) {
|
|
1767
|
+
return '❌ 当前无权查看 CLI 会话';
|
|
1768
|
+
}
|
|
1769
|
+
const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
|
|
1770
|
+
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1771
|
+
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1772
|
+
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
1773
|
+
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
|
|
1774
|
+
if (orphanCliSessions.length === 0) {
|
|
1775
|
+
return `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话`;
|
|
1776
|
+
}
|
|
1777
|
+
// 构建显示数据(复用于卡片和文本)
|
|
1778
|
+
const cliDisplayItems = orphanCliSessions.map(c => {
|
|
1779
|
+
const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
1780
|
+
const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
|
|
1781
|
+
const uuid = c.uuid.substring(0, 8);
|
|
1782
|
+
return { uuid, fullUuid: c.uuid, time, message };
|
|
1783
|
+
});
|
|
1784
|
+
// 尝试发送 ActionInteraction 卡片
|
|
1785
|
+
if (this.interactionRouter && cliDisplayItems.length > 0) {
|
|
1786
|
+
const requestId = `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1787
|
+
const buttons = cliDisplayItems.map(item => ({
|
|
1788
|
+
key: item.uuid,
|
|
1789
|
+
label: item.uuid,
|
|
1790
|
+
style: 'default',
|
|
1791
|
+
}));
|
|
1792
|
+
const bodyLines = cliDisplayItems.map(item => `• ${item.time} (${item.uuid}) "${item.message}"`);
|
|
1793
|
+
const interaction = {
|
|
1794
|
+
type: 'interaction',
|
|
1795
|
+
id: requestId,
|
|
1796
|
+
channelId,
|
|
1797
|
+
sessionId: session.id,
|
|
1798
|
+
kind: {
|
|
1799
|
+
kind: 'action',
|
|
1800
|
+
title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
|
|
1801
|
+
body: bodyLines.join('\n'),
|
|
1802
|
+
buttons,
|
|
1803
|
+
},
|
|
1804
|
+
};
|
|
1805
|
+
const replyCtx = this.getReplyContext(session);
|
|
1806
|
+
const cardSent = await this.sendInteractionCard({
|
|
1807
|
+
channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
|
|
1808
|
+
callback: async (action, _values, operatorId) => {
|
|
1809
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1810
|
+
return;
|
|
1811
|
+
const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1812
|
+
if (result) {
|
|
1813
|
+
const adapter = this.adapters.get(channel);
|
|
1814
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
});
|
|
1818
|
+
if (cardSent)
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
// 降级:文本列表
|
|
1822
|
+
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
|
|
1823
|
+
for (const item of cliDisplayItems) {
|
|
1824
|
+
lines.push(` ${item.time} (${item.uuid}) "${item.message}"`);
|
|
1825
|
+
}
|
|
1826
|
+
lines.push('');
|
|
1827
|
+
lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
|
|
1828
|
+
return lines.join('\n');
|
|
1319
1829
|
}
|
|
1830
|
+
// /slist — 仅显示 EvolClaw 会话
|
|
1320
1831
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1321
1832
|
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1322
1833
|
// 从 SDK 同步会话名称(发现 CLI 改名)
|
|
@@ -1335,46 +1846,97 @@ export class CommandHandler {
|
|
|
1335
1846
|
catch (error) {
|
|
1336
1847
|
logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
|
|
1337
1848
|
}
|
|
1338
|
-
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1849
|
+
// 构建可显示会话列表(复用于卡片和文本)
|
|
1850
|
+
const hideTopics = currentProjectSessions.length > 10;
|
|
1851
|
+
const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
|
|
1852
|
+
const maxDisplay = 10;
|
|
1853
|
+
const displaySessions = [];
|
|
1854
|
+
let displayIndex = 0;
|
|
1855
|
+
for (let i = 0; i < currentProjectSessions.length; i++) {
|
|
1856
|
+
const s = currentProjectSessions[i];
|
|
1857
|
+
if (hideTopics && s.threadId)
|
|
1858
|
+
continue;
|
|
1859
|
+
if (displayIndex >= maxDisplay)
|
|
1860
|
+
break;
|
|
1861
|
+
const isActive = s.metadata?.isActive === true;
|
|
1862
|
+
displayIndex++;
|
|
1863
|
+
const name = s.name || '(未命名)';
|
|
1864
|
+
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
1865
|
+
const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
|
|
1866
|
+
let status = '[空闲]';
|
|
1867
|
+
if (fileMissing) {
|
|
1868
|
+
status = '[会话文件缺失]';
|
|
1869
|
+
}
|
|
1870
|
+
else if (!!s.processingState) {
|
|
1871
|
+
status = '[处理中]';
|
|
1872
|
+
}
|
|
1873
|
+
else if (isActive) {
|
|
1874
|
+
status = '[活跃]';
|
|
1875
|
+
}
|
|
1876
|
+
displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
|
|
1877
|
+
}
|
|
1878
|
+
// 尝试发送 ActionInteraction 卡片(每个会话一个按钮,一键切换)
|
|
1879
|
+
if (this.interactionRouter && displaySessions.length >= 1) {
|
|
1880
|
+
const requestId = `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1881
|
+
const buttons = displaySessions.map(ds => {
|
|
1882
|
+
const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
|
|
1883
|
+
return {
|
|
1884
|
+
key: String(ds.index),
|
|
1885
|
+
label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
|
|
1886
|
+
style: ds.isActive ? 'primary' : 'default',
|
|
1887
|
+
};
|
|
1888
|
+
});
|
|
1889
|
+
const bodyLines = displaySessions.map(ds => {
|
|
1890
|
+
const prefix = ds.isActive ? '▶' : '•';
|
|
1891
|
+
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1892
|
+
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1893
|
+
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
1894
|
+
return `${prefix} ${ds.index}. ${threadTag}${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
|
|
1895
|
+
});
|
|
1896
|
+
const interaction = {
|
|
1897
|
+
type: 'interaction',
|
|
1898
|
+
id: requestId,
|
|
1899
|
+
channelId,
|
|
1900
|
+
sessionId: session.id,
|
|
1901
|
+
kind: {
|
|
1902
|
+
kind: 'action',
|
|
1903
|
+
title: `📋 ${path.basename(session.projectPath)} 会话列表`,
|
|
1904
|
+
body: bodyLines.join('\n'),
|
|
1905
|
+
buttons,
|
|
1906
|
+
},
|
|
1907
|
+
};
|
|
1908
|
+
const replyCtx = this.getReplyContext(session);
|
|
1909
|
+
const cardSent = await this.sendInteractionCard({
|
|
1910
|
+
channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
|
|
1911
|
+
callback: async (action, _values, operatorId) => {
|
|
1912
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1913
|
+
return;
|
|
1914
|
+
const target = displaySessions.find(ds => String(ds.index) === action);
|
|
1915
|
+
if (target && !target.isActive) {
|
|
1916
|
+
const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1917
|
+
if (result) {
|
|
1918
|
+
const adapter = this.adapters.get(channel);
|
|
1919
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
},
|
|
1923
|
+
});
|
|
1924
|
+
if (cardSent)
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
// 降级:文本列表
|
|
1343
1928
|
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
|
|
1344
1929
|
if (currentProjectSessions.length > 0) {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
const s = currentProjectSessions[i];
|
|
1353
|
-
if (hideTopics && s.threadId)
|
|
1354
|
-
continue;
|
|
1355
|
-
if (displayIndex >= maxDisplay)
|
|
1356
|
-
break;
|
|
1357
|
-
const isActive = s.metadata?.isActive === true;
|
|
1358
|
-
displayIndex++;
|
|
1359
|
-
const prefix = isActive ? ' ✓' : ' ';
|
|
1360
|
-
const num = `${displayIndex}.`;
|
|
1361
|
-
const threadTag = s.threadId ? '[话题] ' : '';
|
|
1362
|
-
const name = s.name || '(未命名)';
|
|
1363
|
-
const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
|
|
1364
|
-
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
1365
|
-
if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId)) {
|
|
1366
|
-
lines.push(`${prefix} ${num} ${threadTag}❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
|
|
1930
|
+
for (const ds of displaySessions) {
|
|
1931
|
+
const prefix = ds.isActive ? ' ✓' : ' ';
|
|
1932
|
+
const num = `${ds.index}.`;
|
|
1933
|
+
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1934
|
+
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1935
|
+
if (ds.fileMissing) {
|
|
1936
|
+
lines.push(`${prefix} ${num} ${threadTag}❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
1367
1937
|
}
|
|
1368
1938
|
else {
|
|
1369
|
-
|
|
1370
|
-
let status = '[空闲]';
|
|
1371
|
-
if (sIsProcessing) {
|
|
1372
|
-
status = '[处理中]';
|
|
1373
|
-
}
|
|
1374
|
-
else if (isActive) {
|
|
1375
|
-
status = '[活跃]';
|
|
1376
|
-
}
|
|
1377
|
-
lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
|
|
1939
|
+
lines.push(`${prefix} ${num} ${threadTag}${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
1378
1940
|
}
|
|
1379
1941
|
}
|
|
1380
1942
|
const hiddenCount = currentProjectSessions.length - displayIndex - topicCount;
|
|
@@ -1388,29 +1950,23 @@ export class CommandHandler {
|
|
|
1388
1950
|
}
|
|
1389
1951
|
lines.push('');
|
|
1390
1952
|
}
|
|
1391
|
-
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid)).slice(0, 5);
|
|
1392
|
-
if (orphanCliSessions.length > 0) {
|
|
1393
|
-
lines.push('【CLI 会话】(最新5个)');
|
|
1394
|
-
for (const c of orphanCliSessions) {
|
|
1395
|
-
const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
1396
|
-
const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
|
|
1397
|
-
const uuid = c.uuid.substring(0, 8);
|
|
1398
|
-
lines.push(` ${time} (${uuid}) "${message}"`);
|
|
1399
|
-
}
|
|
1400
|
-
lines.push('');
|
|
1401
|
-
}
|
|
1402
1953
|
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
1954
|
+
lines.push('使用 /s cli 查看 CLI 会话');
|
|
1403
1955
|
return lines.join('\n');
|
|
1404
1956
|
}
|
|
1957
|
+
// /session(无参数):直接复用 /slist 逻辑(含卡片交互)
|
|
1958
|
+
if (normalizedContent === '/session') {
|
|
1959
|
+
return this.handle('/slist', channel, channelId, undefined, userId, threadId);
|
|
1960
|
+
}
|
|
1961
|
+
// /session cli(= /s cli):列出未导入的 CLI 会话
|
|
1962
|
+
if (normalizedContent === '/session cli') {
|
|
1963
|
+
return this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
|
|
1964
|
+
}
|
|
1405
1965
|
// /session 或 /s 命令:切换会话
|
|
1406
1966
|
if (normalizedContent.startsWith('/session ')) {
|
|
1407
1967
|
const sessionName = normalizedContent.slice(9).trim();
|
|
1408
1968
|
if (!sessionName)
|
|
1409
1969
|
return '用法: /s <序号、会话名称或前8位UUID>';
|
|
1410
|
-
const isProcessing = !!session?.processingState;
|
|
1411
|
-
if (isProcessing) {
|
|
1412
|
-
return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
|
|
1413
|
-
}
|
|
1414
1970
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
1415
1971
|
// 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
|
|
1416
1972
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
@@ -1426,7 +1982,7 @@ export class CommandHandler {
|
|
|
1426
1982
|
targetSession = visibleSessions[idx - 1];
|
|
1427
1983
|
}
|
|
1428
1984
|
else {
|
|
1429
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /
|
|
1985
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
|
|
1430
1986
|
}
|
|
1431
1987
|
}
|
|
1432
1988
|
if (!targetSession && sessionName.length === 8) {
|
|
@@ -1451,7 +2007,7 @@ export class CommandHandler {
|
|
|
1451
2007
|
}
|
|
1452
2008
|
}
|
|
1453
2009
|
if (!targetSession) {
|
|
1454
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /
|
|
2010
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
|
|
1455
2011
|
}
|
|
1456
2012
|
const lastInput = targetSession.agentSessionId
|
|
1457
2013
|
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
|
|
@@ -1530,14 +2086,14 @@ export class CommandHandler {
|
|
|
1530
2086
|
targetSession = visibleSessions[idx - 1];
|
|
1531
2087
|
}
|
|
1532
2088
|
else {
|
|
1533
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /
|
|
2089
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
|
|
1534
2090
|
}
|
|
1535
2091
|
}
|
|
1536
2092
|
if (!targetSession && sessionName.length === 8) {
|
|
1537
2093
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
1538
2094
|
}
|
|
1539
2095
|
if (!targetSession) {
|
|
1540
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /
|
|
2096
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
|
|
1541
2097
|
}
|
|
1542
2098
|
if (targetSession.id === session.id) {
|
|
1543
2099
|
return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
|
|
@@ -1568,7 +2124,7 @@ export class CommandHandler {
|
|
|
1568
2124
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
1569
2125
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
1570
2126
|
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
1571
|
-
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /
|
|
2127
|
+
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话`;
|
|
1572
2128
|
}
|
|
1573
2129
|
catch (error) {
|
|
1574
2130
|
logger.error('[CommandHandler] Fork session failed:', error);
|
|
@@ -1586,7 +2142,7 @@ export class CommandHandler {
|
|
|
1586
2142
|
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
1587
2143
|
}
|
|
1588
2144
|
const repairAgent = this.getAgent(repairSession.agentId);
|
|
1589
|
-
const { checkSessionFile, backupSessionFile } = await import('
|
|
2145
|
+
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
1590
2146
|
try {
|
|
1591
2147
|
if (!repairSession.agentSessionId) {
|
|
1592
2148
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|