Добавление адаптера платформы
Это руководство описывает добавление нового мессенджер-платформы в 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 pointPLUGIN.yaml
Заголовок раздела «PLUGIN.yaml»Метаданные плагина. Блоки 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
или расширенный dict для лучшего UX
Заголовок раздела «или расширенный dict для лучшего UX»description: "Канал для подключения"prompt: "Канал"password: falseoptional_env:
- name: MY_PLATFORM_HOME_CHANNEL description: “Канал по умолчанию для доставки cron”password: false
adapter.py
Заголовок раздела «adapter.py»pythonimport osfrom 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, )Configuration
Заголовок раздела «Configuration»Users configure the platform in config.yaml:
yamlgateway: 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 Noneseed = {"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.yamlname: my_platform-platformlabel: My Platformkind: platformversion: 1.0.0description: > Адаптер gateway для My Platform в Hermes Agent.author: Your Namerequires_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: falseoptional_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`:
```pythonclass Platform(str, Enum):
# ... existing platforms ... NEWPLAT = "newplat"2. Файл адаптера
Заголовок раздела «2. Файл адаптера»Создайте gateway/platforms/newplat.py:
pythonfrom gateway.config import Platform, PlatformConfigfrom 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)`:
```pythonsource = 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)Три точки взаимодействия:»get_connected_platforms()— Добавьте проверку обязательных учётных данных для вашей платформыload_gateway_config()— Добавьте запись в карту переменных окружения токенов:Platform.NEWPLAT: "NEWPLAT_TOKEN"_apply_env_overrides()— Сопоставьте все переменные окруженияNEWPLAT_*с конфигурацией
4. Gateway Runner (gateway/run.py)
Заголовок раздела «4. Gateway Runner (gateway/run.py)»Пять точек взаимодействия:1. _create_adapter() — Добавить ветку elif platform == Platform.NEWPLAT:
2. Карта _is_user_authorized() allowed_users — Platform.NEWPLAT: "NEWPLAT_ALLOWED_USERS"
3. Карта _is_user_authorized() allow_all — Platform.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
5. Кроссплатформенная доставка
Заголовок раздела «5. Кроссплатформенная доставка»gateway/platforms/webhook.py— Добавьте"newplat"в кортеж типов доставки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»hermes_cli/gateway.py— Добавьте запись в список_PLATFORMSс ключом, меткой, эмодзи, token_var, setup_instructions и varshermes_cli/platforms.py— Добавьте записьPlatformInfoс меткой и default_toolset (используетсяskills_configиtools_configTUI)4.hermes_cli/setup.py— Добавьте функцию_setup_newplat()(может делегироватьgateway.py) и добавьте кортеж в список платформ обмена сообщениямиhermes_cli/status.py— Добавьте запись обнаружения платформы:"NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")hermes_cli/dump.py— Добавьте"newplat": "NEWPLAT_TOKEN"в словарь обнаружения платформ
7. Инструменты
Заголовок раздела «7. Инструменты»tools/send_message_tool.py— Добавьте"newplat": Platform.NEWPLATв карту платформtools/cronjob_tools.py— Добавьтеnewplatв строку описания цели доставки
8. Наборы инструментов
Заголовок раздела «8. Наборы инструментов»toolsets.py— Добавьте определение набора инструментов"hermes-newplat"с_HERMES_CORE_TOOLStoolsets.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 platformsearch_files "bluebubbles" output_mode="files_only" file_glob="*.py"
# Find every .py file mentioning the new platformsearch_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))Callback/Webhook Адаптеры
Заголовок раздела «Callback/Webhook Адаптеры»Если платформа отправляет сообщения на ваш конечный адрес (например, 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 минут — встроенные ответы в рамках окна обратного вызова невозможны.
### Блокировки токенов
Если адаптер удерживает постоянное соединение с уникальными учётными данными, добавьте блокировку с ограниченной областью действия, чтобы предотвратить использование одних и тех же учётных данных двумя профилями:
```pythonfrom 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 | Полнофункциональный адаптер с группами и тредами |