中文EN

@kyle123740/dsh-message-recall

sessionnpmMIT

给会话里的每条消息加上「撤回 / 删除 / 删除此处及之后」悬浮按钮:以 append-only 墓碑让内容同时从模型上下文与对话界面消失,带空内容兜底,旧删除不会打回严格校验的 provider。已实测支持 DSH 2.0 桌面版。

下载量 / Downloads
703

安装

dsh plugin --profile web add @kyle123740/dsh-message-recall

沙箱测试

这个插件还没测过。提交测试 →

README

简体中文 | [English](./README.en.md)

dsh-message-recall

本插件的用途:删掉不喜欢的 AI 回复,或者删掉已经发出去的错误提示词。

给 DeepSeek Harness 会话里的每一条消息加上「撤回 / 删除」:鼠标悬停到任意一条消息上,右上角浮出一组小按钮。

你的提问      [ 撤回 ]  [ 删除 ]  [ 删除此处及之后 ]
AI 的回复                [ 删除 ]  [ 删除此处及之后 ]
工具调用行               [ 删除 ]  [ 删除此处及之后 ]
  • 就地生效:不 fork、不新建会话、不换窗口。
  • 真删:被删内容同时从模型上下文和界面消失,刷新、重启之后仍然不在。
  • 不破坏日志:DSH 的会话日志是 append-only,本插件用的是官方 compaction 的同一套机制(见「工作原理」)。

三个动作

动作 · 适用范围 · 行为

撤回 · 只限你自己发出的消息(含运行中插话 steering) · 该消息从模型上下文与界面移除,原文自动回填输入框,改好可直接重发。对话里不留任何痕迹。

删除 · 任意一条消息(AI 回复、工具行都能删) · 删 AI 回复时连带它这一步的工具结果一起删 —— 一个 step = 一次模型调用 + 它请求的工具执行;只删一半会让下一次请求因 call/result 配不上对而报错。点工具行会反查到它所属的那条回复,一起删。属危险动作,需点两下确认。

删除此处及之后 · 任意一条消息 · 从这条起截断到会话末尾,就地清空后半段。

适配版本

实测通过 · DSH 0.1.7 线 —— 桌面版 core @deepseek-ai/dsh-base 0.1.7-rc.2,插件解析到的 peer 包 0.1.7-alpha.2,Node 22.23

声明下限 · dsh >= 0.1.7-alpha.1(写在 package.json 的 dsh.engines.dsh,宿主安装时会拿它做兼容性拦截)

未验证 · 0.1.6 及更早。不是「已知会坏」,是没测过,所以不放行

查你自己的版本:

dsh --version

本插件依赖的宿主接口,升级 DSH 后若功能异常,先对照这几处:

接口 · 用途 · 可用性

Session.append("user/message", …, { surfaceOp: { op: "replace", … }, sourceEventSeqs }) · 写墓碑 · dsh-session 0.1.x 起(与官方 compaction 同源机制)

isReplacementSurfaceEvent,子路径 @deepseek-ai/dsh-session/surface · 区分替换事件 · 同上

agent.runMaintenance · 与正在执行的 step 抢锁 · dsh-agent 0.1.x

webServer.register({ kind: "exact" }) · HTTP 路由 · dsh-host-webserver

座位 conversation.input.overlay · 客户端无头挂载点 · dsh-client-ui-conversation 0.1.7

DOM 标记 data-chat-flow / -key / -kind / data-chat-turn · 行装饰与遮蔽 · 0.1.x(data-chat-turn 自 rc.6 起就被社区插件依赖)

ctx.sessions.binding(id).eventSource · 读墓碑 · 0.1.7 面;代码保留 sessions.get(id) 回退到旧面

一个真实的跨实例事实:插件的 peer 包与宿主 core 可能不是同一份模块实例(本机就是 0.1.7-rc.2 的 core 在跑 0.1.7-alpha.2 的包)。之所以安全,是因为本插件只碰结构化数据 —— createUserMessage 产出的是普通冻结对象,surface 校验走 JSON 而不是 instanceof。如果你的环境报 peer 不兼容,别去硬改 engines,先跑一遍 npm test 看接口还在不在。

安装

在 DSH 内置终端(或任意能跑 dsh 的终端)里:

# 桌面版
dsh plugin --profile desktop add github:kyle123740/dsh-message-recall

# 网页版
dsh plugin --profile web add github:kyle123740/dsh-message-recall

想固定版本(可复现安装)就带上 tag 或 commit:

dsh plugin --profile desktop add github:kyle123740/dsh-message-recall#v0.1.7

安装后重启该 profile 一次(Host 半边要重新 import),界面刷新一次(Client 半边要重新取 bundle)。开关也可以随时在「设置 → 插件」里拨动:

dsh plugin --profile desktop disable dsh-message-recall
dsh plugin --profile desktop enable  dsh-message-recall
插件以当前 DSH 进程的权限运行,安装时可能执行代码。装之前请先看源码与许可。

卸载

dsh plugin --profile desktop remove dsh-message-recall

拔除不残留状态:墓碑只是普通日志事件,卸载后那些位置不再被隐藏(被删内容会重新出现在界面里),会话照常可读。

删除后在界面上留下什么

