fix(obsidian): provider check — precise error, no Notice after unload, keep unknown data.json keys, settings button (M-3, M-4, M-5, O-6, #7)

M-3: only readBinary is in try/catch, so 'Не удалось прочитать' is about reading only; transcribe() never throws. M-4: after each await the check returns silently if the plugin was unloaded. M-5: saveSettings writes {...data loaded from data.json, openai}, other top-level keys survive. O-6: 'Проверить' button in the settings tab runs the same check as the command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-10 19:24:40 +04:00
parent 9144427bba
commit 543af13c1f
2 changed files with 24 additions and 8 deletions

View File

@ -12,6 +12,7 @@ interface VoiceNoteData {
// провайдера и команда его проверки. // провайдера и команда его проверки.
export default class VoiceNotePlugin extends Plugin { export default class VoiceNotePlugin extends Plugin {
override settings: VoiceNoteData = { openai: readSettings(undefined) }; override settings: VoiceNoteData = { openai: readSettings(undefined) };
private data: Record<string, unknown> = {};
private readonly controller = new AbortController(); private readonly controller = new AbortController();
private transcriber = new OpenAiTranscriber(obsidianHttp, () => this.settings.openai, Math.random); private transcriber = new OpenAiTranscriber(obsidianHttp, () => this.settings.openai, Math.random);
@ -30,16 +31,20 @@ export default class VoiceNotePlugin extends Plugin {
} }
async loadSettings(): Promise<void> { async loadSettings(): Promise<void> {
const data = (await this.loadData()) as { openai?: unknown } | null; const data: unknown = await this.loadData();
this.settings = { openai: readSettings(data?.openai) }; this.data = data !== null && typeof data === 'object' && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
this.settings = { openai: readSettings(this.data.openai) };
} }
// Ключи data.json, которых плагин пока не знает, сохраняются как были при загрузке.
async saveSettings(): Promise<void> { async saveSettings(): Promise<void> {
await this.saveData(this.settings); await this.saveData({ ...this.data, ...this.settings });
} }
// Только уведомление: заметки, frontmatter и очередь не трогаются. // Только уведомление: заметки, frontmatter и очередь не трогаются.
private async checkTranscriber(): Promise<void> { // После выгрузки плагина (signal.aborted) уведомлений не показывает.
async checkTranscriber(): Promise<void> {
const signal = this.controller.signal;
const files = this.app.vault.getFiles(); const files = this.app.vault.getFiles();
const picked = pickCheckAudio(files.map((f) => f.path)); const picked = pickCheckAudio(files.map((f) => f.path));
const file = picked && files.find((f) => f.path === picked.path); const file = picked && files.find((f) => f.path === picked.path);
@ -48,12 +53,16 @@ export default class VoiceNotePlugin extends Plugin {
return; return;
} }
new Notice(`Проверка провайдера: ${picked.path}…`); new Notice(`Проверка провайдера: ${picked.path}…`);
let audio: ArrayBuffer;
try { try {
const audio = await this.app.vault.readBinary(file); audio = await this.app.vault.readBinary(file);
const result = await this.transcriber.transcribe(audio, picked.mime, this.controller.signal);
new Notice(checkNotice(result), 10_000);
} catch (e) { } catch (e) {
new Notice(`Не удалось прочитать ${picked.path}: ${e instanceof Error ? e.message : String(e)}`, 10_000); if (!signal.aborted) new Notice(`Не удалось прочитать ${picked.path}: ${e instanceof Error ? e.message : String(e)}`, 10_000);
} return;
}
if (signal.aborted) return;
// transcribe() не бросает: любая ошибка приходит в result.
const result = await this.transcriber.transcribe(audio, picked.mime, signal);
if (!signal.aborted) new Notice(checkNotice(result), 10_000);
} }
} }

View File

@ -1,4 +1,5 @@
import { type App, PluginSettingTab, Setting } from 'obsidian'; import { type App, PluginSettingTab, Setting } from 'obsidian';
import { SOURCE_DIR } from '../core/naming';
import type { OpenAiSettings } from '../core/openai'; import type { OpenAiSettings } from '../core/openai';
import type VoiceNotePlugin from '../main'; import type VoiceNotePlugin from '../main';
@ -18,6 +19,12 @@ export class VoiceNoteSettingTab extends PluginSettingTab {
this.field('Модель', 'Например, gpt-4o-transcribe', 'model'); this.field('Модель', 'Например, gpt-4o-transcribe', 'model');
this.field('API-ключ', 'Хранится только в data.json плагина и синхронизируется вместе с vault', 'apiKey', true); this.field('API-ключ', 'Хранится только в data.json плагина и синхронизируется вместе с vault', 'apiKey', true);
this.field('Язык', 'Код языка (ru, en). Пусто — модель определяет язык сама', 'language'); this.field('Язык', 'Код языка (ru, en). Пусто — модель определяет язык сама', 'language');
new Setting(containerEl)
.setName('Проверка провайдера')
.setDesc(
`Отправит последнюю запись из ${SOURCE_DIR}/ и покажет результат в уведомлении. То же — команда «Проверить провайдер распознавания».`,
)
.addButton((button) => button.setButtonText('Проверить').onClick(() => void this.plugin.checkTranscriber()));
} }
private field(name: string, desc: string, key: keyof OpenAiSettings, secret = false): void { private field(name: string, desc: string, key: keyof OpenAiSettings, secret = false): void {