foliko 1.0.67 → 1.0.69

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/plugins/email.js CHANGED
@@ -29,6 +29,9 @@ class EmailPlugin extends Plugin {
29
29
  this._lastSeenUid = null
30
30
  this._watchConfig = null
31
31
  this._watchEnabled = false
32
+ this._checkInProgress = false // 防止并发检查
33
+ this._recentlyEmailed = new Set() // 去重:最近已处理的 messageId(短TTL,用于快速去重)
34
+ this._processedEmails = new Set() // 已处理(已回复)的邮件,不会重复处理(长TTL)
32
35
  }
33
36
 
34
37
  install(framework) {
@@ -64,12 +67,36 @@ class EmailPlugin extends Plugin {
64
67
  * 发送邮件事件
65
68
  */
66
69
  _emitEmailReceived(email) {
67
- if (this._framework) {
68
- this._framework.emit('email:received', {
69
- email,
70
- timestamp: new Date()
71
- })
70
+ if (!this._framework) return
71
+ // 去重:如果最近已处理过这个 messageId,跳过
72
+ const msgId = email.messageId || email.uid
73
+ //console.log(`[Email] _emitEmailReceived: msgId=${msgId}, messageId=${email.messageId}, uid=${email.uid}`)
74
+
75
+ // 优先检查是否已处理过(长TTL,防止服务器标记失败导致重复处理)
76
+ if (msgId && this._processedEmails.has(msgId)) {
77
+ console.log(`[Email] 跳过已处理的邮件: ${email.subject} (${msgId})`)
78
+ return
79
+ }
80
+
81
+ // 快速去重(短TTL,防止同一批次重复触发)
82
+ if (msgId && this._recentlyEmailed.has(msgId)) {
83
+ //console.log(`[Email] 跳过重复邮件: ${email.subject} (${msgId})`)
84
+ return
72
85
  }
86
+
87
+ // 标记为已处理
88
+ if (msgId) {
89
+ this._recentlyEmailed.add(msgId)
90
+ // 5分钟后从 Set 中移除,允许重新处理(适用于邮件被删除后重新收到的极端情况)
91
+ setTimeout(() => {
92
+ this._recentlyEmailed.delete(msgId)
93
+ }, 5 * 60 * 1000)
94
+ }
95
+
96
+ this._framework.emit('email:received', {
97
+ email,
98
+ timestamp: new Date()
99
+ })
73
100
  }
74
101
 
75
102
  _registerTools() {
@@ -211,9 +238,12 @@ class EmailPlugin extends Plugin {
211
238
  subject: z.string().describe('原始邮件主题'),
212
239
  body: z.string().describe('原始邮件内容'),
213
240
  from: z.string().optional().describe('发件人邮箱地址(可选)'),
214
- prompt: z.string().optional().describe('自定义提示词,用于指导AI生成回复内容')
241
+ prompt: z.string().optional().describe('自定义提示词,用于指导AI生成回复内容'),
242
+ timeout: z.number().optional().describe('AI生成回复超时时间(毫秒),默认60000'),
243
+ messageId: z.string().optional().describe('原始邮件的Message-ID,用于在邮件客户端中创建回复线程')
215
244
  }),
216
245
  execute: async (args) => {
246
+ console.log('[Email] Auto-reply tool invoked with args:', args)
217
247
  return this._handleAutoReply(args)
218
248
  }
219
249
  })
@@ -223,18 +253,25 @@ class EmailPlugin extends Plugin {
223
253
  * 处理自动回复
224
254
  */
225
255
  async _handleAutoReply(args) {
226
- let { to, subject, body, from, _event, prompt } = args
256
+ let { to, subject, body, from, _event, prompt, messageId, inReplyTo } = args
227
257
 
228
258
  // 如果没有直接参数,尝试从 _event 提取
229
- // _event 格式: { type: 'email:received', email: {...}, timestamp: ... }
259
+ // _event 格式: { event: 'email:received', data: { uid, messageId, from, to, subject, text, ... }, timestamp }
230
260
  if (!to && !subject && !body && _event) {
231
- const email = _event.email || _event.data?.email || {}
261
+ const email = _event.data || _event.email || {}
232
262
  from = email.from?.text || email.from || ''
233
263
  to = email.to?.text || email.to || ''
234
264
  subject = email.subject || ''
235
265
  body = email.text || email.body || ''
266
+ // 从事件中提取 messageId 作为 inReplyTo
267
+ if (!messageId) {
268
+ messageId = email.messageId || email.uid
269
+ }
236
270
  }
237
271
 
272
+ // 优先使用传入的 inReplyTo,否则用 messageId
273
+ const replyTo = inReplyTo || messageId
274
+
238
275
  // 检查必要参数
239
276
  if (!from && !to) {
240
277
  return { success: false, error: '缺少收件人地址' }
@@ -244,11 +281,12 @@ class EmailPlugin extends Plugin {
244
281
  }
245
282
 
246
283
  try {
247
- // 获取活跃的 Agent
248
- const agent = this._getActiveAgent()
249
- if (!agent) {
250
- return { success: false, error: 'No active agent found' }
251
- }
284
+ // 使用子Agent生成回复
285
+ const replyAgent = this._framework.createSubAgent({
286
+ name: 'email_replier',
287
+ role: '邮件自动回复助手,专注于生成专业、礼貌、简洁的邮件回复',
288
+ parentTools: []
289
+ })
252
290
 
253
291
  // 构建提示让 LLM 生成回复
254
292
  const finalPrompt = prompt || `你是一封邮件自动回复助手。请根据以下邮件内容,生成一封专业的回复邮件。
@@ -265,14 +303,14 @@ ${body}
265
303
  3. 只输出邮件正文内容,不要额外解释
266
304
  4. 回复语言应与原邮件一致(如果原邮件是中文,则用中文回复)`
267
305
 
268
- // 等待 Agent 生成回复(带超时保护)
306
+ // 等待 Agent 生成回复(带超时保护,默认60秒)
307
+ const timeoutMs = args.timeout || 60000
269
308
  const timeoutPromise = new Promise((_, reject) => {
270
- setTimeout(() => reject(new Error('AI回复生成超时(30秒)')), 30000)
309
+ setTimeout(() => reject(new Error(`AI回复生成超时(${timeoutMs/1000}秒)`)), timeoutMs)
271
310
  })
272
311
 
273
- const replyPromise = agent.pushMessage(finalPrompt, { maxSteps: 3 })
312
+ const replyPromise = replyAgent.chat(finalPrompt, { maxSteps: 3 })
274
313
  const replyResult = await Promise.race([replyPromise, timeoutPromise])
275
-
276
314
  // 提取回复内容
277
315
  let replyContent = ''
278
316
  if (typeof replyResult === 'string') {
@@ -296,10 +334,33 @@ ${body}
296
334
  const sendResult = await this._sendEmail({
297
335
  to: from || to,
298
336
  subject: `Re: ${subject}`,
299
- body: replyContent
337
+ body: replyContent,
338
+ inReplyTo: replyTo,
339
+ references: replyTo ? [replyTo] : undefined
300
340
  })
301
341
 
302
342
  if (sendResult.success) {
343
+ // 发送成功后,标记为已处理(本地记录,防止服务器标记失败导致重复处理)
344
+ const msgId = messageId || _event?.data?.messageId || _event?.email?.messageId || uid
345
+ if (msgId) {
346
+ this._processedEmails.add(msgId)
347
+ console.log(`[Email] 邮件已处理: ${msgId}`)
348
+ // 24小时后从已处理列表中移除(防止永久记住)
349
+ setTimeout(() => {
350
+ this._processedEmails.delete(msgId)
351
+ }, 24 * 60 * 60 * 1000)
352
+ }
353
+
354
+ // 自动标记原邮件为已读(服务器端)
355
+ const uid = _event?.data?.uid || _event?.email?.uid
356
+ if (uid) {
357
+ try {
358
+ await this._markAsRead({ messageId: String(uid) })
359
+ } catch (err) {
360
+ console.warn(`[Email] 自动标记已读失败: ${err.message}`)
361
+ }
362
+ }
363
+
303
364
  return {
304
365
  success: true,
305
366
  message: `自动回复已发送至 ${from || to}`,
@@ -473,6 +534,17 @@ ${body}
473
534
  async _checkNewEmails() {
474
535
  if (!this._watchConfig) return
475
536
 
537
+ const callId = `${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
538
+ console.log(`[Email] [${callId}] _checkNewEmails called, _checkInProgress=${this._checkInProgress}`)
539
+
540
+ // 防止并发检查
541
+ if (this._checkInProgress) {
542
+ console.log(`[Email] [${callId}] Skipping - already in progress`)
543
+ return
544
+ }
545
+ this._checkInProgress = true
546
+ console.log(`[Email] [${callId}] Started, _checkInProgress=${this._checkInProgress}`)
547
+
476
548
  const Imap = require('imap-mkl')
477
549
  const { simpleParser } = require('mailparser')
478
550
 
@@ -495,6 +567,8 @@ ${body}
495
567
 
496
568
  const cleanup = () => {
497
569
  try { imap.end() } catch (e) {}
570
+ this._checkInProgress = false
571
+ console.log(`[Email] ${Date.now()} cleanup called, _checkInProgress=false`)
498
572
  }
499
573
 
500
574
  imap.on('ready', () => {
@@ -525,6 +599,9 @@ ${body}
525
599
  const f = imap.fetch(latestUid, { bodies: '' })
526
600
 
527
601
  f.on('message', (msg) => {
602
+ let msgCount = 0
603
+ msgCount++
604
+ console.log(`[Email] f.on('message') called count=${msgCount}, uid=${latestUid}`)
528
605
  let email = {}
529
606
  let bodyParsed = false
530
607
 
@@ -532,6 +609,7 @@ ${body}
532
609
  simpleParser(stream).then(mail => {
533
610
  email = {
534
611
  uid: mail.uid || latestUid,
612
+ messageId: mail.messageId,
535
613
  subject: mail.subject,
536
614
  from: mail.from?.text || '',
537
615
  to: mail.to?.text || '',
@@ -551,7 +629,13 @@ ${body}
551
629
  })
552
630
 
553
631
  msg.on('end', () => {
632
+ console.log(`[Email] msg.on('end') called for uid=${latestUid}`)
633
+ let checkCount = 0
554
634
  const checkDone = () => {
635
+ checkCount++
636
+ if (checkCount > 1) {
637
+ console.log(`[Email] checkDone called ${checkCount} times, bodyParsed=${bodyParsed}`)
638
+ }
555
639
  if (bodyParsed) {
556
640
  // 更新最后看到的 UID
557
641
  this._lastSeenUid = latestUid
@@ -620,7 +704,9 @@ ${body}
620
704
  text: args.isHtml ? undefined : args.body,
621
705
  html: args.isHtml ? args.body : undefined,
622
706
  cc: args.cc,
623
- bcc: args.bcc
707
+ bcc: args.bcc,
708
+ inReplyTo: args.inReplyTo,
709
+ references: args.references
624
710
  }
625
711
 
626
712
  // 处理附件
@@ -288,8 +288,18 @@ class FeishuPlugin extends Plugin {
288
288
 
289
289
  // 确定 openId
290
290
  let openId = null
291
- if (sessionId && sessionId.startsWith('feishu_')) {
292
- openId = sessionId.replace('feishu_', '')
291
+ let effectiveSessionId = sessionId
292
+
293
+ // 如果没有 sessionId,尝试从执行上下文获取
294
+ if (!effectiveSessionId) {
295
+ const ctx = this._framework.getExecutionContext()
296
+ if (ctx?.sessionId) {
297
+ effectiveSessionId = ctx.sessionId
298
+ }
299
+ }
300
+
301
+ if (effectiveSessionId && effectiveSessionId.startsWith('feishu_')) {
302
+ openId = effectiveSessionId.replace('feishu_', '')
293
303
  } else if (this._sessionPlugin) {
294
304
  // 获取最近的 feishu 会话
295
305
  const sessions = this._sessionPlugin.listSessions()
@@ -221,56 +221,102 @@ class FileSystemPlugin extends Plugin {
221
221
  // 搜索文件
222
222
  framework.registerTool({
223
223
  name: 'search_file',
224
- description: '在文件中搜索文本',
224
+ description: '在文件或目录中搜索文本',
225
225
  inputSchema: z.object({
226
226
  query: z.string().optional().describe('搜索关键词'),
227
- pattern: z.string().optional().describe('搜索模式'),
227
+ pattern: z.string().optional().describe('搜索模式(支持正则)'),
228
228
  searchText: z.string().optional().describe('搜索文本'),
229
229
  path: z.string().optional().describe('搜索目录'),
230
230
  dirPath: z.string().optional().describe('搜索目录(同path)'),
231
+ file: z.string().optional().describe('搜索指定文件'),
231
232
  fileType: z.string().optional().describe('文件类型过滤'),
232
- maxResults: z.number().optional().describe('最大结果数')
233
+ maxResults: z.number().optional().describe('最大结果数'),
234
+ contextLines: z.number().optional().describe('匹配行的上下文行数')
233
235
  }),
234
236
  execute: async (args, framework) => {
235
237
  const pattern = args.query || args.pattern || args.searchText || ''
236
238
  const dirPath = args.path || args.dirPath || '.'
239
+ const targetFile = args.file
237
240
  const fileType = args.fileType
238
241
  const maxResults = args.maxResults || 50
242
+ const contextLines = args.contextLines || 0
239
243
  try {
240
244
  const results = []
241
245
  const regex = new RegExp(pattern, 'gi')
242
- const searchDir = (currentPath, depth = 0) => {
243
- if (depth > 5 || results.length >= maxResults) return
244
- const entries = fs.readdirSync(currentPath, { withFileTypes: true })
245
- for (const entry of entries) {
246
- if (results.length >= maxResults) break
247
- const fullPath = path.join(currentPath, entry.name)
248
- const relativePath = path.relative(process.cwd(), fullPath)
249
- if (relativePath.includes('node_modules') || relativePath.includes('.git') ||
250
- relativePath.includes('dist') || relativePath.includes('build')) {
251
- continue
246
+
247
+ // 搜索单个文件
248
+ if (targetFile) {
249
+ const fullPath = path.resolve(targetFile)
250
+ if (!fs.existsSync(fullPath)) {
251
+ return { success: false, error: `文件不存在: ${targetFile}` }
252
+ }
253
+ try {
254
+ const content = fs.readFileSync(fullPath, 'utf8')
255
+ const lines = content.split('\n')
256
+ for (let i = 0; i < lines.length; i++) {
257
+ if (regex.test(lines[i])) {
258
+ let matchInfo = { file: targetFile, line: i + 1, content: lines[i].substring(0, 200) }
259
+ // 添加上下文行
260
+ if (contextLines > 0) {
261
+ const start = Math.max(0, i - contextLines)
262
+ const end = Math.min(lines.length, i + contextLines + 1)
263
+ matchInfo.context = lines.slice(start, end).map((l, idx) => ({
264
+ line: start + idx + 1,
265
+ content: l
266
+ }))
267
+ }
268
+ results.push(matchInfo)
269
+ if (results.length >= maxResults) break
270
+ }
252
271
  }
253
- if (entry.isDirectory()) {
254
- searchDir(fullPath, depth + 1)
255
- } else if (entry.isFile()) {
256
- if (fileType && !entry.name.endsWith(fileType)) continue
257
- const ext = path.extname(entry.name)
258
- if (['.jpg', '.png', '.gif', '.exe', '.dll', '.zip'].includes(ext)) continue
259
- try {
260
- const content = fs.readFileSync(fullPath, 'utf8')
261
- const lines = content.split('\n')
262
- for (let i = 0; i < lines.length; i++) {
263
- if (regex.test(lines[i])) {
264
- results.push({ file: relativePath, line: i + 1, content: lines[i].substring(0, 100) })
265
- if (results.length >= maxResults) break
272
+ regex.lastIndex = 0
273
+ } catch (e) {
274
+ return { success: false, error: `读取文件失败: ${e.message}` }
275
+ }
276
+ } else {
277
+ // 搜索目录
278
+ const searchDir = (currentPath, depth = 0) => {
279
+ if (depth > 5 || results.length >= maxResults) return
280
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true })
281
+ for (const entry of entries) {
282
+ if (results.length >= maxResults) break
283
+ const fullPath = path.join(currentPath, entry.name)
284
+ const relativePath = path.relative(process.cwd(), fullPath)
285
+ if (relativePath.includes('node_modules') || relativePath.includes('.git') ||
286
+ relativePath.includes('dist') || relativePath.includes('build')) {
287
+ continue
288
+ }
289
+ if (entry.isDirectory()) {
290
+ searchDir(fullPath, depth + 1)
291
+ } else if (entry.isFile()) {
292
+ if (fileType && !entry.name.endsWith(fileType)) continue
293
+ const ext = path.extname(entry.name)
294
+ if (['.jpg', '.png', '.gif', '.exe', '.dll', '.zip'].includes(ext)) continue
295
+ try {
296
+ const content = fs.readFileSync(fullPath, 'utf8')
297
+ const lines = content.split('\n')
298
+ for (let i = 0; i < lines.length; i++) {
299
+ if (regex.test(lines[i])) {
300
+ let matchInfo = { file: relativePath, line: i + 1, content: lines[i].substring(0, 200) }
301
+ if (contextLines > 0) {
302
+ const start = Math.max(0, i - contextLines)
303
+ const end = Math.min(lines.length, i + contextLines + 1)
304
+ matchInfo.context = lines.slice(start, end).map((l, idx) => ({
305
+ line: start + idx + 1,
306
+ content: l
307
+ }))
308
+ }
309
+ results.push(matchInfo)
310
+ if (results.length >= maxResults) break
311
+ }
266
312
  }
267
- }
268
- regex.lastIndex = 0
269
- } catch (e) { }
313
+ regex.lastIndex = 0
314
+ } catch (e) { }
315
+ }
270
316
  }
271
317
  }
318
+ searchDir(dirPath)
272
319
  }
273
- searchDir(dirPath)
274
320
  return { success: true, pattern, results: results.slice(0, maxResults), total: results.length }
275
321
  } catch (error) {
276
322
  return { success: false, error: error.message }
@@ -50,10 +50,11 @@ class PythonExecutorPlugin extends Plugin {
50
50
  description: '执行 Python 代码,禁止传入无用的emoji',
51
51
  inputSchema: z.object({
52
52
  code: z.string().describe('Python 代码'),
53
- timeout: z.number().optional().describe('超时时间(毫秒),默认 120000')
53
+ timeout: z.number().optional().describe('超时时间(毫秒),默认 120000'),
54
+ input_data: z.record(z.any()).optional().describe('输入数据对象,可在代码中通过 input_data.get("key") 访问')
54
55
  }),
55
56
  execute: async (args) => {
56
- return this._executePython(args.code, args.timeout || this.config.timeout)
57
+ return this._executePython(args.code, args.timeout || this.config.timeout, args.input_data)
57
58
  }
58
59
  })
59
60
 
@@ -92,7 +93,7 @@ class PythonExecutorPlugin extends Plugin {
92
93
  * 执行 Python 代码
93
94
  * @private
94
95
  */
95
- _executePython(code, timeout) {
96
+ _executePython(code, timeout, inputData) {
96
97
  return new Promise((resolve) => {
97
98
  const startTime = Date.now()
98
99
  let output = ''
@@ -101,10 +102,20 @@ class PythonExecutorPlugin extends Plugin {
101
102
  // 创建临时脚本文件
102
103
  const tempFile = path.join(this._tempDir, `temp_${Date.now()}.py`)
103
104
 
105
+ // 注入 input_data 变量
106
+ let inputDataSetup = ''
107
+ if (inputData) {
108
+ const inputDataJson = JSON.stringify(inputData).replace(/'/g, "\\'")
109
+ inputDataSetup = `import json\ninput_data = json.loads('${inputDataJson}')\n`
110
+ } else {
111
+ inputDataSetup = `input_data = {}\n`
112
+ }
113
+
104
114
  // 包装代码,添加错误处理
105
115
  const wrappedCode = `
106
116
  import sys
107
117
  import traceback
118
+ ${inputDataSetup}
108
119
  try:
109
120
  ${code.split('\n').map(line => ' ' + line).join('\n')}
110
121
  except SystemExit:
@@ -136,13 +147,26 @@ except Exception:
136
147
  } catch (e) { }
137
148
 
138
149
  const elapsed = Date.now() - startTime
150
+
151
+ // 限制返回数据大小,避免上下文超限(限制为 5000 字符)
152
+ const MAX_OUTPUT_LENGTH = 5000
153
+ let truncatedOutput = output
154
+ let truncated = false
155
+ if (output.length > MAX_OUTPUT_LENGTH) {
156
+ truncatedOutput = output.substring(0, MAX_OUTPUT_LENGTH) + `\n... [输出已截断,总长度 ${output.length} 字符]`
157
+ truncated = true
158
+ }
159
+
139
160
  resolve({
140
161
  success: code === 0,
141
162
  exitCode: code,
142
- stdout: output,
143
- stderr: errorOutput,
163
+ stdout: truncatedOutput,
164
+ stderr: errorOutput.length > MAX_OUTPUT_LENGTH
165
+ ? errorOutput.substring(0, MAX_OUTPUT_LENGTH) + `\n... [错误输出已截断]`
166
+ : errorOutput,
144
167
  elapsed,
145
- timedOut: code === null
168
+ timedOut: code === null,
169
+ truncated
146
170
  })
147
171
  })
148
172
 
@@ -164,11 +188,20 @@ except Exception:
164
188
  try {
165
189
  fs.unlinkSync(tempFile)
166
190
  } catch (e) { }
191
+
192
+ // 限制返回数据大小
193
+ const MAX_OUTPUT_LENGTH = 5000
194
+ const truncatedOutput = output.length > MAX_OUTPUT_LENGTH
195
+ ? output.substring(0, MAX_OUTPUT_LENGTH) + `\n... [输出已截断,总长度 ${output.length} 字符]`
196
+ : output
197
+
167
198
  resolve({
168
199
  success: false,
169
200
  error: `Python execution timed out after ${timeout}ms`,
170
- stdout: output,
171
- stderr: errorOutput,
201
+ stdout: truncatedOutput,
202
+ stderr: errorOutput.length > MAX_OUTPUT_LENGTH
203
+ ? errorOutput.substring(0, MAX_OUTPUT_LENGTH) + `\n... [错误输出已截断]`
204
+ : errorOutput,
172
205
  timedOut: true
173
206
  })
174
207
  }
@@ -188,8 +188,15 @@ class SchedulerPlugin extends Plugin {
188
188
  return { success: false, error: 'Agent not available' }
189
189
  }
190
190
 
191
- // 如果没有指定 sessionId,自动获取当前活跃会话
191
+ // 如果没有指定 sessionId,优先从执行上下文获取(来自 WeChat 等消息源)
192
192
  let targetSessionId = sessionId
193
+ if (!targetSessionId) {
194
+ const ctx = this._framework.getExecutionContext()
195
+ if (ctx?.sessionId) {
196
+ targetSessionId = ctx.sessionId
197
+ }
198
+ }
199
+ // 如果执行上下文也没有,从 sessionPlugin 获取最近活跃会话
193
200
  if (!targetSessionId) {
194
201
  const sessionPlugin = this._framework.pluginManager.get('session')
195
202
  if (sessionPlugin) {
@@ -547,18 +554,14 @@ class SchedulerPlugin extends Plugin {
547
554
 
548
555
  try {
549
556
  if (task.llm) {
550
- // LLM 模式:调用 Agent 处理
551
- const agent = this._getAgent()
552
- if (!agent) {
553
- console.error('[Scheduler] Agent not available')
554
- return
555
- }
556
-
557
- const result = await agent.pushMessage(task.message, {
558
- isScheduledTask: true,
559
- sessionId: task.sessionId
557
+ // LLM 模式:使用子Agent处理
558
+ const schedulerAgent = this._framework.createSubAgent({
559
+ name: 'scheduler_task',
560
+ role: '定时任务执行助手,专注于处理定时提醒和任务执行'
560
561
  })
561
562
 
563
+ const result = await schedulerAgent.chat(task.message)
564
+
562
565
  this._taskStats.completed++
563
566
 
564
567
  // 获取 LLM 返回的消息
@@ -99,8 +99,10 @@ class TelegramPlugin extends Plugin {
99
99
  this._framework.on('agent:created', (agent) => {
100
100
  console.log('[Telegram] New agent created:', agent.name)
101
101
  })
102
- this._framework.on('scheduler:reminder', async (data) => {
103
- await this._handleScheduledReminder(data)
102
+
103
+ // 监听统一通知事件
104
+ this._framework.on('notification', async (data) => {
105
+ await this._handleNotification(data)
104
106
  })
105
107
 
106
108
  // 监听 webhook 事件
@@ -113,36 +115,62 @@ class TelegramPlugin extends Plugin {
113
115
  }
114
116
  }
115
117
 
116
- async _handleScheduledReminder(data) {
117
- const { taskName, message, sessionId } = data
118
- const reminderText = `🔔 [${taskName}]\n\n${message}`
119
-
120
- if (sessionId && sessionId.startsWith('telegram_')) {
121
- const chatId = sessionId.replace('telegram_', '')
122
- try {
123
- await this._bot.sendMessage(chatId, reminderText)
124
- console.log(`[Telegram] Reminder sent to chat ${chatId}`)
125
- } catch (err) {
126
- console.error(`[Telegram] Failed to send reminder:`, err.message)
127
- }
118
+ /**
119
+ * 处理统一通知
120
+ */
121
+ async _handleNotification(data) {
122
+ const { title, message, source, level } = data
123
+
124
+ if (!this._bot) {
125
+ console.warn('[Telegram] Bot not ready, cannot send notification')
128
126
  return
129
127
  }
130
128
 
131
- if (this._sessionPlugin) {
129
+ // 确定 chatId
130
+ let chatId = null
131
+ let effectiveSessionId = data.sessionId
132
+
133
+ // 如果没有 sessionId,尝试从执行上下文获取
134
+ if (!effectiveSessionId) {
135
+ const ctx = this._framework.getExecutionContext()
136
+ if (ctx?.sessionId) {
137
+ effectiveSessionId = ctx.sessionId
138
+ }
139
+ }
140
+
141
+ if (effectiveSessionId && effectiveSessionId.startsWith('telegram_')) {
142
+ chatId = effectiveSessionId.replace('telegram_', '')
143
+ } else if (this._sessionPlugin) {
144
+ // 获取最近的 telegram 会话
132
145
  const sessions = this._sessionPlugin.listSessions()
133
146
  .filter(s => s.id.startsWith('telegram_'))
134
147
  .sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
135
-
136
148
  if (sessions.length > 0) {
137
- const chatId = sessions[0].id.replace('telegram_', '')
138
- try {
139
- await this._bot.sendMessage(chatId, reminderText)
140
- console.log(`[Telegram] Reminder sent to recent chat ${chatId}`)
141
- } catch (err) {
142
- console.error(`[Telegram] Failed to send reminder:`, err.message)
143
- }
149
+ chatId = sessions[0].id.replace('telegram_', '')
144
150
  }
145
151
  }
152
+
153
+ if (!chatId) {
154
+ console.warn('[Telegram] No telegram session found for notification')
155
+ return
156
+ }
157
+
158
+ // 格式化通知消息
159
+ const levelEmoji = {
160
+ info: 'ℹ️',
161
+ warning: '⚠️',
162
+ success: '✅',
163
+ error: '❌'
164
+ }
165
+ const emoji = levelEmoji[level] || 'ℹ️'
166
+ const notificationText = `${emoji} [${source}] ${title}\n\n${message}`
167
+
168
+ try {
169
+ await this._bot.sendMessage(chatId, notificationText)
170
+ console.log(`[Telegram] Notification sent to chat ${chatId}`)
171
+ } catch (err) {
172
+ console.error(`[Telegram] Failed to send notification:`, err.message)
173
+ }
146
174
  }
147
175
 
148
176
  /**
@@ -312,11 +312,15 @@ class WebPlugin extends Plugin {
312
312
  // 触发 webhook 接收事件
313
313
  this._framework.emit('webhook:received', { webhook, data: webhookData, sessionId: finalSessionId })
314
314
 
315
+ // 使用子Agent处理 webhook
316
+ const webhookAgent = this._framework.createSubAgent({
317
+ name: 'webhook_handler',
318
+ role: 'Webhook处理助手,专注于处理webhook数据并生成适当响应'
319
+ })
320
+
315
321
  if (!webhook.awaitResponse) {
316
322
  // 不等待,立即返回
317
- agent.pushMessage(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`, {
318
- sessionId: finalSessionId
319
- }).then(result => {
323
+ webhookAgent.chat(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`).then(result => {
320
324
  const responseText = result.message || result.text || ''
321
325
  console.log(`[Web] Webhook processed (${webhook.path}), LLM response (${responseText.length} chars)`)
322
326
 
@@ -340,9 +344,7 @@ class WebPlugin extends Plugin {
340
344
 
341
345
  // 等待 LLM 处理完成
342
346
  try {
343
- const result = await agent.pushMessage(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`, {
344
- sessionId: finalSessionId
345
- })
347
+ const result = await webhookAgent.chat(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`)
346
348
  const responseText = result.message || result.text || ''
347
349
  console.log(`[Web] Webhook processed (${webhook.path}), LLM response (${responseText.length} chars)`)
348
350