mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-04 00:29:27 +08:00
🍉 Sync 2026-05-16 08:37:13
This commit is contained in:
parent
004b1bc214
commit
ccfd18b795
@ -3,7 +3,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=easytier-noweb
|
||||
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.2)
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
ifeq ($(ARCH),mipsel)
|
||||
APP_ARCH:=mipsel
|
||||
@ -37,6 +37,8 @@ PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
RSTRIP:=:
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
@ -64,8 +66,10 @@ endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-core $(1)/usr/bin/easytier-core
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-cli $(1)/usr/bin/easytier-cli
|
||||
$(CP) $(PKG_BUILD_DIR)/easytier-core $(1)/usr/bin/
|
||||
chmod 0755 $(1)/usr/bin/easytier-core
|
||||
$(CP) $(PKG_BUILD_DIR)/easytier-cli $(1)/usr/bin/
|
||||
chmod 0755 $(1)/usr/bin/easytier-cli
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@ -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 " ..
|
||||
|
||||
64
luci-app-openclaw/luasrc/openclaw/paths.lua
Normal file
64
luci-app-openclaw/luasrc/openclaw/paths.lua
Normal file
@ -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
luci-app-openclaw/root/usr/libexec/openclaw-node.sh
Executable file
57
luci-app-openclaw/root/usr/libexec/openclaw-node.sh
Executable file
@ -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
luci-app-openclaw/root/usr/libexec/openclaw-paths.sh
Executable file
111
luci-app-openclaw/root/usr/libexec/openclaw-paths.sh
Executable file
@ -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
luci-app-openclaw/tests/test_openclaw_contracts.sh
Executable file
41
luci-app-openclaw/tests/test_openclaw_contracts.sh
Executable file
@ -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
luci-app-openclaw/tests/test_openclaw_node.sh
Executable file
19
luci-app-openclaw/tests/test_openclaw_node.sh
Executable file
@ -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"
|
||||
32
luci-app-openclaw/tests/test_openclaw_paths.lua
Normal file
32
luci-app-openclaw/tests/test_openclaw_paths.lua
Normal file
@ -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
luci-app-openclaw/tests/test_openclaw_paths.sh
Executable file
31
luci-app-openclaw/tests/test_openclaw_paths.sh
Executable file
@ -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"
|
||||
@ -15,6 +15,7 @@
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"terser": "^5.43.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vite": "^7.1.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
|
||||
|
||||
@ -29,6 +29,9 @@ importers:
|
||||
terser:
|
||||
specifier: ^5.43.1
|
||||
version: 5.46.0
|
||||
tw-animate-css:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
vite:
|
||||
specifier: ^7.1.1
|
||||
version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)
|
||||
@ -244,66 +247,79 @@ packages:
|
||||
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
|
||||
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.57.1':
|
||||
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.57.1':
|
||||
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.57.1':
|
||||
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.57.1':
|
||||
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
|
||||
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.57.1':
|
||||
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
|
||||
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.57.1':
|
||||
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.57.1':
|
||||
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.57.1':
|
||||
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.57.1':
|
||||
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.57.1':
|
||||
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
|
||||
@ -373,24 +389,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
@ -518,24 +538,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@ -679,6 +703,9 @@ packages:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tw-animate-css@1.4.0:
|
||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
@ -1174,6 +1201,8 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tw-animate-css@1.4.0: {}
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0):
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
nocompatible: true;
|
||||
}
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: oklch(0.968 0.007 247.896);
|
||||
--foreground: oklch(0.208 0.042 265.755);
|
||||
@ -75,6 +77,7 @@
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--container-max-width: 80rem;
|
||||
--radius-base: 0.5rem;
|
||||
}
|
||||
|
||||
[data-darkmode="true"] {
|
||||
@ -206,6 +209,15 @@
|
||||
|
||||
--spacing: var(--spacing);
|
||||
--container-max-width: var(--container-max-width);
|
||||
|
||||
--radius-sm: calc(var(--radius-base) * 0.25);
|
||||
--radius: calc(var(--radius-base) * 0.5);
|
||||
--radius-md: calc(var(--radius-base) * 0.75);
|
||||
--radius-lg: var(--radius-base);
|
||||
--radius-xl: calc(var(--radius-base) * 1.5);
|
||||
--radius-2xl: calc(var(--radius-base) * 2);
|
||||
--radius-3xl: calc(var(--radius-base) * 3);
|
||||
--radius-4xl: calc(var(--radius-base) * 4);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@ -434,7 +446,7 @@
|
||||
@apply max-w-max-width mx-auto min-h-[calc(100vh-4rem)] w-23/24 px-4 max-md:w-full max-md:px-3;
|
||||
|
||||
#view {
|
||||
@apply md:border-border/30 md:bg-page-bg md:hover:border-border/50 mx-0 w-full p-0 shadow-none transition-all duration-300 empty:hidden md:rounded-4xl md:border md:p-6 md:shadow-xl md:hover:shadow-2xl;
|
||||
@apply animate-in fade-in-0 slide-in-from-top-2 fill-mode-both md:border-border/30 md:bg-page-bg md:hover:border-border/50 mx-0 w-full p-0 shadow-none transition-all duration-300 empty:hidden md:rounded-4xl md:border md:p-6 md:shadow-xl md:hover:shadow-2xl;
|
||||
|
||||
.cbi-title-section {
|
||||
@apply mb-6 leading-relaxed max-md:mx-2 max-md:mb-3;
|
||||
@ -1949,7 +1961,7 @@
|
||||
@apply relative inline-block pl-8!;
|
||||
@apply before:absolute before:top-1/2 before:left-[6px] before:size-5 before:-translate-y-1/2 before:animate-spin before:bg-current before:[mask:url('@assets/icons/spinning.svg')_center/cover_no-repeat];
|
||||
#view > & {
|
||||
@apply first:mx-auto first:grid first:min-h-[15vh] first:w-fit first:place-items-center first:max-md:min-h-dvh;
|
||||
@apply first:mx-auto first:grid first:min-h-48 first:w-fit first:place-items-center first:max-md:min-h-[calc(100dvh-4rem)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
luci-theme-aurora/.gitignore
vendored
5
luci-theme-aurora/.gitignore
vendored
@ -7,4 +7,7 @@ yarn.lock
|
||||
.env
|
||||
|
||||
# System Configuration File
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Superpowers AI workflow docs
|
||||
docs/superpowers/
|
||||
@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=0.11.5
|
||||
PKG_RELEASE:=8
|
||||
PKG_VERSION:=0.11.7
|
||||
PKG_RELEASE:=9
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
@ -19,16 +19,6 @@
|
||||
<img src="https://raw.githubusercontent.com/eamonxg/assets/master/aurora/preview/theme/multi-device-showcase.png" alt="Multi-Device Showcase" width="100%">
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> **🤖 Exploring AI Workflows...**
|
||||
>
|
||||
> Hey everyone — I've been pretty busy with work lately, so my time for Aurora has been limited. But I don't want the project to stall, so I've been exploring AI-assisted workflows to help pick up the slack, especially on the debugging side.
|
||||
>
|
||||
> There's a backlog of open issues right now, and honestly, most of them are styling compatibility problems with third-party plugins. These aren't huge issues individually, but they're incredibly time-consuming to track down — you have to install the plugin, sometimes set up specific conditions on the router just to reproduce the problem.
|
||||
>
|
||||
> The root cause is that LuCI only provides a basic set of web components. When plugin authors need more complex UI, they end up rolling their own HTML/CSS/JS with no shared conventions, so compatibility headaches are inevitable. With the sheer number of LuCI plugins out there, getting them all to play nicely is a massive undertaking — especially when some plugins don't even follow LuCI's own JS conventions.
|
||||
>
|
||||
> So I'm bringing AI into the loop to handle these tedious but important tasks, and ideally automate as much of it as possible. If you have better ideas or suggestions, I'd love to hear them — come chat on [Discord](https://discord.gg/EBncRrzfTw).
|
||||
|
||||
## Features
|
||||
|
||||
@ -80,36 +70,16 @@ OpenWrt 25.12+ and snapshots use `apk`; other versions use `opkg`:
|
||||
|
||||
## Contributing
|
||||
|
||||
<details>
|
||||
<summary><b>Development</b></summary>
|
||||
Aurora uses **Vite** and a modern front-end toolchain, and is experimenting with end-to-end AI integration across the full development workflow. See [Development Documentation](.dev/docs/DEVELOPMENT.md) to get started. Suggestions and PRs are always welcome.
|
||||
|
||||
<p>As we step into 2026, front-end toolchains have become mature and advanced, and modern browsers now support a wide range of exciting new features. Fully embracing the modern front-end ecosystem is at the core of the Aurora theme.</p>
|
||||
|
||||
<p>Aurora uses <strong>Vite</strong> as the build tool.</p>
|
||||
|
||||
<ul>
|
||||
<li>During development, you can freely choose and integrate any CSS toolchain.</li>
|
||||
<li>In production, multiple strategies are available to optimize bundled assets.</li>
|
||||
<li>Most importantly, with the help of a local proxy server, style changes can be previewed in real time!</li>
|
||||
</ul>
|
||||
|
||||
<p>For more development information, see <a href=".dev/docs/DEVELOPMENT.md">Development Documentation</a>.</p>
|
||||
|
||||
</details>
|
||||
|
||||
<h4>Community</h4>
|
||||
|
||||
<p>I've got a Discord server going - come hang out and chat! 👋 <a href="https://discord.gg/EBncRrzfTw">Join Discord</a></p>
|
||||
[discord.gg/EBncRrzfTw](https://discord.gg/EBncRrzfTw)
|
||||
|
||||
## License & Credits
|
||||
|
||||
This project is licensed under the Apache License 2.0.
|
||||
[Apache 2.0](LICENSE). Thanks to:
|
||||
|
||||
The production build structure is based on the official OpenWrt theme
|
||||
[luci-theme-bootstrap](https://github.com/openwrt/luci/tree/master/themes/luci-theme-bootstrap).
|
||||
|
||||
Visual style and some implementations are influenced by
|
||||
[Apple](https://www.apple.com/) and [Vercel](https://vercel.com/).
|
||||
|
||||
The project is developed using a modern front-end stack, including
|
||||
[Vite](https://vitejs.dev/) and [Tailwind CSS](https://tailwindcss.com/).
|
||||
- [luci-theme-bootstrap](https://github.com/openwrt/luci/tree/master/themes/luci-theme-bootstrap)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Claude Code](https://claude.ai/code)
|
||||
- [Apple](https://www.apple.com/) and [Vercel](https://vercel.com/) — design inspiration
|
||||
|
||||
@ -19,16 +19,6 @@
|
||||
<img src="https://raw.githubusercontent.com/eamonxg/assets/master/aurora/preview/theme/multi-device-showcase.png" alt="Multi-Device Showcase" width="100%">
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> **🤖 AI 工作流探索中…**
|
||||
>
|
||||
> 大家好,最近工作比较忙,能投入到 Aurora 主题的时间有限,但我不希望 Aurora 因此停滞不前,所以最近一直在研究 AI 工作流,希望借助 AI 来分担一部分工作,尤其是调试方面。
|
||||
>
|
||||
> 目前主题积压了不少 issue,其实大部分都是第三方插件的样式兼容问题。这类问题说大不大,但调起来真的很耗时间——你得先装上对应的插件,有时候路由器还得满足特定条件才能复现。
|
||||
>
|
||||
> 归根结底,LuCI 本身只提供了一套基础的 Web 组件,插件作者想实现更复杂的交互,就只能用 HTML + CSS + JS 自行实现,没有统一的规范,各种兼容问题在所难免。LuCI 插件数量庞大,要逐一做好兼容,工作量可想而知——更何况有些插件本身也没有按照 LuCI JS 的规范来开发。
|
||||
>
|
||||
> 所以我希望引入 AI,把这些琐碎但繁重的任务交给它来完成,如果能实现自动化那就更理想了。如果你有更好的建议或想法,欢迎来 [Discord](https://discord.gg/EBncRrzfTw) 聊聊。
|
||||
|
||||
## 特性
|
||||
|
||||
@ -81,33 +71,16 @@ OpenWrt 25.12+ 和 Snapshot 版本使用 `apk`;其他版本使用 `opkg`:
|
||||
|
||||
## 加入贡献
|
||||
|
||||
<details>
|
||||
<summary><b>关于开发</b></summary>
|
||||
Aurora 使用 **Vite** 与现代前端工具链构建,并尝试将 AI 融入开发全链路。详见[开发文档](.dev/docs/DEVELOPMENT.md)。欢迎提交建议或 PR。
|
||||
|
||||
<p>步入 2026 年,前端工具链已相当成熟先进,并且现代浏览器也支持了很多有趣的新特性。所以全面拥抱现代前端生态是 Aurora 主题的核心理念。</p>
|
||||
|
||||
<p>Aurora 使用 <strong>Vite</strong> 作为构建工具。</p>
|
||||
|
||||
<ul>
|
||||
<li>在开发过程中,您可以自由选择并集成任何 CSS 工具链。</li>
|
||||
<li>在生产环境中,提供多种策略来优化打包资源。</li>
|
||||
<li>最重要的是,借助于本地代理服务器,样式的修改可以实时预览!</li>
|
||||
</ul>
|
||||
|
||||
<p>更多开发信息,请参阅 <a href=".dev/docs/DEVELOPMENT.md">开发文档</a>。</p>
|
||||
|
||||
</details>
|
||||
|
||||
<h4>社区</h4>
|
||||
|
||||
<p>我建立了一个 Discord 服务器 —— 欢迎来聊天!👋 <a href="https://discord.gg/EBncRrzfTw">加入 Discord</a></p>
|
||||
[discord.gg/EBncRrzfTw](https://discord.gg/EBncRrzfTw)
|
||||
|
||||
## 许可与致谢
|
||||
|
||||
本项目基于 Apache License 2.0 许可开源。
|
||||
[Apache 2.0](LICENSE)。致谢:
|
||||
|
||||
生产构建结构基于官方 OpenWrt 主题 [luci-theme-bootstrap](https://github.com/openwrt/luci/tree/master/themes/luci-theme-bootstrap)。
|
||||
|
||||
视觉风格和部分实现受到 [Apple](https://www.apple.com/) 和 [Vercel](https://vercel.com/) 的影响。
|
||||
|
||||
本项目使用现代前端技术栈开发,包括 [Vite](https://vitejs.dev/) 和 [Tailwind CSS](https://tailwindcss.com/)。
|
||||
- [luci-theme-bootstrap](https://github.com/openwrt/luci/tree/master/themes/luci-theme-bootstrap)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Claude Code](https://claude.ai/code)
|
||||
- [Apple](https://www.apple.com/) 和 [Vercel](https://vercel.com/) — 设计灵感
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user