zlwu 11 месяцев назад
Родитель
Сommit
6e640eacfc

+ 11 - 4
frontend/src/pages/ChatBotPage copy.tsx

@@ -577,8 +577,8 @@ export const ChatBotPage = () => {
       emotionReceived: 0,
     };
 
-    recorderRef.current?.start(
-      (pcmData) => {
+    try {
+      await recorderRef.current?.start((pcmData) => {
         if (chunkIndexRef.current === 0) {
           performanceLogger.recordFirstChunkSent(requestId);
         }
@@ -586,8 +586,15 @@ export const ChatBotPage = () => {
         const base64 = btoa(String.fromCharCode(...Array.from(uint8Array)));
         wsRef.current?.sendAudioChunk(requestId, base64, chunkIndexRef.current);
         chunkIndexRef.current++;
-      }
-    );
+      });
+    } catch (error) {
+      console.error(`[录音] [${requestId.substring(0, 8)}] 启动失败`, error);
+      antMessage.error('麦克风启动失败,请检查权限或重试');
+      performanceLogger.clearRequest(requestId, 'recorder_start_failed');
+      setState('idle');
+      reset();
+      return;
+    }
     startRecordingTimer();
   };
 

+ 24 - 4
frontend/src/pages/ChatBotPage.tsx

@@ -527,6 +527,11 @@ export const ChatBotPage = () => {
       antMessage.warning('语音服务正在重连,请稍后再试');
       return;
     }
