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
@@ -2,39 +2,39 @@ import asyncio
2
2
  import json
3
3
  import logging
4
4
  from datetime import datetime
5
+ from pathlib import Path
6
+
5
7
  from aiogram import Router
6
8
  from aiogram.filters import Command
7
- from aiogram.types import (
8
- Message,
9
- InlineKeyboardMarkup,
10
- InlineKeyboardButton,
11
- FSInputFile,
12
- )
9
+ from aiogram.types import (FSInputFile, InlineKeyboardButton,
10
+ InlineKeyboardMarkup, Message)
13
11
  from aiogram.utils.media_group import MediaGroupBuilder
14
12
 
15
- from pathlib import Path
16
- from ..core.decorators import (
17
- execute_scheduled_task_from_event,
18
- execute_global_handler_from_event,
19
- )
13
+ from ..core.decorators import (execute_global_handler_from_event,
14
+ execute_scheduled_task_from_event)
15
+
20
16
 
21
17
  # Функция для получения глобальных переменных
22
18
  def get_global_var(var_name):
23
19
  """Получает глобальную переменную из модуля bot_utils"""
24
20
  import sys
21
+
25
22
  current_module = sys.modules[__name__]
26
23
  return getattr(current_module, var_name, None)
27
24
 
25
+
28
26
  logger = logging.getLogger(__name__)
29
27
 
30
28
 
31
29
  # Создаем роутер для общих команд
32
30
  utils_router = Router()
33
31
 
32
+
34
33
  def setup_utils_handlers(dp):
35
34
  """Настройка обработчиков утилит"""
36
35
  dp.include_router(utils_router)
37
36
 
37
+
38
38
  def parse_ai_response(ai_response: str) -> tuple[str, dict]:
39
39
  """Исправленная функция парсинга JSON из конца ответа ИИ"""
40
40
  try:
@@ -43,52 +43,54 @@ def parse_ai_response(ai_response: str) -> tuple[str, dict]:
43
43
  if last_etap_pos == -1:
44
44
  logger.debug("JSON без ключа 'этап' не найден")
45
45
  return ai_response, {}
46
-
46
+
47
47
  # Ищем открывающую скобку перед "этап"
48
48
  json_start = -1
49
49
  for i in range(last_etap_pos, -1, -1):
50
- if ai_response[i] == '{':
50
+ if ai_response[i] == "{":
51
51
  json_start = i
52
52
  break
53
-
53
+
54
54
  if json_start == -1:
55
55
  logger.debug("Открывающая скобка перед 'этап' не найдена")
56
56
  return ai_response, {}
57
-
57
+
58
58
  # Теперь найдем соответствующую закрывающую скобку
59
59
  brace_count = 0
60
60
  json_end = -1
61
-
61
+
62
62
  for i in range(json_start, len(ai_response)):
63
63
  char = ai_response[i]
64
- if char == '{':
64
+ if char == "{":
65
65
  brace_count += 1
66
- elif char == '}':
66
+ elif char == "}":
67
67
  brace_count -= 1
68
68
  if brace_count == 0:
69
69
  json_end = i
70
70
  break
71
-
71
+
72
72
  if json_end == -1:
73
73
  logger.debug("Соответствующая закрывающая скобка не найдена")
74
74
  return ai_response, {}
75
-
75
+
76
76
  # Извлекаем JSON и текст ответа
77
- json_str = ai_response[json_start:json_end + 1]
77
+ json_str = ai_response[json_start : json_end + 1]
78
78
  response_text = ai_response[:json_start].strip()
79
-
79
+
80
80
  # 🆕 ИСПРАВЛЕНИЕ: Если response_text пустой, используем исходный ответ БЕЗ JSON
81
81
  if not response_text:
82
- logger.debug("Текст ответа пустой после удаления JSON, используем исходный ответ без JSON части")
82
+ logger.debug(
83
+ "Текст ответа пустой после удаления JSON, используем исходный ответ без JSON части"
84
+ )
83
85
  # Берем все кроме JSON части
84
- remaining_text = ai_response[json_end + 1:].strip()
86
+ remaining_text = ai_response[json_end + 1 :].strip()
85
87
  if remaining_text:
86
88
  response_text = remaining_text
87
89
  else:
88
90
  # Если и после JSON ничего нет, значит ответ был только JSON
89
91
  response_text = "Ответ обработан системой."
90
92
  logger.warning("Ответ ИИ содержал только JSON без текста")
91
-
93
+
92
94
  try:
93
95
  metadata = json.loads(json_str)
94
96
  logger.debug(f"JSON успешно распарсен: {metadata}")
@@ -97,66 +99,67 @@ def parse_ai_response(ai_response: str) -> tuple[str, dict]:
97
99
  logger.warning(f"Ошибка парсинга JSON: {e}")
98
100
  logger.debug(f"JSON строка: {json_str}")
99
101
  return parse_ai_response_method2(ai_response)
100
-
102
+
101
103
  except Exception as e:
102
104
  logger.warning(f"Ошибка парсинга JSON от ИИ: {e}")
103
105
  return parse_ai_response_method2(ai_response)
104
106
 
107
+
105
108
  def parse_ai_response_method2(ai_response: str) -> tuple[str, dict]:
106
109
  """Резервный метод парсинга JSON - поиск по строкам (переименован для соответствия тестам)"""
107
110
  try:
108
111
  logger.debug("Используем резервный метод парсинга JSON")
109
-
110
- lines = ai_response.strip().split('\n')
111
-
112
+
113
+ lines = ai_response.strip().split("\n")
114
+
112
115
  # Ищем строку с "этап"
113
116
  etap_line = -1
114
117
  for i, line in enumerate(lines):
115
118
  if '"этап"' in line:
116
119
  etap_line = i
117
120
  break
118
-
121
+
119
122
  if etap_line == -1:
120
123
  return ai_response, {}
121
-
124
+
122
125
  # Ищем начало JSON (строку с { перед этап)
123
126
  json_start_line = -1
124
127
  for i in range(etap_line, -1, -1):
125
- if lines[i].strip().startswith('{'):
128
+ if lines[i].strip().startswith("{"):
126
129
  json_start_line = i
127
130
  break
128
-
131
+
129
132
  if json_start_line == -1:
130
133
  return ai_response, {}
131
-
134
+
132
135
  # Ищем конец JSON (балансируем скобки)
133
136
  brace_count = 0
134
137
  json_end_line = -1
135
-
138
+
136
139
  for i in range(json_start_line, len(lines)):
137
140
  line = lines[i]
138
141
  for char in line:
139
- if char == '{':
142
+ if char == "{":
140
143
  brace_count += 1
141
- elif char == '}':
144
+ elif char == "}":
142
145
  brace_count -= 1
143
146
  if brace_count == 0:
144
147
  json_end_line = i
145
148
  break
146
149
  if json_end_line != -1:
147
150
  break
148
-
151
+
149
152
  if json_end_line == -1:
150
153
  return ai_response, {}
151
-
154
+
152
155
  # Собираем JSON
153
- json_lines = lines[json_start_line:json_end_line + 1]
154
- json_str = '\n'.join(json_lines)
155
-
156
+ json_lines = lines[json_start_line : json_end_line + 1]
157
+ json_str = "\n".join(json_lines)
158
+
156
159
  # Собираем текст ответа
157
160
  response_lines = lines[:json_start_line]
158
- response_text = '\n'.join(response_lines).strip()
159
-
161
+ response_text = "\n".join(response_lines).strip()
162
+
160
163
  try:
161
164
  metadata = json.loads(json_str)
162
165
  logger.debug(f"JSON распарсен резервным методом: {metadata}")
@@ -164,278 +167,372 @@ def parse_ai_response_method2(ai_response: str) -> tuple[str, dict]:
164
167
  except json.JSONDecodeError as e:
165
168
  logger.warning(f"Резервный метод: ошибка JSON: {e}")
166
169
  return ai_response, {}
167
-
170
+
168
171
  except Exception as e:
169
172
  logger.warning(f"Ошибка резервного метода: {e}")
170
173
  return ai_response, {}
171
174
 
175
+
172
176
  async def process_events(session_id: str, events: list, user_id: int) -> bool:
173
177
  """
174
178
  Обрабатывает события из ответа ИИ
175
-
179
+
176
180
  Returns:
177
181
  bool: True если нужно отправить сообщение от ИИ, False если не нужно
178
182
  """
179
-
183
+
180
184
  # Проверяем кастомный процессор
181
- custom_processor = get_global_var('custom_event_processor')
182
-
185
+ custom_processor = get_global_var("custom_event_processor")
186
+
183
187
  if custom_processor:
184
188
  # Используем кастомную функцию обработки событий
185
- logger.info(f"🔄 Используется кастомная обработка событий: {custom_processor.__name__}")
189
+ logger.info(
190
+ f"🔄 Используется кастомная обработка событий: {custom_processor.__name__}"
191
+ )
186
192
  await custom_processor(session_id, events, user_id)
187
193
  return True # По умолчанию отправляем сообщение
188
-
194
+
189
195
  # Стандартная обработка
190
- supabase_client = get_global_var('supabase_client')
191
-
196
+ supabase_client = get_global_var("supabase_client")
197
+
192
198
  # Флаг для отслеживания, нужно ли отправлять сообщение от ИИ
193
199
  should_send_ai_response = True
194
-
200
+
195
201
  for event in events:
196
202
  try:
197
- event_type = event.get('тип', '')
198
- event_info = event.get('инфо', '')
199
-
203
+ event_type = event.get("тип", "")
204
+ event_info = event.get("инфо", "")
205
+
200
206
  if not event_type:
201
207
  logger.warning(f"⚠️ Событие без типа: {event}")
202
208
  continue
203
-
204
- logger.info(f"\n🔔 Обработка события:")
209
+
210
+ logger.info("\n🔔 Обработка события:")
205
211
  logger.info(f" 📝 Тип: {event_type}")
206
212
  logger.info(f" 📄 Данные: {event_info}")
207
-
213
+
208
214
  # Определяем категорию события и сохраняем в БД
209
215
  event_id = None
210
216
  should_notify = False
211
-
217
+
212
218
  try:
213
219
  # Проверяем зарегистрированные обработчики через роутер-менеджер
214
- from ..core.decorators import get_router_manager, _event_handlers, _scheduled_tasks, _global_handlers
215
-
220
+ from ..core.decorators import (_event_handlers,
221
+ _global_handlers,
222
+ _scheduled_tasks,
223
+ get_router_manager)
224
+
216
225
  # Получаем обработчики из роутеров или fallback к старым декораторам
217
226
  router_manager = get_router_manager()
218
227
  if router_manager:
219
228
  event_handlers = router_manager.get_event_handlers()
220
229
  scheduled_tasks = router_manager.get_scheduled_tasks()
221
230
  global_handlers = router_manager.get_global_handlers()
222
- logger.debug(f"🔍 RouterManager найден: {len(event_handlers)} событий, {len(scheduled_tasks)} задач, {len(global_handlers)} глобальных обработчиков")
223
- logger.debug(f"🔍 Доступные scheduled_tasks: {list(scheduled_tasks.keys())}")
231
+ logger.debug(
232
+ f"🔍 RouterManager найден: {len(event_handlers)} событий, {len(scheduled_tasks)} задач, {len(global_handlers)} глобальных обработчиков"
233
+ )
234
+ logger.debug(
235
+ f"🔍 Доступные scheduled_tasks: {list(scheduled_tasks.keys())}"
236
+ )
224
237
  else:
225
238
  event_handlers = _event_handlers
226
239
  scheduled_tasks = _scheduled_tasks
227
240
  global_handlers = _global_handlers
228
- logger.warning("⚠️ RouterManager не найден, используем старые декораторы")
229
- logger.debug(f"🔍 Старые scheduled_tasks: {list(scheduled_tasks.keys())}")
230
-
241
+ logger.warning(
242
+ "⚠️ RouterManager не найден, используем старые декораторы"
243
+ )
244
+ logger.debug(
245
+ f"🔍 Старые scheduled_tasks: {list(scheduled_tasks.keys())}"
246
+ )
247
+
231
248
  # Сначала пробуем как обычное событие
232
249
  if event_type in event_handlers:
233
- from ..core.decorators import execute_event_handler, update_event_result
234
-
250
+ from ..core.decorators import execute_event_handler
251
+
235
252
  event_handler_info = event_handlers.get(event_type, {})
236
- once_only = event_handler_info.get('once_only', True)
237
- send_ai_response_flag = event_handler_info.get('send_ai_response', True)
238
-
239
- logger.info(f" 🔍 Обработчик '{event_type}': once_only={once_only}, send_ai_response={send_ai_response_flag}")
240
-
253
+ once_only = event_handler_info.get("once_only", True)
254
+ send_ai_response_flag = event_handler_info.get(
255
+ "send_ai_response", True
256
+ )
257
+
258
+ logger.info(
259
+ f" 🔍 Обработчик '{event_type}': once_only={once_only}, send_ai_response={send_ai_response_flag}"
260
+ )
261
+
241
262
  # Проверяем флаг send_ai_response ИЗ ДЕКОРАТОРА
242
263
  if not send_ai_response_flag:
243
264
  should_send_ai_response = False
