name: erispulse-adapter-dev description: "适用于:要把一个聊天平台接入 ErisPulse:协议转换、发送规范、发布流程。覆盖场景:стартовая (обязательная к прочтению) статья цикла разработки адаптеров. Здесь описано, как написать адаптер с нуля, каковы границы ответственности адаптера и структура проекта; после прочтения переходите по необходимости к разделам Конвертер событий и Подробное описание SendDSL.。Когда стоит читать этот документ: обязательный материал на пути разработки адаптеров (полное описание ключевых механизмов: BaseAdapter / SendDSL / жизненный цикл и др.). Если вам нужно лишь начать работу, сначала прочитайте Введение в разработку адаптеров, а к этому документу возвращайтесь при возникновении вопросов уровня «почему».。Когда это нужно прочитать: обязательно к прочтению в рамках пути разработки адаптеров (DSL отправки). Если это не требуется — пропустите, на использование остальных функций это не влияет.。Когда нужно прочитать эту статью: Обязательно для чтения при разработке адаптера (конвертер событий). Если н..."
ErisPulse 适配器开发
This skill is auto-built from the ErisPulse documentation. Repository: https://github.com/ErisPulse/ErisPulse Source paths are annotated per section; see docs/ in the repository for the latest content.
内容大纲
AI 按需读取:只读需要的章节即可,不必全文加载。
- 适配器开发入门: 87-96 行
- Введение в адаптеры: 98-121 行
- Структура каталогов: 123-136 行
- Быстрый старт: 138-469 行
- Локальная разработка (без упаковки): 471-503 行
- Объявление зависимостей (опционально, 2.8.0+): 505-539 行
- Замечания по
__init__: 541-738 行 - Что дальше: 740-751 行
- 适配器核心概念: 753-762 行
- Архитектура адаптера: 764-803 行
- AdapterManager адаптер-менеджер: 805-1038 行
- BaseAdapter (базовый класс): 1040-1423 行
- DSL для отправки сообщений Send: 1425-1496 行
- Конвертер событий: 1498-1569 行
- Управление соединениями: 1571-1741 行
- Стандартные ответы API: 1743-1775 行
- Поддержка нескольких аккаунтов: 1777-1848 行
- Обработка ошибок: 1850-1902 行
- Управление состоянием ботов: 1904-1955 行
- Связанная документация: 1957-1966 行
- 发送 DSL: 1968-1978 行
- Базовые способы вызова: 1980-2004 行
- Цепочка методов: 2006-2014 行
- Методы отправки: 2016-2064 行
- Модифицирующие методы: 2066-2130 行
- Использование модифицирующих методов в классе-обёртке Event: 2132-2194 行
- Управление аккаунтами: 2196-2221 行
- Асинхронная обработка: 2223-2246 行
- Система правил отправки: 2248-2371 行
- Режим пакетного построения: 2373-2496 行
- Правила именования: 2498-2543 行
- Внутреннее устройство цепочки отправки: 2545-2603 行
- Возвращаемые значения: 2605-2660 行
- Полные примеры: 2662-2698 行
- Связанная документация: 2700-2710 行
- 转换器: 2712-2721 行
- Ответственность Converter: 2723-2735 行
- Базовый класс BaseConverter (рекомендуется): 2737-2783 行
- Метод convert(): 2785-2825 行
- Обязательные поля: 2827-2853 行
- Преобразование сегментов сообщений: 2855-2885 行
- Расширения платформы: 2887-2908 行
- Полный пример: 2910-2990 行
- Пример преобразования мультимедийного сообщения: 2992-3039 行
- Common Pitfalls: 3041-3083 行
- Рекомендуемые практики: 3085-3091 行
- Связанные документы: 3093-3103 行
- 适配器最佳实践: 3105-3115 行
- Управление состоянием ботов и Meta-события: 3117-3214 行
- Управление подключением: 3216-3331 行
- Преобразование событий: 3333-3385 行
- Реализация SendDSL: 3387-3448 行
- Ответы API: 3450-3499 行
- Поддержка нескольких аккаунтов: 3501-3561 行
- Обработка ошибок: 3563-3615 行
- Тестирование: 3617-3661 行
- Обратное преобразование и построение сообщений: 3663-3671 行
- Расширение методами событий платформы: 3673-3725 行
- Поддержка документации: 3727-3734 行
- Основная информация: 3736-3738 行
- Поддерживаемые типы отправляемых сообщений: 3740-3741 行
- Специфичные типы событий: 3743-3744 行
- Параметры конфигурации: 3746-3757 行
- Связанная документация: 3759-3769 行
- 平台接入指南索引: 3771-3788 行
- Платформо-специфичные функции: 3790-3813 行
- Общие интерфейсы: 3815-3993 行
- Стандартный формат: 3995-4049 行
- Ссылки: 4051-4061 行
- Участие в разработке: 4063-4072 行
适配器开发入门
来源:
docs/ru/developer-guide/adapters/getting-started.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/developer-guide/adapters/getting-started.md)
Введение в разработку адаптеров
Tip
Когда читать этот документ: стартовая (обязательная к прочтению) статья цикла разработки адаптеров. Здесь описано, как написать адаптер с нуля, каковы границы ответственности адаптера и структура проекта; после прочтения переходите по необходимости к разделам Конвертер событий и Подробное описание SendDSL.
Пройдя это руководство, вы получите минимально работоспособный адаптер, способный принимать события и отправлять сообщения (главный класс + конвертер + Send), и будете чётко понимать границы ответственности каждого компонента — вся работа по подключению новой платформы начинается именно отсюда.
Введение в адаптеры
Что такое адаптер
Адаптер — это мост между ErisPulse и различными платформами обмена сообщениями, отвечающий за:
- Прямое преобразование: приём событий платформы и преобразование их в стандартный формат OneBot12 (Converter)
- Обратное преобразование: преобразование сегментов сообщений OneBot12 в вызовы API платформы (
Raw_ob12) - Управление соединением с платформой (WebSocket/WebHook)
- Предоставление унифицированного интерфейса отправки сообщений SendDSL
Архитектура адаптера
flowchart LR
subgraph receive["Прямое преобразование (приём)"]
direction TB
P1["Событие платформы"] --> C1["Converter.convert()"] --> O1["Стандартное событие OneBot12"] --> S1["Система событий"] --> M1["Обработка модулем"]
end
subgraph send["Обратное преобразование (отправка)"]
direction TB
M2["Модуль формирует сообщение"] --> R1["Send.Raw_ob12()"] --> N1["Вызов нативного API платформы"] --> R2["Стандартный формат ответа"]
end
Структура каталогов
Стандартная структура пакета адаптера:
MyAdapter/
├── pyproject.toml # Конфигурация проекта
├── README.md # Описание проекта
├── LICENSE # Лицензия
└── MyAdapter/
├── __init__.py # Точка входа пакета
├── Core.py # Главный класс адаптера
└── Converter.py # Конвертер событий
Быстрый старт
1. Создание проекта
Рекомендуется сразу сгенерировать полный каркас с помощью скаффолда (включая pyproject, главный класс и конвертер):
# Стандартный проект (для упаковки и публикации)
epsdk create adapter -n MyAdapter
# Локальный плагин-адаптер (без упаковки; достаточно поместить в каталог plugins/ для автозагрузки, см. раздел «Локальная разработка» ниже)
epsdk create adapter -n MyAdapter --local
Скаффолд по умолчанию генерирует каркас WebSocket-сервера; если платформа использует другой способ подключения, можно выбрать шаблон режима связи через --template
(используются соответствующие базовые классы-заготовки, start/shutdown писать самому не нужно):
epsdk create adapter -n MyAdapter --template polling # HTTP-опрос (pull через getUpdates)
epsdk create adapter -n MyAdapter --template webhook # HTTP-колбэк (push со стороны платформы)
Различия трёх режимов подключения и минимальный набор реализаций для базовых классов-заготовок описаны в разделе Основные концепции адаптеров — Базовые классы-заготовки режимов подключения. Полные рабочие примеры смотрите в репозитории:
examples/example-adapter-polling/иexamples/example-adapter-webhook/.
Можно также создать всё вручную:
mkdir MyAdapter && cd MyAdapter
2. Создание pyproject.toml
[project]
name = "ErisPulse-MyAdapter"
version = "1.0.0"
description = "Адаптер для платформы MyAdapter"
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [ { name = "yourname", email = "[email protected]" } ]
dependencies = [
"ErisPulse>=2.4.0" # ErisPulse уже включает aiohttp, отдельная зависимость обычно не нужна
]
[project.urls]
"homepage" = "https://github.com/yourname/MyAdapter"
[project.entry-points."erispulse.adapter"]
"MyAdapter" = "MyAdapter:MyAdapter"
3. Создание главного класса адаптера
Фреймворк предоставляет декларативное управление конфигурацией через ConfigClass / AccountConfigClass; адаптеру достаточно объявить класс конфигурации, и загрузка, валидация и генерация шаблона конфигурации выполнятся автоматически.
# MyAdapter/Core.py
from dataclasses import dataclass, field
# Рекомендуется импорт из корня (2.10+): часто используемые символы импортируются напрямую из корневого пакета, глубокие пути остаются совместимыми
from ErisPulse import BaseAdapter, BaseConfig
@dataclass
class MyAdapterConfig(BaseConfig):
"""Конфигурация MyAdapter"""
api_endpoint: str = field(
default="https://api.example.com",
metadata={
"description": {"i18n": "my_adapter.api_endpoint", "default": "Адрес API"},
"required": False,
"ui": {"widget": "text", "group": "connection", "order": 1},
},
)
token: str = field(
default="",
metadata={
"description": {"i18n": "my_adapter.token", "default": "Token платформы"},
"required": True,
"secret": True,
"ui": {"widget": "password", "group": "basic", "order": 2},
},
)
class MyAdapter(BaseAdapter):
ConfigClass = MyAdapterConfig # Объявляем класс конфигурации, фреймворк управляет им автоматически
# Не нужно переопределять __init__! Фреймворк автоматически:
# - устанавливает self.sdk / self.logger
# - self.cfg читает конфигурацию в реальном времени
# - автоматически инициализирует self.Send / self.Request
def _setup_converter(self):
from .Converter import MyPlatformConverter
return MyPlatformConverter()
⚠️ О
__init__: в новых версияхBaseAdapter.__init__(self, sdk=None)автоматически обрабатывает ссылку на SDK, инициализацию логирования и загрузку конфигурации. Большинству адаптеров больше не нужно переопределять__init__. Подробнее см. Замечания по__init__.
⚠️ О
super().__init__():BaseAdapter.__init__()отвечает за создание фабрикSendиRequest. Если забыть его вызвать, все операции отправки сообщений и запросов будут завершатьсяAttributeError. Подробнее см. Замечания по__init__.
4. Реализация обязательных методов
class MyAdapter(BaseAdapter):
# ... код __init__ ...
async def start(self):
"""Запуск адаптера (обязателен к реализации)"""
# Регистрируем маршруты WebSocket или WebHook
router.register_websocket(
module_name="myplatform",
path="/ws",
handler=self._ws_handler
)
self.logger.info("Адаптер запущен")
async def shutdown(self):
"""Остановка адаптера (обязательна к реализации)"""
router.unregister_websocket(
module_name="myplatform",
path="/ws"
)
# Очищаем соединения и ресурсы
self.logger.info("Адаптер остановлен")
async def call_api(self, endpoint: str, **params):
"""Вызов API платформы (обязателен к реализации)"""
raise NotImplementedError("Необходимо реализовать call_api")
Проактивная отправка Meta-событий
Адаптер должен проактивно отправлять meta-события, чтобы фреймворк мог отслеживать состояние бота (в сети/не в сети). С помощью emit_meta() это делается одной строкой:
class MyAdapter(BaseAdapter):
async def _ws_handler(self, websocket):
bot_id = self._get_bot_id()
# Бот в сети
await self.emit_meta("connect", bot_id, user_name="MyBot")
try:
while True:
data = await websocket.receive_text()
event = self.convert(data)
if event:
await self.adapter.emit(event)
except WebSocketDisconnect:
pass
finally:
# Бот отключился
await self.emit_meta("disconnect", bot_id)
Подробное описание управления состоянием бота и Meta-событий см. в разделе Лучшие практики адаптеров — Управление состоянием бота.
5. Реализация класса Send
Модификаторы At/AtAll/Reply уже встроены в базовый класс SendDSL фреймворка; адаптеру достаточно реализовать Raw_ob12 и конкретные методы отправки.
Фреймворк предоставляет два ключевых вспомогательных метода:
self._apply_modifiers(message)— автоматически объединяет модификаторы At/AtAll/Reply в сегменты сообщенияself.send_context— получение словаря контекста отправки (target_type,target_id,account_id)
import asyncio
class MyAdapter(BaseAdapter):
# ... остальной код ...
class Send(BaseAdapter.Send):
def Raw_ob12(self, message, **kwargs):
"""
Отправка сообщения в формате OneBot12 (обязательна к реализации)
Использует _apply_modifiers для автоматического объединения
состояния модификаторов, а send_context — для получения
контекста отправки.
"""
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 наследуются от базового класса SendDSL,
# по умолчанию они делегируют Raw_ob12, повторная реализация не требуется.
# При необходимости платформо-специфичной логики можно переопределить отдельный метод:
# def Text(self, text: str):
# return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
Ключевые моменты реализации методов отправки медиафайлов (Image/Video/File):
- Реализация по умолчанию в базовом классе оборачивает параметр
fileв сегмент сообщения OneBot12 и передаёт его вRaw_ob12; адаптер должен обрабатывать скачивание/загрузку внутриRaw_ob12 - Параметр
fileдолжен поддерживать как бинарные данныеbytes, так и URL-строкиstr - При передаче URL нужно сначала скачать файл, а затем загрузить его на платформу
- Платформам обычно требуется сначала вызвать API загрузки для получения идентификатора файла, а затем вызвать API отправки
Магический метод __getattr__:
- Обеспечивает регистронезависимость имён методов (можно вызывать
Text,text,TEXT) - Неопределённые методы должны возвращать информационное сообщение, а не вызывать ошибку
Метод Raw_ob12:
- Преобразует стандартный формат сообщений OneBot12 в формат платформы и отправляет
- Использует
self._apply_modifiers(message)для автоматической обработки модификаторов At/AtAll/Reply - Использует
**self.send_contextдля передачи информации о цели отправки и аккаунте
6. Реализация конвертера
# MyAdapter/Converter.py
import time
import uuid
class MyPlatformConverter:
def convert(self, raw_event):
"""Преобразует нативное событие платформы в стандартный формат OneBot12"""
if not isinstance(raw_event, dict):
return None
onebot_event = {
"id": str(raw_event.get("event_id", uuid.uuid4())),
"time": int(time.time()),
"type": self._convert_event_type(raw_event.get("type")),
"detail_type": self._convert_detail_type(raw_event),
"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
def _convert_event_type(self, event_type):
"""Преобразует тип события"""
type_map = {
"message": "message",
"notice": "notice"
}
return type_map.get(event_type, "unknown")
def _convert_detail_type(self, raw_event):
"""Преобразует детальный тип"""
return "private" # упрощённый пример
7. Реализация класса Request (операции с запросами)
Если ваша платформа поддерживает запросы, требующие решения бота (заявки в друзья, приглашения в группы и т. п.), можно реализовать внутренний класс Request:
from ErisPulse import BaseAdapter, RequestDSL
class MyAdapter(BaseAdapter):
# ... Send и остальной код ...
class Request(RequestDSL):
"""Реализация операций с запросами (заявки в друзья, приглашения в группы и т. д.)"""
def accept(self, **kwargs):
"""Принять запрос"""
async def _do():
result = await self._adapter.call_api(
endpoint="/set_request",
request_id=self._request_id,
approve=True,
**kwargs,
)
return {
"status": "ok" if result.get("code") == 0 else "failed",
"retcode": result.get("code", 0),
"data": None,
"message_id": "",
"message": result.get("message", ""),
}
return self._create_task(_do())
def reject(self, **kwargs):
"""Отклонить запрос"""
async def _do():
result = await self._adapter.call_api(
endpoint="/set_request",
request_id=self._request_id,
approve=False,
**kwargs,
)
return {
"status": "ok" if result.get("code") == 0 else "failed",
"retcode": result.get("code", 0),
"data": None,
"message_id": "",
"message": result.get("message", ""),
}
return self._create_task(_do())
Как этим пользуются разработчики модулей:
from ErisPulse import request
@request.on_friend_request()
async def handle_friend_request(event):
# Через удобные методы Event
await event.approve()
# Или напрямую через адаптер
await adapter.myplatform.Request("req_id").accept()
Если платформа не поддерживает операции с запросами, внутренний класс
Requestможно не реализовывать. Базовый класс по умолчанию возвращаетretcode=10002(неподдерживаемая операция). Подробнее см. Спецификация операций с запросами.
8. Создание точки входа пакета
# MyAdapter/__init__.py
from .Core import MyAdapter
Локальная разработка (без упаковки)
Как и модули, адаптеры можно разрабатывать напрямую в локальном каталоге плагинов проекта (по умолчанию plugins/, настраивается через ErisPulse.framework.plugins_dir), без pip install -e .:
# Сгенерировать локальный плагин-адаптер
epsdk create adapter -n MyAdapter --local
# Сгенерированная структура:
# plugins/
# └── MyAdapter/
# ├── __init__.py
# ├── Core.py
# └── Converter.py
При запуске фреймворк автоматически сканирует каталог плагинов и загружает находящиеся в нём модули и адаптеры (с теми же правами, что и пакеты, установленные через entry-point: состояние включения, объявление конфигурации, горячее обновление и очистка при удалении полностью идентичны):
- Правила распознавания: подходит как каталог пакета (с
__init__.py), так и одиночный файл.py; в первую очередь распознаётся класс с именемMain, иначе берётся первый подклассBaseAdapter/BaseModuleв плагине - Переопределение при совпадении имён: если локальный плагин и адаптер, установленный с PyPI, имеют одинаковое имя, приоритет у локального плагина (удобно для отладки с переопределением)
- Горячая перезагрузка: мониторинг каталога плагинов, включаемый через
sdk.enable_plugin_hot_reload(), также распространяется на адаптеры; горячая перезагрузка кода адаптера на уровне кода доступна с версии 2.11 —await sdk.adapter.reload("имя платформы")илиawait sdk.reload_adapter("имя платформы")(предварительная проверка синтаксиса → очистка при выгрузке → очистка кэша и повторный импорт → повторная регистрация и запуск; при сбое выполняется автоматический откат; подробнее см. Обзор архитектуры)
От локального плагина к публикации
После локальной проверки с помощью подкоманды pack можно в один клик преобразовать плагин в готовый к публикации стандартный проект (интерактивный выбор плагина, заполнение метаданных, генерация pyproject с entry-point):
epsdk create pack
# Выберите плагин из каталога плагинов → введите название/описание/автора проекта → будет сгенерирован каталог проекта MyAdapter/
cd MyAdapter
pip install -e . # Преобразование в стандартную установку; затем следуйте процессу публикации для загрузки на PyPI
Подсказка: после преобразования рекомендуется удалить исходную копию из
plugins/, чтобы избежать двойной загрузки локального плагина и установленного пакета (при совпадении имён приоритет у локального, из-за чего можно ошибочно решить, что работает установленный пакет).
Объявление зависимостей (опционально, 2.8.0+)
Адаптер может объявлять зависимости от других адаптеров или модулей, обеспечивая взаимодействие между адаптерами и опциональные функции:
from typing import ClassVar
class MyAdapter(BaseAdapter):
# Жёсткие зависимости: при их отсутствии запуск пропускается (предупреждение + событие status=skipped-dependency)
depends: ClassVar[dict] = {
"adapters": ["onebot11"], # Зависимости-адаптеры (по имени платформы)
"modules": ["TranslateEngine"], # Зависимости-модули (по имени регистрации)
}
# Мягкие зависимости: их отсутствие не влияет на запуск; при загрузке/выгрузке модуля приходит колбэк (режим опциональных функций)
optional_modules: ClassVar[list] = ["TranslateEngine"]
- Порядок запуска: адаптеры, объявившие жёсткие зависимости от модулей, откладывают запуск до завершения инициализации модулей
- Уведомления о мягких зависимостях: при загрузке модуля из
optional_modules(или из жёстких зависимостей модулей) вызываетсяon_dependency_ready(module_name); при выгрузке —on_dependency_lost(module_name)(по умолчанию пустые реализации, можно переопределить) — охватывает сценарии поздней загрузки и горячей перезагрузки:
async def on_dependency_ready(self, module_name):
"""Мягкая зависимость готова: включаем соответствующую опциональную функцию"""
if module_name == "TranslateEngine":
self._translate = self.sdk.TranslateEngine
async def on_dependency_lost(self, module_name):
"""Мягкая зависимость потеряна: деградация функций"""
if module_name == "TranslateEngine":
self._translate = None
Note
Для этой возможности требуется ErisPulse 2.8.0+.
Замечания по __init__
В разработке адаптеров есть три уровня, где может потребоваться переопределение __init__. Ниже — правильный подход для каждого уровня.
1. Уровень BaseAdapter (в большинстве случаев переопределение не требуется)
BaseAdapter.__init__(self, sdk=None) отвечает за создание фабрик Send / Request и автоматически выполняет следующее:
- принимает параметр
sdkи устанавливаетself.sdk,self.logger - если объявлен
ConfigClass, черезself.cfgможно читать глобальную конфигурацию в реальном времени - если объявлен
AccountConfigClass, черезself.accountsможно читать конфигурацию нескольких аккаунтов в реальном времени
В большинстве случаев переопределять __init__ не нужно, достаточно объявить ConfigClass:
class MyAdapter(BaseAdapter):
ConfigClass = MyAdapterConfig # После объявления фреймворк управляет конфигурацией автоматически
async def start(self):
cfg = self.cfg # Типобезопасно, чтение в реальном времени
...
Если действительно нужна собственная логика инициализации, достаточно вызвать super().__init__(sdk):
class MyAdapter(BaseAdapter):
ConfigClass = MyAdapterConfig
def __init__(self, sdk=None):
super().__init__(sdk) # Передаём sdk
self.converter = self._setup_converter()
self.convert = self.converter.convert
2. Внутренний класс Send (в большинстве случаев переопределение не требуется)
SendDSL.__init__ отвечает за передачу состояния при цепочечных вызовах (тип цели, ID цели, аккаунт и т. д.). В большинстве случаев нужно переопределять только методы (Raw_ob12, Text и т. д.), а не __init__.
Если это действительно необходимо (например, для инициализации специфичного для платформы состояния), обязательно передавайте все параметры дальше:
class MyAdapter(BaseAdapter):
class Send(BaseAdapter.Send):
# Параметры: adapter, target_type, target_id, account_id
def __init__(self, adapter, target_type=None, target_id=None, account_id=None):
super().__init__(adapter, target_type, target_id, account_id) # ← обязательно передать дальше
self._my_state = None # Инициализация, специфичная для платформы
Почему обязательно передавать дальше? Каждый шаг цепочечного вызова создаёт новый экземпляр через self.__class__(...):
adapter.Send.To("user", "123") # → Send(adapter, "user", "123", None)
adapter.Send.To("user", "123").Using("bot1") # → Send(adapter, "user", "123", "bot1")
Если сигнатура __init__ не совпадает или не вызван super(), цепочка вызовов прервётся.
3. Внутренний класс Request (в большинстве случаев переопределение не требуется)
Аналогично Send. Параметры: adapter, request_id, account_id:
class MyAdapter(BaseAdapter):
class Request(RequestDSL):
# Параметры: adapter, request_id, account_id
def __init__(self, adapter, request_id=None, account_id=None):
super().__init__(adapter, request_id, account_id) # ← обязательно передать дальше
self._my_state = None # Инициализация, специфичная для платформы
Итог
| Уровень | Когда переопределять | Что обязательно сделать |
|---|---|---|
| BaseAdapter | Если нужна собственная логика инициализации | super().__init__(sdk) (передать параметр sdk) |
| Внутренний класс Send | Если нужно инициализировать состояние, связанное с отправкой | super().__init__(adapter, target_type, target_id, account_id) |
| Внутренний класс Request | Если нужно инициализировать состояние, связанное с запросами | super().__init__(adapter, request_id, account_id) |
| Все три уровня | В большинстве случаев | Просто объявить ConfigClass, не трогая __init__ |
9. Сведения о подключении и обнаружение маршрутов
После регистрации маршрутов адаптером фреймворк сохраняет всю информацию о маршрутах. Пользователи могут посмотреть адреса подключения адаптера через следующие API:
from ErisPulse import sdk
# Получить полную информацию о подключении адаптера
info = sdk.adapter.get_connection_info("myplatform")
# {
# "platform": "myplatform",
# "status": "started",
# "connection": {
# "base_url": "http://localhost:8080",
# "http_routes": [
# {"path": "/myplatform/webhook", "method": "POST",
# "url": "http://localhost:8080/myplatform/webhook"}
# ],
# "websocket_routes": [
# {"path": "/myplatform/ws",
# "url": "ws://localhost:8080/myplatform/ws"}
# ]
# }
# }
# Перечислить маршруты всех пространств имён (адаптеры/модули)
namespaces = sdk.router.list_namespaces()
# {"myplatform": {"http": ["/myplatform/webhook"], "websocket": ["/myplatform/ws"]}}
# Получить полные URL подключения пространства имён
urls = sdk.router.get_module_urls("myplatform")
# {"base_url": "http://localhost:8080", "http": [...], "websocket": [...]}
# Получить подробную информацию о маршрутах пространства имён
routes = sdk.router.get_module_routes("myplatform")
# {"http": [{"path": "/myplatform/webhook", "methods": ["POST"]}],
# "websocket": [{"path": "/myplatform/ws", "auth": false}]}
Подсказка: информация, возвращаемая
get_connection_info(), подходит для отображения пользователю (например, в WebUI) и помогает настроить адрес колбэка или адрес WebSocket-подключения на стороне платформы.module_name, указанный при регистрации маршрута, должен полностью совпадать с именемplatform, под которым адаптер зарегистрирован в ErisPulse, иначе обнаружение маршрутов не сможет корректно их связать.
10. Поддержка SSE (Server-Sent Events)
ErisPulse имеет встроенную поддержку SSE, не зависящую от сервера; модули и адаптеры могут регистрировать SSE-эндпоинты через @sdk.router.sse().
Базовое использование
import asyncio
from ErisPulse import sdk
@sdk.router.sse("MyModule", "/events")
async def event_stream(sse):
"""Отправка SSE-событий"""
count = 0
while not sse.closed:
await sse.send({"count": count}, event="update")
count += 1
await asyncio.sleep(1)
Использование параметров запроса
Обработчик может объявить параметр request для доступа к информации о клиентском запросе:
@sdk.router.sse("MyModule", "/events")
async def event_stream(request, sse):
token = request.query_params.get("token")
if not validate_token(token):
await sse.close()
return
while not sse.closed:
data = await fetch_data(token)
await sse.send(data)
await asyncio.sleep(5)
SseEmitter API
| Метод | Описание |
|---|---|
sse.send(data, event=None, id=None, retry=None) |
Отправка SSE-события. data, не являющийся str, автоматически сериализуется в JSON |
sse.close() |
Корректное закрытие SSE-соединения (безопасно вызывать многократно) |
sse.closed |
Закрыто ли соединение |
sse.request |
Объект низкоуровневого запроса (можно использовать для чтения query params, headers) |
Использование в RouteGroup
api = sdk.router.group("MyModule", "/api", version="1")
@api.sse("/events")
async def events(sse):
await sse.send({"msg": "hello"})
Обнаружение маршрутов
SSE-маршруты автоматически появляются в API обнаружения маршрутов:
# list_namespaces включает ключ "sse"
sdk.router.list_namespaces()
# {"MyModule": {"http": [...], "websocket": [...], "sse": ["/MyModule/events"]}}
# get_module_routes помечает streaming: true
sdk.router.get_module_routes("MyModule")
# {"http": [...], "websocket": [...], "sse": [{"path": "/MyModule/events", "streaming": true}]}
# get_module_urls генерирует полные URL
sdk.router.get_module_urls("MyModule")
# {"sse": [{"path": "/MyModule/events", "url": "http://localhost:8080/MyModule/events"}]}
Независимость от сервера:
SseEmitterотделён от низкоуровневого HTTP-фреймворка через колбэки. Фреймворк предоставляетregister_sse()и декоратор@sseв качестве единой точки регистрации, поэтому адаптер может реализовать SSE-эндпоинты без прямой зависимости от какого-либо низкоуровневого HTTP-фреймворка.
Что дальше
Дополните три ключевых компонента адаптера
- Конвертер событий — детали реализации преобразования событий платформы → OneBot12 и типичные ловушки
- Подробное описание SendDSL — цепочка отправки сообщений, методы-модификаторы и правила отправки
- Основные концепции адаптеров — описание механизмов BaseAdapter / жизненного цикла / мультиаккаунта
После того как каркас заработал
- Лучшие практики адаптеров — инженерная доводка: повторные попытки подключения, heartbeat, обработка ошибок
- Руководство по публикации и магазину модулей — публикация на PyPI и в магазине модулей
适配器核心概念
来源:
docs/ru/developer-guide/adapters/core-concepts.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/developer-guide/adapters/core-concepts.md)
Основные концепции адаптеров
Tip
Когда стоит читать этот документ: обязательный материал на пути разработки адаптеров (полное описание ключевых механизмов: BaseAdapter / SendDSL / жизненный цикл и др.). Если вам нужно лишь начать работу, сначала прочитайте Введение в разработку адаптеров, а к этому документу возвращайтесь при возникновении вопросов уровня «почему».
Этот документ объясняет, как работают адаптеры: как AdapterManager регистрирует и распределяет события, что BaseAdapter делает за вас при инициализации, как объявлять конфигурацию и поддержку нескольких аккаунтов — если при написании адаптера возникает вопрос «почему нужно писать именно так», ищите ответ здесь.
Архитектура адаптера
Отношения между компонентами
Прямое преобразование (направление приёма) Обратное преобразование (направление отправки)
───────────────── ─────────────────
┌──────────────────┐ ┌──────────────────┐
│ Платформа-специфич. события │ │ Модуль формирует сообщение │
└────────┬─────────┘ └────────┬─────────┘
│ │
↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ │ │ Адаптер (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
Управление соединениями
Два корректных способа реализации start()
Адаптерам клиентского режима (WebSocket-клиент, HTTP-опрос и т.д.) необходимо запускать в start() резидентный цикл соединения. Фреймворк поддерживает два способа написания — оба корректны, но выбрать можно только один из них:
Способ 1: spawn с немедленным возвратом (рекомендуется) — через spawn_client_loop() цикл планируется как фоновая задача, принадлежащая данной платформе, при этом start() сразу возвращает управление:
class MyAdapter(BaseAdapter):
async def start(self):
# Задача цикла принадлежит self._platform: при shutdown фреймворк отменяет её автоматически
self.spawn_client_loop(self._connect_loop())
# Здесь start() завершается — «запуск = успех», сбои соединения обрабатываются повторами внутри цикла
async def _connect_loop(self):
while True:
try:
await self._connect_and_receive() # при разрыве выбрасывается исключение → отсрочка ниже
except asyncio.CancelledError:
raise
except Exception:
await asyncio.sleep(5) # самостоятельная отсрочка и переподключение внутри цикла
Способ 2: резидентный — start() напрямую выполняет await цикла соединения и никогда не возвращает управление:
class MyAdapter(BaseAdapter):
async def start(self):
await self._connect_loop() # резидентный, не возвращает управление; исключение в цикле = сбой start()
Различия и общие черты двух способов:
| Способ 1: spawn с возвратом | Способ 2: резидентный | |
|---|---|---|
Момент возврата start() |
сразу после запуска цикла | при выходе/сбое цикла соединения (фреймворк повторяет попытку согласно стратегии отсрочки) |
| Обработка сбоя соединения | самостоятельный перехват и отсрочка внутри цикла | исключение передаётся выше, в цепочку повторных попыток фреймворка |
| Применимые сценарии | требуется быстрый возврат подтверждения запуска (несколько соединений, после регистрации маршрутов нужно запустить другие задачи) | простая реализация с семантикой «соединение = запуск» |
spawn_client_loop(coro, *, name="client_loop") — точка входа планирования для способа 1: задача регистрируется по принадлежности к self._platform, при shutdown() адаптера фреймворк автоматически отменяет её в соответствии с этой принадлежностью, поэтому управлять дескрипторами задач вручную не нужно; неперехваченные исключения внутри цикла автоматически фиксируются в логах (ожидаемые разрывы с последующим переподключением перехватывайте внутри цикла самостоятельно).
Единственно ошибочный способ — их смешивание: после spawn выполнить
awaitтого же цикла — это лишитstart()семантики немедленного возврата, а путь отмены задачи станет непредсказуемым.
Полуготовые базовые классы режимов соединения
Помимо двух способов из предыдущего раздела, для двух самых распространённых режимов соединения фреймворк предоставляет «полуготовые» базовые классы (ErisPulse.Core.Bases.adapter_patterns, импортируются напрямую из корневого пакета): вся оркестрация жизненного цикла start / shutdown выполняется базовым классом, а подкласс реализует только бизнес-хуки.
PollingAdapter (HTTP-опрос)
Подходит для платформ с «регулярной выборкой событий клиентом» (типа Telegram getUpdates). Базовый класс в стиле spawn с возвратом запускает резидентный цикл опроса; при сбое выборки сбой автоматически фиксируется в логах и применяется экспоненциальная отсрочка (база — 1 секунда, максимум — 60 секунд, после успеха сбрасывается в ноль; константы POLLING_ADAPTER_BACKOFF_BASE_SECS / POLLING_ADAPTER_BACKOFF_MAX_SECS можно настроить в Core/constants.py):
from ErisPulse import PollingAdapter
class MyAdapter(PollingAdapter):
# Интервал опроса по умолчанию 5 секунд; после объявления поля poll_interval в ConfigClass приоритет имеет значение из конфигурации (применяется при горячем обновлении)
poll_interval = 5.0
async def fetch_events(self):
"""Выбирает пакет нативных событий платформы (исключения обрабатываются базовым классом через отсрочку, try/except не нужен)"""
resp = await self.sdk.client.get(f"{self.cfg.endpoint}/getUpdates")
data = await resp.json()
return data.get("result", [])
def convert_one(self, raw):
"""Преобразует одно нативное событие в OneBot12; возвращает None для пропуска"""
return self.converter.convert(raw)
async def call_api(self, endpoint: str, **params):
... # как у обычного адаптера
Минимальный набор для реализации: fetch_events / convert_one / call_api. start() сразу возвращает управление после запуска цикла; shutdown() не требует реализации в подклассе (цикл опроса автоматически отменяется по принадлежности к owner).
WebhookAdapter (HTTP-колбэк)
Подходит для платформ с «активной отправкой колбэков со стороны платформы». start() базового класса регистрирует маршрут приёма на роутере фреймворка (итоговый путь /{имя платформы}/webhook, метод POST), а регистрация обёрнута в owner_scope — при выгрузке адаптера всё автоматически очищается по owner, отменять регистрацию вручную не нужно:
from ErisPulse import WebhookAdapter
class MyAdapter(WebhookAdapter):
def verify_request(self, request) -> bool:
"""Необязательный хук: проверка колбэка (по умолчанию пропускает; при возврате False базовый класс отвечает 403)"""
cfg = self.cfg
if not cfg.token:
return True
auth = request.headers.get("authorization", "")
return auth == f"Bearer {cfg.token}"
def convert(self, payload):
"""Преобразует полезную нагрузку колбэка в OneBot12; возвращает None для пропуска"""
return self.converter.convert(payload)
async def call_api(self, endpoint: str, **params):
... # как у обычного адаптера
Минимальный набор для реализации: convert / call_api (verify_request необязателен). Адрес колбэка получают через sdk.adapter.get_connection_info(platform) и указывают в панели управления платформы; при неудачной проверке запрос возвращает 403, при ошибке разбора полезной нагрузки — 400, при успехе возвращается {"status": "ok"}.
Как выбрать между тремя режимами
| Протокол платформы | Наследование | Скаффолд |
|---|---|---|
| Платформа подключается сама (WebSocket-сервер) | BaseAdapter |
epsdk create adapter (по умолчанию ws-server) |
| Мы сами опрашиваем (HTTP-опрос) | PollingAdapter |
epsdk create adapter --template polling |
| Платформа сама присылает данные (HTTP-колбэк) | WebhookAdapter |
epsdk create adapter --template webhook |
Полностью работоспособные примеры см. в репозитории: examples/example-adapter/ (ws-server), examples/example-adapter-polling/, examples/example-adapter-webhook/.
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:
"""Аутентификация WebSocket"""
token = websocket.query_params.get("token")
return token == "valid_token"
WebHook-соединение
class MyAdapter(BaseAdapter):
async def start(self):
"""Регистрация WebHook-маршрута"""
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"}
Платформам чисто колбэк-типа не нужно вручную описывать показанную выше оркестрацию регистрации/проверки/очистки — достаточно напрямую унаследоваться от «полуготового» базового класса
WebhookAdapterи реализовать толькоconvert(при желании переопределивverify_request).
Запрос информации о маршрутах: маршруты, зарегистрированные адаптером (HTTP, WebSocket, SSE), позволяют получить полный адрес соединения (включающий
base_url+ путь) черезsdk.adapter.get_connection_info(platform)иsdk.router.get_module_urls(module_name). Подробнее см. Введение в разработку адаптеров - Информация о соединении и обнаружение маршрутов и Поддержка 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 в событии:
- События meta: выполняют соответствующие действия в зависимости от
detail_type(connect — регистрация / disconnect — пометка офлайн / heartbeat — обновление времени активности) - Обычные события (message/notice/request): автоматическое обнаружение бота и обновление времени активности
# Любое событие, содержащее поле self, запускает автоматическое обнаружение
await self.adapter.emit({
"type": "message",
"platform": "myplatform",
"self": {"platform": "myplatform", "user_id": "bot123"},
# ...
})
# Бот "bot123" автоматически зарегистрирован (при первом появлении) и время активности обновлено
Типы Meta-событий
detail_type |
Описание | Поведение фреймворка |
|---|---|---|
connect |
Подключение бота | Регистрирует бота и запускает событие жизненного цикла adapter.bot.online |
disconnect |
Отключение бота | Помечает бота офлайн и запускает событие жизненного цикла adapter.bot.offline |
heartbeat |
Heartbeat бота | Обновляет время активности и метаданные бота |
Отправка Meta-событий адаптером
Способы отправки (отправка одной строкой через emit_meta(), события heartbeat и ручное формирование формата) описаны в разделе Лучшие практики адаптеров - Управление состоянием ботов и Meta-события.
Расширенная информация в поле self
Помимо обязательных полей platform и user_id, поле self поддерживает следующие необязательные поля:
| Поле | Описание |
|---|---|
user_name |
Имя пользователя бота |
nickname |
Никнейм бота |
avatar |
URL аватара бота |
account_id |
Идентификатор мультиаккаунта |
Запрос состояния ботов
API для запросов (get_bot_info() / list_bots() / is_bot_online() / get_status_summary()) описаны в разделе API системы адаптеров - Управление состоянием ботов.
Прослушивание жизненного цикла ботов
Таблицу событий жизненного цикла adapter.bot.online / adapter.bot.offline и примеры прослушивания см. в разделе API системы адаптеров - Управление состоянием ботов.
Связанная документация
Подробное изучение реализации
- Подробное руководство по SendDSL — изучение отправки сообщений
- Лучшие практики разработки адаптеров — создание высококачественных адаптеров
Возврат к начальным шагам
- Введение в разработку адаптеров — создание первого адаптера
发送 DSL
来源:
docs/ru/developer-guide/adapters/send-dsl.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/developer-guide/adapters/send-dsl.md)
Подробное руководство по SendDSL
Tip
Когда это нужно прочитать: обязательно к прочтению в рамках пути разработки адаптеров (DSL отправки). Если это не требуется — пропустите, на использование остальных функций это не влияет.
SendDSL позволяет выполнить отправку сообщения единой цепочкой: указание цели, @ / ответ, повтор / тайм-аут / задержка, пакетная отправка — всё делается одной строкой цепного вызова. В этой статье рассматриваются методы отправки, модифицирующие методы, правила отправки и внутренние механизмы цепочки отправки.
Базовые способы вызова
1. Указание типа и ID
await adapter.Send.To("group", "123").Text("Hello")
2. Указание только ID
await adapter.Send.To("123").Text("Hello")
3. Указание аккаунта отправки
await adapter.Send.Using("bot1").Text("Hello")
4. Комбинированное использование
await adapter.Send.Using("bot1").To("group", "123").Text("Hello")
Цепочка методов
flowchart LR
A["Using / Account<br/>(выбор аккаунта отправки, опционально)"] --> B["To<br/>(выбор типа цели и ID)"]
B --> C["Модифицирующие методы<br/>At / Reply / Expire / ForMember и др."]
C --> D["Методы отправки<br/>Text / Image / Voice / Raw_ob12"]
D --> E["Возвращает asyncio.Task"]
Методы отправки
Все методы отправки возвращают объект asyncio.Task.
Базовые методы (встроены в базовый класс)
Следующие стандартные методы уже встроены в базовый класс SendDSL и по умолчанию делегируются Raw_ob12; подклассы адаптеров могут использовать их напрямую без повторной реализации, а IDE обеспечивает автодополнение:
| Название метода | Описание | Возвращаемое значение |
|---|---|---|
Text(text: str) |
Отправка текстового сообщения | asyncio.Task |
Image(file: bytes | str) |
Отправка изображения | asyncio.Task |
Voice(file: bytes | str) |
Отправка голосового сообщения (сегмент audio OneBot12) |
asyncio.Task |
Video(file: bytes | str) |
Отправка видео | asyncio.Task |
File(file: bytes | str, filename: str = None) |
Отправка файла | asyncio.Task |
Адаптер может переопределить отдельный стандартный метод, чтобы реализовать специфичную для платформы логику:
class Send(SendDSL):
def Raw_ob12(self, message, **kwargs):
# Обязателен к реализации
...
# Опционально: переопределение Text для платформенно-специфичной логики
# def Text(self, text: str):
# return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
Протокольные методы
| Название метода | Описание | Возвращаемое значение | Обязателен ли |
|---|---|---|---|
Raw_ob12(message) |
Отправка сообщения в формате OneBot12 | asyncio.Task |
Обязателен к реализации |
Важно:
Raw_ob12— ключевой метод адаптера, обязательный к реализации. Это единая точка входа для обратного преобразования (OneBot12 → платформа). Если метод не реализован, базовый класс запишет журнал уровня error и вернёт стандартный ответ с ошибкой (status: "failed",retcode: 10002). Стандартные методы (Text,Imageи др.) по умолчанию делегируютсяRaw_ob12.
Платформенно-специфичные методы
Адаптер может добавлять в подкласс Send платформенно-специфичные методы отправки (они распознаются через event.supports() / event.available_methods()):
class Send(SendDSL):
def Raw_ob12(self, message, **kwargs): ...
# Платформенно-специфичный метод
def Sticker(self, sticker_id: str):
return self.Raw_ob12([{"type": "sticker", "data": {"id": sticker_id}}])
Модифицирующие методы
Модифицирующие методы возвращают self, что позволяет использовать цепные вызовы.
Метод At
# Упоминание одного пользователя
await adapter.Send.To("group", "123").At("456").Text("Привет")
# Упоминание нескольких пользователей
await adapter.Send.To("group", "123").At("456").At("789").Text("Всем привет")
Метод AtAll
# Упоминание всех участников
await adapter.Send.To("group", "123").AtAll().Text("Привет всем")
Метод Reply
# Ответ на сообщение
await adapter.Send.To("group", "123").Reply("msg_id").Text("Текст ответа")
Комбинирование модификаторов
await adapter.Send.To("group", "123").At("456").Reply("msg_id").Text("Ответ на упомянутое сообщение")
Платформенно-специфичные модифицирующие методы
Помимо встроенных At/AtAll/Reply, адаптер может определять платформенно-специфичные модифицирующие методы. Таким методам достаточно возвращать self — какие-либо декораторы не нужны, фреймворк распознаёт их автоматически:
- Возвращает
self(экземпляр SendDSL) → модифицирующий метод: не запускает обёртку отправки / события жизненного цикла, цепочка продолжается - Возвращает
Task/Awaitable→ метод отправки
class Send(SendDSL):
def Raw_ob12(self, message, **kwargs): ...
# Модифицирующий метод: возвращает self, ничего не отправляет
def Expire(self, seconds: int):
self._expire = seconds
return self
def ForMember(self, user_id: str):
self._member = user_id
return self
# Метод отправки: возвращает Task и зависит от состояния, заданного модифицирующими методами
def Board(self, content: str, **kwargs):
return self.Raw_ob12([{"type": "board", "data": {"text": content}}])
Использование:
# Модифицирующие методы можно накладывать цепочкой друг за другом
await adapter.Send.To("group", "big").Expire(3600).ForMember("114").Board("Содержимое доски")
Использование модифицирующих методов в классе-обёртке Event
Note
Для функций reply(via=) и event.send_chain() требуется ErisPulse 2.7.0+.
event.reply() по умолчанию предоставляет только встроенные параметры модификации, такие как at_sender/at_users/at_all/quote. Чтобы использовать платформенно-специфичные модифицирующие методы, есть два способа:
Способ 1: параметр via метода reply()
Подходит для небольшого набора заранее известных модифицирующих методов:
await event.reply("Содержимое доски", method="Board",
via=[("Expire", 3600), ("ForMember", "114514")])
via — это список, каждый элемент которого может иметь вид:
| Форма записи | Эквивалентный цепной вызов |
|---|---|
"Name" |
.Name() |
("Name", arg1, arg2) |
.Name(arg1, arg2) |
("Name", (arg1,), {kw: val}) |
.Name(arg1, kw=val) |
Способ 2: event.send_chain()
Подходит для нескольких модифицирующих методов подряд или методов-действий без параметров содержимого (например, отзыв, удаление). send_chain() возвращает цепочку отправки с уже настроенными To/Using, к которой можно свободно добавлять любые модифицирующие методы и методы отправки:
# Платформенно-специфичный модифицирующий метод + отправка доски
await event.send_chain().Expire(3600).Board("Истечёт через час")
# Несколько модифицирующих методов подряд
await (event.send_chain()
.Expire(3600)
.ForMember("114514")
.Board("Содержимое доски", content_type="markdown"))
# Встроенные модифицирующие методы также доступны
await event.send_chain().At("123").Reply("msg_id").Text("hi")
# Методы-действия без параметров содержимого
await event.send_chain().DismissBoard()
send_chain()возвращает полноценный экземпляр SendDSL, поэтому доступны все возможности цепочек — не только модифицирующие методы, но и правила отправки и пакетное построение:
# Правила отправки: повтор + тайм-аут + колбэк успешной отправки
await (event.send_chain()
.Retry(3).Timeout(10)
.Hook(lambda r: print("Отправка успешна"))
.Text("Надёжная отправка"))
# Отложенная отправка + платформенный модификатор + доска
await event.send_chain().Defer(5).Expire(3600).Board("Отложенная доска")
# Режим пакетного построения
results = await (event.send_chain()
.Build()
.Text("Первая фраза").Image("pic.jpg").Text("Вторая фраза")
.send_all())
Управление аккаунтами
Метод Using
Using() используется для указания аккаунта, от имени которого отправляется сообщение. Переданный идентификатор сопоставляется через _resolve_account() со следующим приоритетом:
- Имя аккаунта — ключ в конфигурации (например,
"default","bot1") - bot_id, внедрённый во время выполнения — идентификатор, автоматически внедряемый при конвертации события
- Любое строковое (str) поле — прочие строковые поля в конфигурации
- Запасной вариант — первый включённый аккаунт
# Использование имени аккаунта
await adapter.Send.Using("account1").To("user", "123").Text("Hello")
# Использование bot_id (то есть self.user_id из события)
await adapter.Send.Using("bot_123").To("user", "123").Text("Hello")
Метод Account
Метод Account эквивалентен Using:
await adapter.Send.Account("account1").To("user", "123").Text("Hello")
Асинхронная обработка
Без ожидания результата
# Сообщение отправляется в фоновом режиме
task = adapter.Send.To("user", "123").Text("Hello")
# Продолжаем выполнять другие операции
# ...
С ожиданием результата
# Получение результата напрямую через await
result = await adapter.Send.To("user", "123").Text("Hello")
print(f"Результат отправки: {result}")
# Сначала сохраняем Task, затем ожидаем
task = adapter.Send.To("user", "123").Text("Hello")
# ... другие операции ...
result = await task
Система правил отправки
SendDSL включает набор декораторов правил отправки: правила добавляются через цепные методы и применяются единообразно при финальной отправке. Правила покрывают типовые производственные сценарии: контроль тайм-аутов, повтор при сбоях, колбэки успешной отправки, отложенная отправка, отбрасывание по приоритету, мониторинг прогресса.
Методы правил возвращают self (как At/AtAll/Reply) и должны вызываться перед методом отправки (Text/Image и т.д.). Правила распространяются на новые экземпляры, создаваемые To/Using/Account.
Обзор методов правил
| Метод | Описание |
|---|---|
.Hook(callback) |
Колбэк, выполняемый после успешной отправки (можно вызывать многократно, выполняются по порядку) |
.Retry(times=1) |
Автоматический повтор N раз при сбое (всего N+1 попыток с учётом первой) |
.Timeout(seconds) |
Тайм-аут одной отправки; при превышении текущая попытка отменяется (можно комбинировать с Retry) |
.Defer(seconds=1.0) |
Отложенная отправка (таймер внутри процесса, без персистентности) |
.Priority(level, drop_if_busy=False) |
Установка приоритета; возможно отбрасывание при перегрузке очереди |
.OnProgress(callback) |
Колбэк прогресса по этапам (передаётся SendContext) |
.OnError(callback) |
Колбэк ошибки при финальном сбое (срабатывает только один раз) |
Логика после успешной отправки (Hook)
# Синхронный колбэк
await (adapter.Send.To("user", "123")
.Hook(lambda r: print(f"Отправка успешна, ID сообщения: {r['message_id']}"))
.Text("Привет"))
# Асинхронный колбэк
async def deduct_points(result):
await db.update(user_id="123", points=-1)
await adapter.Send.To("user", "123").Hook(deduct_points).Text("Списание баллов")
Hook выполняется только при финальном успехе отправки (включая успех после повторов); при сбое, тайм-ауте или отмене он не срабатывает.
Автоматический повтор при сбое (Retry)
# 2 повтора после первого сбоя, всего 3 попытки
result = await adapter.Send.To("user", "123").Retry(2).Text("С повтором")
Условия срабатывания повтора: исключение при отправке, тайм-аут отправки, ответ со status == "failed".
Автоматическая отмена по тайм-ауту (Timeout)
# Отмена, если одна отправка превышает 10 секунд
await adapter.Send.To("user", "123").Timeout(10).Text("С тайм-аутом")
# Тайм-аут + повтор: 10 секунд на попытку, максимум 3 попытки
await adapter.Send.To("user", "123").Timeout(10).Retry(2).Text("Тайм-аут с повтором")
Мониторинг прогресса (OnProgress / OnError)
def on_progress(ctx):
print(f"Этап: {ctx.stage}, попытка: {ctx.attempt + 1}/{ctx.max_attempts}, затрачено: {ctx.elapsed:.2f}s")
if ctx.stage == "failed":
print(f" Ошибка: {ctx.error!r}")
async def on_error(ctx):
await notify_admin(f"Не удалось отправить {ctx.target_id}: {ctx.error!r}")
await (adapter.Send.To("user", "123")
.Retry(3).Timeout(10)
.OnProgress(on_progress)
.OnError(on_error)
.Text("Мониторинг"))
Поля, которые содержит SendContext: task_id, platform, method, target_type, target_id, bot_id, stage, attempt, max_attempts, started_at, finished_at, elapsed, error, result, extra.
Возможные значения stage: pending, sending, retrying, success, failed, timeout, cancelled, dropped.
Отложенная отправка (Defer)
# Отправка через 5 секунд
await adapter.Send.To("user", "123").Defer(5).Text("Запоздалое сообщение")
Примечание: задержка реализуется таймером внутри процесса; при перезапуске процесса она теряется, персистентность не предусмотрена.
Приоритет и отбрасывание при перегрузке (Priority)
# Низкоприоритетное сообщение, автоматически отбрасывается при накоплении очереди
result = await (adapter.Send.To("user", "123")
.Priority(-1, drop_if_busy=True)
.Text("Отбрасываемое уведомление"))
# Если отброшено, result["status"] == "failed"
Если включён drop_if_busy, при превышении порога числа находящихся в работе задач отправки (по умолчанию 64) отправка сразу отменяется. Глобальный порог можно настроить через .PriorityThreshold(n).
Комбинация правил и фоновое выполнение
# Не блокирует основной поток — правила по-прежнему действуют
task = (adapter.Send.To("user", "123")
.Hook(lambda r: print("Отправка успешна!"))
.Retry(3)
.Timeout(10)
.OnProgress(on_progress)
.Text("Привет"))
# Продолжаем выполнять другие операции
await handle_next_action()
Распространение правил
Правила распространяются на новые экземпляры, создаваемые To/Using/Account, что предотвращает потерю правил в цепных вызовах:
# Правила, заданные до To, также распространяются на экземпляр, созданный To
builder = adapter.Send.Retry(3).Timeout(10)
send = builder.To("user", "123") # send по-прежнему несёт Retry(3) и Timeout(10)
await send.Text("hi")
Правила у разных экземпляров независимы друг от друга (список hooks глубоко копируется).
Режим пакетного построения
Помимо режима одиночной отправки, SendDSL поддерживает режим пакетного построения: несколько методов отправки записываются в одну цепочку и единовременно выполняются в конце. Подходит для сценариев «отправить несколько сообщений одним махом».
Вход в режим построения
Вызовите .Build() перед методом отправки; он возвращает SendBuilder. После этого методы отправки (Text/Image и т.д.) не выполняются сразу, а накапливаются как намерения отправки:
results = await (adapter.Send.To("user", "123")
.Build() # Вход в режим построения
.Text("Первая фраза")
.Image("pic.jpg")
.Text("Вторая фраза")
.send_all()) # Единое выполнение
# results = [результат Text, результат Image, результат Text]
.send_all() возвращает asyncio.Task; после await получается список результатов (в порядке намерений).
Параллельное и последовательное выполнение
По умолчанию выполнение параллельное (отправка с одновременным запуском; общее время примерно равно времени самой медленной). Чтобы гарантировать порядок доставки сообщений, вызовите .Sequential():
# Последовательно: отправка по порядку друг за другом
await (adapter.Send.To("group", "456")
.Build()
.Sequential()
.Text("Сначала это").Text("Затем это")
.send_all())
# Параллельно (по умолчанию, можно вызвать явно)
await (adapter.Send.To("group", "456")
.Build()
.Parallel()
.Text("Параллельно 1").Text("Параллельно 2")
.send_all())
Продолжение при сбое и повтор
Пакетное выполнение использует стратегию продолжения при сбое: сбой одного элемента не прерывает отправку остальных. В сочетании с .Retry() неудавшиеся элементы автоматически повторяются (повтор применяется к отдельному элементу, а не ко всей партии):
await (adapter.Send.To("user", "123")
.Build()
.Retry(2) # Каждая позиция повторяется отдельно 2 раза
.Text("Возможен сбой").Image("Тоже возможен сбой")
.send_all())
Правила для всей партии и колбэки
Правила применяются единообразно ко всей партии:
| Метод | Описание |
|---|---|
.Timeout(seconds) |
Тайм-аут одной попытки для каждой отправки |
.Retry(times) |
Повтор для каждой отправки отдельно (продолжение при сбое) |
.Defer(seconds) |
Отложенная отправка всей партии |
.Hook(callback) |
Срабатывает после успеха всей партии, получает список results |
.OnError(callback) |
Срабатывает при наличии сбоев в партии, получает BatchContext |
.OnProgress(callback) |
Срабатывает по завершении каждой позиции, получает BatchContext |
def on_progress(ctx):
print(f"Прогресс: {ctx.completed}/{ctx.total}, успешно: {ctx.succeeded}, сбоев: {ctx.failed}")
async def on_error(ctx):
print(f"В партии {ctx.failed} сбоев")
results = await (adapter.Send.To("user", "123")
.Build()
.Retry(2).Timeout(10)
.OnProgress(on_progress)
.OnError(on_error)
.Hook(lambda rs: print("Вся партия завершена"))
.Text("a").Text("b").Text("c")
.send_all())
BatchContext содержит: task_id, total, completed, succeeded, failed, stage, results, errors, elapsed, extra.
Возможные значения stage: pending, sending, success (всё успешно), partial (частично успешно), failed (всё с ошибкой).
Наследование модификаторов и правил
Модификаторы At/AtAll/Reply и правила, заданные до .Build(), наследуются всей партией и применяются к каждому сообщению:
await (adapter.Send.To("group", "456")
.At("789") # Наследование: каждое сообщение с @789
.Build()
.Retry(2) # Наследование + добавление: повтор для каждой позиции отдельно
.Text("@Уведомление для вас")
.Image("объявление.jpg")
.send_all())
После входа в Build всё ещё можно добавлять модификаторы (они действуют на всю партию):
await (adapter.Send.To("group", "456")
.Build()
.At("111").At("222") # Добавление @, применяется ко всей партии
.Text("@нескольким")
.send_all())
Фоновое выполнение
Как и при одиночной отправке, .send_all() возвращает Task; можно не делать await и позволить ей выполняться в фоне:
task = (adapter.Send.To("user", "123")
.Build()
.Hook(lambda rs: print("Пакетная отправка завершена"))
.Text("a").Text("b")
.send_all())
# Не блокирует основной поток
await do_something_else()
Правила именования
Именование в PascalCase
Все методы отправки используют нотацию PascalCase (UpperCamelCase):
# ✅ Правильно
def Text(self, text: str):
pass
def Image(self, file: bytes):
pass
# ❌ Неправильно
def text(self, text: str):
pass
def send_image(self, file: bytes):
pass
Платформенно-специфичные методы
Не рекомендуется добавлять методы с префиксом платформы:
# ✅ Рекомендуется
def Sticker(self, sticker_id: str):
pass
# ❌ Не рекомендуется
def TelegramSticker(self, sticker_id: str):
pass
Вместо этого используйте метод Raw:
# ✅ Рекомендуется
await adapter.Send.Raw_ob12([{"type": "sticker", ...}])
# ❌ Не рекомендуется
def TelegramSticker(self, ...):
pass
Внутреннее устройство цепочки отправки
За одним вызовом await adapter.Send.To("group", "123").Text("x") фреймворк выполняет за вас следующую цепочку действий:
flowchart TD
A["adapter.Send.To(...).Text(...)"] --> B["Цепные методы To/Using<br/>каждый раз возвращают новый неизменяемый экземпляр (порядок не важен)"]
B --> C["__getattribute__ перехватывает метод отправки<br/>и оборачивает его в обёртку правил"]
C --> D["Вызов исходного метода (например, Text)<br/>внутри делегируется Raw_ob12"]
D --> E["Raw_ob12 возвращает asyncio.create_task(...)"]
E --> F["Запись журнала [Send]"]
F --> G["emit message.sending (fire-and-forget)"]
G --> H{"Объявлены правила отправки?"}
H -->|"Нет"| I["Task done_callback → emit message.sent"]
H -->|"Да"| J["apply_send_rules оборачивает во внешний Task<br/>повтор/тайм-аут/задержка/приоритет"]
J --> I
I --> K["await возвращает стандартный dict-ответ"]
Что фреймворк делает на каждом шаге:
| Этап | Что делает фреймворк |
|---|---|
| Слияние цепочки | To/Using/Account при каждом вызове создают новый неизменяемый экземпляр, наследующий уже установленные поля, поэтому To(...).Using(...) и Using(...).To(...) эквивалентны, порядок не важен |
| Обёртка методов | Методы отправки (Text и др.) перехватываются __getattribute__ и оборачиваются; модифицирующие методы (To/Using/At/Retry и др.) не оборачиваются. Вложенные вызовы Raw_ob12 защищены от повторной обёртки флагом _in_rule_wrap |
| Создание Task | Настоящая точка создания Task — это asyncio.create_task() внутри Raw_ob12; Text() лишь синхронно возвращает этот Task, не блокируя |
| Журнал отправки | Записывается событийный журнал [Send] platform/method -> target (можно отключить через exclude_levels=["EVENT"]) |
message.sending |
Срабатывает немедленно в режиме fire-and-forget при вызове метода отправки (только при наличии подписчиков — сначала короткая проверка has_handlers) |
message.sent |
Привязывается к done_callback Task — при наличии правил покрывает итоговый результат всего процесса повторов, без правил — само завершение исходного Task |
Цепочка отката при разрешении аккаунта
Когда адаптер внутри вызывает _resolve_account(account_id), конкретный аккаунт определяется в следующем порядке:
- Адаптер с единственным аккаунтом (нет
AccountConfigClass) → возврат напрямую - Точное совпадение имени аккаунта с
account_id - Совпадение по полю
bot_idкаждого аккаунта - Совпадение по любому строковому (
str) значению полей аккаунтов (кромеenabled/name) - Запасной вариант — первый включённый аккаунт
- Если всё не удалось → выбрасывается
ValueError
Передаваемый
account_idберётся из: явного указания вUsing()> полейselfсобытия (account_idимеет приоритет надuser_id, автоматически внедряется черезevent.reply()) > не указан (адаптер в качестве запасного варианта берёт первый включённый аккаунт).
Движок правил отправки (повтор/тайм-аут/задержка)
Правила оборачиваются в новый внешний Task после того, как Raw_ob12 вернёт Task, и не затрагивают основной поток. Ключевые факты:
| Правило | Описание |
|---|---|
Retry(n) |
Всего n+1 попыток; после сбоя немедленная повторная отправка, без экспоненциальной задержки |
Timeout(s) |
Отмена одной отправки по тайм-ауту (asyncio.wait_for); если попытки не исчерпаны — повтор |
Defer(s) |
Задержка (sleep) перед отправкой |
Priority(level, drop_if_busy) |
При превышении порога накопления очереди сразу возвращает {status:"failed", retcode:10002, message:"dropped_low_priority"} |
Hook(fn) |
Выполняется по порядку только при финальном успехе |
on_progress / on_error |
Колбэки по этапам / финального сбоя |
Внимание: повтор — это «немедленная повторная отправка», без интервала между попытками; если платформа ограничивает частоту запросов и требует выдержки, выполните sleep в колбэке
on_errorи повторите отправку вручную. Правила считают отправку успешной поstatus == "ok"возвращаемого dict (retcode == 0).
Полная семантика формата стандартного ответа и
retcodeописана в Спецификации ответов API.
Возвращаемые значения
Объект Task
Все методы отправки возвращают asyncio.Task. Адаптеру достаточно реализовать Raw_ob12; стандартные методы (Text/Image и т.д.) по умолчанию делегируются ему:
import asyncio
def Raw_ob12(self, message, **kwargs):
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
# При необходимости переопределить стандартный метод достаточно вернуть asyncio.Task:
# def Text(self, text: str):
# return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
Стандартизированный ответ
call_api должна возвращать стандартизированный ответ. Рекомендуется использовать методы make_response() / make_error():
async def call_api(self, endpoint: str, **params):
try:
result = await self._do_api_call(endpoint, **params)
return self.make_response(
data=result.get("data"),
message_id=result.get("message_id", ""),
raw=result,
)
except Exception as e:
return self.make_error(message=str(e))
Также поддерживается ручное построение (старый способ по-прежнему совместим):
async def call_api(self, endpoint: str, **params):
return {
"status": "ok" or "failed",
"retcode": 0 or error_code,
"data": {...},
"message_id": "msg_id" or "",
"message": "",
"{platform}_raw": raw_response
}
Полные примеры
Базовое использование
from ErisPulse.Core import adapter
my_adapter = adapter.get("myplatform")
# Отправка текста
await my_adapter.Send.To("user", "123").Text("Hello World!")
# Отправка изображения
await my_adapter.Send.To("group", "456").Image("https://example.com/image.jpg")
# Отправка файла
with open("document.pdf", "rb") as f:
await my_adapter.Send.To("user", "123").File(f.read())
Цепные вызовы
# Упоминание пользователя + ответ
await my_adapter.Send.To("group", "456").At("789").Reply("msg123").Text("Ответ на упомянутое сообщение")
# Упоминание всех + несколько модификаторов
await my_adapter.Send.Using("bot1").To("group", "456").AtAll().Text("Объявление")
Сырые сообщения и построение сообщений
Raw_ob12 — ключевая точка входа обратного преобразования (приём сегментов сообщений OB12 → вызов API платформы), а MessageBuilder — инструмент цепного построения сегментов сообщений, используемый вместе с ним.
Полную спецификацию реализации
Raw_ob12, использованиеMessageBuilderи примеры кода смотрите в:
Связанная документация
Продолжение пути разработки адаптеров
- Введение в разработку адаптеров - создание адаптера
- Ключевые концепции адаптеров - знакомство с архитектурой адаптеров
- Лучшие практики разработки адаптеров - разработка качественных адаптеров
Следование официальным спецификациям
- Спецификация методов отправки - полная спецификация методов отправки (реализация
Raw_ob12иMessageBuilder)
转换器
来源:
docs/ru/developer-guide/adapters/converter.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/developer-guide/adapters/converter.md)
Руководство по реализации конвертера событий
Tip
Когда нужно прочитать эту статью: Обязательно для чтения при разработке адаптера (конвертер событий). Если не требуется, можно пропустить, это не повлияет на использование других функций.
После получения платформой исходных событий адаптером, конвертер событий (Converter) преобразует их в единый формат OneBot12, используемый ErisPulse. Именно этот класс определяет, как события платформы попадают в фреймворк.
Ответственность Converter
Платформенные нативные события ──→ Converter.convert() ──→ События в стандарте OneBot12
Converter отвечает только за прямое преобразование (направление получения), то есть за преобразование данных платформенных нативных событий в стандартный формат OneBot12. Обратное преобразование (направление отправки) обрабатывается методом Send.Raw_ob12().
Основные принципы
- Без потерь: исходные данные должны быть полностью сохранены в поле
{platform}_raw - Совместимость со стандартом: преобразованные события должны соответствовать стандартному формату OneBot12
- Платформенные расширения: платформенно-специфические данные хранятся в полях с префиксом
{platform}_
Базовый класс BaseConverter (рекомендуется)
Начиная с версии 2.7.0, фреймворк предоставляет базовый класс BaseConverter (ErisPulse.Core.Bases), который封装ует общие поля событий OneBot12 и вспомогательные методы для часто используемых сообщений, позволяя конвертеру фокусироваться только на типовом преобразовании:
from ErisPulse.Core.Bases import BaseConverter
class MyConverter(BaseConverter):
def __init__(self):
super().__init__(platform="myplatform")
def convert(self, raw_event: dict) -> dict | None:
if not isinstance(raw_event, dict):
return None
event_type = raw_event.get("type", "")
base = self.build_base_event(raw_event, event_type) # id/time/platform/self/raw
if event_type == "message":
base["type"] = "message"
base["detail_type"] = "group" if raw_event.get("group_id") else "private"
base["user_id"] = str(raw_event.get("sender_id", ""))
base["message"] = [self.text(raw_event.get("content", ""))]
base["alt_message"] = raw_event.get("content", "")
return base
return None
Поле build_base_event() уже заполнено общими полями:
| Поле | Источник |
|---|---|
id |
raw_event["event_id"], если отсутствует, генерируется UUID |
time |
raw_event["timestamp"], если отсутствует, устанавливается текущее время |
platform |
Переданное при создании platform |
self |
{"platform": ..., "user_id": raw_event["bot_id"]} |
{platform}_raw |
Исходное событие (соответствует принципу "без потерь") |
{platform}_raw_type |
Тип исходного события |
Общие вспомогательные методы для сообщений (все статические, можно напрямую использовать):
converter.text("hi") # {"type": "text", "data": {"text": "hi"}}
converter.at("123456") # {"type": "at", "data": {"user_id": "123456"}}
converter.image("file.png") # {"type": "image", "data": {"file": "file.png"}}
При ручной реализации конструирование общих полей
build_base_eventявляется обязательным и повторяющимся шаблонным кодом, использованиеBaseConverterпозволяет избежать этой части и обеспечивает "без потерь" преобразование (исходное событие всегда попадает в{platform}_raw).
Метод convert()
Подпись метода
def convert(self, raw_event: dict) -> dict:
"""
Преобразует событие платформы в стандартный формат OneBot12
:param raw_event: Данные события платформы
:return: Словарь события в стандартном формате OneBot12
"""
pass
Структура возвращаемого значения
Словарь события после преобразования должен содержать следующие стандартные поля:
{
"id": "Уникальный ID события",
"time": 1234567890, # Unix-время (секунды)
"type": "message", # Тип события
"detail_type": "private", # Подтип события
"platform": "myplatform", # Название платформы
"self": {
"platform": "myplatform",
"user_id": "bot_user_id"
},
# Поля сообщения
"user_id": "id_отправителя",
"message": [...], # Список сегментов сообщения в формате OneBot12
"alt_message": "Содержимое в виде обычного текста",
# Должны сохраняться исходные данные
"myplatform_raw": { ... }, # Полные исходные данные события платформы
"myplatform_raw_type": "Имя типа исходного события",
}
Обязательные поля
Общие поля (для всех типов событий)
| OB12 поле | Тип | Описание |
|---|---|---|
id |
str | Уникальный идентификатор события |
time |
int | Временная метка Unix (в секундах) |
type |
str | Тип события: message / notice / request / meta |
detail_type |
str | Детальный тип: private / group / friend и т.д. |
platform |
str | Название платформы, совпадает с именем адаптера |
self |
dict | Информация о боте: {"platform": "...", "user_id": "..."} |
Дополнительные поля для событий сообщений
| OB12 поле | Тип | Описание |
|---|---|---|
user_id |
str | ID отправителя |
message |
list[dict] | Список сегментов сообщений OneBot12 |
alt_message |
str | Резервный текстовый контент |
Дополнительные поля для уведомлений
| OB12 поле | Тип | Описание |
|---|---|---|
user_id |
str | ID связанного пользователя |
operator_id |
str | ID оператора (например, при изменении участников группы) |
Преобразование сегментов сообщений
Стандарт OneBot12 определяет следующие типы сегментов сообщений:
# Текст
{"type": "text", "data": {"text": "Привет"}}
# Изображение
{"type": "image", "data": {"file": "https://example.com/img.jpg"}}
# Аудио
{"type": "audio", "data": {"file": "https://example.com/audio.mp3"}}
# Видео
{"type": "video", "data": {"file": "https://example.com/video.mp4"}}
# Файл
{"type": "file", "data": {"file": "https://example.com/doc.pdf"}}
# Упоминание пользователя
{"type": "mention", "data": {"user_id": "123"}}
# Упоминание всех
{"type": "mention_all", "data": {}}
# Ответ на сообщение
{"type": "reply", "data": {"message_id": "msg_123"}}
Если платформа не поддерживает определённый тип сегмента сообщений, этот сегмент можно опустить или преобразовать в наиболее близкий стандартный тип.
Расширения платформы
Данные, специфичные для платформы, должны храниться с префиксом {platform}_, чтобы избежать конфликта с стандартными полями:
{
# Стандартные поля
"type": "message",
"detail_type": "group",
# ...
# Расширения платформы
"myplatform_raw": { ... }, # Исходные данные события (обязательно)
"myplatform_raw_type": "chat", # Тип исходного события (обязательно)
# Другие специфические для платформы поля
"myplatform_group_name": "Название группы",
"myplatform_sender_role": "администратор",
}
Важно: Поле
{platform}_rawобязательно, так как система событий и модули ErisPulse могут зависеть от него для доступа к исходным данным платформы.
Полный пример
Ниже представлен полный пример реализации Converter:
class MyConverter:
def __init__(self, platform: str):
self.platform = platform
def convert(self, raw_event: dict) -> dict:
event_type = raw_event.get("type", "")
base_event = {
"id": raw_event.get("id", ""),
"time": raw_event.get("timestamp", 0),
"platform": self.platform,
"self": {
"platform": self.platform,
"user_id": raw_event.get("self_id", ""),
},
"myplatform_raw": raw_event,
"myplatform_raw_type": event_type,
}
if event_type == "chat":
return self._convert_message(raw_event, base_event)
elif event_type == "notification":
return self._convert_notice(raw_event, base_event)
elif event_type == "request":
return self._convert_request(raw_event, base_event)
return base_event
def _convert_message(self, raw: dict, base: dict) -> dict:
base["type"] = "message"
base["detail_type"] = "group" if raw.get("group_id") else "private"
base["user_id"] = raw.get("sender_id", "")
base["message"] = self._convert_message_segments(raw.get("content", ""))
base["alt_message"] = raw.get("content", "")
if raw.get("group_id"):
base["group_id"] = raw["group_id"]
return base
def _convert_message_segments(self, content: str) -> list:
segments = []
if content:
segments.append({"type": "text", "data": {"text": content}})
return segments
def _convert_notice(self, raw: dict, base: dict) -> dict:
base["type"] = "notice"
notification_type = raw.get("notification_type", "")
if notification_type == "member_join":
base["detail_type"] = "group_member_increase"
base["user_id"] = raw.get("user_id", "")
base["group_id"] = raw.get("group_id", "")
base["operator_id"] = raw.get("operator_id", "")
elif notification_type == "friend_add":
base["detail_type"] = "friend_increase"
base["user_id"] = raw.get("user_id", "")
return base
def _convert_request(self, raw: dict, base: dict) -> dict:
base["type"] = "request"
request_type = raw.get("request_type", "")
if request_type == "friend":
base["detail_type"] = "friend"
base["user_id"] = raw.get("user_id", "")
base["comment"] = raw.get("message", "")
elif request_type == "group_invite":
base["detail_type"] = "group"
base["group_id"] = raw.get("group_id", "")
base["user_id"] = raw.get("inviter_id", "")
return base
Пример преобразования мультимедийного сообщения
Сообщения, отправленные на реальных платформах, обычно содержат мультимедийные элементы, такие как изображения, упоминания пользователей и ответы. Ниже приведён пример обработки _convert_message_segments для различных типов сообщений:
def _convert_message_segments(self, raw_content: list) -> list:
"""Преобразует список сегментов сообщений платформы в стандартный формат OneBot12"""
segments = []
for item in raw_content:
item_type = item.get("type", "")
if item_type == "text":
segments.append({
"type": "text",
"data": {"text": item.get("content", "")}
})
elif item_type == "image":
file_url = item.get("url") or item.get("file_id", "")
segments.append({
"type": "image",
"data": {"file": file_url}
})
elif item_type == "at":
segments.append({
"type": "mention",
"data": {"user_id": item.get("target_id", "")}
})
elif item_type == "reply":
segments.append({
"type": "reply",
"data": {"message_id": item.get("reply_to_id", "")}
})
elif item_type == "at_all":
segments.append({"type": "mention_all", "data": {}})
else:
segments.append({
"type": "text",
"data": {"text": f"[Неподдерживаемый тип сообщения: {item_type}]"}
})
return segments
Common Pitfalls
1. Missing {platform}_raw Field
This is the most common mistake. Missing the raw data field prevents the module from accessing platform-specific information.
base_event["myplatform_raw"] = raw_event # Required!
base_event["myplatform_raw_type"] = event_type # Required!
2. Incorrect Timestamp Format
OneBot12 standard requires the time field to be a Unix timestamp in seconds (integer). If your platform returns a millisecond timestamp or an ISO-formatted string, you need to convert it:
import time
# Milliseconds → Seconds
"time": raw_event.get("timestamp", 0) // 1000
# ISO string → Seconds
"time": int(time.mktime(time.strptime(raw_event["created_at"], "%Y-%m-%dT%H:%M:%S")))
3. Missing self Field
The self field contains information about the bot itself, with user_id being the bot's account ID. This field is crucial in multi-bot scenarios:
"self": {
"platform": self.platform,
"user_id": raw_event.get("bot_id", ""), # ID of the bot itself
}
4. Using Non-Standard detail_type Values
The detail_type must use values defined by the OneBot12 standard, such as private, group, friend_increase, group_member_increase, etc. Do not use platform-specific naming.
5. Round-Trip Consistency
Ensure that the message segment types generated by the Converter correspond to the methods supported by the Send endpoint. For example, if the Converter converts the platform's image message to {"type": "image", ...}, then the Image() method in the Send endpoint must be able to handle image sending.
Рекомендуемые практики
- Всегда сохраняйте исходные данные: поле
{platform}_rawне должно быть опущено - Используйте стандартные сообщения: по возможности преобразуйте сообщения платформы в стандартные сообщения OneBot12
- Рационально настройте detail_type: используйте стандартные типы (
private/group/channelи т.д.), не создавайте собственные - Обрабатывайте граничные случаи: исходное событие может не содержать некоторых полей, используйте
.get()и задавайте разумные значения по умолчанию - Учитывайте производительность:
convert()вызывается для каждого события, избегайте выполнения длительных операций внутри него
Связанные документы
Продолжение пути разработки адаптера
- Основные концепции адаптера - Общая архитектура адаптера
- Подробности SendDSL - Обратное преобразование (направление отправки)
Следование официальным стандартам
- Стандарт преобразования событий - Официальный стандарт преобразования событий
- Система типов сессий - Правила сопоставления типов сессий
适配器最佳实践
来源:
docs/ru/developer-guide/adapters/best-practices.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/developer-guide/adapters/best-practices.md)
Лучшие практики разработки адаптеров
Tip
Когда стоит читать этот документ: рекомендуется к прочтению после того, как скелет адаптера заработал (инженерные лучшие практики). Если это не нужно — пропустите, это не повлияет на использование остальных функций.
Здесь собраны инженерные соглашения при разработке адаптеров: отчёты о состоянии ботов, повторные попытки подключения и heartbeat, разрешение мультиаккаунтов, стандартизированные ответы об ошибках — следование им помогает избежать типичных проблем при интеграции с платформами: незаметных отключений, отправки сообщений не тому адресату при нескольких аккаунтах, трудностей с локализацией ошибок.
Управление состоянием ботов и Meta-события
Адаптер должен активно отправлять meta-события через adapter.emit(), чтобы фреймворк автоматически отслеживал состояние подключения бота, его вход/выход из сети и информацию heartbeat.
1. Когда отправлять Meta-события
| Событие | detail_type |
Когда срабатывает | Поведение фреймворка |
|---|---|---|---|
| Подключение | "connect" |
При установлении соединения бота с платформой | Регистрирует бота, вызывает событие жизненного цикла adapter.bot.online |
| Отключение | "disconnect" |
При разрыве соединения бота с платформой | Помечает бота как офлайн, вызывает событие жизненного цикла adapter.bot.offline |
| Heartbeat | "heartbeat" |
Периодически (рекомендуется 30-60 секунд) | Обновляет время активности бота и метаданные |
2. Отправка Meta-событий
Фреймворк предоставляет метод emit_meta(), позволяющий отправить meta-событие одной строкой:
class MyAdapter(BaseAdapter):
async def _ws_handler(self, websocket):
bot_id = self._get_bot_id()
# Бот в сети: отправка события connect одной строкой
await self.emit_meta("connect", bot_id, user_name="MyBot", nickname="Мой бот")
try:
while True:
data = await websocket.receive_text()
event = self.convert(data)
if event:
await self.adapter.emit(event)
except WebSocketDisconnect:
pass
finally:
# Бот не в сети
await self.emit_meta("disconnect", bot_id)
Также можно вручную сконструировать стандартное meta-событие (старый способ остаётся совместимым):
await self.adapter.emit({
"id": str(uuid4()),
"time": int(time.time()),
"type": "meta",
"detail_type": "connect",
"platform": "myplatform",
"self": {
"platform": "myplatform",
"user_id": bot_id,
"user_name": "MyBot",
"avatar": "https://example.com/avatar.jpg",
},
"myplatform_raw": raw_data,
"myplatform_raw_type": "connected",
})
3. События heartbeat
Адаптер должен периодически отправлять события heartbeat, пока соединение активно, обновляя время активности бота:
class MyAdapter(BaseAdapter):
async def _heartbeat_loop(self, bot_id: str):
while self._connected:
# Отправка meta heartbeat фреймворку (одной строкой)
await self.emit_meta("heartbeat", bot_id)
await asyncio.sleep(30)
4. Автоматическое обнаружение поля self
adapter.emit() фреймворка автоматически обрабатывает поле self во всех событиях (не только в meta-событиях):
- В обычных событиях (message/notice/request) поле
selfавтоматически обнаруживается и регистрирует бота - Расширенная информация в поле
self: поддерживаются необязательные поляuser_name,nickname,avatar,account_id
# Наличие поля self в конвертере достаточно для автоматической регистрации бота
onebot_event = {
"type": "message",
"detail_type": "private",
"platform": "myplatform",
"self": {
"platform": "myplatform",
"user_id": "bot123",
"user_name": "MyBot",
"nickname": "Мой бот",
},
# ... другие поля
}
await self.adapter.emit(onebot_event)
# Бот "bot123" автоматически зарегистрирован, время активности обновлено
5. Запрос состояния бота
API запросов (get_bot_info() / list_bots() / is_bot_online() / get_status_summary()) и таблицу событий жизненного цикла бота см. в API системы адаптеров - Управление состоянием ботов.
Управление подключением
1. Реализация повторных попыток подключения
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()
self.logger.info("Подключение успешно")
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} с ({retry_count}/{max_retries}): {e}"
)
await asyncio.sleep(wait_time)
else:
self.logger.error("Не удалось подключиться: достигнуто максимальное число попыток")
raise
2. Управление состоянием подключения
class MyAdapter(BaseAdapter):
async def start(self):
self.connection = None
self._connected = False
async def _ws_handler(self, websocket: WebSocket):
self.connection = websocket
self._connected = True
self.logger.info("Соединение установлено")
try:
while True:
data = await websocket.receive_text()
await self._process_event(data)
except WebSocketDisconnect:
self.logger.info("Соединение разорвано")
finally:
self.connection = None
self._connected = False
3. Поддержание соединения heartbeat и Meta-heartbeat
Heartbeat адаптера должен решать две задачи одновременно: отправлять платформе heartbeat для поддержания соединения и отправлять фреймворку meta-событие heartbeat.
class MyAdapter(BaseAdapter):
async def start(self):
self.connection = await self._connect_to_platform()
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
async def _heartbeat_loop(self):
while self.connection:
try:
# 1. Отправка heartbeat платформе для поддержания соединения
await self.connection.send_json({"type": "ping"})
# 2. Отправка meta heartbeat фреймворку (одной строкой через emit_meta)
await self.emit_meta("heartbeat", self._bot_id)
await asyncio.sleep(30)
except Exception as e:
self.logger.error(f"Ошибка heartbeat: {e}")
break
4. Предоставление информации о подключении
Маршруты, зарегистрированные адаптером, должны быть видимы пользователю — это упрощает настройку адреса обратного вызова на стороне платформы. Рекомендуется активно выводить информацию о подключении в start():
class MyAdapter(BaseAdapter):
async def start(self):
router.register_websocket(
module_name=self.platform,
path="/ws",
handler=self._ws_handler
)
if self.sdk:
info = self.sdk.adapter.get_connection_info(self.platform)
if info:
self.logger.info(f"WebSocket-адрес: "
f"{info.get('connection', {}).get('base_url', '')}"
f"{info.get('connection', {}).get('websocket_routes', [])}")
Пользователи могут использовать следующие API для просмотра всех маршрутов и адресов подключения адаптера:
from ErisPulse import sdk
# Информация о подключении на уровне адаптера (рекомендуется)
info = sdk.adapter.get_connection_info("myplatform")
# Запросы на уровне менеджера маршрутов
sdk.router.list_namespaces() # Список всех пространств имён
sdk.router.get_module_routes("myplatform") # Подробная информация о маршрутах
sdk.router.get_module_urls("myplatform") # Полные URL подключения
Примечание:
module_name, указанный при регистрации маршрута, должен полностью совпадать с именемplatform, под которым адаптер зарегистрирован в ErisPulse, иначеget_connection_info()не сможет связать маршруты. Мультиаккаунтные адаптеры должны регистрировать подпути для каждого аккаунта (например,/account1/webhook,/account2/webhook), а не использовать разныеmodule_name.
Преобразование событий
1. Строгое следование стандарту OneBot12
class MyPlatformConverter:
def convert(self, raw_event):
"""Преобразование события"""
onebot_event = {
"id": str(raw_event.get("event_id", uuid.uuid4())),
"time": int(time.time()),
"type": self._convert_type(raw_event.get("type")),
"detail_type": self._convert_detail_type(raw_event),
"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
2. Стандартизация временных меток
def _convert_timestamp(self, timestamp):
"""Преобразование в 10-значную временную метку в секундах"""
if not timestamp:
return int(time.time())
# Если временная метка в миллисекундах
if timestamp > 10**12:
return int(timestamp / 1000)
# Если временная метка в секундах
return int(timestamp)
3. Генерация ID событий
import uuid
def _generate_event_id(self, raw_event):
"""Генерация ID события"""
event_id = raw_event.get("event_id")
if event_id:
return str(event_id)
# Если платформа не предоставила ID, генерируется UUID
return str(uuid.uuid4())
Реализация SendDSL
Модификаторы At/AtAll/Reply уже встроены в базовый класс SendDSL фреймворка — адаптеру достаточно реализовать Raw_ob12 и конкретные методы отправки. Используйте self._apply_modifiers(message) и self.send_context для упрощения разработки.
1. Обязательный возврат объекта Task
class Send(BaseAdapter.Send):
def Raw_ob12(self, message, **kwargs):
"""Рекомендуемая реализация: использование вспомогательных методов фреймворка"""
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())
def Text(self, text: str):
return self.Raw_ob12([{"type": "text", "data": {"text": text}}])
2. Цепочные методы-модификаторы возвращают self
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 # возвращает self
3. Поддержка специфичных для платформы методов
class Send(BaseAdapter.Send):
def Sticker(self, sticker_id: str):
"""Отправка стикера"""
return asyncio.create_task(
self._adapter.call_api(
endpoint="/send_sticker",
message=[{"type": "sticker", "data": {"id": sticker_id}}],
**self.send_context
)
)
def Card(self, card_data: dict):
"""Отправка карточного сообщения"""
return asyncio.create_task(
self._adapter.call_api(
endpoint="/send_card",
message=[{"type": "card", "data": card_data}],
**self.send_context
)
)
Ответы API
1. Стандартизированный формат ответов
Фреймворк предоставляет методы make_response() и make_error() для построения стандартизированных ответов:
async def call_api(self, endpoint: str, **params):
try:
raw_response = await self._platform_api_call(endpoint, **params)
if raw_response.get("success"):
return self.make_response(
data=raw_response.get("data"),
message_id=raw_response.get("data", {}).get("message_id", ""),
raw=raw_response,
)
else:
return self.make_error(
retcode=raw_response.get("code", 10001),
message=raw_response.get("message", ""),
raw=raw_response,
)
except Exception as e:
return self.make_error(message=str(e))
make_response() автоматически генерирует словарь ответа с ключом {platform}_raw. make_error() по умолчанию использует retcode=34000 (Platform Error).
2. Спецификация кодов ошибок
Следуйте стандартным кодам ошибок OneBot12:
# 1xxxx - Ошибки запроса действия
10001: Bad Request
10002: Unsupported Action
10003: Bad Param
# 2xxxx - Ошибки обработчика действия
20001: Bad Handler
20002: Internal Handler Error
# 3xxxx - Ошибки выполнения действия
31000: Database Error
32000: Filesystem Error
33000: Network Error
34000: Platform Error
35000: Logic Error
Поддержка нескольких аккаунтов
1. Декларативная конфигурация (рекомендуется)
После объявления класса конфигурации через AccountConfigClass фреймворк автоматически управляет загрузкой нескольких аккаунтов, их валидацией и генерацией шаблонов. Базовый класс BotAccountConfig предоставляет поля enabled и name, поэтому адаптеру не нужно объявлять их самостоятельно:
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BotAccountConfig
@dataclass
class MyBotConfig(BotAccountConfig):
token: str = field(default="", metadata={
"description": {"i18n": "my_adapter.bot_token", "default": "Bot Token"},
"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}")
await self._connect(name, account.token)
# bot_id автоматически получается фреймворком из протокола платформы/ответа на авторизацию и заполняется обратно
async def call_api(self, endpoint: str, **params):
account_id = params.pop("account_id", None)
name, account = self._resolve_account(account_id)
# name: имя аккаунта, account: экземпляр MyBotConfig
Файл конфигурации генерируется автоматически:
[MyAdapter.accounts.default]
token = ""
enabled = true
name = ""
2. Механизм выбора аккаунта
Во фреймворк встроен метод _resolve_account() со следующими приоритетами сопоставления:
- Имя аккаунта — точное совпадение с ключом конфигурации
- Поле
bot_id— автоматически полученный bot_id (т.е.event["self"]["user_id"]) - Любое str-поле — другие строковые поля в конфигурации
- Запасной вариант — первый включённый аккаунт
# Сопоставление по имени аккаунта
name, account = self._resolve_account("account1")
# Сопоставление по bot_id (самый распространённый способ, берётся из события)
name, account = self._resolve_account("bot_123")
# Получение первого включённого аккаунта (передаётся None)
name, account = self._resolve_account(None)
Обработка ошибок
1. Классифицированная обработка исключений
Используйте make_error() для построения стандартизированных ответов об ошибках. При выполнении запросов через sdk.client перехватывайте исключения ErisPulse:
from ErisPulse.Core.Bases.errors import ClientError, ClientTimeoutError
async def call_api(self, endpoint: str, **params):
try:
from ErisPulse.Core import client
resp = await client.post(
f"https://api.platform.com/{endpoint}",
json=params,
max_retries=2,
)
response = await resp.json()
return self.make_response(data=response, raw=response)
except ClientTimeoutError:
self.logger.error(f"Таймаут запроса: {endpoint}")
return self.make_error(retcode=32000, message="Таймаут запроса")
except ClientError as e:
self.logger.error(f"Сетевая ошибка: {e}")
return self.make_error(retcode=33000, message="Сбой сетевого запроса")
except json.JSONDecodeError:
self.logger.error("Ошибка разбора JSON")
return self.make_error(retcode=10006, message="Неверный формат ответа")
except Exception as e:
self.logger.error(f"Неизвестная ошибка: {e}", exc_info=True)
return self.make_error(message=str(e))
Обратная совместимость: старый код адаптеров, напрямую использующий
aiohttp, не затрагивается — по-прежнему можно перехватыватьaiohttp.ClientError. Преобразование исключений действует только при запросах черезsdk.client.
2. Логирование
Фреймворк автоматически создаёт дочерний logger для адаптера (sdk.logger.get_child("MyAdapter")), ручная инициализация не требуется:
class MyAdapter(BaseAdapter):
# ConfigClass = ... # после объявления класса конфигурации self.logger доступен автоматически
async def start(self):
self.logger.info("Запуск адаптера...")
# ...
self.logger.info("Адаптер запущен")
async def shutdown(self):
self.logger.info("Остановка адаптера...")
# ...
self.logger.info("Адаптер остановлен")
Тестирование
1. Модульные тесты
import pytest
from ErisPulse.Core.Bases import BaseAdapter
class TestMyAdapter:
def test_converter(self):
"""Тестирование конвертера"""
converter = MyPlatformConverter()
raw_event = {"type": "message", "content": "Hello"}
result = converter.convert(raw_event)
assert result is not None
assert result["platform"] == "myplatform"
assert "myplatform_raw" in result
def test_api_response(self):
"""Тестирование формата ответа API"""
adapter = MyAdapter()
response = adapter.call_api("/test", param="value")
assert "status" in response
assert "retcode" in response
2. Интеграционные тесты
@pytest.mark.asyncio
async def test_adapter_start():
"""Тестирование запуска адаптера"""
adapter = MyAdapter()
await adapter.start()
assert adapter._connected is True
@pytest.mark.asyncio
async def test_send_message():
"""Тестирование отправки сообщений"""
adapter = MyAdapter()
await adapter.start()
result = await adapter.Send.To("user", "123").Text("Hello")
assert result is not None
Обратное преобразование и построение сообщений
Raw_ob12 — метод, который адаптер обязан реализовать: это единая точка входа обратного преобразования (OneBot12 → платформа). Стандартные методы (Text, Image и т.д.) должны делегировать в Raw_ob12, а состояние модификаторов (At/Reply/AtAll) должно объединяться в сегменты сообщения внутри Raw_ob12.
MessageBuilder — инструмент построения сегментов сообщений, используемый вместе с Raw_ob12; поддерживает цепочные вызовы и быстрое построение.
Полную спецификацию реализации, примеры кода и инструкции по использованию см. в:
Расширение методами событий платформы
Адаптер может регистрировать специфичные для платформы методы у классов-обёрток Event, чтобы разработчики модулей могли удобнее обращаться к специфичным данным платформы.
1. Массовая регистрация с помощью Mixin-класса (рекомендуется)
Если у платформы несколько специфичных методов, рекомендуется использовать Mixin-класс:
# Регистрация в start() адаптера или на уровне модуля
from ErisPulse.Core.Event import register_event_mixin
class MyPlatformEventMixin:
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)
def get_message_type(self):
"""Получение типа сообщения платформы"""
return self.get("myplatform_raw", {}).get("msg_type", "text")
# Массовая регистрация
register_event_mixin("myplatform", MyPlatformEventMixin)
2. Регистрация отдельного метода с помощью декоратора
from ErisPulse.Core.Event import register_event_method
@register_event_method("myplatform")
def get_chat_name(self):
return self.get("myplatform_raw", {}).get("chat", {}).get("name", "")
3. Очистка при остановке адаптера
from ErisPulse.Core.Event import unregister_platform_event_methods
class MyAdapter(BaseAdapter):
async def shutdown(self):
# Очистка регистрации методов событий платформы
unregister_platform_event_methods("myplatform")
# ... остальная очистка
Более подробные сведения о регистрации и отмене регистрации см. в API системы событий - Регистрация методов расширения платформы.
Поддержка документации
1. Поддержка документации по особенностям платформы
Создайте документ {platform}.md в каталоге docs/ru/platform-guide/ (версии на других языках будут сгенерированы автоматически):
# Документация адаптера платформы
## Основная информация
- Версия соответствующего модуля: 1.0.0
- Сопровождающий: Ваше имя
## Поддерживаемые типы отправляемых сообщений
...
## Специфичные типы событий
...
## Параметры конфигурации
...
2. Обновление информации о версии
При выпуске новой версии обновите информацию о версии в документации:
[project]
version = "2.0.0" # обновить номер версии
Связанная документация
Предварительные знания
- Введение в разработку адаптеров — создание первого адаптера
- Основные концепции адаптеров — знакомство с архитектурой адаптеров
- Подробно о SendDSL — изучение отправки сообщений
После доработки адаптера
- Руководство по публикации и магазину модулей — публикация на PyPI и в магазине модулей
平台接入指南索引
来源:
docs/ru/platform-guide/README.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/platform-guide/README.md)
Документация по функциям платформы ErisPulse
Базовый протокол: OneBot12
В этом документе содержится руководство по платформенно-специфичным функциям, включающее:
- Примеры цепочки вызовов метода Send для каждого адаптера
- Объяснение платформенно-специфичных форматов событий/сообщений
Общие методы использования см. в:
Платформо-специфичные функции
Эта часть поддерживается разработчиками адаптеров и предназначена для описания отличий и расширений адаптера по сравнению со стандартом OneBot12. Пожалуйста, ознакомьтесь с подробной документацией для каждой платформы:
Кроме того, существует адаптер
sandbox, но для него не требуется документация по платформо-специфичным функциям
Общие интерфейсы
Chain-вызов Send
Все адаптеры поддерживают следующий стандартный способ вызова:
Важно:
{AdapterName}в документации нужно заменить на фактическое имя адаптера (например,yunhu,telegram,onebot11,
- Указание типа и идентификатора:
To(type,id).Func()# Получение экземпляра адаптера my_adapter = adapter.get("{AdapterName}") # Отправка сообщения await my_adapter.Send.To("user", "U1001").Text("Hello") # Например: yunhu = adapter.get("yunhu") await yunhu.Send.To("user", "U1001").Text("Hello") - Только указание идентификатора:
To(id).Func()my_adapter = adapter.get("{AdapterName}") await my_adapter.Send.To("U1001").Text("Hello") # Например: telegram = adapter.get("telegram") await telegram.Send.To("U1001").Text("Hello") - Указание отправляющего аккаунта:
Using(account_id)my_adapter = adapter.get("{AdapterName}") await my_adapter.Send.Using("bot1").To("U1001").Text("Hello") # Например: onebot11 = adapter.get("onebot11") await onebot11.Send.Using("bot1").To("U1001").Text("Hello") - Прямой вызов:
Func()my_adapter = adapter.get("{AdapterName}") await my_adapter.Send.Text("Broadcast message") # Например: email = adapter.get("email") await email.Send.Text("Broadcast message")
Асинхронная отправка и обработка результатов
Методы Send DSL возвращают объект asyncio.Task, что означает, что вы можете выбрать, нужно ли немедленно ожидать результат:
# Получение экземпляра адаптера
my_adapter = adapter.get("{AdapterName}")
# Не ждать результата, сообщение отправляется в фоне
task = my_adapter.Send.To("user", "123").Text("Hello")
# Если нужно получить результат отправки, можно ожидать позже
result = await task
Декораторы правил отправки
В реальном разработке часто требуется: выполнять последующую логику только после успешной отправки, автоматически повторять отправку при сбое, отменять при таймауте, отслеживать прогресс отправки и т.д. В Send DSL встроена система декораторов правил отправки, которые можно добавлять цепочкой методов:
| Метод | Описание |
|---|---|
.Hook(callback) |
Выполняется после успешной отправки (можно вызывать несколько раз) |
.Retry(times=1) |
Автоматически повторяет отправку N раз (включая первую, всего N+1 раз) |
.Timeout(seconds) |
Останавливает отправку при превышении времени (можно использовать вместе с Retry) |
.Defer(seconds) |
Откладывает отправку (внутрипроцессное таймерное ожидание, не сохраняется) |
.OnProgress(callback) |
Вызывает обратный вызов на каждом этапе, передаёт SendContext |
.OnError(callback) |
Вызывается при окончательном сбое (вызывается только один раз) |
yunhu = adapter.get("yunhu")
# Вычитание баллов только после успешной отправки
await (yunhu.Send.To("user", "123")
.Hook(lambda r: deduct_points("123"))
.Text("Успешная покупка"))
# Повтор отправки + отмена при таймауте + отслеживание прогресса
def on_progress(ctx):
print(f"Этап: {ctx.stage}, Попытка: {ctx.attempt + 1}/{ctx.max_attempts}")
task = (yunhu.Send.To("user", "123")
.Retry(3) # Повторяет до 3 раз
.Timeout(10) # Каждая отправка имеет таймаут 10 секунд
.OnProgress(on_progress)
.OnError(lambda ctx: notify_admin(ctx.error))
.Text("Важное уведомление"))
Методы правил возвращают self, и их необходимо вызывать до отправки методов (Text/Image и т.д.). SendContext содержит поля stage (pending/sending/retrying/success/failed/timeout), attempt, elapsed, error, result и т.д., что полезно для отслеживания.
Режим построения пакетов (Build)
В одной цепочке можно построить несколько методов отправки, а затем выполнить их все сразу. Подходит для сценариев "отправить несколько сообщений за один раз":
yunhu = adapter.get("yunhu")
# Строим несколько сообщений и отправляем их одновременно
results = await (yunhu.Send.To("user", "123")
.Build() # Переходим в режим построения
.Text("Уведомление 1")
.Image("pic.jpg")
.Text("Уведомление 2")
.send_all()) # Выполняем все сообщения
# results = [результат Text, результат Image, результат Text]
.send_all() по умолчанию выполняется параллельно (параллельная отправка, высокая эффективность). Если нужно гарантировать порядок доставки, вызовите .Sequential() для последовательного выполнения:
# Последовательное выполнение (гарантирует порядок) + повтор при сбое
await (yunhu.Send.To("group", "456")
.Build()
.Sequential() # Отправка по порядку
.Retry(2) # Каждая неудачная отправка будет повторена
.Text("Первое сообщение").Text("Второе сообщение")
.send_all())
Пакетная отправка использует стратегию "продолжать при сбое": если одна отправка не удалась, это не прерывает другие, и неудачные сообщения автоматически повторяются. Пакетная отправка также поддерживает Hook для всех успешных отправок, OnError при наличии ошибок и OnProgress для обратного вызова о прогрессе.
Подробное описание правил и построения пакетов см. в разделе SendDSL详解.
Обработка событий
Существует три способа обработки событий:
Обработка событий на уровне платформы:
from ErisPulse.Core import adapter, logger @adapter.on("event_type", raw=True, platform="{AdapterName}") async def handler(data): logger.info(f"Получено событие от {AdapterName}: {data}")Обработка стандартных событий OneBot12:
from ErisPulse.Core import adapter, logger # Обработка стандартного события OneBot12 @adapter.on("event_type") async def handler(data): logger.info(f"Получено стандартное событие: {data}") # Обработка стандартного события для конкретной платформы @adapter.on("event_type", platform="{AdapterName}") async def handler(data): logger.info(f"Получено событие от {AdapterName}: {data}")Обработка событий через модуль
Event: События в модулеEventоснованы на функцииadapter.on(), поэтому формат событий, предоставляемыхEvent, соответствует стандарту OneBot12.from ErisPulse.Core.Event import message, notice, request, command message.on_message()(message_handler) notice.on_notice()(notice_handler) request.on_request()(request_handler) command("hello", help="Отправить приветственное сообщение", usage="hello")(command_handler) async def message_handler(event): logger.info(f"Получено сообщение: {event}") async def notice_handler(event): logger.info(f"Получено уведомление: {event}") async def request_handler(event): logger.info(f"Получен запрос: {event}") async def command_handler(event): logger.info(f"Получена команда: {event}")
Наиболее рекомендуемый способ обработки событий — использование модуля Event, поскольку он предоставляет богатый набор типов событий и методов обработки.
Стандартный формат
Для удобства справки здесь приведён простой формат событий. Для получения подробной информации, пожалуйста, обратитесь к ссылкам выше.
Примечание: Ниже представлен базовый стандартный формат OneBot12, который может быть дополнен адаптерами. Для получения подробной информации, пожалуйста, обратитесь к описанию специфических функций каждого адаптера.
Стандартный формат событий
Формат преобразования событий, который должен реализовать каждый адаптер:
{
"id": "event_123",
"time": 1752241220,
"type": "message",
"detail_type": "group",
"platform": "example_platform",
"self": {"platform": "example_platform", "user_id": "bot_123"},
"message_id": "msg_abc",
"message": [
{"type": "text", "data": {"text": "Привет"}}
],
"alt_message": "Привет",
"user_id": "user_456",
"user_nickname": "ExampleUser",
"group_id": "group_789"
}
Стандартный формат ответа
Успешная отправка сообщения
{
"status": "ok",
"retcode": 0,
"data": {
"message_id": "1234",
"time": 1632847927.599013
},
"message_id": "1234",
"message": "",
"echo": "1234",
"{platform}_raw": {...}
}
Неудачная отправка сообщения
{
"status": "failed",
"retcode": 10003,
"data": null,
"message_id": "",
"message": "Отсутствуют необходимые параметры",
"echo": "1234",
"{platform}_raw": {...}
}
Ссылки
Проект ErisPulse:
- Основной репозиторий
- Библиотека адаптера Yunhu
- Библиотека адаптера Telegram
- Библиотека адаптера OneBot
Связанная официальная документация:
- Официальная документация протокола OneBot V11
- Официальная документация Telegram Bot API
- Официальная документация Yunhu
Участие в разработке
Мы приветствуем больше разработчиков, желающих участвовать в написании и поддержке документации адаптеров! Пожалуйста, выполните следующие шаги, чтобы отправить свой вклад:
- Сделайте форк репозитория ErisPuls.
- Создайте файл Markdown в каталоге
docs/platform-features/и назовите его в формате<platform-name>.md. - Добавьте ссылку на ваш вклад в адаптер и соответствующую официальную документацию в этот файл
README.md. - Отправьте Pull Request.
Благодарим за вашу поддержку!