from loguru import logger from typing import Dict, Any, Tuple, Optional, List from base.plugin_common.plugin_interface import PluginInterface, PluginStatus from db.stats_db import StatsDBOperator from db.connection import DBConnectionManager class StatsCollectorPlugin(PluginInterface): """统计收集插件""" @property def name(self) -> str: return "指令记录" @property def version(self) -> str: return "1.0.0" @property def description(self) -> str: return "群聊指令数据记录" @property def author(self) -> str: return "Liuwei" @property def command_prefix(self) -> Optional[str]: return "#" @property def commands(self) -> List[str]: return [] def __init__(self): super().__init__() self.LOG = logger self.LOG.debug(f"正在初始化 {self.name} 插件...") # 默认配置: # 1. 这个插件现在不再依赖事件总线,而是由主消息分发链路直接回调; # 2. 因此这里保留一份轻量配置,只控制“是否记录”和“排除哪些插件”; # 3. 这样既能延续原有统计面板数据结构,也能避免事件系统带来的额外复杂度。 self.config = { "enable": True, "record_all_plugins": True, # 是否记录所有插件的调用 "excluded_plugins": [], # 排除的插件列表 } self.db_manager = DBConnectionManager.get_instance() self.stats_db = StatsDBOperator(self.db_manager) def initialize(self, context: Dict[str, Any]) -> bool: """初始化插件""" # 这里显式只读取插件自己的配置,不再把 system_context 整体 merge 进来: # 1. 旧实现把 initialize 入参当成“插件配置”使用,但实际传入的是 system_context; # 2. 结果会把 db_manager、redis_pool 等运行时对象误写到 self.config 中; # 3. 改成只消费 load_config 后的 self._config,避免配置结构持续污染。 if isinstance(self._config, dict): self.config.update(self._config) # 若主系统已经初始化了 DB 管理器,则优先复用统一实例,避免插件侧再走兜底单例。 if isinstance(context, dict) and context.get("db_manager") is not None: self.db_manager = context.get("db_manager") self.stats_db = StatsDBOperator(self.db_manager) if not self.config["enable"]: self.LOG.info("统计收集插件已禁用") return False return True def record_plugin_call( self, *, plugin_name: str, command: str, user_id: str, group_id: Optional[str], is_group: bool, process_result: bool, process_time_ms: float, trace_id: str = "", ) -> None: """由主链路直接调用,记录一次插件执行结果。""" # 主链路可能会在高频场景下频繁回调这里,因此先做最便宜的开关判断, # 避免对已关闭或被排除插件继续产生数据库写入成本。 if not self._should_record_plugin(plugin_name): return try: self.stats_db.record_plugin_call( plugin_name=plugin_name, command=command, user_id=user_id, group_id=group_id if is_group else None, success=process_result, process_time_ms=process_time_ms, ) self.LOG.debug( f"记录插件调用结束: trace_id={trace_id or '-'} " f"{plugin_name} - {command} - 成功: {process_result} - 处理时间: {process_time_ms}ms" ) except Exception as e: self.LOG.error(f"记录插件调用统计数据出错: {e}") def record_plugin_error( self, *, plugin_name: str, command: str, user_id: str, group_id: Optional[str], is_group: bool, error_message: str, trace_id: str = "", stack_trace: Optional[str] = None, ) -> None: """由主链路直接调用,记录一次插件执行异常。""" if not self._should_record_plugin(plugin_name): return try: self.stats_db.record_error( plugin_name=plugin_name, command=command, user_id=user_id, group_id=group_id if is_group else None, # 错误表当前没有独立 trace_id 字段,因此先把 trace_id 前缀写入文案, # 这样后台查看错误列表时,仍然可以把数据库错误记录与运行日志串起来。 error_message=f"[trace_id={trace_id}] {error_message}" if trace_id else error_message, stack_trace=(f"[trace_id={trace_id}]\n{stack_trace}" if trace_id and stack_trace else stack_trace), ) self.LOG.debug( f"记录插件调用错误: trace_id={trace_id or '-'} {plugin_name} - {command} - {error_message}" ) except Exception as e: self.LOG.error(f"记录插件错误信息出错: {e}") def _should_record_plugin(self, plugin_name: str) -> bool: """检查是否应该记录该插件的调用""" if not self.config["record_all_plugins"]: return False if plugin_name in self.config["excluded_plugins"]: return False # 不记录自身的调用 if plugin_name == self.name: return False return True def match_command(self, content: str) -> bool: """匹配命令""" # 该插件不处理用户消息 return False def process_message(self, message: Dict[str, Any]) -> Tuple[bool, str]: """处理消息""" # 该插件不处理用户消息 return False, "" def shutdown(self) -> None: """关闭插件""" self.LOG.info("统计收集插件已关闭") def start(self) -> bool: """启动插件""" self.LOG.debug(f"[{self.name}] 插件已启动") self.status = PluginStatus.RUNNING return True def stop(self) -> bool: """停止插件""" self.LOG.info(f"[{self.name}] 插件已停止") self.status = PluginStatus.STOPPED return True