evolclaw 2.1.0 → 2.1.1
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 +9 -4
- package/dist/channels/feishu.js +7 -1
- package/dist/cli.js +13 -7
- package/dist/config.js +4 -1
- package/dist/core/agent-runner.js +32 -0
- package/dist/core/command-handler.js +196 -33
- package/dist/core/message-processor.js +39 -15
- package/dist/core/session-manager.js +24 -46
- package/dist/index.js +5 -2
- package/dist/index.js.bak +340 -0
- package/dist/utils/stream-flusher.js +42 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,10 +128,10 @@ evolclaw init wechat
|
|
|
128
128
|
},
|
|
129
129
|
"idleMonitor": {
|
|
130
130
|
"enabled": true, // 任务超时监控开关
|
|
131
|
-
"timeout":
|
|
131
|
+
"timeout": 120, // 超时阈值(秒),默认 120 秒
|
|
132
132
|
"safeModeThreshold": 3 // 连续超时 N 次后进入安全模式(设为 0 禁用安全模式)
|
|
133
133
|
},
|
|
134
|
-
"flushDelay":
|
|
134
|
+
"flushDelay": 4 // 工具活动消息聚合发送间隔(秒),默认 4 秒
|
|
135
135
|
}
|
|
136
136
|
```
|
|
137
137
|
|
|
@@ -139,6 +139,7 @@ evolclaw init wechat
|
|
|
139
139
|
- `apiKey`:配置文件 → `ANTHROPIC_AUTH_TOKEN` 环境变量 → `~/.claude/settings.json`
|
|
140
140
|
- `baseUrl`:配置文件 → `ANTHROPIC_BASE_URL` 环境变量 → `~/.claude/settings.json`
|
|
141
141
|
- `model`:配置文件 → `~/.claude/settings.json` → 默认 `sonnet`
|
|
142
|
+
- `effort`:配置文件 → `~/.claude/settings.json` → SDK 默认值(`auto`)
|
|
142
143
|
|
|
143
144
|
### 3. 运行
|
|
144
145
|
|
|
@@ -192,6 +193,7 @@ evolclaw/
|
|
|
192
193
|
- `/slist` - 列出当前项目的所有会话
|
|
193
194
|
- `/s <名称>` - 切换到指定会话
|
|
194
195
|
- `/name <新名称>` - 重命名当前会话
|
|
196
|
+
- `/del <名称>` - 删除指定会话(仅解绑,不删除文件)
|
|
195
197
|
- `/status` - 显示会话状态
|
|
196
198
|
- `/help` - 显示所有命令
|
|
197
199
|
|
|
@@ -212,8 +214,11 @@ evolclaw/
|
|
|
212
214
|
- `/safe` - 进入安全模式
|
|
213
215
|
|
|
214
216
|
**模型管理**:
|
|
215
|
-
- `/model` -
|
|
216
|
-
- `/model <model
|
|
217
|
+
- `/model` - 显示当前模型和推理强度
|
|
218
|
+
- `/model <model>` - 切换模型
|
|
219
|
+
- `/model <effort>` - 切换推理强度(low / medium / high / max)
|
|
220
|
+
- `/model <model> <effort>` - 同时切换模型和推理强度
|
|
221
|
+
- `/model auto` - 恢复 SDK 默认推理强度
|
|
217
222
|
|
|
218
223
|
## 技术栈
|
|
219
224
|
|
package/dist/channels/feishu.js
CHANGED
|
@@ -228,7 +228,8 @@ export class FeishuChannel {
|
|
|
228
228
|
logger.error('[Feishu] Failed to process message:', error);
|
|
229
229
|
}
|
|
230
230
|
},
|
|
231
|
-
'im.message.message_read_v1': async () => { }
|
|
231
|
+
'im.message.message_read_v1': async () => { },
|
|
232
|
+
'im.message.reaction.created_v1': async () => { }
|
|
232
233
|
});
|
|
233
234
|
this.wsClient = new lark.WSClient({
|
|
234
235
|
appId: this.config.appId,
|
|
@@ -336,6 +337,11 @@ export class FeishuChannel {
|
|
|
336
337
|
logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
|
|
337
338
|
}
|
|
338
339
|
catch (error) {
|
|
340
|
+
// 230011: 消息已被撤回,降级为普通消息重试
|
|
341
|
+
if (error.response?.data?.code === 230011 && options?.replyToMessageId) {
|
|
342
|
+
logger.warn('[Feishu] Message withdrawn (230011), retrying without reply');
|
|
343
|
+
return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
|
|
344
|
+
}
|
|
339
345
|
logger.error('[Feishu] Failed to send message:', error);
|
|
340
346
|
throw error;
|
|
341
347
|
}
|
package/dist/cli.js
CHANGED
|
@@ -537,8 +537,9 @@ async function cmdRestartMonitor() {
|
|
|
537
537
|
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
538
538
|
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
539
539
|
await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
540
|
-
// 调用 claude CLI
|
|
541
|
-
const
|
|
540
|
+
// 调用 claude CLI 修复(递增超时:3/4/5 分钟)
|
|
541
|
+
const timeout = (2 + attempt) * 60 * 1000;
|
|
542
|
+
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, timeout, log);
|
|
542
543
|
if (!healed) {
|
|
543
544
|
log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
|
|
544
545
|
continue;
|
|
@@ -633,7 +634,7 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
633
634
|
/**
|
|
634
635
|
* 调用 claude CLI 进行自动修复
|
|
635
636
|
*/
|
|
636
|
-
async function invokeClaude(p, attempt, maxAttempts, log) {
|
|
637
|
+
async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
637
638
|
const projectDir = getPackageRoot();
|
|
638
639
|
const selfHealLog = p.selfHealLog;
|
|
639
640
|
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
@@ -659,26 +660,31 @@ async function invokeClaude(p, attempt, maxAttempts, log) {
|
|
|
659
660
|
|
|
660
661
|
注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
|
|
661
662
|
try {
|
|
662
|
-
log(`Invoking claude CLI (attempt ${attempt})...`);
|
|
663
|
+
log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
|
|
663
664
|
const { stdout, stderr } = await execFileAsync('claude', [
|
|
664
665
|
'-p', prompt,
|
|
665
666
|
'--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
|
|
666
667
|
'--output-format', 'text',
|
|
667
668
|
], {
|
|
668
669
|
cwd: projectDir,
|
|
669
|
-
timeout
|
|
670
|
+
timeout,
|
|
670
671
|
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
|
|
671
672
|
maxBuffer: 10 * 1024 * 1024,
|
|
672
673
|
});
|
|
673
674
|
if (stdout)
|
|
674
675
|
log(`Claude output: ${stdout.slice(0, 500)}`);
|
|
675
676
|
if (stderr)
|
|
676
|
-
log(`Claude stderr: ${stderr.slice(0,
|
|
677
|
+
log(`Claude stderr: ${stderr.slice(0, 500)}`);
|
|
677
678
|
log(`Claude CLI completed (attempt ${attempt})`);
|
|
678
679
|
return true;
|
|
679
680
|
}
|
|
680
681
|
catch (error) {
|
|
681
|
-
|
|
682
|
+
const msg = error.message || String(error);
|
|
683
|
+
log(`Claude CLI error: ${msg.slice(0, 800)}`);
|
|
684
|
+
if (error.stdout)
|
|
685
|
+
log(`Stdout: ${String(error.stdout).slice(0, 500)}`);
|
|
686
|
+
if (error.stderr)
|
|
687
|
+
log(`Stderr: ${String(error.stderr).slice(0, 500)}`);
|
|
682
688
|
return false;
|
|
683
689
|
}
|
|
684
690
|
}
|
package/dist/config.js
CHANGED
|
@@ -37,7 +37,10 @@ export function resolveAnthropicConfig(config) {
|
|
|
37
37
|
const model = config.agents?.anthropic?.model
|
|
38
38
|
|| settings.model
|
|
39
39
|
|| 'sonnet';
|
|
40
|
-
|
|
40
|
+
const effort = config.agents?.anthropic?.effort
|
|
41
|
+
|| settings.effortLevel
|
|
42
|
+
|| undefined;
|
|
43
|
+
return { apiKey, baseUrl, model, effort };
|
|
41
44
|
}
|
|
42
45
|
export function loadConfig(configPath = resolvePaths().config) {
|
|
43
46
|
if (!fs.existsSync(configPath)) {
|
|
@@ -10,6 +10,7 @@ import { encodePath } from '../utils/platform.js';
|
|
|
10
10
|
export class AgentRunner {
|
|
11
11
|
apiKey;
|
|
12
12
|
model;
|
|
13
|
+
effort;
|
|
13
14
|
baseUrl;
|
|
14
15
|
config;
|
|
15
16
|
activeSessions = new Map();
|
|
@@ -19,6 +20,7 @@ export class AgentRunner {
|
|
|
19
20
|
constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
|
|
20
21
|
this.apiKey = apiKey;
|
|
21
22
|
this.model = model || 'sonnet';
|
|
23
|
+
this.effort = undefined;
|
|
22
24
|
this.baseUrl = baseUrl;
|
|
23
25
|
this.config = config;
|
|
24
26
|
this.onSessionIdUpdate = onSessionIdUpdate;
|
|
@@ -38,10 +40,38 @@ export class AgentRunner {
|
|
|
38
40
|
getModel() {
|
|
39
41
|
return this.model;
|
|
40
42
|
}
|
|
43
|
+
setEffort(effort) {
|
|
44
|
+
this.effort = effort;
|
|
45
|
+
}
|
|
46
|
+
getEffort() {
|
|
47
|
+
return this.effort;
|
|
48
|
+
}
|
|
49
|
+
syncFromUserSettings() {
|
|
50
|
+
try {
|
|
51
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
52
|
+
if (!fs.existsSync(settingsPath))
|
|
53
|
+
return;
|
|
54
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
55
|
+
if (settings.model && settings.model !== this.model) {
|
|
56
|
+
logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
|
|
57
|
+
this.model = settings.model;
|
|
58
|
+
}
|
|
59
|
+
const newEffort = settings.effortLevel || undefined;
|
|
60
|
+
if (newEffort !== this.effort) {
|
|
61
|
+
logger.info(`[AgentRunner] Synced effort from ~/.claude/settings.json: ${newEffort ?? 'auto'}`);
|
|
62
|
+
this.effort = newEffort;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
logger.debug(`[AgentRunner] Failed to sync from ~/.claude/settings.json:`, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
41
69
|
setCompactStartCallback(callback) {
|
|
42
70
|
this.onCompactStart = callback;
|
|
43
71
|
}
|
|
44
72
|
async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
|
|
73
|
+
// 同步用户级配置到内存
|
|
74
|
+
this.syncFromUserSettings();
|
|
45
75
|
ensureDir(projectPath);
|
|
46
76
|
ensureDir(path.join(projectPath, '.claude'));
|
|
47
77
|
// 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
|
|
@@ -117,9 +147,11 @@ export class AgentRunner {
|
|
|
117
147
|
const useSettingSources = this.config?.agents?.anthropic?.useSettingSources !== false;
|
|
118
148
|
const enableSummaries = this.config?.agents?.anthropic?.agentProgressSummaries !== false;
|
|
119
149
|
// 公共 options(新旧模式共用)
|
|
150
|
+
logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'}`);
|
|
120
151
|
const commonOptions = {
|
|
121
152
|
cwd: projectPath,
|
|
122
153
|
model: this.model,
|
|
154
|
+
...(this.effort ? { effort: this.effort } : {}),
|
|
123
155
|
canUseTool,
|
|
124
156
|
permissionMode: 'default',
|
|
125
157
|
persistSession: true,
|
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import { renameSession as sdkRenameSession, forkSession as sdkForkSession, listSessions as sdkListSessions } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { resolvePaths, getPackageRoot } from '../config.js';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
const availableModels = ['opus', 'sonnet', 'haiku', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'];
|
|
8
|
+
const availableEfforts = ['low', 'medium', 'high', 'max'];
|
|
9
|
+
function effortBar(level) {
|
|
10
|
+
const levels = {
|
|
11
|
+
low: '◆◇◇◇', medium: '◆◆◇◇', high: '◆◆◆◇', max: '◆◆◆◆'
|
|
12
|
+
};
|
|
13
|
+
return levels[level] || '◆◆◇◇';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
|
|
17
|
+
*/
|
|
18
|
+
function writeUserSettings(updates) {
|
|
19
|
+
try {
|
|
20
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
21
|
+
let settings = {};
|
|
22
|
+
if (fs.existsSync(settingsPath)) {
|
|
23
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
if (updates.model !== undefined)
|
|
26
|
+
settings.model = updates.model;
|
|
27
|
+
if (updates.effortLevel !== undefined) {
|
|
28
|
+
if (updates.effortLevel === null) {
|
|
29
|
+
delete settings.effortLevel;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
settings.effortLevel = updates.effortLevel;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
36
|
+
if (!fs.existsSync(claudeDir)) {
|
|
37
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
40
|
+
return { success: true };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { success: false, error: error.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
7
46
|
/**
|
|
8
47
|
* 计算两个字符串的 Levenshtein 距离(编辑距离)
|
|
9
48
|
*/
|
|
@@ -46,7 +85,7 @@ function formatIdleTime(ms) {
|
|
|
46
85
|
return '刚刚';
|
|
47
86
|
}
|
|
48
87
|
// 支持的命令列表
|
|
49
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork'];
|
|
88
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del'];
|
|
50
89
|
// 命令别名映射
|
|
51
90
|
const aliases = {
|
|
52
91
|
'/p': '/project',
|
|
@@ -56,7 +95,7 @@ const aliases = {
|
|
|
56
95
|
// 命令快速路径前缀(不进入消息队列的命令)
|
|
57
96
|
// 注意:/clear, /compact, /safe 故意不在此列表中,它们需要进入队列触发中断机制
|
|
58
97
|
// /stop 是快速命令:直接调用 agentRunner.interrupt(),不走队列(否则队列自动中断后 /stop 检测不到活跃任务)
|
|
59
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/p ', '/s ', '/name '];
|
|
98
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/del', '/p ', '/s ', '/name '];
|
|
60
99
|
export class CommandHandler {
|
|
61
100
|
sessionManager;
|
|
62
101
|
agentRunner;
|
|
@@ -89,6 +128,11 @@ export class CommandHandler {
|
|
|
89
128
|
return session.id;
|
|
90
129
|
return `${channel}-${channelId}`;
|
|
91
130
|
}
|
|
131
|
+
/** 从 session 提取话题回复选项 */
|
|
132
|
+
getThreadSendOpts(session) {
|
|
133
|
+
const rootId = session.metadata?.feishu?.rootId;
|
|
134
|
+
return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
|
|
135
|
+
}
|
|
92
136
|
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
93
137
|
async ensureSession(channel, channelId, threadId) {
|
|
94
138
|
if (threadId) {
|
|
@@ -136,14 +180,14 @@ export class CommandHandler {
|
|
|
136
180
|
const { isOwner: checkOwner } = await import('../config.js');
|
|
137
181
|
// 话题内禁用部分命令
|
|
138
182
|
if (threadId) {
|
|
139
|
-
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork'];
|
|
183
|
+
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del'];
|
|
140
184
|
const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
|
|
141
185
|
if (isBlocked)
|
|
142
186
|
return '⚠️ 话题中不支持此命令';
|
|
143
187
|
}
|
|
144
188
|
const isAdmin = !userId || checkOwner(this.config, channel, userId);
|
|
145
189
|
if (normalizedContent.startsWith('/')) {
|
|
146
|
-
const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/s '];
|
|
190
|
+
const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
147
191
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
148
192
|
if (!isUserCommand && !isAdmin) {
|
|
149
193
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
@@ -178,6 +222,7 @@ export class CommandHandler {
|
|
|
178
222
|
/slist - 列出当前项目的所有会话
|
|
179
223
|
/s, /session <名称> - 切换到指定会话
|
|
180
224
|
/name, /rename <新名称> - 重命名当前会话
|
|
225
|
+
/del <名称> - 删除指定会话(仅解绑,不删除文件)
|
|
181
226
|
/status - 显示会话状态
|
|
182
227
|
|
|
183
228
|
❓ 帮助:
|
|
@@ -195,6 +240,7 @@ export class CommandHandler {
|
|
|
195
240
|
/slist - 列出当前项目的所有会话
|
|
196
241
|
/s, /session <名称> - 切换到指定会话
|
|
197
242
|
/name, /rename <新名称> - 重命名当前会话
|
|
243
|
+
/del <名称> - 删除指定会话(仅解绑,不删除文件)
|
|
198
244
|
/fork [名称] - 分支当前会话(从当前对话点创建分支)
|
|
199
245
|
/status - 显示会话状态
|
|
200
246
|
/clear - 清空当前会话的对话历史
|
|
@@ -207,31 +253,94 @@ export class CommandHandler {
|
|
|
207
253
|
/safe - 进入安全模式
|
|
208
254
|
|
|
209
255
|
🤖 模型管理:
|
|
210
|
-
/model [model
|
|
256
|
+
/model [model] [effort] - 查看或切换模型/推理强度
|
|
211
257
|
|
|
212
258
|
❓ 帮助:
|
|
213
259
|
/help - 显示此帮助信息`;
|
|
214
260
|
}
|
|
215
|
-
// /model
|
|
261
|
+
// /model 命令:查看或切换模型/推理强度
|
|
216
262
|
if (normalizedContent.startsWith('/model')) {
|
|
217
263
|
const args = normalizedContent.slice(6).trim();
|
|
218
264
|
if (!args) {
|
|
219
265
|
const currentModel = this.agentRunner.getModel();
|
|
266
|
+
const currentEffort = this.agentRunner.getEffort() || 'auto';
|
|
267
|
+
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : `${currentEffort} ${effortBar(currentEffort)}`;
|
|
220
268
|
const modelList = availableModels.map(m => `- ${m}`).join('\n');
|
|
221
|
-
return `当前模型: ${currentModel}\n\n可用模型:\n${modelList}\n\n
|
|
269
|
+
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
|
+
}
|
|
271
|
+
const parts = args.split(/\s+/);
|
|
272
|
+
let newModel;
|
|
273
|
+
let newEffort;
|
|
274
|
+
if (parts.length === 1) {
|
|
275
|
+
const arg = parts[0];
|
|
276
|
+
if (arg === 'auto') {
|
|
277
|
+
// 清除 effort,恢复 SDK 默认
|
|
278
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
279
|
+
if ('error' in result)
|
|
280
|
+
return result.error;
|
|
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默认)';
|
|
288
|
+
}
|
|
289
|
+
// 单参数:模型 或 effort
|
|
290
|
+
if (availableEfforts.includes(arg)) {
|
|
291
|
+
newEffort = arg;
|
|
292
|
+
}
|
|
293
|
+
else if (availableModels.includes(arg)) {
|
|
294
|
+
newModel = arg;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
const modelList = availableModels.map(m => `- ${m}`).join('\n');
|
|
298
|
+
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')}`;
|
|
299
|
+
}
|
|
222
300
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
301
|
+
else {
|
|
302
|
+
// 双参数:model effort
|
|
303
|
+
const [modelArg, effortArg] = parts;
|
|
304
|
+
if (!availableModels.includes(modelArg)) {
|
|
305
|
+
return `❌ 无效的模型ID: ${modelArg}`;
|
|
306
|
+
}
|
|
307
|
+
if (!availableEfforts.includes(effortArg)) {
|
|
308
|
+
return `❌ 无效的推理强度: ${effortArg}\n可选: ${availableEfforts.join(' / ')}`;
|
|
309
|
+
}
|
|
310
|
+
newModel = modelArg;
|
|
311
|
+
newEffort = effortArg;
|
|
226
312
|
}
|
|
227
313
|
if (!this.config.agents)
|
|
228
314
|
this.config.agents = {};
|
|
229
315
|
if (!this.config.agents.anthropic)
|
|
230
316
|
this.config.agents.anthropic = {};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
317
|
+
// 获取当前会话的项目路径
|
|
318
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
319
|
+
if ('error' in result)
|
|
320
|
+
return result.error;
|
|
321
|
+
const { session } = result;
|
|
322
|
+
const changes = [];
|
|
323
|
+
const updates = {};
|
|
324
|
+
if (newModel) {
|
|
325
|
+
updates.model = newModel;
|
|
326
|
+
this.agentRunner.setModel(newModel);
|
|
327
|
+
changes.push(`模型: ${newModel}`);
|
|
328
|
+
}
|
|
329
|
+
if (newEffort) {
|
|
330
|
+
const modelAfterSwitch = newModel ?? this.agentRunner.getModel();
|
|
331
|
+
if (newEffort === 'max' && !modelAfterSwitch.includes('opus')) {
|
|
332
|
+
return '⚠️ max 推理强度仅 Opus 模型支持(opus / claude-opus-4-6)';
|
|
333
|
+
}
|
|
334
|
+
updates.effortLevel = newEffort;
|
|
335
|
+
this.agentRunner.setEffort(newEffort);
|
|
336
|
+
changes.push(`推理强度: ${newEffort} ${effortBar(newEffort)}`);
|
|
337
|
+
}
|
|
338
|
+
// 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
|
|
339
|
+
const writeResult = writeUserSettings(updates);
|
|
340
|
+
if (!writeResult.success) {
|
|
341
|
+
return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
|
|
342
|
+
}
|
|
343
|
+
return `✓ 已切换\n ${changes.join('\n ')}`;
|
|
235
344
|
}
|
|
236
345
|
// /stop 命令:中断当前任务
|
|
237
346
|
if (normalizedContent === '/stop') {
|
|
@@ -287,7 +396,7 @@ export class CommandHandler {
|
|
|
287
396
|
? session.projectPath
|
|
288
397
|
: path.resolve(process.cwd(), session.projectPath);
|
|
289
398
|
if (sendMessage) {
|
|
290
|
-
await sendMessage(channelId, '⏳ 正在压缩会话上下文...');
|
|
399
|
+
await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getThreadSendOpts(session));
|
|
291
400
|
}
|
|
292
401
|
const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, projectPath);
|
|
293
402
|
if (compacted) {
|
|
@@ -324,15 +433,7 @@ export class CommandHandler {
|
|
|
324
433
|
const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
325
434
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
326
435
|
const isThread = !!session.threadId;
|
|
327
|
-
|
|
328
|
-
if ((isThread || session.isActive) && isCurrentlyProcessing) {
|
|
329
|
-
if (queueLength > 0) {
|
|
330
|
-
activeStatus += ` [处理中,队列${queueLength}条]`;
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
activeStatus += ' [处理中]';
|
|
334
|
-
}
|
|
335
|
-
}
|
|
436
|
+
const sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
|
|
336
437
|
const projectName = this.getProjectName(session.projectPath);
|
|
337
438
|
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
338
439
|
const timeSinceSuccess = Date.now() - health.lastSuccessTime;
|
|
@@ -351,10 +452,10 @@ export class CommandHandler {
|
|
|
351
452
|
}
|
|
352
453
|
const lines = [];
|
|
353
454
|
if (isAdmin) {
|
|
354
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`,
|
|
455
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `异常计数: ${health.consecutiveErrors}`, `安全模式: ${health.safeMode ? '是 ⚠️' : '否 ✓'}`, `最后成功: ${timeStr}`, `Claude会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
355
456
|
}
|
|
356
457
|
else {
|
|
357
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `会话: ${session.name || '(未命名)'}`, `状态: ${
|
|
458
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `会话: ${session.name || '(未命名)'}`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
358
459
|
}
|
|
359
460
|
if (health.safeMode) {
|
|
360
461
|
lines.push('');
|
|
@@ -647,13 +748,16 @@ export class CommandHandler {
|
|
|
647
748
|
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
648
749
|
if (currentProjectSessions.length > 0) {
|
|
649
750
|
lines.push('【EvolClaw 会话】');
|
|
650
|
-
for (
|
|
751
|
+
for (let i = 0; i < currentProjectSessions.length; i++) {
|
|
752
|
+
const s = currentProjectSessions[i];
|
|
651
753
|
const prefix = s.isActive ? ' ✓' : ' ';
|
|
754
|
+
const num = `${i + 1}.`;
|
|
755
|
+
const threadTag = s.threadId ? '[话题] ' : '';
|
|
652
756
|
const name = s.name || '(未命名)';
|
|
653
757
|
const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
|
|
654
758
|
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
655
759
|
if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId)) {
|
|
656
|
-
lines.push(`${prefix} ❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
|
|
760
|
+
lines.push(`${prefix} ${num} ${threadTag}❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
|
|
657
761
|
}
|
|
658
762
|
else {
|
|
659
763
|
let status = '[空闲]';
|
|
@@ -663,7 +767,7 @@ export class CommandHandler {
|
|
|
663
767
|
else if (s.isActive) {
|
|
664
768
|
status = '[活跃]';
|
|
665
769
|
}
|
|
666
|
-
lines.push(`${prefix} ${name} ${uuid} - ${idleTime} ${status}`);
|
|
770
|
+
lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
|
|
667
771
|
}
|
|
668
772
|
}
|
|
669
773
|
lines.push('');
|
|
@@ -679,20 +783,32 @@ export class CommandHandler {
|
|
|
679
783
|
}
|
|
680
784
|
lines.push('');
|
|
681
785
|
}
|
|
682
|
-
lines.push('使用 /s
|
|
786
|
+
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
683
787
|
return lines.join('\n');
|
|
684
788
|
}
|
|
685
789
|
// /session 或 /s 命令:切换会话
|
|
686
790
|
if (normalizedContent.startsWith('/session ')) {
|
|
687
791
|
const sessionName = normalizedContent.slice(9).trim();
|
|
688
792
|
if (!sessionName)
|
|
689
|
-
return '用法: /s
|
|
793
|
+
return '用法: /s <序号、会话名称或前8位UUID>';
|
|
690
794
|
const sessionKey = `${channel}-${channelId}`;
|
|
691
795
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
692
796
|
if (queueLength > 0) {
|
|
693
797
|
return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
|
|
694
798
|
}
|
|
695
799
|
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
800
|
+
// 序号切换:纯数字时按当前项目会话列表序号匹配
|
|
801
|
+
if (!targetSession && /^\d+$/.test(sessionName) && session) {
|
|
802
|
+
const idx = parseInt(sessionName, 10);
|
|
803
|
+
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
804
|
+
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
|
|
805
|
+
if (idx >= 1 && idx <= projectSessions.length) {
|
|
806
|
+
targetSession = projectSessions[idx - 1];
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
return `❌ 序号超出范围 (1-${projectSessions.length})\n使用 /slist 查看可用会话`;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
696
812
|
if (!targetSession && sessionName.length === 8) {
|
|
697
813
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
698
814
|
}
|
|
@@ -729,11 +845,16 @@ export class CommandHandler {
|
|
|
729
845
|
if (targetSession.id === session.id) {
|
|
730
846
|
return `当前已在会话: ${targetSession.name || sessionName}`;
|
|
731
847
|
}
|
|
848
|
+
// 阻止从主会话切换到话题会话
|
|
849
|
+
if (!session.threadId && targetSession.threadId) {
|
|
850
|
+
return `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用`;
|
|
851
|
+
}
|
|
732
852
|
const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
|
|
733
853
|
if (!switched) {
|
|
734
854
|
return `❌ 切换会话失败`;
|
|
735
855
|
}
|
|
736
|
-
|
|
856
|
+
const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
|
|
857
|
+
return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
|
|
737
858
|
}
|
|
738
859
|
// /rename 或 /name 命令:重命名当前会话
|
|
739
860
|
if (normalizedContent.startsWith('/rename ')) {
|
|
@@ -767,6 +888,48 @@ export class CommandHandler {
|
|
|
767
888
|
}
|
|
768
889
|
return `✓ 已将当前会话重命名为: ${newName}`;
|
|
769
890
|
}
|
|
891
|
+
// /del 命令:删除指定会话(仅解绑,不删除文件)
|
|
892
|
+
if (normalizedContent.startsWith('/del ')) {
|
|
893
|
+
const sessionName = normalizedContent.slice(5).trim();
|
|
894
|
+
if (!sessionName)
|
|
895
|
+
return '用法: /del <序号、会话名称或前8位UUID>';
|
|
896
|
+
if (!session) {
|
|
897
|
+
return `❌ 当前没有活跃会话`;
|
|
898
|
+
}
|
|
899
|
+
// 群聊权限检查:只有管理员可以删除
|
|
900
|
+
const isGroup = await this.isGroupChat(channel, channelId);
|
|
901
|
+
if (isGroup && !isAdmin) {
|
|
902
|
+
return `❌ 无权限:群聊中仅管理员可删除会话`;
|
|
903
|
+
}
|
|
904
|
+
let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
905
|
+
// 序号删除
|
|
906
|
+
if (!targetSession && /^\d+$/.test(sessionName)) {
|
|
907
|
+
const idx = parseInt(sessionName, 10);
|
|
908
|
+
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
909
|
+
const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
|
|
910
|
+
if (idx >= 1 && idx <= projectSessions.length) {
|
|
911
|
+
targetSession = projectSessions[idx - 1];
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
return `❌ 序号超出范围 (1-${projectSessions.length})\n使用 /slist 查看可用会话`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!targetSession && sessionName.length === 8) {
|
|
918
|
+
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
919
|
+
}
|
|
920
|
+
if (!targetSession) {
|
|
921
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
|
|
922
|
+
}
|
|
923
|
+
if (targetSession.id === session.id) {
|
|
924
|
+
return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
|
|
925
|
+
}
|
|
926
|
+
const success = await this.sessionManager.unbindSession(targetSession.id);
|
|
927
|
+
if (!success) {
|
|
928
|
+
return `❌ 删除失败`;
|
|
929
|
+
}
|
|
930
|
+
await this.agentRunner.closeSession(targetSession.id);
|
|
931
|
+
return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
|
|
932
|
+
}
|
|
770
933
|
// /fork 命令:分支当前会话
|
|
771
934
|
if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
|
|
772
935
|
const forkName = normalizedContent.slice(5).trim() || undefined;
|