smart-bot-factory 0.3.6__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 +480 -324
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1145 -739
  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 +682 -466
  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.6.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.6.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.6.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,523 +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
+
1323
+ # 🆕 Добавляем bot_id если указан
1324
+ if supabase_client.bot_id:
1325
+ event_record["bot_id"] = supabase_client.bot_id
1326
+
1138
1327
  try:
1139
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1140
- 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"]
1141
1334
  logger.info(f"💾 Событие сохранено в БД: {event_id}")
1142
1335
  return event_id
1143
1336
  except Exception as e:
1144
1337
  logger.error(f"❌ Ошибка сохранения события в БД: {e}")
1145
1338
  raise
1146
1339
 
1340
+
1147
1341
  async def save_scheduled_task(
1148
1342
  task_name: str,
1149
1343
  user_id: int,
1150
1344
  user_data: str,
1151
1345
  delay_seconds: int,
1152
- session_id: str = None
1346
+ session_id: str = None,
1153
1347
  ) -> str:
1154
1348
  """Сохраняет запланированную задачу"""
1155
-
1349
+
1156
1350
  supabase_client = get_supabase_client()
1157
1351
  if not supabase_client:
1158
1352
  logger.error("❌ Supabase клиент не найден")
1159
1353
  raise RuntimeError("Supabase клиент не инициализирован")
1160
-
1354
+
1161
1355
  # Проверяем, нужно ли предотвращать дублирование
1162
1356
  router_manager = get_router_manager()
1163
1357
  if router_manager:
1164
1358
  scheduled_tasks = router_manager.get_scheduled_tasks()
1165
1359
  else:
1166
1360
  scheduled_tasks = _scheduled_tasks
1167
-
1361
+
1168
1362
  task_info = scheduled_tasks.get(task_name, {})
1169
- once_only = task_info.get('once_only', True)
1170
-
1363
+ once_only = task_info.get("once_only", True)
1364
+
1171
1365
  if once_only:
1172
1366
  # Проверяем, была ли уже запланирована аналогичная задача для этого пользователя
1173
- 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
+ )
1174
1370
  if already_processed:
1175
- logger.info(f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем")
1371
+ logger.info(
1372
+ f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем"
1373
+ )
1176
1374
  raise ValueError(f"Задача '{task_name}' уже запланирована (once_only=True)")
1177
-
1375
+
1178
1376
  scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
1179
-
1377
+
1180
1378
  event_record = {
1181
- 'event_type': task_name,
1182
- 'event_category': 'scheduled_task',
1183
- 'user_id': user_id,
1184
- 'event_data': user_data,
1185
- 'scheduled_at': scheduled_at.isoformat(),
1186
- 'status': 'pending',
1187
- '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,
1188
1386
  }
1189
-
1387
+
1388
+ # 🆕 Добавляем bot_id если указан
1389
+ if supabase_client.bot_id:
1390
+ event_record["bot_id"] = supabase_client.bot_id
1391
+
1190
1392
  try:
1191
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1192
- event_id = response.data[0]['id']
1193
- 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
+ )
1194
1402
  return event_id
1195
1403
  except Exception as e:
1196
1404
  logger.error(f"❌ Ошибка сохранения запланированной задачи в БД: {e}")
1197
1405
  raise
1198
1406
 
1407
+
1199
1408
  async def save_global_event(
1200
- handler_type: str,
1201
- handler_data: str,
1202
- delay_seconds: int = 0
1409
+ handler_type: str, handler_data: str, delay_seconds: int = 0
1203
1410
  ) -> str:
1204
1411
  """Сохраняет глобальное событие"""
1205
-
1412
+
1206
1413
  supabase_client = get_supabase_client()
1207
1414
  if not supabase_client:
1208
1415
  logger.error("❌ Supabase клиент не найден")
1209
1416
  raise RuntimeError("Supabase клиент не инициализирован")
1210
-
1417
+
1211
1418
  # Проверяем, нужно ли предотвращать дублирование
1212
1419
  router_manager = get_router_manager()
1213
1420
  if router_manager:
1214
1421
  global_handlers = router_manager.get_global_handlers()
1215
1422
  else:
1216
1423
  global_handlers = _global_handlers
1217
-
1424
+
1218
1425
  handler_info = global_handlers.get(handler_type, {})
1219
- once_only = handler_info.get('once_only', True)
1220
-
1426
+ once_only = handler_info.get("once_only", True)
1427
+
1221
1428
  if once_only:
1222
1429
  # Проверяем, было ли уже запланировано аналогичное глобальное событие
1223
- 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
+ )
1224
1433
  if already_processed:
1225
- logger.info(f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем")
1226
- raise ValueError(f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)")
1227
-
1434
+ logger.info(
1435
+ f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем"
1436
+ )
1437
+ raise ValueError(
1438
+ f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)"
1439
+ )
1440
+
1228
1441
  scheduled_at = None
1229
- status = 'immediate'
1230
-
1442
+ status = "immediate"
1443
+
1231
1444
  if delay_seconds > 0:
1232
1445
  scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
1233
- status = 'pending'
1234
-
1446
+ status = "pending"
1447
+
1235
1448
  event_record = {
1236
- 'event_type': handler_type,
1237
- 'event_category': 'global_handler',
1238
- 'user_id': None, # Глобальное событие
1239
- 'event_data': handler_data,
1240
- 'scheduled_at': scheduled_at.isoformat() if scheduled_at else None,
1241
- '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,
1242
1455
  }
1243
-
1456
+
1457
+ # 🆕 Добавляем bot_id если указан (глобальные события тоже привязаны к боту)
1458
+ if supabase_client.bot_id:
1459
+ event_record["bot_id"] = supabase_client.bot_id
1460
+
1244
1461
  try:
1245
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1246
- 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"]
1247
1468
  logger.info(f"🌍 Глобальное событие сохранено в БД: {event_id}")
1248
1469
  return event_id
1249
1470
  except Exception as e:
1250
1471
  logger.error(f"❌ Ошибка сохранения глобального события в БД: {e}")
1251
1472
  raise
1252
1473
 
1474
+
1253
1475
  async def update_event_result(
1254
- event_id: str,
1255
- status: str,
1256
- result_data: Any = None,
1257
- error_message: str = None
1476
+ event_id: str, status: str, result_data: Any = None, error_message: str = None
1258
1477
  ):
1259
1478
  """Обновляет результат выполнения события"""
1260
-
1479
+
1261
1480
  supabase_client = get_supabase_client()
1262
1481
  if not supabase_client:
1263
1482
  logger.error("❌ Supabase клиент не найден")
1264
1483
  return
1265
-
1484
+
1266
1485
  update_data = {
1267
- 'status': status,
1268
- 'executed_at': datetime.now(timezone.utc).isoformat()
1486
+ "status": status,
1487
+ "executed_at": datetime.now(timezone.utc).isoformat(),
1269
1488
  }
1270
-
1489
+
1271
1490
  if result_data:
1272
1491
  import json
1273
- update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
1274
-
1492
+
1493
+ update_data["result_data"] = json.dumps(result_data, ensure_ascii=False)
1494
+
1275
1495
  # Проверяем наличие поля 'info' для дашборда
1276
- if isinstance(result_data, dict) and 'info' in result_data:
1277
- 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
+ )
1278
1500
  logger.info(f"📊 Дашборд данные добавлены в событие {event_id}")
1279
-
1501
+
1280
1502
  if error_message:
1281
- update_data['last_error'] = error_message
1503
+ update_data["last_error"] = error_message
1282
1504
  # Получаем текущее количество попыток
1283
1505
  try:
1284
- current_retry = supabase_client.client.table('scheduled_events').select('retry_count').eq('id', event_id).execute().data[0]['retry_count']
1285
- update_data['retry_count'] = current_retry + 1
1286
- except:
1287
- update_data['retry_count'] = 1
1288
-
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
+
1289
1518
  try:
1290
- 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()
1291
1522
  logger.info(f"📝 Результат события {event_id} обновлен: {status}")
1292
1523
  except Exception as e:
1293
1524
  logger.error(f"❌ Ошибка обновления результата события {event_id}: {e}")
1294
1525
 
1526
+
1295
1527
  async def get_pending_events(limit: int = 50) -> list:
1296
1528
  """Получает события готовые к выполнению СЕЙЧАС"""
1297
-
1529
+
1298
1530
  supabase_client = get_supabase_client()
1299
1531
  if not supabase_client:
1300
1532
  logger.error("❌ Supabase клиент не найден")
1301
1533
  return []
1302
-
1534
+
1303
1535
  try:
1304
1536
  now = datetime.now(timezone.utc).isoformat()
1305
-
1306
- response = supabase_client.client.table('scheduled_events')\
1307
- .select('*')\
1308
- .in_('status', ['pending', 'immediate'])\
1309
- .or_(f'scheduled_at.is.null,scheduled_at.lte.{now}')\
1310
- .order('created_at')\
1311
- .limit(limit)\
1312
- .execute()
1313
-
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")
1544
+ .limit(limit)
1545
+ )
1546
+
1547
+ # 🆕 Фильтруем по bot_id если указан
1548
+ if supabase_client.bot_id:
1549
+ query = query.eq("bot_id", supabase_client.bot_id)
1550
+
1551
+ response = query.execute()
1552
+
1314
1553
  return response.data
1315
1554
  except Exception as e:
1316
1555
  logger.error(f"❌ Ошибка получения событий из БД: {e}")
1317
1556
  return []
1318
1557
 
1558
+
1319
1559
  async def get_pending_events_in_next_minute(limit: int = 100) -> list:
1320
1560
  """Получает события готовые к выполнению в течение следующей минуты"""
1321
-
1561
+
1322
1562
  supabase_client = get_supabase_client()
1323
1563
  if not supabase_client:
1324
1564
  logger.error("❌ Supabase клиент не найден")
1325
1565
  return []
1326
-
1566
+
1327
1567
  try:
1328
1568
  now = datetime.now(timezone.utc)
1329
1569
  next_minute = now + timedelta(seconds=60)
1330
-
1331
- response = supabase_client.client.table('scheduled_events')\
1332
- .select('*')\
1333
- .in_('status', ['pending', 'immediate'])\
1334
- .or_(f'scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}')\
1335
- .order('created_at')\
1336
- .limit(limit)\
1337
- .execute()
1338
-
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")
1577
+ .limit(limit)
1578
+ )
1579
+
1580
+ # 🆕 Фильтруем по bot_id если указан
1581
+ if supabase_client.bot_id:
1582
+ query = query.eq("bot_id", supabase_client.bot_id)
1583
+
1584
+ response = query.execute()
1585
+
1339
1586
  return response.data
1340
1587
  except Exception as e:
1341
1588
  logger.error(f"❌ Ошибка получения событий из БД: {e}")
1342
1589
  return []
1343
1590
 
1591
+
1344
1592
  async def background_event_processor():
1345
1593
  """Фоновый процессор для ВСЕХ типов событий включая админские (проверяет БД каждую минуту)"""
