212ff3ff80
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>
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import {
|
|
expandObservedFetchRange,
|
|
extractEventTime,
|
|
itemMatchesTimeWindow,
|
|
} from '../event-time-extract.mjs';
|
|
|
|
function newTimelineId(sourceRef) {
|
|
const hash = crypto.createHash('sha256').update(`timeline-v1|${sourceRef}`).digest('hex');
|
|
return [
|
|
hash.slice(0, 8),
|
|
hash.slice(8, 12),
|
|
`4${hash.slice(13, 16)}`,
|
|
hash.slice(16, 20),
|
|
hash.slice(20, 32),
|
|
].join('-');
|
|
}
|
|
|
|
function parseUserMessage(row) {
|
|
try {
|
|
return typeof row.user_message_json === 'string'
|
|
? JSON.parse(row.user_message_json)
|
|
: row.user_message_json;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function extractText(message) {
|
|
if (!message) return '';
|
|
if (typeof message === 'string') return message.trim();
|
|
if (typeof message.content === 'string') return message.content.trim();
|
|
if (Array.isArray(message.content)) {
|
|
return message.content
|
|
.map((part) => (typeof part === 'string' ? part : part?.text ?? ''))
|
|
.join('')
|
|
.trim();
|
|
}
|
|
if (typeof message.text === 'string') return message.text.trim();
|
|
return '';
|
|
}
|
|
|
|
function scoreTextImportance(text, expandedQueries = []) {
|
|
let score = 0.5;
|
|
const t = String(text ?? '');
|
|
if (/重要|紧急|安排|会议|待办|记得|跟进|截止|确认/.test(t)) score += 0.18;
|
|
for (const q of expandedQueries) {
|
|
if (q && t.includes(q)) score += 0.05;
|
|
}
|
|
return Math.min(0.95, score);
|
|
}
|
|
|
|
function matchesExpanded(text, expandedQueries) {
|
|
if (!expandedQueries?.length) return true;
|
|
return expandedQueries.some((q) => q && text.includes(q));
|
|
}
|
|
|
|
/**
|
|
* @param {import('mysql2/promise').Pool} pool
|
|
* @param {{ userId: string, retrieval: object, time: object, sessionId?: string }} ctx
|
|
*/
|
|
export async function searchChat(pool, ctx) {
|
|
if (!pool?.query) return [];
|
|
|
|
let sql = `
|
|
SELECT id, user_message_json, created_at, agent_session_id
|
|
FROM h5_agent_runs
|
|
WHERE user_id = ? AND created_at >= ? AND created_at < ?`;
|
|
const fetchRange = expandObservedFetchRange(ctx.time, ctx.temporalMode);
|
|
const params = [ctx.userId, new Date(fetchRange.start).getTime(), new Date(fetchRange.end).getTime()];
|
|
if (ctx.sessionId) {
|
|
sql += ' AND agent_session_id = ?';
|
|
params.push(ctx.sessionId);
|
|
}
|
|
sql += ' ORDER BY created_at ASC LIMIT 300';
|
|
|
|
const [rows] = await pool.query(sql, params);
|
|
const items = [];
|
|
for (const row of rows) {
|
|
const message = parseUserMessage(row);
|
|
const text = extractText(message);
|
|
if (text.length < 2) continue;
|
|
if (!matchesExpanded(text, ctx.retrieval.expanded_queries) && text.length < 12) continue;
|
|
const observed = new Date(Number(row.created_at)).toISOString();
|
|
const extracted = extractEventTime(text, observed);
|
|
const sourceRef = `chat:run:${row.id}`;
|
|
const item = {
|
|
timeline_item_id: newTimelineId(sourceRef),
|
|
user_id: ctx.userId,
|
|
source: 'chat',
|
|
type: /待办|记得|别忘了|跟进|截止|安排/.test(text) ? 'commitment' : 'mention',
|
|
event_time: extracted.event_time,
|
|
observed_time: observed,
|
|
title: text.slice(0, 80),
|
|
content: text.slice(0, 8192),
|
|
importance: scoreTextImportance(text, ctx.retrieval.expanded_queries),
|
|
confidence: extracted.event_time ? extracted.confidence : 0.88,
|
|
source_ref: sourceRef,
|
|
participants: [],
|
|
status: extracted.status === 'planned' ? 'planned' : 'mentioned',
|
|
metadata: { agent_session_id: row.agent_session_id },
|
|
};
|
|
if (!itemMatchesTimeWindow(item, ctx.time, ctx.temporalMode)) continue;
|
|
items.push(item);
|
|
}
|
|
return items;
|
|
}
|