Files
clawpanel/src/pages/setup.js
晴天 bb2c6df8ec 优化:初始设置页面自动创建默认配置文件
当检测到 OpenClaw CLI 已安装但 openclaw.json 不存在时:
1. 自动创建包含合理默认值的配置文件(mode:local, tools:full 等)
2. 如果自动创建失败,显示「一键初始化配置」按钮供手动操作
3. 新增 init_openclaw_config API(dev-api.js + Tauri Rust 后端)
4. 用户不再需要去终端手动执行 openclaw configure
2026-03-05 23:14:13 +08:00

366 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 初始设置页面 — openclaw 未安装时的引导
* 自动检测环境 → 版本选择 → 一键安装 → 自动跳转
*/
import { api } 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'
export async function render() {
const page = document.createElement('div')
page.className = 'page'
page.innerHTML = `
<div style="max-width:560px;margin:48px auto;text-align:center">
<div style="margin-bottom:var(--space-lg)">
<img src="/images/logo-brand.png" alt="ClawPanel" style="max-width:160px;width:100%;height:auto">
</div>
<h1 style="font-size:var(--font-size-xl);margin-bottom:var(--space-xs)">欢迎使用 ClawPanel</h1>
<p style="color:var(--text-secondary);margin-bottom:var(--space-xl);line-height:1.6">
OpenClaw AI Agent 框架的桌面管理面板
</p>
<div id="setup-steps"></div>
<div style="margin-top:var(--space-lg)">
<button class="btn btn-secondary btn-sm" id="btn-recheck" style="min-width:120px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
重新检测
</button>
</div>
</div>
`
page.querySelector('#btn-recheck').addEventListener('click', () => runDetect(page))
runDetect(page)
return page
}
async function runDetect(page) {
const stepsEl = page.querySelector('#setup-steps')
stepsEl.innerHTML = `
<div class="stat-card loading-placeholder" style="height:48px"></div>
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
`
// 并行检测 Node.js、OpenClaw CLI、配置文件
const [nodeRes, clawRes, configRes] = await Promise.allSettled([
api.checkNode(),
api.getServicesStatus(),
api.checkInstallation(),
])
const node = nodeRes.status === 'fulfilled' ? nodeRes.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 }
// CLI 已装但配置缺失 → 自动创建默认配置
if (cliOk && !config.installed) {
try {
const initResult = await api.initOpenclawConfig()
if (initResult?.created) {
// 重新检测配置
config = await api.checkInstallation()
}
} catch (e) {
console.warn('[setup] 自动初始化配置失败:', e)
}
}
renderSteps(page, { node, cliOk, config })
}
function stepIcon(ok) {
const color = ok ? 'var(--success)' : 'var(--text-tertiary)'
return `<span style="color:${color};font-weight:700;width:18px;display:inline-block">${ok ? '✓' : '✗'}</span>`
}
function renderSteps(page, { node, cliOk, config }) {
const stepsEl = page.querySelector('#setup-steps')
const nodeOk = node.installed
const allOk = nodeOk && cliOk && config.installed
let html = ''
// 第一步Node.js
html += `
<div class="config-section" style="text-align:left">
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
${stepIcon(nodeOk)} Node.js 环境
</div>
${nodeOk
? `<p style="color:var(--success);font-size:var(--font-size-sm)">已安装 ${node.version || ''}</p>`
: `<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
OpenClaw 基于 Node.js 运行,请先安装。
</p>
<a class="btn btn-primary btn-sm" href="https://nodejs.org/" target="_blank" rel="noopener">下载 Node.js</a>
<span class="form-hint" style="margin-left:8px">安装后点击「重新检测」</span>
<div style="margin-top:var(--space-sm);padding:8px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.6">
<strong>已经装了但检测不到?</strong>
${isMacPlatform()
? `macOS 上从 Finder 启动可能找不到 Node.js。试试关掉 ClawPanel 后从终端启动:<br>
<code style="background:var(--bg-secondary);padding:2px 6px;border-radius:3px;user-select:all">open /Applications/ClawPanel.app</code>`
: `安装 Node.js 后需要<strong>重启 ClawPanel</strong>,新的环境变量才能生效。`
}
<div style="margin-top:8px;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">🔍 自动扫描</button>
<span style="color:var(--text-tertiary)">或手动指定路径:</span>
</div>
<div style="margin-top:6px;display:flex;gap:6px">
<input id="input-node-path" type="text" placeholder="${isMacPlatform() ? '/usr/local/bin' : 'F:\\\\AI\\\\Node'}"
style="flex:1;padding:4px 8px;border:1px solid var(--border-primary);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:11px;font-family:monospace">
<button class="btn btn-primary btn-sm" id="btn-check-path" style="font-size:11px;padding:3px 10px">检测</button>
</div>
<div id="scan-result" style="margin-top:6px;display:none"></div>
</div>`
}
</div>
`
// 第二步OpenClaw CLI
html += `
<div class="config-section" style="text-align:left;${nodeOk ? '' : 'opacity:0.4;pointer-events:none'}">
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
${stepIcon(cliOk)} OpenClaw CLI
</div>
${cliOk
? `<p style="color:var(--success);font-size:var(--font-size-sm)">CLI 可用</p>`
: renderInstallSection()
}
</div>
`
// 第三步:配置文件
html += `
<div class="config-section" style="text-align:left;${cliOk ? '' : 'opacity:0.4;pointer-events:none'}">
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
${stepIcon(config.installed)} 配置文件
</div>
${config.installed
? `<p style="color:var(--success);font-size:var(--font-size-sm)">配置文件位于 ${config.path || ''}</p>`
: `<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
配置文件不存在,点击下方按钮自动创建默认配置。
</p>
<button class="btn btn-primary btn-sm" id="btn-init-config">一键初始化配置</button>`
}
</div>
`
// 全部就绪 → 进入面板
if (allOk) {
html += `
<div style="margin-top:var(--space-lg)">
<button class="btn btn-primary" id="btn-enter" style="min-width:200px">进入面板</button>
</div>
`
}
stepsEl.innerHTML = html
bindEvents(page, nodeOk)
}
function renderInstallSection() {
return `
<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
选择版本后点击安装,将自动执行 npm 全局安装。
</p>
<div style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-sm)">
<label class="setup-source-option" style="flex:1;cursor:pointer">
<input type="radio" name="install-source" value="chinese" checked style="margin-right:6px">
<div>
<div style="font-weight:600;font-size:var(--font-size-sm)">汉化优化版(推荐)</div>
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary)">@qingchencloud/openclaw-zh</div>
</div>
</label>
<label class="setup-source-option" style="flex:1;cursor:pointer">
<input type="radio" name="install-source" value="official" style="margin-right:6px">
<div>
<div style="font-weight:600;font-size:var(--font-size-sm)">官方原版</div>
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary)">openclaw</div>
</div>
</label>
</div>
<div style="margin-bottom:var(--space-sm)">
<label style="font-size:var(--font-size-xs);color:var(--text-tertiary);display:block;margin-bottom:4px">npm 镜像源</label>
<select id="registry-select" style="width:100%;padding:6px 8px;border-radius:var(--radius-sm);border:1px solid var(--border-primary);background:var(--bg-secondary);color:var(--text-primary);font-size:var(--font-size-sm)">
<option value="https://registry.npmmirror.com">淘宝镜像(推荐国内用户)</option>
<option value="https://registry.npmjs.org">npm 官方源</option>
<option value="https://repo.huaweicloud.com/repository/npm/">华为云镜像</option>
</select>
</div>
<button class="btn btn-primary btn-sm" id="btn-install">一键安装</button>
`
}
function bindEvents(page, nodeOk) {
// 进入面板
page.querySelector('#btn-enter')?.addEventListener('click', () => {
window.location.hash = '/dashboard'
})
// 一键初始化配置
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 = '<span style="color:var(--text-tertiary)">正在扫描常见安装路径...</span>'
try {
const results = await api.scanNodePaths()
if (results.length === 0) {
resultEl.innerHTML = '<span style="color:var(--warning)">未找到 Node.js 安装,请手动指定路径或下载安装。</span>'
} else {
resultEl.innerHTML = results.map(r =>
`<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
<span style="color:var(--success)">✓</span>
<code style="flex:1;background:var(--bg-secondary);padding:2px 6px;border-radius:3px;font-size:11px">${r.path}</code>
<span style="font-size:11px;color:var(--text-tertiary)">${r.version}</span>
<button class="btn btn-primary btn-sm btn-use-path" data-path="${r.path}" style="font-size:10px;padding:2px 8px">使用</button>
</div>`
).join('')
resultEl.querySelectorAll('.btn-use-path').forEach(b => {
b.addEventListener('click', async () => {
await api.saveCustomNodePath(b.dataset.path)
toast('Node.js 路径已保存,正在重新检测...', 'success')
setTimeout(() => window.location.reload(), 500)
})
})
}
} catch (e) {
resultEl.innerHTML = `<span style="color:var(--danger)">扫描失败: ${e}</span>`
} finally {
btn.disabled = false
btn.textContent = '🔍 自动扫描'
}
})
// 手动指定路径检测
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 = '<span style="color:var(--text-tertiary)">检测中...</span>'
try {
const result = await api.checkNodeAtPath(dir)
if (result.installed) {
await api.saveCustomNodePath(dir)
resultEl.innerHTML = `<span style="color:var(--success)">✓ 找到 Node.js ${result.version},路径已保存</span>`
toast('Node.js 路径已保存,正在重新检测...', 'success')
setTimeout(() => window.location.reload(), 500)
} else {
resultEl.innerHTML = `<span style="color:var(--warning)">该目录下未找到 node 可执行文件,请确认路径正确。</span>`
}
} catch (e) {
resultEl.innerHTML = `<span style="color:var(--danger)">检测失败: ${e}</span>`
}
})
// 一键安装
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 registry = page.querySelector('#registry-select')?.value
const modal = showUpgradeModal()
let unlistenLog, unlistenProgress
setUpgrading(true)
try {
if (window.__TAURI_INTERNALS__) {
try {
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))
} catch { /* Web 模式无 Tauri event */ }
} else {
modal.appendLog('Web 模式:安装日志不可用,请等待完成...')
}
// 先设置镜像源
if (registry) {
modal.appendLog(`设置 npm 镜像源: ${registry}`)
try { await api.setNpmRegistry(registry) } catch {}
}
const msg = await api.upgradeOpenclaw(source)
modal.setDone(msg)
// 安装成功后自动安装 Gateway
modal.appendLog('正在安装 Gateway 服务...')
try {
await api.installGateway()
modal.appendLog('✅ Gateway 服务已安装')
} catch (e) {
modal.appendLog('⚠️ Gateway 安装失败: ' + e)
}
// 确保 openclaw.json 有关键默认值,否则 Gateway 启动不了或功能受限
try {
const config = await api.readOpenclawConfig()
if (config) {
let patched = false
if (!config.mode) {
config.mode = 'local'
patched = true
modal.appendLog('✅ 已设置 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.appendLog('✅ 已开启 Agent 工具全部权限')
}
if (patched) await api.writeOpenclawConfig(config)
}
} catch (e) {
modal.appendLog('⚠️ 自动配置失败: ' + e)
}
toast('OpenClaw 安装成功', 'success')
setTimeout(() => window.location.reload(), 1500)
} catch (e) {
const errStr = String(e)
modal.appendLog(errStr)
const diagnosis = diagnoseInstallError(errStr)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendLog(' ' + diagnosis.hint)
if (diagnosis.command) modal.appendLog('💻 ' + diagnosis.command)
} finally {
setUpgrading(false)
unlistenLog?.()
unlistenProgress?.()
}
})
}