简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

name: erispulse-adapter-standards description: "适用于:实现/评审适配器时需要对照的硬性技术规范。覆盖场景:когда вы разрабатываете новый адаптер, добавляете новую возможность для какой-либо платформы, вам нужно решить «включать в стандарт или оформлять как расширение {platform}_», как придумывать имена, как обрабатывать различия между платформами — сначала прочитайте этот документ. Если нужно лишь уточнить конкретную спецификацию по какой-то области, можно сразу перейти к соответствующему документу из карты стандартов в §2.。Когда нужно читать эту статью: когда вы адаптируете платформу, сопоставляя нативные сессии с платформы с типами (private/group/channel/guild/thread), или в модуле, если не уверены, какой тип цели нужно передавать в Send.To(), или хотите понять, как происходит автоматическое преобразование event.reply(). Если не нужно, можете пропустить.。Когда нужно читать эту статью: когда вы конвертируете нативные события платформы в стандартные события OneBot12 в адаптере — проверьте обязательные поля, структуру сегментов сообщений, именование расширенных полей (..."

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 按需读取:只读需要的章节即可,不必全文加载。

标准化总纲

来源: docs/ru/standards/standardization-guide.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/standardization-guide.md)

Руководство по стандартизации адаптеров (генеральное руководство)

Tip

Когда читать этот документ: когда вы разрабатываете новый адаптер, добавляете новую возможность для какой-либо платформы, вам нужно решить «включать в стандарт или оформлять как расширение {platform}_», как придумывать имена, как обрабатывать различия между платформами — сначала прочитайте этот документ. Если нужно лишь уточнить конкретную спецификацию по какой-то области, можно сразу перейти к соответствующему документу из карты стандартов в §2.

Настоящий документ является генеральным руководством по стандартизации адаптеров ErisPulse: он устанавливает принципы кроссплатформенной стандартизации, карту стандартов, паттерны обработки различий, процесс включения новых возможностей в стандарт, а также даёт полные определения стандарта интерактивных компонентов (кнопки/клавиатуры, выпадающие списки и т.д.) и интерактивных событий обратного вызова. Любой разработчик адаптеров при реализации новой функциональности должен в первую очередь использовать стандартные определения, чтобы разработчики модулей получали одинаковый опыт на любой платформе с одним и тем же кодом — одинаковые имена, одинаковые параметры, одинаковые возвращаемые значения.


1. Принципы стандартизации

  1. Единообразие имён: одно и то же понятие на всех платформах использует одно и то же имя (например, отзыв сообщения единообразно delete_message, сегмент кнопок единообразно keyboard), независимо от платформы
  2. Единообразие параметров: структура параметров стандартных действий/сегментов/методов одинакова на всех платформах; платформенно-специфичные параметры предоставляются как необязательные расширяемые параметры или поля расширения, не загрязняя стандартную сигнатуру
  3. Единообразие возвращаемых значений: все вызовы API/отправки возвращают стандартную структуру ответа (status/retcode/data/message_id/message), стандартные поля внутри data (такие как user_id/user_name) имеют одинаковую семантику; исходные данные платформы помещаются в {platform}_raw
  4. Обоснованные расширения: платформенно-специфичные возможности именуются с префиксом {platform}_ (сегменты: yunhu_form; действия: yunhu.board; поля событий: qqbot_button_id), явно обозначая их некроссплатформенность
  5. Различия поглощаются на уровне адаптера: код модулей программируется ориентированно на стандарт, различия платформ преобразуются адаптером (маппинг параметров, нормализация полей, деградация возможностей), от модулей не требуется писать платформенные ветвления
  6. Деградация возможностей без ошибок: когда платформа не поддерживает некоторую стандартную возможность, выполняется грациозная деградация (возврат retcode=10002, текстовизация компонента в alt_message), без выбрасывания исключений, прерывающих логику модуля

2. Карта стандартов

Область Документ стандарта Охватываемое содержание
Преобразование событий Стандарт преобразования событий Структура событий, стандартные сегменты сообщений (text/image/mention/reply/keyboard и др.), спецификации платформенных сегментов-расширений
Интерактивные компоненты §5 настоящего документа Кнопки/клавиатуры, выпадающие списки, карточки, стандартные поля интерактивных событий обратного вызова, соглашения о декораторах, маппинг для каждой платформы
API-действия Стандарт API-действий Унифицированный интерфейс стандартных действий OneBot12 (пользователи/группы/каналы/управление сообщениями/мета-действия) и ApiDSL
Операции с запросами Спецификация операций с запросами Поля событий запросов (request_id) и Request DSL (approve/reject)
Методы отправки Спецификация методов отправки Именование методов класса Send, параметры, декораторы, обратное преобразование (OB12→платформа)
Типы сессий Стандарт типов сессий Определения и маппинг типов сессий user/group/channel/guild/dms и др.
Ответы API Стандарт ответов API Стандартная структура ответа и соглашения о retcode

3. Рабочий процесс стандартизации (как новые возможности попадают в стандарт)

Платформенно-специфичная возможность (префикс {platform}_)
        │  одна и та же возможность появилась на ≥2 платформах
        ▼
Выявление общего (извлечение универсальных понятий и подмножества параметров)
        │
        ▼
Черновик стандарта (именование + параметры + возвращаемые значения + таблицы маппинга для каждой платформы)
        │  ревью
        ▼
Внесение в настоящее генеральное руководство / документы стандартов по областям + реализация слоя совместимости в базовом классе фреймворка/адаптерах
        │
        ▼
Стандартный сегмент/действие (без префикса) — доступно модулям кроссплатформенно

Пример: кнопки изначально были реализованы независимо на каждой платформе (telegram_inline_keyboard / кнопки Yunhu / QQBot keyboard) → появились на ≥3 платформах → извлечена общая структура (label/type/data + rows) → опубликован стандартный сегмент keyboard → каждый адаптер реализует слой совместимости (декораторы принимают общую структуру + преобразование стандартного сегмента + прозрачная передача нативных сегментов).

3.1 Правила именования

Объект Правило Пример
Стандартные сегменты сообщений нижний регистр, общие понятия без префикса keyboard, select, mention
Платформенные сегменты-расширения префикс {platform}_ telegram_sticker, yunhu_form
Стандартные API-действия стандартные имена OB12 (snake_case) get_group_info, delete_message
Платформенные действия-расширения префикс {platform}. или общее имя протокола yunhu.board, send_poke (расширение OB11)
Декораторы PascalCase, общие возможности входят в базовый класс фреймворка .Keyboard(rows), .At(uid)
Стандартные поля событий общие понятия без префикса interaction_id, button_data, request_id
Платформенные поля событий префикс {platform}_ qqbot_event_id, telegram_chat_id

3.2 Правила параметров и возвращаемых значений

4. Паттерны обработки различий (уровень адаптера)

Паттерн Описание Пример
Маппинг параметров стандартные параметры → нативные параметры платформы delete_message(message_id) → TG deleteMessage(chat_id, message_id) (chat_id дополняется из реестра)
Преобразование структур стандартный сегмент → нативная структура платформы сегмент keyboard → inline_keyboard / buttons / QQBot keyboard
Маппинг действий имя стандартного действия → имя действия платформы get_self_info → get_login_info (OB11)
Нормализация полей ответ платформы → стандартные поля getMe() → {user_id, user_name, user_displayname}
Синтез идентификаторов генерация детерминированного ID, когда платформа не имеет нативного идентификатора TG join request без ID → tjr_{chat}_{user}_{date}
Деградация возможностей при отсутствии поддержки — текстовизация/возврат 10002 Kook без keyboard → текстовизация в alt_message; get_friend_list → 10002
Нормализация «грязные» данные платформы → стандартный формат @-метка QQBot openid → bot_id (нормализация имён)
Двухрежимная совместимость одновременное принятие стандартной структуры и нативной структуры платформы .Keyboard() принимает общую структуру rows или нативную структуру

5. Стандарт интерактивных компонентов

5.1 Перечень компонентов и их состояние

Компонент type стандартного сегмента Статус Поддерживаемые платформы
Кнопки/клавиатура (keyboard) keyboard ✅ стандартизировано Telegram / Yunhu / QQBot
Выпадающий список (select) select 📋 зарезервировано (структура см. §5.4) Discord / Telegram(bot)
Карточка (card) card 📋 зарезервировано (см. §5.5) Kook / Yunhu(html)
Модальное окно (modal) modal 📋 зарезервировано Discord

Соглашение об уровнях декораторов: Send-декораторы универсальных интерактивных компонентов реализуются классом Send адаптера (не встроены в базовый класс фреймворка), именование следует соглашениям §5.2, параметры следуют стандартной структуре настоящего документа.

5.2 keyboard — кнопки/инлайн-клавиатура

Структура сегмента сообщения (направление отправки)

Стандартный сегмент keyboard = двумерный массив rows, каждая кнопка имеет вид {"label": "отображаемый текст", "type": "callback" | "link", "data": "данные обратного вызова или адрес перехода"}, также допускаются платформенно-специфичные необязательные поля (адаптер маппит по возможностям или игнорирует).

Эталонное определение структуры: полный JSON сегмента сообщения, таблица полей и семантика полей приведены в Стандарте преобразования событий §4.1.1 — сегмент keyboard (кнопки/инлайн-клавиатура), в настоящем разделе не повторяются.

Таблица маппинга по платформам

Платформа Стандартный сегмент → нативная структура Справочник нативной структуры
Telegram reply_markup.inline_keyboard: [{text, callback_data | url}] callback→callback_data (≤64 байт), link→url
Yunhu content.buttons: [{label, action_type}] callback→action_type:2 + action + value, link→action_type:1 + url
QQBot keyboard.content.rows: [{label, type, data}] callback→type:2 + data, link→type:0 + data (сообщение должно быть типа markdown)
Kook модуль action-group карточки: [{type, text, value, click}] callback→click:return + value, link→click:link + url
Discord components[].components: [{label, style, custom_id | url}] callback→style:1 + custom_id, link→style:5 + url

Send-декораторы (реализуются классом Send каждого адаптера)

Универсальные декораторы реализуются каждым адаптером в собственном классе Send (без изменения базового класса фреймворка):

# Один и тот же код — любая платформа (декораторы и преобразования предоставляются каждым адаптером)
rows = [[{"label": "Нравится", "type": "callback", "data": "like:1"},
         {"label": "Профиль", "type": "link", "data": "https://example.com"}]]
await adapter.Send.To("group", gid).Keyboard(rows).Text("Выберите")

Список совместимости адаптеров: QQBot / Telegram / Yunhu уже реализовано (декораторы + преобразование стандартного сегмента); новые адаптеры могут просто реализовать согласно настоящему документу (см. §7 Checklist).

5.3 Интерактивные события обратного вызова (после нажатия кнопки)

После нажатия кнопки пользователем событие, отправляемое платформой, обязано предоставлять следующие стандартные поля (detail_type может сохранять платформенное именование):

Стандартное поле Тип Обязательно Описание
interaction_id str да ID данного взаимодействия (можно использовать для ответа, например, спиннер/подсказка)
button_data str да Данные, возвращаемые кнопкой (то есть data из §5.2)
button_label str нет Отображаемый текст кнопки
user_id str да Нажавший пользователь
message_id str нет Сообщение, содержащее кнопку
group_id / channel_id str нет Сессия-источник

Соглашение о detail_type: сохраняются существующие имена каждой платформы (qqbot_interaction / telegram_callback_query / yunhu_a2ui_button и др.), но стандартные поля должны присутствовать полностью — модуль может получать значения кроссплатформенно с помощью event.get("button_data").

Рекомендуемые расширения Event (предоставляются EventMixin адаптера):

def get_button_data(self) -> str: ...     # button_data
def get_interaction_id(self) -> str: ...  # interaction_id

Ответ на взаимодействие (в зависимости от возможностей платформы): adapter.reply_interaction(interaction_id, code=0) (QQBot) / answerCallbackQuery (Telegram) и т.д.; именование следует семейству методов платформы, принудительная унификация не применяется.

Маппинг событий обратного вызова по платформам

