恢复首页基础设施详细状态与任务调度卡片
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user