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

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

Есть два способа добавления платформы: - **Плагин** (рекомендуется для сообщества/сторонних разработчиков): Поместите директорию плагина в `~/.hermes/plugins/` — изменения основного кода не требуются. См. [Путь плагина](#plugin-path-recommended) ниже. - **Встроенная**: Измените 20+ файлов в коде, конфигурации и документации. Используйте [Контрольный список встроенной платформы](#step-by-step-checklist-built-in-path) ниже.

Обзор архитектуры

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

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

Входящие сообщения принимаются адаптером и передаются через self.handle_message(event), который базовый класс маршрутизирует к раннеру шлюза.

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

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

PLUGIN.yaml

Метаданные плагина. Блоки requires_env и optional_env автоматически заполняют записи в UI hermes config (см. Отображение переменных окружения в hermes config ниже).

name: my-platform
label: My Platform
kind: platform
version: 1.0.0
description: My custom messaging platform adapter
author: Your Name
requires_env:
  - MY_PLATFORM_TOKEN          # bare string works
  - name: MY_PLATFORM_CHANNEL  # or rich dict for better UX
    description: "Channel to join"
    prompt: "Channel"
    password: false
optional_env:
  - name: MY_PLATFORM_HOME_CHANNEL
    description: "Default channel for cron delivery"
    password: false

adapter.py

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:
        # Connect to the platform API, start listeners
        self._mark_connected()
        return True

    async def disconnect(self) -> None:
        self._mark_disconnected()

    async def send(self, chat_id, content, reply_to=None, metadata=None):
        # Send message via platform 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 seed


def register(ctx):
    """Plugin entry point — called by the Hermes plugin system."""
    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",
        # Env-driven auto-configuration — seeds PlatformConfig.extra from
        # env vars before adapter construction. See "Env-Driven Auto-
        # Configuration" section below.
        env_enablement_fn=_env_enablement,
        # Cron home-channel delivery support. Lets deliver=my_platform cron
        # jobs route without editing cron/scheduler.py. See "Cron Delivery"
        # section below.
        cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
        # Per-platform user authorization env vars
        allowed_users_env="MY_PLATFORM_ALLOWED_USERS",
        allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS",
        # Message length limit for smart chunking (0 = no limit)
        max_message_length=4000,
        # LLM guidance injected into system prompt
        platform_hint=(
            "You are chatting via My Platform. "
            "It supports markdown formatting."
        ),
        # Display
        emoji="💬",
    )

    # Optional: register platform-specific tools
    ctx.register_tool(
        name="my_platform_search",
        toolset="my_platform",
        schema={...},
        handler=my_search_handler,
    )

Конфигурация

Пользователи настраивают платформу в config.yaml:

gateway:
  platforms:
    my_platform:
      enabled: true
      extra:
        token: "..."
        channel: "#general"

Или через переменные окружения (которые адаптер читает в __init__).

Что система плагинов обрабатывает автоматически

Когда вы вызываете ctx.register_platform(), следующие точки интеграции обрабатываются за вас — изменения основного кода не требуются:

Точка интеграции Как это работает
Создание адаптера шлюза Реестр проверяется перед встроенной цепочкой if/elif
Разбор конфигурации Platform._missing_() принимает любое имя платформы
Валидация подключенной платформы Вызывается validate_config() из реестра
Авторизация пользователей Проверяются allowed_users_env / allow_all_env
Автовключение через env env_enablement_fn заполняет PlatformConfig.extra + home_channel
Доставка Cron cron_deliver_env_var обеспечивает работу deliver=<имя>
Записи в UI hermes config requires_env / optional_env в plugin.yaml автозаполняются
Инструмент send_message Маршрутизируется через активный адаптер шлюза
Кросс-платформенная доставка webhook Реестр проверяется на известные платформы
Доступ к команде /update Флаг allow_update_command
Каталог каналов Платформы-плагины включены в перечисление
Подсказки в системном промпте 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:
    """Seed PlatformConfig.extra from env vars.

    Called by the platform registry during load_gateway_config().
    Return None when the platform isn't minimally configured — the
    caller then skips auto-enabling. Return a dict to seed extras.

    The special 'home_channel' key is extracted and becomes a proper
    HomeChannel dataclass on the PlatformConfig; every other key is
    merged into 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 seed


def 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,
        # ... other fields
    )

Доставка Cron

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

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, которые выполняются до заполнения env.

Доставка cron в отдельном процессе

cron_deliver_env_var делает вашу платформу распознаваемой целью deliver=. Чтобы фактическая отправка срабатывала, когда задание cron выполняется в отдельном процессе от шлюза (т.е. hermes cron run отдельно от hermes gateway), зарегистрируйте standalone_sender_fn:

async def _standalone_send(
    pconfig,
    chat_id,
    message,
    *,
    thread_id=None,
    media_files=None,
    force_document=False,
):
    """Open an ephemeral connection / acquire a fresh token, send, and close."""
    # ... open connection, send message, return result ...
    return {"success": True, "message_id": "..."}
    # or {"error": "..."}

ctx.register_platform(
    name="my_platform",
    ...
    cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
    standalone_sender_fn=_standalone_send,
)

Почему этот хук необходим: встроенные платформы (Telegram, Discord, Slack и т.д.) имеют прямые REST-помощники в tools/send_message_tool.py, так что cron может доставлять сообщения без удержания шлюза в том же процессе. Платформы-плагины исторически полагались на _gateway_runner_ref(), который возвращает None вне процесса шлюза, поэтому без standalone_sender_fn отправка со стороны cron завершается ошибкой No live adapter for platform '<name>'.

Функция получает те же pconfig и chat_id, которые получил бы активный адаптер, плюс опциональные именованные аргументы thread_id, media_files и force_document. Возврат {"success": True, "message_id": ...} считается успешной доставкой; возврат {"error": "..."} отображает сообщение в delivery_errors cron. Исключения, возникшие внутри функции, перехватываются диспетчером и сообщаются как Plugin standalone send failed: <причина>. Эталонные реализации находятся в plugins/platforms/{irc,teams,google_chat}/adapter.py.

Отображение переменных окружения в hermes config

hermes_cli/config.py сканирует plugins/platforms/*/plugin.yaml во время импорта и автоматически заполняет OPTIONAL_ENV_VARS из блоков requires_env и (опционально) optional_env. Используйте расширенную форму словаря для указания описаний, подсказок, флагов паролей и URL — UI настройки CLI подхватит их автоматически.

# plugins/platforms/my_platform/plugin.yaml
name: my_platform-platform
label: My Platform
kind: platform
version: 1.0.0
description: >
  My Platform gateway adapter for Hermes Agent.
author: Your Name
requires_env:
  - name: MY_PLATFORM_TOKEN
    description: "Bot API token from the My Platform console"
    prompt: "My Platform bot token"
    url: "https://my-platform.example.com/bots"
    password: true
  - name: MY_PLATFORM_CHANNEL
    description: "Channel to join (e.g. #hermes)"
    prompt: "Channel"
    password: false
optional_env:
  - name: MY_PLATFORM_HOME_CHANNEL
    description: "Default channel for cron delivery (defaults to MY_PLATFORM_CHANNEL)"
    prompt: "Home channel (or empty)"
    password: false
  - name: MY_PLATFORM_ALLOWED_USERS
    description: "Comma-separated user IDs allowed to talk to the bot"
    prompt: "Allowed users (comma-separated)"
    password: false

Поддерживаемые ключи словаря: name (обязательно), description, prompt, url, password (bool; автоматически определяется по суффиксу *_TOKEN / *_SECRET / *_KEY / *_PASSWORD / *_JSON, если опущен), category (по умолчанию "messaging").

Записи в виде простой строки (- MY_PLATFORM_TOKEN) всё ещё работают — они получают общее описание, автоматически выведенное из label плагина. Если жёстко заданная запись для той же переменной уже существует в OPTIONAL_ENV_VARS, она имеет приоритет (обратная совместимость); форма plugin.yaml действует как запасной вариант.

UX для медленных LLM на конкретных платформах

Некоторые платформы имеют ограничения, которые меняют способ представления медленного ответа LLM:

Это реальные ограничения, которые базовый BasePlatformAdapter не может предвидеть. Поверхность плагина намеренно оставляет место для адаптера, чтобы добавить специфичную для платформы UX поверх базового цикла набора, не расширяя список kwargs.

Паттерн: переопределите _keep_typing для добавления промежуточной UX

BasePlatformAdapter._keep_typing — это сердцебиение индикатора набора; оно выполняется как фоновая задача, пока LLM генерирует ответ, и отменяется, когда ответ доставлен. Чтобы добавить специфичное для платформы поведение по порогу (например, отправить пузырёк «всё ещё думаю» через 45 секунд), переопределите _keep_typing в вашем адаптере, запланируйте свою задачу вместе с super()._keep_typing() и отмените её в finally:

class LineAdapter(BasePlatformAdapter):
    async def _keep_typing(self, chat_id: str, *args, **kwargs) -> None:
        if self.slow_response_threshold <= 0:
            await super()._keep_typing(chat_id, *args, **kwargs)
            return

        async def _fire_at_threshold() -> None:
            try:
                await asyncio.sleep(self.slow_response_threshold)
            except asyncio.CancelledError:
                raise
            # Platform-specific work here — for LINE, send a Template
            # Buttons "Get answer" bubble using the cached reply token
            # so the user can fetch the cached response later via a
            # fresh (free) reply token from the postback callback.
            await self._send_slow_response_button(chat_id)

        side_task = asyncio.create_task(_fire_at_threshold())
        try:
            await super()._keep_typing(chat_id, *args, **kwargs)
        finally:
            if not side_task.done():
                side_task.cancel()
                try:
                    await side_task
                except (asyncio.CancelledError, Exception):
                    pass

Ключевые моменты:

Паттерн: переопределите send для маршрутизации через кэш вместо немедленной отправки

Если ваша UX для медленных ответов кэширует результат для последующего получения (постбэк LINE), ваше переопределение send должно распознавать три режима:

  1. Ожидается постбэк для этого чата → кэшируйте ответ под request_id, не отправляйте ничего видимого.

  2. Системное подтверждение занятости (⚡ Interrupting, ⏳ Queued, ⏩ Steered) → обходите кэш и отправляйте видимо, чтобы пользователь видел ответ шлюза на свой ввод.

  3. Обычный ответ → отправляйте через reply-token-или-push как обычно.

async def send(self, chat_id: str, content: str, **kw) -> SendResult:
    if _is_system_bypass(content):
        return await self._send_text_chunks(chat_id, content, force_push=False)
    pending_rid = self._pending_buttons.get(chat_id)
    if pending_rid:
        self._cache.set_ready(pending_rid, content)
        return SendResult(success=True, message_id=pending_rid)
    return await self._send_text_chunks(chat_id, content, force_push=False)

_SYSTEM_BYPASS_PREFIXES — это собственные префиксы подтверждения занятости шлюза (, , , 💾). Всегда пропускайте их видимо, независимо от состояния кэшированной UX.

Когда этот паттерн уместен

Используйте подход с переопределением цикла набора, когда:

Используйте более простой путь slow_response_threshold = 0 (всегда Push), когда:

LINE поддерживает оба варианта: порог по умолчанию 45 секунд для бесплатного постбэка, а LINE_SLOW_RESPONSE_THRESHOLD=0 возвращается к «всегда Push fallback».

Эталонная реализация

См. plugins/platforms/line/adapter.py для полной реализации постбэка LINE — конечный автомат RequestCache (PENDING → READY → DELIVERED, плюс ERROR для /stop), переопределение _keep_typing, которое запускает пузырёк с кнопками по порогу, переопределение send, которое маршрутизирует через кэш, и переопределение interrupt_session_activity, которое разрешает осиротевшие записи PENDING.

Эталонные реализации (путь плагина)

См. plugins/platforms/irc/ в репозитории для полного рабочего примера — полноценный async IRC адаптер без внешних зависимостей. plugins/platforms/teams/ покрывает Bot Framework / Adaptive Cards, plugins/platforms/google_chat/ — OAuth-основанные REST API, а plugins/platforms/line/ — webhook-управляемые Messaging API со специфичной для платформы UX для медленных LLM.


Пошаговый контрольный список (встроенный путь)

Этот контрольный список предназначен для добавления платформы непосредственно в основную кодовую базу Hermes — обычно выполняется основными разработчиками для официально поддерживаемых платформ. Платформы сообщества/сторонних разработчиков должны использовать [Путь плагина](#plugin-path-recommended) выше.

1. Перечисление платформ (Enum)

Добавьте вашу платформу в перечисление Platform в gateway/config.py:

class Platform(str, Enum):
    # ... existing platforms ...
    NEWPLAT = "newplat"

2. Файл адаптера

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

from gateway.config import Platform, PlatformConfig
from gateway.platforms.base import (
    BasePlatformAdapter, MessageEvent, MessageType, SendResult,
)

def check_newplat_requirements() -> bool:
    """Return True if dependencies are available."""
    return SOME_SDK_AVAILABLE

class NewPlatAdapter(BasePlatformAdapter):
    def __init__(self, config: PlatformConfig):
        super().__init__(config, Platform.NEWPLAT)
        # Read config from config.extra dict
        extra = config.extra or {}
        self._api_key = extra.get("api_key") or os.getenv("NEWPLAT_API_KEY", "")

    async def connect(self) -> bool:
        # Set up connection, start polling/webhook
        self._mark_connected()
        return True

    async def disconnect(self) -> None:
        self._running = False
        self._mark_disconnected()

    async def send(self, chat_id, content, reply_to=None, metadata=None):
        # Send message via platform 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):

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/config.py)

Три точки взаимодействия:

  1. get_connected_platforms() — Добавьте проверку учётных данных вашей платформы

  2. load_gateway_config() — Добавьте запись в карту env токенов: Platform.NEWPLAT: "NEWPLAT_TOKEN"

  3. _apply_env_overrides() — Сопоставьте все переменные окружения NEWPLAT_* с конфигурацией

4. Раннер шлюза (gateway/run.py)

Пять точек взаимодействия:

  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. Ранняя проверка env кортеж _any_allowlist — Добавьте "NEWPLAT_ALLOWED_USERS"

  5. Ранняя проверка env кортеж _allow_all — Добавьте "NEWPLAT_ALLOW_ALL_USERS"

  6. frozenset _UPDATE_ALLOWED_PLATFORMS — Добавьте Platform.NEWPLAT

5. Кросс-платформенная доставка

  1. gateway/platforms/webhook.py — Добавьте "newplat" в кортеж типов доставки

  2. cron/scheduler.py — Добавьте в frozenset _KNOWN_DELIVERY_PLATFORMS и карту платформ _deliver_result()

6. Интеграция с CLI

  1. hermes_cli/config.py — Добавьте все переменные NEWPLAT_* в _EXTRA_ENV_KEYS

  2. hermes_cli/gateway.py — Добавьте запись в список _PLATFORMS с ключом, меткой, эмодзи, token_var, инструкциями по настройке и переменными

  3. hermes_cli/platforms.py — Добавьте запись PlatformInfo с меткой и toolset по умолчанию (используется в TUI skills_config и tools_config)

  4. hermes_cli/setup.py — Добавьте функцию _setup_newplat() (может делегировать gateway.py) и добавьте кортеж в список платформ обмена сообщениями

  5. hermes_cli/status.py — Добавьте запись обнаружения платформы: "NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")

  6. hermes_cli/dump.py — Добавьте "newplat": "NEWPLAT_TOKEN" в словарь обнаружения платформ

7. Инструменты

  1. tools/send_message_tool.py — Добавьте "newplat": Platform.NEWPLAT в карту платформ

  2. tools/cronjob_tools.py — Добавьте newplat в строку описания цели доставки

8. Наборы инструментов (Toolsets)

  1. toolsets.py — Добавьте определение toolset "hermes-newplat" с _HERMES_CORE_TOOLS

  2. toolsets.py — Добавьте "hermes-newplat" в список включений "hermes-gateway"

9. Опционально: Подсказки платформы

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

_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, покрывающий:

11. Документация

Файл Что добавить
website/docs/user-guide/messaging/newplat.md Полная страница настройки платформы
website/docs/user-guide/messaging/index.md Таблица сравнения платформ, диаграмма архитектуры, таблица toolsets, раздел безопасности, ссылка на следующие шаги
website/docs/reference/environment-variables.md Все переменные окружения NEWPLAT_*
website/docs/reference/toolsets-reference.md Toolset hermes-newplat
website/docs/integrations/index.md Ссылка на платформу
website/sidebars.ts Запись в боковой панели для страницы документации
website/docs/developer-guide/architecture.md Количество адаптеров + список
website/docs/developer-guide/gateway-internals.md Список файлов адаптера

Аудит паритетности

Прежде чем отмечать PR новой платформы как завершённый, проведите аудит паритетности относительно существующей платформы:

# 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-Poll)

Если ваш адаптер использует длинный опрос (как Telegram или Weixin), используйте цикл опроса:

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

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

Адаптеры с обратным вызовом/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 минут — встроенные ответы в окне обратного вызова невозможны.

Блокировки токенов

Если адаптер поддерживает постоянное соединение с уникальными учётными данными, добавьте ограниченную блокировку, чтобы предотвратить использование одних и тех же учётных данных двумя профилями:

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)

Эталонные реализации

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