244
- logger.warning(f" 🔇🔇🔇 ОБРАБОТЧИК '{event_type}' ЗАПРЕТИЛ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇")
245
-
265
+ logger.warning(
266
+ f" 🔇🔇🔇 ОБРАБОТЧИК '{event_type}' ЗАПРЕТИЛ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇"
267
+ )
268
+
246
269
  # Если once_only=True - проверяем в БД наличие выполненных событий
247
270
  if once_only:
248
- check_query = supabase_client.client.table('scheduled_events')\
249
- .select('id, status, session_id')\
250
- .eq('event_type', event_type)\
251
- .eq('user_id', user_id)\
252
- .eq('status', 'completed')
253
-
271
+ check_query = (
272
+ supabase_client.client.table("scheduled_events")
273
+ .select("id, status, session_id")
274
+ .eq("event_type", event_type)
275
+ .eq("user_id", user_id)
276
+ .eq("status", "completed")
277
+ )
278
+
254
279
  # НЕ фильтруем по session_id - проверяем ВСЕ выполненные события пользователя
255
280
  # if session_id:
256
281
  # check_query = check_query.eq('session_id', session_id)
257
-
282
+
283
+ # 🆕 Фильтруем по bot_id если указан
284
+ if supabase_client.bot_id:
285
+ check_query = check_query.eq(
286
+ "bot_id", supabase_client.bot_id
287
+ )
288
+
258
289
  existing = check_query.execute()
259
-
260
- logger.info(f" 🔍 Проверка БД: найдено {len(existing.data) if existing.data else 0} выполненных событий '{event_type}' для user_id={user_id}")
261
-
290
+
291
+ logger.info(
292
+ f" 🔍 Проверка БД: найдено {len(existing.data) if existing.data else 0} выполненных событий '{event_type}' для user_id={user_id}"
293
+ )
294
+
262
295
  if existing.data:
263
- logger.info(f" 🔄 Событие '{event_type}' уже выполнялось для пользователя {user_id}, пропускаем (once_only=True)")
296
+ logger.info(
297
+ f" 🔄 Событие '{event_type}' уже выполнялось для пользователя {user_id}, пропускаем (once_only=True)"
298
+ )
264
299
  logger.info(f" 📋 Найденные события: {existing.data}")
265
300
  continue
266
-
301
+
267
302
  # Немедленно выполняем событие
268
- logger.info(f" 🎯 Немедленно выполняем user_event: '{event_type}'")
269
-
303
+ logger.info(
304
+ f" 🎯 Немедленно выполняем user_event: '{event_type}'"
305
+ )
306
+
270
307
  try:
271
308
  # Выполняем событие
272
- result = await execute_event_handler(event_type, user_id, event_info)
273
-
309
+ result = await execute_event_handler(
310
+ event_type, user_id, event_info
311
+ )
312
+
274
313
  # Проверяем наличие поля 'info' для дашборда
275
314
  import json
315
+
276
316
  info_dashboard_json = None
277
- if isinstance(result, dict) and 'info' in result:
278
- info_dashboard_json = json.dumps(result['info'], ensure_ascii=False)
279
- logger.info(f" 📊 Дашборд данные добавлены: {result['info'].get('title', 'N/A')}")
280
-
317
+ if isinstance(result, dict) and "info" in result:
318
+ info_dashboard_json = json.dumps(
319
+ result["info"], ensure_ascii=False
320
+ )
321
+ logger.info(
322
+ f" 📊 Дашборд данные добавлены: {result['info'].get('title', 'N/A')}"
323
+ )
324
+
281
325
  # Сохраняем в БД УЖЕ со статусом completed (избегаем дублирования)
282
326
  event_record = {
283
- 'event_type': event_type,
284
- 'event_category': 'user_event',
285
- 'user_id': user_id,
286
- 'event_data': event_info,
287
- 'scheduled_at': None,
288
- 'status': 'completed', # Сразу completed!
289
- 'session_id': session_id,
290
- 'executed_at': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
291
- 'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None,
292
- 'info_dashboard': info_dashboard_json # Добавится только если есть поле 'info'
327
+ "event_type": event_type,
328
+ "event_category": "user_event",
329
+ "user_id": user_id,
330
+ "event_data": event_info,
331
+ "scheduled_at": None,
332
+ "status": "completed", # Сразу completed!
333
+ "session_id": session_id,
334
+ "executed_at": __import__("datetime")
335
+ .datetime.now(__import__("datetime").timezone.utc)
336
+ .isoformat(),
337
+ "result_data": (
338
+ __import__("json").dumps(result, ensure_ascii=False)
339
+ if result
340
+ else None
341
+ ),
342
+ "info_dashboard": info_dashboard_json, # Добавится только если есть поле 'info'
293
343
  }
294
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
295
- event_id = response.data[0]['id']
296
-
297
- should_notify = event_handler_info.get('notify', False)
298
-
299
- logger.info(f" ✅ Событие {event_id} выполнено и сохранено как completed")
300
-
344
+
345
+ # 🆕 Добавляем bot_id если указан
346
+ if supabase_client.bot_id:
347
+ event_record["bot_id"] = supabase_client.bot_id
348
+
349
+ response = (
350
+ supabase_client.client.table("scheduled_events")
351
+ .insert(event_record)
352
+ .execute()
353
+ )
354
+ event_id = response.data[0]["id"]
355
+
356
+ should_notify = event_handler_info.get("notify", False)
357
+
358
+ logger.info(
359
+ f" ✅ Событие {event_id} выполнено и сохранено как completed"
360
+ )
361
+
301
362
  except Exception as e:
302
363
  logger.error(f" ❌ Ошибка выполнения события: {e}")
303
364
  # Сохраняем ошибку в БД
304
365
  event_record = {
305
- 'event_type': event_type,
306
- 'event_category': 'user_event',
307
- 'user_id': user_id,
308
- 'event_data': event_info,
309
- 'scheduled_at': None,
310
- 'status': 'failed',
311
- 'session_id': session_id,
312
- 'last_error': str(e)
366
+ "event_type": event_type,
367
+ "event_category": "user_event",
368
+ "user_id": user_id,
369
+ "event_data": event_info,
370
+ "scheduled_at": None,
371
+ "status": "failed",
372
+ "session_id": session_id,
373
+ "last_error": str(e),
313
374
  }
314
- supabase_client.client.table('scheduled_events').insert(event_record).execute()
375
+
376
+ # 🆕 Добавляем bot_id если указан
377
+ if supabase_client.bot_id:
378
+ event_record["bot_id"] = supabase_client.bot_id
379
+
380
+ supabase_client.client.table("scheduled_events").insert(
381
+ event_record
382
+ ).execute()
315
383
  raise
316
-
384
+
317
385
  # Если не user_event, пробуем как запланированную задачу
318
386
  elif event_type in scheduled_tasks:
319
387
  try:
320
388
  # Достаем метаданные задачи
321
389
  task_info = scheduled_tasks.get(event_type, {})
