用 Obsidian 或纯 Markdown 记笔记,前 50 篇很爽,过了 100 篇就开始烂:索引文件永远是上周的版本,新建的笔记没进去;凭印象写了 [[某篇笔记]],其实那篇还没建,跳过去是空的;有些笔记写完之后再也没被任何一篇引用过,等于白写。
手工维护索引这件事,本质上是"每次新增/改名都要记得去改三处"——只要有一处漏了,索引就开始失真。凡是靠"记得"的流程,最后一定会烂。所以正确做法不是勤快一点,而是根本不手工维护。
让脚本扫一遍目录自动生成索引,顺手把断链和孤儿笔记报出来;再让 WorkBuddy 只读这个索引文件做检索,不要把 200 篇全文都塞进上下文。
适用边界:本地 Markdown 库、文件在 1000 篇以内、以 wikilink 为主要串联方式。超过这个规模或者笔记散落在多个网盘/在线文档里,这套就不够用了,得换成检索服务。
索引脚本最怕的是每篇笔记格式都不一样。只强制三件事,别的随便写:
.md,根目录或子目录都行;title / tags / updated,不填也能跑(会退化成取文件名和首个 # 标题);[[双链]]。---
title: 选题池维护
tags: [内容运营, 流程]
updated: 2026-10-18
---在库根目录建 .tools/kb_index.py(完整脚本见文末附件,约 200 行,零第三方依赖,Python 3.9+)。核心就四件事:
# 1) 跳过不该扫的目录(隐藏目录一律跳过)
SKIP_DIRS = {".git", ".obsidian", ".trash", ".trash-backup",
".workbuddy", ".tools", "node_modules", "__pycache__"}
# 2) 提取 wikilink 之前,先把代码块围栏剔掉,
# 否则教程里写的 [[示例]] 会被当成真链接,凭空多出一堆断链
FENCE_RE = re.compile(r"```.*?```", re.S)
WIKILINK_RE = re.compile(r"\[\[([^\]\|#]+)(?:#[^\]\|]*)?(?:\|[^\]]*)?\]\]")
def extract_wikilinks(body: str) -> list:
cleaned = FENCE_RE.sub("", body)
cleaned = re.sub(r"`[^`\n]*`", "", cleaned)
return [nfc(x.strip()) for x in WIKILINK_RE.findall(cleaned) if x.strip()]
# 3) 建别名表:文件名 / 相对路径 / frontmatter 标题,都算可链接目标
for n in notes:
alias.setdefault(nfc(n.path.stem), n) # 文件名
alias.setdefault(nfc(n.rel[:-3]), n) # 相对路径
alias.setdefault(nfc(n.title), n) # 标题
# 4) 匹配不上的就是断链;入链为 0 的就是孤儿笔记关键设计:默认只读。 只有显式加 --write 才落盘,别的一律只打印报告。这条救过我——第一次跑脚本时正则写错了,要是默认就写文件,整个索引会被写成一堆垃圾。
# macOS / Python 3.13
cd ~/Documents/MyVault # 换成你自己的库路径
python3 .tools/kb_index.py .输出长这样:
====================================================
知识库:/Users/lee/Documents/MyVault
====================================================
笔记总数 : 8
wikilink : 10 条,解析成功 8 条
断链 : 2 条
✗ 02-方法论/自动化选型 → [[失败自愈]]
✗ 02-方法论/自动化选型 → [[定时任务排查清单]]
孤儿笔记 : 3 篇
· 03-周报/2026-10-06周报
· 03-周报/2026-10-13周报
· README
耗时 : 0.7 ms
====================================================
(只读模式,未写文件。加 --write 落盘)python3 .tools/kb_index.py . --write生成的 00-索引.md 包含三部分:按目录的笔记表(标题/标签/更新日期/入链数)、⚠️ 断链清单、🔍 孤儿笔记清单。表格里用的是 [[路径\|标题]] 写法,在 Obsidian 里点击可直接跳转。
文件头会自动加一行提醒,防止手改:
> 本文件由 `kb_index.py` 自动生成,**请勿手工编辑**,改了会被下次运行覆盖。macOS 用 launchd(比 crontab 靠谱,crontab 在 macOS 上遇到全盘权限和睡眠唤醒容易漏跑):
cat > ~/Library/LaunchAgents/com.user.kbindex.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.user.kbindex</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/lee/Documents/MyVault/.tools/kb_index.py</string>
<string>/Users/lee/Documents/MyVault</string>
<string>--write</string>
</array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>30</integer></dict>
<key>StandardOutPath</key><string>/tmp/kbindex.log</string>
<key>StandardErrorPath</key><string>/tmp/kbindex.err</string>
</dict>
</plist>
EOF
launchctl load -w ~/Library/LaunchAgents/com.user.kbindex.plist
launchctl list | grep kbindex # 确认已注册这是省积分的关键一步。200 篇全文塞进上下文,光输入 token 就烧掉一大截;而索引文件只有几 KB。
提示词模板(可直接复制):
读取 ~/Documents/MyVault/00-索引.md 这个文件(只读,不要扫描其他文件)。
基于其中的「按目录」表回答我的问题:{你的问题}
如果索引里的信息不足以回答,告诉我你还需要读哪几篇具体笔记,
把文件名列出来等我确认,不要自己去遍历目录。配合 WorkBuddy 5.8.0 的独立文件浏览器窗口,可以一边在主窗口对话,一边在独立窗口里翻 Markdown,互不遮挡;5.8.0 起资料库还支持上传压缩包并用于后续检索,大库可以先打包再问。
以下均为本机(macOS + Apple Silicon,Python 3.13.12)真实执行结果,跑 3 次取 min/avg/max:
库 | 笔记数 | wikilink 数 | 断链 | 孤儿笔记 | 扫描+分析耗时 |
|---|---|---|---|---|---|
演示库(虚构示例) | 8 | 10 | 2 | 3 | 1.2 / 1.7 / 2.0 ms |
本人真实在用库 | 196 | 141 | 10 | 155 | 26.3 / 47.6 / 81.3 ms |
三个可直接用的结论:
脚本另带 --json 输出,方便接别的自动化;带 --fail-on-broken,有断链时退出码为 1,可以直接当 CI 或定时任务的门禁用。
坑 1:macOS 中文文件名的 NFD/NFC 归一化(真正会静默出错的那个)
macOS 的文件系统保留文件名的原始 Unicode 形式。同一个词,é(预组合)和 e + ◌́(分解)在字节上完全不同。如果文件名是分解形式、你的 wikilink 是预组合形式,朴素字符串匹配会失败,而且不报错,只是默默算成断链。
本机实测(名字里含 é / が / ě 这类带组合字符的文件):
listdir: 'Café-が-ě 笔记.md' | NFD? True | NFC? False
朴素匹配: False ← 被误判成断链
NFC 归一后: True解法:所有文件名、链接目标在进入字典前统一 unicodedata.normalize("NFC", s)。
⚠️ 但要说清楚边界:纯汉字文件名实测不会触发这个坑("测试方法"的 NFD 等于 NFC,朴素匹配通过)。只有名字里带拉丁字母的声调符号、日语浊音符号这类组合字符才会踩到。纯中文库可以不管,混排文件名必须处理。
坑 2:代码块里的 [[ ]] 会被误判成链接。 写教程的库尤其容易中招。解法是提取前先用 re.sub(r"```.*?```", "", body, flags=re.S) 剥掉围栏,行内代码 也要剥。
坑 3:--write 会无条件覆写 00-索引.md。 如果你在索引上手写了内容,第一次跑之前先备份。索引文件本身也被排除在扫描之外(否则会自己链自己,入链数乱掉)。
不适用的场景(别硬套):
os.walk 扫不到,得换成走 API 的方案;索引这活儿,人类必输给脚本——不是因为人不够勤快,而是因为"每次都记得改三处"这件事在概率上就不可能长期成立。把规则写进脚本,让它每天 8:30 自己跑一遍,你只负责看断链清单决定"这篇要不要补"。
顺带一个用法:断链清单天然就是你的写作待办列表,比任何 TODO 工具都准——因为它是你在真实写作上下文里亲手写下的"我打算引用它"。
文件 | 说明 |
|---|---|
| 完整脚本,零第三方依赖,Python 3.9+ |
| 8 篇虚构演示笔记(含 2 处故意埋的断链、1 篇孤儿),可照着跑一遍 |
| 脚本真实生成的索引输出样例 |
演示库内所有笔记内容均为虚构示例,与任何真实业务无关。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。