npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-model-rotator

v0.2.14

Published

Model auto-ranking and transparent failover for DeepSeek Harness: ranks discovered models by measured connectivity and speed, switches to the next candidate inside the same turn when the current one is unusable, and ships a grouped model list with per-cha

Readme

dsh-model-rotator · 模型自动排序与故障转移

简体中文 | English | 更新说明

给 DeepSeek Harness 装上一个「换模型的自动驾驶」:当前模型撞上额度墙、密钥失效、被限流,或者网络刚好抖动时,插件会在同一轮对话之内换到下一个最合适的已配置模型,继续把这轮回答完——用户看到的仍然是一条普通回复,不会中途断掉。

本机实测规模:906 个 provider/model,其中 384 个在轮转池里,跨 48 个渠道(24 个对话渠道 + 24 个识图渠道)。

它到底做了什么

一轮对话失败时,插件依次做这几件事:

  1. 摸清家底:调 ctx.llm.listProviders() 加各适配器的 listModels(),枚举本机所有能拨通的 provider/model。
  2. 剔除非对话模型:向量/重排、语音合成与识别、OCR、图像与视频生成、内容审核、评判模型等,一律不进池子。理由很直接——拨它们只可能让本轮失败,还会白占一个本该留给真模型的冷却位。
  3. 跳过识图孪生:dsh-vision-router 会给每个渠道套一层 <provider>-vision 的「识图外壳」,它的作用是给同一个模型加读图能力。也就是说,它是同一个模型的第二条通路,不是第二个模型。把文本轮次切过去,等于让回答绕道图像管线;更糟的是「优先选同款模型」这条规则会把孪生排到最前面。所以这些路由永不作候选(注意:只是名字里带 vision 的真对话模型不受影响,仍然照常参与)。
  4. 排序:按实测连通率(拉普拉斯平滑的成功率)、速度(TTFT 中位数与吞吐)、以及当前冷却状态排出一张候选表。你正在用的那个模型、或者它在别的渠道上的近亲,会拿到加权——毕竟你选它自有道理,多半只是这条渠道坏了。
  5. 先重试,再判死:渠道瞬时抽风很常见(限流毛刺、连接被重置、信道过载),所以单次失败不算故障。同一个模型还会按 retriesPerModel 再试几次(默认 2 次,可设 0–5),每次间隔 retryBackoffMs(默认 300ms,之后翻倍)。重试不等于切换——它不问你、也不改任何状态。
  6. 真的换:在 llm/stream 瀑布上接管。第一次尝试走宿主原路径;只有当这次失败一个字的正文都没产出时,才放弃这次尝试、关掉它的上游流,用全新的 ctx.llm.stream() 重新下发下一候选。

两条边界,是整件事的关键

已经出过正文就不再重试。 一旦有任何正文分片吐给用户,这次尝试就锁死了——再试一遍会让用户看到重复的文字。所以「可重试窗口」精确等于「零内容窗口」。

不是所有分片都算内容。 reasoning-delta 不算:重试时宿主会把思考过程连同那一轮一起折叠掉,这正是「思考死循环」「思考强度不被支持」这类失败可以安全重试的原因。标记分片(block-start,或携带思考块的 block-end)也不算。这里踩过坑:早先版本把一个 block-start 当成内容,于是窗口刚开就被关上,紧接着的一个普通错误被当成终局——一个明明活着的模型看起来像死了。

被取消的轮次不折腾。 用户点了停止、超时到点、客户端断开——走链立即停下,不会把候选浪费在没人再看的回复上。

自动调用不当作对话轮次

压缩摘要和会话标题也走同一条 llm/stream 接缝,但它们不是用户轮次。这类调用的拒绝,通常说的是那条专用通道的状况(比如代理回一句「压缩归 bili 管」),此时换一个 provider 去干这段管道活是错误反应:既花掉一个真模型去做没人读的工作,又是虚假切换的头号来源。所以凡是带 purpose: "compaction" 或 purpose: "session-title" 的调用,一律原样透传,错误也透传;没带标记的普通轮次照旧故障转移。

换了模型会告诉你

切换本来是无声的——回复只是换了个模型回来,你根本看不出来。现在它记录在对话顶部的 「模型切换」页签里(与 对话 / 轨迹 / 上下文 并列),逐条列出时间、从哪个模型到哪个模型、以及原因:

14:07:31  modelscope/deepseek-ai/DeepSeek-V4.1-Flash → trae/deepseek-v4.1-flash   (quota)

