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