Add scheduled task automation skill with worker execution pipeline.
Memind CI / Test, build, and release guards (push) Failing after 18s
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:
@@ -0,0 +1,131 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { buildChatSkillPrompt, SCHEDULED_TASK_AUTOMATION_SKILL_NAME } from './chat-skills.mjs';
|
||||
import { localDateLabel } from './schedule-time.mjs';
|
||||
|
||||
function messageText(message) {
|
||||
if (typeof message?.content === 'string') return message.content.trim();
|
||||
if (!Array.isArray(message?.content)) return '';
|
||||
return message.content
|
||||
.filter((item) => item?.type === 'text')
|
||||
.map((item) => String(item?.text ?? '').trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function buildScheduledTaskExecutionPrompt(task, {
|
||||
now = Date.now(),
|
||||
timezone = task?.timezone ?? 'Asia/Shanghai',
|
||||
} = {}) {
|
||||
const dateLabel = localDateLabel(now, timezone);
|
||||
const body = [
|
||||
'【定时任务自动执行 Scheduled Automation】',
|
||||
`任务标题:${task.title}`,
|
||||
`执行日期:${dateLabel}`,
|
||||
`任务要求:${task.taskSpec}`,
|
||||
'执行约束:',
|
||||
'- 这是系统自动触发的定时任务,请直接完成可交付结果,不要反问用户。',
|
||||
'- 若需要生成页面,必须先 load_skill → static-page-publish,再 write_file 到 public/*.html,并给出正式可访问 URL。',
|
||||
'- 若只需摘要/文本,给出完整中文结果摘要。',
|
||||
'- 完成后在回复中明确写出交付结果(链接或摘要)。',
|
||||
].join('\n');
|
||||
const prefix = buildChatSkillPrompt(
|
||||
'scheduled-task-automation',
|
||||
SCHEDULED_TASK_AUTOMATION_SKILL_NAME,
|
||||
);
|
||||
return {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: `${prefix}${body}` }],
|
||||
metadata: {
|
||||
displayText: `定时任务:${task.title}`,
|
||||
userVisible: false,
|
||||
agentVisible: true,
|
||||
memindScheduledTask: {
|
||||
taskId: task.id,
|
||||
recurrence: task.recurrence,
|
||||
automated: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function extractScheduledTaskDeliveryText(messages, task) {
|
||||
const lastAssistant = [...(Array.isArray(messages) ? messages : [])]
|
||||
.reverse()
|
||||
.find((message) => message?.role === 'assistant');
|
||||
const text = messageText(lastAssistant);
|
||||
if (text) return text.trim();
|
||||
return `定时任务「${task.title}」已执行完成。`;
|
||||
}
|
||||
|
||||
export function formatScheduledTaskDeliveryMessage(task, deliveryText) {
|
||||
const header = `定时任务完成:${task.title}`;
|
||||
const body = String(deliveryText ?? '').trim() || '任务已执行,请前往 MindSpace 查看结果。';
|
||||
return `${header}\n\n${body}`.trim();
|
||||
}
|
||||
|
||||
export async function executeScheduledTask(task, {
|
||||
userAuth,
|
||||
tkmindProxy,
|
||||
sessionSnapshotService = null,
|
||||
timeoutMs = 15 * 60 * 1000,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
if (!task?.userId) throw new Error('缺少 task.userId');
|
||||
if (!userAuth || typeof userAuth.canUseChat !== 'function') {
|
||||
throw new Error('缺少 userAuth.canUseChat');
|
||||
}
|
||||
if (
|
||||
!tkmindProxy
|
||||
|| typeof tkmindProxy.startSessionForUser !== 'function'
|
||||
|| typeof tkmindProxy.submitSessionReplyAndAwaitFinishForUser !== 'function'
|
||||
) {
|
||||
throw new Error('缺少 tkmindProxy 会话执行能力');
|
||||
}
|
||||
|
||||
const gate = await userAuth.canUseChat(task.userId);
|
||||
if (!gate?.ok) {
|
||||
const err = new Error(gate?.message ?? '当前账户无法执行定时任务');
|
||||
err.code = gate?.code ?? 'CHAT_NOT_ALLOWED';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
const started = await tkmindProxy.startSessionForUser(task.userId, {
|
||||
origin: 'h5',
|
||||
});
|
||||
const sessionId = started?.id ?? started?.sessionId;
|
||||
if (!sessionId) throw new Error('创建定时任务会话失败');
|
||||
|
||||
const userMessage = buildScheduledTaskExecutionPrompt(task);
|
||||
logger.info?.('[ScheduledTask] executing', {
|
||||
taskId: task.id,
|
||||
userId: task.userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
await tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
|
||||
task.userId,
|
||||
sessionId,
|
||||
requestId,
|
||||
userMessage,
|
||||
{ timeoutMs },
|
||||
);
|
||||
|
||||
let messages = [];
|
||||
if (typeof sessionSnapshotService?.refresh === 'function') {
|
||||
const refreshed = await sessionSnapshotService.refresh(sessionId).catch(() => null);
|
||||
messages = refreshed?.messages ?? refreshed?.conversation?.messages ?? [];
|
||||
} else if (typeof sessionSnapshotService?.get === 'function') {
|
||||
const snapshot = await sessionSnapshotService.get(sessionId).catch(() => null);
|
||||
messages = snapshot?.messages ?? snapshot?.conversation?.messages ?? [];
|
||||
}
|
||||
|
||||
const deliveryText = extractScheduledTaskDeliveryText(messages, task);
|
||||
return {
|
||||
sessionId,
|
||||
requestId,
|
||||
deliveryText,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user