Compare commits

...

48 Commits

Author SHA1 Message Date
bf12a157c5 f 2025-08-07 17:11:36 +08:00
b960ebac72 s 2025-08-07 17:00:14 +08:00
432e9b9dfb f 2025-08-07 16:55:50 +08:00
062f8f0b3a f 2025-08-07 16:53:52 +08:00
b2010ae1ce s 2025-08-07 16:36:49 +08:00
240b1ef3cc d 2025-08-07 16:15:34 +08:00
e770e412ad f 2025-08-07 16:02:45 +08:00
238e8eb79a fff 2025-08-07 13:38:27 +08:00
e7bb2e38d4 f 2025-08-07 13:29:21 +08:00
3381383e36 s 2025-08-07 12:57:31 +08:00
7af6716b4b chore 2025-08-07 12:51:01 +08:00
58a6e31111 f 2025-08-07 12:21:10 +08:00
cffa38605d d 2025-08-07 12:13:27 +08:00
048d2a905c 动画 2025-08-07 11:43:41 +08:00
c94fe202cc f 2025-08-07 11:31:26 +08:00
4f0fb96a9b f 2025-08-07 11:31:00 +08:00
326848de64 f 2025-08-07 11:21:50 +08:00
780fc293e4 xia 2025-08-07 11:15:33 +08:00
35a6f8eacb chore: 滚动useEffect 2025-08-07 11:05:52 +08:00
85a4771024 chore: 注释掉一些use effect 2025-08-07 10:57:58 +08:00
b08f972678 chore: 注视下 2025-08-07 10:53:17 +08:00
2095da05cd fix: ws api 2025-08-07 10:40:12 +08:00
81c1bf7c88 fix: 移除hello 2025-08-07 10:24:45 +08:00
d0ce370509 t 2025-08-07 01:37:02 +08:00
ed97527e2b test 2025-08-07 01:36:33 +08:00
12c3eb0901 final 2025-08-07 01:25:32 +08:00
b3b31baab2 chore: 注释websocket 2025-08-07 01:16:58 +08:00
3047215ada fix 2025-08-07 01:16:18 +08:00
918f4da40e fix 2025-08-07 01:03:38 +08:00
6ac3f69e24 refactor 2025-08-07 00:50:26 +08:00
b35bb3cbeb chore: 注释ws 2025-08-07 00:47:17 +08:00
05263afc89 fix 2025-08-07 00:46:43 +08:00
136346e189 feat: 延迟初始化 2025-08-07 00:38:36 +08:00
ac80003c5d fix: 错误处理 2025-08-07 00:21:39 +08:00
3469cfb332 fix 2025-08-07 00:17:33 +08:00
b1ba4edffc tmp 2025-08-06 20:11:40 +08:00
0e86fe5659 chore: 注释ws 2025-08-06 17:40:25 +08:00
ba494c1396 fix 2025-08-06 17:28:33 +08:00
67a71400de chore: 取消其他注释 2025-08-06 17:28:07 +08:00
7c86362830 chore: 取消hello的注释 2025-08-06 17:27:11 +08:00
685d187c02 chore: 注释ws连接逻辑 2025-08-06 17:15:20 +08:00
f370036537 chore: 注释聊天页面 2025-08-06 16:48:39 +08:00
582ed4b037 chore: 注释预加载逻辑 2025-08-06 16:34:14 +08:00
998607adc1 chore: 移除AskHello 2025-08-06 16:24:17 +08:00
5171ea8b99 fix: EXPO_PUBLIC env 2025-08-06 15:51:05 +08:00
17a3e146ce chore: 注释 2025-08-06 15:34:42 +08:00
7d59e20f39 chore: 调整 2025-08-06 15:03:46 +08:00
6b35cefbdc feat: eas update 2025-08-06 14:20:39 +08:00
15 changed files with 954 additions and 450 deletions

View File

@ -90,6 +90,10 @@
"eas": {
"projectId": "04721dd4-6b15-495a-b9ec-98187c613172"
}
},
"runtimeVersion": "1.0.0.1",
"updates": {
"url": "https://u.expo.dev/04721dd4-6b15-495a-b9ec-98187c613172"
}
}
}

View File

