fix(schedule): harden reminder utterance routing for WeChat ITL
Memind CI / Test, build, and release guards (push) Successful in 3m43s

Expand ASR normalization and colloquial reminder parsing so simple timed reminders route to L1 confirm cards instead of the agent, and disable schedule-guard false failures when ITL is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-24 18:02:19 +08:00
parent 2319c9c808
commit 9dfafb99ca
10 changed files with 319 additions and 26 deletions
+64 -21
View File
@@ -4,22 +4,22 @@ import {
normalizeTimezone,
zonedTimeToEpochMs,
} from './schedule-time.mjs';
import {
hasReminderSetupCue,
isWeeklyScheduleCue,
normalizeScheduleUtterance,
} from './schedule-utterance-normalize.mjs';
function normalizeText(text) {
return String(text ?? '')
.replace(/^(?:嗯|那个|就是|然后|呃|能不能|请|帮我|麻烦|嗨)+/u, '')
.replace(/^(?:嗯|那个|就是|然后|呃|能不能|请|帮我|麻烦|嗨|好的|好)+/u, '')
.replace(/\s+/g, '')
.trim();
}
/** ASR/wechat often inserts spaces around colons, e.g. "17 08". */
function normalizeLooseTimeText(text) {
return String(text ?? '')
.replace(
/([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})\s*([:])\s*/gu,
'$1$2',
)
.replace(/([:])\s*([0-9]{1,2})/gu, '$1$2');
return normalizeScheduleUtterance(text);
}
function pad2(value) {
@@ -55,9 +55,14 @@ function chineseHourToNumber(value) {
export function parseHourMinute(text) {
const normalized = normalizeLooseTimeText(text);
const match = normalized.match(
/(?:今天|明天|后天|今晚|明晚)?(?:早上|上午|清晨|每天(?:早上|上午)?)?(早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|)(半|[0-9]{1,2}分?)?/,
let match = normalized.match(
/(?:今天|明天|后天|今晚|明晚|明早|今早)?(?:早上|上午|清晨|每天(?:早上|上午)?)?(早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|)(半|[0-9]{1,2}分?)?/,
);
if (!match) {
match = normalized.match(
/(?:今天|明天|后天|今晚|明晚|明早|今早)?(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?(早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})点(半|[0-9]{1,2}分?)?/,
);
}
if (!match) return null;
const period = String(match[1] ?? '');
let hour = chineseHourToNumber(match[2]);
@@ -72,6 +77,8 @@ export function parseHourMinute(text) {
hour += 12;
} else if (/上午|早上|清晨|凌晨/u.test(period) && hour === 12) {
hour = 0;
} else if (/今晚/u.test(normalized) && hour < 12 && !/上午|早上|清晨|凌晨/u.test(period)) {
hour += 12;
}
return { hour, minute };
}
@@ -85,13 +92,8 @@ function countTimeExpressions(text) {
}
function wantsSimpleTimedReminder(compact, text) {
const hasReminderCue =
/(?:提醒我|到点提醒|提醒一下|闹钟|叫我)/u.test(compact)
|| /(?:设置|设).{0,40}提醒/u.test(compact);
if (!hasReminderCue) return false;
if (/(?:生成|制作|创建|做|执行|推送|发送|整理).{0,12}(?:页面|日报|报告|摘要)/u.test(compact)) {
return false;
}
if (isWeeklyScheduleCue(compact)) return false;
if (!hasReminderSetupCue(compact)) return false;
if (countTimeExpressions(text) !== 1) return false;
if (/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|).{1,24}[0-9零一二两三四五六七八九十]{1,3}(?:点|:|)/u.test(compact)) {
return false;
@@ -116,7 +118,9 @@ function cleanupTimedReminderTitleFragment(text) {
.replace(/^(?:帮我|请|麻烦)?(?:设置|设|创建|添加)(?:一个|个)?提醒[,、:\s]*/u, '')
.replace(/^(?:帮我|请)?(?:设置|设)(?:一个|个)?(?:待办|代办|带办|代拜)[,、:\s]*/u, '');
title = title
.replace(/^(?:帮我|请|麻烦)?(?:设置|设)(?:一个|个)?(?:每天|每日|天天)?/u, '')
.replace(/^(?:帮我|请|麻烦)?(?:设置|设|加|添加|创建|安排|定)(?:一个|个|一下)?(?:每天|每日|天天)?/u, '')
.replace(/^(?:帮我|请|麻烦)?(?:加个|添加|创建|安排)(?:一个|个)?提醒/u, '')
.replace(/^(?:帮我|请|麻烦)?(?:定|设)个?(?:闹钟|闹铃)/u, '')
.replace(/(?:今天|今日|今晚|明天|后天|明早|今早)/gu, ' ')
.replace(/(?:每天|每日|天天)/gu, ' ')
.replace(/(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)/gu, ' ')
@@ -124,7 +128,7 @@ function cleanupTimedReminderTitleFragment(text) {
/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|)(?:半|[0-9]{1,2}(?:\s*分(?!开)|(?![0-9]))?)?/gu,
' ',
)
.replace(/(?:提醒我|提醒|闹钟|叫我|设置提醒|设提醒)/gu, ' ')
.replace(/(?:提醒我|提醒|闹钟|闹铃|叫我|设置提醒|设提醒|备忘提醒|定时提醒)/gu, ' ')
.replace(/[,、:]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
@@ -148,6 +152,8 @@ export function buildTimedReminderLocal({
let dayOffset = 0;
if (/后天/u.test(compact)) dayOffset = 2;
else if (/明天|明早|明晚/u.test(compact)) dayOffset = 1;
else if (/今晚/u.test(compact)) dayOffset = 0;
else if (/今早/u.test(compact)) dayOffset = 0;
const tz = normalizeTimezone(timezone);
let dayStart = addLocalDays(now, dayOffset, tz);
@@ -303,7 +309,9 @@ export function parseScheduleIntent(text, {
const timedReminder = parseSimpleTimedReminder(text, { timezone, now });
if (timedReminder) return timedReminder;
if (/^(?:帮我|请|麻烦)?(?:设置|设)(?:一个|个)?提醒$/u.test(compact)) {
if (/^(?:帮我|请|麻烦)?(?:设置|设)(?:一个|个|一下)?提醒$/u.test(compact)
|| /^(?:帮我|请|麻烦)?(?:加个|添加|创建|安排)(?:一个|个)?提醒$/u.test(compact)
|| /^(?:帮我|请|麻烦)?(?:定|设)个?(?:闹钟|闹铃)$/u.test(compact)) {
return {
action: 'create_timed_reminder',
needsClarification: ['reminder_time', 'reminder_title'],
@@ -347,7 +355,42 @@ export function shouldUseScheduleAssistant(text) {
return false;
}
if (isScheduleIntent(parseScheduleIntent(text))) return true;
const scheduleKeywords = /(提醒|待办|代办|带办|代拜|日程|行程|安排|计划|闹钟)/;
const timeKeywords = /(今天|今晚|明天|后天|早上|上午|中午|下午|晚上|\d{1,2}[点:])/;
if (hasReminderSetupCue(compact) && !isWeeklyScheduleCue(compact)) return true;
const scheduleKeywords = /(提醒|待办|代办|带办|代拜|日程|行程|安排|计划|闹钟|闹铃)/;
const timeKeywords = /(今天|今晚|明天|后天|早上|上午|中午|下午|晚上|\d{1,2}[点::]|[零一二两三四五六七八九十]{1,3}点)/;
return scheduleKeywords.test(compact) && timeKeywords.test(compact);
}
/** 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',
};
}