fix(chat):建立连接前进行 token 有效性验证
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user