DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Restart Task — DeepSeek Harness 插件(DSH Plugin)
← Plugins
R

dsh-restart-task

Restart Task

DSH web plugin:针对中断回合的三层恢复策略——静默回合内重试、针对截断输出的回合内保持连接,以及可选择启用的后续回合;包括编辑器中的回合控制、将随附的发送按钮接管为“继续”按钮,以及设置卡片。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:zchuxi/dsh-restart-task#4b7a88dad651c874192549ec389af71c68f0ba0a
README兼容性版本
设置卡片(上):标题与实时策略摘要、优先级说明、请求失败的原地重试分组、输出超限的同轮续写分组设置卡片(下):回合已结束后的继续方式、触发条件、继续指令与呈现方式(隐藏折叠记录、轮次导航标记)

兼容性与来源证明

Restart Task 以 dsh-restart-task 发布,当前版本为 1.2.1。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/9/25

版本

1.2.1stable
2026/9/25

相关插件

正在加载相关插件…

最新版
1.2.1
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/9/25
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录

相关插件

继续浏览 productivity-workflow 分类下经过校验的插件。

Acp App@deepseek-ai/dsh-acp-appdsh ACP 配置文件包:基于 dsh-base 的仅限自动化的 JSON-RPC stdio 和进程生命周期管理Client Ui Task Board@linxin666/dsh-client-ui-task-board面向 DSH Web GUI 的主机权威任务面板,支持实际会话执行、主机 cron 调度以及可选的跨平台空闲睡眠保护;以挂载方式提供,无需修改 DSH 源代码。Web All@linxin666/dsh-web-allDSH Web UI 全家桶聚合插件:一键安装全部功能插件(task-board / git-graph / pet / remote-web-ui / web-ui-settings / skin-center / community-plugins / compat shim)。compat 桥接层已并入本包(src/client),无需独立 compat npm 包。Codex Ui@michengai/dsh-codex-uiDSH Codex UI — 为 DeepSeek Harness Web 提供 Codex 风格侧栏、工作区会话树、全局搜索和轮次导航

README


description: "DSH Web 插件:把被打断的回合救回来 —— 同一步内静默重试、输出被截断时同轮续写、可选的新一轮继续;输入框为空时发送键本身会变成「继续」。" kind: "package-reference"

dsh-restart-task

中文 | English

DeepSeek Harness Web profile 插件:输入框里的一个回合恢复控件、它背后的三段式恢复策略,以及一整套设置卡片。

所有设计只服务于一条原则:恢复过的请求不该留下痕迹。

层级何时生效对话记录里多出什么
1. 同一步内重试模型请求失败什么都不多 —— 连一行都没有
2. 同轮续写(keep-alive)回复撞上输出上限不新开回合;续写那一行被隐藏
3. 新开一轮继续回合已经以失败收场一个回合 + 一条折叠记录(默认隐藏)

0. 适用版本

针对 DSH 0.1.7-rc.1 及其后的 0.1.x 编写,并且在 loader 会真正校验的地方写明了这一点:peerDependencies["@deepseek-ai/dsh"] 为 >=0.1.7-rc.1 <0.1.8-0 || >=0.1.8-rc.1 <0.2.0-0(强制字段),声明性的 engines.dsh 与之保持一致,插件自带的 @deepseek-ai/schemastery 为 ^3.18.4(第一个支持 .volatile() schema 的版本,下面的配置模型依赖它)。

范围为什么要写成两段:node-semver 只有当范围里某个比较符与该版本的 major.minor.patch 元组完全一致、且自身带预发布标签时,才放行预发布版本。写成看着更宽的 >=0.1.7-rc.1 时,0.1.8-rc.1 这类下一条补丁线的 rc 会被静默排除,用户只会撞上 ERESOLVE(或 loader 直接跳过这个 bundle)。所以每个受支持的元组各占一段,并用 <0.2.0-0 把下一个大版本挡在外面 —— 0.2.0 上能否工作没有验证过,宁可让 loader 明确跳过。

