#!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging import asyncio import importlib.util import sys import os import inspect import time from pathlib import Path from typing import Dict, List, Any, Optional, Callable from dataclasses import dataclass import yaml import traceback from sdk.plugin_command_decorator import plugin_command, command logger = logging.getLogger(__name__) @dataclass class PluginInfo: """插件信息数据类""" name: str version: str description: str author: str enabled: bool loaded: bool error_count: int permissions: List[str] plugin_path: Path commands: Dict[str, Dict] = None # 新增命令信息 class PluginService: """插件服务 - 管理插件的加载、卸载和运行""" def __init__(self, config: Dict, permission_service, bridge_service, service_manager): self.config = config self.permission_service = permission_service self.bridge_service = bridge_service self.service_manager = service_manager # 新增服务管理器 self.bridge_service.service_manager = self.service_manager self.plugins: Dict[str, Any] = {} self.plugin_info: Dict[str, PluginInfo] = {} self.plugins_dir = Path("plugins") self.is_running = False logger.debug("PluginService初始化开始") async def start(self): """启动插件服务""" try: logger.info("启动插件服务") # 创建插件目录 self.plugins_dir.mkdir(exist_ok=True) # 自动加载插件 if self.config['plugins']['auto_load']: await self.load_all_plugins() await self.save_command_config() # 启动 watchdog 热重载 if self.config.get('plugins', {}).get('hot_reload', False): self._loop = asyncio.get_running_loop() self._start_watchdog() self.is_running = True logger.info("插件服务启动完成") except Exception as e: logger.error(f"启动插件服务时出错: {str(e)}", exc_info=True) raise # ── watchdog 热重载 ────────────────────────────────────── def _start_watchdog(self): """启动 watchdog 监听 plugins/ 目录实现热重载""" try: from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler plugin_service = self # 闭包引用 class _PluginReloadHandler(FileSystemEventHandler): """防抖 + 插件级重载""" def __init__(self): self._debounce: Dict[str, float] = {} self._debounce_sec = 1.0 # 1 秒内同插件只触发一次 def _plugin_name_from_path(self, event_path: str) -> Optional[str]: """从事件路径提取插件名(plugins//...)""" try: rel = os.path.relpath(event_path, str(plugin_service.plugins_dir)) parts = Path(rel).parts if parts and not parts[0].startswith('.'): return parts[0] except ValueError: pass return None def _should_handle(self, plugin_name: str) -> bool: """防抖 — 同插件在冷却时间内跳过""" now = time.time() last = self._debounce.get(plugin_name, 0) if now - last < self._debounce_sec: return False self._debounce[plugin_name] = now return True def on_modified(self, event): if event.is_directory: return path = event.src_path if not path.endswith(('.py', '.yaml', '.yml')): return plugin_name = self._plugin_name_from_path(path) if not plugin_name: return if not self._should_handle(plugin_name): return logger.info( f"🔁 检测到插件文件变更: {plugin_name} ({os.path.basename(path)})" ) asyncio.run_coroutine_threadsafe( plugin_service._reload_plugin(plugin_name), plugin_service._loop, ) def on_created(self, event): self.on_modified(event) self._watchdog_observer = Observer() self._watchdog_observer.schedule( _PluginReloadHandler(), str(self.plugins_dir), recursive=True, ) self._watchdog_observer.start() logger.info("👁️ 插件热重载已启动 (watchdog)") except ImportError: logger.warning( "watchdog 未安装,插件热重载不可用。pip install watchdog" ) except Exception as e: logger.warning(f"启动 watchdog 失败 (不影响框架): {e}") async def _reload_plugin(self, plugin_name: str): """热重载单个插件 — 卸载后重新加载""" plugin_dir = self.plugins_dir / plugin_name if not plugin_dir.is_dir(): logger.debug(f"插件目录已消失,跳过重载: {plugin_name}") return if plugin_name in self.plugins: logger.info(f" ⏳ 卸载旧版本: {plugin_name}") await self.unload_plugin(plugin_name) await asyncio.sleep(0.2) # 给文件系统缓冲 success = await self.load_plugin(plugin_name) if success: # 重载后重新注册延迟路由 try: internet = self.service_manager.get_service("internet") if internet and internet.is_running: plugin = self.plugins.get(plugin_name) if plugin and hasattr(plugin, 'network_bridge'): await self._reinitialize_plugin_network(plugin, internet) except Exception: pass logger.info(f" ✅ 热重载完成: {plugin_name}") else: logger.warning(f" ❌ 热重载失败: {plugin_name}") def _stop_watchdog(self): """停止 watchdog observer""" obs = getattr(self, '_watchdog_observer', None) if obs and obs.is_alive(): obs.stop() obs.join(timeout=3) logger.info("👁️ 插件热重载已停止") async def stop(self): """停止插件服务""" logger.info("关闭插件服务") self._stop_watchdog() for name in list(self.plugins.keys()): await self.unload_plugin(name) self.is_running = False def _should_isolate(self, plugin_config: dict) -> bool: """检查插件是否应使用进程隔离模式""" global_isolation = self.config.get('plugins', {}).get('isolation', False) plugin_isolation = plugin_config.get('settings', {}).get('isolation', None) if plugin_isolation is not None: return bool(plugin_isolation) return global_isolation async def load_all_plugins(self): """加载所有插件""" try: logger.debug("开始加载所有插件") if not self.plugins_dir.exists(): logger.warning("插件目录不存在,跳过加载") return loaded_count = 0 error_count = 0 isolated_count = 0 # 遍历插件目录 for plugin_dir in self.plugins_dir.iterdir(): if plugin_dir.is_dir(): try: # 先读配置判断是否需要隔离 config_file = plugin_dir / "config.yaml" plugin_config = {} if config_file.exists(): with open(config_file, 'r') as f: plugin_config = yaml.safe_load(f) or {} if self._should_isolate(plugin_config): success = await self._load_plugin_isolated(plugin_dir.name) if success: isolated_count += 1 loaded_count += 1 else: error_count += 1 else: success = await self.load_plugin(plugin_dir.name) if success: loaded_count += 1 else: error_count += 1 except Exception as e: logger.error(f"加载插件 {plugin_dir.name} 时出错: {str(e)}", exc_info=True) error_count += 1 logger.info( f"插件加载完成: 成功 {loaded_count} (其中隔离 {isolated_count}), 失败 {error_count}" ) except Exception as e: logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True) raise async def _load_plugin_isolated(self, plugin_name: str) -> bool: """在独立子进程中加载插件(进程隔离模式)""" try: from services.process_isolated import IsolatedPlugin plugin_path = self.plugins_dir / plugin_name main_module = plugin_path / "__init__.py" if not main_module.exists(): logger.error(f"隔离插件主模块不存在: {main_module}") return False config_file = plugin_path / "config.yaml" plugin_config = {} if config_file.exists(): with open(config_file) as f: plugin_config = yaml.safe_load(f) or {} iso = IsolatedPlugin( plugin_name, str(main_module), plugin_config, ) self.plugins[plugin_name] = iso self.plugin_info[plugin_name] = PluginInfo( name=plugin_config.get('name', plugin_name), version=plugin_config.get('version', '0.1.0'), description=plugin_config.get('description', ''), author=plugin_config.get('author', ''), enabled=True, loaded=True, error_count=0, permissions=[], plugin_path=plugin_path, commands={}, ) logger.info(f"🔒 隔离插件加载成功: {plugin_name} (PID={iso.pid})") return True except Exception as e: logger.error(f"加载隔离插件 {plugin_name} 失败: {e}") return False async def load_plugin(self, plugin_name: str) -> bool: """加载单个插件 - 支持异步权限处理""" try: logger.debug(f"开始加载插件: {plugin_name}") plugin_path = self.plugins_dir / plugin_name if not plugin_path.exists(): logger.error(f"插件目录不存在: {plugin_path}") return False # 检查插件配置文件 config_file = plugin_path / "config.yaml" if not config_file.exists(): logger.error(f"插件配置文件不存在: {config_file}") return False # 加载插件配置 with open(config_file, 'r', encoding='utf-8') as f: plugin_config = yaml.safe_load(f) # 检查权限文件 permission_file = plugin_path / "permissions.yaml" if not permission_file.exists(): logger.error(f"插件权限文件不存在: {permission_file}") return False # 加载权限配置 with open(permission_file, 'r', encoding='utf-8') as f: permission_config = yaml.safe_load(f) # 验证插件信息 required_fields = ['name', 'version', 'description', 'author'] for field in required_fields: if field not in plugin_config: logger.error(f"插件配置缺少必要字段: {field}") return False # 检查主模块 main_module = plugin_path / "__init__.py" if not main_module.exists(): logger.error(f"插件主模块不存在: {main_module}") return False # 动态加载插件模块 module_name = f"plugins.{plugin_name}" spec = importlib.util.spec_from_file_location(module_name, main_module) if not spec: logger.error(f"无法创建模块规范: {module_name}") return False module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module try: spec.loader.exec_module(module) logger.debug(f"插件模块加载成功: {module_name}") except Exception as e: logger.error(f"执行插件模块时出错: {str(e)}", exc_info=True) return False # 获取插件类实例 if not hasattr(module, 'Plugin'): logger.error(f"插件类 'Plugin' 不存在: {module_name}") return False # 权限申请和验证 - 非阻塞版本 permissions = permission_config.get('permissions', []) if permissions: # 非阻塞权限请求,立即返回True让插件继续加载 permission_result = await self.permission_service.request_permissions(plugin_name, permissions) if not permission_result: logger.warning(f"插件权限申请失败: {plugin_name}") # 即使权限申请失败,也允许插件以受限模式运行 logger.info(f"插件 {plugin_name} 将以受限模式运行") # 实例化插件 try: plugin_instance = module.Plugin( plugin_name=plugin_name, config=plugin_config, bridge=self.bridge_service ) # 初始化插件 if hasattr(plugin_instance, 'initialize'): if asyncio.iscoroutinefunction(plugin_instance.initialize): await plugin_instance.initialize() else: plugin_instance.initialize() # 扫描并注册 TUI 命令 plugin_commands = await self._scan_and_register_commands(plugin_name, plugin_instance, plugin_config) # 自动暴露插件命令为 REST 端点 if hasattr(plugin_instance, 'network_bridge') and plugin_instance.network_bridge: await plugin_instance.network_bridge.register_command_routes( plugin_instance, require_auth=True ) # 注册插件 self.plugins[plugin_name] = plugin_instance # 保存插件信息 self.plugin_info[plugin_name] = PluginInfo( name=plugin_config['name'], version=plugin_config['version'], description=plugin_config['description'], author=plugin_config['author'], enabled=True, loaded=True, error_count=0, permissions=permissions, plugin_path=plugin_path, commands=plugin_commands ) logger.info(f"插件加载成功: {plugin_name} v{plugin_config['version']}, 注册了 {len(plugin_commands)} 个命令") return True except Exception as e: logger.error(f"实例化插件时出错: {str(e)}", exc_info=True) return False except Exception as e: logger.error(f"加载插件 {plugin_name} 时出错: {str(e)}", exc_info=True) return False async def _scan_and_register_commands(self, plugin_name: str, plugin_instance: Any, plugin_config: Dict) -> Dict[str, Dict]: """扫描并注册插件命令 - 修正版本""" try: logger.debug(f"扫描插件命令: {plugin_name}") command_service = self.service_manager.get_service("command") if not command_service: logger.error("命令服务不可用,无法注册插件命令") return {} # 扫描插件中的命令方法 command_methods = {} for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod): # 检查方法是否有命令装饰器或符合命名约定 if (hasattr(method, '_is_plugin_command') or name.startswith('cmd_') or name.startswith('command_')): command_name = self._get_command_name(name, method, plugin_config) command_description = self._get_command_description(name, method, plugin_config) command_permissions = self._get_command_permissions(name, method, plugin_config) # 修正:使用正确的source格式 command_service.register_command( name=command_name, handler=method, description=command_description, permissions=command_permissions, source=f"plugin.{plugin_name}" # 使用 plugin.插件名 格式 ) command_methods[command_name] = { 'method_name': name, 'description': command_description, 'permissions': command_permissions } logger.debug(f"注册插件命令: {command_name} -> {name}") return command_methods except Exception as e: logger.error(f"扫描插件命令时出错: {str(e)}", exc_info=True) return {} def _get_command_name(self, method_name: str, method: Callable, plugin_config: Dict) -> str: """获取命令名称""" try: # 如果方法有装饰器指定的名称 if hasattr(method, '_command_name'): return getattr(method, '_command_name') # 从方法名提取命令名 if method_name.startswith('cmd_'): return method_name[4:] elif method_name.startswith('command_'): return method_name[8:] else: return method_name except Exception as e: logger.error(f"获取命令名称时出错: {str(e)}") return method_name def _get_command_description(self, method_name: str, method: Callable, plugin_config: Dict) -> str: """获取命令描述""" try: # 如果方法有装饰器指定的描述 if hasattr(method, '_command_description'): return getattr(method, '_command_description') # 使用方法的文档字符串 if method.__doc__: # 提取第一行作为描述 doc_lines = method.__doc__.strip().split('\n') return doc_lines[0].strip() # 默认描述 return f"插件命令: {method_name}" except Exception as e: logger.error(f"获取命令描述时出错: {str(e)}") return f"插件命令: {method_name}" def _get_command_permissions(self, method_name: str, method: Callable, plugin_config: Dict) -> List[str]: """获取命令权限""" try: # 如果方法有装饰器指定的权限 if hasattr(method, '_command_permissions'): return getattr(method, '_command_permissions') # 从插件配置中获取默认权限 default_permissions = plugin_config.get('default_command_permissions', []) return default_permissions.copy() except Exception as e: logger.error(f"获取命令权限时出错: {str(e)}") return [] async def unload_plugin(self, plugin_name: str) -> bool: """卸载插件""" try: logger.debug(f"开始卸载插件: {plugin_name}") if plugin_name not in self.plugins: logger.warning(f"插件未加载: {plugin_name}") return False plugin_instance = self.plugins[plugin_name] plugin_info = self.plugin_info[plugin_name] # 注销插件命令 (隔离插件可能没有注册命令) if not self._is_isolated(plugin_instance): await self._unregister_plugin_commands(plugin_name) # 调用插件的清理方法 try: if hasattr(plugin_instance, 'shutdown'): if asyncio.iscoroutinefunction(plugin_instance.shutdown): await plugin_instance.shutdown() else: plugin_instance.shutdown() except Exception as e: logger.error(f"插件清理时出错 {plugin_name}: {str(e)}", exc_info=True) # 从模块缓存中移除 module_name = f"plugins.{plugin_name}" if module_name in sys.modules: del sys.modules[module_name] # 移除插件实例和信息 del self.plugins[plugin_name] plugin_info.loaded = False plugin_info.enabled = False logger.info(f"插件卸载成功: {plugin_name}") return True except Exception as e: logger.error(f"卸载插件 {plugin_name} 时出错: {str(e)}", exc_info=True) return False @staticmethod def _is_isolated(plugin_instance) -> bool: from services.process_isolated import IsolatedPlugin return isinstance(plugin_instance, IsolatedPlugin) async def _unregister_plugin_commands(self, plugin_name: str): """注销插件命令""" try: command_service = self.service_manager.get_service("command") if not command_service: return # 从命令服务中移除该插件的所有命令 commands_to_remove = [] for cmd_name, cmd_info in command_service.commands.items(): if cmd_info.source.startswith(f"plugin.{plugin_name}"): commands_to_remove.append(cmd_name) for cmd_name in commands_to_remove: del command_service.commands[cmd_name] logger.debug(f"注销插件命令: {cmd_name}") logger.info(f"已注销插件 {plugin_name} 的 {len(commands_to_remove)} 个命令") except Exception as e: logger.error(f"注销插件命令时出错: {str(e)}", exc_info=True) async def save_command_config(self): """保存命令配置到文件""" try: command_service = self.service_manager.get_service("command") if not command_service: logger.error("命令服务不可用") return False command_list = command_service.get_command_list() config_path = Path("config") / "plugins" / "commands.yaml" # 确保目录存在 config_path.parent.mkdir(parents=True, exist_ok=True) config_data = { "commands": {}, "plugin_commands": {}, "last_updated": asyncio.get_event_loop().time(), "total_commands": len(command_list) } # 按来源分组命令 for cmd in command_list: cmd_info = { "description": cmd['description'], "permissions": cmd['permissions'], "source": cmd['source'] } config_data["commands"][cmd['name']] = cmd_info # 按插件分组 if cmd['source'].startswith("plugin."): plugin_name = cmd['source'].split('.', 1)[1] if plugin_name not in config_data["plugin_commands"]: config_data["plugin_commands"][plugin_name] = {} config_data["plugin_commands"][plugin_name][cmd['name']] = cmd_info with open(config_path, 'w', encoding='utf-8') as f: yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, indent=2) logger.info(f"命令配置已保存: {config_path}, 共 {len(command_list)} 个命令") return True except Exception as e: logger.error(f"保存命令配置时出错: {str(e)}", exc_info=True) return False async def register_delayed_routes(self, internet_service): """注册延迟的路由(在网络服务启动后)""" try: if not internet_service: logger.warning("网络服务不可用,跳过延迟路由注册") return for plugin_name, plugin_instance in self.plugins.items(): try: # 检查插件是否有延迟注册方法 if hasattr(plugin_instance, 'register_delayed_routes'): await plugin_instance.register_delayed_routes(internet_service) logger.info(f"延迟注册插件路由: {plugin_name}") else: # 如果插件没有延迟注册方法,尝试重新初始化网络功能 await self._reinitialize_plugin_network(plugin_instance, internet_service) except Exception as e: logger.error(f"延迟注册插件 {plugin_name} 路由时出错: {str(e)}") except Exception as e: logger.error(f"注册延迟路由时出错: {str(e)}") async def _reinitialize_plugin_network(self, plugin_instance, internet_service): """重新初始化插件的网络功能""" try: plugin_name = plugin_instance.plugin_name # 检查插件是否有网络桥接 if hasattr(plugin_instance, 'network_bridge'): # 重新创建网络桥接 plugin_instance.network_bridge = PluginNetworkBridge( plugin_name, internet_service, plugin_instance.bridge ) # 重新设置网络路由 if hasattr(plugin_instance, '_setup_network_routes'): await plugin_instance._setup_network_routes() logger.info(f"重新初始化插件网络功能: {plugin_name}") except Exception as e: logger.error(f"重新初始化插件网络功能时出错: {str(e)}")