想做护眼工具却脑子一片空白?我用 OpenSpec 把模糊想法聊成了 v0.1

一、前言导读:除了"我想做",其他都是空白

在做这个项目时,我心里只有一个想法:

我必须每天面对电脑,干眼越来越严重,只能靠眼药水和热敷进行辅助缓解。但忙起来我总会忘记。通过手机定闹钟我又觉得太麻烦了------掏手机、解锁、点掉提醒、回头继续写代码,整个流程太重。所以我决定开发一款适合我的护眼提醒工具。

就这一句。后面所有的 proposal / design / specs / tasks 都是从这一句开始聊出来的。

没有界面草图,没有功能清单,没有技术选型,没有架构图。

如果那天 AI 让我「先写代码」,结局大概是这样的:

  • AI 给我一个 Electron + React + Material UI 的庞然大物,5MB 装包变 80MB
  • 主界面塞满色温滑块、亮度调节、休息时长、休息音乐
  • 装上第一次启动,三个提醒一齐弹,我立刻卸载它自己写的工具

「想做个产品」与「产品从 0 到 1」之间的鸿沟,技术从来不是问题,思考才是

这篇文章我想讲一件具体的事:我怎么用 OpenSpec(spec-driven 工作流)从一个模糊想法聊到 v0.1 发布,中间经历过哪些真实决策。


二、最开始的 1 小时:脑子里到底有多空白

回放一下我打开项目时的真实状态。

2.1 界面:完全没概念

我想要的 UI 是什么?

  • 要几个窗口?不知道
  • 弹窗长什么样?不知道
  • 主界面要不要显示今日数据?不知道
  • 配色?不知道

我唯一确定的是「装上就忘」这四个字。意味着 UI 存在感要低。但这只是个形容词,不是设计方案。

2.2 核心功能:想到啥算啥

我想要的功能是什么?一锅粥。

  • 20-20-20 提醒(听说过,但眼睛已经干,预防不解决当下问题)
  • 滴眼药水提醒(自己需要,每天 1-2 次,忙起来会忘)
  • 热敷提醒(自己需要,每天至少 1 次,同样会忘)
  • 屏幕使用时长(听说护眼软件都有)
  • 色温调节(听说过 f.lux)
  • 蓝光过滤(听说过)
  • 休息音乐 / 白噪声(听说过)

问题是我自己也没分清:市面软件大多在帮眼睛「少用」,但我需要的是「别忘了用」------滴眼药水、热敷,这些治疗动作到了时间必须有人提醒我执行。到底哪些是真需求,哪些是市面软件让我以为我需要?我分不清。

2.3 技术架构:连问题都不会问

我应该用什么架构?问不出来。

  • 单进程?多进程?
  • 提醒调度跑在前端还是后端?
  • 屏幕状态怎么感知?
  • 状态保存在哪?
  • 多窗口之间怎么通信?

最可怕的是,我连「应该问什么问题」都不知道。

2.4 技术栈:随大流

我应该用什么技术栈?没研究过。

Electron?Tauri?Flutter?Wails?Qt?这些名字我能背出来,但每个名字背后是什么生态,我一无所知。

如果你和我当时一样,这篇文章就是为你写的。


三、解法:OpenSpec 是 spec-driven 工作流,我负责回答和审阅

OpenSpec 在我眼里就是一套 spec-driven 工作流 :模板 + 命令 + AI 协作。默认提供 4 个核心命令(explore / propose / apply / archive)。其中 opsx:explore 走对话(它问,我答),其他 3 个 命令(propose/apply/archive)走文档与任务管理。

角色 干什么
工作流工具 OpenSpec 走对话(explore)+ 模板填充(propose)+ 任务追踪(apply)+ 归档(archive)等命令协作
需求方(我) 回答问题,把需求和范围聊明白
审阅人(我) 打开 4 个文件,确认内容和我理解的一致
代码执行者 AI 规格审阅通过后,按 task 顺序写实现

这篇文章我重点讲我自己的如何使用OpenSpec :回答它的提问、审阅它生成的文件。

我实际跑的 OpenSpec 顺序是这样的(4 个核心命令):

markdown 复制代码
1. opsx:explore  ──  探索模式,OpenSpec 反问我 8 个问题,我把回答整理成思路
2. opsx:propose  ──  一条命令同时生成 proposal + design + specs + tasks 4 个文件
                   (仓库里看到的 openspec/changes/archive/2026-07-20-add-mumu-eye-care/ 就是这次产出的归档)
3. opsx:apply    ──  4 个文件都审过之后,AI 按 task 顺序写代码
4. opsx:archive  ──  实施完成后归档,保留审计轨迹

下面我按这个顺序讲。每一步展示「我原来在想什么 → OpenSpec 问了我什么 → 我回答了什么 → 它生成了什么文件 → 我怎么审阅」。


四、Step 1:探索模式------「我到底想要啥」

4.1 我输入的 prompt

bash 复制代码
opsx:explore "我想做一款护眼工具"

4.2 AI 反过来问我的 8 个问题

