HumansXドキュメントセンター 公式サイトへ ↗

WebSocket 通信統合ガイド

通信アーキテクチャの概要

フロントエンドは WebSocket を通じてバックエンドと永続的な接続を確立し、双方向のリアルタイム通信を実現します。システムはメッセージタイプに基づくルーティングの仕組みを採用し、ストリーミングデータ転送、状態の同期、エラーからの復旧に対応しています。

接続管理

WebSocket 接続の初期化

const getWebSocketUrl = useCallback(() => {
  // 1. 优先使用传入的URL
  if (websocketUrl) return websocketUrl

  // 2. 使用环境变量
  const envWsUrl = process.env.NEXT_PUBLIC_WEBSOCKET_URL
  if (envWsUrl) return envWsUrl

  // 3. 动态构建URL
  if (typeof window !== "undefined") {
    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
    return `${protocol}//${window.location.hostname}:8000/ws/chat`
  }

  return "ws://localhost:8000/ws/chat"
}, [websocketUrl])

接続状態の管理

type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'

interface ConnectionInfo {
  state: ConnectionState
  url: string
  error: string | null
  reconnectAttempts: number
  socket: WebSocket | null
}

自動再接続の仕組み

const scheduleReconnect = useCallback(() => {
  if (state.connection.reconnectAttempts >= maxReconnectAttempts) {
    dispatch({
      type: 'CONNECTION_ERROR',
      payload: { error: "已达到最大重连次数" }
    })
    return
  }

  const attempts = state.connection.reconnectAttempts + 1
  const delay = baseReconnectDelay * Math.pow(2, attempts - 1) // 指数退避算法

  dispatch({ type: 'CONNECTION_RECONNECTING', payload: { attempts } })

  reconnectTimeoutRef.current = setTimeout(() => {
    connectWebSocket()
  }, delay)
}, [state.connection.reconnectAttempts, connectWebSocket])

メッセージプロトコル仕様

基本的なメッセージ形式

interface WebSocketMessage {
  type: string                    // 消息类型标识
  payload: Record<string, any>    // 消息载荷
  timestamp?: number              // 可选时间戳
}

メッセージタイプの分類

分類 メッセージタイプ 方向 説明
制御系 USER_TEXT_INPUT C→S ユーザーのテキスト入力
INTERRUPT_TURN C→S 現在の対話を中断
TOGGLE_ASR_LISTENING C→S ASR の聞き取りを切り替え
状態系 SYSTEM_STATUS S→C システム状態の更新
TTS_STATUS S→C TTS 再生状態
データ系 LLM_CHUNK S→C LLM のストリーミング応答の断片
LLM_FINAL_RESPONSE S→C LLM の最終応答
ASR_FINAL_RESULT S→C ASR の認識結果
ツール系 TOOL_CALL_START S→C ツール呼び出しの開始
TOOL_CALL_RESULT S→C ツール呼び出しの結果
セッション系 TURN_ENDED S→C 対話ラウンドの終了
TURN_INTERRUPTED S→C 対話の中断
思考チェーン REASONING_START S→C 推論の開始
REASONING_CHUNK S→C 推論内容の断片
REASONING_END S→C 推論の終了

クライアントからのメッセージ送信

1. ユーザーのテキスト入力

const sendUserInput = useCallback((text?: string) => {
  const inputText = text || state.conversation.currentInput
  if (!inputText.trim()) return

  const success = sendMessage({
    type: 'USER_TEXT_INPUT',
    payload: { text: inputText },
  })

  if (success) {
    dispatch({ type: 'INPUT_CLEAR' })
    dispatch({ type: 'TURN_END' }) // 清除当前轮次,等待助手响应
  }
}, [state.conversation.currentInput, sendMessage])

2. 現在の対話の中断

const interruptTurn = useCallback(() => {
  dispatch({ type: 'TURN_INTERRUPT' })

  // 发送中断消息到服务器
  sendMessage({
    type: 'INTERRUPT_TURN',
    payload: {},
  })
}, [sendMessage])

3. 音声認識の切り替え

const setListening = useCallback((isListening: boolean) => {
  dispatch({ type: 'UI_SET_LISTENING', payload: { isListening } })

  // 发送ASR监听切换消息
  sendMessage({
    type: 'TOGGLE_ASR_LISTENING',
    payload: { enabled: isListening },
  })
}, [sendMessage])

サーバー応答の処理

1. LLM ストリーミング応答の処理

