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()