foliko 1.0.8 → 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 +5 -1
- package/README.md +29 -2
- package/cli/src/ui/chat-ui.js +41 -2
- package/examples/test-chat.js +1 -1
- package/package.json +1 -1
- package/plugins/default-plugins.js +139 -59
- package/plugins/install-plugin.js +115 -12
- package/skills/vb-agent-dev/AGENTS.md +81 -10
- package/skills/vb-agent-dev/SKILL.md +149 -25
- package/src/core/plugin-manager.js +104 -21
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"Bash(cd D:/Code/vb-agent && timeout 8 node test-tg.js 2>&1 || true)",
|
|
31
31
|
"Bash(cd D:/Code/vb-agent && timeout 10 node test-tg.js 2>&1 || true)",
|
|
32
32
|
"Bash(find /d/Code/vb-agent -name \"*email*\" -type f 2>/dev/null | grep -v node_modules | grep -v .git)",
|
|
33
|
-
"Bash(find /d/Code/vb-agent -maxdepth 2 -name \"*.md\" -type f 2>/dev/null | grep -v node_modules)"
|
|
33
|
+
"Bash(find /d/Code/vb-agent -maxdepth 2 -name \"*.md\" -type f 2>/dev/null | grep -v node_modules)",
|
|
34
|
+
"Bash(node -c plugins/default-plugins.js && node -c src/core/plugin-manager.js && echo \"Syntax OK\")",
|
|
35
|
+
"Bash(node -c plugins/install-plugin.js && echo \"Syntax OK\")",
|
|
36
|
+
"Bash(node -c cli/src/ui/chat-ui.js && echo \"Syntax OK\")",
|
|
37
|
+
"Bash(node -c plugins/default-plugins.js && echo \"Syntax OK\")"
|
|
34
38
|
]
|
|
35
39
|
}
|
|
36
40
|
}
|
package/README.md
CHANGED
|
@@ -87,8 +87,27 @@ ai_provider: minimax
|
|
|
87
87
|
|
|
88
88
|
### 用户插件(.agent/plugins/)
|
|
89
89
|
|
|
90
|
+
插件支持两种结构:**文件夹结构**(推荐)和**单文件结构**。
|
|
91
|
+
|
|
92
|
+
#### 文件夹结构(推荐)
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
.agent/plugins/my-plugin/
|
|
96
|
+
├── package.json # 可选,main 字段指定入口
|
|
97
|
+
├── index.js # 默认入口
|
|
98
|
+
└── node_modules/ # 可选,插件私有依赖
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
// package.json 示例
|
|
103
|
+
{
|
|
104
|
+
"name": "my-plugin",
|
|
105
|
+
"main": "index.js"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
90
109
|
```javascript
|
|
91
|
-
// .agent/plugins/my-plugin.js
|
|
110
|
+
// .agent/plugins/my-plugin/index.js
|
|
92
111
|
module.exports = function(Plugin) {
|
|
93
112
|
return class MyPlugin extends Plugin {
|
|
94
113
|
constructor(config = {}) {
|
|
@@ -117,9 +136,17 @@ module.exports = function(Plugin) {
|
|
|
117
136
|
}
|
|
118
137
|
```
|
|
119
138
|
|
|
139
|
+
#### 单文件结构(兼容)
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
.agent/plugins/my-plugin.js
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
如果同时存在文件夹和同名 `.js` 文件,**文件夹优先**。
|
|
146
|
+
|
|
120
147
|
**注意**:如果插件需要第三方库(如 `zod`),需要先安装:
|
|
121
148
|
|
|
122
|
-
1.
|
|
149
|
+
1. 创建插件文件/文件夹
|
|
123
150
|
2. 调用 `install` 工具安装依赖
|
|
124
151
|
3. 热重载插件
|
|
125
152
|
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -22,6 +22,10 @@ class ChatUI extends EventEmitter {
|
|
|
22
22
|
this.lastKeyTime = 0
|
|
23
23
|
this.pasteBuffer = ''
|
|
24
24
|
this.isPasting = false
|
|
25
|
+
|
|
26
|
+
// 多行输入:记录上次 Enter 时间,检测连续两次空回车
|
|
27
|
+
this.lastEnterTime = 0
|
|
28
|
+
this.LINE_ENDING_INTERVAL = 800 // 毫秒内连续两次空回车结束输入
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -73,14 +77,14 @@ class ChatUI extends EventEmitter {
|
|
|
73
77
|
*/
|
|
74
78
|
printWelcome() {
|
|
75
79
|
console.log(`${colored('Foliko', CYAN)} - 持续对话聊天`)
|
|
76
|
-
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('
|
|
80
|
+
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('Enter', DIM)} 换行 | ${colored('双Enter', DIM)} 发送\n`)
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
/**
|
|
80
84
|
* 显示提示符
|
|
81
85
|
*/
|
|
82
86
|
prompt() {
|
|
83
|
-
const prefix = this.isFirstLine ? colored('
|
|
87
|
+
const prefix = this.isFirstLine ? colored('> ', GREEN) : colored('- ', DIM)
|
|
84
88
|
process.stdout.write(`\r${CLEAR_LINE}${prefix}${this.currentLine}`)
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -157,12 +161,46 @@ class ChatUI extends EventEmitter {
|
|
|
157
161
|
* 处理发送
|
|
158
162
|
*/
|
|
159
163
|
async handleSubmit() {
|
|
164
|
+
const now = Date.now()
|
|
160
165
|
const content = this.lines.length > 0
|
|
161
166
|
? [...this.lines, this.currentLine].join('\n')
|
|
162
167
|
: this.currentLine
|
|
163
168
|
|
|
164
169
|
const trimmed = content.trim()
|
|
165
170
|
|
|
171
|
+
// 检测连续两次空回车(多行输入结束)
|
|
172
|
+
if (this.currentLine.trim() === '' && this.lines.length > 0) {
|
|
173
|
+
if (now - this.lastEnterTime < this.LINE_ENDING_INTERVAL) {
|
|
174
|
+
// 第二次空回车,结束输入
|
|
175
|
+
const finalContent = this.lines.join('\n').trim()
|
|
176
|
+
this.resetInput()
|
|
177
|
+
console.log()
|
|
178
|
+
|
|
179
|
+
if (!finalContent) {
|
|
180
|
+
this.prompt()
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await this.sendMessage(finalContent)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.lastEnterTime = now
|
|
189
|
+
|
|
190
|
+
// 输入 !! 立即结束多行输入
|
|
191
|
+
if (this.currentLine.trim() === '!!') {
|
|
192
|
+
const finalContent = this.lines.join('\n').trim()
|
|
193
|
+
this.resetInput()
|
|
194
|
+
console.log()
|
|
195
|
+
|
|
196
|
+
if (finalContent) {
|
|
197
|
+
await this.sendMessage(finalContent)
|
|
198
|
+
} else {
|
|
199
|
+
this.prompt()
|
|
200
|
+
}
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
166
204
|
// 显示空行
|
|
167
205
|
console.log()
|
|
168
206
|
|
|
@@ -199,6 +237,7 @@ class ChatUI extends EventEmitter {
|
|
|
199
237
|
this.pasteId = 0
|
|
200
238
|
this.isPasting = false
|
|
201
239
|
this.pasteBuffer = ''
|
|
240
|
+
this.lastEnterTime = 0
|
|
202
241
|
}
|
|
203
242
|
|
|
204
243
|
/**
|
package/examples/test-chat.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
const lines = []
|
|
53
53
|
|
|
54
54
|
const question = () => {
|
|
55
|
-
rl.question(lines.length === 0 ? '
|
|
55
|
+
rl.question(lines.length === 0 ? '> ' : '- ', (input) => {
|
|
56
56
|
// 输入 !! 立即结束
|
|
57
57
|
if (input.trim() === '!!') {
|
|
58
58
|
const result = lines.join('\n').trim()
|
package/package.json
CHANGED
|
@@ -182,8 +182,29 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
182
182
|
// 合并 AI 配置
|
|
183
183
|
const aiConfig = { ...agentConfig.ai, ...config.aiConfig }
|
|
184
184
|
|
|
185
|
+
// 核心插件列表(不能禁用,必须加载)
|
|
186
|
+
const CORE_PLUGINS = new Set([
|
|
187
|
+
'install', 'ai', 'storage', 'tools', 'workflow', 'skill-manager',
|
|
188
|
+
'mcp-executor', 'shell-executor', 'python-executor', 'session',
|
|
189
|
+
'audit', 'rules', 'scheduler', 'file-system', 'think',
|
|
190
|
+
'python-plugin-loader', 'telegram'
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
// 辅助函数:检查插件是否应该加载(核心插件不能禁用)
|
|
194
|
+
const shouldLoad = (name) => {
|
|
195
|
+
if (framework.pluginManager.has(name)) {
|
|
196
|
+
console.log(`[Bootstrap] ${name} Plugin already loaded, skipping`)
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
// 核心插件不能禁用
|
|
200
|
+
if (CORE_PLUGINS.has(name) && framework.pluginManager.isEnabled(name) === false) {
|
|
201
|
+
console.log(`[Bootstrap] ${name} is a core plugin, cannot be disabled`)
|
|
202
|
+
}
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
// 0. Install 工具插件(最先加载,让其他插件能用到它的 node_modules)
|
|
186
|
-
if (
|
|
207
|
+
if (shouldLoad('install')) {
|
|
187
208
|
const { InstallPlugin } = require('./install-plugin')
|
|
188
209
|
await framework.loadPlugin(new InstallPlugin({ agentDir: agentConfig.agentDir }))
|
|
189
210
|
console.log('[Bootstrap] Install Plugin loaded')
|
|
@@ -197,10 +218,10 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
197
218
|
|
|
198
219
|
console.log('[Bootstrap] Loading default plugins...')
|
|
199
220
|
|
|
200
|
-
//
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
} else
|
|
221
|
+
// AI 插件(如果已禁用则跳过)
|
|
222
|
+
if (!shouldLoad('ai') || !(aiConfig.provider || aiConfig.model || aiConfig.apiKey)) {
|
|
223
|
+
// 跳过或已禁用
|
|
224
|
+
} else {
|
|
204
225
|
const { AIPlugin } = require('./ai-plugin')
|
|
205
226
|
const aiPlugin = new AIPlugin({
|
|
206
227
|
provider: aiConfig.provider || 'deepseek',
|
|
@@ -232,46 +253,38 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
// 2. Storage 存储插件
|
|
235
|
-
if (
|
|
256
|
+
if (shouldLoad('storage')) {
|
|
236
257
|
const { StoragePlugin } = require('./storage-plugin')
|
|
237
258
|
await framework.loadPlugin(new StoragePlugin())
|
|
238
259
|
console.log('[Bootstrap] Storage Plugin loaded')
|
|
239
|
-
} else {
|
|
240
|
-
console.log('[Bootstrap] Storage Plugin already loaded, skipping')
|
|
241
260
|
}
|
|
242
261
|
|
|
243
262
|
// 3. 内置工具插件
|
|
244
|
-
if (
|
|
263
|
+
if (shouldLoad('tools')) {
|
|
245
264
|
const { ToolsPlugin } = require('./tools-plugin')
|
|
246
265
|
await framework.loadPlugin(new ToolsPlugin())
|
|
247
266
|
console.log('[Bootstrap] Tools Plugin loaded')
|
|
248
|
-
} else {
|
|
249
|
-
console.log('[Bootstrap] Tools Plugin already loaded, skipping')
|
|
250
267
|
}
|
|
251
268
|
|
|
252
269
|
// 4. 工作流插件
|
|
253
|
-
if (
|
|
270
|
+
if (shouldLoad('workflow')) {
|
|
254
271
|
const { WorkflowPlugin } = require('../src/capabilities/workflow-engine')
|
|
255
272
|
await framework.loadPlugin(new WorkflowPlugin())
|
|
256
273
|
console.log('[Bootstrap] Workflow Plugin loaded')
|
|
257
|
-
} else {
|
|
258
|
-
console.log('[Bootstrap] Workflow Plugin already loaded, skipping')
|
|
259
274
|
}
|
|
260
275
|
|
|
261
276
|
// 5. Skill 管理器插件
|
|
262
|
-
if (
|
|
277
|
+
if (shouldLoad('skill-manager')) {
|
|
263
278
|
if (skillsDirs.length > 0) {
|
|
264
279
|
const { SkillManagerPlugin } = require('../src/capabilities/skill-manager')
|
|
265
280
|
// 传递所有 skills 目录
|
|
266
281
|
await framework.loadPlugin(new SkillManagerPlugin({ skillsDirs }))
|
|
267
282
|
console.log('[Bootstrap] Skill Manager loaded')
|
|
268
283
|
}
|
|
269
|
-
} else {
|
|
270
|
-
console.log('[Bootstrap] Skill Manager already loaded, skipping')
|
|
271
284
|
}
|
|
272
285
|
|
|
273
286
|
// 6. MCP 执行器插件(始终加载,确保 mcp_reload 工具可用)
|
|
274
|
-
if (
|
|
287
|
+
if (shouldLoad('mcp-executor')) {
|
|
275
288
|
const mcpServers = Object.entries(agentConfig.mcpServers || {})
|
|
276
289
|
const servers = mcpServers.map(([name, cfg]) => ({
|
|
277
290
|
...cfg,
|
|
@@ -285,93 +298,73 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
285
298
|
const { MCPExecutorPlugin } = require('../src/executors/mcp-executor')
|
|
286
299
|
await framework.loadPlugin(new MCPExecutorPlugin({ servers }))
|
|
287
300
|
console.log(`[Bootstrap] MCP Executor loaded${servers.length > 0 ? ` (${servers.length} servers)` : ' (no servers)'}`)
|
|
288
|
-
} else {
|
|
289
|
-
console.log('[Bootstrap] MCP Executor already loaded, skipping')
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
// 7. Shell 执行器插件
|
|
293
|
-
if (
|
|
304
|
+
if (shouldLoad('shell-executor')) {
|
|
294
305
|
const { ShellExecutorPlugin } = require('./shell-executor-plugin')
|
|
295
306
|
await framework.loadPlugin(new ShellExecutorPlugin())
|
|
296
307
|
console.log('[Bootstrap] Shell Executor loaded')
|
|
297
|
-
} else {
|
|
298
|
-
console.log('[Bootstrap] Shell Executor already loaded, skipping')
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
// 8. Python 执行器插件
|
|
302
|
-
if (
|
|
311
|
+
if (shouldLoad('python-executor')) {
|
|
303
312
|
const { PythonExecutorPlugin } = require('./python-executor-plugin')
|
|
304
313
|
await framework.loadPlugin(new PythonExecutorPlugin())
|
|
305
314
|
console.log('[Bootstrap] Python Executor loaded')
|
|
306
|
-
} else {
|
|
307
|
-
console.log('[Bootstrap] Python Executor already loaded, skipping')
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
// 9. Session 会话管理插件
|
|
311
|
-
if (
|
|
318
|
+
if (shouldLoad('session')) {
|
|
312
319
|
const { SessionPlugin } = require('./session-plugin')
|
|
313
320
|
await framework.loadPlugin(new SessionPlugin())
|
|
314
321
|
console.log('[Bootstrap] Session Plugin loaded')
|
|
315
|
-
} else {
|
|
316
|
-
console.log('[Bootstrap] Session Plugin already loaded, skipping')
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
// 10. Audit 审计日志插件
|
|
320
|
-
if (
|
|
325
|
+
if (shouldLoad('audit')) {
|
|
321
326
|
const { AuditPlugin } = require('./audit-plugin')
|
|
322
327
|
await framework.loadPlugin(new AuditPlugin())
|
|
323
328
|
console.log('[Bootstrap] Audit Plugin loaded')
|
|
324
|
-
} else {
|
|
325
|
-
console.log('[Bootstrap] Audit Plugin already loaded, skipping')
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
// 10. Rules 规则引擎插件
|
|
329
|
-
if (
|
|
332
|
+
if (shouldLoad('rules')) {
|
|
330
333
|
const { RulesPlugin } = require('./rules-plugin')
|
|
331
334
|
await framework.loadPlugin(new RulesPlugin())
|
|
332
335
|
console.log('[Bootstrap] Rules Plugin loaded')
|
|
333
|
-
} else {
|
|
334
|
-
console.log('[Bootstrap] Rules Plugin already loaded, skipping')
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
// 11. Scheduler 定时任务插件
|
|
338
|
-
if (
|
|
339
|
+
if (shouldLoad('scheduler')) {
|
|
339
340
|
const { SchedulerPlugin } = require('./scheduler-plugin')
|
|
340
341
|
await framework.loadPlugin(new SchedulerPlugin())
|
|
341
342
|
console.log('[Bootstrap] Scheduler Plugin loaded')
|
|
342
|
-
} else {
|
|
343
|
-
console.log('[Bootstrap] Scheduler Plugin already loaded, skipping')
|
|
344
343
|
}
|
|
345
344
|
|
|
346
345
|
// 11. FileSystem 文件系统插件
|
|
347
|
-
if (
|
|
346
|
+
if (shouldLoad('file-system')) {
|
|
348
347
|
const { FileSystemPlugin } = require('./file-system-plugin')
|
|
349
348
|
await framework.loadPlugin(new FileSystemPlugin())
|
|
350
349
|
console.log('[Bootstrap] FileSystem Plugin loaded')
|
|
351
|
-
} else {
|
|
352
|
-
console.log('[Bootstrap] FileSystem Plugin already loaded, skipping')
|
|
353
350
|
}
|
|
354
351
|
|
|
355
352
|
// 12. Think 主动思考插件
|
|
356
|
-
if (
|
|
353
|
+
if (shouldLoad('think')) {
|
|
357
354
|
const { ThinkPlugin } = require('./think-plugin')
|
|
358
355
|
await framework.loadPlugin(new ThinkPlugin())
|
|
359
356
|
console.log('[Bootstrap] Think Plugin loaded')
|
|
360
|
-
} else {
|
|
361
|
-
console.log('[Bootstrap] Think Plugin already loaded, skipping')
|
|
362
357
|
}
|
|
363
358
|
|
|
364
359
|
// 12.5 Python 插件加载器
|
|
365
|
-
if (
|
|
360
|
+
if (shouldLoad('python-plugin-loader')) {
|
|
366
361
|
const { PythonPluginLoader } = require('./python-plugin-loader')
|
|
367
362
|
await framework.loadPlugin(new PythonPluginLoader({ agentDir: agentConfig.agentDir }))
|
|
368
363
|
console.log('[Bootstrap] Python Plugin Loader loaded')
|
|
369
|
-
} else {
|
|
370
|
-
console.log('[Bootstrap] Python Plugin Loader already loaded, skipping')
|
|
371
364
|
}
|
|
372
365
|
|
|
373
366
|
// 12.6 Telegram 插件(如果配置了Token)
|
|
374
|
-
if (
|
|
367
|
+
if (shouldLoad('telegram')) {
|
|
375
368
|
const telegramToken = process.env.TELEGRAM_BOT_TOKEN || agentConfig.telegram?.botToken
|
|
376
369
|
if (telegramToken) {
|
|
377
370
|
const { Plugin } = require('../src/core/plugin-base')
|
|
@@ -380,8 +373,6 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
380
373
|
await framework.loadPlugin(new TelegramPlugin({ botToken: telegramToken }))
|
|
381
374
|
console.log('[Bootstrap] Telegram Plugin loaded')
|
|
382
375
|
}
|
|
383
|
-
} else {
|
|
384
|
-
console.log('[Bootstrap] Telegram Plugin already loaded, skipping')
|
|
385
376
|
}
|
|
386
377
|
|
|
387
378
|
// 13. 加载自定义插件
|
|
@@ -399,6 +390,80 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
399
390
|
console.log('[Bootstrap] Tools:', framework.getTools().map(t => t.name))
|
|
400
391
|
}
|
|
401
392
|
|
|
393
|
+
/**
|
|
394
|
+
* 解析插件路径
|
|
395
|
+
* 支持两种结构:
|
|
396
|
+
* 1. 文件夹结构: .agent/plugins/my-plugin/index.js
|
|
397
|
+
* 2. 单文件结构: .agent/plugins/my-plugin.js
|
|
398
|
+
* @param {string} pluginsDir - 插件目录
|
|
399
|
+
* @param {string} name - 插件名称
|
|
400
|
+
* @returns {{path: string, type: 'folder'|'file'}|null} 插件路径和类型
|
|
401
|
+
*/
|
|
402
|
+
function resolvePluginPath(pluginsDir, name) {
|
|
403
|
+
const folderPath = path.join(pluginsDir, name)
|
|
404
|
+
const filePath = path.join(pluginsDir, `${name}.js`)
|
|
405
|
+
|
|
406
|
+
// 文件夹优先
|
|
407
|
+
if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) {
|
|
408
|
+
const pkgPath = path.join(folderPath, 'package.json')
|
|
409
|
+
if (fs.existsSync(pkgPath)) {
|
|
410
|
+
try {
|
|
411
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
412
|
+
const main = pkg.main || 'index.js'
|
|
413
|
+
const mainPath = path.join(folderPath, main)
|
|
414
|
+
if (fs.existsSync(mainPath)) {
|
|
415
|
+
return { path: mainPath, type: 'folder' }
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.warn(`[resolvePluginPath] Failed to parse package.json for ${name}:`, err.message)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// 默认加载 index.js
|
|
422
|
+
const indexPath = path.join(folderPath, 'index.js')
|
|
423
|
+
if (fs.existsSync(indexPath)) {
|
|
424
|
+
return { path: indexPath, type: 'folder' }
|
|
425
|
+
}
|
|
426
|
+
console.warn(`[resolvePluginPath] No entry point found for plugin folder: ${name}`)
|
|
427
|
+
return null
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 单文件回退
|
|
431
|
+
if (fs.existsSync(filePath)) {
|
|
432
|
+
return { path: filePath, type: 'file' }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 扫描插件目录,返回所有插件名称
|
|
440
|
+
* @param {string} pluginsDir - 插件目录
|
|
441
|
+
* @returns {string[]} 插件名称列表
|
|
442
|
+
*/
|
|
443
|
+
function scanPluginNames(pluginsDir) {
|
|
444
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
445
|
+
return []
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const names = new Set()
|
|
449
|
+
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true })
|
|
450
|
+
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
if (entry.isDirectory()) {
|
|
453
|
+
// 文件夹插件
|
|
454
|
+
names.add(entry.name)
|
|
455
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
456
|
+
// 单文件插件(排除与文件夹同名的)
|
|
457
|
+
const baseName = entry.name.replace(/\.js$/, '')
|
|
458
|
+
if (!names.has(baseName)) {
|
|
459
|
+
names.add(baseName)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return Array.from(names)
|
|
465
|
+
}
|
|
466
|
+
|
|
402
467
|
async function loadCustomPlugins(framework, agentConfig) {
|
|
403
468
|
// 加载 .agent/plugins 目录下的自定义插件
|
|
404
469
|
const pluginsDir = path.resolve(process.cwd(), '.agent', 'plugins')
|
|
@@ -407,11 +472,18 @@ async function loadCustomPlugins(framework, agentConfig) {
|
|
|
407
472
|
return
|
|
408
473
|
}
|
|
409
474
|
|
|
410
|
-
|
|
475
|
+
// 扫描所有插件名称(支持文件夹和单文件)
|
|
476
|
+
const pluginNames = scanPluginNames(pluginsDir)
|
|
411
477
|
|
|
412
|
-
for (const
|
|
478
|
+
for (const pluginName of pluginNames) {
|
|
413
479
|
try {
|
|
414
|
-
const
|
|
480
|
+
const resolved = resolvePluginPath(pluginsDir, pluginName)
|
|
481
|
+
if (!resolved) {
|
|
482
|
+
console.warn(`[Bootstrap] Cannot resolve plugin: ${pluginName}`)
|
|
483
|
+
continue
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const { path: pluginPath, type } = resolved
|
|
415
487
|
|
|
416
488
|
// 清除缓存
|
|
417
489
|
delete require.cache[require.resolve(pluginPath)]
|
|
@@ -430,18 +502,24 @@ async function loadCustomPlugins(framework, agentConfig) {
|
|
|
430
502
|
// 获取插件名称
|
|
431
503
|
const tempPlugin = typeof plugin === 'function' && plugin.prototype?.name
|
|
432
504
|
? new plugin() : plugin
|
|
433
|
-
const
|
|
505
|
+
const resolvedPluginName = tempPlugin.name || pluginName
|
|
434
506
|
|
|
435
507
|
// 如果插件已加载且已启动,跳过
|
|
436
|
-
if (framework.pluginManager.has(
|
|
437
|
-
framework.pluginManager.get(
|
|
508
|
+
if (framework.pluginManager.has(resolvedPluginName) &&
|
|
509
|
+
framework.pluginManager.get(resolvedPluginName)?._started) {
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// 如果插件被禁用(在 state 文件中 enabled: false),跳过
|
|
514
|
+
if (framework.pluginManager.isEnabled(resolvedPluginName) === false) {
|
|
515
|
+
console.log(`[Bootstrap] Custom plugin '${resolvedPluginName}' is disabled, skipping`)
|
|
438
516
|
continue
|
|
439
517
|
}
|
|
440
518
|
|
|
441
|
-
console.log(`[Bootstrap] Loading custom plugin: ${
|
|
519
|
+
console.log(`[Bootstrap] Loading custom plugin: ${pluginName} (${type})`)
|
|
442
520
|
await framework.loadPlugin(plugin)
|
|
443
521
|
} catch (err) {
|
|
444
|
-
console.error(`[Bootstrap] Failed to load plugin ${
|
|
522
|
+
console.error(`[Bootstrap] Failed to load plugin ${pluginName}:`, err.message)
|
|
445
523
|
}
|
|
446
524
|
}
|
|
447
525
|
}
|
|
@@ -450,5 +528,7 @@ module.exports = {
|
|
|
450
528
|
DefaultPlugins,
|
|
451
529
|
loadAgentConfig,
|
|
452
530
|
bootstrapDefaults,
|
|
453
|
-
loadCustomPlugins
|
|
531
|
+
loadCustomPlugins,
|
|
532
|
+
resolvePluginPath,
|
|
533
|
+
scanPluginNames
|
|
454
534
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* InstallPlugin - npm 包安装工具
|
|
3
|
-
* 自动安装缺少的 node 模块到 .agent
|
|
3
|
+
* 自动安装缺少的 node 模块到 .agent 目录或指定目录
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { execSync } = require('child_process')
|
|
@@ -14,7 +14,7 @@ class InstallPlugin extends Plugin {
|
|
|
14
14
|
super()
|
|
15
15
|
this.name = 'install'
|
|
16
16
|
this.version = '1.0.0'
|
|
17
|
-
this.description = '自动安装 npm
|
|
17
|
+
this.description = '自动安装 npm 包到指定目录'
|
|
18
18
|
|
|
19
19
|
this._agentDir = config.agentDir || '.agent'
|
|
20
20
|
this._nodeModulesDir = null
|
|
@@ -35,35 +35,73 @@ class InstallPlugin extends Plugin {
|
|
|
35
35
|
// 注册 install 工具
|
|
36
36
|
framework.registerTool({
|
|
37
37
|
name: 'install',
|
|
38
|
-
description: '安装 npm
|
|
38
|
+
description: '安装 npm 包到指定目录',
|
|
39
39
|
inputSchema: z.object({
|
|
40
|
-
package: z.string().describe('包名,如 lodash
|
|
40
|
+
package: z.string().optional().describe('包名,如 lodash、lodash@4.17.21 或 @scope/package'),
|
|
41
|
+
path: z.string().optional().describe('安装目标目录,如 .agent/plugins/my-plugin(默认为 .agent)'),
|
|
42
|
+
file: z.string().optional().describe('本地 package.json 路径,从该文件安装所有依赖')
|
|
41
43
|
}),
|
|
42
44
|
execute: async (args) => {
|
|
43
|
-
const { package: packageName } = args
|
|
44
|
-
|
|
45
|
+
const { package: packageName, path: targetPath, file: packageJsonPath } = args
|
|
46
|
+
|
|
47
|
+
// 优先处理 file 参数(从 package.json 安装)
|
|
48
|
+
if (packageJsonPath) {
|
|
49
|
+
const resolvedPath = path.resolve(process.cwd(), packageJsonPath)
|
|
50
|
+
return this._installFromPackageJson(resolvedPath, targetPath)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// package 和 path 都提供
|
|
54
|
+
if (packageName) {
|
|
55
|
+
return this._installPackage(packageName, targetPath)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 只有 path,安装该目录的 package.json
|
|
59
|
+
if (targetPath) {
|
|
60
|
+
const resolvedPath = path.resolve(process.cwd(), targetPath)
|
|
61
|
+
const pkgJson = path.join(resolvedPath, 'package.json')
|
|
62
|
+
if (fs.existsSync(pkgJson)) {
|
|
63
|
+
return this._installFromPackageJson(pkgJson, targetPath)
|
|
64
|
+
}
|
|
65
|
+
return { success: false, error: `package.json not found at ${pkgJson}` }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 什么都不提供,报错
|
|
69
|
+
return { success: false, error: 'Must provide package name or package.json path' }
|
|
45
70
|
}
|
|
46
71
|
})
|
|
47
72
|
|
|
48
73
|
return this
|
|
49
74
|
}
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 安装单个包到指定目录
|
|
78
|
+
* @param {string} packageName - 包名
|
|
79
|
+
* @param {string|null} targetPath - 目标目录,默认为 .agent
|
|
80
|
+
*/
|
|
81
|
+
_installPackage(packageName, targetPath = null) {
|
|
52
82
|
try {
|
|
53
|
-
|
|
83
|
+
const installPath = targetPath
|
|
84
|
+
? path.resolve(process.cwd(), targetPath)
|
|
85
|
+
: this._agentDir
|
|
86
|
+
|
|
87
|
+
console.log(`[InstallPlugin] Installing ${packageName} to ${installPath}...`)
|
|
88
|
+
|
|
89
|
+
// 确保目标目录存在
|
|
90
|
+
if (!fs.existsSync(installPath)) {
|
|
91
|
+
fs.mkdirSync(installPath, { recursive: true })
|
|
92
|
+
}
|
|
54
93
|
|
|
55
94
|
// 使用 npm install 安装到指定目录
|
|
56
|
-
|
|
57
|
-
execSync(`npm install ${packageName} --prefix "${this._agentDir}"`, {
|
|
95
|
+
execSync(`npm install ${packageName} --prefix "${installPath}"`, {
|
|
58
96
|
stdio: 'inherit',
|
|
59
|
-
cwd:
|
|
97
|
+
cwd: installPath
|
|
60
98
|
})
|
|
61
99
|
|
|
62
100
|
console.log(`[InstallPlugin] Successfully installed ${packageName}`)
|
|
63
101
|
|
|
64
102
|
return {
|
|
65
103
|
success: true,
|
|
66
|
-
message: `Successfully installed ${packageName}`
|
|
104
|
+
message: `Successfully installed ${packageName} to ${installPath}`
|
|
67
105
|
}
|
|
68
106
|
} catch (err) {
|
|
69
107
|
return {
|
|
@@ -73,6 +111,61 @@ class InstallPlugin extends Plugin {
|
|
|
73
111
|
}
|
|
74
112
|
}
|
|
75
113
|
|
|
114
|
+
/**
|
|
115
|
+
* 从 package.json 安装所有依赖
|
|
116
|
+
* @param {string} packageJsonPath - package.json 路径
|
|
117
|
+
* @param {string|null} targetPath - 目标目录,默认为 package.json 所在目录
|
|
118
|
+
*/
|
|
119
|
+
_installFromPackageJson(packageJsonPath, targetPath = null) {
|
|
120
|
+
try {
|
|
121
|
+
const resolvedPkgPath = path.resolve(process.cwd(), packageJsonPath)
|
|
122
|
+
const pkgDir = path.dirname(resolvedPkgPath)
|
|
123
|
+
const installPath = targetPath
|
|
124
|
+
? path.resolve(process.cwd(), targetPath)
|
|
125
|
+
: pkgDir
|
|
126
|
+
|
|
127
|
+
console.log(`[InstallPlugin] Installing dependencies from ${resolvedPkgPath} to ${installPath}...`)
|
|
128
|
+
|
|
129
|
+
// 读取 package.json 获取要安装的包
|
|
130
|
+
const pkg = JSON.parse(fs.readFileSync(resolvedPkgPath, 'utf-8'))
|
|
131
|
+
const packages = []
|
|
132
|
+
|
|
133
|
+
if (pkg.dependencies) {
|
|
134
|
+
packages.push(...Object.keys(pkg.dependencies))
|
|
135
|
+
}
|
|
136
|
+
if (pkg.devDependencies) {
|
|
137
|
+
packages.push(...Object.keys(pkg.devDependencies))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (packages.length === 0) {
|
|
141
|
+
return { success: true, message: 'No dependencies to install' }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 确保目标目录存在
|
|
145
|
+
if (!fs.existsSync(installPath)) {
|
|
146
|
+
fs.mkdirSync(installPath, { recursive: true })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 使用 npm install 安装
|
|
150
|
+
execSync(`npm install --prefix "${installPath}" --no-save`, {
|
|
151
|
+
stdio: 'inherit',
|
|
152
|
+
cwd: pkgDir
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
console.log(`[InstallPlugin] Successfully installed ${packages.length} packages`)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
message: `Successfully installed ${packages.length} dependencies to ${installPath}`
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: `Failed to install from package.json: ${err.message}`
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
76
169
|
/**
|
|
77
170
|
* 获取 node_modules 路径,供其他插件使用
|
|
78
171
|
*/
|
|
@@ -88,6 +181,16 @@ class InstallPlugin extends Plugin {
|
|
|
88
181
|
module.paths.unshift(this._nodeModulesDir)
|
|
89
182
|
}
|
|
90
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 添加自定义路径到 module.paths
|
|
187
|
+
* @param {string} dir - 目录路径
|
|
188
|
+
*/
|
|
189
|
+
addCustomModulePath(dir) {
|
|
190
|
+
if (dir && fs.existsSync(dir) && !module.paths.includes(dir)) {
|
|
191
|
+
module.paths.unshift(dir)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
91
194
|
}
|
|
92
195
|
|
|
93
196
|
module.exports = { InstallPlugin }
|
|
@@ -8,26 +8,50 @@ This file provides guidance to AI coding agents on how to create plugins for the
|
|
|
8
8
|
|
|
9
9
|
User plugins go in `.agent/plugins/` and are **automatically loaded** on bootstrap.
|
|
10
10
|
|
|
11
|
+
**Recommended: Use folder structure for complex plugins**
|
|
12
|
+
|
|
11
13
|
```
|
|
12
14
|
项目目录/
|
|
13
15
|
└── .agent/
|
|
14
16
|
└── plugins/
|
|
15
|
-
├── my-plugin
|
|
16
|
-
|
|
17
|
+
├── my-plugin/ ✅ folder structure (recommended)
|
|
18
|
+
│ ├── package.json # optional, main field specifies entry
|
|
19
|
+
│ ├── index.js # default entry point
|
|
20
|
+
│ └── node_modules/ # optional, plugin-private dependencies
|
|
21
|
+
├── another-plugin/ ✅ another folder plugin
|
|
22
|
+
│ └── index.js
|
|
23
|
+
└── legacy.js ✅ single-file still supported
|
|
17
24
|
```
|
|
18
25
|
|
|
26
|
+
**Why folder structure?**
|
|
27
|
+
- Supports `package.json` with `main` field for custom entry points
|
|
28
|
+
- Supports `node_modules` for plugin-private dependencies
|
|
29
|
+
- Better organization for complex plugins
|
|
30
|
+
|
|
19
31
|
### Built-in Plugins: `plugins/` (internal)
|
|
20
32
|
|
|
21
33
|
Built-in framework plugins are in `plugins/` directory.
|
|
22
34
|
|
|
23
35
|
## Plugin Export Formats
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
### Folder structure (recommended for `.agent/plugins/`)
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
.agent/plugins/my-plugin/
|
|
41
|
+
├── package.json # optional
|
|
42
|
+
└── index.js # entry point
|
|
43
|
+
```
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
```json
|
|
46
|
+
// package.json example
|
|
47
|
+
{
|
|
48
|
+
"name": "my-plugin",
|
|
49
|
+
"main": "index.js"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
28
52
|
|
|
29
53
|
```javascript
|
|
30
|
-
// .agent/plugins/my-plugin.js
|
|
54
|
+
// .agent/plugins/my-plugin/index.js
|
|
31
55
|
module.exports = function(Plugin) {
|
|
32
56
|
return class MyPlugin extends Plugin {
|
|
33
57
|
constructor(config = {}) {
|
|
@@ -60,10 +84,22 @@ module.exports = function(Plugin) {
|
|
|
60
84
|
}
|
|
61
85
|
```
|
|
62
86
|
|
|
87
|
+
### Single-file structure (still supported)
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// .agent/plugins/my-plugin.js
|
|
91
|
+
module.exports = function(Plugin) {
|
|
92
|
+
return class MyPlugin extends Plugin {
|
|
93
|
+
// ... same as above
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
63
98
|
**Key points:**
|
|
64
99
|
- `Plugin` base class passed automatically by system (no require needed)
|
|
65
100
|
- Factory function returns plugin class
|
|
66
101
|
- Use `require('zod')` inside methods
|
|
102
|
+
- Folder takes priority over single-file if both exist with same name
|
|
67
103
|
|
|
68
104
|
### Traditional format for `plugins/` (built-in)
|
|
69
105
|
|
|
@@ -149,14 +185,49 @@ The framework object provides:
|
|
|
149
185
|
| `framework.registerTool(tool)` | Register tool |
|
|
150
186
|
| `framework.getTools()` | Get all tools |
|
|
151
187
|
| `framework.executeTool(name, args)` | Execute tool |
|
|
188
|
+
| `framework.callTool(name, args)` | Call tool (for installing deps) |
|
|
152
189
|
| `framework.createAgent(config)` | Create Agent |
|
|
153
190
|
| `framework.reloadPlugin(name)` | Reload single plugin |
|
|
154
191
|
| `framework.reloadAllPlugins()` | Reload all plugins |
|
|
155
192
|
|
|
193
|
+
## Dependency Management
|
|
194
|
+
|
|
195
|
+
Use the `install` tool to install npm packages:
|
|
196
|
+
|
|
197
|
+
| Plugin Type | Install Location | Command |
|
|
198
|
+
|-------------|------------------|---------|
|
|
199
|
+
| **Folder plugin** | `.agent/plugins/plugin-name/node_modules/` | `install({ package: "pkg", path: ".agent/plugins/plugin-name" })` |
|
|
200
|
+
| **Single-file plugin** | `.agent/node_modules/` | `install({ package: "pkg" })` |
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
// Folder plugin - install to plugin directory
|
|
204
|
+
await framework.callTool('install', {
|
|
205
|
+
package: 'axios',
|
|
206
|
+
path: '.agent/plugins/my-plugin'
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Single-file plugin - install to .agent
|
|
210
|
+
await framework.callTool('install', {
|
|
211
|
+
package: 'zod'
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Install from package.json
|
|
215
|
+
await framework.callTool('install', {
|
|
216
|
+
file: './my-plugin/package.json',
|
|
217
|
+
path: '.agent/plugins/my-plugin'
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**When encountering `Cannot find module 'xxx'` error:**
|
|
222
|
+
1. Identify plugin directory
|
|
223
|
+
2. Call `install` tool with `path` parameter for folder plugins
|
|
224
|
+
3. Reload plugin
|
|
225
|
+
4. Report status to user
|
|
226
|
+
|
|
156
227
|
## Common Mistakes
|
|
157
228
|
|
|
158
|
-
1. ❌
|
|
159
|
-
2. ❌ Forgetting `
|
|
160
|
-
3. ❌
|
|
161
|
-
4. ❌
|
|
162
|
-
5. ❌
|
|
229
|
+
1. ❌ Forgetting `install()` - tools won't be registered
|
|
230
|
+
2. ❌ Forgetting `return this` - chain calls will fail
|
|
231
|
+
3. ❌ Using `parameters` instead of `inputSchema`
|
|
232
|
+
4. ❌ Registering tools outside `install()`
|
|
233
|
+
5. ❌ Using same name for folder and file - folder takes priority, file will be ignored
|
|
@@ -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
|
|
26
|
-
|
|
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.
|
|
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`
|
|
165
|
+
当插件需要使用第三方 npm 包(如 `zod`、`axios` 等)时,根据插件类型选择安装位置:
|
|
166
|
+
|
|
167
|
+
| 插件类型 | 安装位置 | 说明 |
|
|
168
|
+
|----------|----------|------|
|
|
169
|
+
| **文件夹插件** | `.agent/plugins/插件名/node_modules/` | 插件自包含,可独立迁移 |
|
|
170
|
+
| **单文件插件** | `.agent/node_modules/` | 共享依赖 |
|
|
89
171
|
|
|
90
172
|
### 自动安装工具
|
|
91
173
|
|
|
92
|
-
框架提供了 `install`
|
|
174
|
+
框架提供了 `install` 工具,支持以下用法:
|
|
93
175
|
|
|
94
176
|
```javascript
|
|
95
|
-
//
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
180
|
+
// 安装到指定目录(用于文件夹插件)
|
|
181
|
+
install({ package: "axios", path: ".agent/plugins/my-plugin" })
|
|
105
182
|
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
|
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
|
-
|
|
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
|
|
353
|
+
for (const pluginName of pluginNames) {
|
|
280
354
|
try {
|
|
281
|
-
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
|
|
282
362
|
|
|
283
363
|
// 添加模块路径到搜索路径(优先级从高到低)
|
|
284
364
|
const modulePathsToAdd = [
|
|
@@ -305,24 +385,24 @@ class PluginManager {
|
|
|
305
385
|
}
|
|
306
386
|
|
|
307
387
|
// 获取插件名称
|
|
308
|
-
let
|
|
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
|
-
|
|
392
|
+
resolvedPluginName = tempPlugin.name || pluginName
|
|
313
393
|
} catch {
|
|
314
|
-
|
|
394
|
+
resolvedPluginName = pluginName
|
|
315
395
|
}
|
|
316
396
|
|
|
317
397
|
// 如果插件已加载且已启动,跳过
|
|
318
|
-
if (this.has(
|
|
398
|
+
if (this.has(resolvedPluginName) && this.get(resolvedPluginName)?._started) {
|
|
319
399
|
continue
|
|
320
400
|
}
|
|
321
401
|
|
|
322
|
-
console.log(`[PluginManager] Loading new plugin: ${
|
|
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 ${
|
|
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'
|
|
468
|
+
// 如果插件已加载,尝试重新启动(reload 会调用 start)
|
|
469
|
+
if (entry.status === 'loaded') {
|
|
390
470
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
entry.instance.
|
|
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]
|
|
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)
|