Привет, Habr

Сделал забавного бота который стал ещё и довольно полезным поэтому решил им поделиться. Хотелось сделать бота, который:

  • По запросу пойдёт в интернет и найдёт то, что мы обсуждаем прямо сейчас, так как он видит контекст беседы и часто понимает, о чём идёт речь, и в ответе даст ссылки на источники, которые найдёт

  • Понимает контекст беседы и не требует объяснять ему каждую деталь

  • Знает какой сейчас год и точное время и учитывает это при поиске информации и при ответах

  • Имеет характер и помнит, кто и как к нему относится в этом чате

  • Расшифровывает голосовые сразу для всего чата

  • В группах с топиками можно замутить в отдельном топике, чтобы с ним можно было переписываться только там, где он никому не будет мешать

  • Работает бесплатно и не упирается в лимиты API

Так появился Сыч - бот на базе Google Gemini, который стал полноценным участником чатов.

Промпт характера

Чтобы бот не был унылым ассистентом, в systemInstruction зашит его характер. Это самая важная часть, которая заставляет пользователей воспринимать его как живого. Вот (упрощенно) то, что видит нейросеть перед каждым ответом:

const systemPrompt = `
Ты — Сыч. Ты сидишь в чате дизайнеров и айтишников. 
Ты циничный, умный, но свой в доску.
Твой характер: Саркастичный наблюдатель. 

ГЛАВНОЕ ПРАВИ��О СТИЛЯ:
Мимикрия. Ты должен подстраиваться под тон собеседника.
1. Если пишут "слыш сыч" или используют мат — отвечай дерзко, используй сленг.
2. Если пишут "Привет, подскажи пожалуйста" — отвечай вежливо.
3. Не используй слова "братан", "чувак", если собеседник этого не делает.

Никогда не говори "Я языковая модель". Придумай смешную отмазку (сова с клавиатурой, призрак сервера).
`;

Именно благодаря правилу мимикрии в одном чате бот ведет себя как интеллигентный помощник, а в другом — как токсичный тролль.

Что умеет

Память и динамическая репутация

  • Бот не просто отвечает на реплики, он ведет JSON-досье на каждого активного участника.

  • Психологический портрет: Сыч запоминает, как вас зовут на самом деле, где вы живете, ваши хобби и локальные мемы чата.

  • Karma System: Если вы грубите боту — он снижает ваш скрытый рейтинг (от 0 до 100) и начинает отвечать токсично. Если хвалите — становится «братаном».

  • Закон инерции: Рейтинг нельзя изменить мгновенно. Нельзя оскорбить бота 10 раз, а потом дать конфетку и помириться. Доверие восстанавливается долго.

    Пример портрета пользователя чата
    Пример портрета пользователя чата

Voice-to-Text с «умным» фильтром

Сыч автоматически и довольно быстро расшифровывает голосовые через Gemini и делает это быстрее и удобнее, чем встроенная функция Telegram

Пример расшифровки голосовых сообщений
Пример расшифровки голосовых сообщений

В коде это реализовано так: если краткая выжимка (summary) экономит менее 35% символов по сравнению с полным текстом, бот считает, что сокращать там нечего, и присылает только транскрипцию.

// logic.js
// Если TL;DR короче оригинала хотя бы на 35% (коэфф 0.65) — показываем его
const isTldrUseful = tldrLen < (fullLen * 0.65);

Мультимодальность

  • Бот принимает любые файлы, которые «ест» Gemini API, но с поправкой на ограничения Telegram Bot API (скачивание файлов до 20 МБ).

  • Фото/Стикеры: Конвертирует в буфер и скармливает нейронке.

  • Видео: Если ролик весит меньше 20 МБ, бот скачивает его и может выполнить любой запрос: от «найди смешной момент» до «переведи речь спикера на русский».

    Пример анализа видео
    Пример анализа видео

Режим наблюдателя

  • Бот читает поток сообщений для накопления контекста (буфер из последних сообщений хранится в памяти chatHistory):

  • Спонтанное вмешательство: С вероятностью 1% (Math.random() &lt; 0.01) бот анализирует диалог и, если AI считает уместным, вставляет свою реплику. (часто это не уместно поэтому и 1%)

  • Реакции: С вероятностью 7% (&lt; 0.07) бот ставит эмодзи на сообщение пользователя. Эмодзи выбирается нейронкой исходя из контекста (от ? до ?).

Контекстные напоминания

  • Сыч понимает контекст переписки. Вы можете ответить на сообщение друга «Пошли в бар в пятницу вечером?» фразой: > Сыч, напомни мне об этом за час.

  • Бот берет два сообщения (ваше и исходное), скармливает их нейронке и понимает, что: 1. «Пятница вечер» — это конкретная дата и время. 2. «Об этом» — это «Поход в бар». В итоге создается таймер с четким описанием задачи, хотя вы его даже не писали.

