smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.9__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 +474 -332
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1200 -755
  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 +676 -472
  28. smart_bot_factory/integrations/openai_client.py +218 -168
  29. smart_bot_factory/integrations/supabase_client.py +948 -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.7.dist-info → smart_bot_factory-0.3.9.dist-info}/METADATA +3 -1
  41. smart_bot_factory-0.3.9.dist-info/RECORD +59 -0
  42. smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.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,292 +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
+
258
283
  # 🆕 Фильтруем по bot_id если указан
259
284
  if supabase_client.bot_id:
260
- check_query = check_query.eq('bot_id', supabase_client.bot_id)
261
-
285
+ check_query = check_query.eq(
286
+ "bot_id", supabase_client.bot_id
287
+ )
288
+
262
289
  existing = check_query.execute()
263
-
264
- logger.info(f" 🔍 Проверка БД: найдено {len(existing.data) if existing.data else 0} выполненных событий '{event_type}' для user_id={user_id}")
265
-
290
+
291
+ logger.info(
292
+ f" 🔍 Проверка БД: найдено {len(existing.data) if existing.data else 0} выполненных событий '{event_type}' для user_id={user_id}"
293
+ )
294
+
266
295
  if existing.data:
267
- logger.info(f" 🔄 Событие '{event_type}' уже выполнялось для пользователя {user_id}, пропускаем (once_only=True)")
296
+ logger.info(
297
+ f" 🔄 Событие '{event_type}' уже выполнялось для пользователя {user_id}, пропускаем (once_only=True)"
298
+ )
268
299
  logger.info(f" 📋 Найденные события: {existing.data}")
269
300
  continue
270
-
301
+
271
302
  # Немедленно выполняем событие
272
- logger.info(f" 🎯 Немедленно выполняем user_event: '{event_type}'")
273
-
303
+ logger.info(
304
+ f" 🎯 Немедленно выполняем user_event: '{event_type}'"
305
+ )
306
+
274
307
  try:
275
308
  # Выполняем событие
276
- result = await execute_event_handler(event_type, user_id, event_info)
277
-
309
+ result = await execute_event_handler(
310
+ event_type, user_id, event_info
311
+ )
312
+
278
313
  # Проверяем наличие поля 'info' для дашборда
279
314
  import json
315
+
280
316
  info_dashboard_json = None
281
- if isinstance(result, dict) and 'info' in result:
282
- info_dashboard_json = json.dumps(result['info'], ensure_ascii=False)
283
- logger.info(f" 📊 Дашборд данные добавлены: {result['info'].get('title', 'N/A')}")
284
-
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
+
285
325
  # Сохраняем в БД УЖЕ со статусом completed (избегаем дублирования)
286
326
  event_record = {
287
- 'event_type': event_type,
288
- 'event_category': 'user_event',
289
- 'user_id': user_id,
290
- 'event_data': event_info,
291
- 'scheduled_at': None,
292
- 'status': 'completed', # Сразу completed!
293
- 'session_id': session_id,
294
- 'executed_at': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
295
- 'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None,
296
- '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'
297
343
  }
298
-
344
+
299
345
  # 🆕 Добавляем bot_id если указан
300
346
  if supabase_client.bot_id:
301
- event_record['bot_id'] = supabase_client.bot_id
302
-
303
- response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
304
- event_id = response.data[0]['id']
305
-
306
- should_notify = event_handler_info.get('notify', False)
307
-
308
- logger.info(f" ✅ Событие {event_id} выполнено и сохранено как completed")
309
-
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
+
310
362
  except Exception as e:
311
363
  logger.error(f" ❌ Ошибка выполнения события: {e}")
312
364
  # Сохраняем ошибку в БД
313
365
  event_record = {
314
- 'event_type': event_type,
315
- 'event_category': 'user_event',
316
- 'user_id': user_id,
317
- 'event_data': event_info,
318
- 'scheduled_at': None,
319
- 'status': 'failed',
320
- 'session_id': session_id,
321
- '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),
322
374
  }
323
-
375
+
324
376
  # 🆕 Добавляем bot_id если указан
325
377
  if supabase_client.bot_id:
326
- event_record['bot_id'] = supabase_client.bot_id
327
-
328
- supabase_client.client.table('scheduled_events').insert(event_record).execute()
378
+ event_record["bot_id"] = supabase_client.bot_id
379
+
380
+ supabase_client.client.table("scheduled_events").insert(
381
+ event_record
382
+ ).execute()
329
383
  raise
