diff --git a/admin/dashboard/blueprints/system.py b/admin/dashboard/blueprints/system.py index b2a0e81..7eabebd 100644 --- a/admin/dashboard/blueprints/system.py +++ b/admin/dashboard/blueprints/system.py @@ -11,6 +11,7 @@ from collections import deque import gzip import json import yaml +from utils.markdown_to_image import get_md2img_health_snapshot, warmup_md2img_browser_sync # 创建系统信息蓝图 system_bp = Blueprint('system', __name__) @@ -305,6 +306,47 @@ def update_system_llm_config(): return jsonify({"success": False, "message": str(e)}), 500 +@system_bp.route('/api/system/md2img_health', methods=['GET']) +@login_required +def get_md2img_health(): + """查询 Markdown 转图运行时健康状态。""" + try: + # 默认只读取状态,不主动拉起 runtime。 + # 当后台希望“刷新并顺便拉起”时,可传 ensure_runtime=true。 + ensure_runtime = str(request.args.get('ensure_runtime', 'false')).strip().lower() in {'1', 'true', 'yes', 'on'} + data = get_md2img_health_snapshot(ensure_runtime=ensure_runtime) + return jsonify({"success": True, "data": data}) + except Exception as e: + logger.error(f"获取 md2img 健康状态失败: {e}") + return jsonify({"success": False, "message": str(e)}), 500 + + +@system_bp.route('/api/system/md2img_warmup', methods=['POST']) +@login_required +def trigger_md2img_warmup(): + """手动触发 Markdown 转图浏览器预热。""" + try: + payload = request.get_json(silent=True) or {} + timeout_seconds = int(payload.get('timeout_seconds', 45) or 45) + timeout_seconds = max(10, min(timeout_seconds, 180)) + ok = warmup_md2img_browser_sync(timeout_seconds=timeout_seconds) + data = get_md2img_health_snapshot(ensure_runtime=False) + if ok: + return jsonify({ + "success": True, + "message": f"预热完成(timeout={timeout_seconds}s)", + "data": data, + }) + return jsonify({ + "success": False, + "message": f"预热失败(timeout={timeout_seconds}s),请查看运行日志", + "data": data, + }), 500 + except Exception as e: + logger.error(f"触发 md2img 预热失败: {e}") + return jsonify({"success": False, "message": str(e)}), 500 + + @system_bp.route('/api/restart_service', methods=['POST']) @login_required def restart_service(): diff --git a/admin/dashboard/templates/system_status.html b/admin/dashboard/templates/system_status.html index 7202584..7799ba1 100644 --- a/admin/dashboard/templates/system_status.html +++ b/admin/dashboard/templates/system_status.html @@ -9,8 +9,23 @@
System Workspace

资源监控

直接在后台查看系统资源变化与运行状态,保持监控入口简洁清晰。

+
+ 转图运行时 + {% raw %}{{ runtimeTagText }}{% endraw %} + {% raw %}{{ browserTagText }}{% endraw %} + {% raw %}{{ md2imgBrief }}{% endraw %} + + {% raw %}{{ md2imgHealth.timestamp }}{% endraw %} + +
+ + 预热转图 + + + 刷新转图状态 + 刷新面板 新窗口打开 重启服务 @@ -41,11 +56,42 @@ return { currentView: '14', frameUrl: '{{ src_url }}', - restarting: false + restarting: false, + md2imgLoading: false, + md2imgWarming: false, + md2imgHealth: null + } + }, + computed: { + runtimeTagText() { + const runtime = this.md2imgHealth && this.md2imgHealth.runtime ? this.md2imgHealth.runtime : {}; + return runtime.loop_running ? '运行时在线' : '运行时未就绪'; + }, + runtimeTagType() { + const runtime = this.md2imgHealth && this.md2imgHealth.runtime ? this.md2imgHealth.runtime : {}; + return runtime.loop_running ? 'success' : 'warning'; + }, + browserTagText() { + const browser = this.md2imgHealth && this.md2imgHealth.browser ? this.md2imgHealth.browser : {}; + return browser.connected ? '浏览器已连接' : '浏览器未连接'; + }, + browserTagType() { + const browser = this.md2imgHealth && this.md2imgHealth.browser ? this.md2imgHealth.browser : {}; + return browser.connected ? 'success' : 'info'; + }, + md2imgBrief() { + if (!this.md2imgHealth) return '尚未获取状态'; + const runtime = this.md2imgHealth.runtime || {}; + const browser = this.md2imgHealth.browser || {}; + const loopText = runtime.loop_id ? `loop=${runtime.loop_id}` : 'loop=-'; + const pidText = browser.pid ? `pid=${browser.pid}` : 'pid=-'; + const sourceText = browser.launch_source ? `source=${browser.launch_source}` : 'source=-'; + return `${loopText} · ${pidText} · ${sourceText}`; } }, mounted() { this.currentView = '14'; + this.loadMd2ImgHealth(); }, methods: { reloadIframe() { @@ -80,6 +126,41 @@ } finally { this.restarting = false; } + }, + async loadMd2ImgHealth() { + if (this.md2imgLoading) return; + this.md2imgLoading = true; + try { + // 默认不强制拉起 runtime,避免纯查看状态时引入副作用。 + const response = await axios.get('/api/system/md2img_health'); + if (response.data && response.data.success) { + this.md2imgHealth = response.data.data || null; + } else { + this.$message.error(response.data?.message || '获取转图状态失败'); + } + } catch (error) { + this.$message.error(error.response?.data?.message || '获取转图状态失败'); + } finally { + this.md2imgLoading = false; + } + }, + async warmupMd2Img() { + if (this.md2imgWarming) return; + this.md2imgWarming = true; + try { + const response = await axios.post('/api/system/md2img_warmup', { timeout_seconds: 60 }); + if (response.data && response.data.success) { + this.$message.success(response.data.message || '转图预热成功'); + this.md2imgHealth = response.data.data || this.md2imgHealth; + } else { + this.$message.error(response.data?.message || '转图预热失败'); + } + } catch (error) { + this.$message.error(error.response?.data?.message || '转图预热失败'); + } finally { + this.md2imgWarming = false; + this.loadMd2ImgHealth(); + } } } }); @@ -96,6 +177,20 @@ .page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; } .page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; } .page-hero-copy p { color: #64748b; font-size: 14px; } + .md2img-health-inline { + margin-top: 12px; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 8px 10px; + border-radius: 12px; + background: rgba(255,255,255,0.72); + border: 1px solid rgba(148, 163, 184, 0.18); + } + .health-title { font-size: 12px; font-weight: 700; color: #334155; } + .health-brief { font-size: 12px; color: #475569; } + .health-time { font-size: 12px; color: #94a3b8; } .workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .workspace-header h3 { font-size: 18px; margin-bottom: 4px; } .workspace-header p { font-size: 13px; color: #64748b; } @@ -112,6 +207,7 @@ .workspace-header { flex-direction: column; align-items: flex-start; } .page-hero-actions { flex-wrap: wrap; } .iframe-url { max-width: 100%; } + .md2img-health-inline { width: 100%; } } {% endblock %} diff --git a/utils/markdown_to_image.py b/utils/markdown_to_image.py index eed4468..9c67301 100644 --- a/utils/markdown_to_image.py +++ b/utils/markdown_to_image.py @@ -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()