/**
* 关于页面
* 版本信息、项目链接、相关项目、系统环境
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showUpgradeModal, showConfirm } from '../components/modal.js'
import { setUpgrading } from '../lib/app-state.js'
import { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
page.className = 'page'
page.innerHTML = `
ClawPanel 基于 Tauri v2 构建,前端 Vanilla JS + Vite,后端 Rust。
MIT License © 2026 武汉晴辰天下网络科技有限公司
`
loadData(page)
renderCommunity(page)
renderProjects(page)
renderContribute(page)
renderLinks(page)
renderCompany(page)
return page
}
async function loadData(page) {
const cards = page.querySelector('#version-cards')
try {
const [version, install] = await Promise.all([
api.getVersionInfo(),
api.checkInstallation(),
])
// 尝试从 Tauri API 获取 ClawPanel 自身版本号,失败则 fallback
let panelVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.1.0'
try {
const { getVersion } = await import('@tauri-apps/api/app')
panelVersion = await getVersion()
} catch {
// 非 Tauri 环境或 API 不可用,使用构建时注入的版本号
}
// 异步检查前端热更新
let panelUpdateHtml = '检查更新中...'
checkHotUpdate(cards, panelVersion)
const isInstalled = !!version.current
const sourceLabel = version.source === 'official' ? '官方版' : '汉化版'
const btnSm = 'padding:2px 8px;font-size:var(--font-size-xs)'
const hasRecommended = !!version.recommended
const aheadOfRecommended = isInstalled && hasRecommended && !!version.ahead_of_recommended
const driftFromRecommended = isInstalled && hasRecommended && !version.is_recommended && !aheadOfRecommended
const policyRiskHint = aheadOfRecommended
? `检测到你本地安装的是高于推荐稳定版的 ${version.current},可能存在接口、事件或配置兼容性问题。建议回退到 ${version.recommended};如果你要继续使用高版本,请自行验证兼容性并关注 issue / release。`
: '当前面板默认只保证推荐稳定版的兼容性;如果你要尝试其他版本或预览版,请自行验证兼容性。若希望面板尽快支持最新版特性,欢迎提交 issue 告诉我们。'
cards.innerHTML = `
${panelVersion}
${panelUpdateHtml}
${version.current || '未安装'}
${isInstalled && hasRecommended
? (aheadOfRecommended
? `当前版本高于推荐稳定版: ${version.recommended}
`
: driftFromRecommended
? `推荐稳定版: ${version.recommended}
`
: '已是推荐稳定版')
: ''}
${version.latest_update_available && version.latest ? `最新上游: ${version.latest}` : ''}
${isInstalled ? `` : ''}
${policyRiskHint}
${install.path || '未知'}
${install.installed ? '配置文件存在' : '未找到配置文件'}
`
const applyRecommendedBtn = cards.querySelector('#btn-apply-recommended')
if (applyRecommendedBtn && version.recommended) {
applyRecommendedBtn.onclick = () => doInstall(page, aheadOfRecommended ? '回退到推荐稳定版' : '切换到推荐稳定版', version.source, version.recommended)
}
// 版本管理 / 安装
const versionMgmtBtn = cards.querySelector('#btn-version-mgmt')
if (versionMgmtBtn) {
versionMgmtBtn.onclick = () => showVersionPicker(page, version)
}
// 卸载
const uninstallBtn = cards.querySelector('#btn-uninstall')
if (uninstallBtn) {
uninstallBtn.onclick = async () => {
const confirmed = await showConfirm('确定要卸载 OpenClaw 吗?\n\n这将停止 Gateway 服务并卸载 npm 全局包。\n配置文件(~/.openclaw/)默认保留,可稍后手动删除。')
if (!confirmed) return
const modal = showUpgradeModal('卸载 OpenClaw')
modal.onClose(() => loadData(page))
modal.appendLog('开始卸载 OpenClaw...')
let unlistenLog, unlistenProgress, unlistenDone, unlistenError
const cleanup = () => { unlistenLog?.(); unlistenProgress?.(); unlistenDone?.(); unlistenError?.() }
try {
if (window.__TAURI_INTERNALS__) {
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))
unlistenDone = await listen('upgrade-done', (e) => { cleanup(); modal.setDone(typeof e.payload === 'string' ? e.payload : '卸载完成') })
unlistenError = await listen('upgrade-error', (e) => { cleanup(); modal.setError('卸载失败: ' + (e.payload || '未知错误')) })
await api.uninstallOpenclaw(false)
modal.appendLog('后台卸载任务已启动...')
} else {
const msg = await api.uninstallOpenclaw(false)
modal.setDone(typeof msg === 'string' ? msg : '卸载完成')
cleanup()
}
} catch (e) {
cleanup()
modal.setError('卸载失败: ' + (e?.message || e))
}
}
}
} catch {
cards.innerHTML = ''
}
}
/**
* 版本选择器弹窗 — 选择版本(汉化版/原版)+ 版本号
*/
async function showVersionPicker(page, currentVersion) {
const isInstalled = !!currentVersion.current
const overlay = document.createElement('div')
overlay.className = 'modal-overlay'
overlay.innerHTML = `
${isInstalled ? '切换版本' : '安装 OpenClaw'}
默认建议使用当前面板绑定的推荐稳定版。若手动切换到其它版本,尤其是预览版/最新版,请自行验证兼容性;如果你希望面板优先适配最新版功能,欢迎提交 issue。
`
document.body.appendChild(overlay)
const select = overlay.querySelector('#oc-version-select')
const confirmBtn = overlay.querySelector('#oc-confirm-btn')
const hintEl = overlay.querySelector('#oc-action-hint')
const radios = overlay.querySelectorAll('input[name="oc-source"]')
const lblChinese = overlay.querySelector('#lbl-chinese')
const lblOfficial = overlay.querySelector('#lbl-official')
const close = () => overlay.remove()
overlay.querySelector('[data-action="cancel"]').onclick = close
overlay.addEventListener('click', (e) => { if (e.target === overlay) close() })
overlay.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
let versionsCache = {}
let currentSelect = currentVersion.source === 'chinese' ? 'chinese' : 'official'
function updateRadioStyle() {
const sel = currentSelect
lblChinese.style.borderColor = sel !== 'official' ? 'var(--primary)' : 'var(--border)'
lblChinese.style.background = sel !== 'official' ? 'var(--primary-bg, rgba(99,102,241,0.06))' : ''
lblOfficial.style.borderColor = sel === 'official' ? 'var(--primary)' : 'var(--border)'
lblOfficial.style.background = sel === 'official' ? 'var(--primary-bg, rgba(99,102,241,0.06))' : ''
}
function updateHint() {
const targetSource = currentSelect
const targetVer = select.value
if (!targetVer || targetVer === '') { hintEl.textContent = ''; confirmBtn.disabled = true; return }
const targetTag = select.selectedIndex === 0 ? '(推荐稳定版)' : '(需自测兼容性)'
const sameSource = targetSource === (currentVersion.source === 'official' ? 'official' : 'chinese')
if (!isInstalled) {
confirmBtn.textContent = '安装'
hintEl.textContent = `将安装 ${targetSource === 'official' ? '原版' : '汉化版'} ${targetVer}${targetTag}`
confirmBtn.disabled = false
return
}
if (!sameSource) {
confirmBtn.textContent = '切换'
hintEl.innerHTML = `当前: ${currentVersion.source === 'official' ? '原版' : '汉化版'} ${currentVersion.current} → ${targetSource === 'official' ? '原版' : '汉化版'} ${targetVer}${targetTag}`
confirmBtn.disabled = false
return
}
// 同源,比较版本
const parseVer = v => v.split(/[^0-9]/).filter(Boolean).map(Number)
const cur = parseVer(currentVersion.current)
const tgt = parseVer(targetVer)
let cmp = 0
for (let i = 0; i < Math.max(cur.length, tgt.length); i++) {
if ((tgt[i] || 0) > (cur[i] || 0)) { cmp = 1; break }
if ((tgt[i] || 0) < (cur[i] || 0)) { cmp = -1; break }
}
if (cmp === 0) {
confirmBtn.textContent = '重新安装'
hintEl.textContent = `当前已是 ${targetVer}${targetTag}`
confirmBtn.disabled = false
} else if (cmp > 0) {
confirmBtn.textContent = '升级'
hintEl.innerHTML = `${currentVersion.current} → ${targetVer}${targetTag}`
confirmBtn.disabled = false
} else {
confirmBtn.textContent = '降级'
hintEl.innerHTML = `${currentVersion.current} → ${targetVer}${targetTag}`
confirmBtn.disabled = false
}
}
let showNightly = false
async function loadVersions(source) {
select.innerHTML = ''
confirmBtn.disabled = true
hintEl.textContent = ''
try {
if (!versionsCache[source]) {
versionsCache[source] = await api.listOpenclawVersions(source)
}
const allVersions = versionsCache[source]
if (!allVersions.length) {
select.innerHTML = ''
return
}
const stable = allVersions.filter(v => !v.includes('nightly') && !v.includes('canary') && !v.includes('alpha') && !v.includes('beta') && !v.includes('rc') && !v.includes('dev') && !v.includes('next'))
const versions = showNightly ? allVersions : (stable.length > 0 ? stable : allVersions)
const nightlyCount = allVersions.length - stable.length
select.innerHTML = versions.map((v, idx) => {
const isCurrent = isInstalled && v === currentVersion.current && source === (currentVersion.source === 'official' ? 'official' : 'chinese')
return ``
}).join('')
// nightly 切换提示
const toggleEl = overlay.querySelector('#nightly-toggle')
if (toggleEl) {
if (nightlyCount > 0) {
toggleEl.style.display = ''
toggleEl.innerHTML = showNightly
? `隐藏预览版 (${nightlyCount})`
: `显示预览版 (${nightlyCount})`
toggleEl.querySelector('#btn-toggle-nightly').onclick = (e) => { e.preventDefault(); showNightly = !showNightly; loadVersions(source) }
} else {
toggleEl.style.display = 'none'
}
}
updateHint()
} catch (e) {
select.innerHTML = ``
}
}
radios.forEach(radio => {
radio.addEventListener('change', () => {
currentSelect = radio.value
updateRadioStyle()
loadVersions(currentSelect)
})
})
select.addEventListener('change', updateHint)
confirmBtn.onclick = () => {
const source = currentSelect
const ver = select.value
const action = confirmBtn.textContent
close()
doInstall(page, `${action} OpenClaw`, source, ver)
}
updateRadioStyle()
loadVersions(currentSelect)
}
/**
* 执行安装/升级/降级/切换操作(带进度弹窗)
*/
async function doInstall(page, title, source, version) {
const modal = showUpgradeModal(title)
modal.onClose(() => loadData(page))
let unlistenLog, unlistenProgress, unlistenDone, unlistenError
setUpgrading(true)
const cleanup = () => {
setUpgrading(false)
unlistenLog?.(); unlistenProgress?.(); unlistenDone?.(); unlistenError?.()
}
try {
if (window.__TAURI_INTERNALS__) {
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))
unlistenDone = await listen('upgrade-done', (e) => {
cleanup()
modal.setDone(typeof e.payload === 'string' ? e.payload : '操作完成')
})
unlistenError = await listen('upgrade-error', async (e) => {
cleanup()
const errStr = String(e.payload || '未知错误')
modal.appendLog(errStr)
const { diagnoseInstallError } = await import('../lib/error-diagnosis.js')
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({ title: diagnosis.title, error: fullLog, scene: title, hint: diagnosis.hint })
}
})
await api.upgradeOpenclaw(source, version)
modal.appendLog('后台任务已启动,请等待完成...')
} else {
modal.appendLog('Web 模式:安装过程日志不可用,请等待完成...')
const msg = await api.upgradeOpenclaw(source, version)
modal.setDone(typeof msg === 'string' ? msg : (msg?.message || '操作完成'))
cleanup()
}
} catch (e) {
cleanup()
const errStr = String(e)
modal.appendLog(errStr)
const { diagnoseInstallError } = await import('../lib/error-diagnosis.js')
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
}
}
async function checkHotUpdate(cards, panelVersion) {
const el = () => cards.querySelector('#panel-update-meta')
try {
const info = await api.checkFrontendUpdate()
const meta = el()
if (!meta) return
if (info.updateReady) {
// 已下载更新,等待重载
const ver = info.manifest?.version || info.latestVersion || ''
meta.innerHTML = `
v${ver} 已就绪
`
meta.querySelector('#btn-hot-reload')?.addEventListener('click', () => {
window.location.reload()
})
meta.querySelector('#btn-hot-rollback')?.addEventListener('click', async () => {
try {
await api.rollbackFrontendUpdate()
toast('已回退到内嵌版本,重载中...', 'success')
setTimeout(() => window.location.reload(), 800)
} catch (e) {
toast('回退失败: ' + (e.message || e), 'error')
}
})
} else if (info.hasUpdate) {
// 有新版本可下载
const ver = info.latestVersion
const manifest = info.manifest || {}
const changelog = manifest.changelog || ''
meta.innerHTML = `
新版本: v${ver}
${changelog ? `${changelog}` : ''}
完整安装包
`
meta.querySelector('#btn-hot-download')?.addEventListener('click', async () => {
const btn = meta.querySelector('#btn-hot-download')
if (btn) { btn.disabled = true; btn.textContent = '下载中...' }
try {
await api.downloadFrontendUpdate(manifest.url, manifest.hash || '')
toast('更新下载完成,点击「重载应用」生效', 'success')
checkHotUpdate(cards, panelVersion)
} catch (e) {
toast('下载失败: ' + (e.message || e), 'error')
if (btn) { btn.disabled = false; btn.textContent = '重试' }
}
})
} else if (!info.compatible) {
meta.innerHTML = '需要更新完整安装包 前往官网下载 GitHub'
} else {
meta.innerHTML = '已是最新'
}
} catch (err) {
const meta = el()
if (!meta) return
meta.innerHTML = `暂无法检查更新 前往官网下载`
}
}
function compareVersions(a, b) {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] || 0
const nb = pb[i] || 0
if (na > nb) return 1
if (na < nb) return -1
}
return 0
}
function renderCommunity(page) {
const el = page.querySelector('#community-section')
el.innerHTML = `
QQ 交流群
微信交流群
抖音交流群
飞书交流群
扫码或点击链接加入交流群,反馈问题、获取帮助
2000 人大群,满员自动切换 · 碰到问题可直接在群内反馈
`
}
const PROJECTS = [
{
name: 'OpenClaw',
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理',
url: 'https://github.com/openclaw/openclaw',
},
{
name: 'OpenClaw-zh',
desc: '我们维护的 OpenClaw 汉化版,3000+ Star,中文界面 + 国内镜像优化',
url: 'https://github.com/1186258278/OpenClawChineseTranslation',
},
{
name: 'ClawPanel',
desc: 'OpenClaw 可视化管理面板,Tauri v2 桌面应用',
url: 'https://github.com/qingchencloud/clawpanel',
gitee: 'https://gitee.com/QtCodeCreators/clawpanel',
},
{
name: 'ClawApp',
desc: '跨平台移动聊天客户端,H5 + 代理服务器架构,支持离线和流式传输',
url: 'https://github.com/qingchencloud/clawapp',
},
{
name: 'cftunnel',
desc: '全协议内网穿透工具,Cloud 模式免费 HTTP/WS + Relay 模式自建中继',
url: 'https://github.com/qingchencloud/cftunnel',
},
]
function renderProjects(page) {
const el = page.querySelector('#projects-list')
el.innerHTML = PROJECTS.map(p => `
`).join('')
}
const LINKS = [
{ label: 'Claw 项目官网', url: 'https://claw.qt.cool', primary: true },
{ label: 'OpenClaw 中文翻译', url: 'https://github.com/1186258278/OpenClawChineseTranslation' },
{ label: 'ClawApp 手机客户端', url: 'https://clawapp.qt.cool' },
{ label: 'cftunnel 内网穿透', url: 'https://cftunnel.qt.cool' },
]
function renderContribute(page) {
const el = page.querySelector('#contribute-section')
el.innerHTML = `
ClawPanel 是开源项目,欢迎参与贡献!遇到问题请提 Issue,功能建议和代码改进欢迎提 PR。
国内镜像:
Gitee(无法访问 GitHub 时可用)
`
}
function renderLinks(page) {
const el = page.querySelector('#links-list')
el.innerHTML = ``
}
function renderCompany(page) {
const el = page.querySelector('#company-section')
el.innerHTML = `
武汉晴辰天下网络科技有限公司
QingchenCloud
我们是 OpenClaw 汉化版(3000+ Star)和 ClawPanel 的作者团队。日常做 AI Agent 相关的产品和开源工具,也接企业私有化部署、定制开发之类的活儿。有事直接群里找我们就行。
`
}