foliko 2.0.0 → 2.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -65,6 +65,9 @@ async function bootstrapDefaults(framework, config = {}) {
65
65
  return;
66
66
  }
67
67
 
68
+ // 启用 bootstrap 模式:load() 只 install 不 start,由 startAll() 统一启动
69
+ framework.pluginManager.setBootstrapping(true);
70
+
68
71
  const shouldLoad = (plugin) => {
69
72
  const name = typeof plugin === 'string' ? plugin : (plugin.name || plugin.prototype?.name);
70
73
  if (framework.pluginManager.has(name)) { return false; }
@@ -187,11 +190,30 @@ async function bootstrapDefaults(framework, config = {}) {
187
190
  }));
188
191
  }
189
192
 
193
+ // 6.75 Messaging plugins (from plugins/messaging/ subdirectory)
194
+ const messagingPlugins = ['telegram', 'qq', 'weixin', 'feishu', 'email'];
195
+ for (const name of messagingPlugins) {
196
+ if (shouldLoad(name)) {
197
+ try {
198
+ const pluginDir = path.resolve(__dirname, '..', '..', '..', 'plugins', 'messaging', name);
199
+ if (!fs.existsSync(pluginDir)) continue;
200
+ const PluginClass = require(`../../messaging/${name}`);
201
+ const pluginConfig = agentConfig[name] || {};
202
+ await framework.loadPlugin(new PluginClass(pluginConfig));
203
+ } catch (err) {
204
+ log.warn(`Messaging plugin '${name}' failed to load:`, err.message);
205
+ }
206
+ }
207
+ }
208
+
190
209
  // 7. Custom plugins from directories
191
210
  await loadCustomPlugins(framework, agentConfig);
192
211
 
193
- // Start all
212
+ // 统一启动所有插件
194
213
  await framework.pluginManager.startAll();
214
+
215
+ // 退出 bootstrap 模式
216
+ framework.pluginManager.setBootstrapping(false);
195
217
  }
196
218
 
