From 918f4da40e39b00635ed2366a896ddbca2a1c8eb Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Thu, 7 Aug 2025 01:03:38 +0800 Subject: [PATCH] fix --- app/(tabs)/ask.tsx | 130 ++++++++++++++-------- components/ask/send.tsx | 175 ++++++++++++++++++++---------- hooks/useWebSocketSubscription.ts | 20 ++-- 3 files changed, 215 insertions(+), 110 deletions(-) diff --git a/app/(tabs)/ask.tsx b/app/(tabs)/ask.tsx index b8fa3bf..05d301d 100644 --- a/app/(tabs)/ask.tsx +++ b/app/(tabs)/ask.tsx @@ -21,8 +21,7 @@ import { TouchableOpacity, View } from 'react-native'; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; -import { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function AskScreen() { @@ -30,6 +29,11 @@ export default function AskScreen() { const insets = useSafeAreaInsets(); const chatListRef = useRef(null); + const isMountedRef = useRef(true); + const scrollTimeoutRef = useRef | null>(null); + const keyboardTimeoutRef = useRef | null>(null); + const abortControllerRef = useRef(null); + const [isHello, setIsHello] = useState(true); const [conversationId, setConversationId] = useState(null); const [userMessages, setUserMessages] = useState([]); @@ -43,29 +47,49 @@ export default function AskScreen() { newSession: string; }>(); - // 创建一个可复用的滚动函数 + // 创建一个安全的滚动函数 const scrollToEnd = useCallback((animated = true) => { - if (chatListRef.current) { - setTimeout(() => chatListRef.current?.scrollToEnd({ animated }), 100); + if (!isMountedRef.current || !chatListRef.current) return; + + // 清理之前的定时器 + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + + scrollTimeoutRef.current = setTimeout(() => { + if (isMountedRef.current && chatListRef.current) { + try { + chatListRef.current.scrollToEnd({ animated }); + } catch (error) { + console.warn('滚动到底部失败:', error); + } + } + }, 100); + }, []); + + // 清理函数 + const cleanup = useCallback(() => { + isMountedRef.current = false; + + // 清理定时器 + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + scrollTimeoutRef.current = null; + } + if (keyboardTimeoutRef.current) { + clearTimeout(keyboardTimeoutRef.current); + keyboardTimeoutRef.current = null; + } + + // 取消API请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; } }, []); - // 右滑 - const gesture = Gesture.Pan() - .onEnd((event) => { - const { translationX } = event; - const threshold = 100; // 滑动阈值 - - if (translationX > threshold) { - // 从左向右滑动,跳转页面 - runOnJS(router.replace)("/memo-list"); - } - }) - .minPointers(1) - .activeOffsetX([-10, 10]); // 在 X 方向触发的范围 - useEffect(() => { - if (!isHello && userMessages.length > 0) { + if (!isHello && userMessages.length > 0 && isMountedRef.current) { scrollToEnd(); } }, [userMessages, isHello, scrollToEnd]); @@ -74,8 +98,12 @@ export default function AskScreen() { const keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', (e) => { - setTimeout(() => { - if (!isHello) { + if (keyboardTimeoutRef.current) { + clearTimeout(keyboardTimeoutRef.current); + } + + keyboardTimeoutRef.current = setTimeout(() => { + if (isMountedRef.current && !isHello) { scrollToEnd(); } }, 100); @@ -85,8 +113,12 @@ export default function AskScreen() { const keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', () => { - setTimeout(() => { - if (!isHello) { + if (keyboardTimeoutRef.current) { + clearTimeout(keyboardTimeoutRef.current); + } + + keyboardTimeoutRef.current = setTimeout(() => { + if (isMountedRef.current && !isHello) { scrollToEnd(false); } }, 100); @@ -96,27 +128,29 @@ export default function AskScreen() { return () => { keyboardDidShowListener.remove(); keyboardDidHideListener.remove(); + if (keyboardTimeoutRef.current) { + clearTimeout(keyboardTimeoutRef.current); + } }; - }, [isHello]); + }, [isHello, scrollToEnd]); useFocusEffect( useCallback(() => { - let isMounted = true; + isMountedRef.current = true; // 使用新的WebSocket订阅hook - const { subscribeToWebSocket } = useWebSocketSubscription(setUserMessages, isMounted); + const { subscribeToWebSocket } = useWebSocketSubscription(setUserMessages, isMountedRef.current); // 订阅WebSocket消息 const unsubscribe = subscribeToWebSocket(); return () => { - // 设置组件卸载标志 - isMounted = false; - // 取消订阅 unsubscribe(); + // 执行清理 + cleanup(); }; - }, [t]) + }, [t, cleanup]) ); // 创建动画样式 @@ -141,39 +175,50 @@ export default function AskScreen() { }, [isHello]); useEffect(() => { - if (sessionId) { + if (sessionId && isMountedRef.current) { setConversationId(sessionId); setIsHello(false); - fetchApi(`/chats/${sessionId}/message-history`).then((res) => { - setUserMessages(res); + + // 创建新的AbortController + abortControllerRef.current = new AbortController(); + + fetchApi(`/chats/${sessionId}/message-history`, { + signal: abortControllerRef.current.signal + }).then((res) => { + if (isMountedRef.current) { + setUserMessages(res); + } + }).catch((error) => { + if (error.name !== 'AbortError') { + console.error('获取消息历史失败:', error); + } }); } - if (newSession) { + if (newSession && isMountedRef.current) { setIsHello(true); setConversationId(null); } }, [sessionId, newSession]); useEffect(() => { - if (!isHello) { + if (!isHello && isMountedRef.current) { // 不再自动关闭键盘,让用户手动控制 // 这里可以添加其他需要在隐藏hello界面时执行的逻辑 scrollToEnd(false); } - }, [isHello]); + }, [isHello, scrollToEnd]); useFocusEffect( useCallback(() => { - if (!sessionId) { + if (!sessionId && isMountedRef.current) { setIsHello(true); - setUserMessages([]) + setUserMessages([]); } }, [sessionId]) ); return ( - - + {/* 导航栏 */} - - + ); } diff --git a/components/ask/send.tsx b/components/ask/send.tsx index 92d678b..abad7f0 100644 --- a/components/ask/send.tsx +++ b/components/ask/send.tsx @@ -31,39 +31,67 @@ const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒 export default function SendMessage(props: Props) { const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props; - const { t } = useTranslation() + const { t } = useTranslation(); // 用户询问 const [inputValue, setInputValue] = useState(''); - // 添加一个ref来跟踪键盘状态 + // 添加组件挂载状态跟踪 + const isMountedRef = useRef(true); const isKeyboardVisible = useRef(false); const chunkQueue = useRef([]); const renderInterval = useRef | null>(null); + + // 清理函数 + const cleanup = useCallback(() => { + isMountedRef.current = false; + + // 清理定时器 + if (renderInterval.current) { + clearInterval(renderInterval.current); + renderInterval.current = null; + } + + // 清理队列 + chunkQueue.current = []; + }, []); useEffect(() => { const handleChatStream = (message: WsMessage) => { - if (message.type !== 'ChatStream' || !message.chunk) return; + if (!isMountedRef.current || message.type !== 'ChatStream' || !message.chunk) return; chunkQueue.current.push(message.chunk); if (!renderInterval.current) { renderInterval.current = setInterval(() => { + if (!isMountedRef.current) { + if (renderInterval.current) { + clearInterval(renderInterval.current); + renderInterval.current = null; + } + return; + } + if (chunkQueue.current.length > 0) { const textToRender = chunkQueue.current.join(''); chunkQueue.current = []; setUserMessages(prevMessages => { - if (prevMessages.length === 0) return prevMessages; + try { + if (prevMessages.length === 0) return prevMessages; - const lastMessage = prevMessages[prevMessages.length - 1]; - if (lastMessage.role !== 'assistant') return prevMessages; + const lastMessage = prevMessages[prevMessages.length - 1]; + if (lastMessage.role !== 'assistant') return prevMessages; - const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + textToRender; + const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + textToRender; - const updatedLastMessage = { ...lastMessage, content: updatedContent }; + const updatedLastMessage = { ...lastMessage, content: updatedContent }; - return [...prevMessages.slice(0, -1), updatedLastMessage]; + return [...prevMessages.slice(0, -1), updatedLastMessage]; + } catch (error) { + console.error('处理流式消息时出错:', error); + return prevMessages; + } }); } else { if (renderInterval.current) { @@ -76,7 +104,7 @@ export default function SendMessage(props: Props) { }; const handleChatStreamEnd = (message: WsMessage) => { - if (message.type !== 'ChatStreamEnd') return; + if (!isMountedRef.current || message.type !== 'ChatStreamEnd') return; // Stop the timer and process any remaining chunks if (renderInterval.current) { @@ -88,37 +116,47 @@ export default function SendMessage(props: Props) { chunkQueue.current = []; setUserMessages(prevMessages => { - if (prevMessages.length === 0) return prevMessages; + try { + if (prevMessages.length === 0) return prevMessages; - const lastMessage = prevMessages[prevMessages.length - 1]; - if (lastMessage.role !== 'assistant') return prevMessages; + const lastMessage = prevMessages[prevMessages.length - 1]; + if (lastMessage.role !== 'assistant') return prevMessages; - // Apply remaining chunks from the queue - const contentWithQueue = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + remainingText; + // Apply remaining chunks from the queue + const contentWithQueue = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + remainingText; - // Create the final updated message object - const updatedLastMessage = { - ...lastMessage, - // Use the final message from ChatStreamEnd if available, otherwise use the content with queued text - content: message.message ? message.message.content : contentWithQueue, - timestamp: message.message ? message.message.timestamp : lastMessage.timestamp, - }; + // Create the final updated message object + const updatedLastMessage = { + ...lastMessage, + // Use the final message from ChatStreamEnd if available, otherwise use the content with queued text + content: message.message ? message.message.content : contentWithQueue, + timestamp: message.message ? message.message.timestamp : lastMessage.timestamp, + }; - return [...prevMessages.slice(0, -1), updatedLastMessage]; + return [...prevMessages.slice(0, -1), updatedLastMessage]; + } catch (error) { + console.error('处理ChatStreamEnd消息时出错:', error); + return prevMessages; + } }); }; const handleChatResponse = (message: WsMessage) => { - if (message.type === 'ChatResponse' && message.message) { - setUserMessages(prev => { - const newMessages = prev.filter(item => item.content !== 'keepSearchIng'); - return [...newMessages, { - ...(message.message as Message), - role: 'assistant', - }]; + if (!isMountedRef.current || message.type !== 'ChatResponse') return; + + if (message.message) { + setUserMessages(prevMessages => { + try { + const updatedMessages = [...prevMessages]; + updatedMessages[updatedMessages.length - 1] = message.message as Message; + return updatedMessages; + } catch (error) { + console.error('处理聊天响应时出错:', error); + return prevMessages; + } }); } - } + }; const typedHandleChatStream = handleChatStream as (message: WsMessage) => void; const typedHandleChatStreamEnd = handleChatStreamEnd as (message: WsMessage) => void; @@ -130,14 +168,21 @@ export default function SendMessage(props: Props) { webSocketManager.subscribe('ChatResponse', typedHandleChatResponse); return () => { - webSocketManager.unsubscribe('ChatStream', typedHandleChatStream); - webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd); - webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse); - if (renderInterval.current) { - clearInterval(renderInterval.current); - } + webSocketManager.unsubscribe('ChatStream', handleChatStream); + webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd); + webSocketManager.unsubscribe('ChatResponse', handleChatResponse); + + // 执行清理 + cleanup(); }; - }, [setUserMessages]); + }, [setUserMessages, cleanup]); + + // 组件卸载时的清理 + useEffect(() => { + return () => { + cleanup(); + }; + }, [cleanup]); useEffect(() => { // 使用keyboardWillShow而不是keyboardDidShow,这样可以在键盘完全显示前更新UI @@ -172,11 +217,15 @@ export default function SendMessage(props: Props) { // 发送询问 const handleSubmit = useCallback(async () => { + if (!inputValue.trim() || !isMountedRef.current) return; + const text = inputValue.trim(); - // 用户输入信息之后进行后续操作 - if (text) { - // 将用户输入信息添加到消息列表中 - setUserMessages(pre => ([...pre, { + setIsHello(false); + + // 添加用户消息和占位符助手消息 + setUserMessages(prev => [ + ...prev, + { id: Math.random().toString(36).substring(2, 9), content: text, role: 'user', @@ -188,24 +237,18 @@ export default function SendMessage(props: Props) { role: 'assistant', timestamp: new Date().toISOString() } - ])); + ]); + + try { let currentSessionId = conversationId; - // 如果没有对话ID,先创建一个新对话 if (!currentSessionId) { currentSessionId = await createNewConversation(text); - setConversationId(currentSessionId); - const webSocketManager = getWebSocketManager(); - webSocketManager.send({ - type: 'Chat', - session_id: currentSessionId, - message: text, - image_material_ids: selectedImages.length > 0 ? selectedImages : undefined, - }); - setSelectedImages([]); + if (currentSessionId && isMountedRef.current) { + setConversationId(currentSessionId); + } } - // 通过 WebSocket 发送消息 - if (currentSessionId) { + if (currentSessionId && isMountedRef.current) { const webSocketManager = getWebSocketManager(); webSocketManager.send({ type: 'Chat', @@ -217,9 +260,19 @@ export default function SendMessage(props: Props) { } else { console.error("无法获取 session_id,消息发送失败。"); // 可以在这里处理错误,例如显示一个提示 + if (isMountedRef.current) { + setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); + } + } + } catch (error) { + console.error('发送消息时出错:', error); + if (isMountedRef.current) { setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); } - // 将输入框清空 + } + + // 将输入框清空 + if (isMountedRef.current) { setInputValue(''); // 只有在键盘可见时才关闭键盘 if (isKeyboardVisible.current) { @@ -228,8 +281,10 @@ export default function SendMessage(props: Props) { } }, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]); - const handleQuitly = (type: string) => { - setIsHello(false) + const handleQuitly = useCallback((type: string) => { + if (!isMountedRef.current) return; + + setIsHello(false); setUserMessages(pre => ([ ...pre, { @@ -240,8 +295,8 @@ export default function SendMessage(props: Props) { role: 'assistant', timestamp: new Date().toISOString() } - ])) - }; + ])); + }, [t, setIsHello, setUserMessages]); return ( diff --git a/hooks/useWebSocketSubscription.ts b/hooks/useWebSocketSubscription.ts index fb3ff69..796b60a 100644 --- a/hooks/useWebSocketSubscription.ts +++ b/hooks/useWebSocketSubscription.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { getWebSocketManager, WsMessage, getWebSocketErrorMessage } from '@/lib/websocket-util'; import { Message, Assistant } from '@/types/ask'; @@ -9,10 +9,16 @@ export const useWebSocketSubscription = ( isMounted: boolean ) => { const { t } = useTranslation(); + const isMountedRef = useRef(isMounted); + + // 更新挂载状态 + useEffect(() => { + isMountedRef.current = isMounted; + }, [isMounted]); const handleChatStream = useCallback((message: WsMessage) => { // 确保组件仍然挂载 - if (!isMounted) return; + if (!isMountedRef.current) return; if (message.type === 'ChatStream') { setUserMessages(prevMessages => { @@ -71,7 +77,7 @@ export const useWebSocketSubscription = ( const handleChatStreamEnd = useCallback((message: WsMessage) => { // 确保组件仍然挂载 - if (!isMounted) return; + if (!isMountedRef.current) return; if (message.type === 'ChatStreamEnd') { setUserMessages(prevMessages => { @@ -106,7 +112,7 @@ export const useWebSocketSubscription = ( const handleError = useCallback((message: WsMessage) => { // 确保组件仍然挂载 - if (!isMounted) return; + if (!isMountedRef.current) return; if (message.type === 'Error') { console.log(`WebSocket Error: ${message.code} - ${message.message}`); @@ -139,7 +145,7 @@ export const useWebSocketSubscription = ( } }); } - }, [setUserMessages, isMounted, t]); + }, [setUserMessages, t]); const subscribeToWebSocket = useCallback(() => { const webSocketManager = getWebSocketManager(); @@ -155,8 +161,8 @@ export const useWebSocketSubscription = ( webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd); webSocketManager.unsubscribe('Error', handleError); - // 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度 - webSocketManager.disconnect(); + // 不要在这里断开连接,因为其他组件可能还在使用 + // webSocketManager.disconnect(); }; }, [handleChatStream, handleChatStreamEnd, handleError]);