smart-bot-factory 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/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 +583 -84
- 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 +82 -22
- smart_bot_factory/message/__init__.py +15 -11
- smart_bot_factory-0.1.9.dist-info/METADATA +602 -0
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.9.dist-info}/RECORD +16 -16
- smart_bot_factory-0.1.7.dist-info/METADATA +0 -466
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.9.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.9.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -242,3 +242,210 @@ async def send_message_by_human(
|
|
|
242
242
|
"error": str(e),
|
|
243
243
|
"user_id": user_id
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
async def send_message_to_users_by_stage(
|
|
247
|
+
stage: str,
|
|
248
|
+
message_text: str,
|
|
249
|
+
bot_id: str
|
|
250
|
+
) -> Dict[str, Any]:
|
|
251
|
+
"""
|
|
252
|
+
Отправляет сообщение всем пользователям, находящимся на определенной стадии
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
stage: Стадия диалога (например, 'introduction', 'qualification', 'closing')
|
|
256
|
+
message_text: Текст сообщения для отправки
|
|
257
|
+
bot_id: ID бота (если не указан, используется текущий бот)
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Результат отправки с количеством отправленных сообщений
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
# Импортируем необходимые компоненты
|
|
264
|
+
from ..handlers.handlers import get_global_var
|
|
265
|
+
bot = get_global_var('bot')
|
|
266
|
+
supabase_client = get_global_var('supabase_client')
|
|
267
|
+
current_bot_id = get_global_var('config').BOT_ID if get_global_var('config') else bot_id
|
|
268
|
+
|
|
269
|
+
if not current_bot_id:
|
|
270
|
+
return {
|
|
271
|
+
"status": "error",
|
|
272
|
+
"error": "Не удалось определить bot_id"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
logger.info(f"🔍 Ищем пользователей на стадии '{stage}' для бота '{current_bot_id}'")
|
|
276
|
+
|
|
277
|
+
# Получаем последние сессии для каждого пользователя с нужной стадией
|
|
278
|
+
# Сначала получаем все активные сессии с нужной стадией
|
|
279
|
+
sessions_query = supabase_client.client.table('sales_chat_sessions').select(
|
|
280
|
+
'user_id, id, current_stage, created_at'
|
|
281
|
+
).eq('status', 'active').eq('current_stage', stage)
|
|
282
|
+
|
|
283
|
+
# Фильтруем по bot_id если указан
|
|
284
|
+
if current_bot_id:
|
|
285
|
+
sessions_query = sessions_query.eq('bot_id', current_bot_id)
|
|
286
|
+
|
|
287
|
+
# Сортируем по дате создания (последние сначала)
|
|
288
|
+
sessions_query = sessions_query.order('created_at', desc=True)
|
|
289
|
+
|
|
290
|
+
sessions_data = sessions_query.execute()
|
|
291
|
+
|
|
292
|
+
if not sessions_data.data:
|
|
293
|
+
logger.info(f"📭 Пользователи на стадии '{stage}' не найдены")
|
|
294
|
+
return {
|
|
295
|
+
"status": "success",
|
|
296
|
+
"stage": stage,
|
|
297
|
+
"users_found": 0,
|
|
298
|
+
"messages_sent": 0,
|
|
299
|
+
"errors": []
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# Выбираем уникальные user_id (берем только последнюю сессию для каждого пользователя)
|
|
303
|
+
unique_users = {}
|
|
304
|
+
for session in sessions_data.data:
|
|
305
|
+
user_id = session['user_id']
|
|
306
|
+
# Если пользователь еще не добавлен, добавляем его (так как сессии отсортированы по дате, первая будет самой последней)
|
|
307
|
+
if user_id not in unique_users:
|
|
308
|
+
unique_users[user_id] = {
|
|
309
|
+
'session_id': session['id'],
|
|
310
|
+
'current_stage': session['current_stage']
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
logger.info(f"👥 Найдено {len(unique_users)} уникальных пользователей на стадии '{stage}'")
|
|
314
|
+
|
|
315
|
+
# Отправляем сообщения
|
|
316
|
+
messages_sent = 0
|
|
317
|
+
errors = []
|
|
318
|
+
|
|
319
|
+
for user_id, user_data in unique_users.items():
|
|
320
|
+
session_id = user_data['session_id']
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# Отправляем сообщение пользователю
|
|
324
|
+
await bot.send_message(
|
|
325
|
+
chat_id=user_id,
|
|
326
|
+
text=message_text
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Сохраняем сообщение в БД
|
|
330
|
+
await supabase_client.add_message(
|
|
331
|
+
session_id=session_id,
|
|
332
|
+
role='assistant',
|
|
333
|
+
content=message_text,
|
|
334
|
+
message_type='text',
|
|
335
|
+
metadata={
|
|
336
|
+
'sent_by_stage_broadcast': True,
|
|
337
|
+
'target_stage': stage,
|
|
338
|
+
'broadcast_timestamp': datetime.now().isoformat()
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
messages_sent += 1
|
|
343
|
+
logger.info(f"✅ Сообщение отправлено пользователю {user_id} (стадия: {stage})")
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
error_msg = f"Ошибка отправки пользователю {user_id}: {str(e)}"
|
|
347
|
+
errors.append(error_msg)
|
|
348
|
+
logger.error(f"❌ {error_msg}")
|
|
349
|
+
|
|
350
|
+
result = {
|
|
351
|
+
"status": "success",
|
|
352
|
+
"stage": stage,
|
|
353
|
+
"users_found": len(unique_users),
|
|
354
|
+
"messages_sent": messages_sent,
|
|
355
|
+
"errors": errors
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
logger.info(f"📊 Результат рассылки по стадии '{stage}': {messages_sent}/{len(unique_users)} сообщений отправлено")
|
|
359
|
+
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"❌ Ошибка в send_message_to_users_by_stage: {e}")
|
|
364
|
+
return {
|
|
365
|
+
"status": "error",
|
|
366
|
+
"error": str(e),
|
|
367
|
+
"stage": stage
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async def get_users_by_stage_stats(
|
|
371
|
+
bot_id: Optional[str] = None
|
|
372
|
+
) -> Dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Получает статистику пользователей по стадиям
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
bot_id: ID бота (если не указан, используется текущий бот)
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Статистика по стадиям с количеством пользователей
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
# Импортируем необходимые компоненты
|
|
384
|
+
from ..handlers.handlers import get_global_var
|
|
385
|
+
supabase_client = get_global_var('supabase_client')
|
|
386
|
+
current_bot_id = get_global_var('config').BOT_ID if get_global_var('config') else bot_id
|
|
387
|
+
|
|
388
|
+
if not current_bot_id:
|
|
389
|
+
return {
|
|
390
|
+
"status": "error",
|
|
391
|
+
"error": "Не удалось определить bot_id"
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
logger.info(f"📊 Получаем статистику по стадиям для бота '{current_bot_id}'")
|
|
395
|
+
|
|
396
|
+
# Получаем статистику по стадиям с user_id для подсчета уникальных пользователей
|
|
397
|
+
stats_query = supabase_client.client.table('sales_chat_sessions').select(
|
|
398
|
+
'user_id, current_stage, created_at'
|
|
399
|
+
).eq('status', 'active')
|
|
400
|
+
|
|
401
|
+
# Фильтруем по bot_id если указан
|
|
402
|
+
if current_bot_id:
|
|
403
|
+
stats_query = stats_query.eq('bot_id', current_bot_id)
|
|
404
|
+
|
|
405
|
+
# Сортируем по дате создания (последние сначала)
|
|
406
|
+
stats_query = stats_query.order('created_at', desc=True)
|
|
407
|
+
|
|
408
|
+
sessions_data = stats_query.execute()
|
|
409
|
+
|
|
410
|
+
# Подсчитываем уникальных пользователей по стадиям (берем последнюю сессию каждого пользователя)
|
|
411
|
+
user_stages = {} # {user_id: stage}
|
|
412
|
+
|
|
413
|
+
for session in sessions_data.data:
|
|
414
|
+
user_id = session['user_id']
|
|
415
|
+
stage = session['current_stage'] or 'unknown'
|
|
416
|
+
|
|
417
|
+
# Если пользователь еще не добавлен, добавляем его стадию (первая встреченная - самая последняя)
|
|
418
|
+
if user_id not in user_stages:
|
|
419
|
+
user_stages[user_id] = stage
|
|
420
|
+
|
|
421
|
+
# Подсчитываем количество пользователей по стадиям
|
|
422
|
+
stage_stats = {}
|
|
423
|
+
for stage in user_stages.values():
|
|
424
|
+
stage_stats[stage] = stage_stats.get(stage, 0) + 1
|
|
425
|
+
|
|
426
|
+
total_users = len(user_stages)
|
|
427
|
+
|
|
428
|
+
# Сортируем по количеству пользователей (по убыванию)
|
|
429
|
+
sorted_stages = sorted(stage_stats.items(), key=lambda x: x[1], reverse=True)
|
|
430
|
+
|
|
431
|
+
result = {
|
|
432
|
+
"status": "success",
|
|
433
|
+
"bot_id": current_bot_id,
|
|
434
|
+
"total_active_users": total_users,
|
|
435
|
+
"stages": dict(sorted_stages),
|
|
436
|
+
"stages_list": sorted_stages
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
logger.info(f"📊 Статистика по стадиям: {total_users} активных пользователей")
|
|
440
|
+
for stage, count in sorted_stages:
|
|
441
|
+
logger.info(f" {stage}: {count} пользователей")
|
|
442
|
+
|
|
443
|
+
return result
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.error(f"❌ Ошибка в get_users_by_stage_stats: {e}")
|
|
447
|
+
return {
|
|
448
|
+
"status": "error",
|
|
449
|
+
"error": str(e),
|
|
450
|
+
"bot_id": bot_id
|
|
451
|
+
}
|
smart_bot_factory/core/router.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Роутер для Smart Bot Factory - аналог aiogram Router
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Dict, Any, Callable
|
|
5
|
+
from typing import Dict, Any, Callable, Union
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
@@ -57,7 +57,7 @@ class Router:
|
|
|
57
57
|
return wrapper
|
|
58
58
|
return decorator
|
|
59
59
|
|
|
60
|
-
def schedule_task(self, task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True):
|
|
60
|
+
def schedule_task(self, task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: str = None):
|
|
61
61
|
"""
|
|
62
62
|
Декоратор для регистрации запланированной задачи в роутере
|
|
63
63
|
|
|
@@ -66,18 +66,43 @@ class Router:
|
|
|
66
66
|
notify: Уведомлять ли админов
|
|
67
67
|
smart_check: Использовать ли умную проверку
|
|
68
68
|
once_only: Выполнять ли только один раз
|
|
69
|
+
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
70
|
+
event_type: Тип события для напоминания (например, 'appointment_booking') - ОПЦИОНАЛЬНО
|
|
69
71
|
"""
|
|
70
72
|
def decorator(func: Callable) -> Callable:
|
|
73
|
+
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
74
|
+
if delay is None:
|
|
75
|
+
raise ValueError(f"Для задачи '{task_name}' в роутере {self.name} ОБЯЗАТЕЛЬНО нужно указать параметр delay")
|
|
76
|
+
|
|
77
|
+
# Импортируем функцию парсинга времени
|
|
78
|
+
from .decorators import parse_time_string
|
|
79
|
+
|
|
80
|
+
# Парсим время
|
|
81
|
+
try:
|
|
82
|
+
default_delay_seconds = parse_time_string(delay)
|
|
83
|
+
if event_type:
|
|
84
|
+
logger.info(f"⏰ Роутер {self.name}: задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)")
|
|
85
|
+
else:
|
|
86
|
+
logger.info(f"⏰ Роутер {self.name}: задача '{task_name}' настроена с задержкой: {delay} ({default_delay_seconds}с)")
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
logger.error(f"❌ Ошибка парсинга времени для задачи '{task_name}' в роутере {self.name}: {e}")
|
|
89
|
+
raise
|
|
90
|
+
|
|
71
91
|
self._scheduled_tasks[task_name] = {
|
|
72
92
|
'handler': func,
|
|
73
93
|
'name': func.__name__,
|
|
74
94
|
'notify': notify,
|
|
75
95
|
'smart_check': smart_check,
|
|
76
96
|
'once_only': once_only,
|
|
77
|
-
'router': self.name
|
|
97
|
+
'router': self.name,
|
|
98
|
+
'default_delay': default_delay_seconds,
|
|
99
|
+
'event_type': event_type # Новое поле для типа события
|
|
78
100
|
}
|
|
79
101
|
|
|
80
|
-
|
|
102
|
+
if event_type:
|
|
103
|
+
logger.info(f"⏰ Роутер {self.name}: зарегистрирована задача-напоминание '{task_name}' для события '{event_type}': {func.__name__}")
|
|
104
|
+
else:
|
|
105
|
+
logger.info(f"⏰ Роутер {self.name}: зарегистрирована задача '{task_name}': {func.__name__}")
|
|
81
106
|
|
|
82
107
|
from functools import wraps
|
|
83
108
|
@wraps(func)
|
|
@@ -90,7 +115,7 @@ class Router:
|
|
|
90
115
|
return wrapper
|
|
91
116
|
return decorator
|
|
92
117
|
|
|
93
|
-
def global_handler(self, handler_type: str, notify: bool = False, once_only: bool = True):
|
|
118
|
+
def global_handler(self, handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None):
|
|
94
119
|
"""
|
|
95
120
|
Декоратор для регистрации глобального обработчика в роутере
|
|
96
121
|
|
|
@@ -98,14 +123,31 @@ class Router:
|
|
|
98
123
|
handler_type: Тип глобального обработчика
|
|
99
124
|
notify: Уведомлять ли админов
|
|
100
125
|
once_only: Выполнять ли только один раз
|
|
126
|
+
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
101
127
|
"""
|
|
102
128
|
def decorator(func: Callable) -> Callable:
|
|
129
|
+
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
130
|
+
if delay is None:
|
|
131
|
+
raise ValueError(f"Для глобального обработчика '{handler_type}' в роутере {self.name} ОБЯЗАТЕЛЬНО нужно указать параметр delay")
|
|
132
|
+
|
|
133
|
+
# Импортируем функцию парсинга времени
|
|
134
|
+
from .decorators import parse_time_string
|
|
135
|
+
|
|
136
|
+
# Парсим время
|
|
137
|
+
try:
|
|
138
|
+
default_delay_seconds = parse_time_string(delay)
|
|
139
|
+
logger.info(f"🌍 Роутер {self.name}: глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)")
|
|
140
|
+
except ValueError as e:
|
|
141
|
+
logger.error(f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}' в роутере {self.name}: {e}")
|
|
142
|
+
raise
|
|
143
|
+
|
|
103
144
|
self._global_handlers[handler_type] = {
|
|
104
145
|
'handler': func,
|
|
105
146
|
'name': func.__name__,
|
|
106
147
|
'notify': notify,
|
|
107
148
|
'once_only': once_only,
|
|
108
|
-
'router': self.name
|
|
149
|
+
'router': self.name,
|
|
150
|
+
'default_delay': default_delay_seconds
|
|
109
151
|
}
|
|
110
152
|
|
|
111
153
|
logger.info(f"🌍 Роутер {self.name}: зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
|
|
@@ -120,7 +162,7 @@ class Router:
|
|
|
120
162
|
raise
|
|
121
163
|
return wrapper
|
|
122
164
|
return decorator
|
|
123
|
-
|
|
165
|
+
|
|
124
166
|
def get_event_handlers(self) -> Dict[str, Dict[str, Any]]:
|
|
125
167
|
"""Получает все обработчики событий роутера"""
|
|
126
168
|
return self._event_handlers.copy()
|
|
@@ -169,5 +211,4 @@ class Router:
|
|
|
169
211
|
logger.info(f"🔗 Роутер {self.name}: включен роутер {router.name}")
|
|
170
212
|
|
|
171
213
|
def __repr__(self):
|
|
172
|
-
return f"Router(name='{self.name}', events={len(self._event_handlers)}, tasks={len(self._scheduled_tasks)}, globals={len(self._global_handlers)})"
|
|
173
|
-
|
|
214
|
+
return f"Router(name='{self.name}', events={len(self._event_handlers)}, tasks={len(self._scheduled_tasks)}, globals={len(self._global_handlers)})"
|
|
@@ -60,29 +60,41 @@ class RouterManager:
|
|
|
60
60
|
'global_handlers': {}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
logger.debug(f"🔍 RouterManager._update_combined_handlers(): обновляем обработчики для {len(self._routers)} роутеров")
|
|
64
|
+
|
|
63
65
|
# Собираем обработчики из всех роутеров
|
|
64
66
|
for router in self._routers:
|
|
67
|
+
logger.debug(f"🔍 Обрабатываем роутер: {router.name}")
|
|
68
|
+
|
|
65
69
|
# Обработчики событий
|
|
66
|
-
|
|
70
|
+
event_handlers = router.get_event_handlers()
|
|
71
|
+
logger.debug(f"🔍 Роутер {router.name}: {len(event_handlers)} обработчиков событий")
|
|
72
|
+
for event_type, handler_info in event_handlers.items():
|
|
67
73
|
if event_type in self._combined_handlers['event_handlers']:
|
|
68
74
|
existing_router = self._combined_handlers['event_handlers'][event_type]['router']
|
|
69
75
|
logger.warning(f"⚠️ Конфликт обработчиков событий '{event_type}' между роутерами {existing_router} и {router.name}")
|
|
70
76
|
self._combined_handlers['event_handlers'][event_type] = handler_info
|
|
71
77
|
|
|
72
78
|
# Запланированные задачи
|
|
73
|
-
|
|
79
|
+
scheduled_tasks = router.get_scheduled_tasks()
|
|
80
|
+
logger.debug(f"🔍 Роутер {router.name}: {len(scheduled_tasks)} запланированных задач: {list(scheduled_tasks.keys())}")
|
|
81
|
+
for task_name, task_info in scheduled_tasks.items():
|
|
74
82
|
if task_name in self._combined_handlers['scheduled_tasks']:
|
|
75
83
|
existing_router = self._combined_handlers['scheduled_tasks'][task_name]['router']
|
|
76
84
|
logger.warning(f"⚠️ Конфликт задач '{task_name}' между роутерами {existing_router} и {router.name}")
|
|
77
85
|
self._combined_handlers['scheduled_tasks'][task_name] = task_info
|
|
78
86
|
|
|
79
87
|
# Глобальные обработчики
|
|
80
|
-
|
|
88
|
+
global_handlers = router.get_global_handlers()
|
|
89
|
+
logger.debug(f"🔍 Роутер {router.name}: {len(global_handlers)} глобальных обработчиков")
|
|
90
|
+
for handler_type, handler_info in global_handlers.items():
|
|
81
91
|
if handler_type in self._combined_handlers['global_handlers']:
|
|
82
92
|
existing_router = self._combined_handlers['global_handlers'][handler_type]['router']
|
|
83
93
|
logger.warning(f"⚠️ Конфликт глобальных обработчиков '{handler_type}' между роутерами {existing_router} и {router.name}")
|
|
84
94
|
self._combined_handlers['global_handlers'][handler_type] = handler_info
|
|
85
95
|
|
|
96
|
+
logger.debug(f"🔍 RouterManager._update_combined_handlers(): итого - {len(self._combined_handlers['scheduled_tasks'])} задач: {list(self._combined_handlers['scheduled_tasks'].keys())}")
|
|
97
|
+
|
|
86
98
|
total_handlers = (len(self._combined_handlers['event_handlers']) +
|
|
87
99
|
len(self._combined_handlers['scheduled_tasks']) +
|
|
88
100
|
len(self._combined_handlers['global_handlers']))
|
|
@@ -95,7 +107,9 @@ class RouterManager:
|
|
|
95
107
|
|
|
96
108
|
def get_scheduled_tasks(self) -> Dict[str, Dict[str, Any]]:
|
|
97
109
|
"""Получает все запланированные задачи"""
|
|
98
|
-
|
|
110
|
+
tasks = self._combined_handlers['scheduled_tasks'].copy()
|
|
111
|
+
logger.debug(f"🔍 RouterManager.get_scheduled_tasks(): возвращаем {len(tasks)} задач: {list(tasks.keys())}")
|
|
112
|
+
return tasks
|
|
99
113
|
|
|
100
114
|
def get_global_handlers(self) -> Dict[str, Dict[str, Any]]:
|
|
101
115
|
"""Получает все глобальные обработчики"""
|
|
@@ -383,11 +383,16 @@ class BotBuilder:
|
|
|
383
383
|
# Устанавливаем глобальные переменные в модуле бота для удобного доступа
|
|
384
384
|
self.set_global_vars_in_module(self.bot_id)
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
# Устанавливаем роутер-менеджер в декораторы
|
|
386
|
+
# Устанавливаем роутер-менеджер в декораторы ПЕРЕД настройкой обработчиков
|
|
388
387
|
if self.router_manager:
|
|
389
388
|
from ..core.decorators import set_router_manager
|
|
390
389
|
set_router_manager(self.router_manager)
|
|
390
|
+
logger.info("✅ RouterManager установлен в decorators")
|
|
391
|
+
|
|
392
|
+
# Обновляем обработчики после установки RouterManager
|
|
393
|
+
# (на случай если декораторы выполнялись после добавления роутера)
|
|
394
|
+
self.router_manager._update_combined_handlers()
|
|
395
|
+
logger.info("✅ RouterManager обработчики обновлены")
|
|
391
396
|
|
|
392
397
|
# Фоновые задачи выполняются через asyncio.create_task в decorators.py
|
|
393
398
|
|
|
@@ -64,6 +64,116 @@ async def start_handler(message: Message, state: FSMContext):
|
|
|
64
64
|
logger.error(f"Ошибка при обработке /start: {e}")
|
|
65
65
|
await send_message(message, "Произошла ошибка при инициализации. Попробуйте позже.")
|
|
66
66
|
|
|
67
|
+
@router.message(Command(commands=["timeup"]))
|
|
68
|
+
async def timeup_handler(message: Message, state: FSMContext):
|
|
69
|
+
"""Обработчик команды /timeup - тестирование запланированных событий"""
|
|
70
|
+
from ..core.decorators import process_scheduled_event, update_event_result
|
|
71
|
+
from datetime import datetime
|
|
72
|
+
|
|
73
|
+
supabase_client = get_global_var('supabase_client')
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
await message.answer("🔄 Запускаю тестирование запланированных событий...")
|
|
77
|
+
|
|
78
|
+
# Получаем события для этого пользователя И глобальные события (user_id = null)
|
|
79
|
+
# 1. События пользователя
|
|
80
|
+
user_events = supabase_client.client.table('scheduled_events').select(
|
|
81
|
+
'*'
|
|
82
|
+
).eq('user_id', message.from_user.id).in_('status', ['pending', 'immediate']).execute()
|
|
83
|
+
|
|
84
|
+
# 2. Глобальные события (без user_id)
|
|
85
|
+
global_events = supabase_client.client.table('scheduled_events').select(
|
|
86
|
+
'*'
|
|
87
|
+
).is_('user_id', 'null').in_('status', ['pending', 'immediate']).execute()
|
|
88
|
+
|
|
89
|
+
# Объединяем события
|
|
90
|
+
all_events = (user_events.data or []) + (global_events.data or [])
|
|
91
|
+
|
|
92
|
+
if not all_events:
|
|
93
|
+
await message.answer("📭 Нет запланированных событий для тестирования")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
total_events = len(all_events)
|
|
97
|
+
user_count = len(user_events.data or [])
|
|
98
|
+
global_count = len(global_events.data or [])
|
|
99
|
+
|
|
100
|
+
status_msg = f"📋 Найдено {total_events} событий:"
|
|
101
|
+
if user_count > 0:
|
|
102
|
+
status_msg += f"\n 👤 Ваших: {user_count}"
|
|
103
|
+
if global_count > 0:
|
|
104
|
+
status_msg += f"\n 🌍 Глобальных: {global_count}"
|
|
105
|
+
status_msg += "\n\nВыполняю их немедленно..."
|
|
106
|
+
|
|
107
|
+
await message.answer(status_msg)
|
|
108
|
+
|
|
109
|
+
# Выполняем каждое событие
|
|
110
|
+
success_count = 0
|
|
111
|
+
failed_count = 0
|
|
112
|
+
results = []
|
|
113
|
+
|
|
114
|
+
for event in all_events:
|
|
115
|
+
event_id = event['id']
|
|
116
|
+
event_type = event['event_type']
|
|
117
|
+
event_category = event['event_category']
|
|
118
|
+
is_global = event.get('user_id') is None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
event_label = f"🌍 {event_type}" if is_global else f"👤 {event_type}"
|
|
122
|
+
logger.info(f"🧪 Тестируем событие {event_id}: {event_category}/{event_type} ({'глобальное' if is_global else f'пользователя {message.from_user.id}'})")
|
|
123
|
+
|
|
124
|
+
# Выполняем событие
|
|
125
|
+
await process_scheduled_event(event)
|
|
126
|
+
|
|
127
|
+
# Помечаем как выполненное
|
|
128
|
+
await update_event_result(event_id, 'completed', {
|
|
129
|
+
"executed": True,
|
|
130
|
+
"test_mode": True,
|
|
131
|
+
"tested_by_user": message.from_user.id,
|
|
132
|
+
"tested_at": datetime.now().isoformat()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
success_count += 1
|
|
136
|
+
results.append(f"✅ {event_label}")
|
|
137
|
+
logger.info(f"✅ Событие {event_id} успешно выполнено")
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
failed_count += 1
|
|
141
|
+
error_msg = str(e)
|
|
142
|
+
event_label = f"🌍 {event_type}" if is_global else f"👤 {event_type}"
|
|
143
|
+
results.append(f"❌ {event_label}: {error_msg[:50]}")
|
|
144
|
+
logger.error(f"❌ Ошибка выполнения события {event_id}: {error_msg}")
|
|
145
|
+
|
|
146
|
+
# Помечаем как failed
|
|
147
|
+
await update_event_result(event_id, 'failed', None, error_msg)
|
|
148
|
+
|
|
149
|
+
# Отправляем итоговую статистику
|
|
150
|
+
result_text = [
|
|
151
|
+
"📊 **Результаты тестирования:**",
|
|
152
|
+
"",
|
|
153
|
+
f"✅ Успешно: {success_count}",
|
|
154
|
+
f"❌ Ошибок: {failed_count}",
|
|
155
|
+
f"📋 Всего: {total_events}",
|
|
156
|
+
"",
|
|
157
|
+
"**События:**",
|
|
158
|
+
f"👤 - ваши события",
|
|
159
|
+
f"🌍 - глобальные события",
|
|
160
|
+
""
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
# Добавляем результаты (максимум 10 событий)
|
|
164
|
+
for result in results[:10]:
|
|
165
|
+
result_text.append(result)
|
|
166
|
+
|
|
167
|
+
if len(results) > 10:
|
|
168
|
+
result_text.append(f"... и еще {len(results) - 10} событий")
|
|
169
|
+
|
|
170
|
+
await message.answer("\n".join(result_text))
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"❌ Критическая ошибка в timeup_handler: {e}")
|
|
174
|
+
await message.answer(f"❌ Ошибка тестирования: {str(e)}")
|
|
175
|
+
|
|
176
|
+
|
|
67
177
|
async def user_start_handler(message: Message, state: FSMContext):
|
|
68
178
|
"""Обработчик /start для обычных пользователей"""
|
|
69
179
|
supabase_client = get_global_var('supabase_client')
|
|
@@ -805,31 +805,21 @@ class SupabaseClient:
|
|
|
805
805
|
return {}
|
|
806
806
|
|
|
807
807
|
async def get_user_last_message_info(self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
808
|
-
"""Получает информацию о
|
|
808
|
+
"""Получает информацию о последней активности пользователя из сессии"""
|
|
809
809
|
try:
|
|
810
|
-
# Получаем
|
|
811
|
-
response = self.client.table('
|
|
812
|
-
'id', '
|
|
813
|
-
).eq('
|
|
810
|
+
# Получаем последнюю сессию пользователя
|
|
811
|
+
response = self.client.table('sales_chat_sessions').select(
|
|
812
|
+
'id', 'current_stage', 'created_at', 'updated_at'
|
|
813
|
+
).eq('user_id', user_id).order('updated_at', desc=True).limit(1).execute()
|
|
814
814
|
|
|
815
815
|
if not response.data:
|
|
816
816
|
return None
|
|
817
817
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
# Получаем информацию о сессии
|
|
821
|
-
session_response = self.client.table('sales_chat_sessions').select(
|
|
822
|
-
'id', 'current_stage', 'updated_at'
|
|
823
|
-
).eq('id', last_message['session_id']).execute()
|
|
824
|
-
|
|
825
|
-
if not session_response.data:
|
|
826
|
-
return None
|
|
827
|
-
|
|
828
|
-
session = session_response.data[0]
|
|
818
|
+
session = response.data[0]
|
|
829
819
|
|
|
830
820
|
return {
|
|
831
|
-
'last_message_at':
|
|
832
|
-
'session_id':
|
|
821
|
+
'last_message_at': session['updated_at'],
|
|
822
|
+
'session_id': session['id'],
|
|
833
823
|
'current_stage': session['current_stage'],
|
|
834
824
|
'session_updated_at': session['updated_at']
|
|
835
825
|
}
|
|
@@ -842,21 +832,91 @@ class SupabaseClient:
|
|
|
842
832
|
"""Проверяет, изменился ли этап пользователя с момента планирования события"""
|
|
843
833
|
try:
|
|
844
834
|
# Получаем текущую информацию о сессии
|
|
845
|
-
|
|
835
|
+
current_response = self.client.table('sales_chat_sessions').select(
|
|
846
836
|
'id', 'current_stage'
|
|
847
837
|
).eq('user_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
|
|
848
838
|
|
|
849
|
-
if not
|
|
839
|
+
if not current_response.data:
|
|
850
840
|
return False
|
|
851
841
|
|
|
852
|
-
current_session =
|
|
842
|
+
current_session = current_response.data[0]
|
|
853
843
|
|
|
854
844
|
# Если сессия изменилась - этап точно изменился
|
|
855
845
|
if current_session['id'] != original_session_id:
|
|
856
846
|
return True
|
|
857
847
|
|
|
848
|
+
# Если сессия та же, получаем оригинальный этап из scheduled_events
|
|
849
|
+
# и сравниваем с текущим
|
|
850
|
+
original_response = self.client.table('sales_chat_sessions').select(
|
|
851
|
+
'current_stage'
|
|
852
|
+
).eq('id', original_session_id).execute()
|
|
853
|
+
|
|
854
|
+
if not original_response.data:
|
|
855
|
+
# Если не нашли оригинальную сессию, считаем что этап не изменился
|
|
856
|
+
return False
|
|
857
|
+
|
|
858
|
+
original_stage = original_response.data[0]['current_stage']
|
|
859
|
+
current_stage = current_session['current_stage']
|
|
860
|
+
|
|
861
|
+
# Проверяем, изменился ли этап внутри той же сессии
|
|
862
|
+
if original_stage != current_stage:
|
|
863
|
+
logger.info(f"🔄 Этап изменился: {original_stage} -> {current_stage} (сессия {original_session_id})")
|
|
864
|
+
return True
|
|
865
|
+
|
|
858
866
|
return False
|
|
859
867
|
|
|
860
868
|
except Exception as e:
|
|
861
869
|
logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
|
|
862
|
-
return False
|
|
870
|
+
return False
|
|
871
|
+
|
|
872
|
+
async def get_last_event_info_by_user_and_type(self, user_id: int, event_type: str) -> Optional[str]:
|
|
873
|
+
"""
|
|
874
|
+
Получает event_info последнего события определенного типа для пользователя
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
user_id: Telegram ID пользователя
|
|
878
|
+
event_type: Тип события для поиска
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
str: event_info последнего найденного события или None если не найдено
|
|
882
|
+
"""
|
|
883
|
+
try:
|
|
884
|
+
# 1. Получаем последнюю сессию пользователя
|
|
885
|
+
sessions_query = self.client.table('sales_chat_sessions').select(
|
|
886
|
+
'id'
|
|
887
|
+
).eq('user_id', user_id).order('created_at', desc=True).limit(1)
|
|
888
|
+
|
|
889
|
+
# Фильтруем по bot_id если указан
|
|
890
|
+
if self.bot_id:
|
|
891
|
+
sessions_query = sessions_query.eq('bot_id', self.bot_id)
|
|
892
|
+
|
|
893
|
+
sessions_response = sessions_query.execute()
|
|
894
|
+
|
|
895
|
+
if not sessions_response.data:
|
|
896
|
+
logger.info(f"Пользователь {user_id} не найден в сессиях")
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
session_id = sessions_response.data[0]['id']
|
|
900
|
+
logger.info(f"Найдена последняя сессия {session_id} для пользователя {user_id}")
|
|
901
|
+
|
|
902
|
+
# 2. Ищем последнее событие с этим session_id и event_type
|
|
903
|
+
events_response = self.client.table('session_events').select(
|
|
904
|
+
'event_info', 'created_at'
|
|
905
|
+
).eq('session_id', session_id).eq('event_type', event_type).order(
|
|
906
|
+
'created_at', desc=True
|
|
907
|
+
).limit(1).execute()
|
|
908
|
+
|
|
909
|
+
if not events_response.data:
|
|
910
|
+
logger.info(f"События типа '{event_type}' не найдены для сессии {session_id}")
|
|
911
|
+
return None
|
|
912
|
+
|
|
913
|
+
event_info = events_response.data[0]['event_info']
|
|
914
|
+
created_at = events_response.data[0]['created_at']
|
|
915
|
+
|
|
916
|
+
logger.info(f"Найдено последнее событие '{event_type}' для пользователя {user_id}: {event_info[:50]}... (создано: {created_at})")
|
|
917
|
+
|
|
918
|
+
return event_info
|
|
919
|
+
|
|
920
|
+
except Exception as e:
|
|
921
|
+
logger.error(f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}")
|
|
922
|
+
return None
|