diff --git a/luci-app-openclaw/CHANGELOG.md b/luci-app-openclaw/CHANGELOG.md index fb82e215c..4939c25c1 100644 --- a/luci-app-openclaw/CHANGELOG.md +++ b/luci-app-openclaw/CHANGELOG.md @@ -4,6 +4,49 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。 +## [2.0.10] - 2026-07-08 + +### 修复微信渠道配对 + +- 启动 OpenClaw 时自动自愈微信 npm 插件注册,补齐 `plugins.installs.openclaw-weixin`、`plugins.allow` 和 `channels.openclaw-weixin.enabled`。 +- 微信安装、升级和登录流程增加 `https://ilinkai.weixin.qq.com` 连通性检查,日志直接显示 HTTP、TLS 或 timeout 结果。 +- 兼容没有 `su` / `runuser` 的 OpenWrt 固件,微信安装、升级、登录和下线改用 `start-stop-daemon` 兜底以 `openclaw` 用户执行。 +- 微信安装网络探测优先使用 `curl`,避免低内存/精简 musl 固件上 Node `fetch` 触发 undici Wasm OOM。 +- 微信官方 CLI 安装触发 undici Wasm OOM 时,自动改用 npm 直装到 OpenClaw `npm/projects` 并继续注册插件配置。 +- 微信登录前补充 Node、python3、插件目录、账号状态目录和配置写权限检查,并清理残留登录进程与旧二维码状态。 +- 登录失败时在 LuCI 页面展示真实日志详情,不再只显示“登录失败”。 +- 二维码链接提取更稳,页面明确提示“点击链接后用微信扫码”。 +- README 补充微信插件正确配对流程和常见失败原因。 + +### 修复权限混乱 + +- 新增统一权限修复工具 `openclaw-permissions.sh`。 +- 避免把整个 `OC_DATA` 或 `OC_STATE_DIR` 递归改成 `openclaw`。 +- `npm/projects` 保持 `openclaw` 可写,插件源码目录保持 `root:root 755`,避免 `EACCES` 和 `suspicious ownership` 互相打架。 +- retained npm generation 目录保持可清理,修复 Gateway 清理旧 generation 的权限报错。 +- legacy `extensions` / `archived-extensions` 保持 root-owned,降低 OpenClaw 插件安全检查误报风险。 + +### 修复检测升级 + +- “快捷操作 → 检测升级”改为语义版本比较,只在远端版本真正高于当前版本时提示升级。 +- 本地版本高于 GitHub latest 时不再误判为可升级。 +- 已实测旧版 `2.0.8` 可通过 LuCI 检测并一键升级到 `2.0.9`,升级日志返回成功且服务保持正常。 + +### 修复干净安装启动失败 + +- 修复首次安装后 `doctor --fix` 可能移除 `gateway.auth.token`,导致 Gateway 绑定 LAN 时因缺少认证直接退出,页面显示“启动失败,退出码 78”。 +- `doctor --fix` 执行环境补齐 `NODE_ICU_DATA` 和 Node/OpenClaw PATH,避免部分 musl 固件上配置迁移阶段触发 ICU/Unicode 正则错误。 +- Gateway 启动时增加 `OPENCLAW_GATEWAY_TOKEN` 环境变量兜底,确保 JSON 配置被迁移工具改写后仍能使用 UCI token 启动。 +- `.run` 安装器补齐运行依赖安装,避免精简固件缺少 GNU tar 时 Node.js `.tar.xz` 解压失败。 +- `openclaw-env setup` 改为先完整解压验证 Node.js,再替换正式目录;安装失败时保留已有运行目录,避免重装失败清空 `/opt/openclaw`。 + +### 验证 + +- 通过 shell / Node / Lua 语法检查。 +- 通过契约测试 `tests/test_openclaw_contracts.sh`。 + +--- + ## [2.0.9] - 2026-07-04 ### 修复微信 npm 插件注册 diff --git a/luci-app-openclaw/Makefile b/luci-app-openclaw/Makefile index 31f33080c..5b25c3496 100644 --- a/luci-app-openclaw/Makefile +++ b/luci-app-openclaw/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openclaw PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0")) -PKG_RELEASE:=19 +PKG_RELEASE:=20 PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com> PKG_LICENSE:=GPL-3.0 @@ -59,6 +59,7 @@ define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/usr/libexec $(INSTALL_BIN) ./root/usr/libexec/openclaw-paths.sh $(1)/usr/libexec/openclaw-paths.sh $(INSTALL_BIN) ./root/usr/libexec/openclaw-node.sh $(1)/usr/libexec/openclaw-node.sh + $(INSTALL_BIN) ./root/usr/libexec/openclaw-permissions.sh $(1)/usr/libexec/openclaw-permissions.sh $(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller $(INSTALL_DATA) ./luasrc/controller/openclaw.lua $(1)/usr/lib/lua/luci/controller/openclaw.lua $(INSTALL_DIR) $(1)/usr/lib/lua/openclaw diff --git a/luci-app-openclaw/README.md b/luci-app-openclaw/README.md index 5d837e6f9..47bab26a4 100644 --- a/luci-app-openclaw/README.md +++ b/luci-app-openclaw/README.md @@ -113,6 +113,8 @@ openclaw-env setup - `openclaw` 系统用户是否存在,不存在时自动创建 - `python3` 是否已安装;缺失时会自动执行 `opkg update && opkg install python3-light` - npm cache、tmp、OpenClaw 数据目录是否可由 `openclaw` 用户写入 +- 路由器到 `https://ilinkai.weixin.qq.com` 的 TLS/超时状态 +- npm 插件是否已写入 `plugins.installs.openclaw-weixin`、`plugins.allow` 和 `channels.openclaw-weixin.enabled` - 旧渠道名 `weixin` 会迁移为 `openclaw-weixin` 如自动安装失败,可手动安装: @@ -122,6 +124,22 @@ opkg update opkg install python3-light ``` +微信配对推荐流程: + +1. 在 LuCI「微信配置」页点击「安装/重新安装插件」。 +2. 插件安装成功后点击「登录账号」。 +3. 页面出现链接后,先点击打开链接,再用微信「扫一扫」扫码。 +4. 等待页面提示登录成功,网关会重新加载微信账号。 +5. 用微信给网关发一条测试消息,确认能触发 AI 回复。 + +常见失败原因: + +- 二维码过期:重新点击「登录账号」生成新链接。 +- 微信账号触发安全风控:更换常用设备/常用网络后再扫码,或换账号测试。 +- 路由器网络异常:检查日志中的 `微信接口连通性检查`,重点看 TLS、timeout、DNS。 +- 插件注册缺失:重新安装插件,启动时也会自动补齐 `openclaw-weixin` 配置。 +- 目录权限错误:按页面日志提示修复 OpenClaw 数据目录权限。 + ## 已知说明 - OpenClaw 的 diagnostic heartbeat 可能在日志中出现类似周期性探测记录。它不是一次真实用户对话请求;如需降低噪音,优先在 OpenClaw 配置或日志采集侧降低诊断日志级别,不建议直接修改模型调用逻辑。 diff --git a/luci-app-openclaw/VERSION b/luci-app-openclaw/VERSION index 09843e3be..0a692060f 100644 --- a/luci-app-openclaw/VERSION +++ b/luci-app-openclaw/VERSION @@ -1 +1 @@ -2.0.9 +2.0.10 diff --git a/luci-app-openclaw/luasrc/controller/openclaw.lua b/luci-app-openclaw/luasrc/controller/openclaw.lua index 2ed75a0ea..0af5fcf55 100644 --- a/luci-app-openclaw/luasrc/controller/openclaw.lua +++ b/luci-app-openclaw/luasrc/controller/openclaw.lua @@ -52,6 +52,34 @@ local function is_safe_openclaw_root(value) return value == "/opt/openclaw" or value:match("^/mnt/[^/]+/openclaw$") ~= nil or value:match("^/media/[^/]+/openclaw$") ~= nil end +local function compare_versions(a, b) + local function parts(v) + local out = {} + for n in tostring(v or ""):gsub("^v", ""):gmatch("(%d+)") do + out[#out + 1] = tonumber(n) or 0 + end + return out + end + local aa, bb = parts(a), parts(b) + local len = math.max(#aa, #bb) + for i = 1, len do + local av, bv = aa[i] or 0, bb[i] or 0 + if av > bv then return 1 end + if av < bv then return -1 end + end + return 0 +end + +local function is_newer_version(latest, current) + return latest ~= nil and current ~= nil and latest ~= "" and current ~= "" and compare_versions(latest, current) > 0 +end + +local function fix_openclaw_state_permissions(oc_data) + local sys = require "luci.sys" + local state_dir = tostring(oc_data or "") .. "/.openclaw" + sys.exec("if [ -x /usr/libexec/openclaw-permissions.sh ]; then /usr/libexec/openclaw-permissions.sh fix-state " .. shellquote(state_dir) .. " >/dev/null 2>&1; fi") +end + local function ensure_openclaw_user(oc_data) local sys = require "luci.sys" local uid = sys.exec("id -u openclaw 2>/dev/null"):gsub("%s+", "") @@ -86,7 +114,8 @@ local function find_wechat_plugin_dir(install_path) return nil end -local function wechat_register_plugin_cmd(install_path, node_bin, log_file) +local function wechat_register_plugin_cmd(install_path, node_bin, log_file, exit_file) + exit_file = exit_file or "/tmp/openclaw-wechat-install.exit" local oc_data = install_path .. "/data" local config_file = oc_data .. "/.openclaw/openclaw.json" local npm_projects = oc_data .. "/.openclaw/npm/projects" @@ -163,11 +192,102 @@ fs.writeFileSync(configPath, JSON.stringify(d, null, 2) + '\n'); "if [ $REG_RC -eq 0 ]; then " .. "chown openclaw:openclaw " .. shellquote(config_file) .. " 2>/dev/null; " .. "echo 'Registered openclaw-weixin npm plugin in OpenClaw config.' >> " .. shellquote(log_file) .. "; " .. - "else RC=$REG_RC; echo $RC > /tmp/openclaw-wechat-install.exit; echo 'Failed to register openclaw-weixin npm plugin in OpenClaw config.' >> " .. shellquote(log_file) .. "; fi; " .. - "else RC=127; echo $RC > /tmp/openclaw-wechat-install.exit; echo 'Node.js not found, cannot register openclaw-weixin plugin.' >> " .. shellquote(log_file) .. "; fi; " .. + "else RC=$REG_RC; echo $RC > " .. shellquote(exit_file) .. "; echo 'Failed to register openclaw-weixin npm plugin in OpenClaw config.' >> " .. shellquote(log_file) .. "; fi; " .. + "else RC=127; echo $RC > " .. shellquote(exit_file) .. "; echo 'Node.js not found, cannot register openclaw-weixin plugin.' >> " .. shellquote(log_file) .. "; fi; " .. "fi; " end + +local function wechat_network_probe_cmd(node_bin, log_file) + local target = "https://ilinkai.weixin.qq.com/ilink/bot/getupdates" + local probe_js = [[ +const target = 'https://ilinkai.weixin.qq.com/ilink/bot/getupdates'; +const started = Date.now(); +const controller = new AbortController(); +const timer = setTimeout(() => controller.abort(new Error('timeout')), 10000); +(async () => { + try { + const res = await fetch(target, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: '{}', + signal: controller.signal + }); + const text = await res.text(); + const sample = text.replace(/\s+/g, ' ').slice(0, 180); + console.log(`微信接口连通性检查: HTTP ${res.status} ${res.statusText || ''} ${Date.now() - started}ms ${sample}`); + } catch (e) { + const code = e && (e.code || (e.cause && e.cause.code) || e.name) || 'ERR'; + const msg = e && e.message ? e.message : String(e); + console.log(`微信接口连通性检查失败: ${code} ${msg}`); + } finally { + clearTimeout(timer); + } +})(); +]] + return "echo '微信接口连通性检查: https://ilinkai.weixin.qq.com' >> " .. shellquote(log_file) .. "; " .. + "if command -v curl >/dev/null 2>&1; then " .. + "_oc_probe_body=/tmp/openclaw-wechat-probe-$$.txt; " .. + "_oc_probe_code=$(curl -sS -o \"$_oc_probe_body\" -w '%%{http_code}' --connect-timeout 8 --max-time 15 -X POST -H 'content-type: application/json' --data '{}' " .. shellquote(target) .. " 2>> " .. shellquote(log_file) .. "); " .. + "_oc_probe_rc=$?; " .. + "if [ $_oc_probe_rc -eq 0 ]; then _oc_probe_sample=$(tr '\\n\\r\\t' ' ' < \"$_oc_probe_body\" 2>/dev/null | cut -c1-180); echo \"微信接口连通性检查: HTTP $_oc_probe_code $_oc_probe_sample\" >> " .. shellquote(log_file) .. "; else echo \"微信接口连通性检查失败: curl exit $_oc_probe_rc\" >> " .. shellquote(log_file) .. "; fi; " .. + "rm -f \"$_oc_probe_body\"; " .. + "elif [ -x " .. shellquote(node_bin) .. " ]; then " .. + "NODE_ICU_DATA=\"${NODE_ICU_DATA:-/opt/openclaw/node/share/icu}\" " .. shellquote(node_bin) .. " -e " .. shellquote(probe_js) .. " >> " .. shellquote(log_file) .. " 2>&1 || true; " .. + "else echo '⚠️ curl/Node.js 不存在,跳过微信接口连通性检查' >> " .. shellquote(log_file) .. "; fi; " +end + +local function openclaw_user_runner_cmd() + return "_oc_as_openclaw() { " .. + "if command -v su >/dev/null 2>&1; then su -s /bin/sh openclaw -c \"$1\"; " .. + "elif command -v runuser >/dev/null 2>&1; then runuser -u openclaw -- sh -c \"$1\"; " .. + "elif command -v start-stop-daemon >/dev/null 2>&1; then _oc_pid=/tmp/openclaw-user-$$.pid; _oc_cwd=$(pwd); rm -f \"$_oc_pid\"; start-stop-daemon -S -m -p \"$_oc_pid\" -c openclaw:openclaw -d \"$_oc_cwd\" -x /bin/sh -- -c \"$1\"; _oc_rc=$?; rm -f \"$_oc_pid\"; return $_oc_rc; " .. + "else echo '❌ 缺少 su/runuser/start-stop-daemon,无法以 openclaw 用户运行命令' >&2; return 127; fi; " .. + "}; " +end + +local function wechat_npm_fallback_install_cmd(install_path, log_file, exit_file) + local package_json = "{\n \"private\": true,\n \"dependencies\": {\n \"@tencent-weixin/openclaw-weixin\": \"latest\"\n }\n}\n" + return "if [ $RC -ne 0 ]; then " .. + "echo '⚠️ 官方微信 CLI 安装失败,尝试 npm 直装兜底...' >> " .. shellquote(log_file) .. "; " .. + "OC_WECHAT_NPM_PROJECT=\"$OC_WECHAT_DATA/.openclaw/npm/projects/tencent-weixin-openclaw-weixin-7783ac86ba\"; " .. + "mkdir -p \"$OC_WECHAT_NPM_PROJECT\" \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/npm/projects\"; " .. + "chown -R openclaw:openclaw \"$OC_WECHAT_NPM_PROJECT\" \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/npm/projects\" 2>/dev/null; " .. + "cat > \"$OC_WECHAT_NPM_PROJECT/package.json\" <<'OC_WECHAT_PACKAGE_JSON'\n" .. + package_json .. + "OC_WECHAT_PACKAGE_JSON\n" .. + "chown openclaw:openclaw \"$OC_WECHAT_NPM_PROJECT/package.json\" 2>/dev/null; " .. + "cd \"$OC_WECHAT_NPM_PROJECT\" && " .. + "_oc_as_openclaw 'HOME=$OC_WECHAT_DATA OPENCLAW_HOME=$OC_WECHAT_DATA OPENCLAW_STATE_DIR=$OC_WECHAT_DATA/.openclaw OPENCLAW_CONFIG_PATH=$OC_WECHAT_DATA/.openclaw/openclaw.json " .. + "NPM_CONFIG_CACHE=$OC_WECHAT_DATA/.npm npm_config_cache=$OC_WECHAT_DATA/.npm TMPDIR=$OC_WECHAT_DATA/.tmp " .. + "PATH=" .. install_path .. "/node/bin:" .. install_path .. "/global/bin:$PATH " .. + "npm install --omit=dev --omit=peer --legacy-peer-deps --no-audit --no-fund --loglevel=error' >> " .. shellquote(log_file) .. " 2>&1; " .. + "RC=$?; echo $RC > " .. shellquote(exit_file) .. "; " .. + "if [ $RC -eq 0 ]; then echo '✅ npm 直装兜底完成' >> " .. shellquote(log_file) .. "; else echo '❌ npm 直装兜底失败 (exit: '$RC')' >> " .. shellquote(log_file) .. "; fi; " .. + "fi; " +end + +local function wechat_tail_detail(text, max_lines) + if not text or text == "" then + return "" + end + local lines = {} + for line in (text .. "\n"):gmatch("([^\r\n]*)\r?\n") do + if line and line ~= "" then + line = line:gsub("\27%[[0-9;]*m", "") + if not line:match("^%s*$") then + table.insert(lines, line) + end + end + end + local start = math.max(1, #lines - (max_lines or 30) + 1) + local out = {} + for i = start, #lines do + table.insert(out, lines[i]) + end + return table.concat(out, "\n") +end + local function write_wechat_log_and_exit(log_file, exit_file, content, exit_code) local f = io.open(log_file, "w") if f then @@ -669,7 +789,7 @@ function action_check_update() end end - if plugin_current ~= "" and plugin_latest ~= "" and plugin_current ~= plugin_latest then + if is_newer_version(plugin_latest, plugin_current) then plugin_has_update = true end @@ -1104,7 +1224,7 @@ function action_backup() local extract_out = sys.exec(extract_cmd) -- 6) 修复权限 - sys.exec("chown -R openclaw:openclaw " .. oc_data_dir .. " 2>/dev/null") + fix_openclaw_state_permissions(oc_data_dir) -- 7) 重启服务 sys.exec("/etc/init.d/openclaw start >/dev/null 2>&1 &") @@ -1437,39 +1557,41 @@ function action_wechat_install() local extensions_dir = install_path .. "/data/.openclaw/extensions" local install_cmd = string.format( "( " .. + openclaw_user_runner_cmd() .. "echo '开始安装微信插件...' > /tmp/openclaw-wechat-install.log; " .. "echo '安装路径: %s' >> /tmp/openclaw-wechat-install.log; " .. "echo 'npx 路径: %s' >> /tmp/openclaw-wechat-install.log; " .. + "echo 'Node 版本:' $(%s -v 2>/dev/null || echo 未检测到) >> /tmp/openclaw-wechat-install.log; " .. + wechat_network_probe_cmd(node_bin, "/tmp/openclaw-wechat-install.log") .. wechat_python3_bootstrap_cmd("/tmp/openclaw-wechat-install.log") .. - "mkdir -p %s/.npm %s/.tmp %s/.openclaw/extensions; " .. - "if [ ! -w %s/.openclaw/extensions ]; then chown -R openclaw:openclaw %s/.openclaw/extensions 2>/dev/null; fi; " .. - "chown -R openclaw:openclaw %s/.npm %s/.tmp %s/.openclaw 2>/dev/null; " .. + "OC_WECHAT_DATA=%s; export OC_WECHAT_DATA; " .. + "if [ -x /usr/libexec/openclaw-permissions.sh ]; then /usr/libexec/openclaw-permissions.sh prepare-workdirs \"$OC_WECHAT_DATA\" >/dev/null 2>&1; " .. + "else mkdir -p \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/extensions\"; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" 2>/dev/null; chown openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw\" 2>/dev/null; fi; " .. "if [ ! -w %s/.npm ] || [ ! -w %s/.tmp ]; then echo '❌ npm cache/tmp 目录不可写' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; fi; " .. - "su -s /bin/sh openclaw -c 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " .. + "_oc_as_openclaw 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " .. "cd %s && " .. - "su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " .. + "_oc_as_openclaw 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " .. "OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm TMPDIR=%s/.tmp " .. "PATH=%s/node/bin:%s/global/bin:$PATH " .. "%s -y @tencent-weixin/openclaw-weixin-cli install' >> /tmp/openclaw-wechat-install.log 2>&1; " .. "RC=$?; echo $RC > /tmp/openclaw-wechat-install.exit; " .. + wechat_npm_fallback_install_cmd(install_path, "/tmp/openclaw-wechat-install.log", "/tmp/openclaw-wechat-install.exit") .. wechat_register_plugin_cmd(install_path, node_bin, "/tmp/openclaw-wechat-install.log") .. -- 关键修复: 安装完成后强制修复插件目录权限 (确保 Gateway 可读取插件) -- 原因: npx/npm 以 root 身份创建目录,默认权限 700 导致其他用户无法读取 -- 注意: 保持 root:root 属主 (OpenClaw v2026.4.9+ 安全要求),仅修复权限模式 - "chown -R root:root %s 2>/dev/null; chmod -R 755 %s 2>/dev/null; " .. + "[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh fix-state %s/.openclaw >/dev/null 2>&1; " .. "if [ $RC -eq 0 ]; then echo '✅ 微信插件安装成功!' >> /tmp/openclaw-wechat-install.log; " .. "else echo '❌ 安装失败 (exit: '$RC')' >> /tmp/openclaw-wechat-install.log; fi " .. ") & echo $! > /tmp/openclaw-wechat-install.pid", - install_path, npx_bin, - oc_data, oc_data, oc_data, - oc_data, oc_data, - oc_data, oc_data, oc_data, + install_path, npx_bin, node_bin, + shellquote(oc_data), oc_data, oc_data, oc_data, oc_data, oc_data, install_path, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, install_path, install_path, npx_bin, - extensions_dir, extensions_dir + oc_data ) sys.exec(install_cmd) @@ -1573,28 +1695,53 @@ function action_wechat_login() -- 清理旧状态和可能的残留进程 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") -- 后台启动登录流程,将二维码输出到文件 + sys.exec("rm -f /tmp/openclaw-wechat-qrcode.txt /tmp/openclaw-wechat-login.pid /tmp/openclaw-wechat-login.exit /tmp/openclaw-wechat-restarted") + + local wechat_plugin_dir = find_wechat_plugin_dir(install_path) + if not wechat_plugin_dir then + write_wechat_log_and_exit( + "/tmp/openclaw-wechat-qrcode.txt", + "/tmp/openclaw-wechat-login.exit", + "微信插件未安装...\n安装路径: " .. install_path .. "\n未找到包含 openclaw.plugin.json 的微信插件目录。\n请先安装插件或重新安装插件。\n", + 1 + ) + http.prepare_content("application/json") + http.write_json({ status = "error", message = "微信插件未安装或未找到,请先安装/重新安装插件" }) + return + end + + -- 后台启动登录流程,将二维码输出到文件 local login_cmd = string.format( "( " .. + openclaw_user_runner_cmd() .. "echo '正在启动微信登录...' > /tmp/openclaw-wechat-qrcode.txt; " .. "echo '安装路径: %s' >> /tmp/openclaw-wechat-qrcode.txt; " .. + "echo 'OpenClaw 入口: %s' >> /tmp/openclaw-wechat-qrcode.txt; " .. + "echo '微信插件目录: %s' >> /tmp/openclaw-wechat-qrcode.txt; " .. + "echo 'Node 版本:' $(%s -v 2>/dev/null || echo 未检测到) >> /tmp/openclaw-wechat-qrcode.txt; " .. + "if command -v python3 >/dev/null 2>&1; then echo 'python3: 已安装' >> /tmp/openclaw-wechat-qrcode.txt; else echo '⚠️ python3: 未安装,微信插件可能无法完成配对' >> /tmp/openclaw-wechat-qrcode.txt; fi; " .. + wechat_network_probe_cmd(node_bin, "/tmp/openclaw-wechat-qrcode.txt") .. "mkdir -p %s/.npm %s/.tmp %s/.openclaw/openclaw-weixin; " .. "touch %s/.openclaw/openclaw.json 2>/dev/null || true; " .. "chown -R openclaw:openclaw %s/.npm %s/.tmp %s/.openclaw/openclaw-weixin 2>/dev/null; " .. "chown openclaw:openclaw %s/.openclaw %s/.openclaw/openclaw.json 2>/dev/null; " .. - "su -s /bin/sh openclaw -c 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw && test -w %s/.openclaw/openclaw-weixin && test -w %s/.openclaw/openclaw.json' || { echo '❌ openclaw 用户无法写入微信登录目录,请检查数据目录权限' >> /tmp/openclaw-wechat-qrcode.txt; echo 1 > /tmp/openclaw-wechat-login.exit; exit 0; }; " .. + "_oc_as_openclaw 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw && test -w %s/.openclaw/openclaw-weixin && test -w %s/.openclaw/openclaw.json' || { echo '❌ openclaw 用户无法写入微信登录目录,请检查数据目录权限' >> /tmp/openclaw-wechat-qrcode.txt; echo 1 > /tmp/openclaw-wechat-login.exit; exit 0; }; " .. + "RC=0; " .. + wechat_register_plugin_cmd(install_path, node_bin, "/tmp/openclaw-wechat-qrcode.txt", "/tmp/openclaw-wechat-login.exit") .. + "if [ $RC -ne 0 ]; then echo '❌ 微信插件注册失败,无法登录' >> /tmp/openclaw-wechat-qrcode.txt; exit 0; fi; " .. "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 " .. + "_oc_as_openclaw 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json " .. "NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm TMPDIR=%s/.tmp PATH=%s/node/bin:%s/global/bin:$PATH " .. "%s %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, + install_path, oc_entry, wechat_plugin_dir, node_bin, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, install_path, install_path, node_bin, oc_entry - ) sys.exec(login_cmd) + ) + sys.exec(login_cmd) http.prepare_content("application/json") http.write_json({ status = "ok", message = "微信登录流程已启动" }) @@ -1637,11 +1784,21 @@ function action_wechat_login_status() end end - -- 提取最后生成的二维码 URL + -- 提取最后生成的二维码 URL;排除前置网络探测中的接口 URL,避免页面误导用户扫码错误链接 local qrcode_url = "" - for url in qrcode:gmatch("https?://[^\n\r]+") do - qrcode_url = url - end -- 检查是否登录成功 + for url in qrcode:gmatch("https?://[^%s%]%)}\"'<>]+") do + if url:match("liteapp%.weixin%.qq%.com/q/") or url:match("weixin%.qq%.com/q/") then + qrcode_url = url + end + end + if qrcode_url == "" then + for url in qrcode:gmatch("https?://[^%s%]%)}\"'<>]+") do + if not url:match("ilinkai%.weixin%.qq%.com") then + qrcode_url = url + end + end + 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" @@ -1657,11 +1814,24 @@ function action_wechat_login_status() state = "failed" end - -- 如果刚登录成功,触发一次重启,确保主进程加载微信 + local error_detail = "" + local message = "" + if state == "failed" then + error_detail = wechat_tail_detail(qrcode, 35) + if error_detail ~= "" then + message = "登录失败,下面是最近日志,请按提示处理" + else + message = "登录失败,请查看 /tmp/openclaw-wechat-qrcode.txt" + end + 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") + message = "微信登录成功,正在重新加载微信账号" + end + http.prepare_content("application/json") http.write_json({ status = "ok", state = state, @@ -1669,7 +1839,9 @@ function action_wechat_login_status() qrcode_url = qrcode_url, running = running, exit_code = exit_code, - logged_in = logged_in + logged_in = logged_in, + message = message, + error_detail = error_detail }) end @@ -1778,7 +1950,7 @@ function action_wechat_check_upgrade() latest_version = sys.exec(check_cmd):gsub("%s+", "") local has_upgrade = false - if current_version ~= "" and latest_version ~= "" and current_version ~= latest_version then + if is_newer_version(latest_version, current_version) then has_upgrade = true end @@ -1849,38 +2021,40 @@ function action_wechat_upgrade_plugin() local extensions_dir = install_path .. "/data/.openclaw/extensions" local upgrade_cmd = string.format( "( " .. + openclaw_user_runner_cmd() .. "echo '正在升级微信插件...' > /tmp/openclaw-wechat-install.log; " .. "echo '安装路径: %s' >> /tmp/openclaw-wechat-install.log; " .. "echo 'npx 路径: %s' >> /tmp/openclaw-wechat-install.log; " .. + "echo 'Node 版本:' $(%s -v 2>/dev/null || echo 未检测到) >> /tmp/openclaw-wechat-install.log; " .. + wechat_network_probe_cmd(node_bin, "/tmp/openclaw-wechat-install.log") .. wechat_python3_bootstrap_cmd("/tmp/openclaw-wechat-install.log") .. - "mkdir -p %s/.npm %s/.tmp %s/.openclaw/extensions; " .. - "if [ ! -w %s/.openclaw/extensions ]; then chown -R openclaw:openclaw %s/.openclaw/extensions 2>/dev/null; fi; " .. - "chown -R openclaw:openclaw %s/.npm %s/.tmp %s/.openclaw 2>/dev/null; " .. + "OC_WECHAT_DATA=%s; export OC_WECHAT_DATA; " .. + "if [ -x /usr/libexec/openclaw-permissions.sh ]; then /usr/libexec/openclaw-permissions.sh prepare-workdirs \"$OC_WECHAT_DATA\" >/dev/null 2>&1; " .. + "else mkdir -p \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/extensions\"; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" 2>/dev/null; chown openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw\" 2>/dev/null; fi; " .. "if [ ! -w %s/.npm ] || [ ! -w %s/.tmp ]; then echo '❌ npm cache/tmp 目录不可写' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; fi; " .. - "su -s /bin/sh openclaw -c 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " .. + "_oc_as_openclaw 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " .. "cd %s && " .. - "su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " .. + "_oc_as_openclaw 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw " .. "OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm TMPDIR=%s/.tmp " .. "PATH=%s/node/bin:%s/global/bin:$PATH " .. "%s -y @tencent-weixin/openclaw-weixin-cli install' >> /tmp/openclaw-wechat-install.log 2>&1; " .. "RC=$?; echo $RC > /tmp/openclaw-wechat-install.exit; " .. + wechat_npm_fallback_install_cmd(install_path, "/tmp/openclaw-wechat-install.log", "/tmp/openclaw-wechat-install.exit") .. wechat_register_plugin_cmd(install_path, node_bin, "/tmp/openclaw-wechat-install.log") .. -- 关键修复: 升级完成后强制修复插件目录权限 (确保 Gateway 可读取插件) -- 注意: 保持 root:root 属主 (OpenClaw v2026.4.9+ 安全要求),仅修复权限模式 - "chown -R root:root %s 2>/dev/null; chmod -R 755 %s 2>/dev/null; " .. + "[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh fix-state %s/.openclaw >/dev/null 2>&1; " .. "if [ $RC -eq 0 ]; then echo '✅ 微信插件升级成功!' >> /tmp/openclaw-wechat-install.log; " .. "else echo '❌ 升级失败 (exit: '$RC')' >> /tmp/openclaw-wechat-install.log; fi " .. ") & echo $! > /tmp/openclaw-wechat-install.pid", - install_path, npx_bin, - oc_data, oc_data, oc_data, - oc_data, oc_data, - oc_data, oc_data, oc_data, + install_path, npx_bin, node_bin, + shellquote(oc_data), oc_data, oc_data, oc_data, oc_data, oc_data, install_path, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, oc_data, install_path, install_path, npx_bin, - extensions_dir, extensions_dir + oc_data ) sys.exec(upgrade_cmd) @@ -1940,7 +2114,7 @@ function action_wechat_logout() -- 在后台执行 logout local logout_cmd = string.format( - "cd %s && su -s /bin/sh openclaw -c 'HOME=%s OPENCLAW_HOME=%s OPENCLAW_STATE_DIR=%s/.openclaw OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json " .. + openclaw_user_runner_cmd() .. "cd %s && _oc_as_openclaw '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 diff --git a/luci-app-openclaw/luasrc/view/openclaw/wechat.htm b/luci-app-openclaw/luasrc/view/openclaw/wechat.htm index 01dcd7331..02fc4af31 100644 --- a/luci-app-openclaw/luasrc/view/openclaw/wechat.htm +++ b/luci-app-openclaw/luasrc/view/openclaw/wechat.htm @@ -309,11 +309,12 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
⌛ 二维码已刷新,请点击最新链接扫码登录
' - : '📱 请点击下方按钮打开微信扫码登录
'; + : '📱 请点击下方链接打开二维码,再用微信扫码
'; if (d.state === 'success' || d.logged_in) { showLoginSuccess(); } else if (d.state === 'failed') { - showLoginError('登录失败'); + showLoginError(d.message || '登录失败', d.error_detail || d.qrcode || ''); } 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 = ''; 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) { + var extractedUrl = extractWechatLoginUrl(d.qrcode); + if (extractedUrl) { document.getElementById('oc-login-progress').style.display = 'none'; qrcodeEl.style.display = 'block'; - var extractedUrl = urlMatches[urlMatches.length - 1]; // 取最后一个 var safeUrl2 = escapeHtml(extractedUrl); qrcodeEl.innerHTML = ''; @@ -770,17 +785,22 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793" document.getElementById('oc-login-qrcode').style.display = 'none'; var resultEl = document.getElementById('oc-login-result'); resultEl.style.display = 'block'; - resultEl.innerHTML = '' + escapeHtml(detail) + ''; + } + html += '
=0;y-=1)if(_=this.octets[y],_ in m){if(w=m[_],g&&w!==0)return null;w!==8&&(g=!0),p+=w}else return null;return 32-p},d.prototype.range=function(){return h.subnetMatch(this,this.SpecialRanges)},d.prototype.toByteArray=function(){return this.octets.slice(0)},d.prototype.toIPv4MappedAddress=function(){return h.IPv6.parse(`::ffff:${this.toString()}`)},d.prototype.toNormalizedString=function(){return this.toString()},d.prototype.toString=function(){return this.octets.join(".")},d}(),h.IPv4.broadcastAddressFromCIDR=function(d){try{const p=this.parseCIDR(d),g=p[0].toByteArray(),m=this.subnetMaskFromPrefixLength(p[1]).toByteArray(),y=[];let _=0;for(;_<4;)y.push(parseInt(g[_],10)|parseInt(m[_],10)^255),_++;return new this(y)}catch(p){throw new Error("ipaddr: the address does not have IPv4 CIDR format")}},h.IPv4.isIPv4=function(d){return this.parser(d)!==null},h.IPv4.isValid=function(d){try{return new this(this.parser(d)),!0}catch(p){return!1}},h.IPv4.isValidFourPartDecimal=function(d){return!!(h.IPv4.isValid(d)&&d.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/))},h.IPv4.networkAddressFromCIDR=function(d){let p,g,m,y,_;try{for(p=this.parseCIDR(d),m=p[0].toByteArray(),_=this.subnetMaskFromPrefixLength(p[1]).toByteArray(),y=[],g=0;g<4;)y.push(parseInt(m[g],10)&parseInt(_[g],10)),g++;return new this(y)}catch(w){throw new Error("ipaddr: the address does not have IPv4 CIDR format")}},h.IPv4.parse=function(d){const p=this.parser(d);if(p===null)throw new Error("ipaddr: string is not formatted like an IPv4 Address");return new this(p)},h.IPv4.parseCIDR=function(d){let p;if(p=d.match(/^(.+)\/(\d+)$/)){const g=parseInt(p[2]);if(g>=0&&g<=32){const m=[this.parse(p[1]),g];return Object.defineProperty(m,"toString",{value:function(){return this.join("/")}}),m}}throw new Error("ipaddr: string is not formatted like an IPv4 CIDR range")},h.IPv4.parser=function(d){let p,g,m;if(p=d.match(n.fourOctet))return function(){const y=p.slice(1,6),_=[];for(let w=0;w =0;y-=1)if(_=this.octets[y],_ in m){if(w=m[_],g&&w!==0)return null;w!==8&&(g=!0),p+=w}else return null;return 32-p},d.prototype.range=function(){return h.subnetMatch(this,this.SpecialRanges)},d.prototype.toByteArray=function(){return this.octets.slice(0)},d.prototype.toIPv4MappedAddress=function(){return h.IPv6.parse(`::ffff:${this.toString()}`)},d.prototype.toNormalizedString=function(){return this.toString()},d.prototype.toString=function(){return this.octets.join(".")},d}(),h.IPv4.broadcastAddressFromCIDR=function(d){try{const p=this.parseCIDR(d),g=p[0].toByteArray(),m=this.subnetMaskFromPrefixLength(p[1]).toByteArray(),y=[];let _=0;for(;_<4;)y.push(parseInt(g[_],10)|parseInt(m[_],10)^255),_++;return new this(y)}catch(p){throw new Error("ipaddr: the address does not have IPv4 CIDR format")}},h.IPv4.isIPv4=function(d){return this.parser(d)!==null},h.IPv4.isValid=function(d){try{return new this(this.parser(d)),!0}catch(p){return!1}},h.IPv4.isValidFourPartDecimal=function(d){return!!(h.IPv4.isValid(d)&&d.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/))},h.IPv4.networkAddressFromCIDR=function(d){let p,g,m,y,_;try{for(p=this.parseCIDR(d),m=p[0].toByteArray(),_=this.subnetMaskFromPrefixLength(p[1]).toByteArray(),y=[],g=0;g<4;)y.push(parseInt(m[g],10)&parseInt(_[g],10)),g++;return new this(y)}catch(w){throw new Error("ipaddr: the address does not have IPv4 CIDR format")}},h.IPv4.parse=function(d){const p=this.parser(d);if(p===null)throw new Error("ipaddr: string is not formatted like an IPv4 Address");return new this(p)},h.IPv4.parseCIDR=function(d){let p;if(p=d.match(/^(.+)\/(\d+)$/)){const g=parseInt(p[2]);if(g>=0&&g<=32){const m=[this.parse(p[1]),g];return Object.defineProperty(m,"toString",{value:function(){return this.join("/")}}),m}}throw new Error("ipaddr: string is not formatted like an IPv4 CIDR range")},h.IPv4.parser=function(d){let p,g,m;if(p=d.match(n.fourOctet))return function(){const y=p.slice(1,6),_=[];for(let w=0;w=0){var w=u?Kt(l,d,g,y,x):Kt(s,h,p,m,x);return u?[r,w]:[w,r]}}s=m,l=y;break}}},t}(kt),W5=function(e){J(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(Rx),Y5=function(e){J(t,e);function t(r){var n=e.call(this,r)||this;return n.type="ec-polygon",n}return t.prototype.getDefaultShape=function(){return new W5},t.prototype.buildPath=function(r,n){var i=n.points,a=n.stackedOnPoints,o=0,s=i.length/2,l=n.smoothMonotone;if(n.connectNulls){for(;s>0&&_i(i[s*2-2],i[s*2-1]);s--);for(;ot){a?r.push(o(a,l,t)):i&&r.push(o(i,l,0),o(i,l,t));break}else i&&(r.push(o(i,l,0)),i=null),r.push(l),a=l}return r}function Z5(e,t,r){var n=e.getVisual("visualMeta");if(!(!n||!n.length||!e.count())&&t.type==="cartesian2d"){for(var i,a,o=n.length-1;o>=0;o--){var s=e.getDimensionInfo(n[o].dimension);if(i=s&&s.coordDim,i==="x"||i==="y"){a=n[o];break}}if(!!a){var l=t.getAxis(i),u=ct(a.stops,function(_){return{coord:l.toGlobalCoord(l.dataToCoord(_.value)),color:_.color}}),f=u.length,c=a.outerColors.slice();f&&u[0].coord>u[f-1].coord&&(u.reverse(),c.reverse());var v=K5(u,i==="x"?r.getWidth():r.getHeight()),h=v.length;if(!h&&f)return u[0].coord<0?c[1]?c[1]:u[f-1].color:c[0]?c[0]:u[0].color;var d=10,p=v[0].coord-d,g=v[h-1].coord+d,m=g-p;if(m<.001)return"transparent";I(v,function(_){_.offset=(_.coord-p)/m}),v.push({offset:h?v[h-1].offset:.5,color:c[1]||"transparent"}),v.unshift({offset:h?v[0].offset:.5,color:c[0]||"transparent"});var y=new sb(0,0,0,0,v,!0);return y[i]=p,y[i+"2"]=g,y}}}function Q5(e,t,r){var n=e.get("showAllSymbol"),i=n==="auto";if(!(n&&!i)){var a=r.getAxesByScale("ordinal")[0];if(!!a&&!(i&&J5(a,t))){var o=t.mapDimension(a.dim),s={};return I(a.getViewLabels(),function(l){var u=a.scale.getRawOrdinalNumber(l.tickValue);s[u]=1}),function(l){return!s.hasOwnProperty(t.get(o,l))}}}}function J5(e,t){var r=e.getExtent(),n=Math.abs(r[1]-r[0])/e.scale.count();isNaN(n)&&(n=0);for(var i=t.count(),a=Math.max(1,Math.round(i/5)),o=0;on)return!1;return!0}function tz(e,t){return isNaN(e)||isNaN(t)}function ez(e){for(var t=e.length/2;t>0&&tz(e[t*2-2],e[t*2-1]);t--);return t-1}function q0(e,t){return[e[t*2],e[t*2+1]]}function rz(e,t,r){for(var n=e.length/2,i=r==="x"?0:1,a,o,s=0,l=-1,u=0;u