mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-25 17:54:10 +08:00
feat: R2通用tarball模式 — 一个52MB包覆盖全平台(win/mac/linux/arm64)
This commit is contained in:
@@ -142,7 +142,6 @@ async function _tryR2Install(version, source, logs) {
|
||||
const r2 = r2Config()
|
||||
if (!r2.enabled || !r2.baseUrl) return false
|
||||
const platform = r2PlatformKey()
|
||||
if (platform === 'unknown') return false
|
||||
|
||||
logs.push('尝试从 CDN 加速下载...')
|
||||
const manifestUrl = `${r2.baseUrl}/latest.json`
|
||||
@@ -151,95 +150,120 @@ async function _tryR2Install(version, source, logs) {
|
||||
const manifest = await resp.json()
|
||||
|
||||
const sourceKey = source === 'official' ? 'official' : 'chinese'
|
||||
const asset = manifest?.[sourceKey]?.assets?.[platform]
|
||||
if (!asset?.url) throw new Error(`CDN 无 ${sourceKey}/${platform} 归档`)
|
||||
const sourceObj = manifest?.[sourceKey]
|
||||
if (!sourceObj) throw new Error(`CDN 无 ${sourceKey} 配置`)
|
||||
|
||||
const cdnVersion = manifest?.[sourceKey]?.version || version
|
||||
const cdnVersion = sourceObj.version || version
|
||||
if (version !== 'latest' && !versionsMatch(cdnVersion, version)) {
|
||||
throw new Error(`CDN 版本 ${cdnVersion} 与请求版本 ${version} 不匹配`)
|
||||
}
|
||||
|
||||
const sizeMb = asset.size ? `${(asset.size / 1048576).toFixed(0)}MB` : '未知大小'
|
||||
logs.push(`CDN 下载: ${cdnVersion} (${platform}, ${sizeMb})`)
|
||||
// 优先通用 tarball(npm pack 产物,~50MB,全平台通用),其次平台特定 assets
|
||||
const tarball = sourceObj.tarball
|
||||
const asset = sourceObj.assets?.[platform]
|
||||
const useTarball = !!tarball?.url
|
||||
|
||||
if (!useTarball && !asset?.url) {
|
||||
if (platform === 'unknown') throw new Error('当前平台不支持 R2 加速')
|
||||
throw new Error(`CDN 无 ${sourceKey}/${platform} 归档`)
|
||||
}
|
||||
|
||||
const archiveUrl = useTarball ? tarball.url : asset.url
|
||||
const expectedSha = useTarball ? (tarball.sha256 || '') : (asset.sha256 || '')
|
||||
const expectedSize = useTarball ? (tarball.size || 0) : (asset.size || 0)
|
||||
const sizeMb = expectedSize ? `${(expectedSize / 1048576).toFixed(0)}MB` : '未知大小'
|
||||
const mode = useTarball ? '通用 tarball' : `${platform} 预装归档`
|
||||
logs.push(`CDN 下载: ${cdnVersion} (${mode}, ${sizeMb})`)
|
||||
|
||||
// 下载到临时文件
|
||||
const tmpPath = path.join(os.tmpdir(), `openclaw-${platform}.tgz`)
|
||||
const dlResp = await globalThis.fetch(asset.url, { signal: AbortSignal.timeout(300000) })
|
||||
const tmpPath = path.join(os.tmpdir(), `openclaw-cdn.tgz`)
|
||||
const dlResp = await globalThis.fetch(archiveUrl, { signal: AbortSignal.timeout(300000) })
|
||||
if (!dlResp.ok) throw new Error(`CDN 下载失败 (HTTP ${dlResp.status})`)
|
||||
const buffer = Buffer.from(await dlResp.arrayBuffer())
|
||||
fs.writeFileSync(tmpPath, buffer)
|
||||
|
||||
// SHA256 校验
|
||||
if (asset.sha256) {
|
||||
if (expectedSha) {
|
||||
const crypto = require('crypto')
|
||||
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
|
||||
if (hash !== asset.sha256) {
|
||||
if (hash !== expectedSha) {
|
||||
fs.unlinkSync(tmpPath)
|
||||
throw new Error(`SHA256 校验失败: 期望 ${asset.sha256}, 实际 ${hash}`)
|
||||
throw new Error(`SHA256 校验失败: 期望 ${expectedSha}, 实际 ${hash}`)
|
||||
}
|
||||
logs.push('SHA256 校验通过 ✓')
|
||||
}
|
||||
|
||||
// 确定 npm 全局 node_modules 目录
|
||||
let modulesDir
|
||||
if (isWindows) {
|
||||
modulesDir = path.join(process.env.APPDATA || '', 'npm', 'node_modules')
|
||||
} else if (isMac) {
|
||||
modulesDir = fs.existsSync('/opt/homebrew/lib/node_modules')
|
||||
? '/opt/homebrew/lib/node_modules'
|
||||
: '/usr/local/lib/node_modules'
|
||||
} else {
|
||||
if (useTarball) {
|
||||
// 通用 tarball 模式:npm install -g ./file.tgz(全平台通用,npm 自动处理原生模块)
|
||||
logs.push('通用 tarball 模式,执行 npm install...')
|
||||
const npmBin = isWindows ? 'npm.cmd' : 'npm'
|
||||
try {
|
||||
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim()
|
||||
modulesDir = path.join(prefix, 'lib', 'node_modules')
|
||||
} catch {
|
||||
modulesDir = '/usr/local/lib/node_modules'
|
||||
execSync(`${npmBin} install -g "${tmpPath}" --force 2>&1`, { timeout: 120000, windowsHide: true })
|
||||
logs.push('npm install 完成 ✓')
|
||||
} catch (e) {
|
||||
try { fs.unlinkSync(tmpPath) } catch {}
|
||||
throw new Error('npm install -g tarball 失败: ' + (e.stderr?.toString() || e.message).slice(-300))
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(modulesDir)) fs.mkdirSync(modulesDir, { recursive: true })
|
||||
|
||||
// 清理旧目录
|
||||
const qcDir = path.join(modulesDir, '@qingchencloud')
|
||||
if (fs.existsSync(qcDir)) fs.rmSync(qcDir, { recursive: true, force: true })
|
||||
|
||||
// 解压
|
||||
logs.push(`解压到 ${modulesDir}`)
|
||||
execSync(`tar -xzf "${tmpPath}" -C "${modulesDir}"`, { timeout: 60000, windowsHide: true })
|
||||
|
||||
// 归档内目录可能是 qingchencloud/(Windows tar 不支持 @ 前缀),需要重命名
|
||||
const noAtDir = path.join(modulesDir, 'qingchencloud')
|
||||
if (fs.existsSync(noAtDir) && !fs.existsSync(qcDir)) {
|
||||
fs.renameSync(noAtDir, qcDir)
|
||||
logs.push('目录已修正: qingchencloud → @qingchencloud')
|
||||
}
|
||||
|
||||
// 创建 bin 链接
|
||||
let binDir
|
||||
if (isWindows) {
|
||||
binDir = path.join(process.env.APPDATA || '', 'npm')
|
||||
} else if (isMac) {
|
||||
binDir = fs.existsSync('/opt/homebrew/bin') ? '/opt/homebrew/bin' : '/usr/local/bin'
|
||||
} else {
|
||||
try {
|
||||
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim()
|
||||
binDir = path.join(prefix, 'bin')
|
||||
} catch {
|
||||
binDir = '/usr/local/bin'
|
||||
}
|
||||
}
|
||||
const openclawJs = path.join(modulesDir, '@qingchencloud', 'openclaw-zh', 'bin', 'openclaw.js')
|
||||
if (fs.existsSync(openclawJs)) {
|
||||
// 平台特定归档模式:直接解压到 npm 全局 node_modules
|
||||
let modulesDir
|
||||
if (isWindows) {
|
||||
const cmdContent = `@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\nSETLOCAL\r\nCALL :find_dp0\r\n\r\nIF EXIST "%dp0%\\node.exe" (\r\n SET "_prog=%dp0%\\node.exe"\r\n) ELSE (\r\n SET "_prog=node"\r\n SET PATHEXT=%PATHEXT:;.JS;=;%\r\n)\r\n\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "${openclawJs}" %*\r\n`
|
||||
fs.writeFileSync(path.join(binDir, 'openclaw.cmd'), cmdContent)
|
||||
modulesDir = path.join(process.env.APPDATA || '', 'npm', 'node_modules')
|
||||
} else if (isMac) {
|
||||
modulesDir = fs.existsSync('/opt/homebrew/lib/node_modules')
|
||||
? '/opt/homebrew/lib/node_modules'
|
||||
: '/usr/local/lib/node_modules'
|
||||
} else {
|
||||
const linkPath = path.join(binDir, 'openclaw')
|
||||
try { fs.unlinkSync(linkPath) } catch {}
|
||||
fs.symlinkSync(openclawJs, linkPath)
|
||||
try { fs.chmodSync(openclawJs, 0o755) } catch {}
|
||||
try { fs.chmodSync(linkPath, 0o755) } catch {}
|
||||
try {
|
||||
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim()
|
||||
modulesDir = path.join(prefix, 'lib', 'node_modules')
|
||||
} catch {
|
||||
modulesDir = '/usr/local/lib/node_modules'
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(modulesDir)) fs.mkdirSync(modulesDir, { recursive: true })
|
||||
|
||||
const qcDir = path.join(modulesDir, '@qingchencloud')
|
||||
if (fs.existsSync(qcDir)) fs.rmSync(qcDir, { recursive: true, force: true })
|
||||
|
||||
logs.push(`解压到 ${modulesDir}`)
|
||||
execSync(`tar -xzf "${tmpPath}" -C "${modulesDir}"`, { timeout: 60000, windowsHide: true })
|
||||
|
||||
// 归档内目录可能是 qingchencloud/(Windows tar 不支持 @ 前缀),需要重命名
|
||||
const noAtDir = path.join(modulesDir, 'qingchencloud')
|
||||
if (fs.existsSync(noAtDir) && !fs.existsSync(qcDir)) {
|
||||
fs.renameSync(noAtDir, qcDir)
|
||||
logs.push('目录已修正: qingchencloud → @qingchencloud')
|
||||
}
|
||||
|
||||
// 创建 bin 链接
|
||||
let binDir
|
||||
if (isWindows) {
|
||||
binDir = path.join(process.env.APPDATA || '', 'npm')
|
||||
} else if (isMac) {
|
||||
binDir = fs.existsSync('/opt/homebrew/bin') ? '/opt/homebrew/bin' : '/usr/local/bin'
|
||||
} else {
|
||||
try {
|
||||
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim()
|
||||
binDir = path.join(prefix, 'bin')
|
||||
} catch {
|
||||
binDir = '/usr/local/bin'
|
||||
}
|
||||
}
|
||||
const openclawJs = path.join(modulesDir, '@qingchencloud', 'openclaw-zh', 'bin', 'openclaw.js')
|
||||
if (fs.existsSync(openclawJs)) {
|
||||
if (isWindows) {
|
||||
const cmdContent = `@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\nSETLOCAL\r\nCALL :find_dp0\r\n\r\nIF EXIST "%dp0%\\node.exe" (\r\n SET "_prog=%dp0%\\node.exe"\r\n) ELSE (\r\n SET "_prog=node"\r\n SET PATHEXT=%PATHEXT:;.JS;=;%\r\n)\r\n\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "${openclawJs}" %*\r\n`
|
||||
fs.writeFileSync(path.join(binDir, 'openclaw.cmd'), cmdContent)
|
||||
} else {
|
||||
const linkPath = path.join(binDir, 'openclaw')
|
||||
try { fs.unlinkSync(linkPath) } catch {}
|
||||
fs.symlinkSync(openclawJs, linkPath)
|
||||
try { fs.chmodSync(openclawJs, 0o755) } catch {}
|
||||
try { fs.chmodSync(linkPath, 0o755) } catch {}
|
||||
}
|
||||
logs.push('bin 链接已创建 ✓')
|
||||
}
|
||||
logs.push('bin 链接已创建 ✓')
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
|
||||
@@ -1018,29 +1018,50 @@ async fn try_r2_install(
|
||||
.await
|
||||
.map_err(|e| format!("CDN 清单解析失败: {e}"))?;
|
||||
|
||||
// 2. 查找版本和平台对应的归档 URL
|
||||
// 2. 查找归档:优先通用 tarball(全平台),其次平台特定 assets
|
||||
let source_key = if source == "official" {
|
||||
"official"
|
||||
} else {
|
||||
"chinese"
|
||||
};
|
||||
let asset = manifest
|
||||
.get(source_key)
|
||||
.and_then(|s| s.get("assets"))
|
||||
.and_then(|a| a.get(platform))
|
||||
.ok_or_else(|| format!("CDN 无 {source_key}/{platform} 归档"))?;
|
||||
let archive_url = asset
|
||||
.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("CDN 归档 URL 缺失")?;
|
||||
let expected_sha = asset.get("sha256").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let expected_size = asset.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let cdn_version = manifest
|
||||
.get(source_key)
|
||||
let source_obj = manifest.get(source_key);
|
||||
let cdn_version = source_obj
|
||||
.and_then(|s| s.get("version"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(version);
|
||||
|
||||
// 优先通用 tarball(npm pack 产物,~50MB,全平台通用)
|
||||
let tarball = source_obj.and_then(|s| s.get("tarball"));
|
||||
// 其次平台特定 assets(预装 node_modules,~200MB)
|
||||
let asset = source_obj
|
||||
.and_then(|s| s.get("assets"))
|
||||
.and_then(|a| a.get(platform));
|
||||
let use_tarball = tarball
|
||||
.and_then(|t| t.get("url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some();
|
||||
|
||||
let (archive_url, expected_sha, expected_size) = if use_tarball {
|
||||
let t = tarball.unwrap();
|
||||
(
|
||||
t.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("tarball URL 缺失")?,
|
||||
t.get("sha256").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
t.get("size").and_then(|v| v.as_u64()).unwrap_or(0),
|
||||
)
|
||||
} else if let Some(a) = asset {
|
||||
(
|
||||
a.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("归档 URL 缺失")?,
|
||||
a.get("sha256").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("size").and_then(|v| v.as_u64()).unwrap_or(0),
|
||||
)
|
||||
} else {
|
||||
return Err(format!("CDN 无 {source_key} 可用归档"));
|
||||
};
|
||||
|
||||
// 版本匹配检查(如果用户指定了版本,CDN 版本必须匹配)
|
||||
if version != "latest" && !versions_match(cdn_version, version) {
|
||||
return Err(format!(
|
||||
@@ -1122,114 +1143,127 @@ async fn try_r2_install(
|
||||
|
||||
let _ = app.emit("upgrade-progress", 72);
|
||||
|
||||
// 5. 解压到 npm 全局 node_modules 目录
|
||||
let modules_dir = npm_global_modules_dir().ok_or("无法确定 npm 全局 node_modules 目录")?;
|
||||
if !modules_dir.exists() {
|
||||
std::fs::create_dir_all(&modules_dir)
|
||||
.map_err(|e| format!("创建 node_modules 目录失败: {e}"))?;
|
||||
}
|
||||
|
||||
let _ = app.emit("upgrade-log", format!("解压到 {}", modules_dir.display()));
|
||||
|
||||
// 清理旧的 @qingchencloud 目录(如果存在)
|
||||
let qc_dir = modules_dir.join("@qingchencloud");
|
||||
if qc_dir.exists() {
|
||||
let _ = std::fs::remove_dir_all(&qc_dir);
|
||||
}
|
||||
|
||||
// 解压 tgz
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
let status = Command::new("tar")
|
||||
.args([
|
||||
"-xzf",
|
||||
&archive_path.to_string_lossy(),
|
||||
"-C",
|
||||
&modules_dir.to_string_lossy(),
|
||||
])
|
||||
.creation_flags(0x08000000)
|
||||
.status()
|
||||
.map_err(|e| format!("解压失败: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err("tar 解压失败".into());
|
||||
// 5. 安装:通用 tarball 用 npm install -g,平台归档用 tar 解压
|
||||
if use_tarball {
|
||||
// 通用 tarball 模式:npm install -g ./file.tgz(全平台通用,npm 自动处理原生模块)
|
||||
let _ = app.emit("upgrade-log", "通用 tarball 模式,执行 npm install...");
|
||||
let mut install_cmd = npm_command();
|
||||
install_cmd.args(["install", "-g", &archive_path.to_string_lossy(), "--force"]);
|
||||
apply_git_install_env(&mut install_cmd);
|
||||
let install_output = install_cmd
|
||||
.output()
|
||||
.map_err(|e| format!("npm install 执行失败: {e}"))?;
|
||||
if !install_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&install_output.stderr);
|
||||
let _ = std::fs::remove_file(&archive_path);
|
||||
return Err(format!(
|
||||
"npm install -g tarball 失败: {}",
|
||||
&stderr[stderr.len().saturating_sub(300)..]
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let status = Command::new("tar")
|
||||
.args([
|
||||
"-xzf",
|
||||
&archive_path.to_string_lossy(),
|
||||
"-C",
|
||||
&modules_dir.to_string_lossy(),
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("解压失败: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err("tar 解压失败".into());
|
||||
let _ = app.emit("upgrade-log", "npm install 完成 ✓");
|
||||
} else {
|
||||
// 平台特定归档模式:直接解压到 npm 全局 node_modules
|
||||
let modules_dir = npm_global_modules_dir().ok_or("无法确定 npm 全局 node_modules 目录")?;
|
||||
if !modules_dir.exists() {
|
||||
std::fs::create_dir_all(&modules_dir)
|
||||
.map_err(|e| format!("创建 node_modules 目录失败: {e}"))?;
|
||||
}
|
||||
}
|
||||
let _ = app.emit("upgrade-log", format!("解压到 {}", modules_dir.display()));
|
||||
|
||||
// 归档内目录可能是 qingchencloud/(Windows tar 不支持 @ 前缀),需要重命名
|
||||
let no_at_dir = modules_dir.join("qingchencloud");
|
||||
if no_at_dir.exists() && !qc_dir.exists() {
|
||||
std::fs::rename(&no_at_dir, &qc_dir)
|
||||
.map_err(|e| format!("重命名 qingchencloud → @qingchencloud 失败: {e}"))?;
|
||||
let _ = app.emit("upgrade-log", "目录已修正: qingchencloud → @qingchencloud");
|
||||
}
|
||||
let qc_dir = modules_dir.join("@qingchencloud");
|
||||
if qc_dir.exists() {
|
||||
let _ = std::fs::remove_dir_all(&qc_dir);
|
||||
}
|
||||
|
||||
let _ = app.emit("upgrade-progress", 85);
|
||||
let _ = app.emit("upgrade-log", "解压完成,创建 bin 链接...");
|
||||
|
||||
// 6. 创建 bin 链接
|
||||
let bin_dir = npm_global_bin_dir().ok_or("无法确定 npm bin 目录")?;
|
||||
let openclaw_js = modules_dir
|
||||
.join("@qingchencloud")
|
||||
.join("openclaw-zh")
|
||||
.join("bin")
|
||||
.join("openclaw.js");
|
||||
|
||||
if openclaw_js.exists() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows: 创建 .cmd 包装脚本
|
||||
let cmd_path = bin_dir.join("openclaw.cmd");
|
||||
let cmd_content = format!(
|
||||
"@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\nSETLOCAL\r\nCALL :find_dp0\r\n\r\nIF EXIST \"%dp0%\\node.exe\" (\r\n SET \"_prog=%dp0%\\node.exe\"\r\n) ELSE (\r\n SET \"_prog=node\"\r\n SET PATHEXT=%PATHEXT:;.JS;=;%\r\n)\r\n\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & \"%_prog%\" \"{}\" %*\r\n",
|
||||
openclaw_js.display()
|
||||
);
|
||||
std::fs::write(&cmd_path, cmd_content)
|
||||
.map_err(|e| format!("创建 openclaw.cmd 失败: {e}"))?;
|
||||
// 也创建 .ps1 版本
|
||||
let ps1_path = bin_dir.join("openclaw.ps1");
|
||||
let ps1_content = format!(
|
||||
"#!/usr/bin/env pwsh\r\n$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n\r\n$exe=\"\"\r\nif ($PSVersionTable.PSVersion -lt \"6.0\" -or $IsWindows) {{\r\n $exe=\".exe\"\r\n}}\r\n$ret=0\r\nif (Test-Path \"$basedir/node$exe\") {{\r\n if ($MyInvocation.ExpectingInput) {{\r\n $input | & \"$basedir/node$exe\" \"{}\" $args\r\n }} else {{\r\n & \"$basedir/node$exe\" \"{}\" $args\r\n }}\r\n $ret=$LASTEXITCODE\r\n}} else {{\r\n if ($MyInvocation.ExpectingInput) {{\r\n $input | & \"node$exe\" \"{}\" $args\r\n }} else {{\r\n & \"node$exe\" \"{}\" $args\r\n }}\r\n $ret=$LASTEXITCODE\r\n}}\r\nexit $ret\r\n",
|
||||
openclaw_js.display(), openclaw_js.display(), openclaw_js.display(), openclaw_js.display()
|
||||
);
|
||||
let _ = std::fs::write(&ps1_path, ps1_content);
|
||||
use std::os::windows::process::CommandExt;
|
||||
let status = Command::new("tar")
|
||||
.args([
|
||||
"-xzf",
|
||||
&archive_path.to_string_lossy(),
|
||||
"-C",
|
||||
&modules_dir.to_string_lossy(),
|
||||
])
|
||||
.creation_flags(0x08000000)
|
||||
.status()
|
||||
.map_err(|e| format!("解压失败: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err("tar 解压失败".into());
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// Unix: 创建 symlink
|
||||
let link_path = bin_dir.join("openclaw");
|
||||
let _ = std::fs::remove_file(&link_path);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(&openclaw_js, &link_path)
|
||||
.map_err(|e| format!("创建 symlink 失败: {e}"))?;
|
||||
// 确保可执行权限
|
||||
let _ = Command::new("chmod")
|
||||
.args(["+x", &openclaw_js.to_string_lossy()])
|
||||
.status();
|
||||
let _ = Command::new("chmod")
|
||||
.args(["+x", &link_path.to_string_lossy()])
|
||||
.status();
|
||||
let status = Command::new("tar")
|
||||
.args([
|
||||
"-xzf",
|
||||
&archive_path.to_string_lossy(),
|
||||
"-C",
|
||||
&modules_dir.to_string_lossy(),
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("解压失败: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err("tar 解压失败".into());
|
||||
}
|
||||
}
|
||||
let _ = app.emit("upgrade-log", "bin 链接已创建 ✓");
|
||||
} else {
|
||||
let _ = app.emit("upgrade-log", "⚠️ openclaw.js 未找到,bin 链接跳过");
|
||||
|
||||
// 归档内目录可能是 qingchencloud/(Windows tar 不支持 @ 前缀),需要重命名
|
||||
let no_at_dir = modules_dir.join("qingchencloud");
|
||||
if no_at_dir.exists() && !qc_dir.exists() {
|
||||
std::fs::rename(&no_at_dir, &qc_dir)
|
||||
.map_err(|e| format!("重命名 qingchencloud → @qingchencloud 失败: {e}"))?;
|
||||
let _ = app.emit("upgrade-log", "目录已修正: qingchencloud → @qingchencloud");
|
||||
}
|
||||
|
||||
let _ = app.emit("upgrade-log", "解压完成,创建 bin 链接...");
|
||||
|
||||
// 创建 bin 链接
|
||||
let bin_dir = npm_global_bin_dir().ok_or("无法确定 npm bin 目录")?;
|
||||
let openclaw_js = modules_dir
|
||||
.join("@qingchencloud")
|
||||
.join("openclaw-zh")
|
||||
.join("bin")
|
||||
.join("openclaw.js");
|
||||
|
||||
if openclaw_js.exists() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let cmd_path = bin_dir.join("openclaw.cmd");
|
||||
let cmd_content = format!(
|
||||
"@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\nSETLOCAL\r\nCALL :find_dp0\r\n\r\nIF EXIST \"%dp0%\\node.exe\" (\r\n SET \"_prog=%dp0%\\node.exe\"\r\n) ELSE (\r\n SET \"_prog=node\"\r\n SET PATHEXT=%PATHEXT:;.JS;=;%\r\n)\r\n\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & \"%_prog%\" \"{}\" %*\r\n",
|
||||
openclaw_js.display()
|
||||
);
|
||||
std::fs::write(&cmd_path, cmd_content)
|
||||
.map_err(|e| format!("创建 openclaw.cmd 失败: {e}"))?;
|
||||
let ps1_path = bin_dir.join("openclaw.ps1");
|
||||
let ps1_content = format!(
|
||||
"#!/usr/bin/env pwsh\r\n$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n\r\n$exe=\"\"\r\nif ($PSVersionTable.PSVersion -lt \"6.0\" -or $IsWindows) {{\r\n $exe=\".exe\"\r\n}}\r\n$ret=0\r\nif (Test-Path \"$basedir/node$exe\") {{\r\n if ($MyInvocation.ExpectingInput) {{\r\n $input | & \"$basedir/node$exe\" \"{}\" $args\r\n }} else {{\r\n & \"$basedir/node$exe\" \"{}\" $args\r\n }}\r\n $ret=$LASTEXITCODE\r\n}} else {{\r\n if ($MyInvocation.ExpectingInput) {{\r\n $input | & \"node$exe\" \"{}\" $args\r\n }} else {{\r\n & \"node$exe\" \"{}\" $args\r\n }}\r\n $ret=$LASTEXITCODE\r\n}}\r\nexit $ret\r\n",
|
||||
openclaw_js.display(), openclaw_js.display(), openclaw_js.display(), openclaw_js.display()
|
||||
);
|
||||
let _ = std::fs::write(&ps1_path, ps1_content);
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let link_path = bin_dir.join("openclaw");
|
||||
let _ = std::fs::remove_file(&link_path);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(&openclaw_js, &link_path)
|
||||
.map_err(|e| format!("创建 symlink 失败: {e}"))?;
|
||||
let _ = Command::new("chmod")
|
||||
.args(["+x", &openclaw_js.to_string_lossy()])
|
||||
.status();
|
||||
let _ = Command::new("chmod")
|
||||
.args(["+x", &link_path.to_string_lossy()])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
let _ = app.emit("upgrade-log", "bin 链接已创建 ✓");
|
||||
} else {
|
||||
let _ = app.emit("upgrade-log", "⚠️ openclaw.js 未找到,bin 链接跳过");
|
||||
}
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
|
||||
Reference in New Issue
Block a user