Платформа Нативное событие detail_type Источник interaction_id Источник button_data
Telegram callback_query telegram_callback_query (notice) callback_query.id callback_query.data
Yunhu событие нажатия кнопки yunhu_button_click / yunhu_a2ui_button buttonId / sourceComponentId value / actionName
QQBot INTERACTION_CREATE qqbot_interaction interaction.id data.resolved.button_data
Kook событие нажатия кнопки kook_button_click msg_id+value value
Discord INTERACTION_CREATE discord_interaction interaction.id data.custom_id

5.4 select — выпадающий список (резерв)

{
  "type": "select",
  "data": {
    "placeholder": "Выберите",
    "options": [
      {"label": "Вариант A", "data": "opt:A"},
      {"label": "Вариант B", "data": "opt:B"}
    ],
    "min_values": 1,
    "max_values": 1
  }
}

События обратного вызова повторно используют поля §5.3 (button_data = выбранный data, при множественном выборе — JSON-массив). Платформы первой волны реализации: Discord (select menu), Telegram (переключение keyboard). Платформы без реализации, получив данный сегмент, должны деградировать его до текстового списка в alt_message.

5.5 card — карточки (резерв, стандартизация отложена)

Структуры карточек различаются чрезвычайно сильно (полнофункциональные карточные модули Kook vs html Yunhu vs QQ markdown+keyboard), пока не вводим жёсткий стандарт. Рекомендации:


6. Будущие кандидаты (Roadmap)

Следующие возможности уже появились или ожидаются на ≥2 платформах; стандартизация продвигается в порядке приоритета:

Кандидат Затронутые платформы Приоритет Примечания
select — выпадающий список Discord / Telegram высокий Черновик структуры см. §5.4
Реакции (reactions) QQBot / Telegram / Discord / Kook высокий Стандартизация обеих сторон: действие + событие
Действия управления группой (мут/кик/одобрение) QQBot / Yunhu / OB11 высокий Большинство уже реализовано как платформенные действия, предстоит извлечь стандартные сигнатуры
Объявления/доски Yunhu / Telegram / Discord средний Действия типа set_announcement
Модель файловых ресурсов (двухсегментный file_id) все платформы низкий (отложено) Прямая отправка SendDSL.File(file, filename) уже является стандартным путём (прямая передача URL/пути/bytes, см. Спецификацию методов отправки §2.1); модель file_id передаётся прозрачно только если бэкенд изначально её поддерживает, см. Стандарт API-действий §3.5
Карточки card Kook / Yunhu низкий Структуры сильно различаются, см. §5.5
Формы form Yunhu низкий Платформенно-специфично, сохраняется префикс {platform}_
Транскодирование медиа/определение размера все платформы низкий Реализуется внутри адаптера, наружу не стандартизируется

7. Стандартный чек-лист (Checklist) для разработчиков новых адаптеров

При разработке нового адаптера сверяйтесь с этим списком при реализации (★ — обязательно, остальное — рекомендуется):

8. Связанные документы

会话类型

来源: docs/ru/standards/session-types.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/session-types.md)

Стандарт типов сессий ErisPulse

Tip

Когда нужно читать эту статью: когда вы адаптируете платформу, сопоставляя нативные сессии с платформы с типами (private/group/channel/guild/thread), или в модуле, если не уверены, какой тип цели нужно передавать в Send.To(), или хотите понять, как происходит автоматическое преобразование event.reply(). Если не нужно, можете пропустить.

В этом документе определены стандартные типы сессий, поддерживаемые ErisPulse, включая типы событий получения и типы целей отправки.

1. Основные понятия

1.1 Типы получения и отправки

ErisPulse различает два типа сессий:

1.2 Отношения между типами

Тип получения (detail_type)     Тип отправки (Send.To)
─────────────────        ────────────────
private                 →        user
group                   →        group
channel                 →        channel
guild                   →        guild
thread                  →        thread
user                    →        user

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

2. Стандартные типы сессий

2.1 Стандартные типы OneBot12

private

group

user

2.2 Расширенные типы ErisPulse

channel

guild

thread

3. Сопоставление типов платформ

3.1 Принцип сопоставления

Адаптер отвечает за сопоставление нативных типов платформы со стандартными типами ErisPulse:

Нативный тип платформы → Стандартный тип ErisPulse → Тип отправки

3.2 Примеры сопоставления популярных платформ

Telegram

Тип Telegram          Тип получения ErisPulse    Тип отправки
─────────────────      ────────────────       ───────────
private                private                 user
group                  group                   group
supergroup             group                   group  # Сопоставляется с group
channel                channel                 channel

Discord

Тип Discord          Тип получения ErisPulse    Тип отправки
─────────────────      ────────────────       ───────────
Direct Message         private                user
Text Channel           channel                channel
Guild                  guild                  guild
Thread                 thread                 thread

OneBot11

Тип OneBot11        Тип получения ErisPulse    Тип отправки
─────────────────      ────────────────       ───────────
private                private                user
group                  group                  group
discuss                group                  group  # Сопоставляется с group

4. Расширение с помощью пользовательских типов

4.1 Регистрация пользовательских типов

Адаптер может зарегистрировать пользовательские типы сессий:

from ErisPulse.Core.Event import register_custom_type

# Регистрация пользовательского типа
register_custom_type(
    receive_type="my_custom_type",
    send_type="custom",
    id_field="custom_id",
    platform="MyPlatform"
)

4.2 Использование пользовательских типов

После регистрации система автоматически обрабатывает преобразование и вывод типа:

# Автоматический вывод
receive_type = infer_receive_type(event, platform="MyPlatform")
# Возвращает: "my_custom_type"

# Преобразование в тип отправки
send_type = convert_to_send_type(receive_type, platform="MyPlatform")
# Возвращает: "custom"

# Получение соответствующего ID
target_id = get_target_id(event, platform="MyPlatform")
# Возвращает: event["custom_id"]

4.3 Удаление пользовательских типов

from ErisPulse.Core.Event import unregister_custom_type

unregister_custom_type("my_custom_type", platform="MyPlatform")

5. Автоматическое определение типа

Если у события отсутствует явное поле detail_type, система автоматически определяет тип по доступным полям ID:

Note

Изменение поведения в 2.7.0+: detail_type используется только если это известный тип сессии (стандартный или пользовательский). У событий notice/request (например, group_member_increase, friend_increase) detail_type является семантическим подтипом, а не типом сессии, и система будет определять правильный тип сессии по полям ID.

5.1 Приоритет определения

Приоритет (от высокого к низкому):
1. group_id     → group
2. channel_id   → channel
3. guild_id     → guild
4. thread_id    → thread
5. user_id      → private

5.2 Примеры использования

# У события только group_id
event = {"group_id": "123", "user_id": "456"}
receive_type = infer_receive_type(event)
# Возвращает: "group" (используется приоритетный group_id)

# У события только user_id
event = {"user_id": "123"}
receive_type = infer_receive_type(event)
# Возвращает: "private"

# У события notice detail_type — семантический подтип, в 2.7.0+ определяется по полю ID
event = {"type": "notice", "detail_type": "group_member_increase", "group_id": "123"}
receive_type = infer_receive_type(event)
# Возвращает: "group" (а не "group_member_increase")

6. Примеры использования API

6.1 Отправка сообщений

from ErisPulse import adapter

# Отправка пользователю
await adapter.myplatform.Send.To("user", "123").Text("Hello")

# Отправка в группу
await adapter.myplatform.Send.To("group", "456").Text("Hello")

# Автоматическое преобразование private → user: использование приемлемого, но не рекомендуемого способа (может быть несовместимость)
await adapter.myplatform.Send.To("private", "789").Text("Hello")
# Внутри автоматически преобразуется в: Send.To("user", "789")
# Рекомендуемый способ: при ручной отправке используйте тип отправки user, или используйте event.reply() для автоматического преобразования (см. 6.2)

6.2 Ответ на событие

from ErisPulse.Core.Event import Event

# Event.reply() автоматически обрабатывает преобразование типов (рекомендуемый путь)
await event.reply("Сообщение ответа")
# Внутри автоматически используется правильный тип отправки (автоматическое преобразование private → user происходит здесь)

6.3 Обработка команд

from ErisPulse.Core.Event import command

@command(name="test")
async def handle_test(event):
    # Система автоматически обрабатывает тип сессии
    # Не нужно вручную проверять group_id или user_id
    await event.reply("Команда выполнена успешно")

7. Справочник основных API

7.1 Преобразование типов

from ErisPulse.Core.Event import convert_to_send_type, convert_to_receive_type

# Преобразование типа получения в тип отправки
convert_to_send_type("private")  # → "user"
convert_to_send_type("group")    # → "group"

# Преобразование типа отправки в тип получения
convert_to_receive_type("user")   # → "private"
convert_to_receive_type("group")  # → "group"

7.2 Получение полей ID

from ErisPulse.Core.Event import get_id_field, get_receive_type

get_id_field("group")    # → "group_id"
get_id_field("private")  # → "user_id"

get_receive_type("group_id")  # → "group"
get_receive_type("user_id")   # → "private"

7.3 Получение информации для отправки за один вызов

from ErisPulse.Core.Event import get_send_type_and_target_id

event = {"detail_type": "private", "user_id": "123"}
send_type, target_id = get_send_type_and_target_id(event)
# send_type = "user", target_id = "123"

# Непосредственно для использования в Send.To()
await adapter.Send.To(send_type, target_id).Text("Hello")

7.4 Получение целевого ID

from ErisPulse.Core.Event import get_target_id

event = {"detail_type": "group", "group_id": "456"}
get_target_id(event)  # → "456"

8. Вспомогательные методы

from ErisPulse.Core.Event import (
    is_standard_type,
    is_valid_send_type,
    get_standard_types,
    get_send_types,
    clear_custom_types,
)

is_standard_type("private")     # True
is_standard_type("custom_type") # False

is_valid_send_type("user")      # True
is_valid_send_type("invalid")   # False

get_standard_types()  # {"private", "group", "channel", "guild", "thread", "user"}
get_send_types()      # {"user", "group", "channel", "guild", "thread"}

clear_custom_types()                # Очистка всех пользовательских типов
clear_custom_types(platform="discord")  # Очистка пользовательских типов только для указанной платформы

9. Рекомендуемые практики

7.1 Разработчики адаптеров

  1. Используйте стандартные сопоставления: по возможности сопоставляйте с типами стандарта, а не создавайте новые типы
  2. Правильно преобразуйте: убедитесь, что сопоставление типов получения и отправки корректно
  3. Сохраняйте исходные данные: сохраняйте оригинальный тип события в {platform}_raw
  4. Документируйте: в документации адаптера укажите сопоставления типов

7.2 Разработчики модулей

  1. Используйте вспомогательные методы: используйте методы, такие как get_send_type_and_target_id()
  2. Избегайте жесткого кодирования: не пишите код вида if group_id else "private"
  3. Учитывайте все типы: код должен поддерживать все стандартные типы, а не только private/group
  4. Гибко проектируйте: используйте методы обертки событий, а не прямой доступ к полям

7.3 Определение типов

10. Часто задаваемые вопросы

В1: Почему при отправке private нужно преобразовывать в user?

О: Это требование стандарта OneBot12. private — это понятие получения, при отправке лучше использовать user, что соответствует семантике.

В2: Как поддержать новые типы сессий?

О: Зарегистрируйте пользовательский тип с помощью register_custom_type(), или используйте стандартные типы, такие как channel, guild.

В3: Что делать, если у события нет detail_type?

О: Система автоматически определит тип по доступным полям ID. Приоритет: group > channel > guild > thread > user.

В4: Как адаптер должен сопоставлять Telegram supergroup?

О: В логике преобразования адаптера сопоставьте supergroup со стандартным типом group.

В5: Как обрабатывать специальные платформы, такие как почта?

О: Для неуниверсальных или платформенных специфических типов используйте {platform}_raw и {platform}_raw_type для сохранения исходных данных, адаптер должен обрабатывать их самостоятельно.

11. Связанные документы

Со стороны адаптера (сопоставление и преобразование):

Со стороны модуля (если не хотите вручную управлять преобразованием типов):

事件转换

来源: docs/ru/standards/event-conversion.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/event-conversion.md)

Стандартизированные правила преобразования адаптеров

