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.
- smart_bot_factory/__init__.py +3 -0
- smart_bot_factory/admin/__init__.py +18 -0
- smart_bot_factory/admin/admin_events.py +1223 -0
- smart_bot_factory/admin/admin_logic.py +553 -0
- smart_bot_factory/admin/admin_manager.py +156 -0
- smart_bot_factory/admin/admin_tester.py +157 -0
- smart_bot_factory/admin/timeout_checker.py +547 -0
- smart_bot_factory/aiogram_calendar/__init__.py +14 -0
- smart_bot_factory/aiogram_calendar/common.py +64 -0
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +259 -0
- smart_bot_factory/aiogram_calendar/schemas.py +99 -0
- smart_bot_factory/aiogram_calendar/simple_calendar.py +224 -0
- smart_bot_factory/analytics/analytics_manager.py +414 -0
- smart_bot_factory/cli.py +806 -0
- smart_bot_factory/config.py +258 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +133 -0
- smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
- smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/core/bot_utils.py +1108 -0
- smart_bot_factory/core/conversation_manager.py +653 -0
- smart_bot_factory/core/decorators.py +2464 -0
- smart_bot_factory/core/message_sender.py +729 -0
- smart_bot_factory/core/router.py +347 -0
- smart_bot_factory/core/router_manager.py +218 -0
- smart_bot_factory/core/states.py +27 -0
- smart_bot_factory/creation/__init__.py +7 -0
- smart_bot_factory/creation/bot_builder.py +1093 -0
- smart_bot_factory/creation/bot_testing.py +1122 -0
- smart_bot_factory/dashboard/__init__.py +3 -0
- smart_bot_factory/event/__init__.py +7 -0
- smart_bot_factory/handlers/handlers.py +2013 -0
- smart_bot_factory/integrations/langchain_openai.py +542 -0
- smart_bot_factory/integrations/openai_client.py +513 -0
- smart_bot_factory/integrations/supabase_client.py +1678 -0
- smart_bot_factory/memory/__init__.py +8 -0
- smart_bot_factory/memory/memory_manager.py +299 -0
- smart_bot_factory/memory/static_memory.py +214 -0
- smart_bot_factory/message/__init__.py +56 -0
- smart_bot_factory/rag/__init__.py +5 -0
- smart_bot_factory/rag/decorators.py +29 -0
- smart_bot_factory/rag/router.py +54 -0
- smart_bot_factory/rag/templates/__init__.py +3 -0
- smart_bot_factory/rag/templates/create_table.sql +7 -0
- smart_bot_factory/rag/templates/create_table_and_function_template.py +94 -0
- smart_bot_factory/rag/templates/match_function.sql +61 -0
- smart_bot_factory/rag/templates/match_services_template.py +82 -0
- smart_bot_factory/rag/vectorstore.py +449 -0
- smart_bot_factory/router/__init__.py +10 -0
- smart_bot_factory/setup_checker.py +512 -0
- smart_bot_factory/supabase/__init__.py +7 -0
- smart_bot_factory/supabase/client.py +631 -0
- smart_bot_factory/utils/__init__.py +11 -0
- smart_bot_factory/utils/debug_routing.py +114 -0
- smart_bot_factory/utils/prompt_loader.py +529 -0
- smart_bot_factory/utils/tool_router.py +68 -0
- smart_bot_factory/utils/user_prompt_loader.py +55 -0
- smart_bot_factory/utm_link_generator.py +123 -0
- smart_bot_factory-1.1.1.dist-info/METADATA +1135 -0
- smart_bot_factory-1.1.1.dist-info/RECORD +73 -0
- smart_bot_factory-1.1.1.dist-info/WHEEL +4 -0
- smart_bot_factory-1.1.1.dist-info/entry_points.txt +2 -0
- 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
|