smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.8__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.

Files changed (45) hide show
  1. smart_bot_factory/admin/__init__.py +7 -7
  2. smart_bot_factory/admin/admin_events.py +483 -383
  3. smart_bot_factory/admin/admin_logic.py +234 -158
  4. smart_bot_factory/admin/admin_manager.py +68 -53
  5. smart_bot_factory/admin/admin_tester.py +46 -40
  6. smart_bot_factory/admin/timeout_checker.py +201 -153
  7. smart_bot_factory/aiogram_calendar/__init__.py +11 -3
  8. smart_bot_factory/aiogram_calendar/common.py +12 -18
  9. smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
  10. smart_bot_factory/aiogram_calendar/schemas.py +49 -28
  11. smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
  12. smart_bot_factory/analytics/analytics_manager.py +414 -392
  13. smart_bot_factory/cli.py +204 -148
  14. smart_bot_factory/config.py +123 -102
  15. smart_bot_factory/core/bot_utils.py +474 -332
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1129 -749
  18. smart_bot_factory/core/message_sender.py +287 -266
  19. smart_bot_factory/core/router.py +170 -100
  20. smart_bot_factory/core/router_manager.py +121 -83
  21. smart_bot_factory/core/states.py +4 -3
  22. smart_bot_factory/creation/__init__.py +1 -1
  23. smart_bot_factory/creation/bot_builder.py +320 -242
  24. smart_bot_factory/creation/bot_testing.py +440 -365
  25. smart_bot_factory/dashboard/__init__.py +1 -3
  26. smart_bot_factory/event/__init__.py +2 -7
  27. smart_bot_factory/handlers/handlers.py +676 -472
  28. smart_bot_factory/integrations/openai_client.py +218 -168
  29. smart_bot_factory/integrations/supabase_client.py +928 -637
  30. smart_bot_factory/message/__init__.py +18 -22
  31. smart_bot_factory/router/__init__.py +2 -2
  32. smart_bot_factory/setup_checker.py +162 -126
  33. smart_bot_factory/supabase/__init__.py +1 -1
  34. smart_bot_factory/supabase/client.py +631 -515
  35. smart_bot_factory/utils/__init__.py +2 -3
  36. smart_bot_factory/utils/debug_routing.py +38 -27
  37. smart_bot_factory/utils/prompt_loader.py +153 -120
  38. smart_bot_factory/utils/user_prompt_loader.py +55 -56
  39. smart_bot_factory/utm_link_generator.py +123 -116
  40. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
  41. smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
  42. smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
@@ -5,22 +5,23 @@
5
5
  import asyncio
6
6
  import logging
7
7
  import re
8
- from typing import Callable, Any, Dict, Union
9
8
  from datetime import datetime, timedelta, timezone
10
9
  from functools import wraps
10
+ from typing import Any, Callable, Dict, Union
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
+
14
15
  def format_seconds_to_human(seconds: int) -> str:
15
16
  """
16
17
  Форматирует секунды в человекочитаемый формат
17
-
18
+
18
19
  Args:
19
20
  seconds: Количество секунд
20
-
21
+
21
22
  Returns:
22
23
  str: Человекочитаемое время
23
-
24
+
24
25
  Examples:
25
26
  format_seconds_to_human(3600) -> "1ч 0м"
26
27
  format_seconds_to_human(5445) -> "1ч 30м"
@@ -28,11 +29,11 @@ def format_seconds_to_human(seconds: int) -> str:
28
29
  """
29
30
  if seconds < 60:
30
31
  return f"{seconds}с"
31
-
32
+
32
33
  days = seconds // 86400
33
34
  hours = (seconds % 86400) // 3600
34
35
  minutes = (seconds % 3600) // 60
35
-
36
+
36
37
  parts = []
37
38
  if days > 0:
38
39
  parts.append(f"{days}д")
@@ -40,19 +41,20 @@ def format_seconds_to_human(seconds: int) -> str:
40
41
  parts.append(f"{hours}ч")
41
42
  if minutes > 0:
42
43
  parts.append(f"{minutes}м")
43
-
44
+
44
45
  return " ".join(parts) if parts else "0м"
45
46
 
47
+
46
48
  def parse_time_string(time_str: Union[str, int]) -> int:
47
49
  """
48
50
  Парсит время в удобном формате и возвращает секунды
49
-
51
+
50
52
  Args:
51
53
  time_str: Время в формате "1h 30m 45s" или число (секунды)
52
-
54
+
53
55
  Returns:
54
56
  int: Количество секунд
55
-
57
+
56
58
  Examples:
57
59
  parse_time_string("1h 30m 45s") -> 5445
58
60
  parse_time_string("2h") -> 7200
@@ -62,48 +64,51 @@ def parse_time_string(time_str: Union[str, int]) -> int:
62
64
  """
63
65
  if isinstance(time_str, int):
64
66
  return time_str
65
-
67
+
66
68
  # Убираем лишние пробелы и приводим к нижнему регистру
67
69
  time_str = time_str.strip().lower()
68
-
70
+
69
71
  # Если это просто число - возвращаем как секунды
70
72
  if time_str.isdigit():
71
73
  return int(time_str)
72
-
74
+
73
75
  total_seconds = 0
74
-
76
+
75
77
  # Регулярное выражение для поиска времени: число + единица (h, m, s)
76
- pattern = r'(\d+)\s*(h|m|s)'
78
+ pattern = r"(\d+)\s*(h|m|s)"
77
79
  matches = re.findall(pattern, time_str)
78
-
80
+
79
81
  if not matches:
80
- raise ValueError(f"Неверный формат времени: '{time_str}'. Используйте формат '1h 30m 45s'")
81
-
82
+ raise ValueError(
83
+ f"Неверный формат времени: '{time_str}'. Используйте формат '1h 30m 45s'"
84
+ )
85
+
82
86
  for value, unit in matches:
83
87
  value = int(value)
84
-
85
- if unit == 'h': # часы
88
+
89
+ if unit == "h": # часы
86
90
  total_seconds += value * 3600
87
- elif unit == 'm': # минуты
91
+ elif unit == "m": # минуты
88
92
  total_seconds += value * 60
89
- elif unit == 's': # секунды
93
+ elif unit == "s": # секунды
90
94
  total_seconds += value
91
-
95
+
92
96
  if total_seconds <= 0:
93
97
  raise ValueError(f"Время должно быть больше 0: '{time_str}'")
94
-
98
+
95
99
  return total_seconds
96
100
 
101
+
97
102
  def parse_supabase_datetime(datetime_str: str) -> datetime:
98
103
  """
99
104
  Парсит дату и время из формата Supabase в объект datetime
100
-
105
+
101
106
  Args:
102
107
  datetime_str: Строка даты и времени из Supabase (ISO 8601 формат)
103
-
108
+
104
109
  Returns:
105
110
  datetime: Объект datetime с timezone
106
-
111
+
107
112
  Examples:
108
113
  parse_supabase_datetime("2024-01-15T10:30:45.123456Z") -> datetime(2024, 1, 15, 10, 30, 45, 123456, tzinfo=timezone.utc)
109
114
  parse_supabase_datetime("2024-01-15T10:30:45+00:00") -> datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
@@ -111,118 +116,124 @@ def parse_supabase_datetime(datetime_str: str) -> datetime:
111
116
  """
112
117
  if not datetime_str:
113
118
  raise ValueError("Пустая строка даты и времени")
114
-
119
+
115
120
  # Убираем лишние пробелы
116
121
  datetime_str = datetime_str.strip()
117
-
122
+
118
123
  try:
119
124
  # Пробуем парсить ISO 8601 формат с Z в конце
120
- if datetime_str.endswith('Z'):
125
+ if datetime_str.endswith("Z"):
121
126
  # Заменяем Z на +00:00 для корректного парсинга
122
- datetime_str = datetime_str[:-1] + '+00:00'
127
+ datetime_str = datetime_str[:-1] + "+00:00"
123
128
  return datetime.fromisoformat(datetime_str)
124
-
129
+
125
130
  # Пробуем парсить ISO 8601 формат с timezone
126
- if '+' in datetime_str or datetime_str.count('-') > 2:
131
+ if "+" in datetime_str or datetime_str.count("-") > 2:
127
132
  return datetime.fromisoformat(datetime_str)
128
-
133
+
129
134
  # Если нет timezone, добавляем UTC
130
- if 'T' in datetime_str:
131
- return datetime.fromisoformat(datetime_str + '+00:00')
132
-
135
+ if "T" in datetime_str:
136
+ return datetime.fromisoformat(datetime_str + "+00:00")
137
+
133
138
  # Если это только дата, добавляем время 00:00:00 и UTC
134
- return datetime.fromisoformat(datetime_str + 'T00:00:00+00:00')
135
-
139
+ return datetime.fromisoformat(datetime_str + "T00:00:00+00:00")
140
+
136
141
  except ValueError as e:
137
- raise ValueError(f"Неверный формат даты и времени: '{datetime_str}'. Ошибка: {e}")
142
+ raise ValueError(
143
+ f"Неверный формат даты и времени: '{datetime_str}'. Ошибка: {e}"
144
+ )
145
+
138
146
 
139
147
  def format_datetime_for_supabase(dt: datetime) -> str:
140
148
  """
141
149
  Форматирует объект datetime в формат для Supabase
142
-
150
+
143
151
  Args:
144
152
  dt: Объект datetime
145
-
153
+
146
154
  Returns:
147
155
  str: Строка в формате ISO 8601 для Supabase
148
-
156
+
149
157
  Examples:
150
158
  format_datetime_for_supabase(datetime.now(timezone.utc)) -> "2024-01-15T10:30:45.123456+00:00"
151
159
  """
152
160
  if not isinstance(dt, datetime):
153
161
  raise ValueError("Ожидается объект datetime")
154
-
162
+
155
163
  # Если нет timezone, добавляем UTC
156
164
  if dt.tzinfo is None:
157
165
  dt = dt.replace(tzinfo=timezone.utc)
158
-
166
+
159
167
  return dt.isoformat()
160
168
 
169
+
161
170
  def get_time_difference_seconds(dt1: datetime, dt2: datetime) -> int:
162
171
  """
163
172
  Вычисляет разность между двумя датами в секундах
164
-
173
+
165
174
  Args:
166
175
  dt1: Первая дата
167
176
  dt2: Вторая дата
168
-
177
+
169
178
  Returns:
170
179
  int: Разность в секундах (dt2 - dt1)
171
-
180
+
172
181
  Examples:
173
182
  get_time_difference_seconds(datetime1, datetime2) -> 3600 # 1 час
174
183
  """
175
-
184
+
176
185
  # Если у дат нет timezone, добавляем UTC
177
186
  if dt1.tzinfo is None:
178
187
  dt1 = dt1.replace(tzinfo=timezone.utc)
179
188
  if dt2.tzinfo is None:
180
189
  dt2 = dt2.replace(tzinfo=timezone.utc)
181
-
190
+
182
191
  return int((dt2 - dt1).total_seconds())
183
192
 
193
+
184
194
  def is_datetime_recent(dt: datetime, max_age_seconds: int = 3600) -> bool:
185
195
  """
186
196
  Проверяет, является ли дата недавней (не старше указанного времени)
187
-
197
+
188
198
  Args:
189
199
  dt: Дата для проверки
190
200
  max_age_seconds: Максимальный возраст в секундах (по умолчанию 1 час)
191
-
201
+
192
202
  Returns:
193
203
  bool: True если дата недавняя, False если старая
194
-
204
+
195
205
  Examples:
196
206
  is_datetime_recent(datetime.now(), 1800) -> True # если дата сейчас
197
207
  is_datetime_recent(datetime.now() - timedelta(hours=2), 3600) -> False # если дата 2 часа назад
198
208
  """
199
209
  if not isinstance(dt, datetime):
200
210
  raise ValueError("Ожидается объект datetime")
201
-
211
+
202
212
  now = datetime.now(timezone.utc)
203
-
213
+
204
214
  # Если у даты нет timezone, добавляем UTC
205
215
  if dt.tzinfo is None:
206
216
  dt = dt.replace(tzinfo=timezone.utc)
207
-
217
+
208
218
  age_seconds = (now - dt).total_seconds()
209
219
  return age_seconds <= max_age_seconds
210
220
 
221
+
211
222
  def parse_appointment_data(data_str: str) -> Dict[str, Any]:
212
223
  """
213
224
  Парсит данные записи на прием из строки формата "ключ: значение, ключ: значение"
214
-
225
+
215
226
  Args:
216
227
  data_str: Строка с данными записи
217
-
228
+
218
229
  Returns:
219
230
  Dict[str, Any]: Словарь с распарсенными данными
220
-
231
+
221
232
  Examples:
222
233
  parse_appointment_data("имя: Михаил, телефон: +79965214968, процедура: Ламинирование + окрашивание, мастер: Софья, дата: 2025-10-01, время: 19:00")
223
234
  -> {
224
235
  'имя': 'Михаил',
225
- 'телефон': '+79965214968',
236
+ 'телефон': '+79965214968',
226
237
  'процедура': 'Ламинирование + окрашивание',
227
238
  'мастер': 'Софья',
228
239
  'дата': '2025-10-01',
@@ -231,68 +242,71 @@ def parse_appointment_data(data_str: str) -> Dict[str, Any]:
231
242
  """
232
243
  if not data_str or not isinstance(data_str, str):
233
244
  return {}
234
-
245
+
235
246
  result = {}
236
-
247
+
237
248
  try:
238
249
  # Разделяем по запятым, но учитываем что внутри значений могут быть запятые
239
250
  # Используем более умный подход - ищем паттерн "ключ: значение"
240
- pattern = r'([^:]+):\s*([^,]+?)(?=,\s*[^:]+:|$)'
251
+ pattern = r"([^:]+):\s*([^,]+?)(?=,\s*[^:]+:|$)"
241
252
  matches = re.findall(pattern, data_str.strip())
242
-
253
+
243
254
  for key, value in matches:
244
255
  # Очищаем ключ и значение от лишних пробелов
245
256
  clean_key = key.strip()
246
257
  clean_value = value.strip()
247
-
258
+
248
259
  # Убираем запятые в конце значения если есть
249
- if clean_value.endswith(','):
260
+ if clean_value.endswith(","):
250
261
  clean_value = clean_value[:-1].strip()
251
-
262
+
252
263
  result[clean_key] = clean_value
253
-
264
+
254
265
  # Дополнительная обработка для даты и времени
255
- if 'дата' in result and 'время' in result:
266
+ if "дата" in result and "время" in result:
256
267
  try:
257
268
  # Создаем полную дату и время
258
- date_str = result['дата']
259
- time_str = result['время']
260
-
269
+ date_str = result["дата"]
270
+ time_str = result["время"]
271
+
261
272
  # Парсим дату и время
262
- appointment_datetime = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
263
-
273
+ appointment_datetime = datetime.strptime(
274
+ f"{date_str} {time_str}", "%Y-%m-%d %H:%M"
275
+ )
276
+
264
277
  # Добавляем в результат
265
- result['datetime'] = appointment_datetime
266
- result['datetime_str'] = appointment_datetime.strftime("%Y-%m-%d %H:%M")
267
-
278
+ result["datetime"] = appointment_datetime
279
+ result["datetime_str"] = appointment_datetime.strftime("%Y-%m-%d %H:%M")
280
+
268
281
  # Проверяем, не в прошлом ли запись
269
282
  now = datetime.now()
270
283
  if appointment_datetime < now:
271
- result['is_past'] = True
284
+ result["is_past"] = True
272
285
  else:
273
- result['is_past'] = False
274
-
286
+ result["is_past"] = False
287
+
275
288
  except ValueError as e:
276
289
  logger.warning(f"Ошибка парсинга даты/времени: {e}")
