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:
+21
-3
@@ -1,5 +1,9 @@
|
||||
import { detectQueryGuard } from './intent-query-guard.mjs';
|
||||
import { parseScheduleIntent, shouldUseScheduleAssistant } from './schedule-intent.mjs';
|
||||
import {
|
||||
parseScheduleIntent,
|
||||
shouldUseScheduleAssistant,
|
||||
tryResolveTimedReminderIntent,
|
||||
} from './schedule-intent.mjs';
|
||||
import {
|
||||
isScheduledTaskIntent,
|
||||
parseScheduledTaskIntent,
|
||||
@@ -8,10 +12,12 @@ import {
|
||||
|
||||
function detectAmbiguity(text) {
|
||||
const compact = String(text ?? '').replace(/\s+/g, '');
|
||||
const notifyCue = /(?:提醒我|设置提醒|设个?提醒|闹钟|叫我)/u.test(compact);
|
||||
const notifyCue =
|
||||
/(?:提醒我|设置提醒|设个?提醒|闹钟|闹铃|叫我|到点提醒|提醒一下)/u.test(compact)
|
||||
|| /(?:设置|设|加|添加|创建|安排|定)(?:一个|个|一下)?(?:每天|每日|天天)?(?:.{0,40})?提醒/u.test(compact);
|
||||
const actCue = /(?:生成|制作|创建|做|执行|推送|发送|整理).{0,12}(?:页面|日报|报告|摘要)/u.test(compact);
|
||||
if (notifyCue && actCue) return true;
|
||||
if (notifyCue && /(?:自动|生成)/u.test(compact)) return true;
|
||||
if (notifyCue && /(?:自动|生成)/u.test(compact) && actCue) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,6 +70,18 @@ export function classifyUserIntent(text, { now = Date.now(), timezone = 'Asia/Sh
|
||||
if (shouldUseScheduledTaskAutomation(text)) {
|
||||
return { layer: 'L2', kind: 'scheduled_task', action: 'agent_automation', detail: schedTask };
|
||||
}
|
||||
|
||||
const timedFallback = tryResolveTimedReminderIntent(text, { now, timezone });
|
||||
if (timedFallback?.action === 'create_timed_reminder') {
|
||||
return {
|
||||
layer: 'L1',
|
||||
kind: 'timed_reminder',
|
||||
action: timedFallback.action,
|
||||
detail: timedFallback,
|
||||
clarify: timedFallback.needsClarification ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldUseScheduleAssistant(text)) {
|
||||
const multiTime = (String(text).match(/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)/gu) ?? []).length > 1;
|
||||
return { layer: multiTime ? 'L3' : 'L1', kind: 'agent_schedule', action: 'agent_schedule' };
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@
|
||||
"verify:goosed-proxy-boundary": "node scripts/check-goosed-proxy-boundary.mjs",
|
||||
"verify:h5-session-patches": "node scripts/verify-h5-session-patches.mjs",
|
||||
"verify:scheduled-task-automation": "node scripts/verify-scheduled-task-automation.mjs",
|
||||
"verify:intent-transaction-layer": "node scripts/verify-intent-transaction-layer.mjs && node --test intent-classifier.test.mjs intent-query-guard.test.mjs intent-draft-service.test.mjs task-unified-service.test.mjs task-unified-sync.test.mjs wechat/handlers/intent-transaction.test.mjs",
|
||||
"verify:intent-transaction-layer": "node scripts/verify-intent-transaction-layer.mjs && node scripts/verify-schedule-reminder-routing.mjs && node --test intent-classifier.test.mjs intent-query-guard.test.mjs intent-draft-service.test.mjs task-unified-service.test.mjs task-unified-sync.test.mjs wechat/handlers/intent-transaction.test.mjs schedule-intent.test.mjs schedule-utterance-normalize.test.mjs",
|
||||
"migrate:legacy-tasks-to-h5-tasks": "node scripts/migrate-legacy-tasks-to-h5-tasks.mjs --dry-run",
|
||||
"migrate:tang-unified-tasks-103": "node scripts/migrate-tang-unified-tasks-103.mjs",
|
||||
"verify:tang-itl-readiness-103": "node scripts/verify-tang-itl-readiness-103.mjs",
|
||||
|
||||
+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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,3 +219,39 @@ test('nextDailyRunAt rolls to tomorrow when today time has passed', () => {
|
||||
const next = nextDailyRunAt({ hour: 7, minute: 0, timezone: 'Asia/Shanghai', now });
|
||||
assert.equal(next, Date.UTC(2026, 5, 18, 23, 0, 0)); // 2026-06-19 07:00 Asia/Shanghai
|
||||
});
|
||||
|
||||
test('parses colloquial add-reminder phrases', () => {
|
||||
const now = Date.UTC(2026, 7, 24, 9, 0, 0);
|
||||
const cases = [
|
||||
['加个提醒明天9点开会', { title: '开会', hour: 9 }],
|
||||
['添加提醒后天10点体检', { title: '体检', hour: 10 }],
|
||||
['创建提醒明早6点跑步', { title: '跑步', hour: 6 }],
|
||||
['备忘提醒下午3点交报告', { title: '交报告', hour: 15 }],
|
||||
];
|
||||
for (const [text, expected] of cases) {
|
||||
const intent = parseScheduleIntent(text, { timezone: 'Asia/Shanghai', now });
|
||||
assert.equal(intent.action, 'create_timed_reminder', text);
|
||||
assert.equal(intent.title, expected.title, text);
|
||||
assert.equal(intent.hour, expected.hour, text);
|
||||
}
|
||||
});
|
||||
|
||||
test('colloquial add-reminder phrases route to ITL timed reminder', () => {
|
||||
for (const text of [
|
||||
'加个提醒明天9点开会',
|
||||
'添加提醒后天10点体检',
|
||||
'备忘提醒下午3点交报告',
|
||||
]) {
|
||||
const result = classifyUserIntent(text);
|
||||
assert.equal(result.kind, 'timed_reminder', text);
|
||||
assert.equal(result.layer, 'L1', text);
|
||||
}
|
||||
});
|
||||
|
||||
test('time-only reminder without title asks for clarification', () => {
|
||||
const now = Date.UTC(2026, 7, 24, 9, 0, 0);
|
||||
const intent = parseScheduleIntent('安排一个提醒今晚8点', { timezone: 'Asia/Shanghai', now });
|
||||
assert.equal(intent.action, 'create_timed_reminder');
|
||||
assert.deepEqual(intent.needsClarification, ['reminder_title']);
|
||||
assert.equal(intent.hour, 20);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Normalize WeChat / ASR schedule utterances before intent parsing.
|
||||
* Keeps raw user text for display; use this for matching only.
|
||||
*/
|
||||
export function normalizeScheduleUtterance(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/[\uFF10-\uFF19]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xff10 + 0x30))
|
||||
.replace(/:/g, ':')
|
||||
.replace(/([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})\s*([::])\s*/gu, '$1$2')
|
||||
.replace(/([::])\s*([0-9]{1,2})/gu, '$1$2')
|
||||
.replace(/([0-9]{1,2})\s*(点|:|:)/gu, '$1$2')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** True when user is trying to set a reminder/alarm (not automation/digest). */
|
||||
export function hasReminderSetupCue(compact) {
|
||||
if (!compact) return false;
|
||||
if (/(?:生成|制作|创建|做|执行|推送|发送|整理).{0,12}(?:页面|日报|报告|摘要)/u.test(compact)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/(?:提醒我|到点提醒|提醒一下|闹钟|闹铃|叫我|记得提醒)/u.test(compact)
|
||||
|| /(?:加个|添加|创建|安排)(?:一个|个)?提醒/u.test(compact)
|
||||
|| /(?:设置|设|加|添加|创建|安排|定)(?:一个|个|一下)?(?:每天|每日|天天)?(?:.{0,40})?提醒/u.test(compact)
|
||||
|| /(?:定|设)个?(?:闹钟|闹铃)/u.test(compact)
|
||||
|| /^定时提醒/u.test(compact)
|
||||
|| /(?:待办|代办|带办|代拜)提醒/u.test(compact)
|
||||
|| /备忘提醒/u.test(compact)
|
||||
);
|
||||
}
|
||||
|
||||
export function isWeeklyScheduleCue(compact) {
|
||||
return /(?:每周|每个周|星期[一二三四五六日天]|周[一二三四五六日天])/u.test(compact);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
hasReminderSetupCue,
|
||||
isWeeklyScheduleCue,
|
||||
normalizeScheduleUtterance,
|
||||
} from './schedule-utterance-normalize.mjs';
|
||||
|
||||
test('normalizeScheduleUtterance fixes ASR spaced colons and fullwidth digits', () => {
|
||||
assert.equal(normalizeScheduleUtterance('下午 17 : 08 开会'), '下午 17:08 开会');
|
||||
assert.equal(normalizeScheduleUtterance('18 :00'), '18:00');
|
||||
});
|
||||
|
||||
test('hasReminderSetupCue recognizes common colloquial reminder phrases', () => {
|
||||
const compact = (text) => String(text).replace(/\s+/g, '');
|
||||
assert.equal(hasReminderSetupCue(compact('加个提醒明天9点开会')), true);
|
||||
assert.equal(hasReminderSetupCue(compact('添加提醒后天10点体检')), true);
|
||||
assert.equal(hasReminderSetupCue(compact('定个闹钟明天7点')), true);
|
||||
assert.equal(hasReminderSetupCue(compact('备忘提醒下午3点交报告')), true);
|
||||
assert.equal(hasReminderSetupCue(compact('设置每天18点提醒打卡')), true);
|
||||
assert.equal(hasReminderSetupCue(compact('每天6点帮我做今日新闻页面')), false);
|
||||
});
|
||||
|
||||
test('isWeeklyScheduleCue detects weekly recurrence wording', () => {
|
||||
assert.equal(isWeeklyScheduleCue('每周一早上9点提醒我开会'), true);
|
||||
assert.equal(isWeeklyScheduleCue('明天9点提醒我开会'), false);
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify WeChat reminder utterances route to ITL timed_reminder (L1), not agent_schedule.
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { classifyUserIntent } from '../intent-classifier.mjs';
|
||||
import { parseScheduleIntent, tryResolveTimedReminderIntent } from '../schedule-intent.mjs';
|
||||
import { hasReminderSetupCue } from '../schedule-utterance-normalize.mjs';
|
||||
|
||||
const timezone = 'Asia/Shanghai';
|
||||
const now = Date.UTC(2026, 7, 24, 9, 0, 0); // 2026-08-24 17:00 Asia/Shanghai
|
||||
|
||||
const matrix = [
|
||||
{
|
||||
text: '设置一个提醒,今天下午 17 :08要开会',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '要开会', hour: 17, minute: 8 },
|
||||
},
|
||||
{
|
||||
text: '设置每天18:00提醒打卡',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '打卡', recurrence: 'daily', hour: 18 },
|
||||
},
|
||||
{
|
||||
text: '加个提醒明天9点开会',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '开会', hour: 9 },
|
||||
},
|
||||
{
|
||||
text: '添加提醒后天10点体检',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '体检', hour: 10 },
|
||||
},
|
||||
{
|
||||
text: '创建提醒明早6点跑步',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '跑步', hour: 6 },
|
||||
},
|
||||
{
|
||||
text: '备忘提醒下午3点交报告',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '交报告', hour: 15 },
|
||||
},
|
||||
{
|
||||
text: '安排一个提醒今晚8点',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', clarify: ['reminder_title'], hour: 20 },
|
||||
},
|
||||
{
|
||||
text: '定个闹钟明天7点起床',
|
||||
expect: { kind: 'timed_reminder', action: 'create_timed_reminder', title: '起床', hour: 7 },
|
||||
},
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function pass(label) {
|
||||
passed += 1;
|
||||
console.log(`✔ ${label}`);
|
||||
}
|
||||
|
||||
function fail(label, detail) {
|
||||
failed += 1;
|
||||
console.error(`✘ ${label}: ${detail}`);
|
||||
}
|
||||
|
||||
for (const row of matrix) {
|
||||
const compact = String(row.text).replace(/\s+/g, '');
|
||||
if (!hasReminderSetupCue(compact)) {
|
||||
fail(row.text, 'missing reminder setup cue');
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseScheduleIntent(row.text, { timezone, now })
|
||||
?? tryResolveTimedReminderIntent(row.text, { timezone, now });
|
||||
if (parsed?.action !== row.expect.action) {
|
||||
fail(row.text, `parse action ${parsed?.action} !== ${row.expect.action}`);
|
||||
continue;
|
||||
}
|
||||
if (row.expect.title && parsed.title !== row.expect.title) {
|
||||
fail(row.text, `title ${parsed.title} !== ${row.expect.title}`);
|
||||
continue;
|
||||
}
|
||||
if (row.expect.hour != null && parsed.hour !== row.expect.hour) {
|
||||
fail(row.text, `hour ${parsed.hour} !== ${row.expect.hour}`);
|
||||
continue;
|
||||
}
|
||||
if (row.expect.minute != null && parsed.minute !== row.expect.minute) {
|
||||
fail(row.text, `minute ${parsed.minute} !== ${row.expect.minute}`);
|
||||
continue;
|
||||
}
|
||||
if (row.expect.recurrence && parsed.recurrence !== row.expect.recurrence) {
|
||||
fail(row.text, `recurrence ${parsed.recurrence} !== ${row.expect.recurrence}`);
|
||||
continue;
|
||||
}
|
||||
if (row.expect.clarify) {
|
||||
assert.deepEqual(parsed.needsClarification, row.expect.clarify);
|
||||
}
|
||||
|
||||
const classified = classifyUserIntent(row.text, { timezone, now });
|
||||
if (classified.kind !== row.expect.kind) {
|
||||
fail(row.text, `classified kind ${classified.kind} !== ${row.expect.kind}`);
|
||||
continue;
|
||||
}
|
||||
if (classified.action === 'agent_schedule') {
|
||||
fail(row.text, 'must not route to agent_schedule');
|
||||
continue;
|
||||
}
|
||||
|
||||
pass(row.text);
|
||||
}
|
||||
|
||||
console.log(`\nschedule reminder routing: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
wasWechatDeliveryDeferred,
|
||||
} from './wechat/customer-service-deferred.mjs';
|
||||
import { guardScheduleConfirmationReply } from './wechat/handlers/schedule-guard.mjs';
|
||||
import { isIntentTransactionEnabled } from './intent-transaction-config.mjs';
|
||||
import { handleWechatScheduleIntent } from './wechat/handlers/schedule.mjs';
|
||||
import { handleWechatIntentTransaction } from './wechat/handlers/intent-transaction.mjs';
|
||||
import { handleWechatScheduledTaskIntent } from './wechat/handlers/scheduled-task.mjs';
|
||||
@@ -2871,6 +2872,7 @@ export function createWechatMpService({
|
||||
userId: user.userId,
|
||||
sourceMessageId: intent.msgId,
|
||||
logger,
|
||||
intentTransactionEnabled: isIntentTransactionEnabled(),
|
||||
});
|
||||
try {
|
||||
const requestStartedAt = Date.now();
|
||||
|
||||
@@ -2,18 +2,30 @@ export function looksLikeScheduleConfirmation(text) {
|
||||
const compact = String(text ?? '').replace(/\s+/g, '');
|
||||
if (!compact) return false;
|
||||
if (/(没有|没能|未能|无法|不能|失败|需要你|请补充|请告诉)/.test(compact)) return false;
|
||||
return /(已经|已|现在).{0,12}(设置|安排|记录|加上|添加|创建).{0,12}(待办|提醒|日程|安排|闹钟)|设置好了|已经设置好了|已经加上/.test(
|
||||
return /(已经|已|现在).{0,12}(设置|安排|记录|加上|添加|创建).{0,12}(待办|提醒|日程|安排|闹钟|闹铃)|设置好了|已经设置好了|已经加上/.test(
|
||||
compact,
|
||||
);
|
||||
}
|
||||
|
||||
export function rewriteScheduleConfirmationWhenItlEnabled(replyText, { enabled = false } = {}) {
|
||||
if (!enabled || !looksLikeScheduleConfirmation(replyText)) return replyText;
|
||||
return [
|
||||
'这类提醒需要你先确认卡片后才会写入。',
|
||||
'请直接发完整安排(例如「每天18点提醒打卡」),我会先发确认卡片;回复「确认」后再生效。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export async function guardScheduleConfirmationReply({
|
||||
replyText,
|
||||
scheduleService,
|
||||
userId,
|
||||
sourceMessageId,
|
||||
logger,
|
||||
intentTransactionEnabled = false,
|
||||
}) {
|
||||
if (intentTransactionEnabled) {
|
||||
return rewriteScheduleConfirmationWhenItlEnabled(replyText, { enabled: true });
|
||||
}
|
||||
if (!scheduleService || !looksLikeScheduleConfirmation(replyText)) return replyText;
|
||||
const messageId = String(sourceMessageId ?? '').trim();
|
||||
if (!messageId || typeof scheduleService.listItemsBySourceMessage !== 'function') {
|
||||
|
||||
@@ -154,6 +154,19 @@ test('schedule confirmation guard blocks false positives', async () => {
|
||||
assert.match(guarded, /不能算设置成功/);
|
||||
});
|
||||
|
||||
test('schedule confirmation guard rewrites agent pseudo-confirm when ITL enabled', async () => {
|
||||
const guarded = await guardScheduleConfirmationReply({
|
||||
replyText: '已经帮你设置好了待办提醒',
|
||||
scheduleService: { listItemsBySourceMessage: async () => [] },
|
||||
userId: 'user-1',
|
||||
sourceMessageId: 'msg-1',
|
||||
logger: null,
|
||||
intentTransactionEnabled: true,
|
||||
});
|
||||
assert.match(guarded, /确认卡片/);
|
||||
assert.doesNotMatch(guarded, /不能算设置成功/);
|
||||
});
|
||||
|
||||
test('resolvePageGenerateOutcome fails when only stub artifacts exist', () => {
|
||||
const outcome = resolvePageGenerateOutcome({
|
||||
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/tang-poem.html' },
|
||||
|
||||
Reference in New Issue
Block a user