1346
-
1347
- logger.info("🔄 Запуск фонового процессора событий (user_event, scheduled_task, global_handler, admin_event)")
1348
-
1594
+
1595
+ logger.info(
1596
+ "🔄 Запуск фонового процессора событий (user_event, scheduled_task, global_handler, admin_event)"
1597
+ )
1598
+
1349
1599
  while True:
1350
1600
  try:
1351
1601
  # Получаем события готовые к выполнению в следующую минуту
1352
1602
  pending_events = await get_pending_events_in_next_minute(limit=100)
1353
-
1603
+
1354
1604
  if pending_events:
1355
1605
  logger.info(f"📋 Найдено {len(pending_events)} событий для обработки")
1356
-
1606
+
1357
1607
  for event in pending_events:
1358
1608
  try:
1359
- event_type = event['event_type']
1360
- event_category = event['event_category']
1361
- user_id = event.get('user_id')
1362
- session_id = event.get('session_id')
1363
-
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
+
1364
1614
  # ========== ОБРАБОТКА АДМИНСКИХ СОБЫТИЙ ==========
1365
- if event_category == 'admin_event':
1615
+ if event_category == "admin_event":
1366
1616
  try:
1367
1617
  # Обрабатываем и получаем результат
1368
1618
  result = await process_admin_event(event)
1369
-
1619
+
1370
1620
  # Сохраняем результат в result_data
1371
1621
  import json
1622
+
1372
1623
  supabase_client = get_supabase_client()
1373
- supabase_client.client.table('scheduled_events').update({
1374
- 'status': 'completed',
1375
- 'executed_at': datetime.now(timezone.utc).isoformat(),
1376
- 'result_data': json.dumps(result, ensure_ascii=False) if result else None
1377
- }).eq('id', event['id']).execute()
1378
-
1379
- 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
+ )
1380
1641
  continue
1381
-
1642
+
1382
1643
  except Exception as e:
1383
- logger.error(f"❌ Ошибка обработки админского события {event['id']}: {e}")
1384
-
1644
+ logger.error(
1645
+ f"❌ Ошибка обработки админского события {event['id']}: {e}"
1646
+ )
1647
+
1385
1648
  # Обновляем статус на failed
1386
1649
  supabase_client = get_supabase_client()
1387
- supabase_client.client.table('scheduled_events').update({
1388
- 'status': 'failed',
1389
- 'last_error': str(e),
1390
- 'executed_at': datetime.now(timezone.utc).isoformat()
1391
- }).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()
1392
1659
  continue
1393
-
1660
+
1394
1661
  # ========== ОБРАБОТКА USER СОБЫТИЙ ==========
1395
- if event_category == 'user_event':
1662
+ if event_category == "user_event":
1396
1663
  router_manager = get_router_manager()
1397
1664
  if router_manager:
1398
1665
  event_handlers = router_manager.get_event_handlers()
1399
1666
  else:
1400
1667
  event_handlers = _event_handlers
1401
-
1668
+
1402
1669
  event_handler_info = event_handlers.get(event_type, {})
1403
- once_only = event_handler_info.get('once_only', True)
1404
-
1670
+ once_only = event_handler_info.get("once_only", True)
1671
+
1405
1672
  if once_only:
1406
1673
  # Проверяем, было ли уже выполнено это событие для данного пользователя
1407
1674
  supabase_client = get_supabase_client()
1408
- check_query = supabase_client.client.table('scheduled_events')\
1409
- .select('id')\
1410
- .eq('event_type', event_type)\
1411
- .eq('user_id', user_id)\
1412
- .eq('status', 'completed')\
1413
- .neq('id', event['id']) # Исключаем текущее событие
1414
-
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
+
1415
1684
  if session_id:
1416
- check_query = check_query.eq('session_id', session_id)
1417
-
1685
+ check_query = check_query.eq(
1686
+ "session_id", session_id
1687
+ )
1688
+
1418
1689
  existing = check_query.execute()
1419
-
1690
+
1420
1691
  if existing.data:
1421
- await update_event_result(event['id'], 'cancelled', {"reason": "already_executed_once_only"})
1422
- 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
+ )
1423
1700
  continue
1424
-
1701
+
1425
1702
  # Для scheduled_task - проверяем smart_check и once_only
1426
- if event_category == 'scheduled_task':
1703
+ if event_category == "scheduled_task":
1427
1704
  router_manager = get_router_manager()
1428
- 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
+ )
1429
1710
  task_info = scheduled_tasks.get(event_type, {})
1430
- use_smart_check = task_info.get('smart_check', True)
1431
- once_only = task_info.get('once_only', True)
1432
-
1711
+ use_smart_check = task_info.get("smart_check", True)
1712
+ once_only = task_info.get("once_only", True)
1713
+
1433
1714
  # Проверяем once_only для задач
1434
1715
  if once_only:
1435
1716
  supabase_client = get_supabase_client()
1436
- check_query = supabase_client.client.table('scheduled_events')\
1437
- .select('id')\
1438
- .eq('event_type', event_type)\
1439
- .eq('user_id', user_id)\
1440
- .eq('status', 'completed')\
1441
- .neq('id', event['id'])
1442
-
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
+
1443
1726
  if session_id:
1444
- check_query = check_query.eq('session_id', session_id)
1445
-
1727
+ check_query = check_query.eq(
1728
+ "session_id", session_id
1729
+ )
1730
+
1446
1731
  existing = check_query.execute()
1447
-
1732
+
1448
1733
  if existing.data:
1449
- await update_event_result(event['id'], 'cancelled', {"reason": "already_executed_once_only"})
1450
- 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
+ )
1451
1742
  continue
1452
-
1743
+
1453
1744
  if use_smart_check:
1454
1745
  # Умная проверка
