Files
nl-tcm-agent-admin/src/views/system/index.vue

127 lines
5.7 KiB
Vue
Raw Normal View History

2026-08-15 12:52:39 +08:00
<script setup>
// 系统状态:进程 / 内存(含前端采样的 60 点迷你趋势)/ 并发 / 运行时开关(只读)
// 5s 轮询;并发占用 >80% 时给告警条并联动运行记录
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app'
import { getSystem, getAgentConfig } from '@/api/agent'
import { usePolling } from '@/composables/usePolling'
import { fmtUptime, fmtTimeFull } from '@/utils/format'
import PageHeader from '@/components/PageHeader.vue'
import EChart from '@/components/EChart.vue'
import GlassCard from '@/components/GlassCard.vue'
import KvGrid from '@/components/KvGrid.vue'
const router = useRouter()
const app = useAppStore()
const sys = ref(null)
const cfg = ref(null)
// 内存趋势采样(前端保留最近 60 个点,5s 一个 ≈ 5 分钟窗口)
const heapSamples = ref([])
async function load() {
const res = await getSystem({ silent: true })
sys.value = res.data
heapSamples.value.push(Number(res.data.heap_alloc_mb.toFixed(1)))
if (heapSamples.value.length > 60) heapSamples.value.shift()
}
usePolling(load, 5000)
// 运行时开关(低频,进页拉一次)
getAgentConfig({ silent: true }).then(res => { cfg.value = res.data }).catch(() => {})
const concUsed = computed(() => sys.value?.enhance_concurrency?.used ?? 0)
const concCap = computed(() => sys.value?.enhance_concurrency?.capacity ?? 16)
const concPercent = computed(() => Math.round((concUsed.value / Math.max(concCap.value, 1)) * 100))
// 进程卡键值对(KvGrid 数据化渲染)
const procItems = computed(() => {
const s = sys.value
if (!s) return []
return [
{ label: '启动时间', value: fmtTimeFull(s.started_at) },
{ label: '运行时长', value: fmtUptime(s.uptime_seconds), color: 'var(--ok)' },
{ label: 'Go 版本', value: s.go_version },
{ label: 'goroutines', value: s.goroutines },
{ label: 'RunLog 缓冲', value: `${s.runlog_usage?.used} / ${s.runlog_usage?.capacity}` }
]
})
// 运行时开关键值对(中文值不用等宽字体)
const switchItems = computed(() => {
const c = cfg.value
if (!c) return []
return [
{ label: 'ReAct 引擎', value: c.react?.enabled ? '开启' : '关闭', mono: false },
{ label: '医疗守卫', value: c.medical_guard?.enabled ? '开启' : '关闭', mono: false },
{ label: 'token 预算', value: c.token_budget?.enabled ? '开启' : '关闭', mono: false },
{ label: 'debug 日志', value: c.debug?.log_request_body ? '开(含请求体)' : '关', mono: false,
color: c.debug?.log_request_body ? 'var(--warn)' : undefined },
{ label: 'KB 来源', value: c.kb?.source }
]
})
const heapOption = computed(() => {
const dark = app.theme === 'dark'
const c = dark ? '#f59e0b' : '#4361ee'
return {
backgroundColor: 'transparent',
grid: { left: 36, right: 8, top: 8, bottom: 18 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: heapSamples.value.map((_, i) => i), show: false },
yAxis: { type: 'value', axisLabel: { color: dark ? '#8d7c60' : '#98a1b3', fontSize: 10 }, splitLine: { lineStyle: { color: 'rgba(128,128,128,0.1)' } } },
series: [{ type: 'line', data: heapSamples.value, smooth: true, showSymbol: false, areaStyle: { opacity: .25 }, itemStyle: { color: c } }]
}
})
</script>
<template>
<div>
<PageHeader title="系统状态" desc="Go 进程运行时指标(5s 轮询,零外部依赖)" />
<!-- 并发告警(互通:直达运行记录) -->
<a-alert v-if="concPercent >= 80" type="warning" show-icon class="!mb-4"
:message="`enhance 并发已占用 ${concUsed}/${concCap}(${concPercent}%),接近限流阈值`">
<template #action>
<a-button size="small" @click="router.push('/runs')">查看运行中请求</a-button>
</template>
</a-alert>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-4">
<!-- 进程卡 -->
<GlassCard title="进程">
<KvGrid v-if="sys" :items="procItems" />
<a-skeleton v-else active :paragraph="{ rows: 4 }" :title="false" />
</GlassCard>
<!-- 并发卡(居中环形图,非 KV 结构,仅换外壳) -->
<GlassCard title="enhance 并发信号量">
<div class="flex flex-col items-center justify-center">
<a-progress type="circle" :percent="concPercent" :size="120"
:stroke-color="concPercent >= 80 ? 'var(--err)' : concPercent >= 50 ? 'var(--warn)' : 'var(--ok)'">
<template #format><span class="mono text-sm">{{ concUsed }}/{{ concCap }}</span></template>
</a-progress>
<div class="text-xs mt-3" style="color: var(--text-3)">满载时新请求快速失败(503),PHP 端自动降级直连</div>
</div>
</GlassCard>
<!-- 运行时开关(只读) -->
<GlassCard title="运行时开关">
<template #extra><a-tag :bordered="false" color="default">只读</a-tag></template>
<KvGrid v-if="cfg" :items="switchItems" />
<a-skeleton v-else active :paragraph="{ rows: 4 }" :title="false" />
<a-button size="small" type="link" class="!px-0 mt-2" @click="router.push('/config')">完整配置 →</a-button>
</GlassCard>
</div>
<!-- 内存卡 + 趋势 -->
<GlassCard title="堆内存趋势(前端采样 5 分钟窗口)">
<template #extra>
<span v-if="sys" class="text-xs mono" style="color: var(--text-2)">
heap {{ sys.heap_alloc_mb?.toFixed(1) }} MB · OS {{ sys.sys_mb?.toFixed(0) }} MB · GC {{ sys.num_gc }} 次
</span>
</template>
<EChart :option="heapOption" height="200px" />
</GlassCard>
</div>
</template>