feat: add AI-powered cover letter generation and improve modal handling
- Add automatic cover letter generation using Gemini AI - Implement smart detection of "Add cover letter" button in modals - Generate personalized cover letters based on real resume data and vacancy text - Improve modal window detection with updated selectors for HH.ru - Fix code formatting to comply with black and flake8 standards - Add .flake8 configuration with modern standards (88 char limit) - Handle cover letter field detection and auto-filling - Graceful fallback to default letter if AI generation fails Features: - Smart cover letter button detection - Personalized content generation via Gemini AI - Robust error handling for cover letter functionality - Maintains existing functionality without cover letters
This commit is contained in:
parent
fd8da44b84
commit
5973d0f70e
|
@ -0,0 +1,11 @@
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 88
|
||||||
|
extend-ignore = E203, W503
|
||||||
|
exclude =
|
||||||
|
.git,
|
||||||
|
__pycache__,
|
||||||
|
.pytest_cache,
|
||||||
|
venv,
|
||||||
|
env,
|
||||||
|
.venv,
|
||||||
|
.env
|
|
@ -85,7 +85,7 @@ class CLIInterface:
|
||||||
print(f"📝 Всего заявок: {stats['total_applications']}")
|
print(f"📝 Всего заявок: {stats['total_applications']}")
|
||||||
print(f"✅ Успешных: {stats['successful']}")
|
print(f"✅ Успешных: {stats['successful']}")
|
||||||
print(f"❌ Неудачных: {stats['failed']}")
|
print(f"❌ Неудачных: {stats['failed']}")
|
||||||
|
|
||||||
if "skipped" in stats and stats["skipped"] > 0:
|
if "skipped" in stats and stats["skipped"] > 0:
|
||||||
print(f"⏭️ Пропущено: {stats['skipped']}")
|
print(f"⏭️ Пропущено: {stats['skipped']}")
|
||||||
|
|
||||||
|
|
|
@ -197,7 +197,7 @@ class Settings:
|
||||||
return bool(self.gemini.api_key)
|
return bool(self.gemini.api_key)
|
||||||
|
|
||||||
def get_exclude_keywords(self) -> list:
|
def get_exclude_keywords(self) -> list:
|
||||||
return ['стажер', 'cv']
|
return ["стажер", "cv"]
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
|
@ -131,7 +131,9 @@ class AutomationOrchestrator:
|
||||||
def _apply_to_vacancies(self, vacancies: List[Vacancy]) -> List[ApplicationResult]:
|
def _apply_to_vacancies(self, vacancies: List[Vacancy]) -> List[ApplicationResult]:
|
||||||
max_successful_apps = settings.application.max_applications
|
max_successful_apps = settings.application.max_applications
|
||||||
|
|
||||||
logger.info(f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)")
|
logger.info(
|
||||||
|
f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)"
|
||||||
|
)
|
||||||
logger.info("💡 Между заявками добавляются паузы")
|
logger.info("💡 Между заявками добавляются паузы")
|
||||||
logger.info("💡 Лимит считается только по успешным заявкам")
|
logger.info("💡 Лимит считается только по успешным заявкам")
|
||||||
|
|
||||||
|
@ -141,27 +143,34 @@ class AutomationOrchestrator:
|
||||||
|
|
||||||
for vacancy in vacancies:
|
for vacancy in vacancies:
|
||||||
if successful_count >= max_successful_apps:
|
if successful_count >= max_successful_apps:
|
||||||
logger.info(f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}")
|
logger.info(
|
||||||
|
f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
processed_count += 1
|
processed_count += 1
|
||||||
truncated_name = UIFormatter.truncate_text(vacancy.name, medium=True)
|
truncated_name = UIFormatter.truncate_text(vacancy.name, medium=True)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Обработка {processed_count}: {truncated_name} (успешных: {successful_count}/{max_successful_apps})"
|
f"Обработка {processed_count}: {truncated_name} "
|
||||||
|
f"(успешных: {successful_count}/{max_successful_apps})"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.browser_service.apply_to_vacancy(
|
result = self.browser_service.apply_to_vacancy(vacancy)
|
||||||
vacancy.alternate_url, vacancy.name
|
|
||||||
)
|
|
||||||
application_results.append(result)
|
application_results.append(result)
|
||||||
self._log_application_result(result)
|
self._log_application_result(result)
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
successful_count += 1
|
successful_count += 1
|
||||||
logger.info(f" 🎉 Успешных заявок: {successful_count}/{max_successful_apps}")
|
logger.info(
|
||||||
|
f" 🎉 Успешных заявок: "
|
||||||
|
f"{successful_count}/{max_successful_apps}"
|
||||||
|
)
|
||||||
|
|
||||||
if processed_count < len(vacancies) and successful_count < max_successful_apps:
|
if (
|
||||||
|
processed_count < len(vacancies)
|
||||||
|
and successful_count < max_successful_apps
|
||||||
|
):
|
||||||
self.browser_service.add_random_pause()
|
self.browser_service.add_random_pause()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -174,7 +183,10 @@ class AutomationOrchestrator:
|
||||||
)
|
)
|
||||||
application_results.append(error_result)
|
application_results.append(error_result)
|
||||||
|
|
||||||
logger.info(f"🏁 Обработка завершена. Обработано вакансий: {processed_count}, успешных заявок: {successful_count}")
|
logger.info(
|
||||||
|
f"🏁 Обработка завершена. Обработано вакансий: {processed_count}, "
|
||||||
|
f"успешных заявок: {successful_count}"
|
||||||
|
)
|
||||||
return application_results
|
return application_results
|
||||||
|
|
||||||
def _log_search_results(
|
def _log_search_results(
|
||||||
|
|
|
@ -15,13 +15,15 @@ from selenium.common.exceptions import NoSuchElementException
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
|
||||||
from ..config.settings import settings, AppConstants, UIFormatter
|
from ..config.settings import settings, AppConstants, UIFormatter
|
||||||
from ..models.vacancy import ApplicationResult
|
from ..models.vacancy import ApplicationResult, Vacancy
|
||||||
|
|
||||||
|
|
||||||
class SubmissionResult(Enum):
|
class SubmissionResult(Enum):
|
||||||
SUCCESS = "success"
|
SUCCESS = "success"
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
SKIPPED = "skipped"
|
SKIPPED = "skipped"
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ class SessionManager:
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(self.cookies_file, 'w', encoding='utf-8') as f:
|
with open(self.cookies_file, "w", encoding="utf-8") as f:
|
||||||
json.dump(session_data, f, indent=2, ensure_ascii=False)
|
json.dump(session_data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
logger.info(f"Сессия сохранена в {self.cookies_file}")
|
logger.info(f"Сессия сохранена в {self.cookies_file}")
|
||||||
|
@ -61,7 +63,7 @@ class SessionManager:
|
||||||
logger.info("Файл сессии не найден")
|
logger.info("Файл сессии не найден")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with open(self.cookies_file, 'r', encoding='utf-8') as f:
|
with open(self.cookies_file, "r", encoding="utf-8") as f:
|
||||||
session_data = json.load(f)
|
session_data = json.load(f)
|
||||||
|
|
||||||
if not self._is_session_valid(session_data):
|
if not self._is_session_valid(session_data):
|
||||||
|
@ -167,11 +169,11 @@ class AuthenticationHandler:
|
||||||
try:
|
try:
|
||||||
print("\n🔐 ПРОВЕРКА СОХРАНЕННОЙ СЕССИИ")
|
print("\n🔐 ПРОВЕРКА СОХРАНЕННОЙ СЕССИИ")
|
||||||
logger.info("Проверяем сохраненную сессию...")
|
logger.info("Проверяем сохраненную сессию...")
|
||||||
|
|
||||||
if self.session_manager.load_session():
|
if self.session_manager.load_session():
|
||||||
self.driver.refresh()
|
self.driver.refresh()
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
if self._check_authentication():
|
if self._check_authentication():
|
||||||
print("✅ Использована сохраненная сессия")
|
print("✅ Использована сохраненная сессия")
|
||||||
logger.info("Авторизация через сохраненную сессию успешна!")
|
logger.info("Авторизация через сохраненную сессию успешна!")
|
||||||
|
@ -193,12 +195,12 @@ class AuthenticationHandler:
|
||||||
|
|
||||||
if self._check_authentication():
|
if self._check_authentication():
|
||||||
logger.info("Авторизация успешна!")
|
logger.info("Авторизация успешна!")
|
||||||
|
|
||||||
if self.session_manager.save_session():
|
if self.session_manager.save_session():
|
||||||
print("✅ Сессия сохранена для следующих запусков")
|
print("✅ Сессия сохранена для следующих запусков")
|
||||||
else:
|
else:
|
||||||
print("⚠️ Не удалось сохранить сессию")
|
print("⚠️ Не удалось сохранить сессию")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error("Авторизация не завершена")
|
logger.error("Авторизация не завершена")
|
||||||
|
@ -250,21 +252,19 @@ class VacancyApplicator:
|
||||||
def __init__(self, driver: webdriver.Chrome):
|
def __init__(self, driver: webdriver.Chrome):
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
|
|
||||||
def apply_to_vacancy(
|
def apply_to_vacancy(self, vacancy: Vacancy) -> ApplicationResult:
|
||||||
self, vacancy_url: str, vacancy_name: str
|
|
||||||
) -> ApplicationResult:
|
|
||||||
"""Подача заявки на вакансию"""
|
"""Подача заявки на вакансию"""
|
||||||
try:
|
try:
|
||||||
truncated_name = UIFormatter.truncate_text(vacancy_name)
|
truncated_name = UIFormatter.truncate_text(vacancy.name)
|
||||||
logger.info(f"Переход к вакансии: {truncated_name}...")
|
logger.info(f"Переход к вакансии: {truncated_name}...")
|
||||||
self.driver.get(vacancy_url)
|
self.driver.get(vacancy.alternate_url)
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
apply_button = self._find_apply_button()
|
apply_button = self._find_apply_button()
|
||||||
if not apply_button:
|
if not apply_button:
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="",
|
vacancy_id="",
|
||||||
vacancy_name=vacancy_name,
|
vacancy_name=vacancy.name,
|
||||||
success=False,
|
success=False,
|
||||||
error_message="Кнопка отклика не найдена",
|
error_message="Кнопка отклика не найдена",
|
||||||
)
|
)
|
||||||
|
@ -273,7 +273,7 @@ class VacancyApplicator:
|
||||||
if self._is_already_applied(button_text):
|
if self._is_already_applied(button_text):
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="",
|
vacancy_id="",
|
||||||
vacancy_name=vacancy_name,
|
vacancy_name=vacancy.name,
|
||||||
success=False,
|
success=False,
|
||||||
already_applied=True,
|
already_applied=True,
|
||||||
error_message="Уже откликались на эту вакансию",
|
error_message="Уже откликались на эту вакансию",
|
||||||
|
@ -283,28 +283,30 @@ class VacancyApplicator:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
logger.info("Кнопка отклика нажата, ищем форму заявки...")
|
logger.info("Кнопка отклика нажата, ищем форму заявки...")
|
||||||
|
|
||||||
submit_result = self._submit_application_form()
|
submit_result = self._submit_application_form(vacancy)
|
||||||
|
|
||||||
if submit_result == SubmissionResult.SUCCESS:
|
if submit_result == SubmissionResult.SUCCESS:
|
||||||
logger.info("✅ Заявка успешно отправлена")
|
logger.info("✅ Заявка успешно отправлена")
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="", vacancy_name=vacancy_name, success=True
|
vacancy_id="", vacancy_name=vacancy.name, success=True
|
||||||
)
|
)
|
||||||
elif submit_result == SubmissionResult.SKIPPED:
|
elif submit_result == SubmissionResult.SKIPPED:
|
||||||
logger.warning("⚠️ Вакансия пропущена (нет модального окна)")
|
logger.warning("⚠️ Вакансия пропущена (нет модального окна)")
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="",
|
vacancy_id="",
|
||||||
vacancy_name=vacancy_name,
|
vacancy_name=vacancy.name,
|
||||||
success=False,
|
success=False,
|
||||||
skipped=True,
|
skipped=True,
|
||||||
error_message="Модальное окно не найдено (возможно тестовая вакансия)",
|
error_message=(
|
||||||
|
"Модальное окно не найдено " "(возможно тестовая вакансия)"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else: # FAILED
|
else: # FAILED
|
||||||
logger.warning("❌ Не удалось отправить заявку в модальном окне")
|
logger.warning("❌ Не удалось отправить заявку в модальном окне")
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="",
|
vacancy_id="",
|
||||||
vacancy_name=vacancy_name,
|
vacancy_name=vacancy.name,
|
||||||
success=False,
|
success=False,
|
||||||
error_message="Ошибка отправки в модальном окне",
|
error_message="Ошибка отправки в модальном окне",
|
||||||
)
|
)
|
||||||
|
@ -313,7 +315,7 @@ class VacancyApplicator:
|
||||||
logger.error(f"Ошибка при подаче заявки: {e}")
|
logger.error(f"Ошибка при подаче заявки: {e}")
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="",
|
vacancy_id="",
|
||||||
vacancy_name=vacancy_name,
|
vacancy_name=vacancy.name,
|
||||||
success=False,
|
success=False,
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
@ -334,19 +336,19 @@ class VacancyApplicator:
|
||||||
indicator in button_text for indicator in self.ALREADY_APPLIED_INDICATORS
|
indicator in button_text for indicator in self.ALREADY_APPLIED_INDICATORS
|
||||||
)
|
)
|
||||||
|
|
||||||
def _submit_application_form(self) -> SubmissionResult:
|
def _submit_application_form(self, vacancy: Vacancy) -> SubmissionResult:
|
||||||
"""Отправка заявки в модальном окне"""
|
"""Отправка заявки в модальном окне"""
|
||||||
try:
|
try:
|
||||||
modal_selectors = [
|
modal_selectors = [
|
||||||
'[data-qa="modal-overlay"]',
|
'[data-qa="modal-overlay"]',
|
||||||
'.magritte-modal-overlay',
|
".magritte-modal-overlay",
|
||||||
'[data-qa="modal"]',
|
'[data-qa="modal"]',
|
||||||
'[data-qa="vacancy-response-popup"]',
|
'[data-qa="vacancy-response-popup"]',
|
||||||
'.vacancy-response-popup',
|
".vacancy-response-popup",
|
||||||
'.modal',
|
".modal",
|
||||||
'.bloko-modal',
|
".bloko-modal",
|
||||||
]
|
]
|
||||||
|
|
||||||
submit_selectors = [
|
submit_selectors = [
|
||||||
'[data-qa="vacancy-response-submit-popup"]',
|
'[data-qa="vacancy-response-submit-popup"]',
|
||||||
'button[form="RESPONSE_MODAL_FORM_ID"]',
|
'button[form="RESPONSE_MODAL_FORM_ID"]',
|
||||||
|
@ -372,7 +374,10 @@ class VacancyApplicator:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not modal_found:
|
if not modal_found:
|
||||||
logger.warning("⚠️ Модальное окно не найдено - пропускаем вакансию (возможно тестовая или ошибка)")
|
logger.warning(
|
||||||
|
"⚠️ Модальное окно не найдено - пропускаем вакансию "
|
||||||
|
"(возможно тестовая или ошибка)"
|
||||||
|
)
|
||||||
return SubmissionResult.SKIPPED
|
return SubmissionResult.SKIPPED
|
||||||
|
|
||||||
form_selectors = [
|
form_selectors = [
|
||||||
|
@ -380,7 +385,7 @@ class VacancyApplicator:
|
||||||
'form[id="RESPONSE_MODAL_FORM_ID"]',
|
'form[id="RESPONSE_MODAL_FORM_ID"]',
|
||||||
'form[data-qa*="response"]',
|
'form[data-qa*="response"]',
|
||||||
]
|
]
|
||||||
|
|
||||||
form_found = False
|
form_found = False
|
||||||
for form_selector in form_selectors:
|
for form_selector in form_selectors:
|
||||||
try:
|
try:
|
||||||
|
@ -391,11 +396,12 @@ class VacancyApplicator:
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not form_found:
|
if not form_found:
|
||||||
logger.warning("Форма отклика не найдена в модальном окне - пропускаем")
|
logger.warning("Форма отклика не найдена в модальном окне - пропускаем")
|
||||||
return SubmissionResult.SKIPPED
|
return SubmissionResult.SKIPPED
|
||||||
|
|
||||||
|
self._add_cover_letter_if_possible(vacancy)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
for selector in submit_selectors:
|
for selector in submit_selectors:
|
||||||
|
@ -404,8 +410,13 @@ class VacancyApplicator:
|
||||||
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
||||||
)
|
)
|
||||||
if submit_button:
|
if submit_button:
|
||||||
logger.info(f"Нажимаем кнопку отправки: {submit_button.text.strip()}")
|
logger.info(
|
||||||
self.driver.execute_script("arguments[0].click();", submit_button)
|
f"Нажимаем кнопку отправки: "
|
||||||
|
f"{submit_button.text.strip()}"
|
||||||
|
)
|
||||||
|
self.driver.execute_script(
|
||||||
|
"arguments[0].click();", submit_button
|
||||||
|
)
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
if self._check_success_message():
|
if self._check_success_message():
|
||||||
return SubmissionResult.SUCCESS
|
return SubmissionResult.SUCCESS
|
||||||
|
@ -421,12 +432,93 @@ class VacancyApplicator:
|
||||||
logger.error(f"Ошибка в модальном окне: {e}")
|
logger.error(f"Ошибка в модальном окне: {e}")
|
||||||
return SubmissionResult.FAILED
|
return SubmissionResult.FAILED
|
||||||
|
|
||||||
|
def _add_cover_letter_if_possible(self, vacancy: Vacancy) -> None:
|
||||||
|
"""Добавление сопроводительного письма если возможно"""
|
||||||
|
try:
|
||||||
|
cover_letter_button_selectors = [
|
||||||
|
'[data-qa="add-cover-letter"]',
|
||||||
|
'button[data-qa*="cover-letter"]',
|
||||||
|
'button:contains("Добавить сопроводительное")',
|
||||||
|
'button:contains("сопроводительное")',
|
||||||
|
]
|
||||||
|
|
||||||
|
cover_letter_button = None
|
||||||
|
for selector in cover_letter_button_selectors:
|
||||||
|
try:
|
||||||
|
if selector.startswith("button:contains"):
|
||||||
|
buttons = self.driver.find_elements(By.TAG_NAME, "button")
|
||||||
|
text_to_find = selector.split('"')[1].lower()
|
||||||
|
for button in buttons:
|
||||||
|
if text_to_find in button.text.lower():
|
||||||
|
cover_letter_button = button
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
cover_letter_button = self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, selector
|
||||||
|
)
|
||||||
|
|
||||||
|
if cover_letter_button:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not cover_letter_button:
|
||||||
|
logger.info("Кнопка сопроводительного письма не найдена")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Найдена кнопка сопроводительного письма, нажимаем...")
|
||||||
|
self.driver.execute_script("arguments[0].click();", cover_letter_button)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
cover_letter_field_selectors = [
|
||||||
|
'textarea[data-qa*="cover-letter"]',
|
||||||
|
'textarea[name*="letter"]',
|
||||||
|
'textarea[placeholder*="письм"]',
|
||||||
|
'textarea[id*="letter"]',
|
||||||
|
".modal textarea",
|
||||||
|
"form textarea",
|
||||||
|
]
|
||||||
|
|
||||||
|
cover_letter_field = None
|
||||||
|
for selector in cover_letter_field_selectors:
|
||||||
|
try:
|
||||||
|
cover_letter_field = WebDriverWait(self.driver, 3).until(
|
||||||
|
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
||||||
|
)
|
||||||
|
if cover_letter_field:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not cover_letter_field:
|
||||||
|
logger.warning("Поле для сопроводительного письма не найдено")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Генерация сопроводительного письма...")
|
||||||
|
|
||||||
|
from ..services.gemini_service import GeminiAIService
|
||||||
|
|
||||||
|
gemini_service = GeminiAIService()
|
||||||
|
cover_letter_text = gemini_service.generate_cover_letter(vacancy)
|
||||||
|
|
||||||
|
if cover_letter_text:
|
||||||
|
logger.info("Заполняем сопроводительное письмо...")
|
||||||
|
cover_letter_field.clear()
|
||||||
|
cover_letter_field.send_keys(cover_letter_text)
|
||||||
|
logger.info("✅ Сопроводительное письмо добавлено")
|
||||||
|
else:
|
||||||
|
logger.warning("Не удалось сгенерировать сопроводительное письмо")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Ошибка при добавлении сопроводительного письма: {e}")
|
||||||
|
logger.info("Продолжаем без сопроводительного письма")
|
||||||
|
|
||||||
def _check_success_message(self) -> bool:
|
def _check_success_message(self) -> bool:
|
||||||
"""Проверка успешной отправки заявки"""
|
"""Проверка успешной отправки заявки"""
|
||||||
try:
|
try:
|
||||||
success_indicators = [
|
success_indicators = [
|
||||||
"отклик отправлен",
|
"отклик отправлен",
|
||||||
"заявка отправлена",
|
"заявка отправлена",
|
||||||
"успешно отправлено",
|
"успешно отправлено",
|
||||||
"спасибо за отклик",
|
"спасибо за отклик",
|
||||||
"ваш отклик получен",
|
"ваш отклик получен",
|
||||||
|
@ -435,50 +527,59 @@ class VacancyApplicator:
|
||||||
"резюме отправлено",
|
"резюме отправлено",
|
||||||
"откликнулись на вакансию",
|
"откликнулись на вакансию",
|
||||||
]
|
]
|
||||||
|
|
||||||
success_selectors = [
|
success_selectors = [
|
||||||
'[data-qa*="success"]',
|
'[data-qa*="success"]',
|
||||||
'[data-qa*="sent"]',
|
'[data-qa*="sent"]',
|
||||||
'.success-message',
|
".success-message",
|
||||||
'.response-sent',
|
".response-sent",
|
||||||
'[class*="success"]',
|
'[class*="success"]',
|
||||||
]
|
]
|
||||||
|
|
||||||
for selector in success_selectors:
|
for selector in success_selectors:
|
||||||
try:
|
try:
|
||||||
success_element = self.driver.find_element(By.CSS_SELECTOR, selector)
|
success_element = self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, selector
|
||||||
|
)
|
||||||
if success_element and success_element.is_displayed():
|
if success_element and success_element.is_displayed():
|
||||||
logger.info(f"Найден элемент успеха: {selector} - {success_element.text}")
|
logger.info(
|
||||||
|
f"Найден элемент успеха: {selector} - "
|
||||||
|
f"{success_element.text}"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
page_text = self.driver.page_source.lower()
|
page_text = self.driver.page_source.lower()
|
||||||
|
|
||||||
for indicator in success_indicators:
|
for indicator in success_indicators:
|
||||||
if indicator in page_text:
|
if indicator in page_text:
|
||||||
logger.info(f"Найдено подтверждение: '{indicator}'")
|
logger.info(f"Найдено подтверждение: '{indicator}'")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
current_url = self.driver.current_url
|
current_url = self.driver.current_url
|
||||||
if "sent" in current_url or "success" in current_url or "response" in current_url:
|
if (
|
||||||
|
"sent" in current_url
|
||||||
|
or "success" in current_url
|
||||||
|
or "response" in current_url
|
||||||
|
):
|
||||||
logger.info(f"URL указывает на успешную отправку: {current_url}")
|
logger.info(f"URL указывает на успешную отправку: {current_url}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
modal_disappeared = True
|
modal_disappeared = True
|
||||||
try:
|
try:
|
||||||
self.driver.find_element(By.CSS_SELECTOR, '[data-qa="modal-overlay"]')
|
self.driver.find_element(By.CSS_SELECTOR, '[data-qa="modal-overlay"]')
|
||||||
modal_disappeared = False
|
modal_disappeared = False
|
||||||
except NoSuchElementException:
|
except NoSuchElementException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if modal_disappeared:
|
if modal_disappeared:
|
||||||
logger.info("Модальное окно исчезло - возможно отклик отправлен")
|
logger.info("Модальное окно исчезло - возможно отклик отправлен")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.info("Подтверждение отправки не найдено")
|
logger.info("Подтверждение отправки не найдено")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка проверки успеха: {e}")
|
logger.error(f"Ошибка проверки успеха: {e}")
|
||||||
return False
|
return False
|
||||||
|
@ -531,19 +632,17 @@ class BrowserService:
|
||||||
self._is_authenticated = True
|
self._is_authenticated = True
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def apply_to_vacancy(
|
def apply_to_vacancy(self, vacancy: Vacancy) -> ApplicationResult:
|
||||||
self, vacancy_url: str, vacancy_name: str
|
|
||||||
) -> ApplicationResult:
|
|
||||||
"""Подача заявки на вакансию"""
|
"""Подача заявки на вакансию"""
|
||||||
if not self.is_ready():
|
if not self.is_ready():
|
||||||
return ApplicationResult(
|
return ApplicationResult(
|
||||||
vacancy_id="",
|
vacancy_id="",
|
||||||
vacancy_name=vacancy_name,
|
vacancy_name=vacancy.name,
|
||||||
success=False,
|
success=False,
|
||||||
error_message="Браузер не готов или нет авторизации",
|
error_message="Браузер не готов или нет авторизации",
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.applicator.apply_to_vacancy(vacancy_url, vacancy_name)
|
return self.applicator.apply_to_vacancy(vacancy)
|
||||||
|
|
||||||
def add_random_pause(self) -> None:
|
def add_random_pause(self) -> None:
|
||||||
"""Случайная пауза между действиями"""
|
"""Случайная пауза между действиями"""
|
||||||
|
|
|
@ -84,8 +84,14 @@ class GeminiApiClient:
|
||||||
json_str = content[json_start:json_end]
|
json_str = content[json_start:json_end]
|
||||||
parsed_response = json.loads(json_str)
|
parsed_response = json.loads(json_str)
|
||||||
|
|
||||||
score = parsed_response.get("match_score", 0)
|
if "match_score" in parsed_response:
|
||||||
logger.info(f"Gemini анализ завершен: {score}")
|
score = parsed_response.get("match_score", 0)
|
||||||
|
logger.info(f"Gemini анализ завершен: {score}")
|
||||||
|
elif "cover_letter" in parsed_response:
|
||||||
|
logger.info("Gemini сгенерировал сопроводительное письмо")
|
||||||
|
else:
|
||||||
|
logger.info("Получен ответ от Gemini")
|
||||||
|
|
||||||
return parsed_response
|
return parsed_response
|
||||||
else:
|
else:
|
||||||
logger.error("JSON не найден в ответе Gemini")
|
logger.error("JSON не найден в ответе Gemini")
|
||||||
|
@ -330,3 +336,88 @@ class GeminiAIService:
|
||||||
return score >= settings.gemini.match_threshold
|
return score >= settings.gemini.match_threshold
|
||||||
|
|
||||||
return self.analyzer.should_apply(vacancy)
|
return self.analyzer.should_apply(vacancy)
|
||||||
|
|
||||||
|
def generate_cover_letter(self, vacancy: Vacancy) -> Optional[str]:
|
||||||
|
"""Генерация сопроводительного письма для вакансии"""
|
||||||
|
if not self.is_available():
|
||||||
|
logger.warning("Gemini API недоступен, используем базовое письмо")
|
||||||
|
return self._get_default_cover_letter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
resume_data = self.resume_loader.load()
|
||||||
|
vacancy_text = self._get_vacancy_full_text(vacancy)
|
||||||
|
|
||||||
|
experience_text = resume_data.get("experience", "")
|
||||||
|
about_me_text = resume_data.get("about_me", "")
|
||||||
|
skills_text = resume_data.get("skills", "")
|
||||||
|
|
||||||
|
my_profile = f"""
|
||||||
|
Опыт работы:
|
||||||
|
{experience_text}
|
||||||
|
|
||||||
|
О себе:
|
||||||
|
{about_me_text}
|
||||||
|
|
||||||
|
Навыки и технологии:
|
||||||
|
{skills_text}
|
||||||
|
"""
|
||||||
|
|
||||||
|
prompt_text = (
|
||||||
|
"Напиши короткое, человечное и честное сопроводительное письмо "
|
||||||
|
"для отклика на вакансию на русском языке. Не придумывай опыт, "
|
||||||
|
"которого нет. Используй только мой реальный опыт и навыки ниже. "
|
||||||
|
"Пиши по делу, дружелюбно и без официоза. Не делай письмо слишком "
|
||||||
|
"длинным. Всегда заканчивай строкой «Telegram — @itqen»."
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = f"""{prompt_text}
|
||||||
|
|
||||||
|
**Верни только JSON с ключом "cover_letter", без других пояснений.**
|
||||||
|
|
||||||
|
Пример формата вывода:
|
||||||
|
{{"cover_letter": "текст письма здесь"}}
|
||||||
|
|
||||||
|
**Вот мой опыт:**
|
||||||
|
{my_profile}
|
||||||
|
|
||||||
|
**Вот текст вакансии:**
|
||||||
|
{vacancy_text}"""
|
||||||
|
|
||||||
|
logger.info("Генерация сопроводительного письма через Gemini")
|
||||||
|
response = self.api_client.generate_content(prompt)
|
||||||
|
|
||||||
|
if response and "cover_letter" in response:
|
||||||
|
cover_letter = response["cover_letter"]
|
||||||
|
logger.info("Сопроводительное письмо сгенерировано")
|
||||||
|
return cover_letter
|
||||||
|
else:
|
||||||
|
logger.error("Не удалось получить сопроводительное письмо от Gemini")
|
||||||
|
return self._get_default_cover_letter()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка генерации сопроводительного письма: {e}")
|
||||||
|
return self._get_default_cover_letter()
|
||||||
|
|
||||||
|
def _get_vacancy_full_text(self, vacancy: Vacancy) -> str:
|
||||||
|
"""Получение полного текста вакансии"""
|
||||||
|
parts = [
|
||||||
|
f"Название: {vacancy.name}",
|
||||||
|
f"Компания: {vacancy.employer.name}",
|
||||||
|
]
|
||||||
|
|
||||||
|
if vacancy.snippet.requirement:
|
||||||
|
parts.append(f"Требования: {vacancy.snippet.requirement}")
|
||||||
|
|
||||||
|
if vacancy.snippet.responsibility:
|
||||||
|
parts.append(f"Обязанности: {vacancy.snippet.responsibility}")
|
||||||
|
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
def _get_default_cover_letter(self) -> str:
|
||||||
|
"""Базовое сопроводительное письмо на случай ошибки"""
|
||||||
|
return """Добрый день!
|
||||||
|
|
||||||
|
Заинтересован в данной вакансии. Готов обсудить детали и возможности сотрудничества.
|
||||||
|
|
||||||
|
С уважением,
|
||||||
|
Telegram — @itqen"""
|
||||||
|
|
Loading…
Reference in New Issue