☰
手写极简命令行任务管理工具:Caveman设计与实现解析
2026/10/8 5:22:22 网站建设 项目流程

最近半年我几乎把所有效率类软件都卸干净了——不是矫情,是真的被“工具肥胖症”折磨够了。Caveman 就是我为了解决这个问题亲手写的命令行任务管理小工具:只有一个二进制、一堆 JSON 文件、八九条命令,却撑起了我每天的任务记录、番茄钟专注和晚间复盘。名字起得很直白,它就是要像原始人手里的石斧一样,只有一块石头一根棍子,但能干活、能打猎、能活下来。这篇文章我会把它的设计思路、核心实现、踩过的坑全部拆开讲,如果你也想做一个属于自己的极简工具,可以直接照着抄。

1. 项目整体设计与思路拆解

1.1 为什么做一个“原始人”级别的工具

先交代一下背景。我先后用过 Notion、Todoist、滴答清单、Things,来回搬了至少三次家。每次换工具,都要重新设计标签体系、整理分组结构、处理多端同步冲突。等我把工具收拾利索,写任务清单的那个冲动早过去了。更崩溃的是,这些软件里有一半的功能我根本没用过,却要为它们承担学习成本和订阅费用。后来我干脆退回文本文件记待办,撑了两个月,发现比任何 App 都顺手。Caveman 就是在那个状态下诞生的:记录、执行、复盘,三个动作,一个命令。

Caveman 这个名字有两层意思。第一层是“简单到像原始人”,它不提供日历视图、不搞云同步、不做自然语言解析,所有功能加起来只有 8 个动词;第二层是“帮你在电脑前找回专注”,就像原始人一次只追一头猎物,你通过番茄钟一次只做一件事。它解决的核心问题非常具体:减少“选择工具”本身带来的认知负担,让你从打开终端到开始干活,耗时不超过 10 秒。

这个工具适合三类人:每天泡在终端里的开发者,想要极简生活却总被复杂工具劝退的人,以及所有对“配置半小时、干活五分钟”深恶痛绝的人。如果你完全不用命令行,这篇文章里的取舍思路同样值得看——做减法这件事,和用不用终端无关。

1.2 技术选型:从语言到存储到依赖

我做个人工具有个习惯:动手前先问“这玩意儿我要维护几年”。Caveman 定位是“自己天天用、不想折腾”的小工具,所以选型标准只有三条:单文件可执行、零运行时依赖、数据文件人眼能读能改。

语言选了 Go,原因很直接:go build 出来就是一个静态二进制,扔到 /usr/local/bin 就能跑,不需要装 Python 环境,不怕系统升级把依赖弄坏。启动速度也是刚需,命令行工具只要超过 50 毫秒,体感就发粘。你如果更熟悉 Python,用标准库写同样逻辑完全没问题,后面所有设计都是语言无关的。

存储方案是 JSON 单文件加 JSONL 追加日志,没有碰 SQLite。任务数据量撑死几千条,SQLite 的索引、事务、并发控制属于大炮打蚊子。JSON 文件的好处是随时能打开看,出了 bug 一眼定位,备份就是拷走一个文件。缺点是不能并发写,但命令行工具本来就是单用户单进程,注意原子写入就够用。这个选择背后有个很朴素的逻辑:小工具的数据库应该是“你能用手摸着的数据”,而不是一个需要命令行进出的黑盒。

依赖方面我压到极致:时间处理用标准库,颜色输出自己拼 ANSI 转义序列,系统通知做成可选模块,检测到 notify-send 或 osascript 才启用,没有就静默写日志。核心原则是:通知这种锦上添花的功能,绝不能成为主流程的绊脚石。

1.3 功能边界的确定:砍掉哪些“理所当然”

做工具最难的不是加功能,是砍功能。Caveman 第一版砍掉的清单比保留的长得多。没有日历视图、没有标签筛选器、没有团队共享、没有自然语言解析、没有云同步、没有手机端。每次砍功能前我都问自己一个问题:如果今天没有这个功能,我会不会活不下去?

答案全是“不会”。日历视图的本质是把信息换成视觉呈现,但终端里一条时间线就能覆盖 80% 的回顾需求;云同步解决的是多设备一致性问题,而我的真实场景里“公司写完、回家还想看”一周发生不了一次,真需要就用 git 仓库同步一个 JSON 文件,十分钟搞定;自然语言解析“12月5日下午3点前提交周报”看着很酷,可它本质上只是比“add 提交周报 --due 周五”少敲几个字,却要引入一整套解析器,性价比太低。

最终留下来的命令只有 8 个:add、done、rm、ls、do、log、stat、edit。每个动词对应一个动作,没有子命令嵌套,没有 flags 风暴。这个减法过程本身就是项目的核心产出之一。你在设计任何工具时,第一步永远是画一条“功能红线”:线内的做到极致,线外的一律不碰。

