evolclaw 2.8.1 → 2.8.3
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/agents/claude-runner.js +18 -7
- package/dist/agents/codex-runner.js +16 -5
- package/dist/agents/gemini-runner.js +15 -4
- package/dist/agents/templates.js +122 -0
- package/dist/channels/aun-ops.js +1 -1
- package/dist/channels/aun.js +22 -0
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/cli.js +345 -48
- package/dist/config.js +152 -65
- package/dist/core/agent-loader.js +34 -19
- package/dist/core/agent-registry.js +287 -1
- package/dist/core/command-handler.js +643 -192
- package/dist/core/evolagent-registry.js +514 -0
- package/dist/core/evolagent.js +250 -1
- package/dist/core/message/message-bridge.js +23 -3
- package/dist/core/message/message-processor.js +64 -15
- package/dist/core/message/message-queue.js +61 -6
- package/dist/core/reload-hooks.js +87 -0
- package/dist/index.js +140 -21
- package/dist/ipc.js +47 -0
- package/dist/types.js +2 -0
- package/dist/utils/reload-hooks.js +87 -0
- package/dist/utils/rich-content-renderer.js +33 -0
- package/dist/utils/stats-collector.js +15 -10
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -48,82 +48,97 @@ function loadCodexSettings() {
|
|
|
48
48
|
catch { }
|
|
49
49
|
return {};
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Resolve anthropic credentials with optional override (from agent.json).
|
|
53
|
+
*
|
|
54
|
+
* Priority: override > globalConfig.agents.claude > env > ~/.claude/settings.json
|
|
55
|
+
*
|
|
56
|
+
* Override is matched against the same shape as `config.agents.claude` so
|
|
57
|
+
* EvolAgent's `agents.claude` block is wired in directly.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveAnthropicConfig(config, override) {
|
|
52
60
|
const settings = loadClaudeSettings();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
62
|
+
// apiKey: override → global → env → settings.json
|
|
63
|
+
const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
|
|
64
|
+
const globalApiKey = isPlaceholder(config.agents?.claude?.apiKey) ? undefined : config.agents?.claude?.apiKey;
|
|
65
|
+
const apiKey = overrideApiKey
|
|
66
|
+
|| globalApiKey
|
|
59
67
|
|| process.env.ANTHROPIC_AUTH_TOKEN
|
|
60
68
|
|| settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
61
69
|
if (!apiKey) {
|
|
62
|
-
throw new Error('No API key found. Set one of: agents.claude.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
|
|
70
|
+
throw new Error('No API key found. Set one of: agents.claude.apiKey (per-agent or global), env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
|
|
63
71
|
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const baseUrl =
|
|
72
|
+
const isPlaceholderUrl = (v) => !v || v.includes('api.anthropic.com');
|
|
73
|
+
const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
|
|
74
|
+
const globalBaseUrl = isPlaceholderUrl(config.agents?.claude?.baseUrl) ? undefined : config.agents?.claude?.baseUrl;
|
|
75
|
+
const baseUrl = overrideBaseUrl
|
|
76
|
+
|| globalBaseUrl
|
|
68
77
|
|| process.env.ANTHROPIC_BASE_URL
|
|
69
78
|
|| settings.env?.ANTHROPIC_BASE_URL;
|
|
70
|
-
const model =
|
|
79
|
+
const model = override?.model
|
|
80
|
+
|| config.agents?.claude?.model
|
|
71
81
|
|| settings.model
|
|
72
82
|
|| 'sonnet';
|
|
73
|
-
const effort =
|
|
83
|
+
const effort = override?.effort
|
|
84
|
+
|| config.agents?.claude?.effort
|
|
74
85
|
|| settings.effortLevel
|
|
75
86
|
|| undefined;
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
87
|
+
const pickExec = (v) => (!v || v.includes('your-') || v.includes('placeholder')) ? undefined : v;
|
|
88
|
+
const pathToClaudeCodeExecutable = pickExec(override?.pathToClaudeCodeExecutable)
|
|
89
|
+
|| pickExec(config.agents?.claude?.pathToClaudeCodeExecutable);
|
|
79
90
|
return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
|
|
80
91
|
}
|
|
81
|
-
export function resolveOpenaiConfig(config) {
|
|
92
|
+
export function resolveOpenaiConfig(config, override) {
|
|
82
93
|
const codexSettings = loadCodexSettings();
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const apiKey = (isPlaceholderKey ? null : configApiKey)
|
|
94
|
+
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
95
|
+
const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
|
|
96
|
+
const globalApiKey = isPlaceholder(config.agents?.codex?.apiKey) ? undefined : config.agents?.codex?.apiKey;
|
|
97
|
+
const apiKey = overrideApiKey
|
|
98
|
+
|| globalApiKey
|
|
89
99
|
|| process.env.OPENAI_API_KEY
|
|
90
100
|
|| codexSettings.apiKey;
|
|
91
101
|
if (!apiKey) {
|
|
92
|
-
throw new Error('No OpenAI API key found. Set one of: agents.codex.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
|
|
102
|
+
throw new Error('No OpenAI API key found. Set one of: agents.codex.apiKey (per-agent or global), env OPENAI_API_KEY, or ~/.codex/auth.json');
|
|
93
103
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const baseUrl =
|
|
104
|
+
const isPlaceholderUrl = (v) => !v || v.includes('api.openai.com');
|
|
105
|
+
const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
|
|
106
|
+
const globalBaseUrl = isPlaceholderUrl(config.agents?.codex?.baseUrl) ? undefined : config.agents?.codex?.baseUrl;
|
|
107
|
+
const baseUrl = overrideBaseUrl
|
|
108
|
+
|| globalBaseUrl
|
|
98
109
|
|| process.env.OPENAI_BASE_URL
|
|
99
110
|
|| codexSettings.baseUrl
|
|
100
111
|
|| undefined;
|
|
101
|
-
const model =
|
|
112
|
+
const model = override?.model
|
|
113
|
+
|| config.agents?.codex?.model
|
|
102
114
|
|| codexSettings.model
|
|
103
115
|
|| 'gpt-5.2-codex';
|
|
104
|
-
const effort =
|
|
116
|
+
const effort = override?.effort
|
|
117
|
+
|| override?.reasoning
|
|
118
|
+
|| config.agents?.codex?.effort
|
|
119
|
+
|| config.agents?.codex?.reasoning
|
|
120
|
+
|| undefined;
|
|
105
121
|
return { apiKey, baseUrl, model, effort };
|
|
106
122
|
}
|
|
107
|
-
export function resolveGoogleConfig(config) {
|
|
123
|
+
export function resolveGoogleConfig(config, override) {
|
|
108
124
|
const googleCfg = config.agents?.gemini;
|
|
109
|
-
|
|
110
|
-
let cliPath = googleCfg?.cliPath || '';
|
|
125
|
+
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
126
|
+
let cliPath = override?.cliPath || googleCfg?.cliPath || '';
|
|
111
127
|
if (!cliPath) {
|
|
112
128
|
cliPath = commandExists('gemini') ? 'gemini' : '';
|
|
113
129
|
}
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const apiKey = (isPlaceholder ? undefined : configApiKey)
|
|
130
|
+
const model = override?.model || googleCfg?.model || 'gemini-2.5-flash';
|
|
131
|
+
const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
|
|
132
|
+
const globalApiKey = isPlaceholder(googleCfg?.apiKey) ? undefined : googleCfg?.apiKey;
|
|
133
|
+
const apiKey = overrideApiKey
|
|
134
|
+
|| globalApiKey
|
|
120
135
|
|| process.env.GEMINI_API_KEY
|
|
121
136
|
|| process.env.GOOGLE_API_KEY
|
|
122
137
|
|| undefined;
|
|
123
|
-
const mode = googleCfg?.mode || 'cli';
|
|
124
|
-
const useVertex = googleCfg?.useVertex
|
|
125
|
-
const project = googleCfg?.project || process.env.GOOGLE_CLOUD_PROJECT || undefined;
|
|
126
|
-
const location = googleCfg?.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
|
|
138
|
+
const mode = override?.mode || googleCfg?.mode || 'cli';
|
|
139
|
+
const useVertex = override?.useVertex ?? googleCfg?.useVertex ?? false;
|
|
140
|
+
const project = override?.project || googleCfg?.project || process.env.GOOGLE_CLOUD_PROJECT || undefined;
|
|
141
|
+
const location = override?.location || googleCfg?.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
|
|
127
142
|
return { cliPath, model, apiKey, mode, useVertex, project, location };
|
|
128
143
|
}
|
|
129
144
|
export function loadConfig(configPath = resolvePaths().config) {
|
|
@@ -228,6 +243,45 @@ export function normalizeChannelInstances(cfg, defaultName) {
|
|
|
228
243
|
}
|
|
229
244
|
return [{ ...cfg, name: cfg.name ?? defaultName }];
|
|
230
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Parse a defaultChannel reference. Supports:
|
|
248
|
+
* "feishu" → { type: "feishu" }
|
|
249
|
+
* "feishu/feilun" → { type: "feishu", instance: "feilun" }
|
|
250
|
+
*/
|
|
251
|
+
export function parseDefaultChannelRef(ref) {
|
|
252
|
+
const slash = ref.indexOf('/');
|
|
253
|
+
if (slash < 0)
|
|
254
|
+
return { type: ref };
|
|
255
|
+
return { type: ref.slice(0, slash), instance: ref.slice(slash + 1) };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Validate a defaultChannel reference against a channels config block.
|
|
259
|
+
* Returns an error message string if invalid, or null if OK.
|
|
260
|
+
* - type must be in channelTypes
|
|
261
|
+
* - type must have at least one instance configured
|
|
262
|
+
* - if instance specified, must match an existing instance.name
|
|
263
|
+
* - if instance omitted, type must have exactly 1 instance (else ambiguous)
|
|
264
|
+
*/
|
|
265
|
+
export function validateDefaultChannelRef(ref, channelsBlock) {
|
|
266
|
+
const { type, instance } = parseDefaultChannelRef(ref);
|
|
267
|
+
if (!channelTypes.includes(type)) {
|
|
268
|
+
return `channels.defaultChannel='${ref}' references unknown channel type '${type}'`;
|
|
269
|
+
}
|
|
270
|
+
const instances = normalizeChannelInstances(channelsBlock?.[type], type);
|
|
271
|
+
if (instances.length === 0) {
|
|
272
|
+
return `channels.defaultChannel='${ref}' but channels.${type} has no instances`;
|
|
273
|
+
}
|
|
274
|
+
if (instance) {
|
|
275
|
+
if (!instances.some(i => i.name === instance)) {
|
|
276
|
+
return `channels.defaultChannel='${ref}' but channels.${type} has no instance named '${instance}'`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (instances.length > 1) {
|
|
280
|
+
const names = instances.map(i => i.name).join(', ');
|
|
281
|
+
return `channels.defaultChannel='${ref}' is ambiguous: channels.${type} has ${instances.length} instances (${names}); use 'type/instanceName' form`;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
231
285
|
/**
|
|
232
286
|
* Validate that all channel instance names are unique across all channel types.
|
|
233
287
|
* Throws if duplicate names are found.
|
|
@@ -263,10 +317,14 @@ export function getOwner(config, channelOrType) {
|
|
|
263
317
|
}
|
|
264
318
|
return undefined;
|
|
265
319
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Find a channel instance by name in a config-like object and set its owner.
|
|
322
|
+
* Returns true if the instance was found and updated.
|
|
323
|
+
*/
|
|
324
|
+
export function writeOwnerToChannelInstance(root, instanceName, userId) {
|
|
325
|
+
const channels = root?.channels;
|
|
326
|
+
if (!channels || typeof channels !== 'object')
|
|
327
|
+
return false;
|
|
270
328
|
for (const type of channelTypes) {
|
|
271
329
|
const raw = channels[type];
|
|
272
330
|
if (raw === undefined)
|
|
@@ -275,26 +333,37 @@ export function setOwner(config, instanceName, userId, configPath = resolvePaths
|
|
|
275
333
|
const inst = raw.find((item) => item.name === instanceName);
|
|
276
334
|
if (inst) {
|
|
277
335
|
inst.owner = userId;
|
|
278
|
-
|
|
279
|
-
return;
|
|
336
|
+
return true;
|
|
280
337
|
}
|
|
281
338
|
}
|
|
282
339
|
else {
|
|
283
|
-
// Single-object form: match if name matches (or defaults to type name)
|
|
284
340
|
const effectiveName = raw.name ?? type;
|
|
285
341
|
if (effectiveName === instanceName) {
|
|
286
342
|
raw.owner = userId;
|
|
287
|
-
|
|
288
|
-
return;
|
|
343
|
+
return true;
|
|
289
344
|
}
|
|
290
345
|
}
|
|
291
346
|
}
|
|
292
|
-
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
export function setOwner(config, instanceName, userId, configPath = resolvePaths().config) {
|
|
350
|
+
if (!config.channels)
|
|
351
|
+
config.channels = {};
|
|
352
|
+
// 1. Try writing to evolclaw.json (default-agent channels)
|
|
353
|
+
if (writeOwnerToChannelInstance(config, instanceName, userId)) {
|
|
354
|
+
saveConfig(config, configPath);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// 2. Last resort: if instanceName matches a channel type with no config, create it
|
|
293
358
|
if (channelTypes.includes(instanceName)) {
|
|
294
|
-
channels[instanceName] = { owner: userId };
|
|
359
|
+
config.channels[instanceName] = { owner: userId };
|
|
295
360
|
saveConfig(config, configPath);
|
|
296
361
|
return;
|
|
297
362
|
}
|
|
363
|
+
// 3. I4: No match — warn (don't silently lose owner). Callers managing
|
|
364
|
+
// agent-owned channels should route through EvolAgent.setOwner before
|
|
365
|
+
// falling back to this global setter.
|
|
366
|
+
logger.warn(`[setOwner] Channel instance "${instanceName}" not found in evolclaw.json. Owner ${userId} not persisted.`);
|
|
298
367
|
}
|
|
299
368
|
export function getChannelShowActivities(config, instanceName) {
|
|
300
369
|
for (const type of channelTypes) {
|
|
@@ -353,10 +422,10 @@ export function getDefaultSessionMode(config, chatType) {
|
|
|
353
422
|
return cm.private;
|
|
354
423
|
}
|
|
355
424
|
export function isOwner(config, channelOrType, userId) {
|
|
356
|
-
//
|
|
425
|
+
// 按实例名精确匹配(evolclaw.json)
|
|
357
426
|
if (getOwner(config, channelOrType) === userId)
|
|
358
427
|
return true;
|
|
359
|
-
// 按 channelType
|
|
428
|
+
// 按 channelType 匹配:检查该类型下所有实例(evolclaw.json)
|
|
360
429
|
for (const type of channelTypes) {
|
|
361
430
|
if (type !== channelOrType)
|
|
362
431
|
continue;
|
|
@@ -483,17 +552,35 @@ export function validateConfigIntegrity(config) {
|
|
|
483
552
|
reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${defaultAgent} does not exist`);
|
|
484
553
|
}
|
|
485
554
|
}
|
|
486
|
-
// channels —
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
555
|
+
// channels — 单实例自动推断,多实例必填 defaultChannel
|
|
556
|
+
// 支持两种形式:
|
|
557
|
+
// "feishu" → type 级,要求该 type 下只有 1 个实例
|
|
558
|
+
// "feishu/feilun" → type/instanceName,精确指向实例
|
|
559
|
+
const totalInstances = channelTypes.reduce((acc, t) => {
|
|
560
|
+
return acc + normalizeChannelInstances(config.channels?.[t], t).length;
|
|
561
|
+
}, 0);
|
|
562
|
+
if (totalInstances === 0) {
|
|
563
|
+
reasons.push('Missing channels: no channel instances configured');
|
|
564
|
+
}
|
|
565
|
+
else if (totalInstances === 1) {
|
|
566
|
+
// 单实例:defaultChannel 可省略(自动推断)
|
|
567
|
+
const dc = config.channels?.defaultChannel;
|
|
568
|
+
if (dc) {
|
|
569
|
+
const err = validateDefaultChannelRef(dc, config.channels);
|
|
570
|
+
if (err)
|
|
571
|
+
reasons.push(err);
|
|
492
572
|
}
|
|
493
573
|
}
|
|
494
574
|
else {
|
|
495
|
-
|
|
496
|
-
|
|
575
|
+
// 多实例:defaultChannel 必填
|
|
576
|
+
const dc = config.channels?.defaultChannel;
|
|
577
|
+
if (!dc) {
|
|
578
|
+
reasons.push('Missing channels.defaultChannel (multiple channel instances configured; must specify "type" or "type/instanceName")');
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
const err = validateDefaultChannelRef(dc, config.channels);
|
|
582
|
+
if (err)
|
|
583
|
+
reasons.push(err);
|
|
497
584
|
}
|
|
498
585
|
}
|
|
499
586
|
// projects
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent Plugin System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Per-EvolAgent runner instantiation: each (EvolAgent × baseagent) gets its
|
|
5
|
+
* own runner so that runtime state (model/effort/permissionMode/credentials)
|
|
6
|
+
* is fully isolated.
|
|
5
7
|
*/
|
|
6
8
|
import { logger } from '../utils/logger.js';
|
|
7
|
-
/**
|
|
8
|
-
* Agent Loader
|
|
9
|
-
*
|
|
10
|
-
* Manages agent plugin registration and creation.
|
|
11
|
-
*/
|
|
9
|
+
/** Agent Loader — produces one runner per (EvolAgent × baseagent). */
|
|
12
10
|
export class AgentLoader {
|
|
13
11
|
plugins = new Map();
|
|
14
12
|
register(plugin) {
|
|
@@ -18,20 +16,37 @@ export class AgentLoader {
|
|
|
18
16
|
this.plugins.set(plugin.name, plugin);
|
|
19
17
|
logger.debug(`Registered agent plugin: ${plugin.name}`);
|
|
20
18
|
}
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Iterate over all EvolAgents (DefaultAgent + each named agent in registry)
|
|
21
|
+
* × all registered plugins. Each successful (agent, plugin) pair yields one
|
|
22
|
+
* runner instance.
|
|
23
|
+
*/
|
|
24
|
+
createAll(globalConfig, registry, callbacks) {
|
|
22
25
|
const instances = [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
const allAgents = [];
|
|
27
|
+
const def = registry.get('[default]');
|
|
28
|
+
if (def)
|
|
29
|
+
allAgents.push(def);
|
|
30
|
+
for (const a of registry.runnableAgents())
|
|
31
|
+
allAgents.push(a);
|
|
32
|
+
for (const agent of allAgents) {
|
|
33
|
+
for (const [pluginName, plugin] of this.plugins) {
|
|
34
|
+
if (!plugin.isEnabled(globalConfig, agent)) {
|
|
35
|
+
logger.debug(`Plugin '${pluginName}' disabled for agent '${agent.name}', skipping`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const instance = plugin.createAgent(globalConfig, agent, callbacks);
|
|
40
|
+
if (!instance) {
|
|
41
|
+
logger.debug(`Plugin '${pluginName}' returned null for agent '${agent.name}', skipping`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
instances.push(instance);
|
|
45
|
+
logger.info(`✓ Runner created: agent=${instance.evolagentName} baseagent=${instance.baseagent}`);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
logger.error(`✗ Failed to create runner for agent='${agent.name}' baseagent='${pluginName}':`, error);
|
|
49
|
+
}
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
return instances;
|