恢复首页基础设施详细状态与任务调度卡片

This commit is contained in:
liuwei
2026-05-06 08:52:54 +08:00
parent 3730694465
commit ef5db2babd
2 changed files with 762 additions and 106 deletions

View File

@@ -131,7 +131,7 @@
<div class="section-heading section-heading--stack">
<div>
<h3>系统健康快照</h3>
<p>把连接状态、插件运行、异常数量与转图运行时集中到一个面板里。</p>
<p>把连接状态、插件运行、异常数量、LLM 运行态与任务调度集中到一个面板里。</p>
</div>
<div class="health-overview-meta">
<span class="health-overview-meta__label">最近刷新</span>
@@ -148,6 +148,29 @@
</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.serviceBlocks && card.serviceBlocks.length" class="health-service-grid">
<div
v-for="service in card.serviceBlocks"
:key="service.key"
class="health-service-panel"
:class="`health-service-panel--${service.status}`">
<div class="health-service-panel__head">
<div>
<div class="health-service-panel__title">{% raw %}{{ service.title }}{% endraw %}</div>
<div class="health-service-panel__summary">{% raw %}{{ service.summary }}{% endraw %}</div>
</div>
<span class="health-service-panel__badge" :class="`health-service-panel__badge--${service.status}`">
{% raw %}{{ getHealthStatusText(service.status) }}{% endraw %}
</span>
</div>
<div class="health-service-metrics">
<div v-for="metric in service.metrics" :key="metric.label" class="health-service-metric">
<span class="health-service-metric__label">{% raw %}{{ metric.label }}{% endraw %}</span>
<span class="health-service-metric__value">{% raw %}{{ metric.value }}{% endraw %}</span>
</div>
</div>
</div>
</div>
<div v-if="card.extra" class="health-item__extra">{% raw %}{{ card.extra }}{% endraw %}</div>
</div>
</div>
@@ -371,15 +394,38 @@
status: 'warning',
total_calls: 0,
failed_calls: 0,
success_rate: 0,
avg_latency_ms: 0,
summary: '加载中...',
last_call: {}
last_call: {},
scene_count: 0,
target_count: 0,
provider_count: 0,
has_routing: false,
default_scene: '',
default_backend: '',
last_provider: '',
last_backend: '',
last_scene: '',
last_model: '',
last_timestamp: '',
last_latency_ms: 0,
last_error: ''
},
md2img: {
scheduler: {
status: 'warning',
healthy: false,
runtime_ready: false,
browser_ready: false,
total_jobs: 0,
enabled_jobs: 0,
running_jobs: 0,
failed_jobs: 0,
invalid_jobs: 0,
paused_jobs: 0,
never_run_jobs: 0,
system_job_count: 0,
plugin_job_count: 0,
next_run_at: '',
latest_failed_job_name: '',
latest_failed_error: '',
summary: '加载中...'
}
},
@@ -423,7 +469,7 @@
const errors = this.healthSummary.errors || {};
const infrastructure = this.healthSummary.infrastructure || {};
const aiRuntime = this.healthSummary.ai_runtime || {};
const md2img = this.healthSummary.md2img || {};
const scheduler = this.healthSummary.scheduler || {};
return [
{
key: 'robot',
@@ -453,25 +499,30 @@
key: 'infrastructure',
title: '基础设施',
status: infrastructure.status || 'warning',
value: infrastructure.status === 'healthy' ? '正常' : '异常',
value: `${this.countHealthyInfrastructureServices(infrastructure)} / 2`,
summary: infrastructure.summary || '暂无状态',
extra: `MySQL${((infrastructure.mysql || {}).status === 'healthy') ? '正常' : '异常'} / Redis${((infrastructure.redis || {}).status === 'healthy') ? '正常' : '异常'}`
serviceBlocks: this.buildInfrastructureServiceBlocks(infrastructure),
extra: '首页展示的是服务摘要;如果后续要做更深入的运维排查,再单独拆详细页会更合适。'
},
{
key: 'ai_runtime',
title: 'AI 运行态',
title: 'LLM 运行态',
status: aiRuntime.status || 'warning',
value: `${aiRuntime.avg_latency_ms || 0} ms`,
value: (aiRuntime.total_calls || 0) > 0
? `${this.formatMetricNumber(aiRuntime.success_rate, 2)}%`
: `${aiRuntime.scene_count || 0} 个场景`,
summary: aiRuntime.summary || '暂无状态',
extra: `最近调用 ${aiRuntime.total_calls || 0} 次,失败 ${aiRuntime.failed_calls || 0}`
serviceBlocks: this.buildAiRuntimeServiceBlocks(aiRuntime),
extra: this.buildAiRuntimeExtra(aiRuntime)
},
{
key: 'md2img',
title: 'Markdown 转图',
status: md2img.status || 'warning',
value: md2img.healthy ? '就绪' : '待检查',
summary: md2img.summary || '暂无状态',
extra: `Runtime ${md2img.runtime_ready ? '已就绪' : '未就绪'} / Browser ${md2img.browser_ready ? '已就绪' : '未就绪'}`
key: 'scheduler',
title: '任务调度',
status: scheduler.status || 'warning',
value: `${scheduler.enabled_jobs || 0} / ${scheduler.total_jobs || 0}`,
summary: scheduler.summary || '暂无状态',
serviceBlocks: this.buildSchedulerServiceBlocks(scheduler),
extra: this.buildSchedulerExtra(scheduler)
}
];
}
@@ -539,6 +590,133 @@
};
return statusMap[status] || '未知';
},
formatCompactDuration(seconds) {
const totalSeconds = parseInt(seconds) || 0;
if (totalSeconds <= 0) return '-';
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (days > 0) return `${days}D ${hours}H`;
if (hours > 0) return `${hours}H ${minutes}M`;
return `${minutes}M`;
},
formatMetricNumber(value, fractionDigits = 0) {
if (value === null || value === undefined || value === '') return '-';
const numeric = Number(value);
if (Number.isNaN(numeric)) return String(value);
return numeric.toFixed(fractionDigits);
},
countHealthyInfrastructureServices(infrastructure) {
const mysql = infrastructure.mysql || {};
const redis = infrastructure.redis || {};
let count = 0;
if (mysql.status === 'healthy') count += 1;
if (redis.status === 'healthy') count += 1;
return count;
},
buildInfrastructureServiceBlocks(infrastructure) {
const mysql = infrastructure.mysql || {};
const redis = infrastructure.redis || {};
return [
{
key: 'mysql',
title: 'MySQL',
status: mysql.status || 'warning',
summary: mysql.summary || '暂无状态',
metrics: [
{ label: '连接负载', value: `${this.formatMetricNumber(mysql.connection_usage_percent, 1)}%` },
{ label: '连接数', value: `${this.formatMetricNumber(mysql.threads_connected)} / ${mysql.max_connections || '-'}` },
{ label: '运行线程', value: this.formatMetricNumber(mysql.threads_running) },
{ label: 'QPS', value: this.formatMetricNumber(mysql.questions_per_second, 2) },
{ label: '库体积', value: `${this.formatMetricNumber(mysql.schema_size_mb, 2)} MB` },
{ label: '表数量', value: this.formatMetricNumber(mysql.table_count) }
]
},
{
key: 'redis',
title: 'Redis',
status: redis.status || 'warning',
summary: redis.summary || '暂无状态',
metrics: [
{ label: 'Key 数量', value: this.formatMetricNumber(redis.key_count) },
{ label: '客户端', value: this.formatMetricNumber(redis.connected_clients) },
{ label: 'OPS/s', value: this.formatMetricNumber(redis.ops_per_sec) },
{ label: '内存占用', value: redis.used_memory_human || '-' },
{ label: '命中率', value: `${this.formatMetricNumber(redis.hit_rate_percent, 1)}%` },
{ label: '运行时间', value: this.formatCompactDuration(redis.uptime_seconds) }
]
}
];
},
buildAiRuntimeServiceBlocks(aiRuntime) {
return [
{
key: 'ai-routing',
title: '路由配置',
status: aiRuntime.has_routing ? 'healthy' : 'warning',
summary: aiRuntime.default_scene ? `默认场景:${aiRuntime.default_scene}` : '当前未设置默认场景',
metrics: [
{ label: '场景数量', value: this.formatMetricNumber(aiRuntime.scene_count) },
{ label: '目标数量', value: this.formatMetricNumber(aiRuntime.target_count) },
{ label: 'Provider 模板', value: this.formatMetricNumber(aiRuntime.provider_count) },
{ label: '默认后端', value: aiRuntime.default_backend || '-' }
]
},
{
key: 'ai-last-call',
title: '最近调用',
status: (aiRuntime.failed_calls || 0) > 0 ? 'warning' : ((aiRuntime.total_calls || 0) > 0 ? 'healthy' : 'warning'),
summary: aiRuntime.last_timestamp ? `最近一次记录时间:${aiRuntime.last_timestamp}` : '当前窗口内暂无调用记录',
metrics: [
{ label: 'Provider', value: aiRuntime.last_provider || '-' },
{ label: 'Backend', value: aiRuntime.last_backend || '-' },
{ label: 'Scene', value: aiRuntime.last_scene || '-' },
{ label: '模型', value: aiRuntime.last_model || '-' },
{ label: '最近耗时', value: `${this.formatMetricNumber(aiRuntime.last_latency_ms, 2)} ms` },
{ label: '最近错误', value: aiRuntime.last_error || '无' }
]
}
];
},
buildAiRuntimeExtra(aiRuntime) {
return `最近调用 ${aiRuntime.total_calls || 0} 次,失败 ${aiRuntime.failed_calls || 0} 次,平均耗时 ${this.formatMetricNumber(aiRuntime.avg_latency_ms, 2)} ms`;
},
buildSchedulerServiceBlocks(scheduler) {
return [
{
key: 'scheduler-overview',
title: '任务装载',
status: scheduler.enabled_jobs > 0 ? 'healthy' : 'warning',
summary: scheduler.next_run_at ? `下一次执行:${scheduler.next_run_at}` : '当前没有可计算的下一次执行时间',
metrics: [
{ label: '启用任务', value: this.formatMetricNumber(scheduler.enabled_jobs) },
{ label: '暂停任务', value: this.formatMetricNumber(scheduler.paused_jobs) },
{ label: '系统任务', value: this.formatMetricNumber(scheduler.system_job_count) },
{ label: '插件任务', value: this.formatMetricNumber(scheduler.plugin_job_count) }
]
},
{
key: 'scheduler-runtime',
title: '执行状态',
status: scheduler.status || 'warning',
summary: scheduler.latest_failed_job_name ? `最近失败任务:${scheduler.latest_failed_job_name}` : '当前未发现最近失败任务',
metrics: [
{ label: '执行中', value: this.formatMetricNumber(scheduler.running_jobs) },
{ label: '失败任务', value: this.formatMetricNumber(scheduler.failed_jobs) },
{ label: '非法调度', value: this.formatMetricNumber(scheduler.invalid_jobs) },
{ label: '未执行过', value: this.formatMetricNumber(scheduler.never_run_jobs) }
]
}
];
},
buildSchedulerExtra(scheduler) {
if (scheduler.latest_failed_error) {
return `最近失败原因:${scheduler.latest_failed_error}`;
}
return scheduler.next_run_at
? `下次执行时间:${scheduler.next_run_at}`
: '当前暂无可用的下一次执行时间';
},
renderPieChart(chartId, usageValue, label) {
const ctx = document.getElementById(chartId);
if (!ctx) return;
@@ -1095,6 +1273,104 @@
color: #475569;
}
.health-service-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 16px;
}
.health-service-panel {
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(248, 250, 252, 0.72);
}
.health-service-panel--healthy {
box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.08);
}
.health-service-panel--warning {
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.10);
}
.health-service-panel--danger {
box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.10);
}
.health-service-panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.health-service-panel__title {
font-size: 14px;
font-weight: 700;
color: #0f172a;
margin-bottom: 4px;
}
.health-service-panel__summary {
font-size: 12px;
line-height: 1.6;
color: #64748b;
}
.health-service-panel__badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
padding: 4px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
flex-shrink: 0;
}
.health-service-panel__badge--healthy {
color: #047857;
background: rgba(16, 185, 129, 0.12);
}
.health-service-panel__badge--warning {
color: #b45309;
background: rgba(245, 158, 11, 0.14);
}
.health-service-panel__badge--danger {
color: #b91c1c;
background: rgba(239, 68, 68, 0.14);
}
.health-service-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 12px;
}
.health-service-metric {
display: flex;
flex-direction: column;
gap: 4px;
}
.health-service-metric__label {
font-size: 11px;
color: #94a3b8;
}
.health-service-metric__value {
font-size: 13px;
font-weight: 600;
color: #1e293b;
word-break: break-word;
}
.health-item__extra {
margin-top: 12px;
padding-top: 12px;
@@ -1450,6 +1726,10 @@
.health-grid {
grid-template-columns: 1fr;
}
.health-service-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
@@ -1559,6 +1839,10 @@
font-size: 24px;
}
.health-service-metrics {
grid-template-columns: 1fr;
}
.chart-container--large,
.chart-container--panel {
height: 220px;