Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83e7963391 | |||
| e7dbe6fc25 | |||
| 5325b43c95 | |||
| 50e9aa12e4 | |||
| 4df52d48f5 | |||
| e2c0a2572d | |||
| d9f5ec16e1 | |||
| eb35fcec82 | |||
| d4dde34d90 | |||
| f09b693218 | |||
| 88fca32156 | |||
| c4827ce559 | |||
| 1a4f21cc20 | |||
| 066f057003 | |||
| 067c8846b0 | |||
| 666435be3e | |||
| 914c964e08 | |||
| 8267466ee6 | |||
| b0439bd725 | |||
| 39a2700555 | |||
| 691c112ace | |||
| f7aed57df4 | |||
| e07b546f08 | |||
| efda2c8ae9 |
@@ -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
@@ -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.
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user