🍉 Sync 2026-05-16 08:37:13

This commit is contained in:
github-actions[bot] 2026-05-16 08:37:13 +08:00
parent 004b1bc214
commit ccfd18b795
32 changed files with 1145 additions and 234 deletions

View File

@ -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)))

View File

@ -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

View File

@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclaw
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
PKG_RELEASE:=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

View File

@ -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/

View File

@ -1 +1 @@
2.0.4
2.0.5

View File

@ -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 -einit_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 " ..

View 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

View File

@ -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 当前只监听 HTTPLuCI 使用 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';

View File

@ -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>未监听';
}

View File

@ -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

View File

@ -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

View File

@ -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/ 目录

View File

@ -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

View 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
}

View 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
}

View File

@ -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}"

View File

@ -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';

View File

@ -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" ]
}
}
}
}

View File

@ -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

View File

@ -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"

View 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"

View 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"

View 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")

View 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"

View File

@ -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"

View File

@ -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):

View File

@ -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)];
}
}
}

View File

@ -7,4 +7,7 @@ yarn.lock
.env
# System Configuration File
.DS_Store
.DS_Store
# Superpowers AI workflow docs
docs/superpowers/

View File

@ -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:=

View File

@ -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

View File

@ -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