dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 消息删除插件:按条删除用户消息、按步骤删除思考与工具调用、按整条回复删除助手回答,经官方 surface-replace 契约从模型上下文移除并从可见转录隐藏,原始 append-only 日志不改写。
DeepSeek Harness 消息删除插件 —— 把一条消息从模型上下文里真正拿掉,同时从当前转录里隐藏它。 助手回复挂在官方 conversation.chat.assistant-actions 槽位;用户消息、注入上下文与工具调用卡由 DOM 增强补上入口。确认后经官方 surface-replace 契约追加替换事件:模型后续看到的历史里不再有这条内容,而原始会话日志一个字节都不改写。
中文 · English
▲ 悬停消息行时出现的删除按钮(复制按钮右侧)
DSH 的会话日志是 append-only 的:说错话、发错提示词、模型答偏了,这些内容会一直留在模型上下文里,污染后续每一轮。官方只提供了「压缩」(/compact)这种整段摘要机制,没有单条消息级别的删除。
这个插件把删除放回消息本身:
assistant/message 与它请求的 tool/result 一起走,工具配对永不悬空);surfaceOp: { op: 'replace', startSeq, endSeq } 替换事件,被遮蔽的内容不再进入 deriveMessages();与宿主 /compact 同一套官方契约data-chat-flow-* 锚点与官方 useChat 快照定位行,删除后折叠退场;刷新、重启 DSH、换标签页后依旧隐藏▲ 删除前确认:说明影响范围,原始日志不改写 ▲ 删除后:该行从转录中消失,后续内容自然上移
# 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" 段落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 载体,后面的轮次原地保留、编号不变
—— 不重放、不重编号、不碰代理循环,日志只多一条事件设计要点:
system/message 会损坏日志:agent loop 只从它自己开的回合推进回合号,外部开掉的回合号会被它的下一个真实回合复用(冷读报 turn/start does not open the expected turn),而任何按回合号做隐藏的客户端都会把被复用的那个真实回合整轮吞掉——此故障已在真实会话里复现(消息「被吞掉」)。因此载体退回 turn-less 形状,内容用单个零宽空格(空内容数组会被网关 400 user message must have content,可读标记会被模型复述;零宽空格对校验器非空、对模型无字面文本)。代价:模型可能把这条载体读成一条空白 user 消息。历史日志里已经写入的簿记回合(旧版本产生)由客户端按 /state 的 markerTurns 隐藏。data-chat-flow-* 锚点与官方 useChat 标准 hook,宿主 UI 重构不会静默失效。/state 路由在每次打开会话时重建它。/state 报出 turnSeqs(现在哪些行可以整轮删除),它由路由规划用的同一个函数算出(turnableSeqs 对每个候选调用 planRange),所以界面上的入口不会变成一次注定被拒的点击。sessionController.cancel(等价于你按停止键,且保留排队的输入),等这一轮真正收尾(turn/end 落地)后再删;停止超时(默认 15 秒)、停止被拒、或正在压缩(压缩没有"停止"语义)时仍会拒绝并给出原因。/compact)移出模型上下文的内容不再显示删除入口:它已经不在上下文里,转录用意保留;入口只在内容仍可删时才出现。force 真正执行。系统提示词头(surface 节点 0)本身不可删除;含它的那一轮可以整轮删除(头会被排除在窗口外、留在原位)。(工作区) —— 会话 id 校验放宽 + 空会话按空状态处理。
bili-whale,~/.dsh/storages//sessions/bili-whale.json)是完全合法的会话,但这里的校验只接受 UUID 形式,于是对它的每个请求都被 400 拒掉 —— 面板上就是「状态加载失败,稍后重试。」。现在按 DSH 自己的语法(宿主侧 schema 就是 z.string().min(1))放宽为 /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,仍然挡住 /、`、..`、空白与控制字符。见 CHANGELOG.md(中文 / English)。
cd dsh-delete-turn
npm test # 62 例:纯逻辑 + 宿主路由 + 真实校验器契约 + 客户端 DOM
node tools/verify-real-session.mjs <session.v4.jsonl.zstd> [--turn N]npm test 里的 test/contract.test.js 与 tools/verify-real-session.mjs 需要已安装的 @deepseek-ai/dsh-session / @deepseek-ai/dsh-session-format-catalog(插件本身不 import 它们)。解析顺序见 tools/dsh-modules.mjs:DSH_SESSION_DIR → 本仓库 / 父目录 / 兄弟仓库的 node_modules → ~/.npm/_npx/*/node_modules(npx 安装的 DSH)→ 裸包名。解析不到时测试会明确失败(而不是静默跳过),因为那意味着契约没有被验证。tools/verify-real-session.mjs 不会写它拿到的文件:它解码日志、跑一次严格冷读、用解出来的事件建一个真实 Session,然后对这个会话驱动插件的真实 HTTP 路由,最后再跑一次严格冷读并打印折叠前后的节点序列。拿真实会话试之前先 cp 到 /tmp(本仓库的验收就是这么做的)。0.1.6-alpha.2、0.1.7-alpha.2 与 0.2.0-rc.1(web 与 desktop profile,Safari / WebKit 与 Chromium 内核均验证)。plugin:dsh-delete-turn,隐藏台账会同时识别 { kind: 'plugin', plugin: ... } 与 { kind: 'plugin:...' } 两代形状。sessionQuery 时回退到 live 会话快照。MIT
▲ The delete action appears when a message row is hovered
A DSH session log is append-only: a wrong prompt or a bad answer stays in the model context and pollutes every later turn. The only official tool is whole-range compaction (/compact); there is no per-message delete.
This plugin puts deletion back on the message itself:
assistant/message and the tool/result it requested leave together, so tool pairs never dangle);surfaceOp: { op: 'replace', startSeq, endSeq } intent; shadowed content no longer reaches deriveMessages(). Same contract as /compact.data-chat-flow-* anchors and the official useChat snapshot, then collapse out. The hide survives a reload, a DSH restart and other tabs.▲ Confirmation states the exact scope; the log is never rewritten ▲ After deletion the row is gone from the transcript
# npm (web profile)
dsh plugin --profile web add dsh-delete-turn
# local workspace (development)
dsh plugin --profile web add link:/path/to/dsh-delete-turnThen fully restart the DSH process (the host plugin tree is read at startup only). The browser bundle is served dynamically, so a hard refresh after the restart is enough. Verify:
dsh --profile web --dump-config # expect a "# == dsh-delete-turn" sectionUI (official slot action / DOM-enhanced action)
→ confirmation dialog
→ POST /dsh-delete-turn/delete { sessionId, mode, seq? / messageId? / turn? }
Host:
sessionQuery.readSession() reads the complete log (live-preferred)
local surface fold → current surface nodes + historical shadowed seqs
validate (current node / clean window / closed turn / protected head)
session.append('user/message', short marker placeholder, {
surfaceOp: { op: 'replace', startSeq, endSeq },
sourceEventSeqs: [every shadowed seq],
})
await the official session/flush durability checkpoint
→ { hidden: [{ seq, mode }] }
Browser:
useChat snapshot maps data-chat-flow-key to nodes; hidden seqs collapse rows
GET /dsh-delete-turn/state rebuilds the hidden set on every session open
Turn delete (mode: 'turn'):
ONE ordinary surface replace over this turn's own surface nodes
(prompt + injected context + every step's assistant/message and tool/result)
landing one turn-less carrier; later turns keep their place and their numbers
- no replay, no renumbering, no loop state touched, one extra eventDesign notes:
system/message corrupts the log: the agent loop advances its turn counter only from the turns it opens itself, so its next real turn reuses the number this plugin burned (turn/start does not open the expected turn on the next cold read), and any turn-number-keyed client hiding then swallows that reused real turn — reproduced in production as messages “being eaten”. The carrier therefore stays turn-less, with a single zero-width space: truly empty content is refused by the gateway with 400 user message must have content, and a readable marker gets quoted back by the model; a zero-width space is non-empty for every validator and carries no literal text. The cost: the model may read it as one blank user message. Bookkeeping turns already written to historical logs (older versions) are hidden client-side via markerTurns from /state.data-chat-flow-* anchors and the official useChat standard hook, so a host UI refactor cannot silently detach the actions./state route./state publishes turnSeqs (the rows that can be turn-deleted right now), computed by the SAME function the route plans with (turnableSeqs tries planRange on each candidate), so an entry can never become a click that is bound to fail.sessionController.cancel (the same stop the UI's own button uses, keeping the queued inbox), waits for the turn to actually close (turn/end landed), and only then deletes. A stop that times out (15 s default), a refused stop, or a running compaction (which has no stop semantics) is still refused with a reason./compact) no longer offers a delete action: it is not in the context any more and the transcript keeps it on purpose.force-carrying retry removes them. The system-prompt head (surface node 0) itself cannot be deleted; the turn that introduced it can be turn-deleted - the head is excluded from the window and stays in place.cd dsh-delete-turn
npm test # 62 cases: pure logic + host route + real-validator contract + client DOM
node tools/verify-real-session.mjs <session.v4.jsonl.zstd> [--turn N]test/contract.test.js and tools/verify-real-session.mjs need an installed @deepseek-ai/dsh-session / @deepseek-ai/dsh-session-format-catalog (the plugin itself never imports them). Lookup order lives in tools/dsh-modules.mjs: DSH_SESSION_DIR → the repo's / its parent's / the sibling repo's node_modules → ~/.npm/_npx/*/node_modules (an npx-installed DSH) → the bare specifier. When nothing resolves the test fails loudly instead of skipping, because that would mean the contract is unverified.tools/verify-real-session.mjs never writes the file it is given: it decodes the log, runs one strict cold read, builds a real Session from the decoded events, drives the plugin's actual HTTP route against it, then cold-reads the result again and prints the folded surface before and after. cp a real session to /tmp first, exactly as this repository's own acceptance run did.See CHANGELOG.md.
0.1.6-alpha.2, 0.1.7-alpha.2 and 0.2.0-rc.1 (web and desktop profiles; WebKit and Chromium engines).plugin:dsh-delete-turn, and the hidden ledger recognizes both { kind: 'plugin', plugin: ... } and { kind: 'plugin:...' } shapes.sessionQuery is absent.MIT