322
- send_ai_response_flag = task_info.get('send_ai_response', True)
323
-
324
- logger.info(f" ⏰ Планируем scheduled_task: '{event_type}', send_ai_response={send_ai_response_flag}")
325
-
390
+ send_ai_response_flag = task_info.get("send_ai_response", True)
391
+
392
+ logger.info(
393
+ f" ⏰ Планируем scheduled_task: '{event_type}', send_ai_response={send_ai_response_flag}"
394
+ )
395
+
326
396
  # Проверяем флаг send_ai_response ИЗ ДЕКОРАТОРА
327
397
  if not send_ai_response_flag:
328
398
  should_send_ai_response = False
329
- logger.warning(f" 🔇🔇🔇 ЗАДАЧА '{event_type}' ЗАПРЕТИЛА ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇")
330
-
399
+ logger.warning(
400
+ f" 🔇🔇🔇 ЗАДАЧА '{event_type}' ЗАПРЕТИЛА ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇"
401
+ )
402
+
331
403
  # Используем новую логику - время берется из декоратора
332
- result = await execute_scheduled_task_from_event(user_id, event_type, event_info, session_id)
333
- event_id = result.get('event_id', 'unknown')
334
- should_notify = result.get('notify', False)
404
+ result = await execute_scheduled_task_from_event(
405
+ user_id, event_type, event_info, session_id
406
+ )
407
+ event_id = result.get("event_id", "unknown")
408
+ should_notify = result.get("notify", False)
335
409
  logger.info(f" 💾 Задача запланирована: {event_id}")
336
-
410
+
337
411
  except Exception as e:
338
412
  if "once_only=True" in str(e):
339
- logger.info(f" 🔄 Задача '{event_type}' уже запланирована, пропускаем")
413
+ logger.info(
414
+ f" 🔄 Задача '{event_type}' уже запланирована, пропускаем"
415
+ )
340
416
  continue
341
417
  else:
342
- logger.error(f" ❌ Ошибка планирования scheduled_task '{event_type}': {e}")
418
+ logger.error(
419
+ f" ❌ Ошибка планирования scheduled_task '{event_type}': {e}"
420
+ )
343
421
  continue
344
-
422
+
345
423
  # Если не scheduled_task, пробуем как глобальный обработчик
346
424
  elif event_type in global_handlers:
347
425
  try:
348
426
  # Используем новую логику - время берется из декоратора
349
- logger.info(f" 🌍 Планируем global_handler: '{event_type}' с данными: '{event_info}'")
350
- result = await execute_global_handler_from_event(event_type, event_info)
351
- event_id = result.get('event_id', 'unknown')
352
- should_notify = result.get('notify', False)
353
- logger.info(f" 💾 Глобальное событие запланировано: {event_id}")
354
-
427
+ logger.info(
428
+ f" 🌍 Планируем global_handler: '{event_type}' с данными: '{event_info}'"
429
+ )
430
+ result = await execute_global_handler_from_event(
431
+ event_type, event_info
432
+ )
433
+ event_id = result.get("event_id", "unknown")
434
+ should_notify = result.get("notify", False)
435
+ logger.info(
436
+ f" 💾 Глобальное событие запланировано: {event_id}"
437
+ )
438
+
355
439
  except Exception as e:
356
440
  if "once_only=True" in str(e):
357
- logger.info(f" 🔄 Глобальное событие '{event_type}' уже запланировано, пропускаем")
441
+ logger.info(
442
+ f" 🔄 Глобальное событие '{event_type}' уже запланировано, пропускаем"
443
+ )
358
444
  continue
359
445
  else:
360
- logger.error(f" ❌ Ошибка планирования global_handler '{event_type}': {e}")
446
+ logger.error(
447
+ f" ❌ Ошибка планирования global_handler '{event_type}': {e}"
448
+ )
361
449
  continue
362
-
450
+
363
451
  else:
364
- logger.warning(f" ⚠️ Обработчик '{event_type}' не найден среди зарегистрированных")
365
- logger.debug(f" 🔍 Доступные обработчики:")
366
- logger.debug(f" - event_handlers: {list(event_handlers.keys())}")
367
- logger.debug(f" - scheduled_tasks: {list(scheduled_tasks.keys())}")
368
- logger.debug(f" - global_handlers: {list(global_handlers.keys())}")
369
-
452
+ logger.warning(
453
+ f" ⚠️ Обработчик '{event_type}' не найден среди зарегистрированных"
454
+ )
455
+ logger.debug(" 🔍 Доступные обработчики:")
456
+ logger.debug(
457
+ f" - event_handlers: {list(event_handlers.keys())}"
458
+ )
459
+ logger.debug(
460
+ f" - scheduled_tasks: {list(scheduled_tasks.keys())}"
461
+ )
462
+ logger.debug(
463
+ f" - global_handlers: {list(global_handlers.keys())}"
464
+ )
465
+
370
466
  except ValueError as e:
371
467
  logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
372
468
  except Exception as e:
373
469
  logger.error(f" ❌ Ошибка в обработчике/задаче: {e}")
374
470
  logger.exception(" Стек ошибки:")
375
-
471
+
376
472
  # Уведомляем админов только если result.notify = True
377
473
  if should_notify:
378
474
  await notify_admins_about_event(user_id, event)
379
- logger.info(f" ✅ Админы уведомлены")
475
+ logger.info(" ✅ Админы уведомлены")
380
476
  else:
381
477
  logger.info(f" 🔕 Уведомления админам отключены для '{event_type}'")
382
-
478
+
383
479
  except Exception as e:
384
480
  logger.error(f"❌ Ошибка обработки события {event}: {e}")
385
481
  logger.exception("Стек ошибки:")
386
-
482
+
387
483
  # Возвращаем флаг, нужно ли отправлять сообщение от ИИ
388
- logger.warning(f"🔊🔊🔊 ИТОГОВЫЙ ФЛАГ send_ai_response: {should_send_ai_response} 🔊🔊🔊")
484
+ logger.warning(
485
+ f"🔊🔊🔊 ИТОГОВЫЙ ФЛАГ send_ai_response: {should_send_ai_response} 🔊🔊🔊"
486
+ )
389
487
  return should_send_ai_response
390
488
 
489
+
391
490
  async def notify_admins_about_event(user_id: int, event: dict):
392
491
  """Отправляем уведомление админам о событии с явным указанием ID пользователя"""
393
- supabase_client = get_global_var('supabase_client')
394
- admin_manager = get_global_var('admin_manager')
395
- bot = get_global_var('bot')
396
-
397
- event_type = event.get('тип', '')
398
- event_info = event.get('инфо', '')
399
-
492
+ supabase_client = get_global_var("supabase_client")
493
+ admin_manager = get_global_var("admin_manager")
494
+ bot = get_global_var("bot")
495
+
496
+ event_type = event.get("тип", "")
497
+ event_info = event.get("инфо", "")
498
+
400
499
  if not event_type:
401
500
  return
402
-
501
+
403
502
  # Получаем информацию о пользователе для username
404
503
  try:
405
- user_response = supabase_client.client.table('sales_users').select(
406
- 'first_name', 'last_name', 'username'
407
- ).eq('telegram_id', user_id).execute()
408
-
504
+ user_response = (
505
+ supabase_client.client.table("sales_users")
506
+ .select("first_name", "last_name", "username")
507
+ .eq("telegram_id", user_id)
508
+ .execute()
509
+ )
510
+
409
511
  user_info = user_response.data[0] if user_response.data else {}
410
-
512
+
411
513
  # Формируем имя пользователя (без ID)
412
514
  name_parts = []
413
- if user_info.get('first_name'):
414
- name_parts.append(user_info['first_name'])
415
- if user_info.get('last_name'):
416
- name_parts.append(user_info['last_name'])
417
-
515
+ if user_info.get("first_name"):
516
+ name_parts.append(user_info["first_name"])
517
+ if user_info.get("last_name"):
518
+ name_parts.append(user_info["last_name"])
519
+
418
520
  user_name = " ".join(name_parts) if name_parts else "Без имени"
419
-
521
+
420
522
  # Формируем отображение пользователя с ОБЯЗАТЕЛЬНЫМ ID
421
- if user_info.get('username'):
523
+ if user_info.get("username"):
422
524
  user_display = f"{user_name} (@{user_info['username']})"
423
525
  else:
424
526
  user_display = user_name
425
-
527
+
426
528
  except Exception as e:
427
529
  logger.error(f"Ошибка получения информации о пользователе {user_id}: {e}")
428
530
  user_display = "Пользователь"
429
-
430
- emoji_map = {
431
- 'телефон': '📱',
432
- 'консультация': '💬',
433
- 'покупка': '💰',
434
- 'отказ': '❌'
435
- }
436
-
437
- emoji = emoji_map.get(event_type, '🔔')
438
-
531
+
532
+ emoji_map = {"телефон": "📱", "консультация": "💬", "покупка": "💰", "отказ": "❌"}
533
+
534
+ emoji = emoji_map.get(event_type, "🔔")
535
+
439
536
  # 🆕 ИСПРАВЛЕНИЕ: ID всегда отображается отдельной строкой для удобства копирования
440
537
  notification = f"""
441
538
  {emoji} {event_type.upper()}!
@@ -444,116 +541,140 @@ async def notify_admins_about_event(user_id: int, event: dict):
444
541
  📝 {event_info}
445
542
  🕐 {datetime.now().strftime('%H:%M')}
446
543
  """
447
-
544
+
448
545
  # Создаем клавиатуру с кнопками
449
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
450
- [
451
- InlineKeyboardButton(text="💬 Чат", callback_data=f"admin_chat_{user_id}"),
452
- InlineKeyboardButton(text="📋 История", callback_data=f"admin_history_{user_id}")
546
+ keyboard = InlineKeyboardMarkup(
547
+ inline_keyboard=[
548
+ [
549
+ InlineKeyboardButton(
550
+ text="💬 Чат", callback_data=f"admin_chat_{user_id}"
551
+ ),
552
+ InlineKeyboardButton(
553
+ text="📋 История", callback_data=f"admin_history_{user_id}"
554
+ ),
555
+ ]
453
556
  ]
454
- ])
455
-
557
+ )
558
+
456
559
  try:
457
560
  # Отправляем всем активным админам
458
561
  active_admins = await admin_manager.get_active_admins()
459
562
  for admin_id in active_admins:
460
563
  try:
461
- await bot.send_message(admin_id, notification.strip(), reply_markup=keyboard)
564
+ await bot.send_message(
565
+ admin_id, notification.strip(), reply_markup=keyboard
566
+ )
462
567
  except Exception as e:
463
568
  logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
464
-
569
+
465
570
  except Exception as e:
466
571
  logger.error(f"Ошибка отправки уведомления админам: {e}")
467
-
468
- async def send_message(message: Message, text: str, files_list: list = [], directories_list: list = [], **kwargs):
572
+
573
+
574
+ async def send_message(
575
+ message: Message,
576
+ text: str,
577
+ files_list: list = [],
578
+ directories_list: list = [],
579
+ **kwargs,
580
+ ):
469
581
  """Вспомогательная функция для отправки сообщений с настройкой parse_mode"""
470
- config = get_global_var('config')
471
-
472
- logger.info(f"📤 send_message вызвана:")
582
+ config = get_global_var("config")
583
+
584
+ logger.info("📤 send_message вызвана:")
473
585
  logger.info(f" 👤 Пользователь: {message.from_user.id}")
474
586
  logger.info(f" 📝 Длина текста: {len(text)} символов")
475
587
  logger.info(f" 🐛 Debug режим: {config.DEBUG_MODE}")
476
-
588
+
477
589
  try:
478
- parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != 'None' else None
590
+ parse_mode = (
591
+ config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != "None" else None
592
+ )
479
593
  logger.info(f" 🔧 Parse mode: {parse_mode}")
480
-
594
+
481
595
  # Получаем user_id и импортируем supabase_client
482
596
  user_id = message.from_user.id
483
- supabase_client = get_global_var('supabase_client')
484
-
597
+ supabase_client = get_global_var("supabase_client")
598
+
485
599
  # Текст уже готов, используем как есть
486
600
  final_text = text
487
-
601
+
488
602
  # Работаем с переданными файлами и каталогами
489
603
  logger.info(f" 📦 Передано файлов: {files_list}")
490
604
  logger.info(f" 📂 Передано каталогов: {directories_list}")
491
-
605
+
492
606
  # Получаем список уже отправленных файлов и каталогов
493
607
  sent_files = await supabase_client.get_sent_files(user_id)
494
608
  sent_directories = await supabase_client.get_sent_directories(user_id)
495
-
609
+
496
610
  logger.info(f" 📋 Уже отправлено файлов: {sent_files}")
497
611
  logger.info(f" 📋 Уже отправлено каталогов: {sent_directories}")
498
-
612
+
499
613
  # Фильтруем файлы и каталоги, которые уже отправлялись
500
614
  actual_files_list = [f for f in files_list if f not in sent_files]
501
- actual_directories_list = [d for d in directories_list if str(d) not in sent_directories]
502
-
615
+ actual_directories_list = [
616
+ d for d in directories_list if str(d) not in sent_directories
617
+ ]
618
+
503
619
  logger.info(f" 🆕 После фильтрации файлов: {actual_files_list}")
504
620
  logger.info(f" 🆕 После фильтрации каталогов: {actual_directories_list}")
