Агент-адаптер: один текст на 5 платформ

Вечером 7 октября конвейер кофейни «Зерно» дошёл до картинки и получил отказ. Шлюз CloseRouter на любую модель отвечал 500 internal_error, хотя днём тот же запрос работал. Пост всё равно вышел: в Telegram-канал текстом, во ВКонтакте тоже текстом. В этом уроке вы научите сценарий заказывать картинку к посту и спокойно публиковать пост без неё, если картинка не пришла.

Чему научитесь в этом уроке:
• понимать, что каждая площадка принимает от сценария: Telegram умеет фото с подписью, ВКонтакте с ключом сообщества принимает только текст;
• заказывать картинку у модели через шлюз CloseRouter одним узлом HTTP Request;
• собирать черновик с флагом hasImage, чтобы пост не терялся без картинки.

Один текст, две площадки

Копирайтер и редактор из урока 3.3 отдают один готовый текст на 400–600 знаков. В курсе он уходит на обе площадки как есть: такой длины пост без правок помещается и в Telegram, и во ВКонтакте. Разница в том, что площадка готова принять вместе с текстом.

  • Telegram. Бот умеет отправить в канал простое сообщение (Send Message) или фото с подписью (Send Photo). Бот может взять фото по ссылке, а текст поста положить в подпись. Нашу ссылку на картинку в формате webp в прогоне курса через Send Photo не отправляли, об этом ниже. Для справки: подпись к фото в Telegram ограничена 1024 знаками, пост в 400–600 знаков в неё помещается с запасом.
  • ВКонтакте. Пост на стену сообщества публикует метод wall.post с ключом сообщества. Как получить ключ и настроить запрос, разберём в уроке 3.6. Текст так уходит без вопросов. С фото хуже: чтобы приложить картинку, её сначала загружают на сервер ВК методом photos.getWallUploadServer, а с ключом сообщества этот метод отвечает ошибкой 27 «Group authorization failed». Загружать фото на стену может только ключ пользователя, и это отдельная настройка.

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

Когда отдельная версия текста под площадку имеет смысл? Например, если во ВКонтакте вы хотите длинный пост с хештегами, а в Telegram короткий. Тогда после редактора можно поставить ещё один узел модели, который переписывает готовый пост под вторую площадку по своим правилам. Это идея на будущее: в прогоне курса такой узел не собирали, и в уроках модуля его нет.

Фотограф на заказ

Представьте, что к каждому посту вы вызываете фотографа. Вы говорите ему сюжет, он приносит снимок и оставляет ссылку, по которой снимок лежит 30 дней. Шлюз CloseRouter, через который вы с трека 2 подключаете модели (урок «Подключаем модель»), работает именно так, только фотограф там нейросеть.

Заказ делает узел HTTP Request: он отправляет запрос на адрес в интернете и возвращает ответ. Добавьте его после узла «Итог редактора», назовите «Картинка» и заполните:

  • Method: POST;
  • URL: https://api.closerouter.dev/v1/images/generations;
  • Authentication: Predefined Credential Type, тип OpenAI, те же учётные данные CloseRouter, что у модели копирайтера;
  • Send Body: включить, формат JSON, тело задайте выражением:
{{ {
  "model": "google/imagen-4-fast",
  "n": 1,
  "size": "1024x1024",
  "prompt": "Фото для поста кофейни, без текста и логотипов, мягкий свет. Сюжет: " + $json.topic
} }}

Четыре поля тела: model выбирает художника, n сколько картинок, size размер в точках, prompt сюжет. Тело записано выражением-объектом в двойных фигурных скобках. При подготовке курса такая запись оказалась надёжнее, чем сборка строки через JSON.stringify(...), поэтому берите форму как в примере.

Ответ приходит в поле data[0].url. Это публичная ссылка на картинку в формате webp (формат картинки, как jpg): она открывается без ключа и живёт 30 дней. Этого хватает, чтобы пост прошёл одобрение и ушёл в канал.

Картинка, которую модель google/imagen-4-fast сделала при подготовке урока: чашка фильтр-кофе светлой обжарки на деревянном столе у окна, рядом веточка жасмина и ягоды черники, мягкий утренний свет, без текста и логотипов.

Какую модель взять и сколько это стоит

