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 { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.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 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.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) => {
|
||||
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||
await handleResumeList(chatId)
|
||||
@@ -88,11 +111,14 @@ async function clearAwaitingState(chatId: number): Promise<void> {
|
||||
state.queryPromptMessageId,
|
||||
state.maxPromptMessageId,
|
||||
state.promptPromptMessageId,
|
||||
state.onboardingMsgId,
|
||||
]
|
||||
state.awaitingEmail = false
|
||||
state.awaitingQuery = false
|
||||
state.awaitingMax = false
|
||||
state.awaitingPrompt = false
|
||||
state.onboardingStep = null
|
||||
state.onboardingMsgId = null
|
||||
state.loginPromptMessageId = null
|
||||
state.queryPromptMessageId = null
|
||||
state.maxPromptMessageId = null
|
||||
@@ -142,13 +168,62 @@ export function registerHHCommands() {
|
||||
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])
|
||||
|
||||
if (isMenuButton && isAwaiting) {
|
||||
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) {
|
||||
state.awaitingEmail = false
|
||||
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import bot from '@bot'
|
||||
import prisma from '@prisma'
|
||||
import { listResumes, login, saveResume } from '../scraper.js'
|
||||
import { MAIN_REPLY_KEYBOARD } from '../ui.js'
|
||||
import { getState } from '../state.js'
|
||||
import { startOnboarding } from './onboarding.js'
|
||||
import type { ResumeListItem } from '../types.js'
|
||||
|
||||
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, '✅ Вход выполнен!', { reply_markup: MAIN_REPLY_KEYBOARD })
|
||||
await bot.sendMessage(chatId, '✅ Вход выполнен!')
|
||||
|
||||
if (resumes === null) {
|
||||
// таймаут при загрузке резюме
|
||||
}
|
||||
else if (resumes.length === 0) {
|
||||
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
|
||||
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: [
|
||||
|
||||
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 { getState } from '../state.js'
|
||||
import { escapeHtml, NO_RESUME_MARKUP, safeEdit } from '../ui.js'
|
||||
import { startOnboarding } from './onboarding.js'
|
||||
|
||||
const log = createLogger('resume')
|
||||
|
||||
@@ -97,6 +98,10 @@ export async function handleResumePick(chatId: number, messageId: number, idx: n
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
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
|
||||
maxPromptMessageId: number | null
|
||||
promptPromptMessageId: number | null
|
||||
onboardingStep: 'max' | 'query' | 'prompt' | null
|
||||
onboardingMsgId: number | null
|
||||
onboardingAfterResume: boolean
|
||||
}
|
||||
|
||||
function makeUserState(): UserState {
|
||||
@@ -28,6 +31,9 @@ function makeUserState(): UserState {
|
||||
queryPromptMessageId: null,
|
||||
maxPromptMessageId: null,
|
||||
promptPromptMessageId: null,
|
||||
onboardingStep: null,
|
||||
onboardingMsgId: null,
|
||||
onboardingAfterResume: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user