// ============================================================ // useSessionList — 会话历史列表管理(后端 API 驱动) // 职责:会话 CRUD、消息加载、切换会话 // 数据源:后端 /api/conversations REST API // ============================================================ import { useCallback, useEffect, useRef, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import { listConversations, createConversation, deleteConversation, renameConversation, getConversationMessages, type ConversationListItem, } from "../lib/api"; import type { ChatMessage, SessionSummary } from "../types"; const LAST_ACTIVE_KEY = "camtalk:last_active_session"; /** 截取预览文本 */ function getPreview(text: string, maxLen = 50): string { const clean = text.replace(/[\n\r]/g, " ").trim(); return clean.length > maxLen ? clean.slice(0, maxLen) + "…" : clean; } /** 将后端 ConversationListItem 转为前端 SessionSummary */ function toSessionSummary(item: ConversationListItem): SessionSummary { return { id: item.id, title: item.title || "新对话", createdAt: new Date(item.created_at).getTime(), lastActiveAt: new Date(item.updated_at).getTime(), messageCount: item.message_count, preview: getPreview(item.last_message || ""), }; } /** 将后端 StoredMessage 转为前端 ChatMessage */ function toChatMessage(msg: { id: number; role: string; content: string; tokens_used: number; created_at: string; }): ChatMessage { return { id: String(msg.id), role: msg.role as ChatMessage["role"], content: msg.content, timestamp: new Date(msg.created_at).getTime(), tokensUsed: msg.tokens_used || undefined, }; } export function useSessionList(accessToken?: string | null) { const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState( () => localStorage.getItem(LAST_ACTIVE_KEY) ); const [isLoading, setIsLoading] = useState(false); const sessionsRef = useRef(sessions); useEffect(() => { sessionsRef.current = sessions; }, [sessions]); // 持久化 activeSessionId 到 localStorage(仅用于恢复选中状态) useEffect(() => { if (activeSessionId) { localStorage.setItem(LAST_ACTIVE_KEY, activeSessionId); } else { localStorage.removeItem(LAST_ACTIVE_KEY); } }, [activeSessionId]); /** 从后端加载会话列表 */ const loadSessions = useCallback(async () => { if (!accessToken) return; setIsLoading(true); try { const res = await listConversations(accessToken); if (res.data) { const list = res.data.conversations.map(toSessionSummary); setSessions(list); // 恢复上次选中的会话(如果仍然存在) const lastId = localStorage.getItem(LAST_ACTIVE_KEY); if (lastId && list.find((s) => s.id === lastId)) { setActiveSessionId(lastId); } else if (list.length > 0) { setActiveSessionId(list[0].id); } } } catch (err) { console.error("[SessionList] 加载会话列表失败:", err); } finally { setIsLoading(false); } }, [accessToken]); // 初始化加载 + token 变化时重新加载 useEffect(() => { if (accessToken) { loadSessions(); } }, [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps /** 创建新会话 */ const createSession = useCallback(async (): Promise => { if (!accessToken) { // 未登录时回退到本地 ID const id = uuidv4(); const now = Date.now(); const summary: SessionSummary = { id, title: "新对话", createdAt: now, lastActiveAt: now, messageCount: 0, preview: "", }; setSessions((prev) => [summary, ...prev]); setActiveSessionId(id); return id; } try { const res = await createConversation(accessToken); if (res.data) { const id = res.data.id; const now = Date.now(); const summary: SessionSummary = { id, title: res.data.title || "新对话", createdAt: new Date(res.data.created_at).getTime() || now, lastActiveAt: new Date(res.data.updated_at).getTime() || now, messageCount: 0, preview: "", }; setSessions((prev) => [summary, ...prev]); setActiveSessionId(id); return id; } } catch (err) { console.error("[SessionList] 创建会话失败:", err); } return null; }, [accessToken]); /** 删除会话 */ const deleteSession = useCallback( async (id: string) => { setSessions((prev) => prev.filter((s) => s.id !== id)); if (accessToken) { try { await deleteConversation(accessToken, id); } catch (err) { console.error("[SessionList] 删除会话失败:", err); } } // 如果删除的是当前会话,切换到第一个或清空 setActiveSessionId((prev) => { if (prev === id) { const remaining = sessionsRef.current.filter((s) => s.id !== id); return remaining.length > 0 ? remaining[0].id : null; } return prev; }); }, [accessToken] ); /** 重命名会话 */ const renameSession = useCallback( async (id: string, title: string) => { setSessions((prev) => prev.map((s) => (s.id === id ? { ...s, title } : s)) ); if (accessToken) { try { await renameConversation(accessToken, id, title); } catch (err) { console.error("[SessionList] 重命名会话失败:", err); } } }, [accessToken] ); /** 加载指定会话的消息历史(从后端 API) */ const loadMessages = useCallback( async (sessionId: string): Promise => { if (!accessToken) return []; try { const res = await getConversationMessages(accessToken, sessionId); if (res.data) { return res.data.messages.map(toChatMessage); } } catch (err) { console.error("[SessionList] 加载消息失败:", err); } return []; }, [accessToken] ); /** 选择会话 */ const selectSession = useCallback( async (id: string): Promise => { setActiveSessionId(id); return loadMessages(id); }, [loadMessages] ); return { sessions, activeSessionId, setActiveSessionId, isLoading, createSession, deleteSession, renameSession, loadMessages, selectSession, loadSessions, }; }