330
-
384
+
331
385
  # Если не user_event, пробуем как запланированную задачу
332
386
  elif event_type in scheduled_tasks:
333
387
  try:
334
388
  # Достаем метаданные задачи
335
389
  task_info = scheduled_tasks.get(event_type, {})
336
- send_ai_response_flag = task_info.get('send_ai_response', True)
337
-
338
- logger.info(f" ⏰ Планируем scheduled_task: '{event_type}', send_ai_response={send_ai_response_flag}")
339
-
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
+
340
396
  # Проверяем флаг send_ai_response ИЗ ДЕКОРАТОРА
341
397
  if not send_ai_response_flag:
342
398
  should_send_ai_response = False
343
- logger.warning(f" 🔇🔇🔇 ЗАДАЧА '{event_type}' ЗАПРЕТИЛА ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇")
344
-
399
+ logger.warning(
400
+ f" 🔇🔇🔇 ЗАДАЧА '{event_type}' ЗАПРЕТИЛА ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇"
401
+ )
402
+
345
403
  # Используем новую логику - время берется из декоратора
346
- result = await execute_scheduled_task_from_event(user_id, event_type, event_info, session_id)
347
- event_id = result.get('event_id', 'unknown')
348
- 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)
349
409
  logger.info(f" 💾 Задача запланирована: {event_id}")
350
-
410
+
351
411
  except Exception as e:
352
412
  if "once_only=True" in str(e):
353
- logger.info(f" 🔄 Задача '{event_type}' уже запланирована, пропускаем")
413
+ logger.info(
414
+ f" 🔄 Задача '{event_type}' уже запланирована, пропускаем"
415
+ )
354
416
  continue
355
417
  else:
356
- logger.error(f" ❌ Ошибка планирования scheduled_task '{event_type}': {e}")
418
+ logger.error(
419
+ f" ❌ Ошибка планирования scheduled_task '{event_type}': {e}"
420
+ )
357
421
  continue
358
-
422
+
359
423
  # Если не scheduled_task, пробуем как глобальный обработчик
360
424
  elif event_type in global_handlers:
361
425
  try:
362
426
  # Используем новую логику - время берется из декоратора
363
- logger.info(f" 🌍 Планируем global_handler: '{event_type}' с данными: '{event_info}'")
364
- result = await execute_global_handler_from_event(event_type, event_info)
365
- event_id = result.get('event_id', 'unknown')
366
- should_notify = result.get('notify', False)
367
- logger.info(f" 💾 Глобальное событие запланировано: {event_id}")
368
-
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
+
369
439
  except Exception as e:
370
440
  if "once_only=True" in str(e):
371
- logger.info(f" 🔄 Глобальное событие '{event_type}' уже запланировано, пропускаем")
441
+ logger.info(
442
+ f" 🔄 Глобальное событие '{event_type}' уже запланировано, пропускаем"
443
+ )
372
444
  continue
373
445
  else:
374
- logger.error(f" ❌ Ошибка планирования global_handler '{event_type}': {e}")
446
+ logger.error(
447
+ f" ❌ Ошибка планирования global_handler '{event_type}': {e}"
448
+ )
375
449
  continue
376
-
450
+
377
451
  else:
378
- logger.warning(f" ⚠️ Обработчик '{event_type}' не найден среди зарегистрированных")
379
- logger.debug(f" 🔍 Доступные обработчики:")
380
- logger.debug(f" - event_handlers: {list(event_handlers.keys())}")
381
- logger.debug(f" - scheduled_tasks: {list(scheduled_tasks.keys())}")
382
- logger.debug(f" - global_handlers: {list(global_handlers.keys())}")
383
-
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
+
384
466
  except ValueError as e:
385
467
  logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
386
468
  except Exception as e:
387
469
  logger.error(f" ❌ Ошибка в обработчике/задаче: {e}")
388
470
  logger.exception(" Стек ошибки:")
389
-
471
+
390
472
  # Уведомляем админов только если result.notify = True
391
473
  if should_notify:
392
474
  await notify_admins_about_event(user_id, event)
393
- logger.info(f" ✅ Админы уведомлены")
475
+ logger.info(" ✅ Админы уведомлены")
394
476
  else:
