diff --git a/admin/dashboard/blueprints/system.py b/admin/dashboard/blueprints/system.py index 948a5c6..235c59d 100644 --- a/admin/dashboard/blueprints/system.py +++ b/admin/dashboard/blueprints/system.py @@ -14,6 +14,7 @@ import yaml import toml from utils.markdown_to_image import get_md2img_health_snapshot, warmup_md2img_browser_sync from utils.ai.llm_registry import LLMRegistry +from base.plugin_common.plugin_interface import PluginStatus # 创建系统信息蓝图 system_bp = Blueprint('system', __name__) @@ -364,6 +365,129 @@ def api_system_info(): return jsonify({"success": False, "error": str(e)}), 500 +@system_bp.route('/api/system_health_summary') +@login_required +def api_system_health_summary(): + """聚合首页可观测性所需的关键健康信号。""" + try: + server = current_app.dashboard_server + robot = getattr(server, "robot", None) + plugin_manager = getattr(server, "plugin_manager", None) + plugin_map = getattr(plugin_manager, "plugins", {}) or {} + + # 统计插件运行状态,便于首页快速判断“加载了多少、真正跑起来多少、是否有异常插件”。 + plugin_status_counter = { + "total": len(plugin_map), + "running": 0, + "loaded": 0, + "stopped": 0, + "error": 0, + "unloaded": 0, + "unknown": 0, + } + for plugin in plugin_map.values(): + status = getattr(plugin, "status", None) + if status == PluginStatus.RUNNING: + plugin_status_counter["running"] += 1 + elif status == PluginStatus.LOADED: + plugin_status_counter["loaded"] += 1 + elif status == PluginStatus.STOPPED: + plugin_status_counter["stopped"] += 1 + elif status == PluginStatus.ERROR: + plugin_status_counter["error"] += 1 + elif status == PluginStatus.UNLOADED: + plugin_status_counter["unloaded"] += 1 + else: + plugin_status_counter["unknown"] += 1 + + # 错误数量直接复用现有统计库,避免为了首页卡片再单独写一套 SQL。 + _, recent_error_count = server.stats_db.get_error_logs(days=1, page=1, limit=1) + + # md2img 健康快照已经有现成实现,这里只做聚合,不主动预热运行时。 + md2img_snapshot = get_md2img_health_snapshot(ensure_runtime=False) or {} + browser_ready = bool( + md2img_snapshot.get("browser_ready") + or md2img_snapshot.get("playwright_ready") + or md2img_snapshot.get("ready") + ) + runtime_ready = bool( + md2img_snapshot.get("runtime_ready") + or md2img_snapshot.get("runtime_initialized") + or md2img_snapshot.get("initialized") + ) + md2img_healthy = runtime_ready and browser_ready + + # 首页只需要“够判断”的轻量结论,因此统一产出 status + summary 文本,前端无需重复拼装业务规则。 + robot_running = bool(getattr(robot, "ipad_running", False)) + robot_nickname = str(getattr(robot, "nickname", "") or "").strip() + robot_wxid = str(getattr(robot, "wxid", "") or "").strip() + robot_summary = "已连接并正在处理消息" if robot_running else "未连接或主循环未运行" + if robot_nickname or robot_wxid: + robot_summary = f"{robot_summary} · {robot_nickname or robot_wxid}" + + if plugin_status_counter["error"] > 0: + plugin_status = "warning" + plugin_summary = f"异常 {plugin_status_counter['error']} 个,运行中 {plugin_status_counter['running']} / {plugin_status_counter['total']}" + elif plugin_status_counter["running"] == 0 and plugin_status_counter["total"] > 0: + plugin_status = "warning" + plugin_summary = f"暂无运行中插件,共加载 {plugin_status_counter['total']} 个" + else: + plugin_status = "healthy" + plugin_summary = f"运行中 {plugin_status_counter['running']} / {plugin_status_counter['total']}" + + if recent_error_count > 0: + error_status = "warning" + error_summary = f"近 24 小时记录到 {recent_error_count} 条异常" + else: + error_status = "healthy" + error_summary = "近 24 小时未记录到异常" + + if md2img_healthy: + md2img_status = "healthy" + md2img_summary = "运行时与浏览器均已就绪" + elif runtime_ready or browser_ready: + md2img_status = "warning" + md2img_summary = "运行时部分可用,建议检查预热状态" + else: + md2img_status = "danger" + md2img_summary = "运行时未就绪,相关转图能力可能不可用" + + return jsonify({ + "success": True, + "data": { + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "robot": { + "status": "healthy" if robot_running else "danger", + "running": robot_running, + "nickname": robot_nickname, + "wxid": robot_wxid, + "summary": robot_summary, + }, + "plugins": { + "status": plugin_status, + "summary": plugin_summary, + **plugin_status_counter, + }, + "errors": { + "status": error_status, + "recent_24h_count": recent_error_count, + "summary": error_summary, + }, + "md2img": { + "status": md2img_status, + "healthy": md2img_healthy, + "runtime_ready": runtime_ready, + "browser_ready": browser_ready, + "summary": md2img_summary, + "detail": md2img_snapshot, + }, + } + }) + except Exception as e: + logger.error(f"获取系统健康摘要失败: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + @system_bp.route('/api/wx_logs') @login_required def api_wx_logs(): diff --git a/admin/dashboard/templates/index.html b/admin/dashboard/templates/index.html index 744b180..23f6758 100644 --- a/admin/dashboard/templates/index.html +++ b/admin/dashboard/templates/index.html @@ -125,6 +125,36 @@ + + + +
+
+

