diff --git a/src/elderly/services/websocketService.ts b/src/elderly/services/websocketService.ts new file mode 100644 index 0000000..e9f803a --- /dev/null +++ b/src/elderly/services/websocketService.ts @@ -0,0 +1,226 @@ +/** + * WebSocket service for connecting to dialog engine (port 10002) + */ + +export interface WebSocketMessage { + Topic?: string; + Data: { + Key: 'text' | 'audio' | 'question' | 'log'; + Value?: string; + Text?: string; + IsFirst?: number; + IsEnd?: number; + }; + Username?: string; + robot?: string; +} + +export interface WebSocketServiceOptions { + url: string; + onMessage?: (message: WebSocketMessage) => void; + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Event) => void; + maxReconnectAttempts?: number; +} + +const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10; +const INITIAL_RECONNECT_DELAY = 1000; +const MAX_RECONNECT_DELAY = 30000; + +export class WebSocketService { + private ws: WebSocket | null = null; + private url: string; + private reconnectAttempts = 0; + private maxReconnectAttempts: number; + private reconnectTimer: number | null = null; + private isConnecting = false; + private onMessageCallback?: (message: WebSocketMessage) => void; + private onConnectCallback?: () => void; + private onDisconnectCallback?: () => void; + private onErrorCallback?: (error: Event) => void; + + constructor(options: WebSocketServiceOptions) { + this.url = options.url; + this.onMessageCallback = options.onMessage; + this.onConnectCallback = options.onConnect; + this.onDisconnectCallback = options.onDisconnect; + this.onErrorCallback = options.onError; + this.maxReconnectAttempts = options.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS; + } + + connect(): void { + if (this.isConnecting) { + console.log('[Fay] 已有连接正在进行中,跳过'); + return; + } + + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('[Fay] 达到最大重连次数限制'); + return; + } + + this.isConnecting = true; + console.log('[Fay] 正在连接到', this.url); + + try { + this.cleanup(false); + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('[Fay] 已连接到数字人接口'); + this.isConnecting = false; + this.reconnectAttempts = 0; + + // 发送初始化消息 + const initMsg = { Username: 'User', Output: false }; + console.log('[Fay] 发送初始化消息:', initMsg); + this.send(initMsg); + + this.onConnectCallback?.(); + }; + + this.ws.onmessage = (event) => { + console.log('='.repeat(60)); + console.log('[Fay] 收到消息:', event.data); + + try { + const message: WebSocketMessage = JSON.parse(event.data); + console.log('[Fay] 解析后:', JSON.stringify(message, null, 2)); + + if (!message || !message.Data || !message.Data.Key) { + console.warn('[Fay] 消息格式不完整:', message); + return; + } + + // 处理文本消息 + if (message.Data.Key === 'text') { + const text = message.Data.Value || ''; + const isFirst = message.Data.IsFirst === 1; + const isEnd = message.Data.IsEnd === 1; + + if (text && text.trim()) { + console.log('[xmov] 收到文本消息:', text, { is_start: isFirst, is_end: isEnd }); + this.onMessageCallback?.(message); + } else { + console.warn('[Fay] 收到空文本消息'); + } + } + // 处理音频消息 + else if (message.Data.Key === 'audio') { + const text = message.Data.Text || ''; + const isFirst = message.Data.IsFirst === 1; + const isEnd = message.Data.IsEnd === 1; + + if (text && text.trim()) { + console.log('[xmov] 收到音频消息:', text, { is_start: isFirst, is_end: isEnd }); + this.onMessageCallback?.(message); + } else { + console.warn('[Fay] 收到空音频文本'); + } + } + // 处理日志消息(思考中等状态提示) + else if (message.Data.Key === 'log') { + const logText = message.Data.Value || ''; + console.log('[Fay] 收到日志消息:', logText); + this.onMessageCallback?.(message); + } + // 处理问题消息 + else if (message.Data.Key === 'question') { + console.log('[Fay] 用户问题:', message.Data.Value); + } else { + console.log('[Fay] 未知消息类型:', message.Data.Key); + } + } catch (error) { + console.error('[Fay] 处理消息出错:', error, '原始数据:', event.data); + } + console.log('='.repeat(60)); + }; + + this.ws.onerror = (error) => { + console.error('[Fay] WebSocket错误:', error); + this.isConnecting = false; + this.onErrorCallback?.(error); + }; + + this.ws.onclose = (event) => { + console.log('[Fay] WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason); + this.isConnecting = false; + this.onDisconnectCallback?.(); + + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + + // 使用指数退避策略重连 + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + const delay = this.calculateReconnectDelay(); + console.log(`[Fay] ${delay}ms后尝试第${this.reconnectAttempts}次重连...`); + + this.reconnectTimer = window.setTimeout(() => { + this.connect(); + }, delay); + } else { + console.error('[Fay] 已达到最大重连次数'); + } + }; + } catch (error) { + console.error('[Fay] 创建WebSocket连接失败:', error); + this.isConnecting = false; + } + } + + send(data: any): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + try { + this.ws.send(JSON.stringify(data)); + } catch (error) { + console.error('[Fay] 发送消息失败:', error); + } + } + } + + disconnect(): void { + this.cleanup(true); + } + + private cleanup(resetReconnectAttempts: boolean): void { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + + if (this.ws) { + try { + this.ws.onclose = null; + this.ws.onerror = null; + this.ws.onmessage = null; + this.ws.onopen = null; + this.ws.close(); + } catch (error) { + console.warn('[Fay] 清理WebSocket时出错:', error); + } + this.ws = null; + } + + this.isConnecting = false; + if (resetReconnectAttempts) { + this.reconnectAttempts = 0; + } + } + + private calculateReconnectDelay(): number { + const exponentialDelay = Math.min( + INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts - 1), + MAX_RECONNECT_DELAY + ); + const jitter = exponentialDelay * 0.2 * (Math.random() * 2 - 1); + return Math.floor(exponentialDelay + jitter); + } + + isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } +}