/**
* 服务管理页面
* 服务启停 + 更新检测 + 配置备份管理
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showConfirm, showUpgradeModal } from '../components/modal.js'
// HTML 转义,防止 XSS
function escapeHtml(str) {
if (!str) return ''
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
}
export async function render() {
const page = document.createElement('div')
page.className = 'page'
page.innerHTML = `
加载中...
配置备份
备份范围:openclaw.json 主配置文件(含模型、Provider、Gateway 设置)。Agent 数据和记忆文件不在此备份范围内。
加载中...
`
bindEvents(page)
loadAll(page)
return page
}
async function loadAll(page) {
await Promise.all([
loadVersion(page),
loadServices(page),
loadRegistry(page),
loadBackups(page),
])
}
// ===== 版本检测 =====
// 后端检测到的当前安装源
let detectedSource = 'chinese'
async function loadVersion(page) {
const bar = page.querySelector('#version-bar')
try {
const info = await api.getVersionInfo()
detectedSource = info.source || 'chinese'
const ver = info.current || '未知'
const hasUpdate = info.update_available
const isChinese = detectedSource === 'chinese'
const sourceTag = isChinese ? '汉化优化版' : '官方原版'
const switchLabel = isChinese ? '切换到官方版' : '切换到汉化版'
const switchTarget = isChinese ? 'official' : 'chinese'
bar.innerHTML = `
${ver}
${hasUpdate ? '新版本: ' + info.latest : '已是最新版本'}
${hasUpdate ? '' : ''}
`
} catch (e) {
bar.innerHTML = ``
}
}
// ===== npm 源设置 =====
const REGISTRIES = [
{ label: '淘宝镜像 (推荐)', value: 'https://registry.npmmirror.com' },
{ label: 'npm 官方源', value: 'https://registry.npmjs.org' },
{ label: '华为云镜像', value: 'https://repo.huaweicloud.com/repository/npm/' },
]
async function loadRegistry(page) {
const bar = page.querySelector('#registry-bar')
bar.innerHTML = '加载中...
'
try {
const current = await api.getNpmRegistry()
const isPreset = REGISTRIES.some(r => r.value === current)
bar.innerHTML = `
升级和版本检测使用此源下载 npm 包,国内用户推荐淘宝镜像
`
// 切换预设/自定义
const select = bar.querySelector('[data-name="registry"]')
const customInput = bar.querySelector('[data-name="custom-registry"]')
select.onchange = () => {
customInput.style.display = select.value === 'custom' ? '' : 'none'
}
} catch (e) {
bar.innerHTML = `加载失败: ${escapeHtml(String(e))}
`
}
}
// ===== 服务列表 =====
async function loadServices(page) {
const container = page.querySelector('#services-list')
container.innerHTML = '加载中...
'
try {
const services = await api.getServicesStatus()
renderServices(container, services)
} catch (e) {
container.innerHTML = `加载服务列表失败: ${escapeHtml(String(e))}
`
}
}
function renderServices(container, services) {
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
let html = ''
if (gw) {
// 检测 CLI 是否安装
const cliMissing = gw.cli_installed === false
html += `
${gw.label}
${cliMissing
? 'OpenClaw CLI 未安装'
: (gw.description || '') + (gw.pid ? ' (PID: ' + gw.pid + ')' : '')
}
${cliMissing
? `
请先安装 OpenClaw CLI:
npm install -g @qingchencloud/openclaw-zh
`
: gw.running
? `
`
: `
`
}
`
} else {
html += `
ai.openclaw.gateway
Gateway 服务未安装
`
}
container.innerHTML = html
}
// ===== 备份管理 =====
async function loadBackups(page) {
const list = page.querySelector('#backup-list')
list.innerHTML = '加载中...
'
try {
const backups = await api.listBackups()
renderBackups(list, backups)
} catch (e) {
list.innerHTML = `加载备份列表失败: ${e}
`
}
}
function renderBackups(container, backups) {
if (!backups || !backups.length) {
container.innerHTML = '暂无备份
'
return
}
container.innerHTML = backups.map(b => {
const date = b.created_at ? new Date(b.created_at * 1000).toLocaleString('zh-CN') : '未知'
const size = b.size ? (b.size / 1024).toFixed(1) + ' KB' : ''
return `
${b.name}
${date}${size ? ' · ' + size : ''}
`
}).join('')
}
// ===== 事件绑定(事件委托) =====
function bindEvents(page) {
page.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-action]')
if (!btn) return
const action = btn.dataset.action
btn.disabled = true
try {
switch (action) {
case 'start':
case 'stop':
case 'restart':
await handleServiceAction(action, btn.dataset.label, page)
break
case 'create-backup':
await handleCreateBackup(page)
break
case 'restore-backup':
await handleRestoreBackup(btn.dataset.name, page)
break
case 'delete-backup':
await handleDeleteBackup(btn.dataset.name, page)
break
case 'upgrade':
await handleUpgrade(btn, page)
break
case 'switch-source':
await handleSwitchSource(btn.dataset.source, page)
break
case 'install-gateway':
await handleInstallGateway(btn, page)
break
case 'uninstall-gateway':
await handleUninstallGateway(btn, page)
break
case 'refresh-services':
await loadServices(page)
break
case 'save-registry':
await handleSaveRegistry(btn, page)
break
}
} catch (e) {
toast(e.toString(), 'error')
} finally {
btn.disabled = false
}
})
}
// ===== 服务操作 =====
const ACTION_LABELS = { start: '启动', stop: '停止', restart: '重启' }
async function handleServiceAction(action, label, page) {
const fn = { start: api.startService, stop: api.stopService, restart: api.restartService }[action]
await fn(label)
toast(`${ACTION_LABELS[action]} ${label} 成功`, 'success')
await loadServices(page)
}
// ===== 备份操作 =====
async function handleCreateBackup(page) {
const result = await api.createBackup()
toast(`备份已创建: ${result.name}`, 'success')
await loadBackups(page)
}
async function handleRestoreBackup(name, page) {
const yes = await showConfirm(`确定要恢复备份 "${name}" 吗?\n当前配置将自动备份后再恢复。`)
if (!yes) return
await api.restoreBackup(name)
toast('配置已恢复', 'success')
await loadBackups(page)
}
async function handleDeleteBackup(name, page) {
const yes = await showConfirm(`确定要删除备份 "${name}" 吗?此操作不可撤销。`)
if (!yes) return
await api.deleteBackup(name)
toast('备份已删除', 'success')
await loadBackups(page)
}
// ===== 升级操作 =====
async function doUpgradeWithModal(source, page) {
const modal = showUpgradeModal()
let unlistenLog, unlistenProgress
try {
const { listen } = await import('@tauri-apps/api/event')
unlistenLog = await listen('upgrade-log', (e) => modal.appendLog(e.payload))
unlistenProgress = await listen('upgrade-progress', (e) => modal.setProgress(e.payload))
const msg = await api.upgradeOpenclaw(source)
modal.setDone(msg)
await loadVersion(page)
} catch (e) {
modal.appendLog(String(e))
modal.setError('升级失败')
} finally {
unlistenLog?.()
unlistenProgress?.()
}
}
async function handleUpgrade(btn, page) {
const sourceLabel = detectedSource === 'official' ? '官方原版' : '汉化优化版'
const yes = await showConfirm(`确定要升级 OpenClaw 到最新${sourceLabel}吗?\n升级过程中 Gateway 会短暂中断。`)
if (!yes) return
await doUpgradeWithModal(detectedSource, page)
}
async function handleSwitchSource(target, page) {
const targetLabel = target === 'official' ? '官方原版' : '汉化优化版'
const yes = await showConfirm(`确定要切换到${targetLabel}吗?\n这会安装对应的 npm 包,配置数据不受影响。`)
if (!yes) return
await doUpgradeWithModal(target, page)
}
// ===== Gateway 安装/卸载 =====
async function handleInstallGateway(btn, page) {
btn.textContent = '安装中...'
await api.installGateway()
toast('Gateway 服务已安装', 'success')
await loadServices(page)
}
async function handleUninstallGateway(btn, page) {
const yes = await showConfirm('确定要卸载 Gateway 服务吗?\n这会停止服务并移除 LaunchAgent。')
if (!yes) return
btn.textContent = '卸载中...'
await api.uninstallGateway()
toast('Gateway 服务已卸载', 'success')
await loadServices(page)
}
async function handleSaveRegistry(btn, page) {
const section = page.querySelector('#registry-section')
const select = section.querySelector('[data-name="registry"]')
const customInput = section.querySelector('[data-name="custom-registry"]')
const registry = select.value === 'custom' ? customInput.value.trim() : select.value
if (!registry) { toast('请输入源地址', 'error'); return }
await api.setNpmRegistry(registry)
toast('npm 源已保存', 'success')
}