mirror of
https://github.com/hempyhemp/hh-auto-reply.git
synced 2026-06-08 18:04:57 +00:00
🔧 refactor(file): Исправлена обработка HTML тегов на странице.
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Settings" ADD COLUMN "selectedResumeId" TEXT;
|
||||||
@@ -36,8 +36,9 @@ model Resume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Settings {
|
model Settings {
|
||||||
telegramId BigInt @id
|
telegramId BigInt @id
|
||||||
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?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
|
||||||
'[data-qa^="resume-card-link-"]',
|
const cardLinks = await page.$$('[data-qa^="resume-card-link-"]')
|
||||||
links => links.map((a) => {
|
|
||||||
const card = a.closest('[data-qa^="resume-card"]') ?? a.parentElement
|
let items: ResumeListItem[]
|
||||||
const titleEl = card?.querySelector('[data-qa="cell-text-content"]')
|
if (cardLinks.length > 1) {
|
||||||
return {
|
items = await page.$$eval(
|
||||||
href: (a as HTMLAnchorElement).getAttribute('href') ?? '',
|
'[data-qa^="resume-card-link-"]',
|
||||||
title: titleEl?.textContent?.trim() ?? '(без названия)',
|
links => links.map((a) => {
|
||||||
}
|
const card = a.closest('[data-qa^="resume-card"]') ?? a.parentElement
|
||||||
}),
|
const titleEl = card?.querySelector('[data-qa="resume-title"] h3') ?? card?.querySelector('[data-qa="title"]')
|
||||||
)
|
return {
|
||||||
|
href: (a as HTMLAnchorElement).getAttribute('href') ?? '',
|
||||||
|
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,60 +251,76 @@ 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 currentResumeTitle = (await currentResumeEl?.innerText())?.trim() ?? ''
|
const currentResumeEl = await page.$('[data-qa="resume-title"]')
|
||||||
console.log('Текущее резюме на странице:', currentResumeTitle)
|
const currentResumeTitle = (await currentResumeEl?.innerText())?.trim() ?? ''
|
||||||
console.log('Ожидаемое резюме из БД:', resume.title)
|
console.log('Текущее резюме на странице:', currentResumeTitle)
|
||||||
|
console.log('Ожидаемое резюме из БД:', resume.title)
|
||||||
|
|
||||||
if (currentResumeTitle !== resume.title) {
|
if (currentResumeTitle !== resume.title) {
|
||||||
console.log('Резюме не совпадает, нужно сменить')
|
console.log('Резюме не совпадает, нужно сменить')
|
||||||
await currentResumeEl?.click()
|
await currentResumeEl?.click()
|
||||||
await page.waitForSelector('[data-qa="magritte-select-option-list"]', { timeout: 5000 })
|
await page.waitForSelector('[data-qa="magritte-select-option-list"]', { timeout: 5000 })
|
||||||
await page.pause()
|
await page.pause()
|
||||||
const options = await page.$$('label[role="option"]')
|
const options = await page.$$('label[role="option"]')
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
const titleEl = await option.$('[data-qa="resume-title"] [data-qa="cell-text-content"]')
|
const titleEl = await option.$('[data-qa="resume-title"] [data-qa="cell-text-content"]')
|
||||||
const title = (await titleEl?.innerText())?.trim()
|
const title = (await titleEl?.innerText())?.trim()
|
||||||
if (title === resume.title) {
|
if (title === resume.title) {
|
||||||
await option.click()
|
await option.click()
|
||||||
await page.waitForTimeout(randomDelay())
|
await page.waitForTimeout(randomDelay())
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
await page.pause()
|
|
||||||
|
|
||||||
const addLetter = await page.$('[data-qa="add-cover-letter"]')
|
|
||||||
|
|
||||||
if (addLetter) {
|
|
||||||
await addLetter?.hover()
|
|
||||||
await addLetter?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const letter = await letterPromise
|
|
||||||
await keep(`✅ <b>${vacancy.title}</b>\n\n${letter}`)
|
|
||||||
|
|
||||||
if (letter) {
|
|
||||||
const letterInput = await page.$('[data-qa="vacancy-response-popup-form-letter-input"]')
|
|
||||||
|
|
||||||
await letterInput?.click()
|
|
||||||
await letterInput?.fill(letter)
|
|
||||||
await page.pause()
|
await page.pause()
|
||||||
}
|
|
||||||
|
|
||||||
const submitBtn = await page.$('[data-qa="vacancy-response-submit-popup"]')// vacancy-response-popup-submit
|
const addLetter = await page.$('[data-qa="add-cover-letter"]')
|
||||||
if (submitBtn) {
|
|
||||||
await submitBtn.click()
|
if (addLetter) {
|
||||||
await page.waitForTimeout(randomDelay())
|
await addLetter?.hover()
|
||||||
|
await addLetter?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const letter = await letterPromise
|
||||||
|
await keep(`✅ <b>${escapeHtml(vacancy.title)}</b>\n\n${escapeHtml(letter)}`)
|
||||||
|
|
||||||
|
if (letter) {
|
||||||
|
const letterInput = await page.$('[data-qa="vacancy-response-popup-form-letter-input"]')
|
||||||
|
|
||||||
|
await letterInput?.click()
|
||||||
|
await letterInput?.fill(letter)
|
||||||
|
await page.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitBtn = 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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const errMsg = 'Not found submit button'
|
console.log('single flow')
|
||||||
console.log(errMsg)
|
|
||||||
results.errors.push({ ...ref, message: errMsg })
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user