evolclaw 2.1.2 → 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 +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → 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 +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.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() {
|
|
@@ -15,6 +16,38 @@ function loadClaudeSettings() {
|
|
|
15
16
|
catch { }
|
|
16
17
|
return {};
|
|
17
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
|
+
}
|
|
18
51
|
export function resolveAnthropicConfig(config) {
|
|
19
52
|
const settings = loadClaudeSettings();
|
|
20
53
|
// 过滤占位符,视为未配置
|
|
@@ -42,9 +75,95 @@ export function resolveAnthropicConfig(config) {
|
|
|
42
75
|
|| undefined;
|
|
43
76
|
return { apiKey, baseUrl, model, effort };
|
|
44
77
|
}
|
|
78
|
+
export function resolveOpenaiConfig(config) {
|
|
79
|
+
const codexSettings = loadCodexSettings();
|
|
80
|
+
// 过滤占位符,视为未配置
|
|
81
|
+
const configApiKey = config.agents?.openai?.apiKey;
|
|
82
|
+
const isPlaceholderKey = !configApiKey ||
|
|
83
|
+
configApiKey.includes('your-') ||
|
|
84
|
+
configApiKey.includes('placeholder');
|
|
85
|
+
const apiKey = (isPlaceholderKey ? null : configApiKey)
|
|
86
|
+
|| process.env.OPENAI_API_KEY
|
|
87
|
+
|| codexSettings.apiKey;
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
throw new Error('No OpenAI API key found. Set one of: agents.openai.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
|
|
90
|
+
}
|
|
91
|
+
// baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
|
|
92
|
+
const configBaseUrl = config.agents?.openai?.baseUrl;
|
|
93
|
+
const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
|
|
94
|
+
const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
|
|
95
|
+
|| process.env.OPENAI_BASE_URL
|
|
96
|
+
|| codexSettings.baseUrl
|
|
97
|
+
|| undefined;
|
|
98
|
+
const model = config.agents?.openai?.model
|
|
99
|
+
|| codexSettings.model
|
|
100
|
+
|| 'gpt-5.2-codex';
|
|
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 };
|
|
125
|
+
}
|
|
45
126
|
export function loadConfig(configPath = resolvePaths().config) {
|
|
46
127
|
if (!fs.existsSync(configPath)) {
|
|
47
|
-
|
|
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
|
+
}
|
|
48
167
|
}
|
|
49
168
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
50
169
|
const config = JSON.parse(content);
|
|
@@ -54,42 +173,140 @@ export function loadConfig(configPath = resolvePaths().config) {
|
|
|
54
173
|
export function saveConfig(config, configPath = resolvePaths().config) {
|
|
55
174
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
56
175
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 }];
|
|
60
192
|
}
|
|
61
|
-
|
|
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) {
|
|
62
229
|
if (!config.channels)
|
|
63
230
|
config.channels = {};
|
|
64
231
|
const channels = config.channels;
|
|
65
|
-
|
|
66
|
-
channels[
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
260
|
}
|
|
70
|
-
export function isOwner(config,
|
|
71
|
-
|
|
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;
|
|
72
277
|
}
|
|
73
278
|
function validateConfig(config) {
|
|
74
279
|
// anthropic 部分不再强制校验,由 resolveAnthropicConfig() 处理
|
|
75
|
-
// Feishu
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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)`);
|
|
79
286
|
}
|
|
80
|
-
if (!
|
|
81
|
-
logger.warn(
|
|
287
|
+
if (!inst.appSecret || inst.appSecret.startsWith('YOUR_')) {
|
|
288
|
+
logger.warn(`⚠ Feishu${label} appSecret not configured (Feishu channel will be disabled)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
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)`);
|
|
82
299
|
}
|
|
83
300
|
}
|
|
84
|
-
if (!config.channels?.aun?.domain)
|
|
85
|
-
throw new Error('Missing channels.aun.domain');
|
|
86
|
-
if (!config.channels?.aun?.agentName)
|
|
87
|
-
throw new Error('Missing channels.aun.agentName');
|
|
88
301
|
if (!config.projects?.defaultPath)
|
|
89
302
|
throw new Error('Missing projects.defaultPath');
|
|
90
|
-
// WeChat 配置可选,但如果启用了就需要 token
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
310
|
}
|
|
94
311
|
}
|
|
95
312
|
export function ensureDir(dirPath) {
|
|
@@ -97,3 +314,38 @@ export function ensureDir(dirPath) {
|
|
|
97
314
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
98
315
|
}
|
|
99
316
|
}
|
|
317
|
+
// agents.defaultAgent → config key 映射
|
|
318
|
+
const agentKeyMap = { claude: 'anthropic', codex: 'openai', gemini: 'google' };
|
|
319
|
+
/**
|
|
320
|
+
* 配置结构完整性校验(不校验凭据有效性)。
|
|
321
|
+
* 要求 agents/channels/projects 三段同时具备必要的锚点字段。
|
|
322
|
+
*/
|
|
323
|
+
export function validateConfigIntegrity(config) {
|
|
324
|
+
const reasons = [];
|
|
325
|
+
// agents
|
|
326
|
+
const defaultAgent = config.agents?.defaultAgent;
|
|
327
|
+
if (!defaultAgent) {
|
|
328
|
+
reasons.push('Missing agents.defaultAgent');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const key = agentKeyMap[defaultAgent] || defaultAgent;
|
|
332
|
+
if (!config.agents?.[key]) {
|
|
333
|
+
reasons.push(`agents.defaultAgent='${defaultAgent}' but agents.${key} does not exist`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// channels
|
|
337
|
+
const defaultChannel = config.channels?.defaultChannel;
|
|
338
|
+
if (!defaultChannel) {
|
|
339
|
+
reasons.push('Missing channels.defaultChannel');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
if (!config.channels?.[defaultChannel]) {
|
|
343
|
+
reasons.push(`channels.defaultChannel='${defaultChannel}' but channels.${defaultChannel} does not exist`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// projects
|
|
347
|
+
if (!config.projects?.defaultPath) {
|
|
348
|
+
reasons.push('Missing projects.defaultPath');
|
|
349
|
+
}
|
|
350
|
+
return { valid: reasons.length === 0, reasons };
|
|
351
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Plugin System
|
|
3
|
+
*
|
|
4
|
+
* Provides a lightweight plugin interface for channel integration.
|
|
5
|
+
* Plugins are responsible for creating channel instances only.
|
|
6
|
+
* The main service (index.ts) handles registration and message flow wiring.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* Channel Loader
|
|
11
|
+
*
|
|
12
|
+
* Manages channel plugin registration and lifecycle.
|
|
13
|
+
*/
|
|
14
|
+
export class ChannelLoader {
|
|
15
|
+
plugins = new Map();
|
|
16
|
+
register(plugin) {
|
|
17
|
+
if (this.plugins.has(plugin.name)) {
|
|
18
|
+
throw new Error(`Channel plugin '${plugin.name}' already registered`);
|
|
19
|
+
}
|
|
20
|
+
this.plugins.set(plugin.name, plugin);
|
|
21
|
+
logger.debug(`Registered channel plugin: ${plugin.name}`);
|
|
22
|
+
}
|
|
23
|
+
async createAll(config) {
|
|
24
|
+
const instances = [];
|
|
25
|
+
for (const [name, plugin] of this.plugins) {
|
|
26
|
+
if (!plugin.isEnabled(config)) {
|
|
27
|
+
logger.info(`Channel '${name}' is disabled, skipping`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
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
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error(`✗ Failed to create channel '${name}':`, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return instances;
|
|
47
|
+
}
|
|
48
|
+
async connectAll(instances) {
|
|
49
|
+
const results = await Promise.allSettled(instances.map(async (inst) => {
|
|
50
|
+
await inst.connect();
|
|
51
|
+
return inst.adapter.channelName;
|
|
52
|
+
}));
|
|
53
|
+
const connected = results
|
|
54
|
+
.filter((r) => r.status === 'fulfilled')
|
|
55
|
+
.map((r) => r.value);
|
|
56
|
+
const failed = results
|
|
57
|
+
.filter((r) => r.status === 'rejected')
|
|
58
|
+
.map((r) => r.reason);
|
|
59
|
+
if (failed.length > 0) {
|
|
60
|
+
logger.warn(`Some channels failed to connect:`, failed);
|
|
61
|
+
}
|
|
62
|
+
return connected;
|
|
63
|
+
}
|
|
64
|
+
async disconnectAll(instances) {
|
|
65
|
+
await Promise.allSettled(instances.map((inst) => inst.disconnect()));
|
|
66
|
+
}
|
|
67
|
+
}
|