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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user