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

118 lines
4.2 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 -*-
"""主程序 IPC 客户端(环回 HTTP)。
Agent 通过本机 HTTP 调用主程序被控端 API/api/admin)。
"""
import logging
from urllib.parse import quote
import requests
log = logging.getLogger(__name__)
class MainAppClient:
"""主程序管理接口客户端。"""
def __init__(self, config):
self.config = config
self._session = requests.Session()
# 连接失败快速返回(探测场景多,不能长时间阻塞)
self._session.mount("http://", requests.adapters.HTTPAdapter())
# ================= 业务接口 =================
def health(self) -> dict:
"""主程序健康检查:返回解析后的 dict;不可达返回 {"ok":False,"error":"..."}"""
body = self._get_json(self.config.main_health_url)
if body is None:
return {"ok": False, "error": "主程序不可达"}
try:
node = body.json()
return {
"ok": True,
"code": node.get("code"),
"data": node.get("data"),
}
except Exception as e:
return {"ok": False, "error": f"健康响应解析失败:{e}"}
def version(self) -> dict:
"""主程序版本。"""
body = self._get_json(self._url("/api/admin/version"))
if body is None:
return {"ok": False}
try:
node = body.json()
return {"ok": True, "data": node.get("data")}
except Exception as e:
return {"ok": False, "error": str(e)}
def log_tail(self, file: str, lines: int) -> str | None:
"""日志尾部(原始 JSON 文本)。"""
return self._get_text(self._url(f"/api/admin/log/tail?file={quote(file)}&lines={lines}"))
def log_files(self) -> str | None:
"""日志文件列表(原始 JSON 文本)。"""
return self._get_text(self._url("/api/admin/log/files"))
def restart(self, delay_ms: int) -> bool:
"""触发主程序优雅重启。"""
return self._post_text(self._url(f"/api/admin/restart?delayMs={delay_ms}"), "") is not None
def config_file(self, hospital: str) -> str | None:
"""读取医院配置文件(原始 JSON 文本)。"""
return self._get_text(self._url(f"/api/admin/config/file?hospital={quote(hospital)}"))
def save_config_file(self, hospital: str, content: str) -> bool:
"""写回医院配置文件。"""
body = self._post_text(
self._url("/api/admin/config/file"),
f"hospital={quote(hospital)}&content={quote(content)}",
)
return body is not None
# ================= 基础 HTTP =================
def _url(self, path: str) -> str:
"""基于健康地址推导 /api/admin 基地址。"""
idx = self.config.main_health_url.find("/api/")
base = self.config.main_health_url[:idx] if idx >= 0 else self.config.main_health_url
return base + path
def _get_json(self, url: str):
"""GET 并返回 Response 对象;失败返回 None。"""
try:
resp = self._session.get(
url,
headers={"X-Admin-Token": self.config.main_admin_token},
timeout=(5, 30),
)
return resp if resp.ok else None
except Exception as e:
log.debug("GET %s 失败:%s", url, e)
return None
def _get_text(self, url: str) -> str | None:
"""GET 并返回文本;失败返回 None。"""
resp = self._get_json(url)
return resp.text if resp is not None else None
def _post_text(self, url: str, form_body: str) -> str | None:
"""POST 表单并返回文本;失败返回 None。"""
try:
resp = self._session.post(
url,
headers={
"X-Admin-Token": self.config.main_admin_token,
"Content-Type": "application/x-www-form-urlencoded",
},
data=form_body.encode("utf-8"),
timeout=(5, 30),
)
return resp.text if resp.ok else None
except Exception as e:
log.debug("POST %s 失败:%s", url, e)
return None