foliko 1.0.8 → 1.0.10

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.
@@ -18,14 +18,26 @@ VB-Agent 是一个基于插件的 Agent 框架,核心简单,通过插件扩
18
18
 
19
19
  用户自定义插件放在项目根目录的 `.agent/plugins/` 下,**自动加载**,无需手动注册。
20
20
 
21
+ **推荐使用文件夹结构**(适合复杂插件):
22
+
21
23
  ```
22
24
  项目目录/
23
25
  └── .agent/
24
26
  └── plugins/
25
- ├── my-plugin.js 正确
26
- └── another-plugin.js 正确
27
+ ├── my-plugin/ 文件夹结构(推荐)
28
+ │ ├── package.json # 可选,main 字段指定入口
29
+ │ ├── index.js # 默认入口
30
+ │ └── node_modules/ # 可选,插件私有依赖
31
+ ├── another-plugin/ ✅ 另一个文件夹插件
32
+ │ └── index.js
33
+ └── legacy.js ✅ 单文件仍支持
27
34
  ```
28
35
 
36
+ **文件夹结构的优势:**
37
+ - 支持 `package.json` 的 `main` 字段自定义入口
38
+ - 支持 `node_modules` 存放插件私有依赖
39
+ - 更适合复杂插件的代码组织
40
+
29
41
  ### `plugins/` 目录(内置插件)
30
42
 
31
43
  框架内置插件位于 `plugins/` 目录。
@@ -36,6 +48,69 @@ VB-Agent 是一个基于插件的 Agent 框架,核心简单,通过插件扩
36
48
 
37
49
  **必须使用免引入写法**:
38
50
 
51
+ ### 文件夹结构(推荐)
52
+
53
+ ```
54
+ .agent/plugins/my-plugin/
55
+ ├── package.json # 可选
56
+ └── index.js # 入口
57
+ ```
58
+
59
+ ```json
60
+ // package.json 示例
61
+ {
62
+ "name": "my-plugin",
63
+ "main": "index.js"
64
+ }
65
+ ```
66
+
67
+ ```javascript
68
+ // .agent/plugins/my-plugin/index.js
69
+ module.exports = function(Plugin) {
70
+ return class MyPlugin extends Plugin {
71
+ constructor(config = {}) {
72
+ super()
73
+ this.name = 'my-plugin'
74
+ this.version = '1.0.0'
75
+ this.description = '我的工具插件'
76
+ this.priority = 10
77
+ }
78
+
79
+ install(framework) {
80
+ const { z } = require('zod')
81
+ framework.registerTool({
82
+ name: 'my_tool',
83
+ description: '我的工具',
84
+ inputSchema: z.object({
85
+ param: z.string().describe('参数描述')
86
+ }),
87
+ execute: async (args, framework) => {
88
+ return { success: true, result: args.param }
89
+ }
90
+ })
91
+ return this
92
+ }
93
+
94
+ uninstall(framework) {
95
+ // 清理资源
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### 单文件结构(仍支持)
102
+
103
+ ```javascript
104
+ // .agent/plugins/my-plugin.js
105
+ module.exports = function(Plugin) {
106
+ return class MyPlugin extends Plugin {
107
+ // ... 与上面相同
108
+ }
109
+ }
110
+ ```
111
+
112
+ **注意**:如果文件夹和同名 `.js` 文件同时存在,**文件夹优先**。
113
+
39
114
  ```javascript
40
115
  // .agent/plugins/my-plugin.js
