smart-bot-factory 0.1.3__py3-none-any.whl → 0.1.5__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/__init__.py +0 -48
- smart_bot_factory/admin/admin_logic.py +11 -11
- smart_bot_factory/cli.py +299 -106
- smart_bot_factory/clients/__init__.py +33 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
- smart_bot_factory/core/__init__.py +43 -22
- smart_bot_factory/core/bot_utils.py +268 -95
- smart_bot_factory/core/conversation_manager.py +542 -535
- smart_bot_factory/core/decorators.py +943 -229
- smart_bot_factory/core/globals.py +68 -0
- smart_bot_factory/core/message_sender.py +6 -6
- smart_bot_factory/core/router.py +172 -0
- smart_bot_factory/core/router_manager.py +165 -0
- smart_bot_factory/creation/__init__.py +1 -2
- smart_bot_factory/creation/bot_builder.py +116 -8
- smart_bot_factory/creation/bot_testing.py +74 -13
- smart_bot_factory/handlers/handlers.py +10 -2
- smart_bot_factory/integrations/__init__.py +1 -0
- smart_bot_factory/integrations/supabase_client.py +272 -2
- smart_bot_factory/utm_link_generator.py +106 -0
- smart_bot_factory-0.1.5.dist-info/METADATA +466 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +26 -31
- smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
- smart_bot_factory/configs/growthmed-helper/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/uv.lock +0 -2004
- smart_bot_factory-0.1.3.dist-info/METADATA +0 -126
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,229 +1,943 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Декораторы для обработчиков событий и временных задач
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import logging
|
|
7
|
-
from typing import Callable, Any, Dict
|
|
8
|
-
from datetime import datetime, timedelta
|
|
9
|
-
from functools import wraps
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
# Глобальный реестр обработчиков событий
|
|
14
|
-
_event_handlers: Dict[str, Callable] = {}
|
|
15
|
-
_scheduled_tasks: Dict[str, Dict[str, Any]] = {}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
1
|
+
"""
|
|
2
|
+
Декораторы для обработчиков событий и временных задач
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Callable, Any, Dict
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from functools import wraps
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Глобальный реестр обработчиков событий
|
|
14
|
+
_event_handlers: Dict[str, Callable] = {}
|
|
15
|
+
_scheduled_tasks: Dict[str, Dict[str, Any]] = {}
|
|
16
|
+
_global_handlers: Dict[str, Dict[str, Any]] = {}
|
|
17
|
+
|
|
18
|
+
# Глобальный менеджер роутеров
|
|
19
|
+
_router_manager = None
|
|
20
|
+
|
|
21
|
+
def event_handler(event_type: str, notify: bool = False, once_only: bool = True):
|
|
22
|
+
"""
|
|
23
|
+
Декоратор для регистрации обработчика события
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
event_type: Тип события (например, 'appointment_booking', 'phone_collection')
|
|
27
|
+
notify: Уведомлять ли админов о выполнении события (по умолчанию False)
|
|
28
|
+
once_only: Обрабатывать ли событие только один раз (по умолчанию True)
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
# Обработчик только один раз (по умолчанию)
|
|
32
|
+
@event_handler("appointment_booking", notify=True)
|
|
33
|
+
async def book_appointment(user_id: int, appointment_data: dict):
|
|
34
|
+
# Логика записи на прием
|
|
35
|
+
return {"status": "success", "appointment_id": "123"}
|
|
36
|
+
|
|
37
|
+
# Обработчик может выполняться многократно
|
|
38
|
+
@event_handler("phone_collection", once_only=False)
|
|
39
|
+
async def collect_phone(user_id: int, phone_data: dict):
|
|
40
|
+
# Логика сбора телефона
|
|
41
|
+
return {"status": "phone_collected"}
|
|
42
|
+
"""
|
|
43
|
+
def decorator(func: Callable) -> Callable:
|
|
44
|
+
_event_handlers[event_type] = {
|
|
45
|
+
'handler': func,
|
|
46
|
+
'name': func.__name__,
|
|
47
|
+
'notify': notify,
|
|
48
|
+
'once_only': once_only
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
|
|
52
|
+
|
|
53
|
+
@wraps(func)
|
|
54
|
+
async def wrapper(*args, **kwargs):
|
|
55
|
+
try:
|
|
56
|
+
logger.info(f"🔧 Выполняем обработчик события '{event_type}'")
|
|
57
|
+
result = await func(*args, **kwargs)
|
|
58
|
+
logger.info(f"✅ Обработчик '{event_type}' выполнен успешно")
|
|
59
|
+
|
|
60
|
+
# Автоматически добавляем флаг notify к результату
|
|
61
|
+
if isinstance(result, dict):
|
|
62
|
+
result['notify'] = notify
|
|
63
|
+
else:
|
|
64
|
+
# Если результат не словарь, создаем словарь
|
|
65
|
+
result = {
|
|
66
|
+
'status': 'success',
|
|
67
|
+
'result': result,
|
|
68
|
+
'notify': notify
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"❌ Ошибка в обработчике '{event_type}': {e}")
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
return wrapper
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True):
|
|
80
|
+
"""
|
|
81
|
+
Декоратор для регистрации задачи, которую можно запланировать на время
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
task_name: Название задачи (например, 'send_reminder', 'follow_up')
|
|
85
|
+
notify: Уведомлять ли админов о выполнении задачи (по умолчанию False)
|
|
86
|
+
smart_check: Использовать ли умную проверку активности пользователя (по умолчанию True)
|
|
87
|
+
once_only: Выполнять ли задачу только один раз (по умолчанию True)
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
# С умной проверкой (по умолчанию)
|
|
91
|
+
@schedule_task("send_reminder", notify=False)
|
|
92
|
+
async def send_reminder(user_id: int, user_data: str):
|
|
93
|
+
# user_data содержит текст напоминания от ИИ
|
|
94
|
+
# Логика отправки напоминания (выполняется на фоне)
|
|
95
|
+
return {"status": "sent", "message": user_data}
|
|
96
|
+
|
|
97
|
+
# Без умной проверки (всегда выполняется по времени)
|
|
98
|
+
@schedule_task("system_notification", smart_check=False)
|
|
99
|
+
async def system_notification(user_id: int, user_data: str):
|
|
100
|
+
# Выполняется точно по времени, без проверки активности
|
|
101
|
+
return {"status": "sent", "message": user_data}
|
|
102
|
+
|
|
103
|
+
# Задача может выполняться многократно
|
|
104
|
+
@schedule_task("recurring_reminder", once_only=False)
|
|
105
|
+
async def recurring_reminder(user_id: int, user_data: str):
|
|
106
|
+
# Может запускаться несколько раз
|
|
107
|
+
return {"status": "sent", "message": user_data}
|
|
108
|
+
|
|
109
|
+
# ИИ может отправлять события в форматах:
|
|
110
|
+
# {"тип": "send_reminder", "инфо": "3600"} - просто время
|
|
111
|
+
# {"тип": "send_reminder", "инфо": "3600|Не забудьте про встречу!"} - время + текст
|
|
112
|
+
"""
|
|
113
|
+
def decorator(func: Callable) -> Callable:
|
|
114
|
+
_scheduled_tasks[task_name] = {
|
|
115
|
+
'handler': func,
|
|
116
|
+
'name': func.__name__,
|
|
117
|
+
'notify': notify,
|
|
118
|
+
'smart_check': smart_check,
|
|
119
|
+
'once_only': once_only
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
|
|
123
|
+
|
|
124
|
+
@wraps(func)
|
|
125
|
+
async def wrapper(*args, **kwargs):
|
|
126
|
+
try:
|
|
127
|
+
logger.info(f"⏰ Выполняем запланированную задачу '{task_name}'")
|
|
128
|
+
result = await func(*args, **kwargs)
|
|
129
|
+
logger.info(f"✅ Задача '{task_name}' выполнена успешно")
|
|
130
|
+
|
|
131
|
+
# Автоматически добавляем флаг notify к результату
|
|
132
|
+
if isinstance(result, dict):
|
|
133
|
+
result['notify'] = notify
|
|
134
|
+
else:
|
|
135
|
+
# Если результат не словарь, создаем словарь
|
|
136
|
+
result = {
|
|
137
|
+
'status': 'success',
|
|
138
|
+
'result': result,
|
|
139
|
+
'notify': notify
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"❌ Ошибка в задаче '{task_name}': {e}")
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
return wrapper
|
|
148
|
+
return decorator
|
|
149
|
+
|
|
150
|
+
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True):
|
|
151
|
+
"""
|
|
152
|
+
Декоратор для регистрации глобального обработчика (для всех пользователей)
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
handler_type: Тип глобального обработчика (например, 'global_announcement', 'mass_notification')
|
|
156
|
+
notify: Уведомлять ли админов о выполнении (по умолчанию False)
|
|
157
|
+
once_only: Выполнять ли обработчик только один раз (по умолчанию True)
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
# Глобальный обработчик только один раз (по умолчанию)
|
|
161
|
+
@global_handler("global_announcement", notify=True)
|
|
162
|
+
async def send_global_announcement(announcement_text: str):
|
|
163
|
+
# Логика отправки анонса всем пользователям
|
|
164
|
+
return {"status": "sent", "recipients_count": 150}
|
|
165
|
+
|
|
166
|
+
# Глобальный обработчик может выполняться многократно
|
|
167
|
+
@global_handler("daily_report", once_only=False)
|
|
168
|
+
async def send_daily_report(report_data: str):
|
|
169
|
+
# Может запускаться каждый день
|
|
170
|
+
return {"status": "sent", "report_type": "daily"}
|
|
171
|
+
"""
|
|
172
|
+
def decorator(func: Callable) -> Callable:
|
|
173
|
+
_global_handlers[handler_type] = {
|
|
174
|
+
'handler': func,
|
|
175
|
+
'name': func.__name__,
|
|
176
|
+
'notify': notify,
|
|
177
|
+
'once_only': once_only
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
|
|
181
|
+
|
|
182
|
+
@wraps(func)
|
|
183
|
+
async def wrapper(*args, **kwargs):
|
|
184
|
+
try:
|
|
185
|
+
logger.info(f"🌍 Выполняем глобальный обработчик '{handler_type}'")
|
|
186
|
+
result = await func(*args, **kwargs)
|
|
187
|
+
logger.info(f"✅ Глобальный обработчик '{handler_type}' выполнен успешно")
|
|
188
|
+
|
|
189
|
+
# Автоматически добавляем флаг notify к результату
|
|
190
|
+
if isinstance(result, dict):
|
|
191
|
+
result['notify'] = notify
|
|
192
|
+
else:
|
|
193
|
+
# Если результат не словарь, создаем словарь
|
|
194
|
+
result = {
|
|
195
|
+
'status': 'success',
|
|
196
|
+
'result': result,
|
|
197
|
+
'notify': notify
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"❌ Ошибка в глобальном обработчике '{handler_type}': {e}")
|
|
203
|
+
raise
|
|
204
|
+
|
|
205
|
+
return wrapper
|
|
206
|
+
return decorator
|
|
207
|
+
|
|
208
|
+
def get_event_handlers() -> Dict[str, Dict[str, Any]]:
|
|
209
|
+
"""Возвращает все зарегистрированные обработчики событий"""
|
|
210
|
+
return _event_handlers.copy()
|
|
211
|
+
|
|
212
|
+
def get_scheduled_tasks() -> Dict[str, Dict[str, Any]]:
|
|
213
|
+
"""Возвращает все зарегистрированные задачи"""
|
|
214
|
+
return _scheduled_tasks.copy()
|
|
215
|
+
|
|
216
|
+
def get_global_handlers() -> Dict[str, Dict[str, Any]]:
|
|
217
|
+
"""Возвращает все зарегистрированные глобальные обработчики"""
|
|
218
|
+
return _global_handlers.copy()
|
|
219
|
+
|
|
220
|
+
def set_router_manager(router_manager):
|
|
221
|
+
"""Устанавливает глобальный менеджер роутеров"""
|
|
222
|
+
global _router_manager
|
|
223
|
+
_router_manager = router_manager
|
|
224
|
+
logger.info("🔄 RouterManager установлен в decorators")
|
|
225
|
+
|
|
226
|
+
def get_router_manager():
|
|
227
|
+
"""Получает глобальный менеджер роутеров"""
|
|
228
|
+
return _router_manager
|
|
229
|
+
|
|
230
|
+
def get_handlers_for_prompt() -> str:
|
|
231
|
+
"""
|
|
232
|
+
Возвращает описание всех обработчиков для добавления в промпт
|
|
233
|
+
"""
|
|
234
|
+
# Сначала пробуем получить из роутеров
|
|
235
|
+
if _router_manager:
|
|
236
|
+
return _router_manager.get_handlers_for_prompt()
|
|
237
|
+
|
|
238
|
+
# Fallback к старым декораторам
|
|
239
|
+
if not _event_handlers and not _scheduled_tasks and not _global_handlers:
|
|
240
|
+
return ""
|
|
241
|
+
|
|
242
|
+
prompt_parts = []
|
|
243
|
+
|
|
244
|
+
if _event_handlers:
|
|
245
|
+
prompt_parts.append("ДОСТУПНЫЕ ОБРАБОТЧИКИ СОБЫТИЙ:")
|
|
246
|
+
for event_type, handler_info in _event_handlers.items():
|
|
247
|
+
prompt_parts.append(f"- {event_type}: {handler_info['name']}")
|
|
248
|
+
|
|
249
|
+
if _scheduled_tasks:
|
|
250
|
+
prompt_parts.append("\nДОСТУПНЫЕ ЗАДАЧИ ДЛЯ ПЛАНИРОВАНИЯ:")
|
|
251
|
+
for task_name, task_info in _scheduled_tasks.items():
|
|
252
|
+
prompt_parts.append(f"- {task_name}: {task_info['name']}")
|
|
253
|
+
|
|
254
|
+
if _global_handlers:
|
|
255
|
+
prompt_parts.append("\nДОСТУПНЫЕ ГЛОБАЛЬНЫЕ ОБРАБОТЧИКИ:")
|
|
256
|
+
for handler_type, handler_info in _global_handlers.items():
|
|
257
|
+
prompt_parts.append(f"- {handler_type}: {handler_info['name']}")
|
|
258
|
+
|
|
259
|
+
return "\n".join(prompt_parts)
|
|
260
|
+
|
|
261
|
+
async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
|
|
262
|
+
"""Выполняет обработчик события по типу"""
|
|
263
|
+
# Сначала пробуем получить из роутеров
|
|
264
|
+
if _router_manager:
|
|
265
|
+
event_handlers = _router_manager.get_event_handlers()
|
|
266
|
+
if event_type in event_handlers:
|
|
267
|
+
handler_info = event_handlers[event_type]
|
|
268
|
+
return await handler_info['handler'](*args, **kwargs)
|
|
269
|
+
|
|
270
|
+
# Fallback к старым декораторам
|
|
271
|
+
if event_type not in _event_handlers:
|
|
272
|
+
raise ValueError(f"Обработчик события '{event_type}' не найден")
|
|
273
|
+
|
|
274
|
+
handler_info = _event_handlers[event_type]
|
|
275
|
+
return await handler_info['handler'](*args, **kwargs)
|
|
276
|
+
|
|
277
|
+
async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -> Any:
|
|
278
|
+
"""Выполняет запланированную задачу по имени"""
|
|
279
|
+
# Сначала пробуем получить из роутеров
|
|
280
|
+
if _router_manager:
|
|
281
|
+
scheduled_tasks = _router_manager.get_scheduled_tasks()
|
|
282
|
+
if task_name in scheduled_tasks:
|
|
283
|
+
task_info = scheduled_tasks[task_name]
|
|
284
|
+
return await task_info['handler'](user_id, user_data)
|
|
285
|
+
|
|
286
|
+
# Fallback к старым декораторам
|
|
287
|
+
if task_name not in _scheduled_tasks:
|
|
288
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
289
|
+
|
|
290
|
+
task_info = _scheduled_tasks[task_name]
|
|
291
|
+
return await task_info['handler'](user_id, user_data)
|
|
292
|
+
|
|
293
|
+
async def execute_global_handler(handler_type: str, *args, **kwargs) -> Any:
|
|
294
|
+
"""Выполняет глобальный обработчик по типу"""
|
|
295
|
+
# Сначала пробуем получить из роутеров
|
|
296
|
+
if _router_manager:
|
|
297
|
+
global_handlers = _router_manager.get_global_handlers()
|
|
298
|
+
if handler_type in global_handlers:
|
|
299
|
+
handler_info = global_handlers[handler_type]
|
|
300
|
+
return await handler_info['handler'](*args, **kwargs)
|
|
301
|
+
|
|
302
|
+
# Fallback к старым декораторам
|
|
303
|
+
if handler_type not in _global_handlers:
|
|
304
|
+
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
305
|
+
|
|
306
|
+
handler_info = _global_handlers[handler_type]
|
|
307
|
+
return await handler_info['handler'](*args, **kwargs)
|
|
308
|
+
|
|
309
|
+
async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: int, user_data: str):
|
|
310
|
+
"""
|
|
311
|
+
Планирует выполнение задачи через указанное время
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
task_name: Название задачи
|
|
315
|
+
delay_seconds: Задержка в секундах
|
|
316
|
+
user_id: ID пользователя
|
|
317
|
+
user_data: Простой текст для задачи
|
|
318
|
+
"""
|
|
319
|
+
if task_name not in _scheduled_tasks:
|
|
320
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
321
|
+
|
|
322
|
+
logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
|
|
323
|
+
|
|
324
|
+
async def delayed_task():
|
|
325
|
+
await asyncio.sleep(delay_seconds)
|
|
326
|
+
await execute_scheduled_task(task_name, user_id, user_data)
|
|
327
|
+
|
|
328
|
+
# Запускаем задачу в фоне
|
|
329
|
+
asyncio.create_task(delayed_task())
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
"status": "scheduled",
|
|
333
|
+
"task_name": task_name,
|
|
334
|
+
"delay_seconds": delay_seconds,
|
|
335
|
+
"scheduled_at": datetime.now().isoformat()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str):
|
|
339
|
+
"""
|
|
340
|
+
Выполняет запланированную задачу на основе события от ИИ
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
user_id: ID пользователя
|
|
344
|
+
task_name: Название задачи
|
|
345
|
+
event_info: Информация от ИИ (простой текст)
|
|
346
|
+
"""
|
|
347
|
+
if task_name not in _scheduled_tasks:
|
|
348
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
# ИИ может присылать время в двух форматах:
|
|
352
|
+
# 1. Просто время: "3600"
|
|
353
|
+
# 2. Время с данными: "3600|Текст напоминания"
|
|
354
|
+
|
|
355
|
+
if '|' in event_info:
|
|
356
|
+
# Формат с дополнительными данными
|
|
357
|
+
delay_seconds_str, user_data = event_info.split('|', 1)
|
|
358
|
+
delay_seconds = int(delay_seconds_str.strip())
|
|
359
|
+
user_data = user_data.strip()
|
|
360
|
+
else:
|
|
361
|
+
# Просто время
|
|
362
|
+
delay_seconds = int(event_info)
|
|
363
|
+
user_data = f"Напоминание через {delay_seconds} секунд"
|
|
364
|
+
|
|
365
|
+
# Планируем задачу на фоне с сохранением в БД
|
|
366
|
+
result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds)
|
|
367
|
+
|
|
368
|
+
return result
|
|
369
|
+
|
|
370
|
+
except ValueError as e:
|
|
371
|
+
logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
|
|
372
|
+
# Fallback - планируем через 1 час с сохранением в БД
|
|
373
|
+
return await schedule_task_for_later_with_db(task_name, user_id, "Напоминание через 1 час (fallback)", 3600)
|
|
374
|
+
|
|
375
|
+
async def schedule_global_handler_for_later(handler_type: str, delay_seconds: int, handler_data: str):
|
|
376
|
+
"""
|
|
377
|
+
Планирует выполнение глобального обработчика через указанное время
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
handler_type: Тип глобального обработчика
|
|
381
|
+
delay_seconds: Задержка в секундах
|
|
382
|
+
handler_data: Данные для обработчика (время в секундах как строка)
|
|
383
|
+
"""
|
|
384
|
+
if handler_type not in _global_handlers:
|
|
385
|
+
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
386
|
+
|
|
387
|
+
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
|
|
388
|
+
|
|
389
|
+
async def delayed_global_handler():
|
|
390
|
+
await asyncio.sleep(delay_seconds)
|
|
391
|
+
# Передаем данные обработчику (может быть текст анонса или другие данные)
|
|
392
|
+
await execute_global_handler(handler_type, handler_data)
|
|
393
|
+
|
|
394
|
+
# Запускаем задачу в фоне
|
|
395
|
+
asyncio.create_task(delayed_global_handler())
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
"status": "scheduled",
|
|
399
|
+
"handler_type": handler_type,
|
|
400
|
+
"delay_seconds": delay_seconds,
|
|
401
|
+
"scheduled_at": datetime.now().isoformat()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async def execute_global_handler_from_event(handler_type: str, event_info: str):
|
|
405
|
+
"""
|
|
406
|
+
Выполняет глобальный обработчик на основе события от ИИ
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
handler_type: Тип глобального обработчика
|
|
410
|
+
event_info: Информация от ИИ (содержит данные для обработчика и время)
|
|
411
|
+
"""
|
|
412
|
+
if handler_type not in _global_handlers:
|
|
413
|
+
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
# ИИ присылает время в секундах, парсим его
|
|
417
|
+
delay_seconds = int(event_info)
|
|
418
|
+
|
|
419
|
+
# Планируем на будущее с сохранением в БД
|
|
420
|
+
result = await schedule_global_handler_for_later_with_db(handler_type, delay_seconds, event_info)
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
except ValueError as e:
|
|
424
|
+
logger.error(f"Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}")
|
|
425
|
+
# Fallback - планируем через 1 час с сохранением в БД
|
|
426
|
+
return await schedule_global_handler_for_later_with_db(handler_type, 3600, event_info)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# =============================================================================
|
|
430
|
+
# ФУНКЦИИ ДЛЯ РАБОТЫ С БД СОБЫТИЙ
|
|
431
|
+
# =============================================================================
|
|
432
|
+
|
|
433
|
+
def get_supabase_client():
|
|
434
|
+
"""Получает клиент Supabase из глобальных переменных"""
|
|
435
|
+
import sys
|
|
436
|
+
current_module = sys.modules[__name__]
|
|
437
|
+
supabase_client = getattr(current_module, 'supabase_client', None)
|
|
438
|
+
|
|
439
|
+
# Если не найден в decorators, пробуем получить из bot_utils
|
|
440
|
+
if not supabase_client:
|
|
441
|
+
try:
|
|
442
|
+
bot_utils_module = sys.modules.get('smart_bot_factory.core.bot_utils')
|
|
443
|
+
if bot_utils_module:
|
|
444
|
+
supabase_client = getattr(bot_utils_module, 'supabase_client', None)
|
|
445
|
+
except:
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
return supabase_client
|
|
449
|
+
|
|
450
|
+
async def save_immediate_event(
|
|
451
|
+
event_type: str,
|
|
452
|
+
user_id: int,
|
|
453
|
+
event_data: str,
|
|
454
|
+
session_id: str = None
|
|
455
|
+
) -> str:
|
|
456
|
+
"""Сохраняет событие для немедленного выполнения"""
|
|
457
|
+
|
|
458
|
+
supabase_client = get_supabase_client()
|
|
459
|
+
if not supabase_client:
|
|
460
|
+
logger.error("❌ Supabase клиент не найден")
|
|
461
|
+
raise RuntimeError("Supabase клиент не инициализирован")
|
|
462
|
+
|
|
463
|
+
# Проверяем, нужно ли предотвращать дублирование
|
|
464
|
+
# Получаем информацию об обработчике через роутер-менеджер или fallback
|
|
465
|
+
if _router_manager:
|
|
466
|
+
event_handlers = _router_manager.get_event_handlers()
|
|
467
|
+
event_handler_info = event_handlers.get(event_type, {})
|
|
468
|
+
else:
|
|
469
|
+
event_handler_info = _event_handlers.get(event_type, {})
|
|
470
|
+
once_only = event_handler_info.get('once_only', True)
|
|
471
|
+
|
|
472
|
+
if once_only:
|
|
473
|
+
# Проверяем, было ли уже обработано аналогичное событие
|
|
474
|
+
already_processed = await check_event_already_processed(event_type, user_id, session_id)
|
|
475
|
+
if already_processed:
|
|
476
|
+
logger.info(f"🔄 Событие '{event_type}' уже обрабатывалось для пользователя {user_id}, пропускаем")
|
|
477
|
+
raise ValueError(f"Событие '{event_type}' уже обрабатывалось (once_only=True)")
|
|
478
|
+
|
|
479
|
+
event_record = {
|
|
480
|
+
'event_type': event_type,
|
|
481
|
+
'event_category': 'user_event',
|
|
482
|
+
'user_id': user_id,
|
|
483
|
+
'event_data': event_data,
|
|
484
|
+
'scheduled_at': None, # Немедленное выполнение
|
|
485
|
+
'status': 'immediate',
|
|
486
|
+
'session_id': session_id
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
491
|
+
event_id = response.data[0]['id']
|
|
492
|
+
logger.info(f"💾 Событие сохранено в БД: {event_id}")
|
|
493
|
+
return event_id
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.error(f"❌ Ошибка сохранения события в БД: {e}")
|
|
496
|
+
raise
|
|
497
|
+
|
|
498
|
+
async def save_scheduled_task(
|
|
499
|
+
task_name: str,
|
|
500
|
+
user_id: int,
|
|
501
|
+
user_data: str,
|
|
502
|
+
delay_seconds: int,
|
|
503
|
+
session_id: str = None
|
|
504
|
+
) -> str:
|
|
505
|
+
"""Сохраняет запланированную задачу"""
|
|
506
|
+
|
|
507
|
+
supabase_client = get_supabase_client()
|
|
508
|
+
if not supabase_client:
|
|
509
|
+
logger.error("❌ Supabase клиент не найден")
|
|
510
|
+
raise RuntimeError("Supabase клиент не инициализирован")
|
|
511
|
+
|
|
512
|
+
# Проверяем, нужно ли предотвращать дублирование
|
|
513
|
+
# Получаем информацию о задаче через роутер-менеджер или fallback
|
|
514
|
+
if _router_manager:
|
|
515
|
+
scheduled_tasks = _router_manager.get_scheduled_tasks()
|
|
516
|
+
task_info = scheduled_tasks.get(task_name, {})
|
|
517
|
+
else:
|
|
518
|
+
task_info = _scheduled_tasks.get(task_name, {})
|
|
519
|
+
once_only = task_info.get('once_only', True)
|
|
520
|
+
|
|
521
|
+
if once_only:
|
|
522
|
+
# Проверяем, была ли уже запланирована аналогичная задача
|
|
523
|
+
already_processed = await check_event_already_processed(task_name, user_id, session_id)
|
|
524
|
+
if already_processed:
|
|
525
|
+
logger.info(f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем")
|
|
526
|
+
raise ValueError(f"Задача '{task_name}' уже запланирована (once_only=True)")
|
|
527
|
+
|
|
528
|
+
scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
|
|
529
|
+
|
|
530
|
+
event_record = {
|
|
531
|
+
'event_type': task_name,
|
|
532
|
+
'event_category': 'scheduled_task',
|
|
533
|
+
'user_id': user_id,
|
|
534
|
+
'event_data': user_data,
|
|
535
|
+
'scheduled_at': scheduled_at.isoformat(),
|
|
536
|
+
'status': 'pending',
|
|
537
|
+
'session_id': session_id
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
542
|
+
event_id = response.data[0]['id']
|
|
543
|
+
logger.info(f"⏰ Запланированная задача сохранена в БД: {event_id} (через {delay_seconds}с)")
|
|
544
|
+
return event_id
|
|
545
|
+
except Exception as e:
|
|
546
|
+
logger.error(f"❌ Ошибка сохранения запланированной задачи в БД: {e}")
|
|
547
|
+
raise
|
|
548
|
+
|
|
549
|
+
async def save_global_event(
|
|
550
|
+
handler_type: str,
|
|
551
|
+
handler_data: str,
|
|
552
|
+
delay_seconds: int = 0
|
|
553
|
+
) -> str:
|
|
554
|
+
"""Сохраняет глобальное событие"""
|
|
555
|
+
|
|
556
|
+
supabase_client = get_supabase_client()
|
|
557
|
+
if not supabase_client:
|
|
558
|
+
logger.error("❌ Supabase клиент не найден")
|
|
559
|
+
raise RuntimeError("Supabase клиент не инициализирован")
|
|
560
|
+
|
|
561
|
+
# Проверяем, нужно ли предотвращать дублирование
|
|
562
|
+
# Получаем информацию о глобальном обработчике через роутер-менеджер или fallback
|
|
563
|
+
if _router_manager:
|
|
564
|
+
global_handlers = _router_manager.get_global_handlers()
|
|
565
|
+
handler_info = global_handlers.get(handler_type, {})
|
|
566
|
+
else:
|
|
567
|
+
handler_info = _global_handlers.get(handler_type, {})
|
|
568
|
+
once_only = handler_info.get('once_only', True)
|
|
569
|
+
|
|
570
|
+
if once_only:
|
|
571
|
+
# Проверяем, было ли уже запланировано аналогичное глобальное событие
|
|
572
|
+
already_processed = await check_event_already_processed(handler_type, user_id=None)
|
|
573
|
+
if already_processed:
|
|
574
|
+
logger.info(f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем")
|
|
575
|
+
raise ValueError(f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)")
|
|
576
|
+
|
|
577
|
+
scheduled_at = None
|
|
578
|
+
status = 'immediate'
|
|
579
|
+
|
|
580
|
+
if delay_seconds > 0:
|
|
581
|
+
scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
|
|
582
|
+
status = 'pending'
|
|
583
|
+
|
|
584
|
+
event_record = {
|
|
585
|
+
'event_type': handler_type,
|
|
586
|
+
'event_category': 'global_handler',
|
|
587
|
+
'user_id': None, # Глобальное событие
|
|
588
|
+
'event_data': handler_data,
|
|
589
|
+
'scheduled_at': scheduled_at.isoformat() if scheduled_at else None,
|
|
590
|
+
'status': status
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
595
|
+
event_id = response.data[0]['id']
|
|
596
|
+
logger.info(f"🌍 Глобальное событие сохранено в БД: {event_id}")
|
|
597
|
+
return event_id
|
|
598
|
+
except Exception as e:
|
|
599
|
+
logger.error(f"❌ Ошибка сохранения глобального события в БД: {e}")
|
|
600
|
+
raise
|
|
601
|
+
|
|
602
|
+
async def update_event_result(
|
|
603
|
+
event_id: str,
|
|
604
|
+
status: str,
|
|
605
|
+
result_data: Any = None,
|
|
606
|
+
error_message: str = None
|
|
607
|
+
):
|
|
608
|
+
"""Обновляет результат выполнения события"""
|
|
609
|
+
|
|
610
|
+
supabase_client = get_supabase_client()
|
|
611
|
+
if not supabase_client:
|
|
612
|
+
logger.error("❌ Supabase клиент не найден")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
update_data = {
|
|
616
|
+
'status': status,
|
|
617
|
+
'executed_at': datetime.now(timezone.utc).isoformat()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if result_data:
|
|
621
|
+
import json
|
|
622
|
+
update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
|
|
623
|
+
|
|
624
|
+
if error_message:
|
|
625
|
+
update_data['last_error'] = error_message
|
|
626
|
+
# Получаем текущее количество попыток
|
|
627
|
+
try:
|
|
628
|
+
current_retry = supabase_client.client.table('scheduled_events').select('retry_count').eq('id', event_id).execute().data[0]['retry_count']
|
|
629
|
+
update_data['retry_count'] = current_retry + 1
|
|
630
|
+
except:
|
|
631
|
+
update_data['retry_count'] = 1
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
supabase_client.client.table('scheduled_events').update(update_data).eq('id', event_id).execute()
|
|
635
|
+
logger.info(f"📝 Результат события {event_id} обновлен: {status}")
|
|
636
|
+
except Exception as e:
|
|
637
|
+
logger.error(f"❌ Ошибка обновления результата события {event_id}: {e}")
|
|
638
|
+
|
|
639
|
+
async def get_pending_events(limit: int = 50) -> list:
|
|
640
|
+
"""Получает события готовые к выполнению"""
|
|
641
|
+
|
|
642
|
+
supabase_client = get_supabase_client()
|
|
643
|
+
if not supabase_client:
|
|
644
|
+
logger.error("❌ Supabase клиент не найден")
|
|
645
|
+
return []
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
649
|
+
|
|
650
|
+
response = supabase_client.client.table('scheduled_events')\
|
|
651
|
+
.select('*')\
|
|
652
|
+
.in_('status', ['pending', 'immediate'])\
|
|
653
|
+
.or_(f'scheduled_at.is.null,scheduled_at.lte.{now}')\
|
|
654
|
+
.order('created_at')\
|
|
655
|
+
.limit(limit)\
|
|
656
|
+
.execute()
|
|
657
|
+
|
|
658
|
+
return response.data
|
|
659
|
+
except Exception as e:
|
|
660
|
+
logger.error(f"❌ Ошибка получения событий из БД: {e}")
|
|
661
|
+
return []
|
|
662
|
+
|
|
663
|
+
async def background_event_processor():
|
|
664
|
+
"""Фоновый процессор для всех типов событий"""
|
|
665
|
+
|
|
666
|
+
logger.info("🔄 Запуск фонового процессора событий")
|
|
667
|
+
|
|
668
|
+
while True:
|
|
669
|
+
try:
|
|
670
|
+
# Получаем события готовые к выполнению
|
|
671
|
+
pending_events = await get_pending_events(limit=50)
|
|
672
|
+
|
|
673
|
+
if pending_events:
|
|
674
|
+
logger.info(f"📋 Найдено {len(pending_events)} событий для обработки")
|
|
675
|
+
|
|
676
|
+
for event in pending_events:
|
|
677
|
+
try:
|
|
678
|
+
await process_scheduled_event(event)
|
|
679
|
+
await update_event_result(event['id'], 'completed', {"processed": True})
|
|
680
|
+
|
|
681
|
+
except Exception as e:
|
|
682
|
+
logger.error(f"❌ Ошибка обработки события {event['id']}: {e}")
|
|
683
|
+
await update_event_result(event['id'], 'failed', None, str(e))
|
|
684
|
+
|
|
685
|
+
await asyncio.sleep(30)
|
|
686
|
+
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.error(f"❌ Ошибка в фоновом процессоре: {e}")
|
|
689
|
+
await asyncio.sleep(60)
|
|
690
|
+
|
|
691
|
+
async def process_scheduled_event(event: Dict):
|
|
692
|
+
"""Обрабатывает одно событие из БД"""
|
|
693
|
+
|
|
694
|
+
event_type = event['event_type']
|
|
695
|
+
event_category = event['event_category']
|
|
696
|
+
event_data = event['event_data']
|
|
697
|
+
user_id = event.get('user_id')
|
|
698
|
+
|
|
699
|
+
logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
|
|
700
|
+
|
|
701
|
+
if event_category == 'scheduled_task':
|
|
702
|
+
await execute_scheduled_task(event_type, user_id, event_data)
|
|
703
|
+
elif event_category == 'global_handler':
|
|
704
|
+
await execute_global_handler(event_type, event_data)
|
|
705
|
+
elif event_category == 'user_event':
|
|
706
|
+
await execute_event_handler(event_type, user_id, event_data)
|
|
707
|
+
else:
|
|
708
|
+
logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
|
|
709
|
+
|
|
710
|
+
# =============================================================================
|
|
711
|
+
# ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
|
|
712
|
+
# =============================================================================
|
|
713
|
+
|
|
714
|
+
async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_data: str, delay_seconds: int, session_id: str = None):
|
|
715
|
+
"""Планирует выполнение задачи через указанное время с сохранением в БД"""
|
|
716
|
+
|
|
717
|
+
# Проверяем через роутер-менеджер или fallback к старым декораторам
|
|
718
|
+
if _router_manager:
|
|
719
|
+
scheduled_tasks = _router_manager.get_scheduled_tasks()
|
|
720
|
+
if task_name not in scheduled_tasks:
|
|
721
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
722
|
+
else:
|
|
723
|
+
if task_name not in _scheduled_tasks:
|
|
724
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
725
|
+
|
|
726
|
+
logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
|
|
727
|
+
|
|
728
|
+
# Сохраняем в БД
|
|
729
|
+
event_id = await save_scheduled_task(task_name, user_id, user_data, delay_seconds, session_id)
|
|
730
|
+
|
|
731
|
+
async def delayed_task():
|
|
732
|
+
await asyncio.sleep(delay_seconds)
|
|
733
|
+
|
|
734
|
+
# Проверяем, нужна ли умная проверка
|
|
735
|
+
task_info = _scheduled_tasks.get(task_name, {})
|
|
736
|
+
use_smart_check = task_info.get('smart_check', True)
|
|
737
|
+
|
|
738
|
+
if use_smart_check:
|
|
739
|
+
# Умная проверка перед выполнением
|
|
740
|
+
try:
|
|
741
|
+
result = await smart_execute_check(event_id, user_id, session_id, task_name, user_data)
|
|
742
|
+
if result['action'] == 'execute':
|
|
743
|
+
await execute_scheduled_task(task_name, user_id, user_data)
|
|
744
|
+
await update_event_result(event_id, 'completed', {"executed": True, "reason": "scheduled_execution"})
|
|
745
|
+
elif result['action'] == 'cancel':
|
|
746
|
+
await update_event_result(event_id, 'cancelled', {"reason": result['reason']})
|
|
747
|
+
elif result['action'] == 'reschedule':
|
|
748
|
+
# Перепланируем задачу на новое время
|
|
749
|
+
new_delay = result['new_delay']
|
|
750
|
+
await update_event_result(event_id, 'rescheduled', {
|
|
751
|
+
"new_delay": new_delay,
|
|
752
|
+
"reason": result['reason']
|
|
753
|
+
})
|
|
754
|
+
# Запускаем новую задачу
|
|
755
|
+
await asyncio.sleep(new_delay)
|
|
756
|
+
await execute_scheduled_task(task_name, user_id, user_data)
|
|
757
|
+
await update_event_result(event_id, 'completed', {"executed": True, "reason": "rescheduled_execution"})
|
|
758
|
+
|
|
759
|
+
except Exception as e:
|
|
760
|
+
await update_event_result(event_id, 'failed', None, str(e))
|
|
761
|
+
raise
|
|
762
|
+
else:
|
|
763
|
+
# Простое выполнение без умной проверки
|
|
764
|
+
try:
|
|
765
|
+
await execute_scheduled_task(task_name, user_id, user_data)
|
|
766
|
+
await update_event_result(event_id, 'completed', {"executed": True, "reason": "simple_execution"})
|
|
767
|
+
except Exception as e:
|
|
768
|
+
await update_event_result(event_id, 'failed', None, str(e))
|
|
769
|
+
raise
|
|
770
|
+
|
|
771
|
+
# Запускаем задачу в фоне
|
|
772
|
+
asyncio.create_task(delayed_task())
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
"status": "scheduled",
|
|
776
|
+
"task_name": task_name,
|
|
777
|
+
"delay_seconds": delay_seconds,
|
|
778
|
+
"event_id": event_id,
|
|
779
|
+
"scheduled_at": datetime.now(timezone.utc).isoformat()
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async def schedule_global_handler_for_later_with_db(handler_type: str, delay_seconds: int, handler_data: str):
|
|
783
|
+
"""Планирует выполнение глобального обработчика через указанное время с сохранением в БД"""
|
|
784
|
+
|
|
785
|
+
# Проверяем через роутер-менеджер или fallback к старым декораторам
|
|
786
|
+
if _router_manager:
|
|
787
|
+
global_handlers = _router_manager.get_global_handlers()
|
|
788
|
+
if handler_type not in global_handlers:
|
|
789
|
+
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
790
|
+
else:
|
|
791
|
+
if handler_type not in _global_handlers:
|
|
792
|
+
raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
|
|
793
|
+
|
|
794
|
+
logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
|
|
795
|
+
|
|
796
|
+
# Сохраняем в БД
|
|
797
|
+
event_id = await save_global_event(handler_type, handler_data, delay_seconds)
|
|
798
|
+
|
|
799
|
+
async def delayed_global_handler():
|
|
800
|
+
await asyncio.sleep(delay_seconds)
|
|
801
|
+
try:
|
|
802
|
+
await execute_global_handler(handler_type, handler_data)
|
|
803
|
+
await update_event_result(event_id, 'completed', {"executed": True})
|
|
804
|
+
except Exception as e:
|
|
805
|
+
await update_event_result(event_id, 'failed', None, str(e))
|
|
806
|
+
raise
|
|
807
|
+
|
|
808
|
+
# Запускаем задачу в фоне
|
|
809
|
+
asyncio.create_task(delayed_global_handler())
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
"status": "scheduled",
|
|
813
|
+
"handler_type": handler_type,
|
|
814
|
+
"delay_seconds": delay_seconds,
|
|
815
|
+
"event_id": event_id,
|
|
816
|
+
"scheduled_at": datetime.now(timezone.utc).isoformat()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async def smart_execute_check(event_id: str, user_id: int, session_id: str, task_name: str, user_data: str) -> Dict[str, Any]:
|
|
820
|
+
"""
|
|
821
|
+
Умная проверка перед выполнением запланированной задачи
|
|
822
|
+
|
|
823
|
+
Логика:
|
|
824
|
+
1. Если пользователь перешел на новый этап - отменяем событие
|
|
825
|
+
2. Если прошло меньше времени чем планировалось - переносим на разницу
|
|
826
|
+
3. Если прошло достаточно времени - выполняем
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
Dict с action: 'execute', 'cancel', 'reschedule'
|
|
830
|
+
"""
|
|
831
|
+
supabase_client = get_supabase_client()
|
|
832
|
+
if not supabase_client:
|
|
833
|
+
logger.error("❌ Supabase клиент не найден для умной проверки")
|
|
834
|
+
return {"action": "execute", "reason": "no_supabase_client"}
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
# Получаем информацию о последнем сообщении пользователя
|
|
838
|
+
user_info = await supabase_client.get_user_last_message_info(user_id)
|
|
839
|
+
|
|
840
|
+
if not user_info:
|
|
841
|
+
logger.info(f"🔄 Пользователь {user_id} не найден, выполняем задачу")
|
|
842
|
+
return {"action": "execute", "reason": "user_not_found"}
|
|
843
|
+
|
|
844
|
+
# Проверяем, изменился ли этап
|
|
845
|
+
stage_changed = await supabase_client.check_user_stage_changed(user_id, session_id)
|
|
846
|
+
if stage_changed:
|
|
847
|
+
logger.info(f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}")
|
|
848
|
+
return {"action": "cancel", "reason": "user_stage_changed"}
|
|
849
|
+
|
|
850
|
+
# Получаем информацию о событии из БД
|
|
851
|
+
event_response = supabase_client.client.table('scheduled_events').select(
|
|
852
|
+
'created_at', 'scheduled_at'
|
|
853
|
+
).eq('id', event_id).execute()
|
|
854
|
+
|
|
855
|
+
if not event_response.data:
|
|
856
|
+
logger.error(f"❌ Событие {event_id} не найдено в БД")
|
|
857
|
+
return {"action": "execute", "reason": "event_not_found"}
|
|
858
|
+
|
|
859
|
+
event = event_response.data[0]
|
|
860
|
+
created_at = datetime.fromisoformat(event['created_at'].replace('Z', '+00:00'))
|
|
861
|
+
scheduled_at = datetime.fromisoformat(event['scheduled_at'].replace('Z', '+00:00'))
|
|
862
|
+
last_message_at = datetime.fromisoformat(user_info['last_message_at'].replace('Z', '+00:00'))
|
|
863
|
+
|
|
864
|
+
# Вычисляем разницу во времени
|
|
865
|
+
now = datetime.now(timezone.utc)
|
|
866
|
+
time_since_creation = (now - created_at).total_seconds()
|
|
867
|
+
time_since_last_message = (now - last_message_at).total_seconds()
|
|
868
|
+
planned_delay = (scheduled_at - created_at).total_seconds()
|
|
869
|
+
|
|
870
|
+
logger.info(f"🔄 Анализ для пользователя {user_id}:")
|
|
871
|
+
logger.info(f" Время с создания события: {time_since_creation:.0f}с")
|
|
872
|
+
logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
|
|
873
|
+
logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
|
|
874
|
+
|
|
875
|
+
# Если прошло меньше времени чем планировалось, но пользователь недавно писал
|
|
876
|
+
if time_since_creation < planned_delay and time_since_last_message < planned_delay:
|
|
877
|
+
# Пересчитываем время - отправляем через planned_delay после последнего сообщения
|
|
878
|
+
new_delay = max(0, planned_delay - time_since_last_message)
|
|
879
|
+
logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (через {planned_delay:.0f}с после последнего сообщения)")
|
|
880
|
+
return {
|
|
881
|
+
"action": "reschedule",
|
|
882
|
+
"new_delay": new_delay,
|
|
883
|
+
"reason": f"user_active_recently_{new_delay:.0f}s_delay"
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
# Если прошло достаточно времени - выполняем
|
|
887
|
+
if time_since_creation >= planned_delay:
|
|
888
|
+
logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id}")
|
|
889
|
+
return {"action": "execute", "reason": "time_expired"}
|
|
890
|
+
|
|
891
|
+
# Если что-то пошло не так - выполняем
|
|
892
|
+
logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")
|
|
893
|
+
return {"action": "execute", "reason": "unexpected_situation"}
|
|
894
|
+
|
|
895
|
+
except Exception as e:
|
|
896
|
+
logger.error(f"❌ Ошибка в умной проверке для пользователя {user_id}: {e}")
|
|
897
|
+
return {"action": "execute", "reason": f"error_in_check: {str(e)}"}
|
|
898
|
+
|
|
899
|
+
async def check_event_already_processed(event_type: str, user_id: int = None, session_id: str = None) -> bool:
|
|
900
|
+
"""
|
|
901
|
+
Проверяет, был ли уже обработан аналогичный event_type для пользователя/сессии
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
event_type: Тип события
|
|
905
|
+
user_id: ID пользователя (для user_event и scheduled_task)
|
|
906
|
+
session_id: ID сессии (для дополнительной проверки)
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
True если событие уже обрабатывалось или в процессе
|
|
910
|
+
"""
|
|
911
|
+
supabase_client = get_supabase_client()
|
|
912
|
+
if not supabase_client:
|
|
913
|
+
logger.error("❌ Supabase клиент не найден для проверки дублирования")
|
|
914
|
+
return False
|
|
915
|
+
|
|
916
|
+
try:
|
|
917
|
+
# Строим запрос для поиска аналогичных событий
|
|
918
|
+
query = supabase_client.client.table('scheduled_events').select('id').eq('event_type', event_type)
|
|
919
|
+
|
|
920
|
+
# Для глобальных событий (user_id = None)
|
|
921
|
+
if user_id is None:
|
|
922
|
+
query = query.is_('user_id', 'null')
|
|
923
|
+
else:
|
|
924
|
+
query = query.eq('user_id', user_id)
|
|
925
|
+
|
|
926
|
+
# Добавляем фильтр по статусам (pending, immediate, completed)
|
|
927
|
+
query = query.in_('status', ['pending', 'immediate', 'completed'])
|
|
928
|
+
|
|
929
|
+
# Если есть session_id, добавляем его в фильтр
|
|
930
|
+
if session_id:
|
|
931
|
+
query = query.eq('session_id', session_id)
|
|
932
|
+
|
|
933
|
+
response = query.execute()
|
|
934
|
+
|
|
935
|
+
if response.data:
|
|
936
|
+
logger.info(f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'")
|
|
937
|
+
return True
|
|
938
|
+
|
|
939
|
+
return False
|
|
940
|
+
|
|
941
|
+
except Exception as e:
|
|
942
|
+
logger.error(f"❌ Ошибка проверки дублирования для '{event_type}': {e}")
|
|
943
|
+
return False
|