如果你和我一样,早年间用手机备忘录记灵感,后来辗转于 Notion、Obsidian、语雀之间,最后却发现最想找的那条信息往往躺在三个月前的一个 txt 文件里,那你大概能理解,为什么我会花一个周末写出这个叫caveman的命令行小工具。它不联网、没有数据库、没有富文本编辑器,甚至没有图形界面,核心逻辑只有三个词:存、找、列。
这个项目最初的灵感来自一次整理旧博客草稿的痛苦经历。我发现过去四年的零散想法散布在微信收藏、flomo、备忘录、Notion 页面、以及好几个从来懒得打开的 Markdown 目录里。真正想引用某个观点时,脑子里只有模糊印象,却完全记不清它被“归档”到了哪座孤岛。Caveman 想解决的就是这个问题:给所有碎片信息一个统一的、极端简单的、永远属于你自己的存储位置。无论你是整天敲命令的程序员,还是习惯写长文的作者,只要你受够了笔记应用里层层叠叠的数据库、看板、模板和插件,这篇文章里的思路就值得看完。
1. 内容整体设计与思路拆解
1.1 为什么我会写一个“反现代”的笔记工具
现代笔记工具的问题是功能太多,而不是太少。Notion 的一个页面里能塞数据库、看板、公式、嵌入视频,但我常常只是想记一句“客厅灯坏了,要买 E27 灯泡”;Obsidian 有上千个社区插件,光是同步方案就能让人研究一晚上;语雀富文本排版干净,导出代码块却经常乱掉。功能堆叠带来的是决策成本:打开新笔记前要选模板、填属性、建双向链接,一套流程走完,灵感的火花早就灭了。
我想要的工具其实非常有限:在终端里敲一行字,存一条内容;过两三个月再敲两行字,能把它找回来;同时保证这些内容永远属于我,不存在某个随时可能停止服务的云服务器上。Caveman 把这一切拉回到了最原始的状态——没有登录、没有数据库、没有富文本、没有云同步。每条信息就是一个 Markdown 文件,用路径和 YAML 头部记录时间、标签、标题,正文就是你能想到的任何内容。它像穴居人的石斧一样,简单到让人觉得不太现代,但也可靠到丢了服务器也不会丢数据。
如果你觉得这个思路太极端,不妨看看主流工具的对比:
| 工具 | 数据存储 | 同步方式 | 离线可用 | 导出成本 |
|---|---|---|---|---|
| Notion | 远程数据库 | 官方同步 | 弱 | 逐页导出 |
| Obsidian | 本地 Markdown | 插件或云盘 | 强 | 极高 |
| Caveman | 本地 Markdown 文件 | Git / SyncThing | 强 | 零 |
表格里最刺眼的一行是“导出成本”。对一个以记录为生的人来说,数据一旦能自由迁移,工具的更替就失去了风险。Caveman 从设计上把“导出”变成了“打开文件夹”,因为存储格式本身就是最大众化的 Markdown,导出成本为零。
1.2 设计哲学:工具的复杂度应该由创建者承担
大多数产品设计都在做加法,而 Caveman 在刻意做减法。这个减法有明确的取舍逻辑:把复杂度沉淀在代码里,而不是暴露给用户。项目给自己定了四条原则。
第一条,命令不超过五个。add、list、get、drop、config,任何使用场景都能落到这五个动词上。第二条,存储格式在任何环境下都能打开。如果有一天这个工具停止维护,我依然能进入目录,用任意编辑器、grep、甚至是手机自带的文件查看器读取全部内容。第三条,不依赖任何网络服务,地铁上断网时记录照样能落盘。第四条,目录即索引。文件和目录本身就可以是人类可读的数据库,不需要额外的索引文件、缓存文件或锁文件。
这套原则在执行中会牺牲一些便利。比如没有全局搜索框的即时下拉提示,没有全文高亮,没有通过标签自动生成的关联图谱。但换来的是心智负担的骤降:我不再需要维护“笔记系统”,只需要维护“一个文件夹”。系统是脆弱的,文件夹是永恒的。用文件夹承载信息,几十年后依然可读;用某个专有数据库承载信息,服务一挂就是数字黑历史。作为一个喜欢囤积信息的人,我需要的就是这种最原始的踏实感。
1.3 技术选型:Python 标准库加文件系统
选型时我认真考虑过 Go、Rust 和 Node.js,最后仍然选了 Python,理由很实际。Python 标准库覆盖面足够大,几乎所有平台都能直接运行;开发周期短,核心功能一天就能写完;对于命令行工具来说启动速度完全够用。我在一万条笔记规模上实测,任何查询延迟都在 100 毫秒以内。如果将来性能真的扛不住,用 Go 重写也不会有什么迁移成本,因为存储格式就是一堆普通文件。
存储方面则坚持“零数据库”。最初我也犹豫过:要不要引入 SQLite?毕竟全文检索用 SQLite 的 FTS5 扩展会非常爽。但想了几轮之后,我放弃了这个念头。Caveman 的核心场景是“少量高价值信息的低成本存取”,而不是“海量文档的搜索引擎”。SQLite 再轻量,也是一个需要维护、备份、恢复的额外产物。如果改用“一个目录加几千个小文件”的原始方案,备份就直接复制文件夹,同步就直接交给 Git 或 SyncThing,任意文本工具都能处理。数据库带来的确定性优势,在这个场景下不足以抵消“文件即数据”带来的自由度。
2. 核心细节解析与实操要点
2.1 命令设计:只有五个动词
Caveman 的用户界面不是菜单,而是五个动词,对应完整的操作闭环:写入、浏览、查询、删除、设置。
add:记录一条笔记,可以从命令行参数、标准输入、临时文件三个来源获取正文。list:按时间或标签列出笔记,默认输出最近二十条。get:通过 ID 或关键词找到并查看一条或多条笔记。drop:删除笔记,默认移动进回收站而不是直接销毁。config:查看、修改配置项,比如编辑器路径、存储根目录、时区。
为什么是五个而不是六个?因为命令行工具最大的学习成本在于记忆命令名和参数。每多一个动词,用户就要多记一个分支,就要多一次“敲错命令再看帮助”的挫败感。export、stats、sync这类锦上添花的命令,我全都没做,而是引导用户直接在目录上使用外部工具。比如想要统计笔记数量,一条find ~/.caveman/notes -name "*.md" | wc -l就够了。
以add举例,最常用的快速记录方式是:
caveman add "客厅灯坏了要买E27灯泡" --tag home这条命令会在仓库里生成一个类似~/.caveman/notes/2024/06/15/1832_e27_bulb_reminder.md的文件。整个过程一秒内完成,不需要打开任何编辑器,不需要填任何表单。如果你想写更长的内容,可以加一个--editor参数:
caveman add "2024年Q3技术方案设计" --tag work --editor此时会调用你环境变量里的$EDITOR,通常就是 vim,保存退出后内容才正式落盘。我一直觉得,“短命令做快速捕获,编辑器做深度编辑”是命令行工具非常合理的分工。前者保障了记录时不被打断,后者保障了内容复杂时的编辑体验。
2.2 存储结构:用文件系统当时钟和分类器
Caveman 的仓库目录结构长这样:
~/.caveman/ ├── config.toml ├── notes/ │ ├── 2024/ │ │ ├── 06/ │ │ │ ├── 15/ │ │ │ │ ├── 1832_e27_bulb_reminder.md │ │ │ │ └── 2140_q3_tech_design.md │ │ │ └── 14/ │ │ │ └── 2301_meeting_notes.md ├── trash/ │ └── 2024/ │ └── ... └── hooks/ └── ...日期目录直接展开成年、月、日三层嵌套,天然支持按时间范围浏览。文件名以本地时间戳开头,保证同一目录内不会冲突;后面接一个英文短横线连成的 slug,方便肉眼识别内容,比如e27_bulb_reminder。
有人会问,为什么文件名里不用中文?我在 macOS 和 Linux 之间同步仓库时踩过坑:不同系统对中文文件名、某些特殊空格字符的规范化规则不一致,同步工具会把同一个文件识别成两个不同实体,导致莫名其妙的重复文件。所以文件名部分我统一用英文小写加短横线,中文标题和正文全部留在文件内部,由 YAML 头部记录。生成 slug 的逻辑也简单,只保留 ASCII 字母、数字、空格,把空格替换成横线,再转小写。
文件内容的格式是“YAML 头部加 Markdown 正文”:
--- title: 客厅灯坏了要买E27灯泡 tags: [home, todo] created_at: 2024-06-15T18:32:00+08:00 --- 客厅灯坏了,要买 E27 灯泡。问过房东,旧款也可以用 E27 替代。 顺便看一下门口的路由器电源,感觉也快挂了。为什么要用 YAML front matter?主要是为了让元数据既可以被程序解析,又完全不影响人类阅读。目录结构已经解决了时间维度,YAML 负责标题和标签,Markdown 正文只管内容。任何一个懂 Markdown 的人,都能直接编辑文件头部字段来批量调整标签,这不比任何数据库方案更直观吗?
2.3 检索逻辑:先过滤元数据再全文扫描
Caveman 的查询逻辑可以用四个字概括:先窄后宽。搜索时先通过目录名、文件名和 YAML 元数据把候选集合缩小,再对候选文件做正文匹配。
假设执行caveman get "灯泡",内部流程大致分三步:
- 遍历
notes/下所有.md文件,检查文件名里的 slug、YAML 里的 title 和 tags 是否包含“灯泡”,这类命中直接判定为高相关。 - 如果第一步命中的结果少于三条,再对所有候选文件解析正文,用普通字符串匹配做不区分大小写的扫描,把正文里包含关键词的文件加入候选列表。
- 结果统一按
created_at倒序输出,每行打印 ID、时间、标题、命中的片段。
这种策略有两个明显好处。一是性能优:文件名和元数据匹配只需要遍历文件名做字符串判断,速度远快于全文正则扫描;二是准确率高:如果你记得标题里的词,它可以精准定位;如果你只记得正文里的某句表述,兜底的全文件扫描也能捞出来。实测一万条笔记规模下,任何一次查询都不会让人有卡顿感。
这里藏着一个刻意为之的设计:默认不做索引缓存。没有索引,就没有“索引过期了需要重建”的烦恼,也没有“索引文件损坏导致记录丢失”的风险。对个人笔记这种低频写入、高频低频读取的场景,直接扫文件反而更稳妥。
3. 实操过程与核心环节实现
3.1 安装与初始化配置
安装方式尽量简单。作为 Python 项目,用源码目录安装最直接:
git clone https://github.com/yourname/caveman.git cd caveman pip install -e . caveman config --store ~/Documents/caveman-notes如果只是想快速试一下,也可以不安装,直接运行目录里的python -m caveman。首次运行会自动创建~/.caveman作为默认仓库,并把命令补全脚本写入当前 shell 配置。
我强烈建议用户在初始化后立刻做一件事:把~/.caveman目录初始化成 Git 仓库。理由后面在误删恢复章节会详细讲,但简单来说,对一个以文件为数据模型的工具而言,Git 版本管理是最自然、最强大的保险。
当前配置项一共有四项:
| 配置项 | 默认值 | 作用 |
|---|---|---|
store_path | ~/.caveman/notes | 笔记根目录 |
trash_path | ~/.caveman/trash | 回收站目录 |
editor | $EDITOR环境变量 | 长文本编辑时调用的编辑器 |
timezone | 系统时区 | 时间戳生成与输出显示时区 |
3.2 从零开始完成一次捕获、整理与回顾
我拿一个真实的工作日流程来演示,这样比任何说明书都直观。
捕获阶段,项目评审刚结束,我回工位写:
caveman add "项目评审会议纪要" --tag work --priority high --editor回车后 vim 打开一个预填了元数据的临时文件,我只需要把会议里几个关键结论写进正文,保存退出,捕获就完成了。整个过程没有切换窗口、没有网络同步、没有选择一个笔记放在哪个文件夹。
整理阶段,晚上回家后我会把当天零散的笔记批量补标签。因为数据就是纯文本,任何 Unix 文本工具都能直接参与处理。比如把所有笔记的标签数组里加上work标签:
find ~/.caveman/notes -name "*.md" | xargs sed -i 's/tags: \[/tags: [work,/'这个能力是“纯文件存储”的隐藏福利。数据库没法被sed、awk、rg直接处理,文件系统可以。我平时甚至会写一小段 Python 脚本,自动从一批笔记中提取出现过三次以上的关键词,生成标签建议。
回顾阶段,第二天早上看一眼昨日清单:
caveman list --since yesterday输出大概是:
[2024-06-15 18:32] 客厅灯坏了要买E27灯泡 [home, todo] [2024-06-15 21:08] 项目评审会议纪要 [work, high] > 核心问题:老接口兼容性、新方案上线时间、QA人员排期需要继续深入查看某一条时,用get 2按 ID 打开完整内容,或者get "兼容性"直接搜索正文。这套“列表里扫一眼,get 里看细节”的节奏,非常接近邮件列表的操作习惯,没有多余动作。
3.3 核心代码模块的设计骨架
Caveman 的代码量刻意保持克制,全部核心逻辑大约八百行。主体拆成三个模块:store.py负责文件读写,search.py负责检索排序,cli.py负责参数解析与命令路由。
store 模块的核心是一个 Note 数据类:
from dataclasses import dataclass from datetime import datetime from pathlib import Path @dataclass class Note: file_path: Path title: str tags: list[str] created_at: datetime body: str @property def note_id(self) -> str: return self.file_path.stem.split("_")[0] def to_markdown(self) -> str: meta = ( f"---\n" f"title: {self.title}\n" f"tags: [{', '.join(self.tags)}]\n" f"created_at: {self.created_at.isoformat()}\n" f"---\n" ) return meta + self.body.strip() + "\n"这里最关键的设计是note_id直接取文件名第一部分的时间戳。它既是 ID 也是时间信息,省掉了一个自增主键或者随机 UUID 的额外复杂度。用户不需要记类似a3f8...的随机串,只要知道“那条昨天晚上记的”大概就能顺着list找到。
search 模块的核心函数长这样:
def search(store_dir: Path, keyword: str, limit: int = 20) -> list[Note]: candidates = list(store_dir.rglob("*.md")) meta_hits, body_hits = [], [] for path in candidates: note = parse_note(path) if ( keyword.lower() in note.title.lower() or any(keyword in t for t in note.tags) ): meta_hits.append(note) elif keyword.lower() in note.body.lower(): body_hits.append(note) merged = sorted( meta_hits + body_hits, key=lambda n: n.created_at, reverse=True ) return merged[:limit]代码本身不复杂,但它清晰地表达了“元数据命中优先于正文命中,合并后按时间倒序”的策略。真正的生产环境里,我还会增加一个--cursor参数做分页,避免一次输出太多。
我始终不建议在这个项目里引入异步、多线程或者内存缓存。笔记工具最珍贵的品质是确定性:一次查询要么立即返回全部结果,要么明确告诉你没找到。异步优化会让超时、缓存击穿这些问题全部卷入一个原本只需要 800 行的项目,完全是自找麻烦。
3.4 与日常工具链的组合实践
Caveman 的价值有一半要靠和已有工具链的组合来发挥。
用 Git 做自动版本备份,在 crontab 里加一行:
0 2 * * * cd ~/.caveman && git add -A && git commit -m "daily backup" >/dev/null之后每天凌晨自动提交一次。所有笔记天然拥有版本历史,误删文件后可以用git log找回任意历史版本。我实际使用下来,这种“每个文件都是可 diff 的文本”加上 Git 的结构,对普通笔记来说已经远超绝大多数云笔记的还原能力。
用 fzf 做交互式快速选择。执行caveman list --all | fzf | awk '{print $1}' | xargs caveman get --id可以用一个 shell 别名封装起来,直接在模糊搜索列表里选中并打开指定笔记,体验完全不输给图形化笔记应用。
定时回顾则更简单:cron 每天早九点执行caveman list --since yesterday,输出重定向到$HOME/.caveman/daily-digest.md,配合系统的通知服务,变成一个私人的每日摘要。方法原始,但比大多数待办应用自动生成的假提醒可靠得多。
4. 常见问题与排查技巧实录
4.1 中文内容乱码或者搜不到中文
最早版本里我犯过一个典型错误:解析文件时没有显式指定 UTF-8 编码。Linux 下默认通常是 UTF-8,一切正常;但 macOS 上某些 locale 环境下,open()的默认编码可能是 ASCII,读取中文文件直接抛异常,写入时则会把中文变成乱码。
修复方法是在所有文件读写处统一指定编码:
open(path, "r", encoding="utf-8") open(path, "w", encoding="utf-8")另外,YAML 标签里如果混入了中文逗号“,”或全角冒号“:”,解析时很容易被当成普通字符串的一部分,导致标签体系悄悄分裂。我在解析前加了一个规范化函数,把所有全角标点统一替换成半角标点再切分。搜索中文时同样要注意,全文匹配用str.find或in就足够,不要用正则里的\b词边界,因为\b对中文完全无效,会把“灯泡”拆成两个永远匹配不到的片段。
4.2 笔记数量暴涨之后,全量搜索开始变慢
我在一台较老的笔记本上实测:两万条笔记、五万多个文件时,全量rglob加正则匹配大约耗时 1.2 秒。这个速度绝大多数人可以接受,但对每天记几十条的重度用户来说,确实会感受到明显的停顿。
优化方案有几档,按需使用。第一档是缩小扫描范围:命令支持--path 2024/06这类路径过滤,直接把扫描范围缩小一个数量级。第二档是给标签建一个简单的索引文件tags_index.txt,启动时先加载到内存,按标签过滤之后再决定是否需要全文扫描。第三档才是真正引入 SQLite 做全文索引。但就个人笔记场景而言,绝大多数人的数据量根本到不了第三档。先检查搜索时是不是把.git目录、备份图片等二进制文件也纳入了扫描范围,光是这一步排除,速度往往就能翻一倍。
4.3 误删笔记后的恢复手段
Caveman 的drop命令默认不真正删除文件,而是把文件移动到trash目录,并保留原日期目录结构:
caveman drop 3 # 实际执行:mv notes/2024/06/15/1832_e27_bulb_reminder.md trash/2024/06/15/恢复误删笔记最简单的方式,就是用文件管理器把文件从回收站拖回原目录。如果要彻底清空回收站,再执行caveman drop --purge。
如果已经 purge 了,就只能靠 Git 历史。仓库初始化时如果配了每日自动提交,任何时候都能用:
git log --all --diff-filter=D --summary git checkout <commit>^ -- notes/把被删除的文件恢复到指定提交点。这个设计背后是一个理念:数据的可恢复性,比删除操作本身更值得投入设计精力。与其做一个能瞬间物理删除的命令,不如让“删除”变成“移入另一个目录”,把最终决定权留给用户。顺带一提,我在写这个功能时踩过一个坑:drop --purge清空回收站前应先警告并等待三秒确认,否则一次手滑,后悔药就得靠 Git 才能吃上。
4.4 多设备同步的冲突处理
同步方案我推荐 Git 或 SyncThing 二选一。Git 适合能接受手动 push、pull 的极客,SyncThing 适合希望全自动同步的用户。
用 Git 时,冲突问题基本只出现在两台设备同时新建了文件名完全一样的笔记。由于 Caveman 的文件名以时间戳开头,同一秒内撞名的概率极小;最常见的冲突反而是“设备 A 改了某个文件,设备 B 也改了同一个文件”。解决方式很原始:让 Git 强制以最后提交方为准,或者人工合并。因为笔记文件通常不大,合并成本极低。
SyncThing 的冲突处理会生成形如文件名.sync-conflict-20240615-183200的副本文件,不影响正常阅读,只是偶尔要手动清理。我的建议是:家里台式机和笔记本用 SyncThing,手机想临时查看笔记,直接把notes目录放进任意云盘同步盘即可。因为文件格式足够通用,所有同步服务都能处理。
常见问题的速查表我整理在下面:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 中文显示乱码 | 文件读写未指定 UTF-8 | 显式指定encoding="utf-8" |
| 中文搜索不到 | 使用\b正则词边界 | 改用in或find匹配 |
| 搜索速度慢 | 扫描了.git或二进制文件 | 排除隐藏目录,按月份路径过滤 |
| 误删除后找不回 | 使用了物理删除 | 用drop目录内部移动,配合 Git |
| 多端同步出现重复文件 | 中文文件名规范化差异 | 文件名统一用英文 slug |
| 标签体系混乱 | 全角逗号混入标签 | 解析前统一替换全角标点 |
4.5 “命令行工具用不惯”的补救方案
总有人觉得命令行门槛太高,Caveman 也没有把这些人拒之门外。因为所有笔记都是普通 Markdown 文件,完全可以用 VS Code、Obsidian 或 Typora 直接打开notes目录浏览编辑。
更推荐的做法是:把仓库目录作为“文件型笔记库”纳入现有工作流。它在 Obsidian 里就是一个 vault,标签、双链都能正常使用;在 VS Code 里就是一个文件夹,导航树一目了然;在命令行世界里,它又是为懒得打开 IDE 的场景准备的快速入口。三种入口之间切换不会有任何格式冲突,因为本质都是“编辑器读取目录里的文本文件”。我自己的电脑上,三个入口平时都会用:终端记短内容、Obsidian 翻阅整理、VS Code 做批量编辑。
5. 实测半年后的心得与可能的进化方向
5.1 它治好了我的笔记焦虑,但局限也很明显
用了大半年,Caveman 最大的收获不是“功能强大”,而是“记录的心理门槛被降到极低”。以前用 Notion 时,我总想着把一条笔记放进哪个数据库、用什么属性、加不加封面图,犹豫之下干脆就不记了。Caveman 几乎没有决策成本:敲一行命令,或者打开编辑器写几句话,记录本身变成了肌肉记忆。
局限也同样明显。它不适合做多人协作知识库,没有评论、权限、实时协同;不适合嵌入大图片、表格和音视频;搜索算法也没有做语义优化。换句话说,它是一个效率型工具,而不是承载型平台。作为“第二大脑”它稍显朴素,但作为“快速把口袋里想法转成可读文件”的管道,异常称职。如果我的需求变得重协作、重多媒体、重智能关联,我也会老老实实回到 Notion 或者 Obsidian 的怀抱,而不是硬把 Caveman 改装成它本来不是的东西。
5.2 如果继续演进,我会优先做这几件事
第一,支持标签自动联想。在add命令输入--tag时,根据已有标签做模糊补全,避免标签体系悄悄分裂成todo、TODO、待办好几个变体。第二,增加一个export子命令,把若干笔记打包成一个单文件 Markdown 或 PDF,方便直接发给别人。第三,做一个“每日回顾邮件”能力:每天把昨天新写的笔记按模板渲染成摘要发给自己。这些都是加法,但每个功能都围绕“让信息的写入和取出更顺滑”这个核心,不会为了追赶潮流加入看板、日历、脑图之类的东西。
最后还有一件我更想做的事:给手机做一个快捷入口。手机浏览器里打开一个本地页面,输入框直接把内容 POST 给本机运行的 Caveman HTTP 接口,碎片想法就能进仓库。不用开发 App,只用 Web 标准,这依然是“原始哲学”的延续。
最后说说这个项目名吧。Caveman 不是自嘲,而是我心里对工具的一种理想状态:信息管理应该像原始人用石斧一样直接——拿起来就能用,放下就能走;不华丽,但足够可靠。如果你现在正在几个笔记应用之间反复搬家,也许可以给自己一个周末,把这套思路照着实现一遍。就算一行代码都不写,仅仅用“文件夹加 Markdown”这个习惯,也已经走出了大半步。剩下的,就是让这些文本文件真正成为你日常记录的一部分。