1455
1746
  check_result = await smart_execute_check(
1456
- event['id'],
1457
- user_id,
1747
+ event["id"],
1748
+ user_id,
1458
1749
  session_id,
1459
1750
  event_type,
1460
- event['event_data']
1751
+ event["event_data"],
1461
1752
  )
1462
-
1463
- if check_result['action'] == 'cancel':
1464
- await update_event_result(event['id'], 'cancelled', {"reason": check_result['reason']})
1465
- 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
+ )
1466
1763
  continue
1467
- elif check_result['action'] == 'reschedule':
1764
+ elif check_result["action"] == "reschedule":
1468
1765
  # Обновляем scheduled_at в БД
1469
- 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"])
1470
1769
  supabase_client = get_supabase_client()
1471
- supabase_client.client.table('scheduled_events').update({
1472
- 'scheduled_at': new_scheduled_at.isoformat(),
1473
- 'status': 'pending'
1474
- }).eq('id', event['id']).execute()
1475
- 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
+ )
1476
1783
  continue
1477
-
1784
+
1478
1785
  # Выполняем событие
1479
1786
  result = await process_scheduled_event(event)
1480
-
1787
+
1481
1788
  # Проверяем наличие поля 'info' для дашборда
1482
1789
  result_data = {"processed": True}
1483
1790
  if isinstance(result, dict):
1484
1791
  result_data.update(result)
1485
- if 'info' in result:
1486
- logger.info(f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}")
1487
-
1488
- 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)
1489
1798
  logger.info(f"✅ Событие {event['id']} выполнено")
1490
-
1799
+
1491
1800
  except Exception as e:
1492
1801
  logger.error(f"❌ Ошибка обработки события {event['id']}: {e}")
1493
- await update_event_result(event['id'], 'failed', None, str(e))
1494
-
1802
+ await update_event_result(event["id"], "failed", None, str(e))
1803
+
1495
1804
  await asyncio.sleep(60) # Проверяем каждую минуту
1496
-
1805
+
1497
1806
  except Exception as e:
1498
1807
  logger.error(f"❌ Ошибка в фоновом процессоре: {e}")
1499
1808
  await asyncio.sleep(60)
1500
1809
 
1810
+
1501
1811
  async def process_scheduled_event(event: Dict):
1502
1812
  """Обрабатывает одно событие из БД и возвращает результат"""
1503
-
1504
- event_type = event['event_type']
1505
- event_category = event['event_category']
1506
- event_data = event['event_data']
1507
- user_id = event.get('user_id')
1508
-
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
+
1509
1819
  logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
1510
-
1820
+
1511
1821
  result = None
1512
- if event_category == 'scheduled_task':
1822
+ if event_category == "scheduled_task":
1513
1823
  result = await execute_scheduled_task(event_type, user_id, event_data)
1514
- elif event_category == 'global_handler':
1824
+ elif event_category == "global_handler":
1515
1825
  result = await execute_global_handler(event_type, event_data)
1516
- elif event_category == 'user_event':
1826
+ elif event_category == "user_event":
1517
1827
  result = await execute_event_handler(event_type, user_id, event_data)
1518
1828
  else:
1519
1829
  logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
1520
-
1830
+
1521
1831
  return result
1522
1832
 
1833
+
1523
1834
  # =============================================================================
1524
1835
  # ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
1525
1836
  # =============================================================================
1526
1837
 
1527
- 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
+ ):
1528
1846
  """Планирует выполнение задачи через указанное время с сохранением в БД (без asyncio.sleep)"""
1529
-
1847
+
1530
1848
  # Проверяем через RouterManager или fallback к старым декораторам
1531
1849
  router_manager = get_router_manager()
1532
1850
  if router_manager:
1533
1851
  scheduled_tasks = router_manager.get_scheduled_tasks()
1534
1852
  else:
1535
1853
  scheduled_tasks = _scheduled_tasks
1536
-
1854
+
1537
1855
  if task_name not in scheduled_tasks:
1538
1856
  import inspect
1857
+
1539
1858
  frame = inspect.currentframe()
1540
1859
  line_no = frame.f_lineno if frame else "unknown"
1541
1860
  available_tasks = list(scheduled_tasks.keys())
1542
- 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
+ )
1543
1864
  raise ValueError(f"Задача '{task_name}' не найдена")
1544
-
1865
+
1545
1866
  human_time = format_seconds_to_human(delay_seconds)
1546
- logger.info(f"⏰ Планируем задачу '{task_name}' через {human_time} ({delay_seconds}с) для user_id={user_id}")
1547
-
1867
+ logger.info(
1868
+ f"⏰ Планируем задачу '{task_name}' через {human_time} ({delay_seconds}с) для user_id={user_id}"
1869
+ )
1870
+
1548
1871
  # Просто сохраняем в БД - фоновый процессор сам выполнит задачу
1549
- event_id = await save_scheduled_task(task_name, user_id, user_data, delay_seconds, session_id)
1550
-
1551
- logger.info(f"💾 Задача '{task_name}' сохранена в БД с ID {event_id}, будет обработана фоновым процессором")
1552
-
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
+
1553
1880
  return {
1554
1881
  "status": "scheduled",
1555
1882
  "task_name": task_name,
1556
1883
  "delay_seconds": delay_seconds,
1557
1884
  "event_id": event_id,
1558
- "scheduled_at": datetime.now(timezone.utc).isoformat()
1885
+ "scheduled_at": datetime.now(timezone.utc).isoformat(),
1559
1886
  }
1560
1887
 
1561
- 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
+ ):
1562
1892
  """Планирует выполнение глобального обработчика через указанное время с сохранением в БД (без asyncio.sleep)"""
