feat: 升级进度弹窗 + 安装源自动检测与切换

- 升级过程改为流式日志推送(Tauri Event),前端展示进度条和实时日志
- 后端自动检测当前安装的是官方版(openclaw)还是汉化版(openclaw-zh)
- 服务管理页支持一键切换安装源,切换时先卸载旧包避免 bin 冲突
- 版本号比较改为逐段数值比较,支持 -zh.X 后缀的小版本检测
- 仪表盘、关于页同步显示当前安装源标识
This commit is contained in:
晴天
2026-02-28 12:43:19 +08:00
parent 84a6ab4d45
commit 3fd98623c0
8 changed files with 259 additions and 37 deletions

View File

@@ -134,3 +134,63 @@ export function showModal({ title, fields, onConfirm }) {
const firstInput = overlay.querySelector('input, select')
if (firstInput) firstInput.focus()
}
/**
* 升级进度弹窗 — 带进度条和实时日志
* @returns {{ appendLog, setProgress, setDone, setError, destroy }}
*/
export function showUpgradeModal() {
const overlay = document.createElement('div')
overlay.className = 'modal-overlay'
overlay.innerHTML = `
<div class="modal" style="max-width:520px">
<div class="modal-title">升级 OpenClaw</div>
<div class="upgrade-progress-wrap">
<div class="upgrade-progress-bar"><div class="upgrade-progress-fill" style="width:0%"></div></div>
<div class="upgrade-progress-text">准备中...</div>
</div>
<div class="upgrade-log-box"></div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="close" disabled>关闭</button>
</div>
</div>
`
document.body.appendChild(overlay)
const fill = overlay.querySelector('.upgrade-progress-fill')
const text = overlay.querySelector('.upgrade-progress-text')
const logBox = overlay.querySelector('.upgrade-log-box')
const closeBtn = overlay.querySelector('[data-action="close"]')
closeBtn.onclick = () => overlay.remove()
overlay.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !closeBtn.disabled) overlay.remove()
})
return {
appendLog(line) {
const div = document.createElement('div')
div.textContent = line
logBox.appendChild(div)
logBox.scrollTop = logBox.scrollHeight
},
setProgress(pct) {
fill.style.width = pct + '%'
text.textContent = pct >= 100 ? '完成' : `升级中... ${pct}%`
},
setDone(msg) {
text.textContent = msg || '升级完成'
fill.style.width = '100%'
fill.classList.add('done')
closeBtn.disabled = false
closeBtn.focus()
},
setError(msg) {
text.textContent = msg || '升级失败'
fill.classList.add('error')
closeBtn.disabled = false
closeBtn.focus()
},
destroy() { overlay.remove() },
}
}

View File

@@ -141,7 +141,7 @@ export const api = {
readMcpConfig: () => invoke('read_mcp_config'),
writeMcpConfig: (config) => invoke('write_mcp_config', { config }),
reloadGateway: () => invoke('reload_gateway'),
upgradeOpenclaw: () => invoke('upgrade_openclaw'),
upgradeOpenclaw: (source = 'chinese') => invoke('upgrade_openclaw', { source }),
installGateway: () => invoke('install_gateway'),
uninstallGateway: () => invoke('uninstall_gateway'),
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { baseUrl, apiKey, modelId }),

View File

