Одно-кликовая отправка: кнопка «Отправить отклик» после генерации, /send-now, скриншот результата
This commit is contained in:
1 parent
a79be642f0
commit
40c2c86452
3 files changed
+124
-22
No files matched your search
+31
-8
@@ -164,9 +164,10 @@ def _save_screenshot(page, screenshot_dir, vid):
|
|||||||
|
|
||||||
|
|
||||||
def open_response_page(account_dir, user, account, vacancy, message,
|
def open_response_page(account_dir, user, account, vacancy, message,
|
||||||
resume_id=None, screenshot_dir=None):
|
resume_id=None, screenshot_dir=None, submit=False):
|
||||||
"""Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст,
|
"""Открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст.
|
||||||
делает скриншот. Возвращает (ok, note, screenshot_path)."""
|
При submit=True дополнительно жмёт «Отправить» и снимает скриншот результата.
|
||||||
|
Возвращает (ok, note, screenshot_path)."""
|
||||||
key = (user, account)
|
key = (user, account)
|
||||||
# Закрываем предыдущую страницу этого аккаунта
|
# Закрываем предыдущую страницу этого аккаунта
|
||||||
prev = _pages.pop(key, None)
|
prev = _pages.pop(key, None)
|
||||||
@@ -222,10 +223,32 @@ def open_response_page(account_dir, user, account, vacancy, message,
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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"])
|
shot = _save_screenshot(page, screenshot_dir, vacancy["id"])
|
||||||
if filled:
|
return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot
|
||||||
return True, "Текст вставлен. Проверьте скриншот и нажмите «Отправить»", shot
|
|
||||||
return False, "Не нашёл поле сообщения — проверьте скриншот", shot
|
|
||||||
|
|
||||||
|
|
||||||
def confirm_send(user, account, vid):
|
def confirm_send(user, account, vid):
|
||||||
@@ -270,10 +293,10 @@ def close_context(user, account):
|
|||||||
# ---------- Публичные обёртки (можно вызывать из любого потока) ----------
|
# ---------- Публичные обёртки (можно вызывать из любого потока) ----------
|
||||||
|
|
||||||
def browser_open(account_dir, user, account, vacancy, message,
|
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(
|
return run_in_browser(lambda: open_response_page(
|
||||||
account_dir, user, account, vacancy, message,
|
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):
|
def browser_send(user, account, vid):
|
||||||
|
|||||||
+49
-9
@@ -252,7 +252,7 @@
|
|||||||
<img id="shot-img" alt="Скриншот вакансии">
|
<img id="shot-img" alt="Скриншот вакансии">
|
||||||
<div class="toolbar" style="margin-top:12px">
|
<div class="toolbar" style="margin-top:12px">
|
||||||
<button id="btn-send" onclick="confirmSend()">Отправить отклик</button>
|
<button id="btn-send" onclick="confirmSend()">Отправить отклик</button>
|
||||||
<button class="secondary" onclick="markDone(currentShotId)">Отправил (отметить)</button>
|
<button id="btn-done" class="secondary" onclick="markDone(currentShotId)">Отправил (отметить)</button>
|
||||||
<span id="send-note" class="muted"></span>
|
<span id="send-note" class="muted"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,6 +263,7 @@ let currentAccount = null;
|
|||||||
let vacancies = [];
|
let vacancies = [];
|
||||||
let generatedTexts = {};
|
let generatedTexts = {};
|
||||||
let currentShotId = null;
|
let currentShotId = null;
|
||||||
|
let sentTexts = new Set();
|
||||||
|
|
||||||
// ---------- Вкладки ----------
|
// ---------- Вкладки ----------
|
||||||
function showTab(name) {
|
function showTab(name) {
|
||||||
@@ -557,12 +558,15 @@ function renderTexts(errors = {}) {
|
|||||||
vacancies.filter(v => generatedTexts[v.id]).forEach(v => {
|
vacancies.filter(v => generatedTexts[v.id]).forEach(v => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'item';
|
div.className = 'item';
|
||||||
|
const sent = sentTexts.has(v.id);
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="title">${v.title} — ${v.company || ''}</div>
|
<div class="title">${v.title} — ${v.company || ''}</div>
|
||||||
<textarea data-id="${v.id}">${escapeHtml(generatedTexts[v.id])}</textarea>
|
<textarea data-id="${v.id}">${escapeHtml(generatedTexts[v.id])}</textarea>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="secondary" onclick="openVacancy('${v.id}')">Открыть в браузере</button>
|
${sent
|
||||||
<button class="secondary" onclick="markDone('${v.id}')">Отправил</button>
|
? '<button class="secondary" disabled>Отправлено ✓</button>'
|
||||||
|
: `<button class="secondary" onclick="sendVacancy('${v.id}')">Отправить отклик</button>
|
||||||
|
<button class="secondary" onclick="openVacancy('${v.id}')">Открыть в браузере</button>`}
|
||||||
<span class="muted" id="note-${v.id}"></span>
|
<span class="muted" id="note-${v.id}"></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
list.appendChild(div);
|
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) {
|
async function openVacancy(id) {
|
||||||
const ta = document.querySelector(`textarea[data-id="${id}"]`);
|
const ta = document.querySelector(`textarea[data-id="${id}"]`);
|
||||||
const message = ta ? ta.value : (generatedTexts[id] || '');
|
const message = ta ? ta.value : (generatedTexts[id] || '');
|
||||||
@@ -593,12 +607,7 @@ async function openVacancy(id) {
|
|||||||
});
|
});
|
||||||
if (note) note.textContent = data.note || '';
|
if (note) note.textContent = data.note || '';
|
||||||
if (data.screenshot) {
|
if (data.screenshot) {
|
||||||
currentShotId = id;
|
showShotModal(id, data.screenshot, data.note || '', false);
|
||||||
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');
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.note || 'Не удалось открыть вакансию');
|
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() {
|
function closeShot() {
|
||||||
document.getElementById('shot-modal').classList.add('hidden');
|
document.getElementById('shot-modal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -625,7 +661,9 @@ async function confirmSend() {
|
|||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
const v = vacancies.find(x => x.id === currentShotId);
|
const v = vacancies.find(x => x.id === currentShotId);
|
||||||
if (v) v.responded = true;
|
if (v) v.responded = true;
|
||||||
|
sentTexts.add(currentShotId);
|
||||||
renderVacancies();
|
renderVacancies();
|
||||||
|
renderTexts();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
note.textContent = 'Ошибка: ' + e.message;
|
note.textContent = 'Ошибка: ' + e.message;
|
||||||
@@ -640,7 +678,9 @@ async function markDone(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;
|
||||||
|
sentTexts.add(id);
|
||||||
renderVacancies();
|
renderVacancies();
|
||||||
|
renderTexts();
|
||||||
const note = document.getElementById(`note-${id}`);
|
const note = document.getElementById(`note-${id}`);
|
||||||
if (note) note.textContent = 'Отмечено как отправленное';
|
if (note) note.textContent = 'Отмечено как отправленное';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,15 @@ def save_progress(acc_dir, progress):
|
|||||||
write_json(os.path.join(acc_dir, "progress.json"), 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):
|
def get_cache(user, account):
|
||||||
with _cache_lock:
|
with _cache_lock:
|
||||||
return _cache.setdefault((user, account), {})
|
return _cache.setdefault((user, account), {})
|
||||||
@@ -534,6 +543,40 @@ def api_open(name):
|
|||||||
return jsonify({"error": f"Ошибка браузера: {e}"}), 500
|
return jsonify({"error": f"Ошибка браузера: {e}"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/accounts/<name>/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/<name>/send", methods=["POST"])
|
@app.route("/api/accounts/<name>/send", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_send(name):
|
def api_send(name):
|
||||||
@@ -571,11 +614,7 @@ def api_done(name):
|
|||||||
return jsonify({"error": "Аккаунт не найден"}), 404
|
return jsonify({"error": "Аккаунт не найден"}), 404
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
vid = data.get("id")
|
vid = data.get("id")
|
||||||
progress = get_progress(acc_dir)
|
mark_responded(acc_dir, vid)
|
||||||
responded = set(progress.get("responded", []))
|
|
||||||
responded.add(vid)
|
|
||||||
progress["responded"] = sorted(responded)
|
|
||||||
save_progress(acc_dir, progress)
|
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user