Files
aiform_prod/backend/app/services/n8n_service.py
AI Assistant 2e45786e46 feat: Telegram Mini App integration and UX improvements
- Добавлена полная интеграция с Telegram Mini App (динамическая загрузка SDK)
- Отдельный компактный дизайн для Telegram Mini App
- Добавлен loader при инициализации (предотвращает мелькание SMS-авторизации)
- Улучшена навигация: кнопки "Назад" и "К списку заявок" теперь сохраняют авторизацию
- Telegram Mini App: кнопка "Выход" просто закрывает приложение
- Telegram Mini App: заявки "В работе" скрыты из списка
- Веб-версия: для заявок "В работе" добавлена кнопка "Просмотреть в Telegram" (ссылка на @klientprav_bot)
- Telegram Mini App: кнопки действий в черновиках расположены вертикально
- Веб-версия: убрано отображение номера телефона в приветствии
- Исправлена проблема с возвратом к списку черновиков (не требует повторной SMS-авторизации)
- Заблокировано удаление и редактирование заявок со статусом "В работе"
- Добавлена документация по Telegram Mini App интеграции
2026-01-29 16:12:48 +03:00

217 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Сервис для работы с n8n API
"""
import httpx
import logging
from typing import Optional
from ..config import settings
from ..services.redis_service import redis_service
logger = logging.getLogger(__name__)
# Workflow ID для ticket_form:description
WORKFLOW_ID = "b4K4u851b4JFivyD"
N8N_URL = "https://n8n.clientright.pro"
MIN_RESTART_INTERVAL = 300 # Минимум 5 минут между перезапусками
MAX_RETRY_ATTEMPTS = 2 # Максимум попыток перезапуска подряд
async def check_workflow_status() -> Optional[dict]:
"""
Проверка статуса workflow через n8n API
Returns:
dict с данными workflow или None при ошибке
"""
if not settings.n8n_api_key:
logger.warning("⚠️ N8N_API_KEY не настроен")
return None
headers = _get_headers()
if not headers:
return None
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
f"{N8N_URL}/api/v1/workflows/{WORKFLOW_ID}",
headers=headers
)
if response.status_code == 200:
return response.json()
else:
logger.warning(f"⚠️ n8n API вернул статус {response.status_code}")
return None
except Exception as e:
logger.error(f"❌ Ошибка при проверке статуса workflow: {e}")
return None
async def restart_workflow() -> bool:
"""
Перезапуск workflow через n8n API с улучшенной обработкой зависших состояний
Returns:
True если успешно, False при ошибке
"""
if not settings.n8n_api_key:
logger.error("❌ N8N_API_KEY не настроен! Не могу перезапустить workflow")
return False
headers = _get_headers()
if not headers:
return False
import asyncio
try:
# Увеличиваем таймаут для обработки зависших workflow
async with httpx.AsyncClient(timeout=30.0) as client:
# Шаг 1: Проверяем текущий статус
logger.info(f"🔍 Проверяю текущий статус workflow {WORKFLOW_ID}...")
status_response = await client.get(
f"{N8N_URL}/api/v1/workflows/{WORKFLOW_ID}",
headers=headers
)
if status_response.status_code == 200:
workflow_data = status_response.json()
is_active = workflow_data.get("active", False)
logger.info(f"📊 Workflow активен: {is_active}")
# Шаг 2: Деактивировать workflow (даже если уже неактивен - для сброса состояния)
logger.info(f"🔄 Деактивирую workflow {WORKFLOW_ID}...")
try:
deactivate_response = await client.post(
f"{N8N_URL}/api/v1/workflows/{WORKFLOW_ID}/deactivate",
headers=headers,
timeout=15.0 # Отдельный таймаут для деактивации
)
if deactivate_response.status_code in [200, 404]:
logger.info("✅ Workflow деактивирован")
else:
logger.warning(
f"⚠️ Неожиданный статус при деактивации: "
f"{deactivate_response.status_code} - {deactivate_response.text[:200]}"
)
# Продолжаем даже если деактивация не удалась - возможно workflow уже неактивен
except httpx.TimeoutException:
logger.warning("⏱️ Таймаут при деактивации workflow (возможно завис)")
# Продолжаем попытку активации - иногда помогает
except Exception as e:
logger.warning(f"⚠️ Ошибка при деактивации: {e}, продолжаю...")
# Задержка перед активацией (увеличена для стабильности)
await asyncio.sleep(3)
# Шаг 3: Активировать workflow
logger.info(f"🔄 Активирую workflow {WORKFLOW_ID}...")
try:
activate_response = await client.post(
f"{N8N_URL}/api/v1/workflows/{WORKFLOW_ID}/activate",
headers=headers,
timeout=15.0 # Отдельный таймаут для активации
)
if activate_response.status_code == 200:
logger.info("✅ Workflow активирован")
# Дополнительная задержка для инициализации trigger node
await asyncio.sleep(2)
# После успешного перезапуска отправляем сообщения из буфера
await _send_buffered_messages()
return True
else:
logger.error(
f"❌ Ошибка активации workflow: "
f"{activate_response.status_code} - {activate_response.text[:200]}"
)
return False
except httpx.TimeoutException:
logger.error("⏱️ Таймаут при активации workflow - возможно n8n перегружен")
return False
except Exception as e:
logger.error(f"❌ Ошибка при активации workflow: {e}")
return False
except httpx.TimeoutException:
logger.error("⏱️ Общий таймаут при перезапуске workflow")
return False
except Exception as e:
logger.error(f"❌ Неожиданная ошибка при перезапуске workflow: {e}", exc_info=True)
return False
async def _send_buffered_messages():
"""
Отправить все сообщения из буфера после восстановления workflow
"""
try:
buffer_key = "description" # Буфер для ticket_form:description
messages = await redis_service.buffer_get_all(buffer_key)
if not messages:
logger.info("📭 Буфер пуст, нечего отправлять")
return
logger.info(f"📤 Отправляю {len(messages)} сообщений из буфера...")
import json
channel = f"{settings.redis_prefix}description"
sent_count = 0
failed_count = 0
for message in messages:
try:
event_json = json.dumps(message.get("event", message), ensure_ascii=False)
subscribers = await redis_service.publish(channel, event_json)
if subscribers > 0:
sent_count += 1
logger.info(
f"✅ Буферированное сообщение отправлено: "
f"session_id={message.get('session_id', 'unknown')}, "
f"subscribers={subscribers}"
)
else:
failed_count += 1
logger.warning(
f"⚠️ Буферированное сообщение не доставлено "
f"(подписчиков нет): session_id={message.get('session_id', 'unknown')}"
)
# Возвращаем обратно в буфер если не доставлено
await redis_service.buffer_push(buffer_key, message)
except Exception as e:
failed_count += 1
logger.error(f"❌ Ошибка отправки буферизованного сообщения: {e}")
# Возвращаем обратно в буфер
await redis_service.buffer_push(buffer_key, message)
logger.info(
f"📊 Результат отправки буфера: {sent_count} отправлено, {failed_count} не доставлено"
)
except Exception as e:
logger.exception(f"❌ Ошибка при отправке буферизованных сообщений: {e}")
def _get_headers() -> Optional[dict]:
"""Получить заголовки для n8n API"""
if not settings.n8n_api_key:
return None
api_key = settings.n8n_api_key
# Убираем "Bearer " если есть - n8n API использует X-N8N-API-KEY
clean_key = api_key.replace("Bearer ", "").strip()
# n8n API принимает ключ в заголовке X-N8N-API-KEY
return {"X-N8N-API-KEY": clean_key}