如果你和我一样,浏览器收藏夹里躺着几百个“稍后读”,剪贴板里反复粘贴同一段文献却找不到最初出处,笔记软件里堆着半年前随手拍的书页照片——那你大概率需要一套顺手的工作流。这套我命名为knowledge-work-plugins的插件体系,就是为这些知识工作碎片设计的。它不是一个大型软件,而是一组彼此独立又能串成流水线的小插件,专门处理网页摘录、PDF还原、语音转写、知识库检索这类“不上不下”的脏活。适合内容创作者、做研究的人,以及所有靠大量阅读和写作吃饭的岗位。接下来我把整个项目从起因到实现拆开讲,代码和配置都直接给。
1. 从“收藏即遗忘”到“捕获即结构”:这个项目想解决的痛点
1.1 动手之前的几个真实场景
我最早意识到非改工作流不可,是在一次整理季度报告素材时。当时要引用某个论坛帖子里的几张数据表,结果发现浏览器收藏夹里的原始链接已经失效,而剪贴板里还留着当时复制的那行文字,但一点上下文都没有了。类似情况反复出现:PDF复制出来带硬换行,粘贴进Markdown之后每行都断在奇怪的位置;会议录音转写出来的文本没有标点分段,想搜索一句话根本做不到全文匹配;用过的笔记软件至少换过两个,每次迁移都要重新考虑数据格式和兼容性。
这些问题单独看都不严重,但叠在一起就是持续的信息损耗。我统计过自己一周的工作流:大约有三十多次从网页、PDF、聊天记录里复制信息,其中一半以上最终没有进入任何笔记文件;真正进入笔记的,又有相当一部分缺少来源链接、摘录日期和原文上下文。换句话说,收藏和复制这个动作太容易了,但把一小段信息变成可检索、可引用、可关联的知识资产,这个过程没有工具帮我们完成。
1.2 设计原则:本地优先、纯文本为王、模块化
动手设计 knowledge-work-plugins 前,我给自己定了三条原则。
第一,本地优先。信息要保存在自己可控的目录里,不强制依赖某个在线服务,避免服务停摆导致数据丢失。同时本地处理速度更快,还能做到离线可用。很多知识管理工具的问题在于,只要网络不好或者服务商调整策略,整套工作流就瘫痪了。
第二,纯文本为王。所有中间产物统一用 Markdown 存储,而不是各自私有的二进制格式。纯文本的好处是肉眼可读、可 diff、任何编辑器都能打开,就算这个插件体系以后不维护了,零散的.md文件也照样能在新工具里用。从长期主义角度看,这比任何花哨的数据库格式都稳妥。
第三,模块化。每个插件解决单一问题,通过命令行和文件系统协作,而不是耦合在一个大进程里。这样我可以单独使用pdf-digest处理几篇论文,也可以把clip-catcher接上snippet-gen形成完整的“摘录到卡片”流水线。模块各自独立演化,不会出现改一个功能连带炸掉整个系统的情况。
三条原则贯彻下来,项目骨架其实很朴素:一个总入口,十个插件模块,一组约定好名称的目录。重点不是在“开发”上花功夫,而是花功夫设计出一条从原始信息到结构化知识片段的稳定管道。
2. 插件体系划分与数据流转
2.1 模块清单与职责边界
整套插件的目录和模块划分如下,我把每个模块的职责、输入输出以及依赖都写在表里。
| 模块名 | 职责 | 输入 → 输出 | 关键依赖 |
|---|---|---|---|
| clip-catcher | 剪贴板监听、网页正文提取与清洗 | 网页链接或剪贴板HTML → 干净的Markdown正文 | pyperclip, readability |
| pdf-digest | PDF文本提取、结构还原 | PDF文件 → 带标题层级的Markdown文档 | pdfplumber, ocrmypdf |
| voice-transit | 语音转写后的文本整理 | 音频文件 → 分段落、有标点的文本 | faster-whisper, webrtcvad |
| markdown-forge | Markdown写作增强 | Markdown文件 → 自动补全目录、脚注、链接 | Python标准库 |
| local-succ | 本地知识库检索 | 查询词 → 相关笔记列表 | SQLite FTS5, jieba |
| snippet-gen | 素材卡片生成 | 笔记库 → 引文卡片、摘要、周报素材 | pandas, jinja2 |
| title-helper | 标题与关键词推荐 | 文本正文 → 候选标题、标签 | jieba, sklearn TF-IDF |
| link-checker | 链接与引用检查 | Markdown文件 → 失效链接清单 | requests, bs4 |
| archive-mover | 文件归档整理 | 散落目录 → 按日期/标签归档 | watchdog |
| daily-review | 每日回顾卡片 | 当日新增笔记 → 回顾摘要 | jinja2 |
这些模块不是一开始就全部规划好的,而是我在真实使用中逐步拆出来的。一开始只有 clip-catcher 和 pdf-digest,后来发现检索环节缺了,才补上 local-succ;再后来写作时反复手动做目录和脚注,才写了 markdown-forge。模块化设计最大的好处就在这里:按需生长,每个组件都已经在真实场景里验证过了。
2.2 数据流水线:捕获、净化、入库、加工、输出
模块再多也得有一条清晰的流水线,不然就是一堆孤立脚本。我在机器上维护的目录结构是这样的:
~/kp/ ├── raw/ # 原始抓取内容:网页HTML、PDF原文、音频文件 ├── clean/ # 清洗后的Markdown文本,按来源命名 ├── notes/ # 结构化笔记,带YAML front matter ├── cards/ # 单条知识卡片,适合后续引用 ├── assets/ # 图片、附件等二进制资源 └── db/ # SQLite元数据库与检索索引整个数据流可以概括为“捕获 → 净化 → 入库 → 加工 → 输出”。以网页摘录为例:clip-catcher 捕获链接后把原始 HTML 存进raw/,然后抽取出正文并存为 Markdown 到clean/;接着 local-succ 的索引器扫描clean/下的文件,把标题、正文、来源写入 SQLite;之后 snippet-gen 可以按标签生成卡片;最终 markdown-forge 负责在写作时把这些卡片嵌入文章。
数据流的关键点是中间态全部落在文件系统上。每个插件读取文件、处理、输出新文件,下一步插件再读取,中间不需要进程间通信。这样调试特别方便:某一步结果不对,直接打开对应 Markdown 就能检查,问题出在哪一目了然。
2.3 选型理由:为什么是 Python + SQLite + Click
技术栈我刻意保持极简:Python 3.10 作为主要实现语言,SQLite 做元数据存储,Click 做统一命令行入口。Python 的选择没什么悬念,生态里 PDF 解析、语音转写、文本清洗都有相对成熟的库,而且写脚本和写插件的边界非常灵活。SQLite 才是这个项目真正的重心,它单文件、零配置、支持 FTS5 全文检索,对于个人知识库这种千万级以下规模的数据量来说完全够用。Click 让我把所有模块统一收敛到kp这个命令下,使用体验类似git的子命令结构。
kp capture --url "https://example.com/article" kp digest --pdf paper.pdf kp transit --audio meeting.m4a kp search "知识管理 插件" --rank-vector kp card --id 128 --tag "灵感"这套方案的取舍很明确:放弃高并发、放弃分布式,换取零部署成本和极低的维护负担。对个人知识工作场景来说,长期可维护性远比极端性能重要。
3. 核心插件拆解:摘录清洗、PDF还原与语音转写整理
3.1 clip-catcher:剪贴板监听与HTML正文提取
clip-catcher 是这个体系里使用频率最高的模块,它解决的核心问题是“从网页复制内容时,顺便把结构和来源一起存下来”。我最初用 pyperclip 做轮询剪贴板,检测到复制动作后,如果剪贴板内容是 URL 就触发网页抓取;如果复制的是富文本 HTML,就做清洗转换。这里最耗时间的部分是正文提取,我参考了 readability 的处理思路,但加了针对中文网页的专属规则。
清洗流程的核心步骤包括:去掉导航栏、页脚、广告区块;把<div>嵌套造成的噪声标签移除;修正相对链接为绝对链接;最后把所有标签替换为 Markdown 语法。实测处理一个 20KB 左右的典型中文博客页,从抓到输出干净的 Markdown 大约耗时 1.2 秒,其中网页下载占大头,清洗本身只要几百毫秒。如果页面用了大量 JavaScript 渲染,仅靠 requests 抓不到正文,我会改用有头浏览器渲染一次再抓取,这部分在踩坑章节会详细说明。
去重逻辑我也单独实现了一份。因为复制同一篇文章的来源不同,存下来的正文可能略有差别,我用一个简化版的 simhash 指纹对正文做指纹计算,如果与已入库条目汉明距离小于等于 3,就判定为重复内容,并自动给新条目附加一条 source alias,而不是再插入一条新记录。
3.2 pdf-digest:从 PDF 到大纲和正文
处理 PDF 的难点不是“取出文字”,而是“还原结构”。pdfplumber 能给出每个文本块的位置、字体、字号、是否加粗,我利用这些信息做打分:字号大于页正文 1.4 倍且加粗的文字块,标记为一级标题;字号介于 1.1 到 1.4 倍的标记为二级标题;同一段连续文字块按垂直坐标聚类为段落。这样处理后的输出可以直接看到大纲,我一般会先扫一遍标题层级,再决定要不要深入读正文。
# 简化版:按字号和是否加粗为文本块打标题分 def heading_score(char): size = char["size"] bold = char["fontname"].lower().__contains__("bold") score = 0 if size > 11.5: score += 1 if size > 13.5: score += 1 if bold: score += 1 return score表格提取是另一个刚需场景。pdfplumber 的extract_table在绝大多数版面规整的论文里表现不错,但如果单元格内换行太多,提取出的表格会散架。我的处理策略是:先尝试标准表格提取,然后对每个单元格做去尾空白和换行合并,最后统一转成 Markdown 表格语法。这套流程用在文本版 PDF 上,标题还原的准确率能达到 95% 以上,表格提取成功率高但并非百发百中,遇到合并单元格多的表还是要人工检查。
至于扫描版 PDF,pdfplumber 直接拿不到文本。我的应急方案是把 PDF 先喂给 ocrmypdf 做 OCR,再走后续流程。OCR 之后的使用感受是:文字检索没问题,但标题还原会受识别错误影响,准确率掉到 85% 左右。所以扫描版论文我一般建议配合原版 PDF 一起看,不要完全依赖抽取结果。
3.3 voice-transit:语音转写后的智能整理
voice-transit 是我后补的模块,起因是一次两小时的评审会议,转写软件给出的文本是一长串没有标点的“文字流”,基本没法快速浏览。我的方案分三步:先用 faster-whisper 做本地语音转写,再做自动化文本整理,最后按段落打上关键词标签。
faster-whisper 的模型选择直接影响效率。在我测试的机器上,10 分钟的无损录音配合 small 模型,CPU 大约要 6 分钟,GPU 约 1 分钟,显存占用在 2GB 左右;如果换 medium 模型,CPU 耗时直接拉到 20 分钟以上。对于内部工作流来说,small 模型准确率是完全可接受的。转写之后,文本还是连续无标点状态,我用一组正则做标点修复和段落重排:中文后紧跟英文逗号句号的替换为中文标点,连续超过 60 字没有标点的句子按语气词和停顿词切分,遇到“但是”“所以”“接下来”这类转折词时自动做分段。
# 转写文本的标点和分段修复 def repair_punct(text: str) -> str: text = re.sub(r"([\u4e00-\u9fa5])[,;!?]", lambda m: m.group(1) + ",;!?"[",;!?".index(m.group(2))], text) text = re.sub(r"([。!?])", r"\1\n", text) return "\n".join(line.strip() for line in text.splitlines() if line.strip())最后一步关键词标记用的是 jieba 分词后取 TF-IDF 权重最高的 5 个词,落到文件的 front matter 里。这样后续检索时,哪怕不记得原文完整句子,只记着一个主题词也能定位到对应段落。这套流程让语音转写文本从“没法读”变成了“可以搜、可以快速浏览”,价值提升很明显。
4. 本地知识库的检索设计
4.1 中文搜索的麻烦:默认分词不够用
本地知识库有了大量 Markdown 文件之后,检索就变成最核心的需求。我最开始直接用 SQLite FTS5 的 unicode61 分词器,英文效果好,中文却一塌糊涂,因为 unicode61 会把连续的汉字当成一整个 token,搜“知识管理”时完全匹配不到“知识工作插件”这类自然语言变体。
解决办法是引入 jieba 分词器,在建索引时先对标题、正文做分词,再把分词结果拼接成空格分隔的 token 交给 FTS5。注意建表和插入语句都要保持同一套分词逻辑,否则索引和查询的分词结果不一致,查出来的内容会莫名其妙对应不上。我只对标题和正文做 FTS 索引,front matter 里的标签单独建普通 B-tree 索引,这样查tag: 灵感时可以直接走整列扫描而不需要全文匹配。
4.2 FTS5 + jieba 自定义分词表
具体实现上,我把百科式的通用词表和自己的领域词表合并成一份 userdict,里面存放那些 jieba 默认会分错的词,比如某些专业术语、产品名、人名。词表格式是每行“词语 频率 词性”,维护成本很低,但收益非常大。比如默认分词会把“知识工作”切成“知识”和“工作”,导致检索“知识工作”时出现大量无关结果;加入词典后把它当一个整体,精度立刻不一样。
建表语句如下:
CREATE VIRTUAL TABLE fts_entries USING fts5( title, body_md, content='entries', content_rowid='id', tokenize='unicode61' );写入时先调用split_text函数产出分词串:
kp index --rebuild重建一万篇笔记索引大约耗时 3 秒,基本可以随时跑。FTS5 的 bm25 排序在短文本场景下表现稳定,默认排序结果基本符合预期,不需要额外调权。
4.3 拼音索引:容忍错别字和读音联想
中文搜索另一个高频需求是输入拼音也能找到结果。我一开始觉得“谁会只记得拼音”,但真实场景里偏偏经常发生:说话提到某本书名,只记得大概发音,不记得准确汉字。为此我建了一张拼音索引表,每个条目对应一组带声调和去声调的拼音串。查询时先判断输入是不是包含非汉字字符,如果看起来像拼音,就先用拼音表做候选召回,再拿到候选 ID 去 FTS 里二次精确验证。
CREATE TABLE pinyin_map ( entry_id INTEGER, pinyin TEXT, phrase TEXT );拼音索引的维护成本很低,因为只有新增笔记时才需要生成一次。实测查“zhishiguanli”能正确返回“知识管理”相关笔记,误召回率也在可接受范围内。但拼音搜索在语句级的准确率还是不如汉字直接搜,所以我把它定位为容错插件,不改变主检索逻辑。
4.4 轻量向量召回:关键词匹配不够时的补充手段
FTS5 能解决“记录了原文出现过的词”的问题,但解决不了“语义相似但字面不同”的问题,比如搜“怎么提升专注力”却找不到正文写的是“番茄工作法的具体操作”。向量召回可以补上这一环,但我不希望引入重型服务,所以用了本地嵌入模型。
方案是加载一个体积约玩数百兆的通用嵌入模型,把标题和正文前 512 字编码成向量,存入 SQLite 扩展出的向量列。查询时同样对查询词编码,然后与全库向量做余弦相似度排序。暴力检索一万条向量的耗时约 2 秒,个人笔记库完全能接受。
最终检索结果我用 RRF 融合算法合并 FTS 和向量两路候选:每路只取前 50 条,按排名倒数加权求和。实测对比下来,纯 FTS 的 Top10 精确率大约 78%,加上向量召回后能提到 85% 以上,代价是少量相关度不高的结果会混进来。对个人知识探索场景来说,稍微多几个候选总比漏掉重要信息好。
5. 写作增强与素材回捞
5.1 markdown-forge:目录、脚注与内部链接
知识工作到最后总要落到写作上,markdown-forge 专门解决 Markdown 写作过程中那些“手动做很烦、不做又不行”的琐事。比如长文目录,一篇一万字的文章要手工维护目录索引很痛苦,我写了一个遍历标题的脚本,按#、##、###层级自动生成带锚点的 TOC,插入到文档指定位置。
脚注管理也很实用。我在写作时习惯给每个引用出处标序号,但粘贴文献时经常顺序错乱,markdown-forge 会自动重排所有[^n]脚注编号并确保引用块定义都在文末。内部链接则支持[[笔记标题]]的 wikilink 语法,写作时随手写一个笔记名,发布前再统一解析成相对路径链接。这样做的底气是知识库文件名都按“YYYY-MM-DD-slug”统一命名,链接稳定性有保障。
5.2 snippet-gen:从笔记库生成卡片与引用块
snippet-gen 是把知识库的素材“捞”到当前文档的模块。它的一个核心功能是从cards/目录里读取标签体系下所有知识卡片,按标签和创建时间筛选,然后以干净文本或 Markdown 引用块的形式输出。
kp snippet --tag "用户洞察" --limit 5 kp snippet --note-id 128 --as quote这个命令在写方案类文档时特别有用。比如用户研究报告里要放几条原始访谈引语,我只需要根据标签筛选候选片段,再人工挑出最合适的三四条,而不是翻聊天记录和会议纪要找半天出处。每张卡片本身还保留了来源链接和摘录时间,所以引用的时候不会出现“这句话到底哪来的”这种尴尬。
snippet-gen 另外附带一个每日摘要功能,把当天新增的笔记按来源归类生成一份简短的回顾文档,配合 daily-review 模块展示给第二天早上的自己。这让我养成了“当天摘录、次日回顾”的习惯,知识利用效率明显提升。
5.3 一个真实的写作流程演示
我把这套流程串起来举一个实际例子。假设我要写一篇关于“如何整理会议录音”的实践总结,完整流程是这样:先在浏览器里看到一篇相关的长文,用 clip-catcher 抓取并存为笔记;再把一份此前录制的访谈音频丢给 voice-transit 转写整理;随后用 local-succ 全文搜索所有与“会议记录”“字段抽取”相关的旧笔记,顺手把两年前的某篇卡片也捞回来;接着用 snippet-gen 选几条适合引用的原文片段;最后打开 markdown-forge 补目录、理顺脚注、生成内部链接。
整个过程从收集到成稿大约一个下午,其中真正花在“写”上的时间只有一半,另一半如果全靠手动整理,往往会拖到一整天。这就是插件体系带来的结构性效率提升。
6. 踩坑记录与调优实测
6.1 playwright 页面渲染与反爬限制
第一个踩坑发生在网页抓取环节。一开始 requests 抓静态页面很好用,但碰上由 JavaScript 渲染的页面,抓回来只有壳没有正文,调试时肉眼能看到浏览器里是完整内容,代码里却拿不到。后来我对疑似动态页面做无头浏览器渲染,效果是好了,但引入了两个新问题:无头浏览器容易被目标网站识别,有些站点会直接返回验证页面;而且每次渲染要启动一个完整浏览器进程,内存开销一下高了非常多。
调试后的折中方案是保留三种抓取策略:静态请求优先,静态请求判断正文长度不足时用无头浏览器渲染一次,仍失败则记录到失败队列人工处理。实测动态页面抓取成功率从不足 60% 提到了 92%,剩下的 8% 基本是强登录或强验证站点,不在我追求的范围内。
6.2 扫描版 PDF 的 OCR 折腾
pdf-digest 最初只做文本版 PDF,对扫描版一律输出“无文本层,需要OCR”的提示。后来处理一批早期扫描文献时实在需要内容,才引入 ocrmypdf + tesseract 的组合。踩的坑主要是中文扫描件识别质量不稳定:清晰扫描件识别能到 85% 以上,但斜着拍的、带水印的、纸张泛黄的,识别出来的文字经常把“工”识别成“土”,把“的”识别成“白勺”。
我的应对是在 OCR 前先做图像预处理:转灰度、自适应二值化、轻度去噪,让识别率稳定一些。另外 OCR 产出的文本要经过 markdown-forge 的标点修复才能进入下游流程,不然检索时会被断句错误拖累。折腾完这批文献后,我的原则变成:文本版 PDF 走 pdf-digest,扫描版只对必要且量少的文献走 OCR,大批量扫描数字化还是得依赖更适合的专用方案。
6.3 FTS5 的 UNICODE 分词坑与自定义词典
FTS5 的 unicode61 分词器对中文处理确实是硬伤,但比它更隐蔽的坑是索引与查询的分词一致性。曾经出现过明明正文里有“本地优先”,用“本地优先”四个字搜反而搜不到,后来排查发现是建索引时用了 jieba 分词,查询时却走了默认 unicode61——两者切出来的 token 完全不同。
修复方式是把分词函数统一封装成一个入口,索引和查询都调用同一函数。同时维护一份自己的词典,把 jieba 容易切错的领域词汇手动加入。这一项投入不大,但直接决定了检索质量的上限。投入产出比最高的工作就是把长期高频出现的专有名词整理成 userdict。
6.4 SQLite 并发写锁的缓解方案
SQLite 的写锁问题在单用户场景下几乎不是问题,但我的模块里 clip-catcher 会持续监听剪贴板,daily-review 又在早上定时跑,两者偶尔同时触发写库时,会出现database is locked。
缓解方案分三层:一是所有写事务尽量缩短,插入前先做完清洗,不要在事务里跑耗时操作;二是给关键写操作加busy_timeout,等待 5 秒而不是立即失败;三是把高频写入的采集动作与低频整理动作拆到两个数据库文件里,采集库只做快速存储,整理任务再同步到检索主库。三层下来基本没有再碰见过锁冲突。如果以后条目规模再上一个量级,我会考虑迁移到 PostgreSQL,但目前个人库的量级实在没必要。
6.5 最终配置与参数建议
我把几个踩坑后确定的配置列成一个速查表,方便大家直接抄作业。
| 事项 | 推荐配置 | 说明 |
|---|---|---|
| FTS5 tokenize | unicode61 + jieba 统一入口 | 禁止索引与查询使用不同分词 |
| 网页抓取 | 静态请求优先,无头浏览器兜底 | 对强验证站点人工处理 |
| 语音转写 | faster-whisper small 模型 | CPU 可跑,准确率够用 |
| 扫描版 OCR | 先二值化预处理再 OCR | 不接受只 OCR 原始件 |
| SQLite 写并发 | busy_timeout=5000,拆双库 | 单用户场景足够 |
| 向量检索 | 本地小模型,暴力检索 | 万条规模 2 秒内可接受 |
这套组合跑了一段时间后,我自己最大的感受是:收藏动作依然高频,但每一份摘录都有了去处和来源;需要回看某个知识点时,总能在一个命令内找到上下文。最后再分享一个小技巧:我用系统自带的定时任务每天早上触发一次kp daily-review,它会把昨天所有新笔记串成一张“回顾卡片”,有时还能根据关键词自动关联到一篇老笔记。这个习惯让我连“遗忘的收藏”也已经变成了每天会被重新打捞一遍的资产。知识工作插件的价值不在某个插件多聪明,而在整套流水线让信息从“被复制”到“被使用”的距离变得更短。