Промты для ТГ бота

Промты для ТГ бота

Для удобства

P1 — Core MVP (TS-only, 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.

– Алгоритм:

  1. nowTZ=DateTime.now().setZone(TIMEZONE).
  2. Сгенерируй ISO-слоты на seedDays вперёд по hours, только > nowTZ, дедуп, сортировка.
  3. Пометь занятые по bookings.json, оставь свободные.
  4. Если свободных < minFree → добавляй дни партиями до maxDays, пока свободных ≥ minFree.
  5. Сохрани в data/slots.json. На каждом запуске выполняй ensureSlots().
  6. – В лог на старте вывести ровно одну строку:
  7. 📅 Slots ready: free=<N> total=<T> window=<firstDate>…<lastDate>
  8. – Если после 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.

P2 — Уведомление админу + кнопки ✅/❌/✉ (идемпотентно) + /whoami**

Продолжи проект из P1. Ничего не ломай. Всё по-прежнему .ts.

Устойчивые уведомления

– initNotifyRoute(telegram,{adminId?,logChatId?}) → "admin"|"log"|"none":

  1. Тихий ping ADMIN_ID (sendMessage('Bot ready'), тут же удаляй); если 400/403 — пробуй LOG_CHAT_ID; если тоже нельзя — "none". Ничего не бросай.
  2. – 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 работает.

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).


Report Page