import { parseHourMinute } from './schedule-intent.mjs'; import { addLocalDays, getLocalParts, normalizeTimezone, startOfLocalDay, zonedTimeToEpochMs, } from './schedule-time.mjs'; function normalizeText(text) { return String(text ?? '') .replace(/^(?:嗯|那个|就是|然后|呃|能不能|请|帮我|麻烦|嗨)+/u, '') .replace(/\s+/g, '') .trim(); } const EXECUTE_VERBS = /(?:做|生成|制作|创建|写|执行|跑|更新|整理|汇总|推送|发送|发给|发|看看|检查|监控|瞧瞧)/u; const SCHEDULE_MARKERS = /(?:定时(?:自动)?任务|scheduled\s*task|scheduled\s*automation|cron\s*job|recurring\s*task)/iu; const RECURRENCE_MARKERS = /(?:每天|每日|每周|定时|到点|届时|自动)/u; const WEEKDAY_LABELS = [ ['周日', '周天', '星期日', '星期天'], ['周一', '星期一'], ['周二', '星期二'], ['周三', '星期三'], ['周四', '星期四'], ['周五', '星期五'], ['周六', '星期六'], ]; const META_TASK_SPEC_PATTERNS = [ /^我要?(?:做|创建|设置|弄)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u, /^帮我?(?:做|创建|设置|弄)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u, /^请?(?:帮我)?(?:做|创建|设置|弄)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u, /^定时(?:自动)?执行(?:任务)?$/u, /^执行(?:任务)?$/u, /^做(?:一个|个)?(?:定时(?:自动)?)?(?:执行)?任务$/u, /^创建(?:一个|个)?定时(?:自动)?执行(?:任务)?$/u, /^想(?:要)?(?:做|创建|设置)?(?:一个|个)?(?:定时(?:自动)?)?执行(?:任务)?$/u, ]; function isMetaScheduledTaskSpec(spec) { const normalized = String(spec ?? '').replace(/\s+/g, '').trim(); if (!normalized) return true; return META_TASK_SPEC_PATTERNS.some((pattern) => pattern.test(normalized)); } function wantsScheduledTaskAutomation(compact) { if (SCHEDULE_MARKERS.test(compact)) return true; if (RECURRENCE_MARKERS.test(compact) && /(?:看看|检查|监控|瞧瞧).{0,20}(?:有没有|是否有)/u.test(compact)) { return true; } if (!RECURRENCE_MARKERS.test(compact)) return false; if (!EXECUTE_VERBS.test(compact)) return false; // Pure reminders/digests stay on schedule-assistant or wechat schedule handler. if (/(?:提醒我|闹钟|待办记录|待办列表|待办清单|当天待办|一天的待办)/u.test(compact)) return false; if (/(?:待办|代办|带办|代拜).{0,8}(?:记录|列表|清单|摘要|汇总)/u.test(compact)) { // 「待办摘要页面 / 待办汇总 HTML」仍是定时自动任务,不是纯 digest 推送。 if (!/(?:页面|网页|html|h5)/iu.test(compact)) return false; } return true; } function parseWeekday(compact) { for (let index = 0; index < WEEKDAY_LABELS.length; index += 1) { if (WEEKDAY_LABELS[index].some((label) => compact.includes(label))) { return index; } } return null; } function extractScheduledTaskSpec(text) { const original = String(text ?? '').trim(); if (!original) return null; let spec = original .replace(/^(?:不是|别是|并非)(?:待办|代办|提醒|日程|闹钟)[,,、::\s]*/u, '') .replace( /^(?:帮我)?(?:设|设置|创建|添加|想创建)(?:一个|个)?(?:定时(?:自动)?任务|定时执行任务)?[::,,、\s]*/u, '', ) .replace( /(?:一次|单次|仅一次|once|每天|每日|weekly|每周|daily|定时|到点|届时|自动)/giu, ' ', ) .replace( /(?:今天|今日|今晚|明天|后天|早上|上午|清晨|下午|晚上)?[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)(半|[0-9]{1,2}分?)?/gu, ' ', ) .replace(/(?:周[一二三四五六日天]|星期[一二三四五六日天])/gu, ' ') .replace(/(?:帮我|请|麻烦)/gu, ' ') .replace(/^执行[,,、::\s]+/u, '') .replace(/[,,、::\s]+执行$/u, '') .replace(/\s+/g, ' ') .trim(); if (!spec || spec.length < 3) return null; if (isMetaScheduledTaskSpec(spec)) return null; if (!EXECUTE_VERBS.test(spec.replace(/\s+/g, ''))) return null; return spec; } function formatRunAtLocal(epochMs, timezone) { const parts = getLocalParts(epochMs, timezone); return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')} ${String(parts.hour).padStart(2, '0')}:${String(parts.minute).padStart(2, '0')}`; } function resolveOnceRunAtLocal(text, compact, { timezone, now = Date.now() } = {}) { const time = parseHourMinute(text); if (!time) return null; let dayOffset = 0; if (/后天/u.test(compact)) dayOffset = 2; else if (/明天/u.test(compact)) dayOffset = 1; else if (/(?:今晚|今天|今日)/u.test(compact)) dayOffset = 0; const todayStart = startOfLocalDay(now, timezone); let targetDayStart = addLocalDays(todayStart, dayOffset, timezone); let parts = getLocalParts(targetDayStart, timezone); let runAt = zonedTimeToEpochMs( { year: parts.year, month: parts.month, day: parts.day, hour: time.hour, minute: time.minute, second: 0, }, timezone, ); if (runAt <= now) { targetDayStart = addLocalDays(targetDayStart, 1, timezone); parts = getLocalParts(targetDayStart, timezone); runAt = zonedTimeToEpochMs( { year: parts.year, month: parts.month, day: parts.day, hour: time.hour, minute: time.minute, second: 0, }, timezone, ); } return formatRunAtLocal(runAt, timezone); } export function shouldUseScheduledTaskAutomation(text) { const compact = normalizeText(text); if (!compact) return false; return wantsScheduledTaskAutomation(compact); } export function parseScheduledTaskIntent(text, { now = Date.now(), timezone = 'Asia/Shanghai' } = {}) { const compact = normalizeText(text); const original = String(text ?? '').trim(); if (!compact) return { action: 'none' }; const wantsCancel = /(?:取消|停止|关闭|删除).{0,16}(?:定时|自动|每日|每天)/u.test(compact) || /(?:取消|停止|关闭|删除).{0,16}(?:新闻|天气).{0,8}任务/u.test(compact) || /(?:cancel|stop|disable).{0,12}(?:scheduled|automation|task)/iu.test(compact); if (wantsCancel) { return { action: 'cancel_scheduled_task' }; } const wantsList = /(?:查看|看看|列出|我的).{0,12}(?:定时|自动).{0,12}(?:任务|自动化)/u.test(compact) || /(?:list|show).{0,12}(?:scheduled|automation).{0,12}tasks/iu.test(compact); if (wantsList) { return { action: 'list_scheduled_tasks' }; } if (!wantsScheduledTaskAutomation(compact)) { return { action: 'none' }; } const tz = normalizeTimezone(timezone); const recurrence = /(?:一次|单次|仅一次|once)/iu.test(compact) ? 'once' : /(?:每周|weekly)/iu.test(compact) ? 'weekly' : /(?:每天|每日|daily)/u.test(compact) ? 'daily' : null; const time = parseHourMinute(original); const weekday = recurrence === 'weekly' ? parseWeekday(compact) : null; const runAtLocal = recurrence === 'once' || (!recurrence && time) ? resolveOnceRunAtLocal(original, compact, { timezone: tz, now }) : null; const taskSpec = extractScheduledTaskSpec(original); const hasTaskSpec = Boolean(taskSpec); const needsClarification = []; if (recurrence === 'weekly' && weekday == null) { needsClarification.push('weekday'); } if (recurrence === 'once' || (!recurrence && /(?:今晚|今天|明天|后天)/u.test(compact))) { if (!runAtLocal) needsClarification.push('schedule'); } else if (recurrence === 'daily' || recurrence === 'weekly' || recurrence == null) { if (!time) needsClarification.push('schedule'); } else if (!time && !runAtLocal && !/(?:明天|后天|今天|今晚|\d{1,2}[点::]|[零一二两三四五六七八九十]{1,3}点)/u.test(compact)) { needsClarification.push('schedule'); } if (!hasTaskSpec) { needsClarification.push('task_spec'); } const resolvedRecurrence = recurrence ?? (runAtLocal ? 'once' : 'daily'); if (needsClarification.length > 0) { return { action: 'create_scheduled_task', needsClarification, recurrence: resolvedRecurrence, hour: time?.hour ?? null, minute: time?.minute ?? 0, weekday, runAtLocal, taskSpec, }; } return { action: 'create_scheduled_task', recurrence: resolvedRecurrence, hour: time?.hour ?? null, minute: time?.minute ?? 0, weekday, runAtLocal, taskSpec, title: taskSpec?.slice(0, 80) ?? null, }; } export function buildScheduledTaskCreatePayload(intent, { userId, sourceChannel = 'agent', sourceSessionId = null, sourceMessageId = null, sourceText = null, timezone = 'Asia/Shanghai', notifyChannel = 'both', } = {}) { if (!userId) throw new Error('缺少用户'); if (intent?.action !== 'create_scheduled_task') { throw new Error('不是创建定时自动任务意图'); } if (intent.needsClarification?.length) { throw new Error('创建定时任务前仍需澄清信息'); } const taskSpec = String(intent.taskSpec ?? '').trim(); if (!taskSpec) throw new Error('缺少 taskSpec(执行内容)'); return { userId, title: intent.title ?? taskSpec.slice(0, 80), taskSpec, recurrence: intent.recurrence ?? 'daily', runAtLocal: intent.recurrence === 'once' ? intent.runAtLocal : null, hour: intent.recurrence === 'once' ? null : intent.hour, minute: intent.minute ?? 0, weekday: intent.recurrence === 'weekly' ? intent.weekday : null, timezone: normalizeTimezone(timezone), notifyChannel, sourceChannel, sourceSessionId, sourceMessageId, sourceText, }; } export function formatScheduledTaskCreateReply(task, { workerWarning = null } = {}) { const recurrenceLabel = task.recurrence === 'once' ? '一次性' : task.recurrence === 'weekly' ? '每周' : '每天'; const timeLabel = task.recurrence === 'once' ? intentRunAtLabel(task) : `${String(task.hour).padStart(2, '0')}:${String(task.minute ?? 0).padStart(2, '0')}`; const lines = [ `已设置${recurrenceLabel}定时任务:${task.title}`, `执行内容:${task.taskSpec}`, `执行时间:${timeLabel}(${task.timezone || 'Asia/Shanghai'})`, '到点会自动执行并通过服务号/站内推送结果。', ]; if (workerWarning) { lines.push(`⚠️ ${workerWarning}`); } return lines.join('\n'); } function intentRunAtLabel(task) { if (task.nextRunAt) { return formatRunAtLocal(Number(task.nextRunAt), task.timezone); } return '待确认'; } export function formatScheduledTaskClarification(intent) { const missing = intent?.needsClarification ?? []; if (missing.includes('task_spec') && missing.includes('schedule')) { return '可以。请告诉我具体执行时间和任务内容,例如“每天 6 点帮我做今日新闻页面”或“今晚 21:45 执行做新闻页面”。'; } if (missing.includes('task_spec')) { return '可以。到点需要自动执行什么?例如“搜索并生成今日新闻页面”。'; } if (missing.includes('weekday')) { return '可以。这是每周任务,请告诉我是周几、几点执行,例如“每周一 7 点整理待办摘要”。'; } if (missing.includes('schedule')) { return '可以。请告诉我想几点执行,例如“每天 6 点”或“今晚 21:45”。'; } return '可以。请补充定时任务的执行时间和具体内容。'; } export function isScheduledTaskIntent(intent) { return intent?.action && intent.action !== 'none'; } export function formatScheduledTaskListReply(tasks = []) { if (!tasks.length) return '你当前没有进行中的定时自动任务。'; const lines = ['你的定时自动任务:']; for (const task of tasks.slice(0, 10)) { const when = task.recurrence === 'once' ? formatRunAtLocal(Number(task.nextRunAt), task.timezone) : `${String(task.hour).padStart(2, '0')}:${String(task.minute ?? 0).padStart(2, '0')}`; lines.push(`- ${task.title}(${task.recurrence} ${when})`); } return lines.join('\n'); } export { extractScheduledTaskSpec, isMetaScheduledTaskSpec, parseWeekday, resolveOnceRunAtLocal, };