diff --git a/addons/gpt/handlers.py b/addons/gpt/handlers.py
index e9dab8b..f39d824 100644
--- a/addons/gpt/handlers.py
+++ b/addons/gpt/handlers.py
@@ -2,21 +2,71 @@ import base64
import aiohttp
import logging
from aiogram import Dispatcher, Bot
-from aiogram.types import Message
+from aiogram.types import Message, InlineKeyboardButton, CallbackQuery
+from aiogram.utils.keyboard import InlineKeyboardBuilder
from utils.antispam import saving, save_message, admin_required
from aiogram.filters import Command
from models.state import BotState
+import hashlib
+import json
+from typing import Dict, List, Tuple, Optional
logger = logging.getLogger(__name__)
+# Глобальные переменные на уровне модуля
+current_model = "google/gemma-3-12b"
+model_hash_map: Dict[str, str] = {} # Словарь для хранения соответствия хэшей и полных названий моделей
+
+# Список моделей, которые точно поддерживают изображения (можно расширять)
+IMAGE_SUPPORTED_MODELS = [
+ "llava", # Модели LLaVA
+ "vision", # Модели с поддержкой vision
+ "clip", # CLIP модели
+ "qwen2-vl", # Qwen с vision
+ "cogvlm", # CogVLM
+ "paligemma", # PaLI-Gemma
+ "gemma-2-2b-it", # Gemma 2 может поддерживать
+ "gemma-2-9b-it",
+ "gemini", # Gemini
+ "llava-hf", # LLaVA HuggingFace
+ "moondream", # Moondream
+]
+
+
+def get_model_hash(model_name: str) -> str:
+ """Создает короткий хэш для названия модели"""
+ return hashlib.md5(model_name.encode()).hexdigest()[:16]
+
+
+def truncate_model_name(model_name: str, max_length: int = 40) -> str:
+ """Сокращает название модели для отображения"""
+ if len(model_name) <= max_length:
+ return model_name
+ return model_name[:max_length - 3] + "..."
+
+
+def supports_images(model_name: str) -> bool:
+ """Проверяет, поддерживает ли модель изображения"""
+ model_lower = model_name.lower()
+ # Если модель содержит ключевые слова, связанные с поддержкой изображений
+ for keyword in IMAGE_SUPPORTED_MODELS:
+ if keyword.lower() in model_lower:
+ return True
+ return False
+
+
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
chat_history = {}
MAX_HISTORY = 20 # храним последние 20 сообщений (user+assistant)
MODEL = "google/gemma-3-12b"
- #MODEL = "google/gemma-3n-e4b"
- URL = "http://192.168.31.197:1234/v1/chat/completions"
- #URL = "http://192.168.31.95:1234/v1/chat/completions"
+ # MODEL = "google/gemma-3n-e4b"
+ URL_BASE = "10.180.139.124"
+ URL = f"http://{URL_BASE}:1234/v1/chat/completions"
+ # URL = "http://192.168.31.95:1234/v1/chat/completions"
+ MODELS_URL = f"http://{URL_BASE}:1234/v1/models" # URL для получения списка моделей
+ # Используем глобальные переменные
+ global current_model, model_hash_map
# # Системный промт для всех чатов
SYSTEM_PROMPT = {
@@ -60,10 +110,171 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
# ],
# }
- @dp.message(Command("gpt"))
+ @dp.message(Command("models"))
+ @admin_required(0)
@saving
- async def ask_gpt(message: Message):
- chat_id = message.chat.id
+ async def list_models(message: Message):
+ """Получить список доступных моделей и выбрать одну"""
+ global model_hash_map, current_model
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(MODELS_URL) as resp:
+ if resp.status != 200:
+ error_text = await resp.text()
+ await message.reply(
+ f"❌ Ошибка при получении списка моделей: {resp.status} {error_text}"
+ )
+ return
+
+ data = await resp.json()
+
+ # Формат ответа может отличаться в зависимости от API
+ models = []
+
+ # Формат OpenAI-compatible API
+ if isinstance(data, dict) and "data" in data:
+ models = [model["id"] for model in data["data"]]
+
+ # Другой возможный формат
+ elif isinstance(data, list):
+ models = data
+
+ # Если не распознали формат
+ else:
+ await message.reply(f"❌ Неизвестный формат ответа от API: {data}")
+ return
+
+ if not models:
+ await message.reply("❌ Нет доступных моделей")
+ return
+
+ # Создаем клавиатуру для выбора модели
+ builder = InlineKeyboardBuilder()
+
+ for model in models:
+ # Создаем короткий хэш для callback_data
+ model_hash = get_model_hash(model)
+ model_hash_map[model_hash] = model # Сохраняем в глобальный словарь
+
+ # Сокращаем для отображения
+ display_name = truncate_model_name(model)
+
+ # Помечаем текущую модель и добавляем иконку для моделей с поддержкой изображений
+ prefix = "✅ " if model == current_model else "• "
+ icon = "🖼️ " if supports_images(model) else ""
+
+ builder.row(
+ InlineKeyboardButton(
+ text=f"{prefix}{icon}{display_name}",
+ callback_data=f"model:{model_hash}"
+ )
+ )
+
+ # Кнопки для управления
+ builder.row(
+ InlineKeyboardButton(
+ text="🔄 Обновить список",
+ callback_data="refresh_models"
+ ),
+ InlineKeyboardButton(
+ text="📋 Показать текущую",
+ callback_data="show_current"
+ ),
+ InlineKeyboardButton(
+ text="🖼️ Модели с картинками",
+ callback_data="show_image_models"
+ )
+ )
+
+ # Сохраняем карту хэшей в файл для отладки (опционально)
+ try:
+ with open("model_hash_map.json", "w") as f:
+ json.dump(model_hash_map, f, indent=2, ensure_ascii=False)
+ except:
+ pass
+
+ # Разделяем модели на поддерживающие изображения и обычные
+ image_models = [m for m in models if supports_images(m)]
+ text_only_models = [m for m in models if not supports_images(m)]
+
+ models_list = "\n".join(
+ [f"{i + 1}. {model} {'🖼️' if supports_images(model) else ''}" for i, model in
+ enumerate(models[:10])])
+ if len(models) > 10:
+ models_list += f"\n... и еще {len(models) - 10} моделей"
+
+ await message.reply(
+ f"📋 Доступные модели ({len(models)} шт.):\n"
+ f"🖼️ Поддерживают изображения: {len(image_models)}\n"
+ f"📝 Только текст: {len(text_only_models)}\n"
+ f"Текущая модель: {current_model} {'🖼️' if supports_images(current_model) else '📝'}\n\n"
+ "Первые 10 моделей:\n" + models_list + "\n\n"
+ "Выберите модель из списка ниже (🖼️ - поддерживает изображения):",
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+
+ except Exception as e:
+ logger.error(f"Ошибка при получении списка моделей: {e}")
+ await message.reply(f"❌ Ошибка при получении списка моделей: {e}")
+
+ @dp.message(Command("setmodel"))
+ @admin_required(0)
+ @saving
+ async def set_model(message: Message):
+ """Установить модель через команду"""
+ global current_model, model_hash_map
+
+ if not message.text:
+ await message.reply("❌ Укажите название модели: /setmodel <название_модели>")
+ return
+
+ args = message.text.split(maxsplit=1)
+ if len(args) < 2:
+ await message.reply("❌ Укажите название модели: /setmodel <название_модели>")
+ return
+
+ new_model = args[1]
+
+ # Проверяем, есть ли модель в текущем кэше (опционально)
+ if model_hash_map:
+ model_hashes = list(model_hash_map.values())
+ if new_model not in model_hashes:
+ await message.reply(
+ f"⚠️ Модель {new_model} не найдена в последнем списке.\n"
+ f"Используйте /models для просмотра доступных моделей.\n"
+ f"Продолжаем смену модели...",
+ parse_mode="HTML"
+ )
+
+ old_model = current_model
+ current_model = new_model
+
+ # Сообщаем о поддержке изображений
+ image_support = "🖼️ поддерживает изображения" if supports_images(new_model) else "📝 только текст"
+
+ await message.reply(
+ f"✅ Модель успешно изменена! ({image_support})\n"
+ f"📊 Старая модель: {old_model}\n"
+ f"📈 Новая модель: {current_model}",
+ parse_mode="HTML"
+ )
+
+ @dp.message(Command("currentmodel"))
+ @saving
+ async def show_current_model(message: Message):
+ """Показать текущую модель"""
+ global current_model
+ image_support = "🖼️ Поддерживает изображения" if supports_images(current_model) else "📝 Только текст"
+ await message.reply(
+ f"🤖 Текущая модель: {current_model}\n"
+ f"{image_support}",
+ parse_mode="HTML"
+ )
+
+ async def prepare_gpt_request(chat_id: int, message: Message) -> Tuple[Optional[List[dict]], Optional[str]]:
+ """Подготавливает запрос к GPT, обрабатывая текст и изображения"""
if chat_id not in chat_history:
chat_history[chat_id] = [SYSTEM_PROMPT]
@@ -78,26 +289,42 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if message.caption:
user_prompt = message.caption
+ # Добавляем текстовый промпт, если есть
if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt})
+ elif not message.photo:
+ # Если нет ни текста, ни фото
+ return None, "❗ Укажи текст или прикрепи фото"
- # Фото → base64 → image_url
- if message.photo:
- photo = message.photo[-1]
- file = await bot.get_file(photo.file_id)
- file_bytes = await bot.download_file(file.file_path)
- image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
- content_blocks.append(
- {
- "type": "image_url",
- "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
- }
- )
+ # Обрабатываем фото, если модель поддерживает изображения
+ if message.photo and supports_images(current_model):
+ try:
+ photo = message.photo[-1]
+ file = await bot.get_file(photo.file_id)
+ file_bytes = await bot.download_file(file.file_path)
+ image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
+ content_blocks.append(
+ {
+ "type": "image_url",
+ "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
+ }
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при обработке изображения: {e}")
+ # Если не удалось обработать изображение, продолжаем без него
+ if not user_prompt:
+ return None, "❌ Ошибка при обработке изображения"
+ elif message.photo and not supports_images(current_model):
+ # Если модель не поддерживает изображения, отправляем текстовое описание
+ if not user_prompt:
+ # Если нет текста, просим переформулировать
+ return None, f"⚠️ Текущая модель {current_model} не поддерживает изображения.\nОтправьте текстовое описание или смените модель на поддерживающую изображения (🖼️)."
+ # Если есть текст, просто игнорируем изображение и отправляем текст
+ logger.info(f"Модель {current_model} не поддерживает изображения, отправляем только текст")
+ # Если после всех проверок content_blocks пустой
if not content_blocks:
- await message.reply("❗ Укажи текст или прикрепи фото")
- return
-
+ return None, "❗ Укажи текст или прикрепи фото"
# Добавляем новое сообщение в историю
chat_history[chat_id].append({"role": "user", "content": content_blocks})
@@ -106,27 +333,86 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
+ return chat_history[chat_id], None
+
+ @dp.message(Command("gpt"))
+ @saving
+ async def ask_gpt(message: Message):
+ global current_model
+ chat_id = message.chat.id
+
+ # Подготавливаем запрос
+ prepared_history, error_message = await prepare_gpt_request(chat_id, message)
+ if error_message:
+ await message.reply(error_message, parse_mode="HTML")
+ return
+
payload = {
- "model": MODEL,
- "messages": chat_history[chat_id],
+ "model": current_model,
+ "messages": prepared_history,
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
"ttl": 300,
}
+ # Флаг, указывающий на попытку отправки изображения
+ has_image = bool(message.photo)
+ image_attempt_failed = False
+
try:
async with aiohttp.ClientSession() as session:
async with session.post(URL, json=payload) as resp:
if resp.status != 200:
error_text = await resp.text()
- await message.reply(
- f"❌ Ошибка LM Studio: {resp.status} {error_text}"
- )
- return
- data = await resp.json()
- reply_text = data["choices"][0]["message"]["content"]
+ # Проверяем, если ошибка из-за изображения
+ if has_image and "does not support images" in error_text:
+ image_attempt_failed = True
+ logger.warning(
+ f"Модель {current_model} не поддерживает изображения, пробуем без изображения")
+
+ # Удаляем последнее сообщение из истории (с изображением)
+ chat_history[chat_id].pop()
+
+ # Пробуем отправить только текст
+ if message.caption:
+ # Используем caption как промпт
+ content_blocks = [{"type": "text", "text": message.caption}]
+ elif message.text:
+ parts = message.text.split(maxsplit=1)
+ if len(parts) > 1:
+ content_blocks = [{"type": "text", "text": parts[1]}]
+ else:
+ content_blocks = [{"type": "text", "text": "Опиши изображение"}]
+ else:
+ content_blocks = [{"type": "text", "text": "Опиши изображение"}]
+
+ # Добавляем текстовый запрос
+ chat_history[chat_id].append({"role": "user", "content": content_blocks})
+
+ # Обновляем payload
+ payload["messages"] = chat_history[chat_id]
+
+ # Повторяем запрос без изображения
+ async with session.post(URL, json=payload) as retry_resp:
+ if retry_resp.status != 200:
+ error_text = await retry_resp.text()
+ await message.reply(
+ f"❌ Ошибка LM Studio: {retry_resp.status} {error_text}"
+ )
+ return
+
+ data = await retry_resp.json()
+ reply_text = data["choices"][0]["message"]["content"]
+ else:
+ await message.reply(
+ f"❌ Ошибка LM Studio: {resp.status} {error_text}"
+ )
+ return
+ else:
+ data = await resp.json()
+ reply_text = data["choices"][0]["message"]["content"]
# Сохраняем ответ ассистента в историю
chat_history[chat_id].append(
@@ -140,14 +426,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
+ # Добавляем заметку о попытке отправки изображения
+ image_note = ""
+ if image_attempt_failed:
+ image_note = f"\n\n⚠️ Модель не поддерживает изображения, отправлен только текстовый запрос."
+
# Делим сообщение на части по 4000 символов
MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + MAX_LEN]
- msg = await message.reply(f"🤖 Ответ:\n{chunk}")
+ if i == 0:
+ msg = await message.reply(f"🤖 Ответ (модель: {current_model}){image_note}:\n{chunk}")
+ else:
+ msg = await message.reply(chunk)
save_message(msg.chat.id, msg.message_id)
-
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@@ -155,59 +448,27 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("agpt"))
@admin_required(0)
@saving
- async def ask_gpt(message: Message):
+ async def ask_gpt_admin(message: Message):
+ global current_model
chat_id = message.chat.id
- if chat_id not in chat_history:
- chat_history[chat_id] = [SYSTEM_PROMPT]
- content_blocks = []
- user_prompt = None
-
- # Текст после команды или caption
- if message.text:
- parts = message.text.split(maxsplit=1)
- if len(parts) > 1:
- user_prompt = parts[1]
- if message.caption:
- user_prompt = message.caption
-
- if user_prompt:
- content_blocks.append({"type": "text", "text": user_prompt})
-
- # Фото → base64 → image_url
- if message.photo:
- photo = message.photo[-1]
- file = await bot.get_file(photo.file_id)
- file_bytes = await bot.download_file(file.file_path)
- image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
- content_blocks.append(
- {
- "type": "image_url",
- "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
- }
- )
-
- if not content_blocks:
- await message.reply("❗ Укажи текст или прикрепи фото")
+ # Подготавливаем запрос
+ prepared_history, error_message = await prepare_gpt_request(chat_id, message)
+ if error_message:
+ await message.reply(error_message, parse_mode="HTML")
return
-
- # Добавляем новое сообщение в историю
- chat_history[chat_id].append({"role": "user", "content": content_blocks})
-
- # Ограничиваем историю (оставляем последние MAX_HISTORY сообщений)
- if len(chat_history[chat_id]) > MAX_HISTORY:
- chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
-
payload = {
- "model": MODEL,
- "messages": chat_history[chat_id],
+ "model": current_model,
+ "messages": prepared_history,
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
"ttl": 300,
}
+ target_chat_id = -1003038389942
+
try:
async with aiohttp.ClientSession() as session:
async with session.post(URL, json=payload) as resp:
@@ -237,10 +498,10 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + MAX_LEN]
- msg = await bot.send_message(chat_id=-1003038389942, text=f"{chunk}")
+ msg = await bot.send_message(chat_id=target_chat_id,
+ text=f"🤖 Ответ (модель: {current_model}):\n{chunk}")
save_message(msg.chat.id, msg.message_id)
-
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@@ -248,13 +509,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("igpt"))
@admin_required(0)
@saving
- async def ask_gpt(message: Message):
+ async def ask_gpt_interactive(message: Message):
+ global current_model
raw_text = message.text or message.caption
- if not raw_text and not (
- message.photo or message.document or message.audio or message.video
- ):
+ if not raw_text and not (message.photo):
await message.reply(
- "❌ Укажи ID чата и текст или прикрепи файл/медиа: /igpt <сообщение>"
+ "❌ Укажи ID чата и текст или прикрепи фото: /igpt <сообщение>"
)
return
@@ -264,47 +524,60 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return
try:
- chat_id = int(args[1]) # первый аргумент — ID чата
+ target_chat_id = int(args[1]) # первый аргумент — ID чата
except ValueError:
await message.reply("❌ Неверный формат chat_id")
return
user_prompt = args[2] if len(args) > 2 else ""
- if chat_id not in chat_history:
- chat_history[chat_id] = [SYSTEM_PROMPT]
+
+ # Используем локальный chat_id для обработки запроса
+ local_chat_id = message.chat.id
+ if local_chat_id not in chat_history:
+ chat_history[local_chat_id] = [SYSTEM_PROMPT]
content_blocks = []
if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt})
- # Фото → base64 → image_url
- if message.photo:
- photo = message.photo[-1]
- file = await bot.get_file(photo.file_id)
- file_bytes = await bot.download_file(file.file_path)
- image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
- content_blocks.append(
- {
- "type": "image_url",
- "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
- }
- )
+ # Фото → base64 → image_url (только если модель поддерживает)
+ if message.photo and supports_images(current_model):
+ try:
+ photo = message.photo[-1]
+ file = await bot.get_file(photo.file_id)
+ file_bytes = await bot.download_file(file.file_path)
+ image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
+ content_blocks.append(
+ {
+ "type": "image_url",
+ "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
+ }
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при обработке изображения: {e}")
+ if not user_prompt:
+ await message.reply("❌ Ошибка при обработке изображения")
+ return
+ elif message.photo and not supports_images(current_model):
+ await message.reply(f"⚠️ Текущая модель {current_model} не поддерживает изображения.",
+ parse_mode="HTML")
+ if not user_prompt:
+ return
if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото")
return
-
# Добавляем новое сообщение в историю
- chat_history[chat_id].append({"role": "user", "content": content_blocks})
+ chat_history[local_chat_id].append({"role": "user", "content": content_blocks})
- # Ограничиваем историю (оставляем последние MAX_HISTORY сообщений)
- if len(chat_history[chat_id]) > MAX_HISTORY:
- chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
+ # Ограничиваем историю
+ if len(chat_history[local_chat_id]) > MAX_HISTORY:
+ chat_history[local_chat_id] = chat_history[local_chat_id][-MAX_HISTORY:]
payload = {
- "model": MODEL,
- "messages": chat_history[chat_id],
+ "model": current_model,
+ "messages": chat_history[local_chat_id],
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
@@ -325,29 +598,121 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
reply_text = data["choices"][0]["message"]["content"]
# Сохраняем ответ ассистента в историю
- chat_history[chat_id].append(
+ chat_history[local_chat_id].append(
{
"role": "assistant",
"content": [{"type": "text", "text": reply_text}],
}
)
- # Ограничиваем снова (чтобы не разрасталось)
- if len(chat_history[chat_id]) > MAX_HISTORY:
- chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
+ # Ограничиваем снова
+ if len(chat_history[local_chat_id]) > MAX_HISTORY:
+ chat_history[local_chat_id] = chat_history[local_chat_id][-MAX_HISTORY:]
# Делим сообщение на части по 4000 символов
MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + MAX_LEN]
- msg = await bot.send_message(chat_id=chat_id, text=chunk)
+ msg = await bot.send_message(chat_id=target_chat_id,
+ text=f"🤖 Ответ (модель: {current_model}):\n{chunk}")
save_message(msg.chat.id, msg.message_id)
-
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@dp.message(Command("clear"))
async def clear(message: Message):
- chat_history.pop(message.chat.id, None)
\ No newline at end of file
+ chat_history.pop(message.chat.id, None)
+ await message.reply("✅ История диалога очищена")
+
+ # Создаем простые фильтры для callback
+ async def model_callback_filter(callback_query: CallbackQuery) -> bool:
+ return callback_query.data.startswith("model:")
+
+ async def refresh_models_filter(callback_query: CallbackQuery) -> bool:
+ return callback_query.data == "refresh_models"
+
+ async def show_current_filter(callback_query: CallbackQuery) -> bool:
+ return callback_query.data == "show_current"
+
+ async def show_image_models_filter(callback_query: CallbackQuery) -> bool:
+ return callback_query.data == "show_image_models"
+
+ @dp.callback_query(model_callback_filter)
+ async def select_model_callback(callback_query: CallbackQuery):
+ """Обработка выбора модели из инлайн-клавиатуры"""
+ global current_model, model_hash_map
+
+ model_hash = callback_query.data.split(":", 1)[1]
+
+ # Пытаемся загрузить из файла, если в памяти нет
+ if not model_hash_map:
+ try:
+ with open("model_hash_map.json", "r") as f:
+ model_hash_map = json.load(f)
+ except Exception as e:
+ logger.error(f"Ошибка загрузки model_hash_map из файла: {e}")
+
+ # Получаем полное название модели из карты
+ if model_hash not in model_hash_map:
+ await callback_query.answer("❌ Ошибка: модель не найдена в кэше. Используйте /models для обновления списка",
+ show_alert=True)
+ return
+
+ model_id = model_hash_map[model_hash]
+ old_model = current_model
+ current_model = model_id
+
+ # Сообщаем о поддержке изображений
+ image_support = "🖼️ поддерживает изображения" if supports_images(model_id) else "📝 только текст"
+
+ # Обновляем сообщение
+ try:
+ await callback_query.message.edit_text(
+ f"✅ Модель успешно изменена! ({image_support})\n\n"
+ f"📊 Старая модель:\n{old_model}\n\n"
+ f"📈 Новая модель:\n{current_model}\n\n"
+ f"Для просмотра всех моделей используйте /models",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при редактировании сообщения: {e}")
+
+ # Отправляем подтверждение
+ await callback_query.answer(f"✅ Модель изменена на:\n{truncate_model_name(model_id, 30)}")
+
+ @dp.callback_query(refresh_models_filter)
+ async def refresh_models_callback(callback_query: CallbackQuery):
+ """Обновить список моделей"""
+ await list_models(callback_query.message)
+ await callback_query.answer("🔄 Список моделей обновлен")
+
+ @dp.callback_query(show_current_filter)
+ async def show_current_callback(callback_query: CallbackQuery):
+ """Показать текущую модель"""
+ global current_model
+ image_support = "🖼️ Поддерживает изображения" if supports_images(current_model) else "📝 Только текст"
+ await callback_query.answer(f"Текущая модель: {current_model}\n{image_support}", show_alert=True)
+
+ @dp.callback_query(show_image_models_filter)
+ async def show_image_models_callback(callback_query: CallbackQuery):
+ """Показать модели с поддержкой изображений"""
+ global model_hash_map
+
+ if not model_hash_map:
+ await callback_query.answer("❌ Список моделей пуст. Используйте /models", show_alert=True)
+ return
+
+ image_models = [model for model in model_hash_map.values() if supports_images(model)]
+
+ if not image_models:
+ await callback_query.answer("🖼️ Нет моделей с поддержкой изображений", show_alert=True)
+ return
+
+ models_text = "\n".join([f"• {model}" for model in image_models[:10]])
+ if len(image_models) > 10:
+ models_text += f"\n... и еще {len(image_models) - 10} моделей"
+
+ await callback_query.answer(f"🖼️ Модели с поддержкой изображений ({len(image_models)} шт.):\n{models_text}",
+ show_alert=True)
\ No newline at end of file
diff --git a/addons/miniapps/handlers.py b/addons/miniapps/handlers.py
index e6f6a5e..aeba302 100644
--- a/addons/miniapps/handlers.py
+++ b/addons/miniapps/handlers.py
@@ -20,7 +20,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
async def send_welcome(message: Message):
# Создаём инлайн-кнопку для открытия Web App
keyboard = InlineKeyboardMarkup(inline_keyboard=[
- [InlineKeyboardButton(text="Открыть мини-приложение", web_app=WebAppInfo(url=f"https://college.by/accounts/raspis/{datetime.now().month}/{get_day()}-PODNAM.htm"))]
+ [InlineKeyboardButton(text="Открыть мини-приложение", web_app=WebAppInfo(url="https://overfit-percussively-nicolas.ngrok-free.dev"))]
])
await message.answer(
diff --git a/addons/mute/__init__.py b/addons/mute/__init__.py
new file mode 100644
index 0000000..8a90404
--- /dev/null
+++ b/addons/mute/__init__.py
@@ -0,0 +1,9 @@
+def register(dp, state, bot):
+ from . import handlers
+
+ handlers.register_handlers(dp, state, bot)
+
+
+def unregister(dp):
+ # Здесь можно удалить хендлеры, если нужно
+ dp.message_handlers.handlers.clear()
\ No newline at end of file
diff --git a/addons/mute/handlers.py b/addons/mute/handlers.py
new file mode 100644
index 0000000..94e9298
--- /dev/null
+++ b/addons/mute/handlers.py
@@ -0,0 +1,76 @@
+from aiogram import Dispatcher, Bot
+from aiogram.types import Message, ChatPermissions
+from aiogram.filters import Command
+from logging import getLogger
+from datetime import timedelta
+import asyncio
+
+from utils.antispam import admin_required
+from models.state import BotState
+
+
+logger = getLogger(__name__)
+
+# Максимальное время мьюта — 4 минуты (240 секунд)
+MAX_MUTE_SECONDS = 4 * 60 # 240
+
+
+def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
+ @dp.message(Command("mute"))
+ @admin_required(0)
+ async def mute_user(message: Message):
+ logger.info(f"Команда /mute от пользователя {message.from_user.id} в чате {message.chat.id}")
+
+ if not message.reply_to_message:
+ await message.reply("Эта команда должна использоваться в ответ на сообщение.")
+ return
+
+ command_text = message.text or ""
+ if command_text.lower().startswith("/mute@"):
+ command_prefix = command_text.split()[0]
+ else:
+ command_prefix = "/mute"
+
+ args_part = command_text[len(command_prefix):].strip()
+ args = args_part.split() if args_part else []
+
+ # Парсим время (по умолчанию 60 секунд)
+ mute_time = int(args[0]) if args and args[0].isdigit() else 60
+ reason = " ".join(args[1:]) if len(args) > 1 else "Без причины"
+
+ # Ограничиваем максимум 4 минутами
+ if mute_time > MAX_MUTE_SECONDS:
+ old_time = mute_time
+ mute_time = MAX_MUTE_SECONDS
+ reason += f" (время ограничено 4 минутами, было указано {old_time} сек)"
+
+ user_id = message.reply_to_message.from_user.id
+ chat_id = message.chat.id
+
+ try:
+ await bot.restrict_chat_member(
+ chat_id=chat_id,
+ user_id=user_id,
+ permissions=ChatPermissions(can_send_messages=False),
+ until_date=message.date + timedelta(seconds=mute_time)
+ )
+
+ await message.delete()
+
+ notification = await message.reply_to_message.reply(
+ f"⛔ Вас замьютили на {mute_time} секунд.\nПричина: {reason}"
+ )
+
+ asyncio.create_task(delete_after_delay(notification, delay=5))
+
+ except Exception as e:
+ logger.error(f"Ошибка при муте пользователя {user_id}: {e}")
+ await message.reply("Не удалось замьютить пользователя. Возможно, у меня недостаточно прав или пользователь — админ.")
+
+
+async def delete_after_delay(message: Message, delay: int = 5):
+ await asyncio.sleep(delay)
+ try:
+ await message.delete()
+ except Exception as e:
+ logger.debug(f"Не удалось удалить уведомление о мьюте: {e}")
\ No newline at end of file
diff --git a/addons/poll/handlers.py b/addons/poll/handlers.py
index 320c9c7..28029bd 100644
--- a/addons/poll/handlers.py
+++ b/addons/poll/handlers.py
@@ -1,11 +1,10 @@
from config import Config
from utils.antispam import admin_required
from aiogram import Dispatcher, Bot
-from aiogram.types import Message, FSInputFile
+from aiogram.types import Message
from models.state import BotState
from aiogram.filters import Command
from logging import getLogger
-from aiogram.types import PollAnswer
from storage.message_storage import save_message
logger = getLogger(__name__)
diff --git a/addons/x_days_to/handlers.py b/addons/x_days_to/handlers.py
index 39eb011..058d73f 100644
--- a/addons/x_days_to/handlers.py
+++ b/addons/x_days_to/handlers.py
@@ -58,9 +58,20 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
logger.debug(f"До лета осталось {delta} дней")
return delta
+ async def days_to_session() -> int:
+ """Считает дни до 1 июня текущего года (или следующего, если уже лето прошло)."""
+ now = datetime.now()
+ summer = datetime(2026, 7, 6)
+ if now >= summer:
+ logger.warning("days_to_session")
+ delta = (summer - now).days
+ logger.debug(f"До Сессии осталось {delta} дней")
+ return delta
+
async def send_days_to_new_years(user_id: int):
days_ny = await days_to_new_years()
days_summer = await days_to_summer()
+ days_session = await days_to_session()
last_days = await get_last_days(user_id)
if last_days == days_ny:
@@ -69,10 +80,17 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
await save_days_to_db(user_id, days_ny)
- message_text = (
- f"🌞 До лета осталось {days_summer} дней!\n"
- f"🎄 До Нового Года осталось {days_ny} дней!"
- )
+ events = [
+ ("🌞 До лета", days_summer),
+ ("📚 До конца сессии", days_session),
+ ("🎄 До Нового года", days_ny),
+ ]
+
+ # сортировка по числу дней (от меньшего к большему)
+ events_sorted = sorted(events, key=lambda x: x[1])
+
+ message_text = "\n".join([f"{emoji} осталось {days} дней!" for emoji, days in events_sorted])
+
for chat_id in Config.CHAT_IDS:
try:
diff --git a/bot/core.py b/bot/core.py
index 839632f..0e6f059 100644
--- a/bot/core.py
+++ b/bot/core.py
@@ -17,7 +17,7 @@ class TelegramBot:
# Регистрируем обработчики из разных модулей
admin.register_handlers(self.dp, self.state, self.bot)
- schedule.register_handlers(self.dp, self.state)
+ # schedule.register_handlers(self.dp, self.state)
# media.register_handlers(self.dp, self.state, self.bot)
# common.register_handlers(self.dp, self.state, self.bot)
@@ -26,10 +26,10 @@ class TelegramBot:
self.addons.load("id")
self.addons.load("send_message")
self.addons.load("poll")
- # self.addons.load("hello")
- self.addons.load("draw")
+ self.addons.load("hello")
+ # self.addons.load("draw")
self.addons.load("gpt")
- # self.addons.load("rule34")
+ self.addons.load("rule34")
# self.addons.load("todo")
self.addons.load("miniapps")
self.addons.load("x_days_to")
diff --git a/model_hash_map.json b/model_hash_map.json
new file mode 100644
index 0000000..f1fc4b6
--- /dev/null
+++ b/model_hash_map.json
@@ -0,0 +1,8 @@
+{
+ "ca7463d0df0fc143": "captainerisnebula-12b-chimera-v1.1-iq-imatrix",
+ "b6886fc896f68593": "liquid/lfm2.5-1.2b",
+ "43efb8b5d51c38d7": "tiefighter-holodeck-holomax-mythomax-f1-v1-compos-20b",
+ "4f9b128b9b567451": "text-embedding-nomic-embed-text-v1.5",
+ "1ce7277325bb3050": "google/gemma-3n-e4b",
+ "390b8888e1038e17": "google/gemma-3-12b"
+}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 17a5357..5427acd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,79 +1,84 @@
-aenum==3.1.16
-aiofiles==24.1.0
-aiogram==3.22.0
-aiohappyeyeballs==2.6.1
-aiohttp==3.12.15
-aiosignal==1.4.0
-annotated-types==0.7.0
-anyio==4.11.0
-attrs==25.3.0
-beautifulsoup4==4.14.2
-bs4==0.0.2
-certifi==2025.8.3
-cffi==2.0.0
-charset-normalizer==3.4.3
-click==8.3.0
-cryptography==46.0.2
-dataclasses-json==0.6.7
-deepgram-sdk==3.11.0
-deprecation==2.1.0
-distro==1.9.0
-dotenv==0.9.9
-dropbox==12.0.2
-filelock==3.20.0
-frozenlist==1.7.0
-fsspec==2025.9.0
-greenlet==3.2.4
-h11==0.16.0
-hf-xet==1.1.10
-httpcore==1.0.9
-httpx==0.28.1
-huggingface-hub==0.35.3
-idna==3.10
-Jinja2==3.1.6
-jiter==0.11.1
-joblib==1.5.2
-magic-filter==1.0.12
-MarkupSafe==3.0.3
-marshmallow==3.26.1
-mpmath==1.3.0
-msal==1.34.0
-multidict==6.6.4
-mutagen==1.47.0
-mypy_extensions==1.1.0
-networkx==3.5
-numpy==2.3.4
-openai==2.5.0
-packaging==25.0
-playwright==1.55.0
-ply==3.11
-propcache==0.3.2
-pycparser==2.23
-pydantic==2.11.10
-pydantic_core==2.33.2
-pyee==13.0.0
-PyJWT==2.10.1
-python-dotenv==1.1.1
-PyYAML==6.0.3
-regex==2025.9.18
-requests==2.32.5
-ruff==0.14.0
-sacremoses==0.1.1
-safetensors==0.6.2
-sentencepiece==0.2.1
-setuptools==80.9.0
-six==1.17.0
-sniffio==1.3.1
-soupsieve==2.8
-stone==3.3.1
-sympy==1.14.0
-tokenizers==0.22.1
-torch==2.9.0
-tqdm==4.67.1
-transformers==4.57.1
-typing-inspect==0.9.0
-typing-inspection==0.4.2
-typing_extensions==4.15.0
-urllib3==2.5.0
-websockets==15.0.1
-yarl==1.20.1
+ aenum==3.1.16
+ aiofiles==24.1.0
+ aiogram==3.22.0
+ aiohappyeyeballs==2.6.1
+ aiohttp==3.12.15
+ aiosignal==1.4.0
+ aiosqlite==0.21.0
+ annotated-types==0.7.0
+ anyio==4.11.0
+ attrs==25.3.0
+ beautifulsoup4==4.14.2
+ bs4==0.0.2
+ certifi==2025.8.3
+ cffi==2.0.0
+ charset-normalizer==3.4.3
+ click==8.3.0
+ cryptography==46.0.2
+ dataclasses-json==0.6.7
+ deepgram-sdk==3.11.0
+ deprecation==2.1.0
+ distro==1.9.0
+ dotenv==0.9.9
+ dropbox==12.0.2
+ filelock==3.20.0
+ frozenlist==1.7.0
+ fsspec==2025.9.0
+ greenlet==3.2.4
+ h11==0.16.0
+ hf-xet==1.1.10
+ httpcore==1.0.9
+ httpx==0.28.1
+ huggingface-hub==0.35.3
+ idna==3.10
+ Jinja2==3.1.6
+ jiter==0.11.1
+ joblib==1.5.2
+ lxml==6.0.2
+ magic-filter==1.0.12
+ MarkupSafe==3.0.3
+ marshmallow==3.26.1
+ mpmath==1.3.0
+ msal==1.34.0
+ multidict==6.6.4
+ mutagen==1.47.0
+ mypy_extensions==1.1.0
+ networkx==3.5
+ numpy==2.3.4
+ ollama==0.6.0
+ openai==2.5.0
+ packaging==25.0
+ pillow==12.0.0
+ playwright==1.55.0
+ ply==3.11
+ propcache==0.3.2
+ pycparser==2.23
+ pydantic==2.11.10
+ pydantic_core==2.33.2
+ pyee==13.0.0
+ PyJWT==2.10.1
+ python-dotenv==1.1.1
+ PyYAML==6.0.3
+ regex==2025.9.18
+ requests==2.32.5
+ ruff==0.14.6
+ sacremoses==0.1.1
+ safetensors==0.6.2
+ sentencepiece==0.2.1
+ setuptools==80.9.0
+ six==1.17.0
+ sniffio==1.3.1
+ soupsieve==2.8
+ stone==3.3.1
+ sympy==1.14.0
+ tokenizers==0.22.1
+ torch==2.9.0
+ tqdm==4.67.1
+ transformers==4.57.1
+ typing-inspect==0.9.0
+ typing-inspection==0.4.2
+ typing_extensions==4.15.0
+ urllib3==2.5.0
+ websockets==15.0.1
+ yarl==1.20.1
+ yt-dlp==2025.10.22