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

Основные концепции модуля

Понимание основных концепций модуля ErisPulse — это основа для разработки высококачественных модулей.

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

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

from ErisPulse.Core.Bases import BaseModule
from ErisPulse.loaders import ModuleLoadStrategy

class MyModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        """Возвращает стратегию загрузки модуля"""
        return ModuleLoadStrategy(
            lazy_load=True,   # Ленивая загрузка или немедленная загрузка
            priority=0,       # Приоритет загрузки (чем больше, тем раньше загрузка)
            depends=["OtherModule"]  # Опционально: объявляет зависимости от других модулей
        )

Модули, перечисленные в depends, если не зарегистрированы, приведут к пропуску текущего модуля и записи предупреждения. Порядок загрузки определяется топологической сортировкой, а модули с одинаковым приоритетом сортируются по убывающему приоритету.

Note

Каскадное выгрузка / каскадное перезагрузка (ErisPulse 2.8.0+): При выгрузке модуля, от которого зависят другие модули, зависимые модули будут сначала выгружены каскадно (в логе указан цепочка каскадного выгрузки); при горячей перезагрузке любого модуля (локальный плагин / пакет PyPI), зависимые модули также каскадно перезагружаются, чтобы избежать использования устаревших экземпляров. Объявление циклических зависимостей приведет к отказу в загрузке с RuntimeError.

Метод on_load

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

async def on_load(self, event):
    # Регистрация обработчиков событий
    @command("hello", help="Команда приветствия")
    async def hello_handler(event):
        await event.reply("Привет!")

    # Использование встроенного HTTP-клиента SDK (автоматически управляет пулом соединений, не нужно создавать session вручную)
    # Через sdk.client можно отправлять запросы

Метод on_unload

Вызывается при выгрузке модуля, используется для очистки ресурсов:

async def on_unload(self, event):
    # Очистка пользовательских ресурсов
    # sdk.client управляется фреймворком, не нужно закрывать вручную

    # Отмена обработчиков событий (фреймворк обрабатывает автоматически)
    self.logger.info("Модуль выгружен")

Создание и очистка фоновых задач (через self.spawn() / автоматическая отмена фреймворком) подробно описано в управлении жизненным циклом.

Выгрузка и полная выгрузка (purge)

Note

Эта функция доступна в ErisPulse 2.8.0+.

Метод unload() по умолчанию отменяет загрузку (выгружает экземпляр и ресурсы), но сохраняет метаданные (класс модуля и метаинформацию) — модуль по-прежнему может быть обнаружен и перезагружен с помощью load(), без необходимости повторной register().

Если требуется полная выгрузка (освобождение ссылки на класс модуля, очистка sys.modules, чтобы плагин и его уникальные зависимости могли быть собраны сборщиком мусора), передайте purge=True:

# Только отмена загрузки: сохраняются метаданные, можно перезагрузить в любое время
await sdk.module.unload("MyModule")

# Полная выгрузка: удаляются метаданные + очистка sys.modules (только для плагинов из папки)
await sdk.module.unload("MyModule", purge=True)
Смысл unload() по умолчанию unload(purge=True)
Выгрузка экземпляра и ресурсов (события/task/маршруты/lifecycle/i18n) ✅ ✅
Сохранение метаданных (класс модуля и метаинформация) ✅ ❌ Удалено
Очистка sys.modules (только для плагинов из папки) ❌ ✅
Класс модуля может быть собран сборщиком мусора ❌ ✅
Перезагрузка load() можно использовать напрямую Нужно сначала register() + load()

При purge=True зависимые модули также будут полностью выгружены. После выгрузки фреймворк выполнит gc.collect() и проверит, доступен ли класс модуля для сборки мусора, остаточные ссылки будут предупреждены в логах (с указанием источника, уровень DEBUG).

Жизненный цикл в целом

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

flowchart TD
    subgraph Load["Загрузка (register → load)"]
        L1["register: Регистрация класса модуля и метаинформации"] --> L2["Проверка зависимостей<br/>Если отсутствуют, пропустить"]
        L2 --> L3["Топологическая сортировка (Kahn + priority)"]
        L3 --> L4["Внедрение owner current_owner"]
        L4 --> L5["Генерация шаблона конфигурации + регистрация ключей перевода i18n"]
        L5 --> L6["Инстанцирование модуля (внедрение sdk)"]
        L6 --> L7["Вызов on_load()"]
        L7 --> L8["Присоединение к свойству sdk + emit module.load"]
    end

    subgraph Unload["Выгрузка (unload)"]
        U1["Вызов on_unload()"] --> U2["Каскадная отмена фоновых задач (self.spawn принадлежность)"]
        U2 --> U3["Очистка ключей перевода i18n"]
        U3 --> U4["Удаление маршрутов / команд / обработчиков событий (по owner)"]
        U4 --> U5["Очистка хуков lifecycle (по owner)"]
        U5 --> U6["Удаление свойства SDK + ленивый загрузчик"]
        U6 --> U7["emit module.unload"]
    end

    Load --> Unload