395
477
  logger.info(f" 🔕 Уведомления админам отключены для '{event_type}'")
396
-
478
+
397
479
  except Exception as e:
398
480
  logger.error(f"❌ Ошибка обработки события {event}: {e}")
399
481
  logger.exception("Стек ошибки:")
400
-
482
+
401
483
  # Возвращаем флаг, нужно ли отправлять сообщение от ИИ
402
- logger.warning(f"🔊🔊🔊 ИТОГОВЫЙ ФЛАГ send_ai_response: {should_send_ai_response} 🔊🔊🔊")
484
+ logger.warning(
485
+ f"🔊🔊🔊 ИТОГОВЫЙ ФЛАГ send_ai_response: {should_send_ai_response} 🔊🔊🔊"
486
+ )
403
487
  return should_send_ai_response
404
488
 
489
+
405
490
  async def notify_admins_about_event(user_id: int, event: dict):
406
491
  """Отправляем уведомление админам о событии с явным указанием ID пользователя"""
407
- supabase_client = get_global_var('supabase_client')
408
- admin_manager = get_global_var('admin_manager')
409
- bot = get_global_var('bot')
410
-
411
- event_type = event.get('тип', '')
412
- event_info = event.get('инфо', '')
413
-
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
+
414
499
  if not event_type:
415
500
  return
416
-
501
+
417
502
  # Получаем информацию о пользователе для username
418
503
  try:
419
- user_response = supabase_client.client.table('sales_users').select(
420
- 'first_name', 'last_name', 'username'
421
- ).eq('telegram_id', user_id).execute()
422
-
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
+
423
511
  user_info = user_response.data[0] if user_response.data else {}
424
-
512
+
425
513
  # Формируем имя пользователя (без ID)
426
514
  name_parts = []
427
- if user_info.get('first_name'):
428
- name_parts.append(user_info['first_name'])
429
- if user_info.get('last_name'):
430
- name_parts.append(user_info['last_name'])
431
-
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
+
432
520
  user_name = " ".join(name_parts) if name_parts else "Без имени"
433
-
521
+
434
522
  # Формируем отображение пользователя с ОБЯЗАТЕЛЬНЫМ ID
435
- if user_info.get('username'):
523
+ if user_info.get("username"):
436
524
  user_display = f"{user_name} (@{user_info['username']})"
437
525
  else:
438
526
  user_display = user_name
439
-
527
+
440
528
  except Exception as e:
441
529
  logger.error(f"Ошибка получения информации о пользователе {user_id}: {e}")
442
530
  user_display = "Пользователь"
443
-
444
- emoji_map = {
445
- 'телефон': '📱',
446
- 'консультация': '💬',
447
- 'покупка': '💰',
448
- 'отказ': '❌'
449
- }
450
-
451
- emoji = emoji_map.get(event_type, '🔔')
452
-
531
+
532
+ emoji_map = {"телефон": "📱", "консультация": "💬", "покупка": "💰", "отказ": "❌"}
533
+
534
+ emoji = emoji_map.get(event_type, "🔔")
535
+
453
536
  # 🆕 ИСПРАВЛЕНИЕ: ID всегда отображается отдельной строкой для удобства копирования
454
537
  notification = f"""
455
538
  {emoji} {event_type.upper()}!
@@ -458,116 +541,140 @@ async def notify_admins_about_event(user_id: int, event: dict):
458
541
  📝 {event_info}
459
542
  🕐 {datetime.now().strftime('%H:%M')}
460
543
  """
461
-
544
+
462
545
  # Создаем клавиатуру с кнопками
463
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
464
- [
465
- InlineKeyboardButton(text="💬 Чат", callback_data=f"admin_chat_{user_id}"),
466
- 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
+ ]
467
556
  ]
468
- ])
469
-
557
+ )
558
+
470
559
  try:
471
560
  # Отправляем всем активным админам
472
561
  active_admins = await admin_manager.get_active_admins()
473
562
  for admin_id in active_admins:
474
563
  try:
475
- 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
+ )
476
567
  except Exception as e:
477
568
  logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
478
-
569
+
479
570
  except Exception as e:
480
571
  logger.error(f"Ошибка отправки уведомления админам: {e}")
481
-
482
- 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
+ ):
483
581
  """Вспомогательная функция для отправки сообщений с настройкой parse_mode"""
