fix: wrap entire SD handler in try-except to prevent crashes when SD is unreachable

This commit is contained in:
Денис Кривоченко
2026-07-30 23:13:03 +07:00
parent 3d6c99ed24
commit d3f6206113
+115 -108
View File
@@ -286,114 +286,121 @@ async def _wait_for_attachments_and_create(user_id, session):
async def sd_module_handler(msg: Message): async def sd_module_handler(msg: Message):
try: try:
user_id = msg.from_user.id user_id = msg.from_user.id
sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}") sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}")
if states.get_state(user_id) != "SD_MODE": if states.get_state(user_id) != "SD_MODE":
return return
msg_text = _extract_text_from_content(msg.content) msg_text = _extract_text_from_content(msg.content)
is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT"
inline_attachments = _extract_attachments_from_content(msg.content, is_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)}") 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 msg.handled = True
login = user_id.split("@")[0] if "@" in user_id else user_id login = user_id.split("@")[0] if "@" in user_id else user_id
cmd = msg_text.lower() cmd = msg_text.lower()
# 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ
for raw_num, emoji_num in EMOJI_DIGITS.items(): for raw_num, emoji_num in EMOJI_DIGITS.items():
if cmd == emoji_num: if cmd == emoji_num:
cmd = raw_num cmd = raw_num
break break
# --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) --- # --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) ---
if cmd in ["0", "9", "/start", "меню"]: if cmd in ["0", "9", "/start", "меню"]:
log_menu_stats(user_id, "Service Desk", "Выход в главное меню") log_menu_stats(user_id, "Service Desk", "Выход в главное меню")
states.clear_state(user_id) states.clear_state(user_id)
sd_sessions.pop(user_id, None) sd_sessions.pop(user_id, None)
await msg.answer(MENU_TEXT, parse_mode="html") await msg.answer(MENU_TEXT, parse_mode="html")
return return
if user_id not in sd_sessions: if user_id not in sd_sessions:
sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": []} sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": []}
session = sd_sessions[user_id] session = sd_sessions[user_id]
# --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА --- # --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА ---
if session["step"] == "need_text": if session["step"] == "need_text":
if not msg_text.strip() and not inline_attachments: if not msg_text.strip() and not inline_attachments:
await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html") await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html")
return return
# Если только вложение без текста — сохраняем и ждем текст # Если только вложение без текста — сохраняем и ждем текст
if not msg_text.strip() and inline_attachments: if not msg_text.strip() and inline_attachments:
session["files_queue"].extend(inline_attachments) session["files_queue"].extend(inline_attachments)
sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text") sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text")
await msg.answer(SD_TEXT_REQUIRED, parse_mode="html") await msg.answer(SD_TEXT_REQUIRED, parse_mode="html")
return return
# Если пришел текст (с вложениями или без) # Если пришел текст (с вложениями или без)
if inline_attachments: if inline_attachments:
session["files_queue"].extend(inline_attachments) session["files_queue"].extend(inline_attachments)
sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue") sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue")
# Настраиваем параметры сессии для ожидания # Настраиваем параметры сессии для ожидания
session["step"] = "waiting_for_attachments" session["step"] = "waiting_for_attachments"
session["msg_text"] = msg_text session["msg_text"] = msg_text
session["msg"] = msg session["msg"] = msg
session["user_id"] = user_id session["user_id"] = user_id
session["login"] = login session["login"] = login
# Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота # Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота
await msg.answer(SD_CREATING, parse_mode="html") await msg.answer(SD_CREATING, parse_mode="html")
# Запускаем фоновый таймер с механизмом сброса (Debounce) # Запускаем фоновый таймер с механизмом сброса (Debounce)
if "timer_task" in session and not session["timer_task"].done(): if "timer_task" in session and not session["timer_task"].done():
session["timer_task"].cancel() session["timer_task"].cancel()
session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) 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...") sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...")
return return
# --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) --- # --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) ---
if session["step"] == "waiting_for_attachments": if session["step"] == "waiting_for_attachments":
if inline_attachments: if inline_attachments:
session["files_queue"].extend(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'])}") sd_workflow_logger.info(f"📎 [SD] Received {len(inline_attachments)} more files. Total queue: {len(session['files_queue'])}")
if msg_text.strip(): if msg_text.strip():
session["msg_text"] = session["msg_text"] + " " + msg_text.strip() session["msg_text"] = session["msg_text"] + " " + msg_text.strip()
sd_workflow_logger.info(f"📝 [SD] Additional text appended") sd_workflow_logger.info(f"📝 [SD] Additional text appended")
# Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед) # Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед)
if "timer_task" in session and not session["timer_task"].done(): if "timer_task" in session and not session["timer_task"].done():
session["timer_task"].cancel() session["timer_task"].cancel()
session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) 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") sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s")
return return
# --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") --- # --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") ---
if session["step"] == "creating_ticket": if session["step"] == "creating_ticket":
if inline_attachments: if inline_attachments:
if "post_create_queue" not in session: if "post_create_queue" not in session:
session["post_create_queue"] = [] session["post_create_queue"] = []
session["post_create_queue"].extend(inline_attachments) 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.") sd_workflow_logger.info(f"📥 [SD Hole-Fix] Captured {len(inline_attachments)} file(s) DURING ticket creation API call.")
return return
# --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ --- # --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ ---
if session["step"] == "ticket_created": if session["step"] == "ticket_created":
if inline_attachments: if inline_attachments:
ticket_id = session["ticket_id"] ticket_id = session["ticket_id"]
for att in inline_attachments: for att in inline_attachments:
await msg.answer(sd_uploading_file(ticket_id), parse_mode="html") 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) 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}") sd_workflow_logger.info(f"📎 [Late File Upload] Name: {att['file_name']} -> Success: {ok}")
if ok: if ok:
log_menu_stats(user_id, "Service Desk", f"Добавление файла к заявке #{ticket_id}") log_menu_stats(user_id, "Service Desk", f"Добавление файла к заявке #{ticket_id}")
await msg.answer(sd_file_attached(), parse_mode="html") await msg.answer(sd_file_attached(), parse_mode="html")
else: else:
await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html")
elif msg_text.strip(): elif msg_text.strip():
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
return 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