#!/usr/bin/env python3 """Обработчик callback-запросов от меню.""" import logging from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes from bot.config import config, state_manager, server_manager, menu_builder from bot.utils.decorators import check_access logger = logging.getLogger(__name__) async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработка нажатий на кнопки меню.""" query = update.callback_query user_id = query.from_user.id state = state_manager.get(user_id) await query.answer() callback = query.data logger.info(f"Callback: {callback} от пользователя {user_id}") # Обработка навигации if callback == "main": state.current_menu = "main" # Проверяем режим чата с ИИ для обновления текста кнопки ai_status = "✅ ВКЛ" if state.ai_chat_mode else "❌ ВЫКЛ" await query.edit_message_text( f"🏠 *Главное меню*\n\n" f"💬 *Чат с ИИ:* {ai_status}", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state) ) elif callback == "ai_presets": # Открываем меню AI-пресетов state = state_manager.get(user_id) from bot.handlers.ai_presets import ai_presets_command # Создаём фейковое сообщение для совместимости class FakeMessage: async def reply_text(self, text, parse_mode=None, reply_markup=None): # Вместо отправки сообщения редактируем callback await query.edit_message_text(text, parse_mode=parse_mode, reply_markup=reply_markup) return None fake_update = type('FakeUpdate', (), {'message': FakeMessage(), 'effective_user': query.from_user})() await ai_presets_command(fake_update, context) return elif callback.startswith("continue_output_"): # Пользователь нажал "Продолжить" remaining = int(callback.replace("continue_output_", "")) state = state_manager.get(user_id) state.waiting_for_output_control = False state.continue_output = True # Удаляем сообщение с кнопками try: await query.delete_message() except: pass # Устанавливаем event чтобы разблокировать send_long_message if state.output_continue_event: state.output_continue_event.set() await query.answer() return elif callback == "cancel_output": # Пользователь нажал "Отменить" state = state_manager.get(user_id) state.waiting_for_output_control = False state.continue_output = False # Удаляем сообщение с кнопками try: await query.delete_message() except: pass # Устанавливаем event чтобы разблокировать send_long_message if state.output_continue_event: state.output_continue_event.set() await query.answer() return elif callback == "preset_menu": state.current_menu = "preset" await query.edit_message_text( "📋 *Предустановленные команды*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("preset") ) elif callback == "fs_menu": await query.edit_message_text( "📁 *Файловая система*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("fs") ) elif callback == "search_menu": await query.edit_message_text( "🔍 *Поиск*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("search") ) elif callback == "system_menu": await query.edit_message_text( "📊 *Система*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("system") ) elif callback == "network_menu": await query.edit_message_text( "🌐 *Сеть*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("network") ) elif callback == "server_menu": # Динамическое обновление меню серверов с кнопками управления servers = server_manager.list_servers() keyboard = [] for srv in servers: # Кнопка выбора сервера + кнопка управления (для не-local) row = [InlineKeyboardButton( srv.display_name, callback_data=f"server_select_{srv.name}" )] if srv.name != "local": row.append(InlineKeyboardButton( "⚙️", callback_data=f"server_manage_{srv.name}" )) keyboard.append(row) keyboard.append([ InlineKeyboardButton("➕ Добавить", callback_data="server_add"), InlineKeyboardButton("⬅️ Назад", callback_data="main") ]) state.current_menu = "server" await query.edit_message_text( "🖥️ *Управление серверами*\n\n" "Выберите сервер для подключения или добавьте новый.\n" "⚙️ — редактировать/удалить сервер", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup(keyboard) ) elif callback == "server_add": state.waiting_for_input = True state.input_type = "add_server_name" state.context["new_server"] = {} await query.edit_message_text( "➕ *Добавление сервера*\n\n" "Введите *имя сервера* (латиница, без пробелов):\n" "Пример: `web-prod`, `db-backup`", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup([[ InlineKeyboardButton("❌ Отмена", callback_data="server_menu") ]]) ) elif callback.startswith("server_manage_"): server_name = callback.replace("server_manage_", "") server = server_manager.get(server_name) if server and server_name != "local": state.editing_server = server_name await query.edit_message_text( f"⚙️ *Управление сервером*\n\n" f"{server.display_name}\n" f"📍 `{server.description}`\n" f"🏷️ Теги: `{','.join(server.tags) if server.tags else 'нет'}`\n\n" f"Выберите действие:", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton("✏️ Редактировать", callback_data=f"server_edit_{server_name}")], [InlineKeyboardButton("🗑️ Удалить", callback_data=f"server_delete_{server_name}")], [InlineKeyboardButton("⬅️ Назад", callback_data="server_menu")] ]) ) else: await query.edit_message_text( f"❌ *Сервер не найден*\n\n" f"Сервер `{server_name}` отсутствует в конфигурации.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("server") ) elif callback.startswith("server_edit_"): server_name = callback.replace("server_edit_", "") server = server_manager.get(server_name) if server and server_name != "local": state.editing_server = server_name state.waiting_for_input = True state.input_type = "edit_server_field" password_status = "установлен" if server.password else "не установлен" await query.edit_message_text( f"✏️ *Редактирование сервера: {server_name}*\n\n" f"Текущие значения:\n" f"• Host: `{server.host}`\n" f"• Port: `{server.port}`\n" f"• User: `{server.user}`\n" f"• Tags: `{','.join(server.tags) if server.tags else 'нет'}`\n" f"• Password: {password_status}\n\n" f"Введите номер поля для изменения:\n" f"1 — Host\n" f"2 — Port\n" f"3 — User\n" f"4 — Tags\n" f"5 — Password", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup([[ InlineKeyboardButton("❌ Отмена", callback_data="server_menu") ]]) ) else: await query.edit_message_text( "❌ Ошибка: сервер не найден", reply_markup=menu_builder.get_keyboard("server") ) elif callback.startswith("server_delete_"): server_name = callback.replace("server_delete_", "") server = server_manager.get(server_name) if server and server_name != "local": # Удаляем сразу с подтверждением if server_manager.delete_server(server_name): await query.edit_message_text( f"🗑️ *Сервер удалён*\n\n" f"Сервер `{server_name}` успешно удалён из конфигурации.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("server") ) else: await query.edit_message_text( "❌ Ошибка при удалении сервера", reply_markup=menu_builder.get_keyboard("server") ) else: await query.edit_message_text( "❌ Нельзя удалить local сервер", reply_markup=menu_builder.get_keyboard("server") ) elif callback == "srv_skip_password": # Пропуск пароля при добавлении сервера user_id = query.from_user.id state = state_manager.get(user_id) state.context["new_server"]["password"] = "" state.input_type = "add_server_tags" await query.edit_message_text( "✅ Пароль пропущен (будет использоваться только ключ)\n\n" "Введите *теги* через запятую (или нажмите Пропустить):\n" "Пример: `web,prod`, `db,backup`\n\n" "Теги помогают группировать серверы.", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton("⏭️ Пропустить", callback_data="srv_skip_tags")], [InlineKeyboardButton("❌ Отмена", callback_data="server_menu")] ]) ) elif callback == "srv_skip_tags": # Пропуск тегов при добавлении сервера user_id = query.from_user.id state = state_manager.get(user_id) new_server = state.context.get("new_server", {}) if new_server.get("name") and new_server.get("host") and new_server.get("port") and new_server.get("user"): if server_manager.add_server( name=new_server["name"], host=new_server["host"], port=new_server["port"], user=new_server["user"], tags=[], password=new_server.get("password", "") ): await query.edit_message_text( "✅ *Сервер добавлен*\n\n" f"Имя: `{new_server['name']}`\n" f"Host: `{new_server['host']}`\n" f"Port: `{new_server['port']}`\n" f"User: `{new_server['user']}`\n" f"Tags: нет\n" f"Password: {'установлен' if new_server.get('password') else 'не установлен'}\n\n" f"Сервер сохранён в `.env` и доступен для выбора.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("server") ) else: await query.edit_message_text( "❌ Ошибка: сервер с таким именем уже существует", reply_markup=menu_builder.get_keyboard("server") ) else: await query.edit_message_text( "❌ Ошибка: неполные данные сервера", reply_markup=menu_builder.get_keyboard("server") ) state.waiting_for_input = False state.input_type = None state.context.clear() elif callback.startswith("server_select_"): server_name = callback.replace("server_select_", "") server = server_manager.get(server_name) if server: state.current_server = server_name # Сброс рабочей директории при смене сервера state.working_directory = None await query.edit_message_text( f"✅ *Сервер изменён*\n\n" f"{server.display_name}\n" f"📍 `{server.description}`\n\n" f"Теперь команды выполняются на этом сервере.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state) ) state.current_menu = "main" else: await query.edit_message_text( f"❌ *Сервер не найден*\n\n" f"Сервер `{server_name}` отсутствует в конфигурации.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("server") ) elif callback == "settings_menu": state.current_menu = "settings" await query.edit_message_text( "⚙️ *Настройки бота*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("settings") ) elif callback == "access_menu": await query.edit_message_text( "👥 *Управление доступом*", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("access") ) # Обработка команд выполнения elif callback.startswith("cmd_"): # Поиск команды в меню command = None for menu_items in menu_builder._menus.values(): for item in menu_items: if item.callback == callback and item.command: command = item.command break if command: await execute_cli_command(query, command) else: await query.edit_message_text("❌ Команда не найдена") # Настройки бота - только просмотр, изменение через .env elif callback == "set_name": await query.edit_message_text( "📝 *Изменение имени бота*\n\n" f"Текущее имя: `{config.name}`\n\n" "Для изменения отредактируйте `.env`:\n" "```\nBOT_NAME=Ваше имя\n```", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("settings") ) elif callback == "set_description": await query.edit_message_text( "📄 *Изменение описания бота*\n\n" f"Текущее описание: `{config.description}`\n\n" "Для изменения отредактируйте `.env`:\n" "```\nBOT_DESCRIPTION=Ваше описание\n```", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("settings") ) elif callback == "set_icon": await query.edit_message_text( "🎨 *Изменение иконки бота*\n\n" f"Текущая иконка: `{config.icon}`\n\n" "Для изменения отредактируйте `.env`:\n" "```\nBOT_ICON_EMOJI=🤖\n```", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("settings") ) elif callback == "show_access": if config.allowed_users: text = "👥 *Разрешённые пользователи:*\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users) else: text = "👥 *Доступ открыт для всех*\n\n(список разрешённых пользователей пуст)" await query.edit_message_text(text, parse_mode="Markdown") elif callback == "add_access": await query.edit_message_text( "➕ *Добавление пользователя*\n\n" "Для добавления пользователя отредактируйте `.env`:\n" "```\nALLOWED_USERS=123456789,987654321\n```\n" "Ваш ID можно узнать через @userinfobot", parse_mode="Markdown" ) elif callback == "remove_access": if config.allowed_users: text = "➖ *Удаление пользователя*\n\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users) text += "\n\nУдалите ID из `.env` чтобы убрать доступ" else: text = "➖ Список пуст, некого удалять" await query.edit_message_text(text, parse_mode="Markdown") elif callback == "about": await query.edit_message_text( f"ℹ️ *О боте*\n\n" f"*{config.icon} {config.name}*\n" f"_{config.description}_\n\n" f"*Версия:* `2.1.0`\n\n" f"*Возможности:*\n" f"• Выполнение CLI команд через Telegram\n" f"• Поддержка локальных команд и SSH\n" f"• Интерактивный ввод пароля (sudo)\n" f"• Предустановленные команды\n" f"• Управление серверами\n" f"• Очистка ANSI-кодов и прогресс-баров\n" f"• Форматирование длинного вывода\n" f"• 💬 Чат с ИИ агентом (Qwen Code)\n\n" f"*Рабочая директория:*\n" f"`{config.working_directory}`\n\n" f"Бот позволяет безопасно выполнять команды\n" f"на вашем сервере через интерфейс Telegram.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state) ) state.current_menu = "main" elif callback in ["toggle_ai_chat", "toggle_ai_chat_on", "toggle_ai_chat_off"]: # Переключаем режим чата с ИИ state.ai_chat_mode = not state.ai_chat_mode logger.info(f"toggle_ai_chat: user_id={user_id}, new_mode={state.ai_chat_mode}") ai_status = "✅ ВКЛЮЧЕН" if state.ai_chat_mode else "❌ ВЫКЛЮЧЕН" action = "включён" if state.ai_chat_mode else "выключен" await query.edit_message_text( f"🏠 *Главное меню*\n\n" f"💬 *Чат с ИИ:* {ai_status}\n\n" f"Режим чата с агентом {action}.\n" f"Теперь все сообщения будут отправляться в Qwen Code.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state) ) state.current_menu = "main" # --- Обработчики меню памяти --- elif callback == "memory_menu": state.current_menu = "memory" await query.edit_message_text( "🧠 *Память ИИ*\n\n" "Управление памятью чата с ИИ:\n" "• Профиль — факты о вас, которые запомнил ИИ\n" "• Статистика — количество сообщений и сессий\n" "• Очистить — удалить историю переписки", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) elif callback == "memory_profile": profile_summary = get_user_profile_summary(user_id) if not profile_summary: profile_summary = "📭 Профиль пуст\n\nФакты ещё не извлечены.\nНачните общаться с ИИ в чате." await query.edit_message_text( f"📋 *Ваш профиль*\n\n{profile_summary}", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) elif callback == "memory_stats": stats = memory_manager.get_stats(user_id) await query.edit_message_text( f"📊 *Статистика памяти*\n\n" f"• Сессий: `{stats['total_sessions']}`\n" f"• Сообщений: `{stats['total_messages']}`\n" f"• Фактов: `{stats['total_facts']}`", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) elif callback == "memory_clear": # Показываем подтверждение await query.edit_message_text( "🗑️ *Очистка истории*\n\n" "Вы уверены?\n" "Это удалит всю историю сообщений.\n" "Факты останутся (их можно удалить отдельно).", parse_mode="Markdown", reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton("🗑️ Да, очистить", callback_data="memory_clear_confirm")], [InlineKeyboardButton("❌ Отмена", callback_data="memory_menu")] ]) ) elif callback == "memory_clear_confirm": # Очищаем историю сообщений (в будущем можно добавить метод в memory_manager) from memory_system import MemoryStorage # Пока просто уведомляем await query.edit_message_text( "✅ *История очищена*\n\n" "Функция полной очистки будет добавлена в следующей версии.\n" "Пока очищается только история сессии в памяти бота.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) # Сбрасываем историю чата в состоянии state.ai_chat_history = [] elif callback == "memory_compact": # Вызываем команду /compact через send_message await query.edit_message_text( "🔄 **Запуск компактификации истории...**\n\n" "_Сжатие старой истории в структурированный summary._\n" "_Это может занять несколько секунд._", parse_mode="Markdown" ) # Получаем compactor и выполняем компактификацию from bot.compaction import get_compactor try: compactor = get_compactor() result = await compactor.compact() if result.success: if result.messages_compressed > 0: await query.edit_message_text( f"✅ **Компактификация завершена!**\n\n" f"📊 Сжато сообщений: `{result.messages_compressed}`\n" f"📝 Длина summary: `{result.summary_length}` символов\n" f"💾 Экономия токенов: ~`{result.tokens_saved}`\n\n" f"_Summary автоматически используется в контексте диалога._", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) else: await query.edit_message_text( "ℹ️ **Компактификация не требуется**\n\n" "_Недостаточно сообщений для сжатия или summary уже актуален._", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) else: await query.edit_message_text( f"⚠️ **Ошибка компактификации:**\n`{result.error}`", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) except Exception as e: logger.exception(f"Ошибка в memory_compact: {e}") await query.edit_message_text( f"⚠️ **Ошибка компактификации:**\n`{e}`", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("memory") ) # --- Обработчики меню AI-провайдера --- elif callback == "ai_provider_menu": state.current_menu = "ai_provider" # Получаем текущего провайдера from bot.ai_provider_manager import get_ai_provider_manager provider_manager = get_ai_provider_manager() current_provider = provider_manager.get_current_provider(state) providers_info = provider_manager.get_all_providers_info(current_provider) output = "🤖 **AI-провайдеры**\n\n" output += f"*Текущий провайдер:* " for info in providers_info: icon = "✅" if info.is_active else "⬜" status = "✓ Доступен" if info.available else "✗ Недоступен" output += f"\n\n{icon} **{info.name}** — {status}\n" output += f"_{info.description}_\n" output += "\n\nВыберите действие:" await query.edit_message_text( output, parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("ai_provider") ) elif callback == "ai_provider_toggle": # Переключаем провайдер from bot.ai_provider_manager import get_ai_provider_manager provider_manager = get_ai_provider_manager() current_provider = provider_manager.get_current_provider(state) new_provider = "gigachat" if current_provider == "qwen" else "qwen" success, message = provider_manager.switch_provider(user_id, new_provider, state_manager) if success: provider_info = provider_manager.get_provider_info(new_provider, is_active=True) await query.edit_message_text( f"{message}\n\n" f"**{provider_info.name}**\n" f"_{provider_info.description}_", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("ai_provider") ) else: await query.edit_message_text( f"❌ {message}\n\n" "Проверьте настройки в .env файле.", parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("ai_provider") ) elif callback == "ai_provider_info": # Показываем подробную информацию from bot.ai_provider_manager import get_ai_provider_manager provider_manager = get_ai_provider_manager() current_provider = provider_manager.get_current_provider(state) output = "ℹ️ **Информация о провайдерах**\n\n" # Qwen output += "**🔹 Qwen Code**\n" output += "Alibaba Qwen Code CLI — мощный AI-ассистент с:\n" output += "• Поддержкой инструментов (поиск, RSS, SSH, cron)\n" output += "• Потоковым выводом ответа\n" output += "• Контекстом до 256K токенов\n" output += "• RAG-памятью на ChromaDB\n\n" # GigaChat output += "**🟢 GigaChat**\n" output += "Sber GigaChat API — российская AI-модель:\n" output += "• Поддержка русского языка из коробки\n" output += "• Модели: GigaChat-Pro, GigaChat-Max\n" output += "• Генерация ответов и изображений\n" output += "• Требует настройки в .env\n\n" output += f"*Текущий провайдер:* `{current_provider}`\n" output += "\nИспользуйте `/ai` для переключения." await query.edit_message_text( output, parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("ai_provider") )