新增系统健康快照并更新优化文档
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user