@@ -4,6 +4,7 @@
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showUpgradeModal } from '../components/modal.js'
export async function render() {
const page = document.createElement('div')
@@ -71,7 +72,7 @@ async function loadData(page) {
<div class="stat-card-meta">Tauri v2 桌面应用</div>
</div>
<div class="stat-card">
<div class="stat-card-header"><span class="stat-card-label">OpenClaw</span></div>
<div class="stat-card-header"><span class="stat-card-label">OpenClaw · ${version.source === 'official' ? '官方版' : '汉化版'}</span></div>
<div class="stat-card-value">${version.current || '未安装'}</div>
<div class="stat-card-meta" style="display:flex;align-items:center;gap:8px">
${version.update_available
@@ -90,17 +91,21 @@ async function loadData(page) {
const upgradeBtn = cards.querySelector('#btn-upgrade')
if (upgradeBtn) {
upgradeBtn.onclick = async () => {
upgradeBtn.disabled = true
upgradeBtn.textContent = '升级中...'
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()
upgradeBtn.textContent = '完成'
// 刷新版本信息
modal.setDone(msg)
loadData(page)
} catch (e) {
upgradeBtn.disabled = false
upgradeBtn.textContent = '升级失败'
toast('升级失败: ' + e, 'error')
modal.appendLog(String(e))
modal.setError('升级失败')
} finally {
unlistenLog?.()
unlistenProgress?.()
}
}
}

View File

@@ -70,7 +70,7 @@ function renderStatCards(page, services, version) {
</div>
<div class="stat-card">
<div class="stat-card-header">
<span class="stat-card-label">版本</span>
<span class="stat-card-label">版本 · ${version.source === 'official' ? '官方' : '汉化'}</span>
</div>
<div class="stat-card-value">${version.current || '未知'}</div>
<div class="stat-card-meta">${version.update_available ? '有新版本: ' + version.latest : '已是最新'}</div>

View File

@@ -4,7 +4,7 @@
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showConfirm } from '../components/modal.js'
import { showConfirm, showUpgradeModal } from '../components/modal.js'
// HTML 转义,防止 XSS
function escapeHtml(str) {
@@ -51,21 +51,32 @@ async function loadAll(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 = `
<div class="stat-cards" style="margin-bottom:var(--space-lg)">
<div class="stat-card">
<div class="stat-card-header">
<span class="stat-card-label">当前版本</span>
<span class="stat-card-label">当前版本 · <span style="color:var(--accent)">${sourceTag}</span></span>
</div>
<div class="stat-card-value">${ver}</div>
<div class="stat-card-meta">${hasUpdate ? '新版本: ' + info.latest : '已是最新版本'}</div>
${hasUpdate ? '<button class="btn btn-primary btn-sm" data-action="upgrade" style="margin-top:var(--space-sm)">升级到最新版</button>' : ''}
<div style="display:flex;gap:var(--space-sm);margin-top:var(--space-sm);flex-wrap:wrap">
${hasUpdate ? '<button class="btn btn-primary btn-sm" data-action="upgrade">升级到最新版</button>' : ''}
<button class="btn btn-secondary btn-sm" data-action="switch-source" data-source="${switchTarget}">${switchLabel}</button>
</div>
</div>
</div>
`
@@ -194,6 +205,9 @@ function bindEvents(page) {
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
@@ -246,13 +260,37 @@ async function handleDeleteBackup(name, 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 yes = await showConfirm('确定要升级 OpenClaw 到最新版本吗?\n升级过程中 Gateway 会短暂中断。')
const sourceLabel = detectedSource === 'official' ? '官方原版' : '汉化优化版'
const yes = await showConfirm(`确定要升级 OpenClaw 到最新${sourceLabel}吗?\n升级过程中 Gateway 会短暂中断。`)
if (!yes) return
btn.textContent = '升级中...'
const msg = await api.upgradeOpenclaw()
toast(msg, 'success')
await loadVersion(page)
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 安装/卸载 =====

View File

@@ -173,3 +173,45 @@
padding: var(--space-sm) var(--space-lg);
border-bottom: 1px solid var(--border-secondary);
}
/* 升级进度弹窗 */
.upgrade-progress-wrap {
margin-bottom: var(--space-md);
}
.upgrade-progress-bar {
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
margin-bottom: var(--space-xs);
}
.upgrade-progress-fill {
height: 100%;
background: var(--accent);
border-radius: 3px;
transition: width 0.4s ease;
}
.upgrade-progress-fill.done { background: var(--success); }
.upgrade-progress-fill.error { background: var(--error); }
.upgrade-progress-text {
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.upgrade-log-box {
max-height: 220px;
overflow-y: auto;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: var(--space-sm) var(--space-md);
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.6;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
}