505
-
506
-
621
+
507
622
  # Проверяем, что есть что отправлять
508
623
  if not final_text or not final_text.strip():
509
- logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
624
+ logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
510
625
  logger.error(f" Исходный text: '{text[:200]}...'")
511
626
  final_text = "Ошибка формирования ответа. Попробуйте еще раз."
512
-
627
+
513
628
  logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
514
629
  logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
515
630
  logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
516
-
631
+
517
632
  # Проверяем наличие файлов для отправки
518
633
  if actual_files_list or actual_directories_list:
519
634
  # Функция определения типа медиа по расширению
520
635
  def get_media_type(file_path: str) -> str:
521
636
  ext = Path(file_path).suffix.lower()
522
- if ext in {'.jpg', '.jpeg', '.png'}:
523
- return 'photo'
524
- elif ext in {'.mp4', '.mov'}:
525
- return 'video'
637
+ if ext in {".jpg", ".jpeg", ".png"}:
638
+ return "photo"
639
+ elif ext in {".mp4", ".mov"}:
640
+ return "video"
526
641
  else:
527
- return 'document'
528
-
642
+ return "document"
643
+
529
644
  # Создаем списки для разных типов файлов
530
645
  video_files = [] # для видео
531
646
  photo_files = [] # для фото
532
647
  document_files = [] # для документов
533
-
648
+
534
649
  # Функция обработки файла
535
650
  def process_file(file_path: Path, source: str = ""):
536
651
  if file_path.is_file():
537
652
  media_type = get_media_type(str(file_path))
538
- if media_type == 'video':
653
+ if media_type == "video":
539
654
  video_files.append(file_path)
540
- logger.info(f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}")
541
- elif media_type == 'photo':
655
+ logger.info(
656
+ f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}"
657
+ )
658
+ elif media_type == "photo":
542
659
  photo_files.append(file_path)
543
- logger.info(f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}")
660
+ logger.info(
661
+ f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}"
662
+ )
544
663
  else:
545
664
  document_files.append(file_path)
546
- logger.info(f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}")
665
+ logger.info(
666
+ f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}"
667
+ )
547
668
  else:
548
669
  logger.warning(f" ⚠️ Файл не найден: {file_path}")
549
-
670
+
550
671
  # Обрабатываем прямые файлы
551
672
  for file_name in actual_files_list:
552
673
  try:
553
674
  process_file(Path(f"files/{file_name}"))
554
675
  except Exception as e:
555
676
  logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
556
-
677
+
557
678
  # Обрабатываем файлы из каталогов
558
679
  for dir_name in actual_directories_list:
559
680
  dir_name = Path(dir_name)
@@ -563,85 +684,101 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
563
684
  try:
564
685
  process_file(file_path, dir_name)
565
686
  except Exception as e:
566
- logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
687
+ logger.error(
688
+ f" ❌ Ошибка обработки файла {file_path}: {e}"
689
+ )
567
690
  else:
568
691
  logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
569
692
  except Exception as e:
570
693
  logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
571
-
694
+
572
695
  # Списки для отслеживания реально отправленных файлов
573
696
  sent_files_to_save = []
574
697
  sent_dirs_to_save = []
575
-
698
+
576
699
  # 1. Отправляем видео (если есть)
577
700
  if video_files:
578
701
  video_group = MediaGroupBuilder()
579
702
  for file_path in video_files:
580
703
  video_group.add_video(media=FSInputFile(str(file_path)))
581
-
704
+
582
705
  videos = video_group.build()
583
706
  if videos:
584
707
  await message.answer_media_group(media=videos)
585
708
  logger.info(f" ✅ Отправлено {len(videos)} видео")
586
-
709
+
587
710
  # 2. Отправляем фото (если есть)
588
711
  if photo_files:
589
712
  photo_group = MediaGroupBuilder()
590
713
  for file_path in photo_files:
591
714
  photo_group.add_photo(media=FSInputFile(str(file_path)))
592
-
715
+
593
716
  photos = photo_group.build()
594
717
  if photos:
595
718
  await message.answer_media_group(media=photos)
596
719
  logger.info(f" ✅ Отправлено {len(photos)} фото")
597
-
720
+
598
721
  # 3. Отправляем текст
599
722
  result = await message.answer(final_text, parse_mode=parse_mode)
600
- logger.info(f" ✅ Отправлен текст сообщения")
601
-
723
+ logger.info(" ✅ Отправлен текст сообщения")
724
+
602
725
  # 4. Отправляем документы (если есть)
603
726
  if document_files:
604
727
  doc_group = MediaGroupBuilder()
605
728
  for file_path in document_files:
606
729
  doc_group.add_document(media=FSInputFile(str(file_path)))
607
-
730
+
608
731
  docs = doc_group.build()
609
732
  if docs:
610
733
  await message.answer_media_group(media=docs)
611
734
  logger.info(f" ✅ Отправлено {len(docs)} документов")
612
-
735
+
613
736
  # 5. Собираем список реально отправленных файлов и каталогов
614
737
  # Если были отправлены файлы из actual_files_list - сохраняем их
615
738
  if video_files or photo_files or document_files:
616
739
  # Сохраняем прямые файлы из actual_files_list (если отправлены)
617
740
  sent_files_to_save.extend(actual_files_list)
618
- logger.info(f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}")
741
+ logger.info(
742
+ f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}"
743
+ )
619
744
  # Сохраняем каталоги из actual_directories_list (если отправлены файлы из них)
620
745
  sent_dirs_to_save.extend([str(d) for d in actual_directories_list])
621
- logger.info(f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}")
622
-
746
+ logger.info(
747
+ f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}"
748
+ )
749
+
623
750
  # 6. Обновляем информацию в БД
624
751
  if sent_files_to_save or sent_dirs_to_save:
625
752
  try:
626
753
  if sent_files_to_save:
627
754
  logger.info(f" 💾 Сохраняем файлы в БД: {sent_files_to_save}")
628
- await supabase_client.add_sent_files(user_id, sent_files_to_save)
755
+ await supabase_client.add_sent_files(
756
+ user_id, sent_files_to_save
757
+ )
629
758
  if sent_dirs_to_save:
630
- logger.info(f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}")
631
- await supabase_client.add_sent_directories(user_id, sent_dirs_to_save)
632
- logger.info(f" ✅ Обновлена информация о отправленных файлах в БД")
759
+ logger.info(
760
+ f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}"
761
+ )
762
+ await supabase_client.add_sent_directories(
763
+ user_id, sent_dirs_to_save
764
+ )
765
+ logger.info(
766
+ " ✅ Обновлена информация о отправленных файлах в БД"
767
+ )
633
768
  except Exception as e:
