知识管理工具的演进始终面临一个核心张力:功能丰富性与系统可维护性之间的平衡。Obsidian 等工具通过插件生态实现了极强的扩展性,但也带来了配置复杂度和认知负担。files.md 选择了一条截然不同的路径 —— 以 Git 为原生基础设施,构建极简但完整的 Markdown 知识库同步方案。
双轨同步架构:批量与单文件并行
files.md 的同步机制采用双轨制设计,这一架构决策直接影响了冲突检测的精度和用户体验的流畅性。
批量同步通道(syncTextsWithServer → POST /syncFilenames)负责处理非活跃文件的变更传播。客户端维护一个 server.files 快照对象,记录每个文件的三元组:内容哈希、服务器最后修改时间(lastModified)、客户端最后同步时间(lastClientModified)。当检测到磁盘文件的 mtime 晚于 lastClientSynced 指针时,该文件被标记为 modified 并进入批量同步队列。同时,客户端还会扫描 server.files 中存在但磁盘已消失的文件,标记为 deleted。
单文件同步通道(syncLocalFileWithServer → POST /syncFile)则专门处理当前活跃编辑器的内容。这一分离至关重要 —— 它避免了批量同步与用户实时编辑之间的竞争条件。当用户在编辑器中输入时,单文件通道以 1000ms 的间隔触发,确保本地修改能够及时推送到服务端,而不会被批量扫描干扰。
两个通道的协作遵循明确的优先级规则:当前打开的文件仅通过单文件通道处理,批量通道主动跳过这些路径。这种设计消除了 "编辑冲突" 的最常见来源:同步进程覆盖用户正在输入的内容。
时间戳精度与冲突检测
files.md 在 2025 年 6 月完成了一次关键架构升级:将文件变更跟踪从毫秒级切换到微秒级(microseconds)。这一决策源于对 Linux 内核时间缓存机制的深入理解 ——Linux 使用 CONFIG_HZ 间隔更新缓存时间(通常为 1ms),而实际文件操作由于用户交互、网络延迟和磁盘 I/O 的影响,间隔往往远大于 1ms。微秒级精度提供了充足的安全边际,同时避免了 JavaScript 在处理 int64 纳秒时间戳时的精度丢失问题。
冲突检测的核心逻辑基于 mtime(内容修改时间)和 ctime(元数据变更时间)的差异化使用:
- mtime 用于内容同步决策。它不受权限、所有权变更的影响,能够从
.git/archive恢复,是判断 "内容是否变化" 的可靠指标。 - ctime 用于追踪文件位置变更(重命名、移动)。当文件被移动到
archive/目录或重命名时,ctime的变化会被记录到追加式同步日志中。
服务端在接收到同步请求后,执行三向比较:客户端发送的 lastModified、客户端本地记录的 clientLastModified、以及服务端存储的当前版本时间戳。根据比较结果,返回四种状态之一:notModified(服务器未变)、updatedOnServer(服务器已更新,需拉取)、merged(自动合并完成)、或隐式的冲突状态(需要人工介入)。
竞争条件防护:状态机与锁机制
跨端编辑的最大技术挑战在于竞争条件的处理。files.md 通过多层防护机制确保状态一致性。
编辑器状态锁(isMessingWithCurrentEditor)防止同步回调与文件切换操作的交错执行。当用户点击侧边栏切换文件时,openFile 函数首先触发 syncCurrentEditor 保存当前编辑器内容。此时 switchAwayEditor 标志被设为 true,告诉同步逻辑:当前处于 "切换离开" 模式,不应执行从磁盘重新加载(Reload)分支 —— 这正是 2026 年 4 月修复的一个关键 bug,该 bug 曾导致递归调用 openFile 时全局 currentEditor 被错误旋转。
顺序处理保证(sequential per-user updates)确保同一用户的多个操作不会并发执行。虽然细粒度锁(per-file、per-journal、per-config)用于避免第三方 API 调用(如 ChatGPT 集成)时的全局阻塞,但所有更新最终按用户 ID 排队顺序处理。这消除了文件写入的竞争条件,也解决了消息转发时的乱序处理问题。
漂移密封线(drift-seal line)是代码审查中识别的一个关键检查点。在 files.js:1028 处,系统验证 currentEditor 是否在异步操作期间被意外旋转。如果验证失败,写入操作会被中止,防止数据写入错误的编辑器实例。
冲突消解策略
当双端同时修改同一文件时,files.md 采用务实的冲突消解策略:
-
自动合并优先:如果变更发生在文件的不同区域(基于行级差异比较),系统自动合并并返回
merged状态。 -
服务端优先回退:当自动合并不可行时,默认采用服务端版本为权威版本,客户端在收到
updatedOnServer响应后,将本地内容更新为服务端版本。这一设计假设 "最后写入者获胜",同时通过lastModified时间戳让用户明确知道发生了覆盖。 -
人工介入兜底:对于复杂冲突,系统依赖 Git 的原生能力。由于所有文件都是纯 Markdown,用户可以直接在 Git 工作流中使用标准的三向合并工具(
git mergetool)解决冲突,然后重新同步。
这种分层策略避免了过度工程化 —— 没有试图在浏览器中实现复杂的 OT(Operational Transformation)或 CRDT 算法,而是利用 Git 的成熟机制处理边缘情况。
可落地的工程参数
基于 files.md 的实现经验,构建 Git 原生 Markdown 同步系统时可参考以下参数配置:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 批量同步间隔 | 30000ms | 平衡实时性与电池消耗 |
| 单文件同步间隔 | 1000ms | 确保活跃编辑的及时持久化 |
| 时间戳精度 | 微秒(μs) | 避免 JavaScript int64 问题,满足 Linux 缓存粒度 |
| 冲突检测阈值 | mtime 差值 > 0 | 任何非零差值即视为潜在冲突 |
| 重试退避策略 | 指数退避,最大 60s | 网络恢复后的优雅重连 |
| 快照持久化 | localStorage | SERVER_STORAGE_KEY 存储服务端视图 |
与 Obsidian 的差异化定位
files.md 并非要取代 Obsidian,而是提供一种特定场景下的更优解:
- 无安装负担:纯浏览器 PWA,无需 Electron 运行时
- 代码可完全理解:整个项目代码量小到 "一个人或一个 LLM 能装进脑子"
- 零配置同步:服务端仅需单个二进制文件,或直接使用 iCloud/Dropbox/Git 作为同步后端
- Telegram Bot 入口:移动端通过聊天界面快速记录,降低输入摩擦
其设计哲学强调 "限制激发创造力"—— 没有插件系统意味着没有配置迷宫,没有双向链接图谱意味着用户必须真正理解笔记内容而非依赖视觉化呈现。
资料来源
- files.md 官方文档:Sync Flow
- files.md GitHub 仓库:zakirullin/files.md
- Hacker News 讨论:files.md - Your life in plain .md files
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。