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.
- package/.claude/settings.local.json +7 -1
- package/.env.example +23 -0
- package/README.md +29 -2
- package/SPEC.md +75 -2
- package/cli/src/ui/chat-ui.js +41 -2
- package/docs/quick-reference.md +30 -4
- package/docs/user-manual.md +158 -3
- package/{test-chat.js → examples/test-chat.js} +2 -2
- package/{test-mcp.js → examples/test-mcp.js} +2 -2
- package/{test-reload.js → examples/test-reload.js} +2 -2
- package/{test-telegram.js → examples/test-telegram.js} +1 -1
- package/{test-tg-bot.js → examples/test-tg-bot.js} +1 -1
- package/{test-tg.js → examples/test-tg.js} +1 -1
- package/{test-think.js → examples/test-think.js} +1 -1
- package/package.json +4 -1
- package/plugins/ai-plugin.js +8 -0
- package/plugins/default-plugins.js +139 -59
- package/plugins/email.js +382 -0
- package/plugins/install-plugin.js +115 -12
- package/plugins/telegram-plugin.js +9 -0
- package/plugins/tools-plugin.js +75 -0
- package/skills/vb-agent-dev/AGENTS.md +81 -10
- package/skills/vb-agent-dev/SKILL.md +149 -25
- package/src/core/framework.js +27 -0
- package/src/core/plugin-manager.js +272 -16
- /package/{test-tg-simple.js → examples/test-tg-simple.js} +0 -0
|
@@ -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
|
-
|
|
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
|
|
353
|
+
for (const pluginName of pluginNames) {
|
|
214
354
|
try {
|
|
215
|
-
const
|
|
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
|
|
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
|
-
|
|
392
|
+
resolvedPluginName = tempPlugin.name || pluginName
|
|
247
393
|
} catch {
|
|
248
|
-
|
|
394
|
+
resolvedPluginName = pluginName
|
|
249
395
|
}
|
|
250
396
|
|
|
251
397
|
// 如果插件已加载且已启动,跳过
|
|
252
|
-
if (this.has(
|
|
398
|
+
if (this.has(resolvedPluginName) && this.get(resolvedPluginName)?._started) {
|
|
253
399
|
continue
|
|
254
400
|
}
|
|
255
401
|
|
|
256
|
-
console.log(`[PluginManager] Loading new plugin: ${
|
|
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 ${
|
|
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) {
|
|
File without changes
|