634
- logger.error(f" ❌ Ошибка обновления информации о файлах в БД: {e}")
769
+ logger.error(
770
+ f" ❌ Ошибка обновления информации о файлах в БД: {e}"
771
+ )
635
772
  else:
636
- logger.info(f" ℹ️ Нет новых файлов для сохранения в БД")
637
-
773
+ logger.info(" ℹ️ Нет новых файлов для сохранения в БД")
774
+
638
775
  return result
639
776
  else:
640
777
  # Если нет файлов, отправляем просто текст
641
- logger.warning(" ⚠️ Нет файлов для отправки, отправляем как текст")
642
- result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
643
- return result
644
-
778
+ logger.warning(" ⚠️ Нет файлов для отправки, отправляем как текст")
779
+ result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
780
+ return result
781
+
645
782
  except Exception as e:
646
783
  # Проверяем, является ли ошибка блокировкой бота
647
784
  if "Forbidden: bot was blocked by the user" in str(e):
@@ -650,32 +787,37 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
650
787
  elif "TelegramForbiddenError" in str(type(e).__name__):
651
788
  logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
652
789
  return None
653
-
790
+
654
791
  logger.error(f"❌ ОШИБКА в send_message: {e}")
655
792
  logger.exception("Полный стек ошибки send_message:")
656
-
793
+
657
794
  # Пытаемся отправить простое сообщение без форматирования
658
795
  try:
659
796
  fallback_text = "Произошла ошибка при отправке ответа. Попробуйте еще раз."
660
797
  result = await message.answer(fallback_text)
661
- logger.info(f"✅ Запасное сообщение отправлено")
798
+ logger.info("✅ Запасное сообщение отправлено")
662
799
  return result
663
800
  except Exception as e2:
664
801
  # Проверяем и здесь блокировку бота
665
802
  if "Forbidden: bot was blocked by the user" in str(e2):
666
- logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
803
+ logger.warning(
804
+ f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
805
+ )
667
806
  return None
668
807
  elif "TelegramForbiddenError" in str(type(e2).__name__):
669
- logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
808
+ logger.warning(
809
+ f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
810
+ )
670
811
  return None
671
-
812
+
672
813
  logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
673
814
  raise
674
-
815
+
816
+
675
817
  async def cleanup_expired_conversations():
676
818
  """Периодическая очистка просроченных диалогов"""
677
- conversation_manager = get_global_var('conversation_manager')
678
-
819
+ conversation_manager = get_global_var("conversation_manager")
820
+
679
821
  while True:
680
822
  try:
681
823
  await asyncio.sleep(300) # каждые 5 минут
@@ -683,18 +825,20 @@ async def cleanup_expired_conversations():
683
825
  except Exception as e:
684
826
  logger.error(f"Ошибка очистки просроченных диалогов: {e}")
685
827
 
828
+
686
829
  # 🆕 Вспомогательные функции для приветственного файла
687
830
 
831
+
688
832
  async def get_welcome_file_path() -> str | None:
689
833
  """Возвращает путь к PDF файлу из папки WELCOME_FILE_DIR из конфига.
690
834
 
691
835
  Источник настроек: configs/<bot_id>/.env (переменная WELCOME_FILE_DIR)
692
836
  Рабочая директория уже установлена запускалкой на configs/<bot_id>.
693
-
837
+
694
838
  Returns:
695
839
  str | None: Путь к PDF файлу или None, если файл не найден
696
840
  """
697
- config = get_global_var('config')
841
+ config = get_global_var("config")
698
842
  try:
699
843
  folder_value = config.WELCOME_FILE_DIR
700
844
  if not folder_value:
@@ -702,28 +846,31 @@ async def get_welcome_file_path() -> str | None:
702
846
 
703
847
  folder = Path(folder_value)
704
848
  if not folder.exists():
705
- logger.info(f"Директория приветственных файлов не существует: {folder_value}")
849
+ logger.info(
850
+ f"Директория приветственных файлов не существует: {folder_value}"
851
+ )
706
852
  return None
707
-
853
+
708
854
  if not folder.is_dir():
709
855
  logger.info(f"Путь не является директорией: {folder_value}")
710
856
  return None
711
857
 
712
858
  # Ищем первый PDF файл в директории
713
859
  for path in folder.iterdir():
714
- if path.is_file() and path.suffix.lower() == '.pdf':
860
+ if path.is_file() and path.suffix.lower() == ".pdf":
715
861
  return str(path)
716
-
862
+
717
863
  logger.info(f"PDF файл не найден в директории: {folder_value}")
718
864
  return None
719
-
865
+
720
866
  except Exception as e:
721
867
  logger.error(f"Ошибка при поиске приветственного файла: {e}")
722
868
  return None
723
869
 
870
+
724
871
  async def get_welcome_msg_path() -> str | None:
725
872
  """Возвращает путь к файлу welcome_file_msg.txt из той же директории, где находится PDF файл.
726
-
873
+
727
874
  Returns:
728
875
  str | None: Путь к файлу с подписью или None, если файл не найден
729
876
  """
@@ -731,28 +878,29 @@ async def get_welcome_msg_path() -> str | None:
731
878
  pdf_path = await get_welcome_file_path()
732
879
  if not pdf_path:
733
880
  return None
734
-
735
- msg_path = str(Path(pdf_path).parent / 'welcome_file_msg.txt')
881
+
882
+ msg_path = str(Path(pdf_path).parent / "welcome_file_msg.txt")
736
883
  if not Path(msg_path).is_file():
737
884
  logger.info(f"Файл подписи не найден: {msg_path}")
738
885
  return None
739
-
886
+
740
887
  return msg_path
741
-
888
+
742
889
  except Exception as e:
743
890
  logger.error(f"Ошибка при поиске файла подписи: {e}")
744
891
  return None
745
892
 
893
+
746
894
  async def send_welcome_file(message: Message) -> str:
747
895
  """
748
896
  Отправляет приветственный файл с подписью из файла welcome_file_msg.txt.
749
897
  Если файл подписи не найден, используется пустая подпись.
750
-
898
+
751
899
  Returns:
752
900
  str: текст подписи
753
901
  """
754
902
  try:
755
- config = get_global_var('config')
903
+ config = get_global_var("config")
756
904
 
757
905
  file_path = await get_welcome_file_path()
758
906
  if not file_path:
@@ -763,7 +911,7 @@ async def send_welcome_file(message: Message) -> str:
763
911
  msg_path = await get_welcome_msg_path()
764
912
  if msg_path:
765
913
  try:
766
- with open(msg_path, 'r', encoding='utf-8') as f:
914
+ with open(msg_path, "r", encoding="utf-8") as f:
767
915
  caption = f.read().strip()
768
916
  logger.info(f"Подпись загружена из файла: {msg_path}")
769
917
  except Exception as e:
@@ -771,23 +919,27 @@ async def send_welcome_file(message: Message) -> str:
771
919
 
