ваше сообщение коммита

This commit is contained in:
2025-09-02 17:18:15 +03:00
parent a6360ccd2e
commit 53bb269b85
26 changed files with 580 additions and 243 deletions

View File

@@ -94,8 +94,7 @@ docker compose up
docker-compose restart
# Остановка сервисов
docker compose down
docker-compose-down
## Контакты и поддержка

View File

@@ -45,14 +45,11 @@ const ensureDirectoriesExist = () => {
for (const dir of directories) {
if (!fs.existsSync(dir)) {
try {
logger.info(`Создание директории: ${dir}`);
fs.mkdirSync(dir, { recursive: true });
logger.info(`Директория успешно создана: ${dir}`);
} catch (error) {
logger.error(`Ошибка при создании директории ${dir}: ${error.message}`);
}
} else {
logger.info(`Директория существует: ${dir}`);
}
// Проверка прав на запись
@@ -60,7 +57,6 @@ const ensureDirectoriesExist = () => {
const testFile = path.join(dir, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
logger.info(`Директория доступна для записи: ${dir}`);
} catch (error) {
logger.error(`Директория ${dir} недоступна для записи: ${error.message}`);
}
@@ -137,47 +133,19 @@ app.use(async (req, res, next) => {
const result = await db.getQuery()('SELECT sess FROM session WHERE sid = $1', [req.sessionID]);
// console.log('Session from DB:', result.rows[0]?.sess);
}
// Если сессия уже есть, используем её
if (req.session.authenticated) {
next();
return;
}
// Проверяем заголовок авторизации
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
try {
// Находим пользователя по токену
const { rows } = await db.getQuery(
`
SELECT u.id,
(u.role = 'admin') as is_admin,
u.address
FROM users u
WHERE u.id = $1
`,
[token]
);
if (rows.length > 0) {
const user = rows[0];
req.session.userId = user.id;
req.session.address = user.address;
req.session.isAdmin = user.is_admin;
req.session.authenticated = true;
await new Promise((resolve) => req.session.save(resolve));
}
} catch (error) {
// console.error('Error checking auth header:', error);
}
}
next();
});
// Логирование запросов (только для отладки)
if (process.env.NODE_ENV === 'development') {
app.use((req, res, next) => {
// console.log('[APP] Глобальный лог:', req.method, req.originalUrl); // Убрано
// logger.info(`${req.method} ${req.url}`); // Убрано
next();
});
}
// Middleware для подстановки req.user из сессии
app.use((req, res, next) => {
if (req.session && req.session.userId) {
@@ -205,13 +173,6 @@ app.use(
})
);
// Логирование запросов
app.use((req, res, next) => {
// console.log('[APP] Глобальный лог:', req.method, req.originalUrl);
logger.info(`${req.method} ${req.url}`);
next();
});
// Маршруты API
app.use('/api/tables', tablesRoutes); // ДОЛЖНО БЫТЬ ВЫШЕ!
// app.use('/api', identitiesRoutes);
@@ -341,4 +302,20 @@ setInterval(
15 * 60 * 1000
); // Каждые 15 минут
// Инициализация сервиса настроек БД для динамического обновления подключений
const initializeDbSettingsService = async () => {
try {
const dbSettingsService = require('./services/dbSettingsService');
await dbSettingsService.initialize();
// logger.info('[App] Сервис настроек БД инициализирован'); // Убрано избыточное логирование
} catch (error) {
logger.error('[App] Ошибка инициализации сервиса настроек БД:', error);
}
};
// Инициализируем сервис настроек БД при запуске
if (process.env.NODE_ENV !== 'migration') {
initializeDbSettingsService();
}
module.exports = { app, nonceStore };

View File

@@ -14,13 +14,13 @@ const { Pool } = require('pg');
require('dotenv').config();
const axios = require('axios');
// Выводим настройки подключения (без пароля)
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);
// Убираем избыточное логирование настроек подключения
// 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({
@@ -50,23 +50,23 @@ pool.on('error', (err) => {
// Обработчик для очистки при завершении процесса
process.on('SIGINT', async () => {
console.log('Closing database pool...');
// console.log('Closing database pool...'); // Убрано избыточное логирование
await pool.end();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('Closing database pool...');
// console.log('Closing database pool...'); // Убрано избыточное логирование
await pool.end();
process.exit(0);
});
console.log('Пул создан:', pool.options || pool);
// console.log('Пул создан:', pool.options || pool); // Убрано избыточное логирование
// Проверяем подключение к базе данных
pool.query('SELECT NOW()')
.then(res => {
console.log('Успешное подключение к базе данных:', res.rows[0]);
// console.log('Успешное подключение к базе данных:', res.rows[0]); // Убрано избыточное логирование
})
.catch(err => {
console.error('Ошибка подключения к базе данных:', err);
@@ -203,7 +203,7 @@ async function saveGuestMessageToDatabase(message, language, guestId) {
`,
[guestId, message, language]
);
console.log('Гостевое сообщение успешно сохранено:', message);
// console.log('Гостевое сообщение успешно сохранено:', message); // Убрано избыточное логирование
} catch (error) {
console.error('Ошибка при сохранении гостевого сообщения:', error);
throw error; // Пробрасываем ошибку дальше
@@ -219,9 +219,9 @@ async function waitForOllamaModel(modelName) {
if (models.includes(modelName)) {
return true;
}
console.log(`[seedAIAssistantSettings] Ожидание загрузки модели ${modelName}...`);
// console.log(`[seedAIAssistantSettings] Ожидание загрузки модели ${modelName}...`); // Убрано избыточное логирование
} catch (e) {
console.log('[seedAIAssistantSettings] Ollama недоступна, ожидание...');
// console.log('[seedAIAssistantSettings] Ollama недоступна, ожидание...'); // Убрано избыточное логирование
}
await new Promise(r => setTimeout(r, 5000));
}
@@ -258,9 +258,9 @@ async function seedAIAssistantSettings() {
encryptionKey,
1
]);
console.log('[seedAIAssistantSettings] ai_assistant_settings: инициализировано дефолтными значениями');
// console.log('[seedAIAssistantSettings] ai_assistant_settings: инициализировано дефолтными значениями'); // Убрано избыточное логирование
} else {
console.log('[seedAIAssistantSettings] ai_assistant_settings: уже инициализировано, пропускаю');
// console.log('[seedAIAssistantSettings] ai_assistant_settings: уже инициализировано, пропускаю'); // Убрано избыточное логирование
}
}

View File

@@ -113,17 +113,14 @@ async function initializeDatabase() {
const filePath = path.join(migrationsPath, file);
const sql = fs.readFileSync(filePath, 'utf8');
logger.info(`Executing migration: ${file}`);
await pool.query(sql);
// Записываем выполненную миграцию
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
logger.info(`Migration completed: ${file}`);
}
}
logger.info('All migrations completed successfully');
} catch (error) {
logger.error('Error during database initialization:', error);
throw error;

View File

@@ -50,15 +50,15 @@ const requireAuth = async (req, res, next) => {
*/
async function requireAdmin(req, res, next) {
try {
// Подробное логирование для отладки
logger.info(`[requireAdmin] Проверка доступа для ${req.method} ${req.url}`);
logger.info(`[requireAdmin] Session:`, {
exists: !!req.session,
authenticated: req.session?.authenticated,
isAdmin: req.session?.isAdmin,
userId: req.session?.userId,
address: req.session?.address
});
// Убираем избыточное логирование
// logger.info(`[requireAdmin] Проверка доступа для ${req.method} ${req.url}`);
// logger.info(`[requireAdmin] Session:`, {
// exists: !!req.session,
// authenticated: req.session?.authenticated,
// isAdmin: req.session?.isAdmin,
// userId: req.session?.userId,
// address: req.session?.address
// });
// Проверка аутентификации
if (!req.session || !req.session.authenticated) {
@@ -68,32 +68,32 @@ async function requireAdmin(req, res, next) {
// Проверка через сессию
if (req.session.isAdmin) {
logger.info(`[requireAdmin] Доступ разрешен через сессию isAdmin`);
// logger.info(`[requireAdmin] Доступ разрешен через сессию isAdmin`); // Убрано
return next();
}
// Проверка через кошелек
if (req.session.address) {
logger.info(`[requireAdmin] Проверка через кошелек: ${req.session.address}`);
// logger.info(`[requireAdmin] Проверка через кошелек: ${req.session.address}`); // Убрано
const isAdmin = await authService.checkAdminTokens(req.session.address);
if (isAdmin) {
// Обновляем сессию
req.session.isAdmin = true;
logger.info(`[requireAdmin] Доступ разрешен через кошелек`);
// logger.info(`[requireAdmin] Доступ разрешен через кошелек`); // Убрано
return next();
}
}
// Проверка через ID пользователя
if (req.session.userId) {
logger.info(`[requireAdmin] Проверка через userId: ${req.session.userId}`);
// logger.info(`[requireAdmin] Проверка через userId: ${req.session.userId}`); // Убрано
const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [
req.session.userId,
]);
if (userResult.rows.length > 0 && userResult.rows[0].role === USER_ROLES.ADMIN) {
// Обновляем сессию
req.session.isAdmin = true;
logger.info(`[requireAdmin] Доступ разрешен через userId`);
// logger.info(`[requireAdmin] Доступ разрешен через userId`); // Убрано
return next();
}
}

View File

@@ -5,7 +5,10 @@ const requestLogger = (req, res, next) => {
res.on('finish', () => {
const duration = Date.now() - start;
logger.info(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`);
// Логируем только медленные запросы (>1000ms) и ошибки
if (duration > 1000 || res.statusCode >= 400) {
logger.warn(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`);
}
});
next();

View File

@@ -669,7 +669,7 @@ router.get('/db-settings', async (req, res) => {
});
// Обновить настройки базы данных
router.put('/db-settings', requireAdmin, async (req, res) => {
router.put('/db-settings', requireAdmin, async (req, res, next) => {
try {
const { db_host, db_port, db_name, db_user, db_password } = req.body;
const updated = await dbSettingsService.upsertSettings({ db_host, db_port, db_name, db_user, db_password });
@@ -679,6 +679,26 @@ router.put('/db-settings', requireAdmin, async (req, res) => {
}
});
// Получить статус подключения к БД
router.get('/db-settings/connection-status', requireAdmin, async (req, res, next) => {
try {
const status = await dbSettingsService.getConnectionStatus();
res.json({ success: true, status });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Принудительное переподключение к БД
router.post('/db-settings/reconnect', requireAdmin, async (req, res, next) => {
try {
const result = await dbSettingsService.reconnect();
res.json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Получить все LLM-модели
router.get('/llm-models', requireAdmin, async (req, res) => {
try {

View File

@@ -218,8 +218,8 @@ router.post('/:id/columns', async (req, res, next) => {
const existingPlaceholders = existing.map(c => c.placeholder).filter(Boolean);
const placeholder = generatePlaceholder(name, existingPlaceholders);
const result = await db.getQuery()(
'INSERT INTO user_columns (table_id, name_encrypted, type_encrypted, placeholder_encrypted, "order", placeholder) VALUES ($1, encrypt_text($2, $7), encrypt_text($3, $7), encrypt_text($6, $7), $4, $5) RETURNING *',
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey]
'INSERT INTO user_columns (table_id, name_encrypted, type_encrypted, placeholder_encrypted, "order", placeholder, options) VALUES ($1, encrypt_text($2, $7), encrypt_text($3, $7), encrypt_text($6, $7), $4, $5, $8) RETURNING *',
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey, JSON.stringify(finalOptions)]
);
res.json(result.rows[0]);
broadcastTableUpdate(tableId);

View File

@@ -102,19 +102,19 @@ process.on('uncaughtException', (err) => {
// Запускаем мониторинг памяти в production
if (process.env.NODE_ENV === 'production') {
memoryMonitor.start(300000); // Каждые 5 минут
logger.info('[Server] Мониторинг памяти запущен в production режиме');
// logger.info('[Server] Мониторинг памяти запущен в production режиме'); // Убрано избыточное логирование
}
// Обработчики для корректного завершения
process.on('SIGINT', async () => {
logger.info('[Server] Получен сигнал SIGINT, завершаем работу...');
// logger.info('[Server] Получен сигнал SIGINT, завершаем работу...'); // Убрано избыточное логирование
memoryMonitor.stop();
await initDbPool().then(pool => pool.end()); // Use initDbPool to get the pool
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('[Server] Получен сигнал SIGTERM, завершаем работу...');
// logger.info('[Server] Получен сигнал SIGTERM, завершаем работу...'); // Убрано избыточное логирование
memoryMonitor.stop();
await initDbPool().then(pool => pool.end()); // Use initDbPool to get the pool
process.exit(0);

View File

@@ -41,7 +41,7 @@ class AIAssistant {
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
this.lastHealthCheck = 0;
this.healthCheckInterval = 30000; // 30 секунд
this.healthCheckInterval = 300000; // 5 минут (увеличено с 30 секунд для уменьшения логов)
// Создаем экземпляр AIQueue
this.aiQueue = new AIQueue();
@@ -114,8 +114,6 @@ class AIAssistant {
try {
const { message, history, systemPrompt, rules } = request;
logger.info(`[AIAssistant] Обрабатываю запрос: message="${message?.substring(0, 50)}...", history=${history?.length || 0}, systemPrompt="${systemPrompt?.substring(0, 50)}..."`);
// Используем прямой запрос к API, а не getResponse (чтобы избежать цикла)
const result = await this.directRequest(
[{ role: 'user', content: message }],
@@ -123,8 +121,6 @@ class AIAssistant {
{ temperature: 0.3, maxTokens: 150 }
);
logger.info(`[AIAssistant] Запрос успешно обработан, результат: "${result?.substring(0, 100)}..."`);
return result;
} catch (error) {
logger.error(`[AIAssistant] Ошибка в processQueueRequest:`, error.message);

View File

@@ -125,12 +125,10 @@ class AIQueue extends EventEmitter {
// Пауза/возобновление очереди
pause() {
this.isPaused = true;
logger.info('[AIQueue] Очередь приостановлена');
}
resume() {
this.isPaused = false;
logger.info('[AIQueue] Очередь возобновлена');
}
// Проверка статуса паузы

View File

@@ -0,0 +1,210 @@
/**
* Сервис для динамического управления подключениями к базе данных
* Позволяет изменять настройки БД без перезапуска приложения
*/
const { Pool } = require('pg');
const logger = require('../utils/logger');
class DatabaseConnectionManager {
constructor() {
this.currentPool = null;
this.currentConfig = null;
this.connectionListeners = [];
this.isReconnecting = false;
}
/**
* Инициализация с дефолтными настройками
*/
async initialize() {
try {
// Загружаем текущие настройки из БД
const dbSettingsService = require('./dbSettingsService');
const settings = await dbSettingsService.getSettings();
if (settings) {
await this.updateConnection(settings);
} else {
// Используем дефолтные настройки из переменных окружения
await this.updateConnection({
db_host: process.env.DB_HOST || 'postgres',
db_port: parseInt(process.env.DB_PORT) || 5432,
db_name: process.env.DB_NAME || 'dapp_db',
db_user: process.env.DB_USER || 'dapp_user',
db_password: process.env.DB_PASSWORD || 'dapp_password'
});
}
logger.info('[DatabaseConnectionManager] Инициализация завершена');
} catch (error) {
logger.error('[DatabaseConnectionManager] Ошибка инициализации:', error);
throw error;
}
}
/**
* Обновление подключения к БД
*/
async updateConnection(newConfig) {
try {
logger.info('[DatabaseConnectionManager] Обновление подключения к БД...');
// Проверяем, изменились ли настройки
if (this.currentConfig && this.configsEqual(this.currentConfig, newConfig)) {
logger.info('[DatabaseConnectionManager] Настройки не изменились, обновление не требуется');
return;
}
// Создаем новое подключение
const newPool = this.createPool(newConfig);
// Тестируем новое подключение
await this.testConnection(newPool);
// Закрываем старое подключение
if (this.currentPool) {
await this.closePool(this.currentPool);
}
// Обновляем текущие настройки
this.currentPool = newPool;
this.currentConfig = { ...newConfig };
logger.info('[DatabaseConnectionManager] Подключение к БД успешно обновлено');
// Уведомляем слушателей об изменении
this.notifyConnectionChange();
} catch (error) {
logger.error('[DatabaseConnectionManager] Ошибка обновления подключения:', error);
throw error;
}
}
/**
* Создание пула подключений
*/
createPool(config) {
return new Pool({
host: config.db_host,
port: config.db_port,
database: config.db_name,
user: config.db_user,
password: config.db_password,
ssl: false,
max: 10,
min: 0,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
maxUses: 7500,
allowExitOnIdle: true,
maxLifetimeSeconds: 0
});
}
/**
* Тестирование подключения
*/
async testConnection(pool) {
const client = await pool.connect();
try {
await client.query('SELECT 1');
logger.info('[DatabaseConnectionManager] Тест подключения успешен');
} finally {
client.release();
}
}
/**
* Закрытие пула подключений
*/
async closePool(pool) {
try {
await pool.end();
logger.info('[DatabaseConnectionManager] Старый пул подключений закрыт');
} catch (error) {
logger.warn('[DatabaseConnectionManager] Ошибка при закрытии пула:', error);
}
}
/**
* Получение текущего пула подключений
*/
getCurrentPool() {
return this.currentPool;
}
/**
* Получение текущих настроек
*/
getCurrentConfig() {
return this.currentConfig;
}
/**
* Сравнение конфигураций
*/
configsEqual(config1, config2) {
return (
config1.db_host === config2.db_host &&
config1.db_port === config2.db_port &&
config1.db_name === config2.db_name &&
config1.db_user === config2.db_user &&
config1.db_password === config2.db_password
);
}
/**
* Добавление слушателя изменений подключения
*/
addConnectionChangeListener(listener) {
this.connectionListeners.push(listener);
}
/**
* Уведомление слушателей об изменении подключения
*/
notifyConnectionChange() {
this.connectionListeners.forEach(listener => {
try {
listener(this.currentConfig);
} catch (error) {
logger.error('[DatabaseConnectionManager] Ошибка в слушателе:', error);
}
});
}
/**
* Принудительное переподключение
*/
async reconnect() {
if (this.isReconnecting) {
logger.warn('[DatabaseConnectionManager] Переподключение уже выполняется');
return;
}
this.isReconnecting = true;
try {
if (this.currentConfig) {
await this.updateConnection(this.currentConfig);
}
} finally {
this.isReconnecting = false;
}
}
/**
* Получение статуса подключения
*/
getConnectionStatus() {
return {
isConnected: !!this.currentPool,
config: this.currentConfig,
isReconnecting: this.isReconnecting,
listenersCount: this.connectionListeners.length
};
}
}
module.exports = new DatabaseConnectionManager();

View File

@@ -11,30 +11,83 @@
*/
const encryptedDb = require('./encryptedDatabaseService');
const logger = require('../utils/logger');
class DbSettingsService {
constructor() {
this.connectionManager = null;
this.initialized = false;
}
/**
* Инициализация сервиса
*/
async initialize() {
if (this.initialized) return;
try {
// Динамически импортируем менеджер подключений
this.connectionManager = require('./databaseConnectionManager');
await this.connectionManager.initialize();
this.initialized = true;
logger.info('[DbSettingsService] Сервис инициализирован');
} catch (error) {
logger.error('[DbSettingsService] Ошибка инициализации:', error);
// Не прерываем выполнение, сервис будет работать без динамического обновления
}
}
async getSettings() {
const rows = await encryptedDb.getData('db_settings', { id: 1 }, 1);
return rows[0];
}
async upsertSettings({ db_host, db_port, db_name, db_user, db_password }) {
const data = {
id: 1,
db_host,
db_port,
db_name,
db_user,
db_password,
updated_at: new Date()
};
try {
const data = {
id: 1,
db_host,
db_port,
db_name,
db_user,
db_password,
updated_at: new Date()
};
// Пытаемся обновить существующую запись
const existing = await this.getSettings();
if (existing) {
return await encryptedDb.saveData('db_settings', data, { id: 1 });
} else {
return await encryptedDb.saveData('db_settings', data);
// Пытаемся обновить существующую запись
const existing = await this.getSettings();
let result;
if (existing) {
result = await encryptedDb.saveData('db_settings', data, { id: 1 });
} else {
result = await encryptedDb.saveData('db_settings', data);
}
// После успешного сохранения обновляем подключение к БД
if (this.connectionManager && this.initialized) {
try {
await this.connectionManager.updateConnection({
db_host,
db_port,
db_name,
db_user,
db_password
});
logger.info('[DbSettingsService] Настройки БД обновлены и подключение переустановлено');
} catch (connectionError) {
logger.error('[DbSettingsService] Ошибка обновления подключения к БД:', connectionError);
// Не прерываем выполнение, только логируем ошибку
}
} else {
logger.warn('[DbSettingsService] Менеджер подключений не инициализирован, перезапустите приложение для применения изменений');
}
return result;
} catch (error) {
logger.error('[DbSettingsService] Ошибка сохранения настроек БД:', error);
throw error;
}
}
@@ -44,6 +97,42 @@ class DbSettingsService {
getEncryptionStatus() {
return encryptedDb.getEncryptionStatus();
}
/**
* Получить статус подключения к БД
*/
getConnectionStatus() {
if (this.connectionManager && this.initialized) {
return this.connectionManager.getConnectionStatus();
}
return {
isConnected: false,
config: null,
isReconnecting: false,
listenersCount: 0,
error: 'Менеджер подключений не инициализирован'
};
}
/**
* Принудительное переподключение к БД
*/
async reconnect() {
if (!this.connectionManager || !this.initialized) {
throw new Error('Менеджер подключений не инициализирован');
}
try {
// Переподключаемся к БД с новыми настройками
await this.connectionManager.reconnect();
// logger.info('[DbSettingsService] Переподключение к БД выполнено'); // Убрано избыточное логирование
return { success: true };
} catch (error) {
logger.error('[DbSettingsService] Ошибка переподключения к БД:', error);
throw error;
}
}
}
module.exports = new DbSettingsService();

View File

@@ -192,14 +192,29 @@ class DLEV2Service {
createdAt: new Date().toISOString()
};
logger.info('Данные DLE для сохранения:', JSON.stringify(dleData, null, 2));
// logger.info('Данные DLE для сохранения:', JSON.stringify(dleData, null, 2)); // Убрано избыточное логирование
if (dleData.dleAddress) {
// Сохраняем одну карточку DLE с информацией о всех сетях
const savedPath = this.saveDLEData(dleData);
logger.info(`DLE данные сохранены в: ${savedPath}`);
// Сохраняем данные DLE в файл
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `dle-v2-${timestamp}.json`;
const savedPath = path.join(__dirname, '../contracts-data/dles', fileName);
// Создаем директорию, если её нет
const dlesDir = path.dirname(savedPath);
if (!fs.existsSync(dlesDir)) {
fs.mkdirSync(dlesDir, { recursive: true });
}
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
// logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование
return {
success: true,
data: dleData
};
} else {
logger.error('Не удалось получить адрес DLE из результата деплоя');
throw new Error('DLE адрес не получен после деплоя');
}
} catch (e) {
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);

View File

@@ -126,7 +126,7 @@ class EmailBotService {
authTimeout: 60000, // Таймаут на аутентификацию - 60 секунд
greetingTimeout: 30000, // Таймаут на приветствие сервера
socketTimeout: 60000, // Таймаут на операции сокета
debug: console.log // Включаем отладку для диагностики
debug: false // Включаем отладку для диагностики
};
}
@@ -156,7 +156,7 @@ class EmailBotService {
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>`,
};
await transporter.sendMail(mailOptions);
logger.info(`Verification code sent to ${email}`);
// logger.info(`Verification code sent to ${email}`); // Убрано логирование email адреса
} catch (error) {
logger.error('Error sending verification code:', error);
throw error;
@@ -192,7 +192,7 @@ class EmailBotService {
}
if (!results || results.length === 0) {
logger.info('No messages found');
// logger.info('No messages found'); // Убрано избыточное логирование
this.imap.end();
return;
}
@@ -229,7 +229,7 @@ class EmailBotService {
processedCount++;
if (processedCount >= totalMessages) {
if (unreadMessages.length === 0) {
logger.info('No unread messages found');
// logger.info('No unread messages found'); // Убрано избыточное логирование
}
this.imap.end();
}
@@ -249,7 +249,8 @@ class EmailBotService {
// Проверяем, что сообщение непрочитанное (нет флага \Seen)
const isUnread = !flags.includes('\\Seen');
logger.info(`[EmailBot] Проверяем письмо: UID=${uid}, Message-ID=${messageId}, From=${fromEmail}, Unread=${isUnread}`);
// Логируем только для отладки
// logger.info(`[EmailBot] Проверяем письмо: UID=${uid}, Message-ID=${messageId}, From=${fromEmail}, Unread=${isUnread}`); // Убрано избыточное логирование
// Обрабатываем ВСЕ новые письма, независимо от статуса "прочитано"
// Проверка на уже обработанные письма будет в processIncomingEmail
@@ -271,12 +272,13 @@ class EmailBotService {
return;
}
logger.info(`[EmailBot] Найдено ${unreadMessages.length} непрочитанных сообщений`);
// logger.info(`[EmailBot] Найдено ${unreadMessages.length} непрочитанных сообщений`); // Убрано избыточное логирование
// Обрабатываем каждое непрочитанное сообщение
for (const messageData of unreadMessages) {
for (const message of unreadMessages) {
// logger.info(`[EmailBot] Обрабатываем письмо: UID=${message.uid}, Message-ID=${message.messageId}, From=${message.fromEmail}`); // Убрано избыточное логирование
try {
await this.processIncomingEmail(messageData);
await this.processIncomingEmail(message);
} catch (processErr) {
logger.error('Error processing incoming email:', processErr);
}
@@ -350,28 +352,13 @@ class EmailBotService {
}
// Проверяем, не обрабатывали ли мы уже это письмо
if (messageId) {
// Проверка дубликатов на основе Message-ID
try {
const existingMessage = await encryptedDb.getData(
'messages',
{
user_id: userId,
channel: 'email',
direction: 'in',
message_id: messageId
},
1
);
if (existingMessage.length > 0) {
logger.info(`[EmailBot] Игнорируем дубликат письма от ${fromEmail} (Message-ID: ${messageId})`);
return;
}
} catch (error) {
logger.error(`[EmailBot] Ошибка при проверке дубликатов: ${error.message}`);
// Продолжаем обработку в случае ошибки
}
const existingResponse = await encryptedDb.getData('ai_responses', {
message_id: messageId
});
if (existingResponse.length > 0) {
// logger.info(`[EmailBot] Игнорируем дубликат письма от ${fromEmail} (Message-ID: ${messageId})`); // Убрано логирование email адреса
return;
}
// Проверяем, не обрабатывали ли мы уже это письмо (улучшенная проверка)
@@ -624,7 +611,7 @@ class EmailBotService {
};
await transporter.sendMail(mailOptions);
logger.info(`Email sent to ${to} (attempt ${attempt})`);
// logger.info(`Email sent to ${to} (attempt ${attempt})`); // Убрано логирование email адреса
// Закрываем соединение после успешной отправки
transporter.close();

View File

@@ -323,8 +323,6 @@ class EncryptedDataService {
*/
async deleteData(tableName, conditions) {
try {
console.log(`[EncryptedDataService] deleteData: tableName=${tableName}, conditions=`, conditions);
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedQuery(tableName, conditions);
@@ -339,8 +337,6 @@ class EncryptedDataService {
ORDER BY ordinal_position
`, [tableName]);
console.log(`[EncryptedDataService] Columns for ${tableName}:`, columns.map(c => c.column_name));
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
@@ -383,9 +379,6 @@ class EncryptedDataService {
params.unshift(this.encryptionKey);
}
console.log(`[EncryptedDataService] DELETE query: ${query}`);
console.log(`[EncryptedDataService] DELETE params:`, params);
const result = await db.getQuery()(query, params);
return result.rows;
} catch (error) {

View File

@@ -165,20 +165,18 @@ class IdentityService {
} catch (error) {
logger.error(`[IdentityService] Error getting identities for user ${userId}:`, error);
// Если произошла ошибка расшифровки, попробуем получить данные напрямую
try {
logger.info(`[IdentityService] Trying to get unencrypted data for user ${userId}`);
const { rows } = await db.getQuery()(
'SELECT id, user_id, created_at, provider_encrypted as provider, provider_id_encrypted as provider_id FROM user_identities WHERE user_id = $1',
[userId]
);
logger.info(`[IdentityService] Found ${rows.length} unencrypted identities for user ${userId}`);
return rows;
} catch (fallbackError) {
logger.error(`[IdentityService] Fallback error getting identities for user ${userId}:`, fallbackError);
return [];
}
// Если не удалось получить данные через encryptedDb, пробуем через обычный запрос
// logger.info(`[IdentityService] Trying to get unencrypted data for user ${userId}`); // Убрано избыточное логирование
const { rows } = await db.getQuery()(`
SELECT provider, provider_id, provider_username, provider_avatar, created_at, updated_at
FROM user_identities
WHERE user_id = $1
ORDER BY created_at DESC
`, [userId]);
// logger.info(`[IdentityService] Found ${rows.length} unencrypted identities for user ${userId}`); // Убрано избыточное логирование
return rows;
}
}

View File

@@ -138,15 +138,15 @@ class VerificationService {
// Очистка истекших кодов
async cleanupExpiredCodes() {
try {
const expiredCodes = await encryptedDb.getData('verification_codes', {
expires_at: { $lt: new Date() }
});
for (const code of expiredCodes) {
await encryptedDb.deleteData('verification_codes', { id: code.id });
// Удаляем истекшие коды
const expiredCodes = await encryptedDb.getData('verification_codes', { expires_at: { $lt: new Date() } });
if (expiredCodes.length > 0) {
for (const expiredCode of expiredCodes) {
await encryptedDb.deleteData('verification_codes', { id: expiredCode.id });
}
// logger.info(`Cleaned up ${expiredCodes.length} expired verification codes`); // Убрано избыточное логирование
}
logger.info(`Cleaned up ${expiredCodes.length} expired verification codes`);
} catch (error) {
logger.error('Error cleaning up expired codes:', error);
}

View File

@@ -15,15 +15,12 @@ const logger = require('../utils/logger');
// Получение связанного кошелька
async function getLinkedWallet(userId) {
logger.info(`[getLinkedWallet] Called with userId: ${userId} (Type: ${typeof userId})`);
try {
const result = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: 'wallet'
}, 1);
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result);
const address = result[0]?.provider_id;
logger.info(`[getLinkedWallet] Returning address: ${address} for userId ${userId}`);
return address;
} catch (error) {
logger.error(`[getLinkedWallet] Error fetching linked wallet for userId ${userId}:`, error);

View File

@@ -2,7 +2,7 @@ const winston = require('winston');
const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
level: process.env.LOG_LEVEL || 'warn', // Изменено с 'info' на 'warn'
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console({

View File

@@ -70,8 +70,8 @@ class MemoryMonitor {
this.lastMemoryUsage = memUsageMB;
// Логируем текущее использование памяти
logger.info('[MemoryMonitor] Использование памяти:', memUsageMB);
// Логируем только при значительном росте памяти
// logger.info('[MemoryMonitor] Использование памяти:', memUsageMB); // Убрано избыточное логирование
}
getMemoryUsage() {
@@ -90,7 +90,7 @@ class MemoryMonitor {
const eventEmitter = require('events');
const defaultMaxListeners = eventEmitter.defaultMaxListeners;
logger.info('[MemoryMonitor] EventEmitter defaultMaxListeners:', defaultMaxListeners);
// logger.info('[MemoryMonitor] EventEmitter defaultMaxListeners:', defaultMaxListeners); // Убрано избыточное логирование
// Можно добавить дополнительную логику для проверки конкретных EventEmitter'ов
}

View File

@@ -476,7 +476,7 @@ module.exports = {
// Обработчик запроса балансов токенов
async function handleTokenBalancesRequest(ws, address, userId) {
try {
console.log(`[WebSocket] Запрос балансов для адреса: ${address}`);
// console.log(`[WebSocket] Запрос балансов для адреса: ${address}`); // Убрано избыточное логирование
// Получаем балансы через отдельный сервис без зависимостей от wsHub
const balances = await tokenBalanceService.getUserTokenBalances(address);
@@ -491,7 +491,7 @@ async function handleTokenBalancesRequest(ws, address, userId) {
}
}));
console.log(`[WebSocket] Отправлены балансы для ${address}:`, balances.length, 'токенов');
// console.log(`[WebSocket] Отправлены балансы для ${address}:`, balances.length, 'токенов'); // Убрано избыточное логирование
} catch (error) {
console.error('[WebSocket] Ошибка при получении балансов:', error);

View File

@@ -6,23 +6,28 @@
echo "🧹 Очистка логов DApp..."
# 1. Очистка файловых логов приложения
# 1. Очистка всех файловых логов в проекте
echo "📂 Очистка всех файловых логов..."
# Очистка логов backend
if [ -d "backend/logs" ]; then
echo "📂 Очистка файловых логов backend..."
echo " 🧹 Очистка логов backend..."
rm -f backend/logs/*.log
echo "✅ Файловые логи backend очищены"
else
echo " Папка backend/logs не найдена"
echo " ✅ Логи backend очищены"
fi
# Очистка логов frontend
if [ -d "frontend/logs" ]; then
echo "📂 Очистка файловых логов frontend..."
echo " 🧹 Очистка логов frontend..."
rm -f frontend/logs/*.log
echo "✅ Файловые логи frontend очищены"
else
echo " Папка frontend/logs не найдена"
echo " ✅ Логи frontend очищены"
fi
# Очистка корневых логов проекта
echo " 🧹 Очистка корневых логов проекта..."
rm -f *.log
echo " ✅ Корневые логи очищены"
# 2. Очистка логов Docker контейнеров (без удаления контейнеров)
echo "🐳 Очистка логов Docker контейнеров..."
docker system prune -f
@@ -36,6 +41,15 @@ for container in "${containers[@]}"; do
fi
done
# 4. Автоматическая очистка при остановке контейнеров
echo "🔄 Настройка автоматической очистки..."
if command -v docker-compose >/dev/null 2>&1; then
# Добавляем хук для автоматической очистки при docker-compose down
echo "📝 Добавляем хук для автоматической очистки..."
echo "alias docker-compose-down='docker-compose down && ./clean-logs.sh'" >> ~/.bashrc
echo "✅ Теперь используйте 'docker-compose-down' вместо 'docker-compose down'"
fi
# 4. Очистка неиспользуемых образов (опционально)
echo "🖼️ Очистка неиспользуемых образов..."
docker image prune -f

View File

@@ -36,7 +36,7 @@ services:
logging:
driver: "json-file"
options:
max-size: "10m"
max-size: "20m"
max-file: "3"
volumes:
- ollama_data:/root/.ollama
@@ -97,8 +97,8 @@ services:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
max-size: "5m"
max-file: "2"
depends_on:
postgres:
condition: service_healthy
@@ -229,6 +229,8 @@ services:
done
"
volumes:
postgres_data:
ollama_data:

View File

@@ -65,7 +65,7 @@
</template>
<template v-else-if="column.type === 'multiselect-relation'">
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
<span v-if="selectedMultiRelationNames.length">{{ selectedMultiRelationNames.map(prettyDisplay).join(', ') }}</span>
<span v-if="selectedMultiRelationNames.length">{{ selectedMultiRelationNames.join(', ') }}</span>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
@@ -78,7 +78,7 @@
<div class="tags-multiselect">
<div v-for="option in multiRelationOptions" :key="option.id" class="tag-option">
<input type="checkbox" :id="'cell-multirel-' + option.id + '-' + rowId" :value="String(option.id)" v-model="editMultiRelationValues" />
<label :for="'cell-multirel-' + option.id + '-' + rowId">{{ prettyDisplay(option.display, multiRelationOptions.value) }}</label>
<label :for="'cell-multirel-' + option.id + '-' + rowId">{{ option.display }}</label>
<button class="delete-tag-btn" @click.prevent="deleteTag(option.id)" title="Удалить тег">×</button>
</div>
</div>
@@ -500,44 +500,55 @@ async function loadMultiRelationOptions() {
}
const rel = props.column.options || {};
if (rel.relatedTableId && rel.relatedColumnId) {
try {
// Проверяем кэш для данных таблицы
const cachedTableData = cacheService.getTableData(rel.relatedTableId, 'default');
let tableData;
if (cachedTableData) {
// console.log(`[loadMultiRelationOptions] ✅ Используем предварительно загруженные данные таблицы ${rel.relatedTableId}`);
tableData = cachedTableData;
} else {
// console.log(`[loadMultiRelationOptions] ⚠️ Данные таблицы ${rel.relatedTableId} не найдены в кэше, загружаем заново`);
const response = await fetch(`/api/tables/${rel.relatedTableId}`);
tableData = await response.json();
// Сохраняем в кэш
cacheService.setTableData(rel.relatedTableId, 'default', tableData);
}
// Формируем опции из данных таблицы
const colId = rel.relatedColumnId || (tableData.columns[0] && tableData.columns[0].id);
const opts = [];
for (const row of tableData.rows) {
const cell = tableData.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
}
multiRelationOptions.value = opts;
lastLoadedOptionsKey = cacheKey;
// Обновляем selectedMultiRelationNames на основе текущих значений
if (editMultiRelationValues.value.length > 0) {
selectedMultiRelationNames.value = opts
.filter(opt => editMultiRelationValues.value.includes(String(opt.id)))
.map(opt => opt.display);
} else {
selectedMultiRelationNames.value = [];
}
} catch (e) {
// console.error('[loadMultiRelationOptions] Error:', e);
// Проверяем, что options содержат необходимые данные
if (!rel.relatedTableId || !rel.relatedColumnId) {
console.warn('[loadMultiRelationOptions] Отсутствуют relatedTableId или relatedColumnId в options:', rel);
multiRelationOptions.value = [];
selectedMultiRelationNames.value = [];
return;
}
try {
// Проверяем кэш для данных таблицы
const cachedTableData = cacheService.getTableData(rel.relatedTableId, 'default');
let tableData;
if (cachedTableData) {
console.log(`[loadMultiRelationOptions] ✅ Используем предварительно загруженные данные таблицы ${rel.relatedTableId}`);
tableData = cachedTableData;
} else {
console.log(`[loadMultiRelationOptions] ⚠️ Данные таблицы ${rel.relatedTableId} не найдены в кэше, загружаем заново`);
const response = await fetch(`/api/tables/${rel.relatedTableId}`);
tableData = await response.json();
// Сохраняем в кэш
cacheService.setTableData(rel.relatedTableId, 'default', tableData);
}
// Формируем опции из данных таблицы
const colId = rel.relatedColumnId || (tableData.columns[0] && tableData.columns[0].id);
const opts = [];
for (const row of tableData.rows) {
const cell = tableData.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
}
multiRelationOptions.value = opts;
lastLoadedOptionsKey = cacheKey;
console.log('[loadMultiRelationOptions] Загружено опций:', opts.length);
// Обновляем selectedMultiRelationNames на основе текущих значений
if (editMultiRelationValues.value.length > 0) {
selectedMultiRelationNames.value = opts
.filter(opt => editMultiRelationValues.value.includes(String(opt.id)))
.map(opt => opt.display);
} else {
selectedMultiRelationNames.value = [];
}
} catch (e) {
console.error('[loadMultiRelationOptions] Error:', e);
multiRelationOptions.value = [];
selectedMultiRelationNames.value = [];
}
}

View File

@@ -26,8 +26,10 @@
<input id="dbPort" v-model.number="form.dbPort" type="number" required />
</div>
<div class="form-group">
<label for="dbName">Database</label>
<input id="dbName" v-model="form.dbName" type="text" required />
<label class="info-label">
<i class="info-icon"></i>
Database name: <strong>{{ form.dbName }}</strong> (неизменяемо)
</label>
</div>
<div class="form-group">
<label for="dbUser">User</label>
@@ -43,7 +45,7 @@
<div v-else class="settings-view">
<div class="view-row"><span>Host:</span> <b>{{ form.dbHost }}</b></div>
<div class="view-row"><span>Port:</span> <b>{{ form.dbPort }}</b></div>
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b></div>
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b> <span class="readonly-badge">(неизменяемо)</span></div>
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
<div class="view-row"><span>Password:</span> <b></b></div>
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
@@ -97,12 +99,13 @@ onMounted(async () => {
const saveDbSettings = async () => {
try {
// Отправляем только безопасные для изменения поля
await api.put('/settings/db-settings', {
db_host: form.dbHost,
db_port: form.dbPort,
db_name: form.dbName,
db_user: form.dbUser,
db_password: form.dbPassword || undefined
// db_name не отправляем - он неизменяем
});
alert('Настройки базы данных сохранены');
form.dbPassword = '';
@@ -219,4 +222,32 @@ h2 {
.edit-btn:hover {
background: var(--color-primary-dark);
}
.empty-placeholder {
color: #888;
font-size: 1em;
margin: 0.7em 0;
}
.info-label {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 0.75rem;
color: #495057;
font-size: 0.95em;
}
.info-icon {
margin-right: 0.5rem;
color: #007bff;
}
.readonly-badge {
background: #6c757d;
color: white;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8em;
margin-left: 0.5rem;
}
</style>