From de8b2ade8018c084950d2d56318a8b4440c240d8 Mon Sep 17 00:00:00 2001 From: 10000ge10000 <10000ge10000@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:21:47 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v1.0.12=20=E2=80=94=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20OpenClaw=20=E7=89=88=E6=9C=AC=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E3=80=81BusyBox=20tar=20=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 +++ VERSION | 2 +- luasrc/controller/openclaw.lua | 178 +++++----------------------- luasrc/model/cbi/openclaw/basic.lua | 108 +++-------------- luasrc/view/openclaw/status.htm | 2 - root/usr/bin/openclaw-env | 22 +++- 6 files changed, 85 insertions(+), 241 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 395b16e..eed9d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。 +## [1.0.12] - 2026-03-11 + +### 移除 OpenClaw 版本检测 & 修复 BusyBox tar 兼容性 + +#### 变更 +- **「检测升级」按钮**: 不再检查 OpenClaw (npm) 版本,仅检查插件 (luci-app-openclaw) 是否有新版本 +- **「检测升级」显示更新内容**: 检测到新插件版本时,直接展示该版本的 Release Notes,告知用户升级了什么 +- **状态面板**: 移除「OpenClaw」版本显示行,保留 Node.js 和插件版本 +- **内部清理**: 移除 `get_openclaw_version()` 函数、`action_do_update`、`action_upgrade_log` 等已废弃后端 API + +#### 修复 +- **BusyBox tar 兼容性** (#18, #30): `openclaw-env` 安装 Node.js 时的解压命令优先使用 GNU tar `--strip-components=1`;若不支持则自动回退到 BusyBox tar 兼容方式(解压到临时目录后移动),无需用户手动安装 `tar` +- **插件升级网络错误提示**: 下载后检测文件内容,若 GitHub 返回 `Not Found`(GFW 拦截等情况)则显示明确提示,并附手动下载链接 + ## [1.0.11] - 2026-03-09 ### 修复 Telegram 配对后无法使用的严重 Bug diff --git a/VERSION b/VERSION index 59e9e60..bb83058 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.11 +1.0.12 diff --git a/luasrc/controller/openclaw.lua b/luasrc/controller/openclaw.lua index 4f44b87..e6b1621 100644 --- a/luasrc/controller/openclaw.lua +++ b/luasrc/controller/openclaw.lua @@ -1,32 +1,6 @@ -- luci-app-openclaw — LuCI Controller module("luci.controller.openclaw", package.seeall) --- 公共辅助: 获取 OpenClaw 版本号 -local function get_openclaw_version() - local sys = require "luci.sys" - -- 优先从 package.json 读取版本号 (轻量),避免每次启动 node 进程 - local dirs = { - "/opt/openclaw/global/lib/node_modules/openclaw", - "/opt/openclaw/global/node_modules/openclaw", - } - -- pnpm 版本目录 - local pnpm_glob = sys.exec("ls -d /opt/openclaw/global/*/node_modules/openclaw 2>/dev/null"):gsub("%s+$", "") - for d in pnpm_glob:gmatch("[^\n]+") do - dirs[#dirs + 1] = d - end - for _, d in ipairs(dirs) do - local pkg = d .. "/package.json" - local f = io.open(pkg, "r") - if f then - local content = f:read("*a") - f:close() - local ver = content:match('"version"%s*:%s*"([^"]+)"') - if ver and ver ~= "" then return ver end - end - end - return "" -end - function index() -- 主入口: 服务 → OpenClaw (🧠 作为菜单图标) local page = entry({"admin", "services", "openclaw"}, alias("admin", "services", "openclaw", "basic"), _("OpenClaw"), 90) @@ -50,15 +24,9 @@ function index() -- 安装/升级日志 API (轮询) entry({"admin", "services", "openclaw", "setup_log"}, call("action_setup_log"), nil).leaf = true - -- 版本检查 API + -- 版本检查 API (仅检查插件版本) entry({"admin", "services", "openclaw", "check_update"}, call("action_check_update"), nil).leaf = true - -- 执行升级 API - entry({"admin", "services", "openclaw", "do_update"}, call("action_do_update"), nil).leaf = true - - -- 升级日志 API (轮询) - entry({"admin", "services", "openclaw", "upgrade_log"}, call("action_upgrade_log"), nil).leaf = true - -- 卸载运行环境 API entry({"admin", "services", "openclaw", "uninstall"}, call("action_uninstall"), nil).leaf = true @@ -99,7 +67,6 @@ function action_status() memory_kb = 0, uptime = "", node_version = "", - openclaw_version = "", plugin_version = "", } @@ -119,12 +86,6 @@ function action_status() result.node_version = node_ver end - -- 检查 OpenClaw 版本 - local oc_ver = get_openclaw_version() - if oc_ver and oc_ver ~= "" then - result.openclaw_version = "v" .. oc_ver - end - -- 网关端口检查 local gw_check = sys.exec("netstat -tlnp 2>/dev/null | grep -c ':" .. port .. " ' || echo 0"):gsub("%s+", "") result.gateway_running = (tonumber(gw_check) or 0) > 0 @@ -306,18 +267,7 @@ function action_check_update() local http = require "luci.http" local sys = require "luci.sys" - -- 当前 OpenClaw 版本 - local current = get_openclaw_version() - - -- 最新 OpenClaw 版本 (从 npm registry 查询) - local latest = sys.exec("PATH=/opt/openclaw/node/bin:/opt/openclaw/global/bin:$PATH npm view openclaw version 2>/dev/null"):gsub("%s+", "") - - local has_update = false - if current ~= "" and latest ~= "" and current ~= latest then - has_update = true - end - - -- 插件版本检查 (从 GitHub API 获取最新 release tag) + -- 插件版本检查 (从 GitHub API 获取最新 release tag + release notes) local plugin_current = "" local pf = io.open("/usr/share/openclaw/VERSION", "r") or io.open("/root/luci-app-openclaw/VERSION", "r") @@ -327,108 +277,38 @@ function action_check_update() end local plugin_latest = "" + local release_notes = "" local plugin_has_update = false - -- 仅在请求参数含 check_plugin=1 或 quick=1 时检查插件版本 - local check_plugin = http.formvalue("check_plugin") or "" - local quick = http.formvalue("quick") or "" - if check_plugin == "1" or quick == "1" then - -- 使用 GitHub API 获取最新 release tag (轻量, 不下载任何文件) - local gh_resp = sys.exec("curl -sf --connect-timeout 5 --max-time 10 'https://api.github.com/repos/10000ge10000/luci-app-openclaw/releases/latest' 2>/dev/null | grep -o '\"tag_name\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | head -1 | cut -d'\"' -f4") - gh_resp = gh_resp:gsub("%s+", "") - if gh_resp ~= "" then - -- tag 可能是 v1.0.3 或 1.0.3 - plugin_latest = gh_resp:gsub("^v", "") + + -- 使用 GitHub API 获取最新 release (tag + body) + local gh_json = sys.exec("curl -sf --connect-timeout 5 --max-time 10 'https://api.github.com/repos/10000ge10000/luci-app-openclaw/releases/latest' 2>/dev/null") + if gh_json and gh_json ~= "" then + -- 提取 tag_name + local tag = gh_json:match('"tag_name"%s*:%s*"([^"]+)"') + if tag and tag ~= "" then + plugin_latest = tag:gsub("^v", ""):gsub("%s+", "") end - if plugin_current ~= "" and plugin_latest ~= "" and plugin_current ~= plugin_latest then - plugin_has_update = true + -- 提取 body (release notes), 处理 JSON 转义 + -- 结束引号后可能紧跟 \n、空格、, 或 },用宽松匹配 + local body = gh_json:match('"body"%s*:%s*"(.-)"[,}%]\n ]') + if body and body ~= "" then + -- 还原 JSON 转义: \n \r \" \\ + body = body:gsub("\\n", "\n"):gsub("\\r", ""):gsub('\\"', '"'):gsub("\\\\", "\\") + release_notes = body end end + if plugin_current ~= "" and plugin_latest ~= "" and plugin_current ~= plugin_latest then + plugin_has_update = true + end + http.prepare_content("application/json") http.write_json({ status = "ok", - current = current, - latest = latest, - has_update = has_update, plugin_current = plugin_current, plugin_latest = plugin_latest, - plugin_has_update = plugin_has_update - }) -end - --- ═══════════════════════════════════════════ --- 执行升级 API (后台执行 + 日志轮询) --- ═══════════════════════════════════════════ -function action_do_update() - local http = require "luci.http" - local sys = require "luci.sys" - - -- 清理旧日志和状态 - sys.exec("rm -f /tmp/openclaw-upgrade.log /tmp/openclaw-upgrade.pid /tmp/openclaw-upgrade.exit") - - -- 后台执行升级,升级完成后自动重启服务 - sys.exec("( /usr/bin/openclaw-env upgrade > /tmp/openclaw-upgrade.log 2>&1; RC=$?; echo $RC > /tmp/openclaw-upgrade.exit; if [ $RC -eq 0 ]; then echo '' >> /tmp/openclaw-upgrade.log; echo '正在重启服务...' >> /tmp/openclaw-upgrade.log; /etc/init.d/openclaw restart >> /tmp/openclaw-upgrade.log 2>&1; echo ' [✓] 服务已重启' >> /tmp/openclaw-upgrade.log; fi ) & echo $! > /tmp/openclaw-upgrade.pid") - - http.prepare_content("application/json") - http.write_json({ - status = "ok", - message = "升级已在后台启动,请查看升级日志..." - }) -end - --- ═══════════════════════════════════════════ --- 升级日志轮询 API --- ═══════════════════════════════════════════ -function action_upgrade_log() - local http = require "luci.http" - local sys = require "luci.sys" - - -- 读取日志内容 - local log = "" - local f = io.open("/tmp/openclaw-upgrade.log", "r") - if f then - log = f:read("*a") or "" - f:close() - end - - -- 检查进程是否还在运行 - local running = false - local pid_file = io.open("/tmp/openclaw-upgrade.pid", "r") - if pid_file then - local pid = pid_file:read("*a"):gsub("%s+", "") - pid_file:close() - if pid ~= "" then - local check = sys.exec("kill -0 " .. pid .. " 2>/dev/null && echo yes || echo no"):gsub("%s+", "") - running = (check == "yes") - end - end - - -- 读取退出码 - local exit_code = -1 - if not running then - local exit_file = io.open("/tmp/openclaw-upgrade.exit", "r") - if exit_file then - local code = exit_file:read("*a"):gsub("%s+", "") - exit_file:close() - exit_code = tonumber(code) or -1 - end - end - - -- 判断状态 - local state = "idle" - if running then - state = "running" - elseif exit_code == 0 then - state = "success" - elseif exit_code > 0 then - state = "failed" - end - - http.prepare_content("application/json") - http.write_json({ - state = state, - exit_code = exit_code, - log = log + plugin_has_update = plugin_has_update, + release_notes = release_notes }) end @@ -512,8 +392,14 @@ function action_plugin_upgrade() "else " .. " FSIZE=$(wc -c < /tmp/luci-app-openclaw-update.run 2>/dev/null | tr -d ' '); " .. " echo \"下载完成 (${FSIZE} bytes)\" >> /tmp/openclaw-plugin-upgrade.log; " .. + " FHEAD=$(head -c 9 /tmp/luci-app-openclaw-update.run 2>/dev/null); " .. " if [ \"$FSIZE\" -lt 10000 ] 2>/dev/null; then " .. - " echo '文件过小,可能下载失败或链接无效' >> /tmp/openclaw-plugin-upgrade.log; " .. + " if [ \"$FHEAD\" = 'Not Found' ]; then " .. + " echo '❌ GitHub 返回 \"Not Found\",可能是网络被拦截(GFW)或 Release 资产不存在' >> /tmp/openclaw-plugin-upgrade.log; " .. + " else " .. + " echo '❌ 文件过小,可能 GitHub 访问受限或网络异常' >> /tmp/openclaw-plugin-upgrade.log; " .. + " fi; " .. + " echo '请检查路由器是否能访问 github.com,或手动下载后安装: %s' >> /tmp/openclaw-plugin-upgrade.log; " .. " echo 1 > /tmp/openclaw-plugin-upgrade.exit; " .. " else " .. " echo '' >> /tmp/openclaw-plugin-upgrade.log; " .. @@ -530,7 +416,7 @@ function action_plugin_upgrade() " rm -f /tmp/luci-app-openclaw-update.run; " .. "fi " .. ") & echo $! > /tmp/openclaw-plugin-upgrade.pid", - version, run_url, run_url + version, run_url, run_url, run_url )) http.prepare_content("application/json") diff --git a/luasrc/model/cbi/openclaw/basic.lua b/luasrc/model/cbi/openclaw/basic.lua index 794a16f..82282d1 100644 --- a/luasrc/model/cbi/openclaw/basic.lua +++ b/luasrc/model/cbi/openclaw/basic.lua @@ -24,8 +24,6 @@ act.cfgvalue = function(self, section) local ctl_url = luci.dispatcher.build_url("admin", "services", "openclaw", "service_ctl") local log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "setup_log") local check_url = luci.dispatcher.build_url("admin", "services", "openclaw", "check_update") - local update_url = luci.dispatcher.build_url("admin", "services", "openclaw", "do_update") - local upgrade_log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "upgrade_log") local uninstall_url = luci.dispatcher.build_url("admin", "services", "openclaw", "uninstall") local plugin_upgrade_url = luci.dispatcher.build_url("admin", "services", "openclaw", "plugin_upgrade") local plugin_upgrade_log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "plugin_upgrade_log") @@ -223,116 +221,44 @@ act.cfgvalue = function(self, section) html[#html+1] = '}catch(e){el.innerHTML="❌ 错误";}' html[#html+1] = '});}' - -- 检测升级 (同时检查 OpenClaw + 插件版本) + -- 检测升级 (只检查插件版本,有新版本时显示更新内容) html[#html+1] = 'function ocCheckUpdate(){' html[#html+1] = 'var btn=document.getElementById("btn-check-update");' html[#html+1] = 'var el=document.getElementById("action-result");' html[#html+1] = 'var act=document.getElementById("oc-update-action");' html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在检测...";el.textContent="";act.style.display="none";' - html[#html+1] = '(new XHR()).get("' .. check_url .. '?check_plugin=1",null,function(x){' + html[#html+1] = '(new XHR()).get("' .. check_url .. '",null,function(x){' html[#html+1] = 'btn.disabled=false;btn.textContent="🔍 检测升级";' html[#html+1] = 'var dot=document.getElementById("update-dot");if(dot)dot.style.display="none";' html[#html+1] = 'try{var r=JSON.parse(x.responseText);' html[#html+1] = 'var msgs=[];' - -- OpenClaw 版本检查 - html[#html+1] = 'if(!r.current){msgs.push("⚠️ OpenClaw 运行环境未安装");}' - html[#html+1] = 'else if(r.has_update){msgs.push("📦 OpenClaw: v"+r.current+" → v"+r.latest+" (有新版本)");}' - html[#html+1] = 'else{msgs.push("✅ OpenClaw: v"+r.current+" (已是最新)");}' -- 插件版本检查 html[#html+1] = 'if(r.plugin_current){' html[#html+1] = 'if(r.plugin_has_update){msgs.push("🔌 插件: v"+r.plugin_current+" → v"+r.plugin_latest+" (有新版本)");}' html[#html+1] = 'else if(r.plugin_latest){msgs.push("✅ 插件: v"+r.plugin_current+" (已是最新)");}' html[#html+1] = 'else{msgs.push("🔌 插件: v"+r.plugin_current+" (无法检查最新版本)");}' html[#html+1] = '}' + html[#html+1] = 'if(msgs.length===0)msgs.push("无法获取版本信息");' html[#html+1] = 'el.innerHTML=msgs.join("
");' - -- 显示 OpenClaw 升级按钮 - html[#html+1] = 'if(r.has_update){' - html[#html+1] = 'act.style.display="block";' - html[#html+1] = 'act.innerHTML=\'\';' - html[#html+1] = '}' - -- 插件有更新时: 一键升级按钮 + GitHub 下载链接 + -- 插件有更新时: release notes + 一键升级按钮 + GitHub 下载链接 html[#html+1] = 'if(r.plugin_has_update){' html[#html+1] = 'act.style.display="block";' html[#html+1] = 'window._pluginLatestVer=r.plugin_latest;' - html[#html+1] = 'act.innerHTML=(act.innerHTML||"")+\' \';' - html[#html+1] = 'act.innerHTML=act.innerHTML+\' 📥 手动下载\';' + html[#html+1] = 'var notesHtml="";' + html[#html+1] = 'if(r.release_notes){' + html[#html+1] = 'var escaped=r.release_notes.replace(/&/g,"&").replace(//g,">");' + html[#html+1] = 'notesHtml=\'
\'' + html[#html+1] = '+\'
📋 v\'+r.plugin_latest+\' 更新内容
\'' + html[#html+1] = '+\'
\'+escaped+\'
\'' + html[#html+1] = '+\'
\';' + html[#html+1] = '}' + html[#html+1] = 'act.innerHTML=notesHtml' + html[#html+1] = '+\'\'' + html[#html+1] = '+\' 📥 手动下载\';' html[#html+1] = '}' html[#html+1] = '}catch(e){el.innerHTML="❌ 检测失败";}' html[#html+1] = '});}' - -- 执行升级 (带实时日志, 和安装一样的体验) - html[#html+1] = 'var _upgradeTimer=null;' - html[#html+1] = 'function ocDoUpdate(){' - html[#html+1] = 'var btn=document.getElementById("btn-do-update");' - html[#html+1] = 'var panel=document.getElementById("setup-log-panel");' - html[#html+1] = 'var logEl=document.getElementById("setup-log-content");' - html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");' - html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");' - html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");' - html[#html+1] = 'var actionEl=document.getElementById("action-result");' - html[#html+1] = 'if(!confirm("确定要升级 OpenClaw?升级期间服务将短暂中断。"))return;' - html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在升级...";' - html[#html+1] = 'actionEl.textContent="";' - html[#html+1] = 'panel.style.display="block";' - html[#html+1] = 'logEl.textContent="正在启动升级...\\n";' - html[#html+1] = 'titleEl.textContent="📋 升级日志";' - html[#html+1] = 'statusEl.innerHTML="⏳ 升级进行中...";' - html[#html+1] = 'resultEl.style.display="none";' - html[#html+1] = '(new XHR()).get("' .. update_url .. '",null,function(x){' - html[#html+1] = 'try{JSON.parse(x.responseText);}catch(e){}' - html[#html+1] = 'ocPollUpgradeLog();' - html[#html+1] = '});' - html[#html+1] = '}' - - -- 轮询升级日志 - html[#html+1] = 'function ocPollUpgradeLog(){' - html[#html+1] = 'if(_upgradeTimer)clearInterval(_upgradeTimer);' - html[#html+1] = '_upgradeTimer=setInterval(function(){' - html[#html+1] = '(new XHR()).get("' .. upgrade_log_url .. '",null,function(x){' - html[#html+1] = 'try{' - html[#html+1] = 'var r=JSON.parse(x.responseText);' - html[#html+1] = 'var logEl=document.getElementById("setup-log-content");' - html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");' - html[#html+1] = 'if(r.log)logEl.textContent=r.log;' - html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;' - html[#html+1] = 'if(r.state==="running"){' - html[#html+1] = 'statusEl.innerHTML="⏳ 升级进行中...";' - html[#html+1] = '}else if(r.state==="success"){' - html[#html+1] = 'clearInterval(_upgradeTimer);_upgradeTimer=null;' - html[#html+1] = 'ocUpgradeDone(true);' - html[#html+1] = '}else if(r.state==="failed"){' - html[#html+1] = 'clearInterval(_upgradeTimer);_upgradeTimer=null;' - html[#html+1] = 'ocUpgradeDone(false);' - html[#html+1] = '}' - html[#html+1] = '}catch(e){}' - html[#html+1] = '});' - html[#html+1] = '},1500);' - html[#html+1] = '}' - - -- 升级完成处理 - html[#html+1] = 'function ocUpgradeDone(ok){' - html[#html+1] = 'var btn=document.getElementById("btn-do-update");' - html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");' - html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");' - html[#html+1] = 'var actEl=document.getElementById("oc-update-action");' - html[#html+1] = 'if(btn){btn.disabled=false;btn.textContent="⬆️ 立即升级";}' - html[#html+1] = 'resultEl.style.display="block";' - html[#html+1] = 'if(ok){' - html[#html+1] = 'statusEl.innerHTML="✅ 升级完成";' - html[#html+1] = 'resultEl.innerHTML="
"+' - html[#html+1] = '"🎉 升级成功!服务已自动重启。
"+' - html[#html+1] = '"点击下方按钮刷新页面查看最新状态。
"+' - html[#html+1] = '"
";' - html[#html+1] = 'actEl.style.display="none";' - html[#html+1] = '}else{' - html[#html+1] = 'statusEl.innerHTML="❌ 升级失败";' - html[#html+1] = 'resultEl.innerHTML="
"+' - html[#html+1] = '"❌ 升级失败
"+' - html[#html+1] = '"请查看上方日志了解详情。也可在终端查看:cat /tmp/openclaw-upgrade.log
"+' - html[#html+1] = '"
";' - html[#html+1] = '}' - html[#html+1] = '}' - -- ═══ 插件一键升级 ═══ html[#html+1] = 'var _pluginUpgradeTimer=null;' @@ -436,9 +362,9 @@ act.cfgvalue = function(self, section) -- 页面加载时静默检查是否有更新 (仅显示小红点提示) html[#html+1] = '(function(){' html[#html+1] = 'setTimeout(function(){' - html[#html+1] = '(new XHR()).get("' .. check_url .. '?quick=1",null,function(x){' + html[#html+1] = '(new XHR()).get("' .. check_url .. '",null,function(x){' html[#html+1] = 'try{var r=JSON.parse(x.responseText);' - html[#html+1] = 'if(r.has_update||r.plugin_has_update){' + html[#html+1] = 'if(r.plugin_has_update){' html[#html+1] = 'var dot=document.getElementById("update-dot");' html[#html+1] = 'if(dot)dot.style.display="block";' html[#html+1] = '}' diff --git a/luasrc/view/openclaw/status.htm b/luasrc/view/openclaw/status.htm index 17d9980..afba591 100644 --- a/luasrc/view/openclaw/status.htm +++ b/luasrc/view/openclaw/status.htm @@ -75,7 +75,6 @@ 内存占用- 运行时间- Node.js- - OpenClaw- 插件版本- @@ -137,7 +136,6 @@ document.getElementById('oc-st-uptime').textContent = d.uptime || '-'; document.getElementById('oc-st-node').textContent = d.node_version || '未安装'; - document.getElementById('oc-st-ocver').textContent = d.openclaw_version || '未安装'; document.getElementById('oc-st-plugin').textContent = d.plugin_version ? ('v' + d.plugin_version) : '-'; } catch(e) { diff --git a/root/usr/bin/openclaw-env b/root/usr/bin/openclaw-env index f6591e2..43074e1 100755 --- a/root/usr/bin/openclaw-env +++ b/root/usr/bin/openclaw-env @@ -224,7 +224,27 @@ download_node() { rm -rf "/overlay/upper${NODE_BASE}" 2>/dev/null fi ensure_mkdir "$NODE_BASE" - tar xf "$tmp_file" -C "$NODE_BASE" --strip-components=1 + # 兼容 BusyBox tar (不支持 --strip-components) 和 GNU tar + # 方法: 先解压到临时目录,再移动顶层子目录内容到目标目录 + if tar --strip-components=1 -xf "$tmp_file" -C "$NODE_BASE" 2>/dev/null; then + : # GNU tar 成功 + else + # BusyBox tar 回退: 解压到临时目录后手动移动 + local tmp_extract="/tmp/node-extract-$$" + ensure_mkdir "$tmp_extract" + tar xf "$tmp_file" -C "$tmp_extract" + # 找顶层目录 (node-vX.X.X-linux-xxx) + local top_dir + top_dir=$(ls "$tmp_extract" 2>/dev/null | head -1) + if [ -n "$top_dir" ] && [ -d "$tmp_extract/$top_dir" ]; then + cp -a "$tmp_extract/$top_dir/." "$NODE_BASE/" + else + log_error "解压后未找到顶层目录,安装失败" + rm -rf "$tmp_extract" + exit 1 + fi + rm -rf "$tmp_extract" + fi rm -f "$tmp_file" # 验证