evolclaw 2.2.0 → 2.4.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.
Files changed (43) hide show
  1. package/README.md +49 -27
  2. package/data/evolclaw.sample.json +6 -3
  3. package/dist/agents/claude-runner.js +125 -52
  4. package/dist/agents/codex-runner.js +10 -5
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +283 -95
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +232 -57
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +803 -247
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +217 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
  16. package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
  17. package/dist/{utils → core/message}/stream-debouncer.js +1 -1
  18. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  19. package/dist/core/permission.js +212 -11
  20. package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
  21. package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
  22. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  23. package/dist/{utils → core/session}/session-file-health.js +1 -1
  24. package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
  25. package/dist/index.js +140 -57
  26. package/dist/{core/ipc-server.js → ipc.js} +36 -1
  27. package/dist/types.js +3 -0
  28. package/dist/utils/cross-platform.js +38 -1
  29. package/dist/utils/error-utils.js +130 -5
  30. package/dist/utils/init-channel.js +649 -0
  31. package/dist/utils/init.js +55 -150
  32. package/dist/utils/logger.js +8 -3
  33. package/dist/utils/media-cache.js +207 -0
  34. package/dist/{core → utils}/stats-collector.js +16 -0
  35. package/package.json +3 -3
  36. package/dist/core/message-bridge.js +0 -187
  37. package/dist/utils/init-feishu.js +0 -263
  38. package/dist/utils/init-wechat.js +0 -172
  39. package/dist/utils/ipc-client.js +0 -36
  40. package/dist/utils/permission-utils.js +0 -71
  41. /package/dist/{utils → core/message}/message-cache.js +0 -0
  42. /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
  43. /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
package/dist/config.js CHANGED
@@ -2,7 +2,8 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { logger } from './utils/logger.js';
5
- import { resolvePaths } from './paths.js';
5
+ import { resolvePaths, getPackageRoot as _getPackageRoot } from './paths.js';
6
+ import { commandExists } from './utils/cross-platform.js';
6
7
  // Re-export path utilities for backward compatibility
