evolclaw 2.2.0 → 2.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/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 +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- 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} +57 -11
- package/dist/index.js +138 -54
- 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) || {
|
|
@@ -237,6 +342,7 @@ export class CommandHandler {
|
|
|
237
342
|
commands: [
|
|
238
343
|
{ cmd: '/new', args: '[name]', label: '创建新会话' },
|
|
239
344
|
{ cmd: '/slist', label: '列出当前项目的所有会话' },
|
|
345
|
+
{ cmd: '/slist', args: 'cli', label: '列出 CLI 会话(未导入的)' },
|
|
240
346
|
{ cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
|
|
241
347
|
{ cmd: '/name', args: '<name>', label: '重命名当前会话' },
|
|
242
348
|
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
@@ -252,13 +358,14 @@ export class CommandHandler {
|
|
|
252
358
|
group: 'Agent 与模型',
|
|
253
359
|
commands: [
|
|
254
360
|
{ cmd: '/agent', args: '[name]', label: '查看或切换 Agent 后端' },
|
|
255
|
-
{ cmd: '/model', args: '[model]
|
|
361
|
+
{ cmd: '/model', args: '[model]', label: '查看或切换模型' },
|
|
362
|
+
{ cmd: '/effort', args: '[level]', label: '查看或切换推理强度' },
|
|
256
363
|
]
|
|
257
364
|
});
|
|
258
365
|
items.push({
|
|
259
366
|
group: '权限管理',
|
|
260
367
|
commands: [
|
|
261
|
-
{ cmd: '/perm', args: '[mode|allow|deny]', label: '权限模式管理' },
|
|
368
|
+
{ cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
|
|
262
369
|
]
|
|
263
370
|
});
|
|
264
371
|
items.push({
|
|
@@ -300,7 +407,7 @@ export class CommandHandler {
|
|
|
300
407
|
* 主命令处理入口
|
|
301
408
|
*/
|
|
302
409
|
async handle(content, channel, channelId, sendMessage, userId, threadId) {
|
|
303
|
-
//
|
|
410
|
+
// 解析身份(按实例名)
|
|
304
411
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
305
412
|
const policy = this.getPolicy(channel);
|
|
306
413
|
// 按当前会话选择 agent 后端
|
|
@@ -335,7 +442,7 @@ export class CommandHandler {
|
|
|
335
442
|
}
|
|
336
443
|
}
|
|
337
444
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
338
|
-
const requiresIdle = ['/new', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind'];
|
|
445
|
+
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
|
|
339
446
|
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
340
447
|
if (threadId) {
|
|
341
448
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
@@ -370,7 +477,7 @@ export class CommandHandler {
|
|
|
370
477
|
}
|
|
371
478
|
const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd));
|
|
372
479
|
if (!isCmd)
|
|
373
|
-
return
|
|
480
|
+
return undefined;
|
|
374
481
|
// /help 命令不需要会话
|
|
375
482
|
if (normalizedContent === '/help') {
|
|
376
483
|
if (!isAdmin) {
|
|
@@ -380,6 +487,7 @@ export class CommandHandler {
|
|
|
380
487
|
'🔄 会话管理:',
|
|
381
488
|
' /new [名称] - 创建新会话(可选命名)',
|
|
382
489
|
' /slist - 列出当前项目的所有会话',
|
|
490
|
+
' /slist cli - 列出 CLI 会话(未导入的)',
|
|
383
491
|
' /s, /session <名称|序号|uuid> - 切换到指定会话',
|
|
384
492
|
' /name, /rename <新名称> - 重命名当前会话',
|
|
385
493
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
@@ -411,12 +519,13 @@ export class CommandHandler {
|
|
|
411
519
|
'',
|
|
412
520
|
'🤖 Agent 与模型:',
|
|
413
521
|
' /agent [name] - 查看或切换 Agent 后端',
|
|
414
|
-
' /model [model]
|
|
522
|
+
' /model [model] - 查看或切换模型',
|
|
523
|
+
' /effort [level] - 查看或切换推理强度',
|
|
415
524
|
'',
|
|
416
525
|
'🔐 权限管理:',
|
|
417
526
|
' /perm - 查看当前权限模式',
|
|
418
|
-
' /perm <
|
|
419
|
-
' /perm allow|deny - 审批权限请求',
|
|
527
|
+
' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
|
|
528
|
+
' /perm allow|always|deny - 审批权限请求',
|
|
420
529
|
'',
|
|
421
530
|
'🛠️ 运维:',
|
|
422
531
|
' /status - 显示会话状态',
|
|
@@ -445,21 +554,60 @@ export class CommandHandler {
|
|
|
445
554
|
if (!hasPermissionController(permAgent)) {
|
|
446
555
|
return '❌ 权限控制不可用';
|
|
447
556
|
}
|
|
448
|
-
const currentMode = permSession.metadata?.permissionMode
|
|
557
|
+
const currentMode = permSession.metadata?.permissionMode ?? 'bypass';
|
|
449
558
|
const modes = permAgent.listModes();
|
|
559
|
+
// 尝试发送交互卡片
|
|
560
|
+
if (this.interactionRouter) {
|
|
561
|
+
const requestId = `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
562
|
+
const availableModes = modes.filter(m => m.available);
|
|
563
|
+
const interaction = {
|
|
564
|
+
type: 'interaction',
|
|
565
|
+
id: requestId,
|
|
566
|
+
channelId,
|
|
567
|
+
sessionId: permSession.id,
|
|
568
|
+
kind: {
|
|
569
|
+
kind: 'action',
|
|
570
|
+
title: '🔐 权限模式',
|
|
571
|
+
body: availableModes.map(m => `${m.key === currentMode ? '▶' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
|
|
572
|
+
buttons: availableModes.map(m => ({
|
|
573
|
+
key: m.key,
|
|
574
|
+
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
575
|
+
style: m.key === currentMode ? 'primary' : 'default',
|
|
576
|
+
})),
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
const replyCtx = this.getReplyContext(permSession);
|
|
580
|
+
const cardSent = await this.sendInteractionCard({
|
|
581
|
+
channel, channelId, sessionId: permSession.id, requestId, interaction, replyCtx,
|
|
582
|
+
callback: async (action, _values, operatorId) => {
|
|
583
|
+
if (action !== currentMode) {
|
|
584
|
+
if (userId && operatorId && operatorId !== userId)
|
|
585
|
+
return;
|
|
586
|
+
const result = await this.handle(`/perm ${action}`, channel, channelId, undefined, userId, threadId);
|
|
587
|
+
if (result) {
|
|
588
|
+
const adapter = this.adapters.get(channel);
|
|
589
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (cardSent)
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
// 降级:文本
|
|
450
598
|
const modeList = modes.map(m => {
|
|
451
599
|
const prefix = m.key === currentMode ? '▶' : ' ';
|
|
452
600
|
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
453
601
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
454
602
|
}).join('\n');
|
|
455
|
-
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式>
|
|
603
|
+
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
|
|
456
604
|
}
|
|
457
605
|
const parts = args.split(/\s+/);
|
|
458
|
-
// /perm <mode> 或 /perm allow|deny:切换模式 / 快捷审批
|
|
606
|
+
// /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
|
|
459
607
|
if (parts.length === 1) {
|
|
460
608
|
const arg = parts[0];
|
|
461
|
-
// /perm allow|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
|
|
462
|
-
if (arg === 'allow' || arg === 'deny') {
|
|
609
|
+
// /perm allow|always|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
|
|
610
|
+
if (arg === 'allow' || arg === 'always' || arg === 'deny') {
|
|
463
611
|
if (!this.permissionGateway) {
|
|
464
612
|
return '❌ 权限审批未启用';
|
|
465
613
|
}
|
|
@@ -471,8 +619,14 @@ export class CommandHandler {
|
|
|
471
619
|
return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}`;
|
|
472
620
|
}
|
|
473
621
|
const requestId = pendingIds[0];
|
|
474
|
-
|
|
475
|
-
|
|
622
|
+
const decision = arg;
|
|
623
|
+
this.permissionGateway.resolvePermission(permSession.id, requestId, decision);
|
|
624
|
+
const labels = {
|
|
625
|
+
allow: '✓ 已授权(本次),继续执行……',
|
|
626
|
+
always: '✓ 已授权(始终允许该工具),继续执行……',
|
|
627
|
+
deny: '✓ 已拒绝'
|
|
628
|
+
};
|
|
629
|
+
return labels[decision];
|
|
476
630
|
}
|
|
477
631
|
// /perm <mode>:切换权限模式
|
|
478
632
|
if (hasPermissionController(permAgent)) {
|
|
@@ -482,6 +636,10 @@ export class CommandHandler {
|
|
|
482
636
|
if (!matched.available) {
|
|
483
637
|
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
484
638
|
}
|
|
639
|
+
// guest 用户只能保持 readonly 模式
|
|
640
|
+
if (identity.role !== 'owner' && arg !== 'readonly') {
|
|
641
|
+
return '❌ 当前身份无法切换权限模式';
|
|
642
|
+
}
|
|
485
643
|
const metadata = permSession.metadata || {};
|
|
486
644
|
metadata.permissionMode = arg;
|
|
487
645
|
await this.sessionManager.updateSession(permSession.id, { metadata });
|
|
@@ -489,12 +647,12 @@ export class CommandHandler {
|
|
|
489
647
|
}
|
|
490
648
|
}
|
|
491
649
|
// 不是已知模式名也不是 allow/deny
|
|
492
|
-
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : '
|
|
493
|
-
return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|deny`;
|
|
650
|
+
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
651
|
+
return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny`;
|
|
494
652
|
}
|
|
495
653
|
// 双参数不再支持,提示正确用法
|
|
496
|
-
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : '
|
|
497
|
-
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|deny`;
|
|
654
|
+
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
|
|
655
|
+
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
|
|
498
656
|
}
|
|
499
657
|
// /agent 命令:查看或切换 Agent 后端
|
|
500
658
|
if (normalizedContent.startsWith('/agent')) {
|
|
@@ -504,7 +662,44 @@ export class CommandHandler {
|
|
|
504
662
|
const available = [...this.agentMap.keys()];
|
|
505
663
|
if (!args) {
|
|
506
664
|
const currentAgent = activeSession?.agentId || this.defaultAgentId;
|
|
507
|
-
|
|
665
|
+
// 尝试发送交互卡片
|
|
666
|
+
if (this.interactionRouter && available.length > 1) {
|
|
667
|
+
const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
668
|
+
const interaction = {
|
|
669
|
+
type: 'interaction',
|
|
670
|
+
id: requestId,
|
|
671
|
+
channelId,
|
|
672
|
+
sessionId: activeSession?.id || requestId,
|
|
673
|
+
kind: {
|
|
674
|
+
kind: 'action',
|
|
675
|
+
title: '🔌 切换 Agent',
|
|
676
|
+
buttons: available.map(a => ({
|
|
677
|
+
key: a,
|
|
678
|
+
label: a === currentAgent ? `✓ ${a}` : a,
|
|
679
|
+
style: a === currentAgent ? 'primary' : 'default',
|
|
680
|
+
})),
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
684
|
+
const cardSent = await this.sendInteractionCard({
|
|
685
|
+
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
686
|
+
callback: async (action, _values, operatorId) => {
|
|
687
|
+
if (action !== currentAgent) {
|
|
688
|
+
if (userId && operatorId && operatorId !== userId)
|
|
689
|
+
return;
|
|
690
|
+
const result = await this.handle(`/agent ${action}`, channel, channelId, undefined, userId, threadId);
|
|
691
|
+
if (result) {
|
|
692
|
+
const adapter = this.adapters.get(channel);
|
|
693
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
if (cardSent)
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
// 降级:文本
|
|
702
|
+
const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
|
|
508
703
|
return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
|
|
509
704
|
}
|
|
510
705
|
if (!this.agentMap.has(args)) {
|
|
@@ -514,15 +709,19 @@ export class CommandHandler {
|
|
|
514
709
|
if ('error' in result)
|
|
515
710
|
return result.error;
|
|
516
711
|
const { session } = result;
|
|
517
|
-
// 取消原会话的 pending
|
|
712
|
+
// 取消原会话的 pending 权限请求和交互卡片
|
|
518
713
|
if (this.permissionGateway) {
|
|
519
714
|
this.permissionGateway.cancelAll(session.id);
|
|
520
715
|
}
|
|
716
|
+
if (this.interactionRouter) {
|
|
717
|
+
this.interactionRouter.cancelAll(session.id);
|
|
718
|
+
}
|
|
521
719
|
// 切换到目标 agent(恢复已有会话或创建新会话)
|
|
522
720
|
const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
|
|
523
721
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
524
722
|
const projectName = this.getProjectName(session.projectPath);
|
|
525
|
-
|
|
723
|
+
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
|
|
724
|
+
return agentSwitchResponse;
|
|
526
725
|
}
|
|
527
726
|
// /model 命令:查看或切换模型/推理强度
|
|
528
727
|
if (normalizedContent.startsWith('/model')) {
|
|
@@ -536,54 +735,72 @@ export class CommandHandler {
|
|
|
536
735
|
const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
|
|
537
736
|
if (!args) {
|
|
538
737
|
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
738
|
+
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
539
739
|
const currentEffort = modelAgent.getEffort?.() || 'auto';
|
|
540
|
-
|
|
740
|
+
// 尝试发送交互卡片
|
|
741
|
+
if (this.interactionRouter && models.length > 0) {
|
|
742
|
+
const requestId = `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
743
|
+
const interaction = {
|
|
744
|
+
type: 'interaction',
|
|
745
|
+
id: requestId,
|
|
746
|
+
channelId,
|
|
747
|
+
sessionId: modelSession.id,
|
|
748
|
+
kind: {
|
|
749
|
+
kind: 'action',
|
|
750
|
+
title: '🤖 切换模型',
|
|
751
|
+
buttons: models.map((m) => ({
|
|
752
|
+
key: m,
|
|
753
|
+
label: m === currentModel ? `✓ ${m}` : m,
|
|
754
|
+
style: m === currentModel ? 'primary' : 'default',
|
|
755
|
+
})),
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
const replyCtx = this.getReplyContext(modelSession);
|
|
759
|
+
const cardSent = await this.sendInteractionCard({
|
|
760
|
+
channel, channelId, sessionId: modelSession.id, requestId, interaction, replyCtx,
|
|
761
|
+
callback: async (action, _values, operatorId) => {
|
|
762
|
+
if (action !== currentModel) {
|
|
763
|
+
if (userId && operatorId && operatorId !== userId)
|
|
764
|
+
return;
|
|
765
|
+
const result = await this.handle(`/model ${action}`, channel, channelId, undefined, userId, threadId);
|
|
766
|
+
if (result) {
|
|
767
|
+
const adapter = this.adapters.get(channel);
|
|
768
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
if (cardSent)
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
// 降级:文本
|
|
541
777
|
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
542
|
-
|
|
778
|
+
const effortHint = efforts.length > 0
|
|
779
|
+
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
780
|
+
: '';
|
|
781
|
+
return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
|
|
543
782
|
}
|
|
544
783
|
const parts = args.split(/\s+/);
|
|
545
784
|
let newModel;
|
|
546
785
|
let newEffort;
|
|
547
786
|
if (parts.length === 1) {
|
|
548
787
|
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默认)';
|
|
788
|
+
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
789
|
+
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
790
|
+
// effort 相关参数统一转发到 /effort
|
|
791
|
+
if (efforts.includes(arg) || arg === 'auto') {
|
|
792
|
+
return this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
|
|
576
793
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
newEffort = arg;
|
|
794
|
+
else if (allEfforts.includes(arg)) {
|
|
795
|
+
return `⚠️ 请使用 /effort ${arg} 调整推理强度`;
|
|
580
796
|
}
|
|
581
797
|
else if (models.includes(arg)) {
|
|
582
798
|
newModel = arg;
|
|
583
799
|
}
|
|
584
800
|
else {
|
|
585
801
|
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
586
|
-
|
|
802
|
+
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
803
|
+
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
|
|
587
804
|
}
|
|
588
805
|
}
|
|
589
806
|
else {
|
|
@@ -592,8 +809,13 @@ export class CommandHandler {
|
|
|
592
809
|
if (!models.includes(modelArg)) {
|
|
593
810
|
return `❌ 无效的模型ID: ${modelArg}`;
|
|
594
811
|
}
|
|
595
|
-
|
|
596
|
-
|
|
812
|
+
const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
|
|
813
|
+
if (targetEfforts.length === 0) {
|
|
814
|
+
return `⚠️ ${modelArg} 不支持推理强度设置`;
|
|
815
|
+
}
|
|
816
|
+
if (!targetEfforts.includes(effortArg)) {
|
|
817
|
+
const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
|
|
818
|
+
return `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}`;
|
|
597
819
|
}
|
|
598
820
|
newModel = modelArg;
|
|
599
821
|
newEffort = effortArg;
|
|
@@ -613,12 +835,8 @@ export class CommandHandler {
|
|
|
613
835
|
changes.push(`模型: ${newModel}`);
|
|
614
836
|
}
|
|
615
837
|
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
838
|
modelAgent.setEffort?.(newEffort);
|
|
621
|
-
changes.push(`推理强度: ${newEffort}
|
|
839
|
+
changes.push(`推理强度: ${newEffort}`);
|
|
622
840
|
}
|
|
623
841
|
// 持久化:写回来源(就近原则)
|
|
624
842
|
// evolclaw.json 配了 → 写 evolclaw.json
|
|
@@ -685,6 +903,137 @@ export class CommandHandler {
|
|
|
685
903
|
}
|
|
686
904
|
return `✓ 已切换\n ${changes.join('\n ')}`;
|
|
687
905
|
}
|
|
906
|
+
// /effort 命令:查看或切换推理强度
|
|
907
|
+
if (normalizedContent.startsWith('/effort')) {
|
|
908
|
+
const args = normalizedContent.slice(7).trim();
|
|
909
|
+
const effortResult = await this.ensureSession(channel, channelId, threadId);
|
|
910
|
+
if ('error' in effortResult)
|
|
911
|
+
return effortResult.error;
|
|
912
|
+
const { session: effortSession } = effortResult;
|
|
913
|
+
const effortAgent = this.getAgent(effortSession.agentId);
|
|
914
|
+
const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
|
|
915
|
+
const efforts = getAvailableEfforts(effortAgent, currentModel);
|
|
916
|
+
const currentEffort = effortAgent.getEffort?.() || 'auto';
|
|
917
|
+
if (efforts.length === 0) {
|
|
918
|
+
return '⚠️ 当前模型不支持推理强度设置';
|
|
919
|
+
}
|
|
920
|
+
if (!args) {
|
|
921
|
+
// /effort(无参数):显示当前推理强度 + 发送 Action 卡片
|
|
922
|
+
if (this.interactionRouter) {
|
|
923
|
+
const requestId = `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
924
|
+
const buttons = [
|
|
925
|
+
...efforts.map(e => ({
|
|
926
|
+
key: e,
|
|
927
|
+
label: e === currentEffort ? `✓ ${e}` : e,
|
|
928
|
+
style: e === currentEffort ? 'primary' : 'default',
|
|
929
|
+
})),
|
|
930
|
+
{
|
|
931
|
+
key: 'auto',
|
|
932
|
+
label: currentEffort === 'auto' ? '✓ auto' : 'auto',
|
|
933
|
+
style: currentEffort === 'auto' ? 'primary' : 'default',
|
|
934
|
+
},
|
|
935
|
+
];
|
|
936
|
+
const interaction = {
|
|
937
|
+
type: 'interaction',
|
|
938
|
+
id: requestId,
|
|
939
|
+
channelId,
|
|
940
|
+
sessionId: effortSession.id,
|
|
941
|
+
kind: {
|
|
942
|
+
kind: 'action',
|
|
943
|
+
title: '⚡ 推理强度',
|
|
944
|
+
buttons,
|
|
945
|
+
},
|
|
946
|
+
};
|
|
947
|
+
const replyCtx = this.getReplyContext(effortSession);
|
|
948
|
+
const cardSent = await this.sendInteractionCard({
|
|
949
|
+
channel, channelId, sessionId: effortSession.id, requestId, interaction, replyCtx,
|
|
950
|
+
callback: async (action, _values, operatorId) => {
|
|
951
|
+
if (action !== currentEffort) {
|
|
952
|
+
if (userId && operatorId && operatorId !== userId)
|
|
953
|
+
return;
|
|
954
|
+
const result = await this.handle(`/effort ${action}`, channel, channelId, undefined, userId, threadId);
|
|
955
|
+
if (result) {
|
|
956
|
+
const adapter = this.adapters.get(channel);
|
|
957
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
if (cardSent)
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
// 降级:文本
|
|
966
|
+
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
967
|
+
const effortList = efforts.map(e => `${e === currentEffort ? ' ✓' : ' '} ${e}`).join('\n');
|
|
968
|
+
return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n ${currentEffort === 'auto' ? ' ✓' : ' '} auto\n\n用法: /effort <level>`;
|
|
969
|
+
}
|
|
970
|
+
// /effort auto:恢复 SDK 默认
|
|
971
|
+
if (args === 'auto') {
|
|
972
|
+
effortAgent.setEffort?.(undefined);
|
|
973
|
+
const isCodex = effortAgent.name === 'codex';
|
|
974
|
+
if (isCodex) {
|
|
975
|
+
if (this.config.agents?.openai?.reasoning) {
|
|
976
|
+
delete this.config.agents.openai.reasoning;
|
|
977
|
+
try {
|
|
978
|
+
saveConfig(this.config);
|
|
979
|
+
}
|
|
980
|
+
catch { }
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
|
|
985
|
+
if (configuredInEvolclaw) {
|
|
986
|
+
delete this.config.agents.anthropic.effort;
|
|
987
|
+
try {
|
|
988
|
+
saveConfig(this.config);
|
|
989
|
+
}
|
|
990
|
+
catch { }
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
writeUserSettings({ effortLevel: null });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return '✓ 推理强度已恢复为 auto (SDK默认)';
|
|
997
|
+
}
|
|
998
|
+
// /effort <level>:切换推理强度
|
|
999
|
+
if (!efforts.includes(args)) {
|
|
1000
|
+
if (allEfforts.includes(args)) {
|
|
1001
|
+
return `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}`;
|
|
1002
|
+
}
|
|
1003
|
+
return `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto`;
|
|
1004
|
+
}
|
|
1005
|
+
const newEffort = args;
|
|
1006
|
+
effortAgent.setEffort?.(newEffort);
|
|
1007
|
+
// 持久化
|
|
1008
|
+
if (!this.config.agents)
|
|
1009
|
+
this.config.agents = {};
|
|
1010
|
+
const isCodex = effortAgent.name === 'codex';
|
|
1011
|
+
if (isCodex) {
|
|
1012
|
+
if (!this.config.agents.openai)
|
|
1013
|
+
this.config.agents.openai = {};
|
|
1014
|
+
this.config.agents.openai.reasoning = newEffort;
|
|
1015
|
+
try {
|
|
1016
|
+
saveConfig(this.config);
|
|
1017
|
+
}
|
|
1018
|
+
catch { }
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
|
|
1022
|
+
if (configuredInEvolclaw) {
|
|
1023
|
+
if (!this.config.agents.anthropic)
|
|
1024
|
+
this.config.agents.anthropic = {};
|
|
1025
|
+
this.config.agents.anthropic.effort = newEffort;
|
|
1026
|
+
try {
|
|
1027
|
+
saveConfig(this.config);
|
|
1028
|
+
}
|
|
1029
|
+
catch { }
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
writeUserSettings({ effortLevel: newEffort });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return `✓ 推理强度: ${newEffort}`;
|
|
1036
|
+
}
|
|
688
1037
|
// /stop 命令:中断当前任务
|
|
689
1038
|
if (normalizedContent === '/stop') {
|
|
690
1039
|
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -827,7 +1176,7 @@ export class CommandHandler {
|
|
|
827
1176
|
}
|
|
828
1177
|
const lines = [];
|
|
829
1178
|
if (isAdmin) {
|
|
830
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
1179
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
831
1180
|
if (health.consecutiveErrors > 0) {
|
|
832
1181
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
833
1182
|
}
|
|
@@ -875,6 +1224,9 @@ export class CommandHandler {
|
|
|
875
1224
|
timestamp: Date.now()
|
|
876
1225
|
});
|
|
877
1226
|
if (session) {
|
|
1227
|
+
// Reset agent backend state so the new
|
|
1228
|
+
// session starts with a fresh conversation history
|
|
1229
|
+
await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
878
1230
|
await agent.closeSession(session.id);
|
|
879
1231
|
}
|
|
880
1232
|
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /slist 查看`;
|
|
@@ -903,15 +1255,30 @@ export class CommandHandler {
|
|
|
903
1255
|
}
|
|
904
1256
|
// Default: show full system health check
|
|
905
1257
|
const lines = ['📡 渠道状态:'];
|
|
1258
|
+
// Group by channelType
|
|
1259
|
+
const groups = new Map();
|
|
906
1260
|
for (const [name] of this.adapters) {
|
|
1261
|
+
const type = this.channelTypeMap.get(name) || name;
|
|
907
1262
|
const ch = this.channelObjects.get(name);
|
|
1263
|
+
let status;
|
|
908
1264
|
if (ch?.getStatus) {
|
|
909
1265
|
const s = ch.getStatus();
|
|
910
|
-
|
|
911
|
-
|
|
1266
|
+
status = s.connected ? '✓ 已连接' : s.reconnectAttempt > 0 ? `⏳ 重连中 (${s.reconnectAttempt}/${s.maxAttempts})` : '✗ 断开';
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
status = '✓ 已注册';
|
|
1270
|
+
}
|
|
1271
|
+
if (!groups.has(type))
|
|
1272
|
+
groups.set(type, []);
|
|
1273
|
+
groups.get(type).push({ name, status });
|
|
1274
|
+
}
|
|
1275
|
+
for (const [type, instances] of groups) {
|
|
1276
|
+
if (instances.length === 1) {
|
|
1277
|
+
lines.push(` ${instances[0].name}: ${instances[0].status}`);
|
|
912
1278
|
}
|
|
913
1279
|
else {
|
|
914
|
-
|
|
1280
|
+
const parts = instances.map(i => `${i.name} ${i.status}`);
|
|
1281
|
+
lines.push(` ${type}: [${parts.join(', ')}]`);
|
|
915
1282
|
}
|
|
916
1283
|
}
|
|
917
1284
|
// 队列状态
|
|
@@ -939,6 +1306,10 @@ export class CommandHandler {
|
|
|
939
1306
|
else {
|
|
940
1307
|
lines.push(` 处理出错: 0`);
|
|
941
1308
|
}
|
|
1309
|
+
if (h.toolErrors > 0) {
|
|
1310
|
+
const toolBreakdown = Object.entries(h.toolErrorsByName).map(([t, c]) => `${t}: ${c}`).join(', ');
|
|
1311
|
+
lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
|
|
1312
|
+
}
|
|
942
1313
|
lines.push(` 被中断: ${h.interrupts}`);
|
|
943
1314
|
lines.push(` 进入安全模式: ${h.safeModeEntries}`);
|
|
944
1315
|
if (h.completed > 0) {
|
|
@@ -957,6 +1328,33 @@ export class CommandHandler {
|
|
|
957
1328
|
const count = this.messageCache.getCount(s.id);
|
|
958
1329
|
return `${s.projectPath} 有 ${count} 条新消息`;
|
|
959
1330
|
});
|
|
1331
|
+
// 执行重启逻辑(共用于卡片回调和文本确认)
|
|
1332
|
+
const executeRestart = async () => {
|
|
1333
|
+
let replyContext;
|
|
1334
|
+
if (threadId) {
|
|
1335
|
+
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
1336
|
+
replyContext = this.getReplyContext(threadSession);
|
|
1337
|
+
}
|
|
1338
|
+
const restartInfo = {
|
|
1339
|
+
channel,
|
|
1340
|
+
channelId,
|
|
1341
|
+
timestamp: Date.now(),
|
|
1342
|
+
...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
|
|
1343
|
+
};
|
|
1344
|
+
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
1345
|
+
const { spawn } = await import('child_process');
|
|
1346
|
+
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
|
|
1347
|
+
detached: true,
|
|
1348
|
+
stdio: 'ignore',
|
|
1349
|
+
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
1350
|
+
}).unref();
|
|
1351
|
+
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
1352
|
+
setTimeout(() => {
|
|
1353
|
+
logger.info('[System] Restarting by user command...');
|
|
1354
|
+
process.exit(0);
|
|
1355
|
+
}, 1000);
|
|
1356
|
+
};
|
|
1357
|
+
// 文本确认流程
|
|
960
1358
|
if (sessionsWithMessages.length > 0) {
|
|
961
1359
|
const restartKey = `${channel}-${channelId}`;
|
|
962
1360
|
const restartConfirmFile = path.join(resolvePaths().dataDir, `restart-confirm-${restartKey}.json`);
|
|
@@ -976,30 +1374,7 @@ export class CommandHandler {
|
|
|
976
1374
|
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
977
1375
|
}
|
|
978
1376
|
}
|
|
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);
|
|
1377
|
+
await executeRestart();
|
|
1003
1378
|
return '🔄 服务正在重启,请稍候...(约 5 秒后恢复)';
|
|
1004
1379
|
}
|
|
1005
1380
|
// /pwd 命令:显示当前项目路径
|
|
@@ -1022,26 +1397,43 @@ export class CommandHandler {
|
|
|
1022
1397
|
if (!rawArg) {
|
|
1023
1398
|
return '用法: /send <相对路径> 或 /send <渠道> <相对路径>\n示例: /send src/index.ts\n示例: /send feishu report.md';
|
|
1024
1399
|
}
|
|
1025
|
-
// 解析目标通道:第一个 token
|
|
1400
|
+
// 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
|
|
1026
1401
|
const tokens = rawArg.split(/\s+/);
|
|
1027
|
-
const knownChannels = [...this.adapters.keys()];
|
|
1028
1402
|
let targetChannel = channel;
|
|
1403
|
+
let targetLabel = channel;
|
|
1029
1404
|
let filePath = rawArg;
|
|
1030
|
-
if (tokens.length >= 2
|
|
1031
|
-
|
|
1032
|
-
|
|
1405
|
+
if (tokens.length >= 2) {
|
|
1406
|
+
const spec = tokens[0];
|
|
1407
|
+
if (this.adapters.has(spec)) {
|
|
1408
|
+
// 精确实例名
|
|
1409
|
+
targetChannel = spec;
|
|
1410
|
+
targetLabel = spec;
|
|
1411
|
+
filePath = tokens.slice(1).join(' ');
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
// 按 channelType 查找第一个匹配的实例
|
|
1415
|
+
for (const [name] of this.adapters) {
|
|
1416
|
+
if ((this.channelTypeMap.get(name) || name) === spec) {
|
|
1417
|
+
targetChannel = name;
|
|
1418
|
+
targetLabel = spec;
|
|
1419
|
+
filePath = tokens.slice(1).join(' ');
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1033
1424
|
}
|
|
1425
|
+
const isCrossChannel = targetChannel !== channel;
|
|
1034
1426
|
// 跨通道仅限 owner
|
|
1035
|
-
if (
|
|
1427
|
+
if (isCrossChannel && identity.role !== 'owner') {
|
|
1036
1428
|
return '❌ 跨通道发送仅限管理员';
|
|
1037
1429
|
}
|
|
1038
1430
|
// 找目标 adapter
|
|
1039
1431
|
const targetAdapter = this.adapters.get(targetChannel);
|
|
1040
1432
|
if (!targetAdapter) {
|
|
1041
|
-
return `❌ 通道 ${
|
|
1433
|
+
return `❌ 通道 ${targetLabel} 未启用或不存在`;
|
|
1042
1434
|
}
|
|
1043
1435
|
if (!targetAdapter.sendFile) {
|
|
1044
|
-
return `❌ 通道 ${
|
|
1436
|
+
return `❌ 通道 ${targetLabel} 不支持文件发送`;
|
|
1045
1437
|
}
|
|
1046
1438
|
// 获取 session(需要 projectPath)
|
|
1047
1439
|
const sendResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -1076,22 +1468,22 @@ export class CommandHandler {
|
|
|
1076
1468
|
}
|
|
1077
1469
|
// 找目标 channelId
|
|
1078
1470
|
let targetChannelId = channelId;
|
|
1079
|
-
if (
|
|
1471
|
+
if (isCrossChannel) {
|
|
1080
1472
|
const ownerPeerId = getOwner(this.config, targetChannel);
|
|
1081
1473
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
|
|
1082
1474
|
if (!targetChannelId) {
|
|
1083
|
-
return `❌ 未找到 ${
|
|
1475
|
+
return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
|
|
1084
1476
|
}
|
|
1085
1477
|
}
|
|
1086
1478
|
// 发送文件
|
|
1087
1479
|
try {
|
|
1088
|
-
const replyCtx =
|
|
1480
|
+
const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
|
|
1089
1481
|
await targetAdapter.sendFile(targetChannelId, realPath, replyCtx);
|
|
1090
1482
|
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
1091
1483
|
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
1092
1484
|
: `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
|
|
1093
|
-
return
|
|
1094
|
-
? `📎 文件已通过 ${
|
|
1485
|
+
return isCrossChannel
|
|
1486
|
+
? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
|
|
1095
1487
|
: `✅ 已发送: ${filePath} (${sizeStr})`;
|
|
1096
1488
|
}
|
|
1097
1489
|
catch (error) {
|
|
@@ -1115,73 +1507,110 @@ export class CommandHandler {
|
|
|
1115
1507
|
|
|
1116
1508
|
提示:群聊不支持切换项目`;
|
|
1117
1509
|
}
|
|
1118
|
-
|
|
1119
|
-
// 收集项目信息并按最近活跃排序
|
|
1510
|
+
// 收集项目信息并按最近活跃排序(唯一来源:evolclaw.json projects.list)
|
|
1120
1511
|
const entries = [];
|
|
1121
|
-
const configuredPaths = new Set();
|
|
1122
1512
|
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1513
|
+
// 跳过不存在的路径
|
|
1514
|
+
if (!fs.existsSync(projectPath))
|
|
1515
|
+
continue;
|
|
1516
|
+
const isCurrent = session ? path.resolve(session.projectPath) === path.resolve(projectPath) : false;
|
|
1125
1517
|
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
1126
1518
|
entries.push({
|
|
1127
1519
|
name, projectPath, projectSession, isCurrent,
|
|
1128
1520
|
updatedAt: projectSession?.updatedAt ?? 0,
|
|
1129
1521
|
});
|
|
1130
1522
|
}
|
|
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
1523
|
// 当前活跃项目置顶,其余按 updatedAt 降序
|
|
1144
1524
|
entries.sort((a, b) => {
|
|
1145
1525
|
if (a.isCurrent !== b.isCurrent)
|
|
1146
1526
|
return a.isCurrent ? -1 : 1;
|
|
1147
1527
|
return b.updatedAt - a.updatedAt;
|
|
1148
1528
|
});
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const statusParts = [];
|
|
1529
|
+
// 构建项目状态文本的辅助函数
|
|
1530
|
+
const buildStatusText = (entry) => {
|
|
1531
|
+
const { projectSession, isCurrent } = entry;
|
|
1532
|
+
if (!projectSession)
|
|
1533
|
+
return '无会话';
|
|
1534
|
+
const parts = [];
|
|
1156
1535
|
if (isCurrent) {
|
|
1157
|
-
|
|
1536
|
+
parts.push('活跃');
|
|
1158
1537
|
}
|
|
1159
1538
|
else {
|
|
1160
|
-
|
|
1161
|
-
statusParts.push(formatIdleTime(idleMs));
|
|
1539
|
+
parts.push(formatIdleTime(Date.now() - projectSession.updatedAt));
|
|
1162
1540
|
}
|
|
1163
|
-
// 用 DB processingState 判断处理状态
|
|
1164
1541
|
const isProcessing = !!projectSession.processingState;
|
|
1165
1542
|
if (isProcessing) {
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
statusParts.push(`[处理中,队列${queueLength}条]`);
|
|
1169
|
-
}
|
|
1170
|
-
else {
|
|
1171
|
-
statusParts.push('[处理中]');
|
|
1172
|
-
}
|
|
1543
|
+
const qLen = this.messageQueue.getQueueLength(projectSession.id);
|
|
1544
|
+
parts.push(qLen > 0 ? `[处理中,队列${qLen}条]` : '[处理中]');
|
|
1173
1545
|
}
|
|
1174
|
-
const
|
|
1175
|
-
if (
|
|
1176
|
-
|
|
1546
|
+
const unread = this.messageCache.getCount(projectSession.id);
|
|
1547
|
+
if (unread > 0) {
|
|
1548
|
+
parts.push(`[${unread}条新消息]`);
|
|
1177
1549
|
}
|
|
1178
1550
|
else if (!isProcessing && !isCurrent) {
|
|
1179
|
-
|
|
1551
|
+
parts.push('[空闲]');
|
|
1180
1552
|
}
|
|
1181
|
-
|
|
1553
|
+
return parts.join(' ');
|
|
1554
|
+
};
|
|
1555
|
+
// 尝试发送 ActionInteraction 卡片(每个项目一个按钮,一键切换)
|
|
1556
|
+
if (this.interactionRouter && entries.length > 0) {
|
|
1557
|
+
const requestId = `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1558
|
+
const buttons = entries.map(e => ({
|
|
1559
|
+
key: e.name,
|
|
1560
|
+
label: e.isCurrent ? `✓ ${e.name}` : e.name,
|
|
1561
|
+
style: e.isCurrent ? 'primary' : 'default',
|
|
1562
|
+
}));
|
|
1563
|
+
const bodyLines = entries.map(e => {
|
|
1564
|
+
const status = buildStatusText(e);
|
|
1565
|
+
const prefix = e.isCurrent ? '▶' : '•';
|
|
1566
|
+
return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
|
|
1567
|
+
});
|
|
1568
|
+
const interaction = {
|
|
1569
|
+
type: 'interaction',
|
|
1570
|
+
id: requestId,
|
|
1571
|
+
channelId,
|
|
1572
|
+
sessionId: activeSession?.id || requestId,
|
|
1573
|
+
kind: {
|
|
1574
|
+
kind: 'action',
|
|
1575
|
+
title: '📂 项目列表',
|
|
1576
|
+
body: bodyLines.join('\n'),
|
|
1577
|
+
buttons,
|
|
1578
|
+
},
|
|
1579
|
+
};
|
|
1580
|
+
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1581
|
+
const cardSent = await this.sendInteractionCard({
|
|
1582
|
+
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
1583
|
+
callback: async (action, _values, operatorId) => {
|
|
1584
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1585
|
+
return;
|
|
1586
|
+
const selectedEntry = entries.find(e => e.name === action);
|
|
1587
|
+
if (selectedEntry && !selectedEntry.isCurrent) {
|
|
1588
|
+
const result = await this.handle(`/project ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1589
|
+
if (result) {
|
|
1590
|
+
const adapter = this.adapters.get(channel);
|
|
1591
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
});
|
|
1596
|
+
if (cardSent)
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
// 降级:文本列表
|
|
1600
|
+
const lines = ['可用项目:'];
|
|
1601
|
+
for (const entry of entries) {
|
|
1602
|
+
const prefix = entry.isCurrent ? ' ✓' : ' ';
|
|
1603
|
+
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
1182
1604
|
}
|
|
1183
1605
|
return lines.join('\n');
|
|
1184
1606
|
}
|
|
1607
|
+
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
1608
|
+
if (normalizedContent === '/project') {
|
|
1609
|
+
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
1610
|
+
// 群聊不能切换项目,交由 /plist 逻辑处理
|
|
1611
|
+
}
|
|
1612
|
+
return this.handle('/plist', channel, channelId, undefined, userId, threadId);
|
|
1613
|
+
}
|
|
1185
1614
|
// /project 命令:切换项目(支持名称或路径)
|
|
1186
1615
|
if (normalizedContent.startsWith('/project ')) {
|
|
1187
1616
|
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
@@ -1307,8 +1736,10 @@ export class CommandHandler {
|
|
|
1307
1736
|
this.projects[projectName] = projectPath;
|
|
1308
1737
|
return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
|
|
1309
1738
|
}
|
|
1310
|
-
// /slist
|
|
1311
|
-
|
|
1739
|
+
// /slist 命令:列出当前项目的会话
|
|
1740
|
+
// /slist — 仅 EvolClaw 会话
|
|
1741
|
+
// /slist cli — 仅 CLI 会话(未导入的)
|
|
1742
|
+
if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
|
|
1312
1743
|
if (!session) {
|
|
1313
1744
|
return `❌ 当前没有活跃会话
|
|
1314
1745
|
|
|
@@ -1317,6 +1748,75 @@ export class CommandHandler {
|
|
|
1317
1748
|
2. /new [名称] - 创建命名会话
|
|
1318
1749
|
3. /project <项目> - 切换到指定项目`;
|
|
1319
1750
|
}
|
|
1751
|
+
const showCliOnly = normalizedContent === '/slist cli';
|
|
1752
|
+
// /slist cli — 仅显示 CLI 会话
|
|
1753
|
+
if (showCliOnly) {
|
|
1754
|
+
const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
|
|
1755
|
+
if (!canImportCli) {
|
|
1756
|
+
return '❌ 当前无权查看 CLI 会话';
|
|
1757
|
+
}
|
|
1758
|
+
const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
|
|
1759
|
+
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1760
|
+
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1761
|
+
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
1762
|
+
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
|
|
1763
|
+
if (orphanCliSessions.length === 0) {
|
|
1764
|
+
return `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话`;
|
|
1765
|
+
}
|
|
1766
|
+
// 构建显示数据(复用于卡片和文本)
|
|
1767
|
+
const cliDisplayItems = orphanCliSessions.map(c => {
|
|
1768
|
+
const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
1769
|
+
const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
|
|
1770
|
+
const uuid = c.uuid.substring(0, 8);
|
|
1771
|
+
return { uuid, fullUuid: c.uuid, time, message };
|
|
1772
|
+
});
|
|
1773
|
+
// 尝试发送 ActionInteraction 卡片
|
|
1774
|
+
if (this.interactionRouter && cliDisplayItems.length > 0) {
|
|
1775
|
+
const requestId = `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1776
|
+
const buttons = cliDisplayItems.map(item => ({
|
|
1777
|
+
key: item.uuid,
|
|
1778
|
+
label: item.uuid,
|
|
1779
|
+
style: 'default',
|
|
1780
|
+
}));
|
|
1781
|
+
const bodyLines = cliDisplayItems.map(item => `• ${item.time} (${item.uuid}) "${item.message}"`);
|
|
1782
|
+
const interaction = {
|
|
1783
|
+
type: 'interaction',
|
|
1784
|
+
id: requestId,
|
|
1785
|
+
channelId,
|
|
1786
|
+
sessionId: session.id,
|
|
1787
|
+
kind: {
|
|
1788
|
+
kind: 'action',
|
|
1789
|
+
title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
|
|
1790
|
+
body: bodyLines.join('\n'),
|
|
1791
|
+
buttons,
|
|
1792
|
+
},
|
|
1793
|
+
};
|
|
1794
|
+
const replyCtx = this.getReplyContext(session);
|
|
1795
|
+
const cardSent = await this.sendInteractionCard({
|
|
1796
|
+
channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
|
|
1797
|
+
callback: async (action, _values, operatorId) => {
|
|
1798
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1799
|
+
return;
|
|
1800
|
+
const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1801
|
+
if (result) {
|
|
1802
|
+
const adapter = this.adapters.get(channel);
|
|
1803
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
});
|
|
1807
|
+
if (cardSent)
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
// 降级:文本列表
|
|
1811
|
+
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
|
|
1812
|
+
for (const item of cliDisplayItems) {
|
|
1813
|
+
lines.push(` ${item.time} (${item.uuid}) "${item.message}"`);
|
|
1814
|
+
}
|
|
1815
|
+
lines.push('');
|
|
1816
|
+
lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
|
|
1817
|
+
return lines.join('\n');
|
|
1818
|
+
}
|
|
1819
|
+
// /slist — 仅显示 EvolClaw 会话
|
|
1320
1820
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1321
1821
|
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1322
1822
|
// 从 SDK 同步会话名称(发现 CLI 改名)
|
|
@@ -1335,46 +1835,97 @@ export class CommandHandler {
|
|
|
1335
1835
|
catch (error) {
|
|
1336
1836
|
logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
|
|
1337
1837
|
}
|
|
1338
|
-
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1838
|
+
// 构建可显示会话列表(复用于卡片和文本)
|
|
1839
|
+
const hideTopics = currentProjectSessions.length > 10;
|
|
1840
|
+
const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
|
|
1841
|
+
const maxDisplay = 10;
|
|
1842
|
+
const displaySessions = [];
|
|
1843
|
+
let displayIndex = 0;
|
|
1844
|
+
for (let i = 0; i < currentProjectSessions.length; i++) {
|
|
1845
|
+
const s = currentProjectSessions[i];
|
|
1846
|
+
if (hideTopics && s.threadId)
|
|
1847
|
+
continue;
|
|
1848
|
+
if (displayIndex >= maxDisplay)
|
|
1849
|
+
break;
|
|
1850
|
+
const isActive = s.metadata?.isActive === true;
|
|
1851
|
+
displayIndex++;
|
|
1852
|
+
const name = s.name || '(未命名)';
|
|
1853
|
+
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
1854
|
+
const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
|
|
1855
|
+
let status = '[空闲]';
|
|
1856
|
+
if (fileMissing) {
|
|
1857
|
+
status = '[会话文件缺失]';
|
|
1858
|
+
}
|
|
1859
|
+
else if (!!s.processingState) {
|
|
1860
|
+
status = '[处理中]';
|
|
1861
|
+
}
|
|
1862
|
+
else if (isActive) {
|
|
1863
|
+
status = '[活跃]';
|
|
1864
|
+
}
|
|
1865
|
+
displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
|
|
1866
|
+
}
|
|
1867
|
+
// 尝试发送 ActionInteraction 卡片(每个会话一个按钮,一键切换)
|
|
1868
|
+
if (this.interactionRouter && displaySessions.length >= 1) {
|
|
1869
|
+
const requestId = `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1870
|
+
const buttons = displaySessions.map(ds => {
|
|
1871
|
+
const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
|
|
1872
|
+
return {
|
|
1873
|
+
key: String(ds.index),
|
|
1874
|
+
label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
|
|
1875
|
+
style: ds.isActive ? 'primary' : 'default',
|
|
1876
|
+
};
|
|
1877
|
+
});
|
|
1878
|
+
const bodyLines = displaySessions.map(ds => {
|
|
1879
|
+
const prefix = ds.isActive ? '▶' : '•';
|
|
1880
|
+
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1881
|
+
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1882
|
+
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
1883
|
+
return `${prefix} ${ds.index}. ${threadTag}${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
|
|
1884
|
+
});
|
|
1885
|
+
const interaction = {
|
|
1886
|
+
type: 'interaction',
|
|
1887
|
+
id: requestId,
|
|
1888
|
+
channelId,
|
|
1889
|
+
sessionId: session.id,
|
|
1890
|
+
kind: {
|
|
1891
|
+
kind: 'action',
|
|
1892
|
+
title: `📋 ${path.basename(session.projectPath)} 会话列表`,
|
|
1893
|
+
body: bodyLines.join('\n'),
|
|
1894
|
+
buttons,
|
|
1895
|
+
},
|
|
1896
|
+
};
|
|
1897
|
+
const replyCtx = this.getReplyContext(session);
|
|
1898
|
+
const cardSent = await this.sendInteractionCard({
|
|
1899
|
+
channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
|
|
1900
|
+
callback: async (action, _values, operatorId) => {
|
|
1901
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1902
|
+
return;
|
|
1903
|
+
const target = displaySessions.find(ds => String(ds.index) === action);
|
|
1904
|
+
if (target && !target.isActive) {
|
|
1905
|
+
const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1906
|
+
if (result) {
|
|
1907
|
+
const adapter = this.adapters.get(channel);
|
|
1908
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
},
|
|
1912
|
+
});
|
|
1913
|
+
if (cardSent)
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
// 降级:文本列表
|
|
1343
1917
|
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
|
|
1344
1918
|
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} [会话文件缺失]`);
|
|
1919
|
+
for (const ds of displaySessions) {
|
|
1920
|
+
const prefix = ds.isActive ? ' ✓' : ' ';
|
|
1921
|
+
const num = `${ds.index}.`;
|
|
1922
|
+
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1923
|
+
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1924
|
+
if (ds.fileMissing) {
|
|
1925
|
+
lines.push(`${prefix} ${num} ${threadTag}❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
1367
1926
|
}
|
|
1368
1927
|
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}`);
|
|
1928
|
+
lines.push(`${prefix} ${num} ${threadTag}${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
1378
1929
|
}
|
|
1379
1930
|
}
|
|
1380
1931
|
const hiddenCount = currentProjectSessions.length - displayIndex - topicCount;
|
|
@@ -1388,29 +1939,19 @@ export class CommandHandler {
|
|
|
1388
1939
|
}
|
|
1389
1940
|
lines.push('');
|
|
1390
1941
|
}
|
|
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
1942
|
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
1943
|
+
lines.push('使用 /slist cli 查看 CLI 会话');
|
|
1403
1944
|
return lines.join('\n');
|
|
1404
1945
|
}
|
|
1946
|
+
// /session(无参数):直接复用 /slist 逻辑(含卡片交互)
|
|
1947
|
+
if (normalizedContent === '/session') {
|
|
1948
|
+
return this.handle('/slist', channel, channelId, undefined, userId, threadId);
|
|
1949
|
+
}
|
|
1405
1950
|
// /session 或 /s 命令:切换会话
|
|
1406
1951
|
if (normalizedContent.startsWith('/session ')) {
|
|
1407
1952
|
const sessionName = normalizedContent.slice(9).trim();
|
|
1408
1953
|
if (!sessionName)
|
|
1409
1954
|
return '用法: /s <序号、会话名称或前8位UUID>';
|
|
1410
|
-
const isProcessing = !!session?.processingState;
|
|
1411
|
-
if (isProcessing) {
|
|
1412
|
-
return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
|
|
1413
|
-
}
|
|
1414
1955
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
1415
1956
|
// 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
|
|
1416
1957
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
@@ -1586,7 +2127,7 @@ export class CommandHandler {
|
|
|
1586
2127
|
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
1587
2128
|
}
|
|
1588
2129
|
const repairAgent = this.getAgent(repairSession.agentId);
|
|
1589
|
-
const { checkSessionFile, backupSessionFile } = await import('
|
|
2130
|
+
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
1590
2131
|
try {
|
|
1591
2132
|
if (!repairSession.agentSessionId) {
|
|
1592
2133
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|