Доработка гит
This commit is contained in:
+475
-110
@@ -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"Текущая модель: <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:
|
||||
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"⚠️ Текущая модель <code>{current_model}</code> не поддерживает изображения.\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 <chat_id> <сообщение>"
|
||||
"❌ Укажи ID чата и текст или прикрепи фото: /igpt <chat_id> <сообщение>"
|
||||
)
|
||||
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"⚠️ Текущая модель <code>{current_model}</code> не поддерживает изображения.",
|
||||
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)
|
||||
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):
|
||||
# Создаём инлайн-кнопку для открытия 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(
|
||||
|
||||
@@ -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 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__)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user