@ -7,7 +7,7 @@ import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { prefetchChats } from '@/lib/prefetch';
import { fetchApi } from '@/lib/server-api-util';
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
import { getWebSocketManager, WebSocketStatus } from '@/lib/websocket-util';
import { TransitionPresets } from '@react-navigation/bottom-tabs';
import * as Notifications from 'expo-notifications';
import { Tabs } from 'expo-router';
@ -15,7 +15,6 @@ import * as SecureStore from 'expo-secure-store';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform } from 'react-native';
interface PollingData {
title: string;
id: string;
@ -73,6 +72,7 @@ export default function TabLayout() {
const handleStatusChange = (status: WebSocketStatus) => {
setWsStatus(status);
};
const webSocketManager = getWebSocketManager();
webSocketManager.subscribeStatus(handleStatusChange);
return () => {
webSocketManager.unsubscribeStatus(handleStatusChange);
@ -268,7 +268,7 @@ export default function TabLayout() {
}}
/>
{/* memo list */}
<Tabs.Screen
< Tabs.Screen
name="memo-list"
options={{
title: 'memo-list',
@ -279,7 +279,7 @@ export default function TabLayout() {
}}
/>
{/* owner */}
<Tabs.Screen
< Tabs.Screen
name="owner"
options={{
title: 'owner',
@ -299,16 +299,6 @@ export default function TabLayout() {
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 对话详情页 */}
<Tabs.Screen
name="chat-details"
options={{
title: 'chat-details',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 隐私协议 */}
<Tabs.Screen
name="privacy-policy"

View File

@ -1,39 +1,41 @@
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
import Chat from "@/components/ask/chat";
import AskHello from "@/components/ask/hello";
import SendMessage from "@/components/ask/send";
import { ThemedText } from "@/components/ThemedText";
import { useWebSocketStreamHandler } from "@/hooks/useWebSocketStreamHandler";
import { fetchApi } from "@/lib/server-api-util";
import { getWebSocketErrorMessage, webSocketManager, WsMessage } from "@/lib/websocket-util";
import { Assistant, Message } from "@/types/ask";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useEffect, useRef, useState } from 'react';
import { Message } from "@/types/ask";
import { useFocusEffect, useLocalSearchParams, useRouter } from "expo-router";
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from "react-i18next";
import {
Animated,
FlatList,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { runOnJS } from 'react-native-reanimated';
import { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AskScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const chatListRef = useRef<FlatList>(null);
const isMountedRef = useRef(true);
const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const keyboardTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const [isHello, setIsHello] = useState(true);
const [conversationId, setConversationId] = useState<string | null>(null);
const [userMessages, setUserMessages] = useState<Message[]>([]);
const [selectedImages, setSelectedImages] = useState<string[]>([]);
const fadeAnim = useRef(new Animated.Value(1)).current;
const fadeAnimChat = useRef(new Animated.Value(0)).current;
const fadeAnim = useSharedValue(1);
const fadeAnimChat = useSharedValue(0);
const { t } = useTranslation();
const { sessionId, newSession } = useLocalSearchParams<{
@ -41,29 +43,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]);
@ -72,8 +94,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);
@ -83,8 +109,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);
@ -94,165 +124,107 @@ export default function AskScreen() {
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
if (keyboardTimeoutRef.current) {
clearTimeout(keyboardTimeoutRef.current);
}
};
}, [isHello, scrollToEnd]);
// 使用新的WebSocket流处理hook使用实时模式
const { subscribeToWebSocket } = useWebSocketStreamHandler({
setUserMessages,
isMounted: true, // 传递静态值hook内部会使用ref跟踪
enableBatching: false // AskScreen使用实时模式
});
// useFocusEffect(
// useCallback(() => {
// isMountedRef.current = true;
// // 订阅WebSocket消息
// const unsubscribe = subscribeToWebSocket();
// return () => {
// // 取消订阅和执行清理
// unsubscribe();
// cleanup();
// };
// }, [subscribeToWebSocket, cleanup])
// );
// 创建动画样式
const welcomeStyle = useAnimatedStyle(() => {
return {
opacity: fadeAnim.value,
pointerEvents: isHello ? 'auto' : 'none',
};
});
const chatStyle = useAnimatedStyle(() => {
return {
opacity: fadeAnimChat.value,
pointerEvents: isHello ? 'none' : 'auto',
};
});
// 触发动画
useEffect(() => {
fadeAnim.value = withTiming(isHello ? 1 : 0, { duration: 300 });
fadeAnimChat.value = withTiming(isHello ? 0 : 1, { duration: 300 });
}, [isHello]);
useFocusEffect(
useCallback(() => {
webSocketManager.connect();
const handleChatStream = (message: WsMessage) => {
if (message.type === 'ChatStream') {
setUserMessages(prevMessages => {
const newMessages = [...prevMessages];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === Assistant) {
if (typeof lastMessage.content === 'string') {
if (lastMessage.content === 'keepSearchIng') {
// 第一次收到流式消息,替换占位符
lastMessage.content = message.chunk;
} else {
// 持续追加流式消息
lastMessage.content += message.chunk;
}
} else {
// 如果 content 是数组,则更新第一个 text 部分
const textPart = lastMessage.content.find(p => p.type === 'text');
if (textPart) {
textPart.text = (textPart.text || '') + message.chunk;
}
}
}
return newMessages;
});
}
};
const handleChatStreamEnd = (message: WsMessage) => {
if (message.type === 'ChatStreamEnd') {
setUserMessages(prevMessages => {
const newMessages = [...prevMessages];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === Assistant) {
// 使用最终消息替换流式消息,确保 message.message 存在
if (message.message) {
newMessages[newMessages.length - 1] = message.message as Message;
} else {
// 如果最终消息为空,则移除 'keepSearchIng' 占位符
return prevMessages.filter(m => !(typeof m.content === 'string' && m.content === 'keepSearchIng'));
}
}
return newMessages;
});
}
};
const handleError = (message: WsMessage) => {
if (message.type === 'Error') {
console.log(`WebSocket Error: ${message.code} - ${message.message}`);
// 可以在这里添加错误提示,例如替换最后一条消息为错误信息
setUserMessages(prev => {
// 创建新的数组和新的消息对象
return prev.map((msg, index) => {
if (index === prev.length - 1 &&
typeof msg.content === 'string' &&
msg.content === 'keepSearchIng') {
// 返回新的消息对象
return {
...msg,
content: getWebSocketErrorMessage(message.code, t)
};
}
return msg;
});
});
}
};
webSocketManager.subscribe('ChatStream', handleChatStream);
webSocketManager.subscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.subscribe('Error', handleError);
return () => {
webSocketManager.unsubscribe('ChatStream', handleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('Error', handleError);
// 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度
// webSocketManager.disconnect();
};
}, [])
);
useEffect(() => {
if (sessionId) {
if (sessionId && isMountedRef.current) {
setConversationId(sessionId);
setIsHello(false);
fetchApi<Message[]>(`/chats/${sessionId}/message-history`).then((res) => {
// 创建新的AbortController
abortControllerRef.current = new AbortController();
fetchApi<Message[]>(`/chats/${sessionId}/message-history`, {
signal: abortControllerRef.current.signal
}).then((res) => {
if (isMountedRef.current) {
console.log("isMountedRef.current", 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) {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(fadeAnimChat, {
toValue: 0,
duration: 300,
useNativeDriver: true,
})
]).start();
} else {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(fadeAnimChat, {
toValue: 1,
duration: 300,
useNativeDriver: true,
})
]).start(() => {
setTimeout(() => {
if (!isHello) {
scrollToEnd(false);
}
}, 50);
});
}
}, [isHello, fadeAnim, fadeAnimChat]);
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])
);
// 组件卸载时的清理
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
return (
<GestureDetector gesture={gesture}>
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
{/* 导航栏 */}
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
@ -280,28 +252,18 @@ export default function AskScreen() {
<View style={styles.contentContainer}>
{/* 欢迎页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnim,
pointerEvents: isHello ? 'auto' : 'none',
zIndex: 1
}
]}
style={[styles.absoluteView,
welcomeStyle,
{ zIndex: 1 }]}
>
<AskHello setUserMessages={setUserMessages} setConversationId={setConversationId} setIsHello={setIsHello} />
</Animated.View>
{/* 聊天页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnimChat,
pointerEvents: isHello ? 'none' : 'auto',
zIndex: 0
}
]}
style={[styles.absoluteView,
chatStyle,
{ zIndex: 0 }]}
>
<Chat
ref={chatListRef}
@ -312,13 +274,13 @@ export default function AskScreen() {
style={styles.chatContainer}
contentContainerStyle={styles.chatContentContainer}
showsVerticalScrollIndicator={false}
onContentSizeChange={() => scrollToEnd()}
// onContentSizeChange={() => scrollToEnd()}
/>
</Animated.View>
</View>
{/* 输入框区域 */}
<KeyboardAvoidingView
{/* <KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={0} >
<View style={styles.inputContainer} key={conversationId}>
@ -331,9 +293,8 @@ export default function AskScreen() {
setSelectedImages={setSelectedImages}
/>
</View>
</KeyboardAvoidingView>
</View >
</GestureDetector>
</KeyboardAvoidingView> */}
</View>
);
}

View File

@ -225,7 +225,7 @@ export default function HomeScreen() {
useEffect(() => {
setIsLoading(true);
checkAuthStatus(router, () => {
router.replace('/ask')
router.replace('/memo-list') // TODO FIXME
}, false).then(() => {
setIsLoading(false);
}).catch(() => {

View File

@ -1,6 +1,6 @@
import IP from "@/assets/icons/svg/ip.svg";
import { ThemedText } from "@/components/ThemedText";
import { webSocketManager } from "@/lib/websocket-util";
import { getWebSocketManager } from "@/lib/websocket-util";
import { Message } from "@/types/ask";
import { Dispatch, SetStateAction } from "react";
import { useTranslation } from "react-i18next";
@ -12,6 +12,7 @@ interface AskHelloProps {
setConversationId: Dispatch<SetStateAction<string | null>>;
setIsHello: Dispatch<SetStateAction<boolean>>;
}
export default function AskHello({ setUserMessages, setConversationId, setIsHello }: AskHelloProps) {
const { t } = useTranslation();
@ -35,6 +36,7 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell
const sessionId = await createNewConversation(text);
if (sessionId) {
setConversationId(sessionId);
const webSocketManager = getWebSocketManager();
webSocketManager.send({
type: 'Chat',
session_id: sessionId,

View File

@ -12,7 +12,8 @@ import {
View
} from 'react-native';
import { webSocketManager, WsMessage } from '@/lib/websocket-util';
import { useWebSocketStreamHandler } from '@/hooks/useWebSocketStreamHandler';
import { getWebSocketManager } from '@/lib/websocket-util';
import { Message } from '@/types/ask';
import { useTranslation } from 'react-i18next';
import { ThemedText } from '../ThemedText';
@ -31,112 +32,39 @@ 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<string[]>([]);
const renderInterval = useRef<ReturnType<typeof setInterval> | null>(null);
// 使用新的WebSocket流处理hook启用批量处理模式
const { subscribeToWebSocket, cleanup } = useWebSocketStreamHandler({
setUserMessages,
isMounted: true, // 传递静态值hook内部会使用ref跟踪
enableBatching: true,
renderInterval: RENDER_INTERVAL
});
// 使用WebSocket订阅
useEffect(() => {
const handleChatStream = (message: WsMessage) => {
if (message.type !== 'ChatStream' || !message.chunk) return;
chunkQueue.current.push(message.chunk);
if (!renderInterval.current) {
renderInterval.current = setInterval(() => {
if (chunkQueue.current.length > 0) {
const textToRender = chunkQueue.current.join('');
chunkQueue.current = [];
setUserMessages(prevMessages => {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== 'assistant') return prevMessages;
const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + textToRender;
const updatedLastMessage = { ...lastMessage, content: updatedContent };
return [...prevMessages.slice(0, -1), updatedLastMessage];
});
} else {
if (renderInterval.current) {
clearInterval(renderInterval.current);
renderInterval.current = null;
}
}
}, RENDER_INTERVAL);
}
};
const handleChatStreamEnd = (message: WsMessage) => {
if (message.type !== 'ChatStreamEnd') return;
// Stop the timer and process any remaining chunks
if (renderInterval.current) {
clearInterval(renderInterval.current);
renderInterval.current = null;
}
const remainingText = chunkQueue.current.join('');
chunkQueue.current = [];
setUserMessages(prevMessages => {
if (prevMessages.length === 0) 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;
// 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];
});
};
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',
}];
});
}
}
const typedHandleChatStream = handleChatStream as (message: WsMessage) => void;
const typedHandleChatStreamEnd = handleChatStreamEnd as (message: WsMessage) => void;
const typedHandleChatResponse = handleChatResponse as (message: WsMessage) => void;
webSocketManager.subscribe('ChatStream', typedHandleChatStream);
webSocketManager.subscribe('ChatStreamEnd', typedHandleChatStreamEnd);
webSocketManager.subscribe('ChatResponse', typedHandleChatResponse);
const unsubscribe = subscribeToWebSocket();
return () => {
webSocketManager.unsubscribe('ChatStream', typedHandleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd);
webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse);
if (renderInterval.current) {
clearInterval(renderInterval.current);
}
unsubscribe();
};
}, [setUserMessages]);
}, [subscribeToWebSocket]);
// 组件卸载时的清理
useEffect(() => {
return () => {
isMountedRef.current = false;
cleanup();
};
}, [cleanup]);
useEffect(() => {
// 使用keyboardWillShow而不是keyboardDidShow这样可以在键盘完全显示前更新UI
@ -171,11 +99,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',
@ -187,23 +119,19 @@ export default function SendMessage(props: Props) {
role: 'assistant',
timestamp: new Date().toISOString()
}
]));
]);
try {
let currentSessionId = conversationId;
// 如果没有对话ID先创建一个新对话
if (!currentSessionId) {
currentSessionId = await createNewConversation(text);
if (currentSessionId && isMountedRef.current) {
setConversationId(currentSessionId);
webSocketManager.send({
type: 'Chat',
session_id: currentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
}
}
// 通过 WebSocket 发送消息
if (currentSessionId) {
if (currentSessionId && isMountedRef.current) {
const webSocketManager = getWebSocketManager();
webSocketManager.send({
type: 'Chat',
session_id: currentSessionId,
@ -214,9 +142,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) {
@ -225,8 +163,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,
{
@ -237,8 +177,8 @@ export default function SendMessage(props: Props) {
role: 'assistant',
timestamp: new Date().toISOString()
}
]))
};
]));
}, [t, setIsHello, setUserMessages]);
return (
<View style={styles.container}>

View File

@ -4,7 +4,7 @@ import PersonInSvg from "@/assets/icons/svg/personIn.svg";
import PersonNotInSvg from "@/assets/icons/svg/personNotIn.svg";
import { WebSocketStatus } from "@/lib/websocket-util";
import { router, usePathname } from "expo-router";
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Dimensions, Image, StyleSheet, TouchableOpacity, View } from 'react-native';
import Svg, { Circle, Ellipse, G, Mask, Path, Rect } from "react-native-svg";
@ -65,20 +65,20 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
}, [wsStatus]);
// 预加载目标页面
useEffect(() => {
const preloadPages = async () => {
try {
await Promise.all([
router.prefetch('/memo-list'),
router.prefetch('/ask'),
router.prefetch('/owner')
]);
} catch (error) {
console.warn('预加载页面失败:', error);
}
};
preloadPages();
}, []);
// useEffect(() => {
// const preloadPages = async () => {
// try {
// await Promise.all([
// router.prefetch('/memo-list'),
// router.prefetch('/ask'),
// router.prefetch('/owner')
// ]);
// } catch (error) {
// console.warn('预加载页面失败:', error);
// }
// };
// preloadPages();
// }, []);
// 使用 useCallback 缓存导航函数
const navigateTo = useCallback((route: string) => {

View File

@ -2,9 +2,9 @@ import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
import { ActivityIndicator, Alert, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
import { useAuth } from "../../contexts/auth-context";
import { fetchApi } from "../../lib/server-api-util";
import { API_ENDPOINT, fetchApi } from "../../lib/server-api-util";
import { User } from "../../types/user";
import { ThemedText } from "../ThemedText";
@ -52,6 +52,7 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
router.replace('/user-message');
}
} catch (error) {
Alert.alert("API_ENDPOINT", API_ENDPOINT);
const errorMessage = error instanceof Error ? error.message : t('auth.login.loginError', { ns: 'login' });
setError(errorMessage);
} finally {

View File

@ -6,13 +6,16 @@
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal"
"distribution": "internal",
"channel": "preview"
},
"production": {
"autoIncrement": true
"autoIncrement": true,
"channel": "release"
}
},
"submit": {

View File

@ -0,0 +1,273 @@
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';
import { Dispatch, SetStateAction } from 'react';
interface UseWebSocketStreamHandlerOptions {
setUserMessages: Dispatch<SetStateAction<Message[]>>;
isMounted: boolean;
enableBatching?: boolean; // 是否启用批量处理
renderInterval?: number; // 渲染间隔默认50ms
}
export const useWebSocketStreamHandler = ({
setUserMessages,
isMounted,
enableBatching = false,
renderInterval = 50
}: UseWebSocketStreamHandlerOptions) => {
const { t } = useTranslation();
const isMountedRef = useRef(isMounted);
// 批量处理相关的refs
const chunkQueue = useRef<string[]>([]);
const renderIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// 更新挂载状态
useEffect(() => {
isMountedRef.current = isMounted;
}, [isMounted]);
// 清理函数
const cleanup = useCallback(() => {
if (renderIntervalRef.current) {
clearInterval(renderIntervalRef.current);
renderIntervalRef.current = null;
}
chunkQueue.current = [];
}, []);
// 批量处理流式消息的函数
const processBatchedChunks = useCallback(() => {
if (!isMountedRef.current) {
cleanup();
return;
}
if (chunkQueue.current.length > 0) {
const textToRender = chunkQueue.current.join('');
chunkQueue.current = [];
setUserMessages(prevMessages => {
try {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== Assistant) return prevMessages;
const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content as string) + textToRender;
const updatedLastMessage = { ...lastMessage, content: updatedContent };
return [...prevMessages.slice(0, -1), updatedLastMessage];
} catch (error) {
console.error('处理批量流式消息时出错:', error);
return prevMessages;
}
});
} else {
cleanup();
}
}, [setUserMessages, cleanup]);
const handleChatStream = useCallback((message: WsMessage) => {
if (!isMountedRef.current || message.type !== 'ChatStream' || !message.chunk) return;
if (enableBatching) {
// 批量处理模式
chunkQueue.current.push(message.chunk);
if (!renderIntervalRef.current) {
renderIntervalRef.current = setInterval(processBatchedChunks, renderInterval);
}
} else {
// 实时处理模式(原有逻辑)
setUserMessages(prevMessages => {
try {
const lastMessage = prevMessages[prevMessages.length - 1];
if (!lastMessage || lastMessage.role !== Assistant) {
return prevMessages;
}
const newMessages = [...prevMessages];
if (typeof lastMessage.content === 'string') {
if (lastMessage.content === 'keepSearchIng') {
newMessages[newMessages.length - 1] = {
...lastMessage,
content: message.chunk
};
} else {
newMessages[newMessages.length - 1] = {
...lastMessage,
content: lastMessage.content + message.chunk
};
}
} else if (Array.isArray(lastMessage.content)) {
const textPartIndex = lastMessage.content.findIndex(p => p.type === 'text');
if (textPartIndex !== -1) {
const updatedContent = [...lastMessage.content];
updatedContent[textPartIndex] = {
...updatedContent[textPartIndex],
text: (updatedContent[textPartIndex].text || '') + message.chunk
};
newMessages[newMessages.length - 1] = {
...lastMessage,
content: updatedContent
};
}
}
return newMessages;
} catch (error) {
console.error('处理 ChatStream 消息时出错:', error);
return prevMessages;
}
});
}
}, [setUserMessages, enableBatching, processBatchedChunks, renderInterval]);
const handleChatStreamEnd = useCallback((message: WsMessage) => {
if (!isMountedRef.current || message.type !== 'ChatStreamEnd') return;
// 如果是批量模式先处理剩余的chunks
if (enableBatching) {
cleanup();
const remainingText = chunkQueue.current.join('');
chunkQueue.current = [];
setUserMessages(prevMessages => {
try {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== Assistant) return prevMessages;
const contentWithQueue = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content as string) + remainingText;
const updatedLastMessage = {
...lastMessage,
content: message.message ? message.message.content : contentWithQueue,
timestamp: message.message ? message.message.timestamp : lastMessage.timestamp,
};
return [...prevMessages.slice(0, -1), updatedLastMessage];
} catch (error) {
console.error('处理ChatStreamEnd消息时出错:', error);
return prevMessages;
}
});
} else {
// 实时模式的处理逻辑
setUserMessages(prevMessages => {
try {
const lastMessage = prevMessages[prevMessages.length - 1];
if (!lastMessage || lastMessage.role !== Assistant) {
return prevMessages;
}
if (message.message) {
const newMessages = [...prevMessages];
newMessages[newMessages.length - 1] = message.message as Message;
return newMessages;
} else {
return prevMessages.filter(m =>
!(typeof m.content === 'string' && m.content === 'keepSearchIng')
);
}
} catch (error) {
console.error('处理 ChatStreamEnd 消息时出错:', error);
return prevMessages;
}
});
}
}, [setUserMessages, enableBatching, cleanup]);
const handleChatResponse = useCallback((message: WsMessage) => {
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;
}
});
}
}, [setUserMessages]);
const handleError = useCallback((message: WsMessage) => {
if (!isMountedRef.current || message.type !== 'Error') return;
console.log(`WebSocket Error: ${message.code} - ${message.message}`);
setUserMessages(prevMessages => {
try {
const lastMessage = prevMessages[prevMessages.length - 1];
if (!lastMessage ||
lastMessage.role !== Assistant ||
typeof lastMessage.content !== 'string' ||
lastMessage.content !== 'keepSearchIng') {
return prevMessages;
}
const newMessages = [...prevMessages];
newMessages[newMessages.length - 1] = {
...lastMessage,
content: getWebSocketErrorMessage(message.code, t)
};
return newMessages;
} catch (error) {
console.error('处理 Error 消息时出错:', error);
return prevMessages;
}
});
}, [setUserMessages, t]);
const subscribeToWebSocket = useCallback(() => {
const webSocketManager = getWebSocketManager();
webSocketManager.connect();
webSocketManager.subscribe('ChatStream', handleChatStream);
webSocketManager.subscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.subscribe('ChatResponse', handleChatResponse);
webSocketManager.subscribe('Error', handleError);
return () => {
// 清理订阅
webSocketManager.unsubscribe('ChatStream', handleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('ChatResponse', handleChatResponse);
webSocketManager.unsubscribe('Error', handleError);
// 清理批量处理资源
cleanup();
};
}, [handleChatStream, handleChatStreamEnd, handleChatResponse, handleError, cleanup]);
// 组件卸载时的清理
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
return {
subscribeToWebSocket,
handleChatStream,
handleChatStreamEnd,
handleChatResponse,
handleError,
cleanup
};
};

View File

@ -0,0 +1,175 @@
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';
import { Dispatch, SetStateAction } from 'react';
export const useWebSocketSubscription = (
setUserMessages: Dispatch<SetStateAction<Message[]>>,
isMounted: boolean
) => {
const { t } = useTranslation();
const isMountedRef = useRef(isMounted);
// 更新挂载状态
useEffect(() => {
isMountedRef.current = isMounted;
}, [isMounted]);
const handleChatStream = useCallback((message: WsMessage) => {
// 确保组件仍然挂载
if (!isMountedRef.current) return;
if (message.type === 'ChatStream') {
setUserMessages(prevMessages => {
// 使用 try-catch 包装以防止错误导致应用崩溃
try {
const lastMessage = prevMessages[prevMessages.length - 1];
// 检查是否是有效的助手消息
if (!lastMessage || lastMessage.role !== Assistant) {
return prevMessages;
}
// 创建新的消息数组
const newMessages = [...prevMessages];
if (typeof lastMessage.content === 'string') {
if (lastMessage.content === 'keepSearchIng') {
// 第一次收到流式消息,替换占位符
newMessages[newMessages.length - 1] = {
...lastMessage,
content: message.chunk
};
} else {
// 持续追加流式消息
newMessages[newMessages.length - 1] = {
...lastMessage,
content: lastMessage.content + message.chunk
};
}
} else if (Array.isArray(lastMessage.content)) {
// 如果 content 是数组,则更新第一个 text 部分
const textPartIndex = lastMessage.content.findIndex(p => p.type === 'text');
if (textPartIndex !== -1) {
const updatedContent = [...lastMessage.content];
updatedContent[textPartIndex] = {
...updatedContent[textPartIndex],
text: (updatedContent[textPartIndex].text || '') + message.chunk
};
newMessages[newMessages.length - 1] = {
...lastMessage,
content: updatedContent
};
}
}
return newMessages;
} catch (error) {
console.error('处理 ChatStream 消息时出错:', error);
// 发生错误时返回原始消息数组
return prevMessages;
}
});
}
}, [setUserMessages, isMounted]);
const handleChatStreamEnd = useCallback((message: WsMessage) => {
// 确保组件仍然挂载
if (!isMountedRef.current) return;
if (message.type === 'ChatStreamEnd') {
setUserMessages(prevMessages => {
// 使用 try-catch 包装以防止错误导致应用崩溃
try {
const lastMessage = prevMessages[prevMessages.length - 1];
// 检查是否是有效的助手消息
if (!lastMessage || lastMessage.role !== Assistant) {
return prevMessages;
}
// 使用最终消息替换流式消息,确保 message.message 存在
if (message.message) {
const newMessages = [...prevMessages];
newMessages[newMessages.length - 1] = message.message as Message;
return newMessages;
} else {
// 如果最终消息为空,则移除 'keepSearchIng' 占位符
return prevMessages.filter(m =>
!(typeof m.content === 'string' && m.content === 'keepSearchIng')
);
}
} catch (error) {
console.error('处理 ChatStreamEnd 消息时出错:', error);
// 发生错误时返回原始消息数组
return prevMessages;
}
});
}
}, [setUserMessages, isMounted]);
const handleError = useCallback((message: WsMessage) => {
// 确保组件仍然挂载
if (!isMountedRef.current) return;
if (message.type === 'Error') {
console.log(`WebSocket Error: ${message.code} - ${message.message}`);
setUserMessages(prevMessages => {
// 使用 try-catch 包装以防止错误导致应用崩溃
try {
const lastMessage = prevMessages[prevMessages.length - 1];
// 检查是否是有效的助手消息且包含占位符
if (!lastMessage ||
lastMessage.role !== Assistant ||
typeof lastMessage.content !== 'string' ||
lastMessage.content !== 'keepSearchIng') {
return prevMessages;
}
// 替换占位符为错误消息
const newMessages = [...prevMessages];
newMessages[newMessages.length - 1] = {
...lastMessage,
content: getWebSocketErrorMessage(message.code, t)
};
return newMessages;
} catch (error) {
console.error('处理 Error 消息时出错:', error);
// 发生错误时返回原始消息数组
return prevMessages;
}
});
}
}, [setUserMessages, t]);
const subscribeToWebSocket = useCallback(() => {
const webSocketManager = getWebSocketManager();
webSocketManager.connect();
webSocketManager.subscribe('ChatStream', handleChatStream);
webSocketManager.subscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.subscribe('Error', handleError);
return () => {
// 清理订阅
webSocketManager.unsubscribe('ChatStream', handleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('Error', handleError);
// 不要在这里断开连接,因为其他组件可能还在使用
// webSocketManager.disconnect();
};
}, [handleChatStream, handleChatStreamEnd, handleError]);
return {
subscribeToWebSocket,
handleChatStream,
handleChatStreamEnd,
handleError
};
};

View File

@ -24,8 +24,8 @@ export interface PagedResult<T> {
}
// 获取.env文件中的变量
export const API_ENDPOINT = Constants.expoConfig?.extra?.API_ENDPOINT || "http://192.168.31.16:31646/api";
// API 端点 - 从环境变量或 app.json 中获取
export const API_ENDPOINT = process.env.EXPO_PUBLIC_API_ENDPOINT || Constants.expoConfig?.extra?.API_ENDPOINT;
// 更新 access_token 的逻辑 - 用于React组件中

View File

@ -4,7 +4,7 @@ import { TFunction } from 'i18next';
import { Platform } from 'react-native';
// 从环境变量或默认值中定义 WebSocket 端点
export const WEBSOCKET_ENDPOINT = Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT || "ws://192.168.31.16:31646/ws/chat";
export const WEBSOCKET_ENDPOINT = process.env.EXPO_PUBLIC_WEBSOCKET_ENDPOINT || Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT;
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
@ -51,20 +51,25 @@ class WebSocketManager {
* 使 token
*/
public async connect() {
// 防止重复连接
if (this.ws && (this.status === 'connected' || this.status === 'connecting')) {
if (this.status === 'connected' || this.status === 'connecting') {
return;
}
}
this.setStatus('connecting');
let token = "";
try {
if (Platform.OS === 'web') {
token = localStorage.getItem('token') || "";
} else {
token = await SecureStore.getItemAsync('token') || "";
}
} catch (error) {
console.error('获取认证 token 时出错:', error);
this.setStatus('disconnected');
return;
}
if (!token) {
console.error('WebSocket: 未找到认证 token无法连接。');
@ -74,9 +79,23 @@ class WebSocketManager {
console.log('WebSocket: 认证 token:', token);
}
// 检查 WebSocket 端点是否已定义
if (!WEBSOCKET_ENDPOINT) {
console.error('WebSocket: 未定义端点 URL。');
this.setStatus('disconnected');
return;
}
const url = `${WEBSOCKET_ENDPOINT}?token=${token}`;
console.log('WebSocket: 连接 URL:', url);
try {
this.ws = new WebSocket(url);
} catch (error) {
console.error('创建 WebSocket 连接时出错:', error);
this.setStatus('disconnected');
return;
}
this.ws.onopen = () => {
console.log('WebSocket connected');
@ -87,12 +106,24 @@ class WebSocketManager {
this.ws.onmessage = (event) => {
try {
// 检查事件数据是否存在
if (!event.data) {
console.warn('WebSocket received empty message');
return;
}
const message: WsMessage = JSON.parse(event.data);
// console.log('WebSocket received message:', message)
// 根据消息类型分发
const eventListeners = this.messageListeners.get(message.type);
if (eventListeners) {
eventListeners.forEach(callback => callback(message));
eventListeners.forEach(callback => {
try {
callback(message);
} catch (error) {
console.error(`处理消息类型 ${message.type} 时出错:`, error);
}
});
}
// 可以在这里处理通用的消息,比如 Pong
if (message.type === 'Pong') {
@ -104,7 +135,9 @@ class WebSocketManager {
};
this.ws.onerror = (error) => {
console.error('WebSocket 发生错误:', error);
console.error('WebSocket error:', error);
this.setStatus('disconnected');
this.handleReconnect();
};
this.ws.onclose = () => {
@ -123,6 +156,7 @@ class WebSocketManager {
* 使退
*/
private handleReconnect() {
try {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
@ -134,6 +168,10 @@ class WebSocketManager {
console.error('WebSocket 重连失败,已达到最大尝试次数。');
this.setStatus('disconnected');
}
} catch (error) {
console.error('处理 WebSocket 重连时出错:', error);
this.setStatus('disconnected');
}
}
/**
@ -145,7 +183,13 @@ class WebSocketManager {
console.error('WebSocket 未连接,无法发送消息。');
return;
}
this.ws.send(JSON.stringify(message));
try {
const messageString = JSON.stringify(message);
this.ws.send(messageString);
} catch (error) {
console.error('发送 WebSocket 消息时出错:', error);
}
}
/**
@ -154,10 +198,14 @@ class WebSocketManager {
* @param callback
*/
public subscribe(type: WsMessage['type'], callback: (message: WsMessage) => void) {
try {
if (!this.messageListeners.has(type)) {
this.messageListeners.set(type, new Set());
}
this.messageListeners.get(type)?.add(callback);
} catch (error) {
console.error(`订阅消息类型 ${type} 时出错:`, error);
}
}
/**
@ -166,6 +214,7 @@ class WebSocketManager {
* @param callback
*/
public unsubscribe(type: WsMessage['type'], callback: (message: WsMessage) => void) {
try {
const eventListeners = this.messageListeners.get(type);
if (eventListeners) {
eventListeners.delete(callback);
@ -173,60 +222,104 @@ class WebSocketManager {
this.messageListeners.delete(type);
}
}
} catch (error) {
console.error(`取消订阅消息类型 ${type} 时出错:`, error);
}
}
/**
* WebSocket
*/
public disconnect() {
try {
this.setStatus('disconnected');
if (this.ws) {
this.ws.close();
}
} catch (error) {
console.error('断开 WebSocket 连接时出错:', error);
} finally {
this.stopPing();
}
}
private setStatus(status: WebSocketStatus) {
try {
if (this.status !== status) {
this.status = status;
this.statusListeners.forEach(listener => listener(status));
this.statusListeners.forEach(listener => {
try {
listener(status);
} catch (error) {
console.error('调用状态监听器时出错:', error);
}
});
}
} catch (error) {
console.error('设置 WebSocket 状态时出错:', error);
}
}
public subscribeStatus(listener: StatusListener) {
try {
this.statusListeners.add(listener);
// Immediately invoke with current status
try {
listener(this.status);
} catch (error) {
console.error('调用状态监听器时出错:', error);
}
} catch (error) {
console.error('订阅状态监听器时出错:', error);
}
}
public unsubscribeStatus(listener: StatusListener) {
try {
this.statusListeners.delete(listener);
} catch (error) {
console.error('取消订阅状态监听器时出错:', error);
}
}
/**
*
*/
private startPing() {
try {
this.stopPing(); // 先停止任何可能正在运行的计时器
this.pingIntervalId = setInterval(() => {
this.send({ type: 'Ping' });
}, this.pingInterval);
} catch (error) {
console.error('启动心跳机制时出错:', error);
}
}
/**
*
*/
private stopPing() {
try {
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
}
} catch (error) {
console.error('停止心跳机制时出错:', error);
}
}
}
// 导出一个单例,确保整个应用共享同一个 WebSocket 连接
export const webSocketManager = new WebSocketManager();
let webSocketManagerInstance: WebSocketManager | null = null;
export const getWebSocketManager = (): WebSocketManager => {
if (!webSocketManagerInstance) {
webSocketManagerInstance = new WebSocketManager();
}
return webSocketManagerInstance;
};
// webscoket 错误映射
export const getWebSocketErrorMessage = (key: string, t: TFunction) => {

60
package-lock.json generated
View File

@ -34,6 +34,7 @@
"expo-iap": "^2.7.5",
"expo-image-manipulator": "~13.1.7",
"expo-image-picker": "~16.1.4",
"expo-insights": "~0.9.3",
"expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.7",
"expo-localization": "^16.1.5",
@ -48,6 +49,7 @@
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.9",
"expo-task-manager": "^13.1.6",
"expo-updates": "~0.28.17",
"expo-video": "~2.2.2",
"expo-video-thumbnails": "~9.1.3",
"expo-web-browser": "~14.2.0",
@ -9062,6 +9064,12 @@
"node": "*"
}
},
"node_modules/expo-eas-client": {
"version": "0.14.4",
"resolved": "https://registry.npmmirror.com/expo-eas-client/-/expo-eas-client-0.14.4.tgz",
"integrity": "sha512-TSL1BbBFIuXchJmPgbPnB7cGpOOuSGJcQ/L7gij/+zPjExwvKm5ckA5dlSulwoFhH8zQt4vb7bfISPSAWQVWBw==",
"license": "MIT"
},
"node_modules/expo-file-system": {
"version": "18.1.11",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz",
@ -9138,6 +9146,18 @@
"expo": "*"
}
},
"node_modules/expo-insights": {
"version": "0.9.3",
"resolved": "https://registry.npmmirror.com/expo-insights/-/expo-insights-0.9.3.tgz",
"integrity": "sha512-ictylDUdERHPXUM4suEYLJGGvlSOB7btDSA0FtlZeVWWOcyTWQlSF5t1Wj3lCCljzjMm/pIj8k9hp8CXCl0gsg==",
"license": "MIT",
"dependencies": {
"expo-eas-client": "~0.14.3"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-json-utils": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
@ -9374,6 +9394,12 @@
"react-native": "*"
}
},
"node_modules/expo-structured-headers": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/expo-structured-headers/-/expo-structured-headers-4.1.0.tgz",
"integrity": "sha512-2X+aUNzC/qaw7/WyUhrVHNDB0uQ5rE12XA2H/rJXaAiYQSuOeU90ladaN0IJYV9I2XlhYrjXLktLXWbO7zgbag==",
"license": "MIT"
},
"node_modules/expo-symbols": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-0.4.5.tgz",
@ -9420,6 +9446,34 @@
"react-native": "*"
}
},
"node_modules/expo-updates": {
"version": "0.28.17",
"resolved": "https://registry.npmmirror.com/expo-updates/-/expo-updates-0.28.17.tgz",
"integrity": "sha512-OiKDrKk6EoBRP9AoK7/4tyj9lVtHw2IfaETIFeUCHMgx5xjgKGX/jjSwqhk8N9BJgLDIy0oD0Sb0MaEbSBb3lg==",
"license": "MIT",
"dependencies": {
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "~11.0.13",
"@expo/config-plugins": "~10.1.2",
"@expo/spawn-async": "^1.7.2",
"arg": "4.1.0",
"chalk": "^4.1.2",
"expo-eas-client": "~0.14.4",
"expo-manifests": "~0.16.6",
"expo-structured-headers": "~4.1.0",
"expo-updates-interface": "~1.1.0",
"glob": "^10.4.2",
"ignore": "^5.3.1",
"resolve-from": "^5.0.0"
},
"bin": {
"expo-updates": "bin/cli.js"
},
"peerDependencies": {
"expo": "*",
"react": "*"
}
},
"node_modules/expo-updates-interface": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz",
@ -9429,6 +9483,12 @@
"expo": "*"
}
},
"node_modules/expo-updates/node_modules/arg": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.0.tgz",
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==",
"license": "MIT"
},
"node_modules/expo-video": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/expo-video/-/expo-video-2.2.2.tgz",

View File

@ -87,7 +87,9 @@
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5",
"react-redux": "^9.2.0",
"worklet": "^1.0.3"
"worklet": "^1.0.3",
"expo-insights": "~0.9.3",
"expo-updates": "~0.28.17"
},
"devDependencies": {
"@babel/core": "^7.25.2",