版本之所以关键,是因为本插件赖以生存的两条接缝都变了:

  • 设置不再是插件自己注册的命名空间。 0.1.7 里 settings.register(namespace, schema, { applies: 'live' }) 与 settings.get(namespace) 已被移除且没有继任者:插件的 Config 本身就是它的设置表单,而表单用 profile 条目 id 来寻址。见 §8。
  • 插件消息不再是 { kind: 'plugin', plugin } 包装。 会话格式 v4 要求「生产者自有」的 source.kind;本插件用自己的包名。见 §4。

在更旧的运行时上,bundle 会在启动时被 loader 按兼容性规则直接跳过(并给出 dsh plugin allow-version 这条明确的豁免途径)—— 这是诚实的结局:上面两条接缝在那里并不存在。

1. 手动控制

当上一轮已经坏掉时,输入框动作区会出现一个圆形控件,位置就在产品自带发送键的左侧。

会话状态控件行为
上一轮以 error / aborted / interrupted 结束继续 —— 请宿主从中断处接着做
上报了 agent 级错误(lastAgentError)继续
只有最后一次发送失败(promptError)重发上一条输入 —— 历史里没有可接续的内容
正在运行、空白会话、无事可做不渲染

结果反馈就在按钮内部(转圈 / 绿勾 / 红色 !,说明文字在 title 里)。本插件渲染的任何东西都不插入布局:早先的版本在输入框下方加一行提示,结果每次状态变化都会让输入框抖动。

1.1 发送键本身(sendBecomesContinue,默认开)

「有继续可做」恰恰是产品发送键最没用的时刻:输入框为空时产品会把它置灰(empty || blocked || uploadsPending)并降到 40% 不透明度 —— 没东西可发,也没东西可点。所以圆形控件不是唯一的门:当有继续可做且输入框没有别的内容可发时,本插件把同一个动作交给发送键。

输入框发送键
空,且上一轮中断变为可用、警告色、圆形箭头、aria-label="继续上次任务" —— 点一下即从中断处继续
你正在输入不动:它照旧是发送键,送你写的内容
智能体正在运行不动:它照旧是产品的 停止 / 排队发送 / 插话发送

机制上,这是本插件唯一写入产品元素的地方,而且写得很克制:

  • 输入框的主按钮是 inline JSX、没有席位(composer 只开放 conversation.input.{left,right,model,attachments,activity,dock,overlay,permission,plan},没有 submit 席位),所以遍历只给那个元素打上本插件自己的属性 data-dyn-continue="1",外观全部由本插件样式表负责;从不触碰 React 拥有的 class。
  • 只接管产品用不了的那个控件 —— 判据是它自己的 disabled。这正是「你正在输入时它仍是发送键」的原因。接管即清掉 disabled;React 若写回,观察器会把接管补上。
  • 点击在 document 的捕获阶段拦下并就地终止:产品的处理器挂在 React 根容器(更靠下),所以发送路径不会同时触发;窗口里其它点击一律放行。
  • 动作不重写:接管后的按钮点击的是本插件自己的圆形控件(接管期间被 CSS 隐藏但仍在 DOM 里),两者共用同一个 busy 闸门、同一个双击围栏、同一份结果反馈 —— 按钮会跟着变绿 / 变红 / 转圈。
  • 交还时只移除属性并恢复产品原本的 aria-label,刻意不写 disabled:错误地「启用」空发送是无害的(产品会拒绝空草稿),而错误地「禁用」会吃掉你自己的消息。
  • hideContinueRow 与这个开关互相独立,卡片里都能关掉。关掉接管后,圆形控件是唯一的入口,发送键则完全保持产品原样。

