obsidian-plugin-dev
SKILL.md
Obsidian 插件开发指南
项目初始化
1. 创建目录结构
plugin-name/
├── manifest.json # 插件清单(必需)
├── package.json # npm 配置
├── tsconfig.json # TypeScript 配置
├── esbuild.config.mjs # 构建配置
├── main.ts # 插件入口(必需)
├── styles.css # 样式文件(可选)
└── src/ # 源代码目录
├── components/ # UI 组件
├── services/ # 服务层
└── utils/ # 工具函数
2. manifest.json 模板
{
"id": "plugin-name",
"name": "Plugin Display Name",
"version": "1.0.0",
"minAppVersion": "0.16.0",
"description": "插件描述",
"author": "作者名",
"authorUrl": "https://github.com/username",
"isDesktopOnly": false
}
3. package.json 模板
{
"name": "plugin-name",
"version": "1.0.0",
"description": "插件描述",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.11.5",
"esbuild": "^0.20.0",
"obsidian": "latest",
"tslib": "^2.8.1",
"typescript": "^5.3.3"
}
}
4. tsconfig.json 模板
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": ["DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts"]
}
5. esbuild.config.mjs 模板(关键配置)
import esbuild from "esbuild";
import process from "process";
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',
],
format: 'cjs',
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
platform: 'node', // 关键:允许使用 Node.js 模块
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}
重要说明:
platform: 'node'是关键配置,允许插件使用 Node.js 内置模块(如http、fs、path)- 不要使用
builtin-modules包将 Node.js 模块标记为外部依赖,否则运行时会找不到这些模块 external数组只包含 Obsidian 相关的模块
插件入口模板 (main.ts)
import { Plugin, Notice, PluginSettingTab, App, Setting } from 'obsidian';
interface MyPluginSettings {
setting1: string;
setting2: boolean;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
setting1: 'default',
setting2: true
};
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
console.log('Loading plugin...');
// 加载设置
await this.loadSettings();
// 添加设置页面
this.addSettingTab(new MyPluginSettingTab(this.app, this));
// 注册命令
this.addCommand({
id: 'my-command',
name: '我的命令',
callback: () => {
new Notice('命令执行成功!');
}
});
// 添加状态栏项
const statusBarItem = this.addStatusBarItem();
statusBarItem.setText('插件已加载');
// 注册事件监听
this.registerEvent(
this.app.vault.on('modify', (file) => {
console.log('File modified:', file.path);
})
);
}
async onunload() {
console.log('Unloading plugin...');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class MyPluginSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: '插件设置' });
new Setting(containerEl)
.setName('设置项 1')
.setDesc('设置项描述')
.addText(text => text
.setPlaceholder('请输入...')
.setValue(this.plugin.settings.setting1)
.onChange(async (value) => {
this.plugin.settings.setting1 = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('设置项 2')
.setDesc('开关类型设置')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.setting2)
.onChange(async (value) => {
this.plugin.settings.setting2 = value;
await this.plugin.saveSettings();
}));
}
}
常见问题及解决方案
问题 1: MCP Server 启动失败 / Node.js 模块无法加载
症状:插件使用 http、ws 等 Node.js 模块时报错
原因:esbuild 配置不正确,Node.js 模块被标记为外部依赖
解决方案:
// esbuild.config.mjs
// 1. 添加 platform: 'node'
platform: 'node',
// 2. 不要使用 builtin-modules
// 错误做法:
// import builtins from "builtin-modules";
// external: [...builtins]
// 3. 只将 Obsidian 相关模块标记为外部
external: [
'obsidian',
'electron',
'@codemirror/*',
'@lezer/*',
]
问题 2: WebSocket 类型冲突
症状:TypeScript 报错 WebSocket 类型冲突
原因:ws 库的 WebSocket 与浏览器全局 WebSocket 类型冲突
解决方案:
// 使用别名导入
import { WebSocketServer, WebSocket as WebSocketNode } from 'ws';
// 使用时
private _wsClients: Set<WebSocketNode> = new Set();
问题 3: PluginSettingTab 继承错误
症状:SpecConfirmSettingTab doesn't extend PluginSettingTab properly
原因:未正确继承 PluginSettingTab 或未调用父类构造函数
解决方案:
import { PluginSettingTab, App } from 'obsidian';
class MySettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin); // 必须调用父类构造函数
this.plugin = plugin;
}
display(): void {
const { containerEl } = this; // containerEl 来自父类
containerEl.empty();
// ...
}
}
问题 4: 缺少依赖
症状:构建时报错缺少 tslib 或类型定义
解决方案:
npm install --save-dev tslib @types/node @types/ws
问题 5: openLinkText 参数类型错误
症状:app.workspace.openLinkText() 第二个参数类型不匹配
解决方案:
// 第二个参数是 sourcePath,应为字符串
await app.workspace.openLinkText(file.path, '');
// 或
await app.workspace.openLinkText(file.path, file.path);
Obsidian API 常用功能
文件操作
// 读取文件
const content = await this.app.vault.read(file);
// 修改文件
await this.app.vault.modify(file, newContent);
// 创建文件
await this.app.vault.create(path, content);
// 删除文件
await this.app.vault.delete(file);
// 获取文件
const file = this.app.vault.getAbstractFileByPath(path);
Frontmatter 操作
import { TFile } from 'obsidian';
// 读取 frontmatter
const cache = this.app.metadataCache.getFileCache(file);
const frontmatter = cache?.frontmatter;
// 更新 frontmatter(推荐方式,原子操作)
await this.app.fileManager.processFrontMatter(file, (fm) => {
fm.status = '已确认';
fm.updated = new Date().toISOString();
});
UI 组件
import { Notice, Modal, Setting } from 'obsidian';
// 显示通知
new Notice('操作成功!');
new Notice('操作成功!', 5000); // 5秒后消失
// 创建模态框
class MyModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: '标题' });
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('确认')
.setCta()
.onClick(() => {
this.close();
}));
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
// 使用模态框
new MyModal(this.app).open();
命令注册
this.addCommand({
id: 'unique-command-id',
name: '命令显示名称',
// 简单回调
callback: () => {
// 执行操作
},
// 或者检查回调(返回 false 禁用命令)
checkCallback: (checking: boolean) => {
const file = this.app.workspace.getActiveFile();
if (file) {
if (!checking) {
// 执行操作
}
return true;
}
return false;
}
});
事件监听
// 文件修改
this.registerEvent(
this.app.vault.on('modify', (file) => {
console.log('Modified:', file.path);
})
);
// 文件创建
this.registerEvent(
this.app.vault.on('create', (file) => {
console.log('Created:', file.path);
})
);
// 文件删除
this.registerEvent(
this.app.vault.on('delete', (file) => {
console.log('Deleted:', file.path);
})
);
// 活动文件变化
this.registerEvent(
this.app.workspace.on('active-leaf-change', (leaf) => {
// 处理活动文件变化
})
);
构建和安装
开发模式
npm run dev
生产构建
npm run build
安装到 Obsidian
-
将以下文件复制到 Obsidian vault 的
.obsidian/plugins/plugin-name/目录:main.jsmanifest.jsonstyles.css(如有)
-
在 Obsidian 设置中启用插件
调试
- 打开 Obsidian 开发者控制台:
Ctrl+Shift+I(Windows/Linux)或Cmd+Option+I(Mac) - 查看 Console 标签页中的日志输出
参考资源
Weekly Installs
5
Repository
hhu3637kr/skillsGitHub Stars
107
First Seen
12 days ago
Security Audits
Installed on
gemini-cli5
github-copilot5
codex5
amp5
cline5
kimi-cli5