smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.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/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +474 -332
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1129 -749
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +676 -472
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +928 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime, timedelta, timezone
|
|
5
|
-
from typing import
|
|
6
|
-
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
7
|
from postgrest.exceptions import APIError
|
|
8
|
+
from supabase import Client, create_client
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
12
|
+
|
|
11
13
|
class SupabaseClient:
|
|
12
14
|
"""Клиент для работы с Supabase с поддержкой bot_id для мультиботовой архитектуры"""
|
|
13
|
-
|
|
15
|
+
|
|
14
16
|
def __init__(self, url: str, key: str, bot_id: str = None):
|
|
15
17
|
"""
|
|
16
18
|
Инициализация клиента Supabase
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
Args:
|
|
19
21
|
url: URL Supabase проекта
|
|
20
22
|
key: API ключ Supabase
|
|
@@ -24,211 +26,264 @@ class SupabaseClient:
|
|
|
24
26
|
self.key = key
|
|
25
27
|
self.bot_id = bot_id # 🆕 Теперь опционально!
|
|
26
28
|
self.client: Optional[Client] = None
|
|
27
|
-
|
|
29
|
+
|
|
28
30
|
if self.bot_id:
|
|
29
31
|
logger.info(f"Инициализация SupabaseClient для bot_id: {self.bot_id}")
|
|
30
32
|
else:
|
|
31
|
-
logger.warning(
|
|
32
|
-
|
|
33
|
+
logger.warning(
|
|
34
|
+
"SupabaseClient инициализирован БЕЗ bot_id - мультиботовая изоляция отключена"
|
|
35
|
+
)
|
|
36
|
+
|
|
33
37
|
async def initialize(self):
|
|
34
38
|
"""Инициализация клиента Supabase"""
|
|
35
39
|
try:
|
|
36
40
|
self.client = create_client(self.url, self.key)
|
|
37
|
-
logger.info(
|
|
41
|
+
logger.info(
|
|
42
|
+
f"Supabase client инициализирован{f' для bot_id: {self.bot_id}' if self.bot_id else ''}"
|
|
43
|
+
)
|
|
38
44
|
except Exception as e:
|
|
39
45
|
logger.error(f"Ошибка инициализации Supabase client: {e}")
|
|
40
46
|
raise
|
|
41
|
-
|
|
47
|
+
|
|
42
48
|
async def create_or_get_user(self, user_data: Dict[str, Any]) -> int:
|
|
43
49
|
"""Создает или получает пользователя с учетом bot_id (если указан)"""
|
|
44
50
|
try:
|
|
45
51
|
# 🆕 Если bot_id указан, фильтруем по нему
|
|
46
|
-
query =
|
|
47
|
-
|
|
52
|
+
query = (
|
|
53
|
+
self.client.table("sales_users")
|
|
54
|
+
.select("telegram_id")
|
|
55
|
+
.eq("telegram_id", user_data["telegram_id"])
|
|
48
56
|
)
|
|
49
57
|
if self.bot_id:
|
|
50
|
-
query = query.eq(
|
|
51
|
-
|
|
58
|
+
query = query.eq("bot_id", self.bot_id)
|
|
59
|
+
|
|
52
60
|
response = query.execute()
|
|
53
|
-
|
|
61
|
+
|
|
54
62
|
if response.data:
|
|
55
63
|
# Получаем текущие данные пользователя для мержинга UTM и сегментов
|
|
56
|
-
existing_user_query =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
existing_user_query = (
|
|
65
|
+
self.client.table("sales_users")
|
|
66
|
+
.select(
|
|
67
|
+
"source", "medium", "campaign", "content", "term", "segments"
|
|
68
|
+
)
|
|
69
|
+
.eq("telegram_id", user_data["telegram_id"])
|
|
70
|
+
)
|
|
71
|
+
|
|
60
72
|
if self.bot_id:
|
|
61
|
-
existing_user_query = existing_user_query.eq(
|
|
62
|
-
|
|
73
|
+
existing_user_query = existing_user_query.eq("bot_id", self.bot_id)
|
|
74
|
+
|
|
63
75
|
existing_response = existing_user_query.execute()
|
|
64
|
-
existing_utm =
|
|
65
|
-
|
|
76
|
+
existing_utm = (
|
|
77
|
+
existing_response.data[0] if existing_response.data else {}
|
|
78
|
+
)
|
|
79
|
+
|
|
66
80
|
# Формируем данные для обновления
|
|
67
81
|
update_data = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
"username": user_data.get("username"),
|
|
83
|
+
"first_name": user_data.get("first_name"),
|
|
84
|
+
"last_name": user_data.get("last_name"),
|
|
85
|
+
"language_code": user_data.get("language_code"),
|
|
86
|
+
"updated_at": datetime.now().isoformat(),
|
|
87
|
+
"is_active": True,
|
|
74
88
|
}
|
|
75
|
-
|
|
89
|
+
|
|
76
90
|
# Мержим UTM данные: обновляем только если новое значение не None
|
|
77
|
-
utm_fields = [
|
|
91
|
+
utm_fields = ["source", "medium", "campaign", "content", "term"]
|
|
78
92
|
for field in utm_fields:
|
|
79
93
|
new_value = user_data.get(field)
|
|
80
94
|
if new_value is not None:
|
|
81
95
|
# Есть новое значение - обновляем
|
|
82
96
|
update_data[field] = new_value
|
|
83
97
|
if existing_utm.get(field) != new_value:
|
|
84
|
-
logger.info(
|
|
98
|
+
logger.info(
|
|
99
|
+
f"📊 UTM обновление: {field} = '{new_value}' (было: '{existing_utm.get(field)}')"
|
|
100
|
+
)
|
|
85
101
|
else:
|
|
86
102
|
# Нового значения нет - сохраняем старое
|
|
87
103
|
update_data[field] = existing_utm.get(field)
|
|
88
|
-
|
|
104
|
+
|
|
89
105
|
# Обрабатываем сегменты с накоплением через запятую
|
|
90
|
-
new_segment = user_data.get(
|
|
106
|
+
new_segment = user_data.get("segment")
|
|
91
107
|
if new_segment:
|
|
92
|
-
existing_segments = existing_utm.get(
|
|
108
|
+
existing_segments = existing_utm.get("segments", "") or ""
|
|
93
109
|
if existing_segments:
|
|
94
110
|
# Разбираем существующие сегменты
|
|
95
|
-
segments_list = [
|
|
111
|
+
segments_list = [
|
|
112
|
+
s.strip() for s in existing_segments.split(",") if s.strip()
|
|
113
|
+
]
|
|
96
114
|
# Добавляем новый сегмент, если его еще нет
|
|
97
115
|
if new_segment not in segments_list:
|
|
98
116
|
segments_list.append(new_segment)
|
|
99
|
-
update_data[
|
|
100
|
-
logger.info(
|
|
117
|
+
update_data["segments"] = ", ".join(segments_list)
|
|
118
|
+
logger.info(
|
|
119
|
+
f"📊 Сегмент добавлен: '{new_segment}' (было: '{existing_segments}')"
|
|
120
|
+
)
|
|
101
121
|
else:
|
|
102
|
-
update_data[
|
|
122
|
+
update_data["segments"] = existing_segments
|
|
103
123
|
logger.info(f"📊 Сегмент '{new_segment}' уже существует")
|
|
104
124
|
else:
|
|
105
125
|
# Первый сегмент
|
|
106
|
-
update_data[
|
|
126
|
+
update_data["segments"] = new_segment
|
|
107
127
|
logger.info(f"📊 Первый сегмент добавлен: '{new_segment}'")
|
|
108
128
|
else:
|
|
109
129
|
# Нового сегмента нет - сохраняем старое значение
|
|
110
|
-
update_data[
|
|
111
|
-
|
|
130
|
+
update_data["segments"] = existing_utm.get("segments")
|
|
131
|
+
|
|
112
132
|
# Обновляем пользователя
|
|
113
|
-
update_query =
|
|
114
|
-
|
|
133
|
+
update_query = (
|
|
134
|
+
self.client.table("sales_users")
|
|
135
|
+
.update(update_data)
|
|
136
|
+
.eq("telegram_id", user_data["telegram_id"])
|
|
137
|
+
)
|
|
138
|
+
|
|
115
139
|
if self.bot_id:
|
|
116
|
-
update_query = update_query.eq(
|
|
117
|
-
|
|
140
|
+
update_query = update_query.eq("bot_id", self.bot_id)
|
|
141
|
+
|
|
118
142
|
update_query.execute()
|
|
119
|
-
|
|
120
|
-
logger.info(
|
|
121
|
-
|
|
143
|
+
|
|
144
|
+
logger.info(
|
|
145
|
+
f"Обновлен пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
146
|
+
)
|
|
147
|
+
return user_data["telegram_id"]
|
|
122
148
|
else:
|
|
123
149
|
# 🆕 Создаем нового пользователя с bot_id (если указан)
|
|
124
150
|
user_insert_data = {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
"telegram_id": user_data["telegram_id"],
|
|
152
|
+
"username": user_data.get("username"),
|
|
153
|
+
"first_name": user_data.get("first_name"),
|
|
154
|
+
"last_name": user_data.get("last_name"),
|
|
155
|
+
"language_code": user_data.get("language_code"),
|
|
156
|
+
"is_active": True,
|
|
157
|
+
"source": user_data.get("source"),
|
|
158
|
+
"medium": user_data.get("medium"),
|
|
159
|
+
"campaign": user_data.get("campaign"),
|
|
160
|
+
"content": user_data.get("content"),
|
|
161
|
+
"term": user_data.get("term"),
|
|
162
|
+
"segments": user_data.get("segment"), # Первый сегмент при создании
|
|
137
163
|
}
|
|
138
164
|
if self.bot_id:
|
|
139
|
-
user_insert_data[
|
|
140
|
-
|
|
141
|
-
response =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
user_insert_data["bot_id"] = self.bot_id
|
|
166
|
+
|
|
167
|
+
response = (
|
|
168
|
+
self.client.table("sales_users").insert(user_insert_data).execute()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if user_data.get("segment"):
|
|
172
|
+
logger.info(
|
|
173
|
+
f"Создан новый пользователь {user_data['telegram_id']} с сегментом '{user_data.get('segment')}'{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
174
|
+
)
|
|
145
175
|
else:
|
|
146
|
-
logger.info(
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
logger.info(
|
|
177
|
+
f"Создан новый пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
178
|
+
)
|
|
179
|
+
return user_data["telegram_id"]
|
|
180
|
+
|
|
149
181
|
except APIError as e:
|
|
150
182
|
logger.error(f"Ошибка при работе с пользователем: {e}")
|
|
151
183
|
raise
|
|
152
|
-
|
|
153
|
-
async def create_chat_session(
|
|
184
|
+
|
|
185
|
+
async def create_chat_session(
|
|
186
|
+
self, user_data: Dict[str, Any], system_prompt: str
|
|
187
|
+
) -> str:
|
|
154
188
|
"""Создает новую сессию чата с учетом bot_id (если указан)"""
|
|
155
189
|
try:
|
|
156
190
|
# Создаем или обновляем пользователя
|
|
157
191
|
user_id = await self.create_or_get_user(user_data)
|
|
158
|
-
|
|
192
|
+
|
|
159
193
|
# 🆕 Завершаем активные сессии пользователя (с учетом bot_id)
|
|
160
194
|
await self.close_active_sessions(user_id)
|
|
161
|
-
|
|
195
|
+
|
|
162
196
|
# 🆕 Создаем новую сессию с bot_id (если указан)
|
|
163
197
|
session_data = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
198
|
+
"user_id": user_id,
|
|
199
|
+
"system_prompt": system_prompt,
|
|
200
|
+
"status": "active",
|
|
201
|
+
"current_stage": "introduction",
|
|
202
|
+
"lead_quality_score": 5,
|
|
203
|
+
"metadata": {
|
|
204
|
+
"user_agent": user_data.get("user_agent", ""),
|
|
205
|
+
"start_timestamp": datetime.now().isoformat(),
|
|
206
|
+
},
|
|
173
207
|
}
|
|
174
208
|
if self.bot_id:
|
|
175
|
-
session_data[
|
|
176
|
-
session_data[
|
|
177
|
-
|
|
178
|
-
response =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
209
|
+
session_data["bot_id"] = self.bot_id
|
|
210
|
+
session_data["metadata"]["bot_id"] = self.bot_id
|
|
211
|
+
|
|
212
|
+
response = (
|
|
213
|
+
self.client.table("sales_chat_sessions").insert(session_data).execute()
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
session_id = response.data[0]["id"]
|
|
217
|
+
|
|
182
218
|
# Создаем запись аналитики
|
|
183
219
|
await self.create_session_analytics(session_id)
|
|
184
|
-
|
|
185
|
-
logger.info(
|
|
220
|
+
|
|
221
|
+
logger.info(
|
|
222
|
+
f"Создана новая сессия {session_id} для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
223
|
+
)
|
|
186
224
|
return session_id
|
|
187
|
-
|
|
225
|
+
|
|
188
226
|
except APIError as e:
|
|
189
227
|
logger.error(f"Ошибка при создании сессии: {e}")
|
|
190
228
|
raise
|
|
191
|
-
|
|
229
|
+
|
|
192
230
|
async def close_active_sessions(self, user_id: int):
|
|
193
231
|
"""Закрывает активные сессии пользователя с учетом bot_id (если указан)"""
|
|
194
232
|
try:
|
|
195
233
|
# 🆕 Закрываем только сессии этого бота (если bot_id указан)
|
|
196
|
-
query =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
234
|
+
query = (
|
|
235
|
+
self.client.table("sales_chat_sessions")
|
|
236
|
+
.update(
|
|
237
|
+
{"status": "completed", "updated_at": datetime.now().isoformat()}
|
|
238
|
+
)
|
|
239
|
+
.eq("user_id", user_id)
|
|
240
|
+
.eq("status", "active")
|
|
241
|
+
)
|
|
242
|
+
|
|
201
243
|
if self.bot_id:
|
|
202
|
-
query = query.eq(
|
|
203
|
-
|
|
244
|
+
query = query.eq("bot_id", self.bot_id)
|
|
245
|
+
|
|
204
246
|
query.execute()
|
|
205
|
-
|
|
206
|
-
logger.info(
|
|
207
|
-
|
|
247
|
+
|
|
248
|
+
logger.info(
|
|
249
|
+
f"Закрыты активные сессии для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
250
|
+
)
|
|
251
|
+
|
|
208
252
|
except APIError as e:
|
|
209
253
|
logger.error(f"Ошибка при закрытии сессий: {e}")
|
|
210
254
|
raise
|
|
211
|
-
|
|
255
|
+
|
|
212
256
|
async def get_active_session(self, telegram_id: int) -> Optional[Dict[str, Any]]:
|
|
213
257
|
"""Получает активную сессию пользователя с учетом bot_id (если указан)"""
|
|
214
258
|
try:
|
|
215
259
|
# 🆕 Ищем активную сессию с учетом bot_id (если указан)
|
|
216
|
-
query =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
260
|
+
query = (
|
|
261
|
+
self.client.table("sales_chat_sessions")
|
|
262
|
+
.select(
|
|
263
|
+
"id",
|
|
264
|
+
"system_prompt",
|
|
265
|
+
"created_at",
|
|
266
|
+
"current_stage",
|
|
267
|
+
"lead_quality_score",
|
|
268
|
+
)
|
|
269
|
+
.eq("user_id", telegram_id)
|
|
270
|
+
.eq("status", "active")
|
|
271
|
+
)
|
|
272
|
+
|
|
220
273
|
if self.bot_id:
|
|
221
|
-
query = query.eq(
|
|
222
|
-
|
|
274
|
+
query = query.eq("bot_id", self.bot_id)
|
|
275
|
+
|
|
223
276
|
response = query.execute()
|
|
224
|
-
|
|
277
|
+
|
|
225
278
|
if response.data:
|
|
226
279
|
session_info = response.data[0]
|
|
227
|
-
logger.info(
|
|
280
|
+
logger.info(
|
|
281
|
+
f"Найдена активная сессия {session_info['id']} для пользователя {telegram_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
282
|
+
)
|
|
228
283
|
return session_info
|
|
229
|
-
|
|
284
|
+
|
|
230
285
|
return None
|
|
231
|
-
|
|
286
|
+
|
|
232
287
|
except APIError as e:
|
|
233
288
|
logger.error(f"Ошибка при поиске активной сессии: {e}")
|
|
234
289
|
return None
|
|
@@ -236,825 +291,1057 @@ class SupabaseClient:
|
|
|
236
291
|
async def create_session_analytics(self, session_id: str):
|
|
237
292
|
"""Создает запись аналитики для сессии"""
|
|
238
293
|
try:
|
|
239
|
-
self.client.table(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
self.client.table("sales_session_analytics").insert(
|
|
295
|
+
{
|
|
296
|
+
"session_id": session_id,
|
|
297
|
+
"total_messages": 0,
|
|
298
|
+
"total_tokens": 0,
|
|
299
|
+
"average_response_time_ms": 0,
|
|
300
|
+
"conversion_stage": "initial",
|
|
301
|
+
"lead_quality_score": 5,
|
|
302
|
+
}
|
|
303
|
+
).execute()
|
|
304
|
+
|
|
248
305
|
logger.debug(f"Создана аналитика для сессии {session_id}")
|
|
249
|
-
|
|
306
|
+
|
|
250
307
|
except APIError as e:
|
|
251
308
|
logger.error(f"Ошибка при создании аналитики: {e}")
|
|
252
309
|
raise
|
|
253
|
-
|
|
310
|
+
|
|
254
311
|
async def add_message(
|
|
255
|
-
self,
|
|
256
|
-
session_id: str,
|
|
257
|
-
role: str,
|
|
258
|
-
content: str,
|
|
259
|
-
message_type: str =
|
|
312
|
+
self,
|
|
313
|
+
session_id: str,
|
|
314
|
+
role: str,
|
|
315
|
+
content: str,
|
|
316
|
+
message_type: str = "text",
|
|
260
317
|
tokens_used: int = 0,
|
|
261
318
|
processing_time_ms: int = 0,
|
|
262
319
|
metadata: Dict[str, Any] = None,
|
|
263
|
-
ai_metadata: Dict[str, Any] = None
|
|
320
|
+
ai_metadata: Dict[str, Any] = None,
|
|
264
321
|
) -> int:
|
|
265
322
|
"""Добавляет сообщение в базу данных"""
|
|
266
323
|
try:
|
|
267
|
-
response =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
324
|
+
response = (
|
|
325
|
+
self.client.table("sales_messages")
|
|
326
|
+
.insert(
|
|
327
|
+
{
|
|
328
|
+
"session_id": session_id,
|
|
329
|
+
"role": role,
|
|
330
|
+
"content": content,
|
|
331
|
+
"message_type": message_type,
|
|
332
|
+
"tokens_used": tokens_used,
|
|
333
|
+
"processing_time_ms": processing_time_ms,
|
|
334
|
+
"metadata": metadata or {},
|
|
335
|
+
"ai_metadata": ai_metadata or {},
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
.execute()
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
message_id = response.data[0]["id"]
|
|
342
|
+
|
|
280
343
|
# Обновляем аналитику сессии
|
|
281
|
-
await self.update_session_analytics(
|
|
282
|
-
|
|
344
|
+
await self.update_session_analytics(
|
|
345
|
+
session_id, tokens_used, processing_time_ms
|
|
346
|
+
)
|
|
347
|
+
|
|
283
348
|
logger.debug(f"Добавлено сообщение {message_id} в сессию {session_id}")
|
|
284
349
|
return message_id
|
|
285
|
-
|
|
350
|
+
|
|
286
351
|
except APIError as e:
|
|
287
352
|
logger.error(f"Ошибка при добавлении сообщения: {e}")
|
|
288
353
|
raise
|
|
289
|
-
|
|
290
|
-
async def get_chat_history(
|
|
354
|
+
|
|
355
|
+
async def get_chat_history(
|
|
356
|
+
self, session_id: str, limit: int = 50
|
|
357
|
+
) -> List[Dict[str, Any]]:
|
|
291
358
|
"""Получает историю сообщений для сессии"""
|
|
292
359
|
try:
|
|
293
|
-
response =
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
360
|
+
response = (
|
|
361
|
+
self.client.table("sales_messages")
|
|
362
|
+
.select(
|
|
363
|
+
"id",
|
|
364
|
+
"role",
|
|
365
|
+
"content",
|
|
366
|
+
"message_type",
|
|
367
|
+
"created_at",
|
|
368
|
+
"metadata",
|
|
369
|
+
"ai_metadata",
|
|
370
|
+
)
|
|
371
|
+
.eq("session_id", session_id)
|
|
372
|
+
.order("created_at", desc=True)
|
|
373
|
+
.limit(limit)
|
|
374
|
+
.execute()
|
|
375
|
+
)
|
|
376
|
+
|
|
297
377
|
# Фильтруем системные сообщения из истории
|
|
298
|
-
messages = [msg for msg in response.data if msg[
|
|
299
|
-
|
|
378
|
+
messages = [msg for msg in response.data if msg["role"] != "system"]
|
|
379
|
+
|
|
300
380
|
# Переворачиваем в хронологический порядок (старые -> новые)
|
|
301
381
|
messages.reverse()
|
|
302
|
-
|
|
382
|
+
|
|
303
383
|
logger.debug(f"Получено {len(messages)} сообщений для сессии {session_id}")
|
|
304
384
|
return messages
|
|
305
|
-
|
|
385
|
+
|
|
306
386
|
except APIError as e:
|
|
307
387
|
logger.error(f"Ошибка при получении истории: {e}")
|
|
308
388
|
raise
|
|
309
|
-
|
|
389
|
+
|
|
310
390
|
async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
311
391
|
"""Получает информацию о сессии с проверкой bot_id (если указан)"""
|
|
312
392
|
try:
|
|
313
|
-
response =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
393
|
+
response = (
|
|
394
|
+
self.client.table("sales_chat_sessions")
|
|
395
|
+
.select(
|
|
396
|
+
"id",
|
|
397
|
+
"user_id",
|
|
398
|
+
"bot_id",
|
|
399
|
+
"system_prompt",
|
|
400
|
+
"status",
|
|
401
|
+
"created_at",
|
|
402
|
+
"metadata",
|
|
403
|
+
"current_stage",
|
|
404
|
+
"lead_quality_score",
|
|
405
|
+
)
|
|
406
|
+
.eq("id", session_id)
|
|
407
|
+
.execute()
|
|
408
|
+
)
|
|
409
|
+
|
|
318
410
|
if response.data:
|
|
319
411
|
session = response.data[0]
|
|
320
412
|
# 🆕 Дополнительная проверка bot_id для безопасности (если указан)
|
|
321
|
-
if self.bot_id and session.get(
|
|
322
|
-
logger.warning(
|
|
413
|
+
if self.bot_id and session.get("bot_id") != self.bot_id:
|
|
414
|
+
logger.warning(
|
|
415
|
+
f"Попытка доступа к сессии {session_id} другого бота: {session.get('bot_id')} != {self.bot_id}"
|
|
416
|
+
)
|
|
323
417
|
return None
|
|
324
418
|
return session
|
|
325
419
|
return None
|
|
326
|
-
|
|
420
|
+
|
|
327
421
|
except APIError as e:
|
|
328
422
|
logger.error(f"Ошибка при получении информации о сессии: {e}")
|
|
329
423
|
raise
|
|
330
|
-
|
|
331
|
-
async def update_session_stage(
|
|
424
|
+
|
|
425
|
+
async def update_session_stage(
|
|
426
|
+
self, session_id: str, stage: str = None, quality_score: int = None
|
|
427
|
+
):
|
|
332
428
|
"""Обновляет этап сессии и качество лида"""
|
|
333
429
|
try:
|
|
334
|
-
update_data = {
|
|
335
|
-
|
|
430
|
+
update_data = {"updated_at": datetime.now().isoformat()}
|
|
431
|
+
|
|
336
432
|
if stage:
|
|
337
|
-
update_data[
|
|
433
|
+
update_data["current_stage"] = stage
|
|
338
434
|
if quality_score is not None:
|
|
339
|
-
update_data[
|
|
340
|
-
|
|
435
|
+
update_data["lead_quality_score"] = quality_score
|
|
436
|
+
|
|
341
437
|
# 🆕 Дополнительная проверка bot_id при обновлении (если указан)
|
|
342
438
|
if self.bot_id:
|
|
343
|
-
response =
|
|
344
|
-
|
|
345
|
-
|
|
439
|
+
response = (
|
|
440
|
+
self.client.table("sales_chat_sessions")
|
|
441
|
+
.select("bot_id")
|
|
442
|
+
.eq("id", session_id)
|
|
443
|
+
.execute()
|
|
444
|
+
)
|
|
445
|
+
if response.data and response.data[0].get("bot_id") != self.bot_id:
|
|
446
|
+
logger.warning(
|
|
447
|
+
f"Попытка обновления сессии {session_id} другого бота"
|
|
448
|
+
)
|
|
346
449
|
return
|
|
347
|
-
|
|
348
|
-
self.client.table(
|
|
349
|
-
|
|
450
|
+
|
|
451
|
+
self.client.table("sales_chat_sessions").update(update_data).eq(
|
|
452
|
+
"id", session_id
|
|
350
453
|
).execute()
|
|
351
|
-
|
|
352
|
-
logger.debug(
|
|
353
|
-
|
|
454
|
+
|
|
455
|
+
logger.debug(
|
|
456
|
+
f"Обновлен этап сессии {session_id}: stage={stage}, quality={quality_score}"
|
|
457
|
+
)
|
|
458
|
+
|
|
354
459
|
except APIError as e:
|
|
355
460
|
logger.error(f"Ошибка при обновлении этапа сессии: {e}")
|
|
356
461
|
raise
|
|
357
|
-
|
|
462
|
+
|
|
358
463
|
async def get_user_sessions(self, telegram_id: int) -> List[Dict[str, Any]]:
|
|
359
464
|
"""Получает все сессии пользователя с учетом bot_id (если указан)"""
|
|
360
465
|
try:
|
|
361
466
|
# 🆕 Получаем только сессии этого бота (если bot_id указан)
|
|
362
|
-
query =
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
467
|
+
query = (
|
|
468
|
+
self.client.table("sales_chat_sessions")
|
|
469
|
+
.select(
|
|
470
|
+
"id",
|
|
471
|
+
"status",
|
|
472
|
+
"created_at",
|
|
473
|
+
"updated_at",
|
|
474
|
+
"current_stage",
|
|
475
|
+
"lead_quality_score",
|
|
476
|
+
)
|
|
477
|
+
.eq("user_id", telegram_id)
|
|
478
|
+
.order("created_at", desc=True)
|
|
479
|
+
)
|
|
480
|
+
|
|
366
481
|
if self.bot_id:
|
|
367
|
-
query = query.eq(
|
|
368
|
-
|
|
482
|
+
query = query.eq("bot_id", self.bot_id)
|
|
483
|
+
|
|
369
484
|
response = query.execute()
|
|
370
485
|
return response.data
|
|
371
|
-
|
|
486
|
+
|
|
372
487
|
except APIError as e:
|
|
373
488
|
logger.error(f"Ошибка при получении сессий пользователя: {e}")
|
|
374
489
|
raise
|
|
375
|
-
|
|
490
|
+
|
|
376
491
|
# 🆕 Новые методы для админской системы с поддержкой bot_id
|
|
377
|
-
|
|
378
|
-
async def add_session_event(
|
|
492
|
+
|
|
493
|
+
async def add_session_event(
|
|
494
|
+
self, session_id: str, event_type: str, event_info: str
|
|
495
|
+
) -> int:
|
|
379
496
|
"""Добавляет событие в сессию"""
|
|
380
497
|
try:
|
|
381
|
-
response =
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
498
|
+
response = (
|
|
499
|
+
self.client.table("session_events")
|
|
500
|
+
.insert(
|
|
501
|
+
{
|
|
502
|
+
"session_id": session_id,
|
|
503
|
+
"event_type": event_type,
|
|
504
|
+
"event_info": event_info,
|
|
505
|
+
"notified_admins": [],
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
.execute()
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
event_id = response.data[0]["id"]
|
|
389
512
|
logger.info(f"Добавлено событие {event_type} для сессии {session_id}")
|
|
390
513
|
return event_id
|
|
391
|
-
|
|
514
|
+
|
|
392
515
|
except APIError as e:
|
|
393
516
|
logger.error(f"Ошибка при добавлении события: {e}")
|
|
394
517
|
raise
|
|
395
|
-
|
|
518
|
+
|
|
396
519
|
async def sync_admin(self, admin_data: Dict[str, Any]):
|
|
397
520
|
"""Синхронизирует админа в БД (админы общие для всех ботов)"""
|
|
398
521
|
try:
|
|
399
522
|
# Проверяем существует ли админ
|
|
400
|
-
response =
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
523
|
+
response = (
|
|
524
|
+
self.client.table("sales_admins")
|
|
525
|
+
.select("telegram_id")
|
|
526
|
+
.eq(
|
|
527
|
+
"telegram_id",
|
|
528
|
+
admin_data["telegram_id"],
|
|
529
|
+
)
|
|
530
|
+
.eq("bot_id", self.bot_id)
|
|
531
|
+
.execute()
|
|
532
|
+
)
|
|
533
|
+
|
|
404
534
|
if response.data:
|
|
405
535
|
# Обновляем существующего
|
|
406
|
-
self.client.table(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
536
|
+
self.client.table("sales_admins").update(
|
|
537
|
+
{
|
|
538
|
+
"username": admin_data.get("username"),
|
|
539
|
+
"first_name": admin_data.get("first_name"),
|
|
540
|
+
"last_name": admin_data.get("last_name"),
|
|
541
|
+
"is_active": True,
|
|
542
|
+
}
|
|
543
|
+
).eq("telegram_id", admin_data["telegram_id"]).eq(
|
|
544
|
+
"bot_id", self.bot_id
|
|
545
|
+
).execute()
|
|
546
|
+
|
|
413
547
|
logger.debug(f"Обновлен админ {admin_data['telegram_id']}")
|
|
414
548
|
else:
|
|
415
549
|
# Создаем нового
|
|
416
|
-
self.client.table(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
550
|
+
self.client.table("sales_admins").insert(
|
|
551
|
+
{
|
|
552
|
+
"telegram_id": admin_data["telegram_id"],
|
|
553
|
+
"bot_id": self.bot_id,
|
|
554
|
+
"username": admin_data.get("username"),
|
|
555
|
+
"first_name": admin_data.get("first_name"),
|
|
556
|
+
"last_name": admin_data.get("last_name"),
|
|
557
|
+
"role": "admin",
|
|
558
|
+
"is_active": True,
|
|
559
|
+
}
|
|
560
|
+
).execute()
|
|
561
|
+
|
|
426
562
|
logger.info(f"Создан новый админ {admin_data['telegram_id']}")
|
|
427
|
-
|
|
563
|
+
|
|
428
564
|
except APIError as e:
|
|
429
565
|
logger.error(f"Ошибка при синхронизации админа: {e}")
|
|
430
566
|
raise
|
|
431
|
-
|
|
432
|
-
async def start_admin_conversation(
|
|
567
|
+
|
|
568
|
+
async def start_admin_conversation(
|
|
569
|
+
self, admin_id: int, user_id: int, session_id: str
|
|
570
|
+
) -> int:
|
|
433
571
|
"""Начинает диалог между админом и пользователем"""
|
|
434
572
|
try:
|
|
435
573
|
# Завершаем активные диалоги этого админа
|
|
436
574
|
await self.end_admin_conversations(admin_id)
|
|
437
|
-
|
|
438
|
-
response =
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
575
|
+
|
|
576
|
+
response = (
|
|
577
|
+
self.client.table("admin_user_conversations")
|
|
578
|
+
.insert(
|
|
579
|
+
{
|
|
580
|
+
"admin_id": admin_id,
|
|
581
|
+
"user_id": user_id,
|
|
582
|
+
"session_id": session_id,
|
|
583
|
+
"status": "active",
|
|
584
|
+
"auto_end_at": (
|
|
585
|
+
datetime.now(timezone.utc) + timedelta(minutes=30)
|
|
586
|
+
).isoformat(),
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
.execute()
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
conversation_id = response.data[0]["id"]
|
|
593
|
+
logger.info(
|
|
594
|
+
f"Начат диалог {conversation_id}: админ {admin_id} с пользователем {user_id}"
|
|
595
|
+
)
|
|
448
596
|
return conversation_id
|
|
449
|
-
|
|
597
|
+
|
|
450
598
|
except APIError as e:
|
|
451
599
|
logger.error(f"Ошибка при начале диалога: {e}")
|
|
452
600
|
raise
|
|
453
|
-
|
|
454
|
-
async def end_admin_conversations(
|
|
601
|
+
|
|
602
|
+
async def end_admin_conversations(
|
|
603
|
+
self, admin_id: int = None, user_id: int = None
|
|
604
|
+
) -> int:
|
|
455
605
|
"""Завершает активные диалоги админа или пользователя"""
|
|
456
606
|
try:
|
|
457
|
-
query =
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
607
|
+
query = (
|
|
608
|
+
self.client.table("admin_user_conversations")
|
|
609
|
+
.update(
|
|
610
|
+
{
|
|
611
|
+
"status": "ended",
|
|
612
|
+
"ended_at": datetime.now(timezone.utc).isoformat(),
|
|
613
|
+
}
|
|
614
|
+
)
|
|
615
|
+
.eq("status", "active")
|
|
616
|
+
)
|
|
617
|
+
|
|
462
618
|
if admin_id:
|
|
463
|
-
query = query.eq(
|
|
619
|
+
query = query.eq("admin_id", admin_id)
|
|
464
620
|
if user_id:
|
|
465
|
-
query = query.eq(
|
|
466
|
-
|
|
621
|
+
query = query.eq("user_id", user_id)
|
|
622
|
+
|
|
467
623
|
response = query.execute()
|
|
468
624
|
ended_count = len(response.data)
|
|
469
|
-
|
|
625
|
+
|
|
470
626
|
if ended_count > 0:
|
|
471
627
|
logger.info(f"Завершено {ended_count} активных диалогов")
|
|
472
|
-
|
|
628
|
+
|
|
473
629
|
return ended_count
|
|
474
|
-
|
|
630
|
+
|
|
475
631
|
except APIError as e:
|
|
476
632
|
logger.error(f"Ошибка при завершении диалогов: {e}")
|
|
477
633
|
return 0
|
|
478
|
-
|
|
479
|
-
async def get_admin_active_conversation(
|
|
634
|
+
|
|
635
|
+
async def get_admin_active_conversation(
|
|
636
|
+
self, admin_id: int
|
|
637
|
+
) -> Optional[Dict[str, Any]]:
|
|
480
638
|
"""Получает активный диалог админа"""
|
|
481
639
|
try:
|
|
482
|
-
response =
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
640
|
+
response = (
|
|
641
|
+
self.client.table("admin_user_conversations")
|
|
642
|
+
.select("id", "user_id", "session_id", "started_at", "auto_end_at")
|
|
643
|
+
.eq("admin_id", admin_id)
|
|
644
|
+
.eq("status", "active")
|
|
645
|
+
.execute()
|
|
646
|
+
)
|
|
647
|
+
|
|
486
648
|
return response.data[0] if response.data else None
|
|
487
|
-
|
|
649
|
+
|
|
488
650
|
except APIError as e:
|
|
489
651
|
logger.error(f"Ошибка при получении диалога админа: {e}")
|
|
490
652
|
return None
|
|
491
|
-
|
|
653
|
+
|
|
492
654
|
async def get_user_conversation(self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
493
655
|
"""Получает активный диалог пользователя"""
|
|
494
656
|
try:
|
|
495
|
-
response =
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
657
|
+
response = (
|
|
658
|
+
self.client.table("admin_user_conversations")
|
|
659
|
+
.select("id", "admin_id", "session_id", "started_at", "auto_end_at")
|
|
660
|
+
.eq("user_id", user_id)
|
|
661
|
+
.eq("status", "active")
|
|
662
|
+
.execute()
|
|
663
|
+
)
|
|
664
|
+
|
|
499
665
|
return response.data[0] if response.data else None
|
|
500
|
-
|
|
666
|
+
|
|
501
667
|
except APIError as e:
|
|
502
668
|
logger.error(f"Ошибка при получении диалога пользователя: {e}")
|
|
503
669
|
return None
|
|
504
|
-
|
|
670
|
+
|
|
505
671
|
# 🆕 Методы совместимости - добавляем недостающие методы из старого кода
|
|
506
|
-
|
|
672
|
+
|
|
507
673
|
async def cleanup_expired_conversations(self) -> int:
|
|
508
674
|
"""Завершает просроченные диалоги админов"""
|
|
509
675
|
try:
|
|
510
676
|
now = datetime.now(timezone.utc).isoformat()
|
|
511
|
-
|
|
512
|
-
response =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
677
|
+
|
|
678
|
+
response = (
|
|
679
|
+
self.client.table("admin_user_conversations")
|
|
680
|
+
.update({"status": "expired", "ended_at": now})
|
|
681
|
+
.eq("status", "active")
|
|
682
|
+
.lt("auto_end_at", now)
|
|
683
|
+
.execute()
|
|
684
|
+
)
|
|
685
|
+
|
|
517
686
|
ended_count = len(response.data)
|
|
518
687
|
if ended_count > 0:
|
|
519
|
-
logger.info(
|
|
520
|
-
|
|
688
|
+
logger.info(
|
|
689
|
+
f"Автоматически завершено {ended_count} просроченных диалогов"
|
|
690
|
+
)
|
|
691
|
+
|
|
521
692
|
return ended_count
|
|
522
|
-
|
|
693
|
+
|
|
523
694
|
except APIError as e:
|
|
524
695
|
logger.error(f"Ошибка при завершении просроченных диалогов: {e}")
|
|
525
696
|
return 0
|
|
526
|
-
|
|
697
|
+
|
|
527
698
|
async def end_expired_conversations(self) -> int:
|
|
528
699
|
"""Алиас для cleanup_expired_conversations для обратной совместимости"""
|
|
529
700
|
return await self.cleanup_expired_conversations()
|
|
530
|
-
|
|
531
|
-
async def get_user_admin_conversation(
|
|
701
|
+
|
|
702
|
+
async def get_user_admin_conversation(
|
|
703
|
+
self, user_id: int
|
|
704
|
+
) -> Optional[Dict[str, Any]]:
|
|
532
705
|
"""Проверяет, ведется ли диалог с пользователем (для совместимости)"""
|
|
533
706
|
return await self.get_user_conversation(user_id)
|
|
534
|
-
|
|
707
|
+
|
|
535
708
|
# 🆕 Методы аналитики с фильтрацией по bot_id
|
|
536
|
-
|
|
709
|
+
|
|
537
710
|
async def get_analytics_summary(self, days: int = 7) -> Dict[str, Any]:
|
|
538
711
|
"""Получает сводку аналитики за последние дни с учетом bot_id (если указан)"""
|
|
539
712
|
try:
|
|
540
713
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
541
|
-
|
|
714
|
+
|
|
542
715
|
# 🆕 Получаем сессии с учетом bot_id (если указан)
|
|
543
|
-
query =
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
716
|
+
query = (
|
|
717
|
+
self.client.table("sales_chat_sessions")
|
|
718
|
+
.select("id", "current_stage", "lead_quality_score", "created_at")
|
|
719
|
+
.gte("created_at", cutoff_date.isoformat())
|
|
720
|
+
)
|
|
721
|
+
|
|
547
722
|
if self.bot_id:
|
|
548
|
-
query = query.eq(
|
|
549
|
-
|
|
723
|
+
query = query.eq("bot_id", self.bot_id)
|
|
724
|
+
|
|
550
725
|
sessions_response = query.execute()
|
|
551
|
-
|
|
726
|
+
|
|
552
727
|
sessions = sessions_response.data
|
|
553
728
|
total_sessions = len(sessions)
|
|
554
|
-
|
|
729
|
+
|
|
555
730
|
# Группировка по этапам
|
|
556
731
|
stages = {}
|
|
557
732
|
quality_scores = []
|
|
558
|
-
|
|
733
|
+
|
|
559
734
|
for session in sessions:
|
|
560
|
-
stage = session.get(
|
|
735
|
+
stage = session.get("current_stage", "unknown")
|
|
561
736
|
stages[stage] = stages.get(stage, 0) + 1
|
|
562
|
-
|
|
563
|
-
score = session.get(
|
|
737
|
+
|
|
738
|
+
score = session.get("lead_quality_score", 5)
|
|
564
739
|
if score:
|
|
565
740
|
quality_scores.append(score)
|
|
566
|
-
|
|
567
|
-
avg_quality =
|
|
568
|
-
|
|
741
|
+
|
|
742
|
+
avg_quality = (
|
|
743
|
+
sum(quality_scores) / len(quality_scores) if quality_scores else 5
|
|
744
|
+
)
|
|
745
|
+
|
|
569
746
|
return {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
747
|
+
"bot_id": self.bot_id,
|
|
748
|
+
"period_days": days,
|
|
749
|
+
"total_sessions": total_sessions,
|
|
750
|
+
"stages": stages,
|
|
751
|
+
"average_lead_quality": round(avg_quality, 1),
|
|
752
|
+
"generated_at": datetime.now().isoformat(),
|
|
576
753
|
}
|
|
577
|
-
|
|
754
|
+
|
|
578
755
|
except APIError as e:
|
|
579
756
|
logger.error(f"Ошибка при получении аналитики: {e}")
|
|
580
757
|
return {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
758
|
+
"bot_id": self.bot_id,
|
|
759
|
+
"error": str(e),
|
|
760
|
+
"generated_at": datetime.now().isoformat(),
|
|
584
761
|
}
|
|
585
|
-
|
|
762
|
+
|
|
586
763
|
async def update_session_analytics(
|
|
587
|
-
self,
|
|
588
|
-
session_id: str,
|
|
589
|
-
tokens_used: int = 0,
|
|
590
|
-
processing_time_ms: int = 0
|
|
764
|
+
self, session_id: str, tokens_used: int = 0, processing_time_ms: int = 0
|
|
591
765
|
):
|
|
592
766
|
"""Обновляет аналитику сессии"""
|
|
593
767
|
try:
|
|
594
768
|
# Получаем текущую аналитику
|
|
595
|
-
response =
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
769
|
+
response = (
|
|
770
|
+
self.client.table("sales_session_analytics")
|
|
771
|
+
.select("total_messages", "total_tokens", "average_response_time_ms")
|
|
772
|
+
.eq("session_id", session_id)
|
|
773
|
+
.execute()
|
|
774
|
+
)
|
|
775
|
+
|
|
599
776
|
if response.data:
|
|
600
777
|
current = response.data[0]
|
|
601
|
-
new_total_messages = current[
|
|
602
|
-
new_total_tokens = current[
|
|
603
|
-
|
|
778
|
+
new_total_messages = current["total_messages"] + 1
|
|
779
|
+
new_total_tokens = current["total_tokens"] + tokens_used
|
|
780
|
+
|
|
604
781
|
# Вычисляем среднее время ответа
|
|
605
782
|
if processing_time_ms > 0:
|
|
606
|
-
current_avg = current[
|
|
607
|
-
new_avg = (
|
|
783
|
+
current_avg = current["average_response_time_ms"]
|
|
784
|
+
new_avg = (
|
|
785
|
+
(current_avg * (new_total_messages - 1)) + processing_time_ms
|
|
786
|
+
) / new_total_messages
|
|
608
787
|
else:
|
|
609
|
-
new_avg = current[
|
|
610
|
-
|
|
788
|
+
new_avg = current["average_response_time_ms"]
|
|
789
|
+
|
|
611
790
|
# Обновляем аналитику
|
|
612
|
-
self.client.table(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
791
|
+
self.client.table("sales_session_analytics").update(
|
|
792
|
+
{
|
|
793
|
+
"total_messages": new_total_messages,
|
|
794
|
+
"total_tokens": new_total_tokens,
|
|
795
|
+
"average_response_time_ms": int(new_avg),
|
|
796
|
+
"updated_at": datetime.now().isoformat(),
|
|
797
|
+
}
|
|
798
|
+
).eq("session_id", session_id).execute()
|
|
799
|
+
|
|
619
800
|
except APIError as e:
|
|
620
801
|
logger.error(f"Ошибка при обновлении аналитики: {e}")
|
|
621
802
|
# Не прерываем выполнение, аналитика не критична
|
|
622
|
-
|
|
803
|
+
|
|
623
804
|
# Методы совместимости
|
|
624
|
-
async def update_conversion_stage(
|
|
805
|
+
async def update_conversion_stage(
|
|
806
|
+
self, session_id: str, stage: str, quality_score: int = None
|
|
807
|
+
):
|
|
625
808
|
"""Обновляет этап конверсии и качество лида (для совместимости)"""
|
|
626
809
|
await self.update_session_stage(session_id, stage, quality_score)
|
|
627
|
-
|
|
810
|
+
|
|
628
811
|
async def archive_old_sessions(self, days: int = 7):
|
|
629
812
|
"""Архивирует старые завершенные сессии с учетом bot_id (если указан)"""
|
|
630
813
|
try:
|
|
631
814
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
632
|
-
|
|
815
|
+
|
|
633
816
|
# 🆕 Архивируем только сессии этого бота (если bot_id указан)
|
|
634
|
-
query =
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
817
|
+
query = (
|
|
818
|
+
self.client.table("sales_chat_sessions")
|
|
819
|
+
.update({"status": "archived"})
|
|
820
|
+
.eq("status", "completed")
|
|
821
|
+
.lt("updated_at", cutoff_date.isoformat())
|
|
822
|
+
)
|
|
823
|
+
|
|
638
824
|
if self.bot_id:
|
|
639
|
-
query = query.eq(
|
|
640
|
-
|
|
825
|
+
query = query.eq("bot_id", self.bot_id)
|
|
826
|
+
|
|
641
827
|
query.execute()
|
|
642
|
-
|
|
643
|
-
logger.info(
|
|
644
|
-
|
|
828
|
+
|
|
829
|
+
logger.info(
|
|
830
|
+
f"Архивированы сессии старше {days} дней{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
|
|
831
|
+
)
|
|
832
|
+
|
|
645
833
|
except APIError as e:
|
|
646
834
|
logger.error(f"Ошибка при архивировании сессий: {e}")
|
|
647
835
|
raise
|
|
648
|
-
|
|
836
|
+
|
|
649
837
|
async def get_sent_files(self, user_id: int) -> List[str]:
|
|
650
838
|
"""Получает список отправленных файлов для пользователя
|
|
651
|
-
|
|
839
|
+
|
|
652
840
|
Args:
|
|
653
841
|
user_id: Telegram ID пользователя
|
|
654
|
-
|
|
842
|
+
|
|
655
843
|
Returns:
|
|
656
844
|
List[str]: Список имен файлов, разделенных запятой
|
|
657
845
|
"""
|
|
658
846
|
try:
|
|
659
|
-
query =
|
|
660
|
-
|
|
847
|
+
query = (
|
|
848
|
+
self.client.table("sales_users")
|
|
849
|
+
.select("files")
|
|
850
|
+
.eq("telegram_id", user_id)
|
|
851
|
+
)
|
|
852
|
+
|
|
661
853
|
if self.bot_id:
|
|
662
|
-
query = query.eq(
|
|
663
|
-
|
|
854
|
+
query = query.eq("bot_id", self.bot_id)
|
|
855
|
+
|
|
664
856
|
response = query.execute()
|
|
665
|
-
|
|
666
|
-
if response.data and response.data[0].get(
|
|
667
|
-
files_str = response.data[0][
|
|
668
|
-
return [f.strip() for f in files_str.split(
|
|
669
|
-
|
|
857
|
+
|
|
858
|
+
if response.data and response.data[0].get("files"):
|
|
859
|
+
files_str = response.data[0]["files"]
|
|
860
|
+
return [f.strip() for f in files_str.split(",") if f.strip()]
|
|
861
|
+
|
|
670
862
|
return []
|
|
671
|
-
|
|
863
|
+
|
|
672
864
|
except Exception as e:
|
|
673
|
-
logger.error(
|
|
865
|
+
logger.error(
|
|
866
|
+
f"Ошибка получения отправленных файлов для пользователя {user_id}: {e}"
|
|
867
|
+
)
|
|
674
868
|
return []
|
|
675
|
-
|
|
869
|
+
|
|
676
870
|
async def get_sent_directories(self, user_id: int) -> List[str]:
|
|
677
871
|
"""Получает список отправленных каталогов для пользователя
|
|
678
|
-
|
|
872
|
+
|
|
679
873
|
Args:
|
|
680
874
|
user_id: Telegram ID пользователя
|
|
681
|
-
|
|
875
|
+
|
|
682
876
|
Returns:
|
|
683
877
|
List[str]: Список путей каталогов, разделенных запятой
|
|
684
878
|
"""
|
|
685
879
|
try:
|
|
686
|
-
query =
|
|
687
|
-
|
|
880
|
+
query = (
|
|
881
|
+
self.client.table("sales_users")
|
|
882
|
+
.select("directories")
|
|
883
|
+
.eq("telegram_id", user_id)
|
|
884
|
+
)
|
|
885
|
+
|
|
688
886
|
if self.bot_id:
|
|
689
|
-
query = query.eq(
|
|
690
|
-
|
|
887
|
+
query = query.eq("bot_id", self.bot_id)
|
|
888
|
+
|
|
691
889
|
response = query.execute()
|
|
692
|
-
|
|
693
|
-
if response.data and response.data[0].get(
|
|
694
|
-
dirs_str = response.data[0][
|
|
695
|
-
return [d.strip() for d in dirs_str.split(
|
|
696
|
-
|
|
890
|
+
|
|
891
|
+
if response.data and response.data[0].get("directories"):
|
|
892
|
+
dirs_str = response.data[0]["directories"]
|
|
893
|
+
return [d.strip() for d in dirs_str.split(",") if d.strip()]
|
|
894
|
+
|
|
697
895
|
return []
|
|
698
|
-
|
|
896
|
+
|
|
699
897
|
except Exception as e:
|
|
700
|
-
logger.error(
|
|
898
|
+
logger.error(
|
|
899
|
+
f"Ошибка получения отправленных каталогов для пользователя {user_id}: {e}"
|
|
900
|
+
)
|
|
701
901
|
return []
|
|
702
|
-
|
|
902
|
+
|
|
703
903
|
async def add_sent_files(self, user_id: int, files_list: List[str]):
|
|
704
904
|
"""Добавляет файлы в список отправленных для пользователя
|
|
705
|
-
|
|
905
|
+
|
|
706
906
|
Args:
|
|
707
907
|
user_id: Telegram ID пользователя
|
|
708
908
|
files_list: Список имен файлов для добавления
|
|
709
909
|
"""
|
|
710
910
|
try:
|
|
711
911
|
logger.info(f"Добавление файлов для пользователя {user_id}: {files_list}")
|
|
712
|
-
|
|
912
|
+
|
|
713
913
|
# Получаем текущий список
|
|
714
914
|
current_files = await self.get_sent_files(user_id)
|
|
715
915
|
logger.info(f"Текущие файлы в БД: {current_files}")
|
|
716
|
-
|
|
916
|
+
|
|
717
917
|
# Объединяем с новыми файлами (без дубликатов)
|
|
718
918
|
all_files = list(set(current_files + files_list))
|
|
719
919
|
logger.info(f"Объединенный список файлов: {all_files}")
|
|
720
|
-
|
|
920
|
+
|
|
721
921
|
# Сохраняем обратно
|
|
722
|
-
files_str =
|
|
922
|
+
files_str = ", ".join(all_files)
|
|
723
923
|
logger.info(f"Сохраняем строку: {files_str}")
|
|
724
|
-
|
|
725
|
-
query =
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
924
|
+
|
|
925
|
+
query = (
|
|
926
|
+
self.client.table("sales_users")
|
|
927
|
+
.update({"files": files_str})
|
|
928
|
+
.eq("telegram_id", user_id)
|
|
929
|
+
)
|
|
930
|
+
|
|
729
931
|
if self.bot_id:
|
|
730
|
-
query = query.eq(
|
|
932
|
+
query = query.eq("bot_id", self.bot_id)
|
|
731
933
|
logger.info(f"Фильтр по bot_id: {self.bot_id}")
|
|
732
|
-
|
|
934
|
+
|
|
733
935
|
response = query.execute()
|
|
734
936
|
logger.info(f"Ответ от БД: {response.data}")
|
|
735
|
-
|
|
736
|
-
logger.info(
|
|
737
|
-
|
|
937
|
+
|
|
938
|
+
logger.info(
|
|
939
|
+
f"✅ Добавлено {len(files_list)} файлов для пользователя {user_id}"
|
|
940
|
+
)
|
|
941
|
+
|
|
738
942
|
except Exception as e:
|
|
739
|
-
logger.error(
|
|
943
|
+
logger.error(
|
|
944
|
+
f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}"
|
|
945
|
+
)
|
|
740
946
|
logger.exception("Полный стек ошибки:")
|
|
741
|
-
|
|
947
|
+
|
|
742
948
|
async def add_sent_directories(self, user_id: int, dirs_list: List[str]):
|
|
743
949
|
"""Добавляет каталоги в список отправленных для пользователя
|
|
744
|
-
|
|
950
|
+
|
|
745
951
|
Args:
|
|
746
952
|
user_id: Telegram ID пользователя
|
|
747
953
|
dirs_list: Список путей каталогов для добавления
|
|
748
954
|
"""
|
|
749
955
|
try:
|
|
750
956
|
logger.info(f"Добавление каталогов для пользователя {user_id}: {dirs_list}")
|
|
751
|
-
|
|
957
|
+
|
|
752
958
|
# Получаем текущий список
|
|
753
959
|
current_dirs = await self.get_sent_directories(user_id)
|
|
754
960
|
logger.info(f"Текущие каталоги в БД: {current_dirs}")
|
|
755
|
-
|
|
961
|
+
|
|
756
962
|
# Объединяем с новыми каталогами (без дубликатов)
|
|
757
963
|
all_dirs = list(set(current_dirs + dirs_list))
|
|
758
964
|
logger.info(f"Объединенный список каталогов: {all_dirs}")
|
|
759
|
-
|
|
965
|
+
|
|
760
966
|
# Сохраняем обратно
|
|
761
|
-
dirs_str =
|
|
967
|
+
dirs_str = ", ".join(all_dirs)
|
|
762
968
|
logger.info(f"Сохраняем строку: {dirs_str}")
|
|
763
|
-
|
|
764
|
-
query =
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
969
|
+
|
|
970
|
+
query = (
|
|
971
|
+
self.client.table("sales_users")
|
|
972
|
+
.update({"directories": dirs_str})
|
|
973
|
+
.eq("telegram_id", user_id)
|
|
974
|
+
)
|
|
975
|
+
|
|
768
976
|
if self.bot_id:
|
|
769
|
-
query = query.eq(
|
|
977
|
+
query = query.eq("bot_id", self.bot_id)
|
|
770
978
|
logger.info(f"Фильтр по bot_id: {self.bot_id}")
|
|
771
|
-
|
|
979
|
+
|
|
772
980
|
response = query.execute()
|
|
773
981
|
logger.info(f"Ответ от БД: {response.data}")
|
|
774
|
-
|
|
775
|
-
logger.info(
|
|
776
|
-
|
|
982
|
+
|
|
983
|
+
logger.info(
|
|
984
|
+
f"✅ Добавлено {len(dirs_list)} каталогов для пользователя {user_id}"
|
|
985
|
+
)
|
|
986
|
+
|
|
777
987
|
except Exception as e:
|
|
778
|
-
logger.error(
|
|
988
|
+
logger.error(
|
|
989
|
+
f"❌ Ошибка добавления отправленных каталогов для пользователя {user_id}: {e}"
|
|
990
|
+
)
|
|
779
991
|
logger.exception("Полный стек ошибки:")
|
|
780
992
|
|
|
781
993
|
# =============================================================================
|
|
782
994
|
# МЕТОДЫ ДЛЯ АНАЛИТИКИ
|
|
783
995
|
# =============================================================================
|
|
784
|
-
|
|
996
|
+
|
|
785
997
|
async def get_funnel_stats(self, days: int = 7) -> Dict[str, Any]:
|
|
786
998
|
"""Получает статистику воронки продаж"""
|
|
787
999
|
try:
|
|
788
1000
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
users_query = self.client.table(
|
|
792
|
-
|
|
1001
|
+
|
|
1002
|
+
# Получаем ВСЕ уникальные пользователи из sales_users с фильтром по bot_id
|
|
1003
|
+
users_query = self.client.table("sales_users").select("telegram_id")
|
|
1004
|
+
|
|
793
1005
|
if self.bot_id:
|
|
794
|
-
users_query = users_query.eq(
|
|
795
|
-
|
|
1006
|
+
users_query = users_query.eq("bot_id", self.bot_id)
|
|
1007
|
+
|
|
796
1008
|
# Исключаем тестовых пользователей
|
|
797
|
-
users_query = users_query.neq(
|
|
798
|
-
|
|
1009
|
+
users_query = users_query.neq("username", "test_user")
|
|
1010
|
+
|
|
799
1011
|
users_response = users_query.execute()
|
|
800
1012
|
total_unique_users = len(users_response.data) if users_response.data else 0
|
|
801
|
-
|
|
1013
|
+
|
|
802
1014
|
# Получаем сессии с учетом bot_id за период
|
|
803
|
-
sessions_query =
|
|
804
|
-
|
|
805
|
-
|
|
1015
|
+
sessions_query = (
|
|
1016
|
+
self.client.table("sales_chat_sessions")
|
|
1017
|
+
.select(
|
|
1018
|
+
"id", "user_id", "current_stage", "lead_quality_score", "created_at"
|
|
1019
|
+
)
|
|
1020
|
+
.gte("created_at", cutoff_date.isoformat())
|
|
1021
|
+
)
|
|
806
1022
|
|
|
807
1023
|
if self.bot_id:
|
|
808
|
-
sessions_query = sessions_query.eq(
|
|
1024
|
+
sessions_query = sessions_query.eq("bot_id", self.bot_id)
|
|
809
1025
|
|
|
810
1026
|
sessions_response = sessions_query.execute()
|
|
811
1027
|
sessions = sessions_response.data
|
|
812
|
-
|
|
1028
|
+
|
|
813
1029
|
# Исключаем сессии тестовых пользователей
|
|
814
1030
|
if sessions:
|
|
815
1031
|
# Получаем telegram_id тестовых пользователей
|
|
816
|
-
test_users_query =
|
|
1032
|
+
test_users_query = (
|
|
1033
|
+
self.client.table("sales_users")
|
|
1034
|
+
.select("telegram_id")
|
|
1035
|
+
.eq("username", "test_user")
|
|
1036
|
+
)
|
|
817
1037
|
if self.bot_id:
|
|
818
|
-
test_users_query = test_users_query.eq(
|
|
819
|
-
|
|
1038
|
+
test_users_query = test_users_query.eq("bot_id", self.bot_id)
|
|
1039
|
+
|
|
820
1040
|
test_users_response = test_users_query.execute()
|
|
821
|
-
test_user_ids =
|
|
822
|
-
|
|
1041
|
+
test_user_ids = (
|
|
1042
|
+
{user["telegram_id"] for user in test_users_response.data}
|
|
1043
|
+
if test_users_response.data
|
|
1044
|
+
else set()
|
|
1045
|
+
)
|
|
1046
|
+
|
|
823
1047
|
# Фильтруем сессии
|
|
824
|
-
sessions = [s for s in sessions if s[
|
|
825
|
-
|
|
1048
|
+
sessions = [s for s in sessions if s["user_id"] not in test_user_ids]
|
|
1049
|
+
|
|
826
1050
|
total_sessions = len(sessions)
|
|
827
1051
|
|
|
828
1052
|
# Группировка по этапам
|
|
829
|
-
|
|
1053
|
+
|
|
830
1054
|
# Группировка по этапам
|
|
831
1055
|
stages = {}
|
|
832
1056
|
quality_scores = []
|
|
833
|
-
|
|
1057
|
+
|
|
834
1058
|
for session in sessions:
|
|
835
|
-
stage = session.get(
|
|
1059
|
+
stage = session.get("current_stage", "unknown")
|
|
836
1060
|
stages[stage] = stages.get(stage, 0) + 1
|
|
837
|
-
|
|
838
|
-
score = session.get(
|
|
1061
|
+
|
|
1062
|
+
score = session.get("lead_quality_score", 5)
|
|
839
1063
|
if score:
|
|
840
1064
|
quality_scores.append(score)
|
|
841
|
-
|
|
842
|
-
avg_quality =
|
|
843
|
-
|
|
1065
|
+
|
|
1066
|
+
avg_quality = (
|
|
1067
|
+
sum(quality_scores) / len(quality_scores) if quality_scores else 5
|
|
1068
|
+
)
|
|
1069
|
+
|
|
844
1070
|
return {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1071
|
+
"total_sessions": total_sessions,
|
|
1072
|
+
"total_unique_users": total_unique_users, # ✅ ВСЕ уникальные пользователи бота
|
|
1073
|
+
"stages": stages,
|
|
1074
|
+
"avg_quality": round(avg_quality, 1),
|
|
1075
|
+
"period_days": days,
|
|
850
1076
|
}
|
|
851
|
-
|
|
1077
|
+
|
|
852
1078
|
except APIError as e:
|
|
853
1079
|
logger.error(f"Ошибка получения статистики воронки: {e}")
|
|
854
1080
|
return {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1081
|
+
"total_sessions": 0,
|
|
1082
|
+
"stages": {},
|
|
1083
|
+
"avg_quality": 0,
|
|
1084
|
+
"period_days": days,
|
|
859
1085
|
}
|
|
860
|
-
|
|
1086
|
+
|
|
861
1087
|
async def get_events_stats(self, days: int = 7) -> Dict[str, int]:
|
|
862
1088
|
"""Получает статистику событий"""
|
|
863
1089
|
try:
|
|
864
1090
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
865
|
-
|
|
1091
|
+
|
|
866
1092
|
# Получаем события с учетом bot_id через сессии
|
|
867
|
-
query =
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1093
|
+
query = (
|
|
1094
|
+
self.client.table("session_events")
|
|
1095
|
+
.select("event_type", "session_id")
|
|
1096
|
+
.gte("created_at", cutoff_date.isoformat())
|
|
1097
|
+
)
|
|
1098
|
+
|
|
871
1099
|
events_response = query.execute()
|
|
872
1100
|
events = events_response.data if events_response.data else []
|
|
873
|
-
|
|
1101
|
+
|
|
874
1102
|
# Фильтруем события по bot_id через сессии
|
|
875
1103
|
if self.bot_id and events:
|
|
876
1104
|
# Получаем ID сессий этого бота
|
|
877
|
-
sessions_query =
|
|
1105
|
+
sessions_query = (
|
|
1106
|
+
self.client.table("sales_chat_sessions")
|
|
1107
|
+
.select("id", "user_id")
|
|
1108
|
+
.eq("bot_id", self.bot_id)
|
|
1109
|
+
)
|
|
878
1110
|
sessions_response = sessions_query.execute()
|
|
879
|
-
|
|
1111
|
+
|
|
880
1112
|
# Исключаем сессии тестовых пользователей
|
|
881
1113
|
if sessions_response.data:
|
|
882
1114
|
# Получаем telegram_id тестовых пользователей
|
|
883
|
-
test_users_query =
|
|
1115
|
+
test_users_query = (
|
|
1116
|
+
self.client.table("sales_users")
|
|
1117
|
+
.select("telegram_id")
|
|
1118
|
+
.eq("username", "test_user")
|
|
1119
|
+
)
|
|
884
1120
|
if self.bot_id:
|
|
885
|
-
test_users_query = test_users_query.eq(
|
|
886
|
-
|
|
1121
|
+
test_users_query = test_users_query.eq("bot_id", self.bot_id)
|
|
1122
|
+
|
|
887
1123
|
test_users_response = test_users_query.execute()
|
|
888
|
-
test_user_ids =
|
|
889
|
-
|
|
1124
|
+
test_user_ids = (
|
|
1125
|
+
{user["telegram_id"] for user in test_users_response.data}
|
|
1126
|
+
if test_users_response.data
|
|
1127
|
+
else set()
|
|
1128
|
+
)
|
|
1129
|
+
|
|
890
1130
|
# Фильтруем сессии: только не тестовые
|
|
891
|
-
bot_sessions = [
|
|
892
|
-
|
|
1131
|
+
bot_sessions = [
|
|
1132
|
+
s
|
|
1133
|
+
for s in sessions_response.data
|
|
1134
|
+
if s["user_id"] not in test_user_ids
|
|
1135
|
+
]
|
|
1136
|
+
bot_session_ids = {session["id"] for session in bot_sessions}
|
|
893
1137
|
else:
|
|
894
1138
|
bot_session_ids = set()
|
|
895
|
-
|
|
1139
|
+
|
|
896
1140
|
# Фильтруем события
|
|
897
|
-
events = [
|
|
898
|
-
|
|
1141
|
+
events = [
|
|
1142
|
+
event for event in events if event["session_id"] in bot_session_ids
|
|
1143
|
+
]
|
|
1144
|
+
|
|
899
1145
|
# Группируем по типам событий
|
|
900
1146
|
event_counts = {}
|
|
901
1147
|
for event in events:
|
|
902
|
-
event_type = event.get(
|
|
1148
|
+
event_type = event.get("event_type", "unknown")
|
|
903
1149
|
event_counts[event_type] = event_counts.get(event_type, 0) + 1
|
|
904
|
-
|
|
1150
|
+
|
|
905
1151
|
return event_counts
|
|
906
|
-
|
|
1152
|
+
|
|
907
1153
|
except APIError as e:
|
|
908
1154
|
logger.error(f"Ошибка получения статистики событий: {e}")
|
|
909
1155
|
return {}
|
|
910
1156
|
|
|
911
|
-
async def get_user_last_message_info(
|
|
1157
|
+
async def get_user_last_message_info(
|
|
1158
|
+
self, user_id: int
|
|
1159
|
+
) -> Optional[Dict[str, Any]]:
|
|
912
1160
|
"""Получает информацию о последней активности пользователя из сессии"""
|
|
913
1161
|
try:
|
|
914
1162
|
# Получаем последнюю сессию пользователя
|
|
915
|
-
response =
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1163
|
+
response = (
|
|
1164
|
+
self.client.table("sales_chat_sessions")
|
|
1165
|
+
.select("id", "current_stage", "created_at", "updated_at")
|
|
1166
|
+
.eq("user_id", user_id)
|
|
1167
|
+
.order("updated_at", desc=True)
|
|
1168
|
+
.limit(1)
|
|
1169
|
+
.execute()
|
|
1170
|
+
)
|
|
1171
|
+
|
|
919
1172
|
if not response.data:
|
|
920
1173
|
return None
|
|
921
|
-
|
|
1174
|
+
|
|
922
1175
|
session = response.data[0]
|
|
923
|
-
|
|
1176
|
+
|
|
924
1177
|
return {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1178
|
+
"last_message_at": session["updated_at"],
|
|
1179
|
+
"session_id": session["id"],
|
|
1180
|
+
"current_stage": session["current_stage"],
|
|
1181
|
+
"session_updated_at": session["updated_at"],
|
|
929
1182
|
}
|
|
930
|
-
|
|
1183
|
+
|
|
931
1184
|
except Exception as e:
|
|
932
|
-
logger.error(
|
|
1185
|
+
logger.error(
|
|
1186
|
+
f"Ошибка получения информации о последнем сообщении пользователя {user_id}: {e}"
|
|
1187
|
+
)
|
|
933
1188
|
return None
|
|
934
1189
|
|
|
935
|
-
async def check_user_stage_changed(
|
|
1190
|
+
async def check_user_stage_changed(
|
|
1191
|
+
self, user_id: int, original_session_id: str
|
|
1192
|
+
) -> bool:
|
|
936
1193
|
"""Проверяет, изменился ли этап пользователя с момента планирования события"""
|
|
937
1194
|
try:
|
|
938
1195
|
# Получаем текущую информацию о сессии
|
|
939
|
-
current_response =
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1196
|
+
current_response = (
|
|
1197
|
+
self.client.table("sales_chat_sessions")
|
|
1198
|
+
.select("id", "current_stage")
|
|
1199
|
+
.eq("user_telegram_id", user_id)
|
|
1200
|
+
.order("created_at", desc=True)
|
|
1201
|
+
.limit(1)
|
|
1202
|
+
.execute()
|
|
1203
|
+
)
|
|
1204
|
+
|
|
943
1205
|
if not current_response.data:
|
|
944
1206
|
return False
|
|
945
|
-
|
|
1207
|
+
|
|
946
1208
|
current_session = current_response.data[0]
|
|
947
|
-
|
|
1209
|
+
|
|
948
1210
|
# Если сессия изменилась - этап точно изменился
|
|
949
|
-
if current_session[
|
|
1211
|
+
if current_session["id"] != original_session_id:
|
|
950
1212
|
return True
|
|
951
|
-
|
|
1213
|
+
|
|
952
1214
|
# Если сессия та же, получаем оригинальный этап из scheduled_events
|
|
953
1215
|
# и сравниваем с текущим
|
|
954
|
-
original_response =
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1216
|
+
original_response = (
|
|
1217
|
+
self.client.table("sales_chat_sessions")
|
|
1218
|
+
.select("current_stage")
|
|
1219
|
+
.eq("id", original_session_id)
|
|
1220
|
+
.execute()
|
|
1221
|
+
)
|
|
1222
|
+
|
|
958
1223
|
if not original_response.data:
|
|
959
1224
|
# Если не нашли оригинальную сессию, считаем что этап не изменился
|
|
960
1225
|
return False
|
|
961
|
-
|
|
962
|
-
original_stage = original_response.data[0][
|
|
963
|
-
current_stage = current_session[
|
|
964
|
-
|
|
1226
|
+
|
|
1227
|
+
original_stage = original_response.data[0]["current_stage"]
|
|
1228
|
+
current_stage = current_session["current_stage"]
|
|
1229
|
+
|
|
965
1230
|
# Проверяем, изменился ли этап внутри той же сессии
|
|
966
1231
|
if original_stage != current_stage:
|
|
967
|
-
logger.info(
|
|
1232
|
+
logger.info(
|
|
1233
|
+
f"🔄 Этап изменился: {original_stage} -> {current_stage} (сессия {original_session_id})"
|
|
1234
|
+
)
|
|
968
1235
|
return True
|
|
969
|
-
|
|
1236
|
+
|
|
970
1237
|
return False
|
|
971
|
-
|
|
1238
|
+
|
|
972
1239
|
except Exception as e:
|
|
973
1240
|
logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
|
|
974
1241
|
return False
|
|
975
1242
|
|
|
976
|
-
async def get_last_event_info_by_user_and_type(
|
|
1243
|
+
async def get_last_event_info_by_user_and_type(
|
|
1244
|
+
self, user_id: int, event_type: str
|
|
1245
|
+
) -> Optional[str]:
|
|
977
1246
|
"""
|
|
978
1247
|
Получает event_info последнего события определенного типа для пользователя
|
|
979
|
-
|
|
1248
|
+
|
|
980
1249
|
Args:
|
|
981
1250
|
user_id: Telegram ID пользователя
|
|
982
1251
|
event_type: Тип события для поиска
|
|
983
|
-
|
|
1252
|
+
|
|
984
1253
|
Returns:
|
|
985
1254
|
str: event_info последнего найденного события или None если не найдено
|
|
986
1255
|
"""
|
|
987
1256
|
try:
|
|
988
1257
|
# 1. Получаем последнюю сессию пользователя
|
|
989
|
-
sessions_query =
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1258
|
+
sessions_query = (
|
|
1259
|
+
self.client.table("sales_chat_sessions")
|
|
1260
|
+
.select("id")
|
|
1261
|
+
.eq("user_id", user_id)
|
|
1262
|
+
.order("created_at", desc=True)
|
|
1263
|
+
.limit(1)
|
|
1264
|
+
)
|
|
1265
|
+
|
|
993
1266
|
# Фильтруем по bot_id если указан
|
|
994
1267
|
if self.bot_id:
|
|
995
|
-
sessions_query = sessions_query.eq(
|
|
996
|
-
|
|
1268
|
+
sessions_query = sessions_query.eq("bot_id", self.bot_id)
|
|
1269
|
+
|
|
997
1270
|
sessions_response = sessions_query.execute()
|
|
998
|
-
|
|
1271
|
+
|
|
999
1272
|
if not sessions_response.data:
|
|
1000
1273
|
logger.info(f"Пользователь {user_id} не найден в сессиях")
|
|
1001
1274
|
return None
|
|
1002
|
-
|
|
1003
|
-
session_id = sessions_response.data[0][
|
|
1004
|
-
logger.info(
|
|
1005
|
-
|
|
1275
|
+
|
|
1276
|
+
session_id = sessions_response.data[0]["id"]
|
|
1277
|
+
logger.info(
|
|
1278
|
+
f"Найдена последняя сессия {session_id} для пользователя {user_id}"
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1006
1281
|
# 2. Ищем последнее событие с этим session_id и event_type
|
|
1007
|
-
events_response =
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1282
|
+
events_response = (
|
|
1283
|
+
self.client.table("session_events")
|
|
1284
|
+
.select("event_info", "created_at")
|
|
1285
|
+
.eq("session_id", session_id)
|
|
1286
|
+
.eq("event_type", event_type)
|
|
1287
|
+
.order("created_at", desc=True)
|
|
1288
|
+
.limit(1)
|
|
1289
|
+
.execute()
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1013
1292
|
if not events_response.data:
|
|
1014
|
-
logger.info(
|
|
1293
|
+
logger.info(
|
|
1294
|
+
f"События типа '{event_type}' не найдены для сессии {session_id}"
|
|
1295
|
+
)
|
|
1015
1296
|
return None
|
|
1016
|
-
|
|
1017
|
-
event_info = events_response.data[0][
|
|
1018
|
-
created_at = events_response.data[0][
|
|
1019
|
-
|
|
1020
|
-
logger.info(
|
|
1021
|
-
|
|
1297
|
+
|
|
1298
|
+
event_info = events_response.data[0]["event_info"]
|
|
1299
|
+
created_at = events_response.data[0]["created_at"]
|
|
1300
|
+
|
|
1301
|
+
logger.info(
|
|
1302
|
+
f"Найдено последнее событие '{event_type}' для пользователя {user_id}: {event_info[:50]}... (создано: {created_at})"
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1022
1305
|
return event_info
|
|
1023
|
-
|
|
1306
|
+
|
|
1024
1307
|
except Exception as e:
|
|
1025
|
-
logger.error(
|
|
1308
|
+
logger.error(
|
|
1309
|
+
f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}"
|
|
1310
|
+
)
|
|
1026
1311
|
return None
|
|
1027
1312
|
|
|
1028
1313
|
async def get_all_segments(self) -> List[str]:
|
|
1029
1314
|
"""
|
|
1030
1315
|
Получает все уникальные сегменты из таблицы sales_users
|
|
1031
|
-
|
|
1316
|
+
|
|
1032
1317
|
Returns:
|
|
1033
1318
|
List[str]: Список уникальных сегментов
|
|
1034
1319
|
"""
|
|
1035
1320
|
try:
|
|
1036
1321
|
# Запрос всех непустых сегментов
|
|
1037
|
-
query =
|
|
1038
|
-
|
|
1322
|
+
query = (
|
|
1323
|
+
self.client.table("sales_users").select("segments").neq("segments", "")
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1039
1326
|
if self.bot_id:
|
|
1040
|
-
query = query.eq(
|
|
1041
|
-
|
|
1327
|
+
query = query.eq("bot_id", self.bot_id)
|
|
1328
|
+
|
|
1042
1329
|
response = query.execute()
|
|
1043
|
-
|
|
1330
|
+
|
|
1044
1331
|
# Собираем все уникальные сегменты
|
|
1045
1332
|
all_segments = set()
|
|
1046
1333
|
for row in response.data:
|
|
1047
|
-
segments_str = row.get(
|
|
1334
|
+
segments_str = row.get("segments", "")
|
|
1048
1335
|
if segments_str:
|
|
1049
1336
|
# Разбираем сегменты через запятую
|
|
1050
|
-
segments = [s.strip() for s in segments_str.split(
|
|
1337
|
+
segments = [s.strip() for s in segments_str.split(",") if s.strip()]
|
|
1051
1338
|
all_segments.update(segments)
|
|
1052
|
-
|
|
1339
|
+
|
|
1053
1340
|
segments_list = sorted(list(all_segments))
|
|
1054
1341
|
logger.info(f"Найдено {len(segments_list)} уникальных сегментов")
|
|
1055
|
-
|
|
1342
|
+
|
|
1056
1343
|
return segments_list
|
|
1057
|
-
|
|
1344
|
+
|
|
1058
1345
|
except Exception as e:
|
|
1059
1346
|
logger.error(f"Ошибка получения сегментов: {e}")
|
|
1060
1347
|
return []
|
|
@@ -1062,192 +1349,191 @@ class SupabaseClient:
|
|
|
1062
1349
|
async def get_users_by_segment(self, segment: str = None) -> List[Dict[str, Any]]:
|
|
1063
1350
|
"""
|
|
1064
1351
|
Получает пользователей по сегменту или всех пользователей
|
|
1065
|
-
|
|
1352
|
+
|
|
1066
1353
|
Args:
|
|
1067
1354
|
segment: Название сегмента (если None - возвращает всех)
|
|
1068
|
-
|
|
1355
|
+
|
|
1069
1356
|
Returns:
|
|
1070
1357
|
List[Dict]: Список пользователей с telegram_id
|
|
1071
1358
|
"""
|
|
1072
1359
|
try:
|
|
1073
|
-
query = self.client.table(
|
|
1074
|
-
|
|
1360
|
+
query = self.client.table("sales_users").select("telegram_id, segments")
|
|
1361
|
+
|
|
1075
1362
|
if self.bot_id:
|
|
1076
|
-
query = query.eq(
|
|
1077
|
-
|
|
1363
|
+
query = query.eq("bot_id", self.bot_id)
|
|
1364
|
+
|
|
1078
1365
|
response = query.execute()
|
|
1079
|
-
|
|
1366
|
+
|
|
1080
1367
|
if segment is None:
|
|
1081
1368
|
# Все пользователи
|
|
1082
1369
|
logger.info(f"Получено {len(response.data)} всех пользователей")
|
|
1083
1370
|
return response.data
|
|
1084
|
-
|
|
1371
|
+
|
|
1085
1372
|
# Фильтруем по сегменту
|
|
1086
1373
|
users = []
|
|
1087
1374
|
for row in response.data:
|
|
1088
|
-
segments_str = row.get(
|
|
1375
|
+
segments_str = row.get("segments", "")
|
|
1089
1376
|
if segments_str:
|
|
1090
|
-
segments = [s.strip() for s in segments_str.split(
|
|
1377
|
+
segments = [s.strip() for s in segments_str.split(",") if s.strip()]
|
|
1091
1378
|
if segment in segments:
|
|
1092
1379
|
users.append(row)
|
|
1093
|
-
|
|
1380
|
+
|
|
1094
1381
|
logger.info(f"Найдено {len(users)} пользователей с сегментом '{segment}'")
|
|
1095
1382
|
return users
|
|
1096
|
-
|
|
1383
|
+
|
|
1097
1384
|
except Exception as e:
|
|
1098
1385
|
logger.error(f"Ошибка получения пользователей по сегменту '{segment}': {e}")
|
|
1099
1386
|
return []
|
|
1100
|
-
|
|
1387
|
+
|
|
1101
1388
|
# =============================================================================
|
|
1102
1389
|
# МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ СОБЫТИЙ В SUPABASE STORAGE
|
|
1103
1390
|
# =============================================================================
|
|
1104
|
-
|
|
1391
|
+
|
|
1105
1392
|
async def upload_event_file(
|
|
1106
|
-
self,
|
|
1107
|
-
event_id: str,
|
|
1108
|
-
file_data: bytes,
|
|
1109
|
-
original_name: str,
|
|
1110
|
-
file_id: str
|
|
1393
|
+
self, event_id: str, file_data: bytes, original_name: str, file_id: str
|
|
1111
1394
|
) -> Dict[str, str]:
|
|
1112
1395
|
"""
|
|
1113
1396
|
Загружает файл события в Supabase Storage
|
|
1114
|
-
|
|
1397
|
+
|
|
1115
1398
|
Args:
|
|
1116
1399
|
event_id: ID события из БД (используется как папка)
|
|
1117
1400
|
file_data: Байты файла
|
|
1118
1401
|
original_name: Оригинальное имя файла (для метаданных)
|
|
1119
1402
|
file_id: Уникальный ID файла для хранения
|
|
1120
|
-
|
|
1403
|
+
|
|
1121
1404
|
Returns:
|
|
1122
1405
|
Dict с storage_path и original_name
|
|
1123
1406
|
"""
|
|
1124
1407
|
try:
|
|
1125
|
-
bucket_name =
|
|
1126
|
-
|
|
1408
|
+
bucket_name = "admin-events"
|
|
1409
|
+
|
|
1127
1410
|
# Формируем путь: admin-events/event_id/file_id.ext
|
|
1128
|
-
extension = original_name.split(
|
|
1411
|
+
extension = original_name.split(".")[-1] if "." in original_name else ""
|
|
1129
1412
|
storage_name = f"{file_id}.{extension}" if extension else file_id
|
|
1130
1413
|
storage_path = f"events/{event_id}/files/{storage_name}"
|
|
1131
|
-
|
|
1414
|
+
|
|
1132
1415
|
# Определяем MIME-type по оригинальному имени файла
|
|
1133
1416
|
import mimetypes
|
|
1417
|
+
|
|
1134
1418
|
content_type, _ = mimetypes.guess_type(original_name)
|
|
1135
1419
|
if not content_type:
|
|
1136
|
-
content_type =
|
|
1137
|
-
|
|
1420
|
+
content_type = "application/octet-stream"
|
|
1421
|
+
|
|
1138
1422
|
# Загружаем в Storage
|
|
1139
1423
|
self.client.storage.from_(bucket_name).upload(
|
|
1140
|
-
storage_path,
|
|
1141
|
-
file_data,
|
|
1142
|
-
file_options={"content-type": content_type}
|
|
1424
|
+
storage_path, file_data, file_options={"content-type": content_type}
|
|
1143
1425
|
)
|
|
1144
|
-
|
|
1426
|
+
|
|
1145
1427
|
logger.info(f"✅ Файл загружен в Storage: {storage_path}")
|
|
1146
|
-
|
|
1147
|
-
return {
|
|
1148
|
-
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1428
|
+
|
|
1429
|
+
return {"storage_path": storage_path}
|
|
1430
|
+
|
|
1151
1431
|
except Exception as e:
|
|
1152
1432
|
logger.error(f"❌ Ошибка загрузки файла в Storage: {e}")
|
|
1153
1433
|
raise
|
|
1154
|
-
|
|
1434
|
+
|
|
1155
1435
|
async def download_event_file(self, event_id: str, storage_path: str) -> bytes:
|
|
1156
1436
|
"""
|
|
1157
1437
|
Скачивает файл события из Supabase Storage
|
|
1158
|
-
|
|
1438
|
+
|
|
1159
1439
|
Args:
|
|
1160
1440
|
event_id: ID события
|
|
1161
1441
|
storage_path: Полный путь к файлу в Storage
|
|
1162
|
-
|
|
1442
|
+
|
|
1163
1443
|
Returns:
|
|
1164
1444
|
bytes: Содержимое файла
|
|
1165
1445
|
"""
|
|
1166
1446
|
try:
|
|
1167
|
-
bucket_name =
|
|
1168
|
-
|
|
1447
|
+
bucket_name = "admin-events"
|
|
1448
|
+
|
|
1169
1449
|
# Скачиваем файл
|
|
1170
1450
|
file_data = self.client.storage.from_(bucket_name).download(storage_path)
|
|
1171
|
-
|
|
1451
|
+
|
|
1172
1452
|
logger.info(f"✅ Файл скачан из Storage: {storage_path}")
|
|
1173
1453
|
return file_data
|
|
1174
|
-
|
|
1454
|
+
|
|
1175
1455
|
except Exception as e:
|
|
1176
1456
|
logger.error(f"❌ Ошибка скачивания файла из Storage: {e}")
|
|
1177
1457
|
raise
|
|
1178
|
-
|
|
1458
|
+
|
|
1179
1459
|
async def delete_event_files(self, event_id: str):
|
|
1180
1460
|
"""
|
|
1181
1461
|
Удаляет ВСЕ файлы события из Supabase Storage
|
|
1182
|
-
|
|
1462
|
+
|
|
1183
1463
|
Args:
|
|
1184
1464
|
event_id: ID события
|
|
1185
1465
|
"""
|
|
1186
1466
|
try:
|
|
1187
|
-
bucket_name =
|
|
1467
|
+
bucket_name = "admin-events"
|
|
1188
1468
|
event_path = f"events/{event_id}/files"
|
|
1189
|
-
|
|
1469
|
+
|
|
1190
1470
|
# Получаем список всех файлов в папке события
|
|
1191
1471
|
files_list = self.client.storage.from_(bucket_name).list(event_path)
|
|
1192
|
-
|
|
1472
|
+
|
|
1193
1473
|
if not files_list:
|
|
1194
1474
|
logger.info(f"ℹ️ Нет файлов для удаления в событии '{event_id}'")
|
|
1195
1475
|
return
|
|
1196
|
-
|
|
1476
|
+
|
|
1197
1477
|
# Формируем пути для удаления
|
|
1198
1478
|
file_paths = [f"{event_path}/{file['name']}" for file in files_list]
|
|
1199
|
-
|
|
1479
|
+
|
|
1200
1480
|
# Удаляем файлы
|
|
1201
1481
|
self.client.storage.from_(bucket_name).remove(file_paths)
|
|
1202
|
-
|
|
1203
|
-
logger.info(
|
|
1204
|
-
|
|
1482
|
+
|
|
1483
|
+
logger.info(
|
|
1484
|
+
f"✅ Удалено {len(file_paths)} файлов события '{event_id}' из Storage"
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1205
1487
|
except Exception as e:
|
|
1206
1488
|
logger.error(f"❌ Ошибка удаления файлов события из Storage: {e}")
|
|
1207
1489
|
# Не прерываем выполнение, только логируем
|
|
1208
1490
|
|
|
1209
1491
|
async def save_admin_event(
|
|
1210
|
-
self,
|
|
1211
|
-
event_name: str,
|
|
1212
|
-
event_data: Dict[str, Any],
|
|
1213
|
-
scheduled_datetime: datetime
|
|
1492
|
+
self, event_name: str, event_data: Dict[str, Any], scheduled_datetime: datetime
|
|
1214
1493
|
) -> Dict[str, Any]:
|
|
1215
1494
|
"""
|
|
1216
1495
|
Сохраняет админское событие в таблицу scheduled_events
|
|
1217
|
-
|
|
1496
|
+
|
|
1218
1497
|
Args:
|
|
1219
1498
|
event_name: Название события
|
|
1220
1499
|
event_data: Данные события (сегмент, сообщение, файлы)
|
|
1221
1500
|
scheduled_datetime: Дата и время отправки (должно быть в UTC с timezone info)
|
|
1222
|
-
|
|
1501
|
+
|
|
1223
1502
|
Returns:
|
|
1224
1503
|
Dict[str, Any]: {'id': str, 'event_type': str, ...} - все данные созданного события
|
|
1225
1504
|
"""
|
|
1226
1505
|
try:
|
|
1227
1506
|
import json
|
|
1228
|
-
|
|
1507
|
+
|
|
1229
1508
|
# Убеждаемся что datetime в правильном формате для PostgreSQL
|
|
1230
1509
|
# Если есть timezone info - используем, иначе предполагаем что это UTC
|
|
1231
1510
|
if scheduled_datetime.tzinfo is None:
|
|
1232
|
-
logger.warning(
|
|
1511
|
+
logger.warning(
|
|
1512
|
+
"⚠️ scheduled_datetime без timezone info, предполагаем UTC"
|
|
1513
|
+
)
|
|
1233
1514
|
from datetime import timezone
|
|
1515
|
+
|
|
1234
1516
|
scheduled_datetime = scheduled_datetime.replace(tzinfo=timezone.utc)
|
|
1235
|
-
|
|
1517
|
+
|
|
1236
1518
|
event_record = {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1519
|
+
"event_type": event_name,
|
|
1520
|
+
"event_category": "admin_event",
|
|
1521
|
+
"user_id": None, # Для всех пользователей
|
|
1522
|
+
"event_data": json.dumps(event_data, ensure_ascii=False),
|
|
1523
|
+
"scheduled_at": scheduled_datetime.isoformat(),
|
|
1524
|
+
"status": "pending",
|
|
1243
1525
|
}
|
|
1244
|
-
|
|
1245
|
-
response =
|
|
1526
|
+
|
|
1527
|
+
response = (
|
|
1528
|
+
self.client.table("scheduled_events").insert(event_record).execute()
|
|
1529
|
+
)
|
|
1246
1530
|
event = response.data[0]
|
|
1247
|
-
|
|
1248
|
-
logger.info(
|
|
1531
|
+
|
|
1532
|
+
logger.info(
|
|
1533
|
+
f"💾 Админское событие '{event_name}' сохранено в БД: {event['id']} на {scheduled_datetime.isoformat()}"
|
|
1534
|
+
)
|
|
1249
1535
|
return event
|
|
1250
|
-
|
|
1536
|
+
|
|
1251
1537
|
except Exception as e:
|
|
1252
1538
|
logger.error(f"❌ Ошибка сохранения админского события: {e}")
|
|
1253
1539
|
raise
|
|
@@ -1255,54 +1541,59 @@ class SupabaseClient:
|
|
|
1255
1541
|
async def get_admin_events(self, status: str = None) -> List[Dict[str, Any]]:
|
|
1256
1542
|
"""
|
|
1257
1543
|
Получает админские события
|
|
1258
|
-
|
|
1544
|
+
|
|
1259
1545
|
Args:
|
|
1260
1546
|
status: Фильтр по статусу (pending, completed, cancelled)
|
|
1261
|
-
|
|
1547
|
+
|
|
1262
1548
|
Returns:
|
|
1263
1549
|
List[Dict]: Список админских событий
|
|
1264
1550
|
"""
|
|
1265
1551
|
try:
|
|
1266
|
-
query =
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1552
|
+
query = (
|
|
1553
|
+
self.client.table("scheduled_events")
|
|
1554
|
+
.select("*")
|
|
1555
|
+
.eq("event_category", "admin_event")
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1270
1558
|
if status:
|
|
1271
|
-
query = query.eq(
|
|
1272
|
-
|
|
1273
|
-
response = query.order(
|
|
1274
|
-
|
|
1559
|
+
query = query.eq("status", status)
|
|
1560
|
+
|
|
1561
|
+
response = query.order("scheduled_at", desc=False).execute()
|
|
1562
|
+
|
|
1275
1563
|
logger.info(f"Найдено {len(response.data)} админских событий")
|
|
1276
1564
|
return response.data
|
|
1277
|
-
|
|
1565
|
+
|
|
1278
1566
|
except Exception as e:
|
|
1279
1567
|
logger.error(f"Ошибка получения админских событий: {e}")
|
|
1280
1568
|
return []
|
|
1281
|
-
|
|
1569
|
+
|
|
1282
1570
|
async def check_event_name_exists(self, event_name: str) -> bool:
|
|
1283
1571
|
"""
|
|
1284
1572
|
Проверяет существует ли активное событие с таким названием
|
|
1285
|
-
|
|
1573
|
+
|
|
1286
1574
|
Args:
|
|
1287
1575
|
event_name: Название события для проверки
|
|
1288
|
-
|
|
1576
|
+
|
|
1289
1577
|
Returns:
|
|
1290
1578
|
bool: True если активное событие с таким именем существует
|
|
1291
1579
|
"""
|
|
1292
1580
|
try:
|
|
1293
|
-
response =
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1581
|
+
response = (
|
|
1582
|
+
self.client.table("scheduled_events")
|
|
1583
|
+
.select("id", "event_type", "status")
|
|
1584
|
+
.eq("event_category", "admin_event")
|
|
1585
|
+
.eq("event_type", event_name)
|
|
1586
|
+
.eq("status", "pending")
|
|
1587
|
+
.execute()
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1299
1590
|
exists = len(response.data) > 0
|
|
1300
|
-
|
|
1591
|
+
|
|
1301
1592
|
if exists:
|
|
1302
1593
|
logger.info(f"⚠️ Найдено активное событие с названием '{event_name}'")
|
|
1303
|
-
|
|
1594
|
+
|
|
1304
1595
|
return exists
|
|
1305
|
-
|
|
1596
|
+
|
|
1306
1597
|
except Exception as e:
|
|
1307
1598
|
logger.error(f"Ошибка проверки названия события: {e}")
|
|
1308
|
-
return False
|
|
1599
|
+
return False
|