197
219
  module.exports = {
@@ -89,6 +89,8 @@ function loadAgentConfig(framework, agentDir = '.foliko') {
89
89
  const pc = JSON.parse(fs.readFileSync(pluginsFile, 'utf-8'));
90
90
  if (pc.telegram) config.telegram = pc.telegram;
91
91
  if (pc.weixin) config.weixin = pc.weixin;
92
+ if (pc.qq) config.qq = pc.qq;
93
+ if (pc.feishu) config.feishu = pc.feishu;
92
94
  if (pc.email) config.email = pc.email;
93
95
  if (pc.pluginLinks) config.pluginLinks = pc.pluginLinks;
94
96
  } catch (err) { log.error('Failed to load plugins.json:', err.message); }
@@ -181,7 +183,7 @@ function loadAgentConfig(framework, agentDir = '.foliko') {
181
183
 
182
184
  const homePlugins = path.join(homeDir, 'plugins.json');
183
185
  if (!fs.existsSync(path.join(resolvedDir, 'plugins.json')) && fs.existsSync(homePlugins)) {
184
- try { const pc = JSON.parse(fs.readFileSync(homePlugins, 'utf-8')); if (pc.telegram && !config.telegram) config.telegram = pc.telegram; if (pc.weixin && !config.weixin) config.weixin = pc.weixin; if (pc.email && !config.email) config.email = pc.email; config.pluginLinks = { ...(pc.pluginLinks || {}), ...config.pluginLinks }; } catch {}
186
+ try { const pc = JSON.parse(fs.readFileSync(homePlugins, 'utf-8')); if (pc.telegram && !config.telegram) config.telegram = pc.telegram; if (pc.weixin && !config.weixin) config.weixin = pc.weixin; if (pc.qq && !config.qq) config.qq = pc.qq; if (pc.feishu && !config.feishu) config.feishu = pc.feishu; if (pc.email && !config.email) config.email = pc.email; config.pluginLinks = { ...(pc.pluginLinks || {}), ...config.pluginLinks }; } catch {}
185
187
  }
186
188
 
187
189
  const homeMcp = path.join(homeDir, 'mcp_config.json');
@@ -75,7 +75,7 @@ class EmailPlugin extends Plugin {
75
75
  - attachments.content: Base64内容
76
76
  - attachments.cid: 嵌入式图片CID(HTML中用 <img src="cid:xxx"> 引用)`
77
77
  this.priority = 50
78
- this.enabled = false
78
+ this.enabled = config.enabled === true
79
79
 
80
80
  this.tools={}
81
81
 
@@ -20,7 +20,7 @@ class FeishuPlugin extends Plugin {
20
20
  this.version = '1.1.0'
21
21
  this.description = '飞书对话插件,使用 WebSocket 长连接接收消息'
22
22
  this.priority = 80
23
- this.enabled = false
23
+ this.enabled = config.enabled === true
24
24
  this.systemPrompt = `你是一个飞书助手。回复内容不要使用markdown格式文本。
25
25
 
26
26
  **重要:** 子Agent 匹配规则必须遵守:
@@ -25,7 +25,7 @@ class QQPlugin extends Plugin {
25
25
  this.version = '1.0.0'
26
26
  this.description = 'QQ 对话插件,使用 QQ 开放平台进行对话'
27
27
  this.priority = 80
28
- this.enabled = false
28
+ this.enabled = config.enabled === true
29
29
  this.path = `.foliko/data`
30
30
 
31
31
  this.config = {
@@ -27,7 +27,7 @@ class TelegramPlugin extends Plugin {
27
27
  this.version = '2.1.0'
28
28
  this.description = 'Telegram 对话插件,绑定主Agent进行持续对话'
29
29
  this.priority = 80
30
- this.enabled = false
30
+ this.enabled = config.enabled === true
31
31
  this.systemPrompt = `你是一个有帮助的AI助手。回复内容不要使用markdown格式文本。
32
32
 
33
33
  **重要:** 子Agent 匹配规则必须遵守:
@@ -6,8 +6,8 @@
6
6
  * - forceLogin: 是否强制重新扫码登录
7
7
  * - qrcodeTerminal: 是否在终端渲染二维码 (默认 true)
8
8
  */
9
- const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../../../cli/src/utils/ansi');
10
- const { renderLine } = require('../../../cli/src/utils/markdown');
9
+ const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../../../src/cli/utils/ansi');
10
+ const { renderLine } = require('../../../src/cli/utils/markdown');
11
11
  const { debounce, throttle } = require('../../../src/utils');
12
12
  const interval = (fn) => {
13
13
  const handle = setInterval(fn, 3000);
@@ -32,7 +32,7 @@ class WeixinPlugin extends Plugin {
32
32
  this.description = '微信对话插件,使用微信网页账号进行对话'
33
33
  this.priority = 80
34
34
  // 默认不启用,需要在 plugins.json 中设置 enabled: true
35
- this.enabled = false
35
+ this.enabled = config.enabled === true
36
36
  this.path=`.foliko/data`
37
37
  this.systemPrompt=`你是一个微信助手。
38
38
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * PluginManager 插件管理器
5
- * 负责插件的加载、卸载、重载
5
+ * 负责插件的加载、卸载、重载、启用/禁用
6
6
  */
7
7
 
8
8
  const { Plugin } = require('./base');
@@ -18,6 +18,7 @@ class PluginManager {
18
18
  this.framework = framework;
19
19
  this._plugins = new Map();
20
20
  this._loading = false;
21
+ this._bootstrapping = false;
21
22
  this._stateFile = path.join(
22
23
  framework?.getCwd?.() ?? process.cwd(),
23
24
  '.foliko',
@@ -37,13 +38,41 @@ class PluginManager {
37
38
  // placeholder - plugins can register event descriptions
38
39
  }
39
40
 
40
- register(plugin) {
41
+ /**
42
+ * 注册插件(不加载,只记录状态)
43
+ */
44
+ register(plugin, options = {}) {
41
45
  if (!(plugin instanceof Plugin)) {
42
46
  throw new PluginError('Plugin must be an instance of Plugin');
43
47
  }
44
- this._plugins.set(plugin.name, { instance: plugin, enabled: true });
48
+
49
+ const savedState = this._loadState();
50
+ const savedEnabled = savedState[plugin.name]?.enabled;
51
+ const savedConfig = savedState[plugin.name]?.config;
52
+
53
+ // 恢复保存的配置
54
+ if (savedConfig && plugin.config && plugin.name !== 'ai') {
55
+ plugin.config = { ...plugin.config, ...savedConfig };
56
+ }
57
+
58
+ // 确定启用状态:系统插件强制启用 → state 文件记录 → 插件默认
59
+ let enabled;
60
+ if (plugin.system) {
61
+ enabled = true;
62
+ } else if (savedEnabled !== undefined) {
63
+ enabled = savedEnabled;
64
+ } else {
65
+ enabled = plugin.enabled !== false;
66
+ }
67
+
68
+ this._plugins.set(plugin.name, {
69
+ instance: plugin,
70
+ status: 'registered',
71
+ enabled,
72
+ sourcePath: plugin.sourcePath || null,
73
+ });
45
74
  this._knownPlugins.add(plugin.name);
46
- this._log.debug(`Plugin registered: ${plugin.name}`);
75
+ this._log.debug(`Plugin registered: ${plugin.name} (enabled: ${enabled})`);
47
76
  return this;
48
77
  }
49
78
 
@@ -71,7 +100,6 @@ class PluginManager {
71
100
 
72
101
  /**
73
102
  * 获取所有已知插件信息(包括已加载和未加载的)
74
- * @returns {Array<{name: string, status: string, enabled: boolean, version?: string, system?: boolean}>}
75
103
  */
76
104
  getAllKnown() {
77
105
  const result = [];
@@ -81,7 +109,7 @@ class PluginManager {
81
109
  const inst = entry.instance;
82
110
  result.push({
83
111
  name,
84
- status: 'loaded',
112
+ status: entry.status || 'registered',
85
113
  enabled: entry.enabled !== false,
86
114
  version: inst.version,
87
115
  system: inst.system,
@@ -90,6 +118,18 @@ class PluginManager {
90
118
  result.push({ name, status: 'known', enabled: false });
91
119
  }
92
120
  }
121
+ // 加入已加载但不在 knownPlugins 中的
122
+ for (const [name, entry] of this._plugins) {
123
+ if (!result.find(p => p.name === name)) {
124
+ result.push({
125
+ name,
126
+ status: entry.status || 'registered',
127
+ enabled: entry.enabled !== false,
128
+ version: entry.instance?.version,
129
+ system: entry.instance?.system || false,
130
+ });
131
+ }
132
+ }
93
133
  return result;
94
134
  }
95
135
 
@@ -97,31 +137,71 @@ class PluginManager {
97
137
  return this._plugins.size;
98
138
  }
99
139
 
100
- async load(plugin) {
101
- const name = plugin.name;
140
+ /**
141
+ * 加载插件
142
+ * @param {Plugin|Object} plugin - 插件实例或类
143
+ * @param {Object} [options] - 加载选项
144
+ * @param {boolean} [options.forceEnabled] - 强制启用,忽略 state 文件的 disabled
145
+ */
146
+ async load(plugin, options = {}) {
147
+ let pluginInstance = plugin;
148
+
149
+ // 如果是类(Plugin 子类),实例化
150
+ if (typeof plugin === 'function' && plugin.prototype instanceof Plugin) {
151
+ pluginInstance = new plugin();
152
+ }
153
+
154
+ const name = pluginInstance.name;
102
155
 
103
- if (this._plugins.has(name)) {
156
+ // 已存在且已加载 → 跳过
157
+ const existing = this._plugins.get(name);
158
+ if (existing && existing.status === 'loaded' && pluginInstance._started) {
104
159
  this._log.debug(`Plugin '${name}' already loaded, skipping`);
105
160
  return;
106
161
  }
107
162
 
163
+ // 已注册但未加载 → 检查 enabled 状态
164
+ if (existing) {
165
+ if (!existing.enabled && !options.forceEnabled) {
166
+ this._log.info(`Plugin '${name}' is disabled, skipping load`);
167
+ return;
168
+ }
169
+ pluginInstance = existing.instance;
170
+ } else {
171
+ // 未注册 → 先注册
172
+ this.register(pluginInstance, options);
173
+ }
174
+
175
+ const entry = this._plugins.get(name);
176
+ if (!entry.enabled && !options.forceEnabled) {
177
+ this._log.info(`Plugin '${name}' is disabled, skipping load`);
178
+ return;
179
+ }
180
+
181
+ // 如果 forceEnabled,覆盖 enabled 状态
182
+ if (options.forceEnabled) {
183
+ entry.enabled = true;
184
+ pluginInstance.enabled = true;
185
+ }
186
+
108
187
  this._log.info(`Loading plugin: ${name}`);
109
188
 
110
- if (typeof plugin.install === 'function') {
111
- plugin.install(this.framework);
189
+ if (typeof pluginInstance.install === 'function') {
190
+ pluginInstance.install(this.framework);
112
191
  }
113
192
 
114
- this._plugins.set(name, {
115
- instance: plugin,
116
- sourcePath: plugin.sourcePath || null,
117
- enabled: plugin.enabled !== false,
118
- });
119
- this._knownPlugins.add(name);
193
+ entry.status = 'loaded';
120
194
 
121
- if (typeof plugin.start === 'function') {
122
- await plugin.start(this.framework);
195
+ // bootstrapping 模式下不 start,由 startAll() 统一启动
196
+ if (!this._bootstrapping) {
197
+ if (typeof pluginInstance.start === 'function') {
198
+ await pluginInstance.start(this.framework);
199
+ pluginInstance._started = true;
200
+ }
123
201
  }
124
202
 
203
+ this.framework?.emit('plugin:loaded', pluginInstance);
204
+ this._saveState();
125
205
  this._log.info(`Plugin loaded: ${name}`);
126
206
  }
127
207
 
@@ -139,7 +219,7 @@ class PluginManager {
139
219
  }
140
220
 
141
221
  this._plugins.delete(name);
142
- this._log.info(`Plugin unloaded: ${name}`);
222
+ return true;
143
223
  }
144
224
 
145
225
  async unloadExcept(keepSet) {
@@ -169,7 +249,6 @@ class PluginManager {
169
249
  if (typeof plugin.reload === 'function') {
170
250
  await plugin.reload(this.framework);
171
251
  } else {
172
- // Default reload: uninstall + install + start
173
252
  if (typeof plugin.uninstall === 'function') {
174
253
  plugin.uninstall(this.framework);
175
254
  }
@@ -181,50 +260,145 @@ class PluginManager {
181
260
  }
182
261
  }
183
262
 
263
+ this.framework?.emit('plugin:reloaded', plugin);
184
264
  this._log.info(`Plugin reloaded: ${name}`);
185
265
  }
186
266
 
267
+ /**
268
+ * 启动所有已加载、已启用、未启动的插件(按优先级排序)
269
+ */
187
270
  async startAll() {
188
- for (const [name, entry] of this._plugins) {
271
+ const entries = Array.from(this._plugins.values())
272
+ .filter(e => e.status === 'loaded' && e.enabled)
273
+ .sort((a, b) => (a.instance.priority || 100) - (b.instance.priority || 100));
274
+
275
+ for (const entry of entries) {
189
276
  const plugin = entry.instance;
277
+ if (plugin._started) continue;
190
278
  if (typeof plugin.start === 'function') {
191
279
  try {
192
280
  await plugin.start(this.framework);
281
+ plugin._started = true;
193
282
  } catch (err) {
194
- this._log.warn(`startAll: ${name} failed: ${err.message}`);
283
+ this._log.warn(`startAll: ${plugin.name} failed: ${err.message}`);
195
284
  }
196
285
  }
197
286
  }
198
287
  }
199
288
 
200
- async reloadAll() {
201
- const names = this.getNames();
202
- for (const name of names) {
289
+ /**
290
+ * 启用插件
291
+ */
292
+ async enable(name) {
293
+ const entry = this._plugins.get(name);
294
+ if (!entry) {
295
+ throw new PluginNotFoundError(name);
296
+ }
297
+ if (entry.instance?.system) {
298
+ throw new PluginError(`Plugin '${name}' is a system plugin, cannot be disabled/enabled`);
299
+ }
300
+ if (entry.enabled) {
301
+ this._log.info(`Plugin '${name}' already enabled`);
302
+ return;
303
+ }
304
+
305
+ entry.enabled = true;
306
+ if (entry.instance) {
307
+ entry.instance.enabled = true;
308
+ }
309
+
310
+ // 如果已加载,重新启动
311
+ if (entry.status === 'loaded') {
203
312
  try {
204
- await this.reload(name);
313
+ // 停止旧实例
314
+ if (entry.instance._started) {
315
+ if (typeof entry.instance.stopBot === 'function') {
316
+ await entry.instance.stopBot();
317
+ } else if (typeof entry.instance.stop === 'function') {
318
+ await entry.instance.stop();
319
+ }
320
+ }
321
+
322
+ // 如果记录过 sourcePath,重新 require 最新代码
323
+ if (entry.sourcePath) {
324
+ delete require.cache[require.resolve(entry.sourcePath)];
325
+ const mod = require(entry.sourcePath);
326
+ let NewClass = mod.default || mod;
327
+ if (typeof NewClass === 'function' && NewClass.prototype instanceof Plugin) {
328
+ const newInstance = new NewClass();
329
+ newInstance._sourcePath = entry.sourcePath;
330
+ if (entry.instance.config) newInstance.config = { ...entry.instance.config };
331
+ entry.instance = newInstance;
332
+ await newInstance.install(this.framework);
333
+ await newInstance.start(this.framework);
334
+ newInstance._started = true;
335
+ this.framework?.emit('plugin:reloaded', newInstance);
336
+ }
337
+ } else {
338
+ // 没有 sourcePath,调用 reload
339
+ if (typeof entry.instance.reload === 'function') {
340
+ await entry.instance.reload(this.framework);
341
+ }
342
+ if (typeof entry.instance.start === 'function') {
343
+ await entry.instance.start(this.framework);
344
+ }
345
+ entry.instance._started = true;
346
+ }
205
347
  } catch (err) {
206
- this._log.warn(`Failed to reload '${name}': ${err.message}`);
348
+ this._log.error(`Enable/start failed for '${name}':`, err.message);
349
+ }
350
+ } else if (entry.status === 'registered') {
351
+ // 只注册过但未加载,现在加载
352
+ try {
353
+ await this.load(entry.instance);
354
+ } catch (err) {
355
+ this._log.error(`Enable/load failed for '${name}':`, err.message);
207
356
  }
208
357
  }
209
- }
210
358
 
211
- async enable(name) {
212
- const entry = this._plugins.get(name);
213
- if (entry) {
214
- entry.enabled = true;
215
- this._log.info(`Plugin enabled: ${name}`);
216
- }
359
+ this.framework?.emit('plugin:enabled', entry.instance);
360
+ this._saveState();
361
+ this._log.info(`Plugin '${name}' enabled`);
217
362
  }
218
363
 
364
+ /**
365
+ * 禁用插件
366
+ */
219
367
  async disable(name) {
220
368
  const entry = this._plugins.get(name);
221
- if (entry) {
222
- if (entry.instance.system) {
223
- throw new PluginError(`Cannot disable system plugin '${name}'`);
369
+ if (!entry) {
370
+ throw new PluginNotFoundError(name);
371
+ }
372
+ if (entry.instance?.system) {
373
+ throw new PluginError(`Plugin '${name}' is a system plugin, cannot be disabled`);
374
+ }
375
+ if (!entry.enabled) {
376
+ this._log.info(`Plugin '${name}' already disabled`);
377
+ return;
378
+ }
379
+
380
+ entry.enabled = false;
381
+ if (entry.instance) {
382
+ entry.instance.enabled = false;
383
+ }
384
+
385
+ // 如果正在运行,停止
386
+ if (entry.instance._started) {
387
+ try {
388
+ if (typeof entry.instance.stopBot === 'function') {
389
+ await entry.instance.stopBot();
390
+ } else if (typeof entry.instance.stop === 'function') {
391
+ await entry.instance.stop();
392
+ }
393
+ entry.instance._started = false;
394
+ } catch (err) {
395
+ this._log.error(`Stop failed for '${name}':`, err.message);
224
396
  }
225
- entry.enabled = false;
226
- this._log.info(`Plugin disabled: ${name}`);
227
397
  }
398
+
399
+ this.framework?.emit('plugin:disabled', entry.instance);
400
+ this._saveState();
401
+ this._log.info(`Plugin '${name}' disabled`);
228
402
  }
229
403
 
230
404
  updatePluginConfig(name, config) {
@@ -235,26 +409,75 @@ class PluginManager {
235
409
  if (entry.instance.config) {
236
410
  Object.assign(entry.instance.config, config);
237
411
  }
412
+ this._saveState();
238
413
  }
239
414
 
240
- registerEventDescription(eventType, description, schema = null) {
241
- this._eventDescriptions.set(eventType, { description, schema });
415
+ setBootstrapping(val) {
416
+ this._bootstrapping = val !== false;
242
417
  }
243
418
 
244
- getEventDescriptions() {
245
- return new Map(this._eventDescriptions);
419
+ // ========== 状态持久化 ==========
420
+
421
+ _getStateFile() {
422
+ const dir = path.dirname(this._stateFile);
423
+ if (!fs.existsSync(dir)) {
424
+ fs.mkdirSync(dir, { recursive: true });
425
+ }
426
+ return this._stateFile;
246
427
  }
247
428
 
248
- getEventDescription(eventType) {
249
- return this._eventDescriptions.get(eventType) || null;
429
+ _saveState() {
430
+ try {
431
+ const stateFile = this._getStateFile();
432
+ let state = safeJsonParse(
433
+ fs.existsSync(stateFile) ? fs.readFileSync(stateFile, 'utf-8') : '{}',
434
+ {}
435
+ );
436
+ for (const [name, entry] of this._plugins) {
437
+ state[name] = {
438
+ enabled: entry.enabled,
439
+ config: entry.instance?.config || {},
440
+ };
441
+ }
442
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
443
+ this._stateCache = state;
444
+ } catch (err) {
445
+ this._log.error('Failed to save state:', err.message);
446
+ }
250
447
  }
251
448
 
252
- setBootstrapping(val) {
253
- this._bootstrapping = val !== false;
449
+ _loadState() {
450
+ if (this._stateCache !== null) return this._stateCache;
451
+ try {
452
+ const stateFile = this._getStateFile();
453
+ if (fs.existsSync(stateFile)) {
454
+ this._stateCache = safeJsonParse(fs.readFileSync(stateFile, 'utf-8'), {});
455
+ return this._stateCache;
456
+ }
457
+ } catch (err) {
458
+ this._log.error('Failed to load state:', err.message);
459
+ }
460
+ this._stateCache = {};
461
+ return this._stateCache;
254
462
  }
255
463
 
256
464
  _setStateFile(cwd) {
257
465
  this._stateFile = path.join(cwd, '.foliko', 'data', 'plugins-state.json');
466
+ this._stateCache = null;
467
+ }
468
+
469
+ // ========== 事件描述 ==========
470
+
471
+ registerEventDescription(eventType, description, schema = null) {
472
+ this._eventDescriptions.set(eventType, { description, schema });
473
+ }
474
+
475
+ getEventDescriptions() {
476
+ return new Map(this._eventDescriptions);
477
+ }
478
+
479
+ getEventDescription(eventType) {
480
+ return this._eventDescriptions.get(eventType) || null;
258
481
  }
259
482
  }
260
483