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

name: erispulse-getting-started description: "适用于:第一次接触 ErisPulse,想从零跑起一个机器人并理解核心概念。覆盖场景:ErisPulse — это асинхронный модульный робот-фреймворк, основанный на событийном подходе. Бизнес-логику нужно писать только один раз, а с помощью адаптеров развертывать на различных платформах, таких как QQ, Telegram, Kook, Yunhu и других. В комплекте предоставляется система плагинов, горячая перезагрузка и полный набор инструментов для разработчиков.。1. Установка (Python >= 3.10):。Когда стоит читать эту статью: бот уже запускается, и вы хотите написать первый обработчик команды /hello, а также разобраться, что именно делают sdk.run и декораторы команд. Если вы ещё не установили ErisPulse и не инициализировали проект, сначала выполните три шага из быстрого старта за 5 минут: «Установка», «Инициализация проекта» и «Запуск проекта»; если всё готово — пропустите.。Какие компоненты проходит сообщение от отправки пользователем до ответа бота? За что отвечают адаптеры и за что модули? Прочитав эту статью, вы сможете самостоятельно отвечать..."

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/README.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/README.md)

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

ErisPulse — это асинхронный модульный робот-фреймворк, основанный на событийном подходе. Бизнес-логику нужно писать только один раз, а с помощью адаптеров развертывать на различных платформах, таких как QQ, Telegram, Kook, Yunhu и других. В комплекте предоставляется система плагинов, горячая перезагрузка и полный набор инструментов для разработчиков.

Впервые используете? Начните с 5-минутного краткого руководства — от установки до запуска первого робота, всё пошагово.


Выбор вашего пути

Выберите путь обучения в зависимости от ваших целей. Все пути организованы по степени сложности.

Important

Не нужно читать все документы. Большинству разработчиков достаточно прочитать пять основных статей уровня L1 (примерно 1–2 часа), чтобы самостоятельно реализовать 90% функционала; остальные документы — это справочники для конкретных сценариев — читайте их по мере возникновения необходимости. В начале каждой статьи указано, когда её следует читать.

Пять основных статей уровня L1: Введение в разработку модулей → Основные понятия → Обёртка событий → Описание конфигурационного файла → Обработка исключений

Авторам адаптеров дополнительно: Введение в разработку адаптеров → Конвертер событий → Подробное руководство SendDSL

1. Я хочу использовать бота

Запустите бота, установите модули и настройте.

Прогресс Документ Описание
① Основы Быстрый старт за 5 минут Установка, инициализация, запуск — единственный входной путь
Прямая установка Клиент ErisPulse-App Официальный клиент для всех платформ: графический интерфейс на телефоне / компьютере, запуск и управление без терминала
② Глубже Создание первого бота Написание первого обработчика команд
③ Понятия Основные понятия Понимание архитектуры адаптеров/модулей/событий
④ Практика Примеры распространённых задач Хранение данных, планирование задач, управление правами
Справочник Описание конфигурационного файла · Справочник команд CLI · Руководство по развертыванию Справочники по мере необходимости
Справочник Руководство по особенностям платформ Различия между платформами (облачный океан / QQ / Telegram и т.д.)

2. Я хочу разрабатывать модули / адаптеры

Написание расширений для ErisPulse, которые можно распространять.

Тип Введение Продвинутый
Разработка модулей (рекомендуется) Введение в разработку модулей Основные понятия · Обёртка событий · Лучшие практики
Разработка адаптеров Введение в разработку адаптеров Основные понятия · Подробное руководство SendDSL · Конвертер событий · Лучшие практики
Технические стандарты Обзор стандартов Стандарты, которые необходимо соблюдать при разработке адаптеров: Типы сессий · Конвертация событий · Методы отправки · Ответ API · Действия запроса
Тестирование и отладка Тестирование модулей (ErisPulse-Testing) Решение проблем с цепочкой вызовов при публикации ("Почему команда не срабатывает?")
Публикация Публикация и магазин модулей Публикация на PyPI и в магазин модулей

3. Я хочу глубже понять принципы работы

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

Архитектура и API

Документ Описание
Обзор архитектуры Визуализированные диаграммы: основная архитектура, процесс инициализации, обработка событий, жизненный цикл, стратегия загрузки модулей (включая ленивую активацию по событиям activate_on), архитектура локальных плагинов и горячая перезагрузка модулей (поддержка всех источников модулей)
Система событий Полный API для пяти типов событий
Система адаптеров Регистрация, запуск, остановка адаптеров, вызов API
Основные модули Базовые возможности: Storage / Config / Logger / Router и т.д.

Обязательные статьи уровня L2 (обязательно для написания производственных модулей)

Документ Описание
Области видимости (scope) Трёхмерный контроль областей видимости: доступность модуля / допуск событий / ограничения на действия (включая тонкие правила на уровне методов, наследование правил через merge)
Система принадлежности (owner) Принадлежность ресурсов и автоматическая очистка: контекст owner, карта ресурсов, последовательность очистки при выгрузке, границы проектирования и руководство для авторов модулей
Управление жизненным циклом Система событий жизненного цикла: что делать при загрузке, выгрузке, перезагрузке, отключении модуля / адаптера
Система взаимодействия (взаимодействие) Полное объяснение wait_reply, таймеры сессий (remind/escalate), многоканальный ожидание (select), взаимоисключающие сеансы, почтовый ящик, транзакции сообщений, трассировка цепочек

По сценариям (по мере необходимости)

Группа Документ Проблемы, если не понять первую статью
Запуск Процесс запуска и ручное управление · Ленивая загрузка Без понимания цепочки запуска (Finder / Loader / Manager / Router) при сбое загрузки модуля можно только смотреть в логи
Коммуникация Межмодульная коммуникация · Пул соединений и трансляция · Система маршрутизации Без понимания契约 (meta.services) белого списка вызов module.call из другого модуля будет отклонён с ошибкой ServiceNotProvidedError без объяснения
Хранение данных Хранилища данных · SQL-построитель · HTTP-клиент Без понимания семантики периодов охлаждения при сбое подключения при дёргании базы данных можно ошибочно считать, что фреймворк "сломался" (на самом деле операции с хранилищем быстро завершаются с ошибкой, а затем автоматически переподключаются после охлаждения)
Диалоги Многошаговые диалоги (Conversation) · Почтовый ящик сессий · MessageBuilder · Международная локализация Без понимания взаимоисключающих сеансов два модуля одновременно ждущие ответ от одного пользователя будут мешать друг другу
Градация Теневые модули и градация Без пяти уровней изоляции при раннем переходе теневого модуля к реальному, его исходящие сообщения могут попасть к реальным пользователям

Сводная таблица понятий

Когда сложно понять, какая подсистема отвечает за что, воспользуйтесь этой таблицей:

Подсистема Отвечает на какие вопросы Краткая формулировка
Коммуникация (module-communication) Как модули могут вызывать друг друга и как отправлять события? Для межмодульных вызовов используйте module.call; если другой модуль объявил meta.services, можно вызывать только методы из белого списка
Права доступа (scope) Кто может использовать этот модуль, кто может запускать события, какие действия разрешены? Три уровня — привязка модуля, допуск идентичности, ограничение действий, всё определяется пользовательскими настройками
Принадлежность (ownership) Что происходит с соединениями, таймерами, подписками модуля после его выгрузки? Кто зарегистрировал, тот и отвечает, при выгрузке ресурсы автоматически удаляются
Ленивая загрузка и сон (lazy-loading) Когда модуль загружается и куда уходит в простой? Модуль загружается при использовании (activate_on), в простой переходит (dormant_after)
Взаимодействие (interaction) Как ждать следующего ответа пользователя, как писать многошаговые сборы? wait_reply ждёт одного сообщения, .got() цепочкой собирает несколько, сеансы гарантируют отсутствие конфликтов
Тень и градация (shadow) Как можно протестировать новую версию модуля, не влияя на основную? Теневой модуль получает копии, исходящие сообщения записываются, переход к основному происходит только после подтверждения
Хранилище (storage-backends) Где хранятся данные, как можно менять хранилище? Одна и та же API хранения, sqlite / mysql / postgres переключаются через конфигурацию

4. Экосистема и официальный клиент

Официальный клиент + модули экосистемы, которые можно устанавливать по мере необходимости (не входят в состав фреймворка).

Документ Описание
Обзор экосистемы Как устанавливать модули экосистемы, почему они не входят в состав фреймворка
ErisPulse-App Официальный клиент для всех платформ (Android / Windows / Linux / macOS): нативный интерфейс для управления несколькими экземплярами, запуск на телефоне, автономный трей на рабочем столе
ErisPulse-Dashboard Веб-панель управления + API регистрации виджетов (модули могут регистрировать собственные страницы в боковой панели)
ErisPulse-Cron Модуль для планирования задач: одноразовые / периодические / Cron-выражения, передача параметров в обратный вызов, SQLite-хранилище, другие модули могут делегировать планирование обратных вызовов
ErisPulse-Takumi Рендеринг изображений (HTML / дерево узлов / SVG / анимации, встроенные шрифты на китайском и английском языках)

5. Я хочу внести вклад в ErisPulse

Сделать фреймворк лучше

Документ Описание
Вклад в ErisPulse Обзор способов вклада: документация / локализация / ошибка / модуль / адаптер
Первый вклад От форка до отправки PR

Справочник по расширенным функциям по версиям

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

Версия Введённые возможности
2.7 Базовая платформа для продакшена: стандартизированный базовый класс OneBot12 (ApiDSL / BaseConverter), систематизированная горячая перезагрузка конфигурации, элегантное завершение работы и обработка сигналов SIGTERM/SIGHUP, перекрытие переменных окружения, хуки обратных вызовов жизненного цикла, ленивая загрузка Web-стека; в 2.7.1 исправлены проблемы с надежностью записи конфигурации и управлением поколениями GC
2.8 Основа для прав и коммуникации: трёхмерная система области действия (scope), инфраструктура интерактивных сессий (аренда / контрольные точки / транзакции сообщений / ящик сессии transcript), модульный RPC-протокол (сервисные контракты module.call / meta.services / направленные события emit(to=)), асинхронное хранение данных с несколькими бэкендами (sqlite / mysql / postgres, не блокирует работу при ошибках подключения), очистка прав собственности, сохранение комментариев в конфигурации; последующие версии 2.8.x улучшают CLI-скелет, подкоманды и т.д.
2.9 Основная линия улучшений "опыта разработки модулей" (EPRFC-2026-001): декларативные параметры команд (args= / options=) и управление командами (cooldown= / rate_limit=), единая инъекция зависимостей (Depends), право промежуточных обработчиков на отклонение событий, встроенный ORM (автоматическое миграция + сопоставление отношений), модули-теневые для постепенного перехода в продакшн, трассировка и диагностика цепочки принятия решений при распространении, официальная поддержка Python 3.14
2.10 Ресурсы связи первого класса (реестр connections: трансляции / деление по бизнес-группам / повторное использование между модулями / автоматическая очистка при отключении), корневой импортный контракт (все основные API доступны через from ErisPulse import), завершение системы прав собственности, перекрытие стратегии сохранения ящика сессии в режиме выполнения; в 2.10.2 адаптированы локальные каталоги плагинов адаптера (plugins/) + epsdk create pack + безопасная запись конфигурации с помощью tomlclass
2.11 (dev) Замкнутый цикл разработки "изменить код → увидеть результат": горячая перезагрузка по умолчанию с полной семантикой (предварительная проверка синтаксиса + проверка идентичности кода + повторная перезагрузка зависимых компонентов), ленивая загрузка в спящем режиме (dormant_after), горячая перезагрузка кода адаптера на уровне кода (sdk.adapter.reload), синтаксический сахар для командной последовательности .got(), управление в режиме выполнения командами epsdk stop / restart

