新增系统健康快照并更新优化文档

This commit is contained in:
liuwei
2026-04-30 15:07:03 +08:00
parent ce38f66b7b
commit 83910b287b
3 changed files with 401 additions and 3 deletions

View File

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

View File

@@ -125,6 +125,36 @@
</el-col>
</el-row>
<el-row :gutter="16" class="health-row">
<el-col :span="24">
<el-card class="health-overview-card" shadow="hover">
<div class="section-heading section-heading--stack">
<div>
<h3>系统健康快照</h3>
<p>把连接状态、插件运行、异常数量与转图运行时集中到一个面板里。</p>
</div>
<div class="health-overview-meta">
<span class="health-overview-meta__label">最近刷新</span>
<span class="health-overview-meta__value">{% raw %}{{ healthSummary.timestamp || '-' }}{% endraw %}</span>
</div>
</div>
<div class="health-grid">
<div v-for="card in healthCards" :key="card.key" class="health-item" :class="`health-item--${card.status}`">
<div class="health-item__head">
<span class="health-item__title">{% raw %}{{ card.title }}{% endraw %}</span>
<span class="health-item__badge" :class="`health-item__badge--${card.status}`">
{% raw %}{{ getHealthStatusText(card.status) }}{% endraw %}
</span>
</div>
<div class="health-item__value">{% raw %}{{ card.value }}{% endraw %}</div>
<div class="health-item__summary">{% raw %}{{ card.summary }}{% endraw %}</div>
<div v-if="card.extra" class="health-item__extra">{% raw %}{{ card.extra }}{% endraw %}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="stats-highlight-row">
<el-col :span="8">
<el-card class="insight-card" shadow="hover">
@@ -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;

View File

@@ -17,6 +17,7 @@
- 已剥离未实际使用的事件系统实现,减少主链路无效抽象
- 已将插件调用统计改为主链路直接埋点,降低维护复杂度
- 已在消息主链路接入 `trace_id`,用于串联消息处理、插件统计与异常日志
- 已在后台首页补充“系统健康快照”,可集中查看机器人连接、插件运行、近 24 小时异常与 md2img 运行状态
## 2. 项目现状判断
@@ -310,6 +311,11 @@
- 让系统运行状态可视化、可量化
当前进展:
- 第一阶段已完成:首页已增加系统健康快照,可快速查看核心运行状态
- 后续可继续补充更细粒度的吞吐、延迟、存储连接与 AI 调用链指标
建议内容:
- 增加系统吞吐量指标