OpenSpec 的 explore 模式不是直接给我方案,是反问。我把回答贴出来,因为我后来发现这 8 个问题就是后面所有文档的事实基础。

  1. 你的目标用户是谁?我自己,干眼患者。这决定了「专业术语不能用」。
  2. 它要解决的核心问题是什么?我需要的是每天按时滴眼药水(1-2 次)、按时热敷(至少 1 次),把治疗动作执行到位。
  3. 哪些功能是必须的,哪些是 nice-to-have?20-20-20 + 滴眼药水必须,色温 nice-to-have。
  4. 你希望它的「存在感」是什么?存在感低,33MB 内存、70ms 启动。
  5. 触发场景是主动还是被动?被动提醒,绝不打断心流。
  6. 用户拒绝提醒的代价是什么?高。所以必须可一键暂停 30 分钟。
  7. 数据需要同步吗?不联网,所有数据本地。
  8. 失败时降级到哪?Win32 API 调用失败就退化为「按时间提醒」,别让功能挂。

4.3 我回答完后,OpenSpec 输出什么

一份结构化的探索纪要,把所有回答汇总:

复制代码
用户:干眼患者,每天 10h+ 电脑工作
核心:从「我忘了」变为「工具记得」
存在:低,常驻后台无感
必选:20-20-20 + 滴眼药水 + 热敷
可选:色温调节(v0.2 再做)
触发:被动提醒,绝不主动打断
暂停:一键 30 分钟 / 1 小时 / 到明早
数据:本地 SQLite,不联网
降级:Win32 API 失败 → 退化为纯时间提醒

这段思考纪要就是后续所有文档的事实基础。没有它,proposal 就是凭空捏造。


五、Step 2:proposal.md------「为什么做、做什么、影响什么」

5.1 proposal.md 是什么 + 我怎么审阅它

proposal.md 是 OpenSpec 在我回答完探索问题后,整理出的第一份正式文件。它有 4 段:

我需要确认它写的是不是我心里想的
Why 它把我回答的"痛苦"总结成一段话了吗?
What Changes 它把我列的功能改写成了动词开头的清单吗?
Impact 它把"做什么 / 不做什么 / 影响谁 / 用什么依赖"都列清了吗?
Capabilities 它把功能拆成了 reminders / statistics / ui / settings 4 个独立能力吗?

5.2 opsx:propose 生成的 proposal.md(节选自仓库)

markdown 复制代码
## Why
我是一名干眼症患者,每天对着电脑 10 小时以上。
我尝试过几款护眼软件,总有某个点让我想卸------弹窗太勤、关不掉、功能堆得太杂。
手机定闹钟又太重------掏手机、解锁、点掉提醒、回头继续写代码,整个流程太麻烦。

更让我难受的是:我经常忘记滴眼药水、忘记热敷眼罩。
市面软件大多在帮眼睛「少用」,但我需要的是「别忘了用」------按时把治疗动作执行到位。

## What Changes
- 新增 定时提醒 能力:20-20-20 法则默认每 20 分钟提醒一次
- 新增 眼药水/热敷提醒 能力:默认每 2 小时提醒一次眼药水、中午 13:00 提醒热敷
- 新增 屏幕使用时长统计 能力:算法是「亮屏时长 − 锁屏时长 − 30 分钟无操作」
- 新增 UI 界面 能力:极简单列主界面、右下角紧凑弹窗、动态眼睛托盘图标

## Impact
- 目标用户:干眼患者 / 长时间电脑工作者
- 新增依赖:前端 React 18+TS+Tailwind+Zustand+Radix;后端 Rust 1.78+ + Tauri 2 + tokio + rusqlite
- 运行时占用:常驻内存 < 20MB
- 不影响:不联网、不上传任何数据、不监听输入内容、不使用摄像头

5.3 我审阅 proposal 时关心的几件事

我原来空白 proposal 里我要重点确认的
做哪些功能 What Changes 列的能力清单,是不是我刚才说的那些
这功能值不值得做 Why 段是不是从我的痛苦出发,没把"听起来很酷"的功能塞进来
做出来用户敢用吗 Impact 段有没有写明「不影响什么」,建立信任的边界
模块怎么拆 Capabilities 段拆的是不是合理,能独立交付

如果 proposal 有任何一段写偏了,我会回去改我的回答,让 OpenSpec 重新生成,直到 4 段都和我理解的一致。


六、Step 3:design.md------「界面到底长啥样」

6.1 我原来空白到什么程度

打开 design.md 模板前,我脑子里只有:

  • 主界面「应该有个大数字显示今日时长」
  • 弹窗「应该有个倒计时」
  • 配色「应该温柔一点」

没了。

6.2 design.md 12 节:我审阅的内容

design.md 不是让你「画图」,是让你定规矩。沐目真实的设计规范(design.md)有 12 节:

  1. 设计理念:温暖治愈 + 极致克制
  2. 配色方案:米白 #FAF8F5 + 薄荷绿 #87A878 + 警示橙 #C8956D
  3. 字体规范:微软雅黑 / 苹方 / Noto Sans CJK SC
  4. 间距与圆角:4px 基础单位 / 圆角 4/8/12px
  5. 动效规范:入场 300ms / 退场 500ms
  6. 组件规范:滑块 / 单选 / 复选 / 按钮
  7. 弹窗详细规范:320×200 / 距离边缘 24px / z-index 1000
  8. 托盘图标规范:睁眼 / 闭眼
  9. 主界面规范:480×360 / 巨号数字 64px
  10. 设置窗口规范:800×600
  11. 文案规范:不用「您」、不用「色温 4500K」这种术语
  12. 待补充:V2+

6.3 一处 design 如何落到 Tailwind 配置

