foliko 1.0.6 → 1.0.8

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.
@@ -28,7 +28,9 @@
28
28
  "Bash(cd D:/Code/vb-agent && pnpm install --shamefully-hoist 2>&1 | head -20)",
29
29
  "Bash(cd D:/Code/vb-agent && rm -rf node_modules && pnpm install)",
30
30
  "Bash(cd D:/Code/vb-agent && timeout 8 node test-tg.js 2>&1 || true)",
31
- "Bash(cd D:/Code/vb-agent && timeout 10 node test-tg.js 2>&1 || true)"
31
+ "Bash(cd D:/Code/vb-agent && timeout 10 node test-tg.js 2>&1 || true)",
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)"
32
34
  ]
33
35
  }
34
36
  }
package/.env.example ADDED
@@ -0,0 +1,23 @@
1
+ # ========== AI API Keys ==========
2
+ DEEPSEEK_API_KEY=sk-your-deepseek-api-key
3
+ MINIMAX_API_KEY=sk-your-minimax-api-key
4
+
5
+ # ========== Email Configuration ==========
6
+ # SMTP Settings (for sending emails)
7
+ SMTP_HOST=smtp.gmail.com
8
+ SMTP_PORT=587
9
+ SMTP_SECURE=false
10
+ SMTP_USER=your-email@gmail.com
11
+ SMTP_PASS=your-app-password
12
+
13
+ # IMAP Settings (for reading emails)
14
+ IMAP_HOST=imap.gmail.com
15
+ IMAP_PORT=993
16
+ IMAP_USER=your-email@gmail.com
17
+ IMAP_PASS=your-app-password
18
+
19
+ # Default sender email address
20
+ FROM_EMAIL=your-email@gmail.com
21
+
22
+ # ========== Telegram Bot (optional) ==========
23
+ TELEGRAM_BOT_TOKEN=your-telegram-bot-token
package/SPEC.md CHANGED
@@ -165,7 +165,9 @@ D:\code\vb-agent\
165
165
  │ ├── rules-plugin.js # Rules规则引擎插件
166
166
  │ ├── scheduler-plugin.js # Scheduler定时任务插件
167
167
  │ ├── storage-plugin.js # Storage存储插件
168
- └── subagent-plugin.js # SubAgent子代理插件
168
+ ├── subagent-plugin.js # SubAgent子代理插件
169
+ │ ├── email.js # Email邮件插件
170
+ │ └── telegram-plugin.js # Telegram对话插件
169
171
  ├── examples/
170
172
  │ ├── basic.js # 基础示例
171
173
  │ ├── bootstrap.js # Bootstrap示例
@@ -182,6 +184,25 @@ D:\code\vb-agent\
182
184
 
183
185
  ## 四、核心模块
184
186
 
187
+ ### 4.5 插件配置持久化
188
+
189
+ 插件状态和配置自动保存到 `.agent/data/plugins-state.json`
190
+
191
+ ```javascript
192
+ // 更新插件配置
193
+ framework.updatePluginConfig('telegram', { allowedChats: ['123'] })
194
+
195
+ // 启用/禁用插件
196
+ framework.enablePlugin('telegram')
197
+ framework.disablePlugin('telegram')
198
+ ```
199
+
200
+ **保存的内容:**
201
+ - `enabled` - 插件启用状态
202
+ - `config` - 插件配置对象
203
+
204
+ **自动恢复:** 框架启动时自动加载保存的状态
205
+
185
206
  ### 4.1 Framework (src/core/framework.js)
186
207
 
187
208
  **职责**:
@@ -278,6 +299,8 @@ const response = await agent.chat('你好')
278
299
  'plugin:start' // 插件启动
279
300
  'plugin:reload' // 插件重载
280
301
  'plugin:uninstall' // 插件卸载
302
+ 'plugin:enabled' // 插件启用
303
+ 'plugin:disabled' // 插件禁用
281
304
  'agent:message' // Agent消息
282
305
  'agent:tool-call' // Agent调用工具
283
306
  'agent:tool-result' // 工具执行结果
@@ -337,6 +360,40 @@ agent.chat('请重载 my-plugin 插件')
337
360
  **内置工具**:
338
361
  - `reload_plugins` - 热重载插件
339
362
  - `list_plugins` - 列出已加载插件
363
+ - `enable_plugin` - 启用插件
364
+ - `disable_plugin` - 禁用插件
365
+ - `get_plugin_config` - 获取插件配置
366
+ - `update_plugin_config` - 更新插件配置
367
+
368
+ ### 7.3 Email Plugin (plugins/email-plugin.js)
369
+
370
+ **功能**:
371
+ - SMTP 发送邮件
372
+ - IMAP 读取邮件
373
+
374
+ **工具**:
375
+ - `email_send` - 发送邮件
376
+ - `email_read` - 读取邮件
377
+ - `email_unread_count` - 未读数量
378
+ - `email_mark_read` - 标记已读
379
+
380
+ ### 7.4 Telegram Plugin (plugins/telegram-plugin.js)
381
+
382
+ **功能**:
383
+ - Telegram Bot 对话
384
+ - 绑定主 Agent 持续对话
385
+ - 支持 MarkdownV2
386
+ - 图片/文档接收保存
387
+
388
+ **配置**:
389
+ ```javascript
390
+ {
391
+ botToken: '...',
392
+ allowedChats: ['123'],
393
+ groupMode: false,
394
+ prefix: '/'
395
+ }
396
+ ```
340
397
 
341
398
  ## 八、使用示例
342
399
 
