"""插件管理器 - 负责插件的发现、加载和管理""" from typing import Dict, Optional, List, Type from pathlib import Path from scout.config.paths import DATA_DIR as _SCOUT_DATA_DIR import importlib.util import sys import json import logging import inspect from datetime import datetime from . import Plugin, EventType, EventContext from .event import event_bus, Event logger = logging.getLogger(__name__) class PluginManager: """插件管理器""" def __init__(self, plugins_dir: Path): self.plugins_dir = plugins_dir self.plugins_dir.mkdir(exist_ok=True) self._plugins: Dict[str, Plugin] = {} self._plugin_classes: Dict[str, Type[Plugin]] = {} self._config_file = self.plugins_dir / "plugins.json" # 加载配置 self._config = self._load_config() def auto_discover(self) -> int: """自动发现并加载所有插件""" discovered = self.discover_plugins() loaded = 0 for name in discovered: if self.load_plugin(name): loaded += 1 logger.info(f"自动发现:共加载 {loaded}/{len(discovered)} 个插件") return loaded def _load_config(self) -> Dict: """加载插件配置""" if self._config_file.exists(): try: with open(self._config_file, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: logger.error(f"加载插件配置失败: {e}") return {"plugins": {}} def _save_config(self): """保存插件配置""" try: with open(self._config_file, 'w', encoding='utf-8') as f: json.dump(self._config, f, indent=2, ensure_ascii=False) except Exception as e: logger.error(f"保存插件配置失败: {e}") def discover_plugins(self) -> List[str]: """发现插件目录""" discovered = [] for item in self.plugins_dir.iterdir(): if item.is_dir() and not item.name.startswith('_'): # 检查是否有 __init__.py init_file = item / "__init__.py" if init_file.exists(): discovered.append(item.name) logger.debug(f"发现插件: {item.name}") return discovered def load_plugin(self, name: str) -> bool: """加载单个插件""" try: plugin_dir = self.plugins_dir / name init_file = plugin_dir / "__init__.py" if not init_file.exists(): logger.error(f"插件 {name} 缺少 __init__.py") return False # 动态加载模块 spec = importlib.util.spec_from_file_location( f"plugins.{name}", init_file ) module = importlib.util.module_from_spec(spec) sys.modules[f"plugins.{name}"] = module spec.loader.exec_module(module) # 查找插件类 plugin_class = None for attr_name in dir(module): attr = getattr(module, attr_name) if (inspect.isclass(attr) and issubclass(attr, Plugin) and attr is not Plugin): plugin_class = attr break if not plugin_class: logger.error(f"插件 {name} 未找到 Plugin 子类") return False # 实例化插件 plugin = plugin_class(plugin_dir) # 检查是否需要禁用 plugin_config = self._config.get("plugins", {}).get(name, {}) if "enabled" in plugin_config: plugin.enabled = plugin_config["enabled"] # 注册插件 self._plugins[name] = plugin self._plugin_classes[name] = plugin_class # SPI 注册(2026-08-27):插件声明的 provides 自动注册进 SPIRegistry try: from .spi import register_provider for kind in getattr(plugin, "provides", []) or []: impl = plugin.provide(kind) if impl is not None: register_provider(kind, impl, source=name) except Exception as e: logger.error(f"插件 {name} 注册 SPI 失败: {e}") logger.info(f"已加载插件: {name} v{plugin.version}") return True except Exception as e: logger.error(f"加载插件 {name} 失败: {e}", exc_info=True) return False def load_all_plugins(self) -> int: """加载所有插件""" discovered = self.discover_plugins() loaded = 0 for name in discovered: if self.load_plugin(name): loaded += 1 logger.info(f"共加载 {loaded}/{len(discovered)} 个插件") return loaded def unload_plugin(self, name: str) -> bool: """卸载插件""" if name not in self._plugins: logger.warning(f"插件 {name} 未加载") return False try: plugin = self._plugins[name] # 调用卸载钩子 if plugin.enabled: event_bus.emit_sync(Event( name="plugin.unloaded", data={"plugin": name} )) # 注销该插件注册的 SPI(2026-08-27) try: from .spi import SPIRegistry for kind, src in list(SPIRegistry._source.items()): if src == name: SPIRegistry.unregister(kind) except Exception as e: logger.error(f"插件 {name} 注销 SPI 失败: {e}") # 清理 del self._plugins[name] del self._plugin_classes[name] # 从 sys.modules 中移除 module_name = f"plugins.{name}" if module_name in sys.modules: del sys.modules[module_name] logger.info(f"已卸载插件: {name}") return True except Exception as e: logger.error(f"卸载插件 {name} 失败: {e}", exc_info=True) return False def enable_plugin(self, name: str) -> bool: """启用插件""" if name not in self._plugins: logger.warning(f"插件 {name} 未加载") return False plugin = self._plugins[name] if plugin.enabled: logger.info(f"插件 {name} 已启用") return True try: plugin.enabled = True # 更新配置 if "plugins" not in self._config: self._config["plugins"] = {} self._config["plugins"][name] = {"enabled": True} self._save_config() # 发出事件 event_bus.emit_sync(Event( name="plugin.enabled", data={"plugin": name} )) logger.info(f"已启用插件: {name}") return True except Exception as e: logger.error(f"启用插件 {name} 失败: {e}", exc_info=True) return False def disable_plugin(self, name: str) -> bool: """禁用插件""" if name not in self._plugins: logger.warning(f"插件 {name} 未加载") return False plugin = self._plugins[name] if not plugin.enabled: logger.info(f"插件 {name} 已禁用") return True try: plugin.enabled = False # 更新配置 if "plugins" not in self._config: self._config["plugins"] = {} self._config["plugins"][name] = {"enabled": False} self._save_config() # 发出事件 event_bus.emit_sync(Event( name="plugin.disabled", data={"plugin": name} )) logger.info(f"已禁用插件: {name}") return True except Exception as e: logger.error(f"禁用插件 {name} 失败: {e}", exc_info=True) return False def get_plugin(self, name: str) -> Optional[Plugin]: """获取插件实例""" return self._plugins.get(name) def list_plugins(self) -> List[Dict]: """列出所有插件信息""" result = [] for name, plugin in sorted(self._plugins.items()): result.append({ "name": name, "version": plugin.version, "author": plugin.author, "description": plugin.description, "enabled": plugin.enabled, "priority": plugin.priority, }) return result async def emit_event(self, event: EventContext) -> EventContext: """向所有启用的插件发出事件""" # 按优先级排序 sorted_plugins = sorted( self._plugins.values(), key=lambda p: p.priority ) for plugin in sorted_plugins: if not plugin.enabled: continue try: # 调用通用事件处理器 if await plugin.on_event(event): if event.stop_propagation: break # 调用特定事件处理器 if event.event_type == EventType.BEFORE_CHAT: result = await plugin.before_chat( event.data.get("message", ""), event.data.get("session_id", "") ) if result is not None: event.data["message"] = result elif event.event_type == EventType.AFTER_CHAT: result = await plugin.after_chat( event.data.get("message", ""), event.data.get("response", ""), event.data.get("session_id", "") ) if result is not None: event.data["response"] = result elif event.event_type == EventType.ON_MESSAGE: await plugin.on_message( event.data.get("role", ""), event.data.get("content", ""), event.data.get("session_id", "") ) except Exception as e: logger.error(f"插件 {plugin.name} 处理事件 {event.event_type} 失败: {e}") return event def register_with_event_bus(self): """将插件管理器注册到全局事件总线""" @event_bus.on("before_chat") async def handle_before_chat(event: Event): ctx = EventContext( event_type=EventType.BEFORE_CHAT, data=event.data ) result = self.emit_event(ctx) if "message" in result.data: event.data["message"] = result.data["message"] @event_bus.on("after_chat") async def handle_after_chat(event: Event): ctx = EventContext( event_type=EventType.AFTER_CHAT, data=event.data ) result = self.emit_event(ctx) if "response" in result.data: event.data["response"] = result.data["response"] @event_bus.on("on_message") async def handle_on_message(event: Event): ctx = EventContext( event_type=EventType.ON_MESSAGE, data=event.data ) await self.emit_event(ctx) logger.info("插件管理器已注册到事件总线") # ── 全局单例 ── _plugin_manager: Optional[PluginManager] = None def get_plugin_manager() -> PluginManager: """获取全局插件管理器实例(首次创建时自动加载所有插件)""" global _plugin_manager if _plugin_manager is None: # 默认插件目录: ~/.scout/plugins plugins_dir = _SCOUT_DATA_DIR / "plugins" _plugin_manager = PluginManager(plugins_dir) # 与 scout/plugins/api.py 行为一致:首次访问即加载所有插件 _plugin_manager.load_all_plugins() return _plugin_manager def set_plugin_manager(manager: PluginManager) -> None: """设置全局插件管理器实例(用于测试或自定义配置)""" global _plugin_manager _plugin_manager = manager