277
- result['datetime_error'] = str(e)
278
-
290
+ result["datetime_error"] = str(e)
291
+
279
292
  logger.info(f"Распарсены данные записи: {list(result.keys())}")
280
293
  return result
281
-
294
+
282
295
  except Exception as e:
283
296
  logger.error(f"Ошибка парсинга данных записи: {e}")
284
- return {'error': str(e), 'raw_data': data_str}
297
+ return {"error": str(e), "raw_data": data_str}
298
+
285
299
 
286
300
  def format_appointment_data(appointment_data: Dict[str, Any]) -> str:
287
301
  """
288
302
  Форматирует данные записи обратно в строку
289
-
303
+
290
304
  Args:
291
305
  appointment_data: Словарь с данными записи
292
-
306
+
293
307
  Returns:
294
308
  str: Отформатированная строка
295
-
309
+
296
310
  Examples:
297
311
  format_appointment_data({
298
312
  'имя': 'Михаил',
@@ -306,69 +320,78 @@ def format_appointment_data(appointment_data: Dict[str, Any]) -> str:
306
320
  """
307
321
  if not appointment_data or not isinstance(appointment_data, dict):
308
322
  return ""
309
-
323
+
310
324
  # Исключаем служебные поля
311
- exclude_fields = {'datetime', 'datetime_str', 'is_past', 'datetime_error', 'error', 'raw_data'}
312
-
325
+ exclude_fields = {
326
+ "datetime",
327
+ "datetime_str",
328
+ "is_past",
329
+ "datetime_error",
330
+ "error",
331
+ "raw_data",
332
+ }
333
+
313
334
  parts = []
314
335
  for key, value in appointment_data.items():
315
336
  if key not in exclude_fields and value is not None:
316
337
  parts.append(f"{key}: {value}")
317
-
338
+
318
339
  return ", ".join(parts)
319
340
 
341
+
320
342
  def validate_appointment_data(appointment_data: Dict[str, Any]) -> Dict[str, Any]:
321
343
  """
322
344
  Валидирует данные записи на прием
323
-
345
+
324
346
  Args:
325
347
  appointment_data: Словарь с данными записи
326
-
348
+
327
349
  Returns:
328
350
  Dict[str, Any]: Результат валидации с полями 'valid', 'errors', 'warnings'
329
351
  """
330
- result = {
331
- 'valid': True,
332
- 'errors': [],
333
- 'warnings': []
334
- }
335
-
352
+ result = {"valid": True, "errors": [], "warnings": []}
353
+
336
354
  # Проверяем обязательные поля
337
- required_fields = ['имя', 'телефон', 'процедура', 'мастер', 'дата', 'время']
338
-
355
+ required_fields = ["имя", "телефон", "процедура", "мастер", "дата", "время"]
356
+
339
357
  for field in required_fields:
340
358
  if field not in appointment_data or not appointment_data[field]:
341
- result['errors'].append(f"Отсутствует обязательное поле: {field}")
342
- result['valid'] = False
343
-
359
+ result["errors"].append(f"Отсутствует обязательное поле: {field}")
360
+ result["valid"] = False
361
+
344
362
  # Проверяем формат телефона
345
- if 'телефон' in appointment_data:
346
- phone = appointment_data['телефон']
347
- if not re.match(r'^\+?[1-9]\d{10,14}$', phone.replace(' ', '').replace('-', '')):
348
- result['warnings'].append(f"Неверный формат телефона: {phone}")
349
-
363
+ if "телефон" in appointment_data:
364
+ phone = appointment_data["телефон"]
365
+ if not re.match(
366
+ r"^\+?[1-9]\d{10,14}$", phone.replace(" ", "").replace("-", "")
367
+ ):
368
+ result["warnings"].append(f"Неверный формат телефона: {phone}")
369
+
350
370
  # Проверяем дату
351
- if 'дата' in appointment_data:
371
+ if "дата" in appointment_data:
352
372
  try:
353
- datetime.strptime(appointment_data['дата'], "%Y-%m-%d")
373
+ datetime.strptime(appointment_data["дата"], "%Y-%m-%d")
354
374
  except ValueError:
355
- result['errors'].append(f"Неверный формат даты: {appointment_data['дата']}")
356
- result['valid'] = False
357
-
375
+ result["errors"].append(f"Неверный формат даты: {appointment_data['дата']}")
376
+ result["valid"] = False
377
+
358
378
  # Проверяем время
359
- if 'время' in appointment_data:
379
+ if "время" in appointment_data:
360
380
  try:
361
- datetime.strptime(appointment_data['время'], "%H:%M")
381
+ datetime.strptime(appointment_data["время"], "%H:%M")
362
382
  except ValueError:
363
- result['errors'].append(f"Неверный формат времени: {appointment_data['время']}")
364
- result['valid'] = False
365
-
383
+ result["errors"].append(
384
+ f"Неверный формат времени: {appointment_data['время']}"
385
+ )
386
+ result["valid"] = False
387
+
366
388
  # Проверяем, не в прошлом ли запись
367
- if 'is_past' in appointment_data and appointment_data['is_past']:
368
- result['warnings'].append("Запись назначена на прошедшую дату")
369
-
389
+ if "is_past" in appointment_data and appointment_data["is_past"]:
390
+ result["warnings"].append("Запись назначена на прошедшую дату")
391
+
370
392
  return result
371
393
 
394
+
372
395
  # Глобальный реестр обработчиков событий
373
396
  _event_handlers: Dict[str, Callable] = {}
374
397
  _scheduled_tasks: Dict[str, Dict[str, Any]] = {}
@@ -377,72 +400,91 @@ _global_handlers: Dict[str, Dict[str, Any]] = {}
377
400
  # Глобальный менеджер роутеров
378
401
  _router_manager = None
379
402
 
380
- def event_handler(event_type: str, notify: bool = False, once_only: bool = True, send_ai_response: bool = True):
403
+
404
+ def event_handler(
405
+ event_type: str,
406
+ notify: bool = False,
407
+ once_only: bool = True,
408
+ send_ai_response: bool = True,
409
+ ):
381
410
  """
382
411
  Декоратор для регистрации обработчика события
383
-
412
+
384
413
  Args:
385
414
  event_type: Тип события (например, 'appointment_booking', 'phone_collection')
386
415
  notify: Уведомлять ли админов о выполнении события (по умолчанию False)
387
416
  once_only: Обрабатывать ли событие только один раз (по умолчанию True)
388
417
  send_ai_response: Отправлять ли сообщение от ИИ после обработки события (по умолчанию True)
389
-
418
+
390
419
  Example:
391
420
  # Обработчик с отправкой сообщения от ИИ
392
421
  @event_handler("appointment_booking", notify=True)
393
422
  async def book_appointment(user_id: int, appointment_data: dict):
394
423
  # Логика записи на прием
395
424
  return {"status": "success", "appointment_id": "123"}
396
-
425
+
397
426
  # Обработчик БЕЗ отправки сообщения от ИИ
398
427
  @event_handler("phone_collection", once_only=False, send_ai_response=False)
399
428
  async def collect_phone(user_id: int, phone_data: dict):
400
429
  # Логика сбора телефона - ИИ не отправит сообщение
401
430
  return {"status": "phone_collected"}
402
431
  """
432
+
403
433
  def decorator(func: Callable) -> Callable:
404
434
  _event_handlers[event_type] = {
405
- 'handler': func,
406
- 'name': func.__name__,
407
- 'notify': notify,
408
- 'once_only': once_only,
409
- 'send_ai_response': send_ai_response
435
+ "handler": func,
436
+ "name": func.__name__,
437
+ "notify": notify,
438
+ "once_only": once_only,
439
+ "send_ai_response": send_ai_response,
410
440
  }
411
-
412
- logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
413
-
441
+
442
+ logger.info(
443
+ f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}"
444
+ )
445
+
414
446
  @wraps(func)
415
447
  async def wrapper(*args, **kwargs):
416
448
  try:
417
449
  logger.info(f"🔧 Выполняем обработчик события '{event_type}'")
418
450
  result = await func(*args, **kwargs)
419
451
  logger.info(f"✅ Обработчик '{event_type}' выполнен успешно")
420
-
452
+
421
453
  # Автоматически добавляем флаги notify и send_ai_response к результату
422
454
  if isinstance(result, dict):
423
- result['notify'] = notify
424
- result['send_ai_response'] = send_ai_response
455
+ result["notify"] = notify
456
+ result["send_ai_response"] = send_ai_response
425
457
  else:
426
458
  # Если результат не словарь, создаем словарь
427
459
  result = {
428
- 'status': 'success',
429
- 'result': result,
430
- 'notify': notify,
431
- 'send_ai_response': send_ai_response
460
+ "status": "success",
461
+ "result": result,
462
+ "notify": notify,
463
+ "send_ai_response": send_ai_response,
432
464
  }
433
-
465
+
434
466
  return result
435
467
  except Exception as e:
436
468
  logger.error(f"❌ Ошибка в обработчике '{event_type}': {e}")
437
469
  raise
438
-
470
+
439
471
  return wrapper
472
+
440
473
  return decorator
441
474
 
442
- 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, send_ai_response: bool = True):
475
+
476
+ def schedule_task(
477
+ task_name: str,
478
+ notify: bool = False,
479
+ smart_check: bool = True,
480
+ once_only: bool = True,
481
+ delay: Union[str, int] = None,
482
+ event_type: Union[str, Callable] = None,
483
+ send_ai_response: bool = True,
484
+ ):
443
485
  """
444
486
  Декоратор для регистрации задачи, которую можно запланировать на время
445
-
487
+
446
488
  Args:
447
489
  task_name: Название задачи (например, 'send_reminder', 'follow_up')
448
490
  notify: Уведомлять ли админов о выполнении задачи (по умолчанию False)
@@ -453,108 +495,126 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
453
495
  - str: Тип события для поиска в БД (например, 'appointment_booking')
454
496
  - Callable: Функция для получения datetime (например, async def(user_id, user_data) -> datetime)
455
497
  send_ai_response: Отправлять ли сообщение от ИИ после выполнения задачи (по умолчанию True)
456
-
498
+
457
499
  Example:
458
500
  # Обычная задача с фиксированным временем
459
501
  @schedule_task("send_reminder", delay="1h 30m")
460
502
  async def send_reminder(user_id: int, user_data: str):
461
503
  # Задача будет запланирована на 1 час 30 минут
462
504
  return {"status": "sent", "message": user_data}
463
-
505
+
464
506
  # Напоминание о событии из БД (за delay времени до события)
465
507
  @schedule_task("appointment_reminder", delay="2h", event_type="appointment_booking")
466
508
  async def appointment_reminder(user_id: int, user_data: str):
467
509
  # Ищет событие "appointment_booking" в БД
468
510
  # Напоминание будет за 2 часа до времени из события
469
511
  return {"status": "sent", "message": user_data}
470
-
512
+
471
513
  # Напоминание с кастомной функцией получения времени
472
514
  async def get_yclients_appointment_time(user_id: int, user_data: str) -> datetime:
473
515
  '''Получает время записи из YClients API'''
474
516
  from yclients_api import get_next_booking
475
517
  booking = await get_next_booking(user_id)
476
518
  return booking['datetime'] # datetime объект
477
-
519
+
478
520
  @schedule_task("yclients_reminder", delay="1h", event_type=get_yclients_appointment_time)
479
521
  async def yclients_reminder(user_id: int, user_data: str):
480
522
  # Вызовет get_yclients_appointment_time(user_id, user_data)
481
523
  # Напоминание будет за 1 час до возвращенного datetime
482
524
  return {"status": "sent"}
483
-
525
+
484
526
  # Форматы времени:
485
527
  # delay="1h 30m 45s" - 1 час 30 минут 45 секунд
486
528
  # delay="2h" - 2 часа
487
529
  # delay="30m" - 30 минут
488
530
  # delay=3600 - 3600 секунд (число)
489
-
531
+
490
532
  # ИИ может передавать только данные (текст):
491
533
  # {"тип": "send_reminder", "инфо": "Текст напоминания"} - только текст
492
534
  # {"тип": "appointment_reminder", "инфо": ""} - пустой текст, время берется из события/функции
493
535
  """
536
+
494
537
  def decorator(func: Callable) -> Callable:
495
538
  # Время ОБЯЗАТЕЛЬНО должно быть указано
496
539
  if delay is None:
497
- raise ValueError(f"Для задачи '{task_name}' ОБЯЗАТЕЛЬНО нужно указать параметр delay")
498
-
540
+ raise ValueError(
541
+ f"Для задачи '{task_name}' ОБЯЗАТЕЛЬНО нужно указать параметр delay"
542
+ )
543
+
499
544
  # Парсим время
500
545
  try:
501
546
  default_delay_seconds = parse_time_string(delay)
502
547
  if event_type:
503
- logger.info(f"⏰ Задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)")
548
+ logger.info(
549
+ f"⏰ Задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)"
550
+ )
504
551
  else:
505
- logger.info(f"⏰ Задача '{task_name}' настроена с задержкой: {delay} ({default_delay_seconds}с)")
552
+ logger.info(
553
+ f"⏰ Задача '{task_name}' настроена с задержкой: {delay} ({default_delay_seconds}с)"
554
+ )
506
555
  except ValueError as e:
507
556
  logger.error(f"❌ Ошибка парсинга времени для задачи '{task_name}': {e}")
508
557
  raise
509
-
558
+
510
559
  _scheduled_tasks[task_name] = {
511
- 'handler': func,
512
- 'name': func.__name__,
513
- 'notify': notify,
514
- 'smart_check': smart_check,
515
- 'once_only': once_only,
516
- 'default_delay': default_delay_seconds,
517
- 'event_type': event_type, # Новое поле для типа события
518
- 'send_ai_response': send_ai_response
560
+ "handler": func,
561
+ "name": func.__name__,
562
+ "notify": notify,
563
+ "smart_check": smart_check,
564
+ "once_only": once_only,
565
+ "default_delay": default_delay_seconds,
566
+ "event_type": event_type, # Новое поле для типа события
567
+ "send_ai_response": send_ai_response,
519
568
  }
520
-
569
+
521
570
  if event_type:
522
- logger.info(f"⏰ Зарегистрирована задача-напоминание '{task_name}' для события '{event_type}': {func.__name__}")
571
+ logger.info(
572
+ f"⏰ Зарегистрирована задача-напоминание '{task_name}' для события '{event_type}': {func.__name__}"
573
+ )
523
574
  else:
524
575
  logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
525
-
576
+
526
577
  @wraps(func)
527
578
  async def wrapper(*args, **kwargs):
528
579
  try:
529
580
  logger.info(f"⏰ Выполняем запланированную задачу '{task_name}'")
530
581
  result = await func(*args, **kwargs)
531
582
  logger.info(f"✅ Задача '{task_name}' выполнена успешно")
532
-
583
+
533
584
  # Автоматически добавляем флаги notify и send_ai_response к результату
534
585
  if isinstance(result, dict):
535
- result['notify'] = notify
536
- result['send_ai_response'] = send_ai_response
586
+ result["notify"] = notify
587
+ result["send_ai_response"] = send_ai_response
537
588
  else:
538
589
  # Если результат не словарь, создаем словарь
539
590
  result = {
540
- 'status': 'success',
541
- 'result': result,
542
- 'notify': notify,
543
- 'send_ai_response': send_ai_response
591
+ "status": "success",
592
+ "result": result,
593
+ "notify": notify,
594
+ "send_ai_response": send_ai_response,
544
595
  }
545
-
596
+
546
597
  return result
547
598
  except Exception as e:
548
599
  logger.error(f"❌ Ошибка в задаче '{task_name}': {e}")
549
600
  raise
550
-
601
+
551
602
  return wrapper
603
+
552
604
  return decorator
553
605
 
