feat(voice-client): 双层配置持久化、精简手术号 UI 与 WS/服务端排查日志

- machine_config:系统级 + 用户级 voice_client.json 合并,界面失焦保存至用户目录
- 移除「当前手术号」表单项与占位文案;指派后仅在窗口标题显示手术号
- WebSocket 连接日志附带绑定/开录路径排查说明
- 开录未推送时服务端 WARNING(无站点绑定或 camera_ids 不匹配)
- 测试、README、.env.example 同步

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-27 11:45:11 +08:00
parent 6b3adb4ad8
commit 9941e0d131
8 changed files with 352 additions and 53 deletions

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import json
import os
import threading
import time
from collections.abc import Callable
@@ -76,7 +75,12 @@ class VoiceAssignmentListener:
try:
def _on_open(_ws: Any) -> None:
logger.info("WebSocket 已连接,等待服务端 voice_assignment 消息")
logger.info("WebSocket 已连接 terminal_id={!r}", self._terminal_id)
logger.info(
"若开录后仍无 voice_assignment核对本机 ID 与 OR_SITE_CONFIG「voice_or_room_bindings」、"
"开录请求 camera_ids 能否解析到该终端;开录须 POST /client/surgeries/start "
"联调POST /internal/demo/orchestrate-and-start。"
)
def _on_close(_ws: Any, close_status_code: Any, close_msg: Any) -> None:
logger.warning(
@@ -137,7 +141,3 @@ class VoiceAssignmentListener:
self._on_end(sid)
else:
logger.debug("忽略 voice_assignment action={!r}", action)
def default_terminal_id_from_env() -> str:
return (os.environ.get("VOICE_TERMINAL_ID") or "").strip()

View File

@@ -0,0 +1,104 @@
"""语音客户端配置:系统级(运维下发)+ 用户级(界面保存,覆盖同名字段)。
- ``VOICE_CLIENT_MACHINE_CONFIG_FILE``:仅改变**系统级**配置文件路径(测试或定制)。
- ``VOICE_CLIENT_USER_CONFIG_FILE``:仅改变**用户级**配置文件路径(测试或定制)。
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from typing import Any
from loguru import logger
_DEFAULT_HTTP_BASE = "http://127.0.0.1:38080"
_CONFIG_FILENAME = "voice_client.json"
def machine_config_file_path() -> Path:
"""系统级配置:运维部署;只读亦可。"""
override = (os.environ.get("VOICE_CLIENT_MACHINE_CONFIG_FILE") or "").strip()
if override:
return Path(override).expanduser()
if sys.platform == "win32":
base = os.environ.get("PROGRAMDATA", r"C:\ProgramData")
return Path(base) / "OperationRoomMonitor" / _CONFIG_FILENAME
if sys.platform == "darwin":
return (
Path("/Library/Application Support/OperationRoomMonitor")
/ _CONFIG_FILENAME
)
return Path("/etc/operation-room-monitor") / _CONFIG_FILENAME
def user_voice_client_config_path() -> Path:
"""用户级配置:当前登录用户可写,界面编辑后保存到此。"""
override = (os.environ.get("VOICE_CLIENT_USER_CONFIG_FILE") or "").strip()
if override:
return Path(override).expanduser()
if sys.platform == "win32":
base = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData/Local"))
return Path(base) / "OperationRoomMonitor" / _CONFIG_FILENAME
if sys.platform == "darwin":
return (
Path.home() / "Library/Application Support/OperationRoomMonitor" / _CONFIG_FILENAME
)
return Path.home() / ".config/operation-room-monitor" / _CONFIG_FILENAME
def _read_json_object(path: Path) -> dict[str, Any]:
if not path.is_file():
return {}
try:
raw = path.read_text(encoding="utf-8")
data = json.loads(raw)
except OSError as exc:
logger.warning("无法读取语音客户端配置 {}: {}", path, exc)
return {}
except json.JSONDecodeError as exc:
logger.warning("语音客户端配置 JSON 无效 {}: {}", path, exc)
return {}
if not isinstance(data, dict):
logger.warning("语音客户端配置须为 JSON 对象: {}", path)
return {}
return data
def load_voice_client_config() -> dict[str, Any]:
"""合并系统配置与用户配置;同键时用户覆盖系统。"""
system = _read_json_object(machine_config_file_path())
user = _read_json_object(user_voice_client_config_path())
merged: dict[str, Any] = dict(system)
merged.update(user)
return merged
def save_user_voice_client_config(*, voice_terminal_id: str, http_base_url: str) -> None:
"""将当前界面上的连接参数写入用户级配置文件。"""
path = user_voice_client_config_path()
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"voice_terminal_id": voice_terminal_id.strip(),
"http_base_url": http_base_url.strip().rstrip("/"),
}
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def voice_terminal_id_from_config(file_data: dict[str, Any]) -> str:
"""合并后 dict 中的 ``voice_terminal_id``;缺省为空串。"""
v = file_data.get("voice_terminal_id")
return str(v).strip() if v is not None else ""
def http_base_url_from_config(file_data: dict[str, Any]) -> str:
"""合并后 dict 中的 ``http_base_url``;缺省为 ``http://127.0.0.1:38080``。"""
v = file_data.get("http_base_url")
if v is not None and str(v).strip():
return str(v).strip().rstrip("/")
return _DEFAULT_HTTP_BASE