我们做的是一款 AI 生成网站的产品。用户通过对话持续提出需求,Coding Agent 负责读取和修改项目代码,完成页面生成、功能调整和网站发布。
某天,我收到一个线上用户反馈:项目源码丢失。原本以为只是工作区文件异常,用 Git 恢复一下就好。进一步检查后却发现,不仅工作区中的文件已经不存在,Git 中最近三个月的提交也全部丢失,当前能够找到的最新版本停留在三个月前。
对于一个已经持续迭代数月的网站,代码不仅决定页面如何呈现,也包含逐步形成的目录结构、组件边界、业务命名和实现细节。这些内容同时构成 Coding Agent 理解项目、定位文件和继续修改所依赖的上下文。
因此,这次恢复的目标不是重新实现一个视觉相似的网站,而是在尽可能保留原有工程结构的基础上,将项目恢复到接近最后一次发布的状态,并确保后续仍然可以继续通过对话迭代。
当时能够利用的信息包括:
-
三个月前的 Git 仓库;
-
最近三个月的对话与工具调用记录;
-
最后一次发布的构建产物;
-
仍然可以访问的线上页面。
这些信息都不完整,但分别保留了项目在不同维度上的状态。核心问题是,如何组合这些信息,将项目恢复到可以继续开发的状态。
方案一:重放工具调用
第一种方案,是以三个月前的 Git 仓库为基线,按照时间顺序重新执行对话历史中的代码修改。
Coding Agent 修改项目时,通常会调用写文件、搜索替换等工具。工具记录中会包含文件路径、写入内容,或者替换前后的文本。
如果这些记录足够完整,就可以从旧代码开始,依次重放每一次操作,逐步恢复项目。
我先从完整的对话数据中提取了文件写入和增量替换两类工具调用,共四千多条,再通过脚本按照时间顺序应用到旧仓库。第一轮结束后,项目的主体结构已经基本恢复。
这种方案最大的价值,是所有修改仍然作用于原有代码。
目录结构、组件关系、技术栈、文件路径和业务命名都能够得到保留。相比根据页面表现重新生成代码,它更接近真正意义上的源码恢复,也更有利于 Coding Agent 延续此前的上下文。
但工具重放具有很强的顺序依赖。
四千多条操作中,最初约有四百条无法应用。搜索替换通常依赖文件在某一时刻的精确内容,只要前面漏掉一次修改,后续匹配就可能失败,并逐渐扩大偏差。
例如,一条替换操作依赖前一次修改引入的组件名称。如果前一次修改没有被正确执行,当前替换就无法找到目标文本。后续所有基于新结构进行的修改,也可能随之失败。
进一步检查 Coding Agent 的执行机制后发现,影响工作区的并不只有显式的文件工具。
Agent 还可能通过命令执行完成批量替换、依赖安装、代码生成和迁移操作,也可能将一组开发任务交给子 Agent。对于部分子 Agent,系统只保存了任务指令和最终结果,没有保存其内部完整的工具调用过程。
这意味着,仅重放写文件和搜索替换,并不能覆盖项目发生过的全部变化。
我继续从记录中识别可能修改工作区的命令执行,并根据子 Agent 的任务描述和执行结果,重新完成其中能够确定的修改。补充这些路径后,无法应用的操作从约四百条下降到两百条左右。
从页面主体、主要组件和工具调用成功情况来看,此时项目已经恢复了大部分内容,但仍存在明显缺失。
重放后的代码中还包含标签未闭合、变量引用异常、局部语法错误和环境变量缺失等问题。我继续让 Agent 基于当前工程上下文修复这些错误,直到项目能够重新构建和运行。
但能够运行,并不代表已经恢复到最后一次发布的状态。
部分页面仍然缺少文案、图片、区块和交互逻辑。由于相关修改没有被完整记录,仅依靠对话历史已经很难判断具体差异。
方案二:逆向构建产物
第二种方案,是从最后一次发布留下的构建产物中逆向代码。
项目的构建产物主要由 HTML、JavaScript 和 CSS 组成。由于没有 Source Map,无法将编译后的代码直接映射回原始源码,只能对产物进行格式化、拆分和结构化整理。
处理过程主要分为三步:
-
对构建后的 JavaScript Chunk 进行格式化,识别模块和运行时依赖;
-
根据路由、懒加载关系和现有目录结构,将不同 Chunk 映射到具体页面;
-
将关键页面逻辑整理为相对可读的 JavaScript 或 TypeScript 形式,用于分析页面结构和实现逻辑。
经过处理,可以识别出页面包含的区块、文案、图片、链接、条件分支和大部分交互逻辑。
但构建产物经过编译和压缩后,类型信息已经消失,变量名通常被缩短,模块边界也可能被重新组织。一些原本清晰的业务命名,最终只剩下单字母变量或者经过合并的函数。
因此,构建产物可以帮助理解网站最终实现了什么,却无法可靠恢复原来的工程结构。
即使能够将一段 JavaScript 重新整理为 TypeScript,也只是让代码重新变得可读,并不意味着原始类型、组件边界和业务语义已经恢复。
这种方式更接近依据最终产物重新重构源码,而不是恢复源码。
不过,构建产物有一个不可替代的价值:它准确记录了最后一次发布时,浏览器实际执行的结果。
对话历史保存的是过程,但过程可能缺失;构建产物保存的是结果,而且这个结果已经经过构建和发布验证。
因此,它不适合作为恢复后的源码主体,却非常适合作为判断恢复结果是否完整的参照。
方案三:重新开发
第三种方案,是参考仍然可以访问的线上网站,从零进行一次 Vibe Coding。
通过页面截图和实际交互,可以重新实现布局、文案、图片和基础功能。在时间充足的情况下,视觉还原度也可以做到较高。
但线上页面只能呈现系统的外部表现。
原有组件如何拆分、变量如何命名、异常状态如何处理、响应式逻辑如何组织,这些信息都无法从页面中完整获得。一些暂时没有触发的状态和分支,也很难通过页面交互被发现。
最终生成的代码可能在视觉上相似,却已经变成了另一个工程。
原有对话中依赖的文件路径、组件名称和业务上下文也会随之失效,后续 Coding Agent 无法自然延续此前的修改过程。
因此,参考页面重新开发更接近重构,而不是源码恢复。它只适合作为前两种方案失效后的兜底手段,或者用于补齐少量能够从线上页面直接确认的样式和内容差异。
最终方案:以重放为主,以产物为参照
三种方案保留的信息不同。
对话重放能够最大程度保留工程结构,但无法覆盖全部修改;构建产物记录了完整的运行结果,却缺少源码语义;线上页面能够呈现最终视觉效果,但无法提供内部实现细节。
因此,最终恢复以对话重放得到的项目作为源码主体,再通过构建产物定位和补全缺失内容。线上页面则只用于处理最后少量能够直接确认的样式和交互差异。
恢复后的项目包含二十多个页面。
我让一个 Agent 负责任务编排,再将检查工作按照页面拆分为多个独立任务。每个任务负责一个页面,对照恢复后的源码和构建产物,判断当前页面还缺少什么。
第一轮只分析差异,不直接修改代码。
每个任务需要记录:
-
缺失的页面区块;
-
不一致的文案;
-
缺失或错误的图片;
-
链接和跳转差异;
-
条件渲染分支;
-
交互逻辑差异。
汇总分析结果后,再统一按照现有工程结构完成修复。
这样,问题从“根据构建产物重新还原整个项目”,转变成了“判断当前源码距离最后一次发布还缺少什么”。
前者需要重新理解并重建整个系统,后者只需要在已有工程结构中定位差异。范围明显更小,也不容易破坏已经恢复的代码。
利用 Chunk 哈希缩小排查范围
恢复过程中比较关键的一步,是对构建产物哈希值的利用。
项目中的页面使用了懒加载,因此不同页面通常会生成相对独立的异步 Chunk。Chunk 文件名会包含根据文件内容计算出的哈希值。
在以下条件保持一致时:
-
构建环境一致;
-
依赖版本一致;
-
构建配置一致;
-
环境变量和编译条件一致;
如果恢复后的某个页面生成了与原构建产物相同的 Chunk 哈希,可以将其视为该页面构建结果一致的强证据。
哈希一致不能证明恢复后的源码文本与原源码完全相同,但可以证明对应 Chunk 的最终构建输出相同。
对于这次恢复而言,目标是尽可能接近最后一次发布状态,因此这些页面可以从后续排查范围中排除。
第一次构建并比对后,有四五个页面的 Chunk 哈希完全一致。剩余页面进入第二轮检查,重点关注静态网站中更容易遗漏的内容,包括文案、图片、链接、页面区块和条件渲染分支。
完成修复后,再次进行构建和哈希比对,又可以排除一批页面。
对于仍然不一致的页面,再结合逆向后的构建产物检查结构。
例如,构建产物显示某个页面包含八个主要区块,而恢复后的源码只有四个,那么排查范围可以直接收敛到缺失的四个区块,而不必重新分析整个页面。
经过两到三轮检查、修复和验证后,绝大多数页面的结构、内容和主要交互已经能够与发布产物对应。
对于最后少量能够从线上页面直接确认的样式和内容差异,再通过 Vibe Coding 进行补齐。
在旧仓库仍然可用、对话记录相对完整,并且项目规模只有二十多个页面的前提下,整个恢复过程大约持续了两到三个小时。
核心工作并不是重新生成代码,而是利用不同信息不断排除已经正确的部分,将不确定范围逐步收敛到少数具体页面和组件。
总结
这次恢复能够完成,依赖的是几类信息之间的互补。
旧 Git 仓库提供工程基线,对话历史保留了大部分修改过程,构建产物记录了最后一次发布的运行结果。单独依赖其中任何一种信息都无法完成恢复,只有将过程信息与结果信息结合起来,并通过多轮比对不断缩小差异范围,才能将项目恢复到可继续开发的状态。
其中,对话重放是一种很有价值的恢复手段。
它能够让修改继续作用于原有代码,保留项目的目录结构、组件关系、业务命名和工程上下文。相比直接根据页面或构建产物重新生成代码,它更接近源码恢复。
在代码缺失范围有限、工具记录较完整、历史环境仍然可以复现的情况下,重放能够显著降低恢复成本。
但重放并不能代替备份。
它高度依赖记录完整性、执行顺序和历史环境。一旦中间存在未被记录的命令执行、代码生成器或子 Agent 修改,后续操作就可能逐步偏离。
即使保存了全部工具调用,依次重放数千次操作的可靠性和成本,也无法与恢复一个完整代码快照相比。
因此,对话重放适合作为异常发生后的恢复工具,却不适合作为源码备份本身。
对于 Coding Agent 产品,生成、构建和发布构成了最直接、也最容易被看到的交付路径。在这条正常路径之外,还需要建立一条不那么显眼、但同样重要的恢复链路,包括源码备份、版本快照和异常恢复。
更稳妥的做法,是在任务完成、构建成功和发布等关键节点,保存可以独立恢复的代码状态,再将对话重放和构建产物比对作为补充手段,用于恢复快照之间缺失的少量变化。
这次恢复最终能够完成,并不是因为某一种方案足够完整,而是因为旧代码、执行历史和最终产物分别保留了不同维度的信息。
对于 Coding Agent 系统而言,真正可靠的设计不应该寄希望于异常发生后完整重现每一次代码生成过程,而应该确保每次成功交付之后,都存在一个能够脱离对话历史独立恢复的代码版本。