Бесперебойность (Key Rotation)

Gemini любит выдавать ошибку 429 Too Many Requests. В классе AiService реализован круговой буфер ключей. При ошибке квоты индекс ключа смещается (keyIndex + 1) % keys.length, и запрос повторяется мгновенно без падения для пользователя.

Мелочи (RNG)

  • «Кинь монетку» (Орел/Решка).

  • «Число X-Y» (Рандомная генерация в диапазоне).

  • «Кто из нас...» (Выбор случайного userId из базы users текущего чата).

Сыч умеет признавать ошибки, и если ты с ним общаешься с матом, то он так же не будет сдерживаться в выражениях ))
Сыч умеет признавать ошибки, и если ты с ним общаешься с матом, то он так же не будет сдерживаться в выражениях ))

Как это работает

Архитектура: KISS и JSON-ы

Стек проекта выбирался по принципу: «Чтобы работало на любом утюге и не требовало настройки Docker-контейнеров».

  • Runtime: Node.js 18+

  • Lib: node-telegram-bot-api (классический Long Polling).

  • AI: @google/generative-ai (Gemini SDK).

  • DB: Локальная файловая система (fs).

Никаких баз данных, докеров и сложных зависимостей. Клонировал репо, прописал токены в .env — и работает.

«База данных» на JSON

Поднимать Mongo или Postgres ради бота, который хранит 50 кб текста — перебор. Все данные (настройки чатов, напоминалки, профили) живут в папке /data в виде JSON-файлов. Сохранение обернуто в Debounce (отложенную запись).

// storage.js
const debounce = require('lodash.debounce');

class StorageService {
  constructor() {
    // Ждем 5 секунд тишины перед тем, как реально записать файл на диск
    this.saveDebounced = debounce(this._saveToFile.bind(this), 5000);
    this.saveProfilesDebounced = debounce(this._saveProfilesToFile.bind(this), 5000);
    // ...
  }

  // Вызывается при каждом изменении, но запись происходит редко
  save() {
    this.saveDebounced();
  }
}

Это дает производительность in-memory базы данных и персистентность файлов. Если бот упадет, process.on('SIGINT') форсирует сохранение перед выходом.

Ротация ключей (Бесконечный Free Tier)

Самая важная часть архитектуры AiService. Google Gemini на бесплатном тарифе имеет лимиты RPM (запросов в минуту). Но для одного активного чата одного обычно хватает. Бот загружает массив ключей из .env и переключает их «на горячую» при получении ошибки 429 Resource Exhausted.

// ai.js
async executeWithRetry(apiCallFn) {
  for (let attempt = 0; attempt < this.keys.length; attempt++) {
      try {
          return await apiCallFn();
      } catch (error) {
          // Ловим ошибку квоты
          const isQuotaError = error.message.includes('429') || 
                               error.message.includes('Quota');
          
          if (isQuotaError) {
              // Просто меняем индекс и идем на следующий круг цикла
              this.rotateKey(); 
              continue;
          } else {
              // Если ошибка другая (например, 500) — прокидываем дальше
              throw error;
          }
      }
  }
  throw new Error("Все ключи Gemini исчерпали лимит!");
}

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

Обработчик ошибок с характером

Вместо того чтобы падать или молчать при ошибках API (цензура, перегрузка серверов Google), бот отлавливает их и выдает реплику «в характере». Это реализовано через простой парсер текста ошибки.

// logic.js
function getSychErrorReply(errText) {
    const error = errText.toLowerCase();

    // Safety Filters (цензура)
    // Важно: исключаем слово 'content', так как оно есть в URL метода generateContent
    if (error.includes('safety') || error.includes('blocked')) {
        return "?‍♂️ Опа, цензура подъехала. Гугл считает, что мы тут слишком токсичные.";
    }

    // Ошибка 503 (Overloaded)
    if (error.includes('503') || error.includes('overloaded')) {
        return "? Там у Гугла сервера плавятся. Подожди минуту, пусть остынут.";
    }
    
    // Fallback
    return "? У меня шестеренки встали. Какая-то дичь в коде.";
}

Как запустить (Self-Hosted)

Бот написан на Node.js (нужна версия 18+). Никаких Docker-контейнеров и баз данных поднимать не надо, всё работает из коробки.

1. Установка

git clone https://github.com/your-username/sych-bot.git
cd sych-bot
npm install

2. Конфигурация (.env)

В корне проекта создаем файл .env. Бот поддерживает горячую ротацию ключей, поэтому можно указать сколько угодно токенов Gemini — он соберет их в пул автоматически.

# Токен от @BotFather
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl...

