🚗 dsh-auto-driving
DeepSeek Harness (DSH Desktop) 插件 —— 模型自动回退 · 任务自动重试 · 全自动模式
让未完成的任务自己找路继续:一个模型挂了就换下一个,一次请求卡住了就自动救活,人工验证全部自动通过。
主页自动驾驶胶囊:单击直接开关全自动模式,模型切换实时通知
✨ 功能亮点
- 🔁 模型自动回退 —— 选定若干模型供应商分组后,某次请求在产出任何内容之前失败(模型不存在、无凭据、额度耗尽、限流、上下文超限、服务端 5xx 等),自动切换到分组中的下一个模型继续同一个请求,直到找到可用模型。正在进行的 agent 任务无感继续,不会中断。
- 🔄 任务自动重试 —— 所有候选都失败且错误属于非模型异常(网络超时、连接断开、服务端错误等)时,用同一模型自动重试(指数退避),直到成功或达到最大次数。用户主动中断永不重试。
- 🤖 全自动模式 —— 开启后,任务运行中的权限索取、确认提问(含
exit_plan_mode 方案评审)全部按默认允许自动应答,任务不再卡在人工验证;每次自动决策都追加到工作区根目录的 AUTO-MODE.md 审计文件,随时可查。
- 🏠 主页一键开关,按对话隔离,即时生效 —— 对话框输入条(发送按钮旁)有「自动驾驶 / 模型回退」组合胶囊(机器人头像 / 层叠图标),单击即为当前对话开/关全自动模式与模型回退:每个对话持有自己的开关状态(随设置持久化),一个对话的开关绝不影响另一个对话;子代理会话自动继承其所属对话的开关。设置页的勾选是全局总闸兼默认值:关闭某项后,胶囊上对应的半边按钮从所有对话中消失(两项都关闭则整个胶囊不再出现在输入条上),任何对话也都无法再单独启用该模式;开启时,没有单独开关过的对话沿用全局默认值,每个对话可在胶囊上单独开/关。开关即时生效,无需重载或重新布防:任务执行中的任何时间点切换,下一个模型请求 / 下一次审批事件就按新状态执行——关闭某对话的模型回退后,该对话的循环级恢复也一并解除武装。
- 📐 响应式胶囊(抗挤压) —— 两种情况下胶囊自动收起按钮文字、只保留机器人 / 层叠图标(悬浮说明与无障碍标签保持可用,空间恢复后文字即时回来):一是输入条空间紧张(窗口收窄、侧栏展开);二是模型名过长把输入条尾侧按钮组挤到换行——胶囊通过几何测量(尾组换行 / 行内溢出)感知挤压并立即收缩。输入条定位兼容
<textarea> 与新版 DSH 的 Lexical contenteditable 输入框;空闲空间按尾组左缘 − 其左侧紧邻组右缘计算(输入条中间的 modes 组宽度不会被误算成空闲空间)。恢复文字带自校准迟滞区间:文字只在尾组空闲空间大于「文字本身重新占据的宽度 + 安全余量」时才回来(下限 140px,实际宽度按当前字体实时测量),因此文字回来永远不会把尾组再次顶到换行、换行也不会再次把文字挤掉——长模型名停在临界宽度时也不会在紧凑/展开之间来回闪烁、顶动页面。
- 💨 实时气泡通知(按对话隔离) —— 模型切换、恢复执行与全自动模式的每次自动审批(权限 / 提问 / 方案)都会在对话页输入框上方实时弹出气泡:宿主通过 SSE 长连接毫秒级推送(事件写入即到达,无需等待轮询),SSE 不可用时自动降级为间隔轮询兜底。每条事件都携带产生它的对话的会话 id,气泡只出现在该对话的页面上:切换对话时旧气泡立即消失,切回某个对话时若其状态仍在 30 秒内则原样重现,互不串台;无法归属到对话的事件不弹泡(仍完整记录在「运行状态 → 工作日志」)。与文档截图一致的胶囊样式,同时只显示一个(新通知替换旧通知)、保留 30 秒;气泡通过逐帧跟随锚定输入框,输入框位移(侧栏收起、窗口缩放、输入框随内容长高)时气泡保持相对位置同步移动。
🎬 界面速览
安装后会在 设置 界面新增一个「自动驾驶」标签页(排在「插件」之后)。子菜单栏与「刷新 / 放弃修改 / 保存」操作按钮固定在页面上方,不随内容滚动;页内通过子菜单在「回退分组 / 任务重试 / 运行状态 / 全自动模式」四个功能页之间切换,各功能页的未保存草稿在切换后保留:
🔁 模型回退
「回退分组」子页提供:
- 总开关:启用 / 停用自动回退,标题旁实时显示「已启用 / 未启用」徽标;这是全局总闸兼默认值——关闭后输入条胶囊上的「模型回退」半边从所有对话消失,任何对话都无法单独启用;开启时每个对话可在胶囊上单独开关(会话隔离,互不影响);
- 分组循环:勾选参与循环的模型供应商(数据来自「模型」设置页已配置的供应商目录),通过「上移 / 下移」调整优先级(数字越小越先尝试);每个供应商卡片显示模型目录预览与启用状态;
- 模型池勾选:每个分组的卡片内展开「模型池」复选框列表,只有勾选的模型会作为该分组的回退候选(即使它当前健康);不对某分组做任何勾选时,默认其目录内全部模型可用;
- 欠费标记:某 API key 触发钱包级失败(402 / 额度耗尽)时,该账户被自动标记并显示红色「欠费」徽标;之后的模型跳转全部跳过该 key 的模型组(含其 modlens 包装路由)。点击徽标旁的「×」手动清除后,该 key 的模型组立即回到回退池;
- 运行状态卡片(「运行状态」子页):各分组的目录模型数(0 个模型时红色警告)、链预览、「未选定分组也纳入保护」开关;
- 工作日志卡片(「运行状态」子页):插件最近 200 条介入记录(切换 / 恢复 / 循环重试 / 耗尽 / engaged 决策 / 自动允许),最新在前,支持手动刷新与 5 秒自动刷新;数据来自宿主内存环形缓冲,经
/dsh-model-fallback/api/log 同源接口读取(与宿主日志同源)。
🔄 任务重试与看门狗
「任务重试」子页是独立的功能页,拥有独立的「放弃修改 / 保存」按钮组;保存时通过设置通道写回 retry 字段,宿主端在下一个请求立即生效。输入内容即时校验(非法数值按 0 处理,错误码自动去空)。在只读连接(非本机回环)下所有控件禁用。
| 设置项 | 控件 | 说明 |
|---|
| 启用任务重试 | 复选框 | 任务重试总开关;标题旁实时显示「已启用 / 未启用」徽标 |
| 循环级重试 | 复选框 | 流中途失败时,由 agent 循环丢弃半截消息并在同一模型上整轮重发(与次数/延迟设置共用);挂载于官方 agent/request-error 恢复点,原生供应商策略优先,原生放弃后接管 |
| 轮次保活(继续唤醒) | 复选框 | 整轮任务死于瞬态模型异常(「本轮运行失败」出现)时,自动向对话发送「继续」唤醒任务,直到成功;仅可重试类失败触发,全部分组欠费即停止(默认开启) |
| 首次唤醒延迟(ms) | 数字输入框 | 轮次失败后等待多久发送第一条「继续」;连续失败按退避倍率递增 |
| 退避倍率 | 数字输入框 | 连续失败时唤醒间隔的倍率(2 = 5s → 10s → 20s…,可调) |
| 退避上限(ms) | 数字输入框 | 「继续」间隔的上限;长时间故障下仍保持约每分钟一次探测 |
| 最大连续次数 | 数字输入框 | 同一会话连续自动「继续」的上限;0 = 不限(一直尝试直到成功或模型池全部欠费),超过后停止直到用户介入或成功回合 |
| 幂等护栏 | 复选框 | 发送「继续」前检查上一步工具调用:结果未确认或已成功时附加指引,提示模型先确认状态、不要重复执行副作用操作(默认开启) |
| 继续文本 | 文本输入框 | 中断后自动发送的文本;支持 {code} {message} {status} {tool} {turn} {errorCount} {elapsed} 占位符,留空用默认「继续」 |
| 超限时的继续文本 | 文本输入框 | 达到输出 token 上限时自动发送的文本(同样支持占位符);留空用默认「继续」 |
| 自定义可恢复错误 | 多行文本 | 每行一个普通文本片段(不区分大小写);命中错误码、HTTP 状态或消息时强制视为可恢复,优先于内置分类(参考 dsh-auto-continue 的 retryableErrorPatterns) |
| 最大重试次数 | 数字输入框 | 候选链全部失败后,用原始模型重试的最大次数(0 表示不重试) |
| 基础延迟(ms) | 数字输入框 | 首次重试前的等待时间;后续每次翻倍,单次上限 10 秒 |
| 可重试错误码 | 文本输入框 | 逗号、空格或中文逗号分隔;留空 = 除已知模型错误外全部重试 |
| 全部供应商兜底 | 复选框 | 默认关闭:选定分组全部失败后,其余已配置供应商的模型追加为最后候选(可能消耗未勾选供应商的额度)。默认行为严格限定候选池 = 勾选的分组,未勾选一律不参与 |
| 重试间隔预览 | 只读提示 | 按当前设置实时计算,如 500ms → 1s → 2s |
| 恢复默认 | 按钮 | 一键把各设置项填回默认值(需再点「保存」生效) |
多层恢复机制:
- 活性看门狗(防卡死):请求静默超过阈值(默认 5 分钟,最低 250ms,可调)即判定卡住——强制关闭上游流、合成
WATCHDOG_IDLE 失败,走正常的切换 / 重试管线重新激活任务。报错之外,无报错的"假死"同样处理:连接正常且未欠费的请求(402/403 会立即报错,不会静默)长时间无任何输出时照样触发。
- 静默重发:判定卡住后先用同一模型原样重发(默认 2 次,可调),重发预算用尽才切换候选模型。
- 传输层底线:
TRANSPORT(供应商流连接失败)、NETWORK_ERROR、TIMEOUT、CONNECTION_CLOSED 四类瞬态错误始终可重试,不受自定义码表限制。
- 万能码分类:
PI_AI_ERROR 等 catch-all 码按 message 里的真实状态分类——429/5xx/超时/断连 → 瞬态可重试;401/402/403 → 持续型靠切换。中文网关文案("模型服务暂时不可用,请稍后重试"等)同样按瞬态识别;中文欠费措辞("余额不足"等)按账户级失败识别并自动落欠费标记。默认可重试码表也已对齐 pi-ai 适配器词汇(SERVER/RATE_LIMIT/PI_AI_ERROR),链耗尽后的同模型重试在复查时同样携带 message 重新分类(修复此前"retry 1 surfaced non-retryable (SERVER); giving up"的过早放弃)。
- 账户级跳转:402 欠费(钱包级失败)时,同账户(路由 + modlens 包装共享同一 key)的所有剩余候选立即跳过,直接切换到另一个 API key 的模型组;401/403 可能是模型级权限,仍逐个尝试。
- 防死循环:所有恢复机制都有硬上限——链切换以候选池大小为界;循环重试以 maxRetries 为界(按会话/轮次/步骤/供应商持久计数);看门狗终止只是产生一种普通失败码进入同一管线。单请求总尝试次数 ≤ 候选数 × (1 + maxRetries),数学上不可能死循环。
- 切换可视化(对话内可见):每次切换发生时,宿主向发起请求的会话追加一条原生
llm/retry 事件(与 DSH 自带的「已重试模型请求」同一渲染通道),对话消息流里出现一条可展开的重试行,写明「A → switching to B」;同一请求内的所有切换共享同一 retryId,UI 把它们合并成一条不断增长的切换链。切换是真实的调用线路替换:每个接管候选都经 ctx.llm.adapterStream({ provider, model }) 直连新供应商/模型派发,日志与对话行里的每个模型报出各自的真实错误(402/上下文超限等)。链耗尽后的同模型重试(带指数退避)也追加到同一条链(带 delayMs 倒计时),切换与重试构成一条完整的恢复时间线。这些事件经 SSE 长连接(/dsh-model-fallback/api/events)实时推送到客户端,输入框上方气泡毫秒级弹出(单实例、保留 30 秒,且只弹在发起请求的那个对话页面上),输入区的对话框模型显示同步为接管模型(同步同样只响应该会话自己的切换事件)。
- 目标自动恢复:候选池耗尽、任务与目标一起暂停时,只要 3 秒内检测到发生过模型切换,插件自动恢复目标——任务在切换后的新模型上继续执行,而不是停在「已暂停」。护栏:仅全自动模式开启时生效、每 agent 每 10 分钟至多一次、目标轮次预算仍由 DSH 强制。
- 轮次保活(继续唤醒):当一整轮任务仍然死于瞬态模型服务异常——候选链耗尽、同模型重试烧完、循环重试用尽,即对话里出现「本轮运行失败」时——插件在退避延迟后自动向对话发送一条「继续」,像用户手动输入一样唤醒 agent 循环,任务接着跑,直到成功完成。触发条件从严:仅可重试类失败(瞬态错误码或瞬态 message)才触发;用户主动中止、账户欠费(402/额度耗尽)、确定性模型错误(上下文超限、模型不存在等)不触发——即便外层错误码看起来可重试(如
PI_AI_ERROR 包裹 "context length exceeded"),也会先按 message 里的确定性措辞拦截,绝不盲发「继续」让任务在同一超限请求上死循环。停止条件即用户口径:选定分组全部欠费(或未选任何分组)时不再发送,让失败如实呈现;否则连续失败按指数退避(默认 5s 起、倍率 2、封顶 60s,均可调;maxConsecutive 可设上限,0 = 不限),任一轮正常结束(任务推进/成功)即复位退避计数。唤醒文本是模板:continueText 支持 {code} {message} {status} 占位符(如 → );达到输出 token 上限()时用 唤醒(如「继续输出,不要重复已生成的内容」)。(,默认开)在发送前检查上一步工具调用:结果未确认(回合在工具执行中途夭折,如 可能已经推上去了)时,续跑消息会提示模型先确认状态、不要重复执行;工具已确认成功时说明已完成、请勿重复;工具失败则不加护栏(重试本来就是目的)。(,每行一个普通文本片段)可让 provider 专属但确认安全的失败强制走自动续跑,优先于内置分类。agent 上存在已暂停/阻塞的目标时优先恢复目标而不是注入消息,避免与目标机制打架;agent 正在运行或收件箱已有待处理消息(用户自己在操作)时跳过本次注入,等下一次失败再武装。
🤖 全自动模式
「全自动模式」子页(默认关闭,需显式开启):
| 设置项 | 控件 | 说明 |
|---|
| 启用全自动模式 | 复选框 | 总开关,默认关闭;标题旁实时显示「已启用 / 未启用」徽标。这是全局总闸兼默认值——关闭后输入条胶囊上的「自动驾驶」半边从所有对话消失,任何对话都无法单独启用;开启时每个对话可在胶囊上单独开关(会话隔离,互不影响) |
| 自动允许权限审批 | 复选框 | 工具执行所需的权限审批(命令沙箱提权、文件写入确认等)一律按允许处理 |
| 自动应答确认提问 | 复选框 | ask_user_question 等人工选择按推荐项(第一个选项)自动应答 |
| 自动批准方案评审 | 复选框 | exit_plan_mode 提交的实施方案按「批准」自动通过 |
| 工作区审计日志 | 复选框 | 把每一次自动决策追加到会话工作区根目录的 AUTO-MODE.md |
审计文件(AUTO-MODE.md)
全自动模式开启后,插件在会话工作区根目录创建/追加 AUTO-MODE.md:
# ⚡ 全自动模式操作审计(dsh-auto-driving)
> 本文件由插件「全自动模式」自动写入:所有被自动允许的权限审批与自动应答的人工确认都会记录在此。
> 关闭方法:设置 → 全自动模式 → 关闭「启用全自动模式」。
| 时间 | 类型 | 内容 | 结果 |
| --- | --- | --- | --- |
| 2026-08-31T12:30:00.000Z | 权限审批 | 工具 bash — sandbox escalation to danger-full-access | 已自动允许 |
| 2026-08-31T12:31:12.000Z | 方案审批 | Approve this plan and leave plan mode? | 已自动允许 |
审计写入为 fire-and-forget:日志失败绝不阻塞它所记录的审批流程。
📦 安装
在终端执行(desktop 是 DSH Desktop 默认 profile 名):
dsh plugin --profile desktop add /path/to/dsh-auto-driving
或从任意其它目录用相对路径(会被锚定到调用目录):
cd /path/to/dsh-auto-driving && dsh plugin --profile desktop add .
安装完成后重启 DSH Desktop(宿主端插件与客户端设置页都在启动时装载)。
[!NOTE]
本仓库 / 项目名为 dsh-auto-driving;插件包名(package id)目前仍为 dsh-model-fallback,dsh plugin remove 等命令请继续使用该 id。
卸载:
dsh plugin --profile desktop remove dsh-model-fallback
⚙️ 工作原理
- 宿主端在官方
llm/stream waterfall 上注册监听器:凡是 provider 命中已选分组的请求都会被包一层回退循环。
- 首次尝试原样走完整条下游链(请求日志、checkpoint、invariants 全部有效);某次尝试失败且没有产出任何可见内容(text / reasoning / tool-call 增量)时,直接在适配器边界(
LlmRuntime.adapterStream)换下一个候选模型重新派发——上层 invariant 校验的是原始请求头,因此不会被中途换模型破坏。
- 候选链 = [当前请求的 provider/model] + 按优先级排列的已选供应商各自的模型目录(去重)。每个供应商的模型目录经
llm.listModels 缓存 5 分钟,并在设置变更 / 适配器拓扑变化时自动刷新。
- 包装器永不因目录为空/读取失败而失效:目录为空或读取抛错只缩小候选池(错误后 30 秒重试,不锁 5 分钟),单候选链仍然启用——请求级同模型重试始终在线。
- 未选定分组的请求同样受保护(
protectUnselected,默认开启):请求模型打头、选定分组作为候选池,任何请求都有恢复网;关闭后未选定分组原样放行。
- 全自动模式挂钩:在宿主
approval/request waterfall 上注册监听器,启用时直接返回 allowed-once 并写入审计,停用时 next() 交还原有应答链(fail-closed 语义不变);包装 userQuestions.registerProvider 的 UI provider——带意图(intent)的问题按 intent.approve 标签应答,无意图的问题按第一个选项(推荐项)应答,自由文本按默认应答,任何子开关关闭时完整委托给真实 provider;同时向 system prompt 注入说明,让模型知道无需等待人工、直接继续任务。
- 所有挂钩在设置变更后立即生效,无需重启。
- 已产出内容后(流中途)的失败不会切换——把两个模型的半截输出拼进同一条助手消息会破坏会话;这类错误按原样交给上层(loop 自身的重试策略)处理。
- 用户主动中断(abort)永远不会触发切换或重试。
- 回退过程写入宿主日志(
model-fallback: … failed (CODE: …); switching to provider/model),成功恢复时记录 request recovered on provider/model;每条路由每 5 分钟记录一条参与决策日志(model-fallback: engaged for <provider>/<model>, chain=N candidate(s) / not engaged: ...),随时可在宿主日志确认插件是否真实介入。
💾 设置存储
配置保存在 settings 命名空间 model-fallback(随 settings.yaml 持久化),字段:
| 字段 | 类型 | 说明 |
|---|
enabled | boolean(默认 true) | 总开关(全局总闸兼默认值:关闭后胶囊的「模型回退」半边从所有对话消失,sessionModes 里的旧钉住状态也不再生效;开启时每个对话可在输入条胶囊上单独开关,见 sessionModes) |
providers | string[](默认 []) | 参与循环的供应商路由,按优先级排序 |
protectUnselected | boolean(默认 true) | 未选定分组的请求也纳入保护(请求模型打头,选定分组作候选池) |
allProvidersFallback | boolean(默认 false) | 选定分组全部失败后,其余已配置供应商的模型追加为最后候选 |
arrears | Record<string, boolean>(默认 {}) | 欠费账户标记:key 为账户名(modlens- 前缀剥离后的 API key 名)。宿主检测到 402/额度耗尽时自动写入 true 并把该账户全部模型移出回退池;在设置界面点击「×」清除后立即恢复 |
providerModels | Record<string, string[]>(默认 {}) | 每个供应商分组勾选的模型池:value 为可作回退候选的模型 id 列表;缺省 key = 全部模型可用,空数组 = 该分组不贡献候选 |
sessionModes | Record<string, { auto: boolean|null, fallback: boolean|null }>(默认 {}) | 按对话隔离的开关(会话隔离开关):key 为会话 id,fallback / auto 为该对话单独钉住的模型回退 / 全自动状态;null 或缺省 = 跟随全局默认。钉住状态仅在对应模式的全局总闸开启时生效(总闸关闭即不可用,旧钉住不会复活被全局关闭的模式)。由输入条胶囊通过 POST /dsh-model-fallback/api/session-state 写入;子代理会话沿 parentSession 链继承所属对话的钉住状态 |
watchdog.enabled | boolean(默认 true) | 活性看门狗开关 |
watchdog.idleTimeoutMs | number(默认 300000) | 静默判定阈值(ms),最低 250 |
watchdog.resends | number(默认 2) | 判定卡住后同一模型原样重发的次数 |
retry.enabled | boolean(默认 true) | 任务重试总开关 |
retry.maxRetries | (默认 ) |
全自动模式保存在独立命名空间 model-fallback-auto(同样随 settings.yaml 持久化):
| 字段 | 类型 | 说明 |
|---|
enabled | boolean(默认 false) | 全自动模式总开关(默认关闭;全局总闸兼默认值——关闭后胶囊的「自动驾驶」半边从所有对话消失,任何对话都无法单独启用;开启时每个对话可在输入条胶囊上单独开关) |
autoAllowPermissions | boolean(默认 true) | 自动允许权限审批 |
autoAnswerQuestions | boolean(默认 true) | 自动应答确认提问 |
autoApprovePlans | boolean(默认 true) | 自动批准方案评审 |
workspaceLog | boolean(默认 true) | 写工作区审计日志 |
也可直接编辑 settings.yaml:
model-fallback:
enabled: true
providers:
- deepseek
- your-gateway
# 欠费账户标记(宿主自动写入,可在设置界面手动清除)
arrears:
your-gateway: true
# 每个分组的模型池勾选(缺省 = 全部模型可用)
providerModels:
deepseek:
- deepseek-chat
- deepseek-reasoner
watchdog:
enabled: true
idleTimeoutMs: 300000
resends: 2
retry:
enabled: true
maxRetries: 3
baseDelayMs: 500
retryableCodes:
- NETWORK_ERROR
- TIMEOUT
- CONNECTION_CLOSED
- RATE_LIMITED
- SERVER_ERROR
- "500"
- "502"
- "503"
- "504"
keepAlive:
enabled: true
delayMs: 5000
maxDelayMs: 60000
backoffFactor: 2
maxConsecutive: 0 # 0 = 不限
guardTools: true
continueText: "继续" # 支持 {code}/{message}/{status}/{tool}/{turn}/{errorCount}/{elapsed}
continueTextMaxTokens: "继续输出,不要重复已生成的内容"
retryablePatterns: |-
# 每行一个普通文本片段,命中即强制视为可恢复(可留空)
# Upstream rejected the request as invalid
⚠️ 注意事项
- 回退只在**已启用(有可用凭据)**的供应商之间进行;未启用凭据的供应商失败后会被跳过(失败信息见宿主日志)。
- 循环对命中分组的所有 LLM 请求生效(agent 主任务、标题生成等),这也是"未完成任务继续"的来源。
- 跨供应商切换时,历史消息会以 provider-neutral 形式发送,pi-ai 适配器会在历史路由与目标路由不一致时自动丢弃不可用的重放状态(与 DSH 原生行为一致)。
📄 License
MIT