把聊天输入框的模型选择器换成按渠道商分组、可折叠的列表:按渠道商归组,把视觉桥镜像渠道商折叠回上游,并逐模型带上设置里声明的思考强度选项。仅在 DSH 桌面版实测,Web 版未测试。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:ZZJQ678/dsh-model-picker
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
按渠道商(Provider)分组、可折叠的 DSH 模型选择器。
适用范围:仅在 DSH 桌面版实测通过(桌面版的 profile 名为
web)。 Web 版(浏览器部署的 DSH)未做测试,不保证可用。来源说明:本插件的代码、测试与文档全程由 DSH 的
deepseek-4.1-flash模型完成。
替换聊天输入框右下角原生的模型选择器,把扁平列表改成「按渠道商分组 + 可折叠」, 并逐模型继承设置里声明的思考强度。
功能
- 按渠道商分组,组头显示该组模型数量
- 可折叠:默认只展开当前模型所在的组,其余折叠;点击组头切换
- 合并重复渠道商:
(modlens vision)这类「视觉桥」镜像会被折叠回上游渠道商 - 两个视觉标签,都继承设置:
- 「视觉」= 该模型在设置里勾了视觉(
input含image),原生就能读图 - 「视觉桥」= 该模型自己没勾视觉,但 modlens 为它套了桥,可经桥读图
- 两者互斥;设置里读不到声明时两个都不显示(不瞎猜)
- 「视觉」= 该模型在设置里勾了视觉(
- 搜索:匹配渠道商名称/ID,或模型名称/ID;搜索时自动展开命中分组
- 思考强度逐模型继承:面板顶部显示当前模型自己的那套强度选项,切换模型时 带上该模型的默认强度
- 失败可见:渠道商目录加载失败时显示原因,并提供重试
视觉标签是怎么来的
和思考强度一样,完全继承设置,不需要额外配置。但数据来源和强度不同:
settings.yaml 的 llm-pi-ai.providers.<渠道商>.models[].input
= ["text","image"](勾了视觉) | ["text"](取消) | [](未声明)
→ 设置文档镜像(settingsScope.describe())
→ 本插件折叠成「提供方\n模型」集合
为什么要绕这一圈:模型目录(session/modelCatalog)不含视觉字段。
服务端 buildModelCatalog 明明从 resolveModelInfo 拿到了 inputModalities,
却显式把它丢掉,每个条目只保留 {id, name, description?, reasoning?}
(dsh-api-session-controller/lib/index.js:1994-1999)。已用 CDP 拦截真实响应核实:
78 个模型的字段无一例外。所以只能直接读设置文档 —— 官方设置页用的也是同一份镜像。
两个容易踩错的细节:
input: []不等于「没勾视觉」,而是「此处未声明、向下继承」 (dsh-llm-pi-ai/lib/index.js:286的declaredInput)。勾选写["text","image"], 取消写["text"]。因此只有明确含image才算勾了视觉。- 官方 DeepSeek 路由的提供方 id 与它的设置命名空间不同名:
提供方是
deepseek-official,命名空间是llm-deepseek(dsh-llm-deepseek/lib/index.js:2038-2043)。插件经remote.llm.listConfigurableProviders()取这张别名表,否则那个渠道商的 「视觉」标签会整片认不出来。
服务端动态发现型提供方没有视觉声明。 CodeBuddy / WorkBuddy / CodeArts 这类
渠道商的设置命名空间里 providers 是空的(模型由服务端下发),因此读不到任何
视觉声明 —— 此时两个标签都不显示。这是刻意的保守行为:读不到就不猜。
关于「视觉桥」镜像的合并
@liustack/modlens 这类视觉桥插件会为每个上游渠道商注册一个镜像 provider:
- id:
modlens-<上游id>(deepseek-official特殊为deepseek-modlens) - 名称:
<上游名> (modlens vision)
镜像的 listModels 只给同一个模型换个带后缀的显示名,模型 id 与上游完全一致
(依据:modlens 的 dsh/index.js:782-788)。直接列出会让同一渠道商重复出现两遍。
本插件的处理:把镜像折叠回上游渠道商,同名模型只列一条,并给它打上「视觉桥」 标记——信息保留,重复消除。合并要求镜像的 id 特征与名称后缀同时命中, 且上游渠道商确实存在于同一份目录中,因此该插件被卸载后镜像会自然消失, 无需改动这里的代码。
为什么「视觉桥」早先会标错
改版前,「视觉桥」直接由「该模型出现在镜像里」推导。而 modlens 只给没勾视觉的 模型套桥:
// modlens/dsh/index.js:690
if (Array.isArray(info?.inputModalities) && info.inputModalities.includes('image')) return false
于是标签表达的是「这个模型看不了图、但有桥可走」,与「勾了视觉」正好相反。 本机实测:11 个带标签的模型里,勾了视觉的 0 个;而 25 个勾了视觉的模型 全都没有标签。现在改成:勾了视觉 → 「视觉」;没勾但有桥 → 「视觉桥」。
思考强度是怎么来的
不需要额外配置——设置里已经配好的东西会自动生效。完整链路:
settings.yaml 的 llm-pi-ai.providers.<渠道商>.models[].reasoningEfforts
→ dsh-llm-pi-ai 的 resolveModelReasoning 把声明的级别写进 thinkingLevelMap,
未声明的级别 pin 成 null(不支持)
→ pi-ai 的 getSupportedThinkingLevels 算出这个模型真正支持哪些级别
→ dsh-llm-pi-ai 的 reasoningInfo 写进目录的 reasoning.efforts
→ session/modelCatalog → 本插件读取
所以本插件只是读,不自己判断哪个模型支持什么。你在设置里给某个模型声明了
off/minimal/low/medium/high,它就显示这五个;声明了 low/medium/high/xhigh,
就显示那四个;没声明 reasoningEfforts 的模型不显示强度控件。
两个细节与官方实现保持一致:
- 只有
off一个级别的模型不显示控件。off的语义是「不带参数」,单独存在 等同于没有这个能力(这也是dsh-llm-pi-ai自己reasoningInfo的判断)。 - 切换模型时带上该模型的
defaultEffort,与原生selectionOf(dsh-client-ui-model-selection/lib/client.js:908-918)行为一致。
defaultEffort 来自渠道商配置里的 profile 级 reasoning(不是每个模型单独声明),
由 dsh-llm-pi-ai 解析后随目录下发。
设计取舍
只换视图,不碰数据。 目录、选择、思考强度全部复用官方服务
ctx.modelDirectories 与 directory.select(),与原生选择器共用同一份会话级目录。
两个入口(/model 命令与输入框槽位)永远显示一致,也不会修改任何 Provider 插件。
绝不因「加载中」而禁用。 原生触发器只在 locked 时禁用,「加载中」仅是文案。
本插件遵循同一约定——把 loading 做成 disabled 会让用户彻底无法操作。
取不到目录时抛异常,而不是渲染死壳。 槽位是 single 类型,本插件以
priority: -1 遮住原生(priority 0)的条目。若 directoryFor() 抛错,异常会被
ui-renderer 的 SlotErrorBoundary 捕获,并按 abdicate: true 把本条目从格子中
退役,原生选择器随即自动接管。宁可退回原生,也不给死界面。
能力标签只做有真实依据的。 官方模型目录(session/modelCatalog)的 model 对象
只有 {id, name, description?, reasoning?},没有任何视觉/免费/工具调用字段,因此
不凭名字或描述猜这类标签。唯一的两个标签(「视觉」「视觉桥」)都有确切来源:
前者读设置的视觉声明,后者读 modlens 的镜像事实。
UI 用官方组件库。 图标与配色来自 @deepseek-ai/dsh-client-ui-primitives
(原版界面用的就是它),并全部走 DSH 的 CSS 变量,因此自动跟随明暗主题。
该包属于模块系统的静态基座,无需在 dsh.client.external 里声明——这一点由已
正常工作的第三方插件 @liustack/modlens 证明(它的 dsh.client.inject 是空
数组,却直接 require 了本包)。仍保留 try/catch 兜底:万一基座里没有它,插件
退化成无图标的纯 DOM 版本,而不是整个崩掉。
增益功能绝不连累核心。 视觉标签依赖 settingsScope / remote.llm 两个服务;
它们各自单独一层 ctx.inject + try/catch。若把它们混进注册槽位的那一层,
服务一旦缺席就会连累整个选择器不注册——用一个可选增益去赌核心功能,不值得。
安装
从 GitHub 装(公开仓库):
dsh plugin --profile web add "github:ZZJQ678/dsh-model-picker"
本地开发时用 link: 装到桌面版实际使用的 profile:
dsh plugin --profile web add "link:E:\DSH\dsh-model-picker"
装完重启 DSH Desktop(或应用内「重启 Harness」)。补丁层是 live 重载, 但bundle 列表只在启动时固化——新增一个 bundle 必须重启才会进入前端 boot 图, 只按 F5 刷新页面是不够的。
⚠️ 桌面版的 profile 名是
web,不是desktop网上不少插件(例如 dshhub 上的
dsh-model-picker-plus)README 写的是dsh plugin --profile desktop add …。桌面版没有desktop这个 profile, 照抄会装到一个用不上的地方,表现就是「装成功了但界面毫无变化」。 桌面版的 profile 名是web,目录在%APPDATA%\dsh-desktop\harness\profiles\web。
卸载
dsh plugin --profile web remove dsh-model-picker
原生模型选择器会自动恢复。
开发
node --check client.js
node tests/logic.mjs # 装配、槽位注册、inject 契约、失败让位约定
node tests/panel.mjs # 分组、折叠、搜索(桩 React)
node tests/real-render.mjs # 真实 React 18 + jsdom 渲染
node tests/effort.mjs # 思考强度逐模型继承(真实渲染)
node tests/vision.mjs # 「视觉」/「视觉桥」双标签与设置继承(真实渲染)
node tests/reasoning-chain.mjs # 设置 → 目录 的强度解析链路(真实 settings.yaml)
node tests/scan-mojibake.mjs # 扫描双重编码破坏(改源码后顺手跑一次)
real-render.mjs / effort.mjs / vision.mjs 用 DSH profile 自带的
react / react-dom / jsdom,版本与桌面端运行时一致,能捕获真实 hooks 规则违例与
渲染警告。reasoning-chain.mjs 读本机真实的 settings.yaml,用真实 pi-ai 算法
验证「不同模型强度不同」这个前提成立。
写测试桩的三个坑(都已踩过,记录避免重复):
getSnapshot必须返回稳定引用。 官方的dsh-client-store就是如此 (getSnapshot即 zustand 的getState())。若桩每次调用都造新对象,useSyncExternalStore会认为状态一直在变并无限重渲染。- jsdom 里 React 的
onChange不会被input事件触发(React 依赖自己的 事件系统模拟 change,jsdom 下该链路不完整)。这是测试环境的限制,不是插件 缺陷——真实浏览器正常。因此搜索相关的测试直接调用元素的onChangeprop。 - 桩的
ctx.inject必须只给被声明的服务。 真实 Cordis 的 scope 是受门禁 约束的:没在那一层inject里声明的服务根本取不到。若桩把所有服务一股脑 塞进去,就会盖住「别名表取不到」这类真实 bug(vision.mjs正是靠如实模拟 才抓到它)。另外,异步 RPC 的.then是微任务,断言前要await act(async () => { await flush() }),否则测的是中间态。
纯客户端插件:client.js 通过 window.__ModuleLoader__.load 注册,
cordis.patch.yml 声明加载器条目,宿主侧 index.js 是空实现,无构建步骤。
踩过的坑(真实浏览器实测)
上面那套测试都在 Node + jsdom 里跑,盖不住插件与宿主之间的服务契约。 以下失败只在真实浏览器里出现,所以真实环境复验不可省。
1. inject 少写 remote / remote.session → 插件静默失效
症状:装好后界面毫无变化,原生扁平列表照旧;控制台里只有一行
slot entry crashed in 'conversation.input.model':
Error: cannot get property "remote.session" without inject
界面没有任何提示——因为槽位边界把失败条目按 abdicate 退役了,原生随即接管。
原因:modelDirectories.directoryFor() 内部会读 this.ctx.remote.session
(官方 dsh-client-ui-model-selection/lib/client.js:304 的
new ModelDirectory(this.ctx.remote.session, …))。Cordis 对未在 inject
里声明的服务属性访问会直接抛错,而 ctx.inject([...], cb) 拿到的正是一个受
门禁约束的 ctx。我们以 priority: -1 遮住原生,一旦注册项在注入期抛错,就被
退役、原生静默接管。
修法:inject 与官方 dsh-client-ui-model-selection 对齐
(['locale','sessions','slots','remote','remote.session'])。
tests/logic.mjs 已加断言锁死这一点。
教训:「复用官方服务」不等于「只声明官方服务的名字」——被复用服务自己 依赖什么,调用方也必须声明什么。
2. 同一个坑踩了第二遍:别名表取不到
做视觉标签时又栽在同一件事上。remote.llm.listConfigurableProviders() 能给出
「提供方 id ↔ 设置命名空间」的别名表,但最初把它写在注册槽位那一层的
ctx.inject(['slots','modelDirectories'], …) 里 —— 那个 scope 没有 remote,
于是 scope.remote?.llm 静默短路成 undefined,别名表永远拿不到,
deepseek-official 渠道商的「视觉」标签整片认不出来。
这次 vision.mjs 的单测抓到了它,因为它的 ctx.inject 桩如实只给被声明的
服务(若桩把所有服务都塞进去,这个 bug 会被盖住)。
修法:别名映射单独一层 ctx.inject(['remote','remote.llm'], …),并且
不把 remote.llm 混进注册槽位那一层 —— 那样一旦该服务缺席,就会连累整个
选择器不注册。
教训:ctx.inject(deps, cb) 的 cb 只在依赖齐备时执行,这一条既是保护也是
陷阱。增益功能要单独分层,别让可选依赖挡住核心路径。
3. --dsw-alias-* 变量挂在 body 上,不在 :root
我一度用 getComputedStyle(document.documentElement) 检查这些变量,得到
「21 个全部缺失」,差点以为整套配色在靠兜底值硬撑。实际上它们定义在 body
上(主题提供者那一层),而 documentElement 在 body 之上,取不到是必然的。
正确做法:在插件自己的元素上取(getComputedStyle(panel).getPropertyValue(…)),
那才是真正参与渲染的值。实测浅色/深色都能正确解析并跟随
body[data-ds-dark-theme] 切换(面板背景 #fff ↔ #353638),无需额外适配。
4. 桌面版 profile 名是 web
见上文「安装」。这一条是第三方插件「不适配桌面版」的常见根因。
5. 别用 PowerShell 的 Get-Content | Set-Content 改 UTF-8 源码
改版本号时用了
(Get-Content x -Raw) -replace … | Set-Content x -Encoding utf8,
结果把已按 UTF-8 存的中文当成 GBK 读、再按 UTF-8 写出,整份文件变乱码
(导出 → 瀵煎嚭),还吞掉了一个引号导致 JSON 失效。破坏是:
原始 UTF-8 字节 --GBK解码--> 乱码 --UTF-8编码--> 新字节。
这次靠 DSH 自己的 change-ledger(内容寻址的 blob 库,
$DSH_HOME/change-ledger/v1/workspaces/<id>/blobs/)找回了完整原件。
查乱码用 \uFFFD 替换字符 + 「瀵煎嚭」这类高频 mojibake 词最可靠。
正确做法:改 UTF-8 源码一律用编辑器工具(本仓的 edit / write),
不要过 PowerShell 的文本管道。真要用 PowerShell,就走
[System.IO.File]::ReadAllText / WriteAllText 并显式指定 UTF-8。
License
MIT
链接
同类插件
zhu1090093659/dsh-web#packages/dsh-task-board★ 7809
侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
zhu1090093659/dsh-web#packages/dsh-web-all★ 7809
DSH Web UI 插件与皮肤合集:任务看板、git 图、右侧面板、远程移动端 UI、桌宠、实时 token 统计与皮肤中心。
omdsh-dev/DSH-better-sidebar★ 3680
侧边栏完整工作台:内置文件渲染编辑、终端、Git 与子代理,支持三方插件注册新 Tab。
ccch1mneyyy/dsh-TUI★ 3110
Claude Code 风格全屏终端 UI:像素鲸鱼顶栏、实时工作状态行、思考流式展开。
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 2750
右下角常驻的小鲸鱼挂件:余额、今日已用与每轮对话消耗(含峰谷价),余额预警与今日预算的泡泡内容都可编辑;泡泡点击序列模块化自定义,支持并列加权 A/B、随机台词与随机图片;内置 30+ 厂商模板(OpenAI / OpenRouter / Kimi / 硅基流动 / 方舟 / 智谱 / MiniMax 等),按模型查余额与订阅额度;另有任务结束音效、导入音频、自定义角色与资源管理。数据全在本机,无遥测。
Devin-AXIS/deepseek-design#deepseek-idesign★ 1359
可视化设计工作室,支持网站、App 原型、海报、信息卡、报告和杂志的模板创建、元素编辑、选区级 AI 草稿衔接与导出。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。