xiaowei-system/skills/software-development/obsidian-plugin/SKILL.md

7.2 KiB
Executable File
Raw Blame History

tags name description version triggers
software development
obsidian-plugin Obsidian 第三方插件开发工作流 — 从骨架到安装,含 Go CORS 后端适配。D3 图谱、侧栏视图、搜索模态框、esbuild 打包。 1.0.0
开发 obsidian 插件
obsidian plugin
obsidian 插件
织忆 obsidian
zhiyi obsidian

Obsidian 插件开发

构建 Obsidian 第三方插件的完整工作流。覆盖从骨架到安装的全流程,以及与 Go/Rust 后端集成的 CORS 适配。

触发条件

  • 用户要求「开发 Obsidian 插件」
  • 用户要求「给 XX 写 Obsidian 插件」
  • 任何涉及 Obsidian API 的开发任务

核心文件结构

plugins/<name>/
├── manifest.json          # 插件清单id/name/version/minAppVersion/main/styles
├── main.ts                # 插件入口(源,未经 esbuild
├── main.js                # esbuild 产物Obsidian 实际加载这个)
├── styles.css             # 全局样式
├── src/
│   ├── api.ts             # 后端 API 客户端fetch 封装)
│   ├── MyView.ts          # 自定义视图(扩展 ItemView
│   └── MyModal.ts         # 自定义模态框(扩展 Modal
├── esbuild.config.mjs     # 构建配置
└── README.md

标准开发步骤

1. 创建 manifest.json

{
  "id": "zhiyi-memory",
  "name": "织忆",
  "version": "0.1.0",
  "minAppVersion": "0.15.0",
  "description": "...",
  "main": "main.js",
  "styles": ["styles.css"]
}

2. 编写 main.ts插件入口

import { App, Plugin, PluginSettingTab, Setting } from "obsidian";
import { MyView, MY_VIEW_TYPE } from "./src/MyView";
import { MyModal } from "./src/MyModal";

export default class MyPlugin extends Plugin {
  async onload() {
    // 注册视图
    this.registerView(MY_VIEW_TYPE, (leaf) => new MyView(leaf));

    // 添加命令
    this.addCommand({
      id: "my-command",
      name: "My Command",
      callback: () => this.openMyView(),
    });

    // 状态栏
    this.addStatusBarItem().setText("MyPlugin");

    // 设置页
    this.addSettingTab(new MySettingTab(this.app, this));
  }

  async openMyView() {
    const leaf = this.app.workspace.getLeaf("right");
    await leaf.setViewState({ type: MY_VIEW_TYPE, active: true });
    this.app.workspace.revealLeaf(leaf);
  }
}

3. 编写自定义视图ItemView

import { ItemView, WorkspaceLeaf } from "obsidian";

export const MY_VIEW_TYPE = "my-plugin-view";

export class MyView extends ItemView {
  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }

  getViewType() { return MY_VIEW_TYPE; }
  getDisplayText() { return "My View"; }

  async onOpen() {
    this.contentEl.empty();
    this.contentEl.createEl("div", { text: "Hello from MyView" });
  }
}

4. 编写 API 客户端src/api.ts

const API_BASE = "http://localhost:7821";
const API_KEY = "zhiyi-dev-key-2026"; // 实际应从设置读取

async function apiCall<T>(path: string, opts: RequestInit = {}): Promise<T> {
  const headers = {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
    ...(opts.headers as Record<string, string> || {}),
  };
  const res = await fetch(`${API_BASE}${path}`, { ...opts, headers });
  if (!res.ok) throw new Error(`API ${path} failed: ${res.status}`);
  const text = await res.text();
  return text ? JSON.parse(text) : ({} as T);
}

5. 编写 esbuild 配置esbuild.config.mjs

import * as esbuild from "esbuild";

const isWatch = process.argv.includes("--watch");

const config = {
  entryPoints: ["main.ts"],
  bundle: true,
  platform: "browser",
  target: "es2020",
  outfile: "main.js",
  format: "cjs",
  external: ["obsidian"],   // 关键Obsidian API 不打包
  sourcemap: !isWatch,
  minify: !isWatch,
};

if (isWatch) {
  const ctx = await esbuild.context(config);
  await ctx.watch();
} else {
  await esbuild.build(config);
  console.log("Build complete: main.js");
}

6. 安装依赖并构建

cd plugins/<name>
npm init -y
npm install esbuild
node esbuild.config.mjs

npm 常见问题~/.local/bin/npm 脚本路径错误时会导致 MODULE_NOT_FOUND。检查并修复脚本指向的 npm-cli.js 路径。

7. 安装到 Obsidian

mkdir -p ~/.obsidian/plugins/<plugin-id>/
cp main.js manifest.json styles.css ~/.obsidian/plugins/<plugin-id>/

然后在 Obsidian 中:设置 → 社区插件 → 开启「第三方插件」→ 找到插件 → 启用。

Go 后端 CORS 中间件(关键适配)

Obsidian 从 app://obsidian.md 发起 fetch 请求Go API 必须返回对应 CORS 头:

新增文件 go/internal/api/middleware/cors.go

package middleware

const obsidianOrigin = "app://obsidian.md"

func CORS() func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Access-Control-Allow-Origin", obsidianOrigin)
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "X-API-Key, Content-Type, Authorization")
            w.Header().Set("Access-Control-Allow-Credentials", "true")
            if r.Method == http.MethodOptions {
                w.WriteHeader(http.StatusNoContent)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

注册到 server.go

return middleware.CORS()(middleware.Auth(mux))

验证

curl -I -X OPTIONS http://localhost:7821/api/v1/stats \
  -H "Origin: app://obsidian.md" \
  -H "Access-Control-Request-Method: GET"
# 期望: HTTP/1.1 204 No Content + Access-Control-Allow-Origin: app://obsidian.md

D3.js 图谱集成(可选)

D3.js 建议从 CDN 动态加载(不打包):

async function loadD3(): Promise<typeof import("d3")> {
  if ((window as any)["d3"]) return (window as any)["d3"];
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
    script.onload = () => resolve((window as any)["d3"]);
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

Makefile 集成

OBSIDIAN_PLUGIN := plugins/<name>
OBSIDIAN_DEST  := $(HOME)/.obsidian/plugins/<plugin-id>

build-obsidian:
    cd $(OBSIDIAN_PLUGIN) && node esbuild.config.mjs

install-obsidian: build-obsidian
    cp $(OBSIDIAN_PLUGIN)/main.js $(OBSIDIAN_DEST)/
    cp $(OBSIDIAN_PLUGIN)/manifest.json $(OBSIDIAN_DEST)/
    cp $(OBSIDIAN_PLUGIN)/styles.css $(OBSIDIAN_DEST)/

TypeScript LSP 警告说明

编写 Obsidian 插件时TypeScript LSP 会报 Cannot find module 'obsidian' 错误 —— 这是正常的,因为 Obsidian API 由 Obsidian host 在运行时提供本地无类型定义文件。esbuild 构建不受影响LSP 警告可忽略。

参考资料

  • ~/projects/memoryweave/plugins/obsidian/ — 织忆插件完整实现(记忆面板 + 图谱视图 + 搜索模态框)
  • Obsidian 官方样例:obsidianmd/obsidian-sample-plugin