feat: Initial commit
This commit is contained in:
parent
ee04e2f81f
commit
e7c471b505
|
@ -1,22 +1,23 @@
|
|||
# vscode
|
||||
.vscode
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
# Build files
|
||||
main.js
|
||||
*.js.map
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Obsidian
|
||||
.obsidian/
|
||||
|
||||
# Encrypted settings
|
||||
settings.encrypted
|
||||
|
||||
# Legacy settings
|
||||
data.json
|
3
.npmrc
3
.npmrc
|
@ -1 +1,2 @@
|
|||
tag-version-prefix=""
|
||||
@types:registry=https://registry.npmjs.org/
|
||||
registry=https://registry.npmjs.org/
|
203
README.md
203
README.md
|
@ -1,94 +1,157 @@
|
|||
# Obsidian Sample Plugin
|
||||
# Obsidian Notion 同步插件
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
一个用于将 Obsidian 笔记同步到 Notion 数据库的插件。
|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
## 功能特点
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
- 一键同步笔记到 Notion
|
||||
- 支持多种 Markdown 元素
|
||||
- 右键菜单集成
|
||||
- 命令面板支持
|
||||
- 安全的令牌存储
|
||||
- 可配置的同步设置
|
||||
|
||||
## First time developing plugins?
|
||||
## 设置指南
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
### 1. 创建 Notion Integration
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
1. 访问 [Notion Integrations](https://www.notion.so/my-integrations)
|
||||
2. 点击"New integration"
|
||||
3. 输入集成名称(如"Obsidian Sync")
|
||||
4. 选择数据库所在的工作区
|
||||
5. 设置权限(至少需要读写内容权限)
|
||||
6. 保存并复制 Integration Token
|
||||
|
||||
## Releasing new releases
|
||||
### 2. 准备 Notion Database
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
1. 在 Notion 中创建新数据库(或使用现有数据库)
|
||||
2. 数据库必须包含"Name"属性(title 类型)
|
||||
3. 获取 Database ID:
|
||||
- 以全页面视图打开数据库
|
||||
- URL 格式如:`https://notion.so/workspace/1234...abcd`
|
||||
- 复制最后一部分(32个字符)- 这就是 Database ID
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
### 3. 连接 Database 与 Integration
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
1. 在 Notion 中打开数据库
|
||||
2. 点击右上角的"..."
|
||||
3. 进入"Connections"
|
||||
4. 找到并添加你的 integration
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
### 4. 配置插件
|
||||
|
||||
## How to use
|
||||
1. 打开 Obsidian 设置
|
||||
2. 进入"第三方插件" → "Notion 同步"
|
||||
3. 输入 Integration Token
|
||||
4. 输入 Database ID
|
||||
5. 根据需要配置其他设置
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
## 使用方法
|
||||
|
||||
## Manually installing the plugin
|
||||
### 基本同步
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
1. 打开要同步的笔记
|
||||
2. 使用以下方法之一:
|
||||
- 在文件菜单中点击"同步到 Notion"(右键)
|
||||
- 使用命令面板(Ctrl/Cmd + P)搜索"同步到 Notion"
|
||||
|
||||
## Improve code quality with eslint (optional)
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- To use eslint with this project, make sure to install eslint from terminal:
|
||||
- `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command:
|
||||
- `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||
- `eslint .\src\`
|
||||
### 支持的元素
|
||||
|
||||
## Funding URL
|
||||
- 标题(H1-H3)
|
||||
- 段落
|
||||
- 无序列表(支持多级)
|
||||
- 有序列表
|
||||
- 基本文本格式
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
### 多级列表处理
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
插件支持三种方式处理多级列表:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```
|
||||
1. **保持原有层级**(默认)
|
||||
```markdown
|
||||
- 一级项目
|
||||
- 二级项目
|
||||
- 三级项目
|
||||
```
|
||||
同步到 Notion 后保持原有的层级结构:
|
||||
```
|
||||
? 一级项目
|
||||
? 二级项目
|
||||
? 三级项目
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
2. **转为平级结构**
|
||||
```markdown
|
||||
- 一级项目
|
||||
- 二级项目
|
||||
- 三级项目
|
||||
```
|
||||
同步到 Notion 后转换为:
|
||||
```
|
||||
? 一级项目
|
||||
? 二级项目
|
||||
? 三级项目
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
```
|
||||
3. **忽略子级内容**
|
||||
```markdown
|
||||
- 一级项目
|
||||
- 二级项目(会被忽略)
|
||||
- 三级项目(会被忽略)
|
||||
- 另一个一级项目
|
||||
```
|
||||
同步到 Notion 后只保留顶级项目:
|
||||
```
|
||||
? 一级项目
|
||||
? 另一个一级项目
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
选择合适的处理方式:
|
||||
- 如果你的 Notion 数据库需要保持文档的完整层级结构,选择"保持原有层级"
|
||||
- 如果你希望简化列表结构便于在 Notion 中查看,选择"转为平级结构"
|
||||
- 如果你只关注顶层信息,选择"忽略子级内容"
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
### 设置说明
|
||||
|
||||
- **Integration Token**:Notion 集成令牌(安全加密存储)
|
||||
- **Database ID**:目标 Notion 数据库标识符
|
||||
- **列表处理方式**:控制多级列表的同步行为
|
||||
- 保持原有层级:完整保留列表的层级关系
|
||||
- 转为平级结构:将多级列表转换为同级项目
|
||||
- 忽略子级内容:仅同步顶层列表项
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **认证失败**
|
||||
- 验证 Integration Token 是否正确
|
||||
- 检查 Token 是否具有适当权限
|
||||
|
||||
2. **找不到数据库**
|
||||
- 验证 Database ID 是否正确
|
||||
- 确保 Integration 已被授权访问数据库
|
||||
|
||||
3. **同步失败**
|
||||
- 检查网络连接
|
||||
- 确认文件大小在限制内(500KB)
|
||||
- 确保内容格式受支持
|
||||
|
||||
## 安全性
|
||||
|
||||
- Integration Token 采用加密存储
|
||||
- 不向第三方发送数据
|
||||
- 所有通信直接与 Notion API 进行
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT 许可证 - 详见 [LICENSE](LICENSE)
|
||||
|
||||
## 支持
|
||||
|
||||
- [报告问题](https://github.com/e6g2cyvryi/obsidian-notion-sync/issues)
|
||||
- [功能建议](https://github.com/e6g2cyvryi/obsidian-notion-sync/issues)
|
||||
|
||||
## 技术支持
|
||||
|
||||
基于 [Obsidian Plugin API](https://github.com/obsidianmd/obsidian-api) 和 [Notion API](https://developers.notion.com/) 构建
|
|
@ -0,0 +1,15 @@
|
|||
# 测试清理步骤
|
||||
|
||||
1. Obsidian 端
|
||||
- 禁用插件
|
||||
- 删除测试文档
|
||||
- 清除插件设置
|
||||
|
||||
2. Notion 端
|
||||
- 清除测试数据库中的同步记录
|
||||
- 保留数据库结构
|
||||
|
||||
3. 本地文件
|
||||
- 删除 main.js
|
||||
- 删除 *.js.map
|
||||
- 删除 data.json
|
|
@ -0,0 +1,24 @@
|
|||
# 调试技巧
|
||||
|
||||
1. 实时日志查看
|
||||
- 保持开发者工具打开
|
||||
- 在 Console 面板查看日志
|
||||
- 使用 console.log() 添加临时调试信息
|
||||
|
||||
2. 热重载
|
||||
- 修改代码后保存
|
||||
- npm run dev 会自动重新构建
|
||||
- 在 Obsidian 中禁用并重新启用插件
|
||||
|
||||
3. 常见问题排查
|
||||
- 如果插件没有出现在列表中,检查 manifest.json
|
||||
- 如果无法启用插件,检查 main.js 是否生成
|
||||
- 如果同步失败,检查 Notion API 响应
|
||||
|
||||
4. 文件位置验证
|
||||
确保以下文件存在且位置正确:
|
||||
.obsidian/plugins/obsidian-notion-sync/
|
||||
├── manifest.json
|
||||
├── main.js
|
||||
├── styles.css (如果有)
|
||||
└── package.json
|
|
@ -5,45 +5,31 @@ import builtins from "builtin-modules";
|
|||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
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",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
esbuild.build({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ['src/main.ts'],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/closebrackets',
|
||||
'@codemirror/collab',
|
||||
'@codemirror/fold',
|
||||
'@codemirror/gutter',
|
||||
'@codemirror/history',
|
||||
'@codemirror/language',
|
||||
...builtins],
|
||||
format: 'cjs',
|
||||
target: 'es2020', // ¸üÐÂÄ¿±ê»·¾³Îª ES2020 ÒÔÖ§³Ö async generator
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : 'inline',
|
||||
treeShaking: true,
|
||||
outfile: 'main.js',
|
||||
}).catch(() => process.exit(1));
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"id": "sample-plugin",
|
||||
"name": "Sample Plugin",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Demonstrates some of the capabilities of the Obsidian API.",
|
||||
"author": "Obsidian",
|
||||
"authorUrl": "https://obsidian.md",
|
||||
"fundingUrl": "https://obsidian.md/pricing",
|
||||
"isDesktopOnly": false
|
||||
"id": "obsidian-notion-sync",
|
||||
"name": "Notion Sync",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Sync your Obsidian notes with Notion",
|
||||
"author": "Jorahil",
|
||||
"authorUrl": "https://github.com/e6g2cyvryi",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Notion 配置指南
|
||||
|
||||
## 1. 创建 Notion Integration
|
||||
1. 访问 https://www.notion.so/my-integrations
|
||||
2. 点击 "New integration"
|
||||
3. 填写名称(如 "Obsidian Sync")
|
||||
4. 选择关联的工作区
|
||||
5. 提交后获取 Integration Token(以 "secret_" 开头)
|
||||
|
||||
## 2. 创建目标数据库
|
||||
1. 在 Notion 中创建一个新页面
|
||||
2. 添加一个新的数据库(全页面)
|
||||
3. 添加以下属性:
|
||||
- Name (标题类型,默认存在)
|
||||
- Tags (多选类型)
|
||||
- LastSync (日期类型)
|
||||
|
||||
## 3. 配置数据库权限
|
||||
1. 打开数据库页面
|
||||
2. 点击右上角的 "Share" 按钮
|
||||
3. 在 "Connections" 部分添加你创建的 Integration
|
||||
|
||||
## 4. 获取数据库 ID
|
||||
1. 在浏览器中打开数据库页面
|
||||
2. URL 中形如 "https://www.notion.so/xxx/yyyyyyy?v=zzz" 的 yyyyyyy 部分就是数据库 ID
|
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
|
@ -1,24 +1,25 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
"name": "obsidian-notion-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Sync Obsidian notes with Notion",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": ["obsidian", "notion", "sync", "plugin"],
|
||||
"author": "Jorahil",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "^0.17.3",
|
||||
"obsidian": "^1.4.11",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { App, TFile, Notice, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { Client as NotionClient } from '@notionhq/client';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
app: App;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
workspace: Workspace;
|
||||
vault: Vault;
|
||||
}
|
||||
|
||||
interface Workspace {
|
||||
getActiveFile(): TFile | null;
|
||||
on(name: string, callback: (menu: Menu, file: TFile) => any): EventRef;
|
||||
}
|
||||
|
||||
interface Vault {
|
||||
read(file: TFile): Promise<string>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@notionhq/client" {
|
||||
interface NotionClientTypes {
|
||||
// 如果需要扩展 Notion 客户端类型,在这里添加
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -0,0 +1,75 @@
|
|||
import { Plugin, Menu, TFile, Notice } from 'obsidian';
|
||||
import { NotionSyncSettings } from './types';
|
||||
import { NotionSyncSettingTab, DEFAULT_SETTINGS } from './settings';
|
||||
import { NotionService } from './services/notion-service';
|
||||
import { SyncService } from './services/sync-service';
|
||||
import { DataValidator } from './utils/validator';
|
||||
import { StorageService } from './services/storage-service';
|
||||
|
||||
export default class NotionSyncPlugin extends Plugin {
|
||||
settings: NotionSyncSettings;
|
||||
notionService: NotionService;
|
||||
syncService: SyncService;
|
||||
private storageService: StorageService;
|
||||
|
||||
async onload() {
|
||||
try {
|
||||
this.storageService = new StorageService(this.app.vault.adapter);
|
||||
await this.loadSettings();
|
||||
|
||||
this.notionService = new NotionService(this.settings.notionToken);
|
||||
this.syncService = new SyncService(this.app, this.notionService, this.settings);
|
||||
|
||||
// 添加设置标签页
|
||||
this.addSettingTab(new NotionSyncSettingTab(this.app, this));
|
||||
|
||||
// 添加同步命令
|
||||
this.addCommand({
|
||||
id: 'sync-to-notion',
|
||||
name: '同步当前文件到 Notion',
|
||||
callback: async () => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (file) {
|
||||
await this.syncService.syncFile(file);
|
||||
} else {
|
||||
new Notice('请先打开要同步的文件');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加文件菜单项
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-menu', (menu: Menu, file: TFile) => {
|
||||
if (file && file instanceof TFile) {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle('同步到 Notion')
|
||||
.setIcon('upload-cloud')
|
||||
.onClick(async () => {
|
||||
await this.syncService.syncFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
new Notice('插件加载失败');
|
||||
console.error('Plugin load error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
const savedSettings = await this.storageService.loadSettings();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedSettings);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
if (this.settings) {
|
||||
await this.storageService.saveSettings(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
async onunload() {
|
||||
// Obsidian 会自动处理插件的清理工作
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
import { Client } from '@notionhq/client';
|
||||
import { NotionSyncSettings } from '../types';
|
||||
import { Notice, requestUrl, RequestUrlParam } from 'obsidian';
|
||||
|
||||
export class NotionService {
|
||||
private client: Client;
|
||||
private readonly MAX_CONTENT_SIZE = 1024 * 1024; // 1MB
|
||||
private readonly token: string;
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token;
|
||||
this.client = new Client({
|
||||
auth: token,
|
||||
notionVersion: '2022-06-28',
|
||||
fetch: this.customFetch.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
private async customFetch(url: string, options: any): Promise<Response> {
|
||||
try {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
'mode': 'no-cors',
|
||||
'credentials': 'omit'
|
||||
};
|
||||
|
||||
const params: RequestUrlParam = {
|
||||
url,
|
||||
method: options.method,
|
||||
headers,
|
||||
body: options.body
|
||||
};
|
||||
|
||||
const response = await requestUrl(params);
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication failed: Invalid token or insufficient permissions');
|
||||
}
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`API error (${response.status}): ${response.text}`);
|
||||
}
|
||||
|
||||
return new Response(response.text, {
|
||||
status: response.status,
|
||||
headers: new Headers(response.headers)
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('CORS')) {
|
||||
throw new Error('CORS error: Unable to access Notion API. Try refreshing the token.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createOrUpdatePage(databaseId: string, content: any, settings: NotionSyncSettings, fileName: string): Promise<void> {
|
||||
try {
|
||||
const contentSize = new TextEncoder().encode(JSON.stringify(content)).length;
|
||||
if (contentSize > this.MAX_CONTENT_SIZE) {
|
||||
throw new Error('Content size exceeds maximum limit');
|
||||
}
|
||||
|
||||
const blocks = this.formatBlocks(content);
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
parent: {
|
||||
type: 'database_id',
|
||||
database_id: databaseId
|
||||
},
|
||||
properties: {
|
||||
title: {
|
||||
type: 'title',
|
||||
title: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: fileName
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
children: blocks.map(block => ({
|
||||
object: 'block',
|
||||
...block
|
||||
}))
|
||||
};
|
||||
|
||||
const response = await this.client.pages.create(requestData as any);
|
||||
|
||||
if (!response || !response.id) {
|
||||
throw new Error('Failed to create page in Notion');
|
||||
}
|
||||
|
||||
} catch (apiError: any) {
|
||||
throw new Error(`Failed to create page: ${apiError.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error('Failed to sync with Notion');
|
||||
}
|
||||
}
|
||||
|
||||
private formatBlocks(blocks: any[]): any[] {
|
||||
return blocks.map(block => {
|
||||
if (typeof block === 'object' && block.type) {
|
||||
const formattedBlock: any = {
|
||||
type: block.type
|
||||
};
|
||||
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
formattedBlock[block.type] = {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: block.paragraph?.rich_text?.[0]?.text?.content || ''
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
break;
|
||||
case 'heading_1':
|
||||
case 'heading_2':
|
||||
case 'heading_3':
|
||||
formattedBlock[block.type] = {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: block[block.type]?.rich_text?.[0]?.text?.content || ''
|
||||
}
|
||||
}
|
||||
],
|
||||
color: 'default'
|
||||
};
|
||||
break;
|
||||
case 'bulleted_list_item':
|
||||
case 'numbered_list_item':
|
||||
formattedBlock[block.type] = {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: block[block.type]?.rich_text?.[0]?.text?.content || ''
|
||||
}
|
||||
}
|
||||
],
|
||||
color: 'default'
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.warn('Unsupported block type:', block.type);
|
||||
return null;
|
||||
}
|
||||
|
||||
return formattedBlock;
|
||||
}
|
||||
return null;
|
||||
}).filter(block => block !== null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import { DataAdapter, Notice } from 'obsidian';
|
||||
import { NotionSyncSettings } from '../types';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export class StorageService {
|
||||
private adapter: DataAdapter;
|
||||
private readonly SETTINGS_PATH = '.obsidian/plugins/obsidian-notion-sync/settings.encrypted';
|
||||
private readonly LEGACY_SETTINGS_PATH = '.obsidian/plugins/obsidian-notion-sync/data.json';
|
||||
private readonly ALGORITHM = 'aes-256-cbc';
|
||||
private readonly KEY = crypto.scryptSync('obsidian-notion-sync', 'salt', 32);
|
||||
private readonly IV_LENGTH = 16;
|
||||
|
||||
constructor(adapter: DataAdapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
private encrypt(text: string): string {
|
||||
if (!text) return '';
|
||||
try {
|
||||
const iv = crypto.randomBytes(this.IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(this.ALGORITHM, this.KEY, iv);
|
||||
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
return iv.toString('hex') + ':' + encrypted;
|
||||
} catch (error) {
|
||||
console.error('Encryption failed:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private decrypt(encryptedData: string): string {
|
||||
if (!encryptedData) return '';
|
||||
try {
|
||||
if (!encryptedData.includes(':')) {
|
||||
// 未加密的数据,直接返回
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
const [ivHex, encryptedText] = encryptedData.split(':');
|
||||
if (!ivHex || !encryptedText) {
|
||||
console.warn('Invalid encrypted data format');
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const decipher = crypto.createDecipheriv(this.ALGORITHM, this.KEY, iv);
|
||||
|
||||
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
console.error('Decryption failed:', error);
|
||||
return encryptedData; // 如果解密失败,返回原始数据
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings(settings: NotionSyncSettings): Promise<void> {
|
||||
const encryptedSettings = {
|
||||
...settings,
|
||||
notionToken: this.encrypt(settings.notionToken),
|
||||
notionDatabaseId: this.encrypt(settings.databaseId)
|
||||
};
|
||||
|
||||
await this.adapter.write(
|
||||
this.SETTINGS_PATH,
|
||||
JSON.stringify(encryptedSettings)
|
||||
);
|
||||
|
||||
try {
|
||||
if (await this.adapter.exists(this.LEGACY_SETTINGS_PATH)) {
|
||||
await this.adapter.remove(this.LEGACY_SETTINGS_PATH);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove legacy settings file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<NotionSyncSettings | null> {
|
||||
try {
|
||||
if (await this.adapter.exists(this.SETTINGS_PATH)) {
|
||||
const data = await this.adapter.read(this.SETTINGS_PATH);
|
||||
const encryptedSettings = JSON.parse(data);
|
||||
const decryptedToken = this.decrypt(encryptedSettings.notionToken);
|
||||
|
||||
return {
|
||||
...encryptedSettings,
|
||||
notionToken: decryptedToken,
|
||||
notionDatabaseId: this.decrypt(encryptedSettings.notionDatabaseId)
|
||||
};
|
||||
}
|
||||
|
||||
if (await this.adapter.exists(this.LEGACY_SETTINGS_PATH)) {
|
||||
const legacyData = await this.adapter.read(this.LEGACY_SETTINGS_PATH);
|
||||
const legacySettings = JSON.parse(legacyData);
|
||||
await this.saveSettings(legacySettings);
|
||||
return legacySettings;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async clearSettings(): Promise<void> {
|
||||
try {
|
||||
if (await this.adapter.exists(this.SETTINGS_PATH)) {
|
||||
await this.adapter.remove(this.SETTINGS_PATH);
|
||||
}
|
||||
if (await this.adapter.exists(this.LEGACY_SETTINGS_PATH)) {
|
||||
await this.adapter.remove(this.LEGACY_SETTINGS_PATH);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { App, TFile, Notice } from 'obsidian';
|
||||
import { NotionService } from './notion-service';
|
||||
import { MarkdownConverter } from '../utils/converter';
|
||||
import { NotionSyncSettings } from '../types';
|
||||
import { DataValidator } from '../utils/validator';
|
||||
|
||||
export class SyncService {
|
||||
private app: App;
|
||||
private notionService: NotionService;
|
||||
private settings: NotionSyncSettings;
|
||||
private readonly MAX_FILE_SIZE = 500 * 1024; // 500KB
|
||||
|
||||
constructor(app: App, notionService: NotionService, settings: NotionSyncSettings) {
|
||||
this.app = app;
|
||||
this.notionService = notionService;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
private validateSettings(): boolean {
|
||||
if (!this.settings) {
|
||||
new Notice('Configuration required');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!this.settings.databaseId || this.settings.databaseId.length < 32) {
|
||||
new Notice('Invalid database ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async syncFile(file: TFile): Promise<void> {
|
||||
if (!this.validateSettings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
new Notice('Invalid file');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查文件大小
|
||||
if (file.stat.size > this.MAX_FILE_SIZE) {
|
||||
new Notice('File size exceeds maximum limit');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
if (!DataValidator.validateContent(content)) {
|
||||
new Notice('Invalid file content');
|
||||
return;
|
||||
}
|
||||
|
||||
const blocks = MarkdownConverter.convertToNotionBlocks(content);
|
||||
|
||||
await this.notionService.createOrUpdatePage(
|
||||
this.settings.databaseId,
|
||||
blocks,
|
||||
this.settings,
|
||||
file.basename
|
||||
);
|
||||
|
||||
new Notice('Sync completed');
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
new Notice('Sync failed. Check console for details.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import NotionSyncPlugin from './main';
|
||||
import { NotionSyncSettings } from './types';
|
||||
|
||||
export const DEFAULT_SETTINGS: NotionSyncSettings = {
|
||||
notionToken: '',
|
||||
databaseId: '',
|
||||
handleDeepLists: 'keep',
|
||||
autoSync: false,
|
||||
syncOnSave: false
|
||||
};
|
||||
|
||||
export class NotionSyncSettingTab extends PluginSettingTab {
|
||||
plugin: NotionSyncPlugin;
|
||||
|
||||
constructor(app: App, plugin: NotionSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: '同步设置' });
|
||||
|
||||
// Token 设置
|
||||
new Setting(containerEl)
|
||||
.setName('Integration Token')
|
||||
.setDesc(createFragment(fragment => {
|
||||
fragment.appendText('用于连接 Notion 的授权凭证,');
|
||||
fragment.createEl('a', {
|
||||
text: '点击这里创建',
|
||||
href: 'https://www.notion.so/my-integrations'
|
||||
});
|
||||
fragment.appendText('。创建后请确保赋予读写权限。');
|
||||
}))
|
||||
.addText(text => text
|
||||
.setPlaceholder('secret_...')
|
||||
.setValue(this.maskToken(this.plugin.settings.notionToken))
|
||||
.onChange(async (value) => {
|
||||
if (value && !value.startsWith('••••')) {
|
||||
this.plugin.settings.notionToken = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
// Database ID 设置
|
||||
new Setting(containerEl)
|
||||
.setName('Database ID')
|
||||
.setDesc(createFragment(fragment => {
|
||||
fragment.appendText('目标数据库的唯一标识,可在数据库页面的网址中找到:');
|
||||
fragment.createEl('code', { text: 'notion.so/workspace/[database-id]' });
|
||||
fragment.createEl('br');
|
||||
fragment.appendText('请先确保数据库已与你的 Integration 共享。');
|
||||
}))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter Database ID')
|
||||
.setValue(this.plugin.settings.databaseId)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.databaseId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 高级设置
|
||||
containerEl.createEl('h3', { text: '高级设置' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('列表处理方式')
|
||||
.setDesc(createFragment(fragment => {
|
||||
fragment.appendText('设置如何处理多层级的列表结构。');
|
||||
fragment.createEl('br');
|
||||
fragment.createEl('a', {
|
||||
text: '查看详细说明和示例',
|
||||
href: 'https://github.com/e6g2cyvryi/obsidian-notion-sync#多级列表处理'
|
||||
});
|
||||
}))
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('keep', '保持原有层级')
|
||||
.addOption('convert', '转为平级结构')
|
||||
.addOption('skip', '忽略子级内容')
|
||||
.setValue(this.plugin.settings.handleDeepLists)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.handleDeepLists = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
private maskToken(token: string): string {
|
||||
if (!token) return '';
|
||||
return '••••' + token.slice(-4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface NotionSyncSettings {
|
||||
notionToken: string;
|
||||
databaseId: string;
|
||||
handleDeepLists: 'convert' | 'skip' | 'keep';
|
||||
autoSync: boolean;
|
||||
syncOnSave: boolean;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { Plugin } from 'obsidian';
|
||||
|
||||
export interface NotionSyncSettings {
|
||||
notionToken: string;
|
||||
notionDatabaseId: string;
|
||||
handleDeepLists: 'convert' | 'skip' | 'keep';
|
||||
}
|
||||
|
||||
export interface NotionSyncPlugin extends Plugin {
|
||||
settings: NotionSyncSettings;
|
||||
loadSettings(): Promise<void>;
|
||||
saveSettings(): Promise<void>;
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import { DataValidator } from './validator';
|
||||
|
||||
export class MarkdownConverter {
|
||||
/**
|
||||
* Convert Markdown to Notion blocks
|
||||
*/
|
||||
static convertToNotionBlocks(markdown: string, handleDeepLists: 'convert' | 'skip' | 'keep' = 'keep'): any[] {
|
||||
const blocks: any[] = [];
|
||||
const listStack: any[] = [];
|
||||
let previousLevel = 0;
|
||||
let inList = false; // 添加标志来跟踪是否在列表中
|
||||
|
||||
const lines = markdown.split('\n');
|
||||
for (const line of lines) {
|
||||
const listMatch = line.match(/^(\s*|\t*)([-*+])\s(.+)/);
|
||||
if (listMatch) {
|
||||
inList = true;
|
||||
const [, indent, , content] = listMatch;
|
||||
const level = indent.replace(/\t/g, ' ').length / 4;
|
||||
|
||||
// 创建列表项
|
||||
const listItem = {
|
||||
type: "bulleted_list_item",
|
||||
bulleted_list_item: {
|
||||
rich_text: [{
|
||||
type: "text",
|
||||
text: {
|
||||
content: content.trim()
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
if (level === 0) {
|
||||
blocks.push(listItem);
|
||||
listStack.length = 0;
|
||||
listStack.push(listItem);
|
||||
} else {
|
||||
// 子列表项处理
|
||||
if (level > previousLevel) {
|
||||
const parent = listStack[listStack.length - 1];
|
||||
if (!parent.bulleted_list_item.children) {
|
||||
parent.bulleted_list_item.children = [];
|
||||
}
|
||||
parent.bulleted_list_item.children.push(listItem);
|
||||
listStack.push(listItem);
|
||||
} else if (level === previousLevel && listStack.length > 1) {
|
||||
const parent = listStack[listStack.length - 2];
|
||||
if (!parent.bulleted_list_item.children) {
|
||||
parent.bulleted_list_item.children = [];
|
||||
}
|
||||
parent.bulleted_list_item.children.push(listItem);
|
||||
listStack[listStack.length - 1] = listItem;
|
||||
} else {
|
||||
while (listStack.length > level) {
|
||||
listStack.pop();
|
||||
}
|
||||
const parent = listStack[listStack.length - 1];
|
||||
if (!parent.bulleted_list_item.children) {
|
||||
parent.bulleted_list_item.children = [];
|
||||
}
|
||||
parent.bulleted_list_item.children.push(listItem);
|
||||
listStack.push(listItem);
|
||||
}
|
||||
}
|
||||
previousLevel = level;
|
||||
} else {
|
||||
// 非列表内容
|
||||
if (inList) {
|
||||
// 如果之前在列表中,重置列表相关状态
|
||||
listStack.length = 0;
|
||||
previousLevel = 0;
|
||||
inList = false;
|
||||
}
|
||||
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine) { // 只处理非空行
|
||||
// 处理标题
|
||||
const headingMatch = line.match(/^(#{1,6})\s(.+)/);
|
||||
if (headingMatch) {
|
||||
const level = headingMatch[1].length;
|
||||
const content = headingMatch[2];
|
||||
blocks.push({
|
||||
type: `heading_${level}`,
|
||||
[`heading_${level}`]: {
|
||||
rich_text: [{
|
||||
type: "text",
|
||||
text: {
|
||||
content: content.trim()
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 处理普通段落
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: [{
|
||||
type: "text",
|
||||
text: {
|
||||
content: trimmedLine
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Notice } from 'obsidian';
|
||||
|
||||
export class DataValidator {
|
||||
static validateToken(token: string): boolean {
|
||||
return token.startsWith('secret_') && token.length > 50;
|
||||
}
|
||||
|
||||
static validateDatabaseId(id: string): boolean {
|
||||
return /^[a-zA-Z0-9-]{32,}$/.test(id);
|
||||
}
|
||||
|
||||
static validateContent(content: string): boolean {
|
||||
return Boolean(content) && content.length > 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
# Obsidian 测试步骤
|
||||
|
||||
1. 启动 Obsidian
|
||||
- 打开测试 vault(H:\Obsidian\helper-test)
|
||||
- 进入设置 -> 第三方插件
|
||||
- 关闭安全模式
|
||||
- 刷新插件列表
|
||||
- 确认 "Obsidian Notion Sync" 出现在插件列表中
|
||||
- 启用插件
|
||||
|
||||
2. 配置插件
|
||||
- 打开插件设置
|
||||
- 填入 Notion Token
|
||||
- 填入数据库 ID
|
||||
- 选择深层列表处理方式(建议先选择 "convert")
|
||||
|
||||
3. 功能测试
|
||||
- 打开 test-cases/basic.md
|
||||
- 使用命令面板(Ctrl+P)
|
||||
- 输入 "同步到 Notion"
|
||||
- 观察同步结果和提示信息
|
||||
|
||||
4. 错误测试
|
||||
- 尝试同步空文档
|
||||
- 尝试使用错误的 Token
|
||||
- 尝试使用错误的数据库 ID
|
||||
|
||||
5. 日志检查
|
||||
- 打开开发者工具(Ctrl+Shift+I)
|
||||
- 检查 Console 面板中的错误信息
|
|
@ -0,0 +1,22 @@
|
|||
# 常见问题解决
|
||||
|
||||
## 1. 同步失败
|
||||
- 检查 Notion Token 格式
|
||||
- 确认数据库 ID 正确
|
||||
- 验证数据库权限设置
|
||||
- 检查网络连接
|
||||
|
||||
## 2. 格式问题
|
||||
- 确认深层列表处理方式设置
|
||||
- 检查 Markdown 语法是否正确
|
||||
- 验证 Notion 块转换结果
|
||||
|
||||
## 3. 权限问题
|
||||
- 确认 Integration 权限范围
|
||||
- 检查数据库访问权限
|
||||
- 验证 Token 有效性
|
||||
|
||||
## 4. 性能问题
|
||||
- 检查文件大小
|
||||
- 验证网络状态
|
||||
- 观察同步时间
|
|
@ -11,6 +11,7 @@
|
|||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"types": ["node", "obsidian", "@notionhq/client"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
|
@ -19,6 +20,6 @@
|
|||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
# 验证步骤
|
||||
|
||||
1. 插件安装验证
|
||||
- 启动 Obsidian
|
||||
- 检查插件是否出现在已安装插件列表中
|
||||
- 检查是否有错误提示
|
||||
|
||||
2. 配置验证
|
||||
- 打开插件设置
|
||||
- 填入 Notion Token
|
||||
- 填入数据库 ID
|
||||
- 选择深层列表处理方式
|
||||
|
||||
3. 功能验证
|
||||
- 打开测试文档
|
||||
- 使用命令面板执行同步命令
|
||||
- 检查 Notion 数据库中是否出现同步的内容
|
||||
- 验证深层列表是否按照设置正确处理
|
||||
|
||||
4. 错误处理验证
|
||||
- 尝试使用错误的 Token
|
||||
- 尝试同步空文档
|
||||
- 尝试在未配置的情况下同步
|
||||
- 检查错误提示是否正确
|
||||
|
||||
5. 批量同步验证
|
||||
- 选择多个文件
|
||||
- 执行批量同步
|
||||
- 检查成功率提示
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
Loading…
Reference in New Issue