From 2924da00326491f5002182dd8992f0b6fbe34b9b Mon Sep 17 00:00:00 2001 From: root Date: Tue, 20 Oct 2026 08:44:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(gui):=20=E6=A1=8C=E9=9D=A2=E7=89=88?= =?UTF-8?q?=E7=9C=9F=E6=AD=A3=E5=8A=A0=E8=BD=BD=E6=B8=A0=E9=81=93=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=20+=20=E5=A4=96=E9=93=BE=E4=B8=80=E5=BE=8B=E4=BA=A4?= =?UTF-8?q?=E7=BB=99=E9=BB=98=E8=AE=A4=E6=B5=8F=E8=A7=88=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ① 桌面版此前**完全没有插件系统**(渠道页根本不存在) GUI 内嵌内核的 config.yaml 由 main.js 生成,唯独漏了 `plugin_dir` —— 而内核刻意不给它默认值(运维显式开启)。后果:桌面版 `plugin_dir: ""`, `/api/plugins` 返回空列表,channels 插件(多平台账号登录、凭据保管、自动 领取)在桌面端**完全不可见**。实测复现:同一份配置去掉 plugin_dir 就是 `plugins: []`,加上就有 billing/channels/quota/usage 四个。 修法: - 新 profile 直接写 `plugin_dir: /plugins`; - **老 profile 回填**(用户已有 config.yaml,不写就只有老用户永远没有插件)—— 新增 `ensurePluginDirConfigured()`,只在缺少该键时追加,其余内容不动; - 插件树由内核 SeedBundled 播种(按文件补齐,用户改过的副本优先保留)。 ## ② 升级后渠道修复**下发不到桌面用户** 内核播种规则是「文件缺失才写」,所以桌面用户升级 GUI 后 `channels.lua` 仍是旧版 —— 我们发的渠道修复对桌面用户静默失效。桌面版必须自己处理: 按 app 版本打戳(`.plugin-seed-version`),版本变化时删掉 4 个内置插件文件 让新内核重新播种,旧副本留 `.prev-<旧版本>` 可人工恢复;用户自建插件不碰。 逻辑抽到 `cmd/gui/plugin-sync.js`(无 Electron 依赖),配 6 条 node 单测。 ## ③ 登录一律拉起默认浏览器(三条路径都覆盖) 桌面版的内容区是加载内核 WebUI 的 iframe,渠道登录链接是 `target=_blank`。 此前只处理了 `setWindowOpenHandler`,另两条路径仍会把 Electron 窗口带到 第三方页面。现在三条路径统一外抛: 1. `window.open` / target=_blank → setWindowOpenHandler 2. 顶层同标签跳转 → will-navigate 3. iframe 内导航 → will-frame-navigate **loopback 例外必须保留**:channels 的服务端浏览器(noVNC)就在 `127.0.0.1:` 的 iframe 里,拦掉它会让 VNC 登录失效。 实测(打包后的 GUI + Xvfb + CDP 驱动真实窗口): [gui] open in default browser: https://example.com/mr-windowopen-probe [gui] open in default browser: https://example.com/mr-navigate-probe [gui] open in default browser: https://example.com/mr-frame-probe [gui] open in default browser: https://xiaohuanxiong.com/login?code=…(真实商汤登录链接) 顶层 URL 保持在内核页面(跳转被拦下),loopback iframe 正常加载。 ## ④ 顺手 - GUI 版本号 1.9.7 → 2.0.42(与内核同版本发版;package-lock 里还是 1.9.3, 一并同步); - `make gui-test` 跑桌面壳单测。 验证:打包后的 GUI 启动即 `/api/plugins` 列出 channels(ui.page=channels), 插件目录播种 4 个内置插件;外链四类实测均走默认浏览器。 (cherry picked from commit 411503aea5eb3b78360c1929a45cf97daa2ce833) --- Makefile | 10 ++++- cmd/gui/main.js | 61 ++++++++++++++++++++++++++- cmd/gui/package-lock.json | 4 +- cmd/gui/package.json | 5 ++- cmd/gui/plugin-sync.js | 82 +++++++++++++++++++++++++++++++++++++ cmd/gui/plugin-sync.test.js | 80 ++++++++++++++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 cmd/gui/plugin-sync.js create mode 100644 cmd/gui/plugin-sync.test.js diff --git a/Makefile b/Makefile index ca69504..9940bed 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build test gui gui-dev gui-dist gui-deb gui-win gui-win-docker help clean +.PHONY: all build test gui gui-dev gui-dist gui-deb gui-win gui-win-docker help clean gui-test # ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core) export BINARY @@ -65,6 +65,13 @@ gui-dist-dir: test: go test ./... +# Desktop shell unit tests (no Electron needed): plugin-tree seeding/migration. +# These guard the two things that silently break the desktop build: +# - plugin_dir missing from an embedded profile -> channels plugin invisible +# - bundled plugin files never refreshed -> old channels.lua survives upgrades +gui-test: + @cd $(GUI_DIR) && node --test plugin-sync.test.js + clean: rm -rf $(BUILD_DIR) cmd/build cmd/gui/dist cmd/gui/build/gui-dist cmd/gui/bin @@ -75,6 +82,7 @@ help: @echo " make gui run desktop GUI (dev)" @echo " make gui-dev run GUI with devtools" @echo " make gui-dist build deb + rpm + AppImage (linux)" + @echo " make gui-test desktop shell unit tests (plugin tree seeding)" @echo " make gui-deb build deb only" @echo " make gui-win build Windows nsis (needs host mingw + wine)" @echo " make gui-win-docker build Windows nsis all-in-docker (no host mingw needed)" diff --git a/cmd/gui/main.js b/cmd/gui/main.js index abc6ea9..94dc370 100644 --- a/cmd/gui/main.js +++ b/cmd/gui/main.js @@ -18,6 +18,7 @@ const http = require("http"); const os = require("os"); const crypto = require("crypto"); const { spawn, execFileSync } = require("child_process"); +const pluginSync = require("./plugin-sync"); const LOG_FILE = path.join(app.getPath("userData"), "gui.log"); function log(msg) { @@ -40,6 +41,13 @@ console.error = function () { // Per-user profile dir: /profile const PROFILE_DIR = path.join(app.getPath("userData"), "profile"); const CONFIG_FILE = path.join(PROFILE_DIR, "config.yaml"); +// Plugin tree for the embedded core. The core deliberately does NOT default +// plugin_dir (an operator opts in), so the desktop build must set it — without +// it the whole plugin system stays off and the channels plugin (multi-platform +// account login + credential vault + auto check-in) is invisible in the WebUI. +// Seeding is the core's job (SeedBundled fills MISSING files only), so a user's +// edited copy always wins. +const PLUGINS_DIR = path.join(PROFILE_DIR, "plugins"); // Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable). // Dev: cmd/gui/bin, populated by prepare-core.js. const CORE_EXE = path.join( @@ -216,6 +224,9 @@ function writeDefaultEmbeddedConfig() { "listen: 127.0.0.1:" + (settings.port || DEFAULT_PORT), "default_model: AUTO", "adapter_dir: " + JSON.stringify(path.join(PROFILE_DIR, "adapters")), + // Plugin tree: channels/billing/quota/usage ship inside the core binary and + // are seeded into this dir on first start. Omitting it disables plugins. + "plugin_dir: " + JSON.stringify(PLUGINS_DIR), "runtime_file: " + JSON.stringify(path.join(PROFILE_DIR, "runtime.json")), "", // Admin key as a plain `keys` entry (no seed flag) so the web UI never @@ -268,6 +279,9 @@ function startCore() { // ensure profile dir + config + local adapters exist fs.mkdirSync(PROFILE_DIR, { recursive: true }); if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig(); + pluginSync.ensurePluginDirConfigured(CONFIG_FILE, PLUGINS_DIR, log); + fs.mkdirSync(PLUGINS_DIR, { recursive: true }); + pluginSync.syncBundledPluginsOnUpgrade(PROFILE_DIR, PLUGINS_DIR, app.getVersion(), log); coreProc = spawn(CORE_EXE, ["-config", CONFIG_FILE], { cwd: PROFILE_DIR, @@ -622,10 +636,55 @@ function createWindow() { }, }); mainWindow.loadFile(path.join(__dirname, "renderer", "index.html")); + // External links ALWAYS go to the user's default browser, never an in-app + // window. Three paths have to be covered, and missing any one of them is why + // "click the login link and nothing happens / the app navigates away" shows up: + // 1. window.open / target=_blank -> setWindowOpenHandler + // 2. top-level navigation (same-tab click) -> will-navigate + // 3. navigation inside the WebUI