基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
编程提效2026-11-08 04:22

保留未提交改动,精准撤销 AI 操作

dev.to · AI#AI编程#版本控制#检查点
Editor brief · 编辑速览

文章解释 Git 回退为何难以恢复智能体操作前的完整目录状态,包括用户未提交改动、忽略文件和新增文件。它展示写入前保存文件内容的检查点设计,并用约 90 行 Python 与四种 Git 方案进行比较。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

AI 改坏项目后,git checkout -- . 未必能把现场救回来:它恢复的是提交里的版本,而你想要的,通常是本轮 AI 动手前的目录,包括没提交的修改、未跟踪文件,以及被 Git 忽略的 .env。

作者用一个小项目对比了五种撤销方式,又在真实的 React、TypeScript 源码目录上测试成本。结论很明确:要按轮次撤销 AI 修改,需要记录它动手前的文件内容。这个方案实现不复杂,但能否覆盖所有写入,比用了什么存储技术更关键。

为什么 Git 回退后,现场还是不对

作者的测试刻意保留了日常开发中最容易丢失的状态。

项目包含 index.html、212 行的 styles.css、app.js,以及被 .gitignore 排除的 .env。上次提交后,用户修改了页面标题,还创建了未跟踪的 notes.md。这些都是撤销 AI 操作时应该保住的工作。

随后,AI 把样式缩到 41 行,修改页面以加载新建的 theme.css,重写脚本,并把 .env 中的 API_URL 指向 localhost。

作者为每种撤销方法准备独立副本,再逐文件比较字节:

撤销方法 与本轮开始前不同的文件
checkpoint restore 无
git checkout -- . .env、index.html、theme.css
git stash -u .env、index.html、notes.md
git checkout 配合 git clean -fd .env、index.html、notes.md
本轮开始前执行 git stash create .env、theme.css

问题在于恢复目标不同。回到最后一次提交,会连用户尚未提交的标题修改一起撤销;清理未跟踪文件,又可能把用户的笔记一起删掉。git stash -u 包含未跟踪文件,但不包含忽略文件,后者需要 -a。

最后一种方案已经提前留了快照,但作者使用的流程需要先执行 git add -A,会改变真实暂存区;恢复时也没有清掉新建的 theme.css。

Git 能保存版本,但它不会自动区分哪些未提交修改属于用户,哪些属于本轮 AI。

这组实验验证的是几种具体命令组合,不能据此推导出 Git 无法实现同等能力。

按轮次撤销,需要记住哪些东西

osFoundry 的做法是在 AI 写文件前,先保存旧内容。桌面应用用 Rust 文件 checkpoint_store.rs 实现,聊天中通过 /rewind 恢复。原文还提供了约 90 行、仅依赖 Python 3.9 标准库的演示。

旧内容保存在 <project>/.osfoundry/checkpoints/blobs/<sha256>。所谓内容寻址,就是用内容的 SHA-256 当文件名;相同字节只存一份,轮次索引记录文件路径和对应哈希。

正确性取决于三个细节。

第一,同一轮里,同一路径只捕获第一次写入前的状态。AI 连改三次文件,用户要撤销的是整轮,而不是退回第二次修改。

下面这段判断负责保住最初状态:

        cp = next((c for c in self.index if c["turn"] == turn), None)
        if cp and any(f["path"] == path for f in cp["files"]):
            return                      # 同一轮只保留第一次捕获

关键是 return:后续写入不能覆盖已经记录的旧内容。

第二,原来不存在的文件要记录为 absent。恢复时才能准确删除 AI 新建的 theme.css,而不碰用户原有的 notes.md。

第三,超过 MAX_BLOB = 8 * 1024 * 1024 的文件记录为 skipped,附带原因。它们无法恢复,但必须出现在失败报告里,不能悄悄留下新旧混杂的目录。

跨轮次恢复也有讲究。回到第 1 轮之前,必须同时撤销第 2、3 轮。下面的实现按时间从早到晚处理,每个路径只恢复一次:

    def restore(self, turn):
        target = next(c["at"] for c in self.index if c["turn"] == turn)
        affected = sorted((c for c in self.index if c["at"] >= target),
                          key=lambda c: c["at"])          # 从最早的轮次开始
        done, report = set(), {"restored": [], "deleted": [], "failed": []}
        for cp in affected:
            for f in cp["files"]:
                path, pre = f["path"], f["pre"]
                if path in done:
                    continue
                done.add(path)
                if not self._contained(path):            # 恢复时重新检查索引中的路径
                    report["failed"].append((path, "outside the workspace"))
                elif pre["kind"] == "skipped":
                    report["failed"].append((path, pre["reason"]))
                elif pre["kind"] == "absent":
                    Path(path).unlink(missing_ok=True)
                    report["deleted"].append(path)
                else:
                    Path(path).parent.mkdir(parents=True, exist_ok=True)
                    Path(path).write_bytes((self.blobs / pre["sha256"]).read_bytes())
                    report["restored"].append(path)
        self.index = [c for c in self.index if c["at"] < target]
        self._save()
        return report

