evolclaw 2.6.4 → 2.7.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
2
2
  import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
3
3
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
4
- import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getChannelSessionMode } from './config.js';
4
+ import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig, isOwner, isAdmin, validateConfigIntegrity, validateChannelInstanceNames, getOwner, getDefaultSessionMode } from './config.js';
5
5
  import { SessionManager } from './core/session/session-manager.js';
6
6
  import { ClaudeAgentPlugin } from './agents/claude-runner.js';
7
7
  import { CodexAgentPlugin } from './agents/codex-runner.js';
@@ -24,7 +24,8 @@ import { InteractionRouter } from './core/interaction-router.js';
24
24
  import { ChannelLoader } from './core/channel-loader.js';
25
25
  import { AgentLoader } from './core/agent-loader.js';
26
26
  import { IpcServer } from './ipc.js';
27
- import { logger } from './utils/logger.js';
27
+ import { logger, setLogLevel } from './utils/logger.js';
28
+ import { detectDuplicates } from './utils/channel-fingerprint.js';
28
29
  import { loadPromptTemplates } from './prompts/templates.js';
29
30
  import path from 'path';
30
31
  import fs from 'fs';
@@ -53,6 +54,10 @@ async function main() {
53
54
  loadPromptTemplates();
54
55
  // 加载配置
55
56
  const config = loadConfig();
57
+ // 应用配置中的日志级别(优先于环境变量)
58
+ if (config.debug?.logLevel) {
59
+ setLogLevel(config.debug.logLevel);
60
+ }
56
61
  const paths = resolvePaths();
57
62
  // 配置完整性校验
58
63
  const integrity = validateConfigIntegrity(config);
@@ -66,6 +71,14 @@ async function main() {
66
71
  logger.info('✓ Config loaded (API keys hidden)');
67
72
  // Channel instance name uniqueness check
68
73
  validateChannelInstanceNames(config);
74
+ // Detect duplicate channel credentials
75
+ const duplicates = detectDuplicates(config);
76
+ if (duplicates.length > 0) {
77
+ for (const d of duplicates) {
78
+ logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} is used by instances [${d.instances.join(', ')}]. ` +
79
+ `Only the first instance will be active.`);
80
+ }
81
+ }
69
82
  if (anthropic.baseUrl) {
70
83
  logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
71
84
  }
@@ -76,28 +89,9 @@ async function main() {
76
89
  const statsCollector = new StatsCollector(eventBus);
77
90
  // 初始化数据库(带 ownerResolver)
78
91
  const sessionManager = new SessionManager(undefined, eventBus, (channel, userId) => isOwner(config, channel, userId), (channel, userId) => isAdmin(config, channel, userId));
79
- // sessionMode 解析:通道配置锁定 > chatType 默认(AUN 群聊 → proactive,其余 → interactive
80
- sessionManager.setSessionModeResolver((channel, chatType) => {
81
- const locked = getChannelSessionMode(config, channel);
82
- if (locked)
83
- return locked;
84
- // chatType 默认值:仅 AUN 群聊默认为 proactive,其余通道默认 interactive
85
- // channel 在多实例时为 instanceName,需要识别 AUN 系
86
- // 简化:通过 ChannelOptions.channelType 在 MessageProcessor 注册时已知,但 SessionManager 不持有这个映射
87
- // 这里回退到按 instanceName 反查 config.channels.aun
88
- if (chatType === 'group') {
89
- const aun = config.channels?.aun;
90
- if (Array.isArray(aun)) {
91
- if (aun.some((i) => i.name === channel))
92
- return 'proactive';
93
- }
94
- else if (aun) {
95
- const effectiveName = aun.name ?? 'aun';
96
- if (effectiveName === channel)
97
- return 'proactive';
98
- }
99
- }
100
- return undefined;
92
+ // sessionMode 解析:全局 chatmode 配置 > 默认 'interactive'
93
+ sessionManager.setSessionModeResolver((_channel, chatType) => {
94
+ return getDefaultSessionMode(config, chatType);
101
95
  });
102
96
  logger.info('✓ Database initialized');
103
97
  // 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
@@ -243,7 +237,9 @@ async function main() {
243
237
  await handler({
244
238
  channel: channelType, channelId: chatId, content, images, chatType,
245
239
  peerId: peerId || '', peerName, messageId, mentions, threadId,
246
- replyContext: rootId ? { replyToMessageId: rootId, replyInThread: !!threadId } : undefined,
240
+ // 只在话题场景(threadId 有值)才设置 replyContext;
241
+ // 纯引用回复(rootId 有值但无 threadId)不设置,避免所有回复都带引用头
242
+ replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
247
243
  });
248
244
  }), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
249
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';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Channel Fingerprint
3
+ *
4
+ * 为每个 channel 实例提取一个全局唯一标识,用于冲突检测和路由索引。
5
+ * 格式:{type}:{primaryKey}
6
+ */
7
+ /** Channel 类型 → 主键字段映射 */
8
+ const PRIMARY_KEY_MAP = {
9
+ feishu: 'appId',
10
+ aun: 'aid',
11
+ wechat: 'token',
12
+ wecom: 'botId',
13
+ dingtalk: 'clientId',
14
+ qqbot: 'appId',
15
+ };
16
+ export function extractFingerprint(channelType, instance) {
17
+ const keyField = PRIMARY_KEY_MAP[channelType];
18
+ if (!keyField)
19
+ return null;
20
+ const value = instance[keyField];
21
+ if (!value || typeof value !== 'string')
22
+ return null;
23
+ return `${channelType}:${value}`;
24
+ }
25
+ export function detectDuplicates(config) {
26
+ const seen = new Map();
27
+ const channels = config.channels || {};
28
+ for (const [type, raw] of Object.entries(channels)) {
29
+ if (type === 'defaultChannel')
30
+ continue;
31
+ const instances = Array.isArray(raw) ? raw : [raw];
32
+ for (const inst of instances) {
33
+ if (!inst || typeof inst !== 'object')
34
+ continue;
35
+ const fingerprint = extractFingerprint(type, inst);
36
+ if (!fingerprint)
37
+ continue;
38
+ const instName = inst.name ?? type;
39
+ const entry = seen.get(fingerprint);
40
+ if (entry) {
41
+ entry.instances.push(instName);
42
+ }
43
+ else {
44
+ seen.set(fingerprint, { channelType: type, instances: [instName] });
45
+ }
46
+ }
47
+ }
48
+ const duplicates = [];
49
+ for (const [fingerprint, entry] of seen) {
50
+ if (entry.instances.length > 1) {
51
+ duplicates.push({
52
+ fingerprint,
53
+ channelType: entry.channelType,
54
+ instances: entry.instances,
55
+ });
56
+ }
57
+ }
58
+ return duplicates;
59
+ }
@@ -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).
@@ -503,7 +503,7 @@ export async function cmdInit(options) {
503
503
  const config = JSON.parse(fs.readFileSync(sampleSrc, 'utf-8'));
504
504
  config.projects.defaultPath = defaultPath;
505
505
  config.projects.list = { [path.basename(defaultPath)]: defaultPath };
506
- config.agents.anthropic.model = model;
506
+ config.agents.claude.model = model;
507
507
  let channelConfigured = false;
508
508
  while (!channelConfigured) {
509
509
  console.log('\n选择消息渠道:');
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolvePaths } from '../paths.js';
4
4
  const LOG_DIR = resolvePaths().logs;
5
- const LOG_LEVEL = process.env.LOG_LEVEL || 'INFO';
5
+ let currentLevel = process.env.LOG_LEVEL || 'INFO';
6
6
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
7
7
  const config = {
8
8
  messageLog: process.env.MESSAGE_LOG === 'true',
@@ -17,7 +17,7 @@ const streams = {
17
17
  event: config.eventLog ? fs.createWriteStream(path.join(LOG_DIR, 'events.log'), { flags: 'a' }) : null
18
18
  };
19
19
  function shouldLog(level) {
20
- return LEVELS[level] >= LEVELS[LOG_LEVEL];
20
+ return (LEVELS[level] ?? 1) >= (LEVELS[currentLevel] ?? 1);
21
21
  }
22
22
  function write(stream, data) {
23
23
  if (!stream)
@@ -35,9 +35,21 @@ function log(level, ...args) {
35
35
  return;
36
36
  const timestamp = localTimestamp();
37
37
  const msg = `[${timestamp}] [${level}] ${args.join(' ')}`;
38
- // 只写文件,不输出到 console(避免重定向时重复)
39
38
  write(streams.main, msg);
40
39
  }
40
+ /**
41
+ * 设置日志级别(config 加载后调用,覆盖环境变量默认值)
42
+ * 优先级:config.debug.logLevel → LOG_LEVEL 环境变量 → 'INFO'
43
+ */
44
+ export function setLogLevel(level) {
45
+ const upper = level.toUpperCase();
46
+ if (upper in LEVELS) {
47
+ currentLevel = upper;
48
+ }
49
+ }
50
+ export function getLogLevel() {
51
+ return currentLevel;
52
+ }
41
53
  export const logger = {
42
54
  debug: (...args) => log('DEBUG', ...args),
43
55
  info: (...args) => log('INFO', ...args),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.6.4",
3
+ "version": "2.7.1",
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",