@@ -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