foliko 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 }
@@ -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 包到 .agent 目录'
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 包到 .agent 目录,用于解决插件依赖缺失问题',
38
+ description: '安装 npm 包到指定目录',
39
39
  inputSchema: z.object({
40
- package: z.string().describe('包名,如 lodashlodash@4.17.21')
40
+ package: z.string().optional().describe('包名,如 lodashlodash@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
- return this._installPackage(packageName)
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
- _installPackage(packageName) {
76
+ /**
77
+ * 安装单个包到指定目录
78
+ * @param {string} packageName - 包名
79
+ * @param {string|null} targetPath - 目标目录,默认为 .agent
80
+ */
81
+ _installPackage(packageName, targetPath = null) {
52
82
  try {
53
- console.log(`[InstallPlugin] Installing ${packageName} to ${this._nodeModulesDir}...`)
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
- // --prefix 确保安装到 .agent/node_modules
57
- execSync(`npm install ${packageName} --prefix "${this._agentDir}"`, {
95
+ execSync(`npm install ${packageName} --prefix "${installPath}"`, {
58
96
  stdio: 'inherit',
59
- cwd: this._agentDir
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 }
@@ -508,6 +508,15 @@ module.exports = function(Plugin) {
508
508
  }
509
509
  }
510
510
 
511
+ /**
512
+ * 启动 Bot(用于重新启用时)
513
+ */
514
+ start(framework) {
515
+ if (this.config.botToken && !this._bot) {
516
+ this._initBot()
517
+ }
518
+ }
519
+
511
520
  uninstall(framework) {
512
521
  this.stopBot()
513
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
  }