2. 核心细节解析与实操要点

2.1 命令设计:每个动作就是一个动词

Caveman 的交互哲学是“所见即所动”:想记事,输入 add;想专注,输入 do。命令统一采用“动词 + 参数”的扁平结构,拒绝子命令嵌套。我见过太多工具把 add 藏在 create 里、把 list 藏在 view 里,用户得先记忆一棵命令树才能开始干活,这是典型的认知税。

典型会话长这样:

# 记三件事 caveman add 写周报 --tag work --due 周五 caveman add 修数据库慢查询 --tag work,dev caveman add 买猫粮 --tag life # 看今天要做什么 caveman ls # 开始 25 分钟专注(默认取第一个未完成任务) caveman do 1 # 完成一件事 caveman done 1 # 晚上复盘 caveman log

设计时有几个细节很关键。第一,add 成功后会直接打印新建任务的 ID,让“刚建完就 do”的链路顺畅无阻;第二,所有 flag 都有合理默认值,do 不带参数自动选最旧的未完成任务,due 不填默认当天;第三,输出刻意不玩花活——颜色只用来区分状态和 ID,不用来装饰标题,保证输出重定向到文件或管道后依然干干净净。

2.2 事件日志:一切皆追加,状态可重建

Caveman 的数据模型分两层。任务表保存当前状态,事件日志保存历史事实。任务表里的每条记录有 ID、标题、标签、创建时间、完成时间;事件日志用 JSONL 格式追加,每发生一个动作就写一行:

{"ts":"2025-03-16T09:00:00+08:00","type":"focus_start","task_id":1} {"ts":"2025-03-16T09:25:00+08:00","type":"focus_end","task_id":1,"finished":false} {"ts":"2025-03-16T09:25:00+08:00","type":"break_start","len_min":5}

这种“事件溯源”风格对个人工具有三点好处。第一,日志天然不可变,复盘和统计始终有原始数据可查;第二,就算任务表意外损坏,从日志也能重建绝大部分状态;第三,将来想换数据结构、想导出给别的工具,日志就是现成的数据真相。代价是统计要扫描日志,但个人数据量下成本几乎可以忽略。

命令和日志的对应关系是:add 追加 created,done 追加 completed,do 追加 focus_start,番茄钟结束追加 focus_end 或 break_start。每天结束时 log 按时间排序输出时间线,stat 按天聚合算出“今天专注几次、总共多少分钟”。这套设计的精髓在于:任务表只是“视图”,日志才是“事实”。

2.3 番茄钟状态机:用时间戳,不靠倒计时

番茄钟是 Caveman 的核心模块,也是最容易写歪的地方。我第一版用的是 sleep 倒计时,结果发现两个致命问题:终端窗口一关,进程死了,倒计时就丢了;电脑休眠半小时,定时器漂移,醒来界面还停在“还剩 23 分钟”。所以后来我彻底改了思路:只记录“开始时刻”,不记录“还剩多少”。

状态机维护一个 state.json,里面存当前相位(idle、focus、short_break、long_break)和上次变更的时间戳。任何命令进来,先算 elapsed = now - last_ts,再决定是否需要推进状态:

idle ──do──▶ focus ──25min──▶ short_break ──5min──▶ focus │ ▼ 第4轮后 long_break

核心代码逻辑如下:

