From 00045451903d304483e26f7f23757c70414fd1a1 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 13 Oct 2026 10:17:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(channels):=20Loomy=20=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=20appId/uuid=20=E6=AD=A3=E5=88=99=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20=E2=80=94=E2=80=94=20=E4=B9=8B=E5=89=8D=E6=98=AF?= =?UTF-8?q?=E6=88=91=E7=BC=96=E7=9A=84=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 用真上游实测逼出来的两处错误 批准出站主机后逐个平台打真上游,Loomy 报「授权页未包含二维码 uuid」。 核对参考常量后发现: 1. **appId 是我编的**:我写 `wx16e5c2b8e0c0e1f2`,参考是 `wx18d60be432287cf8`。上游不认这个 appid,返回的页面里自然没有二维码。 同时补上微信要求的固定片段 `#wechat_redirect`。 2. **uuid 正则不是参考的那条**:我写的是 `uuid="..."` 形式,而真实微信页把 uuid 内嵌在 ``, 兜底路径是 `l/qrconnect?uuid=`。照参考 extractLoomyWechatUuid 的两条 正则改写(它还带一个 `^[A-Za-z0-9_\-=+/]{6,64}$` 的字符集校验)。 3. UA 逐字统一为参考的 WECHAT_UA(`Chrome/120.0.0.0`)。 ## 教训(值得记) 「照参考逐行移植」这一条,我在这两处做成了「凭印象写个看起来像的」。 **判据是端到端打真上游**,不是测试通过 —— 单测用的是我自己编的夹具, 夹具与生产同源出错时测试照样绿。这次是真上游把它逼出来的。 ## 判据 - 测试夹具改为**真实微信页面形态**(``): 用别的形态(如 `var uuid = "..."`)测出来的正则不是生产要用的那条。 - 生产实测:Loomy 从真微信拿到二维码 uuid(`open.weixin.qq.com/connect/qrcode/`)。 - 四个新平台真上游全部 pending 且返回有效授权入口: qoder→qoder.com(真实 PKCE challenge)、zcode→bigmodel.cn、 loomy→open.weixin.qq.com(真实 uuid)、raccoon→xiaohuanxiong.com(本地 code) - 全量 core 回归通过;内核对照 v2.0.2 diff 为空。 ## 出站主机授权 channels 的 27 个声明主机已全部在设置页批准(8 个新增 + 陈旧条目自动清理), 落盘到 .kernel-perms.json,重启后保留。 --- internal/core/channels_loomy_test.go | 7 +++++-- internal/lua/plugins/channels.lua | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/core/channels_loomy_test.go b/internal/core/channels_loomy_test.go index d743b9a..f218e06 100644 --- a/internal/core/channels_loomy_test.go +++ b/internal/core/channels_loomy_test.go @@ -33,7 +33,10 @@ func newLoomyHarness(t *testing.T) *loomyHarness { switch { case strings.HasPrefix(r.URL.Path, "/connect/qrconnect"): w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, ``) + // 夹具必须用**真实微信页面的形态**:uuid 内嵌在 + // 。 + // 用别的形态(如 var uuid = "...")测出来的正则不是生产要用的那条。 + fmt.Fprint(w, ``) case strings.HasPrefix(r.URL.Path, "/connect/l/qrconnect"): n := atomic.AddInt32(&h.polls, 1) w.Header().Set("Content-Type", "text/plain") @@ -159,7 +162,7 @@ func TestLoomyWechatPollBehavior(t *testing.T) { hh := newLoomyHarness(t) hh.ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/connect/qrconnect") { - fmt.Fprint(w, ``) + fmt.Fprint(w, ``) return } fmt.Fprintf(w, "window.wx_errcode=%s;", tc.errcode) diff --git a/internal/lua/plugins/channels.lua b/internal/lua/plugins/channels.lua index 15bd1e9..55a47ed 100644 --- a/internal/lua/plugins/channels.lua +++ b/internal/lua/plugins/channels.lua @@ -575,13 +575,17 @@ local PLATFORMS = { -- 403=用户取消;402=二维码失效 flow = "wechat-poll", wechat = { + -- appid / redirect_uri 照参考常量(原写的 appid 是我编的,上游不认); + -- 末尾 `#wechat_redirect` 是微信要求的固定片段,缺它对某些客户端会跳错页。 auth_url = "https://open.weixin.qq.com/connect/qrconnect" - .. "?appid=wx16e5c2b8e0c0e1f2&scope=snsapi_login&response_type=code" - .. "&redirect_uri=https%3A%2F%2Floomy.xunfei.cn%2Foauth%2Fwechat%2Fcallback&state={state}", + .. "?appid=wx18d60be432287cf8&scope=snsapi_login&response_type=code" + .. "&redirect_uri=https%3A%2F%2Floomy.xunfei.cn%2Foauth%2Fwechat%2Fcallback" + .. "&state={state}#wechat_redirect", qr_url = "https://open.weixin.qq.com/connect/qrcode/{uuid}", poll_url = "https://long.open.weixin.qq.com/connect/l/qrconnect", + -- 逐字照参考 WECHAT_UA(微信对空 UA / 爬虫 UA 可能拒绝) user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - .. " (KHTML, like Gecko) Chrome/120.0 Safari/537.36", + .. " (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", account_base = "https://account.xfyun.cn", bind_path = "/login/thirdAccount/bind/auth", access_key_id = "2thryby66wxi53sk", @@ -2157,10 +2161,12 @@ function plugin.start_login(req) return { status = 200, json = out } end local html = resp.body or "" - -- 主正则 + 兜底(含 uuid 的查询串) - local uuid = html:match('uuid%s*=%s*"([^"]+)"') - or html:match("uuid%s*=%s*'([^']+)'") - or html:match("[?&]uuid=([%w%-_]+)") + -- 两条路径互为兜底(照参考 extractLoomyWechatUuid 的正则): + -- 主: + -- 兜底:var fordevtool = "…/connect/l/qrconnect?uuid=" + -- ⚠️ 无需执行 JS —— 这是本方案能脱离浏览器的基础。 + local uuid = html:match("/connect/qrcode/([%w_%-=+/]+)") + or html:match("l/qrconnect%?uuid=([%w_%-=+/]+)") if not uuid or uuid == "" then sess.state = "failed" out.error = "微信授权页未包含二维码 uuid(页面结构可能已变化)"