update
All checks were successful
Deploy / deploy (push) Successful in 3m2s

This commit is contained in:
Oscar
2026-06-01 12:00:41 +03:00
parent 199e58b251
commit 79ae47de71
5 changed files with 148 additions and 39 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "hhPhone" TEXT;

View File

@@ -23,6 +23,7 @@ model User {
createdAt DateTime @default(now())
session String?
hhEmail String?
hhPhone String?
resumes Resume[]
prompt String @default("Ты — помощник по написанию сопроводительных писем. Отвечай только текстом самого письма, без вступлений, ремарок и пояснений. Опирайся на резюме и ничего не выдумывай, чего недостаточно в резюме лучше умолчать. Пиши по короче и простыми словами. В конце письма оставляй все контакты для связи.")
Settings Settings?

View File

@@ -2,7 +2,7 @@ import bot from '@bot'
import prisma from '@prisma'
import { debugFunc } from '@/hh/handlers/debug'
import { handleApply } from './handlers/apply.js'
import { doLogin, handleLogin, handleLoginByEmail, handleLoginByPhone } from './handlers/auth.js'
import { doLogin, doLoginByPhone, handleLogin, handleLoginByEmail, handleLoginByPhone } from './handlers/auth.js'
import { handleSkipped, handleStatus } from './handlers/info.js'
import { finishOnboarding, showPromptStep, showQueryStep, showResumeInfo } from './handlers/onboarding.js'
import { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.js'
@@ -64,6 +64,19 @@ const CALLBACK_HANDLERS: Record<string, CallbackHandler> = {
}
await doLogin(chatId, user.hhEmail)
},
hh_login_use_current_phone: async (chatId, messageId) => {
const state = getState(chatId)
state.awaitingPhone = false
state.loginPromptMessageId = null
await bot.deleteMessage(chatId, messageId).catch(() => {})
const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
if (!user?.hhPhone) {
await bot.sendMessage(chatId, '❌ Телефон не найден, введи вручную')
state.awaitingPhone = true
return
}
await doLoginByPhone(chatId, user.hhPhone)
},
hh_keep_query: async (chatId, messageId) => {
const state = getState(chatId)
state.awaitingQuery = false
@@ -252,6 +265,17 @@ export function registerHHCommands() {
return
}
if (state.awaitingPhone) {
state.awaitingPhone = false
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
if (state.loginPromptMessageId) {
await bot.deleteMessage(chatId, state.loginPromptMessageId).catch(() => {})
state.loginPromptMessageId = null
}
await doLoginByPhone(chatId, msg.text)
return
}
if (state.awaitingQuery) {
state.awaitingQuery = false
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})

View File

@@ -1,10 +1,47 @@
import bot from '@bot'
import prisma from '@prisma'
import { listResumes, login, saveResume } from '../scraper.js'
import { listResumes, login, loginByPhone, saveResume } from '../scraper.js'
import { getState } from '../state.js'
import { startOnboarding } from './onboarding.js'
import type { ResumeListItem } from '../types.js'
async function handlePostLogin(chatId: number): Promise<void> {
const state = getState(chatId)
let resumes: ResumeListItem[] | null = null
try {
resumes = await listResumes(chatId)
}
catch {
await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню')
}
await bot.sendMessage(chatId, '✅ Вход выполнен!')
if (resumes === null || resumes.length === 0) {
if (resumes?.length === 0)
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
await startOnboarding(chatId)
}
else if (resumes.length === 1) {
await saveResume(chatId, resumes[0])
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
await startOnboarding(chatId)
}
else {
state.pendingResumes = resumes
state.onboardingAfterResume = true
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
reply_markup: {
inline_keyboard: [
...resumes.map((r, i) => [{ text: r.title, callback_data: `hh_resume_pick_${i}` }]),
[{ text: '◀️ Закрыть', callback_data: 'hh_back' }],
],
},
})
}
}
export async function doLogin(chatId: number, email: string): Promise<void> {
await bot.sendMessage(chatId, '🔄 Логинюсь...')
try {
@@ -14,41 +51,23 @@ export async function doLogin(chatId: number, email: string): Promise<void> {
update: { hhEmail: email },
create: { telegramId: chatId, hhEmail: email, Settings: { create: {} } },
})
await handlePostLogin(chatId)
}
catch (e) {
await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`)
}
}
const state = getState(chatId)
let resumes: ResumeListItem[] | null = null
try {
resumes = await listResumes(chatId)
}
catch {
await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню')
}
await bot.sendMessage(chatId, '✅ Вход выполнен!')
if (resumes === null || resumes.length === 0) {
if (resumes?.length === 0)
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
await startOnboarding(chatId)
}
else if (resumes.length === 1) {
await saveResume(chatId, resumes[0])
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
await startOnboarding(chatId)
}
else {
state.pendingResumes = resumes
state.onboardingAfterResume = true
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
reply_markup: {
inline_keyboard: [
...resumes.map((r, i) => [{ text: r.title, callback_data: `hh_resume_pick_${i}` }]),
[{ text: '◀️ Закрыть', callback_data: 'hh_back' }],
],
},
})
}
export async function doLoginByPhone(chatId: number, phone: string): Promise<void> {
await bot.sendMessage(chatId, '🔄 Логинюсь...')
try {
await loginByPhone(phone, chatId)
await prisma.user.upsert({
where: { telegramId: chatId },
update: { hhPhone: phone },
create: { telegramId: chatId, hhPhone: phone, Settings: { create: {} } },
})
await handlePostLogin(chatId)
}
catch (e) {
await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`)
@@ -94,5 +113,26 @@ export async function handleLoginByEmail(chatId: number): Promise<void> {
}
export async function handleLoginByPhone(chatId: number): Promise<void> {
await bot.sendMessage(chatId, '📱 Авторизация по телефону — скоро будет доступна')
const state = getState(chatId)
const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
state.awaitingPhone = true
if (!user?.hhPhone) {
await bot.sendMessage(chatId, '📱 Введи номер телефона (например: +79001234567):')
}
else {
const prompt = await bot.sendMessage(
chatId,
`📱 Текущий телефон: <b>${user.hhPhone}</b>\n\спользовать его или введи другой:`,
{
parse_mode: 'HTML',
reply_markup: {
inline_keyboard: [[
{ text: `✅ Войти как ${user.hhPhone}`, callback_data: 'hh_login_use_current_phone' },
]],
},
},
)
state.loginPromptMessageId = prompt.message_id
}
}

