From eedd99e29a411ec7ff567c45ee8ee935f5580c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=9A=D1=80=D0=B8=D0=B2?= =?UTF-8?q?=D0=BE=D1=87=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Mon, 20 Jul 2026 15:44:29 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20instruct?= =?UTF-8?q?=20=E2=80=94=20=D0=98=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20TrueConf=20=D0=B8=20=D0=BF=D0=BE=D1=87=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- instruct/handlers.py | 92 +++++++++++++++++++++++++ main.py | 40 +---------- utils/menu.py | 156 +++++++++++++++++++++---------------------- utils/texts.py | 30 +++++++++ 4 files changed, 202 insertions(+), 116 deletions(-) create mode 100644 instruct/handlers.py diff --git a/instruct/handlers.py b/instruct/handlers.py new file mode 100644 index 0000000..9fe6c57 --- /dev/null +++ b/instruct/handlers.py @@ -0,0 +1,92 @@ +""" +Module: Instruct (Инструкции TrueConf и почта) +""" +import logging +import os +from logging.handlers import RotatingFileHandler +from trueconf import Router, Message +from utils.states import set_state, get_state, clear_state +from utils.texts import ( + instruct_main_menu_text, + instruct_tc_text, + instruct_email_text, +) + +# ======================================== +# SETUP +# ======================================== +log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') +os.makedirs(log_dir, exist_ok=True) +instruct_logger = logging.getLogger('instruct') +instruct_logger.setLevel(logging.INFO) +if not instruct_logger.handlers: + handler = RotatingFileHandler(os.path.join(log_dir, 'instruct.log'), maxBytes=10*1024*1024, backupCount=5) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + instruct_logger.addHandler(handler) + +# ======================================== +# БИЗНЕС-ЛОГИКА +# ======================================== + +instruct_sessions = {} + +# ======================================== +# ОБРАБОТЧИКИ +# ======================================== + +@router.message() +async def instruct_main_handler(msg: Message): + """Обработчик главного меню модуля Instruct.""" + user_id = msg.from_user.id + + if get_state(user_id) != "INSTRUCT_MODE": + return + + # Очистка состояния + clear_state(user_id) + user_menu_shown = getattr(msg, "handled", False) + + if user_menu_shown: + return + + # Инициализация сессии + if user_id not in instruct_sessions: + instruct_sessions[user_id] = {"step": "choose_option", "selected": None} + + await msg.answer(instruct_main_menu_text(), parse_mode="html") + instruct_logger.info(f"Instruct main menu shown for user {user_id}") + +@router.message() +async def instruct_tc_handler(msg: Message): + """Обработчик выбора: Приложение TrueConf.""" + user_id = msg.from_user.id + + if get_state(user_id) != "INSTRUCT_MODE": + return + + # Очистка состояния + clear_state(user_id) + + # Убираем обработанность, чтобы сообщение не дублировалось + setattr(msg, "handled", True) + + await msg.answer(instruct_tc_text(), parse_mode="html") + instruct_logger.info(f"User {user_id} selected TrueConf instructions") + +@router.message() +async def instruct_email_handler(msg: Message): + """Обработчик выбора: Почта на мобильном устройстве.""" + user_id = msg.from_user.id + + if get_state(user_id) != "INSTRUCT_MODE": + return + + # Очистка состояния + clear_state(user_id) + + # Убираем обработанность, чтобы сообщение не дублировалось + setattr(msg, "handled", True) + + await msg.answer(instruct_email_text(), parse_mode="html") + instruct_logger.info(f"User {user_id} selected email instructions") diff --git a/main.py b/main.py index 38ba8ff..a7b68e1 100644 --- a/main.py +++ b/main.py @@ -17,27 +17,20 @@ logging.basicConfig( ) logging.getLogger('trueconf.client.chatbot').propagate = False logger = logging.getLogger(__name__) - # Добавляем путь к проекту в sys.path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - from trueconf import Bot, Dispatcher, Router, Message from config.config import * from utils.states import set_state, clear_state, get_state from utils import states - # 🎯 ИСПРАВЛЕНО: Импортируем оба варианта меню (короткий и приветственный) from utils.menu import MENU_TEXT, WELCOME_TEXT, MENU_MAP - # ========================================================= # 🔄 МОДУЛЬ АВТОМАТИЧЕСКОГО КОНТРОЛЯ СЕССИЙ И ТАЙМАУТОВ -# ========================================================= USER_LAST_ACTIVITY = {} USER_MENU_SHOWN = {} DEFAULT_TIMEOUT = 1800 - _orig_get_state = states.get_state - def patched_get_state(user_id): current_time = time.time() last_activity = USER_LAST_ACTIVITY.get(user_id) @@ -48,46 +41,30 @@ def patched_get_state(user_id): logger.info(f"⏳ Сессия пользователя {user_id} сброшена по таймауту бездействия ({session_timeout} сек).") USER_LAST_ACTIVITY[user_id] = current_time return _orig_get_state(user_id) - states.get_state = patched_get_state get_state = patched_get_state - -# ========================================================= # 🔌 КОРПОРАТИВНЫЕ ШАБЛОНЫ ТЕКСТОВ КЛЕВЕР -# ========================================================= from utils.texts import UNKNOWN_MAIN_CMD_TEXT from utils.stats_logger import log_menu_stats - -# ========================================================= # ФУНКЦИЯ МОНИТОРИНГА ОПЕРАТИВНОЙ ПАМЯТИ -# ========================================================= def log_ram_usage(stage_name: str): process = psutil.Process(os.getpid()) ram_mb = process.memory_info().rss / 1024 / 1024 logger.info(f"📊 [RAM MONITOR] {stage_name}: {ram_mb:.2f} MB") - log_ram_usage("Старт скрипта (базовый вес)") - from photo_bot.handlers import router as photo_router, PHOTO_MAIN_MENU_TEXT log_ram_usage("После импорта photo_bot") - from service_desk.handlers import router as sd_router, SD_MAIN_MENU_TEXT log_ram_usage("После импорта service_desk") - from transcription_bot.handlers import router as transcription_router, TRANSCRIPTION_MAIN_MENU_TEXT log_ram_usage("После импорта transcription_router (Виспер)") - from search_bot.handlers import router as search_router, SEARCH_MAIN_MENU_TEXT log_ram_usage("После импорта search_router") - from lk.handlers import router as lk_router +from instruct.handlers import router as instruct_router log_ram_usage("После импорта lk_router (Личный кабинет)") - -# ========================================================= # ГЛАВНЫЙ РОУТЕР -# ========================================================= main_router = Router() - @main_router.message() async def main_menu(msg: Message): if ENABLE_WHITELIST and msg.from_user.id not in ALLOWED_USERS: @@ -95,15 +72,12 @@ async def main_menu(msg: Message): return user_id = msg.from_user.id if getattr(msg, "handled", False): - return if get_state(user_id) is not None: - return is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" if is_attachment: await msg.answer("⚠️ Сначала выберите режим работы из меню.", parse_mode="html") await msg.answer(WELCOME_TEXT, parse_mode="html") USER_MENU_SHOWN[user_id] = True - return menu_shown = USER_MENU_SHOWN.get(user_id, False) if msg.text: cmd = msg.text.strip().lower() @@ -115,40 +89,30 @@ async def main_menu(msg: Message): await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html") return elif action == "PHOTO": - USER_MENU_SHOWN[user_id] = True set_state(user_id, "PHOTO_MODE") log_menu_stats(user_id, "Photo Bot", "Вход") await msg.answer(PHOTO_MAIN_MENU_TEXT, parse_mode="html") - return elif action == "TRANSCRIPTION": - USER_MENU_SHOWN[user_id] = True set_state(user_id, "TRANSCRIPTION_MODE") log_menu_stats(user_id, "Speech-to-Text", "Вход") await msg.answer(TRANSCRIPTION_MAIN_MENU_TEXT, parse_mode="html") - return elif action == "SEARCH": - USER_MENU_SHOWN[user_id] = True set_state(user_id, "SEARCH_MODE") log_menu_stats(user_id, "Search Bot", "Вход") await msg.answer(SEARCH_MAIN_MENU_TEXT, parse_mode="html") - return elif action == "LK": - USER_MENU_SHOWN[user_id] = True set_state(user_id, "LK_MODE") log_menu_stats(user_id, "Личный кабинет", "Вход") from lk.handlers import send_dynamic_lk_main_menu await send_dynamic_lk_main_menu(msg, user_id) - return else: if not menu_shown: USER_MENU_SHOWN[user_id] = True await msg.answer(WELCOME_TEXT, parse_mode="html") return await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") - return USER_MENU_SHOWN[user_id] = True await msg.answer(WELCOME_TEXT, parse_mode="html") - async def main(): logger.info("Инициализация Dispatcher и подключение роутеров...") dp = Dispatcher() @@ -157,6 +121,7 @@ async def main(): dp.include_router(transcription_router) dp.include_router(search_router) dp.include_router(lk_router) + dp.include_router(instruct_router) dp.include_router(main_router) bot = Bot.from_credentials(server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False) states.tc_bot = bot @@ -172,7 +137,6 @@ async def main(): asyncio.create_task(send_startup_notification(bot, target_chat)) logger.info("Подключение к серверу TrueConf...") await bot.run() - if __name__ == "__main__": try: main_router.message() diff --git a/utils/menu.py b/utils/menu.py index 5109646..ce9ed0b 100644 --- a/utils/menu.py +++ b/utils/menu.py @@ -1,79 +1,79 @@ -# /opt/trueconf_bot/utils/menu.py -import config.config as config - -MENU_MAP = {} -MENU_TEXT = "" -WELCOME_TEXT = "" # 💡 Добавили шаблон для развернутого приветствия - -def build_dynamic_menu(): - global MENU_MAP, MENU_TEXT, WELCOME_TEXT - MENU_MAP.clear() - - # Старый формат keycap-цифр (как в VK) - emoji_digits = { - 1: "1⃣", - 2: "2⃣", - 3: "3⃣", - 4: "4⃣", - 5: "5⃣", - 6: "6⃣", - 7: "7⃣", - 8: "8⃣", - 9: "9⃣", - 10: "🔟", - } - - dynamic_items = [] - counter = 1 - - # Безопасно проверяем флаги из конфига и наполняем массив пунктов - if getattr(config, 'ENABLE_LK', True): - num_char = emoji_digits.get(counter, str(counter)) - MENU_MAP[str(counter)] = "LK" - MENU_MAP[num_char] = "LK" - dynamic_items.append(f"{num_char} — 👤 Личный кабинет") - counter += 1 - - if getattr(config, 'ENABLE_TRANSCRIPTION', True): - num_char = emoji_digits.get(counter, str(counter)) - MENU_MAP[str(counter)] = "TRANSCRIPTION" - MENU_MAP[num_char] = "TRANSCRIPTION" - dynamic_items.append(f"{num_char} — 🎙 Расшифровка совещаний") - counter += 1 - - if getattr(config, 'ENABLE_SERVICE_DESK', True): - num_char = emoji_digits.get(counter, str(counter)) - MENU_MAP[str(counter)] = "SD" - MENU_MAP[num_char] = "SD" - dynamic_items.append(f"{num_char} — 🎫 Создать заявку в Service Desk") - counter += 1 - - if getattr(config, 'ENABLE_PHOTO_BOT', True): - num_char = emoji_digits.get(counter, str(counter)) - MENU_MAP[str(counter)] = "PHOTO" - MENU_MAP[num_char] = "PHOTO" - dynamic_items.append(f"{num_char} — Изменить фото профиля") - counter += 1 - - if getattr(config, 'ENABLE_SEARCH_BOT', True): - num_char = emoji_digits.get(counter, str(counter)) - MENU_MAP[str(counter)] = "SEARCH" - MENU_MAP[num_char] = "SEARCH" - dynamic_items.append(f"{num_char} — Поиск по регламентам") - counter += 1 - - if counter == 1: - dynamic_items.append("В данный момент бот находится на техническом обслуживании.") - - # 1️⃣ Сценарий Б: Короткий шаблон для возврата из подмодулей - MENU_TEXT = "🍀 Главное меню:\n\n" + "\n".join(dynamic_items) - - # 2️⃣ Сценарий А: Длинный приветственный шаблон для старта/таймаута (с исправленной грамматикой) - WELCOME_TEXT = ( - "🍀 Здравствуйте! Я КЛЕВЕР — ваш персональный цифровой ассистент.\n\n" - "С моей помощью вы можете быстро получить доступ к нижеперечисленным корпоративным сервисам.\n\n" - "👇 Пожалуйста, отправьте мне в чат цифру, соответствующую нужному пункту меню:\n\n" - ) + "\n".join(dynamic_items) - -# Gенерируем меню один раз при импорте файла +# /opt/trueconf_bot/utils/menu.py +import config.config as config + +MENU_MAP = {} +MENU_TEXT = "" +WELCOME_TEXT = "" # 💡 Добавили шаблон для развернутого приветствия + +def build_dynamic_menu(): + global MENU_MAP, MENU_TEXT, WELCOME_TEXT + MENU_MAP.clear() + + # Старый формат keycap-цифр (как в VK) + emoji_digits = { + 1: "1⃣", + 2: "2⃣", + 3: "3⃣", + 4: "4⃣", + 5: "5⃣", + 6: "6⃣", + 7: "7⃣", + 8: "8⃣", + 9: "9⃣", + 10: "🔟", + } + + dynamic_items = [] + counter = 1 + + # Безопасно проверяем флаги из конфига и наполняем массив пунктов + if getattr(config, 'ENABLE_LK', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "LK" + MENU_MAP[num_char] = "LK" + dynamic_items.append(f"{num_char} — 👤 Личный кабинет") + counter += 1 + + if getattr(config, 'ENABLE_TRANSCRIPTION', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "TRANSCRIPTION" + MENU_MAP[num_char] = "TRANSCRIPTION" + dynamic_items.append(f"{num_char} — 🎙 Расшифровка совещаний") + counter += 1 + + if getattr(config, 'ENABLE_SERVICE_DESK', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "SD" + MENU_MAP[num_char] = "SD" + dynamic_items.append(f"{num_char} — 🎫 Создать заявку в Service Desk") + counter += 1 + + if getattr(config, 'ENABLE_PHOTO_BOT', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "PHOTO" + MENU_MAP[num_char] = "PHOTO" + dynamic_items.append(f"{num_char} — Изменить фото профиля") + counter += 1 + + if getattr(config, 'ENABLE_SEARCH_BOT', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "SEARCH" + MENU_MAP[num_char] = "SEARCH" + dynamic_items.append(f"{num_char} — Поиск по регламентам") + counter += 1 + + if counter == 1: + dynamic_items.append("В данный момент бот находится на техническом обслуживании.") + + # 1️⃣ Сценарий Б: Короткий шаблон для возврата из подмодулей + MENU_TEXT = "🍀 Главное меню:\n\n" + "\n".join(dynamic_items) + + # 2️⃣ Сценарий А: Длинный приветственный шаблон для старта/таймаута (с исправленной грамматикой) + WELCOME_TEXT = ( + "🍀 Здравствуйте! Я КЛЕВЕР — ваш персональный цифровой ассистент.\n\n" + "С моей помощью вы можете быстро получить доступ к нижеперечисленным корпоративным сервисам.\n\n" + "👇 Пожалуйста, отправьте мне в чат цифру, соответствующую нужному пункту меню:\n\n" + ) + "\n".join(dynamic_items) + +# Gенерируем меню один раз при импорте файла build_dynamic_menu() \ No newline at end of file diff --git a/utils/texts.py b/utils/texts.py index 311a233..426c185 100644 --- a/utils/texts.py +++ b/utils/texts.py @@ -7,6 +7,11 @@ # --- Main Bot --- # Общее для главного бота +INSTRUCT_MAIN_MENU_TEXT = """ +""" +# Instructions module +""" + UNKNOWN_MAIN_CMD_TEXT = ( "⚠️ Неизвестная команда.\n\n" "Пожалуйста, используйте только цифры, соответствующие нужным пунктам меню, и отправьте выбранную цифру мне в сообщении." @@ -569,3 +574,28 @@ def lk_nav_text(emojis: dict) -> str: def lk_years_menu(current_year: int, emojis: dict) -> str: return lk_years_menu(current_year, emojis) + +def instruct_main_menu_text(emojis: dict) -> str: + return instruct_main_menu_text_impl(emojis) + +def _instruct_main_menu_text_impl(emojis: dict) -> str: + return ( + f"📖 Инструкции TrueConf и почта\n\n" + f"Выберите, по какому ПО нужна помощь:\n\n" + f"{emojis.get('1', '1⃣')} — Приложение TrueConf\n" + f"{emojis.get('2', '2⃣')} — Почта на мобильном устройстве\n" + ) + +def instruct_tc_text() -> str: + return ( + "📖 Инструкция по приложению TrueConf\n\n" + "Для получения помощи по настройке и использованию приложения TrueConf, " + "свяжитесь с администратором ИТ-инфраструктуры." + ) + +def instruct_email_text() -> str: + return ( + "📧 Инструкция по почте на мобильном устройстве\n\n" + "Для настройки почтового приложения на вашем устройстве, " + "свяжитесь с администратором ИТ-инфраструктуры." + )