资讯动态

从纯文本到命令行:caveman极简任务管理系统的设计复盘

发布时间:2026/10/29 13:44:32 来源:尧图企业网站定制
做了个名叫“caveman”的极简任务管理系统从零到现在用了将近四个月。最初只有标题这两个字正文、关键词、摘要全都没想好纯粹是被一堆重得走不动的工具链烦到不行才决定回到石器时代做东西。这篇是完整的项目回顾从产品定位、存储格式、解析实现到跨设备同步踩的坑、性能边界、可维护性权衡全部展开说说适合那些也想把日常任务系统做得足够轻、足够可控的人参考。1. 两个字的标题怎么就引出一场“去复杂度”实验先交代背景。那段时间我在用一套完整的项目管理系统功能很全看板、日历、标签、自动提醒、周报生成。听起来挺好但连续一周我花在“维护系统”上的时间比“处理任务”本身还多。要改一个截止日期得点开三个面板想给某条任务追加个背景说明得先想清楚这个字段在数据库里属于哪种类型周末打开手机想快速记一条灵感光等应用加载、登录就磨掉了不少耐心。然后我开始做减法。当时手头正好要新建一个内部小项目就在文档里随手写了个代号“caveman”——这个名字的寓意很直白别整那些花哨的像穴居人一样能用一块石头解决的问题绝不用一整套冶炼车间。项目正文一字没写关键词一个没填但反过来想这种“什么资料都没有”的状态本身就是最好的需求分析我知道我烦透了什么我清楚我需要什么剩下的事情就是把它从零“挖掘”出来。带着这个念头我给自己定了三条硬约束零外部依赖不引入数据库服务、不同步远端 API、不上云。装完 Python 就能跑连 pip 安装的第三方包都尽量不碰。单一文本文件作为全部数据源所有任务、备注、状态、标签都落在一个可读的纯文本文件里。这个文件本身就是“数据库”。命令行优先所有操作通过命令完成没有 GUI没有常驻进程没有本地服务端口。用完即走不占内存。这三个约束没有一条是拍脑袋定的。零外部依赖意味着这个工具在任何机器上都能复现单文本文件意味着我可以随时用 grep用编辑器用任何手段直接检查和修改数据命令行优先则绕开了所有图形交互的延迟。这套逻辑不复杂但它和大多数商业工具的设计方向是反着的它们追求的是“功能更多”我追求的是“摩擦更少”。更关键的是这个项目成了一个极好的过滤网。每当我准备给 caveman 加一个功能我都会先写下一句话“这个功能是否值得让那个文本文件变复杂”如果答案是否定的就砍掉。这种“先思考成本再做事”的习惯在后来的其他项目里也帮了大忙。2. 单文件优先的任务存储设计为什么我放弃数据库和 JSON很多人一听“任务管理系统”第一反应是上 SQLite或者至少用个 JSON 文件。我一开始也认真比对过这条路线但很快否掉了。原因不复杂数据库的强项是查询、索引、并发控制而我的使用场景是单人、低频、小量级。为了这些用不上的强项我得付出“数据结构不可直接阅读”的代价。JSON 文件虽然比数据库轻一些但日常查看、修改仍然不够顺手——改错一个逗号整个文件就凉了。所以 caveman 的核心存储最终设计成了一种带轻量标记的纯文本格式。以“部落”为顶层分组每条任务用一个方括号收纳状态后面跟内容、可选的标签和截止时间。下面就是我实际在用的一个文件片段的脱敏版# 工作部落 - [ ] 完成季度数据复盘 工作 复盘 /due:2025-06-30 - [x] 整理上周会议纪要 工作 /due:2025-06-20 - [ ] 评审新智能硬件方案的功耗估算表 工作 硬件 /due:2025-07-05 # 个人部落 - [ ] 预约牙医复诊 生活 /due:2025-06-28 - [ ] 读完《设计的生态学》并写笔记 学习选这种格式的理由有几层。首先它天然兼容 Markdown 的待办列表语法这意味着我可以在任意编辑器、在线代码托管平台、Markdown 预览器里直接看到结构化渲染结果工具“消失”了。其次它足够宽松允许我随时往行尾追加人类可读的补充信息比如设备/功耗数据等评审材料在docs/功耗目录不需要预先设计字段。最后它是文本所以我可以用最原始也最可靠的手段检索它——grep。为了解释这个选择我做了一张对比表格把当时评估过的三种方案拿出来一起看维度数据库SQLiteJSON 文件纯文本 Markdown直接人工修改需要工具困难有语法风险困难随手可改零风险检索能力强SQL 灵活需要写代码解析grep 足够还可跨编辑器搜可读性差中极高版本管理友好度二进制 diff 难懂diff 能看但噪音大diff 极其清晰崩溃恢复依赖事务写坏就全毁行级损坏可部分恢复依赖复杂度高中零打完这张表答案已经非常明显。我不需要“更强的查询”来描述一个每天最多二三十条任务的文件我更需要在凌晨加班熬得头昏脑涨时还能用记事本打开它一眼看出所有待办长什么样。另外还有一个确保长期可用的设计细节文件里不存任何“排序号”。任务顺序就是它们在文件中的物理顺序。这少了一个字段却避免了排序逻辑带来的同步冲突和技术债。想调整顺序就调整文本行的先后顺序这依赖的是最底层、永远不会失效的“文档结构”而不是某一段自以为聪明的排序算法。3. 状态解析的核心实现不到两百行但每行都有讲究存储格式定了之后真正的难点在于怎么稳定、安全地解析它。caveman 最早用 Python 写了个单文件脚本名叫caveman.py。解析器的核心逻辑不到两百行却花了我两个晚上调通。原因嘛文本解析的坑永远比你想象得多。任务行分成四种状态我用状态标签来区分[ ]未开始todo[x]已完成done[!]阻塞中blocked[~]进行中doing这比单纯 to-do/done 多了一点表达力又没复杂到需要工作流引擎。解析的时候我按整行处理剥离前导空格判断首字符是不是-然后读取方括号里的符号。这里有一个容易被轻视的地方方括号的位置不能想当然。比如用户可能在任务内容里手写了类似- [ ] 给前端同学发一份 [重要] 资料的内容如果只用简单正则从头匹配一旦全局搜索第二个方括号就会拿错状态。所以我规定状态标记只在行首出现且严格匹配- [标志]这个前缀后面必须跟一个空格。解析函数大概长这样import sys, os, re from datetime import datetime TODO, DONE, BLOCKED, DOING todo, done, blocked, doing TASK_RE re.compile(r^-\s*\[([ x!~])\]\s(.)$) META_RE re.compile(r^#\s*(.)$) TAG_RE re.compile(r([\w\u4e00-\u9fa5])) DUE_RE re.compile(r/due:(\d{4}-\d{2}-\d{2})) def parse_line(line): m TASK_RE.match(line) if not m: return None flag m.group(1) body m.group(2) state { : TODO, x: DONE, !: BLOCKED, ~: DOING, }[flag] tags TAG_RE.findall(body) due DUE_RE.search(body) due_date due.group(1) if due else None text DUE_RE.sub(, body).rstrip() return { state: state, text: text.strip(), tags: tags, due: due_date, raw: line, }这段代码看起来简单但几个细节都是我踩过坑之后才固定下来的。第一正则用^-\s*而不是^-防止有人在-后不小心多打了空格导致识别失败。第二标签正则只匹配中文和英文字符避免把工作/财务这类带斜杠的复合标签拆碎。第三截止日期用严格格式匹配避免2025-6-30这种不规则写法混进去又查不出来。加上这些约束之后解析稳定性一下子上了一个台阶。构建在这套解析器之上的命令层也保持同样的朴素风格。我设计了五个命令覆盖了百分之九十的日常使用python caveman.py new 完成数据看板切片 工作 /due:2025-07-01 python caveman.py list python caveman.py list --state doing python caveman.py done 3 python caveman.py drop 3done 和 drop后面的数字是“行号”也就是在list输出里看到的序号。用行号代替 UUID 是刻意为之文件里最多几十上百条任务行号是人最容易理解的寻址方式比一长串随机字符串友好得多。代价是一旦删除了中间某行后面的行号就会整体变化。应对方法是删除前先执行一次最新的list让行号始终基于当前状态同时在drop命令里要求二次确认。实现done 3的时候我稍微抽象了一下把“修改第 N 行”统一封装成update_line(filepath, lineno, new_content)其它命令都走同一个函数。这样后续新增rename、move命令时不需要重复处理文件读写逻辑。有一段关于写入安全的代码必须单独拿出来讲。因为文件是唯一数据源写入过程一旦崩溃轻则丢数据重则整个文件损坏。所以我采用了“临时文件 原子替换”的经典做法def write_file_safely(path, lines): temp_path path .tmp with open(temp_path, w, encodingutf-8) as f: f.writelines(lines) f.flush() os.fsync(f.fileno()) os.replace(temp_path, path)先写入同一目录下的临时文件显式刷新到磁盘后再用os.replace覆盖原文件。同一目录保证了文件系统层面不会跨分区拷贝os.replace在主流系统上是原子操作所以要么旧文件还在要么新文件完整落盘不存在中间态。这个经验来自一次真事故后面细说。4. 用户在真实使用中撞上的坑换行、编码与云同步冲突我自己是 caveman 的唯一用户所以“用户反馈”就等于“自己的脸被打”。运行三个月差不多碰上过四类问题每一个都有代表性。第一个坑是 Windows 环境下 CRLF 换行带来的幽灵问题。某次我用记事本改了文件再回到命令行执行list发现所有行尾多了\r解析时正则$受到影响标签里的结尾字总被吞掉。后来我统一在打开文件时做一次换行规范化按文本模式读取后把\r\n和\r统一转成\n写入时再由写入层决定是否转回平台默认值。现在文件里统一用 LF不管在哪个平台打开不会再出岔子。第二个坑是 UTF-8 BOM。有一版文件被 Windows 自带记事本保存过文件头带了一个\ufeff字符。这个不可见字符让第一行“部落标题”永远无法匹配META_RE。排查了十分钟才发现不是正则的问题而是编码。现在的加载函数里解析前直接text text.lstrip(\ufeff)彻底杜绝。第三个坑说出来有点丢人但估计很多人也遇到过——一条任务内容里包含成对的中文引号和英文引号混合比如- [ ] 给客户发邮件跟进“样品封装”方案这看起来人畜无害但有一个版本的标签提取正则没处理好标点导致引号后的中文字符被打散。后来我把标签提取改成“先摘出所有...片段并移除再对剩余部分进行文本清洗”用两遍扫描替代单遍正则才彻底稳定。第四个坑最严重。某天我在公司电脑上新增了三条任务回家打开笔记本发现那三条任务消失了。原因很典型两台设备上的文件都经第三方网盘同步我出门前没执行同步公司电脑上把旧文件改写成了新版本回家后笔记本上的旧版本也被网盘“同步”回来了于是覆盖了公司电脑的改动。问题不在 caveman 本身而在“多端无协调并发写同一个文件”这个场景本身。解决思路有两层。第一层是尽量把“多端同时改文件”变成“同一时刻只在一端改”也就是使用前先手动同步一次用完再同步一次让两端不会并行落笔。第二层是在文件里加一段不显眼的版本信息注释格式类似# version: 20250630-2108每次写入的时候把当前时间戳刷新进去。如果后来发现文件内容不是自己预期的最新版至少能从版本号快速判断是哪一端覆盖了哪一端辅助找回数据。严格地讲这仍是“同步冲突”而非“并发一致性”的解法但对于单文件任务系统避免并发已经是成本最低的合理策略了。这种问题大概率出现在所有想沿用“本地优先”思路的人身上我的建议是别指望网盘替你解决多人协作要么接受单一主设备轮换修改要么主动引入 git 做冲突管理再配一个自动提交的钩子。短期内我选择了“先同步再用”的笨办法它足够有效也足够简单。5. 可维护性边界当纯文本方案开始“吃力”时的信号没有哪个方案是黄金方案。caveman 的纯文本模式在用到一个量级以下时非常舒服但突破某个量级就会出现明显不舒服的信号。把这些信号整理出来比吹捧这个模式更实际。第一个信号是单文件行数超过一千行。一千行以内在任意编辑器里滚动都还算轻快一旦超过加载和搜索开始有迟滞感滚动翻页也容易看丢上下文。1000 行这个阈值不是理论推导的是在list命令平均响应时间和肉眼浏览体验出现明显变化时测出来的。这时候我的建议不是换技术栈而是“分部落”。通过include指令把每周归档、月度复盘挪到外部文件去caveman 只加载一个精简入口。也就是说用内容组织来解决文件膨胀而不是引入数据库。第二个信号是检索需求开始变复杂。当我想回答“去年第四季度完成了多少个含硬件标签的任务”这种跨时间统计时文本模式明显有点吃力。grep 只能查“行”不能查“聚合”。一开始我用 shell 管道做很笨的统计比如list --state done | grep 硬件 | wc -l后来需求更复杂我意识到这不是工具问题而是我试图拿任务系统做报表。正确做法是把报表需求外置给其他专用工具caveman 只需要提供足够稳定的导出接口比如一次输出 JSON 行供统计脚本消费。工具各司其职不要硬让一个文本文件承担数据仓库的职能。第三个信号更微妙是“技术债感”出现但这个技术债不在代码里而在格式里。假如某天我决定把[~]改成[]我需要脚本去批量迁移历史文件。由于每一行都可能是历史遗留格式解析器的容错升级往往比新功能开发更费心。这几乎是所有自定义格式系统的宿命。但换个角度想正因为我知道“改格式很痛”我才会在一开始就把状态标志、标签、截止日期的语法定义得尽量稳定避免频繁破坏性变更。这种约束反过来让项目长期处于低维护状态也算是一种“先痛后爽”的前置投资。当这些信号出现时我的原则是先做“运维层面”的优化比如分文件、开归档目录、规范化标签体系如果仍然吃力则引入一个只做聚合层的外部脚本不修改 caveman 核心。这个边界守住了项目才始终保持在“简单可控”的状态里。6. 如果重新做一遍我会保留和会改的几处设计谈到复盘我一直在想如果这个项目从零再写一次哪些东西必须保留哪些东西值得改。必须保留的是“纯文本即界面”这一条。它最大的价值不是“极客”而是给了用户完全的控制权。不管 caveman 脚本某天更新出了 bug或者某台电脑上没有 Python 环境只要那个文本文件还在里面的任务就都还在而且我能用任何工具把它们取出来。这种“系统可以坏数据不能锁死”的底线是这个项目里最值钱的地方。必须保留的是原子写文件。一旦养成“写文件必须经过临时文件 os.replace”的习惯数据处理的安全基线就立住了。它不花多少代码量却能避免最严重的数据丢失。会改进的是命令解析层的设计。当前我使用行号定位任务简单但脆弱如果重来一遍我会在文件中允许一个可选的短 ID比如id: t3让命令改成done t3。这不会破坏文件的可读性——ID 可以放在最前面例如t3 - [ ] 给客户回邮件而且没有 ID 的旧行仍能通过行号访问构建一个渐进兼容的寻址系统。这样既保留简洁性又降低删除行造成索引漂移的影响。会改进的是索引和盲区。现在的搜索全靠 grep但有两个情况 grep 不擅长一是中文全角空格和英文半角空格混在一起时查询词会被切得七零八落二是任务内容里出现正则表达式里的特殊字符时容易产生误命中。我会在查询函数里预先做归一化把全角空格转半角、统一大小写再走匹配至少把最常见的中英文混排问题兜住。另外还值得补一个小的“时间归因”功能在完成任务的瞬间自动追加一行/done:2025-06-30之类的元信息而不是只把方括号从[ ]改成[x]。这样以后复盘“上个月完成了什么”就多了一个可靠维度。这个功能我一直想加但没排期等下次周末补上。7. 一些真正有用的实践心得而不是正确的废话文章写到这里我想把几个不那么“技术”但同样重要的心得收个尾。它们不像解析器代码那么闪闪发光却恰恰是项目能坚持下来、不被丢弃的原因。第一小工具最容易被忽略的价值是“心智负担低”。caveman 不需要我记住操作流程不需要我看教程因为我用任何文本编辑器打开目标文件就能操作。我把使用它的门槛压到了几乎为零这才是它最终会被我这个懒人一直用下去的根本原因。很多产品的问题不在于功能少而在于“连打开它都需要心理准备”。第二留下清晰的“防呆路径”。在 caveman 里我会在文件头部放一段注释写下最简单用法和急救方法比如“如果命令失灵直接用编辑器改文件就够了”。这套注释就是自己的救援手册。任何项目都应该有一份这样的笔记否则三个月后回来会陷入“这工具是我写的但怎么用来着”的窘境。第三给项目设定“停损点”。我给 caveman 设置过一条规则如果某天我为了优化它开始写超过三百行的复杂状态机那说明我应该用它之外的工具。同理如果一个单文件脚本的开发维护成本超过了它带来的便利这个项目就失去了意义。这条规则让我随时能把精力抽出来不被“自己的作品”绑架。说到底caveman 不是什么了不起的系统它就是一块石头削尖了做成的工具。但石头工具的好处是它永远不需要通电、更新、联网也不会在关键时候背叛你。如果你想做一个类似的项目我的建议非常明确从一个纯文本文件开始把所有操作都想成对这个文件的读写把“可手工修改”当作最高优先级的设计目标。至于数据库、服务端、多端协作那都是后续被真实需求逼着再加的复杂层别提前给自己上难度。最后再分享一个我自己其实一直没彻底解决的问题caveman 处理“周期性任务”——比如每周复盘——仍要靠手动复制旧任务再改日期没有自动生成机制。这是个明确可做的功能但我一直压着不做因为我不想破坏“文件内容完全由人控制”的原则。这种“留一个功能不写”的克制恐怕也是这个项目能保持干净到最后的原因之一。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