/** * 服务管理页面 * 服务启停 + 更新检测 + 配置备份管理 */ 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 = `
加载中...
npm 源设置
加载中...
配置备份
备份范围: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 = `
当前版本 · ${sourceTag}
${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') }