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