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

两句话, 四张图, codex 就给我实现出了一个计时app(可运行,可打包)
aelaxcodex 需要提前安装 superpowers
无法通知, 应该是我没有开发者账号的原因
- 让 codex 完善需求文档
用户只需要写简单的几句需求,确定大体方向, 然后告诉codex 让他自己完善需求, 期间, codex 会根据用户写的简要需求进行详细补充。
- 确认执行
这一句输完之后, 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推导endAtremaining(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:启动/替换/恢复/重排通知
计时业务语义集中在 TimerManager(Sources/mTimerCore/TimerManager.swift:1):
start:若已有倒计时,先 cancel 旧通知,再持久化新 timer 并 schedule 新通知restoreIfNeeded:未结束则 cancel+schedule(防止陈旧计划),已结束则清理stop/rescheduleNotifications:提供明确的生命周期动作
配套测试(Tests/mTimerCoreTests/TimerManagerTests.swift:1)把“替换会清理旧通知”“恢复会先取消旧计划”这类细节锁死,避免回归。
4. AppKit 层落地:菜单栏、设置、持久化、通知调度
4.1 菜单栏与圆环图标
StatusItemController(Sources/mTimer/StatusItemController.swift:1)负责:
- 预设列表:按用户自定义顺序展示,点击即启动/替换
- “更多”:偏好设置、反馈、退出(评分入口隐藏)
- 运行中状态:菜单顶部显示剩余时间;图标用圆环进度更新
圆环渲染在 IconRenderer(Sources/mTimer/IconRenderer.swift:1),用 NSBezierPath 画底圈与进度弧线,设置 isTemplate = true 适配浅色/深色菜单栏。
4.2 偏好设置窗口(NSTabViewController toolbar 样式)
PreferencesWindowController + PreferencesTabViewController(Sources/mTimer/PreferencesWindowController.swift:1)实现三页签:
- 通用:开机自启、结束前提醒(开关 + N 分钟)、提示音开关、通知权限提示、版本号
- 计时器:预设列表(增删/自定义输入/拖拽排序/重复提示)
- 账户:占位说明(YAGNI)
预设管理用 NSTableView + drag & drop(Sources/mTimer/Preferences/TimersViewController.swift:1),排序变更立刻写回 UserDefaults 并通知菜单栏刷新。
4.3 持久化与恢复
- 偏好:
PreferencesStore(Sources/mTimer/PreferencesStore.swift:1)统一读写,变更通过通知广播。 - 运行中 timer:
UserDefaultsActiveTimerStore(Sources/mTimer/UserDefaultsActiveTimerStore.swift:1)用 JSON 持久化ActiveTimer。 - 应用启动时恢复:
TimerService.restoreIfNeeded(Sources/mTimer/TimerService.swift:1)调用 core 的TimerManager.restoreIfNeeded并启动 UI tick(用于圆环刷新)。
4.4 通知调度
UserNotificationScheduler(Sources/mTimer/UserNotificationScheduler.swift:1)把 core 的 NotificationPlan 转成 UNNotificationRequest,并按 identifier 支持取消。
5. 开机自启动:Login Helper + ServiceManagement
macOS 11 下依旧可以通过 SMLoginItemSetEnabled 启用一个 Login Helper:
- 主应用:
LaunchAtLoginController(Sources/mTimer/LaunchAtLoginController.swift:1) - Helper:
Sources/mTimerLoginHelper/main.swift:1启动后拉起主应用并退出
注意:SwiftPM 产物不是天然 .app,需要打包时把 helper 放进主应用 Contents/Library/LoginItems/,这也是自启动能生效的关键。
6. 打包:从 SwiftPM 二进制组装成可双击运行的 .app
用脚本 scripts/build-app.sh:1 做三件事:
swift build -c release- 组装
build/mTimer-*.app(复制可执行文件 +Info.plist) - 内嵌
mTimerLoginHelper.app
可用命令:
1 | swift test |
如果你要配置“反馈…”跳转地址:
- 方式 A:运行时环境变量
MTIMER_FEEDBACK_URL - 方式 B:打包
Info.plist填MTimerFeedbackURL(packaging/mTimer-Info.plist:1)
7. 回顾:这次开发的几个关键经验
- 先写可验收需求,会显著降低“实现完成但不满足预期”的概率。
- 把通知规划做成纯数据,既能 TDD,又能让 UI 层只做系统集成。
- 用 endAt 驱动计时,睡眠/唤醒与卡顿场景自然正确。
- SwiftPM 做 macOS App 没问题,但要补一个打包步骤,尤其是 Login Helper。
- TDD 对这类系统集成项目依旧有价值:核心行为稳定后,UI 只是“把结果展示出来”。
如果你接下来要发布给真实用户,下一步通常是:签名/公证、崩溃上报、以及更完整的手工走查用例(通知权限、首次启动体验等)。