+
+    if (!wsRef.current?.isReady()) {
+      antMessage.warning('语音服务正在恢复,请稍后重试');
+      return;
+    }
     
     // 如果正在播放,先停止播放
     if (currentState === 'playing') {
@@ -577,8 +582,8 @@ export const ChatBotPage = () => {
       emotionReceived: 0,
     };
 
-    recorderRef.current?.start(
-      (pcmData) => {
+    try {
+      await recorderRef.current?.start((pcmData) => {
         if (chunkIndexRef.current === 0) {
           performanceLogger.recordFirstChunkSent(requestId);
         }
@@ -586,8 +591,15 @@ export const ChatBotPage = () => {
         const base64 = btoa(String.fromCharCode(...Array.from(uint8Array)));
         wsRef.current?.sendAudioChunk(requestId, base64, chunkIndexRef.current);
         chunkIndexRef.current++;
-      }
-    );
+      });
+    } catch (error) {
+      console.error(`[录音] [${requestId.substring(0, 8)}] 启动失败`, error);
+      antMessage.error('麦克风启动失败,请检查权限或重试');
+      performanceLogger.clearRequest(requestId, 'recorder_start_failed');
+      setState('idle');
+      reset();
+      return;
+    }
     startRecordingTimer();
   };
 
@@ -607,6 +619,14 @@ export const ChatBotPage = () => {
       return;
     }
 
+    if (!wsRef.current?.isReady()) {
+      antMessage.error('语音服务尚未准备完成,请重新录音发送');
+      recorderRef.current?.stop();
+      performanceLogger.clearRequest(currentRequestId, 'websocket_unready_before_send');
+      reset();
+      return;
+    }
+
     timingRef.current.recordEnd = Date.now();
     sendTimeRef.current = Date.now();
     performanceLogger.recordEnd(currentRequestId, chunkIndexRef.current);

+ 19 - 1
frontend/src/utils/audioRecorder.ts

@@ -103,9 +103,27 @@ export class AudioRecorder {
     return int16Array;
   }
 
-  start(onData: (data: Int16Array) => void, onVisualizer?: (data: Float32Array) => void): void {
+  async start(
+    onData: (data: Int16Array) => void,
+    onVisualizer?: (data: Float32Array) => void
+  ): Promise<void> {
     this.onDataCallback = onData;
     this.onVisualizerCallback = onVisualizer || null;
+
+    if (!this.audioContext) {
+      throw new Error('AudioRecorder 未初始化');
+    }
+
+    if (this.audioContext.state === 'suspended') {
+      try {
+        await this.audioContext.resume();
+      } catch (error) {
+        this.onDataCallback = null;
+        this.onVisualizerCallback = null;
+        throw error;
+      }
+    }
+
     this.isRecording = true;
     this.audioBuffer = [];
 

+ 77 - 4
frontend/src/utils/websocket.ts

@@ -11,6 +11,10 @@ export class WebSocketManager {
   private messageHandlers: Map<string, (message: Message) => void> = new Map();
   private pingInterval: ReturnType<typeof setInterval> | null = null;
   private readonly PING_INTERVAL = 30000; // 30秒
+  private pendingMessages: Partial<Message>[] = [];
+  private bufferTimer: ReturnType<typeof setTimeout> | null = null;
+  private readonly BUFFER_TIMEOUT_MS = 2000;
+  private readonly MAX_BUFFERED_MESSAGES = 40;
 
   connect(url: string, onConnectionChange?: (connected: boolean) => void): void {
     this.ws = new ReconnectingWebSocket(url, [], {
@@ -26,6 +30,7 @@ export class WebSocketManager {
       // 记录WebSocket连接建立时间
       performanceLogger.recordWebSocketConnected();
       this.startHeartbeat();
+      this.flushBufferedMessages();
       if (onConnectionChange) {
         onConnectionChange(true);
       }
@@ -71,12 +76,18 @@ export class WebSocketManager {
     this.messageHandlers.delete(id);
   }
 
-  send(message: Partial<Message>): void {
-    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
-      console.warn(`[WebSocket] [${message.requestId?.substring(0, 8) || ''}] 发送失败: WebSocket未连接`, message.type);
+  private isOpen(): boolean {
+    return !!this.ws && this.ws.readyState === WebSocket.OPEN;
+  }
+
+  isReady(): boolean {
+    return this.isOpen();
+  }
+
+  private doSend(message: Partial<Message>): void {
+    if (!this.ws) {
       return;
     }
-
     const fullMessage = {
       ...message,
       timestamp: Date.now(),
@@ -90,6 +101,63 @@ export class WebSocketManager {
     this.ws.send(JSON.stringify(fullMessage));
   }
 
+  private bufferMessage(message: Partial<Message>): void {
+    if (this.pendingMessages.length >= this.MAX_BUFFERED_MESSAGES) {
+      console.warn(
+        `[WebSocket] [${message.requestId?.substring(0, 8) || ''}] 缓冲区已满,丢弃消息: ${message.type}`
+      );
+      return;
+    }
+    this.pendingMessages.push(message);
+    if (!this.bufferTimer) {
+      this.bufferTimer = setTimeout(() => {
+        if (this.pendingMessages.length > 0) {
+          console.warn('[WebSocket] 缓冲等待超时,清空未发送音频消息');
+          this.pendingMessages = [];
+        }
+        this.bufferTimer = null;
+      }, this.BUFFER_TIMEOUT_MS);
+    }
+  }
+
+  private flushBufferedMessages(): void {
+    if (!this.isOpen() || this.pendingMessages.length === 0) {
+      return;
+    }
+    const bufferedMessages = [...this.pendingMessages];
+    this.pendingMessages = [];
+    if (this.bufferTimer) {
+      clearTimeout(this.bufferTimer);
+      this.bufferTimer = null;
+    }
+    for (const bufferedMessage of bufferedMessages) {
+      this.doSend(bufferedMessage);
+    }
+  }
+
+  private shouldBuffer(messageType: Message['type'] | undefined): boolean {
+    return messageType === 'audio_chunk' || messageType === 'audio_end';
+  }
+
+  send(message: Partial<Message>): void {
+    if (!this.isOpen()) {
+      if (this.shouldBuffer(message.type)) {
+        console.warn(
+          `[WebSocket] [${message.requestId?.substring(0, 8) || ''}] 连接未就绪,暂存消息: ${message.type}`
+        );
+        this.bufferMessage(message);
+      } else {
+        console.warn(
+          `[WebSocket] [${message.requestId?.substring(0, 8) || ''}] 发送失败: WebSocket未连接`,
+          message.type
+        );
+      }
+      return;
+    }
+
+    this.doSend(message);
+  }
+
   sendAudioChunk(requestId: string, data: string, chunkIndex: number): void {
     this.send({
       type: 'audio_chunk',
@@ -136,6 +204,11 @@ export class WebSocketManager {
 
   disconnect(): void {
     this.stopHeartbeat();
+    if (this.bufferTimer) {
+      clearTimeout(this.bufferTimer);
+      this.bufferTimer = null;
+    }
+    this.pendingMessages = [];
     if (this.ws) {
       this.ws.close();
       this.ws = null;