/**
* 仪表盘页面
*/
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 = `
${gw?.running ? '运行中' : '已停止'}
${gw?.pid ? 'PID: ' + gw.pid : '未启动'}
${version.current || '未知'}
${version.update_available ? '有新版本: ' + version.latest : '已是最新'}
${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 ? '已停止' : '未安装')}
${latestBackup ? formatDate(latestBackup.created_at) : '从无备份'}
${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, '>')
}