Tip

Когда нужно читать эту статью: когда вы конвертируете нативные события платформы в стандартные события OneBot12 в адаптере — проверьте обязательные поля, структуру сегментов сообщений, именование расширенных полей ({platform}_ префикс), вывод типа сессии, сопоставляя с этой статьей. Если не нужно, пропустите.

1. Основные принципы

  1. Строгое соответствие: все стандартные поля должны полностью соответствовать спецификации OneBot12
  2. Ясное расширение: функции, специфичные для платформы, должны иметь префикс {platform}_ (например, yunhu_form)
  3. Полная целостность данных: исходные данные события должны сохраняться в поле {platform}_raw, тип исходного события — в поле {platform}_raw_type
  4. Единое время: все временные метки должны быть преобразованы в 10-значные Unix-временные метки (в секундах)
  5. Единое имя платформы: поле platform должно соответствовать названию или псевдониму, зарегистрированному в ErisPulse

2. Требования к стандартным полям

2.1 Обязательные поля

Поле Тип Описание
id string Уникальный идентификатор события
time integer Unix-временная метка (в секундах)
type string Тип события
detail_type string Подтип события (см. Стандарт типов сессий)
platform string Название платформы
self object Информация о боте
self.platform string Название платформы
self.user_id string ID пользователя бота

Правила для detail_type:

2.2 Поля для событий сообщений

Поле Тип Описание
message array Массив сегментов сообщения
alt_message string Запасной текст сегментов сообщения
user_id string ID пользователя
user_nickname string Имя пользователя (необязательно)

2.3 Поля для уведомлений

Поле Тип Описание
user_id string ID пользователя
user_nickname string Имя пользователя (необязательно)
operator_id string ID оператора (необязательно)

2.4 Поля для запросов

Поле Тип Описание
user_id string ID пользователя
user_nickname string Имя пользователя (необязательно)
comment string Комментарий к запросу (необязательно)
request_id string Идентификатор запроса (рекомендуется для согласования/отклонения запроса)

Описание поля request_id:

3. Примеры форматов событий

3.1 Событие сообщения (message)

{
  "id": "1234567890",
  "time": 1752241223,
  "type": "message",
  "detail_type": "group",
  "platform": "yunhu",
  "self": {
    "platform": "yunhu",
    "user_id": "bot_123"
  },
  "message": [
    {
      "type": "text",
      "data": {
        "text": "Розыгрыш суперприза"
      }
    }
  ],
  "alt_message": "Розыгрыш суперприза",
  "user_id": "user_456",
  "user_nickname": "YingXinche",
  "group_id": "group_789",
  "yunhu_raw": {...},
  "yunhu_raw_type": "message.receive.normal",
  "yunhu_command": {
    "name": "Розыгрыш",
    "args": "Суперприз"
  }
}

3.2 Событие уведомления (notice)

{
  "id": "1234567891",
  "time": 1752241224,
  "type": "notice",
  "detail_type": "group_member_increase",
  "platform": "yunhu",
  "self": {
    "platform": "yunhu",
    "user_id": "bot_123"
  },
  "user_id": "user_456",
  "user_nickname": "YingXinche",
  "group_id": "group_789",
  "operator_id": "",
  "yunhu_raw": {...},
  "yunhu_raw_type": "bot.followed"
}

3.3 Событие запроса (request)

{
  "id": "1234567892",
  "time": 1752241225,
  "type": "request",
  "detail_type": "friend",
  "platform": "onebot11",
  "self": {
    "platform": "onebot11",
    "user_id": "bot_123"
  },
  "user_id": "user_456",
  "user_nickname": "YingXinche",
  "comment": "Пожалуйста, добавьте в друзья",
  "request_id": "req_abc123",
  "onebot11_raw": {...},
  "onebot11_raw_type": "request"
}

4. Стандартные сегменты сообщений

4.1 Стандартные сегменты сообщений

Стандартные сегменты сообщений не требуют префикса платформы.

Тип Описание Поля data
text Текст text: str
image Изображение file, url: str
audio Аудио file, url: str
video Видео file, url: str
file Файл file, url: str, filename: str
mention Упоминание пользователя user_id: str, user_name: str
reply Ответ message_id: str
face Эмодзи id: str
location Геолокация latitude: float, longitude: float
keyboard Кнопки/внутренняя клавиатура rows: list[list[button]] (см. 4.1.1)

Формат поля file для медиа-сегментов (направление отправки, общий для image / audio / video / file):

Формат Пример Требования адаптера
HTTP(S) URL https://example.com/a.png Обязательно принимать
Локальный путь к файлу /tmp/a.png, C:\tmp\a.png Обязательно принимать
Бинарные данные bytes Обязательно принимать
URI file:// / Base64 / Data URI file:///tmp/a.png, data:image/png;base64,... Рекомендуется принимать

Полный протокол отправки медиа (порядок определения формата, вывод имени файла, иерархия возможностей) см. в Спецификации методов отправки §2.1.

Семантика направления полей:

{
  "type": "text",
  "data": {
    "text": "Привет, мир"
  }
}

4.1.1 Сегмент кнопок/внутренней клавиатуры keyboard (универсальный для платформ)

Кнопки/внутренняя клавиатура есть на нескольких платформах (Telegram / Yunhu / QQBot / Kook / Discord и др.), и являются универсальной концепцией, поэтому они являются стандартным сегментом (без префикса платформы). Адаптер должен преобразовывать стандартный сегмент в платформенный формат; платформенные расширенные сегменты (например, telegram_inline_keyboard) должны сохраняться и передаваться без изменений.

{
  "type": "keyboard",
  "data": {
    "rows": [
      [
        {"label": "Вариант A", "type": "callback", "data": "vote:A"},
        {"label": "Официальный сайт", "type": "link", "data": "https://example.com"}
      ]
    ]
  }
}

Описание полей:

Поле Тип Обязательно Описание
rows Двумерный массив Да Каждый подмассив — строка кнопок
rows[][].label str Да Текст кнопки
rows[][].type str Да callback (отправка данных при нажатии) / link (переход по URL)
rows[][].data str Да Данные для callback (type=callback) или адрес для перехода (type=link)
rows[][].* Any Нет Платформенные необязательные поля (например, web_app, menus), адаптер отображает по возможностям или игнорирует

Примеры преобразования адаптера (полное отображение и стандарт для событий взаимодействия см. в Стандарте кросс-платформенных компонентов взаимодействия):

Платформа Стандартный сегмент → Платформенный формат
Telegram inline_keyboard: [{text, callback_data | url}]
Yunhu buttons: [{label, action_type: 2=callback | 1=link, ...}]
QQBot keyboard.content.rows: [{label, type: 2=callback | 0=link, data}] (требуется сообщение в формате markdown)
Kook Модуль action-group в карточке
Discord components: action_row + buttons (custom_id/url)

4.2 Платформенные расширенные сегменты сообщений

Сегменты сообщений, специфичные для платформы, должны иметь платформенный префикс:

// Yunhu - форма
{"type": "yunhu_form", "data": {"form_id": "123456", "form_name": "Форма регистрации"}}

// Telegram - стикер
{"type": "telegram_sticker", "data": {"file_id": "CAACAgIAAxkBAA...", "emoji": "😂"}}

Требования к расширенным сегментам сообщений:

  1. Поля внутри data не должны иметь префикса: {"type": "yunhu_form", "data": {"form_id": "..."}}, а не {"type": "yunhu_form", "data": {"yunhu_form_id": "..."}}
  2. Обеспечьте механизм понижения: модуль может не распознавать расширенные сегменты сообщений, адаптер должен предоставить текстовое замещение в alt_message
  3. Документация: каждый расширенный сегмент сообщения должен быть подробно описан в документации адаптера: type, структура data и сценарии использования

5. Обработка неизвестных событий

Для неопознанных типов событий следует генерировать событие предупреждения:

{
  "id": "1234567893",
  "time": 1752241223,
  "type": "unknown",
  "platform": "yunhu",
  "yunhu_raw": {...},
  "yunhu_raw_type": "unknown",
  "warning": "Неизвестный тип события: special_event",
  "alt_message": "Этот тип события не поддерживается системой."
}

6. Стандарты именования расширений

6.1 Именование полей

Правило: {platform}_{field_name}

Платформенный префикс    Имя поля            Полное имя поля
────────    ───────          ──────────
yunhu       command           yunhu_command
telegram    sticker_file_id   telegram_sticker_file_id
onebot11    anonymous         onebot11_anonymous
email       subject           email_subject

Требования:

6.2 Именование типов сегментов сообщений

Правило: {platform}_{segment_type}

Стандартные типы сегментов сообщений (text, image, audio, video, mention, reply и т.д.) не должны иметь платформенный префикс. Только платформенные специфичные типы сегментов сообщений должны иметь префикс.

6.3 Именование полей исходных данных

Следующие имена полей являются зарезервированными, все адаптеры должны соблюдать:

Зарезервированное поле Тип Описание
{platform}_raw any Полная копия исходных данных события платформы
{platform}_raw_type string Идентификатор типа исходного события платформы

Требования:

6.4 Примеры платформенных специфичных полей

{
  "yunhu_command": {
    "name": "Розыгрыш",
    "args": "Суперприз"
  },
  "yunhu_form": {
    "form_id": "123456"
  },
  "telegram_sticker": {
    "file_id": "CAACAgIAAxkBAA..."
  }
}

6.5 Вложенные расширенные поля

Расширенные поля могут быть простыми значениями, а также вложенными объектами:

{
  "telegram_chat": {
    "id": 123456,
    "type": "supergroup",
    "title": "Моя группа"
  },
  "telegram_forward_from": {
    "user_id": "789",
    "user_name": "ForwardUser"
  }
}

Требования к вложенным полям:

6.6 Расширение поля self

Стандартные обязательные поля self (см. §2.1): platform, user_id, а также расширения ErisPulse:

Поле Тип Описание
self.user_name string Имя бота
self.avatar string URL аватара бота
self.account_id string Идентификатор аккаунта в режиме множественных аккаунтов

Отслеживание состояния бота: адаптер сообщает фреймворку о состоянии подключения бота, отправляя событие type: "meta". Поддерживаемые detail_type: connect (онлайн), heartbeat (пинг), disconnect (офлайн). Система автоматически извлекает информацию о боте из поля self для отслеживания состояния. Кроме того, поле self в обычных событиях также автоматически обнаруживается. Подробнее см. в API адаптера - Управление состоянием бота.


7. Расширение типов сессий

ErisPulse расширяет стандартные типы сессий OneBot12 (private, group) следующими типами:

Тип Стандарт OneBot12 Расширение ErisPulse Описание
private ✅ — Личный чат
group ✅ — Групповой чат
user — ✅ Тип пользователя (Telegram и др.)
channel — ✅ Канал (трансляционный)
guild — ✅ Сервер/сообщество
thread — ✅ Тема/подканал

Расширение типов сессий адаптером:

from ErisPulse.Core.Event.session_type import register_custom_type

# Регистрация в начале работы адаптера
register_custom_type(
    receive_type="email",      # detail_type для входящих событий
    send_type="email",         # тип цели при отправке
    id_field="email_id",       # соответствующее имя поля ID
    platform="email"           # идентификатор платформы
)

Требования к пользовательским типам:

Полное определение и сопоставление типов сессий см. в Стандарте типов сессий.


8. Руководство для разработчиков модулей

8.1 Доступ к расширенным полям

from ErisPulse.Core.Event import message

@message()
async def handle_message(event):
    # Доступ к стандартным полям
    text = event.get_text()
    user_id = event.get_user_id()

    # Доступ к платформенным расширенным полям - способ 1: прямой get
    yunhu_command = event.get("yunhu_command")

    # Доступ к платформенным расширенным полям - способ 2: точечный доступ (обёртка Event)
    # event.yunhu_command

    # Доступ к исходным данным
    raw_data = event.get("yunhu_raw")
    raw_type = event.get_raw_type()

    # Определение платформы
    platform = event.get_platform()
    if platform == "yunhu":
        pass
    elif platform == "telegram":
        pass

8.2 Обработка расширенных сегментов сообщений

