evolclaw 2.6.4 → 2.7.0

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, replyInThread: true } : undefined,
247
243
  });
248
244
  }), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
249
245
  replyToMessageId: replyContext?.replyToMessageId,
@@ -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
+ }
@@ -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.0",
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",