ваше сообщение коммита
This commit is contained in:
@@ -21,6 +21,7 @@ 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
|
||||||
|
const { warmupModel } = require('./scripts/warmup-model'); // Добавляем импорт разогрева модели
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const messagesRoutes = require('./routes/messages');
|
const messagesRoutes = require('./routes/messages');
|
||||||
@@ -97,7 +98,9 @@ app.use(
|
|||||||
cors({
|
cors({
|
||||||
origin: [
|
origin: [
|
||||||
'http://localhost:5173',
|
'http://localhost:5173',
|
||||||
'http://127.0.0.1:5173', // Добавляем альтернативный origin
|
'http://127.0.0.1:5173',
|
||||||
|
'https://hb3-accelerator.com',
|
||||||
|
'https://www.hb3-accelerator.com',
|
||||||
],
|
],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
|||||||
@@ -262,6 +262,37 @@ router.get('/conversations', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/conversations - создать беседу для пользователя
|
||||||
|
router.post('/conversations', async (req, res) => {
|
||||||
|
const { userId, title } = req.body;
|
||||||
|
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||||||
|
|
||||||
|
// Получаем ключ шифрования
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversationTitle = title || `Чат с пользователем ${userId}`;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||||||
|
[userId, conversationTitle, encryptionKey]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: 'DB error', details: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Массовая рассылка сообщения во все каналы пользователя
|
// Массовая рассылка сообщения во все каналы пользователя
|
||||||
router.post('/broadcast', async (req, res) => {
|
router.post('/broadcast', async (req, res) => {
|
||||||
const { user_id, content } = req.body;
|
const { user_id, content } = req.body;
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
|
const aiAssistant = require('../services/ai-assistant');
|
||||||
|
const aiCache = require('../services/ai-cache');
|
||||||
|
const aiQueue = require('../services/ai-queue');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const results = {};
|
const results = {};
|
||||||
@@ -50,4 +54,71 @@ router.get('/', async (req, res) => {
|
|||||||
res.json({ status: 'ok', services: results, timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', services: results, timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/monitoring/ai-stats - статистика AI
|
||||||
|
router.get('/ai-stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const aiHealth = await aiAssistant.checkHealth();
|
||||||
|
const cacheStats = aiCache.getStats();
|
||||||
|
const queueStats = aiQueue.getStats();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
ai: {
|
||||||
|
health: aiHealth,
|
||||||
|
model: process.env.OLLAMA_MODEL || 'qwen2.5:7b',
|
||||||
|
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
...cacheStats,
|
||||||
|
hitRate: `${(cacheStats.hitRate * 100).toFixed(1)}%`
|
||||||
|
},
|
||||||
|
queue: {
|
||||||
|
...queueStats,
|
||||||
|
avgResponseTime: `${queueStats.avgResponseTime.toFixed(0)}ms`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting AI stats:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/monitoring/ai-cache/clear - очистка кэша
|
||||||
|
router.post('/ai-cache/clear', async (req, res) => {
|
||||||
|
try {
|
||||||
|
aiCache.clear();
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
message: 'AI cache cleared successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error clearing AI cache:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/monitoring/ai-queue/clear - очистка очереди
|
||||||
|
router.post('/ai-queue/clear', async (req, res) => {
|
||||||
|
try {
|
||||||
|
aiQueue.clear();
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
message: 'AI queue cleared successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error clearing AI queue:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -338,53 +338,7 @@ router.get('/:id/rows', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Изменить значение ячейки (доступно всем)
|
|
||||||
router.patch('/cell/:cellId', async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const cellId = req.params.cellId;
|
|
||||||
const { value } = req.body;
|
|
||||||
// Получаем ключ шифрования
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
let encryptionKey = 'default-key';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
|
||||||
if (fs.existsSync(keyPath)) {
|
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
|
||||||
}
|
|
||||||
} catch (keyError) {
|
|
||||||
console.error('Error reading encryption key:', keyError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.getQuery()(
|
|
||||||
'UPDATE user_cell_values SET value_encrypted = encrypt_text($1, $3), updated_at = NOW() WHERE id = $2 RETURNING *',
|
|
||||||
[value, cellId, encryptionKey]
|
|
||||||
);
|
|
||||||
// Получаем row_id и table_id
|
|
||||||
const row = (await db.getQuery()('SELECT row_id FROM user_cell_values WHERE id = $1', [cellId])).rows[0];
|
|
||||||
if (row) {
|
|
||||||
const rowId = row.row_id;
|
|
||||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
|
||||||
if (table) {
|
|
||||||
const tableId = table.table_id;
|
|
||||||
// Получаем всю строку для upsert
|
|
||||||
const rowData = (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.id = $1', [rowId, encryptionKey])).rows[0];
|
|
||||||
if (rowData) {
|
|
||||||
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
|
|
||||||
console.log('[DEBUG][upsertRows]', upsertRows);
|
|
||||||
if (upsertRows.length > 0) {
|
|
||||||
await vectorSearchClient.upsert(tableId, upsertRows);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.json(result.rows[0]);
|
|
||||||
broadcastTableUpdate(tableId);
|
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создать/обновить значение ячейки (upsert) (доступно всем)
|
// Создать/обновить значение ячейки (upsert) (доступно всем)
|
||||||
router.post('/cell', async (req, res, next) => {
|
router.post('/cell', async (req, res, next) => {
|
||||||
@@ -416,13 +370,15 @@ router.post('/cell', async (req, res, next) => {
|
|||||||
if (table) {
|
if (table) {
|
||||||
const tableId = table.table_id;
|
const tableId = table.table_id;
|
||||||
|
|
||||||
// Проверяем, является ли это таблицей "Теги клиентов" - ОТКЛЮЧАЕМ WebSocket
|
// Проверяем, является ли это таблицей "Теги клиентов"
|
||||||
// const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
|
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
|
||||||
// if (tableName && tableName.name === 'Теги клиентов') {
|
console.log('🔄 [Tables] Проверяем таблицу:', { tableId, tableName: tableName?.name });
|
||||||
// // Отправляем WebSocket уведомление об обновлении тегов
|
if (tableName && tableName.name === 'Теги клиентов') {
|
||||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
// Отправляем WebSocket уведомление об обновлении тегов
|
||||||
// broadcastTagsUpdate();
|
console.log('🔄 [Tables] Обновление ячейки в таблице тегов, отправляем уведомление');
|
||||||
// }
|
const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
|
broadcastTagsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем всю строку для upsert
|
// Получаем всю строку для upsert
|
||||||
const rowData = (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.id = $1', [row_id, encryptionKey])).rows[0];
|
const rowData = (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.id = $1', [row_id, encryptionKey])).rows[0];
|
||||||
@@ -499,11 +455,12 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем WebSocket уведомление, если это была таблица тегов - ОТКЛЮЧАЕМ
|
// Отправляем WebSocket уведомление, если это была таблица тегов
|
||||||
// if (isTagsTable) {
|
if (isTagsTable) {
|
||||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
console.log('🔄 [Tables] Обновление строки в таблице тегов, отправляем уведомление');
|
||||||
// broadcastTagsUpdate();
|
const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
// }
|
broadcastTagsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
// Отправляем WebSocket уведомление об обновлении таблицы
|
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||||
const { broadcastTableUpdate } = require('../wsHub');
|
const { broadcastTableUpdate } = require('../wsHub');
|
||||||
@@ -790,6 +747,31 @@ router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
|
|||||||
const { tableId, rowId } = req.params;
|
const { tableId, rowId } = req.params;
|
||||||
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 path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
|
||||||
|
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||||
|
console.log('🔄 [Tables] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
|
||||||
|
|
||||||
|
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
|
||||||
|
console.log('🔄 [Tables] Multirelations: обновление тегов, отправляем уведомление');
|
||||||
|
const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
|
broadcastTagsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем старые связи для этой строки/столбца
|
// Удаляем старые связи для этой строки/столбца
|
||||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||||
// Добавляем новые связи
|
// Добавляем новые связи
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -120,13 +120,16 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
// Фильтр по поиску
|
// Фильтр по поиску
|
||||||
if (search) {
|
if (search) {
|
||||||
where.push(`(
|
where.push(`(
|
||||||
LOWER(u.first_name) LIKE $${idx} OR
|
LOWER(decrypt_text(u.first_name_encrypted, $${idx++})) LIKE $${idx++} OR
|
||||||
LOWER(u.last_name) LIKE $${idx} OR
|
LOWER(decrypt_text(u.last_name_encrypted, $${idx++})) LIKE $${idx++} OR
|
||||||
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(decrypt_text(ui.provider_id_encrypted, $${idx + 1})) LIKE $${idx})
|
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(decrypt_text(ui.provider_id_encrypted, $${idx++})) LIKE $${idx++})
|
||||||
)`);
|
)`);
|
||||||
|
params.push(encryptionKey); // Для first_name_encrypted
|
||||||
|
params.push(`%${search.toLowerCase()}%`);
|
||||||
|
params.push(encryptionKey); // Для last_name_encrypted
|
||||||
|
params.push(`%${search.toLowerCase()}%`);
|
||||||
|
params.push(encryptionKey); // Для provider_id_encrypted
|
||||||
params.push(`%${search.toLowerCase()}%`);
|
params.push(`%${search.toLowerCase()}%`);
|
||||||
params.push(encryptionKey);
|
|
||||||
idx += 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по блокировке
|
// Фильтр по блокировке
|
||||||
@@ -138,13 +141,22 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
|
|
||||||
// --- Основной SQL ---
|
// --- Основной SQL ---
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT u.id, u.first_name, u.last_name, u.created_at, u.preferred_language, u.is_blocked,
|
SELECT u.id,
|
||||||
|
CASE
|
||||||
|
WHEN u.first_name_encrypted IS NULL OR u.first_name_encrypted = '' THEN NULL
|
||||||
|
ELSE decrypt_text(u.first_name_encrypted, $${idx++})
|
||||||
|
END as first_name,
|
||||||
|
CASE
|
||||||
|
WHEN u.last_name_encrypted IS NULL OR u.last_name_encrypted = '' THEN NULL
|
||||||
|
ELSE decrypt_text(u.last_name_encrypted, $${idx++})
|
||||||
|
END as last_name,
|
||||||
|
u.created_at, u.preferred_language, u.is_blocked,
|
||||||
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email,
|
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email,
|
||||||
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram,
|
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram,
|
||||||
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet
|
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet
|
||||||
FROM users u
|
FROM users u
|
||||||
`;
|
`;
|
||||||
params.push(encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey);
|
params.push(encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey);
|
||||||
|
|
||||||
// Фильтрация по тегам
|
// Фильтрация по тегам
|
||||||
if (tagIds) {
|
if (tagIds) {
|
||||||
@@ -298,13 +310,55 @@ router.patch('/:id/unblock', requireAuth, async (req, res) => {
|
|||||||
router.patch('/:id', requireAuth, async (req, res) => {
|
router.patch('/:id', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { first_name, last_name, preferred_language, is_blocked } = req.body;
|
const { first_name, last_name, name, preferred_language, language, is_blocked } = req.body;
|
||||||
const fields = [];
|
const fields = [];
|
||||||
const values = [];
|
const values = [];
|
||||||
let idx = 1;
|
let idx = 1;
|
||||||
if (first_name !== undefined) { fields.push(`first_name = $${idx++}`); values.push(first_name); }
|
|
||||||
if (last_name !== undefined) { fields.push(`last_name = $${idx++}`); values.push(last_name); }
|
// Получаем ключ шифрования один раз
|
||||||
if (preferred_language !== undefined) { fields.push(`preferred_language = $${idx++}`); values.push(JSON.stringify(preferred_language)); }
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
try {
|
||||||
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
console.log('Encryption key loaded:', encryptionKey.length, 'characters');
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка поля name - разбиваем на first_name и last_name
|
||||||
|
if (name !== undefined) {
|
||||||
|
const nameParts = name.trim().split(' ');
|
||||||
|
const firstName = nameParts[0] || '';
|
||||||
|
const lastName = nameParts.slice(1).join(' ') || '';
|
||||||
|
fields.push(`first_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||||
|
values.push(firstName);
|
||||||
|
values.push(encryptionKey);
|
||||||
|
fields.push(`last_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||||
|
values.push(lastName);
|
||||||
|
values.push(encryptionKey);
|
||||||
|
} else {
|
||||||
|
if (first_name !== undefined) {
|
||||||
|
fields.push(`first_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||||
|
values.push(first_name);
|
||||||
|
values.push(encryptionKey);
|
||||||
|
}
|
||||||
|
if (last_name !== undefined) {
|
||||||
|
fields.push(`last_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||||
|
values.push(last_name);
|
||||||
|
values.push(encryptionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка поля language (alias для preferred_language)
|
||||||
|
const languageToUpdate = language !== undefined ? language : preferred_language;
|
||||||
|
if (languageToUpdate !== undefined) {
|
||||||
|
fields.push(`preferred_language = $${idx++}`);
|
||||||
|
values.push(JSON.stringify(languageToUpdate));
|
||||||
|
}
|
||||||
if (is_blocked !== undefined) {
|
if (is_blocked !== undefined) {
|
||||||
fields.push(`is_blocked = $${idx++}`);
|
fields.push(`is_blocked = $${idx++}`);
|
||||||
values.push(is_blocked);
|
values.push(is_blocked);
|
||||||
@@ -318,6 +372,7 @@ router.patch('/:id', requireAuth, async (req, res) => {
|
|||||||
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = $${idx}`;
|
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = $${idx}`;
|
||||||
values.push(userId);
|
values.push(userId);
|
||||||
await db.query(sql, values);
|
await db.query(sql, values);
|
||||||
|
broadcastContactsUpdate();
|
||||||
res.json({ success: true, message: 'Пользователь обновлен' });
|
res.json({ success: true, message: 'Пользователь обновлен' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Ошибка обновления пользователя:', e);
|
logger.error('Ошибка обновления пользователя:', e);
|
||||||
@@ -326,7 +381,7 @@ router.patch('/:id', requireAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/users/:id — удалить контакт и все связанные данные
|
// DELETE /api/users/:id — удалить контакт и все связанные данные
|
||||||
router.delete('/:id', async (req, res) => {
|
router.delete('/:id', requireAuth, async (req, res) => {
|
||||||
console.log('[users.js] DELETE HANDLER', req.params.id);
|
console.log('[users.js] DELETE HANDLER', req.params.id);
|
||||||
const userId = Number(req.params.id);
|
const userId = Number(req.params.id);
|
||||||
console.log('[ROUTER] Перед вызовом deleteUserById для userId:', userId);
|
console.log('[ROUTER] Перед вызовом deleteUserById для userId:', userId);
|
||||||
@@ -354,7 +409,7 @@ router.get('/:id', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -401,7 +456,7 @@ router.post('/', async (req, res) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -430,7 +485,7 @@ router.post('/import', requireAuth, async (req, res) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -525,9 +580,9 @@ router.patch('/:id/tags', async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
|
// Отправляем WebSocket уведомление об обновлении тегов
|
||||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
// broadcastTagsUpdate();
|
broadcastTagsUpdate();
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -559,9 +614,9 @@ router.delete('/:id/tags/:tagId', async (req, res) => {
|
|||||||
[userId, tagId]
|
[userId, tagId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
|
// Отправляем WebSocket уведомление об обновлении тегов
|
||||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
// broadcastTagsUpdate();
|
broadcastTagsUpdate();
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
69
backend/scripts/warmup-model.js
Normal file
69
backend/scripts/warmup-model.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрипт для разогрева модели Ollama
|
||||||
|
* Запускается при старте backend для ускорения первых запросов
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://ollama:11434';
|
||||||
|
const MODEL_NAME = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
|
||||||
|
|
||||||
|
async function warmupModel() {
|
||||||
|
console.log('🔥 Разогрев модели Ollama...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем доступность Ollama
|
||||||
|
const healthResponse = await fetch(`${OLLAMA_URL}/api/tags`);
|
||||||
|
if (!healthResponse.ok) {
|
||||||
|
throw new Error(`Ollama недоступен: ${healthResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Ollama доступен');
|
||||||
|
|
||||||
|
// Отправляем простой запрос для разогрева
|
||||||
|
const warmupResponse = await fetch(`${OLLAMA_URL}/v1/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: MODEL_NAME,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'Ты полезный ассистент.' },
|
||||||
|
{ role: 'user', content: 'Привет! Как дела?' }
|
||||||
|
],
|
||||||
|
stream: false,
|
||||||
|
options: {
|
||||||
|
temperature: 0.3,
|
||||||
|
num_predict: 50,
|
||||||
|
num_ctx: 512,
|
||||||
|
num_thread: 8,
|
||||||
|
num_gpu: 1,
|
||||||
|
num_gqa: 8,
|
||||||
|
rope_freq_base: 1000000,
|
||||||
|
rope_freq_scale: 0.5,
|
||||||
|
repeat_penalty: 1.1,
|
||||||
|
top_k: 40,
|
||||||
|
top_p: 0.9,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!warmupResponse.ok) {
|
||||||
|
throw new Error(`Ошибка разогрева: ${warmupResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await warmupResponse.json();
|
||||||
|
console.log('✅ Модель разогрета успешно');
|
||||||
|
console.log(`📝 Ответ модели: ${data.choices?.[0]?.message?.content?.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка разогрева модели:', error.message);
|
||||||
|
// Не прерываем запуск приложения
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем разогрев с задержкой
|
||||||
|
setTimeout(warmupModel, 5000);
|
||||||
|
|
||||||
|
module.exports = { warmupModel };
|
||||||
@@ -18,6 +18,7 @@ const logger = require('./utils/logger');
|
|||||||
const { getBot } = require('./services/telegramBot');
|
const { getBot } = require('./services/telegramBot');
|
||||||
const EmailBotService = require('./services/emailBot');
|
const EmailBotService = require('./services/emailBot');
|
||||||
const { initDbPool, seedAIAssistantSettings } = require('./db');
|
const { initDbPool, seedAIAssistantSettings } = require('./db');
|
||||||
|
const { warmupModel } = require('./scripts/warmup-model'); // Добавляем импорт разогрева модели
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
@@ -64,6 +65,15 @@ initWSS(server);
|
|||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initDbPool(); // Дождаться пересоздания пула!
|
await initDbPool(); // Дождаться пересоздания пула!
|
||||||
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
|
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
|
||||||
|
|
||||||
|
// Разогрев модели Ollama
|
||||||
|
console.log('🔥 Запуск разогрева модели...');
|
||||||
|
setTimeout(() => {
|
||||||
|
warmupModel().catch(err => {
|
||||||
|
console.error('❌ Ошибка разогрева модели:', err.message);
|
||||||
|
});
|
||||||
|
}, 10000); // Задержка 10 секунд для полной инициализации
|
||||||
|
|
||||||
await initServices(); // Только теперь запускать сервисы
|
await initServices(); // Только теперь запускать сервисы
|
||||||
console.log(`Server is running on port ${PORT}`);
|
console.log(`Server is running on port ${PORT}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
|||||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
const { OpenAIEmbeddings } = require('@langchain/openai');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const aiCache = require('./ai-cache');
|
||||||
|
const aiQueue = require('./ai-queue');
|
||||||
|
|
||||||
// Простой кэш для ответов
|
// Простой кэш для ответов
|
||||||
const responseCache = new Map();
|
const responseCache = new Map();
|
||||||
@@ -105,6 +107,38 @@ class AIAssistant {
|
|||||||
return cyrillicPattern.test(message) ? 'ru' : 'en';
|
return cyrillicPattern.test(message) ? 'ru' : 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Определение приоритета запроса
|
||||||
|
getRequestPriority(message, history, rules) {
|
||||||
|
let priority = 0;
|
||||||
|
|
||||||
|
// Высокий приоритет для коротких запросов
|
||||||
|
if (message.length < 50) {
|
||||||
|
priority += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Приоритет по типу запроса
|
||||||
|
const urgentKeywords = ['срочно', 'urgent', 'важно', 'important', 'помоги', 'help'];
|
||||||
|
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, language = 'auto', history = null, systemPrompt = '', rules = null) {
|
async getResponse(message, language = 'auto', history = null, systemPrompt = '', rules = null) {
|
||||||
try {
|
try {
|
||||||
@@ -120,14 +154,57 @@ class AIAssistant {
|
|||||||
return 'Извините, модель временно недоступна. Пожалуйста, попробуйте позже.';
|
return 'Извините, модель временно недоступна. Пожалуйста, попробуйте позже.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем ключ кэша
|
// Проверяем кэш
|
||||||
const cacheKey = JSON.stringify({ message, language, systemPrompt, rules });
|
const cacheKey = aiCache.generateKey([{ role: 'user', content: message }], {
|
||||||
const cached = responseCache.get(cacheKey);
|
temperature: 0.3,
|
||||||
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
|
maxTokens: 150
|
||||||
|
});
|
||||||
|
const cachedResponse = aiCache.get(cacheKey);
|
||||||
|
if (cachedResponse) {
|
||||||
console.log('Returning cached response');
|
console.log('Returning cached response');
|
||||||
return cached.response;
|
return cachedResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Определяем приоритет запроса
|
||||||
|
const priority = this.getRequestPriority(message, history, rules);
|
||||||
|
|
||||||
|
// Добавляем запрос в очередь
|
||||||
|
const requestId = await aiQueue.addRequest({
|
||||||
|
message,
|
||||||
|
language,
|
||||||
|
history,
|
||||||
|
systemPrompt,
|
||||||
|
rules
|
||||||
|
}, priority);
|
||||||
|
|
||||||
|
// Ждем результат из очереди
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('Request timeout - очередь перегружена'));
|
||||||
|
}, 60000); // 60 секунд таймаут для очереди
|
||||||
|
|
||||||
|
const onCompleted = (item) => {
|
||||||
|
if (item.id === requestId) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
aiQueue.off('completed', onCompleted);
|
||||||
|
aiQueue.off('failed', onFailed);
|
||||||
|
resolve(item.result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFailed = (item) => {
|
||||||
|
if (item.id === requestId) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
aiQueue.off('completed', onCompleted);
|
||||||
|
aiQueue.off('failed', onFailed);
|
||||||
|
reject(new Error(item.error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
aiQueue.on('completed', onCompleted);
|
||||||
|
aiQueue.on('failed', onFailed);
|
||||||
|
});
|
||||||
|
|
||||||
// Определяем язык, если не указан явно
|
// Определяем язык, если не указан явно
|
||||||
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
|
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
|
||||||
console.log('Detected language:', detectedLanguage);
|
console.log('Detected language:', detectedLanguage);
|
||||||
@@ -179,10 +256,7 @@ class AIAssistant {
|
|||||||
|
|
||||||
// Кэшируем ответ
|
// Кэшируем ответ
|
||||||
if (response) {
|
if (response) {
|
||||||
responseCache.set(cacheKey, {
|
aiCache.set(cacheKey, response);
|
||||||
response,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -200,7 +274,7 @@ class AIAssistant {
|
|||||||
|
|
||||||
// Создаем AbortController для таймаута
|
// Создаем AbortController для таймаута
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 60000); // Увеличиваем до 60 секунд
|
const timeoutId = setTimeout(() => controller.abort(), 120000); // Увеличиваем до 120 секунд
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -211,16 +285,19 @@ class AIAssistant {
|
|||||||
stream: false,
|
stream: false,
|
||||||
options: {
|
options: {
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
num_predict: 200, // Уменьшаем максимальную длину ответа
|
num_predict: 150, // Уменьшаем максимальную длину ответа для ускорения
|
||||||
num_ctx: 1024, // Уменьшаем контекст для экономии памяти
|
num_ctx: 512, // Уменьшаем контекст для экономии памяти и ускорения
|
||||||
num_thread: 8, // Увеличиваем количество потоков
|
num_thread: 12, // Увеличиваем количество потоков для ускорения
|
||||||
num_gpu: 1, // Используем GPU если доступен
|
num_gpu: 1, // Используем GPU если доступен
|
||||||
num_gqa: 8, // Оптимизация для qwen2.5
|
num_gqa: 8, // Оптимизация для qwen2.5
|
||||||
rope_freq_base: 1000000, // Оптимизация для qwen2.5
|
rope_freq_base: 1000000, // Оптимизация для qwen2.5
|
||||||
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
|
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
|
||||||
repeat_penalty: 1.1, // Добавляем штраф за повторения
|
repeat_penalty: 1.1, // Добавляем штраф за повторения
|
||||||
top_k: 40, // Ограничиваем выбор токенов
|
top_k: 20, // Уменьшаем выбор токенов для ускорения
|
||||||
top_p: 0.9, // Используем nucleus sampling
|
top_p: 0.8, // Уменьшаем nucleus sampling для ускорения
|
||||||
|
mirostat: 2, // Используем mirostat для стабильности
|
||||||
|
mirostat_tau: 5.0, // Настройка mirostat
|
||||||
|
mirostat_eta: 0.1, // Настройка mirostat
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -240,7 +317,7 @@ class AIAssistant {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in fallbackRequestOpenAI:', error);
|
console.error('Error in fallbackRequestOpenAI:', error);
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
throw new Error('Request timeout - модель не ответила в течение 60 секунд');
|
throw new Error('Request timeout - модель не ответила в течение 120 секунд');
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
77
backend/services/ai-cache.js
Normal file
77
backend/services/ai-cache.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Кэширование AI ответов для ускорения работы
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
class AICache {
|
||||||
|
constructor() {
|
||||||
|
this.cache = new Map();
|
||||||
|
this.maxSize = 1000; // Максимальное количество кэшированных ответов
|
||||||
|
this.ttl = 24 * 60 * 60 * 1000; // 24 часа в миллисекундах
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерация ключа кэша на основе запроса
|
||||||
|
generateKey(messages, options = {}) {
|
||||||
|
const content = JSON.stringify({
|
||||||
|
messages: messages.map(m => ({ role: m.role, content: m.content })),
|
||||||
|
temperature: options.temperature || 0.3,
|
||||||
|
maxTokens: options.num_predict || 150
|
||||||
|
});
|
||||||
|
return crypto.createHash('md5').update(content).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение ответа из кэша
|
||||||
|
get(key) {
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
if (!cached) return null;
|
||||||
|
|
||||||
|
// Проверяем TTL
|
||||||
|
if (Date.now() - cached.timestamp > this.ttl) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[AICache] Cache hit for key: ${key.substring(0, 8)}...`);
|
||||||
|
return cached.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение ответа в кэш
|
||||||
|
set(key, response) {
|
||||||
|
// Очищаем старые записи если кэш переполнен
|
||||||
|
if (this.cache.size >= this.maxSize) {
|
||||||
|
const oldestKey = this.cache.keys().next().value;
|
||||||
|
this.cache.delete(oldestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache.set(key, {
|
||||||
|
response,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[AICache] Cached response for key: ${key.substring(0, 8)}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очистка кэша
|
||||||
|
clear() {
|
||||||
|
this.cache.clear();
|
||||||
|
logger.info('[AICache] Cache cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Статистика кэша
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
maxSize: this.maxSize,
|
||||||
|
hitRate: this.calculateHitRate()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateHitRate() {
|
||||||
|
// Простая реализация - в реальности нужно отслеживать hits/misses
|
||||||
|
return this.cache.size / this.maxSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new AICache();
|
||||||
@@ -1,377 +1,148 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
* Очередь для AI запросов с приоритизацией
|
||||||
* 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 Queue = require('better-queue');
|
const EventEmitter = require('events');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
class AIQueueService {
|
class AIQueue extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.queue = null;
|
super();
|
||||||
this.isInitialized = false;
|
this.queue = [];
|
||||||
this.userRequestTimes = new Map(); // Добавляем Map для отслеживания запросов пользователей
|
this.processing = false;
|
||||||
|
this.maxConcurrent = 2; // Максимум 2 запроса одновременно
|
||||||
|
this.activeRequests = 0;
|
||||||
this.stats = {
|
this.stats = {
|
||||||
totalProcessed: 0,
|
total: 0,
|
||||||
totalFailed: 0,
|
completed: 0,
|
||||||
averageProcessingTime: 0,
|
failed: 0,
|
||||||
currentQueueSize: 0,
|
avgResponseTime: 0
|
||||||
lastProcessedAt: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.initQueue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initQueue() {
|
// Добавление запроса в очередь
|
||||||
try {
|
async addRequest(request, priority = 0) {
|
||||||
this.queue = new Queue(this.processTask.bind(this), {
|
const queueItem = {
|
||||||
// Ограничиваем количество одновременных запросов к Ollama
|
id: Date.now() + Math.random(),
|
||||||
concurrent: 2,
|
request,
|
||||||
|
priority,
|
||||||
// Максимальное время выполнения задачи
|
|
||||||
maxTimeout: 180000, // 3 минуты
|
|
||||||
|
|
||||||
// Задержка между задачами для предотвращения перегрузки
|
|
||||||
afterProcessDelay: 1000, // 1 секунда
|
|
||||||
|
|
||||||
// Максимальное количество повторных попыток
|
|
||||||
maxRetries: 2,
|
|
||||||
|
|
||||||
// Задержка между повторными попытками
|
|
||||||
retryDelay: 5000, // 5 секунд
|
|
||||||
|
|
||||||
// Функция определения приоритета
|
|
||||||
priority: this.getTaskPriority.bind(this),
|
|
||||||
|
|
||||||
// Функция фильтрации задач
|
|
||||||
filter: this.filterTask.bind(this),
|
|
||||||
|
|
||||||
// Функция слияния одинаковых задач
|
|
||||||
merge: this.mergeTasks.bind(this),
|
|
||||||
|
|
||||||
// ID задачи для предотвращения дублирования
|
|
||||||
id: 'requestId'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.isInitialized = true;
|
|
||||||
|
|
||||||
logger.info('[AIQueue] Queue initialized successfully');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[AIQueue] Failed to initialize queue:', error);
|
|
||||||
this.isInitialized = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Определение приоритета задачи
|
|
||||||
getTaskPriority(task, cb) {
|
|
||||||
try {
|
|
||||||
let priority = 1; // Базовый приоритет
|
|
||||||
|
|
||||||
// Высокий приоритет для администраторов
|
|
||||||
if (task.userRole === 'admin') {
|
|
||||||
priority += 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Приоритет по типу запроса
|
|
||||||
switch (task.type) {
|
|
||||||
case 'urgent':
|
|
||||||
priority += 20;
|
|
||||||
break;
|
|
||||||
case 'chat':
|
|
||||||
priority += 5;
|
|
||||||
break;
|
|
||||||
case 'analysis':
|
|
||||||
priority += 3;
|
|
||||||
break;
|
|
||||||
case 'generation':
|
|
||||||
priority += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Приоритет по размеру запроса (короткие запросы имеют больший приоритет)
|
|
||||||
if (task.message && task.message.length < 100) {
|
|
||||||
priority += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Приоритет по времени ожидания
|
|
||||||
const waitTime = Date.now() - task.timestamp;
|
|
||||||
if (waitTime > 30000) { // Более 30 секунд ожидания
|
|
||||||
priority += 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, priority);
|
|
||||||
} catch (error) {
|
|
||||||
cb(error, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Фильтрация задач
|
|
||||||
filterTask(task, cb) {
|
|
||||||
try {
|
|
||||||
// Проверяем обязательные поля
|
|
||||||
if (!task.message || typeof task.message !== 'string') {
|
|
||||||
return cb('Invalid message format');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!task.requestId) {
|
|
||||||
return cb('Missing request ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем размер сообщения
|
|
||||||
if (task.message.length > 10000) {
|
|
||||||
return cb('Message too long (max 10000 characters)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем частоту запросов от пользователя
|
|
||||||
if (this.isUserRateLimited(task.userId)) {
|
|
||||||
return cb('User rate limit exceeded');
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(null, task);
|
|
||||||
} catch (error) {
|
|
||||||
cb(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Слияние одинаковых задач
|
|
||||||
mergeTasks(oldTask, newTask, cb) {
|
|
||||||
try {
|
|
||||||
// Если это тот же запрос от того же пользователя, обновляем метаданные
|
|
||||||
if (oldTask.message === newTask.message && oldTask.userId === newTask.userId) {
|
|
||||||
oldTask.timestamp = newTask.timestamp;
|
|
||||||
oldTask.retryCount = (oldTask.retryCount || 0) + 1;
|
|
||||||
cb(null, oldTask);
|
|
||||||
} else {
|
|
||||||
cb(null, newTask);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
cb(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка задачи
|
|
||||||
async processTask(task, cb) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
const taskId = task.requestId;
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info(`[AIQueue] Processing task ${taskId} for user ${task.userId}`);
|
|
||||||
|
|
||||||
// Импортируем AI сервис
|
|
||||||
const aiAssistant = require('./ai-assistant');
|
|
||||||
const encryptedDb = require('./encryptedDatabaseService');
|
|
||||||
|
|
||||||
// Выполняем AI запрос
|
|
||||||
const result = await aiAssistant.getResponse(
|
|
||||||
task.message,
|
|
||||||
task.language || 'auto',
|
|
||||||
task.history || null,
|
|
||||||
task.systemPrompt || '',
|
|
||||||
task.rules || null
|
|
||||||
);
|
|
||||||
|
|
||||||
const processingTime = Date.now() - startTime;
|
|
||||||
|
|
||||||
// Сохраняем AI ответ в базу данных
|
|
||||||
if (task.conversationId && result) {
|
|
||||||
try {
|
|
||||||
const aiMessage = await encryptedDb.saveData('messages', {
|
|
||||||
conversation_id: task.conversationId,
|
|
||||||
user_id: task.userId,
|
|
||||||
content: result,
|
|
||||||
sender_type: 'assistant',
|
|
||||||
role: 'assistant',
|
|
||||||
channel: 'web'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получаем расшифрованные данные для WebSocket
|
|
||||||
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
|
|
||||||
if (decryptedAiMessage && decryptedAiMessage[0]) {
|
|
||||||
// Отправляем сообщение через WebSocket
|
|
||||||
const { broadcastChatMessage } = require('../wsHub');
|
|
||||||
broadcastChatMessage(decryptedAiMessage[0], task.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[AIQueue] AI response saved for conversation ${task.conversationId}`);
|
|
||||||
} catch (dbError) {
|
|
||||||
logger.error(`[AIQueue] Error saving AI response:`, dbError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем статистику
|
|
||||||
this.updateStats(true, processingTime);
|
|
||||||
|
|
||||||
logger.info(`[AIQueue] Task ${taskId} completed in ${processingTime}ms`);
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
success: true,
|
|
||||||
result,
|
|
||||||
processingTime,
|
|
||||||
taskId
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const processingTime = Date.now() - startTime;
|
|
||||||
|
|
||||||
// Обновляем статистику
|
|
||||||
this.updateStats(false, processingTime);
|
|
||||||
|
|
||||||
logger.error(`[AIQueue] Task ${taskId} failed:`, error);
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
processingTime,
|
|
||||||
taskId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавление задачи в очередь
|
|
||||||
addTask(taskData) {
|
|
||||||
if (!this.isInitialized || !this.queue) {
|
|
||||||
throw new Error('Queue is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = {
|
|
||||||
...taskData,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
requestId: taskData.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
status: 'pending'
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
this.queue.push(queueItem);
|
||||||
const ticket = this.queue.push(task, (error, result) => {
|
this.queue.sort((a, b) => b.priority - a.priority); // Сортировка по приоритету
|
||||||
if (error) {
|
|
||||||
reject(error);
|
this.stats.total++;
|
||||||
} else {
|
logger.info(`[AIQueue] Added request ${queueItem.id} with priority ${priority}`);
|
||||||
resolve(result);
|
|
||||||
|
// Запускаем обработку если не запущена
|
||||||
|
if (!this.processing) {
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return queueItem.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка очереди
|
||||||
|
async processQueue() {
|
||||||
|
if (this.processing || this.activeRequests >= this.maxConcurrent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
|
||||||
|
const item = this.queue.shift();
|
||||||
|
if (!item) continue;
|
||||||
|
|
||||||
|
this.activeRequests++;
|
||||||
|
item.status = 'processing';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const result = await this.processRequest(item.request);
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
item.status = 'completed';
|
||||||
|
item.result = result;
|
||||||
|
item.responseTime = responseTime;
|
||||||
|
|
||||||
|
this.stats.completed++;
|
||||||
|
this.updateAvgResponseTime(responseTime);
|
||||||
|
|
||||||
|
logger.info(`[AIQueue] Request ${item.id} completed in ${responseTime}ms`);
|
||||||
|
|
||||||
|
// Эмитим событие о завершении
|
||||||
|
this.emit('completed', item);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
item.status = 'failed';
|
||||||
|
item.error = error.message;
|
||||||
|
|
||||||
|
this.stats.failed++;
|
||||||
|
logger.error(`[AIQueue] Request ${item.id} failed:`, error.message);
|
||||||
|
|
||||||
|
// Эмитим событие об ошибке
|
||||||
|
this.emit('failed', item);
|
||||||
|
} finally {
|
||||||
|
this.activeRequests--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
// Если в очереди еще есть запросы, продолжаем обработку
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
setTimeout(() => this.processQueue(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка одного запроса
|
||||||
|
async processRequest(request) {
|
||||||
|
// Прямой вызов AI без очереди
|
||||||
|
const aiAssistant = require('./ai-assistant');
|
||||||
|
|
||||||
|
// Используем прямой метод без очереди
|
||||||
|
const messages = [];
|
||||||
|
if (request.systemPrompt) {
|
||||||
|
messages.push({ role: 'system', content: request.systemPrompt });
|
||||||
|
}
|
||||||
|
if (request.history && Array.isArray(request.history)) {
|
||||||
|
for (const msg of request.history) {
|
||||||
|
if (msg.role && msg.content) {
|
||||||
|
messages.push({ role: msg.role, content: msg.content });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Добавляем обработчики событий для билета
|
|
||||||
ticket.on('failed', (error) => {
|
|
||||||
logger.error(`[AIQueue] Task ${task.requestId} failed:`, error);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
ticket.on('finish', (result) => {
|
|
||||||
logger.info(`[AIQueue] Task ${task.requestId} finished`);
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Настройка обработчиков событий очереди
|
|
||||||
setupEventListeners() {
|
|
||||||
this.queue.on('task_queued', (taskId) => {
|
|
||||||
logger.info(`[AIQueue] Task ${taskId} queued`);
|
|
||||||
this.stats.currentQueueSize = this.queue.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue.on('task_started', (taskId) => {
|
|
||||||
logger.info(`[AIQueue] Task ${taskId} started`);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue.on('task_finish', (taskId, result) => {
|
|
||||||
logger.info(`[AIQueue] Task ${taskId} finished successfully`);
|
|
||||||
this.stats.lastProcessedAt = new Date();
|
|
||||||
this.stats.currentQueueSize = this.queue.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue.on('task_failed', (taskId, error) => {
|
|
||||||
logger.error(`[AIQueue] Task ${taskId} failed:`, error);
|
|
||||||
this.stats.currentQueueSize = this.queue.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue.on('empty', () => {
|
|
||||||
logger.info('[AIQueue] Queue is empty');
|
|
||||||
this.stats.currentQueueSize = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue.on('drain', () => {
|
|
||||||
logger.info('[AIQueue] Queue drained');
|
|
||||||
this.stats.currentQueueSize = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновление статистики
|
|
||||||
updateStats(success, processingTime) {
|
|
||||||
this.stats.totalProcessed++;
|
|
||||||
if (!success) {
|
|
||||||
this.stats.totalFailed++;
|
|
||||||
}
|
}
|
||||||
|
messages.push({ role: 'user', content: request.message });
|
||||||
// Обновляем среднее время обработки
|
|
||||||
const totalTime = this.stats.averageProcessingTime * (this.stats.totalProcessed - 1) + processingTime;
|
// Прямой вызов API без очереди
|
||||||
this.stats.averageProcessingTime = totalTime / this.stats.totalProcessed;
|
return await aiAssistant.fallbackRequestOpenAI(messages, request.language, request.systemPrompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка ограничения частоты запросов пользователя
|
// Обновление средней скорости ответа
|
||||||
isUserRateLimited(userId) {
|
updateAvgResponseTime(responseTime) {
|
||||||
// Простая реализация - можно улучшить с использованием Redis
|
const total = this.stats.completed;
|
||||||
const now = Date.now();
|
this.stats.avgResponseTime =
|
||||||
const userRequests = this.userRequestTimes.get(userId) || [];
|
(this.stats.avgResponseTime * (total - 1) + responseTime) / total;
|
||||||
|
|
||||||
// Удаляем старые запросы (старше 1 минуты)
|
|
||||||
const recentRequests = userRequests.filter(time => now - time < 60000);
|
|
||||||
|
|
||||||
// Ограничиваем до 10 запросов в минуту
|
|
||||||
if (recentRequests.length >= 10) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем текущий запрос
|
|
||||||
recentRequests.push(now);
|
|
||||||
this.userRequestTimes.set(userId, recentRequests);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение статистики очереди
|
// Получение статистики
|
||||||
getStats() {
|
getStats() {
|
||||||
const queueStats = this.queue ? this.queue.getStats() : {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...this.stats,
|
...this.stats,
|
||||||
queueStats,
|
queueLength: this.queue.length,
|
||||||
isInitialized: this.isInitialized,
|
activeRequests: this.activeRequests,
|
||||||
currentQueueSize: this.queue ? this.queue.length : 0,
|
processing: this.processing
|
||||||
runningTasks: this.queue ? this.queue.running : 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очистка очереди
|
// Очистка очереди
|
||||||
clear() {
|
clear() {
|
||||||
if (this.queue) {
|
this.queue = [];
|
||||||
this.queue.destroy();
|
logger.info('[AIQueue] Queue cleared');
|
||||||
this.initQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пауза/возобновление очереди
|
|
||||||
pause() {
|
|
||||||
if (this.queue) {
|
|
||||||
this.queue.pause();
|
|
||||||
logger.info('[AIQueue] Queue paused');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resume() {
|
|
||||||
if (this.queue) {
|
|
||||||
this.queue.resume();
|
|
||||||
logger.info('[AIQueue] Queue resumed');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем и экспортируем единственный экземпляр
|
module.exports = new AIQueue();
|
||||||
const aiQueueService = new AIQueueService();
|
|
||||||
module.exports = aiQueueService;
|
|
||||||
@@ -349,8 +349,8 @@ class EncryptedDataService {
|
|||||||
params.push(...Object.values(conditions));
|
params.push(...Object.values(conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows } = await db.getQuery()(query, params);
|
const result = await db.getQuery()(query, params);
|
||||||
return rows;
|
return result.rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
|
console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ async function getBot() {
|
|||||||
const aiResponse = await Promise.race([
|
const aiResponse = await Promise.race([
|
||||||
aiResponsePromise,
|
aiResponsePromise,
|
||||||
new Promise((_, reject) =>
|
new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('AI response timeout')), 60000)
|
setTimeout(() => reject(new Error('AI response timeout')), 120000)
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -494,7 +494,7 @@ async function getBot() {
|
|||||||
// Запускаем бота с таймаутом
|
// Запускаем бота с таймаутом
|
||||||
const launchPromise = botInstance.launch();
|
const launchPromise = botInstance.launch();
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 10000); // 10 секунд таймаут
|
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 30000); // 30 секунд таймаут
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.race([launchPromise, timeoutPromise]);
|
await Promise.race([launchPromise, timeoutPromise]);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ function initWSS(server) {
|
|||||||
|
|
||||||
wss.on('connection', (ws, req) => {
|
wss.on('connection', (ws, req) => {
|
||||||
console.log('🔌 [WebSocket] Новое подключение');
|
console.log('🔌 [WebSocket] Новое подключение');
|
||||||
|
console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress);
|
||||||
|
console.log('🔌 [WebSocket] User-Agent:', req.headers['user-agent']);
|
||||||
|
console.log('🔌 [WebSocket] Origin:', req.headers.origin);
|
||||||
|
|
||||||
// Добавляем клиента в общий список
|
// Добавляем клиента в общий список
|
||||||
if (!wsClients.has('anonymous')) {
|
if (!wsClients.has('anonymous')) {
|
||||||
@@ -42,13 +45,21 @@ function initWSS(server) {
|
|||||||
// Аутентификация пользователя
|
// Аутентификация пользователя
|
||||||
authenticateUser(ws, data.userId);
|
authenticateUser(ws, data.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.type === 'ping') {
|
||||||
|
// Отправляем pong ответ
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'pong',
|
||||||
|
timestamp: data.timestamp
|
||||||
|
}));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
|
console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', (code, reason) => {
|
||||||
console.log('🔌 [WebSocket] Соединение закрыто');
|
console.log('🔌 [WebSocket] Соединение закрыто', { code, reason: reason.toString() });
|
||||||
// Удаляем клиента из всех списков
|
// Удаляем клиента из всех списков
|
||||||
for (const [userId, clients] of wsClients.entries()) {
|
for (const [userId, clients] of wsClients.entries()) {
|
||||||
clients.delete(ws);
|
clients.delete(ws);
|
||||||
@@ -59,7 +70,7 @@ function initWSS(server) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on('error', (error) => {
|
ws.on('error', (error) => {
|
||||||
console.error('❌ [WebSocket] Ошибка соединения:', error);
|
console.error('❌ [WebSocket] Ошибка соединения:', error.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,46 +235,19 @@ function broadcastTableRelationsUpdate(tableId, rowId, targetUserId = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function broadcastTagsUpdate(targetUserId = null) {
|
function broadcastTagsUpdate(targetUserId = null) {
|
||||||
const now = Date.now();
|
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов');
|
||||||
const cacheKey = targetUserId || 'global';
|
const message = JSON.stringify({
|
||||||
|
|
||||||
// Проверяем, не отправляли ли мы недавно уведомление
|
|
||||||
const lastUpdate = tagsChangeCache.get(cacheKey);
|
|
||||||
if (lastUpdate && (now - lastUpdate) < TAGS_CACHE_TTL) {
|
|
||||||
console.log(`🏷️ [WebSocket] Пропускаем отправку уведомления о тегах (слишком часто)`, { targetUserId });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем кэш
|
|
||||||
tagsChangeCache.set(cacheKey, now);
|
|
||||||
|
|
||||||
console.log(`🏷️ [WebSocket] Отправка обновления тегов`, { targetUserId });
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
type: 'tags-updated',
|
type: 'tags-updated',
|
||||||
timestamp: now
|
timestamp: Date.now()
|
||||||
};
|
});
|
||||||
|
|
||||||
if (targetUserId) {
|
// Отправляем всем подключенным клиентам
|
||||||
// Отправляем конкретному пользователю
|
wss.clients.forEach((client) => {
|
||||||
const userClients = wsClients.get(targetUserId.toString());
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
if (userClients) {
|
console.log('🔔 [WebSocket] Отправляем tags-updated клиенту');
|
||||||
for (const ws of userClients) {
|
client.send(message);
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
// Отправляем всем
|
|
||||||
for (const [userId, clients] of wsClients.entries()) {
|
|
||||||
for (const ws of clients) {
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectedUsers() {
|
function getConnectedUsers() {
|
||||||
@@ -298,6 +282,21 @@ function getStats() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для отправки уведомлений о статусе AI
|
||||||
|
function broadcastAIStatus(status) {
|
||||||
|
console.log('📢 [WebSocket] Отправка статуса AI всем клиентам');
|
||||||
|
for (const [userId, clients] of wsClients.entries()) {
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'ai-status',
|
||||||
|
status
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initWSS,
|
initWSS,
|
||||||
broadcastContactsUpdate,
|
broadcastContactsUpdate,
|
||||||
@@ -307,6 +306,7 @@ module.exports = {
|
|||||||
broadcastTableUpdate,
|
broadcastTableUpdate,
|
||||||
broadcastTableRelationsUpdate,
|
broadcastTableRelationsUpdate,
|
||||||
broadcastTagsUpdate,
|
broadcastTagsUpdate,
|
||||||
|
broadcastAIStatus,
|
||||||
getConnectedUsers,
|
getConnectedUsers,
|
||||||
getStats
|
getStats
|
||||||
};
|
};
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
|
||||||
# All rights reserved.
|
|
||||||
# This software is proprietary and confidential.
|
|
||||||
# For licensing inquiries: info@hb3-accelerator.com
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
external: true
|
|
||||||
name: dapp-for-business_postgres_data
|
|
||||||
ollama_data:
|
|
||||||
external: true
|
|
||||||
name: dapp-for-business_ollama_data
|
|
||||||
vector_search_data:
|
|
||||||
external: true
|
|
||||||
name: dapp-for-business_vector_search_data
|
|
||||||
frontend_node_modules:
|
|
||||||
external: true
|
|
||||||
name: dapp-for-business_frontend_node_modules
|
|
||||||
@@ -42,7 +42,6 @@ services:
|
|||||||
- ollama_data:/root/.ollama
|
- ollama_data:/root/.ollama
|
||||||
# ports:
|
# ports:
|
||||||
# - '11434:11434' # Закрываем - используется только backend'ом
|
# - '11434:11434' # Закрываем - используется только backend'ом
|
||||||
command: serve
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -58,8 +57,8 @@ services:
|
|||||||
test: ["CMD", "ollama", "list"]
|
test: ["CMD", "ollama", "list"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 5
|
||||||
start_period: 60s
|
start_period: 120s
|
||||||
vector-search:
|
vector-search:
|
||||||
build:
|
build:
|
||||||
context: ./vector-search
|
context: ./vector-search
|
||||||
@@ -143,29 +142,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '5173:5173' # Закрываем - используем nginx
|
- '5173:5173' # Закрываем - используем nginx
|
||||||
command: yarn run dev -- --host 0.0.0.0
|
command: yarn run dev -- --host 0.0.0.0
|
||||||
ollama-setup:
|
|
||||||
image: curlimages/curl:latest
|
|
||||||
container_name: dapp-ollama-setup
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "5m"
|
|
||||||
max-file: "2"
|
|
||||||
depends_on:
|
|
||||||
- ollama
|
|
||||||
restart: on-failure
|
|
||||||
command: |
|
|
||||||
sh -c "
|
|
||||||
echo 'Waiting for Ollama to be ready...'
|
|
||||||
until curl -s http://ollama:11434/api/tags >/dev/null; do
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo 'Ollama is ready, pulling qwen2.5-7b model...'
|
|
||||||
curl -X POST http://ollama:11434/api/pull -d '{\"name\":\"${OLLAMA_MODEL:-qwen2.5:7b}\"}' -H 'Content-Type: application/json'
|
|
||||||
echo 'Pulling embeddings model...'
|
|
||||||
curl -X POST http://ollama:11434/api/pull -d '{\"name\":\"${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest}\"}' -H 'Content-Type: application/json'
|
|
||||||
echo 'Done!'
|
|
||||||
"
|
|
||||||
ssh-tunnel-frontend:
|
ssh-tunnel-frontend:
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
container_name: ssh-tunnel-frontend
|
container_name: ssh-tunnel-frontend
|
||||||
|
|||||||
@@ -15,20 +15,61 @@ import { ref, onMounted, onUnmounted } from 'vue';
|
|||||||
export function useTablesWebSocket() {
|
export function useTablesWebSocket() {
|
||||||
const ws = ref(null);
|
const ws = ref(null);
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
|
const isConnecting = ref(false); // Добавляем флаг для предотвращения множественных подключений
|
||||||
const tableUpdateCallbacks = ref(new Map()); // tableId -> callback
|
const tableUpdateCallbacks = ref(new Map()); // tableId -> callback
|
||||||
const tableRelationsUpdateCallbacks = ref(new Map()); // `${tableId}-${rowId}` -> callback
|
const tableRelationsUpdateCallbacks = ref(new Map()); // `${tableId}-${rowId}` -> callback
|
||||||
|
const pingInterval = ref(null); // Интервал для ping сообщений
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||||
|
console.log('[TablesWebSocket] Уже подключены, пропускаем');
|
||||||
return; // Уже подключены
|
return; // Уже подключены
|
||||||
}
|
}
|
||||||
|
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
if (isConnecting.value) {
|
||||||
ws.value = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
console.log('[TablesWebSocket] Уже пытаемся подключиться, пропускаем');
|
||||||
|
return; // Уже пытаемся подключиться
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnecting.value = true;
|
||||||
|
|
||||||
|
// Определяем правильный URL для WebSocket
|
||||||
|
let wsUrl;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// В режиме разработки используем прокси через Vite
|
||||||
|
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
|
||||||
|
} else {
|
||||||
|
// В продакшене используем тот же хост
|
||||||
|
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[TablesWebSocket] Подключение к:', wsUrl);
|
||||||
|
console.log('[TablesWebSocket] Текущий хост:', window.location.host);
|
||||||
|
console.log('[TablesWebSocket] Протокол:', window.location.protocol);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws.value = new WebSocket(wsUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TablesWebSocket] Ошибка создания WebSocket:', error);
|
||||||
|
isConnecting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ws.value.onopen = () => {
|
ws.value.onopen = () => {
|
||||||
console.log('[TablesWebSocket] Соединение установлено');
|
console.log('[TablesWebSocket] Соединение установлено');
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
|
isConnecting.value = false;
|
||||||
|
|
||||||
|
// Запускаем ping каждые 30 секунд
|
||||||
|
pingInterval.value = setInterval(() => {
|
||||||
|
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
ws.value.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TablesWebSocket] Ошибка отправки ping:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.value.onmessage = (event) => {
|
ws.value.onmessage = (event) => {
|
||||||
@@ -36,6 +77,12 @@ export function useTablesWebSocket() {
|
|||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log('[TablesWebSocket] Получено сообщение:', data);
|
console.log('[TablesWebSocket] Получено сообщение:', data);
|
||||||
|
|
||||||
|
// Обрабатываем pong ответ
|
||||||
|
if (data.type === 'pong') {
|
||||||
|
console.log('[TablesWebSocket] Получен pong ответ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.type === 'table-updated') {
|
if (data.type === 'table-updated') {
|
||||||
const callbacks = tableUpdateCallbacks.value.get(data.tableId);
|
const callbacks = tableUpdateCallbacks.value.get(data.tableId);
|
||||||
if (callbacks) {
|
if (callbacks) {
|
||||||
@@ -55,20 +102,37 @@ export function useTablesWebSocket() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.value.onclose = () => {
|
ws.value.onclose = (event) => {
|
||||||
console.log('[TablesWebSocket] Соединение закрыто');
|
console.log('[TablesWebSocket] Соединение закрыто', {
|
||||||
|
code: event.code,
|
||||||
|
reason: event.reason,
|
||||||
|
wasClean: event.wasClean
|
||||||
|
});
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
// Переподключение через 3 секунды
|
isConnecting.value = false;
|
||||||
setTimeout(() => {
|
|
||||||
if (!isConnected.value) {
|
// Останавливаем ping интервал
|
||||||
connect();
|
if (pingInterval.value) {
|
||||||
}
|
clearInterval(pingInterval.value);
|
||||||
}, 3000);
|
pingInterval.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переподключение только если это не было намеренное закрытие
|
||||||
|
if (event.code !== 1000) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isConnected.value && !isConnecting.value) {
|
||||||
|
console.log('[TablesWebSocket] Попытка переподключения...');
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.value.onerror = (error) => {
|
ws.value.onerror = (error) => {
|
||||||
console.error('[TablesWebSocket] Ошибка соединения:', error);
|
console.error('[TablesWebSocket] Ошибка соединения:', error);
|
||||||
|
console.error('[TablesWebSocket] WebSocket readyState:', ws.value?.readyState);
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
|
isConnecting.value = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +181,20 @@ export function useTablesWebSocket() {
|
|||||||
|
|
||||||
function disconnect() {
|
function disconnect() {
|
||||||
if (ws.value) {
|
if (ws.value) {
|
||||||
ws.value.close();
|
// Останавливаем ping интервал
|
||||||
|
if (pingInterval.value) {
|
||||||
|
clearInterval(pingInterval.value);
|
||||||
|
pingInterval.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Корректно закрываем соединение
|
||||||
|
if (ws.value.readyState === WebSocket.OPEN) {
|
||||||
|
ws.value.close(1000, 'Manual disconnect');
|
||||||
|
}
|
||||||
ws.value = null;
|
ws.value = null;
|
||||||
}
|
}
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
|
isConnecting.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -33,24 +33,16 @@ export function useTagsWebSocket() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTagsUpdate(data) {
|
function handleTagsUpdate(data) {
|
||||||
console.log('🏷️ [useTagsWebSocket] Получено обновление тегов:', data);
|
console.log('🏷️ [useTagsWebSocket] Получено уведомление об обновлении тегов:', data);
|
||||||
|
|
||||||
// Очищаем предыдущий таймер
|
// Вызываем все зарегистрированные колбэки
|
||||||
if (debounceTimer) {
|
tagsUpdateCallbacks.value.forEach(callback => {
|
||||||
clearTimeout(debounceTimer);
|
try {
|
||||||
}
|
callback(data);
|
||||||
|
} catch (error) {
|
||||||
// Устанавливаем новый таймер для дебаунсинга
|
console.error('🏷️ [useTagsWebSocket] Ошибка в колбэке:', error);
|
||||||
debounceTimer = setTimeout(() => {
|
}
|
||||||
console.log('🏷️ [useTagsWebSocket] Выполняем обновление тегов после дебаунсинга');
|
});
|
||||||
tagsUpdateCallbacks.value.forEach(callback => {
|
|
||||||
try {
|
|
||||||
callback(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка в callback обновления тегов:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, DEBOUNCE_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ export default {
|
|||||||
return res.data;
|
return res.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка при удалении контакта:', err.response?.status, err.response?.data, err);
|
console.error('Ошибка при удалении контакта:', err.response?.status, err.response?.data, err);
|
||||||
|
|
||||||
|
// Если пользователь уже удален (404), считаем это успехом
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
return { success: true, deleted: 0, message: 'Пользователь уже удален' };
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сервис для работы с DLE v2 (Digital Legal Entity)
|
* Сервис для работы с DLE v2 (Digital Legal Entity)
|
||||||
@@ -24,7 +24,7 @@ class DLEV2Service {
|
|||||||
*/
|
*/
|
||||||
async createDLE(dleParams) {
|
async createDLE(dleParams) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/dle-v2', dleParams);
|
const response = await api.post('/dle-v2', dleParams);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при создании DLE v2:', error);
|
console.error('Ошибка при создании DLE v2:', error);
|
||||||
@@ -38,7 +38,7 @@ class DLEV2Service {
|
|||||||
*/
|
*/
|
||||||
async getAllDLEs() {
|
async getAllDLEs() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/dle-v2');
|
const response = await api.get('/dle-v2');
|
||||||
return response.data.data || [];
|
return response.data.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении списка DLE v2:', error);
|
console.error('Ошибка при получении списка DLE v2:', error);
|
||||||
@@ -52,7 +52,7 @@ class DLEV2Service {
|
|||||||
*/
|
*/
|
||||||
async getDefaults() {
|
async getDefaults() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/dle-v2/defaults');
|
const response = await api.get('/dle-v2/defaults');
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении настроек по умолчанию DLE v2:', error);
|
console.error('Ошибка при получении настроек по умолчанию DLE v2:', error);
|
||||||
@@ -73,7 +73,7 @@ class DLEV2Service {
|
|||||||
*/
|
*/
|
||||||
async deleteDLE(dleAddress) {
|
async deleteDLE(dleAddress) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete(`/api/dle-v2/${dleAddress}`);
|
const response = await api.delete(`/dle-v2/${dleAddress}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при удалении DLE v2:', error);
|
console.error('Ошибка при удалении DLE v2:', error);
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async getMessagesByUserId(userId) {
|
async getMessagesByUserId(userId) {
|
||||||
if (!userId) return [];
|
if (!userId) return [];
|
||||||
const { data } = await axios.get(`/messages?userId=${userId}`);
|
const { data } = await api.get(`/messages?userId=${userId}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async sendMessage({ conversationId, message, attachments = [], toUserId }) {
|
async sendMessage({ conversationId, message, attachments = [], toUserId }) {
|
||||||
@@ -26,7 +26,7 @@ export default {
|
|||||||
attachments.forEach(file => {
|
attachments.forEach(file => {
|
||||||
formData.append('attachments', file);
|
formData.append('attachments', file);
|
||||||
});
|
});
|
||||||
const { data } = await axios.post('/chat/message', formData, {
|
const { data } = await api.post('/chat/message', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
});
|
});
|
||||||
@@ -34,20 +34,20 @@ export default {
|
|||||||
},
|
},
|
||||||
async getMessagesByConversationId(conversationId) {
|
async getMessagesByConversationId(conversationId) {
|
||||||
if (!conversationId) return [];
|
if (!conversationId) return [];
|
||||||
const { data } = await axios.get(`/messages?conversationId=${conversationId}`);
|
const { data } = await api.get(`/messages?conversationId=${conversationId}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async getConversationByUserId(userId) {
|
async getConversationByUserId(userId) {
|
||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
const { data } = await axios.get(`/messages/conversations?userId=${userId}`);
|
const { data } = await api.get(`/messages/conversations?userId=${userId}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async generateAiDraft(conversationId, messages, language = 'auto') {
|
async generateAiDraft(conversationId, messages, language = 'auto') {
|
||||||
const { data } = await axios.post('/chat/ai-draft', { conversationId, messages, language });
|
const { data } = await api.post('/chat/ai-draft', { conversationId, messages, language });
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async broadcastMessage({ userId, message }) {
|
async broadcastMessage({ userId, message }) {
|
||||||
const { data } = await axios.post('/messages/broadcast', {
|
const { data } = await api.post('/messages/broadcast', {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
content: message
|
content: message
|
||||||
}, {
|
}, {
|
||||||
@@ -56,7 +56,7 @@ export default {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async deleteMessagesHistory(userId) {
|
async deleteMessagesHistory(userId) {
|
||||||
const { data } = await axios.delete(`/messages/history/${userId}`, {
|
const { data } = await api.delete(`/messages/history/${userId}`, {
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -64,6 +64,6 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getAllMessages() {
|
export async function getAllMessages() {
|
||||||
const { data } = await axios.get('/messages');
|
const { data } = await api.get('/messages');
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ class WebSocketService {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tags-updated':
|
case 'tags-updated':
|
||||||
console.log('🏷️ [WebSocket] Обновление тегов клиентов');
|
console.log('🔔 [websocketService] Получено сообщение tags-updated');
|
||||||
this.emit('tags-updated');
|
this.emit('tags-updated');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,13 @@ async function loadContact() {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
contact.value = await contactsService.getContactById(route.params.id);
|
contact.value = await contactsService.getContactById(route.params.id);
|
||||||
|
if (!contact.value) {
|
||||||
|
error.value = 'Контакт не найден';
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('Ошибка загрузки контакта:', e);
|
||||||
contact.value = null;
|
contact.value = null;
|
||||||
|
error.value = 'Контакт не найден';
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -66,9 +71,17 @@ async function deleteContact() {
|
|||||||
isDeleting.value = true;
|
isDeleting.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
await contactsService.deleteContact(contact.value.id);
|
const result = await contactsService.deleteContact(contact.value.id);
|
||||||
router.push({ name: 'crm' });
|
console.log('Результат удаления:', result);
|
||||||
|
|
||||||
|
// Если удаление успешно или пользователь уже удален
|
||||||
|
if (result.success || result.message === 'Пользователь уже удален') {
|
||||||
|
router.push({ name: 'contacts-list' });
|
||||||
|
} else {
|
||||||
|
error.value = 'Ошибка при удалении контакта';
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('Ошибка при удалении:', e);
|
||||||
error.value = 'Ошибка при удалении контакта';
|
error.value = 'Ошибка при удалении контакта';
|
||||||
} finally {
|
} finally {
|
||||||
isDeleting.value = false;
|
isDeleting.value = false;
|
||||||
@@ -76,7 +89,7 @@ async function deleteContact() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cancelDelete() {
|
function cancelDelete() {
|
||||||
router.push({ name: 'contact-details', params: { id: route.params.id } });
|
router.push({ name: 'contacts-list' });
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadContact);
|
onMounted(loadContact);
|
||||||
|
|||||||
Reference in New Issue
Block a user