Перейти к содержимому

Добавление адаптера платформы

Это руководство описывает добавление нового мессенджер-платформы в gateway Hermes. Адаптер платформы подключает Hermes к внешнему сервису обмена сообщениями (Telegram, Discord, WeCom и т. д.), позволяя пользователям взаимодействовать с агентом через этот сервис.

User ↔ Messaging Platform ↔ Platform Adapter ↔ Gateway Runner ↔ AIAgent

Каждый адаптер расширяет BasePlatformAdapter из gateway/platforms/base.py и реализует:

  • connect() — Установить соединение (WebSocket, long-poll, HTTP сервер и т.д.) (абстрактный)
  • disconnect() — Корректное завершение работы (абстрактный)
  • send() — Отправить текстовое сообщение в чат (абстрактный)
  • send_typing() — Показать индикатор набора текста (необязательное переопределение)
  • get_chat_info() — Вернуть метаданные чата (необязательное переопределение)Входящие сообщения принимаются адаптером и пересылаются через self.handle_message(event), после чего базовый класс направляет их в gateway runner.

Система плагинов позволяет добавить адаптер платформы без изменения основного кода Hermes. Ваш плагин представляет собой каталог с двумя файлами:

~/.hermes/plugins/my-platform/
PLUGIN.yaml
# Plugin metadata
adapter.py
# Adapter class + register() entry point

Метаданные плагина. Блоки requires_env и optional_env автоматически заполняют записи в интерфейсе hermes config (см. Surfacing Env Vars ниже).name: my-platform label: My Platform kind: platform version: 1.0.0 description: Аптер пользовательской платформы обмена сообщениями author: Ваше имя requires_env:

  • MY_PLATFORM_TOKEN
  • name: MY_PLATFORM_CHANNEL
description: "Канал для подключения"
prompt: "Канал"
password: false

optional_env:

  • name: MY_PLATFORM_HOME_CHANNEL description: “Канал по умолчанию для доставки cron”password: false