@message()
async def handle_message(event):
    message_segments = event.get("message", [])

    for segment in message_segments:
        seg_type = segment.get("type")
        seg_data = segment.get("data", {})

        if seg_type == "text":
            text = seg_data["text"]
        elif seg_type.startswith("yunhu_"):
            if seg_type == "yunhu_form":
                form_id = seg_data["form_id"]
        elif seg_type.startswith("telegram_"):
            if seg_type == "telegram_sticker":
                file_id = seg_data["file_id"]

8.3 Рекомендуемые практики

  1. Приоритет стандартных полей: не предполагайте, что расширенные поля всегда существуют
  2. Определение платформы: используйте event.get_platform() для определения платформы, а не по наличию расширенных полей
  3. Плавное понижение: при невозможности обработки расширенных сегментов сообщений используйте alt_message как резервный вариант
  4. Не жёстко кодируйте префиксы: используйте переменную platform для динамического составления
# ✅ Рекомендуется
platform = event.get_platform()
raw_data = event.get(f"{platform}_raw")

# ❌ Не рекомендуется
raw_data = event.get("yunhu_raw")

8.4 Обработка запросов

Разработчики модулей могут использовать event.approve() и event.reject() для обработки запросов:

from ErisPulse.Core.Event import request

# Автоматическое согласование запроса на добавление в друзья
@request.on_friend_request()
async def handle_friend_request(event):
    user_name = event.get_user_nickname() or event.get_user_id()
    comment = event.get_comment()
    
    # Согласование запроса
    result = await event.approve()
    if result.get("status") == "ok":
        print(f"Согласован запрос на добавление в друзья от {user_name}")
    else:
        print(f"Ошибка согласования запроса: {result.get('message')}")

# Запрос на вступление в группу: решение по условиям
@request.on_group_request()
async def handle_group_request(event):
    comment = event.get_comment()
    
    # Отклонение запроса
    result = await event.reject(comment="Временно не принимаю новые группы")

Прямое управление через адаптер (для сценариев, не связанных с обработчиками событий):

from ErisPulse import adapter

# Прямое управление по request_id
await adapter.myplatform.Request("req_abc123").accept()
await adapter.myplatform.Request("req_abc123").reject()

# Указание аккаунта бота
await adapter.myplatform.Request("req_abc123").Using("bot1").accept()

# С комментарием
await adapter.myplatform.Request("req_abc123").accept(comment="Добро пожаловать")

9. Вывод типа сессии для событий notice / request

9.1 Проблемный фон

Типы detail_type событий notice и request — это семантические подтипы (например, group_member_increase, friend_increase), а не типы сессии (например, group, private).

type        detail_type                  Смысл            Тип сессии
────        ───────────                  ────            ────────
message     group                        Сообщение в группе         group (detail_type — тип сессии)
message     private                      Личное сообщение          private (detail_type — тип сессии)
notice      group_member_increase        Увеличение участников группы    group (выводится по group_id)
notice      friend_increase              Увеличение друзей           private (выводится по user_id)
request     friend                       Запрос на добавление в друзья     private (выводится по user_id)
request     group                        Запрос на вступление в группу      group (detail_type — тип сессии)

9.2 Правила вывода

Порядок вывода infer_receive_type():

  1. Если detail_type — известный тип сессии (private/group/channel/guild/thread/user), используйте его
  2. Если detail_type — пользовательский тип сессии, используйте его
  3. В противном случае (семантические подтипы notice/request), выводите по ID-полю:
    • Если есть group_id → "group"
    • Если есть channel_id → "channel"
    • Если есть guild_id → "guild"
    • Если есть thread_id → "thread"
    • Если есть user_id → "private"

9.3 Вывод цели event.reply()

Цель отправки event.reply() в событиях notice/request определяется выводом типа сессии:

from ErisPulse.Core.Event import notice

@notice.on_group_increase()
async def handle_welcome(event):
    group_id = event.get("group_id")    # "group_789"
    user_id = event.get("user_id")      # "user_456"

    # event.reply() отправляется в группу (group/group_789)
    await event.reply("Добро пожаловать в группу!")

    # Если нужно уведомить администратора (личный чат), явно укажите цель:
    await adapter.Send.To("user", "admin_id").Text(f"Новый участник {user_id} присоединился к {group_id}")

9.4 Рекомендации для разработчиков адаптеров

Убедитесь, что события notice/request содержат правильные ID-поля:

detail_type Обязательные ID-поля Выводимый тип сессии
group_member_increase group_id + user_id group
group_member_decrease group_id + user_id group
friend_increase user_id private
friend_decrease user_id private
friend (запрос) user_id private
group (запрос) group_id group

10. Связанные документы

Для сопоставления при реализации конвертера:

Для отправки сообщений в рамках одного адаптера:

Для изучения известных расширенных событий и сегментов сообщений каждой платформы:

发送方法规范

来源: docs/ru/standards/send-method-spec.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/send-method-spec.md)

Спецификация методов отправки ErisPulse

Tip

Когда стоит читать этот документ: когда вы реализуете класс Send в адаптере — именование методов, поддержку форматов медиапараметров (URL/путь/bytes), обратное преобразование Raw_ob12, возможности объединённой пересылки — сверяйтесь с этим документом. Если это не нужно — пропустите.

Данный документ определяет соглашения об именовании, спецификации параметров и требования к обратному преобразованию для методов отправки класса Send в адаптерах ErisPulse.

0. Соглашения о ключевых словах

Ключевые слова Обязательно (MUST), Рекомендуется (SHOULD), Можно (MAY) в этом документе интерпретируются в следующей семантике (со ссылкой на RFC 2119):

Ключевое слово Семантика Последствия нарушения
Обязательно (MUST) Императивное требование; от него зависят поведение фреймворка и кроссплатформенная согласованность Адаптер считается несоответствующим стандарту, код модулей может не работать
Рекомендуется (SHOULD) Настоятельная рекомендация; следовать, если нет веских причин поступить иначе При отклонении необходимо указать в документации адаптера причину и альтернативное поведение
Можно (MAY) Необязательный пункт; решается самостоятельно в зависимости от возможностей платформы Отсутствуют

1. Стандартное именование методов

Все методы отправки используют PascalCase (стиль ВерхнегоВерблюжьегоРегистра), первая буква заглавная.

1.1 Стандартные методы отправки

Имя метода Описание Тип параметра Требование к реализации
Text Отправка текстового сообщения str Обязательно
Image Отправка изображения str | bytes Обязательно (встроено в базовый класс, см. §6.4)
Voice Отправка голосового сообщения str | bytes Обязательно (встроено в базовый класс; при отсутствии поддержки голоса платформой — деградация согласно §2.1.5)
Video Отправка видео str | bytes Обязательно (встроено в базовый класс; при отсутствии поддержки видео платформой — деградация согласно §2.1.5)
File Отправка файла str | bytes, filename: str | None = None Обязательно (встроено в базовый класс)
At Упоминание пользователя/группы str (user_id) Модифицирующий метод, по необходимости
Face Отправка эмодзи str (emoji) Необязательно
Reply Ответ на сообщение str (message_id) Модифицирующий метод, по необходимости
Forward Пересылка сообщения str (message_id) Необязательно
Markdown Отправка сообщения Markdown str Необязательно
HTML Отправка сообщения HTML str Необязательно
Card Отправка карточного сообщения dict Необязательно

Стандартные методы (Text/Image/Voice/Video/File) встроены в базовый класс SendDSL и по умолчанию делегируют Raw_ob12; адаптер получает сигнатуры типов без повторной реализации. Переопределять отдельные методы следует только тогда, когда платформе требуется особая логика (см. §6.4).

1.2 Модифицирующие методы для цепочек

Имя метода Описание Тип параметра
At Упоминание пользователя (можно вызывать несколько раз) str (user_id)
AtAll Упоминание всех участников Нет
Reply Ответ на сообщение str (message_id)

1.3 Методы протокола

Имя метода Описание Обязателен ли
Raw_ob12 Отправка сегментов сообщений в формате OneBot12 Обязательно

Raw_ob12 — метод, реализация которого обязательна. Это одна из ключевых обязанностей адаптера: принимать стандартные сегменты сообщений OneBot12 и преобразовывать их в вызовы нативного API платформы. Raw_ob12 — единая точка входа обратного преобразования (OneBot12 → платформа), гарантирующая, что модули могут отправлять сообщения стандартными сегментами, не завися от платформенно-специфичных методов.

Поведение при отсутствии переопределения Raw_ob12: реализация по умолчанию в базовом классе записывает лог уровня error и возвращает стандартный формат ответа об ошибке (status: "failed", retcode: 10002), напоминая разработчику адаптера о необходимости реализовать этот метод.

1.4 Рекомендуемые соглашения об именовании расширений

Если адаптеру требуется поддержка отправки сырых данных в формате, отличном от OneBot12 (например, специфичный для платформы JSON, XML и т.д.), рекомендуется использовать следующие соглашения об именовании:

Рекомендуемое имя метода Описание
Raw_json Отправка произвольных данных JSON
Raw_xml Отправка произвольных данных XML

Примечание: эти методы не являются предоставляемыми базовым классом методами по умолчанию, и их реализация не обязательна. Они служат лишь соглашением об именовании; адаптер может определять их самостоятельно по необходимости. Если адаптер не поддерживает эти форматы, определять их не нужно.

MessageBuilder (конструктор сообщений): ErisPulse предоставляет утилитарный класс MessageBuilder для удобного построения списков сегментов сообщений OneBot12, используемых совместно с Raw_ob12. Подробнее см. Подробно о MessageBuilder.

2. Подробная спецификация параметров

2.1 Протокол отправки медиасообщений (Image / Voice / Video / File)

Этот раздел — единый стандарт протокола отправки медиа: модуль вызывает четыре медиаметода одним и тем же кодом, а адаптер отвечает за преобразование различных форм параметра file в нативное для платформы поведение загрузки/отправки.

2.1.1 Допустимые формы параметра file

Форма Пример Требования к адаптеру
HTTP(S) URL https://example.com/image.jpg Обязательно принимать
Локальный путь к файлу /path/to/file.jpg, C:\path\to\file.jpg Обязательно принимать
Бинарные данные b"\x89PNG..." Обязательно принимать
URI file:// file:///path/to/file.jpg Рекомендуется принимать (можно преобразовать в локальный путь для обработки)
Строка Base64 / Data URI iVBORw0KGgo=..., data:image/png;base64,... Рекомендуется принимать (совместимо с практиками экосистемы OneBot12)

Адаптер обязан вести себя единообразно на трёх обязательных формах — независимо от того, передаёт модуль URL, путь или bytes, полученное сообщение будет одним и тем же. Если платформа не может напрямую использовать какую-то форму (например, API платформы не поддерживает ссылки на внешние URL), адаптер самостоятельно скачивает/читает и загружает данные; недопустимо требовать от модуля повторить попытку с другой формой.

2.1.2 Порядок определения формы

При реализации обработки медиапараметров адаптер должен определять форму в следующем порядке:

  1. Тип bytes → прямая загрузка
  2. Строка начинается с http:// / https:// → обрабатывать как URL (прямая ссылка или скачивание с последующей загрузкой — по возможностям платформы)
  3. Строка начинается с file:// → отрезать префикс и обрабатывать как локальный путь
  4. Остальные строки → обрабатывать как локальный путь (если существует — прочитать и загрузить; если нет — вернуть стандартный ответ об ошибке)
def _resolve_media(self, file: "str | bytes") -> bytes:
    """Определение формы и нормализация (пример)"""
    if isinstance(file, (bytes, bytearray)):
        return bytes(file)
    if file.startswith(("http://", "https://")):
        return self._download(file)          # скачивать, когда платформа не может ссылаться на URL
    if file.startswith("file://"):
        file = file[len("file://"):]
    with open(file, "rb") as f:              # локальный путь
        return f.read()

2.1.3 Семантика имени файла для File

Сигнатура метода File: File(file, filename=None) (filename — необязательный параметр, встроен в базовый класс).

