From edbb1bbc95ced2640d13d9672c8384e21f738459 Mon Sep 17 00:00:00 2001 From: dddennnisss Date: Thu, 30 Jul 2026 23:35:06 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20service=5Fdesk/handlers.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service_desk/handlers.py | 154 +++++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 70 deletions(-) diff --git a/service_desk/handlers.py b/service_desk/handlers.py index 49d077d..af91c7f 100644 --- a/service_desk/handlers.py +++ b/service_desk/handlers.py @@ -23,7 +23,7 @@ if not sd_workflow_logger.handlers: handler.setFormatter(formatter) sd_workflow_logger.addHandler(handler) # ---------------------------------- - + # Абсолютный импорт конфигурации spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "config.py")) custom_config = importlib.util.module_from_spec(spec) @@ -44,14 +44,14 @@ from utils.texts import ( sd_uploading_file, sd_file_attached, sd_file_upload_error, + system_error_text, + UNKNOWN_MAIN_CMD_TEXT, ) -from utils.texts import UNKNOWN_MAIN_CMD_TEXT -# 🔌 Импортируем централизованную функцию сбора статистики из main from utils.stats_logger import log_menu_stats - + # Таймаут ожидания вложений после текста (секунды) ATTACHMENT_WAIT_TIMEOUT = 2.0 - + SD_DISPATCHER_SYSTEM_PROMPT = ( "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\n" "ПРАВИЛА:\n" @@ -96,14 +96,14 @@ async def generate_smart_subject(text: str) -> str: OLLAMA_SUBJECT_MODEL = custom_config.OLLAMA_SUBJECT_MODEL def has_chinese(t): return bool(re.search(r'[\u4e00-\u9fff]', str(t))) if not text or len(text.split()) < 2: return "Заявка из КЛЕВЕР" - + user_msg = f"Сформулируй краткую тему для следующей ИТ-заявки:\n{text}" - + messages = [ {"role": "system", "content": SD_DISPATCHER_SYSTEM_PROMPT}, {"role": "user", "content": user_msg} ] - + try: payload = {"model": OLLAMA_SUBJECT_MODEL, "messages": messages, "temperature": 0.0} sd_workflow_logger.info(f"📝 [Subject Gen] Payload: {payload}") @@ -131,17 +131,26 @@ async def create_ticket_in_sd(requester_email: str, subject: str, description: s headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"} endpoint = f"{SD_URL}/api/v3/requests" html_desc = f"

