来源:
notes/documents/网页日程与飞书日历同源:一处定义、两个目的地.md网页日程与飞书日历同源:一处定义、两个目的地
2026-09-22。起点是一个症状:网页日程里 09-19 之后就空了,飞书日历上却有一片。查下来不是投影坏了,
是「什么算一条日程」这件事被写了三遍——飞书事件线一套、飞书笔记线一套、网页(Neon)一套,
三套各说各话。工作票在 .scratch/web-calendar-parity/(11 张,全部 resolved)。
结论
把「日历条目」提成本地的一等定义,两个目的地都当它的兄弟投影。
- 一处定义:本地 SQLite 视图
calendar_entries,取合规则只写在scripts/calendar_entries.py, 事件线与笔记线统一成同一形状(kind区分daily/note)。 - 条目范围 = 现在合规 ∪ 已经发出去过。后半句是关键:日历上的内容是「写出去的历史」,不是
「现在推导出来的集合」。本地 2026-06 那 78 条事件现在
calendar_enabled全是 0,日历上却写着 77 条—— 只按「现在合规」取,网页永远对不上。 - 两个目的地:飞书日历(事件线与笔记线合成一个)与 Neon(网页)都消费这份定义。
- 不撤回:条目一旦发出去就留着(
withdrawn的笔记除外)。这一轮不实现任何删除日历的规则。 - 飞书不是数据源:它只在一条验收对账命令里出现,日常运行、构建、投影都不依赖它。
网页那一份
Neon 里的 calendar_entries 只是投影,随时可 truncate 重灌;权威源始终是本地 SQLite。
投影脚本 web/scripts/project-calendar-entries.mjs 按台账 entity_projection_state
(projection = 'neon')增量发,只处理缺失 / 落后 / 失败的条目。
两个触发点,容错口径相反:
| 触发点 | 谁在跑 | 缺 DATABASE_URL 时 |
|---|---|---|
| 5 分钟定时器 | scripts/sync_daily_log_outputs.py 的第三个目的地 |
警告 + 跳过,整体保持绿色 |
cc-connect /note |
worker 的 neon_projection job |
算失败:failure_summary 点名,三个仓库的 push 一起跳过 |
/note 那条路是有人在场的,绿着却看不到东西比红更难查,所以不加容错开关。代价是明确的:
一台拿来记笔记的机器,配好连接串是前置条件。另外,worker 那一步必须用 bash -lc 起 node——
cc-connect 进程的 PATH 里没有 nvm,裸 node 会落到系统的 Node 18,跑不动脚本里的 node:sqlite。
验收
一条只读命令对差,跑起来才有意义的是「差额都解释得清」:
python3 scripts/calendar_parity_report.py
2026-09-22 收工时(窗口 2025-01-01 起):
本地条目 = 377 日历 = 392 对上 = 376
对上方式: {'ledger': 376} (100% 走台账,不再需要按内容猜)
只在日历: 16(12 白名单 + 4 重复) 只在本地: 1(本地重复行)
时刻不一致: 19(全是旧脚本的 12:00 兜底)
id 级对不上、内容级对上: 0 没有解释的差额: 0
id 级对不上 从 65 归零,是另一张票的成果:老账里 65 条记录的飞书事件被重建过、id 换了,
一次性全修(有链式证据:日历描述里留着老账那条记录的同步键)。详见
notes/documents/日历条目 target_id 过期台账.md。
两处契约缺口的修(ticket 12,2026-09-22)
测这条链路时撞出两处「说好的行为 ≠ 实际行为」,当天都修掉了:
/note现在失败会非零退出。原先runNoteCLI无论成败都返回 nil,退出码恒 0;而 cc-connect 只在子进程报错时才把 stderr 递给用户——也就是静默失败。现在失败先照旧把完整 JSON 吐在 stdout, 再以 1 退出、在 stderr 写一行note sync failed: <失败的步骤名>。- 同步改成尽力而为。原先任一渠道失败会把三个仓库的提交推送一起闸掉;现在失败只记它自己一笔,
别的渠道和本地提交照常走,并逐个点名(措辞是
incomplete而不是failed——本地的记录确实落地了)。 实测:把 Neon 那一路弄挂,飞书两条线与博客照常成功,三个仓库 HEAD 都往前走,退出码 1 只报那一句; 那条笔记在台账里仍是「未投影」,下一次投影归到missing补发。 dry_run透传了。原先 stdin 里写"dry_run": true会被默默忽略、照真实执行(我就是这么被骗了一次)。 现在它一路传到捕获与四个目的地:不写库、不写日志、不推飞书、不投影、不提交不推送。 注意演练不会因为缺DATABASE_URL报错——投影脚本的 dry-run 分支在检查连接串之前就返回了。
相关文档
- 决定:
docs/adr/0010(日历条目是网页与飞书日历的共享定义)、web/docs/adr/0011(日程走数据库)。 - 运维:
web/docs/operations.md的「日程投影」章节(含两个触发点的对照表)。 - 链路:
personal_assistant/plugins/personal-assistant-sync/README.md。