页签的数据来自插件自己的记录(GET /_dsh/model-rotator/switches?session=<会话 id>),不占用对话上下文。页签所在的会话由宿主注入:注册时声明 inject: (sessionId) => ({ sessionId }),组件从 props.sessionId 取——与内置的 对话 / 轨迹 页签同一条契约(0.2.11 前这里读的是 sessions.list 上一个并不存在的字段,页签因此认不出自己在哪个对话里)。记录按会话归属:宿主不向插件路由注入会话,所以会话 id 在切换发生处当场取定(ctx.get('agents').currentInitiator() 返回发起该次调用的 agent),而不是等轮次边界——边界是进程级的,谁能停轮次谁就能把别人的记录整批带走。若某次调用确实没有活动轮次(插件路由、宿主未挂 agents),记录照写、会话留空,空会话只在「全量视图」里可见。

当本对话名下一条记录都没有、而记录总数不为零时,页签会改为展示全部会话的记录,并配一行提示说明这是回退视图——避免「有历史却显示空」。

若你仍想在对话正文里看到那一行(旧行为),把 noticeSwitch 打开即可:

模型已切换:硅基流动/deepseek-4.1 → Loomy/deepseek-4.1  (rate-limit)

已知失败类型时会带上括号,比如 (quota)、(rate-limit)。

| 项目 | 做法 | |------|------| | 默认 | 不贴(noticeSwitch: false)。记录在页签里,通知行是可选项 | | 形态 | 复用宿主自带的通知机制:一条 user/message,source 是 { kind: 'model-rotator/switch', form: 'notice', summary }。对话界面把它渲染成一条上下文行,而不是你发出去的消息 | | 时机 | 通知行放在轮次边界,经 agent/turn-stopping + agent.steer 投递。切换已经发生、回答已经在流,若在轮次中途插一行,就会插在它所描述的回复正中间(记录本身不在此刻写,见上文) | | 范围 | 只作用于根对话。子代理记录里的切换属于别人的日志,不打扰你 | | 打开 | noticeSwitch: true,或设置页里的开关 |

事件类型刻意没自创。持久化读取器会拒绝解释含有未知类型的事件日志(dsh-session 的 KNOWN_SESSION_EVENT_TYPES),自创一个类型会让带它的日志直接加载失败。user/message 是已知类型,notice 是客户端已经理解的形式——宿主自己那条「模型已切换」用的就是这一对。

渠道名优先用宿主发布的 provider displayName(siliconflow 显示成「硅基流动」、modelscope 显示成「魔塔」、buddy 显示成 «CodeBuddy (腾讯)»),没有标签的路由才回落到 id;两者不同时会写成「标签 (id)」的形式。

换之前先问你

故障转移会替换你亲手选的模型,而且可以不声不响——所以默认先问。当前模型零内容失败时,插件经宿主的人机交互接缝(ctx.userQuestions.askTimed())在对话里抛一个问题,给出两个选项:切换 和 保持当前模型。

| 你的回答 | 结果 | |----------|------| | 切换(或任何不算拒绝的回答) | 下一候选接手本轮 | | 保持当前模型(或直接打出「不/否/取消/拒绝/no/cancel/stop」之类) | 不再拨任何候选,本轮以原始错误结束 | | 窗口内没回 | 继续切换——这是一次限时征询,不是否决权 |

提问每轮只问一次:第一次批准即覆盖这一轮剩下的走链,所以一串坏模型不会变成一串一模一样的卡片。默认等 20 秒,可配。

两类失败不问了。 额度耗尽(quota)与提供方不支持(unsupported)是确定性死路:余额不会在 10 秒里回来,不支持的请求下一次仍会被同样拒绝。这种时候「保持当前模型」并不能让模型继续工作,只会让本轮失败——白白多读一张卡片、再等一次超时。所以这两类直接切换,页签里的记录照常写明原因。鉴权(auth)仍然问:403 常常来自突发限速的代理,等一会儿再试是真实的选择而不是形式。

整个机制刻意是「失败即放行」的:接缝不存在、提问抛错、本轮没有活着的根代理(被委托的子代理没人可答)、或者等超时了——走链都会在未确认的情况下继续。无人值守的宿主绝不该卡在一张没人看的卡片上。

失败类型决定后事怎么办

失败不只是一个字符串,它会被归类,而类别决定这个模型要「歇」多久。

