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>
This commit is contained in:
2026-09-03 16:52:34 +08:00
co-authored by Claude
parent b572697e17
commit 0b13de14eb
7 changed files with 366 additions and 152 deletions
+51 -3
View File
@@ -4,12 +4,21 @@
从 conf/agent.properties 读取(key=value),支持环境变量覆盖(AGENT_ 前缀大写)。
配置项:
- server.url B 端 WebSocket 地址(经网关转发,须带 /medical 服务前缀)
- 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.start.cmd 主程序启动命令(Windows: bin\\start.batLinux: bin/start.sh
- 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 秒,须小于网关空闲超时)
@@ -18,6 +27,7 @@
"""
import os
import re
from dataclasses import dataclass
@@ -25,12 +35,28 @@ from dataclasses import 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
@@ -52,18 +78,24 @@ class AgentConfig:
continue
if "=" in line:
key, _, value = line.partition("=")
props[key.strip()] = value.strip()
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")),
@@ -73,6 +105,22 @@ class AgentConfig:
)
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()