dsh-client-ui-model-switcher
DeepSeek Harness Web GUI 的一键服务商/模型切换客户端插件。
会话标题栏加一个滑块按钮:弹出所有已配置的服务商和它们的模型,点一下即切换,不用再进设置一个个配。
功能
- 替换原生模型选择器:输入框右下角的原生模型选择器被替换为增强版——默认显示当前服务商的模型列表,点击「切换服务商 →」可换服务商,两级联动:顶部一排服务商 chips(带模型数),先点服务商,下方联动显示该服务商的模型列表,点模型即切换,服务商(及其地址)自动跟随
- 只显示配置好的:面板通过
llm.providers + settings.describe + credentials.describe 检查每个服务商的 key 状态,没有 key 的服务商不显示(补 key 后自动出现)
- 当前状态:面板顶部显示当前服务商 / 模型,当前项带蓝色高亮和 ✓
- 实时目录:打开时自动刷新模型目录(与
/model 命令、composer 模型席位共享同一份状态,任一入口切换其它入口同步)
- 异常提示:服务商不可用、目录加载失败、失败的服务商列表都会显示
- 会话限制:子代理会话(不支持切换模型的)会禁用并提示
- 联网搜索切换:面板底部提供「🔍 联网」切换 chip,在 on / 沿用上次 / 未联网 三态间循环,点按即写回插件本地设置(host 只暴露全局 searchProvider,无逐会话开关,因此这是一个插件本地已知限制的偏好,详见 README「0.8.3 更新」)
- 面板内供应商/模型管理:面板底部「+ 自定义接口预设」可在面板内直接新增 OpenAI 兼容供应商(名称 / baseURL / 默认模型 / API key 环境变量 + 值),确认后只追加写入
settings.mutate 与 credentials.set,不覆盖已配置项
- 就地删除模型:选中某个服务商后点「删除模型」进入删除模式,每行右侧出现减号按钮,点即从该服务商的存储列表里移除这一行;当前正在使用的模型按钮置灰并说明原因。写入是整段
models 数组一次 set,不留索引空洞
- 一键更新为勾选清单:点「一键更新」先向该服务商接口拉取模型目录,列成候选清单(已选 N / 候选 M,带筛选框),已在列表里的标「已有」、缺失的默认打勾;也可在下方手填模型 id(接口不实现 /models 或返回 401 时仍然可用),确认后才写盘
实现要点
- 槽位:
conversation.session.header.actions(order=25)
- 服务:
modelDirectories(ctx.modelDirectories.directoryFor(sessionId))
- 目录状态:
react.useSyncExternalStore 订阅 directory.store(uSES 兼容 SnapshotStore)
- 切换:
directory.select({ provider, model })——与 /model 弹窗和 composer 模型席位同一个控制器,Host 是唯一事实源
- 纯浏览器侧,手写 ModuleLoader bundle,零构建(仅 react / react/jsx-runtime)
安装
本插件同时支持 CLI / Web 版(web profile)与 桌面版(desktop profile)。
推荐用 DSH 自带 CLI 安装:dsh plugin add 会把依赖和 dsh.profile.bundles 一起写进该 profile 的 package.json,不需要再手工改 cordis.patch.yml。
# CLI / Web 版
dsh plugin --profile web add github:2275803244-cpu/dsh-model-switcher
# 桌面版(桌面应用自带 CLI,位于 <安装目录>\resources\runtime\cli\bin\dsh.cmd)
& "<桌面版安装目录>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add github:2275803244-cpu/dsh-model-switcher
装完重启对应程序,会话标题栏出现滑块按钮。
手工安装(旧方式,仅 web profile)
pnpm --dir "$HOME\.dsh\profiles\web" add github:2275803244-cpu/dsh-model-switcher
cordis.patch.yml 追加:
- insert:
- id: ui-model-switcher
name: 'dsh-client-ui-model-switcher'
重启 dsh web。
服务商目录来自 设置 → 模型 里已配置的适配器;要增加新服务商(如本地 Ollama),先在模型设置中配置好,一键切换里就会出现。
0.9.0 更新(就地编辑模型列表)
0.8.x 只解决「换模型」,0.9.0 让面板同时能改模型列表。两项能力均由 test/client.test.cjs 覆盖,另有测试锁定 Remote 调用形态。
| 能力 | 说明 | 验证 |
|---|
| ① 删除模型 | 选中服务商后点「删除模型」进入删除模式,每行右侧出现减号按钮;当前正在使用的模型置灰不可删(删掉会让会话失去可用路由)。写入是把剩余行整段 set 回 providers.<key>.models,不按索引 unset——远端只接受 set/unset 两种 op,索引删除会留空洞被 schema 拒绝 | 断言单次 set、路径只含 models、删除后其余行保序 |
| ② 一键更新 = 候选清单 | 点「一键更新」先用 llm.discoverModels 拉该服务商目录,不写盘,只把候选列成清单(已选 N / 候选 M + 筛选框):已在列表里的标「已有」,缺失的默认打勾。下方可手填模型 id,接口拒绝(401 / 未实现 /models)时清单为空但手填照用。点「确认加入 N」才写入 | 断言确认前零写入、预勾选正确、确认后单次 set、手填 id 追加到末尾 |
| ③ 调用形态按形参判定 | settings.mutate / llm.discoverModels 在不同 DSH 构建里有位置参数 (ns, ops, rev) 与对象参数 ({ ns, ops }) 两种形态。按被调函数的声明形参个数选择,不做试错:对只声明一个形参的方法传位置参数不会报错,而是把第一个参数当成整个请求静默接受,试错式探测会写错并回报成功 | 新增测试用宽形参实现,断言 namespace / ops / path 逐层到达、且不臆造 expectedRevision |
版本递进说明:新增行为改变 dsh.client bundle 内容,0.8.3 → 0.9.0 提次版本;仍无运行时依赖、无构建产物,lib/client.js 保持 react-only。
0.8.3 更新(联网搜索切换 + 面板内管理 + 兼容适配)
本版本在 0.8.2 的桌面版兼容基础上,补齐了四项能力,每项均由 test/client.test.cjs 覆盖:
| 能力 | 说明 | 验证 |
|---|
| ① 联网搜索(web search)切换 | 会话面板底部新增「🔍 联网」chip,三态循环:on(联网)→ follow(沿用上次)→ off(未联网)。当前 web search 选择(searchPref)通过 api.settings.mutate({ ns: 'dsh-client-ui-model-switcher', ops: [{ op: 'set', path: ['searchPref'], value }] }) 只追加写回插件本地命名空间;浏览器内用 localStorage 立即生效。已知限制:host(@deepseek-ai/dsh-web 的 ctx.web)只有一个执行期全局选定的 searchProvider(config.searchProvider / 环境变量 DSH_WEB_SEARCH_PROVIDER),没有逐会话/逐对话的开启契约,所以这是一个插件本地偏好,并不会真正打开/关闭 host 的搜索 —— 只记忆并持久化你的选择 | 新增测试验证三态循环与 settings.mutate 写回 |
| ② UI/交互改造 | 面板底部独立工具条:左侧「🔍 联网」chip 实时显示当前搜索偏好状态(aria-pressed 标记),右侧「+ 自定义接口预设」管理按钮;chip 点击即时三态切换、无刷新 | _ms_searchBadge / _ms_footer / _ms_manageBtn 样式与 aria-pressed 断言 |
| ③ 面板内供应商/模型管理 | 不离开会话面板就能新增 OpenAI 兼容供应商:UrlPresetForm 收集 名称 / baseURL / 默认模型 / key 环境变量与值,走「下一步:确认 → 确认写入」两段式;只追加写入 settings.mutate(providers.<slug>)与 credentials.set,并用 llm.discoverModels 探测可用性 | 测试断言只写一次、单 op、llm-pi-ai 命名空间、key 只 set 一次 |
| ④ web + desktop 兼容 | 纯浏览器侧 bundle,只依赖 react / react/jsx-runtime,不耦合 DSH 内部包,dsh.client.platform: "web";web 与 desktop profile 均可用 | 0.8.2 实测表格(见下)不变,本版未引入任何 Node API |
版本递进说明:新增行为(联网搜索切换)会改变 dsh.client bundle,因此 0.8.2 → 0.8.3 仅作小版本递进,不引入运行时依赖、无构建产物。
0.8.2 更新(桌面版支持)
DSH 桌面版(@deepseek-ai/dsh-desktop-runtime 0.2.0-rc.2)启用了独立的 desktop profile,插件默认不在其中。本版本确认插件在桌面版下无需改动代码即可工作(纯浏览器侧 bundle,只依赖 react / react/jsx-runtime,不耦合 DSH 内部包):
| 核对项 | 桌面版 0.2.0-rc.2 实测结果 |
|---|
槽位 conversation.session.header.actions | ✅ 仍由 dsh-client-ui-conversation 渲染 |
槽位 conversation.input.right | ✅ 仍存在 |
服务 modelDirectories / slots / sessions / remote / connection | ✅ 均在 |
modelDirectories.select({ provider, model }) | ✅ 签名一致(async select(selection)) |
| 组合树 | ✅ 装入 desktop profile 后可见 ui-model-switcher 行,无报错 |
校验方式:桌面版核心代码打包在 app.asar 内,直接解包比对运行时实际代码;desktop profile 只能由桌面应用启动,组合树校验用一份副本 profile 跑 --dump-config。
0.8.1 修复(本地补丁)
针对 DSH 0.1.2-rc.1 的 Remote 契约与原生选择器行为修正了以下问题(test/client.test.cjs 覆盖):
| # | 问题 | 修复 |
|---|
| 1 | usableProviders 调用不存在的 API(llm.providers({})、settings.describe({})、credentials.describe({refs})),异常被吞掉后返回 null,"只显示配好 key 的服务商" 实际从未生效 | 改用 llm.listProviders() + llm.listConfigurableProviders() + settings.describe() + credentials.describe(refs: string[]),并用注册表判定路由是否 active |
| 2 | 取 API 面用 connection.api,该属性在 ConnectionHandle 上不存在(当前实现是 ctx.remote),导致上面第 1 条必然失败 | 改用 ctx.remote(inject 增加 remote),保留 connection.api 兜底 |
| 3 | usableMap 跨开关持久,补 key 后重开面板仍被旧的过滤结果挡住 | 每次打开重置并重新拉取,异步结果用 live 标记防竞态 |
| 4 | 过滤后若一个服务商都不剩,面板空白无法切换 | 过滤结果为空时回退到完整列表 |
| 5 | 切换模型只提交 {provider, model},丢掉推理强度:与 /model 弹窗/composer 席位行为不一致,原生席位会带 reasoning.defaultEffort,重选当前模型时保留用户已选强度 | 新增 selectionFor(),与原生 selectionOf() 同语义 |
| 6 | 两个触发器都显示原始模型 id(deepseek-v4.1-flash-expires-on-0910),且不显示推理强度 | 新增 modelName()/effortLabel(),显示模型名 + · 强度 |
| 7 | 面板打开时点触发器不会关闭 | 触发器改为切换语义 |
| 8 | rootRef.current 可能为 null 时的 contains() 抛错 | 加空值保护 |
测试:
node test/client.test.cjs # 13 项断言,无需安装依赖
卸载
pnpm --dir "$HOME\.dsh\profiles\web" remove dsh-client-ui-model-switcher
# 删掉 cordis.patch.yml 的 ui-model-switcher 行,重启
贡献
欢迎提交 PR!请先阅读 CONTRIBUTING.md。
- 跑测试:
node test/client.test.cjs(提交前必须全绿)
- 跑 bundle 校验:
node test/bundle-check.cjs(确认 lib/client.js 仍是纯浏览器、react-only、无 Node API 的 ModuleLoader bundle)
- 提交信息遵循 Conventional Commits
参考
ModelDirectory / ModelDirectoryResolver:dsh-client-ui-model-selection/lib/types/client/service.d.ts、directory.d.ts
- 目录状态契约:
dsh-client-ui-model-selection/lib/types/client/directory.d.ts
- 类型:
dsh-host-apiproxy/lib/types/api/sessions.d.ts(ModelSelection / ModelProviderGroup / SessionModels)