已知边界:它读的是产品渲染而不是声明式 API,若某版本改了主按钮的 class 或去掉 data-composer-card,接管会静默停止(不会报错,圆形控件照常工作);在产品因自身原因禁用发送键的少数状态(有上传在跑、等待授权)下,接管同样会生效;接管期间产品自带 tooltip 仍显示「发送消息」(可访问名与 title 是本插件的);Enter 键刻意没动 —— 产品把它注册为只读固定动作,劫持它会让「空草稿 + 回车」变成不请自来的开工。

2. 第 1 层 —— 同一步内重试(默认开,不可见)

agent/request-error 是瀑布(waterfall):监听器返回 { kind: 'retry' } 且不调用 next(),就会让循环就地重跑同一步。本插件先按自己的退避等待(retryBaseDelayMs × 2^attempt,上限 60s),然后认领这次恢复,最多 maxRequestRetries 次 —— 或打开 retryForever 无限重试。

过程中不向会话追加任何东西 —— 没有指令消息、没有额外回合、没有重试行 —— 所以「失败后成功」的请求在对话记录里和一次成功完全一样。确定性客户端错误(4xx,429 除外)在任何预算被考虑之前就交给下游,因此「无限重试」也不会把错误的 API Key 变成死循环。每步的计数器在 turn/end 清空。

产品自带的 dsh-llm-retry 也监听 agent/request-error,而 dsh-base 把它挂在本插件之前,所以它在更上游:先用自己的策略(普通模式 5 次、自己的退避),耗尽后才用 next() 委派,此时本插件的预算才开始。两套预算因此是相加的 —— 若希望更早放弃,请调低产品那一侧,或关掉本插件这一侧。

3. 第 2 层 —— 同轮续写(不新开回合)

当一步因为模型撞上输出上限而结束时,回合即将关闭。agent/turn-stopping 就是可以提出异议的边界:调用 agent.steer(...) 会在边界提交前把新输入放进收件箱,循环随即在同一个回合内再跑一步。

「回合即将关闭……提出异议的监听器会 steer(agent.steer(...)),机器重新读取收件箱:新的 steering 会再跑一步,谁都不会关闭回合。」 —— dsh-agent/lib/types/runtime-types.d.ts,agent/turn-stopping

于是轮次数、轨道和历史都不移动。受 maxTurnContinues 约束(每回合),且仅当该步的结束原因真的是 max-tokens 时才触发 —— 这个原因取自实时 agent/assistant-stream 的 finish 帧,比边界早一帧,因此无需翻会话日志。

两条诚实限制:

  • 该边界只在正常停止时到达。aborted 与 error 是经 throw 离开循环的,所以第 2 层对它们永不适用。
  • 被 steer 的指令仍是一条落盘的 user/message —— 它渲染为一条折叠记录(见 §6),永远不会变成你的发言气泡。

4. 第 3 层 —— 新开一轮继续(默认关,可见)

当回合真的以失败收场时,继续动作会通过 agent.followup 发出一条插件来源的消息:

{ role: 'user',
  content: [{ type: 'text', text: '<continue instruction>' }],
  source: { kind: 'dsh-restart-task', form: 'notice',
            summary: '继续上次中断的任务' } }

source.kind !== 'user' 是它不会变成用户气泡的原因:它渲染为一条折叠记录 —— 而且因为这条消息开启了这一轮,产品会把它渲染成「非人类触发」通知而不是上下文行(§6)。这个 kind 必须是本生产者自己的名字:会话格式 v4 要求生产者自有的 source kind,并拒绝已退休的插件命名空间包装(kind: 'plugin' + plugin 字段),后者只会在 v3→v4 转换既有历史时被抬升。写成旧形状会让整个回合失败并报 format v4 message requires a producer-owned source kind,因为继续消息是在该步能运行之前就被追加的。但它仍然是一个新回合,所以对话记录会多出那一行、轮次轨道会多出一格 —— 这正是 autoContinue 默认关闭、并提供手动控件按需使用的原因。