case 'LLM_CHUNK':
  const { content_delta, is_final } = message.payload
  const currentState = stateRef.current

  // 确保有当前轮次的助手消息
  let currentAssistantMessageId = currentState.conversation.currentTurnId

  if (!currentAssistantMessageId) {
    // 创建新的助手消息
    currentAssistantMessageId = Date.now().toString() + '_assistant'
    dispatch({ type: 'TURN_START', payload: { turnId: currentAssistantMessageId } })

    const assistantMessage: Message = {
      id: currentAssistantMessageId,
      role: 'assistant',
      content: content_delta || '',
      timestamp: Date.now(),
    }
    dispatch({ type: 'MESSAGE_ADD', payload: { message: assistantMessage } })
  } else {
    // 🔒 智能保护机制:在有工具调用的turn中启用静默模式
    const hasToolCallsInTurn = currentState.conversation.hasToolCallsInCurrentTurn

    if (hasToolCallsInTurn) {
      return // 静默模式:等待LLM_FINAL_RESPONSE统一更新
    }

    // 正常的流式追加
    const currentMessage = currentState.conversation.messages.find(m => m.id === currentAssistantMessageId)
    if (currentMessage) {
      const newContent = (currentMessage.content || '') + (content_delta || '')

      dispatch({
        type: 'MESSAGE_UPDATE',
        payload: {
          id: currentAssistantMessageId,
          updates: { content: newContent }
        }
      })
    }
  }

  // 更新流式状态
  if (is_final) {
    dispatch({ type: 'STREAM_END', payload: {} })
  }
  break

2. LLM 最終応答の処理

case 'LLM_FINAL_RESPONSE':
  // 🛡️ 事件去重:防止重复处理相同的LLM_FINAL_RESPONSE
  const responseContent = message.payload.content || ''
  const currentTurnId = stateRef.current.conversation.currentTurnId
  const responseKey = `${currentTurnId}_${responseContent.length}_${responseContent.slice(-20)}`

  if (!window.processedLLMResponses) {
    window.processedLLMResponses = new Set()
  }

  if (window.processedLLMResponses.has(responseKey)) {
    return // 跳过重复的LLM_FINAL_RESPONSE事件
  }

  window.processedLLMResponses.add(responseKey)

  // 智能内容更新逻辑
  const finalCurrentState = stateRef.current
  let finalAssistantMessageId = finalCurrentState.conversation.currentTurnId

  if (finalAssistantMessageId) {
    const currentMessage = finalCurrentState.conversation.messages.find(m => m.id === finalAssistantMessageId)
    const existingContent = currentMessage?.content || ''
    const newContent = message.payload.content || ''
    const hasToolCallsInTurn = finalCurrentState.conversation.hasToolCallsInCurrentTurn

    let finalContent = newContent

    // 🔒 智能保护:如果当前turn有工具调用,智能累积内容
    if (hasToolCallsInTurn && existingContent && newContent) {
      if (existingContent.includes(newContent)) {
        finalContent = existingContent // 防止重复
      } else {
        finalContent = `${existingContent}\n\n${newContent}` // 智能累积
      }
    }

    dispatch({
      type: 'MESSAGE_UPDATE',
      payload: {
        id: finalAssistantMessageId,
        updates: { content: finalContent }
      }
    })
  }

  dispatch({ type: 'STREAM_END', payload: { finalContent: message.payload.content } })
  break

3. ツール呼び出しの処理

case 'TOOL_CALL_START':
  // 解析工具调用参数
  let parsedArgs = {}
  try {
    parsedArgs = message.payload.args_str ? JSON.parse(message.payload.args_str) : {}
  } catch (e) {
    console.error("Failed to parse tool call args:", message.payload.args_str, e)
    parsedArgs = { error: "解析参数失败" }
  }

  const toolCall: ToolCall = {
    name: message.payload.name,
    id: message.payload.id,
    args: parsedArgs,
    status: 'pending',
  }

  dispatch({ type: 'TOOL_CALL_START', payload: { toolCall } })

  // 更新当前助手消息的工具调用列表
  const toolCurrentState = stateRef.current
  let toolAssistantMessageId = toolCurrentState.conversation.currentTurnId

  if (!toolAssistantMessageId) {
    // 创建新的助手消息轮次
    toolAssistantMessageId = Date.now().toString() + '_assistant_with_tools'
    dispatch({ type: 'TURN_START', payload: { turnId: toolAssistantMessageId } })

    const assistantMessage: Message = {
      id: toolAssistantMessageId,
      role: 'assistant',
      content: '',
      timestamp: Date.now(),
      toolCalls: [toolCall]
    }
    dispatch({ type: 'MESSAGE_ADD', payload: { message: assistantMessage } })
  } else {
    // 更新现有助手消息的工具调用
    const currentMessage = toolCurrentState.conversation.messages.find(m => m.id === toolAssistantMessageId)
    const existingToolCalls = currentMessage?.toolCalls || []

    dispatch({
      type: 'MESSAGE_UPDATE',
      payload: {
        id: toolAssistantMessageId,
        updates: {
          toolCalls: [...existingToolCalls, toolCall]
        }
      }
    })
  }
  break