Что фреймворк делает за вас при загрузке (вам нужно только написать on_load, остальное делается автоматически):

Этап Что делает фреймворк
Внедрение owner Во время инстанцирования модуль оборачивается в owner_scope — все зарегистрированные вами команды/события/钩ки/фоновые задачи в on_load автоматически принадлежат этому модулю, при выгрузке они удаляются по owner
Шаблон конфигурации Модули, объявившие ConfigClass, получают автоматически сгенерированный/заполненный конфигурационный сегмент ErisPulse.<ModuleName>
Ключи перевода i18n Модули, объявившие I18nClass, автоматически регистрируют ключи перевода (удаляются при выгрузке)
Зависимости Модули сортируются по depends, гарантируя, что зависимые модули загружаются первыми; циклические зависимости отклоняются с RuntimeError
Присоединение к SDK После инстанцирования модуль присоединяется к sdk.<ModuleName>, благодаря чему вы можете обращаться к sdk.MyModule.xxx

Что фреймворк очищает при выгрузке (соответствует U1→U7 выше): после выполнения on_unload происходит дополнительная очистка — фоновые задачи, созданные через self.spawn, принудительно отменяются (для аккуратного завершения задачи в on_unload нужно сделать это вручную), ключи перевода i18n, маршруты, команды/обработчики событий, хуки lifecycle, затем удаляется свойство SDK. При purge=True дополнительно удаляются метаданные и очищается sys.modules.

Эта автоматическая очистка — основа того, что «вам нужно только написать on_load/on_unload, не нужно вручную unregister» — фреймворк использует принадлежность owner, чтобы сделать «кто зарегистрировал, тот и очищает» одним нажатием.

Объект SDK

Доступ к основным модулям

from ErisPulse import sdk

# Доступ к всем основным модулям через объект sdk
sdk.logger.info("Лог")
sdk.storage.set("key", "value")
config = sdk.config.getConfig("MyModule")

Коммуникация между модулями

# Доступ к другим модулям
other_module = sdk.OtherModule
result = await other_module.some_method()

Запрос методов отправки адаптера

Из-за нового стандарта, требующего перезаписи метода __getattr__ для реализации механизма отправки по умолчанию, невозможно использовать hasattr для проверки существования метода. Начиная с версии 2.3.5, добавлена функция для запроса методов отправки.

Список поддерживаемых методов отправки

# Получение списка всех методов отправки, поддерживаемых платформой
methods = sdk.adapter.list_sends("onebot11")
# Возвращает: ["Text", "Image", "Voice", "Markdown", ...]

Получение подробной информации о методе

# Получение подробной информации о методе
info = sdk.adapter.send_info("onebot11", "Text")
# Возвращает:
# {
#     "name": "Text",
#     "parameters": [
#         {"name": "text", "type": "str", "default": null, "annotation": "str"}
#     ],
#     "return_type": "Awaitable[Any]",
#     "docstring": "Отправка текстового сообщения..."
# }

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

Декларативная конфигурация (рекомендуется)

Начиная с версии v2.5.2, модули могут объявлять класс конфигурации через ConfigClass, используя ту же систему схемы конфигурации, что и адаптеры. Конфигурация доступна через self.cfg в режиме реального времени, изменения немедленно применяются:

from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BaseModule
from ErisPulse.Core.Bases import BaseConfig

@dataclass
class MyModuleConfig(BaseConfig):
    api_key: str = field(
        default="",
        metadata={
            "description": {"i18n": "my_module.api_key", "default": "Ключ API"},
            "required": True,
            "secret": True,
            "ui": {"widget": "password", "group": "basic", "order": 1},
        },
    )
    timeout: int = field(
        default=30,
        metadata={
            "description": {"i18n": "my_module.timeout", "default": "Время ожидания (секунды)"},
            "ui": {"widget": "number", "group": "advanced", "order": 2},
        },
    )

class MyModule(BaseModule):
    ConfigClass = MyModuleConfig

    def __init__(self, sdk):
        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):
        pass

    async def do_something(self):
        cfg = self.cfg  # Чтение в режиме реального времени, типобезопасно
        api_key = cfg.api_key
        timeout = cfg.timeout

BaseConfig — универсальный базовый класс конфигурации, применимый ко всем сценариям: адаптеры, модули, внешние проекты. Поля конфигурации поддерживают многоязычные описания i18n (см. документацию i18n).

Система схемы конфигурации также поддерживает (с v2.8.0, см. основные концепты адаптера):

Декларативные ключи перевода (v2.7.0+)

Начиная с v2.7.0, модули могут объявлять ключи перевода, как и ConfigClass, через вложенный класс I18nClass, объединяя все ключи перевода в одном месте. Фреймворк при загрузке автоматически регистрирует все объявленные ключи перевода, без необходимости вручную вызывать i18n.register(), и регистрация происходит раньше, чем генерация шаблона конфигурации, гарантируя, что ключи перевода, используемые в описаниях конфигурации, уже доступны.

from ErisPulse.Core.Bases import BaseConfig, BaseI18n, I18nKey

class MyModule(BaseModule):
    # Класс конфигурации (необязательно)
    @dataclass
    class ConfigClass(BaseConfig):
        welcome_msg: str = field(
            default="Добро пожаловать",
            metadata={
                "description": {"i18n": "mymodule.welcome_msg", "default": "Сообщение приветствия"},
            },
        )

    # Класс ключей перевода (необязательно)
    class I18nClass(BaseI18n):
        # Имя свойства автоматически объединяется в полный путь ключа: <имя модуля>.<имя свойства>
        welcome_msg: I18nKey = I18nKey(
            default="Welcome Message",   # Безязыковой резервный вариант
            zh_CN="欢迎消息",
            zh_TW="歡迎訊息",
            en="Welcome Message",
            ja="ウェルカムメッセージ",
            ru="Приветственное сообщение",
        )
        hello: I18nKey = I18nKey(
            default="Hello, {name}!",
            zh_CN="你好,{name}!",
            zh_TW="你好,{name}!",
            en="Hello, {name}!",
            ja="こんにちは、{name}!",
            ru="Привет, {name}!",
        )

Подробнее смотри рекомендуемый способ i18n.

Ручное чтение конфигурации (устарело)

Устарело: используйте декларативную конфигурацию + self.cfg для чтения в режиме реального времени.

class MyModule(BaseModule):
    def __init__(self, sdk):
        self.sdk = sdk

    def _load_config(self):
        config = self.sdk.config.getConfig("MyModule")
        if not config:
            self.sdk.config.setConfig("MyModule", {"api_key": "", "timeout": 30})
            return {"api_key": "", "timeout": 30}
        return config

Система хранения

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

# Сохранение данных
sdk.storage.set("user:123", {"name": "张三"})

# Получение данных
user = sdk.storage.get("user:123", {})

# Удаление данных
sdk.storage.delete("user:123")

Использование транзакций

# Использование транзакции для обеспечения согласованности данных
with sdk.storage.transaction():
    sdk.storage.set("key1", "value1")
    sdk.storage.set("key2", "value2")
    # Если какая-либо операция не удалась, все изменения будут откатываться

Обработка событий

Регистрация обработчиков событий

from ErisPulse.Core.Event import command, message

# Регистрация команды
@command("info", help="Получить информацию")
async def info_handler(event):
    await event.reply("Это информация")

# Регистрация обработчика сообщений
@message.on_group_message()
async def group_handler(event):
    sdk.logger.info(f"Получено групповое сообщение: {event.get_text()}")

Жизненный цикл обработчиков событий

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

Механизм ленивой загрузки

Принцип работы

# Модуль будет инициализирован только при первом обращении
result = await sdk.my_module.some_method()
# ↑ Здесь будет вызвана инициализация модуля

Немедленная загрузка

Для модулей, требующих немедленной инициализации (например, триггеров, таймеров):

@staticmethod
def get_load_strategy():
    return ModuleLoadStrategy(
        lazy_load=False,  # Немедленная загрузка
        priority=100
    )

Обработка ошибок

Перехват исключений

async def handle_event(self, event):
    try:
        # Бизнес-логика
        await self.process_event(event)
    except ValueError as e:
        self.logger.warning(f"Ошибка параметра: {e}")
        await event.reply(f"Ошибка параметра: {e}")
    except Exception as e:
        self.logger.error(f"Обработка не удалась: {e}")
        raise

Запись в лог

# Использование разных уровней логирования
self.logger.debug("Отладочная информация")    # Подробная информация для отладки
self.logger.info("Состояние работы")      # Информация о нормальной работе
self.logger.warning("Предупреждение")  # Предупреждающая информация
self.logger.error("Ошибка")    # Ошибка
self.logger.critical("Критическая ошибка") # Критическая ошибка

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