Obsidian 小黑同步插件开发教程
从零构建 Obsidian + OmniBox(小黑)同步插件的完整实战记录
包含开发过程、核心代码、踩坑经验、使用方法
📖 目录
- 小黑(OmniBox)介绍
- 插件功能介绍
- 开发环境准备
- 项目结构
- 核心逻辑说明
- API 对接详解
- 缓存系统设计
- 踩坑与经验教训
- 安装与使用方法
- 完整代码文件清单
一、小黑(OmniBox)介绍
小黑(OmniBox)是一个个人知识库与内容收集平台,帮助用户收集、整理、管理个人知识内容。它提供了文档存储与管理(Markdown 为主)、文件夹分类体系、AI 智能问答、网页内容收集(浏览器插件)、开放 API(Open API)供开发者集成。
Open API 基础信息
| 项目 | 说明 |
|---|---|
| Base URL | https://api.omnibox.pro/v1 |
| 认证方式 | Authorization: Bearer <api-key> |
| 配额限制 | 基础版 10 次/24 h,高级版 1000 次/24 h |
| 配额重置 | 第一次调用 API 时间 + 24 小时 |
| Swagger 文档 | https://api.omnibox.pro/docs |
权限体系
创建 API Key 时需要设置权限范围:
- 资源权限:create, read, update, delete — 管理文件、文件夹、标签关联
- 对话权限:create — 调用 AI 对话能力
- 标签权限:create, read — 创建和查询标签
- 搜索权限:read — 搜索资源
核心 API 端点
| 端点 | 方法 | 用途 | 消耗配额 |
|---|---|---|---|
/api-keys/info | GET | 查询 API Key 信息 | ❌ 不消耗 |
/resources | GET | 获取资源列表 | ✅ 消耗 |
/resources?parent_id={id} | GET | 获取文件夹子内容 | ✅ 消耗 |
/resources/{id} | GET | 下载单个资源 | ✅ 消耗 |
/resources/{id} | DELETE | 删除资源 | ✅ 消耗 |
/resources | POST | 创建资源 | ✅ 消耗 |
/resources/{id} | PATCH | 更新资源 | ✅ 消耗 |
/search | GET | 搜索资源 | ✅ 消耗 |
资源类型
API 中的资源分为两种类型:
doc:文档(包含 content 内容)folder:文件夹(无 content,用于组织文档)
响应数据结构
获取资源列表响应:
{
"resources": [
{
"id": "resource-id",
"name": "Resource name",
"resource_type": "doc",
"updated_at": "2026-07-02T16:08:49.703Z",
"parent_id": "parent-id"
}
],
"total": 42
}获取 API 用量响应:
{
"open_api_requests_quota": {
"limit": 10,
"used": 3,
"remaining": 7,
"reset_at": "2026-07-03T16:08:49.703Z"
}
}二、插件功能介绍
核心功能
| 功能 | 说明 |
|---|---|
| 文件夹展示 | 树形结构显示云端分类,默认折叠 |
| 文档展示 | 按更新时间越新越靠前 |
| 下载文档 | 单个下载,不触发刷新(节省 API) |
| 阅读文档 | 已下载文件显示「阅读」按钮,点击打开本地文件 |
| 删除文档 | 删除云端资源 |
| 增量刷新 | 只获取元数据,不下载内容 |
| 下载更新 | 只下载有变动的文件 |
| 本地缓存 | 独立缓存文件,禁用启用后自动恢复列表 |
| API 用量 | 状态栏实时显示剩余次数和恢复时间 |
状态图标
- 📄 未下载 → 下载 / 删除
- ✅ 已下载 → 阅读 / 再次下载 / 删除
- 🆕 有更新 → 下载 / 删除
排序规则
- 文件夹:按
updatedAt越早越靠前(所有文件夹在文档前面) - 文档:按
updatedAt越新越靠前(递归应用到所有层级)
边栏按钮
- 刷新:获取最新资源列表
- 下载更新:只下载有变动的文件
- 下载全部:下载所有文件(谨慎使用)
- 展开/折叠:切换所有文件夹展开/折叠状态
三、开发环境准备
mkdir omnibox-sync-plugin
cd omnibox-sync-plugin
npm init -y
npm install --save-dev obsidian @types/node esbuild typescript tslib builtin-modules关键点: 文件夹名必须与 manifest.json 中的 id 一致。
package.json scripts:
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
}四、项目结构
omnibox-sync-plugin/
├── manifest.json # 插件清单
├── package.json # 依赖管理
├── tsconfig.json # TypeScript 配置
├── esbuild.config.mjs # 构建配置
├── main.ts # 插件主入口
├── settings.ts # 设置页面
├── sidebar.ts # 边栏视图
├── api.ts # API 封装
└── styles.css # 样式五、核心逻辑说明
main.ts(插件主入口)
onload():加载设置 → 加载缓存 → 初始化 API → 注册设置页 → 注册边栏 → 添加图标和命令loadCache()/saveCache():读写独立缓存文件data_cache.jsongetCachedResources()/updateCache():缓存数据存取clearCache():清空缓存
settings.ts(设置页面)
- API Key 输入
- 本地同步文件夹路径
- 自动同步开关
- 同步间隔设置
- 覆盖策略开关
- 测试连接(移动到覆盖策略下方)
sidebar.ts(边栏视图)
- 树形结构渲染
- 下载/阅读/删除操作
- 增量刷新
- 下载更新
- API 用量显示
- 展开/折叠控制
- 下载状态检测(已下载显示 ✅)
api.ts(API 封装)
fetchResourcesByParent(parentId?):获取指定父级下的资源列表fetchFullTree(parentId?):递归获取完整树形结构downloadResource(id):下载单个资源内容deleteResource(id):删除资源getApiKeyInfo():查询 API 用量(不消耗配额)testConnection():测试 API Key 有效性
六、API 对接详解
关键问题:文件夹识别
API 返回的 resource_type 字段只有 doc 和 folder,但列表接口返回的文件夹 resource_type 可能为 file。解决方案:通过文件名是否包含扩展名来判断。
配额优化策略
- 增量刷新:只获取列表元数据,不下载内容
- 按需下载:只下载用户点击的文件
- 下载更新:只下载有变动的文件
- 缓存复用:禁用启用后从缓存恢复,不消耗 API
API 用量查询
GET /api-keys/info 不消耗配额,返回 open_api_requests_quota 字段:
limit:总配额used:已使用remaining:剩余reset_at:重置时间(第一次调用 + 24 h)
API 消耗对照表
| 操作 | 消耗次数 |
|---|---|
| 刷新列表 | 1 + 文件夹数 次 |
| 下载单个文档 | 1 次 |
| 再次下载 | 1 次 |
| 删除文档 | 1 次 |
| 下载更新 | 变动文件数 次 |
| 查看 API 用量 | 0 次 |
七、缓存系统设计
独立缓存文件
缓存保存在 data_cache.json,与设置文件 data.json 分离。
优点: 清空缓存不影响 API Key 等设置,删除设置文件不影响缓存。
缓存数据结构
{
"cachedResources": [...],
"lastSyncTime": "2026-07-02 T 16:08:49.703 Z"
}缓存加载时机
- 插件加载时自动读取缓存
- 禁用再启用插件后自动恢复
- 重启 Obsidian 后自动恢复
- 刷新成功时自动更新缓存
- API 失败时保留旧缓存(不被覆盖)
缓存保护逻辑
- 只有 API 成功返回时才更新缓存
- API 失败(429)时保持原有缓存不变
- 下载/删除后更新缓存中的时间戳,不触发刷新
八、踩坑与经验教训
坑 1:路径不匹配
问题: data_cache.json 写入失败,因为代码中路径是 omnibox-sync,实际文件夹是 omnibox-sync-plugin。
解决: 统一使用实际文件夹名 omnibox-sync-plugin。
坑 2:API 权限范围
问题: 直接调用 /resources 返回 401,因为 API Key 绑定了特定 parent_id。
解决: 在 API 调用中传入正确的 parent_id(从 getApiKeyInfo() 获取 root_resource_id)。
坑 3:下载后状态栏变「就绪」
问题: 下载文档后,updateStatus('就绪') 覆盖了统计数据。
解决: 保存 currentStatusText,下载后恢复状态,而非重置为「就绪」。
坑 4:缓存模式不显示恢复时间
问题: API 用完进入缓存模式后,状态栏只显示「已限流」,不显示恢复时间。
解决: 缓存模式下仍然调用 GET /api-keys/info 获取配额信息(不消耗配额),显示恢复时间。
坑 5:下载后触发刷新浪费 API
问题: 下载文档后调用 refresh(),导致额外消耗 API 次数。
解决: 下载后只更新缓存中的时间戳,不调用 refresh()。
坑 6:API 失败时缓存被覆盖
问题: API 返回 429 时,旧缓存被错误覆盖。
解决: 只有 API 成功返回时才更新缓存,失败时保持不变。
坑 7:文件夹默认展开
问题: 默认展开所有文件夹,文档多时加载慢。
解决: 改为默认折叠,用户按需展开。
坑 8:排序规则混乱
问题: 文件夹和文档混在一起,不便查找。
解决: 文件夹按时间越早越靠前(在文档前面),文档按时间越新越靠前。
坑 9:TypeScript 类型定义
问题: open_api_requests_quota 字段未定义导致编译错误。
解决: 在 ApiKeyInfo 接口中添加 open_api_requests_quota? 字段定义。
坑 10:缓存文件写入失败
问题: app.vault.adapter.write() 写入失败,因为目录不存在。
解决: 写入前检查目录是否存在,不存在则创建。
坑 11:已下载状态未持久化
问题: 已下载状态只在内存中,重启后丢失。
解决: 每次渲染时实时检测本地文件是否存在,不依赖缓存。
九、安装与使用方法
安装步骤
- 编译插件:
npm run build - 将
main.js、manifest.json、styles.css复制到.obsidian/plugins/omnibox-sync-plugin/ - 在 Obsidian 设置中启用插件
- 在插件设置中填入 API Key
使用流程
- 点击边栏「刷新」加载云端资源
- 点击文件夹前 ▸ 展开查看内容
- 点击「下载」获取文档到本地
- 已下载文档显示 ✅ 和「阅读」按钮,点击打开本地文件
- 点击「下载更新」只下载有变动的文件
- 点击「展开/折叠」切换所有文件夹状态
API 次数管理建议
- 每天先点击「刷新」获取列表
- 再按需下载文档,避免频繁刷新浪费 API
- 使用「下载更新」批量获取有变动的文件
- 查看 API 用量不消耗配额,可随时查询
十、完整代码文件清单
manifest.json
{
"id": "omnibox-sync-plugin",
"name": "OmniBox 同步",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "同步小黑 (OmniBox) 的内容到 Obsidian",
"author": "Your Name",
"isDesktopOnly": false
}tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES 6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": ["DOM", "ES 5", "ES 6", "ES 7"]
},
"include": ["**/*.ts"]
}esbuild.config.mjs
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/`;
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: { js: banner },
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es 2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}十一、状态栏显示说明
| 状态 | 显示示例 | |
|---|---|---|
| 正常 | `3 个文件, 1 个分类, 0 个有更新 | API: 7/10 (剩余 3 次,00:08 重置)` |
| API 用完 | `3 个文件, 1 个分类, 0 个有更新 | ⛔ API 已用完,将于 00:08 恢复` |
| 缓存模式 | `3 个文件, 1 个分类, 0 个有更新 | ⛔ API 已用完,将于 00:08 恢复 (缓存数据)` |
十二、扩展建议
- 双向同步:支持从 Obsidian 推送笔记到小黑
- AI 问答集成:利用小黑 AI 接口在 Obsidian 中提问
- 标签同步:同步小黑和 Obsidian 的标签体系
- 搜索集成:在 Obsidian 中搜索小黑内容
- 自动同步:定时后台同步(需注意 API 配额)
- 冲突处理:云端和本地同时修改时的合并策略
十三、常见问题
Q:API 次数用完了怎么办?
A:等待 24 小时自动重置,或升级到高级版(每天 1000 次)。
Q:为什么文件夹显示为空?
A:API 次数可能已用完,检查状态栏的 API 剩余次数。
Q:禁用再启用插件后列表是空的?
A:需要先成功刷新一次生成缓存,之后禁用启用会自动恢复。
Q:下载后状态栏变「就绪」?
A:已修复,现在下载后 API 用量会自动更新。
Q:如何查看 API 剩余次数?
A:状态栏会显示,或在控制台执行 plugin.api.getApiKeyInfo()。