Files
memind/temporal-recall-service/time-parser.mjs
T
john 212ff3ff80 Add User Model Service and Temporal Recall for MeMind V0.1.
Introduce UMS ingest/snapshot pipeline, Context Planner with multi-source recall, runtime context injection, canonical user mapping, and session snapshot loading on auth/me.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 23:25:18 +08:00

159 lines
5.0 KiB
JavaScript

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 };
}