484
- config = get_global_var('config')
485
-
486
- logger.info(f"📤 send_message вызвана:")
582
+ config = get_global_var("config")
583
+
584
+ logger.info("📤 send_message вызвана:")
487
585
  logger.info(f" 👤 Пользователь: {message.from_user.id}")
488
586
  logger.info(f" 📝 Длина текста: {len(text)} символов")
489
587
  logger.info(f" 🐛 Debug режим: {config.DEBUG_MODE}")
490
-
588
+
491
589
  try:
492
- 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
+ )
493
593
  logger.info(f" 🔧 Parse mode: {parse_mode}")
494
-
594
+
495
595
  # Получаем user_id и импортируем supabase_client
496
596
  user_id = message.from_user.id
497
- supabase_client = get_global_var('supabase_client')
498
-
597
+ supabase_client = get_global_var("supabase_client")
598
+
499
599
  # Текст уже готов, используем как есть
500
600
  final_text = text
501
-
601
+
502
602
  # Работаем с переданными файлами и каталогами
503
603
  logger.info(f" 📦 Передано файлов: {files_list}")
504
604
  logger.info(f" 📂 Передано каталогов: {directories_list}")
505
-
605
+
506
606
  # Получаем список уже отправленных файлов и каталогов
507
607
  sent_files = await supabase_client.get_sent_files(user_id)
508
608
  sent_directories = await supabase_client.get_sent_directories(user_id)
509
-
609
+
510
610
  logger.info(f" 📋 Уже отправлено файлов: {sent_files}")
511
611
  logger.info(f" 📋 Уже отправлено каталогов: {sent_directories}")
512
-
612
+
513
613
  # Фильтруем файлы и каталоги, которые уже отправлялись
514
614
  actual_files_list = [f for f in files_list if f not in sent_files]
515
- actual_directories_list = [d for d in directories_list if str(d) not in sent_directories]
516
-
615
+ actual_directories_list = [
616
+ d for d in directories_list if str(d) not in sent_directories
617
+ ]
618
+
517
619
  logger.info(f" 🆕 После фильтрации файлов: {actual_files_list}")
518
620
  logger.info(f" 🆕 После фильтрации каталогов: {actual_directories_list}")
519
-
520
-
621
+
521
622
  # Проверяем, что есть что отправлять
522
623
  if not final_text or not final_text.strip():
523
- logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
624
+ logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
524
625
  logger.error(f" Исходный text: '{text[:200]}...'")
525
626
  final_text = "Ошибка формирования ответа. Попробуйте еще раз."
526
-
627
+
527
628
  logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
528
629
  logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
529
630
  logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
530
-
631
+
531
632
  # Проверяем наличие файлов для отправки
532
633
  if actual_files_list or actual_directories_list:
533
634
  # Функция определения типа медиа по расширению
534
635
  def get_media_type(file_path: str) -> str:
535
636
  ext = Path(file_path).suffix.lower()
536
- if ext in {'.jpg', '.jpeg', '.png'}:
537
- return 'photo'
538
- elif ext in {'.mp4', '.mov'}:
539
- return 'video'
637
+ if ext in {".jpg", ".jpeg", ".png"}:
638
+ return "photo"
639
+ elif ext in {".mp4", ".mov"}:
640
+ return "video"
540
641
  else:
541
- return 'document'
542
-
642
+ return "document"
643
+
543
644
  # Создаем списки для разных типов файлов
544
645
  video_files = [] # для видео
545
646
  photo_files = [] # для фото
546
647
  document_files = [] # для документов
547
-
648
+
548
649
  # Функция обработки файла
549
650
  def process_file(file_path: Path, source: str = ""):
550
651
  if file_path.is_file():
551
652
  media_type = get_media_type(str(file_path))
552
- if media_type == 'video':
653
+ if media_type == "video":
553
654
  video_files.append(file_path)
554
- logger.info(f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}")
555
- elif media_type == 'photo':
655
+ logger.info(
656
+ f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}"
657
+ )
658
+ elif media_type == "photo":
556
659
  photo_files.append(file_path)
557
- 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
+ )
558
663
  else:
559
664
  document_files.append(file_path)
560
- 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
+ )
561
668
  else:
562
669
  logger.warning(f" ⚠️ Файл не найден: {file_path}")
563
-
670
+
564
671
  # Обрабатываем прямые файлы
565
672
  for file_name in actual_files_list:
566
673
  try:
567
674
  process_file(Path(f"files/{file_name}"))
568
675
  except Exception as e:
569
676
  logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
570
-
677
+
571
678
  # Обрабатываем файлы из каталогов
572
679
  for dir_name in actual_directories_list:
573
680
  dir_name = Path(dir_name)
@@ -577,85 +684,101 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
577
684
  try:
578
685
  process_file(file_path, dir_name)
579
686
  except Exception as e:
580
- logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
687
+ logger.error(
688
+ f" ❌ Ошибка обработки файла {file_path}: {e}"
689
+ )
581
690
  else:
582
691
  logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
583
692
  except Exception as e:
584
693
  logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
585
-
694
+
586
695
  # Списки для отслеживания реально отправленных файлов
587
696
  sent_files_to_save = []
588
697
  sent_dirs_to_save = []
589
-
698
+
590
699
  # 1. Отправляем видео (если есть)
591
700
  if video_files:
592
701
  video_group = MediaGroupBuilder()
593
702
  for file_path in video_files:
594
703
  video_group.add_video(media=FSInputFile(str(file_path)))
595
-
704
+
596
705
  videos = video_group.build()
597
706
  if videos:
598
707
  await message.answer_media_group(media=videos)
599
708
  logger.info(f" ✅ Отправлено {len(videos)} видео")
600
-
709
+
601
710
  # 2. Отправляем фото (если есть)
602
711
  if photo_files:
603
712
  photo_group = MediaGroupBuilder()
604
713
  for file_path in photo_files:
605
714
  photo_group.add_photo(media=FSInputFile(str(file_path)))
606
-
715
+
607
716
  photos = photo_group.build()
608
717
  if photos:
609
718
  await message.answer_media_group(media=photos)
610
719
  logger.info(f" ✅ Отправлено {len(photos)} фото")
611
-
720
+
612
721
  # 3. Отправляем текст
613
722
  result = await message.answer(final_text, parse_mode=parse_mode)
614
- logger.info(f" ✅ Отправлен текст сообщения")
615
-
723
+ logger.info(" ✅ Отправлен текст сообщения")
724
+
616
725
  # 4. Отправляем документы (если есть)
617
726
  if document_files:
618
727
  doc_group = MediaGroupBuilder()
619
728
  for file_path in document_files:
620
729
  doc_group.add_document(media=FSInputFile(str(file_path)))
621
-
730
+
622
731
  docs = doc_group.build()
623
732
  if docs:
624
733
  await message.answer_media_group(media=docs)
625
734
  logger.info(f" ✅ Отправлено {len(docs)} документов")
626
-
735
+
627
736
  # 5. Собираем список реально отправленных файлов и каталогов
628
737
  # Если были отправлены файлы из actual_files_list - сохраняем их
629
738
  if video_files or photo_files or document_files:
630
739
  # Сохраняем прямые файлы из actual_files_list (если отправлены)
631
740
  sent_files_to_save.extend(actual_files_list)
632
- logger.info(f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}")
741
+ logger.info(
742
+ f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}"
743
+ )
633
744
  # Сохраняем каталоги из actual_directories_list (если отправлены файлы из них)
634
745
  sent_dirs_to_save.extend([str(d) for d in actual_directories_list])
635
- logger.info(f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}")
636
-
746
+ logger.info(
747
+ f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}"
748
+ )
749
+
637
750
  # 6. Обновляем информацию в БД
638
751
  if sent_files_to_save or sent_dirs_to_save:
639
752
  try:
640
753
  if sent_files_to_save:
641
754
  logger.info(f" 💾 Сохраняем файлы в БД: {sent_files_to_save}")
642
- 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
+ )
643
758
  if sent_dirs_to_save:
644
- logger.info(f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}")
645
- await supabase_client.add_sent_directories(user_id, sent_dirs_to_save)
646
- 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
+ )
647
768
  except Exception as e:
648
- logger.error(f" ❌ Ошибка обновления информации о файлах в БД: {e}")
769
+ logger.error(
770
+ f" ❌ Ошибка обновления информации о файлах в БД: {e}"
771
+ )
649
772
  else:
650
- logger.info(f" ℹ️ Нет новых файлов для сохранения в БД")
651
-
773
+ logger.info(" ℹ️ Нет новых файлов для сохранения в БД")
774
+
652
775
  return result
