dsh-plugin-message-retry
Verifieddsh-plugin-message-retry · v0.1.0 · MIT · Web UI
Hover any user message in the DeepSeek Harness Web conversation and re-send it from that exact point: the session returns to the state before the message, then the message is sent again
Install
dsh plugin add dsh-plugin-message-retry Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-plugin-message-retry
给 DSH Web 对话里的每条用户消息加一个「重试」按钮:鼠标悬浮到用户语句上时出现,点击即可重新发送。
用法
把鼠标移到任意一条用户消息上,消息右侧的操作行(复制、分支旁边)会出现一个重试按钮:
| 操作 | 行为 |
|---|---|
| 点击 | 从此处重新发送:把会话回退到这条消息之前(最后一个已结束的轮次)的状态,再把这句话发一遍。这句话之后的所有轮次都会被丢弃,模型拿到与当时完全一致的上文重跑,浏览器自动跳转到回退出的新会话。效果就是「回到这句话还没发过的状态,重新发一次」。 |
| Shift + 点击 | 不改历史,只把这句话作为新一轮追加发送到当前会话。偶尔确实想多要一轮回答时才用。 |
按钮状态用颜色反馈:进行中变暗禁用,成功显示主题色,失败显示错误色并把具体原因写进原生 tooltip。
第一条消息的处理:它之前没有任何已结束的轮次,但同样不会退化成追加。 因为 fork 的
atSeq必须指向一个真实存在的事件(Host 校验events[boundary]?.seq !== boundary),最小的一次 fork 也仍然包含会话的第一个事件 —— 「第一条消息之前」这个状态用 fork 表达不出来。所以此时会新建一个空白会话 (ctx.sessions.create,它按定义就是空的),把这句话作为那个会话的第一条发出去。
安装
本包是一个标准的 DSH bundle,用官方插件管理方式安装,不在 profile 里手工插行。
包结构
dsh-plugin-message-retry/
├── package.json dsh.bundle.patch + dsh.client(dual-face)
├── cordis.patch.yml bundle 的 patch 层:insert 自己那一行
└── lib/
├── index.js Host 半边
└── client.js 浏览器半边
package.json 里两个声明各司其职:
"dsh": {
"bundle": { "patch": "./cordis.patch.yml" },
"client": { "platform": "web", "external": [] }
}
cordis.patch.yml 把自己作为 loader 行插入:
- insert:
- id: message-retry
name: "dsh-plugin-message-retry"
bundle 自插入是官方惯用法 —— dsh-web-app 就是这么给自己插 web-runtime 行的
(- id: web-runtime / name: '@deepseek-ai/dsh-web-app')。同一个包里同时声明
dsh.client,于是这一行既是 Host 半边,也是浏览器 roster 条目:client-modules
扫描 loader 行、读该包 manifest 的 dsh.client,据此提供
/plugins/<id>/client.js 并组合 window.__DSH_BOOT__。
注意 name 必须是裸包名,由 profile 的 node_modules 解析。写成文件路径会同时
踩两个坑:Node ESM 不能 import 目录(ERR_UNSUPPORTED_DIR_IMPORT),而且会让
client-modules 的包查找走偏。
安装
三种方式任选其一,都会跑 pnpm 把包装进 ~/.dsh/profiles/<name>/node_modules/、写进
profile 的 package.json 依赖与 dsh.profile.bundles,随后 Plugins 页就会列出它,
带启用开关和 Uninstall。装完刷新页面(客户端 bundle 需要一次刷新)。
1. 从 npm 安装(推荐)
app 内的 Plugins 页签 → Add plugin → 输入包名:
dsh-plugin-message-retry
其他 profile 用 CLI:
dsh plugin --profile web add dsh-plugin-message-retry
2. 直接从 GitHub 安装
DSH 把 github:owner/repo 当作一等安装写法,不必先发布到 npm:
dsh plugin --profile web add github:susices/dsh-plugin-message-retry
app 内的 Add plugin 输入框同样接受这个写法。
3. 从本地目录安装(开发时用)
Plugins 页签 → Add plugin → 输入本包的本地目录路径,例如:
C:\Doc\Projects\Jailbreak\dsh-plugin-message-retry
桌面版 profile 由 Electron 应用独占管理,CLI 会按名字硬拦:
$ dsh plugin --profile desktop add ... error: profile "desktop" is managed exclusively by the Electron application所以桌面版一律走 app 内的 Plugins 页签,三种写法都填在同一个输入框里。
卸载
在 Plugins 页签点 Uninstall(CLI 管理的 profile 用 dsh plugin --profile <name> remove)。
对话记录不受任何影响。
设计说明
为什么不用 conversation.chat.node 插槽
conversation.chat.node 按 user 键注册看起来是「正规」做法,但它的契约写着
replaceRisk: "shadows-shipped-ui" —— 注册即替换官方 UserMessageNodeView。
那样就得自己重写气泡、附件栏、引用标签、复制/时间操作,并且会随 DSH 升级悄悄跑偏。
因此本插件改为增强 DOM:官方 Chat 已经把所需的一切作为稳定公开面暴露出来 ——
- 每个 Chat 行都包在
[data-chat-flow-kind=<ChatNodeKind>]里,所以用户消息可定位为[data-chat-flow-kind="user"]; - 用户行的操作行本来就通过官方
[data-actions-reveal=hover]规则做悬浮淡入,按钮用_actions/_action类名。
插件只往这个已有的操作行里追加一个按钮,配色、间距、悬浮淡入全部继承官方样式表, 不替换、不覆盖任何官方 UI;卸载后 DOM 与原来完全一致。
为什么是纯客户端
两个操作都已经是文档化的 Client 服务,会代理到 Host:
- 重发:
ctx.sessions.binding(id).session.prompt([{type:'text',text}], 'queue') - 从此处重试:
ctx.sessions.fork({ sessionId, atSeq })
所以不需要 Host 半边、不需要 typert Remote 描述符、也不需要动态插件的客户端授权审批。
lib/index.js 只是一个空 apply,存在的意义是让这个包出现在 Host Loader 里,
浏览器半边再通过 dsh.client 声明被发现。
两处容易踩的坑
styles不是包形式的全局量。 包形式的 client bundle 只拿到require;styles.insert是动态插件运行时的符号。官方各 bundle 都是自己document.createElement("style")并挂data-plugin-css做幂等保护的,本插件照做。atSeq是闭区间。 在消息自己的 seq 上 fork 会把它连同整轮一起保留, 所以必须先回退一个事件;这也是「从此处重试」真正的语义所在。
fork 之后子会话只是被编目、尚未 retain,因此发送前要用
sessions.using(childId, ...) 打开它的历史。
渲染开销
对话流式输出时 DOM 会持续变动。MutationObserver 的批处理被合并到 「每动画帧最多一次全量扫描」,避免长对话里每批变更都做一次全文档查询。
测试
node test/smoke-client.mjs lib/client.js
node test/behaviour.mjs lib/client.js
smoke-client.mjs:模拟 DSH 的window.__ModuleLoader__加载协议 + 最小 DOM + 假ctx, 断言样式表插入、会话探针注册、按钮挂进官方操作行。故意不提供styles符号, 以便任何对它的裸引用都会抛错 —— 这正是上面第 1 条坑的回归防护。behaviour.mjs:用假sessions服务跑通两条路径,断言点击发到当前会话、 Shift+点击在消息前一个 seq 上 fork 并把文字发进子会话、以及重复文本按出现次序 解析到不同 seq(同一句话说过两次是很正常的情况)。