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/README.md +1 -1
- package/data/evolclaw.sample.json +3 -4
- package/dist/agents/claude-runner.js +15 -6
- package/dist/channels/aun.js +97 -30
- package/dist/channels/feishu.js +2 -0
- package/dist/cli.js +29 -1
- package/dist/config.js +66 -40
- package/dist/core/command-handler.js +51 -41
- package/dist/core/message/message-processor.js +43 -12
- package/dist/core/session/session-manager.js +9 -7
- package/dist/index.js +21 -25
- package/dist/templates/prompts.md +4 -4
- package/dist/types.js +2 -1
- package/dist/utils/channel-fingerprint.js +59 -0
- package/dist/utils/cross-platform.js +23 -12
- package/dist/utils/init.js +1 -1
- package/dist/utils/logger.js +15 -3
- package/package.json +1 -1
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,
|
|
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
|
|
80
|
-
sessionManager.setSessionModeResolver((
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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/dist/utils/init.js
CHANGED
|
@@ -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.
|
|
506
|
+
config.agents.claude.model = model;
|
|
507
507
|
let channelConfigured = false;
|
|
508
508
|
while (!channelConfigured) {
|
|
509
509
|
console.log('\n选择消息渠道:');
|
package/dist/utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
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[
|
|
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