十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Obsidian插件实战:从Markdown笔记自动生成人物关系Canvas白板

Obsidian插件实战:从Markdown笔记自动生成人物关系Canvas白板 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 能力。它不需要你手动解析 YAMLObsidian 自己会维护每个文件的元信息缓存。3.2 第二步提取人物节点拿到一个文件后先提取主名字、别名和显示名。主名字用 frontmatter 的name字段别名叫aliases。如果 frontmatter 里没有name再退回到文件名。节点信息建议放到一个 Map 里key 是归一化后的人物名value 是完整节点对象。这样后续去重会方便很多。interface PersonNode { id: string; name: string; aliases: string[]; filePath: string; } const personMap new Mapstring, 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: Recordstring, 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 长期维护建议无论你最后选择自己写插件还是用模板加手动流程有几件事值得长期坚持人物笔记统一放在一个固定目录避免散落。别名尽量维护齐全改一次名字要同步修正所有关系。关系格式固定为列表不要中途换成表格或普通段落。每次全库扫描前留出时间检查输出日志不要无脑覆盖。关系白板这类工具真正值钱的地方不是“自动生成”这四个字而是它逼着你把人物信息整理成了机器可读的结构。只要结构稳定后续换插件、换脚本、换工具都不会伤筋动骨。我个人的建议是先建三个测试笔记跑通最小流程确认输入输出都符合预期后再推广到全库。这样踩坑成本最低也比每次都在正式库里反复试错靠谱得多。
返回列表