python
import os
from gateway.platforms.base import (
BasePlatformAdapter, SendResult, MessageEvent, MessageType,
)
from gateway.config import Platform, PlatformConfig
```class MyPlatformAdapter(BasePlatformAdapter):
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform("my_platform"))
extra = config.extra or {}
self.token = os.getenv("MY_PLATFORM_TOKEN") or extra.get("token", "")
async def connect(self) -> bool:
# Подключиться к платформе API, запустить слушатели
self._mark_connected()
return Trueasync def disconnect(self) -> None:
self._mark_disconnected()
async def send(self, chat_id, content, reply_to=None, metadata=None):
# Отправить сообщение через платформу API
return SendResult(success=True, message_id="...")
async def get_chat_info(self, chat_id):
return {"name": chat_id, "type": "dm"}
def check_requirements() -> bool:
return bool(os.getenv("MY_PLATFORM_TOKEN"))def validate_config(config) -> bool:
extra = getattr(config, "extra", {}) or {}
return bool(os.getenv("MY_PLATFORM_TOKEN") or extra.get("token"))def _env_enablement() -> dict | None:
token = os.getenv("MY_PLATFORM_TOKEN", "").strip()
channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip()
if not (token and channel):
return None
seed = {"token": token, "channel": channel}
home = os.getenv("MY_PLATFORM_HOME_CHANNEL")
if home:
seed["home_channel"] = {"chat_id": home, "name": "Home"}
return seeddef register(ctx):
"""Точка входа плагина — вызывается системой плагинов Hermes."""
ctx.register_platform(
name="my_platform",
label="My Platform",
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
check_fn=check_requirements,
validate_config=validate_config,
required_env=["MY_PLATFORM_TOKEN"],
install_hint="pip install my-platform-sdk",
# Автоконфигурация на основе переменных окружения — заполняет PlatformConfig.extra
# из переменных окружения до создания адаптера. См. раздел
# "Автоконфигурация на основе переменных окружения" ниже.
env_enablement_fn=_env_enablement,
# Поддержка доставки в домашний канал через cron. Позволяет маршрутизировать
# cron-задачи с deliver=my_platform без редактирования cron/scheduler.py.
# См. раздел "Доставка через cron" ниже.cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
# Переменные окружения для авторизации пользователей на каждой платформе
allowed_users_env="MY_PLATFORM_ALLOWED_USERS",
allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS",
# Ограничение длины сообщения для умного разбиения на фрагменты (0 = без ограничений)
max_message_length=4000,
# LLM подсказка, внедряемая в системный prompt
platform_hint=(
"Вы общаетесь через My Platform. ""Он поддерживает форматирование markdown."
),
# Отображение
emoji="💬",
)
# Опционально: зарегистрируйте инструменты, специфичные для платформы
ctx.register_tool(
name="my_platform_search",
toolset="my_platform",
schema={...},
handler=my_search_handler,
)

Users configure the platform in config.yaml:

yaml
gateway:
platforms:
my_platform:
enabled: true
extra:
token: "..."
channel: "#general"
```Или через переменные среды (которые адаптер считывает в `__init__`).
### Что система плагинов обрабатывает автоматически
При вызове `ctx.register_platform()` следующие точки интеграции обрабатываются за вас — без необходимости вносить изменения в основной код:| Точка интеграции | Как это работает |
|---|---|
| Создание адаптера gateway | Реестр проверяется перед встроенной цепочкой if/elif |
| Разбор конфигурации | `Platform._missing_()` принимает любое имя платформы |
| Проверка подключённой платформы | Вызывается реестр `validate_config()` |
| Авторизация пользователя | Проверяется `allowed_users_env` / `allow_all_env` |
| Автовключение только через env | `env_enablement_fn` задаёт `PlatformConfig.extra` + `home_channel` || Cron delivery | `cron_deliver_env_var` делает `deliver=<name>` рабочим |
| `hermes config` записи UI | `requires_env` / `optional_env` в `plugin.yaml` заполняются автоматически |
| send_message tool | Маршрутизируется через живой адаптер gateway |
| Webhook cross-platform delivery | Реестр проверяется на наличие известных платформ |
| `/update` доступ к команде | `allow_update_command` флаг |
| Channel directory | Платформы плагинов включены в перечисление || Подсказки системного prompt | `platform_hint` внедряются в контекст LLM |
| Разбиение сообщений на фрагменты | `max_message_length` для умного разделения |
| PII редактирование | флаг `pii_safe` |
| `hermes status` | Показывает платформы плагинов с тегом `(plugin)` |
| `hermes gateway setup` | Платформы плагинов отображаются в меню настройки |
| `hermes tools` / `hermes skills` | Платформы плагинов в конфигурации для каждой платформы || Блокировка токена (мультипрофильный режим) | Используйте `acquire_scoped_lock()` в вашем `connect()` |
| Предупреждение об осиротевшей конфигурации | Описательное сообщение в логе при отсутствии плагина |
## Автоматическая настройка через переменные окружения
Большинство пользователей настраивают платформу, добавляя переменные окружения в `~/.hermes/.env`, а не редактируя `config.yaml`. Хук `env_enablement_fn` позволяет вашему плагину подхватить эти переменные окружения **до** создания адаптера, благодаря чему `hermes gateway status`, `get_connected_platforms()` и cron-доставка видят корректное состояние без необходимости инстанцировать платформу SDK.def _env_enablement() -> dict | None:
"""Заполняет PlatformConfig.extra из переменных окружения.
Вызывается реестром платформ во время load_gateway_config().
Возвращает None, когда платформа не настроена минимально —
вызывающая сторона пропускает автовключение. Возвращает dict для заполнения extras.Специальный ключ 'home_channel' извлекается и становится полноценным экземпляром датакласса HomeChannel в PlatformConfig; все остальные ключи объединяются в PlatformConfig.extra.
"""
token = os.getenv("MY_PLATFORM_TOKEN", "").strip()
channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip()
if not (token and channel):
return None
seed = {"token": token, "channel": channel}
home = os.getenv("MY_PLATFORM_HOME_CHANNEL")if home:
seed["home_channel"] = {
"chat_id": home,
"name": os.getenv("MY_PLATFORM_HOME_CHANNEL_NAME", "Home"),
}
return seeddef register(ctx):
ctx.register_platform(
name="my_platform",
label="My Platform",
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
check_fn=check_requirements,
validate_config=validate_config,
env_enablement_fn=_env_enablement,
# ... другие поля
)

Доставка по cronЧтобы позволить cron-заданиям deliver=my_platform маршрутизировать сообщения в настроенный домашний канал, задайте cron_deliver_env_var в качестве имени переменной окружения, содержащей ID канала/чата по умолчанию chat/room:

Заголовок раздела «Доставка по cronЧтобы позволить cron-заданиям deliver=my_platform маршрутизировать сообщения в настроенный домашний канал, задайте cron_deliver_env_var в качестве имени переменной окружения, содержащей ID канала/чата по умолчанию chat/room:»
ctx.register_platform(
name="my_platform",
...
cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
)
```Планировщик считывает эту переменную окружения при определении целевого домашнего каталога для заданий `deliver=my_platform`, а также рассматривает платформу как допустимую цель cron в проверках в стиле `_KNOWN_DELIVERY_PLATFORMS`. Если ваш `env_enablement_fn` инициализирует словарь `home_channel` (см. выше), он имеет приоритет — `cron_deliver_env_var` используется как запасной вариант для cron-заданий, запускаемых до инициализации переменных окружения.
## Отображение переменных окружения в `hermes config``hermes_cli/config.py` сканирует `plugins/platforms/*/plugin.yaml` во время импорта и автоматически заполняет `OPTIONAL_ENV_VARS` из блоков `requires_env` и (опционально) `optional_env`. Используйте форму rich-dict для добавления корректных описаний, prompts, флагов паролей и URL — интерфейс настройки CLI подхватывает их автоматически.
# plugins/platforms/my_platform/plugin.yaml
name: my_platform-platform
label: My Platform
kind: platform
version: 1.0.0
description: >
Адаптер gateway для My Platform в Hermes Agent.
author: Your Name
requires_env:
- name: MY_PLATFORM_TOKEN
description: "Токен бота API из консоли My Platform"
prompt: "Токен бота My Platform"
url: "https://my-platform.example.com/bots"
password: true
- name: MY_PLATFORM_CHANNELdescription: "Канал для подключения (например, #hermes)"
prompt: "Канал"
password: false
optional_env:
- name: MY_PLATFORM_HOME_CHANNEL
description: "Канал по умолчанию для доставки cron (по умолчанию MY_PLATFORM_CHANNEL)"
prompt: "Домашний канал (или пусто)"
password: false
- name: MY_PLATFORM_ALLOWED_USERS
description: "Идентификаторы пользователей через запятую, которым разрешено общаться с ботом"
prompt: "Разрешённые пользователи (через запятую)"password: false**Поддерживаемые ключи словаря:** `name` (обязательный), `description`, `prompt`, `url`, `password` (bool; определяется автоматически из суффикса `*_TOKEN` / `*_SECRET` / `*_KEY` / `*_PASSWORD` / `*_JSON` при отсутствии), `category` (по умолчанию `"messaging"`).Записи в виде простых строк (`- MY_PLATFORM_TOKEN`) по-прежнему работают — для них автоматически формируется общее описание на основе `label` плагина. Если жёстко заданная запись для той же переменной уже существует в `OPTIONAL_ENV_VARS`, она имеет приоритет (обратная совместимость); форма plugin.yaml используется как запасной вариант.
### Эталонная реализация
См. `plugins/platforms/irc/` в репозитории для полного рабочего примера — полный асинхронный адаптер IRC без внешних зависимостей.
## Пошаговый чек-лист (встроенный путь)
:::note
Этот чек-лист предназначен для добавления платформы непосредственно в основную кодовую базу Hermes — обычно выполняется основными контрибьюторами для официально поддерживаемых платформ. Community/third-party платформы должны использовать [путь плагинов](#plugin-path-recommended) выше.
:::
### 1. Перечисление платформ
Добавьте вашу платформу в перечисление `Platform` в `gateway/config.py`:
```python
class Platform(str, Enum):
# ... existing platforms ...
NEWPLAT = "newplat"

Создайте gateway/platforms/newplat.py:

python
from gateway.config import Platform, PlatformConfig
from gateway.platforms.base import (
BasePlatformAdapter, MessageEvent, MessageType, SendResult,
)
def check_newplat_requirements() -> bool:
"""Возвращает True, если зависимости доступны."""
return SOME_SDK_AVAILABLE
```class NewPlatAdapter(BasePlatformAdapter):
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform.NEWPLAT)
# Прочитать конфигурацию из словаря config.extra
extra = config.extra or {}
self._api_key = extra.get("api_key") or os.getenv("NEWPLAT_API_KEY", "")
async def connect(self) -> bool:
# Настроить подключение, запустить polling/webhook
self._mark_connected()
return Trueasync def disconnect(self) -> None:
self._running = False
self._mark_disconnected()
async def send(self, chat_id, content, reply_to=None, metadata=None):
# Отправить сообщение через платформу API
return SendResult(success=True, message_id="...")
async def get_chat_info(self, chat_id):
return {"name": chat_id, "type": "dm"}Для входящих сообщений создайте `MessageEvent` и вызовите `self.handle_message(event)`:
```python
source = self.build_source(
chat_id=chat_id,
chat_name=name,
chat_type="dm",
# or "group"
user_id=user_id,
user_name=user_name,
)
event = MessageEvent(
text=content,
message_type=MessageType.TEXT,
source=source,
message_id=msg_id,
)
await self.handle_message(event)

3. Конфигурация gateway (gateway/config.py)Три точки взаимодействия:

Заголовок раздела «3. Конфигурация gateway (gateway/config.py)Три точки взаимодействия:»
  1. get_connected_platforms() — Добавьте проверку обязательных учётных данных для вашей платформы
  2. load_gateway_config() — Добавьте запись в карту переменных окружения токенов: Platform.NEWPLAT: "NEWPLAT_TOKEN"
  3. _apply_env_overrides() — Сопоставьте все переменные окружения NEWPLAT_* с конфигурацией

Пять точек взаимодействия:1. _create_adapter() — Добавить ветку elif platform == Platform.NEWPLAT: 2. Карта _is_user_authorized() allowed_usersPlatform.NEWPLAT: "NEWPLAT_ALLOWED_USERS" 3. Карта _is_user_authorized() allow_allPlatform.NEWPLAT: "NEWPLAT_ALLOW_ALL_USERS" 4. Кортеж ранней проверки окружения _any_allowlist — Добавить "NEWPLAT_ALLOWED_USERS" 5. Кортеж ранней проверки окружения _allow_all — Добавить "NEWPLAT_ALLOW_ALL_USERS"6. _UPDATE_ALLOWED_PLATFORMS frozenset — Добавьте Platform.NEWPLAT

  1. gateway/platforms/webhook.py — Добавьте "newplat" в кортеж типов доставки
  2. cron/scheduler.py — Добавьте в frozenset _KNOWN_DELIVERY_PLATFORMS и карту платформ _deliver_result()

6. Интеграция CLI1. hermes_cli/config.py — Добавьте все переменные NEWPLAT_* в _EXTRA_ENV_KEYS

Заголовок раздела «6. Интеграция CLI1. hermes_cli/config.py — Добавьте все переменные NEWPLAT_* в _EXTRA_ENV_KEYS»
  1. hermes_cli/gateway.py — Добавьте запись в список _PLATFORMS с ключом, меткой, эмодзи, token_var, setup_instructions и vars
  2. hermes_cli/platforms.py — Добавьте запись PlatformInfo с меткой и default_toolset (используется skills_config и tools_config TUI)4. hermes_cli/setup.py — Добавьте функцию _setup_newplat() (может делегировать gateway.py) и добавьте кортеж в список платформ обмена сообщениями
  3. hermes_cli/status.py — Добавьте запись обнаружения платформы: "NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")
  4. hermes_cli/dump.py — Добавьте "newplat": "NEWPLAT_TOKEN" в словарь обнаружения платформ
  1. tools/send_message_tool.py — Добавьте "newplat": Platform.NEWPLAT в карту платформ
  2. tools/cronjob_tools.py — Добавьте newplat в строку описания цели доставки
  1. toolsets.py — Добавьте определение набора инструментов "hermes-newplat" с _HERMES_CORE_TOOLS
  2. toolsets.py — Добавьте "hermes-newplat" в список включений "hermes-gateway"

9. Опционально: Подсказки платформы**agent/prompt_builder.py** — Если ваша платформа имеет специфические ограничения на отображение (отсутствие markdown, ограничения на длину сообщений и т.д.), добавьте запись в словарь _PLATFORM_HINTS. Это добавляет специфические для платформы инструкции в системный prompt:

Заголовок раздела «9. Опционально: Подсказки платформы**agent/prompt_builder.py** — Если ваша платформа имеет специфические ограничения на отображение (отсутствие markdown, ограничения на длину сообщений и т.д.), добавьте запись в словарь _PLATFORM_HINTS. Это добавляет специфические для платформы инструкции в системный prompt:»
_PLATFORM_HINTS = {
# ...
"newplat": (
"You are chatting via NewPlat. It supports markdown formatting "
"but has a 4000-character message limit."
),
}
```Не всем платформам нужны подсказки — добавляйте их только если поведение агента должно отличаться.
### 10. Тесты
Создайте `tests/gateway/test_newplat.py`, покрывающие:
- Создание адаптера из конфигурации
- Формирование событий сообщений
- Метод отправки (с мокированием внешнего API)
- Специфичные для платформы функции (шифрование, маршрутизация и т.д.)
### 11. Документация| Файл | Что добавить |
|------|-------------|
| `website/docs/user-guide/messaging/newplat.md` | Полная страница настройки платформы |
| `website/docs/user-guide/messaging/index.md` | Таблица сравнения платформ, диаграмма архитектуры, таблица наборов инструментов, раздел безопасности, ссылка на следующие шаги |
| `website/docs/reference/environment-variables.md` | Все переменные окружения NEWPLAT_* |
| `website/docs/reference/toolsets-reference.md` | набор инструментов hermes-newplat || `website/docs/integrations/index.md` | Ссылка на платформу |
| `website/sidebars.ts` | Элемент боковой панели для страницы документации |
| `website/docs/developer-guide/architecture.md` | Количество адаптеров + список |
| `website/docs/developer-guide/gateway-internals.md` | Список файлов адаптеров |
## Аудит паритета
Перед тем как пометить PR с новой платформой как завершённый, выполните аудит паритета по сравнению с уже существующей платформой:
```bash
# Find every .py file mentioning the reference platform
search_files "bluebubbles" output_mode="files_only" file_glob="*.py"
# Find every .py file mentioning the new platform
search_files "newplat" output_mode="files_only" file_glob="*.py"
# Any file in the first set but not the second is a potential gap
```Повторите для файлов `.md` и `.ts`. Изучите каждое расхождение — является ли это перечислением платформ (требует обновления) или ссылкой на конкретную платформу (пропустите)?
## Типовые паттерны
### Адаптеры с долгим опросом
Если ваш адаптер использует долгий опрос (long-polling), например как Telegram или Weixin, используйте задачу цикла опроса:

python async def connect(self): self._poll_task = asyncio.create_task(self._poll_loop()) self._mark_connected()

while self._running:
messages = await self._fetch_updates()
for msg in messages:
await self.handle_message(self._build_event(msg))

Если платформа отправляет сообщения на ваш конечный адрес (например, WeCom Callback), запустите сервер HTTP:

async def connect(self):
self._app = web.Application()
self._app.router.add_post("/callback", self._handle_callback)
# ... start aiohttp server
self._mark_connected()
async def _handle_callback(self, request):
event = self._build_event(await request.text())
await self._message_queue.put(event)
return web.Response(text="success")
# Acknowledge immediately
```Для платформ с жёсткими ограничениями по времени ответа (например, лимит WeCom в 5 секунд) всегда отправляйте немедленное подтверждение, а ответ агента доставляйте проактивно через API позже. Сессии агента длятся от 3 до 30 минут — встроенные ответы в рамках окна обратного вызова невозможны.
### Блокировки токенов
Если адаптер удерживает постоянное соединение с уникальными учётными данными, добавьте блокировку с ограниченной областью действия, чтобы предотвратить использование одних и тех же учётных данных двумя профилями:
```python
from gateway.status import acquire_scoped_lock, release_scoped_lock
async def connect(self):
if not acquire_scoped_lock("newplat", self._token):
logger.error("Token already in use by another profile")
return False
# ... connect
async def disconnect(self):
release_scoped_lock("newplat", self._token)

Эталонные реализации| Adapter | Pattern | Complexity | Good reference for |

Заголовок раздела «Эталонные реализации| Adapter | Pattern | Complexity | Good reference for |»

|---------|---------|------------|-------------------| | bluebubbles.py | REST + webhook | Medium | Простая интеграция REST API | | weixin.py | Long-poll + CDN | High | Обработка медиа, шифрование | | wecom_callback.py | Callback/webhook | Medium | HTTP сервер, AES криптография, мульти-приложения | | telegram.py | Long-poll + Bot API | High | Полнофункциональный адаптер с группами и тредами |