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',
|
sourceChannel: payload.sourceChannel ?? 'wechat',
|
||||||
sourceMessageId: payload.sourceMessageId ?? draft.sourceMessageId,
|
sourceMessageId: payload.sourceMessageId ?? draft.sourceMessageId,
|
||||||
sourceText: payload.sourceText ?? draft.sourceText,
|
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({
|
const reminder = await scheduleService.createReminder({
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
+10
-1
@@ -85,7 +85,13 @@ function countTimeExpressions(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wantsSimpleTimedReminder(compact, 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 (countTimeExpressions(text) !== 1) return false;
|
||||||
if (/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:).{1,24}[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)/u.test(compact)) {
|
if (/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:).{1,24}[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)/u.test(compact)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -110,7 +116,9 @@ function cleanupTimedReminderTitleFragment(text) {
|
|||||||
.replace(/^(?:帮我|请|麻烦)?(?:设置|设|创建|添加)(?:一个|个)?提醒[,,、::\s]*/u, '')
|
.replace(/^(?:帮我|请|麻烦)?(?:设置|设|创建|添加)(?:一个|个)?提醒[,,、::\s]*/u, '')
|
||||||
.replace(/^(?:帮我|请)?(?:设置|设)(?:一个|个)?(?:待办|代办|带办|代拜)[,,、::\s]*/u, '');
|
.replace(/^(?:帮我|请)?(?:设置|设)(?:一个|个)?(?:待办|代办|带办|代拜)[,,、::\s]*/u, '');
|
||||||
title = title
|
title = title
|
||||||
|
.replace(/^(?:帮我|请|麻烦)?(?:设置|设)(?:一个|个)?(?:每天|每日|天天)?/u, '')
|
||||||
.replace(/(?:今天|今日|今晚|明天|后天|明早|今早)/gu, ' ')
|
.replace(/(?:今天|今日|今晚|明天|后天|明早|今早)/gu, ' ')
|
||||||
|
.replace(/(?:每天|每日|天天)/gu, ' ')
|
||||||
.replace(/(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)/gu, ' ')
|
.replace(/(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)/gu, ' ')
|
||||||
.replace(
|
.replace(
|
||||||
/[0-9零一二两三四五六七八九十]{1,3}(?:点|:|:)(?:半|[0-9]{1,2}(?:\s*分(?!开)|(?![0-9]))?)?/gu,
|
/[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,
|
hour: time.hour,
|
||||||
minute: time.minute,
|
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');
|
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', () => {
|
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 now = Date.UTC(2026, 7, 24, 4, 0, 0); // 2026-08-24 12:00 Asia/Shanghai
|
||||||
const intent = parseScheduleIntent('帮我设置提醒,下午 14:30 分开会,项目计划例会', {
|
const intent = parseScheduleIntent('帮我设置提醒,下午 14:30 分开会,项目计划例会', {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function startScheduleReminderWorker({
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
});
|
});
|
||||||
await scheduleService.markReminderSent(reminder);
|
await scheduleService.markReminderSent(reminder);
|
||||||
|
await scheduleService.scheduleNextDailyReminder?.(reminder);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn?.('Schedule reminder delivery failed:', err);
|
logger.warn?.('Schedule reminder delivery failed:', err);
|
||||||
await scheduleService.logDelivery({
|
await scheduleService.logDelivery({
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ function nowMs() {
|
|||||||
|
|
||||||
function rowToItem(row) {
|
function rowToItem(row) {
|
||||||
if (!row) return null;
|
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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
userId: row.user_id,
|
userId: row.user_id,
|
||||||
@@ -42,6 +52,7 @@ function rowToItem(row) {
|
|||||||
allDay: Boolean(row.all_day),
|
allDay: Boolean(row.all_day),
|
||||||
timezone: row.timezone || DEFAULT_TIMEZONE,
|
timezone: row.timezone || DEFAULT_TIMEZONE,
|
||||||
location: row.location ?? null,
|
location: row.location ?? null,
|
||||||
|
metadata,
|
||||||
createdAt: Number(row.created_at),
|
createdAt: Number(row.created_at),
|
||||||
updatedAt: Number(row.updated_at),
|
updatedAt: Number(row.updated_at),
|
||||||
};
|
};
|
||||||
@@ -660,6 +671,28 @@ export function createScheduleService(pool, options = {}) {
|
|||||||
return { ...reminder, status: 'sent', sentAt: now, lockedUntil: null };
|
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 markReminderCancelled = async (reminder, reason, { now = clock.now() } = {}) => {
|
||||||
const lastError = String(reason ?? '已取消').slice(0, 500);
|
const lastError = String(reason ?? '已取消').slice(0, 500);
|
||||||
await pool.query(
|
await pool.query(
|
||||||
@@ -978,6 +1011,7 @@ export function createScheduleService(pool, options = {}) {
|
|||||||
listDueReminders,
|
listDueReminders,
|
||||||
lockReminder,
|
lockReminder,
|
||||||
markReminderSent,
|
markReminderSent,
|
||||||
|
scheduleNextDailyReminder,
|
||||||
markReminderCancelled,
|
markReminderCancelled,
|
||||||
markReminderFailed,
|
markReminderFailed,
|
||||||
buildReminderText,
|
buildReminderText,
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ function buildDraftPayload(classification, { intent, user, timezone }) {
|
|||||||
...base,
|
...base,
|
||||||
title: detail.title,
|
title: detail.title,
|
||||||
remindLocal: detail.remindLocal,
|
remindLocal: detail.remindLocal,
|
||||||
|
hour: detail.hour,
|
||||||
|
minute: detail.minute,
|
||||||
|
recurrence: detail.recurrence ?? 'once',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (kind === 'create_todo') {
|
if (kind === 'create_todo') {
|
||||||
|
|||||||
Reference in New Issue
Block a user