Files
memind/scheduled-task-executor.mjs
T
john c58a23d155
Memind CI / Test, build, and release guards (push) Failing after 12h5m29s
fix(scheduled-task): improve live verify and short delivery acceptance
Extend live verify wait budget to cover Agent execution timeout, use a
deterministic smoke taskSpec, and accept concise successful replies like
「验证成功」 instead of treating them as non-delivery.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 22:41:29 +08:00

250 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`,
'执行约束:',
'- 这是系统自动触发的定时任务,请直接完成可交付结果,不要反问用户。',
'- 禁止向用户追问时间、频率或任务内容;taskSpec 已是完整执行说明。',
'- 禁止调用 scheduled_task_create / scheduled_task_list / scheduled_task_cancel;只执行 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();
}
const SCHEDULED_TASK_CLARIFICATION_PATTERNS = [
/需确认/u,
/请确认/u,
/请问/u,
/未指定/u,
/需要澄清/u,
/在创建前需要确认/u,
/信息不完整/u,
/具体几点/u,
/缺(?:少|失)/u,
];
export function looksLikeScheduledTaskNonDelivery(text, { readyPaths = [] } = {}) {
if (Array.isArray(readyPaths) && readyPaths.length > 0) return false;
const normalized = String(text ?? '').trim();
if (!normalized) return true;
if (SCHEDULED_TASK_CLARIFICATION_PATTERNS.some((pattern) => pattern.test(normalized))) {
return true;
}
if (/https?:\/\//i.test(normalized)) return false;
if (/public\/[^\s]+\.html/i.test(normalized)) return false;
if (/页面链接/u.test(normalized)) return false;
if (/(?:已生成|已完成|交付).{0,24}(?:页面|链接|结果)/u.test(normalized)) return false;
if (/^验证成功[。!]?$/u.test(normalized)) return false;
if (/^(?:任务)?(?:已)?完成[。!]?$/u.test(normalized)) return false;
if (/^【验证】/u.test(normalized) && normalized.length >= 6) return false;
return normalized.length < 12;
}
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,
};
}