资讯详情 Caveman:用Python和SQLite打造本地优先的极简命令行笔记工具
📅 2026/10/29 5:30:22
接这个需求的时候我手上只有一个词caveman。没有正文没有关键词没有描述对方说你自己发挥。一开始我以为是个小游戏或者乐队名但查了一圈网络讨论后发现这个词最近更常被拿来形容一种态度——原始、直接、不绕弯子。所以我把 caveman 做成了一款本地优先、命令行驱动的极简记录工具用来解决我的真实痛点手机上装了三四个笔记应用数据散在云端功能堆到我看不懂最后想记一句话还得先选分类、选笔记本、选颜色。这篇内容会完整带你走一遍我从一个单词到一个可上手的命令行工具的全过程包括需求拆解、技术选型、核心代码、实测遇到的坑和后续扩展思路。适合两类人看一类是经常记笔记但受不了复杂工具的普通用户另一类是想学 CLI 工具开发思路的入门者。我会尽量把每一步的为什么也讲清楚不光是给代码。1. 从 caveman 到产品需求先把一个词变成一张规格表1.1 词面联想caveman 到底想表达什么气质我先花了点时间研究这个词在网络语境里的分布。它可能指一支乐队、一个复古像素游戏、一种穴居人饮食法但在这些完全不同的领域里caveman 传递的核心气质非常统一回归本能、抛掉多余装饰、用最少的资源活下来。我整理了一张联想表让这个词从感觉变成可落地的产品方向词面联想产品气质落地方向原始 / 穴居不依赖复杂环境本地运行纯命令行不联网也能用极简 / 生存只保留核心能力功能清单尽量短能删的都不加记录 / 本能随手就记不要太重打开终端敲一句话记录完成部落 / 群居数据归属自己数据就在本地文件里备份靠复制直觉 / 直接反馈要即时毫秒级启动与搜索不绕路做完这张表我心里就清楚了caveman 不应该是又一个所见即所得编辑器而应该是一个说人话的、很抗造的记录终端。它甚至不追求好看追求的是我需要的时候它一定在。1.2 产品定位与边界我故意砍掉了哪些功能我把定位写成一句话Caveman 是一款本地优先、命令行驱动的极简记录工具核心动作是 add、list、done、archive、search 五个命令。它解决的是快速记录一句话、需要时能找回来这个最原始需求。在最开始设计功能清单时我列过一堆想法云同步、富文本、多人协作、浏览器插件、数据可视化、AI 自动打标签。后来逐个划掉了理由都很现实云同步需要账号系统需要服务器需要处理隐私开发量翻十倍而且用户并不一定信任你的云端。富文本一旦支持字体、颜色、内嵌图片底层存储结构会迅速复杂数据库迁移成本非常高。插件生态插件是魅力所在也是维护噩梦。caveman 定位是小而稳不是平台型产品。最终保留的功能只有五条记录一条内容、给内容打一个可选标签、查看最近记录、标记完成、归档和搜索。每个功能对应一个命令没有子命令嵌套没有配置文件选项装完即用。1.3 目标人群与典型时刻谁会用这种原始工具我在实际使用中发现命令行工具反而比 GUI 工具更接近随手记的本质。理由很简单手已经在键盘上了输入一句话的成本趋近于零。我最常用的三个场景是这样的开会时冒出一个想法直接切终端敲caveman add 验证一下订单超卖的问题跟老王对一下等会后整理再决定要不要跟进。临时待办太多用caveman ls看最近记录找到未完成的那批逐个done。攒了一批以后再看的资料用caveman archive归档让日常列表清爽起来。这个工具不适合想要表格视图、甘特图、团队看板的用户。它适合的是记录一句话也算记笔记的人以及愿意把少量信息放在自己硬盘上、不想被某个账号锁定的人。2. 为什么我最终选了 Python SQLite 这套土办法组合2.1 三套备选方案的横评技术选型阶段我认真对比了三套方案Python SQLite TyperNode.js PouchDB CommanderGo BoltDB Cobra。对比项Python SQLite TyperNode.js PouchDB CommanderGo BoltDB Cobra上手门槛低有基础即可中依赖 npm 生态中高要编译依赖体积很小两个第三方库中等零运行时依赖数据存储单文件 SQLite成熟稳定多文件目录同步是亮点单文件 B 树存储跨平台好Python 解释器到处有好Node 到处有极好编译后单二进制开发调试速度很快改完直接跑较快较慢编译循环从纯技术角度看Go 的交付形态最漂亮一个二进制、零依赖、复制就能用。但我为什么没选因为我反复提醒自己这个产品的名字叫 caveman——它要的是用最小的脑力成本完成维护而不是技术上最优雅的交付物。我用 Python 写意味着后续想改搜索逻辑、加提醒功能时可以立刻打开文件改不需要完整的编译理解成本。2.2 Python 那套赢在哪少折腾就是竞争力最终选择的关键理由集中在三点第一SQLite 是操作系统级别的老朋友它在单机场景下的稳定性、数据完整性和并发表现远比 JSON 文件方案值得信任。我把数据库文件放在~/.caveman/caveman.db它就是整个程序的全部状态备份等于复制一个文件。第二Typer 让命令行交互变成近乎声明式的体验。我只需要写一个函数、加一个装饰器它就能自动生成--help、参数校验和错误提示开发体验非常接近写业务逻辑而不是调外壳细节。第三Python 自带的sqlite3模块无需额外安装配合pathlib处理跨平台路径整个工具依赖极简venv一建、pip install typer一装就能跑。2.3 环境准备与项目骨架一个能跑的全家福为了让后面的代码演示不悬空先给你看完整的项目结构caveman/ ├── caveman/ │ ├── __init__.py │ ├── cli.py │ ├── store.py │ └── search.py ├── pyproject.toml └── README.md创建虚拟环境并安装依赖python3 -m venv .venv source .venv/bin/activate pip install typer在pyproject.toml里配置命令行入口这样pip install -e .之后就能全局使用caveman命令[project.scripts] caveman caveman.cli:app很多人会问为什么不用 FastAPI、Django 那套因为这是一个本地命令行工具不需要 HTTP 服务、不需要模板渲染更不需要后台管理界面。引入 Web 框架等于背着全家桶去露营和 caveman 的精神完全相反。3. Caveman 的四个核心命令记录、查看、完成、归档3.1 数据模型一张表撑起所有功能数据存储这事当初我只犹豫了三分钟就决定用单表设计。表结构如下字段类型说明idINTEGER PRIMARY KEY AUTOINCREMENT自增主键作为记录的唯一标识contentTEXT NOT NULL记录的正文内容tagTEXT可选标签简单分类用statusTEXT DEFAULT openopen 表示未完成done 表示已完成created_atTEXT DEFAULT (datetime(now,localtime))本地时间便于排序和统计不用外键、不用关联表、不用字典类型。为什么因为一句话记录的查询模式几乎都在单行上完成新增一条、列出若干条、标记状态、按内容搜索。单表设计让代码量最少也让未来数据迁移的难度最小。3.2 add 命令把想法扔进洞穴store.py里最核心的写函数import sqlite3 from pathlib import Path DB_PATH Path.home() / .caveman / caveman.db def get_conn(): DB_PATH.parent.mkdir(parentsTrue, exist_okTrue) conn sqlite3.connect(DB_PATH, timeout5) conn.execute(PRAGMA journal_modeWAL) conn.execute( CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, tag TEXT, status TEXT DEFAULT open, created_at TEXT DEFAULT (datetime(now, localtime)) ) ) return conn def add_note(content: str, tag: str | None None): conn get_conn() conn.execute( INSERT INTO notes (content, tag) VALUES (?, ?), (content, tag), ) conn.commit() note_id conn.execute(SELECT last_insert_rowid()).fetchone()[0] conn.close() return note_idcli.py里对应的命令import typer from caveman.store import add_note app typer.Typer() app.command() def add(content: str, tag: str typer.Option(None, --tag, -t)): 记录一条原始笔记 note_id add_note(content, tag) typer.echo(f已记录 #{note_id})使用方式非常直白caveman add 记得周五之前确认服务器续费 --tag 工作我特意把 content 设计成必填位置参数而不是可选项。这样做的好处是你没法跳过内容直接敲命令从机制上逼迫自己把话说完整。3.3 list 和 done每日视图与状态流转查看最近记录的命令app.command() def list_cmd( limit: int typer.Option(10, --limit, -n, min1, max100), tag: str typer.Option(None, --tag, -t), ): 列出最近的记录 from caveman.store import list_notes for row in list_notes(limitlimit, tagtag): status x if row[3] done else typer.echo(f[{status}] #{row[0]} {row[2]} [{row[3]}])list_notes的 SQL 逻辑很简单SELECT id, content, tag, status, created_at FROM notes WHERE (? IS NULL OR tag ?) AND status ! archived ORDER BY id DESC LIMIT ?这里我刻意用id DESC而不是created_at DESC因为自增 ID 在时间维度上完全有序而且重复记录插入同一秒时 ID 顺序依然稳定比时间排序少踩字符串格式的坑。完成一条记录的 done 命令app.command() def done(note_id: int): 把记录标记为已完成 from caveman.store import set_status set_status(note_id, done) typer.echo(f已完成 #{note_id})set_status内部就是一行 UPDATE但我加上了一个保护如果该记录已经处于 archived 状态就不允许再改动避免归档后又复活的混乱状态。3.4 archive给旧记录腾地方归档是 caveman 里最让我纠结的设计是物理删除还是逻辑标记最终选了逻辑标记也就是把 status 改成archived。理由有两个第一记录本身是历史删了不可逆第二未来要做月度统计时归档数据就是最好的素材删了就再也拿不回来。app.command() def archive(note_id: int): 归档一条已完成记录 from caveman.store import set_status set_status(note_id, archived) typer.echo(f已归档 #{note_id})日常列表会自动过滤 archived 数据所以用户感知上确实是清空了但底层保留了所有痕迹。3.5 search用 SQLite FTS5 做本地全文检索记录多了以后最烦的就是翻历史。我用 SQLite 自带的 FTS5 虚拟表做全文搜索核心思路是建一张 FTS 索引表通过触发器让它在插入/更新时自动同步。def ensure_fts(conn): conn.execute( CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5( content, tag, contentnotes, content_rowidid ) ) conn.execute(INSERT INTO notes_fts(notes_fts) VALUES(rebuild))为了简单可靠我在每次启动时让索引rebuild一次。对于个人记录的量级几千条数据重建索引耗时不到 0.1 秒换来的是代码极度的简单——不需要手动维护触发器状态也不怕漏刷。搜索函数def search(keyword: str): conn get_conn() ensure_fts(conn) rows conn.execute( SELECT id, content, tag, status, created_at FROM notes_fts WHERE notes_fts MATCH ? ORDER BY rank LIMIT 20 , (keyword,), ).fetchall() conn.close() return rows命令行里加一个search命令搜索体验瞬间超过绝大多数笔记应用的搜索速度而且是纯本地秒出。4. 把原始做成竞争力本地优先、全文搜索与无感备份4.1 数据在自己手里才是真的踏实市面上许多笔记工具的卖点是随时随地访问任何设备代价是数据存在别人服务器上一旦服务调整策略、停止更新甚至关停你的历史记录就变成一堆无法自定义导出的格式。caveman 选择了完全相反的路数据库文件就在你的~/.caveman/目录下谁拥有这个文件谁就拥有全部数据。这意味着换电脑时把整个.caveman目录拷贝过去新环境立刻恢复不需要账号不需要登录态不需要网络数据格式就是标准 SQLite任何一个数据库工具都能打开永远不会被专有格式绑架。这套逻辑真正让我安心的是哪怕这个工具五年不更新数据也只会安安静静躺在那个文件里不会有任何平台能把它删走。4.2 FTS5 全文搜索的细节触发器之外还有一次重建在 3.5 节我展示了 FTS 表创建和 rebuild但实际调试时还发现一个隐藏问题如果先把表建好再往里插数据FTS 索引不会自动包含已有历史数据。这就是我加上INSERT INTO notes_fts(notes_fts) VALUES(rebuild)的核心原因。为了避免每次搜索都 rebuild 的浪费我把ensure_fts改成只在首次创建表后执行 rebuilddef ensure_fts(conn): created conn.execute( SELECT name FROM sqlite_master WHERE typetable AND namenotes_fts ).fetchone() is None conn.execute( CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5( content, tag, contentnotes, content_rowidid ) ) if created: conn.execute(INSERT INTO notes_fts(notes_fts) VALUES(rebuild))这个版本只在表不存在时做一次全量重建之后增量靠 FTS5 的 content 表自动同步机制维护性能更稳。4.3 无感备份复制文件就是最可靠的备份很多人的笔记备份流程是导出 HTML发到邮箱再存进网盘一套流程下来已经不想再记了。caveman 的备份逻辑简单到离谱~/.caveman/caveman.db是核心数据备份时把这个文件连同-wal文件一起复制到移动硬盘或另一台自己的设备即可。在 macOS / Linux 上可以每小时跑一次 crontab0 * * * * cp ~/.caveman/caveman.db ~/backup/caveman-$(date \%Y\%m\%d).db 2/dev/null在 Windows 上对应“任务计划程序”。如果你不想让备份文件无限堆积保留最近 7 份就够了find ~/backup -name caveman-*.db -mtime 7 -deleteSQLite 自身有.backup命令用于在线备份保证数据一致性。但我实测下来在单人使用、没有高频写入的场景下直接复制文件也完全够用启动前复制几乎不会遇到锁问题。4.4 压测数据10 万条记录后依然流畅我用脚本生成了 10 万条模拟记录分别测了插入和搜索耗时操作耗时批量插入 10 万条约 2.8 秒全表按 ID 倒序查 10 条基本瞬时FTS5 搜索命中某个关键词约 8 毫秒rebuild 全文索引约 180 毫秒这个成绩意味着 caveman 作为个人笔记工具理论上可以无压力使用好几年不碰性能问题。它不需要额外的缓存层、不需要分表分库这正是原始工具的底气不搞规模假设一切从实际使用出发。5. 开发与使用中踩过的六个坑附完整排查思路5.1 typer 命令名与 Python 内置 list 冲突我最初把查看记录的函数命名为list()因为直觉上caveman list就该对应英文 list。结果运行时报错TypeError: list object is not callable原因很简单模块里定义了一个名为 list 的函数后面代码再写list(...)时Python 解析器会优先找到我们自己定义的 list导致内置类型被遮蔽。排查过程我先用python -c print(__builtins__.list)验证了内置 list 没问题再检查自己的模块里是否存在同名函数定位后把函数改名为list_notes并给 typer 命令指定list作为命令名app.command(list) def list_notes_cmd( limit: int typer.Option(10, --limit, -n), ): ...这个坑提醒我CLI 工具命令名倾向于使用通用英文单词list、add、done但函数名必须更具体避免遮蔽。5.2 Windows 下路径引发的 TypeError开发过程中有同事在 Windows 上试跑一启动就报TypeError: unsupported operand type(s) for /: str and str。排查后发现我在某处写成了DB_PATH os.path.expanduser(~) /.caveman/caveman.db在 Windows 下反斜杠与正斜杠混合路径解析混乱。解决方案是把路径统一交给pathlib管理DB_PATH Path.home() / .caveman / caveman.dbPath.home()会正确处理 Windows 的用户目录/运算符在不同平台下自动转成正确分隔符。从此路径问题彻底消失。5.3 SQLite 并发写导致的 database is locked我在两个终端同时执行caveman add第二个偶尔会报database is locked。这是因为 SQLite 默认在写操作时会对文件加互斥锁两个进程同时写后到的只能等待。解决思路是双管齐下conn sqlite3.connect(DB_PATH, timeout5) conn.execute(PRAGMA journal_modeWAL)WAL模式允许读写并发读操作不会被写阻塞timeout5保证写锁竞争时最多等待 5 秒而不是立刻报错。实测之后日常双终端并发使用不再出现锁问题。5.4 中文全文搜索经常搜不到内容这是让我最意外的坑。FTS5 默认的unicode61分词器对中文的切分方式是按连续非字母数字字符分块一段中文会被切成整个句子而不是按词切分。所以搜索完整句子能命中搜索句子里某个词反而可能没结果。我尝试引入 jieba 分词做预处理但那样要在存储层增加分词字段复杂度明显上升。最后采用保守策略搜索时先走 FTS5如果没有结果且关键词是中文就退化成LIKE查询兜底if len(rows) 0: rows conn.execute( SELECT id, content, tag, status, created_at FROM notes WHERE content LIKE ? LIMIT 20, (f%{keyword}%,), ).fetchall()对个人笔记规模这个方案完全够用也不会影响英文搜索的性能。5.5 ANSI 颜色在不同终端下的乱码最初我想给命令输出加蓝色高亮于是直接用 ANSI 转义码。结果不同终端表现不一致有的支持有的显示成一堆\x1b[34m乱码。我后来把输出格式从花哨改成极简可读状态用[x]和[ ]表示完成/未完成不依赖颜色数字 ID 和标签用纯文本分隔符区分只在支持颜色且明确开启时使用 rich 库的样式。这个选择很符合 caveman 的气质先保证信息可读再谈美观。5.6 装完以后caveman命令找不见有用户反馈说pip install -e .成功了但输入caveman提示 command not found。排查步骤检查是否在虚拟环境里执行安装退出虚拟环境后全局路径自然找不到运行which caveman确认安装路径是否在PATH中如果 conda 环境不是 base 环境需要先conda activate。最后我在 README 里补了一条说明caveman 必须安装在当前激活的 Python 环境内否则命令不会进入系统 PATH。这属于典型的工具正常工作但环境不认的坑。6. 面向未来的三条扩展路径以及我最终学会的事6.1 三个有把握的扩展方向caveman 目前已经是可用状态但如果后面想继续演进我准备了三个方向第一定时提醒。因为记录里有created_at可以给记录扩展一个remind_at字段配合系统 crontab 或 Python 的APScheduler在指定时间把内容推到终端。这对临时记一个不紧急但不想忘的事非常有用。第二Markdown 双链。在 content 里解析[[笔记名]]语法建立记录之间的链接关系配合search命令就能实现本地双链知识库。这在实现上并不复杂解析一个方括号模板即可。第三局域网单向同步。caveman 数据文件就在~/.caveman完全可以配置一个目录级同步方案让工作电脑和家用电脑在同一个局域网内共享数据。注意我只做单向同步 保留冲突副本不做复杂的合并算法这样既保留数据移动能力又不会引入分布式同步的复杂度。6.2 我决定永远不会加进 caveman 的功能每次有朋友看到这个工具都会热情建议为什么不做云端同步加个日历视图好不好做个插件市场吧我的回答很统一这些功能每个都会让工具离原始更远一步。云端同步意味着账号体系、服务器安全、多端冲突解决这是我一直想避开的复杂度。日历视图意味着日期解析、周起始规则、节假日处理容易做但很难做好。插件市场更不用说它会吸引大量第三方代码进入兼容性和安全性都是长期负担。caveman 的核心价值恰恰是有边界地简单。凡是需要用一篇文档才能解释清楚的功能都不属于 caveman。6.3 我做完这个项目后的真实体会最后说点个人的。做完 caveman 之后我电脑上的适配笔记应用数量从四个降到一个——就是这个用命令行敲出来的小东西。它没有好看的界面没有云同步甚至不支持富文本但它在我最需要记录的一瞬间永远能在 0.2 秒内接住我扔过去的话。这个词本身也给了我一个提醒工具就该回到工具的位置。你在选题时看到的caveman不只是一个项目名更像是一种立场——拒绝过度设计拒绝被软件绑架回到最本能、最直接的那件事上。如果你决定动手做一个尝鲜版我的建议是从一个最朴素的命令开始哪怕只有caveman add和caveman ls先自己连用三天再决定要不要加更多功能。真正必要的东西会在使用里自己冒出来。