diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile index cec243f2..46800430 100644 --- a/hickory-dns/Makefile +++ b/hickory-dns/Makefile @@ -1,13 +1,13 @@ include $(TOPDIR)/rules.mk PKG_NAME:=hickory-dns -PKG_VERSION:=2026.03.31-b57b7ae -PKG_RELEASE:=7 +PKG_VERSION:=2026.04.01~b57b7ae +PKG_RELEASE:=8 HICKORY_COMMIT:=b57b7aec2c6c18d64b90f01c97595171963e0e56 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/b57b7aec2c6c18d64b90f01c97595171963e0e56 +PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/b57b7aec2c6c18d64b90f01c97595171963e0e56? PKG_HASH:=skip PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-b57b7aec2c6c18d64b90f01c97595171963e0e56 diff --git a/luci-app-openclaw/CHANGELOG.md b/luci-app-openclaw/CHANGELOG.md index 6bafc8e7..ee1651d7 100644 --- a/luci-app-openclaw/CHANGELOG.md +++ b/luci-app-openclaw/CHANGELOG.md @@ -8,6 +8,16 @@ ### 新增功能 +#### 适配微信通道插件 (openclaw-weixin) + +- **多账号操作面板**: 新增完善的多账号登录登出功能机制,重构 Lua API (`login_cmd`/`logout_cmd`/`install_cmd`/`upgrade_cmd`) 使其支持精细化的多实例运行调度。 +- **登录交互优化**: 解决由于未完成登录流程导致的环境锁定问题,后台新增自愈能力 (`pkill -f 'channels login.*openclaw-weixin'`)。 +- **安全沙箱无缝衔接**: 修复了 Web 触发命令时的 `blocked plugin candidate: suspicious ownership` 安全警告,完整平滑降权至 `openclaw` 用户组执行。 + +**💡 微信通道使用细节提醒:** +1. **多开隔离**:所有上号的微信机器人互不干扰,尽管在 Web 控制台大盘上聊天日志看起来像在一起,但由于后端存在独立的 `BotID / AccountID / Session` 多元结构体鉴权区分,他们在底层是**绝对隔离**的记忆! +2. **退出登录**:遇到登录卡死或想强制下线,点击界面对应微信号的“退出登录”即可强制切除对应后端通道进程。 + #### 支持自定义安装路径 - **功能描述**: 用户现在可以将 OpenClaw 运行环境安装到自定义路径(如第二块硬盘 `/mnt/data`) diff --git a/luci-app-openclaw/Makefile b/luci-app-openclaw/Makefile index df74f38b..26c64721 100644 --- a/luci-app-openclaw/Makefile +++ b/luci-app-openclaw/Makefile @@ -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:=5 +PKG_RELEASE:=6 PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com> PKG_LICENSE:=GPL-3.0 diff --git a/luci-app-openclaw/luasrc/controller/openclaw.lua b/luci-app-openclaw/luasrc/controller/openclaw.lua index 2b441191..139d9f34 100644 --- a/luci-app-openclaw/luasrc/controller/openclaw.lua +++ b/luci-app-openclaw/luasrc/controller/openclaw.lua @@ -12,6 +12,9 @@ function index() -- 配置管理 (View — 嵌入 oc-config Web 终端) entry({"admin", "services", "openclaw", "advanced"}, template("openclaw/advanced"), _("配置管理"), 20).leaf = true + -- 微信配置 (View — 微信渠道配置向导) + entry({"admin", "services", "openclaw", "wechat"}, template("openclaw/wechat"), _("微信配置"), 25).leaf = true + -- Web 控制台 (View — 嵌入 OpenClaw Web UI) entry({"admin", "services", "openclaw", "console"}, template("openclaw/console"), _("Web 控制台"), 30).leaf = true @@ -44,6 +47,67 @@ function index() -- 系统配置检测 API (安装前检测) entry({"admin", "services", "openclaw", "check_system"}, call("action_check_system"), nil).leaf = true + + -- 微信状态 API (检测插件安装和登录状态) + entry({"admin", "services", "openclaw", "wechat_status"}, call("action_wechat_status"), nil).leaf = true + + -- 微信插件安装 API (后台安装) + entry({"admin", "services", "openclaw", "wechat_install"}, call("action_wechat_install"), nil).leaf = true + + -- 微信安装日志轮询 API + entry({"admin", "services", "openclaw", "wechat_install_log"}, call("action_wechat_install_log"), nil).leaf = true + + -- 微信登录 API (启动登录流程) + entry({"admin", "services", "openclaw", "wechat_login"}, call("action_wechat_login"), nil).leaf = true + + -- 微信登录状态/二维码 API + entry({"admin", "services", "openclaw", "wechat_login_status"}, call("action_wechat_login_status"), nil).leaf = true + + -- 微信插件卸载 API + entry({"admin", "services", "openclaw", "wechat_uninstall"}, call("action_wechat_uninstall"), nil).leaf = true + + -- 微信插件检测升级 API + entry({"admin", "services", "openclaw", "wechat_check_upgrade"}, call("action_wechat_check_upgrade"), nil).leaf = true + + -- 微信插件升级 API + entry({"admin", "services", "openclaw", "wechat_upgrade_plugin"}, call("action_wechat_upgrade_plugin"), nil).leaf = true + + -- 微信退出/删除账号 API + entry({"admin", "services", "openclaw", "wechat_logout"}, call("action_wechat_logout"), nil).leaf = true +end-- ═══════════════════════════════════════════ +-- 获取实际安装路径 (辅助函数) +-- ═══════════════════════════════════════════ +local function get_actual_install_path() + local sys = require "luci.sys" + local uci = require "luci.model.uci".cursor() + + -- 从 UCI 读取用户配置的基础路径 + local install_path_uci = uci:get("openclaw", "main", "install_path") or "/opt" + local uci_install_path = install_path_uci .. "/openclaw" + + -- 方法1: 从运行中的 gateway 进程检测实际路径 + local actual_path = sys.exec("ps w 2>/dev/null | grep -E 'openclaw.*gateway|openclaw-gateway' | grep -v grep | head -1"):gsub("%s+", "") + if actual_path and actual_path ~= "" then + local matched_path = actual_path:match("(/[^ ]+)/data/") + if matched_path and nixio.fs.stat(matched_path .. "/data/.openclaw/openclaw.json", "type") then + return matched_path + end + end + + -- 方法2: 检查常见安装位置 (优先检查实际存在的配置) + local possible_paths = { + uci_install_path, -- UCI 配置的路径 + "/mnt/data/openclaw", -- 常见的外置存储路径 + "/opt/openclaw", -- 默认路径 + } + for _, p in ipairs(possible_paths) do + if nixio.fs.stat(p .. "/data/.openclaw/openclaw.json", "type") then + return p + end + end + + -- 方法3: 如果都没找到,返回 UCI 配置的路径 (即使不存在,安装时会创建) + return uci_install_path end -- ═══════════════════════════════════════════ @@ -57,9 +121,9 @@ function action_status() local port = uci:get("openclaw", "main", "port") or "18789" local pty_port = uci:get("openclaw", "main", "pty_port") or "18793" local enabled = uci:get("openclaw", "main", "enabled") or "0" - local install_path_uci = uci:get("openclaw", "main", "install_path") or "/opt" - -- 实际安装路径会在用户路径下创建 openclaw 子目录 - local install_path = install_path_uci .. "/openclaw" + + -- 使用 get_actual_install_path 获取实际安装路径 + local install_path = get_actual_install_path() -- 验证端口值为纯数字,防止命令注入 if not port:match("^%d+$") then port = "18789" end @@ -150,6 +214,9 @@ function action_status() -- 读取已配置的渠道列表 local channels = {} + if content:match('"openclaw%-weixin"%s*:%s*{') then + channels[#channels+1] = "微信" + end if content:match('"qqbot"%s*:%s*{') and content:match('"appId"%s*:%s*"[^"]+"') then channels[#channels+1] = "QQ" end @@ -952,3 +1019,513 @@ function action_check_system() http.prepare_content("application/json") http.write_json(result) end + +-- ═══════════════════════════════════════════ +-- 微信状态 API: 检测插件安装和登录状态 +-- ═══════════════════════════════════════════ +function action_wechat_status() + local http = require "luci.http" + local sys = require "luci.sys" + local uci = require "luci.model.uci".cursor() + + -- 获取安装路径 (优先从运行环境检测) + local install_path_uci = uci:get("openclaw", "main", "install_path") or "/opt" + local install_path = install_path_uci .. "/openclaw" + + -- 检测实际运行的安装路径 (通过查找 gateway 进程) + local actual_path = sys.exec("ps w 2>/dev/null | grep -E 'openclaw.*gateway|openclaw-gateway' | grep -v grep | head -1"):gsub("%s+", "") + if actual_path and actual_path ~= "" then + -- 从进程命令行提取路径 + local matched_path = actual_path:match("(/[^ ]+)/data/") + if matched_path and nixio.fs.stat(matched_path, "type") then + install_path = matched_path + end + end + + -- 也检查常见安装位置 + local possible_paths = { + install_path, + "/mnt/data/openclaw", + "/opt/openclaw", + } + for _, p in ipairs(possible_paths) do + if nixio.fs.stat(p .. "/data/.openclaw/openclaw.json", "type") then + install_path = p + break + end + end + + local result = { + plugin_installed = false, + logged_in = false, + accounts = {}, + install_path = install_path + } + + -- 检测微信插件是否已安装 + local wechat_ext_dir = install_path .. "/data/.openclaw/extensions/openclaw-weixin" + local wechat_plugin_json = wechat_ext_dir .. "/openclaw.plugin.json" + local wechat_package_json = wechat_ext_dir .. "/package.json" + + if nixio.fs.stat(wechat_plugin_json, "type") then + result.plugin_installed = true + -- 尝试读取版本号 + if nixio.fs.stat(wechat_package_json, "type") then + local pf = io.open(wechat_package_json, "r") + if pf then + local p_content = pf:read("*a") + pf:close() + local ver = p_content:match('"version"%s*:%s*"([^"]+)"') + if ver then + result.plugin_version = ver + end + end + end + end -- 从 openclaw.json 和 accounts.json 读取微信账号配置 + local config_file = install_path .. "/data/.openclaw/openclaw.json" + local accounts_file = install_path .. "/data/.openclaw/openclaw-weixin/accounts.json" + + -- 新版 OpenClaw WeChat 插件状态读取 + if nixio.fs.stat(accounts_file, "type") then + local af = io.open(accounts_file, "r") + if af then + local content = af:read("*a") + af:close() + local count = 0 + for acc in content:gmatch('"([^"]+)"') do + count = count + 1 + table.insert(result.accounts, {name = acc}) + end + if count > 0 then + result.logged_in = true + end + end + end + + local cf = io.open(config_file, "r") + if cf then + local content = cf:read("*a") + cf:close() + + if content:match('"openclaw%-weixin"%s*:%s*{') then + -- 兼容旧版 + local accounts_str = content:match('"accounts"%s*:%s*%[([^%]]*)%]') + if accounts_str and accounts_str ~= "" then + local count = 0 + for _ in accounts_str:gmatch('"wxid') do count = count + 1 end + for _ in accounts_str:gmatch('"nickName"') do count = count + 1 end + if count > 0 and not result.logged_in then + result.logged_in = true + result.accounts = {{name = "微信账号"}} + end + end + + if content:match('"credential"%s*:%s*{') and not result.logged_in then + result.logged_in = true + result.accounts = {{name = "微信账号"}} + end + end + end http.prepare_content("application/json") + http.write_json(result) +end + +-- ═══════════════════════════════════════════ +-- 微信插件安装 API (后台安装) +-- ═══════════════════════════════════════════ +function action_wechat_install() + local http = require "luci.http" + local sys = require "luci.sys" + + local install_path = get_actual_install_path() + local node_bin = install_path .. "/node/bin/node" + local npx_bin = install_path .. "/node/bin/npx" + local oc_data = install_path .. "/data" + + -- 清理旧日志和状态 + sys.exec("rm -f /tmp/openclaw-wechat-install.log /tmp/openclaw-wechat-install.pid /tmp/openclaw-wechat-install.exit") + + -- 后台执行安装 + local install_cmd = string.format( + "( " .. + "echo '开始安装微信插件...' > /tmp/openclaw-wechat-install.log; " .. + "echo '安装路径: %s' >> /tmp/openclaw-wechat-install.log; " .. + "cd %s && " .. + "su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " .. + "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; " .. + "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, oc_data, oc_data, oc_data, oc_data, install_path, install_path, npx_bin + ) sys.exec(install_cmd) + + http.prepare_content("application/json") + http.write_json({ status = "ok", message = "微信插件安装已在后台启动..." }) +end + +-- ═══════════════════════════════════════════ +-- 微信安装日志轮询 API +-- ═══════════════════════════════════════════ +function action_wechat_install_log() + local http = require "luci.http" + local sys = require "luci.sys" + + local log = "" + local f = io.open("/tmp/openclaw-wechat-install.log", "r") + if f then + log = f:read("*a") or "" + f:close() + end + + local running = false + local pid_file = io.open("/tmp/openclaw-wechat-install.pid", "r") + if pid_file then + local pid = pid_file:read("*a"):gsub("%s+", "") + pid_file:close() + if pid ~= "" then + local check = sys.exec("kill -0 " .. pid .. " 2>/dev/null && echo yes || echo no"):gsub("%s+", "") + running = (check == "yes") + end + end + + local exit_code = -1 + if not running then + local exit_file = io.open("/tmp/openclaw-wechat-install.exit", "r") + if exit_file then + local code = exit_file:read("*a"):gsub("%s+", "") + exit_file:close() + exit_code = tonumber(code) or -1 + end + end + + local state = "idle" + if running then + state = "running" + elseif exit_code == 0 then + state = "success" + elseif exit_code > 0 then + state = "failed" + end + + http.prepare_content("application/json") + http.write_json({ + status = "ok", + log = log, + state = state, + running = running, + exit_code = exit_code + }) +end + +-- ═══════════════════════════════════════════ +-- 微信登录 API (启动登录流程并获取二维码) +-- ═══════════════════════════════════════════ +function action_wechat_login() + local http = require "luci.http" + local sys = require "luci.sys" + + local install_path = get_actual_install_path() + local node_bin = install_path .. "/node/bin/node" + local oc_data = install_path .. "/data" + local oc_entry = "" + + -- 查找 openclaw 入口 + local search_dirs = { + install_path .. "/global/lib/node_modules/openclaw", + install_path .. "/global/node_modules/openclaw", + install_path .. "/node/lib/node_modules/openclaw", + } + for _, d in ipairs(search_dirs) do + if nixio.fs.stat(d .. "/openclaw.mjs", "type") then + oc_entry = d .. "/openclaw.mjs" + break + elseif nixio.fs.stat(d .. "/dist/cli.js", "type") then + oc_entry = d .. "/dist/cli.js" + break + end + end + + if oc_entry == "" then + http.prepare_content("application/json") + http.write_json({ status = "error", message = "OpenClaw 未安装" }) + return + end + + -- 清理旧状态和可能的残留进程 + sys.exec("kill -9 $(cat /tmp/openclaw-wechat-login.pid 2>/dev/null) 2>/dev/null") + sys.exec("pkill -f 'channels login --channel openclaw-weixin' 2>/dev/null") + sys.exec("rm -f /tmp/openclaw-wechat-qrcode.txt /tmp/openclaw-wechat-login.pid /tmp/openclaw-wechat-login.exit /tmp/openclaw-wechat-restarted") -- 后台启动登录流程,将二维码输出到文件 + local login_cmd = string.format( + "( " .. + "echo '正在启动微信登录...' > /tmp/openclaw-wechat-qrcode.txt; " .. + "echo '安装路径: %s' >> /tmp/openclaw-wechat-qrcode.txt; " .. + "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 " .. + "%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 + ) sys.exec(login_cmd) + + http.prepare_content("application/json") + http.write_json({ status = "ok", message = "微信登录流程已启动" }) +end + +-- ═══════════════════════════════════════════ +-- 微信登录状态/二维码 API +-- ═══════════════════════════════════════════ +function action_wechat_login_status() + local http = require "luci.http" + local sys = require "luci.sys" + + -- 读取二维码输出 + local qrcode = "" + local f = io.open("/tmp/openclaw-wechat-qrcode.txt", "r") + if f then + qrcode = f:read("*a") or "" + f:close() + end + + -- 检查进程状态 + local running = false + local pid_file = io.open("/tmp/openclaw-wechat-login.pid", "r") + if pid_file then + local pid = pid_file:read("*a"):gsub("%s+", "") + pid_file:close() + if pid ~= "" then + local check = sys.exec("kill -0 " .. pid .. " 2>/dev/null && echo yes || echo no"):gsub("%s+", "") + running = (check == "yes") + end + end + + local exit_code = -1 + if not running then + local exit_file = io.open("/tmp/openclaw-wechat-login.exit", "r") + if exit_file then + local code = exit_file:read("*a"):gsub("%s+", "") + exit_file:close() + exit_code = tonumber(code) or -1 + end + end + + -- 提取最后生成的二维码 URL + local qrcode_url = "" + for url in qrcode:gmatch("https?://[^\n\r]+") do + qrcode_url = url + end -- 检查是否登录成功 + local logged_in = qrcode:find("登录成功") ~= nil or qrcode:find("成功登录") ~= nil or qrcode:find("Login success") ~= nil or qrcode:find("Logged in") ~= nil + + local state = "idle" + if logged_in then + state = "success" + elseif running and qrcode_url ~= "" then + state = "qrcode" + elseif running then + state = "starting" + elseif exit_code == 0 then + state = "success" + elseif exit_code > 0 then + state = "failed" + end + + -- 如果刚登录成功,触发一次重启,确保主进程加载微信 + if state == "success" and not nixio.fs.stat("/tmp/openclaw-wechat-restarted", "type") then + sys.exec("touch /tmp/openclaw-wechat-restarted") + sys.exec("/etc/init.d/openclaw restart &") + end http.prepare_content("application/json") + http.write_json({ + status = "ok", + state = state, + qrcode = qrcode, + qrcode_url = qrcode_url, + running = running, + exit_code = exit_code, + logged_in = logged_in + }) +end + +-- ═══════════════════════════════════════════ +-- 微信插件卸载 API +-- ═══════════════════════════════════════════ +function action_wechat_uninstall() + local http = require "luci.http" + local sys = require "luci.sys" + + local install_path = get_actual_install_path() + + -- 删除微信插件目录 + local wechat_ext_dir = install_path .. "/data/.openclaw/extensions/openclaw-weixin" + if nixio.fs.stat(wechat_ext_dir, "type") then + sys.exec("rm -rf " .. wechat_ext_dir) + end + + -- 从配置中删除微信相关配置 + local config_file = install_path .. "/data/.openclaw/openclaw.json" + if nixio.fs.stat(config_file, "type") then + -- 读取配置 + local cf = io.open(config_file, "r") + local content = "" + if cf then + content = cf:read("*a") or "" + cf:close() + end + + -- 删除微信配置块 (简单字符串替换) + content = content:gsub(',?%s*"openclaw%-weixin"%s*:%s*%b{}', "") + content = content:gsub('"openclaw%-weixin"%s*:%s*%b{}%s*,?', "") + + -- 写回配置 + local wf = io.open(config_file, "w") + if wf then + wf:write(content) + wf:close() + end + end + + -- 清理临时文件 + sys.exec("rm -f /tmp/openclaw-wechat-*.log /tmp/openclaw-wechat-*.pid /tmp/openclaw-wechat-*.exit /tmp/openclaw-wechat-qrcode.txt") + + http.prepare_content("application/json") + http.write_json({ status = "ok", message = "微信插件已卸载" }) +end + +-- ═══════════════════════════════════════════ +-- 微信插件检测升级 API +-- ═══════════════════════════════════════════ +function action_wechat_check_upgrade() + local http = require "luci.http" + local sys = require "luci.sys" + + local install_path = get_actual_install_path() + local npx_bin = install_path .. "/node/bin/npx" + local oc_data = install_path .. "/data" + + -- 获取当前已安装版本 + local current_version = "" + local wechat_ext_dir = install_path .. "/data/.openclaw/extensions/openclaw-weixin" + local plugin_json = wechat_ext_dir .. "/openclaw.plugin.json" + local pf = io.open(plugin_json, "r") + if pf then + local content = pf:read("*a") or "" + pf:close() + current_version = content:match('"version"%s*:%s*"([^"]+)"') or "" + end + + -- 检测最新版本 (通过 npm view) + local latest_version = "" + local env_prefix = string.format( + "HOME=%s PATH=%s/node/bin:%s/global/bin:$PATH", + oc_data, install_path, install_path + ) + local check_cmd = string.format( + "%s %s view @tencent-weixin/openclaw-weixin version 2>/dev/null", + env_prefix, npx_bin + ) + latest_version = sys.exec(check_cmd):gsub("%s+", "") + + local has_upgrade = false + if current_version ~= "" and latest_version ~= "" and current_version ~= latest_version then + has_upgrade = true + end + + http.prepare_content("application/json") + http.write_json({ + status = "ok", + current_version = current_version, + latest_version = latest_version, + has_upgrade = has_upgrade + }) +end + +-- ═══════════════════════════════════════════ +-- 微信插件升级 API (后台执行安装命令) +-- ═══════════════════════════════════════════ +function action_wechat_upgrade_plugin() + local http = require "luci.http" + local sys = require "luci.sys" + + local install_path = get_actual_install_path() + local npx_bin = install_path .. "/node/bin/npx" + local oc_data = install_path .. "/data" + + -- 清理旧日志和状态 + sys.exec("rm -f /tmp/openclaw-wechat-install.log /tmp/openclaw-wechat-install.pid /tmp/openclaw-wechat-install.exit") + + -- 后台执行升级 (其实就是重新安装最新版) + local upgrade_cmd = string.format( + "( " .. + "echo '正在升级微信插件...' > /tmp/openclaw-wechat-install.log; " .. + "echo '安装路径: %s' >> /tmp/openclaw-wechat-install.log; " .. + "cd %s && " .. + "su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " .. + "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; " .. + "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, oc_data, oc_data, oc_data, oc_data, install_path, install_path, npx_bin + ) sys.exec(upgrade_cmd) + + http.prepare_content("application/json") + http.write_json({ status = "ok", message = "微信插件升级已在后台启动..." }) +end + +-- ═══════════════════════════════════════════ +-- 微信退出/删除账号 API +-- ═══════════════════════════════════════════ +function action_wechat_logout() + local http = require "luci.http" + 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 + + local install_path = get_actual_install_path() + local node_bin = install_path .. "/node/bin/node" + local oc_data = install_path .. "/data" + local oc_entry = "" + + local search_dirs = { + install_path .. "/global/lib/node_modules/openclaw", + install_path .. "/global/node_modules/openclaw", + install_path .. "/node/lib/node_modules/openclaw", + } + for _, d in ipairs(search_dirs) do + if nixio.fs.stat(d .. "/openclaw.mjs", "type") then + oc_entry = d .. "/openclaw.mjs" + break + elseif nixio.fs.stat(d .. "/dist/cli.js", "type") then + oc_entry = d .. "/dist/cli.js" + break + end + end + + if oc_entry == "" then + http.prepare_content("application/json") + http.write_json({ status = "error", message = "OpenClaw 未安装" }) + return + end + + -- 在后台执行 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 " .. + "%s %s channels logout --channel openclaw-weixin --account \"%s\"'", + oc_data, oc_data, oc_data, oc_data, oc_data, install_path, install_path, node_bin, oc_entry, account_id + ) + + sys.exec(logout_cmd .. " >/dev/null 2>&1") + + -- 重启服务 + sys.exec("/etc/init.d/openclaw restart &") + + http.prepare_content("application/json") + http.write_json({ status = "ok", message = "已下线账号: " .. account_id }) +end diff --git a/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua b/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua index 240d1ed6..4b903bbb 100644 --- a/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua +++ b/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua @@ -295,6 +295,37 @@ act.cfgvalue = function(self, section) html[#html+1] = '}catch(e){el.innerHTML="❌ 错误";}' html[#html+1] = '});}' + -- 简单的 Markdown 转 HTML 函数 (用于渲染 GitHub Release Notes) + html[#html+1] = 'function ocMarkdownToHtml(md){' + html[#html+1] = 'if(!md)return "";' + -- 转义 HTML 特殊字符 + html[#html+1] = 'var html=md.replace(/&/g,"&").replace(//g,">");' + -- 代码块 (```code```) + html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"
"+code.trim()+"
";});' + -- 行内代码 (`code`) + html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"$1");' + -- 标题 (### ## #) + html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"

