mirror of
https://github.com/hempyhemp/hh-auto-reply.git
synced 2026-06-08 18:04:57 +00:00
🚀 feature(file): Реализована функциональность "onboarding" для нового пользователя.
All checks were successful
Deploy / deploy (push) Successful in 50s
All checks were successful
Deploy / deploy (push) Successful in 50s
This commit is contained in:
@@ -7,6 +7,7 @@ import { handleApply } from './handlers/apply.js'
|
|||||||
import { handleStatus, handleSkipped } from './handlers/info.js'
|
import { handleStatus, handleSkipped } from './handlers/info.js'
|
||||||
import { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.js'
|
import { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.js'
|
||||||
import { DEFAULT_PROMPT, handleAutoToggle, handleMax, handlePrompt, handleQuery } from './handlers/settings.js'
|
import { DEFAULT_PROMPT, handleAutoToggle, handleMax, handlePrompt, handleQuery } from './handlers/settings.js'
|
||||||
|
import { finishOnboarding, showPromptStep, showQueryStep, showResumeInfo } from './handlers/onboarding.js'
|
||||||
|
|
||||||
type MsgHandler = (chatId: number) => Promise<void>
|
type MsgHandler = (chatId: number) => Promise<void>
|
||||||
type CallbackHandler = (chatId: number, messageId: number) => Promise<void>
|
type CallbackHandler = (chatId: number, messageId: number) => Promise<void>
|
||||||
@@ -75,6 +76,28 @@ const CALLBACK_HANDLERS: Record<string, CallbackHandler> = {
|
|||||||
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||||
await bot.sendMessage(chatId, '✅ Промт сброшен на дефолтный')
|
await bot.sendMessage(chatId, '✅ Промт сброшен на дефолтный')
|
||||||
},
|
},
|
||||||
|
ob_skip_max: async (chatId, messageId) => {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = null
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||||
|
await showQueryStep(chatId)
|
||||||
|
},
|
||||||
|
ob_skip_query: async (chatId, messageId) => {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = null
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||||
|
await showResumeInfo(chatId)
|
||||||
|
await showPromptStep(chatId)
|
||||||
|
},
|
||||||
|
ob_skip_prompt: async (chatId, messageId) => {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = null
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||||
|
await finishOnboarding(chatId)
|
||||||
|
},
|
||||||
hh_resume_list: async (chatId, messageId) => {
|
hh_resume_list: async (chatId, messageId) => {
|
||||||
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||||
await handleResumeList(chatId)
|
await handleResumeList(chatId)
|
||||||
@@ -88,11 +111,14 @@ async function clearAwaitingState(chatId: number): Promise<void> {
|
|||||||
state.queryPromptMessageId,
|
state.queryPromptMessageId,
|
||||||
state.maxPromptMessageId,
|
state.maxPromptMessageId,
|
||||||
state.promptPromptMessageId,
|
state.promptPromptMessageId,
|
||||||
|
state.onboardingMsgId,
|
||||||
]
|
]
|
||||||
state.awaitingEmail = false
|
state.awaitingEmail = false
|
||||||
state.awaitingQuery = false
|
state.awaitingQuery = false
|
||||||
state.awaitingMax = false
|
state.awaitingMax = false
|
||||||
state.awaitingPrompt = false
|
state.awaitingPrompt = false
|
||||||
|
state.onboardingStep = null
|
||||||
|
state.onboardingMsgId = null
|
||||||
state.loginPromptMessageId = null
|
state.loginPromptMessageId = null
|
||||||
state.queryPromptMessageId = null
|
state.queryPromptMessageId = null
|
||||||
state.maxPromptMessageId = null
|
state.maxPromptMessageId = null
|
||||||
@@ -142,13 +168,62 @@ export function registerHHCommands() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAwaiting = state.awaitingEmail || state.awaitingQuery || state.awaitingMax || state.awaitingPrompt
|
const isAwaiting = state.awaitingEmail || state.awaitingQuery || state.awaitingMax || state.awaitingPrompt || state.onboardingStep !== null
|
||||||
const isMenuButton = Object.values(BTN).includes(msg.text as typeof BTN[keyof typeof BTN])
|
const isMenuButton = Object.values(BTN).includes(msg.text as typeof BTN[keyof typeof BTN])
|
||||||
|
|
||||||
if (isMenuButton && isAwaiting) {
|
if (isMenuButton && isAwaiting) {
|
||||||
await clearAwaitingState(chatId)
|
await clearAwaitingState(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.onboardingStep === 'max') {
|
||||||
|
const num = Number(msg.text)
|
||||||
|
if (Number.isNaN(num) || num < 1 || num > 50) {
|
||||||
|
await bot.sendMessage(chatId, '❌ Введи число от 1 до 50:')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.onboardingStep = null
|
||||||
|
if (state.onboardingMsgId) {
|
||||||
|
await bot.deleteMessage(chatId, state.onboardingMsgId).catch(() => {})
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
}
|
||||||
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
|
await prisma.settings.update({ where: { telegramId: chatId }, data: { maxApplies: num } })
|
||||||
|
await bot.sendMessage(chatId, `✅ Макс откликов: ${num}`)
|
||||||
|
await showQueryStep(chatId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.onboardingStep === 'query') {
|
||||||
|
state.onboardingStep = null
|
||||||
|
if (state.onboardingMsgId) {
|
||||||
|
await bot.deleteMessage(chatId, state.onboardingMsgId).catch(() => {})
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
}
|
||||||
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
|
const updated = await prisma.settings.update({ where: { telegramId: chatId }, data: { searchQuery: msg.text } })
|
||||||
|
await bot.sendMessage(chatId, `✅ Запрос: «${updated.searchQuery}»`)
|
||||||
|
await showResumeInfo(chatId)
|
||||||
|
await showPromptStep(chatId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.onboardingStep === 'prompt') {
|
||||||
|
state.onboardingStep = null
|
||||||
|
if (state.onboardingMsgId) {
|
||||||
|
await bot.deleteMessage(chatId, state.onboardingMsgId).catch(() => {})
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
}
|
||||||
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { telegramId: chatId },
|
||||||
|
update: { prompt: msg.text },
|
||||||
|
create: { telegramId: chatId, prompt: msg.text, Settings: { create: {} } },
|
||||||
|
})
|
||||||
|
await bot.sendMessage(chatId, '✅ Промт сохранён')
|
||||||
|
await finishOnboarding(chatId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (state.awaitingEmail) {
|
if (state.awaitingEmail) {
|
||||||
state.awaitingEmail = false
|
state.awaitingEmail = false
|
||||||
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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, saveResume } from '../scraper.js'
|
||||||
import { MAIN_REPLY_KEYBOARD } from '../ui.js'
|
|
||||||
import { getState } from '../state.js'
|
import { getState } from '../state.js'
|
||||||
|
import { startOnboarding } from './onboarding.js'
|
||||||
import type { ResumeListItem } from '../types.js'
|
import type { ResumeListItem } from '../types.js'
|
||||||
|
|
||||||
export async function doLogin(chatId: number, email: string): Promise<void> {
|
export async function doLogin(chatId: number, email: string): Promise<void> {
|
||||||
@@ -25,20 +25,21 @@ export async function doLogin(chatId: number, email: string): Promise<void> {
|
|||||||
await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню')
|
await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню')
|
||||||
}
|
}
|
||||||
|
|
||||||
await bot.sendMessage(chatId, '✅ Вход выполнен!', { reply_markup: MAIN_REPLY_KEYBOARD })
|
await bot.sendMessage(chatId, '✅ Вход выполнен!')
|
||||||
|
|
||||||
if (resumes === null) {
|
if (resumes === null || resumes.length === 0) {
|
||||||
// таймаут при загрузке резюме
|
if (resumes?.length === 0)
|
||||||
}
|
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
|
||||||
else if (resumes.length === 0) {
|
await startOnboarding(chatId)
|
||||||
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
|
|
||||||
}
|
}
|
||||||
else if (resumes.length === 1) {
|
else if (resumes.length === 1) {
|
||||||
await saveResume(chatId, resumes[0])
|
await saveResume(chatId, resumes[0])
|
||||||
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
||||||
|
await startOnboarding(chatId)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.pendingResumes = resumes
|
state.pendingResumes = resumes
|
||||||
|
state.onboardingAfterResume = true
|
||||||
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
|
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
|
|||||||
120
src/hh/handlers/onboarding.ts
Normal file
120
src/hh/handlers/onboarding.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import bot from '@bot'
|
||||||
|
import prisma from '@prisma'
|
||||||
|
import { getState } from '../state.js'
|
||||||
|
import { escapeHtml, MAIN_REPLY_KEYBOARD } from '../ui.js'
|
||||||
|
import { DEFAULT_PROMPT } from './settings.js'
|
||||||
|
|
||||||
|
export async function startOnboarding(chatId: number): Promise<void> {
|
||||||
|
await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`👋 <b>Давай настроим бота</b> — займёт меньше минуты.\n\nПройдём по двум ключевым параметрам.`,
|
||||||
|
{ parse_mode: 'HTML' },
|
||||||
|
)
|
||||||
|
await showMaxStep(chatId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showMaxStep(chatId: number): Promise<void> {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = 'max'
|
||||||
|
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
|
||||||
|
const current = settings?.maxApplies ?? 1
|
||||||
|
|
||||||
|
const msg = await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`🔢 <b>Шаг 1 из 3 — Максимум откликов</b>\n\n`
|
||||||
|
+ `Сколько вакансий бот обработает за один запуск. Рекомендуем начать с небольшого числа, чтобы проверить письма.\n\n`
|
||||||
|
+ `Текущее значение: <b>${current}</b>\n\n`
|
||||||
|
+ `Введи число от 1 до 50:`,
|
||||||
|
{
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: `Оставить ${current} ✓`, callback_data: 'ob_skip_max' },
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
state.onboardingMsgId = msg.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showQueryStep(chatId: number): Promise<void> {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = 'query'
|
||||||
|
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
|
||||||
|
const current = settings?.searchQuery || 'Vue'
|
||||||
|
|
||||||
|
const msg = await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`🔍 <b>Шаг 2 из 3 — Поисковый запрос</b>\n\n`
|
||||||
|
+ `По этому запросу бот ищет вакансии на hh.ru. Используй профессию или ключевые навыки.\n\n`
|
||||||
|
+ `Текущий запрос: <b>${current}</b>\n\n`
|
||||||
|
+ `Введи новый или оставь текущий:`,
|
||||||
|
{
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: `Оставить «${current}» ✓`, callback_data: 'ob_skip_query' },
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
state.onboardingMsgId = msg.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showResumeInfo(chatId: number): Promise<void> {
|
||||||
|
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
|
||||||
|
const resume = settings?.selectedResumeId
|
||||||
|
? await prisma.resume.findUnique({ where: { id: settings.selectedResumeId } })
|
||||||
|
: await prisma.resume.findFirst({ where: { telegramId: chatId } })
|
||||||
|
|
||||||
|
if (resume) {
|
||||||
|
await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`📄 <b>Резюме</b>\n\nАктивное резюме: <b>${resume.title}</b>\n\nЕсли у тебя несколько резюме на hh.ru — можно выбрать нужное через <i>Настройки → Выбрать резюме</i>.`,
|
||||||
|
{ parse_mode: 'HTML' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`📄 <b>Резюме</b>\n\nРезюме пока не выбрано. Создай его на hh.ru, затем выбери через <i>Настройки → Выбрать резюме</i>.`,
|
||||||
|
{ parse_mode: 'HTML' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showPromptStep(chatId: number): Promise<void> {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = 'prompt'
|
||||||
|
const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
|
||||||
|
const currentPrompt = user?.prompt || DEFAULT_PROMPT
|
||||||
|
|
||||||
|
const msg = await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`📝 <b>Шаг 3 из 3 — Промт для AI</b>\n\n`
|
||||||
|
+ `Инструкция, которую AI получает при написании сопроводительного письма. `
|
||||||
|
+ `Задаёт стиль, тон и то, что важно упомянуть.\n\n`
|
||||||
|
+ `<i>Текущий промт:</i>\n<pre>${escapeHtml(currentPrompt)}</pre>\n\n`
|
||||||
|
+ `Введи свой или оставь дефолтный:`,
|
||||||
|
{
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: 'Оставить дефолтный ✓', callback_data: 'ob_skip_prompt' },
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
state.onboardingMsgId = msg.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function finishOnboarding(chatId: number): Promise<void> {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.onboardingStep = null
|
||||||
|
state.onboardingMsgId = null
|
||||||
|
await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`🎉 <b>Настройка завершена!</b>\n\nВсё готово — нажми <b>🚀 Откликнуться</b>, чтобы запустить бота.\n\nДополнительные параметры (запрос, регион, слова-исключения) доступны в разделе <b>Фильтры</b>.`,
|
||||||
|
{ parse_mode: 'HTML', reply_markup: MAIN_REPLY_KEYBOARD },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import prisma from '@prisma'
|
|||||||
import { listResumes, NoResumeError, saveResume } from '../scraper.js'
|
import { listResumes, NoResumeError, saveResume } from '../scraper.js'
|
||||||
import { getState } from '../state.js'
|
import { getState } from '../state.js'
|
||||||
import { escapeHtml, NO_RESUME_MARKUP, safeEdit } from '../ui.js'
|
import { escapeHtml, NO_RESUME_MARKUP, safeEdit } from '../ui.js'
|
||||||
|
import { startOnboarding } from './onboarding.js'
|
||||||
|
|
||||||
const log = createLogger('resume')
|
const log = createLogger('resume')
|
||||||
|
|
||||||
@@ -97,6 +98,10 @@ export async function handleResumePick(chatId: number, messageId: number, idx: n
|
|||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: messageId,
|
message_id: messageId,
|
||||||
reply_markup: { inline_keyboard: [] },
|
reply_markup: { inline_keyboard: [] },
|
||||||
// reply_markup: BACK_MARKUP,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (state.onboardingAfterResume) {
|
||||||
|
state.onboardingAfterResume = false
|
||||||
|
await startOnboarding(chatId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export interface UserState {
|
|||||||
queryPromptMessageId: number | null
|
queryPromptMessageId: number | null
|
||||||
maxPromptMessageId: number | null
|
maxPromptMessageId: number | null
|
||||||
promptPromptMessageId: number | null
|
promptPromptMessageId: number | null
|
||||||
|
onboardingStep: 'max' | 'query' | 'prompt' | null
|
||||||
|
onboardingMsgId: number | null
|
||||||
|
onboardingAfterResume: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUserState(): UserState {
|
function makeUserState(): UserState {
|
||||||
@@ -28,6 +31,9 @@ function makeUserState(): UserState {
|
|||||||
queryPromptMessageId: null,
|
queryPromptMessageId: null,
|
||||||
maxPromptMessageId: null,
|
maxPromptMessageId: null,
|
||||||
promptPromptMessageId: null,
|
promptPromptMessageId: null,
|
||||||
|
onboardingStep: null,
|
||||||
|
onboardingMsgId: null,
|
||||||
|
onboardingAfterResume: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user