Compare commits
48 Commits
main
...
feat/eas_u
| Author | SHA1 | Date | |
|---|---|---|---|
| bf12a157c5 | |||
| b960ebac72 | |||
| 432e9b9dfb | |||
| 062f8f0b3a | |||
| b2010ae1ce | |||
| 240b1ef3cc | |||
| e770e412ad | |||
| 238e8eb79a | |||
| e7bb2e38d4 | |||
| 3381383e36 | |||
| 7af6716b4b | |||
| 58a6e31111 | |||
| cffa38605d | |||
| 048d2a905c | |||
| c94fe202cc | |||
| 4f0fb96a9b | |||
| 326848de64 | |||
| 780fc293e4 | |||
| 35a6f8eacb | |||
| 85a4771024 | |||
| b08f972678 | |||
| 2095da05cd | |||
| 81c1bf7c88 | |||
| d0ce370509 | |||
| ed97527e2b | |||
| 12c3eb0901 | |||
| b3b31baab2 | |||
| 3047215ada | |||
| 918f4da40e | |||
| 6ac3f69e24 | |||
| b35bb3cbeb | |||
| 05263afc89 | |||
| 136346e189 | |||
| ac80003c5d | |||
| 3469cfb332 | |||
| b1ba4edffc | |||
| 0e86fe5659 | |||
| ba494c1396 | |||
| 67a71400de | |||
| 7c86362830 | |||
| 685d187c02 | |||
| f370036537 | |||
| 582ed4b037 | |||
| 998607adc1 | |||
| 5171ea8b99 | |||
| 17a3e146ce | |||
| 7d59e20f39 | |||
| 6b35cefbdc |
6
app.json
6
app.json
@ -46,7 +46,7 @@
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
[
|
||||
[
|
||||
"expo-background-task",
|
||||
{
|
||||
"minimumInterval": 15
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,246 +124,177 @@ 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) => {
|
||||
setUserMessages(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]}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => {
|
||||
try {
|
||||
if (TextInput.State?.currentlyFocusedInput) {
|
||||
const input = TextInput.State.currentlyFocusedInput();
|
||||
if (input) input.blur();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('失去焦点失败:', error);
|
||||
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
||||
{/* 导航栏 */}
|
||||
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => {
|
||||
try {
|
||||
if (TextInput.State?.currentlyFocusedInput) {
|
||||
const input = TextInput.State.currentlyFocusedInput();
|
||||
if (input) input.blur();
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
router.push('/memo-list');
|
||||
}}
|
||||
>
|
||||
<ReturnArrow />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.title} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
|
||||
<View style={styles.placeholder} />
|
||||
} catch (error) {
|
||||
console.log('失去焦点失败:', error);
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
router.push('/memo-list');
|
||||
}}
|
||||
>
|
||||
<ReturnArrow />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.title} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
{/* 欢迎页面 */}
|
||||
<Animated.View
|
||||
style={[styles.absoluteView,
|
||||
welcomeStyle,
|
||||
{ zIndex: 1 }]}
|
||||
>
|
||||
<AskHello setUserMessages={setUserMessages} setConversationId={setConversationId} setIsHello={setIsHello} />
|
||||
</Animated.View>
|
||||
|
||||
{/* 聊天页面 */}
|
||||
<Animated.View
|
||||
style={[styles.absoluteView,
|
||||
chatStyle,
|
||||
{ zIndex: 0 }]}
|
||||
>
|
||||
<Chat
|
||||
ref={chatListRef}
|
||||
userMessages={userMessages}
|
||||
sessionId={sessionId}
|
||||
setSelectedImages={setSelectedImages}
|
||||
selectedImages={selectedImages}
|
||||
style={styles.chatContainer}
|
||||
contentContainerStyle={styles.chatContentContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
// onContentSizeChange={() => scrollToEnd()}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* 输入框区域 */}
|
||||
{/* <KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={0} >
|
||||
<View style={styles.inputContainer} key={conversationId}>
|
||||
<SendMessage
|
||||
setIsHello={setIsHello}
|
||||
conversationId={conversationId}
|
||||
setConversationId={setConversationId}
|
||||
setUserMessages={setUserMessages}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
{/* 欢迎页面 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.absoluteView,
|
||||
{
|
||||
opacity: fadeAnim,
|
||||
pointerEvents: isHello ? 'auto' : 'none',
|
||||
zIndex: 1
|
||||
}
|
||||
]}
|
||||
>
|
||||
<AskHello setUserMessages={setUserMessages} setConversationId={setConversationId} setIsHello={setIsHello} />
|
||||
</Animated.View>
|
||||
|
||||
{/* 聊天页面 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.absoluteView,
|
||||
{
|
||||
opacity: fadeAnimChat,
|
||||
pointerEvents: isHello ? 'none' : 'auto',
|
||||
zIndex: 0
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Chat
|
||||
ref={chatListRef}
|
||||
userMessages={userMessages}
|
||||
sessionId={sessionId}
|
||||
setSelectedImages={setSelectedImages}
|
||||
selectedImages={selectedImages}
|
||||
style={styles.chatContainer}
|
||||
contentContainerStyle={styles.chatContentContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onContentSizeChange={() => scrollToEnd()}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* 输入框区域 */}
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={0} >
|
||||
<View style={styles.inputContainer} key={conversationId}>
|
||||
<SendMessage
|
||||
setIsHello={setIsHello}
|
||||
conversationId={conversationId}
|
||||
setConversationId={setConversationId}
|
||||
setUserMessages={setUserMessages}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View >
|
||||
</GestureDetector>
|
||||
</KeyboardAvoidingView> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
setConversationId(currentSessionId);
|
||||
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',
|
||||
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}>
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
9
eas.json
9
eas.json
@ -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": {
|
||||
|
||||
273
hooks/useWebSocketStreamHandler.ts
Normal file
273
hooks/useWebSocketStreamHandler.ts
Normal 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
|
||||
};
|
||||
};
|
||||
175
hooks/useWebSocketSubscription.ts
Normal file
175
hooks/useWebSocketSubscription.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@ -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组件中
|
||||
|
||||
@ -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,19 +51,24 @@ class WebSocketManager {
|
||||
* 会自动获取并使用存储的认证 token。
|
||||
*/
|
||||
public async connect() {
|
||||
// 防止重复连接
|
||||
if (this.ws && (this.status === 'connected' || this.status === 'connecting')) {
|
||||
if (this.status === 'connected' || this.status === 'connecting') {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus('connecting');
|
||||
|
||||
let token = "";
|
||||
if (Platform.OS === 'web') {
|
||||
token = localStorage.getItem('token') || "";
|
||||
} else {
|
||||
token = await SecureStore.getItemAsync('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) {
|
||||
@ -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);
|
||||
this.ws = new WebSocket(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,15 +156,20 @@ class WebSocketManager {
|
||||
* 处理自动重连逻辑,使用指数退避策略。
|
||||
*/
|
||||
private handleReconnect() {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
|
||||
console.log(`${delay / 1000}秒后尝试重新连接 (第 ${this.reconnectAttempts} 次)...`);
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, delay);
|
||||
} else {
|
||||
console.error('WebSocket 重连失败,已达到最大尝试次数。');
|
||||
try {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
|
||||
console.log(`${delay / 1000}秒后尝试重新连接 (第 ${this.reconnectAttempts} 次)...`);
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, delay);
|
||||
} else {
|
||||
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) {
|
||||
if (!this.messageListeners.has(type)) {
|
||||
this.messageListeners.set(type, new Set());
|
||||
try {
|
||||
if (!this.messageListeners.has(type)) {
|
||||
this.messageListeners.set(type, new Set());
|
||||
}
|
||||
this.messageListeners.get(type)?.add(callback);
|
||||
} catch (error) {
|
||||
console.error(`订阅消息类型 ${type} 时出错:`, error);
|
||||
}
|
||||
this.messageListeners.get(type)?.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -166,12 +214,16 @@ class WebSocketManager {
|
||||
* @param callback 要移除的回调函数。
|
||||
*/
|
||||
public unsubscribe(type: WsMessage['type'], callback: (message: WsMessage) => void) {
|
||||
const eventListeners = this.messageListeners.get(type);
|
||||
if (eventListeners) {
|
||||
eventListeners.delete(callback);
|
||||
if (eventListeners.size === 0) {
|
||||
this.messageListeners.delete(type);
|
||||
try {
|
||||
const eventListeners = this.messageListeners.get(type);
|
||||
if (eventListeners) {
|
||||
eventListeners.delete(callback);
|
||||
if (eventListeners.size === 0) {
|
||||
this.messageListeners.delete(type);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`取消订阅消息类型 ${type} 时出错:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,54 +231,95 @@ class WebSocketManager {
|
||||
* 手动断开 WebSocket 连接。
|
||||
*/
|
||||
public disconnect() {
|
||||
this.setStatus('disconnected');
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
try {
|
||||
this.setStatus('disconnected');
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('断开 WebSocket 连接时出错:', error);
|
||||
} finally {
|
||||
this.stopPing();
|
||||
}
|
||||
this.stopPing();
|
||||
}
|
||||
|
||||
private setStatus(status: WebSocketStatus) {
|
||||
if (this.status !== status) {
|
||||
this.status = status;
|
||||
this.statusListeners.forEach(listener => listener(status));
|
||||
try {
|
||||
if (this.status !== status) {
|
||||
this.status = status;
|
||||
this.statusListeners.forEach(listener => {
|
||||
try {
|
||||
listener(status);
|
||||
} catch (error) {
|
||||
console.error('调用状态监听器时出错:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置 WebSocket 状态时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public subscribeStatus(listener: StatusListener) {
|
||||
this.statusListeners.add(listener);
|
||||
// Immediately invoke with current status
|
||||
listener(this.status);
|
||||
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) {
|
||||
this.statusListeners.delete(listener);
|
||||
try {
|
||||
this.statusListeners.delete(listener);
|
||||
} catch (error) {
|
||||
console.error('取消订阅状态监听器时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳机制。
|
||||
*/
|
||||
private startPing() {
|
||||
this.stopPing(); // 先停止任何可能正在运行的计时器
|
||||
this.pingIntervalId = setInterval(() => {
|
||||
this.send({ type: 'Ping' });
|
||||
}, this.pingInterval);
|
||||
try {
|
||||
this.stopPing(); // 先停止任何可能正在运行的计时器
|
||||
this.pingIntervalId = setInterval(() => {
|
||||
this.send({ type: 'Ping' });
|
||||
}, this.pingInterval);
|
||||
} catch (error) {
|
||||
console.error('启动心跳机制时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳机制。
|
||||
*/
|
||||
private stopPing() {
|
||||
if (this.pingIntervalId) {
|
||||
clearInterval(this.pingIntervalId);
|
||||
this.pingIntervalId = null;
|
||||
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
60
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user