Telegram-плагин для BB: публикация как отдельная система.

Техническое задание на BB-плагин для Telegram, в котором права, черновики и неопределённые отправки важнее короткого вызова Bot API.

Аудиоверсия

Отправить текст в 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` и основные команды
  без токена, затем сообщи, какие действия требуются от пользователя для
  подключения бота и канала.

В финальном отчёте перечисли созданные файлы, результаты тестов, команды
и любые решения, которые потребуют
подтверждения пользователя.
К списку статей