design.md 第二节写明主强调色是 #87A878(薄荷绿),警示色是 #C8956D(暖橙)。这段落到 mumu/tailwind.config.js

js 复制代码
module.exports = {
  theme: {
    extend: {
      colors: {
        primary:    '#87A878', // 薄荷绿 主强调色
        warning:    '#C8956D', // 暖橙 警示色
        bg:         '#FAF8F5', // 浅色模式主背景 米白
        'bg-card':  '#FFFFFF', // 卡片背景 纯白
        text:       '#2C2825', // 主文字 深棕近黑
        'text-sub': '#8B8378', // 副文字 灰棕
      },
    },
  },
};

色号到 className 的转换就这么简单。

6.4 我审阅 design.md 时关心的几件事

我原来的疑问 design.md 里我重点确认的
主色用什么 配色方案那一节,米白 + 薄荷绿是不是我想要的
弹窗多大 弹窗详细规范那一节,320×200、巨号 64px 是不是合适
倒计时动画多久 动效规范那一节,300ms / 500ms 节奏对不对
滑块最小值 组件规范那一节,15 分钟起是不是合理
文案怎么说 文案规范那一节,不用「您」、不用术语是不是够温柔

design.md 还要包含「不要做」。我那段写的是:

  • ❌ 不做呼吸动画(用户已确认)
  • ❌ 不做弹跳动画(幼稚感)
  • ❌ 不做粒子特效(干扰感)

明确边界比列举功能更重要。AI 看到「不做」就不会越界。


