From e50eb58799beabab2e4509cb3dc50cb8cc1479e4 Mon Sep 17 00:00:00 2001 From: Oscar Date: Thu, 28 May 2026 21:47:30 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix(bot-commands):=20=D0=98?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B8=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D0=B8=D0=BA=D0=B8=20=D0=B8=D1=81=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20listResumes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hh/bot-commands.ts | 22 +++++++++++++++++++--- src/hh/scraper.ts | 19 ++++++++++++++++++- src/hh/ui.ts | 12 +++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/hh/bot-commands.ts b/src/hh/bot-commands.ts index 994b02d..d53d506 100644 --- a/src/hh/bot-commands.ts +++ b/src/hh/bot-commands.ts @@ -2,8 +2,8 @@ import type { ResumeListItem } from './types.js' import bot from '@bot' import prisma from '@prisma' import cron, { type ScheduledTask } from 'node-cron' -import { applyToJobs, checkIsAuth, listResumes, login, saveResume } from './scraper.js' -import { BACK_MARKUP, createStatusReporter, escapeHtml, LOGIN_MARKUP, MAIN_MARKUP, safeEdit, showResult } from './ui.js' +import { applyToJobs, checkIsAuth, listResumes, login, NoResumeError, saveResume } from './scraper.js' +import { BACK_MARKUP, createStatusReporter, escapeHtml, LOGIN_MARKUP, MAIN_MARKUP, NO_RESUME_MARKUP, safeEdit, showResult } from './ui.js' interface UserState { autoCron: ScheduledTask | null @@ -356,7 +356,23 @@ export function registerHHCommands() { message_id: messageId, reply_markup: { inline_keyboard: [] }, }) - const resumes = await listResumes(chatId) + let resumes: Awaited> + try { + resumes = await listResumes(chatId) + } + catch (e) { + if (e instanceof NoResumeError) { + await safeEdit( + '📝 Резюме не найдено.\n\nСоздайте резюме на hh.ru, затем нажмите Повторить.', + { chat_id: chatId, message_id: messageId, reply_markup: NO_RESUME_MARKUP, parse_mode: 'HTML' }, + ) + } + else { + console.error('[hh_resume_list] listResumes failed:', e) + await showResult(chatId, messageId, '❌ Не удалось загрузить резюме. Попробуйте войти заново через «Войти на hh.ru».') + } + break + } if (resumes.length === 0) { await showResult(chatId, messageId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru') } diff --git a/src/hh/scraper.ts b/src/hh/scraper.ts index a59f267..6efed5e 100644 --- a/src/hh/scraper.ts +++ b/src/hh/scraper.ts @@ -8,6 +8,13 @@ import { createMessage } from '@/openai' import { getBrowser, loadSession, newStealthContext, randomDelay, randomScroll } from './browser.js' import { escapeHtml } from './ui.js' +export class NoResumeError extends Error { + constructor() { + super('no_resume') + this.name = 'NoResumeError' + } +} + function waitForOtp(chatId: number): Promise { return new Promise((resolve) => { const handler = (msg: Message) => { @@ -135,6 +142,13 @@ export async function listResumes(chatId: number): Promise { for (let attempt = 1; attempt <= 2; attempt++) { try { await page.goto('https://hh.ru/applicant/resumes', { waitUntil: 'domcontentloaded' }) + const finalUrl = page.url() + if (finalUrl.includes('/profile/resume/professional_role')) { + throw new NoResumeError() + } + if (!finalUrl.includes('/applicant/resumes')) { + throw new Error(`Session expired or redirected: ${finalUrl}`) + } await page.waitForSelector('[data-qa^="resume-card-link-"]', { timeout: 10000 }) const cardLinks = await page.$$('[data-qa^="resume-card-link-"]') @@ -195,6 +209,8 @@ export async function listResumes(chatId: number): Promise { return items } catch (e) { + if (e instanceof NoResumeError) + throw e lastError = e as Error if (attempt < 2) await page.waitForTimeout(4000) @@ -389,7 +405,8 @@ export async function applyToJobs( let letterInput = null for (const sel of LETTER_SELECTORS) { letterInput = await page.$(sel) - if (letterInput) break + if (letterInput) + break } await letterInput?.click() await letterInput?.fill(letter, { force: true }) diff --git a/src/hh/ui.ts b/src/hh/ui.ts index d93196b..296257e 100644 --- a/src/hh/ui.ts +++ b/src/hh/ui.ts @@ -33,6 +33,13 @@ export const BACK_MARKUP = { inline_keyboard: [[{ text: '◀️ Назад', callback_data: 'hh_back' }]], } +export const NO_RESUME_MARKUP = { + inline_keyboard: [[ + { text: '🔄 Повторить', callback_data: 'hh_resume_list' }, + { text: '🔑 Другой аккаунт', callback_data: 'hh_login' }, + ]], +} + export function escapeHtml(text: string): string { return text.replace(/&/g, '&').replace(//g, '>') } @@ -42,7 +49,10 @@ export async function safeEdit( options: Parameters[1], ): Promise { await bot.editMessageText(text, options).catch((e: unknown) => { - if (e instanceof Error && e.message.includes('message is not modified')) + if (e instanceof Error && ( + e.message.includes('message is not modified') + || e.message.includes('message to edit not found') + )) return throw e })