case 'TOOL_CALL_RESULT':
  // 处理工具调用结果(成功或失败)
  if (message.payload.status === 'error') {
    dispatch({
      type: 'TOOL_CALL_ERROR',
      payload: {
        id: message.payload.id,
        error: message.payload.error || '工具调用失败'
      }
    })
  } else {
    dispatch({
      type: 'TOOL_CALL_END',
      payload: {
        id: message.payload.id,
        result: message.payload.result || '执行成功'
      }
    })
  }

  // 更新消息中的工具调用状态
  const resultCurrentState = stateRef.current
  if (resultCurrentState.conversation.currentTurnId) {
    const currentMessage = resultCurrentState.conversation.messages.find(m => m.id === resultCurrentState.conversation.currentTurnId)
    if (currentMessage?.toolCalls) {
      const updatedToolCalls = currentMessage.toolCalls.map(tool =>
        tool.id === message.payload.id ? {
          ...tool,
          result: message.payload.result || (message.payload.status === 'error' ? undefined : '执行成功'),
          error: message.payload.status === 'error' ? (message.payload.error || '工具调用失败') : undefined,
          status: message.payload.status === 'error' ? 'error' : 'success'
        } : tool
      )

      dispatch({
        type: 'MESSAGE_UPDATE',
        payload: {
          id: resultCurrentState.conversation.currentTurnId,
          updates: { toolCalls: updatedToolCalls }
        }
      })
    }
  }
  break

4. 思考チェーンの処理

case 'REASONING_START':
  const currentState2 = stateRef.current
  let currentAssistantMessageId2 = currentState2.conversation.currentTurnId

  // 如果没有当前轮次,创建新的助手消息
  if (!currentAssistantMessageId2) {
    currentAssistantMessageId2 = Date.now().toString() + '_assistant'
    dispatch({ type: 'TURN_START', payload: { turnId: currentAssistantMessageId2 } })

    const assistantMessage: Message = {
      id: currentAssistantMessageId2,
      role: 'assistant',
      content: '',
      timestamp: Date.now(),
      reasoningContent: '',
      isReasoningStreaming: true
    }
    dispatch({ type: 'MESSAGE_ADD', payload: { message: assistantMessage } })
  }

  dispatch({
    type: 'REASONING_START',
    payload: { messageId: currentAssistantMessageId2 }
  })
  break

case 'REASONING_CHUNK':
  const currentState3 = stateRef.current
  const currentAssistantMessageId3 = currentState3.conversation.currentTurnId

  if (currentAssistantMessageId3) {
    dispatch({
      type: 'REASONING_CHUNK',
      payload: {
        messageId: currentAssistantMessageId3,
        content: message.payload.reasoning_delta || ''
      }
    })
  }
  break

case 'REASONING_END':
  const currentState4 = stateRef.current
  const currentAssistantMessageId4 = currentState4.conversation.currentTurnId

  if (currentAssistantMessageId4) {
    dispatch({
      type: 'REASONING_END',
      payload: { messageId: currentAssistantMessageId4 }
    })
  }
  break

5. ASR 結果の処理

case 'ASR_FINAL_RESULT':
  // 只有在语音模式下才处理ASR结果,避免在键盘模式下干扰用户输入
  const asrCurrentState = stateRef.current
  if (asrCurrentState.ui.inputMode === 'voice') {
    dispatch({
      type: 'UI_SET_ASR_RESULT',
      payload: { result: message.payload.text }
    })
  }
  // 键盘模式下忽略ASR结果
  break

状態同期の仕組み

システム状態の更新

case 'SYSTEM_STATUS':
  dispatch({ type: 'SYSTEM_STATUS_UPDATE', payload: message.payload })

  // 特殊处理ASR状态
  if (message.payload.module === 'asr_service') {
    dispatch({
      type: 'UI_SET_LISTENING',
      payload: { isListening: message.payload.status === 'listening' }
    })
  }
  break

case 'TTS_STATUS':
  dispatch({
    type: 'SYSTEM_STATUS_UPDATE',
    payload: {
      module: 'tts_player',
      status: message.payload.status
    }
  })
  break

セッションラウンドの管理

case 'TURN_ENDED':
  dispatch({ type: 'TURN_END' })
  break

case 'TURN_INTERRUPTED':
  dispatch({ type: 'TURN_INTERRUPT' })
  break

️ エラー処理と復旧

接続エラーの処理

