diff --git a/intent-transaction-commit.mjs b/intent-transaction-commit.mjs index 0c7545e..207cdc5 100644 --- a/intent-transaction-commit.mjs +++ b/intent-transaction-commit.mjs @@ -50,7 +50,16 @@ export async function commitIntentDraft({ sourceChannel: payload.sourceChannel ?? 'wechat', sourceMessageId: payload.sourceMessageId ?? draft.sourceMessageId, sourceText: payload.sourceText ?? draft.sourceText, - metadata: { source: 'intent_transaction_layer' }, + metadata: { + source: 'intent_transaction_layer', + ...(payload.recurrence === 'daily' + ? { + recurrence: 'daily', + dailyHour: Number(payload.hour), + dailyMinute: Number(payload.minute ?? 0), + } + : {}), + }, }); const reminder = await scheduleService.createReminder({ userId, diff --git a/schedule-intent.mjs b/schedule-intent.mjs index babff23..5ddc265 100644 --- a/schedule-intent.mjs +++ b/schedule-intent.mjs @@ -85,7 +85,13 @@ function countTimeExpressions(text) { } function wantsSimpleTimedReminder(compact, text) { - if (!/(?:提醒我|设置(?:一个|个)?提醒|设(?:一个|个)?提醒|到点提醒|提醒一下|闹钟|叫我)/u.test(compact)) return false; + const hasReminderCue = + /(?:提醒我|到点提醒|提醒一下|闹钟|叫我)/u.test(compact) + || /(?:设置|设).{0,40}提醒/u.test(compact); + if (!hasReminderCue) return false; + if (/(?:生成|制作|创建|做|执行|推送|发送|整理).{0,12}(?:页面|日报|报告|摘要)/u.test(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; @@ -110,7 +116,9 @@ function cleanupTimedReminderTitleFragment(text) { .replace(/^(?:帮我|请|麻烦)?(?:设置|设|创建|添加)(?:一个|个)?提醒[,,、::\s]*/u, '') .replace(/^(?:帮我|请)?(?:设置|设)(?:一个|个)?(?:待办|代办|带办|代拜)[,,、::\s]*/u, ''); title = title + .replace(/^(?:帮我|请|麻烦)?(?:设置|设)(?:一个|个)?(?:每天|每日|天天)?/u, '') .replace(/(?:今天|今日|今晚|明天|后天|明早|今早)/gu, ' ') + .replace(/(?:每天|每日|天天)/gu, ' ') .replace(/(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)/gu, ' ') .replace( /[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)(?:半|[0-9]{1,2}(?:\s*分(?!开)|(?![0-9]))?)?/gu, @@ -211,6 +219,7 @@ function parseSimpleTimedReminder(text, { timezone = 'Asia/Shanghai', now = Date }), hour: time.hour, minute: time.minute, + recurrence: /(?:每天|每日|天天)/u.test(compact) ? 'daily' : 'once', }; } diff --git a/schedule-intent.test.mjs b/schedule-intent.test.mjs index a422726..ecdc296 100644 --- a/schedule-intent.test.mjs +++ b/schedule-intent.test.mjs @@ -144,6 +144,25 @@ test('ASR spaced colon routes to ITL timed reminder not agent', () => { assert.equal(result.layer, 'L1'); }); +test('parses daily reminder with time between set and remind words', () => { + const now = Date.UTC(2026, 7, 24, 9, 0, 0); // 2026-08-24 17:00 Asia/Shanghai + const intent = parseScheduleIntent('设置每天18:00提醒打卡', { + timezone: 'Asia/Shanghai', + now, + }); + assert.equal(intent.action, 'create_timed_reminder'); + assert.equal(intent.title, '打卡'); + assert.equal(intent.recurrence, 'daily'); + assert.equal(intent.hour, 18); + assert.equal(intent.minute, 0); +}); + +test('daily set-remind phrase routes to ITL timed reminder', () => { + const result = classifyUserIntent('设置每天18:00提醒打卡'); + assert.equal(result.kind, 'timed_reminder'); + assert.equal(result.detail.recurrence, 'daily'); +}); + 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 分开会,项目计划例会', { diff --git a/schedule-reminder-worker.mjs b/schedule-reminder-worker.mjs index 0911c10..76462cc 100644 --- a/schedule-reminder-worker.mjs +++ b/schedule-reminder-worker.mjs @@ -54,6 +54,7 @@ export function startScheduleReminderWorker({ status: 'success', }); await scheduleService.markReminderSent(reminder); + await scheduleService.scheduleNextDailyReminder?.(reminder); } catch (err) { logger.warn?.('Schedule reminder delivery failed:', err); await scheduleService.logDelivery({ diff --git a/schedule-service.mjs b/schedule-service.mjs index 4a806f1..bce74d2 100644 --- a/schedule-service.mjs +++ b/schedule-service.mjs @@ -29,6 +29,16 @@ function nowMs() { function rowToItem(row) { if (!row) return null; + let metadata = null; + if (row.metadata_json != null && row.metadata_json !== '') { + try { + metadata = typeof row.metadata_json === 'object' + ? row.metadata_json + : JSON.parse(row.metadata_json); + } catch { + metadata = null; + } + } return { id: row.id, userId: row.user_id, @@ -42,6 +52,7 @@ function rowToItem(row) { allDay: Boolean(row.all_day), timezone: row.timezone || DEFAULT_TIMEZONE, location: row.location ?? null, + metadata, createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), }; @@ -660,6 +671,28 @@ export function createScheduleService(pool, options = {}) { return { ...reminder, status: 'sent', sentAt: now, lockedUntil: null }; }; + const scheduleNextDailyReminder = async (reminder) => { + if (!reminder?.userId || !reminder?.itemId) return null; + const item = await getItem({ userId: reminder.userId, itemId: reminder.itemId }); + if (!item || item.status !== 'active' || item.metadata?.recurrence !== 'daily') return null; + const hour = Number(item.metadata.dailyHour); + const minute = Number(item.metadata.dailyMinute ?? 0); + if (!Number.isInteger(hour) || hour < 0 || hour > 23) return null; + if (!Number.isInteger(minute) || minute < 0 || minute > 59) return null; + const remindAt = nextDailyRunAt({ + hour, + minute, + timezone: item.timezone || defaultTimezone, + now: clock.now() + 1000, + }); + return createReminder({ + userId: reminder.userId, + itemId: item.id, + remindAt, + channel: reminder.channel ?? 'wechat', + }); + }; + const markReminderCancelled = async (reminder, reason, { now = clock.now() } = {}) => { const lastError = String(reason ?? '已取消').slice(0, 500); await pool.query( @@ -978,6 +1011,7 @@ export function createScheduleService(pool, options = {}) { listDueReminders, lockReminder, markReminderSent, + scheduleNextDailyReminder, markReminderCancelled, markReminderFailed, buildReminderText, diff --git a/wechat/handlers/intent-transaction.mjs b/wechat/handlers/intent-transaction.mjs index 435d778..40ec67a 100644 --- a/wechat/handlers/intent-transaction.mjs +++ b/wechat/handlers/intent-transaction.mjs @@ -26,6 +26,9 @@ function buildDraftPayload(classification, { intent, user, timezone }) { ...base, title: detail.title, remindLocal: detail.remindLocal, + hour: detail.hour, + minute: detail.minute, + recurrence: detail.recurrence ?? 'once', }; } if (kind === 'create_todo') {