554
- def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None, send_ai_response: bool = True):
606
+
607
+ def global_handler(
608
+ handler_type: str,
609
+ notify: bool = False,
610
+ once_only: bool = True,
611
+ delay: Union[str, int] = None,
612
+ event_type: Union[str, Callable] = None,
613
+ send_ai_response: bool = True,
614
+ ):
555
615
  """
556
616
  Декоратор для регистрации глобального обработчика (для всех пользователей)
557
-
617
+
558
618
  Args:
559
619
  handler_type: Тип глобального обработчика (например, 'global_announcement', 'mass_notification')
560
620
  notify: Уведомлять ли админов о выполнении (по умолчанию False)
@@ -564,117 +624,137 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
564
624
  - str: Тип события для поиска в БД
565
625
  - Callable: Функция для получения datetime (например, async def(handler_data: str) -> datetime)
566
626
  send_ai_response: Отправлять ли сообщение от ИИ после выполнения обработчика (по умолчанию True)
567
-
627
+
568
628
  Example:
569
629
  # Глобальный обработчик с задержкой
570
630
  @global_handler("global_announcement", delay="2h", notify=True)
571
631
  async def send_global_announcement(announcement_text: str):
572
632
  # Выполнится через 2 часа
573
633
  return {"status": "sent", "recipients_count": 150}
574
-
634
+
575
635
  # Глобальный обработчик может выполняться многократно
576
636
  @global_handler("daily_report", delay="24h", once_only=False)
577
637
  async def send_daily_report(report_data: str):
578
638
  # Может запускаться каждый день через 24 часа
579
639
  return {"status": "sent", "report_type": "daily"}
580
-
640
+
581
641
  # С кастомной функцией для получения времени
582
642
  async def get_promo_end_time(handler_data: str) -> datetime:
583
643
  '''Получает время окончания акции из CRM'''
584
644
  from crm_api import get_active_promo
585
645
  promo = await get_active_promo()
586
646
  return promo['end_datetime']
587
-
647
+
588
648
  @global_handler("promo_ending_notification", delay="2h", event_type=get_promo_end_time)
589
649
  async def notify_promo_ending(handler_data: str):
590
650
  # Уведомление за 2 часа до окончания акции
591
651
  return {"status": "sent"}
592
-
652
+
593
653
  # Форматы времени:
594
654
  # delay="1h 30m 45s" - 1 час 30 минут 45 секунд
595
655
  # delay="2h" - 2 часа
596
656
  # delay="45m" - 45 минут
597
657
  # delay=3600 - 3600 секунд (число)
598
-
658
+
599
659
  # ИИ может передавать только данные (текст):
600
660
  # {"тип": "global_announcement", "инфо": "Важное объявление!"} - только текст
601
661
  # {"тип": "global_announcement", "инфо": ""} - пустой текст, время из функции
602
662
  """
663
+
603
664
  def decorator(func: Callable) -> Callable:
604
665
  # Время ОБЯЗАТЕЛЬНО должно быть указано
605
666
  if delay is None:
606
- raise ValueError(f"Для глобального обработчика '{handler_type}' ОБЯЗАТЕЛЬНО нужно указать параметр delay")
607
-
667
+ raise ValueError(
668
+ f"Для глобального обработчика '{handler_type}' ОБЯЗАТЕЛЬНО нужно указать параметр delay"
669
+ )
670
+
608
671
  # Парсим время
609
672
  try:
610
673
  default_delay_seconds = parse_time_string(delay)
611
- logger.info(f"🌍 Глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)")
674
+ logger.info(
675
+ f"🌍 Глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)"
676
+ )
612
677
  except ValueError as e:
613
- logger.error(f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}")
678
+ logger.error(
679
+ f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}"
680
+ )
614
681
  raise
615
-
682
+
616
683
  _global_handlers[handler_type] = {
617
- 'handler': func,
618
- 'name': func.__name__,
619
- 'notify': notify,
620
- 'once_only': once_only,
621
- 'default_delay': default_delay_seconds,
622
- 'event_type': event_type, # Добавляем event_type для глобальных обработчиков
623
- 'send_ai_response': send_ai_response
684
+ "handler": func,
685
+ "name": func.__name__,
686
+ "notify": notify,
687
+ "once_only": once_only,
688
+ "default_delay": default_delay_seconds,
689
+ "event_type": event_type, # Добавляем event_type для глобальных обработчиков
690
+ "send_ai_response": send_ai_response,
624
691
  }
625
-
626
- logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
627
-
692
+
693
+ logger.info(
694
+ f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}"
695
+ )
696
+
628
697
  @wraps(func)
629
698
  async def wrapper(*args, **kwargs):
630
699
  try:
631
700
  logger.info(f"🌍 Выполняем глобальный обработчик '{handler_type}'")
632
701
  result = await func(*args, **kwargs)
633
- logger.info(f"✅ Глобальный обработчик '{handler_type}' выполнен успешно")
634
-
702
+ logger.info(
703
+ f"✅ Глобальный обработчик '{handler_type}' выполнен успешно"
704
+ )
705
+
635
706
  # Автоматически добавляем флаги notify и send_ai_response к результату
636
707
  if isinstance(result, dict):
637
- result['notify'] = notify
638
- result['send_ai_response'] = send_ai_response
708
+ result["notify"] = notify
709
+ result["send_ai_response"] = send_ai_response
639
710
  else:
640
711
  # Если результат не словарь, создаем словарь
641
712
  result = {
642
- 'status': 'success',
643
- 'result': result,
644
- 'notify': notify,
645
- 'send_ai_response': send_ai_response
713
+ "status": "success",
714
+ "result": result,
715
+ "notify": notify,
716
+ "send_ai_response": send_ai_response,
646
717
  }
647
-
718
+
648
719
  return result
649
720
  except Exception as e:
650
- logger.error(f"❌ Ошибка в глобальном обработчике '{handler_type}': {e}")
721
+ logger.error(
722
+ f"❌ Ошибка в глобальном обработчике '{handler_type}': {e}"
723
+ )
651
724
  raise
652
-
725
+
653
726
  return wrapper
727
+
654
728
  return decorator
655
729
 
730
+
656
731
  def get_event_handlers() -> Dict[str, Dict[str, Any]]:
657
732
  """Возвращает все зарегистрированные обработчики событий"""
658
733
  return _event_handlers.copy()
659
734
 
735
+
660
736
  def get_scheduled_tasks() -> Dict[str, Dict[str, Any]]:
661
737
  """Возвращает все зарегистрированные задачи"""
662
738
  return _scheduled_tasks.copy()
663
739
 
740
+
664
741
  def get_global_handlers() -> Dict[str, Dict[str, Any]]:
665
742
  """Возвращает все зарегистрированные глобальные обработчики"""
666
743
  return _global_handlers.copy()
667
744
 
745
+
668
746
  def set_router_manager(router_manager):
669
747
  """Устанавливает глобальный менеджер роутеров"""
670
748
  global _router_manager
671
749
  _router_manager = router_manager
672
750
  logger.info("🔄 RouterManager установлен в decorators")
673
751
 
752
+
674
753
  def get_router_manager():
675
754
  """Получает глобальный менеджер роутеров"""
676
755
  return _router_manager
677
756
 
757
+
678
758
  def get_handlers_for_prompt() -> str:
679
759
  """
680
760
  Возвращает описание всех обработчиков для добавления в промпт
@@ -682,30 +762,31 @@ def get_handlers_for_prompt() -> str:
682
762
  # Сначала пробуем получить из роутеров
683
763
  if _router_manager:
684
764
  return _router_manager.get_handlers_for_prompt()
685
-
765
+
686
766
  # Fallback к старым декораторам
687
767
  if not _event_handlers and not _scheduled_tasks and not _global_handlers:
688
768
  return ""
689
-
769
+
690
770
  prompt_parts = []
691
-
771
+
692
772
  if _event_handlers:
693
773
  prompt_parts.append("ДОСТУПНЫЕ ОБРАБОТЧИКИ СОБЫТИЙ:")
694
774
  for event_type, handler_info in _event_handlers.items():
695
775
  prompt_parts.append(f"- {event_type}: {handler_info['name']}")
696
-
776
+
697
777
  if _scheduled_tasks:
698
778
  prompt_parts.append("\nДОСТУПНЫЕ ЗАДАЧИ ДЛЯ ПЛАНИРОВАНИЯ:")
699
779
  for task_name, task_info in _scheduled_tasks.items():
700
780
  prompt_parts.append(f"- {task_name}: {task_info['name']}")
701
-
781
+
702
782
  if _global_handlers:
703
783
  prompt_parts.append("\nДОСТУПНЫЕ ГЛОБАЛЬНЫЕ ОБРАБОТЧИКИ:")
704
784
  for handler_type, handler_info in _global_handlers.items():
705
785
  prompt_parts.append(f"- {handler_type}: {handler_info['name']}")
706
-
786
+
707
787
  return "\n".join(prompt_parts)
708
788
 
789
+
709
790
  async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
710
791
  """Выполняет обработчик события по типу"""
711
792
  # Сначала пробуем получить из роутеров
@@ -713,18 +794,22 @@ async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
713
794
  event_handlers = _router_manager.get_event_handlers()
714
795
  if event_type in event_handlers:
715
796
  handler_info = event_handlers[event_type]
716
- return await handler_info['handler'](*args, **kwargs)
717
-
797
+ return await handler_info["handler"](*args, **kwargs)
798
+
718
799
  # Fallback к старым декораторам
719
800
  if event_type not in _event_handlers:
720
801
  import inspect
802
+
721
803
  frame = inspect.currentframe()
722
804
  line_no = frame.f_lineno if frame else "unknown"
723
- logger.error(f"❌ [decorators.py:{line_no}] Обработчик события '{event_type}' не найден")
805
+ logger.error(
806
+ f"❌ [decorators.py:{line_no}] Обработчик события '{event_type}' не найден"
807
+ )
724
808
  raise ValueError(f"Обработчик события '{event_type}' не найден")
725
-
809
+
726
810
  handler_info = _event_handlers[event_type]
727
- return await handler_info['handler'](*args, **kwargs)
811
+ return await handler_info["handler"](*args, **kwargs)
812
+
728
813
 
729
814
  async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -> Any:
730
815
  """Выполняет запланированную задачу по имени (без планирования, только выполнение)"""
@@ -733,11 +818,11 @@ async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -
733
818
  scheduled_tasks = _router_manager.get_scheduled_tasks()
734
819
  if task_name in scheduled_tasks:
735
820
  task_info = scheduled_tasks[task_name]
736
- return await task_info['handler'](user_id, user_data)
737
-
738
-
821
+ return await task_info["handler"](user_id, user_data)
822
+
739
823
  task_info = _scheduled_tasks[task_name]
740
- return await task_info['handler'](user_id, user_data)
824
+ return await task_info["handler"](user_id, user_data)
825
+
741
826
 
742
827
  async def execute_global_handler(handler_type: str, *args, **kwargs) -> Any:
743
828
  """Выполняет глобальный обработчик по типу"""
@@ -746,17 +831,20 @@ async def execute_global_handler(handler_type: str, *args, **kwargs) -> Any:
746
831
  global_handlers = router_manager.get_global_handlers()
747
832
  else:
748
833
  global_handlers = _global_handlers
749
-
834
+
750
835
  if handler_type not in global_handlers:
751
836
  raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
752
-
837
+
753
838
  handler_info = global_handlers[handler_type]
754
- return await handler_info['handler'](*args, **kwargs)
839
+ return await handler_info["handler"](*args, **kwargs)
840
+
755
841
 
756
- async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: int, user_data: str):
842
+ async def schedule_task_for_later(
843
+ task_name: str, delay_seconds: int, user_id: int, user_data: str
844
+ ):
757
845
  """
758
846
  Планирует выполнение задачи через указанное время
759
-
847
+
760
848
  Args:
761
849
  task_name: Название задачи
762
850
  delay_seconds: Задержка в секундах
@@ -771,32 +859,39 @@ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: i
771
859
  else:
772
860
  scheduled_tasks = _scheduled_tasks
773
861
  logger.debug(f"🔍 Поиск задачи '{task_name}' через глобальный реестр")
774
-
862
+
775
863
  if task_name not in scheduled_tasks:
776
864
  available_tasks = list(scheduled_tasks.keys())
