Основные концепции модуля
Понимание основных концепций модуля 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, см. основные концепты адаптера):
- Автоматическая генерация описания полей из docstring: если не объявлено
metadata description, автоматически извлекается из docstring:ivar поле: описаниеили из разделаAttributes: - Вложенные dataclass конфигурации: при типе поля вложенный dataclass, схема/шаблон/валидация обрабатываются рекурсивно, WebUI отображает вложенные группы
- Поле
example, не сохраняющееся в файл: поле сmetadata={"example": True}не записывается вconfig.toml, сохраняется только вconfig.full.example(подходит для сложных и редко используемых расширенных настроек), после ручной настройки пользователем сохраняется как обычно
Декларативные ключи перевода (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("Критическая ошибка") # Критическая ошибка
Связанные документы
- Введение в разработку модулей - Создание первого модуля
- Обертка для событий - Подробное описание обработки событий
- Лучшие практики - Разработка высококачественных модулей