🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

消息撤回删除 kyle123740/dsh-message-recall

DeepSeek Harnessv0.2.1
kyle1237408 天前有提交已提供中文文档扫描:未发现敏感能力

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

36.5/100
综合分
2
GitHub 星标
187
npm 周下载
—/5
用户评分

数据截至 2026/10/24 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add @kyle123740/dsh-message-recall
npm 源 · @kyle123740/dsh-message-recall 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · other
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 8 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@kyle123740/dsh-message-recall @ 0.2.1
✓Node 引擎要求 >=22.19 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 07:32:45

安全扫描自动 · 静态分析源码
v2.7.0
🟢 扫描:未发现敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。

发起外部网络请求
查看扫描证据(共 1 处,展示前 1 条)
  • 发起网络请求
    lib/client.js:120
    const response = await fetch(ROUTE, {
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-llm@deepseek-ai/dsh-session
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 撤回单条消息
  • 删除单条消息
  • 删除此处及之后
展开完整介绍(121 字)↓

删掉不喜欢的 AI 回复,或删掉发错的提示词——每条消息都能撤回、删除、删除此处及之后(DSH 插件,append-only 墓碑,模型上下文与界面同步)。DeepSeek Harness 逐条消息撤回与删除,需要 dsh >= 0.1.7。

v0.2.22026-10-23
修复兼容性声明:原开区间 >=0.1.7-alpha.1 对 0.2.1-rc.2 求值为 false(预发布版 semver 规则),宿主会据此判 incompatible-version。现按线枚举为 ^0.1.7-alpha.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1。已对照真实 0.2.1-rc.2 核心逐符号核过,三套自检全绿。
v0.2.12026-09-29
0.2.0 的 npm tarball 里带着旧的加载名(\cordis.patch.yml\ 的 row \ ame\ 与 client bundle 的 \__ModuleLoader__\ load id 还是 \dsh-message-recall\),从 npm 安装的用户会碰到插件加载失败(route 404)。两个引用都已改为 \@kyle123740/dsh-message-recall\,HTTP 路径 \/dsh-message-recall\ 不变。
v0.2.02026-09-29
修复一个真 bug(用户发现):墓碑原先写 \content: []\,在不跳过空 user 消息的 provider 上,整条请求被打回 400 \message content cannot be empty\ —— 表现为「换模型就报错」。DeepSeek 自家路由会跳过空消息,所以只在换 provider/模型时暴露。实测 SenseAudio(deepseek-v4.1-flash)捕获到 400。 三层防御: 1. 墓碑非空化:占位文本「[已撤回]」。它不会出现在对话里(surface 替换节点不是 append-surface 事件),遮蔽也从不依赖它。 2. 新增 \migrate\ 维护动作:把会话里残留的旧空墓碑替换成带占位符的新墓碑,保留原 \ emoved\ 列表,客户端遮蔽推导不变。并且每次操作都顺带执行 —— 旧会话第一次被碰到就自愈。幂等。 3. \gent/pre-step\ 兜底:任何请求发出前,空 user 消息(空数组/空串/纯空 text 块)一律补占位文本 —— 连别的 producer 写的空消息也兜住。 另外:包名改为 \@kyle123740/dsh-message-recall\(npm 上裸名 \dsh-message-recall\ 属于无关发布者,裸名发布不了);\inspect\ 新增 \legacy\ 标记;已对照 0.2.0-rc.1 桌面壳的内置核心验证兼容(格式仍 v4,surfaceOp 契约不变)。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-message-recall 本插件做什么:删掉不喜欢的 AI 回复,或者删掉已经发出去的错误提示词。 给 DeepSeek Harness 会话里的每一条消息加上「撤回 / 删除」:鼠标悬停到任意一条消息上,右上角浮出一组小按钮。 你的提问 [ 撤回 ] [ 删除 ] [ 删除此处及之后 ] AI 的回复 [ 删除 ] [ 删除此处及之后 ] 工具调用行 [ 删除 ] [ 删除此处及之后 ] - 就地生效:不 fork、不新建会话、不换窗口。 - 真删:被删内容同时从模型上下文和界面消失,刷新、重启之后仍然不在。 - 不破坏日志:DSH 的会话日志是 append-only,本插件用的是官方 compaction 的同一套机制(见「工作原理」)。 三个动作 | 动作 | 适用范围 | 行为 | | 撤回 | 只限你自己发出的消息(含运行中插话 steering) | 该消息从模型上下文与界面移除,原文自动回填输入框,改好可直接重发。对话里不留任何痕迹。 | | 删除 | 任意一条消息(AI 回复、工具行都能删) | 删 AI 回复时连带它这一步的工具结果一起删 —— 一个 step = 一次模型调用 + 它请求的工具执行;只删一半会让下一次请求因 call/result 配不上对而报错。点工具行会反查到它所属的那条回复,一起删。属危险动作,需点两下确认。
展开完整文档(README 剩余 1 节)
dsh-message-recall

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

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

你的提问 [ 撤回 ] [ 删除 ] [ 删除此处及之后 ]
AI 的回复 [ 删除 ] [ 删除此处及之后 ]
工具调用行 [ 删除 ] [ 删除此处及之后 ]

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

三个动作

| 动作 | 适用范围 | 行为 |
| 撤回 | 只限你自己发出的消息(含运行中插话 steering) | 该消息从模型上下文与界面移除,原文自动回填输入框,改好可直接重发。对话里不留任何痕迹。 |
| 删除 | 任意一条消息(AI 回复、工具行都能删) | 删 AI 回复时连带它这一步的工具结果一起删 —— 一个 step = 一次模型调用 + 它请求的工具执行;只删一半会让下一次请求因 call/result 配不上对而报错。点工具行会反查到它所属的那条回复,一起删。属危险动作,需点两下确认。 |
| 删除此处及之后 | 任意一条消息 | 从这条起截断到会话末尾,就地清空后半段。 |

适配版本

| 实测通过 | DSH 2.0 桌面版 0.2.1-rc.2(对照自带核心逐个核过:会话格式仍 v4,surfaceOp / sourceEventSeqs 契约不变,createUserMessage、foldSurface、isReplacementSurfaceEvent 均在位;三套自检全绿);以及 0.2.0-rc.2、0.1.7 线(core 0.1.7-rc.2,peer 包 0.1.7-alpha.2),Node 22.23 |
| 声明范围 | dsh = ^0.1.7-alpha.1 \|\| ^0.2.0-rc.1 \|\| ^0.2.1-rc.1(写在 package.json 的 dsh.engines.dsh,宿主安装时会拿它做兼容性拦截) |
| 为什么不是一个 >= | 预发布版本的 semver 规则很严:>=0.1.7-alpha.1 对 0.2.1-rc.2 求值是 false(只有带同一 major.minor.patch 的比较项才接纳预发布版)。所以这里按线枚举,而不是写开区间 |
| 未验证 | 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.2.1-rc.2 的 shell 在跑 profile 里 0.2.0-rc.2 的包)。之所以安全,是因为本插件只碰结构化数据 —— createUserMessage 产出的是普通冻结对象,surface 校验走 JSON 而不是 instanceof。如果你的环境报 peer 不兼容,别去硬改 engines,先跑一遍 npm test 看接口还在不在。

安装

在 DSH 内置终端(或任意能跑 dsh 的终端)里,npm 与 GitHub 两种来源任选:

从 npm 装(作用域包,桌面版)
dsh plugin --profile desktop add @kyle123740/dsh-message-recall

从 GitHub 装(桌面版)
dsh plugin --profile desktop add github:kyle123740/dsh-message-recall

网页版同理,把 profile 换成 web
dsh plugin --profile web add @kyle123740/dsh-message-recall

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

dsh plugin --profile desktop add @kyle123740/dsh-message-recall@0.2.2
dsh plugin --profile desktop add github:kyle123740/dsh-message-recall#v0.2.2
dsh plugin 只是把参数原样转发给 pnpm(add / remove / why / ls …),没有 enable/disable 这类子命令——开与关在「设置 → 插件」里拨动。装完可用 why 核对装上的版本:

dsh plugin --profile desktop why @kyle123740/dsh-message-recall

安装后重启该 profile 一次(Host 半边要重新 import),界面刷新一次(Client 半边要重新取 bundle)。

插件以当前 DSH 进程的权限运行,安装时可能执行代码。装之前请先看源码与许可。

卸载

remove 同样走 pnpm,认的是包名(作用域全名,不是插件 id):

dsh plugin --profile desktop remove @kyle123740/dsh-message-recall

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

与 npm 上同名的包无关

npm 上的裸名 dsh-message-recall 是另一个作者的另一个插件(Jipcon 的 DSH Web 撤回/重编辑插件,见 Jipcon/DSH-plugins),与本项目无关。本插件的包名始终带作用域前缀 @kyle123740/,安装、卸载、写配置时都认这个全名,别装错。

删除后在界面上留下什么

什么都不留。 撤回/删除不会在对话里插入任何占位行或提示 —— 被删的行直接从转录本消失,就像它们从未存在过。这也是 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 钉住:它把墓碑写进真实 @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 的 no user message projects empty content 钉住。

注意:这条只修新写入的墓碑。磁盘上已经存在的旧空墓碑(content: [])仍会让含它们的会话在严格上游上 400,需要在新会话里操作,或把老会话继续留给容忍空内容的路线。

surface 不是转录本。 dsh-session 的注释写得很清楚:surface 刻意遮蔽被替换的区间,那是模型视图;人类转录本要的是 append-origin 事件,否则一次替换就把用户已经读过的对话凭空抹掉。所以「从模型上下文里删掉」不会自动让界面行消失。行的隐藏由客户端从墓碑的 source.removed 推导:

- 单条撤回/删除 → 只隐藏那段 span(含交错在段内的日志行);
- 删除此处及之后 → 隐藏从目标起、到那条墓碑事件为止的区间(turn-error、已重试模型请求、轮次操作栏这些 log-only 行一并消失),墓碑之后新发的内容不受影响;
- 状态完全从日志推导,所以刷新、重启、以及安装插件之前就已存在的墓碑,都会一致生效。被隐藏的行带 data-mcr-hidden 标记,只还原我们自己隐的,绝不碰官方自己的 hidden。

界面侧不 shadow 任何官方渲染器:它挂在 conversation.input.overlay(一个 session 作用域的无头座位)上,按官方 DOM 标记 [data-chat-flow] / data-chat-flow-key / data-chat-flow-kind 给行加按钮。只有能在本会话 Chat store 里解析到持久消息的行才会被装饰 —— 所以侧栏里另一条会话的对话流不会被误加按钮,也不可能误操作别的会话。

边界与注意

- Agent 正在跑时不能改:按钮不出现;即使绕过界面直接打接口,Host 也走 agent.runMaintenance,忙时立刻回 423 AGENT_BUSY,而不是悄悄排队。
- 删完整轮后,那一轮可能只剩 turn/start/turn/end 投影出来的空操作栏(它们是日志事件,不在 surface 里);插件会把这种孤栏一并 hidden 掉。那一轮若还有别的内容,操作栏保持不动。
- 被压缩过的历史(compaction 之后的旧轮次)其 surface 节点已被折叠,无法单独删除;插件会明确报「这一步与其它内容交错(可能已被压缩)」,而不是悄悄做一半。
- 系统提示(surface node 0)永远不能撤回或删除。
- 撤回/删除不可撤销:日志里原文还在(必要时可读会话日志手工找回),但界面与模型上下文里不会再出现。
- 只处理当前会话的行。子 Agent 会话窗口同样会显示按钮,但操作对象就是那条被打开的会话。
- 能力边界:Client 半边(lib/client.js)只向本插件在 DSH 进程内注册的本地路由 POST /dsh-message-recall 发请求(同源 fetch,仅限当前 DSH 实例),没有任何外部网络访问;Host 半边(lib/main.js)不直接读写磁盘,持久化全部经由 DSH 的会话存储 API 完成。整个运行时没有子进程/命令执行,没有凭据访问,没有原生二进制。scripts/ 下的排查脚本会用 node:fs 读会话日志,但它们是开发期工具,不在安装包内(未列入 package.json 的 files)。

HTTP 接口

给二次开发用。注册在 webServer 上的精确路由,无需 Typert:

POST /dsh-message-recall
Content-Type: application/json

{ "sessionId": "session-…", "action": "recall" | "delete" | "deleteFrom", "seq": 42 } // 或 "messageId": "…"

{ "sessionId": "session-…", "action": "migrate" } // 修复旧版空墓碑,不需要目标

响应统一是 { ok: true, value } / { ok: false, error: { code, message } }。
| code | HTTP | 含义 |
| INVALID_REQUEST | 400 | 参数缺失或 action 未知 |
| AGENT_BUSY | 423 | 该会话的 Agent 正在干活 |
| SESSION_NOT_LIVE | 409 | 会话当前没有活着的 Agent(先在界面打开它) |
| TARGET_NOT_FOUND | 409 | 目标不在当前 surface(已删/已压缩/尚未落盘) |
| NOT_A_USER_MESSAGE | 409 | 试图撤回非本人消息 |
| NOT_DELETABLE | 409 | 目标是系统提示 |
| SPAN_NOT_CONTIGUOUS | 409 | 该步与其它内容交错,无法原子删除 |

开发

npm install # 拉 peerDependencies(dsh-session / dsh-llm)
node scripts/verify.mjs # Host:真实 dsh-session 上的墓碑、整步展开、截断、守卫 + HTTP 端到端
node scripts/verify-persistence.mjs # 持久化:真实 JSONL 后端往返(写墓碑 → flush → 从磁盘读回 → 重放)
node scripts/verify-client.mjs # Client:最小 DOM 桩跑通行装饰、两下确认、错误本地化、行遮蔽、语言切换
npm test # 三套一起跑

verify.mjs 里最值钱的两条断言:

- foldSurface(events).nodes === session.surface.nodes —— 日志重放与实时 surface 完全一致(保证「重启后不会复活」);
- 用真实 Session 对象打 handleRecallRequest,核对 200/405/409/415/423 信封。

verify-persistence.mjs 之所以必须有:它才是唯一能证明「重启后仍然删除」的测试。 0.1.0–0.1.2 的 bug 完全躲过了前两套 —— 内存里一切正确,只是没落盘。

排查脚本(只读,不启动 DSH 也能跑)

会话日志是「多个 zstd 帧拼接」的容器,直接 gunzip 只能拿到第一帧,所以仓库自带解码器与几个基于它的排查工具 —— 这套东西是 0.1.3 那个静默丢删除的 bug 里唯一能给出答案的手段:

一条会话里:墓碑覆盖了哪段、段内/段后各有什么;--rows 还会加载真实 client bundle 用插件自己的 shadowRanges 算出「界面现在应该保留哪些行、隐藏哪些行」
node scripts/explain-session.mjs "<会话目录>/session.v4.jsonl.zstd" --rows

最近会话总览:标题、事件数、失败轮次数、墓碑数 —— 用来把截图和日志对上号
node scripts/list-sessions.mjs "$DSH_HOME/sessions" --hours 6

扫所有会话,标出哪些含墓碑
node scripts/scan-replace.mjs "$DSH_HOME/sessions" --hours 24

底层:把容器按帧解开并打印事件;verify-decoder.mjs 用来证明解码器读到了文件末尾
node scripts/read-session-log.mjs "<会话目录>/session.v4.jsonl.zstd"
node scripts/verify-decoder.mjs "<会话目录>/session.v4.jsonl.zstd"

只读诊断接口

排查线上问题时可以用一个读动作,它不需要活着的 Agent,也会读磁盘上的日志:

curl -s -X POST http://127.0.0.1:19387/dsh-message-recall \
-H 'content-type: application/json' \
-d '{"sessionId":"session-…","action":"inspect"}'

返回 { live, disk } 两份摘要:事件数、maxSeq、所有 replace 事件(含 plugin/op/sourceEventSeqs)、以及按 seq 列出的类型流水。用它对比「内存里有、磁盘上没有」最直接 —— 这次根因就是靠它 + 手写解码器定位的。每条墓碑还带 legacy: true/false,直接看出还有几条是旧版空内容、没修过。

修复旧版空墓碑(migrate)

旧版本写的墓碑是 content: []。已经在磁盘上的空墓碑会让严格校验的上游把整个请求打回 400(见「工作原理」末尾),只改新墓碑救不了老会话。修复需要该会话在界面上打开着(否则 SESSION_NOT_LIVE):

curl -s -X POST http://127.0.0.1:19387/dsh-message-recall \
-H 'content-type: application/json' \
-d '{"sessionId":"session-…","action":"migrate"}'
→ {"ok":true,"value":{"action":"migrate","migrated":3}}

或者不敲任何东西:在该会话里做一次撤回/删除,任何一次操作都会顺带把旧空墓碑一并修掉(响应里带 migrated: N)。

做法是追加一条 replace 区间指向旧墓碑自己的新墓碑:空节点离开 surface,新节点带同样的占位文本和原样保留的 removed 列表,所以客户端隐藏行的推导结果与修复前完全一致,日志仍然只追加不改写。幂等:再跑一次返回 migrated: 0。回归见 scripts/verify.mjs 的 legacy repair 一节。

发送前的兜底守卫(默认开启)

migrate 修的是日志,但它要求会话先在界面上打开。所以插件另外挂了一个 agent/pre-step 瀑布监听器:请求发出前扫一遍最终消息列表,把任何空内容的 user 消息([]、""、或只有空 text 块)就地补上占位文本再发。于是

- 没来得及 migrate 的老会话也不会再 400;
- 其它插件或未来版本写出的空消息同样兜得住;
- 只动 user 角色 —— 上游本来就放行 assistant/tool 的空内容,而改写 assistant 会破坏 pi-ai 的 replay 状态(它按块数比对校验)。

命中时 Host 打一行 warn,顺带提醒该会话还有未修复的旧墓碑。守卫是运行期兜底、不写日志,migrate 才是持久清理:平时靠守卫,看到 warn 再对那条会话跑一次 migrate 就彻底干净。不想要可以关:

- id: message-recall
name: '@kyle123740/dsh-message-recall'
config:
emptyContentGuard: false

改完客户端代码要注意:Host 在插件挂载那一刻就把 lib/client.js 的字节读进内存,光改文件不会让页面拿到新版;需要 disable + enable 插件(或重启),再刷新界面。仓库里为此埋了版本戳,加载时会在 Console 打一行 [message-recall] client bundle <BUILD>。

服务器到底在发哪一版

界面跑的是哪份 bundle 容易争。Host 用 combo 形态 serve 插件客户端:

GET /plugins/??@kyle123740/dsh-message-recall/client.js&rev=<rev>

rev 是 sha1("plugin-artifact" \0 len:mtimeMs len:ctimeMs len:size) 的前 12 位(dsh-client-modules 的 artifactRevision),只取 lib/client.js 的文件元数据,不哈希内容。所以「改了文件但页面没变」有两种完全不同的原因 —— Host 没重读(rev 还是旧的),或页面没重载(rev 是新的但浏览器拿着旧的)。用文件 mtime/ctime/size 自己算出 rev 打这个 URL,返回体里的 const BUILD = "…" 就是服务器正在发的版本;和页面 Console 里那行版本戳一比就分清了。

出问题时先看这个

界面 Console 里跑:

JSON.stringify(window.__MCR_DEBUG__, null, 1)

这是每次同步后留下的快照,直接回答三个问题:墓碑事件有没有从日志读回来(tombstones,含 startSeq/endSeq/removed)、每行解析到的持久 seq 是多少(rows[].anchor)、它有没有被判成遮蔽(shadowed)。提 issue 时把这段贴上来,基本一眼定位。

遮蔽的权威来源是墓碑事件自己的 surfaceOp.startSeq/endSeq,不是插件写在 source 上的自定义字段 —— 历史分页是日志的再编码投影,自定义字段属于「可能被动到」的那一类。早期版本只信 source.removed,于是出现了「点完当场生效、重启后内容又回来」这种只在重载路径上发作的现象(v0.1.1 修掉,并加了回归用例)。

目录

lib/main.js Host:路由 + 墓碑写入 + inspect lib/client.js Client:手写 window.__ModuleLoader__ bundle,无需构建 cordis.patch.yml 注册 message-recall 行 scripts/verify*.mjs 离线自检(Host / 持久化 / Client) scripts/explain-session.mjs 单会话判定:墓碑区间 + 期望可见行(--rows) scripts/list-sessions.mjs 最近会话总览(标题 / 事件数 / 失败轮次 / 墓碑数) scripts/scan-replace.mjs 扫描所有会话里的墓碑 scripts/read-session-log.mjs / verify-decoder.mjs 多帧 zstd 日志解码器与完整性自检

致谢

设计时参考了社区里几件相邻的活儿:dsh-turn-hard-delete(整轮硬删,同样走 surfaceOp 替换)、dsh-rewind(同窗口回退 + 工作区还原)、dsh-plugin-session-delete(会话级删除)。本插件的差异点是逐条消息粒度、按 step 自动展开保持 provider 配对、以及从日志推导的界面遮蔽层。

许可

MIT
预览分享卡片长什么样 ↓kyle123740/dsh-message-recall
分享 · DeepSeek Harness Hub
kyle123740/dsh-message-recall

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

回到顶部