├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_zh_CN.md ├── asset └── action.png ├── icon.png ├── package.json ├── plugin.json ├── preview.png ├── scripts ├── .gitignore └── make_dev_link.js ├── settings.json ├── src ├── api.ts ├── constants │ └── maps.ts ├── i18n │ ├── en_US.json │ └── zh_CN.json ├── index.scss ├── index.ts ├── interfaces │ └── index.d.ts ├── libs │ ├── index.d.ts │ └── setting-utils.ts ├── server │ ├── memos.ts │ └── siyuan.ts ├── types │ ├── api.d.ts │ └── index.d.ts └── utils │ └── index.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:svelte/recommended", 6 | "turbo", 7 | "prettier", 8 | ], 9 | 10 | parser: "@typescript-eslint/parser", 11 | 12 | overrides: [ 13 | { 14 | files: ["*.svelte"], 15 | parser: "svelte-eslint-parser", 16 | // Parse the script in `.svelte` as TypeScript by adding the following configuration. 17 | parserOptions: { 18 | parser: "@typescript-eslint/parser", 19 | }, 20 | }, 21 | ], 22 | 23 | plugins: ["@typescript-eslint", "prettier"], 24 | 25 | rules: { 26 | // Note: you must disable the base rule as it can report incorrect errors 27 | semi: "off", 28 | quotes: "off", 29 | "no-undef": "off", 30 | "@typescript-eslint/no-var-requires": "off", 31 | "@typescript-eslint/no-this-alias": "off", 32 | "@typescript-eslint/no-non-null-assertion": "off", 33 | "@typescript-eslint/no-unused-vars": "off", 34 | "@typescript-eslint/no-explicit-any": "off", 35 | "turbo/no-undeclared-env-vars": "off", 36 | "prettier/prettier": "error", 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release on Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | # Install Node.js 17 | - name: Install Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | registry-url: "https://registry.npmjs.org" 22 | 23 | # Install pnpm 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v2 26 | id: pnpm-install 27 | with: 28 | version: 8 29 | run_install: false 30 | 31 | # Get pnpm store directory 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 37 | 38 | # Setup pnpm cache 39 | - name: Setup pnpm cache 40 | uses: actions/cache@v3 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | # Install dependencies 48 | - name: Install dependencies 49 | run: pnpm install 50 | 51 | # Build for production, 这一步会生成一个 package.zip 52 | - name: Build for production 53 | run: pnpm build 54 | 55 | - name: Release 56 | uses: ncipollo/release-action@v1 57 | with: 58 | allowUpdates: true 59 | artifactErrorsFailBuild: true 60 | artifacts: "package.zip" 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | prerelease: true 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | pnpm-lock.yaml 5 | package.zip 6 | node_modules 7 | dev 8 | dist 9 | build 10 | tmp 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.3 4 | 5 | ### 修复 6 | 7 | 1. 修复插件设置页面空白的问题 8 | 2. 限制支持插件运行的客户端环境 9 | 10 | ## v0.2.2 11 | 12 | ### 修复 13 | 14 | 1. 修复兼容新的资源下载模式 15 | 16 | ## v0.2.1 17 | 18 | ### 新功能 19 | 20 | 1. 增加开发模式 21 | 22 | ### 优化 23 | 24 | 1. 优化标签匹配方案 25 | 26 | ## v0.2.0 27 | 28 | ### 新功能 29 | 30 | 1. 支持同步后的视频文件可直接播放 31 | 32 | ### 修复 33 | 34 | 1. 修复当关闭Memos服务器 “根据最后修改时间顺序显示” 开关后无法获取更新的问题 35 | 36 | ### 优化 37 | 38 | 1. 优化设置页面 39 | 2. 将资源下载模式默认项调整为第二种 40 | 41 | ## v0.1.9 42 | 43 | ### 修复 44 | 45 | 1. 修复无法识别两个及以上双向链接的问题 46 | 47 | ## v0.1.8 48 | 49 | ### 优化 50 | 51 | 1. 优化设置页面的提示信息 52 | 2. 去除主题路径的必填项检查 53 | 54 | ## v0.1.7 55 | 56 | ### 修复 57 | 58 | 1. 修复必填项判定出错的问题 59 | 60 | ## v0.1.6 61 | 62 | ### 新功能 63 | 64 | 1. 增加同步模式:支持将 Memos 保存到单份文档中 65 | 2. 支持识别双链符号(()) 66 | 3. 支持自定义上级标签 67 | 4. 兼容新的资源下载模式 68 | 69 | ### 优化 70 | 71 | 1. 优化格式:消除多余的空行 72 | 73 | ## v0.1.5 74 | 75 | ### 修复 76 | 77 | 1. 修复同步至 Daily Note 模式下同步失败的问题 78 | 79 | ## v0.1.4 80 | 81 | ### 新功能 82 | 83 | 1. 支持同步至笔记本或文档模式下图片布局调整 84 | 85 | ### 优化 86 | 87 | 1. 优化同步至笔记本或文档模式的文档生成排序 88 | 89 | ## v0.1.3 90 | 91 | ### 新功能 92 | 93 | 1. 支持图片布局模式调整 94 | 95 | ### 优化 96 | 97 | 1. 优化资源保存方案 98 | 99 | ## v0.1.2 100 | 101 | ### 修复 102 | 103 | 1. 修复插件在安装后无法使用的问题 104 | 2. 修复同步至 Daily Note 模式下,同步的内容变成标题 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SiYuan 思源笔记 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Memos 同步插件 3 | 4 | ### 功能介绍 5 | 6 | > 将 Memos 的数据同步到思源笔记。[<< 更新日志](https://github.com/Yimien/plugin-memos-sync/blob/main/CHANGELOG.md) 7 | 8 | 1. 支持将 Memos 中的数据同步到思源笔记,包括文本、图片、标签、引用等等。 9 | 2. 支持三种同步保存方案:Daily Notes、指定的笔记本或文档下、单份文档中。 10 | 3. 支持将 Memos 的引用保存为引用块或者嵌入块。 11 | 4. 支持识别双向链接。 12 | 5. 支持将 Memos 的标签保存在同一标签下。 13 | 14 | ### 使用说明 15 | 16 | 1. 安装插件。 17 | 2. 填写 `服务器地址` 和 `授权码` ,点击 `校验` 按钮,校验通过后,再配置其它的设置,`必填项` 一定要全部配置。 18 | 3. 点击插件图标进行同步。 19 | 20 | ### 常见问题 21 | 22 | 1. 插件在 Safari 浏览器、ios、ipad 等设备或软件上可能出现报错等异常情况无法正常使用。(暂无解决方案) 23 | 24 | ### 特别感谢 25 | 26 | 本项目使用了 [frostime](https://github.com/frostime) 大佬提供的 [plugin-sample-vite](https://github.com/frostime/plugin-sample-vite) 模板仓库,在开发时很大程度参考了 [winter60](https://github.com/winter60) 大佬的 [winter60/plugin-flomo-sync: 用于导入flomo到思源 (github.com)](https://github.com/winter60/plugin-flomo-sync) 项目,同时还要感谢各位开源代码的大佬和群内提供帮助的各位大大。 -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | 2 | # Memos 同步插件 3 | 4 | ### 功能介绍 5 | 6 | > 将 Memos 的数据同步到思源笔记。[<< 更新日志](https://github.com/Yimien/plugin-memos-sync/blob/main/CHANGELOG.md) 7 | 8 | 1. 支持将 Memos 中的数据同步到思源笔记,包括文本、图片、标签、引用等等。 9 | 2. 支持三种同步保存方案:Daily Notes、指定的笔记本或文档下、单份文档中。 10 | 3. 支持将 Memos 的引用保存为引用块或者嵌入块。 11 | 4. 支持识别双向链接。 12 | 5. 支持将 Memos 的标签保存在同一标签下。 13 | 14 | ### 使用说明 15 | 16 | 1. 安装插件。 17 | 2. 填写 `服务器地址` 和 `授权码` ,点击 `校验` 按钮,校验通过后,再配置其它的设置,`必填项` 一定要全部配置。 18 | 3. 点击插件图标进行同步。 19 | 20 | ### 常见问题 21 | 22 | 1. 插件在 Safari 浏览器、ios、ipad 等设备或软件上可能出现报错等异常情况无法正常使用。(暂无解决方案) 23 | 24 | ### 特别感谢 25 | 26 | 本项目使用了 [frostime](https://github.com/frostime) 大佬提供的 [plugin-sample-vite](https://github.com/frostime/plugin-sample-vite) 模板仓库,在开发时很大程度参考了 [winter60](https://github.com/winter60) 大佬的 [winter60/plugin-flomo-sync: 用于导入flomo到思源 (github.com)](https://github.com/winter60/plugin-flomo-sync) 项目,同时还要感谢各位开源代码的大佬。 -------------------------------------------------------------------------------- /asset/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yimien/plugin-memos-sync/66fa76df02324250d7a6ffb0f9aeadbd0ddcbeae/asset/action.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yimien/plugin-memos-sync/66fa76df02324250d7a6ffb0f9aeadbd0ddcbeae/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-memos-sync", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "description": "Memos 同步", 6 | "repository": "", 7 | "homepage": "", 8 | "author": "", 9 | "license": "MIT", 10 | "scripts": { 11 | "make-link": "node --no-warnings ./scripts/make_dev_link.js", 12 | "dev": "vite build --watch", 13 | "tag": "git add ./plugin.json && git commit -m \"update version\" && git push && node ./autoTag.js", 14 | "build": "vite build" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^20.2.0", 18 | "@types/pino": "^7.0.5", 19 | "fast-glob": "^3.2.12", 20 | "glob": "^7.2.3", 21 | "minimist": "^1.2.8", 22 | "moment": "^2.29.4", 23 | "rollup-plugin-livereload": "^2.0.5", 24 | "sass": "^1.62.1", 25 | "siyuan": "0.9.8", 26 | "svelte": "^3.57.0", 27 | "ts-md5": "^1.3.1", 28 | "ts-node": "^10.9.1", 29 | "turndown": "^7.1.2", 30 | "typescript": "^5.0.4", 31 | "vite": "^4.3.7", 32 | "vite-plugin-static-copy": "^0.15.0", 33 | "vite-plugin-zip-pack": "^1.0.5" 34 | }, 35 | "dependencies": { 36 | "pino": "^8.17.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-memos-sync", 3 | "author": "Yimien", 4 | "url": "https://github.com/Yimien/plugin-memos-sync", 5 | "version": "0.2.3", 6 | "minAppVersion": "2.11.3", 7 | "backends": [ 8 | "windows", 9 | "docker" 10 | ], 11 | "frontends": [ 12 | "desktop", 13 | "desktop-window", 14 | "browser-desktop" 15 | ], 16 | "displayName": { 17 | "en_US": "Memos Sync", 18 | "zh_CN": "Memos 同步" 19 | }, 20 | "description": { 21 | "en_US": "Memos Sync", 22 | "zh_CN": "Memos 同步" 23 | }, 24 | "readme": { 25 | "en_US": "README.md", 26 | "zh_CN": "README_zh_CN.md" 27 | }, 28 | "funding": { 29 | "custom": [ 30 | ] 31 | }, 32 | "keywords": [ 33 | "Memos", "同步", "sync" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yimien/plugin-memos-sync/66fa76df02324250d7a6ffb0f9aeadbd0ddcbeae/preview.png -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | build 3 | dist 4 | *.exe 5 | *.spec 6 | -------------------------------------------------------------------------------- /scripts/make_dev_link.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import http from 'node:http'; 3 | import readline from 'node:readline'; 4 | 5 | 6 | //************************************ Write you dir here ************************************ 7 | 8 | //Please write the "workspace/data/plugins" directory here 9 | //请在这里填写你的 "workspace/data/plugins" 目录 10 | let targetDir = ''; 11 | //Like this 12 | // let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`; 13 | //******************************************************************************************** 14 | 15 | const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); 16 | const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); 17 | 18 | let POST_HEADER = { 19 | // "Authorization": `Token ${token}`, 20 | "Content-Type": "application/json", 21 | } 22 | 23 | async function myfetch(url, options) { 24 | //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 25 | return new Promise((resolve, reject) => { 26 | let req = http.request(url, options, (res) => { 27 | let data = ''; 28 | res.on('data', (chunk) => { 29 | data += chunk; 30 | }); 31 | res.on('end', () => { 32 | resolve({ 33 | ok: true, 34 | status: res.statusCode, 35 | json: () => JSON.parse(data) 36 | }); 37 | }); 38 | }); 39 | req.on('error', (e) => { 40 | reject(e); 41 | }); 42 | req.end(); 43 | }); 44 | } 45 | 46 | async function getSiYuanDir() { 47 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; 48 | let conf = {}; 49 | try { 50 | let response = await myfetch(url, { 51 | method: 'POST', 52 | headers: POST_HEADER 53 | }); 54 | if (response.ok) { 55 | conf = await response.json(); 56 | } else { 57 | error(`\tHTTP-Error: ${response.status}`); 58 | return null; 59 | } 60 | } catch (e) { 61 | error(`\tError: ${e}`); 62 | error("\tPlease make sure SiYuan is running!!!"); 63 | return null; 64 | } 65 | return conf.data; 66 | } 67 | 68 | async function chooseTarget(workspaces) { 69 | let count = workspaces.length; 70 | log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) 71 | for (let i = 0; i < workspaces.length; i++) { 72 | log(`\t[${i}] ${workspaces[i].path}`); 73 | } 74 | 75 | if (count == 1) { 76 | return `${workspaces[0].path}/data/plugins`; 77 | } else { 78 | const rl = readline.createInterface({ 79 | input: process.stdin, 80 | output: process.stdout 81 | }); 82 | let index = await new Promise((resolve, reject) => { 83 | rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => { 84 | resolve(answer); 85 | }); 86 | }); 87 | rl.close(); 88 | return `${workspaces[index].path}/data/plugins`; 89 | } 90 | } 91 | 92 | log('>>> Try to visit constant "targetDir" in make_dev_link.js...') 93 | 94 | if (targetDir === '') { 95 | log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....') 96 | let res = await getSiYuanDir(); 97 | 98 | if (res === null || res === undefined || res.length === 0) { 99 | log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); 100 | 101 | // console.log(process.env) 102 | let env = process.env?.SIYUAN_PLUGIN_DIR; 103 | if (env !== undefined && env !== null && env !== '') { 104 | targetDir = env; 105 | log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`); 106 | } else { 107 | error('\tCan not get SiYuan directory from environment variable "SIYUAN_PLUGIN_DIR", failed!'); 108 | process.exit(1); 109 | } 110 | } else { 111 | targetDir = await chooseTarget(res); 112 | } 113 | 114 | 115 | log(`>>> Successfully got target directory: ${targetDir}`); 116 | } 117 | 118 | //Check 119 | if (!fs.existsSync(targetDir)) { 120 | error(`Failed! plugin directory not exists: "${targetDir}"`); 121 | error(`Please set the plugin directory in scripts/make_dev_link.js`); 122 | process.exit(1); 123 | } 124 | 125 | 126 | //check if plugin.json exists 127 | if (!fs.existsSync('./plugin.json')) { 128 | //change dir to parent 129 | process.chdir('../'); 130 | if (!fs.existsSync('./plugin.json')) { 131 | error('Failed! plugin.json not found'); 132 | process.exit(1); 133 | } 134 | } 135 | 136 | //load plugin.json 137 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); 138 | const name = plugin?.name; 139 | if (!name || name === '') { 140 | error('Failed! Please set plugin name in plugin.json'); 141 | process.exit(1); 142 | } 143 | 144 | //dev directory 145 | const devDir = `${process.cwd()}/dev`; 146 | //mkdir if not exists 147 | if (!fs.existsSync(devDir)) { 148 | fs.mkdirSync(devDir); 149 | } 150 | 151 | function cmpPath(path1, path2) { 152 | path1 = path1.replace(/\\/g, '/'); 153 | path2 = path2.replace(/\\/g, '/'); 154 | // sepertor at tail 155 | if (path1[path1.length - 1] !== '/') { 156 | path1 += '/'; 157 | } 158 | if (path2[path2.length - 1] !== '/') { 159 | path2 += '/'; 160 | } 161 | return path1 === path2; 162 | } 163 | 164 | const targetPath = `${targetDir}/${name}`; 165 | //如果已经存在,就退出 166 | if (fs.existsSync(targetPath)) { 167 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); 168 | 169 | if (isSymbol) { 170 | let srcPath = fs.readlinkSync(targetPath); 171 | 172 | if (cmpPath(srcPath, devDir)) { 173 | log(`Good! ${targetPath} is already linked to ${devDir}`); 174 | } else { 175 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`); 176 | } 177 | } else { 178 | error(`Failed! ${targetPath} already exists and is not a symbolic link`); 179 | } 180 | 181 | } else { 182 | //创建软链接 183 | fs.symlinkSync(devDir, targetPath, 'junction'); 184 | log(`Done! Created symlink ${targetPath}`); 185 | } 186 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | //todo-tree settings 2 | { 3 | "todo-tree.regex.regex": "((//|#|