diff --git a/manifest.json b/manifest.json index 86df8e0..645157d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "znote-sync", - "name": "Znote Sync", + "id": "sonicnote-sync", + "name": "SonicNote Sync", "version": "1.0.0", "minAppVersion": "0.15.0", - "description": "Sync recordings from Znote (SonicNote) to Obsidian as Markdown files", - "author": "EasyLink", + "description": "Sync recordings from SonicNote to Obsidian as Markdown files", + "author": "EasyLinkIn", "isDesktopOnly": false } diff --git a/package.json b/package.json index d1346ca..e33087a 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "znote-sync", + "name": "sonicnote-sync", "version": "1.0.0", - "description": "Sync recordings from Znote (SonicNote) to Obsidian as Markdown files", + "description": "Sync recordings from 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"], + "keywords": ["obsidian", "sonicnote", "recording", "sync"], "author": "EasyLink", "license": "MIT", "devDependencies": { diff --git a/release/obsidian-plugin.html b/release/obsidian-plugin.html new file mode 100644 index 0000000..86db441 --- /dev/null +++ b/release/obsidian-plugin.html @@ -0,0 +1,252 @@ + + + + + +SonicNote Sync — Obsidian 插件 + + + + +
+
v1.0.0
+

SonicNote Sync

+

将 SonicNote 妙记的录音、转录、总结同步到 Obsidian,以 Markdown 文件管理。

+ + + 下载插件 + +
+ +
+ +
+

前置要求

+ +
+ +
+

安装

+
    +
  1. 点击上方按钮下载 sonicnote-sync.zip
  2. +
  3. 解压 zip 文件,得到以下文件: +
      +
    • main.js
    • +
    • manifest.json
    • +
    • styles.css
    • +
    +
  4. +
  5. 打开 Obsidian Vault 所在文件夹,进入插件目录:
  6. +
+
.obsidian/plugins/
+
    +
  1. plugins 目录下新建文件夹 sonicnote-sync,将解压的 3 个文件放进去:
  2. +
+
.obsidian/plugins/sonicnote-sync/
+├── main.js
+├── manifest.json
+└── styles.css
+
    +
  1. 重启 Obsidian,进入 设置 → 第三方/社区插件,在列表中找到 SonicNote Sync 并启用
  2. +
+
+ +
+

使用方法

+

登录

+
    +
  1. 进入 设置 → SonicNote Sync
  2. +
  3. 点击"登录"按钮
  4. +
  5. 输入 SonicNote 妙记的手机号和密码(使用 APP 验证码或者申领长期密码)
  6. +
+ +

同步录音

+

登录成功后,通过以下任一方式触发同步:

+ +

同步完成后,Vault 中会生成 SonicNoteSync/ 文件夹,每条录音对应一个 Markdown 文件。

+
+ +
+

同步内容

+

每个录音文件包含:

+ + + + + + + + +
内容说明
Frontmatter录音元数据(ID、时间、时长、设备名、标签等)
笔记你在 App 中手写的笔记内容
转录内容AI 转写文本(含说话人、时间戳)
AI 总结智能总结内容(要点、行动项等)
+
+ +
+

设置项

+ + + + + +
设置默认值说明
同步文件夹SonicNoteSyncMarkdown 文件存放目录
+
+ +
+

卸载

+
    +
  1. 进入 设置 → 社区插件,找到 SonicNote Sync,点击禁用并卸载
  2. +
  3. 手动删除 .obsidian/plugins/sonicnote-sync/ 文件夹
  4. +
+
+ +
+ + + + + + + diff --git a/release/sonicnote-sync.zip b/release/sonicnote-sync.zip new file mode 100644 index 0000000..903b12b Binary files /dev/null and b/release/sonicnote-sync.zip differ diff --git a/src/api.ts b/src/api.ts index 8fad6f5..d346902 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,8 +1,8 @@ import { requestUrl, RequestUrlParam } from 'obsidian'; -import { BackendResponse, Recording, TranscriptSegment, SummaryData, ZnotePluginSettings } from './types'; +import { BackendResponse, Recording, TranscriptSegment, SummaryData, SonicNotePluginSettings } from './types'; -export class ZnoteApiClient { - constructor(private getSettings: () => ZnotePluginSettings) {} +export class SonicNoteApiClient { + constructor(private getSettings: () => SonicNotePluginSettings) {} isAuthenticated(): boolean { return this.getSettings().token !== ''; @@ -31,19 +31,25 @@ export class ZnoteApiClient { 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; + try { + const response = await requestUrl({ + url, + method, + headers, + body: method === 'POST' && options?.body ? JSON.stringify(options.body) : undefined, + }); + return response.json as BackendResponse; + } catch (e: any) { + // requestUrl throws on non-2xx status codes; try to extract the body + if (e?.json) { + return e.json as BackendResponse; + } + throw new Error(e?.message || `请求失败: ${method} ${path}`); + } } async login(phone: string, code: string): Promise<{ token: string; userId: string }> { @@ -53,9 +59,14 @@ export class ZnoteApiClient { if (res.code !== 200) { throw new Error(res.msg || '登录失败'); } + const data = res.data; + const token = typeof data === 'string' ? data : data?.token; + if (!token) { + throw new Error('登录响应中缺少 token'); + } return { - token: res.data.token || res.data, - userId: res.data.userId || '', + token, + userId: data?.user?.userId || data?.userId || '', }; } diff --git a/src/formatter.ts b/src/formatter.ts index d07dab4..8101b8a 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -50,7 +50,7 @@ export function generateFrontmatter(recording: Recording, syncTime: string): str } lines.push('tags:'); - lines.push(' - znote'); + lines.push(' - sonicnote'); const recordTypeLabel = recording.recordType === '00' ? '通话' : '录音'; lines.push(` - ${recordTypeLabel}`); diff --git a/src/main.ts b/src/main.ts index 39b58e8..58af37a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,12 @@ import { Notice, Plugin } from 'obsidian'; -import { ZnoteApiClient } from './api'; +import { SonicNoteApiClient } from './api'; import { SyncService } from './sync'; -import { ZnoteSettingTab } from './settings'; -import { DEFAULT_SETTINGS, ZnotePluginSettings } from './types'; +import { SonicNoteSettingTab } from './settings'; +import { DEFAULT_SETTINGS, SonicNotePluginSettings } from './types'; -export default class ZnoteSyncPlugin extends Plugin { - settings: ZnotePluginSettings = DEFAULT_SETTINGS; - private api!: ZnoteApiClient; +export default class SonicNoteSyncPlugin extends Plugin { + settings: SonicNotePluginSettings = DEFAULT_SETTINGS; + private api!: SonicNoteApiClient; private syncService!: SyncService; private statusBarEl!: HTMLElement; @@ -14,7 +14,7 @@ export default class ZnoteSyncPlugin extends Plugin { await this.loadSettings(); // Initialize API client and sync service - this.api = new ZnoteApiClient(() => this.settings); + this.api = new SonicNoteApiClient(() => this.settings); this.syncService = new SyncService( this.app, this.api, @@ -27,42 +27,42 @@ export default class ZnoteSyncPlugin extends Plugin { this.updateStatusBar(); // Ribbon icon - this.addRibbonIcon('headphones', 'Znote Sync: 同步录音', () => { + this.addRibbonIcon('headphones', 'SonicNote Sync: 同步录音', () => { this.triggerSync(); }); // Commands this.addCommand({ - id: 'znote-sync:sync', + id: 'sonicnote-sync:sync', name: '同步录音', callback: () => this.triggerSync(), }); this.addCommand({ - id: 'znote-sync:login', + id: 'sonicnote-sync:login', name: '登录', callback: () => { // @ts-ignore - internal API to open settings this.app.setting.open(); // @ts-ignore - this.app.setting.openTabById('znote-sync'); + this.app.setting.openTabById('sonicnote-sync'); }, }); this.addCommand({ - id: 'znote-sync:logout', + id: 'sonicnote-sync:logout', name: '登出', callback: async () => { this.settings.token = ''; this.settings.phoneNumber = ''; await this.saveSettings(); - new Notice('已登出 Znote'); + new Notice('已登出 SonicNote'); this.updateStatusBar(); }, }); // Settings tab - this.addSettingTab(new ZnoteSettingTab( + this.addSettingTab(new SonicNoteSettingTab( this.app, this, this.api, @@ -70,11 +70,11 @@ export default class ZnoteSyncPlugin extends Plugin { () => this.saveSettings() )); - console.log('Znote Sync plugin loaded'); + console.log('SonicNote Sync plugin loaded'); } onunload() { - console.log('Znote Sync plugin unloaded'); + console.log('SonicNote Sync plugin unloaded'); } async loadSettings() { @@ -87,15 +87,15 @@ export default class ZnoteSyncPlugin extends Plugin { private async triggerSync() { if (!this.api.isAuthenticated()) { - new Notice('请先登录 Znote(设置 → Znote Sync)'); + new Notice('请先登录 SonicNote(设置 → SonicNote Sync)'); return; } - this.statusBarEl.setText('Znote: 同步中...'); + this.statusBarEl.setText('SonicNote: 同步中...'); try { const result = await this.syncService.syncAll((msg) => { - this.statusBarEl.setText(`Znote: ${msg}`); + this.statusBarEl.setText(`SonicNote: ${msg}`); }); let message = `同步完成: ${result.synced} 条新/更新`; @@ -115,9 +115,9 @@ export default class ZnoteSyncPlugin extends Plugin { const lastSync = this.settings.lastSyncTime ? `上次同步: ${this.settings.lastSyncTime.substring(0, 16).replace('T', ' ')}` : '未同步'; - this.statusBarEl.setText(`Znote: ${lastSync}`); + this.statusBarEl.setText(`SonicNote: ${lastSync}`); } else { - this.statusBarEl.setText('Znote: 未登录'); + this.statusBarEl.setText('SonicNote: 未登录'); } } } diff --git a/src/settings.ts b/src/settings.ts index ceac65f..d625f74 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,17 +1,17 @@ import { App, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; -import { ZnoteApiClient } from './api'; -import { ZnotePluginSettings, DEFAULT_SETTINGS } from './types'; +import { SonicNoteApiClient } from './api'; +import { SonicNotePluginSettings, DEFAULT_SETTINGS } from './types'; -export class ZnoteSettingTab extends PluginSettingTab { - private api: ZnoteApiClient; - private getSettings: () => ZnotePluginSettings; +export class SonicNoteSettingTab extends PluginSettingTab { + private api: SonicNoteApiClient; + private getSettings: () => SonicNotePluginSettings; private saveSettings: () => Promise; constructor( app: App, plugin: Plugin, - api: ZnoteApiClient, - getSettings: () => ZnotePluginSettings, + api: SonicNoteApiClient, + getSettings: () => SonicNotePluginSettings, saveSettings: () => Promise ) { super(app, plugin); @@ -25,26 +25,14 @@ export class ZnoteSettingTab extends PluginSettingTab { 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(); - })); + containerEl.createEl('h2', { text: 'SonicNote Sync 设置' }); // Sync folder new Setting(containerEl) .setName('同步文件夹') .setDesc('录音 Markdown 文件存放的文件夹(相对于 Vault 根目录)') .addText(text => text - .setPlaceholder('ZnoteSync') + .setPlaceholder('SonicNoteSync') .setValue(settings.syncFolder) .onChange(async (value) => { settings.syncFolder = value; @@ -83,15 +71,15 @@ export class ZnoteSettingTab extends PluginSettingTab { } class LoginModal extends Modal { - private api: ZnoteApiClient; - private getSettings: () => ZnotePluginSettings; + private api: SonicNoteApiClient; + private getSettings: () => SonicNotePluginSettings; private saveSettings: () => Promise; private onCloseCallback: () => void; constructor( app: App, - api: ZnoteApiClient, - getSettings: () => ZnotePluginSettings, + api: SonicNoteApiClient, + getSettings: () => SonicNotePluginSettings, saveSettings: () => Promise, onCloseCallback: () => void ) { @@ -106,7 +94,7 @@ class LoginModal extends Modal { const { contentEl } = this; contentEl.empty(); - contentEl.createEl('h2', { text: '登录智音笔记' }); + contentEl.createEl('h2', { text: '登录 SonicNote' }); let phone = ''; let code = ''; @@ -118,9 +106,10 @@ class LoginModal extends Modal { .onChange((value) => { phone = value; })); new Setting(contentEl) - .setName('验证码') + .setName('密码') + .setDesc('使用 APP 验证码或者申领长期密码') .addText(text => text - .setPlaceholder('请输入验证码') + .setPlaceholder('请输入密码') .onChange((value) => { code = value; })); new Setting(contentEl) @@ -129,7 +118,7 @@ class LoginModal extends Modal { .setCta() .onClick(async () => { if (!phone || !code) { - new Notice('请输入手机号和验证码'); + new Notice('请输入手机号和密码'); return; } try { diff --git a/src/sync.ts b/src/sync.ts index b568a7a..5c9356b 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,18 +1,18 @@ import { App, Notice, TFile, TFolder } from 'obsidian'; -import { ZnoteApiClient } from './api'; -import { ZnotePluginSettings, LocalFileInfo, Recording, SyncResult, TranscriptSegment, SummaryData } from './types'; +import { SonicNoteApiClient } from './api'; +import { SonicNotePluginSettings, LocalFileInfo, Recording, SyncResult, TranscriptSegment, SummaryData } from './types'; import { formatFileName, toMarkdown } from './formatter'; export class SyncService { - private api: ZnoteApiClient; + private api: SonicNoteApiClient; private app: App; - private getSettings: () => ZnotePluginSettings; + private getSettings: () => SonicNotePluginSettings; private saveSettings: () => Promise; constructor( app: App, - api: ZnoteApiClient, - getSettings: () => ZnotePluginSettings, + api: SonicNoteApiClient, + getSettings: () => SonicNotePluginSettings, saveSettings: () => Promise ) { this.app = app; @@ -113,7 +113,7 @@ export class SyncService { return match ? match[1] : null; } - private async processRecording(recording: Recording, localIndex: Map, settings: ZnotePluginSettings): Promise { + private async processRecording(recording: Recording, localIndex: Map, settings: SonicNotePluginSettings): Promise { const syncTime = new Date().toISOString(); // Check if we need to update @@ -150,7 +150,7 @@ export class SyncService { }); } - private async writeRecordingToNewFile(recording: Recording, syncTime: string, settings: ZnotePluginSettings, conflictWithPath?: string): Promise { + private async writeRecordingToNewFile(recording: Recording, syncTime: string, settings: SonicNotePluginSettings, conflictWithPath?: string): Promise { const content = await this.buildRecordingContent(recording, syncTime); let fileName = formatFileName(recording); diff --git a/src/types.ts b/src/types.ts index 485d5fc..bd4e727 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ // ===== 插件设置 ===== -export interface ZnotePluginSettings { +export interface SonicNotePluginSettings { serverUrl: string; syncFolder: string; pageSize: number; @@ -9,9 +9,9 @@ export interface ZnotePluginSettings { lastSyncTime: string; } -export const DEFAULT_SETTINGS: ZnotePluginSettings = { - serverUrl: 'http://ainote.easylinkin.com:8048', - syncFolder: 'ZnoteSync', +export const DEFAULT_SETTINGS: SonicNotePluginSettings = { + serverUrl: 'https://ainote.easylinkin.com:18048/prod-api', + syncFolder: 'SonicNoteSync', pageSize: 50, token: '', phoneNumber: '', diff --git a/styles.css b/styles.css index 457130d..37c422d 100644 --- a/styles.css +++ b/styles.css @@ -1,10 +1,10 @@ -/* Znote Sync Plugin Styles */ +/* SonicNote Sync Plugin Styles */ /* Login modal */ -.znote-login-modal .setting-item { +.sonicnote-login-modal .setting-item { padding: 8px 0; } -.znote-login-modal .setting-item-control input { +.sonicnote-login-modal .setting-item-control input { width: 200px; }