smart-bot-factory 0.3.6__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 +480 -324
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1145 -739
- 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 +682 -466
- 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.6.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.6.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,121 +1,145 @@
|
|
|
1
1
|
# Исправленный conversation_manager.py после обновления из GitHub - фикс расчета времени до автозавершения
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Optional, Dict, Any, List
|
|
5
4
|
from datetime import datetime, timezone
|
|
6
|
-
from
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
7
|
from aiogram.fsm.context import FSMContext
|
|
8
|
+
from aiogram.types import Message, User
|
|
8
9
|
|
|
9
10
|
from .bot_utils import get_global_var
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
class ConversationManager:
|
|
14
16
|
"""Управление диалогами между админами и пользователями"""
|
|
15
|
-
|
|
16
|
-
def __init__(
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, supabase_client, admin_manager, parse_mode, admin_session_timeout_minutes
|
|
20
|
+
):
|
|
17
21
|
self.supabase = supabase_client
|
|
18
22
|
self.admin_manager = admin_manager
|
|
19
23
|
self.parse_mode = parse_mode
|
|
20
24
|
self.admin_session_timeout_minutes = admin_session_timeout_minutes
|
|
21
|
-
|
|
25
|
+
|
|
22
26
|
async def start_admin_conversation(self, admin_id: int, user_id: int) -> bool:
|
|
23
27
|
"""Начинает диалог админа с пользователем"""
|
|
24
28
|
try:
|
|
25
29
|
from ..utils.debug_routing import debug_admin_conversation_creation
|
|
30
|
+
|
|
26
31
|
await debug_admin_conversation_creation(admin_id, user_id)
|
|
27
|
-
|
|
32
|
+
|
|
28
33
|
# Проверяем, что это действительно админ
|
|
29
34
|
if not self.admin_manager.is_admin(admin_id):
|
|
30
35
|
logger.warning(f"Попытка начать диалог не-админом {admin_id}")
|
|
31
36
|
return False
|
|
32
|
-
|
|
37
|
+
|
|
33
38
|
# Получаем активную сессию пользователя
|
|
34
39
|
session_info = await self.supabase.get_active_session(user_id)
|
|
35
40
|
if not session_info:
|
|
36
41
|
logger.warning(f"У пользователя {user_id} нет активной сессии")
|
|
37
42
|
return False
|
|
38
|
-
|
|
39
|
-
session_id = session_info[
|
|
43
|
+
|
|
44
|
+
session_id = session_info["id"]
|
|
40
45
|
logger.info(f"✅ Найдена активная сессия: {session_id}")
|
|
41
|
-
|
|
46
|
+
|
|
42
47
|
# Создаем запись о диалоге в БД
|
|
43
|
-
logger.info(
|
|
48
|
+
logger.info("🔧 Создаем запись о диалоге в БД...")
|
|
44
49
|
conversation_id = await self.supabase.start_admin_conversation(
|
|
45
50
|
admin_id, user_id, session_id
|
|
46
51
|
)
|
|
47
52
|
logger.info(f"✅ Диалог создан с ID: {conversation_id}")
|
|
48
|
-
|
|
53
|
+
|
|
49
54
|
# Показываем последние 5 сообщений
|
|
50
55
|
await self._show_recent_messages(admin_id, user_id, session_id)
|
|
51
|
-
|
|
52
|
-
logger.info(
|
|
56
|
+
|
|
57
|
+
logger.info(
|
|
58
|
+
f"🎉 Диалог успешно начат: админ {admin_id} -> пользователь {user_id}"
|
|
59
|
+
)
|
|
53
60
|
return True
|
|
54
|
-
|
|
61
|
+
|
|
55
62
|
except Exception as e:
|
|
56
|
-
logger.error(
|
|
63
|
+
logger.error(
|
|
64
|
+
f"❌ Ошибка начала диалога админа {admin_id} с пользователем {user_id}: {e}"
|
|
65
|
+
)
|
|
57
66
|
logger.exception("Полный стек ошибки:")
|
|
58
67
|
return False
|
|
59
68
|
|
|
60
69
|
async def _show_recent_messages(self, admin_id: int, user_id: int, session_id: str):
|
|
61
70
|
"""Показывает последние сообщения пользователя"""
|
|
62
71
|
from .message_sender import send_message_by_human
|
|
63
|
-
|
|
72
|
+
|
|
64
73
|
try:
|
|
65
74
|
# Получаем последние 5 сообщений (сортируем по убыванию и берем первые 5)
|
|
66
|
-
response =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
response = (
|
|
76
|
+
self.supabase.client.table("sales_messages")
|
|
77
|
+
.select("role", "content", "created_at")
|
|
78
|
+
.eq("session_id", session_id)
|
|
79
|
+
.order("created_at", desc=True)
|
|
80
|
+
.limit(5)
|
|
81
|
+
.execute()
|
|
82
|
+
)
|
|
83
|
+
|
|
70
84
|
recent_messages = response.data if response.data else []
|
|
71
|
-
|
|
85
|
+
|
|
72
86
|
if not recent_messages:
|
|
73
|
-
await send_message_by_human(
|
|
87
|
+
await send_message_by_human(
|
|
88
|
+
admin_id, "📭 Нет сообщений в текущей сессии"
|
|
89
|
+
)
|
|
74
90
|
return
|
|
75
|
-
|
|
91
|
+
|
|
76
92
|
# Получаем красивое имя пользователя
|
|
77
93
|
user_display = await self.get_user_display_name(user_id)
|
|
78
|
-
|
|
94
|
+
|
|
79
95
|
header = f"📜 Последние сообщения с {user_display}\n{'━' * 40}"
|
|
80
96
|
await send_message_by_human(admin_id, header)
|
|
81
|
-
|
|
97
|
+
|
|
82
98
|
# Разворачиваем чтобы показать в хронологическом порядке (старые -> новые)
|
|
83
99
|
for msg in reversed(recent_messages):
|
|
84
|
-
role_emoji = "👤" if msg[
|
|
85
|
-
timestamp = datetime.fromisoformat(
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
role_emoji = "👤" if msg["role"] == "user" else "🤖"
|
|
101
|
+
timestamp = datetime.fromisoformat(
|
|
102
|
+
msg["created_at"].replace("Z", "+00:00")
|
|
103
|
+
)
|
|
104
|
+
time_str = timestamp.strftime("%H:%M")
|
|
105
|
+
|
|
88
106
|
# Сокращаем длинные сообщения
|
|
89
|
-
content = self._truncate_message(msg[
|
|
90
|
-
|
|
107
|
+
content = self._truncate_message(msg["content"])
|
|
108
|
+
|
|
91
109
|
message_text = f"{role_emoji} {time_str} | {content}"
|
|
92
|
-
|
|
110
|
+
|
|
93
111
|
await send_message_by_human(admin_id, message_text)
|
|
94
|
-
|
|
112
|
+
|
|
95
113
|
# Разделитель
|
|
96
|
-
await send_message_by_human(
|
|
97
|
-
|
|
114
|
+
await send_message_by_human(
|
|
115
|
+
admin_id,
|
|
116
|
+
f"{'━' * 40}\n💬 Диалог начат. Ваши сообщения будут переданы пользователю.",
|
|
117
|
+
)
|
|
118
|
+
|
|
98
119
|
except Exception as e:
|
|
99
120
|
logger.error(f"Ошибка показа последних сообщений: {e}")
|
|
100
121
|
|
|
101
122
|
async def get_user_display_name(self, user_id: int) -> str:
|
|
102
123
|
"""Получает красивое отображение пользователя с username"""
|
|
103
124
|
try:
|
|
104
|
-
response =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
response = (
|
|
126
|
+
self.supabase.client.table("sales_users")
|
|
127
|
+
.select("first_name", "last_name", "username")
|
|
128
|
+
.eq("telegram_id", user_id)
|
|
129
|
+
.execute()
|
|
130
|
+
)
|
|
131
|
+
|
|
108
132
|
if response.data:
|
|
109
133
|
user_info = response.data[0]
|
|
110
134
|
name_parts = []
|
|
111
|
-
if user_info.get(
|
|
112
|
-
name_parts.append(user_info[
|
|
113
|
-
if user_info.get(
|
|
114
|
-
name_parts.append(user_info[
|
|
115
|
-
|
|
135
|
+
if user_info.get("first_name"):
|
|
136
|
+
name_parts.append(user_info["first_name"])
|
|
137
|
+
if user_info.get("last_name"):
|
|
138
|
+
name_parts.append(user_info["last_name"])
|
|
139
|
+
|
|
116
140
|
name = " ".join(name_parts) if name_parts else ""
|
|
117
|
-
|
|
118
|
-
if user_info.get(
|
|
141
|
+
|
|
142
|
+
if user_info.get("username"):
|
|
119
143
|
if name:
|
|
120
144
|
return f"{name} (@{user_info['username']})"
|
|
121
145
|
else:
|
|
@@ -126,7 +150,7 @@ class ConversationManager:
|
|
|
126
150
|
return f"ID {user_id}"
|
|
127
151
|
else:
|
|
128
152
|
return f"ID {user_id}"
|
|
129
|
-
|
|
153
|
+
|
|
130
154
|
except Exception as e:
|
|
131
155
|
logger.error(f"Ошибка получения информации о пользователе {user_id}: {e}")
|
|
132
156
|
return f"ID {user_id}"
|
|
@@ -135,101 +159,109 @@ class ConversationManager:
|
|
|
135
159
|
"""Сокращает длинные сообщения"""
|
|
136
160
|
if not text:
|
|
137
161
|
return ""
|
|
138
|
-
|
|
139
|
-
lines = text.split(
|
|
140
|
-
|
|
162
|
+
|
|
163
|
+
lines = text.split("\n")
|
|
164
|
+
|
|
141
165
|
if len(lines) <= max_lines:
|
|
142
166
|
return text
|
|
143
|
-
|
|
167
|
+
|
|
144
168
|
# Берем первые 3 и последние 3 строки
|
|
145
169
|
first_lines = lines[:3]
|
|
146
170
|
last_lines = lines[-3:]
|
|
147
|
-
|
|
148
|
-
truncated =
|
|
171
|
+
|
|
172
|
+
truncated = "\n".join(first_lines) + "\n...\n" + "\n".join(last_lines)
|
|
149
173
|
return truncated
|
|
150
|
-
|
|
174
|
+
|
|
151
175
|
async def end_admin_conversation(self, admin_id: int) -> bool:
|
|
152
176
|
"""Завершает текущий диалог админа"""
|
|
153
177
|
try:
|
|
154
178
|
await self.supabase.end_admin_conversations(admin_id)
|
|
155
179
|
logger.info(f"Завершен диалог админа {admin_id}")
|
|
156
180
|
return True
|
|
157
|
-
|
|
181
|
+
|
|
158
182
|
except Exception as e:
|
|
159
183
|
logger.error(f"Ошибка завершения диалога админа {admin_id}: {e}")
|
|
160
184
|
return False
|
|
161
|
-
|
|
185
|
+
|
|
162
186
|
async def is_user_in_admin_chat(self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
163
187
|
"""Проверяет, ведется ли диалог с пользователем"""
|
|
164
188
|
try:
|
|
165
189
|
logger.debug(f"🔍 Проверяем диалог с пользователем {user_id}")
|
|
166
|
-
|
|
190
|
+
|
|
167
191
|
conversation = await self.supabase.get_user_admin_conversation(user_id)
|
|
168
|
-
|
|
192
|
+
|
|
169
193
|
if conversation:
|
|
170
|
-
logger.debug(
|
|
194
|
+
logger.debug(
|
|
195
|
+
f"✅ Найден активный диалог: админ {conversation['admin_id']}, ID: {conversation['id']}"
|
|
196
|
+
)
|
|
171
197
|
else:
|
|
172
198
|
logger.debug(f"❌ Активный диалог не найден для пользователя {user_id}")
|
|
173
|
-
|
|
199
|
+
|
|
174
200
|
return conversation
|
|
175
|
-
|
|
201
|
+
|
|
176
202
|
except Exception as e:
|
|
177
203
|
logger.error(f"❌ Ошибка проверки диалога пользователя {user_id}: {e}")
|
|
178
204
|
return None
|
|
179
|
-
|
|
180
|
-
async def get_admin_active_conversation(
|
|
205
|
+
|
|
206
|
+
async def get_admin_active_conversation(
|
|
207
|
+
self, admin_id: int
|
|
208
|
+
) -> Optional[Dict[str, Any]]:
|
|
181
209
|
"""Получает активный диалог админа"""
|
|
182
210
|
try:
|
|
183
211
|
return await self.supabase.get_admin_active_conversation(admin_id)
|
|
184
|
-
|
|
212
|
+
|
|
185
213
|
except Exception as e:
|
|
186
214
|
logger.error(f"Ошибка получения активного диалога админа {admin_id}: {e}")
|
|
187
215
|
return None
|
|
188
216
|
|
|
189
|
-
async def forward_message_to_admin(
|
|
217
|
+
async def forward_message_to_admin(
|
|
218
|
+
self, message: Message, conversation: Dict[str, Any]
|
|
219
|
+
):
|
|
190
220
|
"""Пересылает сообщение пользователя админу"""
|
|
191
221
|
from .message_sender import send_message_by_human
|
|
192
|
-
|
|
193
|
-
admin_id = conversation[
|
|
222
|
+
|
|
223
|
+
admin_id = conversation["admin_id"]
|
|
194
224
|
user_id = message.from_user.id
|
|
195
|
-
|
|
225
|
+
|
|
196
226
|
logger.info(f"📤 Пересылаем сообщение от {user_id} админу {admin_id}")
|
|
197
|
-
|
|
227
|
+
|
|
198
228
|
# Форматируем сообщение для админа
|
|
199
229
|
user_info = self._format_user_info(message.from_user)
|
|
200
|
-
|
|
230
|
+
|
|
201
231
|
# Время с начала диалога
|
|
202
232
|
try:
|
|
203
|
-
start_time = datetime.fromisoformat(
|
|
233
|
+
start_time = datetime.fromisoformat(
|
|
234
|
+
conversation["started_at"].replace("Z", "+00:00")
|
|
235
|
+
)
|
|
204
236
|
duration = datetime.now(start_time.tzinfo) - start_time
|
|
205
237
|
minutes = int(duration.total_seconds() / 60)
|
|
206
238
|
except Exception as e:
|
|
207
239
|
logger.error(f"Ошибка расчета времени диалога: {e}")
|
|
208
240
|
minutes = 0
|
|
209
|
-
|
|
241
|
+
|
|
210
242
|
# ✅ ИСПРАВЛЕНИЕ: Убираем символы, которые могут вызвать ошибки парсинга
|
|
211
243
|
safe_user_info = self._escape_markdown(user_info)
|
|
212
244
|
safe_message_text = self._escape_markdown(message.text or "")
|
|
213
|
-
|
|
245
|
+
|
|
214
246
|
header = f"👤 {safe_user_info} | ⏱️ {minutes} мин"
|
|
215
247
|
separator = "━" * 20
|
|
216
|
-
|
|
248
|
+
|
|
217
249
|
full_message = f"{header}\n{separator}\n{safe_message_text}"
|
|
218
|
-
|
|
250
|
+
|
|
219
251
|
try:
|
|
220
252
|
logger.info(f"📨 Отправляем сообщение админу {admin_id}")
|
|
221
|
-
|
|
253
|
+
|
|
222
254
|
# ✅ ИСПРАВЛЕНИЕ: Убираем parse_mode='Markdown' чтобы избежать ошибок парсинга
|
|
223
255
|
await send_message_by_human(admin_id, full_message)
|
|
224
|
-
|
|
256
|
+
|
|
225
257
|
logger.info(f"✅ Сообщение успешно отправлено админу {admin_id}")
|
|
226
|
-
|
|
258
|
+
|
|
227
259
|
# Добавляем кнопки управления
|
|
228
260
|
await self._send_admin_controls(admin_id, user_id)
|
|
229
|
-
|
|
261
|
+
|
|
230
262
|
except Exception as e:
|
|
231
263
|
logger.error(f"❌ Ошибка пересылки сообщения админу {admin_id}: {e}")
|
|
232
|
-
|
|
264
|
+
|
|
233
265
|
# ✅ ДОБАВЛЯЕМ: Fallback отправка без форматирования
|
|
234
266
|
try:
|
|
235
267
|
simple_message = f"Сообщение от пользователя {user_id}:\n{message.text}"
|
|
@@ -243,63 +275,90 @@ class ConversationManager:
|
|
|
243
275
|
"""Экранирует специальные символы Markdown"""
|
|
244
276
|
if not text:
|
|
245
277
|
return ""
|
|
246
|
-
|
|
278
|
+
|
|
247
279
|
# Символы, которые нужно экранировать в Markdown
|
|
248
|
-
markdown_chars = [
|
|
249
|
-
|
|
280
|
+
markdown_chars = [
|
|
281
|
+
"*",
|
|
282
|
+
"_",
|
|
283
|
+
"`",
|
|
284
|
+
"[",
|
|
285
|
+
"]",
|
|
286
|
+
"(",
|
|
287
|
+
")",
|
|
288
|
+
"~",
|
|
289
|
+
">",
|
|
290
|
+
"#",
|
|
291
|
+
"+",
|
|
292
|
+
"-",
|
|
293
|
+
"=",
|
|
294
|
+
"|",
|
|
295
|
+
"{",
|
|
296
|
+
"}",
|
|
297
|
+
".",
|
|
298
|
+
"!",
|
|
299
|
+
]
|
|
300
|
+
|
|
250
301
|
escaped_text = text
|
|
251
302
|
for char in markdown_chars:
|
|
252
|
-
escaped_text = escaped_text.replace(char, f
|
|
253
|
-
|
|
303
|
+
escaped_text = escaped_text.replace(char, f"\\{char}")
|
|
304
|
+
|
|
254
305
|
return escaped_text
|
|
255
|
-
|
|
256
|
-
async def forward_message_to_user(
|
|
306
|
+
|
|
307
|
+
async def forward_message_to_user(
|
|
308
|
+
self, message: Message, conversation: Dict[str, Any]
|
|
309
|
+
):
|
|
257
310
|
"""Пересылает сообщение админа пользователю"""
|
|
258
311
|
from .message_sender import send_message_by_human
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
312
|
+
|
|
313
|
+
supabase_client = get_global_var("supabase_client")
|
|
314
|
+
|
|
315
|
+
user_id = conversation["user_id"]
|
|
316
|
+
|
|
263
317
|
try:
|
|
264
318
|
# Отправляем сообщение как от бота
|
|
265
|
-
parse_mode = self.parse_mode if self.parse_mode !=
|
|
319
|
+
parse_mode = self.parse_mode if self.parse_mode != "None" else None
|
|
266
320
|
await send_message_by_human(user_id, message.text, parse_mode=parse_mode)
|
|
267
|
-
|
|
321
|
+
|
|
268
322
|
# Сохраняем в БД как сообщение ассистента
|
|
269
323
|
session_info = await supabase_client.get_active_session(user_id)
|
|
270
324
|
if session_info:
|
|
271
325
|
await supabase_client.add_message(
|
|
272
|
-
session_id=session_info[
|
|
273
|
-
role=
|
|
326
|
+
session_id=session_info["id"],
|
|
327
|
+
role="assistant",
|
|
274
328
|
content=message.text,
|
|
275
|
-
message_type=
|
|
276
|
-
metadata={
|
|
329
|
+
message_type="text",
|
|
330
|
+
metadata={"from_admin": message.from_user.id},
|
|
277
331
|
)
|
|
278
|
-
|
|
332
|
+
|
|
279
333
|
except Exception as e:
|
|
280
334
|
logger.error(f"Ошибка пересылки сообщения пользователю {user_id}: {e}")
|
|
281
|
-
|
|
335
|
+
|
|
282
336
|
async def _send_admin_controls(self, admin_id: int, user_id: int):
|
|
283
337
|
"""Отправляет кнопки управления диалогом"""
|
|
338
|
+
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
|
339
|
+
|
|
284
340
|
from .message_sender import send_message_by_human
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
341
|
+
|
|
342
|
+
keyboard = InlineKeyboardMarkup(
|
|
343
|
+
inline_keyboard=[
|
|
344
|
+
[
|
|
345
|
+
InlineKeyboardButton(
|
|
346
|
+
text="📋 История", callback_data=f"admin_history_{user_id}"
|
|
347
|
+
),
|
|
348
|
+
InlineKeyboardButton(
|
|
349
|
+
text="✅ Завершить", callback_data=f"admin_end_{user_id}"
|
|
350
|
+
),
|
|
351
|
+
]
|
|
291
352
|
]
|
|
292
|
-
|
|
293
|
-
|
|
353
|
+
)
|
|
354
|
+
|
|
294
355
|
try:
|
|
295
356
|
await send_message_by_human(
|
|
296
|
-
admin_id,
|
|
297
|
-
"🎛️ Управление диалогом:",
|
|
298
|
-
reply_markup=keyboard
|
|
357
|
+
admin_id, "🎛️ Управление диалогом:", reply_markup=keyboard
|
|
299
358
|
)
|
|
300
359
|
except Exception as e:
|
|
301
360
|
logger.error(f"Ошибка отправки кнопок управления: {e}")
|
|
302
|
-
|
|
361
|
+
|
|
303
362
|
def _format_user_info(self, user: User) -> str:
|
|
304
363
|
"""Форматирует информацию о пользователе"""
|
|
305
364
|
name_parts = []
|
|
@@ -307,14 +366,14 @@ class ConversationManager:
|
|
|
307
366
|
name_parts.append(user.first_name)
|
|
308
367
|
if user.last_name:
|
|
309
368
|
name_parts.append(user.last_name)
|
|
310
|
-
|
|
369
|
+
|
|
311
370
|
name = " ".join(name_parts) if name_parts else "Без имени"
|
|
312
|
-
|
|
371
|
+
|
|
313
372
|
if user.username:
|
|
314
373
|
return f"{name} (@{user.username})"
|
|
315
374
|
else:
|
|
316
375
|
return f"{name} (ID: {user.id})"
|
|
317
|
-
|
|
376
|
+
|
|
318
377
|
async def cleanup_expired_conversations(self):
|
|
319
378
|
"""Очищает просроченные диалоги"""
|
|
320
379
|
try:
|
|
@@ -322,20 +381,20 @@ class ConversationManager:
|
|
|
322
381
|
if ended_count > 0:
|
|
323
382
|
logger.info(f"Очищено {ended_count} просроченных диалогов")
|
|
324
383
|
return ended_count
|
|
325
|
-
|
|
384
|
+
|
|
326
385
|
except Exception as e:
|
|
327
386
|
logger.error(f"Ошибка очистки просроченных диалогов: {e}")
|
|
328
387
|
return 0
|
|
329
|
-
|
|
388
|
+
|
|
330
389
|
async def get_conversation_stats(self) -> Dict[str, int]:
|
|
331
390
|
"""Возвращает статистику диалогов"""
|
|
332
391
|
try:
|
|
333
392
|
# Здесь можно добавить запросы к БД для получения статистики
|
|
334
393
|
# Пока возвращаем заглушку
|
|
335
394
|
return {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
395
|
+
"active_conversations": 0,
|
|
396
|
+
"completed_today": 0,
|
|
397
|
+
"total_admin_messages": 0,
|
|
339
398
|
}
|
|
340
399
|
except Exception as e:
|
|
341
400
|
logger.error(f"Ошибка получения статистики диалогов: {e}")
|
|
@@ -344,46 +403,62 @@ class ConversationManager:
|
|
|
344
403
|
async def get_active_conversations(self) -> List[Dict[str, Any]]:
|
|
345
404
|
"""Получает все активные диалоги админов"""
|
|
346
405
|
try:
|
|
347
|
-
logger.info(
|
|
348
|
-
|
|
406
|
+
logger.info("🔍 Ищем активные диалоги админов...")
|
|
407
|
+
|
|
349
408
|
# Получаем все активные диалоги
|
|
350
|
-
response =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
409
|
+
response = (
|
|
410
|
+
self.supabase.client.table("admin_user_conversations")
|
|
411
|
+
.select("id", "admin_id", "user_id", "started_at", "auto_end_at")
|
|
412
|
+
.eq("status", "active")
|
|
413
|
+
.order("started_at", desc=True)
|
|
414
|
+
.execute()
|
|
415
|
+
)
|
|
416
|
+
|
|
354
417
|
logger.info(f"📊 Найдено {len(response.data)} активных диалогов в БД")
|
|
355
|
-
|
|
418
|
+
|
|
356
419
|
conversations = []
|
|
357
420
|
for conv in response.data:
|
|
358
421
|
# Получаем информацию о пользователе
|
|
359
422
|
try:
|
|
360
|
-
user_response =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
423
|
+
user_response = (
|
|
424
|
+
self.supabase.client.table("sales_users")
|
|
425
|
+
.select("first_name", "last_name", "username")
|
|
426
|
+
.eq("telegram_id", conv["user_id"])
|
|
427
|
+
.execute()
|
|
428
|
+
)
|
|
429
|
+
|
|
364
430
|
user_info = user_response.data[0] if user_response.data else {}
|
|
365
431
|
except Exception as e:
|
|
366
|
-
logger.error(
|
|
432
|
+
logger.error(
|
|
433
|
+
f"Ошибка получения данных пользователя {conv['user_id']}: {e}"
|
|
434
|
+
)
|
|
367
435
|
user_info = {}
|
|
368
|
-
|
|
436
|
+
|
|
369
437
|
# Получаем информацию об админе
|
|
370
438
|
try:
|
|
371
|
-
admin_response =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
439
|
+
admin_response = (
|
|
440
|
+
self.supabase.client.table("sales_admins")
|
|
441
|
+
.select("first_name", "last_name", "username")
|
|
442
|
+
.eq("telegram_id", conv["admin_id"])
|
|
443
|
+
.execute()
|
|
444
|
+
)
|
|
445
|
+
|
|
375
446
|
admin_info = admin_response.data[0] if admin_response.data else {}
|
|
376
447
|
except Exception as e:
|
|
377
|
-
logger.error(
|
|
448
|
+
logger.error(
|
|
449
|
+
f"Ошибка получения данных админа {conv['admin_id']}: {e}"
|
|
450
|
+
)
|
|
378
451
|
admin_info = {}
|
|
379
|
-
|
|
380
|
-
conv[
|
|
381
|
-
conv[
|
|
452
|
+
|
|
453
|
+
conv["user_info"] = user_info
|
|
454
|
+
conv["admin_info"] = admin_info
|
|
382
455
|
conversations.append(conv)
|
|
383
|
-
|
|
384
|
-
logger.info(
|
|
456
|
+
|
|
457
|
+
logger.info(
|
|
458
|
+
f"✅ Получено {len(conversations)} активных диалогов с дополнительной информацией"
|
|
459
|
+
)
|
|
385
460
|
return conversations
|
|
386
|
-
|
|
461
|
+
|
|
387
462
|
except Exception as e:
|
|
388
463
|
logger.error(f"❌ Ошибка получения активных диалогов: {e}")
|
|
389
464
|
return []
|
|
@@ -392,133 +467,145 @@ class ConversationManager:
|
|
|
392
467
|
"""Форматирует список активных диалогов - ИСПРАВЛЕН РАСЧЕТ ВРЕМЕНИ АВТОЗАВЕРШЕНИЯ"""
|
|
393
468
|
if not conversations:
|
|
394
469
|
return "💬 Нет активных диалогов"
|
|
395
|
-
|
|
470
|
+
|
|
396
471
|
lines = ["💬 АКТИВНЫЕ ДИАЛОГИ:", ""]
|
|
397
|
-
|
|
472
|
+
|
|
398
473
|
for i, conv in enumerate(conversations, 1):
|
|
399
474
|
# Информация о пользователе
|
|
400
|
-
user_info = conv.get(
|
|
475
|
+
user_info = conv.get("user_info", {})
|
|
401
476
|
user_name = []
|
|
402
|
-
if user_info.get(
|
|
403
|
-
user_name.append(user_info[
|
|
404
|
-
if user_info.get(
|
|
405
|
-
user_name.append(user_info[
|
|
406
|
-
|
|
477
|
+
if user_info.get("first_name"):
|
|
478
|
+
user_name.append(user_info["first_name"])
|
|
479
|
+
if user_info.get("last_name"):
|
|
480
|
+
user_name.append(user_info["last_name"])
|
|
481
|
+
|
|
407
482
|
user_display = " ".join(user_name) if user_name else f"ID {conv['user_id']}"
|
|
408
|
-
if user_info.get(
|
|
483
|
+
if user_info.get("username"):
|
|
409
484
|
user_display += f" (@{user_info['username']})"
|
|
410
|
-
|
|
485
|
+
|
|
411
486
|
# Информация об админе
|
|
412
|
-
admin_info = conv.get(
|
|
487
|
+
admin_info = conv.get("admin_info", {})
|
|
413
488
|
admin_name = []
|
|
414
|
-
if admin_info.get(
|
|
415
|
-
admin_name.append(admin_info[
|
|
416
|
-
if admin_info.get(
|
|
417
|
-
admin_name.append(admin_info[
|
|
418
|
-
|
|
419
|
-
admin_display =
|
|
420
|
-
|
|
489
|
+
if admin_info.get("first_name"):
|
|
490
|
+
admin_name.append(admin_info["first_name"])
|
|
491
|
+
if admin_info.get("last_name"):
|
|
492
|
+
admin_name.append(admin_info["last_name"])
|
|
493
|
+
|
|
494
|
+
admin_display = (
|
|
495
|
+
" ".join(admin_name) if admin_name else f"ID {conv['admin_id']}"
|
|
496
|
+
)
|
|
497
|
+
|
|
421
498
|
# 🔧 ИСПРАВЛЕНИЕ: Правильный расчет времени с учетом timezone
|
|
422
499
|
try:
|
|
423
|
-
started_at_str = conv[
|
|
500
|
+
started_at_str = conv["started_at"]
|
|
424
501
|
logger.debug(f"🕐 Диалог {i}: started_at = '{started_at_str}'")
|
|
425
|
-
|
|
502
|
+
|
|
426
503
|
# Парсим время начала с правильной обработкой timezone
|
|
427
|
-
if started_at_str.endswith(
|
|
428
|
-
start_time = datetime.fromisoformat(
|
|
429
|
-
|
|
504
|
+
if started_at_str.endswith("Z"):
|
|
505
|
+
start_time = datetime.fromisoformat(
|
|
506
|
+
started_at_str.replace("Z", "+00:00")
|
|
507
|
+
)
|
|
508
|
+
elif "+" in started_at_str or started_at_str.count(":") >= 3:
|
|
430
509
|
# Уже есть timezone info
|
|
431
510
|
start_time = datetime.fromisoformat(started_at_str)
|
|
432
511
|
else:
|
|
433
512
|
# Если нет timezone info, считаем что это UTC
|
|
434
513
|
naive_time = datetime.fromisoformat(started_at_str)
|
|
435
514
|
start_time = naive_time.replace(tzinfo=timezone.utc)
|
|
436
|
-
|
|
515
|
+
|
|
437
516
|
logger.debug(f"✅ Парсед start_time: {start_time}")
|
|
438
|
-
|
|
517
|
+
|
|
439
518
|
# Получаем текущее время в UTC
|
|
440
519
|
now_utc = datetime.now(timezone.utc)
|
|
441
520
|
logger.debug(f"🕐 now_utc: {now_utc}")
|
|
442
|
-
|
|
521
|
+
|
|
443
522
|
# Приводим start_time к UTC если нужно
|
|
444
523
|
if start_time.tzinfo != timezone.utc:
|
|
445
524
|
start_time_utc = start_time.astimezone(timezone.utc)
|
|
446
525
|
else:
|
|
447
526
|
start_time_utc = start_time
|
|
448
|
-
|
|
527
|
+
|
|
449
528
|
# Длительность диалога
|
|
450
529
|
duration = now_utc - start_time_utc
|
|
451
530
|
minutes = max(0, int(duration.total_seconds() / 60))
|
|
452
531
|
logger.debug(f"⏱️ Длительность: {minutes} минут")
|
|
453
|
-
|
|
532
|
+
|
|
454
533
|
except Exception as e:
|
|
455
534
|
logger.error(f"❌ Ошибка расчета времени диалога {i}: {e}")
|
|
456
535
|
logger.error(f" started_at_str: '{started_at_str}'")
|
|
457
536
|
minutes = 0
|
|
458
|
-
|
|
537
|
+
|
|
459
538
|
# 🔧 ИСПРАВЛЕНИЕ: Время до автозавершения с правильной обработкой timezone
|
|
460
539
|
try:
|
|
461
|
-
auto_end_str = conv[
|
|
540
|
+
auto_end_str = conv["auto_end_at"]
|
|
462
541
|
logger.debug(f"🕐 Диалог {i}: auto_end_at = '{auto_end_str}'")
|
|
463
|
-
|
|
542
|
+
|
|
464
543
|
# Парсим время автозавершения с правильной обработкой timezone
|
|
465
|
-
if auto_end_str.endswith(
|
|
466
|
-
auto_end = datetime.fromisoformat(
|
|
467
|
-
|
|
544
|
+
if auto_end_str.endswith("Z"):
|
|
545
|
+
auto_end = datetime.fromisoformat(
|
|
546
|
+
auto_end_str.replace("Z", "+00:00")
|
|
547
|
+
)
|
|
548
|
+
elif "+" in auto_end_str or auto_end_str.count(":") >= 3:
|
|
468
549
|
# Уже есть timezone info
|
|
469
550
|
auto_end = datetime.fromisoformat(auto_end_str)
|
|
470
551
|
else:
|
|
471
552
|
# Если нет timezone info, считаем что это UTC
|
|
472
553
|
naive_time = datetime.fromisoformat(auto_end_str)
|
|
473
554
|
auto_end = naive_time.replace(tzinfo=timezone.utc)
|
|
474
|
-
|
|
555
|
+
|
|
475
556
|
logger.debug(f"✅ Парсед auto_end: {auto_end}")
|
|
476
|
-
|
|
557
|
+
|
|
477
558
|
# Получаем текущее время в UTC
|
|
478
559
|
now_utc = datetime.now(timezone.utc)
|
|
479
560
|
logger.debug(f"🕐 now_utc для auto_end: {now_utc}")
|
|
480
|
-
|
|
561
|
+
|
|
481
562
|
# Приводим auto_end к UTC если нужно
|
|
482
563
|
if auto_end.tzinfo != timezone.utc:
|
|
483
564
|
auto_end_utc = auto_end.astimezone(timezone.utc)
|
|
484
565
|
else:
|
|
485
566
|
auto_end_utc = auto_end
|
|
486
|
-
|
|
567
|
+
|
|
487
568
|
# Оставшееся время
|
|
488
569
|
remaining = auto_end_utc - now_utc
|
|
489
570
|
remaining_minutes = max(0, int(remaining.total_seconds() / 60))
|
|
490
571
|
logger.debug(f"⏰ Remaining: {remaining_minutes} минут")
|
|
491
|
-
|
|
572
|
+
|
|
492
573
|
# 🔧 ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: вычисляем планируемую длительность
|
|
493
574
|
if start_time.tzinfo != timezone.utc:
|
|
494
|
-
start_time_utc = start_time.astimezone(timezone.utc)
|
|
575
|
+
start_time_utc = start_time.astimezone(timezone.utc)
|
|
495
576
|
else:
|
|
496
577
|
start_time_utc = start_time
|
|
497
|
-
|
|
578
|
+
|
|
498
579
|
planned_duration = auto_end_utc - start_time_utc
|
|
499
580
|
planned_minutes = int(planned_duration.total_seconds() / 60)
|
|
500
581
|
logger.debug(f"📏 Планируемая длительность: {planned_minutes} минут")
|
|
501
|
-
|
|
582
|
+
|
|
502
583
|
# Проверяем корректность
|
|
503
584
|
expected_timeout = self.admin_session_timeout_minutes
|
|
504
|
-
|
|
585
|
+
|
|
505
586
|
logger.debug(f"🕐 expected_timeout: {expected_timeout}")
|
|
506
|
-
|
|
507
|
-
if
|
|
508
|
-
|
|
509
|
-
|
|
587
|
+
|
|
588
|
+
if (
|
|
589
|
+
abs(planned_minutes - expected_timeout) > 2
|
|
590
|
+
): # допускаем погрешность в 2 минуты
|
|
591
|
+
logger.warning(
|
|
592
|
+
f"⚠️ Диалог {i}: планируемая длительность {planned_minutes} мин не соответствует конфигу {expected_timeout} мин"
|
|
593
|
+
)
|
|
594
|
+
|
|
510
595
|
except Exception as e:
|
|
511
|
-
logger.error(
|
|
596
|
+
logger.error(
|
|
597
|
+
f"❌ Ошибка расчета времени автозавершения диалога {i}: {e}"
|
|
598
|
+
)
|
|
512
599
|
logger.error(f" auto_end_str: '{auto_end_str}'")
|
|
513
600
|
remaining_minutes = 0
|
|
514
|
-
|
|
601
|
+
|
|
515
602
|
lines.append(f"{i}. 👤 {user_display}")
|
|
516
603
|
lines.append(f" 👑 Админ: {admin_display}")
|
|
517
604
|
lines.append(f" ⏱️ Длительность: {minutes} мин")
|
|
518
605
|
lines.append(f" ⏰ Автозавершение через: {remaining_minutes} мин")
|
|
519
606
|
lines.append(f" 🎛️ /чат {conv['user_id']}")
|
|
520
607
|
lines.append("")
|
|
521
|
-
|
|
608
|
+
|
|
522
609
|
return "\n".join(lines)
|
|
523
610
|
|
|
524
611
|
async def route_admin_message(self, message: Message, state: FSMContext) -> bool:
|
|
@@ -527,17 +614,17 @@ class ConversationManager:
|
|
|
527
614
|
Возвращает True если сообщение обработано как админское
|
|
528
615
|
"""
|
|
529
616
|
admin_id = message.from_user.id
|
|
530
|
-
|
|
617
|
+
|
|
531
618
|
# Проверяем админские команды
|
|
532
|
-
if message.text and message.text.startswith(
|
|
619
|
+
if message.text and message.text.startswith("/"):
|
|
533
620
|
return False # Команды обрабатываются отдельно
|
|
534
|
-
|
|
621
|
+
|
|
535
622
|
# Проверяем, ведется ли диалог с пользователем
|
|
536
623
|
conversation = await self.get_admin_active_conversation(admin_id)
|
|
537
|
-
|
|
624
|
+
|
|
538
625
|
if conversation:
|
|
539
626
|
# Пересылаем сообщение пользователю
|
|
540
627
|
await self.forward_message_to_user(message, conversation)
|
|
541
628
|
return True
|
|
542
|
-
|
|
543
|
-
return False
|
|
629
|
+
|
|
630
|
+
return False
|