从「找到合适的发布插件」到「干脆自己改一个」,记录如何让 Obsidian 里的笔记在保存时自动同步到 Typecho 博客,并在这个过程中把通信协议、字段映射、自动推送和图片上传逐一重构。
起点:为什么想要一套笔记到博客的同步方案
我的笔记和博客长期分属两个系统。Obsidian 负责日常记录、整理和输出,Typecho 负责把成稿发布出去。理想状态下,写完一篇笔记后,应该只需点一下按钮或者保存文件,文章就出现在博客上。如果后续修改,博客上的版本也应该跟着更新,而不需要再登录后台复制粘贴。
这个需求听起来并不复杂,但要把体验做得顺畅,涉及几个问题:发布动作要简单、更新要自动、图片等附件要正确上传、Frontmatter 元数据要能控制文章的标题、分类、标签、状态等字段。再加上一些个人偏好,比如用文件名做标题、用中文命令触发、保存后自动推送,现成的方案很难一次性满足。
于是我开始在 GitHub 上找相关项目,最终锁定了两个基础仓库作为二次开发的起点。
对现有的Obsidian同步到typecho插件的对比可以看这篇文章:
三款 Obsidian-Typecho 同步插件横向对比分析
两个原项目
obsidian-typecho-publisher:已有的 Obsidian 发布插件
skyue/obsidian-typecho-publisher 是一个相对成熟的 Obsidian 插件,功能架构很清晰:通过 XML-RPC 协议与 Typecho 通信,支持读取 Frontmatter、上传图片、转换 Wiki-Link、截断内容,并把文章发布或更新到博客上。插件内部有 PublishService、XmlRpcClient、设置面板、i18n 等模块,整体结构对我非常有参考价值。
它的核心流程大致如下:
1 | flowchart TB |
但在实际测试中,我发现两个关键问题:一是每次更新文章都会新建一篇,无法实现真正的「更新」;
这个问题排查了很久,但都找不到为什么,怀疑是 Typecho 服务端 XML-RPC 的实现存在问题。
既然服务端肯定要动,那不如把通信协议一并换掉,从 XML-RPC 迁移到 RESTful API。
typecho-plugin-Restful:Typecho 的 RESTful 插件
Chen2226/typecho-plugin-Restful 提供了一个 Typecho 的 REST API 扩展,包含文章、页面、分类、标签、评论、文件等端点。它正好可以作为新的服务端基础,让我不用再从零写 API 路由和鉴权。
不过原版的端点和字段设计比较通用,并没有针对 Obsidian 发布场景做专门优化。例如文章发布接口只支持 title、text、slug、mid 等少量字段,匹配策略是「slug 优先、title 回退」,没有 cid 精确匹配,也没有状态、密码、评论开关、封面图、摘要等字段。因此服务端也需要做比较大的改动,才能配合新的 Obsidian 插件工作。
需求梳理与方案取舍
在开始写代码之前,我把需求分成三层:必须有的基础功能、希望优化的体验、未来可能做的扩展。
基础功能
- 对 Obsidian 中的单篇笔记执行发布,自动推送到 Typecho。
- 保存文件后自动检测变化并同步更新。
- 本地图片和附件上传到博客,博客正文中的相对路径替换为远程 URL。
体验优化
- 用文件名作为文章标题,而不是从 Frontmatter 的别名字段读取。
- 评论开关用 Frontmatter 中的布尔值控制。
- 支持封面图、文章摘要、slug 自动回填、公开状态修改、访问密码、分类与标签自动创建等。
- 标题不一致时二次提醒,避免意外覆盖其他文章。
扩展想法
- 通过 AI 快速生成标签元数据。(后面打算通过编写一个skill来实现)
- 从头图或图库中自动选择封面图,不用每次手动去复制图片链接。
- 娱乐影音阅读管理类的特殊页面同步。
关于附件管理,我一开始想联动自托管的 obsidian-fast-note-sync 服务,让图片由同步服务管理,博客只引用链接。但这两个服务本身没有为这种场景设计,不修改源码很难打通。权衡之后,还是选择最稳妥的方案:图片随文章一起上传到 Typecho 作为附件。
二次开发的整体思路
最终方案是「保留 obsidian-typecho-publisher 的交互流程和业务逻辑,把底层通信协议从 XML-RPC 换成 RESTful,再补齐两端缺失的字段和能力」。也就是说,Obsidian 端的用户操作、设置面板、发布流程基本沿用原有设计,但 XmlRpcClient 被完全替换为新的 RestfulClient;Typecho 端则在 Restful 插件基础上扩展发布接口、上传接口和后台配置。
1 | flowchart LR |
下面分别记录服务端和客户端的具体改动。
服务端改造:typecho-plugin-Restful
服务端基于 Chen2226 的 Restful 插件,版本号升级到 1.3.0。最大的改动是 postArticle 接口的重构,其次是上传接口和后台配置面板。
文章发布接口的三级匹配策略
原版接口用 slug 或 title 匹配文章,在高频修改场景下容易出错。我把它改成三级回退:
- 如果请求里带了
cid且大于 0,优先按 cid 精确查询并更新。 - cid 未命中时,按
authorId + slug查询。 - 都未命中则新建文章。
这样只要 Obsidian 端在 Frontmatter 中保留了 cid,后续更新就不会因为标题或 slug 的变化而错改到其他文章。
新增和扩展的字段
发布接口从只支持少量字段,扩展为支持完整的文章元数据:
| 字段 | 说明 |
|---|---|
cid | 文章 ID,0 或未匹配到文章时新建;正整数时优先按 cid 匹配,未命中可回退 slug 匹配 |
created | 自定义发布时间,Unix 时间戳 |
banner | 封面图 URL,写入自定义字段 thumb |
description | 文章摘要 / SEO 描述 |
status | publish / hidden / password / private / waiting |
password | 访问密码,仅在 status=password 时生效 |
allowComments | 0 关闭评论,1 允许评论 |
mid | 分类 / 标签 ID |
返回值也做了语义化,例如 {"cid": 45, "type": "add", "slug": "my-post"},让 Obsidian 端能明确知道是新增还是更新,并回写 slug。
上传接口的多格式兼容
为了让 Obsidian 插件能以不同方式上传图片,上传接口通过新增的 Util.php 支持三种格式:
- 标准的
multipart/form-data文件上传。 - JSON 中的
Uint8Array字节数组,通过pack('C*', ...$bytes)还原为文件。 - Base64 字符串,包括
data:*;base64,xxx和纯 base64 两种形式。
新增文件管理接口
除了上传,还增加了 /api/deleteFile 删除附件、/api/fileList 分页获取附件列表,以及 /api/upgrade 插件自更新接口,方便在后台一键检查并拉取最新代码。
CORS 与鉴权调整
原版 CORS 要求 Origin 在白名单内,否则不返回 Access-Control-Allow-Origin。但 Obsidian 插件作为非浏览器客户端,通常不会携带 Origin 头。我改成:无 Origin 头时自动允许 *,有 Origin 头时再按白名单校验。这样既保留浏览器的安全限制,又让 Obsidian 客户端能直接调用。
鉴权方面,高敏接口默认通过 apiToken 校验,并允许关闭登录校验,让纯 Token 认证场景更易用。
后台配置面板的小调整
服务端插件的配置面板原本已经比较完整,包含 API 状态开关、CORS 域名、字段隐私过滤、设置项白名单、CSRF 盐值、API Token、高敏接口登录校验以及一键升级按钮。每个 API 开关的说明来自代码注释的中文翻译,并会显示对应的端点路径,例如 GET /api/posts。
我主要在这个基础上增加了一个 Butterfly 主题检测提示:检测到 Butterfly 时显示绿色提示,未检测到时显示黄色提示并说明摘要会通过 <!--more--> 方式实现。这样用户在配置时就能直观知道 description 字段会如何存储。
客户端改造:obsidian-typecho-publisher
Obsidian 端基于 skyue 的插件,版本号升级到 2.0.0。核心变化是通信协议替换、字段映射调整和自动推送机制的增加。
通信协议:XML-RPC → RESTful
我删除了 xmlrpc-client.ts,新增 restful-client.ts。新的客户端通过 JSON 与 Typecho 通信,主要调用以下端点:
| 端点 | 用途 |
|---|---|
POST /api/postArticle | 发表或更新文章 |
POST /api/upload | 上传图片,使用 base64 JSON |
GET /api/categories | 获取分类列表 |
GET /api/tags | 获取标签列表 |
POST /api/addMetas | 自动创建缺失的分类或标签 |
GET /api/userList | 获取用户列表,自动填充 authorId |
GET /api/post | 获取文章详情,用于标题比对 |
POST /api/deleteFile | 删除附件 |
连接配置与字段映射
设置面板删除了 xmlrpcUrl、username、password,改为 apiUrl、apiToken、authorId。authorId 旁边加了「获取用户」按钮,可以自动填充。
Frontmatter 字段名也做了统一:原版的 typecho_postid 改为 cid,更符合 Typecho 本身的命名习惯。发布成功后,cid 和 slug 会自动回写到 Frontmatter。
标题取值与标题不一致提醒
新增 useFilenameAsTitle 选项,开启后文章标题直接取文件名,忽略 Frontmatter 中的 title 和正文第一个 # 标题。关闭时则按「Frontmatter → 正文第一个标题 → 文件名」的优先级取值。
另一个安全机制是「标题不一致提醒」:当 cid > 0 且手动推送时,插件会先获取云端文章标题与本地比对,如果不一致会弹窗确认,避免 cid 被意外修改后覆盖到别的文章。
自动保存推送
这是本次改动里最实用的功能之一。文件保存后,插件会在 3 秒防抖后自动推送。为了避免无意义的重复发布,我加了几个保护:
- 基于 DJB2 哈希对比文件内容,内容没有实质变化时跳过。
- 记录 cid 变化,如果发现 Frontmatter 中的 cid 被改成本地不存在的值,停止自动推送并通知用户。
- 用
autoPublishRunningSet 做并发保护,同一文件不会同时被推送多次。 - 因回写 cid/slug 触发的保存会被识别并跳过。
图片上传重构
图片上传从 XML-RPC 的 newMediaObject 改为 POST /api/upload。缓存策略从「基于 Vault 相对路径」改为「基于文件内容哈希」,相同内容的图片不会重复上传。上传失败时不会阻断整篇文章的发布,而是弹出单张图片的失败通知并继续处理其余图片。
同时也保留了 Cloudflare R2 图床的支持,通过 AWS Signature V4 计算签名,适合想把图片放到独立存储的场景。
内容截断重写
原版内容截断只能匹配 1-3 级标题,且逻辑比较粗糙。我重写了 markdown-utils.ts 中的截断函数,支持 1-6 级所有标题,并只移除匹配标题及其子内容,后续内容保留。这样可以在发布时去掉某些仅在本地使用的章节,而不影响正文其余部分。
Wiki-Link 转换与 i18n
Wiki-Link 转换现在会排除图片链接 !...,只对普通双链 [[...]] 做替换。
其实wikilink转换最好是在Obsidian笔记库里面本身完成统一,而不是上传的时候让插件来转换,目前我的笔记库中的所有链接统一使用markdown格式。
设置面板、命令面板、右键菜单等处的触发文字统一改为中文「推送到 Typecho」。i18n 字符串表也做了相应更新,移除 XML-RPC 相关文案,增加 RESTful、自动推送、封面图、摘要、状态、密码、评论开关等新文案。
摘要与 SEO 描述字段的透传
如果 Frontmatter 中填写了 description 字段,插件会把它同时作为文章摘要和 SEO 描述发送给服务端;如果没有填写,则传空值,不会根据正文自动生成摘要。这个功能只是把用户已经写好的描述同步到博客上,而不是替用户生成。
服务端会根据是否安装 Butterfly 主题 做不同处理:检测到 Butterfly 时,把 description 写入 summaryContent 和 desc 等独立字段;未检测到时,则把描述包在一个隐藏 div 中,并在正文开头插入 <!--more--> 标记,让普通主题也能通过 Typecho 摘要机制展示概述。
具体应该会在文章的正文开头显示概述内容,实际效果未经过测试。
修复的两个关键 Bug
在改造过程中,顺手修复了原版中比较影响体验的问题。
标题截断不生效且只支持 1-3 级标题
原版的标题取值逻辑和长标题处理在某些 Frontmatter 配置下会异常,导致截断功能失效。新版本通过 useFilenameAsTitle 和更鲁棒的标题解析解决了这个问题,并把截断范围扩展到 1-6 级标题。
同步后的文章无法被解析为 Markdown
这个问题与服务端有关。Typecho 在保存正文时,如果没有 <!--markdown--> 标记,可能会把 Markdown 当纯文本处理。我在 postArticle 中自动为正文章 prepend 该标记,确保同步后的文章能被正确渲染为 Markdown。
原插件这个问题应该是要在typecho的设置中开启对应的XML的解析 markdown 才行,不过我这边反正不走这个就无所谓了
目前的效果
完成改造后,日常使用流程大致是这样的:在 Obsidian 中写完一篇笔记,点击 Ribbon 按钮或右键选择「推送到 Typecho」,文章就会发布到博客上,并自动在 Frontmatter 中写入 cid 和 slug。之后只要修改笔记并保存,3 秒后博客上的文章会自动更新。
1 | flowchart LR |
现在已支持的功能包括:
- 手动推送与保存后自动推送。
- 用文件名作为标题,或按优先级自动提取标题。
- Frontmatter 控制分类、标签、状态、密码、评论开关、封面图、摘要。
- 本地图片自动上传并替换 URL,支持内容哈希缓存避免重复上传。
- Wiki-Link 自动转换为博客链接。
- 标题不一致时二次确认,防止误覆盖。
- Frontmatter 中的
description同时作为文章摘要与 SEO 描述透传到博客;Butterfly 主题写入独立字段,其他主题通过<!--more-->实现摘要。
尚未完成的主要是娱乐影音阅读管理类的特殊页面同步,这部分需要更独立的界面和模板处理,计划后续单独实现。
一些还可以继续做的事
这个方案已经能覆盖我 90% 以上的发布需求,但还有一些想法没做进去:
- 发布时自动获取当前笔记的头图链接,或从图库中随机/手动选择封面图,并回写到 Frontmatter。
- 娱乐影音阅读管理页的文章同步,可能需要隐藏首页显示并做成独立入口。
这些暂时列在待办里,等实际使用一段时间后再决定优先级。
总结
这次二次开发本质上是一次「协议替换 + 字段补齐 + 体验优化」的组合改造。obsidian-typecho-publisher 提供了很好的发布流程骨架,typecho-plugin-Restful 提供了 REST API 基础,我把两者对接起来,并针对自己的使用习惯做了一些调整。
项目主要使用AI进行的vibe coding,确实好用,主体部分大概只花了一天就开发完成了。
两个二开后的仓库地址如下,如果有类似需求可以参考:
- Obsidian 插件:bluemoon23333/obsidian-typecho-publisher
- Typecho 服务端插件:bluemoon23333/typecho-plugin-Restful
参考链接
- skyue/obsidian-typecho-publisher — 原版 Obsidian 发布插件
- Chen2226/typecho-plugin-Restful — 原版 Typecho RESTful 插件
- bluemoon23333/obsidian-typecho-publisher — 二开后的 Obsidian 插件
- bluemoon23333/typecho-plugin-Restful — 二开后的 Typecho 服务端插件
- haierkeys/obsidian-fast-note-sync — 自托管笔记同步服务