Support linking audio-logs with @ in chat

This commit is contained in:
dimitrievgs 2026-05-03 22:21:15 +03:00
parent 9ae3256c35
commit f66dd0920b
3 changed files with 105 additions and 24 deletions

33
main.ts
View File

@ -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 { EventBus } from './src/utils/EventBus';
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView'; import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView'; import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
@ -24,6 +24,7 @@ interface LLMAgentSettings {
summarizationModel: string; summarizationModel: string;
voiceCommandModel: string; voiceCommandModel: string;
logsFolder: string;
voiceInterval: number; voiceInterval: number;
voiceDisplayLimit: number; // 3000 voiceDisplayLimit: number; // 3000
voiceContextLimit: number; // N2 символов для LLM voiceContextLimit: number; // N2 символов для LLM
@ -43,10 +44,12 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], // Папка "templates" по умолчанию исключена excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], // Папка "templates" по умолчанию исключена
refCacheFolder: 'ref-cache', // По умолчанию относительно vault refCacheFolder: 'ref-cache', // По умолчанию относительно vault
voiceModel: '', summarizationModel: '',
voiceCommandModel: '',
logsFolder: 'audio-logs',
voiceInterval: 1, voiceInterval: 1,
voiceDisplayLimit: 3000, voiceDisplayLimit: 30000,
voiceContextLimit: 3000, voiceContextLimit: 30000,
promptRegular: '', promptRegular: '',
promptCommands: '', promptCommands: '',
markerStart: 'старт', markerStart: 'старт',
@ -74,6 +77,13 @@ export default class LLMAgentPlugin extends Plugin {
settingsToSync.promptRegular = await engine.processRawSettingValue(this.settings.promptRegular); settingsToSync.promptRegular = await engine.processRawSettingValue(this.settings.promptRegular);
settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands); 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`, { const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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) new Setting(containerEl)
.setName('Интервал регулярного анализа (мин)') .setName('Интервал регулярного анализа (мин)')
.setDesc('Как часто LLM анализирует последние фразы (Prompt 1).') .setDesc('Как часто LLM анализирует последние фразы (Prompt 1).')

View File

@ -45,22 +45,36 @@ export class AutocompleteManager {
* @returns массив файлов для автокомплита * @returns массив файлов для автокомплита
*/ */
async getFileOptions(query: string): Promise<AutocompleteItem[]> { async getFileOptions(query: string): Promise<AutocompleteItem[]> {
if (!this.plugin.settings.enableFileReferences) { if (!this.plugin.settings.enableFileReferences) return [];
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(); await this.updateCacheIfNeeded();
const allFiles = this.fileCache.get('all') || []; 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 [...virtualItems, ...filteredFiles].slice(0, AutocompleteManager.MAX_TOTAL_ITEMS);
return allFiles
.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery))
.slice(0, AutocompleteManager.MAX_TOTAL_ITEMS);
} }
/** /**

View File

@ -3,7 +3,7 @@
* Обрабатывает как одинарные (@/#), так и постоянные (@@/##) ссылки. * Обрабатывает как одинарные (@/#), так и постоянные (@@/##) ссылки.
*/ */
import { TFile } from 'obsidian'; import { FileSystemAdapter, TFile } from 'obsidian';
import LLMAgentPlugin from 'main'; import LLMAgentPlugin from 'main';
import { FileReference, ReferenceParser } from './ReferenceParser'; import { FileReference, ReferenceParser } from './ReferenceParser';
import { TemplateEngine } from './TemplateEngine'; import { TemplateEngine } from './TemplateEngine';
@ -79,7 +79,8 @@ export class ReferenceExpander {
}*/ }*/
/** /**
* Извлекает вложения из текста и кеширует файлы * Извлекает вложения из текста и кеширует файлы.
* Обрабатывает как физические файлы, так и виртуальные (current-log).
*/ */
async extractAttachments( async extractAttachments(
text: string, text: string,
@ -97,15 +98,56 @@ export class ReferenceExpander {
let mimeType: string; let mimeType: string;
let sourcePath: 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); const cacheResult = await this.cacheManager.cacheExternalFile(reference.path);
cachedPath = cacheResult.cachedPath; cachedPath = cacheResult.cachedPath;
uuid = cacheResult.uuid; uuid = cacheResult.uuid;
mimeType = cacheResult.mimeType; mimeType = cacheResult.mimeType;
sourcePath = reference.path; // Абсолютный sourcePath = reference.path; // Абсолютный
} else if (reference.type === 'file') { }
// ИЗМЕНЕНО: Кешируем файл из vault // --- Обработка файлов из Vault (@ / @@) ---
else if (reference.type === 'file') {
const file = this.plugin.app.vault.getAbstractFileByPath(reference.path); const file = this.plugin.app.vault.getAbstractFileByPath(reference.path);
if (file instanceof TFile) { if (file instanceof TFile) {
const cacheResult = await this.cacheManager.cacheVaultFile(file); const cacheResult = await this.cacheManager.cacheVaultFile(file);
@ -117,9 +159,9 @@ export class ReferenceExpander {
console.warn(`Файл не найден в vault: ${reference.path}`); console.warn(`Файл не найден в vault: ${reference.path}`);
continue; continue;
} }
} else { }
// prompt // --- Обработка промптов (# / ##) ---
// Кешируем промпт else {
const file = this.plugin.app.vault.getAbstractFileByPath(reference.path); const file = this.plugin.app.vault.getAbstractFileByPath(reference.path);
if (file instanceof TFile) { if (file instanceof TFile) {
@ -144,7 +186,7 @@ export class ReferenceExpander {
mimeType: mimeType mimeType: mimeType
}); });
} catch (error) { } catch (error) {
console.error(`Не удалось закешировать ${reference.name}:`, error); console.error(`Не удалось обработать вложение ${reference.name}:`, error);
} }
} }