Files
memind/scheduled-task-intent.mjs
T
john d58dc2a251 feat(wechat): add Intent Transaction Layer with unified task schema
Introduce Draft → Confirm → Commit flow for WeChat schedule intents behind
feature flags, plus h5_tasks dual-write/read aggregation and rollout scripts
so reminders and automations get explicit user confirmation before persisting.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 15:59:17 +08:00

341 lines
12 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 { 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,
};