{description.replace(chr(10), '
')}



Создано через TrueConf КЛЕВЕР

" + async def _send_req(email, current_city): payload = {"request": {"subject": subject, "description": html_desc, "requester": {"email_id": email}, "udf_fields": {"udf_pick_301": current_city}}} async with httpx.AsyncClient(verify=False) as client: return await client.post(endpoint, headers=headers, data={"input_data": json.dumps(payload)}, timeout=15.0) - resp = await _send_req(requester_email, city) - data = resp.json() if resp.status_code in [200, 201] else {} - if data.get("response_status", {}).get("status_code") != 2000: - resp = await _send_req(requester_email, "Кемерово") + + # Безопасная обработка сбоев сети / отсуствия доступа к серверу SD + try: + resp = await _send_req(requester_email, city) data = resp.json() if resp.status_code in [200, 201] else {} - if data.get("response_status", {}).get("status_code") == 2000: - return data.get("request", {}).get("id") + if data.get("response_status", {}).get("status_code") != 2000: + resp = await _send_req(requester_email, "Кемерово") + data = resp.json() if resp.status_code in [200, 201] else {} + if data.get("response_status", {}).get("status_code") == 2000: + return data.get("request", {}).get("id") + except (httpx.RequestError, httpx.HTTPStatusError, Exception) as e: + logger.error(f"❌ [SD Network Error] Не удалось подключиться к ServiceDesk: {e}") + sd_workflow_logger.error(f"❌ [SD Network Error] {e}") + return None + return None async def process_and_upload_file(file_id: str, filename: str, ticket_id: str): @@ -219,57 +228,64 @@ def _extract_attachments_from_content(content, is_attachment_type=False): async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login): """Логика генерации темы, отправки в SD и загрузки всех очередей вложений.""" session["step"] = "creating_ticket" - ad_user = await asyncio.to_thread(get_ad_user_sync, login) - sd_workflow_logger.info(f"👤 [AD Lookup] User: {login} -> Found: {ad_user is not None}") - sender_email = ad_user.get("mail") if ad_user else DEFAULT_REQUESTER - city = ad_user.get("city") if ad_user else "Кемерово" - subject = await generate_smart_subject(msg_text) - sd_workflow_logger.info(f"📝 [Subject Gen] Text: {msg_text[:50]}... -> Subject: {subject}") - ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city) - sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}") - - if ticket_id: - session["ticket_id"] = ticket_id + try: + ad_user = await asyncio.to_thread(get_ad_user_sync, login) + sd_workflow_logger.info(f"👤 [AD Lookup] User: {login} -> Found: {ad_user is not None}") + sender_email = ad_user.get("mail") if ad_user else DEFAULT_REQUESTER + city = ad_user.get("city") if ad_user else "Кемерово" + subject = await generate_smart_subject(msg_text) + sd_workflow_logger.info(f"📝 [Subject Gen] Text: {msg_text[:50]}... -> Subject: {subject}") - # 1. Загрузка основных вложений (накопленных до и во время таймаута) - queued_files = list(session.get("files_queue", [])) - if queued_files: - sd_workflow_logger.info(f"📎 [Upload] Uploading {len(queued_files)} file(s) to ticket #{ticket_id}") - for f in queued_files: - await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) - sd_workflow_logger.info(f"✅ [Upload] All queued file(s) uploaded") + ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city) + sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}") - # 2. Загрузка вложений, прилетевших во время выполнения API-запросов (из "черной дыры") - post_files = list(session.get("post_create_queue", [])) - if post_files: - sd_workflow_logger.info(f"📎 [Upload Extra] Uploading {len(post_files)} late file(s) to ticket #{ticket_id}") - for f in post_files: - await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) - - session["files_queue"] = [] - session["post_create_queue"] = [] - session["step"] = "ticket_created" - - await msg.answer( - sd_ticket_created(ticket_id, subject, msg_text), - parse_mode="html" - ) - log_menu_stats(user_id, "Service Desk", f"Создание заявки #{ticket_id}") - else: - session["step"] = "need_text" - await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") + if ticket_id: + session["ticket_id"] = ticket_id + + # 1. Загрузка основных вложений + queued_files = list(session.get("files_queue", [])) + if queued_files: + sd_workflow_logger.info(f"📎 [Upload] Uploading {len(queued_files)} file(s) to ticket #{ticket_id}") + for f in queued_files: + await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) + sd_workflow_logger.info(f"✅ [Upload] All queued file(s) uploaded") + + # 2. Загрузка вложений, прилетевших во время выполнения API-запросов + post_files = list(session.get("post_create_queue", [])) + if post_files: + sd_workflow_logger.info(f"📎 [Upload Extra] Uploading {len(post_files)} late file(s) to ticket #{ticket_id}") + for f in post_files: + await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) + + session["files_queue"] = [] + session["post_create_queue"] = [] + session["step"] = "ticket_created" + + await msg.answer( + sd_ticket_created(ticket_id, subject, msg_text), + parse_mode="html" + ) + log_menu_stats(user_id, "Service Desk", f"Создание заявки #{ticket_id}") + else: + session["step"] = "need_text" + await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") + except Exception as e: + logger.exception(f"❌ [SD Ticket Error] {user_id}: {e}") + sd_workflow_logger.error(f"❌ [SD Ticket Error] {user_id}: {e}") + session["step"] = "need_text" + await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") async def _wait_for_attachments_and_create(user_id, session): """Фоновый таймер с возможностью отмены (Debounce).""" try: await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT) except asyncio.CancelledError: - return # Таймер сброшен, прерываем выполнение текущей таски - + return + if session.get("step") != "waiting_for_attachments": return - + sd_workflow_logger.info(f"⏰ [SD] Timeout reached. Initiating ticket creation for user {user_id}") msg = session.get("msg") login = session.get("login") @@ -285,6 +301,16 @@ async def _wait_for_attachments_and_create(user_id, session): except Exception: sd_workflow_logger.error(f"❌ [SD] Failed to send error message to {user_id}") +def _on_sd_timer_done(task: asyncio.Task, user_id: str): + """Безопасный коллбэк завершения таймера создания заявки""" + try: + task.result() + except asyncio.CancelledError: + pass + except Exception as e: + logger.error(f"❌ Ошибка в таске таймера SD для {user_id}: {e}") + sd_workflow_logger.error(f"❌ [SD Timer Error] {user_id}: {e}") + @router.message() async def sd_module_handler(msg: Message): try: @@ -309,7 +335,7 @@ async def sd_module_handler(msg: Message): cmd = raw_num break - # --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) --- + # --- ВЫХОД --- if cmd in ["0", "9", "/start", "меню"]: log_menu_stats(user_id, "Service Desk", "Выход в главное меню") states.clear_state(user_id) @@ -327,29 +353,24 @@ async def sd_module_handler(msg: Message): 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)) @@ -357,7 +378,7 @@ async def sd_module_handler(msg: Message): sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...") return - # --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) --- + # --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ --- if session["step"] == "waiting_for_attachments": if inline_attachments: session["files_queue"].extend(inline_attachments) @@ -366,7 +387,6 @@ async def sd_module_handler(msg: Message): 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)) @@ -374,7 +394,7 @@ async def sd_module_handler(msg: Message): sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s") return - # --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") --- + # --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ --- if session["step"] == "creating_ticket": if inline_attachments: if "post_create_queue" not in session: @@ -405,10 +425,4 @@ async def sd_module_handler(msg: Message): try: await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") except Exception: - pass - - - - - - + pass \ No newline at end of file