从「找到合适的发布插件」到「干脆自己改一个」,记录如何让 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、截断内容,并把文章发布或更新到博客上。插件内部有 PublishServiceXmlRpcClient、设置面板、i18n 等模块,整体结构对我非常有参考价值。

它的核心流程大致如下:

1
2
3
4
5
6
7
8
flowchart TB
A[Obsidian 笔记文件] --> B[解析 Frontmatter]
B --> C[图片上传]
C --> D[Wiki-Link 转换]
D --> E[内容截断]
E --> F[剥离 Frontmatter]
F --> G[XML-RPC 发布/更新]
G --> H[回写 typecho_postid]

但在实际测试中,我发现两个关键问题:一是每次更新文章都会新建一篇,无法实现真正的「更新」;
这个问题排查了很久,但都找不到为什么,怀疑是 Typecho 服务端 XML-RPC 的实现存在问题。
既然服务端肯定要动,那不如把通信协议一并换掉,从 XML-RPC 迁移到 RESTful API。

typecho-plugin-Restful:Typecho 的 RESTful 插件

Chen2226/typecho-plugin-Restful 提供了一个 Typecho 的 REST API 扩展,包含文章、页面、分类、标签、评论、文件等端点。它正好可以作为新的服务端基础,让我不用再从零写 API 路由和鉴权。

不过原版的端点和字段设计比较通用,并没有针对 Obsidian 发布场景做专门优化。例如文章发布接口只支持 titletextslugmid 等少量字段,匹配策略是「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
2
3
4
5
6
7
8
9
10
11
12
13
flowchart LR
subgraph Obsidian
O1[main.ts] --> O2[PublishService]
O2 --> O3[RestfulClient]
end
subgraph Typecho
T1[Plugin.php] --> T2[Action.php]
T2 --> T3[postArticle]
T2 --> T4[upload]
T2 --> T5[fileList]
T2 --> T6[deleteFile]
end
O3 -.->|JSON REST API| T1

下面分别记录服务端和客户端的具体改动。

服务端改造:typecho-plugin-Restful

服务端基于 Chen2226 的 Restful 插件,版本号升级到 1.3.0。最大的改动是 postArticle 接口的重构,其次是上传接口和后台配置面板。

文章发布接口的三级匹配策略

原版接口用 slug 或 title 匹配文章,在高频修改场景下容易出错。我把它改成三级回退:

  1. 如果请求里带了 cid 且大于 0,优先按 cid 精确查询并更新。
  2. cid 未命中时,按 authorId + slug 查询。
  3. 都未命中则新建文章。

这样只要 Obsidian 端在 Frontmatter 中保留了 cid,后续更新就不会因为标题或 slug 的变化而错改到其他文章。

新增和扩展的字段

发布接口从只支持少量字段,扩展为支持完整的文章元数据:

字段说明
cid文章 ID,0 或未匹配到文章时新建;正整数时优先按 cid 匹配,未命中可回退 slug 匹配
created自定义发布时间,Unix 时间戳
banner封面图 URL,写入自定义字段 thumb
description文章摘要 / SEO 描述
statuspublish / hidden / password / private / waiting
password访问密码,仅在 status=password 时生效
allowComments0 关闭评论,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删除附件

连接配置与字段映射

设置面板删除了 xmlrpcUrlusernamepassword,改为 apiUrlapiTokenauthorId。authorId 旁边加了「获取用户」按钮,可以自动填充。

Frontmatter 字段名也做了统一:原版的 typecho_postid 改为 cid,更符合 Typecho 本身的命名习惯。发布成功后,cid 和 slug 会自动回写到 Frontmatter。

标题取值与标题不一致提醒

新增 useFilenameAsTitle 选项,开启后文章标题直接取文件名,忽略 Frontmatter 中的 title 和正文第一个 # 标题。关闭时则按「Frontmatter → 正文第一个标题 → 文件名」的优先级取值。

另一个安全机制是「标题不一致提醒」:当 cid > 0 且手动推送时,插件会先获取云端文章标题与本地比对,如果不一致会弹窗确认,避免 cid 被意外修改后覆盖到别的文章。

自动保存推送

