剥离无效事件系统并收口插件统计链路

- 删除未被实际消费的事件系统实现与相关发布逻辑
- 将插件调用统计改为在机器人主链路中直接埋点记录
- 重构统计收集插件初始化与记录方式,移除事件总线依赖
- 同步更新工程优化文档中的性能与链路治理描述
This commit is contained in:
liuwei
2026-04-30 14:54:22 +08:00
parent 78e4f50b7e
commit 0878f0d4ea
7 changed files with 174 additions and 284 deletions

128
robot.py
View File

@@ -3,13 +3,13 @@ import asyncio
import threading
import time
import tomllib
import traceback
from collections import deque
import toml
from loguru import logger
import wechat_ipad
from base.plugin_common.event_system import EventType, EventSystem
from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus
from base.plugin_common.plugin_manager import PluginManager
@@ -98,13 +98,11 @@ class Robot:
# 初始化插件系统
self.LOG.debug("开始初始化插件系统...")
self.plugin_registry = PluginRegistry()
self.event_system = EventSystem()
self.plugin_modules = {} # 存储已加载的插件模块
self.plugins = {} # 存储已加载的插件实例
# 设置插件系统上下文
self.system_context = {
"config": config,
"event_system": self.event_system,
"plugin_registry": self.plugin_registry,
"db_manager": self.db_manager,
"db_pool": self.db_pool,
@@ -545,11 +543,8 @@ class Robot:
except Exception as e:
self.LOG.error(f"获取群成员信息失败: {e}")
# 发布消息接收事件
self.event_system.publish(EventType.MESSAGE_RECEIVED, {"message": message})
# 尝试使用插件处理消息
plugin_processed = await self.process_plugin_message(message)
await self.process_plugin_message(message)
if is_group:
self.LOG.debug(f"入库和记录群消息: {message}")
@@ -609,13 +604,22 @@ class Robot:
if plugin.status != PluginStatus.RUNNING:
continue
# 这里在进入插件前统一准备统计上下文:
# 1. 事件系统删除后,插件调用统计需要直接在主链路埋点;
# 2. 提前抽出 room_id / sender / command后续无论成功还是异常都能复用
# 3. 这样可以保证观测逻辑收口在一处,避免每个插件自己重复埋点。
room_id = msg.roomid if msg.from_group() else ""
sender = msg.sender
command_name = self._extract_plugin_command(msg)
started_at = time.perf_counter()
try:
# 转换消息为插件可处理的格式
plugin_msg = {
"type": msg.msg_type,
"content": msg.content.clean_content,
"sender": msg.sender,
"roomid": msg.roomid if msg.from_group() else "",
"sender": sender,
"roomid": room_id,
"is_at": msg.is_at(self.wxid),
"timestamp": time.time(),
"all_contacts": self.allContacts,
@@ -628,18 +632,114 @@ class Robot:
# 检查插件是否可以处理该消息
if plugin.can_process(plugin_msg):
processed, _ = await plugin.process_message(plugin_msg)
self._record_plugin_call_result(
plugin=plugin,
msg=msg,
command_name=command_name,
# 这里把“无异常执行完成”视为统计意义上的成功:
# 1. 很多插件返回 False 只是表示“本次不拦截”或“异步排队后继续放行”;
# 2. 若直接把 processed=False 记成失败,会把成功率统计严重拉低;
# 3. 真正的失败已经会走异常分支,因此统计层这里按“未抛错即成功”更合理。
process_result=True,
process_time_ms=self._elapsed_ms(started_at),
)
if processed:
# 发布消息处理事件
self.event_system.publish(EventType.MESSAGE_PROCESSED, {
"message": msg,
"plugin": plugin.name
})
return True
except Exception as e:
self._record_plugin_call_error(
plugin=plugin,
msg=msg,
command_name=command_name,
error=e,
)
self.LOG.error(f"插件 {plugin.name} 处理消息失败: {e}")
return False
@staticmethod
def _elapsed_ms(started_at: float) -> float:
"""把 monotonic 起始时间转换为毫秒耗时。"""
return round((time.perf_counter() - started_at) * 1000, 2)
@staticmethod
def _extract_plugin_command(msg: WxMessage) -> str:
"""尽力从消息内容中提取一个可读的“触发命令”。"""
# 这里不追求把所有命令解析得非常精确,只要能满足后台统计可读性即可:
# 1. 文本消息优先取第一段词,避免把整句长文本都记成 command
# 2. 非文本消息统一落到消息类型名,便于区分“文本触发”和“链接触发”等场景;
# 3. 空内容时返回通用占位,避免统计表出现 NULL / 空字符串。
raw_content = str(getattr(getattr(msg, "content", None), "clean_content", "") or "").strip()
if raw_content:
first_token = raw_content.split()[0].strip()
return first_token[:50] if first_token else "[文本消息]"
msg_type = getattr(getattr(msg, "msg_type", None), "name", "")
return f"[{msg_type or 'UNKNOWN'}]"
def _get_stats_collector_plugin(self):
"""获取运行中的统计收集插件实例。"""
# 统计插件已经从“事件订阅”切到“主链路直接回调”,
# 因此每次埋点前都需要安全地确认插件实例是否存在且处于运行态。
plugin = self.plugin_manager.plugins.get("指令记录")
if not plugin:
return None
if getattr(plugin, "status", None) != PluginStatus.RUNNING:
return None
return plugin
def _record_plugin_call_result(
self,
*,
plugin,
msg: WxMessage,
command_name: str,
process_result: bool,
process_time_ms: float,
) -> None:
"""将插件执行结果直接写入统计插件。"""
stats_plugin = self._get_stats_collector_plugin()
if not stats_plugin or not hasattr(stats_plugin, "record_plugin_call"):
return
try:
stats_plugin.record_plugin_call(
plugin_name=plugin.name,
command=command_name,
user_id=msg.sender,
group_id=msg.roomid if msg.from_group() else None,
is_group=msg.from_group(),
process_result=process_result,
process_time_ms=process_time_ms,
)
except Exception as stats_error:
self.LOG.error(f"记录插件调用统计失败: plugin={plugin.name}, error={stats_error}")
def _record_plugin_call_error(
self,
*,
plugin,
msg: WxMessage,
command_name: str,
error: Exception,
) -> None:
"""将插件执行异常直接写入统计插件。"""
stats_plugin = self._get_stats_collector_plugin()
if not stats_plugin or not hasattr(stats_plugin, "record_plugin_error"):
return
try:
stats_plugin.record_plugin_error(
plugin_name=plugin.name,
command=command_name,
user_id=msg.sender,
group_id=msg.roomid if msg.from_group() else None,
is_group=msg.from_group(),
error_message=str(error),
# 这里保留完整堆栈,便于后台直接查看异常上下文,而不必只看摘要日志。
stack_trace=traceback.format_exc(),
)
except Exception as stats_error:
self.LOG.error(f"记录插件异常统计失败: plugin={plugin.name}, error={stats_error}")
@staticmethod
def _sort_message_plugins(message_plugins):
"""将兜底型插件放到最后执行,避免影响其他插件命中。"""