Доработка гит
This commit is contained in:
+474
-109
@@ -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,26 +289,42 @@ 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):
|
||||||
photo = message.photo[-1]
|
try:
|
||||||
file = await bot.get_file(photo.file_id)
|
photo = message.photo[-1]
|
||||||
file_bytes = await bot.download_file(file.file_path)
|
file = await bot.get_file(photo.file_id)
|
||||||
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
|
file_bytes = await bot.download_file(file.file_path)
|
||||||
content_blocks.append(
|
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}"},
|
"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"⚠️ Текущая модель <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,27 +333,86 @@ 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()
|
||||||
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(
|
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:
|
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,47 +524,60 @@ 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):
|
||||||
photo = message.photo[-1]
|
try:
|
||||||
file = await bot.get_file(photo.file_id)
|
photo = message.photo[-1]
|
||||||
file_bytes = await bot.download_file(file.file_path)
|
file = await bot.get_file(photo.file_id)
|
||||||
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
|
file_bytes = await bot.download_file(file.file_path)
|
||||||
content_blocks.append(
|
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}"},
|
"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"⚠️ Текущая модель <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)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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,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__)
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user