Internals
На этой странице Шлюз обмена сообщениями (messaging gateway) — это долгоживущий процесс, который соединяет Hermes с 14+ внешними платформами обмена сообщениями через единую архитектуру.
Ключевые файлы¶
| Файл | Назначение |
|---|---|
gateway/run.py |
GatewayRunner — главный цикл, слэш-команды, диспетчеризация сообщений (~12 000 строк) |
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/ |
Адаптеры платформ (по одному на каждую платформу обмена сообщениями) |
| ## Обзор архитектуры | |
| [code] | |
| ┌─────────────────────────────────────────────────┐ | |
| │ GatewayRunner │ | |
| │ │ | |
| │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ | |
| │ │ Telegram │ │ Discord │ │ Slack │ │ | |
| │ │ Adapter │ │ Adapter │ │ Adapter │ │ | |
| │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ | |
| │ │ │ │ │ | |
| │ └─────────────┼─────────────┘ │ | |
| │ ▼ │ | |
| │ _handle_message() │ | |
| │ │ │ | |
| │ ┌───────────┼───────────┐ │ | |
| │ ▼ ▼ ▼ │ | |
| │ Slash command AIAgent Queue/BG │ | |
| │ dispatch creation sessions │ | |
| │ │ │ | |
| │ ▼ │ | |
| │ SessionStore │ | |
| │ (SQLite persistence) │ | |
| └───────┴─────────────┴─────────────┴─────────────┘ |
[/code]
Поток сообщений¶
Когда сообщение поступает с любой платформы:
1. Адаптер платформы получает сырое событие, нормализует его в MessageEvent
2. Базовый адаптер проверяет защиту активной сессии:
* Если агент уже выполняется для этой сессии → поставить сообщение в очередь, установить событие прерывания
* Если /approve, /deny, /stop → пропустить защиту (обрабатываются inline)
3. GatewayRunner._handle_message() получает событие:
* Разрешение ключа сессии через _session_key_for_source() (формат: agent:main:{platform}:{chat_type}:{chat_id})
* Проверка авторизации (см. Авторизацию ниже)
* Проверка, является ли сообщение слэш-командой → отправка в обработчик команд
* Проверка, выполняется ли уже агент → перехват команд типа /stop, /status
* В противном случае → создание экземпляра AIAgent и запуск беседы
4. Ответ отправляется обратно через адаптер платформы
Формат ключа сессии¶
Ключи сессии кодируют полный контекст маршрутизации: [code] agent:main:{platform}:{chat_type}:{chat_id}
[/code]
Например: 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 и устанавливает событие прерывания. Это перехватывает сообщения до того, как они достигнут шлюза.
2. Уровень 2 — Шлюз (gateway/run.py): Проверяет _running_agents. Перехватывает определённые команды (/stop, /new, /queue, /status, /approve, /deny) и направляет их соответствующим образом. Всё остальное вызывает running_agent.interrupt().
Команды, которые должны достичь обработчика шлюза, пока агент заблокирован (например, /approve), отправляются inline через await self._message_handler(event) — они обходят систему фоновых задач, чтобы избежать состояний гонки.
Авторизация¶
Шлюз использует многоуровневую проверку авторизации, выполняемую по порядку:
1. Флаг разрешения всех на платформе (например, TELEGRAM_ALLOW_ALL_USERS) — если установлен, все пользователи на этой платформе авторизованы
2. Белый список платформы (например, TELEGRAM_ALLOWED_USERS) — ID пользователей через запятую
3. Привязка в личных сообщениях (DM) — авторизованные пользователи могут привязывать новых пользователей через код привязки
4. Глобальное разрешение всех (GATEWAY_ALLOW_ALL_USERS) — если установлен, все пользователи на всех платформах авторизованы
5. По умолчанию: запрет — неавторизованные пользователи отклоняются
Процедура привязки в DM¶
[code] Админ: /pair Шлюз: "Код привязки: ABC123. Передайте пользователю." Новый пользователь: ABC123 Шлюз: "Привязка выполнена! Теперь вы авторизованы."
[/code]
Состояние привязки сохраняется в gateway/pairing.py и переживает перезапуски.
Диспетчеризация слэш-команд¶
Все слэш-команды в шлюзе проходят через один и тот же конвейер разрешения:
1. resolve_command() из hermes_cli/commands.py сопоставляет ввод с каноническим именем (обрабатывает псевдонимы, сопоставление по префиксу)
2. Каноническое имя проверяется на наличие в GATEWAY_KNOWN_COMMANDS
3. Обработчик в _handle_message() диспетчеризует на основе канонического имени
4. Некоторые команды ограничены конфигурацией (gateway_config_gate в CommandDef)
Защита запущенного агента¶
Команды, которые НЕ ДОЛЖНЫ выполняться, пока агент обрабатывает запрос, отклоняются на раннем этапе: [code] if _quick_key in self._running_agents: if canonical == "model": return "⏳ Агент выполняется — дождитесь завершения или используйте /stop."
[/code]
Команды обхода (/stop, /new, /approve, /deny, /queue, /status) имеют специальную обработку.
Источники конфигурации¶
Шлюз читает конфигурацию из нескольких источников:
Источник| Что предоставляет
---|---
~/.hermes/.env| API-ключи, токены ботов, учётные данные платформ
~/.hermes/config.yaml| Настройки модели, конфигурация инструментов, параметры отображения
Переменные окружения| Переопределяют любой из вышеперечисленных
В отличие от CLI (который использует load_cli_config() с жёстко заданными значениями по умолчанию), шлюз читает config.yaml напрямую через загрузчик YAML. Это означает, что ключи конфигурации, существующие в словаре значений по умолчанию CLI, но отсутствующие в файле конфигурации пользователя, могут вести себя по-разному между CLI и шлюзом.
Адаптеры платформ¶
Каждая платформа обмена сообщениями имеет адаптер в gateway/platforms/:
[code]
gateway/platforms/
├── base.py # BaseAdapter — общая логика для всех платформ
├── telegram.py # Telegram Bot API (long polling или webhook)
├── discord.py # Discord bot через 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 сервер
├── qqbot.py # QQ Bot (Tencent QQ) через Official API v2
├── webhook.py # Адаптер входящего/исходящего webhook
├── api_server.py # Адаптер REST API сервера
└── homeassistant.py # Интеграция с Home Assistant conversation
[/code]
Адаптеры реализуют общий интерфейс:
* connect() / disconnect() — управление жизненным циклом
* send_message() — отправка исходящих сообщений
* on_message() — нормализация входящих сообщений → MessageEvent
Блокировки токенов¶
Адаптеры, которые подключаются с уникальными учётными данными, вызывают acquire_scoped_lock() в connect() и release_scoped_lock() в disconnect(). Это предотвращает одновременное использование одного и того же токена бота двумя профилями.
Доставка сообщений¶
Исходящие доставки (gateway/delivery.py) обрабатывают:
* Прямой ответ — отправка ответа обратно в исходный чат
* Доставка в домашний канал — маршрутизация результатов cron-задач и фоновых результатов в настроенный домашний канал
* Доставка в явно указанный канал — инструмент send_message с параметром telegram:-1001234567890
* Кроссплатформенная доставка — доставка на другую платформу, отличную от исходного сообщения
Доставки cron-задач НЕ зеркалируются в историю сессии шлюза — они существуют только в своей собственной cron-сессии. Это осознанное проектное решение для избежания нарушений чередования сообщений.
Хуки¶
Хуки шлюза — это Python-модули, которые реагируют на события жизненного цикла:
События хуков шлюза¶
| Событие | Когда срабатывает |
|---|---|
gateway:startup |
Запуск процесса шлюза |
session:start |
Начало новой сессии беседы |
session:end |
Завершение сессии или истечение тайм-аута |
session:reset |
Пользователь сбрасывает сессию с помощью /new |
agent:start |
Агент начинает обработку сообщения |
agent:step |
Агент завершает одну итерацию вызова инструмента |
agent:end |
Агент завершает работу и возвращает ответ |
command:* |
Выполняется любая слэш-команда |
Хуки обнаруживаются в gateway/builtin_hooks/ (всегда активны) и ~/.hermes/hooks/ (установленные пользователем). Каждый хук — это директория с манифестом HOOK.yaml и handler.py. |
|
| ## Интеграция с провайдером памяти | |
| Когда включён плагин провайдера памяти (например, Honcho): | |
1. Шлюз создаёт AIAgent для каждого сообщения с ID сессии |
|
2. MemoryManager инициализирует провайдера с контекстом сессии |
|
3. Инструменты провайдера (например, honcho_profile, viking_search) маршрутизируются через: |
[code] AIAgent._invoke_tool() → self._memory_manager.handle_tool_call(name, args) → provider.handle_tool_call(name, args)
[/code]
4. При завершении/сбросе сессии срабатывает 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 для завершения всех процессов шлюза (используется при обновлениях).
Связанные документы¶
- Хранилище сессий
- Внутреннее устройство Cron
- Внутреннее устройство ACP
- Внутреннее устройство цикла агента
- Обзор архитектуры
- Поток сообщений
- Авторизация
- Диспетчеризация слэш-команд
- Источники конфигурации
- Адаптеры платформ
- Доставка сообщений
- Хуки
- Интеграция с провайдером памяти
- Фоновое обслуживание
- Управление процессами
- Связанные документы