什么都不留。 撤回/删除不会在对话里插入任何占位行或提示 —— 被删的行直接从转录本消失,就像它们从未存在过。这也是 0.1.6 的行为变化:更早的版本会在原位留一行「已删除此处及之后的全部内容 · 原文预览」,既制造视觉噪音,又会把你想删掉的文本片段长期留在对话里。

代价是界面上再也无法「撤销查看」被删了什么。原文并没有销毁 —— 它仍在会话日志里($DSH_HOME/sessions/<项目>/<会话id>/session.v4.jsonl.zstd,append-only),仓库里带了几个只读脚本来读它:

node scripts/explain-session.mjs <日志路径>   # 列出墓碑、它覆盖的区间、区间内/后各有哪些事件
node scripts/scan-replace.mjs <会话根目录>    # 扫描所有会话,找出哪些含墓碑(HAS-TOMBSTONES)

工作原理

1. 定位这条消息在当前 surface(模型可见上下文)里的位置;

2. 追加一条 user/message 墓碑事件,内容是一条非空占位文本(content: [{ type: "text", text: "[已撤回]" }],见下文为什么不能是空的),带 surfaceOp: { op: 'replace', startSeq, endSeq } 与 sourceEventSeqs: [被遮蔽的 seq];

3. surface 是派生模型历史的唯一来源(Session.deriveMessages),所以被遮蔽的内容下一次请求就不会再发给模型;

4. 原始事件仍留在日志里;重新打开会话时靠重放 foldSurface(events) 得到完全一致的 surface —— 删掉的东西不会「复活」。

⚠️ v4 磁盘格式拒绝 source.kind: "plugin"(0.1.3 修掉的「静默丢删除」)

这是本项目最值得记下的一课:surface 替换写对了,不等于落盘成功。

dsh-session-format-v3-to-v4 的原话:

if (typeof value["kind"] !== "string" || value["kind"].length === 0 || value["kind"] === "plugin")
    throw new SessionFormatError("format v4 message requires a producer-owned source kind");

source.kind 必须是生产者自己的 kind(user / model / tool / compact-checkpoint …),"plugin" 被明确拒绝。v0.1.0–0.1.2 的墓碑用的正是 kind: "plugin" + plugin: "message-recall",于是:

1. 事件能 append 进内存日志(Session.append 不做这项校验),界面拿到它并据此隐藏对应的行;

2. flush 时持久化编码器对整批抛错 → 一条都没写进磁盘;

3. 重启后从磁盘重建 → 删除消失,被删内容回到界面和模型上下文。

现象就是「点完当场生效、重启后内容又回来」。v0.1.3 起墓碑声明 kind: "user"(它本来就是 user 角色的消息;user/message 在 v4 里不要求处于开启的 turn/step 内,所以追加在 turn/end 之后合法),插件身份改放 producer: "message-recall"。客户端两种形状都认,页面上残留的旧墓碑不会突然失效。

回归用 [scripts/verify-persistence.mjs](scripts/verify-persistence.mjs) 钉住:它把墓碑写进真实 @deepseek-ai/dsh-session-persistence-jsonl 后端的临时目录、flush、再从磁盘读回,断言「事件全数往返 / 墓碑在盘上 / replace 语义保留 / 重放 surface 与实时一致 / 重开会话不再派生被删文本」,最后一条负向断言专门验证 kind: "plugin" 会被格式拒绝。

两个关键细节,都是这个插件踩过之后才写明白的:

墓碑的内容必须非空,否则严格的上游会把整条请求打回 400。 早期版本写 content: [],依据是 dsh-llm-deepseek 编码层里的 if (message.role === "user" && content.length === 0) continue;。但那是那一个适配器的行为:多 provider 的 dsh-llm-pi-ai 恰恰不跳过——它的 textOnlyContext 把每条非 system/assistant/tool 的消息原样下发成 { role: "user", content: flattenText(message) },空块数组于是变成线上的 content: ""。严格校验的 OpenAI 兼容网关会因此拒绝整个请求:实测 SenseAudio(api.senseaudio.cn,模型 deepseek-v4.1-flash)返回

{"is_bifrost_error":false,"status_code":400,"error":{"type":"invalid_request_error","code":"invalid_request_error",
 "message":"messages: Validation error: message content cannot be empty [...]"}}

(content: [] 得到的是 message content parts cannot be empty;content: " " / "\n" / 任意文字则 200,assistant 与 tool 消息的空内容也放行。)DeepSeek 官方与 TokenRhythm 恰好容忍空内容,所以这个坑只在别的路线上炸——表现就是「同一个会话,换到 SenseAudio 的模型就 400,换回去又正常」。因此墓碑现在写 content: [{ type: "text", text: "[已撤回]" }]:占位文本非空、几乎不占上下文,界面照旧不显示它(surface 替换节点不是 append-surface 事件),隐藏逻辑仍然只认 source.producer 与 replace 区间,不受内容影响。回归由 [scripts/verify.mjs](scripts/verify.mjs) 的 no user message projects empty content 钉住。

注意:这条只修新写入的墓碑。磁盘上已经存在的旧空墓碑(content: [])仍会让含它们的会话在严格上游上 400,需

…(truncated)

以上内容来自 npm 上的 README,我们只做格式化,不改内容。

版本

npm 上共发布:2,最后更新 2026-09-29

链接

GitHub · Homepage · npm

同类插件