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

模块间通信

Note

本章内容需要 ErisPulse **2.8.0+**。

ErisPulse 的模块之间有三层通信模型,按"点对点 → 定向 → 广播"排列:

层 API 语义 典型场景
RPC await sdk.module.call("Chat", "get_history", ...) 点对点请求-响应,带契约 / 审计 / 超时 调用另一模块的能力(查历史、翻译、退款)
定向事件 await lifecycle.emit("message_received", {...}, to="Chat") 仅分发给指定模块注册的生命周期钩子 上游状态变化通知下游("收到新消息了")
广播 await lifecycle.emit("config.updated", {...}) 全框架可见的生命周期事件 配置热更新、模块上下线

{!--< tips >!--} 选型口诀:**要返回值用 call,只通知一个模块的钩子用 emit(..., to=...),通知所有人用 emit(...)**。 {!--< /tips >!--}

RPC:module.call

result = await sdk.module.call("Chat", "get_history", session_id, n=20)

与裸属性访问 sdk.module.Chat.get_history(...)(保留不变)的差异:

module.call() 裸属性访问
目标未注册 / 未启用 抛 ModuleNotAvailableError 抛 AttributeError
懒加载模块 自动唤醒(事件驱动模块走激活锁) 异步初始化模块抛 RuntimeError
current_owner 归因到目标模块(其内部 wait_reply / 发送 / 日志正确归属) 保持调用方
超时 默认 30 秒(timeout= 覆盖,None 不限时) 无
scope 审计 调用方过出站闸口 actions.<调用方>.call 无
契约校验 meta.services 白名单 无

异常体系

ModuleError                      # 模块系统异常基类
└── ModuleCallError              # 跨模块调用基类(含 module / method 属性)
    ├── ModuleNotAvailableError  # 目标未注册 / 未启用 / 唤醒失败
    ├── ServiceNotProvidedError  # 方法不在 services 白名单 / 私有方法 / 不存在
    └── ModuleCallTimeoutError   # 协程方法超时

均挂在 ErisPulseError 体系下,可 from ErisPulse.Core import ModuleCallError 捕获。

服务契约:meta.services

服务方在 get_meta() 声明对外提供的白名单(与 commands 字段对称):

from ErisPulse.Core.Bases import BaseModule, ModuleMeta

class ChatModule(BaseModule):
    @staticmethod
    def get_meta() -> ModuleMeta:
        return ModuleMeta(
            name="聊天",
            services=[
                "get_history",                                       # 简单形态
                {"name": "translate", "description": "把文本翻译成指定语言"},  # 带介绍
            ],
        )

    async def get_history(self, session_id, n=20): ...
    async def translate(self, text, target_lang): ...
    def _internal_helper(self): ...   # 下划线方法始终禁止被外部调用

开发者无感是默认:

服务介绍:给每个服务配上人类 / AI 可读的描述——不需要就什么都不写, 介绍自动取方法 docstring 首行(框架本就要求 docstring 风格):

async def translate(self, text, target_lang):
    """把文本翻译成指定语言"""    # ← 这一行自动成为服务介绍
    ...

需要精细控制(覆盖 docstring / 多语言)时用 dict 形态声明 description(支持 i18n 字典):

services=[
    {"name": "translate", "description": "把文本翻译成指定语言"},
    {"name": "summarize", "description": {"i18n": "Chat.meta.svc.summarize", "default": "摘要对话"}},
]

服务目录:services()

sdk.module.services()
# {'Chat': [{'name': 'get_history', 'signature': '(session_id, n=20)',
#            'description': '把文本翻译成指定语言'}]}

sdk.module.services("Chat")   # 仅查询指定模块

{!--< tips >!--} MCP 化路线:服务目录(名称 + 签名 + 描述)即 MCP tool 的形状—— 每个服务天然长成 {"name", "description", "parameters"}。 未来框架可把 services() 直接暴露为 MCP server 端点,让 AI 发现并调用模块能力; scope.actions.call 审计天然成为 AI 调用的安全闸口。 {!--< /tips >!--}

出站审计:谁能调用谁

每次 module.call() 都以调用方模块的身份过 scope 出站闸口:

[ErisPulse.scope.actions.CallerModule.call]
deny = ["Chat.get_history"]        # 禁止 CallerModule 调 Chat 的 get_history
# allow = ["Chat.get_*"]           # 或白名单:只允许调 Chat 的 get 开头服务

配置方式详见 作用域(scope)的出站维度。

定向事件:lifecycle.emit 的 to 参数

生命周期事件支持定向传播:to 指定目标拥有者(owner)后,事件只分发给以该 owner 身份注册的钩子(模块在 on_load 内注册的钩子自动归属本模块), 其它模块与通配符 * 处理器不感知。

from ErisPulse.Core.lifecycle import lifecycle

# 投递方:事件只投给 Chat 模块注册的钩子
await lifecycle.emit("message_received", {"text": "hi", "from": "u1"}, to="Chat")

# 订阅方(Chat 模块内):注册同名钩子,owner 在注册时自动记录
@lifecycle.on("message_received")
async def on_message_received(data): ...

@lifecycle.on("message")          # 点式父级前缀同样生效(按 owner 过滤)
async def on_any(data): ...

语义细节:

Note

定向事件是轻量通知,不做目标校验与懒唤醒;需要目标存在性校验、 契约审计或返回值时,改用 RPC:module.call。

懒加载与调用

module.call() 对懒加载模块是透明唤醒:

即:调用方不需要关心目标模块是否已加载,也无需为唤醒它而等待某条事件。

定向事件(lifecycle.emit(..., to=...))不做懒唤醒——目标未加载即无钩子, 事件静默丢弃;需要确保送达时改用 module.call()。

冷启动回放

新装 / 重启的模块错过了一段聊天——get_load_strategy(replay=...) 让框架在模块 就绪后,把会话收件箱里最近的消息回放给该模块自己:

from ErisPulse.loaders import ModuleLoadStrategy

class MyAIModule(BaseModule):
    @staticmethod
    def get_load_strategy():
        return ModuleLoadStrategy(
            lazy_load=False,
            priority=100,
            replay="5m",        # 回放最近 5 分钟("1h" / "300" 秒写法均可)
        )

    async def on_load(self, event):
        @message.on_message()
        async def handle(e):
            if e.get("replayed"):
                # 合成事件:仅补上下文,不要触发发送等副作用
                ...

语义细节:

事件幂等去重

平台 websocket 重连后经常重推同一事件(相同 event["id"])——分发入口按 id 做 LRU 去重(容量 4096),同 id 事件只分发一次。

[ErisPulse.framework]
event_dedupe = true   # 默认开启;测试环境固定 id 合成事件可关闭

适配器注册(新连接生命周期起点)时自动重置去重缓存。

相关文档