1. caveman是个什么项目:从笔记工具失控到“数字洞穴壁画”
说实话,caveman这个项目一开始是我给自己开的一个玩笑。2025年我实在受不了自己的数字生活了,四千多条笔记分布在六个工具里——微信收藏、系统备忘录、Notion、飞书文档、知乎收藏、两个本地文件目录,彼此之间老死不相往来。有一天我想找一条关于“发酵面团湿度”的记录,翻了整整一个小时,最后发现它老老实实躺在微信收藏里,旁边还挂着两条过期的购物链接。那一刻我做了个决定:从今天起,过“穴居人”的日子。
所谓穴居人,不是不写代码、不碰电脑,而是回到最原始、最笨、但最可靠的技术栈,重新接管自己的内容。这个项目被我命名为caveman,核心是一套极简个人知识库加发布管线:所有内容都落成纯文本Markdown文件,用Git管理版本,用一个自己写的标准库脚本构建出静态页面,再用rsync推到我的小服务器上。说出来可能有点寒酸,但正是这套组合拳,治好了我的信息焦虑。
这个项目适合谁?我觉得最适合两类人。第一类跟我一样,被各种云笔记平台绑定过、导出格式折腾到怀疑人生的普通写作者;第二类是喜欢折腾但已经厌倦了“为了维护系统而维护系统”的技术爱好者。如果你只是想要一个能用的笔记软件,那继续用现成的就好,没问题。但如果你也受够了数据躺在别人手里、想自己掌握一切,又不想学一堆新框架,那caveman这套思路值得参考。
1.1 数据失控是真实痛点,不是矫情
以前我从来没认真盘点过自己的内容资产。直到某个周六下午,我把所有工具的导出文件全部解压到同一个目录里,才发现情况有多混乱:Notion导出的HTML嵌套了七八层div;旧博客的数据库备份打不开,因为PHP版本不匹配;iPhone备忘录的格式虽然能导出来,但无法按时间批量操作;微信收藏甚至没有官方批量导出。
更难受的是检索。有数据库的时候检索很快,但每个平台只有它自己的搜索,跨平台找东西基本靠记忆。很多内容在收藏的那一刻就等于被丢弃了,因为它们和别的信息隔离开来,永远不会被再次发现。caveman要解决的最核心问题,不是“存储”,而是“让内容流动起来”——把我写的、收藏的、摘录的、灵光一现的所有文字,统一放到一个我能完全控制、能全文检索、能批量操作的地方。
我当时的标准就三条:第一,数据必须是普通文本,任何设备、任何年代都能打开;第二,文件名要有时间信息,这样不用查数据库也知道先后顺序;第三,不需要复杂软件就能完成整理和发布。这三个标准基本把市面上的知识库产品全排除干净了。
1.2 我试过的路:大家都在船上,船却在漏
写caveman之前,我试过很多方案。Notion确实好用,功能全面、数据库视图灵活,但我每次导出都觉得像被绑架——导出的Markdown里塞满了它的特有关键词,标题层级也会变乱。而且Notion必须联网,有一次我在高铁上想翻一篇技术笔记,断网状态下页面根本打不开,只能盯着那个旋转的加载图标发呆。
后来试过开源双链笔记软件,装上插件、配好同步,折腾了整整一个周末。说实话,双链图谱第一眼确实惊艳,看得见每篇笔记之间的关系网。但问题在于:图谱好看的前提是你得手动维护链接,或者依赖全局匹配。我写了三十篇笔记之后发现,为了“让图谱好看”这件事本身,我花掉的时间比写内容还多。这完全本末倒置。还有一次本地数据库文件损坏,虽然最后靠备份恢复了,但对它的信任已经没了。
我也想过自建DocuWiki或者BookStack这类带界面的知识库系统,装完之后看着那一堆PHP依赖、配置文件、数据库表结构,我脑子里出现了一个问题:三个月后我还愿意给这个环境打补丁吗?答案是不愿意。于是掉头往反方向走——越轻越好,最好是连数据库都用不上的方案。
1.3 caveman的三条铁律
所以caveman从第一天起就给自己定死了三条规矩:
- 单一数据源:所有内容只存在于content目录下的Markdown文件中,不上别的数据库,不复制到别的工具。
- 纯文本优先:任何编辑器都能打开,任何系统都能读取,格式是Markdown,五年后、十年后依然可读。
- 弱依赖:构建脚本只用Python标准库和Shell命令,不装Node、不装数据库、不装HTTP服务框架,发布只靠Git和rsync。
我把这套东西称为“数字洞穴壁画”。古人在岩壁上画下图案,过几千年我们还能看懂;现在的人把记忆存在私有格式里,过五年可能就得靠厂商心情才能导出。我要的不是花哨,是活得久。caveman这个名字,就是想提醒自己:技术永远在变,但文字本身不需要被技术绑架。
2. 为什么偏偏选“最原始”的技术栈:选型背后的真实逻辑
很多人看到caveman的第一反应是:你放着现成的Hugo、Next.js不用,偏偏自己写脚本,是不是闲得慌?说实话,一开始我也怀疑过自己,但用了三个月之后,我确信这套“原始栈”的选择是有道理的。这一节我把每个组件为什么这么选,一次说清楚。
| 层级 | 方案 | 为什么是它 |
|---|---|---|
| 内容格式 | Markdown纯文本 | 可读性最强、迁移零成本、任何设备都能编辑 |
| 版本管理 | Git | 天然支持历史回溯、冲突管理、多设备同步逻辑 |
| 构建工具 | Python标准库+Makefile | 没有版本依赖,不会半年不维护就跑不起来 |
| 发布方式 | rsync + SSH | 一条命令同步整个目录,不需要额外服务 |
| 本地检索 | ripgrep + fzf | 秒级全文搜索,不需要装搜索引擎 |
2.1 纯文本,是抵抗工具绑架的唯一办法
我见过太多人把文章写进Word,写进各种私有格式的编辑器里,最后导出时遇见一堆乱码。Markdown的好处在于它本质上是带少量符号的普通文本,就算你完全不会渲染,直接用记事本打开也能看懂标题、链接、列表。这意味着我永远不会被单个工具锁死:今天可以用VS Code写,明天用Vim写,后天在手机上随便找一个文本编辑器写,文件仍然是同一个文件。
纯文本还有一个隐藏优势:可以批量改。以前我如果想统一把全文里的“gallery”改成“相册”,在Notion里可能要点半天;现在只要一条sed命令,两秒钟扫完所有文件。有一次我想把某段时间写的两百篇笔记里的个人称呼统一修改,就这么干的,干净利落,而且改完能用git diff看得清清楚楚,哪一行变了、怎么变的,一清二楚。
2.2 Git:不只是备份,是给你一套后悔药
把Git选为caveman的核心,不是因为它新潮,恰恰因为它足够成熟。Git仓库本身就是完整的版本历史,我可以知道每篇笔记是什么时候创建的、哪句话是哪次修改加进去的、为什么删掉了一段。有些事情文字记录早就忘了,但git log会如实告诉你。
多设备同步这个问题,Git的处理方式也简单直接:一台机器上写好,commit之后push到远程仓库;另一台机器上pull下来。中间如果两台设备都改了同一个文件,Git会报告冲突。说实话,这个“冲突”机制一开始让人觉得麻烦,但后来我发现它反而是保护伞——它强迫我面对同一个问题:“到底哪个版本才是对的?”这比网盘那种“静默覆盖”要安全得多。
当然Git不等于备份,远程仓库要自己搭,或者选择信任第三方的Git托管服务。我的做法是在自己的服务器上建了一个裸仓库,这样数据始终在我手里。这一点很关键,尤其当你真的把caveman当长期项目跑的时候,远端仓库就是唯一的锚。
2.3 自写构建脚本:不是硬核,是杀鸡不用牛刀
现成的静态站点生成器很强大,Hugo渲染快、主题多,Next.js社区也很繁荣。但它们每一样都自带依赖生态:升级要跟着走,主题要看作者更新,插件和版本的兼容性时不时出问题。对于一个月发几篇文章的个人知识库来说,这种复杂度完全是多余的。
caveman的构建脚本只做四件事:扫描content目录下的所有Markdown文件,解析每篇文件头部的元信息,按时间排序生成文章列表页,把正文渲染成静态HTML。这些逻辑用Python标准库加几十行代码完全够用,不引入任何第三方包。写完之后我甚至有点后悔——当初拖了两周才动工,真正写脚本只花了一个下午。很多东西的复杂度是被想象放大的。
2.4 发布走rsync而不是一键部署平台
一键部署平台确实方便,代码推上去自动构建自动上线。但对个人内容库来说,我更需要的是“可控”。rsync的思路简单粗暴:本地构建完静态文件,通过SSH同步到服务器上的Web目录。没有构建服务器,没有容器,没有配置中心。服务器上只需要有rsync和SSHD就能跑。
这套方案还有一个很大的好处:芯片级简单,出问题太好排查了。如果发布失败,无外乎三种可能——SSH连不上、目录权限不对、源文件没构建好。不像容器编排那种层层嵌套的环境,出问题你都不知道先看哪个日志。
3. 目录骨架、文件规范和构建脚本:caveman到底怎么搭
聊完选型,该说点能直接抄作业的了。我尽量把caveman的目录设计、文件规范和核心脚本讲得具体一点,你可以照着搭,也可以根据自己习惯改。记住一个原则:结构是为内容服务的,别为了结构本身搞太复杂。
3.1 目录结构一次说清
这是我的caveman仓库的实际目录布局:
kb/ ├── content/ │ ├── posts/ │ │ ├── 2025-06-01-bread-humidity.md │ │ └── 2025-06-05-git-notes.md │ └── notes/ │ └── inbox-20250607.md ├── templates/ │ ├── post.html │ └── list.html ├── scripts/ │ ├── build.py │ └── index.py ├── public/ ├── Makefile └── README.mdcontent目录是全部家当。posts放正式成文的内容,notes放碎片想法和临时摘录,其中inbox文件是我在手机上随手记的入口,每周整理一次。templates里是两个极简HTML模板。public是构建输出目录,平时不进Git。Makefile把构建流程串起来,让发布变成一条命令。
3.2 一篇笔记的完整生命周期
每篇Markdown文件的开头必须有一段frontmatter,用三条短横线包起来的元信息区,长这样:
--- title: 发酵面团湿度笔记 date: 2025-06-01 tags: [cooking, bread] status: publish ---正文就是普通Markdown。status字段有三种状态:draft表示还没写完,构建时会跳过;publish表示要上线;archive表示归档,不再出现在首页列表但保留在站点里。这个三态设计非常重要,它让“写完再发布”和“先记下来再说”两种场景都有了出口。
我的习惯流程是:手机上随手记一条inbox文件,比如“2025-06-07 查一下面包整形手法”;周末整理时把内容结构化,改成正式文件名,补全tags和status。写废了的草稿也不会被丢,状态设成draft就好,它只是不显示出现在网站上,但内容还在Git历史里。
3.3 构建脚本的核心逻辑
构建脚本不需要很复杂,核心就这两个函数。第一个是解析frontmatter:
import pathlib, re def parse_frontmatter(text): m = re.match(r"^---\n(.*?)\n---\n(.*)", text, re.S) if not m: return {}, text meta = {} for line in m.group(1).splitlines(): if ":" in line: k, v = line.split(":", 1) meta[k.strip()] = v.strip().strip('"') return meta, m.group(2)第二个是生成文章列表页:
def build_index(posts): posts.sort(key=lambda x: x["date"], reverse=True) items = "\n".join( f'<li><a href="{p["slug"]}">{p["title"]}</a><em>{p["date"]}</em></li>' for p in posts ) return f"<ul>{items}</ul>"实际代码比这个长一点,但核心逻辑就是这么朴素。构建时扫描posts目录,逐个解析,过滤status,最后渲染到templates里,输出到public。没有增量构建,没有缓存,三百篇页面全部重新生成也就一两秒,我觉得完全不需要优化到毫秒级。
3.4 发布钩子与回滚
发布走Git裸仓库加post-receive钩子。服务器上建一个裸仓库,然后写钩子:
#!/bin/bash GIT_WORK_TREE=/opt/kb GIT_DIR=/srv/git/kb.git git checkout -f cd /opt/kb && make publish本地写完内容后,执行git push一键触发服务器检出并发布。注意钩子脚本要给可执行权限,否则push成功但不会触发任何动作,我当时就栽在这个细节上。
“回滚”同样简单:想回到昨天的版本,本地直接git revert或者reset到对应commit再push,服务器上的钩子会把旧版本重新检出来并发布。我回滚过一次,全程没超过三十秒。
4. 让内容能被找到:标签、索引与搜索的取舍
有了内容、有了发布,真正难的是怎么把旧内容翻出来。这一节记录我在检索侧的实践和取舍。这里想明白之后,caveman才算真正能用起来。
4.1 标签体系的两级设计
我试过很多种标签方案,最终沉淀下来的规则很简单:只有两级标签。第一级是主题域,比如cooking、tech、reading、life;第二级是具体小标签,比如bread、git、koji。搜索时先按主题域收窄,再看具体小标签定位。
不建议搞超过两级的原因是维护成本递增很快。标签一旦超过三级,整理时你要反复思考这个标签应该挂在哪个父级下面,这种思考本身就是在消耗写作精力。而两级刚好:一套放之四海皆准的大分类,加上一套自由生长的小标签,既不冗余,又足够精准。
4.2 自动索引页不用手维护
访客点开站点应该能看到按时间线排列的文章列表,同时每个标签也应当有自己的页面。caveman的构建脚本每次都会自动扫描所有文件的tags,动态生成标签索引页和统一文章列表,不需要我手动维护。这也意味着当你给某篇文章改了标签,重新构建后索引立即跟着变,不用去改第二处地方。
有一点要提醒:如果改了文件名,也就是slug变了,以前发布过的站外链接会失效。我的办法是在旧文件名位置放一个redirect文件,做法虽然原始,但足够解决问题。站内内容之间的引用链,则靠构建时全局扫描旧链接并提示输出警告,保证自己不会带着一屁股死链上线。
4.3 搜索:这条命令我一天用十几次
caveman没有内置搜索框,因为终端里的检索已经足够好用。我在本地用ripgrep配合fzf做一个模糊搜索终端命令:
rg -li "search_keyword" content/ | fzf --preview 'head -20 {}'输入关键词,秒级列出所有包含它的文件,再即时预览前20行,选中后直接打开。这套组合虽然看起来像是“命令行原教旨主义”,但实际体验比很多笔记软件内置的搜索都快。对于几千条纯文本笔记,ripgrep的性能完全不需要担心,零点几秒就能出结果。
4.4 为什么我最终放弃了双链
现在知识管理圈不提双链就像少了点什么。但caveman刻意不做双链,这是我的真实取舍。双链的价值在于自动发现笔记之间隐藏的关联,但代价是你要么花时间手动维护链接,要么接受全局匹配带来的大量噪声。
我自己的实践是:只在需要的文章底部写一个“相关条目”小段落,手动放两三个链接,既不追求图谱,也不追求自动关联。与其维护一套复杂的链接机制,不如每次写文章时顺带想一下“这篇内容跟以前的哪篇相关”。这个动作不需要额外工具,但效果却最直接——半年后你看到“相关条目”四五个字,就能顺着找到整个知识簇。
5. 三个月实测:维护成本、翻车记录和补救办法
方案说得再好听,也得经过真实使用检验。到写这篇文章为止,caveman已经稳定跑了三个多月,我把这段时间的几个关键数字和翻车现场都留个档,给后来者参考。
| 指标 | 数值 |
|---|---|
| 内容总量 | 523篇,其中正式文章47篇,其余为笔记和摘录 |
| 日均新增 | 约1.8条 |
| 全文搜索命中率 | 近三个月检索约50次,48次一次命中 |
| 日常维护时间 | 大约每周15分钟,主要是整理inbox和修标签 |
| 非预期翻车次数 | 3次,全部为操作失误,没有一次是系统崩溃 |
5.1 翻车记录:文件名里的空格害我找了一下午
第一次翻车完全是格式规范问题。我在手机上有条笔记文件名是“发酵 测试.md”,中间带了一个空格。结果build.py在生成链接时没有做URL编码,导致页面里的链接断了一截,样式也乱了。更麻烦的是,我在本地grep的时候明明能看到内容,但构建出来后怎么都打不开。
查了半天才定位到是空格问题。从那天起,我定了一条硬规矩:文件名一律用短横线连接,不出现空格和中文;中文标题放frontmatter里。这条规范看着简单,但能帮你避开一大类线上问题。
5.2 翻车记录:rsync的--delete差点清空线上目录
这是让我最后怕的一次。当时我调整了构建脚本的输出目录,本地已经构建好了,但public目录下还残留着旧版页面。我为了让服务器和本地“绝对一致”,在rsync发布命令里加了--delete参数。结果因为本地构建脚本出了问题,public目录是空的,命令执行后线上目录也被清空了。
事后我总结了两个防呆手段。第一,发布前脚本里必须检查构建产物里的index.html存在才允许继续执行;第二,不再直接rsync到线上目录,而是先同步到一个临时目录,同步完成后再用mv做原子替换。这样即使同步出问题,线上也不会瞬间变成空白页。
5.3 手机端操作与Git冲突处理
移动端的体验是这套方案里最弱的一环。我目前的做法是用Git客户端在手机上提交新增文件,但因为手机端环境不稳定,偶尔会出现和电脑编辑同一份文件的情况,就会产生Git冲突。这事的解决办法很简单:手机上只做新增inbox和草稿,绝不在手机上修改已有正式文件。新文件之间很少冲突,旧文件保持一方权威。
如果确实需要临时改一个旧文件,我宁愿在inbox里记一条备忘,等回到电脑前再统一修改。这个看起来笨拙的决策,帮我把移动端出错的概率降到了几乎没有。
5.4 每周维护其实只有十分钟
很多人以为自建方案维护成本高,但caveman的真实维护时间很少。每周一次整理inbox,把临时记录归入相应主题和标签;偶尔扫一眼tag列表,合并重复标签;如果发布地址变了,改一下配置。全部加起来一次不会超过十五分钟。相比以前在Notion里整理数据库视图、调整分组结构的时间,已经算是极省了。
6. 下一步:caveman v2的几个想法和三个入坑建议
项目跑顺之后,我开始琢磨下一版还能做点什么。当然,caveman的精神就是不做重,所以未来的改动一定是往“更省事”方向改,而不是往“更强大”方向堆功能。
v2我最想做的三件事:一是给构建脚本加一个增量构建开关,只在文件有变化时重建相关页面,省掉每次全量重跑的一两秒;二是在移动端做一个更顺畅的添加入口,目前是用Git客户端提交文件,流程上还能简化;三是完善内容生命周期提醒,让长期不动的draft文件自动进入我的视野,提醒我哪些想法已经过期、哪些值得重新整理。
如果你也想搭一个类似的东西,我给三条发自内心的建议。第一条,头一个月只用一个inbox文件,什么都往里丢,别急着分目录、建标签,先跑通“记录”这个动作。第二条,写一个再也不能更简单的脚本,别一上来就学我的完整结构,因为你的整理习惯一定跟我不同。第三条,git远程仓库越早建越好,最好从第一篇笔记就纳入版本管理——千万别写了两个月才想起备份,那种把内容重写一遍的滋味,我不希望你体验。
好了,这就是caveman到目前为止的全部家底。我依旧每天用这个极简系统记录、整理、发布,也依旧隔三差五靠git log回忆某个想法是什么时候冒出来的。工具链虽然原始,但它给了我一种踏实感:这些文字在我手里,不在别人的服务器上,也不依赖某个厂商的格式继续存活。如果你也正在被越来越重的工具折磨,试试往回走一步,回到最朴素的“穴居人”生活方式,说不定会和我一样,找回记录的轻松感。