style: 优化代码格式和样式,调整设置界面和登录模态样式

This commit is contained in:
huojl 2026-06-13 12:47:27 +08:00
parent e0a7399349
commit d6245a74d3
5 changed files with 593 additions and 584 deletions

View file

@ -1,130 +1,132 @@
import { requestUrl, RequestUrlParam } from 'obsidian';
import { BackendResponse, Recording, TranscriptSegment, SummaryData, StudyReportData, SonicNotePluginSettings } from './types';
import { requestUrl } from 'obsidian';
import { BackendResponse, Recording, TranscriptSegment, SummaryData, StudyReportData } from './types';
export class SonicNoteApiClient {
constructor(private getSettings: () => SonicNotePluginSettings) {}
constructor(private getSettings: () => { token: string; serverUrl: string }) {}
isAuthenticated(): boolean {
return this.getSettings().token !== '';
}
isAuthenticated(): boolean {
return this.getSettings().token !== '';
}
private get serverUrl(): string {
return this.getSettings().serverUrl;
}
private get serverUrl(): string {
return this.getSettings().serverUrl;
}
private get token(): string {
return this.getSettings().token;
}
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();
}
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 headers: Record<string, string> = {};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
if (method === 'POST' && options?.body) {
headers['Content-Type'] = 'application/json';
}
if (method === 'POST' && options?.body) {
headers['Content-Type'] = 'application/json';
}
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;
}
console.error(`[SonicNote] 请求失败 ${method} ${path}:`, e?.message || e);
throw new Error(e?.message || `请求失败: ${method} ${path}`);
}
}
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: unknown) {
const err = e as { json?: BackendResponse; message?: string };
if (err?.json) {
return err.json as BackendResponse;
}
console.error(`[SonicNote] 请求失败 ${method} ${path}:`, err?.message || e);
throw new Error(err?.message || `请求失败: ${method} ${path}`);
}
}
async login(apiKey: string): Promise<{ token: string; userId: string }> {
const res = await this.request('POST', '/app/mcp/login', {
body: { apiKey },
});
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,
userId: data?.user?.userId || data?.userId || '',
};
}
async login(apiKey: string): Promise<{ token: string; userId: string }> {
const res = await this.request('POST', '/app/mcp/login', {
body: { apiKey },
});
if (res.code !== 200) {
throw new Error(res.msg || '登录失败');
}
const data = res.data as { token?: string; user?: { userId: string }; userId?: string };
const token = typeof data === 'string' ? data : data?.token;
if (!token) {
throw new Error('登录响应中缺少 token');
}
return {
token,
userId: data?.user?.userId || 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 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 || '获取录音列表失败');
}
const data = res.data as { records?: Recording[]; list?: Recording[]; total?: number };
return {
list: data?.records || data?.list || [],
total: 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 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 as Recording;
}
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 fetchNote(audioId: string): Promise<string> {
const res = await this.request('GET', '/app/recording/getNote', {
query: { audioId },
});
if (res.code !== 200) {
return '';
}
const data = res.data as { note?: string };
return 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 fetchTranscriptResult(audioId: string): Promise<TranscriptSegment[]> {
const res = await this.request('GET', `/share/${audioId}/transcript/result`);
if (res.code !== 200) {
return [];
}
return (res.data as TranscriptSegment[]) || [];
}
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;
}
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 as SummaryData;
}
async fetchStudyReport(audioId: string): Promise<StudyReportData | null> {
const res = await this.request('GET', `/share/${audioId}/studyReport`);
if (res.code !== 200) {
return null;
}
return res.data;
}
async fetchStudyReport(audioId: string): Promise<StudyReportData | null> {
const res = await this.request('GET', `/share/${audioId}/studyReport`);
if (res.code !== 200) {
return null;
}
return res.data as StudyReportData;
}
}

View file

