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)
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
// Plugin-tree maintenance for the embedded core (no Electron imports, so this
|
|
// is unit-testable with plain node).
|
|
//
|
|
// Two problems this solves, both invisible from the gateway side:
|
|
// 1. The core deliberately does NOT default `plugin_dir` (an operator opts
|
|
// in). A desktop profile without it runs with ZERO plugins — the channels
|
|
// plugin (account login, credential vault, auto check-in) never appears.
|
|
// 2. The core seeds bundled plugins only when the file is ABSENT, so a
|
|
// desktop upgrade would keep the user's old channels.lua forever and
|
|
// silently skip every channel fix. On version change the shell must drop
|
|
// the built-in files so the new binary reseeds them.
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const BUILTIN_PLUGINS = ["billing.lua", "channels.lua", "quota.lua", "usage.lua"];
|
|
|
|
// ensurePluginDirConfigured back-fills `plugin_dir` into an existing config.
|
|
// Appending a top-level YAML key is safe for the shapes this file can have
|
|
// (flat mapping + a keys list). Returns "added" | "present" | "absent" | "error".
|
|
function ensurePluginDirConfigured(configFile, pluginsDir, log) {
|
|
try {
|
|
if (!fs.existsSync(configFile)) return "absent";
|
|
const cur = fs.readFileSync(configFile, "utf-8");
|
|
if (/^\s*plugin_dir\s*:/m.test(cur)) return "present";
|
|
fs.writeFileSync(
|
|
configFile,
|
|
cur.replace(/\s*$/, "") +
|
|
"\n# added by ModelRouter Desktop: enables the plugin tree (channels etc.)\n" +
|
|
"plugin_dir: " + JSON.stringify(pluginsDir) + "\n",
|
|
"utf-8",
|
|
);
|
|
if (log) log("[gui] plugin_dir back-filled into " + configFile);
|
|
return "added";
|
|
} catch (e) {
|
|
if (log) log("[gui] ensurePluginDirConfigured failed: " + e.message);
|
|
return "error";
|
|
}
|
|
}
|
|
|
|
// syncBundledPluginsOnUpgrade drops built-in plugin files when the app version
|
|
// changed, so the core reseeds them from the binary it shipped with. Returns
|
|
// { changed, dropped:[...], kept: "prev-<ver>" }.
|
|
function syncBundledPluginsOnUpgrade(profileDir, pluginsDir, version, log) {
|
|
const stamp = path.join(profileDir, ".plugin-seed-version");
|
|
let have = "";
|
|
try {
|
|
have = fs.readFileSync(stamp, "utf-8").trim();
|
|
} catch (e) {
|
|
/* first run */
|
|
}
|
|
if (have === version) return { changed: false, dropped: [] };
|
|
const dropped = [];
|
|
for (const name of BUILTIN_PLUGINS) {
|
|
const f = path.join(pluginsDir, name);
|
|
if (!fs.existsSync(f)) continue;
|
|
try {
|
|
if (have) fs.renameSync(f, f + ".prev-" + have);
|
|
else fs.unlinkSync(f);
|
|
dropped.push(name);
|
|
} catch (e) {
|
|
if (log) log("[gui] drop builtin plugin " + name + " failed: " + e.message);
|
|
}
|
|
}
|
|
try {
|
|
fs.writeFileSync(stamp, version + "\n", "utf-8");
|
|
} catch (e) {
|
|
if (log) log("[gui] write plugin seed stamp failed: " + e.message);
|
|
}
|
|
if (dropped.length && log) {
|
|
log(
|
|
"[gui] plugins refreshed for " + version + " (" + dropped.length +
|
|
" builtin file(s) reseeded by core)",
|
|
);
|
|
}
|
|
return { changed: true, dropped, kept: have ? "prev-" + have : "" };
|
|
}
|
|
|
|
module.exports = {
|
|
BUILTIN_PLUGINS,
|
|
ensurePluginDirConfigured,
|
|
syncBundledPluginsOnUpgrade,
|
|
};
|