BeforeWrite:每次写盘前留一版,DSH 里的文件时光机

前言

用 DSH 跑 coding agent 时,常见事故不是改错一行,而是 agent 把原本正确的内容覆盖掉之后,你才从 diff 或运行结果里发现问题。Git 要嘛还没 commit,要嘛改动根本不在仓库里;编辑器 Undo 管不到 agent 在别的路径写的文件;回收站也救不了「覆盖」这种没有独立副本的情况。BeforeWrite 是 maintainer caishengshuo 发布的 DSH 宿主插件,挂在写盘前的 fs/write-intent 钩子上,在内容被替换的瞬间把旧内容存下来,并通过工具 time_machine 和 Web 界面里的会话时间轴做列表、对比和回滚。项目在 GitHub 上标为 1 star,目录分类为「记忆」;许可证 MIT,当前 package 版本 5.1.0。

这是什么

BeforeWrite(插件包名 dsh-beforewrite)的定位可以概括成 README 里那句话:Every file, one version before it breaks——粒度是一次写入,不是按小时备份。它注册给 agent 的工具名固定为 time_machine,和项目名 BeforeWrite 不是同一个字符串。

与多数挂在 Client UI 槽位、多一块面板的 DSH 插件不同,BeforeWrite 挂在 Host Event 的 waterfall 上。写盘动作发生前,插件能拿到目标路径和即将被覆盖的旧内容;平时几乎无感,需要时才通过工具或界面找回某一版。

核心功能

自动快照

凡经过 DSH 文件服务的写入或编辑,在内容被替换前会自动留一份快照。索引追加写入 ~/.dsh/time-machine/index.jsonl,正文按 sha256 内容寻址放在 blobs/ 下,相同内容只存一次。

六个工具动作

全部通过 time_machine 调用:

动作 参数 作用
list limit, path 最近快照,新 → 旧
history path 某文件的全部版本
diff id 快照与当前文件的行级对比
restore id 回滚到该快照;回滚前的当前内容也会再存一份
stats — 体量与去重后占用
snapshot path, reason 手动打点

restore 不是简单撤销:替换前会把「当前磁盘内容」再快照一次,反悔可以再 restore 到新编号,历史只增不减。

图形界面时间轴

安装后 Web 界面顶栏下方会出现「时光机」入口。左栏按 DSH 会话切换;右栏合并三类记录——用户真正发出的「说过」、工具调用的「动作」、文件改动的「快照」。快照行可先看 diff,再一键回档。数据旁路监听 session/event,界面通过插件 HTTP 接口读取;回档接口仅接受 POST 并做同源校验,避免跨站页面静默触发回滚。

默认策略与边界

单文件快照上限 2 MiB;索引条数上限 20000,超出按旧到新淘汰。默认忽略 node_modules、.git、time-machine 路径。二进制内容会存并标记,但 diff / restore 会拒绝,避免把字节当文本写回;需从 blobs/ 人工取回。Shell 里 rm、sed -i、重定向、mv 等不经过文件服务,不会触发 write-intent,跑破坏性命令前应手动 snapshot。

安装与启用

前提:DSH 宿主(dsh web)在运行,且 plugin_manager 可用。

从本地目录安装(路径换成你机器上的插件目录):

plugin_manager(action="install_bundle", target="G:\\path\\to\\beforewrite")

README 也写了先取得仓库目录再对同一目录执行 install_bundle 的流程;具体以 GitHub 仓库 说明为准。

装完检查:

  1. 工具列表里出现 time_machine;
  2. 默认存储目录 ~/.dsh/time-machine/ 已创建,含 blobs/ 与 index.jsonl。

若改插件代码后重装无效果,README 指出常见原因是 DSH 按包名缓存插件模块:需同步改 package.json 与 cordis.patch.yml 里的 name 再装一次,细节见仓库内 docs/waterfall-notes.md。

典型用法

列出最近快照并做 diff、回滚的示例(来自 README):

$ time_machine list
最近 6 条快照(共 19 条,新 → 旧):
#19  2026-10-25 00:08:31  write    (当时还不存在)  G:\sklii\tm-final-check.txt
#18  2026-10-25 00:08:13  restore  147 B  G:\sklii\tm-after-restart.txt
#17  2026-10-25 00:08:09  edit     110 B  G:\sklii\tm-after-restart.txt

$ time_machine diff id=17
#17  2026-10-25 00:08:09  G:\sklii\tm-after-restart.txt
(旧 = 快照 / 新 = 现在)
─────────────────────────────
+2 行 / -1 行
@@ …

$ time_machine restore id=17
已回滚 G:\sklii\tm-after-restart.txt
恢复到 #17 的状态,110 B,操作类型 update。
回滚前的版本也存了一份,需要反悔就再 restore 那个新编号。

在 agent 即将用 shell 直接改文件前,可手动打点:

time_machine(action="snapshot", path="G:\\proj\\config.yaml", reason="before-bash-delete")

改 index.js 后可在插件目录离线跑冒烟测试(不依赖宿主):

node smoke.mjs

适用场景与注意

适合长期用 DSH 让 agent 改代码、配置或文档、又希望在「未 commit 的覆盖」这一档多一层兜底的人。不适合指望它替代 Git 版本管理、远程备份,或自动拦截所有 shell 写盘。

插件以当前 DSH 进程权限读写本地文件与 ~/.dsh/time-machine/;安装前应阅读源码与 MIT 许可证,确认存储路径与回档行为可接受。社区目录 SkillHub 插件页 与 GitHub 无 DeepSeek / 幻方官方从属关系,DSH 生态本身是「一切皆插件」的独立社区形态。

README 还列了未实现方向:会话级整轮回滚、更细保留策略、大索引下的读取缓存、以及依赖 fs 服务字节写回的二进制回档等。当前局限包括无远程同步、索引只增不减导致磁盘缓慢增长、工具名固定为 time_machine 等,使用前心里有数即可。

结尾

BeforeWrite 补的是「每一次写入前留一版」这一格:对 agent 经文件服务的改动几乎透明,需要时用 time_machine 或 Web 时间轴找回状态。目录与源码:

  • 目录页:https://www.skillhub.cn/plugins/caishengshuo/beforewrite
  • GitHub:https://github.com/caishengshuo/beforewrite
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