From 67c120930bac029f29e29f09a106bbcb20610a24 Mon Sep 17 00:00:00 2001 From: Oscar Date: Wed, 27 May 2026 10:58:37 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20REFACTOR(bot-commands):=20?= =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=D1=8E=D0=BC=D0=B5.=20RESET=20Menu=20to=20Bot?= =?UTF-8?q?tom.=20=D0=9F=D1=80=D0=B5=D0=B4=D0=BF=D1=80=D0=B8=D0=BD=D1=8F?= =?UTF-8?q?=D1=82=D1=8B=20=D0=BC=D0=B5=D1=80=D1=8B=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hh/bot-commands.ts | 41 ++++++++++++++++++++++++++++++----- src/hh/scraper.ts | 49 +++++++++++++++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/hh/bot-commands.ts b/src/hh/bot-commands.ts index 06c1a04..4f72faa 100644 --- a/src/hh/bot-commands.ts +++ b/src/hh/bot-commands.ts @@ -75,31 +75,62 @@ async function sendResumeSelector(chatId: number, resumes: ResumeListItem[], mes }) } +async function resetMenuToBottom(chatId: number): Promise { + const state = getState(chatId) + if (state.menuMessageId) { + await bot.deleteMessage(chatId, state.menuMessageId).catch(() => {}) + state.menuMessageId = null + } +} + async function doLogin(chatId: number, email: string): Promise { await bot.sendMessage(chatId, '🔄 Логинюсь...') try { await login(email, chatId) await prisma.user.update({ where: { telegramId: chatId }, data: { hhEmail: email } }) - const resumes = await listResumes(chatId) const state = getState(chatId) - if (resumes.length === 0) { + // listResumes может упасть по таймауту сразу после логина — это не критично + let resumes: ResumeListItem[] | null = null + try { + resumes = await listResumes(chatId) + } + catch { + await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню') + } + + await resetMenuToBottom(chatId) + + if (resumes === null) { + // таймаут при загрузке резюме — просто показываем меню + } + else if (resumes.length === 0) { await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru') } else if (resumes.length === 1) { await saveResume(chatId, resumes[0].href) await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`) } - else if (state.menuMessageId) { - await sendResumeSelector(chatId, resumes, state.menuMessageId) + else { + // несколько резюме — отправляем новый селектор внизу + state.pendingResumes = resumes + const selectorMsg = await bot.sendMessage(chatId, '📄 Выбери резюме:', { + reply_markup: { + inline_keyboard: [ + ...resumes.map((r, i) => [{ text: r.title, callback_data: `hh_resume_pick_${i}` }]), + [{ text: '◀️ Назад', callback_data: 'hh_back' }], + ], + }, + }) + state.menuMessageId = selectorMsg.message_id return } - await bot.sendMessage(chatId, '✅ Авторизован! Куки сохранены.') await showMenu(chatId) } catch (e) { + await resetMenuToBottom(chatId) await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`) await showMenu(chatId) } diff --git a/src/hh/scraper.ts b/src/hh/scraper.ts index c114aae..278887e 100644 --- a/src/hh/scraper.ts +++ b/src/hh/scraper.ts @@ -30,7 +30,7 @@ export async function login(email: string, chatId: number): Promise { const page = await context.newPage() await page.goto('https://hh.ru/account/login', { waitUntil: 'networkidle' }) - await bot.sendMessage(chatId, `page: ${page.url()}`) + // await bot.sendMessage(chatId, `page: ${page.url()}`) await page.click('[data-qa="submit-button"]') await page.waitForTimeout(randomDelay()) @@ -88,18 +88,30 @@ export async function listResumes(chatId: number): Promise { const context = await browser.newContext() const page = await context.newPage() await loadSession(page, chatId) - await page.goto('https://hh.ru/applicant/resumes', { waitUntil: 'networkidle' }) - const resumes = await page.$$eval( - '[data-qa^="resume-card-link-"]', - links => links.map(a => ({ - href: (a as HTMLAnchorElement).getAttribute('href') ?? '', - title: a.textContent?.trim() ?? '(без названия)', - })), - ) + let lastError: Error | null = null + for (let attempt = 1; attempt <= 2; attempt++) { + try { + await page.goto('https://hh.ru/applicant/resumes', { waitUntil: 'networkidle' }) + const resumes = await page.$$eval( + '[data-qa^="resume-card-link-"]', + links => links.map(a => ({ + href: (a as HTMLAnchorElement).getAttribute('href') ?? '', + title: a.textContent?.trim() ?? '(без названия)', + })), + ) + await browser.close() + return resumes + } + catch (e) { + lastError = e as Error + if (attempt < 2) + await page.waitForTimeout(4000) + } + } await browser.close() - return resumes + throw lastError! } export async function saveResume(chatId: number, resumeHref: string): Promise { @@ -194,6 +206,23 @@ export async function applyToJobs( const letter = await createMessage(resume.data, description, user!.prompt) await keep(`✅ ${vacancy.title}\n\n${letter}`) + // const applyBtn = await page.$('[data-qa="vacancy-response-link-top"]') + // if (!applyBtn) { + // results.skipped.push(vacancy.title) + // continue + // } + // + // await randomScroll(page) + // + // await applyBtn.click() + // await page.waitForTimeout(randomDelay()) + // + // const submitBtn = await page.$('[data-qa="vacancy-response-popup-submit"]') + // if (submitBtn) { + // await submitBtn.click() + // await page.waitForTimeout(randomDelay()) + // } + results.applied.push(ref) } catch (err) {