AI 把文件改崩时,Ctrl+Z 救不了你 ------ dsh-file-undo v0.20
一个为 DeepSeek Harness(DSH)打造的文件撤销插件:agent 每一次
write/edit落盘前自动进时间线, 侧栏面板红绿 diff 逐条审查,两阶段撤销 + 重做双向循环,面板自动跟随你正在看的会话。纯 TypeScript 双半包,MIT 开源,公开版本 v0.20。
先看结论
用 AI 编码助手干活,文件不是你改的,但锅是你的。几个熟悉的场景:
- 让 agent 重构 20 个文件,跑完测试才发现第 3 步就引入了 bug ------ 中间 17 步全是白做的
- 让它"顺手把那个临时文件删了试试",删完它自己都不记得动过什么
- 想回退,
git status一屏红字翻到眼花;更糟的是工作区根本没git init过
dsh-file-undo 做的事很简单:Agent 的每一次文件操作,落盘前自动进时间线;红绿 diff 逐条审查;看准了一键撤销,反悔了再一键重做。
装好插件重启,侧栏底部多一枚「撤销」图标,徽章实时显示可撤销操作数。点开是审查面板:左栏历史列表(时间、工具徽章、文件名、+恢复 / −移除 行数),右栏红绿 diff 和操作按钮。

创建类操作是真删文件,红键 + 内容/hash 双闸伺候;那条灰色的"失败"记录也是账本的一部分------Agent 试过什么、为什么没成,都在。
新增功能与优化体验
面板跟着你走
以前面板只认「最近做过文件操作的会话」------切到一个还没动过手的会话想看历史,它还停在旧的,看着就像那个会话"没有历史"。
现在面板自动跟随你当前正在查看的会话:切过去,列表、差异、撤销按钮一起跟上,哪怕这个会话还什么都没改过。侧栏徽章的未撤销计数也跟着改成统计当前会话。
想手动指定也可以:面板头部的会话切换器列出同一项目树(当前目录及其父子目录)下的全部会话,显示平台会话标题和活跃/存档状态,选中即钉住,优先级高于自动跟随。
同目录多会话不再靠 uuid 认人
同一个目录下开几个会话,以前列表里只能靠一串 UUID 区分,根本认不出谁是谁。现在直接显示平台会话标题。
修掉一个"历史凭空消失"的坑
D:\proj、D:\proj\、d:\proj、D:/proj 四种写法会派生出四个快照仓库,同一项目的会话历史被劈开,面板只认得其中一个。现在工作区键由规范化后的路径派生------一个目录恒等于一个键,存量数据零迁移。
撤销重做是双向无限循环
反悔了?已撤销的条目上有「重新应用」,写回操作后的内容。回退到中间某一步时被连带作废的那些条目不用逐条救------重新应用上游时整批复活 ,确认框明示「连带恢复 N 条」。复活之后还能再撤销。 

每条快照挂着模型轮次
host 从平台轨迹读工具调用事件的权威载荷,把快照映射回真实的模型轮次。悬停提示长这样:"第 4 轮 · 第 2 步 · 本轮共 6 次工具调用"。
60 秒走一遍
让 Agent 重构一个模块。面板里条目一条条长出来,每条挂着工具徽章和行数统计。
点开第 3 条,红绿 Diff 摆在眼前:绿行 = 撤销将恢复的内容,红行 = 撤销将移除的当前内容------这是"撤销视角"的 Diff,不用你脑内取反。确认改错了,点一次「撤销」按钮变成「确认撤销?」(3 秒内再点才执行,防手滑),文件回到操作前的状态。
后面 17 步怎么办?它们不会凭空消失,而是被标记成 cascade(连带作废)。想回到更早的某个时点,选中任意条目点「回退到此状态」,单次原子写回,会先告诉你这一下会丢弃其后多少步。

不想开面板,聊天里 /undo 一套命令全都在:/undo list 看历史、/undo <n> 撤销指定条目、/undo prune 7 清理 7 天前的快照(只缩回退深度,不动任何当前文件)。
撤销的安全闸,比撤销本身重要
能一键回写文件的工具,危险不在功能,在误触。这个插件的预检逻辑比撤销本身写得还长:
external_modified:快照之后你手动改过这个文件?直接拒绝执行,面板写明原因。AI 不能吞掉你的手工编辑------真要强制撤销,走/undo命令(保留原语义,不做此拦截)。superseded_by_later_ops:直接撤销中间某条旧条目?拒绝,并告诉你它后面还压着自己记录的 N 条编辑------引导你去用「回退到此状态」,而不是制造一个账本对不上的文件。- 乐观锁 :预检通过后执行写回时携带
expectedCurrentHash(sha256)。文件在预览和确认之间又变了,写回直接报stale拒绝。 - 两阶段确认:第一次点击只是武装按钮,3 秒不点自动解除。创建类操作的撤销是真删文件,走同样的确认 + 内容/hash 双闸。
引擎裁决、UI 只展示------「能不能动这个文件」全部在 host 侧判定,客户端只拿分类结果渲染界面。这条原则让它敢于把删除键交给用户。

