evolclaw 2.1.2 → 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 +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → 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 +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolvePaths, getPackageRoot } from '../config.js';
|
|
1
|
+
import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
|
|
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
|
-
const
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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');
|
|
14
32
|
}
|
|
15
33
|
/**
|
|
16
34
|
* 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
|
|
@@ -85,30 +103,50 @@ function formatIdleTime(ms) {
|
|
|
85
103
|
return '刚刚';
|
|
86
104
|
}
|
|
87
105
|
// 支持的命令列表
|
|
88
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del'];
|
|
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'];
|
|
89
107
|
// 命令别名映射
|
|
90
108
|
const aliases = {
|
|
91
109
|
'/p': '/project',
|
|
92
110
|
'/s': '/session',
|
|
93
111
|
'/name': '/rename'
|
|
94
112
|
};
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
// /stop 是快速命令:直接调用 agentRunner.interrupt(),不走队列(否则队列自动中断后 /stop 检测不到活跃任务)
|
|
98
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/del', '/p ', '/s ', '/name '];
|
|
113
|
+
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
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 '];
|
|
99
115
|
export class CommandHandler {
|
|
100
116
|
sessionManager;
|
|
101
|
-
agentRunner;
|
|
102
117
|
config;
|
|
103
118
|
messageCache;
|
|
119
|
+
eventBus;
|
|
104
120
|
adapters = new Map();
|
|
121
|
+
policies = new Map();
|
|
122
|
+
channelObjects = new Map(); // name → actual channel instance (for /check)
|
|
123
|
+
channelTypeMap = new Map(); // name → channelType (for grouping)
|
|
105
124
|
processor;
|
|
106
125
|
messageQueue;
|
|
107
|
-
|
|
126
|
+
permissionGateway;
|
|
127
|
+
interactionRouter;
|
|
128
|
+
statsCollector;
|
|
129
|
+
agentMap;
|
|
130
|
+
defaultAgentId;
|
|
131
|
+
/** 按 agentId 获取 agent,回退到默认 */
|
|
132
|
+
getAgent(agentId) {
|
|
133
|
+
if (agentId && this.agentMap.has(agentId))
|
|
134
|
+
return this.agentMap.get(agentId);
|
|
135
|
+
return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
|
|
136
|
+
}
|
|
137
|
+
constructor(sessionManager, agentRunnerOrMap, config, messageCache, eventBus, defaultAgentId) {
|
|
108
138
|
this.sessionManager = sessionManager;
|
|
109
|
-
this.agentRunner = agentRunner;
|
|
110
139
|
this.config = config;
|
|
111
140
|
this.messageCache = messageCache;
|
|
141
|
+
this.eventBus = eventBus;
|
|
142
|
+
if (agentRunnerOrMap instanceof Map) {
|
|
143
|
+
this.agentMap = agentRunnerOrMap;
|
|
144
|
+
this.defaultAgentId = defaultAgentId || 'claude';
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
|
|
148
|
+
this.defaultAgentId = agentRunnerOrMap.name;
|
|
149
|
+
}
|
|
112
150
|
}
|
|
113
151
|
/** 项目列表快捷访问 */
|
|
114
152
|
get projects() {
|
|
@@ -122,22 +160,104 @@ export class CommandHandler {
|
|
|
122
160
|
getProjectName(projectPath) {
|
|
123
161
|
return this.getConfiguredProjectName(projectPath) || path.basename(projectPath);
|
|
124
162
|
}
|
|
163
|
+
/** 格式化运行时间 */
|
|
164
|
+
formatUptime(ms) {
|
|
165
|
+
const sec = Math.floor(ms / 1000);
|
|
166
|
+
const d = Math.floor(sec / 86400);
|
|
167
|
+
const h = Math.floor((sec % 86400) / 3600);
|
|
168
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
169
|
+
const s = sec % 60;
|
|
170
|
+
const parts = [];
|
|
171
|
+
if (d > 0)
|
|
172
|
+
parts.push(`${d}天`);
|
|
173
|
+
if (h > 0)
|
|
174
|
+
parts.push(`${h}时`);
|
|
175
|
+
if (m > 0)
|
|
176
|
+
parts.push(`${m}分`);
|
|
177
|
+
if (parts.length === 0)
|
|
178
|
+
parts.push(`${s}秒`);
|
|
179
|
+
return parts.join('');
|
|
180
|
+
}
|
|
125
181
|
/** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
|
|
126
|
-
getQueueKey(session,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
getQueueKey(session, _channel, _channelId) {
|
|
183
|
+
// 队列和 agent 均使用 session.id 作为 key
|
|
184
|
+
return session?.id || '';
|
|
185
|
+
}
|
|
186
|
+
/** 从 session 提取渠道预构建的回复上下文 */
|
|
187
|
+
getReplyContext(session) {
|
|
188
|
+
return session.metadata?.replyContext;
|
|
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
|
+
}
|
|
130
226
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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;
|
|
135
252
|
}
|
|
136
253
|
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
137
254
|
async ensureSession(channel, channelId, threadId) {
|
|
138
255
|
if (threadId) {
|
|
139
|
-
//
|
|
140
|
-
const session = await this.sessionManager.
|
|
256
|
+
// 话题会话:仅查询,不创建
|
|
257
|
+
const session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
258
|
+
if (!session) {
|
|
259
|
+
return { error: '❌ 话题中尚未创建会话\n发送消息后自动创建' };
|
|
260
|
+
}
|
|
141
261
|
return { session };
|
|
142
262
|
}
|
|
143
263
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
@@ -152,11 +272,130 @@ export class CommandHandler {
|
|
|
152
272
|
setMessageQueue(messageQueue) {
|
|
153
273
|
this.messageQueue = messageQueue;
|
|
154
274
|
}
|
|
275
|
+
setPermissionGateway(gateway) {
|
|
276
|
+
this.permissionGateway = gateway;
|
|
277
|
+
}
|
|
278
|
+
setInteractionRouter(router) {
|
|
279
|
+
this.interactionRouter = router;
|
|
280
|
+
}
|
|
281
|
+
setStatsCollector(collector) {
|
|
282
|
+
this.statsCollector = collector;
|
|
283
|
+
}
|
|
155
284
|
registerAdapter(adapter) {
|
|
156
|
-
this.adapters.set(adapter.
|
|
285
|
+
this.adapters.set(adapter.channelName, adapter);
|
|
286
|
+
}
|
|
287
|
+
registerChannel(name, channel, channelType) {
|
|
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;
|
|
295
|
+
}
|
|
296
|
+
registerPolicy(channelName, policy) {
|
|
297
|
+
this.policies.set(channelName, policy);
|
|
157
298
|
}
|
|
158
299
|
getAdapter(channelName) {
|
|
159
|
-
|
|
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;
|
|
309
|
+
}
|
|
310
|
+
getPolicy(channel) {
|
|
311
|
+
return this.policies.get(channel) || {
|
|
312
|
+
canSwitchProject: () => true,
|
|
313
|
+
canListProjects: () => true,
|
|
314
|
+
canCreateSession: () => true,
|
|
315
|
+
canDeleteSession: () => true,
|
|
316
|
+
canImportCliSession: () => true,
|
|
317
|
+
messagePrefix: () => '',
|
|
318
|
+
showMiddleResult: () => true,
|
|
319
|
+
showIdleMonitor: () => true,
|
|
320
|
+
accumulateErrors: () => true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 返回结构化命令菜单(供 menu.query 使用)
|
|
325
|
+
* admin 看到全部命令分组,guest 仅看到用户级命令
|
|
326
|
+
*/
|
|
327
|
+
getMenuItems(isAdmin) {
|
|
328
|
+
const items = [];
|
|
329
|
+
if (isAdmin) {
|
|
330
|
+
items.push({
|
|
331
|
+
group: '项目管理',
|
|
332
|
+
commands: [
|
|
333
|
+
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
334
|
+
{ cmd: '/plist', label: '列出所有配置的项目' },
|
|
335
|
+
{ cmd: '/p', args: '<name|path>', label: '切换项目' },
|
|
336
|
+
{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
|
|
337
|
+
]
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
items.push({
|
|
341
|
+
group: '会话管理',
|
|
342
|
+
commands: [
|
|
343
|
+
{ cmd: '/new', args: '[name]', label: '创建新会话' },
|
|
344
|
+
{ cmd: '/slist', label: '列出当前项目的所有会话' },
|
|
345
|
+
{ cmd: '/slist', args: 'cli', label: '列出 CLI 会话(未导入的)' },
|
|
346
|
+
{ cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
|
|
347
|
+
{ cmd: '/name', args: '<name>', label: '重命名当前会话' },
|
|
348
|
+
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
349
|
+
...(isAdmin ? [
|
|
350
|
+
{ cmd: '/fork', args: '[name]', label: '分支当前会话' },
|
|
351
|
+
{ cmd: '/clear', label: '清空会话对话历史' },
|
|
352
|
+
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
353
|
+
] : []),
|
|
354
|
+
]
|
|
355
|
+
});
|
|
356
|
+
if (isAdmin) {
|
|
357
|
+
items.push({
|
|
358
|
+
group: 'Agent 与模型',
|
|
359
|
+
commands: [
|
|
360
|
+
{ cmd: '/agent', args: '[name]', label: '查看或切换 Agent 后端' },
|
|
361
|
+
{ cmd: '/model', args: '[model]', label: '查看或切换模型' },
|
|
362
|
+
{ cmd: '/effort', args: '[level]', label: '查看或切换推理强度' },
|
|
363
|
+
]
|
|
364
|
+
});
|
|
365
|
+
items.push({
|
|
366
|
+
group: '权限管理',
|
|
367
|
+
commands: [
|
|
368
|
+
{ cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
|
|
369
|
+
]
|
|
370
|
+
});
|
|
371
|
+
items.push({
|
|
372
|
+
group: '运维',
|
|
373
|
+
commands: [
|
|
374
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
375
|
+
{ cmd: '/stop', label: '中断当前任务' },
|
|
376
|
+
{ cmd: '/restart', label: '重启服务' },
|
|
377
|
+
{ cmd: '/repair', label: '检查并修复会话' },
|
|
378
|
+
{ cmd: '/safe', label: '进入安全模式' },
|
|
379
|
+
{ cmd: '/send', args: '[渠道] <path>', label: '发送项目内文件' },
|
|
380
|
+
{ cmd: '/check', args: '[rty <channel>]', label: '检查渠道状态或重连指定渠道' },
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
items.push({
|
|
386
|
+
group: '其他',
|
|
387
|
+
commands: [
|
|
388
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
389
|
+
]
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
items.push({
|
|
393
|
+
group: '帮助',
|
|
394
|
+
commands: [
|
|
395
|
+
{ cmd: '/help', label: '显示帮助信息' },
|
|
396
|
+
]
|
|
397
|
+
});
|
|
398
|
+
return items;
|
|
160
399
|
}
|
|
161
400
|
/**
|
|
162
401
|
* 快速判断是否为命令(不进队列的命令)
|
|
@@ -168,6 +407,12 @@ export class CommandHandler {
|
|
|
168
407
|
* 主命令处理入口
|
|
169
408
|
*/
|
|
170
409
|
async handle(content, channel, channelId, sendMessage, userId, threadId) {
|
|
410
|
+
// 解析身份(按实例名)
|
|
411
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
412
|
+
const policy = this.getPolicy(channel);
|
|
413
|
+
// 按当前会话选择 agent 后端
|
|
414
|
+
const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
415
|
+
const agent = this.getAgent(activeSession?.agentId);
|
|
171
416
|
// 规范化命令(将别名转换为完整命令)
|
|
172
417
|
let normalizedContent = content;
|
|
173
418
|
for (const [alias, full] of Object.entries(aliases)) {
|
|
@@ -176,16 +421,19 @@ export class CommandHandler {
|
|
|
176
421
|
break;
|
|
177
422
|
}
|
|
178
423
|
}
|
|
179
|
-
|
|
180
|
-
|
|
424
|
+
if (normalizedContent !== content) {
|
|
425
|
+
logger.debug(`[CommandHandler] normalized: "${content}" -> "${normalizedContent}"`);
|
|
426
|
+
}
|
|
181
427
|
// 话题内禁用部分命令
|
|
182
428
|
if (threadId) {
|
|
183
|
-
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del'];
|
|
429
|
+
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del', '/agent'];
|
|
184
430
|
const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
|
|
185
|
-
if (isBlocked)
|
|
431
|
+
if (isBlocked) {
|
|
186
432
|
return '⚠️ 话题中不支持此命令';
|
|
433
|
+
}
|
|
187
434
|
}
|
|
188
|
-
|
|
435
|
+
// 权限检查:区分用户级命令和管理级命令
|
|
436
|
+
const isAdmin = identity.role === 'owner';
|
|
189
437
|
if (normalizedContent.startsWith('/')) {
|
|
190
438
|
const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
191
439
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
@@ -193,6 +441,23 @@ export class CommandHandler {
|
|
|
193
441
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
194
442
|
}
|
|
195
443
|
}
|
|
444
|
+
// 空闲检查:某些命令需要等待当前会话空闲
|
|
445
|
+
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
|
|
446
|
+
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
447
|
+
if (threadId) {
|
|
448
|
+
// 话题中:检查话题 session 是否在处理(不创建)
|
|
449
|
+
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
450
|
+
if (threadSession) {
|
|
451
|
+
const threadAgent = this.getAgent(threadSession.agentId);
|
|
452
|
+
if (threadAgent.hasActiveStream(threadSession.id)) {
|
|
453
|
+
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (activeSession && agent.hasActiveStream(activeSession.id)) {
|
|
458
|
+
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
196
461
|
// 检查是否以 / 开头(可能是命令)
|
|
197
462
|
if (normalizedContent.startsWith('/')) {
|
|
198
463
|
const inputCmd = normalizedContent.split(' ')[0];
|
|
@@ -212,153 +477,581 @@ export class CommandHandler {
|
|
|
212
477
|
}
|
|
213
478
|
const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd));
|
|
214
479
|
if (!isCmd)
|
|
215
|
-
return
|
|
480
|
+
return undefined;
|
|
216
481
|
// /help 命令不需要会话
|
|
217
482
|
if (normalizedContent === '/help') {
|
|
218
483
|
if (!isAdmin) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
/
|
|
224
|
-
/
|
|
225
|
-
/
|
|
226
|
-
/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
/
|
|
241
|
-
/
|
|
242
|
-
/
|
|
243
|
-
/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
/
|
|
247
|
-
/
|
|
248
|
-
/
|
|
249
|
-
/
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
/
|
|
253
|
-
/
|
|
254
|
-
|
|
255
|
-
🤖
|
|
256
|
-
/
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
484
|
+
const lines = [
|
|
485
|
+
'可用命令:',
|
|
486
|
+
'',
|
|
487
|
+
'🔄 会话管理:',
|
|
488
|
+
' /new [名称] - 创建新会话(可选命名)',
|
|
489
|
+
' /slist - 列出当前项目的所有会话',
|
|
490
|
+
' /slist cli - 列出 CLI 会话(未导入的)',
|
|
491
|
+
' /s, /session <名称|序号|uuid> - 切换到指定会话',
|
|
492
|
+
' /name, /rename <新名称> - 重命名当前会话',
|
|
493
|
+
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
494
|
+
' /status - 显示会话状态',
|
|
495
|
+
'',
|
|
496
|
+
'❓ 帮助:',
|
|
497
|
+
' /help - 显示此帮助信息',
|
|
498
|
+
];
|
|
499
|
+
return lines.join('\n');
|
|
500
|
+
}
|
|
501
|
+
const lines = [
|
|
502
|
+
'可用命令:',
|
|
503
|
+
'',
|
|
504
|
+
'📁 项目管理:',
|
|
505
|
+
' /pwd - 显示当前项目路径',
|
|
506
|
+
' /plist - 列出所有配置的项目',
|
|
507
|
+
' /p, /project <name|path> - 切换项目',
|
|
508
|
+
' /bind <path> - 绑定新项目目录',
|
|
509
|
+
'',
|
|
510
|
+
'🔄 会话管理:',
|
|
511
|
+
' /new [名称] - 创建新会话(可选命名)',
|
|
512
|
+
' /slist - 列出当前项目的所有会话',
|
|
513
|
+
' /s, /session <名称> - 切换到指定会话',
|
|
514
|
+
' /name, /rename <新名称> - 重命名当前会话',
|
|
515
|
+
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
516
|
+
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
517
|
+
' /clear - 清空当前会话的对话历史',
|
|
518
|
+
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
519
|
+
'',
|
|
520
|
+
'🤖 Agent 与模型:',
|
|
521
|
+
' /agent [name] - 查看或切换 Agent 后端',
|
|
522
|
+
' /model [model] - 查看或切换模型',
|
|
523
|
+
' /effort [level] - 查看或切换推理强度',
|
|
524
|
+
'',
|
|
525
|
+
'🔐 权限管理:',
|
|
526
|
+
' /perm - 查看当前权限模式',
|
|
527
|
+
' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
|
|
528
|
+
' /perm allow|always|deny - 审批权限请求',
|
|
529
|
+
'',
|
|
530
|
+
'🛠️ 运维:',
|
|
531
|
+
' /status - 显示会话状态',
|
|
532
|
+
' /stop - 中断当前任务',
|
|
533
|
+
' /restart - 重启服务',
|
|
534
|
+
' /repair - 检查并修复会话',
|
|
535
|
+
' /safe - 进入安全模式',
|
|
536
|
+
' /send [渠道] <路径> - 发送项目内文件',
|
|
537
|
+
'',
|
|
538
|
+
'❓ 帮助:',
|
|
539
|
+
' /help - 显示此帮助信息',
|
|
540
|
+
];
|
|
541
|
+
return lines.join('\n');
|
|
542
|
+
}
|
|
543
|
+
// /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
|
|
544
|
+
if (normalizedContent.startsWith('/perm')) {
|
|
545
|
+
const args = normalizedContent.slice(5).trim();
|
|
546
|
+
// 先获取正确的 session 和 agent(话题可能用不同 agent)
|
|
547
|
+
const permResult = await this.ensureSession(channel, channelId, threadId);
|
|
548
|
+
if ('error' in permResult)
|
|
549
|
+
return permResult.error;
|
|
550
|
+
const { session: permSession } = permResult;
|
|
551
|
+
const permAgent = this.getAgent(permSession.agentId);
|
|
552
|
+
// /perm(无参数):显示当前模式和可选模式
|
|
553
|
+
if (!args) {
|
|
554
|
+
if (!hasPermissionController(permAgent)) {
|
|
555
|
+
return '❌ 权限控制不可用';
|
|
556
|
+
}
|
|
557
|
+
const currentMode = permSession.metadata?.permissionMode ?? 'bypass';
|
|
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
|
+
// 降级:文本
|
|
598
|
+
const modeList = modes.map(m => {
|
|
599
|
+
const prefix = m.key === currentMode ? '▶' : ' ';
|
|
600
|
+
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
601
|
+
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
602
|
+
}).join('\n');
|
|
603
|
+
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
|
|
604
|
+
}
|
|
605
|
+
const parts = args.split(/\s+/);
|
|
606
|
+
// /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
|
|
607
|
+
if (parts.length === 1) {
|
|
608
|
+
const arg = parts[0];
|
|
609
|
+
// /perm allow|always|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
|
|
610
|
+
if (arg === 'allow' || arg === 'always' || arg === 'deny') {
|
|
611
|
+
if (!this.permissionGateway) {
|
|
612
|
+
return '❌ 权限审批未启用';
|
|
613
|
+
}
|
|
614
|
+
const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
|
|
615
|
+
if (pendingIds.length === 0) {
|
|
616
|
+
return '❌ 当前没有待审批的权限请求';
|
|
617
|
+
}
|
|
618
|
+
if (pendingIds.length > 1) {
|
|
619
|
+
return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}`;
|
|
620
|
+
}
|
|
621
|
+
const requestId = pendingIds[0];
|
|
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];
|
|
630
|
+
}
|
|
631
|
+
// /perm <mode>:切换权限模式
|
|
632
|
+
if (hasPermissionController(permAgent)) {
|
|
633
|
+
const modes = permAgent.listModes();
|
|
634
|
+
const matched = modes.find(m => m.key === arg);
|
|
635
|
+
if (matched) {
|
|
636
|
+
if (!matched.available) {
|
|
637
|
+
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
638
|
+
}
|
|
639
|
+
// guest 用户只能保持 readonly 模式
|
|
640
|
+
if (identity.role !== 'owner' && arg !== 'readonly') {
|
|
641
|
+
return '❌ 当前身份无法切换权限模式';
|
|
642
|
+
}
|
|
643
|
+
const metadata = permSession.metadata || {};
|
|
644
|
+
metadata.permissionMode = arg;
|
|
645
|
+
await this.sessionManager.updateSession(permSession.id, { metadata });
|
|
646
|
+
return `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// 不是已知模式名也不是 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`;
|
|
652
|
+
}
|
|
653
|
+
// 双参数不再支持,提示正确用法
|
|
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`;
|
|
656
|
+
}
|
|
657
|
+
// /agent 命令:查看或切换 Agent 后端
|
|
658
|
+
if (normalizedContent.startsWith('/agent')) {
|
|
659
|
+
if (!isAdmin)
|
|
660
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
661
|
+
const args = normalizedContent.slice(6).trim();
|
|
662
|
+
const available = [...this.agentMap.keys()];
|
|
663
|
+
if (!args) {
|
|
664
|
+
const currentAgent = activeSession?.agentId || this.defaultAgentId;
|
|
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');
|
|
703
|
+
return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
|
|
704
|
+
}
|
|
705
|
+
if (!this.agentMap.has(args)) {
|
|
706
|
+
return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
|
|
707
|
+
}
|
|
708
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
709
|
+
if ('error' in result)
|
|
710
|
+
return result.error;
|
|
711
|
+
const { session } = result;
|
|
712
|
+
// 取消原会话的 pending 权限请求和交互卡片
|
|
713
|
+
if (this.permissionGateway) {
|
|
714
|
+
this.permissionGateway.cancelAll(session.id);
|
|
715
|
+
}
|
|
716
|
+
if (this.interactionRouter) {
|
|
717
|
+
this.interactionRouter.cancelAll(session.id);
|
|
718
|
+
}
|
|
719
|
+
// 切换到目标 agent(恢复已有会话或创建新会话)
|
|
720
|
+
const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
|
|
721
|
+
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
722
|
+
const projectName = this.getProjectName(session.projectPath);
|
|
723
|
+
let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
|
|
724
|
+
return agentSwitchResponse;
|
|
260
725
|
}
|
|
261
726
|
// /model 命令:查看或切换模型/推理强度
|
|
262
727
|
if (normalizedContent.startsWith('/model')) {
|
|
263
728
|
const args = normalizedContent.slice(6).trim();
|
|
729
|
+
// 获取当前会话(话题会话可能绑定不同 agent)
|
|
730
|
+
const modelResult = await this.ensureSession(channel, channelId, threadId);
|
|
731
|
+
if ('error' in modelResult)
|
|
732
|
+
return modelResult.error;
|
|
733
|
+
const { session: modelSession } = modelResult;
|
|
734
|
+
const modelAgent = this.getAgent(modelSession.agentId);
|
|
735
|
+
const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
|
|
264
736
|
if (!args) {
|
|
265
|
-
const currentModel =
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
737
|
+
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
738
|
+
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
739
|
+
const currentEffort = modelAgent.getEffort?.() || 'auto';
|
|
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
|
+
// 降级:文本
|
|
777
|
+
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
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)}`;
|
|
270
782
|
}
|
|
271
783
|
const parts = args.split(/\s+/);
|
|
272
784
|
let newModel;
|
|
273
785
|
let newEffort;
|
|
274
786
|
if (parts.length === 1) {
|
|
275
787
|
const arg = parts[0];
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const { session } = result;
|
|
282
|
-
const writeResult = writeUserSettings({ effortLevel: null });
|
|
283
|
-
if (!writeResult.success) {
|
|
284
|
-
return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
|
|
285
|
-
}
|
|
286
|
-
this.agentRunner.setEffort(undefined);
|
|
287
|
-
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);
|
|
288
793
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
newEffort = arg;
|
|
794
|
+
else if (allEfforts.includes(arg)) {
|
|
795
|
+
return `⚠️ 请使用 /effort ${arg} 调整推理强度`;
|
|
292
796
|
}
|
|
293
|
-
else if (
|
|
797
|
+
else if (models.includes(arg)) {
|
|
294
798
|
newModel = arg;
|
|
295
799
|
}
|
|
296
800
|
else {
|
|
297
|
-
const modelList =
|
|
298
|
-
|
|
801
|
+
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
802
|
+
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
803
|
+
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
|
|
299
804
|
}
|
|
300
805
|
}
|
|
301
806
|
else {
|
|
302
807
|
// 双参数:model effort
|
|
303
808
|
const [modelArg, effortArg] = parts;
|
|
304
|
-
if (!
|
|
809
|
+
if (!models.includes(modelArg)) {
|
|
305
810
|
return `❌ 无效的模型ID: ${modelArg}`;
|
|
306
811
|
}
|
|
307
|
-
|
|
308
|
-
|
|
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(' / ')}`;
|
|
309
819
|
}
|
|
310
820
|
newModel = modelArg;
|
|
311
821
|
newEffort = effortArg;
|
|
312
822
|
}
|
|
313
823
|
if (!this.config.agents)
|
|
314
824
|
this.config.agents = {};
|
|
315
|
-
|
|
316
|
-
this.config.agents.anthropic = {};
|
|
317
|
-
// 获取当前会话的项目路径
|
|
318
|
-
const result = await this.ensureSession(channel, channelId, threadId);
|
|
319
|
-
if ('error' in result)
|
|
320
|
-
return result.error;
|
|
321
|
-
const { session } = result;
|
|
825
|
+
const isCodexAgent = modelAgent.name === 'codex';
|
|
322
826
|
const changes = [];
|
|
323
|
-
const updates = {};
|
|
324
827
|
if (newModel) {
|
|
325
|
-
|
|
326
|
-
this.
|
|
828
|
+
modelAgent.setModel?.(newModel);
|
|
829
|
+
this.eventBus.publish({
|
|
830
|
+
type: 'agent:model-changed',
|
|
831
|
+
sessionId: modelSession.id,
|
|
832
|
+
model: newModel,
|
|
833
|
+
timestamp: Date.now()
|
|
834
|
+
});
|
|
327
835
|
changes.push(`模型: ${newModel}`);
|
|
328
836
|
}
|
|
329
837
|
if (newEffort) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
838
|
+
modelAgent.setEffort?.(newEffort);
|
|
839
|
+
changes.push(`推理强度: ${newEffort}`);
|
|
840
|
+
}
|
|
841
|
+
// 持久化:写回来源(就近原则)
|
|
842
|
+
// evolclaw.json 配了 → 写 evolclaw.json
|
|
843
|
+
// evolclaw.json 没配 → 写 agent 全局配置
|
|
844
|
+
if (isCodexAgent) {
|
|
845
|
+
const configuredInEvolclaw = !!(this.config.agents?.openai?.model || this.config.agents?.openai?.reasoning);
|
|
846
|
+
if (configuredInEvolclaw) {
|
|
847
|
+
if (!this.config.agents.openai)
|
|
848
|
+
this.config.agents.openai = {};
|
|
849
|
+
if (newModel)
|
|
850
|
+
this.config.agents.openai.model = newModel;
|
|
851
|
+
if (newEffort)
|
|
852
|
+
this.config.agents.openai.reasoning = newEffort;
|
|
853
|
+
try {
|
|
854
|
+
saveConfig(this.config);
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
// Codex 全局配置(~/.codex/config.toml)目前不支持写入,回退到 evolclaw.json
|
|
862
|
+
if (!this.config.agents.openai)
|
|
863
|
+
this.config.agents.openai = {};
|
|
864
|
+
if (newModel)
|
|
865
|
+
this.config.agents.openai.model = newModel;
|
|
866
|
+
if (newEffort)
|
|
867
|
+
this.config.agents.openai.reasoning = newEffort;
|
|
868
|
+
try {
|
|
869
|
+
saveConfig(this.config);
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
|
|
873
|
+
}
|
|
333
874
|
}
|
|
334
|
-
updates.effortLevel = newEffort;
|
|
335
|
-
this.agentRunner.setEffort(newEffort);
|
|
336
|
-
changes.push(`推理强度: ${newEffort} ${effortBar(newEffort)}`);
|
|
337
875
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
876
|
+
else {
|
|
877
|
+
const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
|
|
878
|
+
if (configuredInEvolclaw) {
|
|
879
|
+
if (!this.config.agents.anthropic)
|
|
880
|
+
this.config.agents.anthropic = {};
|
|
881
|
+
if (newModel)
|
|
882
|
+
this.config.agents.anthropic.model = newModel;
|
|
883
|
+
if (newEffort)
|
|
884
|
+
this.config.agents.anthropic.effort = newEffort;
|
|
885
|
+
try {
|
|
886
|
+
saveConfig(this.config);
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
const updates = {};
|
|
894
|
+
if (newModel)
|
|
895
|
+
updates.model = newModel;
|
|
896
|
+
if (newEffort)
|
|
897
|
+
updates.effortLevel = newEffort;
|
|
898
|
+
const writeResult = writeUserSettings(updates);
|
|
899
|
+
if (!writeResult.success) {
|
|
900
|
+
return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
342
903
|
}
|
|
343
904
|
return `✓ 已切换\n ${changes.join('\n ')}`;
|
|
344
905
|
}
|
|
345
|
-
// /
|
|
346
|
-
if (normalizedContent
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
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 { }
|
|
352
1019
|
}
|
|
353
1020
|
else {
|
|
354
|
-
|
|
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
|
+
}
|
|
355
1034
|
}
|
|
1035
|
+
return `✓ 推理强度: ${newEffort}`;
|
|
1036
|
+
}
|
|
1037
|
+
// /stop 命令:中断当前任务
|
|
1038
|
+
if (normalizedContent === '/stop') {
|
|
1039
|
+
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
1040
|
+
if ('error' in stopResult)
|
|
1041
|
+
return '当前没有正在处理的任务';
|
|
1042
|
+
const { session: stopSession } = stopResult;
|
|
1043
|
+
const stopAgent = this.getAgent(stopSession.agentId);
|
|
1044
|
+
const sessionKey = stopSession.id;
|
|
356
1045
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
357
|
-
const hasActive =
|
|
1046
|
+
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
358
1047
|
if (queueLength === 0 && !hasActive) {
|
|
359
1048
|
return '当前没有正在处理的任务';
|
|
360
1049
|
}
|
|
361
|
-
await
|
|
1050
|
+
await stopAgent.interrupt(sessionKey);
|
|
1051
|
+
// 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
|
|
1052
|
+
this.eventBus.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'stop' });
|
|
1053
|
+
// 强制清除 processing_state
|
|
1054
|
+
this.sessionManager.clearProcessing(sessionKey);
|
|
362
1055
|
return '✓ 已发送中断信号,任务将尽快停止';
|
|
363
1056
|
}
|
|
364
1057
|
// /clear 命令:通过 SDK /clear 清空会话历史
|
|
@@ -367,20 +1060,30 @@ export class CommandHandler {
|
|
|
367
1060
|
if ('error' in result)
|
|
368
1061
|
return result.error;
|
|
369
1062
|
const { session } = result;
|
|
1063
|
+
const sessionAgent = this.getAgent(session.agentId);
|
|
1064
|
+
if (!sessionAgent.capabilities?.clear) {
|
|
1065
|
+
return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代`;
|
|
1066
|
+
}
|
|
370
1067
|
if (!session.agentSessionId) {
|
|
371
1068
|
return '❌ 当前会话没有历史记录,无需清空';
|
|
372
1069
|
}
|
|
373
1070
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
374
1071
|
? session.projectPath
|
|
375
1072
|
: path.resolve(process.cwd(), session.projectPath);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
await
|
|
379
|
-
|
|
380
|
-
|
|
1073
|
+
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
1074
|
+
try {
|
|
1075
|
+
const cleared = await sessionAgent.clearSession(session.id, session.agentSessionId, projectPath);
|
|
1076
|
+
if (cleared) {
|
|
1077
|
+
await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
|
|
1078
|
+
sessionAgent.updateSessionId(session.id, '');
|
|
1079
|
+
return '✅ 已清空当前会话的对话历史';
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
return '❌ 清空会话失败,请稍后重试';
|
|
1083
|
+
}
|
|
381
1084
|
}
|
|
382
|
-
|
|
383
|
-
|
|
1085
|
+
finally {
|
|
1086
|
+
releaseLock();
|
|
384
1087
|
}
|
|
385
1088
|
}
|
|
386
1089
|
// /compact 命令:手动压缩会话上下文
|
|
@@ -389,21 +1092,31 @@ export class CommandHandler {
|
|
|
389
1092
|
if ('error' in result)
|
|
390
1093
|
return result.error;
|
|
391
1094
|
const { session } = result;
|
|
1095
|
+
const sessionAgent = this.getAgent(session.agentId);
|
|
1096
|
+
if (!sessionAgent.capabilities?.compact) {
|
|
1097
|
+
return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact`;
|
|
1098
|
+
}
|
|
392
1099
|
if (!session.agentSessionId) {
|
|
393
1100
|
return '❌ 当前会话没有历史记录,无需压缩';
|
|
394
1101
|
}
|
|
395
1102
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
396
1103
|
? session.projectPath
|
|
397
1104
|
: path.resolve(process.cwd(), session.projectPath);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1105
|
+
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
1106
|
+
try {
|
|
1107
|
+
if (sendMessage) {
|
|
1108
|
+
await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
|
|
1109
|
+
}
|
|
1110
|
+
const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
|
|
1111
|
+
if (compacted) {
|
|
1112
|
+
return '✅ 会话上下文已压缩';
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
return '❌ 会话压缩失败,请稍后重试';
|
|
1116
|
+
}
|
|
404
1117
|
}
|
|
405
|
-
|
|
406
|
-
|
|
1118
|
+
finally {
|
|
1119
|
+
releaseLock();
|
|
407
1120
|
}
|
|
408
1121
|
}
|
|
409
1122
|
// 尝试获取活跃会话(话题时直接查找话题 session)
|
|
@@ -414,26 +1127,37 @@ export class CommandHandler {
|
|
|
414
1127
|
else {
|
|
415
1128
|
session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
416
1129
|
}
|
|
417
|
-
//
|
|
1130
|
+
// 对于需要会话的命令,如果没有会话则使用默认项目创建临时会话
|
|
1131
|
+
// 这样 /pwd、/status 等命令可以在没有活跃会话时返回默认项目信息
|
|
418
1132
|
if (!session && (normalizedContent.startsWith('/new') ||
|
|
419
1133
|
normalizedContent.startsWith('/bind') ||
|
|
420
|
-
normalizedContent.startsWith('/project')
|
|
1134
|
+
normalizedContent.startsWith('/project') ||
|
|
1135
|
+
normalizedContent === '/pwd' ||
|
|
1136
|
+
normalizedContent === '/status')) {
|
|
421
1137
|
session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd());
|
|
422
1138
|
}
|
|
423
1139
|
// /status 命令:显示会话状态
|
|
424
1140
|
if (normalizedContent === '/status') {
|
|
1141
|
+
// session 现在总是存在(上面已自动创建)
|
|
425
1142
|
if (!session) {
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
❌ 当前未创建会话
|
|
429
|
-
|
|
430
|
-
提示:发送任意消息或使用 /new 命令创建会话`;
|
|
1143
|
+
return `❌ 无法创建会话,请检查配置`;
|
|
431
1144
|
}
|
|
432
1145
|
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
433
|
-
const
|
|
1146
|
+
const sessionAgent = this.getAgent(session.agentId);
|
|
1147
|
+
const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
|
|
434
1148
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
435
1149
|
const isThread = !!session.threadId;
|
|
436
|
-
|
|
1150
|
+
let sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
|
|
1151
|
+
// 处理中时显示时长
|
|
1152
|
+
if (isCurrentlyProcessing) {
|
|
1153
|
+
const elapsed = Date.now() - parseInt(session.processingState, 10);
|
|
1154
|
+
if (!isNaN(elapsed) && elapsed > 0) {
|
|
1155
|
+
const sec = Math.floor(elapsed / 1000);
|
|
1156
|
+
sessionStatus = sec < 60 ? `处理中 (${sec}秒)` :
|
|
1157
|
+
sec < 3600 ? `处理中 (${Math.floor(sec / 60)}分钟)` :
|
|
1158
|
+
`处理中 (${Math.floor(sec / 3600)}小时)`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
437
1161
|
const projectName = this.getProjectName(session.projectPath);
|
|
438
1162
|
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
439
1163
|
const timeSinceSuccess = Date.now() - health.lastSuccessTime;
|
|
@@ -443,7 +1167,7 @@ export class CommandHandler {
|
|
|
443
1167
|
// 获取会话文件信息并同步 name
|
|
444
1168
|
let sessionTurns = 0;
|
|
445
1169
|
if (session.agentSessionId) {
|
|
446
|
-
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId);
|
|
1170
|
+
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId, session.agentId);
|
|
447
1171
|
sessionTurns = fileInfo.turns;
|
|
448
1172
|
if (fileInfo.title && fileInfo.title !== session.name) {
|
|
449
1173
|
await this.sessionManager.renameSession(session.id, fileInfo.title);
|
|
@@ -452,10 +1176,17 @@ export class CommandHandler {
|
|
|
452
1176
|
}
|
|
453
1177
|
const lines = [];
|
|
454
1178
|
if (isAdmin) {
|
|
455
|
-
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}`);
|
|
1180
|
+
if (health.consecutiveErrors > 0) {
|
|
1181
|
+
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
1182
|
+
}
|
|
1183
|
+
if (health.safeMode) {
|
|
1184
|
+
lines.push(`安全模式: 是 ⚠️`);
|
|
1185
|
+
}
|
|
1186
|
+
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
456
1187
|
}
|
|
457
1188
|
else {
|
|
458
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`,
|
|
1189
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
459
1190
|
}
|
|
460
1191
|
if (health.safeMode) {
|
|
461
1192
|
lines.push('');
|
|
@@ -482,12 +1213,112 @@ export class CommandHandler {
|
|
|
482
1213
|
}
|
|
483
1214
|
}
|
|
484
1215
|
const projectPath = session?.projectPath || this.config.projects?.defaultPath || process.cwd();
|
|
485
|
-
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName);
|
|
1216
|
+
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.defaultAgentId);
|
|
1217
|
+
this.eventBus.publish({
|
|
1218
|
+
type: 'session:created',
|
|
1219
|
+
sessionId: newSession.id,
|
|
1220
|
+
channel,
|
|
1221
|
+
channelId,
|
|
1222
|
+
projectPath,
|
|
1223
|
+
name: sessionName,
|
|
1224
|
+
timestamp: Date.now()
|
|
1225
|
+
});
|
|
486
1226
|
if (session) {
|
|
487
|
-
|
|
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);
|
|
1230
|
+
await agent.closeSession(session.id);
|
|
488
1231
|
}
|
|
489
1232
|
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /slist 查看`;
|
|
490
1233
|
}
|
|
1234
|
+
// /check 命令:检查渠道状态 / 手动重连指定渠道
|
|
1235
|
+
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
1236
|
+
if (!isAdmin)
|
|
1237
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
1238
|
+
const subCmd = normalizedContent.slice('/check'.length).trim();
|
|
1239
|
+
// /check rty <channel> — 重连指定渠道
|
|
1240
|
+
if (subCmd.startsWith('rty')) {
|
|
1241
|
+
const target = subCmd.slice('rty'.length).trim();
|
|
1242
|
+
if (!target) {
|
|
1243
|
+
return '❌ 请指定渠道名称,例如:/check rty feishu';
|
|
1244
|
+
}
|
|
1245
|
+
const ch = this.channelObjects.get(target);
|
|
1246
|
+
if (!ch) {
|
|
1247
|
+
const available = [...this.channelObjects.keys()].join(', ') || '无';
|
|
1248
|
+
return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
|
|
1249
|
+
}
|
|
1250
|
+
if (!ch.reconnect) {
|
|
1251
|
+
return `❌ 渠道 "${target}" 不支持重连`;
|
|
1252
|
+
}
|
|
1253
|
+
const result = await ch.reconnect();
|
|
1254
|
+
return `🔄 ${target} 重连: ${result}`;
|
|
1255
|
+
}
|
|
1256
|
+
// Default: show full system health check
|
|
1257
|
+
const lines = ['📡 渠道状态:'];
|
|
1258
|
+
// Group by channelType
|
|
1259
|
+
const groups = new Map();
|
|
1260
|
+
for (const [name] of this.adapters) {
|
|
1261
|
+
const type = this.channelTypeMap.get(name) || name;
|
|
1262
|
+
const ch = this.channelObjects.get(name);
|
|
1263
|
+
let status;
|
|
1264
|
+
if (ch?.getStatus) {
|
|
1265
|
+
const s = ch.getStatus();
|
|
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}`);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
const parts = instances.map(i => `${i.name} ${i.status}`);
|
|
1281
|
+
lines.push(` ${type}: [${parts.join(', ')}]`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// 队列状态
|
|
1285
|
+
lines.push('', '📬 队列状态:');
|
|
1286
|
+
lines.push(` 待处理消息: ${this.messageQueue.getGlobalQueueLength()}`);
|
|
1287
|
+
lines.push(` 处理中队列: ${this.messageQueue.getGlobalProcessingCount()}`);
|
|
1288
|
+
// 运行概况
|
|
1289
|
+
lines.push('', '🖥️ 运行概况:');
|
|
1290
|
+
const uptimeMs = this.statsCollector
|
|
1291
|
+
? this.statsCollector.getSnapshot().uptimeMs
|
|
1292
|
+
: process.uptime() * 1000;
|
|
1293
|
+
lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
|
|
1294
|
+
lines.push(` 当前安全模式会话: ${this.sessionManager.getSafeModeSessionCount()}`);
|
|
1295
|
+
// 近 1 小时统计
|
|
1296
|
+
if (this.statsCollector) {
|
|
1297
|
+
const snap = this.statsCollector.getSnapshot();
|
|
1298
|
+
const h = snap.lastHour;
|
|
1299
|
+
lines.push('', '📊 近 1 小时统计:');
|
|
1300
|
+
lines.push(` 收到消息: ${h.received}`);
|
|
1301
|
+
lines.push(` 完成处理: ${h.completed}`);
|
|
1302
|
+
if (h.errors > 0) {
|
|
1303
|
+
const breakdown = Object.entries(h.errorsByType).map(([t, c]) => `${t}: ${c}`).join(', ');
|
|
1304
|
+
lines.push(` 处理出错: ${h.errors} (${breakdown})`);
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
lines.push(` 处理出错: 0`);
|
|
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
|
+
}
|
|
1313
|
+
lines.push(` 被中断: ${h.interrupts}`);
|
|
1314
|
+
lines.push(` 进入安全模式: ${h.safeModeEntries}`);
|
|
1315
|
+
if (h.completed > 0) {
|
|
1316
|
+
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
lines.push('', '💡 /check rty <channel> — 重连指定渠道');
|
|
1320
|
+
return lines.join('\n');
|
|
1321
|
+
}
|
|
491
1322
|
// /restart 命令:重启服务
|
|
492
1323
|
if (normalizedContent === '/restart') {
|
|
493
1324
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
@@ -497,6 +1328,33 @@ export class CommandHandler {
|
|
|
497
1328
|
const count = this.messageCache.getCount(s.id);
|
|
498
1329
|
return `${s.projectPath} 有 ${count} 条新消息`;
|
|
499
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
|
+
// 文本确认流程
|
|
500
1358
|
if (sessionsWithMessages.length > 0) {
|
|
501
1359
|
const restartKey = `${channel}-${channelId}`;
|
|
502
1360
|
const restartConfirmFile = path.join(resolvePaths().dataDir, `restart-confirm-${restartKey}.json`);
|
|
@@ -516,37 +1374,14 @@ export class CommandHandler {
|
|
|
516
1374
|
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
517
1375
|
}
|
|
518
1376
|
}
|
|
519
|
-
|
|
520
|
-
let rootId;
|
|
521
|
-
if (threadId) {
|
|
522
|
-
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
523
|
-
rootId = threadSession.metadata?.feishu?.rootId;
|
|
524
|
-
}
|
|
525
|
-
const restartInfo = {
|
|
526
|
-
channel,
|
|
527
|
-
channelId,
|
|
528
|
-
timestamp: Date.now(),
|
|
529
|
-
...(rootId ? { rootId } : {})
|
|
530
|
-
};
|
|
531
|
-
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
532
|
-
const { spawn } = await import('child_process');
|
|
533
|
-
spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
|
|
534
|
-
detached: true,
|
|
535
|
-
stdio: 'ignore',
|
|
536
|
-
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
537
|
-
}).unref();
|
|
538
|
-
setTimeout(() => {
|
|
539
|
-
logger.info('[System] Restarting by user command...');
|
|
540
|
-
process.exit(0);
|
|
541
|
-
}, 1000);
|
|
1377
|
+
await executeRestart();
|
|
542
1378
|
return '🔄 服务正在重启,请稍候...(约 5 秒后恢复)';
|
|
543
1379
|
}
|
|
544
1380
|
// /pwd 命令:显示当前项目路径
|
|
545
1381
|
if (normalizedContent === '/pwd') {
|
|
1382
|
+
// session 现在总是存在(上面已自动创建)
|
|
546
1383
|
if (!session) {
|
|
547
|
-
return `❌
|
|
548
|
-
|
|
549
|
-
提示:发送任意消息或使用 /new 命令创建会话`;
|
|
1384
|
+
return `❌ 无法创建会话,请检查配置`;
|
|
550
1385
|
}
|
|
551
1386
|
const configName = this.getConfiguredProjectName(session.projectPath);
|
|
552
1387
|
if (configName) {
|
|
@@ -554,75 +1389,243 @@ export class CommandHandler {
|
|
|
554
1389
|
}
|
|
555
1390
|
return `当前项目: ${session.projectPath}`;
|
|
556
1391
|
}
|
|
1392
|
+
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path
|
|
1393
|
+
if (normalizedContent.startsWith('/send')) {
|
|
1394
|
+
// 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
|
|
1395
|
+
// 还原: 将 [text](url) 替换为 text
|
|
1396
|
+
const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
1397
|
+
if (!rawArg) {
|
|
1398
|
+
return '用法: /send <相对路径> 或 /send <渠道> <相对路径>\n示例: /send src/index.ts\n示例: /send feishu report.md';
|
|
1399
|
+
}
|
|
1400
|
+
// 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
|
|
1401
|
+
const tokens = rawArg.split(/\s+/);
|
|
1402
|
+
let targetChannel = channel;
|
|
1403
|
+
let targetLabel = channel;
|
|
1404
|
+
let filePath = rawArg;
|
|
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
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const isCrossChannel = targetChannel !== channel;
|
|
1426
|
+
// 跨通道仅限 owner
|
|
1427
|
+
if (isCrossChannel && identity.role !== 'owner') {
|
|
1428
|
+
return '❌ 跨通道发送仅限管理员';
|
|
1429
|
+
}
|
|
1430
|
+
// 找目标 adapter
|
|
1431
|
+
const targetAdapter = this.adapters.get(targetChannel);
|
|
1432
|
+
if (!targetAdapter) {
|
|
1433
|
+
return `❌ 通道 ${targetLabel} 未启用或不存在`;
|
|
1434
|
+
}
|
|
1435
|
+
if (!targetAdapter.sendFile) {
|
|
1436
|
+
return `❌ 通道 ${targetLabel} 不支持文件发送`;
|
|
1437
|
+
}
|
|
1438
|
+
// 获取 session(需要 projectPath)
|
|
1439
|
+
const sendResult = await this.ensureSession(channel, channelId, threadId);
|
|
1440
|
+
if ('error' in sendResult)
|
|
1441
|
+
return sendResult.error;
|
|
1442
|
+
const sendSession = sendResult.session;
|
|
1443
|
+
// 路径安全校验
|
|
1444
|
+
if (path.isAbsolute(filePath)) {
|
|
1445
|
+
return '❌ 不支持绝对路径\n请使用项目内的相对路径';
|
|
1446
|
+
}
|
|
1447
|
+
if (filePath.split(path.sep).includes('..') || filePath.split('/').includes('..')) {
|
|
1448
|
+
return '❌ 不支持 .. 路径穿越';
|
|
1449
|
+
}
|
|
1450
|
+
const resolvedPath = path.resolve(sendSession.projectPath, filePath);
|
|
1451
|
+
// 存在性检查
|
|
1452
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1453
|
+
return `❌ 文件不存在: ${filePath}`;
|
|
1454
|
+
}
|
|
1455
|
+
// 符号链接安全:realpath 后验证仍在项目目录内
|
|
1456
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
1457
|
+
const realProjectPath = fs.realpathSync(sendSession.projectPath);
|
|
1458
|
+
if (!realPath.startsWith(realProjectPath + path.sep) && realPath !== realProjectPath) {
|
|
1459
|
+
return '❌ 路径不允许: 文件不在项目目录内';
|
|
1460
|
+
}
|
|
1461
|
+
const stat = fs.statSync(resolvedPath);
|
|
1462
|
+
if (stat.isDirectory()) {
|
|
1463
|
+
return '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持';
|
|
1464
|
+
}
|
|
1465
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
1466
|
+
if (stat.size > MAX_SIZE) {
|
|
1467
|
+
return `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)`;
|
|
1468
|
+
}
|
|
1469
|
+
// 找目标 channelId
|
|
1470
|
+
let targetChannelId = channelId;
|
|
1471
|
+
if (isCrossChannel) {
|
|
1472
|
+
const ownerPeerId = getOwner(this.config, targetChannel);
|
|
1473
|
+
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
|
|
1474
|
+
if (!targetChannelId) {
|
|
1475
|
+
return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
// 发送文件
|
|
1479
|
+
try {
|
|
1480
|
+
const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
|
|
1481
|
+
await targetAdapter.sendFile(targetChannelId, realPath, replyCtx);
|
|
1482
|
+
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
1483
|
+
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
1484
|
+
: `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
|
|
1485
|
+
return isCrossChannel
|
|
1486
|
+
? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
|
|
1487
|
+
: `✅ 已发送: ${filePath} (${sizeStr})`;
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
logger.error('[CommandHandler] /send failed:', error);
|
|
1491
|
+
return `❌ 文件发送失败: ${error.message || error}`;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
557
1494
|
// /plist 命令:列出所有项目
|
|
558
1495
|
if (normalizedContent === '/plist') {
|
|
559
|
-
|
|
560
|
-
if (isGroup) {
|
|
1496
|
+
if (!policy.canListProjects(session?.chatType || 'private', identity.role)) {
|
|
561
1497
|
if (!session) {
|
|
562
1498
|
return `❌ 当前群聊未绑定项目
|
|
563
1499
|
|
|
564
1500
|
请使用 /bind <项目路径> 绑定项目`;
|
|
565
1501
|
}
|
|
566
1502
|
const projectName = this.getProjectName(session.projectPath);
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
const status = queueLength > 0 ? '[处理中]' : '[空闲]';
|
|
1503
|
+
const isProcessing = !!session.processingState;
|
|
1504
|
+
const status = isProcessing ? '[处理中]' : '[空闲]';
|
|
570
1505
|
return `当前群聊绑定的项目:
|
|
571
1506
|
${projectName} (${session.projectPath}) - ${status}
|
|
572
1507
|
|
|
573
1508
|
提示:群聊不支持切换项目`;
|
|
574
1509
|
}
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
const processingProject = this.messageQueue.getProcessingProject(sessionKey);
|
|
578
|
-
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
579
|
-
const normalizePath = (p) => p.replace(/[/\\]+$/, '');
|
|
1510
|
+
// 收集项目信息并按最近活跃排序(唯一来源:evolclaw.json projects.list)
|
|
1511
|
+
const entries = [];
|
|
580
1512
|
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
584
|
-
if (!projectSession) {
|
|
585
|
-
lines.push(`${prefix} ${name} (${projectPath}) - 无会话`);
|
|
1513
|
+
// 跳过不存在的路径
|
|
1514
|
+
if (!fs.existsSync(projectPath))
|
|
586
1515
|
continue;
|
|
587
|
-
|
|
588
|
-
const
|
|
1516
|
+
const isCurrent = session ? path.resolve(session.projectPath) === path.resolve(projectPath) : false;
|
|
1517
|
+
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
1518
|
+
entries.push({
|
|
1519
|
+
name, projectPath, projectSession, isCurrent,
|
|
1520
|
+
updatedAt: projectSession?.updatedAt ?? 0,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
// 当前活跃项目置顶,其余按 updatedAt 降序
|
|
1524
|
+
entries.sort((a, b) => {
|
|
1525
|
+
if (a.isCurrent !== b.isCurrent)
|
|
1526
|
+
return a.isCurrent ? -1 : 1;
|
|
1527
|
+
return b.updatedAt - a.updatedAt;
|
|
1528
|
+
});
|
|
1529
|
+
// 构建项目状态文本的辅助函数
|
|
1530
|
+
const buildStatusText = (entry) => {
|
|
1531
|
+
const { projectSession, isCurrent } = entry;
|
|
1532
|
+
if (!projectSession)
|
|
1533
|
+
return '无会话';
|
|
1534
|
+
const parts = [];
|
|
589
1535
|
if (isCurrent) {
|
|
590
|
-
|
|
1536
|
+
parts.push('活跃');
|
|
591
1537
|
}
|
|
592
1538
|
else {
|
|
593
|
-
|
|
594
|
-
statusParts.push(formatIdleTime(idleMs));
|
|
1539
|
+
parts.push(formatIdleTime(Date.now() - projectSession.updatedAt));
|
|
595
1540
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
statusParts.push('[处理中]');
|
|
602
|
-
}
|
|
1541
|
+
const isProcessing = !!projectSession.processingState;
|
|
1542
|
+
if (isProcessing) {
|
|
1543
|
+
const qLen = this.messageQueue.getQueueLength(projectSession.id);
|
|
1544
|
+
parts.push(qLen > 0 ? `[处理中,队列${qLen}条]` : '[处理中]');
|
|
603
1545
|
}
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
|
|
1546
|
+
const unread = this.messageCache.getCount(projectSession.id);
|
|
1547
|
+
if (unread > 0) {
|
|
1548
|
+
parts.push(`[${unread}条新消息]`);
|
|
607
1549
|
}
|
|
608
|
-
else if (!
|
|
609
|
-
|
|
1550
|
+
else if (!isProcessing && !isCurrent) {
|
|
1551
|
+
parts.push('[空闲]');
|
|
610
1552
|
}
|
|
611
|
-
|
|
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)}`);
|
|
612
1604
|
}
|
|
613
1605
|
return lines.join('\n');
|
|
614
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
|
+
}
|
|
615
1614
|
// /project 命令:切换项目(支持名称或路径)
|
|
616
1615
|
if (normalizedContent.startsWith('/project ')) {
|
|
617
|
-
|
|
618
|
-
if (isGroup) {
|
|
1616
|
+
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
619
1617
|
return `❌ 群聊不支持切换项目
|
|
620
1618
|
|
|
621
1619
|
群聊只能绑定一个项目。如需更换项目,请联系管理员重新配置。`;
|
|
622
1620
|
}
|
|
623
|
-
|
|
1621
|
+
let arg = normalizedContent.slice(9).trim();
|
|
624
1622
|
if (!arg)
|
|
625
1623
|
return '用法: /p <name|path> 或 /project <name|path>';
|
|
1624
|
+
// 检查确认标志
|
|
1625
|
+
const hasConfirm = arg.endsWith(' --confirm');
|
|
1626
|
+
if (hasConfirm) {
|
|
1627
|
+
arg = arg.slice(0, -10).trim();
|
|
1628
|
+
}
|
|
626
1629
|
let projectPath;
|
|
627
1630
|
let projectName;
|
|
628
1631
|
if (arg.includes('/')) {
|
|
@@ -649,10 +1652,33 @@ export class CommandHandler {
|
|
|
649
1652
|
return `当前已在项目: ${projectName}\n 路径: ${projectPath}`;
|
|
650
1653
|
}
|
|
651
1654
|
}
|
|
652
|
-
|
|
1655
|
+
// 群聊切换项目需要确认
|
|
1656
|
+
const isGroupChat = session?.chatType === 'group';
|
|
1657
|
+
if (isGroupChat && !hasConfirm) {
|
|
1658
|
+
return `⚠️ 群聊切换项目风险提示:
|
|
1659
|
+
|
|
1660
|
+
切换项目将影响所有群成员的对话上下文,可能导致:
|
|
1661
|
+
• 当前项目的会话历史被切换
|
|
1662
|
+
• 正在处理的任务被中断
|
|
1663
|
+
• 其他成员的工作受到影响
|
|
1664
|
+
|
|
1665
|
+
确认切换请执行:
|
|
1666
|
+
/p ${projectName} --confirm`;
|
|
1667
|
+
}
|
|
1668
|
+
const currentAgentId = activeSession?.agentId || this.defaultAgentId;
|
|
1669
|
+
const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath, currentAgentId);
|
|
1670
|
+
this.eventBus.publish({
|
|
1671
|
+
type: 'project:switched',
|
|
1672
|
+
sessionId: newSession.id,
|
|
1673
|
+
channel,
|
|
1674
|
+
channelId,
|
|
1675
|
+
projectPath,
|
|
1676
|
+
timestamp: Date.now()
|
|
1677
|
+
});
|
|
653
1678
|
const cachedEvents = this.messageCache.getEvents(newSession.id);
|
|
654
1679
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
655
|
-
|
|
1680
|
+
const currentAgent = newSession.agentId || this.defaultAgentId;
|
|
1681
|
+
let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n Agent: ${currentAgent}\n ${hasExistingSession}`;
|
|
656
1682
|
if (cachedEvents.length > 0 && sendMessage) {
|
|
657
1683
|
for (const event of cachedEvents) {
|
|
658
1684
|
if (event.type === 'completed') {
|
|
@@ -674,43 +1700,46 @@ export class CommandHandler {
|
|
|
674
1700
|
}
|
|
675
1701
|
return response;
|
|
676
1702
|
}
|
|
677
|
-
// /bind
|
|
1703
|
+
// /bind 命令:持久化项目到配置(不切换)
|
|
678
1704
|
if (normalizedContent.startsWith('/bind ')) {
|
|
679
1705
|
const projectPath = normalizedContent.slice(6).trim();
|
|
680
1706
|
if (!projectPath)
|
|
681
|
-
return '用法: /bind
|
|
1707
|
+
return '用法: /bind <路径>';
|
|
682
1708
|
if (!path.isAbsolute(projectPath)) {
|
|
683
1709
|
return '❌ 项目路径必须是绝对路径';
|
|
684
1710
|
}
|
|
685
1711
|
if (!fs.existsSync(projectPath)) {
|
|
686
1712
|
return `❌ 路径不存在: ${projectPath}`;
|
|
687
1713
|
}
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (event.type === 'completed') {
|
|
696
|
-
response += `\n✓ 任务完成`;
|
|
697
|
-
if (event.metadata?.duration) {
|
|
698
|
-
response += ` (耗时: ${Math.round(event.metadata.duration / 1000)}s)`;
|
|
699
|
-
}
|
|
700
|
-
const summary = event.message.substring(0, 200);
|
|
701
|
-
response += `\n${summary}${event.message.length > 200 ? '...' : ''}`;
|
|
702
|
-
}
|
|
703
|
-
else if (event.type === 'error') {
|
|
704
|
-
response += `\n❌ 任务失败: ${event.metadata?.errorType || '未知错误'}`;
|
|
705
|
-
response += `\n${event.message}`;
|
|
706
|
-
}
|
|
1714
|
+
// 生成项目名称(使用目录名)
|
|
1715
|
+
const projectName = path.basename(projectPath);
|
|
1716
|
+
// 检查是否已存在
|
|
1717
|
+
if (this.projects[projectName]) {
|
|
1718
|
+
const existingPath = this.projects[projectName];
|
|
1719
|
+
if (existingPath === projectPath) {
|
|
1720
|
+
return `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
|
|
707
1721
|
}
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
1722
|
+
return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existingPath}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
|
|
1723
|
+
}
|
|
1724
|
+
// 添加到配置
|
|
1725
|
+
if (!this.config.projects) {
|
|
1726
|
+
this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
|
|
1727
|
+
}
|
|
1728
|
+
if (!this.config.projects.list) {
|
|
1729
|
+
this.config.projects.list = {};
|
|
1730
|
+
}
|
|
1731
|
+
this.config.projects.list[projectName] = projectPath;
|
|
1732
|
+
// 保存配置
|
|
1733
|
+
const { saveConfig } = await import('../config.js');
|
|
1734
|
+
saveConfig(this.config);
|
|
1735
|
+
// 更新内存中的项目列表
|
|
1736
|
+
this.projects[projectName] = projectPath;
|
|
1737
|
+
return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
|
|
711
1738
|
}
|
|
712
|
-
// /slist
|
|
713
|
-
|
|
1739
|
+
// /slist 命令:列出当前项目的会话
|
|
1740
|
+
// /slist — 仅 EvolClaw 会话
|
|
1741
|
+
// /slist cli — 仅 CLI 会话(未导入的)
|
|
1742
|
+
if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
|
|
714
1743
|
if (!session) {
|
|
715
1744
|
return `❌ 当前没有活跃会话
|
|
716
1745
|
|
|
@@ -719,110 +1748,244 @@ export class CommandHandler {
|
|
|
719
1748
|
2. /new [名称] - 创建命名会话
|
|
720
1749
|
3. /project <项目> - 切换到指定项目`;
|
|
721
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 会话
|
|
722
1820
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
723
|
-
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath);
|
|
1821
|
+
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
724
1822
|
// 从 SDK 同步会话名称(发现 CLI 改名)
|
|
725
1823
|
try {
|
|
726
|
-
const sdkSessions = await
|
|
1824
|
+
const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
|
|
727
1825
|
for (const sdkSession of sdkSessions) {
|
|
728
|
-
|
|
729
|
-
if (!sdkName)
|
|
1826
|
+
if (!sdkSession.title)
|
|
730
1827
|
continue;
|
|
731
1828
|
const dbSession = currentProjectSessions.find(s => s.agentSessionId === sdkSession.sessionId);
|
|
732
|
-
if (dbSession &&
|
|
733
|
-
await this.sessionManager.renameSession(dbSession.id,
|
|
734
|
-
dbSession.name =
|
|
1829
|
+
if (dbSession && sdkSession.title !== dbSession.name) {
|
|
1830
|
+
await this.sessionManager.renameSession(dbSession.id, sdkSession.title);
|
|
1831
|
+
dbSession.name = sdkSession.title;
|
|
735
1832
|
}
|
|
736
1833
|
}
|
|
737
1834
|
}
|
|
738
1835
|
catch (error) {
|
|
739
1836
|
logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
|
|
740
1837
|
}
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
// 降级:文本列表
|
|
1917
|
+
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
|
|
749
1918
|
if (currentProjectSessions.length > 0) {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
|
|
758
|
-
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
759
|
-
if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId)) {
|
|
760
|
-
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}`);
|
|
761
1926
|
}
|
|
762
1927
|
else {
|
|
763
|
-
|
|
764
|
-
if (s.isActive && isProcessing) {
|
|
765
|
-
status = '[处理中]';
|
|
766
|
-
}
|
|
767
|
-
else if (s.isActive) {
|
|
768
|
-
status = '[活跃]';
|
|
769
|
-
}
|
|
770
|
-
lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
|
|
1928
|
+
lines.push(`${prefix} ${num} ${threadTag}${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
|
|
771
1929
|
}
|
|
772
1930
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const uuid = c.uuid.substring(0, 8);
|
|
782
|
-
lines.push(` ${time} (${uuid}) "${message}"`);
|
|
1931
|
+
const hiddenCount = currentProjectSessions.length - displayIndex - topicCount;
|
|
1932
|
+
if (topicCount > 0 || hiddenCount > 0) {
|
|
1933
|
+
const parts = [];
|
|
1934
|
+
if (hiddenCount > 0)
|
|
1935
|
+
parts.push(`${hiddenCount} 个更早的会话`);
|
|
1936
|
+
if (topicCount > 0)
|
|
1937
|
+
parts.push(`${topicCount} 个话题会话`);
|
|
1938
|
+
lines.push(`\n (已隐藏 ${parts.join('、')})`);
|
|
783
1939
|
}
|
|
784
1940
|
lines.push('');
|
|
785
1941
|
}
|
|
786
1942
|
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
1943
|
+
lines.push('使用 /slist cli 查看 CLI 会话');
|
|
787
1944
|
return lines.join('\n');
|
|
788
1945
|
}
|
|
1946
|
+
// /session(无参数):直接复用 /slist 逻辑(含卡片交互)
|
|
1947
|
+
if (normalizedContent === '/session') {
|
|
1948
|
+
return this.handle('/slist', channel, channelId, undefined, userId, threadId);
|
|
1949
|
+
}
|
|
789
1950
|
// /session 或 /s 命令:切换会话
|
|
790
1951
|
if (normalizedContent.startsWith('/session ')) {
|
|
791
1952
|
const sessionName = normalizedContent.slice(9).trim();
|
|
792
1953
|
if (!sessionName)
|
|
793
1954
|
return '用法: /s <序号、会话名称或前8位UUID>';
|
|
794
|
-
const sessionKey = `${channel}-${channelId}`;
|
|
795
|
-
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
796
|
-
if (queueLength > 0) {
|
|
797
|
-
return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
|
|
798
|
-
}
|
|
799
1955
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
800
|
-
//
|
|
1956
|
+
// 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
|
|
801
1957
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
802
1958
|
const idx = parseInt(sessionName, 10);
|
|
803
1959
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
804
|
-
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
|
|
805
|
-
|
|
806
|
-
|
|
1960
|
+
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1961
|
+
// 与 /slist 显示逻辑一致:超过10个时隐藏非活跃话题会话
|
|
1962
|
+
const hideTopics = projectSessions.length > 10;
|
|
1963
|
+
const visibleSessions = hideTopics
|
|
1964
|
+
? projectSessions.filter(s => !s.threadId)
|
|
1965
|
+
: projectSessions;
|
|
1966
|
+
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
1967
|
+
targetSession = visibleSessions[idx - 1];
|
|
807
1968
|
}
|
|
808
1969
|
else {
|
|
809
|
-
return `❌ 序号超出范围 (1-${
|
|
1970
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /slist 查看可用会话`;
|
|
810
1971
|
}
|
|
811
1972
|
}
|
|
812
1973
|
if (!targetSession && sessionName.length === 8) {
|
|
813
1974
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
814
1975
|
}
|
|
815
|
-
const
|
|
816
|
-
if (!targetSession && sessionName.length === 8 &&
|
|
1976
|
+
const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
|
|
1977
|
+
if (!targetSession && sessionName.length === 8 && canImport) {
|
|
817
1978
|
const projectPaths = Object.values(this.projects);
|
|
818
1979
|
if (session) {
|
|
819
1980
|
projectPaths.unshift(session.projectPath);
|
|
820
1981
|
}
|
|
821
1982
|
for (const projectPath of projectPaths) {
|
|
822
|
-
const
|
|
1983
|
+
const currentAgentId = session?.agentId || this.defaultAgentId;
|
|
1984
|
+
const cliSessions = await this.sessionManager.scanCliSessions(projectPath, currentAgentId);
|
|
823
1985
|
const cliSession = cliSessions.find(c => c.uuid.startsWith(sessionName));
|
|
824
1986
|
if (cliSession) {
|
|
825
|
-
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid);
|
|
1987
|
+
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
|
|
1988
|
+
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
826
1989
|
const projectName = this.getProjectName(projectPath);
|
|
827
1990
|
return `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史`;
|
|
828
1991
|
}
|
|
@@ -832,7 +1995,7 @@ export class CommandHandler {
|
|
|
832
1995
|
return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
|
|
833
1996
|
}
|
|
834
1997
|
const lastInput = targetSession.agentSessionId
|
|
835
|
-
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId)
|
|
1998
|
+
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
|
|
836
1999
|
: null;
|
|
837
2000
|
const lastInputLine = lastInput ? `\n 最后输入: "${lastInput}"` : '';
|
|
838
2001
|
if (!session) {
|
|
@@ -853,6 +2016,7 @@ export class CommandHandler {
|
|
|
853
2016
|
if (!switched) {
|
|
854
2017
|
return `❌ 切换会话失败`;
|
|
855
2018
|
}
|
|
2019
|
+
this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
|
|
856
2020
|
const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
|
|
857
2021
|
return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
|
|
858
2022
|
}
|
|
@@ -873,19 +2037,12 @@ export class CommandHandler {
|
|
|
873
2037
|
if (existing && existing.id !== session.id) {
|
|
874
2038
|
return `❌ 会话名称 "${newName}" 已存在,请使用其他名称`;
|
|
875
2039
|
}
|
|
876
|
-
|
|
877
|
-
if (session.agentSessionId) {
|
|
878
|
-
try {
|
|
879
|
-
await sdkRenameSession(session.agentSessionId, newName, { dir: session.projectPath });
|
|
880
|
-
}
|
|
881
|
-
catch (error) {
|
|
882
|
-
logger.warn(`[CommandHandler] SDK renameSession failed (continuing with db update):`, error);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
2040
|
+
const oldName = session.name || '(未命名)';
|
|
885
2041
|
const success = await this.sessionManager.renameSession(session.id, newName);
|
|
886
2042
|
if (!success) {
|
|
887
2043
|
return `❌ 重命名失败`;
|
|
888
2044
|
}
|
|
2045
|
+
this.eventBus.publish({ type: 'session:renamed', sessionId: session.id, oldName, newName });
|
|
889
2046
|
return `✓ 已将当前会话重命名为: ${newName}`;
|
|
890
2047
|
}
|
|
891
2048
|
// /del 命令:删除指定会话(仅解绑,不删除文件)
|
|
@@ -896,22 +2053,25 @@ export class CommandHandler {
|
|
|
896
2053
|
if (!session) {
|
|
897
2054
|
return `❌ 当前没有活跃会话`;
|
|
898
2055
|
}
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
if (isGroup && !isAdmin) {
|
|
2056
|
+
// 权限检查:policy 控制谁可以删除会话
|
|
2057
|
+
if (!policy.canDeleteSession(session.chatType || 'private', identity.role)) {
|
|
902
2058
|
return `❌ 无权限:群聊中仅管理员可删除会话`;
|
|
903
2059
|
}
|
|
904
2060
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
905
|
-
//
|
|
2061
|
+
// 序号删除(与 /slist 显示序号一致)
|
|
906
2062
|
if (!targetSession && /^\d+$/.test(sessionName)) {
|
|
907
2063
|
const idx = parseInt(sessionName, 10);
|
|
908
2064
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
909
|
-
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
|
|
910
|
-
|
|
911
|
-
|
|
2065
|
+
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
2066
|
+
const hideTopics = projectSessions.length > 10;
|
|
2067
|
+
const visibleSessions = hideTopics
|
|
2068
|
+
? projectSessions.filter(s => !s.threadId)
|
|
2069
|
+
: projectSessions;
|
|
2070
|
+
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
2071
|
+
targetSession = visibleSessions[idx - 1];
|
|
912
2072
|
}
|
|
913
2073
|
else {
|
|
914
|
-
return `❌ 序号超出范围 (1-${
|
|
2074
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /slist 查看可用会话`;
|
|
915
2075
|
}
|
|
916
2076
|
}
|
|
917
2077
|
if (!targetSession && sessionName.length === 8) {
|
|
@@ -927,7 +2087,9 @@ export class CommandHandler {
|
|
|
927
2087
|
if (!success) {
|
|
928
2088
|
return `❌ 删除失败`;
|
|
929
2089
|
}
|
|
930
|
-
|
|
2090
|
+
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
2091
|
+
const targetAgent = this.getAgent(targetSession.agentId);
|
|
2092
|
+
await targetAgent.closeSession(targetSession.id);
|
|
931
2093
|
return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
|
|
932
2094
|
}
|
|
933
2095
|
// /fork 命令:分支当前会话
|
|
@@ -937,11 +2099,16 @@ export class CommandHandler {
|
|
|
937
2099
|
return `❌ 当前没有活跃会话,无法分支`;
|
|
938
2100
|
}
|
|
939
2101
|
if (!session.agentSessionId) {
|
|
940
|
-
return `❌
|
|
2102
|
+
return `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
|
|
2103
|
+
}
|
|
2104
|
+
const forkAgent = this.getAgent(session.agentId);
|
|
2105
|
+
if (!forkAgent.capabilities?.fork) {
|
|
2106
|
+
return `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代`;
|
|
941
2107
|
}
|
|
942
2108
|
try {
|
|
943
|
-
const
|
|
944
|
-
const newSession = await this.sessionManager.createForkedSession(session,
|
|
2109
|
+
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
2110
|
+
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
2111
|
+
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
945
2112
|
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /slist 查看所有会话,/s <名称> 切换回原会话`;
|
|
946
2113
|
}
|
|
947
2114
|
catch (error) {
|
|
@@ -951,66 +2118,49 @@ export class CommandHandler {
|
|
|
951
2118
|
}
|
|
952
2119
|
// /repair 命令:检查并修复会话
|
|
953
2120
|
if (normalizedContent === '/repair') {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const
|
|
2121
|
+
const repairResult = await this.ensureSession(channel, channelId, threadId);
|
|
2122
|
+
if ('error' in repairResult)
|
|
2123
|
+
return repairResult.error;
|
|
2124
|
+
const { session: repairSession } = repairResult;
|
|
2125
|
+
const health = await this.sessionManager.getHealthStatus(repairSession.id);
|
|
958
2126
|
if (!health.safeMode) {
|
|
959
2127
|
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
960
2128
|
}
|
|
961
|
-
const
|
|
962
|
-
const
|
|
2129
|
+
const repairAgent = this.getAgent(repairSession.agentId);
|
|
2130
|
+
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
963
2131
|
try {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
return `✓
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
2132
|
+
if (!repairSession.agentSessionId) {
|
|
2133
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2134
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2135
|
+
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2136
|
+
}
|
|
2137
|
+
// 通过 agent 定位 session 文件
|
|
2138
|
+
const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
|
|
2139
|
+
if (!sessionFile) {
|
|
2140
|
+
// 文件不存在(已被删除或从未创建),直接重置
|
|
2141
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2142
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2143
|
+
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
975
2144
|
}
|
|
976
|
-
const healthCheck = await
|
|
2145
|
+
const healthCheck = await checkSessionFile(sessionFile);
|
|
977
2146
|
if (healthCheck.corrupt) {
|
|
978
|
-
const
|
|
2147
|
+
const backupPath = await backupSessionFile(sessionFile);
|
|
2148
|
+
const fsPromises = await import('fs/promises');
|
|
979
2149
|
await fsPromises.unlink(sessionFile);
|
|
980
|
-
await this.sessionManager.
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
${healthCheck.issues.map((i) => `- ${i}`).join('\n')}
|
|
986
|
-
|
|
987
|
-
修复操作:
|
|
988
|
-
- 已删除损坏文件
|
|
989
|
-
- 已创建新会话
|
|
990
|
-
- 已重置异常计数器
|
|
991
|
-
|
|
992
|
-
备份位置:${backupDir}`;
|
|
2150
|
+
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
2151
|
+
repairAgent.updateSessionId(repairSession.id, '');
|
|
2152
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2153
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2154
|
+
return `✓ 修复完成,已退出安全模式\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
|
|
993
2155
|
}
|
|
994
2156
|
if (healthCheck.issues.length > 0) {
|
|
995
|
-
await this.sessionManager.resetHealthStatus(
|
|
996
|
-
|
|
997
|
-
${healthCheck.issues.map((i) => `- ${i}`).join('\n')}
|
|
998
|
-
|
|
999
|
-
建议:
|
|
1000
|
-
1. 使用 /new 创建新会话
|
|
1001
|
-
2. 旧会话已备份到:${backupDir}
|
|
1002
|
-
|
|
1003
|
-
已重置异常计数器,可继续使用当前会话。`;
|
|
2157
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2158
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2159
|
+
return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
|
|
1004
2160
|
}
|
|
1005
|
-
await this.sessionManager.resetHealthStatus(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
修复内容:
|
|
1009
|
-
- 未发现问题
|
|
1010
|
-
- 已重置异常计数器
|
|
1011
|
-
- 已恢复正常会话模式
|
|
1012
|
-
|
|
1013
|
-
备份位置:${backupDir}`;
|
|
2161
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2162
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2163
|
+
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
1014
2164
|
}
|
|
1015
2165
|
catch (error) {
|
|
1016
2166
|
logger.error('[Repair] Failed:', error);
|
|
@@ -1019,10 +2169,12 @@ ${healthCheck.issues.map((i) => `- ${i}`).join('\n')}
|
|
|
1019
2169
|
}
|
|
1020
2170
|
// /safe 命令:手动进入安全模式
|
|
1021
2171
|
if (normalizedContent === '/safe') {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
2172
|
+
const safeResult = await this.ensureSession(channel, channelId, threadId);
|
|
2173
|
+
if ('error' in safeResult)
|
|
2174
|
+
return safeResult.error;
|
|
2175
|
+
const { session: safeSession } = safeResult;
|
|
2176
|
+
await this.sessionManager.setSafeMode(safeSession.id, true);
|
|
2177
|
+
this.eventBus.publish({ type: 'session:safe-mode-entered', sessionId: safeSession.id, reason: 'manual' });
|
|
1026
2178
|
return `✓ 已进入安全模式
|
|
1027
2179
|
|
|
1028
2180
|
当前行为:
|
|
@@ -1036,11 +2188,4 @@ ${healthCheck.issues.map((i) => `- ${i}`).join('\n')}
|
|
|
1036
2188
|
}
|
|
1037
2189
|
return null;
|
|
1038
2190
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
* 通过 adapter 查询是否为群聊
|
|
1041
|
-
*/
|
|
1042
|
-
async isGroupChat(channel, channelId) {
|
|
1043
|
-
const adapter = this.adapters.get(channel);
|
|
1044
|
-
return await adapter?.isGroupChat?.(channelId) ?? false;
|
|
1045
|
-
}
|
|
1046
2191
|
}
|