smart-bot-factory 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/cli.py +10 -61
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +0 -2
- smart_bot_factory/core/bot_utils.py +15 -25
- smart_bot_factory/core/decorators.py +582 -83
- smart_bot_factory/core/message_sender.py +207 -0
- smart_bot_factory/core/router.py +50 -9
- smart_bot_factory/core/router_manager.py +18 -4
- smart_bot_factory/creation/bot_builder.py +7 -2
- smart_bot_factory/handlers/handlers.py +110 -0
- smart_bot_factory/integrations/supabase_client.py +74 -4
- smart_bot_factory/message/__init__.py +15 -11
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +524 -0
- smart_bot_factory-0.1.8.dist-info/METADATA +126 -0
- {smart_bot_factory-0.1.6.dist-info → smart_bot_factory-0.1.8.dist-info}/RECORD +18 -18
- smart_bot_factory/supabase/example_usage.py +0 -1
- smart_bot_factory-0.1.6.dist-info/METADATA +0 -466
- {smart_bot_factory-0.1.6.dist-info → smart_bot_factory-0.1.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.6.dist-info → smart_bot_factory-0.1.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.6.dist-info → smart_bot_factory-0.1.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,12 +4,339 @@
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import logging
|
|
7
|
-
|
|
7
|
+
import re
|
|
8
|
+
from typing import Callable, Any, Dict, Union
|
|
8
9
|
from datetime import datetime, timedelta, timezone
|
|
9
10
|
from functools import wraps
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
def parse_time_string(time_str: Union[str, int]) -> int:
|
|
15
|
+
"""
|
|
16
|
+
Парсит время в удобном формате и возвращает секунды
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
time_str: Время в формате "1h 30m 45s" или число (секунды)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
int: Количество секунд
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
parse_time_string("1h 30m 45s") -> 5445
|
|
26
|
+
parse_time_string("2h") -> 7200
|
|
27
|
+
parse_time_string("45m") -> 2700
|
|
28
|
+
parse_time_string("30s") -> 30
|
|
29
|
+
parse_time_string(3600) -> 3600
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(time_str, int):
|
|
32
|
+
return time_str
|
|
33
|
+
|
|
34
|
+
# Убираем лишние пробелы и приводим к нижнему регистру
|
|
35
|
+
time_str = time_str.strip().lower()
|
|
36
|
+
|
|
37
|
+
# Если это просто число - возвращаем как секунды
|
|
38
|
+
if time_str.isdigit():
|
|
39
|
+
return int(time_str)
|
|
40
|
+
|
|
41
|
+
total_seconds = 0
|
|
42
|
+
|
|
43
|
+
# Регулярное выражение для поиска времени: число + единица (h, m, s)
|
|
44
|
+
pattern = r'(\d+)\s*(h|m|s)'
|
|
45
|
+
matches = re.findall(pattern, time_str)
|
|
46
|
+
|
|
47
|
+
if not matches:
|
|
48
|
+
raise ValueError(f"Неверный формат времени: '{time_str}'. Используйте формат '1h 30m 45s'")
|
|
49
|
+
|
|
50
|
+
for value, unit in matches:
|
|
51
|
+
value = int(value)
|
|
52
|
+
|
|
53
|
+
if unit == 'h': # часы
|
|
54
|
+
total_seconds += value * 3600
|
|
55
|
+
elif unit == 'm': # минуты
|
|
56
|
+
total_seconds += value * 60
|
|
57
|
+
elif unit == 's': # секунды
|
|
58
|
+
total_seconds += value
|
|
59
|
+
|
|
60
|
+
if total_seconds <= 0:
|
|
61
|
+
raise ValueError(f"Время должно быть больше 0: '{time_str}'")
|
|
62
|
+
|
|
63
|
+
return total_seconds
|
|
64
|
+
|
|
65
|
+
def parse_supabase_datetime(datetime_str: str) -> datetime:
|
|
66
|
+
"""
|
|
67
|
+
Парсит дату и время из формата Supabase в объект datetime
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
datetime_str: Строка даты и времени из Supabase (ISO 8601 формат)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
datetime: Объект datetime с timezone
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
parse_supabase_datetime("2024-01-15T10:30:45.123456Z") -> datetime(2024, 1, 15, 10, 30, 45, 123456, tzinfo=timezone.utc)
|
|
77
|
+
parse_supabase_datetime("2024-01-15T10:30:45+00:00") -> datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
|
|
78
|
+
parse_supabase_datetime("2024-01-15T10:30:45") -> datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
|
|
79
|
+
"""
|
|
80
|
+
if not datetime_str:
|
|
81
|
+
raise ValueError("Пустая строка даты и времени")
|
|
82
|
+
|
|
83
|
+
# Убираем лишние пробелы
|
|
84
|
+
datetime_str = datetime_str.strip()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Пробуем парсить ISO 8601 формат с Z в конце
|
|
88
|
+
if datetime_str.endswith('Z'):
|
|
89
|
+
# Заменяем Z на +00:00 для корректного парсинга
|
|
90
|
+
datetime_str = datetime_str[:-1] + '+00:00'
|
|
91
|
+
return datetime.fromisoformat(datetime_str)
|
|
92
|
+
|
|
93
|
+
# Пробуем парсить ISO 8601 формат с timezone
|
|
94
|
+
if '+' in datetime_str or datetime_str.count('-') > 2:
|
|
95
|
+
return datetime.fromisoformat(datetime_str)
|
|
96
|
+
|
|
97
|
+
# Если нет timezone, добавляем UTC
|
|
98
|
+
if 'T' in datetime_str:
|
|
99
|
+
return datetime.fromisoformat(datetime_str + '+00:00')
|
|
100
|
+
|
|
101
|
+
# Если это только дата, добавляем время 00:00:00 и UTC
|
|
102
|
+
return datetime.fromisoformat(datetime_str + 'T00:00:00+00:00')
|
|
103
|
+
|
|
104
|
+
except ValueError as e:
|
|
105
|
+
raise ValueError(f"Неверный формат даты и времени: '{datetime_str}'. Ошибка: {e}")
|
|
106
|
+
|
|
107
|
+
def format_datetime_for_supabase(dt: datetime) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Форматирует объект datetime в формат для Supabase
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
dt: Объект datetime
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
str: Строка в формате ISO 8601 для Supabase
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
format_datetime_for_supabase(datetime.now(timezone.utc)) -> "2024-01-15T10:30:45.123456+00:00"
|
|
119
|
+
"""
|
|
120
|
+
if not isinstance(dt, datetime):
|
|
121
|
+
raise ValueError("Ожидается объект datetime")
|
|
122
|
+
|
|
123
|
+
# Если нет timezone, добавляем UTC
|
|
124
|
+
if dt.tzinfo is None:
|
|
125
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
126
|
+
|
|
127
|
+
return dt.isoformat()
|
|
128
|
+
|
|
129
|
+
def get_time_difference_seconds(dt1: datetime, dt2: datetime) -> int:
|
|
130
|
+
"""
|
|
131
|
+
Вычисляет разность между двумя датами в секундах
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
dt1: Первая дата
|
|
135
|
+
dt2: Вторая дата
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
int: Разность в секундах (dt2 - dt1)
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
get_time_difference_seconds(datetime1, datetime2) -> 3600 # 1 час
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Если у дат нет timezone, добавляем UTC
|
|
145
|
+
if dt1.tzinfo is None:
|
|
146
|
+
dt1 = dt1.replace(tzinfo=timezone.utc)
|
|
147
|
+
if dt2.tzinfo is None:
|
|
148
|
+
dt2 = dt2.replace(tzinfo=timezone.utc)
|
|
149
|
+
|
|
150
|
+
return int((dt2 - dt1).total_seconds())
|
|
151
|
+
|
|
152
|
+
def is_datetime_recent(dt: datetime, max_age_seconds: int = 3600) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Проверяет, является ли дата недавней (не старше указанного времени)
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
dt: Дата для проверки
|
|
158
|
+
max_age_seconds: Максимальный возраст в секундах (по умолчанию 1 час)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
bool: True если дата недавняя, False если старая
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
is_datetime_recent(datetime.now(), 1800) -> True # если дата сейчас
|
|
165
|
+
is_datetime_recent(datetime.now() - timedelta(hours=2), 3600) -> False # если дата 2 часа назад
|
|
166
|
+
"""
|
|
167
|
+
if not isinstance(dt, datetime):
|
|
168
|
+
raise ValueError("Ожидается объект datetime")
|
|
169
|
+
|
|
170
|
+
now = datetime.now(timezone.utc)
|
|
171
|
+
|
|
172
|
+
# Если у даты нет timezone, добавляем UTC
|
|
173
|
+
if dt.tzinfo is None:
|
|
174
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
175
|
+
|
|
176
|
+
age_seconds = (now - dt).total_seconds()
|
|
177
|
+
return age_seconds <= max_age_seconds
|
|
178
|
+
|
|
179
|
+
def parse_appointment_data(data_str: str) -> Dict[str, Any]:
|
|
180
|
+
"""
|
|
181
|
+
Парсит данные записи на прием из строки формата "ключ: значение, ключ: значение"
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
data_str: Строка с данными записи
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dict[str, Any]: Словарь с распарсенными данными
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
parse_appointment_data("имя: Михаил, телефон: +79965214968, процедура: Ламинирование + окрашивание, мастер: Софья, дата: 2025-10-01, время: 19:00")
|
|
191
|
+
-> {
|
|
192
|
+
'имя': 'Михаил',
|
|
193
|
+
'телефон': '+79965214968',
|
|
194
|
+
'процедура': 'Ламинирование + окрашивание',
|
|
195
|
+
'мастер': 'Софья',
|
|
196
|
+
'дата': '2025-10-01',
|
|
197
|
+
'время': '19:00'
|
|
198
|
+
}
|
|
199
|
+
"""
|
|
200
|
+
if not data_str or not isinstance(data_str, str):
|
|
201
|
+
return {}
|
|
202
|
+
|
|
203
|
+
result = {}
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Разделяем по запятым, но учитываем что внутри значений могут быть запятые
|
|
207
|
+
# Используем более умный подход - ищем паттерн "ключ: значение"
|
|
208
|
+
pattern = r'([^:]+):\s*([^,]+?)(?=,\s*[^:]+:|$)'
|
|
209
|
+
matches = re.findall(pattern, data_str.strip())
|
|
210
|
+
|
|
211
|
+
for key, value in matches:
|
|
212
|
+
# Очищаем ключ и значение от лишних пробелов
|
|
213
|
+
clean_key = key.strip()
|
|
214
|
+
clean_value = value.strip()
|
|
215
|
+
|
|
216
|
+
# Убираем запятые в конце значения если есть
|
|
217
|
+
if clean_value.endswith(','):
|
|
218
|
+
clean_value = clean_value[:-1].strip()
|
|
219
|
+
|
|
220
|
+
result[clean_key] = clean_value
|
|
221
|
+
|
|
222
|
+
# Дополнительная обработка для даты и времени
|
|
223
|
+
if 'дата' in result and 'время' in result:
|
|
224
|
+
try:
|
|
225
|
+
# Создаем полную дату и время
|
|
226
|
+
date_str = result['дата']
|
|
227
|
+
time_str = result['время']
|
|
228
|
+
|
|
229
|
+
# Парсим дату и время
|
|
230
|
+
appointment_datetime = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
|
|
231
|
+
|
|
232
|
+
# Добавляем в результат
|
|
233
|
+
result['datetime'] = appointment_datetime
|
|
234
|
+
result['datetime_str'] = appointment_datetime.strftime("%Y-%m-%d %H:%M")
|
|
235
|
+
|
|
236
|
+
# Проверяем, не в прошлом ли запись
|
|
237
|
+
now = datetime.now()
|
|
238
|
+
if appointment_datetime < now:
|
|
239
|
+
result['is_past'] = True
|
|
240
|
+
else:
|
|
241
|
+
result['is_past'] = False
|
|
242
|
+
|
|
243
|
+
except ValueError as e:
|
|
244
|
+
logger.warning(f"Ошибка парсинга даты/времени: {e}")
|
|
245
|
+
result['datetime_error'] = str(e)
|
|
246
|
+
|
|
247
|
+
logger.info(f"Распарсены данные записи: {list(result.keys())}")
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error(f"Ошибка парсинга данных записи: {e}")
|
|
252
|
+
return {'error': str(e), 'raw_data': data_str}
|
|
253
|
+
|
|
254
|
+
def format_appointment_data(appointment_data: Dict[str, Any]) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Форматирует данные записи обратно в строку
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
appointment_data: Словарь с данными записи
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
str: Отформатированная строка
|
|
263
|
+
|
|
264
|
+
Examples:
|
|
265
|
+
format_appointment_data({
|
|
266
|
+
'имя': 'Михаил',
|
|
267
|
+
'телефон': '+79965214968',
|
|
268
|
+
'процедура': 'Ламинирование + окрашивание',
|
|
269
|
+
'мастер': 'Софья',
|
|
270
|
+
'дата': '2025-10-01',
|
|
271
|
+
'время': '19:00'
|
|
272
|
+
})
|
|
273
|
+
-> "имя: Михаил, телефон: +79965214968, процедура: Ламинирование + окрашивание, мастер: Софья, дата: 2025-10-01, время: 19:00"
|
|
274
|
+
"""
|
|
275
|
+
if not appointment_data or not isinstance(appointment_data, dict):
|
|
276
|
+
return ""
|
|
277
|
+
|
|
278
|
+
# Исключаем служебные поля
|
|
279
|
+
exclude_fields = {'datetime', 'datetime_str', 'is_past', 'datetime_error', 'error', 'raw_data'}
|
|
280
|
+
|
|
281
|
+
parts = []
|
|
282
|
+
for key, value in appointment_data.items():
|
|
283
|
+
if key not in exclude_fields and value is not None:
|
|
284
|
+
parts.append(f"{key}: {value}")
|
|
285
|
+
|
|
286
|
+
return ", ".join(parts)
|
|
287
|
+
|
|
288
|
+
def validate_appointment_data(appointment_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
Валидирует данные записи на прием
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
appointment_data: Словарь с данными записи
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Dict[str, Any]: Результат валидации с полями 'valid', 'errors', 'warnings'
|
|
297
|
+
"""
|
|
298
|
+
result = {
|
|
299
|
+
'valid': True,
|
|
300
|
+
'errors': [],
|
|
301
|
+
'warnings': []
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Проверяем обязательные поля
|
|
305
|
+
required_fields = ['имя', 'телефон', 'процедура', 'мастер', 'дата', 'время']
|
|
306
|
+
|
|
307
|
+
for field in required_fields:
|
|
308
|
+
if field not in appointment_data or not appointment_data[field]:
|
|
309
|
+
result['errors'].append(f"Отсутствует обязательное поле: {field}")
|
|
310
|
+
result['valid'] = False
|
|
311
|
+
|
|
312
|
+
# Проверяем формат телефона
|
|
313
|
+
if 'телефон' in appointment_data:
|
|
314
|
+
phone = appointment_data['телефон']
|
|
315
|
+
if not re.match(r'^\+?[1-9]\d{10,14}$', phone.replace(' ', '').replace('-', '')):
|
|
316
|
+
result['warnings'].append(f"Неверный формат телефона: {phone}")
|
|
317
|
+
|
|
318
|
+
# Проверяем дату
|
|
319
|
+
if 'дата' in appointment_data:
|
|
320
|
+
try:
|
|
321
|
+
datetime.strptime(appointment_data['дата'], "%Y-%m-%d")
|
|
322
|
+
except ValueError:
|
|
323
|
+
result['errors'].append(f"Неверный формат даты: {appointment_data['дата']}")
|
|
324
|
+
result['valid'] = False
|
|
325
|
+
|
|
326
|
+
# Проверяем время
|
|
327
|
+
if 'время' in appointment_data:
|
|
328
|
+
try:
|
|
329
|
+
datetime.strptime(appointment_data['время'], "%H:%M")
|
|
330
|
+
except ValueError:
|
|
331
|
+
result['errors'].append(f"Неверный формат времени: {appointment_data['время']}")
|
|
332
|
+
result['valid'] = False
|
|
333
|
+
|
|
334
|
+
# Проверяем, не в прошлом ли запись
|
|
335
|
+
if 'is_past' in appointment_data and appointment_data['is_past']:
|
|
336
|
+
result['warnings'].append("Запись назначена на прошедшую дату")
|
|
337
|
+
|
|
338
|
+
return result
|
|
339
|
+
|
|
13
340
|
# Глобальный реестр обработчиков событий
|
|
14
341
|
_event_handlers: Dict[str, Callable] = {}
|
|
15
342
|
_scheduled_tasks: Dict[str, Dict[str, Any]] = {}
|
|
@@ -76,7 +403,7 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
|
|
|
76
403
|
return wrapper
|
|
77
404
|
return decorator
|
|
78
405
|
|
|
79
|
-
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True):
|
|
406
|
+
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: str = None):
|
|
80
407
|
"""
|
|
81
408
|
Декоратор для регистрации задачи, которую можно запланировать на время
|
|
82
409
|
|
|
@@ -85,41 +412,68 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
85
412
|
notify: Уведомлять ли админов о выполнении задачи (по умолчанию False)
|
|
86
413
|
smart_check: Использовать ли умную проверку активности пользователя (по умолчанию True)
|
|
87
414
|
once_only: Выполнять ли задачу только один раз (по умолчанию True)
|
|
415
|
+
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
416
|
+
event_type: Тип события для напоминания (например, 'appointment_booking') - ОПЦИОНАЛЬНО
|
|
88
417
|
|
|
89
418
|
Example:
|
|
90
|
-
#
|
|
91
|
-
@schedule_task("send_reminder",
|
|
419
|
+
# Обычная задача с фиксированным временем
|
|
420
|
+
@schedule_task("send_reminder", delay="1h 30m")
|
|
92
421
|
async def send_reminder(user_id: int, user_data: str):
|
|
93
|
-
#
|
|
94
|
-
# Логика отправки напоминания (выполняется на фоне)
|
|
422
|
+
# Задача будет запланирована на 1 час 30 минут
|
|
95
423
|
return {"status": "sent", "message": user_data}
|
|
96
424
|
|
|
97
|
-
#
|
|
98
|
-
@schedule_task("
|
|
99
|
-
async def
|
|
100
|
-
#
|
|
425
|
+
# Напоминание о событии (за delay времени до события)
|
|
426
|
+
@schedule_task("appointment_reminder", delay="2h", event_type="appointment_booking")
|
|
427
|
+
async def appointment_reminder(user_id: int, user_data: str):
|
|
428
|
+
# Напоминание будет отправлено за 2 часа до события appointment_booking
|
|
101
429
|
return {"status": "sent", "message": user_data}
|
|
102
430
|
|
|
103
|
-
#
|
|
104
|
-
@schedule_task("
|
|
105
|
-
async def
|
|
106
|
-
#
|
|
431
|
+
# Напоминание о процедуре
|
|
432
|
+
@schedule_task("procedure_reminder", delay="1d", event_type="procedure_booking")
|
|
433
|
+
async def procedure_reminder(user_id: int, user_data: str):
|
|
434
|
+
# Напоминание будет отправлено за 1 день до процедуры
|
|
107
435
|
return {"status": "sent", "message": user_data}
|
|
108
436
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
437
|
+
# Форматы времени:
|
|
438
|
+
# delay="1h 30m 45s" - 1 час 30 минут 45 секунд
|
|
439
|
+
# delay="2h" - 2 часа
|
|
440
|
+
# delay="30m" - 30 минут
|
|
441
|
+
# delay=3600 - 3600 секунд (число)
|
|
442
|
+
|
|
443
|
+
# ИИ может передавать только данные (текст):
|
|
444
|
+
# {"тип": "send_reminder", "инфо": "Текст напоминания"} - только текст
|
|
445
|
+
# {"тип": "appointment_reminder", "инфо": ""} - пустой текст, время берется из события
|
|
112
446
|
"""
|
|
113
447
|
def decorator(func: Callable) -> Callable:
|
|
448
|
+
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
449
|
+
if delay is None:
|
|
450
|
+
raise ValueError(f"Для задачи '{task_name}' ОБЯЗАТЕЛЬНО нужно указать параметр delay")
|
|
451
|
+
|
|
452
|
+
# Парсим время
|
|
453
|
+
try:
|
|
454
|
+
default_delay_seconds = parse_time_string(delay)
|
|
455
|
+
if event_type:
|
|
456
|
+
logger.info(f"⏰ Задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)")
|
|
457
|
+
else:
|
|
458
|
+
logger.info(f"⏰ Задача '{task_name}' настроена с задержкой: {delay} ({default_delay_seconds}с)")
|
|
459
|
+
except ValueError as e:
|
|
460
|
+
logger.error(f"❌ Ошибка парсинга времени для задачи '{task_name}': {e}")
|
|
461
|
+
raise
|
|
462
|
+
|
|
114
463
|
_scheduled_tasks[task_name] = {
|
|
115
464
|
'handler': func,
|
|
116
465
|
'name': func.__name__,
|
|
117
466
|
'notify': notify,
|
|
118
467
|
'smart_check': smart_check,
|
|
119
|
-
'once_only': once_only
|
|
468
|
+
'once_only': once_only,
|
|
469
|
+
'default_delay': default_delay_seconds,
|
|
470
|
+
'event_type': event_type # Новое поле для типа события
|
|
120
471
|
}
|
|
121
472
|
|
|
122
|
-
|
|
473
|
+
if event_type:
|
|
474
|
+
logger.info(f"⏰ Зарегистрирована задача-напоминание '{task_name}' для события '{event_type}': {func.__name__}")
|
|
475
|
+
else:
|
|
476
|
+
logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
|
|
123
477
|
|
|
124
478
|
@wraps(func)
|
|
125
479
|
async def wrapper(*args, **kwargs):
|
|
@@ -147,7 +501,7 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
147
501
|
return wrapper
|
|
148
502
|
return decorator
|
|
149
503
|
|
|
150
|
-
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True):
|
|
504
|
+
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None):
|
|
151
505
|
"""
|
|
152
506
|
Декоратор для регистрации глобального обработчика (для всех пользователей)
|
|
153
507
|
|
|
@@ -155,26 +509,50 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
155
509
|
handler_type: Тип глобального обработчика (например, 'global_announcement', 'mass_notification')
|
|
156
510
|
notify: Уведомлять ли админов о выполнении (по умолчанию False)
|
|
157
511
|
once_only: Выполнять ли обработчик только один раз (по умолчанию True)
|
|
512
|
+
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
158
513
|
|
|
159
514
|
Example:
|
|
160
|
-
# Глобальный обработчик
|
|
161
|
-
@global_handler("global_announcement", notify=True)
|
|
515
|
+
# Глобальный обработчик с задержкой
|
|
516
|
+
@global_handler("global_announcement", delay="2h", notify=True)
|
|
162
517
|
async def send_global_announcement(announcement_text: str):
|
|
163
|
-
#
|
|
518
|
+
# Выполнится через 2 часа
|
|
164
519
|
return {"status": "sent", "recipients_count": 150}
|
|
165
520
|
|
|
166
521
|
# Глобальный обработчик может выполняться многократно
|
|
167
|
-
@global_handler("daily_report", once_only=False)
|
|
522
|
+
@global_handler("daily_report", delay="24h", once_only=False)
|
|
168
523
|
async def send_daily_report(report_data: str):
|
|
169
|
-
# Может запускаться каждый день
|
|
524
|
+
# Может запускаться каждый день через 24 часа
|
|
170
525
|
return {"status": "sent", "report_type": "daily"}
|
|
526
|
+
|
|
527
|
+
# Форматы времени:
|
|
528
|
+
# delay="1h 30m 45s" - 1 час 30 минут 45 секунд
|
|
529
|
+
# delay="2h" - 2 часа
|
|
530
|
+
# delay="45m" - 45 минут
|
|
531
|
+
# delay=3600 - 3600 секунд (число)
|
|
532
|
+
|
|
533
|
+
# ИИ может передавать только данные (текст):
|
|
534
|
+
# {"тип": "global_announcement", "инфо": "Важное объявление!"} - только текст
|
|
535
|
+
# {"тип": "global_announcement", "инфо": ""} - пустой текст
|
|
171
536
|
"""
|
|
172
537
|
def decorator(func: Callable) -> Callable:
|
|
538
|
+
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
539
|
+
if delay is None:
|
|
540
|
+
raise ValueError(f"Для глобального обработчика '{handler_type}' ОБЯЗАТЕЛЬНО нужно указать параметр delay")
|
|
541
|
+
|
|
542
|
+
# Парсим время
|
|
543
|
+
try:
|
|
544
|
+
default_delay_seconds = parse_time_string(delay)
|
|
545
|
+
logger.info(f"🌍 Глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)")
|
|
546
|
+
except ValueError as e:
|
|
547
|
+
logger.error(f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}")
|
|
548
|
+
raise
|
|
549
|
+
|
|
173
550
|
_global_handlers[handler_type] = {
|
|
174
551
|
'handler': func,
|
|
175
552
|
'name': func.__name__,
|
|
176
553
|
'notify': notify,
|
|
177
|
-
'once_only': once_only
|
|
554
|
+
'once_only': once_only,
|
|
555
|
+
'default_delay': default_delay_seconds
|
|
178
556
|
}
|
|
179
557
|
|
|
180
558
|
logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
|
|
@@ -269,13 +647,17 @@ async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
|
|
|
269
647
|
|
|
270
648
|
# Fallback к старым декораторам
|
|
271
649
|
if event_type not in _event_handlers:
|
|
650
|
+
import inspect
|
|
651
|
+
frame = inspect.currentframe()
|
|
652
|
+
line_no = frame.f_lineno if frame else "unknown"
|
|
653
|
+
logger.error(f"❌ [decorators.py:{line_no}] Обработчик события '{event_type}' не найден")
|
|
272
654
|
raise ValueError(f"Обработчик события '{event_type}' не найден")
|
|
273
655
|
|
|
274
656
|
handler_info = _event_handlers[event_type]
|
|
275
657
|
return await handler_info['handler'](*args, **kwargs)
|
|
276
658
|
|
|
277
659
|
async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -> Any:
|
|
278
|
-
"""Выполняет запланированную задачу по имени"""
|
|
660
|
+
"""Выполняет запланированную задачу по имени (без планирования, только выполнение)"""
|
|
279
661
|
# Сначала пробуем получить из роутеров
|
|
280
662
|
if _router_manager:
|
|
281
663
|
scheduled_tasks = _router_manager.get_scheduled_tasks()
|
|
@@ -283,9 +665,6 @@ async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -
|
|
|
283
665
|
task_info = scheduled_tasks[task_name]
|
|
284
666
|
return await task_info['handler'](user_id, user_data)
|
|
285
667
|
|
|
286
|
-
# Fallback к старым декораторам
|
|
287
|
-
if task_name not in _scheduled_tasks:
|
|
288
|
-
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
289
668
|
|
|
290
669
|
task_info = _scheduled_tasks[task_name]
|
|
291
670
|
return await task_info['handler'](user_id, user_data)
|
|
@@ -314,8 +693,19 @@ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: i
|
|
|
314
693
|
user_id: ID пользователя
|
|
315
694
|
user_data: Простой текст для задачи
|
|
316
695
|
"""
|
|
317
|
-
|
|
318
|
-
|
|
696
|
+
# Ищем задачу через RouterManager (новая логика)
|
|
697
|
+
router_manager = get_router_manager()
|
|
698
|
+
if router_manager:
|
|
699
|
+
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
700
|
+
logger.debug(f"🔍 Поиск задачи '{task_name}' через RouterManager")
|
|
701
|
+
else:
|
|
702
|
+
scheduled_tasks = _scheduled_tasks
|
|
703
|
+
logger.debug(f"🔍 Поиск задачи '{task_name}' через глобальный реестр")
|
|
704
|
+
|
|
705
|
+
if task_name not in scheduled_tasks:
|
|
706
|
+
available_tasks = list(scheduled_tasks.keys())
|
|
707
|
+
logger.error(f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
|
|
708
|
+
raise ValueError(f"Задача '{task_name}' не найдена. Доступные: {available_tasks}")
|
|
319
709
|
|
|
320
710
|
logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
|
|
321
711
|
|
|
@@ -333,42 +723,113 @@ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: i
|
|
|
333
723
|
"scheduled_at": datetime.now().isoformat()
|
|
334
724
|
}
|
|
335
725
|
|
|
336
|
-
async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str):
|
|
726
|
+
async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str, session_id: str = None):
|
|
337
727
|
"""
|
|
338
728
|
Выполняет запланированную задачу на основе события от ИИ
|
|
339
729
|
|
|
340
730
|
Args:
|
|
341
731
|
user_id: ID пользователя
|
|
342
732
|
task_name: Название задачи
|
|
343
|
-
event_info: Информация от ИИ (
|
|
733
|
+
event_info: Информация от ИИ (только текст, время задается в декораторе или событии)
|
|
734
|
+
session_id: ID сессии для отслеживания
|
|
344
735
|
"""
|
|
345
|
-
|
|
346
|
-
|
|
736
|
+
router_manager = get_router_manager()
|
|
737
|
+
if router_manager:
|
|
738
|
+
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
739
|
+
logger.debug(f"🔍 RouterManager найден, доступные задачи: {list(scheduled_tasks.keys())}")
|
|
740
|
+
else:
|
|
741
|
+
scheduled_tasks = _scheduled_tasks
|
|
742
|
+
logger.debug(f"🔍 RouterManager не найден, старые задачи: {list(scheduled_tasks.keys())}")
|
|
347
743
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
744
|
+
if task_name not in scheduled_tasks:
|
|
745
|
+
available_tasks = list(scheduled_tasks.keys())
|
|
746
|
+
logger.error(f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
|
|
747
|
+
logger.error(f"❌ RouterManager статус: {'найден' if router_manager else 'НЕ НАЙДЕН'}")
|
|
748
|
+
raise ValueError(f"Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
|
|
749
|
+
|
|
750
|
+
task_info = scheduled_tasks[task_name]
|
|
751
|
+
default_delay = task_info.get('default_delay')
|
|
752
|
+
event_type = task_info.get('event_type')
|
|
753
|
+
|
|
754
|
+
# Время всегда берется из декоратора, ИИ может передавать только текст
|
|
755
|
+
if default_delay is None:
|
|
756
|
+
raise ValueError(f"Для задачи '{task_name}' не указано время в декораторе (параметр delay)")
|
|
757
|
+
|
|
758
|
+
# event_info содержит только текст для задачи
|
|
759
|
+
user_data = event_info.strip() if event_info else f"Напоминание через {default_delay} секунд"
|
|
760
|
+
|
|
761
|
+
# Если указан event_type, то это напоминание о событии
|
|
762
|
+
if event_type:
|
|
763
|
+
logger.info(f"⏰ Задача '{task_name}' - напоминание о событии '{event_type}' за {default_delay}с")
|
|
764
|
+
|
|
765
|
+
# Получаем клиент Supabase
|
|
766
|
+
supabase_client = get_supabase_client()
|
|
767
|
+
if not supabase_client:
|
|
768
|
+
raise RuntimeError("Supabase клиент не найден для получения времени события")
|
|
769
|
+
|
|
770
|
+
try:
|
|
771
|
+
# Получаем данные события
|
|
772
|
+
event_data_str = await supabase_client.get_last_event_info_by_user_and_type(user_id, event_type)
|
|
773
|
+
|
|
774
|
+
if not event_data_str:
|
|
775
|
+
logger.warning(f"Событие '{event_type}' не найдено для пользователя {user_id}")
|
|
776
|
+
# Fallback - планируем через default_delay
|
|
777
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
778
|
+
return result
|
|
779
|
+
|
|
780
|
+
# Парсим данные события
|
|
781
|
+
event_data = parse_appointment_data(event_data_str)
|
|
782
|
+
|
|
783
|
+
if 'datetime' not in event_data:
|
|
784
|
+
logger.warning(f"Не удалось распарсить дату/время из события '{event_type}'")
|
|
785
|
+
# Fallback - планируем через default_delay
|
|
786
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
787
|
+
return result
|
|
788
|
+
|
|
789
|
+
event_datetime = event_data['datetime']
|
|
790
|
+
now = datetime.now()
|
|
791
|
+
|
|
792
|
+
# Вычисляем время напоминания (за default_delay до события)
|
|
793
|
+
reminder_datetime = event_datetime - timedelta(seconds=default_delay)
|
|
794
|
+
|
|
795
|
+
# Проверяем, не в прошлом ли напоминание
|
|
796
|
+
if reminder_datetime <= now:
|
|
797
|
+
logger.warning(f"Напоминание о событии '{event_type}' уже в прошлом, отправляем немедленно")
|
|
798
|
+
# Выполняем задачу немедленно
|
|
799
|
+
result = await execute_scheduled_task(task_name, user_id, user_data)
|
|
800
|
+
return {
|
|
801
|
+
"status": "executed_immediately",
|
|
802
|
+
"task_name": task_name,
|
|
803
|
+
"reason": "reminder_time_passed",
|
|
804
|
+
"event_datetime": event_datetime.isoformat(),
|
|
805
|
+
"result": result
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# Вычисляем задержку до напоминания
|
|
809
|
+
delay_seconds = int((reminder_datetime - now).total_seconds())
|
|
810
|
+
|
|
811
|
+
logger.info(f"⏰ Планируем напоминание '{task_name}' за {default_delay}с до события '{event_type}' (через {delay_seconds}с)")
|
|
812
|
+
|
|
813
|
+
# Планируем напоминание
|
|
814
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds, session_id)
|
|
815
|
+
result['event_datetime'] = event_datetime.isoformat()
|
|
816
|
+
result['reminder_type'] = 'event_reminder'
|
|
817
|
+
|
|
818
|
+
return result
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
logger.error(f"Ошибка при работе с событием '{event_type}': {e}")
|
|
822
|
+
# Fallback - планируем через default_delay
|
|
823
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
824
|
+
return result
|
|
825
|
+
else:
|
|
826
|
+
# Обычная задача с фиксированным временем
|
|
827
|
+
logger.info(f"⏰ Планируем задачу '{task_name}' через {default_delay}с с текстом: '{user_data}'")
|
|
362
828
|
|
|
363
829
|
# Планируем задачу на фоне с сохранением в БД
|
|
364
|
-
result = await schedule_task_for_later_with_db(task_name, user_id, user_data,
|
|
830
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
|
|
365
831
|
|
|
366
832
|
return result
|
|
367
|
-
|
|
368
|
-
except ValueError as e:
|
|
369
|
-
logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
|
|
370
|
-
# Fallback - планируем через 1 час с сохранением в БД
|
|
371
|
-
return await schedule_task_for_later_with_db(task_name, user_id, "Напоминание через 1 час (fallback)", 3600)
|
|
372
833
|
|
|
373
834
|
async def schedule_global_handler_for_later(handler_type: str, delay_seconds: int, handler_data: str):
|
|
374
835
|
"""
|
|
@@ -379,8 +840,19 @@ async def schedule_global_handler_for_later(handler_type: str, delay_seconds: in
|
|
|
379
840
|
delay_seconds: Задержка в секундах
|
|
380
841
|
handler_data: Данные для обработчика (время в секундах как строка)
|
|
381
842
|
"""
|
|
382
|
-
|
|
383
|
-
|
|
843
|
+
# Ищем глобальный обработчик через RouterManager (новая логика)
|
|
844
|
+
router_manager = get_router_manager()
|
|
845
|
+
if router_manager:
|
|
846
|
+
global_handlers = router_manager.get_global_handlers()
|
|
847
|
+
logger.debug(f"🔍 Поиск глобального обработчика '{handler_type}' через RouterManager")
|
|
848
|
+
else:
|
|
849
|
+
global_handlers = _global_handlers
|
|
850
|
+
logger.debug(f"🔍 Поиск глобального обработчика '{handler_type}' через глобальный реестр")
|
|
851
|
+
|
|
852
|
+
if handler_type not in global_handlers:
|
|
853
|
+
available_handlers = list(global_handlers.keys())
|
|
854
|
+
logger.error(f"❌ Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}")
|
|
855
|
+
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}")
|
|
384
856
|
|
|
385
857
|
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
|
|
386
858
|
|
|
@@ -405,23 +877,33 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
|
|
|
405
877
|
|
|
406
878
|
Args:
|
|
407
879
|
handler_type: Тип глобального обработчика
|
|
408
|
-
event_info: Информация от ИИ (
|
|
880
|
+
event_info: Информация от ИИ (только текст, время задается в декораторе)
|
|
409
881
|
"""
|
|
410
|
-
|
|
882
|
+
router_manager = get_router_manager()
|
|
883
|
+
if router_manager:
|
|
884
|
+
global_handlers = router_manager.get_global_handlers()
|
|
885
|
+
else:
|
|
886
|
+
global_handlers = _global_handlers
|
|
887
|
+
|
|
888
|
+
if handler_type not in global_handlers:
|
|
411
889
|
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
412
890
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
891
|
+
handler_info = global_handlers[handler_type]
|
|
892
|
+
default_delay = handler_info.get('default_delay')
|
|
893
|
+
|
|
894
|
+
# Время всегда берется из декоратора, ИИ может передавать только текст
|
|
895
|
+
if default_delay is None:
|
|
896
|
+
raise ValueError(f"Для глобального обработчика '{handler_type}' не указано время в декораторе (параметр delay)")
|
|
897
|
+
|
|
898
|
+
# event_info содержит только текст для обработчика
|
|
899
|
+
handler_data = event_info.strip() if event_info else f"Глобальное событие через {default_delay} секунд"
|
|
900
|
+
|
|
901
|
+
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {default_delay}с с данными: '{handler_data}'")
|
|
902
|
+
|
|
903
|
+
# Планируем обработчик на фоне с сохранением в БД
|
|
904
|
+
result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
|
|
905
|
+
|
|
906
|
+
return result
|
|
425
907
|
|
|
426
908
|
|
|
427
909
|
# =============================================================================
|
|
@@ -703,7 +1185,19 @@ async def process_scheduled_event(event: Dict):
|
|
|
703
1185
|
async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_data: str, delay_seconds: int, session_id: str = None):
|
|
704
1186
|
"""Планирует выполнение задачи через указанное время с сохранением в БД"""
|
|
705
1187
|
|
|
706
|
-
|
|
1188
|
+
# Проверяем через RouterManager или fallback к старым декораторам
|
|
1189
|
+
router_manager = get_router_manager()
|
|
1190
|
+
if router_manager:
|
|
1191
|
+
scheduled_tasks = router_manager.get_scheduled_tasks()
|
|
1192
|
+
else:
|
|
1193
|
+
scheduled_tasks = _scheduled_tasks
|
|
1194
|
+
|
|
1195
|
+
if task_name not in scheduled_tasks:
|
|
1196
|
+
import inspect
|
|
1197
|
+
frame = inspect.currentframe()
|
|
1198
|
+
line_no = frame.f_lineno if frame else "unknown"
|
|
1199
|
+
available_tasks = list(scheduled_tasks.keys())
|
|
1200
|
+
logger.error(f"❌ [decorators.py:{line_no}] Задача '{task_name}' не найдена. Доступные: {available_tasks}")
|
|
707
1201
|
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
708
1202
|
|
|
709
1203
|
logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
|
|
@@ -714,8 +1208,8 @@ async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_dat
|
|
|
714
1208
|
async def delayed_task():
|
|
715
1209
|
await asyncio.sleep(delay_seconds)
|
|
716
1210
|
|
|
717
|
-
#
|
|
718
|
-
task_info =
|
|
1211
|
+
# Получаем информацию о задаче
|
|
1212
|
+
task_info = scheduled_tasks.get(task_name, {})
|
|
719
1213
|
use_smart_check = task_info.get('smart_check', True)
|
|
720
1214
|
|
|
721
1215
|
if use_smart_check:
|
|
@@ -851,26 +1345,31 @@ async def smart_execute_check(event_id: str, user_id: int, session_id: str, task
|
|
|
851
1345
|
time_since_last_message = (now - last_message_at).total_seconds()
|
|
852
1346
|
planned_delay = (scheduled_at - created_at).total_seconds()
|
|
853
1347
|
|
|
1348
|
+
# Проверяем, писал ли пользователь после создания события
|
|
1349
|
+
time_between_creation_and_last_message = (last_message_at - created_at).total_seconds()
|
|
1350
|
+
|
|
854
1351
|
logger.info(f"🔄 Анализ для пользователя {user_id}:")
|
|
855
1352
|
logger.info(f" Время с создания события: {time_since_creation:.0f}с")
|
|
856
1353
|
logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
|
|
857
1354
|
logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
|
|
1355
|
+
logger.info(f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}")
|
|
858
1356
|
|
|
859
|
-
# Если
|
|
860
|
-
|
|
1357
|
+
# Если пользователь писал ПОСЛЕ создания события (недавно активен)
|
|
1358
|
+
# И с момента его последнего сообщения прошло меньше planned_delay
|
|
1359
|
+
if time_between_creation_and_last_message > 0 and time_since_last_message < planned_delay:
|
|
861
1360
|
# Пересчитываем время - отправляем через planned_delay после последнего сообщения
|
|
862
1361
|
new_delay = max(0, planned_delay - time_since_last_message)
|
|
863
|
-
logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (через {planned_delay:.0f}с после последнего сообщения)")
|
|
1362
|
+
logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)")
|
|
864
1363
|
return {
|
|
865
1364
|
"action": "reschedule",
|
|
866
1365
|
"new_delay": new_delay,
|
|
867
|
-
"reason": f"
|
|
1366
|
+
"reason": f"user_active_after_event_creation_{new_delay:.0f}s_delay"
|
|
868
1367
|
}
|
|
869
1368
|
|
|
870
|
-
# Если прошло достаточно времени - выполняем
|
|
871
|
-
if
|
|
872
|
-
logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id}")
|
|
873
|
-
return {"action": "execute", "reason": "
|
|
1369
|
+
# Если прошло достаточно времени с последнего сообщения - выполняем
|
|
1370
|
+
if time_since_last_message >= planned_delay:
|
|
1371
|
+
logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id} (прошло {time_since_last_message:.0f}с с последнего сообщения)")
|
|
1372
|
+
return {"action": "execute", "reason": "time_expired_since_last_message"}
|
|
874
1373
|
|
|
875
1374
|
# Если что-то пошло не так - выполняем
|
|
876
1375
|
logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")
|