Telegram bridge for eva kernel — a module on eva-sdk
  • Rust 87.9%
  • Python 9%
  • Nix 3.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Aleksandr 79f4c40aff Гостевой режим говорит из каталога
Гостевой промпт и тосты «👀» разъехались с волной: они приехали в мост,
пока личность уезжала в конфиг. Возвращаем их в общий строй — нейтральные
встроенные строки, голос оверлеем.

Co-Authored-By: Eva
2026-08-23 21:49:47 +03:00
crates/bridge Гостевой режим говорит из каталога 2026-08-23 21:49:47 +03:00
docs Гостевой режим говорит из каталога 2026-08-23 21:49:47 +03:00
nix Гостевой режим говорит из каталога 2026-08-23 21:49:47 +03:00
triage-benchmark Личность моста переезжает в конфиг ядра 2026-08-23 21:45:15 +03:00
.gitignore Скелет модуля: переехали слои, не державшиеся за ядро 2026-07-28 22:34:18 +03:00
AGENTS.md Инструменты: вход — тип, а не сырой JSON 2026-07-29 12:47:28 +03:00
Cargo.lock eva-sdk переезжает на ядро без каталога 2026-08-23 21:45:15 +03:00
Cargo.toml Скелет модуля: переехали слои, не державшиеся за ядро 2026-07-28 22:34:18 +03:00
CHANGELOG.md Голос приезжает вместе с nix-модулем 2026-08-23 21:45:15 +03:00
flake.lock Флейк: хеш git-зависимости SDK 2026-07-28 23:29:41 +03:00
flake.nix eva-sdk переезжает на ядро без каталога 2026-08-23 21:45:15 +03:00
README.md Голос приезжает вместе с nix-модулем 2026-08-23 21:45:15 +03:00

eva-telegram-bridge

Телеграмная поверхность ассистента: отдельный процесс, который ловит сообщения Bot API, ведёт туры в ядре и доставляет ответы обратно.

Это модуль, а не MCP-сервер: его инструменты попадают в реестр ядра наравне со встроенными — telegram_react, telegram_skip и прочие десять, в группе telegram. Ядро зовёт их, передавая контекст тура, и по нему мост понимает, чей ответ гасить и на какое сообщение отвечать.

Граница простая: ядро отвечает за личность — персону, память, историю, туры, политику доступа к опасным инструментам. Мост отвечает за телеграм — токен, кто кому может писать, дебаунсы, потолки ответа на чат и на человека, спонтанность, кэш вложений, привязку «tg-чат → чат ядра». Своё состояние мост держит в своей базе и в базу ядра не лезет.

Голос — тоже забота моста: встроенные строки нейтральны, а поверх них ложится оверлей из своего конфига (persona.overlay_file — плоский YAML «tg.ключ → строка», ключи чужих клиентов в общем файле мост пропускает). У ядра он спрашивает только идентичность — имя и обращение (GET /v1/persona); они подставляются в {name} и {honorific}, а ядро постарше их не отдаёт, и подстановки остаются нейтральными. Оверлея нет — мост говорит нейтрально; оверлей битый — старт падает: онемевший голос выглядит как подменённая личность, а не как опечатка в пути. Образец — docs/persona-overlay.example.yaml, ключи и встроенные значения — crates/bridge/src/strings.rs.

Как выглядит ответ в телеграме — тоже забота моста: правила поверхности он объявляет ядру на handshake (Module::surface_rules), и ядро вклеивает их в системный промпт наших туров как есть. Своего мнения о телеграме у ядра больше нет.