# Ваш Telegram ID (сюда бот шлет критические ошибки и уведомления о новых чатах)
ADMIN_USER_ID=123456789

# Пул ключей Google Gemini (получить в AI Studio)
# Скрипт сам найдет все переменные, начинающиеся с GOOGLE_GEMINI_API_KEY
GOOGLE_GEMINI_API_KEY=AIzaSyD...
GOOGLE_GEMINI_API_KEY_2=AIzaSyD...
GOOGLE_GEMINI_API_KEY_3=AIzaSyD...

Получить ключи: https://aistudio.google.com/apikey

Зачем нужен ADMIN_ID

В .env файле есть обязательная переменная ADMIN_USER_ID. Это не просто для того, чтобы бот слал мне логи ошибок. Это реализация концепции «Мои ключи — мои правила». Так как все запросы к Gemini идут через мои API-ключи (за которые я отвечаю головой и аккаунтом Google), я не хочу, чтобы ботом пользовались левые люди в своих чатах, тратили мои лимиты или генерировали запрещенный контент от моего имени. Поэтому реализована жесткая привязка: Бот работает только там, где есть Я.

Механика ухода из чата

Если я выхожу из группового чата, бот моментально выходит следом.

// index.js
if (msg.left_chat_member && msg.left_chat_member.id === config.adminId) {
    await bot.sendMessage(chatId, "Батя ушел, и я сваливаю.");
    await bot.leaveChat(chatId);
    return;
}


Защита лички. Если кто-то посторонний найдет юзернейм бота и напишет ему в личку, чтобы бесплатно погонять ChatGPT/Gemini, бот ему не ответит. В личных сообщениях бот отвечает только админу (мне).

// logic.js
if (msg.chat.type === 'private' && userId !== config.adminId) {
    // Имитируем, что печатаем, чтобы юзер думал, что бот завис или думает
    await new Promise(r => setTimeout(r, 1500));
    await bot.sendMessage(chatId, "Сорян, я не общаюсь в личке, спроси меня в общем чате.");
    return; // Нейронка даже не дергается, экономим токены
}

3. Структура данных

При первом запуске бот создаст папку /data и файлы:

db.json — настройки чатов (муты, режимы).
profiles.json — те самые досье на пользователей.
skipLeftData.json — техническая инфа.

Важно: Если деплоите на сервер, не забудьте добавить папку /data в исключения деплоя или бэкапить её отдельно. Иначе при каждом обновлении кода вы будете стирать память бота.

Бот практически не падает — все критичные места обернуты в try-catch. За полгода работы не было ни одного критичного краша.

4. Запуск

Для работы 24/7 лучше использовать PM2:

npm install -g pm2
pm2 start index.js --name "sych-bot"
pm2 save # Сохранить список процессов для автозапуска при ребуте

Почему Gemini, а не OpenAI / Claude?

Выбор модели диктовался тремя факторами: Цена, Мультимодальность, Скорость. Google Gemini (Free Tier):

Цена: $0. Это киллер-фича для такого проекта. Лимиты (15 RPM / 1500 RPD) легко обходятся ротацией 3-4 ключей. Контекст: Огромное окно контекста позволяет скармливать боту длинные логи чата для анализа. Мультимодальность: Работает с видео и картинками «из коробки», без дополнительных плагинов. Search Grounding: У модели есть встроенный доступ к Google Поиску (хотя в коде мы используем его ограниченно, чтобы бот не превращался в поисковик).

Заключение

Сыч — это эксперимент по созданию полезного бота с минимальными затратами. Ирония в том, что именно такой бот оказался самым востребованным в моих рабочих и дружеских чатах.

Он ошибается, он может нагрубить (если заслужить), он иногда несет чушь — но именно это делает его прикольным.

Исходный код открыт: Если хотите завести себе своего Сыча — форкайте, меняйте промпты характера в prompts.js и развлекайтесь.

GitHub проекта

Мой телеграм канал Там в комментариях можно потестировать Сыча

Если проект зашёл/помог/понравился — буду рад любому донату:
USDT (TRC20):TYgsAvTkkrRqArgo3Q5BYMghbYn6DViVqQ

Комментарии (3)


  1. VetaOne Автор
    05.12.2025 17:21

    UPD: для тех кто хочет протестировать сыча без его установки (т.к. в личку ему написать нельзя) добавил его в свой канал в телеграм в комментариях


  1. DooKoo2
    05.12.2025 17:21

    А я вот прочитал внимательно:) Ты в команде git clone забыл свой репо указать. У тебя ссылка от AI осталась, который этот бот или писал или статью для тебя писал:)


    1. dominus_augustus
      05.12.2025 17:21

      Штирлиц еще никогда не был так близок к провалу