From bca791a14b3137f816546a252e08827ccdbc19fa Mon Sep 17 00:00:00 2001 From: JianFeeeee <109188060+JianFeeeee@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:05:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(gui):=20=E8=AE=BE=E5=A4=87=E6=A1=A5=20gatew?= =?UTF-8?q?ay=20=E8=87=AA=E5=8A=A8=E5=8F=91=E7=8E=B0=E5=9B=9E=E5=86=99?= =?UTF-8?q?=E4=B8=BB=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 要判的是什么 2026-10-05 实测故障:GUI 面板里能看到设备通道地址,但设备桥连不上。 日志只有一行 `ws cookie len=0` + 401,没有任何「地址来自发现」的痕迹。 根因不是配置错,是**发现结果与实际连接配置从不联通**: - 渲染进程 app.js 的 loadDiscoveredGateway() 调 /device/gateway, 拿到的地址只写进 **state.discoveredGateway**(UI 展示用); - 主进程 startDeviceBridge() 读的是 **gui-prefs.deviceBridge.gateway**; - 两边从未交换过 —— state 是渲染进程内存,prefs 是主进程文件。 于是服务端明明已经通过 /device/gateway 告诉了客户端正确地址 (proxy.go:1078 生成的 url_portal),GUI 却仍在用用户几个月前手填的旧地址, 且永远 401/ECONNREFUSED、不重试。 ## 修复 发现成功后回写 deviceBridge.set({gateway}),让主进程真正用上。 两个刻意的约束: 1. **仅当本地 gateway 为空才写**(!cur.gateway)—— 手填值优先。 用户可能故意指向内网直连或灰度实例,发现只做缺省补全。 2. **只传 gateway,不传 token** —— /device/gateway 不返回设备令牌 (ws_token),写了反而会写坏已有的正确 token。 deviceBridge.set 会 stop+start 桥,改完立即生效,无需重启进程。 ## 顺带记下一个实测坑(已写进门禁) 设备桥 WS 的认证**不走 cookie**。registry.go ServeWS 只认三种来源: ?token= / X-API-Key / Sec-WebSocket-Protocol: homeagent., cookie 完全不参与 acceptBind。 排查时曾误以为「发 cookie 就能过」—— 实测服务端仍 401。 cookie 那条路是错的,已还原 connections.json。 ## 门禁(cmd/gui/device-gateway-discovery.test.mjs) 9 条判据,文本扫描(自动回写由 refreshAll 的 15s 定时器驱动, 构造那个时序需真 Electron + 真后端 + 会话 cookie,CI 里没有)。 覆盖:发现调用存在、回写接通、本地非空则不覆盖、只传 gateway、 preload 暴露 set、主进程注册 handler、get 返回明文 token、 set 会重启桥、connectDeviceWS 把 token 放进 query。 变异测试验证非假红: - 删掉回写块(复现本次故障形态)⇒ 第 2、3、4 条转红; - 去掉「本地为空才写」改成无条件覆盖 ⇒ 第 3 条转红。 排障过程中判据自身报过 3 次假红(正则窗口不足、字符串切片后仍按 match 数组用 setHandler[0]),均已修正并记在注释里。 --- cmd/gui/device-gateway-discovery.test.mjs | 169 ++++++++++++++++++++++ cmd/gui/renderer/app.js | 23 +++ 2 files changed, 192 insertions(+) create mode 100644 cmd/gui/device-gateway-discovery.test.mjs diff --git a/cmd/gui/device-gateway-discovery.test.mjs b/cmd/gui/device-gateway-discovery.test.mjs new file mode 100644 index 00000000..4240dba5 --- /dev/null +++ b/cmd/gui/device-gateway-discovery.test.mjs @@ -0,0 +1,169 @@ +// 设备桥 gateway 自动发现链路的门禁。 +// +// ## 要判的是什么 +// +// 2026-10-05 实测故障:GUI 面板里能看到设备通道地址,但设备桥连不上。 +// 日志只有一行 `ws cookie len=0` + 401,没有任何「地址来自发现」的痕迹。 +// +// 根因不是配置错,是**发现结果与实际连接配置从不联通**: +// +// · 渲染进程 app.js 的 loadDiscoveredGateway() 调 /device/gateway, +// 拿到的地址只写进 **state.discoveredGateway**(UI 展示用); +// · 主进程 startDeviceBridge() 读的是 **gui-prefs.deviceBridge.gateway**; +// · 两边从未交换过 —— state 是渲染进程内存,prefs 是主进程文件。 +// +// 于是服务端明明已经通过 /device/gateway 告诉了客户端正确地址 +// (proxy.go:1078 生成的 url_portal),GUI 却仍在用用户几个月前 +// 手填的旧地址,且**永远 401/ECONNREFUSED,且不重试**。 +// +// ## 判据形态:文本扫描而非运行时 +// +// 与 endpoint-align / device-align 同路子。自动回写的触发点在 +// loadDiscoveredGateway() 内,由 refreshAll 的 15s 定时器驱动 —— +// 构造那个时序需要真Electron + 真后端,而 /device/gateway 还要求 +// 会话 cookie(CI 里没有)。故从源码文本判「链路是否接通」。 +// +// 运行:node device-gateway-discovery.test.mjs + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const APP = join(here, "renderer", "app.js"); +const MAIN = join(here, "main.js"); +const PRELOAD = join(here, "preload.js"); + +let failures = 0; +const check = (name, ok, detail) => { + if (ok) { + console.log(` ✓ ${name}`); + } else { + failures++; + console.log(` ✗ ${name}${detail ? " — " + detail : ""}`); + } +}; + +const appSrc = readFileSync(APP, "utf8"); +const mainSrc = readFileSync(MAIN, "utf8"); +const preloadSrc = readFileSync(PRELOAD, "utf8"); + +// ── 1) 发现函数存在且会调服务端权威端点 ──────────────────────────── + +check( + "存在 /device/gateway 发现调用", + appSrc.includes("/device/gateway"), + "找不到 —— 服务端发现端点没被调用", +); + +// ── 2) 发现结果必须回写到主进程(本判据的核心)────────────────────── +// +// 这是本次修复的要点。若这段回写被删掉(重构时不慎),设备桥会退回 +// 「只用用户手填地址」的行为,且没有任何报错—— +// 这正是 2026-10-05 那次故障的形态。 + +check( + "发现结果回写到主进程 deviceBridge.set", + /discoveredGateway[\s\S]{0,600}?deviceBridge\s*\.\s*set\s*\(/.test(appSrc), + "loadDiscoveredGateway 里没有 deviceBridge.set 回写 —— " + + "state.discoveredGateway 只用于 UI,主进程仍在用旧的手填地址", +); + +// ── 3) 回写不得覆盖用户手填值 ────────────────────────────────────── +// +// 发现只做**缺省补全**。若用户在设置页手填了内网直连地址(灰度实例、 +// 绕过反代),自动回写把它冲掉就是新的回归。判据钉住「本地为空才写」。 + +// 注意不能用 /set\(\s*\{[\s\S]{0,300}?\}/ —— 调用点前后的 if 判断在花括号 +// **外面**,而 300 字窗口会跨到下一个 deviceBridge.set(saveBridgeChannel +// 的手动保存),判据就误报了。改为直接取「发现函数体内」的那次调用。 +const discoFn = appSrc.match( + /async function loadDiscoveredGateway\(\)[\s\S]*?\n}/, +); +const setCall = discoFn + ? discoFn[0].match(/window\.homeagent\.deviceBridge\.set\([^)]*\)/) + : null; +check( + "回写前检查本地 gateway 为空", + discoFn ? /cur\s*&&\s*!cur\.gateway/.test(discoFn[0]) : false, + discoFn + ? "回写处没看到「本地已配置则跳过」的判断 —— 会覆盖用户手填值" + : "找不到 loadDiscoveredGateway", +); +check( + "回写只传 gateway,不覆盖 token", + setCall ? !/token/.test(setCall[0]) : false, + setCall ? "回写里带了 token —— 发现端点不返回设备令牌,会写坏" : "找不到回写调用", +); + +// ── 4) IPC 链路完整:preload 暴露 get/set,主进程处理 ────────────── +// +// 回写用的是 window.homeagent.deviceBridge.set(...);contextBridge +// 若没暴露或主进程没注册,这次调用是 undefined TypeError(被 catch +// 吞掉,于是又回到「静默不生效」)。 + +check( + "preload 暴露 deviceBridge.set", + /deviceBridge[\s\S]{0,120}?set\s*:/.test(preloadSrc), + "contextBridge 未暴露 set —— 渲染进程调不到", +); +check( + "主进程注册 device-bridge:set", + mainSrc.includes('ipcMain.handle("device-bridge:set"'), + "主进程没有该 handler", +); +check( + "主进程 device-bridge:get 返回明文 token", + /device-bridge:get[\s\S]{0,900}?token:\s*db\.token/.test(mainSrc), + "get 未返回 token 明文 —— 渲染侧拿不到设备令牌", +); + +// ── 5) device-bridge:set 必须真正重启桥 ─────────────────────────── +// +// 只存 prefs 不重启,配置不会立即生效(要等下次进程启动)。 +// 这是同一类「存了但没生效」的静默失效。 + +// 不用大正则匹配整个 handler —— 返回对象里的嵌套 } 会让边界判断很脆。 +// 改为取两个 handler 之间的**源码切片**,再在里面判 stop/start 是否存在。 +const setStart = mainSrc.indexOf('ipcMain.handle("device-bridge:set"'); +const setEnd = mainSrc.indexOf('ipcMain.handle("prefs:get"'); +const setHandler = + setStart >= 0 && setEnd > setStart + ? mainSrc.slice(setStart, setEnd) + : null; +check( + "device-bridge:set 会重启设备桥", + setHandler + ? setHandler.includes("stopDeviceBridge") && + setHandler.includes("startDeviceBridge") + : false, + setHandler + ? "handler 里没有 stop+start —— 新配置要等重启进程才生效" + : "找不到 device-bridge:set handler", +); + +// ── 6) 主进程连接必须带 token ───────────────────────────────────── +// +// 设备桥 WS 走 ?token=(registry.go ServeWS 只认 query/X-API-Key/ +// Sec-WebSocket-Protocol 三种,**cookie 不参与** acceptBind)。 +// 这是实测踩过的坑:曾误以为发 cookie 就能过,实际服务端仍 401。 + +const connectFn = mainSrc.match( + /function connectDeviceWS[\s\S]*?\n}\n/, +); +check( + "connectDeviceWS 把 token 放进 query", + connectFn ? /token=/.test(connectFn[0]) : false, + connectFn + ? "query 里没带 token —— 服务端 acceptBind 会拒" + : "找不到 connectDeviceWS", +); + +// ── 汇总 ──────────────────────────────────────────────────────────── + +console.log(""); +if (failures > 0) { + console.log(`失败 ${failures} 条`); + process.exit(1); +} +console.log("全部通过"); \ No newline at end of file diff --git a/cmd/gui/renderer/app.js b/cmd/gui/renderer/app.js index bf97d7b1..c56a298a 100644 --- a/cmd/gui/renderer/app.js +++ b/cmd/gui/renderer/app.js @@ -761,6 +761,29 @@ async function loadDiscoveredGateway() { (d && d.available && (d.url_portal || d.url)) || ""; if (state.discoveredGateway) { console.log("[device-bridge] discovered gateway: " + state.discoveredGateway); + // ★ 回写到主进程,让设备桥**真的**用上发现结果。 + // + // 此前发现只停在state.discoveredGateway(UI 展示用),而主进程的 + // startDeviceBridge 读的是 gui-prefs.deviceBridge.gateway —— + // 两者从不联通。症状:GUI 面板里能看到设备通道地址,但设备桥连不上, + // 而日志只有一行 ws cookie len=0,没有任何“地址来自发现”的痕迹。 + // + // 仅当本地未配置 gateway 时才写:手填的值优先(用户可能故意指向 + // 内网直连或灰度实例),发现只做**缺省补全**。 + if (window.homeagent && window.homeagent.deviceBridge) { + try { + var cur = await window.homeagent.deviceBridge.get(); + if (cur && !cur.gateway && state.discoveredGateway) { + await window.homeagent.deviceBridge.set({ gateway: state.discoveredGateway }); + console.log( + "[device-bridge] gateway auto-configured from discovery: " + + state.discoveredGateway, + ); + } + } catch (e2) { + console.log("[device-bridge] auto-config skipped: " + e2.message); + } + } } } catch (e) { state.discoveredGateway = "";