fix(web): 修复 useChat 无限请求问题

- 用 useRef 存储回调函数,避免依赖变化导致无限循环
- 添加 onSessionNotFound 回调,会话不存在时自动创建新会话
- 限制 WebSocket 重连次数为 5 次
- 添加 web:dev 快捷脚本
This commit is contained in:
2025-12-12 14:27:54 +08:00
parent 20765efe62
commit 2fe1c55997
4 changed files with 45 additions and 13 deletions
+12 -2
View File
@@ -4,7 +4,7 @@
* 响应式布局:支持桌面端和移动端
*/
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Sidebar } from './components/Sidebar';
import { ChatPage } from './pages/Chat';
import { FileBrowser } from './components/FileBrowser';
@@ -49,6 +49,16 @@ export function App() {
setCurrentSessionId(session.id);
};
// 会话不存在时自动创建新会话
const handleSessionNotFound = useCallback(async () => {
try {
const { data: newSession } = await createSession();
setCurrentSessionId(newSession.id);
} catch (error) {
console.error('Failed to create new session:', error);
}
}, []);
if (isInitializing) {
return (
<div className="h-screen flex items-center justify-center bg-gray-900">
@@ -116,7 +126,7 @@ export function App() {
{/* 聊天区域 */}
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
{currentSessionId ? (
<ChatPage key={currentSessionId} sessionId={currentSessionId} />
<ChatPage key={currentSessionId} sessionId={currentSessionId} onSessionNotFound={handleSessionNotFound} />
) : (
<div className="flex-1 flex items-center justify-center h-full">
<p className="text-gray-400">Select or create a session</p>
+29 -10
View File
@@ -10,6 +10,7 @@ import { createWebSocket, getMessages, type Message } from '../api/client';
interface UseChatOptions {
sessionId: string;
onError?: (error: Error) => void;
onSessionNotFound?: () => void;
}
interface ChatState {
@@ -19,7 +20,7 @@ interface ChatState {
streamingContent: string;
}
export function useChat({ sessionId, onError }: UseChatOptions) {
export function useChat({ sessionId, onError, onSessionNotFound }: UseChatOptions) {
const [state, setState] = useState<ChatState>({
messages: [],
isConnected: false,
@@ -29,6 +30,14 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
const reconnectAttemptsRef = useRef(0);
const maxReconnectAttempts = 5;
// 用 ref 存储回调,避免依赖变化导致无限循环
const onErrorRef = useRef(onError);
const onSessionNotFoundRef = useRef(onSessionNotFound);
onErrorRef.current = onError;
onSessionNotFoundRef.current = onSessionNotFound;
// 加载历史消息
const loadMessages = useCallback(async () => {
@@ -36,9 +45,15 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
const { data } = await getMessages(sessionId);
setState((prev) => ({ ...prev, messages: data }));
} catch (error) {
onError?.(error instanceof Error ? error : new Error('Failed to load messages'));
// 会话不存在(404 或 "Session not found"),通知上层重新创建
const msg = error instanceof Error ? error.message : '';
if (msg.includes('404') || msg.toLowerCase().includes('not found')) {
onSessionNotFoundRef.current?.();
return;
}
onErrorRef.current?.(error instanceof Error ? error : new Error('Failed to load messages'));
}
}, [sessionId, onError]);
}, [sessionId]);
// 连接 WebSocket
const connect = useCallback(() => {
@@ -47,17 +62,21 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
const ws = createWebSocket(sessionId);
ws.onopen = () => {
reconnectAttemptsRef.current = 0; // 连接成功,重置重连次数
setState((prev) => ({ ...prev, isConnected: true }));
};
ws.onclose = () => {
setState((prev) => ({ ...prev, isConnected: false }));
// 自动重连
reconnectTimeoutRef.current = setTimeout(connect, 3000);
// 限制重连次数
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current++;
reconnectTimeoutRef.current = setTimeout(connect, 3000);
}
};
ws.onerror = () => {
onError?.(new Error('WebSocket connection error'));
onErrorRef.current?.(new Error('WebSocket connection error'));
};
ws.onmessage = (event) => {
@@ -98,7 +117,7 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
break;
case 'error':
onError?.(new Error(message.payload?.message || 'Unknown error'));
onErrorRef.current?.(new Error(message.payload?.message || 'Unknown error'));
setState((prev) => ({ ...prev, isLoading: false, streamingContent: '' }));
break;
}
@@ -108,13 +127,13 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
};
wsRef.current = ws;
}, [sessionId, onError]);
}, [sessionId]);
// 发送消息
const sendMessage = useCallback(
(content: string) => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
onError?.(new Error('WebSocket not connected'));
onErrorRef.current?.(new Error('WebSocket not connected'));
return;
}
@@ -128,7 +147,7 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
})
);
},
[sessionId, onError]
[sessionId]
);
// 取消处理
+3 -1
View File
@@ -10,9 +10,10 @@ import { ChatInput } from '../components/ChatInput';
interface ChatPageProps {
sessionId: string;
onSessionNotFound?: () => void;
}
export function ChatPage({ sessionId }: ChatPageProps) {
export function ChatPage({ sessionId, onSessionNotFound }: ChatPageProps) {
const {
messages,
isConnected,
@@ -25,6 +26,7 @@ export function ChatPage({ sessionId }: ChatPageProps) {
onError: (error) => {
console.error('Chat error:', error);
},
onSessionNotFound,
});
const messagesEndRef = useRef<HTMLDivElement>(null);