dsh-delete-turn
DSH 消息删除插件:按条删除用户消息、按步骤删除思考与工具调用、按整条回复删除助手回答,经官方 surface-replace 契约从模型上下文移除并从可见转录隐藏,原始 append-only 日志不改写。
安装
dsh plugin --profile web add dsh-delete-turn
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✓ |
| 2 列装 | ✓ |
| 3 挂载 | ✓ |
| 4 启动 | — |
四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
dsh-delete-turn
DeepSeek Harness 消息删除插件 —— 把一条消息从模型上下文里真正拿掉,同时从当前转录里隐藏它。 助手回复挂在官方 conversation.chat.assistant-actions 槽位;用户消息、注入上下文与工具调用卡由 DOM 增强补上入口。确认后经官方 surface-replace 契约追加替换事件:模型后续看到的历史里不再有这条内容,而原始会话日志一个字节都不改写。
[中文](#中文) · [English](#english)
中文
<div align="center">
<img src="https://raw.githubusercontent.com/DDDMUC/dsh-delete-turn/main/docs/screenshots/01-row-action.png" alt="用户消息行上的删除按钮" width="820" />
<br>
<sub>▲ 悬停消息行时出现的删除按钮(复制按钮右侧)</sub>
</div>
为什么需要它
DSH 的会话日志是 append-only 的:说错话、发错提示词、模型答偏了,这些内容会一直留在模型上下文里,污染后续每一轮。官方只提供了「压缩」(/compact)这种整段摘要机制,没有单条消息级别的删除。
这个插件把删除放回消息本身:
- 用户消息 → 删这一条;
- 思考卡 / 工具调用卡 → 删这一步(该步的
assistant/message与它请求的tool/result一起走,工具配对永不悬空); - 助手回复(官方操作条)→ 删这条回复连同它的思考、工具调用与注入上下文(你的提问保留);
- 用户消息行 → 还有第二个入口:删掉这一整轮(含提问、注入上下文与全部回复);
- 注入上下文行、失败回合行 → 同样有删除入口。
- 任意多行 → 勾选后一次删掉(多选删除;入口在 Session 标题栏右上角,相邻的窗口会合成一次范围替换,一次请求要么全落、要么全不落)。
特性
- 模型上下文级删除 —— 追加一条官方
surfaceOp: { op: 'replace', startSeq, endSeq }替换事件,被遮蔽的内容不再进入deriveMessages();与宿主/compact同一套官方契约 - 整轮删除(新) —— 删掉一轮(含你的提问、注入上下文、全部回复与工具结果):一次普通的范围替换落地,后面的轮次原地保留、连编号都不变——不重放、不重编号、日志只多一条事件
- 多选删除(新) —— 入口常驻在 Session 标题栏右上角(电源按钮附近):点它进入多选,行内随之出现勾选框,勾几行、点底部浮条的「删除选中」,一次请求带走全部;相邻的窗口在宿主侧融合成一条替换事件,非相邻的按 surface 顺序依次落地;任何一次拒绝都不会只删掉一半。助手最终回答段同样能勾——它没有行级垃圾桶(那个范围属于回复动作条),勾它删的是这一条回复、保留你的提问。
- 转录级隐藏 —— 客户端按
data-chat-flow-*锚点与官方useChat快照定位行,删除后折叠退场;刷新、重启 DSH、换标签页后依旧隐藏 - 日志即台账 —— 隐藏依据直接从日志里的替换事件重建(替换事件的消息 source 标记为本插件),不依赖 localStorage、不需要预检,也不会和其它插件(如压缩)的替换混淆
- 原生视觉 —— 复用官方 primitives 的 Modal / Button 与主题 token,明暗主题自动适配;图标与全部文案为原创
- 中英双语 —— 弹窗、提示、错误原因随系统语言即时切换
- 安全边界 —— 只接受回环地址且 Host 为本机的请求;目标必须是当前 surface 节点、不能碰系统提示词头、回合进行中拒绝、已被删除的目标拒绝并给出机器原因码
截图
<div align="center">
<img src="https://raw.githubusercontent.com/DDDMUC/dsh-delete-turn/main/docs/screenshots/02-confirm.png" alt="删除确认弹窗" width="560" />
<br>
<sub>▲ 删除前确认:说明影响范围,原始日志不改写</sub>
<br><br>
<img src="https://raw.githubusercontent.com/DDDMUC/dsh-delete-turn/main/docs/screenshots/03-after-delete.png" alt="删除后该行消失" width="820" />
<br>
<sub>▲ 删除后:该行从转录中消失,后续内容自然上移</sub>
</div>
安装
# npm 安装(web profile) dsh plugin --profile web add dsh-delete-turn # 本地工作区(开发) dsh plugin --profile web add link:/path/to/dsh-delete-turn
安装后需要完全重启 DSH 进程(宿主侧插件树只在启动时读取)。浏览器端 bundle 由宿主按请求动态 serve,重启后硬刷新页面即可生效。核实是否加载:
dsh --profile web --dump-config # 应出现 "# == dsh-delete-turn" 段落
使用
1. 悬停任意消息行,点击行尾的垃圾桶按钮;助手回复的按钮在官方操作条(复制 / 分叉旁边)。
2. 确认弹窗会说明这次删除的影响范围,点「删除」。用户消息行上还有一个「删除这一轮」入口:它删的是整轮(含你的提问、注入上下文与全部回复),后面的轮次原地保留、直接接上。
3. 目标行折叠退场;模型上下文在下一轮请求重建时不再包含它。
4. 想一次删多条:点 Session 标题栏右上角(电源按钮附近)的勾选按钮进入多选——行内随即出现勾选框,点任意行勾选/取消,底部浮条报出已选条数;点「删除选中」确认后一次性落地,点「退出多选」或再点标题栏那个按钮退出。助手最终回答段也能勾,勾它删的是这一条回复,不删你的提问。
工作原理
UI(官方槽按钮 / DOM 增强按钮)
→ 确认弹窗
→ POST /dsh-delete-turn/delete { sessionId, mode, seq? / messageId? / turn? }
宿主:
sessionQuery.readSession() 读完整日志(live 优先)
自实现 surface fold → 当前 surface 节点 + 历史替换遮蔽集
校验(当前节点 / 区间干净 / 回合已闭合 / 不碰系统头)
session.append('user/message', 短标记占位, {
surfaceOp: { op: 'replace', startSeq, endSeq },
sourceEventSeqs: [被遮蔽的全部 seq],
})
等待官方 session/flush 持久化检查点
→ 返回 { hidden: [{ seq, mode }] }
客户端:
useChat 快照把 data-chat-flow-key 映射到节点,按 hidden 集合折叠行
GET /dsh-delete-turn/state 在每次打开会话时重建 hidden 集合
整轮删除(mode: 'turn'):
一次普通的 surface-replace:窗口 = 这一轮的全部表面节点
(提问 + 注入上下文 + 每个 step 的 assistant/message 与 tool/result)
落一条 turn-less 载体,后面的轮次原地保留、编号不变
—— 不重放、不重编号、不碰代理循环,日志只多一条事件
多选删除(POST 带 targets: [{ mode, seq? / turn? }, ...]):
planBatch:每个目标各自 planRange,全部对着**同一份删除前 surface** 算
按 surface 顺序排序;重叠或相邻的窗口合并成一条 replace(一个扇区)
写入前一次性校验全部窗口(foreign / stale),任何一条不干净就整体拒绝
→ 每个扇区一条载体,按顺序落地;要么全落,要么一条都不落
设计要点:
- 为什么载体是 turn-less 的零宽空格 user 消息(绝不能开合成回合):删除的替换载体只能是不带回合的
user/message。开一个「合成 turn+step」来安放对模型隐身的空system/message会损坏日志:agent loop 只从它自己开的回合推进回合号,外部开掉的回合号会被它的下一个真实回合复用(冷读报turn/start does not open the expected turn),而任何按回合号做隐藏的客户端都会把被复用的那个真实回合整轮吞掉——此故障已在真实会话里复现(消息「被吞掉」)。因此载体退回 turn-less 形状,内容用单个零宽空格(空内容数组会被网关 400user message must have content,可读标记会被模型复述;零宽空格对校验器非空、对模型无字面文本)。代价:模型可能把这条载体读成一条空白 user 消息。历史日志里已经写入的簿记回合(旧版本产生)由客户端按/state的markerTurns隐藏。 - 为什么不读 React fiber / CSS 哈希类名:行定位只用官方
data-chat-flow-*锚点与官方useChat标准 hook,宿主 UI 重构不会静默失效。 - 为什么刷新后仍然隐藏:隐藏台账不是浏览器本地状态,而是日志里替换事件的可重放推导;宿主
/state路由在每次打开会话时重建它。 - 整轮删除不重放、不重编号:只遮蔽这一轮自己的表面节点,后面的轮次一条不动——日志的轮号、后续轮的内容、它们对其他事件的引用都不受影响,也不需要任何跨轮操作。
- 多选的入口在标题栏,不在行里:多选是一个模式,而行内按钮只在悬停时存在——把入口藏在一次悬停之后,就等于藏起来(首版就是这么做的,用户找不到)。因此常驻入口是 Session 标题栏的
conversation.session.header.utilities条目,行内勾选框只在模式打开后出现。 - 没有行级入口的行也能勾:助手最终回答段的范围是「整轮回复」,这份入口在回复动作条上,所以行上不再放垃圾桶(否则同一个范围出现两个按钮);多选态下它仍拿到一个勾选框,指向
reply目标——只有该 turn 出现在view.replyTurns里才给,勾它删这条回复、保留提问。 - 多选是一次请求,也是一次全有或全无:窗口在宿主侧算,客户端只发目标;相邻窗口融合后一次落地更少的事件(两轮相邻时是 1 条而不是 2 条),且所有窗口共用一次校验,正常路径下不会出现「删了一半」(只有在两次 append 之间日志被别处改动、第二个窗口被拒的极端情况下,路由才会明确返回
partial,界面随即重读状态显示真实结果)。 - 广告即承诺:
/state报出turnSeqs(现在哪些行可以整轮删除),它由路由规划用的同一个函数算出(turnableSeqs对每个候选调用planRange),所以界面上的入口不会变成一次注定被拒的点击。
已知限制
- append-only 语义下没有「反删除」:被遮蔽的内容无法真正恢复,删除不可撤销(原始日志仍在,可用官方工具自行重建会话)。
- 回合进行中删除会先停止当前生成:走官方
sessionController.cancel(等价于你按停止键,且保留排队的输入),等这一轮真正收尾(turn/end落地)后再删;停止超时(默认 15 秒)、停止被拒、或正在压缩(压缩没有"停止"语义)时仍会拒绝并给出原因。 - 系统提示词头(surface 节点 0)不可删除。
- 多选删除是一次请求:所有窗口在写入前一起校验(foreign / stale),所以不会出现「删了一半」;但它删掉的东西同样不可撤销。
- 助手操作条的删除范围是整条回复;要只删某一步,请用思考卡 / 工具卡上的按钮。
- 过程行(「已思考」「用时 N 秒」)不单独提供删除入口:它的范围同样是整条回复,与操作条重复,因此只保留操作条那一个。
- 已经被官方压缩(
/compact)移出模型上下文的内容不再显示删除入口:它已经不在上下文里,转录用意保留;入口只在内容仍可删时才出现。 - 整轮删除也删掉你的提问:它删的是整轮(提示词 + 注入上下文 + 全部回复步骤);只删回复请用助手操作条,只删提示词请用用户行上的垃圾桶。
- 这一轮的窗口里如果夹着别的插件留下的、既不是本插件占位符也不是静默载体的节点(例如某个可读的注入内容),不会静默地一并删掉:第一次请求只回报"还会多删 N 项",客户端据此弹出强制确认(主按钮变成「一并删除」),你再确认一次才会带
force真正执行。系统提示词头(surface 节点 0)本身不可删除;含它的那一轮可以整轮删除(头会被排除在窗口外、留在原位)。 - 宿主侧插件树仅在 DSH 启动时加载:安装、更新插件后必须完全重启 DSH。
更新日志
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:20,最后更新 2026-11-04