惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Vulnerabilities – Threatpost
G
GRAHAM CLULEY
Simon Willison's Weblog
Simon Willison's Weblog
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
H
Heimdal Security Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Secure Thoughts
MyScale Blog
MyScale Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
LINUX DO - 最新话题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Cloudflare Blog
美团技术团队
Recorded Future
Recorded Future
T
Tailwind CSS Blog
Latest news
Latest news
Security Archives - TechRepublic
Security Archives - TechRepublic
Security Latest
Security Latest
Know Your Adversary
Know Your Adversary
Cloudbric
Cloudbric
Schneier on Security
Schneier on Security
I
Intezer
L
LINUX DO - 热门话题
P
Palo Alto Networks Blog
云风的 BLOG
云风的 BLOG
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Vercel News
Vercel News
Attack and Defense Labs
Attack and Defense Labs
人人都是产品经理
人人都是产品经理
L
LangChain Blog
爱范儿
爱范儿
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
W
WeLiveSecurity
C
Cybersecurity and Infrastructure Security Agency CISA
S
Securelist
SecWiki News
SecWiki News
V2EX - 技术
V2EX - 技术
IT之家
IT之家
Cyberwarzone
Cyberwarzone
F
Full Disclosure
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
T2M Bridge, часть 1: как я написал Java‑библиотеку для MAX Bot API с помощью Codex
Сергей Герасимов · 2026-06-19 · via Все публикации подряд на Хабре

Средний

16 мин

461

Это первая статья из цикла о разработке T2M Bridge — бота для кросспостинга между Telegram‑каналами и MAX‑каналами.

В цикле планируются четыре части:

  1. T2M Bridge, часть 1: как я написал Java‑библиотеку для MAX Bot API с помощью Codex

  2. T2M Bridge, часть 2: как я написал Java‑библиотеку для Telegram Bot API вместе с Codex — уже примерно 50/50

  3. T2M Bridge, часть 3: как я своими силами собрал бота для кросспостинга между Telegram и MAX

  4. T2M Bridge, часть 4: как я выкатывал бота в прод и почему всё оказалось сложнее Docker Compose

В первой части расскажу про maxlib — Java‑библиотеку для разработки ботов под MAX.

Я писал её по большей части с помощью Codex. Причина была простая: сама библиотека не была конечной целью. Мне нужен был рабочий инструмент для T2M Bridge, и я не хотел тратить слишком много времени на инфраструктурный слой.

Но получилось не так, что ИИ написал всё сам, а я только нажал кнопку «готово». На первичную разработку, проверку, исправление ошибок и доведение библиотеки до состояния, в котором её можно использовать в реальном проекте, всё равно ушло около пары недель.

Основная проблема оказалась не в синтаксисе Java и не в boilerplate‑коде. С этим Codex справлялся неплохо. Основная проблема была в другом: ИИ периодически начинал придумывать свои поля, объекты и структуры, вместо того чтобы строго следовать документации MAX Bot API.

В этой статье разберу, зачем мне понадобилась своя библиотека, как она устроена, как ей пользоваться и какие выводы я сделал после разработки с активным использованием ИИ.

Репозиторий:

https://github.com/tardyon‑soft/maxlib

Зачем понадобилась своя библиотека

T2M Bridge — это бот для кросспостинга между Telegram‑каналом и MAX‑каналом.

Чтобы такой бот работал, мне нужно было реализовать две стороны:

  • Telegram‑бота;

  • MAX‑бота.

Для Telegram в Java‑экосистеме уже есть разные библиотеки и подходы. Для MAX на момент старта мне не хватало готового инструмента, который закрывал бы не только HTTP‑вызовы к API, но и более высокий runtime‑слой.

Мне хотелось получить не просто набор методов вроде sendMessage, а нормальную основу для разработки бота:

  • typed client для MAX Bot API;

  • обработку входящих событий;

  • dispatcher;

  • router;

  • фильтры;

  • middleware;

  • FSM;

  • экраны;

  • polling и webhook;

  • интеграцию со Spring Boot;

  • возможность тестировать обработчики.