resend(手动、仅发送失败时)是唯一会发出用户文本的路径 —— 一条从未到达宿主的输入不在历史里,重发它是唯一可能有效的做法。它不注册乐观回显,因此不会出现两行。

5. 为什么已结束的回合不能就地继续

这是 agent 循环的结构性约束,不是缺 API。事实如下,备查:

  • 新回合号只能是 previous + 1,并且总是以 turn/start 宣告(dsh-agent-loop/lib/index.js,turn())。
  • turn/end 在 finally 块里被追加,任何路径都会执行,所以坏掉的回合在任何插件听说它之前就已持久关闭。
  • 每一条进入 step 的消息都以 user/message + surfaceOp: 'append' 追加;插件无法为「唤醒循环的那条消息」选择非追加的 surface op。
  • 会话事件是只追加的:Session 接缝与持久化层都没有 remove / truncate / rewrite API,因此继续动作无法在事后被剪掉。(dsh-rewind-plugin 也不删除事件 —— 它追加替换 surface,而替换副本只对模型可见。)
  • interrupted 标记由崩溃修复路径写入,它在事后关闭一个孤儿回合;循环从不实时发出它,恢复会话也不会唤醒驱动。

所以「能否不新开回合就继续」的完整答案就是第 1、2 层;对已经结束的回合,第 3 层是唯一可能的做法。

6. 隐藏续写行

第 2、3 层的记录存在,是为了让模型读到指令;它们是管道,而 Chat 给它们只有两种形态 —— 两种都无法用样式表单独选中:

形态何时靠什么识别
折叠的上下文行消息没有开启该轮(第 2 层:被 steer 的步骤)一个无值的 data-context-source 属性,生产者名字是文本
非人类触发通知(data-chat-flow-kind="turn-trigger")消息开启了该轮(第 3 层、手动继续)什么都没有 —— 未知 source kind 连「请求触发」都区分不出来

所以 hideContinueRow(默认开)是一个小型的对话记录遍历,而不是一个选择器:凡是能被归到本插件名下的行都会被打上 data-dyn-restart-row="1",再由一条条件样式隐藏本插件自己的标记:

[data-chat-flow-kind="context"][data-dyn-restart-row="1"],
[data-chat-flow-kind="turn-trigger"][data-dyn-restart-row="1"] { display: none !important; }

归属有两条来源,且从不猜测:

  1. 来源标签恰好等于本插件 source.kind 的上下文行。
  2. 本插件所开回合里的触发通知行 —— 轮次来自实时会话事件窗口(ownWakingTurns),窗口由当前显示会话的输入框控件发布。回合的输入是在它的 turn/start 之后追加的,绝不会在之前(持久顺序是 turn/start(7) → user/message(kind: dsh-restart-task) → …),所以一条消息恰好是它落入回合的第一条输入时,就说明这一轮由它开启。第 1、3 层经 next-turn 收件箱投递,这正是它们的行会成为触发通知的原因;第 2 层的 steer 由已开启回合的某个 step 认领,因此永远不是第一条输入,也永远不占轮次。窗口尚未就绪时该规则被跳过,规则 1 仍然覆盖第 2 层。

第二条来源是累积的。客户端手里永远只是一段分页窗口,折叠一个已完成的回合或加载另一片历史都会把它换成更小的窗口;只按当前窗口重算的集合会忘掉它已经认出的回合,轨道标记就会回来,直到下一片历史到达。因此这个集合在同一会话内只增不减,切换到另一个会话时重新开始。

其它生产者的行 —— 时间上下文、AGENTS.md、技能、cron、子代理结算、目标回合 —— 永不打标、永不隐藏。遍历是幂等的(React 重渲染不会碰别人的属性),开关关闭或插件卸载时会清掉自己的标记,读不懂的视图就什么都不做,而不是去隐藏一个归属不明的行。因此「丢失归属」只是观感上的损失(某行继续显示),绝不会变成对别人行的错误猜测。

