skills/hhu3637kr/skills/obsidian-plugin-dev

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 内置模块(如 httpfspath
  • 不要使用 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 模块无法加载

症状:插件使用 httpws 等 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

  1. 将以下文件复制到 Obsidian vault 的 .obsidian/plugins/plugin-name/ 目录:

    • main.js
    • manifest.json
    • styles.css(如有)
  2. 在 Obsidian 设置中启用插件

调试

  • 打开 Obsidian 开发者控制台:Ctrl+Shift+I(Windows/Linux)或 Cmd+Option+I(Mac)
  • 查看 Console 标签页中的日志输出

参考资源

Weekly Installs
5
GitHub Stars
107
First Seen
12 days ago
Installed on
gemini-cli5
github-copilot5
codex5
amp5
cline5
kimi-cli5