Files
clawpanel/src/pages/setup.js
2026-03-21 04:11:13 +08:00

741 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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, 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 = `
<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>
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
`
// 清除缓存,确保拿到最新检测结果
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 `<span style="color:${color};font-weight:700;width:18px;display:inline-block">${ok ? '✓' : '✗'}</span>`
}
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 += `
<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 后点击「重新检测」或使用下方「自动扫描」,无需重启。`
}
<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">${icon('search', 12)} 自动扫描</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>
`
// 第二步Git
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(gitOk)} Git 版本管理
</div>
${gitOk
? `<p style="color:var(--success);font-size:var(--font-size-sm)">已安装 ${git.version || ''}</p>
<p style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-top:4px">✅ 已自动配置 Git 使用 HTTPS避免 SSH 连接问题)</p>`
: `<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);line-height:1.5">
部分依赖需要 Git 下载源码。点击下方按钮自动安装,如果失败请手动安装。
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" id="btn-auto-install-git">一键安装 Git</button>
<a class="btn btn-secondary btn-sm" href="https://git-scm.com/downloads" target="_blank" rel="noopener">手动下载</a>
</div>
<div id="git-install-result" style="margin-top:var(--space-sm);display:none"></div>
<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary);line-height:1.5">
<strong>没有 Git 也能安装?</strong> 大部分情况下可以,但个别依赖可能需要 Git。建议安装以避免问题。
</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>
${version?.ahead_of_recommended && version?.recommended
? `<div style="margin-top:8px;padding:8px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:var(--font-size-xs);color:var(--warning,#f59e0b);line-height:1.6">
检测到当前本地 OpenClaw ${version.current || ''} 高于当前面板推荐稳定版 ${version.recommended},可能存在兼容或稳定性风险。建议稍后到「关于」页回退到推荐版。
</div>`
: ''}`
: 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>`
}
<details style="margin-top:var(--space-sm);cursor:pointer" id="custom-dir-details">
<summary style="font-size:var(--font-size-xs);color:var(--text-secondary);font-weight:600;user-select:none">
自定义 OpenClaw 安装路径
</summary>
<div style="margin-top:var(--space-sm);padding:10px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:var(--font-size-xs);line-height:1.6">
<p style="color:var(--text-secondary);margin-bottom:8px">
如果 OpenClaw 安装在非默认目录(如 <code>E:\\数据\\AI\\.openclaw</code>),可在此指定。留空则使用默认路径。
</p>
<div style="display:flex;gap:6px">
<input id="input-openclaw-dir" type="text" placeholder="例如 E:\\数据\\AI\\.openclaw"
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-save-openclaw-dir" style="font-size:11px;padding:3px 10px">保存</button>
<button class="btn btn-secondary btn-sm" id="btn-reset-openclaw-dir" style="font-size:11px;padding:3px 10px">恢复默认</button>
</div>
<div id="openclaw-dir-result" style="margin-top:6px;display:none"></div>
</div>
</details>
</div>
`
// AI 助手入口
html += `
<div class="config-section" style="text-align:left;margin-top:var(--space-md)">
<div class="config-section-title" style="display:flex;align-items:center;gap:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/></svg>
晴辰助手
</div>
<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);line-height:1.5">
遇到安装问题AI 助手可以帮你诊断和解决。配置好模型后,点击下方按钮${!allOk ? ',当前问题会自动发送给 AI 分析' : ''}
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" id="btn-goto-assistant">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/></svg>
打开 AI 助手
</button>
${!allOk ? `<button class="btn btn-primary btn-sm" id="btn-ask-ai-help">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
让 AI 帮我解决
</button>` : ''}
</div>
</div>
`
// 全部就绪 → 进入面板
if (allOk) {
html += `
<div class="config-section" style="text-align:left;margin-top:var(--space-md)">
<div class="config-section-title">下一步建议</div>
<div style="color:var(--text-secondary);font-size:var(--font-size-sm);line-height:1.7">
当前仅表示运行环境已经就绪,并不代表已经可以直接聊天。通常还需要继续完成以下步骤:
<ol style="margin:8px 0 0 18px;padding:0">
<li>前往「模型配置」添加至少一个可用模型,并确认主模型已设置</li>
<li>前往「Gateway」确认服务已启动</li>
<li>如需飞书、钉钉、QQ 等消息渠道,请到「消息渠道」完成接入与配对</li>
</ol>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
<button class="btn btn-secondary btn-sm" id="btn-goto-models">配置模型</button>
<button class="btn btn-secondary btn-sm" id="btn-goto-gateway">Gateway 设置</button>
<button class="btn btn-secondary btn-sm" id="btn-goto-channels">消息渠道</button>
</div>
</div>
<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, { 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 = `
<div style="margin-top:var(--space-sm);padding:10px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);border-left:3px solid var(--warning);font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.7">
<strong style="color:var(--text-primary)">找不到已安装的 OpenClaw</strong>
<p style="margin:6px 0 2px">ClawPanel 桌面版只能管理<strong>本机</strong>安装的 OpenClaw。以下环境中的安装无法被检测到</p>
<ul style="margin:4px 0 8px 16px;padding:0">
${isWin ? `
<li><strong>WSL (Windows 子系统)</strong> — OpenClaw 装在 WSL 里Windows 侧无法访问</li>
<li><strong>Docker 容器</strong> — 容器内的安装与宿主机隔离</li>
` : ''}
${isMac ? `
<li><strong>Docker 容器</strong> — 容器内的安装与宿主机隔离</li>
<li><strong>远程服务器</strong> — 安装在其他机器上</li>
` : ''}
${!isWin && !isMac ? `
<li><strong>Docker 容器</strong> — 容器内的安装与宿主机隔离</li>
` : ''}
</ul>
<details style="cursor:pointer">
<summary style="font-weight:600;color:var(--primary);margin-bottom:6px">
在对应环境中安装管理面板
</summary>
<div style="margin-top:8px">
${isWin ? `
<div style="margin-bottom:10px">
<div style="font-weight:600;margin-bottom:4px">WSL 中使用 Web 版:</div>
<div style="margin-bottom:2px;opacity:0.8">打开 WSL 终端,一键部署 ClawPanel Web 版:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
<div style="margin-top:4px;opacity:0.7">国内用户如无法访问 GitHub<code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px;user-select:all">curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash</code></div>
<div style="margin-top:4px;opacity:0.7">部署后在浏览器访问 WSL 的 IP 即可管理。</div>
</div>
` : ''}
<div style="margin-bottom:10px">
<div style="font-weight:600;margin-bottom:4px">Docker 容器中使用:</div>
<div style="margin-bottom:2px;opacity:0.8">在容器内安装 OpenClaw + ClawPanel Web 版:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all;margin-bottom:4px">npm i -g @qingchencloud/openclaw-zh</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
<div style="margin-top:4px;opacity:0.7">国内镜像:<code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px;user-select:all">curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash</code></div>
</div>
<div>
<div style="font-weight:600;margin-bottom:4px">远程服务器:</div>
<div style="margin-bottom:2px;opacity:0.8">SSH 登录服务器后执行:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
<div style="margin-top:4px;opacity:0.7">国内镜像:<code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px;user-select:all">curl -fsSL https://gitee.com/QtCodeCreators/clawpanel/raw/main/deploy.sh | bash</code></div>
</div>
</div>
</details>
<div style="margin-top:6px;opacity:0.7">
或者,你也可以在本机重新安装 OpenClaw使用下方的「一键安装」
</div>
</div>`
}
return `
<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
点击安装后,将默认安装当前 ClawPanel 版本绑定的推荐稳定版;如需升降级,可稍后到「关于」页面切换版本。
</p>
<p style="color:var(--text-tertiary);font-size:var(--font-size-xs);line-height:1.6;margin:-4px 0 var(--space-sm)">
如果你是为了体验最新版功能,建议先安装推荐稳定版再手动切换;若希望面板优先适配最新版,欢迎提交 issue。
</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)" id="install-method-section">
<label style="font-size:var(--font-size-xs);color:var(--text-tertiary);display:block;margin-bottom:4px">安装方式</label>
<select id="install-method" 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="auto">自动选择(推荐)</option>
<option value="standalone-r2">独立安装包 · CDN 加速(国内推荐,自带 Node.js无需 npm</option>
<option value="standalone-github">独立安装包 · GitHubCDN 不可用时备选)</option>
<option value="npm">npm 编译安装(传统方式,需要 Node.js + npm + 网络)</option>
</select>
<div id="method-hint" style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-top:4px;line-height:1.5"></div>
</div>
<div style="margin-bottom:var(--space-sm)" id="registry-section">
<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>
${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 = '<span style="color:var(--text-tertiary)">正在安装 Git请稍候...</span>'
}
try {
const msg = await api.autoInstallGit()
if (resultEl) resultEl.innerHTML = `<span style="color:var(--success)">✓ ${msg}</span>`
toast('Git 安装成功', 'success')
// 安装成功后自动配置 HTTPS
api.configureGitHttps().catch(() => {})
setTimeout(() => runDetect(page), 1000)
} catch (e) {
const errMsg = String(e.message || e)
if (resultEl) {
resultEl.innerHTML = `<div>
<span style="color:var(--danger)">自动安装失败: ${errMsg}</span>
<p style="margin-top:6px;font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.5">
请手动安装 Git<br>
<strong>Windows:</strong> 下载 <a href="https://git-scm.com/downloads" target="_blank" style="color:var(--accent)">git-scm.com</a> 安装包<br>
<strong>macOS:</strong> 在终端执行 <code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px">xcode-select --install</code> 或 <code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px">brew install git</code><br>
<strong>Linux:</strong> <code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px">sudo apt install git</code> 或 <code style="background:var(--bg-secondary);padding:2px 4px;border-radius:3px">sudo yum install git</code>
</p>
</div>`
}
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 = '<span style="color:var(--text-tertiary)">保存中...</span>' }
try {
const cfg = await api.readPanelConfig()
cfg.openclawDir = value
await api.writePanelConfig(cfg)
invalidate()
if (dirResultEl) dirResultEl.innerHTML = `<span style="color:var(--success)">✓ 路径已保存,正在重新检测...</span>`
toast('自定义路径已保存', 'success')
setTimeout(() => runDetect(page), 500)
} catch (e) {
if (dirResultEl) dirResultEl.innerHTML = `<span style="color:var(--error)">保存失败: ${e}</span>`
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 = `<span style="color:var(--success)">✓ 已恢复默认路径,正在重新检测...</span>` }
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 = '<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(() => runDetect(page), 300)
})
})
}
} catch (e) {
resultEl.innerHTML = `<span style="color:var(--danger)">扫描失败: ${e}</span>`
} 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 = '<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(() => runDetect(page), 300)
} else {
resultEl.innerHTML = `<span style="color:var(--warning)">该目录下未找到 node 可执行文件,请确认路径正确。</span>`
}
} catch (e) {
resultEl.innerHTML = `<span style="color:var(--danger)">检测失败: ${e}</span>`
}
})
// 安装方式联动:源切换时更新方式选项可见性
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)
}
})
}