Files
hospital-front-agent/agent/config.py
T
lianlonggangandClaude 0b13de14eb feat: 升级流程增强 + 网关化配置 + 重连循环化
- 配置化:gateway.url/socket.path/download.prefix(网关基础地址统一推导 ws 与下载地址)、
  main.home 主目录(jar/pid 相对它推导)、order.basic;properties 中文 \uXXXX 转义自动还原
- 升级流程:下载 PK 魔数检查与诊断日志;SHA 校验;替换改「改名腾位」绕开 Windows 文件锁延迟;
  nssm stop/start 驱动停止与拉起(pid 强杀回退);升级中标志暂停看门狗拉起避免抢占 jar 锁
- ws_client:重连改循环实现修复递归栈溢出(长时间断线后 maximum recursion depth);
  退避封顶延长至 5 分钟;心跳线程独立修复「暂无心跳数据」
- 新增命令:GET_APIS/HTTP_PROXY(接口测试代理,纯透传鉴权)、GET_LOG_DOWNLOAD(日志下载备用)
- 诊断增强:taskkill/进程退出/替换重试逐次日志

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-03 16:52:34 +08:00

131 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Agent 配置加载。
从 conf/agent.properties 读取(key=value),支持环境变量覆盖(AGENT_ 前缀大写)。
配置项:
- gateway.url B 端网关基础地址(如 http://10.10.10.228:19901)。
配置后:WebSocket 地址 = 网关地址(scheme转ws) + socket.path + 设备号;
升级下载地址 = 网关地址 + download.prefix + B端下发的文件相对路径。
- socket.path WebSocket 路由相对路径(默认 /medical/agentSocket/,配合 gateway.url 用)
- download.prefix 网关文件下载路由前缀(默认 /file/down,如
http://网关:端口/file/down/hf-upgrade/20260902/xxx.jar
- server.url B 端 WebSocket 完整地址(未配置 gateway.url 时的回退方式,
以 /agentSocket/ 结尾,连接时自动拼接设备号)
- device.id 设备编号(医院唯一,如 jinghe-01)
- device.token 设备鉴权 Token
- main.health.url 主程序健康检查地址(http://127.0.0.1:8082/api/admin/health
- main.admin.token 主程序管理接口 Token(与主程序 admin.token 一致)
- main.home 主程序主目录(默认 ".",即与 Agent 同目录;分目录部署时如 ../main)
- main.jar.path 主程序 jar 相对 main.home 的路径(默认 bin/hospital-front.jar
- main.pid.file 主程序 PID 文件相对 main.home 的路径(默认 pid/hospital-front.pid
- nssm.path NSSM 可执行文件路径(配置 main.service.name 时用于拉起服务)
- main.service.name 主程序注册的 Windows 服务名(配置后看门狗用 nssm restart 拉起,优先于 main.start.cmd
- heartbeat.sec 心跳间隔(默认 15 秒,须小于网关空闲超时)
- guard.enable 是否守护主程序(默认 true)
- log.dir 日志目录(默认 logs,相对部署根目录)
"""
import os
import re
from dataclasses import dataclass
@dataclass
class AgentConfig:
"""Agent 全量配置(不可变)。"""
gateway_url: str
socket_path: str
download_prefix: str
server_url: str
device_id: str
device_token: str
main_health_url: str
main_admin_token: str
main_start_cmd: str
main_home: str
main_jar_path: str
main_pid_file: str
@property
def main_jar(self) -> str:
"""主程序 jar 完整路径(main.home + main.jar.path)。"""
return os.path.join(self.main_home, self.main_jar_path)
@property
def main_pid(self) -> str:
"""主程序 PID 文件完整路径(main.home + main.pid.file)。"""
return os.path.join(self.main_home, self.main_pid_file)
nssm_path: str
main_service_name: str
heartbeat_sec: int
guard_enabled: bool
log_dir: str
order_basic: str
@classmethod
def load(cls, conf_path: str = "conf/agent.properties") -> "AgentConfig":
"""从指定路径加载配置,环境变量优先。"""
props = {}
if os.path.exists(conf_path):
try:
with open(conf_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# 跳过空行与注释
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
props[key.strip()] = _unescape(value.strip())
except Exception:
# 读取失败时按空配置处理,交由各配置项默认值兜底
pass
return cls(
gateway_url=_get(props, "gateway.url", ""),
socket_path=_get(props, "socket.path", "/medical/agentSocket/"),
download_prefix=_get(props, "download.prefix", "/file/down"),
server_url=_get(props, "server.url", "ws://127.0.0.1:19901/medical/agentSocket/unknown-device"),
device_id=_get(props, "device.id", "unknown-device"),
device_token=_get(props, "device.token", ""),
main_health_url=_get(props, "main.health.url", "http://127.0.0.1:8082/api/admin/health"),
main_admin_token=_get(props, "main.admin.token", ""),
main_start_cmd=_get(props, "main.start.cmd", "bin/start.bat"),
main_home=_get(props, "main.home", "."),
main_jar_path=_get(props, "main.jar.path", "bin/hospital-front.jar"),
main_pid_file=_get(props, "main.pid.file", "pid/hospital-front.pid"),
nssm_path=_get(props, "nssm.path", ""),
main_service_name=_get(props, "main.service.name", ""),
heartbeat_sec=int(_get(props, "heartbeat.sec", "15")),
guard_enabled=_get(props, "guard.enable", "true").lower() == "true",
log_dir=_get(props, "log.dir", "logs"),
order_basic=_get(props, "order.basic", ""),
)
def _unescape(value: str) -> str:
"""还原 Java properties 的 \\uXXXX 中文转义(兼容前置的双反斜杠)。
IDEA 等编辑器保存 .properties 时会把非 ASCII 字符(如中文路径)自动转为
\\uXXXX(有时前置双反斜杠);不还原的话路径按字面反斜杠序列处理,找不到目录。
仅替换「反斜杠+u+4位十六进制」序列,Windows 路径分隔符(\\Users 等)不受影响。
"""
# 「N 个反斜杠 + u + 4 位十六进制」:解码为字符并保留 N-1 个反斜杠
# (双反斜杠时前一个是 Windows 路径分隔符,不能吃掉)
return re.sub(
r"(\\+)u([0-9a-fA-F]{4})",
lambda m: "\\" * (len(m.group(1)) - 1) + chr(int(m.group(2), 16)),
value,
)
def _get(props: dict, key: str, default: str) -> str:
"""读取单个配置项:环境变量 > properties > 默认值。"""
env_key = "AGENT_" + key.replace(".", "_").upper()
env = os.environ.get(env_key)
if env is not None:
return env
return props.get(key, default)