两句话, 四张图, codex 就给我实现出了一个计时app(可运行,可打包)

codex 需要提前安装 superpowers

代码已开源 https://cnb.cool/abigmiu/mTimer

无法通知, 应该是我没有开发者账号的原因

  1. 让 codex 完善需求文档
    用户只需要写简单的几句需求,确定大体方向, 然后告诉codex 让他自己完善需求, 期间, codex 会根据用户写的简要需求进行详细补充。
    用户自己写的需求

第一句话

  1. 确认执行
    确认执行

这一句输完之后, codex 全自动运行, 直接出来了一个可打包, 可运行的app。 app 大小仅500 KB。

================================

包括 下面的ai博客,一个 266 次调用,实在是amazing

从需求截图到可运行的 macOS 菜单栏倒计时:mTimer 的一次 TDD 实战

这次做的是一个 macOS 11+ 原生菜单栏倒计时应用:常驻菜单栏,预设时长一键启动,计时中用圆环图标表示进度;支持结束前提醒与结束提醒(通知 + 可开关提示音);支持开机自启动;应用异常退出/重启后能恢复运行中的倒计时,并在 endAt 到点提醒。

本文记录一次从「截图需求」到「可打包 .app」的完整落地过程,重点放在:需求收敛、核心模型的 TDD、系统集成边界、以及打包/自启动的工程化细节。


1. 需求收敛:先把“看起来像”变成“能验收”

输入是 demand.md:1 加 4 张截图:主界面(预设列表 + 新增/自定义)、通用设置(自启动/结束前提醒)、菜单栏菜单(快速选择 + 更多)、计时中图标(圆环)。

1.1 需求截图

计时器设置页:预设列表与新增入口

图 1:计时器设置页(预设列表 + 快捷新增 + 自定义入口)。

通用设置页:开机自启动与结束前提醒

图 2:通用设置页(开机自启动、计时结束前提醒、提前 N 分钟)。

菜单栏菜单:快速选择预设与更多菜单

图 3:菜单栏菜单(预设快速选择 + “更多”子菜单入口)。

计时中的菜单栏图标:圆环进度

图 4:计时中菜单栏图标(用圆环表示时间进度)。

我做的第一件事不是写代码,而是把这些描述整理成结构化 PRD(demand.md:1):

  • 明确范围(MVP):只做单倒计时,不做多计时器并行,不做账号系统(截图里有“账户”也只是占位)。
  • 行为可验收:例如“重复预设阻止并提示且高亮已有项”、“预设支持拖拽排序并持久化”、“异常退出后仍能到点提醒”等。
  • 避免猜测:把不确定项变成“待确认”,逐个确认后再固化到需求里。

这一步的价值是:后续任何实现分歧都能回到同一套验收标准,而不是“我以为你要这样”。


2. 关键技术决策:KISS + 可测试性优先

为了让需求落地简单可靠,我选择了下面的组合:

2.1 工程结构:SwiftPM + 核心模块解耦

用 SwiftPM 建一个三 target 工程(Package.swift:1):

  • mTimerCore:纯领域逻辑(可 TDD、可单测)
  • mTimer:AppKit 菜单栏应用(NSStatusItem、偏好设置、通知调度、持久化)
  • mTimerLoginHelper:Login Helper(开机自启动拉起主应用)

这符合 SOLID 的“依赖倒置”:UI/系统 API 是细节,核心逻辑是稳定边界。

2.2 计时模型:用 endAt 驱动,拒绝“tick 漂移”

倒计时如果靠每秒 -1,在睡眠/唤醒或主线程卡顿时会漂移。于是核心模型 ActiveTimer 采用:

  • startAt + duration 推导 endAt
  • remaining(at:)progress(at:) 基于 endAt / startAt 计算并 clamp

实现见 Sources/mTimerCore/ActiveTimer.swift:1

2.3 到点提醒:用系统通知计划兜底进程退出

“异常退出/重启仍能到点提醒”意味着不能只依赖进程内 timer。做法是:

  • 启动倒计时后 创建 UNNotificationRequest(结束提醒、可选结束前提醒)
  • 偏好变更(提示音/结束前提醒)后 取消并重建 未触发的通知计划

这样就算应用崩溃,通知依旧能在系统层触发。


3. 用 TDD 写核心:先证明行为,再写实现

我在核心层严格按 Red → Green 做了三块:

3.1 ActiveTimer:剩余时间与进度的边界

测试从行为开始(Tests/mTimerCoreTests/ActiveTimerTests.swift:1),覆盖:

  • remaining 不得为负
  • progress 必须在 0~1

实现最小化(Sources/mTimerCore/ActiveTimer.swift:1),只做计算与 clamp,不掺杂 UI/通知逻辑。

3.2 NotificationPlanner:把“通知需求”变成纯数据

通知规划被抽成纯数据 NotificationPlan,再由 UI 层去 schedule。

测试(Tests/mTimerCoreTests/NotificationPlannerTests.swift:1)覆盖:

  • 必有结束提醒
  • 结束前提醒只在 duration > leadMinutes 时生成
  • 提示音开关影响 sound 字段

实现(Sources/mTimerCore/NotificationPlanner.swift:1)保持纯函数,方便验证。

3.3 TimerManager:启动/替换/恢复/重排通知

计时业务语义集中在 TimerManagerSources/mTimerCore/TimerManager.swift:1):

  • start:若已有倒计时,先 cancel 旧通知,再持久化新 timer 并 schedule 新通知
  • restoreIfNeeded:未结束则 cancel+schedule(防止陈旧计划),已结束则清理
  • stop / rescheduleNotifications:提供明确的生命周期动作

配套测试(Tests/mTimerCoreTests/TimerManagerTests.swift:1)把“替换会清理旧通知”“恢复会先取消旧计划”这类细节锁死,避免回归。


4. AppKit 层落地:菜单栏、设置、持久化、通知调度

4.1 菜单栏与圆环图标

StatusItemControllerSources/mTimer/StatusItemController.swift:1)负责:

  • 预设列表:按用户自定义顺序展示,点击即启动/替换
  • “更多”:偏好设置、反馈、退出(评分入口隐藏)
  • 运行中状态:菜单顶部显示剩余时间;图标用圆环进度更新

圆环渲染在 IconRendererSources/mTimer/IconRenderer.swift:1),用 NSBezierPath 画底圈与进度弧线,设置 isTemplate = true 适配浅色/深色菜单栏。

4.2 偏好设置窗口(NSTabViewController toolbar 样式)

PreferencesWindowController + PreferencesTabViewControllerSources/mTimer/PreferencesWindowController.swift:1)实现三页签:

  • 通用:开机自启、结束前提醒(开关 + N 分钟)、提示音开关、通知权限提示、版本号
  • 计时器:预设列表(增删/自定义输入/拖拽排序/重复提示)
  • 账户:占位说明(YAGNI)

预设管理用 NSTableView + drag & drop(Sources/mTimer/Preferences/TimersViewController.swift:1),排序变更立刻写回 UserDefaults 并通知菜单栏刷新。

4.3 持久化与恢复

  • 偏好:PreferencesStoreSources/mTimer/PreferencesStore.swift:1)统一读写,变更通过通知广播。
  • 运行中 timer:UserDefaultsActiveTimerStoreSources/mTimer/UserDefaultsActiveTimerStore.swift:1)用 JSON 持久化 ActiveTimer
  • 应用启动时恢复:TimerService.restoreIfNeededSources/mTimer/TimerService.swift:1)调用 core 的 TimerManager.restoreIfNeeded 并启动 UI tick(用于圆环刷新)。

4.4 通知调度

UserNotificationSchedulerSources/mTimer/UserNotificationScheduler.swift:1)把 core 的 NotificationPlan 转成 UNNotificationRequest,并按 identifier 支持取消。


5. 开机自启动:Login Helper + ServiceManagement

macOS 11 下依旧可以通过 SMLoginItemSetEnabled 启用一个 Login Helper:

  • 主应用:LaunchAtLoginControllerSources/mTimer/LaunchAtLoginController.swift:1
  • Helper:Sources/mTimerLoginHelper/main.swift:1 启动后拉起主应用并退出

注意:SwiftPM 产物不是天然 .app,需要打包时把 helper 放进主应用 Contents/Library/LoginItems/,这也是自启动能生效的关键。


6. 打包:从 SwiftPM 二进制组装成可双击运行的 .app

用脚本 scripts/build-app.sh:1 做三件事:

  1. swift build -c release
  2. 组装 build/mTimer-*.app(复制可执行文件 + Info.plist
  3. 内嵌 mTimerLoginHelper.app

可用命令:

1
2
swift test
scripts/build-app.sh

如果你要配置“反馈…”跳转地址:

  • 方式 A:运行时环境变量 MTIMER_FEEDBACK_URL
  • 方式 B:打包 Info.plistMTimerFeedbackURLpackaging/mTimer-Info.plist:1

7. 回顾:这次开发的几个关键经验

  1. 先写可验收需求,会显著降低“实现完成但不满足预期”的概率。
  2. 把通知规划做成纯数据,既能 TDD,又能让 UI 层只做系统集成。
  3. 用 endAt 驱动计时,睡眠/唤醒与卡顿场景自然正确。
  4. SwiftPM 做 macOS App 没问题,但要补一个打包步骤,尤其是 Login Helper。
  5. TDD 对这类系统集成项目依旧有价值:核心行为稳定后,UI 只是“把结果展示出来”。

如果你接下来要发布给真实用户,下一步通常是:签名/公证、崩溃上报、以及更完整的手工走查用例(通知权限、首次启动体验等)。