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})