Obsidian 里写小说设定最大的痛,是人物散落在几十篇 Markdown 笔记里,想一眼看完整的关系脉络,只能手动拖白板。这篇文章要解决的,就是怎么让 Obsidian 通过插件自动解析 Markdown 笔记,把人名、别名、关系写进 Canvas 白板文件。适合小说设定党、同人创作者,以及一切需要维护“人物关系图”的知识库用户。核心思路不复杂:把人物笔记格式化成脚本能读的结构,再写一个插件扫描全库,输出.canvas文件。
很多人在这一步会直接去找现成插件,但 Obsidian 生态里能自动“理解”角色关系的插件并不多,真正可用的方案往往需要自己组装。下面按我实际搭过一遍的顺序拆开讲:先确认用途和边界,再准备环境,接着走通最小流程,最后再处理批量扫描和常见问题。
1. 先想清楚:这套方案解决的是“忠实转换”,不是“语义理解”
1.1 适合谁使用,不适合谁使用
这套方案最适合这几类人:
- 在 Obsidian 里写长篇小说的作者,人物超过 10 个,关系散落在不同笔记里。
- 喜欢用 Markdown 做剧本、跑团、历史人物设定的人。
- 对笔记可控性要求高,愿意把人物笔记统一成固定格式的人。
它解决的核心问题只有一个:把分散在 Markdown 笔记里的人物名和人物关系,自动变成一张白板,省去手动连线的时间。
但它的边界也很明显。如果你指望插件能读懂小说正文里“他握紧了她的手,眼里闪过一丝犹豫”这种描写,然后自动分析出感情线,那这个方案做不到。Markdown 本身没有语义层,脚本只能按规则读取,不能像人一样理解语气和暗示。所以那些没有统一结构的老笔记,解析前必须先清理。
1.2 自动化的边界:笔记规范决定解析率
有没有一种“不用整理笔记、导入就能识别”的工具?坦白说,目前我见过的方案都不靠谱。原因很简单:程序无法从自由文本里稳定判断哪个人名算角色,哪句话算关系,哪段描写只是修辞。
所以自动生成人物关系白板的第一步,不是写代码,而是定规矩。你需要规定:
- 哪篇笔记算“人物笔记”。
- 人物名称写在哪个字段。
- 别名写在哪里。
- 关系用什么格式表达。
规矩定得越清晰,解析准确率越高。这不是妥协,而是所有自动化工具共同的前提。
1.3 为什么选择 Canvas,而不是自己画一个关系图组件
Obsidian 原生有白板功能,也就是 Canvas。Canvas 文件本质是带特定结构的 JSON 文件,里面包含nodes和edges两个核心数组。这意味着你可以用脚本直接生成整个 Canvas 文件,Obsidian 打开后就能显示成白板。
相比自己开发一个关系图渲染视图,用 Canvas 有几个明显优势:
- 不需要引入额外前端库,不依赖外网加载资源。
- 生成的是普通
.canvas文件,可以备份、同步、版本管理。 - 自动生成后,还能手动拖拽节点、修改连线、补充说明,不会被脚本锁死。
- 如果节点位置不理想,只要不覆盖文件,手动调整过一次后,下次重新生成时可以选择保留旧布局。
所以后面所有流程都围绕“解析 Markdown -> 生成 Canvas 文件”这个思路展开。
2. 准备环境:插件开发基础与人物笔记结构
2.1 Obsidian 插件开发需要哪些前置条件
如果你打算从零写一个插件,建议先有一个专门做测试的 Vault,不要直接在正式库里反复试。开发环境大体需要这些:
- Obsidian 桌面端,版本以你当前使用的为准。
- 一个测试 Vault。
- Node.js 和 TypeScript 基础环境。
- 一份 Obsidian 插件示例工程。
写插件不是必须一开始就理解全部 API。可以先跑通一个最简单的命令:点击命令后,读取当前 Vault 里的 Markdown 文件,在控制台输出文件名。这个最小闭环跑通后,再逐步加入关系解析和 Canvas 生成。
更简单的方式是直接用 Obsidian 的第三方插件开发模板,把项目拉到本地,运行依赖安装和构建命令。构建成功后,把main.js和manifest.json放到 Vault 的.obsidian/plugins目录下,重新加载 Obsidian 就能看到插件。
这里要注意,插件开发模板的目录结构、构建命令会因为模板版本不同而略有差异。我一般会先用npm install装依赖,再运行模板自带的 watch 命令,一边改代码一边看效果。
2.2 人物笔记怎么写,才能被脚本读懂
我推荐一种非常直接的结构:人物笔记的 YAML frontmatter 里存人物名和别名,正文中以固定列表格式写关系。
示例:
--- name: 林澈 aliases: - 阿澈 - 澈 tags: - 人物 --- - 师父:沈渡 - 恋人:苏晚 - 敌对:顾夜这段 Markdown 表达了三件事:
- 人物节点名是“林澈”。
- “阿澈”和“澈”都是“林澈”的别名。
- 林澈与沈渡之间有一条“师父”关系,方向是林澈指向沈渡。
用 YAML 而不用正文标题存名字,原因很实际:YAML 字段读取稳定,别名列表清晰。脚本解析的时候,能准确知道哪个是主名字,哪些是别名。如果只用标题,人物改名后所有关系都会断掉。
2.3 为什么关系要写成列表,而不是普通段落
有人会问:如果我直接在正文写“林澈的师父是沈渡”,脚本不能解析吗?能,但很难稳定。
原因是自然语言里同一个意思有太多表达方式。比如“林澈的师父是沈渡”和“沈渡是林澈的师父”语义一样,但词序不同;如果再出现“沈渡虽然是林澈名义上的师父,但两人更像宿敌”这种句子,脚本很容易误判。
关系列表没有这个歧义。
- 师父:沈渡这一行就是一条有向关系,左边是关系名,右边是目标人物。程序只负责忠实读取,不负责理解。等脚本成熟后,你甚至可以把这个格式写进日记、角色访谈、阵营梳理笔记里,统一解析。
3. 核心流程拆解:从 Markdown 笔记到人物关系白板
3.1 第一步:确定扫描范围
解析前先想清楚要扫哪些文件。如果全库只写一部小说,可以直接扫描所有带“人物”标签的笔记。如果库里同时有工作日志、学习笔记、多部小说草稿,就一定要限定文件夹或标签范围。
我建议在插件设置里添加两个参数:
- 扫描方式:按标签或按文件夹。
- 具体范围:例如
tags: ["人物"]或folder: "小说/设定/人物"。
这样做的原因很简单:减少误判,提高效率。很多报错和乱七八糟的关系,都是把无关笔记扫进来了。
在代码里,可以先获取所有 Markdown 文件:
const files = this.app.vault.getMarkdownFiles();然后按标签过滤:
for (const file of files) { const cache = this.app.metadataCache.getFileCache(file); const tags = cache?.frontmatter?.tags ?? []; if (!tags.includes('人物')) continue; // 只处理选中的文件 }这里用到的是 Obsidian 的 metadataCache 能力。它不需要你手动解析 YAML,Obsidian 自己会维护每个文件的元信息缓存。
3.2 第二步:提取人物节点
拿到一个文件后,先提取主名字、别名和显示名。主名字用 frontmatter 的name字段,别名叫aliases。如果 frontmatter 里没有name,再退回到文件名。
节点信息建议放到一个 Map 里,key 是归一化后的人物名,value 是完整节点对象。这样后续去重会方便很多。
interface PersonNode { id: string; name: string; aliases: string[]; filePath: string; } const personMap = new Map<string, PersonNode>();关键点是别名的归一化。比如“阿澈”和“澈”都指向“林澈”。脚本在后续解析关系时,遇到目标名字是“阿澈”,要能自动映射到“林澈”。如果映射不好,就会出现人物 A 的笔记里写了关系,但目标人物 B 始终连不上。
3.3 第三步:提取关系边
关系边的来源是人物笔记正文中的所有列表行。解析时最好逐行读取,而不是直接对全文做正则匹配,因为需要跳过代码块、引用块、任务列表等无关内容。
一个最小的解析思路是:
for (const line of content.split('\n')) { const trimmed = line.trim(); // 跳过任务、引用、代码块标记 if (trimmed.startsWith('- [ ]') || trimmed.startsWith('- [x]')) continue; if (trimmed.startsWith('>')) continue; const match = trimmed.match(/^[-*]\s*(.+?)\s*[::]\s*(.+?)\s*$/); if (!match) continue; const relationName = match[1].trim(); const targetName = match[2].trim(); // 将 targetName 映射到真实人物节点 }为什么用这个正则?因为关系行被限制成“短横线开头 + 关系名 + 冒号 + 目标人名”。正则只是负责把这个结构拆开,真正的质量保障来自你笔记格式的一致性。
提取出关系之后,要把当前人物作为起点,目标人物作为终点,生成一条边。边的 label 就是关系名。
3.4 第四步:生成 Canvas JSON 文件
有了节点和边,就可以组装 Canvas 文件。Obsidian 的 Canvas 文件是一个 JSON 对象,核心字段通常包括nodes和edges。
一个最小生成结构如下:
{ "nodes": [ { "id": "node-linche", "type": "text", "text": "林澈", "x": 0, "y": 0, "width": 120, "height": 60 } ], "edges": [ { "id": "edge-1", "fromNode": "node-linche", "toNode": "node-shendu", "fromSide": "right", "toSide": "left", "label": "师父" } ] }生成后写入.canvas文件,Obsidian 识别后会自动渲染成白板。
节点坐标怎么排?如果人物不多,最简单的方式是按解析顺序横向排列,一行放 5 到 6 个。节点多了之后,再用比较简单的网格布局。更复杂的自动布局算法会涉及图计算,不是这篇文章的重点,而且 Obsidian Canvas 本身也支持生成后手动拖动,所以第一步别在布局上花太多时间。
这里要特别提醒:Canvas 的具体 JSON 字段在不同 Obsidian 版本里可能略有差异。第一次生成后,一定要手动打开画布确认一下格式是否正常。不要假设所有字段都永远不变。
4. 关键参数与规则:解析准确率由这些细节决定
4.1 全角冒号和半角冒号必须统一
中文笔记里,冒号经常混用:有时候是“师父:沈渡”,有时候是“师父:沈渡”。如果脚本只支持半角冒号,遇到全角就会漏解析。
最常见的做法是在解析前做一次统一转换,把全角冒号替换成半角:
const normalizedLine = line.replace(/\uFF1A/g, ':');但转换只应该发生在列表行的冒号位置,不要对全文做无差别替换,否则可能把正文里的正常文字也改变。
另一个容易踩的坑是空格。有人写- 师父: 沈渡,冒号后面多了一个空格;有人写- 师父 :沈渡,冒号前面有空格。正则里要支持\s*,或者在提取后对关系名和目标名做trim()。
4.2 双向关系补全需要反向映射表
人物关系不一定总是单方面表达。A 的笔记里写了“师父:B”,但从 B 的视角看,关系应该是“徒弟:A”。如果只生成单向边,白板看起来会缺少一半信息。
解决办法是做一张反向映射表:
const reverseRelationMap: Record<string, string> = { '师父': '徒弟', '徒弟': '师父', '恋人': '恋人', '敌对': '敌对', '朋友': '朋友', };生成边时,如果开启了“双向补全”,就在反向人物上也生成一条反向边。
但反向映射表一定要谨慎。像“暗恋”“仇人”这类关系不是简单反转;而且有些关系反向之后语义会变。我建议默认只对明确对称或已知可逆的关系做补全,其他关系保持单向,后续手动调整。
4.3 多份笔记关系冲突时,以谁为准
人物很多之后,会出现同一条关系在不同笔记里被重复写到。比如林澈笔记里写了“师父:沈渡”,沈渡笔记里也写了“徒弟:林澈”。这两条边本质是同一个故事事实。
处理冲突的原则很简单:
- 同一方向、同一关系名、同一目标,只保留一条边。
- 如果两边都在写,保留来源更明确的一条。
- 不要去重掉不同方向或不同关系名的边。
重复去重可以用以“起点 + 终点 + label”为 key 的 Set 实现。不要轻易删除看起来相似的边,因为“亦敌亦友”和“敌对”是两种完全不同的关系。
4.4 关系强度怎么做
很多小说设定党希望给关系加权重,比如“重要关系”显示得醒目一点。这个思路合理,但 Obsidian Canvas 原生对边的粗细控制有限,直接表达权重并不方便。
我的建议是:在关系行里追加一个可选标记,例如:
- 师父:沈渡 [重要] - 恋人:苏晚 [核心]脚本解析时把这些标记提取出来,作为附加属性保存。生成白板时,可以用文字后缀方式展示,也可以在节点卡片里补充说明。不要试图用 Canvas 不支持的视觉属性硬扛。
5. 批量扫描全库时的性能与稳定性
5.1 先跑小样本,再扫全库
第一次编写完成后,不要立刻在整个小说 Vault 里运行。我会先在测试库建三个笔记,包含两三个人物、三四条关系,跑通完整流程。确认能生成 Canvas 文件、能打开、能显示连线之后,再拿到正式库去扫描。
很多人一上来就想着全自动批量处理,结果生成了一堆错误文件,反而更难排查。插件脚本和人工操作一样,先小步验证,再扩大范围。
5.2 关注哪些性能指标
如果笔记数量很少,性能不是问题。但小说设定库动辄几百篇笔记,批量扫描就要关注几个点:
- 扫描耗时:读取 500 个文件大概需要多久。
- 内存占用:是否出现卡顿或进程内存上涨。
- 生成文件大小:人物超过 200 人时,单个 Canvas 文件会变得很大。
- 打开速度:白板节点太多,Obsidian 渲染会明显变慢。
我的判断标准是:如果生成的人物节点超过 100 个,就建议按卷、按阵营或按故事线拆分生成,不要把所有人物塞进同一张白板。分拆后,每个 Canvas 文件更轻,也更容易手动维护。
5.3 日志和输出目录要提前设计
插件跑完不能只给一个“生成完成”的按钮反馈。脚本最好在控制台或日志文件里输出这些信息:
- 扫描了多少个文件。
- 成功解析出多少个人物。
- 提取出多少条关系。
- 跳过了哪些文件,为什么跳过。
- 有没有目标人物名无法映射到任何节点。
这些日志在问题排查时非常关键。特别是“人物缺失”的问题,如果日志里能列出未匹配目标名,你能立刻找出是别名没写还是扫描范围不对。
输出目录同样重要。不要直接把.canvas文件覆盖到用户已经手动调整过的画布上。我会建议输出到一个固定目录,例如关系白板/人物关系-自动生成.canvas,确认无误后再手动替换正式文件。
5.4 生成前先备份旧文件
Obsidian Canvas 文件是人工可编辑的。如果你上次已经手动拖好了节点位置和连线布局,再次自动生成时直接覆盖,会丢掉所有手动调整结果。
更稳妥的做法是:自动生成前,先把旧文件复制一份,加上时间戳后缀。这样即使新生成结果不满意,也能随时回滚。
6. 常见排查链路:画布空白、关系错乱、不刷新
6.1 Canvas 文件生成了,但白板是空的
先看生成出来的.canvas文件内容,不要急着怀疑 Obsidian 渲染。用文本编辑器打开文件,确认nodes和edges数组是否是空数组。
如果都是空数组,问题通常出在前面的解析阶段:
- 扫描范围没有覆盖到人物笔记。
- frontmatter 里没有
tags或name字段。 - 关系行没有被正则匹配到。
- 文件编码或换行符异常。
按这个顺序查,比反复点按钮更有效。
6.2 人物缺失,最常见的不是脚本问题,而是别名问题
如果你发现笔记 A 里写的关系目标“阿澈”没有出现在白板里,第一反应应该是:脚本有没有把“阿澈”映射到“林澈”这个节点。
常见原因有:
- 人物笔记的
aliases里写的是阿澈, 澈,但脚本按换行拆分,导致解析出的是一个包含逗号的字符串。 aliases字段本身没有读取到,因为 frontmatter 结构写错了。- 目标名字后面带了多余标点或空格,比如“沈渡。”。
排查时优先看日志里的“未匹配目标列表”,把输出打印出来,一眼就能发现是名字没有归一化,还是别名没写全。
6.3 关系线多连、漏连,往往是把无关内容扫进来了
有些 Markdown 笔记里会同时出现人物关系和待办事项。比如:
- [ ] 给沈渡写信 - 师父:沈渡如果脚本只按“短横线开头”来匹配,第一行会被当成关系解析,产生一条名为“给沈渡写信”的连接。所以解析时必须过滤任务列表。更严格一点,还要忽略代码块和引用块里的内容,否则代码示例里的冒号行也可能被误判。
这种做法不是蠢,而是很多脚本出现“乱连”的根源。
6.4 点开画布还是旧数据,不刷新
如果.canvas文件已经更新了内容,但 Obsidian 白板里还是旧数据,通常是因为 Obsidian 对 Canvas 有缓存。可以先关闭这个画布,再重新打开;如果还不行,用命令面板里的 “Reload app without saving” 重新加载 App。
这种问题不是脚本逻辑错误,不用反复改代码。生成文件后自动通知 Obsidian 刷新,是一个更好用的体验,但具体实现依赖于当前 API 能力,如果你只做离线脚本,手动重开也能接受。
6.5 插件按钮点了没反应
按钮没反应时,第一件事是打开 Obsidian 的开发者控制台,看有没有报错。通常原因有三类:
- 插件没正确启用。
- 命令 ID 和菜单里绑定不对。
- 脚本执行中出现了异常,比如读取到不存在的文件路径。
建议在onload里注册命令后,先做一个最简测试:点击命令,在控制台输出一句hello。这个能证明插件壳子是通的,后面再排查解析逻辑。
7. 不想写插件?也有替代方案
7.1 人物不多,用 Dataview + 手动 Canvas
如果你不想维护插件代码,但又想快速生成人物清单,可以先用 Dataview 查询带“人物”标签的笔记,生成一张表格。表格里列出姓名、别名、所属阵营、出现篇目等字段。
然后打开 Obsidian Canvas,手动拖入相关笔记,再用连线补充关系。这种方案适合人物不超过二三十个、关系不太复杂的场景。它不是自动生成,但能减少一部分手动整理成本。
7.2 用 Templater 或 QuickAdd 规范新笔记
自动化的前提是笔记格式统一。即便不用自定义插件,也可以用 Templater 或 QuickAdd 做一个人物笔记模板。每次新建人物笔记时,自动生成 frontmatter 和关系列表的骨架。这样即使以后换用更自动化的脚本,旧数据也不会乱到无法处理。
7.3 社区插件要考虑兼容性
Obsidian 社区里确实有一些画布增强、图谱自动布局、关系展示类插件。真正落地前,至少确认三件事:
- 插件是否还在维护。
- 是否适配你当前 Obsidian 版本。
- 是否允许自定义笔记结构和关系字段。
不要因为某篇文章说“很好用”就直接往正式库里装。先在小库测试,再决定是否长期使用。
7.4 长期维护建议
无论你最后选择自己写插件,还是用模板加手动流程,有几件事值得长期坚持:
- 人物笔记统一放在一个固定目录,避免散落。
- 别名尽量维护齐全,改一次名字,要同步修正所有关系。
- 关系格式固定为列表,不要中途换成表格或普通段落。
- 每次全库扫描前留出时间检查输出日志,不要无脑覆盖。
关系白板这类工具,真正值钱的地方不是“自动生成”这四个字,而是它逼着你把人物信息整理成了机器可读的结构。只要结构稳定,后续换插件、换脚本、换工具都不会伤筋动骨。
我个人的建议是,先建三个测试笔记跑通最小流程,确认输入输出都符合预期后,再推广到全库。这样踩坑成本最低,也比每次都在正式库里反复试错靠谱得多。