/** * 初始设置页面 — openclaw 未安装时的引导 * 自动检测环境 → 版本选择 → 一键安装 → 自动跳转 */ import { api, invalidate } from '../lib/tauri-api.js' import { showUpgradeModal } from '../components/modal.js' import { toast } from '../components/toast.js' import { setUpgrading, isMacPlatform } from '../lib/app-state.js' import { diagnoseInstallError } from '../lib/error-diagnosis.js' import { icon, statusIcon } from '../lib/icons.js' export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `
ClawPanel

欢迎使用 ClawPanel

OpenClaw AI Agent 框架的桌面管理面板

` page.querySelector('#btn-recheck').addEventListener('click', () => runDetect(page)) runDetect(page) return page } async function runDetect(page) { const stepsEl = page.querySelector('#setup-steps') stepsEl.innerHTML = `
` // 清除缓存,确保拿到最新检测结果 invalidate('get_version_info', 'check_node', 'check_git', 'get_services_status', 'check_installation') // 并行检测 Node.js、Git、OpenClaw CLI、配置文件 const [nodeRes, gitRes, clawRes, configRes, versionRes] = await Promise.allSettled([ api.checkNode(), api.checkGit(), api.getServicesStatus(), api.checkInstallation(), api.getVersionInfo(), ]) 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 } const version = versionRes.status === 'fulfilled' ? versionRes.value : null // CLI 已装但配置缺失 → 自动创建默认配置 if (cliOk && !config.installed) { try { const initResult = await api.initOpenclawConfig() if (initResult?.created) { config = await api.checkInstallation() } } catch (e) { console.warn('[setup] 自动初始化配置失败:', e) } } // Git 已安装时,自动配置 HTTPS 替代 SSH(静默执行) if (git.installed) { api.configureGitHttps().catch(() => {}) } renderSteps(page, { node, git, cliOk, config, version }) } 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 const gitOk = git?.installed || false const allOk = nodeOk && cliOk && config.installed let html = '' // 第一步:Node.js html += `
${stepIcon(nodeOk)} Node.js 环境
${nodeOk ? `

已安装 ${node.version || ''}

` : `

OpenClaw 基于 Node.js 运行,请先安装。

下载 Node.js 安装后点击「重新检测」
已经装了但检测不到? ${isMacPlatform() ? `macOS 上从 Finder 启动可能找不到 Node.js。试试关掉 ClawPanel 后从终端启动:
open /Applications/ClawPanel.app` : `安装 Node.js 后点击「重新检测」或使用下方「自动扫描」,无需重启。` }
或手动指定路径:
` }
` // 第二步:Git html += `
${stepIcon(gitOk)} Git 版本管理
${gitOk ? `

已安装 ${git.version || ''}

✅ 已自动配置 Git 使用 HTTPS(避免 SSH 连接问题)

` : `

部分依赖需要 Git 下载源码。点击下方按钮自动安装,如果失败请手动安装。

手动下载
没有 Git 也能安装? 大部分情况下可以,但个别依赖可能需要 Git。建议安装以避免问题。
` }
` // 第三步:OpenClaw CLI html += `
${stepIcon(cliOk)} OpenClaw CLI
${cliOk ? `

CLI 可用

${version?.ahead_of_recommended && version?.recommended ? `
检测到当前本地 OpenClaw ${version.current || ''} 高于当前面板推荐稳定版 ${version.recommended},可能存在兼容或稳定性风险。建议稍后到「关于」页回退到推荐版。
` : ''}` : renderInstallSection() }
` // 第四步:配置文件 + 自定义路径 html += `
${stepIcon(config.installed)} 配置文件
${config.installed ? `

配置文件位于 ${config.path || ''}

` : `

配置文件不存在,点击下方按钮自动创建默认配置。

` }
自定义 OpenClaw 安装路径

如果 OpenClaw 安装在非默认目录(如 E:\\数据\\AI\\.openclaw),可在此指定。留空则使用默认路径。

` // AI 助手入口 html += `
晴辰助手

遇到安装问题?AI 助手可以帮你诊断和解决。配置好模型后,点击下方按钮${!allOk ? ',当前问题会自动发送给 AI 分析' : ''}。

${!allOk ? `` : ''}
` // 全部就绪 → 进入面板 if (allOk) { html += `
下一步建议
当前仅表示运行环境已经就绪,并不代表已经可以直接聊天。通常还需要继续完成以下步骤:
  1. 前往「模型配置」添加至少一个可用模型,并确认主模型已设置
  2. 前往「Gateway」确认服务已启动
  3. 如需飞书、钉钉、QQ 等消息渠道,请到「消息渠道」完成接入与配对
` } stepsEl.innerHTML = html bindEvents(page, nodeOk, { node, git, cliOk, config }) } function renderInstallSection() { 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__ let envHint = '' if (isDesktop) { envHint = `
找不到已安装的 OpenClaw?

ClawPanel 桌面版只能管理本机安装的 OpenClaw。以下环境中的安装无法被检测到:

在对应环境中安装管理面板
${isWin ? `
WSL 中使用 Web 版:
打开 WSL 终端,一键部署 ClawPanel Web 版:
curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
国内用户如无法访问 GitHub:curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash
部署后在浏览器访问 WSL 的 IP 即可管理。
` : ''}
Docker 容器中使用:
在容器内安装 OpenClaw + ClawPanel Web 版:
npm i -g @qingchencloud/openclaw-zh curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
国内镜像:curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash
远程服务器:
SSH 登录服务器后执行:
curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
国内镜像:curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash
或者,你也可以在本机重新安装 OpenClaw(使用下方的「一键安装」)。
` } return `

点击安装后,将默认安装当前 ClawPanel 版本绑定的推荐稳定版;如需升降级,可稍后到「关于」页面切换版本。

如果你是为了体验最新版功能,建议先安装推荐稳定版再手动切换;若希望面板优先适配最新版,欢迎提交 issue。

${envHint} ` } function buildSetupProblemPrompt({ node, git, cliOk, config }) { const problems = [] if (!node.installed) problems.push('- Node.js 未安装或未检测到') else problems.push(`- Node.js 已安装: ${node.version || '版本未知'}`) if (!git?.installed) problems.push('- Git 未安装') else problems.push(`- Git 已安装: ${git.version || '版本未知'}`) if (!cliOk) problems.push('- OpenClaw CLI 未安装') else problems.push('- OpenClaw CLI 已安装') if (!config.installed) problems.push('- 配置文件不存在') else problems.push(`- 配置文件正常: ${config.path || ''}`) return `我在安装 OpenClaw 时遇到问题,以下是当前检测状态: ${problems.join('\n')} 请帮我分析问题并给出解决步骤。如果需要,请使用工具帮我检查系统环境。` } 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' }) // 进入面板 page.querySelector('#btn-enter')?.addEventListener('click', () => { window.location.hash = '/dashboard' }) page.querySelector('#btn-goto-models')?.addEventListener('click', () => { window.location.hash = '/models' }) page.querySelector('#btn-goto-gateway')?.addEventListener('click', () => { window.location.hash = '/gateway' }) page.querySelector('#btn-goto-channels')?.addEventListener('click', () => { window.location.hash = '/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 = '安装中...' if (resultEl) { resultEl.style.display = 'block' resultEl.innerHTML = '正在安装 Git,请稍候...' } try { const msg = await api.autoInstallGit() if (resultEl) resultEl.innerHTML = `✓ ${msg}` toast('Git 安装成功', 'success') // 安装成功后自动配置 HTTPS api.configureGitHttps().catch(() => {}) setTimeout(() => runDetect(page), 1000) } catch (e) { const errMsg = String(e.message || e) if (resultEl) { resultEl.innerHTML = `
自动安装失败: ${errMsg}

请手动安装 Git:
Windows: 下载 git-scm.com 安装包
macOS: 在终端执行 xcode-select --installbrew install git
Linux: sudo apt install gitsudo yum install git

` } toast('Git 自动安装失败,请手动安装', 'warning') } finally { btn.disabled = false btn.textContent = '一键安装 Git' } }) // 自定义 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(() => {}) } page.querySelector('#btn-save-openclaw-dir')?.addEventListener('click', async () => { const value = dirInput?.value?.trim() if (!value) { toast('请输入路径', 'warning'); return } const btn = page.querySelector('#btn-save-openclaw-dir') btn.disabled = true if (dirResultEl) { dirResultEl.style.display = 'block'; dirResultEl.innerHTML = '保存中...' } try { const cfg = await api.readPanelConfig() cfg.openclawDir = value await api.writePanelConfig(cfg) invalidate() if (dirResultEl) dirResultEl.innerHTML = `✓ 路径已保存,正在重新检测...` toast('自定义路径已保存', 'success') setTimeout(() => runDetect(page), 500) } catch (e) { if (dirResultEl) dirResultEl.innerHTML = `保存失败: ${e}` toast('保存失败: ' + 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 = `✓ 已恢复默认路径,正在重新检测...` } toast('已恢复默认路径', 'success') setTimeout(() => runDetect(page), 500) } catch (e) { toast('恢复失败: ' + 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 = '初始化中...' try { const result = await api.initOpenclawConfig() if (result?.created) { toast('配置文件已创建', 'success') } else { toast(result?.message || '配置文件已存在', 'info') } setTimeout(() => runDetect(page), 500) } catch (e) { toast('初始化失败: ' + e, 'error') btn.disabled = false btn.textContent = '一键初始化配置' } }) // 自动扫描 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 = '扫描中...' resultEl.style.display = 'block' resultEl.innerHTML = '正在扫描常见安装路径...' try { const results = await api.scanNodePaths() if (results.length === 0) { resultEl.innerHTML = '未找到 Node.js 安装,请手动指定路径或下载安装。' } else { resultEl.innerHTML = results.map(r => `
${r.path} ${r.version}
` ).join('') resultEl.querySelectorAll('.btn-use-path').forEach(b => { b.addEventListener('click', async () => { await api.saveCustomNodePath(b.dataset.path) toast('Node.js 路径已保存,正在重新检测...', 'success') setTimeout(() => runDetect(page), 300) }) }) } } catch (e) { resultEl.innerHTML = `扫描失败: ${e}` } finally { btn.disabled = false btn.innerHTML = `${icon('search', 12)} 自动扫描` } }) // 手动指定路径检测 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('请输入 Node.js 安装目录', 'warning'); return } resultEl.style.display = 'block' resultEl.innerHTML = '检测中...' try { const result = await api.checkNodeAtPath(dir) if (result.installed) { await api.saveCustomNodePath(dir) resultEl.innerHTML = `✓ 找到 Node.js ${result.version},路径已保存` toast('Node.js 路径已保存,正在重新检测...', 'success') setTimeout(() => runDetect(page), 300) } else { resultEl.innerHTML = `该目录下未找到 node 可执行文件,请确认路径正确。` } } catch (e) { resultEl.innerHTML = `检测失败: ${e}` } }) // 安装方式联动:源切换时更新方式选项可见性 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': '自动选择最优安装方式:优先使用独立安装包(零依赖、最快),失败时自动降级到 npm 编译安装。', 'standalone-r2': '从晴辰云 CDN 下载独立安装包,自带 Node.js 运行时,无需 npm。国内下载速度最快。', 'standalone-github': '从 GitHub Releases 下载独立安装包。CDN 不可用时的备选方案。', 'npm': '传统的 npm install 方式,需要本机已安装 Node.js 和 npm,且网络能访问 npm 仓库。', } 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('安装 OpenClaw') 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 : '安装完成') // 安装成功后自动安装 Gateway modal.appendLog('正在安装 Gateway 服务...') try { await api.installGateway() modal.appendHtmlLog(`${statusIcon('ok', 14)} Gateway 服务已安装`) } catch (ge) { modal.appendHtmlLog(`${statusIcon('warn', 14)} Gateway 安装失败: ${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)} 已设置 Gateway 运行模式为 local`) } 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)} 已开启 Agent 工具全部权限`) } if (patched) await api.writeOpenclawConfig(config) } } catch (ce) { modal.appendHtmlLog(`${statusIcon('warn', 14)} 自动配置失败: ${ce}`) } toast('OpenClaw 安装成功', 'success') setTimeout(() => window.location.reload(), 1500) }) // 后台任务失败 unlistenError = await listen('upgrade-error', async (e) => { cleanup() const errStr = String(e.payload || '未知错误') 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: '初始安装 OpenClaw', hint: diagnosis.hint }) } }) // 先设置镜像源 if (registry) { modal.appendLog(`设置 npm 镜像源: ${registry}`) try { await api.setNpmRegistry(registry) } catch {} } // 发起后台任务(立即返回) await api.upgradeOpenclaw(source, null, method) modal.appendLog('后台安装任务已启动,请等待完成...') } else { // Web 模式:同步等待 modal.appendLog('Web 模式:安装日志不可用,请等待完成...') if (registry) { modal.appendLog(`设置 npm 镜像源: ${registry}`) try { await api.setNpmRegistry(registry) } catch {} } const msg = await api.upgradeOpenclaw(source, null, method) modal.setDone(msg) toast('OpenClaw 安装成功', '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) } }) }