evolclaw 2.8.2 → 3.0.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 +21 -12
- package/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
package/dist/config.js
DELETED
|
@@ -1,576 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { logger } from './utils/logger.js';
|
|
5
|
-
import { resolvePaths, getPackageRoot as _getPackageRoot } from './paths.js';
|
|
6
|
-
import { commandExists } from './utils/cross-platform.js';
|
|
7
|
-
// Re-export path utilities for backward compatibility
|
|
8
|
-
export { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
|
|
9
|
-
function loadClaudeSettings() {
|
|
10
|
-
try {
|
|
11
|
-
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
12
|
-
if (fs.existsSync(settingsPath)) {
|
|
13
|
-
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
catch { }
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
function loadCodexSettings() {
|
|
20
|
-
try {
|
|
21
|
-
// Read auth.json for API key
|
|
22
|
-
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
23
|
-
let apiKey;
|
|
24
|
-
if (fs.existsSync(authPath)) {
|
|
25
|
-
const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
26
|
-
apiKey = auth.OPENAI_API_KEY;
|
|
27
|
-
}
|
|
28
|
-
// Read config.toml for model and baseUrl (simple TOML parsing)
|
|
29
|
-
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
30
|
-
let model;
|
|
31
|
-
let baseUrl;
|
|
32
|
-
if (fs.existsSync(configPath)) {
|
|
33
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
-
const modelMatch = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
35
|
-
if (modelMatch)
|
|
36
|
-
model = modelMatch[1];
|
|
37
|
-
// Extract base_url from model_providers section
|
|
38
|
-
const providerMatch = content.match(/^model_provider\s*=\s*"([^"]+)"/m);
|
|
39
|
-
if (providerMatch) {
|
|
40
|
-
const provider = providerMatch[1];
|
|
41
|
-
const baseUrlMatch = content.match(new RegExp(`\\[model_providers\\.${provider}\\][\\s\\S]*?base_url\\s*=\\s*"([^"]+)"`, 'm'));
|
|
42
|
-
if (baseUrlMatch)
|
|
43
|
-
baseUrl = baseUrlMatch[1];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return { apiKey, baseUrl, model };
|
|
47
|
-
}
|
|
48
|
-
catch { }
|
|
49
|
-
return {};
|
|
50
|
-
}
|
|
51
|
-
export function resolveAnthropicConfig(config) {
|
|
52
|
-
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)
|
|
59
|
-
|| process.env.ANTHROPIC_AUTH_TOKEN
|
|
60
|
-
|| settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
61
|
-
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');
|
|
63
|
-
}
|
|
64
|
-
// baseUrl 也过滤占位符
|
|
65
|
-
const configBaseUrl = config.agents?.claude?.baseUrl;
|
|
66
|
-
const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
|
|
67
|
-
const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
|
|
68
|
-
|| process.env.ANTHROPIC_BASE_URL
|
|
69
|
-
|| settings.env?.ANTHROPIC_BASE_URL;
|
|
70
|
-
const model = config.agents?.claude?.model
|
|
71
|
-
|| settings.model
|
|
72
|
-
|| 'sonnet';
|
|
73
|
-
const effort = config.agents?.claude?.effort
|
|
74
|
-
|| settings.effortLevel
|
|
75
|
-
|| undefined;
|
|
76
|
-
const configExecPath = config.agents?.claude?.pathToClaudeCodeExecutable;
|
|
77
|
-
const isPlaceholderExec = !configExecPath || configExecPath.includes('your-') || configExecPath.includes('placeholder');
|
|
78
|
-
const pathToClaudeCodeExecutable = isPlaceholderExec ? undefined : configExecPath;
|
|
79
|
-
return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
|
|
80
|
-
}
|
|
81
|
-
export function resolveOpenaiConfig(config) {
|
|
82
|
-
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)
|
|
89
|
-
|| process.env.OPENAI_API_KEY
|
|
90
|
-
|| codexSettings.apiKey;
|
|
91
|
-
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');
|
|
93
|
-
}
|
|
94
|
-
// baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
|
|
95
|
-
const configBaseUrl = config.agents?.codex?.baseUrl;
|
|
96
|
-
const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
|
|
97
|
-
const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
|
|
98
|
-
|| process.env.OPENAI_BASE_URL
|
|
99
|
-
|| codexSettings.baseUrl
|
|
100
|
-
|| undefined;
|
|
101
|
-
const model = config.agents?.codex?.model
|
|
102
|
-
|| codexSettings.model
|
|
103
|
-
|| 'gpt-5.2-codex';
|
|
104
|
-
const effort = config.agents?.codex?.effort || config.agents?.codex?.reasoning || undefined;
|
|
105
|
-
return { apiKey, baseUrl, model, effort };
|
|
106
|
-
}
|
|
107
|
-
export function resolveGoogleConfig(config) {
|
|
108
|
-
const googleCfg = config.agents?.gemini;
|
|
109
|
-
// CLI path: config → which gemini
|
|
110
|
-
let cliPath = googleCfg?.cliPath || '';
|
|
111
|
-
if (!cliPath) {
|
|
112
|
-
cliPath = commandExists('gemini') ? 'gemini' : '';
|
|
113
|
-
}
|
|
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)
|
|
120
|
-
|| process.env.GEMINI_API_KEY
|
|
121
|
-
|| process.env.GOOGLE_API_KEY
|
|
122
|
-
|| 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';
|
|
127
|
-
return { cliPath, model, apiKey, mode, useVertex, project, location };
|
|
128
|
-
}
|
|
129
|
-
export function loadConfig(configPath = resolvePaths().config) {
|
|
130
|
-
if (!fs.existsSync(configPath)) {
|
|
131
|
-
// Try to recover from backup files
|
|
132
|
-
const dataDir = path.dirname(configPath);
|
|
133
|
-
const backupPath = path.join(dataDir, 'evolclaw.backup.json');
|
|
134
|
-
if (fs.existsSync(backupPath)) {
|
|
135
|
-
logger.warn(`Config file missing, restoring from backup: ${backupPath}`);
|
|
136
|
-
fs.copyFileSync(backupPath, configPath);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// Look for timestamped backups (evolclaw-YYYYMMDD-HHMMSS.json)
|
|
140
|
-
const timestampedBackups = fs.existsSync(dataDir)
|
|
141
|
-
? fs.readdirSync(dataDir)
|
|
142
|
-
.filter(f => /^evolclaw-\d{8}-\d{6}\.json$/.test(f))
|
|
143
|
-
.sort()
|
|
144
|
-
.reverse()
|
|
145
|
-
: [];
|
|
146
|
-
if (timestampedBackups.length > 0) {
|
|
147
|
-
const latest = path.join(dataDir, timestampedBackups[0]);
|
|
148
|
-
logger.warn(`Config file missing, restoring from timestamped backup: ${latest}`);
|
|
149
|
-
fs.copyFileSync(latest, configPath);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
// Create minimal config from sample
|
|
153
|
-
const samplePath = path.join(_getPackageRoot(), 'data', 'evolclaw.sample.json');
|
|
154
|
-
if (fs.existsSync(samplePath)) {
|
|
155
|
-
logger.warn(`Config file missing, creating from sample: ${samplePath}`);
|
|
156
|
-
const sample = JSON.parse(fs.readFileSync(samplePath, 'utf-8'));
|
|
157
|
-
// Set a usable defaultPath
|
|
158
|
-
const defaultProjectDir = path.join(os.homedir(), 'projects', 'default');
|
|
159
|
-
sample.projects.defaultPath = defaultProjectDir;
|
|
160
|
-
if (!fs.existsSync(defaultProjectDir)) {
|
|
161
|
-
fs.mkdirSync(defaultProjectDir, { recursive: true });
|
|
162
|
-
}
|
|
163
|
-
fs.writeFileSync(configPath, JSON.stringify(sample, null, 2), 'utf-8');
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
throw new Error(`Config file not found: ${configPath}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
172
|
-
const config = JSON.parse(content);
|
|
173
|
-
if (migrateAgentsKeys(config)) {
|
|
174
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
175
|
-
logger.warn(`Config migrated: agents.{anthropic,openai,google} → {claude,codex,gemini} in ${configPath}`);
|
|
176
|
-
}
|
|
177
|
-
validateConfig(config);
|
|
178
|
-
return config;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Rename legacy agent config keys to runner names.
|
|
182
|
-
* Returns true if any rename happened.
|
|
183
|
-
*/
|
|
184
|
-
function migrateAgentsKeys(config) {
|
|
185
|
-
const agents = config?.agents;
|
|
186
|
-
if (!agents || typeof agents !== 'object')
|
|
187
|
-
return false;
|
|
188
|
-
const renames = [
|
|
189
|
-
['anthropic', 'claude'],
|
|
190
|
-
['openai', 'codex'],
|
|
191
|
-
['google', 'gemini'],
|
|
192
|
-
];
|
|
193
|
-
let changed = false;
|
|
194
|
-
for (const [oldKey, newKey] of renames) {
|
|
195
|
-
if (agents[oldKey] === undefined)
|
|
196
|
-
continue;
|
|
197
|
-
if (agents[newKey] === undefined) {
|
|
198
|
-
agents[newKey] = agents[oldKey];
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
logger.warn(`Config has both agents.${oldKey} and agents.${newKey}; keeping new key, dropping legacy one`);
|
|
202
|
-
}
|
|
203
|
-
delete agents[oldKey];
|
|
204
|
-
changed = true;
|
|
205
|
-
}
|
|
206
|
-
return changed;
|
|
207
|
-
}
|
|
208
|
-
export function saveConfig(config, configPath = resolvePaths().config) {
|
|
209
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
210
|
-
}
|
|
211
|
-
// ── Channel instance normalization ──
|
|
212
|
-
export const channelTypes = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom'];
|
|
213
|
-
/**
|
|
214
|
-
* Normalize a channel config value (single object, array, or undefined) into an array
|
|
215
|
-
* where every element has a `name` field.
|
|
216
|
-
* - undefined → []
|
|
217
|
-
* - single object → [{ ...obj, name: obj.name ?? defaultName }]
|
|
218
|
-
* - array → passthrough (names must already be present)
|
|
219
|
-
*/
|
|
220
|
-
export function normalizeChannelInstances(cfg, defaultName) {
|
|
221
|
-
if (cfg === undefined || cfg === null)
|
|
222
|
-
return [];
|
|
223
|
-
if (Array.isArray(cfg)) {
|
|
224
|
-
return cfg.map((item, i) => ({
|
|
225
|
-
...item,
|
|
226
|
-
name: item.name ?? (cfg.length === 1 ? defaultName : `${defaultName}-${i + 1}`),
|
|
227
|
-
}));
|
|
228
|
-
}
|
|
229
|
-
return [{ ...cfg, name: cfg.name ?? defaultName }];
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Parse a defaultChannel reference. Supports:
|
|
233
|
-
* "feishu" → { type: "feishu" }
|
|
234
|
-
* "feishu/feilun" → { type: "feishu", instance: "feilun" }
|
|
235
|
-
*/
|
|
236
|
-
export function parseDefaultChannelRef(ref) {
|
|
237
|
-
const slash = ref.indexOf('/');
|
|
238
|
-
if (slash < 0)
|
|
239
|
-
return { type: ref };
|
|
240
|
-
return { type: ref.slice(0, slash), instance: ref.slice(slash + 1) };
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Validate a defaultChannel reference against a channels config block.
|
|
244
|
-
* Returns an error message string if invalid, or null if OK.
|
|
245
|
-
* - type must be in channelTypes
|
|
246
|
-
* - type must have at least one instance configured
|
|
247
|
-
* - if instance specified, must match an existing instance.name
|
|
248
|
-
* - if instance omitted, type must have exactly 1 instance (else ambiguous)
|
|
249
|
-
*/
|
|
250
|
-
export function validateDefaultChannelRef(ref, channelsBlock) {
|
|
251
|
-
const { type, instance } = parseDefaultChannelRef(ref);
|
|
252
|
-
if (!channelTypes.includes(type)) {
|
|
253
|
-
return `channels.defaultChannel='${ref}' references unknown channel type '${type}'`;
|
|
254
|
-
}
|
|
255
|
-
const instances = normalizeChannelInstances(channelsBlock?.[type], type);
|
|
256
|
-
if (instances.length === 0) {
|
|
257
|
-
return `channels.defaultChannel='${ref}' but channels.${type} has no instances`;
|
|
258
|
-
}
|
|
259
|
-
if (instance) {
|
|
260
|
-
if (!instances.some(i => i.name === instance)) {
|
|
261
|
-
return `channels.defaultChannel='${ref}' but channels.${type} has no instance named '${instance}'`;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else if (instances.length > 1) {
|
|
265
|
-
const names = instances.map(i => i.name).join(', ');
|
|
266
|
-
return `channels.defaultChannel='${ref}' is ambiguous: channels.${type} has ${instances.length} instances (${names}); use 'type/instanceName' form`;
|
|
267
|
-
}
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Validate that all channel instance names are unique across all channel types.
|
|
272
|
-
* Throws if duplicate names are found.
|
|
273
|
-
*/
|
|
274
|
-
export function validateChannelInstanceNames(config) {
|
|
275
|
-
const seen = new Map(); // name → channel type
|
|
276
|
-
for (const type of channelTypes) {
|
|
277
|
-
const instances = normalizeChannelInstances(config.channels?.[type], type);
|
|
278
|
-
for (const inst of instances) {
|
|
279
|
-
const prev = seen.get(inst.name);
|
|
280
|
-
if (prev !== undefined) {
|
|
281
|
-
throw new Error(`Duplicate channel instance name "${inst.name}" (found in ${prev} and ${type})`);
|
|
282
|
-
}
|
|
283
|
-
seen.set(inst.name, type);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
export function getOwner(config, channelOrType) {
|
|
288
|
-
for (const type of channelTypes) {
|
|
289
|
-
const raw = config.channels?.[type];
|
|
290
|
-
const instances = normalizeChannelInstances(raw, type);
|
|
291
|
-
// 按实例名查找
|
|
292
|
-
const found = instances.find((inst) => inst.name === channelOrType);
|
|
293
|
-
if (found)
|
|
294
|
-
return found.owner;
|
|
295
|
-
// 按 channelType 查找:返回该类型下第一个有 owner 的实例
|
|
296
|
-
if (type === channelOrType) {
|
|
297
|
-
for (const inst of instances) {
|
|
298
|
-
if (inst.owner)
|
|
299
|
-
return inst.owner;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Find a channel instance by name in a config-like object and set its owner.
|
|
307
|
-
* Returns true if the instance was found and updated.
|
|
308
|
-
*/
|
|
309
|
-
export function writeOwnerToChannelInstance(root, instanceName, userId) {
|
|
310
|
-
const channels = root?.channels;
|
|
311
|
-
if (!channels || typeof channels !== 'object')
|
|
312
|
-
return false;
|
|
313
|
-
for (const type of channelTypes) {
|
|
314
|
-
const raw = channels[type];
|
|
315
|
-
if (raw === undefined)
|
|
316
|
-
continue;
|
|
317
|
-
if (Array.isArray(raw)) {
|
|
318
|
-
const inst = raw.find((item) => item.name === instanceName);
|
|
319
|
-
if (inst) {
|
|
320
|
-
inst.owner = userId;
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
const effectiveName = raw.name ?? type;
|
|
326
|
-
if (effectiveName === instanceName) {
|
|
327
|
-
raw.owner = userId;
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
334
|
-
export function setOwner(config, instanceName, userId, configPath = resolvePaths().config) {
|
|
335
|
-
if (!config.channels)
|
|
336
|
-
config.channels = {};
|
|
337
|
-
// 1. Try writing to evolclaw.json (default-agent channels)
|
|
338
|
-
if (writeOwnerToChannelInstance(config, instanceName, userId)) {
|
|
339
|
-
saveConfig(config, configPath);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
// 2. Last resort: if instanceName matches a channel type with no config, create it
|
|
343
|
-
if (channelTypes.includes(instanceName)) {
|
|
344
|
-
config.channels[instanceName] = { owner: userId };
|
|
345
|
-
saveConfig(config, configPath);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
// 3. I4: No match — warn (don't silently lose owner). Callers managing
|
|
349
|
-
// agent-owned channels should route through EvolAgent.setOwner before
|
|
350
|
-
// falling back to this global setter.
|
|
351
|
-
logger.warn(`[setOwner] Channel instance "${instanceName}" not found in evolclaw.json. Owner ${userId} not persisted.`);
|
|
352
|
-
}
|
|
353
|
-
export function getChannelShowActivities(config, instanceName) {
|
|
354
|
-
for (const type of channelTypes) {
|
|
355
|
-
const raw = config.channels?.[type];
|
|
356
|
-
if (raw === undefined)
|
|
357
|
-
continue;
|
|
358
|
-
if (Array.isArray(raw)) {
|
|
359
|
-
const inst = raw.find((item) => item.name === instanceName);
|
|
360
|
-
if (inst)
|
|
361
|
-
return inst.showActivities ?? config.showActivities ?? 'all';
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
const effectiveName = raw.name ?? type;
|
|
365
|
-
if (effectiveName === instanceName)
|
|
366
|
-
return raw.showActivities ?? config.showActivities ?? 'all';
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return config.showActivities ?? 'all';
|
|
370
|
-
}
|
|
371
|
-
export function setChannelShowActivities(config, instanceName, mode) {
|
|
372
|
-
if (!config.channels)
|
|
373
|
-
config.channels = {};
|
|
374
|
-
const channels = config.channels;
|
|
375
|
-
for (const type of channelTypes) {
|
|
376
|
-
const raw = channels[type];
|
|
377
|
-
if (raw === undefined)
|
|
378
|
-
continue;
|
|
379
|
-
if (Array.isArray(raw)) {
|
|
380
|
-
const inst = raw.find((item) => item.name === instanceName);
|
|
381
|
-
if (inst) {
|
|
382
|
-
inst.showActivities = mode;
|
|
383
|
-
saveConfig(config);
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
const effectiveName = raw.name ?? type;
|
|
389
|
-
if (effectiveName === instanceName) {
|
|
390
|
-
raw.showActivities = mode;
|
|
391
|
-
saveConfig(config);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* 读取全局 chatmode 配置的默认 sessionMode
|
|
399
|
-
* 按 chatType 返回对应模式,未配置时返回 undefined(由 session-manager 回退到 'interactive')
|
|
400
|
-
*/
|
|
401
|
-
export function getDefaultSessionMode(config, chatType) {
|
|
402
|
-
const cm = config.chatmode;
|
|
403
|
-
if (!cm)
|
|
404
|
-
return undefined;
|
|
405
|
-
if (chatType === 'group')
|
|
406
|
-
return cm.group;
|
|
407
|
-
return cm.private;
|
|
408
|
-
}
|
|
409
|
-
export function isOwner(config, channelOrType, userId) {
|
|
410
|
-
// 按实例名精确匹配(evolclaw.json)
|
|
411
|
-
if (getOwner(config, channelOrType) === userId)
|
|
412
|
-
return true;
|
|
413
|
-
// 按 channelType 匹配:检查该类型下所有实例(evolclaw.json)
|
|
414
|
-
for (const type of channelTypes) {
|
|
415
|
-
if (type !== channelOrType)
|
|
416
|
-
continue;
|
|
417
|
-
const raw = config.channels?.[type];
|
|
418
|
-
const instances = normalizeChannelInstances(raw, type);
|
|
419
|
-
for (const inst of instances) {
|
|
420
|
-
if (inst.owner === userId)
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
export function isAdmin(config, channelOrType, userId) {
|
|
427
|
-
// 按实例名精确匹配
|
|
428
|
-
for (const type of channelTypes) {
|
|
429
|
-
const raw = config.channels?.[type];
|
|
430
|
-
const instances = normalizeChannelInstances(raw, type);
|
|
431
|
-
const found = instances.find((inst) => inst.name === channelOrType);
|
|
432
|
-
if (found) {
|
|
433
|
-
const admins = found.admins || [];
|
|
434
|
-
return admins.includes(userId);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
// 按 channelType 匹配:检查该类型下所有实例
|
|
438
|
-
for (const type of channelTypes) {
|
|
439
|
-
if (type !== channelOrType)
|
|
440
|
-
continue;
|
|
441
|
-
const raw = config.channels?.[type];
|
|
442
|
-
const instances = normalizeChannelInstances(raw, type);
|
|
443
|
-
for (const inst of instances) {
|
|
444
|
-
const admins = inst.admins || [];
|
|
445
|
-
if (admins.includes(userId))
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
function validateConfig(config) {
|
|
452
|
-
// anthropic 部分不再强制校验,由 resolveAnthropicConfig() 处理
|
|
453
|
-
// Feishu 配置可选,但如果配置了就要完整(支持 array / object 两种格式)
|
|
454
|
-
const feishuInstances = normalizeChannelInstances(config.channels?.feishu, 'feishu');
|
|
455
|
-
for (const inst of feishuInstances) {
|
|
456
|
-
if (inst.enabled === false)
|
|
457
|
-
continue;
|
|
458
|
-
const appId = inst.appId || '';
|
|
459
|
-
const appSecret = inst.appSecret || '';
|
|
460
|
-
if (!appId && !appSecret)
|
|
461
|
-
continue;
|
|
462
|
-
const label = feishuInstances.length > 1 ? ` [${inst.name}]` : '';
|
|
463
|
-
if (!appId || appId.startsWith('YOUR_')) {
|
|
464
|
-
logger.warn(`⚠ Feishu${label} appId not configured (Feishu channel will be disabled)`);
|
|
465
|
-
}
|
|
466
|
-
if (!appSecret || appSecret.startsWith('YOUR_')) {
|
|
467
|
-
logger.warn(`⚠ Feishu${label} appSecret not configured (Feishu channel will be disabled)`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// AUN 配置可选,但如果配置了就要有 aid(支持 array / object 两种格式)
|
|
471
|
-
const aunInstances = normalizeChannelInstances(config.channels?.aun, 'aun');
|
|
472
|
-
for (const inst of aunInstances) {
|
|
473
|
-
if (inst.enabled === false)
|
|
474
|
-
continue;
|
|
475
|
-
const label = aunInstances.length > 1 ? ` [${inst.name}]` : '';
|
|
476
|
-
if (!inst.aid) {
|
|
477
|
-
logger.warn(`⚠ AUN${label} aid not configured (AUN channel will be disabled)`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (!config.projects?.defaultPath)
|
|
481
|
-
throw new Error('Missing projects.defaultPath');
|
|
482
|
-
// WeChat 配置可选,但如果启用了就需要 token(支持 array / object 两种格式)
|
|
483
|
-
const wechatInstances = normalizeChannelInstances(config.channels?.wechat, 'wechat');
|
|
484
|
-
for (const inst of wechatInstances) {
|
|
485
|
-
if (inst.enabled && !inst.token) {
|
|
486
|
-
const label = wechatInstances.length > 1 ? ` [${inst.name}]` : '';
|
|
487
|
-
logger.warn(`⚠ WeChat${label} enabled but token not configured (WeChat channel will be disabled)`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// DingTalk 配置可选,但如果配置了就需要 clientId + clientSecret
|
|
491
|
-
const dingtalkInstances = normalizeChannelInstances(config.channels?.dingtalk, 'dingtalk');
|
|
492
|
-
for (const inst of dingtalkInstances) {
|
|
493
|
-
if (inst.enabled === false)
|
|
494
|
-
continue;
|
|
495
|
-
const label = dingtalkInstances.length > 1 ? ` [${inst.name}]` : '';
|
|
496
|
-
const hasClientId = !!inst.clientId && !inst.clientId.includes('your-');
|
|
497
|
-
const hasClientSecret = !!inst.clientSecret && !inst.clientSecret.includes('your-');
|
|
498
|
-
if (hasClientId !== hasClientSecret) {
|
|
499
|
-
logger.warn(`⚠ DingTalk${label} clientId/clientSecret incomplete (DingTalk channel will be disabled)`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
// QQBot 配置可选,但如果配置了就需要 appId + clientSecret
|
|
503
|
-
const qqbotInstances = normalizeChannelInstances(config.channels?.qqbot, 'qqbot');
|
|
504
|
-
for (const inst of qqbotInstances) {
|
|
505
|
-
if (inst.enabled === false)
|
|
506
|
-
continue;
|
|
507
|
-
const label = qqbotInstances.length > 1 ? ` [${inst.name}]` : '';
|
|
508
|
-
const hasAppId = !!inst.appId && !inst.appId.includes('your-');
|
|
509
|
-
const hasSecret = !!inst.clientSecret && !inst.clientSecret.includes('your-');
|
|
510
|
-
if (hasAppId !== hasSecret) {
|
|
511
|
-
logger.warn(`⚠ QQBot${label} appId/clientSecret incomplete (QQBot channel will be disabled)`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
export function ensureDir(dirPath) {
|
|
516
|
-
if (!fs.existsSync(dirPath)) {
|
|
517
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* 配置结构完整性校验(不校验凭据有效性)。
|
|
522
|
-
* 要求 agents/channels/projects 三段同时具备必要的锚点字段。
|
|
523
|
-
*/
|
|
524
|
-
export function validateConfigIntegrity(config) {
|
|
525
|
-
const reasons = [];
|
|
526
|
-
// agents — 单 agent 时自动推断,无需显式 defaultAgent
|
|
527
|
-
const defaultAgent = config.agents?.defaultAgent;
|
|
528
|
-
if (!defaultAgent) {
|
|
529
|
-
const agentKeys = Object.keys(config.agents || {}).filter(k => k !== 'defaultAgent');
|
|
530
|
-
const configuredAgents = agentKeys.filter(k => config.agents?.[k]);
|
|
531
|
-
if (configuredAgents.length === 0 && agentKeys.length !== 1) {
|
|
532
|
-
reasons.push('Missing agents.defaultAgent (multiple or no agents configured)');
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
if (!config.agents?.[defaultAgent]) {
|
|
537
|
-
reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${defaultAgent} does not exist`);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
// channels — 单实例自动推断,多实例必填 defaultChannel
|
|
541
|
-
// 支持两种形式:
|
|
542
|
-
// "feishu" → type 级,要求该 type 下只有 1 个实例
|
|
543
|
-
// "feishu/feilun" → type/instanceName,精确指向实例
|
|
544
|
-
const totalInstances = channelTypes.reduce((acc, t) => {
|
|
545
|
-
return acc + normalizeChannelInstances(config.channels?.[t], t).length;
|
|
546
|
-
}, 0);
|
|
547
|
-
if (totalInstances === 0) {
|
|
548
|
-
reasons.push('Missing channels: no channel instances configured');
|
|
549
|
-
}
|
|
550
|
-
else if (totalInstances === 1) {
|
|
551
|
-
// 单实例:defaultChannel 可省略(自动推断)
|
|
552
|
-
const dc = config.channels?.defaultChannel;
|
|
553
|
-
if (dc) {
|
|
554
|
-
const err = validateDefaultChannelRef(dc, config.channels);
|
|
555
|
-
if (err)
|
|
556
|
-
reasons.push(err);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
else {
|
|
560
|
-
// 多实例:defaultChannel 必填
|
|
561
|
-
const dc = config.channels?.defaultChannel;
|
|
562
|
-
if (!dc) {
|
|
563
|
-
reasons.push('Missing channels.defaultChannel (multiple channel instances configured; must specify "type" or "type/instanceName")');
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
const err = validateDefaultChannelRef(dc, config.channels);
|
|
567
|
-
if (err)
|
|
568
|
-
reasons.push(err);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// projects
|
|
572
|
-
if (!config.projects?.defaultPath) {
|
|
573
|
-
reasons.push('Missing projects.defaultPath');
|
|
574
|
-
}
|
|
575
|
-
return { valid: reasons.length === 0, reasons };
|
|
576
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Plugin System
|
|
3
|
-
*
|
|
4
|
-
* Provides a lightweight plugin interface for agent integration.
|
|
5
|
-
*/
|
|
6
|
-
import { logger } from '../utils/logger.js';
|
|
7
|
-
/**
|
|
8
|
-
* Agent Loader
|
|
9
|
-
*
|
|
10
|
-
* Manages agent plugin registration and creation.
|
|
11
|
-
*/
|
|
12
|
-
export class AgentLoader {
|
|
13
|
-
plugins = new Map();
|
|
14
|
-
register(plugin) {
|
|
15
|
-
if (this.plugins.has(plugin.name)) {
|
|
16
|
-
throw new Error(`Agent plugin '${plugin.name}' already registered`);
|
|
17
|
-
}
|
|
18
|
-
this.plugins.set(plugin.name, plugin);
|
|
19
|
-
logger.debug(`Registered agent plugin: ${plugin.name}`);
|
|
20
|
-
}
|
|
21
|
-
createAll(config, callbacks) {
|
|
22
|
-
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);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return instances;
|
|
38
|
-
}
|
|
39
|
-
}
|