🔧 refactor(file): Исправлена обработка HTML тегов на странице.

This commit is contained in:
Oscar
2026-05-27 17:15:35 +03:00
parent 23899a377a
commit 6920e6d2bb
5 changed files with 139 additions and 101 deletions

View File

@@ -3,5 +3,6 @@ import antfu from '@antfu/eslint-config'
export default antfu({ export default antfu({
rules: { rules: {
'no-console': 0, 'no-console': 0,
'unicorn/prefer-dom-node-text-content': 0,
}, },
}) })

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "selectedResumeId" TEXT;

View File

@@ -40,4 +40,5 @@ model Settings {
user User @relation(references: [telegramId], fields: [telegramId], onDelete: Cascade) user User @relation(references: [telegramId], fields: [telegramId], onDelete: Cascade)
searchQuery String @default("Vue") searchQuery String @default("Vue")
maxApplies Int @default(1) maxApplies Int @default(1)
selectedResumeId String?
} }

View File

@@ -202,7 +202,7 @@ export function registerHHCommands() {
if (result.errors.length) { if (result.errors.length) {
lines.push('') lines.push('')
lines.push('❌ <b>Ошибки:</b>') lines.push('❌ <b>Ошибки:</b>')
result.errors.forEach(v => lines.push(`• <a href="${v.href}">${v.title}</a> — ${v.message}`)) result.errors.forEach(v => lines.push(`• <a href="${v.href}">${escapeHtml(v.title)}</a> — ${escapeHtml(v.message ?? '')}`))
} }
const fullText = lines.join('\n') const fullText = lines.join('\n')
@@ -230,10 +230,9 @@ export function registerHHCommands() {
} }
case 'hh_my_resume': { case 'hh_my_resume': {
const resume = await prisma.resume.findFirst({ const resume = settings?.selectedResumeId
where: { telegramId: chatId }, ? await prisma.resume.findUnique({ where: { id: settings.selectedResumeId } })
orderBy: { id: 'asc' }, : await prisma.resume.findFirst({ where: { telegramId: chatId } })
})
if (!resume) { if (!resume) {
await showResult(chatId, messageId, '📋 Резюме не найдено.\n\nВыбери резюме через кнопку 📄 Выбрать резюме.') await showResult(chatId, messageId, '📋 Резюме не найдено.\n\nВыбери резюме через кнопку 📄 Выбрать резюме.')
break break

View File

@@ -5,6 +5,7 @@ import bot from '@bot'
import prisma from '@prisma' import prisma from '@prisma'
import { createMessage } from '../openai' import { createMessage } from '../openai'
import { getBrowser, loadSession, randomDelay, randomScroll } from './browser.js' import { getBrowser, loadSession, randomDelay, randomScroll } from './browser.js'
import { escapeHtml } from './ui.js'
function waitForOtp(chatId: number): Promise<string> { function waitForOtp(chatId: number): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -97,21 +98,63 @@ export async function listResumes(chatId: number): Promise<ResumeListItem[]> {
try { try {
await page.goto('https://hh.ru/applicant/resumes', { waitUntil: 'domcontentloaded' }) await page.goto('https://hh.ru/applicant/resumes', { waitUntil: 'domcontentloaded' })
await page.waitForSelector('[data-qa^="resume-card-link-"]', { timeout: 10000 }) await page.waitForSelector('[data-qa^="resume-card-link-"]', { timeout: 10000 })
const resumes = await page.$$eval(
const cardLinks = await page.$$('[data-qa^="resume-card-link-"]')
let items: ResumeListItem[]
if (cardLinks.length > 1) {
items = await page.$$eval(
'[data-qa^="resume-card-link-"]', '[data-qa^="resume-card-link-"]',
links => links.map((a) => { links => links.map((a) => {
const card = a.closest('[data-qa^="resume-card"]') ?? a.parentElement const card = a.closest('[data-qa^="resume-card"]') ?? a.parentElement
const titleEl = card?.querySelector('[data-qa="cell-text-content"]') const titleEl = card?.querySelector('[data-qa="resume-title"] h3') ?? card?.querySelector('[data-qa="title"]')
return { return {
href: (a as HTMLAnchorElement).getAttribute('href') ?? '', href: (a as HTMLAnchorElement).getAttribute('href') ?? '',
title: titleEl?.textContent?.trim() ?? '(без названия)', title: titleEl?.textContent?.trim() ?? '(без названия)',
} }
}), }),
) )
}
else {
const href = await cardLinks[0].getAttribute('href') ?? ''
const titleEl = await page.$('[data-qa="resume-title"] h3') ?? await page.$('[data-qa="title"]')
const title = (await titleEl?.innerText())?.trim() ?? '(без названия)'
items = [{ href, title }]
}
// Sync with DB
const hhIds = items.map(item => new URL(`https://hh.ru${item.href}`).pathname.split('/').pop()!)
// Delete resumes removed from hh.ru
await prisma.resume.deleteMany({ where: { telegramId: chatId, id: { notIn: hhIds } } })
// If selected resume was deleted — reset selection
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
if (settings?.selectedResumeId && !hhIds.includes(settings.selectedResumeId)) {
await prisma.settings.update({ where: { telegramId: chatId }, data: { selectedResumeId: null } })
}
// Fetch text and upsert each resume
for (const item of items) {
const id = new URL(`https://hh.ru${item.href}`).pathname.split('/').pop()!
const resumeUrl = `https://hh.ru/resume_converter/resume.txt?hash=${id}&type=txt&hhtmFrom=&hhtmSource=resume`
await page.goto(resumeUrl, { waitUntil: 'load' })
try {
const data = await page.locator('.resume').innerText()
await prisma.resume.upsert({
where: { id },
create: { data, id, telegramId: chatId, title: item.title },
update: { data, title: item.title },
})
}
catch (e) {
console.log(`Failed to fetch resume text for ${item.title}:`, e)
}
}
await browser.close() await browser.close()
console.log(resumes) console.log(items)
return resumes return items
} }
catch (e) { catch (e) {
lastError = e as Error lastError = e as Error
@@ -124,39 +167,12 @@ export async function listResumes(chatId: number): Promise<ResumeListItem[]> {
throw lastError! throw lastError!
} }
export async function saveResume(chatId: number, resumeItem: ResumeListItem): Promise<string | undefined> { export async function saveResume(chatId: number, resumeItem: ResumeListItem): Promise<void> {
const browser = await getBrowser() const id = new URL(`https://hh.ru${resumeItem.href}`).pathname.split('/').pop()!
const context = await browser.newContext() await prisma.settings.update({
const page = await context.newPage() where: { telegramId: chatId },
await loadSession(page, chatId) data: { selectedResumeId: id },
const resumeHref = resumeItem.href
const title = resumeItem.title
const id = new URL(`https://hh.ru${resumeHref}`).pathname.split('/').pop()!
const resumeUrl = `https://hh.ru/resume_converter/resume.txt?hash=${id}&type=txt&hhtmFrom=&hhtmSource=resume`
await page.goto(resumeUrl, { waitUntil: 'load' })
let resume: string | undefined
try {
resume = await page.locator('.resume').innerText()
await prisma.resume.deleteMany({ where: { telegramId: chatId, NOT: { id } } })
await prisma.resume.upsert({
where: { id },
create: { data: resume, id, telegramId: chatId, title },
update: { data: resume, title },
}) })
}
catch (e) {
console.log(e)
await bot.sendMessage(chatId, 'Нет резюме на hh.ru, создайте')
}
finally {
await browser.close()
}
return resume
} }
export async function applyToJobs( export async function applyToJobs(
@@ -192,7 +208,9 @@ export async function applyToJobs(
await status(`✅ Вакансий найдено: ${vacancies.length}`) await status(`✅ Вакансий найдено: ${vacancies.length}`)
const resume = await prisma.resume.findFirst({ where: { telegramId: chatId } }) const resumes = await prisma.resume.findMany({ where: { telegramId: chatId } })
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
const resume = resumes.find(r => r.id === settings?.selectedResumeId) ?? resumes[0]
const user = await prisma.user.findUnique({ where: { telegramId: chatId } }) const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
if (!resume?.data) { if (!resume?.data) {
@@ -219,6 +237,7 @@ export async function applyToJobs(
await status(`✍️ Генерирую письмо: ${vacancy.title}`) await status(`✍️ Генерирую письмо: ${vacancy.title}`)
console.log('[LetterDebug]:', '\nresume: ', resume.data, '\ndescription: ', description, '\nprompt: ', user!.prompt)
const letterPromise = createMessage(resume.data, description, user!.prompt) const letterPromise = createMessage(resume.data, description, user!.prompt)
const applyBtn = await page.$('[data-qa="vacancy-response-link-top"]') const applyBtn = await page.$('[data-qa="vacancy-response-link-top"]')
@@ -232,6 +251,7 @@ export async function applyToJobs(
await applyBtn.click() await applyBtn.click()
await page.waitForTimeout(randomDelay()) await page.waitForTimeout(randomDelay())
if (resumes.length > 1) {
// Выбор резюме // Выбор резюме
const currentResumeEl = await page.$('[data-qa="resume-title"]') const currentResumeEl = await page.$('[data-qa="resume-title"]')
const currentResumeTitle = (await currentResumeEl?.innerText())?.trim() ?? '' const currentResumeTitle = (await currentResumeEl?.innerText())?.trim() ?? ''
@@ -264,7 +284,7 @@ export async function applyToJobs(
} }
const letter = await letterPromise const letter = await letterPromise
await keep(`✅ <b>${vacancy.title}</b>\n\n${letter}`) await keep(`✅ <b>${escapeHtml(vacancy.title)}</b>\n\n${escapeHtml(letter)}`)
if (letter) { if (letter) {
const letterInput = await page.$('[data-qa="vacancy-response-popup-form-letter-input"]') const letterInput = await page.$('[data-qa="vacancy-response-popup-form-letter-input"]')
@@ -284,8 +304,23 @@ export async function applyToJobs(
console.log(errMsg) console.log(errMsg)
results.errors.push({ ...ref, message: errMsg }) results.errors.push({ ...ref, message: errMsg })
} }
}
else {
console.log('single flow')
const letter = await letterPromise
console.log('letter: ', letter)
await keep(`✅ <b>${escapeHtml(vacancy.title)}</b>\n\n${escapeHtml(letter)}`)
if (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.pause()
}
}
results.applied.push(ref) results.applied.push(ref)
} }
catch (err) { catch (err) {