import { summarizePersonalMemoryObservation } from '../memory-v2-user-feedback.mjs'; function assertRouter(api) { if ( !api || typeof api.get !== 'function' || typeof api.post !== 'function' || typeof api.delete !== 'function' ) { throw new Error( 'attachPortalUserMemoryRoutes requires an Express-compatible router', ); } } function parseRecallEventData(raw) { if (!raw) return null; if (typeof raw === 'object') return raw; try { return JSON.parse(String(raw)); } catch { return null; } } export async function resolveLatestMemoryRecallHint(pool, { userId, sessionId }) { if (!pool?.query || !userId || !sessionId) { return { memoryCount: 0, injectionEnabled: false, mode: null, createdAt: null, savedPreview: null, memoryPreviews: [], }; } const [rows] = await pool.query( `SELECT e.data_json, e.created_at, r.id AS run_id FROM h5_agent_run_events e INNER JOIN h5_agent_runs r ON r.id = e.run_id WHERE r.user_id = ? AND r.agent_session_id = ? AND e.event_type = 'agent_memory_resolved' ORDER BY e.created_at DESC LIMIT 1`, [String(userId), String(sessionId)], ); const [candidateRows] = await pool.query( `SELECT content, updated_at FROM h5_memory_v2_candidates WHERE user_id = ? AND session_id = ? AND status = 'accepted' ORDER BY updated_at DESC LIMIT 1`, [String(userId), String(sessionId)], ); const row = rows[0]; const candidate = candidateRows[0]; if (!row) { return { memoryCount: 0, injectionEnabled: false, mode: null, createdAt: null, runId: null, savedPreview: candidate?.content ? String(candidate.content).slice(0, 60) : null, savedAt: candidate?.updated_at == null ? null : Number(candidate.updated_at), memoryPreviews: [], }; } const data = parseRecallEventData(row.data_json); const memoryPreviews = Array.isArray(data?.memoryPreviews) ? data.memoryPreviews.map((item) => String(item).slice(0, 120)).filter(Boolean).slice(0, 5) : []; return { memoryCount: Number(data?.memoryCount ?? data?.count ?? 0) || 0, injectionEnabled: Boolean(data?.injectionEnabled), mode: data?.mode == null ? null : String(data.mode), createdAt: Number(row.created_at ?? 0) || null, runId: row.run_id == null ? null : String(row.run_id), savedPreview: candidate?.content ? String(candidate.content).slice(0, 60) : null, savedAt: candidate?.updated_at == null ? null : Number(candidate.updated_at), memoryPreviews, }; } export function attachPortalUserMemoryRoutes( api, { getMemoryV2 = () => null, getTkmindProxy = () => null, getPool = () => null, ensureUserMemoryCapability = async () => null, ownsAgentSession = async () => false, loadUserVisibleConversation = async () => [], syncUserMemoriesIntoSession = async () => false, resolveUserMemoryItems = async () => [], } = {}, ) { assertRouter(api); api.post('/user-memory/v1/remember-recent', async (req, res) => { const memoryV2 = getMemoryV2(); const memoryStatus = await memoryV2?.getStatus?.().catch(() => null); if (!memoryStatus?.enabled) { return res.status(503).json({ message: '长期记忆功能未启用' }); } if (!getTkmindProxy()) { return res.status(503).json({ message: '会话代理尚未就绪' }); } const capabilityState = await ensureUserMemoryCapability(req, res); if (!capabilityState) return; const sessionId = String(req.body?.sessionId ?? '').trim(); if (!sessionId) { return res.status(400).json({ message: '缺少 sessionId' }); } const owns = await ownsAgentSession(req.currentUser.id, sessionId); if (!owns) { return res.status(403).json({ message: '无权访问该会话' }); } try { const messages = await loadUserVisibleConversation( sessionId, req.currentUser.id, ); const result = await memoryV2.write({ userId: req.currentUser.id, sessionId, messages, }); const syncedToSession = await syncUserMemoriesIntoSession( req.currentUser.id, sessionId, ); const memories = await resolveUserMemoryItems(req.currentUser.id, { sessionId, limit: 200, }); const personalMemory = summarizePersonalMemoryObservation(result.personalMemory); return res.json({ ok: true, analyzed: result.analyzed ?? 0, memories: result.memories ?? 0, totalMemories: memories.length, syncedToSession, personalMemory, }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '保存长期记忆失败', }); } }); api.get('/user-memory/v1/recall-hint', async (req, res) => { const memoryV2 = getMemoryV2(); const memoryStatus = await memoryV2?.getStatus?.().catch(() => null); if (!memoryStatus?.enabled) { return res.status(503).json({ message: '长期记忆功能未启用' }); } const capabilityState = await ensureUserMemoryCapability(req, res); if (!capabilityState) return; const sessionId = String(req.query?.sessionId ?? '').trim(); if (!sessionId) { return res.status(400).json({ message: '缺少 sessionId' }); } const owns = await ownsAgentSession(req.currentUser.id, sessionId); if (!owns) { return res.status(403).json({ message: '无权访问该会话' }); } try { const hint = await resolveLatestMemoryRecallHint(getPool(), { userId: req.currentUser.id, sessionId, }); return res.json({ ok: true, ...hint }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '读取记忆召回提示失败', }); } }); api.post('/user-memory/v1/sync', async (req, res) => { const memoryV2 = getMemoryV2(); const memoryStatus = await memoryV2?.getStatus?.().catch(() => null); if (!memoryStatus?.enabled) { return res.status(503).json({ message: '长期记忆功能未启用' }); } const capabilityState = await ensureUserMemoryCapability(req, res); if (!capabilityState) return; const sessionId = String(req.body?.sessionId ?? '').trim(); if (!sessionId) { return res.status(400).json({ message: '缺少 sessionId' }); } const owns = await ownsAgentSession(req.currentUser.id, sessionId); if (!owns) { return res.status(403).json({ message: '无权访问该会话' }); } try { const result = await memoryV2.compact({ userId: req.currentUser.id, sessionId, }); const syncedToSession = await syncUserMemoriesIntoSession( req.currentUser.id, sessionId, ); const memories = await resolveUserMemoryItems(req.currentUser.id, { sessionId, limit: 200, }); return res.json({ ok: true, analyzed: result.analyzed ?? 0, memories: result.memories ?? 0, totalMemories: memories.length, syncedToSession, }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '刷新长期记忆失败', }); } }); api.get('/user-memory/v1/items', async (req, res) => { const capabilityState = await ensureUserMemoryCapability(req, res); if (!capabilityState) return; try { const memoryV2 = getMemoryV2(); const items = (await memoryV2.listMemories?.({ userId: req.currentUser.id, status: String(req.query?.status ?? 'active'), limit: req.query?.limit, offset: req.query?.offset, })) ?? []; return res.json({ ok: true, items }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '读取长期记忆失败', }); } }); api.delete('/user-memory/v1/items/:memoryId', async (req, res) => { const capabilityState = await ensureUserMemoryCapability(req, res); if (!capabilityState) return; try { const memoryV2 = getMemoryV2(); const result = (await memoryV2.forgetMemory?.({ userId: req.currentUser.id, memoryId: req.params.memoryId, })) ?? { ok: false, skipped: true, reason: 'unavailable' }; if (result.skipped) return res.status(409).json(result); return res.json(result); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '删除长期记忆失败', }); } }); }