Интеграция ИИ-бота в Slack через API

Slack ждёт ответ на команду за три секунды, а модель может думать дольше. Разбираем, как обойти это через response_url, и рабочий код на Bolt.

4 мин · · Обновлено · разработка

Slack-бот на своём ключе решает не техническую, а протокольную задачу: Slack требует подтверждение команды за 3 секунды, а модель, особенно с включённым рассуждением (extended thinking), может думать заметно дольше. Прямой синхронный вызов из обработчика команды регулярно приводит к «operation_timeout» на стороне Slack, даже если ответ от модели в итоге пришёл.

Схема: сразу подтвердить, ответить позже через response_url

Slack Events API и slash-команды дают response_url — отдельный вебхук, на который можно отправить сообщение уже после того, как обработчик команды вернул мгновенный ack. Это и есть обход таймаута:

bash
// slack-handler.js (Express + Slack Bolt)
import { App } from "@slack/bolt";

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

app.command("/ask", async ({ command, ack, respond }) => {
  await ack(); // подтверждаем команду сразу, у Slack есть только 3 секунды

  try {
    const r = await fetch("https://api.zukko.pro/v1/messages", {
      method: "POST",

ack() уходит в Slack мгновенно, а сам вызов к api.zukko.pro и последующий respond() через response_url могут занимать сколько угодно секунд — Slack это уже не ограничивает.

Если нужен OpenAI-совместимый вариант

Та же логика с /v1/chat/completions, отличается только тело запроса и путь к ответу:

bash
const r = await fetch("https://api.zukko.pro/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ZUKKO_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "gpt-5.6-sol",
    messages: [{ role: "user", content: command.text }]
  })
});
const data = await r.json();
await respond(data.choices[0].message.content);

Мониторинг баланса на уровне воркспейса

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

bash
curl https://api.zukko.pro/v1/balance \
  -H "Authorization: Bearer fetch_ваш_ключ"

и присылает предупреждение в служебный канал, если баланс упал ниже порога. Дешевле поймать это заранее, чем получить от бота серию 402 insufficient_balance в рабочее время, когда пользователи как раз активно им пользуются.

Ошибки, которые стоит различать в ответе бота

  • 401 — неверный или истёкший ключ. Для Slack-бота это означает, что переменная окружения с ключом не доехала до продакшена — стоит проверять при деплое, а не только при первой жалобе пользователей.
  • 403 model_not_allowed_for_key — если на ключе настроен список разрешённых моделей (страница ключа в кабинете), а бот пытается вызвать что-то за его пределами. Полезно держать список разрешённых моделей бота и лимита ключа синхронизированными.
  • 429 с key_daily_limit_reached — рабочий чат с активной командой легко упирается в собственный дневной лимит ключа (сброс в 00:00 UTC; месячный — key_monthly_limit_reached). Своего rate limit у шлюза нет, поэтому прочие 429 приходят от провайдера. Сообщение пользователю в Slack стоит отличать от «модель недоступна».
  • 503 model_disabled — модель выключена. Стоит логировать такую ошибку и обновлять model в коде бота (актуальный список — GET https://api.zukko.pro/v1/models), а не ждать, пока кто-то заметит, что бот перестал отвечать.

Считаем стоимость команды

Каждый обычный (нестриминговый) ответ от /v1/messages или /v1/chat/completions несёт заголовок X-Zukko-Cost-Micros — стоимость конкретного вызова в микродолларах; у потоковых ответов (stream: true) этого заголовка нет. Если бот используется несколькими каналами или командами в компании, стоит логировать этот заголовок вместе с именем канала и юзера, вызвавшего команду — тогда на конец месяца видно, какая команда сколько потратила. Через /v1/usage такую разбивку не получить: история там общая на весь аккаунт, без фильтра по ключу.

Кэш для повторяющихся команд

Если /ask часто вызывают с одинаковым текстом (например, шаблонная команда «переведи это» в тред), включите кэш явным заголовком X-Zukko-Cache: true — это основной способ, он работает для любой модели. temperature: 0 тоже включает кэш, но только для моделей, которые принимают это значение: новые модели Claude (claude-sonnet-5, claude-opus-5-5) могут отклонить temperature, отличную от 1, с ошибкой 400, поэтому в запросах к ним temperature лучше не передавать. Повтор запроса с полностью совпадающим телом (тот же текст, модель и параметры — «почти одинаковые» запросы не считаются) в течение 7 суток возвращается мгновенно и бесплатно, с X-Zukko-Cache: hit в ответе. Такие попадания в кэш не записываются в /v1/usage.

Idempotency-Key для повторной доставки событий

Slack повторяет доставку событий и команд, если ваш сервер не ответил достаточно быстро на исходный HTTP-запрос — это отдельная деталь протокола, не связанная с response_url. Если обработчик успел уйти в повтор, а фоновый вызов к модели всё ещё выполняется, без защиты можно списать деньги дважды за одно действие пользователя. Добавьте Idempotency-Key в сам запрос к модели — уникальный для каждого действия пользователя и одинаковый при его повторной доставке. Подходит id команды Slack command.trigger_id; не генерируйте ключ заново при повторе (например, из текущего времени), иначе защита не сработает:

bash
headers: {
  "Authorization": `Bearer ${process.env.ZUKKO_KEY}`,
  "Content-Type": "application/json",
  "anthropic-version": "2023-06-01",
  "Idempotency-Key": command.trigger_id
}

Повторный запрос с тем же ключом идемпотентности в течение 24 часов вернёт тот же ответ без повторного списания.

Разные модели для разных команд Slack

Один и тот же бот может обслуживать несколько slash-команд с разными задачами: короткая /summarize не нуждается в тяжёлой модели с расширенным контекстом, а /review-code, где в сообщение вставлен большой diff, выигрывает от контекста в 1,000,000 токенов, который есть у моделей Claude и GPT в каталоге (у GLM окно 128K). Разумно завести соответствие команда → модель в конфиге бота, а не хардкодить одну модель на все случаи — это одновременно снижает расход и ускоряет ответ там, где глубина не нужна.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.