Способы разработки

ErisPulse поддерживает два способа разработки:

Другое

Получите помощь

Связанные ссылки

快速开始

来源: docs/ru/quick-start.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/quick-start.md)

Быстрый старт

Это ваш первый шаг. Запустите робота ErisPulse за 5 минут — сначала посмотрите результат, а затем разберитесь с каждым шагом.

Эффект за 60 секунд: заставьте бота сначала ответить вам

1. Установка (Python >= 3.10):

pip install ErisPulse

2. Инициализация проекта. Мастер запросит имя проекта, позволит выбрать платформу адаптера и завершит настройку, после чего сгенерирует main.py и config/config.toml:

epsdk init

3. Откройте main.py и замените его на эти 9 строк кода:

from ErisPulse import sdk
from ErisPulse.Core.Event import command

@command("ping", help="Проверка, работает ли бот")
async def ping_handler(event):
    await event.reply("Pong! Бот работает нормально.")

if __name__ == "__main__":
    import asyncio
    asyncio.run(sdk.run(keep_running=True))

4. Запуск:

epsdk run main.py

В терминале появятся логи запуска фреймворка и информация о подключении адаптера. Теперь, если вы отправите боту /ping на подключенной платформе, он ответит:

Pong! Бот работает нормально.

Если вы увидели этот ответ, значит, цепочка фреймворка, адаптера и обработки событий заработала. Если не получилось, просто перейдите в конец документа по ссылке Возникли проблемы; если всё прошло успешно, читайте дальше, чтобы узнать, какие ещё есть варианты на каждом этапе.

Установка ErisPulse: три способа

Выше использовался самый прямой способ — pip. Выбирайте в зависимости от вашей среды:

Установка с помощью pip

Убедитесь, что у вас установлен Python версии >= 3.10, затем установите с помощью pip:

pip install ErisPulse

Если у вас установлен uv, можно использовать uv pip install ErisPulse — установка будет быстрее.

Если вы хотите установить команду epsdk глобально, чтобы не загрязнять окружение проекта, рекомендуется использовать uv tool install:

uv tool install ErisPulse

После установки команда epsdk будет доступна глобально: при запуске в директории проекта автоматически определяется .venv проекта (команда epsdk install устанавливает в окружение проекта, epsdk run запускает в окружении проекта), а сам фреймворк предоставляется из окружения инструмента. Подробнее см. руководство по установке.

Сценарий установки за один клик (рекомендуется для новых сред)

Сценарий автоматически определяет вашу среду (Docker, Python, uv) и предлагает наиболее подходящий способ установки, поддерживает несколько языков (китайский/English/日本語/Русский/繁體中文).

Windows (PowerShell):

irm https://get.erisdev.com/install.ps1 -OutFile install.ps1; powershell -ExecutionPolicy Bypass -File install.ps1

macOS / Linux:

curl -fsSL https://get.erisdev.com/install.sh -o install.sh && chmod +x install.sh && ./install.sh

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

Использование Docker

В Docker-образе уже включён фреймворк ErisPulse и панель управления Dashboard, поддерживается архитектура linux/amd64 и linux/arm64.

# Скачайте docker-compose.yml
curl -O https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/docker-compose.yml

# Установите токен панели управления и запустите
ERISPULSE_DASHBOARD_TOKEN=your-token docker compose up -d
Не работает Docker Hub?

Используйте образ из GitHub Container Registry, изменив image в docker-compose.yml:

image: ghcr.io/erispulse/erispulse:latest

После запуска перейдите по адресу http://<host>:8000/Dashboard и войдите, используя установленный токен.

Инициализация проекта

Интерактивная инициализация (рекомендуется)

epsdk init

Это запустит интерактивное руководство, которое поможет вам выполнить следующие шаги:

Быстрая инициализация

# Быстрый режим с указанием имени проекта
epsdk init -q -n my_bot

# Или просто указание имени проекта
epsdk init -n my_bot

Создание проекта вручную

Если вы предпочитаете создавать проект вручную:

mkdir my_bot && cd my_bot
epsdk init

Установка модуля

Установка через CLI

epsdk install Yunhu AIChat

Просмотр доступных модулей

epsdk list-remote

Интерактивная установка

При отсутствии имени пакета открывается интерактивный интерфейс установки:

epsdk install

Запуск проекта

# Обычный запуск
epsdk run main.py

# Режим горячей перезагрузки (рекомендуется при разработке)
epsdk run main.py --reload

Включение автодополнения в IDE (по желанию)

Динамически обнаруживаемые модули/адаптеры ErisPulse не поддерживаются автодополнением в IDE по умолчанию. Запустите следующую команду для генерации типовых заглушек:

epsdk types

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

from _ep_types import Yunhu
from ErisPulse import sdk

adapter: Yunhu = sdk.adapter.get("yunhu")
await adapter.Send.To("group", "123").Board(...)  # Автодополнение методов, специфичных для платформы

Структура проекта

Структура проекта после инициализации:

my_bot/
├── config/
│   └── config.toml          # Файл конфигурации
└── main.py                  # Точка входа

Конфигурационный файл

Базовый конфигурационный файл config.toml:

[ErisPulse.server]
host = "0.0.0.0"
port = 8000

[ErisPulse.logger]
level = "INFO"

[Yunhu_Adapter]
# Конфигурация адаптера

Далее

После запуска бота выберите дальнейшие шаги в соответствии с вашими целями:

Хотите создать групповой бот (обработка сообщений, управление группой, уведомления)

Хотите написать свой модуль / адаптер

Хотите запустить бота на постоянной основе

Возникли проблемы

第一个机器人

来源: docs/ru/getting-started/first-bot.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/getting-started/first-bot.md)

Создание первого бота

Tip

Когда стоит читать эту статью: бот уже запускается, и вы хотите написать первый обработчик команды /hello, а также разобраться, что именно делают sdk.run и декораторы команд. Если вы ещё не установили ErisPulse и не инициализировали проект, сначала выполните три шага из быстрого старта за 5 минут: «Установка», «Инициализация проекта» и «Запуск проекта»; если всё готово — пропустите.

Успешно пройденный быстрый старт — это лишь первый шаг. Эта статья поможет вам написать первый обработчик команды и объяснит, что происходит с каждой строкой кода внутри фреймворка.

Шаг первый: написание первой команды

Откройте main.py и напишите простой обработчик команды:

from ErisPulse import sdk
from ErisPulse.Core.Event import command

@command("hello", help="Отправить приветственное сообщение")
async def hello_handler(event):
    """Обработка команды hello"""
    user_name = event.get_user_nickname() or "друг"
    await event.reply(f"Привет, {user_name}! Я бот ErisPulse.")

@command("ping", help="Проверить, работает ли бот")
async def ping_handler(event):
    """Обработка команды ping"""
    await event.reply("Pong! Бот работает нормально.")

async def main():
    """Основная точка входа"""
    print("Запуск ErisPulse...")
    
    # keep_running=True (по умолчанию): фреймворк блокируется и продолжает работу, пока не получит сигнал о завершении (например, Ctrl+C)
    await sdk.run(keep_running=True)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Параметр keep_running

Параметр sdk.run(keep_running) управляет тем, блокируется ли фреймворк для поддержания работы:

async def main():
    await sdk.run(keep_running=False)   # Инициализация завершена, немедленно возвращаем управление
    # Фреймворк уже работает в фоне, здесь можно делать что-то ещё
    while True:
        await asyncio.sleep(3600)
        print("Проверка каждый час")

Помимо двух режимов run(), существуют более тонкие способы управления жизненным циклом, такие как init()/uninit() для ручного управления, а также отдельный запуск и остановка адаптеров/маршрутизации. Подробнее см. Жизненный цикл и ручное управление.

Шаг второй: Запуск робота

# Обычный запуск
epsdk run main.py

# Режим разработки (поддержка горячей перезагрузки)
epsdk run main.py --reload

Шаг 3: Тестирование бота

Отправьте команду в вашей системе общения:

/hello

Вы должны получить ответ от бота.

Описание кода

Декоратор команды

@command("hello", help="Отправить приветственное сообщение")

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

async def hello_handler(event):

Параметр event представляет собой объект Event, содержащий:

Полный список методов объекта Event см. в разделе Подробное описание Event-объекта.

Отправка ответа

await event.reply("Содержимое ответа")

Метод event.reply() — это удобный способ отправки сообщения отправителю.

Расширение: добавление дополнительных функций

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

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

Команда не отвечает?

  1. Проверьте, правильно ли настроен адаптер, убедитесь, что status адаптера в файле config/config.toml установлен в true
  2. Проверьте вывод логов в терминале, убедитесь, что нет ошибок (особенно сообщений уровня ERROR)
  3. Убедитесь, что префикс команды верный (по умолчанию это /), можно проверить в разделе [ErisPulse.event.command] файла конфигурации
  4. Убедитесь, что имя команды написано правильно, обратите внимание на настройки чувствительности к регистру

Как изменить префикс команды?

Добавьте в config.toml следующее:

[ErisPulse.event.command]
prefix = "!"
case_sensitive = false

Как поддерживать несколько платформ?

ErisPulse использует стандарт OneBot12 для унификации формата событий на разных платформах. Обработчики, зарегистрированные с помощью @command и @message, автоматически получают события со всех платформ. С помощью event.get_platform() можно определить источник платформы:

@command("hello")
async def hello_handler(event):
    platform = event.get_platform()
    
    if platform == "yunhu":
        await event.reply("Привет! От Юньху")
    elif platform == "telegram":
        await event.reply("Hello! От Telegram")
    else:
        await event.reply("Привет!")

Дополнительные советы по адаптации под несколько платформ можно найти в разделе Примеры типичных задач.

Следующие шаги

基础概念

来源: docs/ru/getting-started/basic-concepts.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/getting-started/basic-concepts.md)

Основные концепции

Note

Обязательно к прочтению для L1 Core — 2-я из «Пяти статей L1 Core» на главной странице документации (Введение в разработку модулей → Основные концепции → Класс-обёртка Event → Описание файлов конфигурации → Перехват исключений).

Какие компоненты проходит сообщение от отправки пользователем до ответа бота? За что отвечают адаптеры и за что модули? Прочитав эту статью, вы сможете самостоятельно отвечать на вопросы по дизайну, возникающие при написании обработчиков, вместо того чтобы просто копировать примеры.