这是本次改动里最实用的功能之一。文件保存后,插件会在 3 秒防抖后自动推送。为了避免无意义的重复发布,我加了几个保护:

  • 基于 DJB2 哈希对比文件内容,内容没有实质变化时跳过。
  • 记录 cid 变化,如果发现 Frontmatter 中的 cid 被改成本地不存在的值,停止自动推送并通知用户。
  • autoPublishRunning Set 做并发保护,同一文件不会同时被推送多次。
  • 因回写 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 写入 summaryContentdesc 等独立字段;未检测到时,则把描述包在一个隐藏 div 中,并在正文开头插入 <!--more--> 标记,让普通主题也能通过 Typecho 摘要机制展示概述。

具体应该会在文章的正文开头显示概述内容,实际效果未经过测试。

修复的两个关键 Bug

在改造过程中,顺手修复了原版中比较影响体验的问题。

标题截断不生效且只支持 1-3 级标题

原版的标题取值逻辑和长标题处理在某些 Frontmatter 配置下会异常,导致截断功能失效。新版本通过 useFilenameAsTitle 和更鲁棒的标题解析解决了这个问题,并把截断范围扩展到 1-6 级标题。

同步后的文章无法被解析为 Markdown

这个问题与服务端有关。Typecho 在保存正文时,如果没有 <!--markdown--> 标记,可能会把 Markdown 当纯文本处理。我在 postArticle 中自动为正文章 prepend 该标记,确保同步后的文章能被正确渲染为 Markdown。

原插件这个问题应该是要在typecho的设置中开启对应的XML的解析 markdown 才行,不过我这边反正不走这个就无所谓了

目前的效果

完成改造后,日常使用流程大致是这样的:在 Obsidian 中写完一篇笔记,点击 Ribbon 按钮或右键选择「推送到 Typecho」,文章就会发布到博客上,并自动在 Frontmatter 中写入 cidslug。之后只要修改笔记并保存,3 秒后博客上的文章会自动更新。

1
2
3
4
5
6
7
flowchart LR
A[Obsidian 保存笔记] --> B{已有 cid?}
B -->|是| C[自动更新文章]
B -->|否| D[弹窗确认新建]
D --> E[发布并回写 cid/slug]
C --> F[完成]
E --> F

现在已支持的功能包括:

  • 手动推送与保存后自动推送。
  • 用文件名作为标题,或按优先级自动提取标题。
  • Frontmatter 控制分类、标签、状态、密码、评论开关、封面图、摘要。
  • 本地图片自动上传并替换 URL,支持内容哈希缓存避免重复上传。
  • Wiki-Link 自动转换为博客链接。
  • 标题不一致时二次确认,防止误覆盖。
  • Frontmatter 中的 description 同时作为文章摘要与 SEO 描述透传到博客;Butterfly 主题写入独立字段,其他主题通过 <!--more--> 实现摘要。

尚未完成的主要是娱乐影音阅读管理类的特殊页面同步,这部分需要更独立的界面和模板处理,计划后续单独实现。

一些还可以继续做的事

这个方案已经能覆盖我 90% 以上的发布需求,但还有一些想法没做进去:

  • 发布时自动获取当前笔记的头图链接,或从图库中随机/手动选择封面图,并回写到 Frontmatter。
  • 娱乐影音阅读管理页的文章同步,可能需要隐藏首页显示并做成独立入口。

这些暂时列在待办里,等实际使用一段时间后再决定优先级。

总结

这次二次开发本质上是一次「协议替换 + 字段补齐 + 体验优化」的组合改造。obsidian-typecho-publisher 提供了很好的发布流程骨架,typecho-plugin-Restful 提供了 REST API 基础,我把两者对接起来,并针对自己的使用习惯做了一些调整。

项目主要使用AI进行的vibe coding,确实好用,主体部分大概只花了一天就开发完成了。

两个二开后的仓库地址如下,如果有类似需求可以参考:


参考链接

  1. skyue/obsidian-typecho-publisher — 原版 Obsidian 发布插件
  2. Chen2226/typecho-plugin-Restful — 原版 Typecho RESTful 插件
  3. bluemoon23333/obsidian-typecho-publisher — 二开后的 Obsidian 插件
  4. bluemoon23333/typecho-plugin-Restful — 二开后的 Typecho 服务端插件
  5. haierkeys/obsidian-fast-note-sync — 自托管笔记同步服务