Устройство шлюза

Шлюз сообщений — это долгоживущий процесс, который через единую архитектуру связывает Hermes с 20+ внешними мессенджерами.

Ключевые файлы

ФайлНазначение
gateway/run.pyGatewayRunner — основной цикл, слеш-команды, диспетчеризация сообщений (большой файл; актуальный объём — смотри git)
gateway/session.pySessionStore — хранение истории разговоров и формирование ключей сессий
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)               │
└───────┴─────────────┴─────────────┴─────────────┘

Поток сообщений

Когда сообщение приходит с любой платформы:

  1. Адаптер платформы получает сырое событие и нормализует его в MessageEvent
  2. Базовый адаптер проверяет защиту активной сессии:
    • Если агент уже запущен для этой сессии → сообщение ставится в очередь, устанавливается событие прерывания
    • Если пришло /approve, /deny, /stop → защита обходится (команда выполняется сразу)
  3. GatewayRunner._handle_message() получает событие:
    • Определяет ключ сессии через _session_key_for_source() (формат: agent:main:{platform}:{chat_type}:{chat_id})
    • Проверяет авторизацию (см. раздел «Авторизация»)
    • Смотрит, не слеш-команда ли это → перенаправляет к нужному обработчику
    • Проверяет, работает ли агент → перехватывает команды /stop, /status
    • Иначе → создаёт инстанс AIAgent и запускает диалог
  4. Ответ возвращается через адаптер платформы

Формат ключа сессии

Ключ сессии кодирует полный контекст маршрутизации:

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. Уровень 1 — базовый адаптер (gateway/platforms/base.py): проверяет _active_sessions. Если сессия активна, сообщение помещается в _pending_messages и устанавливается событие прерывания. Так сообщения перехватываются до того, как они достигнут gateway runner.

  2. Уровень 2 — gateway runner (gateway/run.py): проверяет _running_agents. Перехватывает конкретные команды (/stop, /new, /queue, /status, /approve, /deny) и направляет их соответствующим образом. Всё остальное вызывает running_agent.interrupt().

Команды, которым нужно достичь runner, пока агент заблокирован (например, /approve), выполняются напрямую через await self._message_handler(event) — они обходят систему фоновых задач, чтобы не нарваться на состояние гонки.

Авторизация

Шлюз проверяет авторизацию в несколько слоёв, по порядку:

  1. Флаг разрешения всех пользователей для конкретной платформы (например, TELEGRAM_ALLOW_ALL_USERS) — если установлен, все пользователи этой платформы авторизованы
  2. Список разрешённых пользователей платформы (например, TELEGRAM_ALLOWED_USERS) — ID через запятую
  3. DM-сопряжение — авторизованные пользователи могут подключать новых через код сопряжения
  4. Глобальный флаг разрешения всех (GATEWAY_ALLOW_ALL_USERS) — авторизует всех пользователей на всех платформах
  5. По умолчанию: запрет — неавторизованные пользователи отклоняются

Процесс DM-сопряжения

Admin: /pair
Gateway: "Pairing code: ABC123. Share with the user."
New user: ABC123
Gateway: "Paired! You're now authorized."

Состояние сопряжения сохраняется в gateway/pairing.py и переживает перезапуски.

Диспетчеризация слеш-команд

Все слеш-команды в шлюзе проходят через единый пайплайн разрешения:

  1. resolve_command() из hermes_cli/commands.py приводит ввод к каноническому имени (обрабатывает псевдонимы, префиксное сопоставление)
  2. Каноническое имя сверяется с GATEWAY_KNOWN_COMMANDS
  3. Обработчик в _handle_message() диспетчеризирует по каноническому имени
  4. Часть команд проверяется по конфигу (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/.envAPI-ключи, токены ботов, учётные данные платформ
~/.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) активирован:

  1. Шлюз создаёт инстанс AIAgent для каждого сообщения с переданным ID сессии
  2. MemoryManager инициализирует провайдер с контекстом сессии
  3. Инструменты провайдера (например, honcho_profile, viking_search) маршрутизируются через:
AIAgent._invoke_tool()
  → self._memory_manager.handle_tool_call(name, args)
    → provider.handle_tool_call(name, args)
  1. По завершении или сбросе сессии срабатывает on_session_end() для очистки и финальной записи данных

Жизненный цикл сброса памяти

При сбросе, возобновлении или истечении срока сессии:

  1. Встроенные воспоминания записываются на диск
  2. Срабатывает хук on_session_end() провайдера памяти
  3. Временный инстанс AIAgent выполняет отдельный ход разговора только с памятью
  4. После этого контекст отбрасывается или архивируется

Фоновое обслуживание

Параллельно с обработкой сообщений шлюз выполняет периодические задачи:

  • Тикинг 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 глобально и завершает все процессы шлюза (применяется при обновлениях).

Связанные документы

ESC