Порядок определения имени файла (адаптер генерирует его по этой схеме, когда filename не предоставлен явно):

  1. Явный параметр filename (наивысший приоритет)
  2. basename из URL (например, https://host/a/b/report.pdf → report.pdf; необходимо отрезать query string)
  3. basename из локального пути (например, /tmp/data/backup.zip → backup.zip)
  4. Генерация по умолчанию платформы (например, file_{timestamp}; следует сохранять реальное расширение — расширение влияет на распознавание типа и поведение предпросмотра на стороне платформы)

Image / Voice / Video также могут принимать filename (передаётся через data.filename сегмента сообщения), но кроссплатформенная семантика имени файла гарантируется только для File.

2.1.4 Обязанность декларирования ограничений платформы

Ограничения платформ на максимальный размер медиа, форматы (MIME), длительность (аудио/видео) и т.д. различаются. Адаптер должен:

2.1.5 Лестница деградации возможностей

Когда платформа не поддерживает какой-либо тип медиа, деградация выполняется по следующей лестнице (согласно общему принципу «деградация возможностей без ошибок»):

Сценарий Поведение при деградации
Voice не поддерживает голосовые сообщения Следует отправлять как File (или ближайшую платформенную форму); при невозможности выразить — вернуть retcode=10002
Video не поддерживает видеосообщения То же
Тип медиа полностью не поддерживается (нет файловых возможностей) Вернуть retcode=10002, в message указать неподдерживаемый тип данных
Форма не поддерживается (например, невозможно обработать base64) Вернуть retcode=10002, можно указать в message подсказку модулю использовать URL/bytes

Запрещённое поведение: молчаливое отбрасывание (без ответа), выбрасывание исключений, требование к модулю писать платформенные ветки обработки.

2.2 Спецификация параметра упоминания пользователя

Метод: At (модифицирующий метод)

Параметр: user_id (str)

Требования:

Пример:

# Одиночное упоминание пользователя
Send.To("group", "g123").At("123456").Text("Привет")

# Несколько упоминаний (цепной вызов)
send.To("group", "g123").At("123456").At("789012").Text("Всем привет")

2.3 Спецификация параметра ответа на сообщение

Метод: Reply (модифицирующий метод)

Параметр: message_id (str)

Требования:

Пример:

send.To("group", "g123").Reply("msg_123456").Text("Принято")

2.4 Возможность объединённой пересылки (MergeForward, необязательно)

MergeForward(nodes) — необязательный метод возможности: упаковывает несколько фрагментов содержимого для отправки в форме «объединённой пересылки / свёрнутого форматированного текста»; адаптер определяет оптимальное представление в зависимости от возможностей платформы. Семантика отличается от Forward(from_chat_id, message_id) из §1.1 (пересылка существующего сообщения); оба сосуществуют.

Контекст стандарта: ядро стандарта OneBot12 не содержит сегмента node и API объединённой пересылки — «узлы сообщений» являются концепцией уровня реализации экосистемы OneBot v11 (расширение go-cqhttp); v12 передаёт подобные платформенно-специфические возможности механизму расширений. Реализация в ErisPulse — эквивалент механизма расширений: без добавления новых стандартных сегментов сообщений — через необязательные методы возможностей + обнаружение в рантайме через list_sends — адаптеры с реализованным MergeForward автоматически получают возможность; вызывающая сторона при отсутствии реализации автоматически откатывается к обычному тексту, модули не требуют никаких изменений.

Сигнатура метода:

def MergeForward(self, nodes: List[Dict]) -> asyncio.Task:
    """
    Отправка объединённой пересылки / свёрнутого форматированного текста

    :param nodes: массив узлов объединённой пересылки (формат узлов OneBot v11 из расширения go-cqhttp):
        [
            {
                "type": "node",
                "data": {
                    "user_id": "10000",     # id отправителя для отображения
                    "nickname": "MyBot",     # ник отправителя для отображения
                    "content": [            # массив сегментов сообщения, сейчас сегменты text
                        {"type": "text", "data": {"text": "[1/3] Заголовок\nОсновной текст…"}}
                    ],
                },
            },
        ]
    :return: asyncio.Task, после await возвращает стандартный формат ответа
    """

Требования к реализации:

  1. Платформа имеет нативную объединённую пересылку (семейство OneBot11): обязательно вызывать специализированный эндпоинт протокола (send_group_forward_msg / send_private_forward_msg) — реализации вроде NapCat отклоняют сегменты node, передаваемые через send_msg (retcode 1400); при отказе специализированного эндпоинта можно откатиться к прямой отправке сегментов node через send_msg
  2. Платформа не имеет нативной пересылки, но имеет форматированный текст (Yunhu / Telegram / Kook / Discord / Matrix и др.): следует рендерить узлы в свёрнутую форму форматированного текста платформы (сворачиваемая HTML-карточка / Card / Embed / список blockquote); справочные стили и список задач по каждому адаптеру см. в SDK/Adapter/ADAPTER_FORWARD_GUIDE.md
  3. Нет ни того, ни другого: недопустимо реализовывать этот метод (тем более возвращать успех, отправляя словари node как чистый текст) — без реализации list_sends не содержит MergeForward, и вызывающая сторона автоматически откатывается к обычному тексту
  4. Семантика сбоя: допустимо вернуть стандартный ответ об ошибке (status: "failed") или выбросить исключение; вызывающая сторона откатится к отправке чистого текста, содержимое не теряется
  5. Недопустимо внутри MergeForward снова вызывать Raw_ob12 для отправки тех же nodes (в Raw_ob12 OneBot11 уже встроена автоматическая маршрутизация чистых сегментов node — образуется цикл)
  6. Парсинг исходящих сегментов node (соглашение для всех адаптеров): диспетчеризация сегментов Raw_ob12 каждого адаптера должна регистрировать сегмент node — чистый список node автоматически маршрутизируется в представление объединённой пересылки этого адаптера (на платформах с MergeForward это эквивалентно прямому вызову; на платформах без этого метода извлекается текст каждого узла и выводится построчно как есть в формате «ник: текст»); текст сегментов node в смешанном списке включается в основной текст. Любой адаптер, получивший сегмент node, не должен молча его отбрасывать.

Формы реализации на платформах (подробнее в SDK/Adapter/ADAPTER_FORWARD_GUIDE.md):

Пример обнаружения на стороне вызывающего:

sends = adapter.list_sends(platform)
if "MergeForward" in sends:
    await adapter.Send.To("group", "123").MergeForward(nodes)
else:
    await adapter.Send.To("group", "123").Text(plain_text)

Референсная реализация: ErisPulse-OneBot11Adapter (специализированный эндпоинт → откат к send_msg); шаблоны рендеринга и список задач по каждому адаптеру см. в SDK/Adapter/ADAPTER_FORWARD_GUIDE.md.

3. Именование платформенно-специфичных методов

Не рекомендуется напрямую добавлять методы с платформенным префиксом в класс Send. Рекомендуется использовать универсальные имена методов или методы Raw_{протокол}.

Не рекомендуется:

def YunhuForm(self, form_id: str):  # ❌ Не рекомендуется
    pass

def TelegramSticker(self, sticker_id: str):  # ❌ Не рекомендуется
    pass

Рекомендуется:

def Form(self, form_id: str):  # ✅ Универсальное имя метода
    pass

def Sticker(self, sticker_id: str):  # ✅ Универсальное имя метода
    pass

def Raw_ob12(self, message):  # ✅ Отправка в формате OneBot12
    pass

Требования к методам расширений:

4. Соглашения об именовании параметров

Имя параметра Описание Тип
text Текстовое содержимое str
file Медиа-содержимое (URL / путь / бинарные данные, см. §2.1.1) str / bytes
filename Имя файла (необязательно для File, см. §2.1.3) str / None
user_id ID пользователя str / int
group_id ID группы str / int
message_id ID сообщения str
data Объект данных (например, данные карточки) dict

5. Спецификация возвращаемых значений


6. Спецификация обратного преобразования (OneBot12 → платформа)

Адаптер не только должен преобразовывать нативные события платформы в формат OneBot12 (прямое преобразование), но и обязан предоставлять возможность преобразования сегментов сообщений OneBot12 обратно в вызовы нативного API платформы (обратное преобразование). Единая точка входа обратного преобразования — метод Raw_ob12.

6.1 Модель преобразования

Прямое преобразование (направление приёма)       Обратное преобразование (направление отправки)
─────────────────                ─────────────────
Нативное событие платформы                       Список сегментов сообщений OneBot12
    │                                  │
    ▼                                  ▼
Converter.convert()               Send.Raw_ob12()
    │                                  │
    ▼                                  ▼
Стандартное событие OneBot12                     Вызов нативного API платформы
(содержит {platform}_raw)                        (возвращает стандартный формат ответа)

Ключевая симметрия: прямое преобразование сохраняет исходные данные в {platform}_raw, обратное преобразование принимает стандартный формат OneBot12 и восстанавливает его в вызовы платформы.

6.2 Спецификация реализации Raw_ob12

Raw_ob12 принимает список стандартных сегментов сообщений OneBot12 и обязан преобразовывать их в вызовы нативного API платформы.

Сигнатура метода:

def Raw_ob12(self, message_segments: List[Dict]) -> asyncio.Task:
    """
    Отправка стандартных сегментов сообщений OneBot12

    :param message_segments: список сегментов сообщений OneBot12
        [
            {"type": "text", "data": {"text": "Hello"}},
            {"type": "image", "data": {"file": "https://..."}},
            {"type": "mention", "data": {"user_id": "123"}},
        ]
    :return: asyncio.Task, после await возвращает стандартный формат ответа
    """

Требования к реализации:

  1. Должны обрабатываться все стандартные типы сегментов сообщений: как минимум поддержка text, image, audio, video, file, mention, reply
  2. Должны обрабатываться платформенные сегменты расширений: сегменты типа {platform}_xxx преобразовывать в соответствующие нативные вызовы платформы
  3. Должен возвращаться стандартный формат ответа: согласно Стандарту API-ответов
  4. Неподдерживаемые сегменты должны пропускаться с записью предупреждения; не следует выбрасывать исключение, приводящее к сбою отправки всего сообщения

6.3 Правила преобразования сегментов сообщений

6.3.1 Преобразование стандартных сегментов сообщений

Адаптер обязан реализовать преобразование следующих стандартных сегментов сообщений:

Сегмент OneBot12 Требования к преобразованию
text Напрямую использовать data.text
image data.file обрабатывается согласно медиапротоколу §2.1 (три обязательные формы + порядок определения)
audio Та же логика обработки, что и у image
video Та же логика обработки, что и у image
file Та же логика обработки, что и у image; имя файла обрабатывается по порядку определения из §2.1.3 через data.filename
mention Преобразуется в механизм упоминаний платформы (например, entities в Telegram, at_uid в Yunhu)
reply Преобразуется в механизм цитирования/ответа платформы
face Преобразуется в механизм отправки эмодзи платформы; при отсутствии поддержки — пропустить
location Преобразуется в механизм отправки геопозиции платформы; при отсутствии поддержки — пропустить

6.3.2 Преобразование платформенных сегментов расширений

Для сегментов с платформенным префиксом адаптер должен распознавать и преобразовывать их:

def _convert_ob12_segments(self, segments: List[Dict]) -> Any:
    """Преобразование сегментов сообщений OneBot12 в нативный формат платформы"""
    platform_prefix = f"{self._platform_name}_"
    
    for segment in segments:
        seg_type = segment["type"]
        seg_data = segment["data"]
        
        if seg_type.startswith(platform_prefix):
            # Сегмент расширения платформы → нативный вызов платформы
            self._handle_platform_segment(seg_type, seg_data)
        elif seg_type in self._standard_segment_handlers:
            # Стандартный сегмент → эквивалентная операция платформы
            self._standard_segment_handlers[seg_type](seg_data)
        else:
            # Неизвестный сегмент → записать предупреждение и пропустить
            logger.warning(f"Неподдерживаемый тип сегмента сообщения: {seg_type}")

6.3.3 Обработка составных сегментов сообщений

Одно сообщение может содержать несколько сегментов; адаптер должен корректно обрабатывать составные сообщения:

# Модуль отправляет сообщение с текстом + изображением + упоминанием пользователя
await send.Raw_ob12([
    {"type": "mention", "data": {"user_id": "123"}},
    {"type": "text", "data": {"text": "Привет"}},
    {"type": "image", "data": {"file": "https://example.com/img.jpg"}}
])

Стратегия обработки:

6.4 Отношение Raw_ob12 к стандартным методам

Стандартные методы отправки адаптера (Text, Image и т.д.) уже встроены в базовый класс SendDSL и по умолчанию делегируются Raw_ob12; подклассу адаптера не нужно реализовывать их повторно:

class Send(SendDSL):
    def Raw_ob12(self, message_segments: List[Dict]) -> asyncio.Task:
        """Ключевая реализация: сегменты сообщений OneBot12 → API платформы (обязательна)"""
        return asyncio.create_task(self._send_ob12(message_segments))

    # Text/Image/Voice/Video/File унаследованы от базового класса, автоматически делегируют Raw_ob12
    # Если нужна платформенно-специфичная логика, можно переопределить отдельный метод:
    # def Text(self, text: str) -> asyncio.Task:
    #     return self.Raw_ob12([{"type": "text", "data": {"text": text}}])

Преимущества:

6.5 Пример реализации

class YunhuSend(SendDSL):
    """Реализация Send для платформы Yunhu"""
    
    def Raw_ob12(self, message_segments: list) -> asyncio.Task:
        """Сегменты сообщений OneBot12 → вызов API Yunhu"""
        return asyncio.create_task(self._do_send(message_segments))
    
    async def _do_send(self, segments: list) -> dict:
        """Фактическая логика отправки"""
        # 1. Разбор состояния модификаторов
        at_users = self._at_users or []
        reply_to = self._reply_to
        at_all = self._at_all
        
        # 2. Преобразование сегментов сообщений
        yunhu_elements = []
        for seg in segments:
            seg_type = seg["type"]
            seg_data = seg["data"]
            
            if seg_type == "text":
                yunhu_elements.append({"type": "text", "content": seg_data["text"]})
            elif seg_type == "image":
                yunhu_elements.append({"type": "image", "url": seg_data["file"]})
            elif seg_type == "mention":
                at_users.append(seg_data["user_id"])
            elif seg_type == "reply":
                reply_to = seg_data["message_id"]
            elif seg_type == "yunhu_form":
                # Сегмент расширения платформы
                yunhu_elements.append({"type": "form", "form_id": seg_data["form_id"]})
            else:
                logger.warning(f"Сегмент сообщения, не поддерживаемый Yunhu: {seg_type}")
        
        # 3. Вызов API Yunhu
        response = await self._call_yunhu_api(yunhu_elements, at_users, reply_to, at_all)
        
        # 4. Возврат стандартного формата ответа
        return {
            "status": "ok" if response["code"] == 0 else "failed",
            "retcode": response["code"],
            "data": {"message_id": response.get("msg_id", ""), "time": int(time.time())},
            "message_id": response.get("msg_id", ""),
            "message": "",
            "yunhu_raw": response
        }

7. Обнаружение методов

Разработчики модулей могут через API запрашивать методы отправки, поддерживаемые адаптером (не следует хардкодить в модулях список методов какой-либо платформы — методы расширений адаптеров эволюционируют вместе с версиями, ориентир — обнаружение в рантайме):

from ErisPulse import adapter

# Перечислить все методы отправки
methods = adapter.list_sends("myplatform")
# ["Batch", "Form", "Image", "Recall", "Sticker", "Text", ...]

# Посмотреть детали метода
info = adapter.send_info("myplatform", "Form")
# {
#     "name": "Form",
#     "parameters": [{"name": "form_id", "type": "str", ...}],
#     "return_type": "Awaitable[Any]",
#     "docstring": "Отправка формы Yunhu"
# }

9. Замечания по разработке адаптеров

О том, как правильно переопределять __init__ у BaseAdapter, Send, Request, см. Введение в разработку адаптеров — замечания по __init__.



10. Чек-лист реализации адаптера

Методы отправки

Протокол отправки медиа

Объединённая пересылка (необязательно, см. §2.4)

Обратное преобразование


11. MessageBuilder (конструктор сообщений) — что нужно знать стороне адаптера

MessageBuilder — предоставляемый ErisPulse инструмент построения сегментов сообщений OneBot12 для построения сообщений на стороне модулей: цепные вызовы (или статическое быстрое построение) в итоге выдают стандартный список сегментов сообщений [{"type": ..., "data": ...}], который вызывающая сторона передаёт напрямую в Raw_ob12.

Стороне адаптера нужно знать только одно: то, что получает Raw_ob12, — это и есть результат работы этого инструмента — полностью изоморфный рукописному списку сегментов сообщений; обрабатывайте его согласно логике диспетчеризации сегментов из §6.2, без необходимости определять, откуда он — из MessageBuilder или из литерала.

Полное использование (механизм двух режимов, все методы сегментов сообщений, утилитарные методы copy() / clear() и примеры) см. в Подробно о MessageBuilder; в этом документе повторяться не будем.


12. Связанные документы

Для сопоставления при реализации возвращаемых значений отправки:

Другие стороны того же адаптера:

API 动作规范

来源: docs/ru/standards/api-action-spec.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/api-action-spec.md)

