fix: браузер закрывается на ВСЕХ путях (try/finally в open_response_page, кроме ручного режима; confirm_send закрывает при ошибке); статус меняется при ok=true даже без скриншота
This commit is contained in:
1 parent
8ca070ed70
commit
06c142b1ea
2 files changed
+102
-83
No files matched your search
+89
-79
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user