2026-08-24 15:59:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
addLocalDays,
|
|
|
|
|
|
getLocalParts,
|
|
|
|
|
|
normalizeTimezone,
|
|
|
|
|
|
zonedTimeToEpochMs,
|
|
|
|
|
|
} from './schedule-time.mjs';
|
2026-08-24 18:02:19 +08:00
|
|
|
|
import {
|
|
|
|
|
|
hasReminderSetupCue,
|
|
|
|
|
|
isWeeklyScheduleCue,
|
|
|
|
|
|
normalizeScheduleUtterance,
|
|
|
|
|
|
} from './schedule-utterance-normalize.mjs';
|
2026-08-24 15:59:17 +08:00
|
|
|
|
|
2026-06-19 23:06:43 +08:00
|
|
|
|
function normalizeText(text) {
|
2026-08-24 15:59:17 +08:00
|
|
|
|
return String(text ?? '')
|
2026-08-24 18:02:19 +08:00
|
|
|
|
.replace(/^(?:嗯|那个|就是|然后|呃|能不能|请|帮我|麻烦|嗨|好的|好)+/u, '')
|
2026-08-24 15:59:17 +08:00
|
|
|
|
.replace(/\s+/g, '')
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 17:12:50 +08:00
|
|
|
|
/** ASR/wechat often inserts spaces around colons, e.g. "17 :08". */
|
|
|
|
|
|
function normalizeLooseTimeText(text) {
|
2026-08-24 18:02:19 +08:00
|
|
|
|
return normalizeScheduleUtterance(text);
|
2026-08-24 17:12:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 15:59:17 +08:00
|
|
|
|
function pad2(value) {
|
|
|
|
|
|
return String(value).padStart(2, '0');
|
2026-06-19 23:06:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function chineseHourToNumber(value) {
|
|
|
|
|
|
const raw = String(value ?? '').trim();
|
|
|
|
|
|
if (/^\d{1,2}$/.test(raw)) return Number(raw);
|
|
|
|
|
|
const map = {
|
|
|
|
|
|
零: 0,
|
|
|
|
|
|
一: 1,
|
|
|
|
|
|
二: 2,
|
|
|
|
|
|
两: 2,
|
|
|
|
|
|
三: 3,
|
|
|
|
|
|
四: 4,
|
|
|
|
|
|
五: 5,
|
|
|
|
|
|
六: 6,
|
|
|
|
|
|
七: 7,
|
|
|
|
|
|
八: 8,
|
|
|
|
|
|
九: 9,
|
|
|
|
|
|
十: 10,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (raw === '十') return 10;
|
|
|
|
|
|
if (raw.startsWith('十')) return 10 + (map[raw.slice(1)] ?? 0);
|
|
|
|
|
|
if (raw.endsWith('十')) return (map[raw[0]] ?? 0) * 10;
|
|
|
|
|
|
if (raw.includes('十')) {
|
|
|
|
|
|
const [tens, ones] = raw.split('十');
|
|
|
|
|
|
return (map[tens] ?? 1) * 10 + (map[ones] ?? 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return map[raw] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 19:32:09 +08:00
|
|
|
|
export function parseHourMinute(text) {
|
2026-08-24 17:12:50 +08:00
|
|
|
|
const normalized = normalizeLooseTimeText(text);
|
2026-08-24 18:02:19 +08:00
|
|
|
|
let match = normalized.match(
|
|
|
|
|
|
/(?:今天|明天|后天|今晚|明晚|明早|今早)?(?:早上|上午|清晨|每天(?:早上|上午)?)?(早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|:)(半|[0-9]{1,2}分?)?/,
|
2026-08-24 15:59:17 +08:00
|
|
|
|
);
|
2026-08-24 18:02:19 +08:00
|
|
|
|
if (!match) {
|
|
|
|
|
|
match = normalized.match(
|
|
|
|
|
|
/(?:今天|明天|后天|今晚|明晚|明早|今早)?(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?(早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})点(半|[0-9]{1,2}分?)?/,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-06-19 23:06:43 +08:00
|
|
|
|
if (!match) return null;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
const period = String(match[1] ?? '');
|
|
|
|
|
|
let hour = chineseHourToNumber(match[2]);
|
2026-06-19 23:06:43 +08:00
|
|
|
|
if (hour === null || hour < 0 || hour > 23) return null;
|
|
|
|
|
|
let minute = 0;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
if (match[3] === '半') minute = 30;
|
|
|
|
|
|
else if (match[3]) minute = Number(String(match[3]).replace('分', ''));
|
2026-06-19 23:06:43 +08:00
|
|
|
|
if (!Number.isFinite(minute) || minute < 0 || minute > 59) return null;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
if (period === '中午') {
|
|
|
|
|
|
hour = 12;
|
|
|
|
|
|
} else if (/下午|傍晚|晚上/u.test(period) && hour < 12) {
|
|
|
|
|
|
hour += 12;
|
|
|
|
|
|
} else if (/上午|早上|清晨|凌晨/u.test(period) && hour === 12) {
|
|
|
|
|
|
hour = 0;
|
2026-08-24 18:02:19 +08:00
|
|
|
|
} else if (/今晚/u.test(normalized) && hour < 12 && !/上午|早上|清晨|凌晨/u.test(period)) {
|
|
|
|
|
|
hour += 12;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
}
|
2026-06-19 23:06:43 +08:00
|
|
|
|
return { hour, minute };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 15:59:17 +08:00
|
|
|
|
function countTimeExpressions(text) {
|
2026-08-24 17:12:50 +08:00
|
|
|
|
const normalized = normalizeLooseTimeText(text);
|
|
|
|
|
|
const matches = normalized.match(
|
2026-08-24 15:59:17 +08:00
|
|
|
|
/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)(?:半|[0-9]{1,2}(?:分(?!开)|(?![0-9]))?)?/gu,
|
|
|
|
|
|
);
|
|
|
|
|
|
return matches?.length ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function wantsSimpleTimedReminder(compact, text) {
|
2026-08-24 18:02:19 +08:00
|
|
|
|
if (isWeeklyScheduleCue(compact)) return false;
|
|
|
|
|
|
if (!hasReminderSetupCue(compact)) return false;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
if (countTimeExpressions(text) !== 1) return false;
|
|
|
|
|
|
if (/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:).{1,24}[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)/u.test(compact)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return parseHourMinute(text) != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractTimedReminderTitle(text) {
|
|
|
|
|
|
const original = String(text ?? '').trim();
|
|
|
|
|
|
const commaParts = original.split(/[,,]/).map((part) => part.trim()).filter(Boolean);
|
|
|
|
|
|
if (commaParts.length > 1) {
|
|
|
|
|
|
const tail = commaParts[commaParts.length - 1];
|
|
|
|
|
|
const tailTitle = cleanupTimedReminderTitleFragment(tail);
|
|
|
|
|
|
if (tailTitle && tailTitle.length >= 2) return tailTitle;
|
|
|
|
|
|
}
|
|
|
|
|
|
return cleanupTimedReminderTitleFragment(original);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cleanupTimedReminderTitleFragment(text) {
|
2026-08-24 17:12:50 +08:00
|
|
|
|
let title = normalizeLooseTimeText(String(text ?? '').trim());
|
2026-08-24 15:59:17 +08:00
|
|
|
|
title = title
|
|
|
|
|
|
.replace(/^(?:帮我|请|麻烦)?(?:设置|设|创建|添加)(?:一个|个)?提醒[,,、::\s]*/u, '')
|
|
|
|
|
|
.replace(/^(?:帮我|请)?(?:设置|设)(?:一个|个)?(?:待办|代办|带办|代拜)[,,、::\s]*/u, '');
|
|
|
|
|
|
title = title
|
2026-08-24 18:02:19 +08:00
|
|
|
|
.replace(/^(?:帮我|请|麻烦)?(?:设置|设|加|添加|创建|安排|定)(?:一个|个|一下)?(?:每天|每日|天天)?/u, '')
|
|
|
|
|
|
.replace(/^(?:帮我|请|麻烦)?(?:加个|添加|创建|安排)(?:一个|个)?提醒/u, '')
|
|
|
|
|
|
.replace(/^(?:帮我|请|麻烦)?(?:定|设)个?(?:闹钟|闹铃)/u, '')
|
2026-08-24 15:59:17 +08:00
|
|
|
|
.replace(/(?:今天|今日|今晚|明天|后天|明早|今早)/gu, ' ')
|
2026-08-24 17:42:08 +08:00
|
|
|
|
.replace(/(?:每天|每日|天天)/gu, ' ')
|
2026-08-24 15:59:17 +08:00
|
|
|
|
.replace(/(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)/gu, ' ')
|
2026-08-24 16:29:31 +08:00
|
|
|
|
.replace(
|
|
|
|
|
|
/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)(?:半|[0-9]{1,2}(?:\s*分(?!开)|(?![0-9]))?)?/gu,
|
|
|
|
|
|
' ',
|
|
|
|
|
|
)
|
2026-08-24 18:02:19 +08:00
|
|
|
|
.replace(/(?:提醒我|提醒|闹钟|闹铃|叫我|设置提醒|设提醒|备忘提醒|定时提醒)/gu, ' ')
|
2026-08-24 15:59:17 +08:00
|
|
|
|
.replace(/[,,、::]/g, ' ')
|
|
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
title = title
|
|
|
|
|
|
.replace(/^(?:嗯|那个|就是|然后|呃|能不能|请|帮我|麻烦|嗨)+/u, '')
|
|
|
|
|
|
.replace(/^(?:去|来)?(?:参加|进行)?(?:一个|个)?/u, '')
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
title = title.replace(/^分开/u, '开会').replace(/开会会/u, '开会');
|
|
|
|
|
|
if (!title || title.length < 2 || /^(?:请|麻烦|能不能|嗨|嗯|那个|帮我)$/u.test(title)) return null;
|
|
|
|
|
|
return title;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildTimedReminderLocal({
|
|
|
|
|
|
text,
|
|
|
|
|
|
hour,
|
|
|
|
|
|
minute = 0,
|
|
|
|
|
|
timezone = 'Asia/Shanghai',
|
|
|
|
|
|
now = Date.now(),
|
|
|
|
|
|
} = {}) {
|
|
|
|
|
|
const compact = normalizeText(text);
|
|
|
|
|
|
let dayOffset = 0;
|
|
|
|
|
|
if (/后天/u.test(compact)) dayOffset = 2;
|
|
|
|
|
|
else if (/明天|明早|明晚/u.test(compact)) dayOffset = 1;
|
2026-08-24 18:02:19 +08:00
|
|
|
|
else if (/今晚/u.test(compact)) dayOffset = 0;
|
|
|
|
|
|
else if (/今早/u.test(compact)) dayOffset = 0;
|
2026-08-24 15:59:17 +08:00
|
|
|
|
|
|
|
|
|
|
const tz = normalizeTimezone(timezone);
|
|
|
|
|
|
let dayStart = addLocalDays(now, dayOffset, tz);
|
|
|
|
|
|
let parts = getLocalParts(dayStart, tz);
|
|
|
|
|
|
let remindAt = zonedTimeToEpochMs(
|
|
|
|
|
|
{
|
|
|
|
|
|
year: parts.year,
|
|
|
|
|
|
month: parts.month,
|
|
|
|
|
|
day: parts.day,
|
|
|
|
|
|
hour,
|
|
|
|
|
|
minute,
|
|
|
|
|
|
second: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
tz,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (remindAt <= now) {
|
|
|
|
|
|
dayStart = addLocalDays(dayStart, 1, tz);
|
|
|
|
|
|
parts = getLocalParts(dayStart, tz);
|
|
|
|
|
|
remindAt = zonedTimeToEpochMs(
|
|
|
|
|
|
{
|
|
|
|
|
|
year: parts.year,
|
|
|
|
|
|
month: parts.month,
|
|
|
|
|
|
day: parts.day,
|
|
|
|
|
|
hour,
|
|
|
|
|
|
minute,
|
|
|
|
|
|
second: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
tz,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
const localParts = getLocalParts(remindAt, tz);
|
|
|
|
|
|
return `${localParts.year}-${pad2(localParts.month)}-${pad2(localParts.day)} ${pad2(localParts.hour)}:${pad2(localParts.minute)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseSimpleTimedReminder(text, { timezone = 'Asia/Shanghai', now = Date.now() } = {}) {
|
|
|
|
|
|
const compact = normalizeText(text);
|
|
|
|
|
|
if (!wantsSimpleTimedReminder(compact, text)) return null;
|
|
|
|
|
|
const time = parseHourMinute(text);
|
|
|
|
|
|
if (!time) return null;
|
|
|
|
|
|
const title = extractTimedReminderTitle(text);
|
|
|
|
|
|
if (!title || title.length < 2) {
|
|
|
|
|
|
const needsClarification = time ? ['reminder_title'] : ['reminder_time', 'reminder_title'];
|
|
|
|
|
|
const partial = {
|
|
|
|
|
|
action: 'create_timed_reminder',
|
|
|
|
|
|
needsClarification,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (time) {
|
|
|
|
|
|
partial.hour = time.hour;
|
|
|
|
|
|
partial.minute = time.minute;
|
|
|
|
|
|
partial.remindLocal = buildTimedReminderLocal({
|
|
|
|
|
|
text,
|
|
|
|
|
|
hour: time.hour,
|
|
|
|
|
|
minute: time.minute,
|
|
|
|
|
|
timezone,
|
|
|
|
|
|
now,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return partial;
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
action: 'create_timed_reminder',
|
|
|
|
|
|
title,
|
|
|
|
|
|
remindLocal: buildTimedReminderLocal({
|
|
|
|
|
|
text,
|
|
|
|
|
|
hour: time.hour,
|
|
|
|
|
|
minute: time.minute,
|
|
|
|
|
|
timezone,
|
|
|
|
|
|
now,
|
|
|
|
|
|
}),
|
|
|
|
|
|
hour: time.hour,
|
|
|
|
|
|
minute: time.minute,
|
2026-08-24 17:42:08 +08:00
|
|
|
|
recurrence: /(?:每天|每日|天天)/u.test(compact) ? 'daily' : 'once',
|
2026-08-24 15:59:17 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:19:03 +08:00
|
|
|
|
function extractTodoTitle(text) {
|
|
|
|
|
|
const original = String(text ?? '').trim();
|
|
|
|
|
|
if (!original) return null;
|
|
|
|
|
|
const quoted = original.match(/[「『【((“"']([^」』】))”"']{1,80})[」』】))”"']/);
|
|
|
|
|
|
if (quoted?.[1]) {
|
|
|
|
|
|
const title = quoted[1].trim();
|
|
|
|
|
|
if (title) return title;
|
|
|
|
|
|
}
|
|
|
|
|
|
const trailingCommand = original.match(
|
|
|
|
|
|
/^(.{1,80}?)(?:帮我)?(?:设|设置|设下|记)(?:一个|下一个)?(?:待办|代办|带办|代拜)(?:吧|一下)?$/u,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (trailingCommand?.[1]) {
|
|
|
|
|
|
const title = trailingCommand[1].trim();
|
|
|
|
|
|
if (title) return title;
|
|
|
|
|
|
}
|
|
|
|
|
|
const cleaned = original
|
|
|
|
|
|
.replace(
|
|
|
|
|
|
/^.*?(?:不用提醒|先记一下|帮我记一下|帮我记|记一下|添加待办|添加任务|记个待办|记个任务|设置一个待办|设置一个代办|设置一个带办|设置一个代拜|设置下一个代拜|设下一个代拜|帮我设置一个待办|帮我设置一个代办|帮我设置一个带办|帮我设置一个代拜|待办|代办|带办|代拜|任务)(?:[::,,、\s]+)?/u,
|
|
|
|
|
|
'',
|
|
|
|
|
|
)
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
return cleaned || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 12:10:21 +08:00
|
|
|
|
function wantsDailyTodoDigest(compact) {
|
|
|
|
|
|
const daily = /每天|每日|天天/.test(compact);
|
|
|
|
|
|
const send = /发|发送|推送|提醒|通知|给我/.test(compact);
|
|
|
|
|
|
const digest =
|
|
|
|
|
|
/(待办|todo|任务).*(记录|列表|清单|安排|摘要|汇总)/.test(compact)
|
|
|
|
|
|
|| /(今天|今日|当天).*(待办|todo|任务)/.test(compact)
|
|
|
|
|
|
|| /(待办|todo|任务).*(今天|今日|当天)/.test(compact)
|
|
|
|
|
|
|| /一天的待办/.test(compact);
|
2026-08-24 15:59:17 +08:00
|
|
|
|
if (digest && /(?:页面|网页|html|h5)/iu.test(compact)) return false;
|
|
|
|
|
|
if (daily && send && digest) return true;
|
|
|
|
|
|
// 「早上7点把当天待办发给我」省略了「每天」但语义仍是 digest
|
|
|
|
|
|
if (!daily && send && /(当天|今天|今日).*(待办|todo|任务)/.test(compact) && parseHourMinute(compact)) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
2026-07-07 12:10:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 15:59:17 +08:00
|
|
|
|
export function parseScheduleIntent(text, {
|
|
|
|
|
|
timezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
|
|
|
|
|
now = Date.now(),
|
|
|
|
|
|
} = {}) {
|
2026-06-19 23:06:43 +08:00
|
|
|
|
const compact = normalizeText(text);
|
|
|
|
|
|
if (!compact) return { action: 'none' };
|
|
|
|
|
|
|
2026-07-07 12:10:21 +08:00
|
|
|
|
if (wantsDailyTodoDigest(compact)) {
|
2026-06-19 23:06:43 +08:00
|
|
|
|
const time = parseHourMinute(compact);
|
|
|
|
|
|
if (!time) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
action: 'create_daily_todo_digest',
|
|
|
|
|
|
needsClarification: ['digest_time'],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
action: 'create_daily_todo_digest',
|
|
|
|
|
|
hour: time.hour,
|
|
|
|
|
|
minute: time.minute,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:19:03 +08:00
|
|
|
|
const wantsBalanceAlert = /(余额|钱包|账户).*(不足|低于|提醒|预警|通知)|余额不足|低余额|余额预警/.test(compact);
|
|
|
|
|
|
if (wantsBalanceAlert) {
|
|
|
|
|
|
const thresholdMatch = compact.match(/(?:低于|少于|不足|小于)(\d{1,6})(?:元|块|rmb|人民币|cny)?/i);
|
|
|
|
|
|
const thresholdYuan = thresholdMatch ? Number(thresholdMatch[1]) : null;
|
|
|
|
|
|
const thresholdCents = thresholdYuan == null ? null : Math.max(0, Math.round(thresholdYuan * 100));
|
|
|
|
|
|
return thresholdCents == null
|
|
|
|
|
|
? { action: 'create_balance_alert', needsClarification: ['threshold'] }
|
|
|
|
|
|
: { action: 'create_balance_alert', thresholdCents };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 23:06:43 +08:00
|
|
|
|
if (/看看|查看|看下|列出/.test(compact) && /(待办|日程|行程|计划|任务)/.test(compact)) {
|
|
|
|
|
|
return { action: 'query_schedule' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 15:59:17 +08:00
|
|
|
|
const timedReminder = parseSimpleTimedReminder(text, { timezone, now });
|
|
|
|
|
|
if (timedReminder) return timedReminder;
|
|
|
|
|
|
|
2026-08-24 18:02:19 +08:00
|
|
|
|
if (/^(?:帮我|请|麻烦)?(?:设置|设)(?:一个|个|一下)?提醒$/u.test(compact)
|
|
|
|
|
|
|| /^(?:帮我|请|麻烦)?(?:加个|添加|创建|安排)(?:一个|个)?提醒$/u.test(compact)
|
|
|
|
|
|
|| /^(?:帮我|请|麻烦)?(?:定|设)个?(?:闹钟|闹铃)$/u.test(compact)) {
|
2026-08-24 15:59:17 +08:00
|
|
|
|
return {
|
|
|
|
|
|
action: 'create_timed_reminder',
|
|
|
|
|
|
needsClarification: ['reminder_time', 'reminder_title'],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:19:03 +08:00
|
|
|
|
const wantsTodoRecord =
|
|
|
|
|
|
/(不用提醒|先记一下|帮我记一下|帮我记|记一下|添加待办|添加任务|记个待办|记个任务|设置一个待办|设置一个代办|设置一个带办|设置一个代拜|设置下一个代拜|设下一个代拜|帮我设置一个待办|帮我设置一个代办|帮我设置一个带办|帮我设置一个代拜|待办|代办|带办|代拜|任务)/.test(
|
|
|
|
|
|
compact,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (wantsTodoRecord) {
|
|
|
|
|
|
const explicitNoReminder = /(不用提醒|不提醒|先记一下)/.test(compact);
|
|
|
|
|
|
const hasScheduleTime = /(提醒|闹钟|叫我|今天|今晚|明天|后天|早上|上午|中午|下午|晚上|[0-9零一二两三四五六七八九十]{1,3}(点|:|:))/.test(
|
|
|
|
|
|
compact,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!explicitNoReminder && hasScheduleTime) {
|
|
|
|
|
|
return { action: 'schedule_agent' };
|
|
|
|
|
|
}
|
|
|
|
|
|
const title = extractTodoTitle(text);
|
|
|
|
|
|
if (!title) {
|
|
|
|
|
|
return { action: 'create_todo', needsClarification: ['todo_title'] };
|
|
|
|
|
|
}
|
|
|
|
|
|
return { action: 'create_todo', title };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 23:06:43 +08:00
|
|
|
|
return { action: 'none' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isScheduleIntent(intent) {
|
|
|
|
|
|
return intent?.action && intent.action !== 'none';
|
|
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
|
|
|
|
|
|
export function shouldUseScheduleAssistant(text) {
|
|
|
|
|
|
const compact = normalizeText(text);
|
|
|
|
|
|
if (!compact) return false;
|
2026-07-31 08:02:49 +08:00
|
|
|
|
// Scheduled automation (execute + deliver) uses scheduled-task-automation skill.
|
|
|
|
|
|
if (/(?:定时(?:自动)?任务|scheduled\s*task|scheduled\s*automation)/iu.test(compact)) return false;
|
|
|
|
|
|
if (/(?:每天|每日|每周|定时|到点|届时|自动)/u.test(compact)
|
|
|
|
|
|
&& /(?:做|生成|制作|创建|写|执行|跑|更新|整理|汇总|推送)/u.test(compact)
|
|
|
|
|
|
&& !/(?:提醒我|待办记录|待办列表|待办清单|当天待办|一天的待办)/u.test(compact)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
if (isScheduleIntent(parseScheduleIntent(text))) return true;
|
2026-08-24 18:02:19 +08:00
|
|
|
|
if (hasReminderSetupCue(compact) && !isWeeklyScheduleCue(compact)) return true;
|
|
|
|
|
|
const scheduleKeywords = /(提醒|待办|代办|带办|代拜|日程|行程|安排|计划|闹钟|闹铃)/;
|
|
|
|
|
|
const timeKeywords = /(今天|今晚|明天|后天|早上|上午|中午|下午|晚上|\d{1,2}[点::]|[零一二两三四五六七八九十]{1,3}点)/;
|
2026-06-26 15:19:03 +08:00
|
|
|
|
return scheduleKeywords.test(compact) && timeKeywords.test(compact);
|
|
|
|
|
|
}
|
2026-08-24 18:02:19 +08:00
|
|
|
|
|
|
|
|
|
|
/** Last-resort timed reminder when colloquial phrasing missed parseScheduleIntent. */
|
|
|
|
|
|
export function tryResolveTimedReminderIntent(text, {
|
|
|
|
|
|
timezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
|
|
|
|
|
now = Date.now(),
|
|
|
|
|
|
} = {}) {
|
|
|
|
|
|
const parsed = parseScheduleIntent(text, { timezone, now });
|
|
|
|
|
|
if (parsed.action === 'create_timed_reminder') return parsed;
|
|
|
|
|
|
|
|
|
|
|
|
const compact = normalizeText(text);
|
|
|
|
|
|
if (!hasReminderSetupCue(compact) || isWeeklyScheduleCue(compact)) return null;
|
|
|
|
|
|
if (countTimeExpressions(text) !== 1 || !parseHourMinute(text)) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const time = parseHourMinute(text);
|
|
|
|
|
|
const title = extractTimedReminderTitle(text);
|
|
|
|
|
|
if (title && title.length >= 2) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
action: 'create_timed_reminder',
|
|
|
|
|
|
title,
|
|
|
|
|
|
remindLocal: buildTimedReminderLocal({ text, hour: time.hour, minute: time.minute, timezone, now }),
|
|
|
|
|
|
hour: time.hour,
|
|
|
|
|
|
minute: time.minute,
|
|
|
|
|
|
recurrence: /(?:每天|每日|天天)/u.test(compact) ? 'daily' : 'once',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
action: 'create_timed_reminder',
|
|
|
|
|
|
needsClarification: ['reminder_title'],
|
|
|
|
|
|
hour: time.hour,
|
|
|
|
|
|
minute: time.minute,
|
|
|
|
|
|
remindLocal: buildTimedReminderLocal({ text, hour: time.hour, minute: time.minute, timezone, now }),
|
|
|
|
|
|
recurrence: /(?:每天|每日|天天)/u.test(compact) ? 'daily' : 'once',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|