24 Commits

Author SHA1 Message Date
Денис Кривоченко 83e7963391 Clean: remove __pycache__ and search_bot files from service_desk branch 2026-07-20 15:06:45 +07:00
Денис Кривоченко e7dbe6fc25 Rebase: merge all SD commits into service_desk branch 2026-07-20 15:04:45 +07:00
Денис Кривоченко 5325b43c95 fix: add service_category from template to SD API request 2026-07-15 22:49:26 +07:00
Денис Кривоченко 50e9aa12e4 fix: use template instead of request_template for SD API 2026-07-15 22:40:55 +07:00
Денис Кривоченко 4df52d48f5 fix: use params instead of content for SD API requests 2026-07-15 22:28:15 +07:00
dddennnisss e2c0a2572d fix: добавить Content-Type: application/json и исправить input_data 2026-07-15 22:21:52 +07:00
dddennnisss d9f5ec16e1 fix: double-encode input_data для API Service Desk 2026-07-15 17:01:08 +07:00
dddennnisss eb35fcec82 fix: content= вместо data= для raw JSON body 2026-07-15 16:51:28 +07:00
dddennnisss d4dde34d90 fix: raw JSON body для API Service Desk 2026-07-15 16:40:58 +07:00
dddennnisss f09b693218 fix: добавить логирование ответа API для отладки 2026-07-15 16:16:14 +07:00
dddennnisss 88fca32156 fix: исправить API call (json вместо data) и добавить _sd_ticket_create_error 2026-07-15 16:08:56 +07:00
dddennnisss c4827ce559 feat: встроить шаблоны в SD_MAIN_MENU_TEXT, после выбора показать "Опишите проблему текстом" 2026-07-15 15:54:00 +07:00
dddennnisss 1a4f21cc20 fix: добавить sd_workflow_logger и относительный путь к config 2026-07-15 14:48:15 +07:00
dddennnisss 066f057003 feat: шаблоны показываются в main.py при нажатии 3, sd_module_handler только обрабатывает выбор 2026-07-15 14:37:14 +07:00
dddennnisss 067c8846b0 fix: добавить msg.handled=True чтобы sd_module_handler не перезаписал 2026-07-15 14:19:20 +07:00
dddennnisss 666435be3e feat: показать шаблоны вместо SD_MAIN_MENU_TEXT при нажатии 3 2026-07-15 13:58:24 +07:00
dddennnisss 914c964e08 fix: импортировать и инициализировать sd_sessions в main.py 2026-07-15 13:06:36 +07:00
dddennnisss 8267466ee6 fix: восстановить выбор шаблона после SD_MAIN_MENU_TEXT 2026-07-15 13:03:59 +07:00
dddennnisss b0439bd725 feat: встроить шаблоны в SD_MAIN_MENU_TEXT при нажатии 3 2026-07-15 13:02:38 +07:00
dddennnisss 39a2700555 fix: убрать SD_MAIN_MENU_TEXT из главного меню, показывать шаблоны 2026-07-15 12:52:30 +07:00
dddennnisss 691c112ace fix: инициализация шаблонов до need_text 2026-07-15 12:42:31 +07:00
dddennnisss f7aed57df4 fix: обновить тексты выбора шаблона 2026-07-15 12:38:30 +07:00
dddennnisss e07b546f08 feat: пагинация шаблонов Service Desk по 10 на страницу 2026-07-15 12:33:56 +07:00
dddennnisss efda2c8ae9 feat: добавить выбор шаблона Service Desk перед созданием заявки 2026-07-15 12:24:23 +07:00
4 changed files with 209 additions and 42 deletions
+71 -2
View File
@@ -1,6 +1,7 @@
import sys import sys
import asyncio import asyncio
import logging import logging
from logging.handlers import RotatingFileHandler
import os import os
import psutil import psutil
import time import time
@@ -17,6 +18,13 @@ logging.basicConfig(
) )
logging.getLogger('trueconf.client.chatbot').propagate = False logging.getLogger('trueconf.client.chatbot').propagate = False
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
sd_workflow_logger = logging.getLogger('sd_workflow')
sd_workflow_logger.setLevel(logging.INFO)
if not sd_workflow_logger.handlers:
handler = RotatingFileHandler(os.path.join(os.path.dirname(__file__), 'logs', 'sd_workflow.log'), maxBytes=10*1024*1024, backupCount=5)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
sd_workflow_logger.addHandler(handler)
# Добавляем путь к проекту в sys.path # Добавляем путь к проекту в sys.path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -71,7 +79,7 @@ log_ram_usage("Старт скрипта (базовый вес)")
from photo_bot.handlers import router as photo_router, PHOTO_MAIN_MENU_TEXT from photo_bot.handlers import router as photo_router, PHOTO_MAIN_MENU_TEXT
log_ram_usage("После импорта photo_bot") log_ram_usage("После импорта photo_bot")
from service_desk.handlers import router as sd_router, SD_MAIN_MENU_TEXT from service_desk.handlers import router as sd_router, SD_MAIN_MENU_TEXT, sd_sessions
log_ram_usage("После импорта service_desk") log_ram_usage("После импорта service_desk")
from transcription_bot.handlers import router as transcription_router, TRANSCRIPTION_MAIN_MENU_TEXT from transcription_bot.handlers import router as transcription_router, TRANSCRIPTION_MAIN_MENU_TEXT
@@ -112,7 +120,68 @@ async def main_menu(msg: Message):
USER_MENU_SHOWN[user_id] = True USER_MENU_SHOWN[user_id] = True
set_state(user_id, "SD_MODE") set_state(user_id, "SD_MODE")
log_menu_stats(user_id, "Service Desk", "Вход") log_menu_stats(user_id, "Service Desk", "Вход")
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html") # Помечаем сообщение как обработанное, чтобы sd_module_handler не перезаписал
msg.handled = True
# Инициализируем сессию для service_desk
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": ""}
# Загружаем шаблоны и встраиваем их в текст
try:
import json, asyncio, httpx, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Импортируем SD_URL и SD_TOKEN из config
import importlib.util
spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.abspath(__file__)), "config", "config.py"))
custom_config = importlib.util.module_from_spec(spec)
spec.loader.exec_module(custom_config)
SD_URL = custom_config.SD_URL
SD_TOKEN = custom_config.SD_TOKEN
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)}
url = f"{SD_URL}/api/v3/request_templates"
async with httpx.AsyncClient(verify=False) as client:
resp = await client.get(url, headers=headers, params=params, timeout=30.0)
if resp.status_code == 200:
data = resp.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"
]
async def check_template(t, cl):
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:
pass
return None
tasks = [check_template(t, client) for t in pre_filtered]
results = await asyncio.gather(*tasks)
templates = [t for t in results if t is not None]
template_lines = ""
for idx, t in enumerate(templates):
num = idx + 1
digit = f"{num}" if num <= 9 else "0⃣" if num == 10 else str(num)
template_lines += f"{digit} - {t['name']}\n"
# Сохраняем шаблоны в sd_sessions для sd_module_handler
sd_sessions[user_id]["templates"] = templates
sd_sessions[user_id]["template_names"] = {str(i+1): str(t["id"]) for i, t in enumerate(templates)}
sd_sessions[user_id]["template_menu_text"] = template_lines.strip()
template_text = f"🎫 Режим создания заявки в Service Desk\n\nВыберите, пожалуйста, шаблон подходящий под вашу проблему\n\n{template_lines}"
await msg.answer(template_text, parse_mode="html")
msg.handled = True
else:
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
msg.handled = True
except Exception as e:
sd_workflow_logger.error(f"Ошибка шаблонов: {e}")
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
msg.handled = True
return return
elif action == "PHOTO": elif action == "PHOTO":
USER_MENU_SHOWN[user_id] = True USER_MENU_SHOWN[user_id] = True
+133 -40
View File
@@ -23,7 +23,7 @@ if not sd_workflow_logger.handlers:
handler.setFormatter(formatter) handler.setFormatter(formatter)
sd_workflow_logger.addHandler(handler) 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")) 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) custom_config = importlib.util.module_from_spec(spec)
@@ -44,14 +44,14 @@ from utils.texts import (
sd_uploading_file, sd_uploading_file,
sd_file_attached, sd_file_attached,
sd_file_upload_error, sd_file_upload_error,
) )
from utils.texts import UNKNOWN_MAIN_CMD_TEXT from utils.texts import UNKNOWN_MAIN_CMD_TEXT
# 🔌 Импортируем централизованную функцию сбора статистики из main # 🔌 Импортируем централизованную функцию сбора статистики из main
from utils.stats_logger import log_menu_stats from utils.stats_logger import log_menu_stats
# Таймаут ожидания вложений после текста (секунды) # Таймаут ожидания вложений после текста (секунды)
ATTACHMENT_WAIT_TIMEOUT = 2.0 ATTACHMENT_WAIT_TIMEOUT = 2.0
SD_DISPATCHER_SYSTEM_PROMPT = ( SD_DISPATCHER_SYSTEM_PROMPT = (
"Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\n" "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\n"
"ПРАВИЛА:\n" "ПРАВИЛА:\n"
@@ -59,7 +59,7 @@ SD_DISPATCHER_SYSTEM_PROMPT = (
"2. Отвечать на английском языке ЗАПРЕЩЕНО.\n" "2. Отвечать на английском языке ЗАПРЕЩЕНО.\n"
"3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n" "3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n"
"4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n" "4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n"
"5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.") "5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.")
# Глушим системные предупреждения # Глушим системные предупреждения
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -67,6 +67,51 @@ logger = logging.getLogger(__name__)
router = Router() router = Router()
sd_sessions = {} 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", "Content-Type": "application/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", {})
if detailed.get("show_to_requester") is True:
sc = detailed.get("service_category", {})
t["service_category"] = sc
t["request"] = detailed.get("request", {})
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 []
# ================================================ # ================================================
# БИЗНЕС-ЛОГИКА # БИЗНЕС-ЛОГИКА
# ================================================ # ================================================
@@ -97,14 +142,14 @@ async def generate_smart_subject(text: str) -> str:
OLLAMA_SUBJECT_MODEL = custom_config.OLLAMA_SUBJECT_MODEL OLLAMA_SUBJECT_MODEL = custom_config.OLLAMA_SUBJECT_MODEL
def has_chinese(t): return bool(re.search(r'[\u4e00-\u9fff]', str(t))) def has_chinese(t): return bool(re.search(r'[\u4e00-\u9fff]', str(t)))
if not text or len(text.split()) < 2: return "Заявка из КЛЕВЕР" if not text or len(text.split()) < 2: return "Заявка из КЛЕВЕР"
user_msg = f"Сформулируй краткую тему для следующей ИТ-заявки:\n{text}" user_msg = f"Сформулируй краткую тему для следующей ИТ-заявки:\n{text}"
messages = [ messages = [
{"role": "system", "content": SD_DISPATCHER_SYSTEM_PROMPT}, {"role": "system", "content": SD_DISPATCHER_SYSTEM_PROMPT},
{"role": "user", "content": user_msg} {"role": "user", "content": user_msg}
] ]
try: try:
payload = {"model": OLLAMA_SUBJECT_MODEL, "messages": messages, "temperature": 0.0} payload = {"model": OLLAMA_SUBJECT_MODEL, "messages": messages, "temperature": 0.0}
sd_workflow_logger.info(f"📝 [Subject Gen] Payload: {payload}") sd_workflow_logger.info(f"📝 [Subject Gen] Payload: {payload}")
@@ -128,18 +173,36 @@ async def generate_smart_subject(text: str) -> str:
sd_workflow_logger.error(f"📝 [Subject Gen Error] Exception: {e}\n{traceback.format_exc()}") sd_workflow_logger.error(f"📝 [Subject Gen Error] Exception: {e}\n{traceback.format_exc()}")
return "Заявка из КЛЕВЕР" 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 = None, templates: list | None = None):
headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"} headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json", "Content-Type": "application/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, template_id, templates):
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": {"name": email}}}
async with httpx.AsyncClient(verify=False) as client: if template_id:
return await client.post(endpoint, headers=headers, data={"input_data": json.dumps(payload)}, timeout=15.0) req_payload["request"]["template"] = {"id": template_id}
resp = await _send_req(requester_email, city) # Найти service_category по шаблону
data = resp.json() if resp.status_code in [200, 201] else {} for t in templates:
if data.get("response_status", {}).get("status_code") != 2000: if str(t.get("id")) == str(template_id):
resp = await _send_req(requester_email, "Кемерово") sc = t.get("service_category", {})
if sc:
req_payload["request"]["service_category"] = sc
break
if "udf_pick_301" in req_payload["request"]:
req_payload["request"]["udf_fields"] = {"udf_pick_301": current_city}
elif "udf_fields" in req_payload["request"] and current_city:
req_payload["request"]["udf_fields"]["udf_pick_301"] = current_city
else:
req_payload["request"]["udf_fields"] = {"udf_pick_301": current_city}
async with httpx.AsyncClient(verify=False) as client:
resp = await client.post(endpoint, headers=headers, params={"input_data": json.dumps(req_payload)}, timeout=15.0)
if resp.status_code not in [200, 201]:
sd_workflow_logger.error(f"API Error: status={resp.status_code} body={resp.text}")
return resp
resp = await _send_req(requester_email, city, template_id, templates)
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, "Кемерово", template_id, templates)
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") return data.get("request", {}).get("id")
@@ -178,7 +241,7 @@ async def process_and_upload_file(file_id: str, filename: str, ticket_id: str):
return False return False
finally: finally:
if downloaded_path and os.path.exists(downloaded_path): if downloaded_path and os.path.exists(downloaded_path):
os.remove(downloaded_path) os.remove(downloaded_path)
def _extract_text_from_content(content): def _extract_text_from_content(content):
"""Извлечение текста из msg.content.""" """Извлечение текста из msg.content."""
@@ -220,6 +283,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): async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login):
"""Логика генерации темы, отправки в SD и загрузки всех очередей вложений.""" """Логика генерации темы, отправки в SD и загрузки всех очередей вложений."""
session["step"] = "creating_ticket" session["step"] = "creating_ticket"
template_id = session.get("template_id")
sd_workflow_logger.info(f"[Create] Template ID: {template_id}")
try: try:
ad_user = await asyncio.to_thread(get_ad_user_sync, login) 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}") sd_workflow_logger.info(f"👤 [AD Lookup] User: {login} -> Found: {ad_user is not None}")
@@ -228,7 +293,7 @@ async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login
subject = await generate_smart_subject(msg_text) subject = await generate_smart_subject(msg_text)
sd_workflow_logger.info(f"📝 [Subject Gen] Text: {msg_text[:50]}... -> Subject: {subject}") 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, session.get("templates", []))
sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}") sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}")
if ticket_id: if ticket_id:
@@ -273,10 +338,10 @@ async def _wait_for_attachments_and_create(user_id, session):
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
sd_workflow_logger.info(f"⏰ [SD] Timeout reached. Initiating ticket creation for user {user_id}") sd_workflow_logger.info(f"⏰ [SD] Timeout reached. Initiating ticket creation for user {user_id}")
msg = session.get("msg") msg = session.get("msg")
login = session.get("login") login = session.get("login")
@@ -289,23 +354,23 @@ async def sd_module_handler(msg: Message):
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", "Выход в главное меню")
@@ -313,36 +378,64 @@ async def sd_module_handler(msg: Message):
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": [], "templates": [], "template_names": {}}
session = sd_sessions[user_id] 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(SD_MAIN_MENU_TEXT, parse_mode="html")
return
else:
await msg.answer(
f"Неверный номер. Выберите из списка:\n\n"
f"{session.get('template_menu_text', '')}",
parse_mode="html"
)
return
# --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА --- # --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА ---
if session["step"] == "need_text": if session["step"] == "need_text":
# Пользователь только что вошёл в SD_MODE — показываем меню
if len(msg_text.strip()) == 0 and not inline_attachments:
await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html")
return
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")
@@ -352,7 +445,7 @@ async def sd_module_handler(msg: Message):
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:
@@ -361,14 +454,14 @@ async def sd_module_handler(msg: Message):
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:
@@ -377,7 +470,7 @@ async def sd_module_handler(msg: Message):
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:
@@ -393,4 +486,4 @@ async def sd_module_handler(msg: Message):
await msg.answer(sd_file_upload_error(), parse_mode="html") await msg.answer(sd_file_upload_error(), 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
Binary file not shown.
+5
View File
@@ -170,6 +170,11 @@ SD_UNKNOWN_CMD_TEXT = _sd_unknown(EMOJI_DIGITS)
SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS) SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS)
SD_CREATING = _sd_creating() SD_CREATING = _sd_creating()
def _sd_ticket_create_error(emojis: dict) -> str:
return (
f"❌ <b>Не удалось создать заявку.</b>\n\n" f"Пожалуйста, повторите попытку позже.\n\n" f"{emojis['0']}В главное меню"
)
def sd_ticket_create_error() -> str: def sd_ticket_create_error() -> str:
return _sd_ticket_create_error(EMOJI_DIGITS) return _sd_ticket_create_error(EMOJI_DIGITS)