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/config.js CHANGED
@@ -48,82 +48,97 @@ function loadCodexSettings() {
48
48
  catch { }
49
49
  return {};
50
50
  }
51
- export function resolveAnthropicConfig(config) {
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
- const configApiKey = config.agents?.claude?.apiKey;
55
- const isPlaceholderKey = !configApiKey ||
56
- configApiKey.includes('your-') ||
57
- configApiKey.includes('placeholder');
58
- const apiKey = (isPlaceholderKey ? null : configApiKey)
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
- // baseUrl 也过滤占位符
65
- const configBaseUrl = config.agents?.claude?.baseUrl;
66
- const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
67
- const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
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 = config.agents?.claude?.model
79
+ const model = override?.model
80
+ || config.agents?.claude?.model
71
81
  || settings.model
72
82
  || 'sonnet';
73
- const effort = config.agents?.claude?.effort
83
+ const effort = override?.effort
84
+ || config.agents?.claude?.effort
74
85
  || settings.effortLevel
75
86
  || undefined;
76
- const configExecPath = config.agents?.claude?.pathToClaudeCodeExecutable;
77
- const isPlaceholderExec = !configExecPath || configExecPath.includes('your-') || configExecPath.includes('placeholder');
78
- const pathToClaudeCodeExecutable = isPlaceholderExec ? undefined : configExecPath;
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 configApiKey = config.agents?.codex?.apiKey;
85
- const isPlaceholderKey = !configApiKey ||
86
- configApiKey.includes('your-') ||
87
- configApiKey.includes('placeholder');
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
- // baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
95
- const configBaseUrl = config.agents?.codex?.baseUrl;
96
- const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
97
- const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
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 = config.agents?.codex?.model
112
+ const model = override?.model
113
+ || config.agents?.codex?.model
102
114
  || codexSettings.model
103
115
  || 'gpt-5.2-codex';
104
- const effort = config.agents?.codex?.effort || config.agents?.codex?.reasoning || undefined;
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
- // CLI path: config which gemini
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
- // Model: config default
115
- const model = googleCfg?.model || 'gemini-2.5-flash';
116
- // API key: config env (optional, CLI has OAuth)
117
- const configApiKey = googleCfg?.apiKey;
118
- const isPlaceholder = !configApiKey || configApiKey.includes('your-') || configApiKey.includes('placeholder');
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 || false;
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
- export function setOwner(config, instanceName, userId, configPath = resolvePaths().config) {
267
- if (!config.channels)
268
- config.channels = {};
269
- const channels = config.channels;
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
- saveConfig(config, configPath);
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
- saveConfig(config, configPath);
288
- return;
343
+ return true;
289
344
  }
290
345
  }
291
346
  }
292
- // Fallback: if instanceName matches a channel type with no config, create it
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 — 单通道时自动推断,无需显式 defaultChannel
487
- const defaultChannel = config.channels?.defaultChannel;
488
- if (!defaultChannel) {
489
- const channelKeys = channelTypes.filter(t => config.channels?.[t]);
490
- if (channelKeys.length === 0) {
491
- reasons.push('Missing channels.defaultChannel (no channels configured)');
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
- if (!config.channels?.[defaultChannel]) {
496
- reasons.push(`channels.defaultChannel='${defaultChannel}' but channels.${defaultChannel} does not exist`);
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
- * Provides a lightweight plugin interface for agent integration.
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
- createAll(config, callbacks) {
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
- for (const [name, plugin] of this.plugins) {
24
- if (!plugin.isEnabled(config)) {
25
- logger.info(`Agent '${name}' is disabled, skipping`);
26
- continue;
27
- }
28
- try {
29
- const instance = plugin.createAgent(config, callbacks);
30
- instances.push(instance);
31
- logger.info(`✓ Agent '${name}' instance created`);
32
- }
33
- catch (error) {
34
- logger.error(`✗ Failed to create agent '${name}':`, error);
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;