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