smart-bot-factory 0.1.7__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-0.1.8.dist-info/METADATA +126 -0
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.8.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.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.7.dist-info → smart_bot_factory-0.1.8.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')
|
|
@@ -842,21 +842,91 @@ class SupabaseClient:
|
|
|
842
842
|
"""Проверяет, изменился ли этап пользователя с момента планирования события"""
|
|
843
843
|
try:
|
|
844
844
|
# Получаем текущую информацию о сессии
|
|
845
|
-
|
|
845
|
+
current_response = self.client.table('sales_chat_sessions').select(
|
|
846
846
|
'id', 'current_stage'
|
|
847
847
|
).eq('user_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
|
|
848
848
|
|
|
849
|
-
if not
|
|
849
|
+
if not current_response.data:
|
|
850
850
|
return False
|
|
851
851
|
|
|
852
|
-
current_session =
|
|
852
|
+
current_session = current_response.data[0]
|
|
853
853
|
|
|
854
854
|
# Если сессия изменилась - этап точно изменился
|
|
855
855
|
if current_session['id'] != original_session_id:
|
|
856
856
|
return True
|
|
857
857
|
|
|
858
|
+
# Если сессия та же, получаем оригинальный этап из scheduled_events
|
|
859
|
+
# и сравниваем с текущим
|
|
860
|
+
original_response = self.client.table('sales_chat_sessions').select(
|
|
861
|
+
'current_stage'
|
|
862
|
+
).eq('id', original_session_id).execute()
|
|
863
|
+
|
|
864
|
+
if not original_response.data:
|
|
865
|
+
# Если не нашли оригинальную сессию, считаем что этап не изменился
|
|
866
|
+
return False
|
|
867
|
+
|
|
868
|
+
original_stage = original_response.data[0]['current_stage']
|
|
869
|
+
current_stage = current_session['current_stage']
|
|
870
|
+
|
|
871
|
+
# Проверяем, изменился ли этап внутри той же сессии
|
|
872
|
+
if original_stage != current_stage:
|
|
873
|
+
logger.info(f"🔄 Этап изменился: {original_stage} -> {current_stage} (сессия {original_session_id})")
|
|
874
|
+
return True
|
|
875
|
+
|
|
858
876
|
return False
|
|
859
877
|
|
|
860
878
|
except Exception as e:
|
|
861
879
|
logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
|
|
862
|
-
return False
|
|
880
|
+
return False
|
|
881
|
+
|
|
882
|
+
async def get_last_event_info_by_user_and_type(self, user_id: int, event_type: str) -> Optional[str]:
|
|
883
|
+
"""
|
|
884
|
+
Получает event_info последнего события определенного типа для пользователя
|
|
885
|
+
|
|
886
|
+
Args:
|
|
887
|
+
user_id: Telegram ID пользователя
|
|
888
|
+
event_type: Тип события для поиска
|
|
889
|
+
|
|
890
|
+
Returns:
|
|
891
|
+
str: event_info последнего найденного события или None если не найдено
|
|
892
|
+
"""
|
|
893
|
+
try:
|
|
894
|
+
# 1. Получаем последнюю сессию пользователя
|
|
895
|
+
sessions_query = self.client.table('sales_chat_sessions').select(
|
|
896
|
+
'id'
|
|
897
|
+
).eq('user_id', user_id).order('created_at', desc=True).limit(1)
|
|
898
|
+
|
|
899
|
+
# Фильтруем по bot_id если указан
|
|
900
|
+
if self.bot_id:
|
|
901
|
+
sessions_query = sessions_query.eq('bot_id', self.bot_id)
|
|
902
|
+
|
|
903
|
+
sessions_response = sessions_query.execute()
|
|
904
|
+
|
|
905
|
+
if not sessions_response.data:
|
|
906
|
+
logger.info(f"Пользователь {user_id} не найден в сессиях")
|
|
907
|
+
return None
|
|
908
|
+
|
|
909
|
+
session_id = sessions_response.data[0]['id']
|
|
910
|
+
logger.info(f"Найдена последняя сессия {session_id} для пользователя {user_id}")
|
|
911
|
+
|
|
912
|
+
# 2. Ищем последнее событие с этим session_id и event_type
|
|
913
|
+
events_response = self.client.table('session_events').select(
|
|
914
|
+
'event_info', 'created_at'
|
|
915
|
+
).eq('session_id', session_id).eq('event_type', event_type).order(
|
|
916
|
+
'created_at', desc=True
|
|
917
|
+
).limit(1).execute()
|
|
918
|
+
|
|
919
|
+
if not events_response.data:
|
|
920
|
+
logger.info(f"События типа '{event_type}' не найдены для сессии {session_id}")
|
|
921
|
+
return None
|
|
922
|
+
|
|
923
|
+
event_info = events_response.data[0]['event_info']
|
|
924
|
+
created_at = events_response.data[0]['created_at']
|
|
925
|
+
|
|
926
|
+
logger.info(f"Найдено последнее событие '{event_type}' для пользователя {user_id}: {event_info[:50]}... (создано: {created_at})")
|
|
927
|
+
|
|
928
|
+
return event_info
|
|
929
|
+
|
|
930
|
+
except Exception as e:
|
|
931
|
+
logger.error(f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}")
|
|
932
|
+
return None
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Message модули smart_bot_factory
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from ..core.message_sender import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
"""
|
|
2
|
+
Message модули smart_bot_factory
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from ..core.message_sender import (
|
|
7
|
+
send_message_by_human,
|
|
8
|
+
send_message_by_ai,
|
|
9
|
+
send_message_to_users_by_stage,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'send_message_by_human',
|
|
14
|
+
'send_message_by_ai',
|
|
15
|
+
'send_message_to_users_by_stage',
|
|
12
16
|
]
|