smart-bot-factory 0.2.10__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/cli.py +35 -64
- smart_bot_factory/core/bot_utils.py +69 -21
- smart_bot_factory/core/decorators.py +377 -143
- smart_bot_factory/core/router.py +7 -4
- smart_bot_factory/creation/bot_builder.py +47 -14
- smart_bot_factory/handlers/handlers.py +5 -4
- smart_bot_factory/router/__init__.py +2 -3
- smart_bot_factory-0.3.1.dist-info/METADATA +905 -0
- {smart_bot_factory-0.2.10.dist-info → smart_bot_factory-0.3.1.dist-info}/RECORD +12 -13
- smart_bot_factory/core/telegram_router.py +0 -58
- smart_bot_factory-0.2.10.dist-info/METADATA +0 -789
- {smart_bot_factory-0.2.10.dist-info → smart_bot_factory-0.3.1.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.2.10.dist-info → smart_bot_factory-0.3.1.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.2.10.dist-info → smart_bot_factory-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,6 +11,38 @@ from functools import wraps
|
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
+
def format_seconds_to_human(seconds: int) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Форматирует секунды в человекочитаемый формат
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
seconds: Количество секунд
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
str: Человекочитаемое время
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
format_seconds_to_human(3600) -> "1ч 0м"
|
|
26
|
+
format_seconds_to_human(5445) -> "1ч 30м"
|
|
27
|
+
format_seconds_to_human(102461) -> "1д 4ч 28м"
|
|
28
|
+
"""
|
|
29
|
+
if seconds < 60:
|
|
30
|
+
return f"{seconds}с"
|
|
31
|
+
|
|
32
|
+
days = seconds // 86400
|
|
33
|
+
hours = (seconds % 86400) // 3600
|
|
34
|
+
minutes = (seconds % 3600) // 60
|
|
35
|
+
|
|
36
|
+
parts = []
|
|
37
|
+
if days > 0:
|
|
38
|
+
parts.append(f"{days}д")
|
|
39
|
+
if hours > 0:
|
|
40
|
+
parts.append(f"{hours}ч")
|
|
41
|
+
if minutes > 0:
|
|
42
|
+
parts.append(f"{minutes}м")
|
|
43
|
+
|
|
44
|
+
return " ".join(parts) if parts else "0м"
|
|
45
|
+
|
|
14
46
|
def parse_time_string(time_str: Union[str, int]) -> int:
|
|
15
47
|
"""
|
|
16
48
|
Парсит время в удобном формате и возвращает секунды
|
|
@@ -403,7 +435,7 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
|
|
|
403
435
|
return wrapper
|
|
404
436
|
return decorator
|
|
405
437
|
|
|
406
|
-
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: str = None):
|
|
438
|
+
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None):
|
|
407
439
|
"""
|
|
408
440
|
Декоратор для регистрации задачи, которую можно запланировать на время
|
|
409
441
|
|
|
@@ -413,7 +445,9 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
413
445
|
smart_check: Использовать ли умную проверку активности пользователя (по умолчанию True)
|
|
414
446
|
once_only: Выполнять ли задачу только один раз (по умолчанию True)
|
|
415
447
|
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
416
|
-
event_type:
|
|
448
|
+
event_type: Источник времени события - ОПЦИОНАЛЬНО:
|
|
449
|
+
- str: Тип события для поиска в БД (например, 'appointment_booking')
|
|
450
|
+
- Callable: Функция для получения datetime (например, async def(user_id, user_data) -> datetime)
|
|
417
451
|
|
|
418
452
|
Example:
|
|
419
453
|
# Обычная задача с фиксированным временем
|
|
@@ -422,17 +456,25 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
422
456
|
# Задача будет запланирована на 1 час 30 минут
|
|
423
457
|
return {"status": "sent", "message": user_data}
|
|
424
458
|
|
|
425
|
-
# Напоминание о событии (за delay времени до события)
|
|
459
|
+
# Напоминание о событии из БД (за delay времени до события)
|
|
426
460
|
@schedule_task("appointment_reminder", delay="2h", event_type="appointment_booking")
|
|
427
461
|
async def appointment_reminder(user_id: int, user_data: str):
|
|
428
|
-
#
|
|
462
|
+
# Ищет событие "appointment_booking" в БД
|
|
463
|
+
# Напоминание будет за 2 часа до времени из события
|
|
429
464
|
return {"status": "sent", "message": user_data}
|
|
430
465
|
|
|
431
|
-
# Напоминание
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
466
|
+
# Напоминание с кастомной функцией получения времени
|
|
467
|
+
async def get_yclients_appointment_time(user_id: int, user_data: str) -> datetime:
|
|
468
|
+
'''Получает время записи из YClients API'''
|
|
469
|
+
from yclients_api import get_next_booking
|
|
470
|
+
booking = await get_next_booking(user_id)
|
|
471
|
+
return booking['datetime'] # datetime объект
|
|
472
|
+
|
|
473
|
+
@schedule_task("yclients_reminder", delay="1h", event_type=get_yclients_appointment_time)
|
|
474
|
+
async def yclients_reminder(user_id: int, user_data: str):
|
|
475
|
+
# Вызовет get_yclients_appointment_time(user_id, user_data)
|
|
476
|
+
# Напоминание будет за 1 час до возвращенного datetime
|
|
477
|
+
return {"status": "sent"}
|
|
436
478
|
|
|
437
479
|
# Форматы времени:
|
|
438
480
|
# delay="1h 30m 45s" - 1 час 30 минут 45 секунд
|
|
@@ -442,7 +484,7 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
442
484
|
|
|
443
485
|
# ИИ может передавать только данные (текст):
|
|
444
486
|
# {"тип": "send_reminder", "инфо": "Текст напоминания"} - только текст
|
|
445
|
-
# {"тип": "appointment_reminder", "инфо": ""} - пустой текст, время берется из
|
|
487
|
+
# {"тип": "appointment_reminder", "инфо": ""} - пустой текст, время берется из события/функции
|
|
446
488
|
"""
|
|
447
489
|
def decorator(func: Callable) -> Callable:
|
|
448
490
|
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
@@ -501,7 +543,7 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
501
543
|
return wrapper
|
|
502
544
|
return decorator
|
|
503
545
|
|
|
504
|
-
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None):
|
|
546
|
+
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None):
|
|
505
547
|
"""
|
|
506
548
|
Декоратор для регистрации глобального обработчика (для всех пользователей)
|
|
507
549
|
|
|
@@ -510,6 +552,9 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
510
552
|
notify: Уведомлять ли админов о выполнении (по умолчанию False)
|
|
511
553
|
once_only: Выполнять ли обработчик только один раз (по умолчанию True)
|
|
512
554
|
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
555
|
+
event_type: Источник времени события - ОПЦИОНАЛЬНО:
|
|
556
|
+
- str: Тип события для поиска в БД
|
|
557
|
+
- Callable: Функция для получения datetime (например, async def(handler_data: str) -> datetime)
|
|
513
558
|
|
|
514
559
|
Example:
|
|
515
560
|
# Глобальный обработчик с задержкой
|
|
@@ -524,6 +569,18 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
524
569
|
# Может запускаться каждый день через 24 часа
|
|
525
570
|
return {"status": "sent", "report_type": "daily"}
|
|
526
571
|
|
|
572
|
+
# С кастомной функцией для получения времени
|
|
573
|
+
async def get_promo_end_time(handler_data: str) -> datetime:
|
|
574
|
+
'''Получает время окончания акции из CRM'''
|
|
575
|
+
from crm_api import get_active_promo
|
|
576
|
+
promo = await get_active_promo()
|
|
577
|
+
return promo['end_datetime']
|
|
578
|
+
|
|
579
|
+
@global_handler("promo_ending_notification", delay="2h", event_type=get_promo_end_time)
|
|
580
|
+
async def notify_promo_ending(handler_data: str):
|
|
581
|
+
# Уведомление за 2 часа до окончания акции
|
|
582
|
+
return {"status": "sent"}
|
|
583
|
+
|
|
527
584
|
# Форматы времени:
|
|
528
585
|
# delay="1h 30m 45s" - 1 час 30 минут 45 секунд
|
|
529
586
|
# delay="2h" - 2 часа
|
|
@@ -532,7 +589,7 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
532
589
|
|
|
533
590
|
# ИИ может передавать только данные (текст):
|
|
534
591
|
# {"тип": "global_announcement", "инфо": "Важное объявление!"} - только текст
|
|
535
|
-
# {"тип": "global_announcement", "инфо": ""} - пустой
|
|
592
|
+
# {"тип": "global_announcement", "инфо": ""} - пустой текст, время из функции
|
|
536
593
|
"""
|
|
537
594
|
def decorator(func: Callable) -> Callable:
|
|
538
595
|
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
@@ -552,7 +609,8 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
552
609
|
'name': func.__name__,
|
|
553
610
|
'notify': notify,
|
|
554
611
|
'once_only': once_only,
|
|
555
|
-
'default_delay': default_delay_seconds
|
|
612
|
+
'default_delay': default_delay_seconds,
|
|
613
|
+
'event_type': event_type # Добавляем event_type для глобальных обработчиков
|
|
556
614
|
}
|
|
557
615
|
|
|
558
616
|
logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
|
|
@@ -755,76 +813,107 @@ async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_
|
|
|
755
813
|
if default_delay is None:
|
|
756
814
|
raise ValueError(f"Для задачи '{task_name}' не указано время в декораторе (параметр delay)")
|
|
757
815
|
|
|
758
|
-
# event_info содержит только текст для задачи
|
|
759
|
-
user_data = event_info.strip() if event_info else
|
|
816
|
+
# event_info содержит только текст для задачи (если ИИ не передал - пустая строка)
|
|
817
|
+
user_data = event_info.strip() if event_info else ""
|
|
760
818
|
|
|
761
819
|
# Если указан event_type, то это напоминание о событии
|
|
762
820
|
if event_type:
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
# Получаем клиент Supabase
|
|
766
|
-
supabase_client = get_supabase_client()
|
|
767
|
-
if not supabase_client:
|
|
768
|
-
raise RuntimeError("Supabase клиент не найден для получения времени события")
|
|
821
|
+
event_datetime = None
|
|
769
822
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
823
|
+
# ========== ПРОВЕРЯЕМ ТИП event_type: СТРОКА ИЛИ ФУНКЦИЯ ==========
|
|
824
|
+
if callable(event_type):
|
|
825
|
+
# ВАРИАНТ 2: Функция - вызываем для получения datetime
|
|
826
|
+
logger.info(f"⏰ Задача '{task_name}' - вызываем функцию для получения времени события")
|
|
773
827
|
|
|
774
|
-
|
|
775
|
-
|
|
828
|
+
try:
|
|
829
|
+
# Вызываем функцию пользователя с теми же аргументами что у обработчика
|
|
830
|
+
event_datetime = await event_type(user_id, user_data)
|
|
831
|
+
|
|
832
|
+
if not isinstance(event_datetime, datetime):
|
|
833
|
+
raise ValueError(f"Функция event_type должна вернуть datetime, получен {type(event_datetime)}")
|
|
834
|
+
|
|
835
|
+
logger.info(f"✅ Функция вернула время события: {event_datetime}")
|
|
836
|
+
|
|
837
|
+
except Exception as e:
|
|
838
|
+
logger.error(f"❌ Ошибка в функции event_type: {e}")
|
|
776
839
|
# Fallback - планируем через default_delay
|
|
777
840
|
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
778
841
|
return result
|
|
842
|
+
|
|
843
|
+
else:
|
|
844
|
+
# ВАРИАНТ 1: Строка - ищем событие в БД (текущая логика)
|
|
845
|
+
logger.info(f"⏰ Задача '{task_name}' - напоминание о событии '{event_type}' за {default_delay}с")
|
|
779
846
|
|
|
780
|
-
#
|
|
781
|
-
|
|
847
|
+
# Получаем клиент Supabase
|
|
848
|
+
supabase_client = get_supabase_client()
|
|
849
|
+
if not supabase_client:
|
|
850
|
+
raise RuntimeError("Supabase клиент не найден для получения времени события")
|
|
782
851
|
|
|
783
|
-
|
|
784
|
-
|
|
852
|
+
try:
|
|
853
|
+
# Получаем данные события из БД
|
|
854
|
+
event_data_str = await supabase_client.get_last_event_info_by_user_and_type(user_id, event_type)
|
|
855
|
+
|
|
856
|
+
if not event_data_str:
|
|
857
|
+
logger.warning(f"Событие '{event_type}' не найдено для пользователя {user_id}")
|
|
858
|
+
# Fallback - планируем через default_delay
|
|
859
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
860
|
+
return result
|
|
861
|
+
|
|
862
|
+
# Парсим данные события
|
|
863
|
+
event_data = parse_appointment_data(event_data_str)
|
|
864
|
+
|
|
865
|
+
if 'datetime' not in event_data:
|
|
866
|
+
logger.warning(f"Не удалось распарсить дату/время из события '{event_type}'")
|
|
867
|
+
# Fallback - планируем через default_delay
|
|
868
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
869
|
+
return result
|
|
870
|
+
|
|
871
|
+
event_datetime = event_data['datetime']
|
|
872
|
+
logger.info(f"✅ Получено время события из БД: {event_datetime}")
|
|
873
|
+
|
|
874
|
+
except Exception as e:
|
|
875
|
+
logger.error(f"❌ Ошибка получения события из БД: {e}")
|
|
785
876
|
# Fallback - планируем через default_delay
|
|
786
877
|
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
787
878
|
return result
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
# Fallback - планируем через default_delay
|
|
823
|
-
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
824
|
-
return result
|
|
879
|
+
|
|
880
|
+
# ========== ОБЩАЯ ЛОГИКА ДЛЯ ОБОИХ ВАРИАНТОВ ==========
|
|
881
|
+
# Теперь у нас есть event_datetime (из БД или из функции)
|
|
882
|
+
now = datetime.now()
|
|
883
|
+
|
|
884
|
+
# Вычисляем время напоминания (за default_delay до события)
|
|
885
|
+
reminder_datetime = event_datetime - timedelta(seconds=default_delay)
|
|
886
|
+
|
|
887
|
+
# Проверяем, не в прошлом ли напоминание
|
|
888
|
+
if reminder_datetime <= now:
|
|
889
|
+
logger.warning(f"Напоминание уже в прошлом, отправляем немедленно")
|
|
890
|
+
# Выполняем задачу немедленно
|
|
891
|
+
result = await execute_scheduled_task(task_name, user_id, user_data)
|
|
892
|
+
return {
|
|
893
|
+
"status": "executed_immediately",
|
|
894
|
+
"task_name": task_name,
|
|
895
|
+
"reason": "reminder_time_passed",
|
|
896
|
+
"event_datetime": event_datetime.isoformat(),
|
|
897
|
+
"result": result
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
# Вычисляем задержку до напоминания
|
|
901
|
+
delay_seconds = int((reminder_datetime - now).total_seconds())
|
|
902
|
+
|
|
903
|
+
event_source = "функции" if callable(task_info.get('event_type')) else f"события '{event_type}'"
|
|
904
|
+
human_time = format_seconds_to_human(delay_seconds)
|
|
905
|
+
logger.info(f"⏰ Планируем напоминание '{task_name}' за {format_seconds_to_human(default_delay)} до {event_source} (через {human_time} / {delay_seconds}с)")
|
|
906
|
+
|
|
907
|
+
# Планируем напоминание
|
|
908
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds, session_id)
|
|
909
|
+
result['event_datetime'] = event_datetime.isoformat()
|
|
910
|
+
result['reminder_type'] = 'event_reminder'
|
|
911
|
+
|
|
912
|
+
return result
|
|
825
913
|
else:
|
|
826
914
|
# Обычная задача с фиксированным временем
|
|
827
|
-
|
|
915
|
+
human_time = format_seconds_to_human(default_delay)
|
|
916
|
+
logger.info(f"⏰ Планируем задачу '{task_name}' через {human_time} ({default_delay}с) с текстом: '{user_data}'")
|
|
828
917
|
|
|
829
918
|
# Планируем задачу на фоне с сохранением в БД
|
|
830
919
|
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
@@ -877,7 +966,7 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
|
|
|
877
966
|
|
|
878
967
|
Args:
|
|
879
968
|
handler_type: Тип глобального обработчика
|
|
880
|
-
event_info: Информация от ИИ (только текст, время задается в декораторе)
|
|
969
|
+
event_info: Информация от ИИ (только текст, время задается в декораторе или функции)
|
|
881
970
|
"""
|
|
882
971
|
router_manager = get_router_manager()
|
|
883
972
|
if router_manager:
|
|
@@ -890,20 +979,87 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
|
|
|
890
979
|
|
|
891
980
|
handler_info = global_handlers[handler_type]
|
|
892
981
|
default_delay = handler_info.get('default_delay')
|
|
982
|
+
event_type = handler_info.get('event_type')
|
|
893
983
|
|
|
894
984
|
# Время всегда берется из декоратора, ИИ может передавать только текст
|
|
895
985
|
if default_delay is None:
|
|
896
986
|
raise ValueError(f"Для глобального обработчика '{handler_type}' не указано время в декораторе (параметр delay)")
|
|
897
987
|
|
|
898
|
-
# event_info содержит только текст для обработчика
|
|
899
|
-
handler_data = event_info.strip() if event_info else
|
|
988
|
+
# event_info содержит только текст для обработчика (если ИИ не передал - пустая строка)
|
|
989
|
+
handler_data = event_info.strip() if event_info else ""
|
|
900
990
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
991
|
+
# Если указан event_type, вычисляем время относительно события
|
|
992
|
+
if event_type:
|
|
993
|
+
event_datetime = None
|
|
994
|
+
|
|
995
|
+
# Проверяем тип event_type: строка или функция
|
|
996
|
+
if callable(event_type):
|
|
997
|
+
# ВАРИАНТ 2: Функция - вызываем для получения datetime
|
|
998
|
+
logger.info(f"🌍 Глобальный обработчик '{handler_type}' - вызываем функцию для получения времени")
|
|
999
|
+
|
|
1000
|
+
try:
|
|
1001
|
+
# Вызываем функцию (только с handler_data для глобальных)
|
|
1002
|
+
event_datetime = await event_type(handler_data)
|
|
1003
|
+
|
|
1004
|
+
if not isinstance(event_datetime, datetime):
|
|
1005
|
+
raise ValueError(f"Функция event_type должна вернуть datetime, получен {type(event_datetime)}")
|
|
1006
|
+
|
|
1007
|
+
logger.info(f"✅ Функция вернула время события: {event_datetime}")
|
|
1008
|
+
|
|
1009
|
+
except Exception as e:
|
|
1010
|
+
logger.error(f"❌ Ошибка в функции event_type: {e}")
|
|
1011
|
+
# Fallback - планируем через default_delay
|
|
1012
|
+
result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
|
|
1013
|
+
return result
|
|
1014
|
+
|
|
1015
|
+
else:
|
|
1016
|
+
# ВАРИАНТ 1: Строка - ищем в БД (можно расширить логику если нужно)
|
|
1017
|
+
logger.info(f"🌍 Глобальный обработчик '{handler_type}' - event_type '{event_type}' (строка)")
|
|
1018
|
+
# Для глобальных обработчиков пока просто используем default_delay
|
|
1019
|
+
# Можно расширить логику если понадобится
|
|
1020
|
+
result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
|
|
1021
|
+
return result
|
|
1022
|
+
|
|
1023
|
+
# Общая логика для функций
|
|
1024
|
+
now = datetime.now()
|
|
1025
|
+
|
|
1026
|
+
# Вычисляем время напоминания (за default_delay до события)
|
|
1027
|
+
reminder_datetime = event_datetime - timedelta(seconds=default_delay)
|
|
1028
|
+
|
|
1029
|
+
# Проверяем, не в прошлом ли напоминание
|
|
1030
|
+
if reminder_datetime <= now:
|
|
1031
|
+
logger.warning(f"Напоминание глобального события уже в прошлом, выполняем немедленно")
|
|
1032
|
+
# Выполняем немедленно
|
|
1033
|
+
result = await execute_global_handler(handler_type, handler_data)
|
|
1034
|
+
return {
|
|
1035
|
+
"status": "executed_immediately",
|
|
1036
|
+
"handler_type": handler_type,
|
|
1037
|
+
"reason": "reminder_time_passed",
|
|
1038
|
+
"event_datetime": event_datetime.isoformat(),
|
|
1039
|
+
"result": result
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
# Вычисляем задержку до напоминания
|
|
1043
|
+
delay_seconds = int((reminder_datetime - now).total_seconds())
|
|
1044
|
+
|
|
1045
|
+
human_time = format_seconds_to_human(delay_seconds)
|
|
1046
|
+
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' за {format_seconds_to_human(default_delay)} до события (через {human_time} / {delay_seconds}с)")
|
|
1047
|
+
|
|
1048
|
+
# Планируем обработчик
|
|
1049
|
+
result = await schedule_global_handler_for_later_with_db(handler_type, delay_seconds, handler_data)
|
|
1050
|
+
result['event_datetime'] = event_datetime.isoformat()
|
|
1051
|
+
result['reminder_type'] = 'global_event_reminder'
|
|
1052
|
+
|
|
1053
|
+
return result
|
|
905
1054
|
|
|
906
|
-
|
|
1055
|
+
else:
|
|
1056
|
+
# Обычный глобальный обработчик с фиксированной задержкой
|
|
1057
|
+
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {default_delay}с с данными: '{handler_data}'")
|
|
1058
|
+
|
|
1059
|
+
# Планируем обработчик на фоне с сохранением в БД
|
|
1060
|
+
result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
|
|
1061
|
+
|
|
1062
|
+
return result
|
|
907
1063
|
|
|
908
1064
|
|
|
909
1065
|
# =============================================================================
|
|
@@ -941,11 +1097,17 @@ async def save_immediate_event(
|
|
|
941
1097
|
raise RuntimeError("Supabase клиент не инициализирован")
|
|
942
1098
|
|
|
943
1099
|
# Проверяем, нужно ли предотвращать дублирование
|
|
944
|
-
|
|
1100
|
+
router_manager = get_router_manager()
|
|
1101
|
+
if router_manager:
|
|
1102
|
+
event_handlers = router_manager.get_event_handlers()
|
|
1103
|
+
else:
|
|
1104
|
+
event_handlers = _event_handlers
|
|
1105
|
+
|
|
1106
|
+
event_handler_info = event_handlers.get(event_type, {})
|
|
945
1107
|
once_only = event_handler_info.get('once_only', True)
|
|
946
1108
|
|
|
947
1109
|
if once_only:
|
|
948
|
-
# Проверяем, было ли уже обработано аналогичное событие
|
|
1110
|
+
# Проверяем, было ли уже обработано аналогичное событие для этого пользователя
|
|
949
1111
|
already_processed = await check_event_already_processed(event_type, user_id, session_id)
|
|
950
1112
|
if already_processed:
|
|
951
1113
|
logger.info(f"🔄 Событие '{event_type}' уже обрабатывалось для пользователя {user_id}, пропускаем")
|
|
@@ -985,11 +1147,17 @@ async def save_scheduled_task(
|
|
|
985
1147
|
raise RuntimeError("Supabase клиент не инициализирован")
|
|
986
1148
|
|
|
987
1149
|
# Проверяем, нужно ли предотвращать дублирование
|
|
988
|
-
|
|
1150
|
+
router_manager = get_router_manager()
|
|
1151
|
+
if router_manager:
|
|
1152
|
+
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
1153
|
+
else:
|
|
1154
|
+
scheduled_tasks = _scheduled_tasks
|
|
1155
|
+
|
|
1156
|
+
task_info = scheduled_tasks.get(task_name, {})
|
|
989
1157
|
once_only = task_info.get('once_only', True)
|
|
990
1158
|
|
|
991
1159
|
if once_only:
|
|
992
|
-
# Проверяем, была ли уже запланирована аналогичная задача
|
|
1160
|
+
# Проверяем, была ли уже запланирована аналогичная задача для этого пользователя
|
|
993
1161
|
already_processed = await check_event_already_processed(task_name, user_id, session_id)
|
|
994
1162
|
if already_processed:
|
|
995
1163
|
logger.info(f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем")
|
|
@@ -1108,7 +1276,7 @@ async def update_event_result(
|
|
|
1108
1276
|
logger.error(f"❌ Ошибка обновления результата события {event_id}: {e}")
|
|
1109
1277
|
|
|
1110
1278
|
async def get_pending_events(limit: int = 50) -> list:
|
|
1111
|
-
"""Получает события готовые к выполнению"""
|
|
1279
|
+
"""Получает события готовые к выполнению СЕЙЧАС"""
|
|
1112
1280
|
|
|
1113
1281
|
supabase_client = get_supabase_client()
|
|
1114
1282
|
if not supabase_client:
|
|
@@ -1131,29 +1299,145 @@ async def get_pending_events(limit: int = 50) -> list:
|
|
|
1131
1299
|
logger.error(f"❌ Ошибка получения событий из БД: {e}")
|
|
1132
1300
|
return []
|
|
1133
1301
|
|
|
1302
|
+
async def get_pending_events_in_next_minute(limit: int = 100) -> list:
|
|
1303
|
+
"""Получает события готовые к выполнению в течение следующей минуты"""
|
|
1304
|
+
|
|
1305
|
+
supabase_client = get_supabase_client()
|
|
1306
|
+
if not supabase_client:
|
|
1307
|
+
logger.error("❌ Supabase клиент не найден")
|
|
1308
|
+
return []
|
|
1309
|
+
|
|
1310
|
+
try:
|
|
1311
|
+
now = datetime.now(timezone.utc)
|
|
1312
|
+
next_minute = now + timedelta(seconds=60)
|
|
1313
|
+
|
|
1314
|
+
response = supabase_client.client.table('scheduled_events')\
|
|
1315
|
+
.select('*')\
|
|
1316
|
+
.in_('status', ['pending', 'immediate'])\
|
|
1317
|
+
.or_(f'scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}')\
|
|
1318
|
+
.order('created_at')\
|
|
1319
|
+
.limit(limit)\
|
|
1320
|
+
.execute()
|
|
1321
|
+
|
|
1322
|
+
return response.data
|
|
1323
|
+
except Exception as e:
|
|
1324
|
+
logger.error(f"❌ Ошибка получения событий из БД: {e}")
|
|
1325
|
+
return []
|
|
1326
|
+
|
|
1134
1327
|
async def background_event_processor():
|
|
1135
|
-
"""Фоновый процессор для всех типов событий"""
|
|
1328
|
+
"""Фоновый процессор для всех типов событий (проверяет БД каждую минуту)"""
|
|
1136
1329
|
|
|
1137
1330
|
logger.info("🔄 Запуск фонового процессора событий")
|
|
1138
1331
|
|
|
1139
1332
|
while True:
|
|
1140
1333
|
try:
|
|
1141
|
-
# Получаем события готовые к выполнению
|
|
1142
|
-
pending_events = await
|
|
1334
|
+
# Получаем события готовые к выполнению в следующую минуту
|
|
1335
|
+
pending_events = await get_pending_events_in_next_minute(limit=100)
|
|
1143
1336
|
|
|
1144
1337
|
if pending_events:
|
|
1145
1338
|
logger.info(f"📋 Найдено {len(pending_events)} событий для обработки")
|
|
1146
1339
|
|
|
1147
1340
|
for event in pending_events:
|
|
1148
1341
|
try:
|
|
1342
|
+
event_type = event['event_type']
|
|
1343
|
+
event_category = event['event_category']
|
|
1344
|
+
user_id = event.get('user_id')
|
|
1345
|
+
session_id = event.get('session_id')
|
|
1346
|
+
|
|
1347
|
+
# Для user_event - проверяем once_only
|
|
1348
|
+
if event_category == 'user_event':
|
|
1349
|
+
router_manager = get_router_manager()
|
|
1350
|
+
if router_manager:
|
|
1351
|
+
event_handlers = router_manager.get_event_handlers()
|
|
1352
|
+
else:
|
|
1353
|
+
event_handlers = _event_handlers
|
|
1354
|
+
|
|
1355
|
+
event_handler_info = event_handlers.get(event_type, {})
|
|
1356
|
+
once_only = event_handler_info.get('once_only', True)
|
|
1357
|
+
|
|
1358
|
+
if once_only:
|
|
1359
|
+
# Проверяем, было ли уже выполнено это событие для данного пользователя
|
|
1360
|
+
supabase_client = get_supabase_client()
|
|
1361
|
+
check_query = supabase_client.client.table('scheduled_events')\
|
|
1362
|
+
.select('id')\
|
|
1363
|
+
.eq('event_type', event_type)\
|
|
1364
|
+
.eq('user_id', user_id)\
|
|
1365
|
+
.eq('status', 'completed')\
|
|
1366
|
+
.neq('id', event['id']) # Исключаем текущее событие
|
|
1367
|
+
|
|
1368
|
+
if session_id:
|
|
1369
|
+
check_query = check_query.eq('session_id', session_id)
|
|
1370
|
+
|
|
1371
|
+
existing = check_query.execute()
|
|
1372
|
+
|
|
1373
|
+
if existing.data:
|
|
1374
|
+
await update_event_result(event['id'], 'cancelled', {"reason": "already_executed_once_only"})
|
|
1375
|
+
logger.info(f"⛔ Событие {event['id']} ({event_type}) пропущено: уже выполнялось для пользователя {user_id} (once_only=True)")
|
|
1376
|
+
continue
|
|
1377
|
+
|
|
1378
|
+
# Для scheduled_task - проверяем smart_check и once_only
|
|
1379
|
+
if event_category == 'scheduled_task':
|
|
1380
|
+
router_manager = get_router_manager()
|
|
1381
|
+
scheduled_tasks = router_manager.get_scheduled_tasks() if router_manager else _scheduled_tasks
|
|
1382
|
+
task_info = scheduled_tasks.get(event_type, {})
|
|
1383
|
+
use_smart_check = task_info.get('smart_check', True)
|
|
1384
|
+
once_only = task_info.get('once_only', True)
|
|
1385
|
+
|
|
1386
|
+
# Проверяем once_only для задач
|
|
1387
|
+
if once_only:
|
|
1388
|
+
supabase_client = get_supabase_client()
|
|
1389
|
+
check_query = supabase_client.client.table('scheduled_events')\
|
|
1390
|
+
.select('id')\
|
|
1391
|
+
.eq('event_type', event_type)\
|
|
1392
|
+
.eq('user_id', user_id)\
|
|
1393
|
+
.eq('status', 'completed')\
|
|
1394
|
+
.neq('id', event['id'])
|
|
1395
|
+
|
|
1396
|
+
if session_id:
|
|
1397
|
+
check_query = check_query.eq('session_id', session_id)
|
|
1398
|
+
|
|
1399
|
+
existing = check_query.execute()
|
|
1400
|
+
|
|
1401
|
+
if existing.data:
|
|
1402
|
+
await update_event_result(event['id'], 'cancelled', {"reason": "already_executed_once_only"})
|
|
1403
|
+
logger.info(f"⛔ Задача {event['id']} ({event_type}) пропущена: уже выполнялась для пользователя {user_id} (once_only=True)")
|
|
1404
|
+
continue
|
|
1405
|
+
|
|
1406
|
+
if use_smart_check:
|
|
1407
|
+
# Умная проверка
|
|
1408
|
+
check_result = await smart_execute_check(
|
|
1409
|
+
event['id'],
|
|
1410
|
+
user_id,
|
|
1411
|
+
session_id,
|
|
1412
|
+
event_type,
|
|
1413
|
+
event['event_data']
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
if check_result['action'] == 'cancel':
|
|
1417
|
+
await update_event_result(event['id'], 'cancelled', {"reason": check_result['reason']})
|
|
1418
|
+
logger.info(f"⛔ Задача {event['id']} отменена: {check_result['reason']}")
|
|
1419
|
+
continue
|
|
1420
|
+
elif check_result['action'] == 'reschedule':
|
|
1421
|
+
# Обновляем scheduled_at в БД
|
|
1422
|
+
new_scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=check_result['new_delay'])
|
|
1423
|
+
supabase_client = get_supabase_client()
|
|
1424
|
+
supabase_client.client.table('scheduled_events').update({
|
|
1425
|
+
'scheduled_at': new_scheduled_at.isoformat(),
|
|
1426
|
+
'status': 'pending'
|
|
1427
|
+
}).eq('id', event['id']).execute()
|
|
1428
|
+
logger.info(f"🔄 Задача {event['id']} перенесена на {check_result['new_delay']}с")
|
|
1429
|
+
continue
|
|
1430
|
+
|
|
1431
|
+
# Выполняем событие
|
|
1149
1432
|
await process_scheduled_event(event)
|
|
1150
1433
|
await update_event_result(event['id'], 'completed', {"processed": True})
|
|
1434
|
+
logger.info(f"✅ Событие {event['id']} выполнено")
|
|
1151
1435
|
|
|
1152
1436
|
except Exception as e:
|
|
1153
1437
|
logger.error(f"❌ Ошибка обработки события {event['id']}: {e}")
|
|
1154
1438
|
await update_event_result(event['id'], 'failed', None, str(e))
|
|
1155
1439
|
|
|
1156
|
-
await asyncio.sleep(60)
|
|
1440
|
+
await asyncio.sleep(60) # Проверяем каждую минуту
|
|
1157
1441
|
|
|
1158
1442
|
except Exception as e:
|
|
1159
1443
|
logger.error(f"❌ Ошибка в фоновом процессоре: {e}")
|
|
@@ -1183,7 +1467,7 @@ async def process_scheduled_event(event: Dict):
|
|
|
1183
1467
|
# =============================================================================
|
|
1184
1468
|
|
|
1185
1469
|
async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_data: str, delay_seconds: int, session_id: str = None):
|
|
1186
|
-
"""Планирует выполнение задачи через указанное время с сохранением в БД"""
|
|
1470
|
+
"""Планирует выполнение задачи через указанное время с сохранением в БД (без asyncio.sleep)"""
|
|
1187
1471
|
|
|
1188
1472
|
# Проверяем через RouterManager или fallback к старым декораторам
|
|
1189
1473
|
router_manager = get_router_manager()
|
|
@@ -1200,53 +1484,13 @@ async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_dat
|
|
|
1200
1484
|
logger.error(f"❌ [decorators.py:{line_no}] Задача '{task_name}' не найдена. Доступные: {available_tasks}")
|
|
1201
1485
|
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
1202
1486
|
|
|
1203
|
-
|
|
1487
|
+
human_time = format_seconds_to_human(delay_seconds)
|
|
1488
|
+
logger.info(f"⏰ Планируем задачу '{task_name}' через {human_time} ({delay_seconds}с) для user_id={user_id}")
|
|
1204
1489
|
|
|
1205
|
-
#
|
|
1490
|
+
# Просто сохраняем в БД - фоновый процессор сам выполнит задачу
|
|
1206
1491
|
event_id = await save_scheduled_task(task_name, user_id, user_data, delay_seconds, session_id)
|
|
1207
1492
|
|
|
1208
|
-
|
|
1209
|
-
await asyncio.sleep(delay_seconds)
|
|
1210
|
-
|
|
1211
|
-
# Получаем информацию о задаче
|
|
1212
|
-
task_info = scheduled_tasks.get(task_name, {})
|
|
1213
|
-
use_smart_check = task_info.get('smart_check', True)
|
|
1214
|
-
|
|
1215
|
-
if use_smart_check:
|
|
1216
|
-
# Умная проверка перед выполнением
|
|
1217
|
-
try:
|
|
1218
|
-
result = await smart_execute_check(event_id, user_id, session_id, task_name, user_data)
|
|
1219
|
-
if result['action'] == 'execute':
|
|
1220
|
-
await execute_scheduled_task(task_name, user_id, user_data)
|
|
1221
|
-
await update_event_result(event_id, 'completed', {"executed": True, "reason": "scheduled_execution"})
|
|
1222
|
-
elif result['action'] == 'cancel':
|
|
1223
|
-
await update_event_result(event_id, 'cancelled', {"reason": result['reason']})
|
|
1224
|
-
elif result['action'] == 'reschedule':
|
|
1225
|
-
# Перепланируем задачу на новое время
|
|
1226
|
-
new_delay = result['new_delay']
|
|
1227
|
-
await update_event_result(event_id, 'rescheduled', {
|
|
1228
|
-
"new_delay": new_delay,
|
|
1229
|
-
"reason": result['reason']
|
|
1230
|
-
})
|
|
1231
|
-
# Запускаем новую задачу
|
|
1232
|
-
await asyncio.sleep(new_delay)
|
|
1233
|
-
await execute_scheduled_task(task_name, user_id, user_data)
|
|
1234
|
-
await update_event_result(event_id, 'completed', {"executed": True, "reason": "rescheduled_execution"})
|
|
1235
|
-
|
|
1236
|
-
except Exception as e:
|
|
1237
|
-
await update_event_result(event_id, 'failed', None, str(e))
|
|
1238
|
-
raise
|
|
1239
|
-
else:
|
|
1240
|
-
# Простое выполнение без умной проверки
|
|
1241
|
-
try:
|
|
1242
|
-
await execute_scheduled_task(task_name, user_id, user_data)
|
|
1243
|
-
await update_event_result(event_id, 'completed', {"executed": True, "reason": "simple_execution"})
|
|
1244
|
-
except Exception as e:
|
|
1245
|
-
await update_event_result(event_id, 'failed', None, str(e))
|
|
1246
|
-
raise
|
|
1247
|
-
|
|
1248
|
-
# Запускаем задачу в фоне
|
|
1249
|
-
asyncio.create_task(delayed_task())
|
|
1493
|
+
logger.info(f"💾 Задача '{task_name}' сохранена в БД с ID {event_id}, будет обработана фоновым процессором")
|
|
1250
1494
|
|
|
1251
1495
|
return {
|
|
1252
1496
|
"status": "scheduled",
|
|
@@ -1257,7 +1501,7 @@ async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_dat
|
|
|
1257
1501
|
}
|
|
1258
1502
|
|
|
1259
1503
|
async def schedule_global_handler_for_later_with_db(handler_type: str, delay_seconds: int, handler_data: str):
|
|
1260
|
-
"""Планирует выполнение глобального обработчика через указанное время с сохранением в БД"""
|
|
1504
|
+
"""Планирует выполнение глобального обработчика через указанное время с сохранением в БД (без asyncio.sleep)"""
|
|
1261
1505
|
|
|
1262
1506
|
# Проверяем обработчик через RouterManager или fallback к старым декораторам
|
|
1263
1507
|
router_manager = get_router_manager()
|
|
@@ -1271,20 +1515,10 @@ async def schedule_global_handler_for_later_with_db(handler_type: str, delay_sec
|
|
|
1271
1515
|
|
|
1272
1516
|
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
|
|
1273
1517
|
|
|
1274
|
-
#
|
|
1518
|
+
# Просто сохраняем в БД - фоновый процессор сам выполнит обработчик
|
|
1275
1519
|
event_id = await save_global_event(handler_type, handler_data, delay_seconds)
|
|
1276
1520
|
|
|
1277
|
-
|
|
1278
|
-
await asyncio.sleep(delay_seconds)
|
|
1279
|
-
try:
|
|
1280
|
-
await execute_global_handler(handler_type, handler_data)
|
|
1281
|
-
await update_event_result(event_id, 'completed', {"executed": True})
|
|
1282
|
-
except Exception as e:
|
|
1283
|
-
await update_event_result(event_id, 'failed', None, str(e))
|
|
1284
|
-
raise
|
|
1285
|
-
|
|
1286
|
-
# Запускаем задачу в фоне
|
|
1287
|
-
asyncio.create_task(delayed_global_handler())
|
|
1521
|
+
logger.info(f"💾 Глобальный обработчик '{handler_type}' сохранен в БД с ID {event_id}, будет обработан фоновым процессором")
|
|
1288
1522
|
|
|
1289
1523
|
return {
|
|
1290
1524
|
"status": "scheduled",
|