Skip to content

Fix cloud sync state consistency and provider-safe conflict handling - #1504

Merged
CodFrm merged 108 commits into
scriptscat:mainfrom
cyfung1031:pr/sync-fix/100
Jul 17, 2026
Merged

Fix cloud sync state consistency and provider-safe conflict handling#1504
CodFrm merged 108 commits into
scriptscat:mainfrom
cyfung1031:pr/sync-fix/100

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

背景

本 PR 在现有 main 的 per-file best-effort 同步语义上,修复云同步状态污染、错误吞掉、provider 错误分类不完整以及冲突/覆盖不可见的问题。

核心原则:

  1. 保持 per-file best-effort,单个文件失败不阻塞其他文件。
  2. 成功文件推进自己的 digest,失败文件保留旧 digest,下轮重试。
  3. 旧云端数据继续可读,无需手工迁移。
  4. provider 层只负责 typed error 和原生 digest,不承担同步冲突策略。
  5. 当前所有 provider 均使用普通覆盖写;不宣称、不伪造 atomic CAS/条件写删能力。

主要改动

同步状态与兼容

  • pullScript()、云端删除和 tombstone 写入的真实失败不再静默当作成功。
  • 失败文件不推进 file_digest;分片上传部分成功时只推进已成功文件。
  • scriptcat-sync.json 写回前重新读取并合并远端最新状态,兼容缺字段旧格式和旧 file_digest string map。
  • orphan .user.js without .meta.json 会跳过并保留远端状态。
  • 队列触发的安装/删除只更新当前 uuid 的 digest,避免盖章未对账文件。

方向判定与可见性

  • 使用 sync_content_md5 记录上次同步成功的本地内容基线,减少客户端毫秒时钟与服务端整秒 mtime 的跨时钟误判。
  • 云端已变且本地未变时 pull;两端内容一致时直接收敛基线;两端都变时停走并聚合通知,不自动覆盖任何一端。
  • 无内容基线的升级兼容路径使用整秒对齐的时间比较,并在真实写入成功后记录覆盖日志/通知。
  • 设置页展示同步中、失败、冲突和覆盖计数;“立即同步”构建文件系统失败时不再静默。

provider typed error 与 retry

  • WebDAV、S3、OneDrive、Google Drive、Dropbox、Baidu 的关键 notFound/conflict/auth/rate-limit/瞬时 5xx 转为 FileSystemError
  • Google Drive 兼容 403 rateLimitExceeded / userRateLimitExceeded
  • 只有 read-like 操作(verify/open/read/openDir/list/getDirUrl)会对 typed rate-limit/瞬时错误退避重试。
  • 普通 write/delete/create 不重试,避免非幂等操作重复执行。

生产兼容性与边界

  • .user.js / .meta.json、缺字段 scriptcat-sync.json 和旧 digest map 继续可读。
  • Google Drive / Dropbox / Baidu / WebDAV / S3 / OneDrive 都不宣称 atomic CAS。普通 push 仍存在 list → decision → write TOCTOU 窗口,最后写入者获胜。
  • 真实 provider 的 OAuth、限流、路径缓存和服务端兼容性仍需在有账号/夹具的环境持续验证。

验证

  • 单元测试覆盖同步状态、digest 收敛、冲突/覆盖可见性、provider typed error 与 retry 边界。
  • 云同步设计与当前实现详见 docs/cloud-sync.md

cyfung1031 and others added 3 commits July 14, 2026 23:03
真实服务端实测:坚果云对正确 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>
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@CodFrm 的真实服务端实测结论逐项复核并落地修复,已推到本分支(HEAD c75ea8bc)。

本次修复

