diff --git a/prisma/migrations/20260527141712_skipped_vacancy/migration.sql b/prisma/migrations/20260527141712_skipped_vacancy/migration.sql new file mode 100644 index 0000000..b6b1466 --- /dev/null +++ b/prisma/migrations/20260527141712_skipped_vacancy/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "SkippedVacancy" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "telegramId" BIGINT NOT NULL, + "href" TEXT NOT NULL, + "title" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE UNIQUE INDEX "SkippedVacancy_telegramId_href_key" ON "SkippedVacancy"("telegramId", "href"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9e1dfcb..9bddcb8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -42,3 +42,13 @@ model Settings { maxApplies Int @default(1) selectedResumeId String? } + +model SkippedVacancy { + id Int @id @default(autoincrement()) + telegramId BigInt + href String + title String + createdAt DateTime @default(now()) + + @@unique([telegramId, href]) +} diff --git a/src/hh/bot-commands.ts b/src/hh/bot-commands.ts index 57b3e6d..bdf2db8 100644 --- a/src/hh/bot-commands.ts +++ b/src/hh/bot-commands.ts @@ -328,6 +328,28 @@ export function registerHHCommands() { await showResult(chatId, messageId, '⛔ Авто остановлен') break + case 'hh_skipped': { + const skipped = await prisma.skippedVacancy.findMany({ + where: { telegramId: chatId }, + orderBy: { createdAt: 'desc' }, + take: 50, + }) + if (!skipped.length) { + await showResult(chatId, messageId, '✅ Проблемных вакансий нет') + break + } + const lines = ['🚫 Вакансии с опросником (бот не может откликнуться):', ''] + skipped.forEach(v => lines.push(`• ${escapeHtml(v.title)}`)) + await bot.editMessageText(lines.join('\n'), { + chat_id: chatId, + message_id: messageId, + parse_mode: 'HTML', + disable_web_page_preview: true, + reply_markup: BACK_MARKUP, + }) + break + } + case 'hh_resume_list': { await bot.editMessageText('🔄 Загружаю список резюме...', { chat_id: chatId, diff --git a/src/hh/scraper.ts b/src/hh/scraper.ts index 4507290..da199f8 100644 --- a/src/hh/scraper.ts +++ b/src/hh/scraper.ts @@ -218,8 +218,21 @@ export async function applyToJobs( return results } - for (const vacancy of vacancies.slice(0, maxApplies)) { + const knownSkipped = new Set( + (await prisma.skippedVacancy.findMany({ where: { telegramId: chatId }, select: { href: true } })) + .map(v => v.href), + ) + + let appliedCount = 0 + for (const vacancy of vacancies) { + if (appliedCount >= maxApplies) + break const ref: VacancyRef = { title: vacancy.title, href: vacancy.href } + + if (knownSkipped.has(vacancy.href)) { + continue + } + try { await status(`🔄 Обрабатывается: ${vacancy.title}`) await page.goto(vacancy.href, { waitUntil: 'domcontentloaded' }) @@ -249,8 +262,21 @@ export async function applyToJobs( await randomScroll(page) await applyBtn.click() + await page.waitForLoadState('domcontentloaded').catch(() => {}) await page.waitForTimeout(randomDelay()) + const hasQuestionnaire = await page.$('[data-qa="employer-asking-for-test"]').catch(() => null) + if (hasQuestionnaire) { + console.log(`[x] ${vacancy.title} hasQuestionnaire`) + await prisma.skippedVacancy.upsert({ + where: { telegramId_href: { telegramId: chatId, href: vacancy.href } }, + create: { telegramId: chatId, href: vacancy.href, title: vacancy.title }, + update: {}, + }) + results.skipped.push(ref) + continue + } + if (resumes.length > 1) { // Выбор резюме const currentResumeEl = await page.$('[data-qa="resume-title"]') @@ -284,15 +310,20 @@ export async function applyToJobs( } const letter = await letterPromise - await keep(`✅ ${escapeHtml(vacancy.title)}\n\n${escapeHtml(letter)}`) if (letter) { + await keep(`✅ ${escapeHtml(vacancy.title)}\n\n${escapeHtml(letter)}`) const letterInput = await page.$('[data-qa="vacancy-response-popup-form-letter-input"]') await letterInput?.click() await letterInput?.fill(letter) await page.pause() } + else { + await keep(`Письмо не сгенерировано, ошибка`) + } + + await page.waitForTimeout(randomDelay()) const submitBtn = await page.$('[data-qa="vacancy-response-submit-popup"]')// vacancy-response-popup-submit if (submitBtn) { @@ -303,6 +334,8 @@ export async function applyToJobs( const errMsg = 'Not found submit button' console.log(errMsg) results.errors.push({ ...ref, message: errMsg }) + // results.skipped.push(vacancy) + continue } } else { @@ -310,18 +343,32 @@ export async function applyToJobs( const letter = await letterPromise console.log('letter: ', letter) - await keep(`✅ ${escapeHtml(vacancy.title)}\n\n${escapeHtml(letter)}`) if (letter) { + await keep(`✅ ${escapeHtml(vacancy.title)}\n\n${escapeHtml(letter)}`) const letterInput = await page.$('[data-qa="textarea-native-wrapper"] textarea') ?? await page.$('[data-qa="vacancy-response-popup-form-letter-input"]') await letterInput?.click() await letterInput?.fill(letter) await page.pause() } + + await page.waitForTimeout(randomDelay()) + + const submitBtn = await page.$('[data-qa="vacancy-response-letter-submit"]') ?? await page.$('[data-qa="vacancy-response-submit-popup"]')// vacancy-response-popup-submit + if (submitBtn) { + await submitBtn.click() + await page.waitForTimeout(randomDelay()) + } + else { + const errMsg = 'Not found submit button' + console.log(errMsg) + results.errors.push({ ...ref, message: errMsg }) + } } results.applied.push(ref) + appliedCount++ } catch (err) { results.errors.push({ ...ref, message: (err as Error).message }) diff --git a/src/hh/ui.ts b/src/hh/ui.ts index 6ad93da..ecf5c8d 100644 --- a/src/hh/ui.ts +++ b/src/hh/ui.ts @@ -19,6 +19,7 @@ export const MAIN_MARKUP = { { text: '📄 Выбрать резюме', callback_data: 'hh_resume_list' }, { text: '📋 Моё резюме', callback_data: 'hh_my_resume' }, ], + [{ text: '🚫 Проблемные вакансии', callback_data: 'hh_skipped' }], ], }