evolclaw 2.0.1 → 2.0.2
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/data/evolclaw.sample.json +31 -26
- package/dist/channels/wechat.js +451 -0
- package/dist/cli.js +196 -146
- package/dist/config.js +32 -17
- package/dist/core/agent-runner.js +27 -41
- package/dist/core/command-handler.js +72 -54
- package/dist/core/message-processor.js +36 -10
- package/dist/core/message-queue.js +9 -3
- package/dist/core/session-manager.js +81 -238
- package/dist/index.js +189 -115
- package/dist/utils/init-feishu.js +261 -0
- package/dist/utils/init-wechat.js +170 -0
- package/dist/utils/init.js +98 -69
- package/dist/utils/stream-flusher.js +3 -2
- package/package.json +9 -7
|
@@ -70,6 +70,26 @@ export class CommandHandler {
|
|
|
70
70
|
this.config = config;
|
|
71
71
|
this.messageCache = messageCache;
|
|
72
72
|
}
|
|
73
|
+
/** 项目列表快捷访问 */
|
|
74
|
+
get projects() {
|
|
75
|
+
return this.config.projects?.list || {};
|
|
76
|
+
}
|
|
77
|
+
/** 根据项目路径查找配置中的项目名称 */
|
|
78
|
+
getConfiguredProjectName(projectPath) {
|
|
79
|
+
return Object.entries(this.projects).find(([_, p]) => p === projectPath)?.[0];
|
|
80
|
+
}
|
|
81
|
+
/** 根据项目路径查找项目名称(未配置时回退到目录名) */
|
|
82
|
+
getProjectName(projectPath) {
|
|
83
|
+
return this.getConfiguredProjectName(projectPath) || path.basename(projectPath);
|
|
84
|
+
}
|
|
85
|
+
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
86
|
+
async ensureSession(channel, channelId) {
|
|
87
|
+
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
88
|
+
if (!session) {
|
|
89
|
+
return { error: '❌ 当前没有活跃会话\n使用 /new 创建新会话' };
|
|
90
|
+
}
|
|
91
|
+
return { session };
|
|
92
|
+
}
|
|
73
93
|
setProcessor(processor) {
|
|
74
94
|
this.processor = processor;
|
|
75
95
|
}
|
|
@@ -79,6 +99,9 @@ export class CommandHandler {
|
|
|
79
99
|
registerAdapter(adapter) {
|
|
80
100
|
this.adapters.set(adapter.name, adapter);
|
|
81
101
|
}
|
|
102
|
+
getAdapter(channelName) {
|
|
103
|
+
return this.adapters.get(channelName);
|
|
104
|
+
}
|
|
82
105
|
/**
|
|
83
106
|
* 快速判断是否为命令(不进队列的命令)
|
|
84
107
|
*/
|
|
@@ -97,11 +120,14 @@ export class CommandHandler {
|
|
|
97
120
|
break;
|
|
98
121
|
}
|
|
99
122
|
}
|
|
100
|
-
//
|
|
123
|
+
// 权限检查:区分用户级命令和管理级命令
|
|
124
|
+
const { isOwner: checkOwner } = await import('../config.js');
|
|
125
|
+
const isAdmin = !userId || checkOwner(this.config, channel, userId);
|
|
101
126
|
if (normalizedContent.startsWith('/')) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/s '];
|
|
128
|
+
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
129
|
+
if (!isUserCommand && !isAdmin) {
|
|
130
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
105
131
|
}
|
|
106
132
|
}
|
|
107
133
|
// 检查是否以 / 开头(可能是命令)
|
|
@@ -126,6 +152,18 @@ export class CommandHandler {
|
|
|
126
152
|
return null;
|
|
127
153
|
// /help 命令不需要会话
|
|
128
154
|
if (normalizedContent === '/help') {
|
|
155
|
+
if (!isAdmin) {
|
|
156
|
+
return `可用命令:
|
|
157
|
+
🔄 会话管理:
|
|
158
|
+
/new [名称] - 创建新会话(可选命名)
|
|
159
|
+
/slist - 列出当前项目的所有会话
|
|
160
|
+
/s, /session <名称> - 切换到指定会话
|
|
161
|
+
/name, /rename <新名称> - 重命名当前会话
|
|
162
|
+
/status - 显示会话状态
|
|
163
|
+
|
|
164
|
+
❓ 帮助:
|
|
165
|
+
/help - 显示此帮助信息`;
|
|
166
|
+
}
|
|
129
167
|
return `可用命令:
|
|
130
168
|
📁 项目管理:
|
|
131
169
|
/pwd - 显示当前项目路径
|
|
@@ -167,9 +205,11 @@ export class CommandHandler {
|
|
|
167
205
|
const modelList = availableModels.map(m => `- ${m}`).join('\n');
|
|
168
206
|
return `❌ 无效的模型ID: ${args}\n\n可用模型:\n${modelList}`;
|
|
169
207
|
}
|
|
170
|
-
if (!this.config.
|
|
171
|
-
this.config.
|
|
172
|
-
this.config.anthropic
|
|
208
|
+
if (!this.config.agents)
|
|
209
|
+
this.config.agents = {};
|
|
210
|
+
if (!this.config.agents.anthropic)
|
|
211
|
+
this.config.agents.anthropic = {};
|
|
212
|
+
this.config.agents.anthropic.model = args;
|
|
173
213
|
saveConfig(this.config);
|
|
174
214
|
this.agentRunner.setModel(args);
|
|
175
215
|
return `✓ 已切换到模型: ${args}`;
|
|
@@ -186,10 +226,10 @@ export class CommandHandler {
|
|
|
186
226
|
}
|
|
187
227
|
// /clear 命令:通过 SDK /clear 清空会话历史
|
|
188
228
|
if (normalizedContent === '/clear') {
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
return
|
|
192
|
-
}
|
|
229
|
+
const result = await this.ensureSession(channel, channelId);
|
|
230
|
+
if ('error' in result)
|
|
231
|
+
return result.error;
|
|
232
|
+
const { session } = result;
|
|
193
233
|
if (!session.claudeSessionId) {
|
|
194
234
|
return '❌ 当前会话没有历史记录,无需清空';
|
|
195
235
|
}
|
|
@@ -208,10 +248,10 @@ export class CommandHandler {
|
|
|
208
248
|
}
|
|
209
249
|
// /compact 命令:手动压缩会话上下文
|
|
210
250
|
if (normalizedContent === '/compact') {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
return
|
|
214
|
-
}
|
|
251
|
+
const result = await this.ensureSession(channel, channelId);
|
|
252
|
+
if ('error' in result)
|
|
253
|
+
return result.error;
|
|
254
|
+
const { session } = result;
|
|
215
255
|
if (!session.claudeSessionId) {
|
|
216
256
|
return '❌ 当前会话没有历史记录,无需压缩';
|
|
217
257
|
}
|
|
@@ -258,9 +298,7 @@ export class CommandHandler {
|
|
|
258
298
|
activeStatus += ' [处理中]';
|
|
259
299
|
}
|
|
260
300
|
}
|
|
261
|
-
const
|
|
262
|
-
const projectName = Object.entries(projects)
|
|
263
|
-
.find(([_, p]) => p === session.projectPath)?.[0] || path.basename(session.projectPath);
|
|
301
|
+
const projectName = this.getProjectName(session.projectPath);
|
|
264
302
|
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
265
303
|
const timeSinceSuccess = Date.now() - health.lastSuccessTime;
|
|
266
304
|
const timeStr = timeSinceSuccess < 60000 ? '刚刚' :
|
|
@@ -276,20 +314,13 @@ export class CommandHandler {
|
|
|
276
314
|
session.name = fileInfo.title;
|
|
277
315
|
}
|
|
278
316
|
}
|
|
279
|
-
const lines = [
|
|
280
|
-
|
|
281
|
-
`渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
`异常计数: ${health.consecutiveErrors}`,
|
|
287
|
-
`安全模式: ${health.safeMode ? '是 ⚠️' : '否 ✓'}`,
|
|
288
|
-
`最后成功: ${timeStr}`,
|
|
289
|
-
`Claude会话: ${session.claudeSessionId || '(未初始化)'}`,
|
|
290
|
-
`创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`,
|
|
291
|
-
`更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`
|
|
292
|
-
];
|
|
317
|
+
const lines = [];
|
|
318
|
+
if (isAdmin) {
|
|
319
|
+
lines.push('📊 会话状态:', `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `活跃状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `异常计数: ${health.consecutiveErrors}`, `安全模式: ${health.safeMode ? '是 ⚠️' : '否 ✓'}`, `最后成功: ${timeStr}`, `Claude会话: ${session.claudeSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
lines.push('📊 会话状态:', `会话: ${session.name || '(未命名)'}`, `状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
323
|
+
}
|
|
293
324
|
if (health.safeMode) {
|
|
294
325
|
lines.push('');
|
|
295
326
|
lines.push('⚠️ 当前处于安全模式(历史上下文已禁用)');
|
|
@@ -374,24 +405,14 @@ export class CommandHandler {
|
|
|
374
405
|
|
|
375
406
|
提示:发送任意消息或使用 /new 命令创建会话`;
|
|
376
407
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (projectPath === session.projectPath) {
|
|
381
|
-
projectName = name;
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (projectName) {
|
|
386
|
-
return `当前项目: ${projectName}\n路径: ${session.projectPath}`;
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
return `当前项目: ${session.projectPath}`;
|
|
408
|
+
const configName = this.getConfiguredProjectName(session.projectPath);
|
|
409
|
+
if (configName) {
|
|
410
|
+
return `当前项目: ${configName}\n路径: ${session.projectPath}`;
|
|
390
411
|
}
|
|
412
|
+
return `当前项目: ${session.projectPath}`;
|
|
391
413
|
}
|
|
392
414
|
// /plist 命令:列出所有项目
|
|
393
415
|
if (normalizedContent === '/plist') {
|
|
394
|
-
const projects = this.config.projects?.list || {};
|
|
395
416
|
const isGroup = await this.isGroupChat(channel, channelId);
|
|
396
417
|
if (isGroup) {
|
|
397
418
|
if (!session) {
|
|
@@ -399,8 +420,7 @@ export class CommandHandler {
|
|
|
399
420
|
|
|
400
421
|
请使用 /bind <项目路径> 绑定项目`;
|
|
401
422
|
}
|
|
402
|
-
const projectName =
|
|
403
|
-
.find(([_, p]) => p === session.projectPath)?.[0] || path.basename(session.projectPath);
|
|
423
|
+
const projectName = this.getProjectName(session.projectPath);
|
|
404
424
|
const sessionKey = `${channel}-${channelId}`;
|
|
405
425
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
406
426
|
const status = queueLength > 0 ? '[处理中]' : '[空闲]';
|
|
@@ -414,7 +434,7 @@ export class CommandHandler {
|
|
|
414
434
|
const processingProject = this.messageQueue.getProcessingProject(sessionKey);
|
|
415
435
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
416
436
|
const normalizePath = (p) => p.replace(/\/+$/, '');
|
|
417
|
-
for (const [name, projectPath] of Object.entries(projects)) {
|
|
437
|
+
for (const [name, projectPath] of Object.entries(this.projects)) {
|
|
418
438
|
const isCurrent = session?.projectPath === projectPath;
|
|
419
439
|
const prefix = isCurrent ? ' ✓' : ' ';
|
|
420
440
|
const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
|
|
@@ -473,8 +493,7 @@ export class CommandHandler {
|
|
|
473
493
|
projectName = path.basename(arg);
|
|
474
494
|
}
|
|
475
495
|
else {
|
|
476
|
-
|
|
477
|
-
projectPath = projects[arg];
|
|
496
|
+
projectPath = this.projects[arg];
|
|
478
497
|
if (!projectPath) {
|
|
479
498
|
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /plist 查看可用项目`;
|
|
480
499
|
}
|
|
@@ -637,8 +656,7 @@ export class CommandHandler {
|
|
|
637
656
|
}
|
|
638
657
|
const isGroup = await this.isGroupChat(channel, channelId);
|
|
639
658
|
if (!targetSession && sessionName.length === 8 && !isGroup) {
|
|
640
|
-
const
|
|
641
|
-
const projectPaths = Object.values(projects);
|
|
659
|
+
const projectPaths = Object.values(this.projects);
|
|
642
660
|
if (session) {
|
|
643
661
|
projectPaths.unshift(session.projectPath);
|
|
644
662
|
}
|
|
@@ -647,7 +665,7 @@ export class CommandHandler {
|
|
|
647
665
|
const cliSession = cliSessions.find(c => c.uuid.startsWith(sessionName));
|
|
648
666
|
if (cliSession) {
|
|
649
667
|
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid);
|
|
650
|
-
const projectName =
|
|
668
|
+
const projectName = this.getProjectName(projectPath);
|
|
651
669
|
return `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史`;
|
|
652
670
|
}
|
|
653
671
|
}
|
|
@@ -45,7 +45,7 @@ export class MessageProcessor {
|
|
|
45
45
|
async processMessage(message) {
|
|
46
46
|
const isGroup = message.isGroup ?? false;
|
|
47
47
|
this.currentIsGroup = isGroup;
|
|
48
|
-
const idleMs = this.config.timeout
|
|
48
|
+
const idleMs = this.config.idleMonitor?.timeout ?? 120000;
|
|
49
49
|
const streamKey = `${message.channel}-${message.channelId}`;
|
|
50
50
|
const channelInfo = this.channels.get(message.channel);
|
|
51
51
|
const monitorEnabled = this.config.idleMonitor?.enabled !== false;
|
|
@@ -242,7 +242,7 @@ export class MessageProcessor {
|
|
|
242
242
|
await this.processEventStream(stream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer);
|
|
243
243
|
}
|
|
244
244
|
catch (error) {
|
|
245
|
-
if (
|
|
245
|
+
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.claudeSessionId) {
|
|
246
246
|
// 尝试 compact 压缩会话
|
|
247
247
|
flusher.addActivity('⚠️ 上下文过长,正在压缩会话...');
|
|
248
248
|
await flusher.flush();
|
|
@@ -268,7 +268,18 @@ export class MessageProcessor {
|
|
|
268
268
|
const fileMatches = [...fullText.matchAll(options.fileMarkerPattern)];
|
|
269
269
|
for (const match of fileMatches) {
|
|
270
270
|
const filePath = match[1].trim();
|
|
271
|
+
// 占位符/示例检测:静默跳过,不打扰用户
|
|
272
|
+
if (this.isPlaceholderPath(filePath)) {
|
|
273
|
+
logger.info(`[${adapter.name}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
271
276
|
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
277
|
+
// 文件存在性检查:真实路径但文件不存在,告知用户
|
|
278
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
279
|
+
logger.warn(`[${adapter.name}] File not found: ${resolvedPath}`);
|
|
280
|
+
await adapter.sendText(message.channelId, `⚠️ 文件未找到: ${filePath}`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
272
283
|
logger.info(`[${adapter.name}] Sending file: ${resolvedPath}`);
|
|
273
284
|
try {
|
|
274
285
|
await adapter.sendFile(message.channelId, resolvedPath);
|
|
@@ -471,14 +482,6 @@ export class MessageProcessor {
|
|
|
471
482
|
throw error; // 重新抛出,让外层处理
|
|
472
483
|
}
|
|
473
484
|
}
|
|
474
|
-
/**
|
|
475
|
-
* 判断是否为上下文过长错误
|
|
476
|
-
*/
|
|
477
|
-
isContextTooLongError(error) {
|
|
478
|
-
const msg = (error?.message || String(error)).toLowerCase();
|
|
479
|
-
return msg.includes('上下文过长') || msg.includes('context too long')
|
|
480
|
-
|| msg.includes('context_length_exceeded');
|
|
481
|
-
}
|
|
482
485
|
/**
|
|
483
486
|
* 格式化工具描述(通用)
|
|
484
487
|
*/
|
|
@@ -513,4 +516,27 @@ export class MessageProcessor {
|
|
|
513
516
|
// 都找不到,返回项目根目录路径
|
|
514
517
|
return rootPath;
|
|
515
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* 判断文件路径是否为占位符/示例文本
|
|
521
|
+
* 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
|
|
522
|
+
*/
|
|
523
|
+
isPlaceholderPath(filePath) {
|
|
524
|
+
if (!filePath)
|
|
525
|
+
return true;
|
|
526
|
+
// 精确占位符
|
|
527
|
+
const exactPlaceholders = ['...', '…', 'path', 'file', 'file_path', 'filepath',
|
|
528
|
+
'路径', '文件路径', '文件', 'filename', 'xxx'];
|
|
529
|
+
if (exactPlaceholders.includes(filePath.toLowerCase()))
|
|
530
|
+
return true;
|
|
531
|
+
// 示例路径前缀
|
|
532
|
+
if (/^(\/path\/to\/|\.\/path\/to\/|example\/|示例|\/example)/i.test(filePath))
|
|
533
|
+
return true;
|
|
534
|
+
// 含模板变量
|
|
535
|
+
if (/\$\{.+\}|\{\{.+\}\}|<.+>/.test(filePath))
|
|
536
|
+
return true;
|
|
537
|
+
// 纯标点/特殊字符(非路径字符)
|
|
538
|
+
if (/^[.\s…]+$/.test(filePath))
|
|
539
|
+
return true;
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
516
542
|
}
|
|
@@ -13,6 +13,12 @@ export class MessageQueue {
|
|
|
13
13
|
setInterruptCallback(callback) {
|
|
14
14
|
this.interruptCallback = callback;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* 检查队列 key 是否属于指定 sessionKey
|
|
18
|
+
*/
|
|
19
|
+
matchesSession(key, sessionKey) {
|
|
20
|
+
return key.startsWith(sessionKey + '-');
|
|
21
|
+
}
|
|
16
22
|
/**
|
|
17
23
|
* 生成项目级别的队列 key
|
|
18
24
|
*/
|
|
@@ -72,7 +78,7 @@ export class MessageQueue {
|
|
|
72
78
|
// 计算该 sessionKey 下所有项目队列的总长度
|
|
73
79
|
let total = 0;
|
|
74
80
|
for (const [key, queue] of this.queues.entries()) {
|
|
75
|
-
if (
|
|
81
|
+
if (this.matchesSession(key, sessionKey)) {
|
|
76
82
|
total += queue.length;
|
|
77
83
|
}
|
|
78
84
|
}
|
|
@@ -81,7 +87,7 @@ export class MessageQueue {
|
|
|
81
87
|
isProcessing(sessionKey) {
|
|
82
88
|
// 检查该 sessionKey 下是否有任何项目队列在处理
|
|
83
89
|
for (const key of this.processing.keys()) {
|
|
84
|
-
if (
|
|
90
|
+
if (this.matchesSession(key, sessionKey)) {
|
|
85
91
|
return true;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
@@ -93,7 +99,7 @@ export class MessageQueue {
|
|
|
93
99
|
getProcessingProject(sessionKey) {
|
|
94
100
|
// 查找该 sessionKey 下正在处理的项目
|
|
95
101
|
for (const key of this.processing.keys()) {
|
|
96
|
-
if (
|
|
102
|
+
if (this.matchesSession(key, sessionKey)) {
|
|
97
103
|
// 从 processing 中找到对应的队列,获取 projectPath
|
|
98
104
|
const queue = this.queues.get(key);
|
|
99
105
|
if (queue && queue.length > 0) {
|