/** * 仪表盘页面 */ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `
最近日志
加载中...
` // 异步加载数据 loadDashboardData(page) return page } async function loadDashboardData(page) { 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, agents, config, tunnel) renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents) renderLogs(page, logs) bindActions(page) } 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 = `
Gateway
${gw?.running ? '运行中' : '已停止'}
${gw?.pid ? 'PID: ' + gw.pid : '未启动'}
版本 · ${version.source === 'official' ? '官方' : '汉化'}
${version.current || '未知'}
${version.update_available ? '有新版本: ' + version.latest : '已是最新'}
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 启用
` } function renderLogs(page, logs) { const logsEl = page.querySelector('#recent-logs') if (!logs) { logsEl.textContent = '暂无日志'; return } const lines = logs.trim().split('\n') logsEl.innerHTML = lines.map(l => `
${escapeHtml(l)}
`).join('') logsEl.scrollTop = logsEl.scrollHeight } 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 btnRestart.textContent = '重启中...' try { await api.restartService('ai.openclaw.gateway') toast('Gateway 已重启', 'success') setTimeout(() => loadDashboardData(page), 500) } catch (e) { toast('重启失败: ' + e, 'error') } finally { btnRestart.disabled = false btnRestart.textContent = '重启 Gateway' } }) btnUpdate?.addEventListener('click', async () => { btnUpdate.disabled = true btnUpdate.textContent = '检查中...' try { const info = await api.getVersionInfo() if (info.update_available) { toast(`发现新版本: ${info.latest}`, 'info') } else { toast('已是最新版本', 'success') } } catch (e) { toast('检查更新失败: ' + e, 'error') } finally { btnUpdate.disabled = false 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) { return str.replace(/&/g, '&').replace(//g, '>') }