Files
Kokorone/apps/api/src/task/task-parser.ts
T
JefferyandClaude Sonnet 5 3503cc0be6 feat: 完成 J 群組 — 委託子系統
Task 資料表與自然語句解析(單次提醒)、依原型的接案/觸發/逾期追擊演出、
委託與關係帳本/情緒的回饋迴路(含默契旗標)、主動關照(待辦偵測+作息交叉比對,
稀有度節流不重複台詞)、排程器可靠性(服務重啟後重新掃描 PENDING 任務掛回
JobQueue,逾期立即補觸發)。J.mjs 以 spawnSync 實際重啟服務驗證 J-7。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:35:16 +08:00

113 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}