feat: RESTART 走 NSSM 服务重启 + 失败原因可观测化

- RESTART 命令优先通过 nssm restart 重启主程序服务(假死场景可用),未配置服务名回退环回 HTTP
- nssm 输出按 UTF-16 解码并记录日志,权限不足时提示需以服务方式运行
- 环回管理接口 code=3001 时明确报鉴权失败,不再误报不可达
- 新增 nssm.path / main.service.name / log.dir 配置项
- 增加文件日志(RotatingFileHandler 5MB×5),服务方式运行可查日志
- HTTP 调用失败与状态码异常统一 WARNING 记录(原 DEBUG 不可见)
- 断线日志区分「网络不通」与「被服务器拒绝(未注册/token 错误)」
- 连接 URL 自动拼接 device.id,消除双处配置不一致

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-27 17:24:17 +08:00
co-authored by Claude
parent 86bbe8ef5a
commit f07bbbc49d
7 changed files with 144 additions and 26 deletions
+24 -8
View File
@@ -57,9 +57,13 @@ class MainAppClient:
"""日志文件列表(原始 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 restart(self, delay_ms: int) -> tuple[bool, str]:
"""触发主程序优雅重启。返回 (是否成功, 失败原因)。"""
body, reason = self._post_text_with_reason(self._url(f"/api/admin/restart?delayMs={delay_ms}"), "")
# 主程序管理接口鉴权失败时返回 code=3001 的 JSONHTTP 层可能为 200
if body is not None and "3001" in body:
return False, "管理接口鉴权失败(main.admin.token 与主程序不一致)"
return (body is not None), reason
def config_file(self, hospital: str) -> str | None:
"""读取医院配置文件(原始 JSON 文本)。"""
@@ -89,9 +93,12 @@ class MainAppClient:
headers={"X-Admin-Token": self.config.main_admin_token},
timeout=(5, 30),
)
return resp if resp.ok else None
if not resp.ok:
log.warning("GET %s 失败:HTTP %s", url, resp.status_code)
return None
return resp
except Exception as e:
log.debug("GET %s 失败:%s", url, e)
log.warning("GET %s 失败:%s", url, e)
return None
def _get_text(self, url: str) -> str | None:
@@ -101,6 +108,11 @@ class MainAppClient:
def _post_text(self, url: str, form_body: str) -> str | None:
"""POST 表单并返回文本;失败返回 None。"""
body, _ = self._post_text_with_reason(url, form_body)
return body
def _post_text_with_reason(self, url: str, form_body: str) -> tuple[str | None, str]:
"""POST 表单并返回 (文本, 失败原因);成功时原因为空串。"""
try:
resp = self._session.post(
url,
@@ -111,7 +123,11 @@ class MainAppClient:
data=form_body.encode("utf-8"),
timeout=(5, 30),
)
return resp.text if resp.ok else None
if not resp.ok:
reason = f"HTTP {resp.status_code}"
log.warning("POST %s 失败:%s", url, reason)
return None, reason
return resp.text, ""
except Exception as e:
log.debug("POST %s 失败:%s", url, e)
return None
log.warning("POST %s 失败:%s", url, e)
return None, str(e)