剥离无效事件系统并收口插件统计链路
- 删除未被实际消费的事件系统实现与相关发布逻辑 - 将插件调用统计改为在机器人主链路中直接埋点记录 - 重构统计收集插件初始化与记录方式,移除事件总线依赖 - 同步更新工程优化文档中的性能与链路治理描述
This commit is contained in:
128
robot.py
128
robot.py
@@ -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):
|
||||
"""将兜底型插件放到最后执行,避免影响其他插件命中。"""
|
||||
|
||||
Reference in New Issue
Block a user