Доработка гит

This commit is contained in:
Niken
2026-03-15 20:36:16 +03:00
parent 4b50941b86
commit c49b00ba15
9 changed files with 680 additions and 200 deletions
+449 -84
View File
@@ -2,21 +2,71 @@ import base64
import aiohttp import aiohttp
import logging import logging
from aiogram import Dispatcher, Bot 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 utils.antispam import saving, save_message, admin_required
from aiogram.filters import Command from aiogram.filters import Command
from models.state import BotState from models.state import BotState
import hashlib
import json
from typing import Dict, List, Tuple, Optional
logger = logging.getLogger(__name__) 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): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
chat_history = {} chat_history = {}
MAX_HISTORY = 20 # храним последние 20 сообщений (user+assistant) MAX_HISTORY = 20 # храним последние 20 сообщений (user+assistant)
MODEL = "google/gemma-3-12b" MODEL = "google/gemma-3-12b"
#MODEL = "google/gemma-3n-e4b" # MODEL = "google/gemma-3n-e4b"
URL = "http://192.168.31.197:1234/v1/chat/completions" URL_BASE = "10.180.139.124"
#URL = "http://192.168.31.95:1234/v1/chat/completions" 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 = { 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 @saving
async def ask_gpt(message: Message): async def list_models(message: Message):
chat_id = message.chat.id """Получить список доступных моделей и выбрать одну"""
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"Текущая модель: <code>{current_model}</code> {'🖼️' 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"⚠️ Модель <code>{new_model}</code> не найдена в последнем списке.\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"📊 Старая модель: <code>{old_model}</code>\n"
f"📈 Новая модель: <code>{current_model}</code>",
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"🤖 Текущая модель: <code>{current_model}</code>\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: if chat_id not in chat_history:
chat_history[chat_id] = [SYSTEM_PROMPT] chat_history[chat_id] = [SYSTEM_PROMPT]
@@ -78,11 +289,16 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if message.caption: if message.caption:
user_prompt = message.caption user_prompt = message.caption
# Добавляем текстовый промпт, если есть
if user_prompt: if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt}) content_blocks.append({"type": "text", "text": user_prompt})
elif not message.photo:
# Если нет ни текста, ни фото
return None, "❗ Укажи текст или прикрепи фото"
# Фото → base64 → image_url # Обрабатываем фото, если модель поддерживает изображения
if message.photo: if message.photo and supports_images(current_model):
try:
photo = message.photo[-1] photo = message.photo[-1]
file = await bot.get_file(photo.file_id) file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path) file_bytes = await bot.download_file(file.file_path)
@@ -93,11 +309,22 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}, "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"⚠️ Текущая модель <code>{current_model}</code> не поддерживает изображения.\nОтправьте текстовое описание или смените модель на поддерживающую изображения (🖼️)."
# Если есть текст, просто игнорируем изображение и отправляем текст
logger.info(f"Модель {current_model} не поддерживает изображения, отправляем только текст")
# Если после всех проверок content_blocks пустой
if not content_blocks: if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото") return None, "❗ Укажи текст или прикрепи фото"
return
# Добавляем новое сообщение в историю # Добавляем новое сообщение в историю
chat_history[chat_id].append({"role": "user", "content": content_blocks}) chat_history[chat_id].append({"role": "user", "content": content_blocks})
@@ -106,25 +333,84 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if len(chat_history[chat_id]) > MAX_HISTORY: if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = 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 = { payload = {
"model": MODEL, "model": current_model,
"messages": chat_history[chat_id], "messages": prepared_history,
"temperature": 0.7, "temperature": 0.7,
"max_tokens": 4096, "max_tokens": 4096,
"stream": False, "stream": False,
"ttl": 300, "ttl": 300,
} }
# Флаг, указывающий на попытку отправки изображения
has_image = bool(message.photo)
image_attempt_failed = False
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(URL, json=payload) as resp: async with session.post(URL, json=payload) as resp:
if resp.status != 200: if resp.status != 200:
error_text = await resp.text() error_text = await resp.text()
# Проверяем, если ошибка из-за изображения
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( await message.reply(
f"❌ Ошибка LM Studio: {resp.status} {error_text}" f"❌ Ошибка LM Studio: {resp.status} {error_text}"
) )
return return
else:
data = await resp.json() data = await resp.json()
reply_text = data["choices"][0]["message"]["content"] reply_text = data["choices"][0]["message"]["content"]
@@ -140,14 +426,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if len(chat_history[chat_id]) > MAX_HISTORY: if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = 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 символов # Делим сообщение на части по 4000 символов
MAX_LEN = 4000 MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN): for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + 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) save_message(msg.chat.id, msg.message_id)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}") logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(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")) @dp.message(Command("agpt"))
@admin_required(0) @admin_required(0)
@saving @saving
async def ask_gpt(message: Message): async def ask_gpt_admin(message: Message):
global current_model
chat_id = message.chat.id chat_id = message.chat.id
if chat_id not in chat_history:
chat_history[chat_id] = [SYSTEM_PROMPT]
content_blocks = [] # Подготавливаем запрос
user_prompt = None prepared_history, error_message = await prepare_gpt_request(chat_id, message)
if error_message:
# Текст после команды или caption await message.reply(error_message, parse_mode="HTML")
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("❗ Укажи текст или прикрепи фото")
return 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 = { payload = {
"model": MODEL, "model": current_model,
"messages": chat_history[chat_id], "messages": prepared_history,
"temperature": 0.7, "temperature": 0.7,
"max_tokens": 4096, "max_tokens": 4096,
"stream": False, "stream": False,
"ttl": 300, "ttl": 300,
} }
target_chat_id = -1003038389942
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(URL, json=payload) as resp: 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 MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN): for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + 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) save_message(msg.chat.id, msg.message_id)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}") logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(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")) @dp.message(Command("igpt"))
@admin_required(0) @admin_required(0)
@saving @saving
async def ask_gpt(message: Message): async def ask_gpt_interactive(message: Message):
global current_model
raw_text = message.text or message.caption raw_text = message.text or message.caption
if not raw_text and not ( if not raw_text and not (message.photo):
message.photo or message.document or message.audio or message.video
):
await message.reply( await message.reply(
"❌ Укажи ID чата и текст или прикрепи файл/медиа: /igpt <chat_id> <сообщение>" "❌ Укажи ID чата и текст или прикрепи фото: /igpt <chat_id> <сообщение>"
) )
return return
@@ -264,21 +524,25 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return return
try: try:
chat_id = int(args[1]) # первый аргумент — ID чата target_chat_id = int(args[1]) # первый аргумент — ID чата
except ValueError: except ValueError:
await message.reply("❌ Неверный формат chat_id") await message.reply("❌ Неверный формат chat_id")
return return
user_prompt = args[2] if len(args) > 2 else "" 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 = [] content_blocks = []
if user_prompt: if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt}) content_blocks.append({"type": "text", "text": user_prompt})
# Фото → base64 → image_url # Фото → base64 → image_url (только если модель поддерживает)
if message.photo: if message.photo and supports_images(current_model):
try:
photo = message.photo[-1] photo = message.photo[-1]
file = await bot.get_file(photo.file_id) file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path) file_bytes = await bot.download_file(file.file_path)
@@ -289,22 +553,31 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}, "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"⚠️ Текущая модель <code>{current_model}</code> не поддерживает изображения.",
parse_mode="HTML")
if not user_prompt:
return
if not content_blocks: if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото") await message.reply("❗ Укажи текст или прикрепи фото")
return 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: if len(chat_history[local_chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:] chat_history[local_chat_id] = chat_history[local_chat_id][-MAX_HISTORY:]
payload = { payload = {
"model": MODEL, "model": current_model,
"messages": chat_history[chat_id], "messages": chat_history[local_chat_id],
"temperature": 0.7, "temperature": 0.7,
"max_tokens": 4096, "max_tokens": 4096,
"stream": False, "stream": False,
@@ -325,25 +598,25 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
reply_text = data["choices"][0]["message"]["content"] reply_text = data["choices"][0]["message"]["content"]
# Сохраняем ответ ассистента в историю # Сохраняем ответ ассистента в историю
chat_history[chat_id].append( chat_history[local_chat_id].append(
{ {
"role": "assistant", "role": "assistant",
"content": [{"type": "text", "text": reply_text}], "content": [{"type": "text", "text": reply_text}],
} }
) )
# Ограничиваем снова (чтобы не разрасталось) # Ограничиваем снова
if len(chat_history[chat_id]) > MAX_HISTORY: if len(chat_history[local_chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:] chat_history[local_chat_id] = chat_history[local_chat_id][-MAX_HISTORY:]
# Делим сообщение на части по 4000 символов # Делим сообщение на части по 4000 символов
MAX_LEN = 4000 MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN): for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + 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) save_message(msg.chat.id, msg.message_id)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}") logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}") await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@@ -351,3 +624,95 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("clear")) @dp.message(Command("clear"))
async def clear(message: Message): async def clear(message: Message):
chat_history.pop(message.chat.id, None) 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"📊 <b>Старая модель:</b>\n<code>{old_model}</code>\n\n"
f"📈 <b>Новая модель:</b>\n<code>{current_model}</code>\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)
+1 -1
View File
@@ -20,7 +20,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
async def send_welcome(message: Message): async def send_welcome(message: Message):
# Создаём инлайн-кнопку для открытия Web App # Создаём инлайн-кнопку для открытия Web App
keyboard = InlineKeyboardMarkup(inline_keyboard=[ 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( await message.answer(
+9
View File
@@ -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()
+76
View File
@@ -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}")
+1 -2
View File
@@ -1,11 +1,10 @@
from config import Config from config import Config
from utils.antispam import admin_required from utils.antispam import admin_required
from aiogram import Dispatcher, Bot from aiogram import Dispatcher, Bot
from aiogram.types import Message, FSInputFile from aiogram.types import Message
from models.state import BotState from models.state import BotState
from aiogram.filters import Command from aiogram.filters import Command
from logging import getLogger from logging import getLogger
from aiogram.types import PollAnswer
from storage.message_storage import save_message from storage.message_storage import save_message
logger = getLogger(__name__) logger = getLogger(__name__)
+22 -4
View File
@@ -58,9 +58,20 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
logger.debug(f"До лета осталось {delta} дней") logger.debug(f"До лета осталось {delta} дней")
return 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): async def send_days_to_new_years(user_id: int):
days_ny = await days_to_new_years() days_ny = await days_to_new_years()
days_summer = await days_to_summer() days_summer = await days_to_summer()
days_session = await days_to_session()
last_days = await get_last_days(user_id) last_days = await get_last_days(user_id)
if last_days == days_ny: 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) await save_days_to_db(user_id, days_ny)
message_text = ( events = [
f"🌞 До лета осталось {days_summer} дней!\n" ("🌞 До лета", days_summer),
f"🎄 До Нового Года осталось {days_ny} дней!" ("📚 До конца сессии", 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: for chat_id in Config.CHAT_IDS:
try: try:
+4 -4
View File
@@ -17,7 +17,7 @@ class TelegramBot:
# Регистрируем обработчики из разных модулей # Регистрируем обработчики из разных модулей
admin.register_handlers(self.dp, self.state, self.bot) 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) # media.register_handlers(self.dp, self.state, self.bot)
# common.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("id")
self.addons.load("send_message") self.addons.load("send_message")
self.addons.load("poll") self.addons.load("poll")
# self.addons.load("hello") self.addons.load("hello")
self.addons.load("draw") # self.addons.load("draw")
self.addons.load("gpt") self.addons.load("gpt")
# self.addons.load("rule34") self.addons.load("rule34")
# self.addons.load("todo") # self.addons.load("todo")
self.addons.load("miniapps") self.addons.load("miniapps")
self.addons.load("x_days_to") self.addons.load("x_days_to")
+8
View File
@@ -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"
}
+84 -79
View File
@@ -1,79 +1,84 @@
aenum==3.1.16 aenum==3.1.16
aiofiles==24.1.0 aiofiles==24.1.0
aiogram==3.22.0 aiogram==3.22.0
aiohappyeyeballs==2.6.1 aiohappyeyeballs==2.6.1
aiohttp==3.12.15 aiohttp==3.12.15
aiosignal==1.4.0 aiosignal==1.4.0
annotated-types==0.7.0 aiosqlite==0.21.0
anyio==4.11.0 annotated-types==0.7.0
attrs==25.3.0 anyio==4.11.0
beautifulsoup4==4.14.2 attrs==25.3.0
bs4==0.0.2 beautifulsoup4==4.14.2
certifi==2025.8.3 bs4==0.0.2
cffi==2.0.0 certifi==2025.8.3
charset-normalizer==3.4.3 cffi==2.0.0
click==8.3.0 charset-normalizer==3.4.3
cryptography==46.0.2 click==8.3.0
dataclasses-json==0.6.7 cryptography==46.0.2
deepgram-sdk==3.11.0 dataclasses-json==0.6.7
deprecation==2.1.0 deepgram-sdk==3.11.0
distro==1.9.0 deprecation==2.1.0
dotenv==0.9.9 distro==1.9.0
dropbox==12.0.2 dotenv==0.9.9
filelock==3.20.0 dropbox==12.0.2
frozenlist==1.7.0 filelock==3.20.0
fsspec==2025.9.0 frozenlist==1.7.0
greenlet==3.2.4 fsspec==2025.9.0
h11==0.16.0 greenlet==3.2.4
hf-xet==1.1.10 h11==0.16.0
httpcore==1.0.9 hf-xet==1.1.10
httpx==0.28.1 httpcore==1.0.9
huggingface-hub==0.35.3 httpx==0.28.1
idna==3.10 huggingface-hub==0.35.3
Jinja2==3.1.6 idna==3.10
jiter==0.11.1 Jinja2==3.1.6
joblib==1.5.2 jiter==0.11.1
magic-filter==1.0.12 joblib==1.5.2
MarkupSafe==3.0.3 lxml==6.0.2
marshmallow==3.26.1 magic-filter==1.0.12
mpmath==1.3.0 MarkupSafe==3.0.3
msal==1.34.0 marshmallow==3.26.1
multidict==6.6.4 mpmath==1.3.0
mutagen==1.47.0 msal==1.34.0
mypy_extensions==1.1.0 multidict==6.6.4
networkx==3.5 mutagen==1.47.0
numpy==2.3.4 mypy_extensions==1.1.0
openai==2.5.0 networkx==3.5
packaging==25.0 numpy==2.3.4
playwright==1.55.0 ollama==0.6.0
ply==3.11 openai==2.5.0
propcache==0.3.2 packaging==25.0
pycparser==2.23 pillow==12.0.0
pydantic==2.11.10 playwright==1.55.0
pydantic_core==2.33.2 ply==3.11
pyee==13.0.0 propcache==0.3.2
PyJWT==2.10.1 pycparser==2.23
python-dotenv==1.1.1 pydantic==2.11.10
PyYAML==6.0.3 pydantic_core==2.33.2
regex==2025.9.18 pyee==13.0.0
requests==2.32.5 PyJWT==2.10.1
ruff==0.14.0 python-dotenv==1.1.1
sacremoses==0.1.1 PyYAML==6.0.3
safetensors==0.6.2 regex==2025.9.18
sentencepiece==0.2.1 requests==2.32.5
setuptools==80.9.0 ruff==0.14.6
six==1.17.0 sacremoses==0.1.1
sniffio==1.3.1 safetensors==0.6.2
soupsieve==2.8 sentencepiece==0.2.1
stone==3.3.1 setuptools==80.9.0
sympy==1.14.0 six==1.17.0
tokenizers==0.22.1 sniffio==1.3.1
torch==2.9.0 soupsieve==2.8
tqdm==4.67.1 stone==3.3.1
transformers==4.57.1 sympy==1.14.0
typing-inspect==0.9.0 tokenizers==0.22.1
typing-inspection==0.4.2 torch==2.9.0
typing_extensions==4.15.0 tqdm==4.67.1
urllib3==2.5.0 transformers==4.57.1
websockets==15.0.1 typing-inspect==0.9.0
yarl==1.20.1 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