要把 oldest first 和 done 放在一起理解:路径首次出现时拿到的,必须是最早那轮的旧内容。在这个算法里反向遍历,会被较晚状态抢先占位。作者说 Rust 版本曾犯过这个错误,由 rewinding_a_turn_also_undoes_later_turns 测试发现。

恢复时还会重新检查路径是否位于工作区,因为 index.json 自身也可能被修改。完成后,目标轮及后续轮次从索引中移除。

性能够轻,但省空间有另一套条件

测试目录来自真实 React、TypeScript 应用的 src:4,686 个文件,共 121.4 MB,没有超过 8 MB 的文件。环境是 Apple M1、8 GB 内存、macOS 15、APFS SSD、Python 3.9.6、git 2.39.5。作者随机选择文件,保存旧内容、追加一行,再恢复并逐字节验证,结果全部一致。

Git 的逐轮快照流程在无修改时耗时 61~68 ms,修改四个文件时为 50~67 ms。一次全目录重写之后,后续运行达到 1,241 ms,作者解释为时间戳变化触发了重新计算哈希。

checkpoint 只处理被写入的文件,但演示代码也有瓶颈:每次捕获都重写整个 index.json。索引增长到 1.3 MB 后,前 100 个文件平均捕获耗时 1.29 ms,后 100 个达到 9.54 ms。

改成每次追加一行 JSON 后,同样 4,686 个文件总耗时 2.5 s,前后平均捕获耗时分别为 0.41 ms、0.61 ms。少量改文件可以用简单索引,整库格式化更适合追加日志。

空间方面,SHA-256 去重只消除完全相同的内容。4,686 个文件有 4,677 种不同内容,源码之间可去重的部分很少。

一个 2 MB 文件连续十轮各追加一行,会留下 10 个 blob,占 20.0 MB;如果每轮写入相同结果,只留下原始内容和重复结果两种 blob,占 4.0 MB。

原实现不压缩。整目录的 121.4 MB 用 zlib level 6 可压到 33.7 MB,约 3.6∶1;同一目录提交后的 Git object store 为 41.0 MB。版本间增量还能继续省,但实现复杂度也会上升。

我的判断

这个方案最有价值的地方,是把恢复边界定在「本轮 AI 开始之前」。对未提交修改很多的开发现场,这比要求每次交互前整理提交更贴近操作习惯。

但它成立有一个硬前提:所有修改都经过捕获入口。演示只看得见 write(),AI 通过 shell 执行 rm、sed -i 就会绕开记录。osFoundry 称它把代理文件写入收口到统一函数,但原文没有证明任意 shell 操作也被覆盖。

另一个风险是恢复会覆盖用户后续修改。作者提出记录 AI 写入结果的哈希,恢复前检查,并在发生变化时另存当前内容;这是改进建议,不是展示代码已经具备的能力。

性能数字有实测支撑,但来自单台机器和特定目录,不能直接推广到所有团队。独立索引、临时提交或完整目录快照也能构建恢复流程,文章没有给出这些方案的全面对照。「普通轮次只需几毫秒」应结合修改数量理解。

长期使用还需要清理无人引用的 blob,并限制保留轮数。保存 .env 旧内容也会留下其中的配置和凭据,原文没有讨论存储保护。

能拿来做什么

  • 使用 AI 修改前端项目之前,先确认编辑器的撤销功能能否恢复 .env、保留未提交修改,并只删除 AI 新建的文件。可以照着文中的小场景验证一次。
  • 给 Node 编写的本地代理增加撤销功能时,把捕获旧内容接入统一写文件入口,同时记录 absent 和 skipped,让恢复报告明确列出未完成的部分。
  • 让 AI 执行整库格式化或批量重命名前,确认 shell 写入是否受保护;自建工具则应优先处理追加日志,避免每次写入都重写完整索引。
  • 如果允许用户在 AI 执行后继续编辑,应增加恢复前的哈希检查,并保存冲突文件当前内容,让一次撤销不会抹掉后续工作。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
Claude Code 免确认设置被指泄露密码
下一篇
从终端控制序列识别智能体待审批状态

资讯速览

dev.to · AI · 2026-11-08 04:22

AI 评分 8.0 / 10

相关标签

#AI编程#版本控制#检查点

继续阅读

  • 用 SQLite 重建 MCP 任务认领与幂等提交
  • Claude Code 免确认设置被指泄露密码
  • 从终端控制序列识别智能体待审批状态
  • Copilot多阶段协作如何衔接计划与审查
  • 把 Claude 代码审查接入 GitHub 工作流