import { taipeiParts } from "../schedule/time-utils.js"; export interface ParsedReminderRequest { content: string; // 去除時間片語與「提醒我」之後的委託內容 triggerAt: Date; } // J-1 自然語句解析:中文數字/阿拉伯數字時刻皆可辨識。 const CHINESE_HOUR_WORDS: [string, number][] = [ ["十二", 12], ["十一", 11], ["十", 10], ["九", 9], ["八", 8], ["七", 7], ["六", 6], ["五", 5], ["四", 4], ["三", 3], ["兩", 2], ["二", 2], ["一", 1], ]; function parseHour(segment: string): number | null { const arabic = segment.match(/(\d{1,2})\s*[點点:時]/); if (arabic) { return Number(arabic[1]); } for (const [word, value] of CHINESE_HOUR_WORDS) { if (segment.includes(`${word}點`) || segment.includes(`${word}点`)) { return value; } } return null; } function parseMinute(segment: string): number { if (segment.includes("半")) { return 30; } const explicit = segment.match(/[點点](\d{1,2})分/); if (explicit) { return Number(explicit[1]); } const colon = segment.match(/:(\d{1,2})/); if (colon) { return Number(colon[1]); } return 0; } // 依 24 小時制/上下午標記/模糊小時(1~12,取最接近的未來時刻)解析出候選小時(依時間先後排序)。 function resolveHourCandidates(segment: string, hour: number): number[] { const isPM = /下午|晚上|傍晚|中午(?!.*上午)/.test(segment) && hour !== 12; const isAM = /早上|上午|凌晨/.test(segment); if (isPM) { return [hour === 12 ? 12 : hour + 12]; } if (isAM) { return [hour === 12 ? 0 : hour]; } if (hour >= 13 || hour === 0) { return [hour]; } if (hour === 12) { return [12, 0]; } return [hour, hour + 12]; } function buildTaipeiDateTime(now: Date, hour: number, minute: number): Date { const { year, month, day } = taipeiParts(now); // Asia/Taipei 為 UTC+8:當地 HH:MM 對應 UTC (HH-8):MM(JS Date.UTC 會自動處理跨日進位/退位)。 return new Date(Date.UTC(year, month - 1, day, hour - 8, minute, 0, 0)); } function nearestFutureOccurrence(now: Date, candidateHours: number[], minute: number): Date { for (const hour of candidateHours) { const candidate = buildTaipeiDateTime(now, hour, minute); if (candidate.getTime() > now.getTime()) { return candidate; } } const fallback = buildTaipeiDateTime(now, candidateHours[0], minute); return new Date(fallback.getTime() + 24 * 60 * 60 * 1000); } // 解析「單次提醒」型委託,例如「三點提醒我開會」「晚上八點半提醒我倒垃圾」。 // 無法辨識時間片語時回傳 null(呼叫端可視為一般對話,不建立任務)。 export function parseReminderRequest(text: string, now: Date = new Date()): ParsedReminderRequest | null { const match = text.match(/^(.*?)提醒我(.+)$/); if (!match) { return null; } const [, timeSegment, rawContent] = match; const hour = parseHour(timeSegment); if (hour === null) { return null; } const minute = parseMinute(timeSegment); const candidates = resolveHourCandidates(timeSegment, hour); const triggerAt = nearestFutureOccurrence(now, candidates, minute); const content = rawContent.trim().replace(/^[,,、]/, "").trim(); if (!content) { return null; } return { content, triggerAt }; }