2025-06-27 09:57:34 +02:00
|
|
|
|
import logging
|
|
|
|
|
from typing import List, Dict, Optional
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
from ..config.settings import settings, AppConstants, UIFormatter
|
|
|
|
|
from ..config.logging_config import LoggingConfigurator
|
|
|
|
|
from ..models.vacancy import Vacancy, ApplicationResult
|
|
|
|
|
from ..services.hh_api_service import HHApiService
|
|
|
|
|
from ..services.gemini_service import GeminiAIService
|
|
|
|
|
from ..services.browser_service import BrowserService
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AutomationOrchestrator:
|
|
|
|
|
"""Оркестратор процесса автоматизации"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.api_service = HHApiService()
|
|
|
|
|
self.browser_service = BrowserService()
|
2025-06-28 17:58:36 +02:00
|
|
|
|
self.ai_service = None
|
|
|
|
|
|
|
|
|
|
def _get_ai_service(self):
|
|
|
|
|
if self.ai_service is None:
|
|
|
|
|
self.ai_service = GeminiAIService()
|
|
|
|
|
return self.ai_service
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
def execute_automation_pipeline(
|
|
|
|
|
self, keywords: Optional[str] = None, use_ai: bool = True
|
|
|
|
|
) -> Dict:
|
|
|
|
|
"""Выполнение полного пайплайна автоматизации"""
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
vacancies = self._search_and_filter_vacancies(keywords)
|
|
|
|
|
if not vacancies:
|
|
|
|
|
return {"error": "Подходящие вакансии не найдены"}
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
if use_ai and self._get_ai_service().is_available():
|
|
|
|
|
vacancies = self._ai_filter_vacancies(vacancies[:3])
|
2025-06-27 09:57:34 +02:00
|
|
|
|
if not vacancies:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
return {"error": "После AI фильтрации подходящих вакансий нет"}
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
init_ok = self._initialize_browser_and_auth()
|
|
|
|
|
if not init_ok:
|
2025-06-27 09:57:34 +02:00
|
|
|
|
return {"error": "Ошибка инициализации браузера или авторизации"}
|
|
|
|
|
|
|
|
|
|
application_results = self._apply_to_vacancies(vacancies)
|
|
|
|
|
|
|
|
|
|
return self._create_stats(application_results)
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
logger.info("Процесс остановлен пользователем")
|
|
|
|
|
return {"error": "Остановлено пользователем"}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Критическая ошибка: {e}")
|
|
|
|
|
return {"error": str(e)}
|
|
|
|
|
finally:
|
|
|
|
|
self._cleanup()
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
def _search_and_filter_vacancies(
|
|
|
|
|
self, keywords: Optional[str] = None
|
|
|
|
|
) -> List[Vacancy]:
|
2025-06-27 09:57:34 +02:00
|
|
|
|
"""Поиск и базовая фильтрация вакансий"""
|
|
|
|
|
logger.info("🔍 ЭТАП 1: Поиск вакансий")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
all_vacancies = self.api_service.search_vacancies(keywords)
|
|
|
|
|
if not all_vacancies:
|
|
|
|
|
logger.warning("Вакансии не найдены через API")
|
|
|
|
|
return []
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
suitable_vacancies = self.api_service.filter_suitable_vacancies(
|
|
|
|
|
all_vacancies, search_keywords=keywords or ""
|
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
self._log_search_results(all_vacancies, suitable_vacancies)
|
|
|
|
|
return suitable_vacancies
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка поиска вакансий: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
def _ai_filter_vacancies(self, vacancies: List[Vacancy]) -> List[Vacancy]:
|
|
|
|
|
"""AI фильтрация вакансий"""
|
|
|
|
|
logger.info("🤖 ЭТАП 2: AI анализ вакансий")
|
|
|
|
|
|
|
|
|
|
ai_suitable = []
|
|
|
|
|
total_count = len(vacancies)
|
|
|
|
|
|
|
|
|
|
for i, vacancy in enumerate(vacancies, 1):
|
|
|
|
|
truncated_name = UIFormatter.truncate_text(vacancy.name)
|
|
|
|
|
logger.info(f"Анализ {i}/{total_count}: {truncated_name}...")
|
|
|
|
|
|
|
|
|
|
try:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
if self._get_ai_service().should_apply(vacancy):
|
2025-06-27 09:57:34 +02:00
|
|
|
|
ai_suitable.append(vacancy)
|
|
|
|
|
logger.info("✅ Добавлено в список для отклика")
|
|
|
|
|
else:
|
|
|
|
|
logger.info("❌ Не рекомендуется")
|
|
|
|
|
|
|
|
|
|
if i < total_count:
|
|
|
|
|
time.sleep(AppConstants.AI_REQUEST_PAUSE)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка AI анализа: {e}")
|
|
|
|
|
ai_suitable.append(vacancy)
|
|
|
|
|
|
|
|
|
|
self._log_ai_results(total_count, ai_suitable)
|
|
|
|
|
return ai_suitable
|
|
|
|
|
|
|
|
|
|
def _initialize_browser_and_auth(self) -> bool:
|
|
|
|
|
"""Инициализация браузера и авторизация"""
|
|
|
|
|
logger.info("🌐 ЭТАП 3: Инициализация браузера и авторизация")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if not self.browser_service.initialize():
|
|
|
|
|
logger.error("Не удалось инициализировать браузер")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if not self.browser_service.authenticate_interactive():
|
|
|
|
|
logger.error("Не удалось авторизоваться")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
logger.info("✅ Браузер готов к работе")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка инициализации: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def _apply_to_vacancies(self, vacancies: List[Vacancy]) -> List[ApplicationResult]:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
max_successful_apps = settings.application.max_applications
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
2025-06-28 18:30:56 +02:00
|
|
|
|
logger.info(
|
|
|
|
|
f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)"
|
|
|
|
|
)
|
2025-06-28 17:58:36 +02:00
|
|
|
|
logger.info("💡 Между заявками добавляются паузы")
|
|
|
|
|
logger.info("💡 Лимит считается только по успешным заявкам")
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
application_results = []
|
2025-06-28 17:58:36 +02:00
|
|
|
|
successful_count = 0
|
|
|
|
|
processed_count = 0
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
for vacancy in vacancies:
|
|
|
|
|
if successful_count >= max_successful_apps:
|
2025-06-28 18:30:56 +02:00
|
|
|
|
logger.info(
|
|
|
|
|
f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}"
|
|
|
|
|
)
|
2025-06-28 17:58:36 +02:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
processed_count += 1
|
2025-06-27 09:57:34 +02:00
|
|
|
|
truncated_name = UIFormatter.truncate_text(vacancy.name, medium=True)
|
2025-06-28 17:58:36 +02:00
|
|
|
|
logger.info(
|
2025-06-28 18:30:56 +02:00
|
|
|
|
f"Обработка {processed_count}: {truncated_name} "
|
|
|
|
|
f"(успешных: {successful_count}/{max_successful_apps})"
|
2025-06-28 17:58:36 +02:00
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
try:
|
2025-06-28 18:30:56 +02:00
|
|
|
|
result = self.browser_service.apply_to_vacancy(vacancy)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
application_results.append(result)
|
|
|
|
|
self._log_application_result(result)
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
if result.success:
|
|
|
|
|
successful_count += 1
|
2025-06-28 18:30:56 +02:00
|
|
|
|
logger.info(
|
|
|
|
|
f" 🎉 Успешных заявок: "
|
|
|
|
|
f"{successful_count}/{max_successful_apps}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
processed_count < len(vacancies)
|
|
|
|
|
and successful_count < max_successful_apps
|
|
|
|
|
):
|
2025-06-27 09:57:34 +02:00
|
|
|
|
self.browser_service.add_random_pause()
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Неожиданная ошибка при подаче заявки: {e}")
|
|
|
|
|
error_result = ApplicationResult(
|
|
|
|
|
vacancy_id="",
|
|
|
|
|
vacancy_name=vacancy.name,
|
|
|
|
|
success=False,
|
|
|
|
|
error_message=str(e),
|
|
|
|
|
)
|
|
|
|
|
application_results.append(error_result)
|
|
|
|
|
|
2025-06-28 18:30:56 +02:00
|
|
|
|
logger.info(
|
|
|
|
|
f"🏁 Обработка завершена. Обработано вакансий: {processed_count}, "
|
|
|
|
|
f"успешных заявок: {successful_count}"
|
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
return application_results
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
def _log_search_results(
|
|
|
|
|
self, all_vacancies: List[Vacancy], suitable: List[Vacancy]
|
|
|
|
|
):
|
2025-06-27 09:57:34 +02:00
|
|
|
|
"""Логирование результатов поиска"""
|
|
|
|
|
logger.info("📊 Результат базовой фильтрации:")
|
|
|
|
|
logger.info(f" 🔍 Всего: {len(all_vacancies)}")
|
|
|
|
|
logger.info(f" ✅ Подходящих: {len(suitable)}")
|
|
|
|
|
if len(all_vacancies) > 0:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
percentage = UIFormatter.format_percentage(
|
|
|
|
|
len(suitable), len(all_vacancies)
|
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
logger.info(f" 📈 % соответствия: {percentage}")
|
|
|
|
|
|
|
|
|
|
def _log_ai_results(self, total_analyzed: int, ai_suitable: List[Vacancy]):
|
|
|
|
|
"""Логирование результатов AI анализа"""
|
|
|
|
|
logger.info("🎯 AI фильтрация завершена:")
|
|
|
|
|
logger.info(f" 🤖 Проанализировано: {total_analyzed}")
|
|
|
|
|
logger.info(f" ✅ Рекомендовано: {len(ai_suitable)}")
|
|
|
|
|
if total_analyzed > 0:
|
|
|
|
|
percentage = UIFormatter.format_percentage(len(ai_suitable), total_analyzed)
|
|
|
|
|
logger.info(f" 📈 % одобрения: {percentage}")
|
|
|
|
|
|
|
|
|
|
def _log_application_result(self, result: ApplicationResult):
|
|
|
|
|
"""Логирование результата подачи заявки"""
|
|
|
|
|
if result.success:
|
|
|
|
|
logger.info(" ✅ Заявка отправлена успешно")
|
|
|
|
|
elif result.already_applied:
|
|
|
|
|
logger.info(" ⚠️ Уже откликались ранее")
|
2025-06-28 17:58:36 +02:00
|
|
|
|
elif result.skipped:
|
|
|
|
|
logger.warning(f" ⏭️ Пропущено: {result.error_message}")
|
2025-06-27 09:57:34 +02:00
|
|
|
|
else:
|
|
|
|
|
logger.warning(f" ❌ Ошибка: {result.error_message}")
|
|
|
|
|
|
|
|
|
|
def _create_stats(self, application_results: List[ApplicationResult]) -> Dict:
|
|
|
|
|
"""Создание итоговой статистики"""
|
|
|
|
|
total_applications = len(application_results)
|
|
|
|
|
successful = sum(1 for r in application_results if r.success)
|
|
|
|
|
already_applied = sum(1 for r in application_results if r.already_applied)
|
2025-06-28 17:58:36 +02:00
|
|
|
|
skipped = sum(1 for r in application_results if r.skipped)
|
|
|
|
|
failed = total_applications - successful - already_applied - skipped
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"total_applications": total_applications,
|
|
|
|
|
"successful": successful,
|
|
|
|
|
"failed": failed,
|
|
|
|
|
"already_applied": already_applied,
|
2025-06-28 17:58:36 +02:00
|
|
|
|
"skipped": skipped,
|
2025-06-27 09:57:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _cleanup(self):
|
|
|
|
|
"""Очистка ресурсов"""
|
|
|
|
|
logger.info("🔒 Закрытие браузера...")
|
|
|
|
|
self.browser_service.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class JobApplicationManager:
|
|
|
|
|
"""Главный менеджер для управления процессом поиска и откликов на вакансии"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
LoggingConfigurator.setup_logging(
|
|
|
|
|
log_file="logs/hh_bot.log", console_output=True
|
|
|
|
|
)
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
self.orchestrator = AutomationOrchestrator()
|
|
|
|
|
self.application_results: List[ApplicationResult] = []
|
|
|
|
|
|
2025-06-28 17:58:36 +02:00
|
|
|
|
def run_automation(
|
|
|
|
|
self, keywords: Optional[str] = None, use_ai: bool = True
|
|
|
|
|
) -> Dict:
|
2025-06-27 09:57:34 +02:00
|
|
|
|
"""Запуск полного цикла автоматизации"""
|
|
|
|
|
print("🚀 Запуск автоматизации HH.ru")
|
|
|
|
|
print(UIFormatter.create_separator())
|
|
|
|
|
|
|
|
|
|
stats = self.orchestrator.execute_automation_pipeline(keywords, use_ai)
|
|
|
|
|
|
|
|
|
|
if "error" not in stats:
|
2025-06-28 17:58:36 +02:00
|
|
|
|
self.application_results = []
|
2025-06-27 09:57:34 +02:00
|
|
|
|
|
|
|
|
|
return stats
|
|
|
|
|
|
|
|
|
|
def get_application_results(self) -> List[ApplicationResult]:
|
|
|
|
|
"""Получение результатов подачи заявок"""
|
|
|
|
|
return self.application_results.copy()
|
|
|
|
|
|
|
|
|
|
def print_detailed_report(self, stats: Dict) -> None:
|
|
|
|
|
"""Детальный отчет о работе"""
|
|
|
|
|
UIFormatter.print_section_header("📊 ДЕТАЛЬНЫЙ ОТЧЕТ", long=True)
|
|
|
|
|
|
|
|
|
|
if "error" in stats:
|
|
|
|
|
print(f"❌ Ошибка выполнения: {stats['error']}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"📝 Всего попыток подачи заявок: {stats['total_applications']}")
|
|
|
|
|
print(f"✅ Успешно отправлено: {stats['successful']}")
|
|
|
|
|
print(f"⚠️ Уже откликались ранее: {stats['already_applied']}")
|
2025-06-28 17:58:36 +02:00
|
|
|
|
print(f"⏭️ Пропущено (тестовые/ошибки): {stats['skipped']}")
|
2025-06-27 09:57:34 +02:00
|
|
|
|
print(f"❌ Неудачных попыток: {stats['failed']}")
|
|
|
|
|
|
|
|
|
|
if stats["total_applications"] > 0:
|
|
|
|
|
success_rate = UIFormatter.format_percentage(
|
|
|
|
|
stats["successful"], stats["total_applications"]
|
|
|
|
|
)
|
|
|
|
|
print(f"📈 Успешность: {success_rate}")
|
|
|
|
|
|
|
|
|
|
print(UIFormatter.create_separator(long=True))
|
|
|
|
|
|
|
|
|
|
if stats["successful"] > 0:
|
|
|
|
|
print(f"🎉 Отлично! Отправлено {stats['successful']} новых заявок!")
|
|
|
|
|
print("💡 Рекомендуется запускать автоматизацию 2-3 раза в день")
|
|
|
|
|
elif stats["already_applied"] > 0:
|
|
|
|
|
print("💡 На большинство подходящих вакансий уже подавали заявки")
|
|
|
|
|
else:
|
|
|
|
|
print("😕 Новые заявки не были отправлены")
|
|
|
|
|
print("💡 Попробуйте изменить ключевые слова поиска или настройки")
|