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