fix(chat):建立连接前进行 token 有效性验证

This commit is contained in:
2026-06-05 13:06:20 +08:00
parent 57369772c7
commit 709b029c4e
3 changed files with 109 additions and 6 deletions
+33 -5
View File
@@ -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<Props> = ({ 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<AgentModel>(
"deepseek/deepseek-v4-pro",
);
@@ -33,6 +36,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
const composerRef = useRef<AgentComposerHandle | null>(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<Props> = ({ 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<Props> = ({ open, onClose }) => {
<AgentComposer
ref={composerRef}
isHydrating={isHydrating}
isHydrating={isHydrating || isCheckingAuth}
isStreaming={isStreaming}
isListening={isListening}
isSttSupported={isSttSupported}
@@ -14,6 +14,7 @@ jest.mock("@/lib/chatStream", () => ({
}));
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<string | undefined>((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;
@@ -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);
}