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