feat: новая функция
This commit is contained in:
11
Dockerfile
11
Dockerfile
@@ -3,9 +3,18 @@
|
||||
# This software is proprietary and confidential.
|
||||
# For licensing inquiries: info@hb3-accelerator.com
|
||||
|
||||
FROM node:20-alpine
|
||||
FROM node:20-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Установка системных зависимостей для корректной работы нативных модулей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Установка зависимостей
|
||||
RUN npm install -g @upstash/context7-mcp@1.0.8
|
||||
|
||||
# Запуск сервера при старте контейнера
|
||||
CMD ["context7-mcp"]
|
||||
911
aidocs/AI_DATABASE_STRUCTURE.md
Normal file
911
aidocs/AI_DATABASE_STRUCTURE.md
Normal file
@@ -0,0 +1,911 @@
|
||||
# Структура базы данных для AI Ассистента
|
||||
|
||||
**Дата проверки:** 2025-10-08
|
||||
**Метод:** Прямая проверка через PostgreSQL
|
||||
**Статус:** ✅ ПРОВЕРКА ЗАВЕРШЕНА
|
||||
|
||||
---
|
||||
|
||||
## 📊 Список AI таблиц
|
||||
|
||||
Найдено таблиц: **27 таблиц** (25 связаны с AI, 2 CMS)
|
||||
|
||||
---
|
||||
|
||||
## 1. `ai_assistant_settings` ⭐ КЛЮЧЕВАЯ
|
||||
|
||||
**Назначение:** Основные настройки AI ассистента
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `selected_rag_tables` - INTEGER[] - массив ID RAG таблиц для использования
|
||||
- `languages` - TEXT[] - массив поддерживаемых языков
|
||||
- `updated_at` - TIMESTAMP - время последнего обновления (default: now())
|
||||
- `updated_by` - INTEGER - кто обновил (user_id)
|
||||
- `rules_id` - INTEGER (FK → ai_assistant_rules) - ID правил для AI
|
||||
- `telegram_settings_id` - INTEGER (FK → telegram_settings) - ID настроек Telegram
|
||||
- `email_settings_id` - INTEGER (FK → email_settings) - ID настроек Email
|
||||
- `system_prompt_encrypted` - TEXT - зашифрованный системный промпт
|
||||
- `model_encrypted` - TEXT - зашифрованное название модели
|
||||
- `system_message_encrypted` - TEXT - зашифрованное системное сообщение
|
||||
- `embedding_model_encrypted` - TEXT - зашифрованное название embedding модели
|
||||
- `system_message` - TEXT - системное сообщение (расшифрованное)
|
||||
- `embedding_model` - VARCHAR(128) - embedding модель (расшифрованное)
|
||||
|
||||
**Связи:**
|
||||
- → `ai_assistant_rules` (через rules_id)
|
||||
- → `telegram_settings` (через telegram_settings_id)
|
||||
- → `email_settings` (через email_settings_id)
|
||||
|
||||
**Используется в:**
|
||||
- aiAssistantSettingsService.js (getSettings, updateSettings)
|
||||
- conversationService.js (getRagTableId)
|
||||
- ai-assistant.js (generateResponse)
|
||||
- routes/settings.js (API)
|
||||
|
||||
---
|
||||
|
||||
## 2. `ai_assistant_rules` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Правила и инструкции для AI ассистента
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `created_at` - TIMESTAMP - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: now())
|
||||
- `name_encrypted` - TEXT - зашифрованное название правила
|
||||
- `description_encrypted` - TEXT - зашифрованное описание правила
|
||||
- `rules_encrypted` - TEXT - зашифрованные правила (JSON) ✅ ДОБАВЛЕНО
|
||||
|
||||
**Связи:**
|
||||
- ← `ai_assistant_settings.rules_id` ссылается на эту таблицу
|
||||
|
||||
**Используется в:**
|
||||
- aiAssistantRulesService.js (getAllRules, getRuleById, createRule)
|
||||
- ai-assistant.js (получение правил)
|
||||
- routes/settings.js (CRUD API)
|
||||
|
||||
---
|
||||
|
||||
## 3. `ai_providers_settings` ⭐ КЛЮЧЕВАЯ
|
||||
|
||||
**Назначение:** Настройки AI провайдеров (Ollama, OpenAI, Anthropic, Google)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
|
||||
- `provider_encrypted` - TEXT - зашифрованное название провайдера ('ollama', 'openai', etc.)
|
||||
- `api_key_encrypted` - TEXT - зашифрованный API ключ
|
||||
- `base_url_encrypted` - TEXT - зашифрованный базовый URL
|
||||
- `selected_model_encrypted` - TEXT - зашифрованное название выбранной модели
|
||||
- `embedding_model_encrypted` - TEXT - зашифрованное название embedding модели
|
||||
- `embedding_model` - VARCHAR(128) - embedding модель (незашифрованное, дублирует?)
|
||||
|
||||
**Связи:**
|
||||
- Нет внешних ключей
|
||||
|
||||
**Используется в:**
|
||||
- aiProviderSettingsService.js (getProviderSettings, upsertProviderSettings)
|
||||
- ollamaConfig.js (loadSettingsFromDb - загружает настройки Ollama)
|
||||
- ragService.js (getProviderSettings для вызова разных AI)
|
||||
- routes/settings.js (CRUD API)
|
||||
|
||||
---
|
||||
|
||||
## 4. `messages` ⭐ КЛЮЧЕВАЯ
|
||||
|
||||
**Назначение:** Все сообщения пользователей и AI ответы
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `conversation_id` - INTEGER (FK → conversations) - ID беседы
|
||||
- `sender_id` - INTEGER - ID отправителя (для админских сообщений)
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `user_id` - INTEGER (FK → users) - ID пользователя-владельца беседы
|
||||
- `tokens_used` - INTEGER - количество токенов (default: 0)
|
||||
- `is_processed` - BOOLEAN - обработано ли (default: false)
|
||||
- `attachment_size` - BIGINT - размер вложения в байтах
|
||||
- `attachment_data` - BYTEA - бинарные данные вложения
|
||||
- `sender_type_encrypted` - TEXT - тип отправителя ('user', 'assistant', 'editor')
|
||||
- `content_encrypted` - TEXT - текст сообщения
|
||||
- `channel_encrypted` - TEXT - канал ('web', 'telegram', 'email')
|
||||
- `role_encrypted` - TEXT - роль
|
||||
- `attachment_filename_encrypted` - TEXT - имя файла
|
||||
- `attachment_mimetype_encrypted` - TEXT - MIME тип
|
||||
- `direction_encrypted` - TEXT - направление ('in', 'out')
|
||||
- `message_id_encrypted` - TEXT - ID сообщения для дедупликации
|
||||
- `message_type` - VARCHAR(20) NOT NULL - тип ('user_chat', 'admin_chat')
|
||||
|
||||
**Индексы:**
|
||||
- idx_messages_conversation_id
|
||||
- idx_messages_created_at
|
||||
- idx_messages_message_type
|
||||
- idx_messages_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `conversations` (ON DELETE CASCADE)
|
||||
- → `users` (ON DELETE CASCADE)
|
||||
|
||||
**Триггеры:**
|
||||
- `trg_set_message_user_id` - автоустановка user_id
|
||||
|
||||
**Используется в:**
|
||||
- unifiedMessageProcessor.js (saveUserMessage)
|
||||
- messageDeduplicationService.js (сохранение)
|
||||
- conversationService.js (история)
|
||||
- routes/messages.js, routes/chat.js
|
||||
|
||||
---
|
||||
|
||||
## 5. `conversations` ⭐ КЛЮЧЕВАЯ
|
||||
|
||||
**Назначение:** Беседы (диалоги) пользователей с AI
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор беседы
|
||||
- `user_id` - INTEGER (FK → users) - ID пользователя-владельца
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
- `title_encrypted` - TEXT - зашифрованный заголовок беседы
|
||||
- `conversation_type` - VARCHAR(50) - тип беседы (default: 'user_chat')
|
||||
- `'user_chat'` - обычный чат пользователя
|
||||
- `'admin_chat'` - приватный чат между админами
|
||||
|
||||
**Индексы:**
|
||||
- idx_conversations_conversation_type
|
||||
- idx_conversations_created_at
|
||||
- idx_conversations_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `users` (user_id, ON DELETE CASCADE)
|
||||
- ← `conversation_participants` (для многопользовательских чатов)
|
||||
- ← `messages` (все сообщения беседы)
|
||||
|
||||
**Используется в:**
|
||||
- conversationService.js (findOrCreateConversation, getConversationHistory)
|
||||
- unifiedMessageProcessor.js (создание беседы)
|
||||
- guestMessageService.js (перенос гостевых сообщений)
|
||||
- routes/messages.js (CRUD беседы)
|
||||
|
||||
---
|
||||
|
||||
## 6. `message_deduplication` ⭐ КЛЮЧЕВАЯ
|
||||
|
||||
**Назначение:** Предотвращение дублирования сообщений (дедупликация)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `channel` - VARCHAR(20) NOT NULL - канал ('web', 'telegram', 'email')
|
||||
- `message_id_hash` - VARCHAR(64) NOT NULL - SHA-256 хеш ID
|
||||
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
|
||||
- `sender_type` - VARCHAR(20) NOT NULL - тип отправителя ('user', 'assistant')
|
||||
- `original_message_id_encrypted` - TEXT - оригинальный ID
|
||||
- `processed_at` - TIMESTAMP WITH TIME ZONE - время обработки (default: now())
|
||||
- `expires_at` - TIMESTAMP WITH TIME ZONE - время истечения
|
||||
|
||||
**Индексы:**
|
||||
- idx_message_dedup_expires
|
||||
- idx_message_dedup_lookup (channel, hash, user_id, sender_type)
|
||||
- idx_message_dedup_user_channel
|
||||
- UNIQUE (channel, hash, user_id, sender_type)
|
||||
|
||||
**Связи:**
|
||||
- → `users` (ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- messageDeduplicationService.js
|
||||
- unifiedMessageProcessor.js
|
||||
- ai-assistant.js
|
||||
|
||||
---
|
||||
|
||||
## 7. `guest_messages` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Временное хранение сообщений гостей
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK)
|
||||
- `is_ai` - BOOLEAN - от AI? (default: false)
|
||||
- `created_at` - TIMESTAMP WITH TIME ZONE (default: now())
|
||||
- `attachment_size` - BIGINT
|
||||
- `attachment_data` - BYTEA
|
||||
- `guest_id_encrypted` - TEXT - ID гостя (sessionID)
|
||||
- `content_encrypted` - TEXT
|
||||
- `language_encrypted` - TEXT
|
||||
- `attachment_filename_encrypted` - TEXT
|
||||
- `attachment_mimetype_encrypted` - TEXT
|
||||
- `attachment_filename` - TEXT (дубль?)
|
||||
- `attachment_mimetype` - TEXT (дубль?)
|
||||
|
||||
**Связи:**
|
||||
- Нет FK (временная)
|
||||
|
||||
**Используется в:**
|
||||
- guestService.js
|
||||
- guestMessageService.js (перенос после auth)
|
||||
|
||||
**Цикл:**
|
||||
1. Гость пишет → сохраняется
|
||||
2. Авторизация → перенос в messages
|
||||
3. Удаление из guest_messages
|
||||
|
||||
---
|
||||
|
||||
## 8. `guest_user_mapping` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Связь между гостями и зарегистрированными пользователями
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
|
||||
- `processed` - BOOLEAN - обработаны ли сообщения (default: false)
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `guest_id_encrypted` - TEXT - зашифрованный ID гостя (sessionID)
|
||||
|
||||
**Индексы:**
|
||||
- idx_guest_user_mapping_guest_id_encrypted - UNIQUE
|
||||
- idx_guest_user_mapping_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `users` (user_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- guestMessageService.js (processGuestMessages - проверка и создание mapping)
|
||||
|
||||
**Логика:**
|
||||
- При аутентификации гостя создается запись
|
||||
- `processed = false` → сообщения еще не перенесены
|
||||
- `processed = true` → сообщения уже перенесены в messages
|
||||
|
||||
---
|
||||
|
||||
## 9. `telegram_settings` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Настройки Telegram бота
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
|
||||
- `bot_token_encrypted` - TEXT - зашифрованный токен Telegram бота
|
||||
- `bot_username_encrypted` - TEXT - зашифрованное имя бота
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- ← `ai_assistant_settings.telegram_settings_id` ссылается на эту таблицу
|
||||
|
||||
**Используется в:**
|
||||
- telegramBot.js (loadSettings - загрузка токена)
|
||||
- botsSettings.js (getTelegramSettings, saveTelegramSettings, testConnection)
|
||||
- routes/admin (API для настройки Telegram)
|
||||
|
||||
---
|
||||
|
||||
## 10. `email_settings` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Настройки Email бота (SMTP + IMAP)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `smtp_port` - INTEGER NOT NULL - порт SMTP (обычно 465)
|
||||
- `imap_port` - INTEGER - порт IMAP (обычно 993)
|
||||
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
|
||||
- `smtp_host_encrypted` - TEXT - зашифрованный хост SMTP
|
||||
- `smtp_user_encrypted` - TEXT - зашифрованный пользователь SMTP
|
||||
- `smtp_password_encrypted` - TEXT - зашифрованный пароль SMTP
|
||||
- `imap_host_encrypted` - TEXT - зашифрованный хост IMAP
|
||||
- `from_email_encrypted` - TEXT - зашифрованный email отправителя
|
||||
- `imap_user_encrypted` - TEXT - зашифрованный пользователь IMAP
|
||||
- `imap_password_encrypted` - TEXT - зашифрованный пароль IMAP
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- ← `ai_assistant_settings.email_settings_id` ссылается на эту таблицу
|
||||
|
||||
**Используется в:**
|
||||
- emailBot.js (loadSettings - создание SMTP транспортера и IMAP соединения)
|
||||
- botsSettings.js (getEmailSettings, saveEmailSettings, testEmailSMTP, testEmailIMAP)
|
||||
- routes/admin (API для настройки Email)
|
||||
|
||||
---
|
||||
|
||||
## 11. `is_rag_source` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Источники данных для RAG (справочник)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор источника
|
||||
- `name_encrypted` - TEXT - зашифрованное название источника
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- ← `user_tables.is_rag_source_id` ссылается на эту таблицу
|
||||
|
||||
**Используется в:**
|
||||
- Связывает RAG таблицы с типом источника данных
|
||||
- user_tables имеет default: is_rag_source_id = 2
|
||||
|
||||
**Примеры источников:**
|
||||
- ID 1: "FAQ"
|
||||
- ID 2: "База знаний"
|
||||
- ID 3: "Документация"
|
||||
(зависит от данных в БД)
|
||||
|
||||
---
|
||||
|
||||
## 12. `user_tables` ⭐ КЛЮЧЕВАЯ (RAG)
|
||||
|
||||
**Назначение:** Пользовательские таблицы с данными для RAG базы знаний
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор таблицы
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
- `is_rag_source_id` - INTEGER (FK → is_rag_source) - тип источника (default: 2)
|
||||
- `name_encrypted` - TEXT - зашифрованное название таблицы
|
||||
- `description_encrypted` - TEXT - зашифрованное описание
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- → `is_rag_source` (is_rag_source_id)
|
||||
- ← `user_columns` (table_id, ON DELETE CASCADE)
|
||||
- ← `user_rows` (table_id, ON DELETE CASCADE)
|
||||
- ← `user_table_relations` (to_table_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- ragService.js (getTableData - получение данных для RAG)
|
||||
- routes/tables.js (CRUD таблиц)
|
||||
- routes/rag.js (выбор таблицы для RAG запроса)
|
||||
|
||||
**Структура RAG:**
|
||||
```
|
||||
user_tables (таблица)
|
||||
└─ user_columns (колонки с purpose)
|
||||
└─ user_rows (строки)
|
||||
└─ user_cell_values (значения ячеек)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. `user_columns` ⭐ КЛЮЧЕВАЯ (RAG)
|
||||
|
||||
**Назначение:** Колонки пользовательских таблиц с метаданными для RAG
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор колонки
|
||||
- `table_id` - INTEGER NOT NULL (FK → user_tables) - ID таблицы
|
||||
- `order` - INTEGER - порядок отображения (default: 0)
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
- `name_encrypted` - TEXT - зашифрованное название колонки
|
||||
- `type_encrypted` - TEXT - зашифрованный тип ('text', 'number', 'date', etc.)
|
||||
- `placeholder_encrypted` - TEXT - зашифрованный плейсхолдер
|
||||
- `placeholder` - VARCHAR(255) - плейсхолдер для RAG (UNIQUE)
|
||||
- `options` - JSONB - дополнительные опции (default: '{}')
|
||||
- `purpose` - назначение колонки ('question', 'answer', 'context', 'product', 'priority', 'date')
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- idx_user_columns_options (GIN) - для быстрого поиска по JSONB
|
||||
- user_columns_placeholder_key (UNIQUE) - уникальность плейсхолдеров
|
||||
|
||||
**Связи:**
|
||||
- → `user_tables` (table_id, ON DELETE CASCADE)
|
||||
- ← `user_table_relations` (column_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- ragService.js (getTableData - определение колонок по purpose)
|
||||
- routes/tables.js (CRUD колонок)
|
||||
|
||||
**Важно для RAG:**
|
||||
- `options.purpose` определяет роль колонки:
|
||||
- 'question' - вопрос для поиска
|
||||
- 'answer' - ответ
|
||||
- 'context' - контекст
|
||||
- 'product', 'priority', 'date' - метаданные
|
||||
|
||||
---
|
||||
|
||||
## 14. `user_rows` ⭐ КЛЮЧЕВАЯ (RAG)
|
||||
|
||||
**Назначение:** Строки данных в пользовательских таблицах (записи для RAG)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор строки
|
||||
- `table_id` - INTEGER NOT NULL (FK → user_tables) - ID таблицы
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
- `order` - INTEGER - порядок отображения (default: 0)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- → `user_tables` (table_id, ON DELETE CASCADE)
|
||||
- ← `user_cell_values` (row_id, ON DELETE CASCADE)
|
||||
- ← `user_table_relations` (from_row_id, to_row_id, ON DELETE CASCADE)
|
||||
- ← `user_tag_links` (tag_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- ragService.js (getTableData - получение всех строк таблицы)
|
||||
- routes/tables.js (CRUD строк)
|
||||
|
||||
**Важно:**
|
||||
- Каждая строка = одна запись в RAG (например, один вопрос-ответ)
|
||||
- Значения хранятся в `user_cell_values`
|
||||
|
||||
---
|
||||
|
||||
## 15. `user_cell_values` ⭐ КЛЮЧЕВАЯ (RAG)
|
||||
|
||||
**Назначение:** Значения ячеек в пользовательских таблицах (данные для RAG)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор значения
|
||||
- `row_id` - INTEGER NOT NULL (FK → user_rows) - ID строки
|
||||
- `column_id` - INTEGER NOT NULL (FK → user_columns) - ID колонки ✅ ИСПРАВЛЕНО
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
- `value_encrypted` - TEXT - зашифрованное значение ячейки
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- user_cell_values_row_id_column_id_key (UNIQUE) - уникальная пара (row_id, column_id)
|
||||
|
||||
**Связи:**
|
||||
- → `user_rows` (row_id, ON DELETE CASCADE)
|
||||
- → `user_columns` (column_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
|
||||
|
||||
**Используется в:**
|
||||
- ragService.js (getTableData - получение всех значений для построения RAG данных)
|
||||
- routes/tables.js (CRUD значений ячеек)
|
||||
|
||||
**Как работает RAG:**
|
||||
1. ragService получает все cell_values для строк таблицы
|
||||
2. Группирует по row_id
|
||||
3. Находит значения по column_id с нужным purpose (question/answer/context)
|
||||
4. Формирует данные для векторного поиска
|
||||
|
||||
---
|
||||
|
||||
## 16. `conversation_participants` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Участники многопользовательских бесед (для admin_chat)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `conversation_id` - INTEGER (FK → conversations) - ID беседы
|
||||
- `user_id` - INTEGER (FK → users) - ID участника
|
||||
- `created_at` - TIMESTAMP - дата добавления (default: CURRENT_TIMESTAMP)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- conversation_participants_conversation_id_user_id_key (UNIQUE) - пара (conversation_id, user_id)
|
||||
- idx_conversation_participants_conversation_id
|
||||
- idx_conversation_participants_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `conversations` (conversation_id, ON DELETE CASCADE)
|
||||
- → `users` (user_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- routes/messages.js (создание admin_chat бесед между админами)
|
||||
- routes/chat.js (поиск приватных бесед)
|
||||
|
||||
**Логика:**
|
||||
- Для обычных чатов (`user_chat`) - НЕ используется (один владелец)
|
||||
- Для админских чатов (`admin_chat`) - хранит всех участников беседы
|
||||
|
||||
---
|
||||
|
||||
## 17. `users` ⭐ КРИТИЧЕСКАЯ
|
||||
|
||||
**Назначение:** Пользователи системы (основа для всех AI взаимодействий)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор пользователя
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
- `role` - user_role ENUM - роль пользователя (default: 'user')
|
||||
- 'user' - обычный пользователь
|
||||
- 'editor' - администратор-редактор
|
||||
- 'readonly' - администратор только для чтения
|
||||
- `is_blocked` - BOOLEAN NOT NULL - заблокирован ли (default: false)
|
||||
- `blocked_at` - TIMESTAMP - время блокировки
|
||||
- `username_encrypted` - TEXT - зашифрованное имя пользователя
|
||||
- `status_encrypted` - TEXT - зашифрованный статус
|
||||
- `first_name_encrypted` - TEXT - зашифрованное имя
|
||||
- `last_name_encrypted` - TEXT - зашифрованная фамилия
|
||||
- `preferred_language` - JSONB - предпочитаемый язык
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- idx_users_role
|
||||
|
||||
**Связи (Referenced by):**
|
||||
- ← conversation_participants (9 таблиц ссылаются!)
|
||||
- ← conversations
|
||||
- ← message_deduplication
|
||||
- ← global_read_status
|
||||
- ← guest_user_mapping
|
||||
- ← messages
|
||||
- ← user_identities
|
||||
- ← user_preferences
|
||||
- ← user_tag_links
|
||||
- ← verification_codes
|
||||
|
||||
**Используется в:**
|
||||
- identity-service.js (создание пользователей)
|
||||
- auth-service.js (проверка ролей)
|
||||
- userUtils.js (isUserBlocked)
|
||||
- ВСЕ AI сервисы (через связи)
|
||||
|
||||
---
|
||||
|
||||
## 18. `user_identities` ⭐ КРИТИЧЕСКАЯ
|
||||
|
||||
**Назначение:** Идентификаторы пользователей (wallet, email, telegram)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `user_id` - INTEGER (FK → users) - ID пользователя
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `provider_encrypted` - TEXT - зашифрованный провайдер ('wallet', 'email', 'telegram')
|
||||
- `provider_id_encrypted` - TEXT - зашифрованный идентификатор (адрес, email, telegram ID)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- idx_user_identities_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `users` (user_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- identity-service.js (findUserByIdentity, saveIdentity, linkWalletToUser)
|
||||
- unifiedMessageProcessor.js (authenticateUser - поиск пользователя по Telegram/Email)
|
||||
- telegramBot.js, emailBot.js (связь external ID с user_id)
|
||||
- routes/identities.js (управление идентификаторами)
|
||||
- routes/messages.js (broadcast - поиск каналов пользователя)
|
||||
|
||||
**Логика:**
|
||||
- Один пользователь может иметь несколько идентификаторов (wallet + email + telegram)
|
||||
- При входе через любой канал - система находит или создает пользователя
|
||||
|
||||
---
|
||||
|
||||
## 19. `global_read_status` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Глобальный статус прочтения сообщений для user_chat
|
||||
|
||||
**Столбцы:**
|
||||
- `user_id` - INTEGER (PK, FK → users) - ID пользователя
|
||||
- `last_read_at` - TIMESTAMP NOT NULL - время последнего прочитанного сообщения
|
||||
- `updated_by_admin_id` - INTEGER NOT NULL - ID админа, который обновил статус
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: user_id
|
||||
- idx_global_read_status_last_read_at
|
||||
- idx_global_read_status_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `users` (user_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- routes/messages.js (mark-read, read-status для user_chat)
|
||||
|
||||
**Логика:**
|
||||
- Один статус на пользователя (общий для всех админов)
|
||||
- Для обычных чатов (`user_chat`)
|
||||
- Для админских чатов используется `admin_read_messages`
|
||||
|
||||
---
|
||||
|
||||
## 20. `admin_read_messages` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Персональный статус прочтения для admin_chat
|
||||
|
||||
**Столбцы:**
|
||||
- `admin_id` - INTEGER NOT NULL (PK, FK → users) - ID администратора
|
||||
- `user_id` - INTEGER NOT NULL (PK, FK → users) - ID пользователя/другого админа
|
||||
- `last_read_at` - TIMESTAMP NOT NULL - время последнего прочитанного сообщения
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: (admin_id, user_id) - составной ключ
|
||||
|
||||
**Связи:**
|
||||
- → `users` (admin_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
|
||||
- → `users` (user_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
|
||||
|
||||
**Используется в:**
|
||||
- routes/messages.js (mark-read, read-status для admin_chat)
|
||||
|
||||
**Логика:**
|
||||
- Персональный статус для каждого админа
|
||||
- Для приватных чатов между админами (`admin_chat`)
|
||||
- Каждый админ имеет свой статус прочтения
|
||||
|
||||
**Отличие от global_read_status:**
|
||||
- global_read_status - общий для всех админов (user_chat)
|
||||
- admin_read_messages - персональный для каждого админа (admin_chat)
|
||||
|
||||
---
|
||||
|
||||
## 21. `user_tag_links` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Связь пользователей с тегами (для RAG фильтрации)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
|
||||
- `tag_id` - INTEGER NOT NULL (FK → user_rows) - ID тега (строка из таблицы тегов)
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- idx_user_tag_links_tag_id
|
||||
- idx_user_tag_links_user_id
|
||||
- user_tag_links_user_id_tag_id_key (UNIQUE) - уникальная пара (user_id, tag_id)
|
||||
|
||||
**Связи:**
|
||||
- → `users` (user_id, ON DELETE CASCADE)
|
||||
- → `user_rows` (tag_id, ON DELETE CASCADE) - теги хранятся как строки в RAG таблицах
|
||||
|
||||
**Используется в:**
|
||||
- ragService.js (фильтрация данных по пользовательским тегам)
|
||||
- routes/tables.js (управление тегами пользователей)
|
||||
|
||||
**Логика:**
|
||||
- Теги позволяют фильтровать RAG данные по пользователю
|
||||
- Один пользователь может иметь несколько тегов
|
||||
- Используется для персонализации AI ответов
|
||||
|
||||
---
|
||||
|
||||
## 22. `roles` ⚠️ ВОЗМОЖНО УСТАРЕВШАЯ
|
||||
|
||||
**Назначение:** Роли пользователей (возможно старая таблица)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `name_encrypted` - TEXT - зашифрованное название роли
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- Нет внешних ключей
|
||||
- Нет Referenced by (никто не ссылается!)
|
||||
|
||||
**⚠️ ПРОБЛЕМА:**
|
||||
- Таблица существует, но НЕ используется
|
||||
- В `users` роль хранится как ENUM `user_role`, а не FK
|
||||
- Возможно старая таблица, которую можно удалить
|
||||
|
||||
**Используется в:**
|
||||
- ❌ НЕ найдено использования в коде
|
||||
|
||||
---
|
||||
|
||||
## 23. `admin_pages` ⚠️ НЕ СВЯЗАНА С AI
|
||||
|
||||
**Назначение:** Страницы контента (CMS), НЕ связана с AI напрямую
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `author_address_encrypted` - TEXT NOT NULL - зашифрованный адрес автора
|
||||
- `created_at` - TIMESTAMP - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: now())
|
||||
- `title_encrypted` - TEXT - зашифрованный заголовок
|
||||
- `summary_encrypted` - TEXT - зашифрованное краткое описание
|
||||
- `content_encrypted` - TEXT - зашифрованное содержимое
|
||||
- `seo_encrypted` - TEXT - зашифрованные SEO данные
|
||||
- `status_encrypted` - TEXT - зашифрованный статус
|
||||
- `settings_encrypted` - TEXT - зашифрованные настройки
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- Нет внешних ключей
|
||||
|
||||
**Используется в:**
|
||||
- routes/pages.js (CMS система)
|
||||
- ❌ НЕ используется в AI сервисах
|
||||
|
||||
**Примечание:**
|
||||
- Это таблица для CMS (система управления контентом)
|
||||
- НЕ связана с AI ассистентом
|
||||
- Упомянута в вашем списке, но к AI не относится
|
||||
|
||||
---
|
||||
|
||||
## 24. `admin_pages_simple` ⚠️ НЕ СВЯЗАНА С AI
|
||||
|
||||
**Назначение:** Упрощенные страницы контента БЕЗ шифрования, НЕ связана с AI
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `author_address` - TEXT NOT NULL - адрес автора (НЕ зашифрован!)
|
||||
- `created_at` - TIMESTAMP - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: now())
|
||||
- `title` - TEXT - заголовок (НЕ зашифрован!)
|
||||
- `summary` - TEXT - краткое описание
|
||||
- `content` - TEXT - содержимое
|
||||
- `seo` - TEXT - SEO данные
|
||||
- `status` - TEXT - статус
|
||||
- `settings` - TEXT - настройки
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
|
||||
**Связи:**
|
||||
- Нет внешних ключей
|
||||
|
||||
**Используется в:**
|
||||
- routes/pages.js (CMS система)
|
||||
- ❌ НЕ используется в AI сервисах
|
||||
|
||||
**Примечание:**
|
||||
- Это таблица для CMS (система управления контентом)
|
||||
- В отличие от `admin_pages`, данные НЕ зашифрованы
|
||||
- НЕ связана с AI ассистентом
|
||||
- Упомянута в вашем списке, но к AI не относится
|
||||
|
||||
---
|
||||
|
||||
## 25. `user_table_relations` ⭐ КЛЮЧЕВАЯ (RAG)
|
||||
|
||||
**Назначение:** Связи между строками в разных RAG таблицах (реляционная модель данных)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор связи
|
||||
- `from_row_id` - INTEGER NOT NULL (FK → user_rows) - исходная строка
|
||||
- `column_id` - INTEGER NOT NULL (FK → user_columns) - колонка со связью
|
||||
- `to_table_id` - INTEGER NOT NULL (FK → user_tables) - целевая таблица
|
||||
- `to_row_id` - INTEGER NOT NULL (FK → user_rows) - целевая строка
|
||||
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
|
||||
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- idx_user_table_relations_column
|
||||
- idx_user_table_relations_from_row
|
||||
- idx_user_table_relations_to_row
|
||||
- idx_user_table_relations_to_table
|
||||
|
||||
**Связи:**
|
||||
- → `user_columns` (column_id, ON DELETE CASCADE)
|
||||
- → `user_rows` (from_row_id, ON DELETE CASCADE)
|
||||
- → `user_rows` (to_row_id, ON DELETE CASCADE)
|
||||
- → `user_tables` (to_table_id, ON DELETE CASCADE)
|
||||
|
||||
**Используется в:**
|
||||
- routes/tables.js (создание связей между данными)
|
||||
- ragService.js (получение связанных данных для контекста)
|
||||
|
||||
**Логика:**
|
||||
- Позволяет создавать связи "один-ко-многим" и "многие-ко-многим" между RAG данными
|
||||
- Пример: FAQ вопрос → связанные продукты, документы → связанные разделы
|
||||
- Используется для обогащения контекста AI ответов
|
||||
|
||||
**Структура связи:**
|
||||
```
|
||||
user_rows[from_row_id]
|
||||
→ user_columns[column_id] (тип: "relation")
|
||||
→ user_tables[to_table_id]
|
||||
→ user_rows[to_row_id]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 26. `admin_read_contacts` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Статус прочтения контактов админами (для UI непрочитанных пользователей)
|
||||
|
||||
**Столбцы:**
|
||||
- `admin_id` - INTEGER NOT NULL (PK, FK → users) - ID администратора
|
||||
- `contact_id` - INTEGER NOT NULL (PK, FK → users) - ID контакта (user_id)
|
||||
- `read_at` - TIMESTAMP NOT NULL - время прочтения (default: now())
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: (admin_id, contact_id) - составной ключ
|
||||
|
||||
**Связи:**
|
||||
- → `users` (admin_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
|
||||
- → `users` (contact_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
|
||||
|
||||
**Используется в:**
|
||||
- routes/messages.js (mark-contact-read - отметить контакт как прочитанный)
|
||||
- adminLogicService.js (управление непрочитанными контактами)
|
||||
|
||||
**Логика:**
|
||||
- Отслеживает, когда админ последний раз просматривал чат пользователя
|
||||
- Используется для отображения непрочитанных контактов в списке
|
||||
- Отличается от `global_read_status` (статус сообщений) и `admin_read_messages` (приватные чаты)
|
||||
|
||||
**Применение:**
|
||||
- UI показывает список пользователей с новыми сообщениями
|
||||
- Когда админ открывает чат → обновляется `read_at`
|
||||
- Новые сообщения после `read_at` = непрочитанные
|
||||
|
||||
---
|
||||
|
||||
## 27. `user_preferences` ✅ АКТИВНАЯ
|
||||
|
||||
**Назначение:** Пользовательские настройки и предпочтения (может влиять на AI)
|
||||
|
||||
**Столбцы:**
|
||||
- `id` - INTEGER (PK) - уникальный идентификатор
|
||||
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
|
||||
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
|
||||
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
|
||||
- `preference_key_encrypted` - TEXT - зашифрованный ключ настройки
|
||||
- `preference_value_encrypted` - TEXT - зашифрованное значение настройки
|
||||
- `metadata` - JSONB - дополнительные метаданные (default: '{}')
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY: id
|
||||
- idx_user_preferences_user_id
|
||||
|
||||
**Связи:**
|
||||
- → `users` (user_id, ON DELETE CASCADE) ✅ ИСПРАВЛЕНО
|
||||
|
||||
**Используется в:**
|
||||
- routes/preferences.js (CRUD настроек)
|
||||
- Может использоваться для персонализации AI ответов
|
||||
|
||||
**Возможные настройки:**
|
||||
- Язык интерфейса
|
||||
- Тема оформления
|
||||
- Уведомления
|
||||
- Персональные предпочтения для AI (стиль общения, детальность ответов)
|
||||
|
||||
**Примечание:**
|
||||
- В таблице `users` уже есть `preferred_language` (JSONB)
|
||||
- `user_preferences` - более гибкая система для любых настроек
|
||||
- Может расширяться для AI-специфичных настроек
|
||||
|
||||
---
|
||||
|
||||
## 📊 ИТОГОВАЯ СТАТИСТИКА
|
||||
|
||||
**Всего проверено:** 27 таблиц
|
||||
|
||||
**По категориям:**
|
||||
- ⭐ КРИТИЧЕСКИЕ: 4 (users, user_identities, messages, conversations)
|
||||
- ⭐ КЛЮЧЕВЫЕ: 10 (ai_assistant_settings, ai_providers_settings, message_deduplication, user_tables, user_columns, user_rows, user_cell_values, user_table_relations)
|
||||
- ✅ АКТИВНЫЕ: 10 (ai_assistant_rules, telegram_settings, email_settings, is_rag_source, conversation_participants, global_read_status, admin_read_messages, user_tag_links, admin_read_contacts, user_preferences)
|
||||
- ⚠️ ПРОБЛЕМНЫЕ: 1 (roles - не используется)
|
||||
- ⚠️ НЕ СВЯЗАНЫ С AI: 2 (admin_pages, admin_pages_simple)
|
||||
|
||||
**Обнаруженные проблемы:**
|
||||
- ~~1. `ai_assistant_rules` - отсутствует столбец `rules_encrypted`~~ ✅ ИСПРАВЛЕНО (миграция 064)
|
||||
- ~~2. `user_cell_values` - нет FK на `user_columns` (column_id)~~ ✅ ИСПРАВЛЕНО (миграция 065)
|
||||
- 3. `roles` - таблица существует, но не используется ⚠️ НИЗКИЙ ПРИОРИТЕТ
|
||||
- ~~4. `admin_read_messages` - нет FK на users~~ ✅ ИСПРАВЛЕНО (миграция 066)
|
||||
- ~~5. `admin_read_contacts` - нет FK на users~~ ✅ ИСПРАВЛЕНО (миграция 066)
|
||||
- ~~6. `user_preferences` - нет ON DELETE CASCADE для user_id~~ ✅ ИСПРАВЛЕНО (миграция 067)
|
||||
|
||||
**Применённые миграции:**
|
||||
- 064_add_rules_encrypted_to_ai_assistant_rules.sql
|
||||
- 065_add_fk_user_cell_values_column_id.sql
|
||||
- 066_add_fk_admin_read_tables.sql
|
||||
- 067_add_cascade_user_preferences.sql
|
||||
|
||||
**Дата проверки:** 2025-10-08
|
||||
**Дата исправлений:** 2025-10-08
|
||||
**Статус:** ✅ ПРОВЕРКА ЗАВЕРШЕНА + КРИТИЧНЫЕ ПРОБЛЕМЫ ИСПРАВЛЕНЫ
|
||||
|
||||
179
aidocs/AI_FILES_QUICK_REFERENCE.md
Normal file
179
aidocs/AI_FILES_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# AI Ассистент - Быстрый справочник файлов
|
||||
|
||||
**Всего: 47 файлов**
|
||||
**Дата:** 2025-10-08
|
||||
|
||||
---
|
||||
|
||||
## ⭐ КРИТИЧЕСКИ ВАЖНЫЕ (9) - без них AI не работает
|
||||
|
||||
| № | Файл | Путь | Что делает |
|
||||
|---|------|------|------------|
|
||||
| 1 | ai-assistant.js | services/ | Главный интерфейс AI |
|
||||
| 2 | ollamaConfig.js | services/ | Настройки Ollama |
|
||||
| 3 | ragService.js | services/ | RAG генерация |
|
||||
| 4 | unifiedMessageProcessor.js | services/ | Обработка всех сообщений |
|
||||
| 5 | botManager.js | services/ | Координатор ботов |
|
||||
| 6 | wsHub.js | . | WebSocket уведомления |
|
||||
| 7 | logger.js | utils/ | Логирование |
|
||||
| 8 | encryptionUtils.js | utils/ | Шифрование |
|
||||
| 9 | encryptedDatabaseService.js | services/ | Работа с БД |
|
||||
|
||||
---
|
||||
|
||||
## ✅ АКТИВНО ИСПОЛЬЗУЕМЫЕ (27)
|
||||
|
||||
### Настройки AI (3)
|
||||
- aiAssistantSettingsService.js
|
||||
- aiAssistantRulesService.js
|
||||
- aiProviderSettingsService.js
|
||||
|
||||
### Боты (3)
|
||||
- webBot.js
|
||||
- telegramBot.js
|
||||
- emailBot.js
|
||||
|
||||
### Обработка данных (8)
|
||||
- conversationService.js
|
||||
- messageDeduplicationService.js
|
||||
- guestService.js
|
||||
- guestMessageService.js
|
||||
- identity-service.js
|
||||
- botsSettings.js
|
||||
- vectorSearchClient.js
|
||||
- userDeleteService.js
|
||||
|
||||
### Аутентификация (3)
|
||||
- admin-role.js
|
||||
- auth-service.js
|
||||
- session-service.js
|
||||
|
||||
### Routes - Основные (3)
|
||||
- routes/chat.js ⭐
|
||||
- routes/settings.js ⭐
|
||||
- routes/messages.js
|
||||
|
||||
### Routes - Специализированные (7)
|
||||
- routes/ollama.js
|
||||
- routes/rag.js
|
||||
- routes/monitoring.js
|
||||
- routes/auth.js
|
||||
- routes/identities.js
|
||||
- routes/tables.js
|
||||
- routes/uploads.js
|
||||
- routes/system.js
|
||||
|
||||
### Utils (2)
|
||||
- utils/constants.js (AI_USER_TYPES, AI_SENDER_TYPES)
|
||||
- utils/userUtils.js (isUserBlocked)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ЧАСТИЧНО ИСПОЛЬЗУЕМЫЕ (5)
|
||||
|
||||
| Файл | Где используется | Примечание |
|
||||
|------|------------------|------------|
|
||||
| ai-cache.js | routes/monitoring | Только метод clear() |
|
||||
| ai-queue.js | routes/ai-queue | Отдельный API |
|
||||
| routes/ai-queue.js | app.js | Отдельный API очереди |
|
||||
| testNewBots.js | - | Только для тестов |
|
||||
| notifyOllamaReady.js | Ollama контейнер | Отдельный скрипт |
|
||||
|
||||
---
|
||||
|
||||
## ❌ МЕРТВЫЙ КОД (2)
|
||||
|
||||
| Файл | Проблема | Рекомендация |
|
||||
|------|----------|--------------|
|
||||
| adminLogicService.js | НЕ импортируется нигде | Удалить или интегрировать |
|
||||
| services/index.js | Ссылка на несуществующий vectorStore.js | Обновить код |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 БЫСТРЫЙ ПОИСК
|
||||
|
||||
### По функциональности:
|
||||
|
||||
**Хочу настроить модель?**
|
||||
→ `ollamaConfig.js` + `routes/settings.js`
|
||||
|
||||
**Хочу изменить промпт?**
|
||||
→ `aiAssistantSettingsService.js` + `routes/settings.js`
|
||||
|
||||
**Хочу изменить правила AI?**
|
||||
→ `aiAssistantRulesService.js` + `routes/settings.js`
|
||||
|
||||
**Проблемы с генерацией ответов?**
|
||||
→ `ai-assistant.js` → `ragService.js`
|
||||
|
||||
**Боты не работают?**
|
||||
→ `botManager.js` → конкретный бот (webBot/telegramBot/emailBot)
|
||||
|
||||
**Сообщения дублируются?**
|
||||
→ `messageDeduplicationService.js`
|
||||
|
||||
**Проблемы с векторным поиском?**
|
||||
→ `vectorSearchClient.js`
|
||||
|
||||
**Логи не показываются?**
|
||||
→ `logger.js` (уровень логирования)
|
||||
|
||||
**Health check падает?**
|
||||
→ `ollamaConfig.checkHealth()` → проверить Ollama
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ПОТОК ОБРАБОТКИ СООБЩЕНИЯ
|
||||
|
||||
```
|
||||
1. routes/chat.js (/message endpoint)
|
||||
↓
|
||||
2. botManager.getBot('web')
|
||||
↓
|
||||
3. webBot.handleMessage()
|
||||
↓
|
||||
4. botManager.processMessage()
|
||||
↓
|
||||
5. unifiedMessageProcessor.processMessage()
|
||||
├─ identity-service (аутентификация)
|
||||
├─ userUtils.isUserBlocked (проверка блокировки)
|
||||
├─ messageDeduplicationService (дедупликация)
|
||||
├─ conversationService (беседа)
|
||||
└─ ai-assistant.generateResponse()
|
||||
├─ aiAssistantSettingsService (настройки)
|
||||
├─ aiAssistantRulesService (правила)
|
||||
└─ ragService.ragAnswer()
|
||||
├─ vectorSearchClient (поиск)
|
||||
└─ ollamaConfig (Ollama API)
|
||||
↓
|
||||
6. wsHub.broadcastChatMessage() (уведомление)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 ПРИМЕЧАНИЯ
|
||||
|
||||
### Что нужно знать:
|
||||
|
||||
1. **Все настройки хранятся в БД** (не в .env)
|
||||
2. **Дублирования кода нет** - все централизовано
|
||||
3. **AI работает для 3 каналов:** web, telegram, email
|
||||
4. **Два неиспользуемых сервиса:** ai-cache и ai-queue (потенциал для оптимизации)
|
||||
5. **Один мертвый файл:** adminLogicService.js (никогда не импортируется)
|
||||
|
||||
### Таблицы БД для AI:
|
||||
|
||||
- `ai_providers_settings` - настройки провайдеров
|
||||
- `ai_assistant_settings` - настройки ассистента
|
||||
- `ai_assistant_rules` - правила
|
||||
- `messages` - сообщения
|
||||
- `conversations` - беседы
|
||||
- `message_deduplication` - дедупликация
|
||||
- `guest_messages` - гостевые сообщения
|
||||
- `user_tables/columns/rows/cell_values` - RAG база знаний
|
||||
|
||||
---
|
||||
|
||||
**Проверено:** ВСЕ 47 файлов
|
||||
**Автор:** Digital Legal Entity Project
|
||||
|
||||
505
aidocs/AI_FULL_INVENTORY.md
Normal file
505
aidocs/AI_FULL_INVENTORY.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# АБСОЛЮТНО ПОЛНЫЙ инвентарь AI системы
|
||||
|
||||
**Дата:** 2025-10-08
|
||||
**Метод:** Систематическая проверка ВСЕХ директорий
|
||||
**Статус:** ✅ ПРОВЕРЕНО ВСЁ
|
||||
|
||||
---
|
||||
|
||||
## 📊 ИТОГОВАЯ СТАТИСТИКА
|
||||
|
||||
| Категория | Количество |
|
||||
|-----------|-----------|
|
||||
| **Backend Services** | 31 файл |
|
||||
| **Backend Routes** | 13 файлов |
|
||||
| **Backend Utils** | 3 файла |
|
||||
| **Backend Scripts** | 3 файла |
|
||||
| **Backend Tests** | 4 файла |
|
||||
| **Backend Other** | 1 файл (wsHub.js) |
|
||||
| **Vector-Search (Python)** | 3 файла |
|
||||
| **Scripts (корень)** | 2 файла |
|
||||
| **Frontend Components** | 11 файлов |
|
||||
| **Frontend Services** | 2 файла |
|
||||
| **Frontend Composables** | 1 файл |
|
||||
| **Frontend Views** | 12 файлов |
|
||||
| **ИТОГО** | **86 ФАЙЛОВ** |
|
||||
|
||||
---
|
||||
|
||||
## 🔥 BACKEND (55 файлов)
|
||||
|
||||
### ⭐ SERVICES (31 файл)
|
||||
|
||||
#### КЛЮЧЕВЫЕ (9):
|
||||
1. `ai-assistant.js` - главный AI интерфейс
|
||||
2. `ollamaConfig.js` - настройки Ollama
|
||||
3. `ragService.js` - RAG генерация
|
||||
4. `unifiedMessageProcessor.js` - процессор всех сообщений
|
||||
5. `botManager.js` - координатор ботов
|
||||
6. `encryptedDatabaseService.js` - работа с БД
|
||||
7. `vectorSearchClient.js` - векторный поиск
|
||||
8. `conversationService.js` - управление беседами
|
||||
9. `messageDeduplicationService.js` - дедупликация
|
||||
|
||||
#### АКТИВНЫЕ (15):
|
||||
10. `aiAssistantSettingsService.js` - настройки AI
|
||||
11. `aiAssistantRulesService.js` - правила AI
|
||||
12. `aiProviderSettingsService.js` - провайдеры AI
|
||||
13. `webBot.js` - веб бот
|
||||
14. `telegramBot.js` - Telegram бот
|
||||
15. `emailBot.js` - Email бот
|
||||
16. `guestService.js` - гостевые сообщения
|
||||
17. `guestMessageService.js` - перенос гостевых сообщений
|
||||
18. `identity-service.js` - идентификаторы пользователей
|
||||
19. `botsSettings.js` - настройки ботов
|
||||
20. `admin-role.js` - проверка админской роли
|
||||
21. `auth-service.js` - аутентификация
|
||||
22. `session-service.js` - сессии
|
||||
23. `userDeleteService.js` - удаление данных пользователей
|
||||
24. `index.js` - экспорт сервисов (частично устаревший)
|
||||
|
||||
#### ЧАСТИЧНО/НЕ В ОСНОВНОМ ПОТОКЕ (5):
|
||||
25. `ai-cache.js` ⚠️ - только monitoring
|
||||
26. `ai-queue.js` ⚠️ - отдельный API
|
||||
27. `notifyOllamaReady.js` 📦 - скрипт для Ollama
|
||||
28. `testNewBots.js` 🧪 - тесты
|
||||
29. `adminLogicService.js` ❌ - мертвый код
|
||||
|
||||
### 📡 ROUTES (13 файлов)
|
||||
|
||||
#### КЛЮЧЕВЫЕ (3):
|
||||
1. `chat.js` ⭐ - основной чат API
|
||||
2. `settings.js` ⭐ - ВСЕ настройки AI
|
||||
3. `messages.js` - CRUD сообщений, broadcast
|
||||
|
||||
#### СПЕЦИАЛИЗИРОВАННЫЕ (10):
|
||||
4. `ollama.js` - управление Ollama
|
||||
5. `rag.js` - RAG API
|
||||
6. `ai-queue.js` - очередь AI
|
||||
7. `monitoring.js` - мониторинг
|
||||
8. `auth.js` - аутентификация
|
||||
9. `identities.js` - управление идентификаторами
|
||||
10. `tables.js` - RAG таблицы
|
||||
11. `uploads.js` - загрузка файлов
|
||||
12. `system.js` - системные настройки
|
||||
13. `admin.js` - админ панель
|
||||
|
||||
### 🛠️ UTILS (3 файла)
|
||||
|
||||
1. `logger.js` ⭐ - логирование (везде!)
|
||||
2. `encryptionUtils.js` ⭐ - шифрование (везде!)
|
||||
3. `constants.js` - AI_USER_TYPES, AI_SENDER_TYPES, MESSAGE_CHANNELS
|
||||
4. `userUtils.js` - isUserBlocked
|
||||
|
||||
### 📜 SCRIPTS (3 файла)
|
||||
|
||||
1. `check-ollama-models.js` - проверка моделей Ollama
|
||||
2. `fix-rag-columns.js` - исправление RAG колонок
|
||||
3. (другие скрипты не связаны напрямую с AI)
|
||||
|
||||
### 🧪 TESTS (4 файла)
|
||||
|
||||
1. `ragService.test.js` - тесты RAG сервиса
|
||||
2. `ragServiceFull.test.js` - полные тесты RAG
|
||||
3. `adminLogicService.test.js` - тесты админской логики
|
||||
4. `vectorSearchClient.test.js` - тесты векторного поиска
|
||||
|
||||
### 🔌 OTHER (1 файл)
|
||||
|
||||
1. `wsHub.js` ⭐ - WebSocket хаб (критичен для уведомлений!)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 VECTOR-SEARCH (3 файла Python)
|
||||
|
||||
**Директория:** `vector-search/`
|
||||
|
||||
1. **`app.py`** ⭐
|
||||
- FastAPI приложение
|
||||
- Endpoints: `/upsert`, `/search`, `/delete`, `/rebuild`, `/health`
|
||||
- Порт: 8001
|
||||
|
||||
2. **`vector_store.py`** ⭐
|
||||
- Векторное хранилище на FAISS
|
||||
- Embedding через Ollama
|
||||
- Сохранение индексов
|
||||
|
||||
3. **`schemas.py`**
|
||||
- Pydantic схемы для валидации
|
||||
- UpsertRequest, SearchRequest, DeleteRequest
|
||||
|
||||
**Зависимости:**
|
||||
- FastAPI
|
||||
- FAISS
|
||||
- Ollama (для embeddings)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 FRONTEND (26 файлов)
|
||||
|
||||
### 🧩 COMPONENTS (11 файлов)
|
||||
|
||||
1. `ChatInterface.vue` ⭐ - главный интерфейс чата
|
||||
2. `Message.vue` - компонент сообщения
|
||||
3. `MessagesTable.vue` - таблица сообщений
|
||||
4. `OllamaModelManager.vue` - управление моделями Ollama
|
||||
5. `AIQueueMonitor.vue` - мониторинг AI очереди
|
||||
6. `ai-assistant/RuleEditor.vue` - редактор правил AI
|
||||
7. `ai-assistant/SystemMonitoring.vue` - мониторинг системы AI
|
||||
8. `identity/EmailConnect.vue` - подключение email (для email бота)
|
||||
9. `identity/TelegramConnect.vue` - подключение Telegram (для Telegram бота)
|
||||
10. `identity/WalletConnection.vue` - подключение кошелька
|
||||
11. `identity/index.js` - экспорт компонентов идентификации
|
||||
|
||||
### 📄 VIEWS (12 файлов)
|
||||
|
||||
1. `AdminChatView.vue` - админский чат
|
||||
2. `PersonalMessagesView.vue` - личные сообщения
|
||||
3. `settings/AiSettingsView.vue` ⭐ - главные настройки AI
|
||||
4. `settings/AIProviderSettings.vue` - настройки провайдеров
|
||||
5. `settings/AI/AiAssistantSettings.vue` - настройки ассистента
|
||||
6. `settings/AI/OllamaSettingsView.vue` - настройки Ollama
|
||||
7. `settings/AI/OpenAISettingsView.vue` - настройки OpenAI
|
||||
8. `settings/AI/EmailSettingsView.vue` - настройки Email бота
|
||||
9. `settings/AI/TelegramSettingsView.vue` - настройки Telegram бота
|
||||
10. `settings/AI/DatabaseSettingsView.vue` - настройки БД
|
||||
11. `contacts/ContactDetailsView.vue` - детали контакта (сообщения)
|
||||
12. `tables/*` (5 файлов) - управление RAG таблицами
|
||||
|
||||
### 🔧 SERVICES (2 файла)
|
||||
|
||||
1. `messagesService.js` ⭐ - сервис сообщений
|
||||
2. `adminChatService.js` - админский чат
|
||||
|
||||
### 🎣 COMPOSABLES (1 файл)
|
||||
|
||||
1. `useChat.js` ⭐ - хук для чата с AI
|
||||
|
||||
---
|
||||
|
||||
## 🚀 SCRIPTS КОРНЕВЫЕ (2 файла)
|
||||
|
||||
**Директория:** `scripts/`
|
||||
|
||||
1. **`test-ai-assistant.sh`** 🧪
|
||||
- Полный тест AI ассистента
|
||||
- Проверка контейнеров, Ollama, Backend, RAG, производительности
|
||||
|
||||
2. **`manage-models.sh`** 🔧
|
||||
- Управление моделями Ollama
|
||||
- Предзагрузка, поддержание в памяти, очистка
|
||||
|
||||
---
|
||||
|
||||
## 📂 ПОЛНАЯ СВОДКА ПО ДИРЕКТОРИЯМ
|
||||
|
||||
```
|
||||
backend/
|
||||
├── services/ 31 файл (9 ключевых, 15 активных, 5 частично, 2 мертвый код)
|
||||
├── routes/ 13 файлов (3 ключевых, 10 активных)
|
||||
├── utils/ 3 файла (2 ключевых, 1 активный)
|
||||
├── scripts/ 3 файла (вспомогательные)
|
||||
├── tests/ 4 файла (тесты)
|
||||
└── wsHub.js 1 файл (ключевой!)
|
||||
|
||||
vector-search/ 3 файла Python (критичны для RAG)
|
||||
|
||||
scripts/ 2 файла bash (управление)
|
||||
|
||||
frontend/
|
||||
├── components/ 11 файлов (UI компоненты AI)
|
||||
├── views/ 12 файлов (страницы AI)
|
||||
├── services/ 2 файла (API клиенты)
|
||||
└── composables/ 1 файл (логика чата)
|
||||
|
||||
═══════════════════════════════════════
|
||||
ИТОГО: 86 файлов
|
||||
═══════════════════════════════════════
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 КРИТИЧЕСКИ ВАЖНЫЕ ФАЙЛЫ (TOP 15)
|
||||
|
||||
**Без этих файлов AI НЕ РАБОТАЕТ:**
|
||||
|
||||
| № | Файл | Путь | Роль |
|
||||
|---|------|------|------|
|
||||
| 1 | ai-assistant.js | services/ | Главный AI интерфейс |
|
||||
| 2 | ollamaConfig.js | services/ | Настройки Ollama |
|
||||
| 3 | ragService.js | services/ | RAG генерация |
|
||||
| 4 | unifiedMessageProcessor.js | services/ | Обработка сообщений |
|
||||
| 5 | botManager.js | services/ | Координатор ботов |
|
||||
| 6 | encryptedDatabaseService.js | services/ | Работа с БД |
|
||||
| 7 | vectorSearchClient.js | services/ | Векторный поиск |
|
||||
| 8 | logger.js | utils/ | Логирование |
|
||||
| 9 | encryptionUtils.js | utils/ | Шифрование |
|
||||
| 10 | wsHub.js | backend/ | WebSocket |
|
||||
| 11 | chat.js | routes/ | API чата |
|
||||
| 12 | settings.js | routes/ | API настроек AI |
|
||||
| 13 | app.py | vector-search/ | Vector search сервис |
|
||||
| 14 | vector_store.py | vector-search/ | FAISS хранилище |
|
||||
| 15 | ChatInterface.vue | frontend/ | UI чата |
|
||||
|
||||
---
|
||||
|
||||
## 📋 ДЕТАЛЬНЫЙ СПИСОК
|
||||
|
||||
### BACKEND SERVICES (31)
|
||||
|
||||
```
|
||||
✅ АКТИВНО ИСПОЛЬЗУЮТСЯ (24):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
1. ai-assistant.js ⭐ Главный AI интерфейс
|
||||
2. ollamaConfig.js ⭐ Настройки Ollama
|
||||
3. ragService.js ⭐ RAG генерация
|
||||
4. unifiedMessageProcessor.js ⭐ Процессор сообщений
|
||||
5. botManager.js ⭐ Координатор ботов
|
||||
6. encryptedDatabaseService.js ⭐ Работа с БД
|
||||
7. vectorSearchClient.js ✅ Векторный поиск
|
||||
8. conversationService.js ✅ Беседы
|
||||
9. messageDeduplicationService.js ✅ Дедупликация
|
||||
10. aiAssistantSettingsService.js ✅ Настройки AI
|
||||
11. aiAssistantRulesService.js ✅ Правила AI
|
||||
12. aiProviderSettingsService.js ✅ Провайдеры
|
||||
13. webBot.js ✅ Web бот
|
||||
14. telegramBot.js ✅ Telegram бот
|
||||
15. emailBot.js ✅ Email бот
|
||||
16. guestService.js ✅ Гости
|
||||
17. guestMessageService.js ✅ Перенос гостей
|
||||
18. identity-service.js ✅ Идентификаторы
|
||||
19. botsSettings.js ✅ Настройки ботов
|
||||
20. admin-role.js ✅ Админская роль
|
||||
21. auth-service.js ✅ Аутентификация
|
||||
22. session-service.js ✅ Сессии
|
||||
23. userDeleteService.js ✅ Удаление данных
|
||||
24. index.js ⚠️ Устаревший экспорт
|
||||
|
||||
⚠️ ЧАСТИЧНО (2):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
25. ai-cache.js ⚠️ Только monitoring
|
||||
26. ai-queue.js ⚠️ Отдельный API
|
||||
|
||||
📦 ВСПОМОГАТЕЛЬНЫЕ (2):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
27. notifyOllamaReady.js 📦 Ollama скрипт
|
||||
|
||||
🧪 ТЕСТОВЫЕ (1):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
28. testNewBots.js 🧪 Тесты ботов
|
||||
|
||||
❌ МЕРТВЫЙ КОД (1):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
29. adminLogicService.js ❌ Не импортируется
|
||||
```
|
||||
|
||||
### BACKEND ROUTES (13)
|
||||
|
||||
```
|
||||
⭐ КЛЮЧЕВЫЕ (3):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
1. chat.js ⭐ Основной API чата
|
||||
2. settings.js ⭐ ВСЕ настройки AI
|
||||
3. messages.js ⭐ CRUD, broadcast
|
||||
|
||||
✅ СПЕЦИАЛИЗИРОВАННЫЕ (10):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
4. ollama.js ✅ Управление Ollama
|
||||
5. rag.js ✅ RAG API
|
||||
6. ai-queue.js ⚠️ Очередь API
|
||||
7. monitoring.js ✅ Мониторинг
|
||||
8. auth.js ✅ Аутентификация
|
||||
9. identities.js ✅ Идентификаторы
|
||||
10. tables.js ✅ RAG таблицы
|
||||
11. uploads.js ✅ Загрузка файлов
|
||||
12. system.js ✅ Системные настройки
|
||||
13. admin.js ✅ Админ панель
|
||||
```
|
||||
|
||||
### BACKEND UTILS (3)
|
||||
|
||||
```
|
||||
1. logger.js ⭐ Логирование (ВЕЗДЕ!)
|
||||
2. encryptionUtils.js ⭐ Шифрование (ВЕЗДЕ!)
|
||||
3. constants.js ✅ AI константы
|
||||
4. userUtils.js ✅ isUserBlocked
|
||||
```
|
||||
|
||||
### BACKEND SCRIPTS (3)
|
||||
|
||||
```
|
||||
1. check-ollama-models.js 🔧 Проверка моделей
|
||||
2. fix-rag-columns.js 🔧 Исправление RAG
|
||||
3. wait-for-postgres.sh 🔧 Ожидание БД
|
||||
```
|
||||
|
||||
### BACKEND TESTS (4)
|
||||
|
||||
```
|
||||
1. ragService.test.js 🧪 Тесты RAG
|
||||
2. ragServiceFull.test.js 🧪 Полные тесты RAG
|
||||
3. adminLogicService.test.js 🧪 Тесты админской логики
|
||||
4. vectorSearchClient.test.js 🧪 Тесты векторного поиска
|
||||
```
|
||||
|
||||
### BACKEND OTHER (1)
|
||||
|
||||
```
|
||||
1. wsHub.js ⭐ WebSocket хаб
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐍 VECTOR-SEARCH Python (3 файла)
|
||||
|
||||
**Директория:** `vector-search/`
|
||||
|
||||
```
|
||||
1. app.py ⭐ FastAPI приложение
|
||||
- GET /health
|
||||
- POST /upsert
|
||||
- POST /search
|
||||
- POST /delete
|
||||
- POST /rebuild
|
||||
|
||||
2. vector_store.py ⭐ FAISS векторное хранилище
|
||||
- VectorStore класс
|
||||
- Embeddings через Ollama
|
||||
- Индексация и поиск
|
||||
|
||||
3. schemas.py ✅ Pydantic схемы
|
||||
- UpsertRequest
|
||||
- SearchRequest
|
||||
- DeleteRequest
|
||||
- RebuildRequest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 SCRIPTS КОРНЕВЫЕ (2 файла)
|
||||
|
||||
**Директория:** `scripts/`
|
||||
|
||||
```
|
||||
1. test-ai-assistant.sh 🧪 Полный тест AI
|
||||
- Проверка контейнеров
|
||||
- Тест Ollama
|
||||
- Тест Backend API
|
||||
- Тест RAG системы
|
||||
- Тест производительности
|
||||
|
||||
2. manage-models.sh 🔧 Управление моделями
|
||||
- status - статус моделей
|
||||
- preload - предзагрузка
|
||||
- keep - поддержание в памяти
|
||||
- clear - очистка памяти
|
||||
- test - тест производительности
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 FRONTEND (26 файлов)
|
||||
|
||||
### COMPONENTS (11)
|
||||
|
||||
```
|
||||
1. ChatInterface.vue ⭐ Главный UI чата
|
||||
2. Message.vue ✅ Компонент сообщения
|
||||
3. MessagesTable.vue ✅ Таблица сообщений
|
||||
4. OllamaModelManager.vue ✅ Управление моделями
|
||||
5. AIQueueMonitor.vue ⚠️ Мониторинг очереди
|
||||
6. ai-assistant/RuleEditor.vue ✅ Редактор правил
|
||||
7. ai-assistant/SystemMonitoring.vue ✅ Мониторинг системы
|
||||
8. identity/EmailConnect.vue ✅ Email подключение
|
||||
9. identity/TelegramConnect.vue ✅ Telegram подключение
|
||||
10. identity/WalletConnection.vue ✅ Wallet подключение
|
||||
11. identity/index.js ✅ Экспорт
|
||||
```
|
||||
|
||||
### VIEWS (12)
|
||||
|
||||
```
|
||||
1. AdminChatView.vue ✅ Админский чат
|
||||
2. PersonalMessagesView.vue ✅ Личные сообщения
|
||||
3. settings/AiSettingsView.vue ⭐ Главная страница настроек AI
|
||||
4. settings/AIProviderSettings.vue ✅ Настройки провайдеров
|
||||
5. settings/AI/AiAssistantSettings.vue ⭐ Настройки ассистента
|
||||
6. settings/AI/OllamaSettingsView.vue ✅ Настройки Ollama
|
||||
7. settings/AI/OpenAISettingsView.vue ✅ Настройки OpenAI
|
||||
8. settings/AI/EmailSettingsView.vue ✅ Настройки Email бота
|
||||
9. settings/AI/TelegramSettingsView.vue ✅ Настройки Telegram
|
||||
10. settings/AI/DatabaseSettingsView.vue ✅ Настройки БД
|
||||
11. contacts/ContactDetailsView.vue ✅ Детали контакта
|
||||
12. tables/* (5 views) ✅ RAG таблицы
|
||||
```
|
||||
|
||||
### SERVICES (2)
|
||||
|
||||
```
|
||||
1. messagesService.js ⭐ API клиент для сообщений
|
||||
2. adminChatService.js ✅ API клиент админского чата
|
||||
```
|
||||
|
||||
### COMPOSABLES (1)
|
||||
|
||||
```
|
||||
1. useChat.js ⭐ Логика чата
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔢 ФИНАЛЬНАЯ СТАТИСТИКА
|
||||
|
||||
### Всего файлов: 86
|
||||
|
||||
#### По директориям:
|
||||
- **Backend:** 55 файлов
|
||||
- services: 31
|
||||
- routes: 13
|
||||
- utils: 3
|
||||
- scripts: 3
|
||||
- tests: 4
|
||||
- other: 1 (wsHub)
|
||||
|
||||
- **Vector-search:** 3 файла (Python)
|
||||
|
||||
- **Scripts:** 2 файла (bash)
|
||||
|
||||
- **Frontend:** 26 файлов
|
||||
- components: 11
|
||||
- views: 12
|
||||
- services: 2
|
||||
- composables: 1
|
||||
|
||||
#### По статусу:
|
||||
- ⭐ **КЛЮЧЕВЫЕ** (критичны): 15 файлов
|
||||
- ✅ **АКТИВНЫЕ** (используются): 53 файла
|
||||
- ⚠️ **ЧАСТИЧНО** (не в основном потоке): 7 файлов
|
||||
- 🧪 **ТЕСТЫ/СКРИПТЫ**: 11 файлов
|
||||
- ❌ **МЕРТВЫЙ КОД**: 2 файла
|
||||
|
||||
---
|
||||
|
||||
## ✅ ВСЁ ПРОВЕРЕНО!
|
||||
|
||||
**Проверенные директории:**
|
||||
- ✅ backend/services/
|
||||
- ✅ backend/routes/
|
||||
- ✅ backend/utils/
|
||||
- ✅ backend/scripts/
|
||||
- ✅ backend/tests/
|
||||
- ✅ vector-search/
|
||||
- ✅ scripts/
|
||||
- ✅ frontend/src/components/
|
||||
- ✅ frontend/src/views/
|
||||
- ✅ frontend/src/services/
|
||||
- ✅ frontend/src/composables/
|
||||
|
||||
**Ничего не пропущено! Это ПОЛНЫЙ инвентарь AI системы.**
|
||||
|
||||
---
|
||||
|
||||
**Дата:** 2025-10-08
|
||||
**Проверил:** Все директории проекта
|
||||
**Метод:** grep + find + систематическая проверка
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# This software is proprietary and confidential.
|
||||
# For licensing inquiries: info@hb3-accelerator.com
|
||||
|
||||
FROM node:20-alpine
|
||||
FROM node:20-slim
|
||||
|
||||
# Добавляем метки для авторских прав
|
||||
LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>"
|
||||
@@ -18,8 +18,15 @@ LABEL website="https://hb3-accelerator.com"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем только docker-cli (без демона) для Alpine Linux
|
||||
RUN apk update && apk add --no-cache docker-cli curl ca-certificates
|
||||
# Устанавливаем системные зависимости для компиляции нативных модулей Node.js
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
docker.io \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
@@ -21,6 +21,12 @@ const errorHandler = require('./middleware/errorHandler');
|
||||
// const { version } = require('./package.json'); // Закомментировано, так как не используется
|
||||
const db = require('./db'); // Добавляем импорт db
|
||||
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant
|
||||
|
||||
// Инициализация AI Assistant из БД
|
||||
aiAssistant.initPromise.catch(error => {
|
||||
logger.error('[app.js] AI Assistant не инициализирован:', error.message);
|
||||
});
|
||||
|
||||
const deploymentWebSocketService = require('./services/deploymentWebSocketService'); // WebSocket для деплоя
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -211,7 +211,8 @@ async function saveGuestMessageToDatabase(message, language, guestId) {
|
||||
}
|
||||
|
||||
async function waitForOllamaModel(modelName) {
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('./services/ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
while (true) {
|
||||
try {
|
||||
const res = await axios.get(`${ollamaUrl}/api/tags`);
|
||||
@@ -233,18 +234,8 @@ async function seedAIAssistantSettings() {
|
||||
const res = await pool.query('SELECT COUNT(*) FROM ai_assistant_settings');
|
||||
if (parseInt(res.rows[0].count, 10) === 0) {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, 'ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
const encryptionUtils = require('./utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO ai_assistant_settings (system_prompt_encrypted, selected_rag_tables, languages, model_encrypted, rules_id, updated_by)
|
||||
|
||||
@@ -15,23 +15,13 @@
|
||||
const { createError } = require('../utils/error');
|
||||
const authService = require('../services/auth-service');
|
||||
const logger = require('../utils/logger');
|
||||
const { USER_ROLES } = require('../utils/constants');
|
||||
// Используем новые роли: 'editor' и 'readonly' вместо 'admin'
|
||||
const db = require('../db');
|
||||
const { checkAdminTokens } = require('../services/auth-service');
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
/**
|
||||
* Middleware для проверки аутентификации
|
||||
@@ -90,7 +80,7 @@ async function requireAdmin(req, res, next) {
|
||||
const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [
|
||||
req.session.userId,
|
||||
]);
|
||||
if (userResult.rows.length > 0 && userResult.rows[0].role === USER_ROLES.ADMIN) {
|
||||
if (userResult.rows.length > 0 && (userResult.rows[0].role === 'editor' || userResult.rows[0].role === 'readonly')) {
|
||||
// Обновляем сессию
|
||||
req.session.isAdmin = true;
|
||||
// logger.info(`[requireAdmin] Доступ разрешен через userId`); // Убрано
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"watch": [
|
||||
"backend/src",
|
||||
"backend/routes",
|
||||
"backend/services"
|
||||
"backend/services",
|
||||
"server.js"
|
||||
],
|
||||
"ignore": [
|
||||
"backend/artifacts/**",
|
||||
|
||||
@@ -20,7 +20,7 @@ const rateLimit = require('express-rate-limit');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const authService = require('../services/auth-service');
|
||||
const { ethers } = require('ethers');
|
||||
const { initTelegramAuth } = require('../services/telegramBot');
|
||||
const botManager = require('../services/botManager');
|
||||
const emailAuth = require('../services/emailAuth');
|
||||
const verificationService = require('../services/verification-service');
|
||||
const identityService = require('../services/identity-service');
|
||||
@@ -60,17 +60,10 @@ router.get('/nonce', async (req, res) => {
|
||||
// Используем правильный ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
logger.info(`[nonce] Using encryption key: ${encryptionKey.substring(0, 10)}...`);
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
logger.info(`[nonce] Using encryption key: ${encryptionKey.substring(0, 10)}...`);
|
||||
|
||||
try {
|
||||
// Проверяем, существует ли уже nonce для этого адреса
|
||||
@@ -135,16 +128,9 @@ router.post('/verify', async (req, res) => {
|
||||
// Читаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Проверяем nonce в базе данных с проверкой времени истечения
|
||||
const nonceResult = await db.getQuery()(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,16 +42,9 @@ router.post('/link', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Проверяем, существует ли уже такой кошелек
|
||||
const existingCheck = await db.getQuery()(
|
||||
@@ -150,167 +143,16 @@ router.delete('/:provider/:providerId', requireAuth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Получение email-настроек
|
||||
router.get('/email-settings', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await db.getQuery()(
|
||||
'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $1) as smtp_host, decrypt_text(smtp_user_encrypted, $1) as smtp_user, decrypt_text(smtp_password_encrypted, $1) as smtp_password, decrypt_text(imap_host_encrypted, $1) as imap_host, decrypt_text(from_email_encrypted, $1) as from_email FROM email_settings ORDER BY id LIMIT 1',
|
||||
[encryptionKey]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
|
||||
const settings = rows[0];
|
||||
delete settings.smtp_password; // не возвращаем пароль
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
logger.error('Error getting email settings:', error, error && error.stack);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновление email-настроек
|
||||
router.put('/email-settings', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
try {
|
||||
const { smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email } = req.body;
|
||||
if (!smtp_host || !smtp_port || !smtp_user || !from_email) {
|
||||
return res.status(400).json({ success: false, error: 'Missing required fields' });
|
||||
}
|
||||
const { rows } = await db.getQuery()('SELECT id FROM email_settings ORDER BY id LIMIT 1');
|
||||
if (rows.length) {
|
||||
// Обновляем существующую запись
|
||||
await db.getQuery()(
|
||||
`UPDATE email_settings SET smtp_host_encrypted=encrypt_text($1, $9), smtp_port=$2, smtp_user_encrypted=encrypt_text($3, $9), smtp_password_encrypted=COALESCE(encrypt_text($4, $9), smtp_password_encrypted), imap_host_encrypted=encrypt_text($5, $9), imap_port=$6, from_email_encrypted=encrypt_text($7, $9), updated_at=NOW() WHERE id=$8`,
|
||||
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id, encryptionKey]
|
||||
);
|
||||
} else {
|
||||
// Вставляем новую
|
||||
await db.getQuery()(
|
||||
`INSERT INTO email_settings (smtp_host_encrypted, smtp_port, smtp_user_encrypted, smtp_password_encrypted, imap_host_encrypted, imap_port, from_email_encrypted) VALUES (encrypt_text($1, $8), $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), $6, encrypt_text($7, $8))`,
|
||||
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, encryptionKey]
|
||||
);
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Error updating email settings:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Получение telegram-настроек
|
||||
router.get('/telegram-settings', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await db.getQuery()(
|
||||
'SELECT id, created_at, updated_at, decrypt_text(bot_token_encrypted, $1) as bot_token, decrypt_text(bot_username_encrypted, $1) as bot_username FROM telegram_settings ORDER BY id LIMIT 1',
|
||||
[encryptionKey]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
|
||||
const settings = rows[0];
|
||||
delete settings.bot_token; // не возвращаем токен
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
logger.error('Error getting telegram settings:', error, error && error.stack);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновление telegram-настроек
|
||||
router.put('/telegram-settings', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
try {
|
||||
const { bot_token, bot_username } = req.body;
|
||||
if (!bot_token || !bot_username) {
|
||||
return res.status(400).json({ success: false, error: 'Missing required fields' });
|
||||
}
|
||||
const { rows } = await db.getQuery()('SELECT id FROM telegram_settings ORDER BY id LIMIT 1');
|
||||
if (rows.length) {
|
||||
// Обновляем существующую запись
|
||||
await db.getQuery()(
|
||||
`UPDATE telegram_settings SET bot_token_encrypted=encrypt_text($1, $4), bot_username_encrypted=encrypt_text($2, $4), updated_at=NOW() WHERE id=$3`,
|
||||
[bot_token, bot_username, rows[0].id, encryptionKey]
|
||||
);
|
||||
} else {
|
||||
// Вставляем новую
|
||||
await db.getQuery()(
|
||||
`INSERT INTO telegram_settings (bot_token_encrypted, bot_username_encrypted) VALUES (encrypt_text($1, $3), encrypt_text($2, $3))` ,
|
||||
[bot_token, bot_username, encryptionKey]
|
||||
);
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Error updating telegram settings:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
// Дублирующиеся маршруты email/telegram-settings удалены - используются маршруты из settings.js
|
||||
|
||||
// Получение db-настроек
|
||||
router.get('/db-settings', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
const { rows } = await db.getQuery()(
|
||||
|
||||
@@ -14,8 +14,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||||
const telegramBot = require('../services/telegramBot');
|
||||
const emailBot = new (require('../services/emailBot'))();
|
||||
const botManager = require('../services/botManager');
|
||||
const { isUserBlocked } = require('../utils/userUtils');
|
||||
|
||||
// GET /api/messages?userId=123
|
||||
@@ -23,43 +22,32 @@ router.get('/', async (req, res) => {
|
||||
const userId = req.query.userId;
|
||||
const conversationId = req.query.conversationId;
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (conversationId) {
|
||||
result = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
|
||||
FROM messages
|
||||
WHERE conversation_id = $1 AND message_type = 'user_chat'
|
||||
WHERE conversation_id = $1
|
||||
ORDER BY created_at ASC`,
|
||||
[conversationId, encryptionKey]
|
||||
);
|
||||
} else if (userId) {
|
||||
result = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
|
||||
FROM messages
|
||||
WHERE user_id = $1 AND message_type = 'user_chat'
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at ASC`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
} else {
|
||||
result = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data
|
||||
FROM messages
|
||||
WHERE message_type = 'user_chat'
|
||||
ORDER BY created_at ASC`,
|
||||
[encryptionKey]
|
||||
);
|
||||
@@ -73,48 +61,10 @@ router.get('/', async (req, res) => {
|
||||
// POST /api/messages
|
||||
router.post('/', async (req, res) => {
|
||||
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
|
||||
|
||||
// Определяем тип сообщения
|
||||
const senderId = req.user && req.user.id;
|
||||
let messageType = 'user_chat'; // по умолчанию для публичных сообщений
|
||||
|
||||
if (senderId) {
|
||||
// Проверяем, является ли отправитель админом
|
||||
const senderCheck = await db.getQuery()(
|
||||
'SELECT role FROM users WHERE id = $1',
|
||||
[senderId]
|
||||
);
|
||||
|
||||
if (senderCheck.rows.length > 0 && (senderCheck.rows[0].role === 'editor' || senderCheck.rows[0].role === 'readonly')) {
|
||||
// Если отправитель админ, проверяем получателя
|
||||
const recipientCheck = await db.getQuery()(
|
||||
'SELECT role FROM users WHERE id = $1',
|
||||
[user_id]
|
||||
);
|
||||
|
||||
// Если получатель тоже админ, то это приватное сообщение
|
||||
if (recipientCheck.rows.length > 0 && (recipientCheck.rows[0].role === 'editor' || recipientCheck.rows[0].role === 'readonly')) {
|
||||
messageType = 'admin_chat';
|
||||
} else {
|
||||
// Если получатель обычный пользователь, то это публичное сообщение
|
||||
messageType = 'user_chat';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
// Проверка блокировки пользователя
|
||||
@@ -149,72 +99,29 @@ router.post('/', async (req, res) => {
|
||||
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
|
||||
}
|
||||
}
|
||||
// 1. Проверяем, есть ли беседа для user_id
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[user_id, encryptionKey]
|
||||
);
|
||||
let conversation;
|
||||
|
||||
if (messageType === 'admin_chat') {
|
||||
// Для админских сообщений ищем приватную беседу через conversation_participants
|
||||
let conversationResult = await db.getQuery()(`
|
||||
SELECT c.id
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||
WHERE c.conversation_type = 'admin_chat'
|
||||
LIMIT 1
|
||||
`, [senderId, user_id]);
|
||||
|
||||
if (conversationResult.rows.length === 0) {
|
||||
// Создаем новую приватную беседу между админами
|
||||
const title = `Приватная беседа ${senderId} - ${user_id}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *',
|
||||
[user_id, title, encryptionKey, 'admin_chat']
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
|
||||
// Добавляем участников в беседу
|
||||
await db.getQuery()(
|
||||
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)',
|
||||
[conversation.id, senderId, user_id]
|
||||
);
|
||||
} else {
|
||||
conversation = { id: conversationResult.rows[0].id };
|
||||
}
|
||||
} else {
|
||||
// Для обычных пользовательских сообщений используем старую логику с user_id
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[user_id, encryptionKey]
|
||||
if (conversationResult.rows.length === 0) {
|
||||
// 2. Если нет — создаём новую беседу
|
||||
const title = `Чат с пользователем ${user_id}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||||
[user_id, title, encryptionKey]
|
||||
);
|
||||
|
||||
if (conversationResult.rows.length === 0) {
|
||||
// Создаем новую беседу
|
||||
const title = `Чат с пользователем ${user_id}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *',
|
||||
[user_id, title, encryptionKey, 'user_chat']
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
conversation = conversationResult.rows[0];
|
||||
}
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
conversation = conversationResult.rows[0];
|
||||
}
|
||||
// 3. Сохраняем сообщение с conversation_id
|
||||
let result;
|
||||
if (messageType === 'admin_chat') {
|
||||
// Для админских сообщений добавляем sender_id
|
||||
result = await db.getQuery()(
|
||||
`INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||||
VALUES ($1,$2,$3,encrypt_text($4,$13),encrypt_text($5,$13),encrypt_text($6,$13),encrypt_text($7,$13),encrypt_text($8,$13),$9,NOW(),encrypt_text($10,$13),encrypt_text($11,$13),$12,$14) RETURNING *`,
|
||||
[conversation.id, user_id, senderId, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||||
);
|
||||
} else {
|
||||
// Для обычных сообщений без sender_id
|
||||
result = await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||||
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),$13,NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
|
||||
[user_id, conversation.id, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||||
);
|
||||
}
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||||
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
|
||||
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||||
);
|
||||
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
|
||||
if (channel === 'telegram' && direction === 'out') {
|
||||
try {
|
||||
@@ -228,10 +135,15 @@ router.post('/', async (req, res) => {
|
||||
if (tgIdentity.rows.length > 0) {
|
||||
const telegramId = tgIdentity.rows[0].provider_id;
|
||||
// console.log(`[messages.js] Отправка сообщения в Telegram ID: ${telegramId}, текст: ${content}`);
|
||||
const bot = await telegramBot.getBot();
|
||||
try {
|
||||
const sendResult = await bot.telegram.sendMessage(telegramId, content);
|
||||
// console.log(`[messages.js] Результат отправки в Telegram:`, sendResult);
|
||||
const telegramBot = botManager.getBot('telegram');
|
||||
if (telegramBot && telegramBot.isInitialized) {
|
||||
const bot = telegramBot.getBot();
|
||||
const sendResult = await bot.telegram.sendMessage(telegramId, content);
|
||||
// console.log(`[messages.js] Результат отправки в Telegram:`, sendResult);
|
||||
} else {
|
||||
logger.warn('[messages.js] Telegram Bot не инициализирован');
|
||||
}
|
||||
} catch (sendErr) {
|
||||
// console.error(`[messages.js] Ошибка при отправке в Telegram:`, sendErr);
|
||||
}
|
||||
@@ -252,16 +164,18 @@ router.post('/', async (req, res) => {
|
||||
);
|
||||
if (emailIdentity.rows.length > 0) {
|
||||
const email = emailIdentity.rows[0].provider_id;
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
const emailBot = botManager.getBot('email');
|
||||
if (emailBot && emailBot.isInitialized) {
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
} else {
|
||||
logger.warn('[messages.js] Email Bot не инициализирован для отправки');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// console.error('[messages.js] Ошибка отправки email:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомления
|
||||
broadcastMessagesUpdate();
|
||||
|
||||
res.json({ success: true, message: result.rows[0] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'DB error', details: e.message });
|
||||
@@ -274,8 +188,7 @@ router.post('/mark-read', async (req, res) => {
|
||||
// console.log('[DEBUG] /mark-read req.user:', req.user);
|
||||
// console.log('[DEBUG] /mark-read req.body:', req.body);
|
||||
const adminId = req.user && req.user.id;
|
||||
const { userId, lastReadAt, messageType = 'user_chat' } = req.body;
|
||||
|
||||
const { userId, lastReadAt } = req.body;
|
||||
if (!adminId) {
|
||||
// console.error('[ERROR] /mark-read: adminId (req.user.id) is missing');
|
||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||
@@ -284,30 +197,12 @@ router.post('/mark-read', async (req, res) => {
|
||||
// console.error('[ERROR] /mark-read: userId or lastReadAt missing');
|
||||
return res.status(400).json({ error: 'userId and lastReadAt required' });
|
||||
}
|
||||
|
||||
// Логика зависит от типа сообщения
|
||||
if (messageType === 'user_chat') {
|
||||
// Обновляем глобальный статус для всех админов
|
||||
await db.query(`
|
||||
INSERT INTO global_read_status (user_id, last_read_at, updated_by_admin_id)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
last_read_at = EXCLUDED.last_read_at,
|
||||
updated_by_admin_id = EXCLUDED.updated_by_admin_id,
|
||||
updated_at = NOW()
|
||||
`, [userId, lastReadAt, adminId]);
|
||||
} else if (messageType === 'admin_chat') {
|
||||
// Обновляем персональный статус для админских сообщений
|
||||
await db.query(`
|
||||
INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at
|
||||
`, [adminId, userId, lastReadAt]);
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' });
|
||||
}
|
||||
|
||||
res.json({ success: true, messageType });
|
||||
await db.query(`
|
||||
INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at
|
||||
`, [adminId, userId, lastReadAt]);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
// console.error('[ERROR] /mark-read:', e);
|
||||
res.status(500).json({ error: e.message });
|
||||
@@ -321,24 +216,11 @@ router.get('/read-status', async (req, res) => {
|
||||
// console.log('[DEBUG] /read-status req.session:', req.session);
|
||||
// console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId);
|
||||
const adminId = req.user && req.user.id;
|
||||
const { messageType = 'user_chat' } = req.query;
|
||||
|
||||
if (!adminId) {
|
||||
// console.error('[ERROR] /read-status: adminId (req.user.id) is missing');
|
||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||
}
|
||||
|
||||
let result;
|
||||
if (messageType === 'user_chat') {
|
||||
// Возвращаем глобальный статус для сообщений с пользователями
|
||||
result = await db.query('SELECT user_id, last_read_at FROM global_read_status');
|
||||
} else if (messageType === 'admin_chat') {
|
||||
// Возвращаем персональный статус для админских сообщений
|
||||
result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]);
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' });
|
||||
}
|
||||
|
||||
const result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]);
|
||||
// console.log('[DEBUG] /read-status SQL result:', result.rows);
|
||||
const map = {};
|
||||
for (const row of result.rows) {
|
||||
@@ -374,19 +256,9 @@ router.post('/conversations', async (req, res) => {
|
||||
const { userId, title } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
const conversationTitle = title || `Чат с пользователем ${userId}`;
|
||||
@@ -407,19 +279,9 @@ router.post('/broadcast', async (req, res) => {
|
||||
return res.status(400).json({ error: 'user_id и content обязательны' });
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
// Получаем все идентификаторы пользователя
|
||||
@@ -450,15 +312,21 @@ router.post('/broadcast', async (req, res) => {
|
||||
const email = identities.find(i => i.provider === 'email')?.provider_id;
|
||||
if (email) {
|
||||
try {
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
// Сохраняем в messages с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', 'user_chat', encryptionKey]
|
||||
);
|
||||
results.push({ channel: 'email', status: 'sent' });
|
||||
sent = true;
|
||||
const emailBot = botManager.getBot('email');
|
||||
if (emailBot && emailBot.isInitialized) {
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
// Сохраняем в messages с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey, 'user_chat']
|
||||
);
|
||||
results.push({ channel: 'email', status: 'sent' });
|
||||
sent = true;
|
||||
} else {
|
||||
logger.warn('[messages.js] Email Bot не инициализирован');
|
||||
results.push({ channel: 'email', status: 'error', error: 'Bot not initialized' });
|
||||
}
|
||||
} catch (err) {
|
||||
results.push({ channel: 'email', status: 'error', error: err.message });
|
||||
}
|
||||
@@ -467,15 +335,21 @@ router.post('/broadcast', async (req, res) => {
|
||||
const telegram = identities.find(i => i.provider === 'telegram')?.provider_id;
|
||||
if (telegram) {
|
||||
try {
|
||||
const bot = await telegramBot.getBot();
|
||||
await bot.telegram.sendMessage(telegram, content);
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', 'user_chat', encryptionKey]
|
||||
);
|
||||
results.push({ channel: 'telegram', status: 'sent' });
|
||||
sent = true;
|
||||
const telegramBot = botManager.getBot('telegram');
|
||||
if (telegramBot && telegramBot.isInitialized) {
|
||||
const bot = telegramBot.getBot();
|
||||
await bot.telegram.sendMessage(telegram, content);
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey, 'user_chat']
|
||||
);
|
||||
results.push({ channel: 'telegram', status: 'sent' });
|
||||
sent = true;
|
||||
} else {
|
||||
logger.warn('[messages.js] Telegram Bot не инициализирован');
|
||||
results.push({ channel: 'telegram', status: 'error', error: 'Bot not initialized' });
|
||||
}
|
||||
} catch (err) {
|
||||
results.push({ channel: 'telegram', status: 'error', error: err.message });
|
||||
}
|
||||
@@ -520,13 +394,19 @@ router.delete('/history/:userId', async (req, res) => {
|
||||
[userId]
|
||||
);
|
||||
|
||||
// Удаляем хеши дедупликации для этого пользователя
|
||||
const dedupResult = await db.getQuery()(
|
||||
'DELETE FROM message_deduplication WHERE user_id = $1 RETURNING id',
|
||||
[userId]
|
||||
);
|
||||
|
||||
// Удаляем беседы пользователя (если есть)
|
||||
const conversationResult = await db.getQuery()(
|
||||
'DELETE FROM conversations WHERE user_id = $1 RETURNING id',
|
||||
[userId]
|
||||
);
|
||||
|
||||
console.log(`[messages.js] Deleted ${result.rowCount} messages and ${conversationResult.rowCount} conversations for user ${userId}`);
|
||||
console.log(`[messages.js] Deleted ${result.rowCount} messages, ${dedupResult.rowCount} deduplication hashes, and ${conversationResult.rowCount} conversations for user ${userId}`);
|
||||
|
||||
// Отправляем обновление через WebSocket
|
||||
broadcastMessagesUpdate();
|
||||
@@ -542,254 +422,4 @@ router.delete('/history/:userId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/messages/admin/send - отправка сообщения админу
|
||||
router.post('/admin/send', async (req, res) => {
|
||||
try {
|
||||
const adminId = req.user && req.user.id;
|
||||
const { recipientAdminId, content } = req.body;
|
||||
|
||||
if (!adminId) {
|
||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||
}
|
||||
if (!recipientAdminId || !content) {
|
||||
return res.status(400).json({ error: 'recipientAdminId and content required' });
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Ищем существующую приватную беседу между двумя админами через conversation_participants
|
||||
let conversationResult = await db.getQuery()(`
|
||||
SELECT c.id
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||
WHERE c.conversation_type = 'admin_chat'
|
||||
LIMIT 1
|
||||
`, [adminId, recipientAdminId]);
|
||||
|
||||
let conversationId;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
// Создаем новую приватную беседу между админами
|
||||
const title = `Приватная беседа ${adminId} - ${recipientAdminId}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING id',
|
||||
[recipientAdminId, title, encryptionKey, 'admin_chat']
|
||||
);
|
||||
conversationId = newConv.rows[0].id;
|
||||
|
||||
// Добавляем участников в беседу
|
||||
await db.getQuery()(
|
||||
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)',
|
||||
[conversationId, adminId, recipientAdminId]
|
||||
);
|
||||
|
||||
console.log(`[admin/send] Создана новая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`);
|
||||
} else {
|
||||
conversationId = conversationResult.rows[0].id;
|
||||
console.log(`[admin/send] Найдена существующая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`);
|
||||
}
|
||||
|
||||
// Сохраняем сообщение с типом 'admin_chat'
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, $3, encrypt_text($4, $9), encrypt_text($5, $9), encrypt_text($6, $9), encrypt_text($7, $9), encrypt_text($8, $9), $10, NOW()) RETURNING id`,
|
||||
[conversationId, recipientAdminId, adminId, 'admin', content, 'web', 'admin', 'out', encryptionKey, 'admin_chat']
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомления
|
||||
broadcastMessagesUpdate();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messageId: result.rows[0].id,
|
||||
conversationId,
|
||||
messageType: 'admin_chat'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[ERROR] /admin/send:', e);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/admin/conversations - получить личные чаты админа
|
||||
router.get('/admin/conversations', async (req, res) => {
|
||||
try {
|
||||
const adminId = req.user && req.user.id;
|
||||
|
||||
if (!adminId) {
|
||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||
}
|
||||
|
||||
// Получаем список админов, с которыми есть переписка
|
||||
const conversations = await db.query(`
|
||||
SELECT DISTINCT
|
||||
CASE
|
||||
WHEN sender_type = 'admin' AND user_id != $1 THEN user_id
|
||||
ELSE sender_id
|
||||
END as admin_id,
|
||||
MAX(created_at) as last_message_at
|
||||
FROM messages
|
||||
WHERE message_type = 'admin_chat'
|
||||
AND (user_id = $1 OR sender_id = $1)
|
||||
GROUP BY admin_id
|
||||
ORDER BY last_message_at DESC
|
||||
`, [adminId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
conversations: conversations.rows
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[ERROR] /admin/conversations:', e);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/admin/contacts - получить админов для приватного чата
|
||||
router.get('/admin/contacts', async (req, res) => {
|
||||
try {
|
||||
const adminId = req.user && req.user.id;
|
||||
|
||||
if (!adminId) {
|
||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Получаем всех пользователей, с которыми есть приватные беседы через conversation_participants
|
||||
const adminContacts = await db.getQuery()(`
|
||||
SELECT DISTINCT
|
||||
other_user.id,
|
||||
COALESCE(
|
||||
decrypt_text(other_user.first_name_encrypted, $2),
|
||||
decrypt_text(other_user.username_encrypted, $2),
|
||||
'Пользователь ' || other_user.id
|
||||
) as name,
|
||||
'admin@system' as email,
|
||||
CASE
|
||||
WHEN other_user.role = 'editor' THEN 'admin'
|
||||
WHEN other_user.role = 'readonly' THEN 'admin'
|
||||
ELSE 'user'
|
||||
END as contact_type,
|
||||
MAX(m.created_at) as last_message_at,
|
||||
COUNT(m.id) as message_count
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_participants cp_current ON cp_current.conversation_id = c.id AND cp_current.user_id = $1
|
||||
INNER JOIN conversation_participants cp_other ON cp_other.conversation_id = c.id AND cp_other.user_id != $1
|
||||
INNER JOIN users other_user ON other_user.id = cp_other.user_id
|
||||
LEFT JOIN messages m ON m.conversation_id = c.id AND m.message_type = 'admin_chat'
|
||||
WHERE c.conversation_type = 'admin_chat'
|
||||
GROUP BY
|
||||
other_user.id,
|
||||
other_user.first_name_encrypted,
|
||||
other_user.username_encrypted,
|
||||
other_user.role
|
||||
ORDER BY MAX(m.created_at) DESC
|
||||
`, [adminId, encryptionKey]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
contacts: adminContacts.rows.map(contact => ({
|
||||
...contact,
|
||||
created_at: contact.last_message_at, // Используем время последнего сообщения как время создания для сортировки
|
||||
telegram: null,
|
||||
wallet: null
|
||||
}))
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[ERROR] /admin/contacts:', e);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/admin/:adminId - получить сообщения с конкретным админом
|
||||
router.get('/admin/:adminId', async (req, res) => {
|
||||
try {
|
||||
const currentAdminId = req.user && req.user.id;
|
||||
const { adminId } = req.params;
|
||||
|
||||
if (!currentAdminId) {
|
||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Получаем сообщения из приватной беседы между админами через conversation_participants
|
||||
const result = await db.getQuery()(
|
||||
`SELECT m.id, m.user_id, m.sender_id,
|
||||
decrypt_text(m.sender_type_encrypted, $3) as sender_type,
|
||||
decrypt_text(m.content_encrypted, $3) as content,
|
||||
decrypt_text(m.channel_encrypted, $3) as channel,
|
||||
decrypt_text(m.role_encrypted, $3) as role,
|
||||
decrypt_text(m.direction_encrypted, $3) as direction,
|
||||
m.created_at, m.message_type,
|
||||
-- Получаем wallet адреса отправителей (расшифровываем provider_id_encrypted)
|
||||
CASE
|
||||
WHEN sender_ui.provider_encrypted = encrypt_text('wallet', $3)
|
||||
THEN decrypt_text(sender_ui.provider_id_encrypted, $3)
|
||||
ELSE 'Админ'
|
||||
END as sender_wallet,
|
||||
CASE
|
||||
WHEN recipient_ui.provider_encrypted = encrypt_text('wallet', $3)
|
||||
THEN decrypt_text(recipient_ui.provider_id_encrypted, $3)
|
||||
ELSE 'Админ'
|
||||
END as recipient_wallet
|
||||
FROM messages m
|
||||
INNER JOIN conversations c ON c.id = m.conversation_id
|
||||
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||
LEFT JOIN user_identities sender_ui ON sender_ui.user_id = m.sender_id
|
||||
LEFT JOIN user_identities recipient_ui ON recipient_ui.user_id = m.user_id
|
||||
WHERE m.message_type = 'admin_chat' AND c.conversation_type = 'admin_chat'
|
||||
ORDER BY m.created_at ASC`,
|
||||
[currentAdminId, adminId, encryptionKey]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messages: result.rows,
|
||||
messageType: 'admin_chat'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[ERROR] /admin/:adminId:', e);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -16,8 +16,8 @@ const axios = require('axios');
|
||||
const db = require('../db');
|
||||
const aiAssistant = require('../services/ai-assistant');
|
||||
const aiCache = require('../services/ai-cache');
|
||||
const aiQueue = require('../services/ai-queue');
|
||||
const logger = require('../utils/logger');
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const results = {};
|
||||
@@ -37,7 +37,8 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// Ollama
|
||||
try {
|
||||
const ollama = await axios.get(process.env.OLLAMA_BASE_URL ? process.env.OLLAMA_BASE_URL + '/api/tags' : 'http://ollama:11434/api/tags', { timeout: 2000 });
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
const ollama = await axios.get(ollamaConfig.getApiUrl('tags'), { timeout: 2000 });
|
||||
results.ollama = { status: 'ok', models: ollama.data.models?.length || 0 };
|
||||
} catch (e) {
|
||||
results.ollama = { status: 'error', error: e.message };
|
||||
@@ -57,25 +58,27 @@ router.get('/', async (req, res) => {
|
||||
// GET /api/monitoring/ai-stats - статистика AI
|
||||
router.get('/ai-stats', async (req, res) => {
|
||||
try {
|
||||
const aiHealth = await aiAssistant.checkHealth();
|
||||
const cacheStats = aiCache.getStats();
|
||||
const queueStats = aiQueue.getStats();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
ai: {
|
||||
health: aiHealth,
|
||||
model: process.env.OLLAMA_MODEL || 'qwen2.5:7b',
|
||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||
health: 'ok',
|
||||
model: ollamaConfig.getDefaultModel(),
|
||||
baseUrl: ollamaConfig.getBaseUrl()
|
||||
},
|
||||
cache: {
|
||||
...cacheStats,
|
||||
hitRate: `${(cacheStats.hitRate * 100).toFixed(1)}%`
|
||||
size: 0,
|
||||
maxSize: 100,
|
||||
hitRate: 0
|
||||
},
|
||||
queue: {
|
||||
...queueStats,
|
||||
avgResponseTime: `${queueStats.avgResponseTime.toFixed(0)}ms`
|
||||
totalAdded: 0,
|
||||
totalProcessed: 0,
|
||||
totalFailed: 0,
|
||||
averageProcessingTime: 0,
|
||||
currentQueueSize: 0,
|
||||
lastProcessedAt: null,
|
||||
uptime: 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -107,7 +110,7 @@ router.post('/ai-cache/clear', async (req, res) => {
|
||||
// POST /api/monitoring/ai-queue/clear - очистка очереди
|
||||
router.post('/ai-queue/clear', async (req, res) => {
|
||||
try {
|
||||
aiQueue.clear();
|
||||
aiAssistant.aiQueue.clearQueue();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'AI queue cleared successfully'
|
||||
|
||||
@@ -22,7 +22,8 @@ const { requireAuth } = require('../middleware/auth');
|
||||
router.get('/status', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
// Проверяем API Ollama через HTTP запрос
|
||||
try {
|
||||
@@ -54,7 +55,8 @@ router.get('/status', requireAuth, async (req, res) => {
|
||||
router.get('/models', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
|
||||
@@ -22,18 +22,9 @@ async function ensureAdminPagesTable(fields) {
|
||||
const tableName = `admin_pages_simple`;
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Проверяем, есть ли таблица
|
||||
const existsRes = await db.getQuery()(
|
||||
@@ -131,18 +122,9 @@ router.get('/', async (req, res) => {
|
||||
const tableName = `admin_pages_simple`;
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Проверяем, есть ли таблица
|
||||
const existsRes = await db.getQuery()(
|
||||
|
||||
@@ -49,9 +49,7 @@ const aiAssistant = require('../services/ai-assistant');
|
||||
const dns = require('node:dns').promises;
|
||||
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
||||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||
const telegramBot = require('../services/telegramBot');
|
||||
const EmailBotService = require('../services/emailBot');
|
||||
const emailBotService = new EmailBotService();
|
||||
const botsSettings = require('../services/botsSettings');
|
||||
const dbSettingsService = require('../services/dbSettingsService');
|
||||
const { broadcastAuthTokenAdded, broadcastAuthTokenDeleted, broadcastAuthTokenUpdated } = require('../wsHub');
|
||||
|
||||
@@ -76,16 +74,9 @@ router.get('/rpc', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const rpcProvidersResult = await db.getQuery()(
|
||||
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
|
||||
@@ -165,16 +156,9 @@ router.get('/auth-tokens', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const tokensResult = await db.getQuery()(
|
||||
'SELECT id, min_balance, readonly_threshold, editor_threshold, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
|
||||
@@ -510,7 +494,7 @@ router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) =>
|
||||
// Получить текущие настройки Email (для страницы Email)
|
||||
router.get('/email-settings', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const settings = await emailBotService.getSettingsFromDb();
|
||||
const settings = await botsSettings.getEmailSettings();
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
@@ -556,7 +540,7 @@ router.put('/email-settings', requireAdmin, async (req, res, next) => {
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
const result = await emailBotService.saveEmailSettings(settings);
|
||||
const result = await botsSettings.saveEmailSettings(settings);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при обновлении email настроек:', error);
|
||||
@@ -577,11 +561,7 @@ router.post('/email-settings/test', requireAdmin, async (req, res, next) => {
|
||||
}
|
||||
|
||||
// Отправляем тестовое письмо
|
||||
const result = await emailBotService.sendEmail(
|
||||
test_email,
|
||||
'Тест Email системы DLE',
|
||||
'Это тестовое письмо для проверки работы email системы. Если вы его получили, значит настройки работают корректно!'
|
||||
);
|
||||
const result = await botsSettings.testEmailSMTP(test_email);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -597,7 +577,7 @@ router.post('/email-settings/test', requireAdmin, async (req, res, next) => {
|
||||
// Тест IMAP подключения
|
||||
router.post('/email-settings/test-imap', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const result = await emailBotService.testImapConnection();
|
||||
const result = await botsSettings.testEmailIMAP();
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при тестировании IMAP подключения:', error);
|
||||
@@ -608,7 +588,7 @@ router.post('/email-settings/test-imap', requireAdmin, async (req, res, next) =>
|
||||
// Тест SMTP подключения
|
||||
router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const result = await emailBotService.testSmtpConnection();
|
||||
const result = await botsSettings.testEmailSMTP();
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при тестировании SMTP подключения:', error);
|
||||
@@ -619,7 +599,7 @@ router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) =>
|
||||
// Получить список всех email (для ассистента)
|
||||
router.get('/email-settings/list', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const emails = await emailBotService.getAllEmailSettings();
|
||||
const emails = await botsSettings.getAllEmailSettings();
|
||||
res.json({ success: true, items: emails });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
@@ -629,7 +609,7 @@ router.get('/email-settings/list', requireAdmin, async (req, res) => {
|
||||
// Получить текущие настройки Telegram-бота (для страницы Telegram)
|
||||
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const settings = await telegramBot.getTelegramSettings();
|
||||
const settings = await botsSettings.getTelegramSettings();
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
@@ -657,7 +637,7 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
const result = await telegramBot.saveTelegramSettings(settings);
|
||||
const result = await botsSettings.saveTelegramSettings(settings);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при обновлении настроек Telegram:', error);
|
||||
@@ -668,7 +648,7 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
// Получить список всех Telegram-ботов (для ассистента)
|
||||
router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const bots = await telegramBot.getAllBots();
|
||||
const bots = await botsSettings.getAllTelegramBots();
|
||||
res.json({ success: true, items: bots });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
|
||||
@@ -1,99 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
* Системные endpoints для управления готовностью системы
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const memoryMonitor = require('../utils/memoryMonitor');
|
||||
const logger = require('../utils/logger');
|
||||
const { checkAdminRole } = require('../services/admin-role');
|
||||
|
||||
// Middleware для проверки прав администратора
|
||||
const requireAdmin = async (req, res, next) => {
|
||||
/**
|
||||
* HTTP fallback endpoint для Ollama контейнера
|
||||
* Используется когда WebSocket недоступен
|
||||
*/
|
||||
router.post('/ollama-ready', async (req, res) => {
|
||||
try {
|
||||
if (!req.session || !req.session.userId) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const isAdmin = await checkAdminRole(req.session.userId);
|
||||
if (!isAdmin) {
|
||||
return res.status(403).json({ success: false, error: 'Admin access required' });
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Error checking admin role:', error);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// GET /api/system/memory - Получить информацию о памяти
|
||||
router.get('/memory', requireAdmin, (req, res) => {
|
||||
try {
|
||||
const memoryUsage = memoryMonitor.getMemoryUsage();
|
||||
logger.info('[System] 🔌 Ollama готов к работе');
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
memory: memoryUsage,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
message: 'Ollama готов'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting memory usage:', error);
|
||||
res.status(500).json({ success: false, error: 'Failed to get memory usage' });
|
||||
logger.error('[System] ❌ Ошибка:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/system/memory/start - Запустить мониторинг памяти
|
||||
router.post('/memory/start', requireAdmin, (req, res) => {
|
||||
/**
|
||||
* Endpoint для проверки статуса системы
|
||||
*/
|
||||
router.get('/status', (req, res) => {
|
||||
try {
|
||||
const { interval } = req.body;
|
||||
memoryMonitor.start(interval || 60000);
|
||||
res.json({ success: true, message: 'Memory monitoring started' });
|
||||
} catch (error) {
|
||||
logger.error('Error starting memory monitoring:', error);
|
||||
res.status(500).json({ success: false, error: 'Failed to start memory monitoring' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/system/memory/stop - Остановить мониторинг памяти
|
||||
router.post('/memory/stop', requireAdmin, (req, res) => {
|
||||
try {
|
||||
memoryMonitor.stop();
|
||||
res.json({ success: true, message: 'Memory monitoring stopped' });
|
||||
} catch (error) {
|
||||
logger.error('Error stopping memory monitoring:', error);
|
||||
res.status(500).json({ success: false, error: 'Failed to stop memory monitoring' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/system/health - Проверка здоровья системы
|
||||
router.get('/health', (req, res) => {
|
||||
try {
|
||||
const memoryUsage = memoryMonitor.getMemoryUsage();
|
||||
const uptime = process.uptime();
|
||||
|
||||
const botManager = require('../services/botManager');
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: 'healthy',
|
||||
uptime: Math.round(uptime),
|
||||
memory: memoryUsage,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
systemReady: true, // Система всегда готова после запуска
|
||||
botsInitialized: botManager.isInitialized,
|
||||
bots: botManager.getStatus(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting system health:', error);
|
||||
res.status(500).json({ success: false, error: 'Failed to get system health' });
|
||||
logger.error('[System] Ошибка получения статуса:', error);
|
||||
|
||||
res.status(500).json({
|
||||
error: error.message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
@@ -21,14 +21,8 @@ const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHu
|
||||
|
||||
// Вспомогательная функция для получения ключа шифрования
|
||||
function getEncryptionKey() {
|
||||
const fs = require('fs');
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
|
||||
if (!fs.existsSync(keyPath)) {
|
||||
throw new Error('Encryption key file not found');
|
||||
}
|
||||
|
||||
return fs.readFileSync(keyPath, 'utf8').trim();
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
return encryptionUtils.getEncryptionKey();
|
||||
}
|
||||
|
||||
router.use((req, res, next) => {
|
||||
@@ -39,14 +33,9 @@ router.use((req, res, next) => {
|
||||
// Получить список всех таблиц (доступно всем)
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const result = await db.getQuery()('SELECT id, created_at, updated_at, is_rag_source_id, decrypt_text(name_encrypted, $1) as name, decrypt_text(description_encrypted, $1) as description FROM user_tables ORDER BY id', [encryptionKey]);
|
||||
res.json(result.rows);
|
||||
@@ -60,14 +49,9 @@ router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const { name, description, isRagSourceId } = req.body;
|
||||
|
||||
// Получаем ключ шифрования
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const result = await db.getQuery()(
|
||||
'INSERT INTO user_tables (name_encrypted, description_encrypted, is_rag_source_id) VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3) RETURNING *',
|
||||
@@ -82,14 +66,9 @@ router.post('/', async (req, res, next) => {
|
||||
// Получить данные из таблицы is_rag_source с расшифровкой
|
||||
router.get('/rag-sources', async (req, res, next) => {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const result = await db.getQuery()(
|
||||
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id',
|
||||
@@ -107,14 +86,9 @@ router.get('/rag-sources', async (req, res, next) => {
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
// Получаем ключ шифрования
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Выполняем все 4 запроса параллельно для ускорения
|
||||
const [tableMetaResult, columnsResult, rowsResult, cellValuesResult] = await Promise.all([
|
||||
@@ -193,25 +167,9 @@ router.post('/:id/columns', async (req, res, next) => {
|
||||
finalOptions.purpose = purpose;
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем уже существующие плейсхолдеры во всей базе данных
|
||||
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE placeholder IS NOT NULL', [])).rows;
|
||||
@@ -237,25 +195,9 @@ router.post('/:id/rows', async (req, res, next) => {
|
||||
[tableId]
|
||||
);
|
||||
// console.log('[DEBUG][addRow] result.rows[0]:', result.rows[0]);
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем все строки и значения для upsert
|
||||
const rows = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
|
||||
@@ -277,25 +219,9 @@ router.get('/:id/rows', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
const { product, tags, ...relationFilters } = req.query; // tags = "B2B,VIP", relation_{colId}=rowId
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем все столбцы, строки и значения ячеек
|
||||
const columns = (await db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows;
|
||||
@@ -368,25 +294,9 @@ router.get('/:id/rows', async (req, res, next) => {
|
||||
router.post('/cell', async (req, res, next) => {
|
||||
try {
|
||||
const { row_id, column_id, value } = req.body;
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO user_cell_values (row_id, column_id, value_encrypted) VALUES ($1, $2, encrypt_text($3, $4))
|
||||
@@ -438,25 +348,9 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
||||
|
||||
// Получаем все строки для rebuild
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const rows = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
|
||||
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
||||
@@ -513,25 +407,9 @@ router.patch('/column/:columnId', async (req, res, next) => {
|
||||
const columnId = req.params.columnId;
|
||||
const { name, type, options, order, placeholder } = req.body;
|
||||
// Получаем table_id для проверки уникальности плейсхолдера
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const colInfo = (await db.getQuery()('SELECT table_id, decrypt_text(name_encrypted, $2) as name FROM user_columns WHERE id = $1', [columnId, encryptionKey])).rows[0];
|
||||
if (!colInfo) return res.status(404).json({ error: 'Column not found' });
|
||||
@@ -644,24 +522,9 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
||||
return res.status(403).json({ error: 'Доступ только для администратора' });
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const tableId = req.params.id;
|
||||
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId);
|
||||
@@ -823,25 +686,9 @@ router.delete('/:tableId/row/:rowId/relations/:relationId', async (req, res, nex
|
||||
router.get('/:id/placeholders', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const columns = (await db.getQuery()('SELECT id, decrypt_text(name_encrypted, $2) as name, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows;
|
||||
res.json(columns.map(col => ({
|
||||
|
||||
@@ -89,19 +89,9 @@ router.post('/user/:rowId/multirelations', async (req, res) => {
|
||||
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id тегов
|
||||
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
|
||||
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||
|
||||
@@ -81,16 +81,9 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// --- Формируем условия ---
|
||||
const where = [];
|
||||
@@ -323,18 +316,8 @@ router.patch('/:id', requireAuth, async (req, res) => {
|
||||
let idx = 1;
|
||||
|
||||
// Получаем ключ шифрования один раз
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
console.log('Encryption key loaded:', encryptionKey.length, 'characters');
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Обработка поля name - разбиваем на first_name и last_name
|
||||
if (name !== undefined) {
|
||||
@@ -413,31 +396,11 @@ router.get('/:id', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
const query = db.getQuery();
|
||||
// Получаем пользователя
|
||||
@@ -485,16 +448,9 @@ router.post('/', async (req, res) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
@@ -514,16 +470,9 @@ router.post('/import', requireAuth, async (req, res) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
const contacts = req.body;
|
||||
|
||||
@@ -16,8 +16,7 @@ const http = require('http');
|
||||
const { initWSS } = require('./wsHub');
|
||||
const deploymentWebSocketService = require('./services/deploymentWebSocketService');
|
||||
const logger = require('./utils/logger');
|
||||
const { getBot } = require('./services/telegramBot');
|
||||
const EmailBotService = require('./services/emailBot');
|
||||
// systemReadinessService удален - теперь используется WebSocket endpoint
|
||||
const { initDbPool, seedAIAssistantSettings } = require('./db');
|
||||
const memoryMonitor = require('./utils/memoryMonitor');
|
||||
|
||||
@@ -27,63 +26,28 @@ const PORT = process.env.PORT || 8000;
|
||||
// console.log('Переменная окружения PORT:', process.env.PORT);
|
||||
// console.log('Используемый порт:', process.env.PORT || 8000);
|
||||
|
||||
// Инициализация сервисов
|
||||
async function initServices() {
|
||||
try {
|
||||
// console.log('Инициализация сервисов...');
|
||||
// console.log('[initServices] Запуск Email-бота...');
|
||||
// console.log('[initServices] Создаю экземпляр EmailBotService...');
|
||||
let emailBot;
|
||||
try {
|
||||
emailBot = new EmailBotService();
|
||||
// console.log('[initServices] Экземпляр EmailBotService создан');
|
||||
} catch (err) {
|
||||
// console.error('[initServices] Ошибка при создании экземпляра EmailBotService:', err);
|
||||
throw err;
|
||||
}
|
||||
// console.log('[initServices] Перед вызовом emailBot.start()');
|
||||
try {
|
||||
await emailBot.start();
|
||||
// console.log('[initServices] Email-бот успешно запущен');
|
||||
} catch (err) {
|
||||
// console.error('[initServices] Ошибка при запуске emailBot:', err);
|
||||
}
|
||||
// console.log('[initServices] Запуск Telegram-бота...');
|
||||
try {
|
||||
await getBot();
|
||||
// console.log('[initServices] Telegram-бот успешно запущен');
|
||||
} catch (err) {
|
||||
// console.error('[initServices] Ошибка при запуске Telegram-бота:', err);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Ошибка при инициализации сервисов:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const server = http.createServer(app);
|
||||
initWSS(server);
|
||||
|
||||
// WebSocket сервис для деплоя модулей теперь интегрирован в основной WebSocket сервер
|
||||
|
||||
// WebSocket уже инициализирован в wsHub.js
|
||||
|
||||
async function startServer() {
|
||||
await initDbPool(); // Дождаться пересоздания пула!
|
||||
await initDbPool();
|
||||
|
||||
// Инициализация AI ассистента В ФОНЕ (неблокирующая)
|
||||
seedAIAssistantSettings().catch(error => {
|
||||
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
|
||||
});
|
||||
|
||||
// Разогрев модели Ollama
|
||||
// console.log('🔥 Запуск разогрева модели...');
|
||||
setTimeout(() => {
|
||||
}, 10000); // Задержка 10 секунд для полной инициализации
|
||||
// Инициализация ботов сразу при старте (не ждем Ollama)
|
||||
console.log('[Server] ▶️ Импортируем BotManager...');
|
||||
const botManager = require('./services/botManager');
|
||||
console.log('[Server] ▶️ Вызываем botManager.initialize()...');
|
||||
botManager.initialize()
|
||||
.then(() => console.log('[Server] ✅ botManager.initialize() завершен'))
|
||||
.catch(error => {
|
||||
console.error('[Server] ❌ Ошибка botManager.initialize():', error.message);
|
||||
logger.error('[Server] Ошибка инициализации ботов:', error);
|
||||
});
|
||||
|
||||
// Запускаем сервисы в фоне (неблокирующе)
|
||||
initServices().catch(error => {
|
||||
console.warn('[Server] Ошибка инициализации сервисов:', error.message);
|
||||
});
|
||||
console.log(`✅ Server is running on port ${PORT}`);
|
||||
}
|
||||
|
||||
@@ -113,16 +77,36 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// Обработчики для корректного завершения
|
||||
process.on('SIGINT', async () => {
|
||||
// logger.info('[Server] Получен сигнал SIGINT, завершаем работу...'); // Убрано избыточное логирование
|
||||
memoryMonitor.stop();
|
||||
await initDbPool().then(pool => pool.end()); // Use initDbPool to get the pool
|
||||
console.log('[Server] Получен SIGINT, завершаем работу...');
|
||||
try {
|
||||
// Останавливаем боты
|
||||
const botManager = require('./services/botManager');
|
||||
if (botManager.isInitialized) {
|
||||
console.log('[Server] Останавливаем боты...');
|
||||
await botManager.stop();
|
||||
}
|
||||
memoryMonitor.stop();
|
||||
await initDbPool().then(pool => pool.end());
|
||||
} catch (error) {
|
||||
console.error('[Server] Ошибка при завершении:', error);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
// logger.info('[Server] Получен сигнал SIGTERM, завершаем работу...'); // Убрано избыточное логирование
|
||||
memoryMonitor.stop();
|
||||
await initDbPool().then(pool => pool.end()); // Use initDbPool to get the pool
|
||||
console.log('[Server] Получен SIGTERM, завершаем работу...');
|
||||
try {
|
||||
// Останавливаем боты
|
||||
const botManager = require('./services/botManager');
|
||||
if (botManager.isInitialized) {
|
||||
console.log('[Server] Останавливаем боты...');
|
||||
await botManager.stop();
|
||||
}
|
||||
memoryMonitor.stop();
|
||||
await initDbPool().then(pool => pool.end());
|
||||
} catch (error) {
|
||||
console.error('[Server] Ошибка при завершении:', error);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,19 +34,9 @@ async function checkAdminRole(address) {
|
||||
let foundTokens = false;
|
||||
let errorCount = 0;
|
||||
const balances = {};
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем токены и RPC из базы с расшифровкой
|
||||
const tokensResult = await db.getQuery()(
|
||||
|
||||
222
backend/services/adminLogicService.js
Normal file
222
backend/services/adminLogicService.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Сервис логики для админских функций
|
||||
* Определяет права доступа, приоритеты и логику работы с админами
|
||||
*/
|
||||
|
||||
/**
|
||||
* Определить тип отправителя на основе сессии
|
||||
* @param {Object} session - Сессия пользователя
|
||||
* @returns {Object} { senderType, role }
|
||||
*/
|
||||
function determineSenderType(session) {
|
||||
if (!session) {
|
||||
return { senderType: 'user', role: 'user' };
|
||||
}
|
||||
|
||||
if (session.isAdmin === true) {
|
||||
return { senderType: 'admin', role: 'admin' };
|
||||
}
|
||||
|
||||
return { senderType: 'user', role: 'user' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Определить, нужно ли генерировать AI ответ
|
||||
* @param {Object} params - Параметры
|
||||
* @param {string} params.senderType - Тип отправителя (user/admin)
|
||||
* @param {number} params.userId - ID пользователя
|
||||
* @param {number} params.recipientId - ID получателя
|
||||
* @param {string} params.channel - Канал (web/telegram/email)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldGenerateAiReply(params) {
|
||||
const { senderType, userId, recipientId } = params;
|
||||
|
||||
// Обычные пользователи всегда получают AI ответ
|
||||
if (senderType !== 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Админ, пишущий себе, получает AI ответ
|
||||
if (userId === recipientId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Админ, пишущий другому пользователю, не получает AI ответ
|
||||
// (это личное сообщение от админа)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли пользователь писать в беседу
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {number} params.userId - ID пользователя
|
||||
* @param {number} params.conversationUserId - ID владельца беседы
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function canWriteToConversation(params) {
|
||||
const { isAdmin, userId, conversationUserId } = params;
|
||||
|
||||
// Админ может писать в любую беседу
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обычный пользователь может писать только в свою беседу
|
||||
return userId === conversationUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить приоритет запроса для очереди AI
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {string} params.message - Текст сообщения
|
||||
* @param {Array} params.history - История сообщений
|
||||
* @returns {number} Приоритет (чем выше, тем важнее)
|
||||
*/
|
||||
function getRequestPriority(params) {
|
||||
const { isAdmin, message, history = [] } = params;
|
||||
|
||||
let priority = 10; // Базовый приоритет
|
||||
|
||||
// Админ получает повышенный приоритет
|
||||
if (isAdmin) {
|
||||
priority += 5;
|
||||
}
|
||||
|
||||
// Срочные ключевые слова
|
||||
const urgentKeywords = ['срочно', 'urgent', 'помогите', 'help', 'критично', 'critical'];
|
||||
const messageLC = (message || '').toLowerCase();
|
||||
|
||||
if (urgentKeywords.some(keyword => messageLC.includes(keyword))) {
|
||||
priority += 10;
|
||||
}
|
||||
|
||||
// Короткие сообщения обрабатываются быстрее
|
||||
if (message && message.length < 50) {
|
||||
priority += 5;
|
||||
}
|
||||
|
||||
// Первое сообщение в беседе
|
||||
if (!history || history.length === 0) {
|
||||
priority += 3;
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли пользователь выполнить админское действие
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {string} params.action - Название действия
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function canPerformAdminAction(params) {
|
||||
const { isAdmin, action } = params;
|
||||
|
||||
// Только админ может выполнять админские действия
|
||||
if (!isAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Список разрешенных админских действий
|
||||
const allowedActions = [
|
||||
'delete_message_history',
|
||||
'view_all_conversations',
|
||||
'manage_users',
|
||||
'manage_ai_settings',
|
||||
'broadcast_message',
|
||||
'delete_user',
|
||||
'modify_user_settings'
|
||||
];
|
||||
|
||||
return allowedActions.includes(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить настройки админа
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {string} params.channel - Канал
|
||||
* @returns {Object} Настройки
|
||||
*/
|
||||
function getAdminSettings(params) {
|
||||
const { isAdmin } = params;
|
||||
|
||||
if (!isAdmin) {
|
||||
// Ограниченные права для обычного пользователя
|
||||
return {
|
||||
canWriteToAnyConversation: false,
|
||||
canViewAllConversations: false,
|
||||
canManageUsers: false,
|
||||
canManageAISettings: false,
|
||||
aiReplyPriority: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Полные права для админа
|
||||
return {
|
||||
canWriteToAnyConversation: true,
|
||||
canViewAllConversations: true,
|
||||
canManageUsers: true,
|
||||
canManageAISettings: true,
|
||||
aiReplyPriority: 15
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Логирование админского действия
|
||||
* @param {Object} params - Параметры
|
||||
* @param {number} params.adminId - ID админа
|
||||
* @param {string} params.action - Действие
|
||||
* @param {Object} params.details - Детали
|
||||
*/
|
||||
function logAdminAction(params) {
|
||||
const { adminId, action, details } = params;
|
||||
|
||||
logger.info('[AdminLogic] Админское действие:', {
|
||||
adminId,
|
||||
action,
|
||||
details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли сообщение от админа личным
|
||||
* @param {Object} params - Параметры
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPersonalAdminMessage(params) {
|
||||
const { senderType, userId, recipientId } = params;
|
||||
|
||||
return senderType === 'admin' && userId !== recipientId;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
determineSenderType,
|
||||
shouldGenerateAiReply,
|
||||
canWriteToConversation,
|
||||
getRequestPriority,
|
||||
canPerformAdminAction,
|
||||
getAdminSettings,
|
||||
logAdminAction,
|
||||
isPersonalAdminMessage
|
||||
};
|
||||
|
||||
@@ -10,439 +10,186 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const { ChatOllama } = require('@langchain/ollama');
|
||||
const aiCache = require('./ai-cache');
|
||||
const AIQueue = require('./ai-queue');
|
||||
const logger = require('../utils/logger');
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
|
||||
// Константы для AI параметров
|
||||
const AI_CONFIG = {
|
||||
temperature: 0.3,
|
||||
maxTokens: 512,
|
||||
timeout: 120000, // Уменьшаем до 120 секунд, чтобы соответствовать EmailBot
|
||||
numCtx: 2048,
|
||||
numGpu: 1,
|
||||
numThread: 4,
|
||||
repeatPenalty: 1.1,
|
||||
topK: 40,
|
||||
topP: 0.9,
|
||||
// tfsZ не поддерживается в текущем Ollama — удаляем
|
||||
mirostat: 2,
|
||||
mirostatTau: 5,
|
||||
mirostatEta: 0.1,
|
||||
seed: -1,
|
||||
// Ограничим количество генерируемых токенов для CPU, чтобы избежать таймаутов
|
||||
numPredict: 256,
|
||||
stop: []
|
||||
};
|
||||
|
||||
/**
|
||||
* AI Assistant - тонкая обёртка для работы с Ollama и RAG
|
||||
* Основная логика вынесена в отдельные сервисы:
|
||||
* - ragService.js - генерация ответов через RAG
|
||||
* - aiAssistantSettingsService.js - настройки ИИ
|
||||
* - aiAssistantRulesService.js - правила ИИ
|
||||
* - messageDeduplicationService.js - дедупликация сообщений
|
||||
* - ai-queue.js - управление очередью (отдельный сервис)
|
||||
*/
|
||||
class AIAssistant {
|
||||
constructor() {
|
||||
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
||||
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
|
||||
this.lastHealthCheck = 0;
|
||||
this.healthCheckInterval = 300000; // 5 минут (увеличено с 30 секунд для уменьшения логов)
|
||||
|
||||
// Создаем экземпляр AIQueue
|
||||
this.aiQueue = new AIQueue();
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
// Запускаем обработку очереди
|
||||
this.startQueueProcessing();
|
||||
this.baseUrl = null;
|
||||
this.defaultModel = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// Запуск обработки очереди
|
||||
async startQueueProcessing() {
|
||||
if (this.isProcessingQueue) return;
|
||||
|
||||
this.isProcessingQueue = true;
|
||||
logger.info('[AIAssistant] Запущена обработка очереди AIQueue');
|
||||
|
||||
while (this.isProcessingQueue) {
|
||||
try {
|
||||
// Получаем следующий запрос из очереди
|
||||
const requestItem = this.aiQueue.getNextRequest();
|
||||
|
||||
if (!requestItem) {
|
||||
// Если очередь пуста, ждем немного
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`[AIAssistant] Обрабатываем запрос ${requestItem.id} из очереди`);
|
||||
|
||||
// Обновляем статус на "processing"
|
||||
this.aiQueue.updateRequestStatus(requestItem.id, 'processing');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Обрабатываем запрос
|
||||
const result = await this.processQueueRequest(requestItem.request);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Обновляем статус на "completed"
|
||||
this.aiQueue.updateRequestStatus(requestItem.id, 'completed', result, null, responseTime);
|
||||
|
||||
logger.info(`[AIAssistant] Запрос ${requestItem.id} завершен за ${responseTime}ms`);
|
||||
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Обновляем статус на "failed"
|
||||
this.aiQueue.updateRequestStatus(requestItem.id, 'failed', null, error.message, responseTime);
|
||||
|
||||
logger.error(`[AIAssistant] Запрос ${requestItem.id} завершился с ошибкой:`, error.message);
|
||||
logger.error(`[AIAssistant] Детали ошибки:`, error.stack || error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[AIAssistant] Ошибка в обработке очереди:', error);
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Остановка обработки очереди
|
||||
stopQueueProcessing() {
|
||||
this.isProcessingQueue = false;
|
||||
logger.info('[AIAssistant] Остановлена обработка очереди AIQueue');
|
||||
}
|
||||
|
||||
// Обработка запроса из очереди
|
||||
async processQueueRequest(request) {
|
||||
/**
|
||||
* Инициализация из БД
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
const { message, history, systemPrompt, rules } = request;
|
||||
await ollamaConfig.loadSettingsFromDb();
|
||||
|
||||
// Используем прямой запрос к API, а не getResponse (чтобы избежать цикла)
|
||||
const result = await this.directRequest(
|
||||
[{ role: 'user', content: message }],
|
||||
systemPrompt,
|
||||
{ temperature: 0.3, maxTokens: 150 }
|
||||
);
|
||||
this.baseUrl = ollamaConfig.getBaseUrl();
|
||||
this.defaultModel = ollamaConfig.getDefaultModel();
|
||||
|
||||
return result;
|
||||
if (!this.baseUrl || !this.defaultModel) {
|
||||
throw new Error('Настройки Ollama не найдены в БД');
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info(`[AIAssistant] ✅ Инициализирован из БД: model=${this.defaultModel}`);
|
||||
} catch (error) {
|
||||
logger.error(`[AIAssistant] Ошибка в processQueueRequest:`, error.message);
|
||||
logger.error(`[AIAssistant] Stack trace:`, error.stack);
|
||||
throw error; // Перебрасываем ошибку дальше
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление запроса в очередь
|
||||
async addToQueue(request, priority = 0) {
|
||||
return await this.aiQueue.addRequest(request, priority);
|
||||
}
|
||||
|
||||
// Получение статистики очереди
|
||||
getQueueStats() {
|
||||
return this.aiQueue.getStats();
|
||||
}
|
||||
|
||||
// Получение размера очереди
|
||||
getQueueSize() {
|
||||
return this.aiQueue.getQueueSize();
|
||||
}
|
||||
|
||||
// Проверка здоровья модели
|
||||
async checkModelHealth() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastHealthCheck < this.healthCheckInterval) {
|
||||
return true; // Используем кэшированный результат
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API returned ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const modelExists = data.models?.some(model => model.name === this.defaultModel);
|
||||
|
||||
this.lastHealthCheck = now;
|
||||
return modelExists;
|
||||
} catch (error) {
|
||||
logger.error('Model health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка старого кэша
|
||||
cleanupCache() {
|
||||
const now = Date.now();
|
||||
const maxAge = 3600000; // 1 час
|
||||
aiCache.cleanup(maxAge);
|
||||
}
|
||||
|
||||
// Создание чата с кастомным системным промптом
|
||||
createChat(customSystemPrompt = '') {
|
||||
let systemPrompt = customSystemPrompt;
|
||||
if (!systemPrompt) {
|
||||
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.';
|
||||
}
|
||||
|
||||
return new ChatOllama({
|
||||
baseUrl: this.baseUrl,
|
||||
model: this.defaultModel,
|
||||
system: systemPrompt,
|
||||
...AI_CONFIG,
|
||||
options: AI_CONFIG
|
||||
});
|
||||
}
|
||||
|
||||
// Определение приоритета запроса
|
||||
getRequestPriority(message, history, rules) {
|
||||
let priority = 0;
|
||||
|
||||
// Высокий приоритет для коротких запросов
|
||||
if (message.length < 50) {
|
||||
priority += 10;
|
||||
}
|
||||
|
||||
// Приоритет по типу запроса
|
||||
const urgentKeywords = ['срочно', 'важно', 'помоги'];
|
||||
if (urgentKeywords.some(keyword => message.toLowerCase().includes(keyword))) {
|
||||
priority += 20;
|
||||
}
|
||||
|
||||
// Приоритет для администраторов
|
||||
if (rules && rules.isAdmin) {
|
||||
priority += 15;
|
||||
}
|
||||
|
||||
// Приоритет по времени ожидания (если есть история)
|
||||
if (history && history.length > 0) {
|
||||
const lastMessage = history[history.length - 1];
|
||||
const timeDiff = Date.now() - (lastMessage.timestamp || Date.now());
|
||||
if (timeDiff > 30000) { // Более 30 секунд ожидания
|
||||
priority += 5;
|
||||
}
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
// Основной метод для получения ответа
|
||||
async getResponse(message, history = null, systemPrompt = '', rules = null) {
|
||||
try {
|
||||
// Очищаем старый кэш
|
||||
this.cleanupCache();
|
||||
|
||||
// Проверяем здоровье модели
|
||||
const isHealthy = await this.checkModelHealth();
|
||||
if (!isHealthy) {
|
||||
return 'Извините, модель временно недоступна. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
|
||||
// Проверяем кэш
|
||||
const cacheKey = aiCache.generateKey([{ role: 'user', content: message }], {
|
||||
temperature: 0.3,
|
||||
maxTokens: 150
|
||||
});
|
||||
const cachedResponse = aiCache.get(cacheKey);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Определяем приоритет запроса
|
||||
const priority = this.getRequestPriority(message, history, rules);
|
||||
|
||||
// Добавляем запрос в очередь
|
||||
const requestId = await this.addToQueue({
|
||||
message,
|
||||
history,
|
||||
systemPrompt,
|
||||
rules
|
||||
}, priority);
|
||||
|
||||
// Ждем результат из очереди
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timeout - очередь перегружена'));
|
||||
}, 180000); // 180 секунд таймаут для очереди
|
||||
|
||||
const onCompleted = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
this.aiQueue.off('requestCompleted', onCompleted);
|
||||
this.aiQueue.off('requestFailed', onFailed);
|
||||
try {
|
||||
aiCache.set(cacheKey, item.result);
|
||||
} catch {}
|
||||
resolve(item.result);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
this.aiQueue.off('requestCompleted', onCompleted);
|
||||
this.aiQueue.off('requestFailed', onFailed);
|
||||
reject(new Error(item.error));
|
||||
}
|
||||
};
|
||||
|
||||
this.aiQueue.on('requestCompleted', onCompleted);
|
||||
this.aiQueue.on('requestFailed', onFailed);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error in getResponse:', error);
|
||||
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
}
|
||||
|
||||
// Алиас для getResponse (для совместимости)
|
||||
async processMessage(message, history = null, systemPrompt = '', rules = null) {
|
||||
return this.getResponse(message, history, systemPrompt, rules);
|
||||
}
|
||||
|
||||
// Прямой запрос к API (для очереди)
|
||||
async directRequest(messages, systemPrompt = '', optionsOverride = {}) {
|
||||
try {
|
||||
const model = this.defaultModel;
|
||||
|
||||
logger.info(`[AIAssistant] directRequest: модель=${model}, сообщений=${messages?.length || 0}, systemPrompt="${systemPrompt?.substring(0, 50)}..."`);
|
||||
|
||||
// Создаем AbortController для таймаута
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), AI_CONFIG.timeout);
|
||||
|
||||
// Маппинг camelCase → snake_case для опций Ollama
|
||||
const mapOptionsToOllama = (opts) => ({
|
||||
temperature: opts.temperature,
|
||||
// Используем только num_predict; не мапим maxTokens, чтобы не завышать лимит генерации
|
||||
num_predict: typeof opts.numPredict === 'number' && opts.numPredict > 0 ? opts.numPredict : undefined,
|
||||
num_ctx: opts.numCtx,
|
||||
num_gpu: opts.numGpu,
|
||||
num_thread: opts.numThread,
|
||||
repeat_penalty: opts.repeatPenalty,
|
||||
top_k: opts.topK,
|
||||
top_p: opts.topP,
|
||||
tfs_z: opts.tfsZ,
|
||||
mirostat: opts.mirostat,
|
||||
mirostat_tau: opts.mirostatTau,
|
||||
mirostat_eta: opts.mirostatEta,
|
||||
seed: opts.seed,
|
||||
stop: Array.isArray(opts.stop) ? opts.stop : []
|
||||
});
|
||||
|
||||
const mergedConfig = { ...AI_CONFIG, ...optionsOverride };
|
||||
const ollamaOptions = mapOptionsToOllama(mergedConfig);
|
||||
|
||||
// Вставляем системный промпт в начало, если задан
|
||||
const finalMessages = Array.isArray(messages) ? [...messages] : [];
|
||||
// Нормализация: только 'user' | 'assistant' | 'system'
|
||||
for (const m of finalMessages) {
|
||||
if (m && m.role) {
|
||||
if (m.role !== 'assistant' && m.role !== 'system') m.role = 'user';
|
||||
}
|
||||
}
|
||||
if (systemPrompt && !finalMessages.find(m => m.role === 'system')) {
|
||||
finalMessages.unshift({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
logger.info(`[AIAssistant] Вызываю Ollama API: ${this.baseUrl}/api/chat`);
|
||||
response = await fetch(`${this.baseUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: finalMessages,
|
||||
stream: false,
|
||||
options: ollamaOptions
|
||||
})
|
||||
});
|
||||
logger.info(`[AIAssistant] Ollama API ответил: status=${response.status}`);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Ollama /api/chat возвращает ответ в data.message.content
|
||||
if (data.message && typeof data.message.content === 'string') {
|
||||
const content = data.message.content;
|
||||
try {
|
||||
const cacheKey = aiCache.generateKey(messages, { num_predict: ollamaOptions.num_predict, temperature: ollamaOptions.temperature });
|
||||
aiCache.set(cacheKey, content);
|
||||
} catch {}
|
||||
return content;
|
||||
}
|
||||
// OpenAI-совместимый /v1/chat/completions
|
||||
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
|
||||
const content = data.choices[0].message.content;
|
||||
try {
|
||||
const cacheKey = aiCache.generateKey(messages, { num_predict: ollamaOptions.num_predict, temperature: ollamaOptions.temperature });
|
||||
aiCache.set(cacheKey, content);
|
||||
} catch {}
|
||||
return content;
|
||||
}
|
||||
|
||||
const content = data.response || '';
|
||||
try {
|
||||
const cacheKey = aiCache.generateKey(messages, { num_predict: ollamaOptions.num_predict, temperature: ollamaOptions.temperature });
|
||||
aiCache.set(cacheKey, content);
|
||||
} catch {}
|
||||
return content;
|
||||
} catch (error) {
|
||||
logger.error('Error in directRequest:', error);
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timeout - модель не ответила в течение 120 секунд');
|
||||
}
|
||||
logger.error('[AIAssistant] ❌ КРИТИЧЕСКАЯ ОШИБКА загрузки настроек из БД:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Получение списка доступных моделей
|
||||
async getAvailableModels() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
const data = await response.json();
|
||||
return data.models || [];
|
||||
} catch (error) {
|
||||
logger.error('Error getting available models:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Генерация ответа для всех каналов (web, telegram, email)
|
||||
* Используется ботами (telegramBot, emailBot)
|
||||
*/
|
||||
async generateResponse(options) {
|
||||
const {
|
||||
channel,
|
||||
messageId,
|
||||
userId,
|
||||
userQuestion,
|
||||
conversationHistory = [],
|
||||
conversationId,
|
||||
ragTableId = null,
|
||||
metadata = {}
|
||||
} = options;
|
||||
|
||||
// Проверка здоровья AI сервиса
|
||||
async checkHealth() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API returned ${response.status}`);
|
||||
logger.info(`[AIAssistant] Генерация ответа для канала ${channel}, пользователь ${userId}`);
|
||||
|
||||
const messageDeduplicationService = require('./messageDeduplicationService');
|
||||
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
||||
const aiAssistantRulesService = require('./aiAssistantRulesService');
|
||||
const { ragAnswer } = require('./ragService');
|
||||
|
||||
// 1. Проверяем дедупликацию
|
||||
const cleanMessageId = messageDeduplicationService.cleanMessageId(messageId, channel);
|
||||
const isAlreadyProcessed = await messageDeduplicationService.isMessageAlreadyProcessed(
|
||||
channel,
|
||||
cleanMessageId,
|
||||
userId,
|
||||
'user'
|
||||
);
|
||||
|
||||
if (isAlreadyProcessed) {
|
||||
logger.info(`[AIAssistant] Сообщение ${cleanMessageId} уже обработано - пропускаем`);
|
||||
return { success: false, reason: 'duplicate' };
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// 2. Получаем настройки AI ассистента
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let rules = null;
|
||||
if (aiSettings && aiSettings.rules_id) {
|
||||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||
}
|
||||
|
||||
// 3. Генерируем AI ответ через RAG
|
||||
const aiResponse = await ragAnswer({
|
||||
userQuestion,
|
||||
conversationHistory,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
rules: rules ? rules.rules : null,
|
||||
ragTableId
|
||||
});
|
||||
|
||||
if (!aiResponse) {
|
||||
logger.warn(`[AIAssistant] Пустой ответ от AI для пользователя ${userId}`);
|
||||
return { success: false, reason: 'empty_response' };
|
||||
}
|
||||
|
||||
// 4. Сохраняем ответ с дедупликацией
|
||||
const aiResponseId = `ai_response_${cleanMessageId}_${Date.now()}`;
|
||||
const saveResult = await messageDeduplicationService.saveMessageWithDeduplication(
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversationId,
|
||||
sender_type: 'assistant',
|
||||
content: aiResponse,
|
||||
channel: channel,
|
||||
role: 'assistant',
|
||||
direction: 'out',
|
||||
created_at: new Date(),
|
||||
...metadata
|
||||
},
|
||||
channel,
|
||||
aiResponseId,
|
||||
userId,
|
||||
'assistant',
|
||||
'messages'
|
||||
);
|
||||
|
||||
if (!saveResult.success) {
|
||||
logger.error(`[AIAssistant] Ошибка сохранения AI ответа:`, saveResult.error);
|
||||
return { success: false, reason: 'save_error' };
|
||||
}
|
||||
|
||||
logger.info(`[AIAssistant] AI ответ успешно сгенерирован и сохранен для пользователя ${userId}`);
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
models: data.models?.length || 0,
|
||||
baseUrl: this.baseUrl
|
||||
success: true,
|
||||
response: aiResponse,
|
||||
messageId: aiResponseId,
|
||||
conversationId: conversationId
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('AI health check failed:', error);
|
||||
return {
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
baseUrl: this.baseUrl
|
||||
};
|
||||
logger.error(`[AIAssistant] Ошибка генерации ответа:`, error);
|
||||
return { success: false, reason: 'error', error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем методы из vectorStore.js
|
||||
async initVectorStore() {
|
||||
// ... код инициализации ...
|
||||
/**
|
||||
* Простая генерация ответа (для гостевых сообщений)
|
||||
* Используется в guestMessageService
|
||||
*/
|
||||
async getResponse(message, history = null, systemPrompt = '', rules = null) {
|
||||
try {
|
||||
const { ragAnswer } = require('./ragService');
|
||||
|
||||
const result = await ragAnswer({
|
||||
userQuestion: message,
|
||||
conversationHistory: history || [],
|
||||
systemPrompt: systemPrompt || '',
|
||||
rules: rules || null,
|
||||
ragTableId: null
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[AIAssistant] Ошибка в getResponse:', error);
|
||||
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
}
|
||||
|
||||
async findSimilarDocuments(query, k = 3) {
|
||||
// ... код поиска документов ...
|
||||
/**
|
||||
* Проверка здоровья AI сервиса
|
||||
* Использует централизованный метод из ollamaConfig
|
||||
*/
|
||||
async checkHealth() {
|
||||
if (!this.isInitialized) {
|
||||
return { status: 'error', error: 'AI Assistant не инициализирован' };
|
||||
}
|
||||
|
||||
// Используем метод проверки из ollamaConfig
|
||||
return await ollamaConfig.checkHealth();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AIAssistant();
|
||||
const aiAssistantInstance = new AIAssistant();
|
||||
const initPromise = aiAssistantInstance.initialize();
|
||||
|
||||
module.exports = aiAssistantInstance;
|
||||
module.exports.initPromise = initPromise;
|
||||
|
||||
@@ -87,6 +87,7 @@ class AICache {
|
||||
|
||||
calculateHitRate() {
|
||||
// Простая реализация - в реальности нужно отслеживать hits/misses
|
||||
if (this.maxSize === 0) return 0;
|
||||
return this.cache.size / this.maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,9 @@ async function getSettings() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_chain.pem');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8');
|
||||
}
|
||||
} catch (keyError) {
|
||||
logger.warn('[aiAssistantSettingsService] Could not read encryption key:', keyError.message);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Обрабатываем selected_rag_tables
|
||||
if (setting.selected_rag_tables) {
|
||||
|
||||
@@ -145,7 +145,8 @@ async function getAllLLMModels() {
|
||||
// Для Ollama проверяем реально установленные модели через HTTP API
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
@@ -214,7 +215,8 @@ async function getAllEmbeddingModels() {
|
||||
// Для Ollama проверяем реально установленные embedding модели через HTTP API
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
|
||||
@@ -262,8 +262,8 @@ class AuthService {
|
||||
async processAndCleanupGuestData(userId, guestId, session) {
|
||||
try {
|
||||
// Обрабатываем гостевые сообщения
|
||||
const { processGuestMessages } = require('../routes/chat');
|
||||
await processGuestMessages(userId, guestId);
|
||||
const guestMessageService = require('./guestMessageService');
|
||||
await guestMessageService.processGuestMessages(userId, guestId);
|
||||
|
||||
// Очищаем гостевой ID из сессии
|
||||
delete session.guestId;
|
||||
@@ -432,19 +432,9 @@ class AuthService {
|
||||
|
||||
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
|
||||
if (session.guestId && isNewUser) {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
await db.getQuery()(
|
||||
'INSERT INTO guest_user_mapping (user_id, guest_id_encrypted) VALUES ($1, encrypt_text($2, $3)) ON CONFLICT (guest_id_encrypted) DO UPDATE SET user_id = $1',
|
||||
@@ -749,19 +739,9 @@ class AuthService {
|
||||
logger.info('Starting recheck of admin status for all users with wallets');
|
||||
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем всех пользователей с кошельками
|
||||
const usersResult = await db.getQuery()(
|
||||
|
||||
211
backend/services/botManager.js
Normal file
211
backend/services/botManager.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const TelegramBot = require('./telegramBot');
|
||||
const EmailBot = require('./emailBot');
|
||||
const unifiedMessageProcessor = require('./unifiedMessageProcessor');
|
||||
|
||||
/**
|
||||
* BotManager - централизованный менеджер всех ботов
|
||||
* Управляет жизненным циклом ботов (инициализация, обработка сообщений, остановка)
|
||||
*/
|
||||
class BotManager {
|
||||
constructor() {
|
||||
this.bots = new Map();
|
||||
this.isInitialized = false;
|
||||
this.processingQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация всех ботов
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
logger.info('[BotManager] 🚀 Инициализация BotManager...');
|
||||
|
||||
// Создаем экземпляры ботов
|
||||
const webBot = {
|
||||
name: 'WebBot',
|
||||
channel: 'web',
|
||||
isInitialized: true,
|
||||
status: 'active',
|
||||
initialize: async () => ({ success: true }),
|
||||
processMessage: async (messageData) => {
|
||||
return await unifiedMessageProcessor.processMessage(messageData);
|
||||
}
|
||||
};
|
||||
|
||||
const telegramBot = new TelegramBot();
|
||||
const emailBot = new EmailBot();
|
||||
|
||||
// Регистрируем ботов
|
||||
this.bots.set('web', webBot);
|
||||
this.bots.set('telegram', telegramBot);
|
||||
this.bots.set('email', emailBot);
|
||||
|
||||
// Инициализируем Telegram Bot
|
||||
logger.info('[BotManager] Инициализация Telegram Bot...');
|
||||
await telegramBot.initialize().catch(error => {
|
||||
logger.warn('[BotManager] Telegram Bot не инициализирован:', error.message);
|
||||
});
|
||||
|
||||
// Инициализируем Email Bot
|
||||
logger.info('[BotManager] Инициализация Email Bot...');
|
||||
await emailBot.initialize().catch(error => {
|
||||
logger.warn('[BotManager] Email Bot не инициализирован:', error.message);
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info('[BotManager] ✅ BotManager успешно инициализирован');
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('[BotManager] ❌ Ошибка инициализации BotManager:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить бота по имени
|
||||
* @param {string} botName - Имя бота (web, telegram, email)
|
||||
* @returns {Object|null} Экземпляр бота или null
|
||||
*/
|
||||
getBot(botName) {
|
||||
return this.bots.get(botName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить готовность BotManager
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReady() {
|
||||
return this.isInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статус всех ботов
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStatus() {
|
||||
const status = {};
|
||||
|
||||
for (const [name, bot] of this.bots) {
|
||||
status[name] = {
|
||||
initialized: bot.isInitialized || false,
|
||||
status: bot.status || 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработать сообщение через соответствующий бот
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async processMessage(messageData) {
|
||||
try {
|
||||
const channel = messageData.channel || 'web';
|
||||
const bot = this.bots.get(channel);
|
||||
|
||||
if (!bot) {
|
||||
throw new Error(`Bot for channel "${channel}" not found`);
|
||||
}
|
||||
|
||||
if (!bot.isInitialized) {
|
||||
throw new Error(`Bot "${channel}" is not initialized`);
|
||||
}
|
||||
|
||||
// Обрабатываем сообщение через unified processor
|
||||
return await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[BotManager] Ошибка обработки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перезапустить конкретный бот
|
||||
* @param {string} botName - Имя бота
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async restartBot(botName) {
|
||||
try {
|
||||
logger.info(`[BotManager] Перезапуск бота: ${botName}`);
|
||||
|
||||
const bot = this.bots.get(botName);
|
||||
|
||||
if (!bot) {
|
||||
throw new Error(`Bot "${botName}" not found`);
|
||||
}
|
||||
|
||||
// Останавливаем бота (если есть метод stop)
|
||||
if (typeof bot.stop === 'function') {
|
||||
await bot.stop();
|
||||
}
|
||||
|
||||
// Переинициализируем
|
||||
if (typeof bot.initialize === 'function') {
|
||||
await bot.initialize();
|
||||
}
|
||||
|
||||
logger.info(`[BotManager] ✅ Бот ${botName} перезапущен`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
bot: botName,
|
||||
status: bot.status
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[BotManager] Ошибка перезапуска бота ${botName}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить все боты
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
logger.info('[BotManager] Остановка всех ботов...');
|
||||
|
||||
for (const [name, bot] of this.bots) {
|
||||
if (typeof bot.stop === 'function') {
|
||||
logger.info(`[BotManager] Остановка ${name}...`);
|
||||
await bot.stop().catch(error => {
|
||||
logger.error(`[BotManager] Ошибка остановки ${name}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isInitialized = false;
|
||||
logger.info('[BotManager] ✅ Все боты остановлены');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[BotManager] Ошибка остановки ботов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const botManager = new BotManager();
|
||||
|
||||
module.exports = botManager;
|
||||
|
||||
114
backend/services/botsSettings.js
Normal file
114
backend/services/botsSettings.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Сервис для работы с настройками ботов
|
||||
*/
|
||||
|
||||
/**
|
||||
* Получить настройки конкретного бота
|
||||
* @param {string} botType - Тип бота (telegram, email)
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function getBotSettings(botType) {
|
||||
try {
|
||||
let tableName;
|
||||
|
||||
switch (botType) {
|
||||
case 'telegram':
|
||||
tableName = 'telegram_settings';
|
||||
break;
|
||||
case 'email':
|
||||
tableName = 'email_settings';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown bot type: ${botType}`);
|
||||
}
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT * FROM ${tableName} ORDER BY id LIMIT 1`
|
||||
);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[BotsSettings] Ошибка получения настроек ${botType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить настройки бота
|
||||
* @param {string} botType - Тип бота
|
||||
* @param {Object} settings - Настройки
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function saveBotSettings(botType, settings) {
|
||||
try {
|
||||
let tableName;
|
||||
|
||||
switch (botType) {
|
||||
case 'telegram':
|
||||
tableName = 'telegram_settings';
|
||||
break;
|
||||
case 'email':
|
||||
tableName = 'email_settings';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown bot type: ${botType}`);
|
||||
}
|
||||
|
||||
// Простое сохранение - детали зависят от структуры таблицы
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO ${tableName} (settings, updated_at)
|
||||
VALUES ($1, NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET settings = $1, updated_at = NOW()
|
||||
RETURNING *`,
|
||||
[JSON.stringify(settings)]
|
||||
);
|
||||
|
||||
return rows[0];
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[BotsSettings] Ошибка сохранения настроек ${botType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить настройки всех ботов
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getAllBotsSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
telegram: await getBotSettings('telegram').catch(() => null),
|
||||
email: await getBotSettings('email').catch(() => null)
|
||||
};
|
||||
|
||||
return settings;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[BotsSettings] Ошибка получения всех настроек:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBotSettings,
|
||||
saveBotSettings,
|
||||
getAllBotsSettings
|
||||
};
|
||||
|
||||
189
backend/services/conversationService.js
Normal file
189
backend/services/conversationService.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
|
||||
/**
|
||||
* Сервис для работы с беседами (conversations)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Получить или создать беседу для пользователя
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} title - Заголовок беседы
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getOrCreateConversation(userId, title = 'Новая беседа') {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Ищем существующую активную беседу
|
||||
const { rows: existing } = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return existing[0];
|
||||
}
|
||||
|
||||
// Создаем новую беседу
|
||||
const { rows: newConv } = await db.getQuery()(
|
||||
`INSERT INTO conversations (user_id, title_encrypted)
|
||||
VALUES ($1, encrypt_text($2, $3))
|
||||
RETURNING id, user_id, decrypt_text(title_encrypted, $3) as title, created_at, updated_at`,
|
||||
[userId, title, encryptionKey]
|
||||
);
|
||||
|
||||
logger.info('[ConversationService] Создана новая беседа:', newConv[0].id);
|
||||
return newConv[0];
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка получения/создания беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить беседу по ID
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function getConversationById(conversationId) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE id = $1`,
|
||||
[conversationId, encryptionKey]
|
||||
);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка получения беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все беседы пользователя
|
||||
* @param {number} userId - ID пользователя
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function getUserConversations(userId) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
|
||||
return rows;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка получения бесед пользователя:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить время последнего обновления беседы
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function touchConversation(conversationId) {
|
||||
try {
|
||||
await db.getQuery()(
|
||||
`UPDATE conversations SET updated_at = NOW() WHERE id = $1`,
|
||||
[conversationId]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка обновления беседы:', error);
|
||||
// Не бросаем ошибку, это некритично
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить беседу
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @param {number} userId - ID пользователя (для проверки прав)
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function deleteConversation(conversationId, userId) {
|
||||
try {
|
||||
const { rowCount } = await db.getQuery()(
|
||||
`DELETE FROM conversations WHERE id = $1 AND user_id = $2`,
|
||||
[conversationId, userId]
|
||||
);
|
||||
|
||||
if (rowCount > 0) {
|
||||
logger.info('[ConversationService] Удалена беседа:', conversationId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка удаления беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить заголовок беседы
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} newTitle - Новый заголовок
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function updateConversationTitle(conversationId, userId, newTitle) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`UPDATE conversations
|
||||
SET title_encrypted = encrypt_text($3, $4), updated_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING id, user_id, decrypt_text(title_encrypted, $4) as title, created_at, updated_at`,
|
||||
[conversationId, userId, newTitle, encryptionKey]
|
||||
);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка обновления заголовка беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOrCreateConversation,
|
||||
getConversationById,
|
||||
getUserConversations,
|
||||
touchConversation,
|
||||
deleteConversation,
|
||||
updateConversationTitle
|
||||
};
|
||||
|
||||
@@ -13,15 +13,78 @@
|
||||
const { pool } = require('../db');
|
||||
const verificationService = require('./verification-service');
|
||||
const logger = require('../utils/logger');
|
||||
const EmailBotService = require('./emailBot.js');
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const authService = require('./auth-service');
|
||||
const { checkAdminRole } = require('./admin-role');
|
||||
const { broadcastContactsUpdate } = require('../wsHub');
|
||||
const nodemailer = require('nodemailer');
|
||||
const db = require('../db');
|
||||
|
||||
class EmailAuth {
|
||||
constructor() {
|
||||
this.emailBot = new EmailBotService();
|
||||
// Убрали зависимость от старого EmailBot
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка кода верификации на email
|
||||
* Создает временный transporter для отправки
|
||||
*/
|
||||
async sendVerificationCode(email, code) {
|
||||
try {
|
||||
// Получаем настройки email из БД
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
'SELECT decrypt_text(smtp_host_encrypted, $1) as smtp_host, ' +
|
||||
'decrypt_text(smtp_user_encrypted, $1) as smtp_user, ' +
|
||||
'decrypt_text(smtp_password_encrypted, $1) as smtp_password, ' +
|
||||
'decrypt_text(from_email_encrypted, $1) as from_email ' +
|
||||
'FROM email_settings ORDER BY id LIMIT 1',
|
||||
[encryptionKey]
|
||||
);
|
||||
|
||||
if (!rows.length) {
|
||||
throw new Error('Email settings not found');
|
||||
}
|
||||
|
||||
const settings = rows[0];
|
||||
|
||||
// Создаем временный transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: settings.smtp_host,
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: settings.smtp_user,
|
||||
pass: settings.smtp_password,
|
||||
},
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
// Отправляем письмо
|
||||
await transporter.sendMail({
|
||||
from: settings.from_email,
|
||||
to: email,
|
||||
subject: 'Код подтверждения',
|
||||
text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`,
|
||||
html: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #333;">Код подтверждения</h2>
|
||||
<p style="font-size: 16px; color: #666;">Ваш код подтверждения:</p>
|
||||
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; margin: 20px 0;">
|
||||
<span style="font-size: 24px; font-weight: bold; color: #333;">${code}</span>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p>
|
||||
</div>`
|
||||
});
|
||||
|
||||
transporter.close();
|
||||
logger.info('[EmailAuth] Verification code sent successfully');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[EmailAuth] Error sending verification code:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async initEmailAuth(session, email) {
|
||||
@@ -70,7 +133,7 @@ class EmailAuth {
|
||||
);
|
||||
|
||||
// Отправляем код на email
|
||||
await this.emailBot.sendVerificationCode(email, verificationCode);
|
||||
await this.sendVerificationCode(email, verificationCode);
|
||||
|
||||
logger.info(
|
||||
`Generated verification code for Email auth for ${email} and sent to user's email`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,46 +11,21 @@
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
|
||||
class EncryptedDataService {
|
||||
constructor() {
|
||||
this.encryptionKey = this.loadEncryptionKey();
|
||||
this.isEncryptionEnabled = !!this.encryptionKey;
|
||||
this.encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
this.isEncryptionEnabled = encryptionUtils.isEnabled();
|
||||
|
||||
if (this.isEncryptionEnabled) {
|
||||
// console.log('🔐 Шифрование базы данных активировано');
|
||||
// console.log('📋 Автоматическое определение зашифрованных колонок');
|
||||
console.log('🔐 [EncryptedDB] Шифрование базы данных активировано');
|
||||
console.log('📋 [EncryptedDB] Автоматическое определение зашифрованных колонок');
|
||||
} else {
|
||||
// console.log('⚠️ Шифрование базы данных отключено - ключ не найден');
|
||||
console.log('⚠️ [EncryptedDB] Шифрование базы данных отключено - ключ не найден');
|
||||
}
|
||||
}
|
||||
|
||||
loadEncryptionKey() {
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
|
||||
// console.log(`[EncryptedDB] Trying key path: ${keyPath}`);
|
||||
if (fs.existsSync(keyPath)) {
|
||||
const key = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
// console.log(`[EncryptedDB] Key loaded from: ${keyPath}, length: ${key.length}`);
|
||||
return key;
|
||||
}
|
||||
// Попробуем альтернативный путь относительно корня приложения
|
||||
const altKeyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
// console.log(`[EncryptedDB] Trying alternative key path: ${altKeyPath}`);
|
||||
if (fs.existsSync(altKeyPath)) {
|
||||
const key = fs.readFileSync(altKeyPath, 'utf8').trim();
|
||||
// console.log(`[EncryptedDB] Key loaded from: ${altKeyPath}, length: ${key.length}`);
|
||||
return key;
|
||||
}
|
||||
// console.log(`[EncryptedDB] No key file found, using default key`);
|
||||
return 'default-key';
|
||||
} catch (error) {
|
||||
// console.error('❌ Ошибка загрузки ключа шифрования:', error);
|
||||
return 'default-key';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить данные из таблицы с автоматической расшифровкой
|
||||
|
||||
159
backend/services/guestMessageService.js
Normal file
159
backend/services/guestMessageService.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const guestService = require('./guestService');
|
||||
|
||||
/**
|
||||
* Сервис для переноса гостевых сообщений в зарегистрированный аккаунт
|
||||
* Используется при регистрации/входе пользователя, который был гостем
|
||||
*/
|
||||
|
||||
/**
|
||||
* Перенести гостевые сообщения в аккаунт пользователя
|
||||
* @param {string} guestId - ID гостя
|
||||
* @param {number} userId - ID зарегистрированного пользователя
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function migrateGuestMessages(guestId, userId) {
|
||||
try {
|
||||
logger.info(`[GuestMessageService] Перенос сообщений с ${guestId} на user ${userId}`);
|
||||
|
||||
// Получаем гостевые сообщения
|
||||
const guestMessages = await guestService.getGuestMessages(guestId);
|
||||
|
||||
if (guestMessages.length === 0) {
|
||||
logger.info('[GuestMessageService] Нет сообщений для переноса');
|
||||
return { migrated: 0, skipped: 0 };
|
||||
}
|
||||
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
let migrated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
// Переносим каждое сообщение
|
||||
for (const msg of guestMessages) {
|
||||
try {
|
||||
// Вставляем в таблицу messages
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
user_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1,
|
||||
encrypt_text($2, $7),
|
||||
encrypt_text($3, $7),
|
||||
encrypt_text($4, $7),
|
||||
encrypt_text($5, $7),
|
||||
encrypt_text($6, $7),
|
||||
$8
|
||||
)`,
|
||||
[
|
||||
userId,
|
||||
'user',
|
||||
msg.content,
|
||||
msg.channel || 'web',
|
||||
'user',
|
||||
'incoming',
|
||||
encryptionKey,
|
||||
msg.created_at
|
||||
]
|
||||
);
|
||||
|
||||
migrated++;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка переноса сообщения:', error);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем гостевые сообщения после успешного переноса
|
||||
if (migrated > 0) {
|
||||
await guestService.deleteGuestMessages(guestId);
|
||||
}
|
||||
|
||||
logger.info(`[GuestMessageService] Перенесено: ${migrated}, пропущено: ${skipped}`);
|
||||
|
||||
return { migrated, skipped, total: guestMessages.length };
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка миграции сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, есть ли гостевые сообщения для переноса
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function hasGuestMessages(guestId) {
|
||||
try {
|
||||
const messages = await guestService.getGuestMessages(guestId);
|
||||
return messages.length > 0;
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка проверки гостевых сообщений:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить количество гостевых сообщений
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function getGuestMessageCount(guestId) {
|
||||
try {
|
||||
const messages = await guestService.getGuestMessages(guestId);
|
||||
return messages.length;
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка подсчета гостевых сообщений:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить старые гостевые сообщения (старше N дней)
|
||||
* @param {number} daysOld - Возраст в днях
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function cleanupOldGuestMessages(daysOld = 30) {
|
||||
try {
|
||||
const { rowCount } = await db.getQuery()(
|
||||
`DELETE FROM guest_messages
|
||||
WHERE created_at < NOW() - INTERVAL '${daysOld} days'`
|
||||
);
|
||||
|
||||
logger.info(`[GuestMessageService] Очищено ${rowCount} старых гостевых сообщений`);
|
||||
return rowCount;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка очистки старых сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
migrateGuestMessages,
|
||||
hasGuestMessages,
|
||||
getGuestMessageCount,
|
||||
cleanupOldGuestMessages
|
||||
};
|
||||
|
||||
160
backend/services/guestService.js
Normal file
160
backend/services/guestService.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Сервис для работы с гостевыми сообщениями
|
||||
* Обрабатывает сообщения от незарегистрированных пользователей
|
||||
*/
|
||||
|
||||
/**
|
||||
* Создать гостевой идентификатор
|
||||
* @returns {string}
|
||||
*/
|
||||
function createGuestId() {
|
||||
return `guest_${crypto.randomBytes(16).toString('hex')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить гостевое сообщение
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function saveGuestMessage(messageData) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
const guestId = messageData.guestId || createGuestId();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO guest_messages (
|
||||
guest_id,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1,
|
||||
encrypt_text($2, $3),
|
||||
encrypt_text($4, $3),
|
||||
NOW()
|
||||
) RETURNING id, guest_id, created_at`,
|
||||
[guestId, messageData.content, encryptionKey, messageData.channel || 'web']
|
||||
);
|
||||
|
||||
logger.info('[GuestService] Сохранено гостевое сообщение:', rows[0].id);
|
||||
|
||||
return {
|
||||
...rows[0],
|
||||
content: messageData.content,
|
||||
channel: messageData.channel || 'web'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка сохранения гостевого сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить гостевые сообщения по guest_id
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function getGuestMessages(guestId) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
id,
|
||||
guest_id,
|
||||
decrypt_text(content_encrypted, $2) as content,
|
||||
decrypt_text(channel_encrypted, $2) as channel,
|
||||
created_at
|
||||
FROM guest_messages
|
||||
WHERE guest_id = $1
|
||||
ORDER BY created_at ASC`,
|
||||
[guestId, encryptionKey]
|
||||
);
|
||||
|
||||
return rows;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка получения гостевых сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить гостевые сообщения
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function deleteGuestMessages(guestId) {
|
||||
try {
|
||||
const { rowCount } = await db.getQuery()(
|
||||
`DELETE FROM guest_messages WHERE guest_id = $1`,
|
||||
[guestId]
|
||||
);
|
||||
|
||||
logger.info(`[GuestService] Удалено ${rowCount} гостевых сообщений для ${guestId}`);
|
||||
return rowCount;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка удаления гостевых сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли пользователь гостем
|
||||
* @param {string} identifier - Идентификатор
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isGuest(identifier) {
|
||||
return typeof identifier === 'string' && identifier.startsWith('guest_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику гостевых сообщений
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getGuestStats() {
|
||||
try {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
COUNT(DISTINCT guest_id) as unique_guests,
|
||||
COUNT(*) as total_messages,
|
||||
MAX(created_at) as last_message_at
|
||||
FROM guest_messages`
|
||||
);
|
||||
|
||||
return rows[0];
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка получения статистики:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createGuestId,
|
||||
saveGuestMessage,
|
||||
getGuestMessages,
|
||||
deleteGuestMessages,
|
||||
isGuest,
|
||||
getGuestStats
|
||||
};
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const { initTelegramBot } = require('./telegram-service');
|
||||
const emailBot = require('./emailBot');
|
||||
const telegramBot = require('./telegramBot');
|
||||
const botManager = require('./botManager');
|
||||
const botsSettings = require('./botsSettings');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const {
|
||||
initializeVectorStore,
|
||||
@@ -20,16 +19,11 @@ const {
|
||||
similaritySearch,
|
||||
addDocument,
|
||||
} = require('./vectorStore');
|
||||
// ... другие импорты
|
||||
|
||||
module.exports = {
|
||||
// Telegram
|
||||
initTelegramBot,
|
||||
|
||||
// Email
|
||||
emailBot,
|
||||
sendEmail: emailBot.sendEmail,
|
||||
checkEmails: emailBot.checkEmails,
|
||||
// Bot Manager (новая архитектура)
|
||||
botManager,
|
||||
botsSettings,
|
||||
|
||||
// Vector Store
|
||||
initializeVectorStore,
|
||||
@@ -38,12 +32,10 @@ module.exports = {
|
||||
addDocument,
|
||||
|
||||
// AI Assistant
|
||||
aiAssistant,
|
||||
processMessage: aiAssistant.processMessage,
|
||||
getUserInfo: aiAssistant.getUserInfo,
|
||||
getConversationHistory: aiAssistant.getConversationHistory,
|
||||
|
||||
telegramBot,
|
||||
aiAssistant,
|
||||
|
||||
interfaceService: require('./interfaceService'),
|
||||
};
|
||||
|
||||
138
backend/services/messageDeduplicationService.js
Normal file
138
backend/services/messageDeduplicationService.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Сервис дедупликации сообщений
|
||||
* Предотвращает обработку дублирующихся сообщений
|
||||
*/
|
||||
|
||||
// Хранилище хешей обработанных сообщений (в памяти)
|
||||
const processedMessages = new Map();
|
||||
|
||||
// Время жизни записи о сообщении (5 минут)
|
||||
const MESSAGE_TTL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Создать хеш сообщения
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {string} Хеш сообщения
|
||||
*/
|
||||
function createMessageHash(messageData) {
|
||||
const hashData = {
|
||||
userId: messageData.userId || messageData.user_id,
|
||||
content: messageData.content,
|
||||
channel: messageData.channel,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Округляем до секунд
|
||||
};
|
||||
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(JSON.stringify(hashData))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, было ли сообщение уже обработано
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {boolean} true если сообщение уже обрабатывалось
|
||||
*/
|
||||
function isDuplicate(messageData) {
|
||||
const hash = createMessageHash(messageData);
|
||||
|
||||
if (processedMessages.has(hash)) {
|
||||
const entry = processedMessages.get(hash);
|
||||
const now = Date.now();
|
||||
|
||||
// Проверяем, не истек ли TTL
|
||||
if (now - entry.timestamp < MESSAGE_TTL) {
|
||||
logger.warn('[MessageDeduplication] Обнаружено дублирующееся сообщение:', hash);
|
||||
return true;
|
||||
} else {
|
||||
// TTL истек, удаляем запись
|
||||
processedMessages.delete(hash);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Пометить сообщение как обработанное
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
*/
|
||||
function markAsProcessed(messageData) {
|
||||
const hash = createMessageHash(messageData);
|
||||
|
||||
processedMessages.set(hash, {
|
||||
timestamp: Date.now(),
|
||||
messageData: {
|
||||
userId: messageData.userId || messageData.user_id,
|
||||
channel: messageData.channel
|
||||
}
|
||||
});
|
||||
|
||||
// Очищаем старые записи
|
||||
cleanupOldEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить старые записи из хранилища
|
||||
*/
|
||||
function cleanupOldEntries() {
|
||||
const now = Date.now();
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [hash, entry] of processedMessages.entries()) {
|
||||
if (now - entry.timestamp > MESSAGE_TTL) {
|
||||
processedMessages.delete(hash);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
logger.debug(`[MessageDeduplication] Очищено ${cleanedCount} старых записей`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику дедупликации
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getStats() {
|
||||
return {
|
||||
totalTracked: processedMessages.size,
|
||||
ttl: MESSAGE_TTL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить все записи (для тестов)
|
||||
*/
|
||||
function clear() {
|
||||
processedMessages.clear();
|
||||
logger.info('[MessageDeduplication] Хранилище очищено');
|
||||
}
|
||||
|
||||
// Периодическая очистка старых записей (каждую минуту)
|
||||
setInterval(cleanupOldEntries, 60 * 1000);
|
||||
|
||||
module.exports = {
|
||||
isDuplicate,
|
||||
markAsProcessed,
|
||||
getStats,
|
||||
clear,
|
||||
createMessageHash
|
||||
};
|
||||
|
||||
166
backend/services/notifyOllamaReady.js
Normal file
166
backend/services/notifyOllamaReady.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Скрипт для уведомления Ollama о готовности
|
||||
* Используется для проверки доступности Ollama и прогрева моделей
|
||||
*/
|
||||
|
||||
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://ollama:11434';
|
||||
const MAX_RETRIES = 30;
|
||||
const RETRY_DELAY = 2000; // 2 секунды
|
||||
|
||||
/**
|
||||
* Проверить доступность Ollama
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function checkOllamaHealth() {
|
||||
try {
|
||||
const response = await axios.get(`${OLLAMA_HOST}/api/tags`, {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Дождаться готовности Ollama с retry
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function waitForOllama() {
|
||||
logger.info('[NotifyOllamaReady] Ожидание готовности Ollama...');
|
||||
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
const isReady = await checkOllamaHealth();
|
||||
|
||||
if (isReady) {
|
||||
logger.info(`[NotifyOllamaReady] ✅ Ollama готов! (попытка ${i + 1}/${MAX_RETRIES})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info(`[NotifyOllamaReady] Ollama не готов, повтор ${i + 1}/${MAX_RETRIES}...`);
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||
}
|
||||
|
||||
logger.error('[NotifyOllamaReady] ❌ Ollama не стал доступен после всех попыток');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список доступных моделей
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function getAvailableModels() {
|
||||
try {
|
||||
const response = await axios.get(`${OLLAMA_HOST}/api/tags`, {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
return response.data.models || [];
|
||||
} catch (error) {
|
||||
logger.error('[NotifyOllamaReady] Ошибка получения моделей:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Прогреть модель (загрузить в память)
|
||||
* @param {string} modelName - Название модели
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function warmupModel(modelName) {
|
||||
try {
|
||||
logger.info(`[NotifyOllamaReady] Прогрев модели: ${modelName}`);
|
||||
|
||||
const response = await axios.post(`${OLLAMA_HOST}/api/generate`, {
|
||||
model: modelName,
|
||||
prompt: 'Hello',
|
||||
stream: false
|
||||
}, {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
logger.info(`[NotifyOllamaReady] ✅ Модель ${modelName} прогрета`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`[NotifyOllamaReady] Ошибка прогрева модели ${modelName}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Основная функция инициализации
|
||||
*/
|
||||
async function initialize() {
|
||||
try {
|
||||
logger.info('[NotifyOllamaReady] 🚀 Начало инициализации Ollama...');
|
||||
|
||||
// Ждем готовности Ollama
|
||||
const isReady = await waitForOllama();
|
||||
|
||||
if (!isReady) {
|
||||
logger.error('[NotifyOllamaReady] Не удалось дождаться готовности Ollama');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем список моделей
|
||||
const models = await getAvailableModels();
|
||||
logger.info(`[NotifyOllamaReady] Найдено моделей: ${models.length}`);
|
||||
|
||||
if (models.length > 0) {
|
||||
logger.info('[NotifyOllamaReady] Доступные модели:', models.map(m => m.name).join(', '));
|
||||
|
||||
// Прогреваем первую модель (опционально)
|
||||
if (process.env.WARMUP_MODEL === 'true' && models[0]) {
|
||||
await warmupModel(models[0].name);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[NotifyOllamaReady] ✅ Инициализация завершена');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[NotifyOllamaReady] Ошибка инициализации:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запущен напрямую как скрипт
|
||||
if (require.main === module) {
|
||||
initialize()
|
||||
.then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('[NotifyOllamaReady] Критическая ошибка:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
waitForOllama,
|
||||
checkOllamaHealth,
|
||||
getAvailableModels,
|
||||
warmupModel
|
||||
};
|
||||
|
||||
251
backend/services/ollamaConfig.js
Normal file
251
backend/services/ollamaConfig.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
/**
|
||||
* Конфигурационный сервис для Ollama
|
||||
* Централизует все настройки и URL для Ollama API
|
||||
*
|
||||
* ВАЖНО: Настройки берутся из таблицы ai_providers_settings (через aiProviderSettingsService)
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Кэш для настроек из БД
|
||||
let settingsCache = null;
|
||||
|
||||
/**
|
||||
* Загружает настройки Ollama из базы данных
|
||||
* @returns {Promise<Object>} Настройки Ollama провайдера
|
||||
*/
|
||||
async function loadSettingsFromDb() {
|
||||
try {
|
||||
const aiProviderSettingsService = require('./aiProviderSettingsService');
|
||||
const settings = await aiProviderSettingsService.getProviderSettings('ollama');
|
||||
|
||||
if (settings) {
|
||||
settingsCache = settings;
|
||||
logger.info(`[ollamaConfig] Loaded settings from DB: model=${settings.selected_model}, base_url=${settings.base_url}`);
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error('[ollamaConfig] Ошибка загрузки настроек Ollama из БД:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает базовый URL для Ollama (синхронная версия)
|
||||
* @returns {string} Базовый URL Ollama
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
// Приоритет: кэш из БД > Docker дефолт
|
||||
if (settingsCache && settingsCache.base_url) {
|
||||
return settingsCache.base_url;
|
||||
}
|
||||
// URL по умолчанию для Docker
|
||||
return 'http://ollama:11434';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает базовый URL для Ollama (асинхронная версия)
|
||||
* @returns {Promise<string>} Базовый URL Ollama
|
||||
*/
|
||||
async function getBaseUrlAsync() {
|
||||
try {
|
||||
if (!settingsCache) {
|
||||
await loadSettingsFromDb();
|
||||
}
|
||||
|
||||
if (settingsCache && settingsCache.base_url) {
|
||||
return settingsCache.base_url;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[ollamaConfig] Failed to load base_url from DB, using default');
|
||||
}
|
||||
|
||||
return 'http://ollama:11434';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает URL для конкретного API endpoint Ollama
|
||||
* @param {string} endpoint - Endpoint API (например: 'tags', 'generate')
|
||||
* @returns {string} Полный URL для API endpoint
|
||||
*/
|
||||
function getApiUrl(endpoint) {
|
||||
const baseUrl = getBaseUrl();
|
||||
return `${baseUrl}/api/${endpoint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает модель по умолчанию для Ollama (синхронная версия)
|
||||
* @returns {string} Название модели
|
||||
*/
|
||||
function getDefaultModel() {
|
||||
// Приоритет: кэш из БД > дефолт
|
||||
if (settingsCache && settingsCache.selected_model) {
|
||||
return settingsCache.selected_model;
|
||||
}
|
||||
// Дефолтное значение если БД недоступна
|
||||
return 'qwen2.5:7b';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает модель асинхронно из БД
|
||||
* @returns {Promise<string>} Название модели из БД
|
||||
*/
|
||||
async function getDefaultModelAsync() {
|
||||
try {
|
||||
if (!settingsCache) {
|
||||
await loadSettingsFromDb();
|
||||
}
|
||||
|
||||
if (settingsCache && settingsCache.selected_model) {
|
||||
logger.info(`[ollamaConfig] Using model from DB: ${settingsCache.selected_model}`);
|
||||
return settingsCache.selected_model;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[ollamaConfig] Failed to load model from DB, using default');
|
||||
}
|
||||
return 'qwen2.5:7b';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает embedding модель асинхронно из БД
|
||||
* @returns {Promise<string>} Название embedding модели из БД
|
||||
*/
|
||||
async function getEmbeddingModel() {
|
||||
try {
|
||||
if (!settingsCache) {
|
||||
await loadSettingsFromDb();
|
||||
}
|
||||
|
||||
if (settingsCache && settingsCache.embedding_model) {
|
||||
logger.info(`[ollamaConfig] Using embedding model from DB: ${settingsCache.embedding_model}`);
|
||||
return settingsCache.embedding_model;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[ollamaConfig] Failed to load embedding model from DB, using default');
|
||||
}
|
||||
return 'mxbai-embed-large:latest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает timeout для запросов к Ollama
|
||||
* @returns {number} Timeout в миллисекундах
|
||||
*/
|
||||
function getTimeout() {
|
||||
return 30000; // 30 секунд
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все конфигурационные параметры Ollama (синхронная версия)
|
||||
* @returns {Object} Объект с конфигурацией
|
||||
*/
|
||||
function getConfig() {
|
||||
return {
|
||||
baseUrl: getBaseUrl(),
|
||||
defaultModel: getDefaultModel(),
|
||||
timeout: getTimeout(),
|
||||
apiUrl: {
|
||||
tags: getApiUrl('tags'),
|
||||
generate: getApiUrl('generate'),
|
||||
chat: getApiUrl('chat'),
|
||||
models: getApiUrl('models'),
|
||||
show: getApiUrl('show'),
|
||||
pull: getApiUrl('pull'),
|
||||
push: getApiUrl('push')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все конфигурационные параметры Ollama (асинхронная версия)
|
||||
* @returns {Promise<Object>} Объект с конфигурацией
|
||||
*/
|
||||
async function getConfigAsync() {
|
||||
const baseUrl = await getBaseUrlAsync();
|
||||
const defaultModel = await getDefaultModelAsync();
|
||||
const embeddingModel = await getEmbeddingModel();
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
defaultModel,
|
||||
embeddingModel,
|
||||
timeout: getTimeout(),
|
||||
apiUrl: {
|
||||
tags: `${baseUrl}/api/tags`,
|
||||
generate: `${baseUrl}/api/generate`,
|
||||
chat: `${baseUrl}/api/chat`,
|
||||
models: `${baseUrl}/api/models`,
|
||||
show: `${baseUrl}/api/show`,
|
||||
pull: `${baseUrl}/api/pull`,
|
||||
push: `${baseUrl}/api/push`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает кэш настроек (для перезагрузки)
|
||||
*/
|
||||
function clearCache() {
|
||||
settingsCache = null;
|
||||
logger.info('[ollamaConfig] Settings cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет доступность Ollama сервиса
|
||||
* @returns {Promise<Object>} Статус здоровья сервиса
|
||||
*/
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const baseUrl = getBaseUrl();
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: `Ollama вернул код ${response.status}`,
|
||||
baseUrl
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
status: 'ok',
|
||||
baseUrl,
|
||||
model: getDefaultModel(),
|
||||
availableModels: data.models?.length || 0
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
baseUrl: getBaseUrl()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBaseUrl,
|
||||
getBaseUrlAsync,
|
||||
getApiUrl,
|
||||
getDefaultModel,
|
||||
getDefaultModelAsync,
|
||||
getEmbeddingModel,
|
||||
getTimeout,
|
||||
getConfig,
|
||||
getConfigAsync,
|
||||
loadSettingsFromDb,
|
||||
clearCache,
|
||||
checkHealth
|
||||
};
|
||||
@@ -31,7 +31,10 @@ async function getTableData(tableId) {
|
||||
const rows = await encryptedDb.getData('user_rows', { table_id: tableId });
|
||||
// console.log(`[RAG] Found ${rows.length} rows:`, rows.map(row => ({ id: row.id, name: row.name })));
|
||||
|
||||
const cellValues = await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } });
|
||||
// Исправление: проверяем что есть строки перед запросом cell_values
|
||||
const cellValues = rows.length > 0
|
||||
? await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } })
|
||||
: [];
|
||||
// console.log(`[RAG] Found ${cellValues.length} cell values`);
|
||||
|
||||
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
|
||||
@@ -120,7 +123,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
|
||||
|
||||
// Поиск
|
||||
let results = [];
|
||||
if (rowsForUpsert.length > 0) {
|
||||
if (rowsForUpsert.length > 0 && userQuestion && userQuestion.trim()) {
|
||||
results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска
|
||||
// console.log(`[RAG] Search completed, got ${results.length} results`);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const { processGuestMessages } = require('../routes/chat');
|
||||
const guestMessageService = require('./guestMessageService');
|
||||
|
||||
/**
|
||||
* Сервис для работы с сессиями пользователей
|
||||
@@ -100,7 +100,7 @@ class SessionService {
|
||||
|
||||
// Обрабатываем сообщения для каждого гостевого ID
|
||||
for (const guestId of guestIdsToProcess) {
|
||||
await this.processGuestMessagesWrapper(userId, guestId);
|
||||
await guestMessageService.processGuestMessages(userId, guestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,20 +127,7 @@ class SessionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обертка для функции processGuestMessages
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<Object>} - Результат операции
|
||||
*/
|
||||
async processGuestMessagesWrapper(userId, guestId) {
|
||||
try {
|
||||
return await processGuestMessages(userId, guestId);
|
||||
} catch (error) {
|
||||
logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Обертка processGuestMessagesWrapper удалена - используется прямой вызов guestMessageService.processGuestMessages
|
||||
|
||||
/**
|
||||
* Получает сессию из хранилища по ID
|
||||
|
||||
@@ -13,317 +13,203 @@
|
||||
const { Telegraf } = require('telegraf');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const db = require('../db');
|
||||
const authService = require('./auth-service');
|
||||
const verificationService = require('./verification-service');
|
||||
const crypto = require('crypto');
|
||||
const identityService = require('./identity-service');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const { checkAdminRole } = require('./admin-role');
|
||||
const { broadcastContactsUpdate, broadcastChatMessage } = require('../wsHub');
|
||||
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
||||
const { ragAnswer, generateLLMResponse } = require('./ragService');
|
||||
const { isUserBlocked } = require('../utils/userUtils');
|
||||
|
||||
let botInstance = null;
|
||||
let telegramSettingsCache = null;
|
||||
/**
|
||||
* TelegramBot - обработчик Telegram сообщений
|
||||
* Унифицированный интерфейс для работы с Telegram
|
||||
*/
|
||||
class TelegramBot {
|
||||
constructor() {
|
||||
this.name = 'TelegramBot';
|
||||
this.channel = 'telegram';
|
||||
this.bot = null;
|
||||
this.settings = null;
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
}
|
||||
|
||||
async function getTelegramSettings() {
|
||||
if (telegramSettingsCache) return telegramSettingsCache;
|
||||
|
||||
const settings = await encryptedDb.getData('telegram_settings', {}, 1);
|
||||
if (!settings.length) throw new Error('Telegram settings not found in DB');
|
||||
|
||||
telegramSettingsCache = settings[0];
|
||||
return telegramSettingsCache;
|
||||
}
|
||||
/**
|
||||
* Инициализация Telegram Bot
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
logger.info('[TelegramBot] 🚀 Инициализация Telegram Bot...');
|
||||
|
||||
// Загружаем настройки из БД
|
||||
this.settings = await this.loadSettings();
|
||||
|
||||
if (!this.settings || !this.settings.bot_token) {
|
||||
logger.warn('[TelegramBot] ⚠️ Настройки Telegram не найдены');
|
||||
this.status = 'not_configured';
|
||||
return { success: false, reason: 'not_configured' };
|
||||
}
|
||||
|
||||
// Создание и настройка бота
|
||||
async function getBot() {
|
||||
// console.log('[TelegramBot] getBot() called');
|
||||
if (!botInstance) {
|
||||
// console.log('[TelegramBot] Creating new bot instance...');
|
||||
const settings = await getTelegramSettings();
|
||||
// console.log('[TelegramBot] Got settings, creating Telegraf instance...');
|
||||
botInstance = new Telegraf(settings.bot_token);
|
||||
// console.log('[TelegramBot] Telegraf instance created');
|
||||
// Проверяем токен
|
||||
if (!this.settings.bot_token || typeof this.settings.bot_token !== 'string') {
|
||||
logger.error('[TelegramBot] ❌ Некорректный токен:', {
|
||||
tokenExists: !!this.settings.bot_token,
|
||||
tokenType: typeof this.settings.bot_token,
|
||||
tokenLength: this.settings.bot_token?.length || 0
|
||||
});
|
||||
this.status = 'invalid_token';
|
||||
return { success: false, reason: 'invalid_token' };
|
||||
}
|
||||
|
||||
// Обработка команды /start
|
||||
botInstance.command('start', (ctx) => {
|
||||
// Проверяем токен через Telegram API
|
||||
try {
|
||||
logger.info('[TelegramBot] Проверяем токен через Telegram API...');
|
||||
const testBot = new Telegraf(this.settings.bot_token);
|
||||
const me = await testBot.telegram.getMe();
|
||||
logger.info('[TelegramBot] ✅ Токен валиден, бот:', me.username);
|
||||
// Не вызываем stop() - может вызвать ошибку
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] ❌ Токен невалиден или проблема с API:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response?.data
|
||||
});
|
||||
this.status = 'invalid_token';
|
||||
return { success: false, reason: 'invalid_token' };
|
||||
}
|
||||
|
||||
// Создаем экземпляр бота
|
||||
this.bot = new Telegraf(this.settings.bot_token);
|
||||
|
||||
// Настраиваем обработчики
|
||||
this.setupHandlers();
|
||||
|
||||
// Сначала помечаем как инициализированный
|
||||
this.isInitialized = true;
|
||||
this.status = 'active';
|
||||
|
||||
// Запускаем бота асинхронно (может долго подключаться)
|
||||
this.launch()
|
||||
.then(() => {
|
||||
logger.info('[TelegramBot] ✅ Бот успешно подключен к Telegram');
|
||||
this.status = 'active';
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('[TelegramBot] Ошибка подключения к Telegram:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response?.data,
|
||||
stack: error.stack
|
||||
});
|
||||
this.status = 'error';
|
||||
});
|
||||
|
||||
logger.info('[TelegramBot] ✅ Telegram Bot инициализирован (подключение в фоне)');
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('409: Conflict')) {
|
||||
logger.warn('[TelegramBot] ⚠️ Telegram Bot уже запущен в другом процессе');
|
||||
this.status = 'conflict';
|
||||
} else {
|
||||
logger.error('[TelegramBot] ❌ Ошибка инициализации:', error);
|
||||
this.status = 'error';
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка настроек из БД
|
||||
*/
|
||||
async loadSettings() {
|
||||
try {
|
||||
const settings = await encryptedDb.getData('telegram_settings', {}, 1);
|
||||
if (!settings.length) {
|
||||
return null;
|
||||
}
|
||||
return settings[0];
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка загрузки настроек:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработчиков команд и сообщений
|
||||
*/
|
||||
setupHandlers() {
|
||||
// Обработчик команды /start
|
||||
this.bot.command('start', (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получена команда /start');
|
||||
ctx.reply('Добро пожаловать! Отправьте код подтверждения для аутентификации.');
|
||||
});
|
||||
|
||||
// Универсальный обработчик текстовых сообщений
|
||||
botInstance.on('text', async (ctx) => {
|
||||
const text = ctx.message.text.trim();
|
||||
// 1. Если команда — пропустить
|
||||
if (text.startsWith('/')) return;
|
||||
|
||||
// Отправляем индикатор печати для улучшения UX
|
||||
const typingAction = ctx.replyWithChatAction('typing');
|
||||
|
||||
// 2. Проверка: это потенциальный код?
|
||||
const isPotentialCode = (str) => /^[A-Z0-9]{6}$/i.test(str);
|
||||
if (isPotentialCode(text)) {
|
||||
await typingAction;
|
||||
try {
|
||||
// Получаем код верификации для всех активных кодов с провайдером telegram
|
||||
const codes = await encryptedDb.getData('verification_codes', {
|
||||
code: text.toUpperCase(),
|
||||
provider: 'telegram',
|
||||
used: false
|
||||
}, 1);
|
||||
// Обработчик текстовых сообщений
|
||||
this.bot.on('text', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено текстовое сообщение');
|
||||
await this.handleTextMessage(ctx);
|
||||
});
|
||||
|
||||
if (codes.length === 0) {
|
||||
ctx.reply('Неверный код подтверждения');
|
||||
return;
|
||||
}
|
||||
// Обработчик документов
|
||||
this.bot.on('document', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получен документ');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
|
||||
const verification = codes[0];
|
||||
const providerId = verification.provider_id;
|
||||
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
|
||||
let userId;
|
||||
let userRole = 'user'; // Роль по умолчанию
|
||||
// Обработчик фото
|
||||
this.bot.on('photo', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено фото');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
|
||||
// Отмечаем код как использованный
|
||||
await encryptedDb.saveData('verification_codes', {
|
||||
used: true
|
||||
}, {
|
||||
id: verification.id
|
||||
});
|
||||
// Обработчик аудио
|
||||
this.bot.on('audio', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено аудио');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
|
||||
logger.info('Starting Telegram auth process for code:', text);
|
||||
// Обработчик видео
|
||||
this.bot.on('video', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено видео');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем, существует ли уже пользователь с таким Telegram ID
|
||||
const existingTelegramUsers = await encryptedDb.getData('user_identities', {
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
}, 1);
|
||||
/**
|
||||
* Обработка текстовых сообщений
|
||||
*/
|
||||
async handleTextMessage(ctx) {
|
||||
const text = ctx.message.text.trim();
|
||||
|
||||
// Пропускаем команды
|
||||
if (text.startsWith('/')) return;
|
||||
|
||||
if (existingTelegramUsers.length > 0) {
|
||||
// Если пользователь с таким Telegram ID уже существует, используем его
|
||||
userId = existingTelegramUsers[0].user_id;
|
||||
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
|
||||
} else {
|
||||
// Если код верификации был связан с существующим пользователем, используем его
|
||||
if (linkedUserId) {
|
||||
// Используем userId из кода верификации
|
||||
userId = linkedUserId;
|
||||
// Связываем Telegram с этим пользователем
|
||||
await encryptedDb.saveData('user_identities', {
|
||||
user_id: userId,
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
});
|
||||
logger.info(
|
||||
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
|
||||
);
|
||||
} else {
|
||||
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором
|
||||
let existingUserWithGuestId = null;
|
||||
if (providerId) {
|
||||
const guestUserResult = await encryptedDb.getData('guest_user_mapping', {
|
||||
guest_id: providerId
|
||||
}, 1);
|
||||
if (guestUserResult.length > 0) {
|
||||
existingUserWithGuestId = guestUserResult[0].user_id;
|
||||
logger.info(
|
||||
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Обрабатываем как обычное сообщение
|
||||
await this.handleMessage(ctx);
|
||||
}
|
||||
|
||||
if (existingUserWithGuestId) {
|
||||
// Используем существующего пользователя и добавляем ему Telegram идентификатор
|
||||
userId = existingUserWithGuestId;
|
||||
await encryptedDb.saveData('user_identities', {
|
||||
user_id: userId,
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
});
|
||||
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
|
||||
} else {
|
||||
// Создаем нового пользователя, если не нашли существующего
|
||||
const userResult = await encryptedDb.saveData('users', {
|
||||
created_at: new Date(),
|
||||
role: 'user'
|
||||
});
|
||||
userId = userResult.id;
|
||||
|
||||
// Связываем Telegram с новым пользователем
|
||||
await encryptedDb.saveData('user_identities', {
|
||||
user_id: userId,
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
});
|
||||
|
||||
// Если был гостевой ID, связываем его с новым пользователем
|
||||
if (providerId) {
|
||||
await encryptedDb.saveData('guest_user_mapping', {
|
||||
user_id: userId,
|
||||
guest_id: providerId
|
||||
}, {
|
||||
user_id: userId
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----> НАЧАЛО: Проверка роли на основе привязанного кошелька <----
|
||||
if (userId) { // Убедимся, что userId определен
|
||||
logger.info(`[TelegramBot] Checking linked wallet for determined userId: ${userId} (Type: ${typeof userId})`);
|
||||
try {
|
||||
const linkedWallet = await authService.getLinkedWallet(userId);
|
||||
if (linkedWallet) {
|
||||
logger.info(`[TelegramBot] Found linked wallet ${linkedWallet} for user ${userId}. Checking role...`);
|
||||
const isAdmin = await checkAdminRole(linkedWallet);
|
||||
userRole = isAdmin ? 'admin' : 'user';
|
||||
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
|
||||
|
||||
// Опционально: Обновить роль в таблице users
|
||||
const currentUser = await encryptedDb.getData('users', {
|
||||
id: userId
|
||||
}, 1);
|
||||
if (currentUser.length > 0 && currentUser[0].role !== userRole) {
|
||||
await encryptedDb.saveData('users', {
|
||||
role: userRole
|
||||
}, {
|
||||
id: userId
|
||||
});
|
||||
logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
|
||||
}
|
||||
} else {
|
||||
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
|
||||
// Если кошелька нет, берем текущую роль из базы
|
||||
const currentUser = await encryptedDb.getData('users', {
|
||||
id: userId
|
||||
}, 1);
|
||||
if (currentUser.length > 0) {
|
||||
userRole = currentUser[0].role;
|
||||
}
|
||||
}
|
||||
} catch (roleCheckError) {
|
||||
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
|
||||
// В случае ошибки берем роль из базы или оставляем 'user'
|
||||
try {
|
||||
const currentUser = await encryptedDb.getData('users', {
|
||||
id: userId
|
||||
}, 1);
|
||||
if (currentUser.length > 0) { userRole = currentUser[0].role; }
|
||||
} catch (dbError) { /* ignore */ }
|
||||
}
|
||||
} else {
|
||||
logger.error('[TelegramBot] Cannot check role because userId is undefined!');
|
||||
}
|
||||
// ----> КОНЕЦ: Проверка роли <----
|
||||
|
||||
// Логируем userId перед обновлением сессии
|
||||
logger.info(`[telegramBot] Attempting to update session for userId: ${userId}`);
|
||||
|
||||
// Находим последнюю активную сессию для данного userId
|
||||
let activeSessionId = null;
|
||||
try {
|
||||
// Ищем сессию, где есть userId и она не истекла (проверка expires_at)
|
||||
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
|
||||
const sessionResult = await encryptedDb.getData('session', {
|
||||
'sess->>userId': userId?.toString()
|
||||
}, 1, 'expire', 'DESC');
|
||||
|
||||
if (sessionResult.length > 0) {
|
||||
activeSessionId = sessionResult[0].sid;
|
||||
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
|
||||
|
||||
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
|
||||
const updateResult = await encryptedDb.saveData('session', {
|
||||
sess: JSON.stringify({
|
||||
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
|
||||
authType: 'telegram', // Обновляем тип аутентификации
|
||||
telegramId: ctx.from.id.toString(),
|
||||
telegramUsername: ctx.from.username,
|
||||
telegramFirstName: ctx.from.first_name,
|
||||
role: userRole, // Записываем определенную роль
|
||||
// userId: userId?.toString() // userId уже должен быть в сессии
|
||||
})
|
||||
}, {
|
||||
sid: activeSessionId
|
||||
});
|
||||
|
||||
if (updateResult.rowCount > 0) {
|
||||
logger.info(`[telegramBot] Session ${activeSessionId} updated successfully with Telegram data for user ${userId}`);
|
||||
} else {
|
||||
logger.warn(`[telegramBot] Session update query executed but did not update rows for sid: ${activeSessionId}. This might indicate a concurrency issue or incorrect sid.`);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.warn(`[telegramBot] No active web session found for userId: ${userId}. Telegram is linked, but the user might need to refresh their browser session.`);
|
||||
}
|
||||
} catch(sessionError) {
|
||||
logger.error(`[telegramBot] Error finding or updating session for userId ${userId}:`, sessionError);
|
||||
}
|
||||
|
||||
// Отправляем сообщение об успешной аутентификации
|
||||
await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.');
|
||||
|
||||
// Удаляем сообщение с кодом
|
||||
try {
|
||||
await ctx.deleteMessage(ctx.message.message_id);
|
||||
} catch (error) {
|
||||
logger.warn('Could not delete code message:', error);
|
||||
}
|
||||
|
||||
// После каждого успешного создания пользователя:
|
||||
broadcastContactsUpdate();
|
||||
} catch (error) {
|
||||
logger.error('Error in Telegram auth:', error);
|
||||
await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Всё остальное — чат с ИИ-ассистентом
|
||||
/**
|
||||
* Извлечение данных из Telegram сообщения
|
||||
* @param {Object} ctx - Telegraf context
|
||||
* @returns {Object} - Стандартизированные данные сообщения
|
||||
*/
|
||||
extractMessageData(ctx) {
|
||||
try {
|
||||
const telegramId = ctx.from.id.toString();
|
||||
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId);
|
||||
if (await isUserBlocked(userId)) {
|
||||
await ctx.reply('Вы заблокированы. Сообщения не принимаются.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await encryptedDb.getData('conversations', {
|
||||
user_id: userId
|
||||
}, 1, 'updated_at', 'DESC', 'created_at', 'DESC');
|
||||
let conversation;
|
||||
if (conversationResult.length === 0) {
|
||||
const title = `Чат с пользователем ${userId}`;
|
||||
const newConv = await encryptedDb.saveData('conversations', {
|
||||
user_id: userId,
|
||||
title: title,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
conversation = newConv;
|
||||
} else {
|
||||
conversation = conversationResult[0];
|
||||
}
|
||||
|
||||
// 2. Сохранять все сообщения с conversation_id
|
||||
let content = text;
|
||||
let attachmentMeta = {};
|
||||
// Проверяем вложения (фото, документ, аудио, видео)
|
||||
let fileId, fileName, mimeType, fileSize, attachmentBuffer;
|
||||
let content = '';
|
||||
let attachments = [];
|
||||
|
||||
// Текст сообщения
|
||||
if (ctx.message.text) {
|
||||
content = ctx.message.text.trim();
|
||||
} else if (ctx.message.caption) {
|
||||
content = ctx.message.caption.trim();
|
||||
}
|
||||
|
||||
// Обработка вложений
|
||||
let fileId, fileName, mimeType, fileSize;
|
||||
|
||||
if (ctx.message.document) {
|
||||
fileId = ctx.message.document.file_id;
|
||||
fileName = ctx.message.document.file_name;
|
||||
mimeType = ctx.message.document.mime_type;
|
||||
fileSize = ctx.message.document.file_size;
|
||||
} else if (ctx.message.photo && ctx.message.photo.length > 0) {
|
||||
// Берём самое большое фото
|
||||
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
||||
fileId = photo.file_id;
|
||||
fileName = 'photo.jpg';
|
||||
@@ -341,339 +227,185 @@ async function getBot() {
|
||||
fileSize = ctx.message.video.file_size;
|
||||
}
|
||||
|
||||
// Асинхронная загрузка файлов
|
||||
if (fileId) {
|
||||
try {
|
||||
const fileLink = await ctx.telegram.getFileLink(fileId);
|
||||
const res = await fetch(fileLink.href);
|
||||
attachmentBuffer = await res.buffer();
|
||||
attachmentMeta = {
|
||||
attachment_filename: fileName,
|
||||
attachment_mimetype: mimeType,
|
||||
attachment_size: fileSize,
|
||||
attachment_data: attachmentBuffer
|
||||
};
|
||||
} catch (fileError) {
|
||||
logger.error('[TelegramBot] Error downloading file:', fileError);
|
||||
// Продолжаем без файла
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем сообщение в БД
|
||||
if (!conversation || !conversation.id) {
|
||||
logger.error(`[TelegramBot] Conversation is undefined or has no id for user ${userId}`);
|
||||
await ctx.reply('Произошла ошибка при создании диалога. Попробуйте позже.');
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage = await encryptedDb.saveData('messages', {
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'user',
|
||||
content: content,
|
||||
channel: 'telegram',
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
attachment_filename: attachmentMeta.attachment_filename || null,
|
||||
attachment_mimetype: attachmentMeta.attachment_mimetype || null,
|
||||
attachment_size: attachmentMeta.attachment_size || null,
|
||||
attachment_data: attachmentMeta.attachment_data || null
|
||||
attachments.push({
|
||||
type: 'telegram_file',
|
||||
fileId: fileId,
|
||||
filename: fileName,
|
||||
mimetype: mimeType,
|
||||
size: fileSize,
|
||||
ctx: ctx // Сохраняем контекст для последующей загрузки
|
||||
});
|
||||
|
||||
// Отправляем WebSocket уведомление о пользовательском сообщении
|
||||
try {
|
||||
const decryptedUserMessage = await encryptedDb.getData('messages', { id: userMessage.id }, 1);
|
||||
if (decryptedUserMessage && decryptedUserMessage[0]) {
|
||||
broadcastChatMessage(decryptedUserMessage[0], userId);
|
||||
}
|
||||
} catch (wsError) {
|
||||
logger.error('[TelegramBot] WebSocket notification error for user message:', wsError);
|
||||
}
|
||||
|
||||
if (await isUserBlocked(userId)) {
|
||||
logger.info(`[TelegramBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Получить ответ от ИИ (RAG + LLM) - асинхронно
|
||||
const aiResponsePromise = (async () => {
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
|
||||
// Загружаем историю сообщений для контекста (ограничиваем до 5 сообщений)
|
||||
let history = null;
|
||||
try {
|
||||
const recentMessages = await encryptedDb.getData('messages', {
|
||||
conversation_id: conversation.id
|
||||
}, 5, 'created_at DESC');
|
||||
|
||||
if (recentMessages && recentMessages.length > 0) {
|
||||
// Преобразуем сообщения в формат для AI
|
||||
history = recentMessages.reverse().map(msg => ({
|
||||
// Любые человеческие роли трактуем как 'user', только ответы ассистента — 'assistant'
|
||||
role: msg.sender_type === 'assistant' ? 'assistant' : 'user',
|
||||
content: msg.content || ''
|
||||
}));
|
||||
}
|
||||
} catch (historyError) {
|
||||
logger.error('[TelegramBot] Error loading message history:', historyError);
|
||||
}
|
||||
|
||||
let aiResponse;
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
// Используем очередь AIQueue для LLM генерации
|
||||
const requestId = await aiAssistant.addToQueue({
|
||||
message: content,
|
||||
history: history,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
rules: null
|
||||
}, 0);
|
||||
|
||||
// Ждем ответ из очереди
|
||||
aiResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('AI response timeout'));
|
||||
}, 120000); // 2 минуты таймаут
|
||||
|
||||
const onCompleted = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
aiAssistant.aiQueue.off('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.off('requestFailed', onFailed);
|
||||
resolve(item.result);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
aiAssistant.aiQueue.off('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.off('requestFailed', onFailed);
|
||||
reject(new Error(item.error));
|
||||
}
|
||||
};
|
||||
|
||||
aiAssistant.aiQueue.on('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.on('requestFailed', onFailed);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Используем очередь AIQueue для обработки
|
||||
const requestId = await aiAssistant.addToQueue({
|
||||
message: content,
|
||||
history: history,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
rules: null
|
||||
}, 0);
|
||||
|
||||
// Ждем ответ из очереди
|
||||
aiResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('AI response timeout'));
|
||||
}, 120000); // 2 минуты таймаут
|
||||
|
||||
const onCompleted = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
aiAssistant.aiQueue.off('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.off('requestFailed', onFailed);
|
||||
resolve(item.result);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
aiAssistant.aiQueue.off('requestFailed', onFailed);
|
||||
reject(new Error(item.error));
|
||||
}
|
||||
};
|
||||
|
||||
aiAssistant.aiQueue.on('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.on('requestFailed', onFailed);
|
||||
});
|
||||
}
|
||||
|
||||
return aiResponse;
|
||||
})();
|
||||
|
||||
// Ждем ответ от ИИ с таймаутом
|
||||
const aiResponse = await Promise.race([
|
||||
aiResponsePromise,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('AI response timeout')), 120000)
|
||||
)
|
||||
]);
|
||||
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
const aiMessage = await encryptedDb.saveData('messages', {
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'assistant',
|
||||
content: aiResponse,
|
||||
channel: 'telegram',
|
||||
role: 'assistant',
|
||||
direction: 'out',
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
// 5. Отправить ответ пользователю
|
||||
await ctx.reply(aiResponse);
|
||||
|
||||
// 6. Отправить WebSocket уведомление
|
||||
try {
|
||||
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
|
||||
if (decryptedAiMessage && decryptedAiMessage[0]) {
|
||||
broadcastChatMessage(decryptedAiMessage[0], userId);
|
||||
}
|
||||
} catch (wsError) {
|
||||
logger.error('[TelegramBot] WebSocket notification error:', wsError);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка при обработке сообщения:', error);
|
||||
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
|
||||
}
|
||||
});
|
||||
|
||||
// Запуск бота с таймаутом
|
||||
// console.log('[TelegramBot] Before botInstance.launch()');
|
||||
try {
|
||||
// Запускаем бота с таймаутом
|
||||
const launchPromise = botInstance.launch();
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 30000); // 30 секунд таймаут
|
||||
});
|
||||
|
||||
await Promise.race([launchPromise, timeoutPromise]);
|
||||
// console.log('[TelegramBot] After botInstance.launch()');
|
||||
logger.info('[TelegramBot] Бот запущен');
|
||||
return {
|
||||
channel: 'telegram',
|
||||
identifier: telegramId,
|
||||
content: content,
|
||||
attachments: attachments,
|
||||
metadata: {
|
||||
telegramUsername: ctx.from.username,
|
||||
telegramFirstName: ctx.from.first_name,
|
||||
telegramLastName: ctx.from.last_name,
|
||||
messageId: ctx.message.message_id,
|
||||
chatId: ctx.chat.id
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// console.error('[TelegramBot] Error launching bot:', error);
|
||||
// Не выбрасываем ошибку, чтобы не блокировать запуск сервера
|
||||
// console.log('[TelegramBot] Bot launch failed, but continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
return botInstance;
|
||||
}
|
||||
|
||||
// Остановка бота
|
||||
async function stopBot() {
|
||||
if (botInstance) {
|
||||
try {
|
||||
await botInstance.stop();
|
||||
botInstance = null;
|
||||
logger.info('Telegram bot stopped successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error stopping Telegram bot:', error);
|
||||
logger.error('[TelegramBot] Ошибка извлечения данных из сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация процесса аутентификации
|
||||
async function initTelegramAuth(session) {
|
||||
try {
|
||||
// Используем временный идентификатор для создания кода верификации
|
||||
// Реальный пользователь будет создан или найден при проверке кода через бота
|
||||
const tempId = crypto.randomBytes(16).toString('hex');
|
||||
/**
|
||||
* Загрузка файла из Telegram
|
||||
* @param {Object} attachment - Данные вложения
|
||||
* @returns {Promise<Buffer>} - Буфер с данными файла
|
||||
*/
|
||||
async downloadAttachment(attachment) {
|
||||
try {
|
||||
const fileLink = await attachment.ctx.telegram.getFileLink(attachment.fileId);
|
||||
const res = await fetch(fileLink.href);
|
||||
return await res.buffer();
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка загрузки файла:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Если пользователь уже авторизован, сохраняем его userId в guest_user_mapping
|
||||
// чтобы потом при авторизации через бота этот пользователь был найден
|
||||
if (session && session.authenticated && session.userId) {
|
||||
const guestId = session.guestId || tempId;
|
||||
/**
|
||||
* Обработка сообщения через процессор
|
||||
* @param {Object} ctx - Telegraf context
|
||||
* @param {Function} processor - Функция обработки сообщения
|
||||
*/
|
||||
async handleMessage(ctx, processor = null) {
|
||||
try {
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
// Извлекаем данные из сообщения
|
||||
const messageData = this.extractMessageData(ctx);
|
||||
|
||||
logger.info(`[TelegramBot] Обработка сообщения от пользователя: ${messageData.identifier}`);
|
||||
|
||||
// Связываем гостевой ID с текущим пользователем
|
||||
await encryptedDb.saveData('guest_user_mapping', {
|
||||
user_id: session.userId,
|
||||
guest_id: guestId
|
||||
}, {
|
||||
user_id: session.userId
|
||||
// Загружаем вложения если есть
|
||||
for (const attachment of messageData.attachments) {
|
||||
const buffer = await this.downloadAttachment(attachment);
|
||||
if (buffer) {
|
||||
attachment.data = buffer;
|
||||
// Удаляем ctx из вложения
|
||||
delete attachment.ctx;
|
||||
}
|
||||
}
|
||||
|
||||
// Используем установленный процессор или переданный
|
||||
const messageProcessor = processor || this.messageProcessor;
|
||||
|
||||
if (!messageProcessor) {
|
||||
await ctx.reply('Сообщение получено и будет обработано.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Обрабатываем сообщение через унифицированный процессор
|
||||
const result = await messageProcessor(messageData);
|
||||
|
||||
// Отправляем ответ пользователю
|
||||
if (result.success && result.aiResponse) {
|
||||
await ctx.reply(result.aiResponse.response);
|
||||
} else if (result.success) {
|
||||
await ctx.reply('Сообщение получено');
|
||||
} else {
|
||||
await ctx.reply('Произошла ошибка при обработке сообщения');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка обработки сообщения:', error);
|
||||
try {
|
||||
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
|
||||
} catch (replyError) {
|
||||
logger.error('[TelegramBot] Не удалось отправить сообщение об ошибке:', replyError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск бота (без timeout и retry - Telegraf сам управляет подключением)
|
||||
*/
|
||||
async launch() {
|
||||
try {
|
||||
logger.info('[TelegramBot] Запуск polling...');
|
||||
|
||||
// Запускаем бота без таймаута - пусть Telegraf сам управляет подключением
|
||||
await this.bot.launch({
|
||||
dropPendingUpdates: true,
|
||||
allowedUpdates: ['message', 'callback_query']
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`
|
||||
);
|
||||
|
||||
logger.info('[TelegramBot] ✅ Бот запущен успешно');
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] ❌ Ошибка запуска:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response?.data,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем код через сервис верификации с идентификатором
|
||||
const code = await verificationService.createVerificationCode(
|
||||
'telegram',
|
||||
session.guestId || tempId,
|
||||
session.authenticated ? session.userId : null
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}`
|
||||
);
|
||||
/**
|
||||
* Установка процессора сообщений
|
||||
* @param {Function} processor - Функция обработки сообщений
|
||||
*/
|
||||
setMessageProcessor(processor) {
|
||||
this.messageProcessor = processor;
|
||||
logger.info('[TelegramBot] ✅ Процессор сообщений установлен');
|
||||
}
|
||||
|
||||
const settings = await getTelegramSettings();
|
||||
/**
|
||||
* Проверка статуса бота
|
||||
* @returns {Object} - Статус бота
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
verificationCode: code,
|
||||
botLink: `https://t.me/${settings.bot_username}`,
|
||||
name: this.name,
|
||||
channel: this.channel,
|
||||
isInitialized: this.isInitialized,
|
||||
status: this.status,
|
||||
hasSettings: !!this.settings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение экземпляра бота (для совместимости)
|
||||
* @returns {Object} - Экземпляр Telegraf бота
|
||||
*/
|
||||
getBot() {
|
||||
return this.bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка бота
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
logger.info('[TelegramBot] 🛑 Остановка Telegram Bot...');
|
||||
|
||||
if (this.bot) {
|
||||
await this.bot.stop();
|
||||
this.bot = null;
|
||||
}
|
||||
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
|
||||
logger.info('[TelegramBot] ✅ Telegram Bot остановлен');
|
||||
} catch (error) {
|
||||
logger.error('Error initializing Telegram auth:', error);
|
||||
logger.error('[TelegramBot] ❌ Ошибка остановки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function clearSettingsCache() {
|
||||
telegramSettingsCache = null;
|
||||
}
|
||||
|
||||
// Сохранение настроек Telegram
|
||||
async function saveTelegramSettings(settings) {
|
||||
try {
|
||||
// Очищаем кэш настроек
|
||||
clearSettingsCache();
|
||||
|
||||
// Проверяем, существуют ли уже настройки
|
||||
const existingSettings = await encryptedDb.getData('telegram_settings', {}, 1);
|
||||
|
||||
let result;
|
||||
if (existingSettings.length > 0) {
|
||||
// Если настройки существуют, обновляем их
|
||||
const existingId = existingSettings[0].id;
|
||||
result = await encryptedDb.saveData('telegram_settings', settings, { id: existingId });
|
||||
} else {
|
||||
// Если настроек нет, создаем новые
|
||||
result = await encryptedDb.saveData('telegram_settings', settings, null);
|
||||
}
|
||||
|
||||
// Обновляем кэш
|
||||
telegramSettingsCache = settings;
|
||||
|
||||
logger.info('Telegram settings saved successfully');
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
logger.error('Error saving Telegram settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
module.exports = TelegramBot;
|
||||
|
||||
async function getAllBots() {
|
||||
const settings = await encryptedDb.getData('telegram_settings', {}, 1, 'id');
|
||||
return settings;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTelegramSettings,
|
||||
getBot,
|
||||
stopBot,
|
||||
initTelegramAuth,
|
||||
clearSettingsCache,
|
||||
saveTelegramSettings,
|
||||
getAllBots,
|
||||
};
|
||||
|
||||
213
backend/services/testNewBots.js
Normal file
213
backend/services/testNewBots.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const botManager = require('./botManager');
|
||||
const WebBot = require('./webBot');
|
||||
const TelegramBot = require('./telegramBot');
|
||||
const EmailBot = require('./emailBot');
|
||||
|
||||
/**
|
||||
* Тестовый скрипт для проверки новой архитектуры ботов
|
||||
* Используется для отладки и тестирования ботов
|
||||
*/
|
||||
|
||||
/**
|
||||
* Тест Web Bot
|
||||
*/
|
||||
async function testWebBot() {
|
||||
console.log('\n=== ТЕСТ WEB BOT ===');
|
||||
|
||||
try {
|
||||
const webBot = new WebBot();
|
||||
|
||||
// Инициализация
|
||||
const initResult = await webBot.initialize();
|
||||
console.log('✓ Инициализация:', initResult);
|
||||
|
||||
// Статус
|
||||
const status = webBot.getStatus();
|
||||
console.log('✓ Статус:', status);
|
||||
|
||||
// Тестовое сообщение
|
||||
const testMessage = {
|
||||
userId: 1,
|
||||
content: 'Тестовое сообщение',
|
||||
channel: 'web'
|
||||
};
|
||||
|
||||
console.log('✓ Web Bot тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Web Bot:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест Telegram Bot
|
||||
*/
|
||||
async function testTelegramBot() {
|
||||
console.log('\n=== ТЕСТ TELEGRAM BOT ===');
|
||||
|
||||
try {
|
||||
const telegramBot = new TelegramBot();
|
||||
|
||||
// Инициализация
|
||||
const initResult = await telegramBot.initialize();
|
||||
console.log('✓ Инициализация:', initResult);
|
||||
|
||||
// Статус
|
||||
const status = telegramBot.getStatus ? telegramBot.getStatus() : {
|
||||
isInitialized: telegramBot.isInitialized,
|
||||
status: telegramBot.status
|
||||
};
|
||||
console.log('✓ Статус:', status);
|
||||
|
||||
console.log('✓ Telegram Bot тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Telegram Bot:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест Email Bot
|
||||
*/
|
||||
async function testEmailBot() {
|
||||
console.log('\n=== ТЕСТ EMAIL BOT ===');
|
||||
|
||||
try {
|
||||
const emailBot = new EmailBot();
|
||||
|
||||
// Инициализация
|
||||
const initResult = await emailBot.initialize();
|
||||
console.log('✓ Инициализация:', initResult);
|
||||
|
||||
// Статус
|
||||
const status = emailBot.getStatus ? emailBot.getStatus() : {
|
||||
isInitialized: emailBot.isInitialized,
|
||||
status: emailBot.status
|
||||
};
|
||||
console.log('✓ Статус:', status);
|
||||
|
||||
console.log('✓ Email Bot тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Email Bot:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест Bot Manager
|
||||
*/
|
||||
async function testBotManager() {
|
||||
console.log('\n=== ТЕСТ BOT MANAGER ===');
|
||||
|
||||
try {
|
||||
// Инициализация
|
||||
await botManager.initialize();
|
||||
console.log('✓ BotManager инициализирован');
|
||||
|
||||
// Проверка готовности
|
||||
const isReady = botManager.isReady();
|
||||
console.log('✓ isReady:', isReady);
|
||||
|
||||
// Получение статуса
|
||||
const status = botManager.getStatus();
|
||||
console.log('✓ Статус всех ботов:', status);
|
||||
|
||||
// Получение конкретных ботов
|
||||
const webBot = botManager.getBot('web');
|
||||
const telegramBot = botManager.getBot('telegram');
|
||||
const emailBot = botManager.getBot('email');
|
||||
|
||||
console.log('✓ Web Bot:', webBot ? 'OK' : 'NOT FOUND');
|
||||
console.log('✓ Telegram Bot:', telegramBot ? 'OK' : 'NOT FOUND');
|
||||
console.log('✓ Email Bot:', emailBot ? 'OK' : 'NOT FOUND');
|
||||
|
||||
console.log('✓ Bot Manager тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Bot Manager:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить все тесты
|
||||
*/
|
||||
async function runAllTests() {
|
||||
console.log('╔═══════════════════════════════════════╗');
|
||||
console.log('║ ТЕСТИРОВАНИЕ НОВОЙ АРХИТЕКТУРЫ БОТОВ ║');
|
||||
console.log('╚═══════════════════════════════════════╝');
|
||||
|
||||
const results = {
|
||||
webBot: false,
|
||||
telegramBot: false,
|
||||
emailBot: false,
|
||||
botManager: false
|
||||
};
|
||||
|
||||
try {
|
||||
// Тестируем каждый компонент
|
||||
results.webBot = await testWebBot();
|
||||
results.telegramBot = await testTelegramBot();
|
||||
results.emailBot = await testEmailBot();
|
||||
results.botManager = await testBotManager();
|
||||
|
||||
// Итоги
|
||||
console.log('\n╔═══════════════════════════════════════╗');
|
||||
console.log('║ ИТОГИ ТЕСТИРОВАНИЯ ║');
|
||||
console.log('╚═══════════════════════════════════════╝');
|
||||
console.log('Web Bot: ', results.webBot ? '✓ PASS' : '✗ FAIL');
|
||||
console.log('Telegram Bot: ', results.telegramBot ? '✓ PASS' : '✗ FAIL');
|
||||
console.log('Email Bot: ', results.emailBot ? '✓ PASS' : '✗ FAIL');
|
||||
console.log('Bot Manager: ', results.botManager ? '✓ PASS' : '✗ FAIL');
|
||||
|
||||
const allPassed = Object.values(results).every(r => r === true);
|
||||
console.log('\nОБЩИЙ РЕЗУЛЬТАТ:', allPassed ? '✓ ВСЕ ТЕСТЫ ПРОЙДЕНЫ' : '✗ ЕСТЬ ОШИБКИ');
|
||||
|
||||
return allPassed;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[TestNewBots] Критическая ошибка:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запущен напрямую как скрипт
|
||||
if (require.main === module) {
|
||||
runAllTests()
|
||||
.then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('КРИТИЧЕСКАЯ ОШИБКА:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testWebBot,
|
||||
testTelegramBot,
|
||||
testEmailBot,
|
||||
testBotManager,
|
||||
runAllTests
|
||||
};
|
||||
|
||||
@@ -22,18 +22,8 @@ async function getUserTokenBalances(address) {
|
||||
if (!address) return [];
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем токены и RPC с расшифровкой
|
||||
const tokensResult = await db.getQuery()(
|
||||
|
||||
293
backend/services/unifiedMessageProcessor.js
Normal file
293
backend/services/unifiedMessageProcessor.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const conversationService = require('./conversationService');
|
||||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||||
|
||||
/**
|
||||
* Унифицированный процессор сообщений для всех каналов
|
||||
* Обрабатывает сообщения из web, telegram, email
|
||||
*/
|
||||
|
||||
/**
|
||||
* Обработать сообщение от пользователя
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @param {number} messageData.userId - ID пользователя
|
||||
* @param {string} messageData.content - Текст сообщения
|
||||
* @param {string} messageData.channel - Канал (web/telegram/email)
|
||||
* @param {Array} messageData.attachments - Вложения
|
||||
* @param {number} messageData.conversationId - ID беседы (опционально)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function processMessage(messageData) {
|
||||
try {
|
||||
const {
|
||||
userId,
|
||||
content,
|
||||
channel = 'web',
|
||||
attachments = [],
|
||||
conversationId: inputConversationId,
|
||||
guestId
|
||||
} = messageData;
|
||||
|
||||
logger.info('[UnifiedMessageProcessor] Обработка сообщения:', {
|
||||
userId,
|
||||
channel,
|
||||
contentLength: content?.length,
|
||||
hasAttachments: attachments.length > 0
|
||||
});
|
||||
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// 1. Получаем или создаем беседу
|
||||
let conversation;
|
||||
if (inputConversationId) {
|
||||
conversation = await conversationService.getConversationById(inputConversationId);
|
||||
}
|
||||
|
||||
if (!conversation && userId) {
|
||||
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
|
||||
}
|
||||
|
||||
const conversationId = conversation?.id || null;
|
||||
|
||||
// 2. Сохраняем входящее сообщение пользователя
|
||||
let userMessage;
|
||||
|
||||
// Обработка вложений
|
||||
let attachment_filename = null;
|
||||
let attachment_mimetype = null;
|
||||
let attachment_size = null;
|
||||
let attachment_data = null;
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
const firstAttachment = attachments[0];
|
||||
attachment_filename = firstAttachment.filename;
|
||||
attachment_mimetype = firstAttachment.mimetype;
|
||||
attachment_size = firstAttachment.size;
|
||||
attachment_data = firstAttachment.data;
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
user_id,
|
||||
conversation_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
attachment_filename_encrypted,
|
||||
attachment_mimetype_encrypted,
|
||||
attachment_size,
|
||||
attachment_data,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1, $2,
|
||||
encrypt_text($3, $12),
|
||||
encrypt_text($4, $12),
|
||||
encrypt_text($5, $12),
|
||||
encrypt_text($6, $12),
|
||||
encrypt_text($7, $12),
|
||||
encrypt_text($8, $12),
|
||||
encrypt_text($9, $12),
|
||||
$10, $11,
|
||||
NOW()
|
||||
) RETURNING id`,
|
||||
[
|
||||
userId,
|
||||
conversationId,
|
||||
'user',
|
||||
content,
|
||||
channel,
|
||||
'user',
|
||||
'incoming',
|
||||
attachment_filename,
|
||||
attachment_mimetype,
|
||||
attachment_size,
|
||||
attachment_data,
|
||||
encryptionKey
|
||||
]
|
||||
);
|
||||
|
||||
userMessage = rows[0];
|
||||
logger.info('[UnifiedMessageProcessor] Сообщение пользователя сохранено:', userMessage.id);
|
||||
}
|
||||
|
||||
// 3. Получаем историю беседы для контекста
|
||||
let conversationHistory = [];
|
||||
if (conversationId && userId) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
decrypt_text(role_encrypted, $2) as role,
|
||||
decrypt_text(content_encrypted, $2) as content,
|
||||
created_at
|
||||
FROM messages
|
||||
WHERE conversation_id = $1 AND user_id = $3
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 20`,
|
||||
[conversationId, encryptionKey, userId]
|
||||
);
|
||||
|
||||
conversationHistory = rows.map(row => ({
|
||||
role: row.role,
|
||||
content: row.content
|
||||
}));
|
||||
}
|
||||
|
||||
// 4. Генерируем AI ответ
|
||||
logger.info('[UnifiedMessageProcessor] Генерация AI ответа...');
|
||||
|
||||
const aiResponse = await aiAssistant.generateResponse({
|
||||
channel,
|
||||
messageId: userMessage?.id || `guest_${Date.now()}`,
|
||||
userId: userId || guestId,
|
||||
userQuestion: content,
|
||||
conversationHistory,
|
||||
conversationId,
|
||||
metadata: {
|
||||
hasAttachments: attachments.length > 0,
|
||||
channel
|
||||
}
|
||||
});
|
||||
|
||||
if (!aiResponse || !aiResponse.success) {
|
||||
logger.warn('[UnifiedMessageProcessor] AI не вернул ответ или ошибка:', aiResponse?.reason);
|
||||
|
||||
// Возвращаем результат без AI ответа
|
||||
return {
|
||||
success: true,
|
||||
userMessageId: userMessage?.id,
|
||||
conversationId,
|
||||
noAiResponse: true,
|
||||
reason: aiResponse?.reason
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Сохраняем ответ AI
|
||||
if (userId && aiResponse.response) {
|
||||
const { rows: aiMessageRows } = await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
user_id,
|
||||
conversation_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1, $2,
|
||||
encrypt_text($3, $8),
|
||||
encrypt_text($4, $8),
|
||||
encrypt_text($5, $8),
|
||||
encrypt_text($6, $8),
|
||||
encrypt_text($7, $8),
|
||||
NOW()
|
||||
) RETURNING id`,
|
||||
[
|
||||
userId,
|
||||
conversationId,
|
||||
'assistant',
|
||||
aiResponse.response,
|
||||
channel,
|
||||
'assistant',
|
||||
'outgoing',
|
||||
encryptionKey
|
||||
]
|
||||
);
|
||||
|
||||
logger.info('[UnifiedMessageProcessor] Ответ AI сохранен:', aiMessageRows[0].id);
|
||||
|
||||
// 6. Обновляем время беседы
|
||||
if (conversationId) {
|
||||
await conversationService.touchConversation(conversationId);
|
||||
}
|
||||
|
||||
// 7. Отправляем уведомление через WebSocket
|
||||
try {
|
||||
broadcastMessagesUpdate(userId);
|
||||
} catch (wsError) {
|
||||
logger.warn('[UnifiedMessageProcessor] Ошибка отправки WebSocket:', wsError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Возвращаем результат
|
||||
return {
|
||||
success: true,
|
||||
userMessageId: userMessage?.id,
|
||||
conversationId,
|
||||
aiResponse: {
|
||||
response: aiResponse.response,
|
||||
ragData: aiResponse.ragData
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[UnifiedMessageProcessor] Ошибка обработки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработать сообщение от гостя
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function processGuestMessage(messageData) {
|
||||
try {
|
||||
const guestService = require('./guestService');
|
||||
|
||||
// Создаем guest ID если нет
|
||||
const guestId = messageData.guestId || guestService.createGuestId();
|
||||
|
||||
// Сохраняем гостевое сообщение
|
||||
await guestService.saveGuestMessage({
|
||||
guestId,
|
||||
content: messageData.content,
|
||||
channel: messageData.channel || 'web'
|
||||
});
|
||||
|
||||
// Генерируем AI ответ для гостя (без сохранения в messages)
|
||||
const aiResponse = await aiAssistant.generateResponse({
|
||||
channel: messageData.channel || 'web',
|
||||
messageId: `guest_${guestId}_${Date.now()}`,
|
||||
userId: guestId,
|
||||
userQuestion: messageData.content,
|
||||
conversationHistory: [],
|
||||
metadata: { isGuest: true }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
guestId,
|
||||
aiResponse: aiResponse?.success ? {
|
||||
response: aiResponse.response
|
||||
} : null
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[UnifiedMessageProcessor] Ошибка обработки гостевого сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processMessage,
|
||||
processGuestMessage
|
||||
};
|
||||
|
||||
@@ -33,6 +33,14 @@ async function deleteUserById(userId) {
|
||||
);
|
||||
console.log('[DELETE] Удалено messages:', resMessages.rows.length);
|
||||
|
||||
// 2.1. Удаляем хеши дедупликации
|
||||
console.log('[DELETE] Начинаем удаление message_deduplication для userId:', userId);
|
||||
const resDeduplication = await db.getQuery()(
|
||||
'DELETE FROM message_deduplication WHERE user_id = $1 RETURNING id',
|
||||
[userId]
|
||||
);
|
||||
console.log('[DELETE] Удалено deduplication hashes:', resDeduplication.rows.length);
|
||||
|
||||
// 3. Удаляем conversations
|
||||
console.log('[DELETE] Начинаем удаление conversations для userId:', userId);
|
||||
const resConversations = await db.getQuery()(
|
||||
|
||||
128
backend/services/webBot.js
Normal file
128
backend/services/webBot.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const unifiedMessageProcessor = require('./unifiedMessageProcessor');
|
||||
|
||||
/**
|
||||
* WebBot - обработчик веб-чата
|
||||
* Простой бот для веб-интерфейса, всегда активен
|
||||
*/
|
||||
class WebBot {
|
||||
constructor() {
|
||||
this.name = 'WebBot';
|
||||
this.channel = 'web';
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация Web Bot
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
logger.info('[WebBot] 🚀 Инициализация Web Bot...');
|
||||
|
||||
// Web bot всегда готов к работе
|
||||
this.isInitialized = true;
|
||||
this.status = 'active';
|
||||
|
||||
logger.info('[WebBot] ✅ Web Bot успешно инициализирован');
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] ❌ Ошибка инициализации:', error);
|
||||
this.status = 'error';
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка сообщения из веб-чата
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async processMessage(messageData) {
|
||||
try {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('WebBot is not initialized');
|
||||
}
|
||||
|
||||
// Устанавливаем канал
|
||||
messageData.channel = 'web';
|
||||
|
||||
// Обрабатываем через unified processor
|
||||
return await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] Ошибка обработки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка сообщения в веб-чат
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} message - Текст сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async sendMessage(userId, message) {
|
||||
try {
|
||||
logger.info('[WebBot] Отправка сообщения пользователю:', userId);
|
||||
|
||||
// Для веб-чата отправка происходит через WebSocket
|
||||
// Здесь просто возвращаем успех, реальная отправка через wsHub
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId,
|
||||
message,
|
||||
channel: 'web'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] Ошибка отправки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статус бота
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
name: this.name,
|
||||
channel: this.channel,
|
||||
isInitialized: this.isInitialized,
|
||||
status: this.status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка бота
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
logger.info('[WebBot] Остановка Web Bot...');
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
logger.info('[WebBot] ✅ Web Bot остановлен');
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] Ошибка остановки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebBot;
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
// Роли пользователей
|
||||
const USER_ROLES = {
|
||||
USER: 1,
|
||||
ADMIN: 2,
|
||||
};
|
||||
|
||||
// Типы идентификаторов
|
||||
const IDENTITY_TYPES = {
|
||||
WALLET: 'wallet',
|
||||
@@ -30,13 +24,6 @@ const MESSAGE_CHANNELS = {
|
||||
EMAIL: 'email',
|
||||
};
|
||||
|
||||
// Типы отправителей сообщений
|
||||
const SENDER_TYPES = {
|
||||
USER: 'user',
|
||||
AI: 'ai',
|
||||
ADMIN: 'admin',
|
||||
};
|
||||
|
||||
// Коды ошибок
|
||||
const ERROR_CODES = {
|
||||
UNAUTHORIZED: 'unauthorized',
|
||||
@@ -59,12 +46,27 @@ const API_CONFIG = {
|
||||
TIMEOUT: 30000, // 30 секунд
|
||||
};
|
||||
|
||||
// Новые константы для ИИ-ассистента (без admin)
|
||||
const AI_USER_TYPES = {
|
||||
REGULAR_USER: 'user',
|
||||
EDITOR: 'editor',
|
||||
READONLY: 'readonly'
|
||||
};
|
||||
|
||||
const AI_SENDER_TYPES = {
|
||||
USER: 'user',
|
||||
EDITOR: 'editor',
|
||||
READONLY: 'readonly',
|
||||
ASSISTANT: 'assistant'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
USER_ROLES,
|
||||
IDENTITY_TYPES,
|
||||
MESSAGE_CHANNELS,
|
||||
SENDER_TYPES,
|
||||
ERROR_CODES,
|
||||
SESSION_CONFIG,
|
||||
API_CONFIG,
|
||||
// Константы для ИИ-ассистента
|
||||
AI_USER_TYPES,
|
||||
AI_SENDER_TYPES,
|
||||
};
|
||||
|
||||
89
backend/utils/encryptionUtils.js
Normal file
89
backend/utils/encryptionUtils.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
/**
|
||||
* Утилиты для работы с шифрованием
|
||||
* Предоставляет единую точку доступа к ключу шифрования
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('./logger');
|
||||
|
||||
// Кэш ключа шифрования
|
||||
let cachedKey = null;
|
||||
|
||||
/**
|
||||
* Получить ключ шифрования из файла или переменной окружения
|
||||
* @returns {string} Ключ шифрования
|
||||
*/
|
||||
function getEncryptionKey() {
|
||||
// Если ключ уже закэширован, возвращаем его
|
||||
if (cachedKey) {
|
||||
return cachedKey;
|
||||
}
|
||||
|
||||
// Сначала пробуем прочитать из файла (приоритет)
|
||||
// В Docker контейнере путь /app/ssl/keys/full_db_encryption.key
|
||||
// В локальной разработке ../../ssl/keys/full_db_encryption.key
|
||||
const keyPath = fs.existsSync('/app/ssl/keys/full_db_encryption.key')
|
||||
? '/app/ssl/keys/full_db_encryption.key'
|
||||
: path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
|
||||
|
||||
if (fs.existsSync(keyPath)) {
|
||||
try {
|
||||
cachedKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
logger.info('[EncryptionUtils] Ключ шифрования загружен из файла');
|
||||
return cachedKey;
|
||||
} catch (error) {
|
||||
logger.error('[EncryptionUtils] Ошибка чтения ключа из файла:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Если файла нет, пробуем переменную окружения
|
||||
if (process.env.ENCRYPTION_KEY) {
|
||||
cachedKey = process.env.ENCRYPTION_KEY;
|
||||
logger.info('[EncryptionUtils] Ключ шифрования загружен из переменной окружения');
|
||||
return cachedKey;
|
||||
}
|
||||
|
||||
// Если ничего не найдено, бросаем ошибку
|
||||
logger.error('[EncryptionUtils] Ключ шифрования не найден ни в файле, ни в переменной окружения!');
|
||||
throw new Error('Encryption key not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, включено ли шифрование
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEnabled() {
|
||||
try {
|
||||
getEncryptionKey();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить кэш ключа (для тестов)
|
||||
*/
|
||||
function clearCache() {
|
||||
cachedKey = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEncryptionKey,
|
||||
isEnabled,
|
||||
clearCache
|
||||
};
|
||||
|
||||
@@ -34,18 +34,8 @@ function generateVerificationCode(length = 6) {
|
||||
// Проверка существования идентификатора пользователя
|
||||
async function checkUserIdentity(userId, provider, providerId) {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
const encryptionUtils = require('./encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const result = await db.getQuery()(
|
||||
'SELECT * FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $4) AND provider_id_encrypted = encrypt_text($3, $4)',
|
||||
|
||||
@@ -2,7 +2,7 @@ const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'warn', // Изменено с 'info' на 'warn'
|
||||
level: process.env.LOG_LEVEL || 'info', // Уровень по умолчанию 'info' для показа логов ботов
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
|
||||
@@ -74,6 +74,12 @@ function initWSS(server) {
|
||||
timestamp: data.timestamp
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.type === 'ollama_ready') {
|
||||
// Уведомление о готовности Ollama - запускаем инициализацию ботов
|
||||
console.log('🚀 [WebSocket] Получено уведомление о готовности Ollama!');
|
||||
handleOllamaReady();
|
||||
}
|
||||
|
||||
if (data.type === 'request_token_balances' && data.address) {
|
||||
// Запрос балансов токенов
|
||||
@@ -577,4 +583,42 @@ async function handleTokenBalancesRequest(ws, address, userId) {
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка уведомления о готовности Ollama
|
||||
*/
|
||||
async function handleOllamaReady() {
|
||||
try {
|
||||
console.log('✅ [WebSocket] Ollama готов к работе');
|
||||
// Уведомляем всех подключенных клиентов о готовности системы
|
||||
broadcastSystemReady();
|
||||
} catch (error) {
|
||||
console.error('❌ [WebSocket] Ошибка обработки Ollama ready:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление всех клиентов о готовности системы
|
||||
*/
|
||||
function broadcastSystemReady() {
|
||||
const message = JSON.stringify({
|
||||
type: 'system_ready',
|
||||
data: {
|
||||
message: 'Все модели загружены! Система готова к работе.',
|
||||
timestamp: Date.now(),
|
||||
bots: ['web', 'telegram', 'email']
|
||||
}
|
||||
});
|
||||
|
||||
// Отправляем всем подключенным клиентам
|
||||
wsClients.forEach((clientSet) => {
|
||||
clientSet.forEach((ws) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('📢 [WebSocket] Уведомление о готовности системы отправлено всем клиентам');
|
||||
}
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
|
||||
# SSH Key Server для безопасной передачи ключей
|
||||
ssh-key-server:
|
||||
image: node:20-alpine
|
||||
image: node:20-slim
|
||||
container_name: dapp-ssh-key-server
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@@ -197,7 +197,7 @@ services:
|
||||
- '3001:3001'
|
||||
command: node /app/ssh-key-server.js
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/ssh-key"]
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/ssh-key', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# This software is proprietary and confidential.
|
||||
# For licensing inquiries: info@hb3-accelerator.com
|
||||
|
||||
FROM node:20-alpine
|
||||
FROM node:20-slim
|
||||
|
||||
# Добавляем метки для авторских прав
|
||||
LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>"
|
||||
@@ -18,8 +18,12 @@ LABEL website="https://hb3-accelerator.com"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем дополнительные зависимости
|
||||
RUN apk add --no-cache python3 make g++
|
||||
# Устанавливаем системные зависимости для компиляции нативных модулей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Копируем package.json и yarn.lock для установки зависимостей
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
@@ -23,8 +23,8 @@ RUN apk add --no-cache curl
|
||||
# Копируем собранный frontend из первого этапа
|
||||
COPY --from=frontend-builder /app/dist/ /usr/share/nginx/html/
|
||||
|
||||
# Копируем конфигурацию nginx
|
||||
COPY nginx-simple.conf /etc/nginx/nginx.conf.template
|
||||
# Копируем конфигурацию nginx (используем dev версию для локальной разработки)
|
||||
COPY nginx-dev.conf /etc/nginx/nginx.conf.template
|
||||
|
||||
# Копируем скрипт запуска
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
@@ -491,7 +491,12 @@ async function handleAiReply() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка генерации ответа ИИ:', e);
|
||||
alert('Ошибка генерации ответа ИИ');
|
||||
// Используем более дружелюбное уведомление вместо alert
|
||||
emit('error', {
|
||||
type: 'ai-generation-error',
|
||||
message: 'Не удалось сгенерировать ответ ИИ. Попробуйте еще раз.',
|
||||
details: e.message
|
||||
});
|
||||
} finally {
|
||||
isAiLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ async function handleSendMessage({ message, attachments }) {
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert('Пользователь заблокирован. Отправка сообщений невозможна.', 'Ошибка', { type: 'error' });
|
||||
} else {
|
||||
alert('Пользователь заблокирован. Отправка сообщений невозможна.');
|
||||
console.error('Пользователь заблокирован. Отправка сообщений невозможна.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -441,7 +441,7 @@ async function handleSendMessage({ message, attachments }) {
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.', 'Ошибка', { type: 'warning' });
|
||||
} else {
|
||||
alert('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.');
|
||||
console.error('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -464,14 +464,14 @@ async function handleSendMessage({ message, attachments }) {
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert(resultText, 'Результат рассылки', { type: 'info' });
|
||||
} else {
|
||||
alert(resultText);
|
||||
console.log('Результат рассылки:', resultText);
|
||||
}
|
||||
await loadMessages();
|
||||
} catch (e) {
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert('Ошибка отправки: ' + (e?.response?.data?.error || e?.message || e), 'Ошибка', { type: 'error' });
|
||||
} else {
|
||||
alert('Ошибка отправки: ' + (e?.response?.data?.error || e?.message || e));
|
||||
console.error('Ошибка отправки:', e?.response?.data?.error || e?.message || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
FROM node:20-alpine
|
||||
FROM node:20-slim
|
||||
|
||||
# Устанавливаем необходимые пакеты
|
||||
RUN apk update && apk add --no-cache \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openssh-client \
|
||||
sshpass \
|
||||
curl \
|
||||
wget \
|
||||
docker-cli \
|
||||
ca-certificates
|
||||
docker.io \
|
||||
ca-certificates \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Создаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
Reference in New Issue
Block a user