foliko 2.0.6 → 2.0.8
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 +2 -1
- package/docs/public-api.md +3 -3
- package/examples/workflow.js +62 -57
- package/package.json +2 -3
- package/plugins/core/scheduler/CHANGELOG.md +31 -0
- package/plugins/core/scheduler/index.js +576 -638
- package/plugins/core/workflow/context.js +941 -0
- package/plugins/core/workflow/engine.js +66 -0
- package/plugins/core/workflow/examples/01-basic.js +42 -0
- package/plugins/core/workflow/examples/01-basic.json +30 -0
- package/plugins/core/workflow/examples/02-choice.js +75 -0
- package/plugins/core/workflow/examples/02-choice.json +59 -0
- package/plugins/core/workflow/examples/03-chain-style.js +114 -0
- package/plugins/core/workflow/examples/03-each.json +41 -0
- package/plugins/core/workflow/examples/04-parallel.js +52 -0
- package/plugins/core/workflow/examples/04-parallel.json +51 -0
- package/plugins/core/workflow/examples/05-chain-style.json +68 -0
- package/plugins/core/workflow/examples/05-each.js +65 -0
- package/plugins/core/workflow/examples/06-script.js +82 -0
- package/plugins/core/workflow/examples/07-module-export.js +43 -0
- package/plugins/core/workflow/examples/08-default-export.js +29 -0
- package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
- package/plugins/core/workflow/examples/10-logger.js +34 -0
- package/plugins/core/workflow/examples/11-storage.js +68 -0
- package/plugins/core/workflow/examples/simple.js +77 -0
- package/plugins/core/workflow/examples/simple.json +75 -0
- package/plugins/core/workflow/index.js +124 -58
- package/plugins/core/workflow/js-runner.js +318 -0
- package/plugins/core/workflow/json-runner.js +323 -0
- package/plugins/core/workflow/stages/action.js +211 -0
- package/plugins/core/workflow/stages/choice.js +74 -0
- package/plugins/core/workflow/stages/delay.js +73 -0
- package/plugins/core/workflow/stages/each.js +123 -0
- package/plugins/core/workflow/stages/parallel.js +69 -0
- package/plugins/core/workflow/stages/try.js +142 -0
- package/plugins/executors/data-splitter/index.js +3 -2
- package/plugins/memory/index.js +10 -3
- package/sandbox/check-context.js +5 -0
- package/sandbox/test-context.js +27 -0
- package/sandbox/test-fixes.js +40 -0
- package/sandbox/test-hello-js.js +46 -0
- package/skills/workflows/SKILL.md +715 -613
- package/src/agent/chat.js +8 -1
- package/src/agent/main.js +5 -1
- package/src/cli/ui/chat-ui.js +4 -3
- package/src/common/json-safe.js +20 -0
- package/src/framework/framework.js +58 -2
- package/src/index.js +10 -0
- package/src/plugin/base.js +7 -4
- package/src/plugin/manager.js +95 -1
- package/src/utils/sandbox.js +1 -1
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -331
|
@@ -1,791 +1,729 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scheduler 定时任务调度插件
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Scheduler 定时任务调度插件 (v2)
|
|
3
|
+
*
|
|
4
|
+
* 基于 croner 重新实现,关键改进:
|
|
5
|
+
* - 用绝对时间戳校验,事件循环阻塞恢复后自动 catch-up(解决"漏触发")
|
|
6
|
+
* - 任务重叠保护(cron 短间隔 vs LLM 慢响应)
|
|
7
|
+
* - 原子化 + debounce 持久化(避免写盘半残、减少 IO)
|
|
8
|
+
* - 过期一次性任务恢复时立即补触发(catch-up 语义)
|
|
9
|
+
* - SubAgent 复用(避免长跑漏内存)
|
|
10
|
+
* - 统一 shell / 普通 任务模型(去除重复代码)
|
|
11
|
+
* - 删除死代码(_checkTasks / _taskStats / _startScheduler)
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
|
-
const { Plugin } = require('../../../src/plugin/base')
|
|
8
|
-
const { logger } = require('../../../src/common/logger')
|
|
9
|
-
const
|
|
10
|
-
const { z } = require('zod')
|
|
11
|
-
const fs = require('fs')
|
|
12
|
-
const path = require('path')
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const { Plugin } = require('../../../src/plugin/base');
|
|
15
|
+
const { logger } = require('../../../src/common/logger');
|
|
16
|
+
const { Cron } = require('croner');
|
|
17
|
+
const { z } = require('zod');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { randomUUID } = require('crypto');
|
|
21
|
+
|
|
22
|
+
const log = logger.child('Scheduler');
|
|
23
|
+
|
|
24
|
+
// 过期一次性任务的最大补触发容忍时间(24h);超过则直接丢弃
|
|
25
|
+
const ONCE_EXPIRY_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
// 持久化 debounce 时间窗
|
|
27
|
+
const PERSIST_DEBOUNCE_MS = 500;
|
|
28
|
+
|
|
29
|
+
// 简单关键词检测:判断 reminder 消息是否需要 LLM 处理
|
|
30
|
+
const LLM_KEYWORDS = [
|
|
31
|
+
'分析', '查询', '查看', '检查', '总结', '搜索', '获取',
|
|
32
|
+
'list', 'get', 'check', 'search', 'find', 'fetch',
|
|
33
|
+
'什么', '如何', '为什么', '什么时候', '多少', '谁',
|
|
34
|
+
'今天', '明天', '昨天', '这周', '这月', '今年',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// 持久化存储:原子写 + 加载
|
|
39
|
+
// ============================================================
|
|
21
40
|
class TaskStore {
|
|
22
41
|
constructor(persistencePath) {
|
|
23
|
-
this._persistencePath = persistencePath
|
|
24
|
-
this._ensureDir()
|
|
42
|
+
this._persistencePath = persistencePath;
|
|
43
|
+
this._ensureDir();
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
_ensureDir() {
|
|
28
47
|
if (!fs.existsSync(this._persistencePath)) {
|
|
29
|
-
fs.mkdirSync(this._persistencePath, { recursive: true })
|
|
48
|
+
fs.mkdirSync(this._persistencePath, { recursive: true });
|
|
30
49
|
}
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
_getTasksPath() {
|
|
34
|
-
return path.join(this._persistencePath, 'tasks.json')
|
|
53
|
+
return path.join(this._persistencePath, 'tasks.json');
|
|
35
54
|
}
|
|
36
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 原子写:先写 .tmp,再 rename(同卷下原子替换)
|
|
58
|
+
* 避免进程在 writeFileSync 中途崩溃导致 tasks.json 半残
|
|
59
|
+
*/
|
|
37
60
|
saveTasks(tasks) {
|
|
61
|
+
const finalPath = this._getTasksPath();
|
|
62
|
+
const tmpPath = finalPath + '.tmp';
|
|
38
63
|
try {
|
|
39
|
-
const serializable = tasks.map(t => ({
|
|
64
|
+
const serializable = tasks.map((t) => ({
|
|
40
65
|
id: t.id,
|
|
41
66
|
name: t.name,
|
|
42
67
|
type: t.type,
|
|
43
68
|
message: t.message,
|
|
69
|
+
shell: t.shell || null,
|
|
44
70
|
enabled: t.enabled,
|
|
45
71
|
createdAt: t.createdAt,
|
|
46
72
|
lastRun: t.lastRun,
|
|
47
73
|
runCount: t.runCount,
|
|
48
74
|
runAt: t.runAt,
|
|
49
75
|
cronExpression: t.cronExpression,
|
|
76
|
+
timezone: t.timezone,
|
|
50
77
|
sessionId: t.sessionId,
|
|
51
78
|
llm: t.llm,
|
|
52
|
-
persistDelay: t.persistDelay,
|
|
53
|
-
persistNextRun: t.persistNextRun,
|
|
54
79
|
notify: t.notify,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fs.
|
|
80
|
+
}));
|
|
81
|
+
fs.writeFileSync(tmpPath, JSON.stringify(serializable, null, 2), 'utf-8');
|
|
82
|
+
fs.renameSync(tmpPath, finalPath);
|
|
58
83
|
} catch (err) {
|
|
59
|
-
log.error(' 保存任务失败:', err.message)
|
|
84
|
+
log.error(' 保存任务失败:', err.message);
|
|
60
85
|
}
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
loadTasks() {
|
|
64
89
|
try {
|
|
65
|
-
const filePath = this._getTasksPath()
|
|
90
|
+
const filePath = this._getTasksPath();
|
|
66
91
|
if (fs.existsSync(filePath)) {
|
|
67
|
-
const data = fs.readFileSync(filePath, 'utf-8')
|
|
68
|
-
return JSON.parse(data)
|
|
92
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
93
|
+
return JSON.parse(data);
|
|
69
94
|
}
|
|
70
95
|
} catch (err) {
|
|
71
|
-
log.error(' 加载任务失败:', err.message)
|
|
96
|
+
log.error(' 加载任务失败:', err.message);
|
|
72
97
|
}
|
|
73
|
-
return []
|
|
98
|
+
return [];
|
|
74
99
|
}
|
|
75
100
|
}
|
|
76
101
|
|
|
102
|
+
// ============================================================
|
|
103
|
+
// 时间解析工具
|
|
104
|
+
// ============================================================
|
|
77
105
|
function parseDelay(delayStr) {
|
|
78
|
-
const match = delayStr.match(/^(\d+)\s*(second|minute|hour|day|week)s?$/i)
|
|
79
|
-
if (!match) return null
|
|
80
|
-
const value = parseInt(match[1])
|
|
81
|
-
const unit = match[2].toLowerCase()
|
|
106
|
+
const match = delayStr.match(/^(\d+)\s*(second|minute|hour|day|week)s?$/i);
|
|
107
|
+
if (!match) return null;
|
|
108
|
+
const value = parseInt(match[1], 10);
|
|
109
|
+
const unit = match[2].toLowerCase();
|
|
82
110
|
const multipliers = {
|
|
83
111
|
second: 1000,
|
|
84
112
|
minute: 60 * 1000,
|
|
85
113
|
hour: 60 * 60 * 1000,
|
|
86
114
|
day: 24 * 60 * 60 * 1000,
|
|
87
|
-
week: 7 * 24 * 60 * 60 * 1000
|
|
88
|
-
}
|
|
89
|
-
return value * (multipliers[unit] || 1000)
|
|
115
|
+
week: 7 * 24 * 60 * 60 * 1000,
|
|
116
|
+
};
|
|
117
|
+
return value * (multipliers[unit] || 1000);
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
function parseAtTime(timeStr) {
|
|
93
|
-
const now = new Date()
|
|
94
|
-
const timeMatch = timeStr.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
|
121
|
+
const now = new Date();
|
|
122
|
+
const timeMatch = timeStr.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
95
123
|
if (timeMatch) {
|
|
96
|
-
const date = new Date(now)
|
|
97
|
-
date.setHours(parseInt(timeMatch[1]), parseInt(timeMatch[2]), parseInt(timeMatch[3] || 0), 0)
|
|
124
|
+
const date = new Date(now);
|
|
125
|
+
date.setHours(parseInt(timeMatch[1], 10), parseInt(timeMatch[2], 10), parseInt(timeMatch[3] || 0, 10), 0);
|
|
98
126
|
if (date <= now) {
|
|
99
|
-
date.setDate(date.getDate() + 1)
|
|
127
|
+
date.setDate(date.getDate() + 1);
|
|
100
128
|
}
|
|
101
|
-
return date
|
|
129
|
+
return date;
|
|
102
130
|
}
|
|
103
|
-
return new Date(timeStr)
|
|
131
|
+
return new Date(timeStr);
|
|
104
132
|
}
|
|
105
133
|
|
|
106
|
-
function
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
134
|
+
function detectLLM(message, explicitFlag) {
|
|
135
|
+
if (explicitFlag === true) return true;
|
|
136
|
+
if (explicitFlag === false) return false;
|
|
137
|
+
const lower = message.toLowerCase();
|
|
138
|
+
return LLM_KEYWORDS.some((kw) => message.includes(kw) || lower.includes(kw.toLowerCase()));
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
// ============================================================
|
|
142
|
+
// SchedulerPlugin
|
|
143
|
+
// ============================================================
|
|
113
144
|
class SchedulerPlugin extends Plugin {
|
|
114
145
|
constructor(config = {}) {
|
|
115
|
-
super()
|
|
116
|
-
this.name = 'scheduler'
|
|
117
|
-
this.version = '
|
|
118
|
-
this.description = '
|
|
119
|
-
this.priority = 15
|
|
120
|
-
this.system = true
|
|
121
|
-
this.config = {
|
|
122
|
-
checkInterval: config.checkInterval || 1000,
|
|
123
|
-
persistencePath: config.persistencePath || '.foliko/data/scheduler'
|
|
124
|
-
}
|
|
146
|
+
super();
|
|
147
|
+
this.name = 'scheduler';
|
|
148
|
+
this.version = '2.0.0';
|
|
149
|
+
this.description = '定时任务调度 v2(croner 引擎,事件循环阻塞自愈 + 持久化 + 重叠保护)';
|
|
150
|
+
this.priority = 15;
|
|
151
|
+
this.system = true;
|
|
125
152
|
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
this.config = {
|
|
154
|
+
persistencePath: config.persistencePath || '.foliko/data/scheduler',
|
|
155
|
+
timezone: config.timezone || process.env.TZ || 'Asia/Shanghai',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this._framework = null;
|
|
159
|
+
this._agent = null;
|
|
160
|
+
this._tasks = new Map();
|
|
161
|
+
this._taskStore = new TaskStore(this.config.persistencePath);
|
|
162
|
+
// 复用的 LLM 执行 subagent(避免每次 tick 新建导致 _agents 数组漏内存)
|
|
163
|
+
this._schedulerSubAgent = null;
|
|
164
|
+
// debounce 写入
|
|
165
|
+
this._persistTimer = null;
|
|
166
|
+
this._persistDirty = false;
|
|
137
167
|
}
|
|
138
168
|
|
|
139
169
|
install(framework) {
|
|
140
|
-
this._framework = framework
|
|
141
|
-
|
|
142
|
-
return this
|
|
170
|
+
this._framework = framework;
|
|
171
|
+
return this;
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
_getAgent() {
|
|
146
|
-
if (this._agent) return this._agent
|
|
175
|
+
if (this._agent) return this._agent;
|
|
176
|
+
if (!this._framework) return null;
|
|
147
177
|
if (this._framework._mainAgent) {
|
|
148
|
-
this._agent = this._framework._mainAgent
|
|
178
|
+
this._agent = this._framework._mainAgent;
|
|
149
179
|
} else {
|
|
150
|
-
const agents = this._framework._agents || []
|
|
151
|
-
this._agent = agents.length > 0 ? agents[agents.length - 1] : null
|
|
180
|
+
const agents = this._framework._agents || [];
|
|
181
|
+
this._agent = agents.length > 0 ? agents[agents.length - 1] : null;
|
|
152
182
|
}
|
|
153
|
-
return this._agent
|
|
183
|
+
return this._agent;
|
|
154
184
|
}
|
|
155
185
|
|
|
186
|
+
_getSchedulerSubAgent() {
|
|
187
|
+
if (this._schedulerSubAgent) return this._schedulerSubAgent;
|
|
188
|
+
if (!this._framework) return null;
|
|
189
|
+
this._schedulerSubAgent = this._framework.createSubAgent({
|
|
190
|
+
name: 'scheduler_task',
|
|
191
|
+
role: '定时任务执行助手,专注于处理定时提醒和任务执行',
|
|
192
|
+
hidden: true,
|
|
193
|
+
});
|
|
194
|
+
return this._schedulerSubAgent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================
|
|
198
|
+
// 工具注册
|
|
199
|
+
// ============================================================
|
|
156
200
|
start(framework) {
|
|
157
|
-
this._agent = this._getAgent()
|
|
201
|
+
this._agent = this._getAgent();
|
|
158
202
|
|
|
159
203
|
framework.registerTool({
|
|
160
204
|
name: 'schedule_task',
|
|
161
205
|
description: '设置定时提醒任务。支持多种时间格式:相对时间(1 minute, 2 hours)、具体时间(12:00)、Cron表达式(* * * * *)。系统会自动判断任务是否需要 LLM 处理。',
|
|
162
206
|
inputSchema: z.object({
|
|
163
207
|
name: z.string().optional().describe('任务名称'),
|
|
164
|
-
scheduleTime: z.string().describe(
|
|
165
|
-
|
|
208
|
+
scheduleTime: z.string().describe(
|
|
209
|
+
'执行时间。支持格式:\n- 相对时间: "1 minute", "2 hours", "1 day"\n- 具体时间: "12:00", "14:30"\n- Cron表达式: "*/5 * * * *" (每5分钟)'
|
|
210
|
+
),
|
|
211
|
+
message: z.string().describe('提醒消息内容(shell 模式下作为命令说明)'),
|
|
166
212
|
repeat: z.boolean().optional().describe('是否重复执行 (默认 false)'),
|
|
167
213
|
cronExpression: z.string().optional().describe('Cron 表达式 (当 repeat 为 true 时使用)'),
|
|
168
214
|
sessionId: z.string().optional().describe('会话 ID(提醒将发送到该会话,不填则使用默认会话)'),
|
|
169
215
|
llm: z.boolean().optional().describe('是否需要 LLM 处理(自动检测,可手动覆盖)'),
|
|
170
|
-
notify: z.boolean().optional().describe('任务完成后是否发送通知(默认 true
|
|
171
|
-
shell: z.string().optional().describe('要执行的 Shell/Bash 命令(如 echo hello, ls -la)')
|
|
216
|
+
notify: z.boolean().optional().describe('任务完成后是否发送通知(默认 true)'),
|
|
217
|
+
shell: z.string().optional().describe('要执行的 Shell/Bash 命令(如 echo hello, ls -la)'),
|
|
172
218
|
}),
|
|
173
|
-
execute: async (args) =>
|
|
174
|
-
|
|
175
|
-
const { scheduleTime, message, repeat, cronExpression, sessionId, shell } = args
|
|
176
|
-
const agent = this._getAgent()
|
|
177
|
-
if (!agent) {
|
|
178
|
-
return { success: false, error: 'Agent not available' }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (shell) {
|
|
182
|
-
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
183
|
-
const isCron = /^[\d*,\/-\s]+$/.test(scheduleTime) && scheduleTime.split(' ').length >= 5
|
|
184
|
-
|
|
185
|
-
if (isCron || repeat) {
|
|
186
|
-
if (!schedule) {
|
|
187
|
-
return { success: false, error: 'node-schedule not installed' }
|
|
188
|
-
}
|
|
189
|
-
const task = {
|
|
190
|
-
id: taskId,
|
|
191
|
-
name: args.name || 'ShellTask',
|
|
192
|
-
type: 'shell',
|
|
193
|
-
cronExpression: cronExpression || scheduleTime,
|
|
194
|
-
message: message || shell,
|
|
195
|
-
enabled: true,
|
|
196
|
-
createdAt: new Date(),
|
|
197
|
-
lastRun: null,
|
|
198
|
-
runCount: 0,
|
|
199
|
-
timer: null,
|
|
200
|
-
cronTask: null,
|
|
201
|
-
sessionId: sessionId || null,
|
|
202
|
-
shell,
|
|
203
|
-
notify: args.notify !== false
|
|
204
|
-
}
|
|
205
|
-
task.cronTask = schedule.scheduleJob(task.cronExpression, async () => {
|
|
206
|
-
await this._executeTask(task)
|
|
207
|
-
})
|
|
208
|
-
this._tasks.set(task.id, task)
|
|
209
|
-
this._taskStats.total++
|
|
210
|
-
this._saveTasks()
|
|
211
|
-
return {
|
|
212
|
-
success: true,
|
|
213
|
-
data: repeat ? 'Shell 定时任务已创建 (重复执行)' : 'Shell 任务已创建',
|
|
214
|
-
metadata: {
|
|
215
|
-
taskId: task.id,
|
|
216
|
-
name: task.name,
|
|
217
|
-
type: 'shell',
|
|
218
|
-
shell,
|
|
219
|
-
scheduleTime,
|
|
220
|
-
cronExpression: task.cronExpression,
|
|
221
|
-
sessionId: sessionId || 'default',
|
|
222
|
-
notify: task.notify
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
} else {
|
|
226
|
-
const delayMs = parseDelay(scheduleTime)
|
|
227
|
-
if (!delayMs && !scheduleTime.includes(':')) {
|
|
228
|
-
return { success: false, error: '无效的时间格式' }
|
|
229
|
-
}
|
|
230
|
-
let runAt = null
|
|
231
|
-
let delay = 0
|
|
232
|
-
if (scheduleTime.includes(':')) {
|
|
233
|
-
runAt = parseAtTime(scheduleTime)
|
|
234
|
-
delay = runAt.getTime() - Date.now()
|
|
235
|
-
} else {
|
|
236
|
-
delay = parseDelay(scheduleTime)
|
|
237
|
-
runAt = new Date(Date.now() + delay)
|
|
238
|
-
}
|
|
239
|
-
const task = {
|
|
240
|
-
id: taskId,
|
|
241
|
-
name: args.name || 'ShellTask',
|
|
242
|
-
type: 'shell',
|
|
243
|
-
runAt,
|
|
244
|
-
message: message || shell,
|
|
245
|
-
enabled: true,
|
|
246
|
-
createdAt: new Date(),
|
|
247
|
-
lastRun: null,
|
|
248
|
-
runCount: 0,
|
|
249
|
-
timer: null,
|
|
250
|
-
sessionId: sessionId || null,
|
|
251
|
-
shell,
|
|
252
|
-
persistDelay: delay,
|
|
253
|
-
notify: args.notify !== false
|
|
254
|
-
}
|
|
255
|
-
task.timer = setTimeout(async () => {
|
|
256
|
-
await this._executeTask(task)
|
|
257
|
-
}, delay)
|
|
258
|
-
this._tasks.set(task.id, task)
|
|
259
|
-
this._taskStats.total++
|
|
260
|
-
this._saveTasks()
|
|
261
|
-
return {
|
|
262
|
-
success: true,
|
|
263
|
-
data: 'Shell 任务已创建',
|
|
264
|
-
metadata: {
|
|
265
|
-
taskId: task.id,
|
|
266
|
-
name: task.name,
|
|
267
|
-
type: 'shell',
|
|
268
|
-
shell,
|
|
269
|
-
scheduleTime,
|
|
270
|
-
executeAt: runAt.toISOString(),
|
|
271
|
-
sessionId: sessionId || 'default',
|
|
272
|
-
notify: task.notify
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let targetSessionId = sessionId
|
|
279
|
-
if (!targetSessionId) {
|
|
280
|
-
const ctx = this._framework.getExecutionContext()
|
|
281
|
-
if (ctx?.sessionId) {
|
|
282
|
-
targetSessionId = ctx.sessionId
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (!targetSessionId) {
|
|
286
|
-
const sessionPlugin = this._framework.pluginManager.get('session')
|
|
287
|
-
if (sessionPlugin) {
|
|
288
|
-
const sessions = sessionPlugin.listSessions()
|
|
289
|
-
if (sessions.length > 0) {
|
|
290
|
-
sessions.sort((a, b) => {
|
|
291
|
-
const aTime = a.lastActive ? new Date(a.lastActive).getTime() : 0
|
|
292
|
-
const bTime = b.lastActive ? new Date(b.lastActive).getTime() : 0
|
|
293
|
-
return bTime - aTime
|
|
294
|
-
})
|
|
295
|
-
targetSessionId = sessions[0].id
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
301
|
-
let task
|
|
302
|
-
|
|
303
|
-
const LLM_KEYWORDS = [
|
|
304
|
-
'分析', '查询', '查看', '检查', '总结', '搜索', '获取',
|
|
305
|
-
'list', 'get', 'check', 'search', 'find', 'fetch',
|
|
306
|
-
'什么', '如何', '为什么', '什么时候', '多少', '谁',
|
|
307
|
-
'今天', '明天', '昨天', '这周', '这月', '今年'
|
|
308
|
-
]
|
|
309
|
-
const messageLower = message.toLowerCase()
|
|
310
|
-
const needsLLM = args.llm === true || LLM_KEYWORDS.some(kw =>
|
|
311
|
-
message.includes(kw) || messageLower.includes(kw.toLowerCase())
|
|
312
|
-
)
|
|
313
|
-
const llmMode = needsLLM
|
|
314
|
-
|
|
315
|
-
const isCron = /^[\d*,\/-\s]+$/.test(scheduleTime) && scheduleTime.split(' ').length >= 5
|
|
316
|
-
|
|
317
|
-
if (isCron || repeat) {
|
|
318
|
-
if (!schedule) {
|
|
319
|
-
return { success: false, error: 'node-schedule not installed' }
|
|
320
|
-
}
|
|
321
|
-
task = {
|
|
322
|
-
id: taskId,
|
|
323
|
-
name: args.name || 'CronTask',
|
|
324
|
-
type: 'cron',
|
|
325
|
-
cronExpression: cronExpression || scheduleTime,
|
|
326
|
-
message,
|
|
327
|
-
enabled: true,
|
|
328
|
-
createdAt: new Date(),
|
|
329
|
-
lastRun: null,
|
|
330
|
-
runCount: 0,
|
|
331
|
-
timer: null,
|
|
332
|
-
cronTask: null,
|
|
333
|
-
sessionId: targetSessionId || null,
|
|
334
|
-
llm: llmMode,
|
|
335
|
-
notify: args.notify !== false
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
task.cronTask = schedule.scheduleJob(task.cronExpression, async () => {
|
|
339
|
-
await this._executeTask(task)
|
|
340
|
-
})
|
|
341
|
-
} else if (scheduleTime.includes(':')) {
|
|
342
|
-
const runAt = parseAtTime(scheduleTime)
|
|
343
|
-
task = {
|
|
344
|
-
id: taskId,
|
|
345
|
-
name: args.name || 'Reminder',
|
|
346
|
-
type: 'once',
|
|
347
|
-
runAt,
|
|
348
|
-
message,
|
|
349
|
-
enabled: true,
|
|
350
|
-
createdAt: new Date(),
|
|
351
|
-
lastRun: null,
|
|
352
|
-
runCount: 0,
|
|
353
|
-
timer: null,
|
|
354
|
-
sessionId: targetSessionId || null,
|
|
355
|
-
llm: llmMode,
|
|
356
|
-
notify: args.notify !== false
|
|
357
|
-
}
|
|
358
|
-
task.timer = setTimeout(async () => {
|
|
359
|
-
await this._executeTask(task)
|
|
360
|
-
}, runAt.getTime() - Date.now())
|
|
361
|
-
} else {
|
|
362
|
-
const delayMs = parseDelay(scheduleTime)
|
|
363
|
-
if (!delayMs) {
|
|
364
|
-
return { success: false, error: '无效的时间格式' }
|
|
365
|
-
}
|
|
366
|
-
const runAt = new Date(Date.now() + delayMs)
|
|
367
|
-
task = {
|
|
368
|
-
id: taskId,
|
|
369
|
-
name: args.name || 'Reminder',
|
|
370
|
-
type: 'once',
|
|
371
|
-
runAt,
|
|
372
|
-
message,
|
|
373
|
-
enabled: true,
|
|
374
|
-
createdAt: new Date(),
|
|
375
|
-
lastRun: null,
|
|
376
|
-
runCount: 0,
|
|
377
|
-
timer: null,
|
|
378
|
-
sessionId: targetSessionId || null,
|
|
379
|
-
llm: llmMode,
|
|
380
|
-
persistDelay: delayMs,
|
|
381
|
-
notify: args.notify !== false
|
|
382
|
-
}
|
|
383
|
-
task.timer = setTimeout(async () => {
|
|
384
|
-
await this._executeTask(task)
|
|
385
|
-
}, delayMs)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
this._tasks.set(task.id, task)
|
|
389
|
-
this._taskStats.total++
|
|
390
|
-
this._saveTasks()
|
|
391
|
-
|
|
392
|
-
this._framework.emit('scheduler:task_created', {
|
|
393
|
-
taskId: task.id,
|
|
394
|
-
taskName: task.name,
|
|
395
|
-
type: task.type,
|
|
396
|
-
scheduleTime,
|
|
397
|
-
cronExpression: task.cronExpression
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
success: true,
|
|
402
|
-
taskId: task.id,
|
|
403
|
-
name: task.name,
|
|
404
|
-
scheduleTime,
|
|
405
|
-
executeAt: task.runAt ? task.runAt.toISOString() : null,
|
|
406
|
-
cronExpression: task.cronExpression,
|
|
407
|
-
message: repeat ? '定时任务已创建 (重复执行)' : '提醒已设置',
|
|
408
|
-
sessionId: sessionId || 'default',
|
|
409
|
-
llm: llmMode,
|
|
410
|
-
notify: task.notify
|
|
411
|
-
}
|
|
412
|
-
} catch (err) {
|
|
413
|
-
return { success: false, error: err.message }
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
})
|
|
219
|
+
execute: async (args) => this._createTask(args),
|
|
220
|
+
});
|
|
417
221
|
|
|
418
222
|
framework.registerTool({
|
|
419
223
|
name: 'schedule_list',
|
|
420
224
|
description: '列出所有定时任务',
|
|
421
225
|
inputSchema: z.object({}),
|
|
422
226
|
execute: async () => {
|
|
423
|
-
const tasks = Array.from(this._tasks.values()).map(t =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
227
|
+
const tasks = Array.from(this._tasks.values()).map((t) => {
|
|
228
|
+
// croner 提供 nextRun() getter:cron 任务下一触发时间;once 任务固定 runAt
|
|
229
|
+
let nextRun = null;
|
|
230
|
+
try {
|
|
231
|
+
nextRun = t.scheduleHandle?.nextRun?.() || t.runAt || null;
|
|
232
|
+
} catch {
|
|
233
|
+
nextRun = t.runAt || null;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
id: t.id,
|
|
237
|
+
name: t.name,
|
|
238
|
+
type: t.type,
|
|
239
|
+
message: t.message,
|
|
240
|
+
shell: t.shell || null,
|
|
241
|
+
enabled: t.enabled,
|
|
242
|
+
nextRun,
|
|
243
|
+
runCount: t.runCount,
|
|
244
|
+
lastRun: t.lastRun,
|
|
245
|
+
cronExpression: t.cronExpression,
|
|
246
|
+
timezone: t.timezone,
|
|
247
|
+
llm: t.llm,
|
|
248
|
+
notify: t.notify,
|
|
249
|
+
sessionId: t.sessionId,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
return { success: true, tasks, total: tasks.length };
|
|
253
|
+
},
|
|
254
|
+
});
|
|
447
255
|
|
|
448
256
|
framework.registerTool({
|
|
449
257
|
name: 'schedule_cancel',
|
|
450
258
|
description: '取消定时任务',
|
|
451
|
-
inputSchema: z.object({
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
execute: async (args) => {
|
|
455
|
-
const task = this._tasks.get(args.taskId)
|
|
456
|
-
if (!task) {
|
|
457
|
-
return { success: false, error: 'Task not found' }
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const taskName = task.name
|
|
461
|
-
this._cancelTask(task)
|
|
462
|
-
this._saveTasks()
|
|
463
|
-
|
|
464
|
-
this._framework.emit('notification', {
|
|
465
|
-
title: '任务已取消',
|
|
466
|
-
message: `定时任务 "${taskName}" 已取消`,
|
|
467
|
-
source: 'scheduler',
|
|
468
|
-
level: 'info',
|
|
469
|
-
sessionId: task.sessionId,
|
|
470
|
-
timestamp: new Date()
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
return { success: true, cancelled: args.taskId }
|
|
474
|
-
}
|
|
475
|
-
})
|
|
259
|
+
inputSchema: z.object({ taskId: z.string().describe('任务 ID') }),
|
|
260
|
+
execute: async (args) => this._cancelTask(args.taskId),
|
|
261
|
+
});
|
|
476
262
|
|
|
477
263
|
framework.registerTool({
|
|
478
264
|
name: 'cron_examples',
|
|
479
265
|
description: '获取常用 Cron 表达式示例',
|
|
480
266
|
inputSchema: z.object({}),
|
|
481
|
-
execute: async () => {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
this._startScheduler()
|
|
501
|
-
|
|
502
|
-
this._loadPersistedTasks()
|
|
503
|
-
|
|
504
|
-
return this
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
_startScheduler() {
|
|
508
|
-
if (this._timer) {
|
|
509
|
-
clearInterval(this._timer)
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
this._timer = setInterval(() => {
|
|
513
|
-
this._checkTasks()
|
|
514
|
-
}, this.config.checkInterval)
|
|
515
|
-
}
|
|
267
|
+
execute: async () => ({
|
|
268
|
+
examples: [
|
|
269
|
+
{ expression: '* * * * *', description: '每分钟' },
|
|
270
|
+
{ expression: '*/5 * * * *', description: '每5分钟' },
|
|
271
|
+
{ expression: '*/15 * * * *', description: '每15分钟' },
|
|
272
|
+
{ expression: '0 * * * *', description: '每小时' },
|
|
273
|
+
{ expression: '0 9 * * *', description: '每天早上9点' },
|
|
274
|
+
{ expression: '0 12 * * *', description: '每天中午12点' },
|
|
275
|
+
{ expression: '0 18 * * *', description: '每天下午6点' },
|
|
276
|
+
{ expression: '0 9 * * 1-5', description: '工作日上午9点' },
|
|
277
|
+
{ expression: '0 9 * * 0,6', description: '周末上午9点' },
|
|
278
|
+
{ expression: '0 9 * * 1', description: '每周一上午9点' },
|
|
279
|
+
{ expression: '0 */2 * * *', description: '每2小时' },
|
|
280
|
+
],
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
516
283
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (!task.enabled) continue
|
|
520
|
-
}
|
|
284
|
+
this._loadPersistedTasks();
|
|
285
|
+
return this;
|
|
521
286
|
}
|
|
522
287
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
288
|
+
// ============================================================
|
|
289
|
+
// 任务创建(统一入口,shell 与普通合并)
|
|
290
|
+
// ============================================================
|
|
291
|
+
async _createTask(args) {
|
|
292
|
+
try {
|
|
293
|
+
const { scheduleTime, message, repeat, cronExpression, sessionId, shell } = args;
|
|
294
|
+
|
|
295
|
+
// 解析时间
|
|
296
|
+
const isCron = /^[\d*,\/-\s]+$/.test(scheduleTime) && scheduleTime.split(/\s+/).length >= 5;
|
|
297
|
+
let taskType; // 'cron' | 'once'
|
|
298
|
+
let runAt = null;
|
|
299
|
+
let cron = null;
|
|
300
|
+
|
|
301
|
+
if (isCron || repeat) {
|
|
302
|
+
taskType = 'cron';
|
|
303
|
+
cron = cronExpression || scheduleTime;
|
|
304
|
+
} else if (scheduleTime.includes(':')) {
|
|
305
|
+
taskType = 'once';
|
|
306
|
+
runAt = parseAtTime(scheduleTime);
|
|
307
|
+
if (isNaN(runAt.getTime())) {
|
|
308
|
+
return { success: false, error: `无法解析时间: ${scheduleTime}` };
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
const delayMs = parseDelay(scheduleTime);
|
|
312
|
+
if (!delayMs) {
|
|
313
|
+
return { success: false, error: '无效的时间格式' };
|
|
314
|
+
}
|
|
315
|
+
taskType = 'once';
|
|
316
|
+
runAt = new Date(Date.now() + delayMs);
|
|
538
317
|
}
|
|
539
318
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
task.cronTask = schedule.scheduleJob(task.cronExpression, async () => {
|
|
545
|
-
await this._executeTask(task)
|
|
546
|
-
})
|
|
547
|
-
this._tasks.set(task.id, task)
|
|
548
|
-
this._taskStats.total++
|
|
549
|
-
} catch (err) {
|
|
550
|
-
log.error(` 恢复 Cron 任务失败: ${task.name}`, err.message)
|
|
551
|
-
}
|
|
319
|
+
// shell 任务必须能用 shell 工具,否则提示
|
|
320
|
+
if (shell && !this._hasShellTool()) {
|
|
321
|
+
// 不阻塞:fallback 到 child_process 执行
|
|
552
322
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
this._taskStats.total++
|
|
560
|
-
} catch (err) {
|
|
561
|
-
log.error(` 恢复 Shell Cron 任务失败: ${task.name}`, err.message)
|
|
562
|
-
}
|
|
323
|
+
|
|
324
|
+
// 解析 sessionId
|
|
325
|
+
let targetSessionId = sessionId;
|
|
326
|
+
if (!targetSessionId) {
|
|
327
|
+
const ctx = this._framework?.getExecutionContext?.();
|
|
328
|
+
if (ctx?.sessionId) targetSessionId = ctx.sessionId;
|
|
563
329
|
}
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
330
|
+
if (!targetSessionId) {
|
|
331
|
+
const sessionPlugin = this._framework?.pluginManager?.get('session');
|
|
332
|
+
if (sessionPlugin) {
|
|
333
|
+
const sessions = sessionPlugin.listSessions();
|
|
334
|
+
if (sessions.length > 0) {
|
|
335
|
+
sessions.sort((a, b) => {
|
|
336
|
+
const aTime = a.lastActive ? new Date(a.lastActive).getTime() : 0;
|
|
337
|
+
const bTime = b.lastActive ? new Date(b.lastActive).getTime() : 0;
|
|
338
|
+
return bTime - aTime;
|
|
339
|
+
});
|
|
340
|
+
targetSessionId = sessions[0].id;
|
|
341
|
+
}
|
|
573
342
|
}
|
|
574
343
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
344
|
+
|
|
345
|
+
const llmMode = shell ? false : detectLLM(message, args.llm);
|
|
346
|
+
const task = {
|
|
347
|
+
id: `task_${randomUUID()}`,
|
|
348
|
+
name: args.name || (shell ? 'ShellTask' : taskType === 'cron' ? 'CronTask' : 'Reminder'),
|
|
349
|
+
type: taskType,
|
|
350
|
+
message,
|
|
351
|
+
shell: shell || null,
|
|
352
|
+
enabled: true,
|
|
353
|
+
createdAt: new Date(),
|
|
354
|
+
lastRun: null,
|
|
355
|
+
runCount: 0,
|
|
356
|
+
cronExpression: cron,
|
|
357
|
+
runAt,
|
|
358
|
+
timezone: this.config.timezone,
|
|
359
|
+
sessionId: targetSessionId || null,
|
|
360
|
+
llm: llmMode,
|
|
361
|
+
notify: args.notify !== false,
|
|
362
|
+
running: false,
|
|
363
|
+
scheduleHandle: null,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// 创建 croner 调度
|
|
367
|
+
try {
|
|
368
|
+
if (taskType === 'cron') {
|
|
369
|
+
task.scheduleHandle = new Cron(cron, this._cronOptions(task), () => this._onFire(task));
|
|
370
|
+
} else {
|
|
371
|
+
// once:startAt 限定未来触发
|
|
372
|
+
task.scheduleHandle = new Cron(runAt, this._cronOptions(task), () => this._onFire(task));
|
|
584
373
|
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
return { success: false, error: `调度创建失败: ${err.message}` };
|
|
585
376
|
}
|
|
377
|
+
|
|
378
|
+
this._tasks.set(task.id, task);
|
|
379
|
+
this._schedulePersist();
|
|
380
|
+
|
|
381
|
+
this._framework?.emit('scheduler:task_created', {
|
|
382
|
+
taskId: task.id,
|
|
383
|
+
taskName: task.name,
|
|
384
|
+
type: task.type,
|
|
385
|
+
scheduleTime,
|
|
386
|
+
cronExpression: task.cronExpression,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
taskId: task.id,
|
|
392
|
+
name: task.name,
|
|
393
|
+
scheduleTime,
|
|
394
|
+
executeAt: task.runAt ? task.runAt.toISOString() : null,
|
|
395
|
+
cronExpression: task.cronExpression,
|
|
396
|
+
message: task.type === 'cron' ? '定时任务已创建 (重复执行)' : '提醒已设置',
|
|
397
|
+
sessionId: targetSessionId || 'default',
|
|
398
|
+
llm: llmMode,
|
|
399
|
+
notify: task.notify,
|
|
400
|
+
};
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return { success: false, error: err.message };
|
|
586
403
|
}
|
|
587
404
|
}
|
|
588
405
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
406
|
+
_cronOptions(task) {
|
|
407
|
+
return {
|
|
408
|
+
timezone: this.config.timezone,
|
|
409
|
+
protect: true, // 防重叠:上一次还没跑完,跳过本次
|
|
410
|
+
catch: true, // catch-up:进程恢复后补触发错过的触发
|
|
411
|
+
name: `scheduler:${task.id}`,
|
|
412
|
+
unref: true, // 不阻止进程退出
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ============================================================
|
|
417
|
+
// 触发回调
|
|
418
|
+
// ============================================================
|
|
419
|
+
async _onFire(task) {
|
|
420
|
+
if (!task.enabled) return;
|
|
421
|
+
if (task.running) {
|
|
422
|
+
// 理论上 protect:true 已经处理过;这里再保险一次
|
|
423
|
+
log.warn(` 任务 ${task.name} 仍在执行中,跳过本次触发`);
|
|
424
|
+
return;
|
|
593
425
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
426
|
+
task.running = true;
|
|
427
|
+
task.lastRun = new Date();
|
|
428
|
+
task.runCount++;
|
|
429
|
+
try {
|
|
430
|
+
await this._executeTask(task);
|
|
431
|
+
} finally {
|
|
432
|
+
task.running = false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// once 任务触发一次后清理
|
|
436
|
+
if (task.type === 'once') {
|
|
437
|
+
this._cleanupTask(task.id);
|
|
438
|
+
} else {
|
|
439
|
+
// cron 任务只更新持久化(不写 runAt)
|
|
440
|
+
this._schedulePersist();
|
|
597
441
|
}
|
|
598
|
-
task.enabled = false
|
|
599
442
|
}
|
|
600
443
|
|
|
601
444
|
async _executeTask(task) {
|
|
602
|
-
task.lastRun = new Date()
|
|
603
|
-
task.runCount++
|
|
604
|
-
this._taskStats.running++
|
|
605
|
-
|
|
606
445
|
try {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const toolName = this._framework.toolRegistry.has('shell_exec') ? 'shell_exec' : 'shell';
|
|
611
|
-
result = await this._framework.executeTool(toolName, { command: task.shell });
|
|
612
|
-
// 适配 shell_exec 的返回格式({success, stdout, stderr})到统一格式
|
|
613
|
-
if (result.success === false && result.error) {
|
|
614
|
-
// shell_exec 已捕获错误
|
|
615
|
-
} else if (result.success === false && result.exitCode !== 0) {
|
|
616
|
-
result = { error: `退出码 ${result.exitCode}: ${result.stderr}`, stdout: result.stdout, stderr: result.stderr };
|
|
617
|
-
} else {
|
|
618
|
-
result = { output: result.stdout, stdout: result.stdout, stderr: result.stderr };
|
|
619
|
-
}
|
|
620
|
-
} else {
|
|
621
|
-
// 兜底:shell 工具不存在时直接用 child_process 执行
|
|
622
|
-
const { exec } = require('child_process');
|
|
623
|
-
const util = require('util');
|
|
624
|
-
const execAsync = util.promisify(exec);
|
|
625
|
-
try {
|
|
626
|
-
const { stdout, stderr } = await execAsync(task.shell, { timeout: 60000 });
|
|
627
|
-
result = { output: stdout.trim(), stdout: stdout.trim(), stderr: stderr.trim() };
|
|
628
|
-
} catch (e) {
|
|
629
|
-
result = { error: e.message, stderr: e.stderr || '' };
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
this._taskStats.completed++
|
|
633
|
-
|
|
634
|
-
const output = result.error ? `执行失败: ${result.error}` : (result.output || result.stdout || JSON.stringify(result))
|
|
635
|
-
if (task.notify !== false) {
|
|
636
|
-
this._framework.emit('notification', {
|
|
637
|
-
title: task.name,
|
|
638
|
-
message: output,
|
|
639
|
-
source: 'scheduler',
|
|
640
|
-
level: result.error ? 'error' : 'info',
|
|
641
|
-
sessionId: task.sessionId,
|
|
642
|
-
timestamp: new Date()
|
|
643
|
-
})
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
this._framework.emit('scheduler:task_completed', {
|
|
647
|
-
taskId: task.id,
|
|
648
|
-
result: output
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
if (task.type === 'once' && !task.cronTask) {
|
|
652
|
-
this._cleanupTask(task.id)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return { success: true }
|
|
656
|
-
} else if (task.llm) {
|
|
657
|
-
const schedulerAgent = this._framework.createSubAgent({
|
|
658
|
-
name: 'scheduler_task',
|
|
659
|
-
role: '定时任务执行助手,专注于处理定时提醒和任务执行',
|
|
660
|
-
hidden: true
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
const result = await schedulerAgent.chat(task.message)
|
|
664
|
-
|
|
665
|
-
this._taskStats.completed++
|
|
666
|
-
|
|
667
|
-
const responseText = result.message
|
|
668
|
-
|
|
669
|
-
if (task.notify !== false && responseText && responseText.trim()) {
|
|
670
|
-
this._framework.emit('notification', {
|
|
671
|
-
title: task.name,
|
|
672
|
-
message: responseText,
|
|
673
|
-
source: 'scheduler',
|
|
674
|
-
level: 'info',
|
|
675
|
-
sessionId: task.sessionId,
|
|
676
|
-
timestamp: new Date()
|
|
677
|
-
})
|
|
678
|
-
this._framework.emit('scheduler:reminder', {
|
|
679
|
-
taskId: task.id,
|
|
680
|
-
message: task.message,
|
|
681
|
-
time: task.runAt || task.nextRun
|
|
682
|
-
})
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
this._framework.emit('scheduler:task_completed', {
|
|
686
|
-
taskId: task.id,
|
|
687
|
-
result: responseText || task.message
|
|
688
|
-
})
|
|
689
|
-
} else {
|
|
690
|
-
if (task.notify !== false) {
|
|
691
|
-
this._framework.emit('notification', {
|
|
692
|
-
title: task.name,
|
|
693
|
-
message: task.message,
|
|
694
|
-
source: 'scheduler',
|
|
695
|
-
level: 'info',
|
|
696
|
-
sessionId: task.sessionId,
|
|
697
|
-
timestamp: new Date()
|
|
698
|
-
})
|
|
699
|
-
this._framework.emit('scheduler:reminder', {
|
|
700
|
-
taskId: task.id,
|
|
701
|
-
message: task.message,
|
|
702
|
-
time: task.runAt || task.nextRun
|
|
703
|
-
})
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
this._framework.emit('scheduler:task_completed', {
|
|
707
|
-
taskId: task.id,
|
|
708
|
-
result: task.message
|
|
709
|
-
})
|
|
710
|
-
this._taskStats.completed++
|
|
446
|
+
// shell 执行
|
|
447
|
+
if (task.shell) {
|
|
448
|
+
return await this._runShell(task);
|
|
711
449
|
}
|
|
712
|
-
|
|
713
|
-
if (task.
|
|
714
|
-
this.
|
|
450
|
+
// LLM 执行
|
|
451
|
+
if (task.llm) {
|
|
452
|
+
return await this._runLLM(task);
|
|
715
453
|
}
|
|
716
|
-
|
|
717
|
-
return
|
|
454
|
+
// 普通提醒(直接发通知)
|
|
455
|
+
return await this._runNotification(task);
|
|
718
456
|
} catch (err) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
this._framework.emit('notification', {
|
|
457
|
+
log.error(` Task ${task.name} failed:`, err.message);
|
|
458
|
+
this._framework?.emit('notification', {
|
|
723
459
|
title: `任务失败: ${task.name}`,
|
|
724
460
|
message: err.message,
|
|
725
461
|
source: 'scheduler',
|
|
726
462
|
level: 'error',
|
|
727
463
|
sessionId: task.sessionId,
|
|
728
|
-
timestamp: new Date()
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
this._framework.emit('scheduler:task_failed', {
|
|
464
|
+
timestamp: new Date(),
|
|
465
|
+
});
|
|
466
|
+
this._framework?.emit('scheduler:task_failed', {
|
|
732
467
|
taskId: task.id,
|
|
733
|
-
error: err.message
|
|
734
|
-
})
|
|
468
|
+
error: err.message,
|
|
469
|
+
});
|
|
470
|
+
return { error: err.message };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
735
473
|
|
|
736
|
-
|
|
737
|
-
|
|
474
|
+
async _runShell(task) {
|
|
475
|
+
let result;
|
|
476
|
+
const shellTool = this._hasShellTool() ? this._pickShellTool() : null;
|
|
477
|
+
if (shellTool) {
|
|
478
|
+
const raw = await this._framework.executeTool(shellTool, { command: task.shell });
|
|
479
|
+
// 适配不同 shell 工具的返回格式
|
|
480
|
+
if (raw?.error) {
|
|
481
|
+
result = { error: raw.error, stdout: raw.stdout, stderr: raw.stderr };
|
|
482
|
+
} else if (raw?.success === false) {
|
|
483
|
+
const msg = raw.exitCode !== undefined ? `退出码 ${raw.exitCode}: ${raw.stderr || ''}` : (raw.error || 'shell 执行失败');
|
|
484
|
+
result = { error: msg, stdout: raw.stdout, stderr: raw.stderr };
|
|
485
|
+
} else {
|
|
486
|
+
result = { output: raw.stdout || raw.output || '', stdout: raw.stdout, stderr: raw.stderr };
|
|
738
487
|
}
|
|
488
|
+
} else {
|
|
489
|
+
// 兜底:直接 child_process
|
|
490
|
+
const { exec } = require('child_process');
|
|
491
|
+
const util = require('util');
|
|
492
|
+
const execAsync = util.promisify(exec);
|
|
493
|
+
try {
|
|
494
|
+
const { stdout, stderr } = await execAsync(task.shell, { timeout: 60000 });
|
|
495
|
+
result = { output: stdout.trim(), stdout: stdout.trim(), stderr: (stderr || '').trim() };
|
|
496
|
+
} catch (e) {
|
|
497
|
+
result = { error: e.message, stderr: e.stderr || '' };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
739
500
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
501
|
+
const output = result.error
|
|
502
|
+
? `执行失败: ${result.error}`
|
|
503
|
+
: (result.output || result.stdout || JSON.stringify(result));
|
|
504
|
+
|
|
505
|
+
if (task.notify !== false) {
|
|
506
|
+
this._framework?.emit('notification', {
|
|
507
|
+
title: task.name,
|
|
508
|
+
message: output,
|
|
509
|
+
source: 'scheduler',
|
|
510
|
+
level: result.error ? 'error' : 'info',
|
|
511
|
+
sessionId: task.sessionId,
|
|
512
|
+
timestamp: new Date(),
|
|
513
|
+
});
|
|
743
514
|
}
|
|
515
|
+
this._framework?.emit('scheduler:task_completed', { taskId: task.id, result: output });
|
|
516
|
+
return { success: true };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async _runLLM(task) {
|
|
520
|
+
const subAgent = this._getSchedulerSubAgent();
|
|
521
|
+
if (!subAgent) {
|
|
522
|
+
return { error: '无法创建 scheduler subagent' };
|
|
523
|
+
}
|
|
524
|
+
const result = await subAgent.chat(task.message);
|
|
525
|
+
const responseText = result?.message;
|
|
526
|
+
if (task.notify !== false && responseText && responseText.trim()) {
|
|
527
|
+
this._framework?.emit('notification', {
|
|
528
|
+
title: task.name,
|
|
529
|
+
message: responseText,
|
|
530
|
+
source: 'scheduler',
|
|
531
|
+
level: 'info',
|
|
532
|
+
sessionId: task.sessionId,
|
|
533
|
+
timestamp: new Date(),
|
|
534
|
+
});
|
|
535
|
+
this._framework?.emit('scheduler:reminder', {
|
|
536
|
+
taskId: task.id,
|
|
537
|
+
message: task.message,
|
|
538
|
+
time: task.runAt || null,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
this._framework?.emit('scheduler:task_completed', {
|
|
542
|
+
taskId: task.id,
|
|
543
|
+
result: responseText || task.message,
|
|
544
|
+
});
|
|
545
|
+
return { success: true };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async _runNotification(task) {
|
|
549
|
+
if (task.notify !== false) {
|
|
550
|
+
this._framework?.emit('notification', {
|
|
551
|
+
title: task.name,
|
|
552
|
+
message: task.message,
|
|
553
|
+
source: 'scheduler',
|
|
554
|
+
level: 'info',
|
|
555
|
+
sessionId: task.sessionId,
|
|
556
|
+
timestamp: new Date(),
|
|
557
|
+
});
|
|
558
|
+
this._framework?.emit('scheduler:reminder', {
|
|
559
|
+
taskId: task.id,
|
|
560
|
+
message: task.message,
|
|
561
|
+
time: task.runAt || null,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
this._framework?.emit('scheduler:task_completed', {
|
|
565
|
+
taskId: task.id,
|
|
566
|
+
result: task.message,
|
|
567
|
+
});
|
|
568
|
+
return { success: true };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ============================================================
|
|
572
|
+
// 取消 & 清理
|
|
573
|
+
// ============================================================
|
|
574
|
+
_cancelTask(taskId) {
|
|
575
|
+
const task = this._tasks.get(taskId);
|
|
576
|
+
if (!task) return { success: false, error: 'Task not found' };
|
|
577
|
+
|
|
578
|
+
const taskName = task.name;
|
|
579
|
+
try {
|
|
580
|
+
task.scheduleHandle?.stop();
|
|
581
|
+
} catch {}
|
|
582
|
+
task.enabled = false;
|
|
583
|
+
this._tasks.delete(taskId);
|
|
584
|
+
this._schedulePersist();
|
|
585
|
+
|
|
586
|
+
this._framework?.emit('notification', {
|
|
587
|
+
title: '任务已取消',
|
|
588
|
+
message: `定时任务 "${taskName}" 已取消`,
|
|
589
|
+
source: 'scheduler',
|
|
590
|
+
level: 'info',
|
|
591
|
+
sessionId: task.sessionId,
|
|
592
|
+
timestamp: new Date(),
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return { success: true, cancelled: taskId };
|
|
744
596
|
}
|
|
745
597
|
|
|
746
598
|
_cleanupTask(taskId) {
|
|
747
|
-
const task = this._tasks.get(taskId)
|
|
748
|
-
if (task)
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
599
|
+
const task = this._tasks.get(taskId);
|
|
600
|
+
if (!task) return;
|
|
601
|
+
try {
|
|
602
|
+
task.scheduleHandle?.stop();
|
|
603
|
+
} catch {}
|
|
604
|
+
this._tasks.delete(taskId);
|
|
605
|
+
this._schedulePersist();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ============================================================
|
|
609
|
+
// 持久化(debounce 500ms + 原子写)
|
|
610
|
+
// ============================================================
|
|
611
|
+
_schedulePersist() {
|
|
612
|
+
this._persistDirty = true;
|
|
613
|
+
if (this._persistTimer) return;
|
|
614
|
+
this._persistTimer = setTimeout(() => {
|
|
615
|
+
this._persistTimer = null;
|
|
616
|
+
if (!this._persistDirty) return;
|
|
617
|
+
this._persistDirty = false;
|
|
618
|
+
this._taskStore.saveTasks(Array.from(this._tasks.values()));
|
|
619
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
620
|
+
this._persistTimer.unref?.();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
_flushPersist() {
|
|
624
|
+
if (this._persistTimer) {
|
|
625
|
+
clearTimeout(this._persistTimer);
|
|
626
|
+
this._persistTimer = null;
|
|
627
|
+
}
|
|
628
|
+
if (this._persistDirty) {
|
|
629
|
+
this._persistDirty = false;
|
|
630
|
+
this._taskStore.saveTasks(Array.from(this._tasks.values()));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ============================================================
|
|
635
|
+
// 启动时从持久化恢复(含过期补触发)
|
|
636
|
+
// ============================================================
|
|
637
|
+
_loadPersistedTasks() {
|
|
638
|
+
const savedTasks = this._taskStore.loadTasks();
|
|
639
|
+
if (!savedTasks || savedTasks.length === 0) return;
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const expiredImmediate = []; // 需要立即补触发的过期任务
|
|
642
|
+
|
|
643
|
+
for (const saved of savedTasks) {
|
|
644
|
+
// 已完成的 once 任务不再恢复
|
|
645
|
+
if (saved.type === 'once' && (saved.runCount || 0) > 0) continue;
|
|
646
|
+
|
|
647
|
+
const task = {
|
|
648
|
+
...saved,
|
|
649
|
+
createdAt: saved.createdAt ? new Date(saved.createdAt) : new Date(),
|
|
650
|
+
lastRun: saved.lastRun ? new Date(saved.lastRun) : null,
|
|
651
|
+
runAt: saved.runAt ? new Date(saved.runAt) : null,
|
|
652
|
+
running: false,
|
|
653
|
+
scheduleHandle: null,
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
if (task.type === 'cron' && task.enabled && task.cronExpression) {
|
|
658
|
+
task.scheduleHandle = new Cron(task.cronExpression, this._cronOptions(task), () => this._onFire(task));
|
|
659
|
+
this._tasks.set(task.id, task);
|
|
660
|
+
} else if (task.type === 'once' && task.enabled) {
|
|
661
|
+
if (!task.runAt) continue;
|
|
662
|
+
const fireAt = task.runAt.getTime();
|
|
663
|
+
if (fireAt <= now) {
|
|
664
|
+
// 已过期:超过 grace period 就丢弃,否则排队立即补触发
|
|
665
|
+
if (now - fireAt > ONCE_EXPIRY_GRACE_MS) {
|
|
666
|
+
log.warn(` 丢弃过期任务 ${task.name}(已过期 ${Math.round((now - fireAt) / 1000)}s,超出 24h 容忍期)`);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
// 安排一个微小延迟的补触发(避免与启动逻辑抢资源)
|
|
670
|
+
setTimeout(() => this._onFire(task), 100).unref?.();
|
|
671
|
+
// 不放入 _tasks 的 scheduleHandle 路径(因为是一次性补触发),
|
|
672
|
+
// 但保留在 _tasks 里供 schedule_list 查看
|
|
673
|
+
this._tasks.set(task.id, { ...task, expiredCatchup: true });
|
|
674
|
+
} else {
|
|
675
|
+
// 未来触发
|
|
676
|
+
task.scheduleHandle = new Cron(task.runAt, this._cronOptions(task), () => this._onFire(task));
|
|
677
|
+
this._tasks.set(task.id, task);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
log.error(` 恢复任务失败: ${task.name}`, err.message);
|
|
756
682
|
}
|
|
757
|
-
this._tasks.delete(taskId)
|
|
758
|
-
this._saveTasks()
|
|
759
683
|
}
|
|
684
|
+
log.info(` 已恢复 ${this._tasks.size} 个定时任务`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ============================================================
|
|
688
|
+
// 辅助
|
|
689
|
+
// ============================================================
|
|
690
|
+
_hasShellTool() {
|
|
691
|
+
if (!this._framework?.toolRegistry) return false;
|
|
692
|
+
return this._framework.toolRegistry.has?.('shell_exec') || this._framework.toolRegistry.has?.('shell');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_pickShellTool() {
|
|
696
|
+
const reg = this._framework.toolRegistry;
|
|
697
|
+
if (reg.has?.('shell_exec')) return 'shell_exec';
|
|
698
|
+
if (reg.has?.('shell')) return 'shell';
|
|
699
|
+
return null;
|
|
760
700
|
}
|
|
761
701
|
|
|
762
702
|
stopAll() {
|
|
763
703
|
for (const task of this._tasks.values()) {
|
|
764
|
-
|
|
704
|
+
try { task.scheduleHandle?.stop(); } catch {}
|
|
765
705
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
clearInterval(this._timer)
|
|
769
|
-
this._timer = null
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
this._saveTasks()
|
|
706
|
+
this._tasks.clear();
|
|
707
|
+
this._flushPersist();
|
|
773
708
|
}
|
|
774
709
|
|
|
775
710
|
reload(framework) {
|
|
776
|
-
this.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
this.
|
|
711
|
+
for (const task of this._tasks.values()) {
|
|
712
|
+
try { task.scheduleHandle?.stop(); } catch {}
|
|
713
|
+
}
|
|
714
|
+
this._tasks.clear();
|
|
715
|
+
this._schedulerSubAgent = null;
|
|
716
|
+
this._framework = framework;
|
|
717
|
+
this._agent = this._getAgent();
|
|
718
|
+
this._loadPersistedTasks();
|
|
780
719
|
}
|
|
781
720
|
|
|
782
721
|
uninstall(framework) {
|
|
783
|
-
this.stopAll()
|
|
784
|
-
this.
|
|
785
|
-
this.
|
|
786
|
-
this.
|
|
787
|
-
this._agent = null
|
|
722
|
+
this.stopAll();
|
|
723
|
+
this._schedulerSubAgent = null;
|
|
724
|
+
this._framework = null;
|
|
725
|
+
this._agent = null;
|
|
788
726
|
}
|
|
789
727
|
}
|
|
790
728
|
|
|
791
|
-
module.exports = SchedulerPlugin
|
|
729
|
+
module.exports = SchedulerPlugin;
|