diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index 744fc192..78119da7 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -1,4 +1,5 @@ import mimetypes +import shutil from typing import Annotated, Any, List, Optional from fastapi import APIRouter, Depends, Header, HTTPException @@ -331,10 +332,11 @@ def reset_plugin(plugin_id: str, """ 根据插件ID重置插件配置及数据 """ + plugin_manager = PluginManager() # 删除配置 - PluginManager().delete_plugin_config(plugin_id) + plugin_manager.delete_plugin_config(plugin_id) # 删除插件所有数据 - PluginManager().delete_plugin_data(plugin_id) + plugin_manager.delete_plugin_data(plugin_id) # 重新加载插件 reload_plugin(plugin_id) return schemas.Response(success=True) @@ -455,10 +457,11 @@ def set_plugin_config(plugin_id: str, conf: dict, """ 更新插件配置 """ + plugin_manager = PluginManager() # 保存配置 - PluginManager().save_plugin_config(plugin_id, conf) + plugin_manager.save_plugin_config(plugin_id, conf) # 重新生效插件 - PluginManager().init_plugin(plugin_id, conf) + plugin_manager.init_plugin(plugin_id, conf) # 注册插件服务 register_plugin(plugin_id) return schemas.Response(success=True) @@ -470,20 +473,34 @@ def uninstall_plugin(plugin_id: str, """ 卸载插件 """ + config_oper = SystemConfigOper() # 删除已安装信息 - install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or [] + install_plugins = config_oper.get(SystemConfigKey.UserInstalledPlugins) or [] for plugin in install_plugins: if plugin == plugin_id: install_plugins.remove(plugin) break - # 保存 - SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, install_plugins) + config_oper.set(SystemConfigKey.UserInstalledPlugins, install_plugins) # 移除插件API remove_plugin_api(plugin_id) # 移除插件服务 Scheduler().remove_plugin_job(plugin_id) + # 判断是否为分身 + plugin_manager = PluginManager() + plugin_class = plugin_manager.plugins.get(plugin_id) + if getattr(plugin_class, "is_clone", False): + # 如果是分身插件,则删除分身数据和配置 + plugin_manager.delete_plugin_config(plugin_id) + plugin_manager.delete_plugin_data(plugin_id) + # 删除分身文件 + plugin_base_dir = settings.ROOT_PATH / "app" / "plugins" / plugin_id.lower() + if plugin_base_dir.exists(): + try: + shutil.rmtree(plugin_base_dir) + except Exception as e: + logger.error(f"删除插件分身目录 {plugin_base_dir} 失败: {str(e)}") # 移除插件 - PluginManager().remove_plugin(plugin_id) + plugin_manager.remove_plugin(plugin_id) return schemas.Response(success=True) diff --git a/app/core/plugin.py b/app/core/plugin.py index 02ca76c1..1184e115 100644 --- a/app/core/plugin.py +++ b/app/core/plugin.py @@ -226,13 +226,21 @@ class PluginManager(metaclass=Singleton): logger.info("插件停止完成") @property - def running_plugins(self): + def running_plugins(self) -> Dict[str, Any]: """ 获取运行态插件列表 :return: 运行态插件列表 """ return self._running_plugins + @property + def plugins(self) -> Dict[str, Any]: + """ + 获取插件列表 + :return: 插件列表 + """ + return self._plugins + def reload_monitor(self): """ 重新加载插件文件修改监测 @@ -1018,8 +1026,8 @@ class PluginManager(metaclass=Singleton): logger.debug(f"获取插件 {plugin_id} 的私钥时发生错误:{e}") return None - def clone_plugin(self, plugin_id: str, suffix: str, name: str, description: str, - version: str = None, icon: str = None) -> Tuple[bool, str]: + def clone_plugin(self, plugin_id: str, suffix: str, name: str, description: str, + version: str = None, icon: str = None) -> Tuple[bool, str]: """ 创建插件分身 :param plugin_id: 原插件ID @@ -1034,48 +1042,54 @@ class PluginManager(metaclass=Singleton): # 验证参数 if not plugin_id or not suffix: return False, "插件ID和分身后缀不能为空" - + # 检查原插件是否存在 if plugin_id not in self._plugins: return False, f"原插件 {plugin_id} 不存在" - + # 生成分身插件ID clone_id = f"{plugin_id}{suffix.lower()}" - + # 检查分身插件是否已存在 if self.is_plugin_exists(clone_id): return False, f"分身插件 {clone_id} 已存在" - + # 获取原插件目录 original_plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / plugin_id.lower() if not original_plugin_dir.exists(): return False, f"原插件目录 {original_plugin_dir} 不存在" - + # 创建分身插件目录 clone_plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / clone_id.lower() - + # 复制插件目录 import shutil shutil.copytree(original_plugin_dir, clone_plugin_dir) logger.info(f"已复制插件目录:{original_plugin_dir} -> {clone_plugin_dir}") - + # 修改插件文件内容 success, msg = self._modify_plugin_files( - clone_plugin_dir, plugin_id, clone_id, suffix, name, description, version, icon + plugin_dir=clone_plugin_dir, + original_id=plugin_id, + suffix=suffix, + name=name, + description=description, + version=version, + icon=icon ) - + if not success: # 如果修改失败,清理已创建的目录 if clone_plugin_dir.exists(): shutil.rmtree(clone_plugin_dir) return False, msg - + # 将分身插件添加到已安装列表 installed_plugins = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or [] if clone_id not in installed_plugins: installed_plugins.append(clone_id) self.systemconfig.set(SystemConfigKey.UserInstalledPlugins, installed_plugins) - + # 为分身插件创建初始配置(从原插件复制配置) logger.info(f"正在为分身插件 {clone_id} 创建初始配置...") original_config = self.get_plugin_config(plugin_id) @@ -1083,16 +1097,17 @@ class PluginManager(metaclass=Singleton): # 复制原插件配置作为分身插件的初始配置 clone_config = original_config.copy() # 可以在这里修改一些默认值,比如禁用分身插件 - clone_config['enable'] = False # 默认禁用分身插件,让用户手动配置 + # 默认禁用分身插件,让用户手动配置 + clone_config['enable'] = False + clone_config['enabled'] = False self.save_plugin_config(clone_id, clone_config) logger.info(f"已为分身插件 {clone_id} 设置初始配置") else: logger.info(f"原插件 {plugin_id} 没有配置,分身插件 {clone_id} 将使用默认配置") - - # 重新初始化插件系统以完全注册新插件 - logger.info(f"正在重新注册插件系统以识别分身插件 {clone_id}...") - self.init_config() - + + # 注册分身插件的API和服务 + logger.info(f"正在注册分身插件 {clone_id} ...") + PluginManager().reload_plugin(plugin_id) # 确保分身插件正确初始化配置 if clone_id in self._running_plugins: clone_instance = self._running_plugins[clone_id] @@ -1101,27 +1116,21 @@ class PluginManager(metaclass=Singleton): logger.info(f"正在为分身插件 {clone_id} 重新初始化配置...") clone_instance.init_plugin(clone_config) logger.info(f"分身插件 {clone_id} 配置重新初始化完成") - - # 注册分身插件的API和服务 - logger.info(f"正在注册分身插件 {clone_id} 的API和服务...") - from app.api.endpoints.plugin import register_plugin - register_plugin(clone_id) - + logger.info(f"插件分身 {clone_id} 创建成功") return True, "插件分身创建成功" - + except Exception as e: logger.error(f"创建插件分身失败:{str(e)}") return False, f"创建插件分身失败:{str(e)}" - - def _modify_plugin_files(self, plugin_dir: Path, original_id: str, clone_id: str, - suffix: str, name: str, description: str, version: str = None, - icon: str = None) -> Tuple[bool, str]: + + def _modify_plugin_files(self, plugin_dir: Path, original_id: str, suffix: str, + name: str, description: str, version: str = None, + icon: str = None) -> Tuple[bool, str]: """ 修改插件文件中的类名和相关信息 :param plugin_dir: 插件目录 :param original_id: 原插件ID - :param clone_id: 分身插件ID :param suffix: 分身后缀 :param name: 分身名称 :param description: 分身描述 @@ -1134,51 +1143,60 @@ class PluginManager(metaclass=Singleton): original_plugin_class = self._plugins.get(original_id) if not original_plugin_class: return False, f"无法获取原插件类 {original_id}" - + # 获取原类名 original_class_name = original_plugin_class.__name__ clone_class_name = f"{original_class_name}{suffix}" - + # 修改 __init__.py 文件 init_file = plugin_dir / "__init__.py" if init_file.exists(): success, msg = self._modify_python_file( - init_file, original_class_name, clone_class_name, name, description, version, icon + file_path=init_file, + original_class_name=original_class_name, + clone_class_name=clone_class_name, + name=name, + description=description, + version=version, + icon=icon ) if not success: return False, msg - + # 检查是否为联邦插件(存在dist目录) dist_dir = plugin_dir / "dist" if dist_dir.exists(): success, msg = self._modify_federation_files( - dist_dir, original_class_name, clone_class_name + dist_dir=dist_dir, + original_class_name=original_class_name, + clone_class_name=clone_class_name ) if not success: return False, msg - + return True, "文件修改成功" - + except Exception as e: logger.error(f"修改插件文件失败:{str(e)}") return False, f"修改插件文件失败:{str(e)}" - - def _modify_python_file(self, file_path: Path, original_class_name: str, - clone_class_name: str, name: str, description: str, - version: str = None, icon: str = None) -> Tuple[bool, str]: + + @staticmethod + def _modify_python_file(file_path: Path, original_class_name: str, + clone_class_name: str, name: str, description: str, + version: str = None, icon: str = None) -> Tuple[bool, str]: """ 修改Python文件中的类名和插件信息 """ try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - + # 替换类名 content = content.replace(f"class {original_class_name}", f"class {clone_class_name}") - + # 替换插件名称和描述 import re - + # 替换 plugin_name if name: content = re.sub( @@ -1186,7 +1204,7 @@ class PluginManager(metaclass=Singleton): f'plugin_name = "{name}"', content ) - + # 替换 plugin_desc if description: content = re.sub( @@ -1194,14 +1212,14 @@ class PluginManager(metaclass=Singleton): f'plugin_desc = "{description}"', content ) - + # 替换 plugin_config_prefix(如果存在) content = re.sub( r'plugin_config_prefix\s*=\s*["\'][^"\']*["\']', f'plugin_config_prefix = "{clone_class_name.lower()}_"', content ) - + # 替换 plugin_version(如果提供了自定义版本) if version: content = re.sub( @@ -1209,7 +1227,7 @@ class PluginManager(metaclass=Singleton): f'plugin_version = "{version}"', content ) - + # 替换 plugin_icon(如果提供了自定义图标) if icon and icon.strip(): old_content = content @@ -1224,19 +1242,25 @@ class PluginManager(metaclass=Singleton): logger.warning(f"插件图标替换失败,未找到匹配的图标设置") else: logger.info("未提供自定义图标,保持原插件图标") - + + # 添加分身标志 + if "def init_plugin(self" in content: + init_index = content.index("def init_plugin(self") + # 在 def init_plugin(self 前添加 is_clone = True + content = content[:init_index] + "is_clone = True\n\n " + content[init_index:] + with open(file_path, 'w', encoding='utf-8') as f: f.write(content) - + logger.debug(f"已修改Python文件:{file_path}") return True, "Python文件修改成功" - + except Exception as e: logger.error(f"修改Python文件失败:{str(e)}") return False, f"修改Python文件失败:{str(e)}" - - def _modify_federation_files(self, dist_dir: Path, original_class_name: str, - clone_class_name: str) -> Tuple[bool, str]: + + def _modify_federation_files(self, dist_dir: Path, original_class_name: str, + clone_class_name: str) -> Tuple[bool, str]: """ 修改联邦插件的前端文件 """ @@ -1244,18 +1268,18 @@ class PluginManager(metaclass=Singleton): # 获取原始插件名(从类名推导) original_plugin_name = original_class_name clone_plugin_name = clone_class_name - + # 遍历dist目录下的所有文件 for file_path in dist_dir.rglob("*"): if not file_path.is_file(): continue - + # 处理JS文件 if file_path.suffix == '.js': try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - + # 替换类名引用(精确匹配) content = content.replace(original_class_name, clone_class_name) # 替换插件名引用(如果存在) @@ -1265,45 +1289,46 @@ class PluginManager(metaclass=Singleton): content = content.replace(f'css__{original_class_name}__', f'css__{clone_class_name}__') # 替换可能的小写类名引用 content = content.replace(original_class_name.lower(), clone_class_name.lower()) - + with open(file_path, 'w', encoding='utf-8') as f: f.write(content) - + logger.debug(f"已修改联邦插件JS文件:{file_path}") - + except Exception as e: logger.warning(f"修改联邦插件文件 {file_path} 失败:{str(e)}") continue - + # 处理CSS文件 elif file_path.suffix == '.css': try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - + # 替换CSS中可能的类名引用 content = content.replace(original_class_name.lower(), clone_class_name.lower()) content = content.replace(original_class_name, clone_class_name) - + with open(file_path, 'w', encoding='utf-8') as f: f.write(content) - + logger.debug(f"已修改联邦插件CSS文件:{file_path}") - + except Exception as e: logger.warning(f"修改联邦插件CSS文件 {file_path} 失败:{str(e)}") continue - + # 重命名构建文件(如果需要) self._rename_federation_assets(dist_dir, original_class_name, clone_class_name) - + return True, "联邦插件文件修改完成" - + except Exception as e: logger.error(f"修改联邦插件文件失败:{str(e)}") return False, f"修改联邦插件文件失败:{str(e)}" - - def _rename_federation_assets(self, dist_dir: Path, original_class_name: str, clone_class_name: str): + + @staticmethod + def _rename_federation_assets(dist_dir: Path, original_class_name: str, clone_class_name: str): """ 重命名联邦插件的资源文件,避免文件名冲突 """ @@ -1312,21 +1337,21 @@ class PluginManager(metaclass=Singleton): for file_path in dist_dir.glob("*"): if not file_path.is_file(): continue - + file_name = file_path.name # 如果文件名包含原类名,则重命名 if original_class_name.lower() in file_name.lower(): new_name = file_name.replace( - original_class_name.lower(), + original_class_name.lower(), clone_class_name.lower() ) new_path = file_path.parent / new_name - + # 避免重命名冲突 if not new_path.exists(): file_path.rename(new_path) logger.debug(f"重命名联邦插件文件:{file_name} -> {new_name}") - + except Exception as e: - logger.warning(f"重命名联邦插件资源文件失败:{str(e)}") # 重命名失败不影响整体流程 + logger.warning(f"重命名联邦插件资源文件失败:{str(e)}") diff --git a/app/plugins/__init__.py b/app/plugins/__init__.py index 01e2d042..7619ba58 100644 --- a/app/plugins/__init__.py +++ b/app/plugins/__init__.py @@ -34,6 +34,8 @@ class _PluginBase(metaclass=ABCMeta): plugin_desc: Optional[str] = "" # 插件顺序 plugin_order: Optional[int] = 9999 + # 是否为插件分身 + is_clone: bool = False def __init__(self): # 插件数据