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

This commit is contained in:
2025-10-09 16:48:20 +03:00
parent dd2c9988a5
commit 13fb51e447
60 changed files with 7694 additions and 1157 deletions

View File

@@ -0,0 +1,759 @@
# 🔧 ЗАДАЧА: Рефакторинг AI сервисов (устранение дублей + интеграция Queue/Cache)
**Дата:** 2025-10-09
**Приоритет:** ВЫСОКИЙ
**Статус:** 📋 В РАЗРАБОТКЕ
---
## 🎯 **ЦЕЛЬ**
Устранить дублирование кода и интегрировать существующие сервисы `ai-queue.js` и `ai-cache.js` в основной поток обработки.
---
## ❌ **НАЙДЕННЫЕ ДУБЛИ**
### **ДУБЛЬ #1: Кэширование** ⭐⭐⭐ КРИТИЧЕСКИЙ
#### **`ragService.js` (строки 20-22, 78-84, 182-185):**
```javascript
const ragCache = new Map(); // ❌ Примитивный дубль!
const RAG_CACHE_TTL = 5 * 60 * 1000;
// Использование:
const cached = ragCache.get(cacheKey);
ragCache.set(cacheKey, { result, timestamp: Date.now() });
```
#### **`ai-cache.js` (весь файл, 95 строк):**
```javascript
class AICache {
this.cache = new Map(); // ✅ Полноценный сервис!
this.maxSize = 1000;
this.ttl = 24 * 60 * 60 * 1000;
// + управление размером
// + автоочистка
// + статистика
}
```
**Вывод:** Удалить `ragCache` → использовать `ai-cache.js`
---
### **ДУБЛЬ #2: Вызовы Ollama API** ⭐⭐⭐ КРИТИЧЕСКИЙ
#### **`ragService.js` (строки 358-371):**
```javascript
const axios = require('axios'); // ❌ Внутри функции!
const ollamaConfig = require('./ollamaConfig');
const response = await axios.post(`${ollamaUrl}/api/chat`, {
model: model || ollamaConfig.getDefaultModel(),
messages: messages,
stream: false
}, {
timeout: ollamaConfig.getTimeout()
});
```
**Проблема:** Прямой вызов → пропускается `ai-queue.js`
**Вывод:** Использовать `ai-queue.addTask()`
---
### **ДУБЛЬ #3: Генерация ключа кэша** ⭐⭐
#### **`ragService.js`:**
```javascript
const cacheKey = `${tableId}:${userQuestion}:${product}`; // ❌ Простая строка
```
#### **`ai-cache.js`:**
```javascript
generateKey(messages, options = {}) {
return crypto.createHash('md5').update(content).digest('hex'); // ✅ MD5 хеш
}
```
**Вывод:** Использовать единый метод из `ai-cache.js`
---
### **ДУБЛЬ #4: Import внутри функций** ⭐⭐
**`ragService.js` (строки 359-361):**
```javascript
async function generateLLMResponse({...}) {
const axios = require('axios'); // ❌ Каждый раз!
const ollamaConfig = require('./ollamaConfig'); // ❌ Каждый раз!
}
```
**Вывод:** Вынести импорты наверх файла
---
### **ДУБЛЬ #5: Fallback на несуществующую очередь** ⭐
**`ragService.js` (строки 375-379):**
```javascript
if (error.message.includes('очередь перегружена') && answer) { // ❌ Очередь не используется!
return answer;
}
```
**Вывод:** Удалить или исправить после интеграции очереди
---
## 🔧 **ПЛАН ИСПРАВЛЕНИЙ**
### **ЭТАП 1: Доработать `ai-cache.js`** ⭐⭐⭐
**Файл:** `backend/services/ai-cache.js`
**Добавить методы:**
```javascript
class AICache {
constructor() {
this.cache = new Map();
this.maxSize = 1000;
this.ttl = 24 * 60 * 60 * 1000; // Default: 24 часа
this.ragTtl = 5 * 60 * 1000; // ✨ НОВОЕ: 5 минут для RAG
}
// ✨ НОВОЕ: Генерация ключа для RAG результатов
generateKeyForRAG(tableId, userQuestion, product = null) {
const content = JSON.stringify({ tableId, userQuestion, product });
return crypto.createHash('md5').update(content).digest('hex');
}
// ✨ НОВОЕ: Получение с учетом типа (RAG или LLM)
getWithTTL(key, type = 'llm') {
const cached = this.cache.get(key);
if (!cached) return null;
const ttl = type === 'rag' ? this.ragTtl : this.ttl;
if (Date.now() - cached.timestamp > ttl) {
this.cache.delete(key);
return null;
}
return cached.response;
}
// ✨ НОВОЕ: Сохранение с типом
setWithType(key, response, type = 'llm') {
// Очищаем старые записи если кэш переполнен
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(),
type: type // ✨ Сохраняем тип
});
logger.info(`[AICache] Cached ${type} response for key: ${key.substring(0, 8)}...`);
}
// ✨ НОВОЕ: Инвалидация по префиксу (для RAG при обновлении таблиц)
invalidateByPrefix(prefix) {
let deletedCount = 0;
for (const [key, value] of this.cache.entries()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
deletedCount++;
}
}
if (deletedCount > 0) {
logger.info(`[AICache] Invalidated ${deletedCount} entries with prefix: ${prefix}`);
}
return deletedCount;
}
// ✨ НОВОЕ: Статистика по типу
getStatsByType() {
const stats = { rag: 0, llm: 0, other: 0 };
for (const [key, value] of this.cache.entries()) {
const type = value.type || 'other';
stats[type] = (stats[type] || 0) + 1;
}
return stats;
}
}
```
---
### **ЭТАП 2: Доработать `ai-queue.js`** ⭐⭐⭐
**Файл:** `backend/services/ai-queue.js`
**Добавить методы для обработки:**
```javascript
const axios = require('axios');
const ollamaConfig = require('./ollamaConfig');
const aiCache = require('./ai-cache');
class AIQueue extends EventEmitter {
constructor() {
super();
this.queue = [];
this.isProcessing = false; // ✨ НОВОЕ
this.maxQueueSize = 100; // ✨ НОВОЕ
this.workerInterval = null; // ✨ НОВОЕ
this.stats = {
totalAdded: 0,
totalProcessed: 0,
totalFailed: 0,
avgResponseTime: 0,
lastProcessedAt: null,
initializedAt: Date.now()
};
}
// ✨ НОВОЕ: Добавление задачи с Promise
async addTask(taskData) {
// Проверяем лимит очереди
if (this.queue.length >= this.maxQueueSize) {
throw new Error('Очередь переполнена');
}
const taskId = Date.now() + Math.random();
const priority = taskData.priority || 5;
const queueItem = {
id: taskId,
request: taskData,
priority,
status: 'queued',
timestamp: Date.now()
};
this.queue.push(queueItem);
this.queue.sort((a, b) => b.priority - a.priority);
this.stats.totalAdded++;
logger.info(`[AIQueue] Задача ${taskId} добавлена (priority: ${priority}). Очередь: ${this.queue.length}`);
this.emit('requestAdded', queueItem);
// Возвращаем Promise для ожидания результата
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Queue timeout'));
}, 120000); // 2 минуты
this.once(`task_${taskId}_completed`, (result) => {
clearTimeout(timeout);
resolve(result.response);
});
this.once(`task_${taskId}_failed`, (error) => {
clearTimeout(timeout);
reject(new Error(error.message));
});
});
}
// ✨ НОВОЕ: Запуск автоматического worker
startWorker() {
if (this.workerInterval) {
logger.warn('[AIQueue] Worker уже запущен');
return;
}
logger.info('[AIQueue] 🚀 Запуск worker для обработки очереди...');
this.workerInterval = setInterval(() => {
this.processNextTask();
}, 100); // Проверяем очередь каждые 100ms
}
// ✨ НОВОЕ: Остановка worker
stopWorker() {
if (this.workerInterval) {
clearInterval(this.workerInterval);
this.workerInterval = null;
logger.info('[AIQueue] ⏹️ Worker остановлен');
}
}
// ✨ НОВОЕ: Обработка следующей задачи
async processNextTask() {
if (this.isProcessing) return;
const task = this.getNextRequest();
if (!task) return;
this.isProcessing = true;
const startTime = Date.now();
try {
logger.info(`[AIQueue] Обработка задачи ${task.id}`);
// 1. Проверяем кэш
const cacheKey = aiCache.generateKey(task.request.messages);
const cached = aiCache.get(cacheKey);
if (cached) {
logger.info(`[AIQueue] Cache HIT для задачи ${task.id}`);
const responseTime = Date.now() - startTime;
this.updateRequestStatus(task.id, 'completed', cached, null, responseTime);
this.emit(`task_${task.id}_completed`, { response: cached, fromCache: true });
return;
}
// 2. Вызываем Ollama API
const ollamaUrl = ollamaConfig.getBaseUrl();
const timeouts = ollamaConfig.getTimeouts();
const response = await axios.post(`${ollamaUrl}/api/chat`, {
model: task.request.model || ollamaConfig.getDefaultModel(),
messages: task.request.messages,
stream: false
}, {
timeout: timeouts.ollamaChat
});
const result = response.data.message.content;
const responseTime = Date.now() - startTime;
// 3. Сохраняем в кэш
aiCache.set(cacheKey, result);
// 4. Обновляем статус
this.updateRequestStatus(task.id, 'completed', result, null, responseTime);
this.emit(`task_${task.id}_completed`, { response: result, fromCache: false });
logger.info(`[AIQueue] ✅ Задача ${task.id} выполнена за ${responseTime}ms`);
} catch (error) {
logger.error(`[AIQueue] ❌ Ошибка задачи ${task.id}:`, error.message);
this.updateRequestStatus(task.id, 'failed', null, error.message);
this.emit(`task_${task.id}_failed`, { message: error.message });
} finally {
this.isProcessing = false;
}
}
}
```
---
### **ЭТАП 3: Рефакторинг `ragService.js`** ⭐⭐⭐
**Файл:** `backend/services/ragService.js`
**Изменения:**
#### **3.1. Вынести импорты наверх (строки 13-23)**
**Было:**
```javascript
const encryptedDb = require('./encryptedDatabaseService');
const vectorSearch = require('./vectorSearchClient');
const logger = require('../utils/logger');
// Простой кэш для RAG результатов
const ragCache = new Map(); // ❌ УДАЛИТЬ!
const RAG_CACHE_TTL = 5 * 60 * 1000; // ❌ УДАЛИТЬ!
```
**Стало:**
```javascript
const encryptedDb = require('./encryptedDatabaseService');
const vectorSearch = require('./vectorSearchClient');
const logger = require('../utils/logger');
const axios = require('axios'); // ✨ НОВОЕ
const ollamaConfig = require('./ollamaConfig'); // ✨ НОВОЕ
const aiCache = require('./ai-cache'); // ✨ НОВОЕ
const aiQueue = require('./ai-queue'); // ✨ НОВОЕ
// Флаги для включения/выключения
const USE_AI_CACHE = process.env.USE_AI_CACHE !== 'false'; // default: true
const USE_AI_QUEUE = process.env.USE_AI_QUEUE !== 'false'; // default: true
```
#### **3.2. Заменить `ragCache` на `ai-cache` (строки 78-84)**
**Было:**
```javascript
const cacheKey = `${tableId}:${userQuestion}:${product}`;
const cached = ragCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < RAG_CACHE_TTL) {
return cached.result;
}
```
**Стало:**
```javascript
// Используем ai-cache с коротким TTL для RAG
const cacheKey = aiCache.generateKeyForRAG(tableId, userQuestion, product);
const cached = aiCache.getWithTTL(cacheKey, 'rag');
if (cached) {
console.log('[RAG] Возврат из кэша');
return cached;
}
```
#### **3.3. Заменить `ragCache.set()` (строки 182-185)**
**Было:**
```javascript
ragCache.set(cacheKey, {
result,
timestamp: Date.now()
});
```
**Стало:**
```javascript
// Сохраняем в ai-cache с типом 'rag'
aiCache.setWithType(cacheKey, result, 'rag');
```
#### **3.4. Заменить прямой вызов Ollama на очередь (строки 358-383)**
**Было:**
```javascript
async function generateLLMResponse({...}) {
// ...
try {
const axios = require('axios'); // ❌ Внутри!
const ollamaConfig = require('./ollamaConfig'); // ❌ Внутри!
const response = await axios.post(`${ollamaUrl}/api/chat`, {...});
llmResponse = response.data.message.content;
} catch (error) {
// ...
}
}
```
**Стало:**
```javascript
async function generateLLMResponse({
userQuestion,
context,
answer,
systemPrompt,
history,
model,
metadata = {}
}) {
try {
// Формируем сообщения для LLM
const messages = [];
const finalSystemPrompt = systemPrompt || 'Ты — ИИ-ассистент для бизнеса. Отвечай кратко и по делу';
if (finalSystemPrompt) {
messages.push({ role: 'system', content: finalSystemPrompt });
}
for (const h of (history || [])) {
if (h && h.content) {
const role = h.role === 'assistant' ? 'assistant' : 'user';
messages.push({ role, content: h.content });
}
}
// Формируем финальный промпт
let prompt = `Вопрос пользователя: ${userQuestion}`;
if (answer) prompt += `\n\nНайденный ответ из базы знаний: ${answer}`;
if (context) prompt += `\n\nДополнительный контекст: ${context}`;
messages.push({ role: 'user', content: prompt });
// ✨ НОВОЕ: Определяем приоритет
const priority = metadata.isAdmin ? 10 : metadata.isGuest ? 1 : 5;
let llmResponse;
// ✨ НОВОЕ: Используем очередь (если включена)
if (USE_AI_QUEUE) {
try {
llmResponse = await aiQueue.addTask({
messages,
model,
priority,
metadata
});
console.log('[RAG] LLM response from queue:', llmResponse?.substring(0, 100) + '...');
return llmResponse;
} catch (queueError) {
logger.warn('[RAG] Queue error, fallback to direct call:', queueError.message);
// Fallback: если очередь перегружена и есть ответ из RAG - возвращаем его
if (queueError.message.includes('переполнена') && answer) {
logger.info('[RAG] Возврат прямого ответа из RAG (очередь переполнена)');
return answer;
}
// Иначе пробуем прямой вызов (без очереди)
// Продолжаем к прямому вызову ниже
}
}
// Прямой вызов (если очередь отключена или ошибка)
try {
const ollamaUrl = ollamaConfig.getBaseUrl();
const timeouts = ollamaConfig.getTimeouts();
const response = await axios.post(`${ollamaUrl}/api/chat`, {
model: model || ollamaConfig.getDefaultModel(),
messages,
stream: false
}, {
timeout: timeouts.ollamaChat
});
llmResponse = response.data.message.content;
console.log('[RAG] LLM response (direct):', llmResponse?.substring(0, 100) + '...');
return llmResponse;
} catch (error) {
console.error('[RAG] Error in direct Ollama call:', error.message);
// Финальный fallback - возврат ответа из RAG
if (answer) {
logger.info('[RAG] Возврат прямого ответа из RAG (ошибка Ollama)');
return answer;
}
return 'Извините, произошла ошибка при генерации ответа.';
}
} catch (error) {
console.error('[RAG] Critical error in generateLLMResponse:', error);
return 'Извините, произошла ошибка при генерации ответа.';
}
}
```
---
### **ЭТАП 4: Запустить Worker в `server.js`**
**Файл:** `backend/server.js`
**Добавить после инициализации BotManager:**
```javascript
// Запускаем AI Queue Worker
const aiQueue = require('./services/ai-queue');
const aiQueueInstance = new aiQueue();
aiQueueInstance.startWorker();
logger.info('[Server] ✅ AI Queue Worker запущен');
// Graceful shutdown
process.on('SIGTERM', () => {
aiQueueInstance.stopWorker();
process.exit(0);
});
```
---
### **ЭТАП 5: Передать метаданные из основного потока**
#### **5.1. `ai-assistant.js` (строка ~120)**
**Добавить в вызов `generateLLMResponse`:**
```javascript
const aiResponse = await generateLLMResponse({
userQuestion,
context: ragResult?.context || '',
answer: ragResult?.answer || '',
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
history: conversationHistory,
model: aiSettings ? aiSettings.model : undefined,
rules: rules ? rules.rules : null,
metadata: { // ✨ НОВОЕ
isAdmin: metadata?.isAdmin || false,
isGuest: metadata?.isGuest || false,
channel: channel
}
});
```
#### **5.2. `UniversalGuestService.js` (строка ~350)**
**Передать metadata:**
```javascript
const aiResponse = await aiAssistant.generateResponse({
channel: channel,
messageId: `guest_${identifier}_${Date.now()}`,
userId: identifier,
userQuestion: fullMessageContent,
conversationHistory: conversationHistory,
metadata: {
isGuest: true, // ✅ Уже есть
priority: 1, // ✨ НОВОЕ - низкий приоритет для гостей
hasMedia: !!processedContent,
mediaSummary: processedContent?.summary
}
});
```
#### **5.3. `unifiedMessageProcessor.js` (строка ~203)**
**Передать metadata:**
```javascript
aiResponse = await aiAssistant.generateResponse({
channel,
messageId: userMessageId,
userId: userId,
userQuestion: content,
conversationHistory,
conversationId,
metadata: {
hasAttachments: attachments.length > 0,
channel,
isAdmin, // ✅ Уже есть
priority: isAdmin ? 10 : 5 // ✨ НОВОЕ - высокий приоритет для админов
}
});
```
---
### **ЭТАП 6: Обновить мониторинг**
**Файл:** `backend/routes/monitoring.js`
**Добавить статистику:**
```javascript
// AI Cache статистика
const aiCache = require('../services/ai-cache');
const cacheStats = aiCache.getStats();
const cacheByType = aiCache.getStatsByType();
results.aiCache = {
status: 'ok',
size: cacheStats.size,
maxSize: cacheStats.maxSize,
hitRate: `${(cacheStats.hitRate * 100).toFixed(2)}%`,
byType: cacheByType
};
// AI Queue статистика
const AIQueue = require('../services/ai-queue');
const queueStats = aiQueueInstance.getStats();
results.aiQueue = {
status: 'ok',
currentSize: queueStats.currentQueueSize,
totalProcessed: queueStats.totalProcessed,
totalFailed: queueStats.totalFailed,
avgResponseTime: `${Math.round(queueStats.averageProcessingTime)}ms`
};
```
---
## 📋 **ЧЕКЛИСТ ИСПРАВЛЕНИЙ**
### **Доработка существующих файлов:**
- [ ] **1. `ai-cache.js`** - добавить методы:
- [ ] `generateKeyForRAG(tableId, question, product)`
- [ ] `getWithTTL(key, type)`
- [ ] `setWithType(key, response, type)`
- [ ] `invalidateByPrefix(prefix)`
- [ ] `getStatsByType()`
- [ ] **2. `ai-queue.js`** - добавить методы:
- [ ] `addTask(taskData)` - возвращает Promise
- [ ] `startWorker()` - запуск обработки
- [ ] `stopWorker()` - остановка
- [ ] `processNextTask()` - обработка с Ollama + Cache
- [ ] Свойство `maxQueueSize = 100`
- [ ] **3. `ragService.js`** - исправить:
- [ ] Удалить `ragCache` и `RAG_CACHE_TTL`
- [ ] Добавить импорты наверху: `axios`, `ollamaConfig`, `aiCache`, `aiQueue`
- [ ] Заменить `ragCache.get()``aiCache.getWithTTL(key, 'rag')`
- [ ] Заменить `ragCache.set()``aiCache.setWithType(key, result, 'rag')`
- [ ] В `generateLLMResponse()`:
- Удалить `require()` внутри функции
- Добавить вызов `aiQueue.addTask()`
- Оставить fallback на прямой вызов
- [ ] **4. `ai-assistant.js`** - передать metadata:
- [ ] Добавить `metadata` в вызов `generateLLMResponse()`
- [ ] **5. `UniversalGuestService.js`** - передать priority:
- [ ] Добавить `priority: 1` в metadata
- [ ] **6. `unifiedMessageProcessor.js`** - передать priority:
- [ ] Добавить `priority: isAdmin ? 10 : 5` в metadata
- [ ] **7. `server.js`** - запустить worker:
- [ ] Создать экземпляр `AIQueue`
- [ ] Вызвать `aiQueueInstance.startWorker()`
- [ ] Добавить graceful shutdown
- [ ] **8. `routes/monitoring.js`** - добавить статистику:
- [ ] Статистика `aiCache`
- [ ] Статистика `aiQueue`
---
## ⏱️ **ОЦЕНКА ВРЕМЕНИ**
| Файл | Изменения | Время |
|------|-----------|-------|
| `ai-cache.js` | +5 методов | 1-2 часа |
| `ai-queue.js` | +3 метода + worker | 2-3 часа |
| `ragService.js` | Удаление дублей, интеграция | 2-3 часа |
| Остальные | Передача metadata | 1 час |
| Тестирование | Полное | 2-3 часа |
**ИТОГО:** 8-12 часов
---
## 🚀 **ПОРЯДОК РАБОТЫ**
1. ✅ Доработать `ai-cache.js` (добавить методы)
2. ✅ Доработать `ai-queue.js` (добавить worker)
3. ✅ Рефакторить `ragService.js` (убрать дубли)
4. ✅ Интегрировать в основной поток
5. ✅ Протестировать
---
**Статус:** ✅ ВЫПОЛНЕНО
## ✅ **ВЫПОЛНЕННЫЕ ЗАДАЧИ:**
1. ✅ Доработан `ai-cache.js` (+5 методов, TTL из ollamaConfig)
2. ✅ Доработан `ai-queue.js` (+worker, FIFO без приоритетов)
3. ✅ Рефакторинг `ragService.js` (удален ragCache, интеграция Cache + Queue)
4. ✅ Обновлен `adminLogicService.js` (editor/readonly, удалены неиспользуемые методы)
5. ✅ Добавлена валидация прав (chat.js, messages.js, auth.js)
6. ✅ Удалены legacy сервисы (guestService, guestMessageService, index.js)
7. ✅ Интегрирован WebBot (botManager использует класс)
8. ✅ Централизованы все AI таймауты в ollamaConfig.js
**Всего изменено:** 13 файлов
**Удалено:** 3 файла
**Создано:** 0 файлов (только доработка существующих!)