1563
-
1893
+
1564
1894
  # Проверяем обработчик через RouterManager или fallback к старым декораторам
1565
1895
  router_manager = get_router_manager()
1566
1896
  if router_manager:
1567
1897
  global_handlers = router_manager.get_global_handlers()
1568
1898
  else:
1569
1899
  global_handlers = _global_handlers
1570
-
1900
+
1571
1901
  if handler_type not in global_handlers:
1572
1902
  raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
1573
-
1574
- logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
1575
-
1903
+
1904
+ logger.info(
1905
+ f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд"
1906
+ )
1907
+
1576
1908
  # Просто сохраняем в БД - фоновый процессор сам выполнит обработчик
1577
1909
  event_id = await save_global_event(handler_type, handler_data, delay_seconds)
1578
-
1579
- logger.info(f"💾 Глобальный обработчик '{handler_type}' сохранен в БД с ID {event_id}, будет обработан фоновым процессором")
1580
-
1910
+
1911
+ logger.info(
1912
+ f"💾 Глобальный обработчик '{handler_type}' сохранен в БД с ID {event_id}, будет обработан фоновым процессором"
1913
+ )
1914
+
1581
1915
  return {
1582
1916
  "status": "scheduled",
1583
1917
  "handler_type": handler_type,
1584
1918
  "delay_seconds": delay_seconds,
1585
1919
  "event_id": event_id,
1586
- "scheduled_at": datetime.now(timezone.utc).isoformat()
1920
+ "scheduled_at": datetime.now(timezone.utc).isoformat(),
1587
1921
  }
1588
1922
 
1589
- 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]:
1590
1927
  """
1591
1928
  Умная проверка перед выполнением запланированной задачи
1592
-
1929
+
1593
1930
  Логика:
1594
1931
  1. Если пользователь перешел на новый этап - отменяем событие
1595
1932
  2. Если прошло меньше времени чем планировалось - переносим на разницу
1596
1933
  3. Если прошло достаточно времени - выполняем
1597
-
1934
+
1598
1935
  Returns:
1599
1936
  Dict с action: 'execute', 'cancel', 'reschedule'
1600
1937
  """
@@ -1602,84 +1939,109 @@ async def smart_execute_check(event_id: str, user_id: int, session_id: str, task
1602
1939
  if not supabase_client:
1603
1940
  logger.error("❌ Supabase клиент не найден для умной проверки")
1604
1941
  return {"action": "execute", "reason": "no_supabase_client"}
1605
-
1942
+
1606
1943
  try:
1607
1944
  # Получаем информацию о последнем сообщении пользователя
1608
1945
  user_info = await supabase_client.get_user_last_message_info(user_id)
1609
-
1946
+
1610
1947
  if not user_info:
1611
1948
  logger.info(f"🔄 Пользователь {user_id} не найден, выполняем задачу")
1612
1949
  return {"action": "execute", "reason": "user_not_found"}
1613
-
1950
+
1614
1951
  # Проверяем, изменился ли этап
1615
- 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
+ )
1616
1955
  if stage_changed:
1617
- logger.info(f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}")
1956
+ logger.info(
1957
+ f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}"
1958
+ )
1618
1959
  return {"action": "cancel", "reason": "user_stage_changed"}
1619
-
1960
+
1620
1961
  # Получаем информацию о событии из БД
1621
- event_response = supabase_client.client.table('scheduled_events').select(
1622
- 'created_at', 'scheduled_at'
1623
- ).eq('id', event_id).execute()
1624
-
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
+
1625
1969
  if not event_response.data:
1626
1970
  logger.error(f"❌ Событие {event_id} не найдено в БД")
1627
1971
  return {"action": "execute", "reason": "event_not_found"}
1628
-
1972
+
1629
1973
  event = event_response.data[0]
1630
- created_at = datetime.fromisoformat(event['created_at'].replace('Z', '+00:00'))
1631
- scheduled_at = datetime.fromisoformat(event['scheduled_at'].replace('Z', '+00:00'))
1632
- last_message_at = datetime.fromisoformat(user_info['last_message_at'].replace('Z', '+00:00'))
1633
-
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
+
1634
1982
  # Вычисляем разницу во времени
1635
1983
  now = datetime.now(timezone.utc)
1636
1984
  time_since_creation = (now - created_at).total_seconds()
1637
1985
  time_since_last_message = (now - last_message_at).total_seconds()
1638
1986
  planned_delay = (scheduled_at - created_at).total_seconds()
1639
-
1987
+
1640
1988
  # Проверяем, писал ли пользователь после создания события
1641
- time_between_creation_and_last_message = (last_message_at - created_at).total_seconds()
1642
-
1989
+ time_between_creation_and_last_message = (
1990
+ last_message_at - created_at
1991
+ ).total_seconds()
1992
+
1643
1993
  logger.info(f"🔄 Анализ для пользователя {user_id}:")
1644
1994
  logger.info(f" Время с создания события: {time_since_creation:.0f}с")
1645
1995
  logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
1646
1996
  logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
1647
- logger.info(f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}")
1648
-
1997
+ logger.info(
1998
+ f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}"
1999
+ )
2000
+
1649
2001
  # Если пользователь писал ПОСЛЕ создания события (недавно активен)
1650
2002
  # И с момента его последнего сообщения прошло меньше planned_delay
1651
- 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
+ ):
1652
2007
  # Пересчитываем время - отправляем через planned_delay после последнего сообщения
1653
2008
  new_delay = max(0, planned_delay - time_since_last_message)
1654
- logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)")
2009
+ logger.info(
2010
+ f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)"
2011
+ )
1655
2012
  return {
1656
- "action": "reschedule",
2013
+ "action": "reschedule",
1657
2014
  "new_delay": new_delay,
1658
- "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",
1659
2016
  }
1660
-
2017
+
1661
2018
  # Если прошло достаточно времени с последнего сообщения - выполняем
1662
2019
  if time_since_last_message >= planned_delay:
1663
- 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
+ )
1664
2023
  return {"action": "execute", "reason": "time_expired_since_last_message"}
1665
-
2024
+
1666
2025
  # Если что-то пошло не так - выполняем
1667
2026
  logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")
1668
2027
  return {"action": "execute", "reason": "unexpected_situation"}
1669
-
2028
+
1670
2029
  except Exception as e:
1671
2030
  logger.error(f"❌ Ошибка в умной проверке для пользователя {user_id}: {e}")
1672
2031
  return {"action": "execute", "reason": f"error_in_check: {str(e)}"}
1673
2032
 
1674
- 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:
1675
2037
  """
1676
2038
  Проверяет, был ли уже обработан аналогичный event_type для пользователя/сессии
1677
-
2039
+
1678
2040
  Args:
1679
2041
  event_type: Тип события
1680
2042
  user_id: ID пользователя (для user_event и scheduled_task)
1681
2043
  session_id: ID сессии (для дополнительной проверки)
1682
-
2044
+
1683
2045
  Returns:
1684
2046
  True если событие уже обрабатывалось или в процессе
1685
2047
  """
@@ -1687,32 +2049,42 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
1687
2049
  if not supabase_client:
1688
2050
  logger.error("❌ Supabase клиент не найден для проверки дублирования")
1689
2051
  return False
1690
-
2052
+
1691
2053
  try:
1692
2054
  # Строим запрос для поиска аналогичных событий
1693
- query = supabase_client.client.table('scheduled_events').select('id').eq('event_type', event_type)
1694
-
2055
+ query = (
2056
+ supabase_client.client.table("scheduled_events")
2057
+ .select("id")
2058
+ .eq("event_type", event_type)
2059
+ )
2060
+
1695
2061
  # Для глобальных событий (user_id = None)
1696
2062
  if user_id is None:
1697
- query = query.is_('user_id', 'null')
2063
+ query = query.is_("user_id", "null")
1698
2064
  else:
1699
- query = query.eq('user_id', user_id)
1700
-
2065
+ query = query.eq("user_id", user_id)
2066
+
1701
2067
  # Добавляем фильтр по статусам (pending, immediate, completed)
1702
- query = query.in_('status', ['pending', 'immediate', 'completed'])
1703
-
2068
+ query = query.in_("status", ["pending", "immediate", "completed"])
2069
+
1704
2070
  # Если есть session_id, добавляем его в фильтр
1705
2071
  if session_id:
1706
- query = query.eq('session_id', session_id)
1707
-
2072
+ query = query.eq("session_id", session_id)
2073
+
2074
+ # 🆕 Фильтруем по bot_id если указан
2075
+ if supabase_client.bot_id:
2076
+ query = query.eq("bot_id", supabase_client.bot_id)
2077
+
1708
2078
  response = query.execute()
1709
-
2079
+
1710
2080
  if response.data:
1711
- logger.info(f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'")
2081
+ logger.info(
2082
+ f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'"
2083
+ )
1712
2084
  return True
1713
-
2085
+
1714
2086
  return False
1715
-
2087
+
1716
2088
  except Exception as e:
1717
2089
  logger.error(f"❌ Ошибка проверки дублирования для '{event_type}': {e}")
1718
2090
  return False
@@ -1721,179 +2093,209 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
1721
2093
  async def process_admin_event(event: Dict):
1722
2094
  """
1723
2095
  Обрабатывает одно админское событие - скачивает файлы из Storage и отправляет пользователям
1724
-
2096
+
1725
2097
  Args:
1726
2098
  event: Событие из БД с данными для отправки
1727
2099
  """
1728
2100
  import json
1729
- import os
1730
2101
  import shutil
1731
2102
  from pathlib import Path
1732
- from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaDocument, InputMediaVideo
1733
-
1734
- event_id = event['id']
1735
- event_name = event['event_type']
1736
- event_data_str = event['event_data']
1737
-
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
+
1738
2110
  try:
1739
2111
  event_data = json.loads(event_data_str)
1740
2112
  except Exception as e:
1741
2113
  logger.error(f"❌ Не удалось распарсить event_data для события {event_id}: {e}")
1742
2114
  return {
1743
- 'success_count': 0,
1744
- 'failed_count': 0,
1745
- 'total_users': 0,
1746
- '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)}",
1747
2119
  }
1748
-
1749
- segment = event_data.get('segment')
1750
- message_text = event_data.get('message')
1751
- files_metadata = event_data.get('files', [])
1752
-
1753
- logger.info(f"📨 Обработка события '{event_name}': сегмент='{segment}', файлов={len(files_metadata)}")
1754
-
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
+
1755
2129
  # Получаем клиенты
1756
2130
  supabase_client = get_supabase_client()
1757
2131
  if not supabase_client:
1758
2132
  logger.error("❌ Supabase клиент не найден")
1759
- return {'success_count': 0, 'failed_count': 0, 'total_users': 0, 'error': 'Нет Supabase клиента'}
1760
-
2133
+ return {
2134
+ "success_count": 0,
2135
+ "failed_count": 0,
2136
+ "total_users": 0,
2137
+ "error": "Нет Supabase клиента",
2138
+ }
2139
+
1761
2140
  from ..handlers.handlers import get_global_var
1762
- bot = get_global_var('bot')
2141
+
2142
+ bot = get_global_var("bot")
1763
2143
  if not bot:
1764
2144
  logger.error("❌ Бот не найден")
1765
- return {'success_count': 0, 'failed_count': 0, 'total_users': 0, 'error': 'Нет бота'}
1766
-
2145
+ return {
2146
+ "success_count": 0,
2147
+ "failed_count": 0,
2148
+ "total_users": 0,
2149
+ "error": "Нет бота",
2150
+ }
2151
+
1767
2152
  # Создаем временные папки
1768
2153
  temp_with_msg = Path("temp_with_msg")
1769
2154
  temp_after_msg = Path("temp_after_msg")
1770
2155
  temp_with_msg.mkdir(exist_ok=True)
1771
2156
  temp_after_msg.mkdir(exist_ok=True)
1772
-
2157
+
1773
2158
  try:
1774
2159
  # 1. Скачиваем файлы из Storage
1775
2160
  for file_info in files_metadata:
1776
2161
  try:
1777
2162
  file_bytes = await supabase_client.download_event_file(
1778
- event_id=event_id,
1779
- storage_path=file_info['storage_path']
2163
+ event_id=event_id, storage_path=file_info["storage_path"]
1780
2164
  )
1781
-
2165
+
1782
2166
  # Сохраняем в соответствующую папку
1783
- if file_info['stage'] == 'with_message':
1784
- 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"]
1785
2169
  else:
1786
- file_path = temp_after_msg / file_info['original_name']
1787
-
1788
- 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:
1789
2173
  f.write(file_bytes)
1790
-
2174
+
1791
2175
  logger.info(f"📥 Скачан файл: {file_path}")
1792
-
2176
+
1793
2177
  except Exception as e:
1794
2178
  logger.error(f"❌ Ошибка скачивания файла {file_info['name']}: {e}")
1795
2179
  raise
1796
-
2180
+
1797
2181
  # 2. Получаем пользователей
1798
2182
  users = await supabase_client.get_users_by_segment(segment)
1799
-
2183
+
1800
2184
  if not users:
1801
2185
  logger.warning(f"⚠️ Нет пользователей для сегмента '{segment}'")
1802
2186
  return {
1803
- 'success_count': 0,
1804
- 'failed_count': 0,
1805
- 'total_users': 0,
1806
- 'segment': segment or 'Все',
1807
- 'warning': 'Нет пользователей'
2187
+ "success_count": 0,
2188
+ "failed_count": 0,
2189
+ "total_users": 0,
2190
+ "segment": segment or "Все",
2191
+ "warning": "Нет пользователей",
1808
2192
  }
1809
-
2193
+
1810
2194
  success_count = 0
1811
2195
  failed_count = 0
1812
-
2196
+
1813
2197
  # 3. Отправляем каждому пользователю
1814
2198
  for user in users:
1815
- telegram_id = user['telegram_id']
1816
-
2199
+ telegram_id = user["telegram_id"]
2200
+
1817
2201
  try:
1818
2202
  # 3.1. Отправляем медиа-группу с сообщением
1819
- files_with_msg = [f for f in files_metadata if f['stage'] == 'with_message']
1820
-
2203
+ files_with_msg = [
2204
+ f for f in files_metadata if f["stage"] == "with_message"
2205
+ ]
2206
+
1821
2207
  if files_with_msg:
1822
2208
  media_group = []
1823
2209
  first_file = True
1824
-
2210
+
1825
2211
  # Сортируем файлы по порядку
1826
- sorted_files = sorted(files_with_msg, key=lambda x: x.get('order', 0))
1827
-
2212
+ sorted_files = sorted(
2213
+ files_with_msg, key=lambda x: x.get("order", 0)
2214
+ )
2215
+
1828
2216
  for file_info in sorted_files:
1829
- file_path = temp_with_msg / file_info['original_name']
1830
-
1831
- if file_info['type'] == 'photo':
2217
+ file_path = temp_with_msg / file_info["original_name"]
2218
+
2219
+ if file_info["type"] == "photo":
1832
2220
  media = InputMediaPhoto(
1833
2221
  media=FSInputFile(file_path),
1834
2222
  caption=message_text if first_file else None,
1835
- parse_mode='Markdown' if first_file else None
2223
+ parse_mode="Markdown" if first_file else None,
1836
2224
  )
1837
2225
  media_group.append(media)
1838
- elif file_info['type'] == 'video':
2226
+ elif file_info["type"] == "video":
1839
2227
  media = InputMediaVideo(
1840
2228
  media=FSInputFile(file_path),
1841
2229
  caption=message_text if first_file else None,
1842
- parse_mode='Markdown' if first_file else None
2230
+ parse_mode="Markdown" if first_file else None,
1843
2231
  )
1844
2232
  media_group.append(media)
1845
-
2233
+
1846
2234
  first_file = False
1847
-
2235
+
1848
2236
  if media_group:
1849
- 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
+ )
1850
2240
  else:
1851
2241
  # Только текст без файлов
1852
- await bot.send_message(chat_id=telegram_id, text=message_text, parse_mode='Markdown')
1853
-
2242
+ await bot.send_message(
2243
+ chat_id=telegram_id, text=message_text, parse_mode="Markdown"
2244
+ )
2245
+
1854
2246
  # 3.2. Отправляем файлы после сообщения
1855
- files_after = [f for f in files_metadata if f['stage'] == 'after_message']
1856
-
2247
+ files_after = [
2248
+ f for f in files_metadata if f["stage"] == "after_message"
2249
+ ]
2250
+
1857
2251
  for file_info in files_after:
1858
- file_path = temp_after_msg / file_info['original_name']
1859
-
1860
- if file_info['type'] == 'document':
1861
- await bot.send_document(chat_id=telegram_id, document=FSInputFile(file_path))
1862
- elif file_info['type'] == 'photo':
1863
- await bot.send_photo(chat_id=telegram_id, photo=FSInputFile(file_path))
1864
- elif file_info['type'] == 'video':
1865
- await bot.send_video(chat_id=telegram_id, video=FSInputFile(file_path))
1866
-
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
+
1867
2267
  success_count += 1
1868
2268
  logger.info(f"✅ Отправлено пользователю {telegram_id}")
1869
-
2269
+
1870
2270
  except Exception as e:
1871
2271
  logger.error(f"❌ Ошибка отправки пользователю {telegram_id}: {e}")
1872
2272
  failed_count += 1
1873
-
1874
- logger.info(f"📊 Результат '{event_name}': успешно={success_count}, ошибок={failed_count}")
1875
-
2273
+
2274
+ logger.info(
2275
+ f"📊 Результат '{event_name}': успешно={success_count}, ошибок={failed_count}"
2276
+ )
2277
+
1876
2278
  # 4. Очистка после успешной отправки
1877
2279
  # 4.1. Удаляем локальные временные файлы
1878
2280
  shutil.rmtree(temp_with_msg, ignore_errors=True)
1879
2281
  shutil.rmtree(temp_after_msg, ignore_errors=True)
1880
2282
  logger.info("🗑️ Временные папки очищены")
1881
-
1882
- # 4.2. Удаляем файлы из Supabase Storage
2283
+
2284
+ # 4.2. Удаляем файлы из Supabase Storage
1883
2285
  try:
1884
2286
  await supabase_client.delete_event_files(event_id)
1885
2287
  logger.info(f"🗑️ Файлы события '{event_id}' удалены из Storage")
1886
2288
  except Exception as e:
1887
2289
  logger.error(f"❌ Ошибка удаления из Storage: {e}")
1888
-
2290
+
1889
2291
  return {
1890
- 'success_count': success_count,
1891
- 'failed_count': failed_count,
1892
- 'total_users': len(users),
1893
- 'segment': segment or 'Все пользователи',
1894
- '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),
1895
2297
  }
1896
-
2298
+
1897
2299
  except Exception as e:
1898
2300
  # В случае ошибки все равно чистим временные файлы
1899
2301
  shutil.rmtree(temp_with_msg, ignore_errors=True)
@@ -1901,34 +2303,34 @@ async def process_admin_event(event: Dict):
1901
2303
  logger.error(f"❌ Критическая ошибка обработки события: {e}")
1902
2304
  raise
1903
2305
 
2306
+
1904
2307
  # =============================================================================
1905
2308
  # ФУНКЦИЯ ДЛЯ ПОДГОТОВКИ ДАННЫХ ДАШБОРДА
1906
2309
  # =============================================================================
1907
2310
 
2311
+
1908
2312
  async def prepare_dashboard_info(
1909
- description_template: str,
1910
- title: str,
1911
- user_id: int
2313
+ description_template: str, title: str, user_id: int
1912
2314
  ) -> Dict[str, Any]:
1913
2315
  """
1914
2316
  Подготавливает данные для дашборда (БЕЗ записи в БД)
1915
-
2317
+
1916
2318
  Возвращаемый dict нужно поместить в поле 'info' результата обработчика.
1917
2319
  bot_utils.py автоматически запишет его в столбец info_dashboard таблицы.
1918
-
2320
+
1919
2321
  Args:
1920
2322
  description_template: Строка с {username}, например "{username} купил подписку"
1921
2323
  title: Заголовок для дашборда
1922
2324
  user_id: Telegram ID
1923
-
2325
+
1924
2326
  Returns:
1925
2327
  Dict с данными для дашборда
1926
-
2328
+
1927
2329
  Example:
1928
2330
  @event_router.event_handler("collect_phone", notify=True)
1929
2331
  async def handle_phone_collection(user_id: int, phone_number: str):
1930
2332
  # ... бизнес-логика ...
1931
-
2333
+
1932
2334
  return {
1933
2335
  "status": "success",
1934
2336
  "phone": phone_number,
@@ -1940,29 +2342,33 @@ async def prepare_dashboard_info(
1940
2342
  }
1941
2343
  """
1942
2344
  supabase_client = get_supabase_client()
1943
-
2345
+
1944
2346
  # Получаем username из sales_users
1945
2347
  username = f"user_{user_id}" # fallback
1946
2348
  if supabase_client:
1947
2349
  try:
1948
- 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
+ )
1949
2355
  if supabase_client.bot_id:
1950
- query = query.eq('bot_id', supabase_client.bot_id)
2356
+ query = query.eq("bot_id", supabase_client.bot_id)
1951
2357
  response = query.execute()
1952
2358
  if response.data:
1953
- username = response.data[0].get('username') or username
2359
+ username = response.data[0].get("username") or username
1954
2360
  except Exception as e:
1955
2361
  logger.warning(f"⚠️ Не удалось получить username для дашборда: {e}")
1956
-
2362
+
1957
2363
  # Форматируем строку
1958
2364
  description = description_template.format(username=username)
1959
-
2365
+
1960
2366
  # Московское время (UTC+3)
1961
2367
  moscow_tz = timezone(timedelta(hours=3))
1962
2368
  moscow_time = datetime.now(moscow_tz)
1963
-
2369
+
1964
2370
  return {
1965
- 'title': title,
1966
- 'description': description,
1967
- 'created_at': moscow_time.isoformat()
2371
+ "title": title,
2372
+ "description": description,
2373
+ "created_at": moscow_time.isoformat(),
1968
2374
  }