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 = `
+
${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?.running ? '端口 ' + clawapp.port : '未启动'}
+
+
+
+
+
+ ${tunnel?.running ? tunnel.tunnel_name : (tunnel?.installed ? '已停止' : '未安装')}
+
+
+
+
+
+ ${mcpCount} 个已挂载
+
+
+
+
+
+
+
+
+ ${latestBackup ? formatDate(latestBackup.created_at) : '从无备份'}
+
+
+
+
+
+
+ ${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;