诚实说明:两条规则读的都是产品渲染而非声明式 API,所以某个版本改了 flow 属性或改了来源标签文案,遍历就会停止匹配(不会坏掉 —— 那行只是重新显示出来);事件尚未进入已加载窗口的回合在被加载之前无法归属 —— 轨道自己的全量 outline 知道每一轮,但它只带 {turn, prompt, response} 而没有生产者 kind,所以「没有人类提问的一轮」在那里和子代理、目标、cron 触发的轮次无法区分,硬猜就会隐藏别人的轮次(这是未来版本可以通过暴露触发 kind 补上的唯一缺口);另外这条规则不会碰第 3 层回合在轨道上的那一格(见 §7)。

7. 轮次轨道

第 3 层的继续是一个真实回合,所以轨道会为它多出一格 —— 而因为这一轮没有人类提问,那一格的悬浮卡只能退化成匿名的「第 N 轮 / Turn N」。这个退化不是插件能改的数据:轨道是 inline JSX、没有扩展点,而整会话的 turnOutline 投影只接受 source.kind === 'user' 的提问预览(dsh-session-turn-outline/lib/types/projection.js)。

因此 continuedRailMarks 把它当作呈现问题,提供三种模式:

模式效果
hide(默认)该轮的轨道标记(连同悬浮卡)不显示
preview标记保留(仍可点击跳转);悬停时不再显示「第 N 轮」,只显示该轮答案预览
keep产品自身行为,完全不干预

机制上这是一次很小的 DOM 遍历,不是样式表技巧。轮次来自 ownWakingTurns 归属给本插件的轮次集合(§6)—— 是轮次本身,而不是被打标的行:第 2 层的 steer 会隐藏「人类自己那一轮」里的一行,但那一轮的轨道标记绝不是本插件该碰的。每个轨道标记通过它的可访问标签(它唯一的逐轮属性)匹配到轮次,然后按配置模式打标:

[data-dyn-continued='hide'] { display: none !important; }
nav[data-dyn-continued-preview='1'] [class*='_previewPrompt'] { display: none !important; }

本插件自己那一轮的标记被打上 hide/preview,其它标记完全保持产品原样。因为 hide 是给标记本身 display: none,悬浮卡就失去了挂载对象:那一轮的「第 N 轮」提示根本不会出现。插件卸载或模式设为 keep 时,本插件写下的标记会被全部清除。

诚实限制:只能标记视图真正渲染过的轮次(远离已加载窗口的轮次在滚动到之前保留标记,进入范围后观察器会补标);标记是虚拟化的,所以任意时刻只有轨道自身滚动窗口里的标记会被标记;它通过可访问标签匹配,因为标记本身没有轮次属性;轨道的 DOM 在 0.1.7 变了(标记容器里的 button[data-index],没有逐标记位置包装),本遍历读的就是这个;hide 是隐藏标记而不是删除它,轨道的虚拟化仍知道这一轮 —— 其余标记保持自己的编号,它留下的空隙可能仍在。

8. 设置卡片

插件页 → dsh-restart-task(侧边栏里由插件管理器拥有的那一页)。卡片注册进该页的 plugins.bundle.config 席位,键是 bundle 的包名(dsh-restart-task)—— 这是该页自己的契约(「自带配置的 bundle」),也是 0.1.7 重构后唯一保留的席位(旧的 settings.plugin.item 标签页已消失)。只有当宿主提供本插件的设置条目时才会注册,所以从未挂载该行的部署看不到本卡片的任何痕迹。

卡片拥有自己的全部外观:可折叠标题(带实时策略摘要)、四个分组、逐字段的「已自定义 / 恢复默认」,以及带写入状态和「全部恢复默认」的页脚。

设置卡片(上):标题与实时策略摘要、优先级说明、请求失败的原地重试分组、输出超限的同轮续写分组

设置卡片(下):回合已结束后的继续方式、触发条件、继续指令与呈现方式(隐藏折叠记录、轮次导航标记)

