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
+70 -1
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", "Вход")
# Помечаем сообщение как обработанное, чтобы 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") 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
+102 -9
View File
@@ -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 []
# ================================================ # ================================================
# БИЗНЕС-ЛОГИКА # БИЗНЕС-ЛОГИКА
# ================================================ # ================================================
@@ -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}}}
if template_id:
req_payload["request"]["template"] = {"id": template_id}
# Найти service_category по шаблону
for t in templates:
if str(t.get("id")) == str(template_id):
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: async with httpx.AsyncClient(verify=False) as client:
return await client.post(endpoint, headers=headers, data={"input_data": json.dumps(payload)}, timeout=15.0) resp = await client.post(endpoint, headers=headers, params={"input_data": json.dumps(req_payload)}, timeout=15.0)
resp = await _send_req(requester_email, city) 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 {} 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:
resp = await _send_req(requester_email, "Кемерово") 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")
@@ -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:
@@ -315,11 +380,39 @@ async def sd_module_handler(msg: Message):
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
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)