https://github.com/swlt-silica/Bilibili-Obsidian-Clipper
我一直在用 B 站看视频学东西,也一直在用 Obsidian 管理笔记。有个插件 Bilibili-Obsidian-Clipper 特别对我胃口——在视频页一键抓字幕、存成 Markdown 笔记,省得自己边看边记。
但用久了发现一个硬伤:它只能把笔记存进"本地 Obsidian"。我后来把 Obsidian 库放到了自建的 Fast Note Sync Service(FNS) 服务器上做多设备同步,这时候就尴尬了——插件写进本地库,还得靠同步插件再推上去,而且要本地 Obsidian 开着才行。
能不能让插件直接写到服务器?我开始动手,最后折腾出了一个功能完整的增强版插件,还顺手合并了另一个作者没被合入的批量抓取功能。
这篇文章记录完整过程:协议分析、两套方案对比、三个"坑",以及最终如何发布。希望能给做浏览器插件、折腾自建服务的朋友一些参考。
一、需求:让「保存」直达服务器
我的目标很简单:
在 B 站视频页点一下「保存」,字幕 Markdown 直接写进服务器上 FNS 同步项目的对应目录,所有设备实时同步。
不要本地 Obsidian 参与,不要二次同步,保存即落库。
二、先搞清楚两套协议为什么不兼容
这是整个改造的关键。我翻了插件源码(background.js),它保存笔记时用的是 Obsidian Local REST API 协议:
PUT http://127.0.0.1:27123/vault/Clippings/Bilibili/xxx.md
Authorization: Bearer <key>
Content-Type: text/markdown
Body: 字幕 markdown
三个端点:PUT /vault/<路径>(写)、GET /vault/<路径>(查存在)、GET /(测试连接)。
而我服务器上的 FNS 是另一套 REST API:
POST http://服务器:9000/api/note
Authorization: Bearer <token>
Content-Type: application/json
Body: { "path": "xxx.md", "vault": "项目文章", "content": "..." }
协议、路径、请求体、认证方式全都不一样。插件发出去的 PUT /vault/xxx.md,FNS 根本没有这个路由,直接 404。在插件设置里填服务器地址是行不通的——不是地址问题,是协议问题。
三、方案对比:适配器 vs 改插件
想通协议不兼容后,有两条路:
方案 A:本地适配器(先做的)
写一个 ~100 行本地代理服务,监听 127.0.0.1:27124,把插件发来的 PUT /vault/* 翻译成 FNS 的 POST /api/note。插件设置里填 http://127.0.0.1:27124,API Key 随便填个占位符,适配器内部存好真实的 FNS token 和 vault 名。
✅ 不用改插件,零侵入
❌ 多跑一个进程,机器重启要手动拉起
方案 B:fork 插件,加一个「保存到 FNS」按钮(最终选择)
既然插件本身是开源 MIT 协议,为什么不直接改它?保留原「发送到 Obsidian」,新增「保存到 FNS」,设置页加 FNS 配置区块。一次到位,用户零额外成本。
我最终走了 B——直接改造插件,效果最好。
四、改插件:新增三个消息处理器
插件的架构是 MV3:content.js(页面内)、popup.js(弹窗)、background.js(service worker,负责发网络请求,不受 CORS 限制)。
我沿用了它「content → background → 第三方服务」的成熟链路,在 background 加了三个消息处理器:
| 消息 | 作用 | 对应 FNS 请求 |
|---|---|---|
test-fns-connection |
设置页测试连接 | 探测一个不存在的路径,返回 430 = token 有效 |
fns-note-exists |
保存前查重 | GET /api/note?path=...&vault=... |
write-fns-note |
保存 | POST /api/note,body 带 {path, vault, content} |
UI 侧:视频面板和 popup 各加一个绿色「保存到 FNS」按钮,设置页加 FNS 配置区块 + 独立「测试 FNS 连接」按钮。
五、三个坑(每个都值得说)
坑 1:FNS 的 token 绑定了客户端类型
装上后测试连接一直报 315 Auth token Scope restricted。翻了 FNS 服务端源码才发现:每个 token 的 scope 绑定了客户端类型,请求必须带 x-client 头,值要和 token 绑定的一致。
从 FNS 后台「Copy API Config」复制的 token 绑的是 ObsidianPlugin,所以每个请求都要加:
x-client: ObsidianPlugin
这个细节官方 REST 文档里没有明说,不翻源码根本想不到。
坑 2:FNS 业务错误一律返回 HTTP 200
这是最容易误判的坑。正常 REST 服务"资源不存在"会返回 404,但 FNS 业务错误全部返回 HTTP 200,错误码塞在 body 里:
| 业务码 | 含义 |
|---|---|
430 |
笔记不存在 |
315 |
scope 受限 |
314 |
客户端受限 |
414 |
vault 不存在 |
507 |
未登录 |
所以判断"笔记是否存在"不能看 HTTP 状态码,要解析 body:code === 430 才表示不存在。一开始我就是按状态码写的,结果把 315 误判成了"存在",差点把假逻辑留进代码。
坑 3:版本号不一致导致页面无限刷新
这是开发中最迷惑的一次。做完 FNS 功能后测试,一点「AI」按钮,页面就开始无限刷新。
排查发现:插件有套"版本探测"机制——background.js 用 chrome.runtime.getManifest().version 作为期望版本,探测页面里的 content script 版本(content.js 里硬编码的 BOC_VERSION)。我打包时把 manifest 版本从 1.1.3 改成了 1.2.0,但 content.js 里的 BOC_VERSION 还是 1.1.3。
于是 background 每次打开侧边栏都发现"脚本过期"→ 强制 tabs.reload() 刷新页面 → 刷新后还是 1.1.3 → 再刷新……无限循环。
修复:content.js 改成动态读取 chrome.runtime.getManifest().version,跟 manifest 永远一致。这也给以后升级提了个醒:版本号要全局统一,别在多个文件里各写一份硬编码。
六、顺手合并了别人的批量抓取功能
改插件期间,我发现原仓库有个 PR #22(作者 zwang-zwang)做了「合集 / 多 P 批量抓取」——一次性抓整个合集的分集字幕,合并成一篇笔记。功能很好,但挂了一个多月没被合并(状态 open,且已经和主线冲突 dirty)。
既然我在 fork 改造,就把它一起合了进来,还额外加了**「批量保存到 FNS」**,让批量抓取的合集也能直接落服务器。
合并时踩了个小坑:PR 基于旧版 main,有些辅助函数在最新主线里改名了,直接合并会缺函数。需要逐项核对依赖的辅助函数是否还在、是否有重复定义,我用一个 24 项检查的脚本验证了消息配对、DOM id 配对、函数完整性——合并别人 PR 时,静态链路检查比肉眼靠谱得多。
七、发布:fork + PR + Release 三步走
代码改完,怎么"上线"?我做了三层:
1. 公开 fork 仓库(核心)
把增强版推到自己的 GitHub 仓库 swlt-silica/Bilibili-Obsidian-Clipper。fork 自带原仓库所有文件(含二进制图标),只需覆盖我改过的几个文本文件。这也是功能传播的主通道——原作者合不合 PR 都不影响别人装我的版本。
2. 提 PR 回馈原作者
PR #24 提给原作者,正文写清新增功能,并明确标注批量逻辑来自 PR #22(作者 zwang-zwang),不能白拿别人的劳动成果。
3. 发布 Release
打个 tag、建 Release、传 zip 安装包,别人下载解压就能装进 Edge。Release 描述里写了完整安装步骤,还特意提醒:Edge 拖拽 zip 到扩展页会解压坏目录结构(icons/icon16.png 会变成乱码文件名导致图标加载失败),必须用「加载解压缩的扩展」选文件夹。
八、收获与总结
- 协议不同不能硬接。插件和服务器各说各话时,要么加适配层,要么改一端——改插件(开源的前提下)往往更干净。
- 别只看 HTTP 状态码。有些服务把业务错误码塞在 200 的 body 里,读服务端源码或文档确认,别凭经验猜。
- 全局一致的版本号。版本探测机制的坑,本质是"一处改了、一处没改",用单一数据源(
getManifest().version)根治。 - fork 不是抢功。MIT 协议允许再发布,但保留原版权、注明来源(包括注明白己用了哪个 PR)、明确标注 AI 辅助开发,是基本的开源礼仪。
- 浏览器插件的坑和普通前端不一样:CORS(靠 service worker 绕)、扩展页刷新循环、zip 安装方式,都得实际踩过才知道。
感谢他们为本文做出的贡献
https://github.com/swlt-silica/Bilibili-Obsidian-Clipper/releases/tag/v1.2.0
https://github.com/haixiong1997/Bilibili-Obsidian-Clipper/pull/24
https://github.com/haixiong1997/Bilibili-Obsidian-Clipper
https://github.com/haixiong1997/Bilibili-Obsidian-Clipper/pull/22