字段默认含义
retryFailedRequeststrue在同一个 step 内重试失败的请求(不可见)
maxRequestRetries3同轮重试次数上限(0 = 不重试)
retryForeverfalse忽略预算,重试到成功或中止
retryBaseDelayMs2000退避基数:2s、4s、8s… 上限 60s
keepAliveOnMaxTokenstrue让被截断的回合保持开启,在回合内续写
maxTurnContinues5单个回合内允许续写几次(0 = 不限)
autoContinuefalse坏掉的回合之后自动开新一轮继续(可见)
delayMs1500中断之后等待多久再继续
maxConsecutive3连续继续的轮数上限(0 = 不限)
onAbortedtrue系统取消(父 agent / hook)之后继续;用户主动停止永不继续
onErrortrue模型请求失败之后继续
onInterruptedtrue崩溃遗留孤儿回合之后继续
sendBecomesContinuetrue输入框为空时,把继续交给产品发送键(见 §1.1)
hideContinueRowtrue在 Chat 里隐藏本插件自己的折叠行
continuedRailMarkshide轨道如何呈现本插件继续出的轮次(hide / preview / keep)
continueText(内置)发给模型的继续指令

autoContinue 关闭时,onAborted / onError / onInterrupted 会被禁用,因为它们只描述那一层何时触发。

值是怎么进出的(0.1.7 的配置模型)

设置命名空间已经不存在,所以接线是这样的:

  • 宿主半边的 Config 就是表单。 每个字段都标了 .volatile(),这是它能被实时编辑的前提;loader 随后给 apply 每个字段一个引用,config.<field>.get() 就是三层策略每次决策时读取的东西。一次写入在下一次中断时生效,无需重启,也不涉及任何插件事件。
  • 表单用 profile 条目 id 寻址,即 restart-task —— 本包 cordis.patch.yml 插入的那个 id。它不是包名,而浏览器半边无法 import 宿主半边的常量,所以两半和 patch 文件各写一遍;回归测试会断言三者一致。
  • 写入经 configForms.get('restart-task')(浏览器半边为该条目共享的表单):快照给出被服务的表单值加原始用户层,逐字段 set / unset,底下是带版本号的改动队列。被拒绝的写入返回 false,冲突则重新读取宿主而不是覆盖它。
  • 宿主不会再生成一个页面:插件在可选的 settings 子上下文里注册 settings.configure({ auto: false }, ctx.fiber)。这个子上下文是刻意可选的 —— 没有设置域的组装仍然能跑三层恢复和输入框控件,只是卡片和两个对话记录遍历不出现。

当客户端与宿主不一致时

浏览器半边可能比正在运行的宿主新 —— 这是插件更新后、profile 重启前的常态。此时条目可能没有被服务,或缺少较新的字段,卡片会说明而不是让写入失败:宿主未提供的字段以只读显示「宿主还是旧版本:重启 DSH 后这一项才会生效」;宿主根本没有提供该条目时提示「宿主没有提供本插件的设置入口」;非回环页面(设置仅在该会话内存中)也会被标注。

结构

  • cordis.patch.yml —— profile bundle patch,插入 restart-task 行;这个 id 同时就是设置条目 id,文件里的注释也这么写。
  • lib/index.js —— 宿主半边:每个字段都 .volatile() 的 Config、同轮重试、同轮续写、可选的继续回合、continue-task 命令。
  • lib/client.js —— 浏览器半边:window.__ModuleLoader__.load bundle,导出 apply + inject(['slots', 'timer'])、输入框控件、插件页卡片(挂在可选的 configForms 子上下文上),以及给本插件自己的行打标、呈现其继续出的轮次、并在输入框为空时把继续交给产品发送键的对话记录遍历。
  • restart-task.test.mjs —— 回归测试(见下)。
  • screenshots.json —— 市场详情页展示的 1–8 张截图(见 §8);docs/awesome-dsh-plugin-entry.yml —— 提交或更新 awesome-dsh-plugin 收录时复制过去的那一条目。截图刻意放在本仓库: 换图只需往这里推一次,不用去那边提 PR、等维护者。