Что умеет

  • long-poll Bot API, воркер на чат с дебаунсом (настройка человека жёстче настройки чата), триаж дешёвой моделью и один тур на всю пачку. Судья видит только сами сообщения: ни названия чата, ни его id в промпте нет — шумная вывеска группы уводит дешёвую модель в «влезай». Разговор вокруг пачки показывается ему лишь по тумблеру 🧾 Контекст триажу, выключенному по умолчанию (глубина хвоста — telegram.triage_context);
  • вердикт триажа — подсказка, а не приказ: судья решает, кого вовлечь, но чем ответить, решает сам ассистент. Тур достаётся последнему вовлечённому каким угодно вердиктом; реакцию он ставит тулом react, молчание объявляет skip, и переиграть судью вправе. «Хватит эмодзи» доезжает до тура одной лишь просьбой о краткости — мост эмодзи больше не выбирает, иначе на дурную новость прилетает случайное сердечко. Итог каждой пачки — вовлечено против отвеченного — уходит в /v1/stats ядра, где копится доля промахов судьи;
  • доставка стримингом: ответ растёт правкой одного сообщения, под ним кнопки « Отмена» и « В фон» — они едут с каждым кадром и живут до конца тура. «Отмена» гасит тур и сразу снимает кнопки, не дожидаясь конца; «В фон» отпускает воркер чата на следующую пачку, гасит кадры и снимает кнопки, а ответ доезжает сам. Обе — для того, кому отвечают, и для владельца: чужой тур в группе прохожему не погасить. Ответ на нажатие приходит всегда, в том числе когда гасить уже нечего или ядро недоступно;
  • реакции вместо слов, явное молчание, вложения в обе стороны, фото собеседника прямо в модель (по цепочке user > chat > конфиг multimodal; без неё — только разрешённым). Реплай на картинку везёт и её саму, и её подпись — разговор о ней не идёт вслепую;
  • выделенный в реплае кусок родителя и источник пересылки приезжают прямо в тексте сообщения: цитата — строками под > с пустой строкой под ними, пересланное — строкой [переслано от X] над телом. Своим полем каждое стало бы ещё одним форматом, который история, триаж и четыре клиента рендерят по-своему (и половина — никак), а обычный текст понимают все и даром. Без выделенного куска реплай по-прежнему означает «отвечаю на сообщение целиком», а без пересылки подписи просто нет;
  • нарисованное ассистентом уезжает в чат вместе с ответом: байты мост берёт у ядра по id, а не по ссылке — качалка по URL прикрыта щитом приватных адресов, и ядро на localhost ей недоступно;
  • кнопочные вопросы, ждущие ответа внутри тура;
  • меню /settings по экранам: обзор — список входов, на каждом видно текущее состояние («🧰 Инструменты · 12/40», «🧠 Модель · kimi-k3», «🎛 Набор · весь реестр»), а тумблеры инструментов (внутри ещё страницы групп), выбор модели и выбор набора живут каждый на своём экране с «⬅ Назад» и «✖ Закрыть». Живые реплики ассистента к действиям (menu_quips); открыть и жать может владелец, админ группы или собеседник в личке. Тулы из master_tools в чатах не показываются вовсе — только в консоли, которую владелец забирает в личку кнопкой с обзора. В обзоре живёт кнопка «🖼 Зрение · ✔️/✖️» — картинки чата прямо в модель; клик переключает, и жмёт её только владелец. Недостижимые из чата тулы (client_only, trusted_only вне лички) меню не обещает. Юзер-меню с пресетами (дебаунс, потолок, краткость, фото в модель) и тул telegram_settings_menu — ассистент открывает меню сам;
  • inline-режим, самооформление бота и живое описание профиля;
  • guest mode (Bot API 10.0): позвали @хэндлом в чат, где Евы нет, — она отвечает одним сообщением, не видя ни истории, ни участников (тумблер «Guest Mode» у BotFather). Через telegram.guest_hide_after («5min», «30sec»; ноль — никогда, по умолчанию 5 минут) ответ сворачивается в «…» с кнопкой «👀» — та разворачивает его и заводит таймер заново;
  • комментарии под постами канала помечаются как контекст, а не как чужая реплика.

Каркас

Модуль стоит на фреймворке eva — том же, что и ядро: конфиг-секции с наложением файлов, логи и супервизор компонентов даются им, а не пишутся заново.

Компонентов два, у каждого своя секция конфига и своё завершение:

компонент секция что делает
поверхность telegram поллинг, туры, доставка
инструменты tools UDS-сокет, к которому подключается ядро

Слои

файл что это
bridge.rs поллинг, воркеры чатов, доставка, меню-действия
tools.rs инструменты модуля и их связь с идущим туром
triage.rs кого из пачки вовлекать
menu.rs клавиатуры /settings
api.rs HTTP-клиент Bot API, отправка и правка rich-сообщений
render.rs сегменты ответа, строки инструментов, кадры стрима, чанковка
rich.rs медиа-блоки ![](url): находим, качаем, подменяем на вложения
photos.rs скачивание и валидация вложений, картинки ядра по id, кэш file_id
questions.rs кнопочные вопросы
strings.rs строки поверхности: нейтральные встроенные и оверлей персоны
db.rs своё состояние: чаты, допуски, настройки, архив
net.rs SSRF-щит для походов по ссылкам из чатов

Запуск

eva-telegram-bridge --config config.yaml --config secret.yaml

Файлы накладываются друг на друга — секреты живут отдельно от остального и не уезжают в общедоступный nix-store. Модулей во флейке два, с одинаковыми опциями: nixosModules.default — системный юнит, homeManagerModules.default — пользовательский (systemd.user, состояние в $XDG_STATE_HOME). Оба дают пакет, юнит, конфиг, secretFile и секцию persona.

Голос модуль собирает сам, и по умолчанию он не нейтральный: в репе лежит nix/persona-overlay.json (39 строк), и он уезжает в конфиг, пока его не отключили. Дальше — по вкусу:

нужно как
поправить пару строк persona.overlay."tg.press.closed" = "…"; — ляжет поверх дефолтных
свой голос целиком persona.useDefaults = false; и свои строки в persona.overlay
нейтральный мост persona.useDefaults = false; с пустым overlay — секции в конфиге не будет вовсе
готовый файл persona.overlayFile = ./catalog.yaml; — побеждает и дефолты, и overlay

Голос не секрет: что бы ни вышло, файл копируется в общедоступный стор и прописывается в settings.persona.overlay_file сам.

Ядру модуль объявляют в его конфиге:

modules:
  servers:
    telegram:
      socket: /run/eva-mcp/telegram.sock

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

Сборка

nix develop -c cargo build