@ -5,154 +5,154 @@ import { SonicNoteSettingTab } from './settings';
import { DEFAULT_SETTINGS, SonicNotePluginSettings } from './types';
export default class SonicNoteSyncPlugin extends Plugin {
settings: SonicNotePluginSettings = DEFAULT_SETTINGS;
private api!: SonicNoteApiClient;
private syncService!: SyncService;
private statusBarEl!: HTMLElement;
private syncTimer: number | null = null;
private syncing = false;
settings: SonicNotePluginSettings = DEFAULT_SETTINGS;
private api!: SonicNoteApiClient;
private syncService!: SyncService;
private statusBarEl!: HTMLElement;
private syncTimer: number | null = null;
private syncing = false;
async onload() {
await this.loadSettings();
async onload() {
await this.loadSettings();
// Initialize API client and sync service
this.api = new SonicNoteApiClient(() => this.settings);
this.syncService = new SyncService(
this.app,
this.api,
() => this.settings,
() => this.saveSettings()
);
// Initialize API client and sync service
this.api = new SonicNoteApiClient(() => this.settings);
this.syncService = new SyncService(
this.app,
this.api,
() => this.settings,
() => this.saveSettings()
);
// Status bar
this.statusBarEl = this.addStatusBarItem();
this.updateStatusBar();
// Status bar
this.statusBarEl = this.addStatusBarItem();
this.updateStatusBar();
// Ribbon icon
this.addRibbonIcon('headphones', 'SonicNote Sync: 同步录音', () => {
this.triggerSync();
});
// Ribbon icon
this.addRibbonIcon('headphones', 'SonicNote Sync: 同步录音', () => {
void this.triggerSync();
});
// Commands
this.addCommand({
id: 'sonicnote-sync:sync',
name: '同步录音',
callback: () => this.triggerSync(),
});
// Commands
this.addCommand({
id: 'sync',
name: '同步录音',
callback: () => void this.triggerSync(),
});
this.addCommand({
id: 'sonicnote-sync:login',
name: '登录',
callback: () => {
// @ts-ignore - internal API to open settings
this.app.setting.open();
// @ts-ignore
this.app.setting.openTabById('sonicnote-sync');
},
});
this.addCommand({
id: 'login',
name: '登录',
callback: () => {
// @ts-ignore - internal API to open settings
this.app.setting.open();
// @ts-ignore
this.app.setting.openTabById('sonicnote-sync');
},
});
this.addCommand({
id: 'sonicnote-sync:logout',
name: '登出',
callback: async () => {
this.settings.token = '';
this.settings.apiKey = '';
await this.saveSettings();
new Notice('已登出 SonicNote');
this.updateStatusBar();
},
});
this.addCommand({
id: 'logout',
name: '登出',
callback: async () => {
this.settings.token = '';
this.settings.apiKey = '';
await this.saveSettings();
new Notice('已登出 SonicNote');
this.updateStatusBar();
},
});
// Settings tab
this.addSettingTab(new SonicNoteSettingTab(
this.app,
this,
this.api,
() => this.settings,
() => this.saveSettings()
));
// Settings tab
this.addSettingTab(new SonicNoteSettingTab(
this.app,
this,
this.api,
() => this.settings,
() => this.saveSettings()
));
console.log('SonicNote Sync plugin loaded');
console.log('SonicNote Sync plugin loaded');
// Auto sync on open
if (this.settings.autoSyncOnOpen && this.api.isAuthenticated()) {
setTimeout(() => this.triggerSync(), 5000);
}
// Auto sync on open
if (this.settings.autoSyncOnOpen && this.api.isAuthenticated()) {
window.setTimeout(() => void this.triggerSync(), 5000);
}
// Periodic resync
this.startAutoSync();
}
// Periodic resync
this.startAutoSync();
}
onunload() {
this.stopAutoSync();
console.log('SonicNote Sync plugin unloaded');
}
onunload() {
this.stopAutoSync();
console.log('SonicNote Sync plugin unloaded');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async saveSettings() {
await this.saveData(this.settings);
}
private async triggerSync() {
if (!this.api.isAuthenticated()) {
new Notice('请先登录 SonicNote设置 → SonicNote Sync');
return;
}
private async triggerSync() {
if (!this.api.isAuthenticated()) {
new Notice('请先登录 SonicNote设置 → SonicNote Sync');
return;
}
if (this.syncing) return;
this.syncing = true;
this.stopAutoSync();
if (this.syncing) return;
this.syncing = true;
this.stopAutoSync();
this.statusBarEl.setText('SonicNote: 同步中...');
this.statusBarEl.setText('SonicNote: 同步中...');
try {
const result = await this.syncService.syncAll((msg) => {
this.statusBarEl.setText(`SonicNote: ${msg}`);
});
try {
const result = await this.syncService.syncAll((msg) => {
this.statusBarEl.setText(`SonicNote: ${msg}`);
});
let message = `同步完成: ${result.synced} 条新/更新`;
if (result.skipped > 0) message += `, ${result.skipped} 条跳过`;
if (result.errors > 0) message += `, ${result.errors} 条失败`;
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();
} finally {
this.syncing = false;
this.startAutoSync();
}
}
new Notice(message, 5000);
this.updateStatusBar();
} catch (e) {
new Notice(`同步失败: ${e instanceof Error ? e.message : '未知错误'}`);
this.updateStatusBar();
} finally {
this.syncing = false;
this.startAutoSync();
}
}
private updateStatusBar() {
if (this.api.isAuthenticated()) {
const lastSync = this.settings.lastSyncTime
? `上次同步: ${this.settings.lastSyncTime}`
: '未同步';
this.statusBarEl.setText(`SonicNote: ${lastSync}`);
} else {
this.statusBarEl.setText('SonicNote: 未登录');
}
}
private updateStatusBar() {
if (this.api.isAuthenticated()) {
const lastSync = this.settings.lastSyncTime
? `上次同步: ${this.settings.lastSyncTime}`
: '未同步';
this.statusBarEl.setText(`SonicNote: ${lastSync}`);
} else {
this.statusBarEl.setText('SonicNote: 未登录');
}
}
startAutoSync() {
this.stopAutoSync();
const minutes = this.settings.resyncIntervalMinutes;
if (minutes > 0 && this.api.isAuthenticated()) {
this.syncTimer = window.setInterval(() => {
this.triggerSync();
}, minutes * 60 * 1000);
}
}
startAutoSync() {
this.stopAutoSync();
const minutes = this.settings.resyncIntervalMinutes;
if (minutes > 0 && this.api.isAuthenticated()) {
this.syncTimer = window.setInterval(() => {
void this.triggerSync();
}, minutes * 60 * 1000);
}
}
stopAutoSync() {
if (this.syncTimer !== null) {
window.clearInterval(this.syncTimer);
this.syncTimer = null;
}
}
stopAutoSync() {
if (this.syncTimer !== null) {
window.clearInterval(this.syncTimer);
this.syncTimer = null;
}
}
}

View file

@ -1,279 +1,283 @@
import { App, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { SonicNoteApiClient } from './api';
import { SonicNotePluginSettings, DEFAULT_SETTINGS, BUILTIN_FRONTMATTER_FIELDS, CustomFrontmatterField } from './types';
import { SonicNotePluginSettings, BUILTIN_FRONTMATTER_FIELDS } from './types';
export class SonicNoteSettingTab extends PluginSettingTab {
private api: SonicNoteApiClient;
private getSettings: () => SonicNotePluginSettings;
private saveSettings: () => Promise<void>;
private api: SonicNoteApiClient;
private getSettings: () => SonicNotePluginSettings;
private saveSettings: () => Promise<void>;
constructor(
app: App,
plugin: Plugin,
api: SonicNoteApiClient,
getSettings: () => SonicNotePluginSettings,
saveSettings: () => Promise<void>
) {
super(app, plugin);
this.api = api;
this.getSettings = getSettings;
this.saveSettings = saveSettings;
}
constructor(
app: App,
plugin: Plugin,
api: SonicNoteApiClient,
getSettings: () => SonicNotePluginSettings,
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();
display(): void {
const { containerEl } = this;
const settings = this.getSettings();
containerEl.empty();
containerEl.createEl('h2', { text: 'SonicNote Sync 设置' });
new Setting(containerEl)
.setName('SonicNote Sync 设置')
.setHeading();
// Sync folder
new Setting(containerEl)
.setName('同步文件夹')
.setDesc('录音 Markdown 文件存放的文件夹(相对于 Vault 根目录)')
.addText(text => text
.setPlaceholder('SonicNoteSync')
.setValue(settings.syncFolder)
.onChange(async (value) => {
settings.syncFolder = value;
await this.saveSettings();
}));
// Sync folder
new Setting(containerEl)
.setName('同步文件夹')
.setDesc('录音 Markdown 文件存放的文件夹(相对于 Vault 根目录)')
.addText(text => text
.setPlaceholder('SonicNoteSync')
.setValue(settings.syncFolder)
.onChange(async (value) => {
settings.syncFolder = value;
await this.saveSettings();
}));
// Include transcript
new Setting(containerEl)
.setName('包含转录内容')
.setDesc('关闭后同步的文件中不包含逐字转录内容')
.addToggle(toggle => toggle
.setValue(settings.includeTranscript)
.onChange(async (value) => {
settings.includeTranscript = value;
await this.saveSettings();
}));
// Include transcript
new Setting(containerEl)
.setName('包含转录内容')
.setDesc('关闭后同步的文件中不包含逐字转录内容')
.addToggle(toggle => toggle
.setValue(settings.includeTranscript)
.onChange(async (value) => {
settings.includeTranscript = value;
await this.saveSettings();
}));
// Auto sync on open
new Setting(containerEl)
.setName('启动时自动同步')
.setDesc('每次打开 Obsidian 时自动执行一次同步')
.addToggle(toggle => toggle
.setValue(settings.autoSyncOnOpen)
.onChange(async (value) => {
settings.autoSyncOnOpen = value;
await this.saveSettings();
}));
// Auto sync on open
new Setting(containerEl)
.setName('启动时自动同步')
.setDesc('每次打开 Obsidian 时自动执行一次同步')
.addToggle(toggle => toggle
.setValue(settings.autoSyncOnOpen)
.onChange(async (value) => {
settings.autoSyncOnOpen = value;
await this.saveSettings();
}));
// Resync interval
new Setting(containerEl)
.setName('定时重同步')
.setDesc('Obsidian 打开期间按指定间隔自动重新同步')
.addDropdown(dropdown => dropdown
.addOptions({
'0': '关闭(手动同步)',
'60': '每 1 小时',
'180': '每 3 小时',
'360': '每 6 小时',
'1440': '每 24 小时',
})
.setValue(String(settings.resyncIntervalMinutes))
.onChange(async (value) => {
settings.resyncIntervalMinutes = parseInt(value, 10);
await this.saveSettings();
const plugin = (this as any).plugin;
if (plugin?.startAutoSync) plugin.startAutoSync();
}));
// Resync interval
new Setting(containerEl)
.setName('定时重同步')
.setDesc('Obsidian 打开期间按指定间隔自动重新同步')
.addDropdown(dropdown => dropdown
.addOptions({
'0': '关闭(手动同步)',
'60': '每 1 小时',
'180': '每 3 小时',
'360': '每 6 小时',
'1440': '每 24 小时',
})
.setValue(String(settings.resyncIntervalMinutes))
.onChange(async (value) => {
settings.resyncIntervalMinutes = parseInt(value, 10);
await this.saveSettings();
const plugin = (this as unknown as { plugin: { startAutoSync: () => void } }).plugin;
if (plugin?.startAutoSync) plugin.startAutoSync();
}));
// Frontmatter fields toggles
const fmSection = containerEl.createDiv();
fmSection.createEl('h3', { text: '文件属性' });
fmSection.createEl('p', { text: '选择同步到 Frontmatter 中的属性字段', cls: 'setting-item-description' });
fmSection.style.marginBottom = '12px';
// Frontmatter fields toggles
new Setting(containerEl)
.setName('文件属性')
.setHeading();
const fmListEl = fmSection.createDiv();
const renderFrontmatterToggles = () => {
fmListEl.empty();
for (const [key, desc] of Object.entries(BUILTIN_FRONTMATTER_FIELDS)) {
const isRequired = key === 'audio_id' || key === 'sync_time';
if (isRequired) {
new Setting(fmListEl)
.setName(`${desc}`)
.setDesc(key)
.addText(text => {
text.setValue('必要属性').setDisabled(true);
text.inputEl.style.width = 'auto';
text.inputEl.style.color = 'var(--text-muted)';
text.inputEl.style.textAlign = 'center';
text.inputEl.style.border = 'none';
text.inputEl.style.background = 'var(--background-secondary)';
text.inputEl.style.borderRadius = '4px';
text.inputEl.style.padding = '2px 8px';
text.inputEl.style.fontSize = '0.8em';
});
} else {
new Setting(fmListEl)
.setName(`${desc}`)
.setDesc(key)
.addToggle(toggle => {
toggle.setValue(settings.frontmatterFields[key] !== false);
toggle.onChange(async (value) => {
settings.frontmatterFields[key] = value;
await this.saveSettings();
});
});
}
}
};
renderFrontmatterToggles();
new Setting(containerEl)
.setDesc('选择同步到 Frontmatter 中的属性字段')
.setName('');
// Custom frontmatter fields
const customSection = containerEl.createDiv();
customSection.createEl('h3', { text: '自定义属性' });
customSection.createEl('p', { text: '添加自定义属性到所有同步文件的 Frontmatter 中', cls: 'setting-item-description' });
customSection.style.marginBottom = '12px';
const fmListEl = containerEl.createDiv({ cls: 'sonicnote-fm-list' });
const renderFrontmatterToggles = () => {
fmListEl.empty();
for (const [key, desc] of Object.entries(BUILTIN_FRONTMATTER_FIELDS)) {
const isRequired = key === 'audio_id' || key === 'sync_time';
if (isRequired) {
new Setting(fmListEl)
.setName(`${desc}`)
.setDesc(key)
.addText(text => {
text.setValue('必要属性').setDisabled(true);
text.inputEl.addClass('sonicnote-required-badge');
});
} else {
new Setting(fmListEl)
.setName(`${desc}`)
.setDesc(key)
.addToggle(toggle => {
toggle.setValue(settings.frontmatterFields[key] !== false);
toggle.onChange(async (value) => {
settings.frontmatterFields[key] = value;
await this.saveSettings();
});
});
}
}
};
renderFrontmatterToggles();
const customListEl = customSection.createDiv();
const renderCustomFields = () => {
customListEl.empty();
// Custom frontmatter fields
new Setting(containerEl)
.setName('自定义属性')
.setHeading();
for (let i = 0; i < settings.customFrontmatter.length; i++) {
const field = settings.customFrontmatter[i];
new Setting(customListEl)
.addText(text => text
.setPlaceholder('属性名')
.setValue(field.key)
.onChange(async (value) => {
field.key = value;
await this.saveSettings();
}))
.addText(text => text
.setPlaceholder('属性值')
.setValue(field.value)
.onChange(async (value) => {
field.value = value;
await this.saveSettings();
}))
.addExtraButton(btn => btn
.setIcon('trash')
.setTooltip('删除')
.onClick(async () => {
settings.customFrontmatter.splice(i, 1);
await this.saveSettings();
renderCustomFields();
}));
}
};
renderCustomFields();
new Setting(containerEl)
.setDesc('添加自定义属性到所有同步文件的 Frontmatter 中')
.setName('');
new Setting(customSection)
.setName('添加属性')
.addButton(btn => btn
.setButtonText('+ 添加')
.onClick(async () => {
settings.customFrontmatter.push({ key: '', value: '' });
await this.saveSettings();
renderCustomFields();
}));
const customListEl = containerEl.createDiv();
const renderCustomFields = () => {
customListEl.empty();
// Login status & actions
const loginSection = containerEl.createDiv();
loginSection.createEl('h3', { text: '账号' });
for (let i = 0; i < settings.customFrontmatter.length; i++) {
const field = settings.customFrontmatter[i];
new Setting(customListEl)
.addText(text => text
.setPlaceholder('属性名')
.setValue(field.key)
.onChange(async (value) => {
field.key = value;
await this.saveSettings();
}))
.addText(text => text
.setPlaceholder('属性值')
.setValue(field.value)
.onChange(async (value) => {
field.value = value;
await this.saveSettings();
}))
.addExtraButton(btn => btn
.setIcon('trash')
.setTooltip('删除')
.onClick(async () => {
settings.customFrontmatter.splice(i, 1);
await this.saveSettings();
renderCustomFields();
}));
}
};
renderCustomFields();
if (this.api.isAuthenticated()) {
const maskedKey = settings.apiKey.length > 10
? settings.apiKey.slice(0, 10) + '...'
: settings.apiKey;
new Setting(loginSection)
.setName('登录状态')
.setDesc(`已登录: ${maskedKey}`)
.addButton(btn => btn
.setButtonText('登出')
.setWarning()
.onClick(async () => {
settings.token = '';
settings.apiKey = '';
await this.saveSettings();
new Notice('已登出');
this.display();
}));
} else {
new Setting(loginSection)
.setName('登录')
.setDesc('使用 API Key 登录 SonicNote')
.addButton(btn => btn
.setButtonText('登录')
.onClick(() => {
new LoginModal(this.app, this.api, this.getSettings, this.saveSettings, () => this.display()).open();
}));
}
}
new Setting(containerEl)
.setName('添加属性')
.addButton(btn => btn
.setButtonText('+ 添加')
.onClick(async () => {
settings.customFrontmatter.push({ key: '', value: '' });
await this.saveSettings();
renderCustomFields();
}));
// Login status & actions
new Setting(containerEl)
.setName('账号')
.setHeading();
if (this.api.isAuthenticated()) {
const maskedKey = settings.apiKey.length > 10
? settings.apiKey.slice(0, 10) + '...'
: settings.apiKey;
new Setting(containerEl)
.setName('登录状态')
.setDesc(`已登录: ${maskedKey}`)
.addButton(btn => btn
.setButtonText('登出')
.setWarning()
.onClick(async () => {
settings.token = '';
settings.apiKey = '';
await this.saveSettings();
new Notice('已登出');
this.display();
}));
} else {
new Setting(containerEl)
.setName('登录')
.setDesc('使用 API Key 登录 SonicNote')
.addButton(btn => btn
.setButtonText('登录')
.onClick(() => {
new LoginModal(this.app, this.api, this.getSettings, this.saveSettings, () => this.display()).open();
}));
}
}
}
class LoginModal extends Modal {
private api: SonicNoteApiClient;
private getSettings: () => SonicNotePluginSettings;
private saveSettings: () => Promise<void>;
private onCloseCallback: () => void;
private api: SonicNoteApiClient;
private getSettings: () => SonicNotePluginSettings;
private saveSettings: () => Promise<void>;
private onCloseCallback: () => void;
constructor(
app: App,
api: SonicNoteApiClient,
getSettings: () => SonicNotePluginSettings,
saveSettings: () => Promise<void>,
onCloseCallback: () => void
) {
super(app);
this.api = api;
this.getSettings = getSettings;
this.saveSettings = saveSettings;
this.onCloseCallback = onCloseCallback;
}
constructor(
app: App,
api: SonicNoteApiClient,
getSettings: () => SonicNotePluginSettings,
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();
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: '登录 SonicNote' });
new Setting(contentEl)
.setName('登录 SonicNote')
.setHeading();
let apiKey = '';
let apiKey = '';
new Setting(contentEl)
.setName('API Key')
.setDesc('在妙记 App → 我的 → API Key 管理中创建')
.addText(text => {
text
.setPlaceholder('sk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')
.onChange((value) => { apiKey = value; });
text.inputEl.type = 'password';
});
new Setting(contentEl)
.setName('API Key')
.setDesc('在妙记 App → 我的 → API Key 管理中创建')
.addText(text => {
text
.setPlaceholder('sk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')
.onChange((value) => { apiKey = value; });
text.inputEl.type = 'password';
});
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('登录')
.setCta()
.onClick(async () => {
if (!apiKey) {
new Notice('请输入 API Key');
return;
}
try {
btn.setButtonText('登录中...');
btn.setDisabled(true);
const result = await this.api.login(apiKey);
const settings = this.getSettings();
settings.token = result.token;
settings.apiKey = apiKey;
await this.saveSettings();
new Notice('登录成功');
this.close();
} catch (e) {
new Notice(`登录失败: ${e instanceof Error ? e.message : '未知错误'}`);
btn.setButtonText('登录');
btn.setDisabled(false);
}
}));
}
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('登录')
.setCta()
.onClick(async () => {
if (!apiKey) {
new Notice('请输入 API Key');
return;
}
try {
btn.setButtonText('登录中...');
btn.setDisabled(true);
const result = await this.api.login(apiKey);
const settings = this.getSettings();
settings.token = result.token;
settings.apiKey = apiKey;
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();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
this.onCloseCallback();
}
}

View file

@ -1,126 +1,126 @@
// ===== 插件设置 =====
export interface CustomFrontmatterField {
key: string;
value: string;
key: string;
value: string;
}
export interface SonicNotePluginSettings {
serverUrl: string;
syncFolder: string;
pageSize: number;
includeTranscript: boolean;
autoSyncOnOpen: boolean;
resyncIntervalMinutes: number;
frontmatterFields: Record<string, boolean>;
customFrontmatter: CustomFrontmatterField[];
token: string;
apiKey: string;
lastSyncTime: string;
serverUrl: string;
syncFolder: string;
pageSize: number;
includeTranscript: boolean;
autoSyncOnOpen: boolean;
resyncIntervalMinutes: number;
frontmatterFields: Record<string, boolean>;
customFrontmatter: CustomFrontmatterField[];
token: string;
apiKey: string;
lastSyncTime: string;
}
export const BUILTIN_FRONTMATTER_FIELDS: Record<string, string> = {
audio_id: '录音 ID',
record_name: '录音文件名',
record_nick_name: '录音标题',
duration: '时长',
record_time: '录音时间',
record_type: '录音类型',
device_name: '设备名称',
audio_url: '音频地址',
tags: '标签',
sync_time: '同步时间',
audio_id: '录音 ID',
record_name: '录音文件名',
record_nick_name: '录音标题',
duration: '时长',
record_time: '录音时间',
record_type: '录音类型',
device_name: '设备名称',
audio_url: '音频地址',
tags: '标签',
sync_time: '同步时间',
};
export const DEFAULT_SETTINGS: SonicNotePluginSettings = {
serverUrl: 'https://ainote.easylinkin.com:18048/prod-api',
syncFolder: 'SonicNoteSync',
pageSize: 50,
includeTranscript: true,
autoSyncOnOpen: false,
resyncIntervalMinutes: 0,
frontmatterFields: {
audio_id: true,
record_name: true,
record_nick_name: true,
duration: true,
record_time: true,
record_type: true,
device_name: true,
audio_url: true,
tags: true,
sync_time: true,
},
customFrontmatter: [],
token: '',
apiKey: '',
lastSyncTime: '',
serverUrl: 'https://ainote.easylinkin.com:18048/prod-api',
syncFolder: 'SonicNoteSync',
pageSize: 50,
includeTranscript: true,
autoSyncOnOpen: false,
resyncIntervalMinutes: 0,
frontmatterFields: {
audio_id: true,
record_name: true,
record_nick_name: true,
duration: true,
record_time: true,
record_type: true,
device_name: true,
audio_url: true,
tags: true,
sync_time: true,
},
customFrontmatter: [],
token: '',
apiKey: '',
lastSyncTime: '',
};
// ===== 后端数据类型 =====
export interface BackendResponse {
code: number;
msg: string;
data: any;
code: number;
msg: string;
data: unknown;
}
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;
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;
spokesperson: string;
text: string;
time: number;
}
export interface SummaryData {
summaryId: string;
audioId: string;
summaryContent: string;
templateId: string;
status: number;
summaryId: string;
audioId: string;
summaryContent: string;
templateId: string;
status: number;
}
export interface StudyReportData {
id: number;
audioId: string;
knowledgePanorama: string;
coreGains: string;
consolidation: string;
status: number;
id: number;
audioId: string;
knowledgePanorama: string;
coreGains: string;
consolidation: string;
status: number;
}
// ===== 同步相关 =====
export interface LocalFileInfo {
path: string;
syncTime: string;
recordNickName: string;
path: string;
syncTime: string;
recordNickName: string;
}
export interface SyncResult {
total: number;
synced: number;
skipped: number;
errors: number;
errorMessages: string[];
total: number;
synced: number;
skipped: number;
errors: number;
errorMessages: string[];
}

View file

@ -1,10 +1,13 @@
/* SonicNote Sync Plugin Styles */
/* Login modal */
.sonicnote-login-modal .setting-item {
padding: 8px 0;
}
.sonicnote-login-modal .setting-item-control input {
width: 200px;
/* Required field badge (replaces inline styles) */
.sonicnote-required-badge {
width: auto;
color: var(--text-muted);
text-align: center;
border: none;
background: var(--background-secondary);
border-radius: 4px;
padding: 2px 8px;
font-size: 0.8em;
}