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
+33 -19
View File
@@ -36,16 +36,16 @@ class AgentClient:
# ================= 生命周期 =================
def start(self):
"""启动:连接线程 + 心跳线程。
"""启动:连接循环线程 + 心跳线程。
注意:_connect 内部 run_forever 永久阻塞(断线重连也在其中递归),
必须放独立线程,否则心跳线程永远起不来(B 端会显示"暂无心跳数据")。
连接线程跑 _connect_loop(循环重连,非递归——递归实现会在长时间断线后
撑爆 Python 调用栈,出现 maximum recursion depth exceeded)。
首次心跳立即发送,避免网关空闲超时在首个周期内掐断连接。
"""
t_heartbeat = threading.Thread(target=self._heartbeat_loop, name="heartbeat", daemon=True)
t_heartbeat.start()
self._threads.append(t_heartbeat)
t_connect = threading.Thread(target=self._connect, name="ws-connect", daemon=True)
t_connect = threading.Thread(target=self._connect_loop, name="ws-connect", daemon=True)
t_connect.start()
self._threads.append(t_connect)
@@ -63,10 +63,34 @@ class AgentClient:
# ================= 连接管理 =================
def _connect(self):
"""建立 WebSocket 连接(阻塞直到连接断开,内部自行调度重连)。"""
# server.url 为通道基地址(以 /medical/agentSocket/ 结尾),设备号统一由 device.id 拼接
url = self.config.server_url.rstrip("/") + "/" + self.config.device_id
def _connect_loop(self):
"""连接循环:断线后指数退避重连(循环代替递归,避免长时间断线栈溢出)。"""
while not self._heartbeat_stop.is_set():
try:
self._connect_once()
except Exception as e:
log.warning("连接过程异常:%s", e)
# 指数退避:1s 起翻倍至 5 分钟封顶(连接成功后 on_open 清零计数)
self._reconnect_attempts += 1
attempt = self._reconnect_attempts
delay = min(1000 * (2 ** min(attempt - 1, 9)), 300_000) / 1000.0
log.info("%.0fs 后进行第 %d 次重连", delay, attempt)
if self._heartbeat_stop.wait(delay):
return
def _connect_once(self):
"""建立一次 WebSocket 连接(阻塞直到连接断开;重连由 _connect_loop 负责)。"""
# 优先用网关基础地址推导:gateway.url(http→ws) + socket.path + 设备号
# 未配置 gateway.url 时回退 server.url(以 /agentSocket/ 结尾)+ 设备号
if self.config.gateway_url:
ws_base = self.config.gateway_url.rstrip("/")
if ws_base.startswith("http://"):
ws_base = "ws://" + ws_base[len("http://"):]
elif ws_base.startswith("https://"):
ws_base = "wss://" + ws_base[len("https://"):]
url = ws_base + "/" + self.config.socket_path.strip("/") + "/" + self.config.device_id
else:
url = self.config.server_url.rstrip("/") + "/" + self.config.device_id
log.info("连接管理服务器:%s", url)
headers = [
@@ -95,13 +119,13 @@ class AgentClient:
def on_close(ws, code, reason):
self._connected = False
# 区分失败类型:1000/1001 为正常关闭;其他关闭码(如 1008 策略拒绝)多为设备未注册或 token 错误
# 重连由 _connect_loop 统一调度(on_close 不再递归调度)
if code in (1000, 1001):
log.info("连接关闭:code=%s, reason=%s", code, reason)
elif reason:
log.warning("连接被服务器拒绝(设备未注册或 token 错误):code=%s, reason=%s", code, reason)
else:
log.warning("连接被服务器关闭:code=%s(设备未注册或 token 错误)", code)
self._schedule_reconnect()
def on_error(ws, error):
self._connected = False
@@ -127,16 +151,6 @@ class AgentClient:
# 阻塞运行(跳过协议层 ping,应用层心跳已足够;关掉默认 ping 避免双重心跳)
ws.run_forever(ping_interval=25, ping_timeout=10)
def _schedule_reconnect(self):
"""指数退避重连:1s 起,翻倍至 60s 封顶。"""
self._reconnect_attempts += 1
attempt = self._reconnect_attempts
delay = min(1000 * (2 ** min(attempt - 1, 6)), 60_000) / 1000.0
log.info("%.0fs 后进行第 %d 次重连", delay, attempt)
time.sleep(delay)
if not self._heartbeat_stop.is_set():
self._connect()
# ================= 心跳 =================
def _heartbeat_loop(self):