From 40c2c8645255013ad0ea0b3693b93e20633fca53 Mon Sep 17 00:00:00 2001 From: apuc Date: Wed, 16 Sep 2026 13:02:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B4=D0=BD=D0=BE-=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=C2=AB=D0=9E=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=BB=D0=B8=D0=BA=C2=BB=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8,=20/send-now,=20=D1=81=D0=BA=D1=80=D0=B8=D0=BD=D1=88?= =?UTF-8?q?=D0=BE=D1=82=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hh_browser.py | 39 +++++++++++++++++++++++------ templates/index.html | 58 +++++++++++++++++++++++++++++++++++++------- webui.py | 49 +++++++++++++++++++++++++++++++++---- 3 files changed, 124 insertions(+), 22 deletions(-) diff --git a/hh_browser.py b/hh_browser.py index c764d17..668810b 100644 --- a/hh_browser.py +++ b/hh_browser.py @@ -164,9 +164,10 @@ def _save_screenshot(page, screenshot_dir, vid): def open_response_page(account_dir, user, account, vacancy, message, - resume_id=None, screenshot_dir=None): - """Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст, - делает скриншот. Возвращает (ok, note, screenshot_path).""" + resume_id=None, screenshot_dir=None, submit=False): + """Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст. + При submit=True дополнительно жмёт «Отправить» и снимает скриншот результата. + Возвращает (ok, note, screenshot_path).""" key = (user, account) # Закрываем предыдущую страницу этого аккаунта prev = _pages.pop(key, None) @@ -222,10 +223,32 @@ def open_response_page(account_dir, user, account, vacancy, message, 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 + # Ждём подтверждение и снимаем скриншот результата + page.wait_for_timeout(2500) + shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) + return True, "Отклик отправлен", shot + shot = _save_screenshot(page, screenshot_dir, vacancy["id"]) - if filled: - return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot - return False, "Не нашёл поле сообщения — проверьте скриншот", shot + return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot def confirm_send(user, account, vid): @@ -270,10 +293,10 @@ def close_context(user, account): # ---------- Публичные обёртки (можно вызывать из любого потока) ---------- def browser_open(account_dir, user, account, vacancy, message, - resume_id=None, screenshot_dir=None): + resume_id=None, screenshot_dir=None, submit=False): return run_in_browser(lambda: open_response_page( account_dir, user, account, vacancy, message, - resume_id=resume_id, screenshot_dir=screenshot_dir)) + resume_id=resume_id, screenshot_dir=screenshot_dir, submit=submit)) def browser_send(user, account, vid): diff --git a/templates/index.html b/templates/index.html index 5caa479..6075cfc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -252,7 +252,7 @@ Скриншот вакансии
- +
@@ -263,6 +263,7 @@ let currentAccount = null; let vacancies = []; let generatedTexts = {}; let currentShotId = null; +let sentTexts = new Set(); // ---------- Вкладки ---------- function showTab(name) { @@ -557,12 +558,15 @@ function renderTexts(errors = {}) { vacancies.filter(v => generatedTexts[v.id]).forEach(v => { const div = document.createElement('div'); div.className = 'item'; + const sent = sentTexts.has(v.id); div.innerHTML = `
${v.title} — ${v.company || ''}
- - + ${sent + ? '' + : ` + `}
`; list.appendChild(div); @@ -582,6 +586,16 @@ function escapeHtml(s) { } // ---------- Открытие, проверка и отправка ---------- +function showShotModal(id, screenshot, note, sent) { + currentShotId = id; + document.getElementById('shot-img').src = screenshot + '?t=' + Date.now(); + document.getElementById('shot-note').textContent = note; + document.getElementById('send-note').textContent = ''; + document.getElementById('btn-send').style.display = sent ? 'none' : ''; + document.getElementById('btn-done').style.display = sent ? 'none' : ''; + document.getElementById('shot-modal').classList.remove('hidden'); +} + async function openVacancy(id) { const ta = document.querySelector(`textarea[data-id="${id}"]`); const message = ta ? ta.value : (generatedTexts[id] || ''); @@ -593,12 +607,7 @@ async function openVacancy(id) { }); if (note) note.textContent = data.note || ''; if (data.screenshot) { - currentShotId = id; - document.getElementById('shot-img').src = data.screenshot + '?t=' + Date.now(); - document.getElementById('shot-note').textContent = data.note || ''; - document.getElementById('send-note').textContent = ''; - document.getElementById('btn-send').disabled = false; - document.getElementById('shot-modal').classList.remove('hidden'); + showShotModal(id, data.screenshot, data.note || '', false); } else { alert(data.note || 'Не удалось открыть вакансию'); } @@ -608,6 +617,33 @@ async function openVacancy(id) { } } +async function sendVacancy(id) { + if (!currentAccount) { alert('Сначала выберите аккаунт'); return; } + const ta = document.querySelector(`textarea[data-id="${id}"]`); + const message = ta ? ta.value : (generatedTexts[id] || ''); + const note = document.getElementById(`note-${id}`); + if (note) { note.textContent = 'Отправляю отклик...'; } + try { + const data = await api(`/api/accounts/${currentAccount}/send-now`, { + method: 'POST', body: JSON.stringify({ id, message }), + }); + if (note) note.textContent = data.note || ''; + if (data.screenshot) { + showShotModal(id, data.screenshot, data.note || 'Отклик отправлен', true); + sentTexts.add(id); + const v = vacancies.find(x => x.id === id); + if (v) v.responded = true; + renderVacancies(); + renderTexts(); + } else { + alert(data.note || 'Не удалось отправить отклик'); + } + } catch (e) { + if (note) note.textContent = 'Ошибка: ' + e.message; + alert('Ошибка: ' + e.message); + } +} + function closeShot() { document.getElementById('shot-modal').classList.add('hidden'); } @@ -625,7 +661,9 @@ async function confirmSend() { if (data.ok) { const v = vacancies.find(x => x.id === currentShotId); if (v) v.responded = true; + sentTexts.add(currentShotId); renderVacancies(); + renderTexts(); } } catch (e) { note.textContent = 'Ошибка: ' + e.message; @@ -640,7 +678,9 @@ async function markDone(id) { }); const v = vacancies.find(x => x.id === id); if (v) v.responded = true; + sentTexts.add(id); renderVacancies(); + renderTexts(); const note = document.getElementById(`note-${id}`); if (note) note.textContent = 'Отмечено как отправленное'; } diff --git a/webui.py b/webui.py index 8da55e2..a156a1d 100644 --- a/webui.py +++ b/webui.py @@ -168,6 +168,15 @@ def save_progress(acc_dir, progress): write_json(os.path.join(acc_dir, "progress.json"), progress) +def mark_responded(acc_dir, vid): + """Добавляет вакансию в список откликнутых.""" + progress = get_progress(acc_dir) + responded = set(progress.get("responded", [])) + responded.add(vid) + progress["responded"] = sorted(responded) + save_progress(acc_dir, progress) + + def get_cache(user, account): with _cache_lock: return _cache.setdefault((user, account), {}) @@ -534,6 +543,40 @@ def api_open(name): return jsonify({"error": f"Ошибка браузера: {e}"}), 500 +@app.route("/api/accounts//send-now", methods=["POST"]) +@login_required +def api_send_now(name): + """Одно-кликовый режим: открывает вакансию, вставляет текст, жмёт «Отправить», + делает скриншот результата и помечает вакансию как откликнутую.""" + acc_dir = account_path(name) + if not os.path.isdir(acc_dir): + return jsonify({"error": "Аккаунт не найден"}), 404 + data = request.get_json(force=True) + vid = data.get("id") + message = data.get("message") or "" + cfg = read_json(os.path.join(acc_dir, "config.json"), {}) + resume_id = cfg.get("resume_id") + cache = get_cache(current_user(), name) + vacancy = cache.get(vid) + if not vacancy: + return jsonify({"error": "Вакансия не найдена в кэше — обновите поиск"}), 404 + + try: + shot_dir = os.path.join(acc_dir, "screenshots") + ok, note, shot = browser_open(acc_dir, current_user(), name, vacancy, message, + resume_id=resume_id, screenshot_dir=shot_dir, + submit=True) + if ok: + mark_responded(acc_dir, vid) + return jsonify({ + "ok": ok, + "note": note, + "screenshot": f"/api/accounts/{name}/screenshot/{vid}" if shot else None, + }) + except Exception as e: + return jsonify({"error": f"Ошибка браузера: {e}"}), 500 + + @app.route("/api/accounts//send", methods=["POST"]) @login_required def api_send(name): @@ -571,11 +614,7 @@ def api_done(name): return jsonify({"error": "Аккаунт не найден"}), 404 data = request.get_json(force=True) vid = data.get("id") - progress = get_progress(acc_dir) - responded = set(progress.get("responded", [])) - responded.add(vid) - progress["responded"] = sorted(responded) - save_progress(acc_dir, progress) + mark_responded(acc_dir, vid) return jsonify({"ok": True})