🏅 Sync 2026-04-01 20:32:41

This commit is contained in:
github-actions[bot] 2026-04-01 20:32:41 +08:00
parent 33b64b2199
commit 82407ad322
19 changed files with 1830 additions and 113 deletions

View File

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

View File

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

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:=5
PKG_RELEASE:=6
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
PKG_LICENSE:=GPL-3.0

View File

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

View File

@ -295,6 +295,37 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 错误</span>";}'
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");'
-- 代码块 (```code```)
html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"<pre style=\\"background:#f6f8fa;padding:10px 14px;border-radius:6px;overflow-x:auto;font-size:13px;line-height:1.5;\\"><code>"+code.trim()+"</code></pre>";});'
-- 行内代码 (`code`)
html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"<code style=\\"background:#f6f8fa;padding:2px 6px;border-radius:3px;font-size:13px;\\">$1</code>");'
-- 标题 (### ## #)
html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"<h4 style=\\"margin:14px 0 8px;font-size:15px;font-weight:600;color:#24292f;\\">$1</h4>");'
html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"<h3 style=\\"margin:16px 0 10px;font-size:16px;font-weight:600;color:#24292f;\\">$1</h3>");'
html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"<h2 style=\\"margin:18px 0 12px;font-size:17px;font-weight:600;color:#24292f;border-bottom:1px solid #d0d7de;padding-bottom:6px;\\">$1</h2>");'
-- 粗体和斜体
html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"<strong>$1</strong>");'
html[#html+1] = 'html=html.replace(/\\*([^*]+)\\*/g,"<em>$1</em>");'
-- 链接 [text](url)
html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"<a href=\\"$2\\" target=\\"_blank\\" rel=\\"noopener\\" style=\\"color:#0969da;text-decoration:none;\\">$1</a>");'
-- 无序列表 (- 或 *)
html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"<li style=\\"margin:6px 0 6px 20px;line-height:1.7;\\">$1</li>");'
-- 有序列表 (1. 2. 等)
html[#html+1] = 'html=html.replace(/^\\d+\\. (.+)$/gm,"<li style=\\"margin:6px 0 6px 20px;list-style-type:decimal;line-height:1.7;\\">$1</li>");'
-- 水平线 (--- 或 ***)
html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"<hr style=\\"border:none;border-top:1px solid #d0d7de;margin:14px 0;\\"/>");'
-- 段落 (连续的非空行合并)
html[#html+1] = 'html=html.replace(/\\n\\n/g,"</p><p style=\\"margin:10px 0;line-height:1.7;\\">");'
-- 换行
html[#html+1] = 'html=html.replace(/\\n/g,"<br/>");'
html[#html+1] = 'return"<div style=\\"font-size:14px;color:#24292f;line-height:1.7;\\">"+html+"</div>";'
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");'
html[#html+1] = 'notesHtml=\'<div style="margin:10px 0 8px;padding:10px 14px;background:#fffbf0;border:1px solid #f0c040;border-radius:6px;">\''
html[#html+1] = '+\'<div style="font-size:12px;font-weight:600;color:#8a6a00;margin-bottom:6px;">📋 v\'+r.plugin_latest+\' 更新内容</div>\''
html[#html+1] = '+\'<pre style="margin:0;font-size:12px;color:#444;white-space:pre-wrap;word-break:break-word;line-height:1.6;">\'+escaped+\'</pre>\''
html[#html+1] = 'var rendered=ocMarkdownToHtml(r.release_notes);'
html[#html+1] = 'notesHtml=\'<div style="margin:10px 0 8px;padding:12px 16px;background:#f6f8fa;border:1px solid #d0d7de;border-radius:8px;max-height:400px;overflow-y:auto;">\''
html[#html+1] = '+\'<div style="font-size:13px;font-weight:600;color:#24292f;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #d0d7de;">📋 v\'+r.plugin_latest+\' 更新内容</div>\''
html[#html+1] = '+rendered'
html[#html+1] = '+\'</div>\';'
html[#html+1] = '}'
html[#html+1] = 'act.innerHTML=notesHtml'

View File

@ -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"
%>
<style type="text/css">
.oc-page-header { margin: 0 0 16px 0; }
.oc-page-header h2 { font-size: 18px; font-weight: 600; color: #333; margin: 0 0 6px 0; }
.oc-page-header p { font-size: 13px; color: #666; margin: 0; line-height: 1.6; }
.oc-prereq-box {
background: #fff8c5;
border: 1px solid #d29922;
border-radius: 8px;
padding: 14px 18px;
margin-bottom: 16px;
}
.oc-prereq-box h3 { margin: 0 0 10px 0; font-size: 14px; color: #9a6700; }
.oc-prereq-box ol { margin: 0; padding-left: 20px; }
.oc-prereq-box li { margin: 6px 0; font-size: 13px; color: #555; line-height: 1.6; }
.oc-prereq-box code {
background: #fff;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: #333;
border: 1px solid #e0e0e0;
}
.oc-prereq-box .warning {
background: #ffeef0;
border: 1px solid #cf222e;
color: #cf222e;
padding: 8px 12px;
border-radius: 6px;
margin-top: 12px;
font-size: 12px;
}
.oc-status-box {
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 14px 18px;
margin-bottom: 16px;
}
.oc-status-box h3 { margin: 0 0 12px 0; font-size: 14px; color: #333; }
.oc-status-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e1e4e8;
}
.oc-status-row:last-child { border-bottom: none; }
.oc-status-label { font-size: 13px; color: #666; }
.oc-status-value { font-size: 13px; font-weight: 500; }
.oc-status-value.ok { color: #1a7f37; }
.oc-status-value.warn { color: #9a6700; }
.oc-status-value.error { color: #cf222e; }
.oc-action-bar {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
/* 进度对话框 */
.oc-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.oc-modal-overlay.show { display: flex; }
.oc-modal {
background: #fff;
border-radius: 12px;
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.oc-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.oc-modal-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.oc-modal-close {
background: none;
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
padding: 0;
line-height: 1;
}
.oc-modal-close:hover { color: #333; }
.oc-modal-body { margin-bottom: 16px; }
.oc-progress-box {
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.oc-progress-box pre {
margin: 0;
font-size: 12px;
color: #333;
white-space: pre;
word-break: normal;
max-height: 300px;
overflow-y: auto;
overflow-x: auto;
font-family: Consolas, Monaco, 'Courier New', monospace;
line-height: 12px;
letter-spacing: 0;
}
.oc-progress-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e0e0e0;
border-top-color: #4a90d9;
border-radius: 50%;
animation: oc-spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes oc-spin {
to { transform: rotate(360deg); }
}
.oc-result-success {
background: #e6f7e9;
border: 1px solid #1a7f37;
border-radius: 8px;
padding: 12px;
color: #1a7f37;
text-align: center;
}
.oc-result-error {
background: #ffeef0;
border: 1px solid #cf222e;
border-radius: 8px;
padding: 12px;
color: #cf222e;
text-align: center;
}
.oc-result-info {
background: #ddf4ff;
border: 1px solid #0969da;
border-radius: 8px;
padding: 12px;
color: #0969da;
text-align: center;
}
/* 二维码显示 */
.oc-qrcode-box {
text-align: center;
padding: 20px;
}
.oc-qrcode-box img {
max-width: 256px;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.oc-qrcode-box p {
margin: 12px 0 0 0;
font-size: 13px;
color: #666;
}
/* ASCII 二维码显示 */
.oc-qrcode-ascii {
background: #fff;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 16px;
margin: 12px 0;
overflow-x: auto;
text-align: left;
}
.oc-qrcode-ascii pre {
font-family: 'Courier New', Consolas, monospace;
font-size: 6px;
line-height: 6px;
margin: 0;
white-space: pre;
letter-spacing: 0;
color: #000;
}
/* 链接按钮样式 */
.oc-qrcode-link-btn {
display: inline-block;
background: #1a7f37;
color: #fff !important;
padding: 14px 28px;
border-radius: 8px;
text-decoration: none !important;
font-size: 15px;
font-weight: 600;
margin: 16px 0;
transition: all 0.2s;
cursor: pointer;
border: none;
box-shadow: 0 2px 8px rgba(26, 127, 55, 0.3);
}
.oc-qrcode-link-btn:hover {
background: #1a6330;
color: #fff !important;
text-decoration: none !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(26, 127, 55, 0.4);
}
.oc-qrcode-link-btn:visited {
color: #fff !important;
}
/* 复制按钮样式 */
.oc-copy-btn {
display: inline-block;
background: #f6f8fa;
color: #333;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: 1px solid #d0d7de;
margin-left: 8px;
transition: all 0.2s;
}
.oc-copy-btn:hover {
background: #f3f4f6;
border-color: #1a7f37;
color: #1a7f37;
}
.oc-copy-btn.copied {
background: #e6f7e9;
border-color: #1a7f37;
color: #1a7f37;
}
/* 小号链接(备用复制) */
.oc-qrcode-copy-link {
font-size: 12px;
color: #666;
margin-top: 12px;
word-break: break-all;
line-height: 1.6;
}
.oc-qrcode-copy-link code {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
border: 1px solid #e1e4e8;
}
</style>
<div class="oc-page-header">
<h2>💬 微信渠道配置</h2>
<p>通过微信 ClawBot 插件连接微信,实现与 AI 助手的私聊对话。支持多账号同时在线。</p>
</div>
<div class="oc-prereq-box">
<h3>📋 前置条件</h3>
<ol>
<li><strong>微信客户端版本 >= 8.0.70</strong> — 旧版本可能不支持插件功能</li>
<li><strong>启用 ClawBot 插件</strong> — 安装插件后→ 打开微信客户端「扫一扫」扫描屏幕中的二维码</li>
</ol>
<div class="warning">
⚠️ 注意:微信登录有可能会验证账号安全环境,为了防止微信风控封号,尽量使用常用账号且确保账号状态正常。通过上方扫码即可直接连接,无需在手机端手动启用额外功能。
</div>
</div>
<div class="oc-status-box">
<h3>📊 插件状态</h3>
<div class="oc-status-row">
<span class="oc-status-label">微信插件</span>
<span class="oc-status-value" id="oc-wechat-plugin">检测中...</span>
</div>
<div class="oc-status-row">
<span class="oc-status-label">插件版本</span>
<span class="oc-status-value" id="oc-wechat-plugin-ver"></span>
</div>
<div class="oc-status-row" id="oc-wechat-accounts-row" style="flex-direction: column; align-items: flex-start;">
<div style="display: flex; justify-content: space-between; width: 100%;">
<span class="oc-status-label" style="align-self: center;">登录状态</span>
<span class="oc-status-value" id="oc-wechat-login">检测中...</span>
</div>
<div id="oc-wechat-accounts-list" style="width: 100%; margin-top: 10px; display: none;">
<!-- 动态插入账号 -->
</div>
</div>
<div class="oc-status-row">
<span class="oc-status-label">OpenClaw 版本</span>
<span class="oc-status-value" id="oc-wechat-ocver">检测中...</span>
</div>
</div>
<div class="oc-action-bar">
<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocInstallWechatPlugin()" id="btn-install-plugin">📦 安装微信插件</button>
<button class="btn cbi-button cbi-button-action" type="button" onclick="ocCheckUpgradeWechat()" id="btn-upgrade-plugin">🔍 检测升级</button>
<button class="btn cbi-button" type="button" onclick="ocRefreshWechatStatus()">🔄 刷新状态</button>
<button class="btn cbi-button" type="button" onclick="ocUninstallWechatPlugin()" style="background:#ffeef0;color:#cf222e;border-color:#cf222e;">🗑️ 卸载插件</button>
</div>
<!-- 安装进度对话框 -->
<div class="oc-modal-overlay" id="oc-install-modal">
<div class="oc-modal">
<div class="oc-modal-header">
<h3 id="oc-install-title">📦 安装微信插件</h3>
<button class="oc-modal-close" onclick="ocCloseInstallModal()">&times;</button>
</div>
<div class="oc-modal-body">
<div id="oc-install-progress">
<div class="oc-progress-box">
<span class="oc-progress-spinner"></span>
<span>正在安装,请稍候...</span>
</div>
<pre id="oc-install-log"></pre>
<div id="oc-install-qrcode-link" style="display:none; margin: 10px 0; text-align: center;"></div>
</div>
<div id="oc-install-result" style="display:none;"></div>
</div>
</div>
</div>
<!-- 登录二维码对话框 -->
<div class="oc-modal-overlay" id="oc-login-modal">
<div class="oc-modal">
<div class="oc-modal-header">
<h3>📱 微信扫码登录</h3>
<button class="oc-modal-close" onclick="ocCloseLoginModal()">&times;</button>
</div>
<div class="oc-modal-body">
<div id="oc-login-progress">
<div class="oc-progress-box">
<span class="oc-progress-spinner"></span>
<span>正在获取登录二维码...</span>
</div>
</div>
<div id="oc-login-qrcode" style="display:none;"></div>
<div id="oc-login-result" style="display:none;"></div>
</div>
</div>
</div>
<!-- 升级检测对话框 -->
<div class="oc-modal-overlay" id="oc-upgrade-modal">
<div class="oc-modal">
<div class="oc-modal-header">
<h3>🔍 检测插件升级</h3>
<button class="oc-modal-close" onclick="ocCloseUpgradeModal()">&times;</button>
</div>
<div class="oc-modal-body">
<div id="oc-upgrade-progress">
<div class="oc-progress-box">
<span class="oc-progress-spinner"></span>
<span>正在检测...</span>
</div>
</div>
<div id="oc-upgrade-result" style="display:none;"></div>
</div>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
(function() {
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
var wechatStatusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_status")%>';
var wechatInstallUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_install")%>';
var wechatInstallLogUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_install_log")%>';
var wechatLoginUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_login")%>';
var wechatLoginStatusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_login_status")%>';
var wechatLogoutUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_logout")%>'; var installPollTimer = null;
var loginPollTimer = null;
var pluginInstalled = false;
// 刷新微信状态
window.ocRefreshWechatStatus = function() {
// 更新 OpenClaw 版本
(new XHR()).get(statusUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
var verEl = document.getElementById('oc-wechat-ocver');
if (d.oc_version) {
verEl.textContent = 'v' + d.oc_version;
verEl.className = 'oc-status-value ok';
if (d.oc_version >= '2026.3.22') {
verEl.textContent += ' ✓ 兼容';
} else {
verEl.textContent += ' ⚠️ 需升级至 2026.3.22+';
verEl.className = 'oc-status-value warn';
}
} else {
verEl.textContent = '未安装';
verEl.className = 'oc-status-value error';
}
} catch(e) {}
});
// 检测微信插件状态
(new XHR()).get(wechatStatusUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
var pluginEl = document.getElementById('oc-wechat-plugin');
var pluginVerEl = document.getElementById('oc-wechat-plugin-ver');
var loginEl = document.getElementById('oc-wechat-login');
var btnInstall = document.getElementById('btn-install-plugin');
var btnUpgrade = document.getElementById('btn-upgrade-plugin');
pluginInstalled = d.plugin_installed;
if (d.plugin_installed) {
pluginEl.textContent = '✅ 已安装';
pluginEl.className = 'oc-status-value ok';
btnInstall.textContent = '📦 重新安装插件';
btnUpgrade.disabled = false;
// 显示插件版本
if (d.plugin_version) {
pluginVerEl.textContent = 'v' + d.plugin_version;
} else {
pluginVerEl.textContent = '已安装';
}
} else {
pluginEl.textContent = '❌ 未安装';
pluginEl.className = 'oc-status-value error';
btnInstall.textContent = '📦 安装微信插件';
btnUpgrade.disabled = true;
pluginVerEl.textContent = '—';
}
if (d.logged_in && d.accounts && d.accounts.length > 0) {
loginEl.textContent = '✅ 已登录 (' + d.accounts.length + ' 个账号)';
loginEl.className = 'oc-status-value ok';
var accHtml = '<ul style="list-style:none; padding:0; margin:0; border-top:1px dashed #eee; padding-top:10px;">';
for (var i = 0; i < d.accounts.length; i++) {
accHtml += '<li style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; padding:6px; background:#fafafa; border-radius:4px;">' +
'<span style="font-size:13px; color:#555;">👤 ' + escapeHtml(d.accounts[i].name) + '</span>' +
'<button class="oc-copy-btn" style="color:#cf222e; border-color:#cf222e;" onclick="ocLogoutWechatAccount(\'' + escapeHtml(d.accounts[i].name) + '\')">退出账号</button>' +
'</li>';
}
accHtml += '</ul><div style="text-align:right; margin-top:8px;"><button class="btn cbi-button" type="button" onclick="ocShowLoginQRCode()"> 添加新账号</button></div>';
document.getElementById('oc-wechat-accounts-list').innerHTML = accHtml;
document.getElementById('oc-wechat-accounts-list').style.display = 'block';
} else if (d.plugin_installed) {
loginEl.textContent = '⚠️ 未登录';
loginEl.className = 'oc-status-value warn';
document.getElementById('oc-wechat-accounts-list').innerHTML = '<div style="text-align:right; margin-top:8px;"><button class="btn cbi-button" type="button" onclick="ocShowLoginQRCode()">登录账号</button></div>';
document.getElementById('oc-wechat-accounts-list').style.display = 'block';
} else {
loginEl.textContent = '—';
loginEl.className = 'oc-status-value';
document.getElementById('oc-wechat-accounts-list').style.display = 'none';
}
} catch(e) {
document.getElementById('oc-wechat-plugin').textContent = '检测失败';
document.getElementById('oc-wechat-login').textContent = '检测失败';
}
});
};
// 安装微信插件 (只安装,不自动登录)
window.ocInstallWechatPlugin = function() {
var btn = document.getElementById('btn-install-plugin');
btn.disabled = true;
btn.textContent = '⏳ 安装中...';
// 显示安装对话框
document.getElementById('oc-install-modal').classList.add('show');
document.getElementById('oc-install-title').textContent = '📦 安装微信插件';
document.getElementById('oc-install-progress').style.display = 'block';
document.getElementById('oc-install-result').style.display = 'none';
document.getElementById('oc-install-log').textContent = '正在启动安装...\n';
// 启动安装
(new XHR()).post(wechatInstallUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
if (d.status === 'ok') {
pollInstallLog(false); // 不自动弹出登录
} else {
showInstallError(d.message || '启动安装失败');
}
} catch(e) {
showInstallError('启动安装失败: ' + e.message);
}
});
};
// 轮询安装日志
function pollInstallLog(autoShowQRCode) {
if (installPollTimer) clearTimeout(installPollTimer);
(new XHR()).get(wechatInstallLogUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
var logEl = document.getElementById('oc-install-log');
if (d.log) {
// 1. 清理 ANSI 控制字符
var cleanLog = d.log.replace(/\x1b\[[0-9;]*m/g, '');
// 2. 清理指定的特定前缀
cleanLog = cleanLog.replace(/\[openclaw-weixin\] /g, '');
// 3. 提取其中的二维码链接
var linkMatch = cleanLog.match(/(https:\/\/liteapp\.weixin\.qq\.com\/q\/[^\s]+)/);
if (linkMatch && linkMatch[1]) {
var linkUrl = linkMatch[1];
var linkContainer = document.getElementById('oc-install-qrcode-link');
linkContainer.style.display = 'block';
linkContainer.innerHTML = '<a href="' + escapeHtml(linkUrl) + '" target="_blank" rel="noopener noreferrer" class="oc-qrcode-link-btn" style="padding: 10px 20px; font-size: 14px; margin: 0;">🔗 点击此处前往浏览器扫码登录</a>';
}
logEl.textContent = cleanLog;
} else {
logEl.textContent = '等待输出...';
}
// 滚动到底部
logEl.scrollTop = logEl.scrollHeight;
if (d.running) {
installPollTimer = setTimeout(function() { pollInstallLog(autoShowQRCode); }, 1500);
} else if (d.state === 'success') {
showInstallSuccess(autoShowQRCode);
} else if (d.state === 'failed') {
showInstallError('安装失败 (exit: ' + d.exit_code + ')');
}
} catch(e) {
installPollTimer = setTimeout(function() { pollInstallLog(autoShowQRCode); }, 1500);
}
});
}
// 显示安装成功
function showInstallSuccess(autoShowQRCode) {
document.getElementById('oc-install-progress').style.display = 'none';
var resultEl = document.getElementById('oc-install-result');
resultEl.style.display = 'block';
resultEl.innerHTML = '<div class="oc-result-success">✅ 微信插件安装成功!</div>';
var btn = document.getElementById('btn-install-plugin');
btn.disabled = false;
btn.textContent = '📦 重新安装插件';
ocRefreshWechatStatus();
// 安装成功后自动弹出登录二维码
setTimeout(function() {
ocCloseInstallModal();
ocShowLoginQRCode();
}, 1000);
}
// 显示安装失败
function showInstallError(message) {
document.getElementById('oc-install-progress').style.display = 'none';
var resultEl = document.getElementById('oc-install-result');
resultEl.style.display = 'block';
resultEl.innerHTML = '<div class="oc-result-error">❌ ' + message + '</div>';
var btn = document.getElementById('btn-install-plugin');
btn.disabled = false;
btn.textContent = '📦 安装微信插件';
}
// 关闭安装对话框
window.ocCloseInstallModal = function() {
document.getElementById('oc-install-modal').classList.remove('show');
document.getElementById('oc-install-qrcode-link').style.display = 'none';
if (installPollTimer) {
clearTimeout(installPollTimer);
installPollTimer = null;
}
};
// 显示登录二维码
window.ocShowLoginQRCode = function() {
document.getElementById('oc-login-modal').classList.add('show');
document.getElementById('oc-login-progress').style.display = 'block';
document.getElementById('oc-login-qrcode').style.display = 'none';
document.getElementById('oc-login-result').style.display = 'none';
// 启动登录流程
(new XHR()).post(wechatLoginUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
if (d.status === 'ok') {
setTimeout(pollLoginStatus, 2000);
} else {
showLoginError(d.message || '启动登录失败');
}
} catch(e) {
showLoginError('启动登录失败: ' + e.message);
}
});
};
window.ocLogoutWechatAccount = function(accountId) {
if (confirm('确认退出账号 ' + accountId + ' 吗?退出后需要重新扫码。')) {
var btn = event.target || event.srcElement;
var oldText = btn.textContent;
btn.textContent = '退出中...';
btn.disabled = true;
(new XHR()).post(wechatLogoutUrl + '?account=' + encodeURIComponent(accountId), null, function(x) {
btn.textContent = oldText;
btn.disabled = false;
ocRefreshWechatStatus();
});
}
};
// HTML 转义函数
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 复制到剪贴板
window.ocCopyToClipboard = function(text, btn) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
btn.textContent = '已复制';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = '复制';
btn.classList.remove('copied');
}, 2000);
}).catch(function() {
fallbackCopy(text, btn);
});
} else {
fallbackCopy(text, btn);
}
};
function fallbackCopy(text, btn) {
var input = document.createElement('input');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.value = text;
document.body.appendChild(input);
input.select();
try {
document.execCommand('copy');
btn.textContent = '已复制';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = '复制';
btn.classList.remove('copied');
}, 2000);
} catch(e) {
alert('复制失败,请手动复制: ' + text);
}
document.body.removeChild(input);
}
// 轮询登录状态
function pollLoginStatus() {
if (loginPollTimer) clearTimeout(loginPollTimer);
(new XHR()).get(wechatLoginStatusUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
var qrcodeEl = document.getElementById('oc-login-qrcode');
// 是否出现二维码过期文本提示
var isExpired = d.qrcode && d.qrcode.indexOf('已被扫描') > -1 || d.qrcode.indexOf('二维码已过期') > -1;
var statusMessage = isExpired ? '<p style="color:#cf222e;font-size:13px;margin:8px 0;text-align:center;">⌛ 二维码已刷新,请点击最新链接扫码登录</p>'
: '<p style="font-size:14px;color:#333;margin-bottom:8px;text-align:center;">📱 请点击下方按钮打开微信扫码登录</p>';
if (d.state === 'success' || d.logged_in) {
showLoginSuccess();
} else if (d.state === 'failed') {
showLoginError('登录失败');
} else if (d.state === 'qrcode' && d.qrcode_url) {
document.getElementById('oc-login-progress').style.display = 'none';
qrcodeEl.style.display = 'block';
var safeUrl = escapeHtml(d.qrcode_url);
qrcodeEl.innerHTML = '<div class="oc-qrcode-box">' +
statusMessage +
'<a href="' + safeUrl + '" target="_blank" rel="noopener noreferrer" class="oc-qrcode-link-btn" style="font-size:16px;">🔗 点击获取微信登录二维码</a>' +
'<p class="oc-qrcode-copy-link">或复制链接: <code>' + safeUrl + '</code>' +
'<button class="oc-copy-btn" onclick="ocCopyToClipboard(\'' + safeUrl.replace(/'/g, "\\'") + '\', this)">复制</button></p>' +
'</div>';
loginPollTimer = setTimeout(pollLoginStatus, 2000);
} else if (d.running) {
if (d.qrcode && d.qrcode.length > 50) {
var urlMatches = d.qrcode.match(/https?:\/\/[^\s\n]+/g);
if (urlMatches && urlMatches.length > 0) {
document.getElementById('oc-login-progress').style.display = 'none';
qrcodeEl.style.display = 'block';
var extractedUrl = urlMatches[urlMatches.length - 1]; // 取最后一个
var safeUrl2 = escapeHtml(extractedUrl);
qrcodeEl.innerHTML = '<div class="oc-qrcode-box">' +
statusMessage +
'<a href="' + safeUrl2 + '" target="_blank" rel="noopener noreferrer" class="oc-qrcode-link-btn" style="font-size:16px;">🔗 点击获取微信登录二维码</a>' +
'<p class="oc-qrcode-copy-link">或复制链接: <code>' + safeUrl2 + '</code>' +
'<button class="oc-copy-btn" onclick="ocCopyToClipboard(\'' + safeUrl2.replace(/'/g, "\\'") + '\', this)">复制</button></p>' +
'</div>';
} else {
qrcodeEl.style.display = 'block';
qrcodeEl.innerHTML = '<div class="oc-qrcode-box"><p>正在加载登录链接...</p></div>';
}
}
loginPollTimer = setTimeout(pollLoginStatus, 2000);
}
} catch(e) {
loginPollTimer = setTimeout(pollLoginStatus, 2000);
}
});
}
// 显示登录成功
function showLoginSuccess() {
document.getElementById('oc-login-progress').style.display = 'none';
document.getElementById('oc-login-qrcode').style.display = 'none';
var resultEl = document.getElementById('oc-login-result');
resultEl.style.display = 'block';
resultEl.innerHTML = '<div class="oc-result-success">✅ 微信登录成功!<br><br>现在可以开始与 AI 助手对话了。</div>';
ocRefreshWechatStatus();
}
// 显示登录失败
function showLoginError(message) {
document.getElementById('oc-login-progress').style.display = 'none';
var resultEl = document.getElementById('oc-login-result');
resultEl.style.display = 'block';
resultEl.innerHTML = '<div class="oc-result-error">❌ ' + message + '</div>';
}
// 关闭登录对话框
window.ocCloseLoginModal = function() {
document.getElementById('oc-login-modal').classList.remove('show');
if (loginPollTimer) {
clearTimeout(loginPollTimer);
loginPollTimer = null;
}
};
// 检测升级
window.ocCheckUpgradeWechat = function() {
document.getElementById('oc-upgrade-modal').classList.add('show');
document.getElementById('oc-upgrade-progress').style.display = 'block';
document.getElementById('oc-upgrade-result').style.display = 'none';
// 调用 npm 检测最新版本
var checkUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_check_upgrade")%>';
(new XHR()).get(checkUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
document.getElementById('oc-upgrade-progress').style.display = 'none';
var resultEl = document.getElementById('oc-upgrade-result');
resultEl.style.display = 'block';
if (d.has_upgrade) {
resultEl.innerHTML = '<div class="oc-result-info">' +
'🔄 发现新版本: ' + d.latest_version + '<br>' +
'当前版本: ' + d.current_version + '<br><br>' +
'<button class="btn cbi-button cbi-button-apply" onclick="ocUpgradeWechatPlugin(\'' + d.latest_version + '\')">立即升级</button>' +
'</div>';
} else if (d.current_version) {
resultEl.innerHTML = '<div class="oc-result-success">✅ 当前已是最新版本: ' + d.current_version + '</div>';
} else {
resultEl.innerHTML = '<div class="oc-result-error">❌ 插件未安装,请先安装</div>';
}
} catch(e) {
document.getElementById('oc-upgrade-progress').style.display = 'none';
var resultEl = document.getElementById('oc-upgrade-result');
resultEl.style.display = 'block';
resultEl.innerHTML = '<div class="oc-result-error">❌ 检测失败: ' + e.message + '</div>';
}
});
};
// 升级插件
window.ocUpgradeWechatPlugin = function(version) {
document.getElementById('oc-upgrade-result').innerHTML = '<div class="oc-progress-box"><span class="oc-progress-spinner"></span><span>正在升级...</span></div>';
var upgradeUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_upgrade_plugin")%>';
(new XHR()).post(upgradeUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
if (d.status === 'ok') {
// 开始轮询安装日志
document.getElementById('oc-upgrade-result').innerHTML = '<div class="oc-progress-box"><pre id="oc-upgrade-log">正在升级...</pre><div id="oc-upgrade-qrcode-link" style="display:none; margin: 10px 0; text-align: center;"></div></div>';
pollUpgradeLog();
} else {
document.getElementById('oc-upgrade-result').innerHTML = '<div class="oc-result-error">❌ ' + (d.message || '升级失败') + '</div>';
}
} catch(e) {
document.getElementById('oc-upgrade-result').innerHTML = '<div class="oc-result-error">❌ 升级失败: ' + e.message + '</div>';
}
});
};
// 轮询升级日志
function pollUpgradeLog() {
(new XHR()).get(wechatInstallLogUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
var logEl = document.getElementById('oc-upgrade-log');
if (logEl) {
if (d.log) {
var cleanLog = d.log.replace(/\x1b\[[0-9;]*m/g, '');
cleanLog = cleanLog.replace(/\[openclaw-weixin\] /g, '');
var linkMatch = cleanLog.match(/(https:\/\/liteapp\.weixin\.qq\.com\/q\/[^\s]+)/);
if (linkMatch && linkMatch[1]) {
var linkUrl = linkMatch[1];
var linkContainer = document.getElementById('oc-upgrade-qrcode-link');
if (linkContainer) {
linkContainer.style.display = 'block';
linkContainer.innerHTML = '<a href="' + escapeHtml(linkUrl) + '" target="_blank" rel="noopener noreferrer" class="oc-qrcode-link-btn" style="padding: 10px 20px; font-size: 14px; margin: 0;">🔗 点击此处前往浏览器扫码登录</a>';
}
}
logEl.textContent = cleanLog;
} else {
logEl.textContent = '等待输出...';
}
logEl.scrollTop = logEl.scrollHeight;
}
if (d.running) {
setTimeout(pollUpgradeLog, 1500);
} else if (d.state === 'success') {
document.getElementById('oc-upgrade-result').innerHTML = '<div class="oc-result-success">✅ 升级成功!</div>';
ocRefreshWechatStatus();
} else if (d.state === 'failed') {
document.getElementById('oc-upgrade-result').innerHTML = '<div class="oc-result-error">❌ 升级失败</div>';
}
} catch(e) {
setTimeout(pollUpgradeLog, 1500);
}
});
}
// 关闭升级对话框
window.ocCloseUpgradeModal = function() {
document.getElementById('oc-upgrade-modal').classList.remove('show');
};
// 卸载微信插件
window.ocUninstallWechatPlugin = function() {
if (!confirm('确定要卸载微信插件吗?这将删除微信相关的所有配置。')) {
return;
}
var btn = event.target;
btn.disabled = true;
btn.textContent = '⏳ 卸载中...';
(new XHR()).post('<%=luci.dispatcher.build_url("admin", "services", "openclaw", "wechat_uninstall")%>', null, function(x) {
try {
var d = JSON.parse(x.responseText);
if (d.status === 'ok') {
alert('✅ ' + d.message);
} else {
alert('❌ ' + (d.message || '卸载失败'));
}
} catch(e) {
alert('卸载失败: ' + e.message);
}
btn.disabled = false;
btn.textContent = '🗑️ 卸载插件';
ocRefreshWechatStatus();
});
};
// 页面加载时刷新状态
ocRefreshWechatStatus();
})();
//]]>
</script>
<%+footer%>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 @@
'<label class="oclm-check"><input type="checkbox" id="oclm-install-accelerated"' + (installAcceleratedChecked ? ' checked' : '') + ' />Kspeeder 加速安装</label>' +
'</div>' +
'<button id="oclm-open-console" class="oclm-button oclm-button-primary' + ((status.running || status.reachable) ? '' : ' oclm-hidden') + '" type="button" data-open-console="1"' + (state.consoleReady ? '' : ' disabled') + '>' + openclawIcon("oclm-button-icon") + (state.consoleReady ? '打开控制台' : '控制台准备中…') + '</button>' +
'<button id="oclm-update-btn" class="oclm-button oclm-button-primary' + (showUpdateAction ? '' : ' oclm-hidden') + '" type="button" data-update-action="1"' + ((updateCheck.checking || updateCheck.upgrading) ? ' disabled' : '') + '>' + updateActionLabel(status) + '</button>' +
'<a id="oclm-open-community" class="oclm-button oclm-button-community" href="https://www.koolcenter.com/t/topic/19042" target="_blank" rel="noreferrer noopener">' + communityIcon("oclm-button-icon") + '玩家交流</a>' +
'<button id="oclm-cancel-install" class="oclm-button oclm-button-danger' + (status.installing ? '' : ' oclm-hidden') + '" type="button" data-op="cancel_install">停止安装</button>' +
'</div>' +
@ -516,6 +515,7 @@
'<button class="oclm-tab' + (activeTab === "basic" ? ' is-active' : '') + '" type="button" data-tab="basic">基础配置</button>' +
'<button class="oclm-tab' + (activeTab === "ai" ? ' is-active' : '') + '" type="button" data-tab="ai">AI配置</button>' +
'<button class="oclm-tab' + (activeTab === "access" ? ' is-active' : '') + '" type="button" data-tab="access">访问控制</button>' +
'<button class="oclm-tab' + (activeTab === "version" ? ' is-active' : '') + '" type="button" data-tab="version">版本管理</button>' +
'<button class="oclm-tab' + (activeTab === "cleanup" ? ' is-active' : '') + '" type="button" data-tab="cleanup">卸载清理</button>' +
'</div>' +
'<div class="' + (activeTab === "basic" ? '' : 'oclm-hidden') + '">' +
@ -569,6 +569,22 @@
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-access"' + (savingAccess ? ' disabled' : '') + '>' + (savingAccess ? '应用中…' : '保存访问控制设置') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
'</div></div>' +
'<div class="' + (activeTab === "version" ? '' : 'oclm-hidden') + '">' +
'<div class="oclm-section-heading"><h2>版本管理</h2><div class="oclm-hint">openclaw 官方正在快速迭代,最新版本可能不兼容旧版本配置导致无法启动。请<a class="oclm-inline-link" href="https://www.koolcenter.com/t/topic/19042" target="_blank" rel="noreferrer noopener">加入交流群</a>了解最新稳定版本号</div></div>' +
'<div class="oclm-form-grid">' +
fieldInput("当前版本", '<div class="oclm-version-tags"><span class="oclm-tag">OpenClaw <strong>' + escapeHtml(status.openclaw_version || "-") + '</strong></span>' +
(showUpdateAction ? '<button id="oclm-update-btn" class="oclm-button oclm-button-tag" type="button" data-update-action="1"' + (updateCheck.checking ? ' disabled' : '') + '>' + updateActionLabel(status) + '</button>' : '') +
'</div>' +
'<div class="oclm-status-note' + (versionNoteText() ? '' : ' oclm-hidden') + '">' + escapeHtml(versionNoteText()) + '</div>') +
fieldInput("安装版本", selectHtml("oclm-install-channel", installChannel, [
["stable", "openclaw@2026.3.28(稳定版)"],
["latest", "openclaw@latest(最新版)"]
])) +
'<div class="oclm-section-submit oclm-section-submit-right">' +
'<button class="oclm-button oclm-button-primary" type="button" id="oclm-version-install" data-version-install-action="1"' + (status.installing ? ' disabled' : '') + '>' + installLabel + '</button>' +
'</div>' +
'</div></div>' +
'<div class="' + (activeTab === "cleanup" ? '' : 'oclm-hidden') + '">' +
'<h2>卸载清理</h2>' +
'<div class="oclm-banner">以下操作会影响当前安装环境,请在确认后执行。</div>' +
@ -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) {

View File

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

View File

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

View File

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

View File

@ -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 <morytyann@gmail.com>

View File

@ -161,7 +161,7 @@
}
-%}
table inet nikki {
table inet momo {
set dns_hijack_nfproto {
type nf_proto
flags interval

View File

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

View File

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

View File

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