/** * 关于页面 * 版本信息、项目链接、相关项目、系统环境 */ 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, getLang } from '../lib/i18n.js' import { getActiveEngineId } from '../lib/engine-manager.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')}

` if (getActiveEngineId() === 'hermes') { loadHermesData(page) } else { loadData(page) } renderCommunity(page) renderProjects(page) renderContribute(page) renderLinks(page) renderCompany(page) return page } async function loadHermesData(page) { const cards = page.querySelector('#version-cards') try { const [hermesInfo, pythonInfo] = await Promise.all([ api.checkHermes().catch(() => null), api.checkPython().catch(() => null), ]) const panelVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.1.0' let panelUpdateHtml = `${t('about.checkingUpdate')}` checkNewVersion(cards, panelVersion) const installed = !!hermesInfo?.installed const gwRunning = !!hermesInfo?.gatewayRunning const version = hermesInfo?.hermesVersion || hermesInfo?.version || '' const model = hermesInfo?.model || '' const port = hermesInfo?.gatewayPort || 8642 const pyVer = pythonInfo?.version || '' const pyPath = pythonInfo?.path || '' const esc = s => String(s || '').replace(/&/g, '&').replace(//g, '>') const btnSm = 'padding:2px 8px;font-size:var(--font-size-xs)' cards.innerHTML = `
ClawPanel
${panelVersion}
${panelUpdateHtml}
Hermes Agent
${installed ? (version || t('about.installed')) : t('about.notInstalled')}
${gwRunning ? `● Gateway ${t('engine.dashRunning')} · :${port}` : `○ Gateway ${t('engine.dashStopped')}`} ${model ? `${t('engine.dashModel')}: ${esc(model)}` : ''} ${!installed ? `${t('about.hermesSetup')}` : ''} ${installed ? ` ${t('about.hermesConfig')} ` : ''}
Python
${pyVer || t('about.notInstalled')}
${esc(pyPath)}
` // Hermes 管理按钮事件 if (installed) { const upgradeBtn = cards.querySelector('#btn-hermes-upgrade') const uninstallBtn = cards.querySelector('#btn-hermes-uninstall') if (upgradeBtn) { upgradeBtn.onclick = async () => { upgradeBtn.disabled = true upgradeBtn.textContent = t('about.upgrading') try { const ver = await api.updateHermes() toast(t('about.hermesUpgradeOk', { version: ver || '' }), 'success') loadHermesData(page) } catch (e) { toast(t('about.hermesUpgradeFail', { error: e.message || e }), 'error') } finally { upgradeBtn.disabled = false upgradeBtn.textContent = t('about.hermesUpgrade') } } } if (uninstallBtn) { uninstallBtn.onclick = async () => { const confirmed = confirm(t('about.hermesUninstallConfirm')) if (!confirmed) return const cleanConfig = confirm(t('about.hermesUninstallCleanConfig')) uninstallBtn.disabled = true uninstallBtn.textContent = t('about.uninstalling') try { await api.uninstallHermes(cleanConfig) toast(t('about.hermesUninstallOk'), 'success') loadHermesData(page) } catch (e) { toast(t('about.hermesUninstallFail', { error: e.message || e }), 'error') } finally { uninstallBtn.disabled = false uninstallBtn.textContent = t('about.hermesUninstall') } } } } } catch { cards.innerHTML = `
${t('common.loadFailed')}
` } } 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 const panelVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.1.0' let panelUpdateHtml = `${t('about.checkingUpdate')}` checkNewVersion(cards, panelVersion) const isInstalled = !!version.current const sourceLabel = version.source === 'official' ? t('about.official') : version.source === 'chinese' ? t('about.chinese') : t('about.unknownSource') 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.setProgressLabels({ preparing: t('about.uninstallStopping'), downloading: t('about.uninstallRemoving'), installing: t('about.uninstallCleaning'), done: t('about.uninstallDone'), }) 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 if (!isInstalled) { confirmBtn.textContent = t('about.btnInstall') hintEl.textContent = t('about.hintInstall', { source: targetSource === 'official' ? t('about.official') : targetSource === 'chinese' ? t('about.chinese') : t('about.unknownSource'), 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') : currentVersion.source === 'chinese' ? t('about.chinese') : t('about.unknownSource')} ${currentVersion.current}${targetSource === 'official' ? t('about.official') : targetSource === 'chinese' ? t('about.chinese') : t('about.unknownSource')} ${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 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 checkNewVersion(cards, panelVersion) { const el = () => cards.querySelector('#panel-update-meta') const btnSm = 'padding:2px 8px;font-size:var(--font-size-xs)' // 尝试获取 Tauri 二进制版本,检测「假更新」: // 前端通过热更新升级到 v0.13.0,但 Tauri 二进制仍是 v0.9.9 let binaryVersion = panelVersion try { const { getVersion } = await import('@tauri-apps/api/app') binaryVersion = await getVersion() } catch {} // 前端版本 > 二进制版本 = 热更新导致版本不一致 const isFakeUpdate = binaryVersion !== panelVersion && compareVersions(panelVersion, binaryVersion) > 0 try { const info = await api.checkPanelUpdate() const meta = el() if (!meta) return const latest = info?.latest || '' // 用二进制版本(真实应用版本)做比较,避免假更新导致误判为「已是最新」 const effectiveVersion = isFakeUpdate ? binaryVersion : panelVersion if (isFakeUpdate) { meta.innerHTML = ` ⚠️ ${t('about.versionMismatch', { frontend: panelVersion, binary: binaryVersion })} ${t('about.hotUpdateDeprecated')} ${t('about.downloadFullInstaller')} ${t('about.downloadFromGitHub')} ` } else if (latest && latest !== effectiveVersion && compareVersions(latest, effectiveVersion) > 0) { meta.innerHTML = ` ${t('about.newVersionAvailable', { version: latest })} ${t('about.downloadFromWebsite')} ${t('about.downloadFromGitHub')} ` } else { meta.innerHTML = `${t('about.upToDate')}` } } catch (err) { const meta = el() if (!meta) return if (isFakeUpdate) { meta.innerHTML = `⚠️ ${t('about.versionMismatch', { frontend: panelVersion, binary: binaryVersion })} ${t('about.downloadFullInstaller')}` } else { 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')}
${!getLang().startsWith('zh') ? `
${t('about.sponsorProject') || 'Sponsor This Project'} · USDT (BNB Smart Chain)
0xbdd7ebdf2b30d873e556799711021c6671ffe88f
${t('about.sponsorDesc') || 'Your support helps us maintain and improve this open-source project.'}
` : ''}
` // QR 点击预览大图 el.querySelector('#sponsor-qr-thumb')?.addEventListener('click', () => { const overlay = document.createElement('div') overlay.className = 'modal-overlay' overlay.innerHTML = ` ` document.body.appendChild(overlay) overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove() }) overlay.querySelector('[data-action="close"]').onclick = () => overlay.remove() }) }