diff --git a/README.md b/README.md index 917e037..5320467 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Автоотклик на hh.ru (полуавтоматический, безопасный) +# Автоотклик на hh.ru (полуавтоматический) Скрипт ищет вакансии на hh.ru по ключевым словам, генерирует персонализированный текст отклика через ИИ (на основе вашего резюме и описания вакансии), открывает -страницу в браузере и подставляет текст. **Кнопку «Отправить» всегда нажимаете вы** — -это не нарушает правила hh.ru и не рискует аккаунтом. +страницу в браузере, подставляет текст и резюме. Отправка — в один клик +(«Отправить отклик»), либо с проверкой текста перед отправкой. ## Установка @@ -25,17 +25,30 @@ PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright .venv/bin .venv/bin/python webui.py ``` -Откройте **http://localhost:5000** — там можно: +Откройте **http://localhost:18091** (порт меняется через `PORT`) — там можно: +- регистрироваться и входить (открытая регистрация, отключается `ALLOW_REGISTRATION=0`) - создавать аккаунты (имя, регион, поисковые запросы, стиль) - загружать резюме файлом (**txt, md, docx, pdf**) - сохранять **ключ сессии hh.ru** (cookies из браузера) — нужен для поиска и списка резюме - выбирать **резюме на hh.ru** — оно автоматически подставляется в модалке отклика - искать вакансии и видеть их списком (название, компания, зарплата, опыт) - генерировать тексты откликов через ИИ (редактируются перед отправкой) +- отправлять отклик в один клик: кнопка «Отправить отклик» в колонке действий — + браузер открывает вакансию, вставляет текст, жмёт «Отправить» и закрывается + (скриншот результата показывается в модалке) - открывать вакансию в браузере с уже вставленным текстом и выбранным резюме + для ручной проверки перед отправкой - отмечать отправленные — они больше не предлагаются +### Регистрация + +При первом запуске откройте `/register` и создайте логин с паролем (минимум +6 символов) — вы сразу войдёте. Каждый пользователь работает со своими +аккаунтами и лимитом генераций. Открытая регистрация отключается +`ALLOW_REGISTRATION=0` (пользователей тогда создаёт администратор через +`manage_users.py`). + ### Ключ сессии hh.ru Чтобы скрипт мог искать вакансии и видеть ваши резюме, нужны cookies из браузера: @@ -89,13 +102,30 @@ accounts/ 1. Скрипт парсит страницу поиска hh.ru по вашим запросам 2. Для каждой новой вакансии получает описание 3. ИИ генерирует текст отклика (вакансия + ваше резюме + стиль) -4. Браузер открывает вакансию, жмёт «Откликнуться», вставляет текст -5. **Вы проверяете текст и нажимаете «Отправить»** -6. Отмечаете отправленное — скрипт переходит к следующей +4. **Один клик**: кнопка «Отправить отклик» — браузер открывает вакансию, + жмёт «Откликнуться», вставляет текст, выбирает резюме, жмёт «Отправить» + и автоматически закрывается (скриншот результата — в модалке) +5. **С проверкой**: «Открыть в браузере» — вы проверяете текст и отправляете + сами, либо жмёте «Отправить отклик» в модалке скриншота +6. Отправленные вакансии отмечаются и больше не предлагаются + +## Переменные окружения (.env) + +| Переменная | По умолчанию | Описание | +|---|---|---| +| `LLM_API_KEY` | — | Ключ API для генерации текстов откликов | +| `SESSION_SECRET` | `dev-secret-change-me` | Секрет сессий Flask (обязательно сменить) | +| `PORT` | `18091` | Порт веб-интерфейса | +| `HOST` | `127.0.0.1` | Адрес прослушивания | +| `ALLOW_REGISTRATION` | `1` | Открытая регистрация (`0` — выключить) | +| `REGISTER_LLM_LIMIT` | `50` | Лимит генераций для новых пользователей | +| `HH_HEADLESS` | `0` | `1` — браузер без окна (для сервера без дисплея) | +| `PROXY_URL` | — | Прокси для браузера: `http://LOGIN:PASS@HOST:PORT` | ## Важно -- Скрипт не нарушает правила hh.ru: финальное действие всегда за человеком +- Отправка в один клик автоматически жмёт «Отправить» — используйте с осторожностью + и не чаще, чем позволяет hh.ru (паузы между страницами уже стоят) - Не запускайте поиск слишком часто — между страницами есть паузы - Если hh.ru покажет капчу — скрипт остановится, решите капчу вручную и запустите снова - `.env` содержит API-ключ — не коммитьте его в git (уже добавлен в `.gitignore`) \ No newline at end of file diff --git a/hh_browser.py b/hh_browser.py index f716398..5b3e84a 100644 --- a/hh_browser.py +++ b/hh_browser.py @@ -37,6 +37,15 @@ SUBMIT_BUTTONS = [ '[data-qa="vacancy-response-popup-form"] button[type="submit"]', 'button:has-text("Отправить")', ] +# Кнопки подтверждения во ВТОРОЙ модалке hh.ru: после клика «Отправить» +# иногда появляется модалка «Отклик будет отправлен» с кнопкой +# «Отправить отклик» — её нужно нажать ещё раз. +CONFIRM_BUTTONS = [ + 'button:has-text("Отправить отклик")', + 'button:has-text("Подтвердить")', + 'button:has-text("Да, отправить")', + '[data-qa="vacancy-response-confirm"]', +] # Живые браузерные контексты и страницы: {(user, account): ...} # Доступны ТОЛЬКО из браузерного потока. @@ -199,6 +208,26 @@ def _save_screenshot(page, screenshot_dir, vid): return None +def _click_second_confirm(page): + """После клика «Отправить» hh.ru иногда показывает вторую модалку + подтверждения («Отклик будет отправлен») — жмём и её. + Возвращает True, если вторая кнопка найдена и нажата.""" + try: + page.wait_for_timeout(1500) + except Exception: + return False + for sel in CONFIRM_BUTTONS: + try: + btn = page.locator(sel).first + if btn.is_visible(timeout=1500): + btn.click() + print(" ✓ Вторая модалка подтверждения — кнопка нажата") + return True + except Exception: + continue + return False + + def open_response_page(account_dir, user, account, vacancy, message, resume_id=None, screenshot_dir=None, submit=False): """Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст. @@ -277,11 +306,21 @@ def open_response_page(account_dir, user, account, vacancy, message, if not submitted: shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) return False, "Не нашёл кнопку «Отправить» — проверьте скриншот", shot - # Ждём подтверждение и снимаем скриншот результата - page.wait_for_timeout(2500) - shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - # Отклик отправлен — закрываем браузер автоматически - _close_context(key) + # hh.ru иногда показывает вторую модалку подтверждения — жмём и её + try: + _click_second_confirm(page) + 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"]) @@ -299,9 +338,18 @@ def confirm_send(user, account, vid): btn = page.locator(sel).first if btn.is_visible(timeout=3000): btn.click() - page.wait_for_timeout(1500) - # Отклик отправлен — закрываем браузер автоматически - _close_context(key) + try: + # Вторая модалка подтверждения (если появилась) + _click_second_confirm(page) + except Exception: + pass + try: + page.wait_for_timeout(1500) + except Exception: + pass + finally: + # Отклик отправлен — закрываем браузер автоматически + _close_context(key) return True, "Кнопка «Отправить» нажата. Проверьте статус отклика на hh.ru." except Exception: continue