267 lines
12 KiB
JavaScript
267 lines
12 KiB
JavaScript
/**
|
||
* Copyright (c) 2024-2026 Тарабанов Александр Викторович
|
||
* 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/VC-HB3-Accelerator
|
||
*/
|
||
|
||
const { Pool, types } = require('pg');
|
||
require('dotenv').config();
|
||
const axios = require('axios');
|
||
|
||
// Настройка парсера для BYTEA - возвращаем Buffer напрямую без конвертации в строку
|
||
// OID для BYTEA в PostgreSQL: 17
|
||
types.setTypeParser(17, (value) => {
|
||
// value уже является Buffer при использовании binary формата
|
||
// Но если это строка, конвертируем её в Buffer
|
||
if (Buffer.isBuffer(value)) {
|
||
return value;
|
||
}
|
||
// Если это строка (hex или base64), конвертируем
|
||
if (typeof value === 'string') {
|
||
// Проверяем, является ли это hex строка
|
||
if (/^[0-9a-fA-F]+$/.test(value)) {
|
||
return Buffer.from(value, 'hex');
|
||
}
|
||
// Иначе считаем binary строкой
|
||
return Buffer.from(value, 'binary');
|
||
}
|
||
return value;
|
||
});
|
||
|
||
// Убираем избыточное логирование настроек подключения
|
||
// console.log('Настройки подключения к базе данных:');
|
||
// console.log('DATABASE_URL:', process.env.DATABASE_URL?.replace(/:([^:@]+)@/, ':***@'));
|
||
// console.log('DB_HOST:', process.env.DB_HOST);
|
||
// console.log('DB_PORT:', process.env.DB_PORT);
|
||
// console.log('DB_NAME:', process.env.DB_NAME);
|
||
// console.log('DB_USER:', process.env.DB_USER);
|
||
|
||
// Первичное подключение по дефолтным значениям
|
||
let pool = new Pool({
|
||
host: process.env.DB_HOST || 'postgres',
|
||
port: parseInt(process.env.DB_PORT || '5432'),
|
||
database: process.env.DB_NAME || 'dapp_db',
|
||
user: process.env.DB_USER || 'dapp_user',
|
||
password: process.env.DB_PASSWORD,
|
||
ssl: false,
|
||
// Настройки для предотвращения утечек памяти и таймаутов
|
||
max: 100, // Увеличиваем максимальное количество клиентов в пуле (было 50)
|
||
min: 10, // Минимальное количество клиентов в пуле для лучшей производительности (было 5)
|
||
idleTimeoutMillis: 180000, // Время жизни неактивного клиента (180 сек, было 120)
|
||
connectionTimeoutMillis: 180000, // Таймаут подключения (180 сек, было 120)
|
||
maxUses: 7500, // Максимальное количество использований клиента
|
||
allowExitOnIdle: true, // Разрешить выход при отсутствии активных клиентов
|
||
});
|
||
|
||
// Увеличиваем лимит обработчиков событий для предотвращения предупреждений
|
||
pool.setMaxListeners(100);
|
||
|
||
// Добавляем обработчики для правильного закрытия пула
|
||
// НЕ завершаем процесс при ошибках на idle клиентах - это может быть временная проблема
|
||
pool.on('error', (err) => {
|
||
console.error('[db] Unexpected error on idle client:', err.message);
|
||
// НЕ завершаем процесс - это может быть временная проблема с сетью или БД
|
||
// process.exit(-1);
|
||
});
|
||
|
||
// Обработчик для очистки при завершении процесса
|
||
process.on('SIGINT', async () => {
|
||
// console.log('Closing database pool...'); // Убрано избыточное логирование
|
||
await pool.end();
|
||
process.exit(0);
|
||
});
|
||
|
||
process.on('SIGTERM', async () => {
|
||
// console.log('Closing database pool...'); // Убрано избыточное логирование
|
||
await pool.end();
|
||
process.exit(0);
|
||
});
|
||
|
||
// console.log('Пул создан:', pool.options || pool); // Убрано избыточное логирование
|
||
|
||
// Проверяем подключение к базе данных
|
||
pool.query('SELECT NOW()')
|
||
.then(res => {
|
||
// console.log('Успешное подключение к базе данных:', res.rows[0]); // Убрано избыточное логирование
|
||
})
|
||
.catch(err => {
|
||
console.error('Ошибка подключения к базе данных:', err);
|
||
});
|
||
|
||
function getPool() {
|
||
return pool;
|
||
}
|
||
|
||
function query(text, params) {
|
||
return pool.query(text, params);
|
||
}
|
||
|
||
function getQuery() {
|
||
return (...args) => pool.query(...args);
|
||
}
|
||
|
||
let poolChangeCallback = null;
|
||
|
||
function setPoolChangeCallback(cb) {
|
||
poolChangeCallback = cb;
|
||
}
|
||
|
||
// Функция для пересоздания пула из db_settings
|
||
async function reinitPoolFromDbSettings() {
|
||
try {
|
||
// Используем прямое подключение для получения настроек
|
||
const res = await pool.query('SELECT * FROM db_settings ORDER BY id LIMIT 1');
|
||
if (!res.rows.length) throw new Error('DB settings not found');
|
||
const dbSettings = res.rows[0];
|
||
|
||
// Закрываем старый пул правильно
|
||
console.log('Закрываем старый пул подключений...');
|
||
await pool.end();
|
||
|
||
// Создаём новый пул с расшифрованными настройками и теми же параметрами для предотвращения утечек
|
||
pool = new Pool({
|
||
host: dbSettings.db_host_encrypted ? await decryptValue(dbSettings.db_host_encrypted) : process.env.DB_HOST || 'postgres',
|
||
port: parseInt(dbSettings.db_port || process.env.DB_PORT || '5432'),
|
||
database: dbSettings.db_name_encrypted ? await decryptValue(dbSettings.db_name_encrypted) : process.env.DB_NAME || 'dapp_db',
|
||
user: dbSettings.db_user_encrypted ? await decryptValue(dbSettings.db_user_encrypted) : process.env.DB_USER || 'dapp_user',
|
||
password: dbSettings.db_password_encrypted ? await decryptValue(dbSettings.db_password_encrypted) : process.env.DB_PASSWORD,
|
||
ssl: false,
|
||
// Те же настройки для предотвращения утечек и таймаутов
|
||
max: 100, // Увеличиваем максимальное количество клиентов (было 50)
|
||
min: 10, // Минимальное количество клиентов для лучшей производительности (было 5)
|
||
idleTimeoutMillis: 180000, // Увеличиваем до 180 сек (было 120)
|
||
connectionTimeoutMillis: 180000, // Увеличиваем таймаут подключения до 180 сек (было 120)
|
||
maxUses: 7500,
|
||
allowExitOnIdle: true,
|
||
});
|
||
|
||
// Устанавливаем лимит обработчиков для нового пула
|
||
pool.setMaxListeners(100);
|
||
|
||
// Добавляем обработчик ошибок для нового пула (не завершаем процесс)
|
||
pool.on('error', (err) => {
|
||
console.error('[db] Unexpected error on idle client (reinit):', err.message);
|
||
// НЕ завершаем процесс - это может быть временная проблема
|
||
});
|
||
|
||
// Пересоздаём session middleware
|
||
if (poolChangeCallback) {
|
||
poolChangeCallback();
|
||
}
|
||
console.log('Пул пересоздан с новыми параметрами из зашифрованных настроек');
|
||
} catch (err) {
|
||
console.error('Ошибка пересоздания пула:', err);
|
||
// Используем дефолтные настройки при ошибке
|
||
console.log('Используем дефолтные настройки подключения');
|
||
}
|
||
}
|
||
|
||
// Функция для расшифровки значений
|
||
async function decryptValue(encryptedValue) {
|
||
try {
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (!fs.existsSync(keyPath)) {
|
||
console.warn('Ключ шифрования не найден, используем дефолтные значения');
|
||
return null;
|
||
}
|
||
|
||
const key = fs.readFileSync(keyPath, 'utf8').trim();
|
||
const algorithm = 'aes-256-cbc';
|
||
|
||
// Декодируем base64
|
||
const encryptedBuffer = Buffer.from(encryptedValue, 'base64');
|
||
|
||
// Извлекаем IV (первые 16 байт)
|
||
const iv = encryptedBuffer.slice(0, 16);
|
||
const encryptedData = encryptedBuffer.slice(16);
|
||
|
||
// Расшифровываем
|
||
const decipher = crypto.createDecipher(algorithm, key);
|
||
decipher.setAutoPadding(false);
|
||
|
||
let decrypted = decipher.update(encryptedData, null, 'utf8');
|
||
decrypted += decipher.final('utf8');
|
||
|
||
// Убираем padding
|
||
const paddingLength = decrypted.charCodeAt(decrypted.length - 1);
|
||
decrypted = decrypted.slice(0, decrypted.length - paddingLength);
|
||
|
||
return decrypted;
|
||
} catch (error) {
|
||
console.error('Ошибка расшифровки:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// При старте приложения — убираем автоматический вызов reinitPoolFromDbSettings
|
||
// if (process.env.NODE_ENV !== 'migration') {
|
||
// reinitPoolFromDbSettings();
|
||
// }
|
||
|
||
// Экспортируем функцию для явной инициализации пула
|
||
async function initDbPool() {
|
||
// Отключаем автоматическое пересоздание пула из настроек БД
|
||
// await reinitPoolFromDbSettings();
|
||
console.log('Используем дефолтные настройки подключения к БД');
|
||
}
|
||
|
||
// Функция saveGuestMessageToDatabase удалена - используется UniversalGuestService
|
||
|
||
async function waitForOllamaModel(modelName) {
|
||
const ollamaConfig = require('./services/ollamaConfig');
|
||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||
while (true) {
|
||
try {
|
||
const res = await axios.get(`${ollamaUrl}/api/tags`);
|
||
const models = res.data.models.map(m => m.name);
|
||
if (models.includes(modelName)) {
|
||
return true;
|
||
}
|
||
// console.log(`[seedAIAssistantSettings] Ожидание загрузки модели ${modelName}...`); // Убрано избыточное логирование
|
||
} catch (e) {
|
||
// console.log('[seedAIAssistantSettings] Ollama недоступна, ожидание...'); // Убрано избыточное логирование
|
||
}
|
||
await new Promise(r => setTimeout(r, 5000));
|
||
}
|
||
}
|
||
|
||
async function seedAIAssistantSettings() {
|
||
const modelName = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
|
||
await waitForOllamaModel(modelName);
|
||
const res = await pool.query('SELECT COUNT(*) FROM ai_assistant_settings');
|
||
if (parseInt(res.rows[0].count, 10) === 0) {
|
||
// Получаем ключ шифрования
|
||
const encryptionUtils = require('./utils/encryptionUtils');
|
||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||
|
||
await pool.query(`
|
||
INSERT INTO ai_assistant_settings (system_prompt_encrypted, selected_rag_tables, languages, model_encrypted, rules_id, updated_by)
|
||
VALUES (encrypt_text($1, $6), $2, $3, encrypt_text($4, $6), $5, $7)
|
||
`, [
|
||
'Ты — ИИ-ассистент для бизнеса. Отвечай кратко и по делу.',
|
||
[],
|
||
['ru'],
|
||
modelName,
|
||
1,
|
||
encryptionKey,
|
||
1
|
||
]);
|
||
// console.log('[seedAIAssistantSettings] ai_assistant_settings: инициализировано дефолтными значениями'); // Убрано избыточное логирование
|
||
} else {
|
||
// console.log('[seedAIAssistantSettings] ai_assistant_settings: уже инициализировано, пропускаю'); // Убрано избыточное логирование
|
||
}
|
||
}
|
||
|
||
// Экспортируем функции для работы с базой данных
|
||
module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback, initDbPool, seedAIAssistantSettings };
|