777
- logger.error(f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
778
- raise ValueError(f"Задача '{task_name}' не найдена. Доступные: {available_tasks}")
779
-
865
+ logger.error(
866
+ f"Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}"
867
+ )
868
+ raise ValueError(
869
+ f"Задача '{task_name}' не найдена. Доступные: {available_tasks}"
870
+ )
871
+
780
872
  logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
781
-
873
+
782
874
  async def delayed_task():
783
875
  await asyncio.sleep(delay_seconds)
784
876
  await execute_scheduled_task(task_name, user_id, user_data)
785
-
877
+
786
878
  # Запускаем задачу в фоне
787
879
  asyncio.create_task(delayed_task())
788
-
880
+
789
881
  return {
790
882
  "status": "scheduled",
791
883
  "task_name": task_name,
792
884
  "delay_seconds": delay_seconds,
793
- "scheduled_at": datetime.now().isoformat()
885
+ "scheduled_at": datetime.now().isoformat(),
794
886
  }
795
887
 
796
- async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str, session_id: str = None):
888
+
889
+ async def execute_scheduled_task_from_event(
890
+ user_id: int, task_name: str, event_info: str, session_id: str = None
891
+ ):
797
892
  """
798
893
  Выполняет запланированную задачу на основе события от ИИ
799
-
894
+
800
895
  Args:
801
896
  user_id: ID пользователя
802
897
  task_name: Название задачи
@@ -806,99 +901,135 @@ async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_
806
901
  router_manager = get_router_manager()
807
902
  if router_manager:
808
903
  scheduled_tasks = router_manager.get_scheduled_tasks()
809
- logger.debug(f"🔍 RouterManager найден, доступные задачи: {list(scheduled_tasks.keys())}")
904
+ logger.debug(
905
+ f"🔍 RouterManager найден, доступные задачи: {list(scheduled_tasks.keys())}"
906
+ )
810
907
  else:
811
908
  scheduled_tasks = _scheduled_tasks
812
- logger.debug(f"🔍 RouterManager не найден, старые задачи: {list(scheduled_tasks.keys())}")
813
-
909
+ logger.debug(
910
+ f"🔍 RouterManager не найден, старые задачи: {list(scheduled_tasks.keys())}"
911
+ )
912
+
814
913
  if task_name not in scheduled_tasks:
815
914
  available_tasks = list(scheduled_tasks.keys())
816
- logger.error(f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
817
- logger.error(f"❌ RouterManager статус: {'найден' if router_manager else 'НЕ НАЙДЕН'}")
818
- raise ValueError(f"Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
819
-
915
+ logger.error(
916
+ f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}"
917
+ )
918
+ logger.error(
919
+ f"❌ RouterManager статус: {'найден' if router_manager else 'НЕ НАЙДЕН'}"
920
+ )
921
+ raise ValueError(
922
+ f"Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}"
923
+ )
924
+
820
925
  task_info = scheduled_tasks[task_name]
821
- default_delay = task_info.get('default_delay')
822
- event_type = task_info.get('event_type')
823
-
926
+ default_delay = task_info.get("default_delay")
927
+ event_type = task_info.get("event_type")
928
+
824
929
  # Время всегда берется из декоратора, ИИ может передавать только текст
825
930
  if default_delay is None:
826
- raise ValueError(f"Для задачи '{task_name}' не указано время в декораторе (параметр delay)")
827
-
931
+ raise ValueError(
932
+ f"Для задачи '{task_name}' не указано время в декораторе (параметр delay)"
933
+ )
934
+
828
935
  # event_info содержит только текст для задачи (если ИИ не передал - пустая строка)
829
936
  user_data = event_info.strip() if event_info else ""
830
-
937
+
831
938
  # Если указан event_type, то это напоминание о событии
832
939
  if event_type:
833
940
  event_datetime = None
834
-
941
+
835
942
  # ========== ПРОВЕРЯЕМ ТИП event_type: СТРОКА ИЛИ ФУНКЦИЯ ==========
836
943
  if callable(event_type):
837
944
  # ВАРИАНТ 2: Функция - вызываем для получения datetime
838
- logger.info(f"⏰ Задача '{task_name}' - вызываем функцию для получения времени события")
839
-
945
+ logger.info(
946
+ f"⏰ Задача '{task_name}' - вызываем функцию для получения времени события"
947
+ )
948
+
840
949
  try:
841
950
  # Вызываем функцию пользователя с теми же аргументами что у обработчика
842
951
  event_datetime = await event_type(user_id, user_data)
843
-
952
+
844
953
  if not isinstance(event_datetime, datetime):
845
- raise ValueError(f"Функция event_type должна вернуть datetime, получен {type(event_datetime)}")
846
-
954
+ raise ValueError(
955
+ f"Функция event_type должна вернуть datetime, получен {type(event_datetime)}"
956
+ )
957
+
847
958
  logger.info(f"✅ Функция вернула время события: {event_datetime}")
848
-
959
+
849
960
  except Exception as e:
850
961
  logger.error(f"❌ Ошибка в функции event_type: {e}")
851
962
  # Fallback - планируем через default_delay
852
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
963
+ result = await schedule_task_for_later_with_db(
964
+ task_name, user_id, user_data, default_delay, session_id
965
+ )
853
966
  return result
854
-
967
+
855
968
  else:
856
969
  # ВАРИАНТ 1: Строка - ищем событие в БД (текущая логика)
857
- logger.info(f"⏰ Задача '{task_name}' - напоминание о событии '{event_type}' за {default_delay}с")
858
-
970
+ logger.info(
971
+ f"⏰ Задача '{task_name}' - напоминание о событии '{event_type}' за {default_delay}с"
972
+ )
973
+
859
974
  # Получаем клиент Supabase
860
975
  supabase_client = get_supabase_client()
861
976
  if not supabase_client:
862
- raise RuntimeError("Supabase клиент не найден для получения времени события")
863
-
977
+ raise RuntimeError(
978
+ "Supabase клиент не найден для получения времени события"
979
+ )
980
+
864
981
  try:
865
982
  # Получаем данные события из БД
866
- event_data_str = await supabase_client.get_last_event_info_by_user_and_type(user_id, event_type)
867
-
983
+ event_data_str = (
984
+ await supabase_client.get_last_event_info_by_user_and_type(
985
+ user_id, event_type
986
+ )
987
+ )
988
+
868
989
  if not event_data_str:
869
- logger.warning(f"Событие '{event_type}' не найдено для пользователя {user_id}")
990
+ logger.warning(
991
+ f"Событие '{event_type}' не найдено для пользователя {user_id}"
992
+ )
870
993
  # Fallback - планируем через default_delay
871
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
994
+ result = await schedule_task_for_later_with_db(
995
+ task_name, user_id, user_data, default_delay, session_id
996
+ )
872
997
  return result
873
-
998
+
874
999
  # Парсим данные события
875
1000
  event_data = parse_appointment_data(event_data_str)
876
-
877
- if 'datetime' not in event_data:
878
- logger.warning(f"Не удалось распарсить дату/время из события '{event_type}'")
1001
+
1002
+ if "datetime" not in event_data:
1003
+ logger.warning(
1004
+ f"Не удалось распарсить дату/время из события '{event_type}'"
1005
+ )
879
1006
  # Fallback - планируем через default_delay
880
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
1007
+ result = await schedule_task_for_later_with_db(
1008
+ task_name, user_id, user_data, default_delay, session_id
1009
+ )
881
1010
  return result
882
-
883
- event_datetime = event_data['datetime']
1011
+
1012
+ event_datetime = event_data["datetime"]
884
1013
  logger.info(f"✅ Получено время события из БД: {event_datetime}")
885
-
1014
+
886
1015
  except Exception as e:
887
1016
  logger.error(f"❌ Ошибка получения события из БД: {e}")
888
1017
  # Fallback - планируем через default_delay
889
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
1018
+ result = await schedule_task_for_later_with_db(
1019
+ task_name, user_id, user_data, default_delay, session_id
1020
+ )
890
1021
  return result
891
-
1022
+
892
1023
  # ========== ОБЩАЯ ЛОГИКА ДЛЯ ОБОИХ ВАРИАНТОВ ==========
893
1024
  # Теперь у нас есть event_datetime (из БД или из функции)
894
1025
  now = datetime.now()
895
-
1026
+
896
1027
  # Вычисляем время напоминания (за default_delay до события)
897
1028
  reminder_datetime = event_datetime - timedelta(seconds=default_delay)
898
-
1029
+
899
1030
  # Проверяем, не в прошлом ли напоминание
900
1031
  if reminder_datetime <= now:
901
- logger.warning(f"Напоминание уже в прошлом, отправляем немедленно")
1032
+ logger.warning("Напоминание уже в прошлом, отправляем немедленно")
902
1033
  # Выполняем задачу немедленно
903
1034
  result = await execute_scheduled_task(task_name, user_id, user_data)
904
1035
  return {
@@ -906,36 +1037,51 @@ async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_
906
1037
  "task_name": task_name,
907
1038
  "reason": "reminder_time_passed",
908
1039
  "event_datetime": event_datetime.isoformat(),
909
- "result": result
1040
+ "result": result,
910
1041
  }
911
-
1042
+
912
1043
  # Вычисляем задержку до напоминания
913
1044
  delay_seconds = int((reminder_datetime - now).total_seconds())
914
-
915
- event_source = "функции" if callable(task_info.get('event_type')) else f"события '{event_type}'"
1045
+
1046
+ event_source = (
1047
+ "функции"
1048
+ if callable(task_info.get("event_type"))
1049
+ else f"события '{event_type}'"
1050
+ )
916
1051
  human_time = format_seconds_to_human(delay_seconds)
917
- logger.info(f"⏰ Планируем напоминание '{task_name}' за {format_seconds_to_human(default_delay)} до {event_source} (через {human_time} / {delay_seconds}с)")
918
-
1052
+ logger.info(
1053
+ f"⏰ Планируем напоминание '{task_name}' за {format_seconds_to_human(default_delay)} до {event_source} (через {human_time} / {delay_seconds}с)"
1054
+ )
1055
+
919
1056
  # Планируем напоминание
920
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds, session_id)
921
- result['event_datetime'] = event_datetime.isoformat()
922
- result['reminder_type'] = 'event_reminder'
923
-
1057
+ result = await schedule_task_for_later_with_db(
1058
+ task_name, user_id, user_data, delay_seconds, session_id
1059
+ )
1060
+ result["event_datetime"] = event_datetime.isoformat()
1061
+ result["reminder_type"] = "event_reminder"
1062
+
924
1063
  return result
925
1064
  else:
926
1065
  # Обычная задача с фиксированным временем
927
1066
  human_time = format_seconds_to_human(default_delay)
928
- logger.info(f"⏰ Планируем задачу '{task_name}' через {human_time} ({default_delay}с) с текстом: '{user_data}'")
929
-
1067
+ logger.info(
1068
+ f"⏰ Планируем задачу '{task_name}' через {human_time} ({default_delay}с) с текстом: '{user_data}'"
1069
+ )
1070
+
930
1071
  # Планируем задачу на фоне с сохранением в БД
931
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
932
-
1072
+ result = await schedule_task_for_later_with_db(
1073
+ task_name, user_id, user_data, default_delay, session_id
1074
+ )
1075
+
933
1076
  return result
934
1077
 
935
- async def schedule_global_handler_for_later(handler_type: str, delay_seconds: int, handler_data: str):
1078
+
1079
+ async def schedule_global_handler_for_later(
1080
+ handler_type: str, delay_seconds: int, handler_data: str
1081
+ ):
936
1082
  """
937
1083
  Планирует выполнение глобального обработчика через указанное время
938
-
1084
+
939
1085
  Args:
940
1086
  handler_type: Тип глобального обработчика
941
1087
  delay_seconds: Задержка в секундах
