foliko 1.0.7 → 1.0.9

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.
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  const { Plugin } = require('./plugin-base')
7
+ const fs = require('fs')
8
+ const path = require('path')
7
9
 
8
10
  class PluginManager {
9
11
  /**
@@ -13,6 +15,53 @@ class PluginManager {
13
15
  this.framework = framework
14
16
  this._plugins = new Map()
15
17
  this._loading = false
18
+ this._stateFile = path.join(process.cwd(), '.agent', 'data', 'plugins-state.json')
19
+ }
20
+
21
+ /**
22
+ * 获取状态文件路径
23
+ */
24
+ _getStateFile() {
25
+ const dir = path.dirname(this._stateFile)
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true })
28
+ }
29
+ return this._stateFile
30
+ }
31
+
32
+ /**
33
+ * 保存插件状态到文件
34
+ */
35
+ _saveState() {
36
+ try {
37
+ const state = {}
38
+ for (const [name, entry] of this._plugins) {
39
+ state[name] = {
40
+ enabled: entry.enabled,
41
+ config: entry.instance?.config || {}
42
+ }
43
+ }
44
+ fs.writeFileSync(this._getStateFile(), JSON.stringify(state, null, 2))
45
+ } catch (err) {
46
+ console.error('[PluginManager] Failed to save state:', err.message)
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 加载插件状态从文件
52
+ */
53
+ _loadState() {
54
+ try {
55
+ const stateFile = this._getStateFile()
56
+ if (fs.existsSync(stateFile)) {
57
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'))
58
+ console.log('[PluginManager] Loaded plugin state from file')
59
+ return state
60
+ }
61
+ } catch (err) {
62
+ console.error('[PluginManager] Failed to load state:', err.message)
63
+ }
64
+ return {}
16
65
  }
17
66
 
18
67
  /**
@@ -30,9 +79,20 @@ class PluginManager {
30
79
  pluginInstance = this._createFromObject(plugin)
31
80
  }
32
81
 
82
+ // 加载保存的状态
83
+ const savedState = this._loadState()
84
+ const savedEnabled = savedState[pluginInstance.name]?.enabled
85
+ const savedConfig = savedState[pluginInstance.name]?.config
86
+
87
+ // 恢复保存的配置到插件实例
88
+ if (savedConfig && pluginInstance.config) {
89
+ pluginInstance.config = { ...pluginInstance.config, ...savedConfig }
90
+ }
91
+
33
92
  this._plugins.set(pluginInstance.name, {
34
93
  instance: pluginInstance,
35
- status: 'registered'
94
+ status: 'registered',
95
+ enabled: savedEnabled !== undefined ? savedEnabled : true // 使用保存的状态,默认启用
36
96
  })
37
97
 
38
98
  this.framework.emit('plugin:registered', pluginInstance)
@@ -61,6 +121,12 @@ class PluginManager {
61
121
  if (existing) {
62
122
  pluginInstance = existing.instance
63
123
 
124
+ // 如果插件被禁用,跳过加载
125
+ if (!existing.enabled) {
126
+ console.log(`[PluginManager] Plugin '${pluginInstance.name}' is disabled`)
127
+ return pluginInstance
128
+ }
129
+
64
130
  // 如果已加载且已启动,直接返回
65
131
  if (existing.status === 'loaded' && pluginInstance._started) {
66
132
  console.warn(`[PluginManager] Plugin '${pluginInstance.name}' already loaded`)
@@ -191,28 +257,108 @@ class PluginManager {
191
257
  await this.startAll()
192
258
  }
193
259
 
260
+ /**
261
+ * 解析插件路径
262
+ * 支持两种结构:
263
+ * 1. 文件夹结构: .agent/plugins/my-plugin/index.js
264
+ * 2. 单文件结构: .agent/plugins/my-plugin.js
265
+ * @param {string} pluginsDir - 插件目录
266
+ * @param {string} name - 插件名称
267
+ * @returns {{path: string, type: 'folder'|'file'}|null} 插件路径和类型
268
+ * @private
269
+ */
270
+ _resolvePluginPath(pluginsDir, name) {
271
+ const folderPath = path.join(pluginsDir, name)
272
+ const filePath = path.join(pluginsDir, `${name}.js`)
273
+
274
+ // 文件夹优先
275
+ if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) {
276
+ const pkgPath = path.join(folderPath, 'package.json')
277
+ if (fs.existsSync(pkgPath)) {
278
+ try {
279
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
280
+ const main = pkg.main || 'index.js'
281
+ const mainPath = path.join(folderPath, main)
282
+ if (fs.existsSync(mainPath)) {
283
+ return { path: mainPath, type: 'folder' }
284
+ }
285
+ } catch (err) {
286
+ console.warn(`[_resolvePluginPath] Failed to parse package.json for ${name}:`, err.message)
287
+ }
288
+ }
289
+ // 默认加载 index.js
290
+ const indexPath = path.join(folderPath, 'index.js')
291
+ if (fs.existsSync(indexPath)) {
292
+ return { path: indexPath, type: 'folder' }
293
+ }
294
+ console.warn(`[_resolvePluginPath] No entry point found for plugin folder: ${name}`)
295
+ return null
296
+ }
297
+
298
+ // 单文件回退
299
+ if (fs.existsSync(filePath)) {
300
+ return { path: filePath, type: 'file' }
301
+ }
302
+
303
+ return null
304
+ }
305
+
306
+ /**
307
+ * 扫描插件目录,返回所有插件名称
308
+ * @param {string} pluginsDir - 插件目录
309
+ * @returns {string[]} 插件名称列表
310
+ * @private
311
+ */
312
+ _scanPluginNames(pluginsDir) {
313
+ if (!fs.existsSync(pluginsDir)) {
314
+ return []
315
+ }
316
+
317
+ const names = new Set()
318
+ const entries = fs.readdirSync(pluginsDir, { withFileTypes: true })
319
+
320
+ for (const entry of entries) {
321
+ if (entry.isDirectory()) {
322
+ // 文件夹插件
323
+ names.add(entry.name)
324
+ } else if (entry.isFile() && entry.name.endsWith('.js')) {
325
+ // 单文件插件(排除与文件夹同名的)
326
+ const baseName = entry.name.replace(/\.js$/, '')
327
+ if (!names.has(baseName)) {
328
+ names.add(baseName)
329
+ }
330
+ }
331
+ }
332
+
333
+ return Array.from(names)
334
+ }
335
+
194
336
  /**
195
337
  * 发现并加载自定义插件
196
338
  * @private
197
339
  */
198
340
  async _discoverCustomPlugins() {
199
- const fs = require('fs')
200
- const path = require('path')
201
-
202
341
  const pluginsDir = path.resolve(process.cwd(), '.agent', 'plugins')
203
342
  if (!fs.existsSync(pluginsDir)) {
204
343
  return
205
344
  }
206
345
 
207
- const files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'))
346
+ // 扫描所有插件名称(支持文件夹和单文件)
347
+ const pluginNames = this._scanPluginNames(pluginsDir)
208
348
 
209
349
  // 从 pluginsDir 推导 agentDir(pluginsDir = <agentDir>/plugins)
210
350
  const agentDir = path.dirname(pluginsDir)
211
351
  const agentNodeModules = path.join(agentDir, 'node_modules')
212
352
 
213
- for (const file of files) {
353
+ for (const pluginName of pluginNames) {
214
354
  try {
215
- const pluginPath = path.join(pluginsDir, file)
355
+ const resolved = this._resolvePluginPath(pluginsDir, pluginName)
356
+ if (!resolved) {
357
+ console.warn(`[PluginManager] Cannot resolve plugin: ${pluginName}`)
358
+ continue
359
+ }
360
+
361
+ const { path: pluginPath, type } = resolved
216
362
 
217
363
  // 添加模块路径到搜索路径(优先级从高到低)
218
364
  const modulePathsToAdd = [
@@ -239,24 +385,24 @@ class PluginManager {
239
385
  }
240
386
 
241
387
  // 获取插件名称
242
- let pluginName
388
+ let resolvedPluginName
243
389
  try {
244
390
  const tempPlugin = plugin.prototype instanceof require('./plugin-base')
245
391
  ? new plugin() : (typeof plugin === 'function' ? plugin() : plugin)
246
- pluginName = tempPlugin.name || file.replace('.js', '')
392
+ resolvedPluginName = tempPlugin.name || pluginName
247
393
  } catch {
248
- pluginName = file.replace('.js', '')
394
+ resolvedPluginName = pluginName
249
395
  }
250
396
 
251
397
  // 如果插件已加载且已启动,跳过
252
- if (this.has(pluginName) && this.get(pluginName)?._started) {
398
+ if (this.has(resolvedPluginName) && this.get(resolvedPluginName)?._started) {
253
399
  continue
254
400
  }
255
401
 
256
- console.log(`[PluginManager] Loading new plugin: ${file}`)
402
+ console.log(`[PluginManager] Loading new plugin: ${pluginName} (${type})`)
257
403
  await this.load(plugin)
258
404
  } catch (err) {
259
- console.error(`[PluginManager] Failed to load plugin ${file}:`, err.message)
405
+ console.error(`[PluginManager] Failed to load plugin ${pluginName}:`, err.message)
260
406
  }
261
407
  }
262
408
  }
@@ -270,11 +416,11 @@ class PluginManager {
270
416
  }
271
417
 
272
418
  /**
273
- * 获取所有已加载插件
419
+ * 获取所有已加载且已启用的插件
274
420
  */
275
421
  getAll() {
276
422
  return Array.from(this._plugins.values())
277
- .filter(e => e.status === 'loaded')
423
+ .filter(e => e.status === 'loaded' && e.enabled)
278
424
  .map(e => ({ name: e.instance.name, instance: e.instance }))
279
425
  }
280
426
 
@@ -294,12 +440,122 @@ class PluginManager {
294
440
  return this._plugins.get(name)?.status === 'loaded'
295
441
  }
296
442
 
443
+ /**
444
+ * 检查插件是否启用
445
+ * @param {string} name - 插件名称
446
+ */
447
+ isEnabled(name) {
448
+ return this._plugins.get(name)?.enabled === true
449
+ }
450
+
451
+ /**
452
+ * 启用插件
453
+ * @param {string} name - 插件名称
454
+ */
455
+ async enable(name) {
456
+ const entry = this._plugins.get(name)
457
+ if (!entry) {
458
+ throw new Error(`Plugin '${name}' not found`)
459
+ }
460
+
461
+ if (entry.enabled) {
462
+ console.log(`[PluginManager] Plugin '${name}' already enabled`)
463
+ return
464
+ }
465
+
466
+ entry.enabled = true
467
+
468
+ // 如果插件已加载,尝试重新启动(reload 会调用 start)
469
+ if (entry.status === 'loaded') {
470
+ try {
471
+ // 如果之前已经启动过,先调用 stop 停止旧实例
472
+ if (entry.instance._started) {
473
+ if (typeof entry.instance.stop === 'function') {
474
+ await entry.instance.stop()
475
+ } else if (typeof entry.instance.stopBot === 'function') {
476
+ await entry.instance.stopBot()
477
+ }
478
+ }
479
+ // 调用 reload 让插件重新初始化(会调用 install 和 start)
480
+ await this.reload(name)
481
+ } catch (err) {
482
+ console.error(`[PluginManager] Enable/reload failed for '${name}':`, err.message)
483
+ }
484
+ }
485
+
486
+ this.framework.emit('plugin:enabled', entry.instance)
487
+ this._saveState()
488
+ console.log(`[PluginManager] Plugin '${name}' enabled`)
489
+ }
490
+
491
+ /**
492
+ * 禁用插件
493
+ * @param {string} name - 插件名称
494
+ */
495
+ async disable(name) {
496
+ const entry = this._plugins.get(name)
497
+ if (!entry) {
498
+ throw new Error(`Plugin '${name}' not found`)
499
+ }
500
+
501
+ if (!entry.enabled) {
502
+ console.log(`[PluginManager] Plugin '${name}' already disabled`)
503
+ return
504
+ }
505
+
506
+ entry.enabled = false
507
+
508
+ // 如果插件正在运行,停止它
509
+ if (entry.instance._started) {
510
+ try {
511
+ // 优先调用 stop 方法,其次调用 stopBot 方法
512
+ if (typeof entry.instance.stop === 'function') {
513
+ await entry.instance.stop()
514
+ } else if (typeof entry.instance.stopBot === 'function') {
515
+ await entry.instance.stopBot()
516
+ }
517
+ entry.instance._started = false
518
+ } catch (err) {
519
+ console.error(`[PluginManager] Stop failed for '${name}':`, err.message)
520
+ }
521
+ }
522
+
523
+ this.framework.emit('plugin:disabled', entry.instance)
524
+ this._saveState()
525
+ console.log(`[PluginManager] Plugin '${name}' disabled`)
526
+ }
527
+
528
+ /**
529
+ * 更新插件配置
530
+ * @param {string} name - 插件名称
531
+ * @param {Object} config - 新配置(会合并到现有配置)
532
+ */
533
+ updatePluginConfig(name, config) {
534
+ const entry = this._plugins.get(name)
535
+ if (!entry) {
536
+ throw new Error(`Plugin '${name}' not found`)
537
+ }
538
+
539
+ if (!entry.instance.config) {
540
+ entry.instance.config = {}
541
+ }
542
+
543
+ // 合并配置
544
+ entry.instance.config = { ...entry.instance.config, ...config }
545
+
546
+ // 保存状态
547
+ this._saveState()
548
+ console.log(`[PluginManager] Plugin '${name}' config updated`)
549
+
550
+ return entry.instance.config
551
+ }
552
+
297
553
  /**
298
554
  * 启动所有已加载但未启动的插件(按优先级排序)
299
555
  */
300
556
  async startAll() {
301
557
  const entries = Array.from(this._plugins.values())
302
- .filter(e => e.status === 'loaded')
558
+ .filter(e => e.status === 'loaded' && e.enabled) // 只启动已启用且已加载的插件
303
559
  .sort((a, b) => (a.instance.priority || 100) - (b.instance.priority || 100))
304
560
 
305
561
  for (const entry of entries) {