smart-bot-factory 0.3.5__py3-none-any.whl → 0.3.7__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/core/bot_utils.py +23 -1
- smart_bot_factory/core/decorators.py +120 -12
- smart_bot_factory/dashboard/__init__.py +5 -0
- smart_bot_factory/handlers/handlers.py +16 -4
- smart_bot_factory/integrations/supabase_client.py +4 -3
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.7.dist-info}/METADATA +233 -20
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.7.dist-info}/RECORD +10 -10
- smart_bot_factory/admin/admin_migration.sql +0 -136
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.7.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.7.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -255,6 +255,10 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
|
255
255
|
# if session_id:
|
|
256
256
|
# check_query = check_query.eq('session_id', session_id)
|
|
257
257
|
|
|
258
|
+
# 🆕 Фильтруем по bot_id если указан
|
|
259
|
+
if supabase_client.bot_id:
|
|
260
|
+
check_query = check_query.eq('bot_id', supabase_client.bot_id)
|
|
261
|
+
|
|
258
262
|
existing = check_query.execute()
|
|
259
263
|
|
|
260
264
|
logger.info(f" 🔍 Проверка БД: найдено {len(existing.data) if existing.data else 0} выполненных событий '{event_type}' для user_id={user_id}")
|
|
@@ -271,6 +275,13 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
|
271
275
|
# Выполняем событие
|
|
272
276
|
result = await execute_event_handler(event_type, user_id, event_info)
|
|
273
277
|
|
|
278
|
+
# Проверяем наличие поля 'info' для дашборда
|
|
279
|
+
import json
|
|
280
|
+
info_dashboard_json = None
|
|
281
|
+
if isinstance(result, dict) and 'info' in result:
|
|
282
|
+
info_dashboard_json = json.dumps(result['info'], ensure_ascii=False)
|
|
283
|
+
logger.info(f" 📊 Дашборд данные добавлены: {result['info'].get('title', 'N/A')}")
|
|
284
|
+
|
|
274
285
|
# Сохраняем в БД УЖЕ со статусом completed (избегаем дублирования)
|
|
275
286
|
event_record = {
|
|
276
287
|
'event_type': event_type,
|
|
@@ -281,8 +292,14 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
|
281
292
|
'status': 'completed', # Сразу completed!
|
|
282
293
|
'session_id': session_id,
|
|
283
294
|
'executed_at': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
|
|
284
|
-
'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None
|
|
295
|
+
'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None,
|
|
296
|
+
'info_dashboard': info_dashboard_json # Добавится только если есть поле 'info'
|
|
285
297
|
}
|
|
298
|
+
|
|
299
|
+
# 🆕 Добавляем bot_id если указан
|
|
300
|
+
if supabase_client.bot_id:
|
|
301
|
+
event_record['bot_id'] = supabase_client.bot_id
|
|
302
|
+
|
|
286
303
|
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
287
304
|
event_id = response.data[0]['id']
|
|
288
305
|
|
|
@@ -303,6 +320,11 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
|
303
320
|
'session_id': session_id,
|
|
304
321
|
'last_error': str(e)
|
|
305
322
|
}
|
|
323
|
+
|
|
324
|
+
# 🆕 Добавляем bot_id если указан
|
|
325
|
+
if supabase_client.bot_id:
|
|
326
|
+
event_record['bot_id'] = supabase_client.bot_id
|
|
327
|
+
|
|
306
328
|
supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
307
329
|
raise
|
|
308
330
|
|
|
@@ -1135,6 +1135,10 @@ async def save_immediate_event(
|
|
|
1135
1135
|
'session_id': session_id
|
|
1136
1136
|
}
|
|
1137
1137
|
|
|
1138
|
+
# 🆕 Добавляем bot_id если указан
|
|
1139
|
+
if supabase_client.bot_id:
|
|
1140
|
+
event_record['bot_id'] = supabase_client.bot_id
|
|
1141
|
+
|
|
1138
1142
|
try:
|
|
1139
1143
|
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
1140
1144
|
event_id = response.data[0]['id']
|
|
@@ -1187,6 +1191,10 @@ async def save_scheduled_task(
|
|
|
1187
1191
|
'session_id': session_id
|
|
1188
1192
|
}
|
|
1189
1193
|
|
|
1194
|
+
# 🆕 Добавляем bot_id если указан
|
|
1195
|
+
if supabase_client.bot_id:
|
|
1196
|
+
event_record['bot_id'] = supabase_client.bot_id
|
|
1197
|
+
|
|
1190
1198
|
try:
|
|
1191
1199
|
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
1192
1200
|
event_id = response.data[0]['id']
|
|
@@ -1241,6 +1249,10 @@ async def save_global_event(
|
|
|
1241
1249
|
'status': status
|
|
1242
1250
|
}
|
|
1243
1251
|
|
|
1252
|
+
# 🆕 Добавляем bot_id если указан (глобальные события тоже привязаны к боту)
|
|
1253
|
+
if supabase_client.bot_id:
|
|
1254
|
+
event_record['bot_id'] = supabase_client.bot_id
|
|
1255
|
+
|
|
1244
1256
|
try:
|
|
1245
1257
|
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
1246
1258
|
event_id = response.data[0]['id']
|
|
@@ -1271,6 +1283,11 @@ async def update_event_result(
|
|
|
1271
1283
|
if result_data:
|
|
1272
1284
|
import json
|
|
1273
1285
|
update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
|
|
1286
|
+
|
|
1287
|
+
# Проверяем наличие поля 'info' для дашборда
|
|
1288
|
+
if isinstance(result_data, dict) and 'info' in result_data:
|
|
1289
|
+
update_data['info_dashboard'] = json.dumps(result_data['info'], ensure_ascii=False)
|
|
1290
|
+
logger.info(f"📊 Дашборд данные добавлены в событие {event_id}")
|
|
1274
1291
|
|
|
1275
1292
|
if error_message:
|
|
1276
1293
|
update_data['last_error'] = error_message
|
|
@@ -1298,13 +1315,18 @@ async def get_pending_events(limit: int = 50) -> list:
|
|
|
1298
1315
|
try:
|
|
1299
1316
|
now = datetime.now(timezone.utc).isoformat()
|
|
1300
1317
|
|
|
1301
|
-
|
|
1318
|
+
query = supabase_client.client.table('scheduled_events')\
|
|
1302
1319
|
.select('*')\
|
|
1303
1320
|
.in_('status', ['pending', 'immediate'])\
|
|
1304
1321
|
.or_(f'scheduled_at.is.null,scheduled_at.lte.{now}')\
|
|
1305
1322
|
.order('created_at')\
|
|
1306
|
-
.limit(limit)
|
|
1307
|
-
|
|
1323
|
+
.limit(limit)
|
|
1324
|
+
|
|
1325
|
+
# 🆕 Фильтруем по bot_id если указан
|
|
1326
|
+
if supabase_client.bot_id:
|
|
1327
|
+
query = query.eq('bot_id', supabase_client.bot_id)
|
|
1328
|
+
|
|
1329
|
+
response = query.execute()
|
|
1308
1330
|
|
|
1309
1331
|
return response.data
|
|
1310
1332
|
except Exception as e:
|
|
@@ -1323,13 +1345,18 @@ async def get_pending_events_in_next_minute(limit: int = 100) -> list:
|
|
|
1323
1345
|
now = datetime.now(timezone.utc)
|
|
1324
1346
|
next_minute = now + timedelta(seconds=60)
|
|
1325
1347
|
|
|
1326
|
-
|
|
1348
|
+
query = supabase_client.client.table('scheduled_events')\
|
|
1327
1349
|
.select('*')\
|
|
1328
1350
|
.in_('status', ['pending', 'immediate'])\
|
|
1329
1351
|
.or_(f'scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}')\
|
|
1330
1352
|
.order('created_at')\
|
|
1331
|
-
.limit(limit)
|
|
1332
|
-
|
|
1353
|
+
.limit(limit)
|
|
1354
|
+
|
|
1355
|
+
# 🆕 Фильтруем по bot_id если указан
|
|
1356
|
+
if supabase_client.bot_id:
|
|
1357
|
+
query = query.eq('bot_id', supabase_client.bot_id)
|
|
1358
|
+
|
|
1359
|
+
response = query.execute()
|
|
1333
1360
|
|
|
1334
1361
|
return response.data
|
|
1335
1362
|
except Exception as e:
|
|
@@ -1471,8 +1498,16 @@ async def background_event_processor():
|
|
|
1471
1498
|
continue
|
|
1472
1499
|
|
|
1473
1500
|
# Выполняем событие
|
|
1474
|
-
await process_scheduled_event(event)
|
|
1475
|
-
|
|
1501
|
+
result = await process_scheduled_event(event)
|
|
1502
|
+
|
|
1503
|
+
# Проверяем наличие поля 'info' для дашборда
|
|
1504
|
+
result_data = {"processed": True}
|
|
1505
|
+
if isinstance(result, dict):
|
|
1506
|
+
result_data.update(result)
|
|
1507
|
+
if 'info' in result:
|
|
1508
|
+
logger.info(f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}")
|
|
1509
|
+
|
|
1510
|
+
await update_event_result(event['id'], 'completed', result_data)
|
|
1476
1511
|
logger.info(f"✅ Событие {event['id']} выполнено")
|
|
1477
1512
|
|
|
1478
1513
|
except Exception as e:
|
|
@@ -1486,7 +1521,7 @@ async def background_event_processor():
|
|
|
1486
1521
|
await asyncio.sleep(60)
|
|
1487
1522
|
|
|
1488
1523
|
async def process_scheduled_event(event: Dict):
|
|
1489
|
-
"""Обрабатывает одно событие из БД"""
|
|
1524
|
+
"""Обрабатывает одно событие из БД и возвращает результат"""
|
|
1490
1525
|
|
|
1491
1526
|
event_type = event['event_type']
|
|
1492
1527
|
event_category = event['event_category']
|
|
@@ -1495,14 +1530,17 @@ async def process_scheduled_event(event: Dict):
|
|
|
1495
1530
|
|
|
1496
1531
|
logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
|
|
1497
1532
|
|
|
1533
|
+
result = None
|
|
1498
1534
|
if event_category == 'scheduled_task':
|
|
1499
|
-
await execute_scheduled_task(event_type, user_id, event_data)
|
|
1535
|
+
result = await execute_scheduled_task(event_type, user_id, event_data)
|
|
1500
1536
|
elif event_category == 'global_handler':
|
|
1501
|
-
await execute_global_handler(event_type, event_data)
|
|
1537
|
+
result = await execute_global_handler(event_type, event_data)
|
|
1502
1538
|
elif event_category == 'user_event':
|
|
1503
|
-
await execute_event_handler(event_type, user_id, event_data)
|
|
1539
|
+
result = await execute_event_handler(event_type, user_id, event_data)
|
|
1504
1540
|
else:
|
|
1505
1541
|
logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
|
|
1542
|
+
|
|
1543
|
+
return result
|
|
1506
1544
|
|
|
1507
1545
|
# =============================================================================
|
|
1508
1546
|
# ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
|
|
@@ -1689,6 +1727,10 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
|
|
|
1689
1727
|
if session_id:
|
|
1690
1728
|
query = query.eq('session_id', session_id)
|
|
1691
1729
|
|
|
1730
|
+
# 🆕 Фильтруем по bot_id если указан
|
|
1731
|
+
if supabase_client.bot_id:
|
|
1732
|
+
query = query.eq('bot_id', supabase_client.bot_id)
|
|
1733
|
+
|
|
1692
1734
|
response = query.execute()
|
|
1693
1735
|
|
|
1694
1736
|
if response.data:
|
|
@@ -1884,3 +1926,69 @@ async def process_admin_event(event: Dict):
|
|
|
1884
1926
|
shutil.rmtree(temp_after_msg, ignore_errors=True)
|
|
1885
1927
|
logger.error(f"❌ Критическая ошибка обработки события: {e}")
|
|
1886
1928
|
raise
|
|
1929
|
+
|
|
1930
|
+
# =============================================================================
|
|
1931
|
+
# ФУНКЦИЯ ДЛЯ ПОДГОТОВКИ ДАННЫХ ДАШБОРДА
|
|
1932
|
+
# =============================================================================
|
|
1933
|
+
|
|
1934
|
+
async def prepare_dashboard_info(
|
|
1935
|
+
description_template: str,
|
|
1936
|
+
title: str,
|
|
1937
|
+
user_id: int
|
|
1938
|
+
) -> Dict[str, Any]:
|
|
1939
|
+
"""
|
|
1940
|
+
Подготавливает данные для дашборда (БЕЗ записи в БД)
|
|
1941
|
+
|
|
1942
|
+
Возвращаемый dict нужно поместить в поле 'info' результата обработчика.
|
|
1943
|
+
bot_utils.py автоматически запишет его в столбец info_dashboard таблицы.
|
|
1944
|
+
|
|
1945
|
+
Args:
|
|
1946
|
+
description_template: Строка с {username}, например "{username} купил подписку"
|
|
1947
|
+
title: Заголовок для дашборда
|
|
1948
|
+
user_id: Telegram ID
|
|
1949
|
+
|
|
1950
|
+
Returns:
|
|
1951
|
+
Dict с данными для дашборда
|
|
1952
|
+
|
|
1953
|
+
Example:
|
|
1954
|
+
@event_router.event_handler("collect_phone", notify=True)
|
|
1955
|
+
async def handle_phone_collection(user_id: int, phone_number: str):
|
|
1956
|
+
# ... бизнес-логика ...
|
|
1957
|
+
|
|
1958
|
+
return {
|
|
1959
|
+
"status": "success",
|
|
1960
|
+
"phone": phone_number,
|
|
1961
|
+
"info": await prepare_dashboard_info(
|
|
1962
|
+
description_template="{username} оставил телефон",
|
|
1963
|
+
title="Новый контакт",
|
|
1964
|
+
user_id=user_id
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1967
|
+
"""
|
|
1968
|
+
supabase_client = get_supabase_client()
|
|
1969
|
+
|
|
1970
|
+
# Получаем username из sales_users
|
|
1971
|
+
username = f"user_{user_id}" # fallback
|
|
1972
|
+
if supabase_client:
|
|
1973
|
+
try:
|
|
1974
|
+
query = supabase_client.client.table('sales_users').select('username').eq('telegram_id', user_id)
|
|
1975
|
+
if supabase_client.bot_id:
|
|
1976
|
+
query = query.eq('bot_id', supabase_client.bot_id)
|
|
1977
|
+
response = query.execute()
|
|
1978
|
+
if response.data:
|
|
1979
|
+
username = response.data[0].get('username') or username
|
|
1980
|
+
except Exception as e:
|
|
1981
|
+
logger.warning(f"⚠️ Не удалось получить username для дашборда: {e}")
|
|
1982
|
+
|
|
1983
|
+
# Форматируем строку
|
|
1984
|
+
description = description_template.format(username=username)
|
|
1985
|
+
|
|
1986
|
+
# Московское время (UTC+3)
|
|
1987
|
+
moscow_tz = timezone(timedelta(hours=3))
|
|
1988
|
+
moscow_time = datetime.now(moscow_tz)
|
|
1989
|
+
|
|
1990
|
+
return {
|
|
1991
|
+
'title': title,
|
|
1992
|
+
'description': description,
|
|
1993
|
+
'created_at': moscow_time.isoformat()
|
|
1994
|
+
}
|
|
@@ -77,14 +77,26 @@ async def timeup_handler(message: Message, state: FSMContext):
|
|
|
77
77
|
|
|
78
78
|
# Получаем события для этого пользователя И глобальные события (user_id = null)
|
|
79
79
|
# 1. События пользователя
|
|
80
|
-
|
|
80
|
+
user_events_query = supabase_client.client.table('scheduled_events').select(
|
|
81
81
|
'*'
|
|
82
|
-
).eq('user_id', message.from_user.id).in_('status', ['pending', 'immediate'])
|
|
82
|
+
).eq('user_id', message.from_user.id).in_('status', ['pending', 'immediate'])
|
|
83
|
+
|
|
84
|
+
# 🆕 Фильтруем по bot_id если указан
|
|
85
|
+
if supabase_client.bot_id:
|
|
86
|
+
user_events_query = user_events_query.eq('bot_id', supabase_client.bot_id)
|
|
87
|
+
|
|
88
|
+
user_events = user_events_query.execute()
|
|
83
89
|
|
|
84
90
|
# 2. Глобальные события (без user_id)
|
|
85
|
-
|
|
91
|
+
global_events_query = supabase_client.client.table('scheduled_events').select(
|
|
86
92
|
'*'
|
|
87
|
-
).is_('user_id', 'null').in_('status', ['pending', 'immediate'])
|
|
93
|
+
).is_('user_id', 'null').in_('status', ['pending', 'immediate'])
|
|
94
|
+
|
|
95
|
+
# 🆕 Фильтруем по bot_id если указан
|
|
96
|
+
if supabase_client.bot_id:
|
|
97
|
+
global_events_query = global_events_query.eq('bot_id', supabase_client.bot_id)
|
|
98
|
+
|
|
99
|
+
global_events = global_events_query.execute()
|
|
88
100
|
|
|
89
101
|
# Объединяем события
|
|
90
102
|
all_events = (user_events.data or []) + (global_events.data or [])
|
|
@@ -398,8 +398,8 @@ class SupabaseClient:
|
|
|
398
398
|
try:
|
|
399
399
|
# Проверяем существует ли админ
|
|
400
400
|
response = self.client.table('sales_admins').select('telegram_id').eq(
|
|
401
|
-
'telegram_id', admin_data['telegram_id']
|
|
402
|
-
).execute()
|
|
401
|
+
'telegram_id', admin_data['telegram_id'],
|
|
402
|
+
).eq('bot_id', self.bot_id).execute()
|
|
403
403
|
|
|
404
404
|
if response.data:
|
|
405
405
|
# Обновляем существующего
|
|
@@ -408,13 +408,14 @@ class SupabaseClient:
|
|
|
408
408
|
'first_name': admin_data.get('first_name'),
|
|
409
409
|
'last_name': admin_data.get('last_name'),
|
|
410
410
|
'is_active': True
|
|
411
|
-
}).eq('telegram_id', admin_data['telegram_id']).execute()
|
|
411
|
+
}).eq('telegram_id', admin_data['telegram_id']).eq('bot_id', self.bot_id).execute()
|
|
412
412
|
|
|
413
413
|
logger.debug(f"Обновлен админ {admin_data['telegram_id']}")
|
|
414
414
|
else:
|
|
415
415
|
# Создаем нового
|
|
416
416
|
self.client.table('sales_admins').insert({
|
|
417
417
|
'telegram_id': admin_data['telegram_id'],
|
|
418
|
+
'bot_id': self.bot_id,
|
|
418
419
|
'username': admin_data.get('username'),
|
|
419
420
|
'first_name': admin_data.get('first_name'),
|
|
420
421
|
'last_name': admin_data.get('last_name'),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-bot-factory
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Библиотека для создания умных чат-ботов
|
|
5
5
|
Author-email: Kopatych <eserov73@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -44,6 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
- [event_handler](#event_handler---обработчики-событий)
|
|
45
45
|
- [schedule_task](#schedule_task---запланированные-задачи)
|
|
46
46
|
- [global_handler](#global_handler---глобальные-обработчики)
|
|
47
|
+
- [Dashboard Info](#-dashboard-info---отправка-данных-в-дашборд)
|
|
47
48
|
- [Хуки для кастомизации](#-хуки-для-кастомизации)
|
|
48
49
|
- [Telegram роутеры](#-telegram-роутеры)
|
|
49
50
|
- [Расширенные возможности](#-расширенные-возможности)
|
|
@@ -187,9 +188,10 @@ sbf link
|
|
|
187
188
|
|
|
188
189
|
```python
|
|
189
190
|
@event_router.event_handler(
|
|
190
|
-
event_type: str,
|
|
191
|
-
notify: bool = False,
|
|
192
|
-
once_only: bool = True
|
|
191
|
+
event_type: str, # Тип события
|
|
192
|
+
notify: bool = False, # Уведомлять админов
|
|
193
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
194
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
193
195
|
)
|
|
194
196
|
async def handler(user_id: int, event_data: str):
|
|
195
197
|
# Ваш код
|
|
@@ -201,6 +203,7 @@ async def handler(user_id: int, event_data: str):
|
|
|
201
203
|
- **`event_type`** (обязательный) - Уникальное имя события
|
|
202
204
|
- **`notify`** (по умолчанию `False`) - Отправлять уведомление админам после выполнения
|
|
203
205
|
- **`once_only`** (по умолчанию `True`) - Если `True`, событие выполнится только 1 раз для пользователя
|
|
206
|
+
- **`send_ai_response`** (по умолчанию `True`) - Если `False`, ИИ НЕ отправит сообщение после выполнения обработчика
|
|
204
207
|
|
|
205
208
|
**Как работает:**
|
|
206
209
|
|
|
@@ -235,6 +238,16 @@ async def handle_question(user_id: int, question: str):
|
|
|
235
238
|
"""Обрабатывает вопросы (может быть много)"""
|
|
236
239
|
# Логика обработки
|
|
237
240
|
return {"status": "answered"}
|
|
241
|
+
|
|
242
|
+
# БЕЗ отправки ответа от ИИ
|
|
243
|
+
@event_router.event_handler("silent_event", send_ai_response=False)
|
|
244
|
+
async def handle_silent(user_id: int, event_data: str):
|
|
245
|
+
"""
|
|
246
|
+
Выполняет логику БЕЗ отправки сообщения от ИИ
|
|
247
|
+
Используйте когда хотите только собрать данные без ответа пользователю
|
|
248
|
+
"""
|
|
249
|
+
await send_message_by_human(user_id, "✅ Данные сохранены")
|
|
250
|
+
return {"status": "saved"}
|
|
238
251
|
```
|
|
239
252
|
|
|
240
253
|
---
|
|
@@ -247,12 +260,13 @@ async def handle_question(user_id: int, question: str):
|
|
|
247
260
|
|
|
248
261
|
```python
|
|
249
262
|
@event_router.schedule_task(
|
|
250
|
-
task_name: str,
|
|
251
|
-
delay: Union[str, int],
|
|
252
|
-
notify: bool = False,
|
|
253
|
-
smart_check: bool = True,
|
|
254
|
-
once_only: bool = True,
|
|
255
|
-
event_type: Union[str, Callable] = None # Источник времени события
|
|
263
|
+
task_name: str, # Название задачи
|
|
264
|
+
delay: Union[str, int], # Задержка: "1h 30m" или секунды
|
|
265
|
+
notify: bool = False, # Уведомлять админов
|
|
266
|
+
smart_check: bool = True, # Умная проверка активности
|
|
267
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
268
|
+
event_type: Union[str, Callable] = None, # Источник времени события
|
|
269
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
256
270
|
)
|
|
257
271
|
async def handler(user_id: int, user_data: str):
|
|
258
272
|
# Ваш код
|
|
@@ -273,6 +287,7 @@ async def handler(user_id: int, user_data: str):
|
|
|
273
287
|
- **`event_type`** (опционально) - Источник времени события:
|
|
274
288
|
- **Строка**: `"appointment_booking"` - ищет событие в БД и вычисляет время
|
|
275
289
|
- **Функция**: `async def(user_id, user_data) -> datetime` - кастомная логика
|
|
290
|
+
- **`send_ai_response`** (по умолчанию `True`) - Если `False`, ИИ НЕ отправит сообщение после выполнения задачи
|
|
276
291
|
|
|
277
292
|
**Формула времени с `event_type`:**
|
|
278
293
|
|
|
@@ -362,11 +377,12 @@ async def important_reminder(user_id: int, text: str):
|
|
|
362
377
|
|
|
363
378
|
```python
|
|
364
379
|
@event_router.global_handler(
|
|
365
|
-
handler_type: str,
|
|
366
|
-
delay: Union[str, int],
|
|
367
|
-
notify: bool = False,
|
|
368
|
-
once_only: bool = True,
|
|
369
|
-
event_type: Union[str, Callable] = None # Источник времени
|
|
380
|
+
handler_type: str, # Тип обработчика
|
|
381
|
+
delay: Union[str, int], # Задержка
|
|
382
|
+
notify: bool = False, # Уведомлять админов
|
|
383
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
384
|
+
event_type: Union[str, Callable] = None, # Источник времени
|
|
385
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
370
386
|
)
|
|
371
387
|
async def handler(handler_data: str):
|
|
372
388
|
# Ваш код
|
|
@@ -420,6 +436,173 @@ async def notify_promo_ending(handler_data: str):
|
|
|
420
436
|
|
|
421
437
|
---
|
|
422
438
|
|
|
439
|
+
## 📊 Dashboard Info - Отправка данных в дашборд
|
|
440
|
+
|
|
441
|
+
**Назначение:** Позволяет отправлять информацию о событиях в дашборд (таблица `scheduled_events`, столбец `info_dashboard`) для аналитики и мониторинга.
|
|
442
|
+
|
|
443
|
+
### Как работает
|
|
444
|
+
|
|
445
|
+
1. Обработчик события возвращает результат с полем `'info'`
|
|
446
|
+
2. Система автоматически извлекает это поле и записывает в `info_dashboard` таблицы
|
|
447
|
+
3. Функция `prepare_dashboard_info` автоматически:
|
|
448
|
+
- Получает `username` из таблицы `sales_users`
|
|
449
|
+
- Форматирует строку с подстановкой данных
|
|
450
|
+
- Добавляет московское время (UTC+3)
|
|
451
|
+
|
|
452
|
+
### Сигнатура
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
from smart_bot_factory.dashboard import prepare_dashboard_info
|
|
456
|
+
|
|
457
|
+
dashboard_data = await prepare_dashboard_info(
|
|
458
|
+
description_template: str, # Строка с {username}, например "{username} купил подписку"
|
|
459
|
+
title: str, # Заголовок для дашборда
|
|
460
|
+
user_id: int # Telegram ID пользователя
|
|
461
|
+
)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Возвращает:**
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
{
|
|
468
|
+
'title': 'Заголовок',
|
|
469
|
+
'description': '@username123 купил подписку', # С подстановкой реального username
|
|
470
|
+
'created_at': '2025-10-18T15:30:45.123456+03:00' # Московское время
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Примеры использования
|
|
475
|
+
|
|
476
|
+
#### С event_handler
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
from smart_bot_factory.dashboard import prepare_dashboard_info
|
|
480
|
+
|
|
481
|
+
@event_router.event_handler("collect_phone", notify=True, once_only=True)
|
|
482
|
+
async def handle_phone_collection(user_id: int, phone_number: str):
|
|
483
|
+
"""Сохраняет телефон клиента"""
|
|
484
|
+
|
|
485
|
+
# Ваша бизнес-логика
|
|
486
|
+
session = await supabase_client.get_active_session(user_id)
|
|
487
|
+
if session:
|
|
488
|
+
metadata = session.get('metadata', {})
|
|
489
|
+
metadata['phone'] = phone_number
|
|
490
|
+
await supabase_client.update_session_metadata(session['id'], metadata)
|
|
491
|
+
|
|
492
|
+
await send_message_by_human(
|
|
493
|
+
user_id=user_id,
|
|
494
|
+
message_text=f"✅ Спасибо! Ваш номер {phone_number} сохранен"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# 📊 Возвращаем результат С данными для дашборда
|
|
498
|
+
return {
|
|
499
|
+
"status": "success",
|
|
500
|
+
"phone": phone_number,
|
|
501
|
+
"info": await prepare_dashboard_info(
|
|
502
|
+
description_template="{username} оставил номер телефона",
|
|
503
|
+
title="Новый контакт",
|
|
504
|
+
user_id=user_id
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### С schedule_task
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
@event_router.schedule_task("follow_up", delay="24h", smart_check=True)
|
|
513
|
+
async def send_follow_up(user_id: int, reminder_text: str):
|
|
514
|
+
"""Напоминание через 24 часа"""
|
|
515
|
+
|
|
516
|
+
await send_message_by_human(
|
|
517
|
+
user_id=user_id,
|
|
518
|
+
message_text=f"👋 {reminder_text}"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# 📊 Работает и для задач!
|
|
522
|
+
return {
|
|
523
|
+
"status": "sent",
|
|
524
|
+
"type": "follow_up",
|
|
525
|
+
"info": await prepare_dashboard_info(
|
|
526
|
+
description_template="{username} получил напоминание",
|
|
527
|
+
title="Напоминание отправлено",
|
|
528
|
+
user_id=user_id
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### БЕЗ дашборда
|
|
534
|
+
|
|
535
|
+
Если не нужно отправлять данные в дашборд - просто не добавляйте поле `'info'`:
|
|
536
|
+
|
|
537
|
+
```python
|
|
538
|
+
@event_router.event_handler("collect_name", once_only=False)
|
|
539
|
+
async def handle_name_collection(user_id: int, client_name: str):
|
|
540
|
+
"""БЕЗ дашборда - просто сохраняем имя"""
|
|
541
|
+
|
|
542
|
+
await send_message_by_human(
|
|
543
|
+
user_id=user_id,
|
|
544
|
+
message_text=f"✅ Спасибо! Ваше имя {client_name} сохранено"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Возвращаем БЕЗ поля 'info' - дашборд останется пустым
|
|
548
|
+
return {"status": "success"}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Что попадает в БД
|
|
552
|
+
|
|
553
|
+
**События С дашбордом:**
|
|
554
|
+
|
|
555
|
+
```sql
|
|
556
|
+
SELECT * FROM scheduled_events WHERE id = '123';
|
|
557
|
+
|
|
558
|
+
id: 123
|
|
559
|
+
event_type: collect_phone
|
|
560
|
+
event_category: user_event
|
|
561
|
+
user_id: 12345
|
|
562
|
+
status: completed
|
|
563
|
+
result_data: {"status": "success", "phone": "+79001234567", "info": {...}}
|
|
564
|
+
info_dashboard: {
|
|
565
|
+
"title": "Новый контакт",
|
|
566
|
+
"description": "@username123 оставил номер телефона",
|
|
567
|
+
"created_at": "2025-10-18T15:30:45+03:00"
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**События БЕЗ дашборда:**
|
|
572
|
+
|
|
573
|
+
```sql
|
|
574
|
+
SELECT * FROM scheduled_events WHERE id = '124';
|
|
575
|
+
|
|
576
|
+
id: 124
|
|
577
|
+
event_type: collect_name
|
|
578
|
+
event_category: user_event
|
|
579
|
+
user_id: 12345
|
|
580
|
+
status: completed
|
|
581
|
+
result_data: {"status": "success"}
|
|
582
|
+
info_dashboard: NULL ← Остается пустым
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Форматирование строк
|
|
586
|
+
|
|
587
|
+
Функция `prepare_dashboard_info` поддерживает подстановку `{username}`:
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
# Примеры шаблонов:
|
|
591
|
+
"{username} купил подписку на 1 год"
|
|
592
|
+
"{username} оставил контакт"
|
|
593
|
+
"{username} записался на консультацию"
|
|
594
|
+
"{username} задал вопрос о продукте"
|
|
595
|
+
"{username} завершил оплату"
|
|
596
|
+
|
|
597
|
+
# После подстановки:
|
|
598
|
+
"@user123 купил подписку на 1 год"
|
|
599
|
+
"@ivan_petrov оставил контакт"
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Если пользователь не найден в `sales_users` - будет использован fallback: `user_12345`
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
423
606
|
## 🎣 Хуки для кастомизации
|
|
424
607
|
|
|
425
608
|
Хуки позволяют внедрять свою логику в стандартную обработку сообщений без переписывания всей функции.
|
|
@@ -813,16 +996,46 @@ DEBUG_MODE=false
|
|
|
813
996
|
|
|
814
997
|
## 🎯 Сравнение декораторов
|
|
815
998
|
|
|
816
|
-
| Декоратор | Когда выполняется | Для кого |
|
|
817
|
-
|
|
818
|
-
| `@event_handler` | Немедленно | 1 пользователь | `event_type`, `notify`, `once_only` |
|
|
819
|
-
| `@schedule_task` | Через время | 1 пользователь | `task_name`, `delay`, `event_type`, `smart_check`, `once_only`, `notify` |
|
|
820
|
-
| `@global_handler` | Через время | Все пользователи | `handler_type`, `delay`, `event_type`, `once_only`, `notify` |
|
|
999
|
+
| Декоратор | Когда выполняется | Для кого | Ключевые параметры |
|
|
1000
|
+
|-----------|-------------------|----------|--------------------|
|
|
1001
|
+
| `@event_handler` | Немедленно | 1 пользователь | `event_type`, `notify`, `once_only`, `send_ai_response` |
|
|
1002
|
+
| `@schedule_task` | Через время | 1 пользователь | `task_name`, `delay`, `event_type`, `smart_check`, `once_only`, `notify`, `send_ai_response` |
|
|
1003
|
+
| `@global_handler` | Через время | Все пользователи | `handler_type`, `delay`, `event_type`, `once_only`, `notify`, `send_ai_response` |
|
|
821
1004
|
|
|
822
1005
|
---
|
|
823
1006
|
|
|
824
1007
|
## 🔑 Ключевые концепции
|
|
825
1008
|
|
|
1009
|
+
### `send_ai_response=True`
|
|
1010
|
+
|
|
1011
|
+
Контролирует отправку сообщения от ИИ после выполнения обработчика:
|
|
1012
|
+
|
|
1013
|
+
- **`True`** (по умолчанию) - ИИ отправит сообщение пользователю после выполнения обработчика
|
|
1014
|
+
- **`False`** - ИИ НЕ отправит сообщение (используйте когда нужна только фоновая обработка или когда отправляете сообщение вручную)
|
|
1015
|
+
|
|
1016
|
+
**Когда использовать `send_ai_response=False`:**
|
|
1017
|
+
|
|
1018
|
+
- Когда нужно только собрать данные без ответа пользователю
|
|
1019
|
+
- Когда вы сами отправляете сообщение через `send_message_by_human()`
|
|
1020
|
+
- Для фоновых задач без взаимодействия с пользователем
|
|
1021
|
+
|
|
1022
|
+
```python
|
|
1023
|
+
# ИИ отправит сообщение (по умолчанию)
|
|
1024
|
+
@event_router.event_handler("collect_phone")
|
|
1025
|
+
async def save_phone(user_id: int, phone: str):
|
|
1026
|
+
# Сохраняем телефон
|
|
1027
|
+
# ИИ автоматически отправит сообщение после выполнения
|
|
1028
|
+
return {"status": "success"}
|
|
1029
|
+
|
|
1030
|
+
# ИИ НЕ отправит сообщение
|
|
1031
|
+
@event_router.event_handler("collect_name", send_ai_response=False)
|
|
1032
|
+
async def save_name(user_id: int, name: str):
|
|
1033
|
+
# Сохраняем имя
|
|
1034
|
+
await send_message_by_human(user_id, f"✅ Имя {name} сохранено")
|
|
1035
|
+
# ИИ не будет отправлять свое сообщение
|
|
1036
|
+
return {"status": "success"}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
826
1039
|
### `once_only=True`
|
|
827
1040
|
|
|
828
1041
|
Гарантирует выполнение события только 1 раз для пользователя:
|
|
@@ -7,7 +7,6 @@ smart_bot_factory/admin/__init__.py,sha256=vdsMTpt_LiXkY-awFu_X9e2Zt7CV50PwmsWkF
|
|
|
7
7
|
smart_bot_factory/admin/admin_events.py,sha256=QCosyTbJgrU8daWSK_bQgf8UZoJSIrV6xyO0R3XV2j0,43289
|
|
8
8
|
smart_bot_factory/admin/admin_logic.py,sha256=vPkNk86bdPsjNUNlZ3qfKtbRr9UuJy2oG54cYUGGNmg,23107
|
|
9
9
|
smart_bot_factory/admin/admin_manager.py,sha256=xlyG9mIjPmtUhS4E9lp36T7o5Kfp5PZpJ-r1QjnSn5g,6394
|
|
10
|
-
smart_bot_factory/admin/admin_migration.sql,sha256=kleMPJBSe2Z7ZZz7rNyOX_yoh4GZivGesqAX90U5PGs,5667
|
|
11
10
|
smart_bot_factory/admin/admin_tester.py,sha256=PGFpf7fmD5Wxea31xR2ZM_A_QpvrB73gsbxvUrHQBkg,6463
|
|
12
11
|
smart_bot_factory/admin/timeout_checker.py,sha256=TzA2FGrxwE8fuhKerGnGrt4qYMEZdIR8x3SQAnIW5YQ,24490
|
|
13
12
|
smart_bot_factory/aiogram_calendar/__init__.py,sha256=_IzB_HJIZuMs__7xBBsYVG20GbRNFw2VowvhOEyiGGc,349
|
|
@@ -30,9 +29,9 @@ smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml,sh
|
|
|
30
29
|
smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml,sha256=bzDulOU4a2LyWlcHzlQU8GYhOky2WTfyizGfjX4ioMY,2436
|
|
31
30
|
smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt,sha256=Db21Mm0r8SBWFdX9EeIF2FZtLQ2cvuwVlSRJd2KEYCg,922
|
|
32
31
|
smart_bot_factory/configs/growthmed-october-24/welcome_file/Чек лист по 152ФЗ и 323ФЗ для медицины.pdf,sha256=BiAiQHNnQXJPMsks9AeL6s0beEjRFkRMJLMlAn4WorA,5284954
|
|
33
|
-
smart_bot_factory/core/bot_utils.py,sha256=
|
|
32
|
+
smart_bot_factory/core/bot_utils.py,sha256=vblk94k8cXvc-3iS3UOZvnU2NtXXKYgt3vzkioca-ac,48395
|
|
34
33
|
smart_bot_factory/core/conversation_manager.py,sha256=eoHL7MCEz68DRvTVwRwZgf2PWwGv4T6J9D-I-thETi8,28289
|
|
35
|
-
smart_bot_factory/core/decorators.py,sha256=
|
|
34
|
+
smart_bot_factory/core/decorators.py,sha256=mYWiN9B0lrgV3uRAaFVkJTCKWZyTpVNM_AneuQcqifA,99872
|
|
36
35
|
smart_bot_factory/core/message_sender.py,sha256=0-SQcK4W1x__VgvyaeVRuFlXcxV56TsR_nNK07Nr4b4,32763
|
|
37
36
|
smart_bot_factory/core/router.py,sha256=ji7rzpuKaO8yKaxFW58WhlgG5ExXlbCgqCTONxAyqL4,15022
|
|
38
37
|
smart_bot_factory/core/router_manager.py,sha256=dUwesog-oHk1U2EDdS8p0e4MTSkwtx5_qXn6nrJ9l9I,9700
|
|
@@ -40,10 +39,11 @@ smart_bot_factory/core/states.py,sha256=L8qp1UmYFuxTN5U9tY076rDuKgxtFbpSGqBpva2e
|
|
|
40
39
|
smart_bot_factory/creation/__init__.py,sha256=IgDk8GDS3pg7Pw_Et41J33ZmeZIU5dRwQdTmYKXfJfE,128
|
|
41
40
|
smart_bot_factory/creation/bot_builder.py,sha256=yGRmOPD7qCMbhcBiltHWISoKxWx8eqjDSnZXpwhqnUs,43115
|
|
42
41
|
smart_bot_factory/creation/bot_testing.py,sha256=JDWXyJfZmbgo-DLdAPk8Sd9FiehtHHa4sLD17lBrTOc,55669
|
|
42
|
+
smart_bot_factory/dashboard/__init__.py,sha256=bDBOWQbcAL1Bmz4KVFouAKg8FN-c6EsC_-YthTt_mP4,100
|
|
43
43
|
smart_bot_factory/event/__init__.py,sha256=hPL449RULIOB-OXv1ZbGNiHctAYaOMUqhSWGPrDHYBM,212
|
|
44
|
-
smart_bot_factory/handlers/handlers.py,sha256=
|
|
44
|
+
smart_bot_factory/handlers/handlers.py,sha256=haxyVvFuyMWvfGCOiAvXPrez96bchoHnwQEEURMqiMI,62409
|
|
45
45
|
smart_bot_factory/integrations/openai_client.py,sha256=fwaJpwojFdLBWChcFWpFGOHK9upG-nCIwDochkCRRlY,24291
|
|
46
|
-
smart_bot_factory/integrations/supabase_client.py,sha256=
|
|
46
|
+
smart_bot_factory/integrations/supabase_client.py,sha256=XV1_caDAADnXLwCM7CkUdGfUNBNHBHYz-HBPSYmdGv4,63653
|
|
47
47
|
smart_bot_factory/message/__init__.py,sha256=-ehDZweUc3uKgmLLxFVsD-KWrDtnHpHms7pCrDelWo0,1950
|
|
48
48
|
smart_bot_factory/router/__init__.py,sha256=5gEbpG3eylOyow5NmidzGUy0K-AZq7RhYLVu9OaUT6c,270
|
|
49
49
|
smart_bot_factory/supabase/__init__.py,sha256=XmZP6yM9ffERM5ddAWyJnrNzEhCYtMu3AcjVCi1rOf8,179
|
|
@@ -52,8 +52,8 @@ smart_bot_factory/utils/__init__.py,sha256=UhsJXEHfrIK8h1AHsroHSwAriijk-LvnqLyvg
|
|
|
52
52
|
smart_bot_factory/utils/debug_routing.py,sha256=BOoDhKBg7UXe5uHQxRk3TSfPfLPOFqt0N7lAo6kjCOo,4719
|
|
53
53
|
smart_bot_factory/utils/prompt_loader.py,sha256=HS_6Vf-qvRBkhvyzu-HNVS1swFgmqWOKNNv0F6We_AQ,20060
|
|
54
54
|
smart_bot_factory/utils/user_prompt_loader.py,sha256=dk6P0X_3UcNqxjRtuIvb0LcPrp03zIIsstZwdmeCPaE,2519
|
|
55
|
-
smart_bot_factory-0.3.
|
|
56
|
-
smart_bot_factory-0.3.
|
|
57
|
-
smart_bot_factory-0.3.
|
|
58
|
-
smart_bot_factory-0.3.
|
|
59
|
-
smart_bot_factory-0.3.
|
|
55
|
+
smart_bot_factory-0.3.7.dist-info/METADATA,sha256=cfOUXXewYMK7EomidcIP78W_yjjCnWHtmQ7efPx8FMA,40662
|
|
56
|
+
smart_bot_factory-0.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
57
|
+
smart_bot_factory-0.3.7.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
|
|
58
|
+
smart_bot_factory-0.3.7.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
|
|
59
|
+
smart_bot_factory-0.3.7.dist-info/RECORD,,
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
-- ФИНАЛЬНАЯ МИГРАЦИЯ АДМИНСКОЙ СИСТЕМЫ
|
|
2
|
-
-- Выполните ПОСЛЕ исправления уникальности telegram_id
|
|
3
|
-
|
|
4
|
-
-- 1. Расширяем существующие таблицы
|
|
5
|
-
ALTER TABLE sales_chat_sessions
|
|
6
|
-
ADD COLUMN IF NOT EXISTS current_stage TEXT,
|
|
7
|
-
ADD COLUMN IF NOT EXISTS lead_quality_score INTEGER;
|
|
8
|
-
|
|
9
|
-
ALTER TABLE sales_messages
|
|
10
|
-
ADD COLUMN IF NOT EXISTS ai_metadata JSONB DEFAULT '{}'::jsonb;
|
|
11
|
-
|
|
12
|
-
-- 2. Создаем функцию обновления updated_at
|
|
13
|
-
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
14
|
-
RETURNS TRIGGER AS $$
|
|
15
|
-
BEGIN
|
|
16
|
-
NEW.updated_at = NOW();
|
|
17
|
-
RETURN NEW;
|
|
18
|
-
END;
|
|
19
|
-
$$ LANGUAGE plpgsql;
|
|
20
|
-
|
|
21
|
-
-- 3. Таблица администраторов
|
|
22
|
-
CREATE TABLE IF NOT EXISTS sales_admins (
|
|
23
|
-
id BIGSERIAL PRIMARY KEY,
|
|
24
|
-
telegram_id BIGINT UNIQUE NOT NULL,
|
|
25
|
-
username TEXT,
|
|
26
|
-
first_name TEXT,
|
|
27
|
-
last_name TEXT,
|
|
28
|
-
role TEXT DEFAULT 'admin',
|
|
29
|
-
is_active BOOLEAN DEFAULT TRUE,
|
|
30
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
31
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
-- 4. Диалоги админов с пользователями
|
|
35
|
-
CREATE TABLE IF NOT EXISTS admin_user_conversations (
|
|
36
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
37
|
-
admin_id BIGINT REFERENCES sales_admins(telegram_id) ON DELETE CASCADE,
|
|
38
|
-
user_id BIGINT REFERENCES sales_users(telegram_id) ON DELETE CASCADE,
|
|
39
|
-
session_id UUID REFERENCES sales_chat_sessions(id) ON DELETE CASCADE,
|
|
40
|
-
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'completed')),
|
|
41
|
-
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
42
|
-
ended_at TIMESTAMP WITH TIME ZONE,
|
|
43
|
-
auto_end_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + INTERVAL '30 minutes'
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
-- 5. События из ответов ИИ
|
|
47
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
48
|
-
id BIGSERIAL PRIMARY KEY,
|
|
49
|
-
session_id UUID REFERENCES sales_chat_sessions(id) ON DELETE CASCADE,
|
|
50
|
-
event_type TEXT NOT NULL,
|
|
51
|
-
event_info TEXT,
|
|
52
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
53
|
-
notified_admins BIGINT[] DEFAULT '{}'
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
-- 6. Индексы
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_sales_admins_telegram_id ON sales_admins(telegram_id);
|
|
58
|
-
CREATE INDEX IF NOT EXISTS idx_admin_conversations_status ON admin_user_conversations(status);
|
|
59
|
-
CREATE INDEX IF NOT EXISTS idx_admin_conversations_admin ON admin_user_conversations(admin_id);
|
|
60
|
-
CREATE INDEX IF NOT EXISTS idx_admin_conversations_user ON admin_user_conversations(user_id);
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_type ON session_events(event_type);
|
|
62
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id);
|
|
63
|
-
CREATE INDEX IF NOT EXISTS idx_sales_chat_sessions_stage ON sales_chat_sessions(current_stage);
|
|
64
|
-
CREATE INDEX IF NOT EXISTS idx_sales_messages_metadata ON sales_messages USING gin(ai_metadata);
|
|
65
|
-
|
|
66
|
-
-- 7. Триггер для sales_admins
|
|
67
|
-
DROP TRIGGER IF EXISTS update_sales_admins_updated_at ON sales_admins;
|
|
68
|
-
CREATE TRIGGER update_sales_admins_updated_at
|
|
69
|
-
BEFORE UPDATE ON sales_admins
|
|
70
|
-
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
71
|
-
|
|
72
|
-
-- 8. Функция завершения просроченных диалогов
|
|
73
|
-
CREATE OR REPLACE FUNCTION end_expired_admin_conversations()
|
|
74
|
-
RETURNS INTEGER AS $$
|
|
75
|
-
DECLARE
|
|
76
|
-
ended_count INTEGER;
|
|
77
|
-
BEGIN
|
|
78
|
-
UPDATE admin_user_conversations
|
|
79
|
-
SET status = 'completed', ended_at = NOW()
|
|
80
|
-
WHERE status = 'active' AND auto_end_at < NOW();
|
|
81
|
-
|
|
82
|
-
GET DIAGNOSTICS ended_count = ROW_COUNT;
|
|
83
|
-
RETURN ended_count;
|
|
84
|
-
END;
|
|
85
|
-
$$ LANGUAGE plpgsql;
|
|
86
|
-
|
|
87
|
-
-- 9. Представления для аналитики
|
|
88
|
-
CREATE OR REPLACE VIEW funnel_stats AS
|
|
89
|
-
SELECT
|
|
90
|
-
current_stage,
|
|
91
|
-
COUNT(*) as count,
|
|
92
|
-
AVG(lead_quality_score) as avg_quality,
|
|
93
|
-
ROUND(COUNT(*) * 100.0 / NULLIF(SUM(COUNT(*)) OVER(), 0), 1) as percentage
|
|
94
|
-
FROM sales_chat_sessions
|
|
95
|
-
WHERE created_at > NOW() - INTERVAL '7 days'
|
|
96
|
-
AND current_stage IS NOT NULL
|
|
97
|
-
GROUP BY current_stage;
|
|
98
|
-
|
|
99
|
-
CREATE OR REPLACE VIEW daily_events AS
|
|
100
|
-
SELECT
|
|
101
|
-
DATE(created_at) as event_date,
|
|
102
|
-
event_type,
|
|
103
|
-
COUNT(*) as count
|
|
104
|
-
FROM session_events
|
|
105
|
-
WHERE created_at > NOW() - INTERVAL '30 days'
|
|
106
|
-
GROUP BY DATE(created_at), event_type
|
|
107
|
-
ORDER BY event_date DESC, event_type;
|
|
108
|
-
|
|
109
|
-
-- 10. RLS политики
|
|
110
|
-
ALTER TABLE sales_admins ENABLE ROW LEVEL SECURITY;
|
|
111
|
-
ALTER TABLE admin_user_conversations ENABLE ROW LEVEL SECURITY;
|
|
112
|
-
ALTER TABLE session_events ENABLE ROW LEVEL SECURITY;
|
|
113
|
-
|
|
114
|
-
DROP POLICY IF EXISTS "Service role can manage all admins" ON sales_admins;
|
|
115
|
-
DROP POLICY IF EXISTS "Service role can manage all conversations" ON admin_user_conversations;
|
|
116
|
-
DROP POLICY IF EXISTS "Service role can manage all events" ON session_events;
|
|
117
|
-
|
|
118
|
-
CREATE POLICY "Service role can manage all admins" ON sales_admins
|
|
119
|
-
FOR ALL USING (current_setting('role') = 'service_role');
|
|
120
|
-
|
|
121
|
-
CREATE POLICY "Service role can manage all conversations" ON admin_user_conversations
|
|
122
|
-
FOR ALL USING (current_setting('role') = 'service_role');
|
|
123
|
-
|
|
124
|
-
CREATE POLICY "Service role can manage all events" ON session_events
|
|
125
|
-
FOR ALL USING (current_setting('role') = 'service_role');
|
|
126
|
-
|
|
127
|
-
-- 11. Комментарии
|
|
128
|
-
COMMENT ON TABLE sales_admins IS 'Администраторы бота';
|
|
129
|
-
COMMENT ON TABLE admin_user_conversations IS 'Активные диалоги админов с пользователями';
|
|
130
|
-
COMMENT ON TABLE session_events IS 'События из ответов ИИ для уведомлений';
|
|
131
|
-
|
|
132
|
-
-- Финальная проверка
|
|
133
|
-
SELECT
|
|
134
|
-
'АДМИНСКАЯ СИСТЕМА СОЗДАНА!' AS status,
|
|
135
|
-
(SELECT COUNT(*) FROM information_schema.tables
|
|
136
|
-
WHERE table_name IN ('sales_admins', 'admin_user_conversations', 'session_events')) AS tables_created;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|