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
											
										
									
								
							
							
								
								
									
										47
									
								
								package.json
								
								
								
								
							
							
						
						
									
										47
									
								
								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. 批量同步验证
 | 
			
		||||
- 选择多个文件
 | 
			
		||||
- 执行批量同步
 | 
			
		||||
- 检查成功率提示 
 | 
			
		||||
| 
						 | 
				
			
			@ -11,4 +11,4 @@ writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
 | 
			
		|||
// update versions.json with target version and minAppVersion from manifest.json
 | 
			
		||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
 | 
			
		||||
versions[targetVersion] = minAppVersion;
 | 
			
		||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
 | 
			
		||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
{
 | 
			
		||||
	"1.0.0": "0.15.0"
 | 
			
		||||
}
 | 
			
		||||
    "1.0.0": "0.15.0"
 | 
			
		||||
} 
 | 
			
		||||
		Loading…
	
		Reference in New Issue