@@ -945,37 +1091,48 @@ async def schedule_global_handler_for_later(handler_type: str, delay_seconds: in
945
1091
  router_manager = get_router_manager()
946
1092
  if router_manager:
947
1093
  global_handlers = router_manager.get_global_handlers()
948
- logger.debug(f"🔍 Поиск глобального обработчика '{handler_type}' через RouterManager")
1094
+ logger.debug(
1095
+ f"🔍 Поиск глобального обработчика '{handler_type}' через RouterManager"
1096
+ )
949
1097
  else:
950
1098
  global_handlers = _global_handlers
951
- logger.debug(f"🔍 Поиск глобального обработчика '{handler_type}' через глобальный реестр")
952
-
1099
+ logger.debug(
1100
+ f"🔍 Поиск глобального обработчика '{handler_type}' через глобальный реестр"
1101
+ )
1102
+
953
1103
  if handler_type not in global_handlers:
954
1104
  available_handlers = list(global_handlers.keys())
955
- logger.error(f"❌ Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}")
956
- raise ValueError(f"Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}")
957
-
958
- logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
959
-
1105
+ logger.error(
1106
+ f"Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}"
1107
+ )
1108
+ raise ValueError(
1109
+ f"Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}"
1110
+ )
1111
+
1112
+ logger.info(
1113
+ f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд"
1114
+ )
1115
+
960
1116
  async def delayed_global_handler():
961
1117
  await asyncio.sleep(delay_seconds)
962
1118
  # Передаем данные обработчику (может быть текст анонса или другие данные)
963
1119
  await execute_global_handler(handler_type, handler_data)
964
-
1120
+
965
1121
  # Запускаем задачу в фоне
966
1122
  asyncio.create_task(delayed_global_handler())
967
-
1123
+
968
1124
  return {
969
1125
  "status": "scheduled",
970
1126
  "handler_type": handler_type,
971
1127
  "delay_seconds": delay_seconds,
972
- "scheduled_at": datetime.now().isoformat()
1128
+ "scheduled_at": datetime.now().isoformat(),
973
1129
  }
974
1130
 
1131
+
975
1132
  async def execute_global_handler_from_event(handler_type: str, event_info: str):
976
1133
  """
977
1134
  Выполняет глобальный обработчик на основе события от ИИ
978
-
1135
+
979
1136
  Args:
980
1137
  handler_type: Тип глобального обработчика
981
1138
  event_info: Информация от ИИ (только текст, время задается в декораторе или функции)
@@ -985,62 +1142,76 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
985
1142
  global_handlers = router_manager.get_global_handlers()
986
1143
  else:
987
1144
  global_handlers = _global_handlers
988
-
1145
+
989
1146
  if handler_type not in global_handlers:
990
1147
  raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
991
-
1148
+
992
1149
  handler_info = global_handlers[handler_type]
993
- default_delay = handler_info.get('default_delay')
994
- event_type = handler_info.get('event_type')
995
-
1150
+ default_delay = handler_info.get("default_delay")
1151
+ event_type = handler_info.get("event_type")
1152
+
996
1153
  # Время всегда берется из декоратора, ИИ может передавать только текст
997
1154
  if default_delay is None:
998
- raise ValueError(f"Для глобального обработчика '{handler_type}' не указано время в декораторе (параметр delay)")
999
-
1155
+ raise ValueError(
1156
+ f"Для глобального обработчика '{handler_type}' не указано время в декораторе (параметр delay)"
1157
+ )
1158
+
1000
1159
  # event_info содержит только текст для обработчика (если ИИ не передал - пустая строка)
1001
1160
  handler_data = event_info.strip() if event_info else ""
1002
-
1161
+
1003
1162
  # Если указан event_type, вычисляем время относительно события
1004
1163
  if event_type:
1005
1164
  event_datetime = None
1006
-
1165
+
1007
1166
  # Проверяем тип event_type: строка или функция
1008
1167
  if callable(event_type):
1009
1168
  # ВАРИАНТ 2: Функция - вызываем для получения datetime
1010
- logger.info(f"🌍 Глобальный обработчик '{handler_type}' - вызываем функцию для получения времени")
1011
-
1169
+ logger.info(
1170
+ f"🌍 Глобальный обработчик '{handler_type}' - вызываем функцию для получения времени"
1171
+ )
1172
+
1012
1173
  try:
1013
1174
  # Вызываем функцию (только с handler_data для глобальных)
1014
1175
  event_datetime = await event_type(handler_data)
1015
-
1176
+
1016
1177
  if not isinstance(event_datetime, datetime):
1017
- raise ValueError(f"Функция event_type должна вернуть datetime, получен {type(event_datetime)}")
1018
-
1178
+ raise ValueError(
1179
+ f"Функция event_type должна вернуть datetime, получен {type(event_datetime)}"
1180
+ )
1181
+
1019
1182
  logger.info(f"✅ Функция вернула время события: {event_datetime}")
1020
-
1183
+
1021
1184
  except Exception as e:
1022
1185
  logger.error(f"❌ Ошибка в функции event_type: {e}")
1023
1186
  # Fallback - планируем через default_delay
1024
- result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
1187
+ result = await schedule_global_handler_for_later_with_db(
1188
+ handler_type, default_delay, handler_data
1189
+ )
1025
1190
  return result
1026
-
1191
+
1027
1192
  else:
1028
1193
  # ВАРИАНТ 1: Строка - ищем в БД (можно расширить логику если нужно)
1029
- logger.info(f"🌍 Глобальный обработчик '{handler_type}' - event_type '{event_type}' (строка)")
1194
+ logger.info(
1195
+ f"🌍 Глобальный обработчик '{handler_type}' - event_type '{event_type}' (строка)"
1196
+ )
1030
1197
  # Для глобальных обработчиков пока просто используем default_delay
1031
1198
  # Можно расширить логику если понадобится
1032
- result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
1199
+ result = await schedule_global_handler_for_later_with_db(
1200
+ handler_type, default_delay, handler_data
1201
+ )
1033
1202
  return result
1034
-
1203
+
1035
1204
  # Общая логика для функций
1036
1205
  now = datetime.now()
1037
-
1206
+
1038
1207
  # Вычисляем время напоминания (за default_delay до события)
1039
1208
  reminder_datetime = event_datetime - timedelta(seconds=default_delay)
1040
-
1209
+
1041
1210
  # Проверяем, не в прошлом ли напоминание
1042
1211
  if reminder_datetime <= now:
1043
- logger.warning(f"Напоминание глобального события уже в прошлом, выполняем немедленно")
1212
+ logger.warning(
1213
+ "Напоминание глобального события уже в прошлом, выполняем немедленно"
1214
+ )
1044
1215
  # Выполняем немедленно
1045
1216
  result = await execute_global_handler(handler_type, handler_data)
1046
1217
  return {
@@ -1048,29 +1219,37 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
1048
1219
  "handler_type": handler_type,
1049
1220
  "reason": "reminder_time_passed",
1050
1221
  "event_datetime": event_datetime.isoformat(),
1051
- "result": result
1222
+ "result": result,
1052
1223
  }
1053
-
1224
+
1054
1225
  # Вычисляем задержку до напоминания
1055
1226
  delay_seconds = int((reminder_datetime - now).total_seconds())
1056
-
1227
+
1057
1228
  human_time = format_seconds_to_human(delay_seconds)
1058
- logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' за {format_seconds_to_human(default_delay)} до события (через {human_time} / {delay_seconds}с)")
1059
-
1229
+ logger.info(
1230
+ f"🌍 Планируем глобальный обработчик '{handler_type}' за {format_seconds_to_human(default_delay)} до события (через {human_time} / {delay_seconds}с)"
1231
+ )
1232
+
1060
1233
  # Планируем обработчик
1061
- result = await schedule_global_handler_for_later_with_db(handler_type, delay_seconds, handler_data)
1062
- result['event_datetime'] = event_datetime.isoformat()
1063
- result['reminder_type'] = 'global_event_reminder'
1064
-
1234
+ result = await schedule_global_handler_for_later_with_db(
1235
+ handler_type, delay_seconds, handler_data
1236
+ )
1237
+ result["event_datetime"] = event_datetime.isoformat()
1238
+ result["reminder_type"] = "global_event_reminder"
1239
+
1065
1240
  return result
1066
-
1241
+
1067
1242
  else:
1068
1243
  # Обычный глобальный обработчик с фиксированной задержкой
1069
- logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {default_delay}с с данными: '{handler_data}'")
1070
-
1244
+ logger.info(
1245
+ f"🌍 Планируем глобальный обработчик '{handler_type}' через {default_delay}с с данными: '{handler_data}'"
1246
+ )
1247
+
1071
1248
  # Планируем обработчик на фоне с сохранением в БД
1072
- result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
1073
-
1249
+ result = await schedule_global_handler_for_later_with_db(
1250
+ handler_type, default_delay, handler_data
1251
+ )
1252
+
1074
1253
  return result
1075
1254
 
1076
1255
 
@@ -1078,545 +1257,681 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
1078
1257
  # ФУНКЦИИ ДЛЯ РАБОТЫ С БД СОБЫТИЙ
1079
1258
  # =============================================================================
1080
1259
 
1260
+
1081
1261
  def get_supabase_client():
1082
1262
  """Получает клиент Supabase из глобальных переменных"""
1083
1263
  import sys
1264
+
1084
1265
  current_module = sys.modules[__name__]
1085
- supabase_client = getattr(current_module, 'supabase_client', None)
1086
-
1266
+ supabase_client = getattr(current_module, "supabase_client", None)
1267
+
1087
1268
  # Если не найден в decorators, пробуем получить из bot_utils
1088
1269
  if not supabase_client:
1089
1270
  try:
1090
- bot_utils_module = sys.modules.get('smart_bot_factory.core.bot_utils')
1271
+ bot_utils_module = sys.modules.get("smart_bot_factory.core.bot_utils")
1091
1272
  if bot_utils_module:
1092
- supabase_client = getattr(bot_utils_module, 'supabase_client', None)
1093
- except:
1094
- pass
1095
-
1273
+ supabase_client = getattr(bot_utils_module, "supabase_client", None)
1274
+ except Exception:
1275
+ logger.debug("Не удалось получить supabase_client из bot_utils")
1276
+
1096
1277
  return supabase_client
1097
1278
 
1279
+
1098
1280
  async def save_immediate_event(
1099
- event_type: str,
1100
- user_id: int,
1101
- event_data: str,
1102
- session_id: str = None
1281
+ event_type: str, user_id: int, event_data: str, session_id: str = None
1103
1282
  ) -> str:
1104
1283
  """Сохраняет событие для немедленного выполнения"""
1105
-
1284
+
1106
1285
  supabase_client = get_supabase_client()
1107
1286
  if not supabase_client:
1108
1287
  logger.error("❌ Supabase клиент не найден")
1109
1288
  raise RuntimeError("Supabase клиент не инициализирован")
1110
-
1289
+
1111
1290
  # Проверяем, нужно ли предотвращать дублирование
1112
1291
  router_manager = get_router_manager()
1113
1292
  if router_manager:
1114
1293
  event_handlers = router_manager.get_event_handlers()
1115
1294
  else:
1116
1295
  event_handlers = _event_handlers
1117
-
1296
+
1118
1297
  event_handler_info = event_handlers.get(event_type, {})
1119
- once_only = event_handler_info.get('once_only', True)
1120
-
1298
+ once_only = event_handler_info.get("once_only", True)
1299
+
1121
1300
  if once_only:
1122
1301
  # Проверяем, было ли уже обработано аналогичное событие для этого пользователя
1123
- already_processed = await check_event_already_processed(event_type, user_id, session_id)
1302
+ already_processed = await check_event_already_processed(
1303
+ event_type, user_id, session_id
1304
+ )
1124
1305
  if already_processed:
1125
- logger.info(f"🔄 Событие '{event_type}' уже обрабатывалось для пользователя {user_id}, пропускаем")
1126
- raise ValueError(f"Событие '{event_type}' уже обрабатывалось (once_only=True)")
1127
-
1306
+ logger.info(
1307
+ f"🔄 Событие '{event_type}' уже обрабатывалось для пользователя {user_id}, пропускаем"
1308
+ )
1309
+ raise ValueError(
1310
+ f"Событие '{event_type}' уже обрабатывалось (once_only=True)"
1311
+ )
1312
+
1128
1313
  event_record = {
1129
- 'event_type': event_type,
1130
- 'event_category': 'user_event',
1131
- 'user_id': user_id,
1132
- 'event_data': event_data,
1133
- 'scheduled_at': None, # Немедленное выполнение
1134
- 'status': 'immediate',
1135
- 'session_id': session_id
1314
+ "event_type": event_type,
1315
+ "event_category": "user_event",
1316
+ "user_id": user_id,
1317
+ "event_data": event_data,
1318
+ "scheduled_at": None, # Немедленное выполнение
1319
+ "status": "immediate",
1320
+ "session_id": session_id,
1136
1321
  }
1137
-
1322
+
1138
1323
  # 🆕 Добавляем bot_id если указан
1139
1324
  if supabase_client.bot_id:
1140
- event_record['bot_id'] = supabase_client.bot_id
1141
-
1325
+ event_record["bot_id"] = supabase_client.bot_id
1326
+
1142
1327
  try:
1143
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1144
- event_id = response.data[0]['id']
1328
+ response = (
1329
+ supabase_client.client.table("scheduled_events")
1330
+ .insert(event_record)
1331
+ .execute()
1332
+ )
1333
+ event_id = response.data[0]["id"]
1145
1334
  logger.info(f"💾 Событие сохранено в БД: {event_id}")
1146
1335
  return event_id
1147
1336
  except Exception as e:
1148
1337
  logger.error(f"❌ Ошибка сохранения события в БД: {e}")
1149
1338
  raise
1150
1339
 
1340
+
1151
1341
  async def save_scheduled_task(
1152
1342
  task_name: str,
1153
1343
  user_id: int,
1154
1344
  user_data: str,
1155
1345
  delay_seconds: int,
1156
- session_id: str = None
1346
+ session_id: str = None,
1157
1347
  ) -> str:
1158
1348
  """Сохраняет запланированную задачу"""
1159
-
1349
+
1160
1350
  supabase_client = get_supabase_client()
1161
1351
  if not supabase_client:
1162
1352
  logger.error("❌ Supabase клиент не найден")
1163
1353
  raise RuntimeError("Supabase клиент не инициализирован")
1164
-
1354
+
1165
1355
  # Проверяем, нужно ли предотвращать дублирование
1166
1356
  router_manager = get_router_manager()
1167
1357
  if router_manager:
1168
1358
  scheduled_tasks = router_manager.get_scheduled_tasks()
1169
1359
  else:
1170
1360
  scheduled_tasks = _scheduled_tasks
1171
-
1361
+
1172
1362
  task_info = scheduled_tasks.get(task_name, {})
1173
- once_only = task_info.get('once_only', True)
1174
-
1363
+ once_only = task_info.get("once_only", True)
1364
+
1175
1365
  if once_only:
1176
1366
  # Проверяем, была ли уже запланирована аналогичная задача для этого пользователя
1177
- already_processed = await check_event_already_processed(task_name, user_id, session_id)
1367
+ already_processed = await check_event_already_processed(
1368
+ task_name, user_id, session_id
1369
+ )
1178
1370
  if already_processed:
1179
- logger.info(f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем")
1371
+ logger.info(
1372
+ f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем"
1373
+ )
1180
1374
  raise ValueError(f"Задача '{task_name}' уже запланирована (once_only=True)")
1181
-
1375
+
1182
1376
  scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
1183
-
1377
+
1184
1378
  event_record = {
1185
- 'event_type': task_name,
1186
- 'event_category': 'scheduled_task',
1187
- 'user_id': user_id,
1188
- 'event_data': user_data,
1189
- 'scheduled_at': scheduled_at.isoformat(),
1190
- 'status': 'pending',
1191
- 'session_id': session_id
1379
+ "event_type": task_name,
1380
+ "event_category": "scheduled_task",
1381
+ "user_id": user_id,
1382
+ "event_data": user_data,
1383
+ "scheduled_at": scheduled_at.isoformat(),
1384
+ "status": "pending",
1385
+ "session_id": session_id,
1192
1386
  }
1193
-
1387
+
1194
1388
  # 🆕 Добавляем bot_id если указан
1195
1389
  if supabase_client.bot_id:
1196
- event_record['bot_id'] = supabase_client.bot_id
1197
-
1390
+ event_record["bot_id"] = supabase_client.bot_id
1391
+
1198
1392
  try:
1199
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1200
- event_id = response.data[0]['id']
1201
- logger.info(f"⏰ Запланированная задача сохранена в БД: {event_id} (через {delay_seconds}с)")
1393
+ response = (
1394
+ supabase_client.client.table("scheduled_events")
1395
+ .insert(event_record)
1396
+ .execute()
1397
+ )
1398
+ event_id = response.data[0]["id"]
1399
+ logger.info(
1400
+ f"⏰ Запланированная задача сохранена в БД: {event_id} (через {delay_seconds}с)"
1401
+ )
1202
1402
  return event_id
1203
1403
  except Exception as e:
1204
1404
  logger.error(f"❌ Ошибка сохранения запланированной задачи в БД: {e}")
1205
1405
  raise
1206
1406
 
1407
+
1207
1408
  async def save_global_event(
1208
- handler_type: str,
1209
- handler_data: str,
1210
- delay_seconds: int = 0
1409
+ handler_type: str, handler_data: str, delay_seconds: int = 0
1211
1410
  ) -> str:
1212
1411
  """Сохраняет глобальное событие"""
1213
-
1412
+
1214
1413
  supabase_client = get_supabase_client()
1215
1414
  if not supabase_client:
1216
1415
  logger.error("❌ Supabase клиент не найден")
1217
1416
  raise RuntimeError("Supabase клиент не инициализирован")
1218
-
1417
+
1219
1418
  # Проверяем, нужно ли предотвращать дублирование
1220
1419
  router_manager = get_router_manager()
1221
1420
  if router_manager:
1222
1421
  global_handlers = router_manager.get_global_handlers()
1223
1422
  else:
1224
1423
  global_handlers = _global_handlers
1225
-
1424
+
1226
1425
  handler_info = global_handlers.get(handler_type, {})
1227
- once_only = handler_info.get('once_only', True)
1228
-
1426
+ once_only = handler_info.get("once_only", True)
1427
+
1229
1428
  if once_only:
1230
1429
  # Проверяем, было ли уже запланировано аналогичное глобальное событие
1231
- already_processed = await check_event_already_processed(handler_type, user_id=None)
1430
+ already_processed = await check_event_already_processed(
1431
+ handler_type, user_id=None
1432
+ )
1232
1433
  if already_processed:
1233
- logger.info(f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем")
1234
- raise ValueError(f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)")
1235
-
1434
+ logger.info(
1435
+ f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем"
1436
+ )
1437
+ raise ValueError(
1438
+ f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)"
1439
+ )
1440
+
1236
1441
  scheduled_at = None
1237
- status = 'immediate'
1238
-
1442
+ status = "immediate"
1443
+
1239
1444
  if delay_seconds > 0:
1240
1445
  scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
1241
- status = 'pending'
1242
-
1446
+ status = "pending"
1447
+
1243
1448
  event_record = {
1244
- 'event_type': handler_type,
1245
- 'event_category': 'global_handler',
1246
- 'user_id': None, # Глобальное событие
1247
- 'event_data': handler_data,
1248
- 'scheduled_at': scheduled_at.isoformat() if scheduled_at else None,
1249
- 'status': status
1449
+ "event_type": handler_type,
1450
+ "event_category": "global_handler",
1451
+ "user_id": None, # Глобальное событие
1452
+ "event_data": handler_data,
1453
+ "scheduled_at": scheduled_at.isoformat() if scheduled_at else None,
1454
+ "status": status,
1250
1455
  }
1251
-
1456
+
1252
1457
  # 🆕 Добавляем bot_id если указан (глобальные события тоже привязаны к боту)
1253
1458
  if supabase_client.bot_id:
1254
- event_record['bot_id'] = supabase_client.bot_id
1255
-
1459
+ event_record["bot_id"] = supabase_client.bot_id
1460
+
1256
1461
  try:
1257
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1258
- event_id = response.data[0]['id']
1462
+ response = (
1463
+ supabase_client.client.table("scheduled_events")
1464
+ .insert(event_record)
1465
+ .execute()
1466
+ )
1467
+ event_id = response.data[0]["id"]
1259
1468
  logger.info(f"🌍 Глобальное событие сохранено в БД: {event_id}")
1260
1469
  return event_id
1261
1470
  except Exception as e:
1262
1471
  logger.error(f"❌ Ошибка сохранения глобального события в БД: {e}")
1263
1472
  raise
1264
1473
 
1474
+
1265
1475
  async def update_event_result(
1266
- event_id: str,
1267
- status: str,
1268
- result_data: Any = None,
1269
- error_message: str = None
1476
+ event_id: str, status: str, result_data: Any = None, error_message: str = None
1270
1477
  ):
1271
1478
  """Обновляет результат выполнения события"""
1272
-
1479
+
1273
1480
  supabase_client = get_supabase_client()
1274
1481
  if not supabase_client:
1275
1482
  logger.error("❌ Supabase клиент не найден")
1276
1483
  return
1277
-
1484
+
1278
1485
  update_data = {
1279
- 'status': status,
1280
- 'executed_at': datetime.now(timezone.utc).isoformat()
1486
+ "status": status,
1487
+ "executed_at": datetime.now(timezone.utc).isoformat(),
1281
1488
  }
1282
-
1489
+
1283
1490
  if result_data:
1284
1491
  import json
1285
- update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
1286
-
1492
+
1493
+ update_data["result_data"] = json.dumps(result_data, ensure_ascii=False)
1494
+
1287
1495
  # Проверяем наличие поля 'info' для дашборда
1288
- if isinstance(result_data, dict) and 'info' in result_data:
1289
- update_data['info_dashboard'] = json.dumps(result_data['info'], ensure_ascii=False)
1496
+ if isinstance(result_data, dict) and "info" in result_data:
1497
+ update_data["info_dashboard"] = json.dumps(
1498
+ result_data["info"], ensure_ascii=False
1499
+ )
1290
1500
  logger.info(f"📊 Дашборд данные добавлены в событие {event_id}")
1291
-
1501
+
1292
1502
  if error_message:
1293
- update_data['last_error'] = error_message
1503
+ update_data["last_error"] = error_message
1294
1504
  # Получаем текущее количество попыток
1295
1505
  try:
1296
- current_retry = supabase_client.client.table('scheduled_events').select('retry_count').eq('id', event_id).execute().data[0]['retry_count']
1297
- update_data['retry_count'] = current_retry + 1
1298
- except:
1299
- update_data['retry_count'] = 1
1300
-
1506
+ current_retry = (
1507
+ supabase_client.client.table("scheduled_events")
1508
+ .select("retry_count")
1509
+ .eq("id", event_id)
1510
+ .execute()
1511
+ .data[0]["retry_count"]
1512
+ )
1513
+ update_data["retry_count"] = current_retry + 1
1514
+ except Exception:
1515
+ logger.debug("Не удалось получить текущее количество попыток, устанавливаем 1")
1516
+ update_data["retry_count"] = 1
1517
+
1301
1518
  try:
1302
- supabase_client.client.table('scheduled_events').update(update_data).eq('id', event_id).execute()
1519
+ supabase_client.client.table("scheduled_events").update(update_data).eq(
1520
+ "id", event_id
1521
+ ).execute()
1303
1522
  logger.info(f"📝 Результат события {event_id} обновлен: {status}")
1304
1523
  except Exception as e:
1305
1524
  logger.error(f"❌ Ошибка обновления результата события {event_id}: {e}")
1306
1525
 
1526
+
1307
1527
  async def get_pending_events(limit: int = 50) -> list:
1308
1528
  """Получает события готовые к выполнению СЕЙЧАС"""
1309
-
1529
+
1310
1530
  supabase_client = get_supabase_client()
1311
1531
  if not supabase_client:
1312
1532
  logger.error("❌ Supabase клиент не найден")
1313
1533
  return []
1314
-
1534
+
1315
1535
  try:
1316
1536
  now = datetime.now(timezone.utc).isoformat()
1317
-
1318
- query = supabase_client.client.table('scheduled_events')\
1319
- .select('*')\
1320
- .in_('status', ['pending', 'immediate'])\
1321
- .or_(f'scheduled_at.is.null,scheduled_at.lte.{now}')\
1322
- .order('created_at')\
1537
+
1538
+ query = (
1539
+ supabase_client.client.table("scheduled_events")
1540
+ .select("*")
1541
+ .in_("status", ["pending", "immediate"])
1542
+ .or_(f"scheduled_at.is.null,scheduled_at.lte.{now}")
1543
+ .order("created_at")
1323
1544
  .limit(limit)
1324
-
1545
+ )
1546
+
1325
1547
  # 🆕 Фильтруем по bot_id если указан
1326
1548
  if supabase_client.bot_id:
1327
- query = query.eq('bot_id', supabase_client.bot_id)
1328
-
1549
+ query = query.eq("bot_id", supabase_client.bot_id)
1550
+
1329
1551
  response = query.execute()
1330
-
1552
+
1331
1553
  return response.data
1332
1554
  except Exception as e:
1333
1555
  logger.error(f"❌ Ошибка получения событий из БД: {e}")
1334
1556
  return []
1335
1557
 
1558
+
1336
1559
  async def get_pending_events_in_next_minute(limit: int = 100) -> list:
1337
1560
  """Получает события готовые к выполнению в течение следующей минуты"""
1338
-
1561
+
1339
1562
  supabase_client = get_supabase_client()
1340
1563
  if not supabase_client:
1341
1564
  logger.error("❌ Supabase клиент не найден")
1342
1565
  return []
1343
-
1566
+
1344
1567
  try:
1345
1568
  now = datetime.now(timezone.utc)
1346
1569
  next_minute = now + timedelta(seconds=60)
1347
-
1348
- query = supabase_client.client.table('scheduled_events')\
1349
- .select('*')\
1350
- .in_('status', ['pending', 'immediate'])\
1351
- .or_(f'scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}')\
1352
- .order('created_at')\
1570
+
1571
+ query = (
1572
+ supabase_client.client.table("scheduled_events")
1573
+ .select("*")
1574
+ .in_("status", ["pending", "immediate"])
1575
+ .or_(f"scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}")
1576
+ .order("created_at")
1353
1577
  .limit(limit)
1354
-
1578
+ )
1579
+
1355
1580
  # 🆕 Фильтруем по bot_id если указан
1356
1581
  if supabase_client.bot_id:
1357
- query = query.eq('bot_id', supabase_client.bot_id)
1358
-
1582
+ query = query.eq("bot_id", supabase_client.bot_id)
1583
+
1359
1584
  response = query.execute()
1360
-
1585
+
1361
1586
  return response.data
1362
1587
  except Exception as e:
1363
1588
  logger.error(f"❌ Ошибка получения событий из БД: {e}")
1364
1589
  return []
1365
1590
 
1591
+
1366
1592
  async def background_event_processor():
1367
1593
  """Фоновый процессор для ВСЕХ типов событий включая админские (проверяет БД каждую минуту)"""
1368
-
1369
- logger.info("🔄 Запуск фонового процессора событий (user_event, scheduled_task, global_handler, admin_event)")
1370
-
1594
+
1595
+ logger.info(
1596
+ "🔄 Запуск фонового процессора событий (user_event, scheduled_task, global_handler, admin_event)"
1597
+ )
1598
+
1371
1599
  while True:
1372
1600
  try:
1373
1601
  # Получаем события готовые к выполнению в следующую минуту
1374
1602
  pending_events = await get_pending_events_in_next_minute(limit=100)
1375
-
1603
+
1376
1604
  if pending_events:
1377
1605
  logger.info(f"📋 Найдено {len(pending_events)} событий для обработки")
1378
-
1606
+
1379
1607
  for event in pending_events:
1380
1608
  try:
1381
- event_type = event['event_type']
1382
- event_category = event['event_category']
1383
- user_id = event.get('user_id')
1384
- session_id = event.get('session_id')
1385
-
1609
+ event_type = event["event_type"]
1610
+ event_category = event["event_category"]
1611
+ user_id = event.get("user_id")
1612
+ session_id = event.get("session_id")
1613
+
1386
1614
  # ========== ОБРАБОТКА АДМИНСКИХ СОБЫТИЙ ==========
1387
- if event_category == 'admin_event':
1615
+ if event_category == "admin_event":
1388
1616
  try:
1389
1617
  # Обрабатываем и получаем результат
1390
1618
  result = await process_admin_event(event)
1391
-
1619
+
1392
1620
  # Сохраняем результат в result_data
1393
1621
  import json
1622
+
1394
1623
  supabase_client = get_supabase_client()
1395
- supabase_client.client.table('scheduled_events').update({
1396
- 'status': 'completed',
1397
- 'executed_at': datetime.now(timezone.utc).isoformat(),
1398
- 'result_data': json.dumps(result, ensure_ascii=False) if result else None
1399
- }).eq('id', event['id']).execute()
1400
-
1401
- logger.info(f" Админское событие {event['id']} выполнено")
1624
+ supabase_client.client.table("scheduled_events").update(
1625
+ {
1626
+ "status": "completed",
1627
+ "executed_at": datetime.now(
1628
+ timezone.utc
1629
+ ).isoformat(),
1630
+ "result_data": (
1631
+ json.dumps(result, ensure_ascii=False)
1632
+ if result
1633
+ else None
1634
+ ),
1635
+ }
1636
+ ).eq("id", event["id"]).execute()
1637
+
1638
+ logger.info(
1639
+ f"✅ Админское событие {event['id']} выполнено"
1640
+ )
1402
1641
  continue
1403
-
1642
+
1404
1643
  except Exception as e:
1405
- logger.error(f"❌ Ошибка обработки админского события {event['id']}: {e}")
1406
-
1644
+ logger.error(
1645
+ f"❌ Ошибка обработки админского события {event['id']}: {e}"
1646
+ )
1647
+
1407
1648
  # Обновляем статус на failed
1408
1649
  supabase_client = get_supabase_client()
1409
- supabase_client.client.table('scheduled_events').update({
1410
- 'status': 'failed',
1411
- 'last_error': str(e),
1412
- 'executed_at': datetime.now(timezone.utc).isoformat()
1413
- }).eq('id', event['id']).execute()
1650
+ supabase_client.client.table("scheduled_events").update(
1651
+ {
1652
+ "status": "failed",
1653
+ "last_error": str(e),
1654
+ "executed_at": datetime.now(
1655
+ timezone.utc
1656
+ ).isoformat(),
1657
+ }
1658
+ ).eq("id", event["id"]).execute()
1414
1659
  continue
1415
-
1660
+
1416
1661
  # ========== ОБРАБОТКА USER СОБЫТИЙ ==========
1417
- if event_category == 'user_event':
1662
+ if event_category == "user_event":
1418
1663
  router_manager = get_router_manager()
1419
1664
  if router_manager:
1420
1665
  event_handlers = router_manager.get_event_handlers()
1421
1666
  else:
1422
1667
  event_handlers = _event_handlers
1423
-
1668
+
1424
1669
  event_handler_info = event_handlers.get(event_type, {})
1425
- once_only = event_handler_info.get('once_only', True)
1426
-
1670
+ once_only = event_handler_info.get("once_only", True)
1671
+
1427
1672
  if once_only:
1428
1673
  # Проверяем, было ли уже выполнено это событие для данного пользователя
1429
1674
  supabase_client = get_supabase_client()
1430
- check_query = supabase_client.client.table('scheduled_events')\
1431
- .select('id')\
1432
- .eq('event_type', event_type)\
1433
- .eq('user_id', user_id)\
1434
- .eq('status', 'completed')\
1435
- .neq('id', event['id']) # Исключаем текущее событие
1436
-
1675
+ check_query = (
1676
+ supabase_client.client.table("scheduled_events")
1677
+ .select("id")
1678
+ .eq("event_type", event_type)
1679
+ .eq("user_id", user_id)
1680
+ .eq("status", "completed")
1681
+ .neq("id", event["id"])
1682
+ ) # Исключаем текущее событие
1683
+
1437
1684
  if session_id:
1438
- check_query = check_query.eq('session_id', session_id)
1439
-
1685
+ check_query = check_query.eq(
1686
+ "session_id", session_id
1687
+ )
1688
+
1440
1689
  existing = check_query.execute()
1441
-
1690
+
1442
1691
  if existing.data:
1443
- await update_event_result(event['id'], 'cancelled', {"reason": "already_executed_once_only"})
1444
- logger.info(f"⛔ Событие {event['id']} ({event_type}) пропущено: уже выполнялось для пользователя {user_id} (once_only=True)")
1692
+ await update_event_result(
1693
+ event["id"],
1694
+ "cancelled",
1695
+ {"reason": "already_executed_once_only"},
1696
+ )
1697
+ logger.info(
1698
+ f"⛔ Событие {event['id']} ({event_type}) пропущено: уже выполнялось для пользователя {user_id} (once_only=True)"
1699
+ )
1445
1700
  continue
1446
-
1701
+
1447
1702
  # Для scheduled_task - проверяем smart_check и once_only
1448
- if event_category == 'scheduled_task':
1703
+ if event_category == "scheduled_task":
1449
1704
  router_manager = get_router_manager()
1450
- scheduled_tasks = router_manager.get_scheduled_tasks() if router_manager else _scheduled_tasks
1705
+ scheduled_tasks = (
1706
+ router_manager.get_scheduled_tasks()
1707
+ if router_manager
1708
+ else _scheduled_tasks
1709
+ )
1451
1710
  task_info = scheduled_tasks.get(event_type, {})
1452
- use_smart_check = task_info.get('smart_check', True)
1453
- once_only = task_info.get('once_only', True)
1454
-
1711
+ use_smart_check = task_info.get("smart_check", True)
1712
+ once_only = task_info.get("once_only", True)
1713
+
1455
1714
  # Проверяем once_only для задач
1456
1715
  if once_only:
1457
1716
  supabase_client = get_supabase_client()
1458
- check_query = supabase_client.client.table('scheduled_events')\
1459
- .select('id')\
1460
- .eq('event_type', event_type)\
1461
- .eq('user_id', user_id)\
1462
- .eq('status', 'completed')\
1463
- .neq('id', event['id'])
1464
-
1717
+ check_query = (
1718
+ supabase_client.client.table("scheduled_events")
1719
+ .select("id")
1720
+ .eq("event_type", event_type)
1721
+ .eq("user_id", user_id)
1722
+ .eq("status", "completed")
1723
+ .neq("id", event["id"])
1724
+ )
1725
+
1465
1726
  if session_id:
1466
- check_query = check_query.eq('session_id', session_id)
1467
-
1727
+ check_query = check_query.eq(
1728
+ "session_id", session_id
1729
+ )
1730
+
1468
1731
  existing = check_query.execute()
1469
-
1732
+
1470
1733
  if existing.data:
1471
- await update_event_result(event['id'], 'cancelled', {"reason": "already_executed_once_only"})
1472
- logger.info(f"⛔ Задача {event['id']} ({event_type}) пропущена: уже выполнялась для пользователя {user_id} (once_only=True)")
1734
+ await update_event_result(
1735
+ event["id"],
1736
+ "cancelled",
1737
+ {"reason": "already_executed_once_only"},
1738
+ )
1739
+ logger.info(
1740
+ f"⛔ Задача {event['id']} ({event_type}) пропущена: уже выполнялась для пользователя {user_id} (once_only=True)"
1741
+ )
1473
1742
  continue
1474
-
1743
+
1475
1744
  if use_smart_check:
1476
1745
  # Умная проверка
1477
1746
  check_result = await smart_execute_check(
1478
- event['id'],
1479
- user_id,
1747
+ event["id"],
1748
+ user_id,
1480
1749
  session_id,
1481
1750
  event_type,
1482
- event['event_data']
1751
+ event["event_data"],
1483
1752
  )
1484
-
1485
- if check_result['action'] == 'cancel':
1486
- await update_event_result(event['id'], 'cancelled', {"reason": check_result['reason']})
1487
- logger.info(f"⛔ Задача {event['id']} отменена: {check_result['reason']}")
1753
+
1754
+ if check_result["action"] == "cancel":
1755
+ await update_event_result(
1756
+ event["id"],
1757
+ "cancelled",
1758
+ {"reason": check_result["reason"]},
1759
+ )
1760
+ logger.info(
1761
+ f"⛔ Задача {event['id']} отменена: {check_result['reason']}"
1762
+ )
1488
1763
  continue
1489
- elif check_result['action'] == 'reschedule':
1764
+ elif check_result["action"] == "reschedule":
1490
1765
  # Обновляем scheduled_at в БД
1491
- new_scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=check_result['new_delay'])
1766
+ new_scheduled_at = datetime.now(
1767
+ timezone.utc
1768
+ ) + timedelta(seconds=check_result["new_delay"])
1492
1769
  supabase_client = get_supabase_client()
1493
- supabase_client.client.table('scheduled_events').update({
1494
- 'scheduled_at': new_scheduled_at.isoformat(),
1495
- 'status': 'pending'
1496
- }).eq('id', event['id']).execute()
1497
- logger.info(f"🔄 Задача {event['id']} перенесена на {check_result['new_delay']}с")
1770
+ supabase_client.client.table(
1771
+ "scheduled_events"
1772
+ ).update(
1773
+ {
1774
+ "scheduled_at": new_scheduled_at.isoformat(),
1775
+ "status": "pending",
1776
+ }
1777
+ ).eq(
1778
+ "id", event["id"]
1779
+ ).execute()
1780
+ logger.info(
1781
+ f"🔄 Задача {event['id']} перенесена на {check_result['new_delay']}с"
1782
+ )
1498
1783
  continue
1499
-
1784
+
1500
1785
  # Выполняем событие
1501
1786
  result = await process_scheduled_event(event)
1502
-
1787
+
1503
1788
  # Проверяем наличие поля 'info' для дашборда
1504
1789
  result_data = {"processed": True}
1505
1790
  if isinstance(result, dict):
1506
1791
  result_data.update(result)
1507
- if 'info' in result:
1508
- logger.info(f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}")
1509
-
1510
- await update_event_result(event['id'], 'completed', result_data)
1792
+ if "info" in result:
1793
+ logger.info(
1794
+ f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}"
1795
+ )
1796
+
1797
+ await update_event_result(event["id"], "completed", result_data)
1511
1798
  logger.info(f"✅ Событие {event['id']} выполнено")
1512
-
1799
+
1513
1800
  except Exception as e:
1514
1801
  logger.error(f"❌ Ошибка обработки события {event['id']}: {e}")
1515
- await update_event_result(event['id'], 'failed', None, str(e))
1516
-
1802
+ await update_event_result(event["id"], "failed", None, str(e))
1803
+
1517
1804
  await asyncio.sleep(60) # Проверяем каждую минуту
1518
-
1805
+
1519
1806
  except Exception as e:
1520
1807
  logger.error(f"❌ Ошибка в фоновом процессоре: {e}")
1521
1808
  await asyncio.sleep(60)
1522
1809
 
1810
+
1523
1811
  async def process_scheduled_event(event: Dict):
1524
1812
  """Обрабатывает одно событие из БД и возвращает результат"""
1525
-
1526
- event_type = event['event_type']
1527
- event_category = event['event_category']
1528
- event_data = event['event_data']
1529
- user_id = event.get('user_id')
1530
-
1813
+
1814
+ event_type = event["event_type"]
1815
+ event_category = event["event_category"]
1816
+ event_data = event["event_data"]
1817
+ user_id = event.get("user_id")
1818
+
1531
1819
  logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
1532
-
1820
+
1533
1821
  result = None
1534
- if event_category == 'scheduled_task':
1822
+ if event_category == "scheduled_task":
1535
1823
  result = await execute_scheduled_task(event_type, user_id, event_data)
1536
- elif event_category == 'global_handler':
1824
+ elif event_category == "global_handler":
1537
1825
  result = await execute_global_handler(event_type, event_data)
1538
- elif event_category == 'user_event':
1826
+ elif event_category == "user_event":
1539
1827
  result = await execute_event_handler(event_type, user_id, event_data)
1540
1828
  else:
1541
1829
  logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
1542
-
1830
+
1543
1831
  return result
1544
1832
 
1833
+
1545
1834
  # =============================================================================
1546
1835
  # ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
1547
1836
  # =============================================================================
1548
1837
 
1549
- async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_data: str, delay_seconds: int, session_id: str = None):
1838
+
1839
+ async def schedule_task_for_later_with_db(
1840
+ task_name: str,
1841
+ user_id: int,
1842
+ user_data: str,
1843
+ delay_seconds: int,
1844
+ session_id: str = None,
1845
+ ):
1550
1846
  """Планирует выполнение задачи через указанное время с сохранением в БД (без asyncio.sleep)"""
1551
-
1847
+
1552
1848
  # Проверяем через RouterManager или fallback к старым декораторам
1553
1849
  router_manager = get_router_manager()
1554
1850
  if router_manager:
1555
1851
  scheduled_tasks = router_manager.get_scheduled_tasks()
1556
1852
  else:
1557
1853
  scheduled_tasks = _scheduled_tasks
1558
-
1854
+
1559
1855
  if task_name not in scheduled_tasks:
1560
1856
  import inspect
1857
+
1561
1858
  frame = inspect.currentframe()
1562
1859
  line_no = frame.f_lineno if frame else "unknown"
1563
1860
  available_tasks = list(scheduled_tasks.keys())
1564
- logger.error(f"❌ [decorators.py:{line_no}] Задача '{task_name}' не найдена. Доступные: {available_tasks}")
1861
+ logger.error(
1862
+ f"❌ [decorators.py:{line_no}] Задача '{task_name}' не найдена. Доступные: {available_tasks}"
1863
+ )
1565
1864
  raise ValueError(f"Задача '{task_name}' не найдена")
1566
-
1865
+
1567
1866
  human_time = format_seconds_to_human(delay_seconds)
1568
- logger.info(f"⏰ Планируем задачу '{task_name}' через {human_time} ({delay_seconds}с) для user_id={user_id}")
1569
-
1867
+ logger.info(
1868
+ f"⏰ Планируем задачу '{task_name}' через {human_time} ({delay_seconds}с) для user_id={user_id}"
1869
+ )
1870
+
1570
1871
  # Просто сохраняем в БД - фоновый процессор сам выполнит задачу
1571
- event_id = await save_scheduled_task(task_name, user_id, user_data, delay_seconds, session_id)
1572
-
1573
- logger.info(f"💾 Задача '{task_name}' сохранена в БД с ID {event_id}, будет обработана фоновым процессором")
1574
-
1872
+ event_id = await save_scheduled_task(
1873
+ task_name, user_id, user_data, delay_seconds, session_id
1874
+ )
1875
+
1876
+ logger.info(
1877
+ f"💾 Задача '{task_name}' сохранена в БД с ID {event_id}, будет обработана фоновым процессором"
1878
+ )
1879
+
1575
1880
  return {
1576
1881
  "status": "scheduled",
1577
1882
  "task_name": task_name,
1578
1883
  "delay_seconds": delay_seconds,
1579
1884
  "event_id": event_id,
1580
- "scheduled_at": datetime.now(timezone.utc).isoformat()
1885
+ "scheduled_at": datetime.now(timezone.utc).isoformat(),
1581
1886
  }
1582
1887
 
1583
- async def schedule_global_handler_for_later_with_db(handler_type: str, delay_seconds: int, handler_data: str):
1888
+
1889
+ async def schedule_global_handler_for_later_with_db(
1890
+ handler_type: str, delay_seconds: int, handler_data: str
1891
+ ):
1584
1892
  """Планирует выполнение глобального обработчика через указанное время с сохранением в БД (без asyncio.sleep)"""
1585
-
1893
+
1586
1894
  # Проверяем обработчик через RouterManager или fallback к старым декораторам
1587
1895
  router_manager = get_router_manager()
1588
1896
  if router_manager:
1589
1897
  global_handlers = router_manager.get_global_handlers()
1590
1898
  else:
1591
1899
  global_handlers = _global_handlers
1592
-
1900
+
1593
1901
  if handler_type not in global_handlers:
1594
1902
  raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
1595
-
1596
- logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
1597
-
1903
+
1904
+ logger.info(
1905
+ f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд"
1906
+ )
1907
+
1598
1908
  # Просто сохраняем в БД - фоновый процессор сам выполнит обработчик
1599
1909
  event_id = await save_global_event(handler_type, handler_data, delay_seconds)
1600
-
1601
- logger.info(f"💾 Глобальный обработчик '{handler_type}' сохранен в БД с ID {event_id}, будет обработан фоновым процессором")
1602
-
1910
+
1911
+ logger.info(
1912
+ f"💾 Глобальный обработчик '{handler_type}' сохранен в БД с ID {event_id}, будет обработан фоновым процессором"
1913
+ )
1914
+
1603
1915
  return {
1604
1916
  "status": "scheduled",
1605
1917
  "handler_type": handler_type,
1606
1918
  "delay_seconds": delay_seconds,
1607
1919
  "event_id": event_id,
1608
- "scheduled_at": datetime.now(timezone.utc).isoformat()
1920
+ "scheduled_at": datetime.now(timezone.utc).isoformat(),
1609
1921
  }
1610
1922
 
1611
- async def smart_execute_check(event_id: str, user_id: int, session_id: str, task_name: str, user_data: str) -> Dict[str, Any]:
1923
+
1924
+ async def smart_execute_check(
1925
+ event_id: str, user_id: int, session_id: str, task_name: str, user_data: str
1926
+ ) -> Dict[str, Any]:
1612
1927
  """
1613
1928
  Умная проверка перед выполнением запланированной задачи
1614
-
1929
+
1615
1930
  Логика:
1616
1931
  1. Если пользователь перешел на новый этап - отменяем событие
1617
1932
  2. Если прошло меньше времени чем планировалось - переносим на разницу
1618
1933
  3. Если прошло достаточно времени - выполняем
1619
-
1934
+
1620
1935
  Returns:
1621
1936
  Dict с action: 'execute', 'cancel', 'reschedule'
1622
1937
  """
@@ -1624,84 +1939,109 @@ async def smart_execute_check(event_id: str, user_id: int, session_id: str, task
1624
1939
  if not supabase_client:
1625
1940
  logger.error("❌ Supabase клиент не найден для умной проверки")
1626
1941
  return {"action": "execute", "reason": "no_supabase_client"}
1627
-
1942
+
1628
1943
  try:
1629
1944
  # Получаем информацию о последнем сообщении пользователя
1630
1945
  user_info = await supabase_client.get_user_last_message_info(user_id)
1631
-
1946
+
1632
1947
  if not user_info:
1633
1948
  logger.info(f"🔄 Пользователь {user_id} не найден, выполняем задачу")
1634
1949
  return {"action": "execute", "reason": "user_not_found"}
1635
-
1950
+
1636
1951
  # Проверяем, изменился ли этап
1637
- stage_changed = await supabase_client.check_user_stage_changed(user_id, session_id)
1952
+ stage_changed = await supabase_client.check_user_stage_changed(
1953
+ user_id, session_id
1954
+ )
1638
1955
  if stage_changed:
1639
- logger.info(f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}")
1956
+ logger.info(
1957
+ f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}"
1958
+ )
1640
1959
  return {"action": "cancel", "reason": "user_stage_changed"}
1641
-
1960
+
1642
1961
  # Получаем информацию о событии из БД
1643
- event_response = supabase_client.client.table('scheduled_events').select(
1644
- 'created_at', 'scheduled_at'
1645
- ).eq('id', event_id).execute()
1646
-
1962
+ event_response = (
1963
+ supabase_client.client.table("scheduled_events")
1964
+ .select("created_at", "scheduled_at")
1965
+ .eq("id", event_id)
1966
+ .execute()
1967
+ )
1968
+
1647
1969
  if not event_response.data:
1648
1970
  logger.error(f"❌ Событие {event_id} не найдено в БД")
1649
1971
  return {"action": "execute", "reason": "event_not_found"}
1650
-
1972
+
1651
1973
  event = event_response.data[0]
1652
- created_at = datetime.fromisoformat(event['created_at'].replace('Z', '+00:00'))
1653
- scheduled_at = datetime.fromisoformat(event['scheduled_at'].replace('Z', '+00:00'))
1654
- last_message_at = datetime.fromisoformat(user_info['last_message_at'].replace('Z', '+00:00'))
1655
-
1974
+ created_at = datetime.fromisoformat(event["created_at"].replace("Z", "+00:00"))
1975
+ scheduled_at = datetime.fromisoformat(
1976
+ event["scheduled_at"].replace("Z", "+00:00")
1977
+ )
1978
+ last_message_at = datetime.fromisoformat(
1979
+ user_info["last_message_at"].replace("Z", "+00:00")
1980
+ )
1981
+
1656
1982
  # Вычисляем разницу во времени
1657
1983
  now = datetime.now(timezone.utc)
1658
1984
  time_since_creation = (now - created_at).total_seconds()
1659
1985
  time_since_last_message = (now - last_message_at).total_seconds()
1660
1986
  planned_delay = (scheduled_at - created_at).total_seconds()
1661
-
1987
+
1662
1988
  # Проверяем, писал ли пользователь после создания события
1663
- time_between_creation_and_last_message = (last_message_at - created_at).total_seconds()
1664
-
1989
+ time_between_creation_and_last_message = (
1990
+ last_message_at - created_at
1991
+ ).total_seconds()
1992
+
1665
1993
  logger.info(f"🔄 Анализ для пользователя {user_id}:")
1666
1994
  logger.info(f" Время с создания события: {time_since_creation:.0f}с")
1667
1995
  logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
1668
1996
  logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
1669
- logger.info(f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}")
1670
-
1997
+ logger.info(
1998
+ f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}"
1999
+ )
2000
+
1671
2001
  # Если пользователь писал ПОСЛЕ создания события (недавно активен)
1672
2002
  # И с момента его последнего сообщения прошло меньше planned_delay
1673
- if time_between_creation_and_last_message > 0 and time_since_last_message < planned_delay:
2003
+ if (
2004
+ time_between_creation_and_last_message > 0
2005
+ and time_since_last_message < planned_delay
2006
+ ):
1674
2007
  # Пересчитываем время - отправляем через planned_delay после последнего сообщения
1675
2008
  new_delay = max(0, planned_delay - time_since_last_message)
1676
- logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)")
2009
+ logger.info(
2010
+ f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)"
2011
+ )
1677
2012
  return {
1678
- "action": "reschedule",
2013
+ "action": "reschedule",
1679
2014
  "new_delay": new_delay,
1680
- "reason": f"user_active_after_event_creation_{new_delay:.0f}s_delay"
2015
+ "reason": f"user_active_after_event_creation_{new_delay:.0f}s_delay",
1681
2016
  }
1682
-
2017
+
1683
2018
  # Если прошло достаточно времени с последнего сообщения - выполняем
1684
2019
  if time_since_last_message >= planned_delay:
1685
- logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id} (прошло {time_since_last_message:.0f}с с последнего сообщения)")
2020
+ logger.info(
2021
+ f"🔄 Выполняем задачу {task_name} для пользователя {user_id} (прошло {time_since_last_message:.0f}с с последнего сообщения)"
2022
+ )
1686
2023
  return {"action": "execute", "reason": "time_expired_since_last_message"}
1687
-
2024
+
1688
2025
  # Если что-то пошло не так - выполняем
1689
2026
  logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")
1690
2027
  return {"action": "execute", "reason": "unexpected_situation"}
1691
-
2028
+
1692
2029
  except Exception as e:
1693
2030
  logger.error(f"❌ Ошибка в умной проверке для пользователя {user_id}: {e}")
1694
2031
  return {"action": "execute", "reason": f"error_in_check: {str(e)}"}
1695
2032
 
1696
- async def check_event_already_processed(event_type: str, user_id: int = None, session_id: str = None) -> bool:
2033
+
2034
+ async def check_event_already_processed(
2035
+ event_type: str, user_id: int = None, session_id: str = None
2036
+ ) -> bool:
1697
2037
  """
1698
2038
  Проверяет, был ли уже обработан аналогичный event_type для пользователя/сессии
1699
-
2039
+
1700
2040
  Args:
1701
2041
  event_type: Тип события
1702
2042
  user_id: ID пользователя (для user_event и scheduled_task)
1703
2043
  session_id: ID сессии (для дополнительной проверки)
1704
-
2044
+
1705
2045
  Returns:
1706
2046
  True если событие уже обрабатывалось или в процессе
1707
2047
  """
@@ -1709,36 +2049,42 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
1709
2049
  if not supabase_client:
1710
2050
  logger.error("❌ Supabase клиент не найден для проверки дублирования")
1711
2051
  return False
1712
-
2052
+
1713
2053
  try:
1714
2054
  # Строим запрос для поиска аналогичных событий
1715
- query = supabase_client.client.table('scheduled_events').select('id').eq('event_type', event_type)
1716
-
2055
+ query = (
2056
+ supabase_client.client.table("scheduled_events")
2057
+ .select("id")
2058
+ .eq("event_type", event_type)
2059
+ )
2060
+
1717
2061
  # Для глобальных событий (user_id = None)
1718
2062
  if user_id is None:
1719
- query = query.is_('user_id', 'null')
2063
+ query = query.is_("user_id", "null")
1720
2064
  else:
1721
- query = query.eq('user_id', user_id)
1722
-
2065
+ query = query.eq("user_id", user_id)
2066
+
1723
2067
  # Добавляем фильтр по статусам (pending, immediate, completed)
1724
- query = query.in_('status', ['pending', 'immediate', 'completed'])
1725
-
2068
+ query = query.in_("status", ["pending", "immediate", "completed"])
2069
+
1726
2070
  # Если есть session_id, добавляем его в фильтр
1727
2071
  if session_id:
1728
- query = query.eq('session_id', session_id)
1729
-
2072
+ query = query.eq("session_id", session_id)
2073
+
1730
2074
  # 🆕 Фильтруем по bot_id если указан
1731
2075
  if supabase_client.bot_id:
1732
- query = query.eq('bot_id', supabase_client.bot_id)
1733
-
2076
+ query = query.eq("bot_id", supabase_client.bot_id)
2077
+
1734
2078
  response = query.execute()
1735
-
2079
+
1736
2080
  if response.data:
1737
- logger.info(f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'")
2081
+ logger.info(
2082
+ f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'"
2083
+ )
1738
2084
  return True
1739
-
2085
+
1740
2086
  return False
1741
-
2087
+
1742
2088
  except Exception as e:
1743
2089
  logger.error(f"❌ Ошибка проверки дублирования для '{event_type}': {e}")
1744
2090
  return False
@@ -1747,179 +2093,209 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
1747
2093
  async def process_admin_event(event: Dict):
1748
2094
  """
1749
2095
  Обрабатывает одно админское событие - скачивает файлы из Storage и отправляет пользователям
1750
-
2096
+
1751
2097
  Args:
1752
2098
  event: Событие из БД с данными для отправки
1753
2099
  """
1754
2100
  import json
1755
- import os
1756
2101
  import shutil
1757
2102
  from pathlib import Path
1758
- from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaDocument, InputMediaVideo
1759
-
1760
- event_id = event['id']
1761
- event_name = event['event_type']
1762
- event_data_str = event['event_data']
1763
-
2103
+
2104
+ from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo
2105
+
2106
+ event_id = event["id"]
2107
+ event_name = event["event_type"]
2108
+ event_data_str = event["event_data"]
2109
+
1764
2110
  try:
1765
2111
  event_data = json.loads(event_data_str)
1766
2112
  except Exception as e:
1767
2113
  logger.error(f"❌ Не удалось распарсить event_data для события {event_id}: {e}")
1768
2114
  return {
1769
- 'success_count': 0,
1770
- 'failed_count': 0,
1771
- 'total_users': 0,
1772
- 'error': f'Ошибка парсинга event_data: {str(e)}'
2115
+ "success_count": 0,
2116
+ "failed_count": 0,
2117
+ "total_users": 0,
2118
+ "error": f"Ошибка парсинга event_data: {str(e)}",
1773
2119
  }
1774
-
1775
- segment = event_data.get('segment')
1776
- message_text = event_data.get('message')
1777
- files_metadata = event_data.get('files', [])
1778
-
1779
- logger.info(f"📨 Обработка события '{event_name}': сегмент='{segment}', файлов={len(files_metadata)}")
1780
-
2120
+
2121
+ segment = event_data.get("segment")
2122
+ message_text = event_data.get("message")
2123
+ files_metadata = event_data.get("files", [])
2124
+
2125
+ logger.info(
2126
+ f"📨 Обработка события '{event_name}': сегмент='{segment}', файлов={len(files_metadata)}"
2127
+ )
2128
+
1781
2129
  # Получаем клиенты
1782
2130
  supabase_client = get_supabase_client()
1783
2131
  if not supabase_client:
1784
2132
  logger.error("❌ Supabase клиент не найден")
1785
- return {'success_count': 0, 'failed_count': 0, 'total_users': 0, 'error': 'Нет Supabase клиента'}
1786
-
2133
+ return {
2134
+ "success_count": 0,
2135
+ "failed_count": 0,
2136
+ "total_users": 0,
2137
+ "error": "Нет Supabase клиента",
2138
+ }
2139
+
1787
2140
  from ..handlers.handlers import get_global_var
1788
- bot = get_global_var('bot')
2141
+
2142
+ bot = get_global_var("bot")
1789
2143
  if not bot:
1790
2144
  logger.error("❌ Бот не найден")
1791
- return {'success_count': 0, 'failed_count': 0, 'total_users': 0, 'error': 'Нет бота'}
1792
-
2145
+ return {
2146
+ "success_count": 0,
2147
+ "failed_count": 0,
2148
+ "total_users": 0,
2149
+ "error": "Нет бота",
2150
+ }
2151
+
1793
2152
  # Создаем временные папки
1794
2153
  temp_with_msg = Path("temp_with_msg")
1795
2154
  temp_after_msg = Path("temp_after_msg")
1796
2155
  temp_with_msg.mkdir(exist_ok=True)
1797
2156
  temp_after_msg.mkdir(exist_ok=True)
1798
-
2157
+
1799
2158
  try:
1800
2159
  # 1. Скачиваем файлы из Storage
1801
2160
  for file_info in files_metadata:
1802
2161
  try:
1803
2162
  file_bytes = await supabase_client.download_event_file(
1804
- event_id=event_id,
1805
- storage_path=file_info['storage_path']
2163
+ event_id=event_id, storage_path=file_info["storage_path"]
1806
2164
  )
1807
-
2165
+
1808
2166
  # Сохраняем в соответствующую папку
1809
- if file_info['stage'] == 'with_message':
1810
- file_path = temp_with_msg / file_info['original_name']
2167
+ if file_info["stage"] == "with_message":
2168
+ file_path = temp_with_msg / file_info["original_name"]
1811
2169
  else:
1812
- file_path = temp_after_msg / file_info['original_name']
1813
-
1814
- with open(file_path, 'wb') as f:
2170
+ file_path = temp_after_msg / file_info["original_name"]
2171
+
2172
+ with open(file_path, "wb") as f:
1815
2173
  f.write(file_bytes)
1816
-
2174
+
1817
2175
  logger.info(f"📥 Скачан файл: {file_path}")
1818
-
2176
+
1819
2177
  except Exception as e:
1820
2178
  logger.error(f"❌ Ошибка скачивания файла {file_info['name']}: {e}")
1821
2179
  raise
1822
-
2180
+
1823
2181
  # 2. Получаем пользователей
1824
2182
  users = await supabase_client.get_users_by_segment(segment)
1825
-
2183
+
1826
2184
  if not users:
1827
2185
  logger.warning(f"⚠️ Нет пользователей для сегмента '{segment}'")
1828
2186
  return {
1829
- 'success_count': 0,
1830
- 'failed_count': 0,
1831
- 'total_users': 0,
1832
- 'segment': segment or 'Все',
1833
- 'warning': 'Нет пользователей'
2187
+ "success_count": 0,
2188
+ "failed_count": 0,
2189
+ "total_users": 0,
2190
+ "segment": segment or "Все",
2191
+ "warning": "Нет пользователей",
1834
2192
  }
1835
-
2193
+
1836
2194
  success_count = 0
1837
2195
  failed_count = 0
1838
-
2196
+
1839
2197
  # 3. Отправляем каждому пользователю
1840
2198
  for user in users:
1841
- telegram_id = user['telegram_id']
1842
-
2199
+ telegram_id = user["telegram_id"]
2200
+
1843
2201
  try:
1844
2202
  # 3.1. Отправляем медиа-группу с сообщением
1845
- files_with_msg = [f for f in files_metadata if f['stage'] == 'with_message']
1846
-
2203
+ files_with_msg = [
2204
+ f for f in files_metadata if f["stage"] == "with_message"
2205
+ ]
2206
+
1847
2207
  if files_with_msg:
1848
2208
  media_group = []
1849
2209
  first_file = True
1850
-
2210
+
1851
2211
  # Сортируем файлы по порядку
1852
- sorted_files = sorted(files_with_msg, key=lambda x: x.get('order', 0))
1853
-
2212
+ sorted_files = sorted(
2213
+ files_with_msg, key=lambda x: x.get("order", 0)
2214
+ )
2215
+
1854
2216
  for file_info in sorted_files:
1855
- file_path = temp_with_msg / file_info['original_name']
1856
-
1857
- if file_info['type'] == 'photo':
2217
+ file_path = temp_with_msg / file_info["original_name"]
2218
+
2219
+ if file_info["type"] == "photo":
1858
2220
  media = InputMediaPhoto(
1859
2221
  media=FSInputFile(file_path),
1860
2222
  caption=message_text if first_file else None,
1861
- parse_mode='Markdown' if first_file else None
2223
+ parse_mode="Markdown" if first_file else None,
1862
2224
  )
1863
2225
  media_group.append(media)
1864
- elif file_info['type'] == 'video':
2226
+ elif file_info["type"] == "video":
1865
2227
  media = InputMediaVideo(
1866
2228
  media=FSInputFile(file_path),
1867
2229
  caption=message_text if first_file else None,
1868
- parse_mode='Markdown' if first_file else None
2230
+ parse_mode="Markdown" if first_file else None,
1869
2231
  )
1870
2232
  media_group.append(media)
1871
-
2233
+
1872
2234
  first_file = False
1873
-
2235
+
1874
2236
  if media_group:
1875
- await bot.send_media_group(chat_id=telegram_id, media=media_group)
2237
+ await bot.send_media_group(
2238
+ chat_id=telegram_id, media=media_group
2239
+ )
1876
2240
  else:
1877
2241
  # Только текст без файлов
1878
- await bot.send_message(chat_id=telegram_id, text=message_text, parse_mode='Markdown')
1879
-
2242
+ await bot.send_message(
2243
+ chat_id=telegram_id, text=message_text, parse_mode="Markdown"
2244
+ )
2245
+
1880
2246
  # 3.2. Отправляем файлы после сообщения
1881
- files_after = [f for f in files_metadata if f['stage'] == 'after_message']
1882
-
2247
+ files_after = [
2248
+ f for f in files_metadata if f["stage"] == "after_message"
2249
+ ]
2250
+
1883
2251
  for file_info in files_after:
1884
- file_path = temp_after_msg / file_info['original_name']
1885
-
1886
- if file_info['type'] == 'document':
1887
- await bot.send_document(chat_id=telegram_id, document=FSInputFile(file_path))
1888
- elif file_info['type'] == 'photo':
1889
- await bot.send_photo(chat_id=telegram_id, photo=FSInputFile(file_path))
1890
- elif file_info['type'] == 'video':
1891
- await bot.send_video(chat_id=telegram_id, video=FSInputFile(file_path))
1892
-
2252
+ file_path = temp_after_msg / file_info["original_name"]
2253
+
2254
+ if file_info["type"] == "document":
2255
+ await bot.send_document(
2256
+ chat_id=telegram_id, document=FSInputFile(file_path)
2257
+ )
2258
+ elif file_info["type"] == "photo":
2259
+ await bot.send_photo(
2260
+ chat_id=telegram_id, photo=FSInputFile(file_path)
2261
+ )
2262
+ elif file_info["type"] == "video":
2263
+ await bot.send_video(
2264
+ chat_id=telegram_id, video=FSInputFile(file_path)
2265
+ )
2266
+
1893
2267
  success_count += 1
1894
2268
  logger.info(f"✅ Отправлено пользователю {telegram_id}")
1895
-
2269
+
1896
2270
  except Exception as e:
1897
2271
  logger.error(f"❌ Ошибка отправки пользователю {telegram_id}: {e}")
1898
2272
  failed_count += 1
1899
-
1900
- logger.info(f"📊 Результат '{event_name}': успешно={success_count}, ошибок={failed_count}")
1901
-
2273
+
2274
+ logger.info(
2275
+ f"📊 Результат '{event_name}': успешно={success_count}, ошибок={failed_count}"
2276
+ )
2277
+
1902
2278
  # 4. Очистка после успешной отправки
1903
2279
  # 4.1. Удаляем локальные временные файлы
1904
2280
  shutil.rmtree(temp_with_msg, ignore_errors=True)
1905
2281
  shutil.rmtree(temp_after_msg, ignore_errors=True)
1906
2282
  logger.info("🗑️ Временные папки очищены")
1907
-
1908
- # 4.2. Удаляем файлы из Supabase Storage
2283
+
2284
+ # 4.2. Удаляем файлы из Supabase Storage
1909
2285
  try:
1910
2286
  await supabase_client.delete_event_files(event_id)
1911
2287
  logger.info(f"🗑️ Файлы события '{event_id}' удалены из Storage")
1912
2288
  except Exception as e:
1913
2289
  logger.error(f"❌ Ошибка удаления из Storage: {e}")
1914
-
2290
+
1915
2291
  return {
1916
- 'success_count': success_count,
1917
- 'failed_count': failed_count,
1918
- 'total_users': len(users),
1919
- 'segment': segment or 'Все пользователи',
1920
- 'files_count': len(files_metadata)
2292
+ "success_count": success_count,
2293
+ "failed_count": failed_count,
2294
+ "total_users": len(users),
2295
+ "segment": segment or "Все пользователи",
2296
+ "files_count": len(files_metadata),
1921
2297
  }
1922
-
2298
+
1923
2299
  except Exception as e:
1924
2300
  # В случае ошибки все равно чистим временные файлы
1925
2301
  shutil.rmtree(temp_with_msg, ignore_errors=True)
@@ -1927,34 +2303,34 @@ async def process_admin_event(event: Dict):
1927
2303
  logger.error(f"❌ Критическая ошибка обработки события: {e}")
1928
2304
  raise
1929
2305
 
2306
+
1930
2307
  # =============================================================================
1931
2308
  # ФУНКЦИЯ ДЛЯ ПОДГОТОВКИ ДАННЫХ ДАШБОРДА
1932
2309
  # =============================================================================
1933
2310
 
2311
+
1934
2312
  async def prepare_dashboard_info(
1935
- description_template: str,
1936
- title: str,
1937
- user_id: int
2313
+ description_template: str, title: str, user_id: int
1938
2314
  ) -> Dict[str, Any]:
1939
2315
  """
1940
2316
  Подготавливает данные для дашборда (БЕЗ записи в БД)
1941
-
2317
+
1942
2318
  Возвращаемый dict нужно поместить в поле 'info' результата обработчика.
1943
2319
  bot_utils.py автоматически запишет его в столбец info_dashboard таблицы.
1944
-
2320
+
1945
2321
  Args:
1946
2322
  description_template: Строка с {username}, например "{username} купил подписку"
1947
2323
  title: Заголовок для дашборда
1948
2324
  user_id: Telegram ID
1949
-
2325
+
1950
2326
  Returns:
1951
2327
  Dict с данными для дашборда
1952
-
2328
+
1953
2329
  Example:
1954
2330
  @event_router.event_handler("collect_phone", notify=True)
1955
2331
  async def handle_phone_collection(user_id: int, phone_number: str):
1956
2332
  # ... бизнес-логика ...
1957
-
2333
+
1958
2334
  return {
1959
2335
  "status": "success",
1960
2336
  "phone": phone_number,
@@ -1966,29 +2342,33 @@ async def prepare_dashboard_info(
1966
2342
  }
1967
2343
  """
1968
2344
  supabase_client = get_supabase_client()
1969
-
2345
+
1970
2346
  # Получаем username из sales_users
1971
2347
  username = f"user_{user_id}" # fallback
1972
2348
  if supabase_client:
1973
2349
  try:
1974
- query = supabase_client.client.table('sales_users').select('username').eq('telegram_id', user_id)
2350
+ query = (
2351
+ supabase_client.client.table("sales_users")
2352
+ .select("username")
2353
+ .eq("telegram_id", user_id)
2354
+ )
1975
2355
  if supabase_client.bot_id:
1976
- query = query.eq('bot_id', supabase_client.bot_id)
2356
+ query = query.eq("bot_id", supabase_client.bot_id)
1977
2357
  response = query.execute()
1978
2358
  if response.data:
1979
- username = response.data[0].get('username') or username
2359
+ username = response.data[0].get("username") or username
1980
2360
  except Exception as e:
1981
2361
  logger.warning(f"⚠️ Не удалось получить username для дашборда: {e}")
1982
-
2362
+
1983
2363
  # Форматируем строку
1984
2364
  description = description_template.format(username=username)
1985
-
2365
+
1986
2366
  # Московское время (UTC+3)
1987
2367
  moscow_tz = timezone(timedelta(hours=3))
1988
2368
  moscow_time = datetime.now(moscow_tz)
1989
-
2369
+
1990
2370
  return {
1991
- 'title': title,
1992
- 'description': description,
1993
- 'created_at': moscow_time.isoformat()
2371
+ "title": title,
2372
+ "description": description,
2373
+ "created_at": moscow_time.isoformat(),
1994
2374
  }