aqiu817/dsh-plugin-auto-retry ↗★ 0

dsh-plugin-auto-retry

接管模型请求失败并继续执行指数退避重试 适合需要应对网络波动、强制执行自定义重试策略并查看失败详情的任务。

套件
dsh-plugin-auto-retry
相容性
待驗證
版本
0.1.0
授權
MIT
最近更新
2026年10月14日

安裝

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:aqiu817/dsh-plugin-auto-retry

配置

六个字段全部是 .volatile():在 Plugins 页面改了不重载 fiber,enabled: false 中途就能掐断一条正在跑的重试链。

在哪里改:侧栏 → 插件 → 找到 dsh-plugin-auto-retry 这个组合包 → 打开它,配置表单就在这一页上。表单由本插件的浏览器半部通过 plugins.bundle.config slot 提供,key 是包名本身。

这一点容易踩空:宿主侧的 Config schema 本身不会在 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 栅栏下,所以并发写入会被拒绝而不是被静默覆盖。离开该页面会丢弃暂存改动。数字字段超出边界或无法解析时,保存会被拦下并指出是哪个字段——不会把宿主会拒绝的值发出去。

字段类型默认边界说明
enabledbooleantrue—总开关
extraRetriesinteger30–10官方预算之外额外允许的尝试次数;0 = 不介入
initialDelayMsinteger10000–600000官方没试过时的起步延迟
maxDelayMsinteger600000–600000外层上限,也用于钳制 provider 延迟
jitterRationumber0.20–1对称抖动比例
retryableCodesstring[]见下至少 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。