Список моделей шлюза отдаёт запрос GET https://api.closerouter.dev/v1/models с теми же учётными данными. При подготовке урока он вернул 118 моделей, из них около 30 рисуют картинки или видео. Три из них мы попробовали на одном сюжете:

  • google/imagen-4-fast: 8,7 секунды, 0,045 $, картинка годная. Её мы и поставили в конвейер;
  • bytedance/seedream-4.5: 57 секунд, 0,073 $;
  • openai/gpt-image-1.5: вернула ошибку «Your request is invalid or could not be processed». Возможно, ей нужен другой размер; если хотите её, проверьте сами.

Картинка обходится примерно в 500 раз дороже текста поста: картинка стоила 0,045 $, а пост на gemini-3.6-flash при замере обошёлся в 0,000088 $. При 30 постах в месяц картинки съедят около 1,35 $. Сумма небольшая, но заметная рядом с текстом, и её стоит держать в голове, когда выбираете модель подороже.

Сюжет для картинки пишите без надписей и логотипов: буквы модели рисуют с ошибками, и пост с кривой вывеской выглядит хуже поста без фото. Поэтому в промпте прямо стоит «без текста и логотипов». Пример сюжета, на котором проверяли модели:

Фото для поста кофейни: чашка фильтр-кофе светлой обжарки на деревянном столе у окна, рядом веточка жасмина и несколько ягод черники, мягкий утренний свет, без текста и логотипов

Когда фотограф не пришёл

Тот самый вечер 7 октября: все три модели отвечали ошибкой 500 за доли секунды. Дело было не в деньгах: ключ почти не потрачен, на балансе 5,51 $, а неудачные запросы в журнал расходов не попали. Сломался сам шлюз. Такое случается у любого внешнего сервиса, и сценарий должен это пережить.

Фотограф иногда не приходит, а пост всё равно должен выйти. Для этого три настройки:

  1. Повтор. В узле «Картинка» на вкладке Settings включите Retry On Fail: 3 попытки через 5 секунд. Короткий сбой переживётся сам.
  2. Не останавливаться на ошибке. Там же On Error поставьте Continue. Если все попытки неудачны, сценарий пойдёт дальше без картинки. Подробнее о таких настройках в уроке «Надёжность» трека 2.
  3. Черновик с флагом. Узел Code «Черновик» собирает текст для одобрения и ставит флаг hasImage: есть картинка или нет. По этому флагу в уроке 3.6 развилка If «Есть картинка?» выберет, отправлять пост с фото или только текстом.

Код узла «Черновик»:

// Собираем текст черновика; картинка необязательна
const ed = $('Итог редактора').first().json;
const t = $('Следующая тема').first().json;
const img = $('Картинка').first().json;
const url = img && img.data && img.data[0] ? img.data[0].url : '';
const issues = (ed.issues || []).join('; ') || 'замечаний нет';
const msg = 'Черновик поста (' + t.date + ', ' + t.rubric + '):\n\n' + ed.text + '\n\nРедактор: ' + issues + '\nКартинка: ' + (url || 'не получена, пост уйдёт без фото');
return [{ json: { msg, url, hasImage: !!url } }];

Текст для одобрения собирается здесь, заранее, а не в самом сообщении узла одобрения. В прогоне выражение в Send and Wait обращалось к упавшему узлу «Картинка», и узел одобрения сам падал с ошибкой «reading ‘trim’». Когда текст готов в Code, в сообщение одобрения идёт просто {{ $json.msg }}. Как устроено одобрение кнопками, разберём в уроке 3.6.

Что в прогоне не проверено. В вечер публикации картинки не было, и пост ушёл в канал текстом. Отправку фото ссылкой на webp через Send Photo в прогоне курса до конца не довели. Если Telegram откажется брать ссылку, проверьте, открывается ли она в браузере, и попробуйте скачать картинку в n8n отдельным HTTP Request и отправить файлом.

Если не получилось

  • Узел «Картинка» отвечает 500 internal_error на любую модель. Это сбой шлюза. Проверьте баланс в кабинете CloseRouter; если деньги есть, подождите и запустите позже. Сценарий с On Error = Continue опубликует пост без фото.
  • Ошибка «Your request is invalid». Модель не приняла тело запроса. Вернитесь к google/imagen-4-fast и размеру 1024×1024, потом меняйте по одному полю.
  • n8n ругается на тело запроса. Проверьте, что тело записано выражением-объектом в {{ { ... } }}, а не строкой через JSON.stringify.
  • Во ВКонтакте ошибка 27 «Group authorization failed». Вы пытаетесь загрузить фото с ключом сообщества. Отправляйте во ВКонтакте только текст или настройте ключ пользователя.
  • На картинке кривые буквы или чужой логотип. Допишите в сюжет «без текста и логотипов».

