Add lightweight server health to control center
This commit is contained in:
@@ -19,6 +19,7 @@ export * from './post';
|
||||
export * from './role';
|
||||
export * from './resource-scope';
|
||||
export * from './system-config';
|
||||
export * from './server-health';
|
||||
export * from './ui-config';
|
||||
export * from './user';
|
||||
export * from './websocket';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export interface ServerHealthOverview {
|
||||
basic_info: {
|
||||
hostname: string;
|
||||
ip_address: string;
|
||||
platform: string;
|
||||
};
|
||||
boot_time: {
|
||||
uptime_formatted: string;
|
||||
};
|
||||
cpu_info: {
|
||||
cpu_percent: number;
|
||||
total_cores: number;
|
||||
};
|
||||
disk_info: {
|
||||
total_read_bytes: number;
|
||||
total_write_bytes: number;
|
||||
};
|
||||
memory_info: {
|
||||
virtual: {
|
||||
percent: number;
|
||||
total: number;
|
||||
used: number;
|
||||
};
|
||||
};
|
||||
network_info: {
|
||||
total: {
|
||||
bytes_recv: number;
|
||||
bytes_sent: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServerRealtimeStats {
|
||||
cpu_percent: number;
|
||||
disk_io: {
|
||||
read_speed: number;
|
||||
write_speed: number;
|
||||
};
|
||||
memory_details: {
|
||||
total: number;
|
||||
used: number;
|
||||
};
|
||||
memory_percent: number;
|
||||
network_io: {
|
||||
download_speed: number;
|
||||
upload_speed: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getServerHealthOverviewApi() {
|
||||
return requestClient.get<ServerHealthOverview>(
|
||||
'/api/core/server_monitor/overview',
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerHealthRealtimeApi() {
|
||||
return requestClient.get<ServerRealtimeStats>(
|
||||
'/api/core/server_monitor/realtime',
|
||||
);
|
||||
}
|
||||
@@ -58,6 +58,12 @@ import {
|
||||
getUserAnnouncementListApi,
|
||||
} from '#/api/core/announcement';
|
||||
import { getMessageListApi, getUnreadCountApi } from '#/api/core/message';
|
||||
import {
|
||||
getServerHealthOverviewApi,
|
||||
getServerHealthRealtimeApi,
|
||||
type ServerHealthOverview,
|
||||
type ServerRealtimeStats,
|
||||
} from '#/api/core/server-health';
|
||||
|
||||
defineOptions({ name: 'ControlCenter' });
|
||||
|
||||
@@ -73,6 +79,8 @@ const knowledgeBases = ref<KnowledgeBaseListItem[]>([]);
|
||||
const runs = ref<WorkflowRunListItem[]>([]);
|
||||
const announcements = ref<UserAnnouncement[]>([]);
|
||||
const messages = ref<Message[]>([]);
|
||||
const serverOverview = ref<ServerHealthOverview>();
|
||||
const realtimeStats = ref<ServerRealtimeStats>();
|
||||
|
||||
const stats = reactive({
|
||||
agentTotal: 0,
|
||||
@@ -292,6 +300,45 @@ const resourceCards = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const serverCards = computed(() => {
|
||||
const overview = serverOverview.value;
|
||||
const realtime = realtimeStats.value;
|
||||
const memory = realtime?.memory_details || overview?.memory_info.virtual;
|
||||
return [
|
||||
{
|
||||
desc: `${overview?.cpu_info.total_cores || '-'} 核心`,
|
||||
icon: Cpu,
|
||||
label: 'CPU',
|
||||
tone: 'blue',
|
||||
value: `${Math.round(realtime?.cpu_percent ?? overview?.cpu_info.cpu_percent ?? 0)}%`,
|
||||
},
|
||||
{
|
||||
desc:
|
||||
memory?.used && memory?.total
|
||||
? `${formatBytes(memory.used)} / ${formatBytes(memory.total)}`
|
||||
: '等待采集',
|
||||
icon: Database,
|
||||
label: '内存',
|
||||
tone: 'green',
|
||||
value: `${Math.round(realtime?.memory_percent ?? overview?.memory_info.virtual.percent ?? 0)}%`,
|
||||
},
|
||||
{
|
||||
desc: `累计 ${formatBytes(overview?.disk_info.total_read_bytes)} / ${formatBytes(overview?.disk_info.total_write_bytes)}`,
|
||||
icon: Server,
|
||||
label: '磁盘读/写',
|
||||
tone: 'amber',
|
||||
value: `${formatBytes(realtime?.disk_io.read_speed)}/s`,
|
||||
},
|
||||
{
|
||||
desc: `上传 ${formatBytes(realtime?.network_io.upload_speed)}/s`,
|
||||
icon: Network,
|
||||
label: '网络下载',
|
||||
tone: 'cyan',
|
||||
value: `${formatBytes(realtime?.network_io.download_speed)}/s`,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function go(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
@@ -301,6 +348,18 @@ function formatTime(value?: string) {
|
||||
return value.replace('T', ' ').slice(0, 16);
|
||||
}
|
||||
|
||||
function formatBytes(value?: number) {
|
||||
if (!value || value <= 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = value;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return `${size >= 10 || unitIndex === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
if (['completed', 'published', 'success'].includes(status)) return 'success';
|
||||
if (status === 'failed') return 'danger';
|
||||
@@ -321,6 +380,8 @@ async function loadDashboard() {
|
||||
announcementUnreadRes,
|
||||
messageRes,
|
||||
messageUnreadRes,
|
||||
serverOverviewRes,
|
||||
realtimeStatsRes,
|
||||
] = await Promise.all([
|
||||
getAgentListApi({ page: 1, pageSize: 6 }),
|
||||
getWorkflowListApi({ page: 1, pageSize: 6 }),
|
||||
@@ -330,6 +391,8 @@ async function loadDashboard() {
|
||||
getUnreadAnnouncementCountApi(),
|
||||
getMessageListApi({ page: 1, pageSize: 5 }),
|
||||
getUnreadCountApi(),
|
||||
getServerHealthOverviewApi().catch(() => undefined),
|
||||
getServerHealthRealtimeApi().catch(() => undefined),
|
||||
]);
|
||||
|
||||
agents.value = agentRes.items || [];
|
||||
@@ -338,6 +401,8 @@ async function loadDashboard() {
|
||||
runs.value = runRes.items || [];
|
||||
announcements.value = announcementRes.items || [];
|
||||
messages.value = messageRes.items || [];
|
||||
serverOverview.value = serverOverviewRes;
|
||||
realtimeStats.value = realtimeStatsRes;
|
||||
|
||||
stats.agentTotal = agentRes.total || agents.value.length;
|
||||
stats.workflowTotal = workflowRes.total || workflows.value.length;
|
||||
@@ -482,12 +547,18 @@ onMounted(() => {
|
||||
<section class="server-panel panel">
|
||||
<div class="panel-title">
|
||||
<Server :size="17" />
|
||||
<span>AI 协作资源</span>
|
||||
<small>ai-agent-admin · lightweight runtime</small>
|
||||
<span>服务器信息</span>
|
||||
<small>
|
||||
{{
|
||||
serverOverview
|
||||
? `${serverOverview.basic_info.hostname} (${serverOverview.basic_info.ip_address}) · 运行时间 ${serverOverview.boot_time.uptime_formatted}`
|
||||
: '等待服务器采集数据'
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
<div class="resource-grid">
|
||||
<div
|
||||
v-for="item in resourceCards"
|
||||
v-for="item in serverCards"
|
||||
:key="item.label"
|
||||
class="resource-card"
|
||||
>
|
||||
@@ -559,6 +630,17 @@ onMounted(() => {
|
||||
<span>运行概览</span>
|
||||
</div>
|
||||
<div class="health-list">
|
||||
<div class="runtime-summary">
|
||||
<div
|
||||
v-for="item in resourceCards"
|
||||
:key="item.label"
|
||||
class="runtime-chip"
|
||||
>
|
||||
<component :is="item.icon" :size="16" />
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>智能体发布率</span>
|
||||
<strong>{{ publishedAgents }} / {{ stats.agentTotal }}</strong>
|
||||
@@ -980,6 +1062,33 @@ onMounted(() => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.runtime-summary {
|
||||
display: grid;
|
||||
gap: 8px !important;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.runtime-chip {
|
||||
align-items: center;
|
||||
background: hsl(var(--muted) / 45%);
|
||||
border-radius: 8px;
|
||||
display: grid !important;
|
||||
gap: 5px !important;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
min-height: 42px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.runtime-chip svg {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.runtime-chip span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.token-row {
|
||||
align-items: center;
|
||||
display: flex !important;
|
||||
|
||||
Reference in New Issue
Block a user