mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
🏅 Sync 2026-04-01 20:32:41
This commit is contained in:
parent
33b64b2199
commit
82407ad322
@ -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
|
||||
|
||||
@ -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`)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,"&").replace(/</g,"<").replace(/>/g,">");'
|
||||
-- 代码块 (```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,"&").replace(/</g,"<").replace(/>/g,">");'
|
||||
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'
|
||||
|
||||
931
luci-app-openclaw/luasrc/view/openclaw/wechat.htm
Normal file
931
luci-app-openclaw/luasrc/view/openclaw/wechat.htm
Normal 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()">×</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()">×</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()">×</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%>
|
||||
@ -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"
|
||||
|
||||
@ -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'})`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)?
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
}
|
||||
-%}
|
||||
|
||||
table inet nikki {
|
||||
table inet momo {
|
||||
set dns_hijack_nfproto {
|
||||
type nf_proto
|
||||
flags interval
|
||||
|
||||
@ -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+
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=
|
||||
|
||||
Loading…
Reference in New Issue
Block a user