2026-09-04 13:49:40 +08:00
|
|
|
|
import { parseTimeScope } from '../temporal-recall-service/time-parser.mjs';
|
|
|
|
|
|
|
|
|
|
|
|
export const RAIN_DEFAULT_DAYS = 3;
|
|
|
|
|
|
|
|
|
|
|
|
const TZ_OFFSET_MIN = Number(process.env.TEMPORAL_RECALL_TZ_OFFSET_MIN ?? 480);
|
|
|
|
|
|
|
|
|
|
|
|
/** @param {Date} anchor @param {number} deltaDays */
|
|
|
|
|
|
function addDays(anchor, deltaDays) {
|
|
|
|
|
|
const shifted = new Date(anchor.getTime() + TZ_OFFSET_MIN * 60_000);
|
|
|
|
|
|
const ms =
|
|
|
|
|
|
Date.UTC(
|
|
|
|
|
|
shifted.getUTCFullYear(),
|
|
|
|
|
|
shifted.getUTCMonth(),
|
|
|
|
|
|
shifted.getUTCDate() + deltaDays,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
) - TZ_OFFSET_MIN * 60_000;
|
|
|
|
|
|
return new Date(ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 16:54:11 +08:00
|
|
|
|
/** @param {Date} anchor */
|
|
|
|
|
|
function startOfDay(anchor) {
|
|
|
|
|
|
const p = new Date(anchor.getTime() + TZ_OFFSET_MIN * 60_000);
|
|
|
|
|
|
const ms =
|
|
|
|
|
|
Date.UTC(p.getUTCFullYear(), p.getUTCMonth(), p.getUTCDate(), 0, 0, 0, 0) -
|
|
|
|
|
|
TZ_OFFSET_MIN * 60_000;
|
|
|
|
|
|
return new Date(ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 13:49:40 +08:00
|
|
|
|
/** @param {Date} anchor */
|
|
|
|
|
|
function endOfDay(anchor) {
|
|
|
|
|
|
const p = new Date(anchor.getTime() + TZ_OFFSET_MIN * 60_000);
|
|
|
|
|
|
const ms =
|
|
|
|
|
|
Date.UTC(p.getUTCFullYear(), p.getUTCMonth(), p.getUTCDate() + 1, 0, 0, 0, 0) -
|
|
|
|
|
|
TZ_OFFSET_MIN * 60_000;
|
|
|
|
|
|
return new Date(ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 16:54:11 +08:00
|
|
|
|
/** @param {Date} now */
|
|
|
|
|
|
function buildDefault3dRange(now) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
start: addDays(now, -RAIN_DEFAULT_DAYS).toISOString(),
|
|
|
|
|
|
end: endOfDay(now).toISOString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const RAIN_TIME_PRESET_IDS = ['today', 'yesterday', '3d', '7d'];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param {string} presetId
|
|
|
|
|
|
* @param {Date} [now]
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function resolveRainTimeRangeFromPreset(presetId, now = new Date()) {
|
|
|
|
|
|
const id = String(presetId ?? '3d').trim();
|
|
|
|
|
|
if (id === 'today') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
range: { start: startOfDay(now).toISOString(), end: endOfDay(now).toISOString() },
|
|
|
|
|
|
source: 'ui_preset',
|
|
|
|
|
|
label: '今天',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
if (id === 'yesterday') {
|
|
|
|
|
|
const day = addDays(now, -1);
|
|
|
|
|
|
return {
|
|
|
|
|
|
range: { start: startOfDay(day).toISOString(), end: endOfDay(day).toISOString() },
|
|
|
|
|
|
source: 'ui_preset',
|
|
|
|
|
|
label: '昨天',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
if (id === '7d') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
range: { start: addDays(now, -7).toISOString(), end: endOfDay(now).toISOString() },
|
|
|
|
|
|
source: 'ui_preset',
|
|
|
|
|
|
label: '近7天',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
range: buildDefault3dRange(now),
|
|
|
|
|
|
source: 'ui_preset',
|
|
|
|
|
|
label: `近${RAIN_DEFAULT_DAYS}天`,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Only truly unknowable time phrases trigger clarification; others fall back to default 3d. */
|
|
|
|
|
|
const STRICT_AMBIGUOUS_TIME_PATTERNS = [
|
2026-09-04 13:49:40 +08:00
|
|
|
|
/不太确定.{0,8}时间/u,
|
2026-09-07 16:54:11 +08:00
|
|
|
|
/说不清.{0,6}(?:哪|什么).{0,4}(?:天|时间)/u,
|
2026-09-04 13:49:40 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const EXPLICIT_TIME_HINT =
|
|
|
|
|
|
/昨天|昨日|今天|今日|明天|明日|前天|后天|上周|这周|本周|这个月|本月|上个月|\d{1,2}\s*月|\d{1,2}\s*[::]\d{2}|最近\s*\d+\s*天|最近一周|最近1周|\d{4}-\d{2}-\d{2}/u;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @param {string} query
|
|
|
|
|
|
* @param {Date} [now]
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function resolveRainTimeRange(query, now = new Date()) {
|
|
|
|
|
|
const text = String(query ?? '').trim();
|
|
|
|
|
|
|
2026-09-07 16:54:11 +08:00
|
|
|
|
for (const pattern of STRICT_AMBIGUOUS_TIME_PATTERNS) {
|
2026-09-04 13:49:40 +08:00
|
|
|
|
if (pattern.test(text)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
needsClarification: true,
|
|
|
|
|
|
clarificationQuestion:
|
|
|
|
|
|
'请说明要分析 MeInput 输入记录的起止时间(例如「9月2日 18:00 到 9月4日 10:00」,或「昨天全天」)。若你不补充,我将默认使用近 3 天。',
|
|
|
|
|
|
range: null,
|
|
|
|
|
|
source: 'ambiguous',
|
|
|
|
|
|
label: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!EXPLICIT_TIME_HINT.test(text)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
needsClarification: false,
|
2026-09-07 16:54:11 +08:00
|
|
|
|
range: buildDefault3dRange(now),
|
2026-09-04 13:49:40 +08:00
|
|
|
|
source: 'default_3d',
|
|
|
|
|
|
label: `近${RAIN_DEFAULT_DAYS}天`,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scope = parseTimeScope(text, now);
|
|
|
|
|
|
const isDefaultFallback =
|
|
|
|
|
|
scope.rule_hits?.includes('time:default_week') ||
|
|
|
|
|
|
scope.rule_hits?.includes('time:recent_fuzzy');
|
|
|
|
|
|
|
2026-09-07 16:54:11 +08:00
|
|
|
|
if (isDefaultFallback && /最近|近期|这几天|前几天|那段(?:时间|日子)/u.test(text) && !/最近\s*\d+\s*天/u.test(text)) {
|
2026-09-04 13:49:40 +08:00
|
|
|
|
return {
|
|
|
|
|
|
needsClarification: false,
|
2026-09-07 16:54:11 +08:00
|
|
|
|
range: buildDefault3dRange(now),
|
2026-09-04 13:49:40 +08:00
|
|
|
|
source: 'default_3d',
|
|
|
|
|
|
label: `近${RAIN_DEFAULT_DAYS}天`,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
needsClarification: false,
|
|
|
|
|
|
range: scope.mention_range,
|
|
|
|
|
|
source: 'user_explicit',
|
|
|
|
|
|
label: scope.relative_label,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function formatRainTimeRangeLabel(range, meta = {}) {
|
|
|
|
|
|
if (!range?.start || !range?.end) return meta.label ?? '未指定';
|
|
|
|
|
|
const start = range.start.slice(0, 16).replace('T', ' ');
|
|
|
|
|
|
const end = range.end.slice(0, 16).replace('T', ' ');
|
|
|
|
|
|
const suffix = meta.source === 'default_3d' ? '(默认近3天)' : '';
|
|
|
|
|
|
return `${start} ~ ${end}${suffix}`;
|
|
|
|
|
|
}
|