const connectWebSocket = useCallback((url?: string) => {
  const wsUrl = url || getWebSocketUrl()

  dispatch({ type: 'CONNECTION_CONNECTING', payload: { url: wsUrl } })

  try {
    const newSocket = new WebSocket(wsUrl)

    newSocket.onopen = () => {
      dispatch({ type: 'CONNECTION_CONNECTED', payload: { socket: newSocket } })
      dispatch({ type: 'CONNECTION_RESET_ATTEMPTS' })
    }

    newSocket.onerror = (error) => {
      console.error("WebSocket error:", error)
      dispatch({
        type: 'CONNECTION_ERROR',
        payload: { error: "WebSocket连接错误" }
      })
    }

    newSocket.onclose = (event) => {
      socketRef.current = null

      if (event.code !== 1000) {
        // 非正常关闭,尝试重连
        scheduleReconnect()
      } else {
        dispatch({ type: 'CONNECTION_DISCONNECTED', payload: {} })
      }
    }

  } catch (error) {
    dispatch({
      type: 'CONNECTION_ERROR',
      payload: { error: "创建WebSocket连接失败" }
    })
  }
}, [getWebSocketUrl])

メッセージ送信失敗の処理

const sendMessage = useCallback((message: WebSocketMessage): boolean => {
  if (!socketRef.current || state.connection.state !== 'connected') {
    dispatch({
      type: 'SYSTEM_ERROR_ADD',
      payload: {
        message: "WebSocket未连接",
        source: "websocket",
        severity: 'warning'
      }
    })
    return false
  }

  try {
    socketRef.current.send(JSON.stringify(message))
    return true
  } catch (error) {
    console.error("Failed to send WebSocket message:", error)
    dispatch({
      type: 'SYSTEM_ERROR_ADD',
      payload: {
        message: "发送消息失败",
        source: "websocket",
        severity: 'error'
      }
    })
    return false
  }
}, [state.connection.state])

メッセージ解析エラーの処理

newSocket.onmessage = (event) => {
  try {
    const message: WebSocketMessage = JSON.parse(event.data)
    dispatch({ type: 'SYSTEM_MESSAGE_RECEIVED', payload: { message } })
    handleWebSocketMessage(message)
  } catch (error) {
    console.error("Failed to parse WebSocket message:", error)
    dispatch({
      type: 'SYSTEM_ERROR_ADD',
      payload: {
        message: "解析WebSocket消息失败",
        source: "websocket",
        severity: 'error'
      }
    })
  }
}

⚡ パフォーマンスの最適化

メッセージ処理の最適化

// 使用useRef避免闭包问题,确保获取最新状态
const stateRef = useRef(state)
useEffect(() => {
  stateRef.current = state
}, [state])

// 消息处理函数使用最新状态
const handleWebSocketMessage = useCallback((message: WebSocketMessage) => {
  const currentState = stateRef.current
  // 处理逻辑...
}, []) // 无依赖,避免重复创建函数

再接続戦略の最適化

// 指数退避算法,避免频繁重连
const baseReconnectDelay = 1000 // 1秒
const maxReconnectAttempts = 5

const delay = baseReconnectDelay * Math.pow(2, attempts - 1)
// attempts=1: 1秒
// attempts=2: 2秒
// attempts=3: 4秒
// attempts=4: 8秒
// attempts=5: 16秒

メモリリークの防止

useEffect(() => {
  return () => {
    // 清理重连定时器
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current)
    }

    // 关闭WebSocket连接
    if (socketRef.current) {
      socketRef.current.close()
    }

    // 清理全局去重状态
    if (typeof window !== 'undefined' && window.processedLLMResponses) {
      window.processedLLMResponses.clear()
    }
  }
}, [])

テストとデバッグ

WebSocket 接続のテスト

// 开发模式下的连接状态监控
useEffect(() => {
  if (process.env.NODE_ENV === 'development') {
    console.log('WebSocket状态变更:', {
      state: state.connection.state,
      url: state.connection.url,
      attempts: state.connection.reconnectAttempts,
      error: state.connection.error
    })
  }
}, [state.connection])

メッセージフローのデバッグ

const handleWebSocketMessage = useCallback((message: WebSocketMessage) => {
  // 开发模式下记录所有消息
  if (process.env.NODE_ENV === 'development') {
    console.group(`📩 WebSocket消息: ${message.type}`)
    console.log('载荷:', message.payload)
    console.log('时间戳:', new Date().toISOString())
    console.groupEnd()
  }

  // 消息处理逻辑...
}, [])

WebSocket 通信の統合はフロントエンドシステムの中核となる基盤であり、充実した接続管理、メッセージ処理、エラー復旧の仕組みにより、バックエンドとの安定した通信と良好なユーザー体験を確保します。