Событийно-ориентированная архитектура

ErisPulse использует событийно-ориентированную архитектуру: все взаимодействия передаются и обрабатываются через события.

Поток событий

Пользователь отправляет сообщение
      │
      ▼
Платформа принимает
      │
      ▼
Адаптер принимает нативное событие платформы
      │
      ▼
Преобразование в стандартное событие OneBot12
      │
      ▼
Передача в систему событий
      │
      ▼
Распределение зарегистрированным обработчикам
      │
      ▼
Модуль обрабатывает событие
      │
      ▼
Отправка ответа через адаптер
      │
      ▼
Платформа отображает пользователю

Стандарт OneBot12

ErisPulse использует OneBot12 в качестве основного стандарта событий. OneBot12 — это универсальный стандарт интерфейса приложений чат-ботов, определяющий единый формат событий.

Все адаптеры преобразуют события, специфичные для платформы, в формат OneBot12, обеспечивая согласованность кода.

Основные компоненты

1. Объект SDK

SDK — это единая точка входа для всей функциональности, предоставляющая доступ к основным компонентам.

from ErisPulse import sdk

# Доступ к основным модулям
sdk.storage    # Система хранения
sdk.config     # Система конфигурации
sdk.logger     # Система логирования
sdk.adapter    # Система адаптеров
sdk.module     # Система модулей
sdk.router     # Система маршрутизации
sdk.client     # HTTP-клиент
sdk.lifecycle  # Система жизненного цикла

Warning

Импортируйте единообразно из корня: from ErisPulse.Core import adapter / import ErisPulse.Core.adapter Такая запись привязывается к экземпляру менеджера (синглтону AdapterManager), а не к субмодулю — re-export пакета ErisPulse.Core затеняет одноимённый субмодуль. При написании тестов, патчинге или когда требуется сам объект модуля, всегда используйте корневой импорт from ErisPulse import sdk (sdk.adapter — это и есть экземпляр менеджера).

2. Объект Event

Объект Event инкапсулирует данные события и предоставляет удобные методы доступа.

@command("info")
async def info_handler(event):
    # Получение информации о событии
    event_id = event.get_id()
    user_id = event.get_user_id()
    platform = event.get_platform()
    text = event.get_text()
    
    # Отправка ответа
    await event.reply(f"Пользователь: {user_id}, платформа: {platform}")

3. Адаптеры

Адаптеры — это мост между ErisPulse и внешними платформами.

Обязанности:

Примеры адаптеров:

4. Модули

Модули — это базовые единицы расширения функциональности, которые могут:

Механизм обнаружения модулей

ErisPulse обнаруживает установленные модули с помощью Python-механизма importlib.metadata.entry_points. Модуль объявляет точку входа в pyproject.toml:

[project.entry-points."erispulse.module"]
MyModule = "my_package:Main"

При инициализации SDK сканируются все точки входа группы erispulse.module, классы модулей регистрируются в ModuleManager, а затем инициализируются по очереди после топологической сортировки по зависимостям.

Минимальный рабочий модуль

from ErisPulse.Core.Bases import BaseModule
from ErisPulse import sdk

class Main(BaseModule):
    def __init__(self):
        self.sdk = sdk
        self.logger = sdk.logger.get_child("MyModule")

    async def on_load(self, event):
        self.logger.info("Модуль загружен")

    async def on_unload(self, event):
        self.logger.info("Модуль выгружен")

Жизненный цикл модуля

Стратегия загрузки

Поведение загрузки модуля объявляется через get_load_strategy():

from ErisPulse.loaders import ModuleLoadStrategy

