liangmianya/dsh-synapse
liangmianya/dsh-synapseDeepSeek Harness 的可视化非线性对话工作区:把会话、追问与分支变成可浏览、可拖拽的对话地图。
DSH 会话消息级回滚、单条删除与重放式恢复;操作以 append-only surface replace 实现,带确认提示和运行中会话保护。需 Node.js 23.5+。
DSH web 插件:消息回滚 + 删除 + 恢复。只补 dsh 官方没有的能力。
0.9.0 是破坏性变更:移除了本插件的分支与 Markdown 导出。
两者 dsh 官方都已有(session/fork与/api/session.export),详见下方「为什么不重复造轮子」。
dsh plugin --profile web add @240xu/dsh-message-ops
# 重启 dsh web 后,会话头部点 Message ops/api/session.export)dsh plugin --profile web remove @240xu/dsh-message-ops能力 · 官方 dsh 0.2.0-rc.2 · 本插件 消息回滚(遮蔽该条及其后全部内容) · ❌ 没有。SurfaceOp 只有 append\ · replace,没有解除遮蔽的操作;undo 仅指草稿编辑器的 Ctrl+Z · ✅ 单条删除 · ❌ 没有。官方 README 明文 *"sessions can be archived but never deleted"* · ✅ 恢复(撤销一次回滚/删除) · ❌ 没有 · ✅
session/fork(atSeq 任意)+ SessionStore.fork()branch_session_click。本插件原先还有一个磁盘实现(applyBranch)作为「官方失败时的回落」,但它语义是错的: 删掉了 isSeeded(官方 fork 设 true)、不写 inheritedEventCount、 也不补 fork closers → 产出的子会话边界可能悬空。已删除,失败时直接提示停止运行中的会话。
/api/session.export 提供 ZIP 归档(原始 JSONL + 附件,面向迁移/复现)。格式确实不重叠,但本插件的导出零 UI 入口(实测路由可用但前端从不调用),等于死代码。已删除。
Message ops 按钮 → 右侧 OpsPanel 抽屉(消息 / 回收站双 tab);旧 OpsDialog 仅 window.__MOPS_LEGACY_DIALOG === true 时回退
Revert to here / Quote to composerRevert to hereRestore(消息级恢复入口)... 菜单面板流程:选一条消息 → 回滚 / 仅删除 / 分支 → 破坏性操作二次确认; 整会话删除在回收站 tab,需输入会话标题确认。
零 npm 依赖(只用 node:fs / node:zlib / node:path / node:url;zstd 压缩走 Node 内建 zlib.zstdCompressSync,需 Node ≥ 23.5,Termux 与 Windows 通用)。
# Termux / Linux
dsh plugin --profile web add @240xu/dsh-message-ops
# Windows
dsh plugin --profile web add @240xu/dsh-message-ops
# 或本地目录:
dsh plugin --profile web add file:C:/path/to/dsh-message-ops重启 DSH web 实例后生效。
surfaceOp: replace 的 system 事件实现,约束由 DSH 引擎 assertProvenance 校验,违规如实上报(HTTP 409)。
sessions.fork(要求先停止运行中的会话),插件不再写磁盘分支。/api 同条件校验 BrowserAuth cookie;有 credentials 服务但client-connection/browser-session 记录不可读时 401 fail-closed,无该服务的老宿主 保留旧围栏。
方法 · 路径 · 说明 GET · /api/message-ops/messages?sessionId= · 消息级列表(磁盘真相 + 可见性标注 + running 状态);&view=compact 只回 seq/role/snippet/time/visible/shadowed(体积约 −54%) GET · /api/message-ops/search?sessionId=&q=&role=&limit= · 服务端全文检索(含已回滚消息之外的全部文本),返回 query 对齐片段 GET · /api/message-ops/text?sessionId=&seq= · 单条消息全文(引用按钮按需取) GET · /api/message-ops/session/trash · 回收站列表(自动清理过期条目) POST · /api/message-ops/revert · {sessionId, seq} 回滚到该条(含) POST · /api/message-ops/delete · {sessionId, seq} 仅遮蔽该条 POST · /api/message-ops/restore · {sessionId, seq} 回滚恢复:seq 为某次 revert/delete 标记事件的 seq POST · /api/message-ops/session/delete · {sessionId, confirmTitle} 整会话移入回收站(7 天保留;运行中 409) POST · /api/message-ops/session/restore · {trashId} 从回收站恢复目录与投影/工作区元数据 POST · /api/message-ops/session/purge · {trashId} 永久删除(唯一 destructive 操作)
encodeSeqRanges 把连续≥3 个 seq 无损压缩成 [start, end] 闭区间,读盘时 decodeSeqRanges 再展开。 0.11.2 的 flat(Infinity) 会把 [[3, 6]] 变成 3, 6,丢掉 4、5,写出的 marker 被引擎判 missing 4, 5。改为 expandSourceSeqs() 按闭区间展开并保持首次出现顺序。
noticeWindowPostamble;postamble/flush 自身抛错时 catch 再写一遍,落成 step/end,turn/end,step/end,turn/end,宿主读盘直接判 step/end does not match an open turn and step,整份会话变 corrupt。 改为 closeWindow() 单点 + closed 标志。
inputActions.setDraft()只改编辑器、不写 localStorage;紧随其后的 openSession() 会用 readConversationDraft() 从旧草稿重读并覆盖。回填后补 persistDraft()。 对话框入口同理。
visible 时,回滚刚落盘、索引未刷新的窗口里会再次选中同一 seq → 409 surface replace: start seq N not found in surface。 /undo 成功后同时就地标记 seq >= target 行为 shadowed。
opsCommit / opsRestore 在取得live session 后改用 session.log 重算 turn 窗口,不再信任可能已过期的磁盘快照。
lastTurn,防止下一条真实消息复用同一 turn。planNoticeWindow 遇到「turn 已开但 step 未开」直接 409,不再嵌套非法合成 turn。applySurfaceReplace / buildMarkerEvent 对 sourceEventSeqs 去重和整数校验(0.11.3 起改为正确展开区间),区间成员不再丢失。
sourceEventSeqs([[start,end]])在 0.11.3 已确认是宿主合法编码,无需维护脚本;tool/result replacement 非 content 字段仍需按需修复。
sessionfix 夹具时自动跳过,不再硬依赖个人绝对路径。src/client.js 新增右侧功能主义抽屉 OpsPanel(560px,≤760px 全屏):消息/回收站双 tab、200ms 防抖搜索、角色过滤、含已回滚开关、46px 固定行高 虚拟列表、/text 详情、回滚/仅删除/分支、整会话删除(输入标题确认)。 旧 OpsDialog 保留为 window.__MOPS_LEGACY_DIALOG === true 回退。
GET /messages?view=compact 为面板默认载荷;GET /search 提供 500 字符上限的服务端全文检索(LRU 8 corpus 缓存,按 logPath mtime+size 失效)。
src/session-trash.js:删除 = 目录 rename + 投影行/工作区/归档/置顶元数据快照写 manifest.json;恢复拒绝覆盖已存在目录(409);purge 才真正删除。 恢复校验 manifest.sessionId 与 rel(必须是 sessions 根下两层、且与 id 匹配), delete 回滚失败时保留回收站唯一副本,不再无条件删除。
readSessionHeader),损坏的正文不会阻塞列表;marker 恢复进度改为单次索引扫描。
location.reload()——改为devkit 标准 toast(window.__dshDevkit.toast 探测,无 devkit 时降级为对话框 内完成文案)+ done 态新增「刷新页面」按钮由用户手动刷新;与 branch 路径 (refreshList 不刷新页面)行为拉齐。
readSessionFile 旧实现把帧 0整段 JSON.parse——旧单帧格式整个文件是一帧、解压出多行 NDJSON,直接抛异常, 与「旧格式兼容」声明矛盾。现改为逐行扫描统一路径:全部帧解压后按行扫描, 首个 type:"session" 行作 header、其余行作事件,v3 多帧 / v4 多帧 / 旧单帧 三种格式一条代码路径(撕裂行仍按完整前缀语义跳过)。
(session.v4.jsonl.zstd,结构同 v3,仅文件名与 version 数字不同)。 findSessionDirs 探测序列改为 v4 → v3 → 旧单帧(此前只找 v3,最新会话 全部 "session log not found");readSessionFile 不做 version 硬校验; applyBranch 分支写回沿用读到的原格式版本(session.v.jsonl.zstd), 不再写死 v3。
listMessages 内联文本提取收敛到 session-file.messageText单点(ops-core 转出保持导入面兼容),消除双实现漂移面。
findSessionDirs 三格式发现与 v4 分支写回;真实 v4 会话日志已实测通过。
MutationObserver 仍观察document.body(菜单由宿主 React 动态渲染,安装期无稳定锚点),但回调改为 精确过滤——只有新增节点本身是(或包含)[role=menu] 时才调度探测;聊天流 渲染、流式 chunk 等海量 mutation 零探测成本;同帧多次命中合并为一次探测。
渐进展开 50 条(上限仍为最近 200 条),避免 200 行单选列表一次性进 DOM; 打开新会话时分页重置。
/api/message-ops/* 路由接入三层信任判定(回环 Host 挡 DNS rebinding + sec-fetch-site: cross-site 拒绝 + Origin 同源校验),非回环/跨站请求一律 403。写操作(revert/delete/branch/restore) 另要求 Content-Type: application/json(text/plain 绕预检的洞 → 415)。 为何不引入一次性 CSRF token:浏览器对所有 POST(含 text/plain)都附带 Origin 头,Origin 同源校验已覆盖跨站 POST;自定义头天然触发预检、与 Origin 校验等价——token 只增加握手复杂度而不增加安全性,围栏已足够(论证见 src/ops-core.js 的 isJsonContentType 注释)。
applySurfaceReplace 写入时先按当前运行时 {startSeq,endSeq} 形状写;若引擎报 invalid replace surfaceOp (validateNext 在事件入 log 前抛出,无半写风险)自动降级 {start,end} 重试并记住拼写。当前与未来 dsh cohort 都不炸;读端双拼写兼容已统一收敛到 session-file.readReplaceOp 单点(computeShadowed / planRestore 共用)。
readSessionFileAsync,逐帧解压每 8 帧setImmediate 让出事件循环;messages/export/restore 路由改走该路径。 帧数超过 500 阈值的 export 在 Markdown 末尾追加耗时提示(partial 语义, 内容完整无截断)。