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' }],
],
}