dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 宿主插件:注册 auto 模型,按有序回退链在多条 provider+model 间静默切换 (DeepSeek Harness)
给 DSH 加一个 auto 模型:模型选择器里多出一个 Auto 分组,组内一条 auto。
选它以后,请求依次尝试多条「provider + model」,某条失败会先按官方同款策略在该路由内原地重试
(瞬时错误,默认最多 5 次),重试耗尽才静默切下一条 —— 对上层(agent loop / 会话日志 /
压缩链路)它就是一个普通模型。用来把多个模型订阅"合并"成一个入口。
模型选择器给 auto 两档推理等级(0.10.0 起):「Default」(插件一个档位都不写,由宿主/上游
自己的默认决定)与「max」(逐跳取该路由能给的最高档,= 0.9.x 的旧行为)。细节与前提见
§3「推理档位:默认 / max 两档(0.10.0 起)」。
0.8.0 起,顺序默认由插件按订阅额度自动决定:有月度重置时刻的订阅按「越早重置越靠前」排,
查不到重置时刻的按你写的顺序,永不过期的按量兜底永远最后;某一跳因窗口额度耗尽被拒
(错误码 QUOTA,或报文自带窗口语义 —— 0.9.1 起判据放宽:Command Code 那种
You've reached your weekly usage limit for your plan. Your limit resets at … 也算,见 §2)
时,插件会补查该来源的额度接口,确认后把它冷却到对应档位的重置时刻
(面板上写明是哪一档,例如「月度已耗尽,10-24 00:00 恢复」),期间不再尝试,到点自动恢复。
0.11.0 起这份冷却表落盘(宿主领域存储,跨 dsh 重启与插件重组合存活到重置时刻 —— 不再有"重启后
第一条白撞");被 QUOTA 拒的当下先同步记一条 5 分钟短闸门(补查确认后换成重置时刻),面板冷却行
右侧多一个「解除」按钮(额度提前恢复时用)。0.11.1 起那条「解除」指令的"已消费"记录也落盘 ——
面板写进配置的值一变就生效,但面板不会把它清掉,所以"这个值已经消费过了"这份记性得插件自己存:
不存的话宿主一重启,旧指令就被当成新指令再消费一次(清冷却 + 删介质记录 + 第一条请求又白撞那家,
2026-10-12 真机实测)。同轮还加了一条陈旧判据:指令里的时刻早于本进程启动时刻 ⇒ 只记为已消费、
什么都不动(面板不可能在宿主没运行时被点到)。想回到「完全按你排的顺序、只失败时切换」的旧行为,
把 ordering.mode 设成 manual(面板上就有这个开关)。
还能把自动压缩点钉在你要的位置(compactWindow,默认 50 万 token):DSH 的压缩引擎按
"该请求声明的窗口"算阈值,而本插件的窗口是按压缩点反算出来的。
插件页那张卡片上还有一块 回退链面板:0.5.0 起只读,0.6.0 起可编辑 ——
拖拽行首手柄改顺序、点行内模型名从 live 模型目录里换一条、按行开关 keepThinking/breakToolLoop,
点保存即写入 profile 的 cordis.patch.yml 并免重启生效;面板下半仍是最近若干次请求实际
怎么回退的(哪条失败、什么错误码、切给了谁、花了多久)。详情见 §2「回退链面板:可编辑(0.6.0 起)」。
0.7.0 起面板上多一段只读的「额度」摘要;0.8.0 起多一段「自动排序」——
一行模式开关(auto/manual,写 ordering.mode,免重启切换)+ 一行实际尝试顺序
(冷却中的条目也列在这里,带「冷却中」、哪一档打满与恢复时刻,例如「月度已耗尽,10-24 00:00 恢复」;
它们不会被尝试;只记了短闸门的那种写「疑似耗尽,{时刻} 后重试」)。0.11.0 起同一段末尾还多一行
冷却表落盘状态(就绪 / 未就绪 / 降级 + 原因),每条冷却行右侧一个「解除」按钮。
0.9.0 起这一段与链编辑器合成一段、按模式显示:开关挪到卡片最上面,
自动模式主视图是「生效顺序」、可编辑的链折进默认收起的「配置顺序」,手动模式反过来。这段是行为,
「额度」那段是观测,两者可能同时出现同一个来源 —— 不是故障,见 §2「排序与耗尽冷却(0.8.0 起)」。
┌─ 用户选了 auto/auto ─┐
请求 ──► AutoAdapter.stream()
│ (0.8.0 起先按额度排一次序、滤掉冷却期内的 provider;
│ `ordering.mode: manual` 时这一步跳过,完全按配置顺序)
│
├─ 1) commandcode/deepseek/deepseek-v4.1-flash
│ ├─ 第 1 次失败(SERVER 502) ──► 白名单内 ⇒ 退避 500ms 后原地重试
│ ├─ 第 2 次失败 ──────────────► 退避 1s 后原地重试 ……(默认最多重试 5 次)
│ └─ 重试耗尽 ────────────────┐
├─ 2) opencode-go/deepseek-v4.1-flash ▼
│ └─ 成功 ⇒ 分片原样透传给上层(前面各次的 usage 等协议分片已被丢弃)
└─ 3) deepseek-official/deepseek-flash ──► (没轮到)
│
└─ 全部失败 ⇒ 抛 AUTO_ROUTES_EXHAUSTED,消息里逐条列出「哪条路由、试了几次、怎么失败的」
本包是组合包(bundle):注册行(id: llm-auto)随包发布在包内 cordis.patch.yml 里,
宿主按 profile package.json 的 dsh.profile.bundles 加载 —— 不需要再往 profile 的
cordis.patch.yml 里贴任何 - insert: 行。
在 DSH profile 目录(~\.dsh\profiles\desktop\)下:
# ① 装进 profile:dshpm 会顺带把包名写进 dsh.profile.bundles
node <工作区>\dsh\dsh-plugin-manager\dshpm.mjs add file:./plugins/dsh-llm-auto --profile desktop
# 公开环境:profile 的 package.json 里加 "dsh-llm-auto": "github:liuyun847/dsh-llm-auto"
# 再 pnpm install(或 npm install) —— 同样只要包名在 dsh.profile.bundles 里
# ② 按本机 provider 改 routes:改包内 cordis.patch.yml 那一行(profile 层也可按 id 覆写)
# ③ 重启一次 DSH(本机桌面端:关掉再打开 DeepSeek Harness 窗口,见 §4「生效条件」)
dsh.profile.bundles);
点开卡片后每一行还有行级开关(向 profile 的 cordis.patch.yml 写 disabled 覆盖 ——
profile 层在包层之后应用,所以覆写优先)。node dshpm.mjs remove dsh-llm-auto --profile desktop。
⚠ 本 profile 的 pnpm 带供应链策略,pnpm remove 会报
ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED;绕过办法是进 profile 目录直接跑
pnpm remove dsh-llm-auto --config.minimum-release-age=0
(dshpm 的 --fast 只对 add 有效 —— pnpm remove 不接受 --minimum-release-age 这类参数)。dsh-client-modules 把
"本包不是客户端包"这一否定结论按 specifier 缓存在 pkgMeta 里,只写不删
(其 lib/index.js:510 / :703),HMR 不会重新扫描 ⇒ 半侧新增后先重启,插件页才会
长出配置表单(见 §2)。重启后:模型选择器出现 Auto 分组 → 选 auto → 发一条消息 → 打开
http://127.0.0.1:19387/api/llm-auto/routes 看它到底走了哪条(以及当前声明的上下文窗口)。
compactWindow 的图形入口在插件页:「已安装」→ 点开 dsh-llm-auto 那张卡片,表单就在
卡片描述与行列表之间(0.4.0 起,见 §2「插件页里的 compactWindow 表单」)。
配置有两个入口:
dsh-llm-auto 卡片 → 表单在描述与行之间。
这里能改的是 compactWindow(0.4.0 起)、routes(0.6.0 起,「回退链」面板)与
ordering.mode(0.8.0 起,「自动排序」那一行的开关);cordis.patch.yml 的注册行(= 本包自带、随组合包加载的那一行):结构性配置
(现在只剩 retry)只能在这里改;profile 层要覆盖就按 id: llm-auto 写覆写行
(profile 层在包层之后应用)。routes 从 0.6.0 起、ordering 从 0.8.0 起都是 .volatile()
⇒ 面板里改完即时生效,不必重启,所以"改链/改模式"不必动文件。标了「热改」的键同时是 schemastery 的 .volatile() 字段,经宿主的设置服务可改、改完即时生效
(效果边界见本节末尾)。
- insert:
- id: llm-auto
name: 'dsh-llm-auto'
config:
routes:
- { provider: commandcode, model: deepseek/deepseek-v4.1-flash }
- { provider: opencode-go, model: deepseek-v4.1-flash }
- { provider: deepseek-official, model: deepseek-flash, keepThinking: true, breakToolLoop: true }
# 压缩点(默认就是 500000,写出来只是显式化)
compactWindow: 500000
# 额度感知的路由行为(0.8.0 起):**包内这份 patch 里没有这一行** —— 不写就是默认的 auto。
# 要改成 manual(或想在文件里留痕)时才放开下面两行;
# 面板上那个「自动排序」开关写的就是它,改完即时生效(volatile)。
# ordering:
# mode: auto # auto(默认)| manual;其余值只 warn 回落 auto
# retry 不写 = 官方默认(每路由 5 次重试);要关掉或改参数再放开:
# retry:
# maxRetries: 5
# retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT]
# backoff: { initialDelayMs: 500, maxDelayMs: 10000, jitterRatio: 0.1 }
| 键 | 必填 | 默认 | 热改 | 说明 |
|---|---|---|---|---|
routes | ✅ | — | ✅ | 有序回退链,第一项即首选。每项 { provider, model, keepThinking?, breakToolLoop? };model 要写全(provider 声明的真实 id,如 deepseek-v4.1-flash、deepseek/deepseek-v4.1-flash,别省前缀)。0.6.0 起可在插件页的「回退链」面板里直接编辑(写入 profile 的 cordis.patch.yml,免重启);手写 YAML 照旧可用,profile 层按 id: llm-auto 覆写优先 |
compactWindow | 500000 | ✅ | 让自动压缩发生在这个 token 数附近(正整数)。对外宣称的窗口由它反算(见下) | |
name | Auto | ✅ | 模型显示名(选择器里的分组名恒为 Auto) | |
contextWindow | 见下 | ✅ | 【旧键】直接声明对外宣称的上下文窗口(正整数)。与 compactWindow 同时给出时以 compactWindow 为准并被忽略(warn 会点名两者) | |
logLimit | 50 | ✅ | 路由日志环形缓冲条数(仅内存) | |
ordering.mode | auto | ✅ | 额度感知的路由行为(0.8.0 起):auto = 按订阅月度重置时刻自动排序 + 额度耗尽时冷却该来源;manual = 完全按 routes 顺序、只失败时切换(0.7.0 的行为)。两个值之外的坏值(标量 / 数组 / 普通对象形态)只 warn 并回落 auto;⚠ 但值里不要写 !!js、函数或类实例(RegExp/Date/Map,YAML 里未加引号的日期也会变成 Date)—— 这类"非普通值"在 schema 层就被拒,会让整行插件加载失败(见本节末尾的 ⚠) | |
retry.maxRetries | 5 | ✗ | 每路由重试上限(不含首次);0 = 关闭重试,恢复"一次败就切" | |
retry.retryableCodes | EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT | ✗ | 可重试的错误码白名单;永久错误(不在表里的)一次败就切,不白烧请求 | |
retry.backoff.initialDelayMs | 500 | ✗ | 指数退避起步(毫秒) | |
retry.backoff.maxDelayMs | 10000 | ✗ | 退避封顶;上游 Retry-After 超过它则不等了,直接切下一条 | |
retry.backoff.jitterRatio | 0.1 | ✗ | ±10% 对称抖动 |
「热改」= 该键是 schemastery .volatile() 字段 ⇒ 由宿主设置服务写入后原地更新引用,
插件下次用到时就是新值(不需要重启、也不需要重新 apply);✗ 的键是结构性配置,
由 apply() 一次性消费,只能改 cordis.patch.yml(包内或 profile 覆写行)。
routes从 0.6.0 起也是.volatile(),但类型写成z.union([z.array(条目), z.any()])—— 这不是画蛇添足:写路径要求目标是 volatile 字段,而 schemastery 禁止 volatile 之下再套 voluntary, 同时本插件"坏形状只 warn"的约定又要保住。union 两个成员各管一头: 第一个给出"数组 + 字段类型"的形状(编辑器写进来的值走这条),第二个兜底放行其它形状 (继续交给normalizeRoutes()逐条判)。
ordering从 0.8.0 起同样是 volatile,形状照抄 0.6.0 的教训写成z.union([z.object({ mode: z.string() }), z.any()]).volatile(): 必须 volatile,因为面板上那个「自动排序」开关承诺"改完即时生效、不用重启"; 必须 union 兜底,因为 schema 是加载期校验 —— 直接写z.object(...)会让ordering: nope这种坏值把整行插件搞成加载失败,那正是 0.6.0 踩过的坑。 坏值只由normalizeOrdering()warn 并回落auto。⚠ 这条不绝对:schema 是加载期校验,值里含
!!js标签、函数或类实例(RegExp/Date/Map) 时仍会被拒 ⇒ 整行插件加载失败(实测:ordering: [{ mode: auto }]这种"数组里套对象"过得了 union,但{ d: new Date(0) }、{ f: () => {} }一律抛)。这条对routes/retry/quota/ordering一视同仁 —— 配置值只写标量、数组与普通对象。
retry 块的形状与默认值全部复用官方 resolveRetryPolicy(@deepseek-ai/dsh-llm,
即自带 dsh-llm-retry 用的那个):选 auto 与选普通模型的重试语义一致。只支持 mode: 'normal'
(always = 无上限重试,单请求可能无上限计费,挂载时 warn 并回落默认);坏值只 warn 不阻止注册。
compactWindow:压缩点 → 对外宣称的窗口DSH 的自动压缩引擎 @deepseek-ai/dsh-compaction-basic 不看真实上游窗口,只用"该请求声明的
窗口"算阈值(该包 lib/index.js:124-147):
threshold = floor(min( cw × 0.8, cw − reserved − 65536 )) # 0.8 / 65536 是引擎默认值
reserved = 请求头 maxTokens ?? 适配器声明的 defaultMaxTokens ?? 0
本插件既不声明 defaultMaxTokens、调用方也不给 maxTokens ⇒ reserved = 0,
于是 threshold = floor(min(cw × 0.8, cw − 65536))。反过来解就能让压缩点落在你要的位置:
cw = max(ceil(T × 1.25), T + 65536) # T = compactWindow;T = 500000 ⇒ cw = 625000
验算(T = 500000):0.8 × 625000 = 500000;625000 − 65536 = 559464 > 500000(min 取前者);
保留尾部 floor(625000 × 0.16) = 100000 < 500000 ✅。挂载日志会把这一步连同假设打出来:
auto: 压缩点 500000 → 声明窗口 625000(假设 compaction-basic thresholdRatio 0.8 / headroomTokens 65536)
行为变化(升级到 0.3.0 就会看到):本机 auto 链的窗口从"第一条可解析路由的 884000"
变成"按默认压缩点反算的 625000",压缩触发点相应从 707200 提前到 500000 ——
后者正好等于本机全链最紧那一跳的可用输入预算(opencode-go 884000 − 384000 = 500000)。
这是有意的:声明窗口在这里只是"给压缩引擎定位用的刻度",不是对上游窗口的承诺。
优先级(纯函数 planDeclaredWindow(),实现在 lib/compact.js):
compactWindow(正整数) → 反算声明窗口 默认 500000 ⇒ 625000
contextWindow(正整数) → 直接声明(旧键) 不写 compactWindow 时才轮到它
逐跳解析 → 第一条可解析路由的窗口
兜底 65536
⚠ 宿主里 schema 总会给
compactWindow补上默认值 500000 ⇒ 默认口径就是反算;contextWindow只在"compactWindow完全没出现"时才生效(直接调apply()的集成方)。 两者同时给出时会打一条 warn 说明用了哪个、忽略了哪个。 ⚠compactWindow低于最小可用值 12484(见minimumUsableCompactWindow())会另打一条 warn:此时引擎的"保留尾部 < 阈值"校验过不去 (floor(0.16 × (T + 65536)) ≥ T),@deepseek-ai/dsh-compaction-basic每轮抛TargetPressureConfigError;该错误被引擎捕获(agent/pre-step那层):第一次 warn、之后对同一目标 静默跳过压缩、回合照常继续 —— 净效果是压缩从不发生(不会报错,但上下文会一直涨)。 这个下界是算出来的,不是拍脑袋的常量(见minimumUsableCompactWindow())。 ⚠compactWindow/contextWindow非法(非正整数)只 warn 并回落默认值,不会让宿主起不来。 ⚠contextWindow ≤ 65536时引擎的 pressure budget ≤ 0,连阈值都算不出来(同样每轮抛错)—— 这也是引入compactWindow的原因之一;反算路径下T ≥ 12484一定是安全的。 ⚠ 该反算依赖上面三个引擎默认常量。引擎换版本、或你把compaction-basic的thresholdRatio/headroomTokens改成别的值,映射就不再精确 —— 挂载日志里那行假设就是为 这种时候留的证据(可用/api/llm-auto/routes的compactWindow/declaredContextWindow复核)。
compactWindow 表单(0.4.0 起)0.4.0 起本包自带浏览器半侧(lib/client.js),注册进插件页为组合包预留的 plugins.bundle.config
slot(键 = 本包包名 dsh-llm-auto)⇒ 插件页 →「已安装」→ 点开 dsh-llm-auto 卡片,
官方 SettingsForm 的表单就渲染在卡片描述与行列表之间,不必再点进任何二级页。
位置沿革(两版同为 2026-09-25):初版注册的是
plugins.row.config(键<包名>#<行 id>), 表单只能从「行llm-auto→「配置」」的二级页进入 —— 要钻四层。现按用户要求提到卡片上, 行上的「配置」控件随之消失(本插件不再占用plugins.row.config)。 两个槽位的契约都在dsh-client-ui-plugin-manager的lib/types/client/slot-contract.d.ts; 该页只在 ledger 收录了这个键时才渲染这一段(其client.js:2879的ledger.bundles.has(pkg.name)), 而 ledger 直接读槽位注册的 key(其client.js:48的keysOf)⇒ 键必须恰好是包名。
compactWindow),与官方四个配置页
(ui-settings-shell / agent-loop / subagent / web-search)同构:用 SettingsFormModel /
SettingsForm / SettingsValueField 暂存草稿,点保存才写入,自带「已覆盖」标记与「恢复默认」;compactWindow 一个键;0.6.0 起
routes 也能写(见下节「回退链面板」),而且它现在就是 .volatile() 字段(union 兜底坏形状)
⇒ 改完不用重启、下一次请求就是新链;retry 仍是结构性配置,只能改包内 cordis.patch.yml;llm-auto(dsh-settings 按 entry.options.id 投影)——
与启停开关、覆写行的寻址键一致;cordis.patch.yml 追加按 id 覆写的顶层行
(- id: llm-auto + name: 'dsh-llm-auto' + config: { routes: [...], compactWindow: N },
profile 层在包层之后应用 ⇒ 遮蔽包内那行 insert),或直接改包内 cordis.patch.yml。
routes 从 0.6.0 起有表单(就在这张卡片的「回退链」面板里),上面这两条只是等价的备选改法;
retry 这类结构性配置仍然没有表单,只能走这两条。同一张卡片上、compactWindow 表单下方多出一块「回退链」面板。0.9.0 起它按排序模式显示, 模式开关画在卡片最上面(「回退链」标题之前):
auto(默认)—— 主视图是生效顺序,可编辑的配置链折进「配置顺序」(默认收起):
排序方式 [当前:自动排序。切到手动]
──────────────────────────────────────────────────────────────────────
回退链 [放弃改动] [刷新] [保存]
自动排序生效中:按订阅的重置时刻排序、额度耗尽的来源暂时跳过;下面是本次实际会尝试的顺序。…
生效顺序
冷却中 opencode-go/deepseek-v4.1-flash 月度已耗尽,10-24 08:00 恢复
commandcode/deepseek/deepseek-v4.1-flash
deepseek-official/deepseek-flash 额度查不到,按配置顺序
⌄ 配置顺序(3 条) ← 点开就是下面「手动」那张里可拖拽的链(改动照旧要点「保存」)
每路由最多重试 5 次,退避 500ms→10000ms
额度 ...............................................(只读;鼠标悬停给一句口径说明)
Command Code 余额 $45.23 · 5h $2.09/$14.00 · 周 $24.77/$35.00 individual-goat
OpenCode Go 5h 0% · 周 0% · 月 已耗尽 10-24 08:00 重置
最近请求 ............................................ 共 13 条记录,容量 50
● 10:05:28 1m4s 成功
1. commandcode/deepseek/deepseek-v4.1-flash ✓ 成功
manual —— 主视图就是那条可编辑的链,不画生效顺序(那种模式本来就不排序):
排序方式 [当前:手动。切回自动]
──────────────────────────────────────────────────────────────────────
回退链 [放弃改动] [刷新] [保存]
按下面的顺序依次尝试:第 1 项即首选。某条重试耗尽、或错误码不允许重试时,才静默切下一条。
拖动行首的手柄改顺序,改完点「保存」写入配置并即时生效(不用重启)。
⠿ commandcode / DeepSeek V4.1 Flash ⌄ 选项 ⌄ ✕
⠿ opencode-go / DeepSeek V4.1 Flash ⌄ 选项 ⌄ ✕
⠿ deepseek-official / DeepSeek-V41-Flash keepThinking · breakToolLoop ⌄ 选项 ⌄ ✕
└─「选项」展开(这一行开着两个开关时):
保留思考块 [开]
工具循环收尾 [开]
[+ 添加模型] [恢复包内默认链]
每路由最多重试 5 次,退避 500ms→10000ms
额度 … / 最近请求 …(与上面那张相同)
⠿ 拖拽排序、点行内模型名开选择器换一条、
「选项」展开两个逐条开关、行尾 ✕ 删除(最后一条不允许删)。改动先落在本地草稿,
只有点「保存」才写配置;「放弃改动」回到端点值。⚠ 0.9.0 起自动模式下这块默认收起
(折进「配置顺序」那一行,点开照旧可改)—— 链本身没变,只是不再跟「生效顺序」抢主视图。rankByName 同判据)、
按 provider 分组、行高 34px、当前项打勾、材质直接用官方 MenuSurface + MenuGroup
(滚动时分组标题吸附)。数据来自新增的 GET /api/llm-auto/catalog。configForms → 宿主 settings.mutate → dsh-config-editor,落进 profile 的
cordis.patch.yml 里那一行 - id: llm-auto 的 config.routes(yaml 文档式写入 ⇒
该文件里那些注释与 !!js 标签原样保留)。profile 层在包层之后应用 ⇒ 这一行会遮蔽包内
那段 insert;面板上的「恢复包内默认链」= unset 掉这个覆盖 —— 该按钮只在 profile 层确实
覆盖了 routes 时才出现(判据是端点响应的 user.routes,见 §5),没有覆盖时它压根不画。routes 是 cosmokit 的 volatile 引用,加载器重建配置时原地更新同一个引用,
而适配器只在每次请求开始时现取一次链(不是构造时抄一份)⇒ 改完链的下一次请求就是新链,
正在跑的那一次不受影响。calls 字段,按一次 auto 请求分组(adapter 给
每条日志写上本次 stream() 的序号 call,宿主侧 lib/calls.js 的 groupCalls() 还原)。
同一条路由的多次尝试合并成一行(试了 N 次),错误码取该路由的最终失败,
→ 切换至 … 指向下一条候选。最近结束的请求排最上面。这一段始终只读。ordering.mode(0.9.0 起画在卡片最上面,见下节),
链仍是你排的顺序(chain 一字不变);它只决定"这一次实际会按什么顺序试、跳过谁"
(ordering.effective)—— 自动模式下后者就是主视图「生效顺序」。curl http://127.0.0.1:19387/api/llm-auto/routes(完整字段见 §5)。--dsw-* token(浅色/深色两套随外壳),状态用官方 StateDot / Tag;
文案走本插件的字典命名空间 llmAutoSettings(中英双语,与 compactWindow 表单同一份)。行为是 0.8.0 加的,界面在 0.9.0 收敛成一段:模式开关(「排序方式」+ 那个按钮)画在卡片
最上面、「回退链」标题之前;auto 时主视图是下面这块「生效顺序」、可编辑的链折进「配置顺序」,
manual 时只给可编辑的链、不画「生效顺序」(两段不再各画一次"当前是什么模式"):
排序方式 [当前:自动排序。切到手动]
生效顺序
冷却中 OpenCode Go/deepseek-v4.1-flash 月度已耗尽,10-24 00:00 恢复 [解除]
Command Code/deepseek/deepseek-v4.1-flash
deepseek-official/deepseek-flash 额度查不到,按配置顺序
冷却表已落盘(跨重启存活)
⌄ 配置顺序(3 条)
auto / manual 之间切(写 ordering.mode,即时生效、不用重启);
悬停给一句口径。提示语随模式换 —— manual 下"按下面的顺序依次尝试…"那句才成立,
auto 下换成"自动排序生效中…"(旧那句在自动模式里是错的:它会排序、也会跳过)。auto 时):这一次请求会依次尝试的路线,来自端点新增的只读字段
ordering.effective。冷却中的条目也列在这里(排在前面,带「冷却中」标签、哪一档打满
与解除时刻,如「月度已耗尽,10-24 00:00 恢复」),它们不会被尝试 —— 真正会走的是去掉这些
条目之后的顺序;查不到额度的标「额度查不到,按配置顺序」。规则(与设计档 §3 逐条对应):
| 项 | 规则 |
|---|---|
| 排序键 | 订阅来源取月度重置时刻升序(Command Code = 套餐 currentPeriodEnd;OpenCode Go = 月度档 resetsAt)。不用 5h/周窗口的重置点 —— 那是速率闸门,按它排会每几小时抖一次 |
| 三桶 | ① 已知重置时刻的订阅(升序)→ ② 重置时刻未知的(按配置顺序)→ ③ 按量兜底(永不过期,按配置顺序,永远最后)。桶内保持配置顺序,且不改 routes 配置本身 |
| 粒度 | 排序按条目;冷却是按 provider(额度是账号级的)⇒ 同一家所有条目一起冷 |
| 触发 | 反应式:某条在会走回退的失败路径上返回 QUOTA,或报文命中窗口额度耗尽判据(0.9.1 起放宽:① insufficient quota/balance/credits ② (quota|usage limit) exceeded/exhausted/reached ③ 窗口词 + 额度词 + 耗尽/重置词三者同时成立,词序不限 —— 覆盖 Command Code 的 You've reached your weekly usage limit … Your limit resets at …)。AUTH / 其它码不触发;瞬时速率限制(报文里没有窗口词,如 429 rate limit exceeded)也不触发。0.11.0 起第一次记账是同步的:失败当下立刻冷 5 分钟(不等补查),因为补查要一次 HTTPS 往返,这期间已经排好序的并发请求本来会照旧白撞同一家(2026-10-12 实测:两条请求差 1 秒、连撞两次) |
| 判定 | 补查该 provider 的额度接口确认,不靠错误文本猜。三档都看:OpenCode Go 的 5 小时 / 周 / 月度,Command Code 的 5 小时窗 / 周窗 / 月度余额(判据:credits.belowThreshold 为真,或 monthly 与 total 两个键都在且都 ≤ 0 —— 字段缺失=不知道,绝不当成 0)。任一档耗尽 ⇒ 冷却到被耗尽那几档里最晚的重置时刻,面板写明是哪一档(「月度已耗尽」/「余额已耗尽」/「周已耗尽」…) |
| 恢复 | 不用定时器:每次请求重算时 now >= until 即自动解除 |
| 兜底 | 过滤后一条都不剩(理论上到不了,按量不参与冷却)⇒ 忽略冷却,避免假故障「链已用尽」 |
| 持久化 | 落盘(0.11.0):宿主领域存储,域 llm_auto_cooldown / 表 cooldowns,介质 <DSH_HOME>\storages\llm_auto_cooldown.json(键 = provider)。打开时水合 + 顺手删掉过期条目;每次 mark / clear 都写穿(串行写链,失败只 warn)。服务缺席 / 打开失败 / 记录坏 ⇒ 降级为纯内存并 warn,行为退回 0.10.0。0.9.1 起「窗口额度耗尽」的失败不原地重试(即使错误码是 RATE_LIMIT),代价只是一次失败往返 |
| 短闸门 | 被 QUOTA 拒的当下先记 now + 5 分钟(来源 gate、不带窗口 id)⇒ 面板写「疑似耗尽,{时刻} 后重试」;补查确认后换成"被耗尽档位里最晚的重置时刻"(来源 confirm,面板写「月度已耗尽,… 恢复」)。闸门到点自然解除,过期条目连介质一起不留 |
| 提前恢复 | 两条出口:① 自愈 —— 周期补查真的查到该家、且没有任何一档耗尽 ⇒ 自动解冻 + 删介质记录;② 面板冷却行右侧的**「解除」按钮(写设置服务的 ordering.unfreeze 一次性指令 <provider>@<epochMs>,值一变就解冻)。0.11.1 起:消费掉的值会写进冷却域的 global 槽 ⇒ 同一个值在宿主重启 / 插件重组合后不会再触发一次**(否则它会清掉刚水合起来的冷却、删掉介质记录,第一条请求又白撞那家);而指令里的时刻早于本进程启动时刻时按陈旧残留忽略 —— 只记为已消费,冷却表与介质记录一律不动。切 manual 也能立刻绕过(那种模式不跳过任何一跳) |
| 查询失败 | fail-open:断网 / 超时 / 形状不认 / 401 ⇒ 不冷却,只记一条 warn(宁可下次白撞,也不误伤一条其实能用的订阅)。这一轮既然没查到,路由侧的额度缓存只记 10 分钟查询闸门后重试(见下面的 §4 说明;它管"什么时候再问",与上面那条 5 分钟冷却闸门不是一回事) |
⚠ 路由侧的额度缓存不与面板那份共用(设计档 §4):面板的
quota仍是 60 秒 TTL、 只在读端点时刷(只读观测,一行不动);路由侧另有一份低频缓存,有效期 = 该来源已知重置时刻里 最早的那个,另挂 6 小时上界兜底。"查过"的判据是这一轮真的拿到status: ok—— 查失败(断网/超时/形状不认/401)只记 10 分钟查询闸门后重试,而不是把失败也当成"查到了"占满 6 小时(⚠ 它与 §2 那条 5 分钟冷却闸门是两件事:一个管"什么时候再问额度",一个管"这家先别试")。 两者复用同一套查询与解析(lib/quota.js),只换"什么时候查"的策略。 ⚠ 路由侧只在三个时机去问:① 进程内第一次有请求需要排序 ② 发生额度耗尽报错(码是QUOTA, 或报文自带窗口语义 —— 判据见 §2 触发行;只查这一家、 绕缓存)③ 缓存过期。①③ 同挂在每次请求的排序路径上(适配器每请求判一次,过期才查)⇒ headless(没人读/routes)也照样会补查;读端点那一次只是幂等地顺带推一下。 三处都是 fire-and-forget —— 查询不阻塞任何一次请求或端点响应;而且只补查真的过期的那几家 (链上查不到的来源永远"该查",不该把别的家的闸门顶穿)。没人用auto时零外部流量 (与 0.7.0 的额度观测同一条纪律)。 ⚠manual下冷却表照旧记着、也照旧落盘(切回auto立刻生效),但端点的ordering.cooldown恒为空数组 —— 那种模式不跳过任何一跳,把表画出来只会让人以为"它被跳过了"; 面板上那一行「解除」与落盘状态行同样只在自动模式下画。 ⚠ profile 遮蔽(0.6.0 的同一个坑):面板上保存过一次链之后,整块config被写进 profile 的- id: llm-auto行 ⇒ 之后改包内cordis.patch.yml的ordering不再生效,要改就改 profile 那一行 (或直接在面板上切)。
同一张卡片上、「最近请求」上方还有一段只读的额度摘要:一家一行。它不是路由策略的一部分 —— 插件不会因此跳过或重排任何一跳(顺序完全由链决定),查不到也只体现在这一段里。
余额 $59.92 · 5h $0.62/$14.00 · 周 $10.08/$35.00,右侧是套餐 id(如 individual-goat)。
余额 = monthly + purchased + free(充值额度可结转),两档窗口的 used/cap 同单位(美元等值)。5h 0% · 周 0% · 月 已耗尽,右侧是该档的重置时间(本地时区,上游给的是 ISO/UTC 串)。
⚠ 它的接口给的是百分比窗口(没有金额口径);status: "rate-limited" 表示该档已经耗尽
(服务端会把 percent 硬编码成 100)⇒ 界面写「已耗尽」,不写 100% 更不写 0%。error_code 1010)/ 超时 /
网络不通 ⇒ 那一行写「不可查:原因」,一个数字都不给。用 0 顶替会把"已耗尽"说成"没用量"。/api/llm-auto/routes 被命中且缓存过期(默认 60s)时才查一次,不轮询;
点面板的「刷新」即重读。第一次读到"查询中…"时面板会自动补拉一次(只补一次)。cordis.patch.yml;它不是 volatile 字段,面板里没有表单):quota:
enabled: true # 只读观测,缺省开;false ⇒ 整段不显示
ttlMs: 60000 # 缓存窗口(从"上次尝试"起算,成败都算)
timeoutMs: 8000 # 单个 HTTP 请求上限(实测 CC 冷连接可达 2.3s,4s 会误报超时)
# userAgent: '...' # 查询用的 UA(默认浏览器 UA)
commandcode: { apiKeyEnv: CMD_API_KEY }
opencodeGo: { apiKeyEnv: OPENCODE_API_KEY }
⚠ 凭据引用名默认
CMD_API_KEY/OPENCODE_API_KEY,走宿主凭据服务(与模型页写的是同一份); 拿不到凭据服务时才退环境变量。⚠ 面板上保存过一次链之后,quota会被一并写进 profile 的 覆盖行(profile 层遮蔽包层)⇒ 之后要改 quota,改 profile 那一行。
Config(= 成为可编辑配置条目),以及它的代价与效果边界0.3.0 起本插件导出一个 schemastery Config(lib/index.js)。效果是配置成为宿主的可编辑条目:
dsh-settings 的 SettingsForms.describe() 会为带 schema 的活动条目生成描述符
(lib/index.js:413-452;schema 取 entry.fiber.runtime.Config,同文件 :538-541),
settings.describe() / settings.mutate() 这条远端通道因此能看到并写入它的字段。
name / compactWindow / contextWindow / logLimit 标了 .volatile(),插件不缓存这些值
(每次用到时重新读引用),所以值一改就生效、不需要重启;0.6.0 起 routes 也是 .volatile()
(适配器每次请求现取一次链,面板里改完即时生效);0.8.0 起 ordering 同样是 .volatile()
(面板那个「自动排序」开关写的就是它,每次排序现读一次模式)。仍是结构性配置的是 retry 与
0.7.0 的 quota(后者是只读观测,刻意不标 volatile),这两者仍走包内 cordis.patch.yml。
⚠ 效果边界(如实说明):导出
Config这一步本身不生成任何表单 —— 随包发布的 Web 客户端 没有"按 schema 自动生成表单"的页面(@deepseek-ai/dsh-settings的 README 自己写着Each form reports autoGenerate … for clients that build pages from the schema; no shipped client does so yet),插件页(Plugins)只渲染经 slot 注册的表单页(dsh-client-ui-plugin-manager的plugins.item/plugins.bundle.config/plugins.row.config)。0.4.0 起本包自带浏览器半侧 (lib/client.js)注册进plugins.bundle.config,才把compactWindow那一个字段变成插件页里可点的 表单(见上一节);宿主侧 schema 的作用是让settings.describe()/settings.mutate()这条远端 通道能看到并写入这些字段。 本节的结论来自源码阅读 + 单测(test/compact.test.mjs里"真 schema 校验 ⇒ 解包 ⇒ 反算 625000")。
代价与取舍(原作者当初"有意不导出 Config"的理由依然成立,只是被权衡掉了):
routes / retry 用 z.any():形状校验继续留在 normalizeRoutes() /
normalizeRetry() 里,坏值依旧只 warn/error;数值字段用 z.number() 但不加 .min()/.step(),
范围与整数性仍由插件自己判并 warn 回落。会硬失败的是两类:类型错误(例如把字符串写进
compactWindow 这种数字字段)与非普通值(!!js 标签、函数、类实例;YAML 里未加引号的日期
会被解析成 Date)—— 所以配置值只写标量、数组与普通对象。.volatile() 的字段(volatileForm(),dsh-settings lib/index.js:122-131;
一个 volatile 字段都没有时整条目被跳过),所以 routes / retry 不在可写字段里 —— 这是有意的:
它们由 apply() 一次性消费,标成 volatile 等于承诺一个做不到的"改了即时生效"。.volatile() 字段在插件里拿到的是 cosmokit 的 Volatile 引用(不是值本身),
读之前必须 unwrapVolatile()(lib/compact.js)。⚠ 宿主侧这条能力("配置成为可编辑条目")的依据是上面引用的源码位置 + 单测里 "真 schema 校验 → 解包 → 反算 625000"这条用例(见 §7);插件页里的表单入口见上面那节 (它走的是同一套 schema:
ctx.configForms只投影标了.volatile()的字段)。 重启后请先看/api/llm-auto/routes的compactWindow/declaredContextWindow是否符合预期。
contextWindow 不写且 compactWindow 也没给时:逐个试路由,取第一条能给出正整数窗口的那条的窗口;
全都拿不到就用保守值 65536(宁可让压缩早触发,也不要谎报一个大窗口导致请求必撞上游上限)。
解析结果缓存 30 秒,目录被反复重建时不会反复去问上游适配器。
⚠
provider不要写auto(自递归):挂载时会 warn 并跳过该条。 ⚠routes为空/缺失/解析后一条不剩:打一条error并拒绝注册(不抛错,宿主照常启动)。 ⚠ 写成auto的整条目的链要避开两个坑:①provider: auto是自递归(挂