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

模組開發最佳實踐

本文檔提供了 ErisPulse 模組開發的最佳實踐建議。

模組設計

1. 單一職責原則

每個模組應該只負責一個核心功能:

# 好的設計:每個模組只負責一個功能
class WeatherModule(BaseModule):
    """天氣查詢模組"""
    pass

class NewsModule(BaseModule):
    """新聞查詢模組"""
    pass

# 不好的設計:一個模組負責多個不相關的功能
class UtilityModule(BaseModule):
    """包含天氣、新聞、笑話等多個功能"""
    pass

2. 模組命名規範

[project]
name = "ErisPulse-ModuleName"  # 使用 ErisPulse- 前綴

3. 清晰的配置管理

推薦使用宣告式配置(ConfigClass + BaseConfig),獲得類型安全、自動模板生成、WebUI 表單支援等能力:

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

@dataclass
class MyModuleConfig(BaseConfig):
    api_url: str = field(default="https://api.example.com", metadata={
        "description": {"i18n": "my_module.api_url", "default": "API 位址"},
    })
    timeout: int = field(default=30, metadata={
        "description": {"i18n": "my_module.timeout", "default": "超時時間(秒)"},
    })
    cache_ttl: int = field(default=3600, metadata={
        "description": {"i18n": "my_module.cache_ttl", "default": "快取存活時間(秒)"},
    })

class MyModule(BaseModule):
    ConfigClass = MyModuleConfig

    async def do_something(self):
        cfg = self.cfg  # 類型安全,即時讀取
        await self._fetch(cfg.api_url, timeout=cfg.timeout)

也可以繼續使用手動方式讀取和寫入配置儲存(見模組核心概念)。

宣告式翻譯鍵(v2.7.0+)

模組可以透過 I18nClass 集中宣告翻譯鍵,框架自動註冊到 i18n 系統,無需手動呼叫 i18n.register()。

from ErisPulse.Core.Bases import BaseI18n, I18nKey

class MyModule(BaseModule):
    class I18nClass(BaseI18n):
        # 帶占位符的業務翻譯鍵
        welcome: I18nKey = I18nKey(
            default="Welcome, {name}!",
            zh_CN="歡迎你,{name}!",
            zh_TW="歡迎你,{name}!",
            en="Welcome, {name}!",
            ja="ようこそ、{name}!",
            ru="Добро пожаловать, {name}!",
        )
        # 配置欄位描述的翻譯
        api_url: I18nKey = I18nKey(
            default="API URL",
            zh_CN="API 位址",
            zh_TW="API 位址",
            en="API URL",
            ja="API URL",
            ru="API URL",
        )

詳細用法見 i18n 文檔。

異步編程

1. 使用異步庫

# 推薦使用 SDK 內建 HTTP 客戶端(異步,自動日誌和統計)
from ErisPulse.Core import client

class MyModule(BaseModule):
    async def fetch_data(self, url):
        resp = await client.get(url)
        return await resp.json()

# 也可透過 sdk.client 使用(效果相同)
from ErisPulse import sdk

class MyModule(BaseModule):
    async def fetch_data(self, url):
        resp = await sdk.client.get(url)
        return await resp.json()

# 不要使用 aiohttp 直接導入(不便於框架統一管理)
import aiohttp

class MyModule(BaseModule):
    async def fetch_data(self, url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                return await response.json()

# 不要使用 requests(同步,會阻塞事件循環)
import requests

class MyModule(BaseModule):
    def fetch_data(self, url):
        return requests.get(url).json()  # 會阻塞事件循環

2. 正確的異步操作

from ErisPulse.Core.Event import Event  # event: Event 注解可獲得 IDE 补全

async def handle_command(self, event: Event):
    # 需要等待結果的耗時操作:直接 await(生命周期明確)
    result = await self._long_operation()

async def on_load(self, event: dict):
    # 後台任務(輪詢/定時/fire-and-forget):用 self.spawn(),
    # 模組卸載時框架在 on_unload 之後兜底取消,避免持有 self 導致泄漏
    self.spawn(self._poll())

Note

後台任務推薦 self.spawn()(ErisPulse 2.8.0+)。2.8.3 起裸 asyncio.create_task 也會自動隱式歸屬模組(Task Factory 自動登記,卸載時兜底取消,不再泄漏 self 引用); self.spawn() 仍是推薦寫法——支援非主循環線程調度回主循環、顯式 owner= 指定。 2.8.3 之前的版本裸任務不歸屬、會持有 self 引用導致模組實例無法被回收 (熱重載泄漏),必須用 self.spawn()。詳見 生命週期管理。

3. 資源管理

async def on_load(self, event):
    # SDK 客戶端已自動管理連接池,無需手動建立 session
    pass
    
async def on_unload(self, event):
    # 如需自訂客戶端,記得清理資源
    pass

事件處理

1. 使用 Event 包裝類

# 使用 Event 包裝類的便捷方法
@command("info")
async def info_command(event: Event):
    user_id = event.get_user_id()
    nickname = event.get_user_nickname()
    await event.reply(f"你好,{nickname}!")

# 而非直接存取字典
@command("info")
async def info_command(event: Event):
    user_id = event["user_id"]  # 不夠清晰,容易出錯

2. 合理使用懶加載

# 低頻命令模組:宣告 activate_on 觸發器,首個匹配命令到達時自動激活(保持懶加載)
class CommandModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(lazy_load=True, activate_on=[
            {"command": {"name": "dice", "help": "擲一個骰子", "aliases": ["d"]}},
        ])

# 低頻監聽器模組:宣告事件觸發器,事件到達時自動激活
class ListenerModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(lazy_load=True, activate_on=[
            {"notice": "group_member_increase"},
        ])

# 高頻觸發(每條訊息都要處理)或啟動時就必須就緒的模組:立即加載
class HotListenerModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(lazy_load=False)

# 工具模組適合懶加載
class UtilityModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(lazy_load=True)

activate_on 的完整語法(事件三形式 / 命令簡寫與 dict 聲明 / help 回退鏈)見 懶加載模組系統。

3. 事件處理器註冊

async def on_load(self, event):
    # 在 on_load 中註冊事件處理器
    @command("hello")
    async def hello_handler(event: Event):
        await event.reply("你好!")
    
    @message.on_group_message()
    async def group_handler(event: Event):
        self.logger.info("收到群訊息")
    
    # 不需要手動註銷,框架會自動處理

工具模組:托管别人东西时要接住"卸载通知"

什么时候需要:你的模組替其他模組保管东西(定時回調、訂閱者、連接、快取條目……)。這些引用在對方模組卸載後如果一直不丟棄,對方實例就永遠無法被回收——這是工具模組最常見的記憶體泄漏來源。

from ErisPulse.Core.Bases import BaseModule
from ErisPulse.runtime import off_cleanup, on_cleanup

class MyToolModule(BaseModule):
    def __init__(self):
        self._entries = {}  # {模組名: 托管的東西}

    def register(self, entry):
        owner = on_cleanup(self._drop)   # ① 登記時掛入清理鏈,自動識別呼叫方
        self._entries.setdefault(owner, []).append(entry)

    def _drop(self, owner: str):
        self._entries.pop(owner, None)   # ② 對方卸載時框架自動呼叫:丟棄它的东西

    async def on_unload(self, event):
        off_cleanup(self._drop)          # ③ 自己卸載前註銷鉤子

就這麼多,框架保證:

不接入的後果:對方 purge 彻底卸載時實例無法回收(泄漏診斷報"不可回收");若對方自己也不在 on_unload 裡向你註銷,泄漏就是永久性的。

普通模組(不托管别人东西)不需要關心這個——框架資源(命令 / 處理器 / 路由 / 後台任務……)的卸載清理是全自動的。

觸發時機、呼叫方識別規則、超時與容錯等細節見 歸屬權系統 · 工具模組指南。

錯誤處理

1. 分類異常處理

from ErisPulse.Core.Bases.errors import ClientError

async def handle_event(self, event: Event):
    try:
        result = await self._process(event)
    except ValueError as e:
        # 預期的業務錯誤
        self.logger.warning(f"業務警告: {e}")
        await event.reply(f"參數錯誤: {e}")
    except ClientError as e:
        # 網路錯誤(sdk.client 的底層 aiohttp 異常已自動轉換)
        self.logger.error(f"網路錯誤 {e.method} {e.url}: {e}")
        await event.reply("網路請求失敗,請稍後重試")
    except Exception as e:
        # 未預期的錯誤
        self.logger.error(f"未知錯誤: {e}", exc_info=True)
        await event.reply("處理失敗,請聯絡管理員")
        raise

2. 超時處理

# 推薦使用 SDK 內建客戶端(自帶超時和重試)
from ErisPulse.Core import client
from ErisPulse.Core.Bases.errors import ClientTimeoutError

async def fetch_with_timeout(self, url, timeout=30):
    try:
        resp = await client.get(url, timeout=timeout)
        return await resp.json()
    except ClientTimeoutError:
        self.logger.warning(f"請求超時: {url}")
        raise

存儲系統

1. 使用事務

# 使用事務確保資料一致性
async def update_user(self, user_id, data):
    with self.sdk.storage.transaction():
        self.sdk.storage.set(f"user:{user_id}:profile", data["profile"])
        self.sdk.storage.set(f"user:{user_id}:settings", data["settings"])

# ❌ 不使用事務可能導致資料不一致
async def update_user(self, user_id, data):
    self.sdk.storage.set(f"user:{user_id}:profile", data["profile"])
    # 如果這裡出錯,上面的設定無法回滾
    self.sdk.storage.set(f"user:{user_id}:settings", data["settings"])

2. 批量操作

# 使用批量操作提高效能
def cache_multiple_items(self, items):
    self.sdk.storage.set_multi({
        f"item:{k}": v for k, v in items.items()
    })

# ❌ 多次呼叫效率低
def cache_multiple_items(self, items):
    for k, v in items.items():
        self.sdk.storage.set(f"item:{k}", v)

日誌記錄

1. 合理使用日誌等級

# DEBUG: 詳細的除錯資訊(僅開發時)
self.logger.debug(f"輸入參數: {params}")

# INFO: 正常運行資訊
self.logger.info("模組已加載")
self.logger.info(f"處理請求: {request_id}")

# WARNING: 警告資訊,不影響主要功能
self.logger.warning(f"配置項 {key} 未設定,使用預設值")
self.logger.warning("API 回應慢,可能需要優化")

# ERROR: 錯誤資訊
self.logger.error(f"API 請求失敗: {e}")
self.logger.error(f"處理事件失敗: {e}", exc_info=True)

# CRITICAL: 致命錯誤,需要立即處理
self.logger.critical("資料庫連線失敗,機器人無法正常運行")

2. 構造化日誌

# 使用構造化日誌,便於解析
self.logger.info(f"處理請求: request_id={request_id}, user_id={user_id}, duration={duration}ms")

# ❌ 使用非構造化日誌
self.logger.info(f"處理請求了,來自使用者 {user_id},用時 {duration} 毫秒")

性能優化

1. 使用快取

class MyModule(BaseModule):
    def __init__(self):
        self._cache = {}
        self._cache_lock = asyncio.Lock()
    
    async def get_data(self, key):
        async with self._cache_lock:
            if key in self._cache:
                return self._cache[key]
            
            # 從資料庫獲取
            data = await self._fetch_from_db(key)
            
            # 快取資料
            self._cache[key] = data
            return data

2. 避免阻塞操作

# 使用異步操作
async def process_message(self, event: Event):
    # 異步處理
    await self._async_process(event)

# ❌ 阻塞操作
async def process_message(self, event: Event):
    # 同步操作,阻塞事件迴圈
    result = self._sync_process(event)

安全性

1. 敏感資料保護

# 敏感資料儲存在配置中(宣告式 ConfigClass,secret 欄位不進入日誌/導出)
from dataclasses import dataclass, field
from ErisPulse.Core.Bases import BaseModule, BaseConfig

@dataclass
class MyModuleConfig(BaseConfig):
    api_key: str = field(
        default="",
        metadata={"description": "API 密鑰", "secret": True},
    )

class MyModule(BaseModule):
    ConfigClass = MyModuleConfig

    def check_api_key(self):
        if not self.cfg.api_key or self.cfg.api_key == "YOUR_API_KEY_HERE":
            raise ValueError("請在 config.toml 中設定有效的 API 密鑰")

# ❌ 敏感資料硬編碼
class MyModule(BaseModule):
    API_KEY = "sk-1234567890"  # 不要這樣做!

2. 輸入驗證

# 驗證使用者輸入
async def process_command(self, event: Event):
    user_input = event.get_text()
    
    # 驗證輸入長度
    if len(user_input) > 1000:
        await event.reply("輸入過長,請重新輸入")
        return
    
    # 驗證輸入格式
    if not re.match(r'^[a-zA-Z0-9]+$', user_input):
        await event.reply("輸入格式不正確")
        return

測試

1. 單元測試

import pytest
from ErisPulse.Core.Bases import BaseModule

class TestMyModule:
    def test_config_defaults(self):
        """測試配置預設值"""
        config = MyModule.ConfigClass()
        assert config.timeout == 30

2. 集成測試

@pytest.mark.asyncio
async def test_command_handling():
    """測試命令處理"""
    module = MyModule()
    await module.on_load({})
    
    # 模擬命令事件
    event = create_test_command_event("hello")
    await module.handle_command(event)

部署

1. 版本管理

[project]
name = "ErisPulse-MyModule"
version = "1.0.0"

遵循語意化版本:

2. README 頭部

epsdk create 生成的 README 已內建 ErisPulse 頭部標識(Logo + 徽章行)。兩種推薦模式:

模式 A — 僅 ErisPulse Logo(預設):

<div align="center">

<img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" width="180" alt="MyModule" />

# MyModule

**一句話描述**

<p>
  <a href="https://pypi.org/project/ErisPulse-MyModule/"><img src="https://img.shields.io/pypi/v/ErisPulse-MyModule?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
  <a href="https://pypi.org/project/ErisPulse-MyModule/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
  <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
  <a href="https://github.com/ErisPulse/ErisPulse"><img src="https://img.shields.io/badge/Powered_by-ErisPulse-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white" alt="ErisPulse"></a>
</p>

</div>

模式 B — 模組圖標 × ErisPulse Logo(有自訂圖標時):

<div align="center">

<img src=".github/assets/MyModuleIcon.svg" width="120" alt="MyModule" />
<span style="font-size:44px;color:#c8c8c8;margin:0 18px;vertical-align:middle;">×</span>
<img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" height="120" alt="ErisPulse" />

# MyModule
(徽章行同上)
</div>

可按需追加 GitHub Stars、Downloads 等徽章。Logo 也可下載到專案本地(.github/assets/ErisPulseLogo.png)改為相對路徑引用。

相關文件