1. 能力过度声明(W1/W2/W3/S2 根因)——WebDAV / S3 / OneDrive 三家 capabilities 全线改为 false

  • 依据跨服务端实测:坚果云对正确 etagIf-Match 也恒 412、Alist 类完全忽略条件头(静默 no-op)、MinIO 忽略 If-None-Match: *、S3 DeleteObject 本无条件删除语义、OneDrive 全支持反而暴露 createOnly 误用会 409 卡死编辑。capability 按协议类型硬编码的假设整体不成立。
  • 同步层据此自动停发全部条件头,回落无条件读写 + last-writer-wins 最终一致——坚果云跨设备卡死、合规服务端编辑 412 两类「不能用」一并解除。
  • provider 侧条件头映射代码与单测保留(inert),供后续「按 endpoint 实测探测能力」方案回开;若倾向整层直接移除,我可以再提一个纯删除的 commit。

2. L4 同秒竞态(头号 bug)——方向判定不再跨时钟域比较

云端 digest 已变时,不再比较「本地毫秒 updatetime vs 服务端整秒 mtime」,改用本地内容基线 sync_content_md5(上次同步成功时的本地内容 md5,push / pull 均记录):

本地(相对基线) 行为
未变 pull(同秒竞态下不再误 push 覆盖较新云端)
已变、但与云端内容一致 收敛基线,不产生写操作
已变、与云端不一致(真冲突) 停走:保留旧 digest 与云端 status,不覆盖任何一端,通知用户
无基线(升级前旧数据) 退回旧时间比较规则,平滑迁移

M4(队列 push 失败后不补偿)此前已由「digest 相等但本地更新时间更晚 → 补偿 push」路径覆盖(43e3aea8 有端到端测试),基线机制不影响该路径。

3. 冲突告知(前面讨论的「要让用户知道」)

真冲突一轮同步聚合一条系统通知,列出所有冲突脚本名;同一批冲突后续轮次不重复轰炸,集合变化后重新通知。8 个 locale 文案已补。

4. 文档(docs/cloud-sync.md)

能力关闭的实测依据、内容基线方向判定、冲突停走 + 通知语义、TOCTOU 窗口的诚实声明(无 CAS 时 list → write 之间 last-writer-wins,同步层无法察觉),以及功能范围声明:GM storage 值与 @require/@resource 资源不参与云同步(N1 列为有意取舍,另案设计)。

此前已在分支修复(本次复核确认)

  • scriptInstall 更新误当新增:队列路径已传 fileDigestMap,已同步过的脚本不再走 createOnly(ad8a4939,含测试)——即合规服务端「每次编辑 409/412」的语义错误已修,能力关闭是在此之上的第二道保险。
  • pull 后本地 updatetime 采用云端文件时间,消除补偿 push 振荡(4819c76d)。
  • 自我 412 收敛、部分失败 digest 不污染、Baidu 2xx 非 JSON / Dropbox 409 收窄 / 501 不重试等错误分类修正见分支提交历史。

验证

  • 全量 pnpm test:290 文件 / 3190 用例全部通过;prettier + tsc + eslint 干净。
  • 新增 6 个回归测试:同秒竞态必须 pull、真冲突不覆盖 + 保留 digest + 通知一次、冲突多轮去重、内容一致自动收敛、无基线退回时间规则、pull 记录基线;另有 3 个 provider capability 断言改为全 false。
  • 局限:以上是 mock / 单测层验证。麻烦 @CodFrm 有空用你本地的 e2e-sync 流程对坚果云 / MinIO / 合规 WebDAV 再压一轮——能力关闭 + 基线判向后,L1–L7 与 L4 同秒场景预期应全通过,真冲突场景应看到停走 + 一条聚合通知。

后续项(不阻塞本 PR)

  • 按账号/endpoint 实测探测条件能力后逐项回开(S3 CAS、OneDrive 三项都有真实价值)。
  • GM values / resources 同步另开 feature PR(隐私、容量、二进制、多端 merge 需单独设计)。
  • 你提到的同步日志/状态面板可以在冲突通知的基础上继续做。

@CodFrm

CodFrm commented Jul 16, 2026

Copy link
Copy Markdown
Member

@cyfung1031 按你提的「最好告知用户有冲突或者覆盖了」补了一轮覆盖/冲突可见性,已在本地实现并跑通 e2e 验证。都落在现有「脚本同步」设置卡片里,没有新增页面:

