Отправить текст в Telegram можно одним HTTP-запросом. Но когда один и тот же канал доступен тредам, запланированным автоматизациям и нескольким проектам, этот запрос уже нельзя считать деталью сценария. Он становится внешним действием, которое трудно отменить: сообщение либо увидели подписчики, либо сеть оборвалась в момент, когда его статус неизвестен.
Поэтому я бы не добавлял токен Telegram в каждый workflow и не учил агента собирать URL Bot API. Для такого случая нужен отдельный BB-плагин. Он берёт на себя канал, права, черновики, журнал попыток и правило, по которому повторный вызов не создаёт ещё один пост. Тред или автоматизация остаются потребителями этой системы: они говорят, какой текст и куда хотят отправить, но не получают прямого доступа к Telegram.
Это статья о контракте такого плагина, а не отчёт о готовой интеграции. В конце оставляю полное задание, по которому его можно поручить агенту. Примеры кода ниже показывают логику доменного слоя и предполагаемый интерфейс CLI, а не копируют текущие типы SDK.

У отправки есть владелец
В BB плагин может добавить собственные CLI-команды, agent tools, SQLite-хранилище и фоновую работу. Для Telegram это удачная граница: в одном месте можно определить, кому разрешено писать в конкретный канал, как хранится история публикаций и что считать завершённой отправкой. Вместо нескольких слегка разных скриптов с токеном получается один владелец побочного действия.
У такого разделения есть практическое следствие. Автоматизация, которая подготовила пост, не должна знать токен бота. Тред, где агент написал текст, не должен угадывать chat_id. Оба вызывают плагин и передают ключ канала. Плагин сам проверяет, разрешён ли этому BB-проекту канал, включён ли он и допускает ли его режим публикацию.
Режимы называются draft и auto. В draft любой текст становится черновиком, а вызов публикации требует прямого подтверждения пользователя. В auto разрешённая автоматизация может отправлять материал сама. Это не делает режим безопасным по умолчанию: связь канала с проектом всё равно должна быть явной. Автоматизация из другого проекта не получает права только потому, что знает ключ канала.
Идемпотентность начинается до Telegram
Самая неприятная ошибка выглядит буднично: сервис отправил запрос, Telegram принял сообщение, а соединение оборвалось до ответа. Если просто повторить запрос, в канале появятся два одинаковых поста. Если не повторять, оператор не понимает, что произошло. Хеш текста здесь полезен для аудита и поиска похожих черновиков, но он не выражает намерение автора: одна и та же заметка может быть опубликована в разные дни, а одно намерение может получить исправление в тексте.
Для этого нужен отдельный idempotencyKey, стабильный в пределах одного действия. Например, редакционная автоматизация может использовать article:bb-plugin-telegram-publisher:announce, а ручной запуск получает ключ при создании черновика. В таблице публикаций пара channel_id и idempotency_key должна быть уникальной.
CREATE TABLE publications (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id),
idempotency_key TEXT NOT NULL,
text TEXT NOT NULL,
text_sha256 TEXT NOT NULL,
source_json TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN (
'draft', 'publishing', 'published', 'uncertain', 'failed'
)),
telegram_message_id TEXT,
last_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (channel_id, idempotency_key)
);
Плагин создаёт запись со статусом publishing до вызова Telegram. Успешный ответ приносит message_id, который сохраняется вместе со статусом published. Если во время запроса связь исчезла и невозможно доказать, что сообщение не дошло, запись получает статус uncertain. Автоматический повтор в таком состоянии опаснее паузы: сначала человеку нужно проверить канал или журнал Telegram.
Упрощённо это выглядит так. Транзакция создания должна защищать уникальный ключ, а запрос к Telegram выполняется уже после того, как состояние видно журналу.
async function publish(input: PublishInput): Promise<PublicationResult> {
assertTelegramTextLength(input.text);
const channel = await channels.findAllowed({
projectId: input.projectId,
channelKey: input.channelKey,
});
assertCanPublish(channel, input.initiator, input.userExplicitlyAsked);
const existing = await publications.find(channel.id, input.idempotencyKey);
if (existing?.status === "published") return toCompactResult(existing);
if (existing?.status === "uncertain") {
throw new PublicationUncertain(existing.id);
}
if (existing?.status === "publishing") {
throw new PublicationInProgress(existing.id);
}
const publication = await publications.createPublishing({
channelId: channel.id,
idempotencyKey: input.idempotencyKey,
text: input.text,
source: input.source,
});
try {
const message = await telegram.sendPlainText(channel.chatId, input.text);
await publications.markPublished(publication.id, message.message_id);
return { publicationId: publication.id, status: "published" };
} catch (error) {
const result = classify(error);
if (result.kind === "definite_failure") {
await publications.markFailed(publication.id, result.safeMessage);
} else {
await publications.markUncertain(publication.id, result.safeMessage);
}
throw error;
}
}
В настоящем коде ошибка Telegram с доказуемым отказом, например отсутствием права на отправку, может перейти в failed. Сетевая ошибка после начала передачи не относится к таким ошибкам. Её нельзя маскировать ретраем. Для неё нужен короткий ответ с идентификатором публикации и инструкция, что именно проверить.
Права канала и токен живут в разных местах
Токен бота не должен попадать ни в SQLite плагина, ни в проектный файл, ни в вывод команды. Он нужен transport-слою только на время вызова. Настройка BB для секрета подходит именно потому, что плагин видит значение при исполнении, а журнал и результат команды показывают лишь безопасные данные: ключ канала, статус и идентификатор публикации.
Доступ к каналу решает не токен. Его решает allow-list проектов. В хранилище достаточно отдельной связи channel_projects, которая хранит идентификатор канала и идентификатор разрешённого BB-проекта. Проверка должна быть частью каждого действия, включая просмотр черновиков. Иначе агент из случайного рабочего каталога сможет увидеть редакционные планы или направить публикацию в чужой канал.
Перед отправкой плагин может проверить токен и доступ бота без публикации: узнать самого бота, прочитать сведения о канале и его участнике. Такая проверка не обещает, что следующий пост обязательно пройдёт, но раньше показывает неверный chat_id, удалённого бота и недостаточные права. В Telegram Bot API текстовое sendMessage принимает от одного до 4096 символов после разбора сущностей. Пока плагин отправляет обычный текст без parse mode, он должен измерить лимит заранее и вернуть ошибку, а не незаметно отрезать конец сообщения.
function assertTelegramTextLength(text: string) {
const length = Array.from(text).length;
if (length < 1 || length > 4096) {
throw new ValidationError("Текст для Telegram должен содержать от 1 до 4096 символов.");
}
}
Форматирование лучше не включать «на всякий случай». Без parse_mode текст остаётся текстом, и публикация не меняет смысл из-за случайной звёздочки или фрагмента HTML. Позднее можно добавить Markdown или HTML отдельным параметром с валидацией и тестами для сущностей.
CLI остаётся удобным, но не становится обходным путём
Команды нужны не только агенту. Человеку полезно увидеть список каналов, создать черновик из файла и отдельно подтвердить публикацию. Однако CLI не должен превращаться в второй прямой клиент Bot API. Все команды проходят те же проверки проекта, режима и ключа идемпотентности, что и agent tools.
Так мог бы выглядеть контракт команд после установки плагина. Это пример желаемого интерфейса, а не команда, которая уже доступна в BB.
bb telegram-publisher channel add \
--key editorial \
--name "Редакционный канал" \
--chat-id @editorial_channel \
--project /work/newsroom \
--mode draft
bb telegram-publisher channel check --key editorial
bb telegram-publisher draft create \
--channel editorial \
--text-file ./post.txt \
--idempotency-key article:bb-plugin-telegram-publisher:announce
bb telegram-publisher publish \
--channel editorial \
--draft <draft-id> \
--idempotency-key article:bb-plugin-telegram-publisher:announce
Для длинного текста важен именно файл. В распределённой BB-среде нельзя трактовать путь, введённый в одном интерфейсе, как путь, который сервер может прочитать через node:fs. Плагин должен воспользоваться файловой API SDK на нужном хосте. В результате текст приходит из выбранного файла, но в JSON-ответе остаются только размер, хеш и короткий фрагмент, если он действительно нужен для различения черновиков.
Инструмент агента должен уметь отказаться
У agent tools простая форма не должна означать простую политику. Набор включает read-only инструмент списка каналов, создание черновика и публикацию. Список возвращает только доступные текущему проекту каналы и режимы. Черновик сохраняет текст и ключ идемпотентности. Инструмент публикации снова проверяет всё перед сетевым вызовом.
Описание инструмента публикации должно говорить об ограничении прямо: агент отправляет текст по явной просьбе пользователя или из разрешённой автоматизации в канале с auto. Фраза «подготовь пост» не равна «отправь пост». Если агент получил недостаточно определённую просьбу, правильным результатом становится черновик, а не публикация.
Здесь не нужен большой ответ с телом поста. Достаточно статуса, ключа канала, идентификатора публикации, Telegram message ID после успеха и кода ошибки. Это экономит контекст и не превращает диагностический лог в ещё одно хранилище неопубликованных текстов.
Тесты проверяют момент, когда уже поздно исправлять
Большинство тестов такой интеграции не должны обращаться в Telegram. Mock transport позволяет воспроизвести успешный ответ, детерминированную ошибку и обрыв соединения после попытки отправки. Особенно важен последний случай: тест ждёт uncertain и проверяет, что следующий вызов с тем же ключом не создаёт новый запрос.
Набор проверок получается довольно земным: миграция создаёт схему, проект без allow-list получает отказ, повтор уже опубликованного ключа возвращает сохранённый результат, длинный текст не уходит в транспорт, а ошибка API остаётся читаемой. Отдельно полезно прогнать CLI без токена: команда справки и просмотр каналов должны работать, а проверка канала должна ясно сказать, какой секрет нужно настроить, не прося его напечатать в чате.
После сборки стоит установить плагин локально и проверить ту же границу руками: пустой список, --help, тестовый канал, бот с нужными правами и подготовленный черновик. Реальное сообщение лучше оставить финальным действием, когда журнал и режимы уже проверены.
Источники
Полный текст задания для агента
Создай отдельный BB-плагин `bb-plugin-telegram-publisher`.
Это инфраструктурный плагин для подключения множества Telegram-каналов и
надёжной публикации из BB-тредов и BB-автоматизаций.
Граница ответственности:
- Плагин отвечает за Telegram: боты, каналы, права доступа, черновики,
отправку сообщений, логи и защиту от дублей.
- Автоматизации и треды не должны обращаться к Telegram Bot API напрямую:
только через CLI или agent tools этого плагина.
Перед реализацией:
- Прочитай актуальные SDK-типы и нужные инструкции BB plugin authoring.
- Создай плагин через `bb plugin new`; не пиши scaffold вручную.
- Используй текущий SDK как источник истины, не изобретай API.
Настройки плагина:
- `botToken`: обязательная secret-настройка плагина.
- `apiBaseUrl`: обычная настройка с безопасным значением по умолчанию
`https://api.telegram.org`.
- Никогда не сохраняй токен в SQLite, файлах проекта, выводе CLI, логах,
сообщениях агента или тестовых фикстурах.
- Если для реальной настройки потребуется токен, попроси его только через
безопасный механизм BB для секретов, не в чате.
Хранилище:
- Используй исключительно `bb.storage.database()` и миграции
`bb.storage.migrate()`. Не создавай SQLite-файл вручную в проекте.
- Сделай таблицы:
- `channels`: ключ канала, название, Telegram `chat_id` или `@username`,
включённость, режим (`draft` или `auto`), даты создания и изменения.
- `channel_projects`: связь каналов с разрешёнными BB-проектами.
- `publications`: канал, idempotency key, текст, хеш, источник, статус,
Telegram message ID, время публикации, последняя ошибка.
- при необходимости `publication_attempts` для журнала попыток.
- Уникальность публикации должна быть как минимум
`(channel_id, idempotency_key)`.
Безопасность публикаций:
- Канал доступен треду или автоматизации только при явном разрешении для
соответствующего проекта.
- Для каждой публикации обязателен `idempotencyKey`.
- Повторный вызов с уже опубликованным ключом должен вернуть сохранённый
результат и не создавать новое сообщение.
- Перед отправкой сохраняй состояние `publishing`.
- Если Telegram вернул успешный ответ, запиши `message_id` и статус
`published`.
- Если сеть оборвалась после потенциальной отправки и исход неизвестен,
ставь `uncertain` и не повторяй отправку автоматически.
- Не публикуй сообщения при ошибке, капче, отсутствии токена или неясном
состоянии.
- По умолчанию текст отправляется без Markdown/HTML parse mode. Поддержку
форматирования можно добавить явно и с валидацией позднее.
- Валидируй лимит Telegram на длину сообщения и возвращай понятную ошибку,
не обрезай текст молча.
CLI плагина:
Реализуй понятные команды с `defineCli`, полной справкой и JSON-выводом.
Нужны возможности:
- подключить канал: ключ, отображаемое название, `chat_id`, проект,
режим публикации;
- показать список каналов и их статус;
- включить или отключить канал;
- безопасно проверить токен и доступ бота к каналу без публикации;
- создать черновик;
- показать черновики и публикации;
- опубликовать текст или черновик с каналом и idempotency key.
Для многострочного текста CLI должен принимать файл. Учти multi-machine
правило BB: не читай пользовательский путь через `node:fs` на сервере;
используй SDK-файловую API соответствующего хоста.
Agent tools:
- Добавь read-only tool для списка доступных каналов.
- Добавь tool создания черновика.
- Добавь tool публикации.
- В инструкциях tool явно укажи: агент публикует только когда пользователь
прямо попросил отправить сообщение либо когда канал имеет режим `auto` и
вызов пришёл от разрешённой автоматизации.
- В agent tools также обязателен `idempotencyKey`.
- Вывод tools и CLI должен быть компактным и не содержать токенов или полного
текста больших сообщений.
Документация и проверка:
- Добавь собственный `skills/` каталог с инструкцией по настройке бота,
подключению канала, режимам `draft` и `auto`, CLI и agent tools.
- Добавь README с архитектурой, ограничениями и примерами команд.
- Добавь тесты миграций, project allow-list, идемпотентности, повторной
публикации, `uncertain`-состояния, ограничений длины и ошибок Telegram API.
- Не отправляй реальное сообщение при тестировании. Используй mock API или
тестовый transport.
- Выполни `bb plugin types` и `bb plugin build`.
- После сборки установи плагин локально, проверь `--help` и основные команды
без токена, затем сообщи, какие действия требуются от пользователя для
подключения бота и канала.
В финальном отчёте перечисли созданные файлы, результаты тестов, команды
и любые решения, которые потребуют
подтверждения пользователя.