class Main(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(
            lazy_load=True,   # Ленивая загрузка (по умолчанию True)
            priority=0        # Приоритет загрузки; чем больше число, тем раньше инициализация
        )

Подробное описание механизма ленивой загрузки см. в Система ленивой загрузки.

Типы событий

ErisPulse поддерживает 5 типов событий:

Тип события Декоратор Описание
События сообщений @message.on_message() Любые сообщения, отправленные пользователями (личные и групповые чаты)
События команд @command("name") Сообщения, начинающиеся с префикса команды (например, /hello)
События уведомлений @notice.on_friend_add() и др. Системные уведомления (добавление в друзья, изменение участников группы и т.д.)
События запросов @request.on_friend_request() и др. Запросы пользователей (заявки в друзья, приглашения в группы)
Мета-события @meta.on_connect() и др. События системного уровня (подключение, отключение, heartbeat)

Подробное использование и примеры кода для каждого типа событий см. в Введение в обработку событий.

Описание основных модулей

Storage (Хранилище)

Система хранения «ключ-значение» на основе SQLite для сохранения данных.

# Установка значения
sdk.storage.set("key", "value")

# Получение значения
value = sdk.storage.get("key", "default_value")

# Массовые операции
sdk.storage.set_multi({
    "key1": "value1",
    "key2": "value2"
})

# Транзакции
with sdk.storage.transaction():
    sdk.storage.set("key1", "value1")
    sdk.storage.set("key2", "value2")

Config (Конфигурация)

Управление файлами конфигурации в формате TOML.

# Получение конфигурации
config = sdk.config.getConfig("MyModule", {})

# Установка конфигурации
sdk.config.setConfig("MyModule", {"key": "value"})

# Чтение вложенной конфигурации
value = sdk.config.getConfig("MyModule.subkey", "default")

Logger (Логирование)

Модульная система логирования.

# Запись в лог
sdk.logger.info("Это информационное сообщение")
sdk.logger.warning("Это предупреждение")
sdk.logger.error("Это ошибка")

# Получение дочернего логгера
child_logger = sdk.logger.get_child("submodule")
child_logger.info("Лог субмодуля")

Синтаксический сахар доступа через атрибуты

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

# Создание дочернего логгера через доступ к атрибутам
sdk.logger.mymodule.info("Сообщение модуля")

# Поддерживается вложенный доступ
sdk.logger.mymodule.database.info("Сообщение базы данных")

Router (Маршрутизация)

Управление HTTP- и WebSocket-маршрутами на основе FastAPI + Uvicorn. Поддерживает маршрутизацию через декораторы, промежуточное ПО, группировку, ограничение частоты запросов и CORS.

from ErisPulse.Core import HttpRequest

@sdk.router.get("MyModule", "/api")
async def handler(request: HttpRequest):
    data = await request.json()
    return {"status": "ok"}

Полный API маршрутизации (WebSocket, промежуточное ПО, ограничение частоты запросов, CORS и т.д.) см. в Менеджер маршрутов.

Client (Сетевой клиент)

Унифицированный сетевой клиент, объединяющий HTTP-запросы, WebSocket-соединения, управление пулом соединений, автоматические повторные попытки, контроль таймаутов, статистику запросов и интеграцию с событиями жизненного цикла.

from ErisPulse.Core import client

# HTTP-запрос
resp = await client.get("https://api.example.com/users")
data = await resp.json()

# С повторными попытками и таймаутом
resp = await client.get(url, timeout=30, max_retries=3)

# WebSocket-соединение
ws = await client.ws_connect("wss://example.com/ws")
async for text in ws.iter_text():
    await ws.send_text(f"Echo: {text}")

Полный API сетевого клиента см. в Сетевой клиент.

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

Адаптеры предоставляют интерфейс отправки сообщений в виде цепочки вызовов.

Базовая отправка

# Получение экземпляра адаптера
yunhu = sdk.adapter.get("yunhu")

# Отправка сообщения
await yunhu.Send.To("user", "U1001").Text("Hello")

# Указание аккаунта для отправки
await yunhu.Send.Using("bot1").To("group", "G1001").Text("Сообщение в группу")

Цепочка модификаторов

# Упоминание пользователя
await yunhu.Send.To("group", "G1001").At("U2001").Text("@сообщение")

# Ответ на сообщение
await yunhu.Send.To("group", "G1001").Reply("msg123").Text("Ответ")

# Упоминание всех
await yunhu.Send.To("group", "G1001").AtAll().Text("Объявление")

Методы ответа Event

Объект Event предоставляет удобные методы ответа:

@command("test")
async def test_handler(event):
    # Простой текстовый ответ
    await event.reply("Содержимое ответа")
    
    # Отправка изображения
    await event.reply("http://example.com/image.jpg", method="Image")
    
    # Отправка голосового сообщения
    await event.reply("http://example.com/voice.mp3", method="Voice")

Система ленивой загрузки

ErisPulse по умолчанию включает ленивую загрузку модулей: модуль инициализируется только при первом обращении (например, sdk.MyModule), что значительно ускоряет запуск.

from ErisPulse.loaders import ModuleLoadStrategy

class Main(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(
            lazy_load=True,   # Включение ленивой загрузки (по умолчанию)
            priority=0        # Приоритет загрузки; чем больше число, тем раньше инициализация
        )

Сценарии, требующие отключения ленивой загрузки (lazy_load=False):

Подробное описание механизма ленивой загрузки и важных замечаний см. в Система ленивой загрузки.

Следующие шаги

事件处理

来源: docs/ru/getting-started/event-handling.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/getting-started/event-handling.md)

Введение в обработку событий

Tip

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

Обработчики событий — это место, где бот "понимает" пользователя и реагирует на него. В этой статье приведены готовые шаблоны для пяти типов событий, а также рассмотрены такие практические сценарии, как анализ параметров, управление правами, ограничение частоты и многоконтурные взаимодействия.

Обзор типов событий

ErisPulse поддерживает следующие типы событий:

Тип события Описание Применение
Событие сообщения Любое сообщение, отправленное пользователем Чат-боты, фильтрация контента
Событие команды Сообщение, начинающееся с префикса команды Обработка команд, точка входа
Событие уведомления Системное уведомление (добавление в друзья, изменения в группе и т.д.) Приветствия, уведомления о состоянии
Событие запроса Запрос пользователя (запрос в друзья, приглашение в группу) Автоматическая обработка запросов
Событие мета Системное событие (подключение, сердцебиение) Мониторинг подключения, проверка состояния

Обработка событий сообщений

Примечание: Рекомендуется использовать аннотацию типа Event в обработчиках событий, чтобы получить поддержку автодополнения и проверки типов в IDE.

from ErisPulse.Core.Event import Event  # Импорт типа события для аннотации

Отслеживание всех сообщений

from ErisPulse.Core.Event import message, Event

@message.on_message()
async def message_handler(event: Event):
    text = event.get_text()
    user_id = event.get_user_id()
    sdk.logger.info(f"Получено сообщение от {user_id}: {text}")

Отслеживание личных сообщений

@message.on_private_message()
async def private_handler(event: Event):
    user_id = event.get_user_id()
    await event.reply(f"Привет, {user_id}! Это личное сообщение.")

Отслеживание групповых сообщений

@message.on_group_message()
async def group_handler(event: Event):
    group_id = event.get_group_id()
    user_id = event.get_user_id()
    sdk.logger.info(f"В группе {group_id} пользователь {user_id} отправил сообщение")

Отслеживание сообщений с упоминанием

@message.on_at_message()
async def at_handler(event: Event):
    # Получить список упомянутых пользователей
    mentions = event.get_mentions()
    await event.reply(f"Вы упомянули этих пользователей: {mentions}")

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

Четыре декоратора сообщения (on_message / on_private_message / on_group_message / on_at_message) поддерживают pattern (подстановочные знаки glob) и regex (регулярные выражения), не соответствующие сообщения не запускают обработчик:

# Подстановочные знаки glob: * любая строка, ? один символ, [seq] набор символов
@message.on_message(pattern="签到*")
async def signin_handler(event: Event):
    await event.reply("签到成功")

# Регулярное выражение: совпадение суммы
@message.on_message(regex=r"\d+\s*元")
async def price_handler(event: Event):
    await event.reply(f"Получена сумма: {event.get_text()}")

# pattern и regex одновременно → оба должны соответствовать
@message.on_message(pattern="*元", regex=r"\d+\s*元")
async def combined_handler(event: Event):
    pass

wait_reply также поддерживает эти два параметра (см. Функция ожидания ответа).

Обработка событий команд

Основные команды

from ErisPulse.Core.Event import command

@command("help", help="Отображает справочную информацию")
async def help_handler(event):
    help_text = """
Доступные команды:
/help - Показать справку
/ping - Тест подключения
/info - Просмотр информации
    """
    await event.reply(help_text)

Псевдонимы команд

@command(["help", "h"], aliases=["помощь"], help="Отображает справочную информацию")
async def help_handler(event):
    await event.reply("Справочная информация...")

Пользователь может вызывать команду любым из следующих способов:

Аргументы команд

@command("echo", help="Вернуть сообщение")
async def echo_handler(event):
    # Получить аргументы команды
    args = event.get_command_args()
    
    if not args:
        await event.reply("Введите сообщение для повтора")
    else:
        await event.reply(f"Вы сказали: {' '.join(args)}")

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

Декларативные аргументы и опции (args= / options=)

Ручная обработка аргументов требует ручного преобразования типов и сообщений об ошибках. При декларации args= / options= фреймворк автоматически анализирует аргументы команды и внедряет их по именам в обработчик после прохождения проверки прав доступа; при неверном вводе пользователем автоматически возвращаются локализованные сообщения и инструкции (без выброса исключений и аварийного завершения), а команда /help <команда> автоматически отображает инструкции:

@command(
    "roll",
    args="<count:int> [sides:int=6]",
    options={"verbose": "-v/--verbose", "label": "--label"},
    help="Бросить кубик",
)
async def roll_handler(event, count: int, sides: int = 6, verbose: bool = False, label: str = ""):
    total = sum(random.randint(1, sides) for _ in range(count))
    await event.reply(f"Бросил {count} раз {sides}-гранного кубика, общее количество очков: {total}")

Синтаксис позиционных аргументов args=: <count:int> обязательный, [sides:int=6] необязательный (с значением по умолчанию). Поддерживаемые типы:

Тип Пример ввода Описание
str hello Текст (тип по умолчанию)
int / float 3 / 0.5 Число
bool да / yes / はい / да / true / no / отмена Логическое значение, использует таблицу подтверждения из Event.confirm()
literal `<mode:literal=fast slow>`
duration 90s、1h30m、1d Продолжительность, преобразуется в секунды как float
rest <text:rest> Остальной текст (должен быть последним)

options= опции объявляются в виде словаря: ключ — имя параметра обработчика, значение — формат флагов (множество алиасов через /). Параметры с аннотацией bool — это булевы флаги (появление означает True); остальные (по умолчанию как str) — опции со значениями, поддерживают --label hello и --label=hello для значений, тип соответствует аннотации обработчика. Опции сначала распознаются и удаляются, оставшиеся токены обрабатываются по args= (остаток rest покрывает оставшийся текст после удаления опций).

Особенности поведения:

Управление командами (cooldown= / rate_limit= / usage_limit= / deprecated=)

Ручная реализация таймеров, ограничения скорости, лимитов использования и устаревших команд может быть заменена декларациями, все четыре параметра могут быть комбинированы.

Таймер — синтаксис длительности аналогичен типу duration в args= (например, "30s"、"1h30m"、"1d"):

@command("daily", cooldown="1d", cooldown_key="user", cooldown_reply="Вы уже отметились сегодня")
async def daily_handler(event):
    await event.reply("Отметка успешно выполнена!")

Ограничение скорости — декларация скользящего окна в формате "количество/окно" (например, "5/minute"、"10/s"、"3/2m"):

@command("search", rate_limit="5/minute", rate_limit_key="user")
async def search_handler(event):
    await event.reply("Результаты поиска")

Лимит использования — максимальное количество вызовов в периоде (например, "100/day"、"10/hour"、"500/30d"), при превышении лимита выполнение отклоняется:

@command("translate", usage_limit="100/day", usage_limit_key="user",
         usage_limit_reply="Лимит переводов на сегодня исчерпан")
async def translate_handler(event):
    ...

В отличие от таймера и ограничения скорости, лимит использования сохраняется в хранилище (ключ erispulse.usage.<key>, не теряется при перезапуске): при недоступности хранилища автоматически переходит в чисто инкрементный счётчик в памяти с предупреждением (в этом случае лимит сбрасывается при перезапуске). Счётчик сбрасывается при смене периода лимита, при выгрузке модуля — очищается. Устаревание — при вызове автоматически возвращается сообщение устаревания, deprecated_reject=True отклоняет выполнение:

@command("oldcmd", deprecated="Пожалуйста, используйте /newcmd", deprecated_reject=True)
async def old_handler(event): ...

Гранулярность ключей (cooldown_key= / rate_limit_key=): "user" (по умолчанию, общие для одного пользователя), "session" (общие для одной сессии, например, в одной группе), "global" (общие для всех пользователей и сессий).

Особенности поведения:

Ограничение скорости обработчиков (throttle=) и дебаунсинг (debounce=)

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

from ErisPulse import sdk

@sdk.message.on_message(throttle="2s", throttle_key="user")
async def handler(event): ...

Дебаунсинг и ограничение скорости дополняют друг друга: в окне выполняется только последнее событие, предыдущие задачи автоматически отменяются (подходит для сценариев, где нужно обрабатывать только после окончания ввода, например, автозаполнение поиска):

@sdk.message.on_message(debounce="2s", debounce_key="user")
async def search(event): ...

on_message / on_private_message / on_group_message / on_at_message поддерживаются; throttle_key= / debounce_key= используют ту же гранулярность ключей, что и управление командами (user / session / global), синтаксис длительности аналогичен duration. Ограничение скорости и pattern= / regex= и другие условия работают в совокупности (все должны выполняться, чтобы сработало); в интервале отбрасываются события, записываются в лог TRACE; декларации проверяются на этапе регистрации; throttle= и debounce= семантически взаимоисключающие (одновременная декларация вызывает ValueError).

Дебаунсинг не прерывает бизнес-логику: только задачи, которые ещё не пересекли окно ожидания, отменяются; уже пересекшие окно и выполняющиеся обработчики не отменяются новыми событиями (избегает остановки на любом await-точке и частичных побочных эффектов).

Внедрение зависимостей (Depends)

Общие зависимости (сессия базы данных, чтение конфигурации и т.д.) можно вынести в функции зависимостей, обработчики объявляют параметры с Depends(функция_зависимости) в качестве значения по умолчанию, фреймворк автоматически вызывает функцию зависимости с объектом контекста и внедряет результат по именам перед вызовом:

from ErisPulse.Core import Depends

async def get_session(event):
    return await sdk.module.call("DB", "get_session")

@command("admin")
async def admin_handler(event, db=Depends(get_session)):
    ...

По умолчанию включено кэширование на уровне запроса: в рамках одной обработки события одна и та же функция зависимости вызывается только один раз, результаты всех точек внедрения разделяются (например, get_db в рамках одного события создает только одну сессию базы данных); между запросами кэш не используется. Можно отключить кэш для отдельной зависимости с Depends(get_db, use_cache=False).

Охватывает все точки внедрения фреймворка — обработчики команд, обработчики событий (message.on_message() и т.д.), жизненный цикл (sdk.lifecycle.on), обработчики маршрутов SSE. Первый параметр функции зависимости — объект контекста точки внедрения (в случае события — Event, в случае жизненного цикла — data, в случае маршрута — HttpRequest / SseEmitter); можно объявлять как синхронные, так и асинхронные функции зависимости.

Синтаксический сахар для объявления сервисов других модулей:

@command("query")
async def query_handler(event, session=Depends.module("DB", "get_session")):
    ...

Depends.module(имя_модуля, имя_метода, *постоянные_аргументы) эквивалентно вызову sdk.module.call(...) внутри функции зависимости. Инициализация модуля (__init__) не входит в область охвата — при инициализации нет объекта контекста; для маршрутов HTTP, использующих FastAPI, используйте оригинальный fastapi.Depends фреймворка FastAPI.

Особенности поведения:

Группы команд

@command("admin.reload", group="admin", help="Перезагрузить модуль")
async def reload_handler(event):
    await event.reply("Модуль перезагружен")

@command("admin.stop", group="admin", help="Остановить бота")
async def stop_handler(event):
    await event.reply("Бот остановлен")

Параметр group используется только для группировки в справке; в примере выше admin.reload — это единое имя команды (точка — стиль именования, пользователь должен ввести /admin.reload).

Подкоманды

Имена команд поддерживают многотокенную структуру с разделением пробелами, реализуя подкоманды вроде /admin add, /admin user ban:

@command("admin", help="Команды администрирования")
async def admin_handler(event):
    await event.reply("Синтаксис: /admin add | /admin remove")

@command("admin add", help="Добавить администратора")
async def admin_add_handler(event):
    target = event.get_command_args()[0]
    await event.reply(f"Добавлен {target}")

@command("admin remove", aliases=["a remove"], help="Удалить администратора")
async def admin_remove_handler(event):
    await event.reply("Удалён")

Правила сопоставления (максимальное префиксное совпадение):

Наследование прав доступа: если подкоманда не декларирует permission, она автоматически наследует ближайшую родительскую команду, декларирующую permission — защита /admin автоматически защищает все её подкоманды; при декларации permission в подкоманде она имеет приоритет:

def is_admin(event):
    return event.get_user_id() in {"user123"}

@command("admin", permission=is_admin, help="Команды администрирования")
async def admin_handler(event):
    ...

# permission не нужно декларировать повторно, наследуется is_admin
@command("admin add", help="Добавить администратора")
async def admin_add_handler(event):
    ...

Примечание: master=True и hidden не наследуются, при необходимости декларируйте отдельно в подкоманде; пользовательские ACL (списки разрешений/запретов) сопоставляются по полному имени команды, шаблоны glob, например, "admin*" могут охватывать целую группу подкоманд.

В обзоре команд /help подкоманды автоматически отображаются вложенно под видимыми родительскими командами (например, admin → admin add с отступом на один уровень, admin user → admin user ban с отступом на два уровня).

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

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

# ① ACL команды (конфигурация со стороны пользователя): по белому и чёрному списку пользователей для команды, при отказе возвращается "Недостаточно прав"
# ② master=True — только владелец фреймворка может выполнить (фреймворк автоматически проверяет, при отказе возвращается "Недостаточно прав")
@command("restart", master=True, help="Перезапустить модуль")
async def restart_handler(event):
    await event.reply("Модуль перезапущен")

# ③ permission=вызов функции — логика управления доступом команды (выполняется, если функция возвращает True)
def is_admin(event):
    return event.get_user_id() in {"user123", "user456"}

@command("panel", permission=is_admin, help="Панель управления")
async def panel_handler(event):
    await event.reply("Добро пожаловать на панель управления")

ACL пользователей команды (ErisPulse.event.command.acl): пользователь может настроить белый и чёрный списки пользователей для любой команды, имена команд поддерживают точное и шаблонное сопоставление (например, "roll*"), при отказе возвращается "Недостаточно прав":

# config.toml — разрешить только 123456 выполнить restart; 666 всегда запрещён
[ErisPulse.event.command.acl.restart]
allow = ["onebot11:123456"]
deny = ["onebot11:666"]

Порядок проверки: если deny совпадает → отказ; если allow не пуст и не совпадает → отказ; если ACL не настроен, используется event.command.default_allow (false = строгий режим, без ACL — отказ; true — передаётся разработчику по умолчанию master=True / permission). API во время выполнения (поддержка шаблонов для имен команд):

from ErisPulse.Core.Event import command

command.allow_user("restart", "onebot11", "123456")   # список разрешённых
command.deny_user("restart", "onebot11", "666")       # список запрещённых
command.remove_acl("restart")                          # очистить белый и чёрный списки
command.get_acl("restart")                             # получить текущий список

Обработчики команд импортируются из пакета событий: from ErisPulse.Core.Event import command; также доступны через пакет SDK: sdk.Event.command (оба являются одним и тем же экземпляром). Обычно в модуле уже импортируются вместе с декоратором команды (from ErisPulse.Core.Event import command).

Управление доступом на уровне события (для конкретного пользователя / группы / бота) — через идентификационные области действия (scope.identity); управление доступностью модуля — через модульные области (scope.platforms / bots / sessions). См. Области действия (scope).

Рекомендация: для взаимодействия с бизнес-логикой внутри команды используйте master=True / permission; для управления доступом по пользователю / группе используйте идентификационные области; для управления доступностью модуля используйте модульные области.

Приоритет команд

# Чем больше значение приоритета, тем раньше выполнение
@message.on_message(priority=10)
async def high_priority_handler(event):
    await event.reply("Обработчик высокого приоритета")

@message.on_message(priority=1)
async def low_priority_handler(event):
    await event.reply("Обработчик низкого приоритета")

Параллельная обработка событий

Система событий ErisPulse использует модель параллельной обработки в пределах одного приоритета, последовательной между разными приоритетами:

Событие поступило
    ↓
группа с приоритетом 10: [обработчик C || обработчик D] параллельно → объединить результаты
    ↓ (если не прервано)
группа с приоритетом 0: [обработчик A || обработчик B] параллельно → объединить результаты
    ↓
...
# Пример: параллельное выполнение обработчиков с одинаковым приоритетом
@message.on_message(priority=0)
async def handler_a(event):
    # Обработка задачи A
    event['result_a'] = process_a()

@message.on_message(priority=0)
async def handler_b(event):
    # Выполняется параллельно с handler_a
    event['result_b'] = process_b()

# Последовательное выполнение с разными приоритетами
@message.on_message(priority=10)
async def handler_c(event):
    # Наивысший приоритет, выполняется первым
    pass

Ограничение параллелизма: все соответствующие задачи немедленно создаются, но ограничиваются сигналом, ограничивающим количество одновременно выполняемых задач, по умолчанию 64 (ErisPulse.framework.handler_max_concurrency, поддержка горячего обновления). Задачи, превышающие лимит, ждут в очереди на сигнале, пока предыдущие не завершатся. Это ваш "сброс давления" при пиковых нагрузках.

Медленные логи: если обработка одной задачи занимает более 1 секунды, фреймворк записывает предупреждение в лог (handler_slow). Время ожидания wait_reply исключается из времени выполнения, чтобы "ожидание ответа" не приводило к ложным предупреждениям о медленной обработке.

Мидлвары: изменение или отклонение до распределения

Мидлвары выполняются до распределения событий по порядку, это правильное место для реализации сценариев, таких как брандмауэр, ограничение скорости, удаление конфиденциальных данных из событий:

from ErisPulse.Core import adapter

@adapter.middleware
async def firewall(data):
    if _is_banned(data.get("user_id")):
        return False          # Отклонение: событие отбрасывается, не попадает ни в один обработчик, без побочных эффектов
    data["checked"] = True    # Возврат dict: изменение данных события (сохранение старого поведения)
    # Возврат None: пропуск, данные события не меняются (старое поведение)
    return data
Возвращаемое значение Действие
False Отклонение: событие немедленно отбрасывается, не попадает ни в один обработчик
dict Изменение данных события и продолжение распределения
None Пропуск, данные события не меняются

При отклонении фреймворк выводит трассировочный лог и запускает хук жизненного цикла adapter.event.blocked (с именем мидлвары и полным событием), что полезно для аудита "почему событие не ответило".

Цепочка принятия решений при распределении команд: почему команда не сработала

Сообщение команды последовательно проходит через: определение текста команды → соответствие имени команды/алиаса (при несовпадении предлагается исправление) → при совпадении команда считается принятой → область действия → пользовательский ACL → владелец → права → холодное ожидание/ограничение скорости → квоты (использование) → устаревание (deprecated, notice/rejected) → разбор параметров → выполнение (промежуточные обработчики могут отклонить на уровне событий, см. предыдущий раздел). Если какое-либо условие не выполняется, процесс прерывается; при нарушении правил (холодное ожидание/ограничение скорости/квоты) действия по умолчанию игнорируются, при отказе по правам пользователь получает ответ, а устаревшие команды отвечают согласно заявленным правилам (ответ/отказ).

В тестовой среде ErisPulse-Testing метод dispatch() напрямую возвращает эту цепочку решений (объект DispatchTrace, trace.explain() выводит построчный отчёт с причинами). В продакшен-среде можно использовать ErisPulse.Core.Event.start_dispatch_trace() для получения аналогичной записи.

Кроме того, ErisPulse.runtime предоставляет два набора API для диагностики: explain_module(имя_модуля) отвечает на вопрос "почему модуль не загрузился" (не зарегистрирован / ленивая загрузка / отключён конфигурацией / отсутствуют зависимости / не соответствует версии SDK, причины перечисляются по порядку), explain_event(событие) отвечает на вопрос "почему событие не обработано" (адаптер не зарегистрирован / аккаунт заблокирован / модуль отключил сессию / команда не соответствует); в связке с format_report() выводится удобочитаемый отчёт.

Фильтрация области: почему мой модуль не получил сообщение

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

  1. Идентификационное измерение (ErisPulse.scope.identity): при входе события в точку распределения проверяется, получает ли оно сообщение по пользователю > группе > боту > адаптеру. Отклоненные все события немедленно отбрасываются, ни один обработчик (включая распределитель команд) не срабатывает.
  2. Модульное измерение (ErisPulse.scope): при поступлении события в обработчик модуля проверяется, доступен ли модуль по сеансу > боту > платформе, если не проходит, молча пропускается.
# Пример 1: все сообщения в группе не распространяются
[ErisPulse.scope.identity.sessions.onebot11."group_123"]
deny = true

# Пример 2: блокировка MyModule для определенного бота
[ErisPulse.scope.bots.onebot11."123456"]
blocked = ["MyModule"]

В этом случае события в группе не будут доставлены обработчикам команд и модуля MyModule. Это не ошибка, а механизм фильтрации — при диагностике "модуль не реагирует" сначала проверьте фильтрацию области по идентификатору и привязке модуля.

Note

Отношение между фильтрацией области и присвоением события (claim): оба молчаливых фильтра происходят до вызова обработчика — обработчики, пропущенные фильтрацией, не имеют возможности выполниться, следовательно, не участвуют в состоянии event.done() / mark_processed(). Событие считается присвоенным только фактически выполненными обработчиками (присвоение командой, присвоение ответом, явный вызов); фильтрация области сама по себе ни присваивает, ни блокирует (молча пропускает, сообщение продолжает проходить оставшуюся цепочку распределения).

Конфигурация области, сопоставление, API времени выполнения смотрите в Области (scope).

Перезапись событий: без изменения кода модуля, перезаписать поведение любого типа событий

Note

Эта функция требует ErisPulse 2.8.0+.

Параметры, объявленные при регистрации обработчика событий (например, pattern / regex / master / hidden), являются по умолчанию для разработчика. Система унифицированной перезаписи позволяет пользователю перезаписать поведение любого модуля по типу события — стандартные типы OneBot12 (meta / message / notice / request) и расширенные типы ErisPulse (command) имеют собственные перезаписываемые параметры:

Тип события Перезаписываемые параметры Действие
message pattern / regex / detail_types Условия срабатывания текста + белый список подтипов сообщений
notice detail_types / pattern / regex Белый список подтипов уведомлений + текстовые условия
request detail_types / pattern / regex Белый список подтипов запросов + текстовые условия
meta detail_types Белый список подтипов мета-событий (connect / heartbeat и т.д.)
command master / hidden / aliases / prefix / help / usage Параметры реализации команды (приоритет для пользователя)
acl (специфично для command) allow / deny Белый и черный списки пользователей команды (по шаблону имени команды)
# message: перезапись условия срабатывания текста (и с условиями в коде AND)
[ErisPulse.event.overrides.message.ChatModule]
pattern = "闲聊*"

# notice: только определенные подтипы уведомлений
[ErisPulse.event.overrides.notice.MyModule]
detail_types = ["group_increase"]

# command: перезапись параметров реализации (приоритет для пользователя — можно ужесточить или ослабить настройки разработчика)
[ErisPulse.event.overrides.command.MyModule.restart]
master = true
hidden = true

# acl: черно-белые списки пользователей команды (для нескольких команд glob)
[ErisPulse.event.overrides.acl."roll*"]
allow = ["onebot11:u_vip"]

# ACL по умолчанию (false = строгий режим: без ACL — отклонить)
acl_default_allow = true

API во время выполнения (from ErisPulse.Core.Event import overrides или sdk.Event.overrides, типы подпространства имен — симметричные set / get / delete для каждого типа):

from ErisPulse.Core.Event import overrides

overrides.message.set("ChatModule", pattern="闲聊*")   # условие текста message
overrides.notice.set("MyModule", detail_types=["group_increase"])
overrides.command.set("MyModule", "restart", master=True)  # параметры команды
overrides.acl.set("roll*", deny=["onebot11:u_bad"])    # черный список пользователей команды

overrides.message.get("ChatModule")     # {"pattern": "闲聊*"}
overrides.message.delete("ChatModule")  # восстановить настройки разработчика

Управление цепочкой: присвоение и блокировка

Note

Параметры event.done() / event.mark_processed() claim= / stop= для этой функции требуют ErisPulse 2.7.1+.

ErisPulse разделяет два ортогональных понятия "присвоение" и "блокировка", объединяя их через event.done(), что удобно для добавления слоев наблюдения (логирование, аудит, права доступа) вокруг обработки команд.

Точное определение двух понятий:

event.done(...) Присвоение Блокировка Сценарий
event.done() ✔ ✔ Стандартный подход после обработки команды / обработчика
event.done(stop=False) ✔ ✘ Только присвоение: низкоприоритетные наблюдатели (логирование / статистика) по-прежнему видят
event.done(claim=False) ✘ ✔ Только блокировка (например, брандмауэр / ограничение скорости), но не делает дедупликацию

event.done(claim=, stop=) является псевдонимом event.mark_processed(claim=, stop=), параметры и поведение полностью эквивалентны.

@command("help")
async def help_cmd(event):
    event.done()            # Присвоение + блокировка (стандартный подход после обработки команды)

@message.on_message(priority=50)
async def observer(event):
    event.done(stop=False)  # Только присвоение: низкоприоритетные обработчики по-прежнему выполняются (логирование / статистика)

@message.on_message(priority=100)
async def firewall(event):
    if denied(event):
        event.done(claim=False)  # Только блокировка: низкоприоритетные обработчики не выполняются, но дедупликация не делается

Настройка блокировки для команд и ответов

Присвоение при сопоставлении команды: как только сообщение сопоставляется с зарегистрированным именем команды (включая подкоманды/псевдонимы), оно будет присвоено и по умолчанию заблокировано распространению — команды, отклоненные правами, больше не будут переданы низкоприоритетным обработчикам сообщений (устранение "повторного ответа" при сопоставлении команды, затем повторном срабатывании on_message).

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

[ErisPulse.event.command]
block = false   # Сообщения команд продолжают распространяться до низкоприоритетных обработчиков (присвоение не влияет, повторная обработка невозможна)

[ErisPulse.event.wait_reply]
block = false   # Ответы, потребленные wait_reply, продолжают распространяться до низкоприоритетных обработчиков

Примечание: block контролирует только блокировку (stop), не влияет на присвоение (claim) — сопоставленные команды никогда не будут повторно обработаны обработчиками сообщений; сообщения, не сопоставленные ни одной команде, по-прежнему распространяются до обработчиков сообщений.

Обработка событий уведомлений

Добавление в друзья

from ErisPulse.Core.Event import notice

@notice.on_friend_add()
async def friend_add_handler(event):
    user_id = event.get_user_id()
    nickname = event.get_user_nickname() or "Новый друг"
    await event.reply(f"Добро пожаловать, {nickname}, добавьте меня в друзья!")

Увеличение участников группы

@notice.on_group_increase()
async def member_increase_handler(event):
    group_id = event.get_group_id()
    user_id = event.get_user_id()
    await event.reply(f"Добро пожаловать, {user_id}, в группу {group_id}")

Уменьшение участников группы

@notice.on_group_decrease()
async def member_decrease_handler(event):
    group_id = event.get_group_id()
    user_id = event.get_user_id()
    await event.reply(f"Пользователь {user_id} покинул группу {group_id}")

Обработка событий запросов

Запрос на добавление в друзья

from ErisPulse.Core.Event import request

@request.on_friend_request()
async def friend_request_handler(event):
    user_id = event.get_user_id()
    comment = event.get_comment()
    
    sdk.logger.info(f"Получен запрос на добавление в друзья: {user_id}, комментарий: {comment}")
    
    # Можно обработать запрос через API адаптера
    # Конкретная реализация см. в документации каждого адаптера

Запрос на группу

@request.on_group_request()
async def group_request_handler(event):
    group_id = event.get_group_id()
    user_id = event.get_user_id()
    
    await event.reply(f"Получено приглашение в группу {group_id} от {user_id}")

Обработка мета-событий

Событие подключения

from ErisPulse.Core.Event import meta

@meta.on_connect()
async def connect_handler(event):
    platform = event.get_platform()
    sdk.logger.info(f"{platform} платформа подключена")

@meta.on_disconnect()
async def disconnect_handler(event):
    platform = event.get_platform()
    sdk.logger.warning(f"{platform} платформа отключена")

Событие сердцебиения

@meta.on_heartbeat()
async def heartbeat_handler(event):
    platform = event.get_platform()
    sdk.logger.debug(f"{platform} сердцебиение")

Запрос состояния бота

После того как адаптер отправляет мета-событие, фреймворк автоматически отслеживает состояние бота, и вы можете в любой момент проверить его:

from ErisPulse import sdk

# Проверить, онлайн ли определенный бот
if sdk.adapter.is_bot_online("telegram", "123456"):
    telegram = sdk.adapter.get("telegram")
    await telegram.Send.To("user", "123456").Text("Бот онлайн")

# Вывести список всех онлайн ботов
bots = sdk.adapter.list_bots()
for platform, bot_list in bots.items():
    for bot_id, info in bot_list.items():
        print(f"{platform}/{bot_id}: {info['status']}")

# Получить полную сводку состояния
summary = sdk.adapter.get_status_summary()

Интерактивная обработка

Использование метода reply для отправки ответа

Метод event.reply() поддерживает различные модификаторы, удобные для отправки сообщений с упоминанием, ответом и т.д.:

# Простой ответ
await event.reply("Привет")

# Отправка различных типов сообщений
await event.reply("http://example.com/image.jpg", method="Image")  # изображение
await event.reply("http://example.com/voice.mp3", method="Voice")  # голосовое сообщение

# Упоминание одного пользователя
await event.reply("Привет", at_users=["user123"])

# Упоминание нескольких пользователей
await event.reply("Всем привет", at_users=["user1", "user2", "user3"])

# Ответ на сообщение
await event.reply("Содержание ответа", reply_to="msg_id")

# Упоминание всех пользователей
await event.reply("Анонс", at_all=True)

# Комбинирование: упоминание пользователей + ответ на сообщение
await event.reply("Содержание", at_users=["user1"], reply_to="msg_id")

Ожидание ответа пользователя

@command("ask", help="Спросить пользователя")
async def ask_handler(event):
    await event.reply("Введите ваше имя:")
    
    # Ожидание ответа пользователя, таймаут 30 секунд
    reply = await event.wait_reply(timeout=30)
    
    if reply:
        name = reply.get_text()
        await event.reply(f"Привет, {name}!")
    else:
        await event.reply("Таймаут ожидания, пожалуйста, повторите ввод.")

Tip

Во время ожидания команды остаются доступными (2.8.3+): сообщения, начинающиеся с префикса команды и соответствующие уже зарегистрированной команде (например, /cancel) выполняются, а не рассматриваются как ответ, ожидание продолжает зависеть — пользователь может в любой момент отменить/переключиться, команда выполнится, и можно продолжить ответ. Для старого поведения, когда "ожидание поглощает весь текст", настройте ErisPulse.event.wait_reply.cmdpass = true или однократно wait_reply(cmdpass=True).

Ожидание с проверкой

@command("age", help="Спросить возраст")
async def age_handler(event):
    def validate_age(event_data):
        """Проверка возраста на валидность"""
        try:
            age = int(event_data.get_text())
            return 0 <= age <= 150
        except ValueError:
            return False
    
    await event.reply("Введите ваш возраст (0-150):")
    
    reply = await event.wait_reply(
        timeout=60,
        validator=validate_age
    )
    
    if reply:
        age = int(reply.get_text())
        await event.reply(f"Ваш возраст: {age} лет")
    else:
        await event.reply("Неверный ввод или таймаут")

Ожидание с обратным вызовом

@command("confirm", help="Подтвердить действие")
async def confirm_handler(event):
    async def handle_confirmation(reply_event):
        text = reply_event.get_text().lower()
        
        if text in ["是", "yes", "y"]:
            await event.reply("Действие подтверждено!")
        else:
            await event.reply("Действие отменено.")
    
    await event.reply("Подтвердить выполнение этого действия? (是/否)")
    
    await event.wait_reply(
        timeout=30,
        callback=handle_confirmation
    )

Подтверждение диалога (confirm)

Ожидание подтверждения или отрицания пользователя, автоматически распознаются встроенные слова подтверждения на китайском и английском:

@command("confirm", help="Подтвердить действие")
async def confirm_handler(event):
    if await event.confirm("Вы уверены, что хотите выполнить это действие?"):
        await event.reply("Подтверждено, выполняется...")
    else:
        await event.reply("Отменено")

# Пользовательские слова подтверждения
if await event.confirm("Продолжить?", yes_words={"go", "继续"}, no_words={"stop", "停止"}):
    pass

Выбор меню (choose)

Пользователь может ответить номером опции или текстом опции:

@command("choose", help="Выбрать")
async def choose_handler(event):
    choice = await event.choose(
        "Выберите цвет:",
        ["красный", "зеленый", "синий"]
    )
    
    if choice is not None:
        colors = ["красный", "зеленый", "синий"]
        await event.reply(f"Вы выбрали: {colors[choice]}")
    else:
        await event.reply("Таймаут выбора")

Объединенный режим: merge_prompt=True вставляет опции в сообщение-подсказку, отправляя все в одном сообщении с указанным методом:

# Отправить объединенную подсказку + опции в Markdown
choice = await event.choose(
    "## Выберите цвет\n{options}\nПожалуйста, введите номер",
    ["красный", "зеленый", "синий"],
    method="Markdown",
    merge_prompt=True,
)

{options} — подстановочный знак для указания позиции вставки опций; если не указан, добавляется в конец подсказки. Можно настроить подстановочный знак через параметр placeholder (например, placeholder="[choices]"). options_format="auto" (по умолчанию) автоматически выбирает стиль в зависимости от метода: Markdown → маркированный список, Html → нумерованный список, другие → простой список. Для текстовых методов (Text/Markdown/Html и т.д.) по умолчанию объединяются опции в конец; для не-текстовых методов (Image и т.д.) по умолчанию отправляются две отдельные сообщения.

Сбор анкеты (collect)

Сбор ввода пользователя в несколько шагов:

@command("register", help="Регистрация")
async def register_handler(event):
    data = await event.collect([
        {"key": "name", "prompt": "Введите имя:"},
        {"key": "age", "prompt": "Введите возраст:", 
         "validator": lambda e: e.get_text().isdigit()},
        {"key": "email", "prompt": "Введите email:"}
    ])
    
    if data:
        await event.reply(f"Регистрация прошла успешно!\nИмя: {data['name']}\nВозраст: {data['age']}\nEmail: {data['email']}")
    else:
        await event.reply("Таймаут регистрации или неверный ввод")

Ожидание произвольного события (wait_for)

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

@command("wait_member", help="Ожидание нового участника")
async def wait_member_handler(event):
    await event.reply("Ожидание нового участника в группе...")
    
    evt = await event.wait_for(
        event_type="notice",
        condition=lambda e: e.get_detail_type() == "group_member_increase",
        timeout=120
    )
    
    if evt:
        await event.reply(f"Добро пожаловать, {evt.get_user_id()}!")
    else:
        await event.reply("Таймаут ожидания")

Многошаговый диалог (conversation)

Создание интерактивного многошагового диалогового контекста:

@command("survey", help="Опрос")
async def survey_handler(event):
    conv = event.conversation(timeout=60)
    
    await conv.say("Добро пожаловать в опрос!")
    
    while conv.is_active:
        reply = await conv.wait()
        
        if reply is None:
            await conv.say("Диалог истек, до свидания!")
            break
        
        text = reply.get_text()
        
        if text == "выход":
            await conv.say("До свидания!")
            break
        
        await conv.say(f"Вы сказали: {text}, продолжайте ввод или ответьте 'выход' для завершения")

Встроенные слова подтверждения

ErisPulse включает в себя набор встроенных слов подтверждения на китайском и английском:

Доступ к данным события

Часто используемые методы объекта Event

@command("info")
async def info_handler(event):
    # Основная информация
    event_id = event.get_id()
    event_time = event.get_time()
    event_type = event.get_type()
    detail_type = event.get_detail_type()
    
    # Информация отправителя
    user_id = event.get_user_id()
    nickname = event.get_user_nickname()
    
    # Содержимое сообщения
    message_segments = event.get_message()
    alt_message = event.get_alt_message()
    text = event.get_text()
    
    # Информация о группе
    group_id = event.get_group_id()
    
    # Информация о боте
    self_id = event.get_self_user_id()
    self_platform = event.get_self_platform()
    
    # Оригинальные данные
    raw_data = event.get_raw()
    raw_type = event.get_raw_type()
    
    # Информация о платформе
    platform = event.get_platform()
    
    # Определение типа сообщения
    is_private = event.is_private_message()
    is_group = event.is_group_message()
    is_at = event.is_at_message()
    
    # Информация о команде
    if event.is_command():
        cmd_name = event.get_command_name()
        cmd_args = event.get_command_args()
        cmd_raw = event.get_command_raw()

Платформенно-специфические методы

Помимо встроенных методов, каждый адаптер платформы регистрирует платформенно-специфические методы, удобные для доступа к платформенно-специфическим данным.

from ErisPulse.Core.Event import message

@message.on_message()
async def handle_message(event):
    platform = event.get_platform()

    # Вызов платформенно-специфических методов в зависимости от платформы
    if platform == "telegram":
        chat_type = event.get_chat_type()      # Telegram специфичный метод
    elif platform == "email":
        subject = event.get_subject()           # Email специфичный метод

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

from ErisPulse.Core.Event import get_platform_event_methods

methods = get_platform_event_methods("telegram")
# ["get_chat_type", "is_bot_message", ...]

Список платформенно-специфических методов см. в соответствующей документации платформы。

Лучшие практики обработки событий

1. Обработка исключений

@command("process")
async def process_handler(event):
    try:
        # бизнес-логика
        result = await do_some_work()
        await event.reply(f"Результат: {result}")
    except ValueError as e:
        # ожидаемая бизнес-ошибка
        await event.reply(f"Ошибка параметра: {e}")
    except Exception as e:
        # неожиданная ошибка
        sdk.logger.error(f"Обработка не удалась: {e}")
        await event.reply("Произошла ошибка, попробуйте позже")

2. Логирование

@message.on_message()
async def message_handler(event):
    user_id = event.get_user_id()
    text = event.get_text()
    
    sdk.logger.info(f"Обработка сообщения: {user_id} - {text}")
    
    # Использование собственного логгера модуля
    from ErisPulse import sdk
    logger = sdk.logger.get_child("MyHandler")
    logger.debug(f"Подробная отладочная информация")

3. Условная обработка

@message.on_message(priority=0)
async def conditional_handler(event):
    """Условная обработка - проверка внутри обработчика"""
    # Обрабатывать только сообщения определенных пользователей
    if event.get_user_id() in ["bot1", "bot2"]:
        return
    
    # Обрабатывать только сообщения с определенным ключевым словом
    if "ключевое слово" not in event.get_text():
        return
    
    await event.reply("Условие выполнено, обрабатываем сообщение")

Далее

常见任务

来源: docs/ru/getting-started/common-tasks.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/getting-started/common-tasks.md)

