Files
hospital-front-agent/agent/dispatcher.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

175 lines
7.4 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 -*-
"""下行命令分发器。
B 端通过 WebSocket 下发 JSON 命令:{"type":"CMD_TYPE","payload":{...}}
已支持命令:
- PING 探活(立即回 pong)
- GET_HEALTH 查询主程序健康
- GET_VERSION 查询主程序版本
- GET_LOG_FILES 日志文件列表
- LOG_TAIL 日志尾部(payload: file, lines
- LOG_TAIL_CTRL 日志实时流开关(payload: action, file
- RESTART 重启主程序(优雅停机后由 ProcessGuard 拉起)
- GET_CONFIG 读取医院配置文件(payload: hospital
- SAVE_CONFIG 写回医院配置文件(payload: hospital, content
- UPGRADE 升级主程序
- GET_APIS 查询主程序 /order 接口清单
- HTTP_PROXY 代理调用前置机任意接口(穿透白名单)
所有命令执行结果通过 AgentClient 上行(CMD_RESULT)。
"""
import logging
import threading
import time
log = logging.getLogger(__name__)
class CommandDispatcher:
"""命令分发:解析 type → 执行 → 回执 CMD_RESULT(透传 cmdId)。"""
def __init__(self, config, main_app_client):
self.config = config
self.main_app_client = main_app_client
self._client = None # 由 main 注入(避免构造环)
self._upgrade_command = None # 升级命令(懒加载)
self._log_stream = None # 日志实时流管理(懒加载)
self._log_stream_lock = threading.Lock()
self._upgrade_lock = threading.Lock()
def set_client(self, client):
"""注入 AgentClientmain 组装阶段调用,避免构造环)。"""
self._client = client
def _get_upgrade_command(self):
"""懒加载升级命令。"""
with self._upgrade_lock:
if self._upgrade_command is None:
from agent.upgrade import UpgradeCommand
self._upgrade_command = UpgradeCommand(self.config, self.main_app_client, self._client)
return self._upgrade_command
def _get_log_stream(self):
"""懒加载日志流管理器。"""
with self._log_stream_lock:
if self._log_stream is None:
from agent.log_stream import LogStreamManager
self._log_stream = LogStreamManager(self.config, self.main_app_client, self._client)
return self._log_stream
def shutdown_log_stream(self):
"""日志实时流(供 main 停机时统一释放)。"""
self._get_log_stream().stop_all()
# ================= 分发 =================
def dispatch(self, msg_type, payload):
"""分发并执行命令。payload 为 dict 或 None。"""
log.info("收到命令:%s", msg_type)
payload = payload if isinstance(payload, dict) else None
if msg_type == "PING":
# 命令协议:B 端下发的 payload 内可带 cmdId,回执时透传以便并发匹配
result = {"pong": True, "ts": int(time.time())}
elif msg_type == "GET_HEALTH":
result = self.main_app_client.health()
elif msg_type == "GET_VERSION":
result = self.main_app_client.version()
elif msg_type == "GET_LOG_FILES":
result = {"ok": True, "data": self.main_app_client.log_files()}
elif msg_type == "LOG_TAIL":
file = (payload or {}).get("file") or "info.log"
lines = (payload or {}).get("lines") or 200
result = {"ok": True, "data": self.main_app_client.log_tail(file, lines)}
elif msg_type == "LOG_TAIL_CTRL":
action = (payload or {}).get("action") or "start"
file = (payload or {}).get("file") or "info.log"
result = self._get_log_stream().control(action, file)
elif msg_type == "RESTART":
# 优先走 NSSM 服务重启(主程序假死时 HTTP 不可达也能重启成功);
# 未配置服务名时回退环回 HTTP 触发主程序优雅重启
if self.config.main_service_name:
from agent.process_guard import restart_main_service
ok, reason = restart_main_service(self.config.nssm_path, self.config.main_service_name)
result = {
"ok": ok,
"msg": "已通过 NSSM 重启主程序服务" if ok else f"NSSM 重启失败:{reason}",
}
else:
ok, reason = self.main_app_client.restart(1000)
result = {
"ok": ok,
"msg": "已触发优雅重启,等待 ProcessGuard 拉起" if ok else f"重启请求失败:{reason}",
}
elif msg_type == "GET_CONFIG":
hospital = (payload or {}).get("hospital") or ""
content = self.main_app_client.config_file(hospital)
result = {"ok": content is not None, "data": content}
elif msg_type == "SAVE_CONFIG":
hospital = (payload or {}).get("hospital") or ""
content = (payload or {}).get("content") or ""
result = {"ok": self.main_app_client.save_config_file(hospital, content)}
elif msg_type == "UPGRADE":
# 异步执行(升级耗时长,避免阻塞 WS 消息循环)
version = (payload or {}).get("version") or "unknown"
download_url = (payload or {}).get("downloadUrl")
sha256 = (payload or {}).get("sha256")
if not download_url or not download_url.strip():
result = {"ok": False, "msg": "缺少 downloadUrl"}
else:
threading.Thread(
target=self._run_upgrade,
args=(version, download_url, sha256),
name="upgrade",
daemon=True,
).start()
result = {"ok": True, "msg": "升级已开始(异步执行,结果将通过 UPGRADE_RESULT 上报)"}
elif msg_type == "GET_APIS":
# 返回主程序 /order 接口清单(供 B 端接口测试工具加载)
result = {"ok": True, "data": self.main_app_client.get_apis()}
elif msg_type == "GET_LOG_DOWNLOAD":
# 日志全量下载(供 B 端浏览器下载,Agent 自动带 admin token 调主程序)
file = (payload or {}).get("file") or "info.log"
content = self.main_app_client.log_download(file)
result = {"ok": content is not None, "data": content}
elif msg_type == "HTTP_PROXY":
# 代理调用前置机任意接口(穿透白名单,本机环回)
p = payload or {}
result = self.main_app_client.http_proxy(
method=p.get("method") or "GET",
path=p.get("path") or "/",
query=p.get("query"),
body=p.get("body"),
extra_headers=p.get("headers"),
)
else:
result = {"ok": False, "msg": f"未知命令:{msg_type}"}
envelope = {"cmd": msg_type}
# 透传 B 端下发的 cmdId(并发命令匹配用)
if payload is not None and payload.get("cmdId") is not None:
envelope["cmdId"] = payload.get("cmdId")
envelope["result"] = result
self._client.send("CMD_RESULT", envelope)
def _run_upgrade(self, version, download_url, sha256):
"""升级线程体:执行并上报 UPGRADE_RESULT。"""
r = self._get_upgrade_command().upgrade(version, download_url, sha256)
self._client.send("UPGRADE_RESULT", r)