Add scheduled task automation skill with worker execution pipeline.
Memind CI / Test, build, and release guards (push) Failing after 18s

Introduce scheduled-task-automation for H5 and WeChat, persist tasks in h5_scheduled_tasks, and run due jobs via a dedicated worker that executes agent tasks and delivers results.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-31 08:02:49 +08:00
parent 4186522c72
commit 81e63c16d3
27 changed files with 1736 additions and 2 deletions
+130
View File
@@ -0,0 +1,130 @@
import {
executeScheduledTask,
formatScheduledTaskDeliveryMessage,
} from './scheduled-task-executor.mjs';
export function startScheduledTaskWorker({
scheduledTaskService,
scheduleService = null,
executeTask = executeScheduledTask,
userAuth = null,
tkmindProxy = null,
sessionSnapshotService = null,
notificationDispatcher = null,
logger = console,
intervalMs = Number(process.env.H5_SCHEDULED_TASK_SCAN_INTERVAL_MS ?? 30_000),
maxAttempts = Number(process.env.H5_SCHEDULED_TASK_MAX_ATTEMPTS ?? 3),
executionTimeoutMs = Number(process.env.H5_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS ?? 15 * 60 * 1000),
runOnStart = true,
} = {}) {
const sendScheduleNotification =
notificationDispatcher?.sendScheduleNotification?.bind(notificationDispatcher) ??
null;
const canExecute =
scheduledTaskService
&& typeof executeTask === 'function'
&& userAuth
&& tkmindProxy;
if (!canExecute) {
return { stop() {} };
}
let stopped = false;
let running = false;
const deliverTaskResult = async (task, deliveryText) => {
const text = formatScheduledTaskDeliveryMessage(task, deliveryText);
const notifyChannel = task.notifyChannel ?? 'both';
if (scheduleService?.createUserNotification && (notifyChannel === 'web' || notifyChannel === 'both')) {
await scheduleService.createUserNotification({
userId: task.userId,
channel: 'web',
notificationType: 'scheduled_task_result',
title: `定时任务完成:${task.title}`,
body: text,
data: {
taskId: task.id,
recurrence: task.recurrence,
},
});
}
if (
(notifyChannel === 'wechat' || notifyChannel === 'both')
&& typeof sendScheduleNotification === 'function'
) {
await sendScheduleNotification({ userId: task.userId, text });
}
};
const runOnce = async () => {
if (running || stopped) return;
running = true;
try {
const dueTasks = await scheduledTaskService.listDueTasks({ limit: 10 });
for (const candidate of dueTasks) {
const task = await scheduledTaskService.lockTask(candidate.id);
if (!task) continue;
try {
await scheduledTaskService.markTaskRunning(task);
const result = await executeTask(task, {
userAuth,
tkmindProxy,
sessionSnapshotService,
timeoutMs: executionTimeoutMs,
logger,
});
await deliverTaskResult(task, result.deliveryText);
await scheduledTaskService.markTaskSucceeded(task, {
result: {
deliveryText: result.deliveryText,
},
deliveryText: result.deliveryText,
sessionId: result.sessionId,
requestId: result.requestId,
});
} catch (err) {
logger.warn?.('Scheduled task execution failed:', err);
const failedTask = await scheduledTaskService.markTaskFailed(task, err, {
maxAttempts,
});
if (failedTask.status === 'failed' && scheduleService?.createUserNotification) {
await scheduleService.createUserNotification({
userId: task.userId,
channel: 'web',
notificationType: 'scheduled_task_failed',
title: `定时任务失败:${task.title}`,
body: String(failedTask.lastError ?? '执行失败'),
data: { taskId: task.id },
}).catch(() => {});
}
if (
failedTask.status === 'failed'
&& (task.notifyChannel === 'wechat' || task.notifyChannel === 'both')
&& typeof sendScheduleNotification === 'function'
) {
await sendScheduleNotification({
userId: task.userId,
text: `定时任务失败:${task.title}\n${failedTask.lastError ?? '执行失败'}`,
}).catch(() => {});
}
}
}
} catch (err) {
logger.warn?.('Scheduled task worker failed:', err);
} finally {
running = false;
}
};
const timer = setInterval(runOnce, Math.max(1000, Number(intervalMs) || 30_000));
timer.unref?.();
if (runOnStart) void runOnce();
return {
runOnce,
stop() {
stopped = true;
clearInterval(timer);
},
};
}