Примеры типичных задач

Tip

Когда читать этот документ: когда нужно добавить боту какую-то конкретную функцию, обращайтесь к нужному разделу — персистентность, задачи по расписанию, фильтрация сообщений, контроль прав, пакетная отправка; каждый раздел — законченный пример, который можно сразу скопировать. Для системного изучения обработки событий сначала прочитайте Введение в обработку событий; если это не нужно — пропустите.

Содержание

  1. Персистентность данных
  2. Задачи по расписанию
  3. Фильтрация сообщений
  4. Адаптация к разным платформам
  5. Продвинутая отправка сообщений (повторы/таймауты/пакетная отправка)
  6. Контроль прав доступа
  7. Статистика сообщений
  8. Функция поиска
  9. Обработка изображений

Персистентность данных

Простой счётчик

from ErisPulse import sdk
from ErisPulse.Core.Event import command

@command("count", help="Просмотр количества вызовов команды")
async def count_handler(event):
    # Получить счётчик
    count = sdk.storage.get("command_count", 0)
    
    # Увеличить счётчик
    count += 1
    sdk.storage.set("command_count", count)
    
    await event.reply(f"Это {count}-й вызов этой команды")

Хранение данных пользователя

@command("profile", help="Просмотр профиля")
async def profile_handler(event):
    user_id = event.get_user_id()
    
    # Получить данные пользователя
    user_data = sdk.storage.get(f"user:{user_id}", {
        "nickname": "",
        "join_date": None,
        "message_count": 0
    })
    
    profile_text = f"""
Ник: {user_data['nickname']}
Дата присоединения: {user_data['join_date']}
Количество сообщений: {user_data['message_count']}
    """
    
    await event.reply(profile_text.strip())

