Bundle
dsh-model-control
Unified model control for DeepSeek Harness: model picker (search/favorites/fold/chips/facts) + model management (channels, per-model capabilities) + reasoning-effort advisory (knowledge base, per-model defaults), in one seat and one settings page.
- Source
- uio-o
- License
- MIT
- Updated
- Updated 3 days ago
Readme
# dsh-model-control
面向 dsh Web GUI 的统一模型控件,接管输入框里的模型座位
(`conversation.input.model`),并在设置里提供一整套模型管理。
## 安装
```sh
dsh plugin --profile <你的 profile 名> add dsh-model-control
```
装完重启应用——bundles 是启动时读入的,只刷新页面不够。
<details>
<summary>其他安装方式</summary>
**从 GitHub 安装**(还没发 npm 时用这个):
```sh
dsh plugin --profile web add https://github.com/uio-o/dsh-model-control/archive/refs/tags/v0.2.2.tar.gz
```
**手工改 profile 的 `package.json`**:
```json
{
"dependencies": {
"dsh-model-control": "^0.2.2"
},
"dsh": {
"profile": {
"bundles": ["dsh-model-control"]
}
}
}
```
依赖与 `bundles` 两处都要写:前者让包被装上,后者让它在启动时被加载。
**本地开发**用 link:`"dsh-model-control": "link:D:/path/to/model-control"`。
</details>
## ⚠️ 与旧插件冲突
本插件整合并取代下面两个,**不要同时启用**:
- [dsh-model-picker](https://github.com/ttmouse/dsh-model-picker)
- [dsh-better-reasoning-effort](https://github.com/HaoyueQin/dsh-better-reasoning-effort)
座位是**单占用**槽位,三方都往那里注册,后注册的会让先注册的失效。迁移时把前两个从
`dsh.profile.bundles` 里移除即可,依赖声明留着无妨。
## 设计
**双区触发按钮**把「选模型」和「选推理档位」拆成两个独立弹窗:
| 区域 | 点击 | 弹窗内容 |
|------|------|----------|
| **左侧**(模型名) | 打开模型选择器 | 顶部搜索框(自动聚焦)、左栏供应商、右栏模型列表 |
| **右侧**(档位) | 打开档位选择器 | 推理档位列表 + 推荐默认档 |
鼠标悬停任一区域会说明它将要做什么。模型区显示 `供应商 · 模型名`:同一个模型 id 常常
由多个供应商提供,光看名字说不清模型跑在哪里。
## 选择器
### 供应商徽标
供应商的首个字符紧跟在模型名前面——DeepSeek 是 `D`,智谱 GLM 是 `智`——**关闭状态的
触发器上**和列表的每一行都会显示,所以还没打开选择器就能看出模型来自哪里。
每个供应商还有一个由其路由 id 推导出的固定底色。没有它的话,`17an-db`、`17an-anbot`、
`17an-mumiao` 都会只显示一个 `1`,看起来像同一个供应商。
### 供应商折叠
若某个供应商的名字等于另一个供应商的名字加上一个括号包裹的能力后缀,它就折叠进基供应
商那一行。modlens 会把视觉能力注册成独立的路由(`DeepSeek (modlens vision)`),不折叠
的话左栏每个供应商都会多出一行近似重复项。
折叠**只影响展示**:每一行始终保留「模型 + 真正服务它的路由」的配对,所以选中折叠行里
的模型,提交的仍是该模型自己的供应商 id。
### 分组列表
右栏始终是同一个分组列表:**收藏**(置顶的镜像副本),然后是每个供应商的分组。左栏是这
个列表的目录。滚动右栏时,左栏会高亮「钉在列表顶边」的那个分组;分组标题是
`position: sticky` 的,所以高亮始终和你看得见的标题一致。点击左栏的供应商会直接跳到对
应分组。搜索状态下,点击供应商改为缩小结果范围。
### 收藏
每个模型行在「名称与信息条之间」都有一个 `☆` / `★` 切换。收藏以**路由**
(`provider/modelId`)为键,而不是单纯的模型 id:同一个模型 id 常常由多个已配置的供应
商提供,收藏其中一条路由并不代表其他几条也被收藏。
被收藏的模型在列表里出现**两次**:一次在它所属供应商的分组里,一次在置顶的「收藏」分组
里。数据存在 `localStorage` 的 `dsh.modelControl.favorites` 下。
### 搜索
查询同时作用于**两栏**:供应商按显示名、路由 id、或「至少有一个模型匹配」来命中;左栏收
窄到匹配的供应商,每个徽章显示该供应商的命中数。未选中任何供应商时,模型列表横跨所有匹
配的供应商,并给每行标注其供应商。
### 键盘操作
焦点始终留在搜索框里,整个弹窗不需要 Tab 穿过每一行就能全程用键盘操作。
- `↑` / `↓` 移动一行,`PageUp` / `PageDown` 移动十行,`Home` / `End` 跳到两端
- `Enter` 提交光标所在行并关闭弹窗,把焦点交还给触发器
- 光标的样子是悬浮底色加上行左边缘的品牌色竖条;对勾表示正在使用的模型,光标表示下一次
`Enter` 会落在哪里,两者是分开的
- 搜索框用 `aria-activedescendant` 指向光标所在行,所以即使 DOM 焦点从不移动,读屏软件
也能播报当前位置
### 模型信息
座位读到的浏览器端目录只有 `id / name / description / reasoning`——没有上下文长度,也
没有输入模态。两者都在 Host 的 `llm` 服务上,所以由 host 半段通过一条回环路由重新发布,
弹窗给每行标注上下文长度胶囊(`1M`、`384K`)与图片图标。
**视觉能力**有两种图标:实心表示模型自己就能读像素;半透明表示这条路由之所以接受图片,
是因为 modlens 做了桥接——模型其实看不见,modlens 先把图片转写成文字。判断依据是图标,
不是名字。
## 模型管理
设置里新增「模型管理」页,同时**接管官方「模型」页**——两者做同一件事,留着两个入口只会
有两个真相来源。接管是可逆的:禁用或卸载插件时接管补丁随之消失,官方页立刻回来;也可以
在插件页里关掉接管开关切回官方页。
### 两层弹窗
设置页本身**只放渠道列表**:一行一个渠道,显示名 + 自定义/内置徽标 + 模型数 + 「编辑」。
点「编辑」打开**第一层弹窗**:这个渠道的配置与模型列表。模型行右侧的「设置」再打开**第二
层弹窗**:那一个模型的档位与能力。
两层都支持点遮罩关闭、右上角 × 关闭,`Esc` 先关上层再关下层。
### 打开弹窗不拉模型
打开渠道弹窗时,列表直接显示这个渠道**已经持有的**模型——自建渠道读 `llm-pi-ai` 文档,
内置渠道读适配器列表,**纯本地读取,不发网络请求**。看一眼模型列表不该依赖端点在线。
「拉取可用模型」是主动点击才发生的动作,结果只进下方的**候选面板**(虚线框,可随时收
掉),勾选后「保存选择」才写进渠道。候选面板不会顶掉已有列表——这是两件事:
> 列表 = 渠道持有的;拉取 = 往候选里添
### 添加渠道
创建是向导式的,创建放最后一步:**测试连接 → 拉取模型 → 勾选 → 创建**。
这个顺序来自一条上游硬约束:pi-ai 拒绝 `models` 为空的渠道,因为内置目录不认识自定义路
由,没有默认值可回退,于是「先建空壳渠道、之后再拉模型」走不通。渠道必须与它的模型一次
写入。
「测试连接」用填好的地址、协议、密钥实际调一次,不写任何东西;端点能连但不返回模型也是
明确的答案,与「连不上」分开报。
### 模型能力
每个模型可以单独声明:
- **文本输入**(固定)与**图片输入 / 识图**
- **上下文窗口**
- **最大输出**
这三项是 pi-ai 聊天模型条目支持的字段。模型条目里**没有**「是否支持工具调用」,也没有
「是否支持生成图片」——工具调用是每个协议都假定的能力,文生图走的是另一个独立的注册表,
不在聊天模型条目里。
### 思考档位
- **知识库建议**:内置 65 条档位表,覆盖 DeepSeek / Claude / GPT / GLM / Kimi / Qwen /
Gemini / MiMo 等 16 族,按最长边界命中匹配
- 未命中时按厂商家族与协议推断,并标注置信度:高(知识库)、中(厂商推断)、低(通用)
- **每模型默认档**:档位弹窗底部可设为默认,按 `provider/model` 存储
- **写入声明**:按 pi-ai 硬规则校验后写入,可备份、可精确撤销
## 槽位与注入契约
注入 `conversation.input.model`(由 `@deepseek-ai/dsh-client-ui-conversation` 声明)。
该座位是**单占用**的——这就是上面「与旧插件冲突」的根因:注册时崩溃会让输入框完全失去
模型控件。
宿主半用 `ctx.effect` 注册 17 条回环路由,禁用时释放,不会留下重复路由。
## 测试
```sh
npm test
```
跑两半:
- `scripts/check-client.mjs` —— 浏览器半的语法、注册形态、以及两条静态防线:每个
`useState` setter 都解析到声明(防「引用了不存在的 setter」这类崩溃),每个 i18n key
在中英两套文案里都存在
- `scripts/test-host.mjs` —— 用桩 `llm` / `settings` / `credentials` 驱动真实的 17 条路由
处理器:pi-ai 文档读写、渠道增删改、能力写入、档位声明与撤销、暗号明文落库、内置渠道的
自身配置、接管开关、以及禁用后重启用不产生重复路由
纯函数模块各自有独立测试:`test-engines.mjs`(建议引擎与分组)、`test-store.mjs`
(sidecar 读写、损坏降级、BOM 容错)、`test-selection.mjs`(选择集合并语义)、
`test-declaration.mjs`(声明校验与写入/撤销计划)、`test-profile-patch.mjs`(profile 补丁
文本编辑)。
## 这个项目是怎么写出来的
**代码由 AI 编写,人负责设计与验收。** 说清楚是因为它影响你怎么读这份代码。
具体分工:
- **AI 写代码**:约 6800 行的 `lib/`、17 条宿主路由、测试套件、以及这份 README。
从一个可用的思路到能跑的实现,大约四小时。
- **人定方向**:要解决什么问题、接哪几个座位、界面走哪条路线。中途否掉过一版两栏布局,
理由是设置窗太窄、左右高度差太大——这类判断来自实际使用,不是代码问题。
- **人做的验收**:装进真实环境用,把出问题的地方截图反馈回来。0.2.1 修的那个兼容性
故障就是这么发现的:插件在设置页显示「异常」,实际是 peer 范围把 DSH 0.2 排除在外。
- **工程约束是人要求的**:写测试、写静态防线(setter 与 i18n 完整性检查)、记录已知限制、
不许在真实配置上试错。
所以这个项目的形态是:**AI 负责把细节铺满,人负责判断哪里不对**。测试是绿的,但那不等于
它没有错——真正的 bug 是两个人在真实界面上找出来的。
如果你要基于它改东西,把 AI 当作者来读:注释解释了「为什么」,但结论的取舍是人定的,
遇到可疑之处欢迎开 issue。
## 出处与许可
MIT,见 [LICENSE](LICENSE)。本插件是衍生作品,两份上游都是 MIT:
| 上游 | 本插件用了什么 |
|---|---|
| [ttmouse/dsh-model-picker](https://github.com/ttmouse/dsh-model-picker) | 选择器座位的实现:双区触发按钮、搜索分面、供应商折叠与徽标、收藏、分组列表与滚动联动、键盘光标、上下文胶囊与视觉图标 |
| [HaoyueQin/dsh-better-reasoning-effort](https://github.com/HaoyueQin/dsh-better-reasoning-effort) | 65 条思考档位知识库(经 `scripts/generate-knowledge.mjs` 提取),以及档位梯形的组织思路 |
`lib/client.js` 中约 37.6% 的行与原 `dsh-model-picker` 相同,原插件的 39 个函数中 38 个
在本插件里保留原名。选择器那一半源自该项目。
本插件新增:模型管理页与两层弹窗、创建向导、按模型的能力配置、档位建议引擎、每模型默认
档、档位声明写入与撤销、厂家分组、选择集合并、接管开关、17 条宿主路由(全部可释放)、以
及 `scripts/` 下的测试套件。
Install
dsh plugin --profile web add github:uio-o/dsh-model-control#6cfe84b5a629611415b63b7b67519f177257cc95
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-model-control from the hub