额度墙和限流是两码事。 429 insufficient balance 和 429 too many requests 是同一个 HTTP 状态码,但一个几秒后自己就活了,另一个得等额度重置。插件会从消息正文里读语义——insufficient quota、out of credits, please top up、余额不足,请充值、额度已用尽、402 Payment Required 这类——然后区别对待:

| 类型 | 冷却多久 | 要不要先重试 | |------|----------|--------------| | quota(额度耗尽) | 歇到次日 quotaCooldownHour(默认本地 00:00),且不少于 quotaCooldownMs | 不重试——两次下发之间余额不会自己长回来 | | auth(鉴权失败) | 常规指数退避 | 重试——被突发限速代理解出来的 403 确实会好 | | unsupported(请求形态不被支持) | 常规指数退避 | 不重试——同一个 provider 对同一个请求只会给同样的拒绝 | | rate-limit(限流) | 常规指数退避 | 重试 | | server / network(服务端/网络) | 常规指数退避 | 重试 | | 未分类 | 常规指数退避 | 重试 |

unsupported 指的是请求形态本身被拒,比如「provider X model Y does not support reasoning effort high」「unsupported model」。它在读请求之前就判定了,重试只会逐字复现同一个拒绝——跳过重试,才能把一次会话级不匹配压成一次切换,而不是三次。

鉴权失败会先于裸 429 被归类,因为等待永远修不好一个密钥:对 429 invalid api key 来说,诚实的名字是 auth 而不是 rate-limit。这个区分只用于记录,不改变处置——两者退避一样;只有 quota 会改变策略。

冷却的下限是「次日 quotaCooldownHour」。23:50 撞上的额度墙仍要付满 quotaCooldownMs,所以它会在次日 11:50 回来,而不是十分钟后的午夜。

model_rotator_status 会把这些都摊开给你看:排名表把这类行标成 QUOTA-REST(并附剩余小时数)而不是 COOLDOWN,type 列写明类别,last failure seen 行列出每个模型最近一次错误及其类型。

实测状态——计数器、最近错误与类型、冷却到期时间——跨进程存活。理由是现实的:「刚被告知余额用完」之后做的那次重启,不该把冷却一并抹掉。它写在 %DSH_HOME%\model-rotator\stats.json(或者 config.json 所在目录),去抖落盘,首次使用时读回一次。延迟采样则刻意只留在内存——它们是 20 个样本的滑动窗口,陈旧的窗口比短的更糟。statsPersistTtlMs 限制读回文件的年龄(默认 24 小时;设 0 表示完全不用这个文件)。

它不碰什么

它从不读、写或改写模型/provider 配置。 不碰 cordis.patch.yml、不碰 agent-default-model、不碰 llm-pi-ai、也不碰任何 provider 注册。它只在运行时观察流式瀑布。如果你的 provider 配置被人动过,那一定不是它干的。

哪些模型会被过滤掉

宿主适配器在交给 listModels() 之前,会把目录自带的 wire: 'chat' 标记剥掉,所以没法从返回行上读能力。名字匹配是唯一可用的信号,而且刻意写得很窄——模式绝不能误伤只是名字里提到某种模态的对话模型(Qwen3-VL-8B-Instruct 是对话模型,保留;Z-Image-Turbo 不是,剔除)。

过滤器覆盖向量与排序、语音、图像生成、文档解析、安全分类器这几大族,普通 provider 与 -vision 变体都算。它在发现阶段就跑,所以插件报的 discovered 已经是过滤后的池子。也因此,一台机器的数字完全取决于它配了哪些 provider——别从本文档抄数字,直接问实际安装:

model_rotator_status      # 看 "discovered" 一行,那就是过滤后的候选数

三个工具

| 工具 | 干什么 | |------|--------| | model_rotator_status | 只读:启用状态、配置路径、已发现池、实测排名,附带「当前生效设置来自哪一层」。 | | model_rotator_benchmark | 用最小真实请求探测候选,刷新 TTFT/吞吐与冷却状态(有次数上限,会花掉一点额度)。 | | model_rotator_select | 固定或取消固定转移顺序的队首;队尾仍然由排名决定。 |

状态工具还会报出两个手动清单和识图规则,这样「目录里看得见、排名里看不见」的模型,不必去读代码就能解释清楚:

  disabled models  : workbuddy/hy3
  vision twins     : never a candidate (the dsh-vision-router mode wraps a model, it is not a second one)
  model list route : /_dsh/model-rotator/models (GET the grouped list; POST {key, enabled} to switch one off or on)