@command("setnick", help="Установить ник")
async def setnick_handler(event):
    user_id = event.get_user_id()
    args = event.get_command_args()
    
    if not args:
        await event.reply("Пожалуйста, введите ник")
        return
    
    # Обновить данные пользователя
    user_data = sdk.storage.get(f"user:{user_id}", {})
    user_data["nickname"] = " ".join(args)
    sdk.storage.set(f"user:{user_id}", user_data)
    
    await event.reply(f"Ник установлен: {' '.join(args)}")

Задачи по расписанию

Простой таймер

from ErisPulse import sdk
from ErisPulse.Core.Event import command
import asyncio

class TimerModule:
    def __init__(self):
        self.sdk = sdk
        self._tasks = []
    
    async def on_load(self, event):
        """Запуск задач по расписанию при загрузке модуля"""
        self._start_timers()
        
        @command("timer", help="Управление таймером")
        async def timer_handler(event):
            await event.reply("Таймер работает...")
    
    def _start_timers(self):
        """Запуск задач по расписанию"""
        # Выполнять каждые 60 секунд
        task = asyncio.create_task(self._every_minute())
        self._tasks.append(task)
        
        # Выполнять каждый день в полночь
        task = asyncio.create_task(self._daily_task())
        self._tasks.append(task)
    
    async def _every_minute(self):
        """Задача, выполняемая каждую минуту"""
        self.sdk.logger.info("Выполнение минутной задачи")
        # Ваша логика...
    
    async def _daily_task(self):
        """Задача, выполняемая каждый день в полночь (примечание: расчёт по времени UTC; при необходимости локального времени скорректируйте самостоятельно)"""
        import time
        
        while True:
            # Вычислить время до полуночи
            now = time.time()
            midnight = now + (86400 - now % 86400)
            
            await asyncio.sleep(midnight - now)
            
            # Выполнить задачу
            self.sdk.logger.info("Выполнение ежедневной задачи")
            # Ваша логика...