做了什么

  1. 同步状态条(卡片顶部):正常 / 同步中 / 有覆盖或冲突(琥珀警示)/ 失败 四态;数据来自每轮同步写入的 cloud_sync_state(设备本地,chrome.storage + onChanged 实时刷新)。
  2. 覆盖单独标记:方向判定的「无内容基线兜底」分支(可能覆盖未知改动)单独打 warn 日志 action:overwrite,并聚合一条通知(仿冲突的一轮一条去重)。
  3. 查看日志深链:状态条 /通知点击 → #/logs?query=… 预过滤 service=synchronize(覆盖态再加 action=overwrite),直接看到「什么时候覆盖了哪个脚本」,自己确认。
  4. 立即同步 / 保存 按钮。

关于「退而求其次告诉用户我什么时候覆盖了」overwrite 只在可检测的无基线兜底场景标记;纯 TOCTOU last-writer-wins 客户端无法察觉,doc 里写明了边界。真冲突仍是停走 + 聚合通知(已有)。

本地 e2e(真实扩展)4 项全过:状态条四态 + 边界(未同步/未启用)+ 深链按 service+action 过滤 + cloudSyncOnce 端点。验证中还发现并修掉一个缺陷——立即同步遇到不可达服务端会静默失败(未捕获异常、状态条无反应),已改为写入 error 状态 + toast 提示,复验通过。

真实 provider「产生覆盖/冲突」的那一轮仍按本 PR 既定口径用单测(mock fs)覆盖:无基线判定 → overwrite 日志、聚合通知一轮一条去重、cloud_sync_state 计数写入;需账号/夹具才能做真云端 e2e。

deeplink-filtered-logs state-warning

@CodFrm

CodFrm commented Jul 16, 2026

Copy link
Copy Markdown
Member

更新到最新 HEAD fbad7394 后重新核对了本 PR 的讨论点,并把本地可验证范围全部重跑了一遍。

对评论里「有问题就显示出来,不要隐藏」的处理

fbad7394 补齐了两个容易误导用户的状态:

  1. 单个文件同步失败时,即使本轮没有顶层 error,只要 cloud_sync_state.counts.failed > 0,设置页状态必须显示 失败,不能显示成同步正常。
  2. 日志深链只有在“纯覆盖”时才附加 action=overwrite;只要本轮含 conflict 或 failed,就只过滤 service=synchronize,避免把真正需要用户处理的冲突/失败日志隐藏掉。

覆盖文案也不再声称固定方向,只告知发生了可检测的覆盖,并让用户进日志确认具体脚本和时间。纯 TOCTOU 的 last-writer-wins 仍然无法由客户端可靠察觉,这个边界继续在文档中明确说明。

最新 HEAD 验证结果

  • pnpm run typecheck:通过。
  • 全量 Vitest:304 files / 3346 tests 全部通过
  • Rspack 开发构建:通过(只有 Monaco 动态 require 的既有 warning)。
  • 真实扩展 scratch e2e:4/4 通过

真实扩展 e2e 重新覆盖了:

  • idle / syncing / warning / error 四态与未启用边界;
  • 纯覆盖:warning,日志深链含 service=synchronize + action=overwrite
  • 含冲突:warning,但深链不带 overwrite 过滤;
  • 文件级失败:error,即使顶层 error 为空,深链也不带 overwrite 过滤;
  • 真实 SW logger → LoggerDAO → 日志页的 service/action 深链过滤;
  • cloudSyncOnce 未启用时安全 no-op;
  • “立即同步”遇到不可达 WebDAV 时写入 error、恢复 syncing:false、toast/状态条可见,页面无未捕获异常。

真实 provider 复验

本轮使用当前生产 provider 代码,对四个真实服务重新执行了临时夹具往返:连接校验 → v1 写入 → list/digest → 读取 → v2 无条件覆盖 → 再次 list/read → digest/eTag 变化 → 删除 → 清理。

