OpenAI в Telegram: как сделать бота на Node.js

В этой статье

Если нужен небольшой Telegram-бот с ответами OpenAI, отдельный фреймворк для Telegram не обязателен. Достаточно Node.js, двух секретов и нескольких HTTPS-запросов: бот получает обновление через getUpdates, передаёт текст в Responses API, а затем отправляет результат обратно методом sendMessage.

Ниже — локальный стартовый вариант на JavaScript. Он не хранит долговременную историю диалога и использует long polling. Это удобно, когда бот запускается на компьютере или на простом сервере без публичного HTTPS-адреса.

Что понадобится

Установите Node.js 20 или новее. В терминале создайте каталог проекта и установите официальный пакет OpenAI:

mkdir telegram-openai-bot
cd telegram-openai-bot
npm init -y
npm install openai

Понадобятся два секрета:

  • токен Telegram-бота, который выдаёт @BotFather;
  • ключ OPENAI_API_KEY из платформы OpenAI.

Telegram передаёт токен в запросах к адресу вида https://api.telegram.org/bot<token>/METHOD_NAME, а OpenAI SDK читает ключ из переменной окружения. Не вставляйте реальные значения в bot.mjs, коммиты и публичные примеры. Описание формата запросов есть в Telegram Bot API, а способ настройки SDK — в официальном quickstart OpenAI.

В Linux или macOS переменные можно задать так:

export TELEGRAM_BOT_TOKEN='123456789:замените-на-токен'
export OPENAI_API_KEY='sk-замените-на-ключ'
export OPENAI_MODEL='gpt-4.1-mini'

В PowerShell:

$env:TELEGRAM_BOT_TOKEN = "123456789:замените-на-токен"
$env:OPENAI_API_KEY = "sk-замените-на-ключ"
$env:OPENAI_MODEL = "gpt-4.1-mini"

В примере ниже модель берётся из OPENAI_MODEL, а если переменная не задана, используется значение по умолчанию. Если в вашем проекте доступна другая модель, укажите её явно.

Как устроен обмен сообщениями

Цикл выглядит так:

  1. getUpdates ждёт новые сообщения Telegram.
  2. Бот находит в обновлении message.text и chat.id.
  3. Текст передаётся в client.responses.create.
  4. Свойство response.output_text становится ответом бота.
  5. sendMessage отправляет этот текст в тот же чат.

После обработки обновления программа увеличивает offset. Благодаря этому Telegram не отдаёт одно и то же сообщение при следующем запросе — это правило также описано в FAQ Telegram о ботах. Вызовы getUpdates и webhook нельзя использовать одновременно; если у бота уже установлен webhook, перед запуском polling его нужно удалить через deleteWebhook.

Рабочий файл bot.mjs

Создайте файл bot.mjs и вставьте код:

import OpenAI from "openai";

const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
const openaiApiKey = process.env.OPENAI_API_KEY;
const model = process.env.OPENAI_MODEL || "gpt-4.1-mini";

if (!telegramToken) {
  throw new Error("Не задана переменная TELEGRAM_BOT_TOKEN");
}

if (!openaiApiKey) {
  throw new Error("Не задана переменная OPENAI_API_KEY");
}

const openai = new OpenAI({ apiKey: openaiApiKey });
const telegramApi = `https://api.telegram.org/bot${telegramToken}`;