Использование событий жизненного цикла

@sdk.lifecycle.on("core.init.complete")
async def init_complete_handler(event_data):
    """Запуск задач по расписанию после инициализации SDK"""
    import asyncio
    
    async def daily_reminder():
        """Ежедневное напоминание"""
        await asyncio.sleep(86400)  # 24 часа
        sdk.logger.info("Выполнение ежедневной задачи")
    
    # Запустить фоновую задачу
    asyncio.create_task(daily_reminder())

Фильтрация сообщений

Фильтрация по ключевым словам

from ErisPulse.Core.Event import message

blocked_words = ["спам", "реклама", "фишинг"]

@message.on_message()
async def filter_handler(event):
    text = event.get_text()
    
    # Проверить наличие запрещённых слов
    for word in blocked_words:
        if word in text:
            sdk.logger.warning(f"Заблокировано сообщение с запрещённым словом: {word}")
            return  # Не обрабатывать это сообщение
    
    # Обычная обработка сообщения
    await event.reply(f"Получено: {text}")

Фильтрация по чёрному списку

# Загрузить чёрный список из конфигурации или хранилища
blacklist = sdk.storage.get("user_blacklist", [])

@message.on_message()
async def blacklist_handler(event):
    user_id = event.get_user_id()
    
    if user_id in blacklist:
        sdk.logger.info(f"Пользователь из чёрного списка: {user_id}")
        return  # Не обрабатывать
    
    # Обычная обработка
    await event.reply(f"Привет, {user_id}")

Адаптация к разным платформам

Ответы для конкретных платформ

@command("help", help="Показать справку")
async def help_handler(event):
    platform = event.get_platform()
    
    if platform == "yunhu":
        await event.reply("Справка для платформы Yunhu...")
    elif platform == "telegram":
        await event.reply("Telegram platform help...")
    elif platform == "onebot11":
        await event.reply("OneBot11 help...")
    else:
        await event.reply("Общая справка")

Определение возможностей платформы

@command("rich", help="Отправить форматированное сообщение")
async def rich_handler(event):
    platform = event.get_platform()
    
    if platform == "yunhu":
        # Yunhu поддерживает HTML
        yunhu = sdk.adapter.get("yunhu")
        await yunhu.Send.To("user", event.get_user_id()).Html(
            "<b>Жирный текст</b><i>Курсивный текст</i>"
        )
    elif platform == "telegram":
        # Telegram поддерживает Markdown
        telegram = sdk.adapter.get("telegram")
        await telegram.Send.To("user", event.get_user_id()).Markdown(
            "**Жирный текст** *Курсивный текст*"
        )
    else:
        # На других платформах использовать простой текст
        await event.reply("Жирный текст Курсивный текст")

Продвинутая отправка сообщений (повторы/таймауты/пакетная отправка)

Помимо простого event.reply(), с помощью Send DSL адаптера можно реализовать более сложные сценарии отправки: автоматический повтор при неудаче, отмена по таймауту, выполнение логики после успешной отправки, пакетная отправка нескольких сообщений.

