feat: 优化对话历史功能

This commit is contained in:
2026-06-20 19:57:36 +08:00
parent 2a7d4c74d4
commit ab07e01adf
15 changed files with 653 additions and 174 deletions

View File

@@ -1,109 +1,227 @@
// ============================================================
// useSessionList — 会话历史列表管理
// 职责:会话 CRUD、消息持久化、切换会话
// useSessionList — 会话历史列表管理(后端 API 驱动)
// 职责:会话 CRUD、消息加载、切换会话
// 数据源:后端 /api/conversations REST API
// ============================================================
import { useCallback, useEffect, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import {
loadSessionSummaries,
saveSessionSummaries,
loadSessionMessages,
saveSessionMessages,
deleteSessionMessages,
} from "../lib/storage";
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;
}
export function useSessionList() {
const [sessions, setSessions] = useState<SessionSummary[]>(() => loadSessionSummaries());
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
/** 将后端 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<SessionSummary[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(
() => localStorage.getItem(LAST_ACTIVE_KEY)
);
const [isLoading, setIsLoading] = useState(false);
const sessionsRef = useRef(sessions);
useEffect(() => { sessionsRef.current = sessions; }, [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((): string => {
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);
// 持久化
const all = [summary, ...sessionsRef.current];
saveSessionSummaries(all);
return id;
}, []);
const createSession = useCallback(async (): Promise<string | null> => {
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((id: string) => {
setSessions((prev) => prev.filter((s) => s.id !== id));
deleteSessionMessages(id);
const remaining = sessionsRef.current.filter((s) => s.id !== id);
saveSessionSummaries(remaining);
// 如果删除的是当前会话,清空 active
setActiveSessionId((prev) => (prev === id ? null : prev));
}, []);
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((id: string, title: string) => {
setSessions((prev) => prev.map((s) => (s.id === id ? { ...s, title } : s)));
const updated = sessionsRef.current.map((s) => (s.id === id ? { ...s, title } : s));
saveSessionSummaries(updated);
}, []);
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]
);
/** 保存指定会话的消息并更新摘要 */
const persistSession = useCallback((sessionId: string, messages: ChatMessage[]) => {
if (!sessionId) return;
saveSessionMessages(sessionId, messages);
// 更新摘要
const firstUserMsg = messages.find((m) => m.role === "user");
const title = firstUserMsg ? getPreview(firstUserMsg.content, 20) : "新对话";
const lastMsg = messages[messages.length - 1];
const summary: Partial<SessionSummary> = {
title,
messageCount: messages.length,
lastActiveAt: lastMsg?.timestamp || Date.now(),
preview: lastMsg ? getPreview(lastMsg.content) : "",
};
setSessions((prev) => {
const updated = prev.map((s) => (s.id === sessionId ? { ...s, ...summary } : s));
saveSessionSummaries(updated);
return updated;
});
}, []);
/** 加载指定会话的消息历史(从后端 API */
const loadMessages = useCallback(
async (sessionId: string): Promise<ChatMessage[]> => {
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 loadMessages = useCallback((sessionId: string): ChatMessage[] => {
return loadSessionMessages(sessionId);
}, []);
/** 选择会话(返回需要加载的消息) */
const selectSession = useCallback((id: string): ChatMessage[] => {
setActiveSessionId(id);
return loadSessionMessages(id);
}, []);
/** 选择会话 */
const selectSession = useCallback(
async (id: string): Promise<ChatMessage[]> => {
setActiveSessionId(id);
return loadMessages(id);
},
[loadMessages]
);
return {
sessions,
activeSessionId,
setActiveSessionId,
isLoading,
createSession,
deleteSession,
renameSession,
persistSession,
loadMessages,
selectSession,
loadSessions,
};
}