Можно было сделать минимальный HTTP‑клиент прямо внутри T2M Bridge.

Например:

  • вызвать API MAX;

  • отправить сообщение;

  • загрузить медиа;

  • обработать входящий update.

Для первого прототипа этого могло бы хватить. Но дальше код быстро начал бы смешиваться: бизнес‑логика кросспостинга, работа с пользователями, состояние диалогов, обработка каналов, MAX API, медиа, callback’и.

Поэтому я решил вынести MAX‑часть в отдельную библиотеку.

Так появилась maxlib.

Почему я подключил Codex

На тот момент я не хотел превращать разработку библиотеки в отдельный большой проект.

Цель была практической: быстро получить рабочий фундамент для бота и не тратить недели на ручное написание однотипной инфраструктуры.

Codex хорошо подходил для таких задач:

  • сгенерировать DTO;

  • набросать builder’ы;

  • сделать каркас клиента;

  • подготовить интерфейсы;

  • написать starter‑конфигурации;

  • собрать routing‑слой;

  • сгенерировать первичные тесты;

  • подготовить README и документацию по модулям.

То есть там, где нужно много механического кода, ИИ действительно экономил время.

Но довольно быстро проявилась граница такого подхода.

Codex хорошо генерировал Java‑код, но не всегда строго следовал документации MAX Bot API. Иногда он начинал достраивать API «по аналогии» с другими платформами или по собственной логике.

В результате появлялись:

  • поля, которых нет в документации;

  • объекты, которых нет в реальном API;

  • неверные названия свойств;

  • лишние enum‑значения;

  • методы, которые выглядели правдоподобно, но не соответствовали API;

  • структуры request/response, которые компилировались, но не работали с реальным MAX.

Это важный момент.

ИИ может уверенно написать код, который выглядит правильно. Особенно если код типичный: DTO, JSON mapping, client method, builder.

Но если источник истины — конкретная документация внешнего API, то красивого кода недостаточно. Нужно проверять каждое поле и каждую модель.

Как выглядел рабочий процесс

На практике процесс был таким:

  1. Я описывал Codex конкретную задачу.

  2. Он генерировал каркас.

  3. Я сверял результат с документацией MAX Bot API.

  4. Удалял выдуманные поля и объекты.

  5. Исправлял request/response‑модели.

  6. Прогонял код на demo‑боте.

  7. Исправлял ошибки, которые проявлялись уже при реальных вызовах.

  8. Только после этого считал часть библиотеки условно готовой.

Самый плохой результат получался, когда задача была слишком широкой.

Например:

Сделай библиотеку для MAX Bot API.

Такой промпт почти гарантированно приводит к тому, что ИИ начинает фантазировать. Он строит красивую архитектуру, но часть моделей может быть не из документации, а из его представления о том, как «обычно устроены bot API».

Лучше работал другой подход:

Вот конкретный раздел документации. Сделай модели только для этих request/response. Не добавляй поля, которых нет в документации. Если данных не хватает‑ не придумывай.

Даже в таком режиме всё равно приходилось проверять результат вручную.

Основной вывод: при работе с внешним API ИИ можно использовать как ускоритель, но нельзя использовать как источник истины.

Источник истины — документация и реальные запросы.

Что получилось в итоге

maxlib сейчас собирается под Java 17 и состоит из нескольких модулей:

  • max-model — модели MAX API;

  • max-client-core — клиентский слой и HTTP transport;

  • max-fsm — FSM storage, scenes и wizard primitives;

  • max-dispatcher — dispatcher, router, filters, middleware, screens, upload contracts;

  • max-spring-boot-starter — auto‑configuration для Spring Boot;

  • max-micronaut-starter — auto‑configuration для Micronaut;

  • max-quarkus-starter — auto‑configuration для Quarkus;

  • max-testkit — утилиты для тестирования обработчиков и update‑потока.

Для T2M Bridge мне в первую очередь был нужен Spring Boot starter, потому что основной бот пишется на Java и Spring.

Позже появились Micronaut и Quarkus starter’ы. Это уже развитие той же идеи: runtime библиотеки не должен быть жестко привязан только к Spring Boot.

