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.

Files changed (45) hide show
  1. smart_bot_factory/admin/__init__.py +7 -7
  2. smart_bot_factory/admin/admin_events.py +483 -383
  3. smart_bot_factory/admin/admin_logic.py +234 -158
  4. smart_bot_factory/admin/admin_manager.py +68 -53
  5. smart_bot_factory/admin/admin_tester.py +46 -40
  6. smart_bot_factory/admin/timeout_checker.py +201 -153
  7. smart_bot_factory/aiogram_calendar/__init__.py +11 -3
  8. smart_bot_factory/aiogram_calendar/common.py +12 -18
  9. smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
  10. smart_bot_factory/aiogram_calendar/schemas.py +49 -28
  11. smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
  12. smart_bot_factory/analytics/analytics_manager.py +414 -392
  13. smart_bot_factory/cli.py +204 -148
  14. smart_bot_factory/config.py +123 -102
  15. smart_bot_factory/core/bot_utils.py +480 -324
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1145 -739
  18. smart_bot_factory/core/message_sender.py +287 -266
  19. smart_bot_factory/core/router.py +170 -100
  20. smart_bot_factory/core/router_manager.py +121 -83
  21. smart_bot_factory/core/states.py +4 -3
  22. smart_bot_factory/creation/__init__.py +1 -1
  23. smart_bot_factory/creation/bot_builder.py +320 -242
  24. smart_bot_factory/creation/bot_testing.py +440 -365
  25. smart_bot_factory/dashboard/__init__.py +1 -3
  26. smart_bot_factory/event/__init__.py +2 -7
  27. smart_bot_factory/handlers/handlers.py +682 -466
  28. smart_bot_factory/integrations/openai_client.py +218 -168
  29. smart_bot_factory/integrations/supabase_client.py +928 -637
  30. smart_bot_factory/message/__init__.py +18 -22
  31. smart_bot_factory/router/__init__.py +2 -2
  32. smart_bot_factory/setup_checker.py +162 -126
  33. smart_bot_factory/supabase/__init__.py +1 -1
  34. smart_bot_factory/supabase/client.py +631 -515
  35. smart_bot_factory/utils/__init__.py +2 -3
  36. smart_bot_factory/utils/debug_routing.py +38 -27
  37. smart_bot_factory/utils/prompt_loader.py +153 -120
  38. smart_bot_factory/utils/user_prompt_loader.py +55 -56
  39. smart_bot_factory/utm_link_generator.py +123 -116
  40. {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
  41. smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
  42. smart_bot_factory-0.3.6.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
  45. {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 aiogram.types import Message, User
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__(self, supabase_client, admin_manager, parse_mode, admin_session_timeout_minutes):
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['id']
43
+
44
+ session_id = session_info["id"]
40
45
  logger.info(f"✅ Найдена активная сессия: {session_id}")
41
-
46
+
42
47
  # Создаем запись о диалоге в БД
43
- logger.info(f"🔧 Создаем запись о диалоге в БД...")
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(f"🎉 Диалог успешно начат: админ {admin_id} -> пользователь {user_id}")
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(f"❌ Ошибка начала диалога админа {admin_id} с пользователем {user_id}: {e}")
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 = self.supabase.client.table('sales_messages').select(
67
- 'role', 'content', 'created_at'
68
- ).eq('session_id', session_id).order('created_at', desc=True).limit(5).execute()
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(admin_id, "📭 Нет сообщений в текущей сессии")
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['role'] == 'user' else "🤖"
85
- timestamp = datetime.fromisoformat(msg['created_at'].replace('Z', '+00:00'))
86
- time_str = timestamp.strftime('%H:%M')
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['content'])
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(admin_id, f"{'━' * 40}\n💬 Диалог начат. Ваши сообщения будут переданы пользователю.")
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 = self.supabase.client.table('sales_users').select(
105
- 'first_name', 'last_name', 'username'
106
- ).eq('telegram_id', user_id).execute()
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('first_name'):
112
- name_parts.append(user_info['first_name'])
113
- if user_info.get('last_name'):
114
- name_parts.append(user_info['last_name'])
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('username'):
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('\n')
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 = '\n'.join(first_lines) + '\n...\n' + '\n'.join(last_lines)
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(f"✅ Найден активный диалог: админ {conversation['admin_id']}, ID: {conversation['id']}")
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(self, admin_id: int) -> Optional[Dict[str, Any]]:
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(self, message: Message, conversation: Dict[str, Any]):
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['admin_id']
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(conversation['started_at'].replace('Z', '+00:00'))
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'\\{char}')
253
-
303
+ escaped_text = escaped_text.replace(char, f"\\{char}")
304
+
254
305
  return escaped_text
