fix(schedule): route daily set-remind phrases through ITL with recurrence
Memind CI / Test, build, and release guards (push) Failing after 3m4s
Memind CI / Test, build, and release guards (push) Failing after 3m4s
Support colloquial forms like「设置每天18:00提醒打卡」, persist daily metadata on commit, and schedule the next reminder after each delivery. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
+10
-1
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 分开会,项目计划例会', {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user