FeiSync
unlistedby Gao-Qian-Long
Sync your notes to Feishu (Lark) Drive.
FeiSync
Sync your Obsidian notes to Feishu (Lark) Drive.
Features
- One-way sync: Upload Obsidian notes to Feishu Drive with local files as the source of truth
- Incremental sync: Skip unchanged files based on SHA-256 content hash to save bandwidth
- Multi-folder sync: Configure multiple local folders mapped to different Feishu folders independently
- Manual sync: Trigger sync via command palette or ribbon icon
- Auto sync (optional): Watch for file changes and upload automatically with debouncing
- Scheduled sync (optional): Auto-sync at configurable intervals (1–1440 minutes)
- Download from Feishu: Pull files from cloud to local, with conflict detection via hash comparison
- Delete sync (optional): Remove cloud files when local files are deleted or renamed
- Ignore rules:
.feisync-ignore.mdfile with gitignore-compatible syntax - File tree browser: Browse Feishu folders and view complete recursive file trees with metadata
- Sync log viewer: Built-in log modal showing upload, skip, delete, download, and error events
- User OAuth: Personal cloud space access without IP whitelist
- Proxy support (optional): Reverse proxy for restricted networks
- Chunked upload: Large files uploaded in 4MB chunks, no size limit
- Rate limiting & retry: Built-in 5 QPS rate limiter with configurable retry attempts
- Concurrency control: Configurable max concurrent uploads (1–10)
Important: This plugin performs one-way sync (Obsidian → Feishu). If you modify files directly in Feishu and then sync from Obsidian, the cloud changes will be overwritten by the local version. Use "Download from Feishu" to pull cloud changes to local first.
Installation
Requirements
- Obsidian 0.15.0+ (Desktop only)
- A Feishu enterprise account
Steps
- Copy the plugin folder to
.obsidian/plugins/in your vault - Restart Obsidian
- Enable "FeiSync" in Community Plugins settings
Quick Start
1. Create a Feishu App
- Go to Feishu Open Platform and log in
- Click "Create Enterprise App"
- Note down App ID and App Secret from "Credentials & Basic Info"
2. Configure App Permissions
Add these permissions in "Permission Management" and then publish a new version:
| Permission | Identifier | Purpose |
|---|---|---|
| Cloud Space | drive:drive | File and folder CRUD operations |
| Export Documents (Readonly) | drive:export:readonly | Export/download online documents |
| Download File | drive:file:download | Download cloud file content |
| Online Document | docx:document | Access online documents |
| Import Document | docs:document:import | Import Markdown as Feishu docs |
| Export Document | docs:document:export | Export Feishu docs to other formats |
3. Configure Web App (OAuth)
- Go to app → "App Features" → "Web App"
- Add a web app with:
- Desktop Homepage:
https://localhost - Redirect URL:
http://localhost:9527/callback
- Desktop Homepage:
4. Configure Plugin
- Open Settings → FeiSync
- Enter App ID and App Secret
- Add folder mappings:
- Auto mode: Automatically create/manage folders under a root Feishu folder
- Custom mode: Specify an exact Feishu folder token
- Click "Start Authorization" and complete OAuth in your browser
- (Optional) Enable auto sync, scheduled sync, or delete sync
5. Sync
- Click the cloud-upload ribbon icon for a menu with sync/download/settings
- Use the Command Palette (
Ctrl+P/Cmd+P):FeiSync: Sync nowFeiSync: Download from feishuFeiSync: View sync log
Folder Mapping Modes
| Mode | Behavior | Use Case |
|---|---|---|
| Auto | Plugin automatically creates a subfolder under the configured Feishu root folder. The folder token is managed internally. | Simple setup, one-click sync |
| Custom | You specify the exact Feishu folder token. Use the built-in Browse Feishu Folder button to navigate and select. | Precise control over cloud location |
Ignore Rules
Create .feisync-ignore.md in your vault root. Syntax is compatible with .gitignore:
# Ignore directories
attachments/
node_modules/
# Ignore by extension
*.log
*.tmp
# Ignore anywhere in the tree
**/.DS_Store
# Un-ignore (exception)
!important.md
Changes to this file are picked up automatically on the next sync.
Plugin Settings
| Setting | Default | Description |
|---|---|---|
| App ID | — | Feishu app identifier |
| App secret | — | Feishu app secret |
| Sync folder mappings | — | One or more local→remote folder pairs |
| Auto sync on change | Off | Watch local files and sync after a debounce |
| Debounce interval | 5s | Delay after file change before auto-sync |
| Scheduled sync | Off | Auto-sync at fixed time intervals |
| Sync interval | 30min | Interval for scheduled sync |
| Delete sync | On | Remove cloud files when local files are deleted |
| Max concurrent uploads | 3 | Parallel upload limit (1–10) |
| Max retry attempts | 3 | API call retry attempts on failure |
| Proxy URL | — | Optional reverse proxy for open.feishu.cn |
Proxy Server (Optional)
Only needed if you cannot directly access open.feishu.cn.
Nginx Config
server {
listen 8080;
server_name _;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
location / {
if ($request_method = 'OPTIONS') { return 204; }
resolver 8.8.8.8 ipv6=off valid=300s;
resolver_timeout 5s;
proxy_pass https://open.feishu.cn/;
proxy_http_version 1.1;
proxy_set_header Host open.feishu.cn;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_ssl_server_name on;
proxy_connect_timeout 30s;
}
}
Architecture
feisync/
├── main.ts # Plugin entry, lifecycle, commands, coordination
├── settings.ts # Settings UI, config management, sync log modal
├── feishuAuth.ts # OAuth & token management (tenant + user tokens)
├── feishuApi.ts # Feishu Drive API wrapper
│ # - Folder/file CRUD
│ # - Upload (full + chunked)
│ # - Download, export, import
│ # - Rate limiting (5 QPS)
│ # - Retry logic with exponential backoff
├── syncEngine.ts # Sync engine
│ # - Incremental sync (SHA-256 hash-based)
│ # - Concurrent upload pool
│ # - Delete sync & rename handling
│ # - Multi-folder support
│ # - Download from Feishu with conflict check
├── fileWatcher.ts # Local file monitoring
│ # - Create/modify/delete/rename events
│ # - Debounced sync trigger
├── feishuFolderBrowser.ts # Interactive folder browser + recursive file tree modal
├── syncFolderConfig.ts # Multi-folder config model & validation
├── ignoreFilter.ts # .feisync-ignore.md parser (gitignore-compatible)
├── fileTypeUtils.ts # File type detection for Feishu API
├── logger.ts # Unified logging with namespace support
├── manifest.json # Plugin metadata
├── styles.css # Plugin UI styles
├── esbuild.config.js # Build configuration
└── package.json
Feishu APIs Used
Authentication
| API | Method | Purpose |
|---|---|---|
/open-apis/auth/v3/tenant_access_token/internal | POST | App-level access token |
/open-apis/auth/v3/app_access_token/internal | POST | App token for OAuth |
/open-apis/authen/v1/authorize | GET | OAuth authorization page |
/open-apis/authen/v2/oauth/token | POST | Exchange code for user token |
/open-apis/authen/v1/user_info | GET | Get authorized user info |
/open-apis/authen/v1/oidc/access_token | POST | Refresh user access token |
File & Folder Operations
| API | Method | Purpose |
|---|---|---|
/open-apis/drive/v1/files | GET | List folder contents |
/open-apis/drive/v1/files/create_folder | POST | Create folder |
/open-apis/drive/v1/files/{token} | DELETE | Delete file/folder |
/open-apis/drive/v1/files/upload_all | POST | Upload file (≤20MB) |
/open-apis/drive/v1/files/upload_prepare | POST | Chunked upload initialization |
/open-apis/drive/v1/files/upload_block | POST | Upload chunk (4MB) |
/open-apis/drive/v1/files/upload_finish | POST | Complete chunked upload |
/open-apis/drive/v1/files/{token}/download | GET | Download cloud file |
/open-apis/drive/v1/export_tasks | POST | Create export task |
/open-apis/drive/v1/export_tasks/{token} | GET | Query export task result |
/open-apis/drive/v1/import_tasks | POST | Create import task |
/open-apis/drive/v1/import_tasks/{token} | GET | Query import task result |
/open-apis/drive/v1/media/batch_get_tmp_download_url | POST | Get batch download URLs |
/open-apis/drive/v1/metas/batch_query | POST | Batch query file metadata |
Commands
| Command | ID | Action |
|---|---|---|
| Sync now | feisync:sync | Trigger one-way upload sync |
| Download from feishu | feisync:download | Pull cloud files to local |
| View sync log | feisync:log | Open settings and view sync history |
Ribbon icon (cloud-upload) provides a quick menu with the same actions plus Open settings.
Data Safety Notes
- One-way sync overwrite risk: If a file is modified in Feishu and then you run "Sync now" from Obsidian, the cloud version will be deleted and replaced with the local (older) version. Always use "Download from feishu" first if you edited files in the cloud.
- Delete sync: When enabled, deleting a local file will also delete its cloud counterpart. This can be disabled in settings.
- Hash-based detection: The plugin uses SHA-256 hashes to detect changes. Files with identical content will be skipped even if their modification times differ.
Development
Setup
npm install
npm run build # Production build (generates main.js)
npm run dev # Watch mode — rebuilds on file changes
Code Quality Review (ESLint)
This plugin uses eslint-plugin-obsidianmd to enforce Obsidian-specific best practices and catch common mistakes.
Installation
Run this once per project to install the required development dependencies:
npm install --save-dev eslint eslint-plugin-obsidianmd @typescript-eslint/parser
Running the Linter
npm run lint # Check for issues
npx eslint . --fix # Automatically fix fixable issues
Configuration
The rules are configured in eslint.config.mjs at the project root. The config is already set up — you just need to install the packages above. Key settings:
- TypeScript parsing:
@typescript-eslint/parseris used withtsconfig.jsonto enable typed rule checks - Ignores:
node_modules/,main.js,dist/,eslint.config.mjs,esbuild.config.js, and all*.jsonfiles are excluded - Recommended rules: Uses
obsidianmdrecommended rule set
Key ESLint Rules Explained
| Rule | What it catches | Fix |
|---|---|---|
prefer-active-window-timers | setTimeout instead of activeWindow.setTimeout — breaks on mobile | --fix auto-fixes |
no-deprecated | Uses a deprecated Obsidian API | Manual — switch to replacement API |
no-unsupported-api | Uses an API not available in Obsidian | Manual — remove or replace |
ui/sentence-case | UI text not in sentence case (e.g., "APP ID" → "app ID") | Manual — rewrite text |
no-static-styles-assignment | Sets .style.borderTop = ... directly in JS | Move styles to CSS classes |
prefer-instanceof | instanceof Array instead of Array.isArray() | --fix auto-fixes |
no-explicit-any | Explicit any type used — use unknown instead | Manual |
no-unsafe-assignment | Assigning an any value to a typed variable | Add type assertion |
no-unsafe-return | Returning an any value from a typed function | Add type assertion |
no-unnecessary-type-assertion | Assertion doesn't change type (e.g., x as T where x is already T) | Remove assertion or use as unknown as T |
Tip: If
--fixremoves a type assertion you need, convert it toas unknown as T(double assertion). This pattern is not removed by--fix.
Re-checking After Manual Edits
If you manually edit files after --fix, always run lint again to check for regressions:
npm run lint
License
MIT
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.