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.
- package/.claude/settings.local.json +3 -1
- package/SPEC.md +27 -1
- package/cli/src/ui/chat-ui.js +53 -223
- package/package.json +1 -1
- package/plugins/scheduler-plugin.js +117 -26
- package/plugins/telegram-plugin.js +35 -0
|
@@ -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`) - 邮件收发
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -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 {
|
|
8
|
+
const { renderLine } = require('../utils/markdown')
|
|
10
9
|
|
|
11
|
-
class ChatUI
|
|
10
|
+
class ChatUI {
|
|
12
11
|
constructor(agent) {
|
|
13
|
-
super()
|
|
14
12
|
this.agent = agent
|
|
15
13
|
this.rl = null
|
|
16
|
-
this.
|
|
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
|
-
|
|
51
|
-
|
|
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('
|
|
36
|
+
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('连续两次回车', DIM)} 发送多行 | ${colored('!!', DIM)} 立即发送\n`)
|
|
81
37
|
}
|
|
82
38
|
|
|
83
39
|
/**
|
|
84
|
-
*
|
|
40
|
+
* 获取多行输入
|
|
85
41
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
}
|
|
56
|
+
// 空行:结束输入并发送(第一次空行就发送)
|
|
57
|
+
if (input.trim() === '') {
|
|
58
|
+
const result = lines.join('\n').trim()
|
|
59
|
+
resolve(result)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
119
62
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
153
|
-
}
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.
|
|
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
|
-
|
|
92
|
+
// 发送消息
|
|
93
|
+
await this.sendMessage(input)
|
|
206
94
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
* 执行任务
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
408
|
+
const result = await agent.chat(task.message, {
|
|
409
|
+
isScheduledTask: true,
|
|
410
|
+
sessionId: task.sessionId
|
|
411
|
+
})
|
|
348
412
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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
|
*/
|