YAOTU INSIGHTS

caveman:用Go打造的极简本地优先命令行笔记工具

caveman:用Go打造的极简本地优先命令行笔记工具
很多人问过我caveman 这个名字到底是什么意思。是谐音梗还是某个科幻电影的彩蛋其实都没那么复杂——我做完这个极简工具脑子里第一个蹦出来的词就是“穴居人”。它剥掉了所有现代软件爱堆上去的花架子回归到一种最原始、最直接的使用状态没有云端、没有账号系统、没有臃肿的图形界面你本地敲一句话它给你一个干净利落的结果。这年头还能让人自愿这么“返祖”的东西不多了。caveman 本质上是一个极简的本地优先命令式工具用最简单的方式帮你完成信息的记录、索引与检索。它不联网、不弹广告、不存你的隐私到任何第三方服务器所有数据都安安静静躺你自己硬盘上。它解决的核心问题是很多人在重度依赖“云笔记”“在线文档”之后突然意识到的那件事我记的东西凭什么要经过别人的服务器才能读到我要是断网了我的笔记怎么办这台电脑换成另一台我的记忆碎片迁个移怎么这么费劲这篇文章就是围绕我如何从零设计并实现这样一个“穴居人”工具的完整复盘。适合对极简开发感兴趣、想动手做一个本地优先小工具的人也适合单纯受够了各种“全家桶式”软件、想找回一点掌控感的普通用户。我会尽量把设计取舍、核心代码逻辑和实操过程中踩过的坑一次讲清楚你可以直接照着抄作业。1. 整体设计思路从“为什么需要”到“怎么做到极简”1.1 为什么放弃云笔记选择本地优先在做 caveman 之前我前前后后用过不少主流笔记和知识管理工具。功能确实一应俱全多端同步、富文本、标签、引用、团队协作……但用久了总有一种说不上来的不对劲。比如有时候在信号不好的地方想查一个以前记过的接口文档App 直接给我一个加载失败再比如某天我突然想搜索一条三年前记的小贴士结果发现免费版只支持搜索到最近几个月的内容再早的得付费解锁。这些体验堆在一起逼我开始重新思考一个问题我的数据到底属于谁答案是显然的数据应该只属于我自己。云笔记确实方便但它把数据的存储、检索、甚至能不能离线读取的权利都握在了平台手里。本地优先Local-first这个概念这几年慢慢被更多人接受核心逻辑其实也很朴素你的数据生来就该住在本机能直接被文件系统管理、能被命令行调用、能在任何文本编辑器里打开而不是被关进某个数据库的黑盒里。caveman 就是奔着这个思路去的。我不需要同步需要同步我可以自己用网盘同步那个文件夹更透明不需要多人协作这是极少数人才有的需求也不需要花里胡哨的所见即所得排版我记笔记是为了内容本身不是给自己看主题皮肤。我需要的就是一条命令把内容写进去一条命令把内容翻出来快、稳、不过度设计。值得一提的是这类“回流”并不是倒退回石器时代。我依然用现代工具链只是把网络的耦合切断把复杂度的开关关掉。更像一种主动的选择在什么都想连网的时代保留一块只有自己说了算的地方。1.2 核心原则去掉每一个“不必要”如果你要做一个叫 caveman 的东西最容易犯的错误就是把它做成一个“长得像穴居人”的普通应用——界面搞得原始风、字体用手写体本质上还是臃肿软件。那就完全跑偏了。真正的极简不是界面风格复古而是交互模型和代码复杂度都尽可能低低到不能再砍。我给自己定了三条硬性设计原则。第一单文件交付。所有代码编译完只产出一个二进制文件没有依赖安装脚本、没有配置文件模板、不需要中间运行时。用户下载这个文件第一步直接就能跑。为什么这么较真因为现代软件最烦人的一点就是环境依赖。让我装 Java 运行时再加一堆 JAR 包的事我拒绝发生在自己项目里。第二数据格式人人可读。caveman 存储数据不使用专属二进制格式而是直接用 Markdown 纯文本加一个极小的索引头。为什么因为纯文本永远不会背叛你。十年后可能有各种软件倒闭、各种数据库格式作废但一套存放在本地、符合基本规范的 Markdown 文件任何一台电脑上用记事本都能打开。数据格式的可迁移性就是长期主义的核心保障。第三交互方式遵循 Unix 哲学。每个命令只做一件事输出要能被管道和脚本进一步处理。比如caveman list输出纯文本列表caveman find 关键词直接把匹配到的条目内容打到标准输出。这样意味着 caveman 可以很自然地和 grep、awk、cron 等原生工具组合使用它不是一个孤岛而是你命令行工具箱里的一员。这三条原则听起来简单但真正贯彻起来需要一次次和“加个功能吧”的冲动做斗争。以做减法为核心才能把东西做得小而美。2. 核心细节与实现方案语言、存储与检索2.1 编程语言的选择Go 的务实理由关于实现语言我几乎没有犹豫就选了 Go。可能有人会问发笔记工具用 Python 不是更快吗用 Electron 不是界面更好看吗我的理由很简单。上面说了最终交付物必须是一个不依赖任何运行时环境的单文件二进制。Go 恰好能以极高的编译效率达成这一点交叉编译也方便我在 Linux 上可以直接编译出 Windows 和 macOS 的可执行文件这对想在不同平台用的朋友是妥妥的利好。Python 脚本虽然写起来快但是要用户装解释器、装 pip 包这个门槛直接把“穴居人”变成“贵族城堡”了。而 Electron 光是一个运行时就得二百多兆和 caveman 追求的方向完全相反。当然用 Go 也得承担一些代价。比如 GUI 开发难度大但这正好帮我强制自己放弃 GUI回归命令行本质。又比如反射和接口的灵活性不如动态语言但我的核心逻辑并不复杂用静态类型反而帮助我在编译阶段就拦截了很多低级错误。权衡之后这个选择是值得的。2.2 存储结构Markdown为主JSON索引导航caveman 的数据目录结构非常简单默认是在用户家目录下的.caveman/文件夹里~/.caveman/ ├── notes/ │ ├── 2025-01-15-1352-买硬盘注意接口.md │ ├── 2025-01-16-0901-咖啡手冲参数.md │ └── ... ├── tags.json └── config.jsonnotes/文件夹里每一条记录就是一个 Markdown 文件文件名自带时间戳和简短的标题摘要。这样设计有一个很实际的好处即使 caveman 本身崩溃了、数据库文件损坏了我依然能在资源管理器里看到那一堆文件名带日期的.md文件手动打开任何一个都不受任何影响。但纯文件系统也有个短板按标签检索的效率太低了。如果每次找标签都要遍历整个目录几百个文件每次搜索都要重新读所有内容那速度再快也会被嫌弃。所以我加了一个tags.json相当于轻量级索引。这个索引文件结构大概长这样{ 咖啡: [ 2025-01-16-0901-咖啡手冲参数.md, 2024-11-03-2111-摩卡壶煮奶泡.md ], 硬盘: [ 2025-01-15-1352-买硬盘注意接口.md ] }每次写入新笔记时caveman 会同步更新这个 JSON 索引。标签系统完全靠用户写笔记时在内容头部声明没有强制规则。索引文件有损坏风险有但重建它只需要一条命令扫描所有 Markdown 文件头部的元信息最多几秒搞定。这也延续了整体理念所有自动化结构都要能被“重新生成”这样就不怕损坏。2.3 全文搜索先简单后进阶检索是知识管理工具的灵魂。caveman 的搜索逻辑我分了两层来处理。第一层是基础的子串匹配用于日常快速过滤。比如我执行caveman find 手冲它会读取tags.json看有没有这个标签有就直接列出对应文件没有就在notes/里做一次线性扫描并把包含关键词的文件名列出来。因为纯文本体积通常不大本地扫描几百个文件的时间几乎可以忽略不计“线性扫描”这个最粗暴的方案反而是性价比最高的。第二层是给有更高需求的人准备的。caveman 提供caveman export命令可以把所有笔记集中导出为一个纯文本文件方便你用 ripgrep、ag 等更专业的全文检索工具去搜。这本质上不是逃避责任而是刻意保持工具边界的清醒基础搜索我自己做重度全文检索有更专业的人做我保证不锁死数据你随时可以拿走自己玩。对大多数用户的日常使用来说第一层线性扫描完全够用了。实测在 5000 条笔记、平均每条 1KB 的规模下一次全量关键词扫描耗时在几十毫秒到一百毫秒之间体感就是“嗖一下”。3. 实操过程手把手从安装到日常使用3.1 一分钟安装单文件真的可以安装过程极简你只需要去 Releases 页面下载对应你系统的二进制文件就行。以 macOS 为例下载后把它放到/usr/local/bin目录赋予执行权限mv caveman-darwin-amd64 /usr/local/bin/caveman chmod x /usr/local/bin/caveman caveman --help看到帮助信息输出说明安装成功。这里有一个可以分享的小心得macOS 用户如果遇到“无法打开因为来自身份不明的开发者”这类提示不要急着去系统设置里点“仍要打开”更稳妥的做法是执行一次xattr -d com.apple.quarantine /usr/local/bin/caveman把隔离属性去掉。不同的发布环境对这个属性的处理有一定差异所以提前知道这个小命令能少走很多弯路。Windows 用户更简单直接下载.exe文件后放在某固定目录用 PowerShell 调用.\caveman.exe即可。要是想让全局都能直接敲caveman把那个目录加进 PATH 环境变量就行。3.2 初始化仓库准备穴居人的洞穴安装只是第一步紧接着是初始化一个数据仓库相当于给穴居人开辟一个山洞来存货caveman init该命令会在当前用户下创建~/.caveman/目录结构并写入一个默认的config.json。你完全可以选择把数据仓库指定到某个专门的数据盘或网络同步盘比如caveman init --dir /Volumes/MyData/caveman-notes这个参数的意义很实际如果你本身就使用 Dropbox、坚果云这类第三方同步盘做宏观备份那直接把 caveman 的仓库放进去等于在不写任何同步代码的前提下获得了全平台多端同步能力。本地优先和数据同步并不冲突只是把同步机制放在了底层而不是由某一个笔记软件自己绑架你。这是很多设计里容易忽略的一个点工具提供标准格式和目录你可以自由把它放进任何你认为可靠的同步基础设施里。初始化之后config.json内容默认是这样的{ dir: /Volumes/MyData/caveman-notes, default_tag: misc, editor: vim }default_tag是你没指定标签时自动挂载的兜底标签editor字段允许你在执行命令时指定用什么编辑器快速修改笔记正文。对于习惯用 VS Code 的人可以把editor改成code对于 vim 党、nano 党各取所需。3.3 创建笔记随手一记不分心记一条笔记的命令非常朴素caveman add 买硬盘注意接口类型默认情况下这条命令会创建一个带当前时间戳的文件并把标题写入文件正文然后调用你配置的编辑器默认 vim打开文件让你补充细节。如果你不想进编辑器只想快速记一句话可以用--quick参数caveman add --quick 今天测试了Go 1.22冒泡排序性能比想象中快这不会弹出编辑器直接把内容写入文件并退出非常适合临时记录灵感。命令执行完会在终端打印创建的文件路径方便你后续核对✔ created: /Volumes/MyData/caveman-notes/notes/2025-06-18-1422-买硬盘注意接口.md带标签的记录则长这样caveman add 咖啡手冲参数记录 --tags 咖啡,手冲,笔记文件内容的头部会像这样--- tags: [咖啡, 手冲, 笔记] created: 2025-06-18 14:22:00 --- # 咖啡手冲参数记录 研磨度中等偏细 水温92℃ 水粉比1:15你没看错这个文件在 Markdown 前面多了一个 YAML-style 的元信息块这不是复杂度膨胀而是给后面统计、索引、导出留了结构化入口。人的肉眼读起来也不累程序员看了一会儿就能明白这是一种非常舒服的兼顾方式。3.4 查找笔记多种姿势找到那条信息查找是日常用得做多的功能。caveman 提供三种使用姿势按需求强度递增。第一按标题模糊查caveman list | grep 硬盘list命令会把所有笔记的标题、创建时间、标签一次性列出来按最新排序。这种“先全量列出现再用外部文本过滤器过滤”的做法能让很多人在老式命令行习惯里感觉非常顺手同时避免了给列表命令叠加太多自定义过滤参数导致的复杂度。第二按标签查caveman find 咖啡这会先读tags.json命中后会直接把所有带“咖啡”标签的文件完整内容填入标准输出。适合你想完整回忆某主题相关所有笔记的时候。执行结果会携带文件名和全文[1] 2025-06-18-1422-咖啡手冲参数记录.md 研磨度中等偏细 水温92℃ 水粉比1:15 [2] 2024-11-03-2111-摩卡壶煮奶泡.md 加热至冒小泡后离火倒入杯中。第三按关键词搜正文caveman search 接口类型如果不启用--tags-only参数search 会做全量线性扫描定位正文中包含关键词的文件。实测在 1000 条小型文本笔记下返回结果几乎在一眨眼之间。这种从“弱搜索”到“略显暴力”的搜索层级设计能覆盖绝大多数用户的日常场景。3.5 编辑、删除与归档极简最重要的底线是不能在基本写操作上束手束脚。caveman 支持直接编辑文件也可以把文件移动到归档目录。caveman edit 2025-06-18-1422-买硬盘注意接口.md caveman archive 2025-06-18-1422-买硬盘注意接口.md没有繁琐的“软删除”“回收站”机制因为底层文件就在你自己目录下想彻底删直接rm就行。但为了让这些操作更顺手我还是提供了对应命令核心实现其实也就一句话找到文件执行编辑或移动。这就是工具与数据解耦的好处上层操作只是给底层文件系统换了一种更友好的口令。3.6 自动化脚本与外部工具联动caveman 的价值很大程度在于能嵌入自动化流程。比如我用 LaunchdmacOS绑定了一个 crontab 任务每天上午九点自动给 caveman 倒入一条“昨天的复盘”空模板提醒我及时补充。这只是外在脚本caveman 不过问。而因为所有输出都是标准纯文本配合 mash 脚本很容易把笔记内容转换成个人博客的草稿格式。举一个具体玩法caveman list --formatjson | jq -r .[].title把所有标题输出成 JSON 流再交给 jq 处理就可以玩出各种花活。这就是为什么我坚持命令行的“机械友好性”要和“人类友好性”并重。对一个工具的最高评价不是它什么都能干而是它能干的事情不容易干砸并且允许你自行拼装出前所未有的能力。4. 踩坑记录与故障排查那些你可能也会遇到的问题4.1 中文搜索无效可能是终端编码和文本规范化在搞鬼第一次在 macOS 上测试caveman search 咖啡时我一度以为代码写错了——明明笔记里有“咖啡”两个字却搜不到。排查半天发现问题的根源出在 macOS 默认文件系统对 Unicode 的规范化处理上。macOS 的 HFS 和 APFS 文件系统使用 NFD分解形式存储文件名也就是把“咖”拆成了“カ”和一个小符号而我命令里的关键词是 NFC组合形式。当去文件名里做字符串匹配时就出现了肉眼看起来一样、计算机实际编码不同的情况。这个细节不处理的话中文用户一半情况都会踩坑。我的解决方案是在实现里自写了一个简单的字符串规范化接口构建索引和文件名匹配前统一将字符串转成 NFC。这个坑虽然细微但对于任何要做跨平台中文文件名处理的技术人来说是一个值得备录的教训。很多工具在这类问题上不处理用户头顶的问号往往就变成对软件的差评。4.2caveman find召回太多或太少怎么调整不少用户反馈过同一个问题输入一个关键词结果要么几百条要么一条都没有。这大概率不是 bug而是搜索模式没理解透。find命令默认是按标签匹配如果你把标签称为“咖啡”但笔记正文里出现的全是“coffee”那么用find 咖啡肯定找不到。这时候应该改用search coffee做全文匹配或者给find命令加上--tag参数指定标签字段。相反如果你觉得search结果太多那是因为它在执行包含匹配任何包含关键字的子串都会被拉进来。此时可以用search 完整短语来精确匹配多词连续文本或者配合 grep 再做一次二次过滤。在命令行生态里工具与工具之间的组合是常态不要把责任全压在某一个命令身上。4.3 索引坏了怎么办前文提到tags.json是索引。这个文件确实有可能损坏比如你在同步网盘时中途进程被杀或者自己手贱编辑坏了。遇到这种情况不要慌执行caveman reindex它会扫描notes/下所有文件的元信息块重建整个索引。整个重建过程就是纯 I/O 扫描一千条记录也就是毫秒级完成。如果你本来就用网盘同步 caveman 目录我建议每次重要笔记新增后顺手执行一次caveman reindex或者干脆跑一次caveman doctor检查数据完整性和索引一致性。4.4 数据目录消失或迁移了怎么办有时候你会想把数据仓库从电脑 A 迁到电脑 B。最简单的方式是直接复制整个~/.caveman/目录然后在新机器上修改config.json里的dir字段。但如果你忘记改配置caveman 启动时会说找不到数据目录——别急。caveman status就是为这个设计的。它会显示当前读取的配置路径、实际数据路径、笔记总数和索引文件大小等。看到信息一目了然只要路径对上了数据自然就能被识别。这里也想提醒一个常被忽略的细节更换电脑时数据仓库的目录层级一定不要变特别是notes/这个子目录名。如果你把它改名为docs/索引重建会失效因为 caveman 默认只索引notes/下的文件。这是为了极简而付出的必要代价我把扫描范围写死没有做成无限可配置换来的是代码路径简单很多。4.5 多台设备同步时出现文件冲突如果你也想把 caveman 仓库放在第三方同步盘里实现多设备使用有一点要提前有心理准备当两台设备同时对同一条笔记进行编辑并同步, 可能会产生冲突副本比如xxxx (MacBook 的冲突副本).md。caveman 不会自动合并冲突因为我实在不想去写一个 sematic merge 引擎这是另一个维度的复杂度。我的实践策略是尽量避免多设备同时写同一时间段的笔记。比如工作电脑只记录工作条目家里的电脑只记录生活条目冲突几率极低。如果真遇到冲突副本直接看时间戳打开两个文件手动合并内容就行。对极简工具来说让用户在极端情况做一次手工合并远比内置一套不可控的自动合并逻辑更透明。这一块我建议任何人使用本地优先工具时都明确一条纪律数据仓库越简单自己越不能迷迷糊糊。至少要知道同步盘在何时同步、何时有延迟并接受它会产生的边缘情况。5. 经验之谈我对极简软件的更深一层理解5.1 功能缺失不是缺陷而是设计策略做完 caveman我对“极简”二字的理解比过去深了一个台阶。极简不是功能少而是不越界。caveman 不主动提供云同步、不做富文本编辑器、不替用户画图表是因为这些功能背后隐藏着巨大的维护负担和决策成本。一旦你让软件自己处理云同步你就要计划账号体系、权限管理、服务器成本、网络冲突策略……这套东西积累下来软件的核心体验——快速存取的爽感很快会被层层叠叠的异常处理淹死。同样地caveman 也刻意不去做复杂的查询语法。我信任用户是聪明人他们会在 caveman 外边套上 awk、rg、jq 甚至 Python 脚本来满足自己的复杂需求。工具与用户之间的边界一清晰反馈周期就变短出问题的时候你很容易定位到底是 caveman 的问题还是下游脚本的问题。这在调试体验上是一种巨大解脱。5.2 数据格式本身也是一种“代码”每次我向别人介绍 caveman都会反复强调一点它的数据是纯 Markdown不是数据库记录。很多开发者朋友第一反应是“效率太低了吧”但真正用了几天以后都会认同这个设计比效率重要得多。数据格式就是你软件的长期 API。如果你把用户数据压进一个二进制数据库用户等于被你锁定了但你用透明格式用户随时可以迁徙、备份、重建索引。caveman 的价值主张某种程度上不是软件本身而是“允许你随时离开”的自由。这种安全感是任何花哨功能都无法替代的。5.3 日常使用里的无感愉悦说实话用 caveman 记了几个月的日常笔记之后最吸引我的反而是“无感”这两个字。它不推送、不弹窗、不要求我升级会员也不在我只用了十分钟后就发一封“我们想念你”的邮件。它就像一把称手的螺丝刀安安静静躺在抽屉里只要我拿起它它就在那儿一秒不差地干我让它干的活。这种体验让我想到更宏观的一个道理大部分软件的问题不是缺功能而是功能太多多到用户开始为自己的东西操不该操的心。caveman 把数据主权交还给用户把复杂度包袱也一并交还给用户这反倒让使用过程变得清爽。你不需要担心“这个月流量够不够”“免费版能不能用这个功能”因为这些问题在本地工具里根本不存在。5.4 后续扩展思路漫谈尽管 caveman 已经比较克制但我在使用过程中还是想到了几个相对收敛的扩展点在这里一并分享有兴趣的人可以在自己的版本上试试。一是加密支持。可以给个别笔记做可选的对称加密密钥只存在本机钥匙串里。这样可以放心在同步盘里存一些敏感信息而不怕文件泄漏后一览无余。二是模板系统。比如指定caveman add --template daily会生成一个固定的复盘格式这样对坚持记日记或工作日志的人比较友好。模板本身也只是一个 Markdown 文件放在~/.caveman/templates/下方便自定义。三是插件钩子。比如在add命令执行前或后触发一个用户自定义脚本。用最简单的 exec 实现不做复杂的事件总线这样用户可以把“笔记加入后自动生成标签云”“笔记加入后自动同步到博客仓库”之类的小仪式变成现实。每个功能依旧遵循老原则可以做但必须是透明的、可退出的、默认关闭的。一个工具能活多久很大程度上取决于它的核心逻辑有多稳定、边界有多清晰。写在实际开发之后我自己回头复盘这个项目最深的体会是有时候限制反而带来自由。如果一开始我就想做“又一个全能笔记软件”caveman 很可能已经折在第五个功能的设计里了。正因为只想做“本地的一个小洞穴帮你存点想记住的东西”它才活了下来且变得越来越顺手。如果你也被各种跨端同步、云服务依赖、冗余功能弄得有点透不过气强烈建议你也动手写一个类似的小工具。不用非用 Go用 Python、Rust甚至一段 Shell 脚本起步都行。从你真正的问题出发而不是从炫技出发设计出的工具通常会比市场里的很多商业产品更适合你自己。另外一个小技巧如果你想给某个常用命令提速可以在 shell 里配置一个短别名比如alias cavcaveman alias cavscaveman search alias cavacaveman add --quick这样日常使用就是cavs 咖啡、cava 今天下雨了记得带伞快得几乎不需要思考。工具嘛本就不该时刻刷存在感该当一个靠谱的影子。