Если нужен низкоуровневый клиент, можно подключить только max-client-core.

Если нужен runtime‑слой с обработкой update’ов, роутингом и middleware, нужен max-dispatcher.

Если приложение пишется на Spring Boot, Micronaut или Quarkus, проще взять соответствующий starter.

Установка

Для Spring Boot:

dependencies {
    implementation("ru.tardyon.botframework:max-spring-boot-starter:<version>")
}

Для Micronaut:

dependencies {
    implementation("ru.tardyon.botframework:max-micronaut-starter:<version>")
}

Для Quarkus:

dependencies {
    implementation("ru.tardyon.botframework:max-quarkus-starter:<version>")
}

Для vanilla Java:

dependencies {
    implementation("ru.tardyon.botframework:max-client-core:<version>")
    implementation("ru.tardyon.botframework:max-dispatcher:<version>")
}

Для тестов:

dependencies {
    testImplementation("ru.tardyon.botframework:max-testkit:<version>")
}

В примерах ниже буду использовать Spring Boot, потому что это основной сценарий для моего проекта.

Минимальный Spring Boot бот через polling

Для polling‑режима нужно подключить starter и добавить конфигурацию:

max:
  bot:
    token: ${MAX_BOT_TOKEN}
    mode: POLLING
    polling:
      enabled: true
      limit: 100
      timeout: 30s

После этого можно объявить Router как Spring bean:

import java.util.concurrent.CompletableFuture;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import ru.tardyon.botframework.dispatcher.BuiltInFilters;
import ru.tardyon.botframework.dispatcher.Router;
import ru.tardyon.botframework.message.Messages;

@Configuration
public class BotConfig {

    @Bean
    Router botRouter() {
        Router router = new Router("main");

        router.message(BuiltInFilters.command("start"), (message, ctx) -> {
            ctx.reply(Messages.text("Привет из Spring Boot"));
            return CompletableFuture.completedFuture(null);
        });

        return router;
    }
}

Starter создаёт основные runtime‑компоненты:

  • MaxBotClient;

  • Dispatcher;

  • facade для сообщений, callback’ов и chat actions;

  • FSM storage;

  • registry для screens и scenes;

  • polling lifecycle.

Для минимального бота не нужно руками собирать весь runtime.

Webhook‑режим

Для webhook нужно добавить web‑зависимость Spring Boot:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
}

И настроить режим:

max:
  bot:
    token: ${MAX_BOT_TOKEN}
    mode: WEBHOOK
    webhook:
      path: /webhook/max
      secret: ${MAX_WEBHOOK_SECRET}
      max-in-flight: 64

В этом режиме starter поднимает HTTP endpoint по указанному пути и передаёт входящие запросы в webhook receiver.

Для локальной разработки polling проще. Для production webhook обычно удобнее, если уже есть внешняя точка входа, TLS и reverse proxy.

Dispatcher и Router

Внутри runtime есть два основных понятия: Dispatcher и Router.

Dispatcher принимает update, определяет тип события, подготавливает RuntimeContext и передаёт управление в router’ы.

Router регистрирует обработчики:

  • update;

  • message;

  • callback;

  • error.

Пример с фильтром команды:

router.message(BuiltInFilters.command("start"), (message, ctx) -> {
    ctx.reply(Messages.text("Старт"));
    return CompletableFuture.completedFuture(null);
});

В BuiltInFilters есть готовые фильтры:

  • command(...);

  • textEquals(...);

  • textStartsWith(...);

  • chatType(...);

  • fromUser(...);

  • hasAttachment();

  • callbackDataPresent();

  • callbackDataEquals(...);

  • callbackDataStartsWith(...);

  • state(...);

  • stateIn(...).

Для простого бота этого уже достаточно: можно маршрутизировать команды, текстовые сообщения, callback’и и состояния пользователя.

Но в реальном боте быстро хочется уйти от ручной регистрации всего через router.message(...).

Поэтому в библиотеке есть аннотационный API.

Аннотационный роутинг

