From 3ac02ea19f1a362d2c56463b7c0f7ce408a465f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sun, 1 Mar 2026 13:26:55 +0800 Subject: [PATCH] feat(dashboard): rich data display to overview system states - add tunnel, mcp configurations insights - show more statistical cards - add create backup quick action --- src/pages/dashboard.js | 166 +++++++++++++++++++++++++++++++++++++++-- src/style/pages.css | 53 +++++++++++++ 2 files changed, 214 insertions(+), 5 deletions(-) diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js index dfd3bca..ac05987 100644 --- a/src/pages/dashboard.js +++ b/src/pages/dashboard.js @@ -18,10 +18,14 @@ export async function render() {
+
+
+
+
最近日志
@@ -35,30 +39,47 @@ export async function render() { } async function loadDashboardData(page) { - const [servicesRes, versionRes, logsRes] = await Promise.allSettled([ + const [servicesRes, versionRes, logsRes, agentsRes, configRes, tunnelRes, mcpRes, clawappRes, backupsRes] = await Promise.allSettled([ api.getServicesStatus(), api.getVersionInfo(), api.readLogTail('gateway', 20), + api.listAgents(), + api.readOpenclawConfig(), + api.getCftunnelStatus(), + api.readMcpConfig(), + api.getClawappStatus(), + api.listBackups(), ]) const services = servicesRes.status === 'fulfilled' ? servicesRes.value : [] const version = versionRes.status === 'fulfilled' ? versionRes.value : {} const logs = logsRes.status === 'fulfilled' ? logsRes.value : '' + const agents = agentsRes.status === 'fulfilled' ? agentsRes.value : [] + const config = configRes.status === 'fulfilled' ? configRes.value : null + const tunnel = tunnelRes.status === 'fulfilled' ? tunnelRes.value : null + const mcpConfig = mcpRes.status === 'fulfilled' ? mcpRes.value : null + const clawapp = clawappRes.status === 'fulfilled' ? clawappRes.value : null + const backups = backupsRes.status === 'fulfilled' ? backupsRes.value : [] if (servicesRes.status === 'rejected') toast('服务状态加载失败', 'error') if (versionRes.status === 'rejected') toast('版本信息加载失败', 'error') if (logsRes.status === 'rejected') toast('日志加载失败', 'error') - renderStatCards(page, services, version) + renderStatCards(page, services, version, agents, config, tunnel) + renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents) renderLogs(page, logs) bindActions(page) } -function renderStatCards(page, services, version) { +function renderStatCards(page, services, version, agents, config, tunnel) { const cardsEl = page.querySelector('#stat-cards') const gw = services.find(s => s.label === 'ai.openclaw.gateway') const runningCount = services.filter(s => s.running).length + const defaultAgent = agents.find(a => a.id === 'main')?.name || 'main' + const modelCount = config?.models?.providers ? Object.values(config.models.providers).reduce((acc, p) => acc + (p.models?.length || 0), 0) : 0 + const providerCount = config?.models?.providers ? Object.keys(config.models.providers).length : 0 + cardsEl.innerHTML = `
@@ -77,10 +98,129 @@ function renderStatCards(page, services, version) {
- 服务 + Agent 舰队 +
+
${agents.length} 个
+
默认: ${defaultAgent}
+
+
+
+ 模型池 +
+
${modelCount} 个
+
基于 ${providerCount} 个渠道商
+
+
+
+ 内网穿透隧道 + +
+
${tunnel?.running ? '运行中' : (tunnel?.installed ? '已停止' : '未配置')}
+
${tunnel?.routes ? tunnel.routes.length + ' 个路由映射' : '——'}
+
+
+
+ 基础服务
${runningCount}/${services.length}
-
运行中
+
存活率 ${services.length ? Math.round(runningCount / services.length * 100) : 0}%
+
+ ` +} + +function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents) { + const containerEl = page.querySelector('#dashboard-overview-container') + const gw = services.find(s => s.label === 'ai.openclaw.gateway') + const mcpCount = mcpConfig?.mcpServers ? Object.keys(mcpConfig.mcpServers).length : 0 + + const formatDate = (timestamp) => { + if (!timestamp) return '——' + const d = new Date(timestamp * 1000) + return \`\${d.getMonth()+1}-\${d.getDate()} \${d.getHours().toString().padStart(2, '0')}:\${d.getMinutes().toString().padStart(2, '0')}\` + } + + const latestBackup = backups.length > 0 ? backups.sort((a,b) => b.created_at - a.created_at)[0] : null + const lastUpdate = config?.meta?.lastTouchedVersion || '未知' + + containerEl.innerHTML = ` +
+
+
+
+ + Gateway 核心网关 +
+
+ ${gw?.running ? '运行中' : '已停止'} +
+
+
+
+ + ClawApp 守护进程 +
+
+ ${clawapp?.running ? '端口 ' + clawapp.port : '未启动'} +
+
+
+
+ + Cloudflare 隧道 +
+
+ ${tunnel?.running ? tunnel.tunnel_name : (tunnel?.installed ? '已停止' : '未安装')} +
+
+
+
+ + MCP 扩展工具 +
+
+ ${mcpCount} 个已挂载 +
+
+
+ +
+
+
+ + 最近备份 +
+
+ ${latestBackup ? formatDate(latestBackup.created_at) : '从无备份'} +
+
+
+
+ + 配置版本标识 +
+
+ ${lastUpdate} +
+
+
+
+ + 并行推理队列最大值 +
+
+ ${config?.agents?.defaults?.maxConcurrent || 4} +
+
+
+
+ + 工作区文件隔离 +
+
+ ${agents.filter(a => a.workspace).length} 个 Agent 启用 +
+
+
` } @@ -96,6 +236,7 @@ function renderLogs(page, logs) { function bindActions(page) { const btnRestart = page.querySelector('#btn-restart-gw') const btnUpdate = page.querySelector('#btn-check-update') + const btnCreateBackup = page.querySelector('#btn-create-backup') btnRestart?.addEventListener('click', async () => { btnRestart.disabled = true @@ -129,6 +270,21 @@ function bindActions(page) { btnUpdate.textContent = '检查更新' } }) + + btnCreateBackup?.addEventListener('click', async () => { + btnCreateBackup.disabled = true + btnCreateBackup.innerHTML = '备份中...' + try { + const res = await api.createBackup() + toast(`已备份: ${res.name}`, 'success') + setTimeout(() => loadDashboardData(page), 500) + } catch (e) { + toast('备份失败: ' + e, 'error') + } finally { + btnCreateBackup.disabled = false + btnCreateBackup.textContent = '创建备份' + } + }) } function escapeHtml(str) { diff --git a/src/style/pages.css b/src/style/pages.css index 151494b..8da1e78 100644 --- a/src/style/pages.css +++ b/src/style/pages.css @@ -6,6 +6,59 @@ margin-bottom: var(--space-xl); } +.dashboard-overview { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-xl); + margin-bottom: var(--space-xl); +} + +.overview-section { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.overview-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-md) var(--space-lg); + background: var(--bg-card); + border: 1px solid var(--border-primary); + border-radius: var(--radius-lg); + transition: all var(--transition-fast); +} + +.overview-item:hover { + background: var(--bg-card-hover); + border-color: var(--border-focus); +} + +.overview-label { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: var(--font-size-sm); + color: var(--text-secondary); + font-weight: 500; +} + +.overview-label svg { + width: 16px; + height: 16px; + color: var(--text-tertiary); +} + +.overview-value { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: var(--font-size-sm); + font-family: var(--font-mono); + color: var(--text-primary); +} + /* 服务卡片 */ .service-card { display: flex;