配置

可选,路径 %DSH_HOME%\model-rotator\config.json,改完即热加载。每个字段都有安全默认值,所以完全不建这个文件也能跑。

{
  "enabled": true,
  "allowImageFailover": false,
  "maxCandidates": 4,
  "cooldownBaseMs": 60000,
  "maxCooldownMs": 1800000,
  "quotaCooldownMs": 43200000,
  "quotaCooldownHour": 0,
  "statsPersistTtlMs": 86400000,
  "confirmSwitch": true,
  "confirmTimeoutMs": 20000,
  "adoptSwitchedModel": true,
  "retriesPerModel": 2,
  "retryBackoffMs": 300,
  "preferSameModel": true,
  "weights": { "connectivity": 0.5, "speed": 0.3 },
  "excludeProviders": [],
  "excludeModels": [],
  "excludePatterns": [],
  "disabledModels": [],
  "enabledModels": [],
  "disabledProviders": [],
  "pinned": ""
}
  • enabled —— 总开关。
  • allowImageFailover —— 图像轮次默认跳过。理由:纯文本备选读不了图,在那里转移只会得到「错误答案」而不是「缺失答案」。
  • maxCandidates —— 每轮最多试几个(硬上限 8)。
  • cooldownBaseMs / maxCooldownMs —— 指数退避的上下界(base * 2^(n-1),截断到 max)。
  • quotaCooldownMs —— 额度墙之后的最短冷却(默认 12 小时,上限 26 小时)。从失败那一刻起算,不按整点取整。
  • quotaCooldownHour —— 本地小时(0–23),耗尽的模型可以在这时刻再被尝试(默认 0 = 次日零点)。冷却总是走到次日的那个时刻,所以 00:05 撞的墙不会当晚就复活。
  • statsPersistTtlMs —— 实测状态跨重启存活多久(默认 24 小时,上限 30 天;0 = 不持久化)。
  • confirmSwitch —— 离开失败模型前先在对话里问一句(默认 true,见上文)。
  • confirmTimeoutMs —— 那个问题等多久之后仍然照切(默认 20000;限 1 秒 … 10 分钟)。
  • adoptSwitchedModel —— 切换之后,把真正作答的模型设成本会话的模型,让会话头部显示实际在用的模型(默认 true)。仅限会话内:不动 provider 配置、不写任何默认值;宿主自己那条「模型已切换」通告也会随之报出正确的组合。
  • retriesPerModel —— 认定失败之前同一模型的额外尝试次数(默认 2,硬上限 5)。重试不需要确认,因为它不换模型。
  • retryBackoffMs —— 每次重试前的停顿(默认 300;第 n 次重试等 n 倍,封顶 10 秒)。
  • preferSameModel —— 优先排同款模型在别的渠道上,其次近亲,最后才按实测排名(默认 true)。
  • windowGuard —— 上下文窗口装不下「当前对话 + 本轮回复」的故障切换候选直接跳过(默认 true)。窗口取自拥有该路由的适配器(resolveModelInfo),不是目录声明:profile 可以声明窗口,但只有实际发起请求的一方知道装不装得下。适配器没公布窗口的候选永不跳过;宿主报不出对话压力的场合整个守卫不生效;与当前路由同窗口的候选也豁免——守卫管的是「换过去」。被扣下的候选在模型列表里标记 window,status 工具会报出它依据的压力读数。
  • noticeSwitch —— 是否额外在对话里贴那一行切换通知(默认 false;不贴时切换记录仍在「模型切换」页签里)。
  • weights —— 评分里连通率与速度的配比。
  • excludeProviders / excludeModels —— 把指定路由排除出池子。
  • excludePatterns —— 追加到非对话黑名单的正则;写错的正则会被跳过,不会致命。
  • disabledModels —— 在模型列表里逐行关掉的模型。每条是 "provider/model",也可以只写裸模型 id(那样会命中所有提供该模型的渠道)。行级条目压过渠道级规则。
  • disabledProviders —— 在渠道表头上点「关闭本渠道」关掉的整条渠道,一条记录一个 provider id。关闭渠道时会一并清掉该渠道内遗留的逐行 enabledModels,免得按钮被静默撤销;此后单独开启某一行会写一条新记录,照样生效。
  • enabledModels —— 相反方向。也是当某个 wrapper 命名不符合 <provider>-vision 约定时,把识图孪生单独放回轮转的唯一方式。由同一个开关写入。
  • pinned —— 转移顺序的优先队首。

