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.
@@ -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 { isOwner } = await import('../config.js');
103
- if (userId && !isOwner(this.config, channel, userId)) {
104
- return '❌ 无权限:只有主人可以执行命令';
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.anthropic)
171
- this.config.anthropic = {};
172
- this.config.anthropic.model = args;
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 session = await this.sessionManager.getSession(channel, channelId);
190
- if (!session) {
191
- return '❌ 当前没有活跃会话\n使用 /new 创建新会话';
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 session = await this.sessionManager.getSession(channel, channelId);
212
- if (!session) {
213
- return '❌ 当前没有活跃会话\n使用 /new 创建新会话';
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 projects = this.config.projects?.list || {};
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
- `会话ID: ${session.id}`,
283
- `项目路径: ${session.projectPath}`,
284
- `活跃状态: ${activeStatus}`,
285
- `会话轮数: ${sessionTurns}`,
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 projects = this.config.projects?.list || {};
378
- let projectName = '';
379
- for (const [name, projectPath] of Object.entries(projects)) {
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 = Object.entries(projects)
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
- const projects = this.config.projects?.list || {};
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 projects = this.config.projects?.list || {};
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 = Object.entries(projects).find(([_, p]) => p === projectPath)?.[0] || path.basename(projectPath);
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?.idle ?? 120000;
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 (this.isContextTooLongError(error) && session.claudeSessionId) {
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 (key.startsWith(sessionKey + '-')) {
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 (key.startsWith(sessionKey + '-')) {
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 (key.startsWith(sessionKey + '-')) {
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) {