资讯动态

极简日记应用caveman:纯文本存储与git同步的设计实践

发布时间:2026/10/8 21:11:40 来源:尧图企业网站定制
每次有人瞄到我电脑里那个叫caveman的文件夹第一反应基本都是笑都什么年代了还拿这么原始的办法记日记等他们真看清楚里面的东西又沉默了——三个 Markdown 文件、一个纯文本统计脚本、一套 git 自动提交的备份任务居然比他们手机里那些推送消息满天飞的日记 App 坚持得更久。这篇就聊聊这个叫 caveman 的极简日记应用它为什么叫这名字存储和同步是怎么设计的核心功能怎么从零长出来以及我砍掉了哪些主流笔记软件里看起来理所应当的功能。如果你想自己动手做一个类似的、用纯文本管理个人日记的小工具这篇应该能给你不少实际参考。1. 为什么我给极简日记应用起了caveman这个名字1.1 一个反讽caveman 其实是最不原始的做法起名那会儿我正被主流日记应用折磨得够呛。打开一个笔记本应用第一件事是先等它加载完启动页再跳过新手引导弹窗最后面对一排工具栏发呆——字体、颜色、插入图片、思维导图、模板商店。我只是想记一句今天下雨心情一般结果要跟一个功能庞大的客户端先周旋两分钟。caveman 这个名字其实是带着一点自我解嘲的反讽。洞穴人不需要智能恒温系统他们只需要一堆干柴和一块燧石就能生火。日记这件事的本源也一样记录保留回头翻看。除此以外的九成功能都只是数字时代自己给自己制造出来的问题。我把工具砍到只剩干柴和燧石结果反而发现这种返祖的做法才是最省心的——它不产生新问题只解决原本的问题。1.2 这个项目是给写日记的人做的不是给笔记爱好者做的建项目之前我认真想过一个问题日记journal和笔记notes的需求本质到底差在哪。笔记是聚合性的需要分类、标签、双链、全文检索信息越积越多检索效率决定工具价值。日记是连续性的按时间顺序一条条往下写核心是今天有没有留下点什么而不是过去的知识有没有被索引。想清楚这一点之后caveman 的定位就非常明确了它不为知识管理场景服务不跟 Notion 这类工具竞争它只服务那些想保持记录习惯、但被重型工具劝退的人。典型使用场景是睡前打开终端敲一行命令写三五行文字关闭。整个过程不超过两分钟和打开手机备忘录差不多快但数据主动权完全在自己手里。后来有朋友问我caveman 适不适合用来记工作日志我直接说不太适合你要的是结构化模板和跨星期汇总那个交给正经笔记软件更合适。工具最怕的不是功能少而是服务对象不清晰。2. 存储与同步的返祖设计纯文本 Markdown 加本地优先2.1 为什么是 Markdown 而不是数据库这是整个项目最重要的决策也决定了后面所有功能的实现难度。caveman 的存储层就是一层文件系统目录没有任何数据库日记本体全是.md文件。选择纯文本的原因很朴素可存活时间最长的数据格式一定是人类可直接阅读的格式。数据库文件依赖软件版本、依赖导出工具、依赖平台兼容性一旦哪天应用不再维护导出数据这件事本身就会变成一个麻烦。而 Markdown 文本文件反过来——就算一百年后没有任何软件认识它用系统自带的记事本打开剥掉极少数标记符号内容依然完整可读。另外一个被很多人忽略的点是纯文本让数据所有权变得具体可感。存在数据库里的日记你只是拥有使用权存在目录里的一堆.md文件你就是字面意义上的拥有者。你可以随时复制、修改、对比、迁移不需要任何工具的配合。2.2 目录结构与文件命名让档案自己说话caveman 的存储结构长这样caveman/ ├── entries/ │ ├── 2025/ │ │ ├── 2025-01-15.md │ │ ├── 2025-02-03.md │ │ └── 2025-03-28.md │ └── 2026/ │ └── 2026-01-01.md ├── stats/ │ └── monthly.md └── index.md文件命名直接采用 ISO 日期格式YYYY-MM-DD.md这个选择不是随手定的。它带来两个免费的好处一是所有系统级的文件排序天然等于时间排序不需要额外的索引二是按某个月写日记这种最常见的回溯需求直接变成对目录的一次轻量扫描连数据库查询都不用写。每个文件内部的头部结构也保持极简# 2025-01-15 天气: 小雨 心情: 3/5 今天终于把拖延了三个月的体检预约做了。 并没有想象中那么难只是之前一直不想面对那个页面。日期字段从文件名读取天气和心情是两个可填可不填的元信息放在正文第一段位置。没有 YAML front matter没有复杂的 schema目的就是为了让人打开文件就能直接写不要被结构框住。2.3 同步方案我用 git 而不是自建云服务很多人一听说本地存储就问那换电脑怎么办多设备怎么办这个问题 caveman 的答案也很原始——用 git。冲突处理机制也自然有了边界真正同时编辑同一个文件的概率极低就算发生了git 的冲突标记至少能让你手动取舍而不是眼睁睁看着云端把某个版本悄悄覆盖。提示这里其实藏着一个同步思路的转变。不是我的数据在云端所以随时能拿到而是我的数据在本地同步只是为了给另一个本地副本搭桥。前者是中心化的依赖后者是对等的连接心理安全感完全不一样。3. 从需求到落地核心功能这样一步步长出来3.1 第一版 MVP 只做了三件事写、读、数caveman 第一版命令行界面只实现了三个命令命令作用caveman new创建今天的日记文件如果已存在则直接打开caveman show date按日期读取某一天的日记支持模糊匹配caveman stats统计本月的记录天数和累计字数我用 Python 写的核心依赖只有两个typer负责命令行参数解析edit依赖系统的$EDITOR环境变量完成文本编辑。没有 GUI没有后台服务没有常驻进程。new命令的逻辑简单得有点原始# caveman/cli.py第一版核心逻辑 cli.command() def new(date: str None): target date or datetime.date.today().isoformat() path entries_dir() / f{target}.md if not path.exists(): # 文件不存在才创建头部信息已有文件则直接进入编辑 path.write_text(f# {target}\n\n, encodingutf-8) # 调用系统的默认编辑器让用户在不学习新操作的前提下完成输入 subprocess.run([os.environ.get(EDITOR, vi), str(path)])这段代码现在看依然觉得舒服。它没有做任何额外的贴心操作只是确保文件存在然后把人交给一个已经用了几十年的编辑工具。这其实就是 caveman 哲学在代码层面的延伸——不在用户和文本之间再加一层自己的编辑器。3.2 安全渲染被 markdown 库的默认行为坑了一次当我想给 caveman 加一个--preview参数在浏览器里预览某一天的日记时踩到了第一个真正的坑Python 生态里常用的 Markdown 渲染库默认情况下并不会过滤原始 HTML 标签。这意味着什么如果你在某一天的日记里粘贴了一段包含script标签的内容——可能是复制网页时带进来的也可能是一时好奇测试——预览时它会被浏览器当作脚本执行。虽然日记是你自己写的威胁模型很小但自己的工具允许执行任意 HTML这件事本身就不对。解决办法是两层过滤叠加# caveman/render.py简化版 import mistune, bleach def render_diary(md_text: str) - str: # 第一步markdown 转 HTML html_body mistune.html(md_text) # 第二步只允许无害标签其余全部转义 allowed_tags [p, h1, h2, h3, ul, ol, li, blockquote, code, pre, em, strong, a] return bleach.clean(html_body, tagsallowed_tags, stripTrue)bleach.clean会把不在白名单里的标签直接剥掉并转义剩余内容相当于给渲染通道加了一道保险闸。这件事给我的教训是任何涉及用户输入的展示功能默认都应当按不可信输入来处理不管这个输入是不是你自己写下的。3.3 月度回望自动生成的统计归档文件stats命令后来升级成了一项我每天都期待的小功能每月自动生成一份monthly.md里面汇总当月写了多少篇、总字数、平均字数、最长的一篇是哪天。生成的逻辑不复杂本质是对目录做一次前缀过滤# caveman/stats.py核心逻辑 def monthly_stats(year: str, month: str) - dict: prefix f{year}-{month} # 形如 2025-07 files [] # 遍历 entries 目录匹配 YYYY-MM 前缀 for md in (entries_dir() / year).glob(*.md): if md.stem.startswith(prefix): files.append(md) total_chars 0 longest_day None for f in files: text f.read_text(encodingutf-8) chars len(re.sub(r\s, , text)) # 忽略空白字符 total_chars chars if longest_day is None or chars longest_day[chars]: longest_day {date: f.stem, chars: chars} return {count: len(files), total_chars: total_chars, longest: longest_day}说实话统计本身没有技术含量但它对坚持写作的意义比想象中大得多。人是需要正反馈的动物当你能直观看到这个月写了 12 天累计 8600 字哪怕内容全是流水账也会有哦我其实没断档的踏实感。caveman 不做花哨的图表一张纯文本统计表就够了——这也算是我对功能克制的又一次践行。4. 四个被我跳过的热门功能以及拒绝它们的理由4.1 标签系统目录结构本身已经足够几乎每个笔记类工具都把标签系统当作标配但日记场景里我强烈怀疑它的必要性。你写今天和同事因为项目进度不愉快你要打几个标签工作、冲突、情绪打完三个标签记录动作已经中断了一次这会直接伤害写作的连续性。caveman 的替代方案是让目录结构承担粗粒度的归档entries/2025/天然按年份分桶月度回望覆盖了按月统计的需求。那细粒度的主题检索怎么办答案很简单用grep。grep -rn 项目进度 caveman/entries/一行命令全目录全文搜索结果比大多数软件的标签检索还可靠。标签是一种需要刻意维护的元数据而全文搜索是无状态的瞬时操作。在日记这种写的时候不要被打断的场景里后者显然更合适。4.2 云同步与端到端加密别让安全功能成为门槛有一些开发者朋友看到 caveman 接 git 同步之后第一反应是你怎么不做端到端加密。这个问题我认真想过最终还是决定不做。核心矛盾在于真正防弹的端到端加密一定伴随密钥管理问题。你用什么保管私钥如果私钥丢失意味着所有历史日记都变成不可读的密文——对日记这种越老越珍贵的数据来说这个风险比云端泄露高得多。当然完全不加密也不是最优解。我采用了一种折中方案日记文件本身不加密但存放目录放在系统级加密卷macOS 的 FileVault、Linux 的 LUKS之下。系统层面兜底工具层面不额外作妖。安全功能的优先级不该高于数据能不能长期存活这个根本诉求。4.3 热力图与连续打卡警惕数据反噬写作动机GitHub 的提交热力图带火了一种连续打卡文化很多日记应用也跟进做了连续写 N 天的激励。我差点也动了这个念头后来想通了打卡统计会悄然改变写作的动机。当你开始在意连续天数这个数字你就会在某些实在没话说的日子写出今天没什么想记录的这种为打卡而存在的句子。这些句子没有记录价值只会制造一种虚假的高产幻觉。更糟的是一旦哪天真忘了写连续记录断掉很多人的第一反应不是无所谓继续写而是断了就不想补了——整条习惯链路随之崩塌。caveman 的stats只展示累计趋势不展示连续状态。这件事没有写在代码注释里但它是我有意为之的设计取舍工具能给的最小激励就是诚实的诚实本身。4.4 移动端 App物理隔离反而保护了写作仪式感关于手机端我做了个看起来完全不方便的决定不做 App在手机上也不看日记只管写。甚至我故意把同步仓库放在一个手机不太容易访问的位置。这种不方便是有意的。夜深人静想写点什么的时候打开电脑进入终端的过程本身就是一种心理仪式——它把一个琐碎的记录动作从刷手机时顺手记一笔变成了正经坐下来写几分钟。手机端的随手可用会稀释这种仪式感把日记降格成即时消息。对我个人来说caveman 的价值恰恰在于它保留了那点需要专门腾出时间的门槛。5. 踩坑记录纯文本方案也有三道坎5.1 中文文件名与 UTF-8 编码暗坑因为核心内容都是中文编码问题从一开始就绕不开。第一版代码里我犯过一个低级错误在 Windows 环境测试时把文件名按系统默认的 GBK 编码处理了一版导致同一批日期文件在跨平台 git 操作时出现乱码直接让同步仓库变得一塌糊涂。教训其实非常基础所有涉及文件读写的地方显式声明encodingutf-8绝不依赖系统默认值。上面代码里每次write_text和read_text我都写过encoding参数这词看起来啰嗦但它能省掉一整类跨平台问题。至于文件名本身用纯数字加连字符的2025-01-15格式天然避开所有编码陷阱——这也是我建议其他人做类似工具时坚持日期命名的原因之一。5.2 写入中断导致文件清空原子写方案另一个更隐蔽的坑出现在编辑器保存环节。最初我用最朴素的方式写文件content editor_output() path.write_text(content, encodingutf-8)听起来没问题但假如编辑器输出内容的过程中进程被中断比如系统崩溃、不小心关了终端文件可能处于被截断的状态原本的日记内容直接丢了。尤其可怕的是这种丢失往往要到下次打开文件时才发现。修复方案是标准的原子写# 原子写先写临时文件再替换原文件 import os, tempfile def atomic_write(path: Path, content: str): fd, tmp_path tempfile.mkstemp(dirpath.parent, suffix.tmp) try: with os.fdopen(fd, w, encodingutf-8) as f: f.write(content) os.replace(tmp_path, path) # 同一个文件系统内替换原子操作 except Exception: os.unlink(tmp_path) # 出错时清理临时文件 raiseos.replace在同一个文件系统内的替换是原子性的要么旧的完整保留要么新的完整生效不会出现半写状态。所有日记写入现在都走这条路径一次崩溃都没再出现过。5.3 编辑器与渲染层的边界源文件永远是唯一权威最后一个坑跟架构原则有关。早期我为了实现实时预览曾经尝试在编辑器之外自己维护一份格式化后的 HTML 副本结果很快发现这会造成双份数据的不一致——有一次我在 HTML 副本上做了编辑却忘记反写回 Markdown 源文件直接丢了两天的日记内容。这件事让我立下一条硬规则caveman 里源文件永远唯一权威渲染结果只是临时视图绝不反写。编辑器只负责编辑.md源文件预览功能每次使用都现场渲染不缓存任何中间态。这条规则看着简单但决定了整个项目后续每一个功能都往如何不污染源数据上想而不是往如何增加一套新数据上想。6. 实测两个月极简日记到底改变了什么6.1 数据说话两个月的使用统计从 11 月中旬到 1 月中旬我几乎每天都在用 caveman 记录。实际的生成数据长这样月份记录天数累计字数平均单篇字数最长一篇2025-11后半14 天823058811202025-1226 天1542059319752026-01前半14 天7610544830对比我此前用主流笔记 App 的三个月App 里总共只有 11 篇日记而且多数是为打卡而写的碎句。caveman 的单篇字数未必更高但它让我真正养成了每天睡前留十分钟的节奏。说到底工具能帮的最大忙就是把记录动作的摩擦成本压到最低不让我在要不要记这个决策上多犹豫一秒钟。6.2 极简界面带来的心理变化写给自己的字不需要被别人评分以前用笔记应用每篇日记旁边都有字数、日期、标签、可能还有点赞数。这些数字堆积在一个界面上形成一种隐形的评价压力——哪怕没有人在看你也会觉得自己在被评分。caveman 的界面里什么装饰都没有只有一个终端光标。这种无评价感带来的实际改变是我逐渐敢在日记里写一些不完整的、碎片化的、甚至逻辑不通的句子。反正没有热力图会为它们标红没有标签系统等着给它们归类它们就是纯粹写给未来的自己看的。写作的自由度比功能丰富的 App 时代高了不少。6.3 仍然想加的功能以及加功能前的最后一道安检两个月的使用让我摸清了真正的需求边界。现在最想加的不是标签也不是图表反而是一个更朴素的导出打印支持——把某个月的日记拼成一个干净的 PDF方便年底装订成册。这个需求是把档案感延伸到物理世界跟极简理念不冲突。不过每次动手前我都会过一道安检这个功能会不会增加写下第一笔的摩擦如果会直接砍掉。用这个标准去看大部分花哨功能在第一轮就会被淘汰剩下的都是真正为记录本身服务的东西。写到这里我想起项目最早的那行描述caveman, a tiny markdown diary for people who just want to write. 两个多月下来它确实做到了。如果你也想搭一个类似的工具我的建议是从一行命令开始从一月的 Markdown 开始别急着规划数据库也别在上面加任何未来你可能根本不会用的功能。先写起来比什么都重要。

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

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

免费获取报价 →
↑