服务 创建/读取 无条件覆盖 digest/eTag 更新 删除与清理 结果
坚果云 WebDAV 通过
第二台 WebDAV(LAN) 通过
S3 / MinIO(LAN) 通过
OneDrive / Microsoft Graph 通过

OneDrive 这次也用了真实 Graph App Root 和真实 eTag,不是 mock。四路测试均使用唯一临时目录/对象,结束后已清理,没有碰现有同步数据。

结论边界

这轮可以确认:移除条件写删后,四个真实 provider 的基础无条件读写往返正常;fbad7394 的失败/冲突/覆盖可见性在真实扩展中正常。

但本轮没有用两个独立扩展 profile 重新跑完整 SynchronizeService 跨设备 L1–L7,所以不会把这次 provider 往返扩大描述成同秒竞态、tombstone、status merge、多文件 best-effort 等完整真实云矩阵都已复验。相关业务状态机本轮由全量单测覆盖;如合并前仍要求最新 HEAD 的双端 L1–L7,我可以再按两个 profile 单独跑一轮并回报。

CodFrm added 4 commits July 17, 2026 10:33
- 补偿 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 表述改为空
@cyfung1031

cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

结论

复查最新 HEAD 38ded05 后,大部分讨论中提出的问题已经修复,但仍有两个可能导致同步状态被错误确认、失败操作无法自动恢复的关键缺口。建议在合并前继续修改。

已经修好的重点

目前以下方向基本处理正确:

  • 已移除不可靠的 provider 条件写、条件删除和 capability 声明。WebDAV、S3、OneDrive 等 provider 统一使用普通覆盖写,避免因不同服务端对 If-MatchIf-None-Match 支持不一致而导致同步不可用。

  • 已使用 sync_content_md5 判断本地内容是否真的发生修改,避免仅通过本地毫秒时间和云端秒级时间判断同步方向。

  • 本地时间和云端时间现在会统一截断到整秒后比较。同一秒内的毫秒差不再被误判为本地更新,避免刚上传完成后又触发一次无意义的补偿 push;无内容基线时,同秒也会优先 pull。

  • 本地和云端都修改时不再自动覆盖,而是保留旧 digest、暂停该脚本同步并提示冲突。

  • 单个文件失败不会阻塞其他文件;失败文件保留旧 digest,成功文件可以推进。

  • scriptcat-sync.json 的旧格式、损坏文件、远端并发更新、远端删除状态被错误复活等问题已有处理。

  • 已增加同步状态、失败提示、冲突通知、覆盖提醒和日志入口,不再把部分失败显示为“同步正常”。

  • 覆盖提醒的登记时机已经修复。现在只有在 push 或 pull 实际成功后,才会记录 overwrite 日志、增加覆盖计数并发送通知。写入失败时不会再误报“已经发生覆盖”,下一轮成功重试后仍会正常提示。

  • PR 描述已经更新,明确当前实现使用普通覆盖写,不宣称支持 atomic CAS 或条件写删,并承认 list → decision → write 之间仍存在 TOCTOU 窗口。

  • Provider 错误分类继续完善:

    • Google Drive 已识别 403 中的 rateLimitExceededuserRateLimitExceeded
    • WebDAV 只对真正的瞬时 5xx 进行重试;
    • 501、505、507 等永久错误不会再重复退避。
  • 当前测试工作流通过。

这些修复说明整体设计方向已经比较稳定。之前提出的覆盖误报、PR 描述过时和同秒时间误判问题不再作为阻塞项。

剩余风险主要集中在:

  1. .user.js.meta.json 文件组的一致性;
  2. 删除操作部分成功后的持久化重试。

问题一:只判断 .user.js,可能把未处理的 .meta.json 标记为已同步

当前主流程判断脚本是否变化时,稳定路径仍然主要检查:

fileDigestMap[file.script.name] === file.script.digest

也就是只比较 <uuid>.user.js 的 digest。

如果 .user.js 没有变化,并且本地时间不比云端源码时间新,就会直接跳过整个 UUID,没有检查 <uuid>.meta.json 的 digest。

