资讯动态

caveman:一个Go语言实现的本地纯文本命令行笔记工具

发布时间:2026/10/29 22:46:05 来源:尧图企业网站定制
大约一年前我在整理自己的知识库时发现一个尴尬现象工具越换越勤笔记却越来越少翻。折腾过各种知名笔记软件每一个都把“收集”做得很好看把“找回”做得很难受。于是我用 Go 写了一个命令行工具起名 caveman。名字很直白——我希望它像原始人在石壁上刻记号一样不依赖数据库服务不强制联网不搞花哨界面一条命令写进去一条命令找回来。caveman 解决的是个人知识管理里最朴素的一环把临时念头、工作备忘、技术片段、账单待办以普通文本文件的形式落在本地目录用最少的命令完成“存、查、改、销”。它适合谁呢我觉得主要是三类人在终端里待一天的高强度工作流用户坚持本地优先、纯文本先行的数据洁癖者以及想从零理解一个存储工具该怎么做架构取舍的开发者。1. 为什么叫“caveman”从云笔记疲劳到本地极简设计1.1 云笔记的隐形代价先说说我为什么不愿意继续用云笔记。市面上大多数笔记产品核心逻辑是“先富集再消费”这本身没问题。但我用下来发现当笔记数量超过两千条之后我的查找路径不是变短了而是变长了。举个例子。我用某款云笔记记录工作日志时标题里如果带日期经常被自动解析成日程提醒想按文件夹整理又会被自动分类功能打乱。最让我受不了的是导出环节有一次想迁移全部内容导出的 Markdown 文件里出现了大量重复引用块表格也全变成了 HTML 源码。那一刻我突然意识到我记下来的内容并不属于我它属于那个平台的渲染链路。当然云同步在多人协作时确实方便但个人的轻量知识管理根本不需要实时同步。我需要的是临时记一笔、长期翻得到并且万一哪天不想用这个工具了整个数据目录拷贝走就能用。这个需求实际上非常原始原始到大部分现代产品都绕了远路。1.2 caveman 的三个设计信条为了不再重复踩坑我给 caveman 定下了三条硬规矩这三条也是整个项目所有功能取舍的底层标准。数据即文件文件即数据。每一条记录就是一个独立的.md或.txt文件放在明确的目录里。没有数据库导出问题没有平台锁定。哪怕 caveman 二进制明天不维护了我几年份的笔记仍然是几百个能直接打开的文本文档。零后台进程。命令执行完就退出不驻留、不常驻内存、不偷偷后台检查更新。触碰它最多消耗几十毫秒的 CPU 时间这对“随手记一笔”是必要的轻量感。网络永远是可选项。默认所有数据都落在本地跨设备同步完全交给 Git、rsync 或者用户自己的同步盘。这样既没有账号体系需要维护也避免了“云端数据悄悄被算法加工”这类不可控风险。这三条信条听起来简单真正落地时却能解决大量争吵。比如有人问我“为什么不在搜索时接入 AI 自动打标签”答案就是那会引入模型依赖违反数据即文件的可读性。这类拒绝比功能设计更重要。2. 存储层设计一块“石壁”如何组织信息2.1 仓库目录结构caveman 初始化后会在用户目录下创建一个类似这样的数据仓库~/.caveman/ INDEX.json notes/ 2025/04/12.md books/高效能人士的七个习惯.md recipes/葱油拌面.md bills/ pending/2025-04-煤气费.txt done/2025-03-电费.txt ideas/ project-x-v2.md看这个结构你会发现caveman 其实只有一个默认的根目录里面按“类别”分文件夹类别里再按你的习惯决定是建日期子目录还是命名子目录。INDEX.json是唯一需要维护的索引文件它记录了每个文件的标题、标签、创建时间、修改时间和分类。我刻意让目录结构保持“即使没有索引也能人肉翻到”的程度。比如某天INDEX.json意外损坏了你仍然可以顺着notes/2025/04/这个路径找到当时记的东西。这是文本文件系统最大的保险——索引是加速器不是唯一入口。2.2 为什么用 JSON 索引而非数据库很多第一次接触源码的朋友会问为什么不用 SQLite这问题很合理。我在项目早期确实用过 SQLite但后来主动去掉了原因是SQLite 存储的文件是二进制格式用户无法直接在编辑器里打开修改同时对于几千条量级的笔记SQLite 的查询能力属于大炮打蚊子。我自己做过一次量化测试在普通笔记本上两万条短文本记录生成的 JSON 索引大约 3MB解析耗时大概在 15ms 到 30ms。这对“敲一条命令找结果”的场景来说几乎是无法感知的延迟。相比之下引入数据库意味着我要额外设计 schema 迁移、处理并发连接还要给用户解释为什么数据目录下多了一个几十 MB 的文件。所以caveman 的索引写入策略是这样的每次写入或者修改记录时先把新索引写入同目录下的临时文件然后再用原子替换的方式覆盖旧的INDEX.json。这样即便写入中途断电最多丢失最后一条操作的索引不会出现截断的半份文件。func atomicReplace(path string, data []byte) error { tmp : path .tmp if err : os.WriteFile(tmp, data, 0644); err ! nil { return err } return os.Rename(tmp, path) }这里的关键是os.Rename在同一文件系统下是原子操作。很多人自己写工具时会忽略这一点直接os.WriteFile覆盖原文件万一进程崩了索引文件就成了一半所有数据看起来都像丢了其实只是索引坏了。2.3 文件散落策略与冲突处理因为每条记录都是独立文件天然需要处理“同名冲突”和“多人多设备写入冲突”的问题。我采取的策略是默认用“用户给的标题”作为文件名如果发现目标文件名已存在则在标题后面追加短横线和五位随机短码。这样既不打断用户的命名习惯又不会因为重名而覆盖旧内容。在多设备场景下如果两台电脑同时往仓库里写文件最终通过 Git 合并时可能出现冲突。这其实是文件方案相较于数据库的优势冲突至少是可见的、可手工解决的而不是被数据库的写入锁机制静默吞掉。把冲突暴露出来远比让用户在不知情时丢失某条记录更安全。3. 命令行交互逻辑把常用操作压缩成肌肉记忆3.1 核心命令一览caveman 的交互设计遵循一个原则动词开头对象其次参数最后。所有高频操作都不需要翻文档直接用直觉就能完成。命令用途典型示例caveman add写入一条短记录caveman add daily 完成注册流程重构caveman note创建长文笔记caveman note books/高效能人士的七个习惯 今日读至第三章caveman find全文检索caveman find 煤气费caveman peek查看某时间段记录caveman peek --since 2025-01-01 --until 2025-03-01caveman done完成一项待办/账单caveman done bills/pending/2025-04-煤气费.txtcaveman tags列出所有标签及计数caveman tagscaveman restore从回收站恢复文件caveman restore notes/2025/04/08.md高频操作实际上只有三个add、find、peek。find做的是子字符串匹配还会顺带把标签匹配的结果排前面。peek主要用于回顾——每天打开终端时回看昨天写了什么比打开任何仪表盘都直接。3.2 参数解析的取舍Go 生态里有很多好用的命令行框架例如 Cobra。但我故意没有用。原因很简单caveman 的命令数量不到十个用 Cobra 会让每个子命令挂一大堆标准参数模板反而压住了我想要的那种“原始工具”的清爽感。我用的方式是解析第一个参数作为子命令名后面的参数手动解析。这样看起来有点“笨”但对于固定模式的小命令非常合适。更重要的是这条路径让我能轻松支持管道输入。比如echo 记得取快递 | caveman add daily这样我不需要额外解释复杂的交互式编辑方式也不需要在add命令里内置文本编辑器。管道输入是 Unix 哲学里最自然的输入方式任何使用终端的人都能立刻理解。一个值得注意的体验细节是当用户执行caveman add时如果标题为空程序会先判断 stdin 是否有关闭如果 stdin 有内容就直接读取如果没有内容就提示用户输入而不是默默写入一个空标题。这个小校验成本极低但能避免产生大量无意义的空记录。3.3 输出设计只回答两个问题caveman 的命令输出默认不做花哨排版。每行回答两个问题这条内容是什么在哪个文件里[2025-04-12 09:41] daily | 完成注册流程重构 ~/.caveman/notes/2025/04/12.md同时提供--json选项方便二次加工caveman find 账单 --json输出 JSON 的好处是任何人都可以用 jq 或者其他脚本工具做进一步处理。我不主动强调这个功能但它天然存在。写完命令后如果执行成功只在终端里输出一行绿色提示如果失败输出黄色警告。很克制但是足够确认操作结果。4. 真实使用场景与工作流整合4.1 每日站会前的快速检索我现在的日常工作流里最依赖 caveman 的场景是每天的晨间回顾。公司上午开站会每个人要简短说昨天做了什么。以前我总要从聊天记录和邮件里回忆线索现在只需要敲一行命令alias yesterdaycaveman peek --since yesterday然后屏幕上就会列出昨天的所有daily类型记录。这个命令我已经用了快一年几乎变成了肌肉记忆。每天早上的第一件事不是打开邮箱而是先跑一次yesterday用五分钟把昨天的工作脉络重新接上。我发现这种简单的“回看”比任何项目管理软件的统计报告都有效因为它记录的是我当时的原始视角而不是完成任务后的美化总结。4.2 把它绑进 Git 做本地版本控制caveman 数据本来就是纯文件所以版本控制特别顺。我用一个简单的脚本实现每日自动快照#!/bin/bash cd ~/.caveman git add -A git commit -m daily snapshot $(date %Y-%m-%d)然后把脚本挂到 cron 里0 2 * * * /home/user/bin/caveman-snapshot.sh凌晨两点自动提交一次相当于给数据上了一道“时光保险”。配合 Git 的历史记录我能看到任何一天的版本。这套方案的优雅之处在于版本管理不归 caveman 管它只是忠实地把数据留在原地剩下的交给标准工具。有人可能会问 Git 对大量小文件效率不高怎么办。这个担忧是合理的但个人笔记数量级本身有限一般不会超过几万个文件Git 完全扛得住。如果真的出现了性能瓶颈还可以简单切换到按年月拆多个仓库的做法数据依旧可读。4.3 定时任务自动归档第三个高频场景是账单“销项”。我把生活中的待办账单都记成bills/pending/下的文件比如煤气费、电费、保险续费。每个月收到缴费通知就caveman add bills/pending 4月电费缴费完成后执行caveman done bills/pending/2025-04-电费.txt这个命令会把文件移动到bills/done/并在原文件中追加一行完成时间和备注。如果你比较“数码宅”还可以定义一条周期性的 at 任务0 9 1 * * caveman done --older-than 30d --choose-longest-pending这不是 caveman 内置的参数而是我脚本里写的逻辑每月一号自动把超过 30 天最旧的待办完成。这类自动化完全不需要 caveman 提供插件体系因为文件就是数据任何外部程序都能操作。对用户来说这意味着“工具可以被任意改造”而不必等待上游功能发布。5. 踩坑记录与修复过程5.1 中文文件名在跨平台同步时的编码差异caveman 早期版本直接用用户输入的标题作为文件名结果在跨平台同步时栽了大跟头。macOS 的文件系统默认使用 NFD 编码也就是把“中文”这类字符拆成基字和修饰符组合Linux 大多使用 NFC 编码会把它们合并成一个字符。同一份仓库在 Mac 上提交、在 Linux 上拉取Git 会出现同一目录下有两个名字看起来一模一样、但二进制编码不同的文件。这个坑非常隐蔽你肉眼完全看不出区别但在git status里却一直显示有未提交的改动。我当时的解决办法是在写入时不再直接使用用户标题作为文件名而是做一次统一的 slug 化处理。所谓 slug 化就是把标题里的空格、斜杠等特殊字符替换为短横线同时保留中文本身另外在INDEX.json里额外存一个display_name字段保存原始标题展示和查找时用原始标题文件系统里用规范化后的名字。func slugify(s string) string { s strings.ToLower(s) s strings.Map(func(r rune) rune { if unicode.IsLetter(r) || unicode.IsDigit(r) { return r } return - }, s) return strings.Trim(s, -) }这个改动虽然小却解决了两个问题第一跨平台文件名稳定不会再出现编码差异导致的重复第二文件名里不会出现/这类目录分隔符用户再怎么写标题都不会意外创建多层目录。5.2 文件数量增大后索引读取变慢的定位与优化最开始设计时caveman 每次执行命令都会把整个INDEX.json加载进内存。当笔记量只有几百条时完全没问题但测试中我塞入了大约两万条记录情况立刻糟糕了任何命令都要等 1 秒以上才开始输出结果。虽然 1 秒在绝对时间上不算长但命令行工具一旦让人感到“卡顿”使用意愿会直线下降。我做了 profiling发现瓶颈主要是 JSON 的反序列化开销。优化方案不是引入 SQLite这是我始终回避的方向而是将索引改成“分桶”结构按月份生成独立的索引块例如INDEX-2025-04.json、INDEX-2025-03.json同时保留一个轻量的INDEX.json用来存放各月份的元数据和文件路径映射。查询时默认只加载当前月和上一个月的索引只有当用户明确指定更早的日期范围时再去加载对应月份的索引块。这个策略把启动时间降回到了 50ms 以内而且对用户没有任何感知差异。如果你也想做类似工具遇到性能问题我建议先考虑“能不能按时间分块”而不是马上寻找重型数据库。5.3 误删数据与恢复策略caveman 最初是没有“撤销”概念的直到我自己在一次整理笔记时手滑执行了删除而且删的是整整一个分类目录。那一刻我意识到任何直接删除文件的操作都是不可原谅的。现在 caveman 的所有破坏性操作都遵循“先回收后清理”的机制删除文件时不是os.Remove而是移动到~/.caveman/.trash/目录。INDEX.json中对应的记录会被标记为deleted: true但保留三十天。提供一个caveman restore命令可列出回收站并恢复任何一条记录。只有执行caveman clean --force才会真正清空回收站。这条规则看似简单但救过我两次。特别是当你长期积累了大量笔记后误删的损失已经不是几条内容的问题而是“让数据消失”带来的信任感崩塌。工具可以不花哨但必须在危险操作上有默认可防御的机制。6. 为什么不用大而全的方案caveman 与主流工具的定位差异6.1 与主流笔记工具的直观对比我经常被问到一个问题“Obsidian 也是本地 Markdown为什么不直接用它”两个人使用工具的习惯差异很大这里直接用表格说明定位差异可能更直观。维度ObsidianNotioncaveman纯文本文件夹启动速度需要加载应用仓库存量越大越慢Web 应用依赖网络毫秒级 CLI取决于编辑器离线能力本地为主但插件生态复杂依赖网络完全本地完全本地数据可读性Markdown但属性块有工具依赖导出格式不完全一致纯 Markdown/纯文本任意格式自动化能力需要写插件或调用本地 API依赖官方 API 权限任意脚本可直接读写文件任意脚本协作能力较弱强无无学习成本中高中极低低Obsidian 适合知识网络和双链思考Notion 适合团队信息组织。caveman 的定位更接近“一支随时可以摸到的笔”你想快速记一条、想从终端里检索、想把数据完全握在自己手里那就用它。它永远不会是第二个 Notion也不应该是。6.2 什么时候你不该用 caveman诚实地说caveman 有几个明确不适合的领域。如果你需要多人实时协作编辑同一篇笔记请别用工具硬凑如果需要复杂的富文本表格、嵌入图片和动态图表纯文件模式会让你写得很痛苦如果你对于“打开笔记软件能看到精美周报视图”有执念caveman 给不了你这些。我见过一些用户试图给 caveman 写大量辅助脚本最后反被脚本维护拖累。工具本身是一条窄路重点是它足够直。在接入一个工具之前先明确自己的核心工作流是什么总比看到什么新功能就往上挂更靠谱。用 caveman 一年多我最大的体会是少即是多不是一句空话而是要在每个设计决策里都能兑现的承诺——记录的过程应该短到让人愿意记录存储的方式应该简单到让人不担心丢失。这样的工具虽然小但用起来极其踏实。如果你也想做一个类似的东西我建议一开始就列一张“绝不做什么”的清单它比功能清单更能帮你守住边界。对我来说caveman 就是那个让我愿意每天主动去写两句的“石壁”。

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

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

免费获取报价 →
↑