Sync 2026-03-31 23:53:13

This commit is contained in:
github-actions[bot] 2026-03-31 23:53:13 +08:00
parent 029b517c03
commit be9ee994ab
14 changed files with 280 additions and 81 deletions

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=hickory-dns
PKG_VERSION:=2026.03.31
PKG_RELEASE:=6
PKG_VERSION:=2026.03.31-b57b7ae
PKG_RELEASE:=7
HICKORY_COMMIT:=b57b7aec2c6c18d64b90f01c97595171963e0e56

View File

@ -4,6 +4,37 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
## [2.0.2] - 2026-03-31
### 新增功能
#### 支持自定义安装路径
- **功能描述**: 用户现在可以将 OpenClaw 运行环境安装到自定义路径(如第二块硬盘 `/mnt/data`
- **LuCI 界面**: 安装对话框增加自定义路径输入框,支持实时检测目标路径的可用空间
- **系统检测**: 安装前系统配置检测会检测自定义路径的磁盘空间,而非默认的 `/opt`
- **服务状态**: 状态 API 返回当前安装路径信息
- **最小空间提示**: 安装界面提示最小需要 2GB 可用空间
#### 技术实现
- 新增 UCI 配置项 `openclaw.main.install_path`,默认值为 `/opt`
- 程序会在用户指定路径下自动创建 `openclaw` 目录进行安装
- 例如:用户输入 `/mnt/data`,实际安装路径为 `/mnt/data/openclaw`
- 所有脚本和配置文件支持从 UCI 读取自定义路径:
- `openclaw-env`: 通过 `OC_INSTALL_PATH` 环境变量或 UCI 配置
- `init.d/openclaw`: 启动时从 UCI 读取路径
- `oc-config.sh`: 支持自定义路径的环境变量
- `profile.d/openclaw.sh`: SSH 环境变量支持
- `uci-defaults/99-openclaw`: 首次安装初始化支持
### 感谢
感谢 @hotwa 提供的修改思路和建议。
---
## [2.0.1] - 2026-03-30
### 适配 OpenClaw v2026.3.28

View File

@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclaw
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
PKG_RELEASE:=4
PKG_RELEASE:=5
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
PKG_LICENSE:=GPL-3.0

View File

@ -1 +1 @@
2.0.1
2.0.2

View File

@ -57,6 +57,9 @@ function action_status()
local port = uci:get("openclaw", "main", "port") or "18789"
local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
local enabled = uci:get("openclaw", "main", "enabled") or "0"
local install_path_uci = uci:get("openclaw", "main", "install_path") or "/opt"
-- 实际安装路径会在用户路径下创建 openclaw 子目录
local install_path = install_path_uci .. "/openclaw"
-- 验证端口值为纯数字,防止命令注入
if not port:match("^%d+$") then port = "18789" end
@ -66,6 +69,7 @@ function action_status()
enabled = enabled,
port = port,
pty_port = pty_port,
install_path = install_path,
gateway_running = false,
gateway_starting = false,
pty_running = false,
@ -75,6 +79,7 @@ function action_status()
node_version = "",
oc_version = "",
plugin_version = "",
disk_free = "",
}
-- 插件版本
@ -86,8 +91,8 @@ function action_status()
-- 安装方式检测 (离线 / 在线)
-- 检查 Node.js
local node_bin = "/opt/openclaw/node/bin/node"
-- 检查 Node.js (使用自定义安装路径)
local node_bin = install_path .. "/node/bin/node"
local f = io.open(node_bin, "r")
if f then
f:close()
@ -95,11 +100,11 @@ function action_status()
result.node_version = node_ver
end
-- OpenClaw 版本 (从 package.json 读取)
-- OpenClaw 版本 (从 package.json 读取,使用自定义安装路径)
local oc_dirs = {
"/opt/openclaw/global/lib/node_modules/openclaw",
"/opt/openclaw/global/node_modules/openclaw",
"/opt/openclaw/node/lib/node_modules/openclaw",
install_path .. "/global/lib/node_modules/openclaw",
install_path .. "/global/node_modules/openclaw",
install_path .. "/node/lib/node_modules/openclaw",
}
for _, d in ipairs(oc_dirs) do
local pf = io.open(d .. "/package.json", "r")
@ -131,8 +136,8 @@ function action_status()
local pty_check = sys.exec("netstat -tulnp 2>/dev/null | grep -c ':" .. pty_port .. " ' || echo 0"):gsub("%s+", "")
result.pty_running = (tonumber(pty_check) or 0) > 0
-- 读取当前活跃模型
local config_file = "/opt/openclaw/data/.openclaw/openclaw.json"
-- 读取当前活跃模型 (使用自定义安装路径)
local config_file = install_path .. "/data/.openclaw/openclaw.json"
local cf = io.open(config_file, "r")
if cf then
local content = cf:read("*a")
@ -192,6 +197,13 @@ function action_status()
end
end
-- 磁盘剩余空间 (检测安装路径所在分区)
local install_parent = install_path:match("^(.*)/[^/]*$") or "/"
local df_output = sys.exec("df -h " .. install_parent .. " 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
if df_output and df_output ~= "" then
result.disk_free = df_output
end
http.prepare_content("application/json")
http.write_json(result)
end
@ -225,6 +237,8 @@ function action_service_ctl()
sys.exec("rm -f /tmp/openclaw-setup.log /tmp/openclaw-setup.pid /tmp/openclaw-setup.exit")
-- 获取用户选择的版本 (stable=指定版本, latest=最新版)
local version = http.formvalue("version") or ""
-- 获取自定义安装路径 (用户输入的是基础路径,如 /opt 或 /mnt/data)
local install_path = http.formvalue("install_path") or ""
local env_prefix = ""
if version == "stable" then
-- 稳定版: 读取 openclaw-env 中定义的 OC_TESTED_VERSION
@ -238,6 +252,17 @@ function action_service_ctl()
env_prefix = "OC_VERSION=" .. version .. " "
end
end
-- 处理自定义安装路径
if install_path ~= "" and install_path ~= "/opt" then
-- 安全检查: 路径不能包含危险字符
install_path = install_path:gsub("[`$;&|<>]", "")
install_path = install_path:gsub("/+$", "")
if install_path ~= "" then
-- 保存到 UCI 配置 (保存用户输入的基础路径)
sys.exec("uci set openclaw.main.install_path='" .. install_path .. "'; uci commit openclaw 2>/dev/null")
env_prefix = env_prefix .. "OC_INSTALL_PATH='" .. install_path .. "' "
end
end
-- 后台安装,成功后自动启用并启动服务
-- 注: openclaw-env 脚本有 set -einit_openclaw 中的非关键失败不应阻止启动
sys.exec("( " .. env_prefix .. "/usr/bin/openclaw-env setup > /tmp/openclaw-setup.log 2>&1; RC=$?; echo $RC > /tmp/openclaw-setup.exit; if [ $RC -eq 0 ]; then uci set openclaw.main.enabled=1; uci commit openclaw; /etc/init.d/openclaw enable 2>/dev/null; sleep 1; /etc/init.d/openclaw start >> /tmp/openclaw-setup.log 2>&1; fi ) & echo $! > /tmp/openclaw-setup.pid")
@ -368,6 +393,12 @@ end
function action_uninstall()
local http = require "luci.http"
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
-- 获取安装路径
local install_path_uci = uci:get("openclaw", "main", "install_path") or "/opt"
-- 实际安装路径
local install_path = install_path_uci .. "/openclaw"
-- 停止服务
sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1")
@ -376,7 +407,7 @@ function action_uninstall()
-- 设置 UCI enabled=0
sys.exec("uci set openclaw.main.enabled=0; uci commit openclaw 2>/dev/null")
-- 删除 Node.js + OpenClaw 运行环境 (包含所有插件: qqbot, 飞书等)
sys.exec("rm -rf /opt/openclaw")
sys.exec("rm -rf " .. install_path)
-- 清理旧数据迁移后可能残留的目录
sys.exec("rm -rf /root/.openclaw 2>/dev/null")
-- 清理临时文件
@ -389,7 +420,7 @@ function action_uninstall()
http.prepare_content("application/json")
http.write_json({
status = "ok",
message = "运行环境已卸载。已清理: Node.js 运行环境 (/opt/openclaw)、所有插件 (qqbot/飞书等)、旧数据目录 (/root/.openclaw)、临时文件、LuCI 缓存。"
message = "运行环境已卸载。已清理: Node.js 运行环境 (" .. install_path .. ")、所有插件 (qqbot/飞书等)、旧数据目录 (/root/.openclaw)、临时文件、LuCI 缓存。"
})
end
@ -544,16 +575,21 @@ end
function action_backup()
local http = require "luci.http"
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local action = http.formvalue("action") or "create"
local node_bin = "/opt/openclaw/node/bin/node"
-- 获取安装路径
local install_path_uci = uci:get("openclaw", "main", "install_path") or "/opt"
-- 实际安装路径
local install_path = install_path_uci .. "/openclaw"
local node_bin = install_path .. "/node/bin/node"
local oc_entry = ""
-- 查找 openclaw 入口
-- 查找 openclaw 入口 (使用自定义安装路径)
local search_dirs = {
"/opt/openclaw/global/lib/node_modules/openclaw",
"/opt/openclaw/global/node_modules/openclaw",
"/opt/openclaw/node/lib/node_modules/openclaw",
install_path .. "/global/lib/node_modules/openclaw",
install_path .. "/global/node_modules/openclaw",
install_path .. "/node/lib/node_modules/openclaw",
}
for _, d in ipairs(search_dirs) do
if nixio.fs.stat(d .. "/openclaw.mjs", "type") then
@ -572,14 +608,15 @@ function action_backup()
end
local env_prefix = string.format(
"HOME=/opt/openclaw/data OPENCLAW_HOME=/opt/openclaw/data " ..
"OPENCLAW_STATE_DIR=/opt/openclaw/data/.openclaw " ..
"OPENCLAW_CONFIG_PATH=/opt/openclaw/data/.openclaw/openclaw.json " ..
"PATH=/opt/openclaw/node/bin:/opt/openclaw/global/bin:$PATH "
"HOME=%s/data OPENCLAW_HOME=%s/data " ..
"OPENCLAW_STATE_DIR=%s/data/.openclaw " ..
"OPENCLAW_CONFIG_PATH=%s/data/.openclaw/openclaw.json " ..
"PATH=%s/node/bin:%s/global/bin:$PATH ",
install_path, install_path, install_path, install_path, install_path, install_path
)
-- 备份目录 (openclaw backup create 输出到 CWD需要 cd)
local backup_dir = "/opt/openclaw/data/.openclaw/backups"
local backup_dir = install_path .. "/data/.openclaw/backups"
local cd_prefix = "mkdir -p " .. backup_dir .. " && cd " .. backup_dir .. " && "
-- ── 辅助: 解析单个备份文件的 manifest 信息 ──
@ -646,7 +683,7 @@ function action_backup()
end
local output = sys.exec(backup_cmd)
-- 完整备份可能输出到 HOME移动到 backup_dir
sys.exec("mv /opt/openclaw/data/*-openclaw-backup.tar.gz " .. backup_dir .. "/ 2>/dev/null")
sys.exec("mv " .. install_path .. "/data/*-openclaw-backup.tar.gz " .. backup_dir .. "/ 2>/dev/null")
-- 提取备份文件路径
local backup_path = output:match("([%S]+%.tar%.gz)")
http.prepare_content("application/json")
@ -692,7 +729,7 @@ function action_backup()
http.write_json({ status = "error", message = "未找到备份文件,请先创建备份" })
return
end
local oc_data_dir = "/opt/openclaw/data/.openclaw"
local oc_data_dir = install_path .. "/data/.openclaw"
local config_path = oc_data_dir .. "/openclaw.json"
-- 1) 先验证备份中的 openclaw.json 是否有效
@ -811,10 +848,20 @@ end
-- 系统配置检测 API (安装前检测)
-- 检测内存和磁盘空间是否满足最低要求
-- 要求: 内存 > 1GB, 磁盘可用空间 > 2GB (OpenClaw v2026.3.28+ 包体积约 200MB)
-- 支持自定义安装路径,检测对应路径的磁盘空间
-- ═══════════════════════════════════════════
function action_check_system()
local http = require "luci.http"
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
-- 获取自定义安装路径 (用户输入的是基础路径,如 /opt 或 /mnt/data)
local install_path = http.formvalue("install_path") or uci:get("openclaw", "main", "install_path") or "/opt"
-- 安全检查: 路径不能包含危险字符
install_path = install_path:gsub("[`$;&|<>]", "")
-- 确保路径不以 / 结尾
install_path = install_path:gsub("/+$", "")
if install_path == "" then install_path = "/opt" end
-- 最低要求配置 (v2026.3.28: 包体积 ~200MB, 建议 2GB 可用空间)
local MIN_MEMORY_MB = 1024 -- 1GB
@ -826,6 +873,8 @@ function action_check_system()
disk_mb = 0,
disk_ok = false,
disk_path = "",
install_path = install_path,
disk_free_str = "",
pass = false,
message = ""
}
@ -845,14 +894,40 @@ function action_check_system()
result.memory_ok = result.memory_mb >= MIN_MEMORY_MB
-- 检测磁盘可用空间
-- 优先检测 /opt 所在分区,如果 /opt 不存在则检测 /overlay 或 /
local disk_paths = {"/opt", "/overlay", "/"}
for _, path in ipairs(disk_paths) do
local df_output = sys.exec("df -m " .. path .. " 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
-- 策略: 从目标路径开始,逐级向上找到存在的挂载点进行检测
-- 例如: /mnt/data -> /mnt/data (若存在) -> /mnt (若存在) -> /
local function find_mount_point(path)
-- 如果路径存在,直接返回
if nixio.fs.stat(path, "type") then
return path
end
-- 逐级向上查找
while path ~= "/" and path ~= "" do
path = path:match("^(.*)/[^/]*$") or "/"
if path == "" then path = "/" end
if nixio.fs.stat(path, "type") then
return path
end
end
return "/"
end
local disk_check_path = find_mount_point(install_path)
-- 使用 df 检测磁盘空间
local df_output = sys.exec("df -m " .. disk_check_path .. " 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
if df_output and df_output ~= "" and tonumber(df_output) then
result.disk_mb = tonumber(df_output)
result.disk_path = disk_check_path
-- 获取可读的磁盘空间格式
result.disk_free_str = sys.exec("df -h " .. disk_check_path .. " 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
else
-- 如果检测失败,尝试检测根分区
df_output = sys.exec("df -m / 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
if df_output and df_output ~= "" and tonumber(df_output) then
result.disk_mb = tonumber(df_output)
result.disk_path = path
break
result.disk_path = "/"
result.disk_free_str = sys.exec("df -h / 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
end
end
result.disk_ok = result.disk_mb >= MIN_DISK_MB

View File

@ -42,9 +42,9 @@ act.cfgvalue = function(self, section)
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 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;">'
html[#html+1] = '<div style="background:#fff;border-radius:12px;padding:24px 28px;max-width:480px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.2);">'
html[#html+1] = '<div style="background:#fff;border-radius:12px;padding:24px 28px;max-width:520px;width:92%;box-shadow:0 8px 32px rgba(0,0,0,0.2);">'
html[#html+1] = '<h3 style="margin:0 0 16px 0;font-size:16px;color:#333;">📦 选择安装版本</h3>'
html[#html+1] = '<div style="display:flex;flex-direction:column;gap:12px;">'
-- 稳定版选项
@ -60,6 +60,15 @@ act.cfgvalue = function(self, section)
html[#html+1] = '<div style="font-size:12px;color:#e36209;margin-top:4px;">⚠️ 安装 npm 上的最新发布版本,可能存在未经验证的兼容性问题。</div>'
html[#html+1] = '</div></label>'
html[#html+1] = '</div>'
-- 自定义安装路径
html[#html+1] = '<div style="margin-top:16px;padding-top:14px;border-top:1px solid #eee;">'
html[#html+1] = '<div style="font-weight:600;font-size:13px;color:#333;margin-bottom:8px;">📂 安装路径</div>'
html[#html+1] = '<div style="display:flex;gap:8px;align-items:center;">'
html[#html+1] = '<input type="text" id="oc-install-path" value="/opt" style="flex:1;padding:8px 12px;border:1px solid #d0d7de;border-radius:6px;font-size:13px;" placeholder="/opt">'
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="ocCheckInstallPath()" style="font-size:12px;padding:4px 10px;">检测空间</button>'
html[#html+1] = '</div>'
html[#html+1] = '<div id="oc-path-info" style="font-size:11px;color:#666;margin-top:6px;">💡 程序将在此路径下创建 openclaw 目录进行安装。最小需要 2GB 可用空间。如安装在第二块硬盘,请确保硬盘已挂载。</div>'
html[#html+1] = '</div>'
-- 按钮区
html[#html+1] = '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px;">'
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="ocCloseSetupDialog()" style="min-width:80px;">取消</button>'
@ -79,6 +88,7 @@ act.cfgvalue = function(self, section)
-- JavaScript
html[#html+1] = '<script type="text/javascript">'
html[#html+1] = 'console.log("OpenClaw JS loading...");'
-- 版本选择对话框逻辑
html[#html+1] = 'var _setupTimer=null;'
@ -91,10 +101,30 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'function ocCloseSetupDialog(){'
html[#html+1] = 'document.getElementById("oc-setup-dialog").style.display="none";'
html[#html+1] = '}'
-- 检测自定义安装路径的磁盘空间
html[#html+1] = 'function ocCheckInstallPath(){'
html[#html+1] = 'var pathEl=document.getElementById("oc-install-path");'
html[#html+1] = 'var infoEl=document.getElementById("oc-path-info");'
html[#html+1] = 'var path=pathEl.value.trim();'
html[#html+1] = 'if(!path){path="/opt";pathEl.value=path;}'
html[#html+1] = 'infoEl.innerHTML="⏳ 正在检测空间...";'
html[#html+1] = '(new XHR()).get("' .. check_system_url .. '?install_path="+encodeURIComponent(path),null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.disk_ok){'
html[#html+1] = 'infoEl.innerHTML="<span style=\\"color:#1a7f37;\\">✅ 可用空间: "+r.disk_free_str+" (检测路径: "+r.disk_path+")</span>";'
html[#html+1] = '}else{'
html[#html+1] = 'infoEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 空间不足: "+r.disk_mb+" MB 可用,需要 ≥ 2048 MB (检测路径: "+r.disk_path+")</span>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){infoEl.innerHTML="<span style=\\"color:#e36209;\\">⚠️ 检测失败</span>";}'
html[#html+1] = '});'
html[#html+1] = '}'
html[#html+1] = 'function ocConfirmSetup(){'
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
html[#html+1] = 'var pathEl=document.getElementById("oc-install-path");'
html[#html+1] = 'var installPath=pathEl.value.trim()||"/opt";'
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_path="+encodeURIComponent(installPath),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,6 +141,7 @@ 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_path+"\\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 可用 (需要 ≥ 2048 MB) — "+(r.disk_ok?"✅ 通过":"❌ 不达标")+"\\n";'
html[#html+1] = 'logEl.textContent+="\\n";'
@ -133,7 +164,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,r.memory_mb,r.disk_mb,r.install_path);'
html[#html+1] = '}catch(e){'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
@ -141,16 +172,17 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}});'
html[#html+1] = '}'
-- 安装运行环境 (带实时日志)
html[#html+1] = 'function ocSetup(version,mem_mb,disk_mb){'
-- 安装运行环境 (带实时日志,支持自定义路径)
html[#html+1] = 'function ocSetup(version,mem_mb,disk_mb,install_path){'
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+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="安装路径: "+install_path+"\\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] = '(new XHR()).get("' .. ctl_url .. '?action=setup&version="+encodeURIComponent(version)+"&install_path="+encodeURIComponent(install_path),null,function(x){'
html[#html+1] = 'try{JSON.parse(x.responseText);}catch(e){}'
html[#html+1] = 'ocPollSetupLog();'
html[#html+1] = '});'
@ -231,15 +263,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/>&nbsp;&nbsp;💡 解决: 尝试手动安装 <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/>&nbsp;&nbsp;💡 解决: 尝试手动安装 <code>openclaw-env setup</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/>&nbsp;&nbsp;💡 解决: 运行 <code>chown -R openclaw:openclaw /opt/openclaw</code> 或以 root 用户重试。");'
html[#html+1] = 'reasons.push("🔒 <b>权限不足</b> — 文件或目录权限问题。<br/>&nbsp;&nbsp;💡 解决: 运行 <code>openclaw-env setup</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/>&nbsp;&nbsp;💡 解决: 删除缓存重试 <code>rm -rf /opt/openclaw/node && openclaw-env setup</code>");'
html[#html+1] = 'reasons.push("📂 <b>解压失败</b> — Node.js 安装包可能下载不完整。<br/>&nbsp;&nbsp;💡 解决: 删除缓存重试 <code>openclaw-env setup</code>");'
html[#html+1] = '}'
-- 验证失败
html[#html+1] = 'if(ll.indexOf("安装验证失败")>=0){'
@ -384,7 +416,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 程序及配置数据,服务将停止运行。\\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="⏳ 正在卸载...";'
@ -400,6 +432,7 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}else{el.innerHTML="<span style=\\"color:red\\">❌ "+(r.message||"卸载失败")+"</span>";}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 请求失败</span>";}'
html[#html+1] = '});}'
html[#html+1] = 'console.log("OpenClaw JS first block loaded");'
-- ═══ 备份/恢复 对话框 + 功能 (v2026.3.8+ openclaw backup) ═══
local backup_url = luci.dispatcher.build_url("admin", "services", "openclaw", "backup")

View File

@ -78,6 +78,8 @@
<tr><td>Node.js</td><td id="oc-st-node">-</td></tr>
<tr><td>OpenClaw</td><td id="oc-st-oc-ver">-</td></tr>
<tr><td>插件版本</td><td id="oc-st-plugin">-</td></tr>
<tr><td>安装路径</td><td id="oc-st-path">-</td></tr>
<tr><td>剩余空间</td><td id="oc-st-disk">-</td></tr>
</table>
</div>
</div>
@ -147,6 +149,22 @@
document.getElementById('oc-st-node').textContent = d.node_version || '未安装';
document.getElementById('oc-st-oc-ver').textContent = d.oc_version ? ('v' + d.oc_version) : '未安装';
document.getElementById('oc-st-plugin').textContent = d.plugin_version ? ('v' + d.plugin_version) : '-';
// 安装路径和剩余空间
var pathEl = document.getElementById('oc-st-path');
if (d.install_path) {
pathEl.innerHTML = '<code style="padding:2px 8px;background:#f0f3f6;border-radius:4px;font-size:12px;">' + d.install_path + '</code>';
} else {
pathEl.textContent = '-';
}
// 剩余空间
var diskEl = document.getElementById('oc-st-disk');
if (d.disk_free) {
diskEl.innerHTML = '<span style="color:#1a7f37;font-weight:500;">' + d.disk_free + '</span> 可用';
} else {
diskEl.textContent = '-';
}
} catch(e) {
document.getElementById('oc-st-status').innerHTML = '<span class="oc-badge oc-badge-unknown">查询失败</span>';
}

View File

@ -4,3 +4,4 @@ config openclaw 'main'
option bind 'lan'
option token ''
option pty_port '18793'
option install_path '/opt'

View File

@ -10,14 +10,21 @@ EXTRA_HELP=" setup 下载 Node.js 并安装 OpenClaw
status_service 显示服务状态
restart_gateway 仅重启 Gateway 实例 (不影响 Web PTY)"
NODE_BASE="/opt/openclaw/node"
OC_GLOBAL="/opt/openclaw/global"
OC_DATA="/opt/openclaw/data"
# ── 安装路径 (支持自定义) ──
# 从 UCI 配置读取自定义路径,默认为 /opt
# 程序会在此路径下自动创建 openclaw 目录
OC_BASE_PATH="$(uci -q get openclaw.main.install_path || echo '/opt')"
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
NODE_BASE="${OC_INSTALL_PATH}/node"
OC_GLOBAL="${OC_INSTALL_PATH}/global"
OC_DATA="${OC_INSTALL_PATH}/data"
# ── OverlayFS 兼容性修复 ──
# Docker bind mount (/overlay/upper/opt/docker) 会导致 /opt 不可写
# 解决: bind mount upper 层的 /opt 到合并视图的 /opt
# 注意: 仅当基础路径为 /opt 时才需要此修复
_oc_fix_opt() {
[ "$OC_BASE_PATH" != "/opt" ] && return 0
mkdir -p /opt/openclaw/.probe 2>/dev/null && { rmdir /opt/openclaw/.probe 2>/dev/null; return 0; }
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null

View File

@ -2,12 +2,17 @@
# ============================================================================
# luci-app-openclaw — 全局环境变量
# 仅在 Node.js 已安装时生效,为 SSH 登录用户提供正确的运行环境
# 解决 Issue #42: 统一配置文件路径,避免 /root/.openclaw 与 /opt/openclaw/data/.openclaw 混乱
# 解决 Issue #42: 统一配置文件路径,避免 /root/.openclaw 与安装路径混乱
# 支持自定义安装路径
# ============================================================================
NODE_BASE="/opt/openclaw/node"
OC_GLOBAL="/opt/openclaw/global"
OC_DATA="/opt/openclaw/data"
# 从 UCI 配置读取自定义安装路径
# 用户配置的是基础路径,程序会在此路径下创建 openclaw 目录
OC_BASE_PATH="$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')"
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
NODE_BASE="${OC_INSTALL_PATH}/node"
OC_GLOBAL="${OC_INSTALL_PATH}/global"
OC_DATA="${OC_INSTALL_PATH}/data"
# 检查 Node.js 是否已安装
[ -x "${NODE_BASE}/bin/node" ] || return 0

View File

@ -1,11 +1,16 @@
#!/bin/sh
# luci-app-openclaw — 首次安装/升级初始化脚本
# 获取安装路径 (支持自定义)
# 用户配置的是基础路径,程序会在此路径下创建 openclaw 目录
OC_BASE_PATH="$(uci -q get openclaw.main.install_path || echo '/opt')"
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
# ── v1.0.16: 清理错误路径下的配置文件 (Issue #42) ──
# 用户在 SSH 中直接运行 openclaw 命令时,可能创建了 /root/.openclaw/ 目录
# 需要迁移数据并清理,避免路径混乱
if [ -d "/root/.openclaw" ]; then
OC_DATA="/opt/openclaw/data"
OC_DATA="${OC_INSTALL_PATH}/data"
# 迁移 skills 目录 (如果存在且目标不存在)
if [ -d "/root/.openclaw/skills" ] && [ ! -d "${OC_DATA}/.openclaw/skills" ]; then
mkdir -p "${OC_DATA}/.openclaw"
@ -41,7 +46,7 @@ if ! id openclaw >/dev/null 2>&1; then
done
# OpenWrt 方式:直接写入 /etc/passwd 和 /etc/shadow
if ! grep -q '^openclaw:' /etc/passwd 2>/dev/null; then
echo "openclaw:x:${OC_UID}:${OC_GID}:openclaw:/opt/openclaw/data:/bin/false" >> /etc/passwd
echo "openclaw:x:${OC_UID}:${OC_GID}:openclaw:${OC_INSTALL_PATH}/data:/bin/false" >> /etc/passwd
fi
if ! grep -q '^openclaw:' /etc/shadow 2>/dev/null; then
echo 'openclaw:x:0:0:99999:7:::' >> /etc/shadow
@ -52,20 +57,22 @@ if ! id openclaw >/dev/null 2>&1; then
fi
# 创建数据目录
# ── OverlayFS 兼容: Docker bind mount 可能导致 /opt 不可写 ──
if ! mkdir -p /opt/openclaw/.probe 2>/dev/null; then
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null
# ── OverlayFS 兼容: Docker bind mount 可能导致 /opt 不可写 (仅当基础路径为 /opt 时) ──
if [ "$OC_BASE_PATH" = "/opt" ]; then
if ! mkdir -p /opt/openclaw/.probe 2>/dev/null; then
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null
fi
rmdir /opt/openclaw/.probe 2>/dev/null
else
rmdir /opt/openclaw/.probe 2>/dev/null
fi
rmdir /opt/openclaw/.probe 2>/dev/null
else
rmdir /opt/openclaw/.probe 2>/dev/null
fi
mkdir -p /opt/openclaw/data/.openclaw
mkdir -p /opt/openclaw/node
mkdir -p /opt/openclaw/global
chown -R openclaw:openclaw /opt/openclaw 2>/dev/null || true
mkdir -p "${OC_INSTALL_PATH}/data/.openclaw"
mkdir -p "${OC_INSTALL_PATH}/node"
mkdir -p "${OC_INSTALL_PATH}/global"
chown -R openclaw:openclaw "${OC_INSTALL_PATH}" 2>/dev/null || true
# 生成随机 Token (如果尚未设置)
CURRENT_TOKEN=$(uci -q get openclaw.main.token)

View File

@ -22,15 +22,25 @@ NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
OC_TESTED_VERSION="2026.3.28"
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
OC_VERSION="${OC_VERSION:-}"
NODE_BASE="/opt/openclaw/node"
OC_GLOBAL="/opt/openclaw/global"
OC_DATA="/opt/openclaw/data"
# ── 安装路径 (支持自定义) ──
# 从 UCI 配置读取自定义路径,默认为 /opt
# 用户可在 LuCI 界面或通过 uci set openclaw.main.install_path=/mnt/data 设置
# 程序会在此路径下自动创建 openclaw 目录
OC_BASE_PATH="${OC_INSTALL_PATH:-$(uci -q get openclaw.main.install_path || echo '/opt')}"
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
NODE_BASE="${OC_INSTALL_PATH}/node"
OC_GLOBAL="${OC_INSTALL_PATH}/global"
OC_DATA="${OC_INSTALL_PATH}/data"
# ── OverlayFS 兼容性修复 ──
# iStoreOS/OpenWrt 上 Docker 的 bind mount (/overlay/upper/opt/docker)
# 会导致 OverlayFS 合并视图中 /opt 完全不可写 (mkdir 报 "Directory not empty")。
# 解决方案: 将 /overlay/upper/opt bind mount 到 /opt绕过 OverlayFS 冲突。
# 注意: 仅当基础路径为 /opt 时才需要此修复
_oc_fix_opt() {
# 如果基础路径不是 /opt无需修复
[ "$OC_BASE_PATH" != "/opt" ] && return 0
# 如果 /opt 可正常写入,无需修复
if mkdir -p /opt/openclaw/.probe 2>/dev/null; then
rmdir /opt/openclaw/.probe 2>/dev/null
@ -97,7 +107,7 @@ find_oc_entry() {
${OC_GLOBAL}/node_modules/openclaw
${NODE_BASE}/lib/node_modules/openclaw"
# 添加 pnpm 版本化目录 (如 /opt/openclaw/global/5/node_modules/openclaw)
# 添加 pnpm 版本化目录 (如 $OC_GLOBAL/5/node_modules/openclaw)
for ver_dir in "${OC_GLOBAL}"/*/node_modules/openclaw; do
[ -d "$ver_dir" ] 2>/dev/null && search_dirs="$search_dirs
$ver_dir"
@ -439,7 +449,7 @@ do_setup() {
else
echo " OpenClaw: 最新版"
fi
echo " 安装路径: ${NODE_BASE}"
echo " 安装路径: ${OC_INSTALL_PATH}"
echo " 数据路径: ${OC_DATA}"
echo ""
@ -493,10 +503,11 @@ do_check() {
echo " 配置: 未初始化"
fi
# 磁盘使用
# 磁盘使用 (检测实际安装路径)
local used
used=$(du -sh /opt/openclaw 2>/dev/null | awk '{print $1}')
used=$(du -sh "$OC_INSTALL_PATH" 2>/dev/null | awk '{print $1}')
echo " 磁盘: ${used:-N/A}"
echo " 安装路径: ${OC_INSTALL_PATH}"
# libc 类型
echo " C库: $(detect_libc)"
@ -651,7 +662,7 @@ do_setup_offline() {
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " 架构: $(uname -m)"
echo " 安装路径: ${NODE_BASE}"
echo " 安装路径: ${OC_INSTALL_PATH}"
echo " 数据路径: ${OC_DATA}"
echo " 离线包: ${offline_dir}"
echo ""

View File

@ -28,10 +28,14 @@ get_pid_by_port() {
fi
}
# ── 路径 (OpenWrt 适配) ──
NODE_BASE="${NODE_BASE:-/opt/openclaw/node}"
OC_GLOBAL="${OC_GLOBAL:-/opt/openclaw/global}"
OC_DATA="${OC_DATA:-/opt/openclaw/data}"
# ── 路径 (OpenWrt 适配,支持自定义安装路径) ──
# 从 UCI 配置读取自定义路径,环境变量可覆盖
# 用户配置的是基础路径,程序会在此路径下创建 openclaw 目录
OC_BASE_PATH="${OC_INSTALL_PATH:-$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')}"
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
NODE_BASE="${NODE_BASE:-${OC_INSTALL_PATH}/node}"
OC_GLOBAL="${OC_GLOBAL:-${OC_INSTALL_PATH}/global}"
OC_DATA="${OC_DATA:-${OC_INSTALL_PATH}/data}"
NODE_BIN="${NODE_BASE}/bin/node"
OC_STATE_DIR="${OC_DATA}/.openclaw"
CONFIG_FILE="${OC_STATE_DIR}/openclaw.json"
@ -99,7 +103,7 @@ json_set() {
local parent_dir="$(dirname "$CONFIG_FILE")"
if ! mkdir -p "$parent_dir" 2>/dev/null; then
echo "ERROR: 无法创建配置目录 $parent_dir" >&2
echo "HINT: 请检查 /opt/openclaw/data 是否存在且有写权限" >&2
echo "HINT: 请检查 $OC_DATA 是否存在且有写权限" >&2
return 1
fi
@ -2428,7 +2432,7 @@ backup_restore_menu() {
# 提取 payload 到根目录 (还原到原始绝对路径)
tar -xzf "$latest" --strip-components=3 -C / "${backup_name}/payload/posix/" 2>&1
# 修复权限
chown -R openclaw:openclaw /opt/openclaw/data/.openclaw 2>/dev/null
chown -R openclaw:openclaw "$OC_STATE_DIR" 2>/dev/null
echo -e " ${GREEN}✅ 配置和数据已完整恢复!原配置已保存为 openclaw.json.pre-restore${NC}"
echo ""
prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart

View File

@ -17,9 +17,16 @@ const os = require('os');
// ── 配置 (OpenWrt 适配) ──
const PORT = parseInt(process.env.OC_CONFIG_PORT || '18793', 10);
const HOST = process.env.OC_CONFIG_HOST || '0.0.0.0'; // token 认证保护,可安全绑定所有接口
const NODE_BASE = process.env.NODE_BASE || '/opt/openclaw/node';
const OC_GLOBAL = process.env.OC_GLOBAL || '/opt/openclaw/global';
const OC_DATA = process.env.OC_DATA || '/opt/openclaw/data';
// 从 UCI 读取安装路径,默认为 /opt/openclaw
const { execSync } = require('child_process');
let installPath = '/opt/openclaw';
try {
const uciPath = execSync('uci -q get openclaw.main.install_path 2>/dev/null', { encoding: 'utf8', timeout: 3000 }).trim();
if (uciPath) installPath = uciPath + '/openclaw';
} catch {}
const NODE_BASE = process.env.NODE_BASE || installPath + '/node';
const OC_GLOBAL = process.env.OC_GLOBAL || installPath + '/global';
const OC_DATA = process.env.OC_DATA || installPath + '/data';
const SCRIPT_PATH = process.env.OC_CONFIG_SCRIPT || '/usr/share/openclaw/oc-config.sh';
const SSL_CERT = '/etc/uhttpd.crt';
const SSL_KEY = '/etc/uhttpd.key';