因此可能出现以下情况:

  1. .user.js 上传成功,但 .meta.json 上传失败。
  2. 成功的 .user.js digest 被推进,失败的 .meta.json digest 保留旧值。
  3. 下一轮 .user.js digest 与本地记录一致。
  4. 本地时间不比云端 .user.js 时间新。
  5. 同步流程直接跳过整个 UUID。
  6. .meta.json 不再获得重试机会。

这意味着当前实现虽然做到了“失败文件保留旧 digest”,但没有保证“下一轮一定会根据旧 digest 生成重试任务”。

生产路径会使这个问题更容易出现

生产代码通过 publishInstallScript() 发布安装或更新消息时,没有传递 updatetimecreatetime

因此 pushScript() 通常会使用:

script.updatetime || script.createtime || Date.now()

也就是使用队列执行时的 Date.now() 作为云端文件修改时间。

.user.js 成功、.meta.json 失败后,下一轮本地脚本的实际更新时间可能:

  • 早于云端 .user.js 的写入时间;
  • 或者与云端写入时间处于同一秒。

新的整秒比较会将这种情况判断为“本地不比云端新”,从而直接跳过,导致 .meta.json 永久不再重试。

现有相关测试人为设置了更晚的 DAO updatetime,确保第二轮一定再次 push,但没有覆盖实际生产消息不包含时间字段的调用方式。

metadata-only 变化也可能被静默确认

如果其他设备或人工操作只修改了 .meta.json,而 .user.js 没有变化,也可能发生:

  1. .user.js digest 与本地记录一致;
  2. 当前流程直接跳过该 UUID;
  3. .meta.json 没有被读取、应用、合并或重新上传;
  4. 最后的 updateFileDigest() 重新列出云端文件;
  5. 新的 .meta.json digest 被写入本地;
  6. 这份实际上没有处理过的 metadata 被标记为“已同步”。

这会导致后续同步不再处理该变化。

建议修改

应将 .user.js.meta.json 作为同一个同步文件组进行对账:

  • 两个 digest 都没有变化时,才能直接跳过。
  • .meta.json digest 发生变化时,必须明确读取、采用、合并或重新上传。
  • 只有实际读取、写入或明确采用成功的文件才能推进 digest。
  • 或者持久化记录具体失败文件,让下一轮直接重试失败分片,而不是重新依赖 .user.js 的方向判断。

至少应增加以下测试:

  1. .user.js 上传成功,.meta.json 上传失败。
  2. 安装消息不携带 updatetime,与真实 publishInstallScript() 路径一致。
  3. 本地时间早于云端写入时间,或两者处于同一秒。
  4. 第二轮 provider 恢复后,仍必须重新上传 .meta.json
  5. 源码未变但 metadata digest 变化时,不能直接推进 digest 后跳过。

问题二:删除操作部分成功后,没有可靠的后续重试机制

删除云端脚本实际上是多步操作。

syncDelete=true 时:

  1. 删除 <uuid>.user.js
  2. 写入 tombstone <uuid>.meta.json

syncDelete=false 时:

  1. 删除 <uuid>.user.js
  2. 删除 <uuid>.meta.json

如果第一步成功、第二步失败,当前代码会记录本轮失败并保留旧 digest,但没有持久化“这个 UUID 仍有未完成删除操作”的状态。

下一轮不会自动恢复剩余操作

第一轮部分失败后,下一轮可能看到:

  • 本地脚本已经不存在;
  • 云端 <uuid>.user.js 已经成功删除;
  • 云端只剩 <uuid>.meta.json

但当前“本地无脚本”的处理路径只会在云端存在 .user.js 时生成 pull 任务。

对于“本地无脚本、云端只有 .meta.json”的状态,不会生成新的删除或 tombstone 重试任务。

因此可能出现:

  • tombstone 写入失败后永久不再重试;
  • 其他设备无法看到删除标记;
  • 其他设备可能根据残留状态重新上传脚本;
  • syncDelete=false 时,残留的 .meta.json 长期无法清理;
  • 后续全量更新 digest 后,之前保留的失败上下文彻底丢失。

也就是说,file_digest 只能表示文件快照,不能可靠充当“未完成操作队列”。

建议修改

应增加持久化的 pending operation,例如:

pending_sync_operations[uuid] = {
  type: "delete" | "write_tombstone",
  remainingFiles: [...]
}

每轮同步开始时优先处理这些未完成操作,只有全部步骤成功后才清除记录。

至少应增加以下两轮恢复测试:

  1. 第一轮删除 .user.js 成功。
  2. 写 tombstone 或删除 .meta.json 失败。
  3. 本地脚本已经不存在。
  4. Service Worker 重启或进入下一轮同步。
  5. Provider 恢复正常。
  6. 同步必须继续完成剩余的 tombstone 写入或 meta 删除。
  7. 全部成功后才清除 pending 状态和旧 digest。

合并建议

新增 commits 已经解决:

  • 覆盖失败时的误报问题;
  • 同秒时间比较问题;
  • PR 描述与最终实现不一致的问题;
  • 部分 provider 错误分类问题。

这些内容不再需要作为阻塞项提出。

但以下两个核心问题仍然存在:

  1. .user.js.meta.json 没有联合对账,metadata 失败或单独变化可能被永久跳过,并被错误标记为已同步。
  2. 删除操作部分成功后没有持久化重试,未完成的删除或 tombstone 意图可能永久丢失。

它们都会导致:

失败文件虽然保留了旧 digest,但下一轮实际上不一定会产生重试操作。

这会直接影响“失败后能否自动恢复”和“digest 是否真实代表已经完成同步”,属于同步正确性的核心问题。

因此结论仍然是:

建议 Request changes,修复上述两个状态恢复问题后再合并。

CodFrm added 6 commits July 17, 2026 10:45
…#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 行注释死代码
@CodFrm CodFrm closed this Jul 17, 2026
@CodFrm CodFrm reopened this Jul 17, 2026
cyfung1031 and others added 2 commits July 17, 2026 19:07
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>
@CodFrm

CodFrm commented Jul 17, 2026

Copy link
Copy Markdown
Member

代码评审 + 多机同步实测小结

完整评审 + 真机/mock 多机同步验证,结论:可以合并。收尾修复已并入本 PR 9beae5a3(均 TDD:先补失败测试再修)。

收尾修复(3 处)

修复 问题 影响
WebDAV verify() 非 401 改抛 typed error(createWebDAVFileSystemError,与 list/read 一致) 此前抛普通 ErrorverifyRETRYABLE_TRANSIENT_OPS 内且每轮同步经 FileSystemFactory.create 必跑 瞬时 5xx 既不被 limiter 重试、又被 classifySyncErrorfatal 中止整轮
「立即同步」按钮门控 draft.enablesavedEnable SW cloudSyncOnce() 用已保存配置,勾选草稿未保存时点击静默 return 按钮随已保存配置禁用,消除点击无反馈
deleteCloudScript tombstone 里 3 行注释死代码 清理

Provider 层评审(错误分类/重试对抗式审查)

Provider 结论
Baidu / S3 / OneDrive ✅ 无问题(状态码映射、ETag、raw 响应转 typed 均正确)
WebDAV ⚠️→✅ 即上面 verify 那条,已修
Dropbox ⚠️ too_many_write_operations(409) 判 fatal 而非 transient,仅影响日志标签(写不重试),非阻断
CAS/条件写残留 ✅ 无(capabilities/If-Match/412/create-only 全线搜空)

多机同步实测(A/B 两设备隔离本地态 + 共享云端目录,跑真实 syncOnce

验证 方式 结果
正常流程 ①上云②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() 属可选优化,不阻断。

@CodFrm
CodFrm merged commit fc9ae99 into scriptscat:main Jul 17, 2026
9 of 10 checks passed
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

终于合并了~ 希望同步处理的相关问题都能够解决吧

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CloudSync Related to CloudSync P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants