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