mirror of
https://github.com/hotwa/luci-app-openclaw.git
synced 2026-03-31 04:52:33 +00:00
feat(storage): support configurable install root
Add a LuCI install-root input, persist the selected path in UCI, and route install, status, backup, uninstall, and runtime scripts through the configured storage root for new installs. Reference: custom install root flow
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
-- luci-app-openclaw — 基本设置 CBI Model
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local oc_paths = require "openclaw.paths"
|
||||
|
||||
local current_paths = oc_paths.derive_paths(uci:get("openclaw", "main", "install_root"))
|
||||
local current_install_root = current_paths.install_root
|
||||
local current_oc_root = current_paths.oc_root
|
||||
|
||||
m = Map("openclaw", "OpenClaw AI 网关",
|
||||
"OpenClaw 是一个 AI 编程代理网关,支持 GitHub Copilot、Claude、GPT、Gemini 等大模型以及 QQ、Telegram、Discord 等多种消息渠道。")
|
||||
@@ -41,6 +47,7 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '</div>'
|
||||
html[#html+1] = '<div id="action-result" style="margin-top:8px;"></div>'
|
||||
html[#html+1] = '<div id="oc-update-action" style="margin-top:8px;display:none;"></div>'
|
||||
html[#html+1] = '<div style="margin:4px 0 8px;color:#666;font-size:12px;">当前安装根目录: <code>' .. current_install_root .. '</code>,实际运行目录: <code>' .. current_oc_root .. '</code></div>'
|
||||
|
||||
-- 版本选择对话框 (默认隐藏)
|
||||
html[#html+1] = '<div id="oc-setup-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center;">'
|
||||
@@ -59,6 +66,12 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '<div><strong style="color:#333;">🆕 最新版</strong>'
|
||||
html[#html+1] = '<div style="font-size:12px;color:#e36209;margin-top:4px;">⚠️ 安装 npm 上的最新发布版本,可能存在未经验证的兼容性问题。</div>'
|
||||
html[#html+1] = '</div></label>'
|
||||
html[#html+1] = '<div style="padding:14px 16px;border:1px solid #d8dee4;border-radius:8px;background:#fafbfc;">'
|
||||
html[#html+1] = '<label for="oc-install-root" style="display:block;font-weight:600;color:#333;margin-bottom:6px;">📁 安装根目录 / 检测目录</label>'
|
||||
html[#html+1] = '<input id="oc-install-root" type="text" value="' .. current_install_root .. '" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #c9d1d9;border-radius:6px;" placeholder="/mnt/emmc">'
|
||||
html[#html+1] = '<div style="font-size:12px;color:#666;margin-top:6px;line-height:1.6;">填写已挂载的存储路径,例如 <code>/mnt/emmc</code>。OpenClaw 会自动使用 <code><目录>/openclaw/</code> 作为实际安装目录。更换已安装环境的目录不做迁移,如需切换请先卸载后重装。</div>'
|
||||
html[#html+1] = '<div id="oc-install-root-preview" style="font-size:12px;color:#0969da;margin-top:6px;">实际安装目录: <code>' .. current_oc_root .. '</code></div>'
|
||||
html[#html+1] = '</div>'
|
||||
html[#html+1] = '</div>'
|
||||
-- 按钮区
|
||||
html[#html+1] = '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px;">'
|
||||
@@ -82,9 +95,15 @@ act.cfgvalue = function(self, section)
|
||||
|
||||
-- 版本选择对话框逻辑
|
||||
html[#html+1] = 'var _setupTimer=null;'
|
||||
html[#html+1] = 'var _ocLastInstallRoot=' .. string.format("%q", current_install_root) .. ';'
|
||||
html[#html+1] = 'var _ocLastInstallPath=' .. string.format("%q", current_oc_root) .. ';'
|
||||
html[#html+1] = 'function ocNormalizeInstallRoot(v){v=(v||"").replace(/^\\s+|\\s+$/g,"");if(!v)return"/opt";if(v.charAt(0)!=="/")return null;if(/\\s/.test(v))return null;v=v.replace(/\\/+$/,"");return v||"/";}'
|
||||
html[#html+1] = 'function ocGetActualInstallPath(root){return root==="/"?"/openclaw":root+"/openclaw";}'
|
||||
html[#html+1] = 'function ocRefreshInstallRootPreview(){var input=document.getElementById("oc-install-root");var preview=document.getElementById("oc-install-root-preview");if(!input||!preview)return;var root=ocNormalizeInstallRoot(input.value);if(!root){preview.innerHTML="<span style=\\"color:#cf222e;\\">请输入绝对路径,且不要包含空格,例如 <code>/mnt/emmc</code></span>";return;}preview.innerHTML="实际安装目录: <code>"+ocGetActualInstallPath(root)+"</code>";}'
|
||||
html[#html+1] = 'function ocShowSetupDialog(){'
|
||||
html[#html+1] = 'var dlg=document.getElementById("oc-setup-dialog");'
|
||||
html[#html+1] = 'dlg.style.display="flex";'
|
||||
html[#html+1] = 'ocRefreshInstallRootPreview();'
|
||||
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
|
||||
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].value==="stable")radios[i].checked=true;}'
|
||||
html[#html+1] = '}'
|
||||
@@ -93,8 +112,13 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'function ocConfirmSetup(){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
|
||||
html[#html+1] = 'var installRootInput=document.getElementById("oc-install-root");'
|
||||
html[#html+1] = 'var installRoot=ocNormalizeInstallRoot(installRootInput?installRootInput.value:"");'
|
||||
html[#html+1] = 'if(!installRoot){alert("请输入绝对路径,且不要包含空格,例如 /mnt/emmc");return;}'
|
||||
html[#html+1] = '_ocLastInstallRoot=installRoot;'
|
||||
html[#html+1] = '_ocLastInstallPath=ocGetActualInstallPath(installRoot);'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 检测系统配置...";'
|
||||
html[#html+1] = '(new XHR()).get("' .. check_system_url .. '",null,function(x){'
|
||||
html[#html+1] = '(new XHR()).get("' .. check_system_url .. '?install_root="+encodeURIComponent(installRoot),null,function(x){'
|
||||
html[#html+1] = 'try{'
|
||||
html[#html+1] = 'var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
|
||||
@@ -111,19 +135,21 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="🔍 系统配置检测\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="检测目录: "+(r.install_root||installRoot)+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="实际安装: "+(r.oc_root||_ocLastInstallPath)+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="内存: "+r.memory_mb+" MB (需要 ≥ 1024 MB) — "+(r.memory_ok?"✅ 通过":"❌ 不达标")+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="磁盘: "+r.disk_mb+" MB 可用 (需要 ≥ 1536 MB) — "+(r.disk_ok?"✅ 通过":"❌ 不达标")+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="磁盘: "+r.disk_mb+" MB 可用"+(r.disk_path?" [检测路径 "+r.disk_path+"]":"")+" (需要 ≥ 1536 MB) — "+(r.disk_ok?"✅ 通过":"❌ 不达标")+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="\\n";'
|
||||
html[#html+1] = 'if(!r.pass){'
|
||||
html[#html+1] = 'ocCloseSetupDialog();'
|
||||
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 系统配置不满足要求</span>";'
|
||||
html[#html+1] = 'logEl.textContent+="❌ 系统配置不满足要求,安装已终止\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="💡 请升级硬件配置或清理磁盘空间后重试\\n";'
|
||||
html[#html+1] = 'if(r.message)logEl.textContent+="💡 "+r.message+"\\n";'
|
||||
html[#html+1] = 'resultEl.style.display="block";'
|
||||
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
|
||||
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 系统配置不满足要求</strong><br/>"+'
|
||||
html[#html+1] = '"<div style=\\"margin-top:8px;font-size:12px;color:#666;\\">💡 请升级硬件配置或清理磁盘空间后重试。</div></div>";'
|
||||
html[#html+1] = '"<div style=\\"margin-top:8px;font-size:12px;color:#666;\\">💡 "+(r.message||"请升级硬件配置或清理磁盘空间后重试。")+"</div></div>";'
|
||||
html[#html+1] = 'return;'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
|
||||
@@ -133,7 +159,7 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = 'var choice="stable";'
|
||||
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].checked){choice=radios[i].value;break;}}'
|
||||
html[#html+1] = 'var verParam=(choice==="stable")?"stable":"latest";'
|
||||
html[#html+1] = 'ocSetup(verParam,r.memory_mb,r.disk_mb);'
|
||||
html[#html+1] = 'ocSetup(verParam,installRoot);'
|
||||
html[#html+1] = '}catch(e){'
|
||||
html[#html+1] = 'ocCloseSetupDialog();'
|
||||
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
|
||||
@@ -142,16 +168,18 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 安装运行环境 (带实时日志)
|
||||
html[#html+1] = 'function ocSetup(version,mem_mb,disk_mb){'
|
||||
html[#html+1] = 'function ocSetup(version,installRoot){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 安装中...";'
|
||||
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="📦 安装运行环境 ("+((version==="stable")?"稳定版":"最新版")+")\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="安装根目录: "+installRoot+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="实际安装目录: "+ocGetActualInstallPath(installRoot)+"\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
|
||||
html[#html+1] = 'logEl.textContent+="正在启动安装...\\n";'
|
||||
html[#html+1] = '(new XHR()).get("' .. ctl_url .. '?action=setup&version="+encodeURIComponent(version),null,function(x){'
|
||||
html[#html+1] = 'try{JSON.parse(x.responseText);}catch(e){}'
|
||||
html[#html+1] = '(new XHR()).get("' .. ctl_url .. '?action=setup&version="+encodeURIComponent(version)+"&install_root="+encodeURIComponent(installRoot),null,function(x){'
|
||||
html[#html+1] = 'try{var r=JSON.parse(x.responseText);if(r.status&&r.status!=="ok"){logEl.textContent+="❌ "+(r.message||"安装启动失败")+"\\n";ocSetupDone(false,logEl.textContent);return;}}catch(e){}'
|
||||
html[#html+1] = 'ocPollSetupLog();'
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = '}'
|
||||
@@ -217,6 +245,12 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = 'var reasons=[];'
|
||||
html[#html+1] = 'if(!log)return"未知错误,请检查日志。";'
|
||||
html[#html+1] = 'var ll=log.toLowerCase();'
|
||||
html[#html+1] = 'var ocRoot=_ocLastInstallPath||' .. string.format("%q", current_oc_root) .. ';'
|
||||
html[#html+1] = 'var nodeBase=ocRoot+"/node";'
|
||||
html[#html+1] = 'var globalBase=ocRoot+"/global";'
|
||||
html[#html+1] = 'if(log.indexOf("当前已安装在")>=0||log.indexOf("检测目录不存在")>=0||log.indexOf("安装根目录")>=0){'
|
||||
html[#html+1] = 'reasons.push("📁 <b>安装目录配置有误</b> — 请选择已挂载的绝对路径;如果当前环境已经安装在其他目录,请先卸载后再切换。");'
|
||||
html[#html+1] = '}'
|
||||
-- 网络问题
|
||||
html[#html+1] = 'if(ll.indexOf("could not resolve")>=0||ll.indexOf("connection timed out")>=0||ll.indexOf("curl")>=0&&ll.indexOf("fail")>=0||ll.indexOf("wget")>=0&&ll.indexOf("fail")>=0||ll.indexOf("所有镜像均下载失败")>=0){'
|
||||
html[#html+1] = 'reasons.push("🌐 <b>网络连接失败</b> — 无法下载 Node.js。请检查路由器是否能访问外网。<br/> 💡 解决: 检查 DNS 设置和网络连接,或手动指定镜像: <code>NODE_MIRROR=https://npmmirror.com/mirrors/node openclaw-env setup</code>");'
|
||||
@@ -231,15 +265,15 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '}'
|
||||
-- npm 安装失败
|
||||
html[#html+1] = 'if(ll.indexOf("npm err")>=0||ll.indexOf("npm warn")>=0&&ll.indexOf("openclaw 安装验证失败")>=0){'
|
||||
html[#html+1] = 'reasons.push("📦 <b>npm 安装 OpenClaw 失败</b> — npm 包下载或安装出错。<br/> 💡 解决: 尝试手动安装 <code>PATH=/opt/openclaw/node/bin:$PATH npm install -g openclaw@latest --prefix=/opt/openclaw/global</code>");'
|
||||
html[#html+1] = 'reasons.push("📦 <b>npm 安装 OpenClaw 失败</b> — npm 包下载或安装出错。<br/> 💡 解决: 尝试手动安装 <code>PATH="+nodeBase+"/bin:$PATH npm install -g openclaw@latest --prefix="+globalBase+"</code>");'
|
||||
html[#html+1] = '}'
|
||||
-- 权限问题
|
||||
html[#html+1] = 'if(ll.indexOf("permission denied")>=0||ll.indexOf("eacces")>=0){'
|
||||
html[#html+1] = 'reasons.push("🔒 <b>权限不足</b> — 文件或目录权限问题。<br/> 💡 解决: 运行 <code>chown -R openclaw:openclaw /opt/openclaw</code> 或以 root 用户重试。");'
|
||||
html[#html+1] = 'reasons.push("🔒 <b>权限不足</b> — 文件或目录权限问题。<br/> 💡 解决: 运行 <code>chown -R openclaw:openclaw "+ocRoot+"</code> 或以 root 用户重试。");'
|
||||
html[#html+1] = '}'
|
||||
-- tar 解压失败
|
||||
html[#html+1] = 'if(ll.indexOf("tar")>=0&&(ll.indexOf("error")>=0||ll.indexOf("fail")>=0)){'
|
||||
html[#html+1] = 'reasons.push("📂 <b>解压失败</b> — Node.js 安装包可能下载不完整。<br/> 💡 解决: 删除缓存重试 <code>rm -rf /opt/openclaw/node && openclaw-env setup</code>");'
|
||||
html[#html+1] = 'reasons.push("📂 <b>解压失败</b> — Node.js 安装包可能下载不完整。<br/> 💡 解决: 删除缓存重试 <code>rm -rf "+nodeBase+" && openclaw-env setup</code>");'
|
||||
html[#html+1] = '}'
|
||||
-- 验证失败
|
||||
html[#html+1] = 'if(ll.indexOf("安装验证失败")>=0){'
|
||||
@@ -251,6 +285,7 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'return reasons.join("<br/><br/>");'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'var _ocInstallRootInput=document.getElementById("oc-install-root");if(_ocInstallRootInput){_ocInstallRootInput.oninput=ocRefreshInstallRootPreview;_ocInstallRootInput.onchange=ocRefreshInstallRootPreview;}'
|
||||
|
||||
-- 普通服务操作 (restart/stop)
|
||||
html[#html+1] = 'function ocServiceCtl(action){'
|
||||
@@ -384,7 +419,7 @@ act.cfgvalue = function(self, section)
|
||||
|
||||
-- 卸载运行环境
|
||||
html[#html+1] = 'function ocUninstall(){'
|
||||
html[#html+1] = 'if(!confirm("确定要卸载 OpenClaw 运行环境?\\n\\n将删除 Node.js、OpenClaw 程序及配置数据(/opt/openclaw 目录),服务将停止运行。\\n\\n插件本身不会被删除,之后可重新安装运行环境。"))return;'
|
||||
html[#html+1] = 'if(!confirm("确定要卸载 OpenClaw 运行环境?\\n\\n将删除 Node.js、OpenClaw 程序及配置数据(' .. current_oc_root .. ' 目录),服务将停止运行。\\n\\n插件本身不会被删除,之后可重新安装运行环境。"))return;'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-uninstall");'
|
||||
html[#html+1] = 'var el=document.getElementById("action-result");'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在卸载...";'
|
||||
|
||||
Reference in New Issue
Block a user