ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

caveman:用Markdown与Git构建属于你的极简自托管笔记系统

caveman:用Markdown与Git构建属于你的极简自托管笔记系统 这两年我一直在折腾个人知识管理从各种笔记软件到在线文档换来换去始终觉得不踏实。直到我把整套系统推倒重来改成了一堆 Markdown 文件加几个 Shell 脚本用 Git 做版本管理再用 cron 定时同步——我把这套东西命名为 caveman意思是回归原始人的用法只用最朴素、最耐用的工具不依赖任何平台、任何云服务、任何花哨的界面。跑了半年多稳定得离谱我再也没丢过一条笔记查阅速度也快过以前任何一款智能软件。这篇文章把这套方案的完整设计思路和搭建过程分享出来内容包括目录怎么规划、脚本怎么写、备份怎么做、多设备怎么同步以及我踩过的坑。它适合那些已经受够了笔记软件割韭菜、想彻底掌控自己数据的人也适合开发者朋友拿来扩展成自己的小工具集。核心就一句话与其找一个完美的软件不如自己造一套够用的石器时代工具。1. 项目定位与设计思路1.1 为什么叫caveman极简主义的技术哲学名字的来源其实很直白。我当时被一个笔记软件伤了心用了两年写了几百条笔记某天它突然改版把原来的导出格式作废我导出的纯文本里到处是乱码图片链接全部失效。那种感觉就像你花了好几年攒起来的家当被人一把火烧了。痛定思痛我问自己一个问题什么样的工具能保证十年后、二十年后我还读得懂自己写的东西答案是纯文本。你的数据库可能会坏你的软件可能会停止维护你的云盘可能会关停但一个.txt文件、一个.md文件只要存储介质还在任何系统上的任何编辑器都能打开。这就是我所谓的原始人逻辑——原始人用的石器哪怕过了几万年你今天捡起来照样能用。现代软件是精密机器坏一个零件就废掉石器没有零件所以它不会坏。有了这个底层逻辑整个项目的设计原则就非常清晰了我把它们总结成三条单一格式原则所有内容都是 Markdown 纯文本不带专有格式不存数据库。不依赖网络所有操作本地完成同步只是 Git 的推送和拉取断网时一切功能照常。脚本优先能用一个alias解决的事绝不写成一个 Web 应用更不搞 Electron 桌面端。这套哲学决定了下面所有具体的技术选型。1.2 方案选型的取舍逻辑先说说我为什么不用现成的开源方案。其实我研究过很多本地优先的笔记工具比如 Obsidian、Logseq、Joplin它们都很好。但问题在于Obsidian本身是闭源的而且插件生态虽然丰富核心编辑器却越来越重我捧着手机想补一条灵感打开它要等好几秒。Joplin数据存在本地数据库里同步要挂它的轮子和加密出问题的时候排查链路很长。Logseq的理念我很喜欢但它的数据格式和目录结构约束太强我想自定义的时候插不进手。说白了这些工具都是在别人的框架里过日子。我要的是完全在我掌控之下的东西。所以我最终选的组合是组件选用方案理由存储格式Markdown YAML frontmatter纯文本、可读性强、方便脚本解析版本控制Git分布式、本地操作、永远可回溯搜索ripgreprg极快、支持正则、尊重 .gitignore索引生成Bash 脚本 awk/sed零依赖、凡是 Unix 环境都能跑自动化cron系统自带、稳定可靠、无需常驻进程同步自建 Git bare 仓库数据永远在自己手里有人可能觉得这太原始了连个图形界面都没有。但反过来想你每天写笔记真正做的无非是新建一条笔记、找到一条笔记、修改一条笔记、定期备份。这四个动作命令行的效率远比鼠标点来点去高。而且命令行工具的优点是你永远不需要等它加载。关于用 Bash 而不是 Python 或 Node 这点我多说一句。不是我不会用高级语言而是我刻意不想引入运行时依赖。Bash 是每一台 Unix 机器的标配脚本写完了从我的 Mac 搬到 Linux 服务器上一行不用改直接跑。如果我用了 Python还得考虑版本、依赖库、虚拟环境这些破事。在够用和复杂之间caveman 永远选前者。2. 核心模块解析与文件规范2.1 目录结构与文件格式约定所有关于怎么组织笔记的问题本质都是怎么在二三十年后还能一眼看懂自己的目录。我第一次搭这个项目的时候把目录结构设计得天花乱坠什么projects/all/inbox、reference/articles/temp结果两周后就乱了——层级越深的目录维护成本越高你永远记不住某个文件该放哪。后来我砍成了非常扁平的结构vault/ ├── inbox/ # 快速捕获区临时想法、随手记 ├── notes/ # 永久笔记整理过的内容 ├── diary/ # 日记/日志按日期归档 ├── assets/ # 图片、PDF、附件 ├── templates/ # 笔记模板 ├── scripts/ # caveman 的脚本本体 ├── archive/ # 已归档的旧笔记 ├── README.md # 自动生成的索引 └── .gitignore这个结构有几个关键设计inbox 是有意的垃圾场。人不可能保证每一条想法都整理得漂漂亮亮所以先丢进 inbox每周找时间清理一次该转正转正该删除删除。这比强制自己写在正确的地方更容易坚持。archive 是终局。任何笔记过了活跃期移到 archive它还在仓库里、还在搜索索引里但不再干扰日常视野。这个做法解决了笔记越来越多、越来越不想打开的痛点。assets 单独放并且我会同步维护一个assets/manifest.txt记录每张图片被哪些笔记引用。为什么要这么做因为纯文本笔记最怕的就是文字还在、配图全挂。有了 manifest我能随时扫描出孤儿图片该压缩的压缩该删的删。文件名格式统一为YYYY-MM-DD-slug.md比如2024-11-03-git-rebase-cheatsheet.md。这样做的直接好处是按文件名排序 按时间排序不需要额外的元数据也不怕重名。如果某个文件改了主题我直接git mv重命名历史提交还在完全无痛。每条笔记的 frontmatter 我固定用四个字段--- title: Git Rebase 速查表 date: 2024-11-03 tags: [git, cheatsheet, 工具] status: active ---status有三个值active活跃、fledgling萌芽还没整理完、evergreen常青内容已经稳定可信。这个字段配合索引脚本可以生成该沉淀了的待办列表比靠脑子记强多了。2.2 索引与检索机制做笔记系统的人十有八九担心笔记多了搜不到。我的答案是用 ripgrep。你可能会觉得奇怪这不就是个命令行搜索工具吗但它的速度是真的快——我的笔记库目前有三千多个 Markdown 文件、大约 40MB 文本rg 全文搜索基本在 0.2 秒以内返回结果。这个速度在你打开任何笔记软件的时候就已经赢了。我封装了三个检索命令都配置成 alias# 全局搜索带文件路径和行号 alias cavgrg --hidden --no-ignore --line-number --smart-case # 按标签搜索 cavg() { rg -l tags:.*$1 ~/vault/notes ~/vault/diary | head -20 } # 在结果中继续过滤管道用法 alias cavprg -n * ~/vault | rg额外给自己做了一个最近编辑命令特别实用alias cavrecentfind ~/vault -name *.md -mtime -3 -not -path */archive/*把思路讲清楚我不追求做一个智能检索因为笔记的价值是你记得你写过什么而不是软件替你联想。全文搜索加标签过滤这两个维度的交叉已经能解决 95% 的查找需求。剩下 5% 靠索引文件。README.md的索引不是手写的是由index.sh脚本自动生成的。它扫描所有笔记提取 frontmatter按标签聚类输出成这样一个表格## 按标签归档 ### git - [2024-11-03 Git Rebase 速查表](notes/2024-11-03-git-rebase-cheatsheet.md) - [2024-10-12 理解 git object 模型](notes/2024-10-12-git-object-model.md) ### 写作 ...每次更新完笔记库跑一遍cavindex索引自动刷新。配合status: fledgling的过滤我还能生成一个待整理清单知道哪些笔记半成品需要补完。这一条就是很多人做笔记坚持不下去的解法——整理不是一次性的大工程而是流水线上的例行工序。2.3 备份与同步策略数据在自己手里还不够还得防自己把数据搞没了。caveman 的备份策略分三层我管它叫3-2-1 石器版本地至少 3 份副本工作目录一份、本地 Git 裸仓库一份、外接移动硬盘快照一份。2 种不同介质机器内置硬盘 移动硬盘。1 份异地我租了一台最便宜的云主机上面只跑一个 Git bare 仓库用密钥认证只做备份不做别的。建立异地备份这一步很多人会想着用 GitHub。我明确不建议把私人笔记整个推到 GitHub 上一是隐私问题二是 Git 仓库里历史提交改起来极其麻烦一旦敏感信息进了历史记录删除等于不可能。自己的小主机或 NAS 完全够用一年电费不到几杯咖啡钱。同步工作流很简单我在另一台电脑上也是 clone 同一个仓库写完就 push换机器就 pull。cron 里的备份任务长这样# 每天凌晨 2:00 自动提交并推送异地备份 0 2 * * * /home/user/vault/scripts/sync.sh /home/user/vault/logs/sync.log 21sync.sh这个脚本我在下一节详细讲。这里先强调一个原则自动提交的消息要可读。我见过很多人用 cron 跑git commit -m backup几天之后 log 里全是无意义的 backup——真出了事你根本不知道上次改动是什么。我的脚本会自动汇总当天改动文件的列表作为提交信息这样回溯的时候一眼就能定位。3. 实操搭建全流程3.1 初始化项目结构实际动手的时候别急着装任何软件先用一条命令把目录骨架搭好。我用的命令是这样的mkdir -p ~/vault/{inbox,notes,diary,assets,templates,scripts,archive,logs} cd ~/vault git init然后写.gitignore这里有个关键点日志文件、临时文件、还有 macOS 的.DS_Store必须排除否则每天自动提交会把一堆垃圾带进版本历史。.DS_Store logs/ *.tmp *~之后把备份用的裸仓库建在本地再把远端备份地址推送进去# 本地裸仓库作为本地第二份副本 git init --bare ~/vault-backup.git # 添加远端假设远端服务器已经建好 bare 仓库 git remote add origin backupmy-server:/srv/git/vault.git # 首次推送 git add . git commit -m init: caveman vault structure git push -u origin master第一次搭的时候我犯过一个错把logs/留在版本控制里结果同步日志每天一提交仓库体积膨胀得飞快而且日志文件会频繁造成无意义的 merge 冲突。这个问题后来靠加进.gitignore解决。类似这种看起来不影响使用、实际上天天恶心你的细节后面我会在常见问题里专门列一列。3.2 核心脚本编写caveman 的七个脚本都在scripts/目录下我挑四个最核心的展开讲其他的列个表格一笔带过。第一步新建笔记脚本new.sh。这个脚本解决的是懒得打开编辑器、新建文件的摩擦。它的流程是读取模板 → 替换日期和标题 → 生成文件 → 直接交给现有编辑器打开。模板放在templates/note.md#!/usr/bin/env bash # usage: cavnew 标题 [tags...] set -euo pipefail VAULT$HOME/vault TITLE$1 shift TAGS$* SLUG$(date %F)-$(echo $TITLE | tr [:upper:] [:lower:] | tr -cd a-z0-9- | cut -c1-40) FILE$VAULT/inbox/$SLUG.md if [ ! -f $FILE ]; then cat $VAULT/templates/note.md $FILE sed -i s/^title:.*/title: $TITLE/; s/^date:.*/date: $(date %F)/; s/^tags:.*/tags: [$TAGS]/ $FILE fi code $FILE # 或者 vim / vscode / typora用set -euo pipefail是 Bash 脚本的基本原则任何一步失败立刻退出变量没有定义就报错管道中的错误不会被吞掉。这一步我建议所有写 Shell 脚本的人都养成习惯。SLUG的生成逻辑则是把标题转小写并去掉标点保证文件名干净。第二步同步脚本sync.sh。它被我吹成全项目最不想出错的脚本。逻辑不复杂先检查有没有变化有变化就打一个带摘要的提交然后推送。关键在提交信息的生成#!/usr/bin/env bash cd $HOME/vault if [ -z $(git status --porcelain) ]; then exit 0 # 没有改动就直接退出勿打扰 fi CHANGES$(git status --porcelain | awk {printf %s , $2} | head -c 200) git add -A git commit -m auto: $(date %F-%H%M) [$CHANGES] git push -q origin master注意我用了--porcelain这个参数来判断是否真的需要提交。因为 cron 每天跑如果没有任何改动还白白 commit 一次日志里全是空提交真要追查改动记录时会被淹没。用exit 0静默退出是最聪明的一步。提交信息里带上日期和 改动文件列表将来回溯某一天改了什么一条命令就看到了。第三步索引脚本index.sh。下面这段是核心逻辑用 awk 解析 frontmatter 里的 tags然后去重归类#!/usr/bin/env bash VAULT$HOME/vault OUT$VAULT/README.md echo # Vault Index $OUT echo -e \n更新于 $(date %Y-%m-%d %H:%M)\n $OUT # 提取所有文件里的 tags awk /^tags:/ {print $0} $(find $VAULT/notes $VAULT/diary -name *.md) 2/dev/null \ | sed s/tags:[[:space:]]*\[//; s/\]// \ | tr , \n \ | sed s/^[[:space:]]*//; s/[[:space:]]*$// \ | sort | uniq -c | sort -rn /tmp/cav-tags.txt echo ## 标签统计 $OUT cat /tmp/cav-tags.txt | awk {printf %s (%d) , $2, $1} $OUT echo -e \n $OUT这个脚本不追求优雅追求能跑、可读、易改。awk 处理 frontmatter 的场景非常典型你不需要引入 yq 之类的工具——记住这句话能用标准工具解决的事永远不要给系统增加一个待维护的依赖。第四步搜索脚本search.sh。核心就一行但周边有一些贴心处理#!/usr/bin/env bash # usage: cavsearch 关键词 rg --hidden --no-ignore --line-number --smart-case $1 \ --glob !*.png --glob !*.jpg --glob !*.pdf \ --glob !archive/** \ ~/vault | head -50排除 archive 是刻意的——已经归档的内容绝大多数情况下不需要出现在搜索结果里会干扰判断。如果你确实要搜归档的旧笔记有单独的cavsearcha命令。头 50 条限制防的是极端情况下的输出刷屏真需要看全部的时候把管道去掉即可。其他辅助脚本就不贴全代码了详见下表脚本功能用法pic.sh把图片压入 assets并写入 manifestcavpic path/to/image.pngmove.sh笔记在 inbox/notes/archive 间移动cavmove 文件 目标位置weekly.sh生成本周待整理清单cavweeklyexpire.sh清理 30 天前且从未整理的 inbox 垃圾cavexpire3.3 日常使用工作流工具搭好之后最关键的是让使用它变成肌肉记忆。我的日常流程分三个环节快速捕获灵光一现的时候我用的命令是cavnew 想法标题 临时直接落到inbox/全部打开只在两秒内完成不用等任何软件加载。前面用code $FILE指的是 VS Code但你完全可以用 vim、nano、Typora 或者任何喜欢的编辑器。我后来甚至做了一个小脚本新增笔记时自动生成一句话摘要占位符迫使自己在写完正文后补上摘要因为摘要能大幅提升索引的可用性。每周整理周日晚上花 15 分钟执行cavweekly。它会列出一周内新增但status还不是active的笔记以及被标记为fledgling的半成品。我的整理动作只有三种move.sh转正、改写补完、删除。整理的过程本质上不是处理笔记而是清理自己的注意力缓存这个习惯比笔记本身值钱。同步与备份因为有 cron 盯着我平常基本不管同步。唯一的例外是跨设备的即时同步——比如我在电脑上写了一篇笔记想马上在手机上看到。这时手动跑一遍sync.sh再在手机上git pull就行过程不到十秒。关于手机访问有人会觉得没手机端就废了。其实手机端只需要做一件事读。我的做法是在手机 Termux 里装 Gitcavsearch的结果通过本地 HTTP 预览用一个 20 行的 Python 静态服务器脚本仅局域网开启。手机上要记的时候我反而不推荐用手机写长笔记——手机屏幕的输入效率根本不适合写作它只适合收藏灵感链接收藏的动作我往 inbox 里丢一个fledgling占位笔记等回到电脑前再补全。工具要顺势而为没必要跟物理输入效率对着干。3.4 扩展玩法caveman 最让我满意的一点是它的扩展成本极低。因为一切都是纯文本和标准工具你想加什么功能加一个脚本就行。我目前已经在用的扩展HTML 发布用pandoc把某篇笔记转成单文件 HTML放到个人网站上整个命令就一行pandoc note.md -s -o note.html零配置。PDF 归档某些重要笔记比如年度总结、旅行清单用pandoc转 PDF丢进 archive 作为不可变的快照。注意Git 虽然能把 PDF 存下来但它本质上是文本工具二进制文件多了会拖慢仓库所以 PDF 快照我只留最新版。日记热力图用一个脚本统计diary/里各月份的日记篇数输出成简单的 ASCII 热力图放在 README 里。这东西功能上没有任何实际用途但它给了我一个让坚持记录变成一个看得见摸得着的反馈人类就是吃这一套。定时提醒在笔记 frontmatter 里加一个due: 2024-12-01字段weekly.sh顺手把到期待办输出出来。这对 GTD 需求完全够用比我用过的任何待办软件都轻。我倒不太建议大家一上来就疯狂堆扩展。先跑一个月纯new/search/sync工作流等真觉得缺了某个动作写脚本补上而不是提前造一堆用不上的轮子。4. 常见问题与排查技巧4.1 操作层面的坑Git 冲突是跨设备同步最常撞上的问题。两台设备同时改了同一个文件pull 的时候 Git 会报冲突。新手看到和就发慌其实处理起来很简单人肉看一下两个版本哪些该留、哪些该删merge 完 commit 一下就好。不过真正的解法是避免冲突。我的经验很简单日记类、笔记类文件一次只在一个设备上写同一个文件的修改周期内先git pull再动笔。配合每写完一个文件就 commit的节奏实际上很少出冲突。万一真出了也一定不要用git checkout -- file粗暴覆盖——你丢掉的可能是另一台设备上写了三个小时的内容。宁可花两分钟看一下冲突标记也别偷懒。大文件污染仓库是另一个高频坑。我刚开始把截图原图直接丢进 assets一张手机照片动辄 5MB一百张就把仓库撑到 500MB每次 pull 都要下载半天。解决办法分两步第一步用pic.sh把图片统一压缩到宽度不超 1600px、质量参数 80一张照片能压到 300~500KB第二步对已经混进历史的大文件做一次git filter-branch清洗把历史提交里的大对象干掉。后者操作要谨慎务必先备份网上教程很多我提一句关键原则在清洗历史之前把整个仓库 clone 一份存到别处再操作。编码问题在 Windows 上常见macOS/Linux 上少见。但我遇到过朋友导出的 GBK 编码文件混进来导致全文搜索正常、grep出现乱码。解决方向其实不难# 批量把仓库内文件统一转码为 UTF-8谨慎使用 find ~/vault -name *.md -exec iconv -f GBK -t UTF-8 {} \;不过更稳妥的做法是把所有文件编码在 README 里写明一律 UTF-8作为团队的约定。如果是个人使用你只需要在编辑时注意另存为 UTF-8 即可。搜索漏结果的坑比较隐蔽。ripgrep 默认遵循.gitignore如果你搜的关键词恰好在一个被忽略目录里它直接不放出来。我搜索的时候特意加了--no-ignore就是规避这个。另外像archive/被我默认排除在外有时候想搜旧笔记忘了这茬会误以为笔记丢了。这个可以靠 alias 命名区分来记忆也可以做一个cavsearch --all选项来兜底看个人习惯。4.2 习惯维持问题工具的问题都好解决难的是你自己坚持不下来。我实测半年多的经验是笔记系统放弃率最高的两个时间点第一个是搞了三天还没写满十条的冷启动期第二个是积攒了三百条笔记却越来越不想打开的倦怠期。冷启动期的破解法把门槛降到变态低。不用一开始就把模板设计得完美无缺不用马上搭全所有脚本。我的建议是第一天只做三件事——建目录、写一条笔记、学会搜索。到第三天再把同步脚本加上。让系统跟着你的使用习惯长出来而不是反过来。倦怠期的破解法制造反馈。前面说的热力图就是一个例子每天写日记的打卡感真的很上头。另外定期归档也是一个正反馈——把不再用的笔记挪进 archive看着主目录越来越清爽那种我掌控了这一切的感觉是任何软件都给不了的。还有一个心理层面的建议不要沉迷于整理工具本身。有一类人喜欢花大量时间折腾笔记系统的配置、插件、模板笔记没写几条系统倒是越搞越复杂。caveman 的哲学恰恰相反它反对把整理本身变成一种拖延。如果哪天你发现自己花了两个小时做索引格式的 CSS请立刻停手出去跑一圈。4.3 问题速查表把上面所有问题整理成一张速查表方便遇到时候直接查症状原因解法pull 时报冲突双端同时改同一文件先备份手动看冲突标记再合仓库体积巨大大图片/二进制文件进了历史压缩图片 filter-branch 清洗搜索出现乱码文件不是 UTF-8 编码iconv 批量转码搜索漏结果关键词命中的文件在忽略目录里rg 加 --no-ignore每天空提交干扰记录sync.sh 无改动也 commit加git status --porcelain判空手机上看不了笔记未做跨端同步装 Termux git pull笔记越攒越乱缺少归档动作每周跑 cavweekly 定期 archive图片链接全失效图片被移动/删除且无记录维护 assets/manifest.txt 做校验4.4 踩过的坑一次真实事故复盘最后分享一次我自己的真实事故这段经历让我深刻体会到上面所有原则的价值。有几天我另一台电脑的同步机制出了问题cron 执行的sync.sh因为远端服务器上硬盘满了push 一直失败但本地 commit 照常进行。那台电脑又是旧的Git 报错日志被日志清理清掉了我完全没察觉。等到我想起很久没在那台电脑上写东西、连上去看一眼时发现自己有一周多的日记都没有进主仓库。如果是以前用在线笔记软件这种数据是自动云端同步的根本轮不到我来操心但反过来想也正是因为一切都在我的控制下我的冒险成本极低——本地仓库里所有提交都在我只是少了一周的日记而已。最后我把那台电脑上尚未推送的提交重新 rebase 推上去十分钟解决。这个事故给我留下的教训是备份系统本身也需要备份监控系统本身也需要监控。那次之后我在sync.sh的输出里加了失败检测一旦 push 失败立刻用 notify-send 弹窗提醒我并且让脚本在连续失败三次后暂停自动同步避免带着错误掩耳盗铃。这种把失败当场炸出来的设计比任何自动恢复魔法都可靠。5. 实操心得与进阶方向如果只让我讲一条 caveman 最核心的心得那一定是复杂系统的抗风险能力来自简单而不是来自更强的技术。我见过太多人拿 Kubernetes 管一个日访问量两位数的博客也见过有人买个 NAS 跑一堆 Docker 容器只是为了存几张照片。技术本身没有错但当你把记笔记这种高频动作绑定在一堆随时可能出问题的组件上时你付出的隐性成本——维护、学习、故障排查——早就超过了工具的收益。caveman 这个项目跑到现在我个人的体会可以浓缩成三点文件结构大于工具链无论你用什么技术一个扁平、清晰、可预测的目录结构永远是系统能长期存活的第一要素。提交信息是可读的日志别让cavbackup之类的无意义消息污染你的历史记录让每一次提交都自带发生了什么的上下文。自动化不是终极目的可恢复才是自动 commit、自动 push 的意义不是省掉两条命令而是确保意外来临时你有干净、完整、独立的副本可以回滚。后续我打算在三个方向上继续扩展它一是给weekly.sh加上简单的自然语言提醒让它每周日晚上推送本周写了什么、哪些笔记需要补全二是用 cron 定期对仓库做一次git gc和体积检查超过阈值自动提醒三是写一个简单的全文索引缓存笔记超过一两万条的时候用一个增量索引替代裸 rg 搜索。不过这些都属于锦上添花核心工作流已经稳定了不需要为了扩展而扩展。最后再送一个小技巧在.bashrc或.zshrc里给cav系列命令加一行提示alias cavecho caveman vault: new/search/sync/weekly。刚开始使用的时候你一定会忘记有哪些命令这一行提示能帮你少翻很多次代码。别高估自己的记性把命令列表放在手边才能真正做到随手用。
返回列表