跳转到内容

03 - 插件事件系统:链式责任模式的实践

学习目标

理解 CowAgent 的插件事件系统如何实现装饰器注册、事件链触发、优先级排序与动态 Git 安装。

前置知识

插件事件系统的通用原理见 插件系统设计。本章聚焦 CowAgent 的具体实现。

项目实践

装饰器注册

CowAgent 的插件通过 @register 装饰器声明:

@register(name="godcmd", desire_priority=100, desc="管理员命令", author="cow")
class GODCMD(Plugin):
def __init__(self):
super().__init__()
self.handlers = {
Event.ON_RECEIVE_MESSAGE: self.on_receive_message,
Event.ON_HANDLE_CONTEXT: self.on_handle_context,
}

注册过程:

  1. 设置插件元数据(name, priority, desc, author, version, enabled)
  2. 按 priority 存入 SortedDict(堆结构维护优先级排序)
  3. 写入 plugins.json 持久化配置

事件链触发

消息处理的四个关键事件按顺序触发,形成链式责任:

EventContext 的关键状态:

  • is_pass():返回 True 表示继续链,False 表示短路
  • action == EventAction.CONTINUE:检查是否继续传递
  • breaked_by:记录哪个插件短路了链

优先级排序

self.plugins = SortedDict(lambda k, v: v.priority, reverse=True)

使用 SortedDict(堆实现)按 priority 降序排列。同一事件的监听插件列表也按 priority 排序:

def refresh_order(self):
for event in self.listening_plugins.keys():
self.listening_plugins[event].sort(
key=lambda name: self.plugins[name].priority, reverse=True
)

动态插件安装

CowAgent 支持通过 Git clone 动态安装插件:

def install_plugin(self, repo: str):
# 1. 验证仓库地址(支持别名映射)
match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
# 2. 使用 dulwich clone 到 plugins/ 目录
dirname = os.path.join("./plugins", match.group(4))
porcelain.clone(repo, dirname, checkout=True)
# 3. 安装 requirements.txt 依赖
pkgmgr.install_requirements(os.path.join(dirname, "requirements.txt"))

更新和卸载同理:update_plugin() 使用 porcelain.pull()uninstall_plugin() 使用 shutil.rmtree()

问题与规避

问题CowAgent 的规避策略
插件异常导致事件链中断emit_event() 中捕获异常,不影响后续插件
高优先级插件短路导致低优先级不执行设计意图——允许权限控制插件(如 banwords)优先拦截
插件 reload 时状态残留reload_plugin() 先清除实例再重新激活
克隆仓库地址不合法正则校验 + source.json 别名映射
插件依赖冲突每个插件独立 requirements.txt,全局安装

设计取舍

为什么使用链式责任而非发布-订阅?

链式责任(Chain of Responsibility)允许插件按优先级有序执行,且可以短路后续插件。这适合权限检查、内容过滤等场景——低优先级插件不应该覆盖高优先级的拦截决策。发布-订阅适合松散耦合的场景,但无法保证执行顺序。

为什么插件配置使用 plugins.json 而非 config.json

插件配置独立于主配置,支持动态安装/卸载/启停而不影响主配置。plugins.json 管理插件级别的 enabled/priority,config.json 管理全局模型/渠道/API Key。

参考来源