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

This commit is contained in:
2025-10-09 20:49:51 +03:00
parent 13fb51e447
commit 34666b44d8
11 changed files with 553 additions and 53 deletions

View File

@@ -0,0 +1,85 @@
/**
* Фильтр сообщений по языку
* AI ассистент работает только на русском языке
*/
/**
* Проверяет наличие кириллицы в тексте
*/
function hasCyrillic(text) {
if (!text || typeof text !== 'string') return false;
return /[а-яА-ЯЁё]/.test(text);
}
/**
* Определяет процент кириллицы в тексте
*/
function getCyrillicPercentage(text) {
if (!text) return 0;
const cyrillicChars = (text.match(/[а-яА-ЯЁё]/g) || []).length;
const totalChars = text.replace(/\s/g, '').length;
return totalChars > 0 ? (cyrillicChars / totalChars) * 100 : 0;
}
/**
* Проверяет, является ли сообщение на русском языке
* @param {string} message - текст сообщения
* @param {number} minCyrillicPercent - минимальный % кириллицы (по умолчанию 10%)
* @returns {boolean}
*/
function isRussianMessage(message, minCyrillicPercent = 10) {
if (!message || typeof message !== 'string') return false;
// Убираем пробелы и спецсимволы для точного подсчета
const cleanText = message.trim();
// Если сообщение очень короткое (например "Hi"), считаем русским
if (cleanText.length < 10) {
return hasCyrillic(cleanText);
}
// Для длинных сообщений проверяем процент кириллицы
const cyrillicPercent = getCyrillicPercentage(cleanText);
return cyrillicPercent >= minCyrillicPercent;
}
/**
* Определяет, нужно ли обрабатывать сообщение AI
* @param {string} message - текст сообщения
* @returns {Object} { shouldProcess: boolean, reason: string }
*/
function shouldProcessWithAI(message) {
if (!message || typeof message !== 'string') {
return { shouldProcess: false, reason: 'Empty message' };
}
const cleanMessage = message.trim();
// Проверка на русский язык
if (!isRussianMessage(cleanMessage)) {
return {
shouldProcess: false,
reason: 'Non-Russian message (AI works only with Russian)'
};
}
// Проверка на максимальный размер (опционально)
const MAX_LENGTH = 10000;
if (cleanMessage.length > MAX_LENGTH) {
return {
shouldProcess: false,
reason: `Message too long (${cleanMessage.length} > ${MAX_LENGTH} chars)`
};
}
return { shouldProcess: true, reason: 'OK' };
}
module.exports = {
hasCyrillic,
getCyrillicPercentage,
isRussianMessage,
shouldProcessWithAI
};

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* 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 logger = require('./logger');
const ollamaConfig = require('../services/ollamaConfig');
/**
* Проверяет, загружена ли модель в память через /api/ps
* НЕ триггерит загрузку модели (в отличие от /api/generate)
* @param {string} modelName - название модели (например: qwen2.5:7b)
* @returns {Promise<boolean>} true если модель в памяти
*/
async function isModelLoaded(modelName) {
try {
const axios = require('axios');
const baseUrl = ollamaConfig.getBaseUrl();
// Используем /api/ps - показывает какие модели сейчас в памяти
// Этот endpoint НЕ триггерит загрузку модели!
const response = await axios.get(`${baseUrl}/api/ps`, {
timeout: 3000
});
// Проверяем, есть ли наша модель в списке загруженных
if (response.data && response.data.models) {
return response.data.models.some(m => {
// Сравниваем без тега (qwen2.5 == qwen2.5:7b)
const modelBaseName = modelName.split(':')[0];
const loadedBaseName = (m.name || m.model || '').split(':')[0];
return loadedBaseName === modelBaseName;
});
}
return false;
} catch (error) {
// API ps может не существовать в старых версиях Ollama
// Или модель не загружена
return false;
}
}
/**
* Ожидание готовности Ollama и загрузки модели в память
* Ollama может загружаться до 4 минут при старте Docker контейнера
* entrypoint.sh загружает модель qwen2.5:7b в память с keep_alive=24h
*
* @param {Object} options - Опции ожидания
* @param {number} options.maxWaitTime - Максимальное время ожидания в мс (по умолчанию 4 минуты)
* @param {number} options.retryInterval - Интервал между попытками в мс (по умолчанию 5 секунд)
* @param {boolean} options.required - Обязательно ли ждать Ollama (по умолчанию false)
* @returns {Promise<boolean>} true если Ollama готов, false если таймаут (и required=false)
*/
async function waitForOllama(options = {}) {
const {
maxWaitTime = parseInt(process.env.OLLAMA_WAIT_TIME) || 4 * 60 * 1000,
retryInterval = parseInt(process.env.OLLAMA_RETRY_INTERVAL) || 5000,
required = process.env.OLLAMA_REQUIRED === 'true' || false
} = options;
const startTime = Date.now();
let attempt = 0;
const maxAttempts = Math.ceil(maxWaitTime / retryInterval);
logger.info(`[waitForOllama] ⏳ Ожидание готовности Ollama и загрузки модели в память (макс. ${maxWaitTime/1000}с, интервал ${retryInterval/1000}с)...`);
while (Date.now() - startTime < maxWaitTime) {
attempt++;
try {
// Шаг 1: Проверяем доступность Ollama API
const healthStatus = await ollamaConfig.checkHealth();
if (healthStatus.status === 'ok') {
const model = healthStatus.model;
// Шаг 2: Проверяем, загружена ли модель в память
const modelReady = await isModelLoaded(model);
if (modelReady) {
const waitedTime = ((Date.now() - startTime) / 1000).toFixed(1);
logger.info(`[waitForOllama] ✅ Ollama готов! Модель ${model} загружена в память (ожидание ${waitedTime}с, попытка ${attempt}/${maxAttempts})`);
logger.info(`[waitForOllama] 📊 Ollama: ${healthStatus.baseUrl}, доступно моделей: ${healthStatus.availableModels}`);
return true;
} else {
logger.info(`[waitForOllama] ⏳ Ollama API готов, но модель ${model} ещё грузится в память... (попытка ${attempt}/${maxAttempts})`);
}
} else {
logger.warn(`[waitForOllama] ⚠️ Ollama API не готов (попытка ${attempt}/${maxAttempts}): ${healthStatus.error}`);
}
} catch (error) {
logger.warn(`[waitForOllama] ⚠️ Ошибка проверки Ollama (попытка ${attempt}/${maxAttempts}): ${error.message}`);
}
// Если не последняя попытка - ждем перед следующей
if (Date.now() - startTime < maxWaitTime) {
const remainingTime = Math.max(0, maxWaitTime - (Date.now() - startTime));
const nextRetry = Math.min(retryInterval, remainingTime);
if (nextRetry > 0) {
await new Promise(resolve => setTimeout(resolve, nextRetry));
}
}
}
// Таймаут истек
const totalWaitTime = ((Date.now() - startTime) / 1000).toFixed(1);
if (required) {
const error = `Ollama не готов после ${totalWaitTime}с ожидания (${attempt} попыток)`;
logger.error(`[waitForOllama] ❌ ${error}`);
throw new Error(error);
} else {
logger.warn(`[waitForOllama] ⚠️ Ollama не готов после ${totalWaitTime}с ожидания (${attempt} попыток). Продолжаем без AI.`);
return false;
}
}
/**
* Проверка готовности Ollama (одна попытка, без ожидания)
* @returns {Promise<boolean>} true если Ollama готов
*/
async function isOllamaReady() {
try {
const healthStatus = await ollamaConfig.checkHealth();
if (healthStatus.status !== 'ok') return false;
// Проверяем модель
return await isModelLoaded(healthStatus.model);
} catch (error) {
return false;
}
}
module.exports = {
waitForOllama,
isOllamaReady
};