d58dc2a251
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>
341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
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,
|
||
};
|