import crypto from 'node:crypto'; import { expandObservedFetchRange, extractEventTime, itemMatchesTimeWindow, } from '../event-time-extract.mjs'; import mysql from 'mysql2/promise'; let meinputPool = null; function getMeinputPool() { const url = process.env.MEINPUT_DATABASE_URL ?? ''; if (!url) return null; if (!meinputPool) { meinputPool = mysql.createPool({ uri: url, waitForConnections: true, connectionLimit: 4 }); } return meinputPool; } 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 scoreTextImportance(text, expandedQueries = []) { let score = 0.45; const t = String(text ?? ''); if (t.length >= 20) score += 0.08; if (/重要|紧急|必须|截止|会议|安排|明天|今天|项目/.test(t)) score += 0.15; for (const q of expandedQueries) { if (q && t.includes(q)) score += 0.05; } return Math.min(0.95, score); } function envelopeToTimelineItem(envelope, expandedQueries, temporalMode, time) { const payload = envelope.payload ?? {}; const text = String(payload.text ?? '').trim(); if (!text) return null; const observed = envelope.occurred_at; const extracted = extractEventTime(text, observed); const appName = payload.context?.app ?? null; const appBundle = payload.context?.app_bundle_id ?? null; const sourceRef = `meinput:segment:${envelope.evidence_id}`; const item = { timeline_item_id: newTimelineId(sourceRef), user_id: envelope.user_id, source: 'meinput', type: /待办|记得|别忘了|跟进|截止/.test(text) ? 'todo' : 'mention', event_time: extracted.event_time, observed_time: observed, title: text.slice(0, 80), content: text.slice(0, 8192), importance: scoreTextImportance(text, expandedQueries), confidence: extracted.event_time ? extracted.confidence : 0.92, source_ref: sourceRef, participants: [], status: extracted.status === 'planned' ? 'planned' : 'mentioned', app_name: appName, app_bundle_id: appBundle, }; if (time && !itemMatchesTimeWindow(item, time, temporalMode)) return null; return item; } async function fetchViaHttp(ctx) { const base = process.env.MEINPUT_BASE_URL ?? 'https://input.tkmind.cn'; const username = process.env.MEINPUT_USERNAME ?? 'admin'; const password = process.env.MEINPUT_PASSWORD ?? ''; if (!password) return []; const loginRes = await fetch(`${base}/v1/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username, password }), }); const login = await loginRes.json(); if (!loginRes.ok || login.user_id !== ctx.userId) { return []; } const url = new URL(`${base}/v1/evidence/export`); url.searchParams.set('limit', '200'); url.searchParams.set('since', ctx.time.mention_range.start); url.searchParams.set('until', ctx.time.mention_range.end); const res = await fetch(url, { headers: { authorization: `Bearer ${login.access_token}` } }); const data = await res.json(); if (!res.ok) return []; return (data.items ?? []) .map((item) => envelopeToTimelineItem( item, ctx.retrieval.expanded_queries, ctx.temporalMode, ctx.time, ), ) .filter(Boolean); } async function fetchViaDb(ctx) { const pool = getMeinputPool(); if (!pool) return null; const fetchRange = expandObservedFetchRange(ctx.time, ctx.temporalMode); const [rows] = await pool.query( `SELECT event_id, text, app_name, app_bundle_id, created_at FROM mi_input_events WHERE user_id = ? AND privacy_level = 'normal' AND created_at >= ? AND created_at < ? ORDER BY created_at ASC LIMIT 800`, [ ctx.userId, fetchRange.start.slice(0, 23).replace('T', ' '), fetchRange.end.slice(0, 23).replace('T', ' '), ], ); return rows .map((row) => { const observed = row.created_at instanceof Date ? row.created_at.toISOString() : new Date(row.created_at).toISOString(); const envelope = { evidence_id: row.event_id, user_id: ctx.userId, occurred_at: observed, payload: { text: row.text, context: { app: row.app_name, app_bundle_id: row.app_bundle_id }, }, }; return envelopeToTimelineItem( envelope, ctx.retrieval.expanded_queries, ctx.temporalMode, ctx.time, ); }) .filter(Boolean); } /** * Rain 模式:按精确时间区间全量拉取 MeInput 原始记录(不过滤、不排序)。 * @param {{ userId: string, range: { start: string, end: string } }} input */ export async function fetchMeinputRangeFull(input) { const userId = String(input.userId ?? '').trim(); const start = String(input.range?.start ?? '').trim(); const end = String(input.range?.end ?? '').trim(); if (!userId || !start || !end) return []; const pool = getMeinputPool(); if (pool) { const [rows] = await pool.query( `SELECT event_id, text, app_name, app_bundle_id, created_at FROM mi_input_events WHERE user_id = ? AND privacy_level = 'normal' AND created_at >= ? AND created_at < ? ORDER BY created_at ASC`, [userId, start.slice(0, 23).replace('T', ' '), end.slice(0, 23).replace('T', ' ')], ); return rows.map((row) => ({ event_id: row.event_id, text: row.text, app_name: row.app_name, app_bundle_id: row.app_bundle_id, created_at: row.created_at instanceof Date ? row.created_at.toISOString() : new Date(row.created_at).toISOString(), })); } const base = process.env.MEINPUT_BASE_URL ?? 'https://input.tkmind.cn'; const username = process.env.MEINPUT_USERNAME ?? 'admin'; const password = process.env.MEINPUT_PASSWORD ?? ''; if (!password) return []; const loginRes = await fetch(`${base}/v1/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username, password }), }); const login = await loginRes.json(); if (!loginRes.ok || login.user_id !== userId) return []; const url = new URL(`${base}/v1/evidence/export`); url.searchParams.set('limit', '10000'); url.searchParams.set('since', start); url.searchParams.set('until', end); const res = await fetch(url, { headers: { authorization: `Bearer ${login.access_token}` } }); const data = await res.json(); if (!res.ok) return []; return (data.items ?? []).map((item) => ({ event_id: item.evidence_id ?? item.event_id, text: item.payload?.text ?? item.text ?? '', app_name: item.payload?.context?.app ?? item.app_name ?? null, app_bundle_id: item.payload?.context?.app_bundle_id ?? item.app_bundle_id ?? null, created_at: item.occurred_at ?? item.created_at, })); } /** * @param {{ userId: string, retrieval: object, time: object, temporalMode: string }} ctx */ export async function searchMeinput(ctx) { const dbItems = await fetchViaDb(ctx); if (dbItems !== null) return dbItems; return fetchViaHttp(ctx); }