Доработка гит
This commit is contained in:
+474
-109
@@ -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,25 +598,25 @@ 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}")
|
||||
@@ -351,3 +624,95 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
|
||||
@dp.message(Command("clear"))
|
||||
async def clear(message: Message):
|
||||
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:
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ class TelegramBot:
|
||||
|
||||
# Регистрируем обработчики из разных модулей
|
||||
admin.register_handlers(self.dp, self.state, self.bot)
|
||||
schedule.register_handlers(self.dp, self.state)
|
||||
# schedule.register_handlers(self.dp, self.state)
|
||||
# media.register_handlers(self.dp, self.state, self.bot)
|
||||
# common.register_handlers(self.dp, self.state, self.bot)
|
||||
|
||||
@@ -26,10 +26,10 @@ class TelegramBot:
|
||||
self.addons.load("id")
|
||||
self.addons.load("send_message")
|
||||
self.addons.load("poll")
|
||||
# self.addons.load("hello")
|
||||
self.addons.load("draw")
|
||||
self.addons.load("hello")
|
||||
# self.addons.load("draw")
|
||||
self.addons.load("gpt")
|
||||
# self.addons.load("rule34")
|
||||
self.addons.load("rule34")
|
||||
# self.addons.load("todo")
|
||||
self.addons.load("miniapps")
|
||||
self.addons.load("x_days_to")
|
||||
|
||||
@@ -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
|
||||
aiofiles==24.1.0
|
||||
aiogram==3.22.0
|
||||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.12.15
|
||||
aiosignal==1.4.0
|
||||
annotated-types==0.7.0
|
||||
anyio==4.11.0
|
||||
attrs==25.3.0
|
||||
beautifulsoup4==4.14.2
|
||||
bs4==0.0.2
|
||||
certifi==2025.8.3
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.3
|
||||
click==8.3.0
|
||||
cryptography==46.0.2
|
||||
dataclasses-json==0.6.7
|
||||
deepgram-sdk==3.11.0
|
||||
deprecation==2.1.0
|
||||
distro==1.9.0
|
||||
dotenv==0.9.9
|
||||
dropbox==12.0.2
|
||||
filelock==3.20.0
|
||||
frozenlist==1.7.0
|
||||
fsspec==2025.9.0
|
||||
greenlet==3.2.4
|
||||
h11==0.16.0
|
||||
hf-xet==1.1.10
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
huggingface-hub==0.35.3
|
||||
idna==3.10
|
||||
Jinja2==3.1.6
|
||||
jiter==0.11.1
|
||||
joblib==1.5.2
|
||||
magic-filter==1.0.12
|
||||
MarkupSafe==3.0.3
|
||||
marshmallow==3.26.1
|
||||
mpmath==1.3.0
|
||||
msal==1.34.0
|
||||
multidict==6.6.4
|
||||
mutagen==1.47.0
|
||||
mypy_extensions==1.1.0
|
||||
networkx==3.5
|
||||
numpy==2.3.4
|
||||
openai==2.5.0
|
||||
packaging==25.0
|
||||
playwright==1.55.0
|
||||
ply==3.11
|
||||
propcache==0.3.2
|
||||
pycparser==2.23
|
||||
pydantic==2.11.10
|
||||
pydantic_core==2.33.2
|
||||
pyee==13.0.0
|
||||
PyJWT==2.10.1
|
||||
python-dotenv==1.1.1
|
||||
PyYAML==6.0.3
|
||||
regex==2025.9.18
|
||||
requests==2.32.5
|
||||
ruff==0.14.0
|
||||
sacremoses==0.1.1
|
||||
safetensors==0.6.2
|
||||
sentencepiece==0.2.1
|
||||
setuptools==80.9.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
soupsieve==2.8
|
||||
stone==3.3.1
|
||||
sympy==1.14.0
|
||||
tokenizers==0.22.1
|
||||
torch==2.9.0
|
||||
tqdm==4.67.1
|
||||
transformers==4.57.1
|
||||
typing-inspect==0.9.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
websockets==15.0.1
|
||||
yarl==1.20.1
|
||||
aenum==3.1.16
|
||||
aiofiles==24.1.0
|
||||
aiogram==3.22.0
|
||||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.12.15
|
||||
aiosignal==1.4.0
|
||||
aiosqlite==0.21.0
|
||||
annotated-types==0.7.0
|
||||
anyio==4.11.0
|
||||
attrs==25.3.0
|
||||
beautifulsoup4==4.14.2
|
||||
bs4==0.0.2
|
||||
certifi==2025.8.3
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.3
|
||||
click==8.3.0
|
||||
cryptography==46.0.2
|
||||
dataclasses-json==0.6.7
|
||||
deepgram-sdk==3.11.0
|
||||
deprecation==2.1.0
|
||||
distro==1.9.0
|
||||
dotenv==0.9.9
|
||||
dropbox==12.0.2
|
||||
filelock==3.20.0
|
||||
frozenlist==1.7.0
|
||||
fsspec==2025.9.0
|
||||
greenlet==3.2.4
|
||||
h11==0.16.0
|
||||
hf-xet==1.1.10
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
huggingface-hub==0.35.3
|
||||
idna==3.10
|
||||
Jinja2==3.1.6
|
||||
jiter==0.11.1
|
||||
joblib==1.5.2
|
||||
lxml==6.0.2
|
||||
magic-filter==1.0.12
|
||||
MarkupSafe==3.0.3
|
||||
marshmallow==3.26.1
|
||||
mpmath==1.3.0
|
||||
msal==1.34.0
|
||||
multidict==6.6.4
|
||||
mutagen==1.47.0
|
||||
mypy_extensions==1.1.0
|
||||
networkx==3.5
|
||||
numpy==2.3.4
|
||||
ollama==0.6.0
|
||||
openai==2.5.0
|
||||
packaging==25.0
|
||||
pillow==12.0.0
|
||||
playwright==1.55.0
|
||||
ply==3.11
|
||||
propcache==0.3.2
|
||||
pycparser==2.23
|
||||
pydantic==2.11.10
|
||||
pydantic_core==2.33.2
|
||||
pyee==13.0.0
|
||||
PyJWT==2.10.1
|
||||
python-dotenv==1.1.1
|
||||
PyYAML==6.0.3
|
||||
regex==2025.9.18
|
||||
requests==2.32.5
|
||||
ruff==0.14.6
|
||||
sacremoses==0.1.1
|
||||
safetensors==0.6.2
|
||||
sentencepiece==0.2.1
|
||||
setuptools==80.9.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
soupsieve==2.8
|
||||
stone==3.3.1
|
||||
sympy==1.14.0
|
||||
tokenizers==0.22.1
|
||||
torch==2.9.0
|
||||
tqdm==4.67.1
|
||||
transformers==4.57.1
|
||||
typing-inspect==0.9.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
websockets==15.0.1
|
||||
yarl==1.20.1
|
||||
yt-dlp==2025.10.22
|
||||
|
||||
Reference in New Issue
Block a user