三股水
Obsidian 小黑同步插件开发教程
本文从 Obsidian 撰写发布

Obsidian 小黑同步插件开发教程

从零构建 Obsidian + OmniBox(小黑)同步插件的完整实战记录
包含开发过程、核心代码、踩坑经验、使用方法

📖 目录

  1. 小黑(OmniBox)介绍
  2. 插件功能介绍
  3. 开发环境准备
  4. 项目结构
  5. 核心逻辑说明
  6. API 对接详解
  7. 缓存系统设计
  8. 踩坑与经验教训
  9. 安装与使用方法
  10. 完整代码文件清单

一、小黑(OmniBox)介绍

小黑(OmniBox)是一个个人知识库与内容收集平台,帮助用户收集、整理、管理个人知识内容。它提供了文档存储与管理(Markdown 为主)、文件夹分类体系、AI 智能问答、网页内容收集(浏览器插件)、开放 API(Open API)供开发者集成。

Open API 基础信息

项目说明
Base URLhttps://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/infoGET查询 API Key 信息❌ 不消耗
/resourcesGET获取资源列表✅ 消耗
/resources?parent_id={id}GET获取文件夹子内容✅ 消耗
/resources/{id}GET下载单个资源✅ 消耗
/resources/{id}DELETE删除资源✅ 消耗
/resourcesPOST创建资源✅ 消耗
/resources/{id}PATCH更新资源✅ 消耗
/searchGET搜索资源✅ 消耗

资源类型

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.json
  • getCachedResources() / 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 字段只有 docfolder,但列表接口返回的文件夹 resource_type 可能为 file。解决方案:通过文件名是否包含扩展名来判断。

配额优化策略

  1. 增量刷新:只获取列表元数据,不下载内容
  2. 按需下载:只下载用户点击的文件
  3. 下载更新:只下载有变动的文件
  4. 缓存复用:禁用启用后从缓存恢复,不消耗 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"
}

缓存加载时机

  1. 插件加载时自动读取缓存
  2. 禁用再启用插件后自动恢复
  3. 重启 Obsidian 后自动恢复
  4. 刷新成功时自动更新缓存
  5. 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:已下载状态未持久化

问题: 已下载状态只在内存中,重启后丢失。
解决: 每次渲染时实时检测本地文件是否存在,不依赖缓存。

九、安装与使用方法

安装步骤

  1. 编译插件:npm run build
  2. main.jsmanifest.jsonstyles.css 复制到 .obsidian/plugins/omnibox-sync-plugin/
  3. 在 Obsidian 设置中启用插件
  4. 在插件设置中填入 API Key

使用流程

  1. 点击边栏「刷新」加载云端资源
  2. 点击文件夹前 ▸ 展开查看内容
  3. 点击「下载」获取文档到本地
  4. 已下载文档显示 ✅ 和「阅读」按钮,点击打开本地文件
  5. 点击「下载更新」只下载有变动的文件
  6. 点击「展开/折叠」切换所有文件夹状态

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 恢复 (缓存数据)`

十二、扩展建议

  1. 双向同步:支持从 Obsidian 推送笔记到小黑
  2. AI 问答集成:利用小黑 AI 接口在 Obsidian 中提问
  3. 标签同步:同步小黑和 Obsidian 的标签体系
  4. 搜索集成:在 Obsidian 中搜索小黑内容
  5. 自动同步:定时后台同步(需注意 API 配额)
  6. 冲突处理:云端和本地同时修改时的合并策略

十三、常见问题

Q:API 次数用完了怎么办?
A:等待 24 小时自动重置,或升级到高级版(每天 1000 次)。

Q:为什么文件夹显示为空?
A:API 次数可能已用完,检查状态栏的 API 剩余次数。

Q:禁用再启用插件后列表是空的?
A:需要先成功刷新一次生成缓存,之后禁用启用会自动恢复。

Q:下载后状态栏变「就绪」?
A:已修复,现在下载后 API 用量会自动更新。

Q:如何查看 API 剩余次数?
A:状态栏会显示,或在控制台执行 plugin.api.getApiKeyInfo()


😊
提交