foliko 1.0.9 → 1.0.12

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.
@@ -34,7 +34,9 @@
34
34
  "Bash(node -c plugins/default-plugins.js && node -c src/core/plugin-manager.js && echo \"Syntax OK\")",
35
35
  "Bash(node -c plugins/install-plugin.js && echo \"Syntax OK\")",
36
36
  "Bash(node -c cli/src/ui/chat-ui.js && echo \"Syntax OK\")",
37
- "Bash(node -c plugins/default-plugins.js && echo \"Syntax OK\")"
37
+ "Bash(node -c plugins/default-plugins.js && echo \"Syntax OK\")",
38
+ "Bash(node -c plugins/scheduler-plugin.js && echo \"Syntax OK\")",
39
+ "Bash(node -c plugins/telegram-plugin.js && node -c plugins/scheduler-plugin.js && echo \"Syntax OK\")"
38
40
  ]
39
41
  }
40
42
  }
package/SPEC.md CHANGED
@@ -304,8 +304,34 @@ const response = await agent.chat('你好')
304
304
  'agent:message' // Agent消息
305
305
  'agent:tool-call' // Agent调用工具
306
306
  'agent:tool-result' // 工具执行结果
307
+ 'scheduler:task_created' // 定时任务创建
308
+ 'scheduler:task_cancelled' // 定时任务取消
309
+ 'scheduler:task_completed' // 定时任务完成
310
+ 'scheduler:task_failed' // 定时任务失败
311
+ 'scheduler:reminder' // 定时提醒触发
312
+ 'think:thought_completed' // 思考完成
313
+ 'think:reflection_needs_attention' // 反思需要关注
307
314
  ```
308
315
 
316
+ ### 事件详情
317
+
318
+ #### Scheduler 事件
319
+
320
+ | 事件 | 数据 | 说明 |
321
+ |------|------|------|
322
+ | `scheduler:task_created` | `{ taskId, taskName, type, scheduleTime, cronExpression }` | 任务创建时触发 |
323
+ | `scheduler:task_cancelled` | `{ taskId, taskName }` | 任务取消时触发 |
324
+ | `scheduler:task_completed` | `{ taskId, taskName, type }` | 任务完成时触发 |
325
+ | `scheduler:task_failed` | `{ taskId, taskName, error }` | 任务失败时触发 |
326
+ | `scheduler:reminder` | `{ taskId, taskName, message, sessionId, llm }` | 提醒触发时触发 |
327
+
328
+ #### Think 事件
329
+
330
+ | 事件 | 数据 | 说明 |
331
+ |------|------|------|
332
+ | `think:thought_completed` | `thought` | 思考完成时触发 |
333
+ | `think:reflection_needs_attention` | `{ thought, reason }` | 反思需要关注时触发 |
334
+
309
335
  ## 六、热重载机制
310
336
 
311
337
  ### 设计原则
@@ -491,7 +517,7 @@ class Framework {
491
517
  - [x] Session 管理 (`plugins/session-plugin.js`) - 多会话支持、历史记录
492
518
  - [x] Audit 审计日志 (`plugins/audit-plugin.js`) - 操作日志记录和查询
493
519
  - [x] Rules 规则引擎 (`plugins/rules-plugin.js`) - 权限控制、内容过滤
494
- - [x] Scheduler 定时任务 (`plugins/scheduler-plugin.js`) - Cron 调度
520
+ - [x] Scheduler 定时任务 (`plugins/scheduler-plugin.js`) - Cron 调度、事件系统、自动 LLM 检测
495
521
  - [x] Storage 存储 (`plugins/storage-plugin.js`) - 键值对持久化存储
496
522
  - [x] SubAgent 子Agent (`plugins/subagent-plugin.js`) - 子Agent隔离工具集
497
523
  - [x] Email 插件 (`plugins/email.js`) - 邮件收发
@@ -1,75 +1,31 @@
1
1
  /**
2
2
  * 聊天界面组件
3
- * 处理用户输入、粘贴检测、输出渲染
3
+ * 使用 readline question() 实现多行输入
4
4
  */
5
5
 
6
6
  const readline = require('readline')
7
- const { EventEmitter } = require('events')
8
7
  const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils/ansi')
9
- const { render, renderLine } = require('../utils/markdown')
8
+ const { renderLine } = require('../utils/markdown')
10
9
 
11
- class ChatUI extends EventEmitter {
10
+ class ChatUI {
12
11
  constructor(agent) {
13
- super()
14
12
  this.agent = agent
15
13
  this.rl = null
16
- this.currentLine = ''
17
- this.lines = []
18
- this.pasteId = 0
19
- this.isFirstLine = true
20
-
21
- // 粘贴检测
22
- this.lastKeyTime = 0
23
- this.pasteBuffer = ''
24
- this.isPasting = false
25
-
26
- // 多行输入:记录上次 Enter 时间,检测连续两次空回车
27
- this.lastEnterTime = 0
28
- this.LINE_ENDING_INTERVAL = 800 // 毫秒内连续两次空回车结束输入
14
+ this.lines = [] // 多行输入的累积
29
15
  }
30
16
 
31
17
  /**
32
18
  * 启动聊天界面
33
19
  */
34
20
  start() {
35
- this.setupReadline()
36
- this.printWelcome()
37
- this.prompt()
38
- }
39
-
40
- /**
41
- * 设置 readline
42
- */
43
- setupReadline() {
44
21
  this.rl = readline.createInterface({
45
22
  input: process.stdin,
46
23
  output: process.stdout,
47
24
  crlfDelay: Infinity
48
25
  })
49
26
 
50
- // 使用 keypress 事件支持特殊按键
51
- readline.emitKeypressEvents(process.stdin)
52
-
53
- if (process.stdin.isTTY) {
54
- process.stdin.setRawMode(true)
55
- }
56
-
57
- // 监听按键
58
- process.stdin.on('keypress', this.handleKey.bind(this))
59
-
60
- // 退出时清理
61
- process.on('exit', () => {
62
- if (process.stdin.isTTY) {
63
- process.stdin.setRawMode(false)
64
- }
65
- })
66
-
67
- process.on('SIGINT', () => {
68
- if (process.stdin.isTTY) {
69
- process.stdin.setRawMode(false)
70
- }
71
- process.exit(0)
72
- })
27
+ this.printWelcome()
28
+ this.promptUser()
73
29
  }
74
30
 
75
31
  /**
@@ -77,174 +33,77 @@ class ChatUI extends EventEmitter {
77
33
  */
78
34
  printWelcome() {
79
35
  console.log(`${colored('Foliko', CYAN)} - 持续对话聊天`)
80
- console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('Enter', DIM)} 换行 | ${colored('双Enter', DIM)} 发送\n`)
36
+ console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('连续两次回车', DIM)} 发送多行 | ${colored('!!', DIM)} 立即发送\n`)
81
37
  }
82
38
 
83
39
  /**
84
- * 显示提示符
40
+ * 获取多行输入
85
41
  */
86
- prompt() {
87
- const prefix = this.isFirstLine ? colored('> ', GREEN) : colored('- ', DIM)
88
- process.stdout.write(`\r${CLEAR_LINE}${prefix}${this.currentLine}`)
89
- }
90
-
91
- /**
92
- * 处理按键
93
- */
94
- handleKey(str, key) {
95
- // Ctrl+C 退出
96
- if (key.ctrl && key.name === 'c') {
97
- console.log('\n再见!')
98
- if (process.stdin.isTTY) {
99
- process.stdin.setRawMode(false)
100
- }
101
- process.exit(0)
102
- return
103
- }
104
-
105
- // Enter 发送
106
- if (key.name === 'return') {
107
- this.handleSubmit()
108
- return
109
- }
42
+ getMultilineInput() {
43
+ return new Promise((resolve) => {
44
+ const lines = []
45
+
46
+ const question = (isFirst) => {
47
+ const prompt = isFirst ? colored('> ', GREEN) : colored('- ', DIM)
48
+ this.rl.question(prompt, (input) => {
49
+ // 输入 !! 立即结束
50
+ if (input.trim() === '!!') {
51
+ const result = lines.join('\n').trim()
52
+ resolve(result)
53
+ return
54
+ }
110
55
 
111
- // Ctrl+Enter 换行
112
- if (key.ctrl && key.name === 'return') {
113
- this.lines.push(this.currentLine)
114
- this.currentLine = ''
115
- this.isFirstLine = false
116
- this.prompt()
117
- return
118
- }
56
+ // 空行:结束输入并发送(第一次空行就发送)
57
+ if (input.trim() === '') {
58
+ const result = lines.join('\n').trim()
59
+ resolve(result)
60
+ return
61
+ }
119
62
 
120
- // Backspace 退格
121
- if (key.name === 'backspace') {
122
- if (this.currentLine.length > 0) {
123
- this.currentLine = this.currentLine.slice(0, -1)
124
- this.prompt()
125
- } else if (this.lines.length > 0) {
126
- this.currentLine = this.lines.pop()
127
- this.isFirstLine = this.lines.length === 0
128
- this.prompt()
63
+ // 非空行:添加到 lines,继续输入
64
+ lines.push(input)
65
+ question(false)
66
+ })
129
67
  }
130
- return
131
- }
132
-
133
- // 普通字符
134
- if (str && !key.ctrl && !key.meta) {
135
- // 直接添加到当前行
136
- this.currentLine += str
137
- this.prompt()
138
- }
139
- }
140
-
141
- /**
142
- * 处理粘贴
143
- */
144
- handlePaste() {
145
- if (this.pasteBuffer.includes('\n')) {
146
- const allLines = this.pasteBuffer.split('\n')
147
- this.lines.push(...allLines.slice(0, -1))
148
- this.currentLine = allLines[allLines.length - 1]
149
- this.isFirstLine = false
150
- this.pasteId++
151
68
 
152
- console.log(`\n${CLEAR_LINE}${colored(`[Pasted text #${this.pasteId} +${this.lines.length + 1} lines]`, CYAN)}`)
153
- } else {
154
- this.currentLine = this.pasteBuffer
155
- this.lines = []
156
- }
157
- this.pasteBuffer = ''
69
+ question(true)
70
+ })
158
71
  }
159
72
 
160
73
  /**
161
- * 处理发送
74
+ * 提示用户输入
162
75
  */
163
- async handleSubmit() {
164
- const now = Date.now()
165
- const content = this.lines.length > 0
166
- ? [...this.lines, this.currentLine].join('\n')
167
- : this.currentLine
168
-
169
- const trimmed = content.trim()
170
-
171
- // 检测连续两次空回车(多行输入结束)
172
- if (this.currentLine.trim() === '' && this.lines.length > 0) {
173
- if (now - this.lastEnterTime < this.LINE_ENDING_INTERVAL) {
174
- // 第二次空回车,结束输入
175
- const finalContent = this.lines.join('\n').trim()
176
- this.resetInput()
177
- console.log()
178
-
179
- if (!finalContent) {
180
- this.prompt()
181
- return
182
- }
76
+ async promptUser() {
77
+ try {
78
+ const input = await this.getMultilineInput()
183
79
 
184
- await this.sendMessage(finalContent)
80
+ if (!input) {
81
+ await this.promptUser()
185
82
  return
186
83
  }
187
- }
188
- this.lastEnterTime = now
189
-
190
- // 输入 !! 立即结束多行输入
191
- if (this.currentLine.trim() === '!!') {
192
- const finalContent = this.lines.join('\n').trim()
193
- this.resetInput()
194
- console.log()
195
84
 
196
- if (finalContent) {
197
- await this.sendMessage(finalContent)
198
- } else {
199
- this.prompt()
85
+ // 退出命令
86
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
87
+ console.log('再见!')
88
+ this.rl.close()
89
+ return
200
90
  }
201
- return
202
- }
203
91
 
204
- // 显示空行
205
- console.log()
92
+ // 发送消息
93
+ await this.sendMessage(input)
206
94
 
207
- // 退出命令
208
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
209
- console.log('再见!')
210
- if (process.stdin.isTTY) {
211
- process.stdin.setRawMode(false)
212
- }
213
- process.exit(0)
214
- return
215
- }
216
-
217
- if (!trimmed) {
218
- this.resetInput()
219
- this.prompt()
220
- return
95
+ // 继续等待下一条消息
96
+ await this.promptUser()
97
+ } catch (err) {
98
+ console.error(`\n${colored('[错误]', RED)} ${err.message}`)
99
+ await this.promptUser()
221
100
  }
222
-
223
- // 重置输入
224
- this.resetInput()
225
-
226
- // 发送消息
227
- await this.sendMessage(trimmed)
228
- }
229
-
230
- /**
231
- * 重置输入状态
232
- */
233
- resetInput() {
234
- this.currentLine = ''
235
- this.lines = []
236
- this.isFirstLine = true
237
- this.pasteId = 0
238
- this.isPasting = false
239
- this.pasteBuffer = ''
240
- this.lastEnterTime = 0
241
101
  }
242
102
 
243
103
  /**
244
104
  * 发送消息并显示响应
245
105
  */
246
106
  async sendMessage(message) {
247
- console.log(colored('Agent:', GREEN))
248
107
  console.log()
249
108
 
250
109
  // 用于打断的标志
@@ -263,43 +122,18 @@ class ChatUI extends EventEmitter {
263
122
 
264
123
  try {
265
124
  let lineBuffer = ''
266
- // 渲染状态追踪
267
125
  const renderState = { inThink: false, inCodeBlock: false }
268
126
 
269
- // 检查是否还有孤立的代理对
270
- const hasOrphanedSurrogate = (str) => {
271
- if (!str || str.length === 0) return false
272
- let i = 0
273
- while (i < str.length) {
274
- const code = str.charCodeAt(i)
275
- if (code >= 0xD800 && code <= 0xDBFF) {
276
- const nextCode = str.charCodeAt(i + 1)
277
- if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) {
278
- i += 2
279
- continue
280
- }
281
- return true // 孤立的高代理
282
- }
283
- i++
284
- }
285
- return false
286
- }
127
+ console.log(colored('Agent:', GREEN))
128
+ console.log()
287
129
 
288
130
  for await (const chunk of this.agent.chatStream(message)) {
289
- // 检查是否被打断
290
131
  if (interrupted) break
291
132
 
292
133
  if (chunk.type === 'text') {
293
134
  lineBuffer += chunk.text
294
135
 
295
- // 如果有孤立代理对,等待下一个chunk补充
296
- if (hasOrphanedSurrogate(lineBuffer)) {
297
- continue
298
- }
299
-
300
- // 当有一行完整内容时,渲染并输出
301
136
  while (lineBuffer.includes('\n')) {
302
- // 检查是否被打断
303
137
  if (interrupted) break
304
138
 
305
139
  const nlIndex = lineBuffer.indexOf('\n')
@@ -316,7 +150,6 @@ class ChatUI extends EventEmitter {
316
150
  }
317
151
  }
318
152
 
319
- // 输出剩余内容
320
153
  if (lineBuffer.trim() && !interrupted) {
321
154
  console.log(renderLine(lineBuffer, renderState))
322
155
  }
@@ -325,11 +158,8 @@ class ChatUI extends EventEmitter {
325
158
  console.error(`\n${colored('[错误]', RED)} ${err.message}\n`)
326
159
  }
327
160
  } finally {
328
- // 移除监听器
329
161
  process.removeListener('SIGINT', interruptHandler)
330
162
  }
331
-
332
- this.prompt()
333
163
  }
334
164
  }
335
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -106,25 +106,60 @@ class SchedulerPlugin extends Plugin {
106
106
  // 注册调度工具
107
107
  framework.registerTool({
108
108
  name: 'schedule_task',
109
- description: '设置定时提醒任务。支持多种时间格式:相对时间(1 minute, 2 hours)、具体时间(12:00)、Cron表达式(* * * * *)',
109
+ description: '设置定时提醒任务。支持多种时间格式:相对时间(1 minute, 2 hours)、具体时间(12:00)、Cron表达式(* * * * *)。系统会自动判断任务是否需要 LLM 处理。',
110
110
  inputSchema: z.object({
111
111
  name: z.string().optional().describe('任务名称'),
112
112
  scheduleTime: z.string().describe('执行时间。支持格式:\n- 相对时间: "1 minute", "2 hours", "1 day"\n- 具体时间: "12:00", "14:30"\n- Cron表达式: "*/5 * * * *" (每5分钟)'),
113
- message: z.string().describe('提醒消息内容'),
113
+ message: z.string().describe('提醒消息内容。系统会自动判断:\n- 简单提醒(喝水、吃饭)直接显示\n- 需要查询/分析的任务(查看列表、分析数据)自动启用 LLM'),
114
114
  repeat: z.boolean().optional().describe('是否重复执行 (默认 false)'),
115
- cronExpression: z.string().optional().describe('Cron 表达式 (当 repeat 为 true 时使用)')
115
+ cronExpression: z.string().optional().describe('Cron 表达式 (当 repeat 为 true 时使用)'),
116
+ sessionId: z.string().optional().describe('会话 ID(提醒将发送到该会话,不填则使用默认会话)'),
117
+ llm: z.boolean().optional().describe('是否需要 LLM 处理(自动检测,可手动覆盖)')
116
118
  }),
117
119
  execute: async (args) => {
118
120
  try {
119
- const { scheduleTime, message, repeat, cronExpression } = args
121
+ const { scheduleTime, message, repeat, cronExpression, sessionId } = args
120
122
  const agent = this._getAgent()
121
123
  if (!agent) {
122
124
  return { success: false, error: 'Agent not available' }
123
125
  }
124
126
 
127
+ // 如果没有指定 sessionId,自动获取当前活跃会话
128
+ let targetSessionId = sessionId
129
+ if (!targetSessionId) {
130
+ const sessionPlugin = this._framework.pluginManager.get('session')
131
+ if (sessionPlugin) {
132
+ const sessions = sessionPlugin.listSessions()
133
+ // 获取最近的活跃会话
134
+ if (sessions.length > 0) {
135
+ // 按 lastActive 排序,取最新的
136
+ sessions.sort((a, b) => {
137
+ const aTime = a.lastActive ? new Date(a.lastActive).getTime() : 0
138
+ const bTime = b.lastActive ? new Date(b.lastActive).getTime() : 0
139
+ return bTime - aTime
140
+ })
141
+ targetSessionId = sessions[0].id
142
+ console.log(`[Scheduler] Auto-detected active session: ${targetSessionId}`)
143
+ }
144
+ }
145
+ }
146
+
125
147
  const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
126
148
  let task
127
149
 
150
+ // 自动检测是否需要 LLM 处理
151
+ const LLM_KEYWORDS = [
152
+ '分析', '查询', '查看', '检查', '总结', '搜索', '获取',
153
+ 'list', 'get', 'check', 'search', 'find', 'fetch',
154
+ '什么', '如何', '为什么', '什么时候', '多少', '谁',
155
+ '今天', '明天', '昨天', '这周', '这月', '今年'
156
+ ]
157
+ const messageLower = message.toLowerCase()
158
+ const needsLLM = args.llm === true || LLM_KEYWORDS.some(kw =>
159
+ message.includes(kw) || messageLower.includes(kw.toLowerCase())
160
+ )
161
+ const llmMode = needsLLM
162
+
128
163
  // 检测是否像 Cron 表达式
129
164
  const isCron = /^[\d*,\/-\s]+$/.test(scheduleTime) && scheduleTime.split(' ').length >= 5
130
165
 
@@ -144,7 +179,9 @@ class SchedulerPlugin extends Plugin {
144
179
  lastRun: null,
145
180
  runCount: 0,
146
181
  timer: null,
147
- cronTask: null
182
+ cronTask: null,
183
+ sessionId: targetSessionId || null,
184
+ llm: llmMode
148
185
  }
149
186
 
150
187
  // 使用 node-cron 调度
@@ -164,7 +201,9 @@ class SchedulerPlugin extends Plugin {
164
201
  createdAt: new Date(),
165
202
  lastRun: null,
166
203
  runCount: 0,
167
- timer: null
204
+ timer: null,
205
+ sessionId: targetSessionId || null,
206
+ llm: llmMode
168
207
  }
169
208
  task.timer = setTimeout(async () => {
170
209
  await this._executeTask(task)
@@ -186,7 +225,9 @@ class SchedulerPlugin extends Plugin {
186
225
  createdAt: new Date(),
187
226
  lastRun: null,
188
227
  runCount: 0,
189
- timer: null
228
+ timer: null,
229
+ sessionId: targetSessionId || null,
230
+ llm: llmMode
190
231
  }
191
232
  task.timer = setTimeout(async () => {
192
233
  await this._executeTask(task)
@@ -196,6 +237,15 @@ class SchedulerPlugin extends Plugin {
196
237
  this._tasks.set(task.id, task)
197
238
  this._taskStats.total++
198
239
 
240
+ // 发送任务创建事件
241
+ this._framework.emit('scheduler:task_created', {
242
+ taskId: task.id,
243
+ taskName: task.name,
244
+ type: task.type,
245
+ scheduleTime,
246
+ cronExpression: task.cronExpression
247
+ })
248
+
199
249
  return {
200
250
  success: true,
201
251
  taskId: task.id,
@@ -203,7 +253,9 @@ class SchedulerPlugin extends Plugin {
203
253
  scheduleTime,
204
254
  executeAt: task.runAt ? task.runAt.toISOString() : null,
205
255
  cronExpression: task.cronExpression,
206
- message: repeat ? '定时任务已创建 (重复执行)' : '提醒已设置'
256
+ message: repeat ? '定时任务已创建 (重复执行)' : '提醒已设置',
257
+ sessionId: sessionId || 'default',
258
+ llm: llmMode
207
259
  }
208
260
  } catch (err) {
209
261
  return { success: false, error: err.message }
@@ -249,7 +301,15 @@ class SchedulerPlugin extends Plugin {
249
301
  return { success: false, error: 'Task not found' }
250
302
  }
251
303
 
304
+ const taskName = task.name
252
305
  this._cancelTask(task)
306
+
307
+ // 发送任务取消事件
308
+ this._framework.emit('scheduler:task_cancelled', {
309
+ taskId: args.taskId,
310
+ taskName
311
+ })
312
+
253
313
  return { success: true, cancelled: args.taskId }
254
314
  }
255
315
  })
@@ -322,37 +382,61 @@ class SchedulerPlugin extends Plugin {
322
382
  }
323
383
 
324
384
  /**
325
- * 执行任务 - 唤醒 Agent 发送消息
385
+ * 执行任务
326
386
  */
327
387
  async _executeTask(task) {
328
- const agent = this._getAgent()
329
- if (!agent) {
330
- console.error('[Scheduler] Agent not available')
331
- return
332
- }
333
-
334
388
  console.log(`[Scheduler] Executing task: ${task.name} (${task.id})`)
335
389
  console.log(`[Scheduler] Message: ${task.message}`)
390
+ if (task.sessionId) {
391
+ console.log(`[Scheduler] Target session: ${task.sessionId}`)
392
+ }
393
+ console.log(`[Scheduler] LLM mode: ${task.llm ? 'enabled' : 'disabled'}`)
336
394
 
337
395
  task.lastRun = new Date()
338
396
  task.runCount++
339
397
  this._taskStats.running++
340
398
 
341
399
  try {
342
- // 调用 Agent 的 chat 方法发送消息
343
- const result = await agent.chat(task.message, {
344
- isScheduledTask: true
345
- })
400
+ if (task.llm) {
401
+ // LLM 模式:调用 Agent 处理
402
+ const agent = this._getAgent()
403
+ if (!agent) {
404
+ console.error('[Scheduler] Agent not available')
405
+ return
406
+ }
346
407
 
347
- this._taskStats.completed++
408
+ const result = await agent.chat(task.message, {
409
+ isScheduledTask: true,
410
+ sessionId: task.sessionId
411
+ })
348
412
 
349
- // 输出结果
350
- if (result && result.message) {
351
- console.log(`\n🔔 [定时提醒] ${result.message}\n`)
352
- } else if (result && result.text) {
353
- console.log(`\n🔔 [定时提醒] ${result.text}\n`)
413
+ this._taskStats.completed++
414
+
415
+ if (result && result.message) {
416
+ console.log(`\n🔔 [定时提醒] ${result.message}\n`)
417
+ } else if (result && result.text) {
418
+ console.log(`\n🔔 [定时提醒] ${result.text}\n`)
419
+ }
354
420
  } else {
421
+ // 直接显示模式:只显示提醒,不发 LLM
355
422
  console.log(`\n🔔 [定时提醒] ${task.message}\n`)
423
+
424
+ // 发送事件,让其他插件(如 telegram)处理通知
425
+ this._framework.emit('scheduler:reminder', {
426
+ taskId: task.id,
427
+ taskName: task.name,
428
+ message: task.message,
429
+ sessionId: task.sessionId,
430
+ llm: task.llm
431
+ })
432
+
433
+ this._framework.emit('scheduler:task_completed', {
434
+ taskId: task.id,
435
+ taskName: task.name,
436
+ type: task.type
437
+ })
438
+
439
+ this._taskStats.completed++
356
440
  }
357
441
 
358
442
  // 一次性任务执行后清理
@@ -360,11 +444,18 @@ class SchedulerPlugin extends Plugin {
360
444
  this._cleanupTask(task.id)
361
445
  }
362
446
 
363
- return result
447
+ return { success: true }
364
448
  } catch (err) {
365
449
  this._taskStats.failed++
366
450
  console.error(`[Scheduler] Task ${task.name} failed: ${err.message}`)
367
451
 
452
+ // 发送任务失败事件
453
+ this._framework.emit('scheduler:task_failed', {
454
+ taskId: task.id,
455
+ taskName: task.name,
456
+ error: err.message
457
+ })
458
+
368
459
  // 一次性任务失败后也清理
369
460
  if (task.type === 'once' && !task.cronTask) {
370
461
  this._cleanupTask(task.id)
@@ -105,6 +105,12 @@ module.exports = function(Plugin) {
105
105
  this._framework.on('agent:created', (agent) => {
106
106
  console.log('[Telegram] New agent created:', agent.name)
107
107
  })
108
+
109
+ // 监听定时提醒事件
110
+ this._framework.on('scheduler:reminder', async (data) => {
111
+ console.log('[Telegram] Received scheduler reminder:', data)
112
+ await this._handleScheduledReminder(data)
113
+ })
108
114
  }
109
115
 
110
116
  } catch (err) {
@@ -112,6 +118,35 @@ module.exports = function(Plugin) {
112
118
  }
113
119
  }
114
120
 
121
+ /**
122
+ * 处理定时提醒
123
+ */
124
+ async _handleScheduledReminder(data) {
125
+ const { taskName, message, sessionId } = data
126
+
127
+ // 如果有 sessionId 是 telegram 类型的,发送到对应 chat
128
+ if (sessionId && sessionId.startsWith('telegram_')) {
129
+ const chatId = sessionId.replace('telegram_', '')
130
+ try {
131
+ await this._bot.sendMessage(chatId, `🔔 [${taskName}]\n\n${message}`)
132
+ console.log(`[Telegram] Reminder sent to chat ${chatId}`)
133
+ } catch (err) {
134
+ console.error(`[Telegram] Failed to send reminder:`, err.message)
135
+ }
136
+ } else if (sessionId) {
137
+ // 如果是其他类型的 sessionId,尝试发送到所有活跃的 telegram 会话
138
+ for (const [chatId, session] of this._sessions) {
139
+ try {
140
+ await this._bot.sendMessage(chatId, `🔔 [${taskName}]\n\n${message}`)
141
+ console.log(`[Telegram] Reminder sent to chat ${chatId}`)
142
+ break // 只发送一个
143
+ } catch (err) {
144
+ console.error(`[Telegram] Failed to send reminder to ${chatId}:`, err.message)
145
+ }
146
+ }
147
+ }
148
+ }
149
+
115
150
  /**
116
151
  * 获取主Agent
117
152
  */