llm-agent-plugin/main.ts
2026-04-30 18:59:57 +03:00

430 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf } 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';
import { WebSocketService } from './src/utils/WebSocketService';
import 'src/css/styles.css';
// ----------------------------------------------- Settings ---------------------------------------------------------------
interface LLMAgentSettings {
apiBaseUrl: string;
systemPrompt: string;
defaultModel: string;
promptsFolder: string;
enableFileReferences: boolean;
enablePromptReferences: boolean;
excludedFolders: string[];
refCacheFolder: string;
voiceModel: string;
voiceInterval: number;
voiceDisplayLimit: number; // 3000
voiceContextLimit: number; // N2 символов для LLM
promptRegularPath: string;
promptCommandsPath: string;
markerStart: string; // "старт"
markerStop: string; // "стоп"
}
const DEFAULT_SETTINGS: LLMAgentSettings = {
apiBaseUrl: 'http://localhost:5000/api',
systemPrompt: 'Ты полезный ИИ ассистент.',
defaultModel: 'gemini-2.5-flash-r',
promptsFolder: 'prompts', // Папка с промптами по умолчанию
enableFileReferences: true,
enablePromptReferences: true,
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], // Папка "templates" по умолчанию исключена
refCacheFolder: 'ref-cache', // По умолчанию относительно vault
voiceModel: '',
voiceInterval: 1,
voiceDisplayLimit: 3000,
voiceContextLimit: 3000,
promptRegularPath: '',
promptCommandsPath: '',
markerStart: 'старт',
markerStop: 'стоп'
};
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
export default class LLMAgentPlugin extends Plugin {
settings: LLMAgentSettings;
eventBus: EventBus;
webSocketService: WebSocketService;
activeStreamControllers: Map<string, AbortController>;
async syncVoiceSettings() {
try {
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemPrompt: this.settings.systemPrompt,
interval: this.settings.voiceInterval,
context_limit: this.settings.voiceContextLimit,
marker_start: this.settings.markerStart,
marker_stop: this.settings.markerStop,
// Передаем также пути к файлам промптов, если нужно
prompt_regular: this.settings.promptRegularPath,
prompt_commands: this.settings.promptCommandsPath
})
});
if (response.ok) {
console.log("LLM Agent: Voice settings synced with backend.");
}
} catch (e) {
console.error("LLM Agent: Failed to sync settings", e);
}
}
async onload() {
await this.loadSettings();
this.eventBus = new EventBus();
this.webSocketService = new WebSocketService(this.eventBus, this.settings.apiBaseUrl);
this.webSocketService.connect();
// Регистрируем кастомные views
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
// Добавляем команды
this.addCommand({
id: 'open-llm-agent-graph',
name: 'Открыть граф диалогов LLM Agent',
callback: () => this.activateGraphView()
});
this.addCommand({
id: 'open-llm-agent-chat',
name: 'Открыть чат LLM Agent',
callback: () => this.activateChatView()
});
this.addCommand({
id: 'new-llm-agent-chat',
name: 'Новый чат LLM Agent',
callback: () => this.handleNewChat()
});
// Добавляем иконку в ribbon
this.addRibbonIcon('brain-circuit', 'LLM Agent', () => {
this.activateGraphView();
this.activateChatView();
});
console.log('LLM Agent plugin загружен');
this.activeStreamControllers = new Map<string, AbortController>();
this.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
this.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// Вызываем один раз при запуске
await this.syncVoiceSettings();
}
onunload() {
if (this.webSocketService) {
this.webSocketService.disconnect();
}
this.activeStreamControllers.clear();
this.eventBus.off('stream-started', this.handleStreamStarted);
this.eventBus.off('stream-ended', this.handleStreamEnded);
console.log('LLM Agent plugin выгружен');
}
// ----------------------------------------------- Settings Management ---------------------------------------------------------------
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
// ----------------------------------------------- View Management ---------------------------------------------------------------
async activateGraphView() {
const existingLeaf = this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE)[0];
if (existingLeaf) {
this.app.workspace.revealLeaf(existingLeaf);
} else {
await this.app.workspace.getLeaf('tab').setViewState({
type: GRAPH_VIEW_TYPE,
active: true
});
}
}
async activateChatView() {
const existingLeaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0];
if (existingLeaf) {
this.app.workspace.revealLeaf(existingLeaf);
} else {
const rightLeaf = this.app.workspace.getRightLeaf(false);
if (rightLeaf) {
await rightLeaf.setViewState({
type: CHAT_VIEW_TYPE,
active: true
});
}
}
}
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
handleNewChat() {
this.eventBus.emit('new-chat');
}
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
this.activeStreamControllers.set(data.nodeId, data.controller);
}
handleStreamEnded(data: { nodeId: string }): void {
this.activeStreamControllers.delete(data.nodeId);
}
}
// ----------------------------------------------- Sample Setting Tab ---------------------------------------------------------------
class SampleSettingTab extends PluginSettingTab {
plugin: LLMAgentPlugin;
constructor(app: App, plugin: LLMAgentPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Основные настройки' });
new Setting(containerEl)
.setName('API Base URL')
.setDesc('Базовый URL для API LLM Agent')
.addText((text) =>
text
.setPlaceholder('http://localhost:5000/api')
.setValue(this.plugin.settings.apiBaseUrl)
.onChange(async (value) => {
this.plugin.settings.apiBaseUrl = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Системный промпт')
.setDesc('Этот промпт будет отправляться LLM перед каждым диалогом.')
.addTextArea((text) =>
text
.setPlaceholder('Ты полезный ИИ ассистент.')
.setValue(this.plugin.settings.systemPrompt)
.onChange(async (value) => {
this.plugin.settings.systemPrompt = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
new Setting(containerEl)
.setName('Интервал регулярного анализа (мин)')
.setDesc('Как часто LLM анализирует последние фразы (Prompt 1).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.voiceInterval))
.onChange(async (value) => {
this.plugin.settings.voiceInterval = Number(value);
await this.plugin.saveSettings();
await this.plugin.syncVoiceSettings();
})
);
new Setting(containerEl)
.setName('Объем контекста (символы)')
.setDesc('Сколько последних символов транскрибации отправлять в модель.')
.addText((text) =>
text
.setValue(String(this.plugin.settings.voiceContextLimit))
.onChange(async (value) => {
this.plugin.settings.voiceContextLimit = Number(value);
await this.plugin.saveSettings();
await this.plugin.syncVoiceSettings();
})
);
new Setting(containerEl)
.setName('Маркер начала команды')
.setDesc('Слово, после которого текст считается командой (Response 2).')
.addText((text) =>
text
.setPlaceholder('старт')
.setValue(this.plugin.settings.markerStart)
.onChange(async (value) => {
this.plugin.settings.markerStart = value;
await this.plugin.saveSettings();
await this.plugin.syncVoiceSettings();
})
);
new Setting(containerEl)
.setName('Маркер конца команды')
.setDesc('Слово, завершающее захват голосовой команды.')
.addText((text) =>
text
.setPlaceholder('стоп')
.setValue(this.plugin.settings.markerStop)
.onChange(async (value) => {
this.plugin.settings.markerStop = value;
await this.plugin.saveSettings();
await this.plugin.syncVoiceSettings();
})
);
new Setting(containerEl)
.setName('Регулярный промпт')
.setDesc('Инструкции для Промпта №1.')
.addTextArea((text) =>
text
// .setPlaceholder('prompts/regular_analysis.md')
.setValue(this.plugin.settings.promptRegularPath)
.onChange(async (value) => {
this.plugin.settings.promptRegularPath = value;
await this.plugin.saveSettings();
await this.plugin.syncVoiceSettings();
})
);
new Setting(containerEl)
.setName('Промпт команд')
.setDesc('Инструкции для Промпта №2.')
.addTextArea((text) =>
text
// .setPlaceholder('prompts/voice_commands.md')
.setValue(this.plugin.settings.promptCommandsPath)
.onChange(async (value) => {
this.plugin.settings.promptCommandsPath = value;
await this.plugin.saveSettings();
await this.plugin.syncVoiceSettings();
})
);
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
new Setting(containerEl)
.setName('Папка с промптами')
.setDesc('Путь к папке, содержащей промпты для команд #/##')
.addText((text) =>
text
.setPlaceholder('prompts')
.setValue(this.plugin.settings.promptsFolder)
.onChange(async (value) => {
this.plugin.settings.promptsFolder = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Папка кеша для вложений')
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
.addText((text) =>
text
.setPlaceholder('ref-cache')
.setValue(this.plugin.settings.refCacheFolder)
.onChange(async (value) => {
this.plugin.settings.refCacheFolder = value;
await this.plugin.saveSettings();
})
);
// Настройки ссылок на файлы
new Setting(containerEl)
.setName('Включить ссылки на файлы')
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
this.plugin.settings.enableFileReferences = value;
await this.plugin.saveSettings();
})
);
// Настройки ссылок на промпты
new Setting(containerEl)
.setName('Включить ссылки на промпты')
.setDesc('Разрешить использование #/## ссылок на промпты')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enablePromptReferences).onChange(async (value) => {
this.plugin.settings.enablePromptReferences = value;
await this.plugin.saveSettings();
})
);
// Новое поле для настройки исключенных папок
new Setting(containerEl)
.setName('Исключенные папки для ссылок')
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
this.plugin.settings.enableFileReferences = value;
await this.plugin.saveSettings();
})
);
// Динамический список исключенных папок
containerEl.createEl('h3', { text: 'Исключенные папки' });
this.plugin.settings.excludedFolders.forEach((folder, index) => {
new Setting(containerEl)
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
.addText((text) => {
text.setPlaceholder('Путь к папке (например, templates/)' + index)
.setValue(folder)
.onChange(async (value) => {
this.plugin.settings.excludedFolders[index] = value;
await this.plugin.saveSettings();
// Очищаем кэш автокомплита при изменении настроек
this.plugin.eventBus.emit('settings-changed');
});
})
.addExtraButton((button) => {
button
.setIcon('x')
.setTooltip('Удалить папку')
.onClick(async () => {
this.plugin.settings.excludedFolders.splice(index, 1);
await this.plugin.saveSettings();
this.display(); // Перерисовать настройки для обновления списка
// Очищаем кэш автокомплита при изменении настроек
this.plugin.eventBus.emit('settings-changed');
});
});
});
new Setting(containerEl).addButton((button) => {
button
.setButtonText('Добавить папку')
.setIcon('plus')
.onClick(async () => {
this.plugin.settings.excludedFolders.push(''); // Добавляем пустое поле
await this.plugin.saveSettings();
this.display(); // Перерисовать настройки для обновления списка
});
});
}
}