fix: браузер закрывается на ВСЕХ путях (try/finally в open_response_page, кроме ручного режима; confirm_send закрывает при ошибке); статус меняется при ok=true даже без скриншота

This commit is contained in:
apuc committed 2026-09-22 11:46:50 +03:00
1 parent 8ca070ed70
commit 06c142b1ea
2 files changed
+102 -83

No files matched your search

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