重构聊天会话管理,支持会话历史和存储

This commit is contained in:
2026-04-30 15:02:08 +08:00
parent c5b0f43a0d
commit e0e78cd95a
11 changed files with 1247 additions and 221 deletions
+209 -32
View File
@@ -9,16 +9,23 @@ import type {
BranchGroup,
BranchTransition,
ChatProgress,
ChatSessionSummary,
LoadedChatState,
Message,
PersistedChatState,
} from "../GlobalChatbox.types";
import {
CHAT_STORAGE_KEY,
cloneBranchGroups,
cloneMessages,
createId,
getInitialChatState,
} from "../GlobalChatbox.utils";
import {
createEmptyChatSession,
deleteChatSession,
listChatSessions,
loadActiveChatState,
loadChatSessionById,
saveActiveChatState,
} from "../chatStorage";
type UseAgentChatSessionOptions = {
onToolCall: (
@@ -88,24 +95,20 @@ export const useAgentChatSession = ({
onToolCall,
onBeforeSend,
}: UseAgentChatSessionOptions) => {
const initialChatStateRef = useRef<PersistedChatState | null>(null);
if (initialChatStateRef.current === null) {
initialChatStateRef.current = getInitialChatState();
}
const storageSessionIdRef = useRef<string | undefined>(undefined);
const hydrationCompletedRef = useRef(false);
const hydrationNonceRef = useRef(0);
const [messages, setMessages] = useState<Message[]>(
initialChatStateRef.current.messages,
);
const [sessionId, setSessionId] = useState<string | undefined>(
initialChatStateRef.current.sessionId,
);
const [branchGroups, setBranchGroups] = useState<BranchGroup[]>(
initialChatStateRef.current.branchGroups ?? [],
);
const [messages, setMessages] = useState<Message[]>([]);
const [sessionTitle, setSessionTitle] = useState<string | undefined>(undefined);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const [branchGroups, setBranchGroups] = useState<BranchGroup[]>([]);
const [chatSessions, setChatSessions] = useState<ChatSessionSummary[]>([]);
const [branchTransition, setBranchTransition] = useState<BranchTransition | null>(null);
const [isStreaming, setIsStreaming] = useState(false);
const [isHydrating, setIsHydrating] = useState(true);
const abortRef = useRef<AbortController | null>(null);
const sessionIdRef = useRef<string | undefined>(initialChatStateRef.current.sessionId);
const sessionIdRef = useRef<string | undefined>(undefined);
const cancelPromiseRef = useRef<Promise<void> | null>(null);
useEffect(() => {
@@ -113,13 +116,74 @@ export const useAgentChatSession = ({
}, [sessionId]);
useEffect(() => {
const state: PersistedChatState = { messages, sessionId, branchGroups };
try {
window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(state));
} catch (error) {
console.error("[GlobalChatbox] Failed to persist chat state:", error);
}
}, [branchGroups, messages, sessionId]);
let cancelled = false;
const hydrate = async () => {
try {
const [loadedState, sessions] = await Promise.all([
loadActiveChatState(),
listChatSessions(),
]);
if (cancelled) return;
storageSessionIdRef.current = loadedState.storageSessionId;
sessionIdRef.current = loadedState.sessionId;
hydrationCompletedRef.current = true;
hydrationNonceRef.current += 1;
setMessages(loadedState.messages);
setSessionTitle(loadedState.title);
setSessionId(loadedState.sessionId);
setBranchGroups(loadedState.branchGroups);
setChatSessions(sessions);
} catch (error) {
console.error("[GlobalChatbox] Failed to hydrate chat state:", error);
} finally {
if (!cancelled) {
setIsHydrating(false);
}
}
};
void hydrate();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (isHydrating || !hydrationCompletedRef.current) return;
const currentHydrationNonce = hydrationNonceRef.current;
const persistTimer = window.setTimeout(() => {
const state: LoadedChatState = {
storageSessionId: storageSessionIdRef.current,
title: sessionTitle,
messages,
sessionId,
branchGroups,
};
void saveActiveChatState(state)
.then((storageSessionId) => {
if (hydrationNonceRef.current !== currentHydrationNonce) return;
storageSessionIdRef.current = storageSessionId;
return listChatSessions();
})
.then((sessions) => {
if (!sessions || hydrationNonceRef.current !== currentHydrationNonce) return;
setChatSessions(sessions);
})
.catch((error) => {
console.error("[GlobalChatbox] Failed to persist chat state:", error);
});
}, 150);
return () => {
window.clearTimeout(persistTimer);
};
}, [branchGroups, isHydrating, messages, sessionId, sessionTitle]);
useEffect(() => {
setBranchGroups((prev) => {
@@ -182,7 +246,7 @@ export const useAgentChatSession = ({
assistantMessage,
}: PromptRunOptions) => {
const prompt = rawPrompt.trim();
if (!prompt || isStreaming) return;
if (!prompt || isStreaming || isHydrating) return;
await cancelPromiseRef.current?.catch(() => undefined);
onBeforeSend?.();
@@ -240,6 +304,11 @@ export const useAgentChatSession = ({
assistantMessageId: nextAssistantMessage.id,
appendArtifact,
});
} else if (event.type === "session_title") {
const nextTitle = event.title.trim();
if (nextTitle) {
setSessionTitle(nextTitle);
}
} else if (event.type === "done") {
setMessages((prev) =>
prev.map((message) => {
@@ -321,7 +390,7 @@ export const useAgentChatSession = ({
setIsStreaming(false);
}
},
[appendArtifact, isStreaming, messages, onBeforeSend, onToolCall],
[appendArtifact, isHydrating, isStreaming, messages, onBeforeSend, onToolCall],
);
const abort = useCallback(() => {
@@ -356,13 +425,115 @@ export const useAgentChatSession = ({
cancelPromiseRef.current = trackedCancelPromise;
}
setMessages([]);
setSessionTitle(undefined);
setBranchGroups([]);
setBranchTransition(null);
setSessionId(undefined);
sessionIdRef.current = undefined;
storageSessionIdRef.current = undefined;
setIsStreaming(false);
}, []);
const createSession = useCallback(async () => {
if (isHydrating || isStreaming) return;
const controller = abortRef.current;
controller?.abort();
setBranchTransition(null);
const newState = await createEmptyChatSession();
const sessions = await listChatSessions();
hydrationNonceRef.current += 1;
storageSessionIdRef.current = newState.storageSessionId;
sessionIdRef.current = newState.sessionId;
setMessages(newState.messages);
setSessionTitle(newState.title);
setSessionId(newState.sessionId);
setBranchGroups(newState.branchGroups);
setChatSessions(sessions);
setIsStreaming(false);
}, [isHydrating, isStreaming]);
const switchSession = useCallback(
async (nextStorageSessionId: string) => {
if (isHydrating || isStreaming || storageSessionIdRef.current === nextStorageSessionId) {
return;
}
setIsHydrating(true);
try {
const [nextState, sessions] = await Promise.all([
loadChatSessionById(nextStorageSessionId),
listChatSessions(),
]);
hydrationNonceRef.current += 1;
storageSessionIdRef.current = nextState.storageSessionId;
sessionIdRef.current = nextState.sessionId;
setBranchTransition(null);
setMessages(nextState.messages);
setSessionTitle(nextState.title);
setSessionId(nextState.sessionId);
setBranchGroups(nextState.branchGroups);
setChatSessions(sessions);
} catch (error) {
console.error("[GlobalChatbox] Failed to switch chat session:", error);
} finally {
setIsHydrating(false);
}
},
[isHydrating, isStreaming],
);
const removeSession = useCallback(
async (targetStorageSessionId: string) => {
if (isHydrating || isStreaming) return;
try {
const nextActiveSessionId = await deleteChatSession(targetStorageSessionId);
const sessions = await listChatSessions();
setChatSessions(sessions);
if (storageSessionIdRef.current !== targetStorageSessionId) {
return;
}
if (!nextActiveSessionId) {
hydrationNonceRef.current += 1;
storageSessionIdRef.current = undefined;
sessionIdRef.current = undefined;
setBranchTransition(null);
setMessages([]);
setSessionTitle(undefined);
setSessionId(undefined);
setBranchGroups([]);
return;
}
setIsHydrating(true);
const [nextState, sessionsAfterDelete] = await Promise.all([
loadChatSessionById(nextActiveSessionId),
listChatSessions(),
]);
hydrationNonceRef.current += 1;
storageSessionIdRef.current = nextState.storageSessionId;
sessionIdRef.current = nextState.sessionId;
setBranchTransition(null);
setMessages(nextState.messages);
setSessionTitle(nextState.title);
setSessionId(nextState.sessionId);
setBranchGroups(nextState.branchGroups);
setChatSessions(sessionsAfterDelete);
} catch (error) {
console.error("[GlobalChatbox] Failed to delete chat session:", error);
} finally {
setIsHydrating(false);
}
},
[isHydrating, isStreaming],
);
const sendPrompt = useCallback(
async (rawPrompt: string) => {
await runPrompt({ prompt: rawPrompt });
@@ -371,7 +542,7 @@ export const useAgentChatSession = ({
);
const regenerate = useCallback(async () => {
if (isStreaming || messages.length === 0) return;
if (isHydrating || isStreaming || messages.length === 0) return;
let lastUserIndex = messages.length - 1;
while (lastUserIndex >= 0 && messages[lastUserIndex].role !== "user") {
@@ -400,11 +571,11 @@ export const useAgentChatSession = ({
userMessage: nextUserMessage,
assistantMessage: nextAssistantMessage,
});
}, [isStreaming, messages, runPrompt]);
}, [isHydrating, isStreaming, messages, runPrompt]);
const editAndResubmit = useCallback(
async (messageId: string, newContent: string) => {
if (isStreaming) return;
if (isHydrating || isStreaming) return;
const trimmedContent = newContent.trim();
if (!trimmedContent) return;
@@ -483,12 +654,12 @@ export const useAgentChatSession = ({
assistantMessage: nextAssistantMessage,
});
},
[isStreaming, messages, runPrompt],
[isHydrating, isStreaming, messages, runPrompt],
);
const cycleBranch = useCallback(
(rootMessageId: string, direction: -1 | 1) => {
if (isStreaming) return;
if (isHydrating || isStreaming) return;
setBranchGroups((prev) => {
const next = cloneBranchGroups(prev);
@@ -519,13 +690,16 @@ export const useAgentChatSession = ({
return next;
});
},
[isStreaming, messages],
[isHydrating, isStreaming, messages],
);
return {
messages,
chatSessions,
activeStorageSessionId: storageSessionIdRef.current,
branchGroups,
branchTransition,
isHydrating,
isStreaming,
sessionId,
sendPrompt,
@@ -533,6 +707,9 @@ export const useAgentChatSession = ({
editAndResubmit,
cycleBranch,
abort,
createSession,
reset,
removeSession,
switchSession,
};
};