evolclaw 2.7.0 → 2.7.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.
@@ -1,5 +1,6 @@
1
1
  import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { ensureDir, resolveAnthropicConfig } from '../config.js';
3
+ import { DEFAULT_PERMISSION_MODE } from '../types.js';
3
4
  import path from 'path';
4
5
  import fs from 'fs';
5
6
  import os from 'os';
@@ -72,7 +73,7 @@ export class AgentRunner {
72
73
  apiKey;
73
74
  model;
74
75
  effort;
75
- permissionMode = 'auto';
76
+ permissionMode = DEFAULT_PERMISSION_MODE;
76
77
  baseUrl;
77
78
  config;
78
79
  activeSessions = new Map();
@@ -206,8 +207,11 @@ export class AgentRunner {
206
207
  return this.handleAskUserQuestionFallback(input, questions);
207
208
  }
208
209
  const answers = {};
209
- // 闭包捕获当前 sendPromptFn,避免异步等待期间被其他会话覆盖
210
- const sendPrompt = this.sendPromptFn;
210
+ // permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
211
+ // 注意:sendPromptFn 是全局单例,多 channel 并发时会被覆盖,导致提示发到错误 channel
212
+ const sendPrompt = permCtx.adapter && permCtx.channelId
213
+ ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
214
+ : this.sendPromptFn;
211
215
  // 逐个 question 发送卡片并等待用户选择
212
216
  for (let i = 0; i < questions.length; i++) {
213
217
  const q = questions[i];
@@ -327,7 +331,10 @@ export class AgentRunner {
327
331
  */
328
332
  async handleExitPlanMode(sessionId, input, options) {
329
333
  const permCtx = this.permissionContexts.get(sessionId);
330
- const sendPrompt = this.sendPromptFn;
334
+ // permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
335
+ const sendPrompt = permCtx?.adapter && permCtx?.channelId
336
+ ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
337
+ : this.sendPromptFn;
331
338
  // 无交互上下文,直接 allow(防御性兜底)
332
339
  if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId || !sendPrompt) {
333
340
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_PERMISSION_MODE } from '../types.js';
1
2
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
3
  import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
3
4
  import { logger } from '../utils/logger.js';
@@ -555,7 +556,7 @@ export class CommandHandler {
555
556
  // guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
556
557
  const guestGroupCommands = [
557
558
  '/status', '/help', '/check', '/chatmode',
558
- '/model', '/effort', '/agent', '/perm', '/activity', '/safe',
559
+ '/model', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
559
560
  ];
560
561
  const userCommands = activeChatType === 'group' && !isAdmin
561
562
  ? guestGroupCommands
@@ -708,8 +709,7 @@ export class CommandHandler {
708
709
  if (!hasPermissionController(permAgent)) {
709
710
  return '❌ 权限控制不可用';
710
711
  }
711
- const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'auto';
712
- const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
712
+ const currentMode = permSession.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
713
713
  const modes = permAgent.listModes();
714
714
  // 尝试发送交互卡片
715
715
  if (this.interactionRouter) {
@@ -8,6 +8,7 @@ import { StreamIdleMonitor } from './stream-idle-monitor.js';
8
8
  import { logger } from '../../utils/logger.js';
9
9
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
10
10
  import { summarizeToolInput } from '../permission.js';
11
+ import { DEFAULT_PERMISSION_MODE } from '../../types.js';
11
12
  import { getOwner } from '../../config.js';
12
13
  import { getPackageRoot, resolveRoot } from '../../paths.js';
13
14
  import { renderPromptSection } from '../../prompts/templates.js';
@@ -385,10 +386,8 @@ export class MessageProcessor {
385
386
  ? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
386
387
  : undefined,
387
388
  });
388
- // 设置 per-session 权限模式(动态默认值:owner bypass,admin → auto,guest → auto)
389
- const role = session.identity?.role;
390
- const defaultPermMode = role === 'owner' ? 'bypass' : 'auto';
391
- agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
389
+ // 设置 per-session 权限模式(默认 bypass,所有角色统一)
390
+ agent.setMode(session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE);
392
391
  // 标记会话为处理中(实时持久化,重启后可恢复)
393
392
  this.sessionManager.markProcessing(session.id);
394
393
  logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
@@ -1,4 +1,5 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
+ import { DEFAULT_PERMISSION_MODE } from '../../types.js';
2
3
  import { ensureDir } from '../../config.js';
3
4
  import { resolvePaths } from '../../paths.js';
4
5
  import { logger } from '../../utils/logger.js';
@@ -471,7 +472,7 @@ export class SessionManager {
471
472
  session.identity = this.resolveIdentity(channel, userId);
472
473
  // 新话题会话补写默认权限模式
473
474
  if (session.metadata && !session.metadata.permissionMode) {
474
- session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'auto';
475
+ session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
475
476
  this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
476
477
  .run(JSON.stringify(session.metadata), Date.now(), session.id);
477
478
  }
@@ -580,9 +581,9 @@ export class SessionManager {
580
581
  updatedAt: Date.now()
581
582
  };
582
583
  session.identity = this.resolveIdentity(channel, userId);
583
- // 写入默认权限模式(基于角色,只在首次创建时设置)
584
+ // 写入默认权限模式(统一 bypass,只在首次创建时设置)
584
585
  if (!sessionMetadata.permissionMode) {
585
- sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'auto';
586
+ sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
586
587
  }
587
588
  this.insertSession(session);
588
589
  this.eventBus.publish({
package/dist/index.js CHANGED
@@ -239,7 +239,7 @@ async function main() {
239
239
  peerId: peerId || '', peerName, messageId, mentions, threadId,
240
240
  // 只在话题场景(threadId 有值)才设置 replyContext;
241
241
  // 纯引用回复(rootId 有值但无 threadId)不设置,避免所有回复都带引用头
242
- replyContext: threadId ? { replyToMessageId: rootId, replyInThread: true } : undefined,
242
+ replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
243
243
  });
244
244
  }), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
245
245
  replyToMessageId: replyContext?.replyToMessageId,
@@ -18,10 +18,10 @@
18
18
 
19
19
  ## proactive
20
20
 
21
- [Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:
22
- - 发送文本:evolclaw ctl send "<消息内容>"
23
- - 发送文件:evolclaw ctl file <路径>
24
- 可多次调用。如不调用,用户将看不到任何回复。
21
+ [Proactive 模式] 你的所有文本输出都会被静默丢弃,用户永远看不到。唯一能让用户收到消息的方式:
22
+ 调用 Bash 工具执行命令 :evolclaw ctl send "<消息内容>"
23
+ 发送文件: evolclaw ctl file <路径>
24
+ 可多次调用发送多条消息 ,如果不想回复停止调用即可。
25
25
 
26
26
 
27
27
  ---
package/dist/types.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // ── Channel config types ──
2
2
  // Single-object form: `name` is optional (defaults to channel type name).
3
3
  // Array form: `name` is required to distinguish instances.
4
- export {};
4
+ /** Default permission mode applied to new sessions. Change here to affect all roles. */
5
+ export const DEFAULT_PERMISSION_MODE = 'bypass';
@@ -152,25 +152,36 @@ export function tailFile(filePath) {
152
152
  child.on('exit', (code) => process.exit(code || 0));
153
153
  return { abort: () => child.kill() };
154
154
  }
155
- // Windows: Node.js-based implementation
155
+ // Windows: Node.js-based implementation using stat polling
156
+ // (fs.watch / ReadDirectoryChangesW is unreliable for cross-process appends)
156
157
  // Output last 20 lines of existing content
157
158
  const content = fs.readFileSync(filePath, 'utf-8');
158
159
  const lines = content.split('\n');
159
160
  const lastLines = lines.slice(-20);
160
161
  process.stdout.write(lastLines.join('\n'));
161
162
  let position = fs.statSync(filePath).size;
162
- const watcher = fs.watch(filePath, () => {
163
- const stat = fs.statSync(filePath);
164
- if (stat.size > position) {
165
- const fd = fs.openSync(filePath, 'r');
166
- const buffer = Buffer.alloc(stat.size - position);
167
- fs.readSync(fd, buffer, 0, buffer.length, position);
168
- fs.closeSync(fd);
169
- process.stdout.write(buffer.toString('utf-8'));
170
- position = stat.size;
163
+ const listener = () => {
164
+ try {
165
+ const stat = fs.statSync(filePath);
166
+ if (stat.size < position) {
167
+ // File was truncated (log rotation) — reset and re-read from start
168
+ position = 0;
169
+ }
170
+ if (stat.size > position) {
171
+ const fd = fs.openSync(filePath, 'r');
172
+ const buffer = Buffer.alloc(stat.size - position);
173
+ fs.readSync(fd, buffer, 0, buffer.length, position);
174
+ fs.closeSync(fd);
175
+ process.stdout.write(buffer.toString('utf-8'));
176
+ position = stat.size;
177
+ }
178
+ }
179
+ catch {
180
+ // File may be briefly unavailable during rotation — ignore and retry next tick
171
181
  }
172
- });
173
- return { abort: () => watcher.close() };
182
+ };
183
+ fs.watchFile(filePath, { interval: 500, persistent: true }, listener);
184
+ return { abort: () => fs.unwatchFile(filePath, listener) };
174
185
  }
175
186
  /**
176
187
  * Resolve file path from import.meta.url (cross-platform safe).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",