yunogasai fd2a219ae1 Прошлые размышления модели не уходят в историю: фикс ошибок Gemini
- ProviderHistoryCompat с правилом strip-past-reasoning: из прошлых реплик вырезаются reasoning-части
  и подписи (reasoning_details), текущий ответ не трогается — Gemini нужны подписи внутри цикла
- Причина: подписи Gemini 3 привязаны к исходному контексту, а из истории вырезаются вызовы инструментов
  и в треде смешаны ответы разных моделей — провайдер отвечал ошибкой (finishReason other/error)
- Пустые реплики ассистента из упавших запросов тоже не отправляются
- onError: в лог пишется текст ошибки провайдера

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:27:48 +02:00

Boltun AI

Telegram-бот для групповых чатов с AI-личностями. Стек: Node.js, TypeScript, grammY, Mastra (OpenRouter или DeepSeek напрямую), SQLite.

Возможности

  • Отвечает только администраторам из ADMIN_IDS, остальных молча игнорирует.
  • Вопросы: /ask <вопрос>, упоминание @бота, ответ на сообщение бота, в личке — любое сообщение.
  • Контекст из чата: бот сохраняет сообщения чата в SQLite (последние HISTORY_LIMIT на чат). Попросите «посмотри последние 100 сообщений и перескажи» — агент сам вызовет инструмент read_chat_messages и загрузит переписку в контекст. Если задать /ask ответом на чьё-то сообщение, оно тоже попадёт в контекст.
  • Участники: бот запоминает имя, фамилию и ник каждого, кто пишет в чат (и тех, кого упомянули по имени-ссылке). Можно спрашивать «что писал @ivan?», «что думает Иван про выручку?», «кто тут самый активный?» — агент найдёт человека по нику или имени (инструменты list_chat_members, read_member_messages) и обращается к людям по имени.
  • Фото: пришли фото с подписью, где упомянут бот (в личке — просто фото), или ответь на фото через /ask / упоминание — модель посмотрит картинку. Нужна модель со зрением (Gemini через OpenRouter по умолчанию); DeepSeek фото не видит и честно об этом скажет.
  • Память — две части:
    • Контекст диалога — вопросы к боту и его ответы в этом чате, хранит Mastra Memory (memory.db рядом с DB_PATH). Подкладывается в каждый запрос, поэтому можно уточнять «а подробнее?». Самые старые реплики Mastra срезает, когда промпт не влезает в CONTEXT_TOKEN_LIMIT; результаты инструментов из прошлых ответов (ToolCallFilter) и фото в историю не попадают. /clear сбрасывает только контекст.
    • Архив чата — вся переписка (живая и импортированная). Модель читает его инструментами по необходимости; /clear его не трогает.
    • /context — что сейчас в контексте и в архиве, ID чата, видит ли бот всю переписку; /import — загрузить старую переписку из экспорта (см. ниже).
  • Личности: задаются в коде, из чата не редактируются. Сейчас одна — boltun (Dirty D).
    • /personas — список и выбор кнопками
    • /persona <id> — выбрать

Импорт старой переписки

Telegram не отдаёт ботам сообщения, написанные до их добавления, — их можно загрузить из экспорта.

  1. Telegram Desktop → меню чата → «Экспорт истории чата». Сними галочки с фото, видео и файлов, формат — «Машиночитаемый JSON».
  2. Пришли боту result.json с подписью /import — прямо в этот чат или боту в личку (в личке бот сам поймёт, какой это чат). Можно и ответить /import на уже отправленный файл.
  3. Бот ответит, сколько сообщений и участников добавил.

Загружаются только сообщения с текстом: стикеры, голосовые, опросы и медиа без подписи пропускаются. HTML-экспорт не подходит — в нём нет ID авторов.

  • Импорт идемпотентен: повторная загрузка ничего не дублирует, уже сохранённые ботом сообщения и ники не перезаписываются.
  • Хранится не больше HISTORY_LIMIT последних сообщений на чат — для большой истории подними лимит.
  • Ников в экспорте нет, только имена: участники из экспорта находятся по имени, ник подтянется, когда человек напишет при боте.
  • Файл больше 20 МБ бот скачать не может (ограничение Bot API). Тогда положи его в volume (/app/data) и запусти в контейнере node dist/scripts/import-history.js /app/data/result.json (локально — pnpm import-history:dev result.json).

Промпты и личности

  • prompts/system.md — общие правила бота (формат ответа, инструменты). Читается при старте.
  • prompts/personas/<id>.md — промпт личности; список личностей (id и название) — в src/constants/personas.ts.
  • pnpm seed синхронизирует личности с БД (добавляет, обновляет, удаляет лишние). В Docker seed выполняется при каждом старте контейнера, локально — в pnpm dev.

Поменял промпт — перезапусти бота (или pnpm seed + перезапуск).

Telegram Bot API не отдаёт историю чата задним числом: бот видит только сообщения, пришедшие после его добавления. Чтобы он видел все сообщения группы, отключите privacy mode в @BotFather (/setprivacy → Disable) до добавления в группу или сделайте бота админом группы.

Запуск локально

nvm use            # Node 24
pnpm install
cp .env.example .env   # заполнить BOT_TOKEN, ADMIN_IDS и OPENROUTER_API_KEY (или DEEPSEEK_API_KEY)
pnpm dev

Свой Telegram ID можно узнать, написав боту /whoami в личку.

Переменные окружения

Переменная Обязательна Описание
BOT_TOKEN да токен от @BotFather
ADMIN_IDS да Telegram ID админов через запятую
OPENROUTER_API_KEY да* ключ OpenRouter; модель по умолчанию — openrouter/google/gemini-3.8-flash (видит фото)
DEEPSEEK_API_KEY да* ключ DeepSeek напрямую (api.deepseek.com); используется по умолчанию, если ключа OpenRouter нет — модель deepseek/deepseek-flash, только текст
AI_MODEL нет явная модель Mastra model router <провайдер>/<модель>, например deepseek/deepseek-v4-pro
AI_IMAGE_INPUT нет true/false — отправлять ли модели фото. Для моделей по умолчанию выставляется само (Gemini — да, DeepSeek — нет); для своей AI_MODEL по умолчанию false

* нужен ключ того провайдера, чья модель выбрана; бот проверяет это при старте. | DB_PATH | нет | путь к SQLite, по умолчанию data/bot.db (в Docker — /app/data/bot.db) | | HISTORY_LIMIT | нет | сколько сообщений на чат хранить в архиве, по умолчанию 5000 | | CONTEXT_TOKEN_LIMIT | нет | бюджет токенов на весь промпт (правила, история диалога, вопрос); сверх него срезаются самые старые реплики. По умолчанию 12000 | | ANSWER_TOKEN_LIMIT | нет | максимальная длина ответа в токенах, по умолчанию 1500 |

Деплой (Dokploy)

Приложение собирается из Dockerfile (Build Type: Dockerfile). Бот работает через long polling — домен и порты не нужны, достаточно одной реплики.

  1. Создать проект и приложение, источник — этот git-репозиторий, ветка main.
  2. Build Type — Dockerfile.
  3. Environment — переменные из таблицы выше.
  4. Volumes — volume, смонтированный в /app/data (там SQLite-база, иначе история и личности пропадут при передеплое).
  5. Deploy.
Description
No description provided
Readme 288 KiB
Languages
TypeScript 98.9%
Dockerfile 1.1%