В примерах ниже event.get_detail_type() и event.get_target_id() используются для получения типа цели и ID из события (в групповом чате автоматически берётся group_id, в личном — user_id), чтобы избежать жёсткого кодирования.

Выполнение логики после успешной отправки

@command("pay", help="Имитация оплаты")
async def pay_handler(event):
    yunhu = sdk.adapter.get(event.get_platform())
    user_id = event.get_user_id()
    # Списывать баллы только после успешной отправки
    await (yunhu.Send.To(event.get_detail_type(), event.get_target_id())
           .Hook(lambda r: sdk.storage.set(f"points:{user_id}", -10))
           .Text("Оплата прошла успешно, списано 10 баллов"))

Повтор при неудаче + отмена по таймауту

@command("notice", help="Отправить важное уведомление")
async def notice_handler(event):
    adapter_inst = sdk.adapter.get(event.get_platform())
    # Не более 3 повторов, таймаут 10 секунд каждый
    task = (adapter_inst.Send.To(event.get_detail_type(), event.get_target_id())
            .Retry(3)
            .Timeout(10)
            .OnError(lambda ctx: sdk.logger.error(f"Не удалось отправить уведомление: {ctx.error}"))
            .Text("Это важное уведомление"))
    # Не ждать, отправка в фоне

Пакетная отправка нескольких сообщений

Несколько сообщений по одной цепочке, единое выполнение:

@command("announce", help="Отправить объявление")
async def announce_handler(event):
    adapter_inst = sdk.adapter.get(event.get_platform())
    # Собрать несколько сообщений и отправить вместе (по умолчанию параллельно)
    results = await (adapter_inst.Send.To(event.get_detail_type(), event.get_target_id())
                    .Build()
                    .Text("📋 Объявление на сегодня")
                    .Image("https://example.com/banner.jpg")
                    .Text("Подробности смотрите на изображении выше")
                    .Retry(2)            # Каждый неудавшийся элемент повторяется отдельно
                    .send_all())
    sdk.logger.info(f"Пакетная отправка завершена, всего {len(results)} сообщений")

Более полные правила и описание пакетной отправки смотрите в Руководстве по возможностям платформ.

Контроль прав доступа

Проверка администратора

# Список владельцев в конфигурации
MASTERS = ["user123", "user456"]

def is_master(user_id):
    """Проверить, является ли пользователь владельцем фреймворка"""
    return user_id in MASTERS

@command("master", help="Команда владельца фреймворка")
async def master_handler(event):
    user_id = event.get_user_id()
    
    if not is_master(user_id):
        await event.reply("Недостаточно прав, эта команда доступна только владельцу фреймворка")
        return
    
    await event.reply("Команда владельца фреймворка выполнена успешно")

@command("addmaster", help="Добавить владельца фреймворка")
async def addmaster_handler(event):
    if not is_master(event.get_user_id()):
        return
    
    args = event.get("text", "").split()
    if len(args) < 2:
        await event.reply("Использование: /addmaster <ID пользователя>")
        return
    
    new_master = args[0]
    MASTERS.append(new_master)
    await event.reply(f"Владелец фреймворка добавлен: {new_master}")

Права в группах

@command("groupinfo", help="Просмотр информации о группе")
async def groupinfo_handler(event):
    if not event.is_group_message():
        await event.reply("Эта команда доступна только в групповых чатах")
        return
    
    group_id = event.get_group_id()
    user_id = event.get_user_id()
    
    await event.reply(f"ID группы: {group_id}, ваш ID: {user_id}")

Статистика сообщений

Подсчёт сообщений

Примечание: в следующих примерах для простого подсчёта используются sdk.storage.get/set. В сценариях с высокой конкурентностью рекомендуется использовать sdk.storage.transaction() для обеспечения атомарности.

@message.on_message()
async def count_handler(event):
    # Получить статистику
    stats = sdk.storage.get("message_stats", {
        "total": 0,
        "by_user": {},
        "by_day": {}
    })
    
    # Обновить статистику
    stats["total"] += 1
    
    user_id = event.get_user_id()
    stats["by_user"][user_id] = stats["by_user"].get(user_id, 0) + 1
    
    # Сохранить
    sdk.storage.set("message_stats", stats)

@command("stats", help="Просмотр статистики сообщений")
async def stats_handler(event):
    stats = sdk.storage.get("message_stats", {
        "total": 0,
        "by_user": {},
        "by_day": {}
    })
    
    top_users = sorted(
        stats["by_user"].items(),
        key=lambda x: x[1],
        reverse=True
    )[:5]
    
    top_text = "\n".join(
        f"{uid}: {count} сообщений" for uid, count in top_users
    )
    
    await event.reply(f"Всего сообщений: {stats['total']}\n\nАктивные пользователи:\n{top_text}")

Функция поиска

Простой поиск

Примечание: в следующем примере история сообщений хранится в списке в памяти, после перезапуска программы данные будут потеряны. В продакшене рекомендуется использовать sdk.storage или таблицу SQLite для персистентного хранения.

from ErisPulse.Core.Event import command, message

# Хранилище истории сообщений
message_history = []

@message.on_message()
async def store_handler(event):
    """Сохранение сообщений для поиска"""
    user_id = event.get_user_id()
    text = event.get_text()
    
    message_history.append({
        "user_id": user_id,
        "text": text,
        "time": event.get_time()
    })
    
    # Ограничить количество записей истории
    if len(message_history) > 1000:
        message_history.pop(0)

@command("search", help="Поиск сообщений")
async def search_handler(event):
    args = event.get_command_args()
    
    if not args:
        await event.reply("Пожалуйста, введите ключевое слово для поиска")
        return
    
    keyword = " ".join(args)
    results = []
    
    # Поиск по истории
    for msg in message_history:
        if keyword in msg["text"]:
            results.append(msg)
    
    if not results:
        await event.reply("Совпадающих сообщений не найдено")
        return
    
    # Показать результаты
    result_text = f"Найдено {len(results)} совпадающих сообщений:\n\n"
    for i, msg in enumerate(results[:10], 1):  # Показывать максимум 10 сообщений
        result_text += f"{i}. {msg['text']}\n"
    
    await event.reply(result_text)

Обработка изображений

Скачивание и сохранение изображений

from ErisPulse.Core import client

@message.on_message()
async def image_handler(event):
    """Обработка сообщений с изображениями"""
    message_segments = event.get_message()
    
    for segment in message_segments:
        if segment.get("type") == "image":
            file_url = segment.get("data", {}).get("file")
            
            if file_url:
                # Рекомендуется использовать встроенный клиент SDK для скачивания изображений
                resp = await client.get(file_url)
                if resp.status == 200:
                    image_data = await resp.read()
                    
                    # Сохранить в файл
                    filename = f"images/{event.get_time()}.jpg"
                    with open(filename, "wb") as f:
                        f.write(image_data)
                    
                    sdk.logger.info(f"Изображение сохранено: {filename}")
                    await event.reply("Изображение сохранено")

Пример распознавания изображений

Примечание: в следующем примере используется адрес-заглушка API; при реальном использовании замените его на свой сервис распознавания изображений.

from ErisPulse.Core import client

@command("identify", help="Распознать изображение")
async def identify_handler(event):
    """Распознавание изображений в сообщении"""
    message_segments = event.get_message()
    
    for segment in message_segments:
        if segment.get("type") == "image":
            file_url = segment.get("data", {}).get("file")
            
            # Вызов API распознавания изображений
            result = await _identify_image(file_url)
            
            await event.reply(f"Результат распознавания: {result}")
            return
    
    await event.reply("Изображение не найдено")

async def _identify_image(url):
    """Вызов API распознавания изображений (пример) — с использованием встроенного клиента SDK"""
    resp = await client.post(
        "https://api.example.com/identify",
        json={"url": url}
    )
    data = await resp.json()
    return data.get("description", "Не удалось распознать")

Дальнейшие шаги

IDE 补全

来源: docs/ru/getting-started/ide-completion.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/getting-started/ide-completion.md)

Генерация типовых заглушек (дополнение IDE)

Tip


Когда следует читать эту статью: если IDE не предлагает дополнение для методов платформы, специфичных для адаптера (например, Board в Yunhu), или для пользовательских методов модуля, выполните команду epsdk types, чтобы сгенерировать файл типовых заглушек; если не нужно, пропустите.

ErisPulse использует динамическое обнаружение модулей/адаптеров через entry-points, и точки входа не могут быть известны на статическом уровне конкретные типы пользовательских классов.
Команда epsdk types сканирует установленные модули/адаптеры и генерирует файл типовых заглушек, позволяя использовать эти типы в аннотациях переменных и получать дополнение в IDE.

Основные принципы проектирования

Заглушечные файлы экспортируют только типы, не предоставляя каких-либо экземпляров во время выполнения:

Основное использование

Запустите в корневом каталоге проекта:

epsdk types

В текущем каталоге будет создан файл _ep_types.py, содержащий типы всех установленных модулей/адаптеров.

Использование в коде

from _ep_types import MyModule, Yunhu
from ErisPulse import sdk

# Используя импортированные типы для аннотации переменных, IDE будет предлагать методы этого класса
my_mod: MyModule = sdk.module.get("MyModule")
my_mod.hello()                  # ← IDE предлагает hello

my_adapter: Yunhu = sdk.adapter.get("yunhu")
await my_adapter.Send.To("group", "123").Board(...)   # ← Предложения специфичных методов платформы

Работа

  1. Сканирование entry-points erispulse.adapter / erispulse.module
  2. Инспекция в целевой среде Python через дочерний процесс для сбора информации о фактических классах каждого адаптера/модуля (включая путь модуля и полное имя)
  3. Генерация .py файла, в котором:
    • Все from xxx import Yyy as Zzz находятся в TYPE_CHECKING
    • Zzz представляет собой имя entry-point в формате PascalCase
  4. IDE читает часть TYPE_CHECKING для предоставления автодополнения; во время выполнения никакой код не выполняется

Пример сгенерированных заглушек:

# _ep_types.py (автоматически сгенерировано)
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Адаптеры
    from MyAdapter.Core import MyAdapter as MyAdapter
    from YunhuAdapter.Core import YunhuAdapter as Yunhu

    # Модули
    from MyModule.Core import Main as MyModule

    __all__ = ['MyAdapter', 'Yunhu', 'MyModule']

Параметры командной строки

Параметр Описание
-o, --output PATH Указывает путь к выходному файлу (по умолчанию ./_ep_types.py)
--force Перезаписывает существующие файлы-заглушки
--adapters-only Сканирует только адаптеры
--modules-only Сканирует только модули

Когда перегенерировать

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

Базовый класс SendDSL уже содержит стандартные методы отправки (Text/Image/Voice/Video/File), и любой полученный экземпляр SendDSL может дополнить эти методы. Команда types в основном используется для дополнения методов, специфичных для платформы (например, Board для Yunhu, Dice для Sandbox) и методов, специфичных для модуля.

Далее