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
+21 -3
View File
@@ -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
View File
@@ -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
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',
};
}
+36
View File
@@ -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);
});
+34
View File
@@ -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);
}
+27
View File
@@ -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: '设置每天1800提醒打卡',
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);
+2
View File
@@ -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();
+13 -1
View File
@@ -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') {
+13
View File
@@ -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' },