fix(web): 修复 useChat 无限请求问题
- 用 useRef 存储回调函数,避免依赖变化导致无限循环 - 添加 onSessionNotFound 回调,会话不存在时自动创建新会话 - 限制 WebSocket 重连次数为 5 次 - 添加 web:dev 快捷脚本
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"clean": "pnpm -r exec rm -rf dist node_modules",
|
"clean": "pnpm -r exec rm -rf dist node_modules",
|
||||||
"server:start": "pnpm --filter @ai-assistant/server start",
|
"server:start": "pnpm --filter @ai-assistant/server start",
|
||||||
"server:dev": "pnpm --filter @ai-assistant/server start:dev",
|
"server:dev": "pnpm --filter @ai-assistant/server start:dev",
|
||||||
|
"web:dev": "pnpm --filter @ai-assistant/web dev",
|
||||||
"desktop:dev": "pnpm --filter @ai-assistant/desktop dev",
|
"desktop:dev": "pnpm --filter @ai-assistant/desktop dev",
|
||||||
"desktop:build": "pnpm --filter @ai-assistant/desktop build"
|
"desktop:build": "pnpm --filter @ai-assistant/desktop build"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 响应式布局:支持桌面端和移动端
|
* 响应式布局:支持桌面端和移动端
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Sidebar } from './components/Sidebar';
|
import { Sidebar } from './components/Sidebar';
|
||||||
import { ChatPage } from './pages/Chat';
|
import { ChatPage } from './pages/Chat';
|
||||||
import { FileBrowser } from './components/FileBrowser';
|
import { FileBrowser } from './components/FileBrowser';
|
||||||
@@ -49,6 +49,16 @@ export function App() {
|
|||||||
setCurrentSessionId(session.id);
|
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) {
|
if (isInitializing) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex items-center justify-center bg-gray-900">
|
<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'}`}>
|
<div className={`flex-1 min-w-0 ${showFileBrowser ? 'hidden md:block md:w-1/2' : 'w-full'}`}>
|
||||||
{currentSessionId ? (
|
{currentSessionId ? (
|
||||||
<ChatPage key={currentSessionId} sessionId={currentSessionId} />
|
<ChatPage key={currentSessionId} sessionId={currentSessionId} onSessionNotFound={handleSessionNotFound} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center h-full">
|
<div className="flex-1 flex items-center justify-center h-full">
|
||||||
<p className="text-gray-400">Select or create a session</p>
|
<p className="text-gray-400">Select or create a session</p>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createWebSocket, getMessages, type Message } from '../api/client';
|
|||||||
interface UseChatOptions {
|
interface UseChatOptions {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
|
onSessionNotFound?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
@@ -19,7 +20,7 @@ interface ChatState {
|
|||||||
streamingContent: string;
|
streamingContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChat({ sessionId, onError }: UseChatOptions) {
|
export function useChat({ sessionId, onError, onSessionNotFound }: UseChatOptions) {
|
||||||
const [state, setState] = useState<ChatState>({
|
const [state, setState] = useState<ChatState>({
|
||||||
messages: [],
|
messages: [],
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -29,6 +30,14 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
|
|||||||
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
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 () => {
|
const loadMessages = useCallback(async () => {
|
||||||
@@ -36,9 +45,15 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
|
|||||||
const { data } = await getMessages(sessionId);
|
const { data } = await getMessages(sessionId);
|
||||||
setState((prev) => ({ ...prev, messages: data }));
|
setState((prev) => ({ ...prev, messages: data }));
|
||||||
} catch (error) {
|
} 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;
|
||||||
}
|
}
|
||||||
}, [sessionId, onError]);
|
onErrorRef.current?.(error instanceof Error ? error : new Error('Failed to load messages'));
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
// 连接 WebSocket
|
// 连接 WebSocket
|
||||||
const connect = useCallback(() => {
|
const connect = useCallback(() => {
|
||||||
@@ -47,17 +62,21 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
|
|||||||
const ws = createWebSocket(sessionId);
|
const ws = createWebSocket(sessionId);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
reconnectAttemptsRef.current = 0; // 连接成功,重置重连次数
|
||||||
setState((prev) => ({ ...prev, isConnected: true }));
|
setState((prev) => ({ ...prev, isConnected: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setState((prev) => ({ ...prev, isConnected: false }));
|
setState((prev) => ({ ...prev, isConnected: false }));
|
||||||
// 自动重连
|
// 限制重连次数
|
||||||
|
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||||
|
reconnectAttemptsRef.current++;
|
||||||
reconnectTimeoutRef.current = setTimeout(connect, 3000);
|
reconnectTimeoutRef.current = setTimeout(connect, 3000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
onError?.(new Error('WebSocket connection error'));
|
onErrorRef.current?.(new Error('WebSocket connection error'));
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -98,7 +117,7 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
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: '' }));
|
setState((prev) => ({ ...prev, isLoading: false, streamingContent: '' }));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -108,13 +127,13 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
}, [sessionId, onError]);
|
}, [sessionId]);
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
(content: string) => {
|
(content: string) => {
|
||||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
||||||
onError?.(new Error('WebSocket not connected'));
|
onErrorRef.current?.(new Error('WebSocket not connected'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +147,7 @@ export function useChat({ sessionId, onError }: UseChatOptions) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[sessionId, onError]
|
[sessionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 取消处理
|
// 取消处理
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { ChatInput } from '../components/ChatInput';
|
|||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
onSessionNotFound?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPage({ sessionId }: ChatPageProps) {
|
export function ChatPage({ sessionId, onSessionNotFound }: ChatPageProps) {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
isConnected,
|
isConnected,
|
||||||
@@ -25,6 +26,7 @@ export function ChatPage({ sessionId }: ChatPageProps) {
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Chat error:', error);
|
console.error('Chat error:', error);
|
||||||
},
|
},
|
||||||
|
onSessionNotFound,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user