Fix cloud sync state consistency and provider-safe conflict handling - #1504
Conversation
真实服务端实测:坚果云对正确 etag 的 If-Match 也恒 412(跨设备更新卡死), Alist 类完全忽略条件头(静默 no-op 无防覆盖),MinIO 忽略 If-None-Match:*, S3 DeleteObject 本无条件删除语义。capability 不能按协议类型硬编码, 探测机制落地前全部声明 false;provider 侧条件头映射代码与单测保留供后续回开。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
本地毫秒 updatetime 与服务端整秒 mtime 属于两个时钟域,对端更新落在同一秒内时 会误走 push 覆盖较新的云端内容(L4)。云端 digest 已变时改由 sync_content_md5 (上次同步成功的本地内容基线,push/pull 均记录)判定方向:本地未变 → pull; 双方都变且内容一致 → 收敛基线;真冲突 → 停走保留旧 digest,聚合通知用户 (一轮一条、同一批冲突不重复),绝不自动覆盖任何一端。无基线时退回时间比较(升级兼容)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
按 @CodFrm 的真实服务端实测结论逐项复核并落地修复,已推到本分支(HEAD 本次修复1. 能力过度声明(W1/W2/W3/S2 根因)——WebDAV / S3 / OneDrive 三家 capabilities 全线改为
2. L4 同秒竞态(头号 bug)——方向判定不再跨时钟域比较 云端 digest 已变时,不再比较「本地毫秒 updatetime vs 服务端整秒 mtime」,改用本地内容基线
M4(队列 push 失败后不补偿)此前已由「digest 相等但本地更新时间更晚 → 补偿 push」路径覆盖(43e3aea8 有端到端测试),基线机制不影响该路径。 3. 冲突告知(前面讨论的「要让用户知道」) 真冲突一轮同步聚合一条系统通知,列出所有冲突脚本名;同一批冲突后续轮次不重复轰炸,集合变化后重新通知。8 个 locale 文案已补。 4. 文档(docs/cloud-sync.md) 能力关闭的实测依据、内容基线方向判定、冲突停走 + 通知语义、TOCTOU 窗口的诚实声明(无 CAS 时 此前已在分支修复(本次复核确认)
验证
后续项(不阻塞本 PR)
|
|
@cyfung1031 按你提的「最好告知用户有冲突或者覆盖了」补了一轮覆盖/冲突可见性,已在本地实现并跑通 e2e 验证。都落在现有「脚本同步」设置卡片里,没有新增页面: 做了什么
关于「退而求其次告诉用户我什么时候覆盖了」: 本地 e2e(真实扩展)4 项全过:状态条四态 + 边界(未同步/未启用)+ 深链按 真实 provider「产生覆盖/冲突」的那一轮仍按本 PR 既定口径用单测(mock fs)覆盖:无基线判定 → overwrite 日志、聚合通知一轮一条去重、
|
|
更新到最新 HEAD 对评论里「有问题就显示出来,不要隐藏」的处理
覆盖文案也不再声称固定方向,只告知发生了可检测的覆盖,并让用户进日志确认具体脚本和时间。纯 TOCTOU 的 last-writer-wins 仍然无法由客户端可靠察觉,这个边界继续在文档中明确说明。 最新 HEAD 验证结果
真实扩展 e2e 重新覆盖了:
真实 provider 复验本轮使用当前生产 provider 代码,对四个真实服务重新执行了临时夹具往返:连接校验 → v1 写入 → list/digest → 读取 → v2 无条件覆盖 → 再次 list/read → digest/eTag 变化 → 删除 → 清理。
OneDrive 这次也用了真实 Graph App Root 和真实 结论边界这轮可以确认:移除条件写删后,四个真实 provider 的基础无条件读写往返正常; 但本轮没有用两个独立扩展 profile 重新跑完整 |
- 补偿 push 与无基线回退比较前两侧截断到整秒:同秒内毫秒余数不再误判"本地更新", 消除真实 provider 上每次编辑后的冗余覆盖 push;无基线回退同秒改判 pull - 无基线兜底的 overwrite 日志/通知在写入成功后才登记:失败轮不谎报覆盖, 去重键也不会把下一轮真覆盖静默掉 - classifySyncError 移除 auth 冗余分支与已无真实来源的 unsupported 分类 - 补 counts.failed / counts.conflict / 顶层 error 状态回归测试
SW 起始写只清 error 不清 counts,failed>0 判定需让位于 syncing; error 字符串仍最高优先(起始写已清 error,实际不会与 syncing 并存)
…iptscat#1504 - Google Drive 403 + reason=rateLimitExceeded/userRateLimitExceeded 转 typed rateLimit/retryable(官方建议指数退避,不能归为永久失败) - WebDAV 409 在 RFC 4918 中是父集合不存在等前置问题,不再判 conflict,仅保留 412
- 决策规则/回退规则补整秒对齐语义(同秒不判本地更新、回退同秒判 pull) - 覆盖日志说明改为写入成功后登记 - 删除「日志按 LogCleanCycle 自动清理」声明(LoggerDAO.deleteBefore 无调用点) - 错误分类表删除已无来源的 unsupported 行;Zip digest 表述改为空
结论复查最新 HEAD 已经修好的重点目前以下方向基本处理正确:
这些修复说明整体设计方向已经比较稳定。之前提出的覆盖误报、PR 描述过时和同秒时间误判问题不再作为阻塞项。 剩余风险主要集中在:
问题一:只判断
|
…#1504 - WebDAV verify() 非 401 错误改抛 createWebDAVFileSystemError(与 list/read 一致): verify 在 RETRYABLE_TRANSIENT_OPS 内且每轮同步经 factory 必跑,此前抛普通 Error 会让瞬时 5xx 既不被 limiter 重试、又被 classifySyncError 判 fatal 而中止整轮同步 - 「立即同步」按钮 disabled 改用 savedEnable:SW cloudSyncOnce 用的是已保存配置, 勾选草稿未保存时点击会静默 return,按钮须随已保存配置禁用,避免点击无反馈 - 清理 deleteCloudScript tombstone 里 3 行注释死代码
SettingsLayout now calls useSearchParams() for the cloud sync deep link (af8164e), which requires a Router context the tests didn't provide. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
代码评审 + 多机同步实测小结完整评审 + 真机/mock 多机同步验证,结论:可以合并。收尾修复已并入本 PR 收尾修复(3 处)
Provider 层评审(错误分类/重试对抗式审查)
多机同步实测(A/B 两设备隔离本地态 + 共享云端目录,跑真实
|
| 验证 | 方式 | 结果 |
|---|---|---|
| 正常流程 ①上云②B拉v1③A改v2④B收v2⑤状态传播⑥删除tombstone⑦orphan跳过 | 真实坚果云 WebDAV | ✅ 7/7 |
L4 同秒竞态修复:人为把 B 本地 updatetime 设成比云端晚 5000ms(旧码必 push v1 覆盖 v2) |
真实坚果云(整秒 mtime,唯一可复现环境) | ✅ B.local=v2, cloud=v2,B 因内容基线匹配而 PULL,云端未被覆盖 |
| 双端冲突:A 改 v2A 上云、B 改 v2B | MockFS 两设备确定性 | ✅ 抛 SyncBothChangedConflictError,两端都不覆盖,counts.conflict=1 |
部分失败+pending 重放:注入首次 .meta.json 写失败 |
MockFS 两设备确定性 | ✅ PushScriptPartialError→登记 pending_sync_ops:{op:"push"}→下轮重放补齐→B 拉到完整脚本 |
客观门禁
| 项 | 结果 |
|---|---|
| 单测 sync / filesystem | ✅ 87 / 174 通过 |
tsc --noEmit |
✅ 零错误 |
结论:同步核心 + 6 provider 已审、两条 Med 已修、多机正常流程真机通过、L4 修复真机确认、硬场景 mock 确认 → 可以合并。 剩余 Dropbox 409 日志标签、pending 重放路径多一次 fs.list() 属可选优化,不阻断。
|
终于合并了~ 希望同步处理的相关问题都能够解决吧 |


背景
本 PR 在现有
main的 per-file best-effort 同步语义上,修复云同步状态污染、错误吞掉、provider 错误分类不完整以及冲突/覆盖不可见的问题。核心原则:
主要改动
同步状态与兼容
pullScript()、云端删除和 tombstone 写入的真实失败不再静默当作成功。file_digest;分片上传部分成功时只推进已成功文件。scriptcat-sync.json写回前重新读取并合并远端最新状态,兼容缺字段旧格式和旧file_digeststring map。.user.jswithout.meta.json会跳过并保留远端状态。方向判定与可见性
sync_content_md5记录上次同步成功的本地内容基线,减少客户端毫秒时钟与服务端整秒 mtime 的跨时钟误判。provider typed error 与 retry
FileSystemError。rateLimitExceeded/userRateLimitExceeded。verify/open/read/openDir/list/getDirUrl)会对 typed rate-limit/瞬时错误退避重试。write/delete/create不重试,避免非幂等操作重复执行。生产兼容性与边界
.user.js/.meta.json、缺字段scriptcat-sync.json和旧 digest map 继续可读。list → decision → writeTOCTOU 窗口,最后写入者获胜。验证
docs/cloud-sync.md。