From efda2c8ae92adf9c5aae7305e488fab0d850a991 Mon Sep 17 00:00:00 2001 From: dddennnisss Date: Wed, 15 Jul 2026 12:24:23 +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=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0=20Service=20Desk=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service_desk/handlers.py | 104 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/service_desk/handlers.py b/service_desk/handlers.py index 8f533da..e9c9d0f 100644 --- a/service_desk/handlers.py +++ b/service_desk/handlers.py @@ -67,6 +67,47 @@ logger = logging.getLogger(__name__) router = Router() sd_sessions = {} +async def get_sd_templates(): + url = f"{SD_URL}/api/v3/request_templates" + headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"} + input_data = {"list_info": {"row_count": 100, "start_index": 1}} + params = {"input_data": json.dumps(input_data)} + try: + async with httpx.AsyncClient(verify=False) as client: + response = await client.get(url, headers=headers, params=params, timeout=30.0) + if response.status_code != 200: + sd_workflow_logger.error(f"Ошибка API шаблонов: HTTP {response.status_code} - {response.text}") + return [] + data = response.json() + all_templates = data.get("request_templates", []) + pre_filtered = [ + t for t in all_templates + if t.get("status") == "ACTIVE" + and not t.get("inactive", False) + and str(t.get("id")) != "2" + ] + sd_workflow_logger.info(f"Шаблонов в системе: {len(all_templates)} | Активных: {len(pre_filtered)}") + async def check_template(t): + try: + t_resp = await client.get(url + "/" + str(t["id"]), headers=headers, timeout=15.0) + if t_resp.status_code == 200: + detailed = t_resp.json().get("request_template", {}) + return t if detailed.get("show_to_requester") is True else None + except Exception as e: + sd_workflow_logger.warning(f"Ошибка деталей шаблона ID {t['id']}: {e}") + return None + tasks = [check_template(t) for t in pre_filtered] + results = await asyncio.gather(*tasks) + active_templates = [t for t in results if t is not None] + sd_workflow_logger.info(f"Шаблоны с show_to_requester=True: {len(active_templates)}") + for temp in active_templates: + sd_workflow_logger.info(f" ID: {temp['id']} | Name: {temp['name']}") + return active_templates + except Exception as e: + sd_workflow_logger.error(f"Ошибка загрузки шаблонов: {e}") + return [] + + # ================================================ # БИЗНЕС-ЛОГИКА # ================================================ @@ -128,14 +169,16 @@ async def generate_smart_subject(text: str) -> str: sd_workflow_logger.error(f"📝 [Subject Gen Error] Exception: {e}\n{traceback.format_exc()}") return "Заявка из КЛЕВЕР" -async def create_ticket_in_sd(requester_email: str, subject: str, description: str, city: str): +async def create_ticket_in_sd(requester_email: str, subject: str, description: str, city: str, template_id: str = None): 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}}} + req_payload = {"request": {"subject": subject, "description": html_desc, "requester": {"email_id": email}, "udf_fields": {"udf_pick_301": current_city}}} + if template_id: + req_payload["request"]["request_template"] = {"id": template_id} 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(req_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: @@ -220,6 +263,8 @@ 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" + template_id = session.get("template_id") + sd_workflow_logger.info(f"[Create] Template ID: {template_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}") @@ -228,7 +273,7 @@ async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login 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) + ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city, template_id) sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}") if ticket_id: @@ -290,6 +335,27 @@ async def sd_module_handler(msg: Message): if states.get_state(user_id) != "SD_MODE": return + + # Инициализация шаблонов при первом входе в SD_MODE + if user_id not in sd_sessions: + sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}, "template_menu_text": ""} + templates = await get_sd_templates() + if templates: + sd_sessions[user_id]["templates"] = templates + sd_sessions[user_id]["template_names"] = {str(i+1): str(t["id"]) for i, t in enumerate(templates)} + template_menu_text = "" + for i, t in enumerate(templates): + template_menu_text += f"{EMOJI_DIGITS[str(i+1)]} - {t['name']}\n" + sd_sessions[user_id]["template_menu_text"] = template_menu_text.strip() + sd_workflow_logger.info(f"[Template] Loaded {len(templates)} templates for user {user_id}") + sd_sessions[user_id]["step"] = "choose_template" + await msg.answer( + f"Выберите шаблон для заявки:\n\n" + f"{template_menu_text}\n\n" + f"Или отправьте пропустить для создания без шаблона.", + parse_mode="html" + ) + 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) @@ -315,8 +381,36 @@ async def sd_module_handler(msg: Message): return 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": [], "templates": [], "template_names": {}} session = sd_sessions[user_id] + + # --- ШАГ 0: ВЫБОР ШАБЛОНА --- + if session["step"] == "choose_template": + if cmd in ["skip", "пропустить", "без шаблона"]: + sd_workflow_logger.info("[Template] User skipped template selection") + session["step"] = "need_text" + await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html") + return + if cmd in session.get("template_names", {}): + template_id = session["template_names"][cmd] + template_name = next((t["name"] for t in session.get("templates", []) if str(t["id"]) == str(template_id)), "Неизвестно") + sd_workflow_logger.info(f"[Template] User selected: {template_id} ({template_name})") + session["template_id"] = template_id + session["step"] = "need_text" + await msg.answer( + f"Выбран шаблон: {template_name}\n\n" + f"Теперь опишите проблему текстом (можно прикрепить файлы):", + parse_mode="html" + ) + return + else: + await msg.answer( + f"Неверный номер. Выберите из списка:\n\n" + f"{session.get('template_menu_text', '')}\n\n" + f"Или отправьте пропустить для создания без шаблона.", + parse_mode="html" + ) + return # --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА --- if session["step"] == "need_text":