七、Step 4:specs/*.md------「提醒什么、多久、什么场景」

7.1 我原来空白到什么程度

我想做「提醒」,但:

  • 提醒什么?只知道 20-20-20
  • 多久提醒一次?默认 20 分钟,但能改吗?
  • 工作时段外要不要提醒?不知道
  • 锁屏时怎么办?不知道
  • 用户已经做了 5 次眼保健操,还要不要提醒?不知道

7.2 specs 14 个 Requirement:我审阅的内容

specs/reminders/spec.md 真实的 14 个 Requirement:

  1. Default reminder parameters:默认 09:00-18:00,间隔 20 分钟
  2. Configurable reminder parameters:间隔 15-60 分钟可调
  3. Reminder trigger conditions:6 个 AND 条件
  4. Reminder popup lifecycle:弹窗生命周期
  5. Countdown display:倒计时显示
  6. Fullscreen application handling:全屏应用不弹
  7. Lock screen handling:锁屏暂停 + 解锁续弹
  8. Shutdown and hibernation handling:关机不补弹
  9. Reminder pause:30 分钟 / 1 小时 / 到明早
  10. Rest count recording:计入每日统计
  11. Cross-period reminders:下班前 20 分钟静默
  12. Eye drop and warm compress reminders:眼药水 + 热敷
  13. Repeated eye drop dismissals:连续 3 次 dismiss 当日静默
  14. Wooden fish sound:木鱼音效规则

7.3 一条 Requirement 的标准写法

specs/*.md 不是散文,是有固定词法和结构的「行为合同」。我从 OpenSpec 那里学到的第一条规则:Requirement 必须用 SHALL/SHOULD/MAY 三个模态词,区分硬约束与软建议:

模态词 语义 用在什么场景
SHALL 必须做到 触发条件、边界处理、状态约束(强提醒必须在工作时段触发)
SHOULD 建议做到 性能、可用性建议(UI 启动应 < 1s)
MAY 可选做到 增强行为(弹窗出现时 MAY 播放木鱼音效)

我用 SHOULD 而不是 SHALL 的地方都是「做不到不影响功能,只是体验差」。

第二条规则:Scenario 用 WHEN / THEN 写具体动作 + 可观察结果,因为 AI 看 WHEN/THEN 能直接生成测试用例。WHEN 是输入条件,THEN 是输出可断言。

仓库里 specs/reminders/spec.md 一条 Requirement 长这样:

markdown 复制代码
### Requirement: Reminder trigger conditions
The system SHALL trigger a reminder only when ALL of the following are true:
current time is within configured work hours, time since last completed reminder
is at least the configured interval, user is not in a paused state, screen is on,
no fullscreen application active, and user is not locked.

#### Scenario: Lock during popup
- **WHEN** user locks the screen 10 seconds into a 20-second rest
- **THEN** the popup SHALL hide and the countdown SHALL pause

Scenario 例子(处理连续 dismiss 的边界):

markdown 复制代码
### Requirement: Eye drop and warm compress reminders (soft prompts)
#### Scenario: Repeated eye drop dismissals
- **WHEN** user dismisses the eye drop prompt 3 consecutive times
- **THEN** no further eye drop reminders SHALL appear until the next workday

7.4 我审阅 specs 时关心的几件事

我原来的疑问 specs 里我重点确认的 Requirement
工作时段外要不要提醒 Reminder trigger conditions 是不是把「不在工作时段 → 不提醒」写进去了
锁屏时弹窗怎么办 Lock screen handling 是不是「隐藏 + 暂停 + 续弹」
关机后要不要补弹 Shutdown and hibernation handling 是不是「不补,重启后按 last + interval 重算」
全屏 PPT 时弹不弹 Fullscreen application handling 是不是「不弹」
用户连续关掉 3 次怎么办 Repeated eye drop dismissals 是不是「当日静默」
下班前 20 分钟要不要静音 Cross-period reminders 是不是「静音弹窗」
倒计时数字多大 Countdown display 是不是 64px 粗体居中

7.5 我审阅 specs 的几条标准

我审 specs 时会按这 4 条标准对照:

  1. Requirement 必须用 SHALL/SHOULD/MAY 模态词,便于区分硬约束与软建议
  2. 每个 Requirement 至少 2 个 Scenario,正面 + 异常都要写
  3. WHEN / THEN 写具体数值。「20 秒」比「短时间」好,「3 次」比「几次」好
  4. 边界场景显式列出。「System muted」「Outside work hours」「Repeated dismissals」这类反例必须有

如果 specs 里某条 Requirement 没写边界,我会回去补我的回答,让 OpenSpec 重新生成。


八、Step 5:tasks.md------「架构怎么搭、技术栈怎么选」

8.1 我原来空白到什么程度

到了 tasks.md 这步,我面对的是:

  • 4 个窗口(主 / 强提醒 / 软提示 / 设置)怎么协同?
  • 提醒调度跑在 Rust 还是 React?
  • 屏幕状态怎么监听?
  • 设置存在哪?
  • 用什么打包?
  • 用什么框架?

这堆问题我连「应该问什么」都不知道。

8.2 tasks.md 是什么样的 + 我审阅什么

tasks.md 时,OpenSpec 模板会让我确认每个 task 写成下面这种结构:

markdown 复制代码
### T07 - 实现提醒调度器
**步骤**:
- 创建 `src-tauri/src/reminders.rs`
- 订阅 T06 的 `mpsc::Receiver<StatisticsEvent>` + 调度 `mpsc::Sender<ReminderCommand>`
- 强提醒判定(pure):工作时段 + 距上次 ≥ 间隔 + ...
- 软提示判定(pure):...

**故意没做**:
- ⏸ 不在 `lib.rs::run()` 里 `.spawn(Scheduler)`:等 T08 托盘菜单一起接入
- ⏸ 强提醒倒计时驱动:MVP 简化为前端 JS setInterval
- ⏸ 重启不补弹:`last_strong_at` 不持久化,重启后按启动时间重算

**验收**:`cargo test --lib reminders` 全部 17 个测试通过;总计 39 个测试全绿

我审 tasks 时最在意「故意没做」这一栏。它帮我确认「这一版不做什么」。没想清楚这一栏的 task,我会补完之后让 OpenSpec 重新生成。

8.3 我从 tasks 里看出技术栈是怎么定的

我看完 tasks 后回头看,发现技术栈选型是被 tasks 一个一个推出来的,不是拍脑袋。

task 暴露的需求 选型
T05 要调 Win32 API windows = "0.58"
T06 要 30s 轮询 + 状态机 tokio = "1"
T04 要本地数据库存统计 rusqlite = { version = "0.32", features = ["bundled"] }
T08 要系统托盘 → Tauri 2 已内置 tauri = { features = ["tray-icon", "image-png"] }
T11 要 React 设置页 react@19 + tailwindcss@3 + radix-ui
状态要在多窗口同步 zustand@4
客户端软件 + 存在感低 → 放弃 Electron 选 Tauri(2.85MB vs 80MB)
跨平台?→ MVP 只做 Windows(v0.2 再考虑 macOS) #[cfg(windows)] 守卫 Win32 调用

这就是「为什么是 Tauri」的真实推导路径。不是看了一篇「Tauri vs Electron」对比表就拍板,是 tasks 一个一个需求列出来后,倒推出来的选型。

8.4 真实 tasks.md 的 24 个 T(22 个实施 + 2 个学习/环境)+ 依赖图

markdown 复制代码
T00 ─► T00.5 ─► T01 ─┬─► T02 ─┬─► T13 ─┐
                     │        ├─► T14 ─┤
                     ├─► T03 ──┤        ├─► T15 ─┬─► T17 ─┬─► T22
                     ├─► T04 ──┤        │        ├─► T18 ─┤
                     ├─► T05 ──┼─► T06 ─┤        └─► T19 ─┤
                     │        └─► T07 ──┤                 ├─► T20 ─┘
                     ├─► T08 ──┤        │                 └─► T21 ─┘
                     │        ├─► T09 ──┤
                     │        ├─► T10 ──┼─► T10.5
                     │        └─► T11 ──┤
                     └─► T12 ──────────┘
                          │
                          └─► T16

6 个阶段、24 个 task(其中 T00 / T00.5 是学习 Rust + 跑通 Tauri Hello World 的准备 task,T01-T22 才是实施 task)、依赖图清晰、每个实施 task 都有「故意没做」。这张图本身就是架构图。

8.5 我审 tasks 时关心的几件事

我原来的疑问 tasks 里我重点确认的 task
提醒调度跑哪 T07 是不是写在 Rust 后端、跑 tokio 任务
屏幕状态怎么监听 T05 是不是调 Win32 API、30s 轮询
状态保存在哪 T03 JSON 存设置 + T04 SQLite 存统计,路径 %APPDATA%\沐目\ 对不对
多窗口怎么通信 T08/T10/T11 各窗口独立 entry + Tauri emit/invoke
用什么打包 T02 NSIS 配置对不对(中文、currentUser、uninstall hook)
性能怎么保 T18 性能验证:内存 < 50MB / CPU < 0.5% / 启动 < 1s

如果 tasks 里有我没想过的事(比如某个 task 用了我不熟的库),我会回去跟 OpenSpec 聊清楚,再让它重新生成。

写完 tasks 后,AI 只看到「这一小步」,且每步都有验收。AI 不再发散,因为它有明确边界,而这个边界是我跟 OpenSpec 一条一条聊清楚的。


九、Step 6:opsx:apply------开始写代码

4 类文档(proposal / design / specs / tasks)都填完后,写代码反而是最简单的部分。

我让 AI 按 task 顺序写实现,规则是:

  • 每个 task 的 pure 函数由 AI 写(should_trigger_strong、compute_color 等)
  • 每个 task 的异步外壳由 AI 写,人 review(tokio::spawn / tauri::command)
  • 每个 task 完成后跑验收命令

写代码的过程反而没什么好讲。该想的都在前面想清楚了。

下面挑 3 个最典型的实现(提醒调度、屏幕状态、托盘),展示 spec 到代码的逐字落地。


十、实操:spec 到代码的逐字落地

10.1 reminders/spec.md → reminders.rs(1558 行)

spec Requirement reminders.rs 实现位置 行数
Default reminder parameters default_settings in settings.rs 36-55
Reminder trigger conditions should_trigger_strong 319-368
Lock screen handling apply_pause + should_trigger_strong::ResumeActive 322-334, 556-569
Reminder pause apply_manual_pause / apply_pause_until_tomorrow 539-553
Rest count recording apply_complete_strongdb::increment_rest_count 486-509
Cross-period reminders mute_sound = (end_secs - now_secs).abs() <= 20*60 363-365
Eye drop and warm compress reminders should_trigger_eye_drop / should_trigger_warm_compress 382-464
Repeated eye drop dismissals SOFT_DISMISS_LIMIT = 3 + purge_old_dismissals 38, 519-524
Wooden fish sound ReminderCommand::ShowStrongReminder { play_sound } 50-56

10.2 真实代码:spec "Reminder trigger conditions" 落地

spec 里那条「trigger only when ALL of the following are true」有 6 个 AND 条件,AI 翻译成 Rust 时写成 5 个并列的早 return(active 续弹判定 + 工作时段 + 未暂停 + 距上次间隔 + 跨时段静默)。每个 return 都是 StrongDecision::NotTrigger,全过才走最后的 Trigger

spec.md 写法(specs/reminders/spec.md:1-12):

markdown 复制代码
The system SHALL trigger a reminder only when ALL of the following are true:
current time is within configured work hours, time since last completed reminder
is at least the configured interval, user is not in a paused state, screen is on,
no fullscreen application is active, and user is not locked.

落到 src-tauri/src/reminders.rs:319-368

rust 复制代码
pub fn should_trigger_strong(
    state: &ReminderState,
    settings: &Settings,
    now: DateTime<Local>,
) -> StrongDecision {
    // 续弹判定(spec: "Lock screen handling")
    if let Some(active) = &state.active_strong {
        if active.hidden {
            return StrongDecision::ResumeActive(active.clone());
        }
        return StrongDecision::NotTrigger;
    }
    // 工作时段(spec: "current time is within configured work hours")
    let now_time = now.time();
    let start = parse_hhmm(&settings.reminders.work_start);
    let end   = parse_hhmm(&settings.reminders.work_end);
    if !within_work_hours(now_time, start, end) {
        return StrongDecision::NotTrigger;
    }
    // 未暂停(spec: "user is not in a paused state")
    if let Some(until) = state.pause_until {
        if now < until { return StrongDecision::NotTrigger; }
    }
    // 距上次间隔(spec: "time since last completed reminder ≥ interval")
    let interval_secs = (settings.reminders.interval_minutes as i64) * 60;
    let effective_last = state.last_strong_at.unwrap_or(now);
    let elapsed = (now - effective_last).num_seconds();
    if elapsed < interval_secs {
        return StrongDecision::NotTrigger;
    }
    // 跨下班前 20 分钟静默(spec: "Cross-period reminders")
    let end_secs = (end.hour() as i64) * 3600 + (end.minute() as i64) * 60;
    let now_secs = (now_time.hour() as i64) * 3600 + (now_time.minute() as i64) * 60;
    let mute_sound = (end_secs - now_secs).abs() <= 20 * 60;
    StrongDecision::Trigger { mute_sound }
}

5 个分支判断、每条都对应 spec 一句话、命名都对得上 spec 词(pause_untillast_strong_atmute_sound)。这就是 spec 能让 AI 不发散的物证。

10.3 ui/spec.md → tauri.conf.json 的 4 窗口

specs/ui/spec.md 规定了 5 个 UI:

UI 实现
主界面 src/App.tsx + tauri.conf.json::main(480×360)
强提醒弹窗 src/reminder/ReminderPopup.tsx + tauri.conf.json::reminder(320×200)
软提示 src/softprompt/SoftPrompt.tsx + tauri.conf.json::softprompt(320×200)
设置窗口 src/settings/SettingsWindow.tsx + tauri.conf.json::settings(800×600)
托盘图标 src-tauri/src/tray.rs

tauri.conf.json:25-65 的弹窗配置逐字对齐 design.md 第七节弹窗规范:

json 复制代码
{
  "label": "reminder", "width": 320, "height": 200,
  "transparent": true, "alwaysOnTop": true,
  "skipTaskbar": true, "focus": false, "decorations": false
}

transparent + alwaysOnTop + skipTaskbar + focus: false 这四个属性是弹窗必须的。少一个,要么挡焦点,要么在任务栏抢位置。


十一、技术架构:specs 推导出 4 窗口 + tokio + mpsc

11.1 架构设计的真实推导

我一开始不会问「tokio 多任务 mpsc 事件总线」。我连 tokio 是什么都不知道。

但写完 specs/spec.md、列完 tasks.md T00 到 T22 后,架构自己浮现了:

  • T06 屏幕统计要 30s 轮询 → 需要 async 任务
  • T07 提醒调度要订阅 T06 事件 → 需要 mpsc channel
  • T09/T10/T11 多窗口要同步状态 → 需要共享 Arc
  • T08 托盘菜单要触发「暂停」→ 需要 SchedulerControl 通道
  • T13 开机自启 → 已有 tauri-plugin-autostart

落到 src-tauri/src/lib.rs:102-156

rust 复制代码
.setup(move |app| {
    install_tray(app.handle())?;
    let db: tauri::State<'_, Arc<DbState>> = app.state();

    // 三条独立 mpsc 通道(specs 推导)
    let (stats_tx, stats_rx)       = tokio::sync::mpsc::channel(32);
    let (reminder_tx, reminder_rx) = tokio::sync::mpsc::channel(32);
    let (control_tx, control_rx)   = tokio::sync::mpsc::channel(8);

    let reminder_state = Arc::new(tokio::sync::Mutex::new(
        reminders::ReminderState::default()));
    app.manage(Arc::clone(&reminder_state));

    // 1) T06 屏幕统计
    tauri::async_runtime::spawn(statistics::StatisticsLoop::new(
        Arc::clone(&db), stats_tx).run());

    // 2) T07 提醒调度器
    tauri::async_runtime::spawn(reminders::ReminderScheduler::new(
        Arc::clone(&db), settings_arc, stats_rx, reminder_tx, control_rx)
        .run(Arc::clone(&reminder_state)));

    // 3) ReminderCommand → 前端事件总线
    tauri::async_runtime::spawn(async move {
        while let Some(cmd) = reminder_rx.recv().await {
            windows::translate_command_to_frontend(&app_handle, cmd).await;
        }
    });

    app.manage(SchedulerControlHandle(control_tx));
    Ok(())
})

这是「从 spec 推导架构」的真实路径。不是先画架构图再写代码,是先写 spec,架构自然浮现。

11.2 调度器主循环

来源:reminders.rs:663-690

rust 复制代码
loop {
    tokio::select! {
        maybe_ev = self.stats_rx.recv() => {
            // T06 心跳 / 锁屏事件
        }
        maybe_ctrl = self.control_rx.recv() => {
            // 前端 skip / complete / 测试按钮
        }
        _ = ticker.tick() => {
            // 每 5s 节拍:跑 should_trigger_* 纯函数
        }
    }
}

判定逻辑全是 pure 函数,单测覆盖率 100%。异步外壳只负责 IO 与状态变更。这个分离是 vibe coding 时让 AI 不发散的关键约束。AI 写 pure 函数比写异步状态机靠谱十倍。


十二、技术栈选型:specs 推导出每一条依赖

我没有一开始就知道要用这些依赖。是写 specs/tasks 时一个一个推导出来的。

12.1 后端依赖推导链

task 暴露的需求 选型 推到依赖的理由
T05 要调 Win32 API windows = "0.58" Tauri 不内置,要 windows crate
T06 要 30s 轮询 tokio = "1" Tauri 2 默认 runtime 就是 tokio
T04 要本地 DB rusqlite = "0.32", features=["bundled"] bundled 避免用户装 VC++ Redist
T03 要 JSON 设置 serde + serde_json + dirs dirs 拿 %APPDATA%
T13 要开机自启 tauri-plugin-autostart = "2" Mac 用 LaunchAgent / Win 写注册表
T08 要系统托盘 tauri = { features = ["tray-icon", "image-png"] } Tauri 2 内置 tray
T06 要时间戳 chrono = "0.4" db.rsYYYY-MM-DD 作 daily_stats 主键,需 chrono 做本地时间格式化
T09 主界面「打开 release 页」 tauri-plugin-opener = "2" Tauri 2 官方 plugin,调系统默认浏览器

12.2 前端依赖推导链

task 暴露的需求 选型
T09/T10/T11 多窗口 UI react@19 + react-dom@19
状态要在多窗口同步 zustand@4(轻量、无 boilerplate)
T11 设置页要滑块 / 复选 / 单选 @radix-ui/{slider,checkbox,radio-group}
T09 主界面要图标 lucide-react@0.469
design.md 的色号要变 className tailwindcss@3
构建速度 vite@7
类型安全 typescript@5.8

12.3 为什么是 Tauri:5 个推导条件

我不是看了「Tauri vs Electron」对比表就拍板。是这么推导的:

  1. Impact 段写「运行时占用 < 20MB」→ Electron 单进程 150MB+ 直接出局
  2. Impact 段写「目标用户是干眼患者,常驻后台」→ 启动越快越好,Tauri 70ms(README 实测)vs Electron 1-2s(行业经验值)
  3. task T02 要 NSIS 打包 → Flutter Desktop 没有原生 NSIS,排除
  4. task T05 要 Win32 API → 排除跨平台优先的方案,选 Tauri(Rust + 系统 API 友好)
  5. 我自己是 Rust 入门者 → Wails(Go)+ Flutter Desktop(Dart)= 新语言 → 选 Tauri

Tauri 是被这 5 个 specs 推导出来的唯一选项。


十三、踩坑复盘:每个 bug 都回头补 spec

# Bug 我原来没想清楚的 OpenSpec 怎么帮我补
T26 active 期间被重发,两个弹窗倒计时互相覆盖 「active 期间还要不要再触发」没想 spec 加 Scenario「active 时不重发」(reminders.rs:329 / 1077)
T29 锁屏检测永远 true 「Tauri 进程在 system session」没意识到 用「3 信号代理」过渡:前台窗口消失 + idle > 5s;OpenInputDesktop 真锁屏 API 推迟到 v0.2
T36+ 装上立刻弹眼药水 / 热敷 / 强提醒 「首次启动 UX」没想 reminders.rs 多处 last=None → far past 逻辑,强制等满 interval
NSIS 安装包启动后图标丢失 「图标变体」没列 tauri.conf.json 的 icon 数组补齐 5 个变体(32×32 / 128×128 / @2x / ico / png)
windows crate Windows API 跨平台编译报错 「仅 Windows 编译」必须约束 Cargo.toml 用 [target.'cfg(windows)'.dependencies] 隔离

踩坑一定要回头补 spec。T29 我记得最清楚。凌晨 2 点我让 AI 重写锁屏检测,它给的第一版还是用 OpenInputDesktop 句柄对比。我提醒了一句「Tauri 进程可能在 system session」,它立刻改成「3 信号组合」,一跑就过了。那个瞬间我意识到:spec 的边界真的能被 AI 严格遵守,前提是 spec 写得明确。


十四、技术评析:OpenSpec vs 其他「帮我想清楚需求」的方案

14.1 OpenSpec vs PRD vs User Story vs 原型设计

下表是我个人用过这 4 种工作流后的主观印象,不是行业基准。OpenSpec 这列基于沐目仓库真实结构;其余 3 列只是行业印象的简化,读者可以用自己的经验对线。

维度 OpenSpec PRD User Story Figma
写规格 WHEN/THEN 散文 As a / I want / So that 不写规格(主写视觉)
视觉规范 design.md 单独文档 单独文档 视觉原型
实施任务 tasks.md 不管实施 Sprint 拆分 不管实施
AI 友好 最高
个人开发者友好 最高
团队协作
沉淀 archive/ Wiki 散落 Jira / Linear Figma 文件

14.2 OpenSpec 最适合的场景

  • 个人开发者 + AI 协作(vibe coding 时代的最优解)
  • 产品边界模糊,需要想清楚才动手
  • 小工具 / 垂直工具 / 自用工具

不适合:

  • 大型企业产品(PRD 更适合)
  • 探索型项目(不需要规格也能跑)

14.3 如果你也是「脑子里空白」的状态

按这个顺序来:

markdown 复制代码
1. 打开 opsx:explore,问 AI「我想做 X」,它反问你 8-10 个问题
2. 回答完后,AI 给你一份「思考纪要」
3. 用思考纪要写 proposal(Why / What Changes / Impact / Capabilities)
4. 用 proposal 写 design.md(视觉 / 交互 / 文案)
5. 用 proposal 写 specs/*.md(每个能力的 WHEN/THEN)
6. 用 specs 写 tasks.md(实施步骤 + 故意没做 + 验收)
7. opsx:apply,开始写代码

写完 design.md,你已经知道 UI 长啥样。写完 specs,你已经知道行为边界。写完 tasks,你已经知道架构与选型。代码反而是最简单的部分。


十五、全文总结

  1. 「想做个产品」的鸿沟,技术从来不是问题,思考才是。你不知道要问自己什么问题,所以永远得不到答案。
  2. OpenSpec 的价值在于帮我和自己聊清楚。proposal 答 Why,design 答 Look,specs 答 What,tasks 答 How,每一类产物对应一类我需要回答的问题。
  3. 技术栈选型是被 specs/tasks 一个一个推导出来的。5 个推导条件推出 Tauri,是这套方法论的副产品。
  4. 架构是 specs 写完自然浮现的。4 窗口 + tokio 多任务 + mpsc 通道,是 specs 列完后唯一合理的方案。
  5. 踩坑一定要回头补 spec。T26/T29/T36 三个 bug 全部沉淀为新的 Scenario,否则下个项目继续踩。

十六、开源项目,欢迎试用与参与

沐目已发布 v0.1.0,MIT 协议开源。

16.1 仓库地址

16.2 我开源它的两个理由

我是干眼患者,知道这种工具对小众人群才有用。国内有大量长期面对屏幕的人存在不同程度干眼症状,这款工具如果只服务我一个人,价值有限。如果有 100 个人用了,哪怕只有 10 个人觉得「眼睛没那么干了」,这件事就值得做。

它是一份完整的方法论样本 。仓库 openspec/changes/archive/2026-07-20-add-mumu-eye-care/ 下放着一套从 0 到 v0.1.0 的完整记录:1 份 proposal / 1 份 design.md / 4 份 spec / 24 个 tasks。如果你想试 vibe coding 但不知道项目怎么起步,fork 这个仓库换一个 idea 走一遍 OpenSpec 全流程,比看 10 篇教程更快。

16.3 v0.1.0 已经能做什么

  • 强提醒弹窗(20-20-20 法则)
  • 弱提示(眼药水 / 热敷)
  • 每日屏幕使用统计
  • 托盘菜单(暂停 30 分钟 / 1 小时 / 到明早 9 点)
  • 开机自启
  • 性能达标:内存 33MB / CPU 0.26% / 启动 70ms

16.4 v0.2 已经在规划中

  • 色温调节(跟随时间变化)
  • 历史数据趋势 + 周报
  • macOS 移植(Tauri 已支持,等人接)

16.5 我希望谁来参与

不需要你是 Rust 大佬。下面这几类人都欢迎:

你是什么人 怎么帮
干眼患者,自己用得着 提 issue 反馈 bug 和体验问题,比 PR 更有价值
前端 / React 同学 改 UI 组件、调动画、做暗色模式
Rust 入门者 接 v0.2 的 macOS 移植,Tauri 跨平台文档够用
AI / Prompt 工程师 帮 OpenSpec 优化它的探索 prompt

16.6 如果你也在做「从一句话 idea 到 v0.1」的项目

不管你用 OpenSpec 还是别的什么工作流,我想说一件事:3 周做不完的项目不要硬做

沐目做出来只有一个目的:让我自己能用上。色温调节、跨平台、深色主题......这些都属于「我现在不需要」的功能,我都没做。

做出来能跑、哪怕丑陋、能发出去让别人用------这比完美的架构图重要 100 倍。

沐目的下一版改进,从你试用开始。


附录 A:沐目 OpenSpec 关键路径速查

想看什么 路径
全局配置 mumu/openspec/config.yaml
完整变更(archive) mumu/openspec/changes/archive/2026-07-20-add-mumu-eye-care/
提案 mumu/openspec/changes/archive/.../proposal.md
视觉规范 mumu/openspec/changes/archive/.../design.md
实施任务(T00 / T00.5 学习准备 + T01-T22 实施 + T10.5 弱提示) mumu/openspec/changes/archive/.../tasks.md
行为规格 1(reminders) mumu/openspec/changes/archive/.../specs/reminders/spec.md
行为规格 2(statistics) mumu/openspec/changes/archive/.../specs/statistics/spec.md
行为规格 3(ui) mumu/openspec/changes/archive/.../specs/ui/spec.md
行为规格 4(settings) mumu/openspec/changes/archive/.../specs/settings/spec.md
调度器核心实现 mumu/src-tauri/src/reminders.rs (1558 行)
锁屏 / 息屏检测 mumu/src-tauri/src/screen_state.rs
tokio 任务装配 mumu/src-tauri/src/lib.rs:102-156
Tauri 命令桥 mumu/src-tauri/src/commands.rs
4 窗口配置 mumu/src-tauri/tauri.conf.json:13-66
Zustand 状态(设置) mumu/src/stores/settings.ts
Zustand 状态(屏幕使用统计) mumu/src/stores/stats.ts
Tailwind 色号(来自 design.md mumu/tailwind.config.js

附录 B:OpenSpec 工作流命令速查

bash 复制代码
## 附录 B:OpenSpec 工作流命令速查

```bash
# 探索:AI 反问你 8-10 个问题,帮你梳理需求
opsx:explore "我想做一款护眼工具"

# 提议:一条命令同时生成 proposal + design + specs + tasks 4 个文件
# 参数是 change ID(kebab-case + 动词开头,在 openspec/changes/ 下唯一,如 fix-t29-lock-detection)
opsx:propose fix-t29-lock-detection

# 实施:边做边勾选 task
opsx:apply

# 归档:实施完成后归档,保留审计轨迹(生成 openspec/changes/archive/<change-name>/)
opsx:archive

如果你脑子里有个 idea 但完全不知道怎么动手,别打开 IDE,也别打开 Cursor。先在终端跑 opsx:explore "我想做 X",让 AI 反问你 8 个问题。然后你回答。然后你跑 opsx:propose,AI 把 4 类文档一次生成。

审完 4 类文档后,代码反而是最简单的部分。3 周后回看,你会感谢那个一开始脑子里空白、但愿意坐下来写规格的自己。

相关推荐
wangruofeng2 小时前
git-filter-repo 把 .git 从 112MB 砍到 1.4MB,但漏推 tag 让 clone 又胖回来
github·devops
峰向AI3 小时前
Block 放出大招!Buzz:一个中继统一代码、聊天、CI 全流程
github
dong_junshuai3 小时前
每天一个开源项目#47 4.4K Stars 的 LikeC4:让架构图随代码进化
github
独立开阀者_FwtCoder6 小时前
最近做了一个健身小程序:智形健身助手,健身的佬们来提点意见
前端·javascript·github
烬羽10 小时前
AI 写代码总翻车?试试"先画图再砌墙"的 Vibe Coding 三步法
react.js·ai编程·vibecoding
夕夕木各10 小时前
从第一个 PR 到 Vite 官方中文文档维护者
github·vite
梦想的颜色11 小时前
2026 VibeCoding 工具链精选|IDE + 大模型成套组合推荐,按场景分级收录
ide·trae·ai 编程·vibecoding·国产海外 ai 编程方案·氛围编程成套配置·副业 ai 开发工具栈
隔窗听雨眠11 小时前
GitHub Actions自动化运维实战:从零构建一体化CI/CD流水线
运维·自动化·github
win4r21 小时前
🚀Graph Engineering范式:Codex Multi-agent V2支持Kimi、MiniMax、GPT多模型混用+动态派生subagent,并行执行、Pi Agent工具调用,效率倍增
aigc·ai编程·vibecoding