mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 01:35:13 +08:00
🍉 Sync 2026-05-16 08:37:13
This commit is contained in:
1 parent
8c1c3dd8ec
commit
8ed4c2a2a5
32 files changed
+1145
-234
No files matched your search
@@ -4,6 +4,53 @@
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
|
||||
## [2.0.5] - 2026-05-15
|
||||
|
||||
### 适配 OpenClaw v2026.5.12
|
||||
|
||||
- `OC_TESTED_VERSION` 更新为 `2026.5.12`,默认稳定版固定到 npm `latest` 稳定标签。
|
||||
- 保持 Node.js 默认运行时 `22.16.0`,安装后按 OpenClaw `engines.node` 最低要求执行强校验;版本过低会直接失败并输出明确日志。
|
||||
- 文档补充 `2026.5.14-beta.2` 不作为默认稳定版的说明。
|
||||
|
||||
### 修复权限与路径问题
|
||||
|
||||
- 保留公开 UCI 字段 `openclaw.main.install_path`,新增统一路径规范化逻辑。
|
||||
- 兼容用户误填 `/mnt/data/openclaw`,避免实际路径变成 `/mnt/data/openclaw/openclaw`。
|
||||
- 拒绝相对路径、空白字符、shell 特殊字符和 `/tmp`、`/usr`、`/overlay` 等危险根目录。
|
||||
- `profile.d/openclaw.sh` 不再全局 `export HOME="$OC_DATA"`,仅在 `openclaw` 命令包装器内注入 HOME,避免影响 SSH/zsh/oh-my-zsh。
|
||||
- 卸载逻辑移除 `chmod -R 777`,并将 `rm -rf` 限定到规范化后的 OpenClaw 运行根目录。
|
||||
- 安装前新增写入探针,提前识别 overlay 已满、只读挂载或目标挂载点不可写。
|
||||
|
||||
### 修复微信插件链路
|
||||
|
||||
- 微信插件安装和升级前检查 `python3`、`openclaw` 系统用户、npm cache/tmp/data 写权限。
|
||||
- 统一以 `openclaw` 用户运行微信插件 CLI,并显式传入 `OPENCLAW_CONFIG_PATH`、`NPM_CONFIG_CACHE`、`TMPDIR`。
|
||||
- 自动迁移旧渠道名 `weixin` 到 `openclaw-weixin`。
|
||||
- 保留 Web PTY `cmd=wechat` 初始化能力,微信向导入口不被破坏。
|
||||
|
||||
### 修复 LuCI 与打包
|
||||
|
||||
- 状态页区分“启动中”和“启动失败”,并显示 procd gateway 退出码。
|
||||
- Web 控制台每次加载前重新读取 token,强制使用正确的 HTTP Gateway URL,避免旧 token 或 HTTPS 混用。
|
||||
- `Makefile`、`.run`、`.ipk` 打包清单补齐 `wechat.htm`、rpcd ACL、共享 shell/Lua helper。
|
||||
- 主包不再直接生成 `openclaw.zh-cn.lmo`,避免与 `luci-i18n-openclaw-zh-cn` 文件冲突。
|
||||
|
||||
### 测试
|
||||
|
||||
- 新增路径规范化、卸载保护、profile HOME、Node 版本比较、打包清单、微信安装预检和控制台 token URL 的契约测试入口。
|
||||
|
||||
### 致谢
|
||||
|
||||
感谢以下用户通过 Issue 或 PR 提供问题反馈、复现信息和修复思路:
|
||||
|
||||
- @svgr110 (#89)、@Cuscito (#88)、@ayysama (#87)、@Jameslau-tech (#86, #66)
|
||||
- @hotwa (#85, #83, #63)、@hwliu11 (#84)、@alan9771 (#82, #65)、@zhuxf8 (#81)
|
||||
- @yiyibuguai (#80, #75)、@nbagui (#79)、@sam528300-lab (#78)、@392377870 (#77)
|
||||
- @ouyangmland (#76, #68, #67)、@lucian521 (#74)、@jiangxiaoqiang-cloud (#73)、@LuckyMai (#72, #62)
|
||||
- @xuguoliang189 (#71, #64)、@ampcwin (#70)、@natserver (#69)、@jh620026 (#61)、@okareyouok (#60)
|
||||
|
||||
---
|
||||
|
||||
## [2.0.4] - 2026-04-10
|
||||
|
||||
### 适配 OpenClaw v2026.4.9
|
||||
|
||||
@@ -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:=14
|
||||
PKG_RELEASE:=15
|
||||
|
||||
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
@@ -56,24 +56,28 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DATA) ./root/etc/profile.d/openclaw.sh $(1)/etc/profile.d/openclaw.sh
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./root/usr/bin/openclaw-env $(1)/usr/bin/openclaw-env
|
||||
$(INSTALL_DIR) $(1)/usr/libexec
|
||||
$(INSTALL_BIN) ./root/usr/libexec/openclaw-paths.sh $(1)/usr/libexec/openclaw-paths.sh
|
||||
$(INSTALL_BIN) ./root/usr/libexec/openclaw-node.sh $(1)/usr/libexec/openclaw-node.sh
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
|
||||
$(INSTALL_DATA) ./luasrc/controller/openclaw.lua $(1)/usr/lib/lua/luci/controller/openclaw.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/openclaw
|
||||
$(INSTALL_DATA) ./luasrc/openclaw/paths.lua $(1)/usr/lib/lua/openclaw/paths.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi/openclaw
|
||||
$(INSTALL_DATA) ./luasrc/model/cbi/openclaw/basic.lua $(1)/usr/lib/lua/luci/model/cbi/openclaw/basic.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/openclaw
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/status.htm $(1)/usr/lib/lua/luci/view/openclaw/status.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/advanced.htm $(1)/usr/lib/lua/luci/view/openclaw/advanced.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/console.htm $(1)/usr/lib/lua/luci/view/openclaw/console.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/wechat.htm $(1)/usr/lib/lua/luci/view/openclaw/wechat.htm
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-openclaw.json $(1)/usr/share/rpcd/acl.d/luci-app-openclaw.json
|
||||
$(INSTALL_DIR) $(1)/usr/share/openclaw
|
||||
$(INSTALL_DATA) ./VERSION $(1)/usr/share/openclaw/VERSION
|
||||
$(INSTALL_BIN) ./root/usr/share/openclaw/oc-config.sh $(1)/usr/share/openclaw/oc-config.sh
|
||||
$(INSTALL_DATA) ./root/usr/share/openclaw/web-pty.js $(1)/usr/share/openclaw/web-pty.js
|
||||
$(INSTALL_DIR) $(1)/usr/share/openclaw/ui
|
||||
$(CP) ./root/usr/share/openclaw/ui/* $(1)/usr/share/openclaw/ui/
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
|
||||
if [ -f ./po/zh-cn/openclaw.po ]; then \
|
||||
po2lmo ./po/zh-cn/openclaw.po $(1)/usr/lib/lua/luci/i18n/openclaw.zh-cn.lmo 2>/dev/null || true; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
|
||||
@@ -19,10 +19,18 @@
|
||||
|------|------|
|
||||
| 架构 | x86_64 或 aarch64 (ARM64) |
|
||||
| C 库 | musl(自动检测;离线包仅支持 musl) |
|
||||
| 依赖 | luci-compat, luci-base, curl, openssl-util |
|
||||
| 存储 | **1.5GB 以上可用空间** |
|
||||
| 依赖 | luci-compat, luci-base, curl, openssl-util, tar, script-utils |
|
||||
| 存储 | **2GB 以上可用空间** |
|
||||
| 内存 | 推荐 1GB 及以上 |
|
||||
|
||||
**当前适配版本**
|
||||
|
||||
| 组件 | 默认版本 | 说明 |
|
||||
|------|----------|------|
|
||||
| OpenClaw | `2026.5.12` | npm `latest` 稳定标签;不默认追 `2026.5.14-beta.2` |
|
||||
| Node.js | `22.16.0` | 安装后会按 OpenClaw `engines.node` 做强校验,低于要求会直接失败 |
|
||||
| 微信插件 | `@tencent-weixin/openclaw-weixin@2.4.3` | CLI 使用 `@tencent-weixin/openclaw-weixin-cli@2.1.4` |
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 方式一:.run 自解压包(推荐)
|
||||
@@ -84,6 +92,41 @@ find bin/ -name "luci-app-openclaw*.ipk"
|
||||
3. 进入「Web 控制台」添加 AI 模型和 API Key
|
||||
4. 进入「配置管理」可使用向导配置消息渠道
|
||||
|
||||
## 自定义安装路径
|
||||
|
||||
UCI 字段仍然是 `openclaw.main.install_path`,语义为基础目录。例如:
|
||||
|
||||
```bash
|
||||
uci set openclaw.main.install_path='/mnt/data'
|
||||
uci commit openclaw
|
||||
openclaw-env setup
|
||||
```
|
||||
|
||||
实际运行目录会固定展开为 `/mnt/data/openclaw`。如果误填 `/mnt/data/openclaw`,插件会自动规范化为 `/mnt/data`,不会再拼成 `/mnt/data/openclaw/openclaw`。
|
||||
|
||||
安装前会执行写入探针;如果 overlay 已满、只读或外置盘未正确挂载,安装会在下载前失败并给出明确日志。
|
||||
|
||||
## 微信插件依赖
|
||||
|
||||
微信渠道安装前会检查:
|
||||
|
||||
- `openclaw` 系统用户是否存在,不存在时自动创建
|
||||
- `python3` 是否已安装
|
||||
- npm cache、tmp、OpenClaw 数据目录是否可由 `openclaw` 用户写入
|
||||
- 旧渠道名 `weixin` 会迁移为 `openclaw-weixin`
|
||||
|
||||
如缺少 Python3:
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install python3
|
||||
```
|
||||
|
||||
## 已知说明
|
||||
|
||||
- OpenClaw 的 diagnostic heartbeat 可能在日志中出现类似周期性探测记录。它不是一次真实用户对话请求;如需降低噪音,优先在 OpenClaw 配置或日志采集侧降低诊断日志级别,不建议直接修改模型调用逻辑。
|
||||
- 当前仓库提供源码、OpenWrt feeds 集成方式、本地 `.run` / `.ipk` 构建脚本入口;本次维护不自动生成 Release 产物。
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
```
|
||||
@@ -91,17 +134,20 @@ luci-app-openclaw/
|
||||
├── Makefile # OpenWrt 包定义
|
||||
├── luasrc/
|
||||
│ ├── controller/openclaw.lua # LuCI 路由和 API
|
||||
│ ├── openclaw/paths.lua # 路径规范化与安全校验
|
||||
│ ├── model/cbi/openclaw/basic.lua # 主页面
|
||||
│ └── view/openclaw/
|
||||
│ ├── status.htm # 状态面板
|
||||
│ ├── advanced.htm # 配置管理(终端)
|
||||
│ └── console.htm # Web 控制台
|
||||
│ ├── console.htm # Web 控制台
|
||||
│ └── wechat.htm # 微信渠道向导
|
||||
├── root/
|
||||
│ ├── etc/
|
||||
│ │ ├── config/openclaw # UCI 配置
|
||||
│ │ ├── init.d/openclaw # 服务脚本
|
||||
│ │ └── uci-defaults/99-openclaw # 初始化脚本
|
||||
│ └── usr/
|
||||
│ ├── libexec/ # 共享 shell helper
|
||||
│ ├── bin/openclaw-env # 环境管理工具
|
||||
│ └── share/openclaw/ # 配置终端资源
|
||||
├── scripts/
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.0.4
|
||||
2.0.5
|
||||
@@ -1,6 +1,89 @@
|
||||
-- luci-app-openclaw — LuCI Controller
|
||||
module("luci.controller.openclaw", package.seeall)
|
||||
|
||||
local paths_ok, oc_paths = pcall(require, "openclaw.paths")
|
||||
|
||||
local function shellquote(value)
|
||||
if paths_ok and oc_paths.shellquote then
|
||||
return oc_paths.shellquote(value)
|
||||
end
|
||||
return "'" .. tostring(value or ""):gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function normalize_install_base(value)
|
||||
if paths_ok and oc_paths.normalize_install_path then
|
||||
return oc_paths.normalize_install_path(value)
|
||||
end
|
||||
local raw = tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", ""):gsub("/+$", "")
|
||||
if raw == "" then raw = "/opt" end
|
||||
if raw:sub(1, 1) ~= "/" then return nil end
|
||||
if raw:match("[%s'\"`$;&|<>()]") then return nil end
|
||||
local unsafe = { "proc", "sys", "dev", "tmp", "var", "etc", "usr", "bin", "sbin", "lib", "rom", "overlay" }
|
||||
if raw == "/" then return nil end
|
||||
for _, name in ipairs(unsafe) do
|
||||
if raw == "/" .. name or raw:match("^/" .. name .. "/") then return nil end
|
||||
end
|
||||
if raw:match("/openclaw$") then raw = raw:gsub("/openclaw$", "") end
|
||||
return raw ~= "" and raw or nil
|
||||
end
|
||||
|
||||
local function get_path_info(input)
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local base = input or uci:get("openclaw", "main", "install_path") or "/opt"
|
||||
if paths_ok and oc_paths.derive_paths then
|
||||
return oc_paths.derive_paths(base)
|
||||
end
|
||||
local normalized = normalize_install_base(base) or "/opt"
|
||||
local root = normalized .. "/openclaw"
|
||||
return {
|
||||
install_path = normalized,
|
||||
oc_root = root,
|
||||
node_base = root .. "/node",
|
||||
oc_global = root .. "/global",
|
||||
oc_data = root .. "/data",
|
||||
config_file = root .. "/data/.openclaw/openclaw.json"
|
||||
}
|
||||
end
|
||||
|
||||
local function is_safe_openclaw_root(value)
|
||||
if paths_ok and oc_paths.is_safe_openclaw_root then
|
||||
return oc_paths.is_safe_openclaw_root(value)
|
||||
end
|
||||
return value == "/opt/openclaw" or value:match("^/mnt/[^/]+/openclaw$") ~= nil or value:match("^/media/[^/]+/openclaw$") ~= nil
|
||||
end
|
||||
|
||||
local function ensure_openclaw_user(oc_data)
|
||||
local sys = require "luci.sys"
|
||||
local uid = sys.exec("id -u openclaw 2>/dev/null"):gsub("%s+", "")
|
||||
if uid ~= "" then return true end
|
||||
|
||||
local script = [[
|
||||
OC_UID=1000
|
||||
while grep -q "^[^:]*:x:${OC_UID}:" /etc/passwd 2>/dev/null; do OC_UID=$((OC_UID + 1)); done
|
||||
OC_GID=$OC_UID
|
||||
while grep -q "^[^:]*:x:${OC_GID}:" /etc/group 2>/dev/null; do OC_GID=$((OC_GID + 1)); done
|
||||
grep -q '^openclaw:' /etc/passwd 2>/dev/null || echo "openclaw:x:${OC_UID}:${OC_GID}:openclaw:${OC_DATA}:/bin/false" >> /etc/passwd
|
||||
grep -q '^openclaw:' /etc/shadow 2>/dev/null || echo 'openclaw:x:0:0:99999:7:::' >> /etc/shadow
|
||||
grep -q '^openclaw:' /etc/group 2>/dev/null || echo "openclaw:x:${OC_GID}:" >> /etc/group
|
||||
]]
|
||||
sys.exec("OC_DATA=" .. shellquote(oc_data) .. " sh -c " .. shellquote(script) .. " >/dev/null 2>&1")
|
||||
uid = sys.exec("id -u openclaw 2>/dev/null"):gsub("%s+", "")
|
||||
return uid ~= ""
|
||||
end
|
||||
|
||||
local function write_wechat_log_and_exit(log_file, exit_file, content, exit_code)
|
||||
local f = io.open(log_file, "w")
|
||||
if f then
|
||||
f:write(content)
|
||||
f:close()
|
||||
end
|
||||
local ef = io.open(exit_file, "w")
|
||||
if ef then
|
||||
ef:write(tostring(exit_code or 1))
|
||||
ef:close()
|
||||
end
|
||||
end
|
||||
|
||||
function index()
|
||||
-- 主入口: 服务 → OpenClaw (🧠 作为菜单图标)
|
||||
local page = entry({"admin", "services", "openclaw"}, alias("admin", "services", "openclaw", "basic"), _("OpenClaw"), 90)
|
||||
@@ -77,32 +160,27 @@ function index()
|
||||
end-- ═══════════════════════════════════════════
|
||||
-- 获取安装路径 (唯一权威来源: UCI 配置)
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 核心原则: 用户在安装时输入的路径是唯一的安装位置
|
||||
-- - UCI install_path 存储用户输入的基础路径 (如 /mnt/data 或 /opt)
|
||||
-- - 实际安装路径为 ${install_path}/openclaw
|
||||
-- - 此函数始终返回 UCI 配置的路径,不做任何"智能"回退
|
||||
-- 核心原则: UCI install_path 继续存储公开兼容字段,语义是基础目录。
|
||||
-- 如果用户误填 /mnt/data/openclaw,这里会规范化为 /mnt/data,再返回真实根目录。
|
||||
-- ═══════════════════════════════════════════
|
||||
local function get_install_path()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
-- 从 UCI 读取用户配置的基础路径,默认 /opt
|
||||
local base_path = uci:get("openclaw", "main", "install_path") or "/opt"
|
||||
-- 返回完整安装路径
|
||||
return base_path .. "/openclaw"
|
||||
return get_path_info().oc_root
|
||||
end
|
||||
|
||||
-- 确保网关端口可用:检测占用并尝试优雅停止或强制杀死占用进程
|
||||
local function ensure_port_free(port)
|
||||
local sys = require "luci.sys"
|
||||
if not port or port == "" then return end
|
||||
if not tostring(port):match("^%d+$") then return end
|
||||
-- 优先尝试使用 openclaw 自身的 stop 命令(如果已安装)
|
||||
sys.exec("openclaw gateway stop >/dev/null 2>&1 || true")
|
||||
|
||||
-- 查询占用端口的行
|
||||
local check_cmd = ""
|
||||
if os.execute("command -v ss >/dev/null 2>&1") == 0 then
|
||||
check_cmd = string.format("ss -tulnp 2>/dev/null | grep -E ':%%s ' || true", port)
|
||||
check_cmd = "ss -tulnp 2>/dev/null | grep -E " .. shellquote(":" .. port .. " ") .. " || true"
|
||||
else
|
||||
check_cmd = string.format("netstat -tulnp 2>/dev/null | grep -E ':%%s ' || true", port)
|
||||
check_cmd = "netstat -tulnp 2>/dev/null | grep -E " .. shellquote(":" .. port .. " ") .. " || true"
|
||||
end
|
||||
local out = sys.exec(check_cmd)
|
||||
out = out or ""
|
||||
@@ -164,6 +242,8 @@ function action_status()
|
||||
install_path = install_path,
|
||||
gateway_running = false,
|
||||
gateway_starting = false,
|
||||
gateway_failed = false,
|
||||
gateway_exit_code = "",
|
||||
pty_running = false,
|
||||
pid = "",
|
||||
memory_kb = 0,
|
||||
@@ -218,9 +298,19 @@ function action_status()
|
||||
|
||||
-- 如果端口未监听但 procd 进程存在,说明正在启动中 (gateway 初始化需要数分钟)
|
||||
if not result.gateway_running and enabled == "1" then
|
||||
local procd_pid = sys.exec("pgrep -f 'openclaw.*gateway' 2>/dev/null | head -1"):gsub("%s+", "")
|
||||
if procd_pid ~= "" then
|
||||
local procd_pid = sys.exec("ubus call service list '{\"name\":\"openclaw\"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.pid' 2>/dev/null"):gsub("%s+", "")
|
||||
local procd_running = sys.exec("ubus call service list '{\"name\":\"openclaw\"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.running' 2>/dev/null"):gsub("%s+", "")
|
||||
local procd_exit = sys.exec("ubus call service list '{\"name\":\"openclaw\"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.exit_code' 2>/dev/null"):gsub("%s+", "")
|
||||
result.gateway_exit_code = procd_exit
|
||||
if procd_exit ~= "" and tonumber(procd_exit) and tonumber(procd_exit) ~= 0 and procd_running ~= "true" then
|
||||
result.gateway_failed = true
|
||||
elseif procd_pid ~= "" or procd_running == "true" then
|
||||
result.gateway_starting = true
|
||||
else
|
||||
local fallback_pid = sys.exec("pgrep -f 'openclaw.*gateway' 2>/dev/null | head -1"):gsub("%s+", "")
|
||||
if fallback_pid ~= "" then
|
||||
result.gateway_starting = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -294,7 +384,7 @@ function action_status()
|
||||
|
||||
-- 磁盘剩余空间 (检测安装路径所在分区)
|
||||
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+", "")
|
||||
local df_output = sys.exec("df -h " .. shellquote(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
|
||||
@@ -339,24 +429,27 @@ function action_service_ctl()
|
||||
-- 稳定版: 读取 openclaw-env 中定义的 OC_TESTED_VERSION
|
||||
local tested_ver = sys.exec("grep '^OC_TESTED_VERSION=' /usr/bin/openclaw-env 2>/dev/null | cut -d'\"' -f2"):gsub("%s+", "")
|
||||
if tested_ver ~= "" then
|
||||
env_prefix = "OC_VERSION=" .. tested_ver .. " "
|
||||
env_prefix = "OC_VERSION=" .. shellquote(tested_ver) .. " "
|
||||
end
|
||||
elseif version == "latest" then
|
||||
env_prefix = "OC_VERSION='latest' "
|
||||
elseif version ~= "" and version ~= "latest" then
|
||||
-- 校验版本号格式 (仅允许数字、点、横线、字母)
|
||||
if version:match("^[%d%.%-a-zA-Z]+$") then
|
||||
env_prefix = "OC_VERSION=" .. version .. " "
|
||||
env_prefix = "OC_VERSION=" .. shellquote(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 .. "' "
|
||||
if install_path ~= "" then
|
||||
local normalized = normalize_install_base(install_path)
|
||||
if not normalized then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "安装路径无效:必须是绝对路径,且不能包含空格、引号或 shell 特殊字符。" })
|
||||
return
|
||||
end
|
||||
-- 保存规范化后的基础路径,公开字段仍为 install_path,避免破坏兼容。
|
||||
sys.exec("uci set openclaw.main.install_path=" .. shellquote(normalized) .. "; uci commit openclaw 2>/dev/null")
|
||||
env_prefix = env_prefix .. "OC_INSTALL_PATH=" .. shellquote(normalized) .. " "
|
||||
end
|
||||
-- 后台安装,成功后自动启用并启动服务
|
||||
-- 注: openclaw-env 脚本有 set -e,init_openclaw 中的非关键失败不应阻止启动
|
||||
@@ -490,10 +583,22 @@ function action_uninstall()
|
||||
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"
|
||||
-- 获取并校验安装路径。卸载只能作用于规范化后的 <base>/openclaw。
|
||||
local configured_base = uci:get("openclaw", "main", "install_path") or "/opt"
|
||||
local normalized_base = normalize_install_base(configured_base)
|
||||
if not normalized_base then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "UCI 安装路径无效,已取消卸载: " .. tostring(configured_base) })
|
||||
return
|
||||
end
|
||||
local path_info = get_path_info(normalized_base)
|
||||
local install_path = path_info.oc_root
|
||||
if not is_safe_openclaw_root(install_path) then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "安装路径未通过安全校验,已取消卸载: " .. install_path })
|
||||
return
|
||||
end
|
||||
local q_install_path = shellquote(install_path)
|
||||
|
||||
-- 1. 停止服务 (通过 init.d 正常流程)
|
||||
sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1")
|
||||
@@ -513,17 +618,13 @@ 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("umount " .. install_path .. " 2>/dev/null")
|
||||
-- 强制赋予最大权限避免 EACCES 阻挡
|
||||
sys.exec("chmod -R 777 " .. install_path .. " /root/.openclaw 2>/dev/null")
|
||||
-- 删除 Node.js + OpenClaw 运行环境
|
||||
sys.exec("rm -rf " .. install_path)
|
||||
-- OverlayFS 兼容: 确保 upper 层的残留也被暴力清空
|
||||
sys.exec("[ -d /overlay/upper" .. install_path .. " ] && rm -rf /overlay/upper" .. install_path .. " 2>/dev/null")
|
||||
|
||||
-- 清理旧数据迁移后可能残留的目录
|
||||
sys.exec("rm -rf /root/.openclaw 2>/dev/null")
|
||||
-- 尝试先解绑可能挂载的目录;失败不阻断后续删除。
|
||||
sys.exec("umount " .. q_install_path .. " 2>/dev/null || true")
|
||||
-- 删除 Node.js + OpenClaw 运行环境。不要通过全局提权绕过权限问题。
|
||||
sys.exec("rm -rf " .. q_install_path)
|
||||
-- OverlayFS 兼容: 只清理同一规范化路径在 upper 层的残留。
|
||||
local overlay_install_path = "/overlay/upper" .. install_path
|
||||
sys.exec("[ -d " .. shellquote(overlay_install_path) .. " ] && rm -rf " .. shellquote(overlay_install_path) .. " 2>/dev/null || true")
|
||||
-- 清理临时文件
|
||||
sys.exec("rm -f /tmp/openclaw-setup.* /tmp/openclaw-update.log /tmp/openclaw-plugin-upgrade.* /var/run/openclaw*.pid")
|
||||
-- 清理 LuCI 缓存
|
||||
@@ -534,7 +635,7 @@ function action_uninstall()
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
status = "ok",
|
||||
message = "运行环境已卸载。已清理: Node.js 运行环境 (" .. install_path .. ")、所有插件 (qqbot/飞书等)、旧数据目录 (/root/.openclaw)、临时文件、LuCI 缓存。"
|
||||
message = "运行环境已卸载。已清理: Node.js 运行环境 (" .. install_path .. ")、所有插件、临时文件、LuCI 缓存。"
|
||||
})
|
||||
end
|
||||
|
||||
@@ -970,12 +1071,8 @@ function action_check_system()
|
||||
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
|
||||
local raw_install_path = http.formvalue("install_path") or uci:get("openclaw", "main", "install_path") or "/opt"
|
||||
local install_path = normalize_install_base(raw_install_path)
|
||||
|
||||
-- 最低要求配置 (v2026.3.28: 包体积 ~200MB, 建议 2GB 可用空间)
|
||||
local MIN_MEMORY_MB = 1024 -- 1GB
|
||||
@@ -987,12 +1084,21 @@ function action_check_system()
|
||||
disk_mb = 0,
|
||||
disk_ok = false,
|
||||
disk_path = "",
|
||||
install_path = install_path,
|
||||
install_path = install_path or tostring(raw_install_path or ""),
|
||||
path_valid = install_path ~= nil,
|
||||
writable_ok = false,
|
||||
disk_free_str = "",
|
||||
pass = false,
|
||||
message = ""
|
||||
}
|
||||
|
||||
if not install_path then
|
||||
result.message = "安装路径无效:必须是绝对路径,且不能包含空格、引号或 shell 特殊字符。"
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(result)
|
||||
return
|
||||
end
|
||||
|
||||
-- 检测总内存 (从 /proc/meminfo 读取 MemTotal)
|
||||
local meminfo = io.open("/proc/meminfo", "r")
|
||||
if meminfo then
|
||||
@@ -1029,12 +1135,12 @@ function action_check_system()
|
||||
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+", "")
|
||||
local df_output = sys.exec("df -m " .. shellquote(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+", "")
|
||||
result.disk_free_str = sys.exec("df -h " .. shellquote(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+", "")
|
||||
@@ -1046,8 +1152,19 @@ function action_check_system()
|
||||
end
|
||||
result.disk_ok = result.disk_mb >= MIN_DISK_MB
|
||||
|
||||
-- 安装前写入探针:先在实际存在的父目录创建临时目录。
|
||||
-- 这能明确识别 overlay 满、只读挂载、路径挂载点不可写等问题,避免下载完才失败。
|
||||
local probe_dir = disk_check_path .. "/.openclaw-write-test-" .. tostring(os.time()) .. "-" .. tostring(math.random(1000, 9999))
|
||||
local probe_rc = os.execute("mkdir " .. shellquote(probe_dir) .. " >/dev/null 2>&1")
|
||||
if probe_rc == 0 or probe_rc == true then
|
||||
result.writable_ok = true
|
||||
os.execute("rmdir " .. shellquote(probe_dir) .. " >/dev/null 2>&1")
|
||||
else
|
||||
result.writable_ok = false
|
||||
end
|
||||
|
||||
-- 综合判断
|
||||
result.pass = result.memory_ok and result.disk_ok
|
||||
result.pass = result.memory_ok and result.disk_ok and result.writable_ok
|
||||
|
||||
-- 生成提示信息
|
||||
if result.pass then
|
||||
@@ -1060,6 +1177,9 @@ function action_check_system()
|
||||
if not result.disk_ok then
|
||||
table.insert(issues, string.format("磁盘空间不足: 当前 %d MB 可用,需要至少 %d MB", result.disk_mb, MIN_DISK_MB))
|
||||
end
|
||||
if not result.writable_ok then
|
||||
table.insert(issues, "安装路径所在挂载点不可写,可能是 overlay 已满、只读或外置盘未正确挂载")
|
||||
end
|
||||
result.message = table.concat(issues, ";")
|
||||
end
|
||||
|
||||
@@ -1191,22 +1311,48 @@ function action_wechat_install()
|
||||
http.write_json({ status = "ok", message = "微信插件安装已在后台启动..." })
|
||||
return
|
||||
end
|
||||
if sys.exec("command -v python3 2>/dev/null"):gsub("%s+", "") == "" then
|
||||
write_wechat_log_and_exit(
|
||||
"/tmp/openclaw-wechat-install.log",
|
||||
"/tmp/openclaw-wechat-install.exit",
|
||||
"开始安装微信插件...\n安装路径: " .. install_path .. "\n❌ 错误: 未检测到 python3。\n请先安装依赖: opkg update && opkg install python3\n",
|
||||
127
|
||||
)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "ok", message = "微信插件安装已在后台启动..." })
|
||||
return
|
||||
end
|
||||
if not ensure_openclaw_user(oc_data) then
|
||||
write_wechat_log_and_exit(
|
||||
"/tmp/openclaw-wechat-install.log",
|
||||
"/tmp/openclaw-wechat-install.exit",
|
||||
"开始安装微信插件...\n安装路径: " .. install_path .. "\n❌ 错误: 无法创建或读取 openclaw 系统用户。\n请检查 /etc/passwd、/etc/group 是否可写。\n",
|
||||
1
|
||||
)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "ok", message = "微信插件安装已在后台启动..." })
|
||||
return
|
||||
end
|
||||
|
||||
-- 后台执行安装
|
||||
-- 在启动安装前,确保网关端口可用(自动清理残留 gateway 进程)
|
||||
local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
ensure_port_free(port)
|
||||
-- 微信插件安装目录路径 (用于安装后权限修复)
|
||||
local wechat_ext_dir = install_path .. "/data/.openclaw/extensions/openclaw-weixin"
|
||||
local extensions_dir = install_path .. "/data/.openclaw/extensions"
|
||||
local install_cmd = string.format(
|
||||
"( " ..
|
||||
"echo '开始安装微信插件...' > /tmp/openclaw-wechat-install.log; " ..
|
||||
"echo '安装路径: %s' >> /tmp/openclaw-wechat-install.log; " ..
|
||||
"echo 'npx 路径: %s' >> /tmp/openclaw-wechat-install.log; " ..
|
||||
-- 修复 npm 缓存目录权限 (避免 root 创建的缓存导致 openclaw 用户写入失败)
|
||||
"chown -R openclaw:openclaw %s/.npm 2>/dev/null; " ..
|
||||
"mkdir -p %s/.npm %s/.tmp %s/.openclaw/extensions; " ..
|
||||
"if [ ! -w %s/.openclaw/extensions ]; then chown -R openclaw:openclaw %s/.openclaw/extensions 2>/dev/null; fi; " ..
|
||||
"chown -R openclaw:openclaw %s/.npm %s/.tmp %s/.openclaw 2>/dev/null; " ..
|
||||
"if [ ! -w %s/.npm ] || [ ! -w %s/.tmp ]; then echo '❌ npm cache/tmp 目录不可写' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; fi; " ..
|
||||
"su -s /bin/sh openclaw -c 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " ..
|
||||
"cd %s && " ..
|
||||
"su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " ..
|
||||
"OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm TMPDIR=%s/.tmp " ..
|
||||
"PATH=%s/node/bin:%s/global/bin:$PATH " ..
|
||||
"%s -y @tencent-weixin/openclaw-weixin-cli install' >> /tmp/openclaw-wechat-install.log 2>&1; " ..
|
||||
"RC=$?; echo $RC > /tmp/openclaw-wechat-install.exit; " ..
|
||||
@@ -1217,8 +1363,16 @@ function action_wechat_install()
|
||||
"if [ $RC -eq 0 ]; then echo '✅ 微信插件安装成功!' >> /tmp/openclaw-wechat-install.log; " ..
|
||||
"else echo '❌ 安装失败 (exit: '$RC')' >> /tmp/openclaw-wechat-install.log; fi " ..
|
||||
") & echo $! > /tmp/openclaw-wechat-install.pid",
|
||||
install_path, npx_bin, oc_data, install_path, oc_data, oc_data, oc_data, install_path, install_path, npx_bin,
|
||||
wechat_ext_dir, wechat_ext_dir
|
||||
install_path, npx_bin,
|
||||
oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data,
|
||||
install_path, oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data, oc_data,
|
||||
install_path, install_path, npx_bin,
|
||||
extensions_dir, extensions_dir
|
||||
)
|
||||
sys.exec(install_cmd)
|
||||
|
||||
@@ -1313,6 +1467,11 @@ function action_wechat_login()
|
||||
http.write_json({ status = "error", message = "OpenClaw 未安装" })
|
||||
return
|
||||
end
|
||||
if not ensure_openclaw_user(oc_data) then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "无法创建或读取 openclaw 系统用户,请检查 /etc/passwd、/etc/group 是否可写" })
|
||||
return
|
||||
end
|
||||
|
||||
-- 清理旧状态和可能的残留进程
|
||||
sys.exec("kill -9 $(cat /tmp/openclaw-wechat-login.pid 2>/dev/null) 2>/dev/null")
|
||||
@@ -1322,13 +1481,17 @@ function action_wechat_login()
|
||||
"( " ..
|
||||
"echo '正在启动微信登录...' > /tmp/openclaw-wechat-qrcode.txt; " ..
|
||||
"echo '安装路径: %s' >> /tmp/openclaw-wechat-qrcode.txt; " ..
|
||||
"mkdir -p %s/.npm %s/.tmp %s/.openclaw; chown -R openclaw:openclaw %s/.npm %s/.tmp %s/.openclaw 2>/dev/null; " ..
|
||||
"cd %s && " ..
|
||||
"su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json " ..
|
||||
"PATH=%s/node/bin:%s/global/bin:$PATH " ..
|
||||
"NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm TMPDIR=%s/.tmp PATH=%s/node/bin:%s/global/bin:$PATH " ..
|
||||
"%s %s channels login --channel openclaw-weixin' >> /tmp/openclaw-wechat-qrcode.txt 2>&1; " ..
|
||||
"echo $? > /tmp/openclaw-wechat-login.exit; " ..
|
||||
") >/dev/null 2>&1 & echo $! > /tmp/openclaw-wechat-login.pid",
|
||||
install_path, oc_data, oc_data, oc_data, oc_data, oc_data, install_path, install_path, node_bin, oc_entry
|
||||
install_path,
|
||||
oc_data, oc_data, oc_data, oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data, install_path, install_path, node_bin, oc_entry
|
||||
) sys.exec(login_cmd)
|
||||
|
||||
http.prepare_content("application/json")
|
||||
@@ -1419,6 +1582,9 @@ function action_wechat_uninstall()
|
||||
|
||||
-- 删除微信插件目录
|
||||
local wechat_ext_dir = install_path .. "/data/.openclaw/extensions/openclaw-weixin"
|
||||
if wechat_ext_dir == install_path .. "/data/.openclaw/extensions/openclaw-weixin" then
|
||||
sys.exec("rm -rf " .. shellquote(wechat_ext_dir) .. " 2>/dev/null")
|
||||
end
|
||||
|
||||
-- 从配置中删除微信相关配置
|
||||
local config_file = install_path .. "/data/.openclaw/openclaw.json"
|
||||
@@ -1536,22 +1702,48 @@ function action_wechat_upgrade_plugin()
|
||||
http.write_json({ status = "ok", message = "微信插件升级已在后台启动..." })
|
||||
return
|
||||
end
|
||||
if sys.exec("command -v python3 2>/dev/null"):gsub("%s+", "") == "" then
|
||||
write_wechat_log_and_exit(
|
||||
"/tmp/openclaw-wechat-install.log",
|
||||
"/tmp/openclaw-wechat-install.exit",
|
||||
"正在升级微信插件...\n安装路径: " .. install_path .. "\n❌ 错误: 未检测到 python3。\n请先安装依赖: opkg update && opkg install python3\n",
|
||||
127
|
||||
)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "ok", message = "微信插件升级已在后台启动..." })
|
||||
return
|
||||
end
|
||||
if not ensure_openclaw_user(oc_data) then
|
||||
write_wechat_log_and_exit(
|
||||
"/tmp/openclaw-wechat-install.log",
|
||||
"/tmp/openclaw-wechat-install.exit",
|
||||
"正在升级微信插件...\n安装路径: " .. install_path .. "\n❌ 错误: 无法创建或读取 openclaw 系统用户。\n请检查 /etc/passwd、/etc/group 是否可写。\n",
|
||||
1
|
||||
)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "ok", message = "微信插件升级已在后台启动..." })
|
||||
return
|
||||
end
|
||||
|
||||
-- 后台执行升级 (其实就是重新安装最新版)
|
||||
-- 在启动升级前,确保网关端口可用(自动清理残留 gateway 进程)
|
||||
local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
ensure_port_free(port)
|
||||
-- 微信插件安装目录路径 (用于升级后权限修复)
|
||||
local wechat_ext_dir = install_path .. "/data/.openclaw/extensions/openclaw-weixin"
|
||||
local extensions_dir = install_path .. "/data/.openclaw/extensions"
|
||||
local upgrade_cmd = string.format(
|
||||
"( " ..
|
||||
"echo '正在升级微信插件...' > /tmp/openclaw-wechat-install.log; " ..
|
||||
"echo '安装路径: %s' >> /tmp/openclaw-wechat-install.log; " ..
|
||||
"echo 'npx 路径: %s' >> /tmp/openclaw-wechat-install.log; " ..
|
||||
-- 修复 npm 缓存目录权限 (避免 root 创建的缓存导致 openclaw 用户写入失败)
|
||||
"chown -R openclaw:openclaw %s/.npm 2>/dev/null; " ..
|
||||
"mkdir -p %s/.npm %s/.tmp %s/.openclaw/extensions; " ..
|
||||
"if [ ! -w %s/.openclaw/extensions ]; then chown -R openclaw:openclaw %s/.openclaw/extensions 2>/dev/null; fi; " ..
|
||||
"chown -R openclaw:openclaw %s/.npm %s/.tmp %s/.openclaw 2>/dev/null; " ..
|
||||
"if [ ! -w %s/.npm ] || [ ! -w %s/.tmp ]; then echo '❌ npm cache/tmp 目录不可写' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; fi; " ..
|
||||
"su -s /bin/sh openclaw -c 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " ..
|
||||
"cd %s && " ..
|
||||
"su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " ..
|
||||
"OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm TMPDIR=%s/.tmp " ..
|
||||
"PATH=%s/node/bin:%s/global/bin:$PATH " ..
|
||||
"%s -y @tencent-weixin/openclaw-weixin-cli install' >> /tmp/openclaw-wechat-install.log 2>&1; " ..
|
||||
"RC=$?; echo $RC > /tmp/openclaw-wechat-install.exit; " ..
|
||||
@@ -1561,8 +1753,16 @@ function action_wechat_upgrade_plugin()
|
||||
"if [ $RC -eq 0 ]; then echo '✅ 微信插件升级成功!' >> /tmp/openclaw-wechat-install.log; " ..
|
||||
"else echo '❌ 升级失败 (exit: '$RC')' >> /tmp/openclaw-wechat-install.log; fi " ..
|
||||
") & echo $! > /tmp/openclaw-wechat-install.pid",
|
||||
install_path, npx_bin, oc_data, install_path, oc_data, oc_data, oc_data, install_path, install_path, npx_bin,
|
||||
wechat_ext_dir, wechat_ext_dir
|
||||
install_path, npx_bin,
|
||||
oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data,
|
||||
install_path, oc_data, oc_data, oc_data,
|
||||
oc_data, oc_data, oc_data, oc_data,
|
||||
install_path, install_path, npx_bin,
|
||||
extensions_dir, extensions_dir
|
||||
)
|
||||
sys.exec(upgrade_cmd)
|
||||
|
||||
@@ -1578,11 +1778,16 @@ function action_wechat_logout()
|
||||
local sys = require "luci.sys"
|
||||
local account_id = http.formvalue("account")
|
||||
|
||||
if not account_id or account_id == "" then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "参数错误:未提供账号 ID" })
|
||||
return
|
||||
end
|
||||
if not account_id or account_id == "" then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "参数错误:未提供账号 ID" })
|
||||
return
|
||||
end
|
||||
if account_id:match("[`$;&|<>\"']") then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "账号 ID 包含非法字符" })
|
||||
return
|
||||
end
|
||||
|
||||
local install_path = get_install_path()
|
||||
local node_bin = install_path .. "/node/bin/node"
|
||||
@@ -1604,13 +1809,18 @@ function action_wechat_logout()
|
||||
end
|
||||
end
|
||||
|
||||
if oc_entry == "" then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "OpenClaw 未安装" })
|
||||
return
|
||||
end
|
||||
if oc_entry == "" then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "OpenClaw 未安装" })
|
||||
return
|
||||
end
|
||||
if not ensure_openclaw_user(oc_data) then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "无法创建或读取 openclaw 系统用户" })
|
||||
return
|
||||
end
|
||||
|
||||
-- 在后台执行 logout
|
||||
-- 在后台执行 logout
|
||||
local logout_cmd = string.format(
|
||||
"cd %s && su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json " ..
|
||||
"PATH=%s/node/bin:%s/global/bin:$PATH " ..
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
local M = {}
|
||||
|
||||
local function trim(value)
|
||||
return (value or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
function M.normalize_install_path(value)
|
||||
local raw = trim(value)
|
||||
if raw == "" then raw = "/opt" end
|
||||
raw = raw:gsub("/+$", "")
|
||||
if raw == "" then raw = "/" end
|
||||
|
||||
if raw:sub(1, 1) ~= "/" then return nil end
|
||||
if raw:match("[%s'\"`$;&|<>()]") then return nil end
|
||||
if raw == "/" or raw == "/proc" or raw:match("^/proc/") or
|
||||
raw == "/sys" or raw:match("^/sys/") or
|
||||
raw == "/dev" or raw:match("^/dev/") or
|
||||
raw == "/tmp" or raw:match("^/tmp/") or
|
||||
raw == "/var" or raw:match("^/var/") or
|
||||
raw == "/etc" or raw:match("^/etc/") or
|
||||
raw == "/usr" or raw:match("^/usr/") or
|
||||
raw == "/bin" or raw:match("^/bin/") or
|
||||
raw == "/sbin" or raw:match("^/sbin/") or
|
||||
raw == "/lib" or raw:match("^/lib/") or
|
||||
raw == "/rom" or raw:match("^/rom/") or
|
||||
raw == "/overlay" or raw:match("^/overlay/") then
|
||||
return nil
|
||||
end
|
||||
|
||||
if raw:match("/openclaw$") then
|
||||
raw = raw:gsub("/openclaw$", "")
|
||||
if raw == "" then raw = "/" end
|
||||
end
|
||||
|
||||
return raw
|
||||
end
|
||||
|
||||
function M.derive_paths(value)
|
||||
local base = M.normalize_install_path(value) or "/opt"
|
||||
local root = (base == "/") and "/openclaw" or (base .. "/openclaw")
|
||||
return {
|
||||
install_path = base,
|
||||
oc_root = root,
|
||||
node_base = root .. "/node",
|
||||
oc_global = root .. "/global",
|
||||
oc_data = root .. "/data",
|
||||
config_file = root .. "/data/.openclaw/openclaw.json"
|
||||
}
|
||||
end
|
||||
|
||||
function M.shellquote(value)
|
||||
return "'" .. tostring(value or ""):gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
function M.is_safe_openclaw_root(value)
|
||||
if type(value) ~= "string" or not value:match("/openclaw$") then return false end
|
||||
if value == "/openclaw" or value == "/opt/openclaw" then return true end
|
||||
if value:match("^/mnt/[^/]+/openclaw$") or value:match("^/media/[^/]+/openclaw$") then return true end
|
||||
if value:match("^/srv/[^/]+/openclaw$") then return true end
|
||||
if value == "/overlay/upper/opt/openclaw" then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -107,9 +107,9 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
var openBtn = document.getElementById('oc-console-open-btn');
|
||||
|
||||
function getConsoleUrl() {
|
||||
var proto = window.location.protocol;
|
||||
var host = window.location.hostname;
|
||||
var url = proto + '//' + host + ':' + gwPort + '/';
|
||||
// OpenClaw Gateway 当前只监听 HTTP;LuCI 使用 HTTPS 时也必须强制走 http,避免浏览器误连 HTTPS 端口。
|
||||
var url = 'http://' + host + ':' + gwPort + '/';
|
||||
if (gwToken) url += '#token=' + encodeURIComponent(gwToken);
|
||||
return url;
|
||||
}
|
||||
@@ -136,6 +136,15 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
openBtn.href = url;
|
||||
openBtn.style.display = '';
|
||||
showIframe(url);
|
||||
} else if (d.gateway_failed) {
|
||||
statusTextEl.innerHTML = '<span style="color:#cf222e;">● 网关启动失败</span>';
|
||||
openBtn.style.display = 'none';
|
||||
loading.style.display = '';
|
||||
loading.innerHTML = '<div style="text-align:center;color:#666;">' +
|
||||
'<div style="font-size:40px;margin-bottom:12px;">×</div>' +
|
||||
'<div style="font-size:15px;margin-bottom:6px;">OpenClaw 网关启动失败</div>' +
|
||||
'<div style="font-size:12px;color:#999;">退出码:' + (d.gateway_exit_code || '-') + '。请查看系统日志或安装日志。</div>' +
|
||||
'</div>';
|
||||
} else if (d.gateway_starting) {
|
||||
statusTextEl.innerHTML = '<span style="color:#9a6700;">⏳ 网关正在启动</span>';
|
||||
openBtn.style.display = 'none';
|
||||
@@ -163,7 +172,10 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
function showIframe(url) {
|
||||
loading.style.display = 'none';
|
||||
var existing = document.getElementById('oc-console-iframe');
|
||||
if (existing) return;
|
||||
if (existing) {
|
||||
if (existing.src !== url) existing.src = url;
|
||||
return;
|
||||
}
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = 'oc-console-iframe';
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
.oc-badge-running { background: #e6f7e9; color: #1a7f37; }
|
||||
.oc-badge-stopped { background: #ffeef0; color: #cf222e; }
|
||||
.oc-badge-starting { background: #fff8c5; color: #9a6700; }
|
||||
.oc-badge-failed { background: #ffeef0; color: #cf222e; }
|
||||
.oc-badge-disabled { background: #f0f0f0; color: #656d76; }
|
||||
.oc-badge-unknown { background: #fff8c5; color: #9a6700; }
|
||||
.oc-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
|
||||
@@ -101,6 +102,8 @@
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-running">运行中</span>';
|
||||
} else if (d.gateway_starting) {
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-starting">⏳ 正在启动...</span>';
|
||||
} else if (d.gateway_failed) {
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-failed">启动失败</span>';
|
||||
} else {
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-stopped">已停止</span>';
|
||||
}
|
||||
@@ -110,6 +113,8 @@
|
||||
gwEl.innerHTML = '<span class="oc-dot oc-dot-green"></span>监听中 :' + d.port;
|
||||
} else if (d.gateway_starting) {
|
||||
gwEl.innerHTML = '<span class="oc-dot oc-dot-gray"></span>初始化中,首次启动可能需要 2~5 分钟...';
|
||||
} else if (d.gateway_failed) {
|
||||
gwEl.innerHTML = '<span class="oc-dot oc-dot-red"></span>启动失败,退出码 ' + (d.gateway_exit_code || '-');
|
||||
} else {
|
||||
gwEl.innerHTML = '<span class="oc-dot oc-dot-red"></span>未监听';
|
||||
}
|
||||
|
||||
@@ -11,13 +11,33 @@ EXTRA_HELP=" setup 下载 Node.js 并安装 OpenClaw
|
||||
restart_gateway 仅重启 Gateway 实例 (不影响 Web PTY)"
|
||||
|
||||
# ── 安装路径 (支持自定义) ──
|
||||
# 从 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"
|
||||
# 从 UCI 配置读取自定义路径,公开字段仍为 install_path。
|
||||
# 统一路径解析可以兼容 /mnt/data/openclaw 输入,避免双拼路径。
|
||||
[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh
|
||||
[ -r /usr/libexec/openclaw-node.sh ] && . /usr/libexec/openclaw-node.sh
|
||||
OC_NODE_MIN_VERSION="${OC_NODE_MIN_VERSION:-22.14.0}"
|
||||
OC_CONFIGURED_PATH="$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')"
|
||||
if command -v oc_load_paths >/dev/null 2>&1; then
|
||||
if ! oc_load_paths "$OC_CONFIGURED_PATH"; then
|
||||
logger -t openclaw "安装路径无效: $OC_CONFIGURED_PATH"
|
||||
OC_BASE_PATH="/opt"
|
||||
OC_INSTALL_PATH="/opt/openclaw"
|
||||
NODE_BASE="${OC_INSTALL_PATH}/node"
|
||||
OC_GLOBAL="${OC_INSTALL_PATH}/global"
|
||||
OC_DATA="${OC_INSTALL_PATH}/data"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
else
|
||||
OC_BASE_PATH="$OPENCLAW_INSTALL_PATH"
|
||||
OC_INSTALL_PATH="$OC_ROOT"
|
||||
fi
|
||||
else
|
||||
OC_BASE_PATH="${OC_CONFIGURED_PATH%/}"
|
||||
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"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
fi
|
||||
|
||||
# ── OverlayFS 兼容性修复 ──
|
||||
# Docker bind mount (/overlay/upper/opt/docker) 会导致 /opt 不可写
|
||||
@@ -220,6 +240,27 @@ if(d.plugins.installs){
|
||||
}
|
||||
});
|
||||
}
|
||||
// v2026.5.x: 微信插件渠道名统一为 openclaw-weixin
|
||||
// 旧配置里可能残留 weixin,启动前迁移,避免渠道加载失败。
|
||||
if(Array.isArray(d.plugins.allow)){
|
||||
d.plugins.allow=d.plugins.allow.map(x=>x==='weixin'?'openclaw-weixin':x)
|
||||
.filter((x,i,a)=>x&&a.indexOf(x)===i);
|
||||
}
|
||||
['entries','installs'].forEach(ns=>{
|
||||
if(d.plugins&&d.plugins[ns]&&d.plugins[ns].weixin){
|
||||
if(!d.plugins[ns]['openclaw-weixin'])d.plugins[ns]['openclaw-weixin']=d.plugins[ns].weixin;
|
||||
delete d.plugins[ns].weixin;
|
||||
}
|
||||
});
|
||||
['channel','channels'].forEach(ns=>{
|
||||
if(d[ns]&&d[ns].weixin){
|
||||
if(!d[ns]['openclaw-weixin'])d[ns]['openclaw-weixin']=d[ns].weixin;
|
||||
delete d[ns].weixin;
|
||||
}
|
||||
});
|
||||
if(!d.plugins.allow.includes('openclaw-weixin') && d.plugins.installs && d.plugins.installs['openclaw-weixin']){
|
||||
d.plugins.allow.push('openclaw-weixin');
|
||||
}
|
||||
// 始终信任 copilot-proxy (内置插件)
|
||||
if(!d.plugins.allow.includes('copilot-proxy'))d.plugins.allow.push('copilot-proxy');
|
||||
fs.writeFileSync(f,JSON.stringify(d,null,2));
|
||||
@@ -248,6 +289,13 @@ echo "未找到 Node.js: $NODE_BIN"
|
||||
echo "请运行: openclaw-env setup"
|
||||
return 1
|
||||
fi
|
||||
if command -v oc_assert_node_min_version >/dev/null 2>&1; then
|
||||
oc_assert_node_min_version "$NODE_BIN" "$OC_NODE_MIN_VERSION" >/dev/null || {
|
||||
echo "Node.js 版本过低,OpenClaw 需要 >= v${OC_NODE_MIN_VERSION}"
|
||||
echo "请运行: openclaw-env node"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# 检查 openclaw 入口
|
||||
local oc_entry
|
||||
|
||||
@@ -6,13 +6,23 @@
|
||||
# 支持自定义安装路径
|
||||
# ============================================================================
|
||||
|
||||
# 从 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"
|
||||
# 从 UCI 配置读取自定义安装路径。
|
||||
# 注意:这里不能修改全局 HOME。profile 会被 SSH shell、zsh/oh-my-zsh 等加载,
|
||||
# 全局改 HOME 会让 cd ~、插件缓存、历史文件全部跑到 OpenClaw 数据目录。
|
||||
# OpenClaw CLI 需要的 HOME 只在 openclaw 命令包装器里单独注入。
|
||||
[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh
|
||||
OC_CONFIGURED_PATH="$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')"
|
||||
if command -v oc_load_paths >/dev/null 2>&1; then
|
||||
oc_load_paths "$OC_CONFIGURED_PATH" || return 0
|
||||
OC_INSTALL_PATH="$OC_ROOT"
|
||||
else
|
||||
OC_BASE_PATH="${OC_CONFIGURED_PATH%/}"
|
||||
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"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
fi
|
||||
|
||||
# 检查 Node.js 是否已安装
|
||||
[ -x "${NODE_BASE}/bin/node" ] || return 0
|
||||
@@ -30,26 +40,32 @@ export NODE_ICU_DATA="${NODE_BASE}/share/icu"
|
||||
# 这些变量确保 openclaw 命令使用正确的配置路径
|
||||
export OPENCLAW_HOME="$OC_DATA"
|
||||
export OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw"
|
||||
export OPENCLAW_CONFIG_PATH="${OC_DATA}/.openclaw/openclaw.json"
|
||||
export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
|
||||
|
||||
# 设置 HOME 为 OpenClaw 数据目录
|
||||
# 这是解决 Issue #42 的关键:确保 OpenClaw CLI 使用正确的配置路径
|
||||
# 注意:这会影响 cd ~ 等行为,但为了配置一致性是必要的
|
||||
export HOME="$OC_DATA"
|
||||
|
||||
# 创建便捷别名:用户可直接运行 openclaw 命令
|
||||
if [ -x "${OC_GLOBAL}/bin/openclaw" ] || [ -x "${NODE_BASE}/bin/openclaw" ]; then
|
||||
# openclaw 已在 PATH 中,无需别名
|
||||
:
|
||||
# 创建便捷包装器:只给 openclaw 命令单独注入 HOME,避免污染用户 shell。
|
||||
_oc_cli=""
|
||||
if [ -x "${OC_GLOBAL}/bin/openclaw" ]; then
|
||||
_oc_cli="${OC_GLOBAL}/bin/openclaw"
|
||||
elif [ -x "${NODE_BASE}/bin/openclaw" ]; then
|
||||
_oc_cli="${NODE_BASE}/bin/openclaw"
|
||||
else
|
||||
# 尝试查找 openclaw 入口并创建别名
|
||||
for _oc_dir in "${OC_GLOBAL}/lib/node_modules/openclaw" "${OC_GLOBAL}/node_modules/openclaw" "${NODE_BASE}/lib/node_modules/openclaw"; do
|
||||
if [ -f "${_oc_dir}/openclaw.mjs" ]; then
|
||||
alias openclaw="${NODE_BASE}/bin/node ${_oc_dir}/openclaw.mjs"
|
||||
break
|
||||
elif [ -f "${_oc_dir}/dist/cli.js" ]; then
|
||||
alias openclaw="${NODE_BASE}/bin/node ${_oc_dir}/dist/cli.js"
|
||||
break
|
||||
fi
|
||||
done
|
||||
for _oc_dir in "${OC_GLOBAL}/lib/node_modules/openclaw" "${OC_GLOBAL}/node_modules/openclaw" "${NODE_BASE}/lib/node_modules/openclaw"; do
|
||||
if [ -f "${_oc_dir}/openclaw.mjs" ]; then
|
||||
_oc_cli="${NODE_BASE}/bin/node ${_oc_dir}/openclaw.mjs"
|
||||
break
|
||||
elif [ -f "${_oc_dir}/dist/cli.js" ]; then
|
||||
_oc_cli="${NODE_BASE}/bin/node ${_oc_dir}/dist/cli.js"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$_oc_cli" ]; then
|
||||
openclaw() {
|
||||
HOME="$OC_DATA" \
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
sh -c 'exec "$@"' openclaw $_oc_cli "$@"
|
||||
}
|
||||
fi
|
||||
@@ -2,9 +2,17 @@
|
||||
# luci-app-openclaw — 首次安装/升级初始化脚本
|
||||
|
||||
# 获取安装路径 (支持自定义)
|
||||
# 用户配置的是基础路径,程序会在此路径下创建 openclaw 目录
|
||||
OC_BASE_PATH="$(uci -q get openclaw.main.install_path || echo '/opt')"
|
||||
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
|
||||
# 用户配置的是基础路径,程序会在此路径下创建 openclaw 目录。
|
||||
# 这里复用统一路径解析,兼容用户误填 /mnt/data/openclaw 的情况。
|
||||
[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh
|
||||
OC_CONFIGURED_PATH="$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')"
|
||||
if command -v oc_load_paths >/dev/null 2>&1 && oc_load_paths "$OC_CONFIGURED_PATH"; then
|
||||
OC_BASE_PATH="$OPENCLAW_INSTALL_PATH"
|
||||
OC_INSTALL_PATH="$OC_ROOT"
|
||||
else
|
||||
OC_BASE_PATH="/opt"
|
||||
OC_INSTALL_PATH="/opt/openclaw"
|
||||
fi
|
||||
|
||||
# ── v1.0.16: 清理错误路径下的配置文件 (Issue #42) ──
|
||||
# 用户在 SSH 中直接运行 openclaw 命令时,可能创建了 /root/.openclaw/ 目录
|
||||
|
||||
@@ -19,19 +19,37 @@ NODE_VERSION_V1="22.15.1"
|
||||
# 默认使用 V2 版本 (可通过 NODE_VERSION 环境变量覆盖)
|
||||
NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
|
||||
# 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试)
|
||||
OC_TESTED_VERSION="2026.4.9"
|
||||
OC_TESTED_VERSION="2026.5.12"
|
||||
# OpenClaw 2026.5.x package.json engines.node 的最低要求。
|
||||
# 如果未来上游提高要求,安装成功后还会从 package.json 再做一次强校验。
|
||||
OC_NODE_MIN_VERSION="${OC_NODE_MIN_VERSION:-22.14.0}"
|
||||
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
|
||||
OC_VERSION="${OC_VERSION:-}"
|
||||
|
||||
[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh
|
||||
[ -r /usr/libexec/openclaw-node.sh ] && . /usr/libexec/openclaw-node.sh
|
||||
|
||||
# ── 安装路径 (支持自定义) ──
|
||||
# 从 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"
|
||||
# UCI 公开字段继续使用 openclaw.main.install_path,语义为“基础目录”。
|
||||
# 兼容用户误填 /mnt/data/openclaw:统一剥离末尾 openclaw,避免得到
|
||||
# /mnt/data/openclaw/openclaw 这种双拼路径。
|
||||
OC_CONFIGURED_PATH="${OC_INSTALL_PATH:-$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')}"
|
||||
if command -v oc_load_paths >/dev/null 2>&1; then
|
||||
if ! oc_load_paths "$OC_CONFIGURED_PATH"; then
|
||||
echo " [✗] 安装路径无效: $OC_CONFIGURED_PATH"
|
||||
echo " [!] 请使用绝对路径,例如 /opt、/mnt/data;不要使用相对路径、空格或 shell 特殊字符。"
|
||||
exit 1
|
||||
fi
|
||||
OC_BASE_PATH="$OPENCLAW_INSTALL_PATH"
|
||||
OC_INSTALL_PATH="$OC_ROOT"
|
||||
else
|
||||
OC_BASE_PATH="${OC_CONFIGURED_PATH%/}"
|
||||
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"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
fi
|
||||
|
||||
# ── OverlayFS 兼容性修复 ──
|
||||
# iStoreOS/OpenWrt 上 Docker 的 bind mount (/overlay/upper/opt/docker)
|
||||
@@ -82,11 +100,58 @@ ensure_mkdir() {
|
||||
[ -d "$target" ] && return 0
|
||||
if ! mkdir -p "$target" 2>/dev/null; then
|
||||
log_error "无法创建目录: $target"
|
||||
log_error "如果安装了 Docker,可能需要手动执行: mount --bind /overlay/upper/opt /opt"
|
||||
local probe_parent="/"
|
||||
command -v oc_find_existing_path >/dev/null 2>&1 && probe_parent=$(oc_find_existing_path "$target")
|
||||
log_error "请检查挂载点是否只读、overlay 是否已满,以及 ${probe_parent} 是否可写。"
|
||||
[ "$OC_BASE_PATH" = "/opt" ] && log_error "如果安装了 Docker,可能需要手动执行: mount --bind /overlay/upper/opt /opt"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_openclaw_user() {
|
||||
# OpenWrt 默认没有 useradd/adduser 的完整实现,所以沿用 uci-defaults 的轻量写法。
|
||||
# 这里在安装链路再兜底一次,避免用户删除账号后微信插件安装或 Gateway 启动失败。
|
||||
if id openclaw >/dev/null 2>&1 || grep -q '^openclaw:' /etc/passwd 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local oc_uid=1000
|
||||
while grep -q "^[^:]*:x:${oc_uid}:" /etc/passwd 2>/dev/null; do
|
||||
oc_uid=$((oc_uid + 1))
|
||||
done
|
||||
local oc_gid="$oc_uid"
|
||||
while grep -q "^[^:]*:x:${oc_gid}:" /etc/group 2>/dev/null; do
|
||||
oc_gid=$((oc_gid + 1))
|
||||
done
|
||||
|
||||
echo "openclaw:x:${oc_uid}:${oc_gid}:openclaw:${OC_DATA}:/bin/false" >> /etc/passwd
|
||||
echo 'openclaw:x:0:0:99999:7:::' >> /etc/shadow
|
||||
echo "openclaw:x:${oc_gid}:" >> /etc/group
|
||||
log_info "已创建 openclaw 系统用户 (uid=${oc_uid}, gid=${oc_gid})"
|
||||
}
|
||||
|
||||
probe_install_root() {
|
||||
if command -v oc_probe_writable_root >/dev/null 2>&1; then
|
||||
if ! oc_probe_writable_root "$OC_BASE_PATH"; then
|
||||
log_error "安装路径不可写: $OC_BASE_PATH"
|
||||
log_error "常见原因: overlay 已满、文件系统只读、挂载点未挂载或权限不正确。"
|
||||
local check_path="/"
|
||||
command -v oc_find_existing_path >/dev/null 2>&1 && check_path=$(oc_find_existing_path "$OC_BASE_PATH")
|
||||
df -h "$check_path" 2>/dev/null | tail -1 || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_partial_install() {
|
||||
# 只在确认目标是规范化后的 openclaw 运行根目录时清理,避免误删用户目录。
|
||||
if command -v oc_safe_openclaw_root >/dev/null 2>&1 && oc_safe_openclaw_root "$OC_INSTALL_PATH"; then
|
||||
log_warn "安装失败,清理未完成的运行目录: $OC_INSTALL_PATH"
|
||||
rm -rf "$OC_INSTALL_PATH" 2>/dev/null || true
|
||||
[ -d /overlay/upper ] && rm -rf "/overlay/upper${OC_INSTALL_PATH}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# 检测 C 运行时类型 (glibc vs musl)
|
||||
detect_libc() {
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
@@ -126,6 +191,38 @@ $ver_dir"
|
||||
done
|
||||
}
|
||||
|
||||
detect_oc_node_requirement() {
|
||||
local oc_entry pkg_json required
|
||||
oc_entry=$(find_oc_entry)
|
||||
[ -n "$oc_entry" ] || return 1
|
||||
pkg_json="$(dirname "$oc_entry")/package.json"
|
||||
[ -f "$pkg_json" ] || return 1
|
||||
[ -x "$NODE_BIN" ] || return 1
|
||||
|
||||
required=$(OC_PKG_JSON="$pkg_json" "$NODE_BIN" -e "
|
||||
const fs=require('fs');
|
||||
try{
|
||||
const pkg=JSON.parse(fs.readFileSync(process.env.OC_PKG_JSON,'utf8'));
|
||||
const spec=(pkg.engines&&pkg.engines.node)||'';
|
||||
const m=String(spec).match(/([0-9]+\\.[0-9]+\\.[0-9]+)/);
|
||||
if(m) process.stdout.write(m[1]);
|
||||
}catch(e){}
|
||||
" 2>/dev/null)
|
||||
[ -n "$required" ] || return 1
|
||||
printf '%s\n' "$required"
|
||||
}
|
||||
|
||||
assert_node_runtime() {
|
||||
local required="$OC_NODE_MIN_VERSION"
|
||||
local from_pkg
|
||||
from_pkg=$(detect_oc_node_requirement 2>/dev/null || true)
|
||||
[ -n "$from_pkg" ] && required="$from_pkg"
|
||||
|
||||
if command -v oc_assert_node_min_version >/dev/null 2>&1; then
|
||||
oc_assert_node_min_version "$NODE_BIN" "$required" || exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
@@ -158,6 +255,7 @@ download_node() {
|
||||
current_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')
|
||||
if [ "$current_ver" = "$node_ver" ]; then
|
||||
log_info "Node.js v${node_ver} 已安装, 跳过下载"
|
||||
assert_node_runtime
|
||||
return 0
|
||||
fi
|
||||
# ARM64 musl 使用 Alpine 打包,版本号可能不完全匹配
|
||||
@@ -166,6 +264,7 @@ download_node() {
|
||||
local want_major=$(echo "$node_ver" | cut -d. -f1)
|
||||
if [ "$cur_major" = "$want_major" ]; then
|
||||
log_info "Node.js v${current_ver} 已安装 (兼容 v${node_ver}), 跳过下载"
|
||||
assert_node_runtime
|
||||
return 0
|
||||
fi
|
||||
log_warn "当前 Node.js v${current_ver}, 将更新到 v${node_ver}"
|
||||
@@ -273,6 +372,7 @@ download_node() {
|
||||
local installed_ver
|
||||
installed_ver=$("$NODE_BIN" --version 2>/dev/null || echo "unknown")
|
||||
log_info "Node.js ${installed_ver} 安装成功"
|
||||
assert_node_runtime
|
||||
else
|
||||
log_error "Node.js 安装验证失败"
|
||||
exit 1
|
||||
@@ -309,12 +409,15 @@ install_openclaw() {
|
||||
|
||||
# 确定安装版本
|
||||
local oc_pkg="openclaw"
|
||||
if [ -n "$OC_VERSION" ]; then
|
||||
if [ -z "$OC_VERSION" ] || [ "$OC_VERSION" = "stable" ]; then
|
||||
oc_pkg="openclaw@${OC_TESTED_VERSION}"
|
||||
log_info "默认稳定版: v${OC_TESTED_VERSION}"
|
||||
elif [ "$OC_VERSION" = "latest" ]; then
|
||||
oc_pkg="openclaw@latest"
|
||||
log_info "安装 npm latest 稳定标签版本"
|
||||
else
|
||||
oc_pkg="openclaw@${OC_VERSION}"
|
||||
log_info "指定版本: v${OC_VERSION}"
|
||||
else
|
||||
oc_pkg="openclaw@latest"
|
||||
log_info "安装最新版本"
|
||||
fi
|
||||
|
||||
local libc_type
|
||||
@@ -371,6 +474,7 @@ install_openclaw() {
|
||||
|
||||
if [ -n "$oc_ver" ]; then
|
||||
log_info "OpenClaw v${oc_ver} 安装成功"
|
||||
assert_node_runtime
|
||||
else
|
||||
log_error "OpenClaw 安装验证失败"
|
||||
exit 1
|
||||
@@ -435,16 +539,23 @@ do_setup() {
|
||||
if [ -n "$OC_VERSION" ]; then
|
||||
echo " OpenClaw: v${OC_VERSION} (稳定版)"
|
||||
else
|
||||
echo " OpenClaw: 最新版"
|
||||
echo " OpenClaw: v${OC_TESTED_VERSION} (默认稳定版)"
|
||||
fi
|
||||
echo " 安装路径: ${OC_INSTALL_PATH}"
|
||||
echo " 数据路径: ${OC_DATA}"
|
||||
echo ""
|
||||
|
||||
ensure_openclaw_user
|
||||
probe_install_root
|
||||
OC_SETUP_CLEANUP=1
|
||||
trap 'rc=$?; if [ $rc -ne 0 ] && [ "${OC_SETUP_CLEANUP:-0}" = "1" ]; then cleanup_partial_install; fi; exit $rc' EXIT
|
||||
|
||||
download_node "$node_ver"
|
||||
install_pnpm
|
||||
install_openclaw
|
||||
init_openclaw
|
||||
OC_SETUP_CLEANUP=0
|
||||
trap - EXIT
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
@@ -533,7 +644,11 @@ do_upgrade() {
|
||||
|
||||
echo "=== 正在升级 OpenClaw ==="
|
||||
echo ""
|
||||
"$NPM_BIN" install -g openclaw@latest --prefix="$OC_GLOBAL" $install_flags 2>&1
|
||||
local target_pkg="openclaw@${OC_TESTED_VERSION}"
|
||||
[ "$OC_VERSION" = "latest" ] && target_pkg="openclaw@latest"
|
||||
[ -n "$OC_VERSION" ] && [ "$OC_VERSION" != "latest" ] && [ "$OC_VERSION" != "stable" ] && target_pkg="openclaw@${OC_VERSION}"
|
||||
echo "目标版本: ${target_pkg#openclaw@}"
|
||||
"$NPM_BIN" install -g "$target_pkg" --prefix="$OC_GLOBAL" $install_flags 2>&1
|
||||
|
||||
# 验证升级结果
|
||||
local new_ver=""
|
||||
@@ -547,6 +662,7 @@ do_upgrade() {
|
||||
|
||||
echo ""
|
||||
if [ -n "$new_ver" ]; then
|
||||
assert_node_runtime
|
||||
if [ "$current_ver" = "$new_ver" ]; then
|
||||
log_info "当前已是最新版本 v${new_ver}"
|
||||
else
|
||||
@@ -658,6 +774,9 @@ do_setup_offline() {
|
||||
echo " 离线包: ${offline_dir}"
|
||||
echo ""
|
||||
|
||||
ensure_openclaw_user
|
||||
probe_install_root
|
||||
|
||||
# [1] 安装 Node.js
|
||||
local node_tarball="$offline_dir/node.tar.xz"
|
||||
if [ -f "$node_tarball" ]; then
|
||||
@@ -681,6 +800,7 @@ do_setup_offline() {
|
||||
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
log_info "Node.js $($NODE_BIN --version 2>/dev/null) 安装成功"
|
||||
assert_node_runtime
|
||||
else
|
||||
log_error "Node.js 安装失败"
|
||||
exit 1
|
||||
@@ -705,6 +825,7 @@ do_setup_offline() {
|
||||
if [ -n "$oc_entry" ]; then
|
||||
local oc_ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | tr -d '[:space:]')
|
||||
log_info "OpenClaw v${oc_ver} 安装成功"
|
||||
assert_node_runtime
|
||||
else
|
||||
log_error "OpenClaw 安装验证失败"
|
||||
exit 1
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# OpenClaw Node.js 版本校验工具。
|
||||
|
||||
oc_normalize_node_version() {
|
||||
local raw="${1:-}"
|
||||
raw=${raw#v}
|
||||
case "$raw" in
|
||||
[0-9]*.[0-9]*.[0-9]*) printf '%s\n' "$raw"; return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
oc_read_node_version() {
|
||||
local node_bin="$1"
|
||||
local raw
|
||||
[ -x "$node_bin" ] || return 1
|
||||
raw=$("$node_bin" --version 2>/dev/null) || return 1
|
||||
oc_normalize_node_version "$raw"
|
||||
}
|
||||
|
||||
oc_node_version_ge() {
|
||||
local current required
|
||||
current=$(oc_normalize_node_version "$1") || return 1
|
||||
required=$(oc_normalize_node_version "$2") || return 1
|
||||
|
||||
awk -v a="$current" -v b="$required" '
|
||||
BEGIN {
|
||||
split(a, av, "."); split(b, bv, ".");
|
||||
for (i = 1; i <= 3; i++) {
|
||||
ai = av[i] + 0; bi = bv[i] + 0;
|
||||
if (ai > bi) exit 0;
|
||||
if (ai < bi) exit 1;
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
oc_assert_node_min_version() {
|
||||
local node_bin="$1"
|
||||
local required="$2"
|
||||
local current
|
||||
|
||||
current=$(oc_read_node_version "$node_bin") || {
|
||||
echo " [✗] Node.js 不可执行或无法读取版本: $node_bin"
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! oc_node_version_ge "$current" "$required"; then
|
||||
echo " [✗] Node.js v${current} 低于 OpenClaw 要求 v${required}"
|
||||
echo " [!] 请重新运行 openclaw-env node 或 openclaw-env setup 更新运行时"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " [✓] Node.js v${current} 满足要求 (>= v${required})"
|
||||
return 0
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
# luci-app-openclaw 统一路径解析工具
|
||||
#
|
||||
# 为什么单独抽出来:
|
||||
# OpenClaw 的安装根目录会被 init、openclaw-env、Web PTY、LuCI 控制器等多处使用。
|
||||
# 如果每处都自己拼路径,用户输入 /mnt/data/openclaw 时就容易得到
|
||||
# /mnt/data/openclaw/openclaw 这种错误路径,后续权限修复和卸载也会变得危险。
|
||||
|
||||
oc_normalize_install_path() {
|
||||
local raw="${1:-}"
|
||||
|
||||
# 空值统一回到 /opt,保持历史兼容。
|
||||
[ -n "$raw" ] || raw="/opt"
|
||||
|
||||
# 去掉首尾空白和末尾斜杠。OpenWrt busybox sed 支持基础表达式。
|
||||
raw=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s:/*$::')
|
||||
[ -n "$raw" ] || raw="/"
|
||||
|
||||
case "$raw" in
|
||||
/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
case "$raw" in
|
||||
*[\ \ ]*|*\'*|*\"*|*\`*|*\$*|*\;*|*\&*|*\|*|*\<*|*\>*|*\(*|*\)*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$raw" in
|
||||
/|/proc|/proc/*|/sys|/sys/*|/dev|/dev/*|/tmp|/tmp/*|/var|/var/*|/etc|/etc/*|/usr|/usr/*|/bin|/bin/*|/sbin|/sbin/*|/lib|/lib/*|/rom|/rom/*|/overlay|/overlay/*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 用户如果填写的是实际运行目录 /xxx/openclaw,这里回退到根目录 /xxx。
|
||||
case "$raw" in
|
||||
*/openclaw)
|
||||
raw=${raw%/openclaw}
|
||||
[ -n "$raw" ] || raw="/"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "$raw"
|
||||
}
|
||||
|
||||
oc_quote() {
|
||||
# POSIX 单引号转义,用于拼接少量必须经 shell 执行的命令。
|
||||
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
|
||||
}
|
||||
|
||||
oc_load_paths() {
|
||||
local input="${1:-}"
|
||||
local normalized
|
||||
|
||||
normalized=$(oc_normalize_install_path "$input") || return 1
|
||||
|
||||
OPENCLAW_INSTALL_PATH="$normalized"
|
||||
if [ "$OPENCLAW_INSTALL_PATH" = "/" ]; then
|
||||
OC_ROOT="/openclaw"
|
||||
else
|
||||
OC_ROOT="${OPENCLAW_INSTALL_PATH}/openclaw"
|
||||
fi
|
||||
NODE_BASE="${OC_ROOT}/node"
|
||||
OC_GLOBAL="${OC_ROOT}/global"
|
||||
OC_DATA="${OC_ROOT}/data"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
|
||||
export OPENCLAW_INSTALL_PATH OC_ROOT NODE_BASE OC_GLOBAL OC_DATA CONFIG_FILE
|
||||
}
|
||||
|
||||
oc_find_existing_path() {
|
||||
local path="$1"
|
||||
while [ -n "$path" ] && [ "$path" != "/" ]; do
|
||||
[ -e "$path" ] && { printf '%s\n' "$path"; return 0; }
|
||||
path=${path%/*}
|
||||
done
|
||||
printf '/\n'
|
||||
}
|
||||
|
||||
oc_probe_writable_root() {
|
||||
local base="$1"
|
||||
local probe_parent
|
||||
local probe_dir
|
||||
|
||||
probe_parent=$(oc_find_existing_path "$base")
|
||||
[ -d "$probe_parent" ] || return 1
|
||||
[ -w "$probe_parent" ] || return 1
|
||||
|
||||
probe_dir="${probe_parent}/.openclaw-write-test-$$"
|
||||
if mkdir "$probe_dir" 2>/dev/null; then
|
||||
rmdir "$probe_dir" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
oc_safe_openclaw_root() {
|
||||
local root="${1:-}"
|
||||
case "$root" in
|
||||
*/openclaw) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
case "$root" in
|
||||
/openclaw|/opt/openclaw|/mnt/*/openclaw|/media/*/openclaw|/srv/*/openclaw|/overlay/upper/opt/openclaw)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
@@ -33,10 +33,20 @@ get_pid_by_port() {
|
||||
}
|
||||
|
||||
# ── 路径 (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"
|
||||
# 从 UCI 配置读取自定义路径,环境变量可覆盖。
|
||||
# 注意 OC_INSTALL_PATH 历史上被当作“基础路径”传入,统一解析后再展开到 openclaw 根目录。
|
||||
[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh
|
||||
OC_CONFIGURED_PATH="${OC_INSTALL_PATH:-$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')}"
|
||||
if command -v oc_load_paths >/dev/null 2>&1 && oc_load_paths "$OC_CONFIGURED_PATH"; then
|
||||
OC_INSTALL_PATH="$OC_ROOT"
|
||||
else
|
||||
OC_BASE_PATH="${OC_CONFIGURED_PATH%/}"
|
||||
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}"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
fi
|
||||
NODE_BASE="${NODE_BASE:-${OC_INSTALL_PATH}/node}"
|
||||
OC_GLOBAL="${OC_GLOBAL:-${OC_INSTALL_PATH}/global}"
|
||||
OC_DATA="${OC_DATA:-${OC_INSTALL_PATH}/data}"
|
||||
|
||||
@@ -19,10 +19,19 @@ const PORT = parseInt(process.env.OC_CONFIG_PORT || '18793', 10);
|
||||
const HOST = process.env.OC_CONFIG_HOST || '0.0.0.0'; // token 认证保护,可安全绑定所有接口
|
||||
// 从 UCI 读取安装路径,默认为 /opt/openclaw
|
||||
const { execSync } = require('child_process');
|
||||
function normalizeInstallPath(raw) {
|
||||
let value = String(raw || '').trim().replace(/\/+$/, '');
|
||||
if (!value) value = '/opt';
|
||||
if (!value.startsWith('/')) return '/opt';
|
||||
if (/[\s'"`$;&|<>()]/.test(value)) return '/opt';
|
||||
if (/^\/(proc|sys|dev|tmp|var|etc|usr|bin|sbin|lib|rom|overlay)(\/|$)/.test(value) || value === '/') return '/opt';
|
||||
if (value.endsWith('/openclaw')) value = value.slice(0, -'/openclaw'.length) || '/opt';
|
||||
return value;
|
||||
}
|
||||
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';
|
||||
if (uciPath) installPath = normalizeInstallPath(uciPath) + '/openclaw';
|
||||
} catch {}
|
||||
const NODE_BASE = process.env.NODE_BASE || installPath + '/node';
|
||||
const OC_GLOBAL = process.env.OC_GLOBAL || installPath + '/global';
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
{
|
||||
"luci-app-openclaw": {
|
||||
"description": "Grant UCI access for luci-app-openclaw",
|
||||
"description": "Grant access to the OpenClaw LuCI application",
|
||||
"read": {
|
||||
"uci": [ "openclaw" ]
|
||||
"uci": [ "openclaw" ],
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "openclaw" ]
|
||||
"uci": [ "openclaw" ],
|
||||
"ubus": {
|
||||
"service": [ "list", "set", "delete" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,10 +51,19 @@ mkdir -p "$DATA_DIR/usr/bin"
|
||||
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$DATA_DIR/usr/bin/"
|
||||
chmod +x "$DATA_DIR/usr/bin/openclaw-env"
|
||||
|
||||
# shared shell helpers
|
||||
mkdir -p "$DATA_DIR/usr/libexec"
|
||||
cp "$PKG_DIR/root/usr/libexec/"*.sh "$DATA_DIR/usr/libexec/"
|
||||
chmod +x "$DATA_DIR/usr/libexec/"*.sh
|
||||
|
||||
# LuCI controller
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/controller"
|
||||
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$DATA_DIR/usr/lib/lua/luci/controller/"
|
||||
|
||||
# shared Lua helpers
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/openclaw"
|
||||
cp "$PKG_DIR/luasrc/openclaw/"*.lua "$DATA_DIR/usr/lib/lua/openclaw/"
|
||||
|
||||
# LuCI CBI
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw"
|
||||
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw/"
|
||||
@@ -63,6 +72,10 @@ cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$DATA_DIR/usr/lib/lua/luci/model/
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/view/openclaw"
|
||||
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$DATA_DIR/usr/lib/lua/luci/view/openclaw/"
|
||||
|
||||
# rpcd ACL
|
||||
mkdir -p "$DATA_DIR/usr/share/rpcd/acl.d"
|
||||
cp "$PKG_DIR/root/usr/share/rpcd/acl.d/"*.json "$DATA_DIR/usr/share/rpcd/acl.d/"
|
||||
|
||||
# oc-config assets
|
||||
mkdir -p "$DATA_DIR/usr/share/openclaw"
|
||||
cp "$PKG_DIR/VERSION" "$DATA_DIR/usr/share/openclaw/VERSION"
|
||||
@@ -78,12 +91,6 @@ mkdir -p "$DATA_DIR/etc/profile.d"
|
||||
cp "$PKG_DIR/root/etc/profile.d/openclaw.sh" "$DATA_DIR/etc/profile.d/"
|
||||
chmod +x "$DATA_DIR/etc/profile.d/openclaw.sh"
|
||||
|
||||
# i18n (po2lmo 可选)
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/i18n"
|
||||
if command -v po2lmo >/dev/null 2>&1 && [ -f "$PKG_DIR/po/zh-cn/openclaw.po" ]; then
|
||||
po2lmo "$PKG_DIR/po/zh-cn/openclaw.po" "$DATA_DIR/usr/lib/lua/luci/i18n/openclaw.zh-cn.lmo" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 计算安装大小
|
||||
INSTALLED_SIZE=$(du -sk "$DATA_DIR" | awk '{print $1}')
|
||||
|
||||
@@ -132,6 +139,7 @@ cat > "$CTRL_DIR/postinst" << 'EOF'
|
||||
USER_BIND=$(sed -n "s/^\s*option\s\+bind\s\+['\"]\\?\\([^'\"]*\\)['\"]\\?.*/\\1/p" "$OLD_CONFIG" 2>/dev/null | tail -1)
|
||||
USER_TOKEN=$(sed -n "s/^\s*option\s\+token\s\+['\"]\\?\\([^'\"]*\\)['\"]\\?.*/\\1/p" "$OLD_CONFIG" 2>/dev/null | tail -1)
|
||||
USER_PTY_PORT=$(sed -n "s/^\s*option\s\+pty_port\s\+['\"]\\?\\([^'\"]*\\)['\"]\\?.*/\\1/p" "$OLD_CONFIG" 2>/dev/null | tail -1)
|
||||
USER_INSTALL_PATH=$(sed -n "s/^\s*option\s\+install_path\s\+['\"]\\?\\([^'\"]*\\)['\"]\\?.*/\\1/p" "$OLD_CONFIG" 2>/dev/null | tail -1)
|
||||
|
||||
# 步骤2: 备份旧配置 (带时间戳)
|
||||
BAK_FILE="/etc/config/openclaw.$(date +%Y%m%d%H%M%S).bak"
|
||||
@@ -149,6 +157,7 @@ cat > "$CTRL_DIR/postinst" << 'EOF'
|
||||
[ -n "$USER_BIND" ] && sed -i "s/^\(\s*option\s\+bind\s\+\).*/\\1'$USER_BIND'/" "$OLD_CONFIG" 2>/dev/null || true
|
||||
[ -n "$USER_TOKEN" ] && sed -i "s/^\(\s*option\s\+token\s\+\).*/\\1'$USER_TOKEN'/" "$OLD_CONFIG" 2>/dev/null || true
|
||||
[ -n "$USER_PTY_PORT" ] && sed -i "s/^\(\s*option\s\+pty_port\s\+\).*/\\1'$USER_PTY_PORT'/" "$OLD_CONFIG" 2>/dev/null || true
|
||||
[ -n "$USER_INSTALL_PATH" ] && sed -i "s/^\(\s*option\s\+install_path\s\+\).*/\\1'$USER_INSTALL_PATH'/" "$OLD_CONFIG" 2>/dev/null || true
|
||||
|
||||
echo "配置合并完成,用户设置已保留"
|
||||
fi
|
||||
|
||||
@@ -54,10 +54,19 @@ install_files() {
|
||||
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$dest/usr/bin/"
|
||||
chmod +x "$dest/usr/bin/openclaw-env"
|
||||
|
||||
# shared shell helpers
|
||||
mkdir -p "$dest/usr/libexec"
|
||||
cp "$PKG_DIR/root/usr/libexec/"*.sh "$dest/usr/libexec/"
|
||||
chmod +x "$dest/usr/libexec/"*.sh
|
||||
|
||||
# LuCI controller
|
||||
mkdir -p "$dest/usr/lib/lua/luci/controller"
|
||||
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$dest/usr/lib/lua/luci/controller/"
|
||||
|
||||
# shared Lua helpers
|
||||
mkdir -p "$dest/usr/lib/lua/openclaw"
|
||||
cp "$PKG_DIR/luasrc/openclaw/"*.lua "$dest/usr/lib/lua/openclaw/"
|
||||
|
||||
# LuCI CBI
|
||||
mkdir -p "$dest/usr/lib/lua/luci/model/cbi/openclaw"
|
||||
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$dest/usr/lib/lua/luci/model/cbi/openclaw/"
|
||||
@@ -66,6 +75,10 @@ install_files() {
|
||||
mkdir -p "$dest/usr/lib/lua/luci/view/openclaw"
|
||||
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$dest/usr/lib/lua/luci/view/openclaw/"
|
||||
|
||||
# rpcd ACL
|
||||
mkdir -p "$dest/usr/share/rpcd/acl.d"
|
||||
cp "$PKG_DIR/root/usr/share/rpcd/acl.d/"*.json "$dest/usr/share/rpcd/acl.d/"
|
||||
|
||||
# oc-config assets
|
||||
mkdir -p "$dest/usr/share/openclaw"
|
||||
cp "$PKG_DIR/VERSION" "$dest/usr/share/openclaw/VERSION"
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q "OC_TESTED_VERSION=\"2026.5.12\"" root/usr/bin/openclaw-env || fail "tested OpenClaw version not pinned"
|
||||
grep -q "oc_assert_node_min_version" root/usr/bin/openclaw-env || fail "Node.js minimum version check missing"
|
||||
|
||||
grep -q "wechat.htm" Makefile || fail "Makefile must install wechat.htm"
|
||||
grep -q "luci-app-openclaw.json" Makefile || fail "Makefile must install rpcd ACL"
|
||||
if grep -q "openclaw.zh-cn.lmo" Makefile; then
|
||||
fail "main package must not install openclaw.zh-cn.lmo"
|
||||
fi
|
||||
|
||||
if grep -q 'export HOME="$OC_DATA"' root/etc/profile.d/openclaw.sh; then
|
||||
fail "profile must not export HOME globally"
|
||||
fi
|
||||
grep -q 'HOME="$OC_DATA"' root/etc/profile.d/openclaw.sh || fail "openclaw wrapper must inject HOME locally"
|
||||
|
||||
if grep -q "chmod -R 777" luasrc/controller/openclaw.lua; then
|
||||
fail "uninstall path must not chmod -R 777"
|
||||
fi
|
||||
grep -q "is_safe_openclaw_root" luasrc/controller/openclaw.lua || fail "uninstall safety check missing"
|
||||
grep -q "local q_install_path = shellquote(install_path)" luasrc/controller/openclaw.lua || fail "uninstall rm must shellquote install_path"
|
||||
grep -q "rm -rf \" .. q_install_path" luasrc/controller/openclaw.lua || fail "uninstall rm must use quoted install_path"
|
||||
|
||||
grep -q "command -v python3" luasrc/controller/openclaw.lua || fail "wechat install must check python3"
|
||||
grep -q "ensure_openclaw_user" luasrc/controller/openclaw.lua || fail "wechat install must ensure openclaw user"
|
||||
grep -q "NPM_CONFIG_CACHE" luasrc/controller/openclaw.lua || fail "wechat install must set npm cache"
|
||||
grep -q "openclaw-weixin" root/etc/init.d/openclaw || fail "weixin channel migration missing"
|
||||
|
||||
grep -q "var url = 'http://'" luasrc/view/openclaw/console.htm || fail "console must force HTTP gateway URL"
|
||||
grep -q "existing.src" luasrc/view/openclaw/console.htm || fail "console iframe must refresh token URL"
|
||||
|
||||
grep -q "root/usr/libexec" scripts/build_ipk.sh || fail "ipk script must package shell helpers"
|
||||
grep -q "root/usr/libexec" scripts/build_run.sh || fail "run script must package shell helpers"
|
||||
|
||||
echo "ok"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
. ./root/usr/libexec/openclaw-node.sh
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$(oc_normalize_node_version v22.16.0)" = "22.16.0" ] || fail "normalize v"
|
||||
oc_node_version_ge 22.16.0 22.16.0 || fail "exact version"
|
||||
oc_node_version_ge 22.16.1 22.16.0 || fail "patch version"
|
||||
oc_node_version_ge 23.0.0 22.16.0 || fail "major version"
|
||||
if oc_node_version_ge 22.15.1 22.16.0; then
|
||||
fail "older minor accepted"
|
||||
fi
|
||||
|
||||
echo "ok"
|
||||
@@ -0,0 +1,32 @@
|
||||
package.path = "./luasrc/?.lua;./luasrc/?/?.lua;" .. package.path
|
||||
|
||||
local paths = require "openclaw.paths"
|
||||
|
||||
local function eq(actual, expected, label)
|
||||
if actual ~= expected then
|
||||
error(string.format("%s: expected %q, got %q", label, tostring(expected), tostring(actual)), 2)
|
||||
end
|
||||
end
|
||||
|
||||
local function check(input, expected_base, expected_root)
|
||||
local derived = paths.derive_paths(input)
|
||||
eq(derived.install_path, expected_base, "install_path")
|
||||
eq(derived.oc_root, expected_root, "oc_root")
|
||||
eq(derived.node_base, expected_root .. "/node", "node_base")
|
||||
eq(derived.oc_global, expected_root .. "/global", "oc_global")
|
||||
eq(derived.oc_data, expected_root .. "/data", "oc_data")
|
||||
end
|
||||
|
||||
check(nil, "/opt", "/opt/openclaw")
|
||||
check("", "/opt", "/opt/openclaw")
|
||||
check("/mnt/data", "/mnt/data", "/mnt/data/openclaw")
|
||||
check("/mnt/data/openclaw", "/mnt/data", "/mnt/data/openclaw")
|
||||
check("/mnt/data/", "/mnt/data", "/mnt/data/openclaw")
|
||||
|
||||
eq(paths.normalize_install_path("relative/path"), nil, "relative path rejected")
|
||||
eq(paths.normalize_install_path("/mnt/data bad"), nil, "space rejected")
|
||||
eq(paths.normalize_install_path("/"), nil, "root rejected")
|
||||
eq(paths.is_safe_openclaw_root("/mnt/data/openclaw"), true, "safe root")
|
||||
eq(paths.is_safe_openclaw_root("/mnt/data"), false, "unsafe root")
|
||||
|
||||
print("ok")
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
. ./root/usr/libexec/openclaw-paths.sh
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
oc_load_paths "/mnt/data/openclaw"
|
||||
[ "$OPENCLAW_INSTALL_PATH" = "/mnt/data" ] || fail "strip trailing openclaw"
|
||||
[ "$OC_ROOT" = "/mnt/data/openclaw" ] || fail "derive root"
|
||||
|
||||
if oc_normalize_install_path "relative/path" >/dev/null 2>&1; then
|
||||
fail "relative path must be rejected"
|
||||
fi
|
||||
|
||||
if oc_normalize_install_path "/mnt/data bad" >/dev/null 2>&1; then
|
||||
fail "path with whitespace must be rejected"
|
||||
fi
|
||||
|
||||
oc_safe_openclaw_root "/mnt/data/openclaw" || fail "safe root rejected"
|
||||
if oc_safe_openclaw_root "/mnt/data"; then
|
||||
fail "non-openclaw root accepted"
|
||||
fi
|
||||
|
||||
quoted=$(oc_quote "/mnt/a'b/openclaw")
|
||||
[ "$quoted" = "'/mnt/a'\\''b/openclaw'" ] || fail "shell quote"
|
||||
|
||||
echo "ok"
|
||||
Reference in new issue
Block a user