Обновить service_desk/handlers.py

This commit is contained in:
2026-07-30 23:35:06 +07:00
parent 0973b83ba0
commit edbb1bbc95
+70 -56
View File
@@ -44,9 +44,9 @@ from utils.texts import (
sd_uploading_file, sd_uploading_file,
sd_file_attached, sd_file_attached,
sd_file_upload_error, 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 from utils.stats_logger import log_menu_stats
# Таймаут ожидания вложений после текста (секунды) # Таймаут ожидания вложений после текста (секунды)
@@ -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"} headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"}
endpoint = f"{SD_URL}/api/v3/requests" endpoint = f"{SD_URL}/api/v3/requests"
html_desc = f"<p>{description.replace(chr(10), '<br>')}</p><br><hr><p style='color:#555;font-size:12px;'><i>Создано через TrueConf КЛЕВЕР</i></p>" html_desc = f"<p>{description.replace(chr(10), '<br>')}</p><br><hr><p style='color:#555;font-size:12px;'><i>Создано через TrueConf КЛЕВЕР</i></p>"
async def _send_req(email, current_city): 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}}} 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: async with httpx.AsyncClient(verify=False) as client:
return await client.post(endpoint, headers=headers, data={"input_data": json.dumps(payload)}, timeout=15.0) 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 {} # Безопасная обработка сбоев сети / отсуствия доступа к серверу SD
if data.get("response_status", {}).get("status_code") != 2000: try:
resp = await _send_req(requester_email, "Кемерово") resp = await _send_req(requester_email, city)
data = resp.json() if resp.status_code in [200, 201] else {} data = resp.json() if resp.status_code in [200, 201] else {}
if data.get("response_status", {}).get("status_code") == 2000: if data.get("response_status", {}).get("status_code") != 2000:
return data.get("request", {}).get("id") 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 return None
async def process_and_upload_file(file_id: str, filename: str, ticket_id: str): async def process_and_upload_file(file_id: str, filename: str, ticket_id: str):
@@ -219,44 +228,51 @@ def _extract_attachments_from_content(content, is_attachment_type=False):
async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login): async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login):
"""Логика генерации темы, отправки в SD и загрузки всех очередей вложений.""" """Логика генерации темы, отправки в SD и загрузки всех очередей вложений."""
session["step"] = "creating_ticket" 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) try:
sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}") 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}")
if ticket_id: ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city)
session["ticket_id"] = ticket_id sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}")
# 1. Загрузка основных вложений (накопленных до и во время таймаута) if ticket_id:
queued_files = list(session.get("files_queue", [])) session["ticket_id"] = ticket_id
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-запросов (из "черной дыры") # 1. Загрузка основных вложений
post_files = list(session.get("post_create_queue", [])) queued_files = list(session.get("files_queue", []))
if post_files: if queued_files:
sd_workflow_logger.info(f"📎 [Upload Extra] Uploading {len(post_files)} late file(s) to ticket #{ticket_id}") sd_workflow_logger.info(f"📎 [Upload] Uploading {len(queued_files)} file(s) to ticket #{ticket_id}")
for f in post_files: for f in queued_files:
await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) await process_and_upload_file(f['file_id'], f['file_name'], ticket_id)
sd_workflow_logger.info(f"✅ [Upload] All queued file(s) uploaded")
session["files_queue"] = [] # 2. Загрузка вложений, прилетевших во время выполнения API-запросов
session["post_create_queue"] = [] post_files = list(session.get("post_create_queue", []))
session["step"] = "ticket_created" 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)
await msg.answer( session["files_queue"] = []
sd_ticket_created(ticket_id, subject, msg_text), session["post_create_queue"] = []
parse_mode="html" session["step"] = "ticket_created"
)
log_menu_stats(user_id, "Service Desk", f"Создание заявки #{ticket_id}") await msg.answer(
else: 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" session["step"] = "need_text"
await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html")
@@ -265,7 +281,7 @@ async def _wait_for_attachments_and_create(user_id, session):
try: try:
await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT) await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT)
except asyncio.CancelledError: except asyncio.CancelledError:
return # Таймер сброшен, прерываем выполнение текущей таски return
if session.get("step") != "waiting_for_attachments": if session.get("step") != "waiting_for_attachments":
return return
@@ -285,6 +301,16 @@ async def _wait_for_attachments_and_create(user_id, session):
except Exception: except Exception:
sd_workflow_logger.error(f"❌ [SD] Failed to send error message to {user_id}") 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() @router.message()
async def sd_module_handler(msg: Message): async def sd_module_handler(msg: Message):
try: try:
@@ -309,7 +335,7 @@ async def sd_module_handler(msg: Message):
cmd = raw_num cmd = raw_num
break break
# --- ВЫХОД (Кнопки 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)
@@ -327,29 +353,24 @@ async def sd_module_handler(msg: Message):
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)
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))
@@ -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...") 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)
@@ -366,7 +387,6 @@ async def sd_module_handler(msg: Message):
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))
@@ -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") 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:
@@ -406,9 +426,3 @@ async def sd_module_handler(msg: Message):
await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html")
except Exception: except Exception:
pass pass