diff --git a/Dockerfile b/Dockerfile index beb7b50..2a33324 100644 --- a/Dockerfile +++ b/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"] \ No newline at end of file diff --git a/aidocs/AI_DATABASE_STRUCTURE.md b/aidocs/AI_DATABASE_STRUCTURE.md new file mode 100644 index 0000000..713db04 --- /dev/null +++ b/aidocs/AI_DATABASE_STRUCTURE.md @@ -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 +**Статус:** ✅ ПРОВЕРКА ЗАВЕРШЕНА + КРИТИЧНЫЕ ПРОБЛЕМЫ ИСПРАВЛЕНЫ + diff --git a/aidocs/AI_FILES_QUICK_REFERENCE.md b/aidocs/AI_FILES_QUICK_REFERENCE.md new file mode 100644 index 0000000..3c352c7 --- /dev/null +++ b/aidocs/AI_FILES_QUICK_REFERENCE.md @@ -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 + diff --git a/aidocs/AI_FULL_INVENTORY.md b/aidocs/AI_FULL_INVENTORY.md new file mode 100644 index 0000000..cb3eda7 --- /dev/null +++ b/aidocs/AI_FULL_INVENTORY.md @@ -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 + систематическая проверка + diff --git a/backend/Dockerfile b/backend/Dockerfile index 95c41ac..8ebcf1c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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="Тарабанов Александр Викторович " @@ -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 diff --git a/backend/app.js b/backend/app.js index 3945b66..fb5414c 100644 --- a/backend/app.js +++ b/backend/app.js @@ -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'); diff --git a/backend/db.js b/backend/db.js index a176edb..55fc318 100644 --- a/backend/db.js +++ b/backend/db.js @@ -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) diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 5465d6b..a43b99b 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -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`); // Убрано diff --git a/backend/nodemon.json b/backend/nodemon.json index 552b084..94a2f79 100644 --- a/backend/nodemon.json +++ b/backend/nodemon.json @@ -2,7 +2,8 @@ "watch": [ "backend/src", "backend/routes", - "backend/services" + "backend/services", + "server.js" ], "ignore": [ "backend/artifacts/**", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 680fb28..90dfae4 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -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()( diff --git a/backend/routes/chat.js b/backend/routes/chat.js index efe5264..9454788 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -14,880 +14,91 @@ const express = require('express'); const router = express.Router(); const multer = require('multer'); const aiAssistant = require('../services/ai-assistant'); -const aiQueueService = require('../services/ai-queue'); // Добавляем импорт AI Queue сервиса const db = require('../db'); -const encryptedDb = require('../services/encryptedDatabaseService'); const logger = require('../utils/logger'); -const { requireAuth } = require('../middleware/auth'); -const crypto = require('crypto'); +const { requireAuth, requireAdmin } = require('../middleware/auth'); const aiAssistantSettingsService = require('../services/aiAssistantSettingsService'); const aiAssistantRulesService = require('../services/aiAssistantRulesService'); -const { isUserBlocked } = require('../utils/userUtils'); -const { broadcastChatMessage, broadcastConversationUpdate } = require('../wsHub'); +const botManager = require('../services/botManager'); // Настройка multer для обработки файлов в памяти const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); -// Функция для обработки гостевых сообщений после аутентификации -async function processGuestMessages(userId, guestId) { - try { - logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`); - - // Получаем ключ шифрования - 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 mappingCheck = await db.getQuery()( - 'SELECT processed FROM guest_user_mapping WHERE guest_id_encrypted = encrypt_text($1, $2)', - [guestId, encryptionKey] - ); - - // Если сообщения уже обработаны, пропускаем - if (mappingCheck.rows.length > 0 && mappingCheck.rows[0].processed) { - logger.info(`Guest messages for guest ID ${guestId} were already processed.`); - return { success: true, message: 'Guest messages already processed' }; - } - - // Проверяем наличие mapping записи и создаем если нет - if (mappingCheck.rows.length === 0) { - 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', - [userId, guestId, encryptionKey] - ); - logger.info(`Created mapping for guest ID ${guestId} to user ${userId}`); - } - - // Получаем все гостевые сообщения со всеми новыми полями - const guestMessagesResult = await db.getQuery()( - `SELECT - id, decrypt_text(guest_id_encrypted, $2) as guest_id, decrypt_text(content_encrypted, $2) as content, decrypt_text(language_encrypted, $2) as language, is_ai, 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 guest_messages WHERE guest_id_encrypted = encrypt_text($1, $2) ORDER BY created_at ASC`, - [guestId, encryptionKey] - ); - - if (guestMessagesResult.rows.length === 0) { - logger.info(`No guest messages found for guest ID ${guestId}`); - const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]); - if (checkResult.rows.length > 0) { - await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]); - logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`); - } else { - logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`); - } - return { success: true, message: 'No guest messages found' }; - } - - const guestMessages = guestMessagesResult.rows; - logger.info(`Found ${guestMessages.length} guest messages for guest ID ${guestId}`); - - // --- Новый порядок: ищем последний диалог пользователя --- - let conversation = null; - const lastConvResult = 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', - [userId, encryptionKey] - ); - if (lastConvResult.rows.length > 0) { - conversation = lastConvResult.rows[0]; - } else { - // Если нет ни одного диалога, создаём новый - const firstMessage = guestMessages[0]; - const title = firstMessage.content && firstMessage.content.trim() - ? (firstMessage.content.trim().length > 30 ? `${firstMessage.content.trim().substring(0, 30)}...` : firstMessage.content.trim()) - : (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог'); - const newConversationResult = await db.getQuery()( - 'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *', - [userId, title, encryptionKey] - ); - conversation = newConversationResult.rows[0]; - logger.info(`Created new conversation ${conversation.id} for guest messages`); - } - // --- КОНЕЦ блока поиска/создания диалога --- - - // Отслеживаем успешные сохранения сообщений - const savedMessageIds = []; - - // Обрабатываем каждое гостевое сообщение - for (const guestMessage of guestMessages) { - logger.info(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content || guestMessage.attachment_filename || '(empty)'}`); - try { - // Сохраняем сообщение пользователя в таблицу messages, включая данные файла - const userMessageResult = await db.getQuery()( - `INSERT INTO messages - (conversation_id, content_encrypted, sender_type_encrypted, role_encrypted, channel_encrypted, created_at, user_id, - attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data) - VALUES - ($1, encrypt_text($2, $9), encrypt_text('user', $9), encrypt_text('user', $9), encrypt_text('web', $9), $3, $4, - encrypt_text($5, $9), encrypt_text($6, $9), $7, $8) - RETURNING *`, - [ - conversation.id, - guestMessage.content, // Текст (может быть NULL) - guestMessage.created_at, - userId, - guestMessage.attachment_filename, // Метаданные и данные файла - guestMessage.attachment_mimetype, - guestMessage.attachment_size, - guestMessage.attachment_data, // BYTEA - encryptionKey - ] - ); - const savedUserMessage = userMessageResult.rows[0]; - logger.info(`Saved user message with ID ${savedUserMessage.id}`); - savedMessageIds.push(guestMessage.id); - // --- Генерируем ответ ИИ на гостевое сообщение, если это текст --- - if (guestMessage.content) { - // Проверяем, что на это сообщение ещё нет ответа ассистента - const aiReplyExists = await db.getQuery()( - `SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type_encrypted = encrypt_text('assistant', $3) AND created_at > $2 LIMIT 1`, - [conversation.id, guestMessage.created_at, encryptionKey] - ); - if (!aiReplyExists.rows.length) { - try { - // Получаем настройки ассистента - const aiSettings = await aiAssistantSettingsService.getSettings(); - let rules = null; - if (aiSettings && aiSettings.rules_id) { - rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id); - } - // Получаем историю сообщений до этого guestMessage (до created_at) - const historyResult = await db.getQuery()( - 'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10', - [conversation.id, guestMessage.created_at, encryptionKey] - ); - const history = historyResult.rows.reverse().map(msg => ({ - role: msg.sender_type === 'user' ? 'user' : 'assistant', - content: msg.content - })); - logger.info('Getting AI response for guest message:', guestMessage.content); - const aiResponseContent = await aiAssistant.getResponse( - guestMessage.content, - history, - aiSettings ? aiSettings.system_prompt : '', - rules ? rules.rules : null - ); - logger.info('AI response for guest message received' + (aiResponseContent ? '' : ' (empty)'), { conversationId: conversation.id }); - if (aiResponseContent) { - await db.getQuery()( - `INSERT INTO messages - (conversation_id, user_id, content_encrypted, sender_type_encrypted, role_encrypted, channel_encrypted) - VALUES ($1, $2, encrypt_text($3, $4), encrypt_text('assistant', $4), encrypt_text('assistant', $4), encrypt_text('web', $4))`, - [conversation.id, userId, aiResponseContent, encryptionKey] - ); - logger.info('AI response for guest message saved', { conversationId: conversation.id }); - } - } catch (aiError) { - logger.error('Error getting or saving AI response for guest message:', aiError); - } - } - } - // --- конец блока генерации ответа ИИ --- - } catch (error) { - logger.error(`Error processing guest message ${guestMessage.id}: ${error.message}`, { stack: error.stack }); - // Продолжаем с другими сообщениями в случае ошибки - } - } - - // Удаляем только успешно обработанные гостевые сообщения - if (savedMessageIds.length > 0) { - await db.getQuery()('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]); - logger.info( - `Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}` - ); - - // Помечаем гостевой ID как обработанный - await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [ - guestId, encryptionKey - ]); - logger.info(`Marked guest mapping as processed for guest ID ${guestId}`); - } else { - logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`); - // Если не было успешных, все равно пометим как обработанные, чтобы не пытаться снова - await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]); - logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`); - } - - return { - success: true, - message: `Processed ${savedMessageIds.length} of ${guestMessages.length} guest messages`, - conversationId: conversation.id, - }; - } catch (error) { - logger.error(`Error in processGuestMessages for guest ID ${guestId}: ${error.message}`, { stack: error.stack }); - // Не пробрасываем ошибку дальше, чтобы не прерывать основной поток, но логируем ее - return { success: false, error: 'Internal error during guest message processing' }; - } -} +// Функция processGuestMessages перенесена в services/guestMessageService.js // Обработчик для гостевых сообщений router.post('/guest-message', upload.array('attachments'), async (req, res) => { - // Логируем полученные данные - logger.info('Received /guest-message request'); - logger.debug('Request Body:', req.body); - logger.debug('Request Files:', req.files); // Файлы будут здесь - - // Получаем ключ шифрования - 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 { - // Извлекаем данные из req.body (текстовые поля) - const { message, language, guestId: requestGuestId } = req.body; - const files = req.files; // Файлы извлекаем из req.files - const file = files && files.length > 0 ? files[0] : null; // Берем только первый файл - - // Валидация: должно быть либо сообщение, либо файл - if (!message && !file) { - logger.warn('Guest message attempt without content or file.', { guestId: requestGuestId }); - return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' }); - } - // Запрещаем и текст, и файл одновременно (согласно новым требованиям) - if (message && file) { - logger.warn('Guest message attempt with both text and file.', { guestId: requestGuestId }); - return res.status(400).json({ success: false, error: 'Нельзя отправить текст и файл одновременно.' }); - } - - // Используем гостевой ID из запроса или из сессии, или генерируем новый - const guestId = requestGuestId || req.session.guestId || crypto.randomBytes(16).toString('hex'); - - // Сохраняем/обновляем ID гостя в сессии - if (req.session.guestId !== guestId) { - req.session.guestId = guestId; - } - - // Подготавливаем данные для вставки - const messageContent = message && message.trim() ? message.trim() : null; // Текст или NULL, если пустой - const attachmentFilename = file ? file.originalname : null; - const attachmentMimetype = file ? file.mimetype : null; - const attachmentSize = file ? file.size : null; - const attachmentData = file ? file.buffer : null; // Сам буфер файла - - // Проверяем, что есть контент для сохранения - if (!messageContent && !attachmentData) { - logger.warn('Guest message attempt without content or file'); - return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' }); - } - - logger.info('Saving guest message:', { - guestId, - message: messageContent, - file: attachmentFilename, - mimetype: attachmentMimetype, - size: attachmentSize - }); - - // Сохраняем сообщение пользователя с текстом или файлом - const result = await db.getQuery()( - `INSERT INTO guest_messages - (guest_id_encrypted, content_encrypted, language_encrypted, is_ai, - attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data) - VALUES (encrypt_text($1, $8), encrypt_text($2, $8), encrypt_text($3, $8), false, encrypt_text($4, $8), encrypt_text($5, $8), $6, $7) RETURNING id`, - [ - guestId, - messageContent || '', // Текст сообщения или пустая строка - 'ru', // Устанавливаем русский язык по умолчанию - attachmentFilename || '', // Имя файла или пустая строка - attachmentMimetype || '', // MIME тип или пустая строка - attachmentSize || null, - attachmentData || null, // BYTEA данные файла или NULL - encryptionKey - ] - ); - - const savedMessageId = result.rows[0].id; - logger.info('Guest message saved with ID:', savedMessageId); - - // Сохраняем сессию после успешной операции с БД - try { - await new Promise((resolve, reject) => { - req.session.save((err) => { - if (err) return reject(err); - resolve(); - }); + // Проверяем готовность системы + if (!botManager.isReady()) { + return res.status(503).json({ + success: false, + error: 'Система ботов не готова. Попробуйте позже.' }); - logger.info('Session saved after guest message'); - } catch (sessionError) { - logger.error('Error saving session after guest message:', sessionError); - // Не прерываем ответ пользователя из-за ошибки сессии } - // ВАЖНО: до авторизации ИИ-ответы гостям не отправляем. Только сохраняем гостевое сообщение и возвращаем системный текст. - let aiResponseContent = null; - - // Получаем настройки ассистента для systemMessage - let telegramBotUrl = null; - let supportEmailAddr = null; - try { - const aiSettings = await aiAssistantSettingsService.getSettings(); - if (aiSettings && aiSettings.telegramBot && aiSettings.telegramBot.bot_username) { - telegramBotUrl = `https://t.me/${aiSettings.telegramBot.bot_username}`; - } - if (aiSettings && aiSettings.supportEmail && aiSettings.supportEmail.from_email) { - supportEmailAddr = aiSettings.supportEmail.from_email; - } - } catch (e) { - logger.error('Ошибка получения настроек ассистента для systemMessage:', e); + // Получаем WebBot + const webBot = botManager.getBot('web'); + if (!webBot || !webBot.isInitialized) { + return res.status(503).json({ + success: false, + error: 'Web Bot не инициализирован' + }); } - res.json({ - success: true, - messageId: savedMessageId, // Возвращаем ID сохраненного сообщения - guestId: guestId, // Возвращаем использованный guestId - aiResponse: aiResponseContent, // Возвращаем AI ответ - systemMessage: 'Для продолжения диалога авторизуйтесь: подключите кошелек, перейдите в чат-бот Telegram или отправьте письмо на email.', - telegramBotUrl, - supportEmail: supportEmailAddr + // Обрабатываем сообщение через новую архитектуру + await webBot.handleMessage(req, res, async (messageData) => { + return await botManager.processMessage(messageData); }); + } catch (error) { - logger.error('Error saving guest message:', error); - res.status(500).json({ success: false, error: 'Ошибка сохранения гостевого сообщения' }); + logger.error('[Chat] Ошибка обработки гостевого сообщения:', error); + res.status(500).json({ + success: false, + error: 'Внутренняя ошибка сервера' + }); } }); +// Старая логика удалена - используется guestService.js); + // Обработчик для сообщений аутентифицированных пользователей router.post('/message', requireAuth, upload.array('attachments'), async (req, res) => { - logger.info('Received /message request'); - logger.debug('Request Body:', req.body); - logger.debug('Request Files:', req.files); - - // Получаем ключ шифрования - 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 userId = req.session.userId; - const { message, language, conversationId: convIdFromRequest } = req.body; - const files = req.files; - const file = files && files.length > 0 ? files[0] : null; - - // Валидация: должно быть либо сообщение, либо файл - if (!message && !file) { - logger.warn('Authenticated message attempt without content or file.', { userId }); - return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' }); - } - // Запрещаем и текст, и файл одновременно - if (message && file) { - logger.warn('Authenticated message attempt with both text and file.', { userId }); - return res.status(400).json({ success: false, error: 'Нельзя отправить текст и файл одновременно.' }); - } - - let conversationId = convIdFromRequest; - let conversation = null; - - try { - // Найти или создать диалог - if (conversationId) { - let convResult; - if (req.session.isAdmin) { - // Админ может писать в любой диалог - convResult = await db.getQuery()( - 'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE id = $1', - [conversationId, encryptionKey] - ); - } else { - // Обычный пользователь — только в свой диалог - convResult = await db.getQuery()( - 'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $3) as title FROM conversations WHERE id = $1 AND user_id = $2', - [conversationId, userId, encryptionKey] - ); - } - if (convResult.rows.length === 0) { - logger.warn('Conversation not found or access denied', { conversationId, userId }); - return res.status(404).json({ success: false, error: 'Диалог не найден или доступ запрещен' }); - } - conversation = convResult.rows[0]; - } else { - // Ищем последний диалог пользователя - const lastConvResult = 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', - [userId, encryptionKey] - ); - if (lastConvResult.rows.length > 0) { - conversation = lastConvResult.rows[0]; - conversationId = conversation.id; - } else { - // Создаем новый диалог, если нет ни одного - const title = message && message.trim() - ? (message.trim().length > 50 ? `${message.trim().substring(0, 50)}...` : message.trim()) - : (file ? `Файл: ${file.originalname}` : 'Новый диалог'); - const newConvResult = await db.getQuery()( - 'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *', - [userId, title, encryptionKey] - ); - conversation = newConvResult.rows[0]; - conversationId = conversation.id; - logger.info('Created new conversation', { conversationId, userId }); - } - } - - // Подготавливаем данные для вставки сообщения пользователя - const messageContent = message && message.trim() ? message.trim() : null; // Текст или NULL, если пустой - const attachmentFilename = file ? file.originalname : null; - const attachmentMimetype = file ? file.mimetype : null; - const attachmentSize = file ? file.size : null; - const attachmentData = file ? file.buffer : null; - - // Определяем user_id для сообщения: всегда user_id диалога (контакта) - const recipientId = conversation.user_id; - // Определяем sender_type - let senderType = 'user'; - let role = 'user'; - if (req.session.isAdmin) { - senderType = 'admin'; - role = 'admin'; - } - - // Сохраняем сообщение через encryptedDb - const userMessage = await encryptedDb.saveData('messages', { - conversation_id: conversationId, - user_id: recipientId, // user_id контакта - content: messageContent, - sender_type: senderType, - role: role, - channel: 'web', - attachment_filename: attachmentFilename, - attachment_mimetype: attachmentMimetype, - attachment_size: attachmentSize, - attachment_data: attachmentData - }); - - // Проверяем, что сообщение было сохранено - if (!userMessage) { - logger.warn('Message not saved - all content was empty'); - return res.status(400).json({ error: 'Message content cannot be empty' }); - } - - logger.info('User message saved', { messageId: userMessage.id, conversationId }); - - if (await isUserBlocked(userId)) { - logger.info(`[Chat] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`); - return; - } - - // --- Новая логика автоответа ИИ по RAG --- - let aiMessage = null; - let shouldGenerateAiReply = true; - if (senderType === 'admin') { - // Если админ пишет не себе, не отвечаем - if (userId !== recipientId) { - shouldGenerateAiReply = false; - } - } - if (messageContent && shouldGenerateAiReply) { // Только для текстовых сообщений и если разрешено - try { - // Получаем настройки ассистента - const aiSettings = await aiAssistantSettingsService.getSettings(); - let rules = null; - if (aiSettings && aiSettings.rules_id) { - rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id); - } - // --- RAG автоответ с поддержкой беседы --- - // Пример работы: - // 1. Пользователь: "Как подключить кошелек?" - // RAG: находит точный ответ → возвращает его - // 2. Пользователь: "А какие документы нужны?" - // RAG: анализирует контекст предыдущего ответа → ищет информацию о документах - // 3. Пользователь: "Сколько это займет времени?" - // RAG: использует полный контекст беседы → дает уточненный ответ - 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; - } - let ragResult = null; - if (ragTableId) { - const { ragAnswerWithConversation, generateLLMResponse } = require('../services/ragService'); - const threshold = 10; // Жёстче порог совпадения, чтобы не подмешивать нерелевантный RAG - - // Получаем историю беседы - const historyResult = await db.getQuery()( - 'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10', - [conversationId, userMessage.id, encryptionKey] - ); - const history = historyResult.rows.reverse().map(msg => ({ - // Любые человеческие сообщения (user/admin) считаем role='user'. Только 'assistant' — ассистент - role: msg.sender_type === 'assistant' ? 'assistant' : 'user', - content: msg.content - })); - - logger.info(`[RAG] Запуск поиска по RAG с беседой: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}, historyLength=${history.length}`); - const ragSearchResult = await ragAnswerWithConversation({ - tableId: ragTableId, - userQuestion: messageContent, - threshold, - history, - conversationId, - // Не пересобираем индекс на каждом запросе. Кнопка /rebuild-index дергает rebuild. - forceReindex: false - }); - logger.info(`[RAG] Результат поиска по RAG:`, ragSearchResult); - logger.info(`[RAG] Score type: ${typeof ragSearchResult.score}, value: ${ragSearchResult.score}, threshold: ${threshold}, isFollowUp: ${ragSearchResult.isFollowUp}`); - const isConfident = ragSearchResult && typeof ragSearchResult.score === 'number' && Math.abs(ragSearchResult.score) <= threshold; - if (isConfident && ragSearchResult.answer) { - logger.info(`[RAG] Найден confident-ответ (score=${ragSearchResult.score}), отправляем ответ из базы.`); - // Прямой ответ из RAG - logger.info(`[RAG] Сохраняем AI сообщение с контентом: "${ragSearchResult.answer}"`); - aiMessage = await encryptedDb.saveData('messages', { - conversation_id: conversationId, - user_id: userId, - content: ragSearchResult.answer, - sender_type: 'assistant', - role: 'assistant', - channel: 'web' - }); - logger.info(`[RAG] AI сообщение сохранено:`, aiMessage); - // Пушим новое сообщение через WebSocket - broadcastChatMessage(aiMessage); - } else if (ragSearchResult) { - logger.info(`[RAG] Нет confident-ответа (score=${ragSearchResult.score}), переходим к генерации через LLM.`); - // Генерация через LLM с подстановкой значений из RAG и историей беседы - const llmResponse = await generateLLMResponse({ - userQuestion: messageContent, - // ВАЖНО: если совпадение неуверенное — НЕ подмешиваем RAG-контент, - // иначе модель уходит в ответы про MetaMask и прочие нерелевантные темы - context: '', - answer: '', - clarifyingAnswer: ragSearchResult.clarifyingAnswer, - objectionAnswer: ragSearchResult.objectionAnswer, - systemPrompt: aiSettings ? aiSettings.system_prompt : '', - history: ragSearchResult.conversationContext ? ragSearchResult.conversationContext.conversationHistory : history, - model: aiSettings ? aiSettings.model : undefined - }); - if (llmResponse) { - aiMessage = await encryptedDb.saveData('messages', { - conversation_id: conversationId, - user_id: userId, - content: llmResponse, - sender_type: 'assistant', - role: 'assistant', - channel: 'web' - }); - // Пушим новое сообщение через WebSocket - broadcastChatMessage(aiMessage); - } else { - logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`); - } - } - } - // --- конец RAG автоответа --- - } catch (aiError) { - logger.error('Error getting or saving AI response (RAG):', aiError); - // Не прерываем основной ответ, но логируем ошибку - } - } - - // Fallback: если AI не смог ответить, создаем fallback сообщение - if (!aiMessage && messageContent && shouldGenerateAiReply) { - try { - logger.info('[Chat] Creating fallback AI response due to AI error'); - aiMessage = await encryptedDb.saveData('messages', { - conversation_id: conversationId, - user_id: userId, - content: 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.', - sender_type: 'assistant', - role: 'assistant', - channel: 'web' - }); - // Пушим новое сообщение через WebSocket - broadcastChatMessage(aiMessage); - } catch (fallbackError) { - logger.error('Error creating fallback AI response:', fallbackError); - } - } - - // Форматируем ответ для фронтенда - const formatMessageForFrontend = (msg) => { - if (!msg) return null; - console.log(`🔍 [formatMessageForFrontend] Форматируем сообщение:`, { - id: msg.id, - sender_type: msg.sender_type, - role: msg.role, - content: msg.content, - // Добавляем все поля для диагностики - allFields: Object.keys(msg), - rawMsg: msg + // Проверяем готовность системы + if (!botManager.isReady()) { + return res.status(503).json({ + success: false, + error: 'Система ботов не готова. Попробуйте позже.' }); - const formatted = { - id: msg.id, - conversation_id: msg.conversation_id, - user_id: msg.user_id, - content: msg.content, // content уже расшифрован encryptedDb - sender_type: msg.sender_type, // sender_type уже расшифрован encryptedDb - role: msg.role, // role уже расшифрован encryptedDb - channel: msg.channel, // channel уже расшифрован encryptedDb - created_at: msg.created_at, - attachments: null // Инициализируем как null - }; - // Добавляем информацию о файле, если она есть - if (msg.attachment_filename) { - formatted.attachments = [{ - originalname: msg.attachment_filename, // attachment_filename уже расшифрован encryptedDb - mimetype: msg.attachment_mimetype, // attachment_mimetype уже расшифрован encryptedDb - size: msg.attachment_size, - // НЕ передаем attachment_data обратно в ответе на POST - }]; - } - return formatted; - }; + } - // Обновляем updated_at у диалога - await db.getQuery()( - 'UPDATE conversations SET updated_at = NOW() WHERE id = $1', - [conversationId] - ); + // Получаем WebBot + const webBot = botManager.getBot('web'); + if (!webBot || !webBot.isInitialized) { + return res.status(503).json({ + success: false, + error: 'Web Bot не инициализирован' + }); + } - // Получаем расшифрованные данные для форматирования - const decryptedUserMessage = userMessage ? await encryptedDb.getData('messages', { id: userMessage.id }, 1) : null; - const decryptedAiMessage = aiMessage ? await encryptedDb.getData('messages', { id: aiMessage.id }, 1) : null; - - const response = { - success: true, - conversationId: conversationId, - userMessage: formatMessageForFrontend(decryptedUserMessage ? decryptedUserMessage[0] : null), - aiMessage: formatMessageForFrontend(decryptedAiMessage ? decryptedAiMessage[0] : null), - }; - - console.log(`📤 [Chat] Отправляем ответ на фронтенд:`, { - userMessage: response.userMessage, - aiMessage: response.aiMessage + // Обрабатываем сообщение через новую архитектуру + await webBot.handleMessage(req, res, async (messageData) => { + return await botManager.processMessage(messageData); }); - - // Отправляем WebSocket уведомления - if (response.userMessage) { - broadcastChatMessage(response.userMessage, userId); - } - if (response.aiMessage) { - broadcastChatMessage(response.aiMessage, userId); - } - broadcastConversationUpdate(conversationId, userId); - - res.json(response); + } catch (error) { - logger.error('Error processing authenticated message:', error); - res.status(500).json({ success: false, error: 'Ошибка обработки сообщения' }); + logger.error('[Chat] Ошибка обработки сообщения:', error); + res.status(500).json({ + success: false, + error: 'Внутренняя ошибка сервера' + }); } }); -// Новый маршрут для обработки сообщений через очередь -router.post('/message-queued', requireAuth, upload.array('attachments'), async (req, res) => { - logger.info('Received /message-queued request'); - - try { - const userId = req.session.userId; - const { message, language, conversationId: convIdFromRequest, type = 'chat' } = req.body; - const files = req.files; - const file = files && files.length > 0 ? files[0] : null; - - // Валидация - if (!message && !file) { - return res.status(400).json({ - success: false, - error: 'Требуется текст сообщения или файл.' - }); - } - - if (message && file) { - return res.status(400).json({ - success: false, - error: 'Нельзя отправить текст и файл одновременно.' - }); - } - - // Получаем ключ шифрования - 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); - } - - let conversationId = convIdFromRequest; - let conversation = null; - - // Найти или создать диалог - if (conversationId) { - let convResult; - if (req.session.isAdmin) { - convResult = await db.getQuery()( - 'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE id = $1', - [conversationId, encryptionKey] - ); - } else { - convResult = await db.getQuery()( - 'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $3) as title FROM conversations WHERE id = $1 AND user_id = $2', - [conversationId, userId, encryptionKey] - ); - } - if (convResult.rows.length === 0) { - return res.status(404).json({ - success: false, - error: 'Диалог не найден или доступ запрещен' - }); - } - conversation = convResult.rows[0]; - } else { - // Ищем последний диалог пользователя - const lastConvResult = 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', - [userId, encryptionKey] - ); - if (lastConvResult.rows.length > 0) { - conversation = lastConvResult.rows[0]; - conversationId = conversation.id; - } else { - // Создаем новый диалог - const title = message && message.trim() - ? (message.trim().length > 50 ? `${message.trim().substring(0, 50)}...` : message.trim()) - : (file ? `Файл: ${file.originalname}` : 'Новый диалог'); - const newConvResult = await db.getQuery()( - 'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *', - [userId, title, encryptionKey] - ); - conversation = newConvResult.rows[0]; - conversationId = conversation.id; - } - } - - // Сохраняем сообщение пользователя - const messageContent = message && message.trim() ? message.trim() : null; - const attachmentFilename = file ? file.originalname : null; - const attachmentMimetype = file ? file.mimetype : null; - const attachmentSize = file ? file.size : null; - const attachmentData = file ? file.buffer : null; - - const recipientId = conversation.user_id; - let senderType = 'user'; - let role = 'user'; - if (req.session.isAdmin) { - senderType = 'admin'; - role = 'admin'; - } - - const userMessage = await encryptedDb.saveData('messages', { - conversation_id: conversationId, - user_id: recipientId, - content: messageContent, - sender_type: senderType, - role: role, - channel: 'web', - attachment_filename: attachmentFilename, - attachment_mimetype: attachmentMimetype, - attachment_size: attachmentSize, - attachment_data: attachmentData - }); - - // Проверяем, нужно ли генерировать AI ответ - if (await isUserBlocked(userId)) { - logger.info(`[Chat] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`); - return res.json({ success: true, message: userMessage }); - } - - let shouldGenerateAiReply = true; - if (senderType === 'admin' && userId !== recipientId) { - shouldGenerateAiReply = false; - } - - if (messageContent && shouldGenerateAiReply) { - try { - // Получаем историю сообщений - const historyResult = await db.getQuery()( - 'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10', - [conversationId, userMessage.id, encryptionKey] - ); - const history = historyResult.rows.reverse().map(msg => ({ - role: msg.sender_type === 'user' ? 'user' : 'assistant', - content: msg.content - })); - - // Получаем настройки AI - const aiSettings = await aiAssistantSettingsService.getSettings(); - let rules = null; - if (aiSettings && aiSettings.rules_id) { - rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id); - } - - // Добавляем задачу в очередь - const taskData = { - message: messageContent, - history: history, - systemPrompt: aiSettings ? aiSettings.system_prompt : '', - rules: rules, - type: type, - userId: userId, - userRole: req.session.isAdmin ? 'admin' : 'user', - conversationId: conversationId, - userMessageId: userMessage.id - }; - - const queueResult = await aiQueueService.addTask(taskData); - - res.json({ - success: true, - message: userMessage, - queueInfo: { - taskId: queueResult.taskId, - status: 'queued', - estimatedWaitTime: aiQueueService.getStats().currentQueueSize * 30 - } - }); - - } catch (error) { - logger.error('Error adding task to queue:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при добавлении задачи в очередь.' - }); - } - } else { - res.json({ success: true, message: userMessage }); - } - - } catch (error) { - logger.error('Error processing queued message:', error); - res.status(500).json({ - success: false, - error: 'Внутренняя ошибка сервера.' - }); - } -}); +// Старая логика полностью удалена - используется только BotManager +// Маршрут /message-queued удален - дублировал логику и не использовал централизованные сервисы // Добавьте этот маршрут для проверки доступных моделей router.get('/models', async (req, res) => { @@ -918,16 +129,9 @@ router.get('/history', requireAuth, 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(); try { // Если нужен только подсчет @@ -1019,7 +223,8 @@ router.post('/process-guest', requireAuth, async (req, res) => { return res.status(400).json({ success: false, error: 'guestId is required' }); } try { - const result = await module.exports.processGuestMessages(userId, guestId); + const guestMessageService = require('../services/guestMessageService'); + const result = await guestMessageService.processGuestMessages(userId, guestId); if (result && result.conversationId) { return res.json({ success: true, conversationId: result.conversationId }); } else { @@ -1039,16 +244,9 @@ router.post('/ai-draft', requireAuth, 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(); if (!conversationId || !Array.isArray(messages) || messages.length === 0) { return res.status(400).json({ success: false, error: 'conversationId и messages обязательны' }); } @@ -1101,6 +299,42 @@ router.post('/ai-draft', requireAuth, async (req, res) => { } }); -// Экспортируем маршрутизатор и функцию processGuestMessages отдельно +// Перезапуск конкретного бота (только для админов) +router.post('/restart-bot', requireAdmin, async (req, res) => { + try { + const { botName } = req.body; + + if (!botName || !['web', 'telegram', 'email'].includes(botName)) { + return res.status(400).json({ + success: false, + error: 'Некорректное имя бота. Допустимые значения: web, telegram, email' + }); + } + + logger.info(`[Chat] Запрос на перезапуск ${botName} бота`); + + const result = await botManager.restartBot(botName); + + if (result.success) { + res.json({ + success: true, + message: `${botName} бот успешно перезапущен` + }); + } else { + res.status(500).json({ + success: false, + error: result.error + }); + } + + } catch (error) { + logger.error('[Chat] Ошибка перезапуска бота:', error); + res.status(500).json({ + success: false, + error: 'Ошибка перезапуска бота' + }); + } +}); + +// Экспортируем маршрутизатор module.exports = router; -module.exports.processGuestMessages = processGuestMessages; diff --git a/backend/routes/identities.js b/backend/routes/identities.js index e705e00..80ff13f 100644 --- a/backend/routes/identities.js +++ b/backend/routes/identities.js @@ -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()( diff --git a/backend/routes/messages.js b/backend/routes/messages.js index 6c75c7c..a73c928 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -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; \ No newline at end of file diff --git a/backend/routes/monitoring.js b/backend/routes/monitoring.js index 5378cb6..5bad8a0 100644 --- a/backend/routes/monitoring.js +++ b/backend/routes/monitoring.js @@ -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' diff --git a/backend/routes/ollama.js b/backend/routes/ollama.js index 57c10a8..e806fa1 100644 --- a/backend/routes/ollama.js +++ b/backend/routes/ollama.js @@ -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 diff --git a/backend/routes/pages.js b/backend/routes/pages.js index 9f9f24f..d52333d 100644 --- a/backend/routes/pages.js +++ b/backend/routes/pages.js @@ -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()( diff --git a/backend/routes/settings.js b/backend/routes/settings.js index 62f4bff..e1ea316 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -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 }); diff --git a/backend/routes/system.js b/backend/routes/system.js index 990df60..5fda90f 100644 --- a/backend/routes/system.js +++ b/backend/routes/system.js @@ -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; \ No newline at end of file diff --git a/backend/routes/tables.js b/backend/routes/tables.js index 25bb861..17baaa9 100644 --- a/backend/routes/tables.js +++ b/backend/routes/tables.js @@ -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 => ({ diff --git a/backend/routes/tags.js b/backend/routes/tags.js index 51843ed..c926043 100644 --- a/backend/routes/tags.js +++ b/backend/routes/tags.js @@ -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]; diff --git a/backend/routes/users.js b/backend/routes/users.js index 7ae19ea..9b83209 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -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; diff --git a/backend/server.js b/backend/server.js index 691a122..7dc9f61 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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); }); diff --git a/backend/services/admin-role.js b/backend/services/admin-role.js index f3d3c5b..ea0b3a5 100644 --- a/backend/services/admin-role.js +++ b/backend/services/admin-role.js @@ -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()( diff --git a/backend/services/adminLogicService.js b/backend/services/adminLogicService.js new file mode 100644 index 0000000..93c7495 --- /dev/null +++ b/backend/services/adminLogicService.js @@ -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 +}; + diff --git a/backend/services/ai-assistant.js b/backend/services/ai-assistant.js index 7ec1598..e45e946 100644 --- a/backend/services/ai-assistant.js +++ b/backend/services/ai-assistant.js @@ -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; diff --git a/backend/services/ai-cache.js b/backend/services/ai-cache.js index 78c4b3a..6391376 100644 --- a/backend/services/ai-cache.js +++ b/backend/services/ai-cache.js @@ -87,6 +87,7 @@ class AICache { calculateHitRate() { // Простая реализация - в реальности нужно отслеживать hits/misses + if (this.maxSize === 0) return 0; return this.cache.size / this.maxSize; } } diff --git a/backend/services/aiAssistantSettingsService.js b/backend/services/aiAssistantSettingsService.js index 39c5930..49962f5 100644 --- a/backend/services/aiAssistantSettingsService.js +++ b/backend/services/aiAssistantSettingsService.js @@ -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) { diff --git a/backend/services/aiProviderSettingsService.js b/backend/services/aiProviderSettingsService.js index 63101d3..da9ce65 100644 --- a/backend/services/aiProviderSettingsService.js +++ b/backend/services/aiProviderSettingsService.js @@ -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 diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index b5dda4b..aeed43e 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -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()( diff --git a/backend/services/botManager.js b/backend/services/botManager.js new file mode 100644 index 0000000..2e120b2 --- /dev/null +++ b/backend/services/botManager.js @@ -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} + */ + 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} + */ + 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; + diff --git a/backend/services/botsSettings.js b/backend/services/botsSettings.js new file mode 100644 index 0000000..02a0348 --- /dev/null +++ b/backend/services/botsSettings.js @@ -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} + */ +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} + */ +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} + */ +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 +}; + diff --git a/backend/services/conversationService.js b/backend/services/conversationService.js new file mode 100644 index 0000000..d8f6973 --- /dev/null +++ b/backend/services/conversationService.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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} + */ +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} + */ +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 +}; + diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js index 0fcea0f..5180942 100644 --- a/backend/services/emailAuth.js +++ b/backend/services/emailAuth.js @@ -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: `
+

Код подтверждения

+

Ваш код подтверждения:

+
+ ${code} +
+

Код действителен в течение 15 минут.

+
` + }); + + 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` diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index 1f7e6fa..e49efa7 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -10,297 +10,311 @@ * GitHub: https://github.com/HB3-ACCELERATOR */ -// console.log('[EmailBot] emailBot.js loaded'); -const encryptedDb = require('./encryptedDatabaseService'); -const db = require('../db'); const nodemailer = require('nodemailer'); const Imap = require('imap'); const simpleParser = require('mailparser').simpleParser; -const { processMessage } = require('./ai-assistant'); -const { inspect } = require('util'); const logger = require('../utils/logger'); -const identityService = require('./identity-service'); -const aiAssistant = require('./ai-assistant'); -const { broadcastContactsUpdate } = require('../wsHub'); -const aiAssistantSettingsService = require('./aiAssistantSettingsService'); -const { ragAnswer, generateLLMResponse } = require('./ragService'); -const { isUserBlocked } = require('../utils/userUtils'); +const encryptedDb = require('./encryptedDatabaseService'); +const db = require('../db'); -class EmailBotService { +/** + * EmailBot - обработчик Email сообщений + * Унифицированный интерфейс для работы с Email (IMAP + SMTP) + */ +class EmailBot { constructor() { - // console.log('[EmailBot] constructor called'); + this.name = 'EmailBot'; + this.channel = 'email'; this.imap = null; - this.isChecking = false; + this.transporter = null; + this.settings = null; + this.isInitialized = false; + this.status = 'inactive'; this.reconnectAttempts = 0; this.maxReconnectAttempts = 3; } - // Метод для очистки IMAP соединения - cleanupImapConnection() { + /** + * Инициализация Email Bot + */ + async initialize() { + try { + logger.info('[EmailBot] 🚀 Инициализация Email Bot...'); + + // Загружаем настройки из БД + this.settings = await this.loadSettings(); + + if (!this.settings) { + logger.warn('[EmailBot] ⚠️ Настройки Email не найдены'); + this.status = 'not_configured'; + return { success: false, reason: 'not_configured' }; + } + + // Создаем SMTP транспортер + this.transporter = await this.createTransporter(); + + // Создаем IMAP соединение + await this.initializeImap(); + + this.isInitialized = true; + this.status = 'active'; + + logger.info('[EmailBot] ✅ Email Bot успешно инициализирован'); + return { success: true }; + + } catch (error) { + logger.error('[EmailBot] ❌ Ошибка инициализации:', error); + this.status = 'error'; + return { success: false, error: error.message }; + } + } + + /** + * Загрузка настроек из БД + */ + async loadSettings() { + try { + const encryptionUtils = require('../utils/encryptionUtils'); + const encryptionKey = encryptionUtils.getEncryptionKey(); + + 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(imap_user_encrypted, $1) as imap_user, ' + + 'decrypt_text(imap_password_encrypted, $1) as imap_password, ' + + 'decrypt_text(from_email_encrypted, $1) as from_email ' + + 'FROM email_settings ORDER BY id LIMIT 1', + [encryptionKey] + ); + + if (!rows.length) { + return null; + } + + return rows[0]; + } catch (error) { + logger.error('[EmailBot] Ошибка загрузки настроек:', error); + throw error; + } + } + + /** + * Создание SMTP транспортера + */ + async createTransporter() { + return nodemailer.createTransport({ + host: this.settings.smtp_host, + port: 465, + secure: true, + auth: { + user: this.settings.smtp_user, + pass: this.settings.smtp_password, + }, + pool: false, + maxConnections: 1, + maxMessages: 1, + tls: { + rejectUnauthorized: false + }, + connectionTimeout: 30000, + greetingTimeout: 30000, + socketTimeout: 60000, + }); + } + + /** + * Инициализация IMAP соединения + */ + async initializeImap() { + try { + // Очищаем предыдущее соединение + this.cleanupImap(); + + this.imap = new Imap({ + user: this.settings.imap_user, + password: this.settings.imap_password, + host: this.settings.imap_host, + port: 993, + tls: true, + tlsOptions: { + rejectUnauthorized: false, + servername: this.settings.imap_host, + ciphers: 'HIGH:!aNULL:!MD5:!RC4' + }, + keepalive: { + interval: 10000, + idleInterval: 300000, + forceNoop: true, + }, + connTimeout: 60000, + authTimeout: 60000, + greetingTimeout: 30000, + socketTimeout: 60000, + debug: false + }); + + // Настраиваем обработчики событий + this.setupImapHandlers(); + + // Подключаемся + this.imap.connect(); + + } catch (error) { + logger.error('[EmailBot] Ошибка инициализации IMAP:', error); + throw error; + } + } + + /** + * Настройка обработчиков IMAP событий + */ + setupImapHandlers() { + this.imap.once('ready', () => { + logger.info('[EmailBot] IMAP соединение установлено'); + this.reconnectAttempts = 0; + this.checkEmails(); + }); + + this.imap.once('end', () => { + logger.info('[EmailBot] IMAP соединение завершено'); + this.cleanupImap(); + }); + + this.imap.once('close', () => { + logger.info('[EmailBot] IMAP соединение закрыто'); + this.cleanupImap(); + }); + + this.imap.once('error', (err) => { + logger.error('[EmailBot] IMAP ошибка:', err.message); + this.cleanupImap(); + this.handleReconnection(err); + }); + } + + /** + * Очистка IMAP соединения + */ + cleanupImap() { if (this.imap) { try { - // Удаляем все обработчики событий this.imap.removeAllListeners('error'); this.imap.removeAllListeners('ready'); this.imap.removeAllListeners('end'); this.imap.removeAllListeners('close'); - // Закрываем соединение if (this.imap.state !== 'disconnected') { this.imap.end(); } } catch (error) { - logger.error('[EmailBot] Error cleaning up IMAP connection:', error); + logger.error('[EmailBot] Ошибка очистки IMAP:', error); } finally { this.imap = null; } } } - async getSettingsFromDb() { - // Получаем ключ шифрования - const fs = require('fs'); - const path = require('path'); - let encryptionKey = 'default-key'; + /** + * Обработка переподключения IMAP + */ + handleReconnection(err) { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + logger.error('[EmailBot] Достигнут максимум попыток переподключения'); + this.status = 'connection_failed'; + return; + } + + let reconnectDelay = 10000; - 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); + if (err.message && err.message.toLowerCase().includes('timed out')) { + reconnectDelay = 15000; + } else if (err.code === 'ECONNREFUSED') { + reconnectDelay = 30000; + } else if (err.code === 'ENOTFOUND') { + reconnectDelay = 60000; } + + this.reconnectAttempts++; + logger.warn(`[EmailBot] Переподключение через ${reconnectDelay/1000}с (попытка ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); - 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(imap_user_encrypted, $1) as imap_user, decrypt_text(imap_password_encrypted, $1) as imap_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 in DB'); - return rows[0]; - } - - async getTransporter() { - const settings = await this.getSettingsFromDb(); - return nodemailer.createTransport({ - host: settings.smtp_host, - port: 465, // Используем порт 465 для SSMTP (SSL) - secure: true, // Включаем SSL - auth: { - user: settings.smtp_user, - pass: settings.smtp_password, - }, - pool: false, // Отключаем пул соединений - maxConnections: 1, // Ограничиваем до 1 соединения - maxMessages: 1, // Ограничиваем до 1 сообщения на соединение - tls: { - rejectUnauthorized: false - // Убираем minVersion и maxVersion для избежания конфликтов TLS - }, - connectionTimeout: 30000, // 30 секунд на подключение - greetingTimeout: 30000, // 30 секунд на приветствие - socketTimeout: 60000, // 60 секунд на операции сокета - }); - } - - async getImapConfig() { - const settings = await this.getSettingsFromDb(); - return { - user: settings.imap_user, // Используем IMAP пользователя - password: settings.imap_password, // Используем IMAP пароль - host: settings.imap_host, - port: 993, // Используем порт 993 для IMAPS (SSL) - tls: true, // Включаем SSL - tlsOptions: { - rejectUnauthorized: false, - servername: settings.imap_host, - // Убираем minVersion и maxVersion для избежания конфликтов TLS - ciphers: 'HIGH:!aNULL:!MD5:!RC4' // Безопасные шифры - }, - keepalive: { - interval: 10000, - idleInterval: 300000, - forceNoop: true, - }, - connTimeout: 60000, // 60 секунд - authTimeout: 60000, // Таймаут на аутентификацию - 60 секунд - greetingTimeout: 30000, // Таймаут на приветствие сервера - socketTimeout: 60000, // Таймаут на операции сокета - debug: false // Включаем отладку для диагностики - }; - } - - // Метод для инициализации email верификации - async initEmailVerification(email, userId, code) { - try { - // Отправляем код на email - await this.sendVerificationCode(email, code); - - return { success: true }; - } catch (error) { - logger.error('Error initializing email verification:', error); - throw error; - } - } - - // Отправка кода верификации - async sendVerificationCode(email, code) { - try { - const settings = await this.getSettingsFromDb(); - const transporter = await this.getTransporter(); - const mailOptions = { - from: settings.from_email, - to: email, - subject: 'Код подтверждения', - text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`, - html: `

