Основные концепции адаптера
Понимание основных концепций адаптера ErisPulse — это основа для разработки адаптеров.
Архитектура адаптера
Отношения между компонентами
Прямое преобразование (направление приёма) Обратное преобразование (направление отправки)
───────────────── ─────────────────
┌──────────────────┐ ┌──────────────────┐
│ Платформа-специфич. события │ │ Модуль формирует сообщение │
└────────┬─────────┘ └────────┬─────────┘
│ │
↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ │ │ Адаптер (MyAdapter) │ │ Send.Raw_ob12() │
│ Converter │ │ ┌──────────────┐ │ │ (точка входа обратного преобразования) │
│ (конвертер событий) │──→│ │ │ │ │ │
│ │ │ │ │ │ │ │
└──────────────────┘ │ └──────────────┘ │ └────────┬─────────┘
└──────────────────┘ │
│ ↓
↓ ┌──────────────────┐
┌──────────────────┐ │ Вызов API платформы │
│ Стандартные события OneBot12 │ └────────┬─────────┘
└────────┬─────────┘ │
│ ↓
↓ ┌──────────────────┐
┌──────────────────┐ │ Стандартный формат ответа │
│ Система событий │ └──────────────────┘
└────────┬─────────┘
│
↓
┌──────────────────┐
│ Модуль (обработка событий) │
└──────────────────┘
Основная симметрия:
- Прямое преобразование (Converter): платформа-специфич. события → события OneBot12, оригинальные данные сохраняются в
{platform}_raw - Обратное преобразование (Raw_ob12): сообщения OneBot12 → вызов API платформы, возвращается стандартный формат ответа
AdapterManager адаптер-менеджер
AdapterManager — это основной компонент адаптерной системы ErisPulse, отвечающий за управление регистрацией, запуском, остановкой и распределением событий всех адаптеров платформ.
Основные функции
- Регистрация адаптеров: Регистрация и управление несколькими адаптерами платформ.
- Управление жизненным циклом: Контроль запуска и остановки адаптеров.
- Распределение событий: Распределение событий по стандарту OneBot12 и событий оригинальных платформ.
- Управление конфигурацией: Управление включением/отключением адаптеров.
- Поддержка промежуточных обработчиков: Поддержка промежуточных обработчиков событий OneBot12.
Основное использование
from ErisPulse import sdk
# Регистрация адаптера (обычно выполняется автоматически загрузчиком)
sdk.adapter.register("myplatform", MyPlatformAdapter)
# Запуск всех адаптеров
await sdk.adapter.startup()
# Запуск указанных адаптеров
await sdk.adapter.startup(["myplatform"])
# Запуск всех адаптеров
await sdk.adapter.startup()
# Получение экземпляра адаптера
my_adapter = sdk.adapter.get("myplatform")
# Или доступ через свойство
my_adapter = sdk.adapter.myplatform
# Остановка всех адаптеров
await sdk.adapter.shutdown()
Запуск и остановка
Запуск адаптера
# Запуск всех зарегистрированных адаптеров
await sdk.adapter.startup()
# Запуск указанной платформы
await sdk.adapter.startup(["platform1", "platform2"])
Процесс запуска:
- Отправка события жизненного цикла
adapter.start. - Отправка события
adapter.status.change(starting). - Параллельный запуск каждого адаптера.
- Автоматическая повторная попытка при сбое (стратегия экспоненциальной задержки).
- Отправка события
adapter.status.change(started) после успешного запуска.
Механизм повторных попыток:
- Первые 4 попытки: 60 секунд, 10 минут, 30 минут, 60 минут.
- С пятой попытки и далее: фиксированный интервал в 3 часа.
Остановка адаптера
# Остановка всех адаптеров
await sdk.adapter.shutdown()
Процесс остановки:
- Отправка события жизненного цикла
adapter.stop. - Вызов метода
shutdown()всех адаптеров. - Остановка маршрутизирующего сервера.
- Очистка обработчиков событий.
- Отправка события жизненного цикла
adapter.stopped.
Управление конфигурацией
Проверка статуса платформы
# Проверка, зарегистрирована ли платформа
exists = sdk.adapter.exists("myplatform")
# Проверка, включена ли платформа
enabled = sdk.adapter.is_enabled("myplatform")
# Использование оператора in
if "myplatform" in sdk.adapter:
print("Платформа существует и включена")
Список платформ
# Получение списка всех зарегистрированных платформ
platforms = sdk.adapter.list_registered()
# Получение списка всех платформ и их статусов
status_dict = sdk.adapter.list_items()
# Возвращает: {"platform1": true, "platform2": false, ...}
# Получение списка включённых платформ
enabled_platforms = [p for p, enabled in status_dict.items() if enabled]
Слушатели событий
Стандартные события OneBot12
from ErisPulse import sdk
# Слушатель всех стандартных событий сообщений по OneBot12
@sdk.adapter.on("message")
async def handle_message(data):
print(f"Получено сообщение OneBot12: {data}")
# Слушатель стандартных событий сообщений для определённой платформы
@sdk.adapter.on("message", platform="myplatform")
async def handle_platform_message(data):
print(f"Получено сообщение myplatform: {data}")
# Слушатель всех событий
@sdk.adapter.on("*")
async def handle_any_event(data):
print(f"Получено событие: {data.get('type')}")
Оригинальные события платформы
# Слушатель оригинальных событий для определённой платформы
@sdk.adapter.on("raw_event_type", raw=True, platform="myplatform")
async def handle_raw_event(data):
print(f"Получено оригинальное событие: {data}")
# Слушатель оригинальных событий для всех платформ (с использованием подстановочного знака)
@sdk.adapter.on("*", raw=True)
async def handle_all_raw_events(data):
print(f"Получено оригинальное событие: {data}")
Механизм распределения событий
При вызове adapter.emit(event_data):
- Обработка промежуточными обработчиками: Сначала выполняются все промежуточные обработчики OneBot12.
- Распределение стандартных событий: Распределяются на соответствующие обработчики стандартных событий OneBot12.
- Распределение оригинальных событий: Если есть оригинальные данные, они распределяются на обработчики оригинальных событий.
Правила сопоставления:
- Точное совпадение:
@sdk.adapter.on("message")сопоставляется только с событиемmessage. - Подстановочный знак:
@sdk.adapter.on("*")сопоставляется со всеми событиями. - Фильтрация по платформе:
platform="myplatform"сопоставляется только с событиями указанной платформы.
Промежуточные обработчики
Добавление промежуточного обработчика
@sdk.adapter.middleware
async def logging_middleware(data):
"""Промежуточный обработчик для логирования"""
print(f"Обработка события: {data.get('type')}")
return data # Обязательно вернуть данные
@sdk.adapter.middleware
async def filter_middleware(data):
"""Промежуточный обработчик для фильтрации событий"""
# Фильтрация ненужных событий
if data.get("type") == "notice":
return None # При возврате None промежуточная цепочка игнорирует результат, оставляя исходные данные для передачи
return data # Обязательно вернуть данные для продолжения передачи
Соглашение о возвращаемых значениях промежуточного обработчика
| Возвращаемое значение | Поведение |
|---|---|
dict |
Изменение полезной нагрузки события (последующие обработчики получают изменённое событие) |
None |
Пропуск, полезная нагрузка не изменяется (вывод предупреждения — рекомендуется явно return data) |
False |
Отклонение: событие отбрасывается, не поступает в обработчики и не вызывает побочных эффектов |
Отклонение подходит для сценариев "отбрасывания события на уровне", таких как брандмауэр, ограничение скорости, чёрный список (ранее это можно было реализовать только с помощью обработчиков высокого приоритета). При отклонении фреймворк записывает трассировку и запускает хук жизненного цикла adapter.event.blocked (содержит имя промежуточного обработчика middleware, полное событие event, platform / event_type / detail_type), что помогает выяснить, почему событие не было обработано:
@sdk.adapter.middleware
async def rate_limit_middleware(data):
"""Промежуточный обработчик для ограничения скорости"""
if _is_rate_limited(data):
return False # Отклонение: событие отбрасывается
data["rate_marked"] = True
return data
@sdk.lifecycle.on("adapter.event.blocked")
async def on_event_blocked(data):
print(f"Событие отброшено {data['middleware']}: {data['event_type']}")
Порядок выполнения промежуточных обработчиков
Промежуточные обработчики выполняются в порядке их регистрации, последние зарегистрированные обработчики выполняются первыми.
Внимание: если промежуточный обработчик возвращает
None(например, забылиreturn data), фреймворк игнорирует этот результат и сохраняет исходные данные для передачи, при этом выводится предупреждение. Это гарантирует, что ошибка одного промежуточного обработчика не приведёт к прерыванию всей цепочки событий.
# Порядок регистрации
sdk.adapter.middleware(middleware1) # Выполняется последним
sdk.adapter.middleware(middleware2) # Выполняется посередине
sdk.adapter.middleware(middleware3) # Выполняется первым
# Порядок выполнения: middleware3 -> middleware2 -> middleware1
Получение экземпляра адаптера
Метод get()
adapter = sdk.adapter.get("myplatform")
if adapter:
await adapter.Send.To("user", "123").Text("Hello")
Доступ через свойства
# Доступ по имени свойства (регистр не имеет значения)
adapter = sdk.adapter.myplatform
await adapter.Send.To("user", "123").Text("Hello")
BaseAdapter (базовый класс)
Основная структура
from dataclasses import dataclass, field
from ErisPulse.Core import BaseAdapter
from ErisPulse.Core.Bases import BaseConfig, BotAccountConfig
@dataclass
class MyConfig(BaseConfig):
"""Конфигурация адаптера (объявляется, фреймворк управляет автоматически)"""
token: str = field(
default="",
metadata={
"description": {"i18n": "my_adapter.token", "default": "Bot Token"},
"required": True,
"secret": True,
"ui": {"widget": "password", "group": "basic", "order": 1},
},
)
class MyAdapter(BaseAdapter):
ConfigClass = MyConfig # Объявить класс конфигурации
# Не нужно переопределять __init__, фреймворк обрабатывает автоматически:
# - self.sdk, self.logger
# - self.cfg (типобезопасная конфигурация, считывается в реальном времени)
# - self.Send, self.Request
async def start(self):
"""Запуск адаптера (обязательно реализовать)"""
cfg = self.cfg # Автоматически загруженная типобезопасная конфигурация
pass
async def shutdown(self):
"""Остановка адаптера (обязательно реализовать)"""
pass
async def call_api(self, endpoint: str, **params):
"""Вызов API платформы (обязательно реализовать)"""
pass
Управление конфигурацией
Фреймворк предоставляет декларативное управление конфигурацией, определяя структуру конфигурации через dataclass, фреймворк автоматически обрабатывает загрузку, проверку и генерацию шаблонов.
Конфигурация с одним аккаунтом
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BaseConfig
@dataclass
class TelegramConfig(BaseConfig):
token: str = field(default="", metadata={
"description": {"i18n": "telegram.token", "default": "Bot Token"},
"required": True,
"secret": True,
"ui": {"widget": "password", "group": "basic", "order": 1},
})
proxy: str = field(default="", metadata={
"description": {"i18n": "telegram.proxy", "default": "Адрес прокси"},
"ui": {"widget": "text", "group": "advanced", "order": 10},
})
class TelegramAdapter(BaseAdapter):
ConfigClass = TelegramConfig
async def start(self):
cfg = self.cfg # Типобезопасно, считывается в реальном времени
if not cfg.token:
raise ValueError("Не настроен токен")
await self._connect(cfg.token, proxy=cfg.proxy)
Конфигурация с несколькими аккаунтами
Базовый класс BotAccountConfig предоставляет поля enabled и name. Большинство адаптеров могут автоматически получать bot_id из протокола платформы или ответа при входе, в процессе преобразования событий он вставляется в конфигурацию аккаунта.:
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BotAccountConfig
# Большинство адаптеров: bot_id получается автоматически во время выполнения, не нужно конфигурировать
@dataclass
class MyBotConfig(BotAccountConfig):
token: str = field(default="", metadata={
"description": {"i18n": "my_adapter.bot_token", "default": "Токен"},
"required": True,
})
# Если при входе невозможно получить bot_id, можно позволить пользователю ввести его в конфигурации
@dataclass
class YunhuBotConfig(BotAccountConfig):
bot_id: str = field(default="", metadata={
"description": {"i18n": "yunhu.bot_id", "default": "ID бота"},
"required": True,
})
token: str = field(default="", metadata={
"description": {"i18n": "yunhu.token", "default": "Токен"},
"required": True,
})
class MyAdapter(BaseAdapter):
AccountConfigClass = MyBotConfig
async def start(self):
for name, account in self.enabled_accounts.items():
user_id = await self._login(name, account)
await self.emit_meta("connect", user_id)
Соглашение о metadata
Поле metadata служит одновременно для генерации комментариев в TOML и рендеринга форм в WebUI:
metadata = {
"description": str | dict, # Описание поля (поддержка i18n)
"required": bool, # Обязательно ли заполнять (проверка + метка обязательности в WebUI)
"secret": bool, # Является ли чувствительным (в WebUI отображается как ***, в логах маскируется)
"example": bool, # Флаг не сохранения: не записывается в config.toml (исключается из значения по умолчанию и шаблона),
# только рендерится в config.full.example; schema с "example": true меткой,
# конфигурационный гид CLI по умолчанию пропускает; после ручной установки пользователем сохраняется нормально
"min": number, "max": number, # Проверка диапазона значений
"ui": { # Настройки элемента управления (старое имя "webui" по-прежнему совместимо)
"widget": str, # Тип элемента: "text" | "switch" | "select" | "number" | "password"
"group": str, # Группа: "basic" | "advanced" | "connection" и т.д.
"order": int, # Вес сортировки (чем меньше, тем ближе)
"options": list, # Доступные опции для элемента select [{label, value}], label поддерживает i18n
"placeholder": str | dict, # Подсказка в поле ввода (поддержка i18n)
},
"extra": dict, # Дополнительные расширенные поля (прозрачно передаются в schema)
}
Все пользовательские видимые текстовые поля поддерживают i18n, единый формат: {"i18n": "ключ", "default": "текст"},
чистая строка прозрачно передается (обратная совместимость). Поддерживаемые поля i18n:
| Поле | Позиция | Описание |
|---|---|---|
description |
metadata поля | Описание поля |
options[].label |
ui.options |
Метка опции элемента select |
placeholder |
ui.placeholder |
Подсказка в поле ввода |
group_labels |
_schema_meta |
Название раздела (заголовок секции Dashboard) |
При использовании i18n необходимо заранее зарегистрировать ключ перевода в систему i18n (см. документацию по i18n для подробностей).
Примеры description / placeholder / options label:
token: str = field(
default="",
metadata={
"description": {"i18n": "my_adapter.token", "default": "Bot Token"},
"ui": {
"widget": "text",
"placeholder": {"i18n": "my_adapter.token.ph", "default": "Введите токен"},
},
},
)
mode: str = field(
default="a",
metadata={
"description": {"i18n": "my_adapter.mode", "default": "Режим"},
"ui": {
"widget": "select",
"options": [
{"label": {"i18n": "my_adapter.mode.a", "default": "Вариант A"}, "value": "a"},
{"label": "Чистая строка метки", "value": "b"}, # Чистая строка передается как есть
],
},
},
)
Пример group_labels (объявляется после определения класса конфигурации):
MyConfig._schema_meta = {
"group_labels": {
"basic": {"i18n": "my_adapter.group.basic", "default": "Основные настройки"},
"advanced": {"i18n": "my_adapter.group.advanced", "default": "Дополнительные настройки"},
}
}
Метод resolve_config_schema() фреймворка автоматически разрешает все перечисленные выше поля i18n в зависимости от текущего языка;
get_config_schema() прозрачно передает словарь i18n, и интерфейс будет разрешать его самостоятельно.
Автоматическое создание описания полей из docstring (v2.8.0+)
Поле, не объявленное в metadata description, фреймворк автоматически извлекает описание из docstring класса конфигурации в качестве запасного варианта, поддерживает два распространенных стиля (можно использовать вместе):
@dataclass
class MyConfig(BaseConfig):
"""
Конфигурация MyAdapter
:ivar endpoint: Адрес API платформы # Стиль reST
:ivar timeout: Время ожидания в секундах
"""
endpoint: str = "https://api.example.com" # Без metadata description → описание из docstring
timeout: int = 30
# Также поддерживается стиль Google (раздел Attributes):
# Attributes:
# endpoint: Адрес API платформы
Приоритет: metadata description > описание поля из docstring > пусто. Описание в формате i18n-словаря не затрагивается (всегда приоритет).
Вложенные конфигурации (v2.8.0+)
При типе поля вложенный dataclass фреймворк рекурсивно обрабатывает: schema с "type": "table" +
"fields" поддерево (WebUI рендерит как складываемую вложенную группу), TOML шаблон рендерится как [подтаблица] секция,
по умолчанию / заполнение / проверка / разрешение i18n рекурсивно применяются.
@dataclass
class RetryConfig(BaseConfig):
"""Стратегия повтора
:ivar max_retries: Максимальное количество повторных попыток
"""
max_retries: int = 3
backoff: float = 0.5
@dataclass
class MyConfig(BaseConfig):
"""Конфигурация MyAdapter"""
endpoint: str = "https://api.example.com"
retry: RetryConfig = field(default_factory=RetryConfig) # Вложенный конфигурационный раздел
Сгенерированный TOML шаблон:
endpoint = "https://api.example.com"
[retry]
# Максимальное количество повторных попыток
max_retries = 3
backoff = 0.5
Рекомендуется использовать прямой тип аннотации для вложенных типов; строковые аннотации (например, для отложенного вычисления) должны гарантировать, что тип можно разрешить из модуля конфигурационного класса, глобально, по
__qualname__или по имени класса.
Не сохраняемые поля example (v2.8.0+)
gc_interval: int = field(default=300, metadata={"example": True})
Поле с example: True:
- Не записывается в config.toml (шаблон конфигурации адаптера/модуля и значения по умолчанию исключаются, используются значения по умолчанию из кода)
- Отображается только в
config.full.exampleпроекта (для справки, копируются по желанию в config.toml) - В schema помечается как
"example": true(интерфейс может самостоятельно решить стратегию отображения), CLI конфигурационный гид по умолчанию пропускает - После ручной установки пользователем сохраняется нормально, нормально обновляется (приоритет явного намерения пользователя)
Подходит для "многочисленных и редко используемых" настроек, чтобы сохранить config.toml пользователя минимальным.
⚠️
_schema_meta— это метаданные на уровне класса (не поле конфигурации). Если объявить внутри тела dataclass класса, необходимо добавить аннотациюClassVar(_schema_meta: ClassVar[dict] = {...}), иначе dataclass будет воспринимать это как обычное поле. Фреймворк защитно исключает поля с подчеркиванием в начале (не включает в любой schema / шаблон / значения по умолчанию / проверки), но рекомендуется соблюдать стандартное объявление.
Декларативные ключи перевода (v2.7.0+)
Адаптер может декларировать ключи перевода, подобно ConfigClass, с помощью вложенного класса I18nClass, фреймворк автоматически зарегистрирует все объявленные ключи перевода в __init__ этапе (до генерации шаблона конфигурации), обеспечивая доступность ключей перевода в описаниях конфигурации при генерации шаблона.
from ErisPulse.Core.Bases import BaseAdapter, BaseI18n, I18nKey
class MyAdapter(BaseAdapter):
class I18nClass(BaseI18n):
endpoint: I18nKey = I18nKey(
default="API Endpoint",
zh_CN="API 地址",
zh_TW="API 位址",
en="API Endpoint",
ja="APIアドレス",
ru="API адрес",
)
token: I18nKey = I18nKey(
default="Platform Token",
zh_CN="平台 Token",
zh_TW="平台權杖",
en="Platform Token",
ja="プラットフォームトークン",
ru="Токен платформы",
)
I18nKey.default— это языкозависимый запасной текст, не регистрируется ни в каком языке. Чтобы перевод был активен, необходимо явно передать хотя бы один параметр языка.
Подробное использование (правила путей ключей, явный параметр key и т.д.) см. в документации по i18n.
Декларативные расширения событий (v2.7.0+)
Адаптер может декларировать платформенно-специфич. методы расширения событий через EventMixin, фреймворк автоматически регистрирует их для текущей платформы.
from ErisPulse.Core import BaseAdapter
class MyAdapter(BaseAdapter):
class EventMixin:
def get_chat_name(self):
"""Получить название чата"""
return self.get("myplatform_raw", {}).get("chat", {}).get("name", "")
def is_official_message(self):
"""Проверить, является ли сообщение официальным"""
raw = self.get("myplatform_raw", {})
return raw.get("sender", {}).get("is_official", False)
После регистрации, методы расширения событий доступны напрямую в объекте события:
@message.on_group_message()
async def handler(event):
if event.is_official_message():
chat_name = event.get_chat_name()
await event.reply(f"[{chat_name}] Официальное сообщение получено")
Методы расширения событий адаптера регистрируются в собственной платформе (
self._platform). Модули, которые нуждаются в расширении событий на разных платформах, должны использовать старый APIregister_event_mixin().
Разрешение аккаунта
Многоаккаунтный адаптер может использовать _resolve_account() для автоматического разрешения целевого аккаунта:
async def call_api(self, endpoint: str, **params):
account_id = params.pop("account_id", None)
name, account = self._resolve_account(account_id)
# name: имя аккаунта, account: экземпляр конфигурации
Стратегия разрешения: сопоставление по имени аккаунта → сопоставление по полю bot_id → сопоставление по другим строковым полям → первый включенный аккаунт.
Горячая перезагрузка конфигурации
Подкласс может переопределить on_config_update() для реакции на изменения конфигурации:
class MyAdapter(BaseAdapter):
ConfigClass = MyConfig
def on_config_update(self, old_config, new_config):
if old_config.token != new_config.token:
self.logger.info("Токен обновлен, переподключение")
Процесс инициализации
Фреймворк автоматически выполняет следующие действия в BaseAdapter.__init__(self, sdk=None):
- Ссылка на SDK: установка
self.sdk,self.logger - Фабрика Send/Request: создание
self.Sendиself.Request - Шаблон конфигурации: если объявлен
ConfigClass, автоматически генерируется шаблон конфигурации (впервые) - Шаблон аккаунта: если объявлен
AccountConfigClass, автоматически генерируется шаблон аккаунта (впервые) - Регистрация EventMixin: если объявлен
EventMixin, автоматически регистрируется вAdapterManagerпосле вставки имени платформы
Конфигурация считывается в реальном времени через self.cfg / self.accounts (каждый доступ читает последнее значение из хранилища конфигурации). self.config как совместимый псевдоним self.cfg по-прежнему доступен.
Большинству адаптеров не нужно переопределять __init__. Если требуется кастомная инициализация:
class MyAdapter(BaseAdapter):
ConfigClass = MyConfig
def __init__(self, sdk=None):
super().__init__(sdk) # передать sdk
self.converter = self._setup_converter()
self.convert = self.converter.convert
DSL для отправки сообщений Send
Наследование
class MyAdapter(BaseAdapter):
class Send(BaseAdapter.Send):
"""Вложенный класс Send, наследующий BaseAdapter.Send"""
pass
Доступные свойства
При вызове Send автоматически устанавливаются следующие свойства:
| Свойство | Описание | Способ установки |
|---|---|---|
_target_id |
Идентификатор цели | To(id) или To(type, id) |
_target_type |
Тип цели | To(type, id) |
_target_to |
Упрощенный идентификатор цели | To(id) |
_account_id |
Идентификатор отправляемого аккаунта | Using(account_id) |
_adapter |
Экземпляр адаптера | Автоматически установлен |
_at_user_ids |
Список пользователей для упоминания | At(user_id) |
_reply_message_id |
Идентификатор сообщения для ответа | Reply(message_id) |
_at_all |
Упоминание всех пользователей | AtAll() |
Рекомендуется: использовать свойство
self.send_contextдля полученияtarget_type,target_id,account_idза один раз, что делает код более понятным, чем прямой доступ к экземплярным переменным.
Вспомогательные методы фреймворка
| Метод/свойство | Описание |
|---|---|
self._apply_modifiers(message) |
Объединение состояний модификаторов At/AtAll/Reply в список сообщений |
self.send_context |
Возвращает словарь {target_type, target_id, account_id} |
Основные методы
Адаптер должен реализовать только Raw_ob12, стандартные методы (Text/Image/Voice/Video/File) уже унаследованы от базового класса SendDSL и по умолчанию делегированы ему:
class Send(BaseAdapter.Send):
def Raw_ob12(self, message, **kwargs):
"""Обязательно реализовать: OneBot12 сообщения → платформенный API"""
async def _do_send():
segments = self._apply_modifiers(message)
return await self._adapter.call_api(
endpoint="/send_message",
message=segments,
**self.send_context,
**kwargs
)
return asyncio.create_task(_do_send())
# Text/Image/Voice/Video/File уже унаследованы от базового класса, автоматически делегируют Raw_ob12, не нужно повторно реализовывать
# Если нужна платформенно-специфич. логика, можно переопределить отдельный метод:
# def Text(self, text: str):
# return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
Цепочечные модифицирующие методы
class Send(BaseAdapter.Send):
def __init__(self, adapter, target_type=None, target_id=None, account_id=None):
super().__init__(adapter, target_type, target_id, account_id)
self.buttons = []
def Button(self, content: list) -> 'Send':
self.buttons.append(content)
return self
Конвертер событий
Процесс преобразования
Платформенно-специфич. событие
↓
Converter.convert()
↓
Стандартное событие OneBot12
Обязательные поля
Все преобразованные события должны содержать:
{
"id": "Уникальный идентификатор события",
"time": 1234567890, # 10-значный Unix timestamp
"type": "message/notice/request/meta",
"detail_type": "Детальный тип события",
"platform": "Название платформы",
"self": {
"platform": "Название платформы",
"user_id": "ID бота" # Должен совпадать с bot_id
},
"{platform}_raw": {...}, # Оригинальные данные (обязательно)
"{platform}_raw_type": "..." # Тип оригинального события (обязательно)
}
Пример конвертера
class MyPlatformConverter:
def convert(self, raw_event):
"""Преобразование платформенно-специфич. события в стандартный формат OneBot12"""
if not isinstance(raw_event, dict):
return None
# Генерация идентификатора события
event_id = raw_event.get("event_id") or str(uuid.uuid4())
# Преобразование временной метки
timestamp = raw_event.get("timestamp")
if timestamp and timestamp > 10**12:
timestamp = int(timestamp / 1000)
else:
timestamp = int(timestamp) if timestamp else int(time.time())
# Преобразование типа события
event_type = self._convert_type(raw_event.get("type"))
detail_type = self._convert_detail_type(raw_event)
# Построение стандартного события
onebot_event = {
"id": str(event_id),
"time": timestamp,
"type": event_type,
"detail_type": detail_type,
"platform": "myplatform",
"self": {
"platform": "myplatform",
"user_id": str(raw_event.get("bot_id", ""))
},
"myplatform_raw": raw_event,
"myplatform_raw_type": raw_event.get("type", "")
}
return onebot_event
Управление подключениями
WebSocket подключение
class MyAdapter(BaseAdapter):
async def start(self):
"""Регистрация WebSocket маршрута"""
router.register_websocket(
module_name="myplatform",
path="/ws",
handler=self._ws_handler,
auth_handler=self._auth_handler
)
async def _ws_handler(self, websocket):
"""Обработчик WebSocket подключения"""
self.connection = websocket
try:
while True:
data = await websocket.receive_text()
onebot_event = self.convert(data)
if onebot_event:
await self.adapter.emit(onebot_event)
except WebSocketDisconnect:
self.logger.info("Подключение разорвано")
finally:
self.connection = None
async def _auth_handler(self, websocket) -> bool:
"""Проверка подключения"""
token = websocket.query_params.get("token")
return token == "valid_token"
WebHook подключение
class MyAdapter(BaseAdapter):
async def start(self):
"""Регистрация HTTP маршрута"""
router.register_http_route(
module_name="myplatform",
path="/webhook",
handler=self._webhook_handler,
methods=["POST"]
)
async def _webhook_handler(self, request):
"""Обработчик WebHook запроса"""
data = await request.json()
onebot_event = self.convert(data)
if onebot_event:
await self.adapter.emit(onebot_event)
return {"status": "ok"}
Информация о маршрутах: зарегистрированные адаптером маршруты (HTTP, WebSocket, SSE) можно получить через
sdk.adapter.get_connection_info(platform)иsdk.router.get_module_urls(module_name), включаяbase_url+ путь. Подробнее см. Введение в разработку адаптеров - Информация о подключениях и обнаружение маршрутов и Поддержка SSE.
Стандартные ответы API
Фреймворк предоставляет методы make_response() и make_error() для построения стандартизированных ответов, не нужно вручную создавать словарь ответа.
Успешный ответ
async def call_api(self, endpoint: str, **params):
try:
raw_response = await self._platform_api_call(endpoint, **params)
return self.make_response(
data=raw_response.get("data"),
message_id=raw_response.get("data", {}).get("message_id", ""),
raw=raw_response,
)
except Exception as e:
return self.make_error(message=str(e), raw=None)
Ручное построение ответа (старый способ по-прежнему совместим)
async def call_api(self, endpoint: str, **params):
return {
"status": "ok",
"retcode": 0,
"data": {...},
"message_id": "msg_id",
"message": "",
"myplatform_raw": raw_response
}
Поддержка нескольких аккаунтов
Декларативная конфигурация (рекомендуется)
После объявления AccountConfigClass фреймворк автоматически управляет загрузкой, проверкой и генерацией шаблонов конфигурации нескольких аккаунтов:
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BotAccountConfig
@dataclass
class MyBotConfig(BotAccountConfig):
bot_id: str = field(default="", metadata={"description": "ID бота", "required": True})
token: str = field(default="", metadata={"description": "Токен", "required": True, "secret": True})
class MyAdapter(BaseAdapter):
AccountConfigClass = MyBotConfig
async def start(self):
for name, account in self.enabled_accounts.items():
self.logger.info(f"Запуск аккаунта {name}: {account.bot_id}")
await self._connect(name, account)
async def call_api(self, endpoint: str, **params):
account_id = params.pop("account_id", None)
name, account = self._resolve_account(account_id)
# Использовать account.token, account.bot_id и т.д.
Файл конфигурации аккаунта
[MyAdapter.accounts.account1]
bot_id = "bot_001"
token = "token1"
enabled = true
[MyAdapter.accounts.account2]
bot_id = "bot_002"
token = "token2"
enabled = true
Отправка сообщения с указанием аккаунта
# Использовать метод Using для указания аккаунта
my_adapter = adapter.get("myplatform")
# Рекомендуется использовать self.user_id из события
await my_adapter.Send.Using(event["self"]["user_id"]).To("user", "123").Text("Hello")
# По имени аккаунта
await my_adapter.Send.Using("account1").To("user", "123").Text("Hello")
Соотношение self.user_id и Using
Механизм ответа фреймворка автоматически извлекает account_id (приоритет) или user_id из поля self события, передавая его как параметр Using. Разработчику адаптера необходимо обеспечить, чтобы Converter правильно устанавливал self.user_id, чтобы Using мог сопоставиться.
Внутренняя логика фреймворка:
# Логика извлечения bot_id фреймворком
bot_id = self.get("self", {}).get("account_id", "") or self.get("self", {}).get("user_id", "")
# Вызов Using только при непустом bot_id
if bot_id:
send_chain = send_chain.Using(bot_id)
Ключевой момент: даже если адаптер использует только одну конфигурацию бота, если Converter правильно устанавливает
self.user_id, фреймворк будет использовать его как параметрUsing. Адаптер должен убедиться, чтоself.user_idсовпадает с идентификатором поля вAccountConfigClass(например,bot_id), чтобы_resolve_account()мог сопоставиться с правильным аккаунтом. Еслиself.user_idпуст, фреймворк не вызоветUsing, иcall_apiполучитaccount_idкакNone,_resolve_account(None)вернёт первый включённый аккаунт.
Обработка ошибок
Повторное подключение
import asyncio
class MyAdapter(BaseAdapter):
async def start(self):
retry_count = 0
max_retries = 5
while retry_count < max_retries:
try:
await self._connect_to_platform()
break
except Exception as e:
retry_count += 1
if retry_count < max_retries:
wait_time = min(60 * (2 ** retry_count), 600)
self.logger.warning(f"Подключение не удалось, повтор через {wait_time} секунд")
await asyncio.sleep(wait_time)
else:
raise
Обработка ошибок API
async def call_api(self, endpoint: str, **params):
try:
# Рекомендуется использовать клиент SDK
from ErisPulse.Core import client
from ErisPulse.Core.Bases.errors import ClientError, ClientTimeoutError
resp = await client.post(
f"https://api.platform.com/{endpoint}",
json=params,
max_retries=2,
)
response = await resp.json()
return self._standardize_response(response)
except ClientTimeoutError:
self.logger.error(f"Запрос таймаут: {endpoint}")
return self._error_response("Запрос таймаут", 32000)
except ClientError as e:
self.logger.error(f"Ошибка сети: {e}")
return self._error_response("Ошибка сети", 33000)
except Exception as e:
self.logger.error(f"Неизвестная ошибка: {e}")
return self._error_response(str(e), 34000)
Обратная совместимость: код старых адаптеров, использующих
aiohttp.ClientSession, не затрагивается, по-прежнему можно перехватыватьaiohttp.ClientError. Два способа могут сосуществовать. Рекомендуется использоватьsdk.client+ систему исключений ErisPulse для нового кода.
Управление состоянием бота
AdapterManager содержит встроенную систему отслеживания состояния ботов, автоматически отслеживает онлайн-статус, время активности и метаинформацию всех зарегистрированных ботов.
Автоматическое обнаружение
При отправке событий через adapter.emit() фреймворк автоматически проверяет поле self событий:
- Мета-события: в зависимости от
detail_typeвыполняются соответствующие действия (connect — регистрация, disconnect — пометка как отключённого, heartbeat — обновление времени активности) - Обычные события (message/notice/request): автоматически обнаруживаются боты и обновляется время активности
# Все события с полем self будут запускать автоматическое обнаружение
await self.adapter.emit({
"type": "message",
"platform": "myplatform",
"self": {"platform": "myplatform", "user_id": "bot123"},
# ...
})
# Бот "bot123" будет автоматически зарегистрирован (если это первый раз) и обновлено время активности
Типы мета-событий
detail_type |
Описание | Действие фреймворка |
|---|---|---|
connect |
Бот подключился | Регистрация бота и запуск события жизненного цикла adapter.bot.online |
disconnect |
Бот отключился | Пометка бота как отключённого и запуск события жизненного цикла adapter.bot.offline |
heartbeat |
Бот отправил heartbeat | Обновление времени активности и метаинформации бота |
Отправка мета-событий адаптером
Использование emit_meta() для отправки мета-событий:
class MyAdapter(BaseAdapter):
async def _on_bot_connect(self, bot_id: str):
# Одним вызовом отправить событие connect
await self.emit_meta("connect", bot_id, user_name="MyBot", nickname="Мой бот")
async def _on_bot_disconnect(self, bot_id: str):
await self.emit_meta("disconnect", bot_id)
Также поддерживается ручное построение (старый способ по-прежнему совместим):
await self.adapter.emit({
"type": "meta",
"detail_type": "connect",
"platform": "myplatform",
"self": {"platform": "myplatform", "user_id": bot_id}
})
Расширение поля self
Поле self помимо обязательных platform и user_id поддерживает следующие необязательные поля:
| Поле | Описание |
|---|---|
user_name |
Имя пользователя бота |
nickname |
Никнейм бота |
avatar |
URL аватара бота |
account_id |
Идентификатор аккаунта |
Запрос состояния бота
from ErisPulse import sdk
# Получить информацию о боте
info = sdk.adapter.get_bot_info("myplatform", "bot123")
# {"status": "online", "last_active": 1712345678.0, "info": {"nickname": "MyBot"}}
# Список всех ботов
all_bots = sdk.adapter.list_bots()
# Список ботов указанной платформы
platform_bots = sdk.adapter.list_bots("myplatform")
# Проверить, онлайн ли бот
is_online = sdk.adapter.is_bot_online("myplatform", "bot123")
# Получить полную сводку состояния (подходит для отображения в WebUI)
summary = sdk.adapter.get_status_summary()
# {"adapters": {"myplatform": {"status": "started", "bots": {...}}}}
Отслеживание жизненного цикла бота
from ErisPulse import sdk
@sdk.lifecycle.on("adapter.bot.online")
async def on_bot_online(data):
platform = data.get("platform")
bot_id = data.get("bot_id")
sdk.logger.info(f"Бот онлайн: {platform}/{bot_id}")
@sdk.lifecycle.on("adapter.bot.offline")
async def on_bot_offline(data):
platform = data.get("platform")
bot_id = data.get("bot_id")
sdk.logger.info(f"Бот оффлайн: {platform}/{bot_id}")
Связанные документы
- Введение в разработку адаптеров - Создание первого адаптера
- SendDSL подробно - Изучение отправки сообщений
- Лучшие практики разработки адаптеров - Разработка высококачественных адаптеров