evolclaw 2.1.2 → 2.2.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 +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +567 -205
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /package/dist/{core → utils}/message-cache.js +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
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
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import os from 'os';
|
|
7
|
-
const availableModels = ['opus', 'sonnet', 'haiku', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'];
|
|
8
7
|
const availableEfforts = ['low', 'medium', 'high', 'max'];
|
|
9
8
|
function effortBar(level) {
|
|
10
9
|
const levels = {
|
|
@@ -85,30 +84,48 @@ function formatIdleTime(ms) {
|
|
|
85
84
|
return '刚刚';
|
|
86
85
|
}
|
|
87
86
|
// 支持的命令列表
|
|
88
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del'];
|
|
87
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check'];
|
|
89
88
|
// 命令别名映射
|
|
90
89
|
const aliases = {
|
|
91
90
|
'/p': '/project',
|
|
92
91
|
'/s': '/session',
|
|
93
92
|
'/name': '/rename'
|
|
94
93
|
};
|
|
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 '];
|
|
94
|
+
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
95
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name '];
|
|
99
96
|
export class CommandHandler {
|
|
100
97
|
sessionManager;
|
|
101
|
-
agentRunner;
|
|
102
98
|
config;
|
|
103
99
|
messageCache;
|
|
100
|
+
eventBus;
|
|
104
101
|
adapters = new Map();
|
|
102
|
+
policies = new Map();
|
|
103
|
+
channelObjects = new Map(); // name → actual channel instance (for /check)
|
|
105
104
|
processor;
|
|
106
105
|
messageQueue;
|
|
107
|
-
|
|
106
|
+
permissionGateway;
|
|
107
|
+
statsCollector;
|
|
108
|
+
agentMap;
|
|
109
|
+
defaultAgentId;
|
|
110
|
+
/** 按 agentId 获取 agent,回退到默认 */
|
|
111
|
+
getAgent(agentId) {
|
|
112
|
+
if (agentId && this.agentMap.has(agentId))
|
|
113
|
+
return this.agentMap.get(agentId);
|
|
114
|
+
return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
|
|
115
|
+
}
|
|
116
|
+
constructor(sessionManager, agentRunnerOrMap, config, messageCache, eventBus, defaultAgentId) {
|
|
108
117
|
this.sessionManager = sessionManager;
|
|
109
|
-
this.agentRunner = agentRunner;
|
|
110
118
|
this.config = config;
|
|
111
119
|
this.messageCache = messageCache;
|
|
120
|
+
this.eventBus = eventBus;
|
|
121
|
+
if (agentRunnerOrMap instanceof Map) {
|
|
122
|
+
this.agentMap = agentRunnerOrMap;
|
|
123
|
+
this.defaultAgentId = defaultAgentId || 'claude';
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
|
|
127
|
+
this.defaultAgentId = agentRunnerOrMap.name;
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
/** 项目列表快捷访问 */
|
|
114
131
|
get projects() {
|
|
@@ -122,22 +139,37 @@ export class CommandHandler {
|
|
|
122
139
|
getProjectName(projectPath) {
|
|
123
140
|
return this.getConfiguredProjectName(projectPath) || path.basename(projectPath);
|
|
124
141
|
}
|
|
142
|
+
/** 格式化运行时间 */
|
|
143
|
+
formatUptime(ms) {
|
|
144
|
+
const sec = Math.floor(ms / 1000);
|
|
145
|
+
const d = Math.floor(sec / 86400);
|
|
146
|
+
const h = Math.floor((sec % 86400) / 3600);
|
|
147
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
148
|
+
const parts = [];
|
|
149
|
+
if (d > 0)
|
|
150
|
+
parts.push(`${d}天`);
|
|
151
|
+
if (h > 0)
|
|
152
|
+
parts.push(`${h}时`);
|
|
153
|
+
parts.push(`${m}分`);
|
|
154
|
+
return parts.join('');
|
|
155
|
+
}
|
|
125
156
|
/** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
|
|
126
|
-
getQueueKey(session,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return `${channel}-${channelId}`;
|
|
157
|
+
getQueueKey(session, _channel, _channelId) {
|
|
158
|
+
// 队列和 agent 均使用 session.id 作为 key
|
|
159
|
+
return session?.id || '';
|
|
130
160
|
}
|
|
131
|
-
/** 从 session
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
|
|
161
|
+
/** 从 session 提取渠道预构建的回复上下文 */
|
|
162
|
+
getReplyContext(session) {
|
|
163
|
+
return session.metadata?.replyContext;
|
|
135
164
|
}
|
|
136
165
|
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
137
166
|
async ensureSession(channel, channelId, threadId) {
|
|
138
167
|
if (threadId) {
|
|
139
|
-
//
|
|
140
|
-
const session = await this.sessionManager.
|
|
168
|
+
// 话题会话:仅查询,不创建
|
|
169
|
+
const session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
170
|
+
if (!session) {
|
|
171
|
+
return { error: '❌ 话题中尚未创建会话\n发送消息后自动创建' };
|
|
172
|
+
}
|
|
141
173
|
return { session };
|
|
142
174
|
}
|
|
143
175
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
@@ -152,12 +184,112 @@ export class CommandHandler {
|
|
|
152
184
|
setMessageQueue(messageQueue) {
|
|
153
185
|
this.messageQueue = messageQueue;
|
|
154
186
|
}
|
|
187
|
+
setPermissionGateway(gateway) {
|
|
188
|
+
this.permissionGateway = gateway;
|
|
189
|
+
}
|
|
190
|
+
setStatsCollector(collector) {
|
|
191
|
+
this.statsCollector = collector;
|
|
192
|
+
}
|
|
155
193
|
registerAdapter(adapter) {
|
|
156
194
|
this.adapters.set(adapter.name, adapter);
|
|
157
195
|
}
|
|
196
|
+
registerChannel(name, channel) {
|
|
197
|
+
this.channelObjects.set(name, channel);
|
|
198
|
+
}
|
|
199
|
+
registerPolicy(channelName, policy) {
|
|
200
|
+
this.policies.set(channelName, policy);
|
|
201
|
+
}
|
|
158
202
|
getAdapter(channelName) {
|
|
159
203
|
return this.adapters.get(channelName);
|
|
160
204
|
}
|
|
205
|
+
getPolicy(channel) {
|
|
206
|
+
return this.policies.get(channel) || {
|
|
207
|
+
canSwitchProject: () => true,
|
|
208
|
+
canListProjects: () => true,
|
|
209
|
+
canCreateSession: () => true,
|
|
210
|
+
canDeleteSession: () => true,
|
|
211
|
+
canImportCliSession: () => true,
|
|
212
|
+
messagePrefix: () => '',
|
|
213
|
+
showMiddleResult: () => true,
|
|
214
|
+
showIdleMonitor: () => true,
|
|
215
|
+
accumulateErrors: () => true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 返回结构化命令菜单(供 menu.query 使用)
|
|
220
|
+
* admin 看到全部命令分组,guest 仅看到用户级命令
|
|
221
|
+
*/
|
|
222
|
+
getMenuItems(isAdmin) {
|
|
223
|
+
const items = [];
|
|
224
|
+
if (isAdmin) {
|
|
225
|
+
items.push({
|
|
226
|
+
group: '项目管理',
|
|
227
|
+
commands: [
|
|
228
|
+
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
229
|
+
{ cmd: '/plist', label: '列出所有配置的项目' },
|
|
230
|
+
{ cmd: '/p', args: '<name|path>', label: '切换项目' },
|
|
231
|
+
{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
|
|
232
|
+
]
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
items.push({
|
|
236
|
+
group: '会话管理',
|
|
237
|
+
commands: [
|
|
238
|
+
{ cmd: '/new', args: '[name]', label: '创建新会话' },
|
|
239
|
+
{ cmd: '/slist', label: '列出当前项目的所有会话' },
|
|
240
|
+
{ cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
|
|
241
|
+
{ cmd: '/name', args: '<name>', label: '重命名当前会话' },
|
|
242
|
+
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
243
|
+
...(isAdmin ? [
|
|
244
|
+
{ cmd: '/fork', args: '[name]', label: '分支当前会话' },
|
|
245
|
+
{ cmd: '/clear', label: '清空会话对话历史' },
|
|
246
|
+
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
247
|
+
] : []),
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
if (isAdmin) {
|
|
251
|
+
items.push({
|
|
252
|
+
group: 'Agent 与模型',
|
|
253
|
+
commands: [
|
|
254
|
+
{ cmd: '/agent', args: '[name]', label: '查看或切换 Agent 后端' },
|
|
255
|
+
{ cmd: '/model', args: '[model] [effort]', label: '查看或切换模型' },
|
|
256
|
+
]
|
|
257
|
+
});
|
|
258
|
+
items.push({
|
|
259
|
+
group: '权限管理',
|
|
260
|
+
commands: [
|
|
261
|
+
{ cmd: '/perm', args: '[mode|allow|deny]', label: '权限模式管理' },
|
|
262
|
+
]
|
|
263
|
+
});
|
|
264
|
+
items.push({
|
|
265
|
+
group: '运维',
|
|
266
|
+
commands: [
|
|
267
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
268
|
+
{ cmd: '/stop', label: '中断当前任务' },
|
|
269
|
+
{ cmd: '/restart', label: '重启服务' },
|
|
270
|
+
{ cmd: '/repair', label: '检查并修复会话' },
|
|
271
|
+
{ cmd: '/safe', label: '进入安全模式' },
|
|
272
|
+
{ cmd: '/send', args: '[渠道] <path>', label: '发送项目内文件' },
|
|
273
|
+
{ cmd: '/check', args: '[rty <channel>]', label: '检查渠道状态或重连指定渠道' },
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
items.push({
|
|
279
|
+
group: '其他',
|
|
280
|
+
commands: [
|
|
281
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
282
|
+
]
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
items.push({
|
|
286
|
+
group: '帮助',
|
|
287
|
+
commands: [
|
|
288
|
+
{ cmd: '/help', label: '显示帮助信息' },
|
|
289
|
+
]
|
|
290
|
+
});
|
|
291
|
+
return items;
|
|
292
|
+
}
|
|
161
293
|
/**
|
|
162
294
|
* 快速判断是否为命令(不进队列的命令)
|
|
163
295
|
*/
|
|
@@ -168,6 +300,12 @@ export class CommandHandler {
|
|
|
168
300
|
* 主命令处理入口
|
|
169
301
|
*/
|
|
170
302
|
async handle(content, channel, channelId, sendMessage, userId, threadId) {
|
|
303
|
+
// 解析身份
|
|
304
|
+
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
305
|
+
const policy = this.getPolicy(channel);
|
|
306
|
+
// 按当前会话选择 agent 后端
|
|
307
|
+
const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
|
|
308
|
+
const agent = this.getAgent(activeSession?.agentId);
|
|
171
309
|
// 规范化命令(将别名转换为完整命令)
|
|
172
310
|
let normalizedContent = content;
|
|
173
311
|
for (const [alias, full] of Object.entries(aliases)) {
|
|
@@ -176,16 +314,19 @@ export class CommandHandler {
|
|
|
176
314
|
break;
|
|
177
315
|
}
|
|
178
316
|
}
|
|
179
|
-
|
|
180
|
-
|
|
317
|
+
if (normalizedContent !== content) {
|
|
318
|
+
logger.debug(`[CommandHandler] normalized: "${content}" -> "${normalizedContent}"`);
|
|
319
|
+
}
|
|
181
320
|
// 话题内禁用部分命令
|
|
182
321
|
if (threadId) {
|
|
183
|
-
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del'];
|
|
322
|
+
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del', '/agent'];
|
|
184
323
|
const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
|
|
185
|
-
if (isBlocked)
|
|
324
|
+
if (isBlocked) {
|
|
186
325
|
return '⚠️ 话题中不支持此命令';
|
|
326
|
+
}
|
|
187
327
|
}
|
|
188
|
-
|
|
328
|
+
// 权限检查:区分用户级命令和管理级命令
|
|
329
|
+
const isAdmin = identity.role === 'owner';
|
|
189
330
|
if (normalizedContent.startsWith('/')) {
|
|
190
331
|
const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
191
332
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
@@ -193,6 +334,23 @@ export class CommandHandler {
|
|
|
193
334
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
194
335
|
}
|
|
195
336
|
}
|
|
337
|
+
// 空闲检查:某些命令需要等待当前会话空闲
|
|
338
|
+
const requiresIdle = ['/new', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind'];
|
|
339
|
+
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
340
|
+
if (threadId) {
|
|
341
|
+
// 话题中:检查话题 session 是否在处理(不创建)
|
|
342
|
+
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
343
|
+
if (threadSession) {
|
|
344
|
+
const threadAgent = this.getAgent(threadSession.agentId);
|
|
345
|
+
if (threadAgent.hasActiveStream(threadSession.id)) {
|
|
346
|
+
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (activeSession && agent.hasActiveStream(activeSession.id)) {
|
|
351
|
+
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
196
354
|
// 检查是否以 / 开头(可能是命令)
|
|
197
355
|
if (normalizedContent.startsWith('/')) {
|
|
198
356
|
const inputCmd = normalizedContent.split(' ')[0];
|
|
@@ -216,56 +374,171 @@ export class CommandHandler {
|
|
|
216
374
|
// /help 命令不需要会话
|
|
217
375
|
if (normalizedContent === '/help') {
|
|
218
376
|
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
|
-
/model [model] [effort] - 查看或切换模型/推理强度
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
/
|
|
377
|
+
const lines = [
|
|
378
|
+
'可用命令:',
|
|
379
|
+
'',
|
|
380
|
+
'🔄 会话管理:',
|
|
381
|
+
' /new [名称] - 创建新会话(可选命名)',
|
|
382
|
+
' /slist - 列出当前项目的所有会话',
|
|
383
|
+
' /s, /session <名称|序号|uuid> - 切换到指定会话',
|
|
384
|
+
' /name, /rename <新名称> - 重命名当前会话',
|
|
385
|
+
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
386
|
+
' /status - 显示会话状态',
|
|
387
|
+
'',
|
|
388
|
+
'❓ 帮助:',
|
|
389
|
+
' /help - 显示此帮助信息',
|
|
390
|
+
];
|
|
391
|
+
return lines.join('\n');
|
|
392
|
+
}
|
|
393
|
+
const lines = [
|
|
394
|
+
'可用命令:',
|
|
395
|
+
'',
|
|
396
|
+
'📁 项目管理:',
|
|
397
|
+
' /pwd - 显示当前项目路径',
|
|
398
|
+
' /plist - 列出所有配置的项目',
|
|
399
|
+
' /p, /project <name|path> - 切换项目',
|
|
400
|
+
' /bind <path> - 绑定新项目目录',
|
|
401
|
+
'',
|
|
402
|
+
'🔄 会话管理:',
|
|
403
|
+
' /new [名称] - 创建新会话(可选命名)',
|
|
404
|
+
' /slist - 列出当前项目的所有会话',
|
|
405
|
+
' /s, /session <名称> - 切换到指定会话',
|
|
406
|
+
' /name, /rename <新名称> - 重命名当前会话',
|
|
407
|
+
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
408
|
+
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
409
|
+
' /clear - 清空当前会话的对话历史',
|
|
410
|
+
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
411
|
+
'',
|
|
412
|
+
'🤖 Agent 与模型:',
|
|
413
|
+
' /agent [name] - 查看或切换 Agent 后端',
|
|
414
|
+
' /model [model] [effort] - 查看或切换模型/推理强度',
|
|
415
|
+
'',
|
|
416
|
+
'🔐 权限管理:',
|
|
417
|
+
' /perm - 查看当前权限模式',
|
|
418
|
+
' /perm <default|request|edit|plan|noask> - 切换权限模式',
|
|
419
|
+
' /perm allow|deny - 审批权限请求',
|
|
420
|
+
'',
|
|
421
|
+
'🛠️ 运维:',
|
|
422
|
+
' /status - 显示会话状态',
|
|
423
|
+
' /stop - 中断当前任务',
|
|
424
|
+
' /restart - 重启服务',
|
|
425
|
+
' /repair - 检查并修复会话',
|
|
426
|
+
' /safe - 进入安全模式',
|
|
427
|
+
' /send [渠道] <路径> - 发送项目内文件',
|
|
428
|
+
'',
|
|
429
|
+
'❓ 帮助:',
|
|
430
|
+
' /help - 显示此帮助信息',
|
|
431
|
+
];
|
|
432
|
+
return lines.join('\n');
|
|
433
|
+
}
|
|
434
|
+
// /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
|
|
435
|
+
if (normalizedContent.startsWith('/perm')) {
|
|
436
|
+
const args = normalizedContent.slice(5).trim();
|
|
437
|
+
// 先获取正确的 session 和 agent(话题可能用不同 agent)
|
|
438
|
+
const permResult = await this.ensureSession(channel, channelId, threadId);
|
|
439
|
+
if ('error' in permResult)
|
|
440
|
+
return permResult.error;
|
|
441
|
+
const { session: permSession } = permResult;
|
|
442
|
+
const permAgent = this.getAgent(permSession.agentId);
|
|
443
|
+
// /perm(无参数):显示当前模式和可选模式
|
|
444
|
+
if (!args) {
|
|
445
|
+
if (!hasPermissionController(permAgent)) {
|
|
446
|
+
return '❌ 权限控制不可用';
|
|
447
|
+
}
|
|
448
|
+
const currentMode = permSession.metadata?.permissionMode || 'default';
|
|
449
|
+
const modes = permAgent.listModes();
|
|
450
|
+
const modeList = modes.map(m => {
|
|
451
|
+
const prefix = m.key === currentMode ? '▶' : ' ';
|
|
452
|
+
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
453
|
+
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
454
|
+
}).join('\n');
|
|
455
|
+
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|deny 审批权限请求`;
|
|
456
|
+
}
|
|
457
|
+
const parts = args.split(/\s+/);
|
|
458
|
+
// /perm <mode> 或 /perm allow|deny:切换模式 / 快捷审批
|
|
459
|
+
if (parts.length === 1) {
|
|
460
|
+
const arg = parts[0];
|
|
461
|
+
// /perm allow|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
|
|
462
|
+
if (arg === 'allow' || arg === 'deny') {
|
|
463
|
+
if (!this.permissionGateway) {
|
|
464
|
+
return '❌ 权限审批未启用';
|
|
465
|
+
}
|
|
466
|
+
const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
|
|
467
|
+
if (pendingIds.length === 0) {
|
|
468
|
+
return '❌ 当前没有待审批的权限请求';
|
|
469
|
+
}
|
|
470
|
+
if (pendingIds.length > 1) {
|
|
471
|
+
return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}`;
|
|
472
|
+
}
|
|
473
|
+
const requestId = pendingIds[0];
|
|
474
|
+
this.permissionGateway.resolvePermission(permSession.id, requestId, arg === 'allow');
|
|
475
|
+
return arg === 'allow' ? `✓ 已授权,继续执行……` : `✓ 已拒绝`;
|
|
476
|
+
}
|
|
477
|
+
// /perm <mode>:切换权限模式
|
|
478
|
+
if (hasPermissionController(permAgent)) {
|
|
479
|
+
const modes = permAgent.listModes();
|
|
480
|
+
const matched = modes.find(m => m.key === arg);
|
|
481
|
+
if (matched) {
|
|
482
|
+
if (!matched.available) {
|
|
483
|
+
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
484
|
+
}
|
|
485
|
+
const metadata = permSession.metadata || {};
|
|
486
|
+
metadata.permissionMode = arg;
|
|
487
|
+
await this.sessionManager.updateSession(permSession.id, { metadata });
|
|
488
|
+
return `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// 不是已知模式名也不是 allow/deny
|
|
492
|
+
const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'default|request|edit|plan|noask';
|
|
493
|
+
return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|deny`;
|
|
494
|
+
}
|
|
495
|
+
// 双参数不再支持,提示正确用法
|
|
496
|
+
const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'default|request|edit|plan|noask';
|
|
497
|
+
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|deny`;
|
|
498
|
+
}
|
|
499
|
+
// /agent 命令:查看或切换 Agent 后端
|
|
500
|
+
if (normalizedContent.startsWith('/agent')) {
|
|
501
|
+
if (!isAdmin)
|
|
502
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
503
|
+
const args = normalizedContent.slice(6).trim();
|
|
504
|
+
const available = [...this.agentMap.keys()];
|
|
505
|
+
if (!args) {
|
|
506
|
+
const currentAgent = activeSession?.agentId || this.defaultAgentId;
|
|
507
|
+
const list = available.map(a => `${a === currentAgent ? '✓' : '-'} ${a}`).join('\n');
|
|
508
|
+
return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
|
|
509
|
+
}
|
|
510
|
+
if (!this.agentMap.has(args)) {
|
|
511
|
+
return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
|
|
512
|
+
}
|
|
513
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
514
|
+
if ('error' in result)
|
|
515
|
+
return result.error;
|
|
516
|
+
const { session } = result;
|
|
517
|
+
// 取消原会话的 pending 权限请求
|
|
518
|
+
if (this.permissionGateway) {
|
|
519
|
+
this.permissionGateway.cancelAll(session.id);
|
|
520
|
+
}
|
|
521
|
+
// 切换到目标 agent(恢复已有会话或创建新会话)
|
|
522
|
+
const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
|
|
523
|
+
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
524
|
+
const projectName = this.getProjectName(session.projectPath);
|
|
525
|
+
return `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
|
|
260
526
|
}
|
|
261
527
|
// /model 命令:查看或切换模型/推理强度
|
|
262
528
|
if (normalizedContent.startsWith('/model')) {
|
|
263
529
|
const args = normalizedContent.slice(6).trim();
|
|
530
|
+
// 获取当前会话(话题会话可能绑定不同 agent)
|
|
531
|
+
const modelResult = await this.ensureSession(channel, channelId, threadId);
|
|
532
|
+
if ('error' in modelResult)
|
|
533
|
+
return modelResult.error;
|
|
534
|
+
const { session: modelSession } = modelResult;
|
|
535
|
+
const modelAgent = this.getAgent(modelSession.agentId);
|
|
536
|
+
const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
|
|
264
537
|
if (!args) {
|
|
265
|
-
const currentModel =
|
|
266
|
-
const currentEffort =
|
|
538
|
+
const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
|
|
539
|
+
const currentEffort = modelAgent.getEffort?.() || 'auto';
|
|
267
540
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : `${currentEffort} ${effortBar(currentEffort)}`;
|
|
268
|
-
const modelList =
|
|
541
|
+
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
269
542
|
return `当前模型: ${currentModel}\n推理强度: ${effortDisplay}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')} / auto\n\n用法:\n /model <model> 切换模型\n /model <model> <effort> 切换模型+推理强度\n /model <effort> 仅切换推理强度\n /model auto 恢复SDK默认`;
|
|
270
543
|
}
|
|
271
544
|
const parts = args.split(/\s+/);
|
|
@@ -274,34 +547,49 @@ export class CommandHandler {
|
|
|
274
547
|
if (parts.length === 1) {
|
|
275
548
|
const arg = parts[0];
|
|
276
549
|
if (arg === 'auto') {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
550
|
+
modelAgent.setEffort?.(undefined);
|
|
551
|
+
// 写回来源
|
|
552
|
+
const isCodex = modelAgent.name === 'codex';
|
|
553
|
+
if (isCodex) {
|
|
554
|
+
if (this.config.agents?.openai?.reasoning) {
|
|
555
|
+
delete this.config.agents.openai.reasoning;
|
|
556
|
+
try {
|
|
557
|
+
saveConfig(this.config);
|
|
558
|
+
}
|
|
559
|
+
catch { }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
|
|
564
|
+
if (configuredInEvolclaw) {
|
|
565
|
+
delete this.config.agents.anthropic.effort;
|
|
566
|
+
try {
|
|
567
|
+
saveConfig(this.config);
|
|
568
|
+
}
|
|
569
|
+
catch { }
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
writeUserSettings({ effortLevel: null });
|
|
573
|
+
}
|
|
285
574
|
}
|
|
286
|
-
this.agentRunner.setEffort(undefined);
|
|
287
575
|
return '✓ 推理强度已恢复为 auto (SDK默认)';
|
|
288
576
|
}
|
|
289
577
|
// 单参数:模型 或 effort
|
|
290
578
|
if (availableEfforts.includes(arg)) {
|
|
291
579
|
newEffort = arg;
|
|
292
580
|
}
|
|
293
|
-
else if (
|
|
581
|
+
else if (models.includes(arg)) {
|
|
294
582
|
newModel = arg;
|
|
295
583
|
}
|
|
296
584
|
else {
|
|
297
|
-
const modelList =
|
|
585
|
+
const modelList = models.map((m) => `- ${m}`).join('\n');
|
|
298
586
|
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')}`;
|
|
299
587
|
}
|
|
300
588
|
}
|
|
301
589
|
else {
|
|
302
590
|
// 双参数:model effort
|
|
303
591
|
const [modelArg, effortArg] = parts;
|
|
304
|
-
if (!
|
|
592
|
+
if (!models.includes(modelArg)) {
|
|
305
593
|
return `❌ 无效的模型ID: ${modelArg}`;
|
|
306
594
|
}
|
|
307
595
|
if (!availableEfforts.includes(effortArg)) {
|
|
@@ -312,53 +600,109 @@ export class CommandHandler {
|
|
|
312
600
|
}
|
|
313
601
|
if (!this.config.agents)
|
|
314
602
|
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;
|
|
603
|
+
const isCodexAgent = modelAgent.name === 'codex';
|
|
322
604
|
const changes = [];
|
|
323
|
-
const updates = {};
|
|
324
605
|
if (newModel) {
|
|
325
|
-
|
|
326
|
-
this.
|
|
606
|
+
modelAgent.setModel?.(newModel);
|
|
607
|
+
this.eventBus.publish({
|
|
608
|
+
type: 'agent:model-changed',
|
|
609
|
+
sessionId: modelSession.id,
|
|
610
|
+
model: newModel,
|
|
611
|
+
timestamp: Date.now()
|
|
612
|
+
});
|
|
327
613
|
changes.push(`模型: ${newModel}`);
|
|
328
614
|
}
|
|
329
615
|
if (newEffort) {
|
|
330
|
-
const modelAfterSwitch = newModel ??
|
|
616
|
+
const modelAfterSwitch = newModel ?? (hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name);
|
|
331
617
|
if (newEffort === 'max' && !modelAfterSwitch.includes('opus')) {
|
|
332
618
|
return '⚠️ max 推理强度仅 Opus 模型支持(opus / claude-opus-4-6)';
|
|
333
619
|
}
|
|
334
|
-
|
|
335
|
-
this.agentRunner.setEffort(newEffort);
|
|
620
|
+
modelAgent.setEffort?.(newEffort);
|
|
336
621
|
changes.push(`推理强度: ${newEffort} ${effortBar(newEffort)}`);
|
|
337
622
|
}
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
623
|
+
// 持久化:写回来源(就近原则)
|
|
624
|
+
// evolclaw.json 配了 → 写 evolclaw.json
|
|
625
|
+
// evolclaw.json 没配 → 写 agent 全局配置
|
|
626
|
+
if (isCodexAgent) {
|
|
627
|
+
const configuredInEvolclaw = !!(this.config.agents?.openai?.model || this.config.agents?.openai?.reasoning);
|
|
628
|
+
if (configuredInEvolclaw) {
|
|
629
|
+
if (!this.config.agents.openai)
|
|
630
|
+
this.config.agents.openai = {};
|
|
631
|
+
if (newModel)
|
|
632
|
+
this.config.agents.openai.model = newModel;
|
|
633
|
+
if (newEffort)
|
|
634
|
+
this.config.agents.openai.reasoning = newEffort;
|
|
635
|
+
try {
|
|
636
|
+
saveConfig(this.config);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
// Codex 全局配置(~/.codex/config.toml)目前不支持写入,回退到 evolclaw.json
|
|
644
|
+
if (!this.config.agents.openai)
|
|
645
|
+
this.config.agents.openai = {};
|
|
646
|
+
if (newModel)
|
|
647
|
+
this.config.agents.openai.model = newModel;
|
|
648
|
+
if (newEffort)
|
|
649
|
+
this.config.agents.openai.reasoning = newEffort;
|
|
650
|
+
try {
|
|
651
|
+
saveConfig(this.config);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
|
|
660
|
+
if (configuredInEvolclaw) {
|
|
661
|
+
if (!this.config.agents.anthropic)
|
|
662
|
+
this.config.agents.anthropic = {};
|
|
663
|
+
if (newModel)
|
|
664
|
+
this.config.agents.anthropic.model = newModel;
|
|
665
|
+
if (newEffort)
|
|
666
|
+
this.config.agents.anthropic.effort = newEffort;
|
|
667
|
+
try {
|
|
668
|
+
saveConfig(this.config);
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
const updates = {};
|
|
676
|
+
if (newModel)
|
|
677
|
+
updates.model = newModel;
|
|
678
|
+
if (newEffort)
|
|
679
|
+
updates.effortLevel = newEffort;
|
|
680
|
+
const writeResult = writeUserSettings(updates);
|
|
681
|
+
if (!writeResult.success) {
|
|
682
|
+
return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
342
685
|
}
|
|
343
686
|
return `✓ 已切换\n ${changes.join('\n ')}`;
|
|
344
687
|
}
|
|
345
688
|
// /stop 命令:中断当前任务
|
|
346
689
|
if (normalizedContent === '/stop') {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
else {
|
|
354
|
-
sessionKey = `${channel}-${channelId}`;
|
|
355
|
-
}
|
|
690
|
+
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
691
|
+
if ('error' in stopResult)
|
|
692
|
+
return '当前没有正在处理的任务';
|
|
693
|
+
const { session: stopSession } = stopResult;
|
|
694
|
+
const stopAgent = this.getAgent(stopSession.agentId);
|
|
695
|
+
const sessionKey = stopSession.id;
|
|
356
696
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
357
|
-
const hasActive =
|
|
697
|
+
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
358
698
|
if (queueLength === 0 && !hasActive) {
|
|
359
699
|
return '当前没有正在处理的任务';
|
|
360
700
|
}
|
|
361
|
-
await
|
|
701
|
+
await stopAgent.interrupt(sessionKey);
|
|
702
|
+
// 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
|
|
703
|
+
this.eventBus.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'stop' });
|
|
704
|
+
// 强制清除 processing_state
|
|
705
|
+
this.sessionManager.clearProcessing(sessionKey);
|
|
362
706
|
return '✓ 已发送中断信号,任务将尽快停止';
|
|
363
707
|
}
|
|
364
708
|
// /clear 命令:通过 SDK /clear 清空会话历史
|
|
@@ -367,20 +711,30 @@ export class CommandHandler {
|
|
|
367
711
|
if ('error' in result)
|
|
368
712
|
return result.error;
|
|
369
713
|
const { session } = result;
|
|
714
|
+
const sessionAgent = this.getAgent(session.agentId);
|
|
715
|
+
if (!sessionAgent.capabilities?.clear) {
|
|
716
|
+
return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代`;
|
|
717
|
+
}
|
|
370
718
|
if (!session.agentSessionId) {
|
|
371
719
|
return '❌ 当前会话没有历史记录,无需清空';
|
|
372
720
|
}
|
|
373
721
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
374
722
|
? session.projectPath
|
|
375
723
|
: path.resolve(process.cwd(), session.projectPath);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
await
|
|
379
|
-
|
|
380
|
-
|
|
724
|
+
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
725
|
+
try {
|
|
726
|
+
const cleared = await sessionAgent.clearSession(session.id, session.agentSessionId, projectPath);
|
|
727
|
+
if (cleared) {
|
|
728
|
+
await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
|
|
729
|
+
sessionAgent.updateSessionId(session.id, '');
|
|
730
|
+
return '✅ 已清空当前会话的对话历史';
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
return '❌ 清空会话失败,请稍后重试';
|
|
734
|
+
}
|
|
381
735
|
}
|
|
382
|
-
|
|
383
|
-
|
|
736
|
+
finally {
|
|
737
|
+
releaseLock();
|
|
384
738
|
}
|
|
385
739
|
}
|
|
386
740
|
// /compact 命令:手动压缩会话上下文
|
|
@@ -389,21 +743,31 @@ export class CommandHandler {
|
|
|
389
743
|
if ('error' in result)
|
|
390
744
|
return result.error;
|
|
391
745
|
const { session } = result;
|
|
746
|
+
const sessionAgent = this.getAgent(session.agentId);
|
|
747
|
+
if (!sessionAgent.capabilities?.compact) {
|
|
748
|
+
return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact`;
|
|
749
|
+
}
|
|
392
750
|
if (!session.agentSessionId) {
|
|
393
751
|
return '❌ 当前会话没有历史记录,无需压缩';
|
|
394
752
|
}
|
|
395
753
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
396
754
|
? session.projectPath
|
|
397
755
|
: path.resolve(process.cwd(), session.projectPath);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
756
|
+
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
757
|
+
try {
|
|
758
|
+
if (sendMessage) {
|
|
759
|
+
await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
|
|
760
|
+
}
|
|
761
|
+
const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
|
|
762
|
+
if (compacted) {
|
|
763
|
+
return '✅ 会话上下文已压缩';
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
return '❌ 会话压缩失败,请稍后重试';
|
|
767
|
+
}
|
|
404
768
|
}
|
|
405
|
-
|
|
406
|
-
|
|
769
|
+
finally {
|
|
770
|
+
releaseLock();
|
|
407
771
|
}
|
|
408
772
|
}
|
|
409
773
|
// 尝试获取活跃会话(话题时直接查找话题 session)
|
|
@@ -414,26 +778,37 @@ export class CommandHandler {
|
|
|
414
778
|
else {
|
|
415
779
|
session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
416
780
|
}
|
|
417
|
-
//
|
|
781
|
+
// 对于需要会话的命令,如果没有会话则使用默认项目创建临时会话
|
|
782
|
+
// 这样 /pwd、/status 等命令可以在没有活跃会话时返回默认项目信息
|
|
418
783
|
if (!session && (normalizedContent.startsWith('/new') ||
|
|
419
784
|
normalizedContent.startsWith('/bind') ||
|
|
420
|
-
normalizedContent.startsWith('/project')
|
|
785
|
+
normalizedContent.startsWith('/project') ||
|
|
786
|
+
normalizedContent === '/pwd' ||
|
|
787
|
+
normalizedContent === '/status')) {
|
|
421
788
|
session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd());
|
|
422
789
|
}
|
|
423
790
|
// /status 命令:显示会话状态
|
|
424
791
|
if (normalizedContent === '/status') {
|
|
792
|
+
// session 现在总是存在(上面已自动创建)
|
|
425
793
|
if (!session) {
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
❌ 当前未创建会话
|
|
429
|
-
|
|
430
|
-
提示:发送任意消息或使用 /new 命令创建会话`;
|
|
794
|
+
return `❌ 无法创建会话,请检查配置`;
|
|
431
795
|
}
|
|
432
796
|
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
433
|
-
const
|
|
797
|
+
const sessionAgent = this.getAgent(session.agentId);
|
|
798
|
+
const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
|
|
434
799
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
435
800
|
const isThread = !!session.threadId;
|
|
436
|
-
|
|
801
|
+
let sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
|
|
802
|
+
// 处理中时显示时长
|
|
803
|
+
if (isCurrentlyProcessing) {
|
|
804
|
+
const elapsed = Date.now() - parseInt(session.processingState, 10);
|
|
805
|
+
if (!isNaN(elapsed) && elapsed > 0) {
|
|
806
|
+
const sec = Math.floor(elapsed / 1000);
|
|
807
|
+
sessionStatus = sec < 60 ? `处理中 (${sec}秒)` :
|
|
808
|
+
sec < 3600 ? `处理中 (${Math.floor(sec / 60)}分钟)` :
|
|
809
|
+
`处理中 (${Math.floor(sec / 3600)}小时)`;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
437
812
|
const projectName = this.getProjectName(session.projectPath);
|
|
438
813
|
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
439
814
|
const timeSinceSuccess = Date.now() - health.lastSuccessTime;
|
|
@@ -443,7 +818,7 @@ export class CommandHandler {
|
|
|
443
818
|
// 获取会话文件信息并同步 name
|
|
444
819
|
let sessionTurns = 0;
|
|
445
820
|
if (session.agentSessionId) {
|
|
446
|
-
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId);
|
|
821
|
+
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId, session.agentId);
|
|
447
822
|
sessionTurns = fileInfo.turns;
|
|
448
823
|
if (fileInfo.title && fileInfo.title !== session.name) {
|
|
449
824
|
await this.sessionManager.renameSession(session.id, fileInfo.title);
|
|
@@ -452,10 +827,17 @@ export class CommandHandler {
|
|
|
452
827
|
}
|
|
453
828
|
const lines = [];
|
|
454
829
|
if (isAdmin) {
|
|
455
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}
|
|
830
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
831
|
+
if (health.consecutiveErrors > 0) {
|
|
832
|
+
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
833
|
+
}
|
|
834
|
+
if (health.safeMode) {
|
|
835
|
+
lines.push(`安全模式: 是 ⚠️`);
|
|
836
|
+
}
|
|
837
|
+
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
456
838
|
}
|
|
457
839
|
else {
|
|
458
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`,
|
|
840
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
459
841
|
}
|
|
460
842
|
if (health.safeMode) {
|
|
461
843
|
lines.push('');
|
|
@@ -482,12 +864,90 @@ export class CommandHandler {
|
|
|
482
864
|
}
|
|
483
865
|
}
|
|
484
866
|
const projectPath = session?.projectPath || this.config.projects?.defaultPath || process.cwd();
|
|
485
|
-
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName);
|
|
867
|
+
const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.defaultAgentId);
|
|
868
|
+
this.eventBus.publish({
|
|
869
|
+
type: 'session:created',
|
|
870
|
+
sessionId: newSession.id,
|
|
871
|
+
channel,
|
|
872
|
+
channelId,
|
|
873
|
+
projectPath,
|
|
874
|
+
name: sessionName,
|
|
875
|
+
timestamp: Date.now()
|
|
876
|
+
});
|
|
486
877
|
if (session) {
|
|
487
|
-
await
|
|
878
|
+
await agent.closeSession(session.id);
|
|
488
879
|
}
|
|
489
880
|
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /slist 查看`;
|
|
490
881
|
}
|
|
882
|
+
// /check 命令:检查渠道状态 / 手动重连指定渠道
|
|
883
|
+
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
884
|
+
if (!isAdmin)
|
|
885
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
886
|
+
const subCmd = normalizedContent.slice('/check'.length).trim();
|
|
887
|
+
// /check rty <channel> — 重连指定渠道
|
|
888
|
+
if (subCmd.startsWith('rty')) {
|
|
889
|
+
const target = subCmd.slice('rty'.length).trim();
|
|
890
|
+
if (!target) {
|
|
891
|
+
return '❌ 请指定渠道名称,例如:/check rty feishu';
|
|
892
|
+
}
|
|
893
|
+
const ch = this.channelObjects.get(target);
|
|
894
|
+
if (!ch) {
|
|
895
|
+
const available = [...this.channelObjects.keys()].join(', ') || '无';
|
|
896
|
+
return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
|
|
897
|
+
}
|
|
898
|
+
if (!ch.reconnect) {
|
|
899
|
+
return `❌ 渠道 "${target}" 不支持重连`;
|
|
900
|
+
}
|
|
901
|
+
const result = await ch.reconnect();
|
|
902
|
+
return `🔄 ${target} 重连: ${result}`;
|
|
903
|
+
}
|
|
904
|
+
// Default: show full system health check
|
|
905
|
+
const lines = ['📡 渠道状态:'];
|
|
906
|
+
for (const [name] of this.adapters) {
|
|
907
|
+
const ch = this.channelObjects.get(name);
|
|
908
|
+
if (ch?.getStatus) {
|
|
909
|
+
const s = ch.getStatus();
|
|
910
|
+
const status = s.connected ? '✓ 已连接' : s.reconnectAttempt > 0 ? `⏳ 重连中 (${s.reconnectAttempt}/${s.maxAttempts})` : '✗ 断开';
|
|
911
|
+
lines.push(` ${name}: ${status}`);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
lines.push(` ${name}: ✓ 已注册`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// 队列状态
|
|
918
|
+
lines.push('', '📬 队列状态:');
|
|
919
|
+
lines.push(` 待处理消息: ${this.messageQueue.getGlobalQueueLength()}`);
|
|
920
|
+
lines.push(` 处理中队列: ${this.messageQueue.getGlobalProcessingCount()}`);
|
|
921
|
+
// 运行概况
|
|
922
|
+
lines.push('', '🖥️ 运行概况:');
|
|
923
|
+
const uptimeMs = this.statsCollector
|
|
924
|
+
? this.statsCollector.getSnapshot().uptimeMs
|
|
925
|
+
: process.uptime() * 1000;
|
|
926
|
+
lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
|
|
927
|
+
lines.push(` 当前安全模式会话: ${this.sessionManager.getSafeModeSessionCount()}`);
|
|
928
|
+
// 近 1 小时统计
|
|
929
|
+
if (this.statsCollector) {
|
|
930
|
+
const snap = this.statsCollector.getSnapshot();
|
|
931
|
+
const h = snap.lastHour;
|
|
932
|
+
lines.push('', '📊 近 1 小时统计:');
|
|
933
|
+
lines.push(` 收到消息: ${h.received}`);
|
|
934
|
+
lines.push(` 完成处理: ${h.completed}`);
|
|
935
|
+
if (h.errors > 0) {
|
|
936
|
+
const breakdown = Object.entries(h.errorsByType).map(([t, c]) => `${t}: ${c}`).join(', ');
|
|
937
|
+
lines.push(` 处理出错: ${h.errors} (${breakdown})`);
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
lines.push(` 处理出错: 0`);
|
|
941
|
+
}
|
|
942
|
+
lines.push(` 被中断: ${h.interrupts}`);
|
|
943
|
+
lines.push(` 进入安全模式: ${h.safeModeEntries}`);
|
|
944
|
+
if (h.completed > 0) {
|
|
945
|
+
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
lines.push('', '💡 /check rty <channel> — 重连指定渠道');
|
|
949
|
+
return lines.join('\n');
|
|
950
|
+
}
|
|
491
951
|
// /restart 命令:重启服务
|
|
492
952
|
if (normalizedContent === '/restart') {
|
|
493
953
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
@@ -516,17 +976,17 @@ export class CommandHandler {
|
|
|
516
976
|
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
517
977
|
}
|
|
518
978
|
}
|
|
519
|
-
// 话题中 restart 时保存
|
|
520
|
-
let
|
|
979
|
+
// 话题中 restart 时保存 replyContext 用于重启后回复到话题
|
|
980
|
+
let replyContext;
|
|
521
981
|
if (threadId) {
|
|
522
982
|
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
523
|
-
|
|
983
|
+
replyContext = this.getReplyContext(threadSession);
|
|
524
984
|
}
|
|
525
985
|
const restartInfo = {
|
|
526
986
|
channel,
|
|
527
987
|
channelId,
|
|
528
988
|
timestamp: Date.now(),
|
|
529
|
-
...(
|
|
989
|
+
...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
|
|
530
990
|
};
|
|
531
991
|
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
532
992
|
const { spawn } = await import('child_process');
|
|
@@ -535,6 +995,7 @@ export class CommandHandler {
|
|
|
535
995
|
stdio: 'ignore',
|
|
536
996
|
env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
|
|
537
997
|
}).unref();
|
|
998
|
+
this.eventBus.publish({ type: 'system:restart', channel, channelId });
|
|
538
999
|
setTimeout(() => {
|
|
539
1000
|
logger.info('[System] Restarting by user command...');
|
|
540
1001
|
process.exit(0);
|
|
@@ -543,10 +1004,9 @@ export class CommandHandler {
|
|
|
543
1004
|
}
|
|
544
1005
|
// /pwd 命令:显示当前项目路径
|
|
545
1006
|
if (normalizedContent === '/pwd') {
|
|
1007
|
+
// session 现在总是存在(上面已自动创建)
|
|
546
1008
|
if (!session) {
|
|
547
|
-
return `❌
|
|
548
|
-
|
|
549
|
-
提示:发送任意消息或使用 /new 命令创建会话`;
|
|
1009
|
+
return `❌ 无法创建会话,请检查配置`;
|
|
550
1010
|
}
|
|
551
1011
|
const configName = this.getConfiguredProjectName(session.projectPath);
|
|
552
1012
|
if (configName) {
|
|
@@ -554,33 +1014,140 @@ export class CommandHandler {
|
|
|
554
1014
|
}
|
|
555
1015
|
return `当前项目: ${session.projectPath}`;
|
|
556
1016
|
}
|
|
1017
|
+
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path
|
|
1018
|
+
if (normalizedContent.startsWith('/send')) {
|
|
1019
|
+
// 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
|
|
1020
|
+
// 还原: 将 [text](url) 替换为 text
|
|
1021
|
+
const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
1022
|
+
if (!rawArg) {
|
|
1023
|
+
return '用法: /send <相对路径> 或 /send <渠道> <相对路径>\n示例: /send src/index.ts\n示例: /send feishu report.md';
|
|
1024
|
+
}
|
|
1025
|
+
// 解析目标通道:第一个 token 若匹配已注册通道名则为目标通道
|
|
1026
|
+
const tokens = rawArg.split(/\s+/);
|
|
1027
|
+
const knownChannels = [...this.adapters.keys()];
|
|
1028
|
+
let targetChannel = channel;
|
|
1029
|
+
let filePath = rawArg;
|
|
1030
|
+
if (tokens.length >= 2 && knownChannels.includes(tokens[0])) {
|
|
1031
|
+
targetChannel = tokens[0];
|
|
1032
|
+
filePath = tokens.slice(1).join(' ');
|
|
1033
|
+
}
|
|
1034
|
+
// 跨通道仅限 owner
|
|
1035
|
+
if (targetChannel !== channel && identity.role !== 'owner') {
|
|
1036
|
+
return '❌ 跨通道发送仅限管理员';
|
|
1037
|
+
}
|
|
1038
|
+
// 找目标 adapter
|
|
1039
|
+
const targetAdapter = this.adapters.get(targetChannel);
|
|
1040
|
+
if (!targetAdapter) {
|
|
1041
|
+
return `❌ 通道 ${targetChannel} 未启用或不存在`;
|
|
1042
|
+
}
|
|
1043
|
+
if (!targetAdapter.sendFile) {
|
|
1044
|
+
return `❌ 通道 ${targetChannel} 不支持文件发送`;
|
|
1045
|
+
}
|
|
1046
|
+
// 获取 session(需要 projectPath)
|
|
1047
|
+
const sendResult = await this.ensureSession(channel, channelId, threadId);
|
|
1048
|
+
if ('error' in sendResult)
|
|
1049
|
+
return sendResult.error;
|
|
1050
|
+
const sendSession = sendResult.session;
|
|
1051
|
+
// 路径安全校验
|
|
1052
|
+
if (path.isAbsolute(filePath)) {
|
|
1053
|
+
return '❌ 不支持绝对路径\n请使用项目内的相对路径';
|
|
1054
|
+
}
|
|
1055
|
+
if (filePath.split(path.sep).includes('..') || filePath.split('/').includes('..')) {
|
|
1056
|
+
return '❌ 不支持 .. 路径穿越';
|
|
1057
|
+
}
|
|
1058
|
+
const resolvedPath = path.resolve(sendSession.projectPath, filePath);
|
|
1059
|
+
// 存在性检查
|
|
1060
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1061
|
+
return `❌ 文件不存在: ${filePath}`;
|
|
1062
|
+
}
|
|
1063
|
+
// 符号链接安全:realpath 后验证仍在项目目录内
|
|
1064
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
1065
|
+
const realProjectPath = fs.realpathSync(sendSession.projectPath);
|
|
1066
|
+
if (!realPath.startsWith(realProjectPath + path.sep) && realPath !== realProjectPath) {
|
|
1067
|
+
return '❌ 路径不允许: 文件不在项目目录内';
|
|
1068
|
+
}
|
|
1069
|
+
const stat = fs.statSync(resolvedPath);
|
|
1070
|
+
if (stat.isDirectory()) {
|
|
1071
|
+
return '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持';
|
|
1072
|
+
}
|
|
1073
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
1074
|
+
if (stat.size > MAX_SIZE) {
|
|
1075
|
+
return `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)`;
|
|
1076
|
+
}
|
|
1077
|
+
// 找目标 channelId
|
|
1078
|
+
let targetChannelId = channelId;
|
|
1079
|
+
if (targetChannel !== channel) {
|
|
1080
|
+
const ownerPeerId = getOwner(this.config, targetChannel);
|
|
1081
|
+
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
|
|
1082
|
+
if (!targetChannelId) {
|
|
1083
|
+
return `❌ 未找到 ${targetChannel} 的私聊会话,请先在该通道发送一条消息`;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// 发送文件
|
|
1087
|
+
try {
|
|
1088
|
+
const replyCtx = targetChannel === channel ? this.getReplyContext(sendSession) : undefined;
|
|
1089
|
+
await targetAdapter.sendFile(targetChannelId, realPath, replyCtx);
|
|
1090
|
+
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
1091
|
+
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
1092
|
+
: `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
|
|
1093
|
+
return targetChannel !== channel
|
|
1094
|
+
? `📎 文件已通过 ${targetChannel} 发送: ${filePath} (${sizeStr})`
|
|
1095
|
+
: `✅ 已发送: ${filePath} (${sizeStr})`;
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
logger.error('[CommandHandler] /send failed:', error);
|
|
1099
|
+
return `❌ 文件发送失败: ${error.message || error}`;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
557
1102
|
// /plist 命令:列出所有项目
|
|
558
1103
|
if (normalizedContent === '/plist') {
|
|
559
|
-
|
|
560
|
-
if (isGroup) {
|
|
1104
|
+
if (!policy.canListProjects(session?.chatType || 'private', identity.role)) {
|
|
561
1105
|
if (!session) {
|
|
562
1106
|
return `❌ 当前群聊未绑定项目
|
|
563
1107
|
|
|
564
1108
|
请使用 /bind <项目路径> 绑定项目`;
|
|
565
1109
|
}
|
|
566
1110
|
const projectName = this.getProjectName(session.projectPath);
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
const status = queueLength > 0 ? '[处理中]' : '[空闲]';
|
|
1111
|
+
const isProcessing = !!session.processingState;
|
|
1112
|
+
const status = isProcessing ? '[处理中]' : '[空闲]';
|
|
570
1113
|
return `当前群聊绑定的项目:
|
|
571
1114
|
${projectName} (${session.projectPath}) - ${status}
|
|
572
1115
|
|
|
573
1116
|
提示:群聊不支持切换项目`;
|
|
574
1117
|
}
|
|
575
1118
|
const lines = ['可用项目:'];
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
const normalizePath = (p) => p.replace(/[/\\]+$/, '');
|
|
1119
|
+
// 收集项目信息并按最近活跃排序
|
|
1120
|
+
const entries = [];
|
|
1121
|
+
const configuredPaths = new Set();
|
|
580
1122
|
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
1123
|
+
configuredPaths.add(projectPath);
|
|
581
1124
|
const isCurrent = session?.projectPath === projectPath;
|
|
582
|
-
const prefix = isCurrent ? ' ✓' : ' ';
|
|
583
1125
|
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
1126
|
+
entries.push({
|
|
1127
|
+
name, projectPath, projectSession, isCurrent,
|
|
1128
|
+
updatedAt: projectSession?.updatedAt ?? 0,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
// Include bound projects not in config (created via /bind)
|
|
1132
|
+
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1133
|
+
for (const s of allSessions) {
|
|
1134
|
+
if (!configuredPaths.has(s.projectPath)) {
|
|
1135
|
+
configuredPaths.add(s.projectPath);
|
|
1136
|
+
const isCurrent = session?.projectPath === s.projectPath;
|
|
1137
|
+
entries.push({
|
|
1138
|
+
name: path.basename(s.projectPath), projectPath: s.projectPath, projectSession: s, isCurrent,
|
|
1139
|
+
updatedAt: s.updatedAt ?? 0,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
// 当前活跃项目置顶,其余按 updatedAt 降序
|
|
1144
|
+
entries.sort((a, b) => {
|
|
1145
|
+
if (a.isCurrent !== b.isCurrent)
|
|
1146
|
+
return a.isCurrent ? -1 : 1;
|
|
1147
|
+
return b.updatedAt - a.updatedAt;
|
|
1148
|
+
});
|
|
1149
|
+
for (const { name, projectPath, projectSession, isCurrent } of entries) {
|
|
1150
|
+
const prefix = isCurrent ? ' ✓' : ' ';
|
|
584
1151
|
if (!projectSession) {
|
|
585
1152
|
lines.push(`${prefix} ${name} (${projectPath}) - 无会话`);
|
|
586
1153
|
continue;
|
|
@@ -593,9 +1160,12 @@ export class CommandHandler {
|
|
|
593
1160
|
const idleMs = Date.now() - projectSession.updatedAt;
|
|
594
1161
|
statusParts.push(formatIdleTime(idleMs));
|
|
595
1162
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1163
|
+
// 用 DB processingState 判断处理状态
|
|
1164
|
+
const isProcessing = !!projectSession.processingState;
|
|
1165
|
+
if (isProcessing) {
|
|
1166
|
+
const queueLength = this.messageQueue.getQueueLength(projectSession.id);
|
|
1167
|
+
if (queueLength > 0) {
|
|
1168
|
+
statusParts.push(`[处理中,队列${queueLength}条]`);
|
|
599
1169
|
}
|
|
600
1170
|
else {
|
|
601
1171
|
statusParts.push('[处理中]');
|
|
@@ -605,7 +1175,7 @@ export class CommandHandler {
|
|
|
605
1175
|
if (unreadCount > 0) {
|
|
606
1176
|
statusParts.push(`[${unreadCount}条新消息]`);
|
|
607
1177
|
}
|
|
608
|
-
else if (!
|
|
1178
|
+
else if (!isProcessing && !isCurrent) {
|
|
609
1179
|
statusParts.push('[空闲]');
|
|
610
1180
|
}
|
|
611
1181
|
lines.push(`${prefix} ${name} (${projectPath}) - ${statusParts.join(' ')}`);
|
|
@@ -614,15 +1184,19 @@ export class CommandHandler {
|
|
|
614
1184
|
}
|
|
615
1185
|
// /project 命令:切换项目(支持名称或路径)
|
|
616
1186
|
if (normalizedContent.startsWith('/project ')) {
|
|
617
|
-
|
|
618
|
-
if (isGroup) {
|
|
1187
|
+
if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
|
|
619
1188
|
return `❌ 群聊不支持切换项目
|
|
620
1189
|
|
|
621
1190
|
群聊只能绑定一个项目。如需更换项目,请联系管理员重新配置。`;
|
|
622
1191
|
}
|
|
623
|
-
|
|
1192
|
+
let arg = normalizedContent.slice(9).trim();
|
|
624
1193
|
if (!arg)
|
|
625
1194
|
return '用法: /p <name|path> 或 /project <name|path>';
|
|
1195
|
+
// 检查确认标志
|
|
1196
|
+
const hasConfirm = arg.endsWith(' --confirm');
|
|
1197
|
+
if (hasConfirm) {
|
|
1198
|
+
arg = arg.slice(0, -10).trim();
|
|
1199
|
+
}
|
|
626
1200
|
let projectPath;
|
|
627
1201
|
let projectName;
|
|
628
1202
|
if (arg.includes('/')) {
|
|
@@ -649,10 +1223,33 @@ export class CommandHandler {
|
|
|
649
1223
|
return `当前已在项目: ${projectName}\n 路径: ${projectPath}`;
|
|
650
1224
|
}
|
|
651
1225
|
}
|
|
652
|
-
|
|
1226
|
+
// 群聊切换项目需要确认
|
|
1227
|
+
const isGroupChat = session?.chatType === 'group';
|
|
1228
|
+
if (isGroupChat && !hasConfirm) {
|
|
1229
|
+
return `⚠️ 群聊切换项目风险提示:
|
|
1230
|
+
|
|
1231
|
+
切换项目将影响所有群成员的对话上下文,可能导致:
|
|
1232
|
+
• 当前项目的会话历史被切换
|
|
1233
|
+
• 正在处理的任务被中断
|
|
1234
|
+
• 其他成员的工作受到影响
|
|
1235
|
+
|
|
1236
|
+
确认切换请执行:
|
|
1237
|
+
/p ${projectName} --confirm`;
|
|
1238
|
+
}
|
|
1239
|
+
const currentAgentId = activeSession?.agentId || this.defaultAgentId;
|
|
1240
|
+
const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath, currentAgentId);
|
|
1241
|
+
this.eventBus.publish({
|
|
1242
|
+
type: 'project:switched',
|
|
1243
|
+
sessionId: newSession.id,
|
|
1244
|
+
channel,
|
|
1245
|
+
channelId,
|
|
1246
|
+
projectPath,
|
|
1247
|
+
timestamp: Date.now()
|
|
1248
|
+
});
|
|
653
1249
|
const cachedEvents = this.messageCache.getEvents(newSession.id);
|
|
654
1250
|
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
655
|
-
|
|
1251
|
+
const currentAgent = newSession.agentId || this.defaultAgentId;
|
|
1252
|
+
let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n Agent: ${currentAgent}\n ${hasExistingSession}`;
|
|
656
1253
|
if (cachedEvents.length > 0 && sendMessage) {
|
|
657
1254
|
for (const event of cachedEvents) {
|
|
658
1255
|
if (event.type === 'completed') {
|
|
@@ -674,40 +1271,41 @@ export class CommandHandler {
|
|
|
674
1271
|
}
|
|
675
1272
|
return response;
|
|
676
1273
|
}
|
|
677
|
-
// /bind
|
|
1274
|
+
// /bind 命令:持久化项目到配置(不切换)
|
|
678
1275
|
if (normalizedContent.startsWith('/bind ')) {
|
|
679
1276
|
const projectPath = normalizedContent.slice(6).trim();
|
|
680
1277
|
if (!projectPath)
|
|
681
|
-
return '用法: /bind
|
|
1278
|
+
return '用法: /bind <路径>';
|
|
682
1279
|
if (!path.isAbsolute(projectPath)) {
|
|
683
1280
|
return '❌ 项目路径必须是绝对路径';
|
|
684
1281
|
}
|
|
685
1282
|
if (!fs.existsSync(projectPath)) {
|
|
686
1283
|
return `❌ 路径不存在: ${projectPath}`;
|
|
687
1284
|
}
|
|
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
|
-
}
|
|
1285
|
+
// 生成项目名称(使用目录名)
|
|
1286
|
+
const projectName = path.basename(projectPath);
|
|
1287
|
+
// 检查是否已存在
|
|
1288
|
+
if (this.projects[projectName]) {
|
|
1289
|
+
const existingPath = this.projects[projectName];
|
|
1290
|
+
if (existingPath === projectPath) {
|
|
1291
|
+
return `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
|
|
707
1292
|
}
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
1293
|
+
return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existingPath}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
|
|
1294
|
+
}
|
|
1295
|
+
// 添加到配置
|
|
1296
|
+
if (!this.config.projects) {
|
|
1297
|
+
this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
|
|
1298
|
+
}
|
|
1299
|
+
if (!this.config.projects.list) {
|
|
1300
|
+
this.config.projects.list = {};
|
|
1301
|
+
}
|
|
1302
|
+
this.config.projects.list[projectName] = projectPath;
|
|
1303
|
+
// 保存配置
|
|
1304
|
+
const { saveConfig } = await import('../config.js');
|
|
1305
|
+
saveConfig(this.config);
|
|
1306
|
+
// 更新内存中的项目列表
|
|
1307
|
+
this.projects[projectName] = projectPath;
|
|
1308
|
+
return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
|
|
711
1309
|
}
|
|
712
1310
|
// /slist 命令:列出当前项目的所有会话
|
|
713
1311
|
if (normalizedContent === '/slist') {
|
|
@@ -720,56 +1318,74 @@ export class CommandHandler {
|
|
|
720
1318
|
3. /project <项目> - 切换到指定项目`;
|
|
721
1319
|
}
|
|
722
1320
|
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
723
|
-
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath);
|
|
1321
|
+
const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
724
1322
|
// 从 SDK 同步会话名称(发现 CLI 改名)
|
|
725
1323
|
try {
|
|
726
|
-
const sdkSessions = await
|
|
1324
|
+
const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
|
|
727
1325
|
for (const sdkSession of sdkSessions) {
|
|
728
|
-
|
|
729
|
-
if (!sdkName)
|
|
1326
|
+
if (!sdkSession.title)
|
|
730
1327
|
continue;
|
|
731
1328
|
const dbSession = currentProjectSessions.find(s => s.agentSessionId === sdkSession.sessionId);
|
|
732
|
-
if (dbSession &&
|
|
733
|
-
await this.sessionManager.renameSession(dbSession.id,
|
|
734
|
-
dbSession.name =
|
|
1329
|
+
if (dbSession && sdkSession.title !== dbSession.name) {
|
|
1330
|
+
await this.sessionManager.renameSession(dbSession.id, sdkSession.title);
|
|
1331
|
+
dbSession.name = sdkSession.title;
|
|
735
1332
|
}
|
|
736
1333
|
}
|
|
737
1334
|
}
|
|
738
1335
|
catch (error) {
|
|
739
1336
|
logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
|
|
740
1337
|
}
|
|
741
|
-
const
|
|
742
|
-
const cliSessions =
|
|
743
|
-
?
|
|
744
|
-
:
|
|
1338
|
+
const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
|
|
1339
|
+
const cliSessions = canImportCli
|
|
1340
|
+
? await this.sessionManager.scanCliSessions(session.projectPath, session.agentId)
|
|
1341
|
+
: [];
|
|
745
1342
|
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
746
|
-
const lines = [`当前项目 ${path.basename(session.projectPath)}
|
|
747
|
-
const sessionKey = `${channel}-${channelId}`;
|
|
748
|
-
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
1343
|
+
const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
|
|
749
1344
|
if (currentProjectSessions.length > 0) {
|
|
1345
|
+
// 超过10个会话时隐藏话题会话(/slist 只能在主会话调用,话题内已禁用)
|
|
1346
|
+
const hideTopics = currentProjectSessions.length > 10;
|
|
1347
|
+
const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
|
|
1348
|
+
const maxDisplay = 10;
|
|
750
1349
|
lines.push('【EvolClaw 会话】');
|
|
1350
|
+
let displayIndex = 0;
|
|
751
1351
|
for (let i = 0; i < currentProjectSessions.length; i++) {
|
|
752
1352
|
const s = currentProjectSessions[i];
|
|
753
|
-
|
|
754
|
-
|
|
1353
|
+
if (hideTopics && s.threadId)
|
|
1354
|
+
continue;
|
|
1355
|
+
if (displayIndex >= maxDisplay)
|
|
1356
|
+
break;
|
|
1357
|
+
const isActive = s.metadata?.isActive === true;
|
|
1358
|
+
displayIndex++;
|
|
1359
|
+
const prefix = isActive ? ' ✓' : ' ';
|
|
1360
|
+
const num = `${displayIndex}.`;
|
|
755
1361
|
const threadTag = s.threadId ? '[话题] ' : '';
|
|
756
1362
|
const name = s.name || '(未命名)';
|
|
757
1363
|
const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
|
|
758
1364
|
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
759
|
-
if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId)) {
|
|
1365
|
+
if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId)) {
|
|
760
1366
|
lines.push(`${prefix} ${num} ${threadTag}❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
|
|
761
1367
|
}
|
|
762
1368
|
else {
|
|
1369
|
+
const sIsProcessing = !!s.processingState;
|
|
763
1370
|
let status = '[空闲]';
|
|
764
|
-
if (
|
|
1371
|
+
if (sIsProcessing) {
|
|
765
1372
|
status = '[处理中]';
|
|
766
1373
|
}
|
|
767
|
-
else if (
|
|
1374
|
+
else if (isActive) {
|
|
768
1375
|
status = '[活跃]';
|
|
769
1376
|
}
|
|
770
1377
|
lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
|
|
771
1378
|
}
|
|
772
1379
|
}
|
|
1380
|
+
const hiddenCount = currentProjectSessions.length - displayIndex - topicCount;
|
|
1381
|
+
if (topicCount > 0 || hiddenCount > 0) {
|
|
1382
|
+
const parts = [];
|
|
1383
|
+
if (hiddenCount > 0)
|
|
1384
|
+
parts.push(`${hiddenCount} 个更早的会话`);
|
|
1385
|
+
if (topicCount > 0)
|
|
1386
|
+
parts.push(`${topicCount} 个话题会话`);
|
|
1387
|
+
lines.push(`\n (已隐藏 ${parts.join('、')})`);
|
|
1388
|
+
}
|
|
773
1389
|
lines.push('');
|
|
774
1390
|
}
|
|
775
1391
|
const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid)).slice(0, 5);
|
|
@@ -777,7 +1393,7 @@ export class CommandHandler {
|
|
|
777
1393
|
lines.push('【CLI 会话】(最新5个)');
|
|
778
1394
|
for (const c of orphanCliSessions) {
|
|
779
1395
|
const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
780
|
-
const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid) || '(无消息)';
|
|
1396
|
+
const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
|
|
781
1397
|
const uuid = c.uuid.substring(0, 8);
|
|
782
1398
|
lines.push(` ${time} (${uuid}) "${message}"`);
|
|
783
1399
|
}
|
|
@@ -791,38 +1407,44 @@ export class CommandHandler {
|
|
|
791
1407
|
const sessionName = normalizedContent.slice(9).trim();
|
|
792
1408
|
if (!sessionName)
|
|
793
1409
|
return '用法: /s <序号、会话名称或前8位UUID>';
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
if (queueLength > 0) {
|
|
1410
|
+
const isProcessing = !!session?.processingState;
|
|
1411
|
+
if (isProcessing) {
|
|
797
1412
|
return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
|
|
798
1413
|
}
|
|
799
1414
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
800
|
-
//
|
|
1415
|
+
// 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
|
|
801
1416
|
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
802
1417
|
const idx = parseInt(sessionName, 10);
|
|
803
1418
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
804
|
-
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
|
|
805
|
-
|
|
806
|
-
|
|
1419
|
+
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1420
|
+
// 与 /slist 显示逻辑一致:超过10个时隐藏非活跃话题会话
|
|
1421
|
+
const hideTopics = projectSessions.length > 10;
|
|
1422
|
+
const visibleSessions = hideTopics
|
|
1423
|
+
? projectSessions.filter(s => !s.threadId)
|
|
1424
|
+
: projectSessions;
|
|
1425
|
+
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
1426
|
+
targetSession = visibleSessions[idx - 1];
|
|
807
1427
|
}
|
|
808
1428
|
else {
|
|
809
|
-
return `❌ 序号超出范围 (1-${
|
|
1429
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /slist 查看可用会话`;
|
|
810
1430
|
}
|
|
811
1431
|
}
|
|
812
1432
|
if (!targetSession && sessionName.length === 8) {
|
|
813
1433
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
814
1434
|
}
|
|
815
|
-
const
|
|
816
|
-
if (!targetSession && sessionName.length === 8 &&
|
|
1435
|
+
const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
|
|
1436
|
+
if (!targetSession && sessionName.length === 8 && canImport) {
|
|
817
1437
|
const projectPaths = Object.values(this.projects);
|
|
818
1438
|
if (session) {
|
|
819
1439
|
projectPaths.unshift(session.projectPath);
|
|
820
1440
|
}
|
|
821
1441
|
for (const projectPath of projectPaths) {
|
|
822
|
-
const
|
|
1442
|
+
const currentAgentId = session?.agentId || this.defaultAgentId;
|
|
1443
|
+
const cliSessions = await this.sessionManager.scanCliSessions(projectPath, currentAgentId);
|
|
823
1444
|
const cliSession = cliSessions.find(c => c.uuid.startsWith(sessionName));
|
|
824
1445
|
if (cliSession) {
|
|
825
|
-
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid);
|
|
1446
|
+
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
|
|
1447
|
+
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
826
1448
|
const projectName = this.getProjectName(projectPath);
|
|
827
1449
|
return `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史`;
|
|
828
1450
|
}
|
|
@@ -832,7 +1454,7 @@ export class CommandHandler {
|
|
|
832
1454
|
return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
|
|
833
1455
|
}
|
|
834
1456
|
const lastInput = targetSession.agentSessionId
|
|
835
|
-
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId)
|
|
1457
|
+
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
|
|
836
1458
|
: null;
|
|
837
1459
|
const lastInputLine = lastInput ? `\n 最后输入: "${lastInput}"` : '';
|
|
838
1460
|
if (!session) {
|
|
@@ -853,6 +1475,7 @@ export class CommandHandler {
|
|
|
853
1475
|
if (!switched) {
|
|
854
1476
|
return `❌ 切换会话失败`;
|
|
855
1477
|
}
|
|
1478
|
+
this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
|
|
856
1479
|
const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
|
|
857
1480
|
return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
|
|
858
1481
|
}
|
|
@@ -873,19 +1496,12 @@ export class CommandHandler {
|
|
|
873
1496
|
if (existing && existing.id !== session.id) {
|
|
874
1497
|
return `❌ 会话名称 "${newName}" 已存在,请使用其他名称`;
|
|
875
1498
|
}
|
|
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
|
-
}
|
|
1499
|
+
const oldName = session.name || '(未命名)';
|
|
885
1500
|
const success = await this.sessionManager.renameSession(session.id, newName);
|
|
886
1501
|
if (!success) {
|
|
887
1502
|
return `❌ 重命名失败`;
|
|
888
1503
|
}
|
|
1504
|
+
this.eventBus.publish({ type: 'session:renamed', sessionId: session.id, oldName, newName });
|
|
889
1505
|
return `✓ 已将当前会话重命名为: ${newName}`;
|
|
890
1506
|
}
|
|
891
1507
|
// /del 命令:删除指定会话(仅解绑,不删除文件)
|
|
@@ -896,22 +1512,25 @@ export class CommandHandler {
|
|
|
896
1512
|
if (!session) {
|
|
897
1513
|
return `❌ 当前没有活跃会话`;
|
|
898
1514
|
}
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
if (isGroup && !isAdmin) {
|
|
1515
|
+
// 权限检查:policy 控制谁可以删除会话
|
|
1516
|
+
if (!policy.canDeleteSession(session.chatType || 'private', identity.role)) {
|
|
902
1517
|
return `❌ 无权限:群聊中仅管理员可删除会话`;
|
|
903
1518
|
}
|
|
904
1519
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
905
|
-
//
|
|
1520
|
+
// 序号删除(与 /slist 显示序号一致)
|
|
906
1521
|
if (!targetSession && /^\d+$/.test(sessionName)) {
|
|
907
1522
|
const idx = parseInt(sessionName, 10);
|
|
908
1523
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
909
|
-
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
|
|
910
|
-
|
|
911
|
-
|
|
1524
|
+
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
|
|
1525
|
+
const hideTopics = projectSessions.length > 10;
|
|
1526
|
+
const visibleSessions = hideTopics
|
|
1527
|
+
? projectSessions.filter(s => !s.threadId)
|
|
1528
|
+
: projectSessions;
|
|
1529
|
+
if (idx >= 1 && idx <= visibleSessions.length) {
|
|
1530
|
+
targetSession = visibleSessions[idx - 1];
|
|
912
1531
|
}
|
|
913
1532
|
else {
|
|
914
|
-
return `❌ 序号超出范围 (1-${
|
|
1533
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /slist 查看可用会话`;
|
|
915
1534
|
}
|
|
916
1535
|
}
|
|
917
1536
|
if (!targetSession && sessionName.length === 8) {
|
|
@@ -927,7 +1546,9 @@ export class CommandHandler {
|
|
|
927
1546
|
if (!success) {
|
|
928
1547
|
return `❌ 删除失败`;
|
|
929
1548
|
}
|
|
930
|
-
|
|
1549
|
+
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
1550
|
+
const targetAgent = this.getAgent(targetSession.agentId);
|
|
1551
|
+
await targetAgent.closeSession(targetSession.id);
|
|
931
1552
|
return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
|
|
932
1553
|
}
|
|
933
1554
|
// /fork 命令:分支当前会话
|
|
@@ -937,11 +1558,16 @@ export class CommandHandler {
|
|
|
937
1558
|
return `❌ 当前没有活跃会话,无法分支`;
|
|
938
1559
|
}
|
|
939
1560
|
if (!session.agentSessionId) {
|
|
940
|
-
return `❌
|
|
1561
|
+
return `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
|
|
1562
|
+
}
|
|
1563
|
+
const forkAgent = this.getAgent(session.agentId);
|
|
1564
|
+
if (!forkAgent.capabilities?.fork) {
|
|
1565
|
+
return `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代`;
|
|
941
1566
|
}
|
|
942
1567
|
try {
|
|
943
|
-
const
|
|
944
|
-
const newSession = await this.sessionManager.createForkedSession(session,
|
|
1568
|
+
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
1569
|
+
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
1570
|
+
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
945
1571
|
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /slist 查看所有会话,/s <名称> 切换回原会话`;
|
|
946
1572
|
}
|
|
947
1573
|
catch (error) {
|
|
@@ -951,66 +1577,49 @@ export class CommandHandler {
|
|
|
951
1577
|
}
|
|
952
1578
|
// /repair 命令:检查并修复会话
|
|
953
1579
|
if (normalizedContent === '/repair') {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const
|
|
1580
|
+
const repairResult = await this.ensureSession(channel, channelId, threadId);
|
|
1581
|
+
if ('error' in repairResult)
|
|
1582
|
+
return repairResult.error;
|
|
1583
|
+
const { session: repairSession } = repairResult;
|
|
1584
|
+
const health = await this.sessionManager.getHealthStatus(repairSession.id);
|
|
958
1585
|
if (!health.safeMode) {
|
|
959
1586
|
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
960
1587
|
}
|
|
961
|
-
const
|
|
962
|
-
const
|
|
1588
|
+
const repairAgent = this.getAgent(repairSession.agentId);
|
|
1589
|
+
const { checkSessionFile, backupSessionFile } = await import('../utils/session-file-health.js');
|
|
963
1590
|
try {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
return `✓
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1591
|
+
if (!repairSession.agentSessionId) {
|
|
1592
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
1593
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
1594
|
+
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
1595
|
+
}
|
|
1596
|
+
// 通过 agent 定位 session 文件
|
|
1597
|
+
const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
|
|
1598
|
+
if (!sessionFile) {
|
|
1599
|
+
// 文件不存在(已被删除或从未创建),直接重置
|
|
1600
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
1601
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
1602
|
+
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
975
1603
|
}
|
|
976
|
-
const healthCheck = await
|
|
1604
|
+
const healthCheck = await checkSessionFile(sessionFile);
|
|
977
1605
|
if (healthCheck.corrupt) {
|
|
978
|
-
const
|
|
1606
|
+
const backupPath = await backupSessionFile(sessionFile);
|
|
1607
|
+
const fsPromises = await import('fs/promises');
|
|
979
1608
|
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}`;
|
|
1609
|
+
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
1610
|
+
repairAgent.updateSessionId(repairSession.id, '');
|
|
1611
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
1612
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
1613
|
+
return `✓ 修复完成,已退出安全模式\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
|
|
993
1614
|
}
|
|
994
1615
|
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
|
-
已重置异常计数器,可继续使用当前会话。`;
|
|
1616
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
1617
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
1618
|
+
return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
|
|
1004
1619
|
}
|
|
1005
|
-
await this.sessionManager.resetHealthStatus(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
修复内容:
|
|
1009
|
-
- 未发现问题
|
|
1010
|
-
- 已重置异常计数器
|
|
1011
|
-
- 已恢复正常会话模式
|
|
1012
|
-
|
|
1013
|
-
备份位置:${backupDir}`;
|
|
1620
|
+
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
1621
|
+
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
1622
|
+
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
1014
1623
|
}
|
|
1015
1624
|
catch (error) {
|
|
1016
1625
|
logger.error('[Repair] Failed:', error);
|
|
@@ -1019,10 +1628,12 @@ ${healthCheck.issues.map((i) => `- ${i}`).join('\n')}
|
|
|
1019
1628
|
}
|
|
1020
1629
|
// /safe 命令:手动进入安全模式
|
|
1021
1630
|
if (normalizedContent === '/safe') {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1631
|
+
const safeResult = await this.ensureSession(channel, channelId, threadId);
|
|
1632
|
+
if ('error' in safeResult)
|
|
1633
|
+
return safeResult.error;
|
|
1634
|
+
const { session: safeSession } = safeResult;
|
|
1635
|
+
await this.sessionManager.setSafeMode(safeSession.id, true);
|
|
1636
|
+
this.eventBus.publish({ type: 'session:safe-mode-entered', sessionId: safeSession.id, reason: 'manual' });
|
|
1026
1637
|
return `✓ 已进入安全模式
|
|
1027
1638
|
|
|
1028
1639
|
当前行为:
|
|
@@ -1036,11 +1647,4 @@ ${healthCheck.issues.map((i) => `- ${i}`).join('\n')}
|
|
|
1036
1647
|
}
|
|
1037
1648
|
return null;
|
|
1038
1649
|
}
|
|
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
1650
|
}
|