Стандарт действий API ErisPulse

Tip

Когда это нужно читать: когда вы реализуете стандартные действия DSL-Api (get_user_info / delete_message и т.д.) в адаптере или в модуле вызываете эти стандартные действия между платформами и хотите убедиться в параметрах и структуре данных, обратитесь к этой статье. Если не нужно, пропустите.

В этом документе определяется единый интерфейс для стандартных действий API OneBot12 в адаптере ErisPulse, чтобы разработчики модулей могли программировать по стандартному интерфейсу, а адаптер отвечал за сопоставление с оригинальными API платформы.

Охват: В стандартных действиях OneBot12, ApiDSL предоставляет строго типизированные методы для пользователей / групп / каналов (Guild) / управления сообщениями / мета-обычные интерфейсы (send_message реализуется через SendDSL.Raw_ob12). Действия с файловыми ресурсами (upload_file / get_file / фрагментация) сохраняются только для понижения уровня, см. §3.5. Расширенные действия платформы вызываются через Api.call("prefix.action", ...). Параметры и структура возврата действий соответствуют спецификации OneBot12 (в хранилище onebot/specs/interface/).

1. Предпосылки проектирования

В ErisPulse форматы сообщений (отправка и получение сообщений) и событий полностью соответствуют стандарту OneBot12, но вызов действий API (например, получение информации о пользователе, получение списка групп, отмена сообщения и т.д.) ранее не был унифицирован — разработчикам модулей приходилось писать разные вызовы call_api для каждой платформы.

ApiDSL решает эту проблему, предоставляя строго типизированные стандартные методы действий:

Код модуля (унифицированный для всех платформ)             Реализация адаптера (платформа специфична)
─────────────────              ──────────────────
adapter.Api.get_user_info("123")  →  вызов адаптера call_api / переопределение
adapter.Api.get_group_list()      →  вызов адаптера call_api / переопределение
adapter.Api.delete_message("id")  →  вызов адаптера call_api / переопределение

2. Параллельная структура DSL на трех уровнях

В адаптере ErisPulse есть три параллельных внутренних класса DSL, каждый со своей задачей:

BaseAdapter
├── Send(SendDSL)       ← отправка сообщений (Text/Image/Raw_ob12)
├── Request(RequestDSL)  ← обработка действий запроса (accept/reject)
└── Api(ApiDSL)          ← стандартные действия API (пользователи/группы/каналы/управление сообщениями/файлами/мета) ★
DSL Задача Стиль методов Возвращаемое значение
Send Отправка сообщений Цепочка + asyncio.Task Стандартный ответ
Request Обработка событий запроса asyncio.Task Стандартный ответ
Api Запросы/управление async методы Стандартный ответ

3. Список стандартных действий

3.1 Действия, связанные с пользователями

Метод OB12 действие Параметры Возвращаемое значение
get_self_info() get_self_info Нет user_id, user_name, user_displayname
get_user_info(user_id) get_user_info user_id: str user_id, user_name, user_displayname, user_remark
get_friend_list() get_friend_list Нет list[get_user_info ответ]

3.2 Действия, связанные с группами

Метод OB12 действие Параметры Возвращаемое значение
get_group_info(group_id) get_group_info group_id: str group_id, group_name
get_group_list() get_group_list Нет list[get_group_info ответ]
get_group_member_info(group_id, user_id) get_group_member_info group_id: str, user_id: str user_id, user_name, user_displayname
get_group_member_list(group_id) get_group_member_list group_id: str list[get_group_member_info ответ]
set_group_name(group_id, group_name) set_group_name group_id: str, group_name: str Нет
leave_group(group_id) leave_group group_id: str Нет

3.3 Управление сообщениями

Метод OB12 действие Параметры Примечание
delete_message(message_id) delete_message message_id: str Отмена/удаление сообщения

Отправка сообщений (send_message) обрабатывается через SendDSL в Raw_ob12, не повторяется в ApiDSL.

3.4 Действия, связанные с каналами (Guild)

Система каналов OneBot12 разделена на два уровня: канал (guild) и подканал (channel).

Метод OB12 действие Параметры Возвращаемое значение
get_guild_info(guild_id) get_guild_info guild_id: str guild_id, guild_name
get_guild_list() get_guild_list Нет list[get_guild_info ответ]
set_guild_name(guild_id, guild_name) set_guild_name guild_id: str, guild_name: str Нет
get_guild_member_info(guild_id, user_id) get_guild_member_info guild_id: str, user_id: str user_id, user_name, user_displayname
get_guild_member_list(guild_id) get_guild_member_list guild_id: str list[get_guild_member_info ответ]
leave_guild(guild_id) leave_guild guild_id: str Нет
get_channel_info(guild_id, channel_id) get_channel_info guild_id: str, channel_id: str channel_id, channel_name
get_channel_list(guild_id, *, joined_only) get_channel_list guild_id: str, joined_only: bool=false list[get_channel_info ответ]
set_channel_name(guild_id, channel_id, channel_name) set_channel_name guild_id, channel_id, channel_name Нет
get_channel_member_info(guild_id, channel_id, user_id) get_channel_member_info guild_id, channel_id, user_id user_id, user_name, user_displayname
get_channel_member_list(guild_id, channel_id) get_channel_member_list guild_id, channel_id list[get_channel_member_info ответ]
leave_channel(guild_id, channel_id) leave_channel guild_id, channel_id Нет

Система каналов и группы (group) независимы: платформы Discord / QQ каналы / Kook реализуют интерфейс канала, традиционные QQ / WeChat реализуют интерфейс группы, оба могут существовать одновременно или только один.

3.5 Действия с файловыми ресурсами

Warning

Модель файловых ресурсов (двухчастный file_id) в ErisPulse считается "пониженного уровня": ErisPulse не использует модель "сначала загрузить, получить file_id, затем ссылаться" для отправки файлов — модули отправляют файлы с помощью SendDSL.File(file, filename) (URL / путь / байты отправляются напрямую во время отправки, см. спецификацию метода отправки). Действия upload_file / get_file / фрагментация зависят от специфичных для платформы возможностей файловых ресурсов file_id, имеют недостаточную универсальность; только когда у адаптера есть такая возможность, они могут быть переданы, встроенные адаптеры не реализуют и не рекомендуют реализовывать; при вызове обычно возвращается retcode=10002. Если модулю нужно отправлять файлы между платформами, используйте SendDSL.File, не полагайтесь на file_id.

Перспектива: стандартизация модели ресурсов file_id на уровне фреймворка — будущее направление, в текущей версии не предоставляется.

Отправка целого файла (маленький файл):

Метод OB12 действие Параметры Возвращаемое значение
upload_file(*, type, name, ...) upload_file type, name, url/path/data, headers?, sha256? file_id
get_file(file_id, type) get_file file_id: str, type: str name, url/path/data

Параметр type метода upload_file:

3.5.1 Фрагментированная передача (большие файлы, входит в вышеупомянутый пониженный уровень)

Действия OneBot12 по фрагментации по stage различаются этапами. ApiDSL разделяет этапы одного действия на отдельные методы (offset — смещение байт, data в JSON — Base64); таблица предназначена для справки, адаптер не должен и не должен реализовывать:

Три этапа фрагментированной загрузки: prepare → transfer (циклически по частям) → finish

Метод Этап Параметры Возвращаемое значение
upload_file_fragmented_prepare(name, total_size) prepare name: str, total_size: int file_id (используется во время передачи)
upload_file_fragmented_transfer(file_id, offset, data) transfer file_id, offset: int, data: bytes Нет
upload_file_fragmented_finish(file_id, sha256) finish file_id, sha256: str (проверка целостности файла) file_id
total = os.path.getsize(path)
r = await adapter.Api.upload_file_fragmented_prepare(os.path.basename(path), total)
fid = r["data"]["file_id"]
offset = 0
with open(path, "rb") as f:
    while chunk := f.read(65536):
        await adapter.Api.upload_file_fragmented_transfer(fid, offset, chunk)
        offset += len(chunk)
sha256 = hashlib.sha256(open(path, "rb").read()).hexdigest()
await adapter.Api.upload_file_fragmented_finish(fid, sha256)

Два этапа фрагментированной загрузки: prepare → transfer (циклически получать части)

Метод Этап Параметры Возвращаемое значение
get_file_fragmented_prepare(file_id) prepare file_id name, total_size, sha256
get_file_fragmented_transfer(file_id, offset, size) transfer file_id, offset: int, size: int data (байты текущей части)

3.6 Мета-действия

Мета-действия не связаны с конкретным аккаунтом, не требуют указания Using() для бота.

Метод OB12 действие Параметры Возвращаемое значение
get_latest_events(limit, timeout) get_latest_events limit: int=0, timeout: int=0 Массив объектов событий (без мета-событий)
get_supported_actions() get_supported_actions Нет list[str] поддерживаемые имена действий
get_status() get_status Нет good: bool, bots: list[{self, online, ...}]
get_version() get_version Нет impl, version, onebot_version

3.7 Общие расширенные действия

Метод Описание
call(action, **params) Эвакуационный люк для расширенных действий платформы, соответствует правилу именования расширения OB12 {prefix}.{action}

4. Способы использования

4.1 Базовый вызов

from ErisPulse import adapter

# Получение информации о пользователе (унифицированный для всех платформ)
result = await adapter.myplatform.Api.get_user_info("123456")
if result["status"] == "ok":
    user_name = result["data"]["user_name"]
    print(f"Имя пользователя: {user_name}")

# Получение списка групп
result = await adapter.myplatform.Api.get_group_list()
groups = result["data"]

# Отмена сообщения
await adapter.myplatform.Api.delete_message("msg_123456")

4.2 Указание аккаунта бота (режим нескольких учетных записей)

# Выполнение операций с указанным аккаунтом бота
info = await adapter.myplatform.Api.Using("bot1").get_self_info()

4.3 Расширенные действия платформы

# Вызов специфичных для платформы расширенных действий (рекомендуется использовать имя {prefix}.{action})
result = await adapter.telegram.Api.call(
    "telegram.send_sticker",
    sticker_id="CAACAgIAAxkBAA...",
)

4.4 Использование в обработчике событий

from ErisPulse.Core.Event import message

@message()
async def handle(event):
    # Получение подробной информации об отправителе
    user_id = event.get_user_id()
    platform = event.get_platform()

    result = await getattr(adapter, platform).Api.get_user_info(user_id)
    if result["status"] == "ok":
        user_name = result["data"]["user_name"]
        await event.reply(f"Привет, {user_name}!")

5. Реализация адаптера

5.1 Стандартное поведение (без настройки)

Стандартная реализация ApiDSL передает имя стандартного действия как endpoint напрямую в adapter.call_api():

# Стандартная реализация ApiDSL эквивалентна:
async def get_user_info(self, user_id: str) -> dict:
    return await self._adapter.call_api("get_user_info", user_id=user_id, account_id=self._account_id)

Сценарий применения: когда базовый API адаптера сам следует стандартному протоколу OneBot12, call_api нативно поддерживает имена стандартных действий (например, напрямую подключается к сервису, соответствующему этому протоколу).

5.2 Переопределение стандартных методов (сопоставление с оригинальными API платформы)

Адаптер может переопределить отдельные стандартные методы, сопоставляя их с оригинальными API платформы:

class MyAdapter(BaseAdapter):

    class Api(BaseAdapter.Api):
        """Реализация стандартных действий API для MyPlatform"""

        async def get_user_info(self, user_id: str) -> dict:
            # Сопоставление с оригинальным API платформы
            raw = await self._adapter._request("GET", f"/users/{user_id}")
            if raw.get("code") != 0:
                # Ошибка на стороне платформы: используйте сегмент 341xx~345xx для трех низших цифр (346xx зарезервирован фреймворком, не используйте)
                return self._adapter.make_error(retcode=34400, message="Пользователь не существует")

            user = raw["data"]
            return self._adapter.make_response(
                data={
                    "user_id": str(user["id"]),
                    "user_name": user.get("nick", ""),
                    "user_displayname": user.get("display_name", ""),
                    "user_remark": user.get("remark", ""),
                },
                raw=raw,
            )

        async def get_friend_list(self) -> dict:
            raw = await self._adapter._request("GET", "/friends")
            friends = [
                {
                    "user_id": str(u["id"]),
                    "user_name": u.get("nick", ""),
                    "user_displayname": u.get("display_name", ""),
                    "user_remark": u.get("remark", ""),
                }
                for u in raw.get("data", [])
            ]
            return self._adapter.make_response(data=friends, raw=raw)

5.3 Не поддерживаемые действия

Методы, которые адаптер не переопределил, используют стандартную реализацию (передаются в call_api). Если call_api также не поддерживает это действие, следует вернуть стандартный ответ об ошибке:

async def call_api(self, endpoint: str, **params):
    if endpoint not in self._supported_endpoints:
        return self.make_error(retcode=10002, message=f"Действие не поддерживается: {endpoint}")
    # ... вызов API платформы

Разработчики модулей могут определить поддержку по значению retcode в возвращаемом значении:

result = await adapter.myplatform.Api.get_friend_list()
if result["retcode"] == 10002:
    print("Эта платформа не поддерживает получение списка друзей")

6. Формат ответа

Все методы ApiDSL возвращают стандартный формат ответа API (см. стандарт ответа API):

{
    "status": "ok",
    "retcode": 0,
    "data": { ... },
    "message_id": "",
    "message": "",
    "myplatform_raw": { ... }
}

Важно: для действий получения информации message_id — пустая строка (только для действий отправки сообщений message_id заполняется).

7. Отношения с SendDSL / RequestDSL

Сценарий Используемый DSL Пример
Отправка сообщений Send adapter.Send.To("group", "123").Text("hi")
Подтверждение/отклонение запроса Request adapter.Request("req_id").accept()
Получение информации о пользователе/группе Api adapter.Api.get_user_info("123")
Отмена сообщения Api adapter.Api.delete_message("msg_id")
Выход из группы Api adapter.Api.leave_group("group_id")

8. Контрольный список реализации адаптера

Стандартные действия

Расширенные действия

9. Связанные документы

Сопутствующие документы при реализации возвратов API DSL:

Другие DSL в одном адаптере:

Сопутствующие документы для стороны событий:

API 响应规范

来源: docs/ru/standards/api-response.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/api-response.md)

Стандарт спецификации возвращаемых значений адаптеров ErisPulse

Tip

Когда читать этот документ: когда вы реализуете возвращаемые значения call_api / Send / Request в адаптере и хотите убедиться, что структура ответа и коды возврата соответствуют стандарту ErisPulse, или вам нужно выяснить значение конкретного retcode — сверяйтесь с этим документом. Если в этом нет необходимости — пропустите.

1. Описание

Откуда взялась эта спецификация?

Для обеспечения единообразия возвращаемых значений интерфейсов отправки на различных платформах и совместимости с OneBot12 адаптеры ErisPulse для формата API-ответов используют стандарт структуры возвращаемых значений отправки сообщений, определённый в OneBot12.

Однако протокол ErisPulse имеет некоторые особые определения:

2. Базовая структура возвращаемого значения

Все ответы на действия должны содержать следующие базовые поля:

Имя поля Тип данных Обязательное Описание
status string Да Статус выполнения, должен быть "ok" или "failed"
retcode int64 Да Код возврата, следует правилам кодов возврата OneBot12
data any Да Данные ответа; при успехе содержит результат запроса, при неудаче — null
message_id string Да Идентификатор сообщения; если отсутствует — пустая строка
message string Да Сообщение об ошибке; при успехе — пустая строка
{platform_name}_raw any Нет Исходные данные ответа

Необязательные поля:

Имя поля Тип данных Обязательное Описание
echo string Нет Если запрос содержит поле echo, оно возвращается в неизменном виде

3. Полная спецификация полей

3.1 Общие поля

Пример успешного ответа

{
    "status": "ok",
    "retcode": 0,
    "data": {
        "message_id": "1234",
        "time": 1632847927.599013
    },
    "message_id": "1234",
    "message": "",
    "echo": "1234",
    "telegram_raw": {...}
}

Пример ответа с ошибкой

{
    "status": "failed",
    "retcode": 10003,
    "data": null,
    "message_id": "",
    "message": "Отсутствует обязательный параметр: user_id",
    "echo": "1234",
    "telegram_raw": {...}
}

3.2 Спецификация кодов возврата

0 Успех (OK)

1xxxx Ошибка запроса действия (Request Error)

Код ошибки Название ошибки Описание
10001 Bad Request Недействительный запрос действия
10002 Unsupported Action Неподдерживаемый запрос действия
10003 Bad Param Недействительные параметры запроса действия
10004 Unsupported Param Неподдерживаемые параметры запроса действия
10005 Unsupported Segment Неподдерживаемый тип сегмента сообщения
10006 Bad Segment Data Недействительные данные сегмента сообщения
10007 Unsupported Segment Data Неподдерживаемые данные сегмента сообщения
10101 Who Am I Не указан аккаунт бота
10102 Unknown Self Неизвестный аккаунт бота

2xxxx Ошибка обработчика действия (Handler Error)

Код ошибки Название ошибки Описание
20001 Bad Handler Ошибка реализации обработчика действия
20002 Internal Handler Error Исключение во время выполнения обработчика действия

3xxxx Ошибка выполнения действия (Execution Error)

Диапазон кодов Тип ошибки Описание
31xxx Database Error Ошибка базы данных
32xxx Filesystem Error Ошибка файловой системы
33xxx Network Error Сетевая ошибка
34xxx Platform Error Ошибка платформы бота
35xxx Logic Error Ошибка логики действия
36xxx I Am Tired Реализация решила забастовать

Зарезервированные сегменты ошибок

4. Требования к реализации

  1. Все ответы должны содержать поля status, retcode, data и message
  2. Если запрос содержит непустое поле echo, ответ должен содержать поле echo с тем же значением
  3. Коды возврата должны строго следовать спецификации OneBot12
  4. Сообщения об ошибках (message) должны быть понятны человеку

5. Расширения спецификации

ErisPulse сделал следующие расширения поверх стандартной структуры возвращаемых значений OneBot12:

5.1 Обязательное поле message_id

В стандарте OneBot12 message_id находится внутри объекта data и не является обязательным. ErisPulse поднял его до обязательного поля верхнего уровня:

5.2 Поле исходного ответа {platform}_raw

Возвращаемое значение должно содержать поле {platform}_raw, в котором хранится полная копия исходных данных ответа платформы:

{
    "status": "ok",
    "retcode": 0,
    "data": {"message_id": "1234", "time": 1632847927},
    "message_id": "1234",
    "message": "",
    "telegram_raw": {
        "ok": true,
        "result": {"message_id": 1234, "date": 1632847927, ...}
    }
}

Требования:

5.3 Расширенные коды возврата фреймворка (пользовательские три младших разряда сегмента ошибок платформы 34xxx)

Спецификация OneBot12 позволяет реализациям определять собственные значения трёх младших разрядов 3xxxx. Семантика 34xxx — Platform Error (ошибка платформы бота, например сбой из-за ограничений платформы). Внутри 34xxx используется послойно по зонам ответственности:

Сегмент (младшие три разряда) Принадлежность Назначение
340xx Реализация адаптера Семейство операций запросов (Request Not Found / Already Handled / Not Supported / Permission Denied, см. request-action-spec §7)
341xx–345xx Реализация адаптера Ошибки на стороне платформы: права доступа / антифрод / ограничения аккаунта и т. п. (младшие три разряда определяются реализацией, исходная ошибка помещается в {platform}_raw)
346xx Фреймворк ErisPulse (зарезервировано) Перехваты самим фреймворком и общие сбои; адаптерам/модулям не занимать
347xx–349xx Реализация адаптера Прочие ошибки выполнения на стороне платформы

Коды 346xx, используемые в настоящее время фреймворком ErisPulse:

Код ошибки Название ошибки Описание
34600 SDK Failure Общий сбой фреймворка (код возврата по умолчанию для make_error())
34601 Action Denied Исходящее действие отключено скоупом (scope.actions); вызов не инициируется, сразу возвращается этот ответ

Разграничение ответственности: 34601 — это перехват фреймворком до вызова (модуль вообще не имеет права инициировать действие); коды платформы 34004 / 34xxx — это действие отправлено, но отклонено платформой (например, у бота нет прав, сработал антифрод). При диагностике проблем с правами модули должны проверять оба случая: сначала 34601 (свой модуль отключён скоупом), затем 34xxx (ограничения на стороне платформы).

Структура возврата — стандартный ответ с ошибкой из §2:

{
    "status": "failed",
    "retcode": 34601,
    "data": null,
    "message_id": "",
    "message": "action 'send' denied by scope.actions"
}

5.4 Контрольный список для реализации адаптера

6. Примечания


Что дальше

请求动作规范

来源: docs/ru/standards/request-action-spec.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/standards/request-action-spec.md)

Стандарты операций запросов ErisPulse

Tip

Когда читать эту статью: Когда ваш адаптер должен обрабатывать запросы от друзей/приглашения в группы (преобразование событий запросов, реализация accept/reject), или когда модуль использует event.approve() / event.reject() для одобрения запросов, обратитесь к данной статье. Если не нужно, пропустите.

Данный документ определяет стандартизированные стандарты операций запросов в адаптере ErisPulse, включая требования к полям событий запроса, использование DSL запросов и требования к реализации адаптера.

1. Обзор

Событие запроса (type: "request") — это специальный тип события, определенный в OneBot12, представляющий запрос, требующий принятия решения ботом (например, запрос на добавление в друзья, приглашение в группу и т.д.).

В отличие от событий сообщений, события запроса требуют двустороннего взаимодействия:

  1. Получение: адаптер преобразует оригинальные запросы платформы в стандартные события запроса
  2. Ответ: модуль выполняет операции с помощью DSL запроса или Event.approve()/Event.reject()
Оригинальное событие запроса платформы
    │
    ▼
Converter.convert()        ← Реализация адаптера (прямое преобразование)
    │
    ▼
Стандартное событие запроса (с request_id)
    │
    ├─→ Обработчик модуля @request.on_friend_request()
    │       │
    │       ├─→ event.approve()     ← Одобрение запроса
    │       └─→ event.reject()      ← Отклонение запроса
    │               │
    │               ▼
    │       adapter.Request(request_id).accept()
    │               │
    │               ▼
    │       BaseAdapter.Request.accept()  ← Переопределение адаптером
    │               │
    │               ▼
    │       Вызов API платформы
    │
    └─→ Или напрямую через операции адаптера
            await adapter.Request("req_id").accept()

2. Требования к полям события запроса

2.1 Стандартные поля

Событие запроса должно содержать обязательные поля стандарта OneBot12, а также следующие поля:

Поле Тип Обязательно Описание
request_id string Рекомендуется Идентификатор запроса, используется для одобрения/отклонения
user_id string Да ID пользователя, отправившего запрос
user_nickname string Нет Никнейм пользователя, отправившего запрос
comment string Нет Комментарий к запросу

2.2 Поле request_id

request_id — это ключевой идентификатор запроса:

2.3 Пример события запроса

{
  "id": "evt_123456",
  "time": 1752241225,
  "type": "request",
  "detail_type": "friend",
  "platform": "onebot11",
  "self": {
    "platform": "onebot11",
    "user_id": "bot_123"
  },
  "user_id": "user_456",
  "user_nickname": "YingXinche",
  "comment": "Пожалуйста, добавьте в друзья",
  "request_id": "flag_abc123",
  "onebot11_raw": {...},
  "onebot11_raw_type": "request"
}

3. DSL запросов

3.1 Цепочечные вызовы

Request предоставляет интерфейс цепочечных вызовов, аналогичный Send:

# Основное использование
await adapter.Request("req_id").accept()
await adapter.Request("req_id").reject()

# Указание учетной записи бота
await adapter.Request("req_id").Using("bot1").accept()

# С комментарием (через kwargs)
await adapter.Request("req_id").accept(comment="Добро пожаловать")
await adapter.Request("req_id").reject(comment="Пока не добавляю в друзья")

# Комбинированное использование
await adapter.Request("req_id").Using("bot1").accept(comment="Добро пожаловать")

3.2 Список методов

Метод Описание Возвращаемое значение
Using(account_id) Указание учетной записи бота для выполнения операции RequestDSL (поддерживает цепочечные вызовы)
accept(**kwargs) Одобрение запроса asyncio.Task (await возвращает стандартный ответ)
reject(**kwargs) Отклонение запроса asyncio.Task (await возвращает стандартный ответ)

3.3 Формат возвращаемых значений

Операция возвращает стандартный формат ответа API:

Успешно:

{
    "status": "ok",
    "retcode": 0,
    "data": null,
    "message_id": "",
    "message": ""
}

Неудачно:

{
    "status": "failed",
    "retcode": 34001,
    "data": null,
    "message_id": "",
    "message": "Запрос просрочен или не существует"
}

Не реализовано (адаптер не переопределил accept/reject):

{
    "status": "failed",
    "retcode": 10002,
    "data": null,
    "message_id": "",
    "message": "Платформа MyAdapter не реализовала операции запроса (accept)"
}

4. Удобные методы Event

Обёртка Event предоставляет удобные методы, подходящие для использования в обработчиках событий запросов:

from ErisPulse.Core.Event import request

@request.on_friend_request()
async def handle_friend_request(event):
    # Проверка идентификатора запроса
    request_id = event.get_request_id()
    if not request_id:
        print("Предупреждение: событие запроса не содержит request_id")
        return
    
    # Одобрение запроса
    result = await event.approve()
    
    # Или отклонение запроса
    # result = await event.reject(comment="Пока не добавляю в друзья")
    
    # Проверка результата
    if result.get("status") == "ok":
        print("Операция выполнена успешно")
    else:
        print(f"Операция не удалась: {result.get('message')}")

4.1 Список методов Event

Метод Описание Возвращаемое значение
get_request_id() Получение идентификатора запроса str
approve(comment=None) Одобрение текущего события запроса Стандартный формат ответа
reject(comment=None) Отклонение текущего события запроса Стандартный формат ответа

5. Требования к реализации адаптера

5.1 Требования к конвертеру

Конвертер адаптера при преобразовании событий запроса должен правильно устанавливать поле request_id:

def convert_request_event(self, raw_event: dict) -> dict:
    """Преобразование оригинального события запроса платформы"""
    return {
        "id": self._generate_event_id(raw_event),
        "time": int(time.time()),
        "type": "request",
        "detail_type": self._map_request_type(raw_event),  # "friend" или "group"
        "platform": self._platform_name,
        "self": {
            "platform": self._platform_name,
            "user_id": str(self._bot_id),
        },
        "user_id": str(raw_event.get("user_id", "")),
        "user_nickname": raw_event.get("nickname", ""),
        "comment": raw_event.get("message", ""),
        "request_id": self._extract_request_id(raw_event),  # ← Ключевое поле
        f"{self._platform_name}_raw": raw_event,
        f"{self._platform_name}_raw_type": raw_event.get("type", ""),
    }

def _extract_request_id(self, raw_event: dict) -> str:
    """
    Извлечение идентификатора запроса из оригинального события платформы
    
    Предпочтительно использовать оригинальный идентификатор платформы, если нет, генерируем уникальный ID
    """
    # Предпочтительно использовать оригинальный идентификатор платформы
    if flag := raw_event.get("flag"):
        return str(flag)
    if request_key := raw_event.get("request_key"):
        return str(request_key)
    
    # Резервное: генерируем уникальный ID
    import hashlib
    raw = f"{self._platform_name}_{raw_event.get('user_id')}_{raw_event.get('timestamp')}"
    return hashlib.md5(raw.encode()).hexdigest()

5.2 Реализация внутреннего класса Request

Адаптер переопределяет accept и reject в внутреннем классе Request:

from ErisPulse.Core import BaseAdapter, RequestDSL

class MyAdapter(BaseAdapter):
    
    class Request(RequestDSL):
        """Реализация операций запроса для MyPlatform"""
        
        def accept(self, **kwargs):
            """
            Одобрение запроса
            
            :param kwargs: Дополнительные параметры, например comment="Комментарий"
            :return: asyncio.Task
            """
            async def _do():
                try:
                    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", ""),
                    }
                except Exception as e:
                    return {
                        "status": "failed",
                        "retcode": 34001,
                        "data": None,
                        "message_id": "",
                        "message": f"Операция запроса не удалась: {e}",
                    }
            
            return self._create_task(_do())
        
        def reject(self, **kwargs):
            """Отклонение запроса"""
            async def _do():
                try:
                    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", ""),
                    }
                except Exception as e:
                    return {
                        "status": "failed",
                        "retcode": 34001,
                        "data": None,
                        "message_id": "",
                        "message": f"Операция запроса не удалась: {e}",
                    }
            
            return self._create_task(_do())

5.3 Платформа не поддерживает операции запроса

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

  1. Не переопределять внутренний класс Request: использовать реализацию по умолчанию базового класса, вызов accept()/reject() вернёт retcode=10002
  2. Не генерировать request_id при преобразовании: не генерировать request_id, чтобы event.approve() выбросил ValueError
  3. Записывать логи: в accept/reject записывать предупреждения и возвращать соответствующий код ошибки

5.4 Итог: Send и Request параллельны

Адаптер имеет два параллельных внутренних класса DSL, каждый с собственной функцией:

BaseAdapter
├── Send(SendDSL)     ← Отправка сообщений
│   ├── Raw_ob12()    ← Обязательно реализовать
│   ├── Text()        ← Рекомендуется реализовать
│   └── Image()       ← Реализовать по необходимости
│
└── Request(RequestDSL) ← Операции запроса
    ├── accept()        ← Реализовать по необходимости
    └── reject()        ← Реализовать по необходимости

5.5 Примечания к __init__ адаптера

При переопределении __init__ внутреннего класса Request необходимо передавать параметры и вызывать super().__init__(), подробнее см. Введение в разработку адаптеров - Примечания к __init__ (Request аналогично, параметры: adapter, request_id, account_id).

6. Проверочный список реализации адаптера

Основные требования

Преобразование запроса

Операции запроса

7. Расширение кодов ошибок

Рекомендуемые коды ошибок адаптера/платформы, связанные с операциями запроса (следуя стандарту ответов API §3.2, в диапазоне 34xxx с самыми низкими тремя цифрами для пользовательских определений):

Код ошибки Название ошибки Описание
34001 Request Not Found Запрос не существует или просрочен
34002 Request Already Handled Запрос уже обработан
34003 Request Not Supported Платформа не поддерживает данную операцию запроса
34004 Permission Denied Бот не имеет прав обрабатывать этот запрос (возвращается платформой)

Граница с кодами фреймворка: вышеуказанные 340xx — это ошибки, возвращаемые платформой/адаптером при обработке запроса; фреймворк ErisPulse, отключая действие request для модуля в scope.actions, до вызова адаптера напрямую возвращает 34601 (Action Denied, см. стандарт ответов API §5.3), эти два типа ошибок не заменяют друг друга: сначала проходит фреймворк 34601, затем платформа 340xx.

8. Связанные документы

Документы, сопутствующие реализации операций запроса:

Другие DSL в одном адаптере: