黑曜石移动端 Phase 6:PC 自动同步与手机刷新可靠性

Phase 5 让手机能写回 GitHub 之后,「黑曜石移动端」还剩最后一段链路:PC 侧的 Obsidian vault 改动要靠手动 git 操作才能上去,手机端也不知道什么时候该刷新。Phase 6 分两半解决——6A 做 PC 端 vaultsync 自动同步守护进程,6B 做 Android 刷新可靠性。同日完成开发、主 Agent 审查、修复与提交,PC↔GitHub↔Android 三端链路全部打通。

整体图景

三端同步链路
图 1:PC(vaultsync 守护进程)↔ GitHub(唯一真相源)↔ Android(刷新可靠性)

Phase 6A:PC 端 vaultsync 守护进程

交付物是 tools/vaultsync/——Node.js + chokidar + 系统 git CLI,约 280 行,Android 端零改动:

  • Vault 文件变化 → 30s debounce(持续编辑时计时重置)→ 自动 add -A + commit + push
  • 每 5 分钟 periodic fetch + rebase + push,拉取手机端推上来的新 commit;
  • 所有触发方式(startup / watcher / periodic / manual)共用单一 syncOnce(),进程内 Promise 链互斥锁保证单飞;
  • rebase 冲突 → --abort 保留双方内容、明确报错、绝不 force;git 失败只记录不退出,下次触发自愈。

审查抓出的 2 个 P1 + 2 个 P2

实现完成后由主 Agent review,发现的问题比实现本身更有记录价值:

级别 问题 修复
P1 git 子进程无超时、未禁交互式凭据提示——网络半开或凭据失效会占死互斥锁,daemon 变成「活着的僵尸」:无日志、不再同步 300s 超时 + GIT_TERMINAL_PROMPT=0,超时错误有明确日志
P1 gitErrorMessage 只取 fatal 首行——remote URL 内嵌 token 时(https://token@...)恰好把凭据挑进日志 日志输出前把 URL userinfo 脱敏为 ***@,且先脱敏再截断,防跨截断点漏半截
P2 rebase 冲突 catch 里的 --abort 不区分 rebase 是谁启动的——可能废弃用户手动 rebase;遗留 rebase 状态会让每轮同步永久误报冲突 doSync 入口检测 `.git/rebase-merge
P2 commit 受用户全局 commit.gpgsign / pre-commit hook 影响,可能每轮永久失败 -c commit.gpgsign=false commit --no-verify

验证:单测 9/9(离线 bare remote 沙箱 + 真实 git,含互斥串行、冲突保双方、遗留 rebase 拒绝插手、凭据脱敏);E2E 双向 PASS——PC→GitHub(gh API 逐字节核对),GitHub→PC(模拟手机 commit 被 periodic 拉下、本地文件到位)。daemon 常驻运行后,连 E2E 测试文件的清理 commit 都是它自己推上 GitHub 的。

Phase 6B:Android 刷新可靠性

架构零变动,唯一刷新入口 refreshTree() 不变,加的是「什么时候该刷、什么时候不该刷」的纪律:

  • Freshness window(30s):刚成功过的自动刷新被静默去重(不发 HTTP、不动 UI 状态);Failed 不置 fresh,失败后重试不被压制;
  • 手动 force 直通:SyncScreen「立即刷新」/ Reader「刷新此笔记」/ Onboarding 连接仓库改 force=true,始终真实请求;
  • 新自动触发源:回到前台(距上次成功 ≥3 分钟)+ Offline→Online 边沿(纯函数检测,每次恢复恰好一次,抖动由 window 兜底);
  • 设置门控:全部自动入口受「自动刷新」设置门控,Settings 语义文案统一。

审查抓出的问题:一个「恒为真」的假测试

这轮审查最有教育意义的是一个 P2:门控设置原本没有真正的自动化测试——原 autoRefreshDisabled_manualForceStillRequests 用例根本不经过门控(refreshTree 不读 autoRefresh 设置),恒为真,造成「门控已测」的错觉。修复是把门控抽为纯函数 autoRefreshAllowed(autoRefresh, hasRepo, isOnline),startup 与 RefreshTriggers 共用,新增 4 个用例真正测上门控。

另一个值得记的 P2:freshness 去重跳过与服务器 304 共用 NotModified 返回值——前者根本没问服务器,未来给自动刷新加结果提示时会说谎。当前安全性没问题(读返回值的调用方全是 force=true),KDoc 注明双重含义并写入文档 Known Issues。

验证:JVM 153/153(新增 4 个门控用例)、androidTest 27/27(MockWebServer + in-memory Room + 假时钟)、assembleDebug 通过。

两轮审查的模式

两个子阶段都走了「先验收实现 → 审查 → 修 P1/P2 → 补测试 → 提交」的节奏,暴露出两类规律性问题:

  • P1 全是「真实环境存活/安全」类:单测全绿挡不住的问题——网络半开占死锁、凭据进日志。教训:守护进程类代码必须假设网络会「半死不活」而非干净失败;日志脱敏要在源头做,不能赌当前配置干净。
  • P2 全是「现在没事、将来咬人」的语义陷阱:abort 别人的 rebase、永真测试造成的假安全感、一个类型两个含义。教训:恢复操作(abort/reset)必须确认作用于自己创建的状态;测试名字承诺了什么,就要真的测到什么

修复成本都很低(几行级),说明早期小步审查比阶段末大审查有效。

遗留事项

  1. 跨设备人工 E2E 未跑:PC→GitHub→Android、Android→GitHub→PC、Offline→Online 三条链路需要真机 + PC 同时在手,步骤已写入 6B 文档;
  2. vault .gitignore 整目录忽略 .obsidian/ 的既有行保留未动——与「同步 Obsidian 配置」的潜在需求冲突,取舍待定;
  3. vaultsync 目前需手动 npm start,开机自启是 Phase 7 候选方向之一。

项目时间线(至此)

  • Phase 1-2(09-02 ~ 09-03):分析与纠正、骨架 + 静态 UI;
  • Phase 3(09-03):GitHub online-first + 缓存 + 阅读闭环;
  • Phase 4(09-04):Obsidian 渲染(WikiLink/Callout/图片/嵌入/Frontmatter);
  • Phase 5(09-04):编辑 + GitHub 写入 + 409 两版本冲突界面,App 达到日常可用 V1;
  • Phase 6(09-04):PC 侧自动同步闭环 + 手机刷新可靠性,三端链路全部打通。

从「手机只能看」到「PC 改完自动上云、手机自动拿到、手机改完安全写回」,整个知识库的最小可用同步环闭上了。下一步是跨设备 E2E 自动化和附件链路强化,但那是下一个阶段的事了。