foliko 1.0.54 → 1.0.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.0.54",
3
+ "version": "1.0.55",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -202,7 +202,8 @@ class GoalManager {
202
202
  const goal = this._goals.get(goalId)
203
203
  if (!goal) return
204
204
  goal.eventsReceived.push({
205
- event: event.event || event.type,
205
+ event: event.type, // "email:received"
206
+ data: event.data, // the actual event data (contains email object)
206
207
  timestamp: new Date()
207
208
  })
208
209
  this._persist()
@@ -250,13 +251,22 @@ class EventWatcher {
250
251
  }
251
252
 
252
253
  _handleEvent(type, data) {
254
+ // Check active goals
253
255
  const activeGoals = this._goalManager.getActiveGoals()
254
256
  for (const goal of activeGoals) {
255
- // Check if goal's conditions match this event
256
257
  if (this._isRelevantToGoal(goal, type, data)) {
257
258
  this._goalManager.addEventToGoal(goal.id, { type, data })
258
259
  }
259
260
  }
261
+
262
+ // Also check pending goals - activate if conditions match
263
+ const pendingGoals = this._goalManager.getPendingGoals()
264
+ for (const goal of pendingGoals) {
265
+ if (this._isRelevantToGoal(goal, type, data)) {
266
+ this._goalManager.activateGoal(goal.id)
267
+ this._goalManager.addEventToGoal(goal.id, { type, data })
268
+ }
269
+ }
260
270
  }
261
271
 
262
272
  _isRelevantToGoal(goal, eventType, data) {
@@ -425,7 +435,6 @@ class ExplorerLoop {
425
435
  }
426
436
 
427
437
  this._tickCount++
428
- this._lastActionTime = Date.now()
429
438
 
430
439
  try {
431
440
  await this._processGoals()
@@ -450,10 +459,19 @@ class ExplorerLoop {
450
459
  // Check if goal has unprocessed events
451
460
  const hasNewEvents = goal.eventsReceived && goal.eventsReceived.length > 0
452
461
 
462
+ // Check if goal is event-driven
463
+ const isEventDriven = goal.conditions && goal.conditions.events && goal.conditions.events.length > 0
464
+
453
465
  // Get next action
454
466
  if (goal.actions && goal.actions.length > 0) {
455
467
  const action = goal.actions[0] // Simple: take first action
456
468
 
469
+ // For event-driven goals, only execute if there are new events
470
+ if (isEventDriven && !hasNewEvents) {
471
+ // Skip execution but don't fail - just wait for events
472
+ continue
473
+ }
474
+
457
475
  // Loop detection (only for non-event-driven actions)
458
476
  if (!hasNewEvents && this._reflector.checkLoopDetection(goal, action.id)) {
459
477
  this._addActivity('goal_failed', { goalId: goal.id, reason: 'Loop detected' })
@@ -467,19 +485,32 @@ class ExplorerLoop {
467
485
  const outcome = this._reflector.evaluateOutcome(goal, result)
468
486
 
469
487
  if (outcome.goalComplete) {
470
- this._goalManager.completeGoal(goal.id)
471
- this._addActivity('goal_completed', { goalId: goal.id, attempts: goal.attempts })
488
+ // For event-driven goals, don't complete - reset and wait for more events
489
+ if (isEventDriven) {
490
+ goal.eventsReceived = []
491
+ goal.attempts = 0
492
+ this._goalManager.updateGoal(goal.id, { eventsReceived: [], attempts: 0 })
493
+ this._addActivity('goal_reset', { goalId: goal.id, reason: 'Event-driven, waiting for more events' })
494
+ } else {
495
+ this._goalManager.completeGoal(goal.id)
496
+ this._addActivity('goal_completed', { goalId: goal.id, attempts: goal.attempts })
497
+ }
472
498
  } else if (!outcome.shouldContinue) {
473
499
  this._goalManager.failGoal(goal.id, 'Max attempts reached')
474
500
  this._addActivity('goal_failed', { goalId: goal.id, reason: 'Max attempts' })
475
501
  } else if (outcome.actionTaken) {
476
- // Remove completed action from queue
477
- goal.actions.shift()
502
+ // For event-driven goals, keep actions in queue for continuous processing
503
+ // Only shift if this is a one-time goal (no pending events)
504
+ if (!isEventDriven) {
505
+ goal.actions.shift()
506
+ }
478
507
  this._addActivity('action_executed', { goalId: goal.id, action: action.id, hasEvents: hasNewEvents })
508
+ // Update last action time for cooldown
509
+ this._lastActionTime = Date.now()
479
510
  }
480
511
 
481
512
  // Clear processed events after action is taken
482
- if (hasNewEvents && outcome.actionTaken) {
513
+ if (hasNewEvents) {
483
514
  goal.eventsReceived = []
484
515
  this._goalManager.updateGoal(goal.id, { eventsReceived: [] })
485
516
  }
@@ -583,7 +614,16 @@ class AmbientAgentPlugin extends Plugin {
583
614
  super()
584
615
  this.name = 'ambient'
585
616
  this.version = '1.0.0'
586
- this.description = 'Ambient Agent - continuous background agent for autonomous monitoring and actions'
617
+ this.description = `Ambient Agent - 持续运行的后台 Agent,用于主动监控和自动操作
618
+ 支持监听的事件:
619
+ - email:received - 收到新邮件(需配合 email_watch 启动监控)
620
+ - webhook:received - 收到 webhook 请求
621
+ - scheduler:reminder - 定时提醒触发
622
+ - scheduler:task_completed - 定时任务完成
623
+ - scheduler:task_failed - 定时任务失败
624
+ - think:thought_completed - 思考完成
625
+ - tool:result - 工具执行结果
626
+ - agent:message - Agent 消息`
587
627
  this.priority = 18
588
628
  this.system = true
589
629
 
@@ -618,7 +658,6 @@ class AmbientAgentPlugin extends Plugin {
618
658
 
619
659
  start(framework) {
620
660
  if (!this.config.enabled) {
621
- console.log('[Ambient] Plugin disabled, skipping start')
622
661
  return this
623
662
  }
624
663
 
@@ -643,7 +682,6 @@ class AmbientAgentPlugin extends Plugin {
643
682
  // Register tools
644
683
  this._registerTools(framework)
645
684
 
646
- console.log('[Ambient] Plugin started')
647
685
  return this
648
686
  }
649
687
 
@@ -864,7 +902,6 @@ class AmbientAgentPlugin extends Plugin {
864
902
  const waitInterval = 500
865
903
 
866
904
  while (agent._status === 'busy' && attempts < maxWaitAttempts) {
867
- console.log('[Ambient] Agent busy, waiting...')
868
905
  await new Promise(resolve => setTimeout(resolve, waitInterval))
869
906
  attempts++
870
907
  }
package/plugins/email.js CHANGED
@@ -11,11 +11,12 @@ class EmailPlugin extends Plugin {
11
11
  super()
12
12
  this.name = 'email'
13
13
  this.version = '1.1.0'
14
- this.description = `邮件收发插件 - 支持读取和发送电子邮件、监控新邮件
14
+ this.description = `邮件收发插件 - 支持读取和发送电子邮件、监控新邮件、自动回复,配置已经预设,不用获取配置
15
15
  功能:
16
16
  - 发送邮件支持附件(本地文件、远程URL、Base64)
17
17
  - 读取邮件(IMAP协议)
18
18
  - 监控新邮件(支持IMAP IDLE推送和定时轮询)
19
+ - 自动回复邮件(AI分析内容并发送回复,无需用户确认)
19
20
  发送邮件支持附件:
20
21
  - attachments.path: 本地文件路径
21
22
  - attachments.url: 远程图片/文件URL(自动下载)
@@ -29,6 +30,7 @@ class EmailPlugin extends Plugin {
29
30
  this._lastSeenUid = null
30
31
  this._watchConfig = null
31
32
  this._watchEnabled = false
33
+ this._checking = false // 防止并发检查
32
34
  }
33
35
 
34
36
  install(framework) {
@@ -39,10 +41,10 @@ class EmailPlugin extends Plugin {
39
41
 
40
42
  start(framework) {
41
43
  // 自动启动邮件监控(如果配置了 IMAP 且尚未运行)
42
- // if (this._watchEnabled) {
43
- // console.log('[Email] Email watch already running, skipping auto-start')
44
- // return this
45
- // }
44
+ if (this._watchEnabled) {
45
+ console.log('[Email] Email watch already running, skipping auto-start')
46
+ return this
47
+ }
46
48
 
47
49
  // if (process.env.IMAP_USER && process.env.IMAP_PASS) {
48
50
  // console.log('[Email] Auto-starting email watch...')
@@ -149,27 +151,27 @@ class EmailPlugin extends Plugin {
149
151
  })
150
152
 
151
153
  // 配置邮箱连接
152
- this._framework.registerTool({
153
- name: 'email_configure',
154
- description: '查看邮箱配置(实际配置通过环境变量设置)',
155
- inputSchema: z.object({}),
156
- execute: async () => {
157
- return {
158
- success: true,
159
- message: '邮箱配置通过环境变量设置',
160
- config: {
161
- smtp_host: process.env.SMTP_HOST || 'smtp.gmail.com',
162
- smtp_port: process.env.SMTP_PORT || 587,
163
- smtp_secure: process.env.SMTP_SECURE || 'false',
164
- imap_host: process.env.IMAP_HOST || 'imap.gmail.com',
165
- imap_port: process.env.IMAP_PORT || 993,
166
- from_email: process.env.FROM_EMAIL || '(未设置)',
167
- client_name: process.env.IMAP_CLIENT_NAME || 'FolikoAgent',
168
- client_version: process.env.IMAP_CLIENT_VERSION || '1.0.0'
169
- }
170
- }
171
- }
172
- })
154
+ // this._framework.registerTool({
155
+ // name: 'email_configure',
156
+ // description: '查看邮箱配置(实际配置通过环境变量设置)',
157
+ // inputSchema: z.object({}),
158
+ // execute: async () => {
159
+ // return {
160
+ // success: true,
161
+ // message: '邮箱配置通过环境变量设置',
162
+ // config: {
163
+ // smtp_host: process.env.SMTP_HOST || 'smtp.gmail.com',
164
+ // smtp_port: process.env.SMTP_PORT || 587,
165
+ // smtp_secure: process.env.SMTP_SECURE || 'false',
166
+ // imap_host: process.env.IMAP_HOST || 'imap.gmail.com',
167
+ // imap_port: process.env.IMAP_PORT || 993,
168
+ // from_email: process.env.FROM_EMAIL || '(未设置)',
169
+ // client_name: process.env.IMAP_CLIENT_NAME || 'FolikoAgent',
170
+ // client_version: process.env.IMAP_CLIENT_VERSION || '1.0.0'
171
+ // }
172
+ // }
173
+ // }
174
+ // })
173
175
 
174
176
  // 监控新邮件
175
177
  this._framework.registerTool({
@@ -178,16 +180,152 @@ class EmailPlugin extends Plugin {
178
180
  inputSchema: z.object({
179
181
  action: z.enum(['start', 'stop', 'status']).describe('操作:启动监控、停止监控、查看状态'),
180
182
  interval: z.number().optional().describe('轮询间隔(秒),默认60秒,适用于不支持IDLE的服务器'),
181
- host: z.string().optional().describe('IMAP服务器地址'),
182
- port: z.number().optional().describe('IMAP端口'),
183
- user: z.string().optional().describe('邮箱用户名'),
184
- password: z.string().optional().describe('邮箱密码'),
183
+ // host: z.string().optional().describe('IMAP服务器地址'),
184
+ // port: z.number().optional().describe('IMAP端口'),
185
+ // user: z.string().optional().describe('邮箱用户名'),
186
+ // password: z.string().optional().describe('邮箱密码'),
185
187
  box: z.string().optional().describe('邮箱文件夹,默认INBOX')
186
188
  }),
187
189
  execute: async (args) => {
188
- return this._handleEmailWatch(args)
190
+
191
+ return this._handleEmailWatch({
192
+ ...args,
193
+ host: process.env.IMAP_HOST,
194
+ port: parseInt(process.env.IMAP_PORT) || 993,
195
+ user: process.env.IMAP_USER,
196
+ password: process.env.IMAP_PASS,
197
+ })
189
198
  }
190
199
  })
200
+
201
+ // 自动回复邮件
202
+ this._framework.registerTool({
203
+ name: 'email_auto_reply',
204
+ description: '自动分析邮件内容并发送回复(无需用户确认)',
205
+ inputSchema: z.object({
206
+ to: z.string().describe('收件人邮箱地址'),
207
+ subject: z.string().describe('原始邮件主题'),
208
+ body: z.string().describe('原始邮件内容'),
209
+ from: z.string().optional().describe('发件人邮箱地址(可选)')
210
+ }),
211
+ execute: async (args) => {
212
+ return this._handleAutoReply(args)
213
+ }
214
+ })
215
+ }
216
+
217
+ /**
218
+ * 处理自动回复
219
+ */
220
+ async _handleAutoReply(args) {
221
+ // 支持两种模式:
222
+ // 1. 直接传参数: { to, subject, body, from }
223
+ // 2. 从 _event 提取: { _event } (来自 ambient agent 的事件触发)
224
+ let { to, subject, body, from, _event } = args
225
+
226
+ // 如果没有直接参数,尝试从 _event 提取
227
+ if (!to && !subject && !body && _event) {
228
+ const email = _event.data?.email || _event.email || {}
229
+ from = email.from?.text || email.from || ''
230
+ to = email.to?.text || email.to || ''
231
+ subject = email.subject || ''
232
+ body = email.text || email.body || ''
233
+ }
234
+
235
+ // 检查必要参数
236
+ if (!from && !to) {
237
+ return { success: false, error: '缺少收件人地址' }
238
+ }
239
+ if (!body) {
240
+ return { success: false, error: '缺少邮件内容' }
241
+ }
242
+
243
+ try {
244
+ // 获取活跃的 Agent
245
+ const agent = this._getActiveAgent()
246
+ if (!agent) {
247
+ return { success: false, error: 'No active agent found' }
248
+ }
249
+
250
+ // 构建提示让 LLM 生成回复
251
+ const prompt = `你是一封邮件自动回复助手。请根据以下邮件内容,生成一封专业的回复邮件。
252
+
253
+ 【原始邮件】
254
+ 发件人: ${from || to}
255
+ 主题: ${subject}
256
+ 内容:
257
+ ${body}
258
+
259
+ 【要求】
260
+ 1. 回复内容要针对邮件中的问题或内容进行回复
261
+ 2. 语言要专业、礼貌、简洁
262
+ 3. 只输出邮件正文内容,不要额外解释
263
+ 4. 回复语言应与原邮件一致(如果原邮件是中文,则用中文回复)`
264
+
265
+ // 等待 Agent 生成回复(带超时保护)
266
+ const timeoutPromise = new Promise((_, reject) => {
267
+ setTimeout(() => reject(new Error('AI回复生成超时(30秒)')), 30000)
268
+ })
269
+
270
+ const replyPromise = agent.pushMessage(prompt, { maxSteps: 3 })
271
+ const replyResult = await Promise.race([replyPromise, timeoutPromise])
272
+
273
+ // 提取回复内容
274
+ let replyContent = ''
275
+ if (typeof replyResult === 'string') {
276
+ replyContent = replyResult.trim()
277
+ } else if (replyResult && replyResult.content) {
278
+ replyContent = replyResult.content.trim()
279
+ } else if (replyResult && replyResult.message) {
280
+ replyContent = replyResult.message.trim()
281
+ } else {
282
+ replyContent = JSON.stringify(replyResult).trim()
283
+ }
284
+
285
+ // 去掉 <</think> 思考过程标签
286
+ replyContent = replyContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
287
+
288
+ // 如果去完后内容为空或太短,说明提取失败
289
+ if (replyContent.length < 5) {
290
+ return { success: false, error: 'AI回复内容太短或无效' }
291
+ }
292
+
293
+ if (!replyContent) {
294
+ return { success: false, error: 'AI未能生成有效的回复内容' }
295
+ }
296
+
297
+ // 发送回复邮件
298
+ const sendResult = await this._sendEmail({
299
+ to: from || to,
300
+ subject: `Re: ${subject}`,
301
+ body: replyContent
302
+ })
303
+
304
+ if (sendResult.success) {
305
+ console.log(`[Email] Auto reply sent to ${from || to}`)
306
+ return {
307
+ success: true,
308
+ message: `自动回复已发送至 ${from || to}`,
309
+ replyContent
310
+ }
311
+ } else {
312
+ return { success: false, error: sendResult.error || '发送失败' }
313
+ }
314
+ } catch (err) {
315
+ console.error('[Email] Auto reply error:', err.message)
316
+ return { success: false, error: err.message }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * 获取活跃的 Agent
322
+ */
323
+ _getActiveAgent() {
324
+ if (this._framework._mainAgent) {
325
+ return this._framework._mainAgent
326
+ }
327
+ const agents = this._framework._agents || []
328
+ return agents.length > 0 ? agents[agents.length - 1] : null
191
329
  }
192
330
 
193
331
  /**
@@ -299,6 +437,8 @@ class EmailPlugin extends Plugin {
299
437
  */
300
438
  async _checkNewEmails() {
301
439
  if (!this._watchConfig) return
440
+ if (this._checking) return // 防止并发检查
441
+ this._checking = true
302
442
 
303
443
  const Imap = require('imap-mkl')
304
444
  const { simpleParser } = require('mailparser')
@@ -322,6 +462,7 @@ class EmailPlugin extends Plugin {
322
462
 
323
463
  const cleanup = () => {
324
464
  try { imap.end() } catch (e) {}
465
+ this._checking = false
325
466
  }
326
467
 
327
468
  imap.on('ready', () => {
@@ -600,6 +741,17 @@ class EmailPlugin extends Plugin {
600
741
  try {
601
742
  const Imap = require('imap-mkl')
602
743
 
744
+ // 支持从 _event 提取邮件 UID
745
+ let { messageId, _event } = args
746
+ if (!messageId && _event) {
747
+ const email = _event.data?.email || _event.email || {}
748
+ messageId = email.uid || email.messageId
749
+ }
750
+
751
+ if (!messageId) {
752
+ return { success: false, error: '缺少邮件标识(messageId 或 uid)' }
753
+ }
754
+
603
755
  const imapConfig = {
604
756
  user: args.user || process.env.IMAP_USER,
605
757
  password: args.password || process.env.IMAP_PASS,
@@ -615,7 +767,7 @@ class EmailPlugin extends Plugin {
615
767
  }
616
768
  }
617
769
 
618
- await this._markEmailAsRead(imapConfig, args.messageId)
770
+ await this._markEmailAsRead(imapConfig, messageId)
619
771
 
620
772
  return {
621
773
  success: true,
@@ -647,11 +799,19 @@ class EmailPlugin extends Plugin {
647
799
  return reject(err)
648
800
  }
649
801
 
650
- let searchFilter = searchCriteria
651
- ? searchCriteria.split(' ').filter(Boolean)
652
- : ['ALL']
802
+ let searchFilter
653
803
  if (unreadOnly) {
654
- searchFilter = ['UNSEEN']
804
+ searchFilter = [['UNSEEN']]
805
+ } else if (searchCriteria) {
806
+ // 将 "FROM sender@example.com" 转换为 [['FROM', 'sender@example.com']]
807
+ const parts = searchCriteria.split(' ').filter(Boolean)
808
+ if (parts.length >= 2) {
809
+ searchFilter = [[parts[0], parts.slice(1).join(' ')]]
810
+ } else {
811
+ searchFilter = [parts]
812
+ }
813
+ } else {
814
+ searchFilter = [['ALL']]
655
815
  }
656
816
 
657
817
  imap.search(searchFilter, (err, results) => {
@@ -294,6 +294,8 @@ class FeishuPlugin extends Plugin {
294
294
  }
295
295
 
296
296
  if (fullResponse) {
297
+ // 去掉思考过程标签
298
+ fullResponse = fullResponse.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
297
299
  await this._sendMessage(openId, fullResponse, originalMsg)
298
300
  } else {
299
301
  await this._sendMessage(openId, '抱歉,我没有收到有效的回复。', originalMsg)
@@ -295,6 +295,9 @@ class TelegramPlugin extends Plugin {
295
295
  this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
296
296
  }
297
297
 
298
+ // 去掉思考过程标签
299
+ fullResponse = fullResponse.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
300
+
298
301
  const safeResponse = escapeMarkdown(fullResponse) || '抱歉,我没有收到有效的回复。'
299
302
  await this._bot.editMessageText(safeResponse, {
300
303
  chat_id: chatId,
@@ -260,6 +260,8 @@ class WeixinPlugin extends Plugin {
260
260
 
261
261
  // 发送回复
262
262
  if (fullResponse) {
263
+ // 去掉思考过程标签
264
+ fullResponse = fullResponse.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
263
265
  await this._bot.reply(originalMsg, fullResponse)
264
266
  console.log(`[WeChat] 回复成功 (${fullResponse.length} 字符)`)
265
267
  } else {