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