Skip to main content 首页 创作者 dicklesworthstone pi_agent_rust obsidian-reference-architecture
obsidian-reference-architecture Implement Obsidian reference architecture with best-practice project layout.
Use when designing new plugins, reviewing project structure,
or establishing architecture standards for Obsidian development.
Trigger with phrases like "obsidian architecture", "obsidian project structure",
"obsidian best practices", "organize obsidian plugin".
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Dicklesworthstone/pi_agent_rust --skill obsidian-reference-architecture命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... name obsidian-reference-architecture description Implement Obsidian reference architecture with best-practice project layout.
Use when designing new plugins, reviewing project structure,
or establishing architecture standards for Obsidian development.
Trigger with phrases like "obsidian architecture", "obsidian project structure",
"obsidian best practices", "organize obsidian plugin".
allowed-tools Read, Grep version 1.0.0 license MIT author Jeremy Longshore <jeremy@intentsolutions.io>
Obsidian Reference Architecture
Overview
Production-ready architecture patterns for Obsidian plugin development.
Prerequisites
Understanding of layered architecture
TypeScript and Obsidian API knowledge
Project setup complete
Project Structure
my-obsidian-plugin/
├── src/
│ ├── main.ts # Plugin entry point
│ ├── types.ts # TypeScript type definitions
│ ├── constants.ts # Constants and configuration
│ │
│ ├── settings/
│ │ ├── settings.ts # Settings interface & defaults
│ │ ├── settings-tab.ts # Settings UI component
│ │ └── settings-migration.ts # Settings version migration
│ │
│ ├── services/
│ │ ├── vault-service.ts # Vault operations
│ │ ├── metadata-service.ts # Frontmatter/cache operations
│ │ ├── search-service.ts # Search functionality
│ │ └── api-service.ts # External API integration
│ │
│ ├── commands/
│ │ ├── index.ts # Command registration
│ │ ├── note-commands.ts # Note-related commands
│ │ └── utility-commands.ts # Utility commands
│ │
│ ├── ui/
│ │ ├── modals/
│ │ │ ├── input-modal.ts
│ │ │ └── confirm-modal.ts
│ │ ├── views/
│ │ │ ├── sidebar-view.ts
│ │ │ └── view-registry.ts
│ │ └── components/
│ │ ├── status-bar.ts
│ │ └── ribbon-icon.ts
│ │
│ ├── events/
│ │ ├── event-manager.ts # Event registration
│ │ └── event-handlers.ts # Event handler implementations
│ │
│ └── utils/
│ ├── debounce.ts
│ ├── async-queue.ts
│ ├── cache.ts
│ └── logger.ts
│
├── tests/
│ ├── services/
│ │ └── vault-service.test.ts
│ ├── commands/
│ │ └── note-commands.test.ts
│ └── setup.ts # Test configuration
│
├── styles/
│ └── styles.css # Plugin styles
│
├── docs/
│ └── ARCHITECTURE.md # Architecture documentation
│
├── manifest.json # Plugin manifest
├── versions.json # Version compatibility
├── package.json # Node dependencies
├── tsconfig.json # TypeScript configuration
├── esbuild.config.mjs # Build configuration
├── .eslintrc.js # Linting rules
└── .gitignore
Layer Architecture ┌─────────────────────────────────────────┐
│ UI Layer │
│ (Views, Modals, Components) │
├─────────────────────────────────────────┤
│ Command Layer │
│ (Commands, Event Handlers) │
├─────────────────────────────────────────┤
│ Service Layer │
│ (Business Logic, Data Access) │
├─────────────────────────────────────────┤
│ Infrastructure Layer │
│ (Cache, Logger, Utilities) │
└─────────────────────────────────────────┘
Key Components
Step 1: Main Plugin Entry Point
import { Plugin } from 'obsidian' ;
import { MyPluginSettings , DEFAULT_SETTINGS , MyPluginSettingsTab } from './settings' ;
import { VaultService } from './services/vault-service' ;
import { registerCommands } from './commands' ;
import { EventManager } from './events/event-manager' ;
import { registerViews } from './ui/views/view-registry' ;
import { Logger } from './utils/logger' ;
export default class MyPlugin extends Plugin {
settings : MyPluginSettings ;
private logger : Logger ;
private vaultService : VaultService ;
private eventManager : EventManager ;
async onload ( ) {
this .logger = new Logger (this .manifest .id );
this .logger .info ('Loading plugin' );
await this .loadSettings ();
this .vaultService = new VaultService (this .app );
this .eventManager = new EventManager (this );
this .addSettingTab (new MyPluginSettingsTab (this .app , this ));
registerViews (this );
registerCommands (this );
this .app .workspace .onLayoutReady (() => {
this .eventManager .registerAll ();
this .logger .info ('Plugin ready' );
});
}
onunload ( ) {
this .logger .info ('Unloading plugin' );
}
async loadSettings ( ) {
const data = await this .loadData ();
this .settings = Object .assign ({}, DEFAULT_SETTINGS , data);
}
async saveSettings ( ) {
await this .saveData (this .settings );
}
getVaultService (): VaultService {
return this .vaultService ;
}
}
Step 2: Service Layer Pattern
import { App , TFile , TFolder , Vault , CachedMetadata } from 'obsidian' ;
export class VaultService {
constructor (private app : App ) {}
get vault (): Vault {
return this .app .vault ;
}
async readFile (file : TFile ): Promise <string > {
return this .vault .read (file);
}
async writeFile (file : TFile , content : string ): Promise <void > {
await this .vault .modify (file, content);
}
async createFile (path : string , content : string ): Promise <TFile > {
await this .ensureFolder (path);
return this .vault .create (path, content);
}
getFileByPath (path : string ): TFile | null {
const file = this .vault .getAbstractFileByPath (path);
return file instanceof TFile ? file : null ;
}
private async ensureFolder (filePath : string ): Promise <void > {
const folderPath = filePath.substring (0 , filePath.lastIndexOf ('/' ));
if (!folderPath) return ;
const folder = this .vault .getAbstractFileByPath (folderPath);
if (!folder) {
await this .vault .createFolder (folderPath);
}
}
getMarkdownFiles (): TFile [] {
return this .vault .getMarkdownFiles ();
}
getFilesInFolder (folderPath : string ): TFile [] {
return this .getMarkdownFiles ().filter (f => f.path .startsWith (folderPath + '/' ));
}
getMetadata (file : TFile ): CachedMetadata | null {
return this .app .metadataCache .getFileCache (file);
}
getFrontmatter (file : TFile ): Record <string , any > | null {
return this .getMetadata (file)?.frontmatter || null ;
}
}
Step 3: Command Registration Pattern
import { Plugin } from 'obsidian' ;
import { registerNoteCommands } from './note-commands' ;
import { registerUtilityCommands } from './utility-commands' ;
export function registerCommands (plugin : Plugin ): void {
registerNoteCommands (plugin);
registerUtilityCommands (plugin);
}
import { Plugin , MarkdownView , Editor } from 'obsidian' ;
export function registerNoteCommands (plugin : Plugin ): void {
plugin.addCommand ({
id : 'my-plugin-action' ,
name : 'Perform Action' ,
callback : () => {
},
});
plugin.addCommand ({
id : 'my-plugin-editor-action' ,
name : 'Editor Action' ,
editorCallback : (editor : Editor , view : MarkdownView ) => {
},
});
plugin.addCommand ({
id : 'my-plugin-conditional' ,
name : 'Conditional Action' ,
checkCallback : (checking : boolean ) => {
const view = plugin.app .workspace .getActiveViewOfType (MarkdownView );
if (view) {
if (!checking) {
}
return true ;
}
return false ;
},
});
}
Step 4: Event Manager Pattern
import { Plugin , TFile , TAbstractFile } from 'obsidian' ;
import { debounce } from '../utils/debounce' ;
export class EventManager {
constructor (private plugin : Plugin ) {}
registerAll (): void {
this .registerVaultEvents ();
this .registerWorkspaceEvents ();
}
private registerVaultEvents (): void {
const onModify = debounce ((file : TAbstractFile ) => {
if (file instanceof TFile ) {
this .handleFileModify (file);
}
}, 500 );
this .plugin .registerEvent (
this .plugin .app .vault .on ('modify' , onModify)
);
this .plugin .registerEvent (
this .plugin .app .vault .on ('create' , (file ) => {
if (file instanceof TFile ) {
this .handleFileCreate (file);
}
})
);
this .plugin .registerEvent (
this .plugin .app .vault .on ('delete' , (file ) => {
this .handleFileDelete (file);
})
);
this .plugin .registerEvent (
this .plugin .app .vault .on ('rename' , (file, oldPath ) => {
if (file instanceof TFile ) {
this .handleFileRename (file, oldPath);
}
})
);
}
private registerWorkspaceEvents (): void {
this .plugin .registerEvent (
this .plugin .app .workspace .on ('file-open' , (file ) => {
if (file) {
this .handleFileOpen (file);
}
})
);
}
private handleFileModify (file : TFile ): void {
}
private handleFileCreate (file : TFile ): void {
}
private handleFileDelete (file : TAbstractFile ): void {
}
private handleFileRename (file : TFile , oldPath : string ): void {
}
private handleFileOpen (file : TFile ): void {
}
}
Step 5: Settings Pattern
export interface MyPluginSettings {
enabled : boolean ;
apiEndpoint : string ;
maxItems : number ;
excludeFolders : string [];
settingsVersion : number ;
}
export const DEFAULT_SETTINGS : MyPluginSettings = {
enabled : true ,
apiEndpoint : '' ,
maxItems : 100 ,
excludeFolders : [],
settingsVersion : 1 ,
};
import { App , PluginSettingTab , Setting } from 'obsidian' ;
import type MyPlugin from '../main' ;
export class MyPluginSettingsTab 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 : 'My Plugin Settings' });
new Setting (containerEl)
.setName ('Enable Plugin' )
.setDesc ('Turn the plugin features on or off' )
.addToggle (toggle => toggle
.setValue (this .plugin .settings .enabled )
.onChange (async (value) => {
this .plugin .settings .enabled = value;
await this .plugin .saveSettings ();
}));
new Setting (containerEl)
.setName ('Max Items' )
.setDesc ('Maximum number of items to display' )
.addSlider (slider => slider
.setLimits (10 , 500 , 10 )
.setValue (this .plugin .settings .maxItems )
.setDynamicTooltip ()
.onChange (async (value) => {
this .plugin .settings .maxItems = value;
await this .plugin .saveSettings ();
}));
}
}
Data Flow Diagram User Action (Command/Event)
│
▼
┌─────────────────┐
│ UI Layer │
│ (Modal/View) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Command Handler │
│ (Orchestration) │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────┐
│ Service Layer │────▶│ Cache │
│ (Business) │ │ (Memory) │
└────────┬────────┘ └─────────────┘
│
▼
┌─────────────────┐
│ Obsidian API │
│ (Vault/Cache) │
└─────────────────┘
Output
Organized project structure
Clear separation of concerns
Reusable service layer
Centralized event management
Type-safe settings
Error Handling Issue Cause Solution Circular dependencies Wrong imports Use interface segregation Missing types Incomplete definitions Create types.ts Event leaks Unregistered events Use registerEvent Settings lost Migration missing Implement version migration
Examples
Quick Setup Script #!/bin/bash
mkdir -p src/{services,commands,ui/{modals,views,components},events,utils,settings}
mkdir -p tests/{services,commands}
touch src/main.ts
touch src/types.ts
touch src/constants.ts
touch src/settings/{settings,settings-tab,settings-migration}.ts
touch src/services/{vault-service,metadata-service,search-service}.ts
touch src/commands/{index,note-commands,utility-commands}.ts
touch src/events/{event-manager,event-handlers}.ts
touch src/utils/{debounce,async-queue,cache,logger}.ts
echo "Plugin structure created!"
Resources
Flagship Skills For multi-environment setup, see obsidian-multi-env-setup.