evolclaw 2.2.0 → 2.3.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/README.md +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
- package/dist/index.js +138 -54
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /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
|
|
101
|
-
return { apiKey, baseUrl, model,
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
channels[
|
|
125
|
-
|
|
126
|
-
|
|
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,
|
|
129
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 (!
|
|
139
|
-
logger.warn(
|
|
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 配置可选,但如果配置了就要有
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
51
|
+
return inst.adapter.channelName;
|
|
45
52
|
}));
|
|
46
53
|
const connected = results
|
|
47
54
|
.filter((r) => r.status === 'fulfilled')
|