diff --git a/main.ts b/main.ts index d2fd3fb..42ae434 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,4 @@ -import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf, TFile } from 'obsidian'; +import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf, TFile, FileSystemAdapter } from 'obsidian'; import { EventBus } from './src/utils/EventBus'; import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView'; import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView'; @@ -24,6 +24,7 @@ interface LLMAgentSettings { summarizationModel: string; voiceCommandModel: string; + logsFolder: string; voiceInterval: number; voiceDisplayLimit: number; // 3000 voiceContextLimit: number; // N2 символов для LLM @@ -43,10 +44,12 @@ const DEFAULT_SETTINGS: LLMAgentSettings = { excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], // Папка "templates" по умолчанию исключена refCacheFolder: 'ref-cache', // По умолчанию относительно vault - voiceModel: '', + summarizationModel: '', + voiceCommandModel: '', + logsFolder: 'audio-logs', voiceInterval: 1, - voiceDisplayLimit: 3000, - voiceContextLimit: 3000, + voiceDisplayLimit: 30000, + voiceContextLimit: 30000, promptRegular: '', promptCommands: '', markerStart: 'старт', @@ -74,6 +77,13 @@ export default class LLMAgentPlugin extends Plugin { settingsToSync.promptRegular = await engine.processRawSettingValue(this.settings.promptRegular); settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands); + const adapter = this.app.vault.adapter; + if (adapter instanceof FileSystemAdapter) { + // Соединяем базовый путь хранилища и относительный путь к логам + settingsToSync.absoluteLogsPath = adapter.getBasePath() + + (this.settings.logsFolder ? '/' + this.settings.logsFolder : ''); + } + const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -310,6 +320,21 @@ class SampleSettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName('Папка аудио-логов') + .setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.') + .addText((text) => + text + .setPlaceholder('audio-logs') + .setValue(this.plugin.settings.logsFolder) + .onChange(async (value) => { + // Убираем лишние слэши для корректности пути + this.plugin.settings.logsFolder = value.replace(/^\/+|\/+$/g, '').trim(); + await this.plugin.saveSettings(); + await this.plugin.syncSettings(); + }) + ); + new Setting(containerEl) .setName('Интервал регулярного анализа (мин)') .setDesc('Как часто LLM анализирует последние фразы (Prompt 1).') diff --git a/src/references/AutocompleteManager.ts b/src/references/AutocompleteManager.ts index e4a4fee..2ca170d 100644 --- a/src/references/AutocompleteManager.ts +++ b/src/references/AutocompleteManager.ts @@ -45,22 +45,36 @@ export class AutocompleteManager { * @returns массив файлов для автокомплита */ async getFileOptions(query: string): Promise { - if (!this.plugin.settings.enableFileReferences) { - return []; + if (!this.plugin.settings.enableFileReferences) return []; + + const lowercaseQuery = query.toLowerCase(); + const virtualItems: AutocompleteItem[] = []; + + // Пункт 1: Если в запросе "cur", добавляем логи в начало + if ("current-log".includes(lowercaseQuery)) { + virtualItems.push({ + name: "current-log", + path: "virtual:current-log", + type: "file", // Для применения иконки @ по умолчанию + icon: "file-text" + }); + virtualItems.push({ + name: "current-log:part", + path: "virtual:current-log:part", + type: "file", + icon: "file-text" + }); } await this.updateCacheIfNeeded(); - const allFiles = this.fileCache.get('all') || []; - if (!query.trim()) { - return allFiles.slice(0, AutocompleteManager.MAX_TOTAL_ITEMS); // Показываем первые 10 файлов - } + // Объединяем: виртуальные вначале, потом поиск по файлам + const filteredFiles = !query.trim() + ? allFiles + : allFiles.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery)); - const lowercaseQuery = query.toLowerCase(); - return allFiles - .filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery)) - .slice(0, AutocompleteManager.MAX_TOTAL_ITEMS); + return [...virtualItems, ...filteredFiles].slice(0, AutocompleteManager.MAX_TOTAL_ITEMS); } /** diff --git a/src/references/ReferenceExpander.ts b/src/references/ReferenceExpander.ts index 26711a9..77cec55 100644 --- a/src/references/ReferenceExpander.ts +++ b/src/references/ReferenceExpander.ts @@ -3,7 +3,7 @@ * Обрабатывает как одинарные (@/#), так и постоянные (@@/##) ссылки. */ -import { TFile } from 'obsidian'; +import { FileSystemAdapter, TFile } from 'obsidian'; import LLMAgentPlugin from 'main'; import { FileReference, ReferenceParser } from './ReferenceParser'; import { TemplateEngine } from './TemplateEngine'; @@ -79,7 +79,8 @@ export class ReferenceExpander { }*/ /** - * Извлекает вложения из текста и кеширует файлы + * Извлекает вложения из текста и кеширует файлы. + * Обрабатывает как физические файлы, так и виртуальные (current-log). */ async extractAttachments( text: string, @@ -97,15 +98,56 @@ export class ReferenceExpander { let mimeType: string; let sourcePath: string; - if (reference.type === 'external') { - // Кешируем внешний файл + // --- Обработка виртуальных ссылок на логи --- + if (reference.path.startsWith("virtual:current-log")) { + const logsFolder = this.plugin.settings.logsFolder || "audio-logs"; + // Ищем md файлы в папке логов, сортируем по дате изменения (самый свежий — первый) + const logFiles = this.plugin.app.vault.getMarkdownFiles() + .filter(f => f.path.startsWith(logsFolder)) + .sort((a, b) => b.stat.mtime - a.stat.mtime); + + if (logFiles.length > 0) { + const file = logFiles[0]; + let content = await this.plugin.app.vault.read(file); + + // Если выбрана часть, отрезаем последние N символов из настроек + if (reference.path.endsWith(":part")) { + const limit = this.plugin.settings.voiceContextLimit; // || 30000; + content = content.slice(-limit); + } + + uuid = "log-" + Date.now(); + cachedPath = `log-snapshot-${Date.now()}.md`; + mimeType = "text/markdown"; + sourcePath = file.path; + + // Сохраняем контент лога во временный файл в папку кеша, чтобы бэкенд мог его прочитать + if (this.plugin.app.vault.adapter instanceof FileSystemAdapter) { + const cacheFolderPath = this.cacheManager.getCacheFolderPath(); + const fs = window.require('fs').promises; + + // Убедимся, что папка кеша существует + try { await fs.mkdir(cacheFolderPath, { recursive: true }); } catch(e) {} + + await fs.writeFile(`${cacheFolderPath}/${cachedPath}`, content, 'utf8'); + } else { + throw new Error("Кеширование логов доступно только в десктопной версии"); + } + } else { + console.warn(`Файлы логов не найдены в папке: ${logsFolder}`); + continue; + } + } + // --- Обработка внешних файлов ($ / !$) --- + else if (reference.type === 'external') { const cacheResult = await this.cacheManager.cacheExternalFile(reference.path); cachedPath = cacheResult.cachedPath; uuid = cacheResult.uuid; mimeType = cacheResult.mimeType; sourcePath = reference.path; // Абсолютный - } else if (reference.type === 'file') { - // ИЗМЕНЕНО: Кешируем файл из vault + } + // --- Обработка файлов из Vault (@ / @@) --- + else if (reference.type === 'file') { const file = this.plugin.app.vault.getAbstractFileByPath(reference.path); if (file instanceof TFile) { const cacheResult = await this.cacheManager.cacheVaultFile(file); @@ -117,9 +159,9 @@ export class ReferenceExpander { console.warn(`Файл не найден в vault: ${reference.path}`); continue; } - } else { - // prompt - // Кешируем промпт + } + // --- Обработка промптов (# / ##) --- + else { const file = this.plugin.app.vault.getAbstractFileByPath(reference.path); if (file instanceof TFile) { @@ -144,7 +186,7 @@ export class ReferenceExpander { mimeType: mimeType }); } catch (error) { - console.error(`Не удалось закешировать ${reference.name}:`, error); + console.error(`Не удалось обработать вложение ${reference.name}:`, error); } }