上一篇 Mac 下用 Git 提交自动生成 Obsidian 工作日报 解决了「做过什么没有留下记录」的问题:Git 提交自动进入每日工作项,人工栏目再补上目标、进展、风险和非代码产出。
但任务写进 Obsidian,并不代表我会按时看到它。文件不会主动弹窗,Obsidian 关闭后更不会持续催办。真正需要处理的问题变成了:如何继续让 Vault 管任务,同时借用 Apple 提醒事项负责系统级通知?
我的最终方案不是把两套任务系统完全对等地同步,而是把 Apple 提醒事项当作 Obsidian 的通知投影:标题、时间和优先级以 Vault 为准,完成状态允许两边合并。
先确定边界:谁才是任务的事实源
双向同步最危险的地方,是两边都能随意修改所有字段。标题冲突、时间覆盖和删除传播很快就会变成无法解释的状态。
所以这套方案先规定字段所有权:
| 字段或动作 | 权威来源 | 同步规则 |
|---|---|---|
| 任务标题 | Obsidian | 每次同步覆盖提醒事项中的标题 |
| 提醒时间 | Obsidian | 每次同步覆盖到期时间和闹钟 |
| 优先级 | Obsidian | P0、P1、P2 映射到高、中、低 |
| 完成状态 | 两端 | 任一端完成,另一端也完成 |
| 重新打开 | 不自动处理 | 已完成任务不会被另一端重新打开 |
| 删除 | 不自动传播 | 源任务消失时不会自动删除提醒事项 |
Vault 保存任务语义,Apple 提醒事项负责提供 macOS 系统通知;只有提醒列表属于 iCloud 账户时,任务才会继续同步到登录同一账户的 iPhone 等设备。
脚本也不会扫描整个 Vault。它只读取两个明确位置:
- 目标日报
## 今日目标中带#提醒的任务;定时任务会把范围扩展为当天及此前 7 天; work/近期规划/近期重点.md的## 重点任务中带#提醒的任务。
工作进展、风险和 Git 提交正文不会被复制到提醒事项。但任务标题、优先级、到期时间、Vault 名称、源文件路径、行号和 Obsidian URL 会写入提醒或备注;如果列表位于 iCloud,这些元数据也会进入 iCloud。
一条可同步的任务长什么样
推荐格式如下:
这一行包含三个同步标记:
#提醒表示明确允许这条任务进入 Apple 提醒事项;2026-08-06 09:30是本地提醒时间;^rem-a81f是稳定块 ID,用来识别「它还是原来的那条任务」。
稳定 ID 比任务标题更重要。任务可能改名,也可能被 $work-daily 迁移到新日报;只要 ID 保持不变,EventKit 助手就会更新原提醒,而不是重复创建一条。
优先级映射如下:
| Obsidian 标记 | Apple 提醒事项 |
|---|---|
P0 | 高 |
P1 | 中 |
P2 | 低 |
P3 或不写 | 无 |
旧格式 #提醒 [remind:: 2026-08-06T09:30:00+08:00] ^rem-a81f 仍然兼容,但新格式更容易直接阅读,也不容易把 Dataview 内联字段写坏。
不要手写稳定 ID:交给 Templater
真正容易出错的不是 #提醒,而是它后面的时间和块 ID。少一个空格、日期不存在或误写其他块 ID,都会让同步行为变得不确定。
因此我增加了一个 Templater 命令:Add or Update Reminder。
使用方式:
- 把光标放在日报「今日目标」或近期重点「重点任务」的非空任务行上;
- 打开命令面板,运行
Templater: Add or Update Reminder; - 输入
明天 09:00或2026-08-06 09:00; - 模板写入绝对时间,并创建或复用
^rem-...稳定 ID。
模板会拒绝错误文件、错误区块、空任务、非法时间以及已存在其他块 ID 的任务。已有提醒再次运行命令时,只更新时间,不会更换 ID。
核心逻辑可以概括为:
手工输入 #提醒 明天 09:00 也能工作,但默认 dry-run 会报告缺少稳定 ID。第一次实际执行 --sync 时,脚本会先把相对日期固化为绝对日期并补齐 ID,再继续校验和调用 EventKit。
这意味着,即使后续校验、权限请求或 EventKit 写入失败,前面的格式规范化仍可能已经写回 Vault。更稳妥的做法是始终先用 Templater 生成完整格式,让 dry-run 在任何外部写入前通过。
整体数据流
Python 脚本负责读取 Markdown、限制允许的区块、解析时间、清理标题、检查重复 ID,并生成交给 Swift 助手的 JSON。
Swift 助手使用 EventKit 读写提醒事项。Apple 的 EventKit 提供日历和提醒事项的数据访问能力;这里使用 EKReminder 保存任务,并用绝对时间的 EKAlarm 安排通知。EventKit 概览、EKAlarm.absoluteDate
这条链路不依赖快捷指令,也不要求 Obsidian 一直打开。真正执行系统写入的是按需编译的本机 Swift 程序。
运行前准备
下面所有相对路径都以 Vault 根目录为起点。先进入自己的 Vault,并确认基础工具可用:
Python 至少需要 3.9,因为脚本使用 zoneinfo 和现代类型语法。Swift 助手需要 swiftc、EventKit SDK 与系统自带的 codesign;如果 xcrun --find swiftc 失败,需要先安装 Xcode Command Line Tools 或完整 Xcode。
提醒事项完整访问 API 还要求当前 macOS 和编译 SDK 支持 requestFullAccessToReminders。本文描述的是当前这台 Mac 已验证通过的环境,不是面向旧版 macOS 的兼容实现。
为什么要用 EventKit 助手
脚本目录包含 eventkit_helper.swift。首次使用时,它会编译到被 Git 忽略的缓存目录,并使用固定标识 com.frevia.obsidian-reminders 做本机 ad-hoc 签名。
先构建并查询权限状态:
常见状态包括 notDetermined、fullAccess 和 denied。只有第一次实际同步才会触发 macOS 权限请求;--status 不请求权限,也不写提醒事项,但助手缺失或过期时可能先在缓存目录重新编译并签名。
由于脚本既要写入任务,也要读取用户是否在提醒事项中完成了任务,所以需要提醒事项的完整访问权限。Apple 提供的 requestFullAccessToReminders 正是用于请求提醒事项读写访问。Apple API 文档
EventKit 助手会创建或复用名为 Obsidian 工作 的列表。若列表不存在,它会沿用系统“新提醒的默认列表”所属账户创建,而不是强制选择 iCloud;若多个账户存在同名列表,当前实现会复用 EventKit 返回的第一项。
因此,第一次同步后应在提醒事项中确认 Obsidian 工作 位于预期账户。若需要跨设备通知,应先把系统默认提醒列表设为 iCloud,避免不同账户出现同名列表,再创建或迁移这张专用列表。
每条提醒的备注中都保存:
obsidian-task-id 用于幂等更新,Obsidian URL 则可以直接跳回源任务。脚本不会依赖 Apple 内部生成的标题或列表顺序来匹配任务。
先 dry-run,再允许写入
默认命令只打印 JSON,不会访问或修改 Apple 提醒事项:
如果只想检查指定日报,不读取近期重点:
存在缺少时间、缺少 ID、重复 ID 或目标区块不存在时,dry-run 会以退出码 2 结束。先让这一步通过,比直接在提醒事项里清理重复任务安全得多。
--sync不是纯粹的“校验通过后再写入” 它会先规范化相对日期并补稳定 ID,再重新解析并调用 EventKit。后续步骤失败时,这部分 Vault 修改不会自动回滚;优先使用 Templater,让 dry-run 先得到完整、可验证的任务。
确认 JSON 后再实际同步:
--apply 与 --sync 当前等价,都会写入 Apple 提醒事项并合并完成状态。使用 --sync 只是让命令意图更清楚。
双向同步只合并完成状态
同步时,助手会比较 Vault 复选框和 Apple 提醒事项的完成状态:
只要任一端已经完成,结果就是完成。若完成发生在 Apple 提醒事项,Python 脚本只会在允许的源区块中定位同一 ^rem-...,把 [ ] 改为 [x]。
它不会反向修改标题、时间或优先级,也不会触碰 Git 自动区块。任务完成后,后续催办闹钟会被移除。
这个合并规则刻意不支持自动重新打开。否则某台设备上的旧状态可能把已经完成的任务重新变成待办。确实需要重做时,我会新建任务,或者手动在两边处理。
怎样让未完成任务反复通知
Apple 提醒事项通常只在设定时间提醒一次,但我的主要目标是:任务没完成,就继续催办。
每次同步会为未完成任务安排一组绝对时间闹钟:
- 任务尚未到期:第一次通知就是到期时间;
- 任务已经逾期:从原到期时间按固定间隔计算下一个未来时间;
- 默认每 30 分钟一次,共安排 8 次;
- 任务完成后移除剩余闹钟。
临时调整频率和次数:
后台同步每 15 分钟执行一次,但不会在每次运行后简单地从「现在」重新计时。逾期任务始终沿原到期时间的固定间隔计算,因此刷新频率不会把通知频率意外翻倍。
如果提醒已经存在却没有弹出系统通知,还要检查 macOS 对「提醒事项」的通知权限、专注模式和勿扰设置。数据同步成功与系统是否展示横幅是两层不同的问题。
用 launchd 每 15 分钟同步
受版本管理的 LaunchAgent 使用 Label com.frevia.obsidian-reminders,登录后启动,并每 900 秒运行一次:
之所以扫描最近 7 天日报,是因为任务可能跨日迁移。同一个稳定 ID 出现在多日日报时,脚本采用最新日期中的任务,完成状态也只回写最新位置。
安装或恢复 LaunchAgent:
检查运行状态和日志:
卸载:
当前安装脚本和 plist 包含 Vault 的本机绝对路径。换电脑或分享给别人时,必须先替换 Vault 根目录、LaunchAgents 目标路径和日志路径,再加载任务。
一天怎样使用
- 早上通过
$work-daily创建工作记录,确认「今日目标」。 - 对真正需要系统通知的任务运行
Add or Update Reminder。 - 先用 dry-run 检查解析结果。
- LaunchAgent 每 15 分钟同步提醒与完成状态。
- 在 Obsidian 或 Apple 提醒事项任一端完成任务。
- 下次同步后,两端都会变成完成,剩余通知也会被移除。
并不是所有任务都要加 #提醒。如果每条任务都反复通知,通知很快会变成新的背景噪音。我只给有明确时间窗口、等待反馈或容易遗忘的任务加提醒。
故障排查
| 现象 | 优先检查 |
|---|---|
| dry-run 报缺少时间 | 使用 Templater 命令重新生成提醒格式 |
| dry-run 报缺少稳定 ID | 运行 Templater 命令,或首次执行实际 --sync 自动补齐 |
--status 为 denied | 在 macOS 隐私设置中重新允许提醒事项访问 |
| 提醒事项中出现重复任务 | 检查重复块 ID,以及是否手工复制后修改过 ID |
| 修改标题后出现新提醒 | 检查迁移或改名时是否保留原 ^rem-... |
| Apple 端完成后 Vault 没变化 | 查看同步日志,确认源任务仍在允许区块且 ID 唯一 |
| 同步成功但没有通知 | 检查提醒事项通知权限、专注模式与闹钟时间 |
| LaunchAgent 没运行 | 用 launchctl print 检查状态,再看两份 launchd 日志 |
删除 #提醒 后 Apple 端仍存在 | 当前不自动传播删除,需要手工删除已有提醒 |
验证脚本、Swift 助手和权限:
和 Git 日报自动化组合起来
Git 自动化回答「今天实际提交了什么」,提醒事项同步回答「哪些任务到了时间还没有完成」。两者都写进工作系统,但职责完全不同:
| 自动化 | 输入 | 输出 | 不负责什么 |
|---|---|---|---|
| git-work-log | 白名单仓库中的 Git 提交 | 日报 Git 区块、仓库动态 | 不判断优先级和完成结论 |
| reminders sync | 显式标记的工作任务 | Apple 提醒事项、完成状态回写 | 不复制 Git 正文和工作上下文 |
两条链路共同遵守一个原则:Obsidian Vault 是事实源,外部系统只是有边界的投影。