$1

");' + html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"

$1

");' + html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"

$1

");' + -- 粗体和斜体 + html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"$1");' + html[#html+1] = 'html=html.replace(/\\*([^*]+)\\*/g,"$1");' + -- 链接 [text](url) + html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"$1");' + -- 无序列表 (- 或 *) + html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"
  • $1
  • ");' + -- 有序列表 (1. 2. 等) + html[#html+1] = 'html=html.replace(/^\\d+\\. (.+)$/gm,"
  • $1
  • ");' + -- 水平线 (--- 或 ***) + html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"
    ");' + -- 段落 (连续的非空行合并) + html[#html+1] = 'html=html.replace(/\\n\\n/g,"

    ");' + -- 换行 + html[#html+1] = 'html=html.replace(/\\n/g,"
    ");' + html[#html+1] = 'return"

    "+html+"
    ";' + html[#html+1] = '}' + -- 检测升级 (只检查插件版本,有新版本时显示更新内容) html[#html+1] = 'function ocCheckUpdate(){' html[#html+1] = 'var btn=document.getElementById("btn-check-update");' @@ -320,10 +351,10 @@ act.cfgvalue = function(self, section) html[#html+1] = 'window._pluginLatestVer=r.plugin_latest;' html[#html+1] = 'var notesHtml="";' html[#html+1] = 'if(r.release_notes){' - html[#html+1] = 'var escaped=r.release_notes.replace(/&/g,"&").replace(//g,">");' - html[#html+1] = 'notesHtml=\'
    \'' - html[#html+1] = '+\'
    📋 v\'+r.plugin_latest+\' 更新内容
    \'' - html[#html+1] = '+\'
    \'+escaped+\'
    \'' + html[#html+1] = 'var rendered=ocMarkdownToHtml(r.release_notes);' + html[#html+1] = 'notesHtml=\'
    \'' + html[#html+1] = '+\'
    📋 v\'+r.plugin_latest+\' 更新内容
    \'' + html[#html+1] = '+rendered' html[#html+1] = '+\'
    \';' html[#html+1] = '}' html[#html+1] = 'act.innerHTML=notesHtml' diff --git a/luci-app-openclaw/luasrc/view/openclaw/wechat.htm b/luci-app-openclaw/luasrc/view/openclaw/wechat.htm new file mode 100644 index 00000000..8f455419 --- /dev/null +++ b/luci-app-openclaw/luasrc/view/openclaw/wechat.htm @@ -0,0 +1,931 @@ +<%# + luci-app-openclaw — 微信渠道配置页面 +-%> +<%+header%> + +<% +local uci = require "luci.model.uci".cursor() +local pty_port = uci:get("openclaw", "main", "pty_port") or "18793" +%> + + + +
    +

    💬 微信渠道配置

    +

    通过微信 ClawBot 插件连接微信,实现与 AI 助手的私聊对话。支持多账号同时在线。

    +
    + +
    +

    📋 前置条件

    +
      +
    1. 微信客户端版本 >= 8.0.70 — 旧版本可能不支持插件功能
    2. +
    3. 启用 ClawBot 插件 — 安装插件后→ 打开微信客户端「扫一扫」扫描屏幕中的二维码
    4. +
    +
    + ⚠️ 注意:微信登录有可能会验证账号安全环境,为了防止微信风控封号,尽量使用常用账号且确保账号状态正常。通过上方扫码即可直接连接,无需在手机端手动启用额外功能。 +
    +
    + +
    +

    📊 插件状态

    +
    + 微信插件 + 检测中... +
    +
    + 插件版本 + +
    +
    +
    + 登录状态 + 检测中... +
    + +
    +
    + OpenClaw 版本 + 检测中... +
    +
    + +
    + + + + +
    + + +
    +
    +
    +

    📦 安装微信插件

    + +
    +
    +
    +
    + + 正在安装,请稍候... +
    +
    
    +				
    +			
    + +
    +
    +
    + + +
    +
    +
    +

    📱 微信扫码登录

    + +
    +
    +
    +
    + + 正在获取登录二维码... +
    +
    + + +
    +
    +
    + + +
    +
    +
    +

    🔍 检测插件升级

    + +
    +
    +
    +
    + + 正在检测... +
    +
    + +
    +
    +
    + + + +<%+footer%> diff --git a/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh b/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh index dd805fe1..edbcf925 100755 --- a/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh +++ b/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh @@ -578,6 +578,14 @@ show_current_config() { echo -e "${GREEN}├──────────────────────────────────────────────────────────┤${NC}" echo -e "${GREEN}│${NC} ${BOLD}渠道配置状态${NC}" + # 检测微信渠道 + local wechat_ext_dir="${OC_STATE_DIR}/extensions/openclaw-weixin" + if [ -d "$wechat_ext_dir" ] && [ -f "${wechat_ext_dir}/openclaw.plugin.json" ]; then + echo -e "${GREEN}│${NC} 微信 .............. ${GREEN}✅ 已配置${NC}" + else + echo -e "${GREEN}│${NC} 微信 .............. ${YELLOW}❌ 未配置${NC}" + fi + local tg_token=$(json_get channels.telegram.botToken) local dc_token=$(json_get channels.discord.botToken) local fs_appid=$(json_get channels.feishu.appId) @@ -1084,18 +1092,36 @@ configure_model() { prompt_with_default "请输入 SiliconFlow API Key" "" api_key if [ -n "$api_key" ]; then echo "" - echo -e " ${CYAN}可用模型:${NC}" - echo -e " ${CYAN}a)${NC} deepseek-ai/DeepSeek-V3 — DeepSeek V3 (推荐)" - echo -e " ${CYAN}b)${NC} deepseek-ai/DeepSeek-R1 — DeepSeek R1" - echo -e " ${CYAN}c)${NC} Qwen/Qwen3-235B-A22B — 通义千问 Qwen3 235B" - echo -e " ${CYAN}d)${NC} 手动输入模型名" + echo -e " ${CYAN}可用模型分类说明:${NC}" + echo -e " ${YELLOW}* Pro模型 (带 Pro/ 前缀): 仅支持充值余额支付,并发与速率(Rate Limits)可变。${NC}" + echo -e " ${YELLOW}* 非Pro模型 (无 Pro/ 前缀): 支持赠费余额(代金券)和充值余额支付,Rate Limits 固定。${NC}" + echo -e " ${GREEN}【建议】如果您是代金券/赠送余额用户,请务必选择【非Pro模型】。${NC}" echo "" - prompt_with_default "请选择模型" "a" model_choice + echo -e " ${CYAN}── 非Pro模型 (支持代金券/免费额度) ──${NC}" + echo -e " ${CYAN}1)${NC} deepseek-ai/DeepSeek-V3 — DeepSeek-V3 (推荐)" + echo -e " ${CYAN}2)${NC} deepseek-ai/DeepSeek-R1 — DeepSeek-R1 (推理模型)" + echo -e " ${CYAN}3)${NC} Qwen/Qwen2.5-72B-Instruct — 通义千问 2.5 72B" + echo -e " ${CYAN}4)${NC} Qwen/Qwen2.5-7B-Instruct — 通义千问 2.5 7B" + echo -e " ${CYAN}5)${NC} THUDM/glm-4-9b-chat — 智谱 GLM-4 9B" + echo -e " ${CYAN}6)${NC} 01-ai/Yi-1.5-34B-Chat-16K — 零一万物 Yi-1.5 34B" + echo "" + echo -e " ${CYAN}── Pro模型 (仅支持充值余额) ──${NC}" + echo -e " ${CYAN}7)${NC} Pro/deepseek-ai/DeepSeek-V3 — DeepSeek-V3 (Pro增强侧)" + echo -e " ${CYAN}8)${NC} Pro/zai-org/GLM-5 — 智谱 GLM-5" + echo "" + echo -e " ${CYAN}0)${NC} 手动输入其他任意模型名称" + echo "" + prompt_with_default "请选择模型 [0-8]" "1" model_choice case "$model_choice" in - a) model_name="deepseek-ai/DeepSeek-V3" ;; - b) model_name="deepseek-ai/DeepSeek-R1" ;; - c) model_name="Qwen/Qwen3-235B-A22B" ;; - d) prompt_with_default "请输入模型名称" "deepseek-ai/DeepSeek-V3" model_name ;; + 1) model_name="deepseek-ai/DeepSeek-V3" ;; + 2) model_name="deepseek-ai/DeepSeek-R1" ;; + 3) model_name="Qwen/Qwen2.5-72B-Instruct" ;; + 4) model_name="Qwen/Qwen2.5-7B-Instruct" ;; + 5) model_name="THUDM/glm-4-9b-chat" ;; + 6) model_name="01-ai/Yi-1.5-34B-Chat-16K" ;; + 7) model_name="Pro/deepseek-ai/DeepSeek-V3" ;; + 8) model_name="Pro/zai-org/GLM-5" ;; + 0) prompt_with_default "请输入模型详细名称" "deepseek-ai/DeepSeek-V3" model_name ;; *) model_name="deepseek-ai/DeepSeek-V3" ;; esac auth_set_apikey siliconflow "$api_key" @@ -1910,8 +1936,10 @@ configure_channels() { echo "" echo -e " ${BOLD}📡 配置消息渠道${NC}" echo "" - echo -e " ${CYAN}1)${NC} QQ 机器人 ${GREEN}(腾讯QQ,推荐国内用户)${NC}" - echo -e " ${CYAN}2)${NC} Telegram ${GREEN}(最常用,推荐)${NC}" + echo -e " ${CYAN}提示: 微信配置请使用 LuCI 界面「微信配置」菜单${NC}" + echo "" + echo -e " ${CYAN}1)${NC} QQ 机器人 ${GREEN}(腾讯QQ)${NC}" + echo -e " ${CYAN}2)${NC} Telegram ${GREEN}(最常用)${NC}" echo -e " ${CYAN}3)${NC} Discord" echo -e " ${CYAN}4)${NC} 飞书 (Feishu)" echo -e " ${CYAN}5)${NC} Slack" diff --git a/luci-app-openclaw/root/usr/share/openclaw/web-pty.js b/luci-app-openclaw/root/usr/share/openclaw/web-pty.js index b1525fe4..ff360cfe 100644 --- a/luci-app-openclaw/root/usr/share/openclaw/web-pty.js +++ b/luci-app-openclaw/root/usr/share/openclaw/web-pty.js @@ -104,7 +104,7 @@ function encodeWSFrame(data, opcode = 0x01) { // ── PTY 进程管理 ── class PtySession { - constructor(socket) { + constructor(socket, initCmd = '') { this.socket = socket; this.proc = null; this.cols = 80; @@ -115,6 +115,7 @@ class PtySession { this._MAX_SPAWN_RETRIES = 5; this._pingTimer = null; this._pongReceived = true; + this.initCmd = initCmd; // 初始命令 (如 'wechat') activeSessions++; console.log(`[oc-config] Session created (active: ${activeSessions}/${MAX_SESSIONS})`); this._setupWSReader(); @@ -193,12 +194,14 @@ class PtySession { return true; } catch { return false; } })(); + // 构建脚本参数 + const scriptArgs = this.initCmd ? [SCRIPT_PATH, this.initCmd] : [SCRIPT_PATH]; if (hasScript) { - this.proc = spawn('script', ['-qc', `stty rows ${this.rows} cols ${this.cols} 2>/dev/null; printf '\\e[?2004l'; sh "${SCRIPT_PATH}"`, '/dev/null'], + this.proc = spawn('script', ['-qc', `stty rows ${this.rows} cols ${this.cols} 2>/dev/null; printf '\\e[?2004l'; sh "${scriptArgs.join('" "')}"`, '/dev/null'], { stdio: ['pipe', 'pipe', 'pipe'], env, detached: true }); } else { console.log('[oc-config] "script" command not found, falling back to sh (install util-linux-script for full PTY support)'); - this.proc = spawn('sh', [SCRIPT_PATH], + this.proc = spawn('sh', scriptArgs, { stdio: ['pipe', 'pipe', 'pipe'], env, detached: true }); } @@ -284,9 +287,9 @@ function handleUpgrade(req, socket, head) { // 认证: 验证查询参数中的 token // 每次连接时实时读取 UCI token (安装/升级可能重新生成 token) const currentToken = loadAuthToken() || AUTH_TOKEN; + const urlObj = new URL(req.url, `http://${req.headers.host || 'localhost'}`); if (currentToken) { - const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); - const clientToken = url.searchParams.get('token') || ''; + const clientToken = urlObj.searchParams.get('token') || ''; if (clientToken !== currentToken) { console.log(`[oc-config] WS auth failed from ${socket.remoteAddress}`); socket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); @@ -295,6 +298,9 @@ function handleUpgrade(req, socket, head) { } } + // 读取初始化命令参数 (如 cmd=wechat) + const initCmd = urlObj.searchParams.get('cmd') || ''; + // 并发会话限制 if (activeSessions >= MAX_SESSIONS) { console.log(`[oc-config] Max sessions reached (${activeSessions}/${MAX_SESSIONS}), rejecting`); @@ -315,8 +321,8 @@ function handleUpgrade(req, socket, head) { socket.write(handshake, () => { if (head && head.length > 0) socket.unshift(head); - new PtySession(socket); - console.log('[oc-config] PTY session started'); + new PtySession(socket, initCmd); + console.log(`[oc-config] PTY session started (cmd=${initCmd || 'menu'})`); }); } diff --git a/luci-app-openclawmgr/Makefile b/luci-app-openclawmgr/Makefile index 1274eee0..e5ce4791 100644 --- a/luci-app-openclawmgr/Makefile +++ b/luci-app-openclawmgr/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=0.1.8 -PKG_RELEASE:=10 +PKG_VERSION:=0.1.9 +PKG_RELEASE:=11 LUCI_TITLE:=LuCI support for OpenClaw Launcher LUCI_PKGARCH:=all diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css index da0ffdc5..4b3a9c57 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css @@ -301,6 +301,32 @@ border-color: transparent; } +.oclm-button-tag { + min-height: 30px; + height: 30px; + padding: 0 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: rgba(47, 103, 246, 0.08); + color: var(--oclm-primary); + border: 1px solid rgba(47, 103, 246, 0.18); + box-shadow: none; +} + +.oclm-button-tag:hover { + color: var(--oclm-primary); + border-color: rgba(47, 103, 246, 0.3); + background: rgba(47, 103, 246, 0.12); + filter: none; +} + +.oclm-app[data-darkmode="true"] .oclm-button-tag { + background: rgba(91, 134, 255, 0.12); + border-color: rgba(91, 134, 255, 0.24); + color: var(--oclm-primary); +} + .oclm-form-grid { display: grid; gap: 14px; @@ -391,6 +417,16 @@ line-height: 1.45; } +.oclm-inline-link { + color: var(--oclm-primary); + text-decoration: none; + font-weight: 700; +} + +.oclm-inline-link:hover { + text-decoration: underline; +} + .oclm-toggle { position: relative; width: 48px; @@ -519,6 +555,10 @@ flex-wrap: wrap; } +.oclm-section-submit-right { + justify-content: flex-end; +} + .oclm-applied-hint { color: var(--oclm-subtle); font-size: 12px; diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js index 55801e4b..543f3776 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js @@ -211,25 +211,24 @@ function updateActionLabel(status) { var updateCheck = getUpdateCheck(); - if (updateCheck.upgrading || (status && status.installing && status.task_op === "upgrade")) return "更新中"; if (updateCheck.checking) return "检测中…"; - if (updateCheck.hasUpdate) return "更新 OpenClaw"; return "检测更新"; } function statusNoteText(status) { - var updateCheck = getUpdateCheck(); if (status && status.installing) { return status.task_op === "upgrade" ? "更新任务正在后台运行,可打开任务日志查看进度。" : "安装任务正在后台运行,可打开任务日志查看进度。"; } + return ""; + } + + function versionNoteText() { + var updateCheck = getUpdateCheck(); if (updateCheck.checking) { return "正在检测远程版本,请稍候…"; } - if (updateCheck.upgrading) { - return "更新任务已提交,请稍候查看版本变化。"; - } if (updateCheck.error) { return updateCheck.error; } @@ -464,6 +463,7 @@ var updateCheck = getUpdateCheck(); var installLabel = status.installing ? (status.task_op === "upgrade" ? "更新中" : "安装中") : "立即安装"; var installAcceleratedChecked = form.install_accelerated == null ? true : form.install_accelerated === true; + var installChannel = form.install_channel === "latest" ? "latest" : "stable"; var showInstallAction = !status.installed; var showUpdateAction = status.installed && !status.installing; var showServiceActions = status.installed && !status.installing; @@ -504,7 +504,6 @@ '' + '
    ' + '' + - '' + '' + communityIcon("oclm-button-icon") + '玩家交流' + '' + '' + @@ -516,6 +515,7 @@ '' + '' + '' + + '' + '' + '' + '
    ' + @@ -569,6 +569,22 @@ '
    ' + (state.lastAppliedAt ? '已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置' : '') + '
    ' + '
    ' + + '
    ' + + '

    版本管理

    openclaw 官方正在快速迭代,最新版本可能不兼容旧版本配置导致无法启动。请加入交流群了解最新稳定版本号
    ' + + '
    ' + + fieldInput("当前版本", '
    OpenClaw ' + escapeHtml(status.openclaw_version || "-") + '' + + (showUpdateAction ? '' : '') + + '
    ' + + '
    ' + escapeHtml(versionNoteText()) + '
    ') + + fieldInput("安装版本", selectHtml("oclm-install-channel", installChannel, [ + ["stable", "openclaw@2026.3.28(稳定版)"], + ["latest", "openclaw@latest(最新版)"] + ])) + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + '

    卸载清理

    ' + '
    以下操作会影响当前安装环境,请在确认后执行。
    ' + @@ -667,7 +683,7 @@ } if (updateBtn) { updateBtn.textContent = updateActionLabel(status); - updateBtn.disabled = !!(updateCheck.checking || updateCheck.upgrading); + updateBtn.disabled = !!updateCheck.checking; if (showUpdateAction) updateBtn.classList.remove("oclm-hidden"); else updateBtn.classList.add("oclm-hidden"); } @@ -779,6 +795,9 @@ var installAccelEl = getEl("oclm-install-accelerated"); if (installAccelEl) state.form.install_accelerated = !!installAccelEl.checked; + var installChannelEl = getEl("oclm-install-channel"); + if (installChannelEl) state.form.install_channel = installChannelEl.value === "latest" ? "latest" : "stable"; + var agentEl = getEl("oclm-agent"); if (agentEl) state.form.default_agent = agentEl.value; @@ -854,6 +873,7 @@ Array.prototype.forEach.call(root.querySelectorAll("[data-install-action]"), function(el) { el.onclick = function() { var accelerated = !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked); + var installChannel = (root.getElementById("oclm-install-channel") && root.getElementById("oclm-install-channel").value === "latest") ? "latest" : ((state.form && state.form.install_channel === "latest") ? "latest" : "stable"); var baseDirEl = root.getElementById("oclm-base-dir"); var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : ""; if (!baseDir) { @@ -876,8 +896,9 @@ return; } state.form.install_accelerated = accelerated; + state.form.install_channel = installChannel; state.form.base_dir = baseDir; - return postForm(config.opUrl, { op: "install" }); + return postForm(config.opUrl, { op: "install", install_channel: installChannel }); }).then(function(rv) { if (!rv) return; if (!rv || !rv.ok) { @@ -901,45 +922,69 @@ }; }); - Array.prototype.forEach.call(root.querySelectorAll("[data-update-action]"), function(el) { + Array.prototype.forEach.call(root.querySelectorAll("[data-version-install-action]"), function(el) { el.onclick = function() { - var updateCheck = getUpdateCheck(); - if (!state.status || !state.status.installed || state.status.installing) { + var accelerated = !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked); + var installChannel = (root.getElementById("oclm-install-channel") && root.getElementById("oclm-install-channel").value === "latest") ? "latest" : "stable"; + var baseDirEl = root.getElementById("oclm-base-dir"); + var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : ""; + var op = (state.status && state.status.installed) ? "upgrade" : "install"; + if (!baseDir) { + window.alert("请先选择数据目录并保存应用,再执行安装。"); + if (baseDirEl) baseDirEl.focus(); return; } - - if (updateCheck.hasUpdate) { - updateCheck.upgrading = true; - updateCheck.error = ""; - state.status.installing = true; - state.status.task_running = true; - state.status.task_op = "upgrade"; - render(); - postForm(config.opUrl, { op: "upgrade" }).then(function(rv) { - if (!rv || !rv.ok) { - updateCheck.upgrading = false; - state.status.installing = false; - state.status.task_running = false; - state.status.task_op = ""; - render(); - if (rv && rv.busy && rv.running_task_id) { - showTaskLog(rv.running_task_id); - return; - } - window.alert((rv && rv.error) || "启动更新失败"); - return; - } - state.lastTaskRunning = true; - showTaskLog((rv && (rv.running_task_id || rv.task_id)) || "openclawmgr"); - scheduleStatusRefresh(10, 1000); - }).catch(function() { - updateCheck.upgrading = false; + if (!state.status || state.status.installing) { + return; + } + state.status.installing = true; + state.status.task_running = true; + state.status.task_op = op; + render(); + postJson(config.configUrl, { base_dir: baseDir, install_accelerated: accelerated }).then(function(cfgRv) { + if (!cfgRv || !cfgRv.ok) { state.status.installing = false; state.status.task_running = false; state.status.task_op = ""; render(); - window.alert("启动更新失败"); - }); + window.alert((cfgRv && cfgRv.error) || "保存安装选项失败"); + return; + } + state.form.install_accelerated = accelerated; + state.form.install_channel = installChannel; + state.form.base_dir = baseDir; + return postForm(config.opUrl, { op: op, install_channel: installChannel }); + }).then(function(rv) { + if (!rv) return; + if (!rv.ok) { + if (rv.busy && rv.running_task_id) { + showTaskLog(rv.running_task_id); + return; + } + state.status.installing = false; + state.status.task_running = false; + state.status.task_op = ""; + render(); + window.alert((rv && rv.error) || "启动安装失败"); + return; + } + state.lastTaskRunning = true; + showTaskLog((rv && (rv.running_task_id || rv.task_id)) || "openclawmgr"); + scheduleStatusRefresh(10, 1000); + }).catch(function() { + state.status.installing = false; + state.status.task_running = false; + state.status.task_op = ""; + render(); + window.alert("启动安装失败"); + }); + }; + }); + + Array.prototype.forEach.call(root.querySelectorAll("[data-update-action]"), function(el) { + el.onclick = function() { + var updateCheck = getUpdateCheck(); + if (!state.status || !state.status.installed || state.status.installing) { return; } @@ -960,7 +1005,6 @@ } updateCheck.checked = true; updateCheck.hasUpdate = !!rv.has_update; - updateCheck.upgrading = false; updateCheck.error = ""; updateCheck.localVersion = String(rv.local_version || (state.status && state.status.openclaw_version) || ""); updateCheck.remoteVersion = String(rv.remote_version || ""); @@ -994,6 +1038,13 @@ }; } + var installChannel = root.getElementById("oclm-install-channel"); + if (installChannel) { + installChannel.onchange = function() { + state.form.install_channel = installChannel.value === "latest" ? "latest" : "stable"; + }; + } + Array.prototype.forEach.call(root.querySelectorAll("[data-copy-token-url]"), function(el) { el.onclick = function() { var value = (state.status && (state.status.token_url || state.status.base_url)) || ""; @@ -1178,13 +1229,9 @@ updateCheck.localVersion = String(state.status.openclaw_version || ""); } if (!state.status || !state.status.installed) { - updateCheck.upgrading = false; updateCheck.hasUpdate = false; updateCheck.checked = false; - } else if (state.status.installing && state.status.task_op === "upgrade") { - updateCheck.upgrading = true; } else if (!state.status.installing) { - updateCheck.upgrading = false; if (updateCheck.remoteVersion && updateCheck.localVersion && updateCheck.remoteVersion === updateCheck.localVersion) { updateCheck.hasUpdate = false; updateCheck.checked = true; @@ -1223,7 +1270,9 @@ if (!data || !data.ok) { return; } + var currentInstallChannel = state.form && state.form.install_channel === "latest" ? "latest" : "stable"; state.form = data.config || {}; + state.form.install_channel = currentInstallChannel; state.form.default_model = resolveModelValue(state.form); state.options = data.options || {}; if (!state.form.base_dir) { diff --git a/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua b/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua index 8701a934..9be8a14e 100644 --- a/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua +++ b/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua @@ -499,10 +499,12 @@ function action_status() local node_ver = "" local oc_ver = "" + local target_oc_ver = "" if base_dir ~= "" then node_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh node_version 2>/dev/null"):gsub("%s+$", "") oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh openclaw_version 2>/dev/null"):gsub("%s+$", "") end + target_oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh latest_openclaw_version 2>/dev/null"):gsub("%s+$", "") local pid = running and get_running_pid() or "" if pid == "" and installing and lock_pid ~= "" then pid = lock_pid @@ -535,6 +537,7 @@ function action_status() token = token, node_version = node_ver, openclaw_version = oc_ver, + target_openclaw_version = target_oc_ver, pid = pid, uptime_human = uptime_human, base_url = base_url, @@ -569,7 +572,14 @@ end function action_check_update() local sys = require "luci.sys" + local http = require "luci.http" + local util = require "luci.util" local uci = require "luci.model.uci".cursor() + local install_channel = http.formvalue("install_channel") or "" + if install_channel ~= "" and install_channel ~= "stable" and install_channel ~= "latest" then + write_json({ ok = false, error = "invalid install_channel" }) + return + end if not require_csrf() then return @@ -597,7 +607,9 @@ function action_check_update() return end - local remote_ver = trim(sys.exec("/usr/libexec/istorec/openclawmgr.sh latest_openclaw_version 2>/dev/null")) + local remote_channel = install_channel ~= "" and install_channel or "latest" + local remote_cmd = "INSTALL_CHANNEL=" .. util.shellquote(remote_channel) .. " /usr/libexec/istorec/openclawmgr.sh latest_openclaw_version 2>/dev/null" + local remote_ver = trim(sys.exec(remote_cmd)) if remote_ver == "" then write_json({ ok = false, error = "获取远程版本失败", installed = true, local_version = local_ver }) return @@ -871,11 +883,16 @@ function action_op() end local op = http.formvalue("op") or "" + local install_channel = http.formvalue("install_channel") or "" local ok_ops = { install = true, upgrade = true, start = true, stop = true, restart = true, apply_config = true, uninstall = true, uninstall_openclaw = true, purge = true, cancel_install = true } if not ok_ops[op] then write_json({ ok = false, error = "unknown op" }) return end + if install_channel ~= "" and install_channel ~= "stable" and install_channel ~= "latest" then + write_json({ ok = false, error = "invalid install_channel" }) + return + end local task_id = "openclawmgr" local script_path = "/usr/libexec/istorec/openclawmgr.sh" @@ -916,12 +933,18 @@ function action_op() if not fs.access("/etc/init.d/tasks") then -- fallback (shouldn't happen on iStoreOS with luci-lib-taskd installed) local cmd = script_path .. " " .. util.shellquote(op) + if install_channel ~= "" and (op == "install" or op == "upgrade") then + cmd = "INSTALL_CHANNEL=" .. util.shellquote(install_channel) .. " " .. cmd + end sys.exec("( " .. cmd .. " ) >/dev/null 2>&1 &") write_json({ ok = true, queued = true, task_id = task_id, warning = "taskd missing; fallback to background exec" }) return end local cmd = string.format("\"%s\" %s", script_path, op) + if install_channel ~= "" and (op == "install" or op == "upgrade") then + cmd = "INSTALL_CHANNEL=" .. util.shellquote(install_channel) .. " " .. cmd + end local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(cmd) .. " >/dev/null 2>&1") if rc == 0 then write_json({ ok = true, task_id = task_id }) diff --git a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh index dc340521..82aa16b1 100755 --- a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh +++ b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh @@ -6,6 +6,9 @@ APP="openclawmgr" UCI_NS="openclawmgr" LOCK_DIR="/tmp/openclawmgr-installer.lock" DIAG_PREFIX="/tmp/openclawmgr-diag" +OPENCLAW_STABLE_NPM_TAG="openclaw@2026.3.28" +OPENCLAW_STABLE_VERSION="2026.3.28" +INSTALL_CHANNEL="${INSTALL_CHANNEL:-}" log_ts() { date "+%Y-%m-%d %H:%M:%S"; } @@ -391,6 +394,10 @@ local_openclaw_version() { latest_openclaw_version() { local npm_registry="" + if [ "${INSTALL_CHANNEL:-stable}" != "latest" ]; then + printf "%s\n" "$OPENCLAW_STABLE_VERSION" + return 0 + fi [ -x "$NPM_BIN" ] || return 0 if [ "${INSTALL_ACCELERATED:-1}" = "1" ]; then npm_registry="https://registry.npmmirror.com" @@ -406,6 +413,14 @@ latest_openclaw_version() { fi } +target_openclaw_package() { + if [ "${INSTALL_CHANNEL:-stable}" = "latest" ]; then + printf "%s\n" "openclaw@latest" + else + printf "%s\n" "$OPENCLAW_STABLE_NPM_TAG" + fi +} + have_openclaw_runtime() { [ -x "$NODE_BIN" ] || return 1 find_entry >/dev/null 2>&1 || return 1 @@ -969,7 +984,9 @@ install_openclaw() { npm_registry="https://registry.npmmirror.com" fi - write_installer_log "Installing OpenClaw from npm" + local target_pkg="" + target_pkg="$(target_openclaw_package)" + write_installer_log "Installing OpenClaw from npm (${target_pkg})" write_installer_log "Node.js: $(PATH=\"${NODE_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \"$NODE_BIN\" --version 2>/dev/null || echo unknown), npm: $(PATH=\"${NODE_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \"$NPM_BIN\" --version 2>/dev/null || echo unknown)" write_installer_log "npm prefix: $GLOBAL_DIR, cache: ${BASE_DIR}/npm-cache, musl: $(is_musl && echo yes || echo no) ${flags:+($flags)}" write_installer_log "npm registry: ${npm_registry:-default}" @@ -1008,12 +1025,12 @@ install_openclaw() { if [ -n "$npm_registry" ]; then HOME="$DATA_DIR" npm_config_cache="${BASE_DIR}/npm-cache" npm_config_registry="$npm_registry" \ PATH="${NODE_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ - "$NPM_BIN" install -g "openclaw@latest" --prefix="$GLOBAL_DIR" \ + "$NPM_BIN" install -g "$target_pkg" --prefix="$GLOBAL_DIR" \ $flags --no-audit --no-fund --no-progress >"$tmp" 2>&1 & else HOME="$DATA_DIR" npm_config_cache="${BASE_DIR}/npm-cache" \ PATH="${NODE_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ - "$NPM_BIN" install -g "openclaw@latest" --prefix="$GLOBAL_DIR" \ + "$NPM_BIN" install -g "$target_pkg" --prefix="$GLOBAL_DIR" \ $flags --no-audit --no-fund --no-progress >"$tmp" 2>&1 & fi local npmpid="$!" @@ -1413,6 +1430,7 @@ NODE_VERSION="24.14.0" [ -n "$DEFAULT_AGENT" ] || DEFAULT_AGENT="anthropic" [ -n "$DEFAULT_MODEL" ] || DEFAULT_MODEL="" [ -n "$INSTALL_ACCELERATED" ] || INSTALL_ACCELERATED="1" +[ -n "$INSTALL_CHANNEL" ] || INSTALL_CHANNEL="stable" [ -n "$PROVIDER_API_KEY" ] || PROVIDER_API_KEY="" [ -n "$PROVIDER_BASE_URL" ] || PROVIDER_BASE_URL="" @@ -1446,6 +1464,10 @@ case "$INSTALL_ACCELERATED" in 1|true|yes|on) INSTALL_ACCELERATED="1" ;; *) INSTALL_ACCELERATED="0" ;; esac +case "$INSTALL_CHANNEL" in + latest) INSTALL_CHANNEL="latest" ;; + *) INSTALL_CHANNEL="stable" ;; +esac require_base_dir() { if [ -z "$BASE_DIR" ]; then diff --git a/mihomo/Makefile b/mihomo/Makefile index 7f9908b9..0dddf953 100644 --- a/mihomo/Makefile +++ b/mihomo/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mihomo -PKG_VERSION:=1.19.21 -PKG_RELEASE:=2 +PKG_VERSION:=1.19.22 +PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)? diff --git a/momo/Makefile b/momo/Makefile index e1b7645e..8dbcd7c1 100644 --- a/momo/Makefile +++ b/momo/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=momo PKG_VERSION:=2026.03.29 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_LICENSE:=GPL-3.0+ PKG_MAINTAINER:=Joseph Mory diff --git a/momo/files/ucode/hijack.ut b/momo/files/ucode/hijack.ut index a0ce32ee..9baa1a73 100644 --- a/momo/files/ucode/hijack.ut +++ b/momo/files/ucode/hijack.ut @@ -161,7 +161,7 @@ } -%} -table inet nikki { +table inet momo { set dns_hijack_nfproto { type nf_proto flags interval diff --git a/nikki/Makefile b/nikki/Makefile index 35f30662..daf6d8bb 100644 --- a/nikki/Makefile +++ b/nikki/Makefile @@ -1,14 +1,14 @@ include $(TOPDIR)/rules.mk PKG_NAME:=nikki -PKG_VERSION:=2026.03.10 -PKG_RELEASE:=8 +PKG_VERSION:=2026.04.01 +PKG_RELEASE:=9 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git -PKG_SOURCE_VERSION:=v1.19.21 +PKG_SOURCE_VERSION:=v1.19.22 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL3.0+ diff --git a/openwrt-caddy/Makefile b/openwrt-caddy/Makefile index 883f9827..d13cafb8 100644 --- a/openwrt-caddy/Makefile +++ b/openwrt-caddy/Makefile @@ -6,13 +6,13 @@ include $(TOPDIR)/rules.mk PKG_NAME:=caddy -PKG_VERSION:=2.10.3 -PKG_RELEASE:=1 +PKG_VERSION:=2.11.3 +PKG_RELEASE:=2 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/caddyserver/caddy.git -PKG_SOURCE_DATE:=2025-10-09 -PKG_SOURCE_VERSION:=935b09de836d5ce001632193ac21c19abf0a57ed +PKG_SOURCE_DATE:=2026-03-31 +PKG_SOURCE_VERSION:=4f504588669e28373f455b694539475ffe4d2926 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-3.0-or-later diff --git a/openwrt-caddy/patches/100-add-Caddy-modules.patch b/openwrt-caddy/patches/100-add-Caddy-modules.patch index f1a9dfdc..41333519 100644 --- a/openwrt-caddy/patches/100-add-Caddy-modules.patch +++ b/openwrt-caddy/patches/100-add-Caddy-modules.patch @@ -1,6 +1,6 @@ diff -rNu a/cmd/caddy/main.go b/cmd/caddy/main.go ---- a/cmd/caddy/main.go 2026-01-31 03:24:59.000000000 +0800 -+++ b/cmd/caddy/main.go 2026-01-31 13:14:17.536684835 +0800 +--- a/cmd/caddy/main.go 2026-03-31 13:46:32.000000000 +0800 ++++ b/cmd/caddy/main.go 2026-04-01 13:25:45.106981103 +0800 @@ -35,6 +35,8 @@ // plug in Caddy modules here @@ -11,35 +11,35 @@ diff -rNu a/cmd/caddy/main.go b/cmd/caddy/main.go func main() { diff -rNu a/go.mod b/go.mod ---- a/go.mod 2026-01-31 03:24:59.000000000 +0800 -+++ b/go.mod 2026-01-31 13:18:07.050593326 +0800 +--- a/go.mod 2026-03-31 13:46:32.000000000 +0800 ++++ b/go.mod 2026-04-01 13:27:36.037581120 +0800 @@ -1,12 +1,13 @@ module github.com/caddyserver/caddy/v2 --go 1.25 -+go 1.25.6 +-go 1.25.0 ++go 1.26.1 require ( github.com/BurntSushi/toml v1.6.0 - github.com/DeRuina/timberjack v1.3.9 + github.com/DeRuina/timberjack v1.4.0 github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 + github.com/aksdb/caddy-cgi/v2 v2.2.7 - github.com/alecthomas/chroma/v2 v2.21.1 + github.com/alecthomas/chroma/v2 v2.23.1 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.25.1 + github.com/caddyserver/certmagic v0.25.2 @@ -19,6 +20,7 @@ - github.com/klauspost/compress v1.18.2 + github.com/klauspost/compress v1.18.5 github.com/klauspost/cpuid/v2 v2.3.0 - github.com/mholt/acmez/v3 v3.1.4 + github.com/mholt/acmez/v3 v3.1.6 + github.com/mholt/caddy-webdav v0.0.0-20260127042217-fa2f366b0d75 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.59.0 - github.com/smallstep/certificates v0.29.0 + github.com/smallstep/certificates v0.30.2 diff -rNu a/go.sum b/go.sum ---- a/go.sum 2026-01-31 03:24:59.000000000 +0800 -+++ b/go.sum 2026-01-31 13:18:07.050593326 +0800 -@@ -36,6 +36,8 @@ +--- a/go.sum 2026-03-31 13:46:32.000000000 +0800 ++++ b/go.sum 2026-04-01 13:27:36.036581120 +0800 +@@ -40,6 +40,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -48,12 +48,12 @@ diff -rNu a/go.sum b/go.sum github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -@@ -234,6 +236,8 @@ +@@ -242,6 +244,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= - github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= - github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= + github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= + github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/mholt/caddy-webdav v0.0.0-20260127042217-fa2f366b0d75 h1:PXPTUbQD59ErghFla/V4czJeRdwURX1VicmMtLsiw/E= +github.com/mholt/caddy-webdav v0.0.0-20260127042217-fa2f366b0d75/go.mod h1:Btj3NBe2qQGXsg1T8QDqfovMACGLUBH1tFqFJBF52cA= - github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= - github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= + github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= + github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=