From d3f6206113ad3618758bd97b3fa9b4de1e9f5430 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: Thu, 30 Jul 2026 23:13:03 +0700 Subject: [PATCH] fix: wrap entire SD handler in try-except to prevent crashes when SD is unreachable --- service_desk/handlers.py | 223 ++++++++++++++++++++------------------- 1 file changed, 115 insertions(+), 108 deletions(-) diff --git a/service_desk/handlers.py b/service_desk/handlers.py index fb906fe..84506c8 100644 --- a/service_desk/handlers.py +++ b/service_desk/handlers.py @@ -286,114 +286,121 @@ async def _wait_for_attachments_and_create(user_id, session): async def sd_module_handler(msg: Message): try: user_id = msg.from_user.id - sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}") - if states.get_state(user_id) != "SD_MODE": - return - - msg_text = _extract_text_from_content(msg.content) - is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" - inline_attachments = _extract_attachments_from_content(msg.content, is_attachment) - - sd_workflow_logger.info(f"🚀 [SD Debug] msg.type={msg.type}, content_type={type(msg.content).__name__}, msg_text='{msg_text}', is_attachment={is_attachment}, inline_attachments={len(inline_attachments)}") - - msg.handled = True - login = user_id.split("@")[0] if "@" in user_id else user_id - cmd = msg_text.lower() - - # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ - for raw_num, emoji_num in EMOJI_DIGITS.items(): - if cmd == emoji_num: - cmd = raw_num - break - - # --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) --- - if cmd in ["0", "9", "/start", "меню"]: - log_menu_stats(user_id, "Service Desk", "Выход в главное меню") - states.clear_state(user_id) - sd_sessions.pop(user_id, None) - await msg.answer(MENU_TEXT, parse_mode="html") - return - - if user_id not in sd_sessions: - sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": []} - session = sd_sessions[user_id] - - # --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА --- - if session["step"] == "need_text": - if not msg_text.strip() and not inline_attachments: - await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html") - return - - # Если только вложение без текста — сохраняем и ждем текст - if not msg_text.strip() and inline_attachments: - session["files_queue"].extend(inline_attachments) - sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text") - await msg.answer(SD_TEXT_REQUIRED, parse_mode="html") - return - - # Если пришел текст (с вложениями или без) - if inline_attachments: - session["files_queue"].extend(inline_attachments) - sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue") - - # Настраиваем параметры сессии для ожидания - session["step"] = "waiting_for_attachments" - session["msg_text"] = msg_text - session["msg"] = msg - session["user_id"] = user_id - session["login"] = login - - # Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота - await msg.answer(SD_CREATING, parse_mode="html") - - # Запускаем фоновый таймер с механизмом сброса (Debounce) - if "timer_task" in session and not session["timer_task"].done(): - session["timer_task"].cancel() - session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) - sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...") - return - - # --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) --- - if session["step"] == "waiting_for_attachments": - if inline_attachments: - session["files_queue"].extend(inline_attachments) - sd_workflow_logger.info(f"📎 [SD] Received {len(inline_attachments)} more files. Total queue: {len(session['files_queue'])}") - if msg_text.strip(): - session["msg_text"] = session["msg_text"] + " " + msg_text.strip() - sd_workflow_logger.info(f"📝 [SD] Additional text appended") - - # Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед) - if "timer_task" in session and not session["timer_task"].done(): - session["timer_task"].cancel() - session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) - sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s") - return - - # --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") --- - if session["step"] == "creating_ticket": - if inline_attachments: - if "post_create_queue" not in session: - session["post_create_queue"] = [] - session["post_create_queue"].extend(inline_attachments) - sd_workflow_logger.info(f"📥 [SD Hole-Fix] Captured {len(inline_attachments)} file(s) DURING ticket creation API call.") - return - - # --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ --- - if session["step"] == "ticket_created": - if inline_attachments: - ticket_id = session["ticket_id"] - for att in inline_attachments: - await msg.answer(sd_uploading_file(ticket_id), parse_mode="html") - ok = await process_and_upload_file(att['file_id'], att['file_name'], ticket_id) - sd_workflow_logger.info(f"📎 [Late File Upload] Name: {att['file_name']} -> Success: {ok}") - if ok: - log_menu_stats(user_id, "Service Desk", f"Добавление файла к заявке #{ticket_id}") - await msg.answer(sd_file_attached(), parse_mode="html") - else: - await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") - elif msg_text.strip(): - await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") - return + sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}") + if states.get_state(user_id) != "SD_MODE": + return + + msg_text = _extract_text_from_content(msg.content) + is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" + inline_attachments = _extract_attachments_from_content(msg.content, is_attachment) + + sd_workflow_logger.info(f"🚀 [SD Debug] msg.type={msg.type}, content_type={type(msg.content).__name__}, msg_text='{msg_text}', is_attachment={is_attachment}, inline_attachments={len(inline_attachments)}") + + msg.handled = True + login = user_id.split("@")[0] if "@" in user_id else user_id + cmd = msg_text.lower() + + # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ + for raw_num, emoji_num in EMOJI_DIGITS.items(): + if cmd == emoji_num: + cmd = raw_num + break + + # --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) --- + if cmd in ["0", "9", "/start", "меню"]: + log_menu_stats(user_id, "Service Desk", "Выход в главное меню") + states.clear_state(user_id) + sd_sessions.pop(user_id, None) + await msg.answer(MENU_TEXT, parse_mode="html") + return + + if user_id not in sd_sessions: + sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": []} + session = sd_sessions[user_id] + + # --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА --- + if session["step"] == "need_text": + if not msg_text.strip() and not inline_attachments: + await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html") + return + + # Если только вложение без текста — сохраняем и ждем текст + if not msg_text.strip() and inline_attachments: + session["files_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text") + await msg.answer(SD_TEXT_REQUIRED, parse_mode="html") + return + + # Если пришел текст (с вложениями или без) + if inline_attachments: + session["files_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue") + + # Настраиваем параметры сессии для ожидания + session["step"] = "waiting_for_attachments" + session["msg_text"] = msg_text + session["msg"] = msg + session["user_id"] = user_id + session["login"] = login + + # Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота + await msg.answer(SD_CREATING, parse_mode="html") + + # Запускаем фоновый таймер с механизмом сброса (Debounce) + if "timer_task" in session and not session["timer_task"].done(): + session["timer_task"].cancel() + session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) + sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...") + return + + # --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) --- + if session["step"] == "waiting_for_attachments": + if inline_attachments: + session["files_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📎 [SD] Received {len(inline_attachments)} more files. Total queue: {len(session['files_queue'])}") + if msg_text.strip(): + session["msg_text"] = session["msg_text"] + " " + msg_text.strip() + sd_workflow_logger.info(f"📝 [SD] Additional text appended") + + # Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед) + if "timer_task" in session and not session["timer_task"].done(): + session["timer_task"].cancel() + session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) + sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s") + return + + # --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") --- + if session["step"] == "creating_ticket": + if inline_attachments: + if "post_create_queue" not in session: + session["post_create_queue"] = [] + session["post_create_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📥 [SD Hole-Fix] Captured {len(inline_attachments)} file(s) DURING ticket creation API call.") + return + + # --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ --- + if session["step"] == "ticket_created": + if inline_attachments: + ticket_id = session["ticket_id"] + for att in inline_attachments: + await msg.answer(sd_uploading_file(ticket_id), parse_mode="html") + ok = await process_and_upload_file(att['file_id'], att['file_name'], ticket_id) + sd_workflow_logger.info(f"📎 [Late File Upload] Name: {att['file_name']} -> Success: {ok}") + if ok: + log_menu_stats(user_id, "Service Desk", f"Добавление файла к заявке #{ticket_id}") + await msg.answer(sd_file_attached(), parse_mode="html") + else: + await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") + elif msg_text.strip(): + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + return + except Exception as e: + logger.exception(f"❌ [SD Fatal Error] user={getattr(msg.from_user, 'id', 'unknown')}: {e}") + sd_workflow_logger.error(f"❌ [SD Fatal] {e}") + try: + await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") + except Exception: + pass