状态:历史 Phase 1 设计与实现记录;方向选择、远端清空保护与恢复规则以
cloud-sync-v2-prd.md为准。 更新时间:2026-07-19 范围:HorseMD 桌面端与 Capacitor 移动端的文件夹级 WebDAV / S3 兼容存储同步。
HorseMD 当前可以打开任意本地 Markdown 文件夹,但内容只存在当前设备。用户希望把自己的笔记、图片和附件在电脑、手机、另一台电脑之间保持一致,同时继续保有普通 Markdown 文件和原有目录结构。
首版的产品目标不是做一个云盘,也不是把用户资料迁入 HorseMD 私有格式,而是:
- 用户可以选择一个已有文件夹并点击“开启云同步”。
- HorseMD 只同步用户明确选择的文件夹及其子目录,不扫描或上传其他本地目录。
- 用户可连接自己的 WebDAV 或 S3 兼容存储(包括 MinIO、R2 等)。
- 首次同步、冲突、删除和失败必须明确可见,且默认不丢数据。
- 同步后的 Markdown、图片与附件仍是正常文件,可继续使用其他编辑器和文件管理器。
用户看到的是“文件夹是否开启云同步”,不是“工作区 ID、JSON、manifest 或同步算法”。
- 主入口名称:
开启云同步。 - 设置页名称:
云同步。 - “HorseMD 工作区”只在说明和高级状态中出现,不作为第一次操作的前置知识。
.horsemd/workspace.json由应用创建和维护,不要求用户查看或编辑。
双向同步会传播删除。因此界面不得把普通同步表述为“自动备份”。在具备远端回收站/版本历史之前,只能称为“云同步”;有保护能力后可说明“可恢复误删”。
默认不移动用户文件夹。已有文件夹点击“开启云同步”后仍留在原路径,原有文章、图片、相对链接、Git 仓库和其他软件路径都不变。
复制到默认 HorseMD 文件夹是辅助选择,适用于用户想保留一份原始资料或主动整理资料库。首版不提供直接删除原目录的“迁移”按钮。
远端为空时,用户只需确认一次“开始同步”。只有本地和远端同时已有变化、同路径内容冲突、凭据失效等风险场景,才展示详情和选择。
| 状态 | 用户含义 | 可用操作 |
|---|---|---|
| 仅本地 | 未上传,HorseMD 不会同步它 | 开启云同步 |
| 已同步 | 已绑定云端,且本地与远端一致 | 立即同步、查看详情、停止同步 |
| 同步中 | 正在传输或处理变更 | 查看进度、取消当前同步 |
| 需要处理 | 存在冲突、凭据或网络错误 | 查看详情、重试、暂停 |
| 已暂停 | 保留配置但不自动同步 | 立即同步、恢复自动同步 |
侧栏、状态栏使用小型状态标识;完整管理集中在设置页,避免打扰只用本地文件的用户。
设置 > 云同步
云端连接
WebDAV · 我的 Nextcloud 已连接
[管理连接]
同步文件夹
我的笔记
~/Documents/我的笔记
已同步 · 刚刚 · 326 个文件
[立即同步] [更多]
工作项目
~/Desktop/工作项目
仅本地
[开启同步] [更多]
[+ 添加要同步的文件夹]
“更多”菜单至少包含:查看同步详情、在文件管理器显示、暂停/恢复自动同步、断开云端、停止同步。停止同步只解除绑定,绝不删除本地文件或远端内容。
用户流程:
- 在侧栏文件夹右键选择“开启云同步”,或在设置页添加文件夹。
- 选择或新建云端连接:WebDAV / S3 兼容存储。
- 应用测试连接,并扫描所选根目录的文件数量和容量。
- 应用在该根目录创建隐藏工作区标记,并把该根目录登记到本机同步列表。
- 常规同步由“立即同步”直接执行;用户可主动查看同步计划。仅当计划包含删除或冲突时,才要求再次确认后传输。
远端为空时的确认页:
“我的笔记”将同步到云端
本地:326 个文件,184 MB
云端:空
将上传:326 个文件
[稍后再说] [开始同步]
远端或本地同时已有内容时,先显示新增、下载、上传、删除与冲突数量。默认行动为“创建冲突副本后同步”,不提供无确认的单向覆盖。
只有用户明确开启同步的根文件夹才创建标记:
~/Documents/我的笔记/
项目/
assets/
读书.md
.horsemd/
workspace.json
- 普通未同步文件夹没有 HorseMD JSON。
- 同一个同步根目录下只有一份
workspace.json。 - 子目录不会各自创建 JSON。
- 不同的同步根目录各自拥有自己的
workspace.json。
标记文件只存工作区 UUID、格式版本和创建时间;不存同步密码、S3 Secret、加密密钥或用户内容。同步引擎将 .horsemd 视为内部控制目录,不将其作为普通用户文档上传。
应用数据目录保存私有登记表(概念路径):
userData/sync/workspace-registry.json
它保存本机已添加工作区的绝对路径、工作区 ID、所绑定连接 ID 和状态摘要。这样设置页可立即列出同步文件夹,完全不需要扫描电脑所有目录。
本机登记表与根目录标记的职责不同:
- 根目录标记:选中、复制或重新添加文件夹时识别该工作区身份。
- 本机登记表:当前设备知道自己正在管理哪些文件夹及其同步配置。
复制整个同步根目录会复制相同工作区 ID。若同一设备登记到两个相同 ID 的本地根目录,应用必须停止并提示用户选择原目录或副本,不能让它们作为同一远端的两个写入源。
移动端的默认 HorseMD 文档库是一个可同步工作区候选根目录。受 iOS 沙盒和 Android SAF 限制,首版移动端不承诺任意外部目录原地同步;移动端先支持其已有 HorseMD 文档库和从该库创建/连接的工作区。桌面端任意文件夹同步不能让移动端显示假入口。
每个同步配置只绑定一个本地根目录和一个远端工作区:
HorseMD/
<workspace-id>/
notes.md
assets/image.png
.horsemd/
manifest.json
changes/
trash/
history/
工作区根目录直接保存用户可读的普通内容,保持原有相对路径。.horsemd/ 保存远端同步清单、删除墓碑、回收站和未来历史记录。凭据只保留在设备安全存储中,不进入本地工作区、远端 manifest 或同步内容。
首版允许一个云端连接绑定多个工作区;一个工作区首版只绑定一个远端连接,以避免多目标双写造成难以解释的冲突。
每个文件的本地同步状态至少记录:
workspaceId
relativePath
contentSha256
size
localMtime
remoteEtagOrRevision
lastSyncedHash
lastSyncedAt
deletedAt (optional)
修改时间只能帮助优化扫描,不能作为冲突唯一依据。决策以“相对上次成功同步后,本地和远端是否发生实质内容变化”为准。
| 本地 | 远端 | 结果 |
|---|---|---|
| 未变 | 未变 | 跳过 |
| 已变 | 未变 | 上传 |
| 未变 | 已变 | 下载 |
| 都变且哈希相同 | 都变 | 标记为一致 |
| 都变且哈希不同 | 都变 | 保留双方,创建冲突副本 |
| 一端删除,另一端未改 | 删除 | 同步删除到回收站 |
| 一端删除,另一端修改 | 冲突 | 保留修改版本,提示处理 |
冲突副本必须是普通文件,例如:
项目计划 (来自 MacBook 的冲突副本 2026-07-18 14-32).md
不做自动文本合并,不做静默“最后写入者胜出”。文本三方合并可在有足够真实数据和测试后单独评估。
同步删除先进入远端 .horsemd/trash/,并写入墓碑;不能直接永久删除。首版设定可恢复窗口和容量策略前,不对外承诺永久版本历史。后续可在此基础上提供回收站和版本恢复界面。
分阶段开放:
- 首次发布:用户点击“立即同步”,首次同步完成后才允许自动同步开关。
- 自动同步:应用启动、应用恢复网络、保存后短暂防抖触发。
- 遇到冲突、未处理错误、大文件限制或移动网络策略时自动暂停,并提供明确状态。
任何同步前必须提交当前 textarea 的 live 内容,避免上传尚未进入 React 状态的源码编辑。
Renderer SyncSettings / useSyncWorkspaces
-> window.api.sync (窄接口)
-> desktop preload IPC / mobile Capacitor shim
-> SyncService
-> SyncEngine
-> LocalScanner + LocalState + SyncPlanner + ConflictResolver
-> SyncProvider (WebDAVProvider | S3Provider)
Renderer 只能请求受限操作:列出已登记工作区、选择文件夹、测试连接、创建同步计划、执行/取消同步、订阅状态。Renderer 不接触原始密码,也不获取任意网络请求能力。
testConnection(config)
list(prefix, cursor)
stat(path)
download(path)
uploadConditional(path, bytes, revision)
moveToTrash(path)
readManifest()
writeManifestConditionally(manifest, revision)WebDAV Provider 处理 PROPFIND、GET、PUT、MOVE/DELETE 和 ETag。S3 Provider 处理列举、读写、条件写入和对象版本/ETag。S3 签名使用受维护的 SigV4 实现,不手写签名。
桌面端网络调用使用 Electron net.fetch;桌面端凭据使用 Electron safeStorage。移动端必须补充与桌面等价的安全凭据存储和原生网络能力后才开放同步 UI。
- 每个本地工作区同一时间只能有一个同步任务。
- 远端 manifest 使用条件写入/ETag 比较,防止两个设备互相覆盖状态。
- 单个内容对象上传、下载和删除同样校验预览时的 revision/内容哈希;发现远端在执行中变化会中止,重新生成计划。
- 不依赖 WebDAV
LOCK作为唯一机制,因为服务实现差异很大。 - 远端拒绝条件写入或发现版本变化时,重新拉取计划并按冲突规则处理。
- 凭据不写入
localStorage、设置 JSON、工作区标记或远端 manifest。 - 默认仅接受 HTTPS;HTTP 仅作为高级兼容项,必须显示明文传输风险并二次确认。
- 当前设置页不提供 HTTP 开关;测试和受控开发环境才可通过内部 API 传入
allowInsecure。 - 连接测试使用受限路径和临时探测对象,失败后清理。
- S3 文档要求最小 Bucket/Prefix 权限,避免建议用户提供整账户权限。
- 首版交付可靠 HTTPS 自托管同步,不把未完成的加密能力宣传为端到端加密。
- E2EE 作为后续独立阶段:密钥生成、导入、丢失、路径/文件名泄露边界、迁移和审计都必须完整设计后再上线。
- 账号体系、HorseMD 托管云和团队协作。
- 实时协同编辑、同段落多人合并、P2P 同步。
- 任意多根目录合并为一个同步源。
- 自动扫描电脑并猜测用户想同步哪些目录。
- 无提示覆盖、无回收站的永久删除。
- 端到端加密的半成品实现。
- 移动端任意外部文件夹的原地同步。
- 新建同步领域的纯函数、数据模型、Mock Provider 与双设备测试矩阵。
- 写入本 PRD,并把同步边界加入 AI 接手文档。
- 停止条件:未侵入现有
useWorkspace、useFileOps、编辑器模式切换逻辑。
- 本地 registry、根目录标记、原地纳入、重复 ID 检测。
- 设置页“云同步”列表与“添加要同步的文件夹”入口。
- 此阶段不上传任何文件,也不要求用户配置云端。
- 停止条件:现有多根工作区、watcher、保存和会话恢复无回归。
- 安全凭据、连接测试、首次同步计划、手动同步、进度、冲突副本和回收站。
- 使用真实 WebDAV 服务与两个隔离应用 profile 验证;当前已覆盖 Apache DAV,发布前仍建议补一次 Nextcloud 实例验证。
- 停止条件:断网、中断、双端编辑、删除、附件场景均不丢数据。
- 复用 SyncEngine,仅增加 S3 Provider 与配置页面。
- 至少在 AWS S3 或 MinIO 和一个 S3 兼容服务验证;当前已通过 MinIO 真实双 profile 链路,发布前还需补一个云厂商兼容服务。
- 停止条件:Endpoint、Region、Prefix、分页、条件写入和大附件通过真实测试。
- 提供移动端安全存储/网络桥接;先支持 HorseMD 本地文档库。
- 启动、恢复、保存防抖自动同步;失败可见、可恢复。
- 停止条件:桌面与 iOS/Android 真实设备间完整双向回归通过。
- 回收站恢复、有限版本历史、导出诊断日志。
- 单独评估并设计端到端加密,不与基础同步风险耦合。
每一个 Provider 至少覆盖以下测试;纯函数 + Mock Provider 测试不能替代真实服务测试:
- 空远端首次上传、空本地首次下载、双方已有内容的合并预览。
- 两个隔离 profile 分别新增、编辑、重命名、删除 Markdown、图片与普通附件。
- 同一路径双端不同修改,双方内容保留且冲突命名可理解。
- 一端删除、另一端修改,修改不丢失且删除可从回收站恢复。
- 断网、认证失效、权限不足、远端被改、应用中断后的恢复。
- 大文档、相对图片、文件夹重命名、隐藏内部
.horsemd目录。 - 桌面现有工作区打开、文件 watcher、保存、源码/富文本切换、会话恢复无回归。
- 移动端能力未完成前,所有同步入口均正确隐藏或标注“即将支持”,不能显示可点击但无效的按钮。
本 PRD 以以下决策为基线:
- 默认采用“原地纳入”,不强制把用户资料移动到
Documents/HorseMD。 - 用户交互称“开启云同步”,而非要求用户先理解“工作区”。
- 一个用户选择的根目录对应一个同步工作区;不扫描全盘。
- 首版是 HTTPS 自托管同步,不承诺 E2EE 或真正备份。
- 冲突保留双方,删除进入回收站,不做静默覆盖。