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.

@@ -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: Тип события для напоминания (например, 'appointment_booking') - ОПЦИОНАЛЬНО
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
- # Напоминание будет отправлено за 2 часа до события appointment_booking
462
+ # Ищет событие "appointment_booking" в БД
463
+ # Напоминание будет за 2 часа до времени из события
429
464
  return {"status": "sent", "message": user_data}
430
465
 
431
- # Напоминание о процедуре
432
- @schedule_task("procedure_reminder", delay="1d", event_type="procedure_booking")
433
- async def procedure_reminder(user_id: int, user_data: str):
434
- # Напоминание будет отправлено за 1 день до процедуры
435
- return {"status": "sent", "message": user_data}
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 f"Напоминание через {default_delay} секунд"
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
- logger.info(f"⏰ Задача '{task_name}' - напоминание о событии '{event_type}' за {default_delay}с")
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
- try:
771
- # Получаем данные события
772
- event_data_str = await supabase_client.get_last_event_info_by_user_and_type(user_id, event_type)
823
+ # ========== ПРОВЕРЯЕМ ТИП event_type: СТРОКА ИЛИ ФУНКЦИЯ ==========
824
+ if callable(event_type):
825
+ # ВАРИАНТ 2: Функция - вызываем для получения datetime
826
+ logger.info(f"⏰ Задача '{task_name}' - вызываем функцию для получения времени события")
773
827
 
774
- if not event_data_str:
775
- logger.warning(f"Событие '{event_type}' не найдено для пользователя {user_id}")
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
- event_data = parse_appointment_data(event_data_str)
847
+ # Получаем клиент Supabase
848
+ supabase_client = get_supabase_client()
849
+ if not supabase_client:
850
+ raise RuntimeError("Supabase клиент не найден для получения времени события")
782
851
 
783
- if 'datetime' not in event_data:
784
- logger.warning(f"Не удалось распарсить дату/время из события '{event_type}'")
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
- event_datetime = event_data['datetime']
790
- now = datetime.now()
791
-
792
- # Вычисляем время напоминания (за default_delay до события)
793
- reminder_datetime = event_datetime - timedelta(seconds=default_delay)
794
-
795
- # Проверяем, не в прошлом ли напоминание
796
- if reminder_datetime <= now:
797
- logger.warning(f"Напоминание о событии '{event_type}' уже в прошлом, отправляем немедленно")
798
- # Выполняем задачу немедленно
799
- result = await execute_scheduled_task(task_name, user_id, user_data)
800
- return {
801
- "status": "executed_immediately",
802
- "task_name": task_name,
803
- "reason": "reminder_time_passed",
804
- "event_datetime": event_datetime.isoformat(),
805
- "result": result
806
- }
807
-
808
- # Вычисляем задержку до напоминания
809
- delay_seconds = int((reminder_datetime - now).total_seconds())
810
-
811
- logger.info(f"⏰ Планируем напоминание '{task_name}' за {default_delay}с до события '{event_type}' (через {delay_seconds}с)")
812
-
813
- # Планируем напоминание
814
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds, session_id)
815
- result['event_datetime'] = event_datetime.isoformat()
816
- result['reminder_type'] = 'event_reminder'
817
-
818
- return result
819
-
820
- except Exception as e:
821
- logger.error(f"Ошибка при работе с событием '{event_type}': {e}")
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
- logger.info(f"⏰ Планируем задачу '{task_name}' через {default_delay}с с текстом: '{user_data}'")
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 f"Глобальное событие через {default_delay} секунд"
988
+ # event_info содержит только текст для обработчика (если ИИ не передал - пустая строка)
989
+ handler_data = event_info.strip() if event_info else ""
900
990
 
901
- logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {default_delay}с с данными: '{handler_data}'")
902
-
903
- # Планируем обработчик на фоне с сохранением в БД
904
- result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
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
- return result
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
- event_handler_info = _event_handlers.get(event_type, {})
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
- task_info = _scheduled_tasks.get(task_name, {})
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 get_pending_events(limit=50)
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
- logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
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
- async def delayed_task():
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
- async def delayed_global_handler():
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",