置顶收藏模型并自动切换故障路由
29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/16 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add PhilYue/dsh-llm-manage该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
README
由 DeepSeek 最新模型翻译生成dsh-llm-manage
一个持久化的 DeepSeek Harness 插件,用来管理某条已配置 LLM 路由的模型清单:置顶与收藏、逐个模型记录不可用原因、与网关目录对账,以及某个模型配额耗尽时自动回退。
故意不提交任何截图。 screenshot.mjs 抓取的是正在运行的 harness,所以图里显示的是那台 harness 自己的路由和模型清单——也就是维护者自己的部署。渲染进 PNG 的路由名没法用文本扫描搜到,而当初就是这样泄露了一批路由名。把脚本对着你自己的 harness 跑(见不重启也能看面板),得到的就是你自己的路由,这张图对你才有意义。
为什么需要它
一条路由把多个服务商聚合在同一个网关后面,会把整个目录摊成一张平铺列表。
展开完整文档(README 剩余 1 节)
dsh-llm-manage
一个持久化的 DeepSeek Harness 插件,用来管理某条已配置 LLM 路由的模型清单:置顶与收藏、逐个模型记录不可用原因、与网关目录对账,以及某个模型配额耗尽时自动回退。
故意不提交任何截图。 screenshot.mjs 抓取的是正在运行的 harness,所以图里显示的是那台 harness 自己的路由和模型清单——也就是维护者自己的部署。渲染进 PNG 的路由名没法用文本扫描搜到,而当初就是这样泄露了一批路由名。把脚本对着你自己的 harness 跑(见不重启也能看面板),得到的就是你自己的路由,这张图对你才有意义。
为什么需要它
一条路由把多个服务商聚合在同一个网关后面,会把整个目录摊成一张平铺列表。于是五件事同时出问题,而且没有一件是网关的错:
| 痛点 | 这个插件做什么 |
| 列表很长,收藏被埋 | 置顶和收藏模型;选择器把它们提到顶部独立分区,面板按置顶 → 收藏 → 最近 → 其余排序 |
| 说不清自己到底依赖哪些模型 | 本地账本按使用频率给每个模型定级,其余按 14 天热度排名,在面板里显示为 Most used 卡片,在选择器里显示为排名分区 |
| 已下线的模型还在列表里,选中就报错 | 把路由的 models: 列表与网关自己的 GET {baseURL}/models 目录对账,缺失的标记为已下线 |
| 配额耗尽,只能手动切换 | 监听 agent/request-error,遇到模型级失败就把会话切到下一个可用模型,等应答的模型确认后才报告这次切换 |
| 同一个模型由多个服务商提供 | 在每一行标注还有多少条路由也提供它,而不是把重复项藏起来 |
| 完全调不通的模型 | 发一个最小请求探测,如实标注不兼容 |
这里没有任何东西写死到某个网关或某个模型 id。
安装
从 npm registry 安装(打包好的发行版,不需要工具链):
dsh plugin --profile web add dsh-llm-manage
dsh plugin 会把 --profile <name> 之后的参数转发给该配置档目录里的 pnpm,所以 pnpm 接受的写法这里都能用(add、remove、why……)。配置档把 node_modules/dsh-llm-manage 映射到 registry 里的包。
开发安装
正常安装请优先用 npm 包。开发插件本身时,还有两种方式可用:
- 从路径安装以链接目录,这样本地改动无需重装就能生效:
dsh plugin --profile web add /absolute/path/to/dsh-llm-manage
这会软链工作副本——你自己就是作者时最合适。每次编辑都即时生效,这就是为什么下面「应用改动」一节是专门为它写的。
- 从 git URL 安装,拿到最新源码的固定副本,而不是 npm 发布版:
dsh plugin --profile web add git+https://github.com/PhilYue/dsh-llm-manage.git
这个包以 bundle 形式安装:它的 dsh.bundle.patch 行插入一个 Host 插件,dsh.client 声明让 web shell 加载浏览器端那一半。Host 那一半需要完整重启 harness;浏览器端那一半由客户端 HMR 监听器拾取,刷新页面就够了。见下面「应用改动」。
配置
所有值都放在插件行的 config 里,这样你自己的补丁层在升级后仍能保留覆盖项。默认值:
- id: llm-manage
name: 'dsh-llm-manage'
config:
selector:
mode: managed # 'native' withdraws the shadowed picker
probe:
intervalMs: 1200 # delay between deep probes
timeoutMs: 30000
batch: 40 # probes accepted per request
directory:
ttlMs: 1800000 # how long a gateway directory read is trusted
fallback:
enabled: true
chain: [] # 'route/model' entries, tried first
usage:
enabled: true # local proficiency/heat counters
seed: true # one pass over existing session logs at startup
flushMs: 5000 # counters are batched to disk on this interval
回退链的顺序是:fallback.chain,然后是固定模型,然后是收藏,然后是同一路由上剩下的模型,最后是其他路由。已经确认退役、协议不兼容、被隐藏,或正处于配额冷却期的模型会被跳过。每轮最多自动切换三次。
哪些失败会切换,哪些永远不会
规则看的是作用范围,不是严重程度:
| 失败 | 作用范围 | 是否切换? | 是否记到模型头上? |
| 模型未找到 / 已退役 | 单个模型 | 是 | 是 |
| 该路由协议不对 | 单个模型 | 是 | 是 |
| 单模型配额耗尽 | 单个模型 | 是 | 是 |
| 其他单模型失败 | 单个模型 | 是 | 是 |
| 该路由没有注册适配器 | 整条路由 | 是 | 否 |
| 账号级速率限制 | 整个 API key | 永不 | 否 |
| 账号级配额 | 整个 API key | 永不 | 否 |
模型级失败会换到另一个模型,因为换个模型能撑过这一轮。账号级失败永不切换:所有模型共用那个 key,切换只会掩盖真正的问题,还白白烧掉一次请求。
没有适配器的路由自成一个作用范围。在那条路由上换模型没用——那里每个模型都以同样方式失败——但换到另一个服务商有用,所以它仍然会切换。它绝不能做的是落进模型的健康记录:error 属于让回退链跳过某个模型的原因集合,而且和 quota 不同,这个判定没有过期逃逸,所以一次路由配置错误就会把未来每条链路上的无辜模型永久受损。
探测不是路由事件
「不可用」和「把对话挪走」是两个不同的结论,只有请求失败才会走到第二个:
- 请求失败(agent/request-error)是正在进行的对话撞了墙。如果失败只影响某个模型,这一轮会在下一个可用模型上重试,并用横幅标明它为哪个模型切换。
- 探测失败只是你针对某个模型提的问题有了答案。它记录该模型的健康状况——这样下一次真正切换时会跳过它——但它不会动到恰好正在使用该模型的会话。因为你跑了一次诊断就把正在进行的对话挪走,等于背着你换模型,而这正是本设计禁止的事。
具体来说,选中 model-gamma 且它的配额已耗尽时:
| 事件 | 之后的选中模型 |
| 你对 model-gamma 做深度探测,探测失败 | 仍是 model-gamma |
| 对 model-gamma 的真实请求因单模型配额失败 | model-beta(下一个可用),显示横幅 |
| 真实请求因账号级密钥限制(2008)失败 | 仍是 model-gamma |
每轮最多发生三次自动切换;如果整条链路都找不到可用模型,失败会原样传出,而不是用不可能成功的方式重试。
切换真的发生了,横幅就是证据
有两种机制会移动模型,区别很重要:
- 会话自身的选择(sessionController.selectModel)才让改动真正生效。它会拿已注册的适配器校验目标,并追加一条持久的 model/selection 事件,所以对话使用的模型确实变了,所有界面也保持一致。之后会把保存的默认模型放回去:自动恢复没理由永久改变新会话从哪个模型开始。
- 按请求的覆盖绑定到它被决定时的那一轮和那一步,用来恢复已经在飞行中的那一轮。之所以需要它,是因为 harness 在提示词组装时应用会话选择,而组装每步只发生一次——在重试循环之前——所以只改会话的话,要到下一轮才生效。
横幅报告的是已被证实的事,而不是已决定的事。切换先记为待定,只有一条已提交的助手消息指明了实际产出响应的模型之后,才会升级成给你看的内容:
| 作答模型是什么 | 横幅说什么 |
| 我们切换到的模型 | Model switched,带 Switch back |
| 另一个模型 | Switch did not take effect,并指明实际运行的模型 |
| 没有消息——这一轮结束了 | Switch attempted, turn not recovered |
早先的版本在决定切换时就写横幅,所以一次失败、被中止或走了第三条路线的重试,也会产出同样的「switched automatically」文案。它还把「配额耗尽」硬编码成所有失败的原因,连跟配额毫无关系的失败也算在内。现在每种原因都有自己的措辞,服务商的原始消息压成一行,悬停时显示全文——全文内联时最多有 400 个字符,横幅比周围的对话还高。
横幅上有两个控件:Switch back(只在切换确认后出现,因为它会把会话的模型切回去)和 Stop switching,后者会抑制该会话的自动切换,直到 harness 重启。
有些网关把模型的配额耗尽报成 HTTP 429,而 harness 自己的分类器会把它映射成可重试的 RATE_LIMIT——跟账号级的速率限制分不出来。两者需要相反的处理方式,所以这个插件从错误体里读出网关的数字业务码,而不是相信映射后的代码。代码表在 index.js 顶部;如果你的网关编号方式不同,就在那里调整。
为什么探测要手动
GET /v1/models 免费,但它只是目录,不是可调用清单:模型可能被列出,第一次用就失败(协议不对、没有容量、只支持图像)。因此,深度探测每个模型是了解它能否工作的唯一办法——而深度探测会花掉网关的按 key 速率预算,所以一次足够有用的扫描,也足够把用户限流在自己的网关之外。
健康状况改为按成本从低到高了解:
1. 免费的目录集合,
2. 真实请求失败时顺带记录(被动,零额外流量),
3. 用户主动发起的显式探测,按 probe.intervalMs 限流,上限为 probe.batch。
每一行的状态是现有最强证据,按此顺序:
| 状态 | 含义 | 显示时机 |
| retired | 网关不再列出该模型 | 目录读取成功,且其中没有该模型 |
| 记录的健康状况 | 探测或真实请求给出了结论 | 该 route+model 存在任何健康记录 |
| ok | 网关列出了它,且没有已知失败 | 目录读取成功,且没有健康记录 |
| unknown | 一无所知 | 该 route 没有暴露 baseURL,没有目录可比对 |
所以 ok 的意思是「已列出且未知损坏」,比「已验证可用」弱——模型可能被列出,第一次用就失败。面板把来源和状态一起显示,让这个区别始终可见:一行实际应答过就显示 usable · deep probe,没应答过就显示 usable · never checked。两者都不隐藏:对大型目录里的大多数模型来说,第二种才是诚实的标签,因为一次性验证全部,正是会把你限流到用不了的原因。
验证它
四个测试套件,都不需要特定部署就能跑:
node verify.mjs # Host half: registry, gateway, classification, fallback
node verify-client.mjs # 浏览器部分:在 jsdom 里渲染面板并点击它
node verify-browser.mjs # 在真实引擎里检查几何布局(无头 Chrome)
node verify-live.mjs # 对着正在运行的 harness 跑完整流程(需手动开启)
每个测试套件会自动选择模式:
- real —— 本机有配置档,其首个服务商路由能解析成功,且该路由的凭据可用。此时被测的设置就是实际部署的那份,网关目录也是实时读取的。正是这个模式能抓出「测试夹具与错误假设一致」的问题。
- synthetic —— 否则就用这个。本地 stub 提供一份合成目录,因此每条代码路径依然会跑到:离线、确定性、不需要凭据,也不需要内部端点。
LLMM_VERIFY_MODE=synthetic node verify.mjs # 强制指定模式
DSH_CHECKOUT=/path/to/deepseek-harness node verify.mjs
DSH_CHECKOUT 指向 harness 的检出目录,真实的 Tool registry、React 和 jsdom 都由它提供。这些依赖没有随本仓库打包;测试套件会先试几个常见位置,找不到就告诉你该设置什么。
verify.mjs 还会把守发布面:如果维护者所在公司的信息扫描器存在,而运行时文件(index.js、client.js、package.json、cordis.patch.yml、icon.svg)里含有任何能指认特定雇主或内部服务的内容,它就会失败;在这个已发布的包里扫描器并不存在,此时这道关卡会如实说明,而不是报告一个它根本没执行的通过。扫描器之所以不随包发布,是因为它自己的规则模式必须引用它要查找的那些标识符。
有些检查之所以存在,是因为实时 harness 抓到过更浅的测试套件放过的 bug:
- 测试套件会真正执行注册的工具,而不只是注册它。一个定义可以注册得很干净,却在第一次调用时抛错。
- 夹具上下文会强制执行 Cordis 的 inject 守卫,所以从 apply 自己的上下文里读 ctx.llm 会失败,和线上表现完全一致。
- 绝不要单独测一半。 两半可以在每个类型上都一致,却在一个字段名上不一致:面板曾经折叠 data.snapshot,而 Host 把同一份状态放在 value 下返回,于是 pin/favorite/hide 什么都没更新,直到用户手动刷新。只测 Host 的套件放过了它,客户端套件也放过了它——因为它的另一半是个 mock,客户端要什么字段就返回什么字段。
- verify-browser.mjs 之所以存在,是因为 jsdom 没有布局:每个 getBoundingClientRect() 都返回零,所以工具提示的位置和按钮的宽度在进程内根本量不出来。它把真实面板加载进无头 Chrome,断言设计所依赖的几何布局,然后在找不到 Chrome 系二进制文件时以退出码 0 跳过(可用 CHROME_PATH 覆盖)。
- verify-live.mjs 之所以存在,是因为其他每个套件都自己造页面,交给插件一个自己掌控的上下文——所以它们都抓不到自己没强制执行的契约。插件曾在顶层声明 slots、modelDirectories 和 sessions,然后调用 modelDirectories.directoryFor(),它内部会访问 this.ctx.remote.session。Cordis 检查的是消费者的 inject 列表,所以在实际运行的应用里,这次读取抛出 cannot get property "remote.session" without inject,选择器的插槽条目崩溃,又因为 Style 渲染在同一个子树里,编辑器的模型控件丢掉了样式表里的每一条规则。
三个测试套件全是绿的:手工搭出来的上下文都是普通对象,什么都不校验。
它打开 dsh web 打印的 URL —— 在真实引擎里,对着真实配置档、真实目录和真实编辑器 —— 没有活跃 harness 应答时以退出码 0 跳过:
DSH_LIVE_URL='http://127.0.0.1:3080/?token=...' node verify-live.mjs
DSH_LIVE_URL=http://127.0.0.1:3080/ DSH_LIVE_COOKIE='dsh-auth-...=...' node verify-live.mjs
测试套件还得学会的另外两个契约错误
这两个都上线了,在每个套件里都是绿的,都是盯着运行中的应用才发现的。值得写出来,因为每一个都改变了测试套件断言的内容。
主题不能由被测对象自己提供。 卡片用 --dsw-alias-bg-overlay 上色,插件 token 契约把它标为 "Overlay and popover background" —— 按文档这么读很合理。在应用里实测它是 #e9ecf2,中灰,比它浮出的白色编辑器还深;DSH 自己的浮层是在子层上画半透明的 --dsw-specific-menu,再加背景模糊。测试套件抓不到,因为它们自己定义了这个 token:verify-browser.mjs 里写的是 --dsw-alias-bg-overlay:#fff,已删除的 preview.mjs 也编了同样的白色。现在测试套件带上应用实际解析出的值,而且只要样式表读取了主题块里没定义的 token,verify-browser.mjs 就失败 ——一共读了 27 个。verify-live.mjs 拿 token 的真实值来断言画出来的表面。
模板字面量里的样式表可能被截断,却不报语法错误。 CSS 注释里的反引号或 ${ 会提前结束字面量。剩下的部分往往还能重新解析成 JavaScript,所以 node --check 通过,最后落到你头上的失败是浏览器拒绝导入 —— 整个客户端部分悄无声息地没了。verify-client.mjs 提取出字面量并断言它完整,这样就能抓到那种能解析但内容错的情况。
这个教训可以推广到 bug 之外:提供上下文的套件无法校验上下文。 inject 声明是与运行时的契约,只有运行时能检查它。
不重启也能看面板
DSH_LIVE_URL='http://127.0.0.1:3080/?token=…' node screenshot.mjs
从运行中的 harness 里抓取面板和模型选择器,亮色和暗色,存到 docs/ —— 这个目录在 gitignore 里,所以图片留在你自己的机器上。它是权威预览,因为它什么都不提供:它打开
dsh web 打印的 URL,拍下那里的东西。
图片没有提交,因为对真实 harness 截图,拍下的就是那台 harness 配置的路由和模型清单。那是维护者的部署,也是文本扫描唯一查不到的一类内容:路由名渲染进 PNG 后读出来只是 PNG 字节,匹配不上任何规则,于是顺利通过。四张这样的截图发出去了,才有人注意到。现在 build-public.mjs 拒绝暂存任何二进制资源,scan-company-info.mjs 把读不了的文件报为无法扫描,而不是算作干净——决定打开哪些扩展名的白名单,才是缺陷的全部所在。
这套东西替换掉了 preview.mjs,后者把插件渲染进一个手写页面,配一套手写主题。那个页面定义了 --dsw-alias-bg-overlay:#fff——这个值在应用里 token 从来不是它,应用里是 #e9ecf2——而且只定义了插件契约文档里写的那 14 个 token,样式表却读了 27 个。模拟出来是白色卡片,应用画出来是灰色卡片。自带主题的预览没法让你看到主题,而一个获批的模拟比没有模拟更糟:灰色卡片能发出去,就是因为它。
替代方案自己什么都不提供,没有 harness 应答时就以退出码 0 跳过,所以它绝不会报告一张自己没拍的图。要到达面板,它发送 settings.open 组合键——web 上是 primary+alt+Comma,不是桌面运行时用的普通 primary+Comma——然后点过设置导航,直到面板出现,不匹配任何标签,因为标签是本地化的。这是开发辅助,不是测试。
它出现在哪里
- Settings → Models → 服务商卡片:管理面板。
- 编辑器模型选择器:受管理的选择器(设置 selector.mode: native 可撤下它,拿回原封不动的自带选择器)。
- 编辑器上方:本会话模型被切换时显示的横幅。
编辑器模型选择器
模型选择器,由合成目录渲染而来
这张图是合成渲染,不是谁家 harness 的照片:preview.mjs 拿真实插件代码对着一个虚构目录渲染——假路由上的假模型——所以截图里不可能出现你还没在源码里看到的路线。想对着你的目录看,就对你自己的 harness 跑 screenshot.mjs:它驱动真实的设置 UI——发送 settings.open 组合键,找到 Models 分区——所以你拿到的是你自己主题下的你自己的目录。
面板,出自同一个渲染器:
模型面板,由同一个合成目录渲染而来
选择器打开后依次是 Pinned、Favorites、Most used,然后每条路由一个分区。置顶和收藏被抽出各自的路由,只在顶部显示一次,所以无论配置档有多少条路由、按字母排有多少模型挡在前面,收藏都只差一次点击。分区之下,每条路由仍然列出其余所有内容,减去这些行已经提上来了——每个列出的模型都只提供一次。隐藏的模型不可选,但也不会被丢弃:它们会汇集到底部的 Hidden 区域,这是菜单唯一能把它们重新提供出来的地方。
一个模型如果既被固定又被收藏,只会出现在 Pinned 下,这与回退链自身的优先级一致:固定优先于收藏。
Most used 是挣来的,不是声明出来的:它收录这个配置档最常使用的五个模型,按热度排序,这样你实际在用的模型会自己升到顶部,不用手动固定。它有数量上限,因为它是通往列表顶部的快捷方式,而不是目录的第二份副本,而且它绝不会重复固定或收藏的模型。
因为提上来的行不再位于所属路由的标题下,所以跨多个路由的区域会在每个模型旁打印路由。同一个模型 id 出现在两个路由上就是两个不同的选项,行必须说明它指的是哪一个。只停留在一个路由上的区域不打印任何东西,因为标签会在每一行上变成噪音。
行保留自己的健康圆点和原因标签,所以不可用的模型在这里也一眼可见不可用——但它仍然可选。插件对模型的判断可能过时,而从选择器里隐藏模型是这套设计唯一拒绝悄悄做的事。搜索会跨提上来的区域以及路由分组一起过滤。
用过的模型会在名字旁带一个熟练度标签(Lv.3),并且当本周与上周不同时,带一个趋势箭头,并附上 7 天计数(↑12、↓4)。计数才是承载意义的部分:单看箭头无法与多一次请求区分开。用过的模型一旦闲置一周就完全不带趋势——没有近期使用可报告,而一个平的箭头会凭空捏造一个。从未用过的模型完全不带标签:在未动过的目录上挂满 Lv.1 徽章只会是噪音而非信息,所以标签本身就是信号,说明这一行背后有历史。
Most used 在每一行下方带一个距下一级的进度条,因为它是唯一完全由使用情况构建的区域,也是你实际在比较的模型所在的地方。进度条是行底边上的一条细线,所以不占列表高度。每一行的工具提示会完整说明整个数字——等级、使用次数、达到下一级所需的计数、token 输入/输出,以及这个数字是否来自历史扫描。
一旦任何模型有使用记录,卡片底部的 Sort 控件就会对每个区域内的行排序:默认的设计顺序、By proficiency 或 By recent use。它位于滚动列表之外,所以始终可触达,并且只在有使用记录可排序时才存在——在全新的配置档上,它会提供三种看起来都一样的排序。两种使用排序回答的是不同问题,在真实配置档上它们会不一致:那个模型你最常用的,往往不是今天要找的那个。
不离开会话就能管理模型
选择器是你最常看模型的地方,所以一旦发现某个模型不能用,你立刻就想处理它。因此每一行都带一个 chevron,点开就是它的管理区块:固定、收藏、隐藏、深度探测,每项都带文字标签,而不是光秃秃的图标,下方显示的是面板详情区块展示的同一条记录——两个界面用同一个组件渲染它,所以不可能对同一个模型给出不同的描述。
在一行上,这个 chevron 就是全部代价。四个常驻图标按钮会从一个 240 到 420 像素宽的菜单里占掉大约一百像素,而模型名才是里面最重要的东西;它们还会把一个点击目标变成五个,于是「选中」时手一偏,就可能悄悄隐藏掉一个模型。chevron 只是一个字形,而且让每个操作都能用指针、键盘和触摸触达——如果做成只在悬停时出现的操作条,那就只能用指针触达了。
把管理功能放进选择器里,有两个后果需要显式处理:
- 固定不会让行在分区之间移动。 这里冻结比在面板里更重要,因为固定会把一行直接拎出它所属的路由分组,放进 Pinned。分区归属在第一次固定或收藏时冻结,并在下次打开菜单时确定下来;而这一行报告的状态保持实时,所以按钮会立刻亮起。冻结行自身的标志位——也就是最初的做法——连固定指示器一起冻住了,菜单便声称这次点击什么都没做。
- 隐藏不是单向门。 隐藏的模型会离开菜单,这符合隐藏的本意,但菜单会把隐藏的行收集到底部的 Hidden 分区,并在同一个区块里提供 Unhide。把隐藏的行从目录里彻底过滤掉——也就是原来的行为——会让在选择器里隐藏变成永久操作,除非你去设置里改。
隐藏的模型仍会列在面板里,只是变暗,在面板里用同样的操作就能把它恢复。
选择器的样式沿用面板自己的一套词汇,而不是另做一套设计:同样的行框(内边距、圆角、悬停背景和悬停边框),同样的变暗效果用于弱化的行,同样的焦点环,同样的 .llmm-chip 用于路由,和面板里表示「also on」时一样,分区标题用次级标签色。品牌色仍然只留给状态——面板里的活动控件和选择器里的当前行。两套测试逐条断言这些属性,所以两个界面不会再各自跑偏。
面板控件
行操作是纯图标按钮,标签在悬停和键盘聚焦时显示,这样四个控件能并排放在模型名旁边,而不会让整行换行:固定、收藏、隐藏、探测。每个都带 aria-label,所以屏幕阅读器也能读出来。头部的两个操作(重新检查网关、深度探测全部)用同样的处理方式。
固定不会移动行
默认排序会把固定和收藏的行排在前面,所以每次切换都重新排序,会把你刚点击的那一行移到列表顶部,还顺带带走你的滚动位置:向下滚动到 80 个模型里的第 40 个,固定它,结果你看到的是列表的另一部分。现在切换固定、收藏或隐藏,只改变该行的状态,不改变它的位置。
排序在第一次切换时冻结,并在你预期列表会稳定的边界处应用:
- 折叠或展开列表,
- 更改排序,
- 重新检查网关。
有未完成的变更时,面板会说明这一点,因为冻结的列表否则和一次悄悄什么都没做的固定无法区分。没有冻结时,光标下的行会移动;有了冻结,唯一移动的东西就是固定图标。
行承载决定;详情块承载记录
行展示做选择所需的信息——健康点、名称、状态、等级、近期表单计数,以及一行原因。Host 知道的一切,在可读的字号下塞不进一行,所以每行都有一个折叠箭头,在它下方打开详情块。折叠箭头和模型名都能打开它;折叠箭头是携带 aria-expanded 和标签的那个,所以不用为每行增加第二个 tab 停靠点,就能从键盘访问该块。
该块完整列出记录:状态、完整原因、检查者和检查时间、路由的网关目录是否仍列出它、等级和到下一级的距离、进度条、使用次数、最近七天和此前七天、热度、首次和最后使用、token 输入/输出,以及这些数字来自历史扫描还是实时计数。
悬停提示是另一种方案,也是更弱的那种:它需要指针,在触摸屏上读不了,也没法在第二行旁边保持打开来对比。
密度,以及为什么原因排在前面
原因是关于被标记模型信息量最大的东西,也是窄行最先截断的东西。它被渲染在最前面——来源和时长跟在后面,因为状态点和状态标签已经说了它们要说的大部分内容。把原因放在后面,意味着省略号吃掉的正是值得读的那部分。
提供两种密度,选择会被记住:
- 紧凑(默认)每行保持一行,把剩余空间给原因。
- 舒适给原因第二行,换行到两行后才截断。
有八十个模型时,紧凑是让列表保持可扫读的方式;舒适适合你正在调查其中某一个的时候。
提示渲染在面板根部,而不是行内部,因为行列表是滚动容器,会把它裁掉;它相对视口定位,并做了钳制,这样靠近边缘的按钮不会把标签推出屏幕。
点击操作会在同一帧更新该行(标志先在本地应用)而 Host 的权威快照随后会替换它。请求失败时,该行会回滚,所以面板绝不会一直声称 Host 已拒绝的状态。行操作始终可见,只是透明度降低,而不是仅在悬停时出现,因为触摸设备没有悬停。
Every mutating action returns the new state under a top-level snapshot; that is the
contract that makes the optimistic update safe.
Proficiency and recent use
The panel's top card is Most used: the three models this route is actually worked with,
each with its level, a bar to the next level, and the number of uses. Below the counts, a
sort control switches the list between the default order, proficiency, and recent use.
Both numbers come from a local ledger, and the distinction between them is the point:
- Level is earned from the lifetime count and never decreases. It answers "how much have
I used this model", which is why it does not fall when you spend a week elsewhere.
- Heat decays with a 14-day half-life, so it answers "what am I reaching for now". Both
Most used sections — the panel's card and the picker's — rank by heat by default, not by the lifetime total, so the oldest model does not win forever.
The level steps are 0, 10, 30, 80, 200, 500 and 1200 uses, giving levels 1–7. Tokens are
recorded and displayed but deliberately do not score: a handful of enormous requests should
not read as expertise.
The ledger is counted from committed assistant messages in this profile's own session
logs. Nothing is sent anywhere and no request is made to build it. Concretely, three
choices keep the numbers honest:
- Counting from committed messages rather than from clicks on the menu means the ledger
reflects work actually done. Picking a model you then cancel does not count.
- A retried request is recorded as an attempt, never as a use. A flaky gateway would
otherwise multiply one answer into several levels of proficiency. (This is also why the
ledger does not hook the streaming event: that one wraps every call, including retries and
replays.)
- A replayed session event is deduplicated, so resuming a session does not double count it.
The ledger starts empty, but your history does not. On startup the plugin folds the session
logs already on disk into the counters, so a model you have been using for months starts at
its real level instead of level 1. This runs in the background and is bounded: it scans for up
to 20 seconds, yields, and resumes on the next pass, because replaying a large corpus is
disk-heavy and must not compete with the live session you are in. Each session it has already
counted is marked, so resuming never double counts.
When it finds an empty or partial ledger, the panel says where the pass stands — waiting for the session service, reading your logs, how many responses it counted from how many sessions, or why it stopped. It says so on failure too, deliberately: an earlier version sampled the session service once at startup, and on a host whose service registered a moment later it answered "service not available" and wrote nothing. That empty ledger was indistinguishable from a correct one, and the feature looked dead while reporting nothing at all. Scan history at the bottom of the panel starts a pass on demand.
Because the counters are local and describe your own habits, deleting them does not require
hunting for a file: Clear usage at the bottom of the panel, or the reset-usage tool
action, for one model, one route, or the whole profile. Set usage.enabled: false to turn
the ledger off entirely.
Storage
One atomically-written JSON document per route under
$DSH_HOME/storages/llm-manage/<route>.json, holding favorites, pins, hidden,
recent and health (status, source, detail, scope, at), plus the usage
counters. Deleting the file resets that route's overlay.
Usage is the one part of that document written in batches rather than per event — counters are debounced (usage.flushMs) because a session can commit many messages in a row, and the panel flushes before it reads. The backfill watermark lives in a separate __usage__.json in the same directory.
Agent tool
The llm_models tool exposes the same operations: snapshot, pin, unpin, favorite,
unfavorite, hide, unhide, probe, sync, usage, seed-usage, reset-usage,
diagnostics. snapshot and diagnostics need no extra arguments; sync needs route;
probe and the overlay actions need both route and model. usage reports the ledger,
seed-usage re-runs the historical scan, and reset-usage deletes counters (all of them
when given no route). The browser half reads the same data over POST /api/llm-manage.
Applying a change after install
The profile installs this package as a symlink, so file edits land in the running
profile immediately. The two halves then behave differently.
index.js (Host half) needs a full harness restart. The host HMR watcher skips
node_modules, where the install symlink lives, and its watch root is the profile
directory — while the real file resolves outside it. A marker written into index.js was still absent from the running Host's responses minutes later, against a listener process started the previous evening.
client.js (Client half) reloads on its own. The client HMR watcher is a stat poll
(default 500 ms) over every row in the composed graph — no node_modules exclusion — and it re-reads the bundle from disk. Touching client.js produces a rebuilt frame on /plugins/events, and the revision the server then advertises matches one computed from the new file's mtime/ctime/size.
Enabling or disabling the bundle does not bust the ESM module cache for either half.
Known limits
- Deep probing assumes the OpenAI-compatible /chat/completions shape. A route speaking
another protocol is reported as probed-with-this-shape, not as protocol-aware.
- Protocol mismatches are labelled, not fixed. A model's protocol cannot be set
per-model: api is a route-level setting. The supported workaround is registering the
same baseURL as a second route with api: anthropic-messages — which only helps if your gateway actually serves that endpoint. If it does not, the model is reported as protocol-incompatible rather than silently "fixed".
- Passive learning needs a real failed request to observe a reason.
- The usage ledger reads session logs through the host's session service. A host that does
not expose it still counts live traffic, but the historical backfill reports itself as
unavailable instead of silently reporting zero history.
- The ledger is per profile, not per project: sessions from different working directories
share one set of counters, because the profile is what owns the model configuration.
- A message that arrives before the plugin is loaded is not counted live. The startup
backfill covers it on the next run, unless usage.seed is off.
- The quota fallback needs the failing model's id, which it takes from the last
agent/request it observed for that session.
- The managed picker shadows the shipped picker at priority: -1 and re-supplies its
inject face. That face is a per-registration business contract, so a future change to
the shipped picker's face needs this plugin updated in step. selector.mode: native
withdraws the shadow without a restart.
Licence
MIT — see LICENSE.预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
PhilYue/dsh-llm-manage
置顶收藏模型并自动切换故障路由
相关插件 · 其他
模型破甲清洗HunYiSage/dsh-purge
⚔️ DeepSeek Harness 网络安全红队工具dsh-purge
★ 16,993已汉化DeepSeek 客户端
73
女娲蒸馏alchaincyf/nuwa-skill
输入名人名字,提炼其思维模型与决策方式供你调用
★ 37,308已汉化DSH
72
逆向分析桥接Fanqi89/dsh-rea
REA(Reverse Engineer Anything)MCP 桥接,用来把 REA 逆向工具接入 DSH 插件
★ 51已汉化DSH
72
语音女友beiyege-01/dsh-voice-ai-girlfriend
语音 AI 女友Voice AI girlfriend for DeepSeek Harness:Whisper…
★ 888已汉化DSH
71.6
红队越狱测试套件Minglink/dsh-infinite-gen-5
DeepSeek v4.1 flash 逆向工具无限五代 求 Star 收藏 ⭐欢迎大家提交项目的改进
★ 3,759已汉化DSH
71.4
协同办公空间dream-num/univer-workspace
一个让人类与 AI Agent 共同创作、协作和审阅的开源 Office 工作空间。
★ 2,270已汉化DSH
70.8
这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。