Внутренний шлюз устройства
Шлюз обмена сообщениями — это долго работающий процесс, который соединяет Hermes с более чем 20 платформами обмена сообщениями через единую архитектуру.
Ключевые файлы
| Файл | Назначение |
|---|---|
шлюз/run.py |
GatewayRunner — главный цикл, слэш-команды, отправка сообщений (большой файл; проверьте git для текущего LOC) |
gateway/session.py |
SessionStore — сохранение диалогов и построение переключения сессии |
gateway/delivery.py |
Доставка исходящих сообщений на целевые платформы/каналы |
gateway/pairing.py |
Процесс привязки через ЛС для авторизации пользователя |
gateway/channel_directory.py |
Сопоставление ID чатов с человекочитаемыми именами для доставки по расписанию |
gateway/hooks.py |
Обнаружение хуков, загрузка и диспетчеризация событий жизненного цикла |
gateway/mirror.py |
Зеркалирование сообщений между сессиями для send_message |
gateway/status.py |
Управление блокировкой токенов для экземпляров шлюза с видимым внешним профилем |
шлюз/builtin_hooks/ |
Расширение точки для всегда зарегистрированных хуков (без ограничений) |
шлюз/платформы/ |
Адаптеры платформы (по одной на каждой платформе обмена сообщениями) |
Обзор конструкции
┌─────────────────────────────────────────────────┐
│ 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}
Например: агент:main:telegram:private:123456789
Платформы с поддержкой тредов (темы форумов Telegram, треды Discord, треды Slack) могут включать идентификатор треда в частьchat_id. Никогда не создавайте ключи сеанса вручную — всегда воспользуйтесь build_session_key() из gateway/session.py.
Двухуровневая защита сообщений
Когда агент активно работает, входящие сообщения передаются через две последовательные проверки:
-
Уровень 1 — Базовый адаптер (
gateway/platforms/base.py): Проверяет_active_sessions. Если сессия активна, помещается сообщение в очередь_pending_messagesи устанавливается событие прерывания. Это перехватывает сообщения до того, как они добились исполнения шлюза. -
Уровень 2 — Исполнитель шлюза (
gateway/run.py): Проверяет_running_agents. Перехватывает определенные команды (/stop,/new,/queue,/status,/approve,/deny) и направляет их соответствующим образом. Всё остальное вызываетrunning_agent.interrupt().
Команды, которые должны достичь исполнителя, пока агент заблокирован (например, /approve), обрабатываются встроенно через await self._message_handler(event) — они обходят фоновые задачи системы, чтобы избежать прохождения гонки.
Авторизация
Шлюз использует многоуровневую проверку авторизации, выполняющую порядку:
- Флаг разрешения для всех платформ (например, «TELEGRAM_ALLOW_ALL_USERS») — если этот флажок установлен, все пользователи на этой платформе авторизованы.
- Белый список платформы (например,
TELEGRAM_ALLOWED_USERS) — идентификатор пользователей через запятую. - Привязка через ЛС — авторизованные пользователи могут привязывать новые пользователи с помощью кода привязки.
- Глобальное разрешение всех (
GATEWAY_ALLOW_ALL_USERS) — если установлено, все пользователи на всех платформах авторизованы. - По: запрет — неавторизованные настройки пользователи отклоняются.
Процесс# привязки через ЛС
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 (длинный опрос или вебхук)
├── discord.py # Discord бот через discord.py
├── slack.py # Slack Socket Mode
├── whatsapp.py # WhatsApp Business Cloud API
├── signal.py # Signal через REST API signal-cli
├── 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 или вебхук
├── wecom.py # WeCom (WeChat Work) обратный вызов
├── weixin.py # Weixin (личный WeChat) через API iLink Bot
├── bluebubbles.py # Apple iMessage через сервер BlueBubbles macOS
├── qqbot/ # QQ Bot (Tencent QQ) через Official API v2 (подпакет: adapter.py, crypto.py, keyboards.py, …)
├── yuanbao.py # Yuanbao (Tencent) адаптер ЛС/групп
├── feishu_comment.py # Обработчик комментариев к документам/дискам Feishu
├── msgraph_webhook.py # Вебхук уведомлений об изменениях Microsoft Graph (Teams, Outlook и т.д.)
├── webhook.py # Адаптер входящего/исходящего вебхука
├── api_server.py # Адаптер REST API сервера
└── homeassistant.py # Интеграция с диалогами Home Assistant
Адаптеры реализуют общий интерфейс:
- connect() / disconnect() — управление жизненным циклом
- send_message() — доставка исходящих сообщений
- on_message() — нормализация входящих сообщений → MessageEvent
Блокировка токенов
Адаптеры, которые подключаются с учетом учётных данных, вызывают acquire_scoped_lock() в connect() и release_scoped_lock() в disconnect(). Это собственное использование одного и того же тока бота двумя разными профилями одновременно.
Путь доставки
Исходящие доставки (gateway/delivery.py) обрабатывают:
- Прямой ответ — отправить ответ обратно в исходный чат
- Доставка в домашний канал — направлять запланированные задачи и фоновые результаты в настроенный домашний канал.
- Явная целевая доставка — инструмент
send_message, указывающийtelegram:-1001234567890 - Кроссплатформенная доставка — доставка на платформу, результат исходящего сообщения.
Доставки запланированных задач НЕ зеркалируются в истории шлюза — они существуют только в своей собственной сессии cron. Это осознанное проектное решение для предотвращения чередования сообщений.
Хуки
Хуки шлюза — это Python-модули, которые реагируют на события жизненного цикла:
События хуков шлюза
| Событие | Когда реализация |
|---|---|
шлюз: запуск |
Запуск процесса шлюза |
сеанс: начало |
Начало нового сеанса диалога |
сеанс: конец |
Завершение сессии или источение времени ожидания |
сеанс: сброс |
Пользователь сбрасывает сессию команды /new |
агент: старт |
Агент начала обработки сообщений |
агент:шаг |
Агент завершает и выполняет одну терацию инструмента |
агент:конец |
Агент завершает работу и возвращает ответ |
команда:* |
Выполняется любая слэш-команда |
Хуки обнаруживаются в gateway/builtin_hooks/ (точка расширения — в настоящее время пуста в поставляемом дистрибутиве; _register_builtin_hooks() — это заглушка без операций) и ~/.hermes/hooks/ (установленные пользователем). Каждый хук представляет собой каталог с манифестом HOOK.yaml и handler.py.
Интеграция с провайдером памяти
При включении разъема провайдера памяти (например, Honcho):
- Шлюз создаёт
AIAgentдля каждого сообщения с идентификатором сессии. 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()для очистки и конечной выгрузки данных.
Жизненный цикл сброса памяти
Когда сессия сбрасывается, возобновляется или прекращается:
1. Встроенные данные памяти сбрасываются на диск.
2. реализация хука on_session_end() провайдера памяти
3. Временный AIAgent выполняет один оборот диалога, управляемый только памятью.
4. После этого контекст выбрасывается или архивируется.
Фоновое обслуживание
Шлюз выполняет периодические задачи по обслуживанию вместе с обработкой сообщений:
- Тиканье cron — на следующий день запланированы задачи и запущены невыполненные задачи.
- Истечение сессий — очищает заброшенные сессии после тайм-аута.
- Сброс памяти — упреждающе сбрасывает память до истечения сессии.
- Обновление кэша — обновляет настройки моделей и статус провайдеров.
Управление процессами
Шлюз работает как долгоживущий процесс, управляемый через:
запуск шлюза Гермеса/останов шлюза Гермес— ручное управлениеsystemctl(Linux) илиlaunchctl(macOS) — управление службами- PID-файл в
~/.hermes/gateway.pid— отслеживание процесса с областью внешнего профиля.
Область внешнего профиля против глобального: start_gateway() использует PID-файлы с областью внешнего профиля. hermes Gateway Stop останавливает шлюз только текущего профиля. hermes Gateway Stop --all использует глобальное сканирование ps aux для завершения всех процессов шлюза (используется при обновлениях).