Debug vault websocket events
This commit is contained in:
parent
5f7f07ee6f
commit
44d996e7a4
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
|
|
|
|||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint", // Проверка ошибок в коде
|
||||
"esbenp.prettier-vscode", // Автоматическое форматирование кода
|
||||
"ms-vscode.js-debug", // Современный отладчик (встроен в VS Code, но важен для JS/TS)
|
||||
"visualstudioexptteam.vscodeintellicode", // Умные подсказки при написании кода
|
||||
"marp-team.marp-vscode", // Если работаете с презентациями в Markdown
|
||||
"christian-kohler.path-intellisense" // Автодополнение путей к файлам (удобно для импортов)
|
||||
]
|
||||
}
|
||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"name": "Attach to Obsidian",
|
||||
"port": 9222,
|
||||
"address": "localhost",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"sourceMaps": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"sourceMapPathOverrides": {
|
||||
"../llm-agent-plugin/*": "${workspaceFolder}/*",
|
||||
"lib/*": "${workspaceFolder}/lib/*",
|
||||
"src/*": "${workspaceFolder}/src/*",
|
||||
"webpack:///./src/*": "${workspaceFolder}/src/*",
|
||||
"esbuild:///*": "${workspaceFolder}/*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
46
DEBUGGING.md
Normal file
46
DEBUGGING.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Инструкция по отладке плагина Obsidian
|
||||
|
||||
Из-за особенностей Obsidian (загрузка плагинов через `eval()`), прямая привязка точек останова (Breakpoints) в интерфейсе VS Code часто невозможна. Рекомендуется использовать встроенные инструменты разработчика Obsidian (Chrome DevTools).
|
||||
|
||||
## 1. Подготовка
|
||||
Убедитесь, что плагин запущен в режиме разработки. Это вшивает карты кода (Source Maps) прямо в файл, позволяя видеть TypeScript вместо скомпилированного JavaScript.
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 2. Способ «debugger» (рекомендуемый)
|
||||
Самый надежный способ остановиться в нужном месте кода:
|
||||
1. В VS Code добавьте ключевое слово `debugger;` в нужную строку:
|
||||
```typescript
|
||||
async onload() {
|
||||
debugger; // Выполнение остановится здесь
|
||||
console.log("Plugin loaded");
|
||||
}
|
||||
```
|
||||
2. В Obsidian откройте консоль разработчика: `Ctrl + Shift + I`.
|
||||
3. Перезагрузите плагин (выключите и включите в настройках или нажмите `Ctrl + R` в окне консоли).
|
||||
4. Obsidian автоматически замерит на этой строке и откроет вкладку **Sources** с вашим оригинальным `.ts` файлом.
|
||||
|
||||
## 3. Визуальные точки останова в Obsidian
|
||||
Если вы не хотите менять код:
|
||||
1. Откройте консоль (`Ctrl + Shift + I`) в Obsidian.
|
||||
2. **ВАЖНО**: Кликните по окну консоли и нажмите `Ctrl + R` (или `Cmd + R`) для полной перезагрузки интерфейса. Это очистит кэш Source Maps.
|
||||
3. Перейдите на вкладку **Sources**.
|
||||
4. В левой панели навигатора прокрутите в самый низ до появления папки `file://` или виртуальной папки с исходниками вашего проекта.
|
||||
5. Там будет доступна чистая структура папки `src/` со всеми оригинальными `.ts` файлами. Вы можете открывать их и ставить синие точки останова кликом по номеру строки.
|
||||
|
||||
## 4. Быстрый поиск файлов (Ctrl + P)
|
||||
Самый верный способ найти ваш файл, если дерево не отображается:
|
||||
1. Кликните в любом месте инструментов разработчика Obsidian.
|
||||
2. Нажмите `Ctrl + P` (или `Cmd + P`).
|
||||
3. Введите имя файла, например `main.ts` — он должен появиться в списке с пометкой `[sm]` (source map).
|
||||
|
||||
## 5. Если плагин все равно не виден в дереве Sources
|
||||
Запустите в консоли Obsidian:
|
||||
```javascript
|
||||
app.plugins.plugins["llm-agent-plugin"].constructor
|
||||
```
|
||||
Кликните по результату (ссылке на функцию), и DevTools сам откроет нужный файл исходного кода.
|
||||
|
||||
## Вспомогательное подключение VS Code
|
||||
Файл `.vscode/launch.json` настроен так, что вы можете нажать **F5**, чтобы видеть логи Obsidian прямо в **Debug Console** VS Code. Это удобно для чтения ошибок, но ставить точки останова через VS Code все равно не получится — используйте методы выше.
|
||||
310
Readme.md
310
Readme.md
|
|
@ -1,271 +1,69 @@
|
|||
## Запрос
|
||||
# LLM Agent Plugin для Obsidian
|
||||
|
||||
Хочу перенести всё это в Obsidian, сделав его плагином (для личного пользования). ПРи этом в средней колонке, где граф отображаетс яштатный обсидиана или замтеки - тм будет react-flow граф с историей взаимдействия (деревом диалогов), где можно быстро будет попросить сгенерировать картинки и одновременно сразу нагенерить в разных ветках , например, локации и персонажей нескольких для НРИ (однорвременно запустить друг за другом запросы в разных ветках и пусть обрабатываются) впараллельно, я на графе всё увижу). А сам чат чтобы вёлся справа в колонк, гнде теги , бэклинки, проперти или Copilot помощник из известного плагина. сначала скажи можно ли перенести и чтобы бы ты посоветовал (пока без кода), может , предлоишь что-то улучшить? Да, граф в пространстве, где новые заметки открываются, он просто открывается там, в новом табе, где новые заметикти открываются ли графы обсидианы. Сделай перенос. Пусть бэкенд, как есть, а фронт перенеси полностью
|
||||
|
||||
# LLM Agent для Obsidian
|
||||
|
||||
Плагин Obsidian для работы с LLM агентом, включающий граф диалогов и интерфейс чата с поддержкой слеш-команд.
|
||||
Интеграция LLM (Large Language Models) в Obsidian с визуализацией диалогов в виде графа и интерактивным чатом.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
ttrpg-obsidian-vault/
|
||||
├── .obsidian/
|
||||
│ └── plugins/
|
||||
│ └── llm-agent-plugin/
|
||||
│ ├── main.js
|
||||
│ ├── manifest.json
|
||||
│ ├── styles.css
|
||||
│ └── src/
|
||||
│ ├── views/
|
||||
│ │ ├── GraphView.js
|
||||
│ │ └── ChatView.js
|
||||
│ ├── components/
|
||||
│ │ ├── ChatPanel.js
|
||||
│ │ ├── CommandInput.js
|
||||
│ │ ├── GraphPanel.js
|
||||
│ │ └── HistoryPanel.js
|
||||
│ └── utils/
|
||||
│ └── EventBus.js
|
||||
├── llm-agent-backend/ # Backend на Python
|
||||
│ ├── env/ # Виртуальное окружение Python
|
||||
│ ├── app/ # Код приложения
|
||||
│ │ ├── api.py
|
||||
│ │ ├── workflows.py
|
||||
│ │ └── ...
|
||||
│ ├── requirements.txt
|
||||
│ └── run.py # Точка входа
|
||||
└── env/
|
||||
```text
|
||||
llm-agent-plugin/
|
||||
├── src/
|
||||
│ ├── components/ # React-компоненты интерфейса
|
||||
│ │ ├── models/ # Модели данных для компонентов
|
||||
│ │ ├── ChatPanel.ts # Панель основного чата
|
||||
│ │ ├── GraphPanel.ts # Панель визуализации графа (React Flow)
|
||||
│ │ ├── HistoryPanel.ts # Панель истории сообщений
|
||||
│ │ └── VoicePanel.ts # Компонент голосового управления
|
||||
│ ├── views/ # Представления (Views) Obsidian
|
||||
│ │ ├── ChatView.ts # Правая панель чата
|
||||
│ │ └── GraphView.ts # Центральная панель графа
|
||||
│ ├── utils/ # Вспомогательные утилиты и сервисы
|
||||
│ │ ├── WebSocketService.ts # Связь с Python бэкендом
|
||||
│ │ ├── EventBus.ts # Шина событий для обмена данными
|
||||
│ │ ├── fileUtils.ts # Инструменты работы с файлами vault
|
||||
│ │ └── VaultQueryHandler.ts # Поиск и фильтрация данных в Obsidian
|
||||
│ ├── references/ # Система упоминаний и связей
|
||||
│ │ ├── AutocompleteManager.ts # Логика выпадающего списка
|
||||
│ │ ├── ReferenceParser.ts # Парсинг [[ссылок]] в сообщениях
|
||||
│ │ └── TemplateEngine.ts # Генерация контента по шаблонам
|
||||
│ ├── modals/ # Всплывающие окна
|
||||
│ │ └── GraphSettingsModal.ts # Настройки визуализации
|
||||
│ ├── constants.ts # Глобальные константы и настройки
|
||||
│ ├── types/ # TypeScript определения и интерфейсы
|
||||
│ ├── css/ # Стили компонентов
|
||||
│ ├── json/ # Конфигурационные JSON файлы
|
||||
│ └── docs/ # Техническая документация и спецификации
|
||||
├── main.ts # Точка входа: инициализация плагина и команд
|
||||
├── mainBasic.ts # Упрощенная версия плагина
|
||||
├── package.json # Зависимости и скрипты сборки (npm)
|
||||
├── esbuild.config.mjs # Конфигурация сборщика esbuild
|
||||
└── manifest.json # Манифест плагина Obsidian
|
||||
```
|
||||
|
||||
## Установка и запуск
|
||||
## Установка
|
||||
|
||||
### 1. Настройка Backend
|
||||
1. **Предварительные требования**:
|
||||
- Установленный [Node.js](https://nodejs.org/) (рекомендуется LTS).
|
||||
- Запущенный [LLM Backend](https://github.com/user/llm-agent-backend) (по умолчанию на порту 5000).
|
||||
|
||||
1. Перейдите в папку backend:
|
||||
2. **Сборка**:
|
||||
```bash
|
||||
cd llm-agent-backend
|
||||
npm install # Установка зависимостей
|
||||
npm run build # Сборка проекта (создает main.js)
|
||||
```
|
||||
|
||||
2. Активируйте виртуальное окружение:
|
||||
```bash
|
||||
# Windows
|
||||
env\Scripts\activate
|
||||
|
||||
# Linux/macOS
|
||||
source env/bin/activate
|
||||
```
|
||||
3. **Активация в Obsidian**:
|
||||
- Поместите папку `llm-agent-plugin` в `.obsidian/plugins/` вашего хранилища.
|
||||
- В настройках Obsidian ("Community Plugins") включите "LLM Agent".
|
||||
|
||||
3. Установите зависимости:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
## Использование
|
||||
|
||||
4. Запустите backend сервер:
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
- **Граф диалогов**: Открывается через иконку 🧠 на левой панели. Позволяет видеть ветвление мыслей LLM.
|
||||
- **Интерактивный чат**: Находится в правой боковой панели. Поддерживает слеш-команды (`/imagine`, `/analyze`).
|
||||
- **Система ссылок**: Используйте `[[название заметки]]` в чате для передачи контекста ваших файлов в LLM.
|
||||
- **Голосовой ввод**: Нажмите иконку микрофона для диктовки сообщений.
|
||||
|
||||
Backend будет доступен на `http://localhost:5000`
|
||||
## Технические особенности
|
||||
|
||||
### 2. Установка плагина в Obsidian
|
||||
|
||||
1. **Автоматическая установка (если плагин уже находится в правильной папке):**
|
||||
- Откройте Obsidian с этим хранилищем
|
||||
- Перейдите в Настройки → Плагины сообщества
|
||||
- Включите "Небезопасные плагины" (если требуется)
|
||||
- Найдите "LLM Agent" в списке установленных плагинов
|
||||
- Включите плагин
|
||||
|
||||
2. **Ручная установка (если нужно скопировать файлы):**
|
||||
```bash
|
||||
# Убедитесь, что файлы плагина находятся в:
|
||||
.obsidian/plugins/llm-agent-plugin/
|
||||
├── main.js
|
||||
├── manifest.json
|
||||
├── styles.css
|
||||
└── src/
|
||||
```
|
||||
|
||||
### 3. Использование
|
||||
|
||||
1. **Запуск интерфейса:**
|
||||
- Нажмите иконку 🧠 в левой панели Obsidian
|
||||
- Или используйте команду "Открыть граф диалогов LLM Agent" (Ctrl+P)
|
||||
|
||||
2. **Доступные слеш-команды:**
|
||||
- `/help` - Список доступных команд
|
||||
- `/imagine [промпт]` - Генерация изображений
|
||||
- `/analyze [url] [промпт]` - Анализ изображения
|
||||
- `/subtitles_meet` - Получение субтитров Google Meet
|
||||
- `/subtitles_teams` - Получение субтитров MS Teams
|
||||
- `/summarize` - Суммаризация истории диалога
|
||||
- `/chat` - Обычный диалог с LLM
|
||||
|
||||
3. **Работа с графом:**
|
||||
- Граф отображается в центральной области Obsidian
|
||||
- Чат и история - в правой боковой панели
|
||||
- Кликайте по узлам графа для навигации по веткам диалога
|
||||
- Используйте "Новый чат" для создания нового диалога
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### Backend API URL
|
||||
|
||||
По умолчанию плагин использует `http://localhost:5000/api`. Для изменения:
|
||||
|
||||
1. Откройте файл `.obsidian/plugins/llm-agent-plugin/main.js`
|
||||
2. Найдите строку:
|
||||
```javascript
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiBaseUrl: 'http://localhost:5000/api'
|
||||
};
|
||||
```
|
||||
3. Измените URL на нужный
|
||||
|
||||
### LLM модели
|
||||
|
||||
Конфигурация моделей находится в `backend/llm-agent-backend/app/llm_client.py`:
|
||||
|
||||
```python
|
||||
MODELS = {
|
||||
"gemini-2.5-flash": {
|
||||
"apiBase": "https://your-api-endpoint.com",
|
||||
"apiKey": "your-api-key",
|
||||
# ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Устранение неполадок
|
||||
|
||||
### Backend не запускается
|
||||
|
||||
1. Проверьте, что виртуальное окружение активировано
|
||||
2. Убедитесь, что все зависимости установлены:
|
||||
```bash
|
||||
pip install flask flask-cors langgraph langchain-core requests
|
||||
```
|
||||
3. Проверьте порт 5000 - он должен быть свободен
|
||||
|
||||
### Плагин не появляется в Obsidian
|
||||
|
||||
1. Убедитесь, что файлы находятся в правильной папке:
|
||||
```
|
||||
.obsidian/plugins/llm-agent-plugin/main.js
|
||||
```
|
||||
2. Проверьте, что включены "Плагины сообщества" в настройках
|
||||
3. Перезапустите Obsidian
|
||||
|
||||
### Ошибки подключения к API
|
||||
|
||||
1. Убедитесь, что backend запущен на порту 5000
|
||||
2. Проверьте консоль разработчика в Obsidian (Ctrl+Shift+I)
|
||||
3. Проверьте настройки CORS в `backend/llm-agent-backend/app/api.py`
|
||||
|
||||
### Граф не отображается
|
||||
|
||||
1. Текущая версия использует упрощенную визуализацию
|
||||
2. Для полноценного React Flow потребуется дополнительная настройка bundler'а
|
||||
|
||||
## Разработка
|
||||
|
||||
### Структура кода плагина
|
||||
|
||||
- `main.js` - Основной класс плагина
|
||||
- `src/views/` - Views для Obsidian (GraphView, ChatView)
|
||||
- `src/components/` - React-like компоненты
|
||||
- `src/utils/` - Утилиты (EventBus)
|
||||
|
||||
### Отладка
|
||||
|
||||
1. Откройте консоль разработчика в Obsidian (Ctrl+Shift+I)
|
||||
2. Логи плагина доступны в консоли
|
||||
3. Backend логи выводятся в терминал где запущен сервер
|
||||
|
||||
### Добавление новых команд
|
||||
|
||||
1. Добавьте команду в `backend/llm-agent-backend/app/nodes.py` в `CommandManager`
|
||||
2. Создайте соответствующий узел обработки
|
||||
3. Обновите маршрутизацию в `workflows.py`
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Основные эндпоинты
|
||||
|
||||
- `POST /api/chat` - Отправка сообщения
|
||||
- `GET /api/graphs` - Список всех графов
|
||||
- `GET /api/graphs/{id}` - Данные конкретного графа
|
||||
- `DELETE /api/graphs/{id}` - Удаление графа
|
||||
- `GET /api/messages/{graph_id}/{node_id}` - Сообщения от корня до узла
|
||||
|
||||
## Лицензия
|
||||
|
||||
Для личного использования.
|
||||
```
|
||||
|
||||
**`backend/llm-agent-backend/requirements.txt`:**
|
||||
```txt
|
||||
flask==2.3.3
|
||||
flask-cors==4.0.0
|
||||
langgraph==0.0.40
|
||||
langchain-core==0.1.45
|
||||
requests==2.31.0
|
||||
sqlite3
|
||||
openai==1.3.0
|
||||
```
|
||||
|
||||
**`backend/llm-agent-backend/run.py`:**
|
||||
```python
|
||||
"""
|
||||
Точка входа для запуска LLM Agent backend сервера.
|
||||
Запускает Flask приложение с настройками для разработки.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем папку app в Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
||||
|
||||
from api import app2
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Запуск LLM Agent Backend сервера...")
|
||||
print("📍 API будет доступно на: http://localhost:5000/api")
|
||||
print("📊 Список графов: http://localhost:5000/api/graphs")
|
||||
print("💬 Чат эндпоинт: http://localhost:5000/api/chat")
|
||||
print("\n⚠️ Для остановки нажмите Ctrl+C\n")
|
||||
|
||||
app2.run(
|
||||
debug=True,
|
||||
port=5000,
|
||||
host='localhost'
|
||||
)
|
||||
```
|
||||
|
||||
**`backend/llm-agent-backend/app/__init__.py`:**
|
||||
```python
|
||||
# Пустой файл для создания Python пакета
|
||||
```
|
||||
|
||||
## Быстрый старт:
|
||||
|
||||
1. **Запуск backend:**
|
||||
```bash
|
||||
cd backend/llm-agent-backend
|
||||
env\Scripts\activate # Windows
|
||||
python run.py
|
||||
```
|
||||
|
||||
2. **Запуск Obsidian:**
|
||||
- Откройте хранилище в Obsidian
|
||||
- Включите плагин "LLM Agent" в настройках
|
||||
- Нажмите иконку 🧠 в левой панели
|
||||
|
||||
3. **Проверка работы:**
|
||||
- В правой панели должна появиться история диалогов
|
||||
- В центре - область для графа
|
||||
- Отправьте сообщение `/help` для проверки
|
||||
|
||||
**Важно:** Backend должен быть запущен перед использованием плагина. При первом запуске может потребоваться установка дополнительных зависимостей.
|
||||
- **Связь**: Использует WebSocket для real-time обновления графа и стриминга ответов.
|
||||
- **UI**: Построен на базе React и React Flow для отрисовки сложных структур графов.
|
||||
- **Интеграция**: Глубокая интеграция с API Obsidian для доступа к метаданным файлов и поиску.
|
||||
|
|
|
|||
3
debug-obsidian.bat
Normal file
3
debug-obsidian.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@echo off
|
||||
start "" "%LocalAppData%\Obsidian\Obsidian.exe" --remote-debugging-port=9222
|
||||
exit
|
||||
69
main.ts
69
main.ts
|
|
@ -8,6 +8,7 @@ import { AutocompleteManager } from './src/references/AutocompleteManager';
|
|||
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
|
||||
import { ReferenceParser } from './src/references/ReferenceParser';
|
||||
import { CustomTool } from './src/components/models/ChatHistoryItem';
|
||||
import { VaultQueryHandler } from 'src/utils/VaultQueryHandler';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import 'src/css/styles.css';
|
||||
|
|
@ -129,6 +130,8 @@ export default class LLMAgentPlugin extends Plugin {
|
|||
activeStreamControllers: Map<string, AbortController>;
|
||||
availableModels: string[];
|
||||
|
||||
vaultQueryHandler: VaultQueryHandler | undefined;
|
||||
|
||||
async syncSettings() {
|
||||
try {
|
||||
const engine = new TemplateEngine(this);
|
||||
|
|
@ -161,6 +164,9 @@ export default class LLMAgentPlugin extends Plugin {
|
|||
|
||||
const adapter = this.app.vault.adapter;
|
||||
if (adapter instanceof FileSystemAdapter) {
|
||||
const vaultPath = adapter.getBasePath();
|
||||
settingsToSync.vaultAbsolutePath = vaultPath;
|
||||
|
||||
// Соединяем базовый путь хранилища и относительный путь к логам
|
||||
settingsToSync.absoluteLogsPath = adapter.getBasePath() +
|
||||
(this.settings.logsFolder ? '/' + this.settings.logsFolder : '');
|
||||
|
|
@ -205,6 +211,41 @@ export default class LLMAgentPlugin extends Plugin {
|
|||
this.webSocketService = new WebSocketService(this.eventBus, this.settings.apiBaseUrl);
|
||||
this.webSocketService.connect();
|
||||
|
||||
// Инициализируем обработчик vault-запросов от бэкенда
|
||||
this.vaultQueryHandler = new VaultQueryHandler(this.app);
|
||||
|
||||
// Регистрируем хук: бэкенд просит Obsidian выполнить операцию с vault
|
||||
this.webSocketService.onServerEvent(
|
||||
'vault_query_request',
|
||||
async (data: { request_id: string; type: string; payload: Record<string, any> }) => {
|
||||
const { request_id, type, payload } = data;
|
||||
console.log(`🔍 vault_query_request: type=${type}, id=${request_id}`);
|
||||
|
||||
try {
|
||||
const result = await this.vaultQueryHandler.handle({
|
||||
request_id,
|
||||
type,
|
||||
payload
|
||||
});
|
||||
|
||||
this.webSocketService.emit('vault_query_response', {
|
||||
request_id,
|
||||
result
|
||||
});
|
||||
|
||||
console.log(`✅ vault_query_response отправлен: id=${request_id}`);
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.message ?? String(e);
|
||||
console.error(`❌ vault_query_request ошибка: ${errorMsg}`);
|
||||
|
||||
this.webSocketService.emit('vault_query_response', {
|
||||
request_id,
|
||||
error: errorMsg
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Регистрируем кастомные views
|
||||
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
|
||||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
|
||||
|
|
@ -644,11 +685,31 @@ class SampleSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
const scriptSetting = new Setting(toolDiv)
|
||||
.setName('Скрипт и Метод')
|
||||
.setDesc('путь/скрипт.py:название_метода (Используйте @)');
|
||||
const scriptRow = toolDiv.createDiv({ attr: { style: 'display: flex; gap: 10px; align-items: flex-end;' } });
|
||||
|
||||
const scriptSetting = new Setting(scriptRow)
|
||||
.setName('Скрипт')
|
||||
.setDesc('Путь к .py (используйте @)');
|
||||
scriptSetting.settingEl.style.flex = '3';
|
||||
scriptSetting.settingEl.style.border = 'none';
|
||||
this.addPromptAutocompleteStrict(scriptSetting, tool, 'scriptPath', 'py');
|
||||
|
||||
const methodSetting = new Setting(scriptRow)
|
||||
.setName('Метод')
|
||||
.setDesc('Имя функции')
|
||||
.addText(t => t
|
||||
.setPlaceholder('') // 'main'
|
||||
.setValue(tool.methodName || '') // 'main'
|
||||
.onChange(async v => {
|
||||
tool.methodName = v.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
methodSetting.settingEl.style.flex = '1';
|
||||
methodSetting.settingEl.style.border = 'none';
|
||||
methodSetting.infoEl.style.display = 'none'; // Скроем описание метода для компактности
|
||||
|
||||
|
||||
const formatSetting = new Setting(toolDiv)
|
||||
.setName('Форматирующий промпт (Пост-процессинг)')
|
||||
.setDesc('Стиль ответа, прикладываемый к результату работы скрипта (Используйте #)');
|
||||
|
|
@ -656,7 +717,7 @@ class SampleSettingTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
new Setting(container).addButton(b => b.setButtonText('➕ Добавить новый инструмент').setCta().onClick(async () => {
|
||||
this.plugin.settings.customTools.push({ id: uuidv4(), name: 'New_Tool', description: '', scriptPath: '', hiddenPrompt: '' });
|
||||
this.plugin.settings.customTools.push({ id: uuidv4(), name: 'New_Tool', description: '', scriptPath: '', methodName: '', hiddenPrompt: '' }); // methodName: 'main'
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface CustomTool {
|
|||
name: string;
|
||||
description: string;
|
||||
scriptPath: string;
|
||||
methodName?: string;
|
||||
hiddenPrompt: string;
|
||||
hiddenPromptExpanded?: string;
|
||||
}
|
||||
423
src/utils/VaultQueryHandler.ts
Normal file
423
src/utils/VaultQueryHandler.ts
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
// src/utils/VaultQueryHandler.ts
|
||||
/**
|
||||
* VaultQueryHandler — обработчик запросов к Obsidian vault от бэкенда.
|
||||
*
|
||||
* Бэкенд (Flask) отправляет событие 'vault_query_request' через WebSocket.
|
||||
* Этот класс выполняет операцию внутри Obsidian и возвращает результат
|
||||
* обратно через 'vault_query_response'.
|
||||
*
|
||||
* Поддерживаемые типы запросов:
|
||||
* get_frontmatter — YAML + тело одного файла
|
||||
* get_all_factions — все файлы в папке (с фильтром по тегу)
|
||||
* get_recent_files — последние N файлов (с фильтром по тегам)
|
||||
* resolve_wikilink — wikilink → путь к файлу (с поддержкой aliases)
|
||||
* write_frontmatter — перезаписать YAML + тело файла
|
||||
* create_file — создать или перезаписать файл
|
||||
*/
|
||||
|
||||
import { App, TFile, TFolder } from 'obsidian';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Типы запросов и ответов
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface VaultQueryRequest {
|
||||
request_id: string;
|
||||
type: string;
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
interface GetFrontmatterResult {
|
||||
path: string;
|
||||
name: string;
|
||||
frontmatter: Record<string, any>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface GetAllFactionsResult {
|
||||
path: string;
|
||||
name: string;
|
||||
frontmatter: Record<string, any>;
|
||||
}
|
||||
|
||||
interface WriteResult {
|
||||
success: boolean;
|
||||
path: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Класс
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class VaultQueryHandler {
|
||||
constructor(private app: App) {}
|
||||
|
||||
/**
|
||||
* Главная точка входа. Диспетчеризует запрос по типу.
|
||||
* Вызывается из main.ts при получении 'vault_query_request'.
|
||||
*/
|
||||
async handle(req: VaultQueryRequest): Promise<any> {
|
||||
const { type, payload } = req;
|
||||
|
||||
switch (type) {
|
||||
case 'get_frontmatter':
|
||||
return this._getFrontmatter(payload.path as string);
|
||||
|
||||
case 'get_all_factions':
|
||||
return this._getAllFilesInFolder(
|
||||
payload.folder as string,
|
||||
payload.tag_filter as string | undefined
|
||||
);
|
||||
|
||||
case 'get_recent_files':
|
||||
return this._getRecentFiles(
|
||||
payload.folder as string,
|
||||
(payload.limit as number) ?? 10,
|
||||
(payload.tags as string[]) ?? []
|
||||
);
|
||||
|
||||
case 'resolve_wikilink':
|
||||
return this._resolveWikilink(payload.name as string);
|
||||
|
||||
case 'write_frontmatter':
|
||||
return this._writeFrontmatter(
|
||||
payload.path as string,
|
||||
payload.frontmatter as Record<string, any>,
|
||||
payload.body as string
|
||||
);
|
||||
|
||||
case 'create_file':
|
||||
return this._createFile(
|
||||
payload.path as string,
|
||||
payload.content as string
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(`VaultQueryHandler: неизвестный тип запроса '${type}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// get_frontmatter
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Возвращает YAML frontmatter и тело (body) одного .md файла.
|
||||
* Использует кэш метаданных Obsidian — не читает файл с диска лишний раз.
|
||||
*/
|
||||
private async _getFrontmatter(path: string): Promise<GetFrontmatterResult> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (!(file instanceof TFile)) {
|
||||
throw new Error(`Файл не найден: ${path}`);
|
||||
}
|
||||
|
||||
// Кэш метаданных — быстро, без I/O
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter: Record<string, any> = { ...(cache?.frontmatter ?? {}) };
|
||||
|
||||
// Удаляем служебное поле Obsidian
|
||||
delete frontmatter['position'];
|
||||
|
||||
// Тело файла нужно для записи обратно (сохраняем лор-текст нетронутым)
|
||||
const raw = await this.app.vault.read(file);
|
||||
const body = this._extractBody(raw);
|
||||
|
||||
return {
|
||||
path: file.path,
|
||||
name: file.basename,
|
||||
frontmatter,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// get_all_factions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Возвращает все .md файлы из папки (рекурсивно).
|
||||
* Опциональный tag_filter: "#faction" — оставит только файлы с этим тегом.
|
||||
* Использует только кэш метаданных — быстро на 2000+ файлах.
|
||||
*/
|
||||
private _getAllFilesInFolder(
|
||||
folderPath: string,
|
||||
tagFilter?: string
|
||||
): GetAllFactionsResult[] {
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!(folder instanceof TFolder)) {
|
||||
console.warn(`VaultQueryHandler: папка не найдена: ${folderPath}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: GetAllFactionsResult[] = [];
|
||||
|
||||
// Нормализуем фильтр: "faction" → "#faction"
|
||||
const normalizedFilter = tagFilter
|
||||
? tagFilter.startsWith('#') ? tagFilter : `#${tagFilter}`
|
||||
: null;
|
||||
|
||||
const collect = (f: TFolder) => {
|
||||
for (const child of f.children) {
|
||||
if (child instanceof TFile && child.extension === 'md') {
|
||||
const cache = this.app.metadataCache.getFileCache(child);
|
||||
const frontmatter: Record<string, any> = { ...(cache?.frontmatter ?? {}) };
|
||||
delete frontmatter['position'];
|
||||
|
||||
if (normalizedFilter) {
|
||||
// Теги могут быть в cache.tags (инлайн #tag) и в frontmatter.tags (список)
|
||||
const inlineTags = (cache?.tags ?? []).map(t => t.tag);
|
||||
const fmTags: string[] = (frontmatter.tags ?? []).map((t: string) =>
|
||||
t.startsWith('#') ? t : `#${t}`
|
||||
);
|
||||
const allTags = [...inlineTags, ...fmTags];
|
||||
|
||||
if (!allTags.includes(normalizedFilter)) continue;
|
||||
}
|
||||
|
||||
results.push({
|
||||
path: child.path,
|
||||
name: child.basename,
|
||||
frontmatter
|
||||
});
|
||||
} else if (child instanceof TFolder) {
|
||||
collect(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
collect(folder);
|
||||
return results;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// get_recent_files
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Возвращает последние N файлов из папки, отсортированных по дате изменения.
|
||||
* tags — массив тегов для фильтрации (хотя бы один должен совпасть).
|
||||
* Теги сравниваются без учёта # и регистра.
|
||||
*/
|
||||
private _getRecentFiles(
|
||||
folderPath: string,
|
||||
limit: number,
|
||||
tags: string[]
|
||||
): GetAllFactionsResult[] {
|
||||
const all = this._getAllFilesInFolder(folderPath);
|
||||
|
||||
let filtered = all;
|
||||
if (tags.length > 0) {
|
||||
// Нормализуем фильтр-теги для сравнения
|
||||
const normalizedFilterTags = tags.map(t =>
|
||||
t.replace(/^#/, '').toLowerCase()
|
||||
);
|
||||
|
||||
filtered = all.filter(item => {
|
||||
const fmTags: string[] = (item.frontmatter.tags ?? []).map((t: string) =>
|
||||
t.replace(/^#/, '').toLowerCase()
|
||||
);
|
||||
return normalizedFilterTags.some(ft => fmTags.includes(ft));
|
||||
});
|
||||
}
|
||||
|
||||
// Сортируем по mtime файла (свежие первые)
|
||||
const withMtime = filtered.map(item => {
|
||||
const file = this.app.vault.getAbstractFileByPath(item.path);
|
||||
const mtime = file instanceof TFile ? file.stat.mtime : 0;
|
||||
return { ...item, mtime };
|
||||
});
|
||||
|
||||
withMtime.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
return withMtime
|
||||
.slice(0, limit)
|
||||
.map(({ mtime: _mtime, ...rest }) => rest);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// resolve_wikilink
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Резолвит wikilink-имя в путь к файлу.
|
||||
* Obsidian metadataCache знает об aliases — работает корректно.
|
||||
* Принимает "Waterdeep" или "[[Waterdeep]]" или "Waterdeep|Город Плащей".
|
||||
*
|
||||
* Возвращает строку-путь или null если файл не найден.
|
||||
*/
|
||||
private _resolveWikilink(name: string): string | null {
|
||||
// Чистим: убираем [[ ]], берём часть до |
|
||||
const clean = name
|
||||
.replace(/^\[\[/, '')
|
||||
.replace(/\]\]$/, '')
|
||||
.split('|')[0]
|
||||
.trim();
|
||||
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(clean, '');
|
||||
return file ? file.path : null;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// write_frontmatter
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Перезаписывает файл: сериализует frontmatter в YAML и сохраняет с телом.
|
||||
* Тело (body) берётся из payload — Python присылает его обратно без изменений.
|
||||
*
|
||||
* YAML сериализуется вручную через простую функцию — не тянем js-yaml,
|
||||
* т.к. нужна только сериализация плоских структур (строки, числа, списки).
|
||||
*/
|
||||
private async _writeFrontmatter(
|
||||
path: string,
|
||||
frontmatter: Record<string, any>,
|
||||
body: string
|
||||
): Promise<WriteResult> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (!(file instanceof TFile)) {
|
||||
throw new Error(`Файл не найден для записи: ${path}`);
|
||||
}
|
||||
|
||||
const yamlStr = this._serializeYaml(frontmatter);
|
||||
const newContent = `---\n${yamlStr}---\n\n${body.trim()}\n`;
|
||||
|
||||
await this.app.vault.modify(file, newContent);
|
||||
return { success: true, path };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// create_file
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Создаёт новый файл или перезаписывает существующий.
|
||||
* Автоматически создаёт промежуточные папки.
|
||||
*/
|
||||
private async _createFile(
|
||||
path: string,
|
||||
content: string
|
||||
): Promise<WriteResult> {
|
||||
// Создаём папки если нужно
|
||||
const folderPath = path.includes('/')
|
||||
? path.substring(0, path.lastIndexOf('/'))
|
||||
: null;
|
||||
|
||||
if (folderPath) {
|
||||
const folderExists = this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folderExists) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
const existing = this.app.vault.getAbstractFileByPath(path);
|
||||
if (existing instanceof TFile) {
|
||||
await this.app.vault.modify(existing, content);
|
||||
} else {
|
||||
await this.app.vault.create(path, content);
|
||||
}
|
||||
|
||||
return { success: true, path };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Вспомогательные методы
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Извлекает тело файла — всё что после закрывающего ---.
|
||||
*/
|
||||
private _extractBody(raw: string): string {
|
||||
const match = raw.match(/^---[\s\S]*?---\n?([\s\S]*)$/);
|
||||
return match ? match[1].trim() : raw.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Простая сериализация объекта в YAML-строку.
|
||||
* Поддерживает: строки, числа, булевы, null, списки строк/чисел,
|
||||
* вложенные объекты (один уровень — для assets).
|
||||
*
|
||||
* Не использует внешние библиотеки.
|
||||
* Obsidian имеет js-yaml глобально, но его API нестабильно между версиями.
|
||||
*/
|
||||
private _serializeYaml(obj: Record<string, any>, indent = 0): string {
|
||||
const pad = ' '.repeat(indent);
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === null || value === undefined) {
|
||||
lines.push(`${pad}${key}:`);
|
||||
} else if (typeof value === 'boolean') {
|
||||
lines.push(`${pad}${key}: ${value}`);
|
||||
} else if (typeof value === 'number') {
|
||||
lines.push(`${pad}${key}: ${value}`);
|
||||
} else if (typeof value === 'string') {
|
||||
// Строки с двоеточием, решёткой или спецсимволами — в кавычки
|
||||
if (this._needsQuoting(value)) {
|
||||
const escaped = value.replace(/"/g, '\\"');
|
||||
lines.push(`${pad}${key}: "${escaped}"`);
|
||||
} else {
|
||||
lines.push(`${pad}${key}: ${value}`);
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
lines.push(`${pad}${key}: []`);
|
||||
} else if (value.every(v => typeof v !== 'object' || v === null)) {
|
||||
// Простой список скаляров
|
||||
lines.push(`${pad}${key}:`);
|
||||
for (const item of value) {
|
||||
if (typeof item === 'string' && this._needsQuoting(item)) {
|
||||
lines.push(`${pad} - "${item.replace(/"/g, '\\"')}"`);
|
||||
} else {
|
||||
lines.push(`${pad} - ${item}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Список объектов (assets)
|
||||
lines.push(`${pad}${key}:`);
|
||||
for (const item of value) {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
const entries = Object.entries(item);
|
||||
if (entries.length === 0) {
|
||||
lines.push(`${pad} - {}`);
|
||||
continue;
|
||||
}
|
||||
const [firstKey, firstVal] = entries[0];
|
||||
const firstValStr = this._scalarToYaml(firstVal);
|
||||
lines.push(`${pad} - ${firstKey}: ${firstValStr}`);
|
||||
for (const [k, v] of entries.slice(1)) {
|
||||
lines.push(`${pad} ${k}: ${this._scalarToYaml(v)}`);
|
||||
}
|
||||
} else {
|
||||
lines.push(`${pad} - ${item}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
// Вложенный объект (travel-distance, nested maps)
|
||||
lines.push(`${pad}${key}:`);
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
const keyStr = this._needsQuoting(k) ? `"${k}"` : k;
|
||||
lines.push(`${pad} ${keyStr}: ${this._scalarToYaml(v)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
private _scalarToYaml(value: any): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'boolean') return String(value);
|
||||
if (typeof value === 'number') return String(value);
|
||||
if (typeof value === 'string') {
|
||||
if (this._needsQuoting(value)) return `"${value.replace(/"/g, '\\"')}"`;
|
||||
return value;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
private _needsQuoting(s: string): boolean {
|
||||
// Квотируем если: содержит : # [ ] { } , & * ? | > ' " % @ ` или начинается с пробела/цифры в некоторых контекстах
|
||||
return /[:#\[\]{},&*?|>'"@`]/.test(s) || s.trim() !== s || s === '';
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,13 @@ export class WebSocketService {
|
|||
private socket: Socket | null = null;
|
||||
private eventBus: EventBus;
|
||||
private apiBaseUrl: string;
|
||||
//private receivedEvents: Set<string> = new Set(); // Для отслеживания полученных событий
|
||||
//private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 10;
|
||||
private pingIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// Прямые подписчики на серверные события (минуя EventBus).
|
||||
// Используется для vault_query_request и других низкоуровневых хуков.
|
||||
private serverEventHandlers: Map<string, (data: any) => void> = new Map();
|
||||
|
||||
constructor(eventBus: EventBus, apiBaseUrl: string) {
|
||||
this.eventBus = eventBus;
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
|
|
@ -53,6 +55,15 @@ export class WebSocketService {
|
|||
private setupEventHandlers(): void {
|
||||
if (!this.socket) return;
|
||||
|
||||
// Снимаем все старые обработчики перед навешиванием новых
|
||||
this.socket.off('connect');
|
||||
this.socket.off('disconnect');
|
||||
this.socket.off('connect_error');
|
||||
this.socket.off('node_type_changed');
|
||||
this.socket.off('graph_title_updated');
|
||||
this.socket.off('node_title_updated');
|
||||
this.socket.off('vault_query_request');
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('✅ WebSocket подключен');
|
||||
});
|
||||
|
|
@ -65,7 +76,7 @@ export class WebSocketService {
|
|||
console.error('Ошибка подключения WebSocket:', error);
|
||||
});
|
||||
|
||||
// ----------------------------------------------- Nodes Update Events ---------------------------------------------------------------
|
||||
// ------- Nodes Update Events -------
|
||||
|
||||
this.socket.on(
|
||||
'node_type_changed',
|
||||
|
|
@ -104,6 +115,39 @@ export class WebSocketService {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ------- Vault Query Hook -------
|
||||
// Бэкенд просит Obsidian выполнить запрос к vault (чтение файлов, резолв wikilinks и т.д.)
|
||||
|
||||
this.socket.on('vault_query_request', (data: any) => {
|
||||
console.log('📡 WebSocket получено: vault_query_request', data?.type, data?.request_id);
|
||||
const handler = this.serverEventHandlers.get('vault_query_request');
|
||||
if (handler) {
|
||||
handler(data);
|
||||
} else {
|
||||
console.warn('⚠️ vault_query_request получен, но обработчик не зарегистрирован.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрирует обработчик для конкретного серверного события.
|
||||
* Используется для низкоуровневых хуков (vault_query_request и т.п.).
|
||||
* В отличие от EventBus — один обработчик на событие (последний побеждает).
|
||||
*/
|
||||
onServerEvent(event: string, handler: (data: any) => void): void {
|
||||
this.serverEventHandlers.set(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет событие на сервер через WebSocket.
|
||||
*/
|
||||
emit(event: string, data: any): void {
|
||||
if (!this.socket?.connected) {
|
||||
console.warn(`⚠️ WebSocket не подключён, событие '${event}' не отправлено.`);
|
||||
return;
|
||||
}
|
||||
this.socket.emit(event, data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -128,38 +172,37 @@ export class WebSocketService {
|
|||
/**
|
||||
* Запускает проактивный пинг для проверки соединения.
|
||||
*/
|
||||
private startProactivePing(): void {
|
||||
this.stopProactivePing(); // Убедимся, что предыдущий интервал остановлен
|
||||
this.pingIntervalId = setInterval(() => {
|
||||
if (this.socket && this.socket.connected) {
|
||||
console.log('Client sending proactive ping...');
|
||||
let ackTimeout = setTimeout(() => {
|
||||
console.warn('Client: No pong response received for proactive ping within timeout. Forcing reconnect.');
|
||||
this.disconnect();
|
||||
this.connect();
|
||||
}, 3000); // Таймаут ожидания ответа на пинг от сервера (3 секунды)
|
||||
private startProactivePing(): void {
|
||||
this.stopProactivePing();
|
||||
this.pingIntervalId = setInterval(() => {
|
||||
// Если сокет не подключён — Socket.IO сам разберётся,
|
||||
// не вмешиваемся принудительно
|
||||
if (!this.socket?.connected) {
|
||||
console.log('Client proactive ping: socket not connected, skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Используем callback для получения подтверждения от сервера
|
||||
this.socket.emit('client_proactive_ping', (response: { status: string }) => {
|
||||
clearTimeout(ackTimeout);
|
||||
if (response && response.status === 'pong') {
|
||||
console.log('Client received proactive pong.');
|
||||
} else {
|
||||
console.warn('Client: Invalid pong response or no response for proactive ping. Forcing reconnect.');
|
||||
this.disconnect();
|
||||
this.connect();
|
||||
}
|
||||
});
|
||||
} else if (this.socket && !this.socket.connected) { // Здесь убираем '&& !this.socket.connecting'
|
||||
// Если сокет не подключен, Socket.IO должен сам управлять переподключением,
|
||||
// так как reconnection: true. Ручной вызов connect() может быть избыточным.
|
||||
console.log('Client proactive ping: Socket not connected. Relying on Socket.IO\'s auto-reconnect logic.');
|
||||
// Если вы все же хотите агрессивно вызвать this.connect() при каждом обнаружении отключения,
|
||||
// оставьте следующую строку:
|
||||
this.connect();
|
||||
}
|
||||
}, 5000); // Проверять каждые 5 секунд
|
||||
}
|
||||
const ackTimeout = setTimeout(() => {
|
||||
console.warn('Pong timeout — forcing reconnect.');
|
||||
// Используем socket.disconnect() + socket.connect()
|
||||
// вместо полного пересоздания сокета
|
||||
this.socket?.disconnect();
|
||||
this.socket?.connect();
|
||||
}, 3000);
|
||||
|
||||
this.socket.emit('client_proactive_ping', (response: { status: string }) => {
|
||||
clearTimeout(ackTimeout);
|
||||
if (!response || response.status !== 'pong') {
|
||||
console.warn('Invalid pong, forcing reconnect.');
|
||||
this.socket?.disconnect();
|
||||
this.socket?.connect();
|
||||
} else {
|
||||
console.log('Proactive pong received.');
|
||||
}
|
||||
});
|
||||
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает проактивный пинг.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user