系统健康快照

+

把连接状态、插件运行、异常数量与转图运行时集中到一个面板里。

+
+
+ 最近刷新 + {% raw %}{{ healthSummary.timestamp || '-' }}{% endraw %} +
+
+
+
+
+ {% raw %}{{ card.title }}{% endraw %} + + {% raw %}{{ getHealthStatusText(card.status) }}{% endraw %} + +
+
{% raw %}{{ card.value }}{% endraw %}
+
{% raw %}{{ card.summary }}{% endraw %}
+
{% raw %}{{ card.extra }}{% endraw %}
+
+
+
+
+
+ @@ -304,6 +334,35 @@ uptime: 0, timestamp: '-' }, + healthSummary: { + timestamp: '-', + robot: { + status: 'warning', + running: false, + nickname: '', + wxid: '', + summary: '加载中...' + }, + plugins: { + status: 'warning', + total: 0, + running: 0, + error: 0, + summary: '加载中...' + }, + errors: { + status: 'warning', + recent_24h_count: 0, + summary: '加载中...' + }, + md2img: { + status: 'warning', + healthy: false, + runtime_ready: false, + browser_ready: false, + summary: '加载中...' + } + }, groups: [], selectedGroupForHourlyTrend: '', hourlyTrendDays: 1, @@ -336,15 +395,56 @@ result += minutes + 'M'; return result; + }, + healthCards() { + // 首页健康卡片统一在这里做展示层映射,模板只负责渲染,避免 HTML 中堆太多业务判断。 + const robot = this.healthSummary.robot || {}; + const plugins = this.healthSummary.plugins || {}; + const errors = this.healthSummary.errors || {}; + const md2img = this.healthSummary.md2img || {}; + return [ + { + key: 'robot', + title: '机器人连接', + status: robot.status || 'warning', + value: robot.running ? '在线' : '离线', + summary: robot.summary || '暂无状态', + extra: robot.wxid ? `标识:${robot.wxid}` : '' + }, + { + key: 'plugins', + title: '插件运行', + status: plugins.status || 'warning', + value: `${plugins.running || 0} / ${plugins.total || 0}`, + summary: plugins.summary || '暂无状态', + extra: `异常 ${plugins.error || 0} 个` + }, + { + key: 'errors', + title: '最近异常', + status: errors.status || 'warning', + value: `${errors.recent_24h_count || 0} 条`, + summary: errors.summary || '暂无状态', + extra: '统计窗口:近 24 小时' + }, + { + key: 'md2img', + title: 'Markdown 转图', + status: md2img.status || 'warning', + value: md2img.healthy ? '就绪' : '待检查', + summary: md2img.summary || '暂无状态', + extra: `Runtime ${md2img.runtime_ready ? '已就绪' : '未就绪'} / Browser ${md2img.browser_ready ? '已就绪' : '未就绪'}` + } + ]; } }, mounted() { this.currentView = '1'; this.loadData(); - this.loadSystemInfo(); + this.refreshRuntimeSnapshot(); this.loadCurrentUserInfo(); this.loadGroups(); - this.systemInfoTimer = setInterval(this.loadSystemInfo, 30000); + this.systemInfoTimer = setInterval(this.refreshRuntimeSnapshot, 30000); }, beforeDestroy() { if (this.systemInfoTimer) { @@ -358,6 +458,11 @@ this.loadPluginStats(days); this.loadPluginTrend(days); }, + refreshRuntimeSnapshot() { + // 系统资源与健康聚合都属于运行态信息,统一刷新可以降低后续维护成本。 + this.loadSystemInfo(); + this.loadSystemHealth(); + }, loadSystemInfo() { axios.get('/api/system_info') .then(response => { @@ -372,11 +477,30 @@ console.error('加载系统信息出错:', error); }); }, + loadSystemHealth() { + axios.get('/api/system_health_summary') + .then(response => { + if (response.data.success) { + this.healthSummary = response.data.data || this.healthSummary; + } + }) + .catch(error => { + console.error('加载系统健康摘要出错:', error); + }); + }, renderSystemCharts() { this.renderPieChart('cpuChart', this.systemInfo.cpu_usage, 'CPU使用率'); this.renderPieChart('memoryChart', this.systemInfo.memory_usage, '内存使用率'); this.renderPieChart('diskChart', this.systemInfo.disk_usage, '磁盘使用率'); }, + getHealthStatusText(status) { + const statusMap = { + healthy: '健康', + warning: '关注', + danger: '异常' + }; + return statusMap[status] || '未知'; + }, renderPieChart(chartId, usageValue, label) { const ctx = document.getElementById(chartId); if (!ctx) return; @@ -816,12 +940,132 @@ } .hero-row, + .health-row, .metric-extended-row, .stats-highlight-row, .chart-row { margin-bottom: 16px; } + .health-overview-card .el-card__body { + padding: 20px !important; + } + + .section-heading--stack { + align-items: flex-start; + } + + .health-overview-meta { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 14px; + background: rgba(248, 250, 252, 0.92); + border: 1px solid rgba(148, 163, 184, 0.12); + color: #64748b; + font-size: 13px; + } + + .health-overview-meta__label { + color: #94a3b8; + } + + .health-overview-meta__value { + color: #0f172a; + font-weight: 600; + } + + .health-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin-top: 18px; + } + + .health-item { + padding: 18px; + border-radius: 18px; + border: 1px solid rgba(148, 163, 184, 0.14); + background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.88)); + min-height: 156px; + } + + .health-item--healthy { + box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.10); + } + + .health-item--warning { + box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.10); + } + + .health-item--danger { + box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.10); + } + + .health-item__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 18px; + } + + .health-item__title { + font-size: 14px; + color: #475569; + font-weight: 600; + } + + .health-item__badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 50px; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + } + + .health-item__badge--healthy { + color: #047857; + background: rgba(16, 185, 129, 0.12); + } + + .health-item__badge--warning { + color: #b45309; + background: rgba(245, 158, 11, 0.14); + } + + .health-item__badge--danger { + color: #b91c1c; + background: rgba(239, 68, 68, 0.14); + } + + .health-item__value { + font-size: 28px; + line-height: 1.1; + font-weight: 700; + color: #0f172a; + margin-bottom: 12px; + } + + .health-item__summary { + font-size: 13px; + line-height: 1.7; + color: #475569; + } + + .health-item__extra { + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed rgba(148, 163, 184, 0.22); + font-size: 12px; + color: #94a3b8; + word-break: break-all; + } + .stats-highlight-row { display: flex; flex-wrap: wrap; @@ -1107,6 +1351,7 @@ @media (max-width: 1200px) { .hero-row > .el-col, + .health-row > .el-col, .metric-extended-row > .el-col, .stats-highlight-row > .el-col, .chart-row > .el-col { @@ -1116,6 +1361,10 @@ .metric-grid > .el-col { width: 50% !important; } + + .health-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 900px) { @@ -1125,6 +1374,7 @@ } .hero-row, + .health-row, .metric-extended-row, .stats-highlight-row, .chart-row { @@ -1158,6 +1408,10 @@ .metric-grid > .el-col { width: 100% !important; } + + .health-grid { + grid-template-columns: 1fr; + } } @media (max-width: 768px) { @@ -1186,7 +1440,8 @@ .hero-card, .chart-card, .insight-card, - .metric-card { + .metric-card, + .health-item { min-height: auto; } @@ -1234,6 +1489,7 @@ } .hero-row, + .health-row, .metric-extended-row, .stats-highlight-row, .chart-row { @@ -1253,6 +1509,18 @@ font-size: 12px; } + .health-overview-card .el-card__body { + padding: 14px !important; + } + + .health-item { + padding: 14px; + } + + .health-item__value { + font-size: 24px; + } + .chart-container--large, .chart-container--panel { height: 220px; diff --git a/docs/工程优化与Feature清单.md b/docs/工程优化与Feature清单.md index 17bcc70..ca38830 100644 --- a/docs/工程优化与Feature清单.md +++ b/docs/工程优化与Feature清单.md @@ -17,6 +17,7 @@ - 已剥离未实际使用的事件系统实现,减少主链路无效抽象 - 已将插件调用统计改为主链路直接埋点,降低维护复杂度 - 已在消息主链路接入 `trace_id`,用于串联消息处理、插件统计与异常日志 +- 已在后台首页补充“系统健康快照”,可集中查看机器人连接、插件运行、近 24 小时异常与 md2img 运行状态 ## 2. 项目现状判断 @@ -310,6 +311,11 @@ - 让系统运行状态可视化、可量化 +当前进展: + +- 第一阶段已完成:首页已增加系统健康快照,可快速查看核心运行状态 +- 后续可继续补充更细粒度的吞吐、延迟、存储连接与 AI 调用链指标 + 建议内容: - 增加系统吞吐量指标