View File

@@ -4,9 +4,9 @@ import type { ApplyOptions, ApplyResult, ResumeListItem, VacancyRef } from './ty
import type { StatusReporter } from './ui.js'
import bot from '@bot'
import prisma from '@prisma'
import { createLogger } from '@/logger'
import { createMessage } from '@/openai'
import { loadSession, newStealthContext, randomDelay, randomScroll, withBrowser } from './browser.js'
import { createLogger } from '@/logger'
import { createStatusReporter, escapeHtml } from './ui.js'
const log = createLogger('scraper')
@@ -64,6 +64,48 @@ async function skipIfQuestionnaire(
return true
}
export async function loginByPhone(phone: string, chatId: number): Promise<void> {
await withBrowser(async (browser) => {
if (!browser.version()) {
log.error('browser error')
return
}
const context = await newStealthContext(browser)
const page = await context.newPage()
await page.goto('https://hh.ru/account/login', { waitUntil: 'domcontentloaded' })
await page.click('[data-qa="submit-button"]')
await page.waitForTimeout(randomDelay())
await page.fill('[data-qa="magritte-phone-input-national-number-input"]', phone)
await page.waitForTimeout(randomDelay())
await page.click('[data-qa="submit-button"]')
await page.waitForTimeout(randomDelay())
await bot.sendMessage(chatId, '🔑 Введи код из SMS')
await page.waitForTimeout(randomDelay())
await page.click('[data-qa="applicant-login-input-otp"]')
const otp = await waitForOtp(chatId)
await page.fill('[data-qa="applicant-login-input-otp"] input', otp)
await page.waitForTimeout(randomDelay())
await page.waitForSelector('[data-qa="profileAndResumes-button"]', { timeout: 15000 })
const cookies = await context.cookies()
await prisma.user.upsert({
where: { telegramId: chatId },
update: { session: JSON.stringify(cookies, null, 2) },
create: { telegramId: chatId, session: JSON.stringify(cookies, null, 2), Settings: { create: {} } },
})
await bot.sendMessage(chatId, cookies.length > 0 ? '✅ Авторизация выполнена' : '❌ Произошла ошибка')
})
}
export async function login(email: string, chatId: number): Promise<void> {
await withBrowser(async (browser) => {
if (!browser.version()) {