From 709b029c4e67b9555fb66d25c786909b7950c953 Mon Sep 17 00:00:00 2001 From: Huarch Date: Fri, 5 Jun 2026 13:06:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat)=EF=BC=9A=E5=BB=BA=E7=AB=8B=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=89=8D=E8=BF=9B=E8=A1=8C=20token=20=E6=9C=89?= =?UTF-8?q?=E6=95=88=E6=80=A7=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/GlobalChatbox.tsx | 38 +++++++++-- .../chat/hooks/useAgentChatSession.test.tsx | 68 ++++++++++++++++++- .../chat/hooks/useAgentChatSession.ts | 9 +++ 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/components/chat/GlobalChatbox.tsx b/src/components/chat/GlobalChatbox.tsx index a56c79e..eb81823 100644 --- a/src/components/chat/GlobalChatbox.tsx +++ b/src/components/chat/GlobalChatbox.tsx @@ -7,7 +7,9 @@ import React, { useState, } from "react"; import { Box, Drawer, alpha, useTheme } from "@mui/material"; +import { useNotification } from "@refinedev/core"; +import { getAccessToken } from "@/lib/authToken"; import type { AgentModel } from "@/lib/chatStream"; import { useProjectStore } from "@/store/projectStore"; import { AgentComposer, type AgentComposerHandle } from "./AgentComposer"; @@ -25,6 +27,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { const [width, setWidth] = useState(520); const [isResizing, setIsResizing] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [isCheckingAuth, setIsCheckingAuth] = useState(false); const [selectedModel, setSelectedModel] = useState( "deepseek/deepseek-v4-pro", ); @@ -33,6 +36,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { const composerRef = useRef(null); const hasResetForOpenRef = useRef(false); const theme = useTheme(); + const { open: openNotification } = useNotification(); const currentProjectId = useProjectStore((state) => state.currentProjectId); const { @@ -108,10 +112,34 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { return () => window.clearTimeout(timer); }, [createSession, isHydrating, open, scrollToBottom]); - const handleSend = useCallback((prompt: string) => { - if (isStreaming) return; - void sendPrompt(prompt); - }, [isStreaming, sendPrompt]); + const handleSend = useCallback(async (prompt: string) => { + if (isStreaming || isCheckingAuth) return; + + setIsCheckingAuth(true); + try { + const accessToken = await getAccessToken(); + if (!accessToken) { + composerRef.current?.setValue(prompt); + openNotification?.({ + type: "error", + message: "登录状态已失效", + description: "请重新登录后再发送对话。", + }); + return; + } + + void sendPrompt(prompt); + } catch (error) { + composerRef.current?.setValue(prompt); + openNotification?.({ + type: "error", + message: "登录状态校验失败", + description: error instanceof Error ? error.message : "请重新登录后再试。", + }); + } finally { + setIsCheckingAuth(false); + } + }, [isCheckingAuth, isStreaming, openNotification, sendPrompt]); const handleNewConversation = useCallback(() => { handleStopSpeech(); @@ -330,7 +358,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { ({ })); const listChatSessions = jest.fn(); +const deleteChatSession = jest.fn(); const saveActiveChatState = jest.fn(); const updateChatSessionTitle = jest.fn(); @@ -25,7 +26,7 @@ jest.mock("../chatStorage", () => ({ sessionId: undefined, branchGroups: [], })), - deleteChatSession: jest.fn(async () => undefined), + deleteChatSession: (...args: unknown[]) => deleteChatSession(...args), listChatSessions: (...args: unknown[]) => listChatSessions(...args), loadChatSessionById: jest.fn(async () => ({ title: "已存在会话", @@ -41,6 +42,7 @@ jest.mock("../chatStorage", () => ({ describe("useAgentChatSession", () => { beforeEach(() => { listChatSessions.mockReset(); + deleteChatSession.mockReset(); saveActiveChatState.mockReset(); updateChatSessionTitle.mockReset(); jest.mocked(abortAgentChat).mockReset(); @@ -49,6 +51,7 @@ describe("useAgentChatSession", () => { jest.mocked(abortAgentChat).mockImplementation(async () => undefined); jest.mocked(resumeAgentChatStream).mockImplementation(async () => undefined); jest.mocked(streamAgentChat).mockImplementation(async () => undefined); + deleteChatSession.mockImplementation(async () => undefined); saveActiveChatState.mockImplementation(async (state) => state.sessionId); }); @@ -109,6 +112,69 @@ describe("useAgentChatSession", () => { ]); }); + it("removes a deleted history entry before the backend delete finishes", async () => { + const initialSessions = [ + { + id: "session-1", + title: "第一段会话", + createdAt: 2, + updatedAt: 2, + }, + { + id: "session-2", + title: "第二段会话", + createdAt: 1, + updatedAt: 1, + }, + ]; + let resolveDelete: ((nextActiveSessionId?: string) => void) | undefined; + + listChatSessions.mockResolvedValue(initialSessions); + deleteChatSession.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveDelete = resolve; + }), + ); + + const { result } = renderHook(() => + useAgentChatSession({ + projectId: "project-1", + onToolCall: jest.fn(), + }), + ); + + await waitFor(() => expect(result.current.isHydrating).toBe(false)); + + act(() => { + void result.current.removeSession("session-2"); + }); + + expect(result.current.chatSessions).toEqual([ + expect.objectContaining({ id: "session-1" }), + ]); + + listChatSessions.mockResolvedValue([ + { + id: "session-1", + title: "第一段会话", + createdAt: 2, + updatedAt: 2, + }, + ]); + + await act(async () => { + resolveDelete?.(); + await Promise.resolve(); + }); + + await waitFor(() => + expect(result.current.chatSessions).toEqual([ + expect.objectContaining({ id: "session-1" }), + ]), + ); + }); + it("persists a new conversation only after the stream is done", async () => { listChatSessions.mockResolvedValue([]); let emitStreamEvent: ((event: StreamEvent) => void) | undefined; diff --git a/src/components/chat/hooks/useAgentChatSession.ts b/src/components/chat/hooks/useAgentChatSession.ts index 2cd64c5..cb8150a 100644 --- a/src/components/chat/hooks/useAgentChatSession.ts +++ b/src/components/chat/hooks/useAgentChatSession.ts @@ -683,6 +683,10 @@ export const useAgentChatSession = ({ async (targetSessionId: string) => { if (isHydrating || isStreaming) return; + setChatSessions((prev) => + prev.filter((session) => session.id !== targetSessionId), + ); + try { const nextActiveSessionId = await deleteChatSession( targetSessionId, @@ -732,6 +736,11 @@ export const useAgentChatSession = ({ setChatSessions(sessionsAfterDelete); } catch (error) { console.error("[GlobalChatbox] Failed to delete chat session:", error); + try { + setChatSessions(await listChatSessions()); + } catch (refreshError) { + console.error("[GlobalChatbox] Failed to refresh chat sessions:", refreshError); + } } finally { setIsHydrating(false); }