mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-05 07:02:29 +00:00
## WebUI:新增「插件」页
- 列表来自 on_disk(不是 loaded 集合)——**加载失败的插件也必须显示并带错误**,
否则一个语法错误看起来和"插件没装"完全一样
- 启用/禁用(PUT {"enabled":bool})、删除、编辑源码、安装/覆盖
- 显示 hook_errors:插件抛异常在别处毫无痕迹,没有这一栏的症状就是
"功能就是不work"
- 插到 dropzone 与代码编辑器都做了泛型化(bindDropzone / openCodeModal),
适配器与插件共用一份,而不是复制第二份只改 4 个 id 的函数
## TABS 收敛为单一常量
tab 清单原本是字面量散在三处:goTab、refresh()、admin-only 隐藏列表。
加一个 tab 意味着三处都要记得改,漏一处就是"路由认得但界面不显示"——
和今天早些时候 chain_step 漏报同一类静默缺口。现在只有 const TABS。
## Electron 壳:设置面板里的插件管理
渲染进程不能直连内嵌核心(没有 key、不知道端口),所以走 IPC:
renderer → plugins:proxy → main → HTTP /api/plugins
代理是 (method, path, body) 透传而不是固定命令表:固定表每加一个端点就要扩,
而"按钮存在但什么都不做"比"没有这个按钮"更糟。透传让渲染层能调用核心将来
新增的任何 /api/plugins 路由,路径在主进程校验。
## ★ GUI 此前零测试,而本次改动就引入了三类"看起来没事"的问题
1. 引用了不存在的 CSS 类(.tag / .sm)——渲染成无样式文本
2. 引用了不存在的 helper(esc / escAttr)——那是 WebUI 的,renderer/app.js
是独立文档,点击时 ReferenceError
3. .ghost/.primary 只在 .form .actions 作用域内生效,插件按钮在 .pl-acts 里
于是是无样式裸按钮
补 4 个静态判据(不启动 Electron,守卫的正是"打开应用才看得见"那一类):
TestGUICSSClassesExist 用到的类必须在样式表里定义
TestGUIHelperFunctionsAreDefined 被调用的函数必须有定义
TestGUIPluginPanelIsReachable 面板在 overlay 内、按钮已绑定、打开设置会加载
TestGUIIPCPathIsConstrained 代理必须限定 /api/plugins 前缀并拒绝路径穿越
写第一个判据时我错了三次:CSS 解析器先丢最后一个 selector、再把变量块当
selector、最后漏掉复合选择器(.tb-btn.tb-close)。两次"判据自己坏了"的
教训和本项目一贯一致——**判据出错的信号是它报了一个假问题**。现在改用宽松的
token 提取 + 显式的 guiKnownUnstyled 豁免表(blob/tgl/rail 是既有无样式类,
不是本次引入,失败它们只会让判据对新工作失去意义)。
## 变异验证
改坏唯一的 CSS 定义(.pl-empty)→ TestGUICSSClassesExist 红
改坏 helper 名 → TestGUIHelperFunctionsAreDefined 红
★ 第一次变异我改了 .pl-broken,判据**正确地没报**——因为它还被另一条规则定义。
这是变异选错目标,不是判据有洞;换 .pl-empty 后如期变红。
363 个测试全绿。
235 lines
8.9 KiB
HTML
235 lines
8.9 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta
|
|
http-equiv="Content-Security-Policy"
|
|
content="default-src 'self'; connect-src * data: blob:; img-src * data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-src *;"
|
|
/>
|
|
<title>ModelRouter</title>
|
|
<link rel="stylesheet" href="style.css" />
|
|
</head>
|
|
<body>
|
|
<!-- sakura/frost glassmorphism background -->
|
|
<div id="bgfx" aria-hidden="true">
|
|
<i class="blob b1"></i><i class="blob b2"></i><i class="blob b3"></i>
|
|
</div>
|
|
|
|
<!-- frameless draggable titlebar -->
|
|
<div class="titlebar">
|
|
<div class="titlebar-brand">
|
|
<span class="tb-logo">M</span>
|
|
<span class="tb-title">ModelRouter</span>
|
|
<span class="status-dot" id="conn-dot" title="内核状态"></span>
|
|
</div>
|
|
<div class="titlebar-right">
|
|
<button class="tb-btn" id="tb-settings" title="设置">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<circle cx="12" cy="12" r="3" />
|
|
<path
|
|
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button class="tb-btn" id="tb-min" title="最小化">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="13"
|
|
height="13"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.6"
|
|
stroke-linecap="round"
|
|
>
|
|
<line x1="4" y1="12" x2="20" y2="12" />
|
|
</svg>
|
|
</button>
|
|
<button class="tb-btn" id="tb-max" title="最大化">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="12"
|
|
height="12"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.6"
|
|
stroke-linecap="round"
|
|
>
|
|
<rect x="5" y="5" width="14" height="14" rx="1.5" />
|
|
</svg>
|
|
</button>
|
|
<button class="tb-btn tb-close" id="tb-close" title="关闭">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.6"
|
|
stroke-linecap="round"
|
|
>
|
|
<path d="M6 6l12 12M18 6L6 18" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- side rail: autostart / silent toggles (Clash-Verge style) -->
|
|
<aside class="rail" id="rail">
|
|
<div class="rail-gap"></div>
|
|
<button
|
|
class="rail-btn tgl"
|
|
id="rail-autostart"
|
|
title="开机自启(点击切换)"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" />
|
|
</svg>
|
|
<span class="rail-dot" id="rail-autostart-dot"></span>
|
|
</button>
|
|
<button
|
|
class="rail-btn tgl"
|
|
id="rail-silent"
|
|
title="静默启动(启动不弹窗仅驻托盘,点击切换)"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M12 3v10l4 4" />
|
|
<path d="M8 21h8" />
|
|
<path d="M12 8a8 8 0 1 0 8 8" />
|
|
</svg>
|
|
<span class="rail-dot" id="rail-silent-dot"></span>
|
|
</button>
|
|
<div class="rail-gap"></div>
|
|
<button class="rail-btn" id="rail-settings" title="设置">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle cx="12" cy="12" r="3" />
|
|
<path
|
|
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button class="rail-btn tgl" id="rail-theme" title="切换亮色/暗色">
|
|
<span class="theme-ic"></span>
|
|
</button>
|
|
</aside>
|
|
|
|
<!-- main content: full-screen embedded WebUI (1:1 clone of the web UI) -->
|
|
<div id="app">
|
|
<div class="app-main">
|
|
<iframe
|
|
id="webui-frame"
|
|
class="webui-frame"
|
|
title="ModelRouter Web UI"
|
|
></iframe>
|
|
<div id="offline" class="offline" style="display: none">
|
|
<div class="off-card">
|
|
<div class="off-logo">M</div>
|
|
<div class="off-title">内核未运行</div>
|
|
<div class="off-sub">
|
|
内嵌 ModelRouter 网关尚未启动,启动后即可使用完整 WebUI
|
|
</div>
|
|
<button class="primary" id="off-start">启动内核</button>
|
|
</div>
|
|
</div>
|
|
<div id="console-loading" class="console-loading" style="display: none">
|
|
<div class="spinner"></div>
|
|
<div>正在启动内核…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- settings overlay -->
|
|
<div id="settings-overlay" class="overlay" style="display: none">
|
|
<div class="overlay-card">
|
|
<div class="overlay-head">
|
|
<h2>设置</h2>
|
|
<button class="tb-btn" id="set-close" title="关闭">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
>
|
|
<path d="M6 6l12 12M18 6L6 18" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="form">
|
|
<div class="row">
|
|
<label>内核端口</label
|
|
><input id="set-port" type="number" min="1" max="65535" />
|
|
</div>
|
|
<div class="row">
|
|
<label class="toggle"
|
|
><input type="checkbox" id="set-autostart" /> 开机自启</label
|
|
><span class="hint">登录后自动启动内核</span>
|
|
</div>
|
|
<div class="row">
|
|
<label class="toggle"
|
|
><input type="checkbox" id="set-silent" /> 静默启动</label
|
|
><span class="hint">启动时不显示主窗口,仅驻留托盘</span>
|
|
</div>
|
|
<div class="row">
|
|
<label class="toggle"
|
|
><input type="checkbox" id="set-tray" /> 关闭时最小化到托盘</label
|
|
><span class="hint">点关闭按钮隐藏到系统托盘</span>
|
|
</div>
|
|
<div class="row">
|
|
<label>插件</label>
|
|
<span class="hint" id="set-plugins-hint">加载中…</span>
|
|
</div>
|
|
<div id="pl-list" class="pl-list"></div>
|
|
<div class="row actions">
|
|
<button class="ghost" id="pl-toggle-all">全部启用</button>
|
|
<button class="ghost" id="pl-reload">刷新</button>
|
|
</div>
|
|
<div class="row actions">
|
|
<button class="ghost" id="set-dir">打开数据目录</button>
|
|
<button class="ghost" id="set-log">查看日志</button>
|
|
</div>
|
|
<div class="row actions right">
|
|
<button class="ghost" id="set-cancel">取消</button>
|
|
<button class="primary" id="set-save">保存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast"></div>
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|