func cmdDo(ws *Workspace, taskID int) { st := ws.LoadState() now := time.Now() switch st.Phase { case "focus": elapsed := now.Sub(st.Ts) if elapsed >= 25*time.Minute { ws.Log("focus_end", st.TaskID, false) st.Phase = "short_break" // 第4轮后切 long_break st.Ts = now } else { fmt.Printf("专注剩余 %d 分钟\n", 25-int(elapsed.Minutes())) } case "idle": st.TaskID = taskID st.Phase = "focus" st.Ts = now ws.Log("focus_start", taskID, true) } ws.SaveState(st) }

这套“状态校正”逻辑只要命令被调用就会执行一次。它解释了为什么 Caveman 不需要后台驻留进程——它只是一个每次运行几毫秒、算一算“当前该处于什么状态”的小程序。这个方案也适合任何“定时提醒但允许中断”的场景,比如喝水提醒、定时发邮件,都可以套用。

2.4 输出与交互:克制是美德

CLI 工具的输出本质是一种接口设计。Caveman 的输出规则只有三条:不用表格库、不用动画、不画花哨边框。ls 的默认输出就是平铺列表:

ID STATUS TITLE TAGS DUE 1 pending 写周报 work 周五 2 pending 修数据库慢查询 work,dev - 3 running 买猫粮 life - (25:00)

唯一称得上“花活”的是 running 状态后面的实时倒计时,它会在每次 do 运行时刷新。颜色只在标准输出是 TTY 时开启,并支持 NO_COLOR 环境变量直接关闭。错误提示我也下了功夫:不报“操作失败”这种废话,而是报“找不到 ID 为 3 的任务,当前最大 ID 是 5”,让用户一眼知道下一步该干什么。这种“错误信息即导航”的思路,比任何错误码都管用。

3. 实操过程与核心环节实现

3.1 环境准备与项目初始化

写这个项目只需要一个 Go 环境。我用的是 Go 1.23,项目目录保持扁平,方便一眼看全:

caveman/ ├── go.mod ├── main.go # 命令入口,子命令路由 ├── cmd_add.go # add / done / rm ├── cmd_do.go # 番茄钟状态机 ├── cmd_log.go # 日志与统计 ├── store.go # JSON 读写、原子写入、备份 └── tasks.go # 任务模型与操作

初始化只需两步:go mod init caveman,然后逐文件填充。main.go 只做一件事——把 os.Args[1] 路由到对应函数,未知命令直接打印用法并返回退出码 1。我故意不引入 cobra 这类 CLI 框架,因为 8 个命令不值得为了几个 flag 解析背上五六个依赖包。标准库 flag 加一层 switch 完全够用,而且所有行为都在自己掌控里,出了问题不用去翻框架源码。

3.2 数据层:原子写入与自动备份

store.go 是 Caveman 的“数据库层”,包含三块:加载、保存、备份。加载用 json.Unmarshal 读 data.json,解析失败时尝试恢复最近的 .bak 文件并打印警告;保存用“先写临时文件、再 rename 覆盖”的原子方式;每次启动时把现有 data.json 复制为 data.json.bak,保留最近两个副本。

func Save(w *Workspace) error { tmp := w.Dir + "/data.json.tmp" f, err := os.Create(tmp) // 写入内容... return os.Rename(tmp, w.Dir+"/data.json") }

提示:临时文件必须和目标文件在同一个目录下,rename 才能保证原子性。跨目录 rename 在某些文件系统上会退化成 copy + delete,失去“写到一半断电也不损坏”的保护意义。

任务模型长这样:

type Task struct { ID int `json:"id"` Title string `json:"title"` Tags []string `json:"tags,omitempty"` Due string `json:"due,omitempty"` CreatedAt time.Time `json:"created_at"` DoneAt *time.Time `json:"done_at,omitempty"` }

ID 分配采用 max+1 自增,删除任务后不复用。为什么坚决不复用?因为日志里存的是 task_id,如果删除后同一个 ID 又给了新任务,日志统计就会串味。个人工具的数据一致性,很多时候不是靠数据库约束,而是靠这种“不给自己挖坑”的编码约定。

3.3 番茄钟状态机的完整实现

番茄钟涉及三个命令:do(开始或恢复)、pause(暂停)、abort(放弃)。核心逻辑我前面已经贴了框架,这里补一些实操细节。

首先是长休息的切换逻辑:统计 state.json 里本轮连续完成的专注次数,每满 4 次,focus_end 后的休息相位就切到 long_break,否则是 short_break。这个计数也在命令每次被调用时重新计算,进程杀掉也不影响。

其次是结束提醒。focus_end 发生时,代码先尝试系统通知:

func Notify(msg string) { if _, err := exec.LookPath("notify-send"); err == nil { exec.Command("notify-send", "caveman", msg).Start() } else if _, err := exec.LookPath("osascript"); err == nil { exec.Command("osascript", "-e", `display notification "`+msg+`" with title "caveman"`).Start() } // 都没有就静默,反正日志里已经记了 }

这里的原则是“尽力而为”:通知发不出去没关系,日志永远在。我还把 caveman status 的输出接进了 zsh 的 RPROMPT(右侧提示符),每次回车都能看到“专注中,剩余 18 分钟”或“休息中”。这是我最推荐的集成方式——番茄钟的提醒感不靠弹窗,靠无处不在的轻量可视化。

3.4 日志与统计:用标准工具也能查

log 命令读 JSONL 日志按时间排序输出,stat 命令做聚合。聚合逻辑不复杂:遍历日志,focus_start 遇到对应的 focus_end 才算一次完整专注,时长等于两者时间戳之差;被 abort 的不计入完成次数,单独列为“中断”。输出按日聚合:

2025-03-16 专注 3 次,共 75 分钟,中断 1 次 2025-03-17 专注 5 次,共 125 分钟,中断 0 次

这里有个彩蛋:因为日志就是 JSONL,你完全可以用标准 shell 工具直接分析,不必等 stat 支持所有场景。比如查今天所有专注开始时间:

grep '"type":"focus_start"' ~/.caveman/journal.jsonl | tail -5

当初选择“人眼可读的纯文本日志”带来的红利就在这里:你不需要为每个统计需求改代码,文件就在那儿,jq、awk、grep 随便折腾。工具的尽头是文本,这不是一句玩笑。

3.5 安装、别名与日常集成

构建安装一条命令搞定:

go build -o caveman . sudo install caveman /usr/local/bin/

日常使用我给它配了短别名。在 .zshrc 里加几行:

alias cm='caveman' alias cml='caveman log --today' alias cms='caveman stat'

每天开工前我的流程固定是三步:caveman add 把当天所有杂事丢进去,caveman ls 挑出三个优先级最高的,然后 caveman do 开始第一个番茄钟。这套流程跑了两三个月,最直接的改变不是“专注时长”变长了,而是我对“今天到底干了什么”有了清晰的交代——每天晚上 caveman log 打出来,时间线就摆在那儿,哪里浪费了自己心里有数。

4. 常见问题与排查技巧实录

工具用了几个月,踩过的坑不少,整理成速查表,全是真实发生过的:

问题现象根本原因解决办法
番茄钟结束没有系统通知系统没装 notify-send / osascript安装 libnotify-bin,或改用 shell 提示符显示状态
电脑休眠后倒计时“穿越”第一版用 sleep 累加计时改时间戳计算,命令进入时做状态校正
data.json 打不开,JSON 报错写入时断电或进程被杀原子写入 + 启动前校验 + .bak 自动回滚
统计数字对不上删除任务后 ID 复用,日志串味ID 永不复用,删除只标记状态
输出重定向后满是乱码颜色ANSI 在非 TTY 环境未关闭检测 isatty,支持 NO_COLOR 环境变量
中文标题在终端里对不齐中英混排宽度计算复杂果断放弃对齐,用空格分隔即可读

4.1 系统休眠导致的状态漂移

这个问题发生在第一版用 sleep 计数器的时候:午休合上笔记本,下午打开发现番茄钟还显示“还剩 18 分钟”,实际已经过去两小时。改成时间戳方案后彻底解决,核心就一句话:只记“开始时刻”,不记“还剩多少”。这个经验我后来用到了很多地方——任何需要“定时但允许中断”的功能,都应该记录起点而不是倒计时。

4.2 数据损坏与自愈机制

有一次我 cat data.json 发现少了个右括号,才知道写文件时被另一个终端里的进程打断了。修复方案分三层:Save 里严格走临时文件加 rename;每次启动先做 json.Valid 校验,非法就复制 .bak 覆盖并打印警告;每周用 cron 把整个 ~/.caveman 目录打包到本地 NAS。一个 JSON 文件承载了我一个月的工作痕迹,再怎么小心都不过分。

4.3 跨平台与终端环境的坑

macOS 上测试一切正常,换到 Windows 的 cmd 里发现颜色全是乱码,原因是老式终端不认 ANSI 转义序列。我的处理是:isatty 检测只区分 TTY 和非 TTY,不区分系统;Windows 用户直接推荐用 WSL 跑,体验和 Linux 一致。另一个经验是时间存储必须带时区偏移(RFC3339),否则用 UTC 存、本机是 +08:00,跨天统计会晚 8 个小时,账目全错。这两个坑都很基础,但值得写进任何 CLI 工具的开发笔记里。

5. 实战心得与可以继续扩展的方向

用了 Caveman 两个多月,我最深的一个体会是:工具的价值不在功能多,在“你用它的频率”。以前装的那些效率软件,打开频率越来越低,最后沦为文件夹里的图标;Caveman 因为足够轻,反而成了我每天打开终端后第一个敲的命令。它不美化数据、不生成花哨图表,就是把时间诚实地摆在那里,让你自己判断哪里值得、哪里浪费了。

我也在琢磨几个扩展方向,但都遵循“先难受再动手”的原则。比如想做 shell 自动补全,因为每次敲 --tag 都要翻帮助;想做只读 web 报表,因为每周总结时想用浏览器看数据;想加提醒音效,因为番茄钟结束全靠眼神瞥到 RPROMPT。这些需求都是真实用出来的,不是拍脑袋想出来的。

最后再分享一个对我很有用的习惯:每天开工前只挑三件事进“今日专注额度”,其余事情不是不重要,而是不配占据今天的深度工作时间。Caveman 只是把这套流程压缩成了三行命令。你要是也想做个类似的小工具,我的建议是先把最常用的三个动作写死,用两周,把难受的点全记下来,第二版再改。工具这东西,永远是“用出来的”,不是“设计出来的”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询