Files
hospital-front-agent/agent/dispatcher.py
T
lianlonggangandClaude 68751c2354 feat: Python 版前置机 Agent 完整实现
- WebSocket 连接 B 端(注册/心跳/指数退避重连)
- 命令分发(10 种远程运维命令)
- 看门狗守护、日志实时流、在线升级含自动回滚
- 修复 main.py 日志格式 style 与占位符不匹配的问题
- 补充 README、配置示例、依赖清单

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 11:58:31 +08:00

142 lines
5.7 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 升级主程序
所有命令执行结果通过 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":
ok = self.main_app_client.restart(1000)
result = {
"ok": ok,
"msg": "已触发优雅重启,等待 ProcessGuard 拉起" if ok else "重启请求失败",
}
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 上报)"}
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)