输入框控件的形状刻意用 !important 钉住:它住在产品的 composer 工具行里,那一行有自己的 button/svg 规则。设置卡片使用产品的 --dsw-alias-* token,并在不依赖任何东西的前提下匹配其卡片外观(.5px 边框、16px 圆角、14–16px 头部内边距)。两个注入的 <style> 标签都带 data-plugin,因为模块系统按该属性归属样式:没有归属的标签会被下一个物化的插件认领,并在那个插件卸载时被删掉。

有三处区域被直接断言 —— lib/index.js 的 auto-logic、lib/client.js 的 core-logic,以及宿主模块自身的导出 —— 所以出厂代码跑的就是被测的决策:

node dsh-restart-task/restart-task.test.mjs   # 305 条断言,两半都覆盖

测试分四层,因为移植到 0.1.7 时坏过三种方式,而行/轨道的呈现只能看它对文档的实际效果:

  1. 规则 —— 重试预算(确定性 4xx、429、倍增、60s 上限、不限次)、保活门槛(只有 max-tokens、每轮上限)、回合级门槛(坏掉的原因、逐原因开关、连击上限)、对话记录读取(只认人类提问、坏掉与干净的结束)、开轮识别、控件模式选择、头部摘要文案。
  2. 接线 —— 所有身份字符串一致(patch 条目 id ↔ 宿主 ENTRY_ID ↔ 客户端 ENTRY_ID,包名 ↔ bundle 席位键 ↔ 生产者 kind),每个 Config 字段都是 volatile 且每个被服务的字段都由卡片渲染,已退休的设置接缝、席位、选择器全部消失,行隐藏规则以本插件自己的标记为键,两个样式标签都写明归属。
  3. 宿主行为 —— 真正 import lib/index.js,并用桩 Cordis 上下文驱动:命令只发一条生产者来源的消息;401 被委派而 500 等待后原地重试;等待中被停止则委派;被截断的一步在回合内 steer,而完成的一步不会;回合级继续只在开启时发生,且永不发生在用户停止之后,也永不越过连击上限。
  4. 对话记录、轨道与发送键(桩 DOM) —— 加载浏览器半边并应用到一份假 document 上:里面有一个本插件开的轮次、一个人类开的轮次(其中带着本插件的同轮 steer)、别的生产者的上下文行与触发通知,以及一个主按钮初始为禁用的输入框。结果是:本插件自己的行被打标而别人的没有;steer 行只靠来源标签就能认出,而触发通知需要会话窗口;本插件那一轮的轨道标记在 hide 模式下被隐藏、preview 模式下被标记,而人类那一轮与外部轮次的标记永不被动;发送键被接管、被点击(点击在产品看到之前就被截停,改由圆形控件执行)、在输入框有内容时不被接管、开关关闭后被交还;窗口缩小时(折叠/分页)不会把已经认出的轮次收回。事件夹具抄自真实的 session.v4.jsonl,包括决定归属的 turn/start → 消息顺序。

延伸阅读

下面这些页面是本插件依赖的接缝,方便逐条对照它声明的版本核对:

  • DeepSeek Harness —— 运行时、插件契约,以及全文引用的各包:dsh-agent(Agent 外观与 agent/request-error / agent/turn-stopping 分发)、dsh-agent-loop(回合与 step 边界)、dsh-settings(配置表单)、dsh-session-format(v4 来源规则)、dsh-client-ui-conversation(输入框及其席位)、dsh-client-ui-chat(flow 行与轮次轨道)。
  • dsh-client-shortcuts —— 本插件刻意不改绑的固定输入动作(fixed.send = Enter、fixed.newline = Shift+Enter、fixed.complementary = Ctrl/Cmd+Enter);「忙时回车」到底是排队还是插话,是 ui-conversation 设置里的 busyEnter。
  • dsh-llm-retry —— 挂在本插件上游、耗尽自身策略后委派给它的产品重试执行器。
  • dsh-client-ui-plugin-manager —— 拥有 plugins.bundle.config 席位(本卡片注册的地方)的插件页。

