430 lines
15 KiB
TypeScript
430 lines
15 KiB
TypeScript
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(); // Перерисовать настройки для обновления списка
|
||
});
|
||
});
|
||
}
|
||
}
|