diff --git a/hh_browser.py b/hh_browser.py index 97bc855..24dd0b9 100644 --- a/hh_browser.py +++ b/hh_browser.py @@ -237,7 +237,9 @@ def open_response_page(account_dir, user, account, vacancy, message, resume_id=None, screenshot_dir=None, submit=False): """Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст. При submit=True дополнительно жмёт «Отправить» и снимает скриншот результата. - Возвращает (ok, note, screenshot_path).""" + Возвращает (ok, note, screenshot_path). + Браузер закрывается ВСЕГДА, кроме ручного режима (submit=False, успех) — + там он нужен для confirm_send.""" key = (user, account) # Закрываем предыдущую страницу этого аккаунта prev = _pages.pop(key, None) @@ -249,98 +251,104 @@ def open_response_page(account_dir, user, account, vacancy, message, page = _new_page(account_dir, user, account) _pages[key] = page - page.goto(vacancy["url"], timeout=45000) - page.wait_for_timeout(2500) - - # Кнопка «Откликнуться» - clicked = False - for sel in RESPONSE_BUTTONS: - try: - btn = page.locator(sel).first - if btn.is_visible(timeout=3000): - btn.click() - clicked = True - break - except Exception: - continue - if not clicked: - shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - return False, "Не нашёл кнопку «Откликнуться» — проверьте скриншот", shot - - # Ждём модалку - page.wait_for_timeout(2000) - - # hh.ru прячет поле письма за переключателем «Сопроводительное письмо» — - # раскрываем его, иначе заполнится textarea вопроса скрининга (task_*) + keep_open = False try: - toggle = page.locator(LETTER_TOGGLE).first - if toggle.is_visible(timeout=2000): - toggle.click() - page.wait_for_timeout(800) - print(" ✓ Переключатель «Сопроводительное письмо» раскрыт") - except Exception: - pass + page.goto(vacancy["url"], timeout=45000) + page.wait_for_timeout(2500) - # Выбор резюме (радио-кнопка name="resumeId" value="{hash}") - if resume_id: - try: - radio = page.locator(f'input[name="resumeId"][value="{resume_id}"]').first - if radio.is_visible(timeout=3000): - radio.check(force=True) - print(f" ✓ Резюме {resume_id} выбрано") - except Exception: - print(" ⚠ Не нашёл радио-кнопку резюме — выберите вручную") - - # Вставляем текст - filled = False - for sel in MESSAGE_FIELDS: - try: - field = page.locator(sel).first - if field.is_visible(timeout=3000): - field.fill(message) - filled = True - break - except Exception: - continue - - if not filled: - shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - return False, "Не нашёл поле сообщения — проверьте скриншот", shot - - # Одно-кликовый режим: сразу жмём «Отправить» - if submit: - submitted = False - for sel in SUBMIT_BUTTONS: + # Кнопка «Откликнуться» + clicked = False + for sel in RESPONSE_BUTTONS: try: btn = page.locator(sel).first if btn.is_visible(timeout=3000): btn.click() - submitted = True + clicked = True break except Exception: continue - if not submitted: + if not clicked: shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - return False, "Не нашёл кнопку «Отправить» — проверьте скриншот", shot - # hh.ru иногда показывает вторую модалку подтверждения — жмём и её + return False, "Не нашёл кнопку «Откликнуться» — проверьте скриншот", shot + + # Ждём модалку + page.wait_for_timeout(2000) + + # hh.ru прячет поле письма за переключателем «Сопроводительное письмо» — + # раскрываем его, иначе заполнится textarea вопроса скрининга (task_*) try: - _click_second_confirm(page) + toggle = page.locator(LETTER_TOGGLE).first + if toggle.is_visible(timeout=2000): + toggle.click() + page.wait_for_timeout(800) + print(" ✓ Переключатель «Сопроводительное письмо» раскрыт") except Exception: pass - # Ждём подтверждение и снимаем скриншот результата. - # После отправки hh.ru может закрыть модалку/перезагрузить страницу — - # ловим любые ошибки, но браузер закрываем ВСЕГДА (finally). - try: - page.wait_for_timeout(2500) - shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - except Exception: - shot = None - finally: - _close_context(key) - return True, "Отклик отправлен", shot - shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot + # Выбор резюме (радио-кнопка name="resumeId" value="{hash}") + if resume_id: + try: + radio = page.locator(f'input[name="resumeId"][value="{resume_id}"]').first + if radio.is_visible(timeout=3000): + radio.check(force=True) + print(f" ✓ Резюме {resume_id} выбрано") + except Exception: + print(" ⚠ Не нашёл радио-кнопку резюме — выберите вручную") + + # Вставляем текст + filled = False + for sel in MESSAGE_FIELDS: + try: + field = page.locator(sel).first + if field.is_visible(timeout=3000): + field.fill(message) + filled = True + break + except Exception: + continue + + if not filled: + shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) + return False, "Не нашёл поле сообщения — проверьте скриншот", shot + + # Одно-кликовый режим: сразу жмём «Отправить» + if submit: + submitted = False + for sel in SUBMIT_BUTTONS: + try: + btn = page.locator(sel).first + if btn.is_visible(timeout=3000): + btn.click() + submitted = True + break + except Exception: + continue + if not submitted: + shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) + return False, "Не нашёл кнопку «Отправить» — проверьте скриншот", shot + # hh.ru иногда показывает вторую модалку подтверждения — жмём и её + try: + _click_second_confirm(page) + except Exception: + pass + # Ждём подтверждение и снимаем скриншот результата. + # После отправки hh.ru может закрыть модалку/перезагрузить страницу — + # ловим любые ошибки, но отклик уже отправлен. + try: + page.wait_for_timeout(2500) + shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) + except Exception: + shot = None + return True, "Отклик отправлен", shot + + # Ручной режим: браузер оставляем открытым для confirm_send + keep_open = True + shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) + return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot + finally: + # Браузер закрываем ВСЕГДА, кроме ручного режима (нужен для confirm_send) + if not keep_open: + _close_context(key) def confirm_send(user, account, vid): @@ -369,6 +377,8 @@ def confirm_send(user, account, vid): return True, "Кнопка «Отправить» нажата. Проверьте статус отклика на hh.ru." except Exception: continue + # Кнопка не найдена — браузер больше не нужен, закрываем + _close_context(key) return False, "Не нашёл кнопку «Отправить» — возможно, форма уже закрыта" diff --git a/templates/index.html b/templates/index.html index 82243fc..da0598f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -679,14 +679,19 @@ async function sendFromModal() { const data = await api(`/api/accounts/${currentAccount}/send-now`, { method: 'POST', body: JSON.stringify({ id: sendModalId, message }), }); - if (data.ok && data.screenshot) { - showShotModal(sendModalId, data.screenshot, data.note || 'Отклик отправлен', true); + if (data.ok) { + // Отклик отправлен — статус меняем всегда, даже если скриншота нет sentTexts.add(sendModalId); const v = vacancies.find(x => x.id === sendModalId); if (v) v.responded = true; renderVacancies(); renderTexts(); closeSendModal(); + if (data.screenshot) { + showShotModal(sendModalId, data.screenshot, data.note || 'Отклик отправлен', true); + } else { + alert(data.note || 'Отклик отправлен'); + } } else if (data.screenshot) { // Бэкенд вернул скриншот, но отклик НЕ отправлен (например, не нашёл поле) showShotModal(sendModalId, data.screenshot, data.note || 'Не удалось отправить отклик', false); @@ -743,13 +748,17 @@ async function sendVacancy(id) { method: 'POST', body: JSON.stringify({ id, message }), }); if (note) note.textContent = data.note || ''; - if (data.ok && data.screenshot) { - showShotModal(id, data.screenshot, data.note || 'Отклик отправлен', true); + if (data.ok) { sentTexts.add(id); const v = vacancies.find(x => x.id === id); if (v) v.responded = true; renderVacancies(); renderTexts(); + if (data.screenshot) { + showShotModal(id, data.screenshot, data.note || 'Отклик отправлен', true); + } else { + alert(data.note || 'Отклик отправлен'); + } } else if (data.screenshot) { showShotModal(id, data.screenshot, data.note || 'Не удалось отправить отклик', false); } else {