7.2 KiB
Executable File
7.2 KiB
Executable File
| tags | name | description | version | triggers | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
obsidian-plugin | Obsidian 第三方插件开发工作流 — 从骨架到安装,含 Go CORS 后端适配。D3 图谱、侧栏视图、搜索模态框、esbuild 打包。 | 1.0.0 |
|
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