figranium 0.12.1 → 0.12.2
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/LICENSE +674 -674
- package/README.md +336 -336
- package/agent.js +1 -1
- package/bin/cli.js +149 -149
- package/common-utils.js +211 -211
- package/dist/assets/{favicon-DmUMR1rm.svg → favicon-DXDXzv5K.svg} +290 -290
- package/dist/assets/index-BaVlGc48.js +18 -0
- package/dist/assets/index-T2xxnq_A.css +1 -0
- package/dist/favicon.svg +290 -290
- package/dist/figranium_icon.svg +290 -290
- package/dist/figranium_logo.svg +60 -60
- package/dist/index.html +26 -26
- package/dist/novnc.html +108 -108
- package/dist/styles.css +86 -86
- package/extraction-worker.js +211 -207
- package/headful.js +584 -569
- package/html-utils.js +24 -24
- package/package.json +82 -82
- package/proxy-rotation.js +261 -261
- package/proxy-utils.js +84 -84
- package/public/favicon.svg +290 -290
- package/public/figranium_icon.svg +290 -290
- package/public/figranium_logo.svg +60 -60
- package/public/novnc.html +108 -108
- package/public/styles.css +86 -86
- package/scrape.js +389 -389
- package/scripts/postinstall.js +21 -21
- package/server.js +626 -626
- package/src/server/cron-parser.js +325 -316
- package/src/server/routes/schedules.js +171 -171
- package/src/server/scheduler.js +379 -379
- package/url-utils.js +339 -323
- package/user-agent-settings.js +76 -76
- package/dist/assets/index-C2rVEs3q.css +0 -1
- package/dist/assets/index-CvaIUcTv.js +0 -18
package/src/server/scheduler.js
CHANGED
|
@@ -1,379 +1,379 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-process task scheduler.
|
|
3
|
-
* Loads tasks with schedule.enabled = true, computes next runs,
|
|
4
|
-
* and executes them at the correct time using setTimeout.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { loadTasks, saveTasks, getTaskById } = require('./storage');
|
|
8
|
-
const { appendExecution } = require('./storage');
|
|
9
|
-
const { getNextRun, scheduleToCron, isValidCron } = require('./cron-parser');
|
|
10
|
-
const { sendExecutionUpdate } = require('./state');
|
|
11
|
-
|
|
12
|
-
// Internal state
|
|
13
|
-
let schedulerTimer = null;
|
|
14
|
-
let scheduledTasks = new Map(); // taskId -> { cron, nextRun: Date }
|
|
15
|
-
let running = false;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Resolve the effective cron expression for a task schedule.
|
|
19
|
-
* Supports both visual (no-code) config and advanced raw cron.
|
|
20
|
-
*/
|
|
21
|
-
function resolveCron(schedule) {
|
|
22
|
-
if (!schedule) return null;
|
|
23
|
-
// If user supplied a raw cron expression (advanced mode)
|
|
24
|
-
if (schedule.cron && isValidCron(schedule.cron)) {
|
|
25
|
-
return schedule.cron;
|
|
26
|
-
}
|
|
27
|
-
// Otherwise build from visual config
|
|
28
|
-
if (schedule.frequency) {
|
|
29
|
-
try {
|
|
30
|
-
return scheduleToCron(schedule);
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Load all tasks with active schedules and compute next runs.
|
|
40
|
-
*/
|
|
41
|
-
async function loadSchedules() {
|
|
42
|
-
const tasks = await loadTasks();
|
|
43
|
-
scheduledTasks.clear();
|
|
44
|
-
|
|
45
|
-
for (const task of tasks) {
|
|
46
|
-
if (!task.schedule || !task.schedule.enabled) continue;
|
|
47
|
-
const cron = resolveCron(task.schedule);
|
|
48
|
-
if (!cron) continue;
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const nextRun = getNextRun(cron);
|
|
52
|
-
scheduledTasks.set(task.id, { cron, nextRun });
|
|
53
|
-
} catch (err) {
|
|
54
|
-
console.error(`[SCHEDULER] Failed to compute next run for task "${task.name}" (${task.id}):`, err.message);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Find the soonest task and schedule a timer for it.
|
|
61
|
-
*/
|
|
62
|
-
function scheduleNext() {
|
|
63
|
-
if (schedulerTimer) {
|
|
64
|
-
clearTimeout(schedulerTimer);
|
|
65
|
-
schedulerTimer = null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (scheduledTasks.size === 0 || !running) return;
|
|
69
|
-
|
|
70
|
-
let soonestId = null;
|
|
71
|
-
let soonestTime = Infinity;
|
|
72
|
-
|
|
73
|
-
for (const [taskId, info] of scheduledTasks) {
|
|
74
|
-
const t = info.nextRun.getTime();
|
|
75
|
-
if (t < soonestTime) {
|
|
76
|
-
soonestTime = t;
|
|
77
|
-
soonestId = taskId;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!soonestId) return;
|
|
82
|
-
|
|
83
|
-
const delay = Math.max(0, soonestTime - Date.now());
|
|
84
|
-
// Cap delay to 2^31-1 ms (~24.8 days) to avoid setTimeout overflow
|
|
85
|
-
const safeDelay = Math.min(delay, 2147483647);
|
|
86
|
-
|
|
87
|
-
schedulerTimer = setTimeout(() => {
|
|
88
|
-
if (!running) return;
|
|
89
|
-
// If we had to cap the delay, just re-schedule
|
|
90
|
-
if (safeDelay < delay) {
|
|
91
|
-
scheduleNext();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
tick(soonestId);
|
|
95
|
-
}, safeDelay);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Execute a scheduled task and re-compute its next run.
|
|
100
|
-
*/
|
|
101
|
-
async function tick(taskId) {
|
|
102
|
-
if (!running) return;
|
|
103
|
-
|
|
104
|
-
const info = scheduledTasks.get(taskId);
|
|
105
|
-
if (!info) {
|
|
106
|
-
scheduleNext();
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.log(`[SCHEDULER] Executing task "${taskId}" (cron: ${info.cron})`);
|
|
111
|
-
|
|
112
|
-
const startTime = Date.now();
|
|
113
|
-
let status = 'success';
|
|
114
|
-
let result = null;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
result = await executeScheduledTask(taskId);
|
|
118
|
-
} catch (err) {
|
|
119
|
-
status = 'error';
|
|
120
|
-
console.error(`[SCHEDULER] Task "${taskId}" failed:`, err.message);
|
|
121
|
-
result = { error: err.message };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const durationMs = Date.now() - startTime;
|
|
125
|
-
|
|
126
|
-
// Update the task's schedule metadata
|
|
127
|
-
try {
|
|
128
|
-
const tasks = await loadTasks();
|
|
129
|
-
const task = getTaskById(taskId);
|
|
130
|
-
if (task && task.schedule) {
|
|
131
|
-
task.schedule.lastRun = startTime;
|
|
132
|
-
task.schedule.lastRunStatus = status;
|
|
133
|
-
task.schedule.lastRunDurationMs = durationMs;
|
|
134
|
-
|
|
135
|
-
// Recompute next run
|
|
136
|
-
try {
|
|
137
|
-
const cron = resolveCron(task.schedule);
|
|
138
|
-
if (cron) {
|
|
139
|
-
const nextRun = getNextRun(cron);
|
|
140
|
-
task.schedule.nextRun = nextRun.getTime();
|
|
141
|
-
scheduledTasks.set(taskId, { cron, nextRun });
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
scheduledTasks.delete(taskId);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
await saveTasks(tasks);
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.error(`[SCHEDULER] Failed to update task "${taskId}" after execution:`, err.message);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Log execution
|
|
154
|
-
try {
|
|
155
|
-
const entry = {
|
|
156
|
-
id: 'sched_' + startTime + '_' + Math.floor(Math.random() * 1000),
|
|
157
|
-
timestamp: startTime,
|
|
158
|
-
method: 'POST',
|
|
159
|
-
path: `/api/tasks/${taskId}/api`,
|
|
160
|
-
status: status === 'success' ? 200 : 500,
|
|
161
|
-
durationMs,
|
|
162
|
-
source: 'scheduler',
|
|
163
|
-
mode: 'unknown',
|
|
164
|
-
taskId,
|
|
165
|
-
taskName: null,
|
|
166
|
-
url: null,
|
|
167
|
-
result
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// Try to get task name
|
|
171
|
-
try {
|
|
172
|
-
const task = getTaskById(taskId);
|
|
173
|
-
if (task) {
|
|
174
|
-
entry.taskName = task.name;
|
|
175
|
-
entry.mode = task.mode || 'agent';
|
|
176
|
-
entry.url = task.url || null;
|
|
177
|
-
}
|
|
178
|
-
} catch { }
|
|
179
|
-
|
|
180
|
-
await appendExecution(entry);
|
|
181
|
-
} catch (err) {
|
|
182
|
-
console.error(`[SCHEDULER] Failed to log execution for task "${taskId}":`, err.message);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
scheduleNext();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Execute a task using the same logic as the API endpoint.
|
|
190
|
-
* Creates mock req/res to reuse existing handlers.
|
|
191
|
-
*/
|
|
192
|
-
async function executeScheduledTask(taskId) {
|
|
193
|
-
await loadTasks();
|
|
194
|
-
const task = getTaskById(taskId);
|
|
195
|
-
if (!task) throw new Error('Task not found: ' + taskId);
|
|
196
|
-
|
|
197
|
-
// Lazy-require to avoid circular deps
|
|
198
|
-
const { handleAgent } = require('../../agent');
|
|
199
|
-
const { handleScrape } = require('../../scrape');
|
|
200
|
-
|
|
201
|
-
// Build runtime variables
|
|
202
|
-
const runtimeVars = {};
|
|
203
|
-
if (task.variables) {
|
|
204
|
-
for (const [key, v] of Object.entries(task.variables)) {
|
|
205
|
-
runtimeVars[key] = v.value;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Construct mock request/response
|
|
210
|
-
const body = {
|
|
211
|
-
...task,
|
|
212
|
-
taskId: task.id,
|
|
213
|
-
variables: runtimeVars,
|
|
214
|
-
taskVariables: runtimeVars,
|
|
215
|
-
actions: task.actions || [],
|
|
216
|
-
mode: task.mode || 'agent',
|
|
217
|
-
runSource: 'scheduler'
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const mockReq = {
|
|
221
|
-
method: 'POST',
|
|
222
|
-
body,
|
|
223
|
-
query: {},
|
|
224
|
-
params: { id: taskId },
|
|
225
|
-
protocol: 'http',
|
|
226
|
-
socket: { remoteAddress: '127.0.0.1' },
|
|
227
|
-
path: `/api/tasks/${taskId}/api`,
|
|
228
|
-
on: () => { },
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
return new Promise((resolve, reject) => {
|
|
232
|
-
let statusCode = 200;
|
|
233
|
-
const mockRes = {
|
|
234
|
-
status: (code) => { statusCode = code; return mockRes; },
|
|
235
|
-
json: (data) => {
|
|
236
|
-
if (statusCode >= 400) {
|
|
237
|
-
reject(new Error(data?.error || `HTTP ${statusCode}`));
|
|
238
|
-
} else {
|
|
239
|
-
resolve(data);
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
locals: {},
|
|
243
|
-
on: () => { },
|
|
244
|
-
setHeader: () => { },
|
|
245
|
-
write: () => { },
|
|
246
|
-
end: () => resolve(null),
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const runId = 'sched_' + Date.now();
|
|
250
|
-
mockReq.body.runId = runId;
|
|
251
|
-
|
|
252
|
-
const handler = task.mode === 'scrape' ? handleScrape : handleAgent;
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
sendExecutionUpdate(runId, { status: 'started' });
|
|
256
|
-
} catch { }
|
|
257
|
-
|
|
258
|
-
Promise.resolve(handler(mockReq, mockRes)).catch(reject);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Start the scheduler. Call this after the server starts.
|
|
264
|
-
*/
|
|
265
|
-
async function startScheduler() {
|
|
266
|
-
if (running) return;
|
|
267
|
-
running = true;
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
await loadSchedules();
|
|
271
|
-
const count = scheduledTasks.size;
|
|
272
|
-
console.log(`[SCHEDULER] Loaded ${count} scheduled task(s).`);
|
|
273
|
-
|
|
274
|
-
// Update nextRun on all scheduled tasks so frontend can display them
|
|
275
|
-
if (count > 0) {
|
|
276
|
-
const tasks = await loadTasks();
|
|
277
|
-
let dirty = false;
|
|
278
|
-
for (const [taskId, info] of scheduledTasks) {
|
|
279
|
-
const task = getTaskById(taskId);
|
|
280
|
-
if (task && task.schedule) {
|
|
281
|
-
const nextRunMs = info.nextRun.getTime();
|
|
282
|
-
if (task.schedule.nextRun !== nextRunMs) {
|
|
283
|
-
task.schedule.nextRun = nextRunMs;
|
|
284
|
-
dirty = true;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (dirty) await saveTasks(tasks);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
scheduleNext();
|
|
292
|
-
} catch (err) {
|
|
293
|
-
console.error('[SCHEDULER] Failed to start:', err.message);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Stop the scheduler.
|
|
299
|
-
*/
|
|
300
|
-
function stopScheduler() {
|
|
301
|
-
running = false;
|
|
302
|
-
if (schedulerTimer) {
|
|
303
|
-
clearTimeout(schedulerTimer);
|
|
304
|
-
schedulerTimer = null;
|
|
305
|
-
}
|
|
306
|
-
scheduledTasks.clear();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Refresh the schedule for a specific task (call after task update).
|
|
311
|
-
*/
|
|
312
|
-
async function refreshSchedule(taskId) {
|
|
313
|
-
const tasks = await loadTasks();
|
|
314
|
-
const task = getTaskById(taskId);
|
|
315
|
-
|
|
316
|
-
if (!task || !task.schedule || !task.schedule.enabled) {
|
|
317
|
-
scheduledTasks.delete(taskId);
|
|
318
|
-
scheduleNext();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const cron = resolveCron(task.schedule);
|
|
323
|
-
if (!cron) {
|
|
324
|
-
scheduledTasks.delete(taskId);
|
|
325
|
-
scheduleNext();
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
const nextRun = getNextRun(cron);
|
|
331
|
-
scheduledTasks.set(taskId, { cron, nextRun });
|
|
332
|
-
|
|
333
|
-
// Persist nextRun
|
|
334
|
-
task.schedule.nextRun = nextRun.getTime();
|
|
335
|
-
await saveTasks(tasks);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
console.error(`[SCHEDULER] Failed to refresh schedule for "${taskId}":`, err.message);
|
|
338
|
-
scheduledTasks.delete(taskId);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
scheduleNext();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Remove the schedule for a specific task.
|
|
346
|
-
*/
|
|
347
|
-
function removeSchedule(taskId) {
|
|
348
|
-
scheduledTasks.delete(taskId);
|
|
349
|
-
scheduleNext();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Get current scheduler status.
|
|
354
|
-
*/
|
|
355
|
-
function getSchedulerStatus() {
|
|
356
|
-
const entries = [];
|
|
357
|
-
for (const [taskId, info] of scheduledTasks) {
|
|
358
|
-
entries.push({
|
|
359
|
-
taskId,
|
|
360
|
-
cron: info.cron,
|
|
361
|
-
nextRun: info.nextRun.toISOString(),
|
|
362
|
-
nextRunMs: info.nextRun.getTime()
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
return {
|
|
366
|
-
running,
|
|
367
|
-
scheduledCount: scheduledTasks.size,
|
|
368
|
-
tasks: entries
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
module.exports = {
|
|
373
|
-
startScheduler,
|
|
374
|
-
stopScheduler,
|
|
375
|
-
refreshSchedule,
|
|
376
|
-
removeSchedule,
|
|
377
|
-
getSchedulerStatus,
|
|
378
|
-
resolveCron,
|
|
379
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* In-process task scheduler.
|
|
3
|
+
* Loads tasks with schedule.enabled = true, computes next runs,
|
|
4
|
+
* and executes them at the correct time using setTimeout.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { loadTasks, saveTasks, getTaskById } = require('./storage');
|
|
8
|
+
const { appendExecution } = require('./storage');
|
|
9
|
+
const { getNextRun, scheduleToCron, isValidCron } = require('./cron-parser');
|
|
10
|
+
const { sendExecutionUpdate } = require('./state');
|
|
11
|
+
|
|
12
|
+
// Internal state
|
|
13
|
+
let schedulerTimer = null;
|
|
14
|
+
let scheduledTasks = new Map(); // taskId -> { cron, nextRun: Date }
|
|
15
|
+
let running = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the effective cron expression for a task schedule.
|
|
19
|
+
* Supports both visual (no-code) config and advanced raw cron.
|
|
20
|
+
*/
|
|
21
|
+
function resolveCron(schedule) {
|
|
22
|
+
if (!schedule) return null;
|
|
23
|
+
// If user supplied a raw cron expression (advanced mode)
|
|
24
|
+
if (schedule.cron && isValidCron(schedule.cron)) {
|
|
25
|
+
return schedule.cron;
|
|
26
|
+
}
|
|
27
|
+
// Otherwise build from visual config
|
|
28
|
+
if (schedule.frequency) {
|
|
29
|
+
try {
|
|
30
|
+
return scheduleToCron(schedule);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load all tasks with active schedules and compute next runs.
|
|
40
|
+
*/
|
|
41
|
+
async function loadSchedules() {
|
|
42
|
+
const tasks = await loadTasks();
|
|
43
|
+
scheduledTasks.clear();
|
|
44
|
+
|
|
45
|
+
for (const task of tasks) {
|
|
46
|
+
if (!task.schedule || !task.schedule.enabled) continue;
|
|
47
|
+
const cron = resolveCron(task.schedule);
|
|
48
|
+
if (!cron) continue;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const nextRun = getNextRun(cron);
|
|
52
|
+
scheduledTasks.set(task.id, { cron, nextRun });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(`[SCHEDULER] Failed to compute next run for task "${task.name}" (${task.id}):`, err.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find the soonest task and schedule a timer for it.
|
|
61
|
+
*/
|
|
62
|
+
function scheduleNext() {
|
|
63
|
+
if (schedulerTimer) {
|
|
64
|
+
clearTimeout(schedulerTimer);
|
|
65
|
+
schedulerTimer = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (scheduledTasks.size === 0 || !running) return;
|
|
69
|
+
|
|
70
|
+
let soonestId = null;
|
|
71
|
+
let soonestTime = Infinity;
|
|
72
|
+
|
|
73
|
+
for (const [taskId, info] of scheduledTasks) {
|
|
74
|
+
const t = info.nextRun.getTime();
|
|
75
|
+
if (t < soonestTime) {
|
|
76
|
+
soonestTime = t;
|
|
77
|
+
soonestId = taskId;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!soonestId) return;
|
|
82
|
+
|
|
83
|
+
const delay = Math.max(0, soonestTime - Date.now());
|
|
84
|
+
// Cap delay to 2^31-1 ms (~24.8 days) to avoid setTimeout overflow
|
|
85
|
+
const safeDelay = Math.min(delay, 2147483647);
|
|
86
|
+
|
|
87
|
+
schedulerTimer = setTimeout(() => {
|
|
88
|
+
if (!running) return;
|
|
89
|
+
// If we had to cap the delay, just re-schedule
|
|
90
|
+
if (safeDelay < delay) {
|
|
91
|
+
scheduleNext();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
tick(soonestId);
|
|
95
|
+
}, safeDelay);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Execute a scheduled task and re-compute its next run.
|
|
100
|
+
*/
|
|
101
|
+
async function tick(taskId) {
|
|
102
|
+
if (!running) return;
|
|
103
|
+
|
|
104
|
+
const info = scheduledTasks.get(taskId);
|
|
105
|
+
if (!info) {
|
|
106
|
+
scheduleNext();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`[SCHEDULER] Executing task "${taskId}" (cron: ${info.cron})`);
|
|
111
|
+
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
let status = 'success';
|
|
114
|
+
let result = null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
result = await executeScheduledTask(taskId);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
status = 'error';
|
|
120
|
+
console.error(`[SCHEDULER] Task "${taskId}" failed:`, err.message);
|
|
121
|
+
result = { error: err.message };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const durationMs = Date.now() - startTime;
|
|
125
|
+
|
|
126
|
+
// Update the task's schedule metadata
|
|
127
|
+
try {
|
|
128
|
+
const tasks = await loadTasks();
|
|
129
|
+
const task = getTaskById(taskId);
|
|
130
|
+
if (task && task.schedule) {
|
|
131
|
+
task.schedule.lastRun = startTime;
|
|
132
|
+
task.schedule.lastRunStatus = status;
|
|
133
|
+
task.schedule.lastRunDurationMs = durationMs;
|
|
134
|
+
|
|
135
|
+
// Recompute next run
|
|
136
|
+
try {
|
|
137
|
+
const cron = resolveCron(task.schedule);
|
|
138
|
+
if (cron) {
|
|
139
|
+
const nextRun = getNextRun(cron);
|
|
140
|
+
task.schedule.nextRun = nextRun.getTime();
|
|
141
|
+
scheduledTasks.set(taskId, { cron, nextRun });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
scheduledTasks.delete(taskId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await saveTasks(tasks);
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error(`[SCHEDULER] Failed to update task "${taskId}" after execution:`, err.message);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Log execution
|
|
154
|
+
try {
|
|
155
|
+
const entry = {
|
|
156
|
+
id: 'sched_' + startTime + '_' + Math.floor(Math.random() * 1000),
|
|
157
|
+
timestamp: startTime,
|
|
158
|
+
method: 'POST',
|
|
159
|
+
path: `/api/tasks/${taskId}/api`,
|
|
160
|
+
status: status === 'success' ? 200 : 500,
|
|
161
|
+
durationMs,
|
|
162
|
+
source: 'scheduler',
|
|
163
|
+
mode: 'unknown',
|
|
164
|
+
taskId,
|
|
165
|
+
taskName: null,
|
|
166
|
+
url: null,
|
|
167
|
+
result
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Try to get task name
|
|
171
|
+
try {
|
|
172
|
+
const task = getTaskById(taskId);
|
|
173
|
+
if (task) {
|
|
174
|
+
entry.taskName = task.name;
|
|
175
|
+
entry.mode = task.mode || 'agent';
|
|
176
|
+
entry.url = task.url || null;
|
|
177
|
+
}
|
|
178
|
+
} catch { }
|
|
179
|
+
|
|
180
|
+
await appendExecution(entry);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(`[SCHEDULER] Failed to log execution for task "${taskId}":`, err.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
scheduleNext();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Execute a task using the same logic as the API endpoint.
|
|
190
|
+
* Creates mock req/res to reuse existing handlers.
|
|
191
|
+
*/
|
|
192
|
+
async function executeScheduledTask(taskId) {
|
|
193
|
+
await loadTasks();
|
|
194
|
+
const task = getTaskById(taskId);
|
|
195
|
+
if (!task) throw new Error('Task not found: ' + taskId);
|
|
196
|
+
|
|
197
|
+
// Lazy-require to avoid circular deps
|
|
198
|
+
const { handleAgent } = require('../../agent');
|
|
199
|
+
const { handleScrape } = require('../../scrape');
|
|
200
|
+
|
|
201
|
+
// Build runtime variables
|
|
202
|
+
const runtimeVars = {};
|
|
203
|
+
if (task.variables) {
|
|
204
|
+
for (const [key, v] of Object.entries(task.variables)) {
|
|
205
|
+
runtimeVars[key] = v.value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Construct mock request/response
|
|
210
|
+
const body = {
|
|
211
|
+
...task,
|
|
212
|
+
taskId: task.id,
|
|
213
|
+
variables: runtimeVars,
|
|
214
|
+
taskVariables: runtimeVars,
|
|
215
|
+
actions: task.actions || [],
|
|
216
|
+
mode: task.mode || 'agent',
|
|
217
|
+
runSource: 'scheduler'
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const mockReq = {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
body,
|
|
223
|
+
query: {},
|
|
224
|
+
params: { id: taskId },
|
|
225
|
+
protocol: 'http',
|
|
226
|
+
socket: { remoteAddress: '127.0.0.1' },
|
|
227
|
+
path: `/api/tasks/${taskId}/api`,
|
|
228
|
+
on: () => { },
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
let statusCode = 200;
|
|
233
|
+
const mockRes = {
|
|
234
|
+
status: (code) => { statusCode = code; return mockRes; },
|
|
235
|
+
json: (data) => {
|
|
236
|
+
if (statusCode >= 400) {
|
|
237
|
+
reject(new Error(data?.error || `HTTP ${statusCode}`));
|
|
238
|
+
} else {
|
|
239
|
+
resolve(data);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
locals: {},
|
|
243
|
+
on: () => { },
|
|
244
|
+
setHeader: () => { },
|
|
245
|
+
write: () => { },
|
|
246
|
+
end: () => resolve(null),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const runId = 'sched_' + Date.now();
|
|
250
|
+
mockReq.body.runId = runId;
|
|
251
|
+
|
|
252
|
+
const handler = task.mode === 'scrape' ? handleScrape : handleAgent;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
sendExecutionUpdate(runId, { status: 'started' });
|
|
256
|
+
} catch { }
|
|
257
|
+
|
|
258
|
+
Promise.resolve(handler(mockReq, mockRes)).catch(reject);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Start the scheduler. Call this after the server starts.
|
|
264
|
+
*/
|
|
265
|
+
async function startScheduler() {
|
|
266
|
+
if (running) return;
|
|
267
|
+
running = true;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await loadSchedules();
|
|
271
|
+
const count = scheduledTasks.size;
|
|
272
|
+
console.log(`[SCHEDULER] Loaded ${count} scheduled task(s).`);
|
|
273
|
+
|
|
274
|
+
// Update nextRun on all scheduled tasks so frontend can display them
|
|
275
|
+
if (count > 0) {
|
|
276
|
+
const tasks = await loadTasks();
|
|
277
|
+
let dirty = false;
|
|
278
|
+
for (const [taskId, info] of scheduledTasks) {
|
|
279
|
+
const task = getTaskById(taskId);
|
|
280
|
+
if (task && task.schedule) {
|
|
281
|
+
const nextRunMs = info.nextRun.getTime();
|
|
282
|
+
if (task.schedule.nextRun !== nextRunMs) {
|
|
283
|
+
task.schedule.nextRun = nextRunMs;
|
|
284
|
+
dirty = true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (dirty) await saveTasks(tasks);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
scheduleNext();
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error('[SCHEDULER] Failed to start:', err.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Stop the scheduler.
|
|
299
|
+
*/
|
|
300
|
+
function stopScheduler() {
|
|
301
|
+
running = false;
|
|
302
|
+
if (schedulerTimer) {
|
|
303
|
+
clearTimeout(schedulerTimer);
|
|
304
|
+
schedulerTimer = null;
|
|
305
|
+
}
|
|
306
|
+
scheduledTasks.clear();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Refresh the schedule for a specific task (call after task update).
|
|
311
|
+
*/
|
|
312
|
+
async function refreshSchedule(taskId) {
|
|
313
|
+
const tasks = await loadTasks();
|
|
314
|
+
const task = getTaskById(taskId);
|
|
315
|
+
|
|
316
|
+
if (!task || !task.schedule || !task.schedule.enabled) {
|
|
317
|
+
scheduledTasks.delete(taskId);
|
|
318
|
+
scheduleNext();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const cron = resolveCron(task.schedule);
|
|
323
|
+
if (!cron) {
|
|
324
|
+
scheduledTasks.delete(taskId);
|
|
325
|
+
scheduleNext();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const nextRun = getNextRun(cron);
|
|
331
|
+
scheduledTasks.set(taskId, { cron, nextRun });
|
|
332
|
+
|
|
333
|
+
// Persist nextRun
|
|
334
|
+
task.schedule.nextRun = nextRun.getTime();
|
|
335
|
+
await saveTasks(tasks);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(`[SCHEDULER] Failed to refresh schedule for "${taskId}":`, err.message);
|
|
338
|
+
scheduledTasks.delete(taskId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
scheduleNext();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Remove the schedule for a specific task.
|
|
346
|
+
*/
|
|
347
|
+
function removeSchedule(taskId) {
|
|
348
|
+
scheduledTasks.delete(taskId);
|
|
349
|
+
scheduleNext();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get current scheduler status.
|
|
354
|
+
*/
|
|
355
|
+
function getSchedulerStatus() {
|
|
356
|
+
const entries = [];
|
|
357
|
+
for (const [taskId, info] of scheduledTasks) {
|
|
358
|
+
entries.push({
|
|
359
|
+
taskId,
|
|
360
|
+
cron: info.cron,
|
|
361
|
+
nextRun: info.nextRun.toISOString(),
|
|
362
|
+
nextRunMs: info.nextRun.getTime()
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
running,
|
|
367
|
+
scheduledCount: scheduledTasks.size,
|
|
368
|
+
tasks: entries
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
startScheduler,
|
|
374
|
+
stopScheduler,
|
|
375
|
+
refreshSchedule,
|
|
376
|
+
removeSchedule,
|
|
377
|
+
getSchedulerStatus,
|
|
378
|
+
resolveCron,
|
|
379
|
+
};
|