设置页与模型列表

插件两半都齐:服务端有给 model-rotator 命名空间用的 Config schema,浏览器端有 client.js(经 dsh.client 与 exports["./client"] 声明),同一个组件渲染到宿主的两个位置:

  • 设置 → 模型自动切换:设置导航里的独立入口(settings.section,id model-rotator,order 20)。
  • 插件面板里的 model-rotator 卡片(plugins.item),卡片正文是同一套表单。

两处都只在宿主真的提供该命名空间时出现。页面上只放「个性化安全」的字段:

| 字段 | 含义 | |------|------| | enabled | 总开关 | | allowImageFailover | 图像轮次也参与转移 | | maxCandidates | 一轮最多试几个模型 | | cooldownBaseMs | 退避基数 | | maxCooldownMs | 退避上限 | | quotaCooldownMs | 额度墙后的最短冷却 | | quotaCooldownHour | 耗尽模型归来的小时点 | | statsPersistTtlMs | 实测数据跨重启存活多久 | | confirmSwitch | 切换前先问 | | confirmTimeoutMs | 问句等待时长 | | adoptSwitchedModel | 把切换后的模型落到本会话 | | retriesPerModel | 认定失败前的尝试次数 | | retryBackoffMs | 重试之间的停顿 | | preferSameModel | 优先同款模型换渠道 |

weights.connectivity / weights.speed 不在页面上:表单模型一次只承载顶层键,嵌套值留在 config.json。需要手工或工具批量编辑的长清单——excludeProviders、excludeModels、excludePatterns、pinned——也留在那里。

模型列表

表单下方,同一页渲染出模型列表:每一个已发现的 provider/model,带健康状态和自己那一行的开关;另外每条渠道的表头上都有一个开关,一键关掉该 provider 背后的所有模型。两级是刻意设计的:密钥或端点整条死掉时,「关整条渠道」才是你要的那一下;而某一个模型坏了、邻居还好着时,你需要的又是行级开关。

列表分成两段,因为它们回答的是两个不同的问题:

| 分区 | 内容 | |------|------| | 对话渠道 | 真正参与自动切换的候选 | | 识图模式(vision-router 创建) | <provider>-vision 孪生,每条都标注它包裹的是哪个渠道(amd-vision → amd) |

折叠分三级、互不干扰:分区、分区内的渠道、整张表。对话渠道默认展开,识图分区默认收起——因为孪生数量和其余模型一样多(本机最近一次实测:906 行里 453 行是孪生),而且它们永不作候选,收起来才让页面可读。全部展开 / 全部收起 两个按钮一次操作所有分区与渠道。

真实长相(本机实测):

▼ 对话渠道                    453 个 · 可用 361 · 冷却 23 · 关闭 69
    ▼ 硅基流动 (siliconflow)     65 个 · 可用 63 · 冷却 2 · 关闭 0   [关闭本渠道]
        ● deepseek-v4.1-flash   冷却中   retry in 511m · rate-limit      [关闭]
    ▼ AMD (amd)                  8 个 · 可用 5 · 冷却 3 · 关闭 0   [关闭本渠道]
▶ 识图模式(vision-router 创建)  453 个 · 可用 0 · 冷却 0 · 关闭 453

| 列 | 含义 | |----|------| | 圆点 + 标签 | 可用(没在冷却、没有失败记录)、未测(还没试过)、冷却中、额度耗尽(歇到次日边界)、异常(上次失败)、已关闭 | | 明细 | TTFT 与成功/尝试次数,或剩余冷却,或这一行为什么不在轮转里 | | 开关 | 关闭 / 启用 |

一行「不在轮转」的原因会写明,而不是笼统灰掉:manual(在这里手动关的)、vision(识图孪生,默认排除但开关可以推翻)、provider / excluded(来自 config.json)、non-chat(非对话模型族)、combo(虚拟路由)。只有前两种能在页面上开关,其余要么是结构性的,要么归文件管。

