Интеграция ИИ-бота в Slack через API
Slack ждёт ответ на команду за три секунды, а модель может думать дольше. Разбираем, как обойти это через response_url, и рабочий код на Bolt.
4 мин · · Обновлено · разработка
Slack-бот на своём ключе решает не техническую, а протокольную задачу: Slack требует подтверждение команды за 3 секунды, а модель, особенно с включённым рассуждением (extended thinking), может думать заметно дольше. Прямой синхронный вызов из обработчика команды регулярно приводит к «operation_timeout» на стороне Slack, даже если ответ от модели в итоге пришёл.
Схема: сразу подтвердить, ответить позже через response_url
Slack Events API и slash-команды дают response_url — отдельный вебхук, на который можно отправить сообщение уже после того, как обработчик команды вернул мгновенный ack. Это и есть обход таймаута:
// 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, отличается только тело запроса и путь к ответу:
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);Мониторинг баланса на уровне воркспейса
Если бот стоит в рабочем воркспейсе и им пользуется вся команда, полезно завести отдельную задачу, которая раз в день проверяет остаток:
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; не генерируйте ключ заново при повторе (например, из текущего времени), иначе защита не сработает:
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.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.