From 1e3a125f8345c5a7d0e4d3fccfe6256c10aa40a4 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 24 Aug 2026 17:12:50 +0800 Subject: [PATCH] fix(schedule): parse ASR times with spaces around colons for ITL routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voice/wechat ASR often emits forms like "17 :08", which bypassed timed_reminder classification and fell through to agent_schedule + schedule-guard false failures. Co-authored-by: Cursor --- schedule-intent.mjs | 18 +++++++++++++++--- schedule-intent.test.mjs | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/schedule-intent.mjs b/schedule-intent.mjs index 512d867..babff23 100644 --- a/schedule-intent.mjs +++ b/schedule-intent.mjs @@ -12,6 +12,16 @@ function normalizeText(text) { .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'); +} + function pad2(value) { return String(value).padStart(2, '0'); } @@ -44,7 +54,8 @@ function chineseHourToNumber(value) { } export function parseHourMinute(text) { - const match = text.match( + const normalized = normalizeLooseTimeText(text); + const match = normalized.match( /(?:今天|明天|后天|今晚|明晚)?(?:早上|上午|清晨|每天(?:早上|上午)?)?(早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|:)(半|[0-9]{1,2}分?)?/, ); if (!match) return null; @@ -66,7 +77,8 @@ export function parseHourMinute(text) { } function countTimeExpressions(text) { - const matches = String(text ?? '').match( + const normalized = normalizeLooseTimeText(text); + const matches = normalized.match( /[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)(?:半|[0-9]{1,2}(?:分(?!开)|(?![0-9]))?)?/gu, ); return matches?.length ?? 0; @@ -93,7 +105,7 @@ function extractTimedReminderTitle(text) { } function cleanupTimedReminderTitleFragment(text) { - let title = String(text ?? '').trim(); + let title = normalizeLooseTimeText(String(text ?? '').trim()); title = title .replace(/^(?:帮我|请|麻烦)?(?:设置|设|创建|添加)(?:一个|个)?提醒[,,、::\s]*/u, '') .replace(/^(?:帮我|请)?(?:设置|设)(?:一个|个)?(?:待办|代办|带办|代拜)[,,、::\s]*/u, ''); diff --git a/schedule-intent.test.mjs b/schedule-intent.test.mjs index 9862506..a422726 100644 --- a/schedule-intent.test.mjs +++ b/schedule-intent.test.mjs @@ -125,6 +125,25 @@ test('specific dated reminder still routes to schedule assistant', () => { assert.equal(shouldUseScheduleAssistant('明天下午三点提醒我开会'), true); }); +test('parses ASR reminder text with spaced fullwidth colon before minutes', () => { + const now = Date.UTC(2026, 7, 24, 9, 7, 0); // 2026-08-24 17:07 Asia/Shanghai + const intent = parseScheduleIntent('设置一个提醒,今天下午 17 :08要开会', { + timezone: 'Asia/Shanghai', + now, + }); + assert.equal(intent.action, 'create_timed_reminder'); + assert.equal(intent.title, '要开会'); + assert.equal(intent.hour, 17); + assert.equal(intent.minute, 8); + assert.equal(intent.remindLocal, '2026-08-24 17:08'); +}); + +test('ASR spaced colon routes to ITL timed reminder not agent', () => { + const result = classifyUserIntent('设置一个提醒,今天下午 17 :08要开会'); + assert.equal(result.kind, 'timed_reminder'); + assert.equal(result.layer, 'L1'); +}); + test('parses direct reminder setup phrase from wechat', () => { const now = Date.UTC(2026, 7, 24, 4, 0, 0); // 2026-08-24 12:00 Asia/Shanghai const intent = parseScheduleIntent('帮我设置提醒,下午 14:30 分开会,项目计划例会', {