模型体验

分层的模型可见性:

  • 第 1 层不可见:被重试的请求用同一段持久历史重建同一个 step,没有任何重试事件、延迟或提供方错误到达模型。
  • 第 2 层多出一条 user/message,其 source.kind 是本插件名:模型读到指令,人类读到一条折叠记录(默认隐藏)。
  • 第 3 层是一个新回合,唯一输入就是这条生产者来源的消息。因此模型看到的是一句「接着做」的指令,绝不会是用户原话的第二份拷贝。

KV Cache 影响

被重试与被继续的请求都会重建与它所恢复的那次尝试相同的前缀,因此在该提供方的规则下缓存复用得以保留。插件本身不贡献 token:不加工具 schema、不加系统提示词文本,除了第 2、3 层真正需要的那一条继续指令之外不加任何上下文。

开发说明

  • 测试就是规格。 restart-task.test.mjs 从出厂源码里切出纯函数区域,并真正驱动宿主模块,所以某条规则不可能在这里通过而出厂代码另做一套。
  • 事件夹具取自真实日志。 决定归属的 turn/start → 消息顺序,是从一份真实 session.v4.jsonl(每批追加一个 zstd 帧)里读出来的 —— 在这之前,规则的第一版假设了相反的顺序,于是静默地什么都认不出来。
  • DOM 遍历读产品渲染是刻意的,因为产品没有为它们作用的那些东西提供席位(折叠上下文行的来源是文本,触发通知没有任何来源属性,轮次轨道是 inline JSX,输入框主按钮也是 inline JSX)。每一处都被写成「读不懂就什么都不做」而不是猜,各自的边界都写在它旁边。

安装 / 移除

直接从本仓库安装(无需 registry):

dsh plugin --profile web add github:zchuxi/dsh-restart-task
# 或:dsh plugin --profile web add https://github.com/zchuxi/dsh-restart-task
# 或从本地 checkout:dsh plugin --profile web add link:<路径不含空格>
# 然后重启 profile(宿主 + 页面):宿主半边变了
dsh plugin --profile web remove dsh-restart-task

desktop 是 Electron 专属的 profile 名,CLI 会拒绝它;请改在插件管理器里用同一个 git 地址安装。

bundle 通过其 dsh.bundle.patch 清单字段加入 dsh.profile.bundles;移除依赖后,下一次启动就不再插入那一行。

本地 checkout 请放在不含空格的路径下。dsh plugin 会把参数经 shell 转发,含空格的路径会被拆成若干个错误依赖。

环境要求。 DSH >=0.1.7-rc.1 <0.1.8-0 || >=0.1.8-rc.1 <0.2.0-0(声明为会被真正校验的 peerDependencies["@deepseek-ai/dsh"] 范围,因此更旧的运行时会在启动时按 loader 的兼容性提示跳过 bundle,而不是在内部某处出错)、Node >=24,以及一个运行时依赖 —— @deepseek-ai/schemastery ^3.18.4(第一个 schema 能 .volatile() 的版本)。开发用 checkout 请用 npm install --legacy-peer-deps 安装:@deepseek-ai/dsh 这个 peer 是插件运行所在的宿主,不该作为构建依赖被拖下来。

浏览器半边是热重载的:dsh-client-hmr 会轮询每个客户端 bundle 的修改时间,把重建后的版本换进正在运行的页面,所以编辑 lib/client.js 保存即生效。宿主半边需要重启 profile —— agent/turn-stopping 和新的设置键都要等 lib/index.js 重新加载后才存在。