Телеграм-бот продавец консультаций за 60 минут в приложении Cursor AI

В этой статье я расскажу вам как создать своего телеграм-бота-продавца консультаций за час, своими руками. Больше интересного в моем ТГ https://t.me/kursorai или @kursorai

Приходите на консультацию - сделаем телеграм-бота вместе, пишите в ТГ @starvee

Зачем это вам
  • сделайте бота прямо сейчас - без длинных курсов;

  • все расписано в 6 шагов, гайд подойдет полным новичкам

  • готовые промты для CursorAI, просто вставляете и получаете результат

К концу часа у вас будет
  • рабочий телеграм-бот: пользовательский интерфейс, админ панель, возможность редактирования текстов внутри бота

  • сбор лидов: контакт пользователя и падающая заявка в ваш личный телеграм

  • базовое понимание работы телеграм бота и платформа для дальнейшего развития

  • понимание как установить бота на сервер и держать его онлайн

Маршрут на 60 минут
1
Подготовка
2
BotFather
3
Базовый бот
4
Доработка
5
Тесты
6
Деплой
Первый этап - Подготовка
  • Устанавливаете Cursor AI

  • Регистрируетесь, для новых пользователь Cursor AI дает пробный период 150 запоров или 2 недели, далее пользование платное 20$ в месяц

  • Переходите в приложение и создаете пустую папку проекта

1. Установка
Установка CursorAI
2. Регистрация
Регистрация в CursorAI
3.Открываем приложение
Открытие приложения CursorAI
4.Создаем новую папку
Как создать папку в CursorAI
5.Должно открыться такое окно
Главное окно Cursor
Второй этап - BotFather
  • Заходим в Telegram и ищем @botfather или проходим 
по ссылке

  • Открываем приложение и жмем Create a New Bot

  • Задаем имя боту и уникальный username

  • Копируем API ключ для подключения бота к проекту в Cursor

