aqiu817/dsh-plugin-auto-retry ↗★ 0
dsh-plugin-auto-retry
DeepSeek Harness plugin that takes over a model-request failure the built-in retry executor declined, continuing its backoff curve, and surfaces the full failure detail the transcript discards. 适合需要应对网络波动、强制执行自定义重试策略并查看失败详情的任务。
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:aqiu817/dsh-plugin-auto-retryREADME
Read the full README ↗配置
六个字段全部是 .volatile():在 Plugins 页面改了不重载 fiber,enabled: false 中途就能掐断一条正在跑的重试链。
在哪里改:侧栏 → 插件 → 找到 dsh-plugin-auto-retry 这个组合包 → 打开它,配置表单就在这一页上。表单由本插件的浏览器半部通过 plugins.bundle.config slot 提供,key 是包名本身。
这一点容易踩空:宿主侧的
Configschema 本身不会在 Plugins 页面产生任何配置控件。配置区只在某个插件注册了 slot 时出现——组合包级的plugins.bundle.config(key = 包名),或行级的plugins.row.config(key =#,需要再点进那一行才会有「配置」入口)。这里选组合包级:本插件只声明一行,两者内容完全一样,但组合包级少一次点击。所以一个只有宿主半部的插件(例如dsh-plugin-continue-on-limit)在 Plugins 页面上看不到配置——它的Config依然生效,只是没有编辑入口。组合包级的 slot 不会把表单对象递给你(行级才会),所以浏览器半部自己从
configForms服务取命名空间auto-retry的表单。这个服务属于设置包,因此它不写在浏览器半部的inject里、而是单独开一个 fiber 去要:这样在一个没装设置包的部署里,丢掉的只有配置页,重试面板照常工作。命名空间
auto-retry必须与cordis.patch.yml里的行 id 一致,否则配置页会一直是空的(契约测试交叉检查这两处)。
表单行为:改动暂存到按下保存;保存只提交真正变动的字段,全部挂在同一个 revision 栅栏下,所以并发写入会被拒绝而不是被静默覆盖。离开该页面会丢弃暂存改动。数字字段超出边界或无法解析时,保存会被拦下并指出是哪个字段——不会把宿主会拒绝的值发出去。
| 字段 | 类型 | 默认 | 边界 | 说明 |
|---|---|---|---|---|
enabled | boolean | true | — | 总开关 |
extraRetries | integer | 3 | 0–10 | 官方预算之外额外允许的尝试次数;0 = 不介入 |
initialDelayMs | integer | 1000 | 0–600000 | 官方没试过时的起步延迟 |
maxDelayMs | integer | 60000 | 0–600000 | 外层上限,也用于钳制 provider 延迟 |
jitterRatio | number | 0.2 | 0–1 | 对称抖动比例 |
retryableCodes | string[] | 见下 | 至少 1 个 | 我们愿意接手的 code |
retryableCodes 在页面上是开关,不是文本框——没有哪个接口能列出「适配器可能返回的全部 code」,所以内置了一份目录,分三组渲染:
| 分组 | 呈现 | code |
|---|---|---|
| 默认开启 | 开关 | EMPTY_RESPONSE RATE_LIMIT SERVER TIMEOUT TRANSPORT PROVIDER_ERROR PI_AI_ERROR |
| 按需开启 | 开关 | QUOTA ACCOUNT_QUOTA INVALID_REQUEST UNKNOWN |
| 永不重试 | 开关但禁用 | AUTH MISSING_CREDENTIAL INVALID_CREDENTIAL NO_ADAPTER INVALID_MODEL INVALID_CONFIG CONTEXT_WINDOW_EXCEEDED IMAGE_OFFLOAD_REQUIRED ABORTED |
第三组照样列出来、但开关是禁用的:这样页面既说清了有哪些 code 存在,也说了它们勾不动——硬拒绝是策略常量,不是偏好。开关的初始状态来自已接受的值,不是目录默认值;目录外的 code(某个适配器自己发明的)落在「其他错误码」文本里,每行一个。
一个 code 都不留是不允许的,页面会在暂存阶段就说明,并在保存时拦下。原因在宿主侧:codesWithin() 把空列表当成笔误,回落到默认白名单(否则一个输错的配置会悄悄把插件关掉,而设置页还显示它开着)。所以存一个空列表会让页面显示「一个都不重试」、而插件实际还在重试 7 个 code。要么勾一个,要么用 enabled: false / extraRetries: 0 表达「不介入」——那两个是明确的,不会和页面打架。
手改
cordis.patch.yml写retryableCodes: []同样会回落到默认白名单。手上的配置能被页面表达,就不该在页面之外生效。
默认白名单:
EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, ← 官方那 5 个(接手 = 延长预算)
PROVIDER_ERROR, PI_AI_ERROR ← 两个适配器的兜底 code
硬拒绝集合(代码常量,不可配置,优先于白名单):
AUTH, MISSING_CREDENTIAL, INVALID_CREDENTIAL, NO_ADAPTER,
INVALID_MODEL, INVALID_CONFIG,
CONTEXT_WINDOW_EXCEEDED, IMAGE_OFFLOAD_REQUIRED, ABORTED
硬拒绝优先于白名单。这是安全底线:一个误输的白名单不该有权力去重试认证失败或上下文溢出。
QUOTA / ACCOUNT_QUOTA / INVALID_REQUEST / UNKNOWN 不在默认白名单,也不在硬拒绝集合——它们既可能是瞬时的,也可能是真正终态的,所以由部署自己决定:加进 retryableCodes 就生效。这样守住了「route on code, never parse message」这条平台原则,又给中继的额度池与 400 方言拒绝留了口子。
cordis.patch.yml 里的默认值:
- insert:
- id: auto-retry
name: 'dsh-plugin-auto-retry'
config:
enabled: true
extraRetries: 3
initialDelayMs: 1000
maxDelayMs: 60000
jitterRatio: 0.2
retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, PROVIDER_ERROR, PI_AI_ERROR]
配置页
同一个浏览器半部还注册 plugins.bundle.config(key dsh-plugin-auto-retry),也就是组合包页面上的配置区。六个字段各一个控件:四个数字输入、一个总开关,加一组 code 开关。
它不做自己的校验逻辑——边界只是让输入框提示正确范围,真正的把关仍在 readSettings() 与 schema 那一层。保存时只提交变动过的字段:
{ op: 'set', path: ['retryableCodes'], value: ['RATE_LIMIT', 'TIMEOUT'] }
{ op: 'set', path: ['extraRetries'], value: 5 }
全部操作共用宿主读到的 revision,所以编辑期间发生的并发写入会让这次保存被拒绝,而不是被静默覆盖。
code 开关的列表顺序与「同一组 code」判断都不敏感:开关永远按目录顺序提交,所以仅仅重排不会产生一次写入。目录里被硬拒绝的 code 即使已经在配置里、也只会以「已勾选但禁用」的样子出现——页面如实报告配置,不会顺手把它删掉,因为读者没碰过的东西不该产生写操作。
组合包级的 slot 是常驻的:只要组合包在,配置区就在,无论宿主是否真的提供了 auto-retry 这个命名空间。所以页面区分三种状态——正在读取(loading)、不可写(unavailable,部署没提供这个命名空间,或本页只读)、就绪——前两种只说状态,不画表单:一个 Save 注定失败的输入框比一句说明更糟。
配置值不会被敲错一个数就失控
.volatile() 字段是从 Plugins 页面直接写进 reference cell 的,不走加载器的 Config 校验。所以 readSettings() 对每个字段做类型检查 + 钳制,负数回落到默认值而不是 0——0 是「关掉这道闸」,一个误输的负数不该有这个权力。
| 输入 | 结果 |
|---|---|
extraRetries: -1 / 99 / 2.5 / 'lots' | 回落到默认 3 |
extraRetries: 0 | 保留 0(显式不介入) |
initialDelayMs: -100 | 回落到默认 1000 |
jitterRatio: -0.5 / 7 | 回落到默认 0.2 |
retryableCodes: [] / 'RATE_LIMIT' / [1,2] | 回落到默认列表 |
enabled: 'nope' | 回落到默认 true |
retryableCodes 的类型检查里 Array.isArray 是承重的:一个裸字符串会走到 String.prototype.includes,任何子串都会匹配,一个白名单笔误就能悄悄授权没人列过的 code。