Telegram-бот: пишет в чат через LLM, ведёт историю сообщений, делает пересказы, копит заметки об участниках и умеет менять «настроение». Одна инсталляция может обслуживать сразу несколько групп — у каждой свои промпты, модели и настроение, список групп управляется прямо из админки. Управление — через веб-админку.
- Python 3.12, aiogram 3.x, aiosqlite (сырой SQL, без ORM)
- Админка: FastAPI + Jinja2 + htmx, тёмная тема, без CDN
- LLM — через OpenRouter (OpenAI-совместимый endpoint)
git clone <repo> urumi-the-bot
cd urumi-the-bot
cp .env.example .env
$EDITOR .env # заполнить BOT_TOKEN, OPENROUTER_API_KEY, ADMIN_PASSWORD
docker compose up -d --build
docker compose logs -fАдминка поднимется на http://<host>:8080 (порт из ADMIN_PORT), вход — по
ADMIN_PASSWORD. Дальше — добавить бота в группу (см. ниже) и активировать её
на странице /chats; GROUP_CHAT_ID в .env теперь нужен только опционально,
для самого первого запуска.
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env && $EDITOR .env
.venv/bin/python main.pyБаза создаётся автоматически при старте (DB_PATH, по умолчанию data/urumi.db),
миграции применяются там же.
scripts/proxmox-install.sh — самостоятельный интерактивный установщик в духе
Proxmox VE Helper-Scripts. Он не является частью каталога community-scripts и
не загружает их библиотеки. Запускается от root в Shell узла Proxmox, создаёт
новый непривилегированный LXC с Ubuntu 24.04 (amd64) и устанавливает Python 3.12.
Нужен актуальный Proxmox VE, поддерживающий этот шаблон, и доступ в интернет
с узла и контейнера: каталог шаблонов, GitHub, репозитории Ubuntu и PyPI.
После публикации скриптов в ветке main этого репозитория запуск выглядит так:
curl -fsSL https://raw.githubusercontent.com/scatari69/urumi/main/scripts/proxmox-install.sh -o /root/urumi-install.sh
bash /root/urumi-install.shМожно также перенести scripts/proxmox-install.sh на узел вручную. Внутренний
proxmox-guest.sh и код приложения скачиваются из выбранной ветки, тега или commit
репозитория scatari69/urumi; эта версия должна содержать оба скрипта. Имена веток
с / не поддерживаются, вместо них можно указать commit SHA.
Установщик спросит:
- свободный ID, имя контейнера, CPU, RAM и диск (по умолчанию 1 ядро, 1024 МБ, 4 ГБ);
- активные хранилища для шаблона и диска, сетевой мост, DHCP или IPv4/CIDR со шлюзом, VLAN (0 — без тега), DNS IPv4 (пусто — наследовать от узла);
- порт админки (8080), версию кода, токен Telegram, ключ OpenRouter, пароль админки
и
ADMIN_USER_IDSв формате[123456789]; - подтверждение создания контейнера и запуска бота.
Секреты вводятся скрыто. Последовательность ${ в секретах не поддерживается
из-за интерполяции .env. До запуска остановите другой экземпляр бота с тем же
токеном. Существующие контейнеры не перезаписываются и автоматически не удаляются
даже при ошибке. При неудаче смотрите вывод команды и журнал в сохранённом LXC;
повторный запуск основного установщика предназначен для нового контейнера.
В конце выводится http://<IP-контейнера>:8080 (или выбранный порт). Если в Proxmox
заданы правила firewall, разрешите доступ к этому порту из своей сети и исходящий
доступ контейнера. Войдите с заданным паролем, отключите privacy mode у BotFather,
перезаведите бота в группу и активируйте её на /chats.
Внутри LXC:
| Путь / команда | Назначение |
|---|---|
/opt/urumi |
Код и .venv; работает Python 3.12 без Docker |
/etc/urumi/urumi.env |
Конфигурация, доступна root и группе urumi; .env в коде — ссылка |
/var/lib/urumi/urumi.db |
SQLite-база, отдельная от кода |
systemctl status urumi |
Статус сервиса |
journalctl -u urumi -f |
Логи |
systemctl restart urumi |
Перезапуск после изменения конфигурации |
Сервис стартует вместе с контейнером, работает от пользователя urumi и
перезапускается при сбое. Проверка установки ждёт сервис и HTTP /health;
отправку и получение сообщений нужно проверить отдельно в Telegram.
Для systemd в Ubuntu 24.04 контейнер создаётся с nesting=1.
Если установка остановилась с Temporary failure resolving, сначала проверьте
сеть из контейнера (здесь и далее 104 замените на его ID):
pct exec 104 -- ip -4 -brief address
pct exec 104 -- ip -4 route
pct exec 104 -- cat /etc/resolv.conf
pct exec 104 -- getent ahostsv4 archive.ubuntu.comОтсутствие IPv4 или маршрута по умолчанию требует проверки DHCP, моста, VLAN
и шлюза. Если они есть, проверьте доступность DNS и правила firewall. DNS задаётся
в Proxmox на вкладке DNS контейнера или через pct set 104 --nameserver <IP-DNS>:
укажите сервер, доступный из сети контейнера. Не копируйте loopback-адрес DNS
узла (127.0.0.1 или 127.0.0.53) в контейнер.
Если eth0 имеет состояние DOWN (unmanaged), а журнал systemd-networkd
содержит Failed to parse configuration file: Permission denied, проверьте права:
pct exec 104 -- namei -l /etc/systemd/network/eth0.network.
Старый установщик передавал Proxmox umask 077: в таком контейнере даже /etc
мог получить права 700, закрывающие доступ для systemd-networkd. Восстановите
права на сетевой путь и перезапустите службу (без рекурсивного chmod):
pct exec 104 -- chmod 755 /etc /etc/systemd /etc/systemd/network
pct exec 104 -- chmod 644 /etc/systemd/network/eth0.network
pct exec 104 -- systemctl restart systemd-networkdВ исправленном установщике команды Proxmox выполняются с umask 022, даже если
у вызывающей оболочки задан 077. Маска 077 действует только в отдельном
подпроцессе записи ключей: временный каталог имеет права 700, файл — 600.
Установщик ждёт готовности IPv4, маршрута и DNS до установки пакетов, а при ошибке
обновления индексов APT останавливается. Если сбой произошёл на этом этапе,
конфигурация ещё лежит в /root/urumi.env, и после исправления сети можно продолжить
установку в том же контейнере, без повторного ввода ключей. Обновите внутренний
скрипт с узла Proxmox и запустите его:
curl -fsSL https://raw.githubusercontent.com/scatari69/urumi/main/scripts/proxmox-guest.sh -o /root/urumi-guest.sh
pct push 104 /root/urumi-guest.sh /root/urumi-install.sh --perms 0700
pct exec 104 -- bash /root/urumi-install.sh mainДля контейнера, созданного старой версией установщика без nesting, сначала включите
его: pct set 104 --features nesting=1, затем pct reboot 104 и дождитесь запуска.
Если в контейнере настроены другие features, сохраните их при изменении.
Продолжение описанным способом поддерживается только до создания каталогов
приложения; существующую установку внутренний скрипт не перезаписывает.
Для обновления сначала сделайте резервную копию LXC через Proxmox. Затем внутри
контейнера выполните от root (замените main на нужный тег или commit при необходимости):
systemctl stop urumi
cd /opt/urumi
git fetch --depth 1 origin main
git checkout --detach FETCH_HEAD
.venv/bin/pip install --no-cache-dir -r requirements.txt
systemctl start urumi
systemctl status urumiВыполняйте команды последовательно; при ошибке не переходите к следующей. Конфигурация и база остаются на месте. Этот способ обновляет код и зависимости; изменения самого systemd-unit из будущих версий установщика применяются отдельно.
Без этого бот не увидит сообщения в группе и не будет работать. По умолчанию
Telegram отдаёт боту только команды (/...) и ответы на его собственные сообщения —
остальные сообщения группы до него просто не доходят, а значит не будет ни истории,
ни контекста, ни пересказов, ни заметок. Privacy mode — свойство самого бота
(токена), не конкретной группы, так что настраивается один раз, а не в каждой
новой группе — но требование "удалить и добавить заново" (шаг 4) применяется к
каждой группе, куда бот заходит после этого момента.
- Открыть @BotFather
/mybots→ выбрать бота → Bot Settings → Group Privacy- Нажать Turn off — должно остаться «Privacy mode is disabled»
- Удалить бота из группы и добавить заново — настройка применяется только при следующем входе в группу, на уже добавленного бота она не подействует
Проверить: написать в группе обычное сообщение (без команд) и открыть в админке
/c/<chat_id>/messages — оно должно там появиться. Если пусто, privacy mode всё
ещё включён либо бот не был перезаведён в группу.
- @BotFather →
/newbot→ имя и username - Скопировать токен вида
123456789:AAE...вBOT_TOKEN - Добавить бота в группу (после выключения privacy mode, см. выше) — группа
появится на
/chatsв админке, останется её там активировать (см. «Ещё одна группа» ниже)
Для команды /summary в группе бот должен уметь читать сообщения; для ответов —
просто быть участником. Права администратора не требуются.
- Зарегистрироваться на openrouter.ai
- openrouter.ai/keys → Create Key
- Скопировать ключ
sk-or-v1-...вOPENROUTER_API_KEY
Ключ нужен даже для бесплатных моделей — по нему считаются лимиты. Бесплатный
тариф ограничен по запросам в сутки; при исчерпании OpenRouter отдаёт 402/403, и
бот переключается на fallback_models (см. ниже).
Список групп, где боту разрешено что-либо делать, живёт в базе (таблица chats),
а не в .env — управляется на странице /chats в админке, а не через BotFather
или переменные окружения.
- Добавить бота в новую группу (не забыв про privacy mode — см. выше, требование «удалить и добавить заново» действует для каждой новой группы).
- Бот сам замечает момент, когда его добавили (это не зависит от privacy mode —
такие системные события Telegram присылает всегда), и заносит группу в
/chatsсо статусом «ожидает». Никаких действий на этом шаге не выполняется — ни истории, ни ответов. - Открыть
/chatsв админке и нажать Активировать — только с этого момента бот начинает логировать сообщения и отвечать в этой группе.
У новой группы — чистые настройки по умолчанию (system_prompt, модели, настроение
и т.д.), независимые от остальных; поправить их можно сразу на /c/<chat_id>/settings.
Каталог настроений (/c/<chat_id>/moods) — общий на все группы, а вот какое
настроение сейчас активно — своё для каждой.
Деактивировать группу можно там же, кнопкой «Деактивировать» — бот перестаёт что-либо делать в ней, но история и заметки не удаляются. «Удалить» стирает саму запись о группе и её настройки (историю, профили и пересказы — нет).
Такое бывает, если бот получил событие добавления не сразу (например был выключен
дольше суток — Telegram столько хранит недоставленные обновления). В этом случае
ID можно добавить вручную через форму внизу /chats.
ID группы — отрицательное число, у супергрупп с префиксом -100
(например -1001234567890). Узнать его:
Способ 1 — через Telegram API (бот при этом должен быть остановлен). Напишите что-нибудь в группе и откройте в браузере:
https://api.telegram.org/bot<BOT_TOKEN>/getUpdates
В ответе найдите "chat":{"id":-1001234567890,...} — это и есть нужный ID.
getUpdatesнельзя вызывать, пока работает polling: Telegram ответит409 Conflict. Сначалаdocker compose down, потом запрос. Пустой результат означает, что новых сообщений нет (напишите в группу ещё раз) либо включён privacy mode.
Способ 2 — сторонние боты вроде @getidsbot или @RawDataBot: добавить в
группу, он покажет ID, после чего его можно удалить. Способ не требует
останавливать своего бота.
Если группа была обновлена до супергруппы, ID меняется — на новый ID это будет уже другая запись в
/chats, старую можно удалить.
ADMIN_USER_IDS — это ID пользователей (положительные числа), список в формате
JSON: ADMIN_USER_IDS=[123456789, 987654321]. Свой ID можно узнать у @userinfobot.
Это единственная настройка, общая на все группы: администраторы бота одни и те же
везде (влияет на /profile и на mood_admin_only).
В истории текстов и фото репосты помечаются с указанием исходного автора или канала. Модель получает правило не приписывать переславшему чужие слова, опыт или взгляды — в ответах, пересказах и заметках. При ответе участника на репост боту также передаётся текст этого репоста, даже если он выпал из истории. Сообщения, скопированные без признака пересылки Telegram, распознать нельзя.
На странице /c/<chat_id>/settings под system_prompt включите de-llmify
и нажмите «Сохранить». Стиль предлагает модели отвечать одной короткой репликой,
обычно в 1–2 предложения, без шаблонных вступлений, пересказа вопроса и ненужных
предложений помощи. Явная просьба о подробностях допускает более длинный ответ.
Поле «Инструкции стиля» можно менять под свою группу; пустое поле возвращает стандартный текст. Это подключаемый блок системного промпта: базовый промпт → настроение → инструкции de-llmify. Он действует только на ответы в чате, не затрагивает пересказы и профили. Предпросмотр настроения учитывает сохранённый стиль. Настройки независимы для каждой группы, по умолчанию стиль выключен. Перезапуск после сохранения не нужен.
Это инструкции модели, а не жёсткое обрезание текста: точная длина и естественность зависят от выбранной модели и пользовательского текста инструкций.
Каталог бесплатных моделей OpenRouter постоянно меняется: модели появляются, дорожают и исчезают. Модель, прописанная в конфиге полгода назад, сегодня с большой вероятностью отдаёт 404.
Поэтому:
- Список моделей подтягивается из API, а не зашит в код.
/c/<chat_id>/settingsпоказывает актуальный каталог (GET /api/v1/models), сгруппированный по вендорам, с фильтром «только бесплатные», поиском, ценой за 1M токенов и кнопкой «Тест». Кэш — час, общий на все группы (в памяти и в глобальной таблицеsettings, отдельно от per-groupchat_settings), кнопка «Обновить список» сбрасывает его. - Фильтр «бесплатные» сравнивает цену как число (
pricing.prompt == 0), а не ищет суффикс:free: часть бесплатных моделей его не носит. - Заполните
fallback_modelsна/c/<chat_id>/settings— две-три запасные модели через запятую, для каждой группы отдельно. При 402/403 (лимит исчерпан) или 404 (модель убрали) клиент проходит список по порядку и логирует, какая в итоге ответила.
Блок «Состояние LLM» на
/c/<chat_id>/settingsи на дашборде (запросы, ошибки, последняя сработавшая модель) — общий на весь процесс, а не для одной группы: httpx-клиент и лимит параллельных запросов к OpenRouter (MAX_CONCURRENCY) один на все группы сразу, так как это один и тот же ключ и один и тот же дневной лимит OpenRouter.
Разумный набор: одна основная бесплатная + одна бесплатная другого вендора + одна дешёвая платная как последний рубеж.
Значение
MODELв.env— это только дефолт для трёх полейchat_model,summary_model,profile_modelв каждой новой группе, если они не заданы в админке. Перед первым запуском стоит проверить на/c/<chat_id>/settings, что такая модель ещё существует: дефолт из.env.example(google/gemma-3-27b-it:free) в каталоге OpenRouter уже отсутствует.
.env — только инфраструктура (см. .env.example):
| Переменная | Описание |
|---|---|
BOT_TOKEN |
токен от BotFather |
OPENROUTER_API_KEY |
ключ OpenRouter |
MODEL |
дефолтная модель для новой группы, если в её настройках не выбрана другая |
GROUP_CHAT_ID |
опционально: ID группы для самого первого запуска (см. «Ещё одна группа») |
ADMIN_USER_IDS |
JSON-список ID админов, напр. [123456789] — общий на все группы |
ADMIN_PASSWORD |
пароль в админку (он же ключ подписи сессии) |
DB_PATH |
путь к SQLite, по умолчанию data/urumi.db |
HISTORY_TTL_HOURS |
сколько часов хранить сообщения |
LOG_LEVEL |
INFO / DEBUG / ... |
ADMIN_PORT |
порт админки |
Всё остальное — промпты, модели, температура, настроения — правится в админке,
отдельно для каждой группы, и живёт в таблице chat_settings (кэш 30 секунд на
группу, сбрасывается при сохранении). Список самих групп — в таблице chats.
| Страница | Что там |
|---|---|
/chats |
список групп: статус (ожидает/активна), активация/деактивация/удаление, ручное добавление по ID |
/c/<chat_id>/ |
дашборд группы: сообщения за 24 ч, топ-10 активных, состояние LLM, последние смены настроения |
/c/<chat_id>/settings |
промпты, три модели (чат / пересказы / заметки), фолбэки, температура, вкл-выкл — для этой группы |
/c/<chat_id>/messages |
лента этой группы с фильтром по участнику, поиском и постраничностью |
/c/<chat_id>/moods |
настроения: каталог общий на все группы, «включить» и текущее — для этой группы |
/c/<chat_id>/profiles |
заметки об участниках этой группы: правка, пересборка, удаление |
/c/<chat_id>/summaries |
история пересказов этой группы и кнопка «сгенерировать сейчас» |
Переключатель группы — в шапке рядом с навигацией.
| Команда | Кто может | Что делает |
|---|---|---|
/summary [часов] |
все | пересказ за N часов (по умолчанию 24, максимум HISTORY_TTL_HOURS) |
/mood |
все | показать текущее настроение и список доступных |
/mood <name> |
все, либо только админы при mood_admin_only |
переключить (не чаще раза в 30 с) |
/mood reset |
там же | вернуть настроение по умолчанию |
/profile |
только ADMIN_USER_IDS |
показать заметку об авторе сообщения, на которое отвечаете |
/createprofile |
все — свой профиль; ADMIN_USER_IDS — также чужой |
без реплая или реплаем на своё сообщение — создать или обновить свой профиль и показать его; администратор может ответить на сообщение другого участника для создания его профиля; использует историю этой группы за HISTORY_TTL_HOURS, без порога в 20 сообщений; отказ через /forgetme сохраняется |
/forgetme |
все | стереть свою заметку и отказаться от профилирования |
/forgetcontext confirm |
только ADMIN_USER_IDS |
необратимо стереть историю сообщений этого чата (контекст для ответов/пересказов/заметок); без confirm — только предупреждение, ничего не удаляет |
Бот отвечает в чате, когда его упомянули через @username, ответили на его
сообщение, либо случайно — с вероятностью random_reply_chance.
Список команд в меню Telegram (кнопка "/") бот перезаписывает сам при каждом
запуске — под scope chat для каждой активной на данный момент группы,
отдельно для языков по умолчанию, ru и en. Это перекрывает то, что в этой же
группе мог нарегать предыдущий бот/фреймворк на этом токене: правки через
BotFather их не брали, потому что команды хранятся отдельно по каждой паре
(scope, language_code), а BotFather штатно правит только scope по умолчанию без
языка. Если старые команды всё же остались в других scope (например, default
или all_group_chats) — на то, что видно в самой группе, они не влияют: scope
chat имеет более высокий приоритет, — но подчистить их полностью можно через
deleteMyCommands напрямую по Bot API.
Группа, активированная через
/chatsбез перезапуска бота, получит меню команд только после следующего перезапуска — это чисто косметическая задержка, сами команды работают сразу.
Триггеры те же (упоминание в подписи, ответ на сообщение бота, случайный шанс).
Если chat_model поддерживает изображения на входе — на /c/<chat_id>/settings в
информации о модели это видно как image в строке вида text+image->text — бот скачивает фото
(до 5 МБ, при нескольких размерах выбирается наибольший подходящий) и отправляет его
модели вместе с подписью. Если модель не поддерживает изображения, или скачать не
удалось, бот всё равно ответит — но только по тексту подписи, саму картинку не увидев.
docker compose ps # статус, в т.ч. health
docker compose logs -f
docker compose restart
docker compose down # данные останутся в volume urumi-data- Данные лежат в именованном volume
urumi-data, смонтированном в/app/data. Контейнер работает под пользователемurumi(uid 10001). Если хочется bind mount, замените том на./data:/app/dataи выполнитеsudo chown -R 10001:10001 ./data, иначе непривилегированный процесс не сможет писать в каталог. - Healthcheck дёргает
/healthвнутри контейнера; состояние видно вdocker compose ps. docker stopшлёт SIGTERM: бот дослушивает текущий запрос, останавливает polling, гасит HTTP-сервер и закрывает httpx-клиент. На это отводитсяstop_grace_period: 20s.
docker compose exec bot python -c "import sqlite3,shutil; shutil.copy('data/urumi.db','data/backup.db')"
docker compose cp bot:/app/data/backup.db ./backup.db| Симптом | Причина |
|---|---|
| Бот молчит на обычные сообщения | группа не активирована на /chats, либо privacy mode включён, либо бота не перезавели в группу |
/c/<chat_id>/messages пустая |
то же самое |
Группы нет в списке /chats |
бот был офлайн больше суток и не получил событие добавления — добавьте вручную по ID (см. «Ещё одна группа») |
TelegramUnauthorizedError |
неверный BOT_TOKEN |
| LLM отвечает 404 | модель исчезла из каталога — выберите другую на /c/<chat_id>/settings |
| LLM отвечает 402/403 | исчерпан бесплатный лимит — заполните fallback_models для этой группы |
| Админка недоступна | проверьте ADMIN_PORT и проброс порта в compose |
| После супергруппы бот молчит | у супергрупп ID меняется — старая запись в /chats больше не действует; если новая не появилась сама, добавьте её ID вручную и активируйте |