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