diff --git a/service_desk/handlers.py b/service_desk/handlers.py index 3d1e0ed..dd04213 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) @@ -48,10 +48,10 @@ from utils.texts import ( 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" @@ -59,7 +59,7 @@ SD_DISPATCHER_SYSTEM_PROMPT = ( "2. Отвечать на английском языке ЗАПРЕЩЕНО.\n" "3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n" "4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n" - "5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.") + "5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.") # Глушим системные предупреждения urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -138,14 +138,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}") @@ -221,7 +221,7 @@ async def process_and_upload_file(file_id: str, filename: str, ticket_id: str): return False finally: if downloaded_path and os.path.exists(downloaded_path): - os.remove(downloaded_path) + os.remove(downloaded_path) def _extract_text_from_content(content): """Извлечение текста из msg.content.""" @@ -318,10 +318,10 @@ async def _wait_for_attachments_and_create(user_id, session): await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT) except asyncio.CancelledError: 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") @@ -334,46 +334,23 @@ async def sd_module_handler(msg: Message): sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}") if states.get_state(user_id) != "SD_MODE": return - - # Инициализация шаблонов при входе в SD_MODE - if user_id not in sd_sessions: - sd_sessions[user_id] = {"step": "choose_template", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}, "template_menu_text": ""} - # Загружаем шаблоны если их ещё нет - if not sd_sessions[user_id].get("templates"): - 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 idx, t in enumerate(templates): - num = idx + 1 - digit = f"{num}⃣" if num <= 9 else "0⃣" if num == 10 else str(num) - template_menu_text += f"{digit} - {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}") - await msg.answer( - f"Выберите нужный шаблон:\n\n" - f"{template_menu_text}", - 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) - + 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", "Выход в главное меню") @@ -381,12 +358,12 @@ async def sd_module_handler(msg: Message): 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": [], "templates": [], "template_names": {}} - session = sd_sessions[user_id] + session = sd_sessions[user_id] + - # --- ШАГ 0: ВЫБОР ШАБЛОНА --- if session["step"] == "choose_template": if cmd in ["skip", "пропустить", "без шаблона"]: @@ -419,26 +396,26 @@ async def sd_module_handler(msg: Message): 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") @@ -448,7 +425,7 @@ async def sd_module_handler(msg: Message): 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: @@ -457,14 +434,14 @@ async def sd_module_handler(msg: Message): 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: @@ -473,7 +450,7 @@ async def sd_module_handler(msg: Message): 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: @@ -489,4 +466,4 @@ async def sd_module_handler(msg: Message): await msg.answer(sd_file_upload_error(), parse_mode="html") elif msg_text.strip(): await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") - return \ No newline at end of file + return