渠道级开关的几条规则(都是实测踩出来的):

  1. 表头按钮的标签取自渠道自己的开关状态,而不是「有没有某一行在轮转」——整条渠道的行因为行级原因都关着,它依然是开启状态;从表头关掉的渠道,必须给出能重新打开它的动作。
  2. 已关闭的渠道按钮显示启用本渠道,同时下面的行级开关仍然可用,方便你把某一个模型单独放回,而不必整条渠道重开。
  3. 关闭渠道会清掉渠道内遗留的逐行覆盖(这样按钮才说到做到);而你在这之后单独打开某一行,那条新记录照样赢(新的、明确的表态压过旧的、笼统的表态)。
  4. 识图渠道不提供渠道级开关。它的行是被孪生规则挡在外面的,渠道开关撬不动它们——放个按钮在那里,等于写了一笔改不动任何东西的配置却看起来像个承诺。请用行级开关。

列表的数据不走设置表单,走 HTTP:路由 /_dsh/model-rotator/models。

  • GET 返回分组列表:groups 是两个分区,channels 每个 provider 一条(带 kind 和孪生所包裹的 sourceChannel),models 是所有行。分区和渠道带同一套 models/ok/resting/off 计数器,所以一种行形状就能渲染两级,折叠时也照样报得出状态。
  • POST {key, enabled, scope} 翻一个开关并回传刷新后的列表。scope: "provider" 用于渠道表头;省略或 "model" 用于单行。

表单承载你的策略(试几个、等多久),列表是名册——provider 一重新发布它就变;而它的每一行是逐个直接写进 config.json 的,所以刷新不会把你正打到一半的数字抹掉。路由通过 ctx.inject(['webServer'], …) 注册,因为宿主是在插件加载之后才发布 web 服务器的;它和其他所有非公开路径一样,由宿主统一做鉴权。

两层配置合并规则是 默认值 < config.json < 设置页。设置页只在它真正拥有的键上获胜:它没设过的键,值仍然来自文件。model_rotator_select 从不把「页面拥有的键」写回 config.json,所以一个陈旧的文件值永远不会伪装成权威。

命名空间就是插件的 cordis id(model-rotator)。只有服务端一半、没有客户端一半,页面上什么都看不到——这也是两半必须一起发的原因。Config 导出需要 schemastery 在宿主进程内,所以插件在 vendor/node_modules/ 里自带了副本,并在裸导入解析不到时按绝对路径加载它——这样在「manifest link 指向 profile 的 node_modules 之外」的安装形态上,页面也照样出得来。

model_rotator_status 的最后有一块 in force right now,逐行列出每个生效键的合并值与它来自哪一层([default] / [config.json] / [settings page]),不用你自己读两个文件在脑子里做合并。再后面几行给出实时画面:ask channel(切换前的问句到底能不能送达你,不能的话为什么)、adopt on switch、model in use now(从本会话请求头读)、session switch、failure verdict、switch order。

安装

从 npm 安装(推荐):

dsh plugin --profile desktop add dsh-model-rotator

从 GitHub 安装:

dsh plugin --profile desktop add github:chimneyQ/dsh-model-rotator

如果你手上是本地目录,同一条命令换成路径即可:

dsh plugin --profile desktop add <本目录路径>

包内声明了 dsh.bundle.patch,所以它自带的 cordis.patch.yml 会自动接线;它没有任何需要构建的依赖。装完需要完整重启应用才会加载。

确认宿主是否真的把它接上了(首次安装失败可能只写下依赖、却没把插件加入 dsh.profile.bundles):检查 <DSH_HOME>/profiles/desktop/package.json 的 dsh.profile.bundles 里是否列出 dsh-model-rotator。

测试

node test-offline.mjs     # 故障转移 + 确认闸门,跑在 stub 宿手上
node test-settings.mjs    # 服务端设置契约,对着宿主自己的规则
node test-client.mjs      # 浏览器端:加载器形态、卡片注册、字段
node release-check.mjs    # 发布自检:manifest / files / exports / 收录条目

四条都不需要网络、也不需要跑着 DSH。离线套件覆盖「失败后重试」「一旦出正文就不再重试」「图像轮次跳过」「所有候选都失败」「冷却记账」「取消信号透传」「经真实瀑布的非对话过滤」「工具注册」「确认闸门」(提问关闭 / 批准 / 按钮拒绝 / 文字拒绝 / 超时 / 每轮一问 / 各条失败即放行路径)。客户端套件用真实的 hooks 运行时渲染名册(状态、副作用、记忆化回调、重渲染)并按下读者会按的按钮,断言返回的东西——两个分区、推导出来的计数、每一级折叠的收放、以及列表只被拉取一次。正则只能证明某个功能被提到过,只有渲染才能证明它真的能用;早先一个重复拉取的循环,正是被这套渲染测出来的。

许可

MIT