feat: standalone 独立安装包集成 + 安装方式选择器 (0.9.4)

- 新增 standalone 安装链路(自带 Node.js,零依赖,无需 npm)
- 安装方式选择器:自动/CDN加速/GitHub/npm 四选一
- 动态查询 latest.json 获取最新版本,不怕旧资源被删除
- GitHub 模式:从 GitHub Releases 下载(CDN 不可用时备选)
- CherryStudio openclaw.exe 干扰过滤
- 默认源改为汉化优化版
- 日志隐藏 R2 下载地址(安全)
- 卸载兼容:standalone + npm 双清理
- 版本检测覆盖所有 standalone 安装路径
- README + 官网添加独立安装包说明
- macOS npm 权限问题通过 standalone 自动解决
This commit is contained in:
晴天
2026-03-17 06:32:23 +08:00
parent 3516f099ff
commit 1809329aaa
13 changed files with 682 additions and 54 deletions

View File

@@ -41,6 +41,16 @@ ClawPanel 是 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslatio
> 🌐 **官网**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下载**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) > 🌐 **官网**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下载**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest)
### ⚡ OpenClaw 独立安装包(零依赖,无需 Node.js/npm
不想折腾 Node.js 环境?直接下载 [OpenClaw 独立安装包](https://github.com/qingchencloud/openclaw-standalone/releases/latest)**内置运行时,解压即用**
- **Windows**: 下载 `.exe` 安装向导,双击即装
- **macOS / Linux / 树莓派**: `curl -fsSL https://dl.qrj.ai/openclaw/install.sh | bash`
- **全平台**: [GitHub Releases](https://github.com/qingchencloud/openclaw-standalone/releases/latest)
> ClawPanel 安装 OpenClaw 时会**自动优先使用独立安装包**,无需手动操作。此方案仅供不使用 ClawPanel 的用户独立安装。
### 🔥 开发板 / 嵌入式设备支持 ### 🔥 开发板 / 嵌入式设备支持
ClawPanel 提供**纯 Web 版部署模式**(零 GUI 依赖),天然兼容 ARM64 开发板和嵌入式设备: ClawPanel 提供**纯 Web 版部署模式**(零 GUI 依赖),天然兼容 ARM64 开发板和嵌入式设备:

View File

@@ -34,7 +34,7 @@
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。", "description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。",
"url": "https://claw.qt.cool/", "url": "https://claw.qt.cool/",
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest", "downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
"softwareVersion": "0.9.3", "softwareVersion": "0.9.4",
"author": { "author": {
"@type": "Organization", "@type": "Organization",
"name": "晴辰云 QingchenCloud", "name": "晴辰云 QingchenCloud",
@@ -1133,7 +1133,7 @@
<div class="orb orb-2" style="top:auto;bottom:-100px"></div> <div class="orb orb-2" style="top:auto;bottom:-100px"></div>
<div class="container-sm" style="position:relative;z-index:10"> <div class="container-sm" style="position:relative;z-index:10">
<div class="section-header"> <div class="section-header">
<div class="reveal download-version"><span class="pulse"></span> v0.9.3 最新版</div> <div class="reveal download-version"><span class="pulse"></span> v0.9.4 最新版</div>
<h2 class="reveal section-title"><span class="gradient-text">下载安装</span></h2> <h2 class="reveal section-title"><span class="gradient-text">下载安装</span></h2>
<p class="reveal section-desc">选择你的操作系统,一键下载安装</p> <p class="reveal section-desc">选择你的操作系统,一键下载安装</p>
</div> </div>
@@ -1143,11 +1143,11 @@
<h3>macOS</h3> <h3>macOS</h3>
<p class="dl-desc">支持 Apple Silicon 和 Intel 芯片</p> <p class="dl-desc">支持 Apple Silicon 和 Intel 芯片</p>
<div class="dl-links"> <div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.3_aarch64.dmg" target="_blank" rel="noopener"> <a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.4_aarch64.dmg" target="_blank" rel="noopener">
Apple Silicon (M1/M2/M3/M4) Apple Silicon (M1/M2/M3/M4)
<span class="dl-format">.dmg</span> <span class="dl-format">.dmg</span>
</a> </a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.3_x64.dmg" target="_blank" rel="noopener"> <a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.4_x64.dmg" target="_blank" rel="noopener">
Intel 芯片 Intel 芯片
<span class="dl-format">.dmg</span> <span class="dl-format">.dmg</span>
</a> </a>
@@ -1165,11 +1165,11 @@
<h3>Windows</h3> <h3>Windows</h3>
<p class="dl-desc">支持 Windows 10 及以上版本</p> <p class="dl-desc">支持 Windows 10 及以上版本</p>
<div class="dl-links"> <div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.3_x64-setup.exe" target="_blank" rel="noopener"> <a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.4_x64-setup.exe" target="_blank" rel="noopener">
安装程序 安装程序
<span class="dl-format">.exe</span> <span class="dl-format">.exe</span>
</a> </a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.3_x64_en-US.msi" target="_blank" rel="noopener"> <a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.4_x64_en-US.msi" target="_blank" rel="noopener">
MSI 安装包 MSI 安装包
<span class="dl-format">.msi</span> <span class="dl-format">.msi</span>
</a> </a>
@@ -1180,17 +1180,29 @@
<h3>Linux</h3> <h3>Linux</h3>
<p class="dl-desc">支持主流 Linux 发行版</p> <p class="dl-desc">支持主流 Linux 发行版</p>
<div class="dl-links"> <div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.3_amd64.AppImage" target="_blank" rel="noopener"> <a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.4_amd64.AppImage" target="_blank" rel="noopener">
通用版 通用版
<span class="dl-format">.AppImage</span> <span class="dl-format">.AppImage</span>
</a> </a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.3_amd64.deb" target="_blank" rel="noopener"> <a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.9.4_amd64.deb" target="_blank" rel="noopener">
Debian / Ubuntu Debian / Ubuntu
<span class="dl-format">.deb</span> <span class="dl-format">.deb</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div class="reveal" style="margin-top:40px;max-width:680px;margin-left:auto;margin-right:auto;border-radius:16px;background:linear-gradient(135deg,rgba(99,102,241,0.08),rgba(168,85,247,0.08));border:1px solid rgba(99,102,241,0.15);padding:28px 32px;text-align:center">
<h3 style="font-size:18px;font-weight:700;margin-bottom:8px">⚡ OpenClaw 独立安装包<span style="font-size:12px;background:var(--accent);color:#fff;padding:2px 8px;border-radius:20px;margin-left:8px;vertical-align:2px">零依赖</span></h3>
<p style="font-size:14px;color:var(--text-s);margin-bottom:16px">不想折腾 Node.js 环境?下载独立安装包,<strong>内置运行时,解压即用</strong>。支持 Windows / macOS / Linux / 树莓派。</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
<a href="https://github.com/qingchencloud/openclaw-standalone/releases/latest" target="_blank" rel="noopener" class="btn btn-primary" style="font-size:14px;padding:10px 20px">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="16" height="16" style="display:inline;vertical-align:-3px;margin-right:4px"><path d="M12 15V3m0 12l-4-4m4 4l4-4M2 17l.621 2.485A2 2 0 004.561 21h14.878a2 2 0 001.94-1.515L22 17"/></svg>
下载独立安装包
</a>
<a href="https://github.com/qingchencloud/openclaw-standalone" target="_blank" rel="noopener" class="btn btn-outline" style="font-size:14px;padding:10px 20px">GitHub 项目</a>
</div>
<p style="font-size:12px;color:var(--text-t);margin-top:12px">ClawPanel 安装 OpenClaw 时会自动优先使用独立安装包,无需手动下载</p>
</div>
<div class="reveal download-note" style="text-align:center"> <div class="reveal download-note" style="text-align:center">
<p>查看 <a href="https://github.com/qingchencloud/clawpanel/releases" target="_blank" rel="noopener">所有版本</a> · 需要帮助?阅读 <a href="https://github.com/qingchencloud/clawpanel#readme" target="_blank" rel="noopener">安装文档</a></p> <p>查看 <a href="https://github.com/qingchencloud/clawpanel/releases" target="_blank" rel="noopener">所有版本</a> · 需要帮助?阅读 <a href="https://github.com/qingchencloud/clawpanel#readme" target="_blank" rel="noopener">安装文档</a></p>
<p style="margin-top:12px"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px;opacity:0.7"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> 国内网络下载慢?加入 <a href="https://qt.cool/c/OpenClaw" target="_blank" rel="noopener">QQ 群</a><a href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener">微信群</a> 获取安装包直传</p> <p style="margin-top:12px"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px;opacity:0.7"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> 国内网络下载慢?加入 <a href="https://qt.cool/c/OpenClaw" target="_blank" rel="noopener">QQ 群</a><a href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener">微信群</a> 获取安装包直传</p>

View File

@@ -1,4 +1,8 @@
{ {
"standalone": {
"baseUrl": "https://dl.qrj.ai/openclaw-standalone",
"enabled": true
},
"r2": { "r2": {
"baseUrl": "https://dl.qrj.ai/openclaw-zh", "baseUrl": "https://dl.qrj.ai/openclaw-zh",
"enabled": true "enabled": true

View File

@@ -1,6 +1,6 @@
{ {
"name": "clawpanel", "name": "clawpanel",
"version": "0.9.3", "version": "0.9.4",
"private": true, "private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用", "description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module", "type": "module",

View File

@@ -127,6 +127,97 @@ function r2Config() {
return policy?.r2 || { enabled: false } return policy?.r2 || { enabled: false }
} }
function standaloneConfig() {
const policy = loadVersionPolicy()
return policy?.standalone || { enabled: false }
}
function standalonePlatformKey() {
const arch = process.arch
const plat = process.platform
if (plat === 'win32' && arch === 'x64') return 'win-x64'
if (plat === 'darwin' && arch === 'arm64') return 'mac-arm64'
if (plat === 'darwin' && arch === 'x64') return 'mac-x64'
if (plat === 'linux' && arch === 'x64') return 'linux-x64'
if (plat === 'linux' && arch === 'arm64') return 'linux-arm64'
return 'unknown'
}
function standaloneInstallDir() {
if (isWindows) return path.join(process.env.LOCALAPPDATA || '', 'OpenClaw')
return path.join(os.homedir(), '.openclaw-bin')
}
async function _tryStandaloneInstall(version, logs, overrideBaseUrl = null) {
const cfg = standaloneConfig()
if (!cfg.enabled || !cfg.baseUrl) return false
const platform = standalonePlatformKey()
if (platform === 'unknown') throw new Error('当前平台不支持 standalone 安装包')
const installDir = standaloneInstallDir()
logs.push('📦 尝试 standalone 独立安装包(汉化版专属,自带 Node.js 运行时,无需 npm')
logs.push('查询最新版本...')
const manifestUrl = `${cfg.baseUrl}/latest.json`
const resp = await globalThis.fetch(manifestUrl, { signal: AbortSignal.timeout(10000) })
if (!resp.ok) throw new Error(`standalone 清单不可用 (HTTP ${resp.status})`)
const manifest = await resp.json()
const remoteVersion = manifest.version
if (!remoteVersion) throw new Error('standalone 清单缺少 version 字段')
if (version !== 'latest' && !versionsMatch(remoteVersion, version)) {
throw new Error(`standalone 版本 ${remoteVersion} 与请求版本 ${version} 不匹配`)
}
const remoteBase = overrideBaseUrl || manifest.base_url || `${cfg.baseUrl}/${remoteVersion}`
const ext = isWindows ? 'zip' : 'tar.gz'
const filename = `openclaw-${remoteVersion}-${platform}.${ext}`
const downloadUrl = `${remoteBase}/${filename}`
logs.push(`从 CDN 下载: ${filename}`)
const tmpPath = path.join(os.tmpdir(), filename)
const dlResp = await globalThis.fetch(downloadUrl, { signal: AbortSignal.timeout(600000) })
if (!dlResp.ok) throw new Error(`standalone 下载失败 (HTTP ${dlResp.status})`)
const buffer = Buffer.from(await dlResp.arrayBuffer())
const sizeMb = (buffer.length / 1048576).toFixed(0)
logs.push(`下载完成 (${sizeMb}MB),解压安装中...`)
fs.writeFileSync(tmpPath, buffer)
// 清理旧安装 & 解压
if (fs.existsSync(installDir)) {
fs.rmSync(installDir, { recursive: true, force: true })
}
fs.mkdirSync(installDir, { recursive: true })
if (isWindows) {
// Windows: 用 PowerShell 解压 zip
execSync(`powershell -NoProfile -Command "Expand-Archive -Path '${tmpPath}' -DestinationPath '${installDir}' -Force"`, { windowsHide: true })
// 处理嵌套 openclaw/ 目录
const nested = path.join(installDir, 'openclaw')
if (fs.existsSync(nested) && fs.existsSync(path.join(nested, 'node.exe'))) {
for (const entry of fs.readdirSync(nested)) {
fs.renameSync(path.join(nested, entry), path.join(installDir, entry))
}
fs.rmSync(nested, { recursive: true, force: true })
}
} else {
// Unix: tar 解压
execSync(`tar -xzf "${tmpPath}" -C "${installDir}" --strip-components=1`, { windowsHide: true })
}
try { fs.unlinkSync(tmpPath) } catch {}
// 验证
const binFile = isWindows ? 'openclaw.cmd' : 'openclaw'
if (!fs.existsSync(path.join(installDir, binFile))) {
throw new Error('standalone 解压后未找到 openclaw 可执行文件')
}
logs.push(`✅ standalone 安装完成 (${remoteVersion})`)
logs.push(`安装目录: ${installDir}`)
return true
}
function r2PlatformKey() { function r2PlatformKey() {
const arch = process.arch // x64, arm64, etc. const arch = process.arch // x64, arm64, etc.
const plat = process.platform // linux, darwin, win32 const plat = process.platform // linux, darwin, win32
@@ -3159,7 +3250,7 @@ const handlers = {
throw new Error('查询版本失败: ' + (lastError?.message || lastError || 'unknown error')) throw new Error('查询版本失败: ' + (lastError?.message || lastError || 'unknown error'))
}, },
async upgrade_openclaw({ source = 'chinese', version } = {}) { async upgrade_openclaw({ source = 'chinese', version, method = 'auto' } = {}) {
const currentSource = detectInstalledSource() const currentSource = detectInstalledSource()
const pkg = npmPackageName(source) const pkg = npmPackageName(source)
const recommended = recommendedVersionFor(source) const recommended = recommendedVersionFor(source)
@@ -3170,16 +3261,30 @@ const handlers = {
const registry = pickRegistryForPackage(pkg) const registry = pickRegistryForPackage(pkg)
const logs = [] const logs = []
// ── R2 CDN 加速:优先尝试从 CDN 下载预装归档 ── // ── standalone 安装auto / standalone-r2 / standalone-github ──
if (source !== 'official') { const tryStandalone = source !== 'official' && ['auto', 'standalone-r2', 'standalone-github'].includes(method)
if (tryStandalone) {
try { try {
const r2Result = await _tryR2Install(ver, source, logs) const githubBase = method === 'standalone-github'
if (r2Result) return logs.join('\n') ? `https://github.com/qingchencloud/openclaw-standalone/releases/download/v${ver}`
: null
const saResult = await _tryStandaloneInstall(ver, logs, githubBase)
if (saResult) {
const label = method === 'standalone-github' ? 'GitHub' : 'CDN'
logs.push(`✅ standalone (${label}) 安装完成`)
return logs.join('\n')
}
} catch (e) { } catch (e) {
logs.push(`CDN 加速不可用(${e.message}),降级到 npm 安装...`) if (method === 'auto') {
logs.push(`standalone 不可用(${e.message}),降级到 npm 安装...`)
} else {
throw new Error(`standalone 安装失败: ${e.message}`)
}
} }
} }
// ── npm install兜底或用户明确选择 ──
if (!version && recommended) { if (!version && recommended) {
logs.push(`ClawPanel ${PANEL_VERSION} 默认绑定 OpenClaw 稳定版: ${recommended}`) logs.push(`ClawPanel ${PANEL_VERSION} 默认绑定 OpenClaw 稳定版: ${recommended}`)
} }
@@ -3214,6 +3319,12 @@ const handlers = {
uninstall_openclaw({ cleanConfig = false } = {}) { uninstall_openclaw({ cleanConfig = false } = {}) {
const npmBin = isWindows ? 'npm.cmd' : 'npm' const npmBin = isWindows ? 'npm.cmd' : 'npm'
// 清理 standalone 安装
const saDir = standaloneInstallDir()
if (fs.existsSync(saDir)) {
try { fs.rmSync(saDir, { recursive: true, force: true }) } catch {}
}
// 清理 npm 安装
try { execSync(`${npmBin} uninstall -g openclaw 2>&1`, { timeout: 60000, windowsHide: true }) } catch {} try { execSync(`${npmBin} uninstall -g openclaw 2>&1`, { timeout: 60000, windowsHide: true }) } catch {}
try { execSync(`${npmBin} uninstall -g @qingchencloud/openclaw-zh 2>&1`, { timeout: 60000, windowsHide: true }) } catch {} try { execSync(`${npmBin} uninstall -g @qingchencloud/openclaw-zh 2>&1`, { timeout: 60000, windowsHide: true }) } catch {}
if (cleanConfig && fs.existsSync(OPENCLAW_DIR)) { if (cleanConfig && fs.existsSync(OPENCLAW_DIR)) {

2
src-tauri/Cargo.lock generated
View File

@@ -328,7 +328,7 @@ dependencies = [
[[package]] [[package]]
name = "clawpanel" name = "clawpanel"
version = "0.9.3" version = "0.9.4"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "clawpanel" name = "clawpanel"
version = "0.9.3" version = "0.9.4"
edition = "2021" edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板" description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"] authors = ["qingchencloud"]

View File

@@ -75,8 +75,19 @@ struct R2Config {
enabled: bool, enabled: bool,
} }
#[derive(Debug, Deserialize, Default)]
struct StandaloneConfig {
#[serde(default)]
#[serde(rename = "baseUrl")]
base_url: Option<String>,
#[serde(default)]
enabled: bool,
}
#[derive(Debug, Deserialize, Default)] #[derive(Debug, Deserialize, Default)]
struct VersionPolicy { struct VersionPolicy {
#[serde(default)]
standalone: StandaloneConfig,
#[serde(default)] #[serde(default)]
r2: R2Config, r2: R2Config,
#[serde(default)] #[serde(default)]
@@ -128,6 +139,76 @@ fn r2_config() -> R2Config {
load_version_policy().r2 load_version_policy().r2
} }
fn standalone_config() -> StandaloneConfig {
load_version_policy().standalone
}
/// standalone 包的平台 key与 CI 构建矩阵一致)
fn standalone_platform_key() -> &'static str {
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
{ "win-x64" }
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{ "mac-arm64" }
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
{ "mac-x64" }
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{ "linux-x64" }
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
{ "linux-arm64" }
#[cfg(not(any(
all(target_os = "windows", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "aarch64"),
)))]
{ "unknown" }
}
/// standalone 包的文件扩展名
fn standalone_archive_ext() -> &'static str {
#[cfg(target_os = "windows")]
{ "zip" }
#[cfg(not(target_os = "windows"))]
{ "tar.gz" }
}
/// standalone 安装目录
fn standalone_install_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
// Inno Setup PrivilegesRequired=lowest 默认安装到 %LOCALAPPDATA%\Programs
std::env::var("LOCALAPPDATA").ok().map(|d| PathBuf::from(d).join("Programs").join("OpenClaw"))
}
#[cfg(not(target_os = "windows"))]
{
dirs::home_dir().map(|h| h.join(".openclaw-bin"))
}
}
/// 所有可能的 standalone 安装位置(用于检测和卸载)
fn all_standalone_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
#[cfg(target_os = "windows")]
{
if let Ok(la) = std::env::var("LOCALAPPDATA") {
dirs.push(PathBuf::from(&la).join("Programs").join("OpenClaw"));
dirs.push(PathBuf::from(&la).join("OpenClaw"));
}
if let Ok(pf) = std::env::var("ProgramFiles") {
dirs.push(PathBuf::from(pf).join("OpenClaw"));
}
}
#[cfg(not(target_os = "windows"))]
{
if let Some(h) = dirs::home_dir() {
dirs.push(h.join(".openclaw-bin"));
}
dirs.push(PathBuf::from("/opt/openclaw"));
}
dirs
}
fn recommended_version_for(source: &str) -> Option<String> { fn recommended_version_for(source: &str) -> Option<String> {
let policy = load_version_policy(); let policy = load_version_policy();
let panel_entry = policy.panels.get(panel_version()); let panel_entry = policy.panels.get(panel_version());
@@ -659,11 +740,38 @@ async fn get_local_version() -> Option<String> {
} }
} }
} }
// Windows: 直接读 npm 全局目录下的 package.json避免 spawn 进程 // Windows: 先查 standalone 安装,再查 npm 全局目录
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
// 检查所有 standalone 安装目录
for sa_dir in all_standalone_dirs() {
let version_file = sa_dir.join("VERSION");
if let Ok(content) = fs::read_to_string(&version_file) {
for line in content.lines() {
if let Some(ver) = line.strip_prefix("openclaw_version=") {
let ver = ver.trim();
if !ver.is_empty() {
return Some(ver.to_string());
}
}
}
}
let sa_pkg = sa_dir
.join("node_modules")
.join("@qingchencloud")
.join("openclaw-zh")
.join("package.json");
if let Ok(content) = fs::read_to_string(&sa_pkg) {
if let Some(ver) = serde_json::from_str::<Value>(&content)
.ok()
.and_then(|v| v.get("version")?.as_str().map(String::from))
{
return Some(ver);
}
}
}
// npm 全局目录
if let Ok(appdata) = std::env::var("APPDATA") { if let Ok(appdata) = std::env::var("APPDATA") {
// 先查汉化版,再查官方版
for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] { for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] {
let pkg_json = PathBuf::from(&appdata) let pkg_json = PathBuf::from(&appdata)
.join("npm") .join("npm")
@@ -682,6 +790,25 @@ async fn get_local_version() -> Option<String> {
} }
} }
// 所有平台通用 fallback: CLI 输出(异步) // 所有平台通用 fallback: CLI 输出(异步)
// Windows: 先确认 openclaw 不是第三方程序(如 CherryStudio
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
if let Ok(o) = std::process::Command::new("where")
.arg("openclaw")
.creation_flags(0x08000000)
.output()
{
let stdout = String::from_utf8_lossy(&o.stdout).to_lowercase();
let all_third_party = stdout
.lines()
.filter(|l| !l.trim().is_empty())
.all(|l| l.contains(".cherrystudio") || l.contains("cherry-studio"));
if all_third_party {
return None;
}
}
}
use crate::utils::openclaw_command_async; use crate::utils::openclaw_command_async;
let output = openclaw_command_async() let output = openclaw_command_async()
.arg("--version") .arg("--version")
@@ -730,6 +857,17 @@ fn detect_installed_source() -> String {
// Windows: 优先通过文件系统检测,避免 npm list 阻塞 // Windows: 优先通过文件系统检测,避免 npm list 阻塞
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
// 检查所有可能的 standalone 安装目录
for sa_dir in all_standalone_dirs() {
let sa_zh = sa_dir
.join("node_modules")
.join("@qingchencloud")
.join("openclaw-zh");
if sa_zh.exists() {
return "chinese".into();
}
}
// 检查 npm 全局目录
if let Some(appdata) = std::env::var_os("APPDATA") { if let Some(appdata) = std::env::var_os("APPDATA") {
let zh_dir = PathBuf::from(&appdata) let zh_dir = PathBuf::from(&appdata)
.join("npm") .join("npm")
@@ -740,7 +878,8 @@ fn detect_installed_source() -> String {
return "chinese".into(); return "chinese".into();
} }
} }
"official".into() // 默认返回汉化版
"chinese".into()
} }
// 所有平台通用: npm list 检测 // 所有平台通用: npm list 检测
#[cfg(not(any(target_os = "macos", target_os = "windows")))] #[cfg(not(any(target_os = "macos", target_os = "windows")))]
@@ -875,11 +1014,12 @@ pub async fn upgrade_openclaw(
app: tauri::AppHandle, app: tauri::AppHandle,
source: String, source: String,
version: Option<String>, version: Option<String>,
method: Option<String>,
) -> Result<String, String> { ) -> Result<String, String> {
let app2 = app.clone(); let app2 = app.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
use tauri::Emitter; use tauri::Emitter;
let result = upgrade_openclaw_inner(app2.clone(), source, version).await; let result = upgrade_openclaw_inner(app2.clone(), source, version, method.unwrap_or_else(|| "auto".into())).await;
match result { match result {
Ok(msg) => { Ok(msg) => {
let _ = app2.emit("upgrade-done", &msg); let _ = app2.emit("upgrade-done", &msg);
@@ -994,6 +1134,272 @@ fn npm_global_bin_dir() -> Option<PathBuf> {
} }
} }
/// 尝试从 standalone 独立安装包安装 OpenClaw自带 Node.js零依赖
/// 动态查询 latest.json 获取最新版本,下载对应平台的归档并解压
/// 成功返回 Ok(版本号),失败返回 Err(原因) 供 caller 降级到 R2/npm
async fn try_standalone_install(
app: &tauri::AppHandle,
version: &str,
override_base_url: Option<&str>,
) -> Result<String, String> {
let source_label = if override_base_url.is_some() { "GitHub" } else { "CDN" };
use tauri::Emitter;
let cfg = standalone_config();
if !cfg.enabled {
return Err("standalone 安装未启用".into());
}
let base_url = cfg.base_url.as_deref().ok_or("standalone baseUrl 未配置")?;
let platform = standalone_platform_key();
if platform == "unknown" {
return Err("当前平台不支持 standalone 安装包".into());
}
let install_dir = standalone_install_dir().ok_or("无法确定 standalone 安装目录")?;
// 1. 动态查询最新版本
let _ = app.emit("upgrade-log", "\u{1F4E6} 尝试 standalone 独立安装包(汉化版专属,自带 Node.js 运行时,无需 npm");
let _ = app.emit("upgrade-log", "查询最新版本...");
let manifest_url = format!("{base_url}/latest.json");
let client = crate::commands::build_http_client(std::time::Duration::from_secs(10), None)
.map_err(|e| format!("HTTP 客户端创建失败: {e}"))?;
let manifest_resp = client
.get(&manifest_url)
.send()
.await
.map_err(|e| format!("standalone 清单获取失败: {e}"))?;
if !manifest_resp.status().is_success() {
return Err(format!("standalone 清单不可用 (HTTP {})", manifest_resp.status()));
}
let manifest: Value = manifest_resp
.json()
.await
.map_err(|e| format!("standalone 清单解析失败: {e}"))?;
let remote_version = manifest
.get("version")
.and_then(|v| v.as_str())
.ok_or("standalone 清单缺少 version 字段")?;
// 版本匹配检查
if version != "latest" && !versions_match(remote_version, version) {
return Err(format!(
"standalone 版本 {remote_version} 与请求版本 {version} 不匹配"
));
}
let default_base = format!("{base_url}/{remote_version}");
let remote_base = if let Some(ovr) = override_base_url {
ovr
} else {
manifest
.get("base_url")
.and_then(|v| v.as_str())
.unwrap_or(&default_base)
};
// 2. 构造下载 URL
let ext = standalone_archive_ext();
let filename = format!("openclaw-{remote_version}-{platform}.{ext}");
let download_url = format!("{remote_base}/{filename}");
let _ = app.emit(
"upgrade-log",
format!("{source_label} 下载: {filename}"),
);
let _ = app.emit("upgrade-progress", 15);
// 3. 流式下载
let tmp_dir = std::env::temp_dir();
let archive_path = tmp_dir.join(&filename);
let dl_client =
crate::commands::build_http_client(std::time::Duration::from_secs(600), None)
.map_err(|e| format!("下载客户端创建失败: {e}"))?;
let dl_resp = dl_client
.get(&download_url)
.send()
.await
.map_err(|e| format!("standalone 下载失败: {e}"))?;
if !dl_resp.status().is_success() {
return Err(format!(
"standalone 下载失败 (HTTP {}): {download_url}",
dl_resp.status()
));
}
let total_bytes = dl_resp.content_length().unwrap_or(0);
let size_mb = if total_bytes > 0 {
format!("{:.0}MB", total_bytes as f64 / 1_048_576.0)
} else {
"未知大小".into()
};
let _ = app.emit("upgrade-log", format!("下载中 ({size_mb})..."));
{
use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
let mut file = tokio::fs::File::create(&archive_path)
.await
.map_err(|e| format!("创建临时文件失败: {e}"))?;
let mut stream = dl_resp.bytes_stream();
let mut downloaded: u64 = 0;
let mut last_progress: u32 = 15;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| format!("下载中断: {e}"))?;
file.write_all(&chunk)
.await
.map_err(|e| format!("写入失败: {e}"))?;
downloaded += chunk.len() as u64;
if total_bytes > 0 {
let pct = 15 + ((downloaded as f64 / total_bytes as f64) * 55.0) as u32;
if pct > last_progress {
last_progress = pct;
let _ = app.emit("upgrade-progress", pct.min(70));
}
}
}
file.flush()
.await
.map_err(|e| format!("刷新文件失败: {e}"))?;
}
let _ = app.emit("upgrade-log", "下载完成,解压安装中...");
let _ = app.emit("upgrade-progress", 72);
// 4. 清理旧安装 & 创建目录
if install_dir.exists() {
let _ = std::fs::remove_dir_all(&install_dir);
}
std::fs::create_dir_all(&install_dir)
.map_err(|e| format!("创建安装目录失败: {e}"))?;
// 5. 解压
#[cfg(target_os = "windows")]
{
// Windows: zip 解压
let archive_file = std::fs::File::open(&archive_path)
.map_err(|e| format!("打开归档失败: {e}"))?;
let mut zip_archive = zip::ZipArchive::new(archive_file)
.map_err(|e| format!("ZIP 解析失败: {e}"))?;
zip_archive
.extract(&install_dir)
.map_err(|e| format!("ZIP 解压失败: {e}"))?;
// 归档内可能有 openclaw/ 子目录,需要提升一层
let nested = install_dir.join("openclaw");
if nested.exists() && nested.join("node.exe").exists() {
for entry in std::fs::read_dir(&nested).map_err(|e| format!("读取目录失败: {e}"))? {
if let Ok(entry) = entry {
let dest = install_dir.join(entry.file_name());
let _ = std::fs::rename(entry.path(), &dest);
}
}
let _ = std::fs::remove_dir_all(&nested);
}
}
#[cfg(not(target_os = "windows"))]
{
// Unix: tar.gz 解压
let status = Command::new("tar")
.args([
"-xzf",
&archive_path.to_string_lossy(),
"-C",
&install_dir.to_string_lossy(),
"--strip-components=1",
])
.status()
.map_err(|e| format!("解压失败: {e}"))?;
if !status.success() {
return Err("tar 解压失败".into());
}
}
// 清理临时文件
let _ = std::fs::remove_file(&archive_path);
let _ = app.emit("upgrade-progress", 85);
// 6. 验证安装
#[cfg(target_os = "windows")]
let openclaw_bin = install_dir.join("openclaw.cmd");
#[cfg(not(target_os = "windows"))]
let openclaw_bin = install_dir.join("openclaw");
if !openclaw_bin.exists() {
return Err("standalone 解压后未找到 openclaw 可执行文件".into());
}
// 7. 添加到 PATHWindows 用户 PATHUnix 创建 symlink
#[cfg(target_os = "windows")]
{
let install_str = install_dir.to_string_lossy().to_string();
// 检查是否已在 PATH 中
let current_path = std::env::var("PATH").unwrap_or_default();
if !current_path
.split(';')
.any(|p| p.eq_ignore_ascii_case(&install_str))
{
// 写入用户 PATH注册表
let _ = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"$p = [Environment]::GetEnvironmentVariable('Path','User'); if ($p -notlike '*{}*') {{ [Environment]::SetEnvironmentVariable('Path', $p + ';{}', 'User') }}",
install_str.replace('\'', "''"),
install_str.replace('\'', "''")
),
])
.creation_flags(0x08000000)
.status();
let _ = app.emit("upgrade-log", format!("已添加到 PATH: {install_str}"));
}
}
#[cfg(not(target_os = "windows"))]
{
// Unix: 创建 /usr/local/bin/openclaw symlink 或 ~/bin/openclaw
let link_targets = [
PathBuf::from("/usr/local/bin/openclaw"),
dirs::home_dir()
.unwrap_or_default()
.join("bin")
.join("openclaw"),
];
for link in &link_targets {
if let Some(parent) = link.parent() {
if parent.exists() {
let _ = std::fs::remove_file(link);
#[cfg(unix)]
{
if std::os::unix::fs::symlink(&openclaw_bin, link).is_ok() {
let _ = Command::new("chmod")
.args(["+x", &openclaw_bin.to_string_lossy()])
.status();
let _ = app.emit(
"upgrade-log",
format!("symlink 已创建: {}", link.display()),
);
break;
}
}
}
}
}
}
let _ = app.emit("upgrade-progress", 95);
let _ = app.emit(
"upgrade-log",
format!("✅ standalone 独立安装包安装完成 ({remote_version})"),
);
let _ = app.emit(
"upgrade-log",
format!("安装目录: {}", install_dir.display()),
);
// 刷新 CLI 检测缓存
crate::commands::service::invalidate_cli_detection_cache();
Ok(remote_version.to_string())
}
/// 尝试从 R2 CDN 下载预装归档安装 OpenClaw跳过 npm 依赖解析) /// 尝试从 R2 CDN 下载预装归档安装 OpenClaw跳过 npm 依赖解析)
/// 成功返回 Ok(版本号),失败返回 Err(原因) 供 caller 降级到 npm install /// 成功返回 Ok(版本号),失败返回 Err(原因) 供 caller 降级到 npm install
async fn try_r2_install( async fn try_r2_install(
@@ -1293,6 +1699,7 @@ async fn upgrade_openclaw_inner(
app: tauri::AppHandle, app: tauri::AppHandle,
source: String, source: String,
version: Option<String>, version: Option<String>,
method: String,
) -> Result<String, String> { ) -> Result<String, String> {
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::process::Stdio; use std::process::Stdio;
@@ -1309,29 +1716,46 @@ async fn upgrade_openclaw_inner(
.unwrap_or("latest"); .unwrap_or("latest");
let pkg = format!("{}@{}", pkg_name, ver); let pkg = format!("{}@{}", pkg_name, ver);
// ── R2 CDN 加速:优先尝试从 CDN 下载预装归档 ── // ── standalone 安装auto / standalone-r2 / standalone-github ──
if source != "official" { let try_standalone = source != "official"
// 目前仅汉化版支持 R2 加速 && (method == "auto" || method == "standalone-r2" || method == "standalone-github");
match try_r2_install(&app, ver, &source).await {
if try_standalone {
// standalone-github 模式:使用 GitHub Releases 下载地址
let github_base = if method == "standalone-github" {
Some(format!(
"https://github.com/qingchencloud/openclaw-standalone/releases/download/v{}",
ver
))
} else {
None
};
match try_standalone_install(&app, ver, github_base.as_deref()).await {
Ok(installed_ver) => { Ok(installed_ver) => {
let _ = app.emit("upgrade-progress", 100); let _ = app.emit("upgrade-progress", 100);
// 刷新缓存
super::refresh_enhanced_path(); super::refresh_enhanced_path();
crate::commands::service::invalidate_cli_detection_cache(); crate::commands::service::invalidate_cli_detection_cache();
let msg = format!("✅ CDN 加速安装完成,当前版本: {installed_ver}"); let label = if method == "standalone-github" { "GitHub" } else { "CDN" };
let msg = format!("✅ standalone ({label}) 安装完成,当前版本: {installed_ver}");
let _ = app.emit("upgrade-log", &msg); let _ = app.emit("upgrade-log", &msg);
return Ok(msg); return Ok(msg);
} }
Err(reason) => { Err(reason) => {
let _ = app.emit( if method == "auto" {
"upgrade-log", let _ = app.emit(
format!("CDN 加速不可用({reason}),降级到 npm 安装..."), "upgrade-log",
); format!("standalone 不可用({reason}),降级到 npm 安装..."),
let _ = app.emit("upgrade-progress", 5); );
let _ = app.emit("upgrade-progress", 5);
} else {
return Err(format!("standalone 安装失败: {reason}"));
}
} }
} }
} }
// ── npm install兜底或用户明确选择 ──
// 切换源时需要卸载旧包,但为避免安装失败导致 CLI 丢失, // 切换源时需要卸载旧包,但为避免安装失败导致 CLI 丢失,
// 先安装新包,成功后再卸载旧包 // 先安装新包,成功后再卸载旧包
let old_pkg = npm_package_name(&current_source); let old_pkg = npm_package_name(&current_source);
@@ -1637,7 +2061,19 @@ async fn uninstall_openclaw_inner(
let _ = openclaw_command().args(["gateway", "uninstall"]).output(); let _ = openclaw_command().args(["gateway", "uninstall"]).output();
} }
// 3. npm uninstall // 3. 清理 standalone 安装(所有可能的位置)
for sa_dir in &all_standalone_dirs() {
if sa_dir.exists() {
let _ = app.emit("upgrade-log", format!("清理 standalone 安装: {}", sa_dir.display()));
if let Err(e) = std::fs::remove_dir_all(sa_dir) {
let _ = app.emit("upgrade-log", format!("⚠️ 清理 standalone 失败: {e}(可能需要管理员权限)"));
} else {
let _ = app.emit("upgrade-log", "standalone 安装已清理 ✓");
}
}
}
// 4. npm uninstall
let _ = app.emit("upgrade-log", format!("$ npm uninstall -g {pkg}")); let _ = app.emit("upgrade-log", format!("$ npm uninstall -g {pkg}"));
let _ = app.emit("upgrade-progress", 20); let _ = app.emit("upgrade-progress", 20);

View File

@@ -653,6 +653,16 @@ mod platform {
fn candidate_cli_paths() -> Vec<PathBuf> { fn candidate_cli_paths() -> Vec<PathBuf> {
let mut candidates = Vec::new(); let mut candidates = Vec::new();
// standalone 安装目录(优先检测,覆盖所有可能位置)
if let Ok(localappdata) = env::var("LOCALAPPDATA") {
// Inno Setup PrivilegesRequired=lowest 默认路径
candidates.push(Path::new(&localappdata).join("Programs").join("OpenClaw").join("openclaw.cmd"));
candidates.push(Path::new(&localappdata).join("OpenClaw").join("openclaw.cmd"));
}
if let Ok(pf) = env::var("ProgramFiles") {
candidates.push(Path::new(&pf).join("OpenClaw").join("openclaw.cmd"));
}
if let Ok(appdata) = env::var("APPDATA") { if let Ok(appdata) = env::var("APPDATA") {
candidates.push(Path::new(&appdata).join("npm").join("openclaw.cmd")); candidates.push(Path::new(&appdata).join("npm").join("openclaw.cmd"));
} }
@@ -698,24 +708,24 @@ mod platform {
} }
// 方式2: 通过 where 查找(兼容 nvm、自定义 prefix 等) // 方式2: 通过 where 查找(兼容 nvm、自定义 prefix 等)
// 过滤掉第三方 openclaw如 CherryStudio 的 .cherrystudio/bin/openclaw.exe
let mut where_cmd = std::process::Command::new("where"); let mut where_cmd = std::process::Command::new("where");
where_cmd.arg("openclaw"); where_cmd.arg("openclaw");
where_cmd.env("PATH", crate::commands::enhanced_path()); where_cmd.env("PATH", crate::commands::enhanced_path());
where_cmd.creation_flags(CREATE_NO_WINDOW); where_cmd.creation_flags(CREATE_NO_WINDOW);
if let Ok(o) = where_cmd.output() { if let Ok(o) = where_cmd.output() {
if o.status.success() && !String::from_utf8_lossy(&o.stdout).trim().is_empty() {
return true;
}
}
// 方式3: 直接执行版本命令兜底
let mut cmd = std::process::Command::new("cmd");
cmd.args(["/c", "openclaw", "--version"]);
cmd.env("PATH", crate::commands::enhanced_path());
cmd.creation_flags(CREATE_NO_WINDOW);
if let Ok(o) = cmd.output() {
if o.status.success() { if o.status.success() {
return true; let stdout = String::from_utf8_lossy(&o.stdout);
for line in stdout.lines() {
let p = line.trim().to_lowercase();
// 跳过已知第三方 openclaw 路径
if p.contains(".cherrystudio") || p.contains("cherry-studio") {
continue;
}
if !p.is_empty() {
return true;
}
}
} }
} }
false false

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json", "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel", "productName": "ClawPanel",
"version": "0.9.3", "version": "0.9.4",
"identifier": "ai.openclaw.clawpanel", "identifier": "ai.openclaw.clawpanel",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
@@ -42,7 +42,10 @@
"silent": true "silent": true
}, },
"nsis": { "nsis": {
"languages": ["SimpChinese", "English"], "languages": [
"SimpChinese",
"English"
],
"displayLanguageSelector": true "displayLanguageSelector": true
} }
} }

View File

@@ -175,7 +175,7 @@ export const api = {
reloadGateway: () => invoke('reload_gateway'), reloadGateway: () => invoke('reload_gateway'),
restartGateway: () => invoke('restart_gateway'), restartGateway: () => invoke('restart_gateway'),
listOpenclawVersions: (source = 'chinese') => invoke('list_openclaw_versions', { source }), listOpenclawVersions: (source = 'chinese') => invoke('list_openclaw_versions', { source }),
upgradeOpenclaw: (source = 'chinese', version = null) => invoke('upgrade_openclaw', { source, version }), upgradeOpenclaw: (source = 'chinese', version = null, method = 'auto') => invoke('upgrade_openclaw', { source, version, method }),
uninstallOpenclaw: (cleanConfig = false) => invoke('uninstall_openclaw', { cleanConfig }), uninstallOpenclaw: (cleanConfig = false) => invoke('uninstall_openclaw', { cleanConfig }),
installGateway: () => invoke('install_gateway'), installGateway: () => invoke('install_gateway'),
uninstallGateway: () => invoke('uninstall_gateway'), uninstallGateway: () => invoke('uninstall_gateway'),

View File

@@ -505,7 +505,7 @@ async function handleSaveConfig(page, restart) {
// ===== 升级操作 ===== // ===== 升级操作 =====
async function doUpgradeWithModal(source, page, version = null) { async function doUpgradeWithModal(source, page, version = null, method = 'auto') {
const modal = showUpgradeModal('升级 / 切换版本') const modal = showUpgradeModal('升级 / 切换版本')
let unlistenLog, unlistenProgress, unlistenDone, unlistenError let unlistenLog, unlistenProgress, unlistenDone, unlistenError
setUpgrading(true) setUpgrading(true)
@@ -549,12 +549,12 @@ async function doUpgradeWithModal(source, page, version = null) {
}) })
// 发起后台任务(立即返回) // 发起后台任务(立即返回)
await api.upgradeOpenclaw(source, version) await api.upgradeOpenclaw(source, version, method)
modal.appendLog('后台任务已启动,请等待完成...') modal.appendLog('后台任务已启动,请等待完成...')
} else { } else {
// Web 模式仍然同步等待dev-api 后端没有 spawn // Web 模式仍然同步等待dev-api 后端没有 spawn
modal.appendLog('Web 模式:升级过程日志不可用,请等待完成...') modal.appendLog('Web 模式:升级过程日志不可用,请等待完成...')
const msg = await api.upgradeOpenclaw(source, version) const msg = await api.upgradeOpenclaw(source, version, method)
modal.setDone(typeof msg === 'string' ? msg : (msg?.message || '升级完成')) modal.setDone(typeof msg === 'string' ? msg : (msg?.message || '升级完成'))
await loadVersion(page) await loadVersion(page)
cleanup() cleanup()

View File

@@ -325,7 +325,17 @@ function renderInstallSection() {
</div> </div>
</label> </label>
</div> </div>
<div style="margin-bottom:var(--space-sm)"> <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> <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)"> <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.npmmirror.com">淘宝镜像(推荐国内用户)</option>
@@ -502,12 +512,44 @@ function bindEvents(page, nodeOk, detectState) {
} }
}) })
// 安装方式联动:源切换时更新方式选项可见性
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') const installBtn = page.querySelector('#btn-install')
if (!installBtn || !nodeOk) return if (!installBtn || !nodeOk) return
installBtn.addEventListener('click', async () => { installBtn.addEventListener('click', async () => {
const source = page.querySelector('input[name="install-source"]:checked')?.value || 'chinese' 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 registry = page.querySelector('#registry-select')?.value
const modal = showUpgradeModal('安装 OpenClaw') const modal = showUpgradeModal('安装 OpenClaw')
let unlistenLog, unlistenProgress let unlistenLog, unlistenProgress
@@ -597,7 +639,7 @@ function bindEvents(page, nodeOk, detectState) {
} }
// 发起后台任务(立即返回) // 发起后台任务(立即返回)
await api.upgradeOpenclaw(source) await api.upgradeOpenclaw(source, null, method)
modal.appendLog('后台安装任务已启动,请等待完成...') modal.appendLog('后台安装任务已启动,请等待完成...')
} else { } else {
// Web 模式:同步等待 // Web 模式:同步等待
@@ -606,7 +648,7 @@ function bindEvents(page, nodeOk, detectState) {
modal.appendLog(`设置 npm 镜像源: ${registry}`) modal.appendLog(`设置 npm 镜像源: ${registry}`)
try { await api.setNpmRegistry(registry) } catch {} try { await api.setNpmRegistry(registry) } catch {}
} }
const msg = await api.upgradeOpenclaw(source) const msg = await api.upgradeOpenclaw(source, null, method)
modal.setDone(msg) modal.setDone(msg)
toast('OpenClaw 安装成功', 'success') toast('OpenClaw 安装成功', 'success')
setTimeout(() => window.location.reload(), 1500) setTimeout(() => window.location.reload(), 1500)