- /archive: последние 10 сообщений архива с кнопками 🗑 (удалить) и «⬅️ Раньше» / «⏭ К последним» - /archive_del 120-130 135: удалить пачку по номерам, с диапазонами - Служебные команды боту больше не записываются в архив (/ask остаётся — в нём сам вопрос) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
9.8 KiB
Markdown
83 lines
9.8 KiB
Markdown
# Boltun AI
|
||
|
||
Telegram-бот для групповых чатов с AI-личностями. Стек: Node.js, TypeScript, [grammY](https://grammy.dev), [Mastra](https://mastra.ai) (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](https://mastra.ai/docs/memory/message-history) (`memory.db` рядом с `DB_PATH`). Подкладывается в каждый запрос, поэтому можно уточнять «а подробнее?». Самые старые реплики Mastra срезает, когда промпт не влезает в `CONTEXT_TOKEN_LIMIT`; результаты инструментов из прошлых ответов (`ToolCallFilter`) и фото в историю не попадают. `/clear` сбрасывает только контекст.
|
||
- *Архив чата* — вся переписка (живая и импортированная). Модель читает его инструментами по необходимости; `/clear` его не трогает.
|
||
- `/archive` — архив чата с кнопками 🗑 для удаления сообщений и «⬅️ Раньше» для листания; `/archive_del 120-130 135` — удалить пачку по номерам. Служебные команды боту в архив не пишутся.
|
||
- `/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) **до** добавления в группу или сделайте бота админом группы.
|
||
|
||
## Запуск локально
|
||
|
||
```bash
|
||
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.
|