feat: новая функция

This commit is contained in:
2025-10-08 18:01:14 +03:00
parent 2c53bce32a
commit 725e7fd5a2
60 changed files with 5427 additions and 3921 deletions

View File

@@ -3,9 +3,18 @@
# This software is proprietary and confidential. # This software is proprietary and confidential.
# For licensing inquiries: info@hb3-accelerator.com # For licensing inquiries: info@hb3-accelerator.com
FROM node:20-alpine FROM node:20-slim
WORKDIR /app 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 RUN npm install -g @upstash/context7-mcp@1.0.8
# Запуск сервера при старте контейнера # Запуск сервера при старте контейнера
CMD ["context7-mcp"] CMD ["context7-mcp"]

View File

@@ -0,0 +1,911 @@
# Структура базы данных для AI Ассистента
**Дата проверки:** 2025-10-08
**Метод:** Прямая проверка через PostgreSQL
**Статус:** ✅ ПРОВЕРКА ЗАВЕРШЕНА
---
## 📊 Список AI таблиц
Найдено таблиц: **27 таблиц** (25 связаны с AI, 2 CMS)
---
## 1. `ai_assistant_settings` ⭐ КЛЮЧЕВАЯ
**Назначение:** Основные настройки AI ассистента
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `selected_rag_tables` - INTEGER[] - массив ID RAG таблиц для использования
- `languages` - TEXT[] - массив поддерживаемых языков
- `updated_at` - TIMESTAMP - время последнего обновления (default: now())
- `updated_by` - INTEGER - кто обновил (user_id)
- `rules_id` - INTEGER (FK → ai_assistant_rules) - ID правил для AI
- `telegram_settings_id` - INTEGER (FK → telegram_settings) - ID настроек Telegram
- `email_settings_id` - INTEGER (FK → email_settings) - ID настроек Email
- `system_prompt_encrypted` - TEXT - зашифрованный системный промпт
- `model_encrypted` - TEXT - зашифрованное название модели
- `system_message_encrypted` - TEXT - зашифрованное системное сообщение
- `embedding_model_encrypted` - TEXT - зашифрованное название embedding модели
- `system_message` - TEXT - системное сообщение (расшифрованное)
- `embedding_model` - VARCHAR(128) - embedding модель (расшифрованное)
**Связи:**
-`ai_assistant_rules` (через rules_id)
-`telegram_settings` (через telegram_settings_id)
-`email_settings` (через email_settings_id)
**Используется в:**
- aiAssistantSettingsService.js (getSettings, updateSettings)
- conversationService.js (getRagTableId)
- ai-assistant.js (generateResponse)
- routes/settings.js (API)
---
## 2. `ai_assistant_rules` ✅ АКТИВНАЯ
**Назначение:** Правила и инструкции для AI ассистента
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `created_at` - TIMESTAMP - дата создания (default: now())
- `updated_at` - TIMESTAMP - дата обновления (default: now())
- `name_encrypted` - TEXT - зашифрованное название правила
- `description_encrypted` - TEXT - зашифрованное описание правила
- `rules_encrypted` - TEXT - зашифрованные правила (JSON) ✅ ДОБАВЛЕНО
**Связи:**
-`ai_assistant_settings.rules_id` ссылается на эту таблицу
**Используется в:**
- aiAssistantRulesService.js (getAllRules, getRuleById, createRule)
- ai-assistant.js (получение правил)
- routes/settings.js (CRUD API)
---
## 3. `ai_providers_settings` ⭐ КЛЮЧЕВАЯ
**Назначение:** Настройки AI провайдеров (Ollama, OpenAI, Anthropic, Google)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
- `provider_encrypted` - TEXT - зашифрованное название провайдера ('ollama', 'openai', etc.)
- `api_key_encrypted` - TEXT - зашифрованный API ключ
- `base_url_encrypted` - TEXT - зашифрованный базовый URL
- `selected_model_encrypted` - TEXT - зашифрованное название выбранной модели
- `embedding_model_encrypted` - TEXT - зашифрованное название embedding модели
- `embedding_model` - VARCHAR(128) - embedding модель (незашифрованное, дублирует?)
**Связи:**
- Нет внешних ключей
**Используется в:**
- aiProviderSettingsService.js (getProviderSettings, upsertProviderSettings)
- ollamaConfig.js (loadSettingsFromDb - загружает настройки Ollama)
- ragService.js (getProviderSettings для вызова разных AI)
- routes/settings.js (CRUD API)
---
## 4. `messages` ⭐ КЛЮЧЕВАЯ
**Назначение:** Все сообщения пользователей и AI ответы
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `conversation_id` - INTEGER (FK → conversations) - ID беседы
- `sender_id` - INTEGER - ID отправителя (для админских сообщений)
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `user_id` - INTEGER (FK → users) - ID пользователя-владельца беседы
- `tokens_used` - INTEGER - количество токенов (default: 0)
- `is_processed` - BOOLEAN - обработано ли (default: false)
- `attachment_size` - BIGINT - размер вложения в байтах
- `attachment_data` - BYTEA - бинарные данные вложения
- `sender_type_encrypted` - TEXT - тип отправителя ('user', 'assistant', 'editor')
- `content_encrypted` - TEXT - текст сообщения
- `channel_encrypted` - TEXT - канал ('web', 'telegram', 'email')
- `role_encrypted` - TEXT - роль
- `attachment_filename_encrypted` - TEXT - имя файла
- `attachment_mimetype_encrypted` - TEXT - MIME тип
- `direction_encrypted` - TEXT - направление ('in', 'out')
- `message_id_encrypted` - TEXT - ID сообщения для дедупликации
- `message_type` - VARCHAR(20) NOT NULL - тип ('user_chat', 'admin_chat')
**Индексы:**
- idx_messages_conversation_id
- idx_messages_created_at
- idx_messages_message_type
- idx_messages_user_id
**Связи:**
-`conversations` (ON DELETE CASCADE)
-`users` (ON DELETE CASCADE)
**Триггеры:**
- `trg_set_message_user_id` - автоустановка user_id
**Используется в:**
- unifiedMessageProcessor.js (saveUserMessage)
- messageDeduplicationService.js (сохранение)
- conversationService.js (история)
- routes/messages.js, routes/chat.js
---
## 5. `conversations` ⭐ КЛЮЧЕВАЯ
**Назначение:** Беседы (диалоги) пользователей с AI
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор беседы
- `user_id` - INTEGER (FK → users) - ID пользователя-владельца
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
- `title_encrypted` - TEXT - зашифрованный заголовок беседы
- `conversation_type` - VARCHAR(50) - тип беседы (default: 'user_chat')
- `'user_chat'` - обычный чат пользователя
- `'admin_chat'` - приватный чат между админами
**Индексы:**
- idx_conversations_conversation_type
- idx_conversations_created_at
- idx_conversations_user_id
**Связи:**
-`users` (user_id, ON DELETE CASCADE)
-`conversation_participants` (для многопользовательских чатов)
-`messages` (все сообщения беседы)
**Используется в:**
- conversationService.js (findOrCreateConversation, getConversationHistory)
- unifiedMessageProcessor.js (создание беседы)
- guestMessageService.js (перенос гостевых сообщений)
- routes/messages.js (CRUD беседы)
---
## 6. `message_deduplication` ⭐ КЛЮЧЕВАЯ
**Назначение:** Предотвращение дублирования сообщений (дедупликация)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `channel` - VARCHAR(20) NOT NULL - канал ('web', 'telegram', 'email')
- `message_id_hash` - VARCHAR(64) NOT NULL - SHA-256 хеш ID
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
- `sender_type` - VARCHAR(20) NOT NULL - тип отправителя ('user', 'assistant')
- `original_message_id_encrypted` - TEXT - оригинальный ID
- `processed_at` - TIMESTAMP WITH TIME ZONE - время обработки (default: now())
- `expires_at` - TIMESTAMP WITH TIME ZONE - время истечения
**Индексы:**
- idx_message_dedup_expires
- idx_message_dedup_lookup (channel, hash, user_id, sender_type)
- idx_message_dedup_user_channel
- UNIQUE (channel, hash, user_id, sender_type)
**Связи:**
-`users` (ON DELETE CASCADE)
**Используется в:**
- messageDeduplicationService.js
- unifiedMessageProcessor.js
- ai-assistant.js
---
## 7. `guest_messages` ✅ АКТИВНАЯ
**Назначение:** Временное хранение сообщений гостей
**Столбцы:**
- `id` - INTEGER (PK)
- `is_ai` - BOOLEAN - от AI? (default: false)
- `created_at` - TIMESTAMP WITH TIME ZONE (default: now())
- `attachment_size` - BIGINT
- `attachment_data` - BYTEA
- `guest_id_encrypted` - TEXT - ID гостя (sessionID)
- `content_encrypted` - TEXT
- `language_encrypted` - TEXT
- `attachment_filename_encrypted` - TEXT
- `attachment_mimetype_encrypted` - TEXT
- `attachment_filename` - TEXT (дубль?)
- `attachment_mimetype` - TEXT (дубль?)
**Связи:**
- Нет FK (временная)
**Используется в:**
- guestService.js
- guestMessageService.js (перенос после auth)
**Цикл:**
1. Гость пишет → сохраняется
2. Авторизация → перенос в messages
3. Удаление из guest_messages
---
## 8. `guest_user_mapping` ✅ АКТИВНАЯ
**Назначение:** Связь между гостями и зарегистрированными пользователями
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
- `processed` - BOOLEAN - обработаны ли сообщения (default: false)
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `guest_id_encrypted` - TEXT - зашифрованный ID гостя (sessionID)
**Индексы:**
- idx_guest_user_mapping_guest_id_encrypted - UNIQUE
- idx_guest_user_mapping_user_id
**Связи:**
-`users` (user_id, ON DELETE CASCADE)
**Используется в:**
- guestMessageService.js (processGuestMessages - проверка и создание mapping)
**Логика:**
- При аутентификации гостя создается запись
- `processed = false` → сообщения еще не перенесены
- `processed = true` → сообщения уже перенесены в messages
---
## 9. `telegram_settings` ✅ АКТИВНАЯ
**Назначение:** Настройки Telegram бота
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
- `bot_token_encrypted` - TEXT - зашифрованный токен Telegram бота
- `bot_username_encrypted` - TEXT - зашифрованное имя бота
**Индексы:**
- PRIMARY KEY: id
**Связи:**
-`ai_assistant_settings.telegram_settings_id` ссылается на эту таблицу
**Используется в:**
- telegramBot.js (loadSettings - загрузка токена)
- botsSettings.js (getTelegramSettings, saveTelegramSettings, testConnection)
- routes/admin (API для настройки Telegram)
---
## 10. `email_settings` ✅ АКТИВНАЯ
**Назначение:** Настройки Email бота (SMTP + IMAP)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `smtp_port` - INTEGER NOT NULL - порт SMTP (обычно 465)
- `imap_port` - INTEGER - порт IMAP (обычно 993)
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
- `smtp_host_encrypted` - TEXT - зашифрованный хост SMTP
- `smtp_user_encrypted` - TEXT - зашифрованный пользователь SMTP
- `smtp_password_encrypted` - TEXT - зашифрованный пароль SMTP
- `imap_host_encrypted` - TEXT - зашифрованный хост IMAP
- `from_email_encrypted` - TEXT - зашифрованный email отправителя
- `imap_user_encrypted` - TEXT - зашифрованный пользователь IMAP
- `imap_password_encrypted` - TEXT - зашифрованный пароль IMAP
**Индексы:**
- PRIMARY KEY: id
**Связи:**
-`ai_assistant_settings.email_settings_id` ссылается на эту таблицу
**Используется в:**
- emailBot.js (loadSettings - создание SMTP транспортера и IMAP соединения)
- botsSettings.js (getEmailSettings, saveEmailSettings, testEmailSMTP, testEmailIMAP)
- routes/admin (API для настройки Email)
---
## 11. `is_rag_source` ✅ АКТИВНАЯ
**Назначение:** Источники данных для RAG (справочник)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор источника
- `name_encrypted` - TEXT - зашифрованное название источника
**Индексы:**
- PRIMARY KEY: id
**Связи:**
-`user_tables.is_rag_source_id` ссылается на эту таблицу
**Используется в:**
- Связывает RAG таблицы с типом источника данных
- user_tables имеет default: is_rag_source_id = 2
**Примеры источников:**
- ID 1: "FAQ"
- ID 2: "База знаний"
- ID 3: "Документация"
(зависит от данных в БД)
---
## 12. `user_tables` ⭐ КЛЮЧЕВАЯ (RAG)
**Назначение:** Пользовательские таблицы с данными для RAG базы знаний
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор таблицы
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
- `is_rag_source_id` - INTEGER (FK → is_rag_source) - тип источника (default: 2)
- `name_encrypted` - TEXT - зашифрованное название таблицы
- `description_encrypted` - TEXT - зашифрованное описание
**Индексы:**
- PRIMARY KEY: id
**Связи:**
-`is_rag_source` (is_rag_source_id)
-`user_columns` (table_id, ON DELETE CASCADE)
-`user_rows` (table_id, ON DELETE CASCADE)
-`user_table_relations` (to_table_id, ON DELETE CASCADE)
**Используется в:**
- ragService.js (getTableData - получение данных для RAG)
- routes/tables.js (CRUD таблиц)
- routes/rag.js (выбор таблицы для RAG запроса)
**Структура RAG:**
```
user_tables (таблица)
└─ user_columns (колонки с purpose)
└─ user_rows (строки)
└─ user_cell_values (значения ячеек)
```
---
## 13. `user_columns` ⭐ КЛЮЧЕВАЯ (RAG)
**Назначение:** Колонки пользовательских таблиц с метаданными для RAG
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор колонки
- `table_id` - INTEGER NOT NULL (FK → user_tables) - ID таблицы
- `order` - INTEGER - порядок отображения (default: 0)
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
- `name_encrypted` - TEXT - зашифрованное название колонки
- `type_encrypted` - TEXT - зашифрованный тип ('text', 'number', 'date', etc.)
- `placeholder_encrypted` - TEXT - зашифрованный плейсхолдер
- `placeholder` - VARCHAR(255) - плейсхолдер для RAG (UNIQUE)
- `options` - JSONB - дополнительные опции (default: '{}')
- `purpose` - назначение колонки ('question', 'answer', 'context', 'product', 'priority', 'date')
**Индексы:**
- PRIMARY KEY: id
- idx_user_columns_options (GIN) - для быстрого поиска по JSONB
- user_columns_placeholder_key (UNIQUE) - уникальность плейсхолдеров
**Связи:**
-`user_tables` (table_id, ON DELETE CASCADE)
-`user_table_relations` (column_id, ON DELETE CASCADE)
**Используется в:**
- ragService.js (getTableData - определение колонок по purpose)
- routes/tables.js (CRUD колонок)
**Важно для RAG:**
- `options.purpose` определяет роль колонки:
- 'question' - вопрос для поиска
- 'answer' - ответ
- 'context' - контекст
- 'product', 'priority', 'date' - метаданные
---
## 14. `user_rows` ⭐ КЛЮЧЕВАЯ (RAG)
**Назначение:** Строки данных в пользовательских таблицах (записи для RAG)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор строки
- `table_id` - INTEGER NOT NULL (FK → user_tables) - ID таблицы
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
- `order` - INTEGER - порядок отображения (default: 0)
**Индексы:**
- PRIMARY KEY: id
**Связи:**
-`user_tables` (table_id, ON DELETE CASCADE)
-`user_cell_values` (row_id, ON DELETE CASCADE)
-`user_table_relations` (from_row_id, to_row_id, ON DELETE CASCADE)
-`user_tag_links` (tag_id, ON DELETE CASCADE)
**Используется в:**
- ragService.js (getTableData - получение всех строк таблицы)
- routes/tables.js (CRUD строк)
**Важно:**
- Каждая строка = одна запись в RAG (например, один вопрос-ответ)
- Значения хранятся в `user_cell_values`
---
## 15. `user_cell_values` ⭐ КЛЮЧЕВАЯ (RAG)
**Назначение:** Значения ячеек в пользовательских таблицах (данные для RAG)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор значения
- `row_id` - INTEGER NOT NULL (FK → user_rows) - ID строки
- `column_id` - INTEGER NOT NULL (FK → user_columns) - ID колонки ✅ ИСПРАВЛЕНО
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
- `value_encrypted` - TEXT - зашифрованное значение ячейки
**Индексы:**
- PRIMARY KEY: id
- user_cell_values_row_id_column_id_key (UNIQUE) - уникальная пара (row_id, column_id)
**Связи:**
-`user_rows` (row_id, ON DELETE CASCADE)
-`user_columns` (column_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
**Используется в:**
- ragService.js (getTableData - получение всех значений для построения RAG данных)
- routes/tables.js (CRUD значений ячеек)
**Как работает RAG:**
1. ragService получает все cell_values для строк таблицы
2. Группирует по row_id
3. Находит значения по column_id с нужным purpose (question/answer/context)
4. Формирует данные для векторного поиска
---
## 16. `conversation_participants` ✅ АКТИВНАЯ
**Назначение:** Участники многопользовательских бесед (для admin_chat)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `conversation_id` - INTEGER (FK → conversations) - ID беседы
- `user_id` - INTEGER (FK → users) - ID участника
- `created_at` - TIMESTAMP - дата добавления (default: CURRENT_TIMESTAMP)
**Индексы:**
- PRIMARY KEY: id
- conversation_participants_conversation_id_user_id_key (UNIQUE) - пара (conversation_id, user_id)
- idx_conversation_participants_conversation_id
- idx_conversation_participants_user_id
**Связи:**
-`conversations` (conversation_id, ON DELETE CASCADE)
-`users` (user_id, ON DELETE CASCADE)
**Используется в:**
- routes/messages.js (создание admin_chat бесед между админами)
- routes/chat.js (поиск приватных бесед)
**Логика:**
- Для обычных чатов (`user_chat`) - НЕ используется (один владелец)
- Для админских чатов (`admin_chat`) - хранит всех участников беседы
---
## 17. `users` ⭐ КРИТИЧЕСКАЯ
**Назначение:** Пользователи системы (основа для всех AI взаимодействий)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор пользователя
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
- `role` - user_role ENUM - роль пользователя (default: 'user')
- 'user' - обычный пользователь
- 'editor' - администратор-редактор
- 'readonly' - администратор только для чтения
- `is_blocked` - BOOLEAN NOT NULL - заблокирован ли (default: false)
- `blocked_at` - TIMESTAMP - время блокировки
- `username_encrypted` - TEXT - зашифрованное имя пользователя
- `status_encrypted` - TEXT - зашифрованный статус
- `first_name_encrypted` - TEXT - зашифрованное имя
- `last_name_encrypted` - TEXT - зашифрованная фамилия
- `preferred_language` - JSONB - предпочитаемый язык
**Индексы:**
- PRIMARY KEY: id
- idx_users_role
**Связи (Referenced by):**
- ← conversation_participants (9 таблиц ссылаются!)
- ← conversations
- ← message_deduplication
- ← global_read_status
- ← guest_user_mapping
- ← messages
- ← user_identities
- ← user_preferences
- ← user_tag_links
- ← verification_codes
**Используется в:**
- identity-service.js (создание пользователей)
- auth-service.js (проверка ролей)
- userUtils.js (isUserBlocked)
- ВСЕ AI сервисы (через связи)
---
## 18. `user_identities` ⭐ КРИТИЧЕСКАЯ
**Назначение:** Идентификаторы пользователей (wallet, email, telegram)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `user_id` - INTEGER (FK → users) - ID пользователя
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `provider_encrypted` - TEXT - зашифрованный провайдер ('wallet', 'email', 'telegram')
- `provider_id_encrypted` - TEXT - зашифрованный идентификатор (адрес, email, telegram ID)
**Индексы:**
- PRIMARY KEY: id
- idx_user_identities_user_id
**Связи:**
-`users` (user_id, ON DELETE CASCADE)
**Используется в:**
- identity-service.js (findUserByIdentity, saveIdentity, linkWalletToUser)
- unifiedMessageProcessor.js (authenticateUser - поиск пользователя по Telegram/Email)
- telegramBot.js, emailBot.js (связь external ID с user_id)
- routes/identities.js (управление идентификаторами)
- routes/messages.js (broadcast - поиск каналов пользователя)
**Логика:**
- Один пользователь может иметь несколько идентификаторов (wallet + email + telegram)
- При входе через любой канал - система находит или создает пользователя
---
## 19. `global_read_status` ✅ АКТИВНАЯ
**Назначение:** Глобальный статус прочтения сообщений для user_chat
**Столбцы:**
- `user_id` - INTEGER (PK, FK → users) - ID пользователя
- `last_read_at` - TIMESTAMP NOT NULL - время последнего прочитанного сообщения
- `updated_by_admin_id` - INTEGER NOT NULL - ID админа, который обновил статус
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
**Индексы:**
- PRIMARY KEY: user_id
- idx_global_read_status_last_read_at
- idx_global_read_status_user_id
**Связи:**
-`users` (user_id, ON DELETE CASCADE)
**Используется в:**
- routes/messages.js (mark-read, read-status для user_chat)
**Логика:**
- Один статус на пользователя (общий для всех админов)
- Для обычных чатов (`user_chat`)
- Для админских чатов используется `admin_read_messages`
---
## 20. `admin_read_messages` ✅ АКТИВНАЯ
**Назначение:** Персональный статус прочтения для admin_chat
**Столбцы:**
- `admin_id` - INTEGER NOT NULL (PK, FK → users) - ID администратора
- `user_id` - INTEGER NOT NULL (PK, FK → users) - ID пользователя/другого админа
- `last_read_at` - TIMESTAMP NOT NULL - время последнего прочитанного сообщения
**Индексы:**
- PRIMARY KEY: (admin_id, user_id) - составной ключ
**Связи:**
-`users` (admin_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
-`users` (user_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
**Используется в:**
- routes/messages.js (mark-read, read-status для admin_chat)
**Логика:**
- Персональный статус для каждого админа
- Для приватных чатов между админами (`admin_chat`)
- Каждый админ имеет свой статус прочтения
**Отличие от global_read_status:**
- global_read_status - общий для всех админов (user_chat)
- admin_read_messages - персональный для каждого админа (admin_chat)
---
## 21. `user_tag_links` ✅ АКТИВНАЯ
**Назначение:** Связь пользователей с тегами (для RAG фильтрации)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
- `tag_id` - INTEGER NOT NULL (FK → user_rows) - ID тега (строка из таблицы тегов)
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
**Индексы:**
- PRIMARY KEY: id
- idx_user_tag_links_tag_id
- idx_user_tag_links_user_id
- user_tag_links_user_id_tag_id_key (UNIQUE) - уникальная пара (user_id, tag_id)
**Связи:**
-`users` (user_id, ON DELETE CASCADE)
-`user_rows` (tag_id, ON DELETE CASCADE) - теги хранятся как строки в RAG таблицах
**Используется в:**
- ragService.js (фильтрация данных по пользовательским тегам)
- routes/tables.js (управление тегами пользователей)
**Логика:**
- Теги позволяют фильтровать RAG данные по пользователю
- Один пользователь может иметь несколько тегов
- Используется для персонализации AI ответов
---
## 22. `roles` ⚠️ ВОЗМОЖНО УСТАРЕВШАЯ
**Назначение:** Роли пользователей (возможно старая таблица)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `name_encrypted` - TEXT - зашифрованное название роли
**Индексы:**
- PRIMARY KEY: id
**Связи:**
- Нет внешних ключей
- Нет Referenced by (никто не ссылается!)
**⚠️ ПРОБЛЕМА:**
- Таблица существует, но НЕ используется
- В `users` роль хранится как ENUM `user_role`, а не FK
- Возможно старая таблица, которую можно удалить
**Используется в:**
-НЕ найдено использования в коде
---
## 23. `admin_pages` ⚠️ НЕ СВЯЗАНА С AI
**Назначение:** Страницы контента (CMS), НЕ связана с AI напрямую
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `author_address_encrypted` - TEXT NOT NULL - зашифрованный адрес автора
- `created_at` - TIMESTAMP - дата создания (default: now())
- `updated_at` - TIMESTAMP - дата обновления (default: now())
- `title_encrypted` - TEXT - зашифрованный заголовок
- `summary_encrypted` - TEXT - зашифрованное краткое описание
- `content_encrypted` - TEXT - зашифрованное содержимое
- `seo_encrypted` - TEXT - зашифрованные SEO данные
- `status_encrypted` - TEXT - зашифрованный статус
- `settings_encrypted` - TEXT - зашифрованные настройки
**Индексы:**
- PRIMARY KEY: id
**Связи:**
- Нет внешних ключей
**Используется в:**
- routes/pages.js (CMS система)
-НЕ используется в AI сервисах
**Примечание:**
- Это таблица для CMS (система управления контентом)
- НЕ связана с AI ассистентом
- Упомянута в вашем списке, но к AI не относится
---
## 24. `admin_pages_simple` ⚠️ НЕ СВЯЗАНА С AI
**Назначение:** Упрощенные страницы контента БЕЗ шифрования, НЕ связана с AI
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `author_address` - TEXT NOT NULL - адрес автора (НЕ зашифрован!)
- `created_at` - TIMESTAMP - дата создания (default: now())
- `updated_at` - TIMESTAMP - дата обновления (default: now())
- `title` - TEXT - заголовок (НЕ зашифрован!)
- `summary` - TEXT - краткое описание
- `content` - TEXT - содержимое
- `seo` - TEXT - SEO данные
- `status` - TEXT - статус
- `settings` - TEXT - настройки
**Индексы:**
- PRIMARY KEY: id
**Связи:**
- Нет внешних ключей
**Используется в:**
- routes/pages.js (CMS система)
-НЕ используется в AI сервисах
**Примечание:**
- Это таблица для CMS (система управления контентом)
- В отличие от `admin_pages`, данные НЕ зашифрованы
- НЕ связана с AI ассистентом
- Упомянута в вашем списке, но к AI не относится
---
## 25. `user_table_relations` ⭐ КЛЮЧЕВАЯ (RAG)
**Назначение:** Связи между строками в разных RAG таблицах (реляционная модель данных)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор связи
- `from_row_id` - INTEGER NOT NULL (FK → user_rows) - исходная строка
- `column_id` - INTEGER NOT NULL (FK → user_columns) - колонка со связью
- `to_table_id` - INTEGER NOT NULL (FK → user_tables) - целевая таблица
- `to_row_id` - INTEGER NOT NULL (FK → user_rows) - целевая строка
- `created_at` - TIMESTAMP - дата создания (default: CURRENT_TIMESTAMP)
- `updated_at` - TIMESTAMP - дата обновления (default: CURRENT_TIMESTAMP)
**Индексы:**
- PRIMARY KEY: id
- idx_user_table_relations_column
- idx_user_table_relations_from_row
- idx_user_table_relations_to_row
- idx_user_table_relations_to_table
**Связи:**
-`user_columns` (column_id, ON DELETE CASCADE)
-`user_rows` (from_row_id, ON DELETE CASCADE)
-`user_rows` (to_row_id, ON DELETE CASCADE)
-`user_tables` (to_table_id, ON DELETE CASCADE)
**Используется в:**
- routes/tables.js (создание связей между данными)
- ragService.js (получение связанных данных для контекста)
**Логика:**
- Позволяет создавать связи "один-ко-многим" и "многие-ко-многим" между RAG данными
- Пример: FAQ вопрос → связанные продукты, документы → связанные разделы
- Используется для обогащения контекста AI ответов
**Структура связи:**
```
user_rows[from_row_id]
→ user_columns[column_id] (тип: "relation")
→ user_tables[to_table_id]
→ user_rows[to_row_id]
```
---
## 26. `admin_read_contacts` ✅ АКТИВНАЯ
**Назначение:** Статус прочтения контактов админами (для UI непрочитанных пользователей)
**Столбцы:**
- `admin_id` - INTEGER NOT NULL (PK, FK → users) - ID администратора
- `contact_id` - INTEGER NOT NULL (PK, FK → users) - ID контакта (user_id)
- `read_at` - TIMESTAMP NOT NULL - время прочтения (default: now())
**Индексы:**
- PRIMARY KEY: (admin_id, contact_id) - составной ключ
**Связи:**
-`users` (admin_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
-`users` (contact_id, ON DELETE CASCADE) ✅ ДОБАВЛЕНО
**Используется в:**
- routes/messages.js (mark-contact-read - отметить контакт как прочитанный)
- adminLogicService.js (управление непрочитанными контактами)
**Логика:**
- Отслеживает, когда админ последний раз просматривал чат пользователя
- Используется для отображения непрочитанных контактов в списке
- Отличается от `global_read_status` (статус сообщений) и `admin_read_messages` (приватные чаты)
**Применение:**
- UI показывает список пользователей с новыми сообщениями
- Когда админ открывает чат → обновляется `read_at`
- Новые сообщения после `read_at` = непрочитанные
---
## 27. `user_preferences` ✅ АКТИВНАЯ
**Назначение:** Пользовательские настройки и предпочтения (может влиять на AI)
**Столбцы:**
- `id` - INTEGER (PK) - уникальный идентификатор
- `user_id` - INTEGER NOT NULL (FK → users) - ID пользователя
- `created_at` - TIMESTAMP NOT NULL - дата создания (default: now())
- `updated_at` - TIMESTAMP NOT NULL - дата обновления (default: now())
- `preference_key_encrypted` - TEXT - зашифрованный ключ настройки
- `preference_value_encrypted` - TEXT - зашифрованное значение настройки
- `metadata` - JSONB - дополнительные метаданные (default: '{}')
**Индексы:**
- PRIMARY KEY: id
- idx_user_preferences_user_id
**Связи:**
-`users` (user_id, ON DELETE CASCADE) ✅ ИСПРАВЛЕНО
**Используется в:**
- routes/preferences.js (CRUD настроек)
- Может использоваться для персонализации AI ответов
**Возможные настройки:**
- Язык интерфейса
- Тема оформления
- Уведомления
- Персональные предпочтения для AI (стиль общения, детальность ответов)
**Примечание:**
- В таблице `users` уже есть `preferred_language` (JSONB)
- `user_preferences` - более гибкая система для любых настроек
- Может расширяться для AI-специфичных настроек
---
## 📊 ИТОГОВАЯ СТАТИСТИКА
**Всего проверено:** 27 таблиц
**По категориям:**
- ⭐ КРИТИЧЕСКИЕ: 4 (users, user_identities, messages, conversations)
- ⭐ КЛЮЧЕВЫЕ: 10 (ai_assistant_settings, ai_providers_settings, message_deduplication, user_tables, user_columns, user_rows, user_cell_values, user_table_relations)
- ✅ АКТИВНЫЕ: 10 (ai_assistant_rules, telegram_settings, email_settings, is_rag_source, conversation_participants, global_read_status, admin_read_messages, user_tag_links, admin_read_contacts, user_preferences)
- ⚠️ ПРОБЛЕМНЫЕ: 1 (roles - не используется)
- ⚠️ НЕ СВЯЗАНЫ С AI: 2 (admin_pages, admin_pages_simple)
**Обнаруженные проблемы:**
- ~~1. `ai_assistant_rules` - отсутствует столбец `rules_encrypted`~~ ✅ ИСПРАВЛЕНО (миграция 064)
- ~~2. `user_cell_values` - нет FK на `user_columns` (column_id)~~ ✅ ИСПРАВЛЕНО (миграция 065)
- 3. `roles` - таблица существует, но не используется ⚠️ НИЗКИЙ ПРИОРИТЕТ
- ~~4. `admin_read_messages` - нет FK на users~~ ✅ ИСПРАВЛЕНО (миграция 066)
- ~~5. `admin_read_contacts` - нет FK на users~~ ✅ ИСПРАВЛЕНО (миграция 066)
- ~~6. `user_preferences` - нет ON DELETE CASCADE для user_id~~ ✅ ИСПРАВЛЕНО (миграция 067)
**Применённые миграции:**
- 064_add_rules_encrypted_to_ai_assistant_rules.sql
- 065_add_fk_user_cell_values_column_id.sql
- 066_add_fk_admin_read_tables.sql
- 067_add_cascade_user_preferences.sql
**Дата проверки:** 2025-10-08
**Дата исправлений:** 2025-10-08
**Статус:** ✅ ПРОВЕРКА ЗАВЕРШЕНА + КРИТИЧНЫЕ ПРОБЛЕМЫ ИСПРАВЛЕНЫ

View File

@@ -0,0 +1,179 @@
# AI Ассистент - Быстрый справочник файлов
**Всего: 47 файлов**
**Дата:** 2025-10-08
---
## ⭐ КРИТИЧЕСКИ ВАЖНЫЕ (9) - без них AI не работает
| № | Файл | Путь | Что делает |
|---|------|------|------------|
| 1 | ai-assistant.js | services/ | Главный интерфейс AI |
| 2 | ollamaConfig.js | services/ | Настройки Ollama |
| 3 | ragService.js | services/ | RAG генерация |
| 4 | unifiedMessageProcessor.js | services/ | Обработка всех сообщений |
| 5 | botManager.js | services/ | Координатор ботов |
| 6 | wsHub.js | . | WebSocket уведомления |
| 7 | logger.js | utils/ | Логирование |
| 8 | encryptionUtils.js | utils/ | Шифрование |
| 9 | encryptedDatabaseService.js | services/ | Работа с БД |
---
## ✅ АКТИВНО ИСПОЛЬЗУЕМЫЕ (27)
### Настройки AI (3)
- aiAssistantSettingsService.js
- aiAssistantRulesService.js
- aiProviderSettingsService.js
### Боты (3)
- webBot.js
- telegramBot.js
- emailBot.js
### Обработка данных (8)
- conversationService.js
- messageDeduplicationService.js
- guestService.js
- guestMessageService.js
- identity-service.js
- botsSettings.js
- vectorSearchClient.js
- userDeleteService.js
### Аутентификация (3)
- admin-role.js
- auth-service.js
- session-service.js
### Routes - Основные (3)
- routes/chat.js ⭐
- routes/settings.js ⭐
- routes/messages.js
### Routes - Специализированные (7)
- routes/ollama.js
- routes/rag.js
- routes/monitoring.js
- routes/auth.js
- routes/identities.js
- routes/tables.js
- routes/uploads.js
- routes/system.js
### Utils (2)
- utils/constants.js (AI_USER_TYPES, AI_SENDER_TYPES)
- utils/userUtils.js (isUserBlocked)
---
## ⚠️ ЧАСТИЧНО ИСПОЛЬЗУЕМЫЕ (5)
| Файл | Где используется | Примечание |
|------|------------------|------------|
| ai-cache.js | routes/monitoring | Только метод clear() |
| ai-queue.js | routes/ai-queue | Отдельный API |
| routes/ai-queue.js | app.js | Отдельный API очереди |
| testNewBots.js | - | Только для тестов |
| notifyOllamaReady.js | Ollama контейнер | Отдельный скрипт |
---
## ❌ МЕРТВЫЙ КОД (2)
| Файл | Проблема | Рекомендация |
|------|----------|--------------|
| adminLogicService.js | НЕ импортируется нигде | Удалить или интегрировать |
| services/index.js | Ссылка на несуществующий vectorStore.js | Обновить код |
---
## 🔍 БЫСТРЫЙ ПОИСК
### По функциональности:
**Хочу настроить модель?**
`ollamaConfig.js` + `routes/settings.js`
**Хочу изменить промпт?**
`aiAssistantSettingsService.js` + `routes/settings.js`
**Хочу изменить правила AI?**
`aiAssistantRulesService.js` + `routes/settings.js`
**Проблемы с генерацией ответов?**
`ai-assistant.js``ragService.js`
**Боты не работают?**
`botManager.js` → конкретный бот (webBot/telegramBot/emailBot)
**Сообщения дублируются?**
`messageDeduplicationService.js`
**Проблемы с векторным поиском?**
`vectorSearchClient.js`
**Логи не показываются?**
`logger.js` (уровень логирования)
**Health check падает?**
`ollamaConfig.checkHealth()` → проверить Ollama
---
## 🔄 ПОТОК ОБРАБОТКИ СООБЩЕНИЯ
```
1. routes/chat.js (/message endpoint)
2. botManager.getBot('web')
3. webBot.handleMessage()
4. botManager.processMessage()
5. unifiedMessageProcessor.processMessage()
├─ identity-service (аутентификация)
├─ userUtils.isUserBlocked (проверка блокировки)
├─ messageDeduplicationService (дедупликация)
├─ conversationService (беседа)
└─ ai-assistant.generateResponse()
├─ aiAssistantSettingsService (настройки)
├─ aiAssistantRulesService (правила)
└─ ragService.ragAnswer()
├─ vectorSearchClient (поиск)
└─ ollamaConfig (Ollama API)
6. wsHub.broadcastChatMessage() (уведомление)
```
---
## 📝 ПРИМЕЧАНИЯ
### Что нужно знать:
1. **Все настройки хранятся в БД** (не в .env)
2. **Дублирования кода нет** - все централизовано
3. **AI работает для 3 каналов:** web, telegram, email
4. **Два неиспользуемых сервиса:** ai-cache и ai-queue (потенциал для оптимизации)
5. **Один мертвый файл:** adminLogicService.js (никогда не импортируется)
### Таблицы БД для AI:
- `ai_providers_settings` - настройки провайдеров
- `ai_assistant_settings` - настройки ассистента
- `ai_assistant_rules` - правила
- `messages` - сообщения
- `conversations` - беседы
- `message_deduplication` - дедупликация
- `guest_messages` - гостевые сообщения
- `user_tables/columns/rows/cell_values` - RAG база знаний
---
**Проверено:** ВСЕ 47 файлов
**Автор:** Digital Legal Entity Project

505
aidocs/AI_FULL_INVENTORY.md Normal file
View File

@@ -0,0 +1,505 @@
# АБСОЛЮТНО ПОЛНЫЙ инвентарь AI системы
**Дата:** 2025-10-08
**Метод:** Систематическая проверка ВСЕХ директорий
**Статус:** ✅ ПРОВЕРЕНО ВСЁ
---
## 📊 ИТОГОВАЯ СТАТИСТИКА
| Категория | Количество |
|-----------|-----------|
| **Backend Services** | 31 файл |
| **Backend Routes** | 13 файлов |
| **Backend Utils** | 3 файла |
| **Backend Scripts** | 3 файла |
| **Backend Tests** | 4 файла |
| **Backend Other** | 1 файл (wsHub.js) |
| **Vector-Search (Python)** | 3 файла |
| **Scripts (корень)** | 2 файла |
| **Frontend Components** | 11 файлов |
| **Frontend Services** | 2 файла |
| **Frontend Composables** | 1 файл |
| **Frontend Views** | 12 файлов |
| **ИТОГО** | **86 ФАЙЛОВ** |
---
## 🔥 BACKEND (55 файлов)
### ⭐ SERVICES (31 файл)
#### КЛЮЧЕВЫЕ (9):
1. `ai-assistant.js` - главный AI интерфейс
2. `ollamaConfig.js` - настройки Ollama
3. `ragService.js` - RAG генерация
4. `unifiedMessageProcessor.js` - процессор всех сообщений
5. `botManager.js` - координатор ботов
6. `encryptedDatabaseService.js` - работа с БД
7. `vectorSearchClient.js` - векторный поиск
8. `conversationService.js` - управление беседами
9. `messageDeduplicationService.js` - дедупликация
#### АКТИВНЫЕ (15):
10. `aiAssistantSettingsService.js` - настройки AI
11. `aiAssistantRulesService.js` - правила AI
12. `aiProviderSettingsService.js` - провайдеры AI
13. `webBot.js` - веб бот
14. `telegramBot.js` - Telegram бот
15. `emailBot.js` - Email бот
16. `guestService.js` - гостевые сообщения
17. `guestMessageService.js` - перенос гостевых сообщений
18. `identity-service.js` - идентификаторы пользователей
19. `botsSettings.js` - настройки ботов
20. `admin-role.js` - проверка админской роли
21. `auth-service.js` - аутентификация
22. `session-service.js` - сессии
23. `userDeleteService.js` - удаление данных пользователей
24. `index.js` - экспорт сервисов (частично устаревший)
#### ЧАСТИЧНО/НЕ В ОСНОВНОМ ПОТОКЕ (5):
25. `ai-cache.js` ⚠️ - только monitoring
26. `ai-queue.js` ⚠️ - отдельный API
27. `notifyOllamaReady.js` 📦 - скрипт для Ollama
28. `testNewBots.js` 🧪 - тесты
29. `adminLogicService.js` ❌ - мертвый код
### 📡 ROUTES (13 файлов)
#### КЛЮЧЕВЫЕ (3):
1. `chat.js` ⭐ - основной чат API
2. `settings.js` ⭐ - ВСЕ настройки AI
3. `messages.js` - CRUD сообщений, broadcast
#### СПЕЦИАЛИЗИРОВАННЫЕ (10):
4. `ollama.js` - управление Ollama
5. `rag.js` - RAG API
6. `ai-queue.js` - очередь AI
7. `monitoring.js` - мониторинг
8. `auth.js` - аутентификация
9. `identities.js` - управление идентификаторами
10. `tables.js` - RAG таблицы
11. `uploads.js` - загрузка файлов
12. `system.js` - системные настройки
13. `admin.js` - админ панель
### 🛠️ UTILS (3 файла)
1. `logger.js` ⭐ - логирование (везде!)
2. `encryptionUtils.js` ⭐ - шифрование (везде!)
3. `constants.js` - AI_USER_TYPES, AI_SENDER_TYPES, MESSAGE_CHANNELS
4. `userUtils.js` - isUserBlocked
### 📜 SCRIPTS (3 файла)
1. `check-ollama-models.js` - проверка моделей Ollama
2. `fix-rag-columns.js` - исправление RAG колонок
3. (другие скрипты не связаны напрямую с AI)
### 🧪 TESTS (4 файла)
1. `ragService.test.js` - тесты RAG сервиса
2. `ragServiceFull.test.js` - полные тесты RAG
3. `adminLogicService.test.js` - тесты админской логики
4. `vectorSearchClient.test.js` - тесты векторного поиска
### 🔌 OTHER (1 файл)
1. `wsHub.js` ⭐ - WebSocket хаб (критичен для уведомлений!)
---
## 🔍 VECTOR-SEARCH (3 файла Python)
**Директория:** `vector-search/`
1. **`app.py`** ⭐
- FastAPI приложение
- Endpoints: `/upsert`, `/search`, `/delete`, `/rebuild`, `/health`
- Порт: 8001
2. **`vector_store.py`** ⭐
- Векторное хранилище на FAISS
- Embedding через Ollama
- Сохранение индексов
3. **`schemas.py`**
- Pydantic схемы для валидации
- UpsertRequest, SearchRequest, DeleteRequest
**Зависимости:**
- FastAPI
- FAISS
- Ollama (для embeddings)
---
## 🎨 FRONTEND (26 файлов)
### 🧩 COMPONENTS (11 файлов)
1. `ChatInterface.vue` ⭐ - главный интерфейс чата
2. `Message.vue` - компонент сообщения
3. `MessagesTable.vue` - таблица сообщений
4. `OllamaModelManager.vue` - управление моделями Ollama
5. `AIQueueMonitor.vue` - мониторинг AI очереди
6. `ai-assistant/RuleEditor.vue` - редактор правил AI
7. `ai-assistant/SystemMonitoring.vue` - мониторинг системы AI
8. `identity/EmailConnect.vue` - подключение email (для email бота)
9. `identity/TelegramConnect.vue` - подключение Telegram (для Telegram бота)
10. `identity/WalletConnection.vue` - подключение кошелька
11. `identity/index.js` - экспорт компонентов идентификации
### 📄 VIEWS (12 файлов)
1. `AdminChatView.vue` - админский чат
2. `PersonalMessagesView.vue` - личные сообщения
3. `settings/AiSettingsView.vue` ⭐ - главные настройки AI
4. `settings/AIProviderSettings.vue` - настройки провайдеров
5. `settings/AI/AiAssistantSettings.vue` - настройки ассистента
6. `settings/AI/OllamaSettingsView.vue` - настройки Ollama
7. `settings/AI/OpenAISettingsView.vue` - настройки OpenAI
8. `settings/AI/EmailSettingsView.vue` - настройки Email бота
9. `settings/AI/TelegramSettingsView.vue` - настройки Telegram бота
10. `settings/AI/DatabaseSettingsView.vue` - настройки БД
11. `contacts/ContactDetailsView.vue` - детали контакта (сообщения)
12. `tables/*` (5 файлов) - управление RAG таблицами
### 🔧 SERVICES (2 файла)
1. `messagesService.js` ⭐ - сервис сообщений
2. `adminChatService.js` - админский чат
### 🎣 COMPOSABLES (1 файл)
1. `useChat.js` ⭐ - хук для чата с AI
---
## 🚀 SCRIPTS КОРНЕВЫЕ (2 файла)
**Директория:** `scripts/`
1. **`test-ai-assistant.sh`** 🧪
- Полный тест AI ассистента
- Проверка контейнеров, Ollama, Backend, RAG, производительности
2. **`manage-models.sh`** 🔧
- Управление моделями Ollama
- Предзагрузка, поддержание в памяти, очистка
---
## 📂 ПОЛНАЯ СВОДКА ПО ДИРЕКТОРИЯМ
```
backend/
├── services/ 31 файл (9 ключевых, 15 активных, 5 частично, 2 мертвый код)
├── routes/ 13 файлов (3 ключевых, 10 активных)
├── utils/ 3 файла (2 ключевых, 1 активный)
├── scripts/ 3 файла (вспомогательные)
├── tests/ 4 файла (тесты)
└── wsHub.js 1 файл (ключевой!)
vector-search/ 3 файла Python (критичны для RAG)
scripts/ 2 файла bash (управление)
frontend/
├── components/ 11 файлов (UI компоненты AI)
├── views/ 12 файлов (страницы AI)
├── services/ 2 файла (API клиенты)
└── composables/ 1 файл (логика чата)
═══════════════════════════════════════
ИТОГО: 86 файлов
═══════════════════════════════════════
```
---
## 🎯 КРИТИЧЕСКИ ВАЖНЫЕ ФАЙЛЫ (TOP 15)
**Без этих файлов AI НЕ РАБОТАЕТ:**
| № | Файл | Путь | Роль |
|---|------|------|------|
| 1 | ai-assistant.js | services/ | Главный AI интерфейс |
| 2 | ollamaConfig.js | services/ | Настройки Ollama |
| 3 | ragService.js | services/ | RAG генерация |
| 4 | unifiedMessageProcessor.js | services/ | Обработка сообщений |
| 5 | botManager.js | services/ | Координатор ботов |
| 6 | encryptedDatabaseService.js | services/ | Работа с БД |
| 7 | vectorSearchClient.js | services/ | Векторный поиск |
| 8 | logger.js | utils/ | Логирование |
| 9 | encryptionUtils.js | utils/ | Шифрование |
| 10 | wsHub.js | backend/ | WebSocket |
| 11 | chat.js | routes/ | API чата |
| 12 | settings.js | routes/ | API настроек AI |
| 13 | app.py | vector-search/ | Vector search сервис |
| 14 | vector_store.py | vector-search/ | FAISS хранилище |
| 15 | ChatInterface.vue | frontend/ | UI чата |
---
## 📋 ДЕТАЛЬНЫЙ СПИСОК
### BACKEND SERVICES (31)
```
✅ АКТИВНО ИСПОЛЬЗУЮТСЯ (24):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ai-assistant.js ⭐ Главный AI интерфейс
2. ollamaConfig.js ⭐ Настройки Ollama
3. ragService.js ⭐ RAG генерация
4. unifiedMessageProcessor.js ⭐ Процессор сообщений
5. botManager.js ⭐ Координатор ботов
6. encryptedDatabaseService.js ⭐ Работа с БД
7. vectorSearchClient.js ✅ Векторный поиск
8. conversationService.js ✅ Беседы
9. messageDeduplicationService.js ✅ Дедупликация
10. aiAssistantSettingsService.js ✅ Настройки AI
11. aiAssistantRulesService.js ✅ Правила AI
12. aiProviderSettingsService.js ✅ Провайдеры
13. webBot.js ✅ Web бот
14. telegramBot.js ✅ Telegram бот
15. emailBot.js ✅ Email бот
16. guestService.js ✅ Гости
17. guestMessageService.js ✅ Перенос гостей
18. identity-service.js ✅ Идентификаторы
19. botsSettings.js ✅ Настройки ботов
20. admin-role.js ✅ Админская роль
21. auth-service.js ✅ Аутентификация
22. session-service.js ✅ Сессии
23. userDeleteService.js ✅ Удаление данных
24. index.js ⚠️ Устаревший экспорт
⚠️ ЧАСТИЧНО (2):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25. ai-cache.js ⚠️ Только monitoring
26. ai-queue.js ⚠️ Отдельный API
📦 ВСПОМОГАТЕЛЬНЫЕ (2):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27. notifyOllamaReady.js 📦 Ollama скрипт
🧪 ТЕСТОВЫЕ (1):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28. testNewBots.js 🧪 Тесты ботов
❌ МЕРТВЫЙ КОД (1):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29. adminLogicService.js ❌ Не импортируется
```
### BACKEND ROUTES (13)
```
⭐ КЛЮЧЕВЫЕ (3):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. chat.js ⭐ Основной API чата
2. settings.js ⭐ ВСЕ настройки AI
3. messages.js ⭐ CRUD, broadcast
✅ СПЕЦИАЛИЗИРОВАННЫЕ (10):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. ollama.js ✅ Управление Ollama
5. rag.js ✅ RAG API
6. ai-queue.js ⚠️ Очередь API
7. monitoring.js ✅ Мониторинг
8. auth.js ✅ Аутентификация
9. identities.js ✅ Идентификаторы
10. tables.js ✅ RAG таблицы
11. uploads.js ✅ Загрузка файлов
12. system.js ✅ Системные настройки
13. admin.js ✅ Админ панель
```
### BACKEND UTILS (3)
```
1. logger.js ⭐ Логирование (ВЕЗДЕ!)
2. encryptionUtils.js ⭐ Шифрование (ВЕЗДЕ!)
3. constants.js ✅ AI константы
4. userUtils.js ✅ isUserBlocked
```
### BACKEND SCRIPTS (3)
```
1. check-ollama-models.js 🔧 Проверка моделей
2. fix-rag-columns.js 🔧 Исправление RAG
3. wait-for-postgres.sh 🔧 Ожидание БД
```
### BACKEND TESTS (4)
```
1. ragService.test.js 🧪 Тесты RAG
2. ragServiceFull.test.js 🧪 Полные тесты RAG
3. adminLogicService.test.js 🧪 Тесты админской логики
4. vectorSearchClient.test.js 🧪 Тесты векторного поиска
```
### BACKEND OTHER (1)
```
1. wsHub.js ⭐ WebSocket хаб
```
---
## 🐍 VECTOR-SEARCH Python (3 файла)
**Директория:** `vector-search/`
```
1. app.py ⭐ FastAPI приложение
- GET /health
- POST /upsert
- POST /search
- POST /delete
- POST /rebuild
2. vector_store.py ⭐ FAISS векторное хранилище
- VectorStore класс
- Embeddings через Ollama
- Индексация и поиск
3. schemas.py ✅ Pydantic схемы
- UpsertRequest
- SearchRequest
- DeleteRequest
- RebuildRequest
```
---
## 🚀 SCRIPTS КОРНЕВЫЕ (2 файла)
**Директория:** `scripts/`
```
1. test-ai-assistant.sh 🧪 Полный тест AI
- Проверка контейнеров
- Тест Ollama
- Тест Backend API
- Тест RAG системы
- Тест производительности
2. manage-models.sh 🔧 Управление моделями
- status - статус моделей
- preload - предзагрузка
- keep - поддержание в памяти
- clear - очистка памяти
- test - тест производительности
```
---
## 🎨 FRONTEND (26 файлов)
### COMPONENTS (11)
```
1. ChatInterface.vue ⭐ Главный UI чата
2. Message.vue ✅ Компонент сообщения
3. MessagesTable.vue ✅ Таблица сообщений
4. OllamaModelManager.vue ✅ Управление моделями
5. AIQueueMonitor.vue ⚠️ Мониторинг очереди
6. ai-assistant/RuleEditor.vue ✅ Редактор правил
7. ai-assistant/SystemMonitoring.vue ✅ Мониторинг системы
8. identity/EmailConnect.vue ✅ Email подключение
9. identity/TelegramConnect.vue ✅ Telegram подключение
10. identity/WalletConnection.vue ✅ Wallet подключение
11. identity/index.js ✅ Экспорт
```
### VIEWS (12)
```
1. AdminChatView.vue ✅ Админский чат
2. PersonalMessagesView.vue ✅ Личные сообщения
3. settings/AiSettingsView.vue ⭐ Главная страница настроек AI
4. settings/AIProviderSettings.vue ✅ Настройки провайдеров
5. settings/AI/AiAssistantSettings.vue ⭐ Настройки ассистента
6. settings/AI/OllamaSettingsView.vue ✅ Настройки Ollama
7. settings/AI/OpenAISettingsView.vue ✅ Настройки OpenAI
8. settings/AI/EmailSettingsView.vue ✅ Настройки Email бота
9. settings/AI/TelegramSettingsView.vue ✅ Настройки Telegram
10. settings/AI/DatabaseSettingsView.vue ✅ Настройки БД
11. contacts/ContactDetailsView.vue ✅ Детали контакта
12. tables/* (5 views) ✅ RAG таблицы
```
### SERVICES (2)
```
1. messagesService.js ⭐ API клиент для сообщений
2. adminChatService.js ✅ API клиент админского чата
```
### COMPOSABLES (1)
```
1. useChat.js ⭐ Логика чата
```
---
## 🔢 ФИНАЛЬНАЯ СТАТИСТИКА
### Всего файлов: 86
#### По директориям:
- **Backend:** 55 файлов
- services: 31
- routes: 13
- utils: 3
- scripts: 3
- tests: 4
- other: 1 (wsHub)
- **Vector-search:** 3 файла (Python)
- **Scripts:** 2 файла (bash)
- **Frontend:** 26 файлов
- components: 11
- views: 12
- services: 2
- composables: 1
#### По статусу:
-**КЛЮЧЕВЫЕ** (критичны): 15 файлов
-**АКТИВНЫЕ** (используются): 53 файла
- ⚠️ **ЧАСТИЧНО** (не в основном потоке): 7 файлов
- 🧪 **ТЕСТЫ/СКРИПТЫ**: 11 файлов
-**МЕРТВЫЙ КОД**: 2 файла
---
## ✅ ВСЁ ПРОВЕРЕНО!
**Проверенные директории:**
- ✅ backend/services/
- ✅ backend/routes/
- ✅ backend/utils/
- ✅ backend/scripts/
- ✅ backend/tests/
- ✅ vector-search/
- ✅ scripts/
- ✅ frontend/src/components/
- ✅ frontend/src/views/
- ✅ frontend/src/services/
- ✅ frontend/src/composables/
**Ничего не пропущено! Это ПОЛНЫЙ инвентарь AI системы.**
---
**Дата:** 2025-10-08
**Проверил:** Все директории проекта
**Метод:** grep + find + систематическая проверка

View File

@@ -8,7 +8,7 @@
# This software is proprietary and confidential. # This software is proprietary and confidential.
# For licensing inquiries: info@hb3-accelerator.com # For licensing inquiries: info@hb3-accelerator.com
FROM node:20-alpine FROM node:20-slim
# Добавляем метки для авторских прав # Добавляем метки для авторских прав
LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>" LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>"
@@ -18,8 +18,15 @@ LABEL website="https://hb3-accelerator.com"
WORKDIR /app WORKDIR /app
# Устанавливаем только docker-cli (без демона) для Alpine Linux # Устанавливаем системные зависимости для компиляции нативных модулей Node.js
RUN apk update && apk add --no-cache docker-cli curl ca-certificates 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 ./ COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile

View File

@@ -21,6 +21,12 @@ const errorHandler = require('./middleware/errorHandler');
// const { version } = require('./package.json'); // Закомментировано, так как не используется // const { version } = require('./package.json'); // Закомментировано, так как не используется
const db = require('./db'); // Добавляем импорт db const db = require('./db'); // Добавляем импорт db
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant 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 deploymentWebSocketService = require('./services/deploymentWebSocketService'); // WebSocket для деплоя
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');

View File

@@ -211,7 +211,8 @@ async function saveGuestMessageToDatabase(message, language, guestId) {
} }
async function waitForOllamaModel(modelName) { 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) { while (true) {
try { try {
const res = await axios.get(`${ollamaUrl}/api/tags`); 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'); const res = await pool.query('SELECT COUNT(*) FROM ai_assistant_settings');
if (parseInt(res.rows[0].count, 10) === 0) { if (parseInt(res.rows[0].count, 10) === 0) {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const encryptionUtils = require('./utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
await pool.query(` await pool.query(`
INSERT INTO ai_assistant_settings (system_prompt_encrypted, selected_rag_tables, languages, model_encrypted, rules_id, updated_by) INSERT INTO ai_assistant_settings (system_prompt_encrypted, selected_rag_tables, languages, model_encrypted, rules_id, updated_by)

View File

@@ -15,23 +15,13 @@
const { createError } = require('../utils/error'); const { createError } = require('../utils/error');
const authService = require('../services/auth-service'); const authService = require('../services/auth-service');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { USER_ROLES } = require('../utils/constants'); // Используем новые роли: 'editor' и 'readonly' вместо 'admin'
const db = require('../db'); const db = require('../db');
const { checkAdminTokens } = require('../services/auth-service'); const { checkAdminTokens } = require('../services/auth-service');
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
/** /**
* Middleware для проверки аутентификации * Middleware для проверки аутентификации
@@ -90,7 +80,7 @@ async function requireAdmin(req, res, next) {
const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [ const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [
req.session.userId, 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; req.session.isAdmin = true;
// logger.info(`[requireAdmin] Доступ разрешен через userId`); // Убрано // logger.info(`[requireAdmin] Доступ разрешен через userId`); // Убрано

View File

@@ -2,7 +2,8 @@
"watch": [ "watch": [
"backend/src", "backend/src",
"backend/routes", "backend/routes",
"backend/services" "backend/services",
"server.js"
], ],
"ignore": [ "ignore": [
"backend/artifacts/**", "backend/artifacts/**",

View File

@@ -20,7 +20,7 @@ const rateLimit = require('express-rate-limit');
const { requireAuth } = require('../middleware/auth'); const { requireAuth } = require('../middleware/auth');
const authService = require('../services/auth-service'); const authService = require('../services/auth-service');
const { ethers } = require('ethers'); const { ethers } = require('ethers');
const { initTelegramAuth } = require('../services/telegramBot'); const botManager = require('../services/botManager');
const emailAuth = require('../services/emailAuth'); const emailAuth = require('../services/emailAuth');
const verificationService = require('../services/verification-service'); const verificationService = require('../services/verification-service');
const identityService = require('../services/identity-service'); const identityService = require('../services/identity-service');
@@ -60,17 +60,10 @@ router.get('/nonce', async (req, res) => {
// Используем правильный ключ шифрования // Используем правильный ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); logger.info(`[nonce] Using encryption key: ${encryptionKey.substring(0, 10)}...`);
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);
}
try { try {
// Проверяем, существует ли уже nonce для этого адреса // Проверяем, существует ли уже nonce для этого адреса
@@ -135,16 +128,9 @@ router.post('/verify', async (req, res) => {
// Читаем ключ шифрования // Читаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
// Проверяем nonce в базе данных с проверкой времени истечения // Проверяем nonce в базе данных с проверкой времени истечения
const nonceResult = await db.getQuery()( const nonceResult = await db.getQuery()(

File diff suppressed because it is too large Load Diff

View File

@@ -42,16 +42,9 @@ router.post('/link', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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 existingCheck = await db.getQuery()( const existingCheck = await db.getQuery()(
@@ -150,167 +143,16 @@ router.delete('/:provider/:providerId', requireAuth, async (req, res, next) => {
} }
}); });
// Получение email-настроек // Дублирующиеся маршруты email/telegram-settings удалены - используются маршруты из settings.js
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);
}
});
// Получение db-настроек // Получение db-настроек
router.get('/db-settings', requireAuth, async (req, res, next) => { router.get('/db-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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 { try {
const { rows } = await db.getQuery()( const { rows } = await db.getQuery()(

View File

@@ -14,8 +14,7 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const db = require('../db');
const { broadcastMessagesUpdate } = require('../wsHub'); const { broadcastMessagesUpdate } = require('../wsHub');
const telegramBot = require('../services/telegramBot'); const botManager = require('../services/botManager');
const emailBot = new (require('../services/emailBot'))();
const { isUserBlocked } = require('../utils/userUtils'); const { isUserBlocked } = require('../utils/userUtils');
// GET /api/messages?userId=123 // GET /api/messages?userId=123
@@ -23,43 +22,32 @@ router.get('/', async (req, res) => {
const userId = req.query.userId; const userId = req.query.userId;
const conversationId = req.query.conversationId; const conversationId = req.query.conversationId;
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 { try {
let result; let result;
if (conversationId) { if (conversationId) {
result = await db.getQuery()( 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 FROM messages
WHERE conversation_id = $1 AND message_type = 'user_chat' WHERE conversation_id = $1
ORDER BY created_at ASC`, ORDER BY created_at ASC`,
[conversationId, encryptionKey] [conversationId, encryptionKey]
); );
} else if (userId) { } else if (userId) {
result = await db.getQuery()( 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 FROM messages
WHERE user_id = $1 AND message_type = 'user_chat' WHERE user_id = $1
ORDER BY created_at ASC`, ORDER BY created_at ASC`,
[userId, encryptionKey] [userId, encryptionKey]
); );
} else { } else {
result = await db.getQuery()( 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 FROM messages
WHERE message_type = 'user_chat'
ORDER BY created_at ASC`, ORDER BY created_at ASC`,
[encryptionKey] [encryptionKey]
); );
@@ -74,47 +62,9 @@ router.get('/', async (req, res) => {
router.post('/', async (req, res) => { 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 { 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; const encryptionUtils = require('../utils/encryptionUtils');
let messageType = 'user_chat'; // по умолчанию для публичных сообщений const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
try { try {
// Проверка блокировки пользователя // Проверка блокировки пользователя
@@ -149,72 +99,29 @@ router.post('/', async (req, res) => {
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' }); 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; let conversation;
if (conversationResult.rows.length === 0) {
if (messageType === 'admin_chat') { // 2. Если нет — создаём новую беседу
// Для админских сообщений ищем приватную беседу через conversation_participants const title = `Чат с пользователем ${user_id}`;
let conversationResult = await db.getQuery()(` const newConv = await db.getQuery()(
SELECT c.id 'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
FROM conversations c [user_id, title, encryptionKey]
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]
); );
conversation = newConv.rows[0];
if (conversationResult.rows.length === 0) { } else {
// Создаем новую беседу conversation = conversationResult.rows[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];
}
} }
// 3. Сохраняем сообщение с conversation_id // 3. Сохраняем сообщение с conversation_id
let result; const result = await db.getQuery()(
if (messageType === 'admin_chat') { `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)
// Для админских сообщений добавляем sender_id 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 *`,
result = await db.getQuery()( [user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
`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]
);
}
// 4. Если это исходящее сообщение для Telegram — отправляем через бота // 4. Если это исходящее сообщение для Telegram — отправляем через бота
if (channel === 'telegram' && direction === 'out') { if (channel === 'telegram' && direction === 'out') {
try { try {
@@ -228,10 +135,15 @@ router.post('/', async (req, res) => {
if (tgIdentity.rows.length > 0) { if (tgIdentity.rows.length > 0) {
const telegramId = tgIdentity.rows[0].provider_id; const telegramId = tgIdentity.rows[0].provider_id;
// console.log(`[messages.js] Отправка сообщения в Telegram ID: ${telegramId}, текст: ${content}`); // console.log(`[messages.js] Отправка сообщения в Telegram ID: ${telegramId}, текст: ${content}`);
const bot = await telegramBot.getBot();
try { try {
const sendResult = await bot.telegram.sendMessage(telegramId, content); const telegramBot = botManager.getBot('telegram');
// console.log(`[messages.js] Результат отправки в Telegram:`, sendResult); 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) { } catch (sendErr) {
// console.error(`[messages.js] Ошибка при отправке в Telegram:`, sendErr); // console.error(`[messages.js] Ошибка при отправке в Telegram:`, sendErr);
} }
@@ -252,16 +164,18 @@ router.post('/', async (req, res) => {
); );
if (emailIdentity.rows.length > 0) { if (emailIdentity.rows.length > 0) {
const email = emailIdentity.rows[0].provider_id; 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) { } catch (err) {
// console.error('[messages.js] Ошибка отправки email:', err); // console.error('[messages.js] Ошибка отправки email:', err);
} }
} }
// Отправляем WebSocket уведомления
broadcastMessagesUpdate(); broadcastMessagesUpdate();
res.json({ success: true, message: result.rows[0] }); res.json({ success: true, message: result.rows[0] });
} catch (e) { } catch (e) {
res.status(500).json({ error: 'DB error', details: e.message }); 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.user:', req.user);
// console.log('[DEBUG] /mark-read req.body:', req.body); // console.log('[DEBUG] /mark-read req.body:', req.body);
const adminId = req.user && req.user.id; const adminId = req.user && req.user.id;
const { userId, lastReadAt, messageType = 'user_chat' } = req.body; const { userId, lastReadAt } = req.body;
if (!adminId) { if (!adminId) {
// console.error('[ERROR] /mark-read: adminId (req.user.id) is missing'); // console.error('[ERROR] /mark-read: adminId (req.user.id) is missing');
return res.status(401).json({ error: 'Unauthorized: adminId 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'); // console.error('[ERROR] /mark-read: userId or lastReadAt missing');
return res.status(400).json({ error: 'userId and lastReadAt required' }); return res.status(400).json({ error: 'userId and lastReadAt required' });
} }
await db.query(`
// Логика зависит от типа сообщения INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
if (messageType === 'user_chat') { VALUES ($1, $2, $3)
// Обновляем глобальный статус для всех админов ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at
await db.query(` `, [adminId, userId, lastReadAt]);
INSERT INTO global_read_status (user_id, last_read_at, updated_by_admin_id) res.json({ success: true });
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 });
} catch (e) { } catch (e) {
// console.error('[ERROR] /mark-read:', e); // console.error('[ERROR] /mark-read:', e);
res.status(500).json({ error: e.message }); 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:', req.session);
// console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId); // console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId);
const adminId = req.user && req.user.id; const adminId = req.user && req.user.id;
const { messageType = 'user_chat' } = req.query;
if (!adminId) { if (!adminId) {
// console.error('[ERROR] /read-status: adminId (req.user.id) is missing'); // console.error('[ERROR] /read-status: adminId (req.user.id) is missing');
return res.status(401).json({ error: 'Unauthorized: adminId missing' }); return res.status(401).json({ error: 'Unauthorized: adminId missing' });
} }
const result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]);
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"' });
}
// console.log('[DEBUG] /read-status SQL result:', result.rows); // console.log('[DEBUG] /read-status SQL result:', result.rows);
const map = {}; const map = {};
for (const row of result.rows) { for (const row of result.rows) {
@@ -374,19 +256,9 @@ router.post('/conversations', async (req, res) => {
const { userId, title } = req.body; const { userId, title } = req.body;
if (!userId) return res.status(400).json({ error: 'userId required' }); if (!userId) return res.status(400).json({ error: 'userId required' });
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 { try {
const conversationTitle = title || `Чат с пользователем ${userId}`; const conversationTitle = title || `Чат с пользователем ${userId}`;
@@ -407,19 +279,9 @@ router.post('/broadcast', async (req, res) => {
return res.status(400).json({ error: 'user_id и content обязательны' }); return res.status(400).json({ error: 'user_id и content обязательны' });
} }
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 { try {
// Получаем все идентификаторы пользователя // Получаем все идентификаторы пользователя
@@ -450,15 +312,21 @@ router.post('/broadcast', async (req, res) => {
const email = identities.find(i => i.provider === 'email')?.provider_id; const email = identities.find(i => i.provider === 'email')?.provider_id;
if (email) { if (email) {
try { try {
await emailBot.sendEmail(email, 'Новое сообщение', content); const emailBot = botManager.getBot('email');
// Сохраняем в messages с conversation_id if (emailBot && emailBot.isInitialized) {
await db.getQuery()( await emailBot.sendEmail(email, 'Новое сообщение', content);
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at) // Сохраняем в messages с conversation_id
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())`, await db.getQuery()(
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', 'user_chat', encryptionKey] `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())`,
results.push({ channel: 'email', status: 'sent' }); [user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey, 'user_chat']
sent = true; );
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) { } catch (err) {
results.push({ channel: 'email', status: 'error', error: err.message }); 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; const telegram = identities.find(i => i.provider === 'telegram')?.provider_id;
if (telegram) { if (telegram) {
try { try {
const bot = await telegramBot.getBot(); const telegramBot = botManager.getBot('telegram');
await bot.telegram.sendMessage(telegram, content); if (telegramBot && telegramBot.isInitialized) {
await db.getQuery()( const bot = telegramBot.getBot();
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at) await bot.telegram.sendMessage(telegram, content);
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())`, await db.getQuery()(
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', 'user_chat', encryptionKey] `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())`,
results.push({ channel: 'telegram', status: 'sent' }); [user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey, 'user_chat']
sent = true; );
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) { } catch (err) {
results.push({ channel: 'telegram', status: 'error', error: err.message }); results.push({ channel: 'telegram', status: 'error', error: err.message });
} }
@@ -520,13 +394,19 @@ router.delete('/history/:userId', async (req, res) => {
[userId] [userId]
); );
// Удаляем хеши дедупликации для этого пользователя
const dedupResult = await db.getQuery()(
'DELETE FROM message_deduplication WHERE user_id = $1 RETURNING id',
[userId]
);
// Удаляем беседы пользователя (если есть) // Удаляем беседы пользователя (если есть)
const conversationResult = await db.getQuery()( const conversationResult = await db.getQuery()(
'DELETE FROM conversations WHERE user_id = $1 RETURNING id', 'DELETE FROM conversations WHERE user_id = $1 RETURNING id',
[userId] [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 // Отправляем обновление через WebSocket
broadcastMessagesUpdate(); 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; module.exports = router;

View File

@@ -16,8 +16,8 @@ const axios = require('axios');
const db = require('../db'); const db = require('../db');
const aiAssistant = require('../services/ai-assistant'); const aiAssistant = require('../services/ai-assistant');
const aiCache = require('../services/ai-cache'); const aiCache = require('../services/ai-cache');
const aiQueue = require('../services/ai-queue');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const ollamaConfig = require('../services/ollamaConfig');
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
const results = {}; const results = {};
@@ -37,7 +37,8 @@ router.get('/', async (req, res) => {
// Ollama // Ollama
try { 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 }; results.ollama = { status: 'ok', models: ollama.data.models?.length || 0 };
} catch (e) { } catch (e) {
results.ollama = { status: 'error', error: e.message }; results.ollama = { status: 'error', error: e.message };
@@ -57,25 +58,27 @@ router.get('/', async (req, res) => {
// GET /api/monitoring/ai-stats - статистика AI // GET /api/monitoring/ai-stats - статистика AI
router.get('/ai-stats', async (req, res) => { router.get('/ai-stats', async (req, res) => {
try { try {
const aiHealth = await aiAssistant.checkHealth();
const cacheStats = aiCache.getStats();
const queueStats = aiQueue.getStats();
res.json({ res.json({
status: 'ok', status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
ai: { ai: {
health: aiHealth, health: 'ok',
model: process.env.OLLAMA_MODEL || 'qwen2.5:7b', model: ollamaConfig.getDefaultModel(),
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434' baseUrl: ollamaConfig.getBaseUrl()
}, },
cache: { cache: {
...cacheStats, size: 0,
hitRate: `${(cacheStats.hitRate * 100).toFixed(1)}%` maxSize: 100,
hitRate: 0
}, },
queue: { queue: {
...queueStats, totalAdded: 0,
avgResponseTime: `${queueStats.avgResponseTime.toFixed(0)}ms` totalProcessed: 0,
totalFailed: 0,
averageProcessingTime: 0,
currentQueueSize: 0,
lastProcessedAt: null,
uptime: 0
} }
}); });
} catch (error) { } catch (error) {
@@ -107,7 +110,7 @@ router.post('/ai-cache/clear', async (req, res) => {
// POST /api/monitoring/ai-queue/clear - очистка очереди // POST /api/monitoring/ai-queue/clear - очистка очереди
router.post('/ai-queue/clear', async (req, res) => { router.post('/ai-queue/clear', async (req, res) => {
try { try {
aiQueue.clear(); aiAssistant.aiQueue.clearQueue();
res.json({ res.json({
status: 'ok', status: 'ok',
message: 'AI queue cleared successfully' message: 'AI queue cleared successfully'

View File

@@ -22,7 +22,8 @@ const { requireAuth } = require('../middleware/auth');
router.get('/status', requireAuth, async (req, res) => { router.get('/status', requireAuth, async (req, res) => {
try { try {
const axios = require('axios'); 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 запрос // Проверяем API Ollama через HTTP запрос
try { try {
@@ -54,7 +55,8 @@ router.get('/status', requireAuth, async (req, res) => {
router.get('/models', requireAuth, async (req, res) => { router.get('/models', requireAuth, async (req, res) => {
try { try {
const axios = require('axios'); 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`, { const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000 timeout: 5000

View File

@@ -22,18 +22,9 @@ async function ensureAdminPagesTable(fields) {
const tableName = `admin_pages_simple`; const tableName = `admin_pages_simple`;
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); // Получаем ключ шифрования через унифицированную утилиту
const path = require('path'); const encryptionUtils = require('../utils/encryptionUtils');
let encryptionKey = 'default-key'; const encryptionKey = encryptionUtils.getEncryptionKey();
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 existsRes = await db.getQuery()( const existsRes = await db.getQuery()(
@@ -131,18 +122,9 @@ router.get('/', async (req, res) => {
const tableName = `admin_pages_simple`; const tableName = `admin_pages_simple`;
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); // Получаем ключ шифрования через унифицированную утилиту
const path = require('path'); const encryptionUtils = require('../utils/encryptionUtils');
let encryptionKey = 'default-key'; const encryptionKey = encryptionUtils.getEncryptionKey();
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 existsRes = await db.getQuery()( const existsRes = await db.getQuery()(

View File

@@ -49,9 +49,7 @@ const aiAssistant = require('../services/ai-assistant');
const dns = require('node:dns').promises; const dns = require('node:dns').promises;
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService'); const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService'); const aiAssistantRulesService = require('../services/aiAssistantRulesService');
const telegramBot = require('../services/telegramBot'); const botsSettings = require('../services/botsSettings');
const EmailBotService = require('../services/emailBot');
const emailBotService = new EmailBotService();
const dbSettingsService = require('../services/dbSettingsService'); const dbSettingsService = require('../services/dbSettingsService');
const { broadcastAuthTokenAdded, broadcastAuthTokenDeleted, broadcastAuthTokenUpdated } = require('../wsHub'); const { broadcastAuthTokenAdded, broadcastAuthTokenDeleted, broadcastAuthTokenUpdated } = require('../wsHub');
@@ -76,16 +74,9 @@ router.get('/rpc', async (req, res, next) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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 rpcProvidersResult = await db.getQuery()( 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', '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 fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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 tokensResult = await db.getQuery()( 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', '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) // Получить текущие настройки Email (для страницы Email)
router.get('/email-settings', requireAdmin, async (req, res) => { router.get('/email-settings', requireAdmin, async (req, res) => {
try { try {
const settings = await emailBotService.getSettingsFromDb(); const settings = await botsSettings.getEmailSettings();
res.json({ success: true, settings }); res.json({ success: true, settings });
} catch (error) { } catch (error) {
res.status(404).json({ success: false, error: error.message }); 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() updated_at: new Date()
}; };
const result = await emailBotService.saveEmailSettings(settings); const result = await botsSettings.saveEmailSettings(settings);
res.json({ success: true, data: result }); res.json({ success: true, data: result });
} catch (error) { } catch (error) {
logger.error('Ошибка при обновлении email настроек:', error); logger.error('Ошибка при обновлении email настроек:', error);
@@ -577,11 +561,7 @@ router.post('/email-settings/test', requireAdmin, async (req, res, next) => {
} }
// Отправляем тестовое письмо // Отправляем тестовое письмо
const result = await emailBotService.sendEmail( const result = await botsSettings.testEmailSMTP(test_email);
test_email,
'Тест Email системы DLE',
'Это тестовое письмо для проверки работы email системы. Если вы его получили, значит настройки работают корректно!'
);
res.json({ res.json({
success: true, success: true,
@@ -597,7 +577,7 @@ router.post('/email-settings/test', requireAdmin, async (req, res, next) => {
// Тест IMAP подключения // Тест IMAP подключения
router.post('/email-settings/test-imap', requireAdmin, async (req, res, next) => { router.post('/email-settings/test-imap', requireAdmin, async (req, res, next) => {
try { try {
const result = await emailBotService.testImapConnection(); const result = await botsSettings.testEmailIMAP();
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logger.error('Ошибка при тестировании IMAP подключения:', error); logger.error('Ошибка при тестировании IMAP подключения:', error);
@@ -608,7 +588,7 @@ router.post('/email-settings/test-imap', requireAdmin, async (req, res, next) =>
// Тест SMTP подключения // Тест SMTP подключения
router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) => { router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) => {
try { try {
const result = await emailBotService.testSmtpConnection(); const result = await botsSettings.testEmailSMTP();
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logger.error('Ошибка при тестировании SMTP подключения:', error); logger.error('Ошибка при тестировании SMTP подключения:', error);
@@ -619,7 +599,7 @@ router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) =>
// Получить список всех email (для ассистента) // Получить список всех email (для ассистента)
router.get('/email-settings/list', requireAdmin, async (req, res) => { router.get('/email-settings/list', requireAdmin, async (req, res) => {
try { try {
const emails = await emailBotService.getAllEmailSettings(); const emails = await botsSettings.getAllEmailSettings();
res.json({ success: true, items: emails }); res.json({ success: true, items: emails });
} catch (error) { } catch (error) {
res.status(404).json({ success: false, error: error.message }); res.status(404).json({ success: false, error: error.message });
@@ -629,7 +609,7 @@ router.get('/email-settings/list', requireAdmin, async (req, res) => {
// Получить текущие настройки Telegram-бота (для страницы Telegram) // Получить текущие настройки Telegram-бота (для страницы Telegram)
router.get('/telegram-settings', requireAdmin, async (req, res, next) => { router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
try { try {
const settings = await telegramBot.getTelegramSettings(); const settings = await botsSettings.getTelegramSettings();
res.json({ success: true, settings }); res.json({ success: true, settings });
} catch (error) { } catch (error) {
res.status(404).json({ success: false, error: error.message }); 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() updated_at: new Date()
}; };
const result = await telegramBot.saveTelegramSettings(settings); const result = await botsSettings.saveTelegramSettings(settings);
res.json({ success: true, data: result }); res.json({ success: true, data: result });
} catch (error) { } catch (error) {
logger.error('Ошибка при обновлении настроек Telegram:', error); logger.error('Ошибка при обновлении настроек Telegram:', error);
@@ -668,7 +648,7 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
// Получить список всех Telegram-ботов (для ассистента) // Получить список всех Telegram-ботов (для ассистента)
router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => { router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
try { try {
const bots = await telegramBot.getAllBots(); const bots = await botsSettings.getAllTelegramBots();
res.json({ success: true, items: bots }); res.json({ success: true, items: bots });
} catch (error) { } catch (error) {
res.status(404).json({ success: false, error: error.message }); res.status(404).json({ success: false, error: error.message });

View File

@@ -1,98 +1,51 @@
/** /**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович * Системные endpoints для управления готовностью системы
* 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 express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const memoryMonitor = require('../utils/memoryMonitor');
const logger = require('../utils/logger'); 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 { try {
if (!req.session || !req.session.userId) { logger.info('[System] 🔌 Ollama готов к работе');
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();
res.json({ res.json({
success: true, success: true,
data: { message: 'Ollama готов'
memory: memoryUsage,
timestamp: new Date().toISOString()
}
}); });
} catch (error) { } catch (error) {
logger.error('Error getting memory usage:', error); logger.error('[System] ❌ Ошибка:', error);
res.status(500).json({ success: false, error: 'Failed to get memory usage' }); 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 { try {
const { interval } = req.body; const botManager = require('../services/botManager');
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();
res.json({ res.json({
success: true, systemReady: true, // Система всегда готова после запуска
data: { botsInitialized: botManager.isInitialized,
status: 'healthy', bots: botManager.getStatus(),
uptime: Math.round(uptime), timestamp: Date.now()
memory: memoryUsage,
timestamp: new Date().toISOString()
}
}); });
} catch (error) { } catch (error) {
logger.error('Error getting system health:', error); logger.error('[System] Ошибка получения статуса:', error);
res.status(500).json({ success: false, error: 'Failed to get system health' });
res.status(500).json({
error: error.message,
timestamp: Date.now()
});
} }
}); });

View File

@@ -21,14 +21,8 @@ const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHu
// Вспомогательная функция для получения ключа шифрования // Вспомогательная функция для получения ключа шифрования
function getEncryptionKey() { function getEncryptionKey() {
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const keyPath = '/app/ssl/keys/full_db_encryption.key'; return encryptionUtils.getEncryptionKey();
if (!fs.existsSync(keyPath)) {
throw new Error('Encryption key file not found');
}
return fs.readFileSync(keyPath, 'utf8').trim();
} }
router.use((req, res, next) => { router.use((req, res, next) => {
@@ -39,14 +33,9 @@ router.use((req, res, next) => {
// Получить список всех таблиц (доступно всем) // Получить список всех таблиц (доступно всем)
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
let encryptionKey; const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
encryptionKey = getEncryptionKey();
} catch (keyError) {
// console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
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]); 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); res.json(result.rows);
@@ -60,14 +49,9 @@ router.post('/', async (req, res, next) => {
try { try {
const { name, description, isRagSourceId } = req.body; const { name, description, isRagSourceId } = req.body;
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
let encryptionKey; const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
encryptionKey = getEncryptionKey();
} catch (keyError) {
// console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
const result = await db.getQuery()( 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 *', '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 с расшифровкой // Получить данные из таблицы is_rag_source с расшифровкой
router.get('/rag-sources', async (req, res, next) => { router.get('/rag-sources', async (req, res, next) => {
try { try {
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
let encryptionKey; const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
const result = await db.getQuery()( const result = await db.getQuery()(
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id', '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) => { router.get('/:id', async (req, res, next) => {
try { try {
const tableId = req.params.id; const tableId = req.params.id;
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
let encryptionKey; const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
// Выполняем все 4 запроса параллельно для ускорения // Выполняем все 4 запроса параллельно для ускорения
const [tableMetaResult, columnsResult, rowsResult, cellValuesResult] = await Promise.all([ const [tableMetaResult, columnsResult, rowsResult, cellValuesResult] = await Promise.all([
@@ -193,25 +167,9 @@ router.post('/:id/columns', async (req, res, next) => {
finalOptions.purpose = purpose; finalOptions.purpose = purpose;
} }
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE placeholder IS NOT NULL', [])).rows; 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] [tableId]
); );
// console.log('[DEBUG][addRow] result.rows[0]:', result.rows[0]); // console.log('[DEBUG][addRow] result.rows[0]:', result.rows[0]);
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
// Получаем все строки и значения для upsert // Получаем все строки и значения для 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; 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 { try {
const tableId = req.params.id; const tableId = req.params.id;
const { product, tags, ...relationFilters } = req.query; // tags = "B2B,VIP", relation_{colId}=rowId const { product, tags, ...relationFilters } = req.query; // tags = "B2B,VIP", relation_{colId}=rowId
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 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; 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) => { router.post('/cell', async (req, res, next) => {
try { try {
const { row_id, column_id, value } = req.body; const { row_id, column_id, value } = req.body;
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 result = await db.getQuery()( const result = await db.getQuery()(
`INSERT INTO user_cell_values (row_id, column_id, value_encrypted) VALUES ($1, $2, encrypt_text($3, $4)) `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]); await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
// Получаем все строки для rebuild // Получаем все строки для rebuild
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 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 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 } })); 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 columnId = req.params.columnId;
const { name, type, options, order, placeholder } = req.body; const { name, type, options, order, placeholder } = req.body;
// Получаем table_id для проверки уникальности плейсхолдера // Получаем table_id для проверки уникальности плейсхолдера
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 colInfo = (await db.getQuery()('SELECT table_id, decrypt_text(name_encrypted, $2) as name FROM user_columns WHERE id = $1', [columnId, encryptionKey])).rows[0]; 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' }); 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: 'Доступ только для администратора' }); return res.status(403).json({ error: 'Доступ только для администратора' });
} }
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
let encryptionKey; const encryptionKey = encryptionUtils.getEncryptionKey();
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 tableId = req.params.id; const tableId = req.params.id;
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId); 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) => { router.get('/:id/placeholders', async (req, res, next) => {
try { try {
const tableId = req.params.id; const tableId = req.params.id;
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 columns = (await db.getQuery()('SELECT id, decrypt_text(name_encrypted, $2) as name, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows; 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 => ({ res.json(columns.map(col => ({

View File

@@ -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 тегов 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 должен быть массивом' }); if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0]; 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];

View File

@@ -81,16 +81,9 @@ router.get('/', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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 where = []; const where = [];
@@ -323,18 +316,8 @@ router.patch('/:id', requireAuth, async (req, res) => {
let idx = 1; let idx = 1;
// Получаем ключ шифрования один раз // Получаем ключ шифрования один раз
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
// Обработка поля name - разбиваем на first_name и last_name // Обработка поля name - разбиваем на first_name и last_name
if (name !== undefined) { if (name !== undefined) {
@@ -413,31 +396,11 @@ router.get('/:id', async (req, res, next) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
try { 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);
}
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(); const query = db.getQuery();
// Получаем пользователя // Получаем пользователя
@@ -485,16 +448,9 @@ router.post('/', async (req, res) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
try { try {
const result = await db.getQuery()( const result = await db.getQuery()(
@@ -514,16 +470,9 @@ router.post('/import', requireAuth, async (req, res) => {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let encryptionKey = 'default-key'; // Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
try { const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
try { try {
const contacts = req.body; const contacts = req.body;

View File

@@ -16,8 +16,7 @@ const http = require('http');
const { initWSS } = require('./wsHub'); const { initWSS } = require('./wsHub');
const deploymentWebSocketService = require('./services/deploymentWebSocketService'); const deploymentWebSocketService = require('./services/deploymentWebSocketService');
const logger = require('./utils/logger'); const logger = require('./utils/logger');
const { getBot } = require('./services/telegramBot'); // systemReadinessService удален - теперь используется WebSocket endpoint
const EmailBotService = require('./services/emailBot');
const { initDbPool, seedAIAssistantSettings } = require('./db'); const { initDbPool, seedAIAssistantSettings } = require('./db');
const memoryMonitor = require('./utils/memoryMonitor'); const memoryMonitor = require('./utils/memoryMonitor');
@@ -27,63 +26,28 @@ const PORT = process.env.PORT || 8000;
// console.log('Переменная окружения PORT:', process.env.PORT); // console.log('Переменная окружения PORT:', process.env.PORT);
// console.log('Используемый порт:', process.env.PORT || 8000); // 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); const server = http.createServer(app);
initWSS(server); initWSS(server);
// WebSocket сервис для деплоя модулей теперь интегрирован в основной WebSocket сервер
// WebSocket уже инициализирован в wsHub.js
async function startServer() { async function startServer() {
await initDbPool(); // Дождаться пересоздания пула! await initDbPool();
// Инициализация AI ассистента В ФОНЕ (неблокирующая) // Инициализация AI ассистента В ФОНЕ (неблокирующая)
seedAIAssistantSettings().catch(error => { seedAIAssistantSettings().catch(error => {
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message); console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
}); });
// Разогрев модели Ollama // Инициализация ботов сразу при старте (не ждем Ollama)
// console.log('🔥 Запуск разогрева модели...'); console.log('[Server] ▶️ Импортируем BotManager...');
setTimeout(() => { const botManager = require('./services/botManager');
}, 10000); // Задержка 10 секунд для полной инициализации 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}`); console.log(`✅ Server is running on port ${PORT}`);
} }
@@ -113,16 +77,36 @@ if (process.env.NODE_ENV === 'production') {
// Обработчики для корректного завершения // Обработчики для корректного завершения
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
// logger.info('[Server] Получен сигнал SIGINT, завершаем работу...'); // Убрано избыточное логирование console.log('[Server] Получен SIGINT, завершаем работу...');
memoryMonitor.stop(); try {
await initDbPool().then(pool => pool.end()); // Use initDbPool to get the pool // Останавливаем боты
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.exit(0);
}); });
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
// logger.info('[Server] Получен сигнал SIGTERM, завершаем работу...'); // Убрано избыточное логирование console.log('[Server] Получен SIGTERM, завершаем работу...');
memoryMonitor.stop(); try {
await initDbPool().then(pool => pool.end()); // Use initDbPool to get the pool // Останавливаем боты
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.exit(0);
}); });

View File

@@ -34,19 +34,9 @@ async function checkAdminRole(address) {
let foundTokens = false; let foundTokens = false;
let errorCount = 0; let errorCount = 0;
const balances = {}; const balances = {};
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
// Получаем токены и RPC из базы с расшифровкой // Получаем токены и RPC из базы с расшифровкой
const tokensResult = await db.getQuery()( const tokensResult = await db.getQuery()(

View File

@@ -0,0 +1,222 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const logger = require('../utils/logger');
/**
* Сервис логики для админских функций
* Определяет права доступа, приоритеты и логику работы с админами
*/
/**
* Определить тип отправителя на основе сессии
* @param {Object} session - Сессия пользователя
* @returns {Object} { senderType, role }
*/
function determineSenderType(session) {
if (!session) {
return { senderType: 'user', role: 'user' };
}
if (session.isAdmin === true) {
return { senderType: 'admin', role: 'admin' };
}
return { senderType: 'user', role: 'user' };
}
/**
* Определить, нужно ли генерировать AI ответ
* @param {Object} params - Параметры
* @param {string} params.senderType - Тип отправителя (user/admin)
* @param {number} params.userId - ID пользователя
* @param {number} params.recipientId - ID получателя
* @param {string} params.channel - Канал (web/telegram/email)
* @returns {boolean}
*/
function shouldGenerateAiReply(params) {
const { senderType, userId, recipientId } = params;
// Обычные пользователи всегда получают AI ответ
if (senderType !== 'admin') {
return true;
}
// Админ, пишущий себе, получает AI ответ
if (userId === recipientId) {
return true;
}
// Админ, пишущий другому пользователю, не получает AI ответ
// (это личное сообщение от админа)
return false;
}
/**
* Проверить, может ли пользователь писать в беседу
* @param {Object} params - Параметры
* @param {boolean} params.isAdmin - Является ли админом
* @param {number} params.userId - ID пользователя
* @param {number} params.conversationUserId - ID владельца беседы
* @returns {boolean}
*/
function canWriteToConversation(params) {
const { isAdmin, userId, conversationUserId } = params;
// Админ может писать в любую беседу
if (isAdmin) {
return true;
}
// Обычный пользователь может писать только в свою беседу
return userId === conversationUserId;
}
/**
* Получить приоритет запроса для очереди AI
* @param {Object} params - Параметры
* @param {boolean} params.isAdmin - Является ли админом
* @param {string} params.message - Текст сообщения
* @param {Array} params.history - История сообщений
* @returns {number} Приоритет (чем выше, тем важнее)
*/
function getRequestPriority(params) {
const { isAdmin, message, history = [] } = params;
let priority = 10; // Базовый приоритет
// Админ получает повышенный приоритет
if (isAdmin) {
priority += 5;
}
// Срочные ключевые слова
const urgentKeywords = ['срочно', 'urgent', 'помогите', 'help', 'критично', 'critical'];
const messageLC = (message || '').toLowerCase();
if (urgentKeywords.some(keyword => messageLC.includes(keyword))) {
priority += 10;
}
// Короткие сообщения обрабатываются быстрее
if (message && message.length < 50) {
priority += 5;
}
// Первое сообщение в беседе
if (!history || history.length === 0) {
priority += 3;
}
return priority;
}
/**
* Проверить, может ли пользователь выполнить админское действие
* @param {Object} params - Параметры
* @param {boolean} params.isAdmin - Является ли админом
* @param {string} params.action - Название действия
* @returns {boolean}
*/
function canPerformAdminAction(params) {
const { isAdmin, action } = params;
// Только админ может выполнять админские действия
if (!isAdmin) {
return false;
}
// Список разрешенных админских действий
const allowedActions = [
'delete_message_history',
'view_all_conversations',
'manage_users',
'manage_ai_settings',
'broadcast_message',
'delete_user',
'modify_user_settings'
];
return allowedActions.includes(action);
}
/**
* Получить настройки админа
* @param {Object} params - Параметры
* @param {boolean} params.isAdmin - Является ли админом
* @param {string} params.channel - Канал
* @returns {Object} Настройки
*/
function getAdminSettings(params) {
const { isAdmin } = params;
if (!isAdmin) {
// Ограниченные права для обычного пользователя
return {
canWriteToAnyConversation: false,
canViewAllConversations: false,
canManageUsers: false,
canManageAISettings: false,
aiReplyPriority: 0
};
}
// Полные права для админа
return {
canWriteToAnyConversation: true,
canViewAllConversations: true,
canManageUsers: true,
canManageAISettings: true,
aiReplyPriority: 15
};
}
/**
* Логирование админского действия
* @param {Object} params - Параметры
* @param {number} params.adminId - ID админа
* @param {string} params.action - Действие
* @param {Object} params.details - Детали
*/
function logAdminAction(params) {
const { adminId, action, details } = params;
logger.info('[AdminLogic] Админское действие:', {
adminId,
action,
details,
timestamp: new Date().toISOString()
});
}
/**
* Проверить, является ли сообщение от админа личным
* @param {Object} params - Параметры
* @returns {boolean}
*/
function isPersonalAdminMessage(params) {
const { senderType, userId, recipientId } = params;
return senderType === 'admin' && userId !== recipientId;
}
module.exports = {
determineSenderType,
shouldGenerateAiReply,
canWriteToConversation,
getRequestPriority,
canPerformAdminAction,
getAdminSettings,
logAdminAction,
isPersonalAdminMessage
};

View File

@@ -10,439 +10,186 @@
* GitHub: https://github.com/HB3-ACCELERATOR * 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 logger = require('../utils/logger');
const ollamaConfig = require('./ollamaConfig');
// Константы для AI параметров /**
const AI_CONFIG = { * AI Assistant - тонкая обёртка для работы с Ollama и RAG
temperature: 0.3, * Основная логика вынесена в отдельные сервисы:
maxTokens: 512, * - ragService.js - генерация ответов через RAG
timeout: 120000, // Уменьшаем до 120 секунд, чтобы соответствовать EmailBot * - aiAssistantSettingsService.js - настройки ИИ
numCtx: 2048, * - aiAssistantRulesService.js - правила ИИ
numGpu: 1, * - messageDeduplicationService.js - дедупликация сообщений
numThread: 4, * - ai-queue.js - управление очередью (отдельный сервис)
repeatPenalty: 1.1, */
topK: 40,
topP: 0.9,
// tfsZ не поддерживается в текущем Ollama — удаляем
mirostat: 2,
mirostatTau: 5,
mirostatEta: 0.1,
seed: -1,
// Ограничим количество генерируемых токенов для CPU, чтобы избежать таймаутов
numPredict: 256,
stop: []
};
class AIAssistant { class AIAssistant {
constructor() { constructor() {
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'; this.baseUrl = null;
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b'; this.defaultModel = null;
this.lastHealthCheck = 0; this.isInitialized = false;
this.healthCheckInterval = 300000; // 5 минут (увеличено с 30 секунд для уменьшения логов)
// Создаем экземпляр AIQueue
this.aiQueue = new AIQueue();
this.isProcessingQueue = false;
// Запускаем обработку очереди
this.startQueueProcessing();
} }
// Запуск обработки очереди /**
async startQueueProcessing() { * Инициализация из БД
if (this.isProcessingQueue) return; */
async initialize() {
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) {
try { try {
const { message, history, systemPrompt, rules } = request; await ollamaConfig.loadSettingsFromDb();
// Используем прямой запрос к API, а не getResponse (чтобы избежать цикла) this.baseUrl = ollamaConfig.getBaseUrl();
const result = await this.directRequest( this.defaultModel = ollamaConfig.getDefaultModel();
[{ role: 'user', content: message }],
systemPrompt,
{ temperature: 0.3, maxTokens: 150 }
);
return result; if (!this.baseUrl || !this.defaultModel) {
throw new Error('Настройки Ollama не найдены в БД');
}
this.isInitialized = true;
logger.info(`[AIAssistant] ✅ Инициализирован из БД: model=${this.defaultModel}`);
} catch (error) { } catch (error) {
logger.error(`[AIAssistant] Ошибка в processQueueRequest:`, error.message); logger.error('[AIAssistant] ❌ КРИТИЧЕСКАЯ ОШИБКА загрузки настроек из БД:', 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 секунд');
}
throw error; throw error;
} }
} }
// Получение списка доступных моделей /**
async getAvailableModels() { * Генерация ответа для всех каналов (web, telegram, email)
try { * Используется ботами (telegramBot, emailBot)
const response = await fetch(`${this.baseUrl}/api/tags`); */
const data = await response.json(); async generateResponse(options) {
return data.models || []; const {
} catch (error) { channel,
logger.error('Error getting available models:', error); messageId,
return []; userId,
} userQuestion,
} conversationHistory = [],
conversationId,
ragTableId = null,
metadata = {}
} = options;
// Проверка здоровья AI сервиса
async checkHealth() {
try { try {
const response = await fetch(`${this.baseUrl}/api/tags`); logger.info(`[AIAssistant] Генерация ответа для канала ${channel}, пользователь ${userId}`);
if (!response.ok) {
throw new Error(`Ollama API returned ${response.status}`); 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 { return {
status: 'ok', success: true,
models: data.models?.length || 0, response: aiResponse,
baseUrl: this.baseUrl messageId: aiResponseId,
conversationId: conversationId
}; };
} catch (error) { } catch (error) {
logger.error('AI health check failed:', error); logger.error(`[AIAssistant] Ошибка генерации ответа:`, error);
return { return { success: false, reason: 'error', error: error.message };
status: 'error',
error: error.message,
baseUrl: this.baseUrl
};
} }
} }
// Добавляем методы из 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;

View File

@@ -87,6 +87,7 @@ class AICache {
calculateHitRate() { calculateHitRate() {
// Простая реализация - в реальности нужно отслеживать hits/misses // Простая реализация - в реальности нужно отслеживать hits/misses
if (this.maxSize === 0) return 0;
return this.cache.size / this.maxSize; return this.cache.size / this.maxSize;
} }
} }

View File

@@ -28,19 +28,9 @@ async function getSettings() {
return null; return null;
} }
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
// Обрабатываем selected_rag_tables // Обрабатываем selected_rag_tables
if (setting.selected_rag_tables) { if (setting.selected_rag_tables) {

View File

@@ -145,7 +145,8 @@ async function getAllLLMModels() {
// Для Ollama проверяем реально установленные модели через HTTP API // Для Ollama проверяем реально установленные модели через HTTP API
try { try {
const axios = require('axios'); 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`, { const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000 timeout: 5000
@@ -214,7 +215,8 @@ async function getAllEmbeddingModels() {
// Для Ollama проверяем реально установленные embedding модели через HTTP API // Для Ollama проверяем реально установленные embedding модели через HTTP API
try { try {
const axios = require('axios'); 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`, { const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000 timeout: 5000

View File

@@ -262,8 +262,8 @@ class AuthService {
async processAndCleanupGuestData(userId, guestId, session) { async processAndCleanupGuestData(userId, guestId, session) {
try { try {
// Обрабатываем гостевые сообщения // Обрабатываем гостевые сообщения
const { processGuestMessages } = require('../routes/chat'); const guestMessageService = require('./guestMessageService');
await processGuestMessages(userId, guestId); await guestMessageService.processGuestMessages(userId, guestId);
// Очищаем гостевой ID из сессии // Очищаем гостевой ID из сессии
delete session.guestId; delete session.guestId;
@@ -432,19 +432,9 @@ class AuthService {
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя // Если есть гостевой ID в сессии, сохраняем его для нового пользователя
if (session.guestId && isNewUser) { if (session.guestId && isNewUser) {
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
await db.getQuery()( 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', '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'); logger.info('Starting recheck of admin status for all users with wallets');
try { try {
// Получаем ключ шифрования // Получаем ключ шифрования через унифицированную утилиту
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 usersResult = await db.getQuery()( const usersResult = await db.getQuery()(

View File

@@ -0,0 +1,211 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const logger = require('../utils/logger');
const TelegramBot = require('./telegramBot');
const EmailBot = require('./emailBot');
const unifiedMessageProcessor = require('./unifiedMessageProcessor');
/**
* BotManager - централизованный менеджер всех ботов
* Управляет жизненным циклом ботов (инициализация, обработка сообщений, остановка)
*/
class BotManager {
constructor() {
this.bots = new Map();
this.isInitialized = false;
this.processingQueue = [];
}
/**
* Инициализация всех ботов
*/
async initialize() {
try {
logger.info('[BotManager] 🚀 Инициализация BotManager...');
// Создаем экземпляры ботов
const webBot = {
name: 'WebBot',
channel: 'web',
isInitialized: true,
status: 'active',
initialize: async () => ({ success: true }),
processMessage: async (messageData) => {
return await unifiedMessageProcessor.processMessage(messageData);
}
};
const telegramBot = new TelegramBot();
const emailBot = new EmailBot();
// Регистрируем ботов
this.bots.set('web', webBot);
this.bots.set('telegram', telegramBot);
this.bots.set('email', emailBot);
// Инициализируем Telegram Bot
logger.info('[BotManager] Инициализация Telegram Bot...');
await telegramBot.initialize().catch(error => {
logger.warn('[BotManager] Telegram Bot не инициализирован:', error.message);
});
// Инициализируем Email Bot
logger.info('[BotManager] Инициализация Email Bot...');
await emailBot.initialize().catch(error => {
logger.warn('[BotManager] Email Bot не инициализирован:', error.message);
});
this.isInitialized = true;
logger.info('[BotManager] ✅ BotManager успешно инициализирован');
return { success: true };
} catch (error) {
logger.error('[BotManager] ❌ Ошибка инициализации BotManager:', error);
throw error;
}
}
/**
* Получить бота по имени
* @param {string} botName - Имя бота (web, telegram, email)
* @returns {Object|null} Экземпляр бота или null
*/
getBot(botName) {
return this.bots.get(botName) || null;
}
/**
* Проверить готовность BotManager
* @returns {boolean}
*/
isReady() {
return this.isInitialized;
}
/**
* Получить статус всех ботов
* @returns {Object}
*/
getStatus() {
const status = {};
for (const [name, bot] of this.bots) {
status[name] = {
initialized: bot.isInitialized || false,
status: bot.status || 'unknown'
};
}
return status;
}
/**
* Обработать сообщение через соответствующий бот
* @param {Object} messageData - Данные сообщения
* @returns {Promise<Object>}
*/
async processMessage(messageData) {
try {
const channel = messageData.channel || 'web';
const bot = this.bots.get(channel);
if (!bot) {
throw new Error(`Bot for channel "${channel}" not found`);
}
if (!bot.isInitialized) {
throw new Error(`Bot "${channel}" is not initialized`);
}
// Обрабатываем сообщение через unified processor
return await unifiedMessageProcessor.processMessage(messageData);
} catch (error) {
logger.error('[BotManager] Ошибка обработки сообщения:', error);
throw error;
}
}
/**
* Перезапустить конкретный бот
* @param {string} botName - Имя бота
* @returns {Promise<Object>}
*/
async restartBot(botName) {
try {
logger.info(`[BotManager] Перезапуск бота: ${botName}`);
const bot = this.bots.get(botName);
if (!bot) {
throw new Error(`Bot "${botName}" not found`);
}
// Останавливаем бота (если есть метод stop)
if (typeof bot.stop === 'function') {
await bot.stop();
}
// Переинициализируем
if (typeof bot.initialize === 'function') {
await bot.initialize();
}
logger.info(`[BotManager] ✅ Бот ${botName} перезапущен`);
return {
success: true,
bot: botName,
status: bot.status
};
} catch (error) {
logger.error(`[BotManager] Ошибка перезапуска бота ${botName}:`, error);
return {
success: false,
error: error.message
};
}
}
/**
* Остановить все боты
*/
async stop() {
try {
logger.info('[BotManager] Остановка всех ботов...');
for (const [name, bot] of this.bots) {
if (typeof bot.stop === 'function') {
logger.info(`[BotManager] Остановка ${name}...`);
await bot.stop().catch(error => {
logger.error(`[BotManager] Ошибка остановки ${name}:`, error);
});
}
}
this.isInitialized = false;
logger.info('[BotManager] ✅ Все боты остановлены');
} catch (error) {
logger.error('[BotManager] Ошибка остановки ботов:', error);
throw error;
}
}
}
// Singleton instance
const botManager = new BotManager();
module.exports = botManager;

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const logger = require('../utils/logger');
/**
* Сервис для работы с настройками ботов
*/
/**
* Получить настройки конкретного бота
* @param {string} botType - Тип бота (telegram, email)
* @returns {Promise<Object|null>}
*/
async function getBotSettings(botType) {
try {
let tableName;
switch (botType) {
case 'telegram':
tableName = 'telegram_settings';
break;
case 'email':
tableName = 'email_settings';
break;
default:
throw new Error(`Unknown bot type: ${botType}`);
}
const { rows } = await db.getQuery()(
`SELECT * FROM ${tableName} ORDER BY id LIMIT 1`
);
return rows.length > 0 ? rows[0] : null;
} catch (error) {
logger.error(`[BotsSettings] Ошибка получения настроек ${botType}:`, error);
throw error;
}
}
/**
* Сохранить настройки бота
* @param {string} botType - Тип бота
* @param {Object} settings - Настройки
* @returns {Promise<Object>}
*/
async function saveBotSettings(botType, settings) {
try {
let tableName;
switch (botType) {
case 'telegram':
tableName = 'telegram_settings';
break;
case 'email':
tableName = 'email_settings';
break;
default:
throw new Error(`Unknown bot type: ${botType}`);
}
// Простое сохранение - детали зависят от структуры таблицы
const { rows } = await db.getQuery()(
`INSERT INTO ${tableName} (settings, updated_at)
VALUES ($1, NOW())
ON CONFLICT (id) DO UPDATE SET settings = $1, updated_at = NOW()
RETURNING *`,
[JSON.stringify(settings)]
);
return rows[0];
} catch (error) {
logger.error(`[BotsSettings] Ошибка сохранения настроек ${botType}:`, error);
throw error;
}
}
/**
* Получить настройки всех ботов
* @returns {Promise<Object>}
*/
async function getAllBotsSettings() {
try {
const settings = {
telegram: await getBotSettings('telegram').catch(() => null),
email: await getBotSettings('email').catch(() => null)
};
return settings;
} catch (error) {
logger.error('[BotsSettings] Ошибка получения всех настроек:', error);
throw error;
}
}
module.exports = {
getBotSettings,
saveBotSettings,
getAllBotsSettings
};

View File

@@ -0,0 +1,189 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const logger = require('../utils/logger');
const encryptionUtils = require('../utils/encryptionUtils');
/**
* Сервис для работы с беседами (conversations)
*/
/**
* Получить или создать беседу для пользователя
* @param {number} userId - ID пользователя
* @param {string} title - Заголовок беседы
* @returns {Promise<Object>}
*/
async function getOrCreateConversation(userId, title = 'Новая беседа') {
try {
const encryptionKey = encryptionUtils.getEncryptionKey();
// Ищем существующую активную беседу
const { rows: existing } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC
LIMIT 1`,
[userId, encryptionKey]
);
if (existing.length > 0) {
return existing[0];
}
// Создаем новую беседу
const { rows: newConv } = await db.getQuery()(
`INSERT INTO conversations (user_id, title_encrypted)
VALUES ($1, encrypt_text($2, $3))
RETURNING id, user_id, decrypt_text(title_encrypted, $3) as title, created_at, updated_at`,
[userId, title, encryptionKey]
);
logger.info('[ConversationService] Создана новая беседа:', newConv[0].id);
return newConv[0];
} catch (error) {
logger.error('[ConversationService] Ошибка получения/создания беседы:', error);
throw error;
}
}
/**
* Получить беседу по ID
* @param {number} conversationId - ID беседы
* @returns {Promise<Object|null>}
*/
async function getConversationById(conversationId) {
try {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
FROM conversations
WHERE id = $1`,
[conversationId, encryptionKey]
);
return rows.length > 0 ? rows[0] : null;
} catch (error) {
logger.error('[ConversationService] Ошибка получения беседы:', error);
throw error;
}
}
/**
* Получить все беседы пользователя
* @param {number} userId - ID пользователя
* @returns {Promise<Array>}
*/
async function getUserConversations(userId) {
try {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC`,
[userId, encryptionKey]
);
return rows;
} catch (error) {
logger.error('[ConversationService] Ошибка получения бесед пользователя:', error);
throw error;
}
}
/**
* Обновить время последнего обновления беседы
* @param {number} conversationId - ID беседы
* @returns {Promise<void>}
*/
async function touchConversation(conversationId) {
try {
await db.getQuery()(
`UPDATE conversations SET updated_at = NOW() WHERE id = $1`,
[conversationId]
);
} catch (error) {
logger.error('[ConversationService] Ошибка обновления беседы:', error);
// Не бросаем ошибку, это некритично
}
}
/**
* Удалить беседу
* @param {number} conversationId - ID беседы
* @param {number} userId - ID пользователя (для проверки прав)
* @returns {Promise<boolean>}
*/
async function deleteConversation(conversationId, userId) {
try {
const { rowCount } = await db.getQuery()(
`DELETE FROM conversations WHERE id = $1 AND user_id = $2`,
[conversationId, userId]
);
if (rowCount > 0) {
logger.info('[ConversationService] Удалена беседа:', conversationId);
return true;
}
return false;
} catch (error) {
logger.error('[ConversationService] Ошибка удаления беседы:', error);
throw error;
}
}
/**
* Обновить заголовок беседы
* @param {number} conversationId - ID беседы
* @param {number} userId - ID пользователя
* @param {string} newTitle - Новый заголовок
* @returns {Promise<Object|null>}
*/
async function updateConversationTitle(conversationId, userId, newTitle) {
try {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`UPDATE conversations
SET title_encrypted = encrypt_text($3, $4), updated_at = NOW()
WHERE id = $1 AND user_id = $2
RETURNING id, user_id, decrypt_text(title_encrypted, $4) as title, created_at, updated_at`,
[conversationId, userId, newTitle, encryptionKey]
);
return rows.length > 0 ? rows[0] : null;
} catch (error) {
logger.error('[ConversationService] Ошибка обновления заголовка беседы:', error);
throw error;
}
}
module.exports = {
getOrCreateConversation,
getConversationById,
getUserConversations,
touchConversation,
deleteConversation,
updateConversationTitle
};

View File

@@ -13,15 +13,78 @@
const { pool } = require('../db'); const { pool } = require('../db');
const verificationService = require('./verification-service'); const verificationService = require('./verification-service');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const EmailBotService = require('./emailBot.js');
const encryptedDb = require('./encryptedDatabaseService'); const encryptedDb = require('./encryptedDatabaseService');
const authService = require('./auth-service'); const authService = require('./auth-service');
const { checkAdminRole } = require('./admin-role'); const { checkAdminRole } = require('./admin-role');
const { broadcastContactsUpdate } = require('../wsHub'); const { broadcastContactsUpdate } = require('../wsHub');
const nodemailer = require('nodemailer');
const db = require('../db');
class EmailAuth { class EmailAuth {
constructor() { constructor() {
this.emailBot = new EmailBotService(); // Убрали зависимость от старого EmailBot
}
/**
* Отправка кода верификации на email
* Создает временный transporter для отправки
*/
async sendVerificationCode(email, code) {
try {
// Получаем настройки email из БД
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
'SELECT decrypt_text(smtp_host_encrypted, $1) as smtp_host, ' +
'decrypt_text(smtp_user_encrypted, $1) as smtp_user, ' +
'decrypt_text(smtp_password_encrypted, $1) as smtp_password, ' +
'decrypt_text(from_email_encrypted, $1) as from_email ' +
'FROM email_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) {
throw new Error('Email settings not found');
}
const settings = rows[0];
// Создаем временный transporter
const transporter = nodemailer.createTransport({
host: settings.smtp_host,
port: 465,
secure: true,
auth: {
user: settings.smtp_user,
pass: settings.smtp_password,
},
tls: { rejectUnauthorized: false }
});
// Отправляем письмо
await transporter.sendMail({
from: settings.from_email,
to: email,
subject: 'Код подтверждения',
text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`,
html: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">Код подтверждения</h2>
<p style="font-size: 16px; color: #666;">Ваш код подтверждения:</p>
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; margin: 20px 0;">
<span style="font-size: 24px; font-weight: bold; color: #333;">${code}</span>
</div>
<p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p>
</div>`
});
transporter.close();
logger.info('[EmailAuth] Verification code sent successfully');
} catch (error) {
logger.error('[EmailAuth] Error sending verification code:', error);
throw error;
}
} }
async initEmailAuth(session, email) { async initEmailAuth(session, email) {
@@ -70,7 +133,7 @@ class EmailAuth {
); );
// Отправляем код на email // Отправляем код на email
await this.emailBot.sendVerificationCode(email, verificationCode); await this.sendVerificationCode(email, verificationCode);
logger.info( logger.info(
`Generated verification code for Email auth for ${email} and sent to user's email` `Generated verification code for Email auth for ${email} and sent to user's email`

File diff suppressed because it is too large Load Diff

View File

@@ -11,46 +11,21 @@
*/ */
const db = require('../db'); const db = require('../db');
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path');
class EncryptedDataService { class EncryptedDataService {
constructor() { constructor() {
this.encryptionKey = this.loadEncryptionKey(); this.encryptionKey = encryptionUtils.getEncryptionKey();
this.isEncryptionEnabled = !!this.encryptionKey; this.isEncryptionEnabled = encryptionUtils.isEnabled();
if (this.isEncryptionEnabled) { if (this.isEncryptionEnabled) {
// console.log('🔐 Шифрование базы данных активировано'); console.log('🔐 [EncryptedDB] Шифрование базы данных активировано');
// console.log('📋 Автоматическое определение зашифрованных колонок'); console.log('📋 [EncryptedDB] Автоматическое определение зашифрованных колонок');
} else { } 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';
}
}
/** /**
* Получить данные из таблицы с автоматической расшифровкой * Получить данные из таблицы с автоматической расшифровкой

View File

@@ -0,0 +1,159 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const logger = require('../utils/logger');
const encryptionUtils = require('../utils/encryptionUtils');
const guestService = require('./guestService');
/**
* Сервис для переноса гостевых сообщений в зарегистрированный аккаунт
* Используется при регистрации/входе пользователя, который был гостем
*/
/**
* Перенести гостевые сообщения в аккаунт пользователя
* @param {string} guestId - ID гостя
* @param {number} userId - ID зарегистрированного пользователя
* @returns {Promise<Object>}
*/
async function migrateGuestMessages(guestId, userId) {
try {
logger.info(`[GuestMessageService] Перенос сообщений с ${guestId} на user ${userId}`);
// Получаем гостевые сообщения
const guestMessages = await guestService.getGuestMessages(guestId);
if (guestMessages.length === 0) {
logger.info('[GuestMessageService] Нет сообщений для переноса');
return { migrated: 0, skipped: 0 };
}
const encryptionKey = encryptionUtils.getEncryptionKey();
let migrated = 0;
let skipped = 0;
// Переносим каждое сообщение
for (const msg of guestMessages) {
try {
// Вставляем в таблицу messages
await db.getQuery()(
`INSERT INTO messages (
user_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
created_at
) VALUES (
$1,
encrypt_text($2, $7),
encrypt_text($3, $7),
encrypt_text($4, $7),
encrypt_text($5, $7),
encrypt_text($6, $7),
$8
)`,
[
userId,
'user',
msg.content,
msg.channel || 'web',
'user',
'incoming',
encryptionKey,
msg.created_at
]
);
migrated++;
} catch (error) {
logger.error('[GuestMessageService] Ошибка переноса сообщения:', error);
skipped++;
}
}
// Удаляем гостевые сообщения после успешного переноса
if (migrated > 0) {
await guestService.deleteGuestMessages(guestId);
}
logger.info(`[GuestMessageService] Перенесено: ${migrated}, пропущено: ${skipped}`);
return { migrated, skipped, total: guestMessages.length };
} catch (error) {
logger.error('[GuestMessageService] Ошибка миграции сообщений:', error);
throw error;
}
}
/**
* Проверить, есть ли гостевые сообщения для переноса
* @param {string} guestId - ID гостя
* @returns {Promise<boolean>}
*/
async function hasGuestMessages(guestId) {
try {
const messages = await guestService.getGuestMessages(guestId);
return messages.length > 0;
} catch (error) {
logger.error('[GuestMessageService] Ошибка проверки гостевых сообщений:', error);
return false;
}
}
/**
* Получить количество гостевых сообщений
* @param {string} guestId - ID гостя
* @returns {Promise<number>}
*/
async function getGuestMessageCount(guestId) {
try {
const messages = await guestService.getGuestMessages(guestId);
return messages.length;
} catch (error) {
logger.error('[GuestMessageService] Ошибка подсчета гостевых сообщений:', error);
return 0;
}
}
/**
* Очистить старые гостевые сообщения (старше N дней)
* @param {number} daysOld - Возраст в днях
* @returns {Promise<number>}
*/
async function cleanupOldGuestMessages(daysOld = 30) {
try {
const { rowCount } = await db.getQuery()(
`DELETE FROM guest_messages
WHERE created_at < NOW() - INTERVAL '${daysOld} days'`
);
logger.info(`[GuestMessageService] Очищено ${rowCount} старых гостевых сообщений`);
return rowCount;
} catch (error) {
logger.error('[GuestMessageService] Ошибка очистки старых сообщений:', error);
throw error;
}
}
module.exports = {
migrateGuestMessages,
hasGuestMessages,
getGuestMessageCount,
cleanupOldGuestMessages
};

View File

@@ -0,0 +1,160 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const logger = require('../utils/logger');
const encryptionUtils = require('../utils/encryptionUtils');
const crypto = require('crypto');
/**
* Сервис для работы с гостевыми сообщениями
* Обрабатывает сообщения от незарегистрированных пользователей
*/
/**
* Создать гостевой идентификатор
* @returns {string}
*/
function createGuestId() {
return `guest_${crypto.randomBytes(16).toString('hex')}`;
}
/**
* Сохранить гостевое сообщение
* @param {Object} messageData - Данные сообщения
* @returns {Promise<Object>}
*/
async function saveGuestMessage(messageData) {
try {
const encryptionKey = encryptionUtils.getEncryptionKey();
const guestId = messageData.guestId || createGuestId();
const { rows } = await db.getQuery()(
`INSERT INTO guest_messages (
guest_id,
content_encrypted,
channel_encrypted,
created_at
) VALUES (
$1,
encrypt_text($2, $3),
encrypt_text($4, $3),
NOW()
) RETURNING id, guest_id, created_at`,
[guestId, messageData.content, encryptionKey, messageData.channel || 'web']
);
logger.info('[GuestService] Сохранено гостевое сообщение:', rows[0].id);
return {
...rows[0],
content: messageData.content,
channel: messageData.channel || 'web'
};
} catch (error) {
logger.error('[GuestService] Ошибка сохранения гостевого сообщения:', error);
throw error;
}
}
/**
* Получить гостевые сообщения по guest_id
* @param {string} guestId - ID гостя
* @returns {Promise<Array>}
*/
async function getGuestMessages(guestId) {
try {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT
id,
guest_id,
decrypt_text(content_encrypted, $2) as content,
decrypt_text(channel_encrypted, $2) as channel,
created_at
FROM guest_messages
WHERE guest_id = $1
ORDER BY created_at ASC`,
[guestId, encryptionKey]
);
return rows;
} catch (error) {
logger.error('[GuestService] Ошибка получения гостевых сообщений:', error);
throw error;
}
}
/**
* Удалить гостевые сообщения
* @param {string} guestId - ID гостя
* @returns {Promise<number>}
*/
async function deleteGuestMessages(guestId) {
try {
const { rowCount } = await db.getQuery()(
`DELETE FROM guest_messages WHERE guest_id = $1`,
[guestId]
);
logger.info(`[GuestService] Удалено ${rowCount} гостевых сообщений для ${guestId}`);
return rowCount;
} catch (error) {
logger.error('[GuestService] Ошибка удаления гостевых сообщений:', error);
throw error;
}
}
/**
* Проверить, является ли пользователь гостем
* @param {string} identifier - Идентификатор
* @returns {boolean}
*/
function isGuest(identifier) {
return typeof identifier === 'string' && identifier.startsWith('guest_');
}
/**
* Получить статистику гостевых сообщений
* @returns {Promise<Object>}
*/
async function getGuestStats() {
try {
const { rows } = await db.getQuery()(
`SELECT
COUNT(DISTINCT guest_id) as unique_guests,
COUNT(*) as total_messages,
MAX(created_at) as last_message_at
FROM guest_messages`
);
return rows[0];
} catch (error) {
logger.error('[GuestService] Ошибка получения статистики:', error);
throw error;
}
}
module.exports = {
createGuestId,
saveGuestMessage,
getGuestMessages,
deleteGuestMessages,
isGuest,
getGuestStats
};

View File

@@ -10,9 +10,8 @@
* GitHub: https://github.com/HB3-ACCELERATOR * GitHub: https://github.com/HB3-ACCELERATOR
*/ */
const { initTelegramBot } = require('./telegram-service'); const botManager = require('./botManager');
const emailBot = require('./emailBot'); const botsSettings = require('./botsSettings');
const telegramBot = require('./telegramBot');
const aiAssistant = require('./ai-assistant'); const aiAssistant = require('./ai-assistant');
const { const {
initializeVectorStore, initializeVectorStore,
@@ -20,16 +19,11 @@ const {
similaritySearch, similaritySearch,
addDocument, addDocument,
} = require('./vectorStore'); } = require('./vectorStore');
// ... другие импорты
module.exports = { module.exports = {
// Telegram // Bot Manager (новая архитектура)
initTelegramBot, botManager,
botsSettings,
// Email
emailBot,
sendEmail: emailBot.sendEmail,
checkEmails: emailBot.checkEmails,
// Vector Store // Vector Store
initializeVectorStore, initializeVectorStore,
@@ -38,12 +32,10 @@ module.exports = {
addDocument, addDocument,
// AI Assistant // AI Assistant
aiAssistant,
processMessage: aiAssistant.processMessage, processMessage: aiAssistant.processMessage,
getUserInfo: aiAssistant.getUserInfo, getUserInfo: aiAssistant.getUserInfo,
getConversationHistory: aiAssistant.getConversationHistory, getConversationHistory: aiAssistant.getConversationHistory,
telegramBot,
aiAssistant,
interfaceService: require('./interfaceService'), interfaceService: require('./interfaceService'),
}; };

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const crypto = require('crypto');
const logger = require('../utils/logger');
/**
* Сервис дедупликации сообщений
* Предотвращает обработку дублирующихся сообщений
*/
// Хранилище хешей обработанных сообщений (в памяти)
const processedMessages = new Map();
// Время жизни записи о сообщении (5 минут)
const MESSAGE_TTL = 5 * 60 * 1000;
/**
* Создать хеш сообщения
* @param {Object} messageData - Данные сообщения
* @returns {string} Хеш сообщения
*/
function createMessageHash(messageData) {
const hashData = {
userId: messageData.userId || messageData.user_id,
content: messageData.content,
channel: messageData.channel,
timestamp: Math.floor(Date.now() / 1000) // Округляем до секунд
};
return crypto
.createHash('sha256')
.update(JSON.stringify(hashData))
.digest('hex');
}
/**
* Проверить, было ли сообщение уже обработано
* @param {Object} messageData - Данные сообщения
* @returns {boolean} true если сообщение уже обрабатывалось
*/
function isDuplicate(messageData) {
const hash = createMessageHash(messageData);
if (processedMessages.has(hash)) {
const entry = processedMessages.get(hash);
const now = Date.now();
// Проверяем, не истек ли TTL
if (now - entry.timestamp < MESSAGE_TTL) {
logger.warn('[MessageDeduplication] Обнаружено дублирующееся сообщение:', hash);
return true;
} else {
// TTL истек, удаляем запись
processedMessages.delete(hash);
}
}
return false;
}
/**
* Пометить сообщение как обработанное
* @param {Object} messageData - Данные сообщения
*/
function markAsProcessed(messageData) {
const hash = createMessageHash(messageData);
processedMessages.set(hash, {
timestamp: Date.now(),
messageData: {
userId: messageData.userId || messageData.user_id,
channel: messageData.channel
}
});
// Очищаем старые записи
cleanupOldEntries();
}
/**
* Очистить старые записи из хранилища
*/
function cleanupOldEntries() {
const now = Date.now();
let cleanedCount = 0;
for (const [hash, entry] of processedMessages.entries()) {
if (now - entry.timestamp > MESSAGE_TTL) {
processedMessages.delete(hash);
cleanedCount++;
}
}
if (cleanedCount > 0) {
logger.debug(`[MessageDeduplication] Очищено ${cleanedCount} старых записей`);
}
}
/**
* Получить статистику дедупликации
* @returns {Object}
*/
function getStats() {
return {
totalTracked: processedMessages.size,
ttl: MESSAGE_TTL
};
}
/**
* Очистить все записи (для тестов)
*/
function clear() {
processedMessages.clear();
logger.info('[MessageDeduplication] Хранилище очищено');
}
// Периодическая очистка старых записей (каждую минуту)
setInterval(cleanupOldEntries, 60 * 1000);
module.exports = {
isDuplicate,
markAsProcessed,
getStats,
clear,
createMessageHash
};

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const axios = require('axios');
const logger = require('../utils/logger');
/**
* Скрипт для уведомления Ollama о готовности
* Используется для проверки доступности Ollama и прогрева моделей
*/
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://ollama:11434';
const MAX_RETRIES = 30;
const RETRY_DELAY = 2000; // 2 секунды
/**
* Проверить доступность Ollama
* @returns {Promise<boolean>}
*/
async function checkOllamaHealth() {
try {
const response = await axios.get(`${OLLAMA_HOST}/api/tags`, {
timeout: 5000
});
return response.status === 200;
} catch (error) {
return false;
}
}
/**
* Дождаться готовности Ollama с retry
* @returns {Promise<boolean>}
*/
async function waitForOllama() {
logger.info('[NotifyOllamaReady] Ожидание готовности Ollama...');
for (let i = 0; i < MAX_RETRIES; i++) {
const isReady = await checkOllamaHealth();
if (isReady) {
logger.info(`[NotifyOllamaReady] ✅ Ollama готов! (попытка ${i + 1}/${MAX_RETRIES})`);
return true;
}
logger.info(`[NotifyOllamaReady] Ollama не готов, повтор ${i + 1}/${MAX_RETRIES}...`);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
}
logger.error('[NotifyOllamaReady] ❌ Ollama не стал доступен после всех попыток');
return false;
}
/**
* Получить список доступных моделей
* @returns {Promise<Array>}
*/
async function getAvailableModels() {
try {
const response = await axios.get(`${OLLAMA_HOST}/api/tags`, {
timeout: 5000
});
return response.data.models || [];
} catch (error) {
logger.error('[NotifyOllamaReady] Ошибка получения моделей:', error.message);
return [];
}
}
/**
* Прогреть модель (загрузить в память)
* @param {string} modelName - Название модели
* @returns {Promise<boolean>}
*/
async function warmupModel(modelName) {
try {
logger.info(`[NotifyOllamaReady] Прогрев модели: ${modelName}`);
const response = await axios.post(`${OLLAMA_HOST}/api/generate`, {
model: modelName,
prompt: 'Hello',
stream: false
}, {
timeout: 30000
});
if (response.status === 200) {
logger.info(`[NotifyOllamaReady] ✅ Модель ${modelName} прогрета`);
return true;
}
return false;
} catch (error) {
logger.error(`[NotifyOllamaReady] Ошибка прогрева модели ${modelName}:`, error.message);
return false;
}
}
/**
* Основная функция инициализации
*/
async function initialize() {
try {
logger.info('[NotifyOllamaReady] 🚀 Начало инициализации Ollama...');
// Ждем готовности Ollama
const isReady = await waitForOllama();
if (!isReady) {
logger.error('[NotifyOllamaReady] Не удалось дождаться готовности Ollama');
return false;
}
// Получаем список моделей
const models = await getAvailableModels();
logger.info(`[NotifyOllamaReady] Найдено моделей: ${models.length}`);
if (models.length > 0) {
logger.info('[NotifyOllamaReady] Доступные модели:', models.map(m => m.name).join(', '));
// Прогреваем первую модель (опционально)
if (process.env.WARMUP_MODEL === 'true' && models[0]) {
await warmupModel(models[0].name);
}
}
logger.info('[NotifyOllamaReady] ✅ Инициализация завершена');
return true;
} catch (error) {
logger.error('[NotifyOllamaReady] Ошибка инициализации:', error);
return false;
}
}
// Если запущен напрямую как скрипт
if (require.main === module) {
initialize()
.then(success => {
process.exit(success ? 0 : 1);
})
.catch(error => {
logger.error('[NotifyOllamaReady] Критическая ошибка:', error);
process.exit(1);
});
}
module.exports = {
initialize,
waitForOllama,
checkOllamaHealth,
getAvailableModels,
warmupModel
};

View File

@@ -0,0 +1,251 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
/**
* Конфигурационный сервис для Ollama
* Централизует все настройки и URL для Ollama API
*
* ВАЖНО: Настройки берутся из таблицы ai_providers_settings (через aiProviderSettingsService)
*/
const logger = require('../utils/logger');
// Кэш для настроек из БД
let settingsCache = null;
/**
* Загружает настройки Ollama из базы данных
* @returns {Promise<Object>} Настройки Ollama провайдера
*/
async function loadSettingsFromDb() {
try {
const aiProviderSettingsService = require('./aiProviderSettingsService');
const settings = await aiProviderSettingsService.getProviderSettings('ollama');
if (settings) {
settingsCache = settings;
logger.info(`[ollamaConfig] Loaded settings from DB: model=${settings.selected_model}, base_url=${settings.base_url}`);
}
return settings;
} catch (error) {
logger.error('[ollamaConfig] Ошибка загрузки настроек Ollama из БД:', error.message);
return null;
}
}
/**
* Получает базовый URL для Ollama (синхронная версия)
* @returns {string} Базовый URL Ollama
*/
function getBaseUrl() {
// Приоритет: кэш из БД > Docker дефолт
if (settingsCache && settingsCache.base_url) {
return settingsCache.base_url;
}
// URL по умолчанию для Docker
return 'http://ollama:11434';
}
/**
* Получает базовый URL для Ollama (асинхронная версия)
* @returns {Promise<string>} Базовый URL Ollama
*/
async function getBaseUrlAsync() {
try {
if (!settingsCache) {
await loadSettingsFromDb();
}
if (settingsCache && settingsCache.base_url) {
return settingsCache.base_url;
}
} catch (error) {
logger.warn('[ollamaConfig] Failed to load base_url from DB, using default');
}
return 'http://ollama:11434';
}
/**
* Получает URL для конкретного API endpoint Ollama
* @param {string} endpoint - Endpoint API (например: 'tags', 'generate')
* @returns {string} Полный URL для API endpoint
*/
function getApiUrl(endpoint) {
const baseUrl = getBaseUrl();
return `${baseUrl}/api/${endpoint}`;
}
/**
* Получает модель по умолчанию для Ollama (синхронная версия)
* @returns {string} Название модели
*/
function getDefaultModel() {
// Приоритет: кэш из БД > дефолт
if (settingsCache && settingsCache.selected_model) {
return settingsCache.selected_model;
}
// Дефолтное значение если БД недоступна
return 'qwen2.5:7b';
}
/**
* Получает модель асинхронно из БД
* @returns {Promise<string>} Название модели из БД
*/
async function getDefaultModelAsync() {
try {
if (!settingsCache) {
await loadSettingsFromDb();
}
if (settingsCache && settingsCache.selected_model) {
logger.info(`[ollamaConfig] Using model from DB: ${settingsCache.selected_model}`);
return settingsCache.selected_model;
}
} catch (error) {
logger.warn('[ollamaConfig] Failed to load model from DB, using default');
}
return 'qwen2.5:7b';
}
/**
* Получает embedding модель асинхронно из БД
* @returns {Promise<string>} Название embedding модели из БД
*/
async function getEmbeddingModel() {
try {
if (!settingsCache) {
await loadSettingsFromDb();
}
if (settingsCache && settingsCache.embedding_model) {
logger.info(`[ollamaConfig] Using embedding model from DB: ${settingsCache.embedding_model}`);
return settingsCache.embedding_model;
}
} catch (error) {
logger.warn('[ollamaConfig] Failed to load embedding model from DB, using default');
}
return 'mxbai-embed-large:latest';
}
/**
* Получает timeout для запросов к Ollama
* @returns {number} Timeout в миллисекундах
*/
function getTimeout() {
return 30000; // 30 секунд
}
/**
* Получает все конфигурационные параметры Ollama (синхронная версия)
* @returns {Object} Объект с конфигурацией
*/
function getConfig() {
return {
baseUrl: getBaseUrl(),
defaultModel: getDefaultModel(),
timeout: getTimeout(),
apiUrl: {
tags: getApiUrl('tags'),
generate: getApiUrl('generate'),
chat: getApiUrl('chat'),
models: getApiUrl('models'),
show: getApiUrl('show'),
pull: getApiUrl('pull'),
push: getApiUrl('push')
}
};
}
/**
* Получает все конфигурационные параметры Ollama (асинхронная версия)
* @returns {Promise<Object>} Объект с конфигурацией
*/
async function getConfigAsync() {
const baseUrl = await getBaseUrlAsync();
const defaultModel = await getDefaultModelAsync();
const embeddingModel = await getEmbeddingModel();
return {
baseUrl,
defaultModel,
embeddingModel,
timeout: getTimeout(),
apiUrl: {
tags: `${baseUrl}/api/tags`,
generate: `${baseUrl}/api/generate`,
chat: `${baseUrl}/api/chat`,
models: `${baseUrl}/api/models`,
show: `${baseUrl}/api/show`,
pull: `${baseUrl}/api/pull`,
push: `${baseUrl}/api/push`
}
};
}
/**
* Очищает кэш настроек (для перезагрузки)
*/
function clearCache() {
settingsCache = null;
logger.info('[ollamaConfig] Settings cache cleared');
}
/**
* Проверяет доступность Ollama сервиса
* @returns {Promise<Object>} Статус здоровья сервиса
*/
async function checkHealth() {
try {
const baseUrl = getBaseUrl();
const response = await fetch(`${baseUrl}/api/tags`);
if (!response.ok) {
return {
status: 'error',
error: `Ollama вернул код ${response.status}`,
baseUrl
};
}
const data = await response.json();
return {
status: 'ok',
baseUrl,
model: getDefaultModel(),
availableModels: data.models?.length || 0
};
} catch (error) {
return {
status: 'error',
error: error.message,
baseUrl: getBaseUrl()
};
}
}
module.exports = {
getBaseUrl,
getBaseUrlAsync,
getApiUrl,
getDefaultModel,
getDefaultModelAsync,
getEmbeddingModel,
getTimeout,
getConfig,
getConfigAsync,
loadSettingsFromDb,
clearCache,
checkHealth
};

View File

@@ -31,7 +31,10 @@ async function getTableData(tableId) {
const rows = await encryptedDb.getData('user_rows', { table_id: 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 }))); // 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`); // console.log(`[RAG] Found ${cellValues.length} cell values`);
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id; 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 = []; let results = [];
if (rowsForUpsert.length > 0) { if (rowsForUpsert.length > 0 && userQuestion && userQuestion.trim()) {
results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска
// console.log(`[RAG] Search completed, got ${results.length} results`); // console.log(`[RAG] Search completed, got ${results.length} results`);

View File

@@ -12,7 +12,7 @@
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const encryptedDb = require('./encryptedDatabaseService'); const encryptedDb = require('./encryptedDatabaseService');
const { processGuestMessages } = require('../routes/chat'); const guestMessageService = require('./guestMessageService');
/** /**
* Сервис для работы с сессиями пользователей * Сервис для работы с сессиями пользователей
@@ -100,7 +100,7 @@ class SessionService {
// Обрабатываем сообщения для каждого гостевого ID // Обрабатываем сообщения для каждого гостевого ID
for (const guestId of guestIdsToProcess) { for (const guestId of guestIdsToProcess) {
await this.processGuestMessagesWrapper(userId, guestId); await guestMessageService.processGuestMessages(userId, guestId);
} }
} }
@@ -127,20 +127,7 @@ class SessionService {
} }
} }
/** // Обертка processGuestMessagesWrapper удалена - используется прямой вызов guestMessageService.processGuestMessages
* Обертка для функции processGuestMessages
* @param {number} userId - ID пользователя
* @param {string} guestId - ID гостя
* @returns {Promise<Object>} - Результат операции
*/
async processGuestMessagesWrapper(userId, guestId) {
try {
return await processGuestMessages(userId, guestId);
} catch (error) {
logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error);
throw error;
}
}
/** /**
* Получает сессию из хранилища по ID * Получает сессию из хранилища по ID

View File

@@ -13,317 +13,203 @@
const { Telegraf } = require('telegraf'); const { Telegraf } = require('telegraf');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const encryptedDb = require('./encryptedDatabaseService'); 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; * Инициализация Telegram Bot
*/
async initialize() {
try {
logger.info('[TelegramBot] 🚀 Инициализация Telegram Bot...');
const settings = await encryptedDb.getData('telegram_settings', {}, 1); // Загружаем настройки из БД
if (!settings.length) throw new Error('Telegram settings not found in DB'); this.settings = await this.loadSettings();
telegramSettingsCache = settings[0]; if (!this.settings || !this.settings.bot_token) {
return telegramSettingsCache; logger.warn('[TelegramBot] ⚠️ Настройки Telegram не найдены');
} this.status = 'not_configured';
return { success: false, reason: 'not_configured' };
}
// Создание и настройка бота // Проверяем токен
async function getBot() { if (!this.settings.bot_token || typeof this.settings.bot_token !== 'string') {
// console.log('[TelegramBot] getBot() called'); logger.error('[TelegramBot] ❌ Некорректный токен:', {
if (!botInstance) { tokenExists: !!this.settings.bot_token,
// console.log('[TelegramBot] Creating new bot instance...'); tokenType: typeof this.settings.bot_token,
const settings = await getTelegramSettings(); tokenLength: this.settings.bot_token?.length || 0
// console.log('[TelegramBot] Got settings, creating Telegraf instance...'); });
botInstance = new Telegraf(settings.bot_token); this.status = 'invalid_token';
// console.log('[TelegramBot] Telegraf instance created'); return { success: false, reason: 'invalid_token' };
}
// Обработка команды /start // Проверяем токен через Telegram API
botInstance.command('start', (ctx) => { 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('Добро пожаловать! Отправьте код подтверждения для аутентификации.'); ctx.reply('Добро пожаловать! Отправьте код подтверждения для аутентификации.');
}); });
// Универсальный обработчик текстовых сообщений // Обработчик текстовых сообщений
botInstance.on('text', async (ctx) => { this.bot.on('text', async (ctx) => {
const text = ctx.message.text.trim(); logger.info('[TelegramBot] 📨 Получено текстовое сообщение');
// 1. Если команда — пропустить await this.handleTextMessage(ctx);
if (text.startsWith('/')) return; });
// Отправляем индикатор печати для улучшения UX // Обработчик документов
const typingAction = ctx.replyWithChatAction('typing'); this.bot.on('document', async (ctx) => {
logger.info('[TelegramBot] 📨 Получен документ');
await this.handleMessage(ctx);
});
// 2. Проверка: это потенциальный код? // Обработчик фото
const isPotentialCode = (str) => /^[A-Z0-9]{6}$/i.test(str); this.bot.on('photo', async (ctx) => {
if (isPotentialCode(text)) { logger.info('[TelegramBot] 📨 Получено фото');
await typingAction; await this.handleMessage(ctx);
try { });
// Получаем код верификации для всех активных кодов с провайдером telegram
const codes = await encryptedDb.getData('verification_codes', {
code: text.toUpperCase(),
provider: 'telegram',
used: false
}, 1);
if (codes.length === 0) { // Обработчик аудио
ctx.reply('Неверный код подтверждения'); this.bot.on('audio', async (ctx) => {
return; logger.info('[TelegramBot] 📨 Получено аудио');
} await this.handleMessage(ctx);
});
const verification = codes[0]; // Обработчик видео
const providerId = verification.provider_id; this.bot.on('video', async (ctx) => {
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть logger.info('[TelegramBot] 📨 Получено видео');
let userId; await this.handleMessage(ctx);
let userRole = 'user'; // Роль по умолчанию });
}
// Отмечаем код как использованный /**
await encryptedDb.saveData('verification_codes', { * Обработка текстовых сообщений
used: true */
}, { async handleTextMessage(ctx) {
id: verification.id const text = ctx.message.text.trim();
});
logger.info('Starting Telegram auth process for code:', text); // Пропускаем команды
if (text.startsWith('/')) return;
// Проверяем, существует ли уже пользователь с таким Telegram ID // Обрабатываем как обычное сообщение
const existingTelegramUsers = await encryptedDb.getData('user_identities', { await this.handleMessage(ctx);
provider: 'telegram', }
provider_id: ctx.from.id.toString()
}, 1);
if (existingTelegramUsers.length > 0) { /**
// Если пользователь с таким Telegram ID уже существует, используем его * Извлечение данных из Telegram сообщения
userId = existingTelegramUsers[0].user_id; * @param {Object} ctx - Telegraf context
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`); * @returns {Object} - Стандартизированные данные сообщения
} else { */
// Если код верификации был связан с существующим пользователем, используем его extractMessageData(ctx) {
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}`
);
}
}
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. Всё остальное — чат с ИИ-ассистентом
try { try {
const telegramId = ctx.from.id.toString(); const telegramId = ctx.from.id.toString();
let content = '';
let attachments = [];
// 1. Найти или создать пользователя // Текст сообщения
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId); if (ctx.message.text) {
if (await isUserBlocked(userId)) { content = ctx.message.text.trim();
await ctx.reply('Вы заблокированы. Сообщения не принимаются.'); } else if (ctx.message.caption) {
return; content = ctx.message.caption.trim();
} }
// 1.1 Найти или создать беседу // Обработка вложений
let conversationResult = await encryptedDb.getData('conversations', { let fileId, fileName, mimeType, fileSize;
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;
if (ctx.message.document) { if (ctx.message.document) {
fileId = ctx.message.document.file_id; fileId = ctx.message.document.file_id;
fileName = ctx.message.document.file_name; fileName = ctx.message.document.file_name;
mimeType = ctx.message.document.mime_type; mimeType = ctx.message.document.mime_type;
fileSize = ctx.message.document.file_size; fileSize = ctx.message.document.file_size;
} else if (ctx.message.photo && ctx.message.photo.length > 0) { } else if (ctx.message.photo && ctx.message.photo.length > 0) {
// Берём самое большое фото
const photo = ctx.message.photo[ctx.message.photo.length - 1]; const photo = ctx.message.photo[ctx.message.photo.length - 1];
fileId = photo.file_id; fileId = photo.file_id;
fileName = 'photo.jpg'; fileName = 'photo.jpg';
@@ -341,339 +227,185 @@ async function getBot() {
fileSize = ctx.message.video.file_size; fileSize = ctx.message.video.file_size;
} }
// Асинхронная загрузка файлов
if (fileId) { if (fileId) {
try { attachments.push({
const fileLink = await ctx.telegram.getFileLink(fileId); type: 'telegram_file',
const res = await fetch(fileLink.href); fileId: fileId,
attachmentBuffer = await res.buffer(); filename: fileName,
attachmentMeta = { mimetype: mimeType,
attachment_filename: fileName, size: fileSize,
attachment_mimetype: mimeType, ctx: ctx // Сохраняем контекст для последующей загрузки
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
}); });
// Отправляем 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('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
} }
});
// Запуск бота с таймаутом return {
// console.log('[TelegramBot] Before botInstance.launch()'); channel: 'telegram',
try { identifier: telegramId,
// Запускаем бота с таймаутом content: content,
const launchPromise = botInstance.launch(); attachments: attachments,
const timeoutPromise = new Promise((_, reject) => { metadata: {
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 30000); // 30 секунд таймаут telegramUsername: ctx.from.username,
}); telegramFirstName: ctx.from.first_name,
telegramLastName: ctx.from.last_name,
await Promise.race([launchPromise, timeoutPromise]); messageId: ctx.message.message_id,
// console.log('[TelegramBot] After botInstance.launch()'); chatId: ctx.chat.id
logger.info('[TelegramBot] Бот запущен'); }
};
} catch (error) { } catch (error) {
// console.error('[TelegramBot] Error launching bot:', error); logger.error('[TelegramBot] Ошибка извлечения данных из сообщения:', 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);
throw error; throw error;
} }
} }
}
// Инициализация процесса аутентификации /**
async function initTelegramAuth(session) { * Загрузка файла из Telegram
try { * @param {Object} attachment - Данные вложения
// Используем временный идентификатор для создания кода верификации * @returns {Promise<Buffer>} - Буфер с данными файла
// Реальный пользователь будет создан или найден при проверке кода через бота */
const tempId = crypto.randomBytes(16).toString('hex'); 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) { * @param {Object} ctx - Telegraf context
const guestId = session.guestId || tempId; * @param {Function} processor - Функция обработки сообщения
*/
async handleMessage(ctx, processor = null) {
try {
await ctx.replyWithChatAction('typing');
// Связываем гостевой ID с текущим пользователем // Извлекаем данные из сообщения
await encryptedDb.saveData('guest_user_mapping', { const messageData = this.extractMessageData(ctx);
user_id: session.userId,
guest_id: guestId logger.info(`[TelegramBot] Обработка сообщения от пользователя: ${messageData.identifier}`);
}, {
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( logger.info('[TelegramBot] ✅ Бот запущен успешно');
`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}` } 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 { return {
verificationCode: code, name: this.name,
botLink: `https://t.me/${settings.bot_username}`, 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) { } catch (error) {
logger.error('Error initializing Telegram auth:', error); logger.error('[TelegramBot] ❌ Ошибка остановки:', error);
throw error; throw error;
} }
} }
function clearSettingsCache() {
telegramSettingsCache = null;
} }
// Сохранение настроек Telegram module.exports = TelegramBot;
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;
}
}
async function getAllBots() {
const settings = await encryptedDb.getData('telegram_settings', {}, 1, 'id');
return settings;
}
module.exports = {
getTelegramSettings,
getBot,
stopBot,
initTelegramAuth,
clearSettingsCache,
saveTelegramSettings,
getAllBots,
};

View File

@@ -0,0 +1,213 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const logger = require('../utils/logger');
const botManager = require('./botManager');
const WebBot = require('./webBot');
const TelegramBot = require('./telegramBot');
const EmailBot = require('./emailBot');
/**
* Тестовый скрипт для проверки новой архитектуры ботов
* Используется для отладки и тестирования ботов
*/
/**
* Тест Web Bot
*/
async function testWebBot() {
console.log('\n=== ТЕСТ WEB BOT ===');
try {
const webBot = new WebBot();
// Инициализация
const initResult = await webBot.initialize();
console.log('✓ Инициализация:', initResult);
// Статус
const status = webBot.getStatus();
console.log('✓ Статус:', status);
// Тестовое сообщение
const testMessage = {
userId: 1,
content: 'Тестовое сообщение',
channel: 'web'
};
console.log('✓ Web Bot тест пройден');
return true;
} catch (error) {
console.error('✗ Ошибка Web Bot:', error.message);
return false;
}
}
/**
* Тест Telegram Bot
*/
async function testTelegramBot() {
console.log('\n=== ТЕСТ TELEGRAM BOT ===');
try {
const telegramBot = new TelegramBot();
// Инициализация
const initResult = await telegramBot.initialize();
console.log('✓ Инициализация:', initResult);
// Статус
const status = telegramBot.getStatus ? telegramBot.getStatus() : {
isInitialized: telegramBot.isInitialized,
status: telegramBot.status
};
console.log('✓ Статус:', status);
console.log('✓ Telegram Bot тест пройден');
return true;
} catch (error) {
console.error('✗ Ошибка Telegram Bot:', error.message);
return false;
}
}
/**
* Тест Email Bot
*/
async function testEmailBot() {
console.log('\n=== ТЕСТ EMAIL BOT ===');
try {
const emailBot = new EmailBot();
// Инициализация
const initResult = await emailBot.initialize();
console.log('✓ Инициализация:', initResult);
// Статус
const status = emailBot.getStatus ? emailBot.getStatus() : {
isInitialized: emailBot.isInitialized,
status: emailBot.status
};
console.log('✓ Статус:', status);
console.log('✓ Email Bot тест пройден');
return true;
} catch (error) {
console.error('✗ Ошибка Email Bot:', error.message);
return false;
}
}
/**
* Тест Bot Manager
*/
async function testBotManager() {
console.log('\n=== ТЕСТ BOT MANAGER ===');
try {
// Инициализация
await botManager.initialize();
console.log('✓ BotManager инициализирован');
// Проверка готовности
const isReady = botManager.isReady();
console.log('✓ isReady:', isReady);
// Получение статуса
const status = botManager.getStatus();
console.log('✓ Статус всех ботов:', status);
// Получение конкретных ботов
const webBot = botManager.getBot('web');
const telegramBot = botManager.getBot('telegram');
const emailBot = botManager.getBot('email');
console.log('✓ Web Bot:', webBot ? 'OK' : 'NOT FOUND');
console.log('✓ Telegram Bot:', telegramBot ? 'OK' : 'NOT FOUND');
console.log('✓ Email Bot:', emailBot ? 'OK' : 'NOT FOUND');
console.log('✓ Bot Manager тест пройден');
return true;
} catch (error) {
console.error('✗ Ошибка Bot Manager:', error.message);
return false;
}
}
/**
* Запустить все тесты
*/
async function runAllTests() {
console.log('╔═══════════════════════════════════════╗');
console.log('║ ТЕСТИРОВАНИЕ НОВОЙ АРХИТЕКТУРЫ БОТОВ ║');
console.log('╚═══════════════════════════════════════╝');
const results = {
webBot: false,
telegramBot: false,
emailBot: false,
botManager: false
};
try {
// Тестируем каждый компонент
results.webBot = await testWebBot();
results.telegramBot = await testTelegramBot();
results.emailBot = await testEmailBot();
results.botManager = await testBotManager();
// Итоги
console.log('\n╔═══════════════════════════════════════╗');
console.log('║ ИТОГИ ТЕСТИРОВАНИЯ ║');
console.log('╚═══════════════════════════════════════╝');
console.log('Web Bot: ', results.webBot ? '✓ PASS' : '✗ FAIL');
console.log('Telegram Bot: ', results.telegramBot ? '✓ PASS' : '✗ FAIL');
console.log('Email Bot: ', results.emailBot ? '✓ PASS' : '✗ FAIL');
console.log('Bot Manager: ', results.botManager ? '✓ PASS' : '✗ FAIL');
const allPassed = Object.values(results).every(r => r === true);
console.log('\nОБЩИЙ РЕЗУЛЬТАТ:', allPassed ? '✓ ВСЕ ТЕСТЫ ПРОЙДЕНЫ' : '✗ ЕСТЬ ОШИБКИ');
return allPassed;
} catch (error) {
logger.error('[TestNewBots] Критическая ошибка:', error);
return false;
}
}
// Если запущен напрямую как скрипт
if (require.main === module) {
runAllTests()
.then(success => {
process.exit(success ? 0 : 1);
})
.catch(error => {
console.error('КРИТИЧЕСКАЯ ОШИБКА:', error);
process.exit(1);
});
}
module.exports = {
testWebBot,
testTelegramBot,
testEmailBot,
testBotManager,
runAllTests
};

View File

@@ -22,18 +22,8 @@ async function getUserTokenBalances(address) {
if (!address) return []; if (!address) return [];
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const encryptionUtils = require('../utils/encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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);
}
// Получаем токены и RPC с расшифровкой // Получаем токены и RPC с расшифровкой
const tokensResult = await db.getQuery()( const tokensResult = await db.getQuery()(

View File

@@ -0,0 +1,293 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const logger = require('../utils/logger');
const encryptionUtils = require('../utils/encryptionUtils');
const aiAssistant = require('./ai-assistant');
const conversationService = require('./conversationService');
const { broadcastMessagesUpdate } = require('../wsHub');
/**
* Унифицированный процессор сообщений для всех каналов
* Обрабатывает сообщения из web, telegram, email
*/
/**
* Обработать сообщение от пользователя
* @param {Object} messageData - Данные сообщения
* @param {number} messageData.userId - ID пользователя
* @param {string} messageData.content - Текст сообщения
* @param {string} messageData.channel - Канал (web/telegram/email)
* @param {Array} messageData.attachments - Вложения
* @param {number} messageData.conversationId - ID беседы (опционально)
* @returns {Promise<Object>}
*/
async function processMessage(messageData) {
try {
const {
userId,
content,
channel = 'web',
attachments = [],
conversationId: inputConversationId,
guestId
} = messageData;
logger.info('[UnifiedMessageProcessor] Обработка сообщения:', {
userId,
channel,
contentLength: content?.length,
hasAttachments: attachments.length > 0
});
const encryptionKey = encryptionUtils.getEncryptionKey();
// 1. Получаем или создаем беседу
let conversation;
if (inputConversationId) {
conversation = await conversationService.getConversationById(inputConversationId);
}
if (!conversation && userId) {
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
}
const conversationId = conversation?.id || null;
// 2. Сохраняем входящее сообщение пользователя
let userMessage;
// Обработка вложений
let attachment_filename = null;
let attachment_mimetype = null;
let attachment_size = null;
let attachment_data = null;
if (attachments && attachments.length > 0) {
const firstAttachment = attachments[0];
attachment_filename = firstAttachment.filename;
attachment_mimetype = firstAttachment.mimetype;
attachment_size = firstAttachment.size;
attachment_data = firstAttachment.data;
}
if (userId) {
const { rows } = await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
attachment_filename_encrypted,
attachment_mimetype_encrypted,
attachment_size,
attachment_data,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $12),
encrypt_text($4, $12),
encrypt_text($5, $12),
encrypt_text($6, $12),
encrypt_text($7, $12),
encrypt_text($8, $12),
encrypt_text($9, $12),
$10, $11,
NOW()
) RETURNING id`,
[
userId,
conversationId,
'user',
content,
channel,
'user',
'incoming',
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data,
encryptionKey
]
);
userMessage = rows[0];
logger.info('[UnifiedMessageProcessor] Сообщение пользователя сохранено:', userMessage.id);
}
// 3. Получаем историю беседы для контекста
let conversationHistory = [];
if (conversationId && userId) {
const { rows } = await db.getQuery()(
`SELECT
decrypt_text(role_encrypted, $2) as role,
decrypt_text(content_encrypted, $2) as content,
created_at
FROM messages
WHERE conversation_id = $1 AND user_id = $3
ORDER BY created_at ASC
LIMIT 20`,
[conversationId, encryptionKey, userId]
);
conversationHistory = rows.map(row => ({
role: row.role,
content: row.content
}));
}
// 4. Генерируем AI ответ
logger.info('[UnifiedMessageProcessor] Генерация AI ответа...');
const aiResponse = await aiAssistant.generateResponse({
channel,
messageId: userMessage?.id || `guest_${Date.now()}`,
userId: userId || guestId,
userQuestion: content,
conversationHistory,
conversationId,
metadata: {
hasAttachments: attachments.length > 0,
channel
}
});
if (!aiResponse || !aiResponse.success) {
logger.warn('[UnifiedMessageProcessor] AI не вернул ответ или ошибка:', aiResponse?.reason);
// Возвращаем результат без AI ответа
return {
success: true,
userMessageId: userMessage?.id,
conversationId,
noAiResponse: true,
reason: aiResponse?.reason
};
}
// 5. Сохраняем ответ AI
if (userId && aiResponse.response) {
const { rows: aiMessageRows } = await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $8),
encrypt_text($4, $8),
encrypt_text($5, $8),
encrypt_text($6, $8),
encrypt_text($7, $8),
NOW()
) RETURNING id`,
[
userId,
conversationId,
'assistant',
aiResponse.response,
channel,
'assistant',
'outgoing',
encryptionKey
]
);
logger.info('[UnifiedMessageProcessor] Ответ AI сохранен:', aiMessageRows[0].id);
// 6. Обновляем время беседы
if (conversationId) {
await conversationService.touchConversation(conversationId);
}
// 7. Отправляем уведомление через WebSocket
try {
broadcastMessagesUpdate(userId);
} catch (wsError) {
logger.warn('[UnifiedMessageProcessor] Ошибка отправки WebSocket:', wsError.message);
}
}
// 8. Возвращаем результат
return {
success: true,
userMessageId: userMessage?.id,
conversationId,
aiResponse: {
response: aiResponse.response,
ragData: aiResponse.ragData
}
};
} catch (error) {
logger.error('[UnifiedMessageProcessor] Ошибка обработки сообщения:', error);
throw error;
}
}
/**
* Обработать сообщение от гостя
* @param {Object} messageData - Данные сообщения
* @returns {Promise<Object>}
*/
async function processGuestMessage(messageData) {
try {
const guestService = require('./guestService');
// Создаем guest ID если нет
const guestId = messageData.guestId || guestService.createGuestId();
// Сохраняем гостевое сообщение
await guestService.saveGuestMessage({
guestId,
content: messageData.content,
channel: messageData.channel || 'web'
});
// Генерируем AI ответ для гостя (без сохранения в messages)
const aiResponse = await aiAssistant.generateResponse({
channel: messageData.channel || 'web',
messageId: `guest_${guestId}_${Date.now()}`,
userId: guestId,
userQuestion: messageData.content,
conversationHistory: [],
metadata: { isGuest: true }
});
return {
success: true,
guestId,
aiResponse: aiResponse?.success ? {
response: aiResponse.response
} : null
};
} catch (error) {
logger.error('[UnifiedMessageProcessor] Ошибка обработки гостевого сообщения:', error);
throw error;
}
}
module.exports = {
processMessage,
processGuestMessage
};

View File

@@ -33,6 +33,14 @@ async function deleteUserById(userId) {
); );
console.log('[DELETE] Удалено messages:', resMessages.rows.length); 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 // 3. Удаляем conversations
console.log('[DELETE] Начинаем удаление conversations для userId:', userId); console.log('[DELETE] Начинаем удаление conversations для userId:', userId);
const resConversations = await db.getQuery()( const resConversations = await db.getQuery()(

128
backend/services/webBot.js Normal file
View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const logger = require('../utils/logger');
const unifiedMessageProcessor = require('./unifiedMessageProcessor');
/**
* WebBot - обработчик веб-чата
* Простой бот для веб-интерфейса, всегда активен
*/
class WebBot {
constructor() {
this.name = 'WebBot';
this.channel = 'web';
this.isInitialized = false;
this.status = 'inactive';
}
/**
* Инициализация Web Bot
*/
async initialize() {
try {
logger.info('[WebBot] 🚀 Инициализация Web Bot...');
// Web bot всегда готов к работе
this.isInitialized = true;
this.status = 'active';
logger.info('[WebBot] ✅ Web Bot успешно инициализирован');
return { success: true };
} catch (error) {
logger.error('[WebBot] ❌ Ошибка инициализации:', error);
this.status = 'error';
return { success: false, error: error.message };
}
}
/**
* Обработка сообщения из веб-чата
* @param {Object} messageData - Данные сообщения
* @returns {Promise<Object>}
*/
async processMessage(messageData) {
try {
if (!this.isInitialized) {
throw new Error('WebBot is not initialized');
}
// Устанавливаем канал
messageData.channel = 'web';
// Обрабатываем через unified processor
return await unifiedMessageProcessor.processMessage(messageData);
} catch (error) {
logger.error('[WebBot] Ошибка обработки сообщения:', error);
throw error;
}
}
/**
* Отправка сообщения в веб-чат
* @param {number} userId - ID пользователя
* @param {string} message - Текст сообщения
* @returns {Promise<Object>}
*/
async sendMessage(userId, message) {
try {
logger.info('[WebBot] Отправка сообщения пользователю:', userId);
// Для веб-чата отправка происходит через WebSocket
// Здесь просто возвращаем успех, реальная отправка через wsHub
return {
success: true,
userId,
message,
channel: 'web'
};
} catch (error) {
logger.error('[WebBot] Ошибка отправки сообщения:', error);
throw error;
}
}
/**
* Получить статус бота
* @returns {Object}
*/
getStatus() {
return {
name: this.name,
channel: this.channel,
isInitialized: this.isInitialized,
status: this.status
};
}
/**
* Остановка бота
*/
async stop() {
try {
logger.info('[WebBot] Остановка Web Bot...');
this.isInitialized = false;
this.status = 'inactive';
logger.info('[WebBot] ✅ Web Bot остановлен');
} catch (error) {
logger.error('[WebBot] Ошибка остановки:', error);
throw error;
}
}
}
module.exports = WebBot;

View File

@@ -10,12 +10,6 @@
* GitHub: https://github.com/HB3-ACCELERATOR * GitHub: https://github.com/HB3-ACCELERATOR
*/ */
// Роли пользователей
const USER_ROLES = {
USER: 1,
ADMIN: 2,
};
// Типы идентификаторов // Типы идентификаторов
const IDENTITY_TYPES = { const IDENTITY_TYPES = {
WALLET: 'wallet', WALLET: 'wallet',
@@ -30,13 +24,6 @@ const MESSAGE_CHANNELS = {
EMAIL: 'email', EMAIL: 'email',
}; };
// Типы отправителей сообщений
const SENDER_TYPES = {
USER: 'user',
AI: 'ai',
ADMIN: 'admin',
};
// Коды ошибок // Коды ошибок
const ERROR_CODES = { const ERROR_CODES = {
UNAUTHORIZED: 'unauthorized', UNAUTHORIZED: 'unauthorized',
@@ -59,12 +46,27 @@ const API_CONFIG = {
TIMEOUT: 30000, // 30 секунд 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 = { module.exports = {
USER_ROLES,
IDENTITY_TYPES, IDENTITY_TYPES,
MESSAGE_CHANNELS, MESSAGE_CHANNELS,
SENDER_TYPES,
ERROR_CODES, ERROR_CODES,
SESSION_CONFIG, SESSION_CONFIG,
API_CONFIG, API_CONFIG,
// Константы для ИИ-ассистента
AI_USER_TYPES,
AI_SENDER_TYPES,
}; };

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
/**
* Утилиты для работы с шифрованием
* Предоставляет единую точку доступа к ключу шифрования
*/
const fs = require('fs');
const path = require('path');
const logger = require('./logger');
// Кэш ключа шифрования
let cachedKey = null;
/**
* Получить ключ шифрования из файла или переменной окружения
* @returns {string} Ключ шифрования
*/
function getEncryptionKey() {
// Если ключ уже закэширован, возвращаем его
if (cachedKey) {
return cachedKey;
}
// Сначала пробуем прочитать из файла (приоритет)
// В Docker контейнере путь /app/ssl/keys/full_db_encryption.key
// В локальной разработке ../../ssl/keys/full_db_encryption.key
const keyPath = fs.existsSync('/app/ssl/keys/full_db_encryption.key')
? '/app/ssl/keys/full_db_encryption.key'
: path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
try {
cachedKey = fs.readFileSync(keyPath, 'utf8').trim();
logger.info('[EncryptionUtils] Ключ шифрования загружен из файла');
return cachedKey;
} catch (error) {
logger.error('[EncryptionUtils] Ошибка чтения ключа из файла:', error);
}
}
// Если файла нет, пробуем переменную окружения
if (process.env.ENCRYPTION_KEY) {
cachedKey = process.env.ENCRYPTION_KEY;
logger.info('[EncryptionUtils] Ключ шифрования загружен из переменной окружения');
return cachedKey;
}
// Если ничего не найдено, бросаем ошибку
logger.error('[EncryptionUtils] Ключ шифрования не найден ни в файле, ни в переменной окружения!');
throw new Error('Encryption key not found');
}
/**
* Проверить, включено ли шифрование
* @returns {boolean}
*/
function isEnabled() {
try {
getEncryptionKey();
return true;
} catch (error) {
return false;
}
}
/**
* Очистить кэш ключа (для тестов)
*/
function clearCache() {
cachedKey = null;
}
module.exports = {
getEncryptionKey,
isEnabled,
clearCache
};

View File

@@ -34,18 +34,8 @@ function generateVerificationCode(length = 6) {
// Проверка существования идентификатора пользователя // Проверка существования идентификатора пользователя
async function checkUserIdentity(userId, provider, providerId) { async function checkUserIdentity(userId, provider, providerId) {
// Получаем ключ шифрования // Получаем ключ шифрования
const fs = require('fs'); const encryptionUtils = require('./encryptionUtils');
const path = require('path'); const encryptionKey = encryptionUtils.getEncryptionKey();
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 result = await db.getQuery()( 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)', 'SELECT * FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $4) AND provider_id_encrypted = encrypt_text($3, $4)',

View File

@@ -2,7 +2,7 @@ const winston = require('winston');
const path = require('path'); const path = require('path');
const logger = winston.createLogger({ 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()), format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({

View File

@@ -75,6 +75,12 @@ function initWSS(server) {
})); }));
} }
if (data.type === 'ollama_ready') {
// Уведомление о готовности Ollama - запускаем инициализацию ботов
console.log('🚀 [WebSocket] Получено уведомление о готовности Ollama!');
handleOllamaReady();
}
if (data.type === 'request_token_balances' && data.address) { if (data.type === 'request_token_balances' && data.address) {
// Запрос балансов токенов // Запрос балансов токенов
handleTokenBalancesRequest(ws, data.address, data.userId); handleTokenBalancesRequest(ws, data.address, data.userId);
@@ -578,3 +584,41 @@ 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] Уведомление о готовности системы отправлено всем клиентам');
}

View File

@@ -186,7 +186,7 @@ services:
# SSH Key Server для безопасной передачи ключей # SSH Key Server для безопасной передачи ключей
ssh-key-server: ssh-key-server:
image: node:20-alpine image: node:20-slim
container_name: dapp-ssh-key-server container_name: dapp-ssh-key-server
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -197,7 +197,7 @@ services:
- '3001:3001' - '3001:3001'
command: node /app/ssh-key-server.js command: node /app/ssh-key-server.js
healthcheck: 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 interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3

View File

@@ -8,7 +8,7 @@
# This software is proprietary and confidential. # This software is proprietary and confidential.
# For licensing inquiries: info@hb3-accelerator.com # For licensing inquiries: info@hb3-accelerator.com
FROM node:20-alpine FROM node:20-slim
# Добавляем метки для авторских прав # Добавляем метки для авторских прав
LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>" LABEL maintainer="Тарабанов Александр Викторович <info@hb3-accelerator.com>"
@@ -18,8 +18,12 @@ LABEL website="https://hb3-accelerator.com"
WORKDIR /app 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 для установки зависимостей # Копируем package.json и yarn.lock для установки зависимостей
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./

View File

@@ -23,8 +23,8 @@ RUN apk add --no-cache curl
# Копируем собранный frontend из первого этапа # Копируем собранный frontend из первого этапа
COPY --from=frontend-builder /app/dist/ /usr/share/nginx/html/ COPY --from=frontend-builder /app/dist/ /usr/share/nginx/html/
# Копируем конфигурацию nginx # Копируем конфигурацию nginx (используем dev версию для локальной разработки)
COPY nginx-simple.conf /etc/nginx/nginx.conf.template COPY nginx-dev.conf /etc/nginx/nginx.conf.template
# Копируем скрипт запуска # Копируем скрипт запуска
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh

View File

@@ -491,7 +491,12 @@ async function handleAiReply() {
} }
} catch (e) { } catch (e) {
console.error('Ошибка генерации ответа ИИ:', e); console.error('Ошибка генерации ответа ИИ:', e);
alert('Ошибка генерации ответа ИИ'); // Используем более дружелюбное уведомление вместо alert
emit('error', {
type: 'ai-generation-error',
message: 'Не удалось сгенерировать ответ ИИ. Попробуйте еще раз.',
details: e.message
});
} finally { } finally {
isAiLoading.value = false; isAiLoading.value = false;
} }

View File

@@ -431,7 +431,7 @@ async function handleSendMessage({ message, attachments }) {
if (typeof ElMessageBox === 'function') { if (typeof ElMessageBox === 'function') {
ElMessageBox.alert('Пользователь заблокирован. Отправка сообщений невозможна.', 'Ошибка', { type: 'error' }); ElMessageBox.alert('Пользователь заблокирован. Отправка сообщений невозможна.', 'Ошибка', { type: 'error' });
} else { } else {
alert('Пользователь заблокирован. Отправка сообщений невозможна.'); console.error('Пользователь заблокирован. Отправка сообщений невозможна.');
} }
return; return;
} }
@@ -441,7 +441,7 @@ async function handleSendMessage({ message, attachments }) {
if (typeof ElMessageBox === 'function') { if (typeof ElMessageBox === 'function') {
ElMessageBox.alert('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.', 'Ошибка', { type: 'warning' }); ElMessageBox.alert('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.', 'Ошибка', { type: 'warning' });
} else { } else {
alert('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.'); console.error('У пользователя нет ни одного идентификатора (email, telegram, wallet). Сообщение не может быть отправлено.');
} }
return; return;
} }
@@ -464,14 +464,14 @@ async function handleSendMessage({ message, attachments }) {
if (typeof ElMessageBox === 'function') { if (typeof ElMessageBox === 'function') {
ElMessageBox.alert(resultText, 'Результат рассылки', { type: 'info' }); ElMessageBox.alert(resultText, 'Результат рассылки', { type: 'info' });
} else { } else {
alert(resultText); console.log('Результат рассылки:', resultText);
} }
await loadMessages(); await loadMessages();
} catch (e) { } catch (e) {
if (typeof ElMessageBox === 'function') { if (typeof ElMessageBox === 'function') {
ElMessageBox.alert('Ошибка отправки: ' + (e?.response?.data?.error || e?.message || e), 'Ошибка', { type: 'error' }); ElMessageBox.alert('Ошибка отправки: ' + (e?.response?.data?.error || e?.message || e), 'Ошибка', { type: 'error' });
} else { } else {
alert('Ошибка отправки: ' + (e?.response?.data?.error || e?.message || e)); console.error('Ошибка отправки:', e?.response?.data?.error || e?.message || e);
} }
} }
} }

View File

@@ -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 \ openssh-client \
sshpass \ sshpass \
curl \ curl \
wget \ wget \
docker-cli \ docker.io \
ca-certificates ca-certificates \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Создаем рабочую директорию # Создаем рабочую директорию
WORKDIR /app WORKDIR /app