Files
memind/scheduled-task-executor.mjs
T
john 005612029f
Memind CI / Test, build, and release guards (push) Failing after 5s
Fix scheduled task page delivery unlock and notification links.
Finalize page delivery contracts after worker execution, fetch session messages via tkmindProxy, and append public page URLs to WeChat notifications.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 09:06:07 +08:00

219 lines
6.6 KiB
JavaScript

import crypto from 'node:crypto';
import path from 'node:path';
import { buildChatSkillPrompt, SCHEDULED_TASK_AUTOMATION_SKILL_NAME } from './chat-skills.mjs';
import { markPageDeliveryContractReady } from './mindspace-delivery-contract.mjs';
import {
collectOwnPublicHtmlRelativePaths,
materializeMissingPublicHtmlWrites,
} from './mindspace-public-finish-sync.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 finalizeScheduledTaskPageDelivery({
pool,
userId,
sessionId,
messages,
publishDir,
currentUser = null,
logger = console,
} = {}) {
if (!pool || !userId || !publishDir) return [];
const materialized = materializeMissingPublicHtmlWrites({ messages, publishDir });
const relativePaths = new Set(
collectOwnPublicHtmlRelativePaths({
messages,
currentUser: currentUser ?? { id: userId },
publishDir,
materialized: materialized.materialized,
skipped: materialized.skipped,
}),
);
if (sessionId) {
const [rows] = await pool.query(
`SELECT workspace_relative_path
FROM h5_page_delivery_contracts
WHERE user_id = ? AND request_id = ? AND status = 'preparing'`,
[userId, sessionId],
);
for (const row of rows ?? []) {
if (row?.workspace_relative_path) relativePaths.add(row.workspace_relative_path);
}
}
const readyPaths = [];
for (const relativePath of relativePaths) {
const ready = await markPageDeliveryContractReady({
pool,
userId,
relativePath,
}).catch(() => false);
if (ready) readyPaths.push(relativePath);
else {
logger.warn?.('[ScheduledTask] delivery contract not ready', {
userId,
sessionId,
relativePath,
});
}
}
return readyPaths;
}
export async function executeScheduledTask(task, {
userAuth,
tkmindProxy,
sessionSnapshotService = null,
pool = null,
h5Root = 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 tkmindProxy.fetchSessionConversationForUser === 'function') {
messages = await tkmindProxy.fetchSessionConversationForUser(task.userId, sessionId).catch(() => []);
} else if (typeof sessionSnapshotService?.get === 'function') {
const snapshot = await sessionSnapshotService.get(sessionId).catch(() => null);
messages = snapshot?.messages ?? snapshot?.conversation?.messages ?? [];
}
const publishDir = h5Root && task.userId
? path.join(h5Root, 'MindSpace', task.userId)
: null;
const readyPaths = await finalizeScheduledTaskPageDelivery({
pool,
userId: task.userId,
sessionId,
messages,
publishDir,
logger,
}).catch((error) => {
logger.warn?.('[ScheduledTask] finalize page delivery failed:', error);
return [];
});
let deliveryText = extractScheduledTaskDeliveryText(messages, task);
if (
readyPaths.length > 0
&& !/https?:\/\//i.test(deliveryText)
&& task.userId
) {
const links = readyPaths.map(
(relativePath) => `https://m.tkmind.cn/MindSpace/${task.userId}/${relativePath}`,
);
deliveryText = `${deliveryText}\n\n页面链接:\n${links.join('\n')}`.trim();
}
return {
sessionId,
requestId,
deliveryText,
messages,
readyPaths,
};
}