mirror of
https://github.com/hempyhemp/hh-auto-reply.git
synced 2026-06-08 18:04:57 +00:00
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "hhPhone" TEXT;
|
||||||
@@ -23,6 +23,7 @@ model User {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
session String?
|
session String?
|
||||||
hhEmail String?
|
hhEmail String?
|
||||||
|
hhPhone String?
|
||||||
resumes Resume[]
|
resumes Resume[]
|
||||||
prompt String @default("Ты — помощник по написанию сопроводительных писем. Отвечай только текстом самого письма, без вступлений, ремарок и пояснений. Опирайся на резюме и ничего не выдумывай, чего недостаточно в резюме лучше умолчать. Пиши по короче и простыми словами. В конце письма оставляй все контакты для связи.")
|
prompt String @default("Ты — помощник по написанию сопроводительных писем. Отвечай только текстом самого письма, без вступлений, ремарок и пояснений. Опирайся на резюме и ничего не выдумывай, чего недостаточно в резюме лучше умолчать. Пиши по короче и простыми словами. В конце письма оставляй все контакты для связи.")
|
||||||
Settings Settings?
|
Settings Settings?
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import bot from '@bot'
|
|||||||
import prisma from '@prisma'
|
import prisma from '@prisma'
|
||||||
import { debugFunc } from '@/hh/handlers/debug'
|
import { debugFunc } from '@/hh/handlers/debug'
|
||||||
import { handleApply } from './handlers/apply.js'
|
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 { handleSkipped, handleStatus } from './handlers/info.js'
|
||||||
import { finishOnboarding, showPromptStep, showQueryStep, showResumeInfo } from './handlers/onboarding.js'
|
import { finishOnboarding, showPromptStep, showQueryStep, showResumeInfo } from './handlers/onboarding.js'
|
||||||
import { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.js'
|
import { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.js'
|
||||||
@@ -64,6 +64,19 @@ const CALLBACK_HANDLERS: Record<string, CallbackHandler> = {
|
|||||||
}
|
}
|
||||||
await doLogin(chatId, user.hhEmail)
|
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) => {
|
hh_keep_query: async (chatId, messageId) => {
|
||||||
const state = getState(chatId)
|
const state = getState(chatId)
|
||||||
state.awaitingQuery = false
|
state.awaitingQuery = false
|
||||||
@@ -252,6 +265,17 @@ export function registerHHCommands() {
|
|||||||
return
|
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) {
|
if (state.awaitingQuery) {
|
||||||
state.awaitingQuery = false
|
state.awaitingQuery = false
|
||||||
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
|
|||||||
@@ -1,10 +1,47 @@
|
|||||||
import bot from '@bot'
|
import bot from '@bot'
|
||||||
import prisma from '@prisma'
|
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 { getState } from '../state.js'
|
||||||
import { startOnboarding } from './onboarding.js'
|
import { startOnboarding } from './onboarding.js'
|
||||||
import type { ResumeListItem } from '../types.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> {
|
export async function doLogin(chatId: number, email: string): Promise<void> {
|
||||||
await bot.sendMessage(chatId, '🔄 Логинюсь...')
|
await bot.sendMessage(chatId, '🔄 Логинюсь...')
|
||||||
try {
|
try {
|
||||||
@@ -14,41 +51,23 @@ export async function doLogin(chatId: number, email: string): Promise<void> {
|
|||||||
update: { hhEmail: email },
|
update: { hhEmail: email },
|
||||||
create: { telegramId: chatId, hhEmail: email, Settings: { create: {} } },
|
create: { telegramId: chatId, hhEmail: email, Settings: { create: {} } },
|
||||||
})
|
})
|
||||||
|
await handlePostLogin(chatId)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const state = getState(chatId)
|
export async function doLoginByPhone(chatId: number, phone: string): Promise<void> {
|
||||||
|
await bot.sendMessage(chatId, '🔄 Логинюсь...')
|
||||||
let resumes: ResumeListItem[] | null = null
|
try {
|
||||||
try {
|
await loginByPhone(phone, chatId)
|
||||||
resumes = await listResumes(chatId)
|
await prisma.user.upsert({
|
||||||
}
|
where: { telegramId: chatId },
|
||||||
catch {
|
update: { hhPhone: phone },
|
||||||
await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню')
|
create: { telegramId: chatId, hhPhone: phone, Settings: { create: {} } },
|
||||||
}
|
})
|
||||||
|
await handlePostLogin(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' }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`)
|
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> {
|
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\nИспользовать его или введи другой:`,
|
||||||
|
{
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: `✅ Войти как ${user.hhPhone}`, callback_data: 'hh_login_use_current_phone' },
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
state.loginPromptMessageId = prompt.message_id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ import type { ApplyOptions, ApplyResult, ResumeListItem, VacancyRef } from './ty
|
|||||||
import type { StatusReporter } from './ui.js'
|
import type { StatusReporter } from './ui.js'
|
||||||
import bot from '@bot'
|
import bot from '@bot'
|
||||||
import prisma from '@prisma'
|
import prisma from '@prisma'
|
||||||
|
import { createLogger } from '@/logger'
|
||||||
import { createMessage } from '@/openai'
|
import { createMessage } from '@/openai'
|
||||||
import { loadSession, newStealthContext, randomDelay, randomScroll, withBrowser } from './browser.js'
|
import { loadSession, newStealthContext, randomDelay, randomScroll, withBrowser } from './browser.js'
|
||||||
import { createLogger } from '@/logger'
|
|
||||||
import { createStatusReporter, escapeHtml } from './ui.js'
|
import { createStatusReporter, escapeHtml } from './ui.js'
|
||||||
|
|
||||||
const log = createLogger('scraper')
|
const log = createLogger('scraper')
|
||||||
@@ -64,6 +64,48 @@ async function skipIfQuestionnaire(
|
|||||||
return true
|
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> {
|
export async function login(email: string, chatId: number): Promise<void> {
|
||||||
await withBrowser(async (browser) => {
|
await withBrowser(async (browser) => {
|
||||||
if (!browser.version()) {
|
if (!browser.version()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user