foliko 1.0.75 → 1.0.76
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 +159 -157
- package/cli/bin/foliko.js +12 -12
- package/cli/src/commands/chat.js +143 -143
- package/cli/src/commands/list.js +93 -93
- package/cli/src/index.js +75 -75
- package/cli/src/ui/chat-ui.js +201 -201
- package/cli/src/utils/ansi.js +40 -40
- package/cli/src/utils/markdown.js +292 -292
- package/examples/ambient-example.js +194 -194
- package/examples/basic.js +115 -115
- package/examples/bootstrap.js +121 -121
- package/examples/mcp-example.js +56 -56
- package/examples/skill-example.js +49 -49
- package/examples/test-chat.js +137 -137
- package/examples/test-mcp.js +85 -85
- package/examples/test-reload.js +59 -59
- package/examples/test-telegram.js +50 -50
- package/examples/test-tg-bot.js +45 -45
- package/examples/test-tg-simple.js +47 -47
- package/examples/test-tg.js +62 -62
- package/examples/test-think.js +43 -43
- package/examples/test-web-plugin.js +103 -103
- package/examples/test-weixin-feishu.js +103 -103
- package/examples/workflow.js +158 -158
- package/package.json +1 -1
- package/plugins/ai-plugin.js +102 -102
- package/plugins/ambient-agent/EventWatcher.js +113 -113
- package/plugins/ambient-agent/ExplorerLoop.js +640 -640
- package/plugins/ambient-agent/GoalManager.js +197 -197
- package/plugins/ambient-agent/Reflector.js +95 -95
- package/plugins/ambient-agent/StateStore.js +90 -90
- package/plugins/ambient-agent/constants.js +101 -101
- package/plugins/ambient-agent/index.js +579 -579
- package/plugins/audit-plugin.js +187 -187
- package/plugins/default-plugins.js +662 -662
- package/plugins/email/constants.js +64 -64
- package/plugins/email/handlers.js +461 -461
- package/plugins/email/index.js +278 -278
- package/plugins/email/monitor.js +269 -269
- package/plugins/email/parser.js +138 -138
- package/plugins/email/reply.js +151 -151
- package/plugins/email/utils.js +124 -124
- package/plugins/feishu-plugin.js +481 -481
- package/plugins/file-system-plugin.js +826 -826
- package/plugins/install-plugin.js +199 -199
- package/plugins/python-executor-plugin.js +367 -367
- package/plugins/python-plugin-loader.js +481 -481
- package/plugins/rules-plugin.js +294 -294
- package/plugins/scheduler-plugin.js +691 -691
- package/plugins/session-plugin.js +369 -369
- package/plugins/shell-executor-plugin.js +197 -197
- package/plugins/storage-plugin.js +240 -240
- package/plugins/subagent-plugin.js +845 -845
- package/plugins/telegram-plugin.js +482 -482
- package/plugins/think-plugin.js +345 -345
- package/plugins/tools-plugin.js +196 -196
- package/plugins/web-plugin.js +606 -606
- package/plugins/weixin-plugin.js +545 -545
- package/src/capabilities/index.js +11 -11
- package/src/capabilities/skill-manager.js +609 -609
- package/src/capabilities/workflow-engine.js +1109 -1109
- package/src/core/agent-chat.js +882 -882
- package/src/core/agent.js +892 -892
- package/src/core/framework.js +465 -465
- package/src/core/index.js +19 -19
- package/src/core/plugin-base.js +219 -219
- package/src/core/plugin-manager.js +863 -863
- package/src/core/provider.js +114 -114
- package/src/core/sub-agent-config.js +264 -264
- package/src/core/system-prompt-builder.js +120 -120
- package/src/core/tool-registry.js +517 -517
- package/src/core/tool-router.js +297 -297
- package/src/executors/executor-base.js +58 -58
- package/src/executors/mcp-executor.js +741 -741
- package/src/index.js +25 -25
- package/src/utils/circuit-breaker.js +301 -301
- package/src/utils/error-boundary.js +363 -363
- package/src/utils/error.js +374 -374
- package/src/utils/event-emitter.js +97 -97
- package/src/utils/id.js +133 -133
- package/src/utils/index.js +217 -217
- package/src/utils/logger.js +181 -181
- package/src/utils/plugin-helpers.js +90 -90
- package/src/utils/retry.js +122 -122
- package/src/utils/sandbox.js +292 -292
- package/test/tool-registry-validation.test.js +218 -218
- package/website/script.js +136 -136
- package/foliko-1.0.75.tgz +0 -0
|
@@ -1,691 +1,691 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scheduler 定时任务调度插件
|
|
3
|
-
* 支持 Cron 表达式、绝对时间、相对时间调度
|
|
4
|
-
* 任务触发时自动唤醒 Agent 发送消息
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { Plugin } = require('../src/core/plugin-base')
|
|
8
|
-
const { logger } = require('../src/utils/logger')
|
|
9
|
-
const log = logger.child('Scheduler')
|
|
10
|
-
const { z } = require('zod')
|
|
11
|
-
const fs = require('fs')
|
|
12
|
-
const path = require('path')
|
|
13
|
-
|
|
14
|
-
// 尝试加载 node-cron
|
|
15
|
-
let cron = null
|
|
16
|
-
try {
|
|
17
|
-
cron = require('node-cron')
|
|
18
|
-
} catch (e) {
|
|
19
|
-
log.warn(' node-cron not installed, cron tasks will not work')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ============================================================================
|
|
23
|
-
// TaskStore - 持久化任务到 .agent/data/scheduler/tasks.json
|
|
24
|
-
// ============================================================================
|
|
25
|
-
class TaskStore {
|
|
26
|
-
constructor(persistencePath) {
|
|
27
|
-
this._persistencePath = persistencePath
|
|
28
|
-
this._ensureDir()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
_ensureDir() {
|
|
32
|
-
if (!fs.existsSync(this._persistencePath)) {
|
|
33
|
-
fs.mkdirSync(this._persistencePath, { recursive: true })
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
_getTasksPath() {
|
|
38
|
-
return path.join(this._persistencePath, 'tasks.json')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
saveTasks(tasks) {
|
|
42
|
-
try {
|
|
43
|
-
// 序列化时移除不可保存的字段(timer, cronTask)
|
|
44
|
-
const serializable = tasks.map(t => ({
|
|
45
|
-
id: t.id,
|
|
46
|
-
name: t.name,
|
|
47
|
-
type: t.type,
|
|
48
|
-
message: t.message,
|
|
49
|
-
enabled: t.enabled,
|
|
50
|
-
createdAt: t.createdAt,
|
|
51
|
-
lastRun: t.lastRun,
|
|
52
|
-
runCount: t.runCount,
|
|
53
|
-
runAt: t.runAt,
|
|
54
|
-
cronExpression: t.cronExpression,
|
|
55
|
-
sessionId: t.sessionId,
|
|
56
|
-
llm: t.llm,
|
|
57
|
-
persistDelay: t.persistDelay, // 相对时间的延迟毫秒数
|
|
58
|
-
persistNextRun: t.persistNextRun // 下次执行时间
|
|
59
|
-
}))
|
|
60
|
-
fs.writeFileSync(this._getTasksPath(), JSON.stringify(serializable, null, 2))
|
|
61
|
-
} catch (err) {
|
|
62
|
-
log.error(' 保存任务失败:', err.message)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
loadTasks() {
|
|
67
|
-
try {
|
|
68
|
-
const filePath = this._getTasksPath()
|
|
69
|
-
if (fs.existsSync(filePath)) {
|
|
70
|
-
const data = fs.readFileSync(filePath, 'utf-8')
|
|
71
|
-
return JSON.parse(data)
|
|
72
|
-
}
|
|
73
|
-
} catch (err) {
|
|
74
|
-
log.error(' 加载任务失败:', err.message)
|
|
75
|
-
}
|
|
76
|
-
return []
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 时间解析辅助函数
|
|
81
|
-
function parseDelay(delayStr) {
|
|
82
|
-
const match = delayStr.match(/^(\d+)\s*(second|minute|hour|day|week)s?$/i)
|
|
83
|
-
if (!match) return null
|
|
84
|
-
const value = parseInt(match[1])
|
|
85
|
-
const unit = match[2].toLowerCase()
|
|
86
|
-
const multipliers = {
|
|
87
|
-
second: 1000,
|
|
88
|
-
minute: 60 * 1000,
|
|
89
|
-
hour: 60 * 60 * 1000,
|
|
90
|
-
day: 24 * 60 * 60 * 1000,
|
|
91
|
-
week: 7 * 24 * 60 * 60 * 1000
|
|
92
|
-
}
|
|
93
|
-
return value * (multipliers[unit] || 1000)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function parseAtTime(timeStr) {
|
|
97
|
-
const now = new Date()
|
|
98
|
-
// 简单时间格式 "12:00"
|
|
99
|
-
const timeMatch = timeStr.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
|
100
|
-
if (timeMatch) {
|
|
101
|
-
const date = new Date(now)
|
|
102
|
-
date.setHours(parseInt(timeMatch[1]), parseInt(timeMatch[2]), parseInt(timeMatch[3] || 0), 0)
|
|
103
|
-
if (date <= now) {
|
|
104
|
-
date.setDate(date.getDate() + 1)
|
|
105
|
-
}
|
|
106
|
-
return date
|
|
107
|
-
}
|
|
108
|
-
return new Date(timeStr)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 生成唯一 ID
|
|
113
|
-
*/
|
|
114
|
-
function generateId() {
|
|
115
|
-
if (require('crypto').randomUUID) {
|
|
116
|
-
return require('crypto').randomUUID()
|
|
117
|
-
}
|
|
118
|
-
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
class SchedulerPlugin extends Plugin {
|
|
122
|
-
constructor(config = {}) {
|
|
123
|
-
super()
|
|
124
|
-
this.name = 'scheduler'
|
|
125
|
-
this.version = '1.0.0'
|
|
126
|
-
this.description = '定时任务调度插件,支持 Cron 表达式、绝对时间、相对时间'
|
|
127
|
-
this.priority = 15
|
|
128
|
-
this.system = true
|
|
129
|
-
this.config = {
|
|
130
|
-
checkInterval: config.checkInterval || 1000, // 每秒检查一次
|
|
131
|
-
persistencePath: config.persistencePath || '.agent/data/scheduler'
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this._framework = null
|
|
135
|
-
this._agent = null
|
|
136
|
-
this._tasks = new Map()
|
|
137
|
-
this._timer = null
|
|
138
|
-
this._taskStore = null
|
|
139
|
-
this._taskStats = {
|
|
140
|
-
total: 0,
|
|
141
|
-
running: 0,
|
|
142
|
-
completed: 0,
|
|
143
|
-
failed: 0
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
install(framework) {
|
|
148
|
-
this._framework = framework
|
|
149
|
-
// 初始化任务存储
|
|
150
|
-
this._taskStore = new TaskStore(this.config.persistencePath)
|
|
151
|
-
return this
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 获取 Agent 实例
|
|
156
|
-
*/
|
|
157
|
-
_getAgent() {
|
|
158
|
-
if (this._agent) return this._agent
|
|
159
|
-
if (this._framework._mainAgent) {
|
|
160
|
-
this._agent = this._framework._mainAgent
|
|
161
|
-
} else {
|
|
162
|
-
const agents = this._framework._agents || []
|
|
163
|
-
this._agent = agents.length > 0 ? agents[agents.length - 1] : null
|
|
164
|
-
}
|
|
165
|
-
return this._agent
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
start(framework) {
|
|
169
|
-
// 获取 Agent 实例
|
|
170
|
-
this._agent = this._getAgent()
|
|
171
|
-
|
|
172
|
-
// 注册调度工具
|
|
173
|
-
framework.registerTool({
|
|
174
|
-
name: 'schedule_task',
|
|
175
|
-
description: '设置定时提醒任务。支持多种时间格式:相对时间(1 minute, 2 hours)、具体时间(12:00)、Cron表达式(* * * * *)。系统会自动判断任务是否需要 LLM 处理。',
|
|
176
|
-
inputSchema: z.object({
|
|
177
|
-
name: z.string().optional().describe('任务名称'),
|
|
178
|
-
scheduleTime: z.string().describe('执行时间。支持格式:\n- 相对时间: "1 minute", "2 hours", "1 day"\n- 具体时间: "12:00", "14:30"\n- Cron表达式: "*/5 * * * *" (每5分钟)'),
|
|
179
|
-
message: z.string().describe('提醒消息内容。系统会自动判断:\n- 简单提醒(喝水、吃饭)直接显示\n- 需要查询/分析的任务(查看列表、分析数据)自动启用 LLM'),
|
|
180
|
-
repeat: z.boolean().optional().describe('是否重复执行 (默认 false)'),
|
|
181
|
-
cronExpression: z.string().optional().describe('Cron 表达式 (当 repeat 为 true 时使用)'),
|
|
182
|
-
sessionId: z.string().optional().describe('会话 ID(提醒将发送到该会话,不填则使用默认会话)'),
|
|
183
|
-
llm: z.boolean().optional().describe('是否需要 LLM 处理(自动检测,可手动覆盖)')
|
|
184
|
-
}),
|
|
185
|
-
execute: async (args) => {
|
|
186
|
-
try {
|
|
187
|
-
const { scheduleTime, message, repeat, cronExpression, sessionId } = args
|
|
188
|
-
const agent = this._getAgent()
|
|
189
|
-
if (!agent) {
|
|
190
|
-
return { success: false, error: 'Agent not available' }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 如果没有指定 sessionId,优先从执行上下文获取(来自 WeChat 等消息源)
|
|
194
|
-
let targetSessionId = sessionId
|
|
195
|
-
if (!targetSessionId) {
|
|
196
|
-
const ctx = this._framework.getExecutionContext()
|
|
197
|
-
if (ctx?.sessionId) {
|
|
198
|
-
targetSessionId = ctx.sessionId
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// 如果执行上下文也没有,从 sessionPlugin 获取最近活跃会话
|
|
202
|
-
if (!targetSessionId) {
|
|
203
|
-
const sessionPlugin = this._framework.pluginManager.get('session')
|
|
204
|
-
if (sessionPlugin) {
|
|
205
|
-
const sessions = sessionPlugin.listSessions()
|
|
206
|
-
// 获取最近的活跃会话
|
|
207
|
-
if (sessions.length > 0) {
|
|
208
|
-
// 按 lastActive 排序,取最新的
|
|
209
|
-
sessions.sort((a, b) => {
|
|
210
|
-
const aTime = a.lastActive ? new Date(a.lastActive).getTime() : 0
|
|
211
|
-
const bTime = b.lastActive ? new Date(b.lastActive).getTime() : 0
|
|
212
|
-
return bTime - aTime
|
|
213
|
-
})
|
|
214
|
-
targetSessionId = sessions[0].id
|
|
215
|
-
//log.info(` Auto-detected active session: ${targetSessionId}`)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
221
|
-
let task
|
|
222
|
-
|
|
223
|
-
// 自动检测是否需要 LLM 处理
|
|
224
|
-
const LLM_KEYWORDS = [
|
|
225
|
-
'分析', '查询', '查看', '检查', '总结', '搜索', '获取',
|
|
226
|
-
'list', 'get', 'check', 'search', 'find', 'fetch',
|
|
227
|
-
'什么', '如何', '为什么', '什么时候', '多少', '谁',
|
|
228
|
-
'今天', '明天', '昨天', '这周', '这月', '今年'
|
|
229
|
-
]
|
|
230
|
-
const messageLower = message.toLowerCase()
|
|
231
|
-
const needsLLM = args.llm === true || LLM_KEYWORDS.some(kw =>
|
|
232
|
-
message.includes(kw) || messageLower.includes(kw.toLowerCase())
|
|
233
|
-
)
|
|
234
|
-
const llmMode = needsLLM
|
|
235
|
-
|
|
236
|
-
// 检测是否像 Cron 表达式
|
|
237
|
-
const isCron = /^[\d*,\/-\s]+$/.test(scheduleTime) && scheduleTime.split(' ').length >= 5
|
|
238
|
-
|
|
239
|
-
if (isCron || repeat) {
|
|
240
|
-
// Cron 任务
|
|
241
|
-
if (!cron) {
|
|
242
|
-
return { success: false, error: 'node-cron not installed' }
|
|
243
|
-
}
|
|
244
|
-
task = {
|
|
245
|
-
id: taskId,
|
|
246
|
-
name: args.name || 'CronTask',
|
|
247
|
-
type: 'cron',
|
|
248
|
-
cronExpression: cronExpression || scheduleTime,
|
|
249
|
-
message,
|
|
250
|
-
enabled: true,
|
|
251
|
-
createdAt: new Date(),
|
|
252
|
-
lastRun: null,
|
|
253
|
-
runCount: 0,
|
|
254
|
-
timer: null,
|
|
255
|
-
cronTask: null,
|
|
256
|
-
sessionId: targetSessionId || null,
|
|
257
|
-
llm: llmMode
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 使用 node-cron 调度
|
|
261
|
-
task.cronTask = cron.schedule(task.cronExpression, async () => {
|
|
262
|
-
await this._executeTask(task)
|
|
263
|
-
})
|
|
264
|
-
} else if (scheduleTime.includes(':')) {
|
|
265
|
-
// 具体时间
|
|
266
|
-
const runAt = parseAtTime(scheduleTime)
|
|
267
|
-
task = {
|
|
268
|
-
id: taskId,
|
|
269
|
-
name: args.name || 'Reminder',
|
|
270
|
-
type: 'once',
|
|
271
|
-
runAt,
|
|
272
|
-
message,
|
|
273
|
-
enabled: true,
|
|
274
|
-
createdAt: new Date(),
|
|
275
|
-
lastRun: null,
|
|
276
|
-
runCount: 0,
|
|
277
|
-
timer: null,
|
|
278
|
-
sessionId: targetSessionId || null,
|
|
279
|
-
llm: llmMode
|
|
280
|
-
}
|
|
281
|
-
task.timer = setTimeout(async () => {
|
|
282
|
-
await this._executeTask(task)
|
|
283
|
-
}, runAt.getTime() - Date.now())
|
|
284
|
-
} else {
|
|
285
|
-
// 相对时间
|
|
286
|
-
const delayMs = parseDelay(scheduleTime)
|
|
287
|
-
if (!delayMs) {
|
|
288
|
-
return { success: false, error: '无效的时间格式' }
|
|
289
|
-
}
|
|
290
|
-
const runAt = new Date(Date.now() + delayMs)
|
|
291
|
-
task = {
|
|
292
|
-
id: taskId,
|
|
293
|
-
name: args.name || 'Reminder',
|
|
294
|
-
type: 'once',
|
|
295
|
-
runAt,
|
|
296
|
-
message,
|
|
297
|
-
enabled: true,
|
|
298
|
-
createdAt: new Date(),
|
|
299
|
-
lastRun: null,
|
|
300
|
-
runCount: 0,
|
|
301
|
-
timer: null,
|
|
302
|
-
sessionId: targetSessionId || null,
|
|
303
|
-
llm: llmMode,
|
|
304
|
-
persistDelay: delayMs // 保存延迟毫秒数用于持久化
|
|
305
|
-
}
|
|
306
|
-
task.timer = setTimeout(async () => {
|
|
307
|
-
await this._executeTask(task)
|
|
308
|
-
}, delayMs)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
this._tasks.set(task.id, task)
|
|
312
|
-
this._taskStats.total++
|
|
313
|
-
this._saveTasks() // 持久化
|
|
314
|
-
|
|
315
|
-
// 发送任务创建事件
|
|
316
|
-
this._framework.emit('scheduler:task_created', {
|
|
317
|
-
taskId: task.id,
|
|
318
|
-
taskName: task.name,
|
|
319
|
-
type: task.type,
|
|
320
|
-
scheduleTime,
|
|
321
|
-
cronExpression: task.cronExpression
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
success: true,
|
|
326
|
-
taskId: task.id,
|
|
327
|
-
name: task.name,
|
|
328
|
-
scheduleTime,
|
|
329
|
-
executeAt: task.runAt ? task.runAt.toISOString() : null,
|
|
330
|
-
cronExpression: task.cronExpression,
|
|
331
|
-
message: repeat ? '定时任务已创建 (重复执行)' : '提醒已设置',
|
|
332
|
-
sessionId: sessionId || 'default',
|
|
333
|
-
llm: llmMode
|
|
334
|
-
}
|
|
335
|
-
} catch (err) {
|
|
336
|
-
return { success: false, error: err.message }
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
framework.registerTool({
|
|
342
|
-
name: 'schedule_list',
|
|
343
|
-
description: '列出所有定时任务',
|
|
344
|
-
inputSchema: z.object({}),
|
|
345
|
-
execute: async () => {
|
|
346
|
-
const tasks = Array.from(this._tasks.values()).map(t => ({
|
|
347
|
-
id: t.id,
|
|
348
|
-
name: t.name,
|
|
349
|
-
type: t.type,
|
|
350
|
-
message: t.message,
|
|
351
|
-
enabled: t.enabled,
|
|
352
|
-
nextRun: t.nextRun || t.runAt,
|
|
353
|
-
runCount: t.runCount,
|
|
354
|
-
lastRun: t.lastRun,
|
|
355
|
-
cronExpression: t.cronExpression,
|
|
356
|
-
llm: t.llm,
|
|
357
|
-
sessionId: t.sessionId
|
|
358
|
-
}))
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
success: true,
|
|
362
|
-
tasks,
|
|
363
|
-
total: tasks.length,
|
|
364
|
-
stats: { ...this._taskStats }
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
framework.registerTool({
|
|
370
|
-
name: 'schedule_cancel',
|
|
371
|
-
description: '取消定时任务',
|
|
372
|
-
inputSchema: z.object({
|
|
373
|
-
taskId: z.string().describe('任务 ID')
|
|
374
|
-
}),
|
|
375
|
-
execute: async (args) => {
|
|
376
|
-
const task = this._tasks.get(args.taskId)
|
|
377
|
-
if (!task) {
|
|
378
|
-
return { success: false, error: 'Task not found' }
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const taskName = task.name
|
|
382
|
-
this._cancelTask(task)
|
|
383
|
-
this._saveTasks() // 持久化
|
|
384
|
-
|
|
385
|
-
// 发送任务取消通知
|
|
386
|
-
this._framework.emit('notification', {
|
|
387
|
-
title: '任务已取消',
|
|
388
|
-
message: `定时任务 "${taskName}" 已取消`,
|
|
389
|
-
source: 'scheduler',
|
|
390
|
-
level: 'info',
|
|
391
|
-
sessionId: task.sessionId,
|
|
392
|
-
timestamp: new Date()
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
return { success: true, cancelled: args.taskId }
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
framework.registerTool({
|
|
400
|
-
name: 'cron_examples',
|
|
401
|
-
description: '获取常用 Cron 表达式示例',
|
|
402
|
-
inputSchema: z.object({}),
|
|
403
|
-
execute: async () => {
|
|
404
|
-
return {
|
|
405
|
-
examples: [
|
|
406
|
-
{ expression: '* * * * *', description: '每分钟' },
|
|
407
|
-
{ expression: '*/5 * * * *', description: '每5分钟' },
|
|
408
|
-
{ expression: '*/15 * * * *', description: '每15分钟' },
|
|
409
|
-
{ expression: '0 * * * *', description: '每小时' },
|
|
410
|
-
{ expression: '0 9 * * *', description: '每天早上9点' },
|
|
411
|
-
{ expression: '0 12 * * *', description: '每天中午12点' },
|
|
412
|
-
{ expression: '0 18 * * *', description: '每天下午6点' },
|
|
413
|
-
{ expression: '0 9 * * 1-5', description: '工作日上午9点' },
|
|
414
|
-
{ expression: '0 9 * * 0,6', description: '周末上午9点' },
|
|
415
|
-
{ expression: '0 9 * * 1', description: '每周一上午9点' },
|
|
416
|
-
{ expression: '0 */2 * * *', description: '每2小时' }
|
|
417
|
-
]
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
// 启动调度循环(用于检查一次性任务)
|
|
423
|
-
this._startScheduler()
|
|
424
|
-
|
|
425
|
-
// 加载持久化的任务
|
|
426
|
-
this._loadPersistedTasks()
|
|
427
|
-
|
|
428
|
-
return this
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* 启动调度循环
|
|
433
|
-
*/
|
|
434
|
-
_startScheduler() {
|
|
435
|
-
if (this._timer) {
|
|
436
|
-
clearInterval(this._timer)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
this._timer = setInterval(() => {
|
|
440
|
-
this._checkTasks()
|
|
441
|
-
}, this.config.checkInterval)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* 检查任务状态(主要用于更新 stats)
|
|
446
|
-
*/
|
|
447
|
-
_checkTasks() {
|
|
448
|
-
for (const [id, task] of this._tasks) {
|
|
449
|
-
if (!task.enabled) continue
|
|
450
|
-
// 统计信息更新
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* 保存任务到持久化存储
|
|
456
|
-
*/
|
|
457
|
-
_saveTasks() {
|
|
458
|
-
if (this._taskStore) {
|
|
459
|
-
this._taskStore.saveTasks(Array.from(this._tasks.values()))
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* 加载持久化的任务并重新调度
|
|
465
|
-
*/
|
|
466
|
-
_loadPersistedTasks() {
|
|
467
|
-
if (!this._taskStore) return
|
|
468
|
-
|
|
469
|
-
const savedTasks = this._taskStore.loadTasks()
|
|
470
|
-
if (!savedTasks || savedTasks.length === 0) return
|
|
471
|
-
|
|
472
|
-
log.info(` 加载 ${savedTasks.length} 个持久化任务...`)
|
|
473
|
-
|
|
474
|
-
for (const saved of savedTasks) {
|
|
475
|
-
// 跳过已过期的任务或已清理的任务
|
|
476
|
-
if (saved.type === 'once' && saved.runCount > 0) {
|
|
477
|
-
log.info(` 跳过已完成的一次性任务: ${saved.name}`)
|
|
478
|
-
continue
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
let task = { ...saved }
|
|
482
|
-
|
|
483
|
-
// 重新调度 Cron 任务
|
|
484
|
-
if (task.type === 'cron' && task.enabled && cron) {
|
|
485
|
-
try {
|
|
486
|
-
task.cronTask = cron.schedule(task.cronExpression, async () => {
|
|
487
|
-
await this._executeTask(task)
|
|
488
|
-
})
|
|
489
|
-
this._tasks.set(task.id, task)
|
|
490
|
-
this._taskStats.total++
|
|
491
|
-
log.info(` 已恢复 Cron 任务: ${task.name}`)
|
|
492
|
-
} catch (err) {
|
|
493
|
-
log.error(` 恢复 Cron 任务失败: ${task.name}`, err.message)
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
// 重新调度相对时间任务(计算新的执行时间)
|
|
497
|
-
else if (task.type === 'once' && task.enabled && task.persistDelay) {
|
|
498
|
-
const newDelay = task.persistDelay
|
|
499
|
-
if (newDelay > 0) {
|
|
500
|
-
task.runAt = new Date(Date.now() + newDelay)
|
|
501
|
-
task.timer = setTimeout(async () => {
|
|
502
|
-
await this._executeTask(task)
|
|
503
|
-
}, newDelay)
|
|
504
|
-
this._tasks.set(task.id, task)
|
|
505
|
-
this._taskStats.total++
|
|
506
|
-
log.info(` 已恢复一次性任务: ${task.name},将在 ${newDelay}ms 后执行`)
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
// 重新调度绝对时间任务
|
|
510
|
-
else if (task.type === 'once' && task.enabled && task.runAt) {
|
|
511
|
-
const runAt = new Date(task.runAt)
|
|
512
|
-
if (runAt > new Date()) {
|
|
513
|
-
const delay = runAt.getTime() - Date.now()
|
|
514
|
-
task.timer = setTimeout(async () => {
|
|
515
|
-
await this._executeTask(task)
|
|
516
|
-
}, delay)
|
|
517
|
-
this._tasks.set(task.id, task)
|
|
518
|
-
this._taskStats.total++
|
|
519
|
-
log.info(` 已恢复一次性任务: ${task.name},将在 ${runAt} 执行`)
|
|
520
|
-
} else {
|
|
521
|
-
log.info(` 跳过已过期的任务: ${task.name}`)
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* 取消任务
|
|
529
|
-
*/
|
|
530
|
-
_cancelTask(task) {
|
|
531
|
-
if (task.timer) {
|
|
532
|
-
clearTimeout(task.timer)
|
|
533
|
-
task.timer = null
|
|
534
|
-
}
|
|
535
|
-
if (task.cronTask) {
|
|
536
|
-
task.cronTask.stop()
|
|
537
|
-
task.cronTask = null
|
|
538
|
-
}
|
|
539
|
-
task.enabled = false
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* 执行任务
|
|
544
|
-
*/
|
|
545
|
-
async _executeTask(task) {
|
|
546
|
-
// log.info(` Executing task: ${task.name} (${task.id})`)
|
|
547
|
-
// log.info(` Message: ${task.message}`)
|
|
548
|
-
// if (task.sessionId) {
|
|
549
|
-
// log.info(` Target session: ${task.sessionId}`)
|
|
550
|
-
// }
|
|
551
|
-
// log.info(` LLM mode: ${task.llm ? 'enabled' : 'disabled'}`)
|
|
552
|
-
|
|
553
|
-
task.lastRun = new Date()
|
|
554
|
-
task.runCount++
|
|
555
|
-
this._taskStats.running++
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
if (task.llm) {
|
|
559
|
-
// LLM 模式:使用子Agent处理
|
|
560
|
-
const schedulerAgent = this._framework.createSubAgent({
|
|
561
|
-
name: 'scheduler_task',
|
|
562
|
-
role: '定时任务执行助手,专注于处理定时提醒和任务执行'
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
const result = await schedulerAgent.chat(task.message)
|
|
566
|
-
|
|
567
|
-
this._taskStats.completed++
|
|
568
|
-
|
|
569
|
-
// 获取 LLM 返回的消息
|
|
570
|
-
let responseText = ''
|
|
571
|
-
if (result && result.message) {
|
|
572
|
-
responseText = result.message
|
|
573
|
-
} else if (result && result.text) {
|
|
574
|
-
responseText = result.text
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// if (responseText) {
|
|
578
|
-
// console.log(`\n🔔 [定时提醒] ${responseText}\n`)
|
|
579
|
-
// }
|
|
580
|
-
|
|
581
|
-
// 发送统一的通知事件
|
|
582
|
-
this._framework.emit('notification', {
|
|
583
|
-
title: task.name,
|
|
584
|
-
message: responseText || task.message,
|
|
585
|
-
source: 'scheduler',
|
|
586
|
-
level: 'info',
|
|
587
|
-
sessionId: task.sessionId,
|
|
588
|
-
timestamp: new Date()
|
|
589
|
-
})
|
|
590
|
-
} else {
|
|
591
|
-
// 直接显示模式:只显示提醒,不发 LLM
|
|
592
|
-
//console.log(`\n🔔 [定时提醒] ${task.message}\n`)
|
|
593
|
-
|
|
594
|
-
// 发送统一的通知事件
|
|
595
|
-
this._framework.emit('notification', {
|
|
596
|
-
title: task.name,
|
|
597
|
-
message: task.message,
|
|
598
|
-
source: 'scheduler',
|
|
599
|
-
level: 'info',
|
|
600
|
-
sessionId: task.sessionId,
|
|
601
|
-
timestamp: new Date()
|
|
602
|
-
})
|
|
603
|
-
|
|
604
|
-
this._taskStats.completed++
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// 一次性任务执行后清理
|
|
608
|
-
if (task.type === 'once' && !task.cronTask) {
|
|
609
|
-
this._cleanupTask(task.id)
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return { success: true }
|
|
613
|
-
} catch (err) {
|
|
614
|
-
this._taskStats.failed++
|
|
615
|
-
log.error(` Task ${task.name} failed: ${err.message}`)
|
|
616
|
-
|
|
617
|
-
// 发送统一的通知事件
|
|
618
|
-
this._framework.emit('notification', {
|
|
619
|
-
title: `任务失败: ${task.name}`,
|
|
620
|
-
message: err.message,
|
|
621
|
-
source: 'scheduler',
|
|
622
|
-
level: 'error',
|
|
623
|
-
sessionId: task.sessionId,
|
|
624
|
-
timestamp: new Date()
|
|
625
|
-
})
|
|
626
|
-
|
|
627
|
-
// 一次性任务失败后也清理
|
|
628
|
-
if (task.type === 'once' && !task.cronTask) {
|
|
629
|
-
this._cleanupTask(task.id)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return { error: err.message }
|
|
633
|
-
} finally {
|
|
634
|
-
this._taskStats.running--
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* 清理任务
|
|
640
|
-
*/
|
|
641
|
-
_cleanupTask(taskId) {
|
|
642
|
-
const task = this._tasks.get(taskId)
|
|
643
|
-
if (task) {
|
|
644
|
-
if (task.timer) {
|
|
645
|
-
clearTimeout(task.timer)
|
|
646
|
-
task.timer = null
|
|
647
|
-
}
|
|
648
|
-
if (task.cronTask) {
|
|
649
|
-
task.cronTask.stop()
|
|
650
|
-
task.cronTask = null
|
|
651
|
-
}
|
|
652
|
-
this._tasks.delete(taskId)
|
|
653
|
-
this._saveTasks() // 持久化
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* 停止所有任务
|
|
659
|
-
*/
|
|
660
|
-
stopAll() {
|
|
661
|
-
for (const task of this._tasks.values()) {
|
|
662
|
-
this._cancelTask(task)
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (this._timer) {
|
|
666
|
-
clearInterval(this._timer)
|
|
667
|
-
this._timer = null
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// 保存状态
|
|
671
|
-
this._saveTasks()
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
reload(framework) {
|
|
675
|
-
this._framework = framework
|
|
676
|
-
this._agent = this._getAgent()
|
|
677
|
-
this._startScheduler()
|
|
678
|
-
// 重新加载持久化任务
|
|
679
|
-
this._loadPersistedTasks()
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
uninstall(framework) {
|
|
683
|
-
this.stopAll()
|
|
684
|
-
this._tasks.clear()
|
|
685
|
-
this._taskStats = { total: 0, running: 0, completed: 0, failed: 0 }
|
|
686
|
-
this._framework = null
|
|
687
|
-
this._agent = null
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
module.exports = { SchedulerPlugin }
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler 定时任务调度插件
|
|
3
|
+
* 支持 Cron 表达式、绝对时间、相对时间调度
|
|
4
|
+
* 任务触发时自动唤醒 Agent 发送消息
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
8
|
+
const { logger } = require('../src/utils/logger')
|
|
9
|
+
const log = logger.child('Scheduler')
|
|
10
|
+
const { z } = require('zod')
|
|
11
|
+
const fs = require('fs')
|
|
12
|
+
const path = require('path')
|
|
13
|
+
|
|
14
|
+
// 尝试加载 node-cron
|
|
15
|
+
let cron = null
|
|
16
|
+
try {
|
|
17
|
+
cron = require('node-cron')
|
|
18
|
+
} catch (e) {
|
|
19
|
+
log.warn(' node-cron not installed, cron tasks will not work')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// TaskStore - 持久化任务到 .agent/data/scheduler/tasks.json
|
|
24
|
+
// ============================================================================
|
|
25
|
+
class TaskStore {
|
|
26
|
+
constructor(persistencePath) {
|
|
27
|
+
this._persistencePath = persistencePath
|
|
28
|
+
this._ensureDir()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_ensureDir() {
|
|
32
|
+
if (!fs.existsSync(this._persistencePath)) {
|
|
33
|
+
fs.mkdirSync(this._persistencePath, { recursive: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_getTasksPath() {
|
|
38
|
+
return path.join(this._persistencePath, 'tasks.json')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
saveTasks(tasks) {
|
|
42
|
+
try {
|
|
43
|
+
// 序列化时移除不可保存的字段(timer, cronTask)
|
|
44
|
+
const serializable = tasks.map(t => ({
|
|
45
|
+
id: t.id,
|
|
46
|
+
name: t.name,
|
|
47
|
+
type: t.type,
|
|
48
|
+
message: t.message,
|
|
49
|
+
enabled: t.enabled,
|
|
50
|
+
createdAt: t.createdAt,
|
|
51
|
+
lastRun: t.lastRun,
|
|
52
|
+
runCount: t.runCount,
|
|
53
|
+
runAt: t.runAt,
|
|
54
|
+
cronExpression: t.cronExpression,
|
|
55
|
+
sessionId: t.sessionId,
|
|
56
|
+
llm: t.llm,
|
|
57
|
+
persistDelay: t.persistDelay, // 相对时间的延迟毫秒数
|
|
58
|
+
persistNextRun: t.persistNextRun // 下次执行时间
|
|
59
|
+
}))
|
|
60
|
+
fs.writeFileSync(this._getTasksPath(), JSON.stringify(serializable, null, 2))
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.error(' 保存任务失败:', err.message)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loadTasks() {
|
|
67
|
+
try {
|
|
68
|
+
const filePath = this._getTasksPath()
|
|
69
|
+
if (fs.existsSync(filePath)) {
|
|
70
|
+
const data = fs.readFileSync(filePath, 'utf-8')
|
|
71
|
+
return JSON.parse(data)
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
log.error(' 加载任务失败:', err.message)
|
|
75
|
+
}
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 时间解析辅助函数
|
|
81
|
+
function parseDelay(delayStr) {
|
|
82
|
+
const match = delayStr.match(/^(\d+)\s*(second|minute|hour|day|week)s?$/i)
|
|
83
|
+
if (!match) return null
|
|
84
|
+
const value = parseInt(match[1])
|
|
85
|
+
const unit = match[2].toLowerCase()
|
|
86
|
+
const multipliers = {
|
|
87
|
+
second: 1000,
|
|
88
|
+
minute: 60 * 1000,
|
|
89
|
+
hour: 60 * 60 * 1000,
|
|
90
|
+
day: 24 * 60 * 60 * 1000,
|
|
91
|
+
week: 7 * 24 * 60 * 60 * 1000
|
|
92
|
+
}
|
|
93
|
+
return value * (multipliers[unit] || 1000)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseAtTime(timeStr) {
|
|
97
|
+
const now = new Date()
|
|
98
|
+
// 简单时间格式 "12:00"
|
|
99
|
+
const timeMatch = timeStr.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
|
100
|
+
if (timeMatch) {
|
|
101
|
+
const date = new Date(now)
|
|
102
|
+
date.setHours(parseInt(timeMatch[1]), parseInt(timeMatch[2]), parseInt(timeMatch[3] || 0), 0)
|
|
103
|
+
if (date <= now) {
|
|
104
|
+
date.setDate(date.getDate() + 1)
|
|
105
|
+
}
|
|
106
|
+
return date
|
|
107
|
+
}
|
|
108
|
+
return new Date(timeStr)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 生成唯一 ID
|
|
113
|
+
*/
|
|
114
|
+
function generateId() {
|
|
115
|
+
if (require('crypto').randomUUID) {
|
|
116
|
+
return require('crypto').randomUUID()
|
|
117
|
+
}
|
|
118
|
+
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class SchedulerPlugin extends Plugin {
|
|
122
|
+
constructor(config = {}) {
|
|
123
|
+
super()
|
|
124
|
+
this.name = 'scheduler'
|
|
125
|
+
this.version = '1.0.0'
|
|
126
|
+
this.description = '定时任务调度插件,支持 Cron 表达式、绝对时间、相对时间'
|
|
127
|
+
this.priority = 15
|
|
128
|
+
this.system = true
|
|
129
|
+
this.config = {
|
|
130
|
+
checkInterval: config.checkInterval || 1000, // 每秒检查一次
|
|
131
|
+
persistencePath: config.persistencePath || '.agent/data/scheduler'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this._framework = null
|
|
135
|
+
this._agent = null
|
|
136
|
+
this._tasks = new Map()
|
|
137
|
+
this._timer = null
|
|
138
|
+
this._taskStore = null
|
|
139
|
+
this._taskStats = {
|
|
140
|
+
total: 0,
|
|
141
|
+
running: 0,
|
|
142
|
+
completed: 0,
|
|
143
|
+
failed: 0
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
install(framework) {
|
|
148
|
+
this._framework = framework
|
|
149
|
+
// 初始化任务存储
|
|
150
|
+
this._taskStore = new TaskStore(this.config.persistencePath)
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 获取 Agent 实例
|
|
156
|
+
*/
|
|
157
|
+
_getAgent() {
|
|
158
|
+
if (this._agent) return this._agent
|
|
159
|
+
if (this._framework._mainAgent) {
|
|
160
|
+
this._agent = this._framework._mainAgent
|
|
161
|
+
} else {
|
|
162
|
+
const agents = this._framework._agents || []
|
|
163
|
+
this._agent = agents.length > 0 ? agents[agents.length - 1] : null
|
|
164
|
+
}
|
|
165
|
+
return this._agent
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
start(framework) {
|
|
169
|
+
// 获取 Agent 实例
|
|
170
|
+
this._agent = this._getAgent()
|
|
171
|
+
|
|
172
|
+
// 注册调度工具
|
|
173
|
+
framework.registerTool({
|
|
174
|
+
name: 'schedule_task',
|
|
175
|
+
description: '设置定时提醒任务。支持多种时间格式:相对时间(1 minute, 2 hours)、具体时间(12:00)、Cron表达式(* * * * *)。系统会自动判断任务是否需要 LLM 处理。',
|
|
176
|
+
inputSchema: z.object({
|
|
177
|
+
name: z.string().optional().describe('任务名称'),
|
|
178
|
+
scheduleTime: z.string().describe('执行时间。支持格式:\n- 相对时间: "1 minute", "2 hours", "1 day"\n- 具体时间: "12:00", "14:30"\n- Cron表达式: "*/5 * * * *" (每5分钟)'),
|
|
179
|
+
message: z.string().describe('提醒消息内容。系统会自动判断:\n- 简单提醒(喝水、吃饭)直接显示\n- 需要查询/分析的任务(查看列表、分析数据)自动启用 LLM'),
|
|
180
|
+
repeat: z.boolean().optional().describe('是否重复执行 (默认 false)'),
|
|
181
|
+
cronExpression: z.string().optional().describe('Cron 表达式 (当 repeat 为 true 时使用)'),
|
|
182
|
+
sessionId: z.string().optional().describe('会话 ID(提醒将发送到该会话,不填则使用默认会话)'),
|
|
183
|
+
llm: z.boolean().optional().describe('是否需要 LLM 处理(自动检测,可手动覆盖)')
|
|
184
|
+
}),
|
|
185
|
+
execute: async (args) => {
|
|
186
|
+
try {
|
|
187
|
+
const { scheduleTime, message, repeat, cronExpression, sessionId } = args
|
|
188
|
+
const agent = this._getAgent()
|
|
189
|
+
if (!agent) {
|
|
190
|
+
return { success: false, error: 'Agent not available' }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 如果没有指定 sessionId,优先从执行上下文获取(来自 WeChat 等消息源)
|
|
194
|
+
let targetSessionId = sessionId
|
|
195
|
+
if (!targetSessionId) {
|
|
196
|
+
const ctx = this._framework.getExecutionContext()
|
|
197
|
+
if (ctx?.sessionId) {
|
|
198
|
+
targetSessionId = ctx.sessionId
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 如果执行上下文也没有,从 sessionPlugin 获取最近活跃会话
|
|
202
|
+
if (!targetSessionId) {
|
|
203
|
+
const sessionPlugin = this._framework.pluginManager.get('session')
|
|
204
|
+
if (sessionPlugin) {
|
|
205
|
+
const sessions = sessionPlugin.listSessions()
|
|
206
|
+
// 获取最近的活跃会话
|
|
207
|
+
if (sessions.length > 0) {
|
|
208
|
+
// 按 lastActive 排序,取最新的
|
|
209
|
+
sessions.sort((a, b) => {
|
|
210
|
+
const aTime = a.lastActive ? new Date(a.lastActive).getTime() : 0
|
|
211
|
+
const bTime = b.lastActive ? new Date(b.lastActive).getTime() : 0
|
|
212
|
+
return bTime - aTime
|
|
213
|
+
})
|
|
214
|
+
targetSessionId = sessions[0].id
|
|
215
|
+
//log.info(` Auto-detected active session: ${targetSessionId}`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
221
|
+
let task
|
|
222
|
+
|
|
223
|
+
// 自动检测是否需要 LLM 处理
|
|
224
|
+
const LLM_KEYWORDS = [
|
|
225
|
+
'分析', '查询', '查看', '检查', '总结', '搜索', '获取',
|
|
226
|
+
'list', 'get', 'check', 'search', 'find', 'fetch',
|
|
227
|
+
'什么', '如何', '为什么', '什么时候', '多少', '谁',
|
|
228
|
+
'今天', '明天', '昨天', '这周', '这月', '今年'
|
|
229
|
+
]
|
|
230
|
+
const messageLower = message.toLowerCase()
|
|
231
|
+
const needsLLM = args.llm === true || LLM_KEYWORDS.some(kw =>
|
|
232
|
+
message.includes(kw) || messageLower.includes(kw.toLowerCase())
|
|
233
|
+
)
|
|
234
|
+
const llmMode = needsLLM
|
|
235
|
+
|
|
236
|
+
// 检测是否像 Cron 表达式
|
|
237
|
+
const isCron = /^[\d*,\/-\s]+$/.test(scheduleTime) && scheduleTime.split(' ').length >= 5
|
|
238
|
+
|
|
239
|
+
if (isCron || repeat) {
|
|
240
|
+
// Cron 任务
|
|
241
|
+
if (!cron) {
|
|
242
|
+
return { success: false, error: 'node-cron not installed' }
|
|
243
|
+
}
|
|
244
|
+
task = {
|
|
245
|
+
id: taskId,
|
|
246
|
+
name: args.name || 'CronTask',
|
|
247
|
+
type: 'cron',
|
|
248
|
+
cronExpression: cronExpression || scheduleTime,
|
|
249
|
+
message,
|
|
250
|
+
enabled: true,
|
|
251
|
+
createdAt: new Date(),
|
|
252
|
+
lastRun: null,
|
|
253
|
+
runCount: 0,
|
|
254
|
+
timer: null,
|
|
255
|
+
cronTask: null,
|
|
256
|
+
sessionId: targetSessionId || null,
|
|
257
|
+
llm: llmMode
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 使用 node-cron 调度
|
|
261
|
+
task.cronTask = cron.schedule(task.cronExpression, async () => {
|
|
262
|
+
await this._executeTask(task)
|
|
263
|
+
})
|
|
264
|
+
} else if (scheduleTime.includes(':')) {
|
|
265
|
+
// 具体时间
|
|
266
|
+
const runAt = parseAtTime(scheduleTime)
|
|
267
|
+
task = {
|
|
268
|
+
id: taskId,
|
|
269
|
+
name: args.name || 'Reminder',
|
|
270
|
+
type: 'once',
|
|
271
|
+
runAt,
|
|
272
|
+
message,
|
|
273
|
+
enabled: true,
|
|
274
|
+
createdAt: new Date(),
|
|
275
|
+
lastRun: null,
|
|
276
|
+
runCount: 0,
|
|
277
|
+
timer: null,
|
|
278
|
+
sessionId: targetSessionId || null,
|
|
279
|
+
llm: llmMode
|
|
280
|
+
}
|
|
281
|
+
task.timer = setTimeout(async () => {
|
|
282
|
+
await this._executeTask(task)
|
|
283
|
+
}, runAt.getTime() - Date.now())
|
|
284
|
+
} else {
|
|
285
|
+
// 相对时间
|
|
286
|
+
const delayMs = parseDelay(scheduleTime)
|
|
287
|
+
if (!delayMs) {
|
|
288
|
+
return { success: false, error: '无效的时间格式' }
|
|
289
|
+
}
|
|
290
|
+
const runAt = new Date(Date.now() + delayMs)
|
|
291
|
+
task = {
|
|
292
|
+
id: taskId,
|
|
293
|
+
name: args.name || 'Reminder',
|
|
294
|
+
type: 'once',
|
|
295
|
+
runAt,
|
|
296
|
+
message,
|
|
297
|
+
enabled: true,
|
|
298
|
+
createdAt: new Date(),
|
|
299
|
+
lastRun: null,
|
|
300
|
+
runCount: 0,
|
|
301
|
+
timer: null,
|
|
302
|
+
sessionId: targetSessionId || null,
|
|
303
|
+
llm: llmMode,
|
|
304
|
+
persistDelay: delayMs // 保存延迟毫秒数用于持久化
|
|
305
|
+
}
|
|
306
|
+
task.timer = setTimeout(async () => {
|
|
307
|
+
await this._executeTask(task)
|
|
308
|
+
}, delayMs)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this._tasks.set(task.id, task)
|
|
312
|
+
this._taskStats.total++
|
|
313
|
+
this._saveTasks() // 持久化
|
|
314
|
+
|
|
315
|
+
// 发送任务创建事件
|
|
316
|
+
this._framework.emit('scheduler:task_created', {
|
|
317
|
+
taskId: task.id,
|
|
318
|
+
taskName: task.name,
|
|
319
|
+
type: task.type,
|
|
320
|
+
scheduleTime,
|
|
321
|
+
cronExpression: task.cronExpression
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
taskId: task.id,
|
|
327
|
+
name: task.name,
|
|
328
|
+
scheduleTime,
|
|
329
|
+
executeAt: task.runAt ? task.runAt.toISOString() : null,
|
|
330
|
+
cronExpression: task.cronExpression,
|
|
331
|
+
message: repeat ? '定时任务已创建 (重复执行)' : '提醒已设置',
|
|
332
|
+
sessionId: sessionId || 'default',
|
|
333
|
+
llm: llmMode
|
|
334
|
+
}
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return { success: false, error: err.message }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
framework.registerTool({
|
|
342
|
+
name: 'schedule_list',
|
|
343
|
+
description: '列出所有定时任务',
|
|
344
|
+
inputSchema: z.object({}),
|
|
345
|
+
execute: async () => {
|
|
346
|
+
const tasks = Array.from(this._tasks.values()).map(t => ({
|
|
347
|
+
id: t.id,
|
|
348
|
+
name: t.name,
|
|
349
|
+
type: t.type,
|
|
350
|
+
message: t.message,
|
|
351
|
+
enabled: t.enabled,
|
|
352
|
+
nextRun: t.nextRun || t.runAt,
|
|
353
|
+
runCount: t.runCount,
|
|
354
|
+
lastRun: t.lastRun,
|
|
355
|
+
cronExpression: t.cronExpression,
|
|
356
|
+
llm: t.llm,
|
|
357
|
+
sessionId: t.sessionId
|
|
358
|
+
}))
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
success: true,
|
|
362
|
+
tasks,
|
|
363
|
+
total: tasks.length,
|
|
364
|
+
stats: { ...this._taskStats }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
framework.registerTool({
|
|
370
|
+
name: 'schedule_cancel',
|
|
371
|
+
description: '取消定时任务',
|
|
372
|
+
inputSchema: z.object({
|
|
373
|
+
taskId: z.string().describe('任务 ID')
|
|
374
|
+
}),
|
|
375
|
+
execute: async (args) => {
|
|
376
|
+
const task = this._tasks.get(args.taskId)
|
|
377
|
+
if (!task) {
|
|
378
|
+
return { success: false, error: 'Task not found' }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const taskName = task.name
|
|
382
|
+
this._cancelTask(task)
|
|
383
|
+
this._saveTasks() // 持久化
|
|
384
|
+
|
|
385
|
+
// 发送任务取消通知
|
|
386
|
+
this._framework.emit('notification', {
|
|
387
|
+
title: '任务已取消',
|
|
388
|
+
message: `定时任务 "${taskName}" 已取消`,
|
|
389
|
+
source: 'scheduler',
|
|
390
|
+
level: 'info',
|
|
391
|
+
sessionId: task.sessionId,
|
|
392
|
+
timestamp: new Date()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
return { success: true, cancelled: args.taskId }
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
framework.registerTool({
|
|
400
|
+
name: 'cron_examples',
|
|
401
|
+
description: '获取常用 Cron 表达式示例',
|
|
402
|
+
inputSchema: z.object({}),
|
|
403
|
+
execute: async () => {
|
|
404
|
+
return {
|
|
405
|
+
examples: [
|
|
406
|
+
{ expression: '* * * * *', description: '每分钟' },
|
|
407
|
+
{ expression: '*/5 * * * *', description: '每5分钟' },
|
|
408
|
+
{ expression: '*/15 * * * *', description: '每15分钟' },
|
|
409
|
+
{ expression: '0 * * * *', description: '每小时' },
|
|
410
|
+
{ expression: '0 9 * * *', description: '每天早上9点' },
|
|
411
|
+
{ expression: '0 12 * * *', description: '每天中午12点' },
|
|
412
|
+
{ expression: '0 18 * * *', description: '每天下午6点' },
|
|
413
|
+
{ expression: '0 9 * * 1-5', description: '工作日上午9点' },
|
|
414
|
+
{ expression: '0 9 * * 0,6', description: '周末上午9点' },
|
|
415
|
+
{ expression: '0 9 * * 1', description: '每周一上午9点' },
|
|
416
|
+
{ expression: '0 */2 * * *', description: '每2小时' }
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
// 启动调度循环(用于检查一次性任务)
|
|
423
|
+
this._startScheduler()
|
|
424
|
+
|
|
425
|
+
// 加载持久化的任务
|
|
426
|
+
this._loadPersistedTasks()
|
|
427
|
+
|
|
428
|
+
return this
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 启动调度循环
|
|
433
|
+
*/
|
|
434
|
+
_startScheduler() {
|
|
435
|
+
if (this._timer) {
|
|
436
|
+
clearInterval(this._timer)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this._timer = setInterval(() => {
|
|
440
|
+
this._checkTasks()
|
|
441
|
+
}, this.config.checkInterval)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 检查任务状态(主要用于更新 stats)
|
|
446
|
+
*/
|
|
447
|
+
_checkTasks() {
|
|
448
|
+
for (const [id, task] of this._tasks) {
|
|
449
|
+
if (!task.enabled) continue
|
|
450
|
+
// 统计信息更新
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 保存任务到持久化存储
|
|
456
|
+
*/
|
|
457
|
+
_saveTasks() {
|
|
458
|
+
if (this._taskStore) {
|
|
459
|
+
this._taskStore.saveTasks(Array.from(this._tasks.values()))
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 加载持久化的任务并重新调度
|
|
465
|
+
*/
|
|
466
|
+
_loadPersistedTasks() {
|
|
467
|
+
if (!this._taskStore) return
|
|
468
|
+
|
|
469
|
+
const savedTasks = this._taskStore.loadTasks()
|
|
470
|
+
if (!savedTasks || savedTasks.length === 0) return
|
|
471
|
+
|
|
472
|
+
log.info(` 加载 ${savedTasks.length} 个持久化任务...`)
|
|
473
|
+
|
|
474
|
+
for (const saved of savedTasks) {
|
|
475
|
+
// 跳过已过期的任务或已清理的任务
|
|
476
|
+
if (saved.type === 'once' && saved.runCount > 0) {
|
|
477
|
+
log.info(` 跳过已完成的一次性任务: ${saved.name}`)
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let task = { ...saved }
|
|
482
|
+
|
|
483
|
+
// 重新调度 Cron 任务
|
|
484
|
+
if (task.type === 'cron' && task.enabled && cron) {
|
|
485
|
+
try {
|
|
486
|
+
task.cronTask = cron.schedule(task.cronExpression, async () => {
|
|
487
|
+
await this._executeTask(task)
|
|
488
|
+
})
|
|
489
|
+
this._tasks.set(task.id, task)
|
|
490
|
+
this._taskStats.total++
|
|
491
|
+
log.info(` 已恢复 Cron 任务: ${task.name}`)
|
|
492
|
+
} catch (err) {
|
|
493
|
+
log.error(` 恢复 Cron 任务失败: ${task.name}`, err.message)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// 重新调度相对时间任务(计算新的执行时间)
|
|
497
|
+
else if (task.type === 'once' && task.enabled && task.persistDelay) {
|
|
498
|
+
const newDelay = task.persistDelay
|
|
499
|
+
if (newDelay > 0) {
|
|
500
|
+
task.runAt = new Date(Date.now() + newDelay)
|
|
501
|
+
task.timer = setTimeout(async () => {
|
|
502
|
+
await this._executeTask(task)
|
|
503
|
+
}, newDelay)
|
|
504
|
+
this._tasks.set(task.id, task)
|
|
505
|
+
this._taskStats.total++
|
|
506
|
+
log.info(` 已恢复一次性任务: ${task.name},将在 ${newDelay}ms 后执行`)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// 重新调度绝对时间任务
|
|
510
|
+
else if (task.type === 'once' && task.enabled && task.runAt) {
|
|
511
|
+
const runAt = new Date(task.runAt)
|
|
512
|
+
if (runAt > new Date()) {
|
|
513
|
+
const delay = runAt.getTime() - Date.now()
|
|
514
|
+
task.timer = setTimeout(async () => {
|
|
515
|
+
await this._executeTask(task)
|
|
516
|
+
}, delay)
|
|
517
|
+
this._tasks.set(task.id, task)
|
|
518
|
+
this._taskStats.total++
|
|
519
|
+
log.info(` 已恢复一次性任务: ${task.name},将在 ${runAt} 执行`)
|
|
520
|
+
} else {
|
|
521
|
+
log.info(` 跳过已过期的任务: ${task.name}`)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 取消任务
|
|
529
|
+
*/
|
|
530
|
+
_cancelTask(task) {
|
|
531
|
+
if (task.timer) {
|
|
532
|
+
clearTimeout(task.timer)
|
|
533
|
+
task.timer = null
|
|
534
|
+
}
|
|
535
|
+
if (task.cronTask) {
|
|
536
|
+
task.cronTask.stop()
|
|
537
|
+
task.cronTask = null
|
|
538
|
+
}
|
|
539
|
+
task.enabled = false
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* 执行任务
|
|
544
|
+
*/
|
|
545
|
+
async _executeTask(task) {
|
|
546
|
+
// log.info(` Executing task: ${task.name} (${task.id})`)
|
|
547
|
+
// log.info(` Message: ${task.message}`)
|
|
548
|
+
// if (task.sessionId) {
|
|
549
|
+
// log.info(` Target session: ${task.sessionId}`)
|
|
550
|
+
// }
|
|
551
|
+
// log.info(` LLM mode: ${task.llm ? 'enabled' : 'disabled'}`)
|
|
552
|
+
|
|
553
|
+
task.lastRun = new Date()
|
|
554
|
+
task.runCount++
|
|
555
|
+
this._taskStats.running++
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
if (task.llm) {
|
|
559
|
+
// LLM 模式:使用子Agent处理
|
|
560
|
+
const schedulerAgent = this._framework.createSubAgent({
|
|
561
|
+
name: 'scheduler_task',
|
|
562
|
+
role: '定时任务执行助手,专注于处理定时提醒和任务执行'
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const result = await schedulerAgent.chat(task.message)
|
|
566
|
+
|
|
567
|
+
this._taskStats.completed++
|
|
568
|
+
|
|
569
|
+
// 获取 LLM 返回的消息
|
|
570
|
+
let responseText = ''
|
|
571
|
+
if (result && result.message) {
|
|
572
|
+
responseText = result.message
|
|
573
|
+
} else if (result && result.text) {
|
|
574
|
+
responseText = result.text
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// if (responseText) {
|
|
578
|
+
// console.log(`\n🔔 [定时提醒] ${responseText}\n`)
|
|
579
|
+
// }
|
|
580
|
+
|
|
581
|
+
// 发送统一的通知事件
|
|
582
|
+
this._framework.emit('notification', {
|
|
583
|
+
title: task.name,
|
|
584
|
+
message: responseText || task.message,
|
|
585
|
+
source: 'scheduler',
|
|
586
|
+
level: 'info',
|
|
587
|
+
sessionId: task.sessionId,
|
|
588
|
+
timestamp: new Date()
|
|
589
|
+
})
|
|
590
|
+
} else {
|
|
591
|
+
// 直接显示模式:只显示提醒,不发 LLM
|
|
592
|
+
//console.log(`\n🔔 [定时提醒] ${task.message}\n`)
|
|
593
|
+
|
|
594
|
+
// 发送统一的通知事件
|
|
595
|
+
this._framework.emit('notification', {
|
|
596
|
+
title: task.name,
|
|
597
|
+
message: task.message,
|
|
598
|
+
source: 'scheduler',
|
|
599
|
+
level: 'info',
|
|
600
|
+
sessionId: task.sessionId,
|
|
601
|
+
timestamp: new Date()
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
this._taskStats.completed++
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 一次性任务执行后清理
|
|
608
|
+
if (task.type === 'once' && !task.cronTask) {
|
|
609
|
+
this._cleanupTask(task.id)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { success: true }
|
|
613
|
+
} catch (err) {
|
|
614
|
+
this._taskStats.failed++
|
|
615
|
+
log.error(` Task ${task.name} failed: ${err.message}`)
|
|
616
|
+
|
|
617
|
+
// 发送统一的通知事件
|
|
618
|
+
this._framework.emit('notification', {
|
|
619
|
+
title: `任务失败: ${task.name}`,
|
|
620
|
+
message: err.message,
|
|
621
|
+
source: 'scheduler',
|
|
622
|
+
level: 'error',
|
|
623
|
+
sessionId: task.sessionId,
|
|
624
|
+
timestamp: new Date()
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
// 一次性任务失败后也清理
|
|
628
|
+
if (task.type === 'once' && !task.cronTask) {
|
|
629
|
+
this._cleanupTask(task.id)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return { error: err.message }
|
|
633
|
+
} finally {
|
|
634
|
+
this._taskStats.running--
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 清理任务
|
|
640
|
+
*/
|
|
641
|
+
_cleanupTask(taskId) {
|
|
642
|
+
const task = this._tasks.get(taskId)
|
|
643
|
+
if (task) {
|
|
644
|
+
if (task.timer) {
|
|
645
|
+
clearTimeout(task.timer)
|
|
646
|
+
task.timer = null
|
|
647
|
+
}
|
|
648
|
+
if (task.cronTask) {
|
|
649
|
+
task.cronTask.stop()
|
|
650
|
+
task.cronTask = null
|
|
651
|
+
}
|
|
652
|
+
this._tasks.delete(taskId)
|
|
653
|
+
this._saveTasks() // 持久化
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* 停止所有任务
|
|
659
|
+
*/
|
|
660
|
+
stopAll() {
|
|
661
|
+
for (const task of this._tasks.values()) {
|
|
662
|
+
this._cancelTask(task)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (this._timer) {
|
|
666
|
+
clearInterval(this._timer)
|
|
667
|
+
this._timer = null
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 保存状态
|
|
671
|
+
this._saveTasks()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
reload(framework) {
|
|
675
|
+
this._framework = framework
|
|
676
|
+
this._agent = this._getAgent()
|
|
677
|
+
this._startScheduler()
|
|
678
|
+
// 重新加载持久化任务
|
|
679
|
+
this._loadPersistedTasks()
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
uninstall(framework) {
|
|
683
|
+
this.stopAll()
|
|
684
|
+
this._tasks.clear()
|
|
685
|
+
this._taskStats = { total: 0, running: 0, completed: 0, failed: 0 }
|
|
686
|
+
this._framework = null
|
|
687
|
+
this._agent = null
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
module.exports = { SchedulerPlugin }
|