diff --git a/instruct/__pycache__/handlers.cpython-311.pyc b/instruct/__pycache__/handlers.cpython-311.pyc new file mode 100644 index 0000000..5072fda Binary files /dev/null and b/instruct/__pycache__/handlers.cpython-311.pyc differ diff --git a/lk/__pycache__/handlers.cpython-311.pyc b/lk/__pycache__/handlers.cpython-311.pyc new file mode 100644 index 0000000..dc6634e Binary files /dev/null and b/lk/__pycache__/handlers.cpython-311.pyc differ diff --git a/photo_bot/__pycache__/handlers.cpython-311.pyc b/photo_bot/__pycache__/handlers.cpython-311.pyc new file mode 100644 index 0000000..546fe29 Binary files /dev/null and b/photo_bot/__pycache__/handlers.cpython-311.pyc differ diff --git a/sd_dispatcher b/sd_dispatcher new file mode 160000 index 0000000..b7b099c --- /dev/null +++ b/sd_dispatcher @@ -0,0 +1 @@ +Subproject commit b7b099cb38cc6060293fb41a9969181dc977eda3 diff --git a/service_desk/__pycache__/handlers.cpython-311.pyc b/service_desk/__pycache__/handlers.cpython-311.pyc new file mode 100644 index 0000000..e2ef18d Binary files /dev/null and b/service_desk/__pycache__/handlers.cpython-311.pyc differ diff --git a/service_desk/test_basic.py b/service_desk/test_basic.py new file mode 100644 index 0000000..86d1d56 --- /dev/null +++ b/service_desk/test_basic.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import asyncio +import httpx +import json +import urllib3 + +BASE_URL = "https://192.168.1.237:8080" +API_TOKEN = "062CE056-C1F5-490C-A6DF-FD2C653A71DD" + +async def main(): + url = f"{BASE_URL}/api/v3/requests" + headers = { + "authtoken": API_TOKEN, + "Accept": "application/vnd.manageengine.sdp.v3+json" + } + + async with httpx.AsyncClient(verify=False, timeout=30.0) as client: + resp = await client.get(url, headers=headers, params={ + "input_data": json.dumps({ + "list_info": {"row_count": 100, "start_index": 1} + }) + }) + data = resp.json() + print(f"Status: {resp.status_code}") + print(f"Has more: {data.get('list_info', {}).get('has_more_rows')}") + requests = data.get("requests", []) + print(f"Total: {len(requests)}") + for r in requests: + print(f" ID={r.get('id')} subject={r.get('subject')} created={r.get('created_time')}") + +if __name__ == "__main__": + urllib3.disable_warnings() + asyncio.run(main()) diff --git a/service_desk/test_date_format.py b/service_desk/test_date_format.py new file mode 100644 index 0000000..5246195 --- /dev/null +++ b/service_desk/test_date_format.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Проверка формата даты и вариантов фильтрации.""" + +import asyncio +import httpx +import json +import urllib3 + +BASE_URL = "https://192.168.1.237:8080" +API_TOKEN = "062CE056-C1F5-490C-A6DF-FD2C653A71DD" + +async def debug(): + url = f"{BASE_URL}/api/v3/requests" + headers = { + "authtoken": API_TOKEN, + "Accept": "application/vnd.manageengine.sdp.v3+json" + } + + # Вариант 1: без фильтра, посмотреть все поля + print("=== Все заявки (без фильтра) ===") + async with httpx.AsyncClient(verify=False, timeout=30.0) as client: + resp = await client.get(url, headers=headers, params={ + "input_data": json.dumps({ + "list_info": {"row_count": 100, "start_index": 1} + }) + }) + data = resp.json() + requests = data.get("requests", []) + + for req in requests: + print(f"\n--- Заявка {req.get('request_id', '?')} ---") + print(f" subject: {req.get('subject')}") + print(f" created_time: {req.get('created_time')}") + print(f" created_time type: {type(req.get('created_time')).__name__}") + # Показать все ключи + print(f" Ключи: {list(req.keys())}") + +if __name__ == "__main__": + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + asyncio.run(debug()) diff --git a/service_desk/test_filter.py b/service_desk/test_filter.py new file mode 100644 index 0000000..c0039a5 --- /dev/null +++ b/service_desk/test_filter.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Поиск рабочего формата фильтрации.""" + +import asyncio +import httpx +import json +import urllib3 + +BASE_URL = "https://192.168.1.237:8080" +API_TOKEN = "062CE056-C1F5-490C-A6DF-FD2C653A71DD" + +async def test_filters(): + url = f"{BASE_URL}/api/v3/requests" + headers = { + "authtoken": API_TOKEN, + "Accept": "application/vnd.manageengine.sdp.v3+json" + } + + # Сначала посмотрим все заявки с их ID и датами + print("=== Все заявки ===") + async with httpx.AsyncClient(verify=False, timeout=30.0) as client: + resp = await client.get(url, headers=headers, params={ + "input_data": json.dumps({ + "list_info": {"row_count": 100, "start_index": 1} + }) + }) + data = resp.json() + requests = data.get("requests", []) + + for req in requests: + req_id = req.get("request_id", req.get("id", "?")) + subject = req.get("subject", "?") + created = req.get("created_time", {}) + if isinstance(created, dict): + created_str = created.get("display_value", "?") + ts = created.get("value", "?") + else: + created_str = str(created) + ts = "?" + print(f" ID={req_id} created={created_str} ts={ts}") + + # Попробуем разные форматы фильтрации + print("\n=== Попробуем фильтры ===") + + filters = [ + # Вариант 1: ISO строка + { + "search_criteria": { + "column_name": "created_time", + "condition": "between", + "values": ["Jul 1, 2026 00:00:00", "Jul 31, 2026 23:59:59"] + } + }, + # Вариант 2: без search_criteria, с custom_field + { + "search_criteria": { + "column_name": "created_time", + "condition": "between", + "values": ["2026-07-01", "2026-07-31"] + } + }, + # Вариант 3: timestamp как строка + { + "search_criteria": { + "column_name": "created_time", + "condition": "between", + "values": ["1782864000000", "1785542399000"] + } + }, + ] + + for i, criteria in enumerate(filters): + params = { + "input_data": json.dumps({ + "list_info": {"row_count": 100, "start_index": 1}, + "search_criteria": criteria.get("search_criteria") + }) + } + resp = await client.get(url, headers=headers, params=params) + data = resp.json() + reqs = data.get("requests", []) + print(f"\nФильтр {i+1}: {criteria['search_criteria']['values']} -> {len(reqs)} заявок") + for r in reqs: + print(f" {r.get('id')}: {r.get('subject')}") + +if __name__ == "__main__": + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + asyncio.run(test_filters()) diff --git a/service_desk/test_list_tickets.py b/service_desk/test_list_tickets.py new file mode 100644 index 0000000..0bceb68 --- /dev/null +++ b/service_desk/test_list_tickets.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Получение списка заявок из ServiceDesk Plus через API v3.""" + +import asyncio +import httpx +import json +import urllib3 + +BASE_URL = "https://192.168.1.237:8080" +API_TOKEN = "062CE056-C1F5-490C-A6DF-FD2C653A71DD" + +async def list_tickets(): + url = f"{BASE_URL}/api/v3/requests" + headers = { + "authtoken": API_TOKEN, + "Accept": "application/vnd.manageengine.sdp.v3+json" + } + + params = { + "input_data": json.dumps({ + "list_info": { + "row_count": 50, + "start_index": 1 + } + }) + } + + async with httpx.AsyncClient(verify=False, timeout=30.0) as client: + resp = await client.get(url, headers=headers, params=params) + data = resp.json() + + requests = data.get("requests", []) + print(f"Всего заявок: {len(requests)}\n") + + for req in requests: + req_id = req.get("request_id", req.get("id", "?")) + subject = req.get("subject", req.get("short_description", "?")) + + # Группа + group = req.get("group", {}) + if isinstance(group, dict): + group_name = group.get("name", "?") + else: + group_name = str(group) + + # Статус + status = req.get("status", {}) + if isinstance(status, dict): + status_name = status.get("name", "?") + else: + status_name = str(status) + + # Приоритет + priority = req.get("priority", {}) + if isinstance(priority, dict): + priority_name = priority.get("name", "?") + else: + priority_name = str(priority) + + # Описание (убираем HTML) + desc = req.get("description", "") + desc_clean = desc.replace("
", "").replace("
", "").replace("{email}).\n\n"
+ f"⏳ Обычно это занимает от 5 до 30 минут.\n\n"
+ f"{_footer(emojis)}"
+ )
+
+def _transcription_api_error(status_code: int, emojis: dict) -> str:
+ return f"❌ Ошибка API сервера ({status_code}).{_footer(emojis)}"
+
+def _transcription_system_error(emojis: dict) -> str:
+ return f"❌ Произошла системная ошибка при передаче файла.{_footer(emojis)}"
+
+# --- Public constants and functions ---
+
+TRANSCRIPTION_MAIN_MENU_TEXT = _transcription_welcome(EMOJI_DIGITS)
+TRANSCRIPTION_UNKNOWN_CMD_TEXT = _transcription_unknown(EMOJI_DIGITS)
+
+def transcription_error_bad_extension(ext: str) -> str:
+ return _transcription_bad_extension(ext, EMOJI_DIGITS)
+
+TRANSCRIPTION_UPLOADING_TEXT = _transcription_uploading()
+
+def transcription_success_received(email: str) -> str:
+ return _transcription_success(email, EMOJI_DIGITS)
+
+def transcription_error_api(status_code: int) -> str:
+ return _transcription_api_error(status_code, EMOJI_DIGITS)
+
+def transcription_error_system() -> str:
+ return _transcription_system_error(EMOJI_DIGITS)
+
+
+# =========================================================
+# SERVICE DESK BOT
+# =========================================================
+# Модуль для создания и управления заявками в Service Desk
+
+# --- Private functions (Internal logic) ---
+
+def _sd_welcome(emojis: dict) -> str:
+ return (
+ f"🎫 Режим создания заявки в Service Desk\n\n"
+ f"• Опишите проблему текстом.\n"
+ f"• Можно прикрепить фото или документ.\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _sd_unknown(emojis: dict) -> str:
+ return (
+ f"⚠️ Неизвестная команда.\n"
+ f"Пожалуйста, используйте цифры меню из сообщения выше.\n\n"
+ f"{emojis['9']} — Назад\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _sd_text_required(emojis: dict) -> str:
+ return f"⚠️ Сначала опишите проблему текстом."
+
+def _sd_creating() -> str:
+ return "⏳ Создаю заявку, пожалуйста, подождите..."
+
+def _sd_ticket_created(ticket_id: int, subject: str, description: str, emojis: dict) -> str:
+ clean_desc = description.replace("\n", " ").strip()
+ if len(clean_desc) > 150:
+ clean_desc = clean_desc[:147] + "..."
+ return (
+ f"✅ Заявка #{ticket_id} создана!\n\n"
+ f"📌 Тема: {subject}\n\n"
+ f"📝 Текст: {clean_desc}\n\n"
+ f"Если нужно добавить скриншоты или документ — отправьте их сейчас.\n\n"
+ f"{_footer(emojis)}"
+ )
+
+def _sd_ticket_create_error(emojis: dict) -> str:
+ return f"❌ Ошибка создания заявки.{_footer_main_only(emojis)}"
+
+def _sd_system_error(emojis: dict) -> str:
+ return f"❌ Системная ошибка. Попробуйте позже.{_footer_main_only(emojis)}"
+
+def _sd_uploading_file(ticket_id: int, emojis: dict) -> str:
+ return f"⏳ Загружаю файл в заявку #{ticket_id}..."
+
+def _sd_file_attached(emojis: dict) -> str:
+ return f"✅ Файл успешно прикреплен!{_footer(emojis)}"
+
+def _sd_file_upload_error(emojis: dict) -> str:
+ return f"❌ Ошибка загрузки файла.{_footer_main_only(emojis)}"
+
+# --- Public constants and functions ---
+
+SD_MAIN_MENU_TEXT = _sd_welcome(EMOJI_DIGITS)
+SD_UNKNOWN_CMD_TEXT = _sd_unknown(EMOJI_DIGITS)
+SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS)
+SD_CREATING = _sd_creating()
+
+def sd_ticket_created(ticket_id: int, subject: str, description: str) -> str:
+ return _sd_ticket_created(ticket_id, subject, description, EMOJI_DIGITS)
+
+def sd_ticket_create_error() -> str:
+ return _sd_ticket_create_error(EMOJI_DIGITS)
+
+def sd_system_error() -> str:
+ return _sd_system_error(EMOJI_DIGITS)
+
+def sd_uploading_file(ticket_id: int) -> str:
+ return _sd_uploading_file(ticket_id, EMOJI_DIGITS)
+
+def sd_file_attached() -> str:
+ return _sd_file_attached(EMOJI_DIGITS)
+
+def sd_file_upload_error() -> str:
+ return _sd_file_upload_error(EMOJI_DIGITS)
+
+
+# =========================================================
+# PHOTO BOT
+# =========================================================
+# Модуль для обновления фото профиля
+
+# --- Private functions (Internal logic) ---
+
+def _photo_welcome(emojis: dict) -> str:
+ return (
+ f"🖼 Режим обновления фото профиля\n\n"
+ f"📸 Пришлите мне вашу фотографию.\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _photo_unknown(emojis: dict) -> str:
+ return (
+ f"⚠️ Неизвестная команда.\n"
+ f"Пожалуйста, используйте цифры меню из сообщения выше.\n\n"
+ f"{emojis['9']} — Назад\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _photo_error_handling(emojis: dict) -> str:
+ return (
+ f"⚠️ В данный момент выполнить это действие невозможно. "
+ f"Информация о проблеме передана ответственным.\n"
+ f"Приносим извинения за доставленные неудобства.\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _photo_uploading() -> str:
+ return "⏳ Отправка..."
+
+def _photo_success_sent() -> str:
+ return "📩 Фото отправлено на утверждение."
+
+def _photo_cancel(emojis: dict) -> str:
+ return (
+ f"🚫 Отменено. Пришлите другое фото.\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _photo_processing() -> str:
+ return "⏳ Обработка..."
+
+def _photo_preview_ready(emojis: dict) -> str:
+ return (
+ f"... Фото готово!\n\n"
+ f"{emojis['1']} — Отправить на утверждение\n"
+ f"{emojis['2']} — Попробовать другое\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _photo_error_processing(error_text: str, emojis: dict) -> str:
+ return (
+ f"❌ Ошибка: {error_text}\n\n"
+ f"Пожалуйста, отправьте другое фото.\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+# --- Public constants and functions ---
+
+PHOTO_MAIN_MENU_TEXT = _photo_welcome(EMOJI_DIGITS)
+PHOTO_UNKNOWN_CMD_TEXT = _photo_unknown(EMOJI_DIGITS)
+
+def photo_error_handling() -> str:
+ return _photo_error_handling(EMOJI_DIGITS)
+
+PHOTO_UPLOADING_TEXT = _photo_uploading()
+
+def photo_success_sent() -> str:
+ return _photo_success_sent()
+
+def photo_cancel_text() -> str:
+ return _photo_cancel(EMOJI_DIGITS)
+
+PHOTO_PROCESSING_TEXT = _photo_processing()
+
+def photo_preview_ready() -> str:
+ return _photo_preview_ready(EMOJI_DIGITS)
+
+def photo_error_processing(error_text: str) -> str:
+ return _photo_error_processing(error_text, EMOJI_DIGITS)
+
+
+# =========================================================
+# SEARCH BOT
+# =========================================================
+# Модуль поиска по регламентам
+
+# --- Private functions (Internal logic) ---
+
+def _search_welcome(emojis: dict) -> str:
+ return (
+ f"📚 Поиск по регламентам\n\n"
+ f"Введите ваш вопрос (например: 'время хранения электронных документов' ).\n"
+ f"Бот найдет наиболее подходящие абзацы из нормативных документов.\n\n"
+ f"Для возврата в главное меню отправьте {emojis['0']}."
+ )
+
+def _search_unknown(emojis: dict) -> str:
+ return (
+ f"⚠️ Неизвестная команда.\n"
+ f"Пожалуйста, используйте цифры меню из сообщения выше.\n\n"
+ f"{emojis['9']} — Назад\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+def _search_searching() -> str:
+ return "🔍 Ищу информацию в регламентах..."
+
+def _search_not_found(emojis: dict) -> str:
+ return (
+ f"🤖 Точной информации в регламентах не найдено.\n\n"
+ f"Для выхода отправьте {emojis['0']}."
+ )
+
+def _search_feedback_thanks(emojis: dict) -> str:
+ return (
+ f"🙏 Спасибо за ваш отзыв! Мы передали его разработчикам.\n\n"
+ f"🔍 Можете сразу задать следующий вопрос по регламентам.\n\n"
+ f"Для выхода отправьте {emojis['0']}."
+ )
+
+def _search_feedback_info(emojis: dict) -> str:
+ return (
+ f"⚠️ Информация передана.\n\n"
+ f"Если желаете, напишите прямо сейчас текстовым сообщением, что конкретно вам не понравилось в ответе.\n\n"
+ f"{emojis['1']} — Задать ещё вопрос\n"
+ f"{emojis['0']} — Выйти в главное меню"
+ )
+
+def _search_unavailable(emojis: dict) -> str:
+ return (
+ "⚠️ В данный момент выполнить это действие невозможно. "
+ "Информация о проблеме передана ответственным.\n"
+ "Приносим извинения за доставленные неудобства.\n\n"
+ f"{emojis['0']} — В главное меню"
+ )
+
+# --- Public constants and functions ---
+
+SEARCH_MAIN_MENU_TEXT = _search_welcome(EMOJI_DIGITS)
+SEARCH_UNKNOWN_CMD_TEXT = _search_unknown(EMOJI_DIGITS)
+
+def search_searching_text() -> str:
+ return _search_searching()
+
+def search_not_found_text(emojis: dict) -> str:
+ return _search_not_found(emojis)
+
+def search_feedback_thanks_text(emojis: dict) -> str:
+ return _search_feedback_thanks(emojis)
+
+def search_feedback_info_text(emojis: dict) -> str:
+ return _search_feedback_info(emojis)
+
+def search_unavailable_action(emojis: dict) -> str:
+ return _search_unavailable(emojis)
+
+
+# =========================================================
+# LK BOT
+# =========================================================
+# Модуль Личного Кабинета (финансовые документы, табель и др.)
+
+# --- Constants ---
+
+DOC_TYPES = {
+ "1": "Справка о доходах физического лица (2-НДФЛ)",
+ "2": "Справка об удержаниях за ДМС",
+ "3": "Справка о средней заработной плате",
+ "4": "Справка о получении единовременного пособия по рождению ребенка",
+ "5": "Справка о получении ежемесячного пособия и назначении отпуска по уходу за ребенком",
+ "6": "Справка об удержаниях по исполнительным листам",
+ "7": "Копия трудовой книжки"
+}
+
+# --- Private functions (Internal logic) ---
+
+def _lk_error_handling(emojis: dict) -> str:
+ return (
+ "⚠️ В данный момент выполнить это действие невозможно. Информация о проблеме передана ответственным. Приносим извинения за доставленные неудобства.\n\n"
+ f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню"
+ )
+
+def _lk_auth_processing(emojis: dict) -> str:
+ return "⏳ Проверка безопасности... Запрашиваю контакты авторизации..."
+
+def _lk_auth_success(emojis: dict) -> str:
+ return "⏳ Авторизация успешна. Загружаю детализацию расчетного листа..."
+
+def _lk_auth_2fa_expired(emojis: dict) -> str:
+ return f"⏱ Время действия кода подтверждения изменилось или истекло.\nПожалуйста, запросите расчетный лист заново.{emojis['9']} — Назад"
+
+def _lk_auth_2fa_wrong_code(emojis: dict, attempts_left: int) -> str:
+ return (
+ f"⚠️ Неверный код подтверждения.\nОсталось попыток: {attempts_left}. Попробуйте еще раз или вернитесь назад.\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню"
+ )
+
+def _lk_auth_2fa_blocked_max(emojis: dict, minutes: int) -> str:
+ return (
+ f"❌ Превышено максимальное количество попыток ввода кода.\nДоступ к финансовым документам заблокирован на {minutes} минут.