底层原理
双半包:host 拿事实,client 只渲染
Host 半挂在工具链的 tools/pre-execute / tools/post-execute 钩子上:落盘前捕获 before 全文,工具执行完回填 after 全文和哈希。Client 半是侧栏面板,通过带 Host 头信任围栏的 /file-undo/api/* 拿结果渲染。
text
agent 调用 write / edit
│
▼
┌─ host 半 ─────────────────────────────────────┐
│ pre-execute:落盘前捕获 before 全文 │
│ post-execute:回填 after 全文 + sha256 │
│ 存储:~/.dsh/file-undo/<工作区>/<会话>/jsonl │
└──────────────┬────────────────────────────────┘
│ /file-undo/api/*
▼
┌─ client 半(侧栏面板)────────────────────────┐
│ 历史列表 → host 侧 LCS diff → 红绿渲染 │
│ 预检分类 → 两阶段确认 → 事务写回(乐观锁) │
└───────────────────────────────────────────────┘
原子化回滚:一次写盘,一本账,不留中间态
回退不是"逐条逆序撤销"------那样中途崩一次就留下半回滚的烂摊子。它是一次原子写回 加一次单事务账本标记:
- 文件要么整体回到目标态、要么完全不动,没有"写了一半"的状态;
- 目标行和它后面所有被连带作废的行,在同一个锁内批量落盘。
这不是洁癖。分两次写就会造出"已标记撤销却没有撤销来源"的僵尸行,而那正是这个插件存在的意义------消除账本滞后。重应用时整批连带复活同理。
全文快照 + 现场算 diff
快照存 before/after 全文加哈希,不是反向补丁------diff 是渲染时现场算的,撤销是整文件写回,两件事解耦,各自简单可靠。补丁链那种断一环全断的设计,不要。
diff 算法值得一句:host 侧行级 LCS 做了前缀/后缀裁剪和整数行 ID + Uint32 DP,超大中段降级为整块替换------800 行以内的 diff 都是逐行精细渲染,再往上给「显示全部」按钮。
存储:不依赖 Git,也不怕大
text
~/.dsh/file-undo/
<workspace-key>/ # 由会话 cwd 规范化派生(消毒 + 8 位哈希后缀防撞)
<chat-key>/ # 会话 id
snapshots.jsonl # append-only,并发安全(单飞写锁 + 原子改名)
git-archive/ # 第二层归档:每条快照一个 commit,静默降级
与你的 Git 仓库零接触,没 git init 过的工作区照样能用。
诚实的边界
- 只覆盖
write/edit两个工具;agent 在 shell 里用Set-Content或重定向改文件,不进时间线 - 「回退到此状态」是丢弃后续改动,不是把后续改动重放到旧基线上------预检会把丢弃数量摆在明处
- 深度集成是它的价值,也是它的边界:这是 DSH 专属插件,离了 DSH 没有意义
- 失败的调用也会进面板("失败日志"而非隐藏)------你知道 agent 试过什么、为什么没成
上手
sh
# 本地构建(当前版本的安装方式)
git clone https://github.com/QinLuza/dsh-file-undo.git
cd dsh-file-undo && pnpm install && pnpm build
# 在 profile 的 package.json 里加依赖
# "dsh-file-undo": "link:<本目录绝对路径>"
# 然后在 profile 目录执行
pnpm install
重启 DSH(host 端生效);之后纯 client 端的改动只需浏览器硬刷新。npm 包发布后可以一条命令直装:dsh plugin --profile web add dsh-file-undo。
仓库:github.com/QinLuza/dsh... ------ 内置一套 verify-*.mjs 回归脚本,快照引擎、diff 渲染、会话隔离每一条语义都有断言盯着,改坏东西会直接红。
收个尾
三句话总结:
- Agent 的每一次文件操作都该有一条时间线------这不是 Git 的替代,是不依赖 Git 的那层"后悔药"
- 安全闸和撤销同等重要:预检分类、乐观锁、两阶段确认,宁可拒绝不可错写
- 多会话工作流里,面板必须跟人走------现在的自动跟随让它终于像个"当前会话的历史",而不是"某个会话的历史"
下一步:装上,让 agent 随便改点东西,打开侧栏那枚「撤销」图标看一眼。之后你大概会像我一样,在每次让 agent 动手之前先确认它装没装。 检分类、乐观锁、两阶段确认,宁可拒绝不可错写
- 多会话工作流里,面板必须跟人走------现在的自动跟随让它终于像个"当前会话的历史",而不是"某个会话的历史"
下一步:装上,让 agent 随便改点东西,打开侧栏那枚「撤销」图标看一眼。之后你大概会像我一样,在每次让 agent 动手之前先确认它装没装。