7
8
  export { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
8
9
  function loadClaudeSettings() {
@@ -97,12 +98,72 @@ export function resolveOpenaiConfig(config) {
97
98
  const model = config.agents?.openai?.model
98
99
  || codexSettings.model
99
100
  || 'gpt-5.2-codex';
100
- const reasoning = config.agents?.openai?.reasoning || undefined;
101
- return { apiKey, baseUrl, model, reasoning };
101
+ const effort = config.agents?.openai?.effort || config.agents?.openai?.reasoning || undefined;
102
+ return { apiKey, baseUrl, model, effort };
103
+ }
104
+ export function resolveGoogleConfig(config) {
105
+ const googleCfg = config.agents?.google;
106
+ // CLI path: config → which gemini
107
+ let cliPath = googleCfg?.cliPath || '';
108
+ if (!cliPath) {
109
+ cliPath = commandExists('gemini') ? 'gemini' : '';
110
+ }
111
+ // Model: config → default
112
+ const model = googleCfg?.model || 'gemini-2.5-flash';
113
+ // API key: config → env (optional, CLI has OAuth)
114
+ const configApiKey = googleCfg?.apiKey;
115
+ const isPlaceholder = !configApiKey || configApiKey.includes('your-') || configApiKey.includes('placeholder');
116
+ const apiKey = (isPlaceholder ? undefined : configApiKey)
117
+ || process.env.GEMINI_API_KEY
118
+ || process.env.GOOGLE_API_KEY
119
+ || undefined;
120
+ const mode = googleCfg?.mode || 'cli';
121
+ const useVertex = googleCfg?.useVertex || false;
122
+ const project = googleCfg?.project || process.env.GOOGLE_CLOUD_PROJECT || undefined;
123
+ const location = googleCfg?.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
124
+ return { cliPath, model, apiKey, mode, useVertex, project, location };
102
125
  }
103
126
  export function loadConfig(configPath = resolvePaths().config) {
104
127
  if (!fs.existsSync(configPath)) {
105
- throw new Error(`Config file not found: ${configPath}`);
128
+ // Try to recover from backup files
129
+ const dataDir = path.dirname(configPath);
130
+ const backupPath = path.join(dataDir, 'evolclaw.backup.json');
131
+ if (fs.existsSync(backupPath)) {
132
+ logger.warn(`Config file missing, restoring from backup: ${backupPath}`);
133
+ fs.copyFileSync(backupPath, configPath);
134
+ }
135
+ else {
136
+ // Look for timestamped backups (evolclaw-YYYYMMDD-HHMMSS.json)
137
+ const timestampedBackups = fs.existsSync(dataDir)
138
+ ? fs.readdirSync(dataDir)
139
+ .filter(f => /^evolclaw-\d{8}-\d{6}\.json$/.test(f))
140
+ .sort()
141
+ .reverse()
142
+ : [];
143
+ if (timestampedBackups.length > 0) {
144
+ const latest = path.join(dataDir, timestampedBackups[0]);
145
+ logger.warn(`Config file missing, restoring from timestamped backup: ${latest}`);
146
+ fs.copyFileSync(latest, configPath);
147
+ }
148
+ else {
149
+ // Create minimal config from sample
150
+ const samplePath = path.join(_getPackageRoot(), 'data', 'evolclaw.sample.json');
151
+ if (fs.existsSync(samplePath)) {
152
+ logger.warn(`Config file missing, creating from sample: ${samplePath}`);
153
+ const sample = JSON.parse(fs.readFileSync(samplePath, 'utf-8'));
154
+ // Set a usable defaultPath
155
+ const defaultProjectDir = path.join(os.homedir(), 'evolclaw-project');
156
+ sample.projects.defaultPath = defaultProjectDir;
157
+ if (!fs.existsSync(defaultProjectDir)) {
158
+ fs.mkdirSync(defaultProjectDir, { recursive: true });
159
+ }
160
+ fs.writeFileSync(configPath, JSON.stringify(sample, null, 2), 'utf-8');
161
+ }
162
+ else {
163
+ throw new Error(`Config file not found: ${configPath}`);
164
+ }
165
+ }
166
+ }
106
167
  }
107
168
  const content = fs.readFileSync(configPath, 'utf-8');
108
169
  const config = JSON.parse(content);
@@ -112,47 +173,140 @@ export function loadConfig(configPath = resolvePaths().config) {
112
173
  export function saveConfig(config, configPath = resolvePaths().config) {
113
174
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
114
175
  }
115
- export function getOwner(config, channel) {
116
- const ch = config.channels?.[channel];
117
- return ch?.owner;
176
+ // ── Channel instance normalization ──
177
+ export const channelTypes = ['feishu', 'wechat', 'aun'];
178
+ /**
179
+ * Normalize a channel config value (single object, array, or undefined) into an array
180
+ * where every element has a `name` field.
181
+ * - undefined → []
182
+ * - single object → [{ ...obj, name: obj.name ?? defaultName }]
183
+ * - array → passthrough (names must already be present)
184
+ */
185
+ export function normalizeChannelInstances(cfg, defaultName) {
186
+ if (cfg === undefined || cfg === null)
187
+ return [];
188
+ if (Array.isArray(cfg)) {
189
+ return cfg;
190
+ }
191
+ return [{ ...cfg, name: cfg.name ?? defaultName }];
118
192
  }
119
- export function setOwner(config, channel, userId, configPath = resolvePaths().config) {
193
+ /**
194
+ * Validate that all channel instance names are unique across all channel types.
195
+ * Throws if duplicate names are found.
196
+ */
197
+ export function validateChannelInstanceNames(config) {
198
+ const seen = new Map(); // name → channel type
199
+ for (const type of channelTypes) {
200
+ const instances = normalizeChannelInstances(config.channels?.[type], type);
201
+ for (const inst of instances) {
202
+ const prev = seen.get(inst.name);
203
+ if (prev !== undefined) {
204
+ throw new Error(`Duplicate channel instance name "${inst.name}" (found in ${prev} and ${type})`);
205
+ }
206
+ seen.set(inst.name, type);
207
+ }
208
+ }
209
+ }
210
+ export function getOwner(config, channelOrType) {
211
+ for (const type of channelTypes) {
212
+ const raw = config.channels?.[type];
213
+ const instances = normalizeChannelInstances(raw, type);
214
+ // 按实例名查找
215
+ const found = instances.find((inst) => inst.name === channelOrType);
216
+ if (found)
217
+ return found.owner;
218
+ // 按 channelType 查找:返回该类型下第一个有 owner 的实例
219
+ if (type === channelOrType) {
220
+ for (const inst of instances) {
221
+ if (inst.owner)
222
+ return inst.owner;
223
+ }
224
+ }
225
+ }
226
+ return undefined;
227
+ }
228
+ export function setOwner(config, instanceName, userId, configPath = resolvePaths().config) {
120
229
  if (!config.channels)
121
230
  config.channels = {};
122
231
  const channels = config.channels;
123
- if (!channels[channel])
124
- channels[channel] = {};
125
- channels[channel].owner = userId;
126
- saveConfig(config, configPath);
232
+ for (const type of channelTypes) {
233
+ const raw = channels[type];
234
+ if (raw === undefined)
235
+ continue;
236
+ if (Array.isArray(raw)) {
237
+ const inst = raw.find((item) => item.name === instanceName);
238
+ if (inst) {
239
+ inst.owner = userId;
240
+ saveConfig(config, configPath);
241
+ return;
242
+ }
243
+ }
244
+ else {
245
+ // Single-object form: match if name matches (or defaults to type name)
246
+ const effectiveName = raw.name ?? type;
247
+ if (effectiveName === instanceName) {
248
+ raw.owner = userId;
249
+ saveConfig(config, configPath);
250
+ return;
251
+ }
252
+ }
253
+ }
254
+ // Fallback: if instanceName matches a channel type with no config, create it
255
+ if (channelTypes.includes(instanceName)) {
256
+ channels[instanceName] = { owner: userId };
257
+ saveConfig(config, configPath);
258
+ return;
259
+ }
127
260
  }
128
- export function isOwner(config, channel, userId) {
129
- return getOwner(config, channel) === userId;
261
+ export function isOwner(config, channelOrType, userId) {
262
+ // 按实例名精确匹配
263
+ if (getOwner(config, channelOrType) === userId)
264
+ return true;
265
+ // 按 channelType 匹配:检查该类型下所有实例
266
+ for (const type of channelTypes) {
267
+ if (type !== channelOrType)
268
+ continue;
269
+ const raw = config.channels?.[type];
270
+ const instances = normalizeChannelInstances(raw, type);
271
+ for (const inst of instances) {
272
+ if (inst.owner === userId)
273
+ return true;
274
+ }
275
+ }
276
+ return false;
130
277
  }
131
278
  function validateConfig(config) {
132
279
  // anthropic 部分不再强制校验,由 resolveAnthropicConfig() 处理
133
- // Feishu 配置可选,但如果配置了就要完整
134
- if (config.channels?.feishu) {
135
- if (!config.channels.feishu.appId || config.channels.feishu.appId.startsWith('YOUR_')) {
136
- logger.warn('⚠ Feishu appId not configured (Feishu channel will be disabled)');
280
+ // Feishu 配置可选,但如果配置了就要完整(支持 array / object 两种格式)
281
+ const feishuInstances = normalizeChannelInstances(config.channels?.feishu, 'feishu');
282
+ for (const inst of feishuInstances) {
283
+ const label = feishuInstances.length > 1 ? ` [${inst.name}]` : '';
284
+ if (!inst.appId || inst.appId.startsWith('YOUR_')) {
285
+ logger.warn(`⚠ Feishu${label} appId not configured (Feishu channel will be disabled)`);
137
286
  }
138
- if (!config.channels.feishu.appSecret || config.channels.feishu.appSecret.startsWith('YOUR_')) {
139
- logger.warn('⚠ Feishu appSecret not configured (Feishu channel will be disabled)');
287
+ if (!inst.appSecret || inst.appSecret.startsWith('YOUR_')) {
288
+ logger.warn(`⚠ Feishu${label} appSecret not configured (Feishu channel will be disabled)`);
140
289
  }
141
290
  }
142
- // AUN 配置可选,但如果配置了就要有 domain agentName
143
- if (config.channels?.aun?.enabled !== false && config.channels?.aun) {
144
- if (!config.channels.aun.domain) {
145
- logger.warn('⚠ AUN domain not configured (AUN channel will be disabled)');
146
- }
147
- if (!config.channels.aun.agentName) {
148
- logger.warn('⚠ AUN agentName not configured (AUN channel will be disabled)');
291
+ // AUN 配置可选,但如果配置了就要有 aid(支持 array / object 两种格式)
292
+ const aunInstances = normalizeChannelInstances(config.channels?.aun, 'aun');
293
+ for (const inst of aunInstances) {
294
+ if (inst.enabled === false)
295
+ continue;
296
+ const label = aunInstances.length > 1 ? ` [${inst.name}]` : '';
297
+ if (!inst.aid) {
298
+ logger.warn(`⚠ AUN${label} aid not configured (AUN channel will be disabled)`);
149
299
  }
150
300
  }
151
301
  if (!config.projects?.defaultPath)
152
302
  throw new Error('Missing projects.defaultPath');
153
- // WeChat 配置可选,但如果启用了就需要 token
154
- if (config.channels?.wechat?.enabled && !config.channels?.wechat?.token) {
155
- logger.warn('⚠ WeChat enabled but token not configured (WeChat channel will be disabled)');
303
+ // WeChat 配置可选,但如果启用了就需要 token(支持 array / object 两种格式)
304
+ const wechatInstances = normalizeChannelInstances(config.channels?.wechat, 'wechat');
305
+ for (const inst of wechatInstances) {
306
+ if (inst.enabled && !inst.token) {
307
+ const label = wechatInstances.length > 1 ? ` [${inst.name}]` : '';
308
+ logger.warn(`⚠ WeChat${label} enabled but token not configured (WeChat channel will be disabled)`);
309
+ }
156
310
  }
157
311
  }
158
312
  export function ensureDir(dirPath) {
@@ -161,7 +315,7 @@ export function ensureDir(dirPath) {
161
315
  }
162
316
  }
163
317
  // agents.defaultAgent → config key 映射
164
- const agentKeyMap = { claude: 'anthropic', codex: 'openai' };
318
+ const agentKeyMap = { claude: 'anthropic', codex: 'openai', gemini: 'google' };
165
319
  /**
166
320
  * 配置结构完整性校验(不校验凭据有效性)。
167
321
  * 要求 agents/channels/projects 三段同时具备必要的锚点字段。
@@ -28,9 +28,16 @@ export class ChannelLoader {
28
28
  continue;
29
29
  }
30
30
  try {
31
- const instance = await plugin.createChannel(config);
32
- instances.push(instance);
33
- logger.info(`✓ Channel '${name}' instance created`);
31
+ if (plugin.createChannels) {
32
+ const channelInstances = await plugin.createChannels(config);
33
+ instances.push(...channelInstances);
34
+ logger.info(`✓ Channel '${name}' created ${channelInstances.length} instance(s)`);
35
+ }
36
+ else {
37
+ const instance = await plugin.createChannel(config);
38
+ instances.push(instance);
39
+ logger.info(`✓ Channel '${name}' instance created`);
40
+ }
34
41
  }
35
42
  catch (error) {
36
43
  logger.error(`✗ Failed to create channel '${name}':`, error);
@@ -41,7 +48,7 @@ export class ChannelLoader {
41
48
  async connectAll(instances) {
42
49
  const results = await Promise.allSettled(instances.map(async (inst) => {
43
50
  await inst.connect();
44
- return inst.adapter.name;
51
+ return inst.adapter.channelName;
45
52
  }));
46
53
  const connected = results
47
54
  .filter((r) => r.status === 'fulfilled')