/** * 初始设置页面 — openclaw 未安装时的引导 * 自动检测环境 → 版本选择 → 一键安装 → 自动跳转 */ import { api, invalidate, isTauriRuntime, safeTauriListen } from '../lib/tauri-api.js' import { showConfirm, showUpgradeModal } from '../components/modal.js' import { toast } from '../components/toast.js' import { setUpgrading, isMacPlatform } from '../lib/app-state.js' import { getActiveEngine } from '../lib/engine-manager.js' import { diagnoseInstallError } from '../lib/error-diagnosis.js' import { icon, statusIcon } from '../lib/icons.js' import { t } from '../lib/i18n.js' function escapeHtml(str) { if (str == null) return '' return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function openclawSourceLabel(src) { return ({ portable: t('dashboard.cliSourcePortable'), standalone: t('dashboard.cliSourceStandalone'), 'npm-zh': t('dashboard.cliSourceNpmZh'), 'npm-official': t('dashboard.cliSourceNpmOfficial'), 'npm-global': t('dashboard.cliSourceNpmGlobal'), })[src] || t('dashboard.cliSourceUnknown') } function parseOpenclawSearchPaths(raw) { const values = [] const seen = new Set() for (const part of String(raw || '').split(/[\r\n;]+/)) { const value = part.trim() if (!value) continue const key = value.toLowerCase() if (seen.has(key)) continue seen.add(key) values.push(value) } return values } function buildStatusMeta(...parts) { return parts .map(part => String(part || '').trim()) .filter(Boolean) .join(' · ') } function isWindowsPlatform() { return navigator.platform?.startsWith('Win') || navigator.userAgent?.includes('Windows') } function isLinuxPlatform() { return navigator.platform?.toLowerCase().includes('linux') || navigator.userAgent?.toLowerCase().includes('linux') } function canAutoUpgradeNode() { return isTauriRuntime() && isWindowsPlatform() } function nodeRuntimeHint(nodeTooOld) { if (!nodeTooOld) return t('setup.winNodeHint') if (isWindowsPlatform()) return t('setup.winNodeUpgradeHint') if (isMacPlatform()) return t('setup.macNodeUpgradeHint') if (isLinuxPlatform()) return t('setup.linuxNodeUpgradeHint') return t('setup.genericNodeUpgradeHint') } function nodePathPlaceholder() { if (isMacPlatform()) return '/usr/local/bin' if (isLinuxPlatform()) return '/usr/bin' return 'F:\\AI\\Node' } function normalizeNodeUpgradeLog(line) { const text = String(line || '').trimEnd() if (!text.trim()) return null if (/[█▓▒░]/.test(text)) return null if (/^[\s\\|/\-]+$/.test(text)) return null return text } function renderDetectionHint(pathValue, sourceLabel = '') { const normalizedPath = String(pathValue || '').trim() const normalizedSource = String(sourceLabel || '').trim() if (!normalizedPath && !normalizedSource) return '' return `
${normalizedPath ? `
${t('setup.detectedPathLabel')}: ${escapeHtml(normalizedPath)}
` : ''} ${normalizedSource ? `${t('setup.detectedFromLabel')}: ${escapeHtml(normalizedSource)}
` : ''} ` } function renderStatusCard(title, ok, meta) { return `
${ok ? '✓' : '✦'}
${title}
${escapeHtml(meta)}
` } export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `

${t('setup.headerTitle')}

${t('setup.headerDesc')}

` page.querySelector('#btn-recheck').addEventListener('click', () => runDetect(page)) // 便携模式徽标:enabled 才显示;状态在进程生命周期内固定,接口失败静默 api.getPortableStatus().then(st => { if (!st?.enabled) return const el = page.querySelector('#setup-portable-badge') if (!el) return const warnings = (st.warnings || []).map(w => { if (w === 'node-missing') return t('setup.portableWarnNode') if (w === 'cli-missing') return t('setup.portableWarnCli') if (w === 'hermes-cli-missing') return t('setup.portableWarnHermesCli') if (typeof w === 'string' && w.startsWith('absolute-path:')) { return t('setup.portableWarnAbsPath', { field: w.slice('absolute-path:'.length) }) } return String(w) }) el.style.display = 'block' el.innerHTML = ` 💾 ${t('setup.portableBadge')} · ${escapeHtml(st.root || '')} ${warnings.length ? `
${warnings.map(escapeHtml).join('
')}
` : ''} ` }).catch(() => {}) // #Compat-4: 用户在浏览器里手动装完 Node.js 后切回 panel,或用户装完 Git/OpenClaw // 后 app 失焦又重新获得焦点时,自动重新检测,避免「装完不识别」。 // handler 自带 guard:page 从 DOM 移除后自动卸载监听器,防止跨页面泄漏。 // 同时监听 visibilitychange(tab 切换)和 window focus(桌面端窗口激活),兜底不同平台行为。 let _lastRedetectAt = 0 const onVisibilityChange = () => { if (!page.isConnected) { document.removeEventListener('visibilitychange', onVisibilityChange) window.removeEventListener('focus', onVisibilityChange) return } if (document.visibilityState !== 'visible') return // 3 秒内不重复触发(避免 focus + visibilitychange 同时连发) const now = Date.now() if (now - _lastRedetectAt < 3000) return _lastRedetectAt = now // 聚焦重检走 silent:需要刷新缓存(可能刚装完 Node/Git),但不清空已渲染内容 runDetect(page, { silent: true }) } document.addEventListener('visibilitychange', onVisibilityChange) window.addEventListener('focus', onVisibilityChange) // 首次进入:跳过缓存清理,尽快出首屏 runDetect(page, { fresh: false }) return page } async function maybeRefreshGatewayServiceBinding() { if (!isMacPlatform()) return false const [versionInfo, dirInfo] = await Promise.all([ api.getVersionInfo().catch(() => null), api.getOpenclawDir().catch(() => null), ]) if (!versionInfo?.cli_path || dirInfo?.configExists === false) { return false } const shouldRefresh = await showConfirm(t('settings.gatewayServiceRefreshConfirm')) if (!shouldRefresh) return false toast(t('settings.gatewayServiceRefreshing'), 'info') try { const services = await api.getServicesStatus().catch(() => []) const gw = services?.find?.(s => s.label === 'ai.openclaw.gateway') || services?.[0] || null const shouldStartAgain = gw?.running === true && gw?.owned_by_current_instance !== false await api.uninstallGateway().catch(() => {}) await api.installGateway() if (shouldStartAgain) { await api.startService('ai.openclaw.gateway') } toast(t('settings.gatewayServiceRefreshed'), 'success') return true } catch (e) { toast(`${t('settings.gatewayServiceRefreshFailed')}: ${e?.message || e}`, 'warning') return false } } async function promptRestart(msg) { if (!window.__TAURI_INTERNALS__) { toast(msg, 'success'); return } const ok = await showConfirm(`${msg}\n\n${t('settings.restartConfirm')}`) if (ok) { toast(t('settings.restarting'), 'info') try { await api.relaunchApp() } catch { toast(t('settings.restartFailed'), 'warning') } } else { toast(`${msg}, ${t('settings.effectNextLaunch')}`, 'success') } } // 检测序号:并发触发(首次加载 / 重新检测 / 窗口聚焦)时,只允许最新一次的结果上屏 let _detectSeq = 0 async function runDetect(page, { fresh = true, silent = false } = {}) { const seq = ++_detectSeq const stepsEl = page.querySelector('#setup-steps') // silent(窗口聚焦触发的后台重检):保留已渲染的步骤卡片,检测完成后原地更新, // 不再每次聚焦都清成骨架屏,避免"一直在检测"的观感 const hasRenderedContent = stepsEl.children.length > 0 && !stepsEl.querySelector('.loading-placeholder') if (!silent || !hasRenderedContent) { stepsEl.innerHTML = `
` } // 首次进入页面(fresh=false)跳过缓存清理:进程刚启动时 PATH 缓存是新鲜的, // 还能复用启动流程已缓存的检测结果,让首屏尽快出现; // 手动「重新检测」、窗口聚焦重检和安装后的重试仍然完整清理(fresh=true)。 if (fresh) { // 清除前端 invoke 缓存 invalidate('get_version_info', 'check_node', 'check_git', 'get_services_status', 'check_installation') // #Compat-4: 同步刷新 Rust 端 PATH 缓存 + CLI 检测缓存 // 用户手动装完 Node.js/Git 后,Tauri 进程的 PATH 仍是启动时快照,且 enhanced_path 有缓存。 // 必须先调此命令扫描文件系统新装路径,才能让 where/which 找到新二进制。 try { await api.invalidatePathCache() } catch {} } try { // 版本信息包含 npm registry 网络查询与全盘安装扫描,是最慢的一项; // 步骤卡片的通过/失败判定不依赖它,先用本地检测结果渲染,版本详情返回后再补充 const versionPromise = api.getVersionInfo().catch(() => null) // 并行检测 Node.js、Git、OpenClaw CLI、配置文件 const [nodeRes, gitRes, clawRes, configRes] = await Promise.allSettled([ api.checkNode(), api.checkGit(), api.getServicesStatus(), api.checkInstallation(), ]) if (seq !== _detectSeq || !page.isConnected) return const node = nodeRes.status === 'fulfilled' ? nodeRes.value : { installed: false } const git = gitRes.status === 'fulfilled' ? gitRes.value : { installed: false } const cliOk = clawRes.status === 'fulfilled' && clawRes.value?.length > 0 && clawRes.value[0]?.cli_installed !== false let config = configRes.status === 'fulfilled' ? configRes.value : { installed: false } // Git 已安装时,自动配置 HTTPS 替代 SSH(静默执行) if (git.installed) { api.configureGitHttps().catch(() => {}) } const nodeOk = node.installed && node.compatible !== false const allOk = nodeOk && cliOk && config.installed // 全部通过 → 自动跳转到仪表盘 if (allOk) { const engine = getActiveEngine() if (engine?.detect) await engine.detect() window.location.hash = '/dashboard' return } renderSteps(page, { node, git, cliOk, config, version: null }) const version = await versionPromise if (seq !== _detectSeq || !page.isConnected || !version) return renderSteps(page, { node, git, cliOk, config, version }) } catch (error) { // 兜底:任何异常都不能让骨架屏永久残留 console.error('[setup] 环境检测失败', error) if (seq !== _detectSeq || !page.isConnected) return stepsEl.innerHTML = `
${escapeHtml(error?.message || String(error))}
${escapeHtml(t('setup.recheck'))}
` } } function stepIcon(ok) { const color = ok ? 'var(--success)' : 'var(--text-tertiary)' return `${ok ? '✓' : '✗'}` } function renderSteps(page, { node, git, cliOk, config, version }) { const stepsEl = page.querySelector('#setup-steps') const nodeOk = node.installed && node.compatible !== false const gitOk = git?.installed || false const allOk = nodeOk && cliOk && config.installed const nodeStatusMeta = node.installed && node.compatible === false ? t('setup.nodeVersionUnsupported', { version: node.version || t('common.unknown'), required: node.requiredVersion || t('common.unknown') }) : nodeOk ? buildStatusMeta(node.version || t('setup.statusReady'), node.path) : t('setup.statusActionNeeded') const gitStatusMeta = gitOk ? buildStatusMeta(git.version || t('setup.statusReady'), git.path) : t('setup.statusActionNeeded') const cliPrimaryMeta = cliOk ? buildStatusMeta(version?.cli_source ? openclawSourceLabel(version.cli_source) : '', version?.current ? `v${version.current}` : t('setup.statusReady')) : '' const cliStatusMeta = cliOk ? buildStatusMeta(cliPrimaryMeta, version?.cli_path) : t('setup.statusActionNeeded') const configStatusMeta = config.installed ? (config.path || t('setup.statusReady')) : t('setup.statusActionNeeded') const statusCards = [ renderStatusCard(t('setup.stepNode'), nodeOk, nodeStatusMeta), renderStatusCard(t('setup.stepGit'), gitOk, gitStatusMeta), renderStatusCard('OpenClaw CLI', cliOk, cliStatusMeta), renderStatusCard(t('setup.stepConfig'), config.installed, configStatusMeta), ].join('') let html = `
${statusCards}
` // 第一步:Node.js if (!nodeOk) { const nodeTooOld = node.installed && node.compatible === false html += `
${stepIcon(nodeOk)} ${t('setup.stepNode')}

${nodeTooOld ? t('setup.nodeUpgradeHint', { version: node.version || t('common.unknown'), required: node.requiredVersion || t('common.unknown') }) : t('setup.stepNodeHint')}

${nodeTooOld && canAutoUpgradeNode() ? `` : ''} ${nodeTooOld ? t('setup.downloadLatestNode') : t('setup.downloadNode')} ${t('setup.recheckAfterInstall')}
${nodeTooOld ? t('setup.nodeUnsupportedTitle') : t('setup.nodeInstalledButNotDetected')} ${isMacPlatform() ? `${nodeTooOld ? t('setup.macNodeUpgradeHint') : t('setup.macNodeHint')}
open /Applications/ClawPanel.app` : `${nodeRuntimeHint(nodeTooOld)}` }
${t('setup.orManualPath')}
` } // 第二步:Git if (!gitOk) { html += `
${stepIcon(gitOk)} ${t('setup.stepGit')}

${t('setup.stepGitHint')}

${t('setup.manualDownload')}
${t('setup.gitOptionalHint')}
` } // 第三步:OpenClaw CLI html += `
${stepIcon(cliOk)} OpenClaw CLI
${cliOk ? `

${t('setup.cliAvailable')}

${renderDetectionHint(version?.cli_path, version?.cli_source ? openclawSourceLabel(version.cli_source) : '')} ${version?.ahead_of_recommended && version?.recommended ? `
${t('setup.cliAheadWarning', { current: version.current || '', recommended: version.recommended })}
` : ''}` : renderInstallSection() }
` html += `
` // 第四步:配置文件 + 自定义路径 html += `
${stepIcon(config.installed)} ${t('setup.stepConfig')}
${config.installed ? `

${t('setup.configAt', { path: config.path || '' })}

${renderDetectionHint(config.path)}` : `

${t('setup.configMissing')}

${renderDetectionHint(config.path)} ` }
${t('setup.customDirTitle')}

${t('setup.customDirHint')}

${t('setup.customDirNotice')}
` // AI 助手入口 html += `
${t('setup.aiAssistant')}

${t('setup.aiAssistantDesc')}${!allOk ? t('setup.aiAssistantDescProblem') : ''}。

${!allOk ? `` : ''}
` html += `
` if (!cliOk) { html += renderEnvironmentHint() } // 全部就绪 → 进入面板 if (allOk) { html += `
${t('setup.nextStepsTitle')}
${t('setup.nextStepsDesc')}
  1. ${t('setup.nextStep1')}
  2. ${t('setup.nextStep2')}
  3. ${t('setup.nextStep3')}
` } stepsEl.innerHTML = html bindEvents(page, nodeOk, { node, git, cliOk, config }) } function renderInstallSection() { return `
${t('setup.searchOpenclawTitle')}
${t('setup.searchOpenclawDesc')}
${t('setup.searchOpenclawHint')}
${t('setup.searchOpenclawAdvancedTitle')}
${t('setup.searchOpenclawAdvancedHint')}
${t('setup.searchOpenclawExtraPathsHint')}
${t('setup.searchOpenclawManualHint')}
${t('setup.installOpenclaw')}

${t('setup.installHint')}

${t('setup.installHint2')}

` } function renderEnvironmentHint() { const isWin = navigator.platform?.startsWith('Win') || navigator.userAgent?.includes('Windows') const isMac = navigator.platform?.startsWith('Mac') || navigator.userAgent?.includes('Macintosh') const isDesktop = !!window.__TAURI_INTERNALS__ if (!isDesktop) return '' return `
${t('setup.envHintTitle')}

${t('setup.envHintDesc')}

${t('setup.envHintInstallManage')}
    ${isWin ? `
  • ${t('setup.envHintWsl')} — ${t('setup.envHintWslDesc')}
  • ${t('setup.envHintDocker')} — ${t('setup.envHintDockerDesc')}
  • ` : ''} ${isMac ? `
  • ${t('setup.envHintDocker')} — ${t('setup.envHintDockerDesc')}
  • ${t('setup.envHintRemote')} — ${t('setup.envHintRemoteDesc')}
  • ` : ''} ${!isWin && !isMac ? `
  • ${t('setup.envHintDocker')} — ${t('setup.envHintDockerDesc')}
  • ` : ''}
${isWin ? `
${t('setup.wslWebHint')}
${t('setup.wslWebDesc')}
curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
${t('setup.domesticMirror')} curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash
${t('setup.wslWebPostDeploy')}
` : ''}
${t('setup.dockerHint')}
${t('setup.dockerDesc')}
npm i -g @qingchencloud/openclaw-zh curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
${t('setup.domesticMirrorShort')} curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash
${t('setup.remoteHint')}
${t('setup.remoteDesc')}
curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
${t('setup.domesticMirrorShort')} curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash
${t('setup.envHintLocalReinstall')}
` } function buildSetupProblemPrompt({ node, git, cliOk, config }) { const problems = [] if (!node.installed) problems.push(`- ${t('setup.promptNodeMissing')}`) else if (node.compatible === false) problems.push(`- ${t('setup.promptNodeUnsupported', { version: node.version || t('common.unknown'), required: node.requiredVersion || t('common.unknown') })}`) else problems.push(`- ${t('setup.promptNodeOk', { version: node.version || t('common.unknown') })}`) if (!git?.installed) problems.push(`- ${t('setup.promptGitMissing')}`) else problems.push(`- ${t('setup.promptGitOk', { version: git.version || t('common.unknown') })}`) if (!cliOk) problems.push(`- ${t('setup.promptCliMissing')}`) else problems.push(`- ${t('setup.promptCliOk')}`) if (!config.installed) problems.push(`- ${t('setup.promptConfigMissing')}`) else problems.push(`- ${t('setup.promptConfigOk', { path: config.path || '' })}`) return `${t('setup.promptIntro')} ${problems.join('\n')} ${t('setup.promptOutro')}` } function bindEvents(page, nodeOk, detectState) { // 打开 AI 助手 page.querySelector('#btn-goto-assistant')?.addEventListener('click', () => { window.location.hash = '/assistant' }) // 让 AI 帮我解决(带问题上下文) page.querySelector('#btn-ask-ai-help')?.addEventListener('click', () => { if (detectState) { const prompt = buildSetupProblemPrompt(detectState) sessionStorage.setItem('assistant-auto-prompt', prompt) } window.location.hash = '/assistant' }) // 进入面板(刷新引擎 ready 状态,触发侧边栏更新) async function refreshAndNavigate(route) { const engine = getActiveEngine() if (engine?.detect) await engine.detect() window.location.hash = route } page.querySelector('#btn-enter')?.addEventListener('click', () => refreshAndNavigate('/dashboard')) page.querySelector('#btn-goto-models')?.addEventListener('click', () => refreshAndNavigate('/models')) page.querySelector('#btn-goto-gateway')?.addEventListener('click', () => refreshAndNavigate('/gateway')) page.querySelector('#btn-goto-channels')?.addEventListener('click', () => refreshAndNavigate('/channels')) // 一键安装 Git page.querySelector('#btn-auto-install-git')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-auto-install-git') const resultEl = page.querySelector('#git-install-result') btn.disabled = true btn.textContent = t('setup.installingGit') if (resultEl) { resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.gitInstallingHint')}` } try { const msg = await api.autoInstallGit() if (resultEl) resultEl.innerHTML = `✓ ${msg}` toast(t('setup.gitInstallSuccess'), 'success') // 安装成功后自动配置 HTTPS api.configureGitHttps().catch(() => {}) setTimeout(() => runDetect(page), 1000) } catch (e) { const errMsg = String(e.message || e) if (resultEl) { resultEl.innerHTML = `
${t('setup.gitAutoInstallFailed', { err: errMsg })}

${t('setup.gitManualHint')}
${t('setup.gitManualInstallHtml')}

` } toast(t('setup.gitAutoInstallFailedToast'), 'warning') } finally { btn.disabled = false btn.textContent = t('setup.autoInstallGitBtn') } }) // 一键安装 / 升级 Node.js page.querySelector('#btn-auto-install-node')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-auto-install-node') const yes = await showConfirm({ title: t('setup.nodeUpgradeConfirmTitle'), message: t('setup.nodeUpgradeConfirmMessage'), impact: [ t('setup.nodeUpgradeConfirmImpactWinget'), t('setup.nodeUpgradeConfirmImpactPermission'), t('setup.nodeUpgradeConfirmImpactRedetect'), ], confirmText: t('setup.autoUpgradeNodeBtn'), cancelText: t('common.cancel'), variant: 'primary', }) if (!yes) return const modal = showUpgradeModal(t('setup.nodeUpgradeTitle')) modal.setProgressLabels({ preparing: t('setup.nodeUpgradePreparing'), downloading: t('setup.nodeUpgradeInstalling'), installing: t('setup.nodeUpgradeVerifying'), done: t('setup.nodeUpgradeDone'), }) modal.setProgress(15) modal.appendLog(t('setup.nodeUpgradeStarting')) let unlistenLog, unlistenProgress const cleanup = () => { setUpgrading(false) unlistenLog?.() unlistenProgress?.() } setUpgrading(true) btn.disabled = true btn.textContent = t('setup.upgradingNode') try { unlistenLog = await safeTauriListen('upgrade-log', (e) => { const line = normalizeNodeUpgradeLog(e.payload) if (line) modal.appendLog(line) }) unlistenProgress = await safeTauriListen('upgrade-progress', (e) => modal.setProgress(e.payload)) const msg = await api.autoInstallNode() modal.setProgress(100) modal.setDone(msg || t('setup.nodeUpgradeSuccess')) modal.setCloseText(t('common.completed')) modal.appendLog(t('setup.nodeUpgradeRedetecting')) modal.appendLog(t('setup.nodeUpgradeStartGatewayHint')) toast(msg || t('setup.nodeUpgradeSuccess'), 'success') await api.invalidatePathCache().catch(() => {}) setTimeout(() => runDetect(page), 800) } catch (e) { const errMsg = String(e?.message || e) modal.setError(t('setup.nodeAutoUpgradeFailedTitle')) modal.appendLog(errMsg) modal.appendLog('') modal.appendLog(t('setup.nodeManualInstallHint')) modal.appendLog('https://nodejs.org/') modal.appendLog(t('setup.nodeUpgradeRestartHint')) toast(t('setup.nodeAutoUpgradeFailed', { err: errMsg }), 'error') btn.disabled = false btn.textContent = t('setup.autoUpgradeNodeBtn') } finally { cleanup() } }) // 自定义 OpenClaw 安装路径 const dirInput = page.querySelector('#input-openclaw-dir') const dirResultEl = page.querySelector('#openclaw-dir-result') // 预填当前自定义路径 if (dirInput) { api.getOpenclawDir().then(info => { if (info.isCustom) { dirInput.value = info.path // 已有自定义路径时自动展开 const details = page.querySelector('#custom-dir-details') if (details) details.open = true } }).catch(() => {}) } const searchPathsInput = page.querySelector('#input-openclaw-search-paths') api.readPanelConfig().then(cfg => { if (searchPathsInput) { const values = Array.isArray(cfg?.openclawSearchPaths) ? cfg.openclawSearchPaths : [] searchPathsInput.value = values.join('\n') } }).catch(() => {}) page.querySelector('#btn-save-openclaw-dir')?.addEventListener('click', async () => { const value = dirInput?.value?.trim() if (!value) { toast(t('setup.enterPath'), 'warning'); return } const btn = page.querySelector('#btn-save-openclaw-dir') btn.disabled = true if (dirResultEl) { dirResultEl.style.display = 'block'; dirResultEl.innerHTML = `${t('setup.saving')}` } try { const cfg = await api.readPanelConfig() cfg.openclawDir = value await api.writePanelConfig(cfg) invalidate() if (dirResultEl) dirResultEl.innerHTML = `✓ ${t('setup.pathSaved')}` const savedMsg = t('setup.customPathSaved') const refreshed = await maybeRefreshGatewayServiceBinding() if (refreshed) toast(savedMsg, 'success') else await promptRestart(savedMsg) setTimeout(() => runDetect(page), 500) } catch (e) { if (dirResultEl) dirResultEl.innerHTML = `${t('setup.saveFailed', { err: e })}` toast(t('setup.saveFailed', { err: e }), 'error') } finally { btn.disabled = false } }) page.querySelector('#btn-save-openclaw-search-paths')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-save-openclaw-search-paths') const resultEl = page.querySelector('#openclaw-search-paths-result') const paths = parseOpenclawSearchPaths(searchPathsInput?.value || '') btn.disabled = true if (resultEl) { resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.saving')}` } try { const cfg = await api.readPanelConfig() if (paths.length > 0) { cfg.openclawSearchPaths = paths } else { delete cfg.openclawSearchPaths } await api.writePanelConfig(cfg) invalidate() if (resultEl) { resultEl.innerHTML = `✓ ${paths.length > 0 ? t('setup.searchOpenclawExtraPathsSaved') : t('setup.searchOpenclawExtraPathsCleared')}` } toast(paths.length > 0 ? t('setup.searchOpenclawExtraPathsSaved') : t('setup.searchOpenclawExtraPathsCleared'), 'success') setTimeout(() => runDetect(page), 300) } catch (e) { if (resultEl) { resultEl.innerHTML = `${t('setup.saveFailed', { err: e })}` } toast(t('setup.saveFailed', { err: e }), 'error') } finally { btn.disabled = false } }) page.querySelector('#btn-reset-openclaw-dir')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-reset-openclaw-dir') btn.disabled = true try { const cfg = await api.readPanelConfig() delete cfg.openclawDir await api.writePanelConfig(cfg) invalidate() if (dirInput) dirInput.value = '' if (dirResultEl) { dirResultEl.style.display = 'block'; dirResultEl.innerHTML = `✓ ${t('setup.defaultRestored')}` } const restoredMsg = t('setup.defaultRestoredToast') const refreshed = await maybeRefreshGatewayServiceBinding() if (refreshed) toast(restoredMsg, 'success') else await promptRestart(restoredMsg) setTimeout(() => runDetect(page), 500) } catch (e) { toast(t('setup.restoreFailed', { err: e }), 'error') } finally { btn.disabled = false } }) // 一键初始化配置 page.querySelector('#btn-init-config')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-init-config') btn.disabled = true btn.textContent = t('setup.initializing') try { const result = await api.initOpenclawConfig() if (result?.restored) { toast(t('setup.configRestored'), 'success') } else if (result?.created) { toast(t('setup.configCreated'), 'success') } else { toast(result?.message || t('setup.configExists'), 'info') } setTimeout(() => runDetect(page), 500) } catch (e) { toast(t('setup.initFailed', { err: e }), 'error') btn.disabled = false btn.textContent = t('setup.initConfigLabel') } }) // 自动扫描 Node.js page.querySelector('#btn-scan-node')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-scan-node') const resultEl = page.querySelector('#scan-result') btn.disabled = true btn.textContent = t('setup.scanning') resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.scanningPaths')}` try { const results = await api.scanNodePaths() if (results.length === 0) { resultEl.innerHTML = `${t('setup.scanNotFound')}` } else { resultEl.innerHTML = results.map(r => { const compatible = r.compatible !== false const color = compatible ? 'var(--success)' : 'var(--danger)' const status = compatible ? '' : `${t('setup.nodeVersionTooLowShort', { required: r.requiredVersion || t('common.unknown') })}` return `
${compatible ? '✓' : '✗'} ${escapeHtml(r.path)} ${escapeHtml(r.version)} ${status}
` } ).join('') resultEl.querySelectorAll('.btn-use-path').forEach(b => { b.addEventListener('click', async () => { try { await api.saveCustomNodePath(b.dataset.path) toast(t('setup.nodeSaved'), 'success') setTimeout(() => runDetect(page), 300) } catch (e) { toast(t('setup.nodePathSaveFailed', { err: e?.message || e }), 'error') } }) }) } } catch (e) { resultEl.innerHTML = `${t('setup.scanFailed', { err: e })}` } finally { btn.disabled = false btn.innerHTML = `${icon('search', 12)} ${t('setup.scanNodeBtn')}` } }) // 手动指定路径检测 page.querySelector('#btn-check-path')?.addEventListener('click', async () => { const input = page.querySelector('#input-node-path') const resultEl = page.querySelector('#scan-result') const dir = input?.value?.trim() if (!dir) { toast(t('setup.enterNodeDir'), 'warning'); return } resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.detecting2')}` try { const result = await api.checkNodeAtPath(dir) if (result.installed) { if (result.compatible === false) { resultEl.innerHTML = `✗ ${t('setup.nodeVersionUnsupported', { version: result.version || t('common.unknown'), required: result.requiredVersion || t('common.unknown') })}` return } await api.saveCustomNodePath(dir) resultEl.innerHTML = `✓ ${t('setup.nodeFoundSaved', { version: result.version })}` toast(t('setup.nodeSaved'), 'success') setTimeout(() => runDetect(page), 300) } else { resultEl.innerHTML = `${t('setup.nodeNotFoundAtPath')}` } } catch (e) { resultEl.innerHTML = `${t('setup.checkFailed', { err: e })}` } }) const bindOpenclawCliPath = async (cliPath, btnEl, resultEl, successText = t('setup.searchOpenclawSelectSuccess'), originalText = btnEl?.textContent) => { if (!cliPath) return false if (btnEl) { btnEl.disabled = true btnEl.textContent = t('setup.searchOpenclawUsing') } try { const cfg = await api.readPanelConfig() cfg.openclawCliPath = cliPath await api.writePanelConfig(cfg) await api.invalidatePathCache().catch(() => {}) if (resultEl) { resultEl.style.display = 'block' resultEl.innerHTML = `✓ ${successText}` } const refreshed = await maybeRefreshGatewayServiceBinding() if (refreshed) toast(successText, 'success') else await promptRestart(successText) setTimeout(() => runDetect(page), 300) return true } catch (e) { if (btnEl) { btnEl.disabled = false btnEl.textContent = originalText || t('setup.scanUseBtn') } if (resultEl) { resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.searchOpenclawSelectFailed', { err: e?.message || e })}` } toast(t('setup.searchOpenclawSelectFailed', { err: e?.message || e }), 'error') return false } } page.querySelector('#btn-check-openclaw-path')?.addEventListener('click', async () => { const input = page.querySelector('#input-openclaw-cli-path') const resultEl = page.querySelector('#scan-openclaw-result') const btn = page.querySelector('#btn-check-openclaw-path') const cliPath = input?.value?.trim() if (!cliPath) { toast(t('setup.enterPath'), 'warning'); return } btn.disabled = true btn.textContent = t('setup.detecting2') resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.detecting2')}` try { const result = await api.checkOpenclawAtPath(cliPath) if (result?.installed && result?.path) { await bindOpenclawCliPath(result.path, btn, resultEl, t('setup.searchOpenclawManualSaved'), t('setup.searchOpenclawManualBtn')) } else { resultEl.innerHTML = `${t('setup.searchOpenclawManualNotFound')}` btn.disabled = false btn.textContent = t('setup.searchOpenclawManualBtn') } } catch (e) { resultEl.innerHTML = `${t('setup.scanFailed', { err: e })}` btn.disabled = false btn.textContent = t('setup.searchOpenclawManualBtn') } }) page.querySelector('#btn-scan-openclaw')?.addEventListener('click', async () => { const btn = page.querySelector('#btn-scan-openclaw') const resultEl = page.querySelector('#scan-openclaw-result') if (!btn || !resultEl) return btn.disabled = true btn.innerHTML = `${icon('search', 12)} ${t('setup.searchOpenclawScanning')}` resultEl.style.display = 'block' resultEl.innerHTML = `${t('setup.searchOpenclawScanning')}` try { const results = await api.scanOpenclawPaths() if (!Array.isArray(results) || results.length === 0) { resultEl.innerHTML = `${t('setup.searchOpenclawEmpty')}` return } resultEl.innerHTML = `${results.map((item, index) => `
${escapeHtml(item.path)} ${escapeHtml(openclawSourceLabel(item.source))}${item.version ? ` · v${escapeHtml(item.version)}` : ''}
`).join('')}
${t('setup.searchOpenclawHint')}
` resultEl.querySelectorAll('.btn-use-openclaw-path').forEach(btnEl => { btnEl.addEventListener('click', async () => { const item = results[Number(btnEl.dataset.index)] if (!item?.path) return await bindOpenclawCliPath(item.path, btnEl, resultEl) }) }) } catch (e) { resultEl.innerHTML = `${t('setup.scanFailed', { err: e })}` } finally { btn.disabled = false btn.innerHTML = `${icon('search', 12)} ${t('setup.searchOpenclawBtn')}` } }) // 安装方式联动:源切换时更新方式选项可见性 const methodSection = page.querySelector('#install-method-section') const registrySection = page.querySelector('#registry-section') const methodSelect = page.querySelector('#install-method') const methodHint = page.querySelector('#method-hint') const sourceRadios = page.querySelectorAll('input[name="install-source"]') const METHOD_HINTS = { 'auto': t('setup.methodHintAuto'), 'standalone-r2': t('setup.methodHintR2'), 'standalone-github': t('setup.methodHintGithub'), 'npm': t('setup.methodHintNpm'), } function updateMethodVisibility() { const source = page.querySelector('input[name="install-source"]:checked')?.value || 'chinese' if (source === 'official') { if (methodSection) methodSection.style.display = 'none' if (registrySection) registrySection.style.display = '' } else { if (methodSection) methodSection.style.display = '' const method = methodSelect?.value || 'auto' if (registrySection) registrySection.style.display = (method === 'npm') ? '' : 'none' } if (methodHint && methodSelect) methodHint.textContent = METHOD_HINTS[methodSelect.value] || '' } sourceRadios.forEach(r => r.addEventListener('change', updateMethodVisibility)) if (methodSelect) methodSelect.addEventListener('change', updateMethodVisibility) updateMethodVisibility() // 一键安装 const installBtn = page.querySelector('#btn-install') if (!installBtn || !nodeOk) return installBtn.addEventListener('click', async () => { const source = page.querySelector('input[name="install-source"]:checked')?.value || 'chinese' const method = (source === 'official') ? 'npm' : (page.querySelector('#install-method')?.value || 'auto') const registry = page.querySelector('#registry-select')?.value const modal = showUpgradeModal(t('setup.installOpenclaw')) let unlistenLog, unlistenProgress setUpgrading(true) const cleanup = () => { setUpgrading(false) unlistenLog?.() unlistenProgress?.() unlistenDone?.() unlistenError?.() } let 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)) // 后台任务完成:继续安装 Gateway + 自动配置 unlistenDone = await listen('upgrade-done', async (e) => { cleanup() modal.setDone(typeof e.payload === 'string' ? e.payload : t('setup.installComplete')) // 安装成功后自动安装 Gateway modal.appendLog(t('setup.installingGateway')) try { await api.installGateway() modal.appendHtmlLog(`${statusIcon('ok', 14)} ${t('setup.gatewayInstalled')}`) } catch (ge) { modal.appendHtmlLog(`${statusIcon('warn', 14)} ${t('setup.gatewayInstallFailed', { err: ge })}`) } // 确保 openclaw.json 有关键默认值 try { const config = await api.readOpenclawConfig() if (config) { let patched = false if (!config.gateway) config.gateway = {} if (!config.gateway.mode) { config.gateway.mode = 'local' patched = true modal.appendHtmlLog(`${statusIcon('ok', 14)} ${t('setup.gwModeSet')}`) } if (!config.tools || config.tools.profile !== 'full') { config.tools = { profile: 'full', sessions: { visibility: 'all' }, ...(config.tools || {}) } config.tools.profile = 'full' if (!config.tools.sessions) config.tools.sessions = {} config.tools.sessions.visibility = 'all' patched = true modal.appendHtmlLog(`${statusIcon('ok', 14)} ${t('setup.toolsFullEnabled')}`) } if (patched) await api.writeOpenclawConfig(config) } } catch (ce) { modal.appendHtmlLog(`${statusIcon('warn', 14)} ${t('setup.autoConfigFailed', { err: ce })}`) } toast(t('setup.installSuccess'), 'success') setTimeout(() => window.location.reload(), 1500) }) // 后台任务失败 unlistenError = await listen('upgrade-error', async (e) => { cleanup() const errStr = String(e.payload || t('common.unknown')) modal.appendLog(errStr) await new Promise(r => setTimeout(r, 150)) 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: t('setup.installScene'), hint: diagnosis.hint }) } }) // 先设置镜像源 if (registry) { modal.appendLog(t('setup.setRegistry', { url: registry })) try { await api.setNpmRegistry(registry) } catch {} } // 发起后台任务(立即返回) await api.upgradeOpenclaw(source, null, method) modal.appendLog(t('setup.bgTaskStarted')) } else { // Web 模式:同步等待 modal.appendLog(t('setup.webModeLogHint')) if (registry) { modal.appendLog(t('setup.setRegistry', { url: registry })) try { await api.setNpmRegistry(registry) } catch {} } const msg = await api.upgradeOpenclaw(source, null, method) modal.setDone(msg) toast(t('setup.installSuccess'), 'success') setTimeout(() => window.location.reload(), 1500) cleanup() } } catch (e) { cleanup() const errStr = String(e) modal.appendLog(errStr) const fullLog = modal.getLogText() + '\n' + errStr const diagnosis = diagnoseInstallError(fullLog) modal.setError(diagnosis.title) } }) }