Files
hospital-front-agent/agent/dispatcher.py
T
lianlonggangandClaude b572697e17 feat: 心跳修复 + 接口测试代理命令(GET_APIS/HTTP_PROXY)
- ws_client: start() 连接改为独立线程,修复心跳线程被阻塞导致 B 端'暂无心跳数据'
- dispatcher: 新增 GET_APIS(返回主程序接口清单) + HTTP_PROXY(代理调用前置机接口)
- main_app_client: 新增 get_apis/http_proxy,自动按 path 加鉴权(/order→Basic, /api/admin→X-Admin-Token)
- config: 新增 order_basic 配置项
- process_guard: 看门狗支持 nssm restart 拉起服务(优先于 start.cmd)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 13:50:01 +08:00

169 lines
7.1 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 == "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)