fix(schedule): harden reminder utterance routing for WeChat ITL
Memind CI / Test, build, and release guards (push) Successful in 3m43s
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:
+64
-21
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user