772
920
  parse_mode = config.MESSAGE_PARSE_MODE
773
921
  document = FSInputFile(file_path)
774
-
775
- await message.answer_document(document=document, caption=caption, parse_mode=parse_mode)
776
-
922
+
923
+ await message.answer_document(
924
+ document=document, caption=caption, parse_mode=parse_mode
925
+ )
926
+
777
927
  logger.info(f"Приветственный файл отправлен: {file_path}")
778
928
  return caption
779
929
  except Exception as e:
780
930
  logger.error(f"Ошибка при отправке приветственного файла: {e}")
781
931
  return ""
782
932
 
933
+
783
934
  # Общие команды
784
935
 
936
+
785
937
  @utils_router.message(Command("help"))
786
938
  async def help_handler(message: Message):
787
939
  """Справка"""
788
- admin_manager = get_global_var('admin_manager')
789
- prompt_loader = get_global_var('prompt_loader')
790
-
940
+ admin_manager = get_global_var("admin_manager")
941
+ prompt_loader = get_global_var("prompt_loader")
942
+
791
943
  try:
792
944
  # Разная справка для админов и пользователей
793
945
  if admin_manager.is_admin(message.from_user.id):
@@ -808,37 +960,41 @@ async def help_handler(message: Message):
808
960
  • Ваши сообщения отправляются пользователю как от бота
809
961
  • Диалоги автоматически завершаются через 30 минут
810
962
  """
811
- await message.answer(help_text, parse_mode='Markdown')
963
+ await message.answer(help_text, parse_mode="Markdown")
812
964
  return
813
-
965
+
814
966
  # Обычная справка для пользователей
815
967
  help_text = await prompt_loader.load_help_message()
816
968
  await send_message(message, help_text)
817
-
969
+
818
970
  except Exception as e:
819
971
  logger.error(f"Ошибка загрузки справки: {e}")
820
972
  # Fallback справка
821
- await send_message(message, "🤖 Ваш помощник готов к работе! Напишите /start для начала диалога.")
973
+ await send_message(
974
+ message,
975
+ "🤖 Ваш помощник готов к работе! Напишите /start для начала диалога.",
976
+ )
977
+
822
978
 
823
979
  @utils_router.message(Command("status"))
824
980
  async def status_handler(message: Message):
825
981
  """Проверка статуса системы"""
826
- openai_client = get_global_var('openai_client')
827
- prompt_loader = get_global_var('prompt_loader')
828
- admin_manager = get_global_var('admin_manager')
829
- config = get_global_var('config')
830
-
982
+ openai_client = get_global_var("openai_client")
983
+ prompt_loader = get_global_var("prompt_loader")
984
+ admin_manager = get_global_var("admin_manager")
985
+ config = get_global_var("config")
986
+
831
987
  try:
832
988
  # Проверяем OpenAI
833
989
  openai_status = await openai_client.check_api_health()
834
-
990
+
835
991
  # Проверяем промпты
836
992
  prompts_status = await prompt_loader.validate_prompts()
837
-
993
+
838
994
  # Статистика для админов
839
995
  if admin_manager.is_admin(message.from_user.id):
840
996
  admin_stats = admin_manager.get_stats()
841
-
997
+
842
998
  status_message = f"""
843
999
  🔧 **Статус системы:**
844
1000
 
@@ -861,62 +1017,62 @@ OpenAI API: {'✅' if openai_status else '❌'}
861
1017
 
862
1018
  Все системы работают нормально!
863
1019
  """
864
-
1020
+
865
1021
  await send_message(message, status_message)
866
-
1022
+
867
1023
  except Exception as e:
868
1024
  logger.error(f"Ошибка проверки статуса: {e}")
869
1025
  await send_message(message, "❌ Ошибка при проверке статуса системы")
870
-
871
-
1026
+
1027
+
872
1028
  def parse_utm_from_start_param(start_param: str) -> dict:
873
1029
  """Парсит UTM-метки и сегмент из start параметра в формате source-vk_campaign-summer2025_seg-premium
874
-
1030
+
875
1031
  Args:
876
1032
  start_param: строка вида 'source-vk_campaign-summer2025_seg-premium' или полная ссылка
877
-
1033
+
878
1034
  Returns:
879
1035
  dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'premium'}
880
-
1036
+
881
1037
  Examples:
882
1038
  >>> parse_utm_from_start_param('source-vk_campaign-summer2025_seg-premium')
883
1039
  {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'premium'}
884
-
1040
+
885
1041
  >>> parse_utm_from_start_param('https://t.me/bot?start=source-vk_campaign-summer2025_seg-vip')
886
1042
  {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'vip'}
887
1043
  """
888
1044
  import re
889
1045
  from urllib.parse import unquote
890
-
1046
+
891
1047
  utm_data = {}
892
-
1048
+
893
1049
  try:
894
1050
  # Если это полная ссылка, извлекаем start параметр
895
- if 't.me/' in start_param or 'https://' in start_param:
896
- match = re.search(r'[?&]start=([^&]+)', start_param)
1051
+ if "t.me/" in start_param or "https://" in start_param:
1052
+ match = re.search(r"[?&]start=([^&]+)", start_param)
897
1053
  if match:
898
1054
  start_param = unquote(match.group(1))
899
1055
  else:
900
1056
  return {}
901
-
1057
+
902
1058
  # Парсим новый формат: source-vk_campaign-summer2025_seg-premium
903
1059
  # Поддерживает как комбинированные параметры, так и одиночные (например, только seg-prem)
904
- if '-' in start_param:
1060
+ if "-" in start_param:
905
1061
  # Разделяем по _ (если есть несколько параметров) или используем весь параметр
906
- parts = start_param.split('_') if '_' in start_param else [start_param]
907
-
1062
+ parts = start_param.split("_") if "_" in start_param else [start_param]
1063
+
908
1064
  for part in parts:
909
- if '-' in part:
910
- key, value = part.split('-', 1)
1065
+ if "-" in part:
1066
+ key, value = part.split("-", 1)
911
1067
  # Преобразуем source/medium/campaign/content/term в utm_*
912
- if key in ['source', 'medium', 'campaign', 'content', 'term']:
913
- key = 'utm_' + key
1068
+ if key in ["source", "medium", "campaign", "content", "term"]:
1069
+ key = "utm_" + key
914
1070
  utm_data[key] = value
915
1071
  # Обрабатываем seg как segment
916
- elif key == 'seg':
917
- utm_data['segment'] = value
918
-
1072
+ elif key == "seg":
1073
+ utm_data["segment"] = value
1074
+
919
1075
  except Exception as e:
920
1076
  print(f"Ошибка парсинга UTM параметров: {e}")
921
-
922
- return utm_data
1077
+
1078
+ return utm_data