refactor: use backend chat sessions

This commit is contained in:
2026-06-04 15:02:27 +08:00
parent 20ca410e0a
commit e60e1f6453
9 changed files with 91 additions and 178 deletions
@@ -12,44 +12,38 @@ jest.mock("@/lib/chatStream", () => ({
streamAgentChat: jest.fn(async () => undefined),
}));
const loadActiveChatState = jest.fn();
const listChatSessions = jest.fn();
const saveActiveChatState = jest.fn();
const updateChatSessionTitle = jest.fn();
jest.mock("../chatStorage", () => ({
deleteChatSession: jest.fn(async () => undefined),
listChatSessions: (...args: unknown[]) => listChatSessions(...args),
loadActiveChatState: (...args: unknown[]) => loadActiveChatState(...args),
loadChatSessionById: jest.fn(async () => ({
storageSessionId: "session-loaded",
title: "已存在会话",
createEmptyChatState: jest.fn(() => ({
title: undefined,
isTitleManuallyEdited: false,
messages: [],
sessionId: undefined,
branchGroups: [],
})),
deleteChatSession: jest.fn(async () => undefined),
listChatSessions: (...args: unknown[]) => listChatSessions(...args),
loadChatSessionById: jest.fn(async () => ({
title: "已存在会话",
isTitleManuallyEdited: false,
messages: [],
sessionId: "session-loaded",
branchGroups: [],
})),
saveActiveChatState: (...args: unknown[]) => saveActiveChatState(...args),
updateChatSessionTitle: (...args: unknown[]) => updateChatSessionTitle(...args),
}));
describe("useAgentChatSession", () => {
beforeEach(() => {
loadActiveChatState.mockReset();
listChatSessions.mockReset();
saveActiveChatState.mockReset();
updateChatSessionTitle.mockReset();
jest.mocked(streamAgentChat).mockReset();
saveActiveChatState.mockImplementation(async (state) => state.storageSessionId);
loadActiveChatState.mockResolvedValue({
storageSessionId: undefined,
title: undefined,
isTitleManuallyEdited: false,
messages: [],
sessionId: undefined,
branchGroups: [],
});
saveActiveChatState.mockImplementation(async (state) => state.sessionId);
});
it("does not add a new empty session to history until there is actual chat content", async () => {
@@ -70,7 +64,7 @@ describe("useAgentChatSession", () => {
await waitFor(() => expect(result.current.sessionTitle).toBe("新对话"));
expect(result.current.chatSessions).toEqual([]);
expect(result.current.activeStorageSessionId).toBeUndefined();
expect(result.current.activeSessionId).toBeUndefined();
expect(result.current.messages).toEqual([]);
expect(result.current.isStreaming).toBe(false);
expect(listChatSessions).toHaveBeenCalledTimes(1);
@@ -164,14 +158,6 @@ describe("useAgentChatSession", () => {
it("ignores generated session titles after the title was edited manually", async () => {
listChatSessions.mockResolvedValue([]);
loadActiveChatState.mockResolvedValue({
storageSessionId: "session-1",
title: "手动标题",
isTitleManuallyEdited: true,
messages: [],
sessionId: "session-1",
branchGroups: [],
});
jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
onEvent({
type: "session_title",
@@ -193,13 +179,23 @@ describe("useAgentChatSession", () => {
await waitFor(() => expect(result.current.isHydrating).toBe(false));
await act(async () => {
await result.current.switchSession("session-loaded");
});
await act(async () => {
await result.current.renameSession("session-loaded", "手动标题");
});
await waitFor(() => expect(updateChatSessionTitle).toHaveBeenCalled());
await act(async () => {
await result.current.sendPrompt("帮我分析一下");
});
expect(result.current.sessionTitle).toBe("手动标题");
expect(updateChatSessionTitle).not.toHaveBeenCalledWith(
"session-1",
"session-loaded",
"自动标题",
expect.anything(),
);
@@ -19,9 +19,9 @@ import {
createId,
} from "../GlobalChatbox.utils";
import {
createEmptyChatState,
deleteChatSession,
listChatSessions,
loadActiveChatState,
loadChatSessionById,
saveActiveChatState,
updateChatSessionTitle,
@@ -50,7 +50,6 @@ type PromptRunOptions = {
const createPersistedStateKey = (state: LoadedChatState) =>
JSON.stringify({
storageSessionId: state.storageSessionId ?? null,
title: state.title ?? null,
isTitleManuallyEdited: state.isTitleManuallyEdited ?? false,
sessionId: state.sessionId ?? null,
@@ -151,7 +150,6 @@ export const useAgentChatSession = ({
onBeforeSend,
getModel,
}: UseAgentChatSessionOptions) => {
const storageSessionIdRef = useRef<string | undefined>(undefined);
const hydrationCompletedRef = useRef(false);
const hydrationNonceRef = useRef(0);
@@ -171,11 +169,10 @@ export const useAgentChatSession = ({
const titleUpdateNonceRef = useRef(0);
const lastPersistedStateKeyRef = useRef(
createPersistedStateKey({
storageSessionId: undefined,
sessionId: undefined,
title: undefined,
isTitleManuallyEdited: false,
messages: [],
sessionId: undefined,
branchGroups: [],
}),
);
@@ -196,10 +193,8 @@ export const useAgentChatSession = ({
hydrationCompletedRef.current = false;
if (!projectId) {
storageSessionIdRef.current = undefined;
sessionIdRef.current = undefined;
lastPersistedStateKeyRef.current = createPersistedStateKey({
storageSessionId: undefined,
title: undefined,
isTitleManuallyEdited: false,
messages: [],
@@ -222,12 +217,11 @@ export const useAgentChatSession = ({
try {
const [loadedState, sessions] = await Promise.all([
loadActiveChatState(projectId),
Promise.resolve(createEmptyChatState()),
listChatSessions(),
]);
if (cancelled) return;
storageSessionIdRef.current = loadedState.storageSessionId;
sessionIdRef.current = loadedState.sessionId;
lastPersistedStateKeyRef.current = createPersistedStateKey(loadedState);
hydrationCompletedRef.current = true;
@@ -262,7 +256,6 @@ export const useAgentChatSession = ({
const currentHydrationNonce = hydrationNonceRef.current;
const persistTimer = window.setTimeout(() => {
const state: LoadedChatState = {
storageSessionId: storageSessionIdRef.current,
title: sessionTitle,
isTitleManuallyEdited: isSessionTitleManuallyEdited,
messages,
@@ -271,7 +264,6 @@ export const useAgentChatSession = ({
};
if (
isStreaming &&
!state.storageSessionId &&
!state.sessionId &&
state.messages.length > 0
) {
@@ -283,13 +275,13 @@ export const useAgentChatSession = ({
return;
}
void saveActiveChatState(state, projectId)
.then((storageSessionId) => {
void saveActiveChatState(state)
.then((sessionId) => {
if (hydrationNonceRef.current !== currentHydrationNonce) return;
storageSessionIdRef.current = storageSessionId;
sessionIdRef.current = sessionId;
lastPersistedStateKeyRef.current = createPersistedStateKey({
...state,
storageSessionId,
sessionId,
});
return listChatSessions();
})
@@ -431,10 +423,10 @@ export const useAgentChatSession = ({
const nextTitle = event.title.trim();
if (nextTitle && !isSessionTitleManuallyEditedRef.current) {
setSessionTitle(nextTitle);
const currentStorageSessionId = storageSessionIdRef.current;
if (currentStorageSessionId) {
const currentSessionId = sessionIdRef.current;
if (currentSessionId) {
const currentNonce = ++titleUpdateNonceRef.current;
void updateChatSessionTitle(currentStorageSessionId, nextTitle, {
void updateChatSessionTitle(currentSessionId, nextTitle, {
isTitleManuallyEdited: false,
})
.then(() => listChatSessions())
@@ -555,10 +547,8 @@ export const useAgentChatSession = ({
setBranchTransition(null);
hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = undefined;
sessionIdRef.current = undefined;
lastPersistedStateKeyRef.current = createPersistedStateKey({
storageSessionId: undefined,
title: "新对话",
isTitleManuallyEdited: false,
messages: [],
@@ -574,21 +564,20 @@ export const useAgentChatSession = ({
}, [isHydrating, isStreaming]);
const switchSession = useCallback(
async (nextStorageSessionId: string) => {
if (isHydrating || isStreaming || storageSessionIdRef.current === nextStorageSessionId) {
async (nextSessionId: string) => {
if (isHydrating || isStreaming || sessionIdRef.current === nextSessionId) {
return;
}
setIsHydrating(true);
try {
const [nextState, sessions] = await Promise.all([
loadChatSessionById(nextStorageSessionId, projectId),
loadChatSessionById(nextSessionId),
listChatSessions(),
]);
hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = nextState.storageSessionId;
sessionIdRef.current = nextState.sessionId;
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
setBranchTransition(null);
@@ -604,32 +593,29 @@ export const useAgentChatSession = ({
setIsHydrating(false);
}
},
[isHydrating, isStreaming, projectId],
[isHydrating, isStreaming],
);
const removeSession = useCallback(
async (targetStorageSessionId: string) => {
async (targetSessionId: string) => {
if (isHydrating || isStreaming) return;
try {
const nextActiveSessionId = await deleteChatSession(
targetStorageSessionId,
projectId,
targetSessionId,
);
const sessions = await listChatSessions();
setChatSessions(sessions);
if (storageSessionIdRef.current !== targetStorageSessionId) {
if (sessionIdRef.current !== targetSessionId) {
return;
}
if (!nextActiveSessionId) {
hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = undefined;
sessionIdRef.current = undefined;
lastPersistedStateKeyRef.current = createPersistedStateKey({
storageSessionId: undefined,
title: undefined,
isTitleManuallyEdited: false,
messages: [],
@@ -647,12 +633,11 @@ export const useAgentChatSession = ({
setIsHydrating(true);
const [nextState, sessionsAfterDelete] = await Promise.all([
loadChatSessionById(nextActiveSessionId, projectId),
loadChatSessionById(nextActiveSessionId),
listChatSessions(),
]);
hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = nextState.storageSessionId;
sessionIdRef.current = nextState.sessionId;
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
setBranchTransition(null);
@@ -668,7 +653,7 @@ export const useAgentChatSession = ({
setIsHydrating(false);
}
},
[isHydrating, isStreaming, projectId],
[isHydrating, isStreaming],
);
const sendPrompt = useCallback(
@@ -679,22 +664,22 @@ export const useAgentChatSession = ({
);
const renameSession = useCallback(
async (targetStorageSessionId: string, nextTitle: string) => {
async (targetSessionId: string, nextTitle: string) => {
const normalizedTitle = nextTitle.trim();
if (!normalizedTitle || isHydrating) return;
try {
await updateChatSessionTitle(targetStorageSessionId, normalizedTitle, {
await updateChatSessionTitle(targetSessionId, normalizedTitle, {
isTitleManuallyEdited: true,
});
const sessions = await listChatSessions();
setChatSessions(sessions);
if (storageSessionIdRef.current === targetStorageSessionId) {
if (sessionIdRef.current === targetSessionId) {
setSessionTitle(normalizedTitle);
setIsSessionTitleManuallyEdited(true);
lastPersistedStateKeyRef.current = createPersistedStateKey({
storageSessionId: targetStorageSessionId,
sessionId: targetSessionId,
title: normalizedTitle,
isTitleManuallyEdited: true,
messages,
@@ -864,7 +849,7 @@ export const useAgentChatSession = ({
return {
messages,
chatSessions,
activeStorageSessionId: storageSessionIdRef.current,
activeSessionId: sessionIdRef.current,
branchGroups,
branchTransition,
isHydrating,