async function telegram(method, body) {
  const response = await fetch(`${telegramApi}/${method}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });

  const data = await response.json();

  if (!response.ok || !data.ok) {
    throw new Error(`Telegram ${method}: ${data.description || response.statusText}`);
  }

  return data.result;
}

function splitMessage(text, maxLength = 4096) {
  const chunks = [];
  let rest = text.trim();

  while (rest.length > maxLength) {
    let cut = rest.lastIndexOf("\n", maxLength);
    if (cut < Math.floor(maxLength * 0.6)) {
      cut = rest.lastIndexOf(" ", maxLength);
    }
    if (cut < 1) {
      cut = maxLength;
    }

    chunks.push(rest.slice(0, cut).trim());
    rest = rest.slice(cut).trim();
  }

  if (rest) {
    chunks.push(rest);
  }

  return chunks;
}

async function answer(text) {
  const response = await openai.responses.create({
    model,
    input: text,
  });

  return response.output_text?.trim() || "OpenAI не вернул текстовый ответ.";
}

async function run() {
  let offset = 0;
  console.log("Бот запущен. Остановить: Ctrl+C");

  while (true) {
    try {
      const updates = await telegram("getUpdates", {
        offset,
        timeout: 30,
        allowed_updates: ["message"],
      });

      for (const update of updates) {
        offset = update.update_id + 1;

        const message = update.message;
        const text = message?.text;
        const chatId = message?.chat?.id;

        if (!text || chatId === undefined) {
          continue;
        }

        try {
          await telegram("sendChatAction", {
            chat_id: chatId,
            action: "typing",
          });

          const reply = await answer(text);

          for (const chunk of splitMessage(reply)) {
            await telegram("sendMessage", {
              chat_id: chatId,
              text: chunk,
            });
          }
        } catch (error) {
          console.error("Ошибка обработки сообщения:", error);
          await telegram("sendMessage", {
            chat_id: chatId,
            text: "Не удалось получить ответ. Проверьте ключ OpenAI и логи бота.",
          }).catch((sendError) => console.error("Ошибка отправки уведомления:", sendError));
        }
      }
    } catch (error) {
      console.error("Ошибка long polling:", error);
      await new Promise((resolve) => setTimeout(resolve, 3000));
    }
  }
}

run().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Вызов responses.create соответствует серверному JavaScript-примеру OpenAI, а текст берётся через response.output_text. Для Telegram важна другая деталь: sendMessage принимает не более 4096 символов после разбора сущностей. Поэтому функция splitMessage режет длинный ответ по переносу строки или пробелу до отправки.

Запуск и первая проверка

Запустите программу из того же терминала, где заданы переменные окружения:

node bot.mjs

Откройте чат с ботом в Telegram и отправьте, например:

Объясни в двух предложениях, что такое long polling

В логе появится сообщение о запуске, а в чате — ответ модели. Если реакции нет, сначала проверьте три вещи: бот действительно получил сообщение, переменная TELEGRAM_BOT_TOKEN не содержит лишних кавычек или пробелов, а OPENAI_API_KEY действителен и для проекта настроен биллинг. Telegram-бот и OpenAI API — разные учётные данные и разные сервисы.

Для отдельной проверки Telegram можно запросить информацию о боте:

curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getMe"

Если раньше у бота был webhook, удалите его один раз перед polling:

curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/deleteWebhook" \
  -H "content-type: application/json" \
  -d '{}'

После этого перезапустите node bot.mjs. Два экземпляра программы одновременно запускать не стоит: они будут конкурировать за одни и те же обновления.

Что учесть перед публикацией

Long polling подходит для локального старта и простого развёртывания. Для production-сценария с публичным HTTPS обычно переходят на webhook: Telegram сам отправляет обновления на ваш URL, а сервер отвечает быстро и контролирует повторную обработку. Это отдельная схема — не добавляйте webhook поверх работающего getUpdates.

Пример намеренно не добавляет долговременную историю. Каждый запрос отправляется в OpenAI отдельно, поэтому бот не помнит предыдущие реплики. Если позже вы решите сохранять историю, заранее определите срок хранения, доступ к базе и состав передаваемых данных. В документации OpenAI о настройках хранения указано стандартное хранение состояния приложения в течение 30 дней; параметр store и собственное хранение следует выбирать осознанно, а сообщения пользователей не стоит передавать без необходимости.

Наконец, не путайте API-ключ с подпиской ChatGPT. Для бота нужен доступ к платформе OpenAI API и отдельная настройка биллинга. Если после технической части вам нужен именно доступ к ChatGPT для аккаунта, а не пополнение OpenAI API, варианты можно посмотреть в каталоге Amber Market; условия и наличие перед оформлением нужно перепроверить на странице.

Источники

Есть следующая задача?Ещё по теме «Telegram-боты» →