255
-
256
- async def forward_message_to_user(self, message: Message, conversation: Dict[str, Any]):
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
- supabase_client = get_global_var('supabase_client')
260
-
261
- user_id = conversation['user_id']
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 != 'None' else None
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['id'],
273
- role='assistant',
326
+ session_id=session_info["id"],
327
+ role="assistant",
274
328
  content=message.text,
275
- message_type='text',
276
- metadata={'from_admin': message.from_user.id}
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
- from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
286
-
287
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
288
- [
289
- InlineKeyboardButton(text="📋 История", callback_data=f"admin_history_{user_id}"),
290
- InlineKeyboardButton(text=" Завершить", callback_data=f"admin_end_{user_id}")
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
- 'active_conversations': 0,
337
- 'completed_today': 0,
338
- 'total_admin_messages': 0
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(f"🔍 Ищем активные диалоги админов...")
348
-
406
+ logger.info("🔍 Ищем активные диалоги админов...")
407
+
349
408
  # Получаем все активные диалоги
350
- response = self.supabase.client.table('admin_user_conversations').select(
351
- 'id', 'admin_id', 'user_id', 'started_at', 'auto_end_at'
352
- ).eq('status', 'active').order('started_at', desc=True).execute()
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 = self.supabase.client.table('sales_users').select(
361
- 'first_name', 'last_name', 'username'
362
- ).eq('telegram_id', conv['user_id']).execute()
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(f"Ошибка получения данных пользователя {conv['user_id']}: {e}")
432
+ logger.error(
433
+ f"Ошибка получения данных пользователя {conv['user_id']}: {e}"
434
+ )
367
435
  user_info = {}
368
-
436
+
369
437
  # Получаем информацию об админе
370
438
  try:
371
- admin_response = self.supabase.client.table('sales_admins').select(
372
- 'first_name', 'last_name', 'username'
373
- ).eq('telegram_id', conv['admin_id']).execute()
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(f"Ошибка получения данных админа {conv['admin_id']}: {e}")
448
+ logger.error(
449
+ f"Ошибка получения данных админа {conv['admin_id']}: {e}"
450
+ )
378
451
  admin_info = {}
379
-
380
- conv['user_info'] = user_info
381
- conv['admin_info'] = admin_info
452
+
453
+ conv["user_info"] = user_info
454
+ conv["admin_info"] = admin_info
382
455
  conversations.append(conv)
383
-
384
- logger.info(f"✅ Получено {len(conversations)} активных диалогов с дополнительной информацией")
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('user_info', {})
475
+ user_info = conv.get("user_info", {})
401
476
  user_name = []
402
- if user_info.get('first_name'):
403
- user_name.append(user_info['first_name'])
404
- if user_info.get('last_name'):
405
- user_name.append(user_info['last_name'])
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('username'):
483
+ if user_info.get("username"):
409
484
  user_display += f" (@{user_info['username']})"
410
-
485
+
411
486
  # Информация об админе
412
- admin_info = conv.get('admin_info', {})
487
+ admin_info = conv.get("admin_info", {})
413
488
  admin_name = []
414
- if admin_info.get('first_name'):
415
- admin_name.append(admin_info['first_name'])
416
- if admin_info.get('last_name'):
417
- admin_name.append(admin_info['last_name'])
418
-
419
- admin_display = " ".join(admin_name) if admin_name else f"ID {conv['admin_id']}"
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['started_at']
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('Z'):
428
- start_time = datetime.fromisoformat(started_at_str.replace('Z', '+00:00'))
429
- elif '+' in started_at_str or started_at_str.count(':') >= 3:
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['auto_end_at']
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('Z'):
466
- auto_end = datetime.fromisoformat(auto_end_str.replace('Z', '+00:00'))
467
- elif '+' in auto_end_str or auto_end_str.count(':') >= 3:
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 abs(planned_minutes - expected_timeout) > 2: # допускаем погрешность в 2 минуты
508
- logger.warning(f"⚠️ Диалог {i}: планируемая длительность {planned_minutes} мин не соответствует конфигу {expected_timeout} мин")
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(f"❌ Ошибка расчета времени автозавершения диалога {i}: {e}")
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