@@ -437,16 +494,32 @@ class Framework {
437
494
  - [x] Scheduler 定时任务 (`plugins/scheduler-plugin.js`) - Cron 调度
438
495
  - [x] Storage 存储 (`plugins/storage-plugin.js`) - 键值对持久化存储
439
496
  - [x] SubAgent 子Agent (`plugins/subagent-plugin.js`) - 子Agent隔离工具集
497
+ - [x] Email 插件 (`plugins/email.js`) - 邮件收发
498
+ - [x] Telegram 插件 (`plugins/telegram-plugin.js`) - Telegram Bot 对话
499
+ - [x] 插件配置持久化 - enabled/config 保存到 plugins-state.json
500
+ - [x] AI 热重载 - LLM 配置更新后自动刷新已有 Agent
440
501
 
441
502
  ## 十一、依赖
442
503
 
443
504
  ```json
444
505
  {
445
506
  "dependencies": {
507
+ "@ai-sdk/anthropic": "^3.0.58",
508
+ "@ai-sdk/mcp": "^1.0.25",
446
509
  "@ai-sdk/openai": "^3.0.41",
447
510
  "@ai-sdk/openai-compatible": "^2.0.35",
511
+ "@anthropic-ai/sdk": "^0.39.0",
512
+ "@modelcontextprotocol/sdk": "^1.27.1",
448
513
  "ai": "^6.0.116",
449
- "zod": "^4.3.6"
514
+ "dotenv": "^17.3.1",
515
+ "imap": "^0.8.19",
516
+ "mailparser": "^3.7.2",
517
+ "marked": "^11.2.0",
518
+ "marked-terminal": "6",
519
+ "node-cron": "^4.2.1",
520
+ "node-telegram-bot-api": "^0.67.0",
521
+ "nodemailer": "^6.10.0",
522
+ "zod": "^3.24.0"
450
523
  }
451
524
  }
452
525
  ```
@@ -40,19 +40,34 @@ const result = await agent.chat('你好')
40
40
  | `execute_command` | 执行命令 |
41
41
  | `schedule_once` | 定时任务 |
42
42
  | `think_now` | 主动思考 |
43
+ | `email_send` | 发送邮件 |
44
+ | `email_read` | 读取邮件 |
45
+ | `enable_plugin` | 启用插件 |
46
+ | `disable_plugin` | 禁用插件 |
47
+ | `update_plugin_config` | 更新插件配置 |
43
48
 
44
49
  ## 插件位置
45
50
 
46
51
  - 内置插件:`plugins/`
47
52
  - 用户插件:`.agent/plugins/`
48
53
 
49
- ## 重启插件
54
+ ## 插件管理
50
55
 
51
56
  ```javascript
52
- // 调用工具
57
+ // 重启插件
53
58
  await framework.executeTool('reload_plugins', {})
54
- //
55
- await framework.reloadAllPlugins()
59
+ await framework.executeTool('reload_plugins', { pluginName: 'telegram' })
60
+
61
+ // 启用/禁用插件
62
+ await framework.executeTool('enable_plugin', { name: 'telegram' })
63
+ await framework.executeTool('disable_plugin', { name: 'telegram' })
64
+
65
+ // 更新插件配置(持久化)
66
+ await framework.executeTool('update_plugin_config', {
67
+ name: 'telegram',
68
+ config: { allowedChats: ['123'], groupMode: true }
69
+ })
70
+ await framework.executeTool('get_plugin_config', { name: 'telegram' })
56
71
  ```
57
72
 
58
73
  ## 事件监听
@@ -129,3 +144,14 @@ agent.getStatus() // 'idle' | 'busy' | 'error'
129
144
  agent.resetStatus() // 重置状态
130
145
  agent.clearHistory() // 清空历史
131
146
  ```
147
+
148
+ ## 配置持久化
149
+
150
+ 插件配置自动保存到 `.agent/data/plugins-state.json`
151
+
152
+ | 文件 | 用途 |
153
+ |------|------|
154
+ | `.agent/data/plugins-state.json` | 插件启用状态和配置 |
155
+ | `.agent/data/storage.json` | 键值存储数据 |
156
+ | `.agent/data/telegram_images/` | Telegram 接收的图片 |
157
+ | `.agent/data/telegram_documents/` | Telegram 接收的文档 |
@@ -244,6 +244,22 @@ await framework.loadPlugin(new AIPlugin({
244
244
  }))
245
245
  ```
246
246
 
247
+ **热重载支持:** 更新 AI 配置后执行 `reload_plugins('ai')`,所有已有 Agent 会自动使用新的 LLM 设置。
248
+
249
+ ```javascript
250
+ // 1. 更新 LLM 配置
251
+ await framework.executeTool('update_plugin_config', {
252
+ name: 'ai',
253
+ config: {
254
+ model: 'gpt-4o',
255
+ apiKey: 'new-key'
256
+ }
257
+ })
258
+
259
+ // 2. 热重载 AI 插件
260
+ await framework.executeTool('reload_plugins', { pluginName: 'ai' })
261
+ ```
262
+
247
263
  ### 4.2 存储插件 (storage-plugin)
248
264
 
249
265
  键值对存储,支持 JSON 文件持久化。
@@ -263,9 +279,13 @@ storage_clear() // 清空存储
263
279
 
264
280
  ```javascript
265
281
  // 注册的工具
266
- reload_plugins(pluginName?) // 热重载插件
267
- list_plugins() // 列出已加载插件
268
- list_tools() // 列出所有工具
282
+ reload_plugins(pluginName?) // 热重载插件(可选指定插件名)
283
+ list_plugins() // 列出已加载插件
284
+ list_tools() // 列出所有工具
285
+ enable_plugin(name) // 启用指定插件
286
+ disable_plugin(name) // 禁用指定插件
287
+ get_plugin_config(name) // 获取插件当前配置
288
+ update_plugin_config(name, config) // 更新插件配置(持久化)
269
289
  ```
270
290
 
271
291
  ### 4.4 文件系统插件 (file-system-plugin)
@@ -461,6 +481,132 @@ loadSkill(skill) // 加载指定技能
461
481
  // 技能存放在 skills/ 或 .agent/skills/ 目录
462
482
  ```
463
483
 
484
+ ### 4.14 Email 插件 (email-plugin)
485
+
486
+ 邮件收发插件,支持 SMTP 发送和 IMAP 读取。
487
+
488
+ ```javascript
489
+ // 配置
490
+ await framework.loadPlugin(new EmailPlugin({
491
+ smtp_host: 'smtp.gmail.com',
492
+ smtp_port: 587,
493
+ smtp_secure: false,
494
+ smtp_user: 'your-email@gmail.com',
495
+ smtp_pass: 'your-app-password',
496
+ imap_host: 'imap.gmail.com',
497
+ imap_port: 993,
498
+ imap_user: 'your-email@gmail.com',
499
+ imap_pass: 'your-app-password',
500
+ from_email: 'your-email@gmail.com'
501
+ }))
502
+
503
+ // 或通过工具配置
504
+ email_configure({
505
+ smtp_host: 'smtp.gmail.com',
506
+ smtp_user: 'your-email@gmail.com',
507
+ smtp_pass: 'your-app-password'
508
+ })
509
+ ```
510
+
511
+ **注册的工具:**
512
+
513
+ | 工具 | 说明 |
514
+ |------|------|
515
+ | `email_send` | 发送电子邮件 |
516
+ | `email_read` | 读取电子邮件(IMAP) |
517
+ | `email_unread_count` | 获取未读邮件数量 |
518
+ | `email_mark_read` | 标记邮件为已读 |
519
+ | `email_configure` | 配置邮箱连接参数 |
520
+
521
+ ```javascript
522
+ // 发送邮件
523
+ email_send({
524
+ to: 'recipient@example.com',
525
+ subject: '主题',
526
+ body: '正文内容',
527
+ cc: 'cc@example.com', // 可选
528
+ isHtml: false
529
+ })
530
+
531
+ // 读取邮件
532
+ email_read({
533
+ box: 'INBOX',
534
+ limit: 10,
535
+ unreadOnly: false
536
+ })
537
+
538
+ // 获取未读数量
539
+ email_unread_count({ box: 'INBOX' })
540
+
541
+ // 标记为已读
542
+ email_mark_read({ messageId: '12345' })
543
+ ```
544
+
545
+ ### 4.15 Telegram 插件 (telegram-plugin)
546
+
547
+ Telegram 对话插件,绑定主 Agent 进行持续对话。
548
+
549
+ ```javascript
550
+ // 配置
551
+ await framework.loadPlugin(new TelegramPlugin({
552
+ botToken: 'YOUR_BOT_TOKEN', // 必需
553
+ allowedChats: ['123456789'], // 可选:允许的聊天ID
554
+ groupMode: false, // 可选:是否启用群组模式
555
+ prefix: '/', // 可选:命令前缀
556
+ systemPrompt: '你是一个有帮助的AI助手。'
557
+ }))
558
+ ```
559
+
560
+ **功能特性:**
561
+
562
+ - 支持私聊和群组模式
563
+ - MarkdownV2 格式支持(自动转义特殊字符)
564
+ - 持续对话(基于 sessionId)
565
+ - 支持图片和文档接收
566
+ - 媒体文件自动保存到 `.agent/data/`
567
+
568
+ **命令:**
569
+
570
+ | 命令 | 说明 |
571
+ |------|------|
572
+ | `/start` | 显示帮助信息 |
573
+ | `/clear` | 清除对话历史 |
574
+ | `/history` | 查看对话状态 |
575
+
576
+ ### 4.16 插件管理扩展
577
+
578
+ #### 插件启用/禁用
579
+
580
+ ```javascript
581
+ // 启用插件
582
+ await framework.enablePlugin('telegram')
583
+
584
+ // 禁用插件
585
+ await framework.disablePlugin('telegram')
586
+ ```
587
+
588
+ #### 插件配置更新
589
+
590
+ ```javascript
591
+ // 更新插件配置(会自动保存到 .agent/data/plugins-state.json)
592
+ await framework.executeTool('update_plugin_config', {
593
+ name: 'telegram',
594
+ config: {
595
+ allowedChats: ['123456789', '987654321'],
596
+ groupMode: true
597
+ }
598
+ })
599
+
600
+ // 获取插件当前配置
601
+ await framework.executeTool('get_plugin_config', {
602
+ name: 'telegram'
603
+ })
604
+ ```
605
+
606
+ **配置持久化:**
607
+ - 插件的 `enabled` 状态和 `config` 会自动保存到 `.agent/data/plugins-state.json`
608
+ - 重启后自动恢复配置
609
+
464
610
  ---
465
611
 
466
612
  ## 5. 插件开发
@@ -792,6 +938,8 @@ agent.emit('eventName', { key: 'value' })
792
938
  | `plugin:loaded` | `plugin` | 插件加载完成 |
793
939
  | `plugin:unloaded` | `plugin` | 插件卸载 |
794
940
  | `plugin:reloaded` | `plugin` | 插件重载 |
941
+ | `plugin:enabled` | `plugin` | 插件启用 |
942
+ | `plugin:disabled` | `plugin` | 插件禁用 |
795
943
  | `tool:registered` | `tool` | 工具注册 |
796
944
  | `agent:created` | `agent` | Agent 创建 |
797
945
 
@@ -998,6 +1146,13 @@ await framework.loadPlugin(plugin)
998
1146
  await framework.reloadPlugin(name)
999
1147
  await framework.reloadAllPlugins()
1000
1148
 
1149
+ // 启用/禁用插件
1150
+ await framework.enablePlugin(name)
1151
+ await framework.disablePlugin(name)
1152
+
1153
+ // 更新插件配置
1154
+ framework.updatePluginConfig(name, config)
1155
+
1001
1156
  // 执行工具
1002
1157
  const result = await framework.executeTool(name, args)
1003
1158
 
@@ -4,7 +4,7 @@
4
4
  * 支持多行输入:连续按两次回车结束输入
5
5
  */
6
6
 
7
- const { Framework } = require('./src')
7
+ const { Framework } = require('../src')
8
8
  const readline = require('readline')
9
9
  require('dotenv').config()
10
10
 
@@ -2,8 +2,8 @@
2
2
  * MCP 插件测试脚本
3
3
  */
4
4
 
5
- const { Framework } = require('./src')
6
- const { MCPExecutorPlugin } = require('./src/executors/mcp-executor')
5
+ const { Framework } = require('../src')
6
+ const { MCPExecutorPlugin } = require('../src/executors/mcp-executor')
7
7
 
8
8
  async function test() {
9
9
  console.log('=== MCP Plugin Test ===\n')
@@ -2,8 +2,8 @@
2
2
  * MCP mcp_reload 功能测试
3
3
  */
4
4
 
5
- const { Framework } = require('./src')
6
- const { MCPExecutorPlugin } = require('./src/executors/mcp-executor')
5
+ const { Framework } = require('../src')
6
+ const { MCPExecutorPlugin } = require('../src/executors/mcp-executor')
7
7
  const fs = require('fs')
8
8
  const path = require('path')
9
9
 
@@ -1,5 +1,5 @@
1
1
  require('dotenv').config();
2
- const { Framework } = require('./src');
2
+ const { Framework } = require('../src');
3
3
 
4
4
  async function main() {
5
5
  const framework = new Framework({ debug: false });
@@ -1,5 +1,5 @@
1
1
  require('dotenv').config();
2
- const { Framework } = require('./src');
2
+ const { Framework } = require('../src');
3
3
 
4
4
  async function main() {
5
5
  console.log('Starting test...\n');
@@ -1,5 +1,5 @@
1
1
  require('dotenv').config();
2
- const { Framework } = require('./src');
2
+ const { Framework } = require('../src');
3
3
 
4
4
  async function main() {
5
5
  const framework = new Framework({ debug: true });
@@ -1,5 +1,5 @@
1
1
  require('dotenv').config();
2
- const { Framework } = require('./src');
2
+ const { Framework } = require('../src');
3
3
 
4
4
  async function main() {
5
5
  const framework = new Framework({ debug: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -28,10 +28,13 @@
28
28
  "@modelcontextprotocol/sdk": "^1.27.1",
29
29
  "ai": "^6.0.116",
30
30
  "dotenv": "^17.3.1",
31
+ "imap": "^0.8.19",
32
+ "mailparser": "^3.7.2",
31
33
  "marked": "^11.2.0",
32
34
  "marked-terminal": "6",
33
35
  "node-cron": "^4.2.1",
34
36
  "node-telegram-bot-api": "^0.67.0",
37
+ "nodemailer": "^6.10.0",
35
38
  "zod": "^3.24.0"
36
39
  }
37
40
  }
@@ -79,6 +79,14 @@ class AIPlugin extends Plugin {
79
79
  console.log('[AIPlugin] Reloading...')
80
80
  this._framework = framework
81
81
  this._initAIClient()
82
+
83
+ // 刷新所有已有 Agent 的 AI client
84
+ for (const agent of framework._agents || []) {
85
+ if (agent._chatHandler) {
86
+ agent._chatHandler.setAIClient(this._aiClient)
87
+ console.log(`[AIPlugin] Refreshed AI client for agent: ${agent.name}`)
88
+ }
89
+ }
82
90
  }
83
91
 
84
92
  uninstall(framework) {
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Email 插件
3
+ * 邮件收发插件 - 支持读取和发送电子邮件
4
+ */
5
+
6
+ const { Plugin } = require('../src/core/plugin-base')
7
+ const { z } = require('zod')
8
+
9
+ class EmailPlugin extends Plugin {
10
+ constructor(config = {}) {
11
+ super()
12
+ this.name = 'email'
13
+ this.version = '1.0.0'
14
+ this.description = '邮件收发插件 - 支持读取和发送电子邮件'
15
+ this.priority = 10
16
+ this.config = config
17
+ }
18
+
19
+ install(framework) {
20
+ this._framework = framework
21
+ this._registerTools()
22
+ return this
23
+ }
24
+
25
+ _registerTools() {
26
+ // 发送邮件工具
27
+ this._framework.registerTool({
28
+ name: 'email_send',
29
+ description: '发送电子邮件',
30
+ inputSchema: z.object({
31
+ to: z.string().describe('收件人邮箱地址'),
32
+ subject: z.string().describe('邮件主题'),
33
+ body: z.string().describe('邮件正文内容'),
34
+ cc: z.string().optional().describe('抄送邮箱地址,多个用逗号分隔'),
35
+ bcc: z.string().optional().describe('密送邮箱地址,多个用逗号分隔'),
36
+ isHtml: z.boolean().optional().describe('是否为HTML格式,默认false')
37
+ }),
38
+ execute: async (args) => {
39
+ return this._sendEmail(args)
40
+ }
41
+ })
42
+
43
+ // 读取邮件工具
44
+ this._framework.registerTool({
45
+ name: 'email_read',
46
+ description: '读取电子邮件(支持IMAP协议)',
47
+ inputSchema: z.object({
48
+ host: z.string().optional().describe('IMAP服务器地址,默认从配置获取'),
49
+ port: z.number().optional().describe('IMAP端口,默认993'),
50
+ user: z.string().optional().describe('邮箱用户名,默认从配置获取'),
51
+ password: z.string().optional().describe('邮箱密码,默认从配置获取'),
52
+ box: z.string().optional().describe('邮箱文件夹,默认INBOX'),
53
+ limit: z.number().optional().describe('读取邮件数量,默认10'),
54
+ unreadOnly: z.boolean().optional().describe('仅读取未读邮件,默认false'),
55
+ searchCriteria: z.string().optional().describe('搜索条件,如 "UNSEEN", "FROM sender@example.com"')
56
+ }),
57
+ execute: async (args) => {
58
+ return this._readEmails(args)
59
+ }
60
+ })
61
+
62
+ // 获取未读邮件数量
63
+ this._framework.registerTool({
64
+ name: 'email_unread_count',
65
+ description: '获取邮箱未读邮件数量',
66
+ inputSchema: z.object({
67
+ host: z.string().optional().describe('IMAP服务器地址'),
68
+ port: z.number().optional().describe('IMAP端口'),
69
+ user: z.string().optional().describe('邮箱用户名'),
70
+ password: z.string().optional().describe('邮箱密码'),
71
+ box: z.string().optional().describe('邮箱文件夹,默认INBOX')
72
+ }),
73
+ execute: async (args) => {
74
+ return this._getUnreadCount(args)
75
+ }
76
+ })
77
+
78
+ // 标记邮件为已读
79
+ this._framework.registerTool({
80
+ name: 'email_mark_read',
81
+ description: '标记邮件为已读',
82
+ inputSchema: z.object({
83
+ host: z.string().optional().describe('IMAP服务器地址'),
84
+ port: z.number().optional().describe('IMAP端口'),
85
+ user: z.string().optional().describe('邮箱用户名'),
86
+ password: z.string().optional().describe('邮箱密码'),
87
+ messageId: z.string().describe('邮件UID或序列号')
88
+ }),
89
+ execute: async (args) => {
90
+ return this._markAsRead(args)
91
+ }
92
+ })
93
+
94
+ // 配置邮箱连接
95
+ this._framework.registerTool({
96
+ name: 'email_configure',
97
+ description: '配置邮箱连接参数',
98
+ inputSchema: z.object({
99
+ smtp_host: z.string().optional().describe('SMTP服务器地址'),
100
+ smtp_port: z.number().optional().describe('SMTP端口'),
101
+ smtp_secure: z.boolean().optional().describe('是否使用SSL/TLS'),
102
+ smtp_user: z.string().optional().describe('SMTP用户名'),
103
+ smtp_pass: z.string().optional().describe('SMTP密码'),
104
+ imap_host: z.string().optional().describe('IMAP服务器地址'),
105
+ imap_port: z.number().optional().describe('IMAP端口'),
106
+ imap_user: z.string().optional().describe('IMAP用户名'),
107
+ imap_pass: z.string().optional().describe('IMAP密码'),
108
+ from_email: z.string().optional().describe('默认发件人地址')
109
+ }),
110
+ execute: async (args) => {
111
+ Object.assign(this.config, args)
112
+ return {
113
+ success: true,
114
+ message: '邮箱配置已更新',
115
+ config: this.config
116
+ }
117
+ }
118
+ })
119
+ }
120
+
121
+ async _sendEmail(args) {
122
+ try {
123
+ const nodemailer = require('nodemailer')
124
+
125
+ const smtpConfig = {
126
+ host: this.config.smtp_host || process.env.SMTP_HOST || 'smtp.gmail.com',
127
+ port: this.config.smtp_port || process.env.SMTP_PORT || 587,
128
+ secure: (this.config.smtp_secure || process.env.SMTP_SECURE || 'false') === 'true',
129
+ auth: {
130
+ user: this.config.smtp_user || process.env.SMTP_USER,
131
+ pass: this.config.smtp_pass || process.env.SMTP_PASS
132
+ }
133
+ }
134
+
135
+ const transporter = nodemailer.createTransport(smtpConfig)
136
+
137
+ const mailOptions = {
138
+ from: this.config.from_email || process.env.FROM_EMAIL || smtpConfig.auth.user,
139
+ to: args.to,
140
+ subject: args.subject,
141
+ text: args.isHtml ? undefined : args.body,
142
+ html: args.isHtml ? args.body : undefined,
143
+ cc: args.cc,
144
+ bcc: args.bcc
145
+ }
146
+
147
+ const info = await transporter.sendMail(mailOptions)
148
+ return {
149
+ success: true,
150
+ message: '邮件发送成功',
151
+ messageId: info.messageId,
152
+ accepted: info.accepted,
153
+ rejected: info.rejected
154
+ }
155
+ } catch (error) {
156
+ return {
157
+ success: false,
158
+ error: error.message,
159
+ details: '请检查SMTP配置是否正确'
160
+ }
161
+ }
162
+ }
163
+
164
+ async _readEmails(args) {
165
+ try {
166
+ const Imap = require('imap')
167
+ const { simpleParser } = require('mailparser')
168
+
169
+ const imapConfig = {
170
+ user: args.user || this.config.imap_user || process.env.IMAP_USER,
171
+ password: args.password || this.config.imap_pass || process.env.IMAP_PASS,
172
+ host: args.host || this.config.imap_host || process.env.IMAP_HOST,
173
+ port: args.port || this.config.imap_port || 993,
174
+ tls: true,
175
+ tlsOptions: { rejectUnauthorized: false }
176
+ }
177
+
178
+ const box = args.box || 'INBOX'
179
+ const limit = args.limit || 10
180
+ const unreadOnly = args.unreadOnly || false
181
+
182
+ const emails = await this._fetchEmails(imapConfig, box, limit, unreadOnly, args.searchCriteria)
183
+
184
+ return {
185
+ success: true,
186
+ count: emails.length,
187
+ emails: emails
188
+ }
189
+ } catch (error) {
190
+ return {
191
+ success: false,
192
+ error: error.message,
193
+ details: '请检查IMAP配置是否正确'
194
+ }
195
+ }
196
+ }
197
+
198
+ async _getUnreadCount(args) {
199
+ try {
200
+ const Imap = require('imap')
201
+
202
+ const imapConfig = {
203
+ user: args.user || this.config.imap_user || process.env.IMAP_USER,
204
+ password: args.password || this.config.imap_pass || process.env.IMAP_PASS,
205
+ host: args.host || this.config.imap_host || process.env.IMAP_HOST,
206
+ port: args.port || this.config.imap_port || 993,
207
+ tls: true,
208
+ tlsOptions: { rejectUnauthorized: false }
209
+ }
210
+
211
+ const box = args.box || 'INBOX'
212
+ const count = await this._fetchUnreadCount(imapConfig, box)
213
+
214
+ return {
215
+ success: true,
216
+ unreadCount: count,
217
+ box: box
218
+ }
219
+ } catch (error) {
220
+ return {
221
+ success: false,
222
+ error: error.message
223
+ }
224
+ }
225
+ }
226
+
227
+ async _markAsRead(args) {
228
+ try {
229
+ const Imap = require('imap')
230
+
231
+ const imapConfig = {
232
+ user: args.user || this.config.imap_user || process.env.IMAP_USER,
233
+ password: args.password || this.config.imap_pass || process.env.IMAP_PASS,
234
+ host: args.host || this.config.imap_host || process.env.IMAP_HOST,
235
+ port: args.port || this.config.imap_port || 993,
236
+ tls: true,
237
+ tlsOptions: { rejectUnauthorized: false }
238
+ }
239
+
240
+ await this._markEmailAsRead(imapConfig, args.messageId)
241
+
242
+ return {
243
+ success: true,
244
+ message: '邮件已标记为已读'
245
+ }
246
+ } catch (error) {
247
+ return {
248
+ success: false,
249
+ error: error.message
250
+ }
251
+ }
252
+ }
253
+
254
+ _fetchEmails(imapConfig, box, limit, unreadOnly, searchCriteria) {
255
+ return new Promise((resolve, reject) => {
256
+ const imap = new Imap(imapConfig)
257
+ const emails = []
258
+
259
+ imap.once('ready', () => {
260
+ imap.openBox(box, true, (err) => {
261
+ if (err) {
262
+ imap.end()
263
+ return reject(err)
264
+ }
265
+
266
+ let searchFilter = searchCriteria ? [searchCriteria] : ['ALL']
267
+ if (unreadOnly) {
268
+ searchFilter = ['UNSEEN']
269
+ }
270
+
271
+ imap.search(searchFilter, (err, results) => {
272
+ if (err) {
273
+ imap.end()
274
+ return reject(err)
275
+ }
276
+
277
+ if (results.length === 0) {
278
+ imap.end()
279
+ return resolve([])
280
+ }
281
+
282
+ const fetchIds = results.slice(-limit)
283
+
284
+ const f = imap.fetch(fetchIds, {
285
+ bodies: 'HEADER.FIELDS (FROM TO SUBJECT DATE)',
286
+ struct: true
287
+ })
288
+
289
+ f.on('message', (msg, seqno) => {
290
+ const email = { seqno }
291
+
292
+ msg.on('body', (stream) => {
293
+ let buffer = ''
294
+ stream.on('data', chunk => buffer += chunk.toString('utf8'))
295
+ stream.once('end', () => {
296
+ const headers = Imap.parseHeader(buffer)
297
+ email.from = headers.from ? headers.from[0] : ''
298
+ email.to = headers.to ? headers.to[0] : ''
299
+ email.subject = headers.subject ? headers.subject[0] : ''
300
+ email.date = headers.date ? headers.date[0] : ''
301
+ })
302
+ })
303
+
304
+ msg.once('attributes', (attrs) => {
305
+ email.uid = attrs.uid
306
+ email.id = attrs.uid
307
+ email.flags = attrs.flags
308
+ })
309
+
310
+ msg.once('end', () => {
311
+ emails.push(email)
312
+ })
313
+ })
314
+
315
+ f.once('error', err => {
316
+ imap.end()
317
+ reject(err)
318
+ })
319
+
320
+ f.once('end', () => {
321
+ imap.end()
322
+ resolve(emails)
323
+ })
324
+ })
325
+ })
326
+ })
327
+
328
+ imap.once('error', err => reject(err))
329
+ imap.connect()
330
+ })
331
+ }
332
+
333
+ _fetchUnreadCount(imapConfig, box) {
334
+ return new Promise((resolve, reject) => {
335
+ const imap = new Imap(imapConfig)
336
+
337
+ imap.once('ready', () => {
338
+ imap.openBox(box, true, (err, box) => {
339
+ if (err) {
340
+ imap.end()
341
+ return reject(err)
342
+ }
343
+ const unreadCount = box.messages.unread
344
+ imap.end()
345
+ resolve(unreadCount)
346
+ })
347
+ })
348
+
349
+ imap.once('error', err => reject(err))
350
+ imap.connect()
351
+ })
352
+ }
353
+
354
+ _markEmailAsRead(imapConfig, messageId) {
355
+ return new Promise((resolve, reject) => {
356
+ const imap = new Imap(imapConfig)
357
+
358
+ imap.once('ready', () => {
359
+ imap.openBox('INBOX', true, (err) => {
360
+ if (err) {
361
+ imap.end()
362
+ return reject(err)
363
+ }
364
+ imap.addFlags(messageId, '\\Seen', (err) => {
365
+ imap.end()
366
+ if (err) reject(err)
367
+ else resolve()
368
+ })
369
+ })
370
+ })
371
+
372
+ imap.once('error', err => reject(err))
373
+ imap.connect()
374
+ })
375
+ }
376
+
377
+ uninstall(framework) {
378
+ // 清理资源
379
+ }
380
+ }
381
+
382
+ module.exports = { EmailPlugin }
@@ -366,9 +366,12 @@ module.exports = function(Plugin) {
366
366
 
367
367
  const savedMsg = `收到图片${caption ? ': ' + caption : ''}\n已保存至: ${filePath}`
368
368
  await this._sendMessage(chatId, savedMsg, msg.message_id)
369
-
369
+ if(caption){
370
+ const message=`图片:${filePath}, ${caption}`
371
+ await this._processChat(chatId, message, msg.message_id)
372
+ }
370
373
  // TODO: 调用 AI 视觉能力分析图片
371
- // await this._analyzeImage(chatId, filePath, caption)
374
+ // await this._analyzeImage(chatId, filePath, capti on)
372
375
  } catch (err) {
373
376
  console.error('[Telegram] Failed to save photo:', err.message)
374
377
  await this._sendMessage(chatId, '图片保存失败: ' + err.message, msg.message_id)
@@ -399,6 +402,10 @@ module.exports = function(Plugin) {
399
402
 
400
403
  const savedMsg = `收到文件: ${fileName}\n已保存至: ${filePath}${caption ? '\n\n说明: ' + caption : ''}`
401
404
  await this._sendMessage(chatId, savedMsg, msg.message_id)
405
+ if(caption){
406
+ const message=`文件:${filePath}, ${caption}`
407
+ await this._processChat(chatId, message, msg.message_id)
408
+ }
402
409
  } catch (err) {
403
410
  console.error('[Telegram] Failed to save document:', err.message)
404
411
  await this._sendMessage(chatId, '文件保存失败: ' + err.message, msg.message_id)
@@ -435,7 +442,7 @@ module.exports = function(Plugin) {
435
442
  if (savedPath !== finalPath) {
436
443
  fs.renameSync(savedPath, finalPath)
437
444
  }
438
- resolve(finalPath)
445
+ resolve(filePath)
439
446
  })
440
447
  .catch(reject)
441
448
  })
@@ -501,6 +508,15 @@ module.exports = function(Plugin) {
501
508
  }
502
509
  }
503
510
 
511
+ /**
512
+ * 启动 Bot(用于重新启用时)
513
+ */
514
+ start(framework) {
515
+ if (this.config.botToken && !this._bot) {
516
+ this._initBot()
517
+ }
518
+ }
519
+
504
520
  uninstall(framework) {
505
521
  this.stopBot()
506
522
  this._sessions.clear()
@@ -78,6 +78,40 @@ class ToolsPlugin extends Plugin {
78
78
  }
79
79
  })
80
80
 
81
+ // 启用插件工具
82
+ framework.registerTool({
83
+ name: 'enable_plugin',
84
+ description: '启用指定插件',
85
+ inputSchema: z.object({
86
+ name: z.string().describe('插件名称')
87
+ }),
88
+ execute: async (args) => {
89
+ try {
90
+ await framework.enablePlugin(args.name)
91
+ return { success: true, message: `插件 '${args.name}' 已启用` }
92
+ } catch (err) {
93
+ return { success: false, error: err.message }
94
+ }
95
+ }
96
+ })
97
+
98
+ // 禁用插件工具
99
+ framework.registerTool({
100
+ name: 'disable_plugin',
101
+ description: '禁用指定插件',
102
+ inputSchema: z.object({
103
+ name: z.string().describe('插件名称')
104
+ }),
105
+ execute: async (args) => {
106
+ try {
107
+ await framework.disablePlugin(args.name)
108
+ return { success: true, message: `插件 '${args.name}' 已禁用` }
109
+ } catch (err) {
110
+ return { success: false, error: err.message }
111
+ }
112
+ }
113
+ })
114
+
81
115
  // 获取工具列表
82
116
  framework.registerTool({
83
117
  name: 'list_tools',
@@ -94,6 +128,43 @@ class ToolsPlugin extends Plugin {
94
128
  }
95
129
  }
96
130
  })
131
+
132
+ // 获取插件配置
133
+ framework.registerTool({
134
+ name: 'get_plugin_config',
135
+ description: '获取指定插件的当前配置',
136
+ inputSchema: z.object({
137
+ name: z.string().describe('插件名称')
138
+ }),
139
+ execute: async (args) => {
140
+ const plugin = framework.pluginManager.get(args.name)
141
+ if (!plugin) {
142
+ return { success: false, error: `Plugin '${args.name}' not found` }
143
+ }
144
+ return {
145
+ success: true,
146
+ config: plugin.config || {}
147
+ }
148
+ }
149
+ })
150
+
151
+ // 更新插件配置
152
+ framework.registerTool({
153
+ name: 'update_plugin_config',
154
+ description: '更新指定插件的配置,会合并到现有配置并持久化保存',
155
+ inputSchema: z.object({
156
+ name: z.string().describe('插件名称'),
157
+ config: z.record(z.any()).describe('要更新的配置(会合并到现有配置)')
158
+ }),
159
+ execute: async (args) => {
160
+ try {
161
+ const newConfig = framework.updatePluginConfig(args.name, args.config)
162
+ return { success: true, message: `插件 '${args.name}' 配置已更新`, config: newConfig }
163
+ } catch (err) {
164
+ return { success: false, error: err.message }
165
+ }
166
+ }
167
+ })
97
168
  }
98
169
 
99
170
  reload(framework) {
@@ -107,6 +178,10 @@ class ToolsPlugin extends Plugin {
107
178
  framework.toolRegistry.unregister('reload_plugins')
108
179
  framework.toolRegistry.unregister('list_plugins')
109
180
  framework.toolRegistry.unregister('list_tools')
181
+ framework.toolRegistry.unregister('enable_plugin')
182
+ framework.toolRegistry.unregister('disable_plugin')
183
+ framework.toolRegistry.unregister('get_plugin_config')
184
+ framework.toolRegistry.unregister('update_plugin_config')
110
185
  this._framework = null
111
186
  }
112
187
  }
@@ -89,6 +89,33 @@ class Framework extends EventEmitter {
89
89
  return this
90
90
  }
91
91
 
92
+ /**
93
+ * 启用插件
94
+ * @param {string} name - 插件名称
95
+ */
96
+ async enablePlugin(name) {
97
+ await this.pluginManager.enable(name)
98
+ return this
99
+ }
100
+
101
+ /**
102
+ * 禁用插件
103
+ * @param {string} name - 插件名称
104
+ */
105
+ async disablePlugin(name) {
106
+ await this.pluginManager.disable(name)
107
+ return this
108
+ }
109
+
110
+ /**
111
+ * 更新插件配置
112
+ * @param {string} name - 插件名称
113
+ * @param {Object} config - 新配置
114
+ */
115
+ updatePluginConfig(name, config) {
116
+ return this.pluginManager.updatePluginConfig(name, config)
117
+ }
118
+
92
119
  /**
93
120
  * 注册工具
94
121
  * @param {Object} tool - 工具定义
@@ -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`)
@@ -270,11 +336,11 @@ class PluginManager {
270
336
  }