Вместо ручной регистрации обработчиков можно описывать route‑классы через аннотации.

Минимальный пример:

import ru.tardyon.botframework.dispatcher.RuntimeContext;
import ru.tardyon.botframework.dispatcher.annotation.Command;
import ru.tardyon.botframework.dispatcher.annotation.Route;
import ru.tardyon.botframework.message.Messages;

@Route(value = "menu", autoRegister = true)
public final class MenuRoute {

    @Command("start")
    public void start(RuntimeContext ctx) {
        ctx.reply(Messages.text("Команды: /menu"));
    }

    @Command("menu")
    public void menu(RuntimeContext ctx) {
        ctx.reply(Messages.text("Меню открыто"));
    }
}

Поддерживаются аннотации:

  • @Route;

  • @Message;

  • @Text;

  • @Command;

  • @Callback;

  • @CallbackPrefix;

  • @State;

  • @UseFilters;

  • @UseMiddleware.

Для Spring Boot starter’а это удобно тем, что route‑классы можно автоматически регистрировать в контексте приложения.

В T2M Bridge такой подход полезен для разделения сценариев: профиль пользователя, связывание аккаунтов, список каналов, настройки кросспостинга и другие части бота можно держать отдельно.

RuntimeContext

Основной объект, с которым работает обработчик, — RuntimeContext.

Через него доступны:

  • messaging();

  • callbacks();

  • actions();

  • media();

  • fsm();

  • scenes();

  • wizard();

  • reply(...);

  • answerCallback(...);

  • chatAction(...).

Например:

@Command("ping")
public void ping(RuntimeContext ctx) {
    ctx.reply(Messages.text("pong"));
}

Или callback:

@Callback("menu:ok")
public void ok(RuntimeContext ctx) {
    ctx.answerCallback("OK");
}

Задача RuntimeContext — скрыть детали сборки клиента, storage, callback facade и screen runtime.

Обработчик получает контекст и работает с ботом на уровне пользовательского сценария.

FSM: состояние пользователя

Для диалоговых сценариев нужен FSM.

Например, пользователь нажал кнопку «Связать профили», запросил код, потом должен ввести этот код во втором боте.

Это уже не обработка одной команды, а последовательность шагов.

В maxlib для этого есть FSMContext.

У него есть базовые операции:

  • currentState();

  • setState(...);

  • clearState();

  • data();

  • setData(...);

  • updateData(...);

  • clearData();

  • clear().

Упрощённый пример:

router.message(BuiltInFilters.command("form"), (message, ctx) ->
        ctx.fsm().setState("demo.form.name")
                .thenAccept(ignored -> ctx.reply(Messages.text("Введите имя")))
);

router.message(BuiltInFilters.state("demo.form.name"), (message, ctx) ->
        ctx.fsm().updateData(Map.of("name", message.text()))
                .thenCompose(ignored -> ctx.fsm().setState("demo.form.done"))
);

По умолчанию в starter’е используется scope USER_IN_CHAT.

Для хранения состояния можно использовать memory storage или Redis.

Spring Boot конфигурация Redis storage:

max:
  bot:
    storage:
      type: REDIS
      redis:
        key-prefix: max:bot:fsm
        ttl: 30m

Для локальной разработки memory storage может быть достаточно. Для сервиса, который должен переживать рестарты, Redis практичнее.

Screens: интерфейс внутри бота

Командный интерфейс быстро становится неудобным.

Можно сделать так:

/start
/link
/channels
/settings
/crosspost_on

Но обычному пользователю не хочется помнить список команд.

Поэтому в maxlib есть screen API. Идея простая: описывать экран как модель, состоящую из текста, кнопок и действий.

Минимальный пример через @Screen:

import ru.tardyon.botframework.screen.ScreenContext;
import ru.tardyon.botframework.screen.ScreenModel;
import ru.tardyon.botframework.screen.Widgets;
import ru.tardyon.botframework.screen.annotation.Render;
import ru.tardyon.botframework.screen.annotation.Screen;

@Screen("profile")
public final class ProfileScreen {

