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, }; }