"""手术室站点配置:单一 JSON 文件,严格结构,无历史格式分支。""" from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path from typing import Any from loguru import logger from app.services.voice_terminal_binding import VoiceTerminalBindingIndex _ALLOWED_TOP_LEVEL = frozenset({"video_rtsp_urls", "voice_or_room_bindings"}) @dataclass(frozen=True) class OrSiteConfig: """根对象须含 ``video_rtsp_urls`` 与 ``voice_or_room_bindings`` 两个键。""" video_rtsp_urls: dict[str, str] voice_bindings: VoiceTerminalBindingIndex def parse_or_site_config_object(data: Any, *, source: str | Path = "") -> OrSiteConfig: label = str(source) if source else "JSON" if not isinstance(data, dict): raise ValueError(f"{label}: OR site config must be a JSON object") extra = set(data.keys()) - _ALLOWED_TOP_LEVEL if extra: raise ValueError( f"{label}: unknown top-level keys {sorted(extra)}; " f"allowed: {sorted(_ALLOWED_TOP_LEVEL)}" ) if "video_rtsp_urls" not in data or "voice_or_room_bindings" not in data: raise ValueError( f"{label}: must include video_rtsp_urls and voice_or_room_bindings" ) urls = data["video_rtsp_urls"] if not isinstance(urls, dict): raise ValueError(f"{label}: video_rtsp_urls must be a JSON object") raw_bindings = data["voice_or_room_bindings"] if not isinstance(raw_bindings, list): raise ValueError(f"{label}: voice_or_room_bindings must be a JSON array") for k, v in urls.items(): if not isinstance(v, str): raise ValueError( f"{label}: video_rtsp_urls[{k!r}] must be a string (RTSP URL)" ) idx = VoiceTerminalBindingIndex.from_binding_list(raw_bindings) if idx is None: raise ValueError(f"{label}: invalid voice_or_room_bindings content") return OrSiteConfig( video_rtsp_urls={str(k): str(v) for k, v in urls.items()}, voice_bindings=idx, ) def load_or_site_config_from_path(path: Path) -> OrSiteConfig: p = path.expanduser() try: raw_text = p.read_text(encoding="utf-8") except OSError as exc: raise ValueError(f"Cannot read OR site config {p}: {exc}") from exc try: data: Any = json.loads(raw_text) except json.JSONDecodeError as exc: raise ValueError(f"Invalid JSON in OR site config {p}: {exc}") from exc return parse_or_site_config_object(data, source=p) def merge_video_rtsp_urls_into_file( path: Path, url_map: dict[str, str], *, replace_host: str, ) -> None: """写入/更新站点配置中的 ``video_rtsp_urls``,保留 ``voice_or_room_bindings``。""" if replace_host in ("", "127.0.0.1"): out_urls = dict(url_map) else: out_urls = { k: v.replace("127.0.0.1", replace_host, 1) for k, v in url_map.items() } path = path.expanduser() path.parent.mkdir(parents=True, exist_ok=True) if path.is_file(): raw_text = path.read_text(encoding="utf-8") data: Any = json.loads(raw_text) parse_or_site_config_object(data, source=path) bindings_list = data["voice_or_room_bindings"] else: bindings_list = [] doc = { "video_rtsp_urls": out_urls, "voice_or_room_bindings": bindings_list, } text = json.dumps(doc, ensure_ascii=False, indent=2, sort_keys=True) + "\n" temp = path.with_name(path.name + ".tmp") temp.write_text(text, encoding="utf-8") temp.replace(path) logger.info("Updated video_rtsp_urls in OR site config {}", path)