""" 語音綁定狀態機 管理語音帳號綁定流程(關鍵字匹配,無 GPT) """ import logging import time from datetime import datetime from typing import Dict, Any, Optional from fastapi import WebSocket logger = logging.getLogger("websocket.voice_binding") class VoiceBindingStateMachine: """ 語音帳號綁定狀態機(硬編碼關鍵字匹配) 流程: 1. 用戶說「我要綁定語音登入」 2. Agent 回應「好的,你現在要綁定誰?」 3. 用戶提供名稱 4. 系統綁定 speaker_label 到用戶帳號 5. Agent 回應「綁定成功!」 """ def __init__(self): # 用戶狀態:{user_id: {state: str, speaker_label: str}} self.user_states: Dict[str, Dict[str, Any]] = {} def check_binding_trigger(self, user_id: str, message: str) -> Optional[str]: """ 檢查是否觸發綁定流程 Returns: - "TRIGGER": 觸發綁定流程 - "AWAITING_NAME": 等待用戶提供名稱 - None: 不是綁定相關訊息 """ message_lower = message.lower().replace(" ", "") # 檢測觸發關鍵字 trigger_keywords = ["綁定語音登入", "語音登入綁定", "綁定語音", "設定語音登入"] for keyword in trigger_keywords: if keyword.replace(" ", "") in message_lower: # 進入等待狀態 self.user_states[user_id] = { "state": "AWAITING_NAME", "timestamp": datetime.now() } return "TRIGGER" # 檢查是否在等待名稱狀態 if user_id in self.user_states: state_info = self.user_states[user_id] if state_info.get("state") == "AWAITING_NAME": # 檢查是否超時(5分鐘) if (datetime.now() - state_info.get("timestamp")).total_seconds() > 300: del self.user_states[user_id] return None return "AWAITING_NAME" return None async def handle_binding_flow( self, user_id: str, message: str, websocket: WebSocket, voice_service: Optional[Any] = None, manager: Optional[Any] = None, ) -> bool: """ 處理綁定流程 Returns: True: 已處理(不要繼續到 Agent) False: 未處理(繼續到 Agent) """ state = self.check_binding_trigger(user_id, message) if state == "TRIGGER": # 用戶觸發綁定 - 先檢查是否已經綁定過 logger.info(f"🎙️ 用戶 {user_id} 觸發語音綁定流程") # 檢查使用者是否已經綁定過 speaker_label from core.database import get_user_by_id try: user_data = await get_user_by_id(user_id) if user_data and user_data.get("speaker_label"): # 已經綁定過了 existing_label = user_data.get("speaker_label") logger.info(f"⚠️ 用戶 {user_id} 已綁定 speaker_label: {existing_label}") await websocket.send_json({ "type": "bot_message", "message": f"你已經綁定過語音了!目前的聲紋標籤是:{existing_label}。如果需要重新綁定,請聯繫管理員。", "timestamp": time.time() }) # 清理 FSM 狀態 self.clear_state(user_id) return True except Exception as e: logger.error(f"❌ 檢查使用者綁定狀態失敗: {e}") await websocket.send_json({ "type": "error", "message": "系統錯誤,無法檢查綁定狀態" }) return True # 未綁定,繼續綁定流程 logger.info(f"✅ 用戶 {user_id} 尚未綁定,啟動綁定流程") # 標記用戶進入語音綁定等待狀態 if manager: user_session = manager.get_client_info(user_id) or {} user_session["voice_binding_pending"] = True user_session["voice_binding_started_at"] = datetime.now() manager.set_client_info(user_id, user_session) await websocket.send_json({ "type": "bot_message", "message": "好的,請錄製一段語音(約3-5秒),用於建立你的聲紋特徵。系統會自動識別並綁定到你的帳號。", "timestamp": time.time() }) await websocket.send_json({ "type": "voice_binding_ready", "message": "請點擊錄音按鈕開始錄製" }) return True elif state == "AWAITING_NAME": # 這個狀態已不再使用,因為我們改為直接錄音綁定 pass return False def clear_state(self, user_id: str): """清理用戶狀態""" self.user_states.pop(user_id, None) def is_in_binding_flow(self, user_id: str) -> bool: """檢查用戶是否在綁定流程中""" return user_id in self.user_states # 全局語音綁定狀態機實例 voice_binding_fsm = VoiceBindingStateMachine()