mirror of
https://github.com/long2icc/sonicnote-sync.git
synced 2026-07-22 07:45:14 +00:00
init
This commit is contained in:
commit
e0a2af6b3c
14 changed files with 1505 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
main.js
|
||||
main.js.map
|
||||
data.json
|
||||
31
esbuild.config.mjs
Normal file
31
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
...builtinModules
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "znote-sync",
|
||||
"name": "Znote Sync",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Sync recordings from Znote (SonicNote) to Obsidian as Markdown files",
|
||||
"author": "EasyLink",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
623
package-lock.json
generated
Normal file
623
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
{
|
||||
"name": "znote-sync",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "znote-sync",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"esbuild": "0.25.5",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.5.tgz",
|
||||
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.5",
|
||||
"@esbuild/android-arm": "0.25.5",
|
||||
"@esbuild/android-arm64": "0.25.5",
|
||||
"@esbuild/android-x64": "0.25.5",
|
||||
"@esbuild/darwin-arm64": "0.25.5",
|
||||
"@esbuild/darwin-x64": "0.25.5",
|
||||
"@esbuild/freebsd-arm64": "0.25.5",
|
||||
"@esbuild/freebsd-x64": "0.25.5",
|
||||
"@esbuild/linux-arm": "0.25.5",
|
||||
"@esbuild/linux-arm64": "0.25.5",
|
||||
"@esbuild/linux-ia32": "0.25.5",
|
||||
"@esbuild/linux-loong64": "0.25.5",
|
||||
"@esbuild/linux-mips64el": "0.25.5",
|
||||
"@esbuild/linux-ppc64": "0.25.5",
|
||||
"@esbuild/linux-riscv64": "0.25.5",
|
||||
"@esbuild/linux-s390x": "0.25.5",
|
||||
"@esbuild/linux-x64": "0.25.5",
|
||||
"@esbuild/netbsd-arm64": "0.25.5",
|
||||
"@esbuild/netbsd-x64": "0.25.5",
|
||||
"@esbuild/openbsd-arm64": "0.25.5",
|
||||
"@esbuild/openbsd-x64": "0.25.5",
|
||||
"@esbuild/sunos-x64": "0.25.5",
|
||||
"@esbuild/win32-arm64": "0.25.5",
|
||||
"@esbuild/win32-ia32": "0.25.5",
|
||||
"@esbuild/win32-x64": "0.25.5"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmmirror.com/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "znote-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Sync recordings from Znote (SonicNote) to Obsidian as Markdown files",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": ["obsidian", "znote", "recording", "sync"],
|
||||
"author": "EasyLink",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"esbuild": "0.25.5",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
110
src/api.ts
Normal file
110
src/api.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { requestUrl, RequestUrlParam } from 'obsidian';
|
||||
import { BackendResponse, Recording, TranscriptSegment, SummaryData, ZnotePluginSettings } from './types';
|
||||
|
||||
export class ZnoteApiClient {
|
||||
constructor(private getSettings: () => ZnotePluginSettings) {}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.getSettings().token !== '';
|
||||
}
|
||||
|
||||
private get serverUrl(): string {
|
||||
return this.getSettings().serverUrl;
|
||||
}
|
||||
|
||||
private get token(): string {
|
||||
return this.getSettings().token;
|
||||
}
|
||||
|
||||
private async request(method: 'GET' | 'POST', path: string, options?: { query?: Record<string, string | number>; body?: unknown }): Promise<BackendResponse> {
|
||||
let url = `${this.serverUrl}${path}`;
|
||||
if (options?.query) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(options.query)) {
|
||||
if (v !== undefined && v !== null) params.set(k, String(v));
|
||||
}
|
||||
url += '?' + params.toString();
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
const params: RequestUrlParam = {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (method === 'POST' && options?.body) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
params.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await requestUrl(params);
|
||||
return response.json as BackendResponse;
|
||||
}
|
||||
|
||||
async login(phone: string, code: string): Promise<{ token: string; userId: string }> {
|
||||
const res = await this.request('POST', '/app/user/loginByPhone', {
|
||||
body: { phonenumber: phone, code: code },
|
||||
});
|
||||
if (res.code !== 200) {
|
||||
throw new Error(res.msg || '登录失败');
|
||||
}
|
||||
return {
|
||||
token: res.data.token || res.data,
|
||||
userId: res.data.userId || '',
|
||||
};
|
||||
}
|
||||
|
||||
async fetchRecordingList(page: number, size: number): Promise<{ list: Recording[]; total: number }> {
|
||||
const res = await this.request('GET', '/app/recording/list', {
|
||||
query: { page, size },
|
||||
});
|
||||
if (res.code !== 200) {
|
||||
throw new Error(res.msg || '获取录音列表失败');
|
||||
}
|
||||
return {
|
||||
list: res.data?.records || res.data?.list || [],
|
||||
total: res.data?.total || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async fetchRecordingDetail(audioId: string): Promise<Recording> {
|
||||
const res = await this.request('GET', '/app/recording/detail', {
|
||||
query: { audioId },
|
||||
});
|
||||
if (res.code !== 200) {
|
||||
throw new Error(res.msg || '获取录音详情失败');
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async fetchNote(audioId: string): Promise<string> {
|
||||
const res = await this.request('GET', '/app/recording/getNote', {
|
||||
query: { audioId },
|
||||
});
|
||||
if (res.code !== 200) {
|
||||
return '';
|
||||
}
|
||||
return res.data?.note || '';
|
||||
}
|
||||
|
||||
async fetchTranscriptResult(audioId: string): Promise<TranscriptSegment[]> {
|
||||
const res = await this.request('GET', `/share/${audioId}/transcript/result`);
|
||||
if (res.code !== 200) {
|
||||
return [];
|
||||
}
|
||||
return res.data || [];
|
||||
}
|
||||
|
||||
async fetchSummary(audioId: string): Promise<SummaryData | null> {
|
||||
const res = await this.request('GET', `/share/${audioId}/summary`);
|
||||
if (res.code !== 200) {
|
||||
return null;
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
116
src/formatter.ts
Normal file
116
src/formatter.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { Recording, TranscriptSegment, SummaryData } from './types';
|
||||
|
||||
export function sanitizeFileName(name: string): string {
|
||||
return name
|
||||
.replace(/[\\/:*?"<>|]/g, '-')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
|| 'untitled';
|
||||
}
|
||||
|
||||
export function formatFileName(recording: Recording): string {
|
||||
const displayName = recording.recordNickName || recording.recordName || '未命名录音';
|
||||
const date = recording.recordTime
|
||||
? recording.recordTime.substring(0, 10)
|
||||
: new Date().toISOString().substring(0, 10);
|
||||
return `${date}-${sanitizeFileName(displayName)}`;
|
||||
}
|
||||
|
||||
function formatDuration(seconds: string | number): string {
|
||||
const s = Number(seconds) || 0;
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = Math.floor(s % 60);
|
||||
if (h > 0) {
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||
}
|
||||
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatTranscriptTime(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
return formatDuration(totalSeconds);
|
||||
}
|
||||
|
||||
export function generateFrontmatter(recording: Recording, syncTime: string): string {
|
||||
const lines: string[] = [
|
||||
'---',
|
||||
`audio_id: "${recording.audioId}"`,
|
||||
`record_name: "${recording.recordName || ''}"`,
|
||||
`record_nick_name: "${(recording.recordNickName || '').replace(/"/g, '\\"')}"`,
|
||||
`duration: "${recording.duration || '0'}"`,
|
||||
`record_time: "${recording.recordTime || ''}"`,
|
||||
`record_type: "${recording.recordType || ''}"`,
|
||||
`device_name: "${recording.deviceName || ''}"`,
|
||||
];
|
||||
|
||||
if (recording.audioUrl) {
|
||||
lines.push(`audio_url: "${recording.audioUrl}"`);
|
||||
}
|
||||
|
||||
lines.push('tags:');
|
||||
lines.push(' - znote');
|
||||
|
||||
const recordTypeLabel = recording.recordType === '00' ? '通话' : '录音';
|
||||
lines.push(` - ${recordTypeLabel}`);
|
||||
|
||||
lines.push(`sync_time: "${syncTime}"`);
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function formatTranscript(segments: TranscriptSegment[]): string {
|
||||
if (!segments || segments.length === 0) return '';
|
||||
|
||||
return segments.map(seg => {
|
||||
const time = formatTranscriptTime(seg.time);
|
||||
const speaker = seg.spokesperson || '未知';
|
||||
return `**[${time}] ${speaker}:** ${seg.text}`;
|
||||
}).join('\n\n');
|
||||
}
|
||||
|
||||
export function toMarkdown(
|
||||
recording: Recording,
|
||||
transcript: TranscriptSegment[] | null,
|
||||
summary: SummaryData | null,
|
||||
note: string,
|
||||
syncTime: string
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Frontmatter
|
||||
parts.push(generateFrontmatter(recording, syncTime));
|
||||
parts.push('');
|
||||
|
||||
// Title
|
||||
const title = recording.recordNickName || recording.recordName || '未命名录音';
|
||||
parts.push(`# ${title}`);
|
||||
parts.push('');
|
||||
|
||||
// Note section
|
||||
if (note && note.trim()) {
|
||||
parts.push('## 笔记');
|
||||
parts.push('');
|
||||
parts.push(note.trim());
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
// Transcript section
|
||||
if (transcript && transcript.length > 0) {
|
||||
parts.push('## 转录内容');
|
||||
parts.push('');
|
||||
parts.push(formatTranscript(transcript));
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
// Summary section
|
||||
if (summary && summary.summaryContent) {
|
||||
parts.push('## AI 总结');
|
||||
parts.push('');
|
||||
parts.push(summary.summaryContent.trim());
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
123
src/main.ts
Normal file
123
src/main.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { Notice, Plugin } from 'obsidian';
|
||||
import { ZnoteApiClient } from './api';
|
||||
import { SyncService } from './sync';
|
||||
import { ZnoteSettingTab } from './settings';
|
||||
import { DEFAULT_SETTINGS, ZnotePluginSettings } from './types';
|
||||
|
||||
export default class ZnoteSyncPlugin extends Plugin {
|
||||
settings: ZnotePluginSettings = DEFAULT_SETTINGS;
|
||||
private api!: ZnoteApiClient;
|
||||
private syncService!: SyncService;
|
||||
private statusBarEl!: HTMLElement;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Initialize API client and sync service
|
||||
this.api = new ZnoteApiClient(() => this.settings);
|
||||
this.syncService = new SyncService(
|
||||
this.app,
|
||||
this.api,
|
||||
() => this.settings,
|
||||
() => this.saveSettings()
|
||||
);
|
||||
|
||||
// Status bar
|
||||
this.statusBarEl = this.addStatusBarItem();
|
||||
this.updateStatusBar();
|
||||
|
||||
// Ribbon icon
|
||||
this.addRibbonIcon('headphones', 'Znote Sync: 同步录音', () => {
|
||||
this.triggerSync();
|
||||
});
|
||||
|
||||
// Commands
|
||||
this.addCommand({
|
||||
id: 'znote-sync:sync',
|
||||
name: '同步录音',
|
||||
callback: () => this.triggerSync(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'znote-sync:login',
|
||||
name: '登录',
|
||||
callback: () => {
|
||||
// @ts-ignore - internal API to open settings
|
||||
this.app.setting.open();
|
||||
// @ts-ignore
|
||||
this.app.setting.openTabById('znote-sync');
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'znote-sync:logout',
|
||||
name: '登出',
|
||||
callback: async () => {
|
||||
this.settings.token = '';
|
||||
this.settings.phoneNumber = '';
|
||||
await this.saveSettings();
|
||||
new Notice('已登出 Znote');
|
||||
this.updateStatusBar();
|
||||
},
|
||||
});
|
||||
|
||||
// Settings tab
|
||||
this.addSettingTab(new ZnoteSettingTab(
|
||||
this.app,
|
||||
this,
|
||||
this.api,
|
||||
() => this.settings,
|
||||
() => this.saveSettings()
|
||||
));
|
||||
|
||||
console.log('Znote Sync plugin loaded');
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log('Znote Sync plugin unloaded');
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
private async triggerSync() {
|
||||
if (!this.api.isAuthenticated()) {
|
||||
new Notice('请先登录 Znote(设置 → Znote Sync)');
|
||||
return;
|
||||
}
|
||||
|
||||
this.statusBarEl.setText('Znote: 同步中...');
|
||||
|
||||
try {
|
||||
const result = await this.syncService.syncAll((msg) => {
|
||||
this.statusBarEl.setText(`Znote: ${msg}`);
|
||||
});
|
||||
|
||||
let message = `同步完成: ${result.synced} 条新/更新`;
|
||||
if (result.skipped > 0) message += `, ${result.skipped} 条跳过`;
|
||||
if (result.errors > 0) message += `, ${result.errors} 条失败`;
|
||||
|
||||
new Notice(message, 5000);
|
||||
this.updateStatusBar();
|
||||
} catch (e) {
|
||||
new Notice(`同步失败: ${e instanceof Error ? e.message : '未知错误'}`);
|
||||
this.updateStatusBar();
|
||||
}
|
||||
}
|
||||
|
||||
private updateStatusBar() {
|
||||
if (this.api.isAuthenticated()) {
|
||||
const lastSync = this.settings.lastSyncTime
|
||||
? `上次同步: ${this.settings.lastSyncTime.substring(0, 16).replace('T', ' ')}`
|
||||
: '未同步';
|
||||
this.statusBarEl.setText(`Znote: ${lastSync}`);
|
||||
} else {
|
||||
this.statusBarEl.setText('Znote: 未登录');
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/settings.ts
Normal file
158
src/settings.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { App, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { ZnoteApiClient } from './api';
|
||||
import { ZnotePluginSettings, DEFAULT_SETTINGS } from './types';
|
||||
|
||||
export class ZnoteSettingTab extends PluginSettingTab {
|
||||
private api: ZnoteApiClient;
|
||||
private getSettings: () => ZnotePluginSettings;
|
||||
private saveSettings: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: Plugin,
|
||||
api: ZnoteApiClient,
|
||||
getSettings: () => ZnotePluginSettings,
|
||||
saveSettings: () => Promise<void>
|
||||
) {
|
||||
super(app, plugin);
|
||||
this.api = api;
|
||||
this.getSettings = getSettings;
|
||||
this.saveSettings = saveSettings;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
const settings = this.getSettings();
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Znote Sync 设置' });
|
||||
|
||||
// Server URL
|
||||
new Setting(containerEl)
|
||||
.setName('服务器地址')
|
||||
.setDesc('智音笔记后端服务地址')
|
||||
.addText(text => text
|
||||
.setPlaceholder('http://localhost:8048')
|
||||
.setValue(settings.serverUrl)
|
||||
.onChange(async (value) => {
|
||||
settings.serverUrl = value;
|
||||
await this.saveSettings();
|
||||
}));
|
||||
|
||||
// Sync folder
|
||||
new Setting(containerEl)
|
||||
.setName('同步文件夹')
|
||||
.setDesc('录音 Markdown 文件存放的文件夹(相对于 Vault 根目录)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ZnoteSync')
|
||||
.setValue(settings.syncFolder)
|
||||
.onChange(async (value) => {
|
||||
settings.syncFolder = value;
|
||||
await this.saveSettings();
|
||||
}));
|
||||
|
||||
// Login status & actions
|
||||
const loginSection = containerEl.createDiv();
|
||||
loginSection.createEl('h3', { text: '账号' });
|
||||
|
||||
if (this.api.isAuthenticated()) {
|
||||
new Setting(loginSection)
|
||||
.setName('登录状态')
|
||||
.setDesc(`已登录: ${settings.phoneNumber}`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('登出')
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
settings.token = '';
|
||||
settings.phoneNumber = '';
|
||||
await this.saveSettings();
|
||||
new Notice('已登出');
|
||||
this.display();
|
||||
}));
|
||||
} else {
|
||||
new Setting(loginSection)
|
||||
.setName('登录')
|
||||
.setDesc('使用手机号和验证码登录智音笔记')
|
||||
.addButton(btn => btn
|
||||
.setButtonText('登录')
|
||||
.onClick(() => {
|
||||
new LoginModal(this.app, this.api, this.getSettings, this.saveSettings, () => this.display()).open();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LoginModal extends Modal {
|
||||
private api: ZnoteApiClient;
|
||||
private getSettings: () => ZnotePluginSettings;
|
||||
private saveSettings: () => Promise<void>;
|
||||
private onCloseCallback: () => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
api: ZnoteApiClient,
|
||||
getSettings: () => ZnotePluginSettings,
|
||||
saveSettings: () => Promise<void>,
|
||||
onCloseCallback: () => void
|
||||
) {
|
||||
super(app);
|
||||
this.api = api;
|
||||
this.getSettings = getSettings;
|
||||
this.saveSettings = saveSettings;
|
||||
this.onCloseCallback = onCloseCallback;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl('h2', { text: '登录智音笔记' });
|
||||
|
||||
let phone = '';
|
||||
let code = '';
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('手机号')
|
||||
.addText(text => text
|
||||
.setPlaceholder('请输入手机号')
|
||||
.onChange((value) => { phone = value; }));
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('验证码')
|
||||
.addText(text => text
|
||||
.setPlaceholder('请输入验证码')
|
||||
.onChange((value) => { code = value; }));
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('登录')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
if (!phone || !code) {
|
||||
new Notice('请输入手机号和验证码');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
btn.setButtonText('登录中...');
|
||||
btn.setDisabled(true);
|
||||
const result = await this.api.login(phone, code);
|
||||
const settings = this.getSettings();
|
||||
settings.token = result.token;
|
||||
settings.phoneNumber = phone;
|
||||
await this.saveSettings();
|
||||
new Notice('登录成功');
|
||||
this.close();
|
||||
} catch (e) {
|
||||
new Notice(`登录失败: ${e instanceof Error ? e.message : '未知错误'}`);
|
||||
btn.setButtonText('登录');
|
||||
btn.setDisabled(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.onCloseCallback();
|
||||
}
|
||||
}
|
||||
204
src/sync.ts
Normal file
204
src/sync.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { App, Notice, TFile, TFolder } from 'obsidian';
|
||||
import { ZnoteApiClient } from './api';
|
||||
import { ZnotePluginSettings, LocalFileInfo, Recording, SyncResult, TranscriptSegment, SummaryData } from './types';
|
||||
import { formatFileName, toMarkdown } from './formatter';
|
||||
|
||||
export class SyncService {
|
||||
private api: ZnoteApiClient;
|
||||
private app: App;
|
||||
private getSettings: () => ZnotePluginSettings;
|
||||
private saveSettings: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
api: ZnoteApiClient,
|
||||
getSettings: () => ZnotePluginSettings,
|
||||
saveSettings: () => Promise<void>
|
||||
) {
|
||||
this.app = app;
|
||||
this.api = api;
|
||||
this.getSettings = getSettings;
|
||||
this.saveSettings = saveSettings;
|
||||
}
|
||||
|
||||
async syncAll(onProgress?: (msg: string) => void): Promise<SyncResult> {
|
||||
const settings = this.getSettings();
|
||||
const result: SyncResult = { total: 0, synced: 0, skipped: 0, errors: 0, errorMessages: [] };
|
||||
|
||||
// 1. Ensure sync folder exists
|
||||
await this.ensureFolder(settings.syncFolder);
|
||||
|
||||
// 2. Build local index
|
||||
onProgress?.('正在读取本地索引...');
|
||||
const localIndex = await this.buildLocalIndex(settings.syncFolder);
|
||||
|
||||
// 3. Fetch all recordings from backend
|
||||
onProgress?.('正在从服务器拉取录音列表...');
|
||||
let page = 1;
|
||||
let allRecordings: Recording[] = [];
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const res = await this.api.fetchRecordingList(page, settings.pageSize);
|
||||
allRecordings = allRecordings.concat(res.list);
|
||||
if (allRecordings.length >= res.total || res.list.length === 0) break;
|
||||
page++;
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : '获取录音列表失败';
|
||||
result.errorMessages.push(msg);
|
||||
new Notice(`同步失败: ${msg}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out deleted
|
||||
allRecordings = allRecordings.filter(r => r.delFlag !== '2');
|
||||
result.total = allRecordings.length;
|
||||
|
||||
// 4. Process each recording
|
||||
for (let i = 0; i < allRecordings.length; i++) {
|
||||
const recording = allRecordings[i];
|
||||
onProgress?.(`正在同步 ${i + 1}/${result.total}: ${recording.recordNickName || recording.recordName}`);
|
||||
|
||||
try {
|
||||
await this.processRecording(recording, localIndex, settings);
|
||||
result.synced++;
|
||||
} catch (e) {
|
||||
result.errors++;
|
||||
const msg = e instanceof Error ? e.message : '未知错误';
|
||||
result.errorMessages.push(`${recording.recordNickName || recording.recordName}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Update last sync time
|
||||
const now = new Date().toISOString();
|
||||
settings.lastSyncTime = now;
|
||||
await this.saveSettings();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async ensureFolder(folderPath: string): Promise<void> {
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
async buildLocalIndex(syncFolder: string): Promise<Map<string, LocalFileInfo>> {
|
||||
const index = new Map<string, LocalFileInfo>();
|
||||
const folder = this.app.vault.getAbstractFileByPath(syncFolder);
|
||||
if (!(folder instanceof TFolder)) return index;
|
||||
|
||||
for (const file of folder.children) {
|
||||
if (!(file instanceof TFile) || file.extension !== 'md') continue;
|
||||
|
||||
try {
|
||||
const content = await this.app.vault.read(file);
|
||||
const audioId = this.extractFrontmatterField(content, 'audio_id');
|
||||
const syncTime = this.extractFrontmatterField(content, 'sync_time');
|
||||
if (audioId) {
|
||||
index.set(audioId.replace(/"/g, ''), { path: file.path, syncTime: syncTime || '' });
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be read
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private extractFrontmatterField(content: string, field: string): string | null {
|
||||
const match = content.match(new RegExp(`^${field}:\\s*"?([^"]*)"\\s*$`, 'm'));
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
private async processRecording(recording: Recording, localIndex: Map<string, LocalFileInfo>, settings: ZnotePluginSettings): Promise<void> {
|
||||
const syncTime = new Date().toISOString();
|
||||
|
||||
// Check if we need to update
|
||||
const local = localIndex.get(recording.audioId);
|
||||
if (local && local.syncTime && recording.updateTime) {
|
||||
if (recording.updateTime <= local.syncTime) {
|
||||
// No update needed
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for conflict (user edited file locally after sync)
|
||||
const file = this.app.vault.getAbstractFileByPath(local.path);
|
||||
if (file instanceof TFile) {
|
||||
const fileMtime = new Date(file.stat.mtime).toISOString();
|
||||
if (fileMtime > local.syncTime) {
|
||||
// Conflict — write to a new file instead
|
||||
await this.writeRecordingToNewFile(recording, syncTime, settings, local.path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing file
|
||||
const content = await this.buildRecordingContent(recording, syncTime);
|
||||
await this.app.vault.modify(file, content);
|
||||
localIndex.set(recording.audioId, { path: local.path, syncTime });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// New recording — create file
|
||||
await this.writeRecordingToNewFile(recording, syncTime, settings);
|
||||
localIndex.set(recording.audioId, {
|
||||
path: `${settings.syncFolder}/${formatFileName(recording)}.md`,
|
||||
syncTime,
|
||||
});
|
||||
}
|
||||
|
||||
private async writeRecordingToNewFile(recording: Recording, syncTime: string, settings: ZnotePluginSettings, conflictWithPath?: string): Promise<void> {
|
||||
const content = await this.buildRecordingContent(recording, syncTime);
|
||||
let fileName = formatFileName(recording);
|
||||
|
||||
if (conflictWithPath) {
|
||||
// Create conflict copy
|
||||
const timestamp = Date.now();
|
||||
fileName = `${fileName}-conflict-${timestamp}`;
|
||||
}
|
||||
|
||||
// Deduplicate filename
|
||||
let filePath = `${settings.syncFolder}/${fileName}.md`;
|
||||
let counter = 2;
|
||||
while (await this.app.vault.adapter.exists(filePath)) {
|
||||
filePath = `${settings.syncFolder}/${fileName}-${counter}.md`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
await this.app.vault.create(filePath, content);
|
||||
}
|
||||
|
||||
private async buildRecordingContent(recording: Recording, syncTime: string): Promise<string> {
|
||||
const tasks: [string, Promise<TranscriptSegment[] | SummaryData | string | null>][] = [];
|
||||
|
||||
// Fetch transcript if available (status 2 = completed)
|
||||
let transcript: TranscriptSegment[] | null = null;
|
||||
if (recording.transcriptStatus === 2) {
|
||||
tasks.push(['transcript', this.api.fetchTranscriptResult(recording.audioId).catch(() => null)]);
|
||||
}
|
||||
|
||||
// Fetch summary if available (status 2 = completed)
|
||||
let summary: SummaryData | null = null;
|
||||
if (recording.summaryStatus === 2) {
|
||||
tasks.push(['summary', this.api.fetchSummary(recording.audioId).catch(() => null)]);
|
||||
}
|
||||
|
||||
// Fetch note
|
||||
let note = '';
|
||||
tasks.push(['note', this.api.fetchNote(recording.audioId).catch(() => '')]);
|
||||
|
||||
// Execute all requests in parallel
|
||||
const results = await Promise.all(tasks.map(([_, p]) => p));
|
||||
tasks.forEach(([name], i) => {
|
||||
const val = results[i];
|
||||
if (name === 'transcript') transcript = val as TranscriptSegment[] | null;
|
||||
if (name === 'summary') summary = val as SummaryData | null;
|
||||
if (name === 'note') note = (val as string) || '';
|
||||
});
|
||||
|
||||
return toMarkdown(recording, transcript, summary, note, syncTime);
|
||||
}
|
||||
}
|
||||
77
src/types.ts
Normal file
77
src/types.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// ===== 插件设置 =====
|
||||
|
||||
export interface ZnotePluginSettings {
|
||||
serverUrl: string;
|
||||
syncFolder: string;
|
||||
pageSize: number;
|
||||
token: string;
|
||||
phoneNumber: string;
|
||||
lastSyncTime: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ZnotePluginSettings = {
|
||||
serverUrl: 'http://ainote.easylinkin.com:8048',
|
||||
syncFolder: 'ZnoteSync',
|
||||
pageSize: 50,
|
||||
token: '',
|
||||
phoneNumber: '',
|
||||
lastSyncTime: '',
|
||||
};
|
||||
|
||||
// ===== 后端数据类型 =====
|
||||
|
||||
export interface BackendResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface Recording {
|
||||
audioId: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
audioUrl: string;
|
||||
recordTime: string;
|
||||
recordName: string;
|
||||
recordNickName: string;
|
||||
duration: string;
|
||||
note: string;
|
||||
recordType: string;
|
||||
isTranscribed: string;
|
||||
isSummarized: string;
|
||||
transcriptStatus: number;
|
||||
summaryStatus: number;
|
||||
delFlag: string;
|
||||
deviceName: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface TranscriptSegment {
|
||||
spokesperson: string;
|
||||
text: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface SummaryData {
|
||||
summaryId: string;
|
||||
audioId: string;
|
||||
summaryContent: string;
|
||||
templateId: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
// ===== 同步相关 =====
|
||||
|
||||
export interface LocalFileInfo {
|
||||
path: string;
|
||||
syncTime: string;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
total: number;
|
||||
synced: number;
|
||||
skipped: number;
|
||||
errors: number;
|
||||
errorMessages: string[];
|
||||
}
|
||||
10
styles.css
Normal file
10
styles.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* Znote Sync Plugin Styles */
|
||||
|
||||
/* Login modal */
|
||||
.znote-login-modal .setting-item {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.znote-login-modal .setting-item-control input {
|
||||
width: 200px;
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue