smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +474 -332
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1200 -755
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +676 -472
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +948 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.9.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.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
|
|
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(
|
|
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 ==
|
|
88
|
+
|
|
89
|
+
if unit == "h": # часы
|
|
86
90
|
total_seconds += value * 3600
|
|
87
|
-
elif unit ==
|
|
91
|
+
elif unit == "m": # минуты
|
|
88
92
|
total_seconds += value * 60
|
|
89
|
-
elif unit ==
|
|
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(
|
|
125
|
+
if datetime_str.endswith("Z"):
|
|
121
126
|
# Заменяем Z на +00:00 для корректного парсинга
|
|
122
|
-
datetime_str = datetime_str[:-1] +
|
|
127
|
+
datetime_str = datetime_str[:-1] + "+00:00"
|
|
123
128
|
return datetime.fromisoformat(datetime_str)
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
# Пробуем парсить ISO 8601 формат с timezone
|
|
126
|
-
if
|
|
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
|
|
131
|
-
return datetime.fromisoformat(datetime_str +
|
|
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 +
|
|
135
|
-
|
|
139
|
+
return datetime.fromisoformat(datetime_str + "T00:00:00+00:00")
|
|
140
|
+
|
|
136
141
|
except ValueError as e:
|
|
137
|
-
raise ValueError(
|
|
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
|
|
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
|
|
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(
|
|
263
|
-
|
|
273
|
+
appointment_datetime = datetime.strptime(
|
|
274
|
+
f"{date_str} {time_str}", "%Y-%m-%d %H:%M"
|
|
275
|
+
)
|
|
276
|
+
|
|
264
277
|
# Добавляем в результат
|
|
265
|
-
result[
|
|
266
|
-
result[
|
|
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[
|
|
284
|
+
result["is_past"] = True
|
|
272
285
|
else:
|
|
273
|
-
result[
|
|
274
|
-
|
|
286
|
+
result["is_past"] = False
|
|
287
|
+
|
|
275
288
|
except ValueError as e:
|
|
276
289
|
logger.warning(f"Ошибка парсинга даты/времени: {e}")
|
|
277
|
-
result[
|
|
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 {
|
|
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 = {
|
|
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
|
-
|
|
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[
|
|
342
|
-
result[
|
|
343
|
-
|
|
359
|
+
result["errors"].append(f"Отсутствует обязательное поле: {field}")
|
|
360
|
+
result["valid"] = False
|
|
361
|
+
|
|
344
362
|
# Проверяем формат телефона
|
|
345
|
-
if
|
|
346
|
-
phone = appointment_data[
|
|
347
|
-
if not re.match(
|
|
348
|
-
|
|
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
|
|
371
|
+
if "дата" in appointment_data:
|
|
352
372
|
try:
|
|
353
|
-
datetime.strptime(appointment_data[
|
|
373
|
+
datetime.strptime(appointment_data["дата"], "%Y-%m-%d")
|
|
354
374
|
except ValueError:
|
|
355
|
-
result[
|
|
356
|
-
result[
|
|
357
|
-
|
|
375
|
+
result["errors"].append(f"Неверный формат даты: {appointment_data['дата']}")
|
|
376
|
+
result["valid"] = False
|
|
377
|
+
|
|
358
378
|
# Проверяем время
|
|
359
|
-
if
|
|
379
|
+
if "время" in appointment_data:
|
|
360
380
|
try:
|
|
361
|
-
datetime.strptime(appointment_data[
|
|
381
|
+
datetime.strptime(appointment_data["время"], "%H:%M")
|
|
362
382
|
except ValueError:
|
|
363
|
-
result[
|
|
364
|
-
|
|
365
|
-
|
|
383
|
+
result["errors"].append(
|
|
384
|
+
f"Неверный формат времени: {appointment_data['время']}"
|
|
385
|
+
)
|
|
386
|
+
result["valid"] = False
|
|
387
|
+
|
|
366
388
|
# Проверяем, не в прошлом ли запись
|
|
367
|
-
if
|
|
368
|
-
result[
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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(
|
|
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[
|
|
424
|
-
result[
|
|
455
|
+
result["notify"] = notify
|
|
456
|
+
result["send_ai_response"] = send_ai_response
|
|
425
457
|
else:
|
|
426
458
|
# Если результат не словарь, создаем словарь
|
|
427
459
|
result = {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
548
|
+
logger.info(
|
|
549
|
+
f"⏰ Задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)"
|
|
550
|
+
)
|
|
504
551
|
else:
|
|
505
|
-
logger.info(
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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(
|
|
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[
|
|
536
|
-
result[
|
|
586
|
+
result["notify"] = notify
|
|
587
|
+
result["send_ai_response"] = send_ai_response
|
|
537
588
|
else:
|
|
538
589
|
# Если результат не словарь, создаем словарь
|
|
539
590
|
result = {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
674
|
+
logger.info(
|
|
675
|
+
f"🌍 Глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)"
|
|
676
|
+
)
|
|
612
677
|
except ValueError as e:
|
|
613
|
-
logger.error(
|
|
678
|
+
logger.error(
|
|
679
|
+
f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}"
|
|
680
|
+
)
|
|
614
681
|
raise
|
|
615
|
-
|
|
682
|
+
|
|
616
683
|
_global_handlers[handler_type] = {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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(
|
|
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(
|
|
634
|
-
|
|
702
|
+
logger.info(
|
|
703
|
+
f"✅ Глобальный обработчик '{handler_type}' выполнен успешно"
|
|
704
|
+
)
|
|
705
|
+
|
|
635
706
|
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
636
707
|
if isinstance(result, dict):
|
|
637
|
-
result[
|
|
638
|
-
result[
|
|
708
|
+
result["notify"] = notify
|
|
709
|
+
result["send_ai_response"] = send_ai_response
|
|
639
710
|
else:
|
|
640
711
|
# Если результат не словарь, создаем словарь
|
|
641
712
|
result = {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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(
|
|
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[
|
|
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(
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
839
|
+
return await handler_info["handler"](*args, **kwargs)
|
|
840
|
+
|
|
755
841
|
|
|
756
|
-
async def schedule_task_for_later(
|
|
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(
|
|
778
|
-
|
|
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
|
-
|
|
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(
|
|
904
|
+
logger.debug(
|
|
905
|
+
f"🔍 RouterManager найден, доступные задачи: {list(scheduled_tasks.keys())}"
|
|
906
|
+
)
|
|
810
907
|
else:
|
|
811
908
|
scheduled_tasks = _scheduled_tasks
|
|
812
|
-
logger.debug(
|
|
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(
|
|
817
|
-
|
|
818
|
-
|
|
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(
|
|
822
|
-
event_type = task_info.get(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
863
|
-
|
|
977
|
+
raise RuntimeError(
|
|
978
|
+
"Supabase клиент не найден для получения времени события"
|
|
979
|
+
)
|
|
980
|
+
|
|
864
981
|
try:
|
|
865
982
|
# Получаем данные события из БД
|
|
866
|
-
event_data_str =
|
|
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(
|
|
990
|
+
logger.warning(
|
|
991
|
+
f"Событие '{event_type}' не найдено для пользователя {user_id}"
|
|
992
|
+
)
|
|
870
993
|
# Fallback - планируем через default_delay
|
|
871
|
-
result = await schedule_task_for_later_with_db(
|
|
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
|
|
878
|
-
logger.warning(
|
|
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(
|
|
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[
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
921
|
-
|
|
922
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1094
|
+
logger.debug(
|
|
1095
|
+
f"🔍 Поиск глобального обработчика '{handler_type}' через RouterManager"
|
|
1096
|
+
)
|
|
949
1097
|
else:
|
|
950
1098
|
global_handlers = _global_handlers
|
|
951
|
-
logger.debug(
|
|
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(
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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(
|
|
994
|
-
event_type = handler_info.get(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1062
|
-
|
|
1063
|
-
|
|
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(
|
|
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(
|
|
1073
|
-
|
|
1249
|
+
result = await schedule_global_handler_for_later_with_db(
|
|
1250
|
+
handler_type, default_delay, handler_data
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1074
1253
|
return result
|
|
1075
1254
|
|
|
1076
1255
|
|
|
@@ -1078,545 +1257,746 @@ 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,
|
|
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(
|
|
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,
|
|
1093
|
-
except:
|
|
1094
|
-
|
|
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(
|
|
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(
|
|
1302
|
+
already_processed = await check_event_already_processed(
|
|
1303
|
+
event_type, user_id, session_id
|
|
1304
|
+
)
|
|
1124
1305
|
if already_processed:
|
|
1125
|
-
logger.info(
|
|
1126
|
-
|
|
1127
|
-
|
|
1306
|
+
logger.info(
|
|
1307
|
+
f"🔄 Событие '{event_type}' уже обрабатывалось для пользователя {user_id}, пропускаем"
|
|
1308
|
+
)
|
|
1309
|
+
raise ValueError(
|
|
1310
|
+
f"Событие '{event_type}' уже обрабатывалось (once_only=True)"
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
# Получаем bot_id
|
|
1314
|
+
bot_id = supabase_client.bot_id
|
|
1315
|
+
if not bot_id:
|
|
1316
|
+
logger.warning("⚠️ bot_id не указан при создании immediate_event")
|
|
1317
|
+
|
|
1128
1318
|
event_record = {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1319
|
+
"event_type": event_type,
|
|
1320
|
+
"event_category": "user_event",
|
|
1321
|
+
"user_id": user_id,
|
|
1322
|
+
"event_data": event_data,
|
|
1323
|
+
"scheduled_at": None, # Немедленное выполнение
|
|
1324
|
+
"status": "immediate",
|
|
1325
|
+
"session_id": session_id,
|
|
1326
|
+
"bot_id": bot_id, # Всегда добавляем bot_id
|
|
1136
1327
|
}
|
|
1137
|
-
|
|
1138
|
-
# 🆕 Добавляем bot_id если указан
|
|
1139
|
-
if supabase_client.bot_id:
|
|
1140
|
-
event_record['bot_id'] = supabase_client.bot_id
|
|
1141
|
-
|
|
1328
|
+
|
|
1142
1329
|
try:
|
|
1143
|
-
response =
|
|
1144
|
-
|
|
1330
|
+
response = (
|
|
1331
|
+
supabase_client.client.table("scheduled_events")
|
|
1332
|
+
.insert(event_record)
|
|
1333
|
+
.execute()
|
|
1334
|
+
)
|
|
1335
|
+
event_id = response.data[0]["id"]
|
|
1145
1336
|
logger.info(f"💾 Событие сохранено в БД: {event_id}")
|
|
1146
1337
|
return event_id
|
|
1147
1338
|
except Exception as e:
|
|
1148
1339
|
logger.error(f"❌ Ошибка сохранения события в БД: {e}")
|
|
1149
1340
|
raise
|
|
1150
1341
|
|
|
1342
|
+
|
|
1151
1343
|
async def save_scheduled_task(
|
|
1152
1344
|
task_name: str,
|
|
1153
1345
|
user_id: int,
|
|
1154
1346
|
user_data: str,
|
|
1155
1347
|
delay_seconds: int,
|
|
1156
|
-
session_id: str = None
|
|
1348
|
+
session_id: str = None,
|
|
1157
1349
|
) -> str:
|
|
1158
1350
|
"""Сохраняет запланированную задачу"""
|
|
1159
|
-
|
|
1351
|
+
|
|
1160
1352
|
supabase_client = get_supabase_client()
|
|
1161
1353
|
if not supabase_client:
|
|
1162
1354
|
logger.error("❌ Supabase клиент не найден")
|
|
1163
1355
|
raise RuntimeError("Supabase клиент не инициализирован")
|
|
1164
|
-
|
|
1356
|
+
|
|
1165
1357
|
# Проверяем, нужно ли предотвращать дублирование
|
|
1166
1358
|
router_manager = get_router_manager()
|
|
1167
1359
|
if router_manager:
|
|
1168
1360
|
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
1169
1361
|
else:
|
|
1170
1362
|
scheduled_tasks = _scheduled_tasks
|
|
1171
|
-
|
|
1363
|
+
|
|
1172
1364
|
task_info = scheduled_tasks.get(task_name, {})
|
|
1173
|
-
once_only = task_info.get(
|
|
1174
|
-
|
|
1365
|
+
once_only = task_info.get("once_only", True)
|
|
1366
|
+
|
|
1175
1367
|
if once_only:
|
|
1176
1368
|
# Проверяем, была ли уже запланирована аналогичная задача для этого пользователя
|
|
1177
|
-
already_processed = await check_event_already_processed(
|
|
1369
|
+
already_processed = await check_event_already_processed(
|
|
1370
|
+
task_name, user_id, session_id
|
|
1371
|
+
)
|
|
1178
1372
|
if already_processed:
|
|
1179
|
-
logger.info(
|
|
1373
|
+
logger.info(
|
|
1374
|
+
f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем"
|
|
1375
|
+
)
|
|
1180
1376
|
raise ValueError(f"Задача '{task_name}' уже запланирована (once_only=True)")
|
|
1181
|
-
|
|
1377
|
+
|
|
1182
1378
|
scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
|
|
1183
|
-
|
|
1379
|
+
|
|
1380
|
+
# Получаем bot_id
|
|
1381
|
+
bot_id = supabase_client.bot_id
|
|
1382
|
+
if not bot_id:
|
|
1383
|
+
logger.warning("⚠️ bot_id не указан при создании scheduled_task")
|
|
1384
|
+
|
|
1184
1385
|
event_record = {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1386
|
+
"event_type": task_name,
|
|
1387
|
+
"event_category": "scheduled_task",
|
|
1388
|
+
"user_id": user_id,
|
|
1389
|
+
"event_data": user_data,
|
|
1390
|
+
"scheduled_at": scheduled_at.isoformat(),
|
|
1391
|
+
"status": "pending",
|
|
1392
|
+
"session_id": session_id,
|
|
1393
|
+
"bot_id": bot_id, # Всегда добавляем bot_id
|
|
1192
1394
|
}
|
|
1193
|
-
|
|
1194
|
-
# 🆕 Добавляем bot_id если указан
|
|
1195
|
-
if supabase_client.bot_id:
|
|
1196
|
-
event_record['bot_id'] = supabase_client.bot_id
|
|
1197
|
-
|
|
1395
|
+
|
|
1198
1396
|
try:
|
|
1199
|
-
response =
|
|
1200
|
-
|
|
1201
|
-
|
|
1397
|
+
response = (
|
|
1398
|
+
supabase_client.client.table("scheduled_events")
|
|
1399
|
+
.insert(event_record)
|
|
1400
|
+
.execute()
|
|
1401
|
+
)
|
|
1402
|
+
event_id = response.data[0]["id"]
|
|
1403
|
+
logger.info(
|
|
1404
|
+
f"⏰ Запланированная задача сохранена в БД: {event_id} (через {delay_seconds}с)"
|
|
1405
|
+
)
|
|
1202
1406
|
return event_id
|
|
1203
1407
|
except Exception as e:
|
|
1204
1408
|
logger.error(f"❌ Ошибка сохранения запланированной задачи в БД: {e}")
|
|
1205
1409
|
raise
|
|
1206
1410
|
|
|
1411
|
+
|
|
1207
1412
|
async def save_global_event(
|
|
1208
|
-
handler_type: str,
|
|
1209
|
-
handler_data: str,
|
|
1210
|
-
delay_seconds: int = 0
|
|
1413
|
+
handler_type: str, handler_data: str, delay_seconds: int = 0
|
|
1211
1414
|
) -> str:
|
|
1212
1415
|
"""Сохраняет глобальное событие"""
|
|
1213
|
-
|
|
1416
|
+
|
|
1214
1417
|
supabase_client = get_supabase_client()
|
|
1215
1418
|
if not supabase_client:
|
|
1216
1419
|
logger.error("❌ Supabase клиент не найден")
|
|
1217
1420
|
raise RuntimeError("Supabase клиент не инициализирован")
|
|
1218
|
-
|
|
1421
|
+
|
|
1219
1422
|
# Проверяем, нужно ли предотвращать дублирование
|
|
1220
1423
|
router_manager = get_router_manager()
|
|
1221
1424
|
if router_manager:
|
|
1222
1425
|
global_handlers = router_manager.get_global_handlers()
|
|
1223
1426
|
else:
|
|
1224
1427
|
global_handlers = _global_handlers
|
|
1225
|
-
|
|
1428
|
+
|
|
1226
1429
|
handler_info = global_handlers.get(handler_type, {})
|
|
1227
|
-
once_only = handler_info.get(
|
|
1228
|
-
|
|
1430
|
+
once_only = handler_info.get("once_only", True)
|
|
1431
|
+
|
|
1229
1432
|
if once_only:
|
|
1230
1433
|
# Проверяем, было ли уже запланировано аналогичное глобальное событие
|
|
1231
|
-
already_processed = await check_event_already_processed(
|
|
1434
|
+
already_processed = await check_event_already_processed(
|
|
1435
|
+
handler_type, user_id=None
|
|
1436
|
+
)
|
|
1232
1437
|
if already_processed:
|
|
1233
|
-
logger.info(
|
|
1234
|
-
|
|
1235
|
-
|
|
1438
|
+
logger.info(
|
|
1439
|
+
f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем"
|
|
1440
|
+
)
|
|
1441
|
+
raise ValueError(
|
|
1442
|
+
f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)"
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1236
1445
|
scheduled_at = None
|
|
1237
|
-
status =
|
|
1238
|
-
|
|
1446
|
+
status = "immediate"
|
|
1447
|
+
|
|
1239
1448
|
if delay_seconds > 0:
|
|
1240
1449
|
scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
|
|
1241
|
-
status =
|
|
1242
|
-
|
|
1450
|
+
status = "pending"
|
|
1451
|
+
|
|
1452
|
+
# Получаем bot_id
|
|
1453
|
+
bot_id = supabase_client.bot_id
|
|
1454
|
+
if not bot_id:
|
|
1455
|
+
logger.warning("⚠️ bot_id не указан при создании global_event")
|
|
1456
|
+
|
|
1243
1457
|
event_record = {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1458
|
+
"event_type": handler_type,
|
|
1459
|
+
"event_category": "global_handler",
|
|
1460
|
+
"user_id": None, # Глобальное событие
|
|
1461
|
+
"event_data": handler_data,
|
|
1462
|
+
"scheduled_at": scheduled_at.isoformat() if scheduled_at else None,
|
|
1463
|
+
"status": status,
|
|
1464
|
+
"bot_id": bot_id, # Всегда добавляем bot_id (глобальные события тоже привязаны к боту)
|
|
1250
1465
|
}
|
|
1251
|
-
|
|
1252
|
-
# 🆕 Добавляем bot_id если указан (глобальные события тоже привязаны к боту)
|
|
1253
|
-
if supabase_client.bot_id:
|
|
1254
|
-
event_record['bot_id'] = supabase_client.bot_id
|
|
1255
|
-
|
|
1466
|
+
|
|
1256
1467
|
try:
|
|
1257
|
-
response =
|
|
1258
|
-
|
|
1468
|
+
response = (
|
|
1469
|
+
supabase_client.client.table("scheduled_events")
|
|
1470
|
+
.insert(event_record)
|
|
1471
|
+
.execute()
|
|
1472
|
+
)
|
|
1473
|
+
event_id = response.data[0]["id"]
|
|
1259
1474
|
logger.info(f"🌍 Глобальное событие сохранено в БД: {event_id}")
|
|
1260
1475
|
return event_id
|
|
1261
1476
|
except Exception as e:
|
|
1262
1477
|
logger.error(f"❌ Ошибка сохранения глобального события в БД: {e}")
|
|
1263
1478
|
raise
|
|
1264
1479
|
|
|
1480
|
+
|
|
1265
1481
|
async def update_event_result(
|
|
1266
|
-
event_id: str,
|
|
1267
|
-
status: str,
|
|
1268
|
-
result_data: Any = None,
|
|
1269
|
-
error_message: str = None
|
|
1482
|
+
event_id: str, status: str, result_data: Any = None, error_message: str = None
|
|
1270
1483
|
):
|
|
1271
1484
|
"""Обновляет результат выполнения события"""
|
|
1272
|
-
|
|
1485
|
+
|
|
1273
1486
|
supabase_client = get_supabase_client()
|
|
1274
1487
|
if not supabase_client:
|
|
1275
1488
|
logger.error("❌ Supabase клиент не найден")
|
|
1276
1489
|
return
|
|
1277
|
-
|
|
1490
|
+
|
|
1278
1491
|
update_data = {
|
|
1279
|
-
|
|
1280
|
-
|
|
1492
|
+
"status": status,
|
|
1493
|
+
"executed_at": datetime.now(timezone.utc).isoformat(),
|
|
1281
1494
|
}
|
|
1282
|
-
|
|
1495
|
+
|
|
1283
1496
|
if result_data:
|
|
1284
1497
|
import json
|
|
1285
|
-
|
|
1286
|
-
|
|
1498
|
+
|
|
1499
|
+
update_data["result_data"] = json.dumps(result_data, ensure_ascii=False)
|
|
1500
|
+
|
|
1287
1501
|
# Проверяем наличие поля 'info' для дашборда
|
|
1288
|
-
if isinstance(result_data, dict) and
|
|
1289
|
-
update_data[
|
|
1502
|
+
if isinstance(result_data, dict) and "info" in result_data:
|
|
1503
|
+
update_data["info_dashboard"] = json.dumps(
|
|
1504
|
+
result_data["info"], ensure_ascii=False
|
|
1505
|
+
)
|
|
1290
1506
|
logger.info(f"📊 Дашборд данные добавлены в событие {event_id}")
|
|
1291
|
-
|
|
1507
|
+
|
|
1292
1508
|
if error_message:
|
|
1293
|
-
update_data[
|
|
1509
|
+
update_data["last_error"] = error_message
|
|
1294
1510
|
# Получаем текущее количество попыток
|
|
1295
1511
|
try:
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1512
|
+
query = (
|
|
1513
|
+
supabase_client.client.table("scheduled_events")
|
|
1514
|
+
.select("retry_count")
|
|
1515
|
+
.eq("id", event_id)
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
# Добавляем фильтр по bot_id если указан
|
|
1519
|
+
if supabase_client.bot_id:
|
|
1520
|
+
query = query.eq("bot_id", supabase_client.bot_id)
|
|
1521
|
+
|
|
1522
|
+
current_retry = query.execute().data[0]["retry_count"]
|
|
1523
|
+
update_data["retry_count"] = current_retry + 1
|
|
1524
|
+
except Exception:
|
|
1525
|
+
logger.debug("Не удалось получить текущее количество попыток, устанавливаем 1")
|
|
1526
|
+
update_data["retry_count"] = 1
|
|
1527
|
+
|
|
1301
1528
|
try:
|
|
1302
|
-
supabase_client.client.table(
|
|
1529
|
+
query = supabase_client.client.table("scheduled_events").update(update_data).eq(
|
|
1530
|
+
"id", event_id
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
# Добавляем фильтр по bot_id если указан
|
|
1534
|
+
if supabase_client.bot_id:
|
|
1535
|
+
query = query.eq("bot_id", supabase_client.bot_id)
|
|
1536
|
+
|
|
1537
|
+
query.execute()
|
|
1303
1538
|
logger.info(f"📝 Результат события {event_id} обновлен: {status}")
|
|
1304
1539
|
except Exception as e:
|
|
1305
1540
|
logger.error(f"❌ Ошибка обновления результата события {event_id}: {e}")
|
|
1306
1541
|
|
|
1542
|
+
|
|
1307
1543
|
async def get_pending_events(limit: int = 50) -> list:
|
|
1308
1544
|
"""Получает события готовые к выполнению СЕЙЧАС"""
|
|
1309
|
-
|
|
1545
|
+
|
|
1310
1546
|
supabase_client = get_supabase_client()
|
|
1311
1547
|
if not supabase_client:
|
|
1312
1548
|
logger.error("❌ Supabase клиент не найден")
|
|
1313
1549
|
return []
|
|
1314
|
-
|
|
1550
|
+
|
|
1315
1551
|
try:
|
|
1316
1552
|
now = datetime.now(timezone.utc).isoformat()
|
|
1317
|
-
|
|
1318
|
-
query =
|
|
1319
|
-
.
|
|
1320
|
-
.
|
|
1321
|
-
.
|
|
1322
|
-
.
|
|
1553
|
+
|
|
1554
|
+
query = (
|
|
1555
|
+
supabase_client.client.table("scheduled_events")
|
|
1556
|
+
.select("*")
|
|
1557
|
+
.in_("status", ["pending", "immediate"])
|
|
1558
|
+
.or_(f"scheduled_at.is.null,scheduled_at.lte.{now}")
|
|
1559
|
+
.order("created_at")
|
|
1323
1560
|
.limit(limit)
|
|
1324
|
-
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1325
1563
|
# 🆕 Фильтруем по bot_id если указан
|
|
1326
1564
|
if supabase_client.bot_id:
|
|
1327
|
-
query = query.eq(
|
|
1328
|
-
|
|
1565
|
+
query = query.eq("bot_id", supabase_client.bot_id)
|
|
1566
|
+
|
|
1329
1567
|
response = query.execute()
|
|
1330
|
-
|
|
1568
|
+
|
|
1331
1569
|
return response.data
|
|
1332
1570
|
except Exception as e:
|
|
1333
1571
|
logger.error(f"❌ Ошибка получения событий из БД: {e}")
|
|
1334
1572
|
return []
|
|
1335
1573
|
|
|
1574
|
+
|
|
1336
1575
|
async def get_pending_events_in_next_minute(limit: int = 100) -> list:
|
|
1337
1576
|
"""Получает события готовые к выполнению в течение следующей минуты"""
|
|
1338
|
-
|
|
1577
|
+
|
|
1339
1578
|
supabase_client = get_supabase_client()
|
|
1340
1579
|
if not supabase_client:
|
|
1341
1580
|
logger.error("❌ Supabase клиент не найден")
|
|
1342
1581
|
return []
|
|
1343
|
-
|
|
1582
|
+
|
|
1344
1583
|
try:
|
|
1345
1584
|
now = datetime.now(timezone.utc)
|
|
1346
1585
|
next_minute = now + timedelta(seconds=60)
|
|
1347
|
-
|
|
1348
|
-
query =
|
|
1349
|
-
.
|
|
1350
|
-
.
|
|
1351
|
-
.
|
|
1352
|
-
.
|
|
1586
|
+
|
|
1587
|
+
query = (
|
|
1588
|
+
supabase_client.client.table("scheduled_events")
|
|
1589
|
+
.select("*")
|
|
1590
|
+
.in_("status", ["pending", "immediate"])
|
|
1591
|
+
.or_(f"scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}")
|
|
1592
|
+
.order("created_at")
|
|
1353
1593
|
.limit(limit)
|
|
1354
|
-
|
|
1594
|
+
)
|
|
1595
|
+
|
|
1355
1596
|
# 🆕 Фильтруем по bot_id если указан
|
|
1356
1597
|
if supabase_client.bot_id:
|
|
1357
|
-
query = query.eq(
|
|
1358
|
-
|
|
1598
|
+
query = query.eq("bot_id", supabase_client.bot_id)
|
|
1599
|
+
|
|
1359
1600
|
response = query.execute()
|
|
1360
|
-
|
|
1601
|
+
|
|
1361
1602
|
return response.data
|
|
1362
1603
|
except Exception as e:
|
|
1363
1604
|
logger.error(f"❌ Ошибка получения событий из БД: {e}")
|
|
1364
1605
|
return []
|
|
1365
1606
|
|
|
1607
|
+
|
|
1366
1608
|
async def background_event_processor():
|
|
1367
1609
|
"""Фоновый процессор для ВСЕХ типов событий включая админские (проверяет БД каждую минуту)"""
|
|
1368
|
-
|
|
1369
|
-
logger.info(
|
|
1370
|
-
|
|
1610
|
+
|
|
1611
|
+
logger.info(
|
|
1612
|
+
"🔄 Запуск фонового процессора событий (user_event, scheduled_task, global_handler, admin_event)"
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1371
1615
|
while True:
|
|
1372
1616
|
try:
|
|
1373
1617
|
# Получаем события готовые к выполнению в следующую минуту
|
|
1374
1618
|
pending_events = await get_pending_events_in_next_minute(limit=100)
|
|
1375
|
-
|
|
1619
|
+
|
|
1376
1620
|
if pending_events:
|
|
1377
1621
|
logger.info(f"📋 Найдено {len(pending_events)} событий для обработки")
|
|
1378
|
-
|
|
1622
|
+
|
|
1379
1623
|
for event in pending_events:
|
|
1380
1624
|
try:
|
|
1381
|
-
event_type = event[
|
|
1382
|
-
event_category = event[
|
|
1383
|
-
user_id = event.get(
|
|
1384
|
-
session_id = event.get(
|
|
1385
|
-
|
|
1625
|
+
event_type = event["event_type"]
|
|
1626
|
+
event_category = event["event_category"]
|
|
1627
|
+
user_id = event.get("user_id")
|
|
1628
|
+
session_id = event.get("session_id")
|
|
1629
|
+
|
|
1386
1630
|
# ========== ОБРАБОТКА АДМИНСКИХ СОБЫТИЙ ==========
|
|
1387
|
-
if event_category ==
|
|
1631
|
+
if event_category == "admin_event":
|
|
1632
|
+
# Проверяем bot_id
|
|
1633
|
+
if not event.get("bot_id"):
|
|
1634
|
+
logger.warning(f"⚠️ Админское событие {event['id']} не имеет bot_id")
|
|
1635
|
+
|
|
1388
1636
|
try:
|
|
1637
|
+
logger.info(f"🔄 Начало обработки админского события {event['id']}")
|
|
1638
|
+
logger.info(f"📝 Данные события: {event}")
|
|
1639
|
+
|
|
1389
1640
|
# Обрабатываем и получаем результат
|
|
1390
1641
|
result = await process_admin_event(event)
|
|
1391
|
-
|
|
1642
|
+
|
|
1392
1643
|
# Сохраняем результат в result_data
|
|
1393
1644
|
import json
|
|
1645
|
+
|
|
1394
1646
|
supabase_client = get_supabase_client()
|
|
1395
|
-
supabase_client
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1647
|
+
if not supabase_client:
|
|
1648
|
+
raise RuntimeError("Не найден supabase_client")
|
|
1649
|
+
|
|
1650
|
+
# Готовим данные для обновления
|
|
1651
|
+
update_data = {
|
|
1652
|
+
"status": "completed",
|
|
1653
|
+
"executed_at": datetime.now(timezone.utc).isoformat(),
|
|
1654
|
+
"result_data": json.dumps(result, ensure_ascii=False) if result else None,
|
|
1655
|
+
}
|
|
1400
1656
|
|
|
1401
|
-
|
|
1657
|
+
# Если у события нет bot_id, но он есть в клиенте - добавляем
|
|
1658
|
+
if not event.get("bot_id") and supabase_client.bot_id:
|
|
1659
|
+
update_data["bot_id"] = supabase_client.bot_id
|
|
1660
|
+
logger.info(f"📝 Добавлен bot_id: {supabase_client.bot_id}")
|
|
1661
|
+
|
|
1662
|
+
# Строим запрос
|
|
1663
|
+
query = (
|
|
1664
|
+
supabase_client.client.table("scheduled_events")
|
|
1665
|
+
.update(update_data)
|
|
1666
|
+
.eq("id", event["id"])
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
# Добавляем фильтр по bot_id если он был в событии
|
|
1670
|
+
if event.get("bot_id"):
|
|
1671
|
+
query = query.eq("bot_id", event["bot_id"])
|
|
1672
|
+
|
|
1673
|
+
# Выполняем обновление
|
|
1674
|
+
query.execute()
|
|
1675
|
+
|
|
1676
|
+
logger.info(
|
|
1677
|
+
f"✅ Админское событие {event['id']} выполнено и обновлено в БД"
|
|
1678
|
+
)
|
|
1402
1679
|
continue
|
|
1403
|
-
|
|
1680
|
+
|
|
1404
1681
|
except Exception as e:
|
|
1405
|
-
logger.error(
|
|
1682
|
+
logger.error(
|
|
1683
|
+
f"❌ Ошибка обработки админского события {event['id']}: {e}"
|
|
1684
|
+
)
|
|
1685
|
+
logger.exception("Стек ошибки:")
|
|
1686
|
+
|
|
1687
|
+
try:
|
|
1688
|
+
# Обновляем статус на failed
|
|
1689
|
+
supabase_client = get_supabase_client()
|
|
1690
|
+
if not supabase_client:
|
|
1691
|
+
raise RuntimeError("Не найден supabase_client")
|
|
1692
|
+
|
|
1693
|
+
# Готовим данные для обновления
|
|
1694
|
+
update_data = {
|
|
1695
|
+
"status": "failed",
|
|
1696
|
+
"last_error": str(e),
|
|
1697
|
+
"executed_at": datetime.now(timezone.utc).isoformat(),
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
# Если у события нет bot_id, но он есть в клиенте - добавляем
|
|
1701
|
+
if not event.get("bot_id") and supabase_client.bot_id:
|
|
1702
|
+
update_data["bot_id"] = supabase_client.bot_id
|
|
1703
|
+
logger.info(f"📝 Добавлен bot_id: {supabase_client.bot_id}")
|
|
1704
|
+
|
|
1705
|
+
# Строим запрос
|
|
1706
|
+
query = (
|
|
1707
|
+
supabase_client.client.table("scheduled_events")
|
|
1708
|
+
.update(update_data)
|
|
1709
|
+
.eq("id", event["id"])
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
# Добавляем фильтр по bot_id если он был в событии
|
|
1713
|
+
if event.get("bot_id"):
|
|
1714
|
+
query = query.eq("bot_id", event["bot_id"])
|
|
1715
|
+
|
|
1716
|
+
# Выполняем обновление
|
|
1717
|
+
query.execute()
|
|
1718
|
+
logger.info(f"✅ Статус события {event['id']} обновлен на failed")
|
|
1719
|
+
|
|
1720
|
+
except Exception as update_error:
|
|
1721
|
+
logger.error(f"❌ Ошибка обновления статуса события: {update_error}")
|
|
1722
|
+
logger.exception("Стек ошибки обновления:")
|
|
1406
1723
|
|
|
1407
|
-
# Обновляем статус на failed
|
|
1408
|
-
supabase_client = get_supabase_client()
|
|
1409
|
-
supabase_client.client.table('scheduled_events').update({
|
|
1410
|
-
'status': 'failed',
|
|
1411
|
-
'last_error': str(e),
|
|
1412
|
-
'executed_at': datetime.now(timezone.utc).isoformat()
|
|
1413
|
-
}).eq('id', event['id']).execute()
|
|
1414
1724
|
continue
|
|
1415
|
-
|
|
1725
|
+
|
|
1416
1726
|
# ========== ОБРАБОТКА USER СОБЫТИЙ ==========
|
|
1417
|
-
if event_category ==
|
|
1727
|
+
if event_category == "user_event":
|
|
1418
1728
|
router_manager = get_router_manager()
|
|
1419
1729
|
if router_manager:
|
|
1420
1730
|
event_handlers = router_manager.get_event_handlers()
|
|
1421
1731
|
else:
|
|
1422
1732
|
event_handlers = _event_handlers
|
|
1423
|
-
|
|
1733
|
+
|
|
1424
1734
|
event_handler_info = event_handlers.get(event_type, {})
|
|
1425
|
-
once_only = event_handler_info.get(
|
|
1426
|
-
|
|
1735
|
+
once_only = event_handler_info.get("once_only", True)
|
|
1736
|
+
|
|
1427
1737
|
if once_only:
|
|
1428
1738
|
# Проверяем, было ли уже выполнено это событие для данного пользователя
|
|
1429
1739
|
supabase_client = get_supabase_client()
|
|
1430
|
-
check_query =
|
|
1431
|
-
.
|
|
1432
|
-
.
|
|
1433
|
-
.eq(
|
|
1434
|
-
.eq(
|
|
1435
|
-
.
|
|
1436
|
-
|
|
1740
|
+
check_query = (
|
|
1741
|
+
supabase_client.client.table("scheduled_events")
|
|
1742
|
+
.select("id")
|
|
1743
|
+
.eq("event_type", event_type)
|
|
1744
|
+
.eq("user_id", user_id)
|
|
1745
|
+
.eq("status", "completed")
|
|
1746
|
+
.neq("id", event["id"])
|
|
1747
|
+
) # Исключаем текущее событие
|
|
1748
|
+
|
|
1437
1749
|
if session_id:
|
|
1438
|
-
check_query = check_query.eq(
|
|
1439
|
-
|
|
1750
|
+
check_query = check_query.eq(
|
|
1751
|
+
"session_id", session_id
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1440
1754
|
existing = check_query.execute()
|
|
1441
|
-
|
|
1755
|
+
|
|
1442
1756
|
if existing.data:
|
|
1443
|
-
await update_event_result(
|
|
1444
|
-
|
|
1757
|
+
await update_event_result(
|
|
1758
|
+
event["id"],
|
|
1759
|
+
"cancelled",
|
|
1760
|
+
{"reason": "already_executed_once_only"},
|
|
1761
|
+
)
|
|
1762
|
+
logger.info(
|
|
1763
|
+
f"⛔ Событие {event['id']} ({event_type}) пропущено: уже выполнялось для пользователя {user_id} (once_only=True)"
|
|
1764
|
+
)
|
|
1445
1765
|
continue
|
|
1446
|
-
|
|
1766
|
+
|
|
1447
1767
|
# Для scheduled_task - проверяем smart_check и once_only
|
|
1448
|
-
if event_category ==
|
|
1768
|
+
if event_category == "scheduled_task":
|
|
1449
1769
|
router_manager = get_router_manager()
|
|
1450
|
-
scheduled_tasks =
|
|
1770
|
+
scheduled_tasks = (
|
|
1771
|
+
router_manager.get_scheduled_tasks()
|
|
1772
|
+
if router_manager
|
|
1773
|
+
else _scheduled_tasks
|
|
1774
|
+
)
|
|
1451
1775
|
task_info = scheduled_tasks.get(event_type, {})
|
|
1452
|
-
use_smart_check = task_info.get(
|
|
1453
|
-
once_only = task_info.get(
|
|
1454
|
-
|
|
1776
|
+
use_smart_check = task_info.get("smart_check", True)
|
|
1777
|
+
once_only = task_info.get("once_only", True)
|
|
1778
|
+
|
|
1455
1779
|
# Проверяем once_only для задач
|
|
1456
1780
|
if once_only:
|
|
1457
1781
|
supabase_client = get_supabase_client()
|
|
1458
|
-
check_query =
|
|
1459
|
-
.
|
|
1460
|
-
.
|
|
1461
|
-
.eq(
|
|
1462
|
-
.eq(
|
|
1463
|
-
.
|
|
1464
|
-
|
|
1782
|
+
check_query = (
|
|
1783
|
+
supabase_client.client.table("scheduled_events")
|
|
1784
|
+
.select("id")
|
|
1785
|
+
.eq("event_type", event_type)
|
|
1786
|
+
.eq("user_id", user_id)
|
|
1787
|
+
.eq("status", "completed")
|
|
1788
|
+
.neq("id", event["id"])
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1465
1791
|
if session_id:
|
|
1466
|
-
check_query = check_query.eq(
|
|
1467
|
-
|
|
1792
|
+
check_query = check_query.eq(
|
|
1793
|
+
"session_id", session_id
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1468
1796
|
existing = check_query.execute()
|
|
1469
|
-
|
|
1797
|
+
|
|
1470
1798
|
if existing.data:
|
|
1471
|
-
await update_event_result(
|
|
1472
|
-
|
|
1799
|
+
await update_event_result(
|
|
1800
|
+
event["id"],
|
|
1801
|
+
"cancelled",
|
|
1802
|
+
{"reason": "already_executed_once_only"},
|
|
1803
|
+
)
|
|
1804
|
+
logger.info(
|
|
1805
|
+
f"⛔ Задача {event['id']} ({event_type}) пропущена: уже выполнялась для пользователя {user_id} (once_only=True)"
|
|
1806
|
+
)
|
|
1473
1807
|
continue
|
|
1474
|
-
|
|
1808
|
+
|
|
1475
1809
|
if use_smart_check:
|
|
1476
1810
|
# Умная проверка
|
|
1477
1811
|
check_result = await smart_execute_check(
|
|
1478
|
-
event[
|
|
1479
|
-
user_id,
|
|
1812
|
+
event["id"],
|
|
1813
|
+
user_id,
|
|
1480
1814
|
session_id,
|
|
1481
1815
|
event_type,
|
|
1482
|
-
event[
|
|
1816
|
+
event["event_data"],
|
|
1483
1817
|
)
|
|
1484
|
-
|
|
1485
|
-
if check_result[
|
|
1486
|
-
await update_event_result(
|
|
1487
|
-
|
|
1818
|
+
|
|
1819
|
+
if check_result["action"] == "cancel":
|
|
1820
|
+
await update_event_result(
|
|
1821
|
+
event["id"],
|
|
1822
|
+
"cancelled",
|
|
1823
|
+
{"reason": check_result["reason"]},
|
|
1824
|
+
)
|
|
1825
|
+
logger.info(
|
|
1826
|
+
f"⛔ Задача {event['id']} отменена: {check_result['reason']}"
|
|
1827
|
+
)
|
|
1488
1828
|
continue
|
|
1489
|
-
elif check_result[
|
|
1829
|
+
elif check_result["action"] == "reschedule":
|
|
1490
1830
|
# Обновляем scheduled_at в БД
|
|
1491
|
-
new_scheduled_at = datetime.now(
|
|
1831
|
+
new_scheduled_at = datetime.now(
|
|
1832
|
+
timezone.utc
|
|
1833
|
+
) + timedelta(seconds=check_result["new_delay"])
|
|
1492
1834
|
supabase_client = get_supabase_client()
|
|
1493
|
-
supabase_client.client.table(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1835
|
+
supabase_client.client.table(
|
|
1836
|
+
"scheduled_events"
|
|
1837
|
+
).update(
|
|
1838
|
+
{
|
|
1839
|
+
"scheduled_at": new_scheduled_at.isoformat(),
|
|
1840
|
+
"status": "pending",
|
|
1841
|
+
}
|
|
1842
|
+
).eq(
|
|
1843
|
+
"id", event["id"]
|
|
1844
|
+
).execute()
|
|
1845
|
+
logger.info(
|
|
1846
|
+
f"🔄 Задача {event['id']} перенесена на {check_result['new_delay']}с"
|
|
1847
|
+
)
|
|
1498
1848
|
continue
|
|
1499
|
-
|
|
1849
|
+
|
|
1500
1850
|
# Выполняем событие
|
|
1501
1851
|
result = await process_scheduled_event(event)
|
|
1502
|
-
|
|
1852
|
+
|
|
1503
1853
|
# Проверяем наличие поля 'info' для дашборда
|
|
1504
1854
|
result_data = {"processed": True}
|
|
1505
1855
|
if isinstance(result, dict):
|
|
1506
1856
|
result_data.update(result)
|
|
1507
|
-
if
|
|
1508
|
-
logger.info(
|
|
1509
|
-
|
|
1510
|
-
|
|
1857
|
+
if "info" in result:
|
|
1858
|
+
logger.info(
|
|
1859
|
+
f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}"
|
|
1860
|
+
)
|
|
1861
|
+
|
|
1862
|
+
await update_event_result(event["id"], "completed", result_data)
|
|
1511
1863
|
logger.info(f"✅ Событие {event['id']} выполнено")
|
|
1512
|
-
|
|
1864
|
+
|
|
1513
1865
|
except Exception as e:
|
|
1514
1866
|
logger.error(f"❌ Ошибка обработки события {event['id']}: {e}")
|
|
1515
|
-
await update_event_result(event[
|
|
1516
|
-
|
|
1867
|
+
await update_event_result(event["id"], "failed", None, str(e))
|
|
1868
|
+
|
|
1517
1869
|
await asyncio.sleep(60) # Проверяем каждую минуту
|
|
1518
|
-
|
|
1870
|
+
|
|
1519
1871
|
except Exception as e:
|
|
1520
1872
|
logger.error(f"❌ Ошибка в фоновом процессоре: {e}")
|
|
1521
1873
|
await asyncio.sleep(60)
|
|
1522
1874
|
|
|
1875
|
+
|
|
1523
1876
|
async def process_scheduled_event(event: Dict):
|
|
1524
1877
|
"""Обрабатывает одно событие из БД и возвращает результат"""
|
|
1525
|
-
|
|
1526
|
-
event_type = event[
|
|
1527
|
-
event_category = event[
|
|
1528
|
-
event_data = event[
|
|
1529
|
-
user_id = event.get(
|
|
1530
|
-
|
|
1878
|
+
|
|
1879
|
+
event_type = event["event_type"]
|
|
1880
|
+
event_category = event["event_category"]
|
|
1881
|
+
event_data = event["event_data"]
|
|
1882
|
+
user_id = event.get("user_id")
|
|
1883
|
+
|
|
1531
1884
|
logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
|
|
1532
|
-
|
|
1885
|
+
|
|
1533
1886
|
result = None
|
|
1534
|
-
if event_category ==
|
|
1887
|
+
if event_category == "scheduled_task":
|
|
1535
1888
|
result = await execute_scheduled_task(event_type, user_id, event_data)
|
|
1536
|
-
elif event_category ==
|
|
1889
|
+
elif event_category == "global_handler":
|
|
1537
1890
|
result = await execute_global_handler(event_type, event_data)
|
|
1538
|
-
elif event_category ==
|
|
1891
|
+
elif event_category == "user_event":
|
|
1539
1892
|
result = await execute_event_handler(event_type, user_id, event_data)
|
|
1540
1893
|
else:
|
|
1541
1894
|
logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
|
|
1542
|
-
|
|
1895
|
+
|
|
1543
1896
|
return result
|
|
1544
1897
|
|
|
1898
|
+
|
|
1545
1899
|
# =============================================================================
|
|
1546
1900
|
# ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
|
|
1547
1901
|
# =============================================================================
|
|
1548
1902
|
|
|
1549
|
-
|
|
1903
|
+
|
|
1904
|
+
async def schedule_task_for_later_with_db(
|
|
1905
|
+
task_name: str,
|
|
1906
|
+
user_id: int,
|
|
1907
|
+
user_data: str,
|
|
1908
|
+
delay_seconds: int,
|
|
1909
|
+
session_id: str = None,
|
|
1910
|
+
):
|
|
1550
1911
|
"""Планирует выполнение задачи через указанное время с сохранением в БД (без asyncio.sleep)"""
|
|
1551
|
-
|
|
1912
|
+
|
|
1552
1913
|
# Проверяем через RouterManager или fallback к старым декораторам
|
|
1553
1914
|
router_manager = get_router_manager()
|
|
1554
1915
|
if router_manager:
|
|
1555
1916
|
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
1556
1917
|
else:
|
|
1557
1918
|
scheduled_tasks = _scheduled_tasks
|
|
1558
|
-
|
|
1919
|
+
|
|
1559
1920
|
if task_name not in scheduled_tasks:
|
|
1560
1921
|
import inspect
|
|
1922
|
+
|
|
1561
1923
|
frame = inspect.currentframe()
|
|
1562
1924
|
line_no = frame.f_lineno if frame else "unknown"
|
|
1563
1925
|
available_tasks = list(scheduled_tasks.keys())
|
|
1564
|
-
logger.error(
|
|
1926
|
+
logger.error(
|
|
1927
|
+
f"❌ [decorators.py:{line_no}] Задача '{task_name}' не найдена. Доступные: {available_tasks}"
|
|
1928
|
+
)
|
|
1565
1929
|
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
1566
|
-
|
|
1930
|
+
|
|
1567
1931
|
human_time = format_seconds_to_human(delay_seconds)
|
|
1568
|
-
logger.info(
|
|
1569
|
-
|
|
1932
|
+
logger.info(
|
|
1933
|
+
f"⏰ Планируем задачу '{task_name}' через {human_time} ({delay_seconds}с) для user_id={user_id}"
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1570
1936
|
# Просто сохраняем в БД - фоновый процессор сам выполнит задачу
|
|
1571
|
-
event_id = await save_scheduled_task(
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1937
|
+
event_id = await save_scheduled_task(
|
|
1938
|
+
task_name, user_id, user_data, delay_seconds, session_id
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
logger.info(
|
|
1942
|
+
f"💾 Задача '{task_name}' сохранена в БД с ID {event_id}, будет обработана фоновым процессором"
|
|
1943
|
+
)
|
|
1944
|
+
|
|
1575
1945
|
return {
|
|
1576
1946
|
"status": "scheduled",
|
|
1577
1947
|
"task_name": task_name,
|
|
1578
1948
|
"delay_seconds": delay_seconds,
|
|
1579
1949
|
"event_id": event_id,
|
|
1580
|
-
"scheduled_at": datetime.now(timezone.utc).isoformat()
|
|
1950
|
+
"scheduled_at": datetime.now(timezone.utc).isoformat(),
|
|
1581
1951
|
}
|
|
1582
1952
|
|
|
1583
|
-
|
|
1953
|
+
|
|
1954
|
+
async def schedule_global_handler_for_later_with_db(
|
|
1955
|
+
handler_type: str, delay_seconds: int, handler_data: str
|
|
1956
|
+
):
|
|
1584
1957
|
"""Планирует выполнение глобального обработчика через указанное время с сохранением в БД (без asyncio.sleep)"""
|
|
1585
|
-
|
|
1958
|
+
|
|
1586
1959
|
# Проверяем обработчик через RouterManager или fallback к старым декораторам
|
|
1587
1960
|
router_manager = get_router_manager()
|
|
1588
1961
|
if router_manager:
|
|
1589
1962
|
global_handlers = router_manager.get_global_handlers()
|
|
1590
1963
|
else:
|
|
1591
1964
|
global_handlers = _global_handlers
|
|
1592
|
-
|
|
1965
|
+
|
|
1593
1966
|
if handler_type not in global_handlers:
|
|
1594
1967
|
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
1595
|
-
|
|
1596
|
-
logger.info(
|
|
1597
|
-
|
|
1968
|
+
|
|
1969
|
+
logger.info(
|
|
1970
|
+
f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд"
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1598
1973
|
# Просто сохраняем в БД - фоновый процессор сам выполнит обработчик
|
|
1599
1974
|
event_id = await save_global_event(handler_type, handler_data, delay_seconds)
|
|
1600
|
-
|
|
1601
|
-
logger.info(
|
|
1602
|
-
|
|
1975
|
+
|
|
1976
|
+
logger.info(
|
|
1977
|
+
f"💾 Глобальный обработчик '{handler_type}' сохранен в БД с ID {event_id}, будет обработан фоновым процессором"
|
|
1978
|
+
)
|
|
1979
|
+
|
|
1603
1980
|
return {
|
|
1604
1981
|
"status": "scheduled",
|
|
1605
1982
|
"handler_type": handler_type,
|
|
1606
1983
|
"delay_seconds": delay_seconds,
|
|
1607
1984
|
"event_id": event_id,
|
|
1608
|
-
"scheduled_at": datetime.now(timezone.utc).isoformat()
|
|
1985
|
+
"scheduled_at": datetime.now(timezone.utc).isoformat(),
|
|
1609
1986
|
}
|
|
1610
1987
|
|
|
1611
|
-
|
|
1988
|
+
|
|
1989
|
+
async def smart_execute_check(
|
|
1990
|
+
event_id: str, user_id: int, session_id: str, task_name: str, user_data: str
|
|
1991
|
+
) -> Dict[str, Any]:
|
|
1612
1992
|
"""
|
|
1613
1993
|
Умная проверка перед выполнением запланированной задачи
|
|
1614
|
-
|
|
1994
|
+
|
|
1615
1995
|
Логика:
|
|
1616
1996
|
1. Если пользователь перешел на новый этап - отменяем событие
|
|
1617
1997
|
2. Если прошло меньше времени чем планировалось - переносим на разницу
|
|
1618
1998
|
3. Если прошло достаточно времени - выполняем
|
|
1619
|
-
|
|
1999
|
+
|
|
1620
2000
|
Returns:
|
|
1621
2001
|
Dict с action: 'execute', 'cancel', 'reschedule'
|
|
1622
2002
|
"""
|
|
@@ -1624,84 +2004,109 @@ async def smart_execute_check(event_id: str, user_id: int, session_id: str, task
|
|
|
1624
2004
|
if not supabase_client:
|
|
1625
2005
|
logger.error("❌ Supabase клиент не найден для умной проверки")
|
|
1626
2006
|
return {"action": "execute", "reason": "no_supabase_client"}
|
|
1627
|
-
|
|
2007
|
+
|
|
1628
2008
|
try:
|
|
1629
2009
|
# Получаем информацию о последнем сообщении пользователя
|
|
1630
2010
|
user_info = await supabase_client.get_user_last_message_info(user_id)
|
|
1631
|
-
|
|
2011
|
+
|
|
1632
2012
|
if not user_info:
|
|
1633
2013
|
logger.info(f"🔄 Пользователь {user_id} не найден, выполняем задачу")
|
|
1634
2014
|
return {"action": "execute", "reason": "user_not_found"}
|
|
1635
|
-
|
|
2015
|
+
|
|
1636
2016
|
# Проверяем, изменился ли этап
|
|
1637
|
-
stage_changed = await supabase_client.check_user_stage_changed(
|
|
2017
|
+
stage_changed = await supabase_client.check_user_stage_changed(
|
|
2018
|
+
user_id, session_id
|
|
2019
|
+
)
|
|
1638
2020
|
if stage_changed:
|
|
1639
|
-
logger.info(
|
|
2021
|
+
logger.info(
|
|
2022
|
+
f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}"
|
|
2023
|
+
)
|
|
1640
2024
|
return {"action": "cancel", "reason": "user_stage_changed"}
|
|
1641
|
-
|
|
2025
|
+
|
|
1642
2026
|
# Получаем информацию о событии из БД
|
|
1643
|
-
event_response =
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
2027
|
+
event_response = (
|
|
2028
|
+
supabase_client.client.table("scheduled_events")
|
|
2029
|
+
.select("created_at", "scheduled_at")
|
|
2030
|
+
.eq("id", event_id)
|
|
2031
|
+
.execute()
|
|
2032
|
+
)
|
|
2033
|
+
|
|
1647
2034
|
if not event_response.data:
|
|
1648
2035
|
logger.error(f"❌ Событие {event_id} не найдено в БД")
|
|
1649
2036
|
return {"action": "execute", "reason": "event_not_found"}
|
|
1650
|
-
|
|
2037
|
+
|
|
1651
2038
|
event = event_response.data[0]
|
|
1652
|
-
created_at = datetime.fromisoformat(event[
|
|
1653
|
-
scheduled_at = datetime.fromisoformat(
|
|
1654
|
-
|
|
1655
|
-
|
|
2039
|
+
created_at = datetime.fromisoformat(event["created_at"].replace("Z", "+00:00"))
|
|
2040
|
+
scheduled_at = datetime.fromisoformat(
|
|
2041
|
+
event["scheduled_at"].replace("Z", "+00:00")
|
|
2042
|
+
)
|
|
2043
|
+
last_message_at = datetime.fromisoformat(
|
|
2044
|
+
user_info["last_message_at"].replace("Z", "+00:00")
|
|
2045
|
+
)
|
|
2046
|
+
|
|
1656
2047
|
# Вычисляем разницу во времени
|
|
1657
2048
|
now = datetime.now(timezone.utc)
|
|
1658
2049
|
time_since_creation = (now - created_at).total_seconds()
|
|
1659
2050
|
time_since_last_message = (now - last_message_at).total_seconds()
|
|
1660
2051
|
planned_delay = (scheduled_at - created_at).total_seconds()
|
|
1661
|
-
|
|
2052
|
+
|
|
1662
2053
|
# Проверяем, писал ли пользователь после создания события
|
|
1663
|
-
time_between_creation_and_last_message = (
|
|
1664
|
-
|
|
2054
|
+
time_between_creation_and_last_message = (
|
|
2055
|
+
last_message_at - created_at
|
|
2056
|
+
).total_seconds()
|
|
2057
|
+
|
|
1665
2058
|
logger.info(f"🔄 Анализ для пользователя {user_id}:")
|
|
1666
2059
|
logger.info(f" Время с создания события: {time_since_creation:.0f}с")
|
|
1667
2060
|
logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
|
|
1668
2061
|
logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
|
|
1669
|
-
logger.info(
|
|
1670
|
-
|
|
2062
|
+
logger.info(
|
|
2063
|
+
f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}"
|
|
2064
|
+
)
|
|
2065
|
+
|
|
1671
2066
|
# Если пользователь писал ПОСЛЕ создания события (недавно активен)
|
|
1672
2067
|
# И с момента его последнего сообщения прошло меньше planned_delay
|
|
1673
|
-
if
|
|
2068
|
+
if (
|
|
2069
|
+
time_between_creation_and_last_message > 0
|
|
2070
|
+
and time_since_last_message < planned_delay
|
|
2071
|
+
):
|
|
1674
2072
|
# Пересчитываем время - отправляем через planned_delay после последнего сообщения
|
|
1675
2073
|
new_delay = max(0, planned_delay - time_since_last_message)
|
|
1676
|
-
logger.info(
|
|
2074
|
+
logger.info(
|
|
2075
|
+
f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)"
|
|
2076
|
+
)
|
|
1677
2077
|
return {
|
|
1678
|
-
"action": "reschedule",
|
|
2078
|
+
"action": "reschedule",
|
|
1679
2079
|
"new_delay": new_delay,
|
|
1680
|
-
"reason": f"user_active_after_event_creation_{new_delay:.0f}s_delay"
|
|
2080
|
+
"reason": f"user_active_after_event_creation_{new_delay:.0f}s_delay",
|
|
1681
2081
|
}
|
|
1682
|
-
|
|
2082
|
+
|
|
1683
2083
|
# Если прошло достаточно времени с последнего сообщения - выполняем
|
|
1684
2084
|
if time_since_last_message >= planned_delay:
|
|
1685
|
-
logger.info(
|
|
2085
|
+
logger.info(
|
|
2086
|
+
f"🔄 Выполняем задачу {task_name} для пользователя {user_id} (прошло {time_since_last_message:.0f}с с последнего сообщения)"
|
|
2087
|
+
)
|
|
1686
2088
|
return {"action": "execute", "reason": "time_expired_since_last_message"}
|
|
1687
|
-
|
|
2089
|
+
|
|
1688
2090
|
# Если что-то пошло не так - выполняем
|
|
1689
2091
|
logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")
|
|
1690
2092
|
return {"action": "execute", "reason": "unexpected_situation"}
|
|
1691
|
-
|
|
2093
|
+
|
|
1692
2094
|
except Exception as e:
|
|
1693
2095
|
logger.error(f"❌ Ошибка в умной проверке для пользователя {user_id}: {e}")
|
|
1694
2096
|
return {"action": "execute", "reason": f"error_in_check: {str(e)}"}
|
|
1695
2097
|
|
|
1696
|
-
|
|
2098
|
+
|
|
2099
|
+
async def check_event_already_processed(
|
|
2100
|
+
event_type: str, user_id: int = None, session_id: str = None
|
|
2101
|
+
) -> bool:
|
|
1697
2102
|
"""
|
|
1698
2103
|
Проверяет, был ли уже обработан аналогичный event_type для пользователя/сессии
|
|
1699
|
-
|
|
2104
|
+
|
|
1700
2105
|
Args:
|
|
1701
2106
|
event_type: Тип события
|
|
1702
2107
|
user_id: ID пользователя (для user_event и scheduled_task)
|
|
1703
2108
|
session_id: ID сессии (для дополнительной проверки)
|
|
1704
|
-
|
|
2109
|
+
|
|
1705
2110
|
Returns:
|
|
1706
2111
|
True если событие уже обрабатывалось или в процессе
|
|
1707
2112
|
"""
|
|
@@ -1709,36 +2114,42 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
|
|
|
1709
2114
|
if not supabase_client:
|
|
1710
2115
|
logger.error("❌ Supabase клиент не найден для проверки дублирования")
|
|
1711
2116
|
return False
|
|
1712
|
-
|
|
2117
|
+
|
|
1713
2118
|
try:
|
|
1714
2119
|
# Строим запрос для поиска аналогичных событий
|
|
1715
|
-
query =
|
|
1716
|
-
|
|
2120
|
+
query = (
|
|
2121
|
+
supabase_client.client.table("scheduled_events")
|
|
2122
|
+
.select("id")
|
|
2123
|
+
.eq("event_type", event_type)
|
|
2124
|
+
)
|
|
2125
|
+
|
|
1717
2126
|
# Для глобальных событий (user_id = None)
|
|
1718
2127
|
if user_id is None:
|
|
1719
|
-
query = query.is_(
|
|
2128
|
+
query = query.is_("user_id", "null")
|
|
1720
2129
|
else:
|
|
1721
|
-
query = query.eq(
|
|
1722
|
-
|
|
2130
|
+
query = query.eq("user_id", user_id)
|
|
2131
|
+
|
|
1723
2132
|
# Добавляем фильтр по статусам (pending, immediate, completed)
|
|
1724
|
-
query = query.in_(
|
|
1725
|
-
|
|
2133
|
+
query = query.in_("status", ["pending", "immediate", "completed"])
|
|
2134
|
+
|
|
1726
2135
|
# Если есть session_id, добавляем его в фильтр
|
|
1727
2136
|
if session_id:
|
|
1728
|
-
query = query.eq(
|
|
1729
|
-
|
|
2137
|
+
query = query.eq("session_id", session_id)
|
|
2138
|
+
|
|
1730
2139
|
# 🆕 Фильтруем по bot_id если указан
|
|
1731
2140
|
if supabase_client.bot_id:
|
|
1732
|
-
query = query.eq(
|
|
1733
|
-
|
|
2141
|
+
query = query.eq("bot_id", supabase_client.bot_id)
|
|
2142
|
+
|
|
1734
2143
|
response = query.execute()
|
|
1735
|
-
|
|
2144
|
+
|
|
1736
2145
|
if response.data:
|
|
1737
|
-
logger.info(
|
|
2146
|
+
logger.info(
|
|
2147
|
+
f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'"
|
|
2148
|
+
)
|
|
1738
2149
|
return True
|
|
1739
|
-
|
|
2150
|
+
|
|
1740
2151
|
return False
|
|
1741
|
-
|
|
2152
|
+
|
|
1742
2153
|
except Exception as e:
|
|
1743
2154
|
logger.error(f"❌ Ошибка проверки дублирования для '{event_type}': {e}")
|
|
1744
2155
|
return False
|
|
@@ -1747,179 +2158,209 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
|
|
|
1747
2158
|
async def process_admin_event(event: Dict):
|
|
1748
2159
|
"""
|
|
1749
2160
|
Обрабатывает одно админское событие - скачивает файлы из Storage и отправляет пользователям
|
|
1750
|
-
|
|
2161
|
+
|
|
1751
2162
|
Args:
|
|
1752
2163
|
event: Событие из БД с данными для отправки
|
|
1753
2164
|
"""
|
|
1754
2165
|
import json
|
|
1755
|
-
import os
|
|
1756
2166
|
import shutil
|
|
1757
2167
|
from pathlib import Path
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2168
|
+
|
|
2169
|
+
from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo
|
|
2170
|
+
|
|
2171
|
+
event_id = event["id"]
|
|
2172
|
+
event_name = event["event_type"]
|
|
2173
|
+
event_data_str = event["event_data"]
|
|
2174
|
+
|
|
1764
2175
|
try:
|
|
1765
2176
|
event_data = json.loads(event_data_str)
|
|
1766
2177
|
except Exception as e:
|
|
1767
2178
|
logger.error(f"❌ Не удалось распарсить event_data для события {event_id}: {e}")
|
|
1768
2179
|
return {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2180
|
+
"success_count": 0,
|
|
2181
|
+
"failed_count": 0,
|
|
2182
|
+
"total_users": 0,
|
|
2183
|
+
"error": f"Ошибка парсинга event_data: {str(e)}",
|
|
1773
2184
|
}
|
|
1774
|
-
|
|
1775
|
-
segment = event_data.get(
|
|
1776
|
-
message_text = event_data.get(
|
|
1777
|
-
files_metadata = event_data.get(
|
|
1778
|
-
|
|
1779
|
-
logger.info(
|
|
1780
|
-
|
|
2185
|
+
|
|
2186
|
+
segment = event_data.get("segment")
|
|
2187
|
+
message_text = event_data.get("message")
|
|
2188
|
+
files_metadata = event_data.get("files", [])
|
|
2189
|
+
|
|
2190
|
+
logger.info(
|
|
2191
|
+
f"📨 Обработка события '{event_name}': сегмент='{segment}', файлов={len(files_metadata)}"
|
|
2192
|
+
)
|
|
2193
|
+
|
|
1781
2194
|
# Получаем клиенты
|
|
1782
2195
|
supabase_client = get_supabase_client()
|
|
1783
2196
|
if not supabase_client:
|
|
1784
2197
|
logger.error("❌ Supabase клиент не найден")
|
|
1785
|
-
return {
|
|
1786
|
-
|
|
2198
|
+
return {
|
|
2199
|
+
"success_count": 0,
|
|
2200
|
+
"failed_count": 0,
|
|
2201
|
+
"total_users": 0,
|
|
2202
|
+
"error": "Нет Supabase клиента",
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1787
2205
|
from ..handlers.handlers import get_global_var
|
|
1788
|
-
|
|
2206
|
+
|
|
2207
|
+
bot = get_global_var("bot")
|
|
1789
2208
|
if not bot:
|
|
1790
2209
|
logger.error("❌ Бот не найден")
|
|
1791
|
-
return {
|
|
1792
|
-
|
|
2210
|
+
return {
|
|
2211
|
+
"success_count": 0,
|
|
2212
|
+
"failed_count": 0,
|
|
2213
|
+
"total_users": 0,
|
|
2214
|
+
"error": "Нет бота",
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1793
2217
|
# Создаем временные папки
|
|
1794
2218
|
temp_with_msg = Path("temp_with_msg")
|
|
1795
2219
|
temp_after_msg = Path("temp_after_msg")
|
|
1796
2220
|
temp_with_msg.mkdir(exist_ok=True)
|
|
1797
2221
|
temp_after_msg.mkdir(exist_ok=True)
|
|
1798
|
-
|
|
2222
|
+
|
|
1799
2223
|
try:
|
|
1800
2224
|
# 1. Скачиваем файлы из Storage
|
|
1801
2225
|
for file_info in files_metadata:
|
|
1802
2226
|
try:
|
|
1803
2227
|
file_bytes = await supabase_client.download_event_file(
|
|
1804
|
-
event_id=event_id,
|
|
1805
|
-
storage_path=file_info['storage_path']
|
|
2228
|
+
event_id=event_id, storage_path=file_info["storage_path"]
|
|
1806
2229
|
)
|
|
1807
|
-
|
|
2230
|
+
|
|
1808
2231
|
# Сохраняем в соответствующую папку
|
|
1809
|
-
if file_info[
|
|
1810
|
-
file_path = temp_with_msg / file_info[
|
|
2232
|
+
if file_info["stage"] == "with_message":
|
|
2233
|
+
file_path = temp_with_msg / file_info["original_name"]
|
|
1811
2234
|
else:
|
|
1812
|
-
file_path = temp_after_msg / file_info[
|
|
1813
|
-
|
|
1814
|
-
with open(file_path,
|
|
2235
|
+
file_path = temp_after_msg / file_info["original_name"]
|
|
2236
|
+
|
|
2237
|
+
with open(file_path, "wb") as f:
|
|
1815
2238
|
f.write(file_bytes)
|
|
1816
|
-
|
|
2239
|
+
|
|
1817
2240
|
logger.info(f"📥 Скачан файл: {file_path}")
|
|
1818
|
-
|
|
2241
|
+
|
|
1819
2242
|
except Exception as e:
|
|
1820
2243
|
logger.error(f"❌ Ошибка скачивания файла {file_info['name']}: {e}")
|
|
1821
2244
|
raise
|
|
1822
|
-
|
|
2245
|
+
|
|
1823
2246
|
# 2. Получаем пользователей
|
|
1824
2247
|
users = await supabase_client.get_users_by_segment(segment)
|
|
1825
|
-
|
|
2248
|
+
|
|
1826
2249
|
if not users:
|
|
1827
2250
|
logger.warning(f"⚠️ Нет пользователей для сегмента '{segment}'")
|
|
1828
2251
|
return {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2252
|
+
"success_count": 0,
|
|
2253
|
+
"failed_count": 0,
|
|
2254
|
+
"total_users": 0,
|
|
2255
|
+
"segment": segment or "Все",
|
|
2256
|
+
"warning": "Нет пользователей",
|
|
1834
2257
|
}
|
|
1835
|
-
|
|
2258
|
+
|
|
1836
2259
|
success_count = 0
|
|
1837
2260
|
failed_count = 0
|
|
1838
|
-
|
|
2261
|
+
|
|
1839
2262
|
# 3. Отправляем каждому пользователю
|
|
1840
2263
|
for user in users:
|
|
1841
|
-
telegram_id = user[
|
|
1842
|
-
|
|
2264
|
+
telegram_id = user["telegram_id"]
|
|
2265
|
+
|
|
1843
2266
|
try:
|
|
1844
2267
|
# 3.1. Отправляем медиа-группу с сообщением
|
|
1845
|
-
files_with_msg = [
|
|
1846
|
-
|
|
2268
|
+
files_with_msg = [
|
|
2269
|
+
f for f in files_metadata if f["stage"] == "with_message"
|
|
2270
|
+
]
|
|
2271
|
+
|
|
1847
2272
|
if files_with_msg:
|
|
1848
2273
|
media_group = []
|
|
1849
2274
|
first_file = True
|
|
1850
|
-
|
|
2275
|
+
|
|
1851
2276
|
# Сортируем файлы по порядку
|
|
1852
|
-
sorted_files = sorted(
|
|
1853
|
-
|
|
2277
|
+
sorted_files = sorted(
|
|
2278
|
+
files_with_msg, key=lambda x: x.get("order", 0)
|
|
2279
|
+
)
|
|
2280
|
+
|
|
1854
2281
|
for file_info in sorted_files:
|
|
1855
|
-
file_path = temp_with_msg / file_info[
|
|
1856
|
-
|
|
1857
|
-
if file_info[
|
|
2282
|
+
file_path = temp_with_msg / file_info["original_name"]
|
|
2283
|
+
|
|
2284
|
+
if file_info["type"] == "photo":
|
|
1858
2285
|
media = InputMediaPhoto(
|
|
1859
2286
|
media=FSInputFile(file_path),
|
|
1860
2287
|
caption=message_text if first_file else None,
|
|
1861
|
-
parse_mode=
|
|
2288
|
+
parse_mode="Markdown" if first_file else None,
|
|
1862
2289
|
)
|
|
1863
2290
|
media_group.append(media)
|
|
1864
|
-
elif file_info[
|
|
2291
|
+
elif file_info["type"] == "video":
|
|
1865
2292
|
media = InputMediaVideo(
|
|
1866
2293
|
media=FSInputFile(file_path),
|
|
1867
2294
|
caption=message_text if first_file else None,
|
|
1868
|
-
parse_mode=
|
|
2295
|
+
parse_mode="Markdown" if first_file else None,
|
|
1869
2296
|
)
|
|
1870
2297
|
media_group.append(media)
|
|
1871
|
-
|
|
2298
|
+
|
|
1872
2299
|
first_file = False
|
|
1873
|
-
|
|
2300
|
+
|
|
1874
2301
|
if media_group:
|
|
1875
|
-
await bot.send_media_group(
|
|
2302
|
+
await bot.send_media_group(
|
|
2303
|
+
chat_id=telegram_id, media=media_group
|
|
2304
|
+
)
|
|
1876
2305
|
else:
|
|
1877
2306
|
# Только текст без файлов
|
|
1878
|
-
await bot.send_message(
|
|
1879
|
-
|
|
2307
|
+
await bot.send_message(
|
|
2308
|
+
chat_id=telegram_id, text=message_text, parse_mode="Markdown"
|
|
2309
|
+
)
|
|
2310
|
+
|
|
1880
2311
|
# 3.2. Отправляем файлы после сообщения
|
|
1881
|
-
files_after = [
|
|
1882
|
-
|
|
2312
|
+
files_after = [
|
|
2313
|
+
f for f in files_metadata if f["stage"] == "after_message"
|
|
2314
|
+
]
|
|
2315
|
+
|
|
1883
2316
|
for file_info in files_after:
|
|
1884
|
-
file_path = temp_after_msg / file_info[
|
|
1885
|
-
|
|
1886
|
-
if file_info[
|
|
1887
|
-
await bot.send_document(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
elif file_info[
|
|
1891
|
-
await bot.
|
|
1892
|
-
|
|
2317
|
+
file_path = temp_after_msg / file_info["original_name"]
|
|
2318
|
+
|
|
2319
|
+
if file_info["type"] == "document":
|
|
2320
|
+
await bot.send_document(
|
|
2321
|
+
chat_id=telegram_id, document=FSInputFile(file_path)
|
|
2322
|
+
)
|
|
2323
|
+
elif file_info["type"] == "photo":
|
|
2324
|
+
await bot.send_photo(
|
|
2325
|
+
chat_id=telegram_id, photo=FSInputFile(file_path)
|
|
2326
|
+
)
|
|
2327
|
+
elif file_info["type"] == "video":
|
|
2328
|
+
await bot.send_video(
|
|
2329
|
+
chat_id=telegram_id, video=FSInputFile(file_path)
|
|
2330
|
+
)
|
|
2331
|
+
|
|
1893
2332
|
success_count += 1
|
|
1894
2333
|
logger.info(f"✅ Отправлено пользователю {telegram_id}")
|
|
1895
|
-
|
|
2334
|
+
|
|
1896
2335
|
except Exception as e:
|
|
1897
2336
|
logger.error(f"❌ Ошибка отправки пользователю {telegram_id}: {e}")
|
|
1898
2337
|
failed_count += 1
|
|
1899
|
-
|
|
1900
|
-
logger.info(
|
|
1901
|
-
|
|
2338
|
+
|
|
2339
|
+
logger.info(
|
|
2340
|
+
f"📊 Результат '{event_name}': успешно={success_count}, ошибок={failed_count}"
|
|
2341
|
+
)
|
|
2342
|
+
|
|
1902
2343
|
# 4. Очистка после успешной отправки
|
|
1903
2344
|
# 4.1. Удаляем локальные временные файлы
|
|
1904
2345
|
shutil.rmtree(temp_with_msg, ignore_errors=True)
|
|
1905
2346
|
shutil.rmtree(temp_after_msg, ignore_errors=True)
|
|
1906
2347
|
logger.info("🗑️ Временные папки очищены")
|
|
1907
|
-
|
|
1908
|
-
|
|
2348
|
+
|
|
2349
|
+
# 4.2. Удаляем файлы из Supabase Storage
|
|
1909
2350
|
try:
|
|
1910
2351
|
await supabase_client.delete_event_files(event_id)
|
|
1911
2352
|
logger.info(f"🗑️ Файлы события '{event_id}' удалены из Storage")
|
|
1912
2353
|
except Exception as e:
|
|
1913
2354
|
logger.error(f"❌ Ошибка удаления из Storage: {e}")
|
|
1914
|
-
|
|
2355
|
+
|
|
1915
2356
|
return {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2357
|
+
"success_count": success_count,
|
|
2358
|
+
"failed_count": failed_count,
|
|
2359
|
+
"total_users": len(users),
|
|
2360
|
+
"segment": segment or "Все пользователи",
|
|
2361
|
+
"files_count": len(files_metadata),
|
|
1921
2362
|
}
|
|
1922
|
-
|
|
2363
|
+
|
|
1923
2364
|
except Exception as e:
|
|
1924
2365
|
# В случае ошибки все равно чистим временные файлы
|
|
1925
2366
|
shutil.rmtree(temp_with_msg, ignore_errors=True)
|
|
@@ -1927,34 +2368,34 @@ async def process_admin_event(event: Dict):
|
|
|
1927
2368
|
logger.error(f"❌ Критическая ошибка обработки события: {e}")
|
|
1928
2369
|
raise
|
|
1929
2370
|
|
|
2371
|
+
|
|
1930
2372
|
# =============================================================================
|
|
1931
2373
|
# ФУНКЦИЯ ДЛЯ ПОДГОТОВКИ ДАННЫХ ДАШБОРДА
|
|
1932
2374
|
# =============================================================================
|
|
1933
2375
|
|
|
2376
|
+
|
|
1934
2377
|
async def prepare_dashboard_info(
|
|
1935
|
-
description_template: str,
|
|
1936
|
-
title: str,
|
|
1937
|
-
user_id: int
|
|
2378
|
+
description_template: str, title: str, user_id: int
|
|
1938
2379
|
) -> Dict[str, Any]:
|
|
1939
2380
|
"""
|
|
1940
2381
|
Подготавливает данные для дашборда (БЕЗ записи в БД)
|
|
1941
|
-
|
|
2382
|
+
|
|
1942
2383
|
Возвращаемый dict нужно поместить в поле 'info' результата обработчика.
|
|
1943
2384
|
bot_utils.py автоматически запишет его в столбец info_dashboard таблицы.
|
|
1944
|
-
|
|
2385
|
+
|
|
1945
2386
|
Args:
|
|
1946
2387
|
description_template: Строка с {username}, например "{username} купил подписку"
|
|
1947
2388
|
title: Заголовок для дашборда
|
|
1948
2389
|
user_id: Telegram ID
|
|
1949
|
-
|
|
2390
|
+
|
|
1950
2391
|
Returns:
|
|
1951
2392
|
Dict с данными для дашборда
|
|
1952
|
-
|
|
2393
|
+
|
|
1953
2394
|
Example:
|
|
1954
2395
|
@event_router.event_handler("collect_phone", notify=True)
|
|
1955
2396
|
async def handle_phone_collection(user_id: int, phone_number: str):
|
|
1956
2397
|
# ... бизнес-логика ...
|
|
1957
|
-
|
|
2398
|
+
|
|
1958
2399
|
return {
|
|
1959
2400
|
"status": "success",
|
|
1960
2401
|
"phone": phone_number,
|
|
@@ -1966,29 +2407,33 @@ async def prepare_dashboard_info(
|
|
|
1966
2407
|
}
|
|
1967
2408
|
"""
|
|
1968
2409
|
supabase_client = get_supabase_client()
|
|
1969
|
-
|
|
2410
|
+
|
|
1970
2411
|
# Получаем username из sales_users
|
|
1971
2412
|
username = f"user_{user_id}" # fallback
|
|
1972
2413
|
if supabase_client:
|
|
1973
2414
|
try:
|
|
1974
|
-
query =
|
|
2415
|
+
query = (
|
|
2416
|
+
supabase_client.client.table("sales_users")
|
|
2417
|
+
.select("username")
|
|
2418
|
+
.eq("telegram_id", user_id)
|
|
2419
|
+
)
|
|
1975
2420
|
if supabase_client.bot_id:
|
|
1976
|
-
query = query.eq(
|
|
2421
|
+
query = query.eq("bot_id", supabase_client.bot_id)
|
|
1977
2422
|
response = query.execute()
|
|
1978
2423
|
if response.data:
|
|
1979
|
-
username = response.data[0].get(
|
|
2424
|
+
username = response.data[0].get("username") or username
|
|
1980
2425
|
except Exception as e:
|
|
1981
2426
|
logger.warning(f"⚠️ Не удалось получить username для дашборда: {e}")
|
|
1982
|
-
|
|
2427
|
+
|
|
1983
2428
|
# Форматируем строку
|
|
1984
2429
|
description = description_template.format(username=username)
|
|
1985
|
-
|
|
2430
|
+
|
|
1986
2431
|
# Московское время (UTC+3)
|
|
1987
2432
|
moscow_tz = timezone(timedelta(hours=3))
|
|
1988
2433
|
moscow_time = datetime.now(moscow_tz)
|
|
1989
|
-
|
|
2434
|
+
|
|
1990
2435
|
return {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2436
|
+
"title": title,
|
|
2437
|
+
"description": description,
|
|
2438
|
+
"created_at": moscow_time.isoformat(),
|
|
1994
2439
|
}
|