335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import Constants from 'expo-constants';
|
||
import * as SecureStore from 'expo-secure-store';
|
||
import { TFunction } from 'i18next';
|
||
import { Platform } from 'react-native';
|
||
|
||
// 从环境变量或默认值中定义 WebSocket 端点
|
||
export const WEBSOCKET_ENDPOINT = process.env.EXPO_PUBLIC_WEBSOCKET_ENDPOINT || Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT;
|
||
|
||
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
||
|
||
type StatusListener = (status: WebSocketStatus) => void;
|
||
|
||
// 消息监听器类型
|
||
type MessageListener = (data: any) => void;
|
||
|
||
// 根据后端 Rust 定义的 WsMessage 枚举创建 TypeScript 类型
|
||
export type WsMessage =
|
||
| { type: 'Chat', session_id: string, message: string, image_material_ids?: string[], video_material_ids?: string[] }
|
||
| { type: 'ChatResponse', session_id: string, message: any, message_id?: string }
|
||
| { type: 'ChatStream', session_id: string, chunk: string }
|
||
| { type: 'ChatStreamEnd', session_id: string, message: any }
|
||
| { type: 'Error', code: string, message: string }
|
||
| { type: 'Ping' }
|
||
| { type: 'Pong' }
|
||
| { type: 'Connected', user_id: number };
|
||
|
||
class WebSocketManager {
|
||
private ws: WebSocket | null = null;
|
||
private status: WebSocketStatus = 'disconnected';
|
||
private messageListeners: Map<string, Set<(message: WsMessage) => void>> = new Map();
|
||
private statusListeners: Set<StatusListener> = new Set();
|
||
private reconnectAttempts = 0;
|
||
private readonly maxReconnectAttempts = 1;
|
||
private readonly reconnectInterval = 1000; // 初始重连间隔为1秒
|
||
private pingIntervalId: ReturnType<typeof setInterval> | null = null;
|
||
private readonly pingInterval = 30000; // 30秒发送一次心跳
|
||
|
||
constructor() {
|
||
// 这是一个单例类,连接通过调用 connect() 方法来启动
|
||
}
|
||
|
||
/**
|
||
* 获取当前 WebSocket 连接状态。
|
||
*/
|
||
public getStatus(): WebSocketStatus {
|
||
return this.status;
|
||
}
|
||
|
||
/**
|
||
* 启动 WebSocket 连接。
|
||
* 会自动获取并使用存储的认证 token。
|
||
*/
|
||
public async connect() {
|
||
// 防止重复连接
|
||
if (this.ws && (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,无法连接。');
|
||
this.setStatus('disconnected');
|
||
return;
|
||
} else {
|
||
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');
|
||
this.setStatus('connected');
|
||
this.reconnectAttempts = 0; // 重置重连尝试次数
|
||
this.startPing();
|
||
};
|
||
|
||
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 => {
|
||
try {
|
||
callback(message);
|
||
} catch (error) {
|
||
console.error(`处理消息类型 ${message.type} 时出错:`, error);
|
||
}
|
||
});
|
||
}
|
||
// 可以在这里处理通用的消息,比如 Pong
|
||
if (message.type === 'Pong') {
|
||
// console.log('Received Pong');
|
||
}
|
||
} catch (error) {
|
||
console.error('处理 WebSocket 消息失败:', error);
|
||
}
|
||
};
|
||
|
||
this.ws.onerror = (error) => {
|
||
console.error('WebSocket error:', error);
|
||
this.setStatus('disconnected');
|
||
this.handleReconnect();
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
console.log('WebSocket disconnected');
|
||
this.ws = null;
|
||
this.stopPing();
|
||
// 只有在不是手动断开连接时才重连
|
||
if (this.status !== 'disconnected') {
|
||
this.setStatus('reconnecting');
|
||
this.handleReconnect();
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 处理自动重连逻辑,使用指数退避策略。
|
||
*/
|
||
private handleReconnect() {
|
||
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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送消息到 WebSocket 服务器。
|
||
* @param message 要发送的消息对象,必须包含 type 字段。
|
||
*/
|
||
public send(message: WsMessage) {
|
||
if (this.status !== 'connected' || !this.ws) {
|
||
console.error('WebSocket 未连接,无法发送消息。');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const messageString = JSON.stringify(message);
|
||
this.ws.send(messageString);
|
||
} catch (error) {
|
||
console.error('发送 WebSocket 消息时出错:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 订阅指定消息类型的消息。
|
||
* @param type 消息类型,例如 'ChatResponse'。
|
||
* @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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消订阅指定消息类型的消息。
|
||
* @param type 消息类型。
|
||
* @param callback 要移除的回调函数。
|
||
*/
|
||
public unsubscribe(type: WsMessage['type'], callback: (message: WsMessage) => void) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 手动断开 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 => {
|
||
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 连接
|
||
let webSocketManagerInstance: WebSocketManager | null = null;
|
||
|
||
export const getWebSocketManager = (): WebSocketManager => {
|
||
if (!webSocketManagerInstance) {
|
||
webSocketManagerInstance = new WebSocketManager();
|
||
}
|
||
return webSocketManagerInstance;
|
||
};
|
||
|
||
// webscoket 错误映射
|
||
export const getWebSocketErrorMessage = (key: string, t: TFunction) => {
|
||
const messages = {
|
||
'INSUFFICIENT_POINTS': t('ask:ask.insufficientPoints'),
|
||
'INVALID_TOKEN': t('ask:ask.invalidToken'),
|
||
'NOT_FOUND': t('ask:ask.notFound'),
|
||
'PERMISSION_DENIED': t('ask:ask.permissionDenied'),
|
||
'NOT_CONNECTED': t('ask:ask.notConnected'),
|
||
};
|
||
|
||
return messages[key as keyof typeof messages] || t('ask:ask.unknownError');
|
||
}; |