/**
* 初始设置页面 — 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
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 += `
下一步建议
当前仅表示运行环境已经就绪,并不代表已经可以直接聊天。通常还需要继续完成以下步骤:
- 前往「模型配置」添加至少一个可用模型,并确认主模型已设置
- 前往「Gateway」确认服务已启动
- 如需飞书、钉钉、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 (Windows 子系统) — OpenClaw 装在 WSL 里,Windows 侧无法访问
- Docker 容器 — 容器内的安装与宿主机隔离
` : ''}
${isMac ? `
- Docker 容器 — 容器内的安装与宿主机隔离
- 远程服务器 — 安装在其他机器上
` : ''}
${!isWin && !isMac ? `
- Docker 容器 — 容器内的安装与宿主机隔离
` : ''}
在对应环境中安装管理面板
${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 --install 或 brew install git
Linux: sudo apt install git 或 sudo 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)
}
})
}