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
+32 -8
View File
@@ -13,6 +13,8 @@ hospital-agent.exe PyInstaller 打包后)
"""
import logging
import logging.handlers
import os
import signal
import sys
import threading
@@ -29,18 +31,13 @@ _client = None
def run():
"""主流程:加载配置 → 写 PID → 组装组件 → 启动 → 主线程保活。"""
"""主流程:加载配置 → 组装组件 → 启动 → 主线程保活。"""
global _dispatcher, _guard, _client
logging.basicConfig(
level=logging.INFO,
format="{asctime} {levelname} [{name}] {message}",
style="{",
)
# 源码运行时根目录即工作目录,PyInstaller 打包后同理(服务管理器负责设置工作目录)
config = AgentConfig.load()
_setup_logging(config.log_dir)
log.info("Agent 启动:deviceId=%s, server=%s", config.device_id, config.server_url)
# 源码运行时根目录即工作目录,PyInstaller 打包后同理(服务管理器负责设置工作目录)
# 组装组件(延迟导入避免环)
from agent.dispatcher import CommandDispatcher
from agent.process_guard import ProcessGuard
@@ -64,6 +61,33 @@ def run():
threading.Event().wait()
def _setup_logging(log_dir: str):
"""日志输出到控制台 + 文件(服务方式运行时靠文件留存,按大小轮转)。"""
fmt = logging.Formatter(
"{asctime} {levelname} [{name}] {message}",
style="{",
)
root = logging.getLogger()
root.setLevel(logging.INFO)
console = logging.StreamHandler()
console.setFormatter(fmt)
root.addHandler(console)
try:
os.makedirs(log_dir, exist_ok=True)
file_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, "agent.log"),
maxBytes=5 * 1024 * 1024, # 单文件 5MB
backupCount=5, # 保留 5 个轮转备份
encoding="utf-8",
)
file_handler.setFormatter(fmt)
root.addHandler(file_handler)
except Exception as e:
root.warning("日志文件初始化失败(仅控制台输出):%s", e)
def _shutdown(signum, frame):
"""优雅停机:释放日志流 → 停守护 → 断连接 → 退出。"""
log.info("Agent 停机中...signal=%s", signum)