feat: новая функция
This commit is contained in:
@@ -981,4 +981,114 @@ router.get('/access-level/:address', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Подключение кошелька через токен связывания
|
||||
* Используется для привязки Telegram/Email идентификаторов к кошельку
|
||||
*/
|
||||
router.post('/wallet-with-link', authLimiter, async (req, res) => {
|
||||
try {
|
||||
const { address, signature, message, token } = req.body;
|
||||
|
||||
if (!address || !signature || !message || !token) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Требуются: address, signature, message, token'
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Проверяем подпись
|
||||
let recoveredAddress;
|
||||
try {
|
||||
recoveredAddress = ethers.verifyMessage(message, signature);
|
||||
} catch (err) {
|
||||
logger.error('[Auth] Ошибка верификации подписи:', err);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Неверная подпись'
|
||||
});
|
||||
}
|
||||
|
||||
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Подпись не соответствует адресу'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Проверяем и используем токен
|
||||
const identityLinkService = require('../services/IdentityLinkService');
|
||||
const linkResult = await identityLinkService.useLinkToken(token, address);
|
||||
|
||||
if (!linkResult.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: linkResult.error
|
||||
});
|
||||
}
|
||||
|
||||
const { userId, identifier, role } = linkResult;
|
||||
|
||||
// 3. Мигрируем историю гостя
|
||||
const universalGuestService = require('../services/UniversalGuestService');
|
||||
const migrationResult = await universalGuestService.migrateToUser(identifier, userId);
|
||||
|
||||
logger.info('[Auth] История мигрирована:', migrationResult);
|
||||
|
||||
// 4. Обновляем сессию
|
||||
req.session.userId = userId;
|
||||
req.session.address = address.toLowerCase();
|
||||
req.session.authenticated = true;
|
||||
req.session.authType = 'wallet';
|
||||
req.session.isAdmin = (role === 'admin' || role === 'editor' || role === 'readonly');
|
||||
|
||||
await sessionService.saveSession(req.session, 'wallet-with-link');
|
||||
|
||||
logger.info(`[Auth] Кошелек подключен через токен: ${address} → user ${userId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
userId,
|
||||
role,
|
||||
conversationId: migrationResult.conversationId,
|
||||
migratedMessages: migrationResult.migrated,
|
||||
message: 'Кошелек успешно подключен, история перенесена'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Auth] Ошибка в wallet-with-link:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Внутренняя ошибка сервера'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✨ НОВОЕ: Получение прав доступа пользователя
|
||||
router.get('/permissions', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.session.userId;
|
||||
|
||||
// Получаем роль пользователя из БД
|
||||
const users = await encryptedDb.getData('users', { id: userId }, 1);
|
||||
const userRole = users && users.length > 0 ? users[0].role : 'user';
|
||||
|
||||
// Получаем настройки прав через adminLogicService
|
||||
const adminLogicService = require('../services/adminLogicService');
|
||||
const permissions = adminLogicService.getAdminSettings({ role: userRole });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
userId: userId,
|
||||
permissions: permissions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Auth] Ошибка получения permissions:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка получения прав доступа'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -15,21 +15,44 @@ const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const aiAssistant = require('../services/ai-assistant');
|
||||
const db = require('../db');
|
||||
const encryptedDb = require('../services/encryptedDatabaseService');
|
||||
const logger = require('../utils/logger');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
||||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||
const botManager = require('../services/botManager');
|
||||
const universalMediaProcessor = require('../services/UniversalMediaProcessor');
|
||||
|
||||
// Настройка multer для обработки файлов в памяти
|
||||
const storage = multer.memoryStorage();
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
// Функция processGuestMessages перенесена в services/guestMessageService.js
|
||||
// Функция processGuestMessages заменена на UniversalGuestService.migrateToUser()
|
||||
|
||||
// Обработчик для гостевых сообщений
|
||||
// Обработчик для гостевых сообщений (НОВАЯ ВЕРСИЯ)
|
||||
router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||||
try {
|
||||
// Frontend отправляет FormData, поэтому читаем из req.body
|
||||
const content = req.body.content || req.body.message;
|
||||
const guestId = req.body.guestId;
|
||||
const files = req.files || [];
|
||||
|
||||
logger.info('[Chat] Получен guest-message запрос:', {
|
||||
content: content?.substring(0, 50),
|
||||
guestId,
|
||||
hasFiles: files.length > 0,
|
||||
bodyKeys: Object.keys(req.body)
|
||||
});
|
||||
|
||||
// Проверяем, что есть либо текст, либо файлы
|
||||
if (!content && (!files || files.length === 0)) {
|
||||
logger.warn('[Chat] Гостевое сообщение без content и файлов:', req.body);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Текст сообщения или файлы обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем готовность системы
|
||||
if (!botManager.isReady()) {
|
||||
return res.status(503).json({
|
||||
@@ -38,18 +61,100 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем WebBot
|
||||
const webBot = botManager.getBot('web');
|
||||
if (!webBot || !webBot.isInitialized) {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
error: 'Web Bot не инициализирован'
|
||||
});
|
||||
const universalGuestService = require('../services/UniversalGuestService');
|
||||
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
|
||||
|
||||
// Создаем или используем существующий гостевой ID
|
||||
const webGuestId = guestId || universalGuestService.generateWebGuestId();
|
||||
const identifier = universalGuestService.createIdentifier('web', webGuestId);
|
||||
|
||||
// Обработка вложений через медиа-процессор
|
||||
let contentData = null;
|
||||
if (files && files.length > 0) {
|
||||
const mediaFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const processedFile = await universalMediaProcessor.processFile(
|
||||
file.buffer,
|
||||
file.originalname,
|
||||
{
|
||||
webUpload: true,
|
||||
originalSize: file.size,
|
||||
mimeType: file.mimetype
|
||||
}
|
||||
);
|
||||
|
||||
mediaFiles.push(processedFile);
|
||||
} catch (fileError) {
|
||||
logger.error('[Chat] Ошибка обработки файла:', fileError);
|
||||
// Fallback: сохраняем как есть
|
||||
mediaFiles.push({
|
||||
type: 'document',
|
||||
content: `[Файл: ${file.originalname}]`,
|
||||
processed: false,
|
||||
error: fileError.message,
|
||||
file: {
|
||||
filename: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
data: file.buffer
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем contentData только если есть обработанные файлы
|
||||
if (mediaFiles.length > 0) {
|
||||
contentData = {
|
||||
text: content,
|
||||
files: mediaFiles.map(file => ({
|
||||
data: file.file?.data || file.file?.buffer,
|
||||
filename: file.file?.originalName || file.file?.filename,
|
||||
metadata: {
|
||||
type: file.type,
|
||||
processed: file.processed,
|
||||
webUpload: true,
|
||||
mimeType: file.file?.mimetype,
|
||||
originalSize: file.file?.size,
|
||||
size: file.file?.size
|
||||
}
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем сообщение через новую архитектуру
|
||||
await webBot.handleMessage(req, res, async (messageData) => {
|
||||
return await botManager.processMessage(messageData);
|
||||
// Обратная совместимость - старый формат attachments
|
||||
const attachments = (files || []).map(file => ({
|
||||
filename: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
data: file.buffer
|
||||
}));
|
||||
|
||||
const messageData = {
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
channel: 'web',
|
||||
attachments: attachments,
|
||||
contentData: contentData
|
||||
};
|
||||
|
||||
// Обработка через unified processor
|
||||
const result = await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
logger.info('[Chat] Результат обработки:', {
|
||||
success: result.success,
|
||||
hasAiResponse: !!result.aiResponse,
|
||||
aiResponseType: typeof result.aiResponse?.response
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
guestId: webGuestId,
|
||||
aiResponse: result.aiResponse ? {
|
||||
response: result.aiResponse.response
|
||||
} : null
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -63,9 +168,27 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||||
|
||||
// Старая логика удалена - используется guestService.js);
|
||||
|
||||
// Обработчик для сообщений аутентифицированных пользователей
|
||||
// Обработчик для сообщений аутентифицированных пользователей (НОВАЯ ВЕРСИЯ)
|
||||
router.post('/message', requireAuth, upload.array('attachments'), async (req, res) => {
|
||||
try {
|
||||
const { content, conversationId, recipientId } = req.body;
|
||||
const userId = req.session.userId;
|
||||
const files = req.files || [];
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Текст сообщения обязателен'
|
||||
});
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Пользователь не авторизован'
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем готовность системы
|
||||
if (!botManager.isReady()) {
|
||||
return res.status(503).json({
|
||||
@@ -74,18 +197,83 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем WebBot
|
||||
const webBot = botManager.getBot('web');
|
||||
if (!webBot || !webBot.isInitialized) {
|
||||
return res.status(503).json({
|
||||
const encryptedDb = require('../services/encryptedDatabaseService');
|
||||
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
|
||||
const identityService = require('../services/identity-service');
|
||||
|
||||
// Получаем информацию о пользователе
|
||||
const users = await encryptedDb.getData('users', { id: userId }, 1);
|
||||
|
||||
// ✨ НОВОЕ: Валидация прав через adminLogicService
|
||||
const adminLogicService = require('../services/adminLogicService');
|
||||
const sessionUserId = req.session.userId;
|
||||
const targetUserId = userId;
|
||||
const isAdmin = req.session.isAdmin || false;
|
||||
|
||||
const canWrite = adminLogicService.canWriteToConversation({
|
||||
isAdmin: isAdmin,
|
||||
userId: sessionUserId,
|
||||
conversationUserId: targetUserId
|
||||
});
|
||||
|
||||
if (!canWrite) {
|
||||
logger.warn(`[Chat] Пользователь ${sessionUserId} пытался писать в беседу ${targetUserId} без прав`);
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Нет прав для отправки сообщений в эту беседу'
|
||||
});
|
||||
}
|
||||
if (!users || users.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Web Bot не инициализирован'
|
||||
error: 'Пользователь не найден'
|
||||
});
|
||||
}
|
||||
|
||||
// Обрабатываем сообщение через новую архитектуру
|
||||
await webBot.handleMessage(req, res, async (messageData) => {
|
||||
return await botManager.processMessage(messageData);
|
||||
const user = users[0];
|
||||
|
||||
// Находим wallet идентификатор пользователя
|
||||
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
|
||||
|
||||
if (!walletIdentity) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Требуется подключение кошелька'
|
||||
});
|
||||
}
|
||||
|
||||
// Создаем identifier для пользователя
|
||||
const identifier = `wallet:${walletIdentity.provider_id}`;
|
||||
|
||||
// Обработка вложений
|
||||
const attachments = files.map(file => ({
|
||||
filename: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
data: file.buffer
|
||||
}));
|
||||
|
||||
const messageData = {
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
channel: 'web',
|
||||
attachments: attachments,
|
||||
conversationId: conversationId || null,
|
||||
recipientId: recipientId || null,
|
||||
userId: userId
|
||||
};
|
||||
|
||||
// Обработка через unified processor
|
||||
const result = await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
userMessageId: result.userMessageId,
|
||||
conversationId: result.conversationId,
|
||||
aiResponse: result.aiResponse ? {
|
||||
response: result.aiResponse.response
|
||||
} : null,
|
||||
noAiResponse: result.noAiResponse
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -223,15 +411,21 @@ router.post('/process-guest', requireAuth, async (req, res) => {
|
||||
return res.status(400).json({ success: false, error: 'guestId is required' });
|
||||
}
|
||||
try {
|
||||
const guestMessageService = require('../services/guestMessageService');
|
||||
const result = await guestMessageService.processGuestMessages(userId, guestId);
|
||||
if (result && result.conversationId) {
|
||||
return res.json({ success: true, conversationId: result.conversationId });
|
||||
const universalGuestService = require('../services/UniversalGuestService');
|
||||
const identifier = `web:${guestId}`; // Старые гости всегда из web
|
||||
const result = await universalGuestService.migrateToUser(identifier, userId);
|
||||
|
||||
if (result && result.success) {
|
||||
return res.json({
|
||||
success: true,
|
||||
conversationId: result.conversationId,
|
||||
migratedMessages: result.migratedCount
|
||||
});
|
||||
} else {
|
||||
return res.json({ success: false, error: result.error || 'No conversation created' });
|
||||
return res.json({ success: false, error: result.error || 'Migration failed' });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in /process-guest:', error);
|
||||
logger.error('Error in /migrate-guest-messages:', error);
|
||||
return res.status(500).json({ success: false, error: 'Internal error' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -199,4 +199,45 @@ router.put('/db-settings', requireAuth, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Проверка статуса токена связывания
|
||||
* Используется на странице /connect-wallet для валидации токена
|
||||
*/
|
||||
router.get('/link-status/:token', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Токен не указан'
|
||||
});
|
||||
}
|
||||
|
||||
const identityLinkService = require('../services/IdentityLinkService');
|
||||
const tokenData = await identityLinkService.verifyLinkToken(token);
|
||||
|
||||
if (!tokenData) {
|
||||
return res.json({
|
||||
valid: false,
|
||||
error: 'Токен недействителен или истек'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
valid: true,
|
||||
provider: tokenData.source_provider,
|
||||
expiresAt: tokenData.expires_at,
|
||||
isUsed: tokenData.is_used
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Identity] Ошибка проверки статуса токена:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Внутренняя ошибка сервера'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -27,6 +27,49 @@ router.get('/', async (req, res) => {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
// Проверяем, это гостевой идентификатор (формат: channel:rawId)
|
||||
if (userId && userId.includes(':')) {
|
||||
const guestResult = await db.getQuery()(
|
||||
`SELECT
|
||||
id,
|
||||
decrypt_text(identifier_encrypted, $2) as user_id,
|
||||
channel,
|
||||
decrypt_text(content_encrypted, $2) as content,
|
||||
content_type,
|
||||
attachments,
|
||||
media_metadata,
|
||||
is_ai,
|
||||
created_at
|
||||
FROM unified_guest_messages
|
||||
WHERE decrypt_text(identifier_encrypted, $2) = $1
|
||||
ORDER BY created_at ASC`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
|
||||
// Преобразуем формат для совместимости с фронтендом
|
||||
const messages = guestResult.rows.map(msg => ({
|
||||
id: msg.id,
|
||||
user_id: msg.user_id,
|
||||
sender_type: msg.is_ai ? 'bot' : 'user',
|
||||
content: msg.content,
|
||||
channel: msg.channel,
|
||||
role: 'guest',
|
||||
direction: msg.is_ai ? 'incoming' : 'outgoing',
|
||||
created_at: msg.created_at,
|
||||
attachment_filename: null,
|
||||
attachment_mimetype: null,
|
||||
attachment_size: null,
|
||||
attachment_data: null,
|
||||
// Дополнительные поля для медиа
|
||||
content_type: msg.content_type,
|
||||
attachments: msg.attachments,
|
||||
media_metadata: msg.media_metadata
|
||||
}));
|
||||
|
||||
return res.json(messages);
|
||||
}
|
||||
|
||||
// Стандартная логика для зарегистрированных пользователей
|
||||
let result;
|
||||
if (conversationId) {
|
||||
result = await db.getQuery()(
|
||||
@@ -279,6 +322,24 @@ router.post('/broadcast', async (req, res) => {
|
||||
return res.status(400).json({ error: 'user_id и content обязательны' });
|
||||
}
|
||||
|
||||
// ✨ Проверка прав через adminLogicService (только editor может делать рассылку!)
|
||||
const encryptedDb = require('../services/encryptedDatabaseService');
|
||||
const users = await encryptedDb.getData('users', { id: req.session.userId }, 1);
|
||||
const userRole = users && users.length > 0 ? users[0].role : 'user';
|
||||
|
||||
const adminLogicService = require('../services/adminLogicService');
|
||||
const canBroadcast = adminLogicService.canPerformAdminAction({
|
||||
role: userRole, // Передаем точную роль ('editor', 'readonly', 'user')
|
||||
action: 'broadcast_message'
|
||||
});
|
||||
|
||||
if (!canBroadcast) {
|
||||
logger.warn(`[Messages] Пользователь ${req.session.userId} (роль: ${userRole}) пытался сделать broadcast без прав`);
|
||||
return res.status(403).json({
|
||||
error: 'Только редакторы (editor) могут делать массовую рассылку'
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
@@ -19,8 +19,11 @@ const aiCache = require('../services/ai-cache');
|
||||
const logger = require('../utils/logger');
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
|
||||
const TIMEOUTS = ollamaConfig.getTimeouts();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const results = {};
|
||||
|
||||
// Backend
|
||||
results.backend = { status: 'ok' };
|
||||
|
||||
@@ -28,7 +31,7 @@ router.get('/', async (req, res) => {
|
||||
try {
|
||||
const baseUrl = process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001';
|
||||
const healthUrl = baseUrl.replace(/\/$/, '') + '/health';
|
||||
const vs = await axios.get(healthUrl, { timeout: 2000 });
|
||||
const vs = await axios.get(healthUrl, { timeout: TIMEOUTS.vectorHealth });
|
||||
results.vectorSearch = { status: 'ok', ...vs.data };
|
||||
} catch (e) {
|
||||
console.log('Vector Search error:', e.message, 'Status:', e.response?.status);
|
||||
@@ -37,8 +40,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// Ollama
|
||||
try {
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
const ollama = await axios.get(ollamaConfig.getApiUrl('tags'), { timeout: 2000 });
|
||||
const ollama = await axios.get(ollamaConfig.getApiUrl('tags'), { timeout: TIMEOUTS.ollamaHealth });
|
||||
results.ollama = { status: 'ok', models: ollama.data.models?.length || 0 };
|
||||
} catch (e) {
|
||||
results.ollama = { status: 'error', error: e.message };
|
||||
@@ -52,6 +54,37 @@ router.get('/', async (req, res) => {
|
||||
results.postgres = { status: 'error', error: e.message };
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: AI Cache статистика
|
||||
try {
|
||||
const ragService = require('../services/ragService');
|
||||
const cacheStats = ragService.getCacheStats();
|
||||
results.aiCache = {
|
||||
status: 'ok',
|
||||
size: cacheStats.size,
|
||||
maxSize: cacheStats.maxSize,
|
||||
hitRate: `${(cacheStats.hitRate * 100).toFixed(2)}%`,
|
||||
byType: cacheStats.byType
|
||||
};
|
||||
} catch (e) {
|
||||
results.aiCache = { status: 'error', error: e.message };
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: AI Queue статистика
|
||||
try {
|
||||
const ragService = require('../services/ragService');
|
||||
const queueStats = ragService.getQueueStats();
|
||||
results.aiQueue = {
|
||||
status: 'ok',
|
||||
currentSize: queueStats.currentQueueSize,
|
||||
totalProcessed: queueStats.totalProcessed,
|
||||
totalFailed: queueStats.totalFailed,
|
||||
avgResponseTime: `${Math.round(queueStats.averageProcessingTime)}ms`,
|
||||
uptime: `${Math.round(queueStats.uptime / 1000)}s`
|
||||
};
|
||||
} catch (e) {
|
||||
results.aiQueue = { status: 'error', error: e.message };
|
||||
}
|
||||
|
||||
res.json({ status: 'ok', services: results, timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
|
||||
@@ -15,20 +15,23 @@ const router = express.Router();
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const execAsync = util.promisify(exec);
|
||||
const axios = require('axios');
|
||||
const logger = require('../utils/logger');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
|
||||
// Инициализируем один раз
|
||||
const TIMEOUTS = ollamaConfig.getTimeouts();
|
||||
|
||||
// Проверка статуса подключения к Ollama
|
||||
router.get('/status', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
// Проверяем API Ollama через HTTP запрос
|
||||
try {
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000 // 5 секунд таймаут
|
||||
timeout: TIMEOUTS.ollamaTags // Централизованный таймаут
|
||||
});
|
||||
|
||||
const models = response.data.models || [];
|
||||
@@ -54,12 +57,10 @@ router.get('/status', requireAuth, async (req, res) => {
|
||||
// Получение списка установленных моделей
|
||||
router.get('/models', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaConfig = require('../services/ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
timeout: TIMEOUTS.ollamaTags
|
||||
});
|
||||
|
||||
const models = (response.data.models || []).map(model => ({
|
||||
|
||||
@@ -17,6 +17,7 @@ const logger = require('../utils/logger');
|
||||
const { ethers } = require('ethers');
|
||||
const db = require('../db');
|
||||
const rpcProviderService = require('../services/rpcProviderService');
|
||||
const encryptedDb = require('../services/encryptedDatabaseService');
|
||||
|
||||
// Функция для получения информации о сети по chain_id
|
||||
function getNetworkInfo(chainId) {
|
||||
@@ -494,10 +495,13 @@ router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) =>
|
||||
// Получить текущие настройки Email (для страницы Email)
|
||||
router.get('/email-settings', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const settings = await botsSettings.getEmailSettings();
|
||||
logger.info('[Settings] Запрос getBotSettings(email)');
|
||||
const settings = await botsSettings.getBotSettings('email');
|
||||
logger.info('[Settings] getBotSettings(email) успешно:', settings);
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
logger.error('[Settings] Ошибка getBotSettings(email):', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -540,7 +544,7 @@ router.put('/email-settings', requireAdmin, async (req, res, next) => {
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
const result = await botsSettings.saveEmailSettings(settings);
|
||||
const result = await botsSettings.saveBotSettings('email', settings);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при обновлении email настроек:', error);
|
||||
@@ -599,20 +603,27 @@ router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) =>
|
||||
// Получить список всех email (для ассистента)
|
||||
router.get('/email-settings/list', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const emails = await botsSettings.getAllEmailSettings();
|
||||
logger.info('[Settings] Запрос списка email');
|
||||
const emails = await encryptedDb.getData('email_settings', {}, 1000, 'id ASC');
|
||||
logger.info('[Settings] Получено email:', emails ? emails.length : 0);
|
||||
res.json({ success: true, items: emails });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
logger.error('[Settings] Ошибка получения списка email:', error);
|
||||
logger.error('[Settings] Stack:', error.stack);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить текущие настройки Telegram-бота (для страницы Telegram)
|
||||
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const settings = await botsSettings.getTelegramSettings();
|
||||
logger.info('[Settings] Запрос getBotSettings(telegram)');
|
||||
const settings = await botsSettings.getBotSettings('telegram');
|
||||
logger.info('[Settings] getBotSettings успешно:', settings);
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
logger.error('[Settings] Ошибка getBotSettings(telegram):', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -637,7 +648,7 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
const result = await botsSettings.saveTelegramSettings(settings);
|
||||
const result = await botsSettings.saveBotSettings('telegram', settings);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при обновлении настроек Telegram:', error);
|
||||
@@ -648,10 +659,14 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
// Получить список всех Telegram-ботов (для ассистента)
|
||||
router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const bots = await botsSettings.getAllTelegramBots();
|
||||
logger.info('[Settings] Запрос списка telegram ботов');
|
||||
const bots = await encryptedDb.getData('telegram_settings', {}, 1000, 'id ASC');
|
||||
logger.info('[Settings] Получено telegram ботов:', bots ? bots.length : 0);
|
||||
res.json({ success: true, items: bots });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
logger.error('[Settings] Ошибка получения списка telegram:', error);
|
||||
logger.error('[Settings] Stack:', error.stack);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Формируем ответ ---
|
||||
// --- Формируем ответ для зарегистрированных пользователей ---
|
||||
const contacts = users.map(u => ({
|
||||
id: u.id,
|
||||
name: [u.first_name, u.last_name].filter(Boolean).join(' ').trim() || null,
|
||||
@@ -222,7 +222,61 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
role: u.role || 'user'
|
||||
}));
|
||||
|
||||
res.json({ success: true, contacts });
|
||||
// --- Добавляем гостевые контакты ---
|
||||
const guestContactsResult = await db.getQuery()(
|
||||
`WITH decrypted_guests AS (
|
||||
SELECT
|
||||
decrypt_text(identifier_encrypted, $1) as guest_identifier,
|
||||
channel,
|
||||
created_at,
|
||||
user_id
|
||||
FROM unified_guest_messages
|
||||
WHERE user_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
guest_identifier,
|
||||
channel,
|
||||
MIN(created_at) as created_at,
|
||||
MAX(created_at) as last_message_at,
|
||||
COUNT(*) as message_count
|
||||
FROM decrypted_guests
|
||||
GROUP BY guest_identifier, channel
|
||||
ORDER BY MAX(created_at) DESC`,
|
||||
[encryptionKey]
|
||||
);
|
||||
|
||||
const guestContacts = guestContactsResult.rows.map(g => {
|
||||
const channelMap = {
|
||||
'web': '🌐',
|
||||
'telegram': '📱',
|
||||
'email': '✉️'
|
||||
};
|
||||
const icon = channelMap[g.channel] || '👤';
|
||||
const rawId = g.guest_identifier.replace(`${g.channel}:`, '');
|
||||
|
||||
return {
|
||||
id: g.guest_identifier, // Используем unified identifier как ID
|
||||
name: `${icon} ${g.channel === 'web' ? 'Гость' : g.channel} (${rawId.substring(0, 8)}...)`,
|
||||
email: g.channel === 'email' ? rawId : null,
|
||||
telegram: g.channel === 'telegram' ? rawId : null,
|
||||
wallet: null,
|
||||
created_at: g.created_at,
|
||||
preferred_language: [],
|
||||
is_blocked: false,
|
||||
contact_type: 'guest',
|
||||
role: 'guest',
|
||||
guest_info: {
|
||||
channel: g.channel,
|
||||
message_count: parseInt(g.message_count),
|
||||
last_message_at: g.last_message_at
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Объединяем списки
|
||||
const allContacts = [...contacts, ...guestContacts];
|
||||
|
||||
res.json({ success: true, contacts: allContacts });
|
||||
} catch (error) {
|
||||
logger.error('Error fetching contacts:', error);
|
||||
next(error);
|
||||
@@ -401,9 +455,64 @@ router.get('/:id', async (req, res, next) => {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
|
||||
const query = db.getQuery();
|
||||
// Получаем пользователя
|
||||
|
||||
// Проверяем, это гостевой идентификатор (формат: channel:rawId)
|
||||
if (userId.includes(':')) {
|
||||
const guestResult = await query(
|
||||
`WITH decrypted_guest AS (
|
||||
SELECT
|
||||
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
||||
channel,
|
||||
created_at
|
||||
FROM unified_guest_messages
|
||||
WHERE decrypt_text(identifier_encrypted, $2) = $1
|
||||
)
|
||||
SELECT
|
||||
guest_identifier,
|
||||
channel,
|
||||
MIN(created_at) as created_at,
|
||||
MAX(created_at) as last_message_at,
|
||||
COUNT(*) as message_count
|
||||
FROM decrypted_guest
|
||||
GROUP BY guest_identifier, channel`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
|
||||
if (guestResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Guest contact not found' });
|
||||
}
|
||||
|
||||
const guest = guestResult.rows[0];
|
||||
const rawId = userId.replace(`${guest.channel}:`, '');
|
||||
const channelMap = {
|
||||
'web': '🌐',
|
||||
'telegram': '📱',
|
||||
'email': '✉️'
|
||||
};
|
||||
const icon = channelMap[guest.channel] || '👤';
|
||||
|
||||
return res.json({
|
||||
id: userId,
|
||||
name: `${icon} ${guest.channel === 'web' ? 'Гость' : guest.channel} (${rawId.substring(0, 8)}...)`,
|
||||
email: guest.channel === 'email' ? rawId : null,
|
||||
telegram: guest.channel === 'telegram' ? rawId : null,
|
||||
wallet: null,
|
||||
created_at: guest.created_at,
|
||||
preferred_language: [],
|
||||
is_blocked: false,
|
||||
contact_type: 'guest',
|
||||
role: 'guest',
|
||||
guest_info: {
|
||||
channel: guest.channel,
|
||||
message_count: parseInt(guest.message_count),
|
||||
last_message_at: guest.last_message_at,
|
||||
raw_identifier: rawId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем пользователя (зарегистрированный)
|
||||
const userResult = await query('SELECT id, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId]);
|
||||
if (userResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
|
||||
Reference in New Issue
Block a user