Устройство шлюза
Шлюз сообщений — это долгоживущий процесс, который через единую архитектуру связывает Hermes с 20+ внешними мессенджерами.
Ключевые файлы
| Файл | Назначение |
|---|---|
gateway/run.py | GatewayRunner — основной цикл, слеш-команды, диспетчеризация сообщений (большой файл; актуальный объём — смотри git) |
gateway/session.py | SessionStore — хранение истории разговоров и формирование ключей сессий |
gateway/delivery.py | Доставка исходящих сообщений на целевые платформы и в каналы |
gateway/pairing.py | Процесс DM-сопряжения для авторизации пользователей |
gateway/channel_directory.py | Сопоставляет ID чатов с человекочитаемыми именами для доставки через cron |
gateway/hooks.py | Обнаружение, загрузка хуков и диспетчеризация событий жизненного цикла |
gateway/mirror.py | Зеркалирование сообщений между сессиями для send_message |
gateway/status.py | Управление блокировками токенов для инстансов шлюза в рамках профиля |
gateway/builtin_hooks/ | Точка расширения для хуков, регистрируемых всегда (в поставке таких нет) |
gateway/platforms/ | Адаптеры платформ (по одному на каждый мессенджер) |
Обзор архитектуры
┌─────────────────────────────────────────────────┐
│ GatewayRunner │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Telegram │ │ Discord │ │ Slack │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ _handle_message() │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ Slash command AIAgent Queue/BG │
│ dispatch creation sessions │
│ │ │
│ ▼ │
│ SessionStore │
│ (SQLite persistence) │
└───────┴─────────────┴─────────────┴─────────────┘
Поток сообщений
Когда сообщение приходит с любой платформы:
- Адаптер платформы получает сырое событие и нормализует его в
MessageEvent - Базовый адаптер проверяет защиту активной сессии:
- Если агент уже запущен для этой сессии → сообщение ставится в очередь, устанавливается событие прерывания
- Если пришло
/approve,/deny,/stop→ защита обходится (команда выполняется сразу)
- GatewayRunner._handle_message() получает событие:
- Определяет ключ сессии через
_session_key_for_source()(формат:agent:main:{platform}:{chat_type}:{chat_id}) - Проверяет авторизацию (см. раздел «Авторизация»)
- Смотрит, не слеш-команда ли это → перенаправляет к нужному обработчику
- Проверяет, работает ли агент → перехватывает команды
/stop,/status - Иначе → создаёт инстанс
AIAgentи запускает диалог
- Определяет ключ сессии через
- Ответ возвращается через адаптер платформы
Формат ключа сессии
Ключ сессии кодирует полный контекст маршрутизации:
agent:main:{platform}:{chat_type}:{chat_id}
Например: agent:main:telegram:private:123456789
Платформы с тредами (топики Telegram, треды Discord и Slack) могут включать ID треда в часть chat_id. Никогда не собирайте ключи сессий вручную — используйте только build_session_key() из gateway/session.py.
Двухуровневая защита входящих сообщений
Пока агент работает, входящие сообщения проходят через два последовательных уровня проверки:
-
Уровень 1 — базовый адаптер (
gateway/platforms/base.py): проверяет_active_sessions. Если сессия активна, сообщение помещается в_pending_messagesи устанавливается событие прерывания. Так сообщения перехватываются до того, как они достигнут gateway runner. -
Уровень 2 — gateway runner (
gateway/run.py): проверяет_running_agents. Перехватывает конкретные команды (/stop,/new,/queue,/status,/approve,/deny) и направляет их соответствующим образом. Всё остальное вызываетrunning_agent.interrupt().
Команды, которым нужно достичь runner, пока агент заблокирован (например, /approve), выполняются напрямую через await self._message_handler(event) — они обходят систему фоновых задач, чтобы не нарваться на состояние гонки.
Авторизация
Шлюз проверяет авторизацию в несколько слоёв, по порядку:
- Флаг разрешения всех пользователей для конкретной платформы (например,
TELEGRAM_ALLOW_ALL_USERS) — если установлен, все пользователи этой платформы авторизованы - Список разрешённых пользователей платформы (например,
TELEGRAM_ALLOWED_USERS) — ID через запятую - DM-сопряжение — авторизованные пользователи могут подключать новых через код сопряжения
- Глобальный флаг разрешения всех (
GATEWAY_ALLOW_ALL_USERS) — авторизует всех пользователей на всех платформах - По умолчанию: запрет — неавторизованные пользователи отклоняются
Процесс DM-сопряжения
Admin: /pair
Gateway: "Pairing code: ABC123. Share with the user."
New user: ABC123
Gateway: "Paired! You're now authorized."
Состояние сопряжения сохраняется в gateway/pairing.py и переживает перезапуски.
Диспетчеризация слеш-команд
Все слеш-команды в шлюзе проходят через единый пайплайн разрешения:
resolve_command()изhermes_cli/commands.pyприводит ввод к каноническому имени (обрабатывает псевдонимы, префиксное сопоставление)- Каноническое имя сверяется с
GATEWAY_KNOWN_COMMANDS - Обработчик в
_handle_message()диспетчеризирует по каноническому имени - Часть команд проверяется по конфигу (
gateway_config_gateнаCommandDef)
Защита от выполнения команд во время работы агента
Команды, которые не должны выполняться, пока агент обрабатывает запрос, отклоняются на ранней стадии:
if _quick_key in self._running_agents:
if canonical == "model":
return "⏳ Agent is running — wait for it to finish or /stop first."
Для обходных команд (/stop, /new, /approve, /deny, /queue, /status) предусмотрена особая обработка.
Источники конфигурации
Шлюз собирает конфигурацию из нескольких источников:
| Источник | Что предоставляет |
|---|---|
~/.hermes/.env | API-ключи, токены ботов, учётные данные платформ |
~/.hermes/config.yaml | Настройки модели, конфигурация инструментов, параметры отображения |
| Переменные окружения | Переопределяют любые из приведённых выше |
В отличие от CLI (который использует load_cli_config() с жёстко заданными дефолтами), шлюз читает config.yaml напрямую через YAML-загрузчик. Поэтому конфиг-ключи, которые есть в дефолтном словаре CLI, но отсутствуют в файле пользователя, в CLI и в шлюзе могут вести себя по-разному.
Адаптеры платформ
У каждого мессенджера есть свой адаптер в gateway/platforms/:
gateway/platforms/
├── base.py # BaseAdapter — общая логика для всех платформ
├── telegram.py # Telegram Bot API (long polling или webhook)
├── discord.py # Discord-бот через discord.py
├── slack.py # Slack Socket Mode
├── whatsapp.py # WhatsApp Business Cloud API
├── signal.py # Signal через signal-cli REST API
├── matrix.py # Matrix через mautrix (опциональное E2EE)
├── mattermost.py # Mattermost WebSocket API
├── email.py # Email через IMAP/SMTP
├── sms.py # SMS через Twilio
├── dingtalk.py # DingTalk WebSocket
├── feishu.py # Feishu/Lark WebSocket или webhook
├── wecom.py # WeCom (WeChat Work) callback
├── weixin.py # Weixin (личный WeChat) через iLink Bot API
├── bluebubbles.py # Apple iMessage через BlueBubbles macOS server
├── qqbot/ # QQ Bot (Tencent QQ) через Official API v2 (субпакет: adapter.py, crypto.py, keyboards.py, …)
├── yuanbao.py # Yuanbao (Tencent) адаптер для DM/групп
├── feishu_comment.py # Обработчик ответов на комментарии в документах/дисках Feishu
├── msgraph_webhook.py # Вебхук изменений Microsoft Graph (Teams, Outlook и др.)
├── webhook.py # Адаптер входящих/исходящих вебхуков
├── api_server.py # Адаптер REST API сервера
└── homeassistant.py # Интеграция с Home Assistant conversation
Адаптеры реализуют общий интерфейс:
connect()/disconnect()— управление жизненным цикломsend_message()— доставка исходящих сообщенийon_message()— нормализация входящих сообщений →MessageEvent
Блокировки токенов
Адаптеры, подключающиеся с уникальными учётными данными, вызывают acquire_scoped_lock() в connect() и release_scoped_lock() в disconnect(). Так два профиля не смогут одновременно использовать один и тот же токен бота.
Путь доставки
Исходящая доставка (gateway/delivery.py) обрабатывает такие сценарии:
- Прямой ответ — отправка ответа в чат, из которого пришло сообщение
- Доставка в домашний канал — маршрутизация вывода cron-задач и фоновых результатов в настроенный домашний канал
- Доставка в явно указанный адрес — инструмент
send_messageс указаниемtelegram:-1001234567890, или CLI-обёрткаhermes sendповерх того же инструмента для шелл-скриптов - Кросс-платформенная доставка — отправка в мессенджер, отличный от того, где пришло исходное сообщение
Cron-доставки не зеркалируются в историю сессий шлюза — они живут только внутри собственной cron-сессии. Это намеренное архитектурное решение: так не нарушается порядок чередования сообщений.
Хуки
Хуки шлюза — это Python-модули, реагирующие на события жизненного цикла:
События хуков шлюза
| Событие | Когда срабатывает |
|---|---|
gateway:startup | Запуск процесса шлюза |
session:start | Начало новой сессии разговора |
session:end | Завершение сессии или истечение таймаута |
session:reset | Сброс сессии пользователем командой /new |
agent:start | Агент начинает обработку сообщения |
agent:step | Агент завершает одну итерацию вызова инструментов |
agent:end | Агент завершает работу и возвращает ответ |
command:* | Выполнена любая слеш-команда |
Хуки обнаруживаются в gateway/builtin_hooks/ (точка расширения — в поставке пуста; _register_builtin_hooks() ничего не делает, это заглушка) и ~/.hermes/hooks/ (устанавливаемые пользователем). Каждый хук — это каталог с манифестом HOOK.yaml и файлом handler.py.
Интеграция провайдера памяти
Когда плагин провайдера памяти (например, Honcho) активирован:
- Шлюз создаёт инстанс
AIAgentдля каждого сообщения с переданным ID сессии MemoryManagerинициализирует провайдер с контекстом сессии- Инструменты провайдера (например,
honcho_profile,viking_search) маршрутизируются через:
AIAgent._invoke_tool()
→ self._memory_manager.handle_tool_call(name, args)
→ provider.handle_tool_call(name, args)
- По завершении или сбросе сессии срабатывает
on_session_end()для очистки и финальной записи данных
Жизненный цикл сброса памяти
При сбросе, возобновлении или истечении срока сессии:
- Встроенные воспоминания записываются на диск
- Срабатывает хук
on_session_end()провайдера памяти - Временный инстанс
AIAgentвыполняет отдельный ход разговора только с памятью - После этого контекст отбрасывается или архивируется
Фоновое обслуживание
Параллельно с обработкой сообщений шлюз выполняет периодические задачи:
- Тикинг cron — проверяет расписания задач и запускает наступившие
- Истечение сессий — очищает заброшенные сессии после таймаута
- Сброс памяти — заранее сбрасывает память, не дожидаясь истечения сессии
- Обновление кэша — обновляет списки моделей и статус провайдеров
Управление процессом
Шлюз работает как долгоживущий процесс, и управляют им так:
hermes gateway start/hermes gateway stop— ручное управлениеsystemctl(Linux) илиlaunchctl(macOS) — управление сервисом- PID-файл в
~/.hermes/gateway.pid— отслеживание процесса в рамках профиля
Профильная vs глобальная область: start_gateway() использует PID-файлы, привязанные к профилю. hermes gateway stop останавливает только шлюз текущего профиля. hermes gateway stop --all сканирует через ps aux глобально и завершает все процессы шлюза (применяется при обновлениях).