const TZ_OFFSET_MIN = Number(process.env.TEMPORAL_RECALL_TZ_OFFSET_MIN ?? 480); function pad2(n) { return String(n).padStart(2, '0'); } /** @param {Date} d */ function toLocalParts(d) { const shifted = new Date(d.getTime() + TZ_OFFSET_MIN * 60_000); return { year: shifted.getUTCFullYear(), month: shifted.getUTCMonth(), day: shifted.getUTCDate(), dow: shifted.getUTCDay(), hour: shifted.getUTCHours(), }; } /** @param {{ year: number, month: number, day: number }} p @param {number} h @param {number} m */ function partsToIso(p, h = 0, m = 0) { const utcMs = Date.UTC(p.year, p.month, p.day, h, m, 0, 0) - TZ_OFFSET_MIN * 60_000; return new Date(utcMs).toISOString(); } /** @param {Date} anchor */ function startOfDay(anchor) { const p = toLocalParts(anchor); return new Date(partsToIso(p, 0, 0)); } /** @param {Date} anchor */ function endOfDay(anchor) { const p = toLocalParts(anchor); const start = Date.UTC(p.year, p.month, p.day + 1, 0, 0, 0, 0) - TZ_OFFSET_MIN * 60_000; return new Date(start); } /** @param {Date} anchor @param {number} deltaDays */ function addDays(anchor, deltaDays) { const p = toLocalParts(anchor); const ms = Date.UTC(p.year, p.month, p.day + deltaDays, 0, 0, 0, 0) - TZ_OFFSET_MIN * 60_000; return new Date(ms); } /** @param {Date} anchor */ function startOfWeek(anchor) { const p = toLocalParts(anchor); const mondayOffset = p.dow === 0 ? -6 : 1 - p.dow; const ms = Date.UTC(p.year, p.month, p.day + mondayOffset, 0, 0, 0, 0) - TZ_OFFSET_MIN * 60_000; return new Date(ms); } /** @param {Date} anchor */ function endOfWeek(anchor) { const start = startOfWeek(anchor); return addDays(start, 7); } /** @param {Date} anchor */ function startOfMonth(anchor) { const p = toLocalParts(anchor); return new Date(Date.UTC(p.year, p.month, 1, 0, 0, 0, 0) - TZ_OFFSET_MIN * 60_000); } /** @param {Date} anchor */ function endOfMonth(anchor) { const p = toLocalParts(anchor); return new Date(Date.UTC(p.year, p.month + 1, 1, 0, 0, 0, 0) - TZ_OFFSET_MIN * 60_000); } /** * @param {string} query * @param {Date} [now] * @returns {{ mention_range: { start: string, end: string }, event_range: { start: string, end: string } | null, relative_label: string | null, rule_hits: string[] }} */ export function parseTimeScope(query, now = new Date()) { const text = String(query ?? ''); const rule_hits = []; let relative_label = null; let rangeStart = null; let rangeEnd = null; const patterns = [ { re: /前天/, label: 'day_before_yesterday', apply: () => { const d = addDays(now, -2); return [startOfDay(d), endOfDay(d)]; }}, { re: /昨天|昨日/, label: 'yesterday', apply: () => { const d = addDays(now, -1); return [startOfDay(d), endOfDay(d)]; }}, { re: /今天|今日/, label: 'today', apply: () => [startOfDay(now), endOfDay(now)] }, { re: /明天|明日/, label: 'tomorrow', apply: () => { const d = addDays(now, 1); return [startOfDay(d), endOfDay(d)]; }}, { re: /后天/, label: 'day_after_tomorrow', apply: () => { const d = addDays(now, 2); return [startOfDay(d), endOfDay(d)]; }}, { re: /上周|上星期|上个星期/, label: 'last_week', apply: () => { const thisWeek = startOfWeek(now); const lastStart = addDays(thisWeek, -7); return [lastStart, thisWeek]; }}, { re: /这周|本周|这个星期|这星期/, label: 'this_week', apply: () => [startOfWeek(now), endOfWeek(now)] }, { re: /上个月/, label: 'last_month', apply: () => { const thisMonth = startOfMonth(now); const p = toLocalParts(thisMonth); const lastStart = new Date(Date.UTC(p.year, p.month - 1, 1, 0, 0, 0, 0) - TZ_OFFSET_MIN * 60_000); return [lastStart, thisMonth]; }}, { re: /这个月|本月/, label: 'this_month', apply: () => [startOfMonth(now), endOfMonth(now)] }, { re: /最近(\d+)天/, label: 'recent_days', apply: (m) => { const days = Number(m[1]); return [addDays(now, -days), endOfDay(now)]; }}, { re: /最近一周|最近1周/, label: 'recent_week', apply: () => [addDays(now, -7), endOfDay(now)] }, ]; for (const { re, label, apply } of patterns) { const m = text.match(re); if (m) { [rangeStart, rangeEnd] = apply(m); relative_label = label; rule_hits.push(`time:${label}`); break; } } if (!rangeStart) { if (/最近|近期|这几天/.test(text)) { [rangeStart, rangeEnd] = [addDays(now, -7), endOfDay(now)]; relative_label = 'recent_week'; rule_hits.push('time:recent_fuzzy'); } else { [rangeStart, rangeEnd] = [addDays(now, -7), endOfDay(now)]; relative_label = 'this_week'; rule_hits.push('time:default_week'); } } const mention_range = { start: rangeStart.toISOString(), end: rangeEnd.toISOString(), }; let event_range = null; if (/安排|行程|会议|约会|几点|日历/.test(text)) { event_range = { ...mention_range }; rule_hits.push('time:event_range_linked'); } return { mention_range, event_range, relative_label, rule_hits }; }