文章
项目怎样记住踩过的坑
复盘 Watch Together 和 ETLP 的知识沉淀机制,看看一次排错怎样变成下一次维护可以直接使用的项目经验。
这段时间维护 Watch Together 和 ETLP,我越来越在意一件很朴素的事。一个问题解决以后,下一次进入项目的人能不能接着往前走。
代码当然会留下,提交记录也会留下。可很多费时间的东西藏在代码外面。某个状态为什么要等两秒再确认,某条命令为什么必须绑定播放会话,更新包为什么少一个文件也不能发,这些判断若只留在一次对话里,过几天就得重新查一遍。
两个项目现在各自有了一套知识沉淀机制。它没有复杂平台,也没有常驻服务。它靠几份位置明确的文档、一套任务结束前的复盘动作,以及代码和测试给出的证据运转。Watch Together 先把这条路走通,ETLP 随后借走方法,再按自己的边界重新验证。
四种知识放在四个地方
最初整理 Watch Together 时,先碰到的是文档越写越乱。协作规则、当前架构、一次排错的结论、本机部署步骤,全塞进同一份文件,短期看着省事,后来谁也说不清哪句话还有效。
现在公开知识分成四层。
AGENTS.md 放稳定的协作规则。Agent 进项目先读它,知道该看哪些资料,允许改什么,完成后要跑什么检查。这里记录的是做事方式,不收某次发布的版本号和临时状态。
docs/architecture.md 只描述当前代码已经实现的结构。Watch Together 在这里写清 SessionBridge、RoomManager、SyncEngine 和更新信任边界。ETLP 则写本地 HTTP 服务、数据解析、播放器管理、远程控制、CloudDrive2 和更新器之间怎样连接。代码改了,架构文档也要跟着当前事实走。
docs/lessons-learned.md 收那些不容易从代码表面看出来、以后又很可能再遇到的坑。每条经验都要有现象、结论和验证依据。它不负责记录某天跑过哪些命令,也不收未经复现的猜测。
docs/adr/ 留给长期技术取舍。公共接口、状态机语义、持久化格式、并发模型和更新信任边界发生变化时,未来维护者需要知道当初为什么这样选,ADR 才有价值。局部修复写进经验文档就够了。
这四层解决了一个常见麻烦。新 Agent 不用从几十页任务记录里猜现状,也不会把一条临时结论当成长期规则。
经验要先过证据门槛
文档多不等于项目记得准。正式知识库的门槛很硬,只有代码、测试、实际运行、日志或可复现结果确认过的内容,才能写进去。
Watch Together 里有一条早期经验很典型。播放器位置每次轮询都会往前走,直接比较两次位置,很容易把自然播放误判为手动拖动。后来同步逻辑改成先按上一位置、播放速度和经过时间估算预期位置,再判断有没有明显跳变。单元测试覆盖正常推进、前后拖动和远程命令回传,这条经验才进入 lessons-learned.md。
随后遇到的会话切换更麻烦。设备重连后可能还在播放同一个视频,旧命令若只看媒体和位置,就可能被新会话错误确认。项目把 Pending 命令、抑制状态和同步阶段都绑定到 session identity,身份变化时丢掉旧命令,重新进入等待。这里留下来的已经是一条跨模块约束,后续写新的命令生命周期逻辑时可以直接拿来检查。
真实客户端又提供了另一类证据。部分客户端在暂停、恢复或重建播放会话时会短暂报告 PlaybackStopped,下一轮快照却恢复正常。若收到一次停止事件就立刻处理,另一端会被错误暂停,双方还可能反复重新同步。现在停止事件只负责唤醒轮询,持续异常达到确认窗口后才作为终态处理。这个结论来自真实日志,也有回归测试托住。
经验文档因此更像一组已经验证过的判断。下一次改同步状态机,Agent 会先看到这些边界,少走很多弯路。
每次实质任务都留一个复盘口
光有文档目录还不够。忙起来时,人和 Agent 都会倾向于修完、测试、提交,然后去做下一件事。知识沉淀需要一个固定动作把经验接住。
两个项目都把这个动作叫作 Knowledge Review。实质性的代码修改、缺陷排查、架构调整或兼容性调查结束前,要检查四类内容。
- 出现了什么新的约束
- 哪个坑以后还会碰到
- 哪条旧假设被证伪
- 是否需要更新架构文档或 ADR
没有新发现就明确写无。这样比为了完成流程强行加一条文档可靠。发现新内容后还要先搜索去重,旧结论已经失效就更新原文,不能在后面再堆一条互相冲突的说法。
多 Agent 协作时,执行任务的 Agent 会在完成报告里交出 Knowledge Findings。主线程把它当作待审核材料,回到真实 worktree 检查代码、完整差异和测试结果,最后决定是否写入项目知识。一次漂亮的总结不能替代证据。
这一步看着多花了几分钟,后面常常能省掉几小时。它把一次解决问题的过程压成下一次任务开头就能读取的约束。
ETLP 只拿走能证明适用的部分
ETLP 建立知识机制时,没有把 Watch Together 的文档整套复制过去。两个项目都处理媒体播放,也都碰到会话身份和自然播放位移,可运行边界差得很大。Watch Together 是 Emby 服务端插件,有房间、参与者和同步状态机。ETLP 是本地播放辅助服务,接收浏览器用户脚本请求,再启动 mpv、IINA 等播放器。
跨项目文档先把可复用原则列出来,再回到 ETLP 的代码和测试逐条核对。请求必须先解析完成再创建播放线程,就是 ETLP 自己证明出来的经验。以前把原始 payload 直接交给后台线程,缺少 file_path 时,异常会在线程里才出现,HTTP 请求却可能已经返回成功。现在解析和校验留在分派之前,测试也直接覆盖这条路径。
本地 HTTP 服务的安全边界同样来自 ETLP 自己。默认只监听回环地址,非回环绑定需要强 token,动作接口使用精确路由和协议头,请求体有大小上限,短期媒体 URL 经过 HMAC 校验。日志只留安全原因,不写完整查询串、token 或敏感路径。这些规则适合 ETLP,房间 gate 和参与者授权则继续留在 Watch Together。
更新机制也保留了这种克制。Watch Together 已经有 manifest、SHA-256、程序集身份和 RSA 签名组成的 fail closed 信任链,还要处理首次可信引导。ETLP 当前发布的是 beta ZIP 和 SHA-256 sidecar。它会先下载到临时文件,校验成功后再原子替换,并检查 ZIP 路径和成员边界。把签名体系直接搬过去会让文档先于实现,项目明确把它留作未来需要单独设计的事项。
跨项目复用做到这一步,拿走的是验证方法和边界意识。每个项目依然对自己的事实负责。
本机运维信息单独留下
这两个项目都有 LOCAL_OPERATIONS.md。它保存机器专属的部署位置、连接方式、恢复步骤和运维注意事项,并通过本地 Git 排除规则留在工作机器上。
这份文件不能变成流水账。当前版本、提交、哈希和线上状态会变化,应当从 Git、发布页或实时检查里读取。长期稳定的本机拓扑和恢复方法才适合写进去。服务器地址、认证信息和密钥也不会进入公开文档。
公开经验和本机手册分开以后,仓库可以放心推送,换一台机器的 Agent 也不会把旧服务器状态当成当前事实。
测试结果也要写清边界
沉淀机制最后还有一道限制,证据分层不能混用。
Watch Together 的单元测试可以证明状态机在给定输入下怎样处理会话和命令,真实 Emby 客户端仍要单独验收同步、通知和主题显示。ETLP 的静态检查和测试可以覆盖请求解析、远程控制、依赖加载和更新器,实际 beta ZIP 还要检查成员与哈希,播放器、字幕和 CloudDrive2 行为则要回到真实客户端。
因此经验条目会写明验证到哪一层。没有跑过真实客户端,就不会用测试通过代替客户端确认。这个分寸很重要,它让下一位维护者知道哪些结论可以直接依赖,哪些地方仍要亲手试一次。
项目开始拥有连续性
我现在回看这两个项目,最有价值的变化发生在任务的开头和结尾。
任务开始时,Agent 按范围读取当前架构和相关经验,不必翻完所有历史。任务结束时,它检查这次工作有没有产生新的稳定知识,再把内容送到合适的位置。中间的代码、测试和运行结果负责证明,主线程负责去重和把关。
这套机制仍然依赖维护者认真执行,它没有 Git Hook 强制拦截,也不会在后台自动总结一切。流程够简单,维护者才愿意每次都做。只要仓库还在,新来的 Agent 就能知道前人在哪些地方吃过亏,哪些判断已经被证明,哪些事情仍然不能假装确定。
项目由此有了连续性。