diff --git a/intent-classifier.mjs b/intent-classifier.mjs index 0bee535..9ec85cf 100644 --- a/intent-classifier.mjs +++ b/intent-classifier.mjs @@ -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' }; diff --git a/package.json b/package.json index e6c45fd..1457140 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/schedule-intent.mjs b/schedule-intent.mjs index 5ddc265..01da754 100644 --- a/schedule-intent.mjs +++ b/schedule-intent.mjs @@ -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', + }; +} diff --git a/schedule-intent.test.mjs b/schedule-intent.test.mjs index ecdc296..ae700ac 100644 --- a/schedule-intent.test.mjs +++ b/schedule-intent.test.mjs @@ -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); +}); diff --git a/schedule-utterance-normalize.mjs b/schedule-utterance-normalize.mjs new file mode 100644 index 0000000..e150622 --- /dev/null +++ b/schedule-utterance-normalize.mjs @@ -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); +} diff --git a/schedule-utterance-normalize.test.mjs b/schedule-utterance-normalize.test.mjs new file mode 100644 index 0000000..908b0d7 --- /dev/null +++ b/schedule-utterance-normalize.test.mjs @@ -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); +}); diff --git a/scripts/verify-schedule-reminder-routing.mjs b/scripts/verify-schedule-reminder-routing.mjs new file mode 100644 index 0000000..10235b8 --- /dev/null +++ b/scripts/verify-schedule-reminder-routing.mjs @@ -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); diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 6671260..ae41a5d 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -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(); diff --git a/wechat/handlers/schedule-guard.mjs b/wechat/handlers/schedule-guard.mjs index 94cacd9..0342b3d 100644 --- a/wechat/handlers/schedule-guard.mjs +++ b/wechat/handlers/schedule-guard.mjs @@ -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') { diff --git a/wechat/wechat-channel.test.mjs b/wechat/wechat-channel.test.mjs index eb9d3e8..f847aff 100644 --- a/wechat/wechat-channel.test.mjs +++ b/wechat/wechat-channel.test.mjs @@ -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' },