mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
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:
10
README.md
10
README.md
@@ -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 开发板和嵌入式设备:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
2
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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. 添加到 PATH(Windows 用户 PATH,Unix 创建 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(¤t_source);
|
let old_pkg = npm_package_name(¤t_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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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">独立安装包 · GitHub(CDN 不可用时备选)</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)
|
||||||
|
|||||||
Reference in New Issue
Block a user