41
116
  module.exports = function(Plugin) {
@@ -70,8 +145,10 @@ module.exports = function(Plugin) {
70
145
  }
71
146
  ```
72
147
  **插件开发流程:**
73
- 1. **创建插件文件**(暂不加载)
74
- 2. **安装依赖**:`install { package: "包名" }`
148
+ 1. **创建插件**(优先使用文件夹结构)
149
+ 2. **安装依赖**:
150
+ - 文件夹插件:`install { package: "包名", path: ".agent/plugins/插件名" }`
151
+ - 单文件插件:`install { package: "包名" }`
75
152
  3. **热重载**:`reload_plugins`
76
153
  4. **检查加载状态**
77
154
 
@@ -85,27 +162,69 @@ module.exports = function(Plugin) {
85
162
 
86
163
  ## 依赖管理
87
164
 
88
- 当插件需要使用第三方 npm 包(如 `zod`、`axios` 等)时,必须自动安装到 `.agent/node_modules`:
165
+ 当插件需要使用第三方 npm 包(如 `zod`、`axios` 等)时,根据插件类型选择安装位置:
166
+
167
+ | 插件类型 | 安装位置 | 说明 |
168
+ |----------|----------|------|
169
+ | **文件夹插件** | `.agent/plugins/插件名/node_modules/` | 插件自包含,可独立迁移 |
170
+ | **单文件插件** | `.agent/node_modules/` | 共享依赖 |
89
171
 
90
172
  ### 自动安装工具
91
173
 
92
- 框架提供了 `install` 工具,可以自动安装 npm 包:
174
+ 框架提供了 `install` 工具,支持以下用法:
93
175
 
94
176
  ```javascript
95
- // execute 中调用 install 工具
96
- execute: async (args, framework) => {
97
- // 安装缺失的包
98
- const installResult = await framework.callTool('install', {
99
- package: 'zod' // 或 'zod@4.3.6' 指定版本
100
- })
177
+ // 安装到默认位置 .agent/node_modules
178
+ install({ package: "zod" })
101
179
 
102
- if (!installResult.success) {
103
- return { success: false, error: installResult.error }
104
- }
180
+ // 安装到指定目录(用于文件夹插件)
181
+ install({ package: "axios", path: ".agent/plugins/my-plugin" })
105
182
 
106
- // 安装成功后可以正常 require
107
- const { z } = require('zod')
108
- // ... 继续执行
183
+ // package.json 安装所有依赖到默认位置
184
+ install({ file: "./package.json" })
185
+
186
+ // 从 package.json 安装所有依赖到指定目录
187
+ install({ file: "./my-plugin/package.json", path: ".agent/plugins/my-plugin" })
188
+
189
+ // 仅指定路径,安装该目录下的 package.json 依赖
190
+ install({ path: ".agent/plugins/my-plugin" })
191
+ ```
192
+
193
+ ### 文件夹插件的依赖安装示例
194
+
195
+ ```javascript
196
+ // .agent/plugins/my-plugin/index.js
197
+ module.exports = function(Plugin) {
198
+ return class MyPlugin extends Plugin {
199
+ constructor(config = {}) {
200
+ super()
201
+ this.name = 'my-plugin'
202
+ }
203
+
204
+ install(framework) {
205
+ framework.registerTool({
206
+ name: 'fetch_data',
207
+ description: '获取远程数据',
208
+ inputSchema: z.object({
209
+ url: z.string().describe('API URL')
210
+ }),
211
+ execute: async (args, framework) => {
212
+ // 安装到插件自己的目录
213
+ const pluginDir = '.agent/plugins/my-plugin'
214
+ await framework.callTool('install', {
215
+ package: 'axios',
216
+ path: pluginDir
217
+ })
218
+
219
+ // 引入插件目录下的 node_modules
220
+ const axios = require(`${pluginDir}/node_modules/axios`)
221
+ const response = await axios.get(args.url)
222
+ return { success: true, data: response.data }
223
+ }
224
+ })
225
+ return this
226
+ }
227
+ }
109
228
  }
110
229
  ```
111
230
 
@@ -113,15 +232,18 @@ execute: async (args, framework) => {
113
232
 
114
233
  当 LLM 检测到 `Cannot find module 'xxx'` 错误时,必须:
115
234
 
116
- 1. **立即调用 `install` 工具** 安装缺失的包
117
- 2. **安装完成后** 重新加载插件
118
- 3. **返回状态** 给用户
235
+ 1. **确定插件目录**:如果插件是文件夹结构,安装到插件目录
236
+ 2. **调用 `install` 工具** 安装缺失的包
237
+ 3. **安装完成后** 重新加载插件
238
+ 4. **返回状态** 给用户
119
239
 
120
- 示例流程:
240
+ 示例流程(文件夹插件):
121
241
  ```
122
242
  错误: Cannot find module 'axios'
123
243
 
124
- 自动安装: install { package: "axios" }
244
+ 确定插件目录: .agent/plugins/my-plugin
245
+
246
+ 自动安装: install { package: "axios", path: ".agent/plugins/my-plugin" }
125
247
 
126
248
  重新加载插件
127
249
 
@@ -130,7 +252,8 @@ execute: async (args, framework) => {
130
252
 
131
253
  ### 重要提醒
132
254
 
133
- - **不要尝试手动 require 全局安装的包** - 必须在 `.agent/node_modules` 中安装
255
+ - **文件夹插件优先安装到插件目录** - 使用 `path` 参数指定插件目录
256
+ - **不要尝试手动 require 全局安装的包** - 必须在指定目录中安装
134
257
  - **使用 `install` 工具自动处理** - 让 LLM 知道如何处理缺失依赖
135
258
  - **插件加载失败时检查依赖** - 优先安装缺失的包再重试
136
259
 
@@ -223,10 +346,11 @@ install(framework) {
223
346
 
224
347
  | 规则 | 说明 |
225
348
  |------|------|
226
- | **禁止创建目录!** | **必须是单个 `.js` 文件!** |
349
+ | **优先使用文件夹结构** | 复杂插件推荐用文件夹,便于管理依赖和代码 |
227
350
  | **必须用 inputSchema** | `inputSchema: z.object({})`(不是 parameters!) |
228
351
  | **必须用 zod 定义参数** | `param: z.string().describe('描述')` |
229
352
  | **install 必须返回 this** | 确保链式调用 |
353
+ | **文件夹与文件重名时** | 文件夹优先,同名 `.js` 文件会被忽略 |
230
354
 
231
355
  ## 开发完成后注意事项
232
356
 
@@ -257,28 +257,108 @@ class PluginManager {
257
257
  await this.startAll()
258
258
  }
259
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
+
260
336
  /**
261
337
  * 发现并加载自定义插件
262
338
  * @private
263
339
  */
264
340
  async _discoverCustomPlugins() {
265
- const fs = require('fs')
266
- const path = require('path')
267
-
268
341
  const pluginsDir = path.resolve(process.cwd(), '.agent', 'plugins')
269
342
  if (!fs.existsSync(pluginsDir)) {
270
343
  return
271
344
  }
272
345
 
273
- const files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'))
346
+ // 扫描所有插件名称(支持文件夹和单文件)
347
+ const pluginNames = this._scanPluginNames(pluginsDir)
274
348
 
275
349
  // 从 pluginsDir 推导 agentDir(pluginsDir = <agentDir>/plugins)
276
350
  const agentDir = path.dirname(pluginsDir)
277
351
  const agentNodeModules = path.join(agentDir, 'node_modules')
278
352
 
279
- for (const file of files) {
353
+ for (const pluginName of pluginNames) {
280
354
  try {
281
- 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
282
362
 
283
363
  // 添加模块路径到搜索路径(优先级从高到低)
284
364
  const modulePathsToAdd = [
@@ -305,24 +385,24 @@ class PluginManager {
305
385
  }
306
386
 
307
387
  // 获取插件名称
308
- let pluginName
388
+ let resolvedPluginName
309
389
  try {
310
390
  const tempPlugin = plugin.prototype instanceof require('./plugin-base')
311
391
  ? new plugin() : (typeof plugin === 'function' ? plugin() : plugin)
312
- pluginName = tempPlugin.name || file.replace('.js', '')
392
+ resolvedPluginName = tempPlugin.name || pluginName
313
393
  } catch {
314
- pluginName = file.replace('.js', '')
394
+ resolvedPluginName = pluginName
315
395
  }
316
396
 
317
397
  // 如果插件已加载且已启动,跳过
318
- if (this.has(pluginName) && this.get(pluginName)?._started) {
398
+ if (this.has(resolvedPluginName) && this.get(resolvedPluginName)?._started) {
319
399
  continue
320
400
  }
321
401
 
322
- console.log(`[PluginManager] Loading new plugin: ${file}`)
402
+ console.log(`[PluginManager] Loading new plugin: ${pluginName} (${type})`)
323
403
  await this.load(plugin)
324
404
  } catch (err) {
325
- console.error(`[PluginManager] Failed to load plugin ${file}:`, err.message)
405
+ console.error(`[PluginManager] Failed to load plugin ${pluginName}:`, err.message)
326
406
  }
327
407
  }
328
408
  }
@@ -385,15 +465,21 @@ class PluginManager {
385
465
 
386
466
  entry.enabled = true
387
467
 
388
- // 如果插件已加载但未启动,启动它
389
- if (entry.status === 'loaded' && !entry.instance._started) {
468
+ // 如果插件已加载,尝试重新启动(reload 会调用 start)
469
+ if (entry.status === 'loaded') {
390
470
  try {
391
- if (typeof entry.instance.start === 'function') {
392
- await entry.instance.start(this.framework)
393
- entry.instance._started = true
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
+ }
394
478
  }
479
+ // 调用 reload 让插件重新初始化(会调用 install 和 start)
480
+ await this.reload(name)
395
481
  } catch (err) {
396
- console.error(`[PluginManager] Start failed for '${name}':`, err.message)
482
+ console.error(`[PluginManager] Enable/reload failed for '${name}':`, err.message)
397
483
  }
398
484
  }
399
485
 
@@ -428,9 +514,6 @@ class PluginManager {
428
514
  } else if (typeof entry.instance.stopBot === 'function') {
429
515
  await entry.instance.stopBot()
430
516
  }
431
- if (typeof entry.instance.uninstall === 'function') {
432
- await entry.instance.uninstall(this.framework)
433
- }
434
517
  entry.instance._started = false
435
518
  } catch (err) {
436
519
  console.error(`[PluginManager] Stop failed for '${name}':`, err.message)