653
776
  else:
654
777
  # Если нет файлов, отправляем просто текст
655
- logger.warning(" ⚠️ Нет файлов для отправки, отправляем как текст")
656
- result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
657
- return result
658
-
778
+ logger.warning(" ⚠️ Нет файлов для отправки, отправляем как текст")
779
+ result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
780
+ return result
781
+
659
782
  except Exception as e:
660
783
  # Проверяем, является ли ошибка блокировкой бота
661
784
  if "Forbidden: bot was blocked by the user" in str(e):
@@ -664,32 +787,37 @@ async def send_message(message: Message, text: str, files_list: list = [], direc
664
787
  elif "TelegramForbiddenError" in str(type(e).__name__):
665
788
  logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
666
789
  return None
667
-
790
+
668
791
  logger.error(f"❌ ОШИБКА в send_message: {e}")
669
792
  logger.exception("Полный стек ошибки send_message:")
670
-
793
+
671
794
  # Пытаемся отправить простое сообщение без форматирования
672
795
  try:
673
796
  fallback_text = "Произошла ошибка при отправке ответа. Попробуйте еще раз."
674
797
  result = await message.answer(fallback_text)
675
- logger.info(f"✅ Запасное сообщение отправлено")
798
+ logger.info("✅ Запасное сообщение отправлено")
676
799
  return result
677
800
  except Exception as e2:
678
801
  # Проверяем и здесь блокировку бота
679
802
  if "Forbidden: bot was blocked by the user" in str(e2):
680
- logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
803
+ logger.warning(
804
+ f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
805
+ )
681
806
  return None
682
807
  elif "TelegramForbiddenError" in str(type(e2).__name__):
683
- logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
808
+ logger.warning(
809
+ f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
810
+ )
684
811
  return None
685
-
812
+
686
813
  logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
687
814
  raise
688
-
815
+
816
+
689
817
  async def cleanup_expired_conversations():
690
818
  """Периодическая очистка просроченных диалогов"""
691
- conversation_manager = get_global_var('conversation_manager')
692
-
819
+ conversation_manager = get_global_var("conversation_manager")
820
+
693
821
  while True:
694
822
  try:
695
823
  await asyncio.sleep(300) # каждые 5 минут
@@ -697,18 +825,20 @@ async def cleanup_expired_conversations():
697
825
  except Exception as e:
698
826
  logger.error(f"Ошибка очистки просроченных диалогов: {e}")
699
827
 
828
+
700
829
  # 🆕 Вспомогательные функции для приветственного файла
701
830
 
831
+
702
832
  async def get_welcome_file_path() -> str | None:
703
833
  """Возвращает путь к PDF файлу из папки WELCOME_FILE_DIR из конфига.
704
834
 
705
835
  Источник настроек: configs/<bot_id>/.env (переменная WELCOME_FILE_DIR)
706
836
  Рабочая директория уже установлена запускалкой на configs/<bot_id>.
707
-
837
+
708
838
  Returns:
709
839
  str | None: Путь к PDF файлу или None, если файл не найден
710
840
  """
711
- config = get_global_var('config')
841
+ config = get_global_var("config")
712
842
  try:
713
843
  folder_value = config.WELCOME_FILE_DIR
714
844
  if not folder_value:
@@ -716,28 +846,31 @@ async def get_welcome_file_path() -> str | None:
716
846
 
717
847
  folder = Path(folder_value)
718
848
  if not folder.exists():
719
- logger.info(f"Директория приветственных файлов не существует: {folder_value}")
849
+ logger.info(
850
+ f"Директория приветственных файлов не существует: {folder_value}"
851
+ )
720
852
  return None
721
-
853
+
722
854
  if not folder.is_dir():
723
855
  logger.info(f"Путь не является директорией: {folder_value}")
724
856
  return None
725
857
 
726
858
  # Ищем первый PDF файл в директории
727
859
  for path in folder.iterdir():
728
- if path.is_file() and path.suffix.lower() == '.pdf':
860
+ if path.is_file() and path.suffix.lower() == ".pdf":
729
861
  return str(path)
730
-
862
+
731
863
  logger.info(f"PDF файл не найден в директории: {folder_value}")
732
864
  return None
733
-
865
+
734
866
  except Exception as e:
735
867
  logger.error(f"Ошибка при поиске приветственного файла: {e}")
736
868
  return None
737
869
 
870
+
738
871
  async def get_welcome_msg_path() -> str | None:
739
872
  """Возвращает путь к файлу welcome_file_msg.txt из той же директории, где находится PDF файл.
740
-
873
+
741
874
  Returns:
742
875
  str | None: Путь к файлу с подписью или None, если файл не найден
743
876
  """
@@ -745,28 +878,29 @@ async def get_welcome_msg_path() -> str | None:
745
878
  pdf_path = await get_welcome_file_path()
746
879
  if not pdf_path:
747
880
  return None
748
-
749
- msg_path = str(Path(pdf_path).parent / 'welcome_file_msg.txt')
881
+
882
+ msg_path = str(Path(pdf_path).parent / "welcome_file_msg.txt")
750
883
  if not Path(msg_path).is_file():
751
884
  logger.info(f"Файл подписи не найден: {msg_path}")
752
885
  return None
753
-
886
+
754
887
  return msg_path
755
-
888
+
756
889
  except Exception as e:
757
890
  logger.error(f"Ошибка при поиске файла подписи: {e}")
758
891
  return None
759
892
 
893
+
760
894
  async def send_welcome_file(message: Message) -> str:
761
895
  """
762
896
  Отправляет приветственный файл с подписью из файла welcome_file_msg.txt.
763
897
  Если файл подписи не найден, используется пустая подпись.
764
-
898
+
765
899
  Returns:
766
900
  str: текст подписи
767
901
  """
768
902
  try:
769
- config = get_global_var('config')
903
+ config = get_global_var("config")
770
904
 
771
905
  file_path = await get_welcome_file_path()
772
906
  if not file_path:
@@ -777,7 +911,7 @@ async def send_welcome_file(message: Message) -> str:
777
911
  msg_path = await get_welcome_msg_path()
778
912
  if msg_path:
779
913
  try:
780
- with open(msg_path, 'r', encoding='utf-8') as f:
914
+ with open(msg_path, "r", encoding="utf-8") as f:
781
915
  caption = f.read().strip()
782
916
  logger.info(f"Подпись загружена из файла: {msg_path}")
783
917
  except Exception as e:
@@ -785,23 +919,27 @@ async def send_welcome_file(message: Message) -> str:
785
919
 
786
920
  parse_mode = config.MESSAGE_PARSE_MODE
787
921
  document = FSInputFile(file_path)
788
-
789
- await message.answer_document(document=document, caption=caption, parse_mode=parse_mode)
790
-
922
+
923
+ await message.answer_document(
924
+ document=document, caption=caption, parse_mode=parse_mode
925
+ )
926
+
791
927
  logger.info(f"Приветственный файл отправлен: {file_path}")
792
928
  return caption
793
929
  except Exception as e:
794
930
  logger.error(f"Ошибка при отправке приветственного файла: {e}")
795
931
  return ""
796
932
 
933
+
797
934
  # Общие команды
798
935
 
936
+
799
937
  @utils_router.message(Command("help"))
800
938
  async def help_handler(message: Message):
801
939
  """Справка"""
802
- admin_manager = get_global_var('admin_manager')
803
- prompt_loader = get_global_var('prompt_loader')
804
-
940
+ admin_manager = get_global_var("admin_manager")
941
+ prompt_loader = get_global_var("prompt_loader")
942
+
805
943
  try:
806
944
  # Разная справка для админов и пользователей
807
945
  if admin_manager.is_admin(message.from_user.id):
@@ -822,37 +960,41 @@ async def help_handler(message: Message):
822
960
  • Ваши сообщения отправляются пользователю как от бота
823
961
  • Диалоги автоматически завершаются через 30 минут
824
962
  """
825
- await message.answer(help_text, parse_mode='Markdown')
963
+ await message.answer(help_text, parse_mode="Markdown")
826
964
  return
827
-
965
+
828
966
  # Обычная справка для пользователей
829
967
  help_text = await prompt_loader.load_help_message()
830
968
  await send_message(message, help_text)
831
-
969
+
832
970
  except Exception as e:
833
971
  logger.error(f"Ошибка загрузки справки: {e}")
834
972
  # Fallback справка
835
- await send_message(message, "🤖 Ваш помощник готов к работе! Напишите /start для начала диалога.")
973
+ await send_message(
974
+ message,
975
+ "🤖 Ваш помощник готов к работе! Напишите /start для начала диалога.",
976
+ )
977
+
836
978
 
837
979
  @utils_router.message(Command("status"))
838
980
  async def status_handler(message: Message):
839
981
  """Проверка статуса системы"""
840
- openai_client = get_global_var('openai_client')
841
- prompt_loader = get_global_var('prompt_loader')
842
- admin_manager = get_global_var('admin_manager')
843
- config = get_global_var('config')
844
-
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
+
845
987
  try:
846
988
  # Проверяем OpenAI
847
989
  openai_status = await openai_client.check_api_health()
848
-
990
+
849
991
  # Проверяем промпты
850
992
  prompts_status = await prompt_loader.validate_prompts()
851
-
993
+
852
994
  # Статистика для админов
853
995
  if admin_manager.is_admin(message.from_user.id):
854
996
  admin_stats = admin_manager.get_stats()
855
-
997
+
856
998
  status_message = f"""
857
999
  🔧 **Статус системы:**
858
1000
 
@@ -875,62 +1017,62 @@ OpenAI API: {'✅' if openai_status else '❌'}
875
1017
 
876
1018
  Все системы работают нормально!
877
1019
  """
878
-
1020
+
879
1021
  await send_message(message, status_message)
880
-
1022
+
881
1023
  except Exception as e:
882
1024
  logger.error(f"Ошибка проверки статуса: {e}")
883
1025
  await send_message(message, "❌ Ошибка при проверке статуса системы")
884
-
885
-
1026
+
1027
+
886
1028
  def parse_utm_from_start_param(start_param: str) -> dict:
887
1029
  """Парсит UTM-метки и сегмент из start параметра в формате source-vk_campaign-summer2025_seg-premium
888
-
1030
+
889
1031
  Args:
890
1032
  start_param: строка вида 'source-vk_campaign-summer2025_seg-premium' или полная ссылка
891
-
1033
+
892
1034
  Returns:
893
1035
  dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'premium'}
894
-
1036
+
895
1037
  Examples:
896
1038
  >>> parse_utm_from_start_param('source-vk_campaign-summer2025_seg-premium')
897
1039
  {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'premium'}
898
-
1040
+
899
1041
  >>> parse_utm_from_start_param('https://t.me/bot?start=source-vk_campaign-summer2025_seg-vip')
900
1042
  {'utm_source': 'vk', 'utm_campaign': 'summer2025', 'segment': 'vip'}
901
1043
  """
902
1044
  import re
903
1045
  from urllib.parse import unquote
904
-
1046
+
905
1047
  utm_data = {}
906
-
1048
+
907
1049
  try:
908
1050
  # Если это полная ссылка, извлекаем start параметр
909
- if 't.me/' in start_param or 'https://' in start_param:
910
- 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)
911
1053
  if match:
912
1054
  start_param = unquote(match.group(1))
913
1055
  else:
914
1056
  return {}
915
-
1057
+
916
1058
  # Парсим новый формат: source-vk_campaign-summer2025_seg-premium
917
1059
  # Поддерживает как комбинированные параметры, так и одиночные (например, только seg-prem)
918
- if '-' in start_param:
1060
+ if "-" in start_param:
919
1061
  # Разделяем по _ (если есть несколько параметров) или используем весь параметр
920
- parts = start_param.split('_') if '_' in start_param else [start_param]
921
-
1062
+ parts = start_param.split("_") if "_" in start_param else [start_param]
1063
+
922
1064
  for part in parts:
923
- if '-' in part:
924
- key, value = part.split('-', 1)
1065
+ if "-" in part:
1066
+ key, value = part.split("-", 1)
925
1067
  # Преобразуем source/medium/campaign/content/term в utm_*
926
- if key in ['source', 'medium', 'campaign', 'content', 'term']:
927
- key = 'utm_' + key
1068
+ if key in ["source", "medium", "campaign", "content", "term"]:
1069
+ key = "utm_" + key
928
1070
  utm_data[key] = value
929
1071
  # Обрабатываем seg как segment
930
- elif key == 'seg':
931
- utm_data['segment'] = value
932
-
1072
+ elif key == "seg":
1073
+ utm_data["segment"] = value
1074
+
933
1075
  except Exception as e:
934
1076
  print(f"Ошибка парсинга UTM параметров: {e}")
935
-
936
- return utm_data
1077
+
1078
+ return utm_data