新增转图运行时健康监控与手动预热

变更项:\n1. 在 markdown_to_image 增加 get_md2img_health_snapshot 健康快照能力,输出 runtime 线程、事件循环、浏览器连接、启动来源与 PID 状态。\n2. 新增系统接口 GET /api/system/md2img_health,支持后台查询转图运行时健康信息。\n3. 新增系统接口 POST /api/system/md2img_warmup,支持后台手动触发转图预热并返回最新状态。\n4. 在资源监控页面接入转图健康状态条,展示运行时在线状态、浏览器连接状态及关键摘要信息。\n5. 在资源监控页面增加转图预热与状态刷新按钮,便于线上快速自愈与排障。\n6. 补充中文注释与错误提示,保持后端与前端可观测性一致。
This commit is contained in:
liuwei
2026-04-17 10:04:18 +08:00
parent 5a84c60b2c
commit c49f5e509c
3 changed files with 206 additions and 1 deletions

View File

@@ -686,6 +686,73 @@ def _get_md2img_runtime() -> _Md2ImgRuntime:
return _MD2IMG_RUNTIME
def get_md2img_health_snapshot(ensure_runtime: bool = False) -> dict:
"""获取 Markdown 转图运行时健康快照(同步)。
Args:
ensure_runtime: 是否在采集前确保运行时已启动。
- False: 仅观察当前状态,不主动拉起线程;
- True: 先启动 md2img runtime再返回状态适合后台手动“刷新并拉起”场景。
Returns:
dict: 结构化健康信息,便于后台页面直接展示。
"""
runtime = _get_md2img_runtime()
if ensure_runtime:
# 显式拉起运行时,方便后台做一次“冷启动检查”。
runtime.ensure_started()
thread_obj = getattr(runtime, "_thread", None)
loop_obj = getattr(runtime, "_loop", None)
runtime_started = bool(thread_obj is not None)
runtime_thread_alive = bool(thread_obj.is_alive()) if thread_obj else False
runtime_loop_running = bool(loop_obj.is_running()) if loop_obj else False
runtime_loop_id = id(loop_obj) if loop_obj else None
runtime_thread_name = thread_obj.name if thread_obj else ""
browser_manager = _BROWSER_MANAGER
browser_connected = False
browser_loop_owner = None
browser_launch_source = ""
browser_pid = None
browser_proc_alive = None
browser_error = ""
if browser_manager is not None:
try:
browser_obj = getattr(browser_manager, "_browser", None)
browser_connected = bool(browser_obj and browser_obj.is_connected())
browser_loop_owner = getattr(browser_manager, "_owner_loop_id", None)
browser_launch_source = str(getattr(browser_manager, "_last_launch_source", "") or "")
browser_pid = getattr(getattr(browser_obj, "process", None), "pid", None) if browser_obj else None
if browser_pid:
# 通过 psutil 二次确认进程是否仍在,避免只看到历史 PID。
browser_proc_alive = psutil.pid_exists(int(browser_pid))
else:
browser_proc_alive = None
except Exception as e:
browser_error = str(e)
return {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"runtime": {
"started": runtime_started,
"thread_alive": runtime_thread_alive,
"thread_name": runtime_thread_name,
"loop_running": runtime_loop_running,
"loop_id": runtime_loop_id,
},
"browser": {
"connected": browser_connected,
"owner_loop_id": browser_loop_owner,
"launch_source": browser_launch_source,
"pid": browser_pid,
"pid_alive": browser_proc_alive,
"error": browser_error,
},
}
async def _run_in_md2img_runtime(coro, timeout_seconds: Optional[int] = None):
"""在 md2img 专用事件循环中执行协程,并在当前调用方 loop 中异步等待结果。"""
runtime = _get_md2img_runtime()