Код подтверждения

Ваш код подтверждения:

${code}

Код действителен в течение 15 минут.

`, - }; - await transporter.sendMail(mailOptions); - // logger.info(`Verification code sent to ${email}`); // Убрано логирование email адреса - } catch (error) { - logger.error('Error sending verification code:', error); - throw error; - } + setTimeout(() => this.initializeImap(), reconnectDelay); } + /** + * Проверка входящих писем + */ checkEmails() { try { - // Добавляем обработчики ошибок - this.imap.once('error', (err) => { - logger.error(`IMAP connection error during check: ${err.message}`); - try { - this.imap.end(); - } catch (e) { - // Игнорируем ошибки при закрытии + this.imap.openBox('INBOX', false, (err, box) => { + if (err) { + logger.error('[EmailBot] Ошибка открытия INBOX:', err); + return; } - }); - this.imap.once('ready', () => { - this.imap.openBox('INBOX', false, (err, box) => { - if (err) { - logger.error(`Error opening inbox: ${err}`); + this.imap.search(['ALL'], (err, results) => { + if (err || !results || results.length === 0) { this.imap.end(); return; } - // Ищем все письма и проверяем их флаги вручную - this.imap.search(['ALL'], (err, results) => { - if (err) { - logger.error(`Error searching messages: ${err}`); - this.imap.end(); - return; - } + const f = this.imap.fetch(results, { + bodies: '', + markSeen: true, + struct: true + }); - if (!results || results.length === 0) { - // logger.info('No messages found'); // Убрано избыточное логирование - this.imap.end(); - return; - } + let processedCount = 0; + const totalMessages = results.length; - // Фильтруем только непрочитанные сообщения - const f = this.imap.fetch(results, { - bodies: '', - markSeen: true, // Помечаем как прочитанные - struct: true // Получаем структуру для Message-ID - }); - - let unreadMessages = []; - let processedCount = 0; - let totalMessages = results.length; - - f.on('message', (msg, seqno) => { - let messageId = null; - let uid = null; - let flags = []; - - // Получаем UID, Message-ID и флаги - msg.once('attributes', (attrs) => { - uid = attrs.uid; - flags = attrs.flags || []; - if (attrs['x-gm-msgid']) { - messageId = attrs['x-gm-msgid']; - } - }); - - msg.on('body', (stream, info) => { - simpleParser(stream, async (err, parsed) => { - if (err) { - logger.error(`Error parsing message: ${err}`); - processedCount++; - if (processedCount >= totalMessages) { - if (unreadMessages.length === 0) { - // logger.info('No unread messages found'); // Убрано избыточное логирование - } - this.imap.end(); - } - return; - } - - // Получаем Message-ID из заголовков - if (!messageId && parsed.messageId) { - messageId = parsed.messageId; - } - - const fromEmail = parsed.from?.value?.[0]?.address; - const subject = parsed.subject || ''; - const text = parsed.text || ''; - const html = parsed.html || ''; - - // Проверяем, что сообщение непрочитанное (нет флага \Seen) - const isUnread = !flags.includes('\\Seen'); - - // Логируем только для отладки - // logger.info(`[EmailBot] Проверяем письмо: UID=${uid}, Message-ID=${messageId}, From=${fromEmail}, Unread=${isUnread}`); // Убрано избыточное логирование - - // Обрабатываем ВСЕ новые письма, независимо от статуса "прочитано" - // Проверка на уже обработанные письма будет в processIncomingEmail - unreadMessages.push({ - uid, - messageId, - fromEmail, - subject, - text, - html, - parsed - }); - - processedCount++; - if (processedCount >= totalMessages) { - if (unreadMessages.length === 0) { - logger.info('No unread messages found'); - this.imap.end(); - return; - } - - // logger.info(`[EmailBot] Найдено ${unreadMessages.length} непрочитанных сообщений`); // Убрано избыточное логирование - - // Обрабатываем каждое непрочитанное сообщение - for (const message of unreadMessages) { - // logger.info(`[EmailBot] Обрабатываем письмо: UID=${message.uid}, Message-ID=${message.messageId}, From=${message.fromEmail}`); // Убрано избыточное логирование - try { - await this.processIncomingEmail(message); - } catch (processErr) { - logger.error('Error processing incoming email:', processErr); - } - } - - this.imap.end(); - } - }); - }); + f.on('message', (msg, seqno) => { + let messageId = null; + let uid = null; + + msg.once('attributes', (attrs) => { + uid = attrs.uid; + if (attrs['x-gm-msgid']) { + messageId = attrs['x-gm-msgid']; + } }); - f.once('error', (err) => { - logger.error(`Error fetching messages: ${err}`); - this.imap.end(); + msg.on('body', (stream, info) => { + simpleParser(stream, async (err, parsed) => { + if (err) { + processedCount++; + if (processedCount >= totalMessages) { + this.imap.end(); + } + return; + } + + if (!messageId && parsed.messageId) { + messageId = parsed.messageId; + } + + const messageData = this.extractMessageData(parsed, messageId, uid); + if (messageData && this.messageProcessor) { + await this.messageProcessor(messageData); + } + + processedCount++; + if (processedCount >= totalMessages) { + this.imap.end(); + } + }); }); }); + + f.once('error', (err) => { + logger.error('[EmailBot] Ошибка получения писем:', err); + this.imap.end(); + }); }); }); - - this.imap.connect(); } catch (error) { - logger.error(`Global error checking emails: ${error.message}`); + logger.error('[EmailBot] Ошибка проверки писем:', error); try { this.imap.end(); } catch (e) { @@ -309,13 +323,19 @@ class EmailBotService { } } - // Метод для отправки email - async processIncomingEmail(messageData) { - const { uid, messageId, fromEmail, subject, text, html, parsed } = messageData; - + /** + * Извлечение данных из Email сообщения + * @param {Object} parsed - Распарсенное письмо + * @param {string} messageId - ID сообщения + * @param {number} uid - UID сообщения + * @returns {Object|null} - Стандартизированные данные сообщения + */ + extractMessageData(parsed, messageId, uid) { try { - logger.info(`[EmailBot] Обрабатываем письмо: UID=${uid}, Message-ID=${messageId}, From=${fromEmail}`); - + const fromEmail = parsed.from?.value?.[0]?.address; + const subject = parsed.subject || ''; + const text = parsed.text || ''; + // Фильтруем системные email адреса const systemEmails = [ 'mailer-daemon@smtp.hostland.ru', @@ -331,492 +351,155 @@ class EmailBotService { fromEmail && fromEmail.toLowerCase().includes(systemEmail.toLowerCase()) ); - if (isSystemEmail) { - logger.info(`[EmailBot] Игнорируем системный email от ${fromEmail}`); - return; + if (isSystemEmail || !fromEmail || !fromEmail.includes('@')) { + return null; } - - // Проверяем, что email адрес валидный - if (!fromEmail || !fromEmail.includes('@')) { - logger.info(`[EmailBot] Игнорируем email с невалидным адресом: ${fromEmail}`); - return; - } - - // Временные ограничения удалены - обрабатываем все письма независимо от возраста - - // 1. Найти или создать пользователя - const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail); - if (await isUserBlocked(userId)) { - logger.info(`Email от заблокированного пользователя ${userId} проигнорирован.`); - return; - } - - // Проверяем, не обрабатывали ли мы уже это письмо - if (messageId) { - try { - // Проверяем, есть ли уже ответ от AI для этого письма - // Ищем сообщения с direction='out' и metadata, содержащим originalMessageId - const existingResponse = await encryptedDb.getData( - 'messages', - { - user_id: userId, - channel: 'email', - direction: 'out' - }, - 1 - ); - - // Проверяем в результатах, есть ли сообщение с metadata.originalMessageId = messageId - const hasResponse = existingResponse.some(msg => { - try { - const metadata = msg.metadata; - return metadata && metadata.originalMessageId === messageId; - } catch (e) { - return false; - } - }); - - if (hasResponse) { - logger.info(`[EmailBot] Письмо ${messageId} уже обработано - найден ответ от AI`); - return; - } - } catch (error) { - logger.error(`[EmailBot] Ошибка при проверке существующих ответов: ${error.message}`); - } - } - - // 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]; - } - - // Проверяем, что conversation создан успешно - if (!conversation || !conversation.id) { - logger.error(`[EmailBot] Conversation is undefined or has no id for user ${userId}`); - return; - } - - // 2. Сохранять все сообщения с conversation_id - let hasAttachments = parsed.attachments && parsed.attachments.length > 0; - if (hasAttachments) { + + const attachments = []; + if (parsed.attachments && parsed.attachments.length > 0) { + const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; // 10MB + for (const att of parsed.attachments) { - await encryptedDb.saveData( - 'messages', - { - user_id: userId, - conversation_id: conversation.id, - sender_type: 'user', - content: text, - channel: 'email', - role: role, - direction: 'in', - created_at: new Date(), - attachment_filename: att.filename, - attachment_mimetype: att.contentType, - attachment_size: att.size, - attachment_data: att.content, - message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted) - } - ); - } - } else { - await encryptedDb.saveData( - 'messages', - { - user_id: userId, - conversation_id: conversation.id, - sender_type: 'user', - content: text, - channel: 'email', - role: role, - direction: 'in', - created_at: new Date(), - message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted) + if (att.size <= MAX_ATTACHMENT_SIZE) { + attachments.push({ + filename: att.filename, + mimetype: att.contentType, + size: att.size, + data: att.content + }); } - ); - } - - // 3. Получить ответ от ИИ (RAG + LLM) - 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; - } - let aiResponse; - if (ragTableId) { - // Сначала ищем ответ через RAG - const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text }); - 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: text, - history: null, - 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: text, - history: null, - 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); - }); } - - if (await isUserBlocked(userId)) { - logger.info(`[EmailBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`); - return; - } - - // 4. Сохранить ответ в БД с conversation_id - await encryptedDb.saveData( - 'messages', - { - user_id: userId, - conversation_id: conversation.id, - sender_type: 'assistant', - content: aiResponse, - channel: 'email', - role: role, - direction: 'out', - created_at: new Date(), - metadata: JSON.stringify({ - subject, - html, - originalMessageId: messageId, - originalUid: uid, - originalFromEmail: fromEmail, - isResponse: true - }) + + return { + channel: 'email', + identifier: fromEmail, + content: text, + attachments: attachments, + metadata: { + subject: subject, + messageId: messageId, + uid: uid, + fromEmail: fromEmail, + html: parsed.html || '' } - ); - - // 5. Отправить ответ на email - try { - await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse); - logger.info(`[EmailBot] Email response sent successfully to ${fromEmail}`); - } catch (emailError) { - logger.error(`[EmailBot] Failed to send email response to ${fromEmail}:`, emailError); - // Продолжаем выполнение, даже если email не отправлен - } - - // После каждого успешного создания пользователя: - broadcastContactsUpdate(); - - } catch (processErr) { - logger.error('Error processing incoming email:', processErr); - } - } - - async sendEmail(to, subject, text) { - const maxRetries = 1; - const retryDelay = 5000; // 5 секунд между попытками - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const settings = await this.getSettingsFromDb(); - const transporter = await this.getTransporter(); - - const mailOptions = { - from: settings.from_email, - to, - subject, - text, - }; - - await transporter.sendMail(mailOptions); - // logger.info(`Email sent to ${to} (attempt ${attempt})`); // Убрано логирование email адреса - - // Закрываем соединение после успешной отправки - transporter.close(); - return true; - - } catch (error) { - logger.error(`Error sending email (attempt ${attempt}/${maxRetries}):`, error); - - // Если это последняя попытка, выбрасываем ошибку - if (attempt === maxRetries) { - throw error; - } - - // Если ошибка связана с превышением лимита соединений, ждем дольше - const isConnectionLimitError = error.message && ( - error.message.includes('too many connections') || - error.message.includes('421 4.7.0') || - error.message.includes('EPROTOCOL') - ); - - const waitTime = isConnectionLimitError ? retryDelay * 2 : retryDelay; - logger.info(`Waiting ${waitTime}ms before retry...`); - await new Promise(resolve => setTimeout(resolve, waitTime)); - } - } - } - - async start() { - try { - // console.log('[EmailBot] start() called'); - logger.info('[EmailBot] start() called'); - - // Очищаем предыдущее соединение если есть - this.cleanupImapConnection(); - - let attempt = 0; - const maxAttempts = 3; - - const tryConnect = async () => { - attempt++; - this.imap = new Imap(await this.getImapConfig()); - - // Устанавливаем обработчики событий - this.imap.once('ready', () => { - this.reconnectAttempts = 0; // Сбрасываем счетчик при успешном подключении - this.checkEmails(); - }); - - this.imap.once('end', () => { - logger.info('[EmailBot] IMAP connection ended'); - this.cleanupImapConnection(); - }); - - this.imap.once('close', () => { - logger.info('[EmailBot] IMAP connection closed'); - this.cleanupImapConnection(); - }); - - this.imap.once('error', (err) => { - logger.error(`[EmailBot] IMAP connection error: ${err.message}`); - logger.error(`[EmailBot] Error details:`, { - code: err.code, - errno: err.errno, - syscall: err.syscall, - hostname: err.hostname, - port: err.port, - stack: err.stack - }); - this.cleanupImapConnection(); - - // Более детальная логика переподключения - if (attempt < maxAttempts) { - let reconnectDelay = 10000; - let reconnectReason = 'default'; - - if (err.message && err.message.toLowerCase().includes('timed out')) { - reconnectDelay = 15000; // Увеличиваем задержку для таймаутов - reconnectReason = 'timeout'; - } else if (err.code === 'ECONNREFUSED') { - reconnectDelay = 30000; // Дольше ждем для отказа в соединении - reconnectReason = 'connection refused'; - } else if (err.code === 'ENOTFOUND') { - reconnectDelay = 60000; // Еще дольше для проблем с DNS - reconnectReason = 'DNS resolution failed'; - } - - logger.warn(`[EmailBot] IMAP reconnecting in ${reconnectDelay/1000} seconds (attempt ${attempt + 1}/${maxAttempts}, reason: ${reconnectReason})...`); - setTimeout(tryConnect, reconnectDelay); - } else { - logger.error(`[EmailBot] Max reconnection attempts reached (${maxAttempts}). Stopping reconnection.`); - } - }); - - this.imap.connect(); }; - tryConnect(); - } catch (err) { - // console.error('[EmailBot] Ошибка при старте:', err); - logger.error('[EmailBot] Ошибка при старте:', err); - this.cleanupImapConnection(); - throw err; + } catch (error) { + logger.error('[EmailBot] Ошибка извлечения данных из письма:', error); + return null; } } - async getAllEmailSettings() { - const settings = await encryptedDb.getData('email_settings', {}, null, 'id'); - return settings; + /** + * Отправка email сообщения + * @param {string} to - Адрес получателя + * @param {string} subject - Тема письма + * @param {string} text - Текст письма + * @returns {Promise} - Успешность отправки + */ + async sendEmail(to, subject, text) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(to)) { + throw new Error(`Неверный формат email адреса: ${to}`); } - - // Сохранение email настроек - async saveEmailSettings(settings) { + try { - // Проверяем, существуют ли уже настройки - const existingSettings = await encryptedDb.getData('email_settings', {}, 1); + const mailOptions = { + from: this.settings.from_email, + to, + subject, + text, + }; - let result; - if (existingSettings.length > 0) { - // Если настройки существуют, обновляем их - const existingId = existingSettings[0].id; - result = await encryptedDb.saveData('email_settings', settings, { id: existingId }); - } else { - // Если настроек нет, создаем новые - result = await encryptedDb.saveData('email_settings', settings, null); - } + await this.transporter.sendMail(mailOptions); + this.transporter.close(); + + logger.info(`[EmailBot] Email отправлен успешно: ${to}`); + return true; - logger.info('Email settings saved successfully'); - return { success: true, data: result }; } catch (error) { - logger.error('Error saving email settings:', error); + logger.error('[EmailBot] Ошибка отправки email:', error); throw error; } } - // Тест IMAP подключения - async testImapConnection() { - return new Promise(async (resolve, reject) => { - try { - logger.info('[EmailBot] Testing IMAP connection...'); - - // Получаем конфигурацию IMAP - const imapConfig = await this.getImapConfig(); - - // Создаем временное IMAP соединение для теста - const testImap = new Imap(imapConfig); - - let connectionTimeout = setTimeout(() => { - testImap.end(); - reject(new Error('IMAP connection timeout after 30 seconds')); - }, 30000); - - testImap.once('ready', () => { - clearTimeout(connectionTimeout); - logger.info('[EmailBot] IMAP connection test successful'); - testImap.end(); - resolve({ - success: true, - message: 'IMAP подключение успешно установлено', - details: { - host: imapConfig.host, - port: imapConfig.port, - user: imapConfig.user - } - }); - }); - - testImap.once('error', (err) => { - clearTimeout(connectionTimeout); - logger.error(`[EmailBot] IMAP connection test failed: ${err.message}`); - testImap.end(); - reject(new Error(`IMAP подключение не удалось: ${err.message}`)); - }); - - testImap.once('end', () => { - clearTimeout(connectionTimeout); - logger.info('[EmailBot] IMAP connection test ended'); - }); - - testImap.connect(); - - } catch (error) { - reject(new Error(`Ошибка при тестировании IMAP: ${error.message}`)); - } - }); + /** + * Отправка кода верификации + * @param {string} email - Email получателя + * @param {string} code - Код верификации + */ + async sendVerificationCode(email, code) { + try { + const mailOptions = { + from: this.settings.from_email, + to: email, + subject: 'Код подтверждения', + text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`, + html: `
+

Код подтверждения

+

Ваш код подтверждения:

+
+ ${code} +
+

Код действителен в течение 15 минут.

+
`, + }; + + await this.transporter.sendMail(mailOptions); + logger.info('[EmailBot] Код верификации отправлен'); + } catch (error) { + logger.error('[EmailBot] Ошибка отправки кода верификации:', error); + throw error; + } } - // Тест SMTP подключения - async testSmtpConnection() { - return new Promise(async (resolve, reject) => { - try { - logger.info('[EmailBot] Testing SMTP connection...'); - - // Получаем транспортер SMTP - const transporter = await this.getTransporter(); - - // Тестируем подключение - await transporter.verify(); - - logger.info('[EmailBot] SMTP connection test successful'); - resolve({ - success: true, - message: 'SMTP подключение успешно установлено', - details: { - host: transporter.options.host, - port: transporter.options.port, - secure: transporter.options.secure - } - }); - - } catch (error) { - logger.error(`[EmailBot] SMTP connection test failed: ${error.message}`); - reject(new Error(`SMTP подключение не удалось: ${error.message}`)); + /** + * Установка процессора сообщений + * @param {Function} processor - Функция обработки сообщений + */ + setMessageProcessor(processor) { + this.messageProcessor = processor; + } + + /** + * Проверка статуса бота + * @returns {Object} - Статус бота + */ + getStatus() { + return { + name: this.name, + channel: this.channel, + isInitialized: this.isInitialized, + status: this.status, + hasSettings: !!this.settings, + reconnectAttempts: this.reconnectAttempts + }; + } + + /** + * Остановка бота + */ + async stop() { + try { + logger.info('[EmailBot] 🛑 Остановка Email Bot...'); + + this.cleanupImap(); + + if (this.transporter) { + this.transporter.close(); + this.transporter = null; } - }); + + this.isInitialized = false; + this.status = 'inactive'; + + logger.info('[EmailBot] ✅ Email Bot остановлен'); + } catch (error) { + logger.error('[EmailBot] ❌ Ошибка остановки:', error); + throw error; + } } } -// console.log('[EmailBot] module.exports = EmailBotService'); -module.exports = EmailBotService; +module.exports = EmailBot; + diff --git a/backend/services/encryptedDatabaseService.js b/backend/services/encryptedDatabaseService.js index 92825c5..e953f57 100644 --- a/backend/services/encryptedDatabaseService.js +++ b/backend/services/encryptedDatabaseService.js @@ -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'; - } - } /** * Получить данные из таблицы с автоматической расшифровкой diff --git a/backend/services/guestMessageService.js b/backend/services/guestMessageService.js new file mode 100644 index 0000000..7cf4082 --- /dev/null +++ b/backend/services/guestMessageService.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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 +}; + diff --git a/backend/services/guestService.js b/backend/services/guestService.js new file mode 100644 index 0000000..2519c8d --- /dev/null +++ b/backend/services/guestService.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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 +}; + diff --git a/backend/services/index.js b/backend/services/index.js index 09dd329..ff2a673 100644 --- a/backend/services/index.js +++ b/backend/services/index.js @@ -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'), }; diff --git a/backend/services/messageDeduplicationService.js b/backend/services/messageDeduplicationService.js new file mode 100644 index 0000000..d7a14ae --- /dev/null +++ b/backend/services/messageDeduplicationService.js @@ -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 +}; + diff --git a/backend/services/notifyOllamaReady.js b/backend/services/notifyOllamaReady.js new file mode 100644 index 0000000..d7c2dfe --- /dev/null +++ b/backend/services/notifyOllamaReady.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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 +}; + diff --git a/backend/services/ollamaConfig.js b/backend/services/ollamaConfig.js new file mode 100644 index 0000000..0f9d016 --- /dev/null +++ b/backend/services/ollamaConfig.js @@ -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} Настройки 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} Базовый 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} Название модели из БД + */ +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} Название 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} Объект с конфигурацией + */ +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} Статус здоровья сервиса + */ +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 +}; diff --git a/backend/services/ragService.js b/backend/services/ragService.js index 66f6525..170ff88 100644 --- a/backend/services/ragService.js +++ b/backend/services/ragService.js @@ -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`); diff --git a/backend/services/session-service.js b/backend/services/session-service.js index 189006d..e9243aa 100644 --- a/backend/services/session-service.js +++ b/backend/services/session-service.js @@ -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} - Результат операции - */ - 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 diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 55f5614..bd7c473 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -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} - Буфер с данными файла + */ + 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, -}; diff --git a/backend/services/testNewBots.js b/backend/services/testNewBots.js new file mode 100644 index 0000000..b5e4579 --- /dev/null +++ b/backend/services/testNewBots.js @@ -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 +}; + diff --git a/backend/services/tokenBalanceService.js b/backend/services/tokenBalanceService.js index 9181437..3c976c5 100644 --- a/backend/services/tokenBalanceService.js +++ b/backend/services/tokenBalanceService.js @@ -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()( diff --git a/backend/services/unifiedMessageProcessor.js b/backend/services/unifiedMessageProcessor.js new file mode 100644 index 0000000..5f3261d --- /dev/null +++ b/backend/services/unifiedMessageProcessor.js @@ -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} + */ +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} + */ +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 +}; + diff --git a/backend/services/userDeleteService.js b/backend/services/userDeleteService.js index 2f1eaa9..7dd6b59 100644 --- a/backend/services/userDeleteService.js +++ b/backend/services/userDeleteService.js @@ -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()( diff --git a/backend/services/webBot.js b/backend/services/webBot.js new file mode 100644 index 0000000..2959534 --- /dev/null +++ b/backend/services/webBot.js @@ -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} + */ + 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} + */ + 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; + diff --git a/backend/utils/constants.js b/backend/utils/constants.js index c8a34f0..daec534 100644 --- a/backend/utils/constants.js +++ b/backend/utils/constants.js @@ -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, }; diff --git a/backend/utils/encryptionUtils.js b/backend/utils/encryptionUtils.js new file mode 100644 index 0000000..b333957 --- /dev/null +++ b/backend/utils/encryptionUtils.js @@ -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 +}; + diff --git a/backend/utils/helpers.js b/backend/utils/helpers.js index 27c5d48..d95640d 100644 --- a/backend/utils/helpers.js +++ b/backend/utils/helpers.js @@ -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)', diff --git a/backend/utils/logger.js b/backend/utils/logger.js index c766ad0..26841ec 100644 --- a/backend/utils/logger.js +++ b/backend/utils/logger.js @@ -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({ diff --git a/backend/wsHub.js b/backend/wsHub.js index 920df69..7e237c7 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -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] Уведомление о готовности системы отправлено всем клиентам'); } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 692b361..092d311 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5f22d1b..c048b04 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -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="Тарабанов Александр Викторович " @@ -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 ./ diff --git a/frontend/nginx.Dockerfile b/frontend/nginx.Dockerfile index 0602eef..a8fe476 100644 --- a/frontend/nginx.Dockerfile +++ b/frontend/nginx.Dockerfile @@ -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 diff --git a/frontend/src/components/ChatInterface.vue b/frontend/src/components/ChatInterface.vue index 0141664..b79201f 100644 --- a/frontend/src/components/ChatInterface.vue +++ b/frontend/src/components/ChatInterface.vue @@ -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; } diff --git a/frontend/src/views/contacts/ContactDetailsView.vue b/frontend/src/views/contacts/ContactDetailsView.vue index 64ac7ba..fc504a3 100644 --- a/frontend/src/views/contacts/ContactDetailsView.vue +++ b/frontend/src/views/contacts/ContactDetailsView.vue @@ -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); } } } diff --git a/webssh-agent/Dockerfile b/webssh-agent/Dockerfile index faa12fa..7c33dc4 100644 --- a/webssh-agent/Dockerfile +++ b/webssh-agent/Dockerfile @@ -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