/** * 关于页面 * 版本信息、项目链接、相关项目、系统环境 */ 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' import { t } from '../lib/i18n.js' export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `
${t('about.sectionCommunity')}
${t('about.sectionProjects')}
${t('about.sectionContribute')}
${t('about.sectionLinks')}
${t('about.sectionAboutUs')}

${t('about.techStack')}

${t('about.copyright')}

` 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 = `${t('about.checkingUpdate')}` checkHotUpdate(cards, panelVersion) const isInstalled = !!version.current const sourceLabel = version.source === 'official' ? t('about.official') : t('about.chinese') 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 ? t('about.policyAhead', { current: version.current, recommended: version.recommended }) : t('about.policyDefault') cards.innerHTML = `
ClawPanel
${panelVersion}
${panelUpdateHtml}
OpenClaw · ${sourceLabel}
${version.current || t('about.notInstalled')}
${isInstalled && hasRecommended ? (aheadOfRecommended ? `${t('about.aheadOfRecommended', { ver: version.recommended })} ` : driftFromRecommended ? `${t('about.recommendedStable', { ver: version.recommended })} ` : `${t('about.isRecommended')}`) : ''} ${version.latest_update_available && version.latest ? `${t('about.latestUpstream', { ver: version.latest })}` : ''} ${isInstalled ? `` : ''}
${policyRiskHint}
${t('about.installPath')}
${install.path || t('common.unknown')}
${install.installed ? t('about.configExists') : t('about.configNotFound')}
` const applyRecommendedBtn = cards.querySelector('#btn-apply-recommended') if (applyRecommendedBtn && version.recommended) { applyRecommendedBtn.onclick = () => doInstall(page, aheadOfRecommended ? t('about.rollbackToRecommendedStable') : t('about.switchToRecommendedStable'), 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(t('about.confirmUninstall')) if (!confirmed) return const modal = showUpgradeModal(t('about.uninstallTitle')) modal.onClose(() => loadData(page)) modal.appendLog(t('about.uninstallStarting')) 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 : t('about.uninstallDone')) }) unlistenError = await listen('upgrade-error', (e) => { cleanup(); modal.setError(t('about.uninstallFailed') + (e.payload || t('common.unknown'))) }) await api.uninstallOpenclaw(false) modal.appendLog(t('about.uninstallTaskStarted')) } else { const msg = await api.uninstallOpenclaw(false) modal.setDone(typeof msg === 'string' ? msg : t('about.uninstallDone')) cleanup() } } catch (e) { cleanup() modal.setError(t('about.uninstallFailed') + (e?.message || e)) } } } } catch { cards.innerHTML = `
${t('common.loadFailed')}
` } } /** * 版本选择器弹窗 — 选择版本(汉化版/原版)+ 版本号 */ async function showVersionPicker(page, currentVersion) { const isInstalled = !!currentVersion.current const overlay = document.createElement('div') overlay.className = 'modal-overlay' overlay.innerHTML = ` ` 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 ? t('about.tagRecommended') : t('about.tagNeedTest') const sameSource = targetSource === (currentVersion.source === 'official' ? 'official' : 'chinese') if (!isInstalled) { confirmBtn.textContent = t('about.btnInstall') hintEl.textContent = t('about.hintInstall', { source: targetSource === 'official' ? t('about.official') : t('about.chinese'), ver: targetVer, tag: targetTag }) confirmBtn.disabled = false return } if (!sameSource) { confirmBtn.textContent = t('about.btnSwitch') hintEl.innerHTML = `${t('about.hintCurrent')}: ${currentVersion.source === 'official' ? t('about.official') : t('about.chinese')} ${currentVersion.current}${targetSource === 'official' ? t('about.official') : t('about.chinese')} ${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 = t('about.btnReinstall') hintEl.textContent = t('about.hintAlreadyVersion', { ver: targetVer, tag: targetTag }) confirmBtn.disabled = false } else if (cmp > 0) { confirmBtn.textContent = t('about.btnUpgrade') hintEl.innerHTML = `${currentVersion.current} → ${targetVer}${targetTag}` confirmBtn.disabled = false } else { confirmBtn.textContent = t('about.btnDowngrade') 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 ? `${t('about.hidePreview', { count: nightlyCount })}` : `${t('about.showPreview', { count: 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 : t('about.operationDone')) }) unlistenError = await listen('upgrade-error', async (e) => { cleanup() const errStr = String(e.payload || t('common.unknown')) 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(t('about.taskStarted')) } else { modal.appendLog(t('about.webModeNoLog')) const msg = await api.upgradeOpenclaw(source, version) modal.setDone(typeof msg === 'string' ? msg : (msg?.message || t('about.operationDone'))) 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} ${t('about.updateReady')} ` meta.querySelector('#btn-hot-reload')?.addEventListener('click', () => { window.location.reload() }) meta.querySelector('#btn-hot-rollback')?.addEventListener('click', async () => { try { await api.rollbackFrontendUpdate() toast(t('about.rollbackSuccess'), 'success') setTimeout(() => window.location.reload(), 800) } catch (e) { toast(t('about.rollbackFailed') + (e.message || e), 'error') } }) } else if (info.hasUpdate) { // 有新版本可下载 const ver = info.latestVersion const manifest = info.manifest || {} const changelog = manifest.changelog || '' meta.innerHTML = ` ${t('about.newVersion')}: v${ver} ${changelog ? `${changelog}` : ''} ${t('about.fullInstaller')} ` meta.querySelector('#btn-hot-download')?.addEventListener('click', async () => { const btn = meta.querySelector('#btn-hot-download') if (btn) { btn.disabled = true; btn.textContent = t('about.downloading') } try { await api.downloadFrontendUpdate(manifest.url, manifest.hash || '') toast(t('about.downloadDone'), 'success') checkHotUpdate(cards, panelVersion) } catch (e) { toast(t('about.downloadFailed') + (e.message || e), 'error') if (btn) { btn.disabled = false; btn.textContent = t('about.retry') } } }) } else if (!info.compatible) { meta.innerHTML = `${t('about.needFullUpdate')} ${t('about.goToWebsite')} GitHub` } else { meta.innerHTML = `${t('about.upToDate')}` } } catch (err) { const meta = el() if (!meta) return meta.innerHTML = `${t('about.checkUpdateFailed')} ${t('about.goToWebsite')}` } } 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 = `
${t('about.qqGroup')}
${t('about.qqGroup')}
${t('about.wechatGroup')}
${t('about.wechatGroup')}
${t('about.douyinGroup')}
${t('about.douyinGroup')}
${t('about.feishuGroup')}
${t('about.feishuGroup')}
${t('about.communityWelcome')}
${t('about.communityWelcomeIntl')}
${t('about.communityDesc')}
${icon('message-circle', 14)} ${t('about.joinDiscord')} ${t('about.joinQQ')} ${t('about.joinWechat')} ${t('about.joinDouyin')} ${t('about.joinFeishu')} ${t('about.joinYuanbao')}
${t('about.communityNote')}
` } const PROJECTS = [ { name: 'OpenClaw', desc: t('about.projectOpenClaw'), url: 'https://github.com/openclaw/openclaw', }, { name: 'OpenClaw-zh', desc: t('about.projectOpenClawZh'), url: 'https://github.com/1186258278/OpenClawChineseTranslation', }, { name: 'ClawPanel', desc: t('about.projectClawPanel'), url: 'https://github.com/qingchencloud/clawpanel', gitee: 'https://gitee.com/QtCodeCreators/clawpanel', }, { name: 'ClawApp', desc: t('about.projectClawApp'), url: 'https://github.com/qingchencloud/clawapp', }, { name: 'cftunnel', desc: t('about.projectCftunnel'), url: 'https://github.com/qingchencloud/cftunnel', }, ] function renderProjects(page) { const el = page.querySelector('#projects-list') el.innerHTML = PROJECTS.map(p => `
${p.name}
${p.desc}
GitHub ${p.gitee ? `${t('about.domesticMirror')}` : ''}
`).join('') } const LINKS = [ { label: t('about.linkWebsite'), url: 'https://claw.qt.cool', primary: true }, { label: t('about.linkOpenClawZh'), url: 'https://github.com/1186258278/OpenClawChineseTranslation' }, { label: t('about.linkClawApp'), url: 'https://clawapp.qt.cool' }, { label: t('about.linkCftunnel'), url: 'https://cftunnel.qt.cool' }, ] function renderContribute(page) { const el = page.querySelector('#contribute-section') el.innerHTML = `
${t('about.contributeDesc')}
${t('about.submitIssue')} ${t('about.submitPR')} ${t('about.contributeGuide')} ${t('about.viewIssues')}
${t('about.domesticMirrorHint')}
` } function renderLinks(page) { const el = page.querySelector('#links-list') el.innerHTML = `
${LINKS.map(l => `${l.label}`).join('')}
` } function renderCompany(page) { const el = page.querySelector('#company-section') el.innerHTML = `
QingchenCloud
${t('about.companyName')}
QingchenCloud
${t('about.officialWebsite')}
qingchencloud.com
${t('about.productWebsite')}
claw.qt.cool
${t('about.openSourceRepo')}
github.com/qingchencloud
${t('about.businessCoop')}
support@qctx.net
${t('about.companyDesc')}
` }