mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## ① 桌面版此前**完全没有插件系统**(渠道页根本不存在)
GUI 内嵌内核的 config.yaml 由 main.js 生成,唯独漏了 `plugin_dir` ——
而内核刻意不给它默认值(运维显式开启)。后果:桌面版 `plugin_dir: ""`,
`/api/plugins` 返回空列表,channels 插件(多平台账号登录、凭据保管、自动
领取)在桌面端**完全不可见**。实测复现:同一份配置去掉 plugin_dir 就是
`plugins: []`,加上就有 billing/channels/quota/usage 四个。
修法:
- 新 profile 直接写 `plugin_dir: <profile>/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:<port>` 的 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 411503aea5)
81 lines
3.6 KiB
JavaScript
81 lines
3.6 KiB
JavaScript
// Unit tests for the desktop plugin-tree maintenance (plain node, no Electron).
|
|
// Run: node --test cmd/gui/plugin-sync.test.js
|
|
const test = require("node:test");
|
|
const assert = require("node:assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { BUILTIN_PLUGINS, ensurePluginDirConfigured, syncBundledPluginsOnUpgrade } = require("./plugin-sync");
|
|
|
|
const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), "plugin-sync-"));
|
|
|
|
test("back-fills plugin_dir into an existing profile without it", () => {
|
|
const dir = tmp();
|
|
const cfg = path.join(dir, "config.yaml");
|
|
fs.writeFileSync(cfg, 'listen: 127.0.0.1:8787\ndefault_model: AUTO\nkeys:\n - key: k\n role: admin\n');
|
|
const plugins = path.join(dir, "plugins");
|
|
assert.equal(ensurePluginDirConfigured(cfg, plugins, null), "added");
|
|
const out = fs.readFileSync(cfg, "utf-8");
|
|
assert.match(out, /^plugin_dir: "/m);
|
|
assert.ok(out.includes(plugins.replace(/\\/g, "\\\\")) || out.includes(plugins), "path written");
|
|
// the rest of the profile must survive untouched
|
|
assert.match(out, /^listen: 127\.0\.0\.1:8787$/m);
|
|
assert.match(out, /^ - key: k$/m);
|
|
});
|
|
|
|
test("leaves a profile that already sets plugin_dir alone", () => {
|
|
const dir = tmp();
|
|
const cfg = path.join(dir, "config.yaml");
|
|
const orig = 'plugin_dir: "/custom/plugins"\nlisten: ":1"\n';
|
|
fs.writeFileSync(cfg, orig);
|
|
assert.equal(ensurePluginDirConfigured(cfg, "/whatever", null), "present");
|
|
assert.equal(fs.readFileSync(cfg, "utf-8"), orig);
|
|
});
|
|
|
|
test("missing config is not created by the back-fill", () => {
|
|
const dir = tmp();
|
|
const cfg = path.join(dir, "config.yaml");
|
|
assert.equal(ensurePluginDirConfigured(cfg, path.join(dir, "p"), null), "absent");
|
|
assert.equal(fs.existsSync(cfg), false);
|
|
});
|
|
|
|
test("upgrade drops builtin plugins (keeping a backup) but not user plugins", () => {
|
|
const dir = tmp();
|
|
const plugins = path.join(dir, "plugins");
|
|
fs.mkdirSync(plugins, { recursive: true });
|
|
for (const f of BUILTIN_PLUGINS) fs.writeFileSync(path.join(plugins, f), "OLD");
|
|
fs.writeFileSync(path.join(plugins, "my-own.lua"), "MINE");
|
|
fs.writeFileSync(path.join(dir, ".plugin-seed-version"), "1.0.0\n");
|
|
|
|
const r = syncBundledPluginsOnUpgrade(dir, plugins, "2.0.42", null);
|
|
assert.equal(r.changed, true);
|
|
assert.deepEqual(r.dropped.sort(), [...BUILTIN_PLUGINS].sort());
|
|
for (const f of BUILTIN_PLUGINS) {
|
|
assert.equal(fs.existsSync(path.join(plugins, f)), false, f + " dropped");
|
|
assert.equal(fs.readFileSync(path.join(plugins, f + ".prev-1.0.0"), "utf-8"), "OLD");
|
|
}
|
|
assert.equal(fs.readFileSync(path.join(plugins, "my-own.lua"), "utf-8"), "MINE");
|
|
assert.equal(fs.readFileSync(path.join(dir, ".plugin-seed-version"), "utf-8").trim(), "2.0.42");
|
|
});
|
|
|
|
test("same version is a no-op (nothing dropped)", () => {
|
|
const dir = tmp();
|
|
const plugins = path.join(dir, "plugins");
|
|
fs.mkdirSync(plugins, { recursive: true });
|
|
fs.writeFileSync(path.join(plugins, "channels.lua"), "CURRENT");
|
|
fs.writeFileSync(path.join(dir, ".plugin-seed-version"), "2.0.42\n");
|
|
const r = syncBundledPluginsOnUpgrade(dir, plugins, "2.0.42", null);
|
|
assert.equal(r.changed, false);
|
|
assert.equal(fs.readFileSync(path.join(plugins, "channels.lua"), "utf-8"), "CURRENT");
|
|
});
|
|
|
|
test("first run on a fresh profile just stamps the version", () => {
|
|
const dir = tmp();
|
|
const plugins = path.join(dir, "plugins");
|
|
fs.mkdirSync(plugins, { recursive: true });
|
|
const r = syncBundledPluginsOnUpgrade(dir, plugins, "2.0.42", null);
|
|
assert.equal(r.changed, true);
|
|
assert.deepEqual(r.dropped, []);
|
|
assert.equal(fs.existsSync(path.join(dir, ".plugin-seed-version")), true);
|
|
});
|