271
337
 
272
338
  /**
273
- * 获取所有已加载插件
339
+ * 获取所有已加载且已启用的插件
274
340
  */
275
341
  getAll() {
276
342
  return Array.from(this._plugins.values())
277
- .filter(e => e.status === 'loaded')
343
+ .filter(e => e.status === 'loaded' && e.enabled)
278
344
  .map(e => ({ name: e.instance.name, instance: e.instance }))
279
345
  }
280
346
 
@@ -294,12 +360,119 @@ class PluginManager {
294
360
  return this._plugins.get(name)?.status === 'loaded'
295
361
  }
296
362
 
363
+ /**
364
+ * 检查插件是否启用
365
+ * @param {string} name - 插件名称
366
+ */
367
+ isEnabled(name) {
368
+ return this._plugins.get(name)?.enabled === true
369
+ }
370
+
371
+ /**
372
+ * 启用插件
373
+ * @param {string} name - 插件名称
374
+ */
375
+ async enable(name) {
376
+ const entry = this._plugins.get(name)
377
+ if (!entry) {
378
+ throw new Error(`Plugin '${name}' not found`)
379
+ }
380
+
381
+ if (entry.enabled) {
382
+ console.log(`[PluginManager] Plugin '${name}' already enabled`)
383
+ return
384
+ }
385
+
386
+ entry.enabled = true
387
+
388
+ // 如果插件已加载但未启动,启动它
389
+ if (entry.status === 'loaded' && !entry.instance._started) {
390
+ try {
391
+ if (typeof entry.instance.start === 'function') {
392
+ await entry.instance.start(this.framework)
393
+ entry.instance._started = true
394
+ }
395
+ } catch (err) {
396
+ console.error(`[PluginManager] Start failed for '${name}':`, err.message)
397
+ }
398
+ }
399
+
400
+ this.framework.emit('plugin:enabled', entry.instance)
401
+ this._saveState()
402
+ console.log(`[PluginManager] Plugin '${name}' enabled`)
403
+ }
404
+
405
+ /**
406
+ * 禁用插件
407
+ * @param {string} name - 插件名称
408
+ */
409
+ async disable(name) {
410
+ const entry = this._plugins.get(name)
411
+ if (!entry) {
412
+ throw new Error(`Plugin '${name}' not found`)
413
+ }
414
+
415
+ if (!entry.enabled) {
416
+ console.log(`[PluginManager] Plugin '${name}' already disabled`)
417
+ return
418
+ }
419
+
420
+ entry.enabled = false
421
+
422
+ // 如果插件正在运行,停止它
423
+ if (entry.instance._started) {
424
+ try {
425
+ // 优先调用 stop 方法,其次调用 stopBot 方法
426
+ if (typeof entry.instance.stop === 'function') {
427
+ await entry.instance.stop()
428
+ } else if (typeof entry.instance.stopBot === 'function') {
429
+ await entry.instance.stopBot()
430
+ }
431
+ if (typeof entry.instance.uninstall === 'function') {
432
+ await entry.instance.uninstall(this.framework)
433
+ }
434
+ entry.instance._started = false
435
+ } catch (err) {
436
+ console.error(`[PluginManager] Stop failed for '${name}':`, err.message)
437
+ }
438
+ }
439
+
440
+ this.framework.emit('plugin:disabled', entry.instance)
441
+ this._saveState()
442
+ console.log(`[PluginManager] Plugin '${name}' disabled`)
443
+ }
444
+
445
+ /**
446
+ * 更新插件配置
447
+ * @param {string} name - 插件名称
448
+ * @param {Object} config - 新配置(会合并到现有配置)
449
+ */
450
+ updatePluginConfig(name, config) {
451
+ const entry = this._plugins.get(name)
452
+ if (!entry) {
453
+ throw new Error(`Plugin '${name}' not found`)
454
+ }
455
+
456
+ if (!entry.instance.config) {
457
+ entry.instance.config = {}
458
+ }
459
+
460
+ // 合并配置
461
+ entry.instance.config = { ...entry.instance.config, ...config }
462
+
463
+ // 保存状态
464
+ this._saveState()
465
+ console.log(`[PluginManager] Plugin '${name}' config updated`)
466
+
467
+ return entry.instance.config
468
+ }
469
+
297
470
  /**
298
471
  * 启动所有已加载但未启动的插件(按优先级排序)
299
472
  */
300
473
  async startAll() {
301
474
  const entries = Array.from(this._plugins.values())
302
- .filter(e => e.status === 'loaded')
475
+ .filter(e => e.status === 'loaded' && e.enabled) // 只启动已启用且已加载的插件
303
476
  .sort((a, b) => (a.instance.priority || 100) - (b.instance.priority || 100))
304
477
 
305
478
  for (const entry of entries) {