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.
Files changed (106) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +105 -30
  3. package/dist/agents/codex-runner.js +15 -7
  4. package/dist/agents/gemini-runner.js +14 -5
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1064 -279
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/baseagent-loader.js +48 -0
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +1090 -838
  40. package/dist/core/evolagent-registry.js +191 -360
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +326 -145
  48. package/dist/core/message/message-queue.js +5 -5
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +437 -273
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -576
  92. package/dist/core/agent-loader.js +0 -39
  93. package/dist/core/agent-registry.js +0 -450
  94. package/dist/core/evolagent-schema.js +0 -72
  95. package/dist/core/message/stream-flusher.js +0 -238
  96. package/dist/core/message/thought-emitter.js +0 -162
  97. package/dist/core/reload-hooks.js +0 -87
  98. package/dist/prompts/templates.js +0 -122
  99. package/dist/templates/skills.md +0 -66
  100. package/dist/utils/channel-fingerprint.js +0 -59
  101. package/dist/utils/error-dict.js +0 -63
  102. package/dist/utils/format.js +0 -32
  103. package/dist/utils/init.js +0 -645
  104. package/dist/utils/migrate-project.js +0 -122
  105. package/dist/utils/reload-hooks.js +0 -87
  106. 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
- }