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
+95
View File
@@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import mysql from 'mysql2/promise';
import { createScheduleService } from './schedule-service.mjs';
import { createScheduledTaskService } from './scheduled-task-service.mjs';
import { resolveScheduleTimestamp } from './schedule-time.mjs';
import { shouldAutoCreateReminderAtStart } from './schedule-service.mjs';
import { renderLongImage } from './mindspace-long-image.mjs';
@@ -484,6 +485,7 @@ const ALL_TOOLS = [
let quotaPool = null;
let scheduleService = null;
let scheduledTaskService = null;
let userDataSpaceService = null;
function isQuotaSyncConfigured() {
@@ -529,6 +531,20 @@ function getScheduleService() {
return scheduleService;
}
function getScheduledTaskService() {
if (!PRIVATE_DATA_USER_ID) throw new Error('当前会话没有用户上下文,不能操作定时任务');
const pool = getQuotaPool();
if (!pool || !isScheduleConfigured()) {
throw new Error('当前环境未配置待办数据库连接');
}
if (!scheduledTaskService) {
scheduledTaskService = createScheduledTaskService(pool, {
defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
});
}
return scheduledTaskService;
}
if (isScheduleConfigured()) {
ALL_TOOLS.push(
{
@@ -591,6 +607,50 @@ if (isScheduleConfigured()) {
},
},
},
{
name: 'scheduled_task_create',
description:
'为当前用户创建定时自动任务(Scheduled Automation)。到点由系统自动执行 taskSpec 并推送结果;不用于纯待办/提醒。',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: '任务摘要标题,可选' },
taskSpec: { type: 'string', description: '到时具体执行什么(必填)' },
recurrence: { type: 'string', description: 'once / daily / weekly,默认 daily' },
runAtLocal: { type: 'string', description: '一次性任务执行时间 YYYY-MM-DD HH:mm' },
hour: { type: 'number', description: 'daily/weekly 的小时 0-23' },
minute: { type: 'number', description: 'daily/weekly 的分钟 0-59,默认 0' },
weekday: { type: 'number', description: 'weekly 的星期 0=周日 … 6=周六' },
timezone: { type: 'string', description: '时区,可选' },
notifyChannel: { type: 'string', description: 'wechat / web / both,默认 both' },
sourceMessageId: { type: 'string', description: '服务号消息 ID;有值时必须原样传入' },
sourceText: { type: 'string', description: '原始用户文本,可选' },
},
required: ['taskSpec'],
},
},
{
name: 'scheduled_task_list',
description: '查询当前用户的定时自动任务列表。仅返回当前用户数据。',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: '任务状态,默认 active,可选 all' },
limit: { type: 'number', description: '返回数量,默认 20,可选' },
},
},
},
{
name: 'scheduled_task_cancel',
description: '取消当前用户的一个定时自动任务。需提供 taskId 或 titleMatch。',
inputSchema: {
type: 'object',
properties: {
taskId: { type: 'string', description: '任务 ID,优先使用' },
titleMatch: { type: 'string', description: '按标题模糊匹配最近一条 active 任务' },
},
},
},
);
}
@@ -1155,6 +1215,41 @@ async function callTool(name, args) {
});
return [{ type: 'text', text: JSON.stringify(items, null, 2) }];
}
case 'scheduled_task_create': {
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
const task = await getScheduledTaskService().createTask({
userId: PRIVATE_DATA_USER_ID,
title: args.title ?? null,
taskSpec: args.taskSpec,
recurrence: args.recurrence ?? 'daily',
runAtLocal: args.runAtLocal ?? null,
hour: args.hour ?? null,
minute: args.minute ?? 0,
weekday: args.weekday ?? null,
timezone,
notifyChannel: args.notifyChannel ?? 'both',
sourceChannel: 'agent',
sourceMessageId: args.sourceMessageId ?? null,
sourceText: args.sourceText ?? null,
});
return [{ type: 'text', text: JSON.stringify(task, null, 2) }];
}
case 'scheduled_task_list': {
const tasks = await getScheduledTaskService().listTasks({
userId: PRIVATE_DATA_USER_ID,
status: args.status ?? 'active',
limit: args.limit ?? 20,
});
return [{ type: 'text', text: JSON.stringify(tasks, null, 2) }];
}
case 'scheduled_task_cancel': {
const task = await getScheduledTaskService().cancelTask({
userId: PRIVATE_DATA_USER_ID,
taskId: args.taskId ?? null,
titleMatch: args.titleMatch ?? null,
});
return [{ type: 'text', text: JSON.stringify(task, null, 2) }];
}
default:
throw new Error(`未知工具:${name}`);
}