Skip to content

Bot API 9.6, gramio 0.9 и новый плагин Onboarding

2 марта – 8 мая 2026

Самый крупный цикл с релиза 0.5. Telegram Bot API 9.6 раскатан по всей экосистеме: managed-боты, обновлённые опросы, новые сущности. gramio 0.9 прокидывает bot.syncCommands() для автосинка команд с меню Telegram, шорткаты на Plugin и автогенерацию allowed_updates из зарегистрированных хэндлеров. Новый официальный плагин @gramio/onboarding — декларативные туториалы для пользователей с тремя режимами параллельных флоу, scope-aware рендером и подключаемыми хранилищами. @gramio/scenes 0.6 наконец-то перестал есть глобальные /cancel и /help. @gramio/views 0.2 добавил ленивые globals — пересчитываются на каждый рендер. А под капотом всё это работает на полностью переписанном wrappergram v2 на middleware.

gramio v0.9.0 — Синк команд, шорткаты на Plugin, умный allowed_updates

bot.syncCommands() — синк меню Telegram без церемоний

Объявить /команду — это полдела. Вторая половина — держать меню Telegram актуальным. Теперь bot.command() принимает опциональный CommandMeta (description, locales, scopes, hide), а один вызов bot.syncCommands() отправляет всё в Telegram с кэшированием по хэшу — неизменённые мета не жгут лимиты.

ts
import { Bot } from "gramio";

const bot = new Bot(process.env.BOT_TOKEN!)
    .command("start", { description: "Запустить бота" }, (ctx) => ctx.send("Привет!"))
    .command(
        "help",
        { description: "Show help", locales: { ru: "Помощь", uk: "Допомога" } },
        (ctx) => ctx.send("Помощь!"),
    )
    .command(
        "admin",
        { description: "Админ-панель", scopes: [{ type: "chat_administrators" }] },
        adminHandler,
    )
    .command("debug", { hide: true }, debugHandler);

bot.onStart(() => bot.syncCommands());
await bot.start();

meta лежит между именем команды и хэндлером — bot.command(name, meta, handler). Плоский двухаргументный вариант bot.command(name, handler) тоже работает, если команду в меню тащить не надо. syncCommands() группирует команды по scope, дедуплицирует по хэшу и пропускает scope целиком, если ничего не поменялось. В паре с новым хелпером localesFor() из @gramio/i18n можно тащить локали прямо из файлов переводов.

Шорткаты на Plugin — Plugin().command(...) работает напрямую

Раньше, чтобы инкапсулировать фичу как Plugin, надо было лезть во внутренний composer плагина просто чтобы зарегать хэндлер. Теперь у Plugin есть command, callbackQuery, hears, reaction, inlineQuery, chosenInlineResult и startParameter прямо на инстансе — та же реализация, что и в Bot/Composer:

ts
import { Plugin } from "gramio";

const adminPlugin = new Plugin("admin")
    .command("ban", banHandler)
    .command("unban", unbanHandler)
    .callbackQuery(adminAction, callbackHandler);

bot.extend(adminPlugin);

Цепочка composer.extend(plugin).command(...) правильно сохраняет накопленные TMethods (77f8c02). А Plugin.extend(plugin) теперь пробрасывает middleware, хуки, декораторы, ошибки, группы и зависимости — раньше тащил только типы.

AllowedUpdatesFilter — выводим allowed_updates из зарегистрированных хэндлеров

chat_member, message_reaction и message_reaction_count — три типа апдейтов, которые Telegram не присылает в getUpdates/setWebhook, пока их явно не указали в allowed_updates. Тихая потеря этих апдейтов — вечный грабль. GramIO 0.9 решает это двумя способами:

ts
import { Bot, AllowedUpdatesFilter } from "gramio";

const bot = new Bot(token)
    .on("chat_member", chatMemberHandler)
    .on("message_reaction", reactionHandler);

// 1. По умолчанию — без аргумента. GramIO сканирует хэндлеры и сам добавляет
//    chat_member / message_reaction / message_reaction_count, если они
//    зарегистрированы.
await bot.start();

// 2. Строгий режим — запрашиваем только те типы, на которые есть хэндлеры,
//    ничего лишнего. Передаём литерал "strict".
await bot.start({ allowedUpdates: "strict" });

// 3. Явный fluent-билдер — иммутабельный Array<AllowedUpdateName>.
await bot.start({
    allowedUpdates: AllowedUpdatesFilter.only("message", "callback_query"),
});

// 4. Дефолтный набор + добавки/исключения.
await bot.start({
    allowedUpdates: AllowedUpdatesFilter.default
        .add("chat_member")
        .except("poll", "poll_answer"),
});

Доступные фабрики: AllowedUpdatesFilter.all / .default / .only(...types), плюс цепочки .add(...) и .except(...) на любом инстансе.

AnyBot больше не схлопывает ctx.isPM() / isGroup() / isChannel() в never

Долгоиграющий баг (gramiojs/gramio#28, фикс в @gramio/contexts 0.5.1): на хэндлерах команд и сообщений Bot/Composer ctx.isPM() и компания сужали ctx до never вместо ожидаемой ветки. Пофикшено в contexts и подтянуто в gramio 0.8.3+.

Инстанс бота теперь доступен в хуках onStart / onStop

Оба хука получают bot, так что bot.api.* можно дёргать на старте/остановке без захвата через замыкание:

ts
bot.onStart(({ bot, info }) => bot.api.sendMessage({ chat_id: ADMIN, text: `Стартанул как @${info.username}` }));

Затронутые пакеты: gramio v0.9.0, @gramio/contexts v0.6.1, @gramio/types v9.6.1, @gramio/files v0.4.0, @gramio/format v0.7.0, @gramio/keyboards v1.4.0, @gramio/composer v0.4.1, @gramio/test v0.7.0.

Bot API 9.6 — Managed-боты, расширенные опросы, новые сущности

Managed bots: ManagedBotCreated, ManagedBotUpdated и новый update managed_bot

Новая модель Telegram: родительский бот программно управляет дочерними. GramIO покрыл это новыми контекстами (managed_bot, managed_bot_created), новыми структурами (ManagedBotCreated, ManagedBotUpdated) и User.canManageBots(). В ChatMemberControlMixin появились getManagedBotToken() и replaceManagedBotToken() — ротация токена живёт рядом с админскими API. @gramio/keyboards 1.4.0 добавил кнопку requestManagedBot для выбора managed-бота через диалог Telegram.

Опросы переделаны — добавление/удаление вариантов, переголосование, описания, persistent ID

В 9.6 опросы перестали быть immutable. Два новых update — poll_option_added и poll_option_deleted — фаерятся, когда пользователи мутируют структуру. У Poll появились allowsRevoting, description, descriptionEntities; correctOptionId стал correctOptionIds (массив). У PollOptionpersistentId, addedByUser, addedByChat, additionDate. У PollAnsweroptionPersistentIds. У MessagereplyToPollOptionId, managedBotCreated, pollOptionAdded, pollOptionDeleted.

Защита от mojibake в эмодзи-енумах

@gramio/types 9.6.0 на короткое время уехал с битыми значениями SendDiceEmoji ("рџЋІ") — апстрим-ответ декодировался как windows-1251. 9.6.1 кидает ошибку при детекте байт-последовательности "рџ", и регенерированный пакет снова содержит чистый юникод.

@gramio/onboarding v0.1.0 — Новый официальный плагин

Декларативные туториалы с многопоточными флоу

Совершенно новый официальный плагин — проводит пользователя по фичам бота шаг за шагом. Шаги сменяются по кнопке "Далее", по факту того, что пользователь сделал нужное действие (advanceOn), или программно из обычного хэндлера (ctx.onboarding.<flow>.next({ from })). Несколько флоу собираются независимо — welcome, premium-upsell, new-feature — с тремя режимами параллельности (queue, preempt, parallel).

ts
import { Bot } from "gramio";
import { createOnboarding } from "@gramio/onboarding";

const welcome = createOnboarding({ id: "welcome" })
    .step("hi", { text: "Привет! Сейчас покажу что к чему.", buttons: ["next", "exit"] })
    .step("links", { text: "Пришли мне любую ссылку — я её скачаю.", buttons: ["next", "dismiss"] })
    .step("done", { text: "Готово!" })
    .onComplete((ctx) => ctx.send("Добро пожаловать! /help всегда под рукой."))
    .build();

const bot = new Bot(process.env.BOT_TOKEN!).extend(welcome);

bot.command("start", (ctx) => {
    ctx.onboarding.welcome.start();
    return ctx.send("Поехали!");
});

Что внутри:

  • Лесенка отказовnext → skip → exit → dismiss → disableAll, всё подключается через кнопки.
  • Учёт scoperenderIn: "dm" | "group" | "any" откладывает шаг, если текущий чат не подходит, и перерисовывает на следующем подходящем апдейте.
  • API «выстрелил и забыл» — каждый вызов ctx.onboarding.* глотает ошибки и пробрасывает их в bot.errorHandler, никогда не кидает в бизнес-логику.
  • Не привязан к хранилищу — подключаемый Storage<OnboardingStorageMap> через @gramio/storage (memory / redis / sqlite / cloudflare). Хелпер getStorageContractCases() без зависимостей от тест-раннера даёт авторам адаптеров проверить контракт.
  • Опциональная интеграция с @gramio/views — кладёшь step.view и пользуешься новой ленивой подачей globals, чтобы view всегда видел живые токены онбординга.

Полный референс: /ru/plugins/official/onboarding.

@gramio/scenes v0.6.0 — Перестали есть глобальные команды

Passthrough: не подходящие шагу апдейты летят к внешним хэндлерам

Раньше, пока пользователь был в сцене, любой апдейт, который не подходил текущему шагу, тихо съедался. Юзер, застрявший в форме, не мог вызвать зарегистрированный снаружи /cancel или /help. 0.6.0 переворачивает дефолт: не подходящие апдейты пробрасываются дальше по цепочке бота, а сцена сохраняет firstTime, чтобы пользователь не потерял место. passthrough: false возвращает старое жадное поведение.

ts
import { Bot } from "gramio";
import { scenes } from "@gramio/scenes";

const bot = new Bot(token)
    .extend(scenes([signupScene]))      // passthrough: true по умолчанию
    .command("cancel", (ctx) => ctx.scene?.exit()); // теперь реально срабатывает!

Под-сцены и enterSub() / exitSub()

Сцены теперь вкладываются. ctx.scene.enterSub(otherScene, params) пушит текущую сцену в стек родителей, гоняет под-сцену до конца и автоматически возвращается на следующий шаг родителя. Стек персистится в storage-записи — после рестарта процесс корректно подхватывает место. .exitData<T>() на под-сцене типизирует данные, которые она возвращает родителю:

ts
const pickAddress = new Scene("pick-address")
    .exitData<{ address: string }>()
    .step("ask", (ctx) => ctx.send("Пришли свой адрес текстом"))
    .step("save", (ctx) => ctx.scene.exitSub({ address: ctx.text! }));

const checkout = new Scene("checkout")
    .step("address", async (ctx) => {
        await ctx.scene.enterSub(pickAddress);   // паузим checkout, гоняем pickAddress
    })
    .step("confirm", (ctx) => {
        // pickAddress зарезолвилась — её exitData попадает в ctx.scene.state через мерж родителя
        return ctx.send("Подтвердить заказ?");
    });

scene.reenter(params) и типизированные параметры scene.enter()

reenter() теперь принимает params, а scene.enter() правильно тайпчекает кортеж параметров на месте вызова — больше не падает в any (c5b2dc5, закрывает #6).

scenesDerives использует Plugin для нормальной дедупликации

Закрывает #5context.scene был undefined, когда scenesDerives подключали через Composer, потому что gramio не мог дедуплицировать безымянный Composer. Переключились на Plugin — баг ушёл, и заодно стало возможным шарить хранилище между bot-level хэндлерами и шагами сцен.

@gramio/format v0.7.0 — Фикс разделителей блоков, регенерация под Bot API 9.6

Markdown: сохраняем переносы между соседними block-токенами

Незаметный, но критичный для любого LLM-бота баг: marked хранит концевые переносы блока в его собственном raw, поэтому склейка top-level токенов пустым сепаратором сцепляла соседние блоки. "Agenda:\n- one\n- two" рендерился как "Agenda:- one\n- two" — каждый перечислительный ответ ассистента ехал в проде криво. Фикс обобщает прошлый workaround только-для-headings до normalizeBlockSeparators, который покрывает paragraph+list, paragraph+blockquote, paragraph+code, heading+что угодно.

Mutator перегенерирован на @gramio/schema-parser

Заменили старый pipeline tg-bot-api/custom.min.json на getCustomSchema() из @gramio/schema-parser. Генератор рекурсивно проходит параметры методов, дедуплицирует трансформации и теперь покрывает 32 метода, включая sendMessageDraft, sendPaidMedia, sendPoll.description, sendChecklist, editMessageChecklist.

formatMiddleware для middleware-цепочки wrappergram v2

@gramio/format/middleware теперь экспортирует готовый middleware, который раскладывает FormattableString на text + entities перед каждым вызовом Telegram API — кладётся в новую цепочку wrappergram v2 (или любую другую) без захода через плагин gramio.

@gramio/views v0.2.0 — Ленивые globals через thunk

buildRender теперь принимает Globals | (() => Globals). Когда передаёшь функцию, она вызывается на каждый рендер — и view видит свежий стейт из изменчивых источников: session, scene, snapshot онбординга, локаль, эскалация роли. Adapter-фабрика тоже пересоздаётся на каждый рендер с уже разрешёнными globals, так что выбор адаптера по локали продолжает работать, даже если локаль поменялась посреди обработчика:

ts
bot.derive(["message", "callback_query"], (ctx) => ({
    render: defineView.buildRender(ctx, () => ({
        user: { id: ctx.from!.id, name: ctx.from!.firstName },
        // захватывается свежим на каждый рендер — флип локали в middleware «просто работает»
        i18n: ctx.t,
        // токены онбординга для плагина @gramio/onboarding
        onboarding: getCurrentOnboardingTokens(),
    })),
}));

Plain-объекты остались без изменений. Геттеры на свойствах plain-объектов и так пересчитывались на каждый рендер, так что смесь геттеров — тоже валидный паттерн.

@gramio/test v0.7.0 — Отслеживание bubble, Telegram Payments, типизированный ApiCall

env.lastBotMessage() — bubble, который синхронизируется с edit

Зеркало MessageObject для последнего sendMessage бота, проактивно синкается с editMessageText / editMessageCaption / editMessageReplyMarkup прямо в proxy — ссылки, схваченные до edit, остаются актуальными. user.on(bubble).clickByText(...) теперь работает через несколько edit на одну и ту же ссылку. reply_markup Builder-инстансы (вроде InlineKeyboard) нормализуются через .toJSON() перед записью — больше никаких JSON.parse(JSON.stringify(...)) в тестах:

ts
import { TelegramTestEnvironment } from "@gramio/test";
import { bot } from "./bot.js";

const env = new TelegramTestEnvironment(bot);
const user = env.user(1, { firstName: "Alice" });

await user.command("start");
const bubble = env.lastBotMessage();              // первый send
await user.on(bubble).clickByText("Next →");      // бот эдитит то же сообщение
// `bubble` уже отражает отредактированное состояние — никакого ручного refresh
expect(bubble.payload.text).toBe("Шаг 2 из 3");

Фильтры withReplyMarkup и where

ts
const bubble = env.lastBotMessage({ withReplyMarkup: true });        // пропускает status/confirmation сообщения
const found  = env.lastBotMessage({ where: (call) => /Agenda/.test(call.params.text) });

Поддержка Telegram Payments

Билдеры PreCheckoutQueryObject и ShippingQueryObject, плюс user.sendPreCheckoutQuery(), user.sendShippingQuery() и user.sendSuccessfulPayment() — полная симуляция платёжного флоу с проверкой апрува pre-checkout от бота.

Типобезопасный ApiCall<Method> и filterApiCalls(method)

ApiCall<Method> тайпит params/response через APIMethodParams/APIMethodReturn. lastApiCall("sendMessage") возвращает типизированный результат, а новый filterApiCalls("sendMessage") отдаёт ApiCall<"sendMessage">[] с полным сужением типов.

@gramio/composer v0.4.1 — registeredEvents() и EventContextOf

registeredEvents() — интроспекция того, что вообще зарегано

Возвращает Set<string> имён событий, зарегистрированных через .on() и event-specific .derive() — включая распарсенные композитные события ("message|callback_query") и entity-паттерны ("message:text"). Под капотом этого работает автогенерация allowed_updates в gramio 0.9 выше.

EventContextOf<T, E> — глобальные + per-event derives в одном типе

Когда пишешь кастомный метод под конкретный тип события, EventContextOf<T, E> достаёт TOut & TDerives[E] из инстанса composer — per-event derive видны без ручного intersection:

ts
import { Bot, EventContextOf } from "gramio";

const bot = new Bot(token)
    .derive(() => ({ session: { count: 0 } }))            // глобальный derive
    .derive("callback_query", (ctx) => ({ payload: ctx.queryData })); // per-event

// Кастомный хелпер под callback_query — видит и session, и payload
function bumpCounter(ctx: EventContextOf<typeof bot, "callback_query">) {
    ctx.session.count += 1;
    return ctx.answer({ text: `Got payload: ${ctx.payload}` });
}

Документирован рядом с уже существующими ContextType и BotContext.

Хранилище commandsMeta стало framework-agnostic

commandsMeta теперь Map с unknown значениями вместо Telegram-специфичных CommandMeta/ScopeShorthand. Telegram-специфика переехала в ядро gramio, где ей и место (2429013).

Предикат guard() больше не схлопывает ctx в any после derive()

Объединение overload'ов type-guard и boolean-предиката заставляло TS уходить в any. Разнос на два overload'а вернул нормальную контекстную типизацию — закрывает #1.

@gramio/i18n v1.5 — localesFor(): мостик между i18n-ключами и syncCommands

Новый метод на инстансе из defineI18n(), возвращающий Record<string, string> всех неосновных переводов ключа — кладётся прямо в CommandMeta.locales:

ts
import { defineI18n } from "@gramio/i18n";

const i18n = defineI18n({
    languages: { en, ru, uk },
    primaryLanguage: "en",
});

bot.command("help", {
    description: i18n.t("en", "cmd.help"),    // строка на основной локали
    locales: i18n.localesFor("cmd.help"),      // { ru: "Помощь", uk: "Допомога" }
}, helpHandler);

localesFor идёт по Object.keys(languages), пропускает primary, гоняет тот же t(lang, key, ...args), который под капотом у ctx.t(), и выкидывает ключи, разрешившиеся в null/отсутствие — частичное покрытие языков работает нормально.

@gramio/auto-answer-callback-query v0.0.3 — Отвечает всегда, даже если хэндлер кинул

Раньше упавший хэндлер пропускал авто-ответ — у пользователя оставался залипший спиннер на кнопке. Middleware теперь оборачивает хэндлер в try/finally, так что answerCallbackQuery гарантированно отрабатывает.

@gramio/jsx — Элемент <date-time>

Поддержка новой сущности dateTime из @gramio/format 0.5+ с unixTime и опциональным format props (r, w, d, D, t, T, wDT, Dt и т. д.):

tsx
<>Сегодня <date-time unixTime={Date.now() / 1000} format="D" /></>

wrappergram v2 — Из голого proxy в middleware-пайплайн

Минимальная обёртка над Bot API, на которой сидит bot.api в gramio, переписана с нуля. Раньше wrappergram экспортировал просто класс Telegram без точек расширения — каждый вызов гнал хардкод convertJsonToFormData → fetch → response.json(), а @gramio/files был обязательной зависимостью. v2 превращает этот пайплайн в middleware-цепочку, в которую можно вклиниться:

ts
// Было — никаких точек расширения, @gramio/files вшит намертво
import { Telegram } from "wrappergram";
const tg = new Telegram(token);
const response = await tg.api.sendMessage({ chat_id, text });

// Стало — явная цепочка middleware, files/format подключаются по желанию
import { Wrappergram, TelegramError } from "wrappergram";
import { filesMiddleware } from "@gramio/files/middleware";
import { formatMiddleware } from "@gramio/format/middleware";

const tg = new Wrappergram({
    token,
    middlewares: [
        async (ctx, next) => {
            const start = Date.now();
            await next();
            console.log(`${ctx.method} занял ${Date.now() - start}мс`);
        },
        formatMiddleware,
        filesMiddleware,
    ],
});

// Подавление ошибок в месте вызова вместо try/catch
const result = await tg.sendMessage({ chat_id, text }, { suppress: true });
if (result instanceof TelegramError) {
    console.error("send failed:", result.code, result.payload);
}

Что внутри:

  • Один тип Middleware(context, next) => unknown. Никаких 4 хуков, никакой каллбэк-каши.
  • Первоклассный TelegramError — несёт method, code, payload плюс captureStackTrace, так что в стеке виден реальный call site.
  • Паттерн suppress: true — возвращает TelegramError | Result вместо того, чтобы кидать. SuppressedAPIMethods через дженерик IsSuppressed выводит правильный return type на уровне типов.
  • Per-request fetch-опции едут вторым аргументом к каждому API-методу.
  • @gramio/files больше не жёсткая зависимость — подключается опционально через новый экспорт filesMiddleware из @gramio/files/middleware. Тот же паттерн у @gramio/format/middleware. Для тех, кому это не нужно, размер бандла стал меньше.

Прочие улучшения

@gramio/schema-parser v1.1.0 — Детектим FormattableString по общим соседним полям

InputTextMessageContent сбрасывает префикс ключа у соседних entity-полей (голый parse_mode / entities вместо message_text_parse_mode), и существующая проверка по полю-префиксу его пропускала. Новый fallback: если у объекта одновременно есть голые parse_mode и массив entities, единственное немаркированное строковое поле повышается до semanticType: "formattable". Закрывает старый пробел, который раньше требовал ручной workaround в генераторе мутатора @gramio/format.

@gramio/contexts v0.5.1 — Type narrowing через AnyBot

Когда дженерик Bot был AnyBot, __Derives разрешалось в any, и схлопывалось сужение GetDerives и Context.is(). Фикс от внешнего контрибьютора @ttempaa (PR #3).

create-gramio v2.2.0 — Scoped Composer + наследование в шагах сцен

Сгенерённые проекты теперь делят plugins/ на base.ts (именованный scoped composer с i18n / session / render) плюс тонкий сборочный файл. Сцены .extend(baseComposer), чтобы у шагов был типизированный доступ к ctx.t, ctx.render, ctx.session и т. д. Дедуп по имени "base" в момент регистрации гарантирует, что middleware прокатывается ровно один раз на апдейт. Заодно бамп gramio 0.5 → 0.9, scenes 0.3 → 0.6, views 0.0.5 → 0.2, test 0.3 → 0.7 и ещё 8 строк зависимостей.

ecosystem-ci — Новый pipeline кросс-репозиторной совместимости

Совершенно новая внутренняя инфра: CLI-оркестратор (resolve-matrix, run-suite, run-all) с полным графом зависимостей 21 пакета @gramio/* через 5 слоёв. Клонит репы, накатывает override'ы зависимостей, гоняет install → build → type-check → tests на каждый пакет. 9 production-like flow-тестов (38 кейсов) покрывают сборку бота, композицию middleware, жизненный цикл сцен, callback+keyboard roundtrip, обработку ошибок, макросы, интеграцию с format, inline-запросы, webhook'и. Ночное расписание + ручной dispatch + repository_dispatch на публикацию пакета.

Документация и Skills

В этом цикле плотно прокачали и доку с AI-тулингом:

  • Skill /gramio-pick-username — генерит кандидатов на username Telegram-бота, валидирует под правила BotFather, батчем чекает доступность на t.me через скрипт check-usernames.mjs.
  • Skill verification gatesbun run check:skills и bun run test:skills теперь гоняются в CI на каждый пуш, который трогает skills/**. Каждый пример экспортирует { bot } и имеет рантайм-тест, который гоняет его через @gramio/test.
  • UX-патерны — playbook про button-first дизайн: hero /start, edit-in-place навигация, breadcrumbs, toggle-кнопки, destructive confirm, loading/empty состояния, ship-чеклист.
  • CLI-тулинг для интроспекции — четыре скрипта в skills/tools/, парсят установленные @gramio/* пакеты и отдают токен-эффективные сигнатуры: 169 методов Bot API, 329 типов Telegram, контекст-геттеры и формы плагинов.
  • Гайд по миграции с grammY — детальные сравнения кода рядом.
  • Свитчер пакетных менеджеров — в верхнем меню VitePress теперь есть глобальный pm-свитчер; все ::: code-group блоки установки переведены на ::: pm-add шорткат.