fix:优化插件分身性能

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