1.Открываем приложение
Как открыть BotFather
2.Создаем нового бота
Как создать нового ТГ бота
3.Придумываем username
Username telegram
4.Получаем API токен
Telegram API токен
Третий этап - базовый бот
  • Скачиваем Node.js по ссылке

  • Закрываем и открываем приложение Cursor AI (чтобы Node.js подгрузился)

  • Проверяем установку Node.js, открываем терминал Ctrl + ` и вводим команды node -v и npm -v

  • Если увидели вывод версий в терминале, то все работает

курсор@mac — 80 x 24
ivan@MacBook-Pro-3 лидмагниттест % node -v
v23.11.0
ivan@MacBook-Pro-3 лидмагниттест % npm -v
10.9.2
Первый промт
Собери Telegram-бота консультаций на TypeScript (ESM, strict) + Telegraf v4 + Luxon + Zod. Без БД — память + JSON.
‼️ В src допускаются ТОЛЬКО .ts. Любые .js/.mjs/.cjs в src — ошибка, немедленно мигрируй в .ts.
parse_mode — только "HTML". Throttle — 300 мс на входящие. Никаких искусственных задержек/typing.
.ENV — создать строго ".env" и грузить dotenv.config({ path: '.env' }):
BOT_TOKEN=
ADMIN_ID=0
LOG_CHAT_ID=0
TIMEZONE=Europe/Moscow
LOCALE=ru-RU
CURRENCY=RUB
Если BOT_TOKEN пуст — выведи «BOT_TOKEN не задан в .env» и process.exit(1) БЕЗ стектрейса.
package.json — скрипты:
dev="tsx watch src/core/index.ts"
build="tsc"
start="node dist/core/index.js"
tsconfig — ESNext, strict, node resolution, resolveJsonModule, verbatimModuleSyntax, outDir=dist.
Структура .ts:
src/core/{index.ts,bot.ts,config.ts}
src/state/session.ts
src/logic/{slots.ts,booking.ts}
src/ui/{keyboards.ts,texts.ts}
src/utils/{time.ts,files.ts,html.ts}
data/{slots.json,bookings.json,messages.json,sessions.json}
Гарантированная генерация слотов
– Реализуй ensureSlots({minFree=12, seedDays=14, maxDays=28, hours=[11,15,19]}) в logic/slots.ts.
– Алгоритм:
nowTZ=DateTime.now().setZone(TIMEZONE).
Сгенерируй ISO-слоты на seedDays вперёд по hours, только > nowTZ, дедуп, сортировка.
Пометь занятые по bookings.json, оставь свободные.
Если свободных < minFree → добавляй дни партиями до maxDays, пока свободных ≥ minFree.
Сохрани в data/slots.json. На каждом запуске выполняй ensureSlots().
– В лог на старте вывести ровно одну строку:
📅 Slots ready: free=<N> total=<T> window=<firstDate>…<lastDate>
– Если после maxDays свободных по-прежнему 0 — добавь ещё один день (fallback) и обязательно создай хотя бы 3 слота завтра в 11:00/15:00/19:00.
Меню и «2 сообщения»
– setMyCommands: /start, /menu, /book, /my, /help.
– setChatMenuButton({ type:'commands' }).
– Сообщение A (статичное, НЕ редактировать):
«Привет! Я бот Аникеева Ивана, вайбкодера. Продаю его консультации по созданию ИТ-продуктов. Сайт – kursorai.ru, канал – https://t.me/kursorai»
– Сообщение B — «живой» экран. upsertLive(ctx, html, kb): editMessageText → при 400/403 sendMessage; запомни liveMsgId в сессии.
Флоу пользователя (хэппи-путь, всё в P1)
Главное меню (state=main)
– Текст: «🏠 Главное меню\nВыберите действие:»
– Кнопки: «🗓 Записаться», «🔍 Подробнее», «📌 Мои записи», «◀ Назад», «🏠 Меню».
Подробнее
– Показать messages.about_html (дефолт ниже) + CTA «🗓 Записаться».
Календарь (2 шага)
– Шаг A (state=pick_day): 7 ближайших дней, где есть ≥1 свободный слот. Кнопки day:YYYY-MM-DD + «← Неделя / Неделя →». Низ: «◀ Назад», «🏠 Меню».
– Шаг B (state=pick_time): 2–5 свободных слотов одного дня, без дублей, отсортированы. Кнопки time:. Низ: «← Назад к дням».
Контакт — отдельное сообщение C (sendMessage, не edit)
– Текст: «📞 Как с вами связаться?\nВыберите удобный способ ниже.»
– Кнопки:
• inline «Использовать @username» — если есть ctx.from.username → фиксируем contact.
• reply-кнопка { text:'Отправить телефон', request_contact:true, one_time_keyboard:true, resize_keyboard:true }.
• inline «Ввести другой контакт» — только тогда примем следующий text как контакт.
– bot.on('contact') — сохраняем phone_number (нормализуй +7/8), убираем клавиатуру remove_keyboard.
– Как только контакт получен — отредактируй C на «✅ Контакт получен: …», очисти кнопки, и сразу переход к «Запросу».
Запрос — отдельное сообщение D + одно напоминание 30с
– Текст: «✍️ Кратко опишите запрос на консультацию\nНапишите сообщение ниже. Если сложно — нажмите «Пропустить».»
– Кнопки: «Пропустить», «◀ Назад».
– state='awaiting_request'. Один setTimeout(30с) → «⏳ Жду ваш запрос…». Очисти на ввод/«Пропустить».
– После ввода/пропуска — редактируй D на «✅ Запрос получен» (кнопки убрать) и сразу показывай Подтверждение.
Подтверждение (state=confirm) — на «живом» экране
– Текст: «Проверьте данные\n📅 Дата: …\n🕐 Время: …\n📞 Контакт: …\n📝 Запрос: …»
– Кнопки: «✅ Подтвердить», «✖ Отмена», «◀ Назад», «🏠 Меню».
Создание брони (без админки, уведомим позже в P2)
– На «✅»: перечитай файлы; если слот занят → предложи 2–3 альтернативы; иначе:
• slot.taken=true;
• booking = {
id:booking_${Date.now()}_${ctx.from.id}, state:'pending',
userId:ctx.from.id, username:ctx.from.username||null,
chatId:ctx.chat?.id ?? ctx.from.id, contact, slotISO, summary, ts:Date.now()
};
• сохрани; пользователю отдельное подтверждение:
«✅ Запись создана\n<ДД.ММ, ЧЧ:ММ>\nЧто дальше: мы подтвердим время и пришлём ссылку.»
Кнопки: «🏠 Меню», «📌 Мои записи».
Мои записи
– /my и кнопка показывают ближайшую активную запись по userId (slotISO>now, !cancelledAt, state∈{'pending','confirmed'}).
– Кнопки: «Изменить время» (возврат в календарь и перенос), «Отменить» (Да/Нет), «◀ Назад».
Дефолтные тексты
about_html:
🔍 Подробнее о консультации\n\nИван Аникеев — вайбкодер, делаю сайты, ботов, быстрые автоматизации, VBA-макросы и прочие сложные ИТ-продукты. Создадим прототип ИТ-продукта прямо на консультации.\n\nНа консультации вы получите:\n1) Понимание, как дальше развивать продукт\n2) Как работать в Cursor\n3) Предложение по формату 1-на-1: от идеи до результата\n\n💡 Формат: 1 час онлайн-встречи\n💰 Стоимость: 7500 ₽
Проверка P1
– При старте в консоли есть строка 📅 Slots ready: ... и свободных ≥12.
– /start → A + B. «Записаться» → День → Время → C (контакт) → D (запрос+напоминание) → Подтверждение → запись.
– /my показывает созданную запись. Телефон ловится через contact.
  • Копируем промт из окна выше, если с этим проблемы можно копировать отсюда, Cursor начинает создавать файлы проекта, мы подтверждаем каждое изменение

Если не хотим ничего подтверждать, включаем auto-run
как включить автоматический режим в CursorAI
  • После того, как курсор создал файлы проекта нужно выоплнить команду в терминале npm install, в окне вы увидите такое

курсор@mac — 80 x 24
ivan@MacBook-Pro-3 лидмагниттест % npm install

added 27 packages, and audited 28 packages in 19s

4 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  • Потом создаем файл .env, если курсор не создал его через терминал

Просто копируем параметры из env.example в .env
env в CursorAI
  • В файле .env заполняем наш BOT_TOKEN и ADMIN_ID и сохраняем файл, значения должны выглядеть так:


  • **BOT_TOKEN**=234567890:AAE1b2C3d4E5f6G7h8I9jK0l1mNoPqRsTu - пример токена, данные берем из BOTFATHER, см. скрин 4, этап 2
    
  • **ADMIN_ID**=20973894 - зайдите в [@userinfobot](https://t.me/userinfobot) и он вам выдаст эту информацию
    
  • Осталось только запустить бота командой в терминале npm run dev

  • После запуска переходите в телеграм и заходите в своего бота, username берите из botfather, нажимаете команду /start и тестируете работоспособность

  • Должно работать меню пользователя и запись на консультацию

⚠️
Внимание
красный крестик

В ходе работы над проектом могут возникнуть ошибки, например:

  • Бот может не запускаться;

  • Курсор может некорректно написать или не дописать код;

  • Команды в терминале могут не работать.

зеленая галочка

Алгоритм действий:

  • Пробуем написать в чат курсора и попросить его исправить ошибку, копируем и вставляем текст ошибки из терминала в чат курсора;

  • Если это не помогло, еще раз внимательно изучаем этапы 2 и 3 и пробудем повторить шаги;

  • Если и это не помогло, создаем чистый проект и пробуем этапы 2 и 3 заново.

Четвертый этап - доработка
  • После того, как мы проверили работоспособность первого промта - остается сделать финальные доработки

  • Сначала копируем промт 2, вставляем в Cursor и ждем финального результата, если не удобно копировать из окна ниже, можно копировать отсюда

Второй промт
P2 — Уведомление админу + кнопки ✅/❌/✉ (идемпотентно) + /whoami**
Продолжи проект из P1. Ничего не ломай. Всё по-прежнему .ts.
Устойчивые уведомления
– initNotifyRoute(telegram,{adminId?,logChatId?}) → "admin"|"log"|"none":
Тихий ping ADMIN_ID (sendMessage('Bot ready'), тут же удаляй); если 400/403 — пробуй LOG_CHAT_ID; если тоже нельзя — "none". Ничего не бросай.
– safeAdminNotify(route, html, inline?) — try/catch, ошибки только console.warn.
Шаблон уведомления о новой записи (HTML, экранируй значения)
– Заголовок «🆕 Новая запись»
– Поля: Имя/ID, @username, Дата, Время, Контакт, Запрос, ID брони.
– Инлайн-кнопки:
• «✅ Подтвердить» → callback "admin:ok:"
• «❌ Отклонить» → "admin:no:"
• «✉ Ответить» → "admin:reply:"
• «≡ Админ» → "admin:dash"
Идемпотентные обработчики
– Доступ только ADMIN_ID — иначе answerCallbackQuery('Только админ').
– booking.state ∈ {'pending','confirmed','rejected'}.
– admin:ok:
• если state!'pending' → answerCallbackQuery('Уже: '+state), отключи клавиатуру сообщения (editMessageReplyMarkup([])).
• иначе: state='confirmed'; сохранить; пользователю sendMessage
«✅ Ваша запись подтверждена\n<дата/время>» (+ кнопка «📌 Мои записи»);
у админа — отредактируй сообщение «✅ Подтверждено» и убери клавиатуру.
– admin:no:
• если state!'pending' → как выше.
• иначе: state='rejected'; освободить слот; сохранить; пользователю sendMessage
«❌ Запись отклонена\nВыберите другое время (кнопка «🗓 Записаться»)»; у админа — «❌ Отклонено», клавиатуру убрать.
– admin:reply: — следующий текст админа отправить пользователю; «Отправлено», вернуть меню.
– admin:dash — пока просто выведи краткую сводку в одном сообщении: свободные/всего за 7 дней, количество активных записей (сегодня/неделя/всего). Кнопка «◀ Назад».
Сервис
– /whoami — верни ctx.from.id и ctx.chat.id.
Проверка P2
– После брони админу приходит карточка с рабочими кнопками.
– Повторные нажатия по ✅/❌ корректно отвечают «Уже: ...», без ошибок, клавиатура отключается.
– Пользователь получает подтверждение/отклонение.
– /whoami работает.
  • Cursor начинает создавать новые файлы

  • По итогам второго промта должны быть реализованы уведомление админу и подтверждение записи,а также функционал подтверждения и отклонения записи

  • Далее переходим к финальному промту

Третий промт
P3 — финал с фиксамии: кнопка админа, понятная инструкция слотов, алерт админу при отмене
Продолжи проект из P2. Важно:
Только .ts в src, ESM, импорты с суффиксом .js, типы через import type.
parse_mode — только "HTML" для системных сообщений. Пользовательские тексты — plain text.
Все админ-хендлеры регистрируй ПЕРЕД bot.on('text')/bot.on('contact').
Любой callback завершаем ctx.answerCbQuery().
Цели P3 (оставляем как в P2 + фиксы):

«≡ Админ» и /admin — единое админ-сообщение (upsert). Фикс: кнопка админа показывается всегда админу.
«Слоты» — управление неделей и днями. Добавление слотов ТОЛЬКО через «Добавить текстом» на экране недели.
В дне — только открыть/закрыть/удалить существующие (без добавления).
Фикс: в инструкции явно описан формат ввода.
«Записи» — список ближайших, действия ✅/❌/✉/⤴ и полный просмотр карточки.
«Тексты» — редактор без HTML: welcome_text, about_text, next_steps_text и др., мгновенное применение.
Фикс: при отмене записи пользователем админ получает уведомление.
Структура/файлы, которые нужно ДОБАВИТЬ/ОБНОВИТЬ:

src/admin/ui.ts — singleton AdminUI с upsert одного сообщения админки.
src/logic/slots.ts — хелперы слотов (загрузка/сохранение/группировка/операции/парсер формата).
src/logic/booking.ts — список/поиск/обновление/перенос/отмена.
data/messages.json — добавить ключи текстов (если нет).
Регистрация экшенов admin в src/core/index.ts ДО общих on('...').
Базовый роутинг и единое сообщение
Префикс админ-callback: "adm:*".
isAdmin(ctx): const A = Number(process.env.ADMIN_ID||0); return A>0 && ctx.from?.id===A;
На любой "adm:*" от не-админа → answerCbQuery('Только админ') и return.
Гарантия кнопки админа. Сделай вспомогательную функцию:
function withAdminBtn(ctx, kb){ return isAdmin(ctx) ? [...kb, [{ text:'≡ Админ', callback_data:'adm:root' }]] : kb }
Применяй её ко всем пользовательским экранам (главное меню, календарь, подтверждение и т.д.).
setMyCommands:
/start, /menu, /book, /my, /help
Если isAdmin(ctx) при /start — дополнительно зарегистрируй /admin для этого чата.
/admin (только админ) и action 'adm:root' → renderAdminRoot():
HTML: "Админ-панель\nВыберите раздел."
Кнопки (2 колонки):
[Слоты][Записи]
[Тексты][Настройки]
[◀ Назад]
AdminUI (src/admin/ui.ts):
class AdminUI { chatId=ADMIN_ID; msgId?:number; filterFreeOnly=false; weekOffset=0;
async upsert(bot, html, reply_markup) {
// если есть msgId → editMessageText; ошибки 400/403 "can't be edited"/"not found" → sendMessage и запомнить msgId
// 400 "message is not modified" — аккуратно игнорировать (не падать)
}
}
Редактор текстов — plain text (как в прошлой версии)
data/messages.json — добавить ключи при отсутствии:
welcome_text, about_text, next_steps_text, reminderOneHour, reminderSoon, cancelAsk, cancelDone.
Пользовательские экраны каждый раз читают свежий messages.json.
Раздел «Тексты»:
adm:texts → список: " — 👁 Предпросмотр • ✏ Изменить", низ [◀ Назад].
adm:t:view: → первые 300 символов + [◀ Назад].
adm:t:edit: → session.adminEdit={key}; в ЛС админу: "Редактирование . Пришлите новый текст."
bot.on('text'): если это админ и adminEdit активен → превью «До/После» + [💾 Сохранить|adm:t:save] [↩ Отменить|adm:t:cancel]
adm:t:save → записать, сбросить, "✅ Сохранено", вернуться к adm:texts.
adm:t:cancel → сброс, "Отменено", adm:texts.
Слоты — только «Добавить текстом» (неделя); в дне — без добавления
Типы/утилиты (src/logic/slots.ts):
export type Slot = { iso:string; taken?:boolean; closed?:boolean }
loadSlots()/saveSlots(slots) — сортировать по iso перед сохранением.
slotsByDateTZ(slots, tz): Map<'YYYY-MM-DD', Slot[]>, сортировка по времени.
safeLabelDate(dateISO, tz) → "dd.MM (EEE)" (locale 'ru'), без "Invalid DateTime".
daySummary(daySlots, tz, nowTZ=DateTime.now().setZone(tz)) → { free, total } где free = !taken && !closed && time>nowTZ.
toISO(dateISO:'YYYY-MM-DD', hhmm:'HH:mm', tz) → string.
isFuture(iso, tz): boolean.
addSlot(dateISO, hhmm, tz) → { ok:true } | { ok:false; reason:'duplicate'|'past'|'invalid' }
toggleSlot(dateISO, hhmm, tz, toClosed:boolean) → { ok:true } | { ok:false; reason:'notfound' }
deleteSlot(dateISO, hhmm, tz) → { ok:true } | { ok:false; reason:'taken'|'notfound' }
Экран «Слоты» — неделя (adm:slots)
Верх:
[➕ Добавить текстом|adm:slots:addtext] [Только свободные: ON/OFF|adm:slots:toggle]
Навигация:
[⟵ Неделя|adm:slots:prev] [Сегодня|adm:slots:today] [Неделя ⟶|adm:slots:next]
Заголовок: "Неделя DD.MM–DD.MM".
Список дней:
"ДД.MM (EEE) — свободно {free} / всего {total}" + [▶ День|adm:day:YYYY-MM-DD]
(если filterFreeOnly=ON — скрывать дни с free=0)
Низ:
[🔄 Обновить|adm:slots] [◀ Назад|adm:root]
Глобальное добавление слотов — adm:slots:addtext (Фикс: явный формат)

session.adminAddRule=true.
В ЛС админу отправь инструкцию (plain text, без HTML):
Добавление слотов (общий формат ввода):
ФОРМАТ 1 — один день: ДД.ММ.ГГГГ ЧЧ:ММ
ФОРМАТ 2 — один день, несколько времен: ДД.ММ.ГГГГ ЧЧ:ММ, ЧЧ:ММ, ...
ФОРМАТ 3 — диапазон дней (каждый день): ДД.ММ.ГГГГ-ДД.ММ.ГГГГ ЧЧ:ММ
ФОРМАТ 4 — диапазон + несколько времен: ДД.ММ.ГГГГ-ДД.ММ.ГГГГ ЧЧ:ММ, ЧЧ:ММ, ...
Обязательно ставим короткое тире

Примеры:
• 20.09.2025 18:00
• 20.09.2025 10:00, 14:30, 19:00
• 20.09.2025–25.09.2025 18:00
• 20.09.2025–25.09.2025 11:00, 15:00, 19:00

Правила:
• Прошедшее время — пропускаем.
• Дубликаты — пропускаем.
• Ошибочные строки — считаем как invalid.
Отмена режима — /cancel

Следующий текст от админа парсится по 4 форматам; для каждой пары {dateISO, hhmm} → addSlot().
Верни сводку: "✅ Добавлено: X • дубликаты: Y • прошлое: Z • некорректные: K".
session.adminAddRule=false; перерисуй adm:slots с текущим weekOffset.
/cancel в этом режиме — отменяет ожидание.
Экран дня — adm:day:YYYY-MM-DD (без добавления)
Заголовок: "${safeLabelDate(dateISO,TIMEZONE)} — свободно {free} / всего {total}"
Подпись курсивом:
"Чтобы добавить новые слоты — вернитесь в «Слоты» → «➕ Добавить текстом»."
Кнопки над списком:
[⟵ К неделе|adm:slots] [🏠 Меню|adm:root]
Список слотов:
• Занят — текст: "🔒 Занят — HH:MM"
• Закрыт — [• Открыть HH:MM|adm:slot:open:DATE:HHmm] [🗑 Удалить HH:MM|adm:slot:del:DATE:HHmm]
• Свободен — [× Закрыть HH:MM|adm:slot:close:DATE:HHmm] [🗑 Удалить HH:MM|adm:slot:del:DATE:HHmm]
Действия:
adm:slot:close/open → toggleSlot(); "Закрыто"/"Открыто"; перерисовать день.
adm:slot:del → deleteSlot(); если 'taken' → "Нельзя удалить: слот занят записью"; иначе "Удалено"; перерисовать день.
Вырежи любые старые обработчики добавления в дне (adm:day:addslot, adm:add:DATE:, adm:pad*).
Записи — список, полный просмотр и действия
adm:bookings — ближайшие 10 активных (!cancelledAt, state∈{'pending','confirmed'}, slotISO>now):
[📄 ДД.MM HH:MM — @username/контакт — state | adm:bk:view:]
[✅|adm:bk:ok:] [❌|adm:bk:no:] [✉|adm:bk:msg:] [⤴ Перенести|adm:bk:move:]
Низ: [◀ Назад|adm:root]
Полный просмотр — adm:bk:view::
"Запись\nID: \nСтатус: \nПользователь: <@username|—> (ID: )\nЧат: <chatId|—>\nКонтакт: \nДата/время: <dd.MM HH:MM> ()\nЗапрос: <summary|—>"
Кнопки: ✅/❌/✉/⤴ и [◀ К списку|adm:bookings]
Действия:
ok — если state!=='pending' → "Уже: "; иначе state='confirmed', сохранить; пользователю "✅ Ваша запись подтверждена\nДД.MM HH:MM"; перерисовать.
no — если state!=='pending' → "Уже: "; иначе state='rejected', освободить слот; пользователю "❌ Запись отклонена\nВыберите другое время: /book"; перерисовать.
msg — session.adminMsg={id}; следующий текст админа → booking.chatId; "Отправлено"; перерисовать.
move — шаг A: 7 дней с ≥1 свободного → [ДД.MM|adm:mv:day::YYYY-MM-DD] → шаг B: свободные времена → [HH:MM|adm:mv:time::YYYY-MM-DD:HHmm] → перенос (старый освободить, новый занять), пользователю "Перенос подтверждён: ДД.MM HH:MM", админу "✅ Перенесено", перерисовать.
Пользовательская отмена — уведомлять админа (Фикс)
В «📌 Мои записи» при «Отменить»:
• подтвердить «Да/Нет»; при «Да» — booking.cancelledAt=Date.now(); соответствующий слот сделать taken=false; сохранить.
• пользователю: "❌ Запись отменена. Можете выбрать другое время: /book".
• safeAdminNotify: админу HTML-карточку "🛑 Отмена пользователем" (Имя/ID, @username, дата/время, контакт, ID брони).
• Перерисовать «Мои записи».
Устойчивость и перерисовка
Любое действие: read → modify → write → re-read → AdminUI.upsert текущего экрана.
Любой callback — короткий answerCbQuery().
AdminUI.upsert: игнорировать 400 "message is not modified".
safeLabelDate — без "Invalid DateTime".
Критерии приёмки
«≡ Админ» и /admin доступны админу на любом пользовательском экране.
«Слоты»:
— Неделя листается; «Только свободные» работает.
— «Добавить текстом» принимает 4 формата; сводка: добавлено/дубликаты/прошлое/invalid.
— Экран дня позволяет ОТКРЫТЬ/ЗАКРЫТЬ/УДАЛИТЬ существующие слоты.
«Записи»:
— Список и полный просмотр работают; ✅/❌/✉/⤴ выполняются; пользователь получает уведомления; UI обновляется.
«Тексты»:
— Редактор plain text; «Сохранить» — тексты сразу применяются.
Пользовательская отмена:
— Слот освобождается; пользователю приходит подтверждение; админу — уведомление.
Если после вставки кнопка «≡ Админ» всё ещё не видна — проверь, что ADMIN_ID в .env совпадает с твоим числовым ID и в рендерах реально вызывается withAdminBtn(ctx, kb).
  • Все, после выполнения трех промтов у вас должен быть готовый полнофункциональный ТГ бот

  • Осталось только все протестить

⚠️
Внимание еще раз
красный крестик

В ходе работы над проектом могут возникнуть ошибки, например:

  • Бот может не запускаться;

  • Курсор может некорректно написать или не дописать код;

  • Команды в терминале могут не работать.

зеленая галочка

Алгоритм действий:

  • Пробуем написать в чат курсора и попросить его исправить ошибку, копируем и вставляем текст ошибки из терминала в чат курсора;

  • Если это не помогло, еще раз внимательно изучаем этапы 2 - 4 и пробудем повторить шаги;

  • Если и это не помогло, создаем чистый проект и пробуем этапы 2 и 4 заново.

Пятый этап - тесты
  • Перед тем как переходить к установке бота на сервер, нужно все хорошо протестировать, алгоритм простой:

  • Проходим путь клиента несколько раз

  • Отмечаем то, что нам не понравилось или то, что не работает

  • Пишем запрос в чат Cursor о том, что необходимо исправить

  • Тестируем исправления

  • После того, как все протестировали, переходим к деплою

Шестой этап - деплой
  • Деплой - это установка бота на сервер, нужен он нам, чтобы бот функционировал 24 часа и не прекращал работу после отключения компьютера

  • Варианта у нас два:

  • Простой - Railway.

  • Пишем в чат Cursor, задеплой бота с помощью Railway

  • Cursor сам проводит нас по всем необходимым шагам

  • На выходе у нас работающий 24 на 7 бот

  • Нам дается месяц пробного периода, потом платно

  • Сложный - Beget:

  • Регистрируемся на Beget, нам нужен облачный сервер

  • Пополняем баланс, сервер стоит от 7 рублей в сутки

  • Находим реквизиты для подключения через SSH, адрес для подключения выглядит вот так root@1.11.11.111, пароль будет у вас на почте

  • Далее пишем в Cursor запрос - задеплой бота на сервер с помощью PM2 (я пользуюсь им)

Дополнительно
  • Чтобы протестировать, что вы все правильно сделали можете запустить моего готового бота @sellerivantestbot

  • Если не хотите долго мучаться, исходники доступны в github

Подписывайтесь на мою телегу и делитесь своими ботами