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, }注册过程:
- 设置插件元数据(name, priority, desc, author, version, enabled)
- 按 priority 存入
SortedDict(堆结构维护优先级排序) - 写入
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。