smart-bot-factory 0.1.2__py3-none-any.whl → 0.1.3__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/__init__.py +51 -0
- smart_bot_factory/admin/__init__.py +16 -0
- smart_bot_factory/admin/admin_logic.py +430 -0
- smart_bot_factory/admin/admin_manager.py +141 -0
- smart_bot_factory/admin/admin_migration.sql +136 -0
- smart_bot_factory/admin/admin_tester.py +151 -0
- smart_bot_factory/admin/timeout_checker.py +499 -0
- smart_bot_factory/analytics/__init__.py +7 -0
- smart_bot_factory/analytics/analytics_manager.py +355 -0
- smart_bot_factory/cli.py +642 -0
- smart_bot_factory/config.py +235 -0
- smart_bot_factory/configs/growthmed-helper/env_example.txt +1 -0
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +9 -0
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +232 -0
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +7 -0
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-helper/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +66 -0
- smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
- smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/core/__init__.py +22 -0
- smart_bot_factory/core/bot_utils.py +693 -0
- smart_bot_factory/core/conversation_manager.py +536 -0
- smart_bot_factory/core/decorators.py +229 -0
- smart_bot_factory/core/message_sender.py +249 -0
- smart_bot_factory/core/states.py +14 -0
- smart_bot_factory/creation/__init__.py +8 -0
- smart_bot_factory/creation/bot_builder.py +329 -0
- smart_bot_factory/creation/bot_testing.py +986 -0
- smart_bot_factory/database/database_structure.sql +57 -0
- smart_bot_factory/database/schema.sql +1094 -0
- smart_bot_factory/handlers/handlers.py +583 -0
- smart_bot_factory/integrations/__init__.py +9 -0
- smart_bot_factory/integrations/openai_client.py +435 -0
- smart_bot_factory/integrations/supabase_client.py +592 -0
- smart_bot_factory/setup_checker.py +476 -0
- smart_bot_factory/utils/__init__.py +9 -0
- smart_bot_factory/utils/debug_routing.py +103 -0
- smart_bot_factory/utils/prompt_loader.py +427 -0
- smart_bot_factory/uv.lock +2004 -0
- smart_bot_factory-0.1.3.dist-info/METADATA +126 -0
- smart_bot_factory-0.1.3.dist-info/RECORD +59 -0
- smart_bot_factory-0.1.3.dist-info/licenses/LICENSE +24 -0
- smart_bot_factory-0.1.2.dist-info/METADATA +0 -31
- smart_bot_factory-0.1.2.dist-info/RECORD +0 -4
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from aiogram import Router
|
|
6
|
+
from aiogram.filters import Command
|
|
7
|
+
from aiogram.types import (
|
|
8
|
+
Message,
|
|
9
|
+
InlineKeyboardMarkup,
|
|
10
|
+
InlineKeyboardButton,
|
|
11
|
+
FSInputFile,
|
|
12
|
+
)
|
|
13
|
+
from aiogram.utils.media_group import MediaGroupBuilder
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from ..core.decorators import execute_event_handler, execute_scheduled_task
|
|
17
|
+
|
|
18
|
+
# Функция для получения глобальных переменных
|
|
19
|
+
def get_global_var(var_name):
|
|
20
|
+
"""Получает глобальную переменную из модуля bot_utils"""
|
|
21
|
+
import sys
|
|
22
|
+
current_module = sys.modules[__name__]
|
|
23
|
+
return getattr(current_module, var_name, None)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Создаем роутер для общих команд
|
|
28
|
+
utils_router = Router()
|
|
29
|
+
|
|
30
|
+
def setup_utils_handlers(dp):
|
|
31
|
+
"""Настройка обработчиков утилит"""
|
|
32
|
+
dp.include_router(utils_router)
|
|
33
|
+
|
|
34
|
+
def parse_ai_response(ai_response: str) -> tuple[str, dict]:
|
|
35
|
+
"""Исправленная функция парсинга JSON из конца ответа ИИ"""
|
|
36
|
+
try:
|
|
37
|
+
# Метод 1: Ищем последнюю позицию, где начинается JSON с "этап"
|
|
38
|
+
last_etap_pos = ai_response.rfind('"этап"')
|
|
39
|
+
if last_etap_pos == -1:
|
|
40
|
+
logger.debug("JSON без ключа 'этап' не найден")
|
|
41
|
+
return ai_response, {}
|
|
42
|
+
|
|
43
|
+
# Ищем открывающую скобку перед "этап"
|
|
44
|
+
json_start = -1
|
|
45
|
+
for i in range(last_etap_pos, -1, -1):
|
|
46
|
+
if ai_response[i] == '{':
|
|
47
|
+
json_start = i
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if json_start == -1:
|
|
51
|
+
logger.debug("Открывающая скобка перед 'этап' не найдена")
|
|
52
|
+
return ai_response, {}
|
|
53
|
+
|
|
54
|
+
# Теперь найдем соответствующую закрывающую скобку
|
|
55
|
+
brace_count = 0
|
|
56
|
+
json_end = -1
|
|
57
|
+
|
|
58
|
+
for i in range(json_start, len(ai_response)):
|
|
59
|
+
char = ai_response[i]
|
|
60
|
+
if char == '{':
|
|
61
|
+
brace_count += 1
|
|
62
|
+
elif char == '}':
|
|
63
|
+
brace_count -= 1
|
|
64
|
+
if brace_count == 0:
|
|
65
|
+
json_end = i
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
if json_end == -1:
|
|
69
|
+
logger.debug("Соответствующая закрывающая скобка не найдена")
|
|
70
|
+
return ai_response, {}
|
|
71
|
+
|
|
72
|
+
# Извлекаем JSON и текст ответа
|
|
73
|
+
json_str = ai_response[json_start:json_end + 1]
|
|
74
|
+
response_text = ai_response[:json_start].strip()
|
|
75
|
+
|
|
76
|
+
# 🆕 ИСПРАВЛЕНИЕ: Если response_text пустой, используем исходный ответ БЕЗ JSON
|
|
77
|
+
if not response_text:
|
|
78
|
+
logger.debug("Текст ответа пустой после удаления JSON, используем исходный ответ без JSON части")
|
|
79
|
+
# Берем все кроме JSON части
|
|
80
|
+
remaining_text = ai_response[json_end + 1:].strip()
|
|
81
|
+
if remaining_text:
|
|
82
|
+
response_text = remaining_text
|
|
83
|
+
else:
|
|
84
|
+
# Если и после JSON ничего нет, значит ответ был только JSON
|
|
85
|
+
response_text = "Ответ обработан системой."
|
|
86
|
+
logger.warning("Ответ ИИ содержал только JSON без текста")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
metadata = json.loads(json_str)
|
|
90
|
+
logger.debug(f"JSON успешно распарсен: {metadata}")
|
|
91
|
+
return response_text, metadata
|
|
92
|
+
except json.JSONDecodeError as e:
|
|
93
|
+
logger.warning(f"Ошибка парсинга JSON: {e}")
|
|
94
|
+
logger.debug(f"JSON строка: {json_str}")
|
|
95
|
+
return parse_ai_response_method2(ai_response)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning(f"Ошибка парсинга JSON от ИИ: {e}")
|
|
99
|
+
return parse_ai_response_method2(ai_response)
|
|
100
|
+
|
|
101
|
+
def parse_ai_response_method2(ai_response: str) -> tuple[str, dict]:
|
|
102
|
+
"""Резервный метод парсинга JSON - поиск по строкам (переименован для соответствия тестам)"""
|
|
103
|
+
try:
|
|
104
|
+
logger.debug("Используем резервный метод парсинга JSON")
|
|
105
|
+
|
|
106
|
+
lines = ai_response.strip().split('\n')
|
|
107
|
+
|
|
108
|
+
# Ищем строку с "этап"
|
|
109
|
+
etap_line = -1
|
|
110
|
+
for i, line in enumerate(lines):
|
|
111
|
+
if '"этап"' in line:
|
|
112
|
+
etap_line = i
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if etap_line == -1:
|
|
116
|
+
return ai_response, {}
|
|
117
|
+
|
|
118
|
+
# Ищем начало JSON (строку с { перед этап)
|
|
119
|
+
json_start_line = -1
|
|
120
|
+
for i in range(etap_line, -1, -1):
|
|
121
|
+
if lines[i].strip().startswith('{'):
|
|
122
|
+
json_start_line = i
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
if json_start_line == -1:
|
|
126
|
+
return ai_response, {}
|
|
127
|
+
|
|
128
|
+
# Ищем конец JSON (балансируем скобки)
|
|
129
|
+
brace_count = 0
|
|
130
|
+
json_end_line = -1
|
|
131
|
+
|
|
132
|
+
for i in range(json_start_line, len(lines)):
|
|
133
|
+
line = lines[i]
|
|
134
|
+
for char in line:
|
|
135
|
+
if char == '{':
|
|
136
|
+
brace_count += 1
|
|
137
|
+
elif char == '}':
|
|
138
|
+
brace_count -= 1
|
|
139
|
+
if brace_count == 0:
|
|
140
|
+
json_end_line = i
|
|
141
|
+
break
|
|
142
|
+
if json_end_line != -1:
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if json_end_line == -1:
|
|
146
|
+
return ai_response, {}
|
|
147
|
+
|
|
148
|
+
# Собираем JSON
|
|
149
|
+
json_lines = lines[json_start_line:json_end_line + 1]
|
|
150
|
+
json_str = '\n'.join(json_lines)
|
|
151
|
+
|
|
152
|
+
# Собираем текст ответа
|
|
153
|
+
response_lines = lines[:json_start_line]
|
|
154
|
+
response_text = '\n'.join(response_lines).strip()
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
metadata = json.loads(json_str)
|
|
158
|
+
logger.debug(f"JSON распарсен резервным методом: {metadata}")
|
|
159
|
+
return response_text, metadata
|
|
160
|
+
except json.JSONDecodeError as e:
|
|
161
|
+
logger.warning(f"Резервный метод: ошибка JSON: {e}")
|
|
162
|
+
return ai_response, {}
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Ошибка резервного метода: {e}")
|
|
166
|
+
return ai_response, {}
|
|
167
|
+
|
|
168
|
+
async def process_events(session_id: str, events: list, user_id: int):
|
|
169
|
+
"""Обрабатывает события из ответа ИИ"""
|
|
170
|
+
supabase_client = get_global_var('supabase_client')
|
|
171
|
+
|
|
172
|
+
for event in events:
|
|
173
|
+
try:
|
|
174
|
+
event_type = event.get('тип', '')
|
|
175
|
+
event_info = event.get('инфо', '')
|
|
176
|
+
|
|
177
|
+
if not event_type:
|
|
178
|
+
logger.warning(f"⚠️ Событие без типа: {event}")
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
logger.info(f"\n🔔 Обработка события:")
|
|
182
|
+
logger.info(f" 📝 Тип: {event_type}")
|
|
183
|
+
logger.info(f" 📄 Данные: {event_info}")
|
|
184
|
+
|
|
185
|
+
# Сохраняем в БД
|
|
186
|
+
await supabase_client.add_session_event(session_id, event_type, event_info)
|
|
187
|
+
logger.info(f" ✅ Событие сохранено в БД")
|
|
188
|
+
|
|
189
|
+
# Уведомляем админов
|
|
190
|
+
await notify_admins_about_event(user_id, event)
|
|
191
|
+
logger.info(f" ✅ Админы уведомлены")
|
|
192
|
+
|
|
193
|
+
# Вызываем зарегистрированный обработчик события или задачи
|
|
194
|
+
try:
|
|
195
|
+
# Сначала пробуем как обычное событие
|
|
196
|
+
try:
|
|
197
|
+
logger.info(f" 🎯 Вызываем обработчик события '{event_type}'")
|
|
198
|
+
result = await execute_event_handler(event_type, user_id, event_info)
|
|
199
|
+
logger.info(f" ✅ Обработчик события вернул: {result}")
|
|
200
|
+
except ValueError:
|
|
201
|
+
# Если обработчик события не найден, пробуем как запланированную задачу
|
|
202
|
+
logger.info(f" ⏰ Пробуем как запланированную задачу '{event_type}'")
|
|
203
|
+
result = await execute_scheduled_task(event_type, user_id, event_info)
|
|
204
|
+
logger.info(f" ✅ Задача выполнена: {result}")
|
|
205
|
+
except ValueError as e:
|
|
206
|
+
logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f" ❌ Ошибка в обработчике/задаче: {e}")
|
|
209
|
+
logger.exception(" Стек ошибки:")
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"❌ Ошибка обработки события {event}: {e}")
|
|
213
|
+
logger.exception("Стек ошибки:")
|
|
214
|
+
|
|
215
|
+
async def notify_admins_about_event(user_id: int, event: dict):
|
|
216
|
+
"""Отправляем уведомление админам о событии с явным указанием ID пользователя"""
|
|
217
|
+
supabase_client = get_global_var('supabase_client')
|
|
218
|
+
admin_manager = get_global_var('admin_manager')
|
|
219
|
+
bot = get_global_var('bot')
|
|
220
|
+
|
|
221
|
+
event_type = event.get('тип', '')
|
|
222
|
+
event_info = event.get('инфо', '')
|
|
223
|
+
|
|
224
|
+
if not event_type:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Получаем информацию о пользователе для username
|
|
228
|
+
try:
|
|
229
|
+
user_response = supabase_client.client.table('sales_users').select(
|
|
230
|
+
'first_name', 'last_name', 'username'
|
|
231
|
+
).eq('telegram_id', user_id).execute()
|
|
232
|
+
|
|
233
|
+
user_info = user_response.data[0] if user_response.data else {}
|
|
234
|
+
|
|
235
|
+
# Формируем имя пользователя (без ID)
|
|
236
|
+
name_parts = []
|
|
237
|
+
if user_info.get('first_name'):
|
|
238
|
+
name_parts.append(user_info['first_name'])
|
|
239
|
+
if user_info.get('last_name'):
|
|
240
|
+
name_parts.append(user_info['last_name'])
|
|
241
|
+
|
|
242
|
+
user_name = " ".join(name_parts) if name_parts else "Без имени"
|
|
243
|
+
|
|
244
|
+
# Формируем отображение пользователя с ОБЯЗАТЕЛЬНЫМ ID
|
|
245
|
+
if user_info.get('username'):
|
|
246
|
+
user_display = f"{user_name} (@{user_info['username']})"
|
|
247
|
+
else:
|
|
248
|
+
user_display = user_name
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error(f"Ошибка получения информации о пользователе {user_id}: {e}")
|
|
252
|
+
user_display = "Пользователь"
|
|
253
|
+
|
|
254
|
+
emoji_map = {
|
|
255
|
+
'телефон': '📱',
|
|
256
|
+
'консультация': '💬',
|
|
257
|
+
'покупка': '💰',
|
|
258
|
+
'отказ': '❌'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
emoji = emoji_map.get(event_type, '🔔')
|
|
262
|
+
|
|
263
|
+
# 🆕 ИСПРАВЛЕНИЕ: ID всегда отображается отдельной строкой для удобства копирования
|
|
264
|
+
notification = f"""
|
|
265
|
+
{emoji} {event_type.upper()}!
|
|
266
|
+
👤 {user_display}
|
|
267
|
+
🆔 ID: {user_id}
|
|
268
|
+
📝 {event_info}
|
|
269
|
+
🕐 {datetime.now().strftime('%H:%M')}
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
# Создаем клавиатуру с кнопками
|
|
273
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
274
|
+
[
|
|
275
|
+
InlineKeyboardButton(text="💬 Чат", callback_data=f"admin_chat_{user_id}"),
|
|
276
|
+
InlineKeyboardButton(text="📋 История", callback_data=f"admin_history_{user_id}")
|
|
277
|
+
]
|
|
278
|
+
])
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
# Отправляем всем активным админам
|
|
282
|
+
active_admins = await admin_manager.get_active_admins()
|
|
283
|
+
for admin_id in active_admins:
|
|
284
|
+
try:
|
|
285
|
+
await bot.send_message(admin_id, notification.strip(), reply_markup=keyboard)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"Ошибка отправки уведомления админу {admin_id}: {e}")
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"Ошибка отправки уведомления админам: {e}")
|
|
291
|
+
|
|
292
|
+
async def send_message(message: Message, text: str, files_list: list = [], directories_list: list = [], **kwargs):
|
|
293
|
+
"""Вспомогательная функция для отправки сообщений с настройкой parse_mode"""
|
|
294
|
+
config = get_global_var('config')
|
|
295
|
+
|
|
296
|
+
logger.info(f"📤 send_message вызвана:")
|
|
297
|
+
logger.info(f" 👤 Пользователь: {message.from_user.id}")
|
|
298
|
+
logger.info(f" 📝 Длина текста: {len(text)} символов")
|
|
299
|
+
logger.info(f" 🐛 Debug режим: {config.DEBUG_MODE}")
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != 'None' else None
|
|
303
|
+
logger.info(f" 🔧 Parse mode: {parse_mode}")
|
|
304
|
+
|
|
305
|
+
# В режиме отладки не скрываем JSON
|
|
306
|
+
if config.DEBUG_MODE:
|
|
307
|
+
final_text = text
|
|
308
|
+
logger.info(f" 🐛 Отправляем полный текст (debug режим)")
|
|
309
|
+
else:
|
|
310
|
+
# Убираем JSON если он есть
|
|
311
|
+
final_text, json_metadata = parse_ai_response(text)
|
|
312
|
+
logger.info(f" ✂️ После очистки JSON: {len(final_text)} символов")
|
|
313
|
+
|
|
314
|
+
# Добавляем информацию о файлах и каталогах в конец сообщения
|
|
315
|
+
if json_metadata:
|
|
316
|
+
logger.info(f" 📊 Найден JSON: {json_metadata}")
|
|
317
|
+
|
|
318
|
+
files_list = json_metadata.get('файлы', [])
|
|
319
|
+
directories_list = json_metadata.get('каталоги', [])
|
|
320
|
+
|
|
321
|
+
files_info = []
|
|
322
|
+
if files_list:
|
|
323
|
+
files_str = "\n".join(f"• {file}" for file in files_list)
|
|
324
|
+
files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
|
|
325
|
+
|
|
326
|
+
if directories_list:
|
|
327
|
+
dirs_str = "\n".join(f"• {directory}" for directory in directories_list)
|
|
328
|
+
files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
|
|
329
|
+
|
|
330
|
+
if files_info:
|
|
331
|
+
final_text = final_text.strip() + "".join(files_info)
|
|
332
|
+
logger.info(f" ✨ Добавлена информация о {len(files_list)} файлах и {len(directories_list)} каталогах")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# Проверяем, что есть что отправлять
|
|
336
|
+
if not final_text or not final_text.strip():
|
|
337
|
+
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
|
|
338
|
+
logger.error(f" Исходный text: '{text[:200]}...'")
|
|
339
|
+
final_text = "Ошибка формирования ответа. Попробуйте еще раз."
|
|
340
|
+
|
|
341
|
+
logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
|
|
342
|
+
|
|
343
|
+
# Проверяем наличие файлов для отправки
|
|
344
|
+
if files_list or directories_list:
|
|
345
|
+
# Функция определения типа медиа по расширению
|
|
346
|
+
def get_media_type(file_path: str) -> str:
|
|
347
|
+
ext = Path(file_path).suffix.lower()
|
|
348
|
+
if ext in {'.jpg', '.jpeg', '.png'}:
|
|
349
|
+
return 'photo'
|
|
350
|
+
elif ext in {'.mp4'}:
|
|
351
|
+
return 'video'
|
|
352
|
+
else:
|
|
353
|
+
return 'document'
|
|
354
|
+
|
|
355
|
+
# Создаем списки для разных типов файлов
|
|
356
|
+
media_files = [] # для фото и видео
|
|
357
|
+
document_files = [] # для документов
|
|
358
|
+
|
|
359
|
+
# Функция обработки файла
|
|
360
|
+
def process_file(file_path: Path, source: str = ""):
|
|
361
|
+
if file_path.is_file():
|
|
362
|
+
media_type = get_media_type(str(file_path))
|
|
363
|
+
if media_type in ('photo', 'video'):
|
|
364
|
+
media_files.append((file_path, media_type))
|
|
365
|
+
logger.info(f" 📸 Добавлен медиафайл{f' из {source}' if source else ''}: {file_path.name}")
|
|
366
|
+
else:
|
|
367
|
+
document_files.append(file_path)
|
|
368
|
+
logger.info(f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}")
|
|
369
|
+
else:
|
|
370
|
+
logger.warning(f" ⚠️ Файл не найден: {file_path}")
|
|
371
|
+
|
|
372
|
+
# Обрабатываем прямые файлы
|
|
373
|
+
for file_name in files_list:
|
|
374
|
+
try:
|
|
375
|
+
# Получаем путь к папке бота
|
|
376
|
+
config = get_global_var('config')
|
|
377
|
+
bot_id = config.BOT_ID if config else "unknown"
|
|
378
|
+
file_path = Path(f"bots/{bot_id}/files/{file_name}")
|
|
379
|
+
process_file(file_path)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
|
|
382
|
+
|
|
383
|
+
# Обрабатываем файлы из каталогов
|
|
384
|
+
for dir_name in directories_list:
|
|
385
|
+
# Получаем путь к каталогу относительно папки бота
|
|
386
|
+
config = get_global_var('config')
|
|
387
|
+
bot_id = config.BOT_ID if config else "unknown"
|
|
388
|
+
dir_path = Path(f"bots/{bot_id}/{dir_name}")
|
|
389
|
+
try:
|
|
390
|
+
if dir_path.is_dir():
|
|
391
|
+
for file_path in dir_path.iterdir():
|
|
392
|
+
try:
|
|
393
|
+
process_file(file_path, dir_path)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
|
|
396
|
+
else:
|
|
397
|
+
logger.warning(f" ⚠️ Каталог не найден: {dir_path}")
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error(f" ❌ Ошибка обработки каталога {dir_path}: {e}")
|
|
400
|
+
|
|
401
|
+
# Отправляем сообщение с медиа (если есть)
|
|
402
|
+
if media_files:
|
|
403
|
+
# Создаем медиа-группу с фото/видео и текстом
|
|
404
|
+
media_group = MediaGroupBuilder(caption=final_text)
|
|
405
|
+
for file_path, media_type in media_files:
|
|
406
|
+
if media_type == 'photo':
|
|
407
|
+
media_group.add_photo(media=FSInputFile(str(file_path)))
|
|
408
|
+
else: # video
|
|
409
|
+
media_group.add_video(media=FSInputFile(str(file_path)))
|
|
410
|
+
|
|
411
|
+
media = media_group.build()
|
|
412
|
+
if media:
|
|
413
|
+
result = await message.answer_media_group(media=media)
|
|
414
|
+
logger.info(f" ✅ Отправлено сообщение с {len(media)} медиафайлами")
|
|
415
|
+
else:
|
|
416
|
+
# Если нет медиа, отправляем просто текст
|
|
417
|
+
result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
|
|
418
|
+
logger.info(f" ✅ Отправлен текст сообщения")
|
|
419
|
+
|
|
420
|
+
# Отправляем документы отдельно (если есть)
|
|
421
|
+
if document_files:
|
|
422
|
+
doc_group = MediaGroupBuilder()
|
|
423
|
+
for file_path in document_files:
|
|
424
|
+
doc_group.add_document(media=FSInputFile(str(file_path)))
|
|
425
|
+
|
|
426
|
+
docs = doc_group.build()
|
|
427
|
+
if docs:
|
|
428
|
+
await message.answer_media_group(media=docs)
|
|
429
|
+
logger.info(f" ✅ Отправлена группа документов: {len(docs)} файлов")
|
|
430
|
+
|
|
431
|
+
return result
|
|
432
|
+
else:
|
|
433
|
+
# Если нет файлов, отправляем просто текст
|
|
434
|
+
logger.warning(" ⚠️ Нет файлов для отправки, отправляем как текст")
|
|
435
|
+
result = await message.answer(final_text, parse_mode=parse_mode, **kwargs)
|
|
436
|
+
return result
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"❌ ОШИБКА в send_message: {e}")
|
|
440
|
+
logger.exception("Полный стек ошибки send_message:")
|
|
441
|
+
|
|
442
|
+
# Пытаемся отправить простое сообщение без форматирования
|
|
443
|
+
try:
|
|
444
|
+
fallback_text = "Произошла ошибка при отправке ответа. Попробуйте еще раз."
|
|
445
|
+
result = await message.answer(fallback_text)
|
|
446
|
+
logger.info(f"✅ Запасное сообщение отправлено")
|
|
447
|
+
return result
|
|
448
|
+
except Exception as e2:
|
|
449
|
+
logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
|
|
450
|
+
raise
|
|
451
|
+
|
|
452
|
+
async def cleanup_expired_conversations():
|
|
453
|
+
"""Периодическая очистка просроченных диалогов"""
|
|
454
|
+
conversation_manager = get_global_var('conversation_manager')
|
|
455
|
+
|
|
456
|
+
while True:
|
|
457
|
+
try:
|
|
458
|
+
await asyncio.sleep(300) # каждые 5 минут
|
|
459
|
+
await conversation_manager.cleanup_expired_conversations()
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Ошибка очистки просроченных диалогов: {e}")
|
|
462
|
+
|
|
463
|
+
# 🆕 Вспомогательные функции для приветственного файла
|
|
464
|
+
|
|
465
|
+
async def get_welcome_file_path() -> str | None:
|
|
466
|
+
"""Возвращает путь к PDF файлу из папки WELCOME_FILE_DIR из конфига.
|
|
467
|
+
|
|
468
|
+
Источник настроек: configs/<bot_id>/.env (переменная WELCOME_FILE_DIR)
|
|
469
|
+
Рабочая директория уже установлена запускалкой на configs/<bot_id>.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
str | None: Путь к PDF файлу или None, если файл не найден
|
|
473
|
+
"""
|
|
474
|
+
config = get_global_var('config')
|
|
475
|
+
try:
|
|
476
|
+
folder_value = config.WELCOME_FILE_DIR
|
|
477
|
+
if not folder_value:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
folder = Path(folder_value)
|
|
481
|
+
if not folder.exists():
|
|
482
|
+
logger.info(f"Директория приветственных файлов не существует: {folder_value}")
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
if not folder.is_dir():
|
|
486
|
+
logger.info(f"Путь не является директорией: {folder_value}")
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
# Ищем первый PDF файл в директории
|
|
490
|
+
for path in folder.iterdir():
|
|
491
|
+
if path.is_file() and path.suffix.lower() == '.pdf':
|
|
492
|
+
return str(path)
|
|
493
|
+
|
|
494
|
+
logger.info(f"PDF файл не найден в директории: {folder_value}")
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.error(f"Ошибка при поиске приветственного файла: {e}")
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
async def get_welcome_msg_path() -> str | None:
|
|
502
|
+
"""Возвращает путь к файлу welcome_file_msg.txt из той же директории, где находится PDF файл.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
str | None: Путь к файлу с подписью или None, если файл не найден
|
|
506
|
+
"""
|
|
507
|
+
try:
|
|
508
|
+
pdf_path = await get_welcome_file_path()
|
|
509
|
+
if not pdf_path:
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
msg_path = str(Path(pdf_path).parent / 'welcome_file_msg.txt')
|
|
513
|
+
if not Path(msg_path).is_file():
|
|
514
|
+
logger.info(f"Файл подписи не найден: {msg_path}")
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
return msg_path
|
|
518
|
+
|
|
519
|
+
except Exception as e:
|
|
520
|
+
logger.error(f"Ошибка при поиске файла подписи: {e}")
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
async def send_welcome_file(message: Message) -> str:
|
|
524
|
+
"""
|
|
525
|
+
Отправляет приветственный файл с подписью из файла welcome_file_msg.txt.
|
|
526
|
+
Если файл подписи не найден, используется пустая подпись.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
str: текст подписи
|
|
530
|
+
"""
|
|
531
|
+
try:
|
|
532
|
+
config = get_global_var('config')
|
|
533
|
+
|
|
534
|
+
file_path = await get_welcome_file_path()
|
|
535
|
+
if not file_path:
|
|
536
|
+
return ""
|
|
537
|
+
|
|
538
|
+
# Получаем путь к файлу с подписью и читаем его
|
|
539
|
+
caption = ""
|
|
540
|
+
msg_path = await get_welcome_msg_path()
|
|
541
|
+
if msg_path:
|
|
542
|
+
try:
|
|
543
|
+
with open(msg_path, 'r', encoding='utf-8') as f:
|
|
544
|
+
caption = f.read().strip()
|
|
545
|
+
logger.info(f"Подпись загружена из файла: {msg_path}")
|
|
546
|
+
except Exception as e:
|
|
547
|
+
logger.error(f"Ошибка при чтении файла подписи {msg_path}: {e}")
|
|
548
|
+
|
|
549
|
+
parse_mode = config.MESSAGE_PARSE_MODE
|
|
550
|
+
document = FSInputFile(file_path)
|
|
551
|
+
|
|
552
|
+
await message.answer_document(document=document, caption=caption, parse_mode=parse_mode)
|
|
553
|
+
|
|
554
|
+
logger.info(f"Приветственный файл отправлен: {file_path}")
|
|
555
|
+
return caption
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.error(f"Ошибка при отправке приветственного файла: {e}")
|
|
558
|
+
return ""
|
|
559
|
+
|
|
560
|
+
# Общие команды
|
|
561
|
+
|
|
562
|
+
@utils_router.message(Command("help"))
|
|
563
|
+
async def help_handler(message: Message):
|
|
564
|
+
"""Справка"""
|
|
565
|
+
admin_manager = get_global_var('admin_manager')
|
|
566
|
+
prompt_loader = get_global_var('prompt_loader')
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
# Разная справка для админов и пользователей
|
|
570
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
571
|
+
if admin_manager.is_in_admin_mode(message.from_user.id):
|
|
572
|
+
help_text = """
|
|
573
|
+
👑 **Справка для администратора**
|
|
574
|
+
|
|
575
|
+
**Команды:**
|
|
576
|
+
• `/стат` - статистика воронки и событий
|
|
577
|
+
• `/история <user_id>` - история пользователя
|
|
578
|
+
• `/чат <user_id>` - начать диалог с пользователем
|
|
579
|
+
• `/чаты` - показать активные диалоги
|
|
580
|
+
• `/стоп` - завершить текущий диалог
|
|
581
|
+
• `/админ` - переключиться в режим пользователя
|
|
582
|
+
|
|
583
|
+
**Особенности:**
|
|
584
|
+
• Все сообщения пользователей к админу пересылаются
|
|
585
|
+
• Ваши сообщения отправляются пользователю как от бота
|
|
586
|
+
• Диалоги автоматически завершаются через 30 минут
|
|
587
|
+
"""
|
|
588
|
+
await message.answer(help_text, parse_mode='Markdown')
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
# Обычная справка для пользователей
|
|
592
|
+
help_text = await prompt_loader.load_help_message()
|
|
593
|
+
await send_message(message, help_text)
|
|
594
|
+
|
|
595
|
+
except Exception as e:
|
|
596
|
+
logger.error(f"Ошибка загрузки справки: {e}")
|
|
597
|
+
# Fallback справка
|
|
598
|
+
await send_message(message, "🤖 Ваш помощник готов к работе! Напишите /start для начала диалога.")
|
|
599
|
+
|
|
600
|
+
@utils_router.message(Command("status"))
|
|
601
|
+
async def status_handler(message: Message):
|
|
602
|
+
"""Проверка статуса системы"""
|
|
603
|
+
openai_client = get_global_var('openai_client')
|
|
604
|
+
prompt_loader = get_global_var('prompt_loader')
|
|
605
|
+
admin_manager = get_global_var('admin_manager')
|
|
606
|
+
config = get_global_var('config')
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# Проверяем OpenAI
|
|
610
|
+
openai_status = await openai_client.check_api_health()
|
|
611
|
+
|
|
612
|
+
# Проверяем промпты
|
|
613
|
+
prompts_status = await prompt_loader.validate_prompts()
|
|
614
|
+
|
|
615
|
+
# Статистика для админов
|
|
616
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
617
|
+
admin_stats = admin_manager.get_stats()
|
|
618
|
+
|
|
619
|
+
status_message = f"""
|
|
620
|
+
🔧 **Статус системы:**
|
|
621
|
+
|
|
622
|
+
OpenAI API: {'✅' if openai_status else '❌'}
|
|
623
|
+
Промпты: {'✅ ' + str(sum(prompts_status.values())) + '/' + str(len(prompts_status)) + ' загружено' if any(prompts_status.values()) else '❌'}
|
|
624
|
+
База данных: ✅ (соединение активно)
|
|
625
|
+
|
|
626
|
+
👑 **Админы:** {admin_stats['active_admins']}/{admin_stats['total_admins']} активны
|
|
627
|
+
🐛 **Режим отладки:** {'Включен' if config.DEBUG_MODE else 'Выключен'}
|
|
628
|
+
|
|
629
|
+
Все системы работают нормально!
|
|
630
|
+
"""
|
|
631
|
+
else:
|
|
632
|
+
status_message = f"""
|
|
633
|
+
🔧 **Статус системы:**
|
|
634
|
+
|
|
635
|
+
OpenAI API: {'✅' if openai_status else '❌'}
|
|
636
|
+
Промпты: {'✅ ' + str(sum(prompts_status.values())) + '/' + str(len(prompts_status)) + ' загружено' if any(prompts_status.values()) else '❌'}
|
|
637
|
+
База данных: ✅ (соединение активно)
|
|
638
|
+
|
|
639
|
+
Все системы работают нормально!
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
await send_message(message, status_message)
|
|
643
|
+
|
|
644
|
+
except Exception as e:
|
|
645
|
+
logger.error(f"Ошибка проверки статуса: {e}")
|
|
646
|
+
await send_message(message, "❌ Ошибка при проверке статуса системы")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def parse_utm_from_start_param(start_param: str) -> dict:
|
|
650
|
+
"""Парсит UTM-метки из start параметра в формате utmSource-vk_utmCampaign-summer2025
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
start_param: строка вида 'utmSource-vk_utmCampaign-summer2025' или полная ссылка
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
|
|
657
|
+
|
|
658
|
+
Examples:
|
|
659
|
+
>>> parse_utm_from_start_param('utmSource-vk_utmCampaign-summer2025')
|
|
660
|
+
{'utm_source': 'vk', 'utm_campaign': 'summer2025'}
|
|
661
|
+
|
|
662
|
+
>>> parse_utm_from_start_param('https://t.me/bot?start=utmSource-vk_utmCampaign-summer2025')
|
|
663
|
+
{'utm_source': 'vk', 'utm_campaign': 'summer2025'}
|
|
664
|
+
"""
|
|
665
|
+
import re
|
|
666
|
+
from urllib.parse import unquote
|
|
667
|
+
|
|
668
|
+
utm_data = {}
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
# Если это полная ссылка, извлекаем start параметр
|
|
672
|
+
if 't.me/' in start_param or 'https://' in start_param:
|
|
673
|
+
match = re.search(r'[?&]start=([^&]+)', start_param)
|
|
674
|
+
if match:
|
|
675
|
+
start_param = unquote(match.group(1))
|
|
676
|
+
else:
|
|
677
|
+
return {}
|
|
678
|
+
|
|
679
|
+
# Парсим формат: utmSource-vk_utmCampaign-summer2025
|
|
680
|
+
if '_' in start_param and '-' in start_param:
|
|
681
|
+
parts = start_param.split('_')
|
|
682
|
+
for part in parts:
|
|
683
|
+
if '-' in part:
|
|
684
|
+
key, value = part.split('-', 1)
|
|
685
|
+
# Преобразуем utmSource в utm_source
|
|
686
|
+
if key.startswith('utm'):
|
|
687
|
+
key = 'utm_' + key[3:].lower()
|
|
688
|
+
utm_data[key] = value
|
|
689
|
+
|
|
690
|
+
except Exception as e:
|
|
691
|
+
print(f"Ошибка парсинга UTM параметров: {e}")
|
|
692
|
+
|
|
693
|
+
return utm_data
|