    @Render
    public ScreenModel render(ScreenContext ctx) {
        String name = String.valueOf(ctx.params().getOrDefault("name", "Гость"));

        return ScreenModel.builder()
                .title("Профиль")
                .widget(Widgets.text("Имя: " + name))
                .showBackButton(true)
                .build();
    }
}

Экран можно открыть из обработчика команды или callback’а, передав параметры. Дальше screen runtime занимается навигацией, callback’ами и перерисовкой.

Для T2M Bridge это важно, потому что пользовательский сценарий состоит из нескольких шагов:

  1. открыть профиль;

  2. связать профили Telegram и MAX;

  3. показать список каналов;

  4. выбрать канал;

  5. связать его с каналом другой платформы;

  6. выбрать режим кросспостинга.

Сделать это только командами можно, но интерфейс получится менее понятным.

@ScreenController: controller‑style API для экранов

В Spring Boot starter есть @ScreenController.

Он нужен, когда удобнее описывать несколько экранов и действий в одном Spring bean.

Пример:

import java.util.Map;
import java.util.concurrent.CompletionStage;

import ru.tardyon.botframework.screen.ScreenButton;
import ru.tardyon.botframework.screen.ScreenContext;
import ru.tardyon.botframework.screen.ScreenModel;
import ru.tardyon.botframework.screen.Widgets;
import ru.tardyon.botframework.spring.screen.annotation.OnScreenAction;
import ru.tardyon.botframework.spring.screen.annotation.OnScreenText;
import ru.tardyon.botframework.spring.screen.annotation.ScreenController;
import ru.tardyon.botframework.spring.screen.annotation.ScreenView;

@ScreenController
public final class ProfileController {

    @ScreenView(screen = "profile")
    public ScreenModel profile(ScreenContext ctx) {
        String name = String.valueOf(ctx.params().getOrDefault("name", "Гость"));

        return ScreenModel.builder()
                .title("Профиль")
                .widget(Widgets.text("Имя: " + name))
                .widget(Widgets.text("Отправьте новый текст, чтобы изменить имя"))
                .widget(Widgets.buttonRow(ScreenButton.of("Сбросить", "reset_name")))
                .showBackButton(true)
                .build();
    }

    @OnScreenAction(screen = "profile", action = "reset_name")
    public CompletionStage<Void> reset(ScreenContext ctx) {
        return ctx.nav().replace("profile", Map.of("name", "Гость"));
    }

    @OnScreenText(screen = "profile")
    public CompletionStage<Void> onText(ScreenContext ctx, String text) {
        String next = text == null || text.isBlank() ? "Гость" : text.trim();
        return ctx.nav().replace("profile", Map.of("name", next));
    }
}

Такой стиль удобен для пользовательских flow, где несколько экранов относятся к одной области.

Например:

  • профиль;

  • связывание профилей;

  • список каналов;

  • настройки конкретного канала.

Один controller может держать несколько связанных screen id, а navigation и handlers остаются рядом.

Widgets

Если часть интерфейса повторяется на разных экранах, её можно вынести в widget.

Для этого есть @WidgetController.

Идея такая:

  • widget сам умеет отрендерить свой фрагмент;

  • widget может обрабатывать свои action’ы;

  • screen может подключить widget через reference.

Упрощённо:

Widgets.ref("demo.counter")

А сам widget controller описывает render и action handlers.

Это полезно для переиспользуемых блоков: статус профиля, список быстрых действий, фрагмент настроек, общий header или footer экрана.

Middleware

В maxlib есть два уровня middleware:

  • OuterMiddleware — на уровне Dispatcher;

  • InnerMiddleware — на уровне Router или конкретного annotated handler.

Middleware может обогащать RuntimeContext.

Пример:

public final class AttemptMiddleware implements InnerMiddleware {

    @Override
    public CompletionStage<DispatchResult> invoke(RuntimeContext context, MiddlewareNext next) {
        context.putEnrichment("attempt", 1);
        return next.proceed();
    }
}

В реальном боте middleware удобно использовать для сквозных задач:

  • логирование;

  • метрики;

  • проверка доступа;

  • загрузка пользователя;

  • ограничение действий;

  • enrichment контекста.

В T2M Bridge почти каждое действие пользователя зависит от связанного профиля, прав на канал и текущего состояния, поэтому такой слой быстро становится полезным.

Тестирование обработчиков

Когда бот состоит из множества сценариев, тестировать его только вручную неудобно.

Для этого в maxlib есть max-testkit.

Его задача — помочь проверить обработчики и поток update’ов без реального запроса к MAX API.

Тест должен отвечать на несколько вопросов:

  • какой update пришёл;

  • какой handler его обработал;

  • какой ответ бот попытался отправить;

  • как изменилось состояние.

Для библиотечного кода это особенно важно.

Codex может быстро сгенерировать много инфраструктуры, но ошибки в деталях всплывают именно на тестах, demo‑приложениях и реальных вызовах API.

Особенно это касается моделей MAX API. Если ИИ придумал поле, которого нет в документации, компилятор может ничего не заметить. Ошибка проявится только когда реальный API вернёт или не вернёт ожидаемые данные.

Vanilla Java

Хотя основной сценарий для меня ‑Spring Boot, библиотека не завязана только на него.

В vanilla Java можно руками собрать:

  • MaxBotClient;

  • Router;

  • Dispatcher;

  • FSM storage;

  • polling runner.

Упрощённо это выглядит так:

MaxApiClientConfig config = MaxApiClientConfig.builder()
        .token(System.getenv("MAX_BOT_TOKEN"))
        .build();

MaxHttpClient transport = new OkHttpMaxHttpClient(config.baseUri(), okHttpClient);
JsonCodec jsonCodec = new JacksonJsonCodec();
MaxBotClient botClient = new DefaultMaxBotClient(config, transport, jsonCodec);

Router router = new Router("main");

router.message(BuiltInFilters.command("start"), (message, ctx) -> {
    ctx.reply(Messages.text("Привет из vanilla Java"));
    return CompletableFuture.completedFuture(null);
});

Dispatcher dispatcher = new Dispatcher()
        .withBotClient(botClient)
        .withFsmStorage(new MemoryStorage())
        .includeRouter(router);

Такой вариант полезен, если не хочется тащить фреймворк или нужно встроить бота в существующее приложение.

Где Codex помог

Codex хорошо помог в местах, где нужно много однотипной работы.

Например:

  • создать структуру модулей;

  • набросать DTO;

  • подготовить builder’ы;

  • сделать первичный client layer;

  • собрать starter’ы;

  • написать часть тестов;

  • подготовить документацию;

  • быстро накидать demo‑примеры.

Если задача хорошо ограничена, результат получается полезным.

Например:

Сделай Spring Boot auto‑configuration для уже существующих компонентов.

Или:

Напиши тесты для этого router handler.

Или:

Подготовь README для модуля по уже существующему коду.

В таких местах ИИ действительно ускоряет работу.

Где Codex мешал

Самая неприятная категория ошибок — выдуманные элементы API.

Codex периодически писал код так, как будто он знает, каким должен быть MAX Bot API. Он достраивал модели по аналогии с другими bot API или просто по внутренней логике.

В результате появлялись поля и объекты, которых не было в документации.

На первый взгляд код выглядел нормально:

  • классы назывались правдоподобно;

  • JSON‑поля выглядели логично;

  • builder’ы собирались;

  • тесты могли проходить;

  • IDE не показывала проблем.

Но при сверке с документацией или реальном запросе выяснялось, что часть модели не соответствует API.

Это особенно опасно в библиотеке.

В прикладном коде ошибка может затронуть один сценарий. В библиотеке ошибка в модели или transport‑слое размножается по всем пользователям библиотеки.

Поэтому большую часть времени после генерации я тратил не на исправление синтаксиса, а на проверку соответствия документации:

  • есть ли такое поле в API;

  • правильно ли оно называется;

  • обязательное оно или нет;

  • какой у него тип;

  • может ли оно быть null;

  • есть ли такой объект в реальном response;

  • не добавил ли ИИ лишний enum;

  • не объединил ли он две разные сущности в одну.

После этого пришлось сделать для себя правило:

Если Codex пишет код для внешнего API, каждую модель нужно проверять по документации. Не выборочно, а полностью.

Что получилось удачно

Первое: разделение на модули.

Можно использовать только typed client, можно подключить dispatcher, а можно взять starter под нужный фреймворк.

Второе: аннотационный routing и screen API.

Для небольшого demo‑бота можно жить на командах. Но для реального пользовательского интерфейса внутри бота screens оказались намного удобнее.

Третье: Codex хорошо помог на boilerplate‑коде.

DTO, builders, starter boilerplate, первичные тесты, документация — это как раз те места, где ИИ экономит время.

Четвёртое: demo‑приложения.

Я специально держу demo‑проекты для разных фреймворков, чтобы проверять не только компиляцию библиотеки, но и реальное подключение starter’ов.

Что я бы сделал иначе

Если бы я начинал заново, я бы жёстче разделил работу с Codex на два режима.

Первый режим — генерация инфраструктуры.

Здесь можно давать ИИ больше свободы:

  • starter;

  • wiring;

  • тестовые утилиты;

  • документация;

  • примеры;

  • boilerplate.

Второй режим — модели внешнего API.

Здесь свободы быть не должно.

Для таких задач я бы сразу задавал ограничения:

  • не добавлять поля, которых нет в документации;

  • не придумывать enum‑значения;

  • не объединять похожие сущности;

  • не переносить поведение из других API;

  • если в документации нет информации — оставлять TODO, а не фантазировать.

И отдельно вести чек‑лист сверки с документацией.

Потому что именно несоответствие реальному API оказалось главным источником ошибок после генерации.

Где это используется в T2M Bridge

В T2M Bridge maxlib используется как MAX‑часть инфраструктуры.

С её помощью бот:

  • принимает события от MAX;

  • показывает пользователю экраны;

  • обрабатывает callback’и;

  • хранит состояние сценариев;

  • работает с каналами;

  • отправляет сообщения и медиа;

  • участвует в связке MAX‑канала с Telegram‑каналом.

В этой статье я не разбираю сам кросспостинг подробно. Это тема третьей части цикла.

Там уже будут вопросы другого уровня:

  • как связать аккаунты пользователя в двух мессенджерах;

  • как сопоставить Telegram‑канал и MAX‑канал;

  • как хранить соответствия между исходными и перенесёнными постами;

  • как переносить разные типы медиа;

  • что делать с replies, редактированием и удалением;

  • где проходит граница между библиотекой и бизнес‑логикой бота.

Итоги

maxlib начиналась как вспомогательная библиотека для одного бота.

Я не хотел тратить на неё слишком много времени, поэтому значительную часть инфраструктурного кода писал с помощью Codex. Это действительно ускорило старт, но не отменило ручное проектирование, проверку и исправление ошибок.

Главная проблема после ИИ была не в том, что код не компилировался. Наоборот, чаще всего он выглядел вполне убедительно.

Проблема была в том, что Codex периодически придумывал поля, объекты и структуры, которых не было в документации MAX Bot API.

Поэтому основной вывод получился такой:

ИИ хорошо ускоряет написание инфраструктурного кода, но при работе с внешним API источником истины должна оставаться документация, а не сгенерированная модель.

В текущем виде maxlib закрывает несколько уровней:

  • typed client к MAX API;

  • dispatcher/router runtime;

  • фильтры и middleware;

  • FSM;

  • screens и widgets;

  • polling и webhook;

  • starter’ы для Spring Boot, Micronaut и Quarkus;

  • testkit.

В следующей статье расскажу про telegalib — Java‑библиотеку для Telegram Bot API, которую я писал уже иначе: не настолько «сгенерировать каркас и поправить», а ближе к формату 50/50 между мной и Codex.

Репозиторий maxlib:

https://github.com/tardyon‑soft/maxlib

Живой MVP и документация T2M Bridge:

https://docs.t2m‑bridge.ru

Новости разработки и roadmap:

https://t.me/telega2max