| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- import asyncio
- import logging
- import time
- from typing import Optional
- import websockets
- from config.logger import setup_logging
- class SuppressInvalidHandshakeFilter(logging.Filter):
- """过滤掉无效握手错误日志(如HTTPS访问WS端口)"""
- def filter(self, record):
- msg = record.getMessage()
- suppress_keywords = [
- "opening handshake failed",
- "did not receive a valid HTTP request",
- "connection closed while reading HTTP request",
- "line without CRLF",
- ]
- return not any(keyword in msg for keyword in suppress_keywords)
- def _setup_websockets_logger():
- """配置 websockets 相关的所有 logger,过滤无效握手错误"""
- filter_instance = SuppressInvalidHandshakeFilter()
- for logger_name in ["websockets", "websockets.server", "websockets.client"]:
- logger = logging.getLogger(logger_name)
- logger.addFilter(filter_instance)
- _setup_websockets_logger()
- from core.connection import ConnectionHandler
- from config.config_loader import get_config_from_api_async
- from core.auth import AuthManager, AuthenticationError
- from core.utils.modules_initialize import initialize_modules
- from core.utils.util import check_vad_update, check_asr_update
- TAG = __name__
- class WebSocketServer:
- def __init__(self, config: dict):
- self.config = config
- self.logger = setup_logging()
- self.config_lock = asyncio.Lock()
- self.connections_lock = asyncio.Lock()
- self.active_connections = {}
- self.gps_lock = asyncio.Lock()
- self.latest_gps_payloads = {}
- self.gps_subscribers = {}
- modules = initialize_modules(
- self.logger,
- self.config,
- "VAD" in self.config["selected_module"],
- "ASR" in self.config["selected_module"],
- "LLM" in self.config["selected_module"],
- False,
- "Memory" in self.config["selected_module"],
- "Intent" in self.config["selected_module"],
- )
- self._vad = modules["vad"] if "vad" in modules else None
- self._asr = modules["asr"] if "asr" in modules else None
- self._llm = modules["llm"] if "llm" in modules else None
- self._intent = modules["intent"] if "intent" in modules else None
- self._memory = modules["memory"] if "memory" in modules else None
- auth_config = self.config["server"].get("auth", {})
- self.auth_enable = auth_config.get("enabled", False)
- # 设备白名单
- self.allowed_devices = set(auth_config.get("allowed_devices", []))
- secret_key = self.config["server"]["auth_key"]
- expire_seconds = auth_config.get("expire_seconds", None)
- self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
- async def start(self):
- server_config = self.config["server"]
- host = server_config.get("ip", "0.0.0.0")
- port = int(server_config.get("port", 8000))
- async with websockets.serve(
- self._handle_connection, host, port, process_request=self._http_response
- ):
- await asyncio.Future()
- async def register_connection(self, conn) -> None:
- device_id = getattr(conn, "device_id", None)
- if not device_id:
- return
- async with self.connections_lock:
- self.active_connections[device_id] = conn
- self.logger.bind(tag=TAG).info(f"Registered active device connection: {device_id}")
- async def unregister_connection(self, conn) -> None:
- device_id = getattr(conn, "device_id", None)
- if not device_id:
- return
- removed = False
- async with self.connections_lock:
- current = self.active_connections.get(device_id)
- if current is conn:
- self.active_connections.pop(device_id, None)
- removed = True
- if removed:
- self.logger.bind(tag=TAG).info(f"Unregistered active device connection: {device_id}")
- async def get_connection(self, device_id: Optional[str] = None):
- async with self.connections_lock:
- if device_id:
- conn = self.active_connections.get(device_id)
- return conn if self._is_connection_alive(conn) else None
- items = list(self.active_connections.items())
- for _, conn in reversed(items):
- if self._is_connection_alive(conn):
- return conn
- return None
- async def list_active_device_ids(self) -> list[str]:
- async with self.connections_lock:
- return [
- device_id
- for device_id, conn in self.active_connections.items()
- if self._is_connection_alive(conn)
- ]
- async def update_device_gps(self, device_id: str, payload: dict) -> None:
- if not device_id or not isinstance(payload, dict):
- return
- async with self.gps_lock:
- self.latest_gps_payloads[device_id] = (time.monotonic(), payload)
- subscribers = list(self.gps_subscribers.get(device_id, []))
- for subscriber in subscribers:
- try:
- if subscriber.full():
- subscriber.get_nowait()
- subscriber.put_nowait(payload)
- except asyncio.QueueFull:
- pass
- except asyncio.QueueEmpty:
- pass
- async def get_latest_gps(self, device_id: str, *, max_age_s: float = 3.0, allow_stale: bool = False):
- async with self.gps_lock:
- cached = self.latest_gps_payloads.get(device_id)
- if not cached:
- return None, False
- age_s = time.monotonic() - cached[0]
- if age_s > max_age_s:
- if not allow_stale:
- return None, False
- payload = dict(cached[1])
- payload["stale"] = True
- return payload, True
- return cached[1], False
- async def subscribe_device_gps(self, device_id: str) -> asyncio.Queue:
- subscriber: asyncio.Queue = asyncio.Queue(maxsize=8)
- async with self.gps_lock:
- self.gps_subscribers.setdefault(device_id, []).append(subscriber)
- return subscriber
- async def unsubscribe_device_gps(self, device_id: str, subscriber: asyncio.Queue) -> None:
- async with self.gps_lock:
- subscribers = self.gps_subscribers.get(device_id)
- if not subscribers:
- return
- if subscriber in subscribers:
- subscribers.remove(subscriber)
- if not subscribers:
- self.gps_subscribers.pop(device_id, None)
- @staticmethod
- def _is_connection_alive(conn) -> bool:
- if conn is None:
- return False
- stop_event = getattr(conn, "stop_event", None)
- if stop_event is not None and stop_event.is_set():
- return False
- websocket = getattr(conn, "websocket", None)
- if websocket is None:
- return False
- if hasattr(websocket, "closed"):
- return not websocket.closed
- if hasattr(websocket, "state"):
- return websocket.state.name != "CLOSED"
- return True
- async def _handle_connection(self, websocket: websockets.ServerConnection):
- headers = dict(websocket.request.headers)
- if headers.get("device-id", None) is None:
- # 尝试从 URL 的查询参数中获取 device-id
- from urllib.parse import parse_qs, urlparse
- # 从 WebSocket 请求中获取路径
- request_path = websocket.request.path
- if not request_path:
- self.logger.bind(tag=TAG).error("无法获取请求路径")
- await websocket.close()
- return
- parsed_url = urlparse(request_path)
- query_params = parse_qs(parsed_url.query)
- if "device-id" not in query_params:
- await websocket.send("端口正常,如需测试连接,请使用test_page.html")
- await websocket.close()
- return
- else:
- websocket.request.headers["device-id"] = query_params["device-id"][0]
- if "client-id" in query_params:
- websocket.request.headers["client-id"] = query_params["client-id"][0]
- if "authorization" in query_params:
- websocket.request.headers["authorization"] = query_params[
- "authorization"
- ][0]
- """处理新连接,每次创建独立的ConnectionHandler"""
- # 先认证,后建立连接
- try:
- await self._handle_auth(websocket)
- except AuthenticationError:
- await websocket.send("认证失败")
- await websocket.close()
- return
- # 创建ConnectionHandler时传入当前server实例
- handler = ConnectionHandler(
- self.config,
- self._vad,
- self._asr,
- self._llm,
- self._memory,
- self._intent,
- self, # 传入server实例
- )
- try:
- await handler.handle_connection(websocket)
- except Exception as e:
- self.logger.bind(tag=TAG).error(f"处理连接时出错: {e}")
- finally:
- # 强制关闭连接(如果还没有关闭的话)
- try:
- # 安全地检查WebSocket状态并关闭
- if hasattr(websocket, "closed") and not websocket.closed:
- await websocket.close()
- elif hasattr(websocket, "state") and websocket.state.name != "CLOSED":
- await websocket.close()
- else:
- # 如果没有closed属性,直接尝试关闭
- await websocket.close()
- except Exception as close_error:
- self.logger.bind(tag=TAG).error(
- f"服务器端强制关闭连接时出错: {close_error}"
- )
- async def _http_response(self, websocket, request_headers):
- # 检查是否为 WebSocket 升级请求
- connection_header = request_headers.headers.get("connection", "").lower()
- upgrade_header = request_headers.headers.get("upgrade", "").lower()
- if "upgrade" in connection_header and "websocket" in upgrade_header:
- # 如果是 WebSocket 请求,返回 None 允许握手继续
- return None
- else:
- # 如果是普通 HTTP 请求,返回 "server is running"
- return websocket.respond(200, "Server is running\n")
- async def update_config(self) -> bool:
- """更新服务器配置并重新初始化组件
- Returns:
- bool: 更新是否成功
- """
- try:
- async with self.config_lock:
- # 重新获取配置(使用异步版本)
- new_config = await get_config_from_api_async(self.config)
- if new_config is None:
- self.logger.bind(tag=TAG).error("获取新配置失败")
- return False
- self.logger.bind(tag=TAG).info(f"获取新配置成功")
- # 检查 VAD 和 ASR 类型是否需要更新
- update_vad = check_vad_update(self.config, new_config)
- update_asr = check_asr_update(self.config, new_config)
- self.logger.bind(tag=TAG).info(
- f"检查VAD和ASR类型是否需要更新: {update_vad} {update_asr}"
- )
- # 更新配置
- self.config = new_config
- # 重新初始化组件
- modules = initialize_modules(
- self.logger,
- new_config,
- update_vad,
- update_asr,
- "LLM" in new_config["selected_module"],
- False,
- "Memory" in new_config["selected_module"],
- "Intent" in new_config["selected_module"],
- )
- # 更新组件实例
- if "vad" in modules:
- self._vad = modules["vad"]
- if "asr" in modules:
- self._asr = modules["asr"]
- if "llm" in modules:
- self._llm = modules["llm"]
- if "intent" in modules:
- self._intent = modules["intent"]
- if "memory" in modules:
- self._memory = modules["memory"]
- self.logger.bind(tag=TAG).info(f"更新配置任务执行完毕")
- return True
- except Exception as e:
- self.logger.bind(tag=TAG).error(f"更新服务器配置失败: {str(e)}")
- return False
- async def _handle_auth(self, websocket: websockets.ServerConnection):
- # 先认证,后建立连接
- if self.auth_enable:
- headers = dict(websocket.request.headers)
- device_id = headers.get("device-id", None)
- client_id = headers.get("client-id", None)
- if self.allowed_devices and device_id in self.allowed_devices:
- # 如果属于白名单内的设备,不校验token,直接放行
- return
- else:
- # 否则校验token
- token = headers.get("authorization", "")
- if token.startswith("Bearer "):
- token = token[7:] # 移除'Bearer '前缀
- else:
- raise AuthenticationError("Missing or invalid Authorization header")
- # 进行认证
- auth_success = self.auth.verify_token(
- token, client_id=client_id, username=device_id
- )
- if not auth_success:
- raise AuthenticationError("Invalid token")
|