Контрольный вопрос. Конвейер пытается приложить картинку к посту во ВКонтакте с ключом сообщества и получает ошибку 27 «Group authorization failed». Что это значит?

АЗакончились деньги на балансе шлюза
БКлюч сообщества не может загружать фото на стену, для этого нужен ключ пользователя
ВКартинка слишком большая
ГПост длиннее 600 знаков

Подсказка: Ошибка пришла от ВКонтакте, а не от шлюза картинок. Чей ключ вы использовали и что этим ключом пытались сделать?

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

Контрольный вопрос. Шлюз картинок вечером отвечает ошибкой 500 на все модели. Что должен сделать правильно собранный конвейер после трёх неудачных попыток?

АОстановиться с ошибкой, пост не публиковать
БВыбрать другую тему поста
ВПродолжить без картинки и собрать черновик с пометкой, что картинки нет
ГОтправить во ВКонтакте пустой пост

Подсказка: Вспомните настройку On Error у узла «Картинка» и флаг, который ставит узел «Черновик».

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

Контрольный вопрос. Какие утверждения о картинке через CloseRouter верны по итогам прогона? Выберите все подходящие.

Выберите все верные варианты.

АСсылка на картинку требует ключа для открытия
БСсылка живёт 30 дней
ВКартинка стоит примерно в 500 раз дороже текста поста
ГСюжет лучше писать без текста и логотипов
ДСсылка на картинку лежит в поле data[0].url

Подсказка: Перечитайте раздел «Фотограф на заказ»: что приходит в ответе и как долго это доступно.

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

Задание. МИССИЯ «Фотограф, который может не прийти». Добавьте в свой конвейер после «Итога редактора» узел «Картинка» (HTTP Request к https://api.closerouter.dev/v1/images/generations) с Retry On Fail 3×5 с и On Error = Continue, а за ним узел Code «Черновик» с флагом hasImage. Публиковать пока ничего не нужно, это будет в уроке 3.6. Сделайте два запуска: обычный и с намеренно сломанным запросом картинки (например, с неверным именем модели). Приложите: (1) сюжет картинки; (2) ссылку на готовую картинку или её снимок; (3) снимок сценария; (4) вывод узла «Черновик» из обоих запусков: с hasImage = true и с hasImage = false; (5) одну фразу: какая модель, сколько секунд и сколько стоила картинка, если видно в кабинете. Принято, если: в первом запуске получена картинка без надписей и логотипов, а в «Черновике» hasImage = true и есть ссылка; во втором запуске сценарий не остановился, hasImage = false и в тексте черновика написано, что картинка не получена; ключи на снимках закрыты.

Подсказка: Чтобы сломать запрос, допишите к имени модели лишнюю букву. Шлюз должен вернуть ошибку, в прогоне курса этот случай не проверяли. Если вдруг пришла картинка, посмотрите поле model в ответе и сломайте запрос иначе, например неверным адресом.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

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

Что дальше

Черновик с текстом и картинкой готов, а если картинки нет, черновик об этом честно пишет. В следующем уроке бот пришлёт его вам в Telegram с кнопками «Опубликовать» и «Отклонить», и пост уйдёт в канал и во ВКонтакте только после вашего нажатия.

Итог:
• один текст уходит на обе площадки; Telegram умеет фото с подписью (нашу webp-ссылку проверьте сами), ВКонтакте с ключом сообщества принимает только текст;
• картинку заказывает HTTP Request к CloseRouter, ссылка из data[0].url живёт 30 дней;
• google/imagen-4-fast отвечала за 8,7 секунды и 0,045 $, это примерно в 500 раз дороже текста;
• повтор, On Error = Continue и флаг hasImage в «Черновике» сохраняют пост и без картинки.

← Назад  ·  ↑ В начало урока  ·  ⌂ В начало курса  ·  Вперёд →

Школа Виктора Комлева