Merge pull request #2735 from InfinityPacer/feature/plugin

This commit is contained in:
jxxghp
2024-09-18 06:44:31 +08:00
committed by GitHub
3 changed files with 242 additions and 142 deletions
+7 -3
View File
@@ -3,7 +3,6 @@ import sys
import threading import threading
from pathlib import Path from pathlib import Path
from typing import Optional, List from typing import Optional, List
from urllib.parse import urlparse
from dotenv import set_key from dotenv import set_key
from pydantic import BaseSettings, validator from pydantic import BaseSettings, validator
@@ -18,8 +17,6 @@ class Settings(BaseSettings):
""" """
# 项目名称 # 项目名称
PROJECT_NAME = "MoviePilot" PROJECT_NAME = "MoviePilot"
# 版本标识,用来区分重大版本,为空则为v1
VERSION_FLAG = "v2"
# 域名 格式;https://movie-pilot.org # 域名 格式;https://movie-pilot.org
APP_DOMAIN: str = "" APP_DOMAIN: str = ""
# API路径 # API路径
@@ -213,6 +210,13 @@ class Settings(BaseSettings):
logger.warning("API_TOKEN 长度不足 16 个字符,存在安全隐患,建议尽快更换为更复杂的密钥!") logger.warning("API_TOKEN 长度不足 16 个字符,存在安全隐患,建议尽快更换为更复杂的密钥!")
return v return v
@property
def VERSION_FLAG(self) -> str:
"""
版本标识,用来区分重大版本,为空则为v1,不允许外部修改
"""
return "v2"
@property @property
def INNER_CONFIG_PATH(self): def INNER_CONFIG_PATH(self):
return self.ROOT_PATH / "config" return self.ROOT_PATH / "config"
+131 -106
View File
@@ -542,133 +542,59 @@ class PluginManager(metaclass=Singleton):
""" """
获取所有在线插件信息 获取所有在线插件信息
""" """
def __get_plugin_info(market: str, version: str = None) -> Optional[List[schemas.Plugin]]:
"""
获取插件信息
"""
online_plugins = self.pluginhelper.get_plugins(repo_url=market, version=version) or {}
if not online_plugins:
if not version:
logger.warn(f"获取插件库失败:{market}")
return
ret_plugins = []
add_time = len(online_plugins)
for pid, plugin_info in online_plugins.items():
# 版本兼容性控制
if not version:
if hasattr(settings, 'VERSION_FLAG') \
and not plugin_info.get(settings.VERSION_FLAG):
# 插件当前版本不兼容
continue
# 运行状插件
plugin_obj = self._running_plugins.get(pid)
# 非运行态插件
plugin_static = self._plugins.get(pid)
# 基本属性
plugin = schemas.Plugin()
# ID
plugin.id = pid
# 安装状态
if pid in installed_apps and plugin_static:
plugin.installed = True
else:
plugin.installed = False
# 是否有新版本
plugin.has_update = False
if plugin_static:
installed_version = getattr(plugin_static, "plugin_version")
if StringUtils.compare_version(installed_version, plugin_info.get("version")) < 0:
# 需要更新
plugin.has_update = True
# 运行状态
if plugin_obj and hasattr(plugin_obj, "get_state"):
try:
state = plugin_obj.get_state()
except Exception as e:
logger.error(f"获取插件 {pid} 状态出错:{str(e)}")
state = False
plugin.state = state
else:
plugin.state = False
# 是否有详情页面
plugin.has_page = False
if plugin_obj and hasattr(plugin_obj, "get_page"):
if ObjectUtils.check_method(plugin_obj.get_page):
plugin.has_page = True
# 公钥
if plugin_info.get("key"):
plugin.plugin_public_key = plugin_info.get("key")
# 权限
if not self.__set_and_check_auth_level(plugin=plugin, source=plugin_info):
continue
# 名称
if plugin_info.get("name"):
plugin.plugin_name = plugin_info.get("name")
# 描述
if plugin_info.get("description"):
plugin.plugin_desc = plugin_info.get("description")
# 版本
if plugin_info.get("version"):
plugin.plugin_version = plugin_info.get("version")
# 图标
if plugin_info.get("icon"):
plugin.plugin_icon = plugin_info.get("icon")
# 标签
if plugin_info.get("labels"):
plugin.plugin_label = plugin_info.get("labels")
# 作者
if plugin_info.get("author"):
plugin.plugin_author = plugin_info.get("author")
# 更新历史
if plugin_info.get("history"):
plugin.history = plugin_info.get("history")
# 仓库链接
plugin.repo_url = market
# 本地标志
plugin.is_local = False
# 添加顺序
plugin.add_time = add_time
# 汇总
ret_plugins.append(plugin)
add_time -= 1
return ret_plugins
if not settings.PLUGIN_MARKET: if not settings.PLUGIN_MARKET:
return [] return []
# 返回值 # 返回值
all_plugins = [] all_plugins = []
# 已安装插件 # 用于存储高于 v1 版本的插件(如 v2, v3 等)
installed_apps = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or [] higher_version_plugins = []
# 用于存储 v1 版本插件
base_version_plugins = []
# 使用多线程获取线上插件 # 使用多线程获取线上插件
with concurrent.futures.ThreadPoolExecutor() as executor: with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [] futures_to_version = {}
for m in settings.PLUGIN_MARKET.split(","): for m in settings.PLUGIN_MARKET.split(","):
if not m: if not m:
continue continue
# v1版本插件 # 提交任务获取 v1 版本插件,存储 future 到 version 的映射
futures.append(executor.submit(__get_plugin_info, m, None)) base_future = executor.submit(self.get_plugins_from_market, m, None)
# v2+版本插件 futures_to_version[base_future] = "base_version"
# 提交任务获取高版本插件(如 v2、v3),存储 future 到 version 的映射
if settings.VERSION_FLAG: if settings.VERSION_FLAG:
futures.append(executor.submit(__get_plugin_info, m, settings.VERSION_FLAG)) higher_version_future = executor.submit(self.get_plugins_from_market, m, settings.VERSION_FLAG)
for future in concurrent.futures.as_completed(futures): futures_to_version[higher_version_future] = "higher_version"
# 按照完成顺序处理结果
for future in concurrent.futures.as_completed(futures_to_version):
plugins = future.result() plugins = future.result()
version = futures_to_version[future]
if plugins: if plugins:
all_plugins.extend(plugins) if version == "higher_version":
higher_version_plugins.extend(plugins) # 收集高版本插件
else:
base_version_plugins.extend(plugins) # 收集 v1 版本插件
# 优先处理高版本插件
all_plugins.extend(higher_version_plugins)
# 将未出现在高版本插件列表中的 v1 插件加入 all_plugins
higher_plugin_ids = {f"{p.id}{p.plugin_version}" for p in higher_version_plugins}
all_plugins.extend([p for p in base_version_plugins if f"{p.id}{p.plugin_version}" not in higher_plugin_ids])
# 去重 # 去重
all_plugins = list({f"{p.id}{p.plugin_version}": p for p in all_plugins}.values()) all_plugins = list({f"{p.id}{p.plugin_version}": p for p in all_plugins}.values())
# 所有插件按repo在设置中的顺序排序 # 所有插件按 repo 在设置中的顺序排序
all_plugins.sort( all_plugins.sort(
key=lambda x: settings.PLUGIN_MARKET.split(",").index(x.repo_url) if x.repo_url else 0 key=lambda x: settings.PLUGIN_MARKET.split(",").index(x.repo_url) if x.repo_url else 0
) )
# 相同ID的插件保留版本号最大版本 # 相同 ID 的插件保留版本号最大版本
max_versions = {} max_versions = {}
for p in all_plugins: for p in all_plugins:
if p.id not in max_versions or StringUtils.compare_version(p.plugin_version, max_versions[p.id]) > 0: if p.id not in max_versions or StringUtils.compare_version(p.plugin_version, max_versions[p.id]) > 0:
max_versions[p.id] = p.plugin_version max_versions[p.id] = p.plugin_version
result = [p for p in all_plugins if result = [p for p in all_plugins if p.plugin_version == max_versions[p.id]]
p.plugin_version == max_versions[p.id]]
logger.info(f"共获取到 {len(result)} 个线上插件") logger.info(f"共获取到 {len(result)} 个线上插件")
return result return result
@@ -764,6 +690,105 @@ class PluginManager(metaclass=Singleton):
logger.debug(f"获取插件是否在本地包中存在失败,{e}") logger.debug(f"获取插件是否在本地包中存在失败,{e}")
return False return False
def get_plugins_from_market(self, market: str, package_version: str = None) -> Optional[List[schemas.Plugin]]:
"""
从指定的市场获取插件信息
:param market: 市场的 URL 或标识
:param package_version: 首选插件版本 (如 "v2", "v3"),如果不指定则获取 v1 版本
:return: 返回插件的列表,若获取失败返回 []
"""
if not market:
return []
# 已安装插件
installed_apps = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or []
# 获取在线插件
online_plugins = self.pluginhelper.get_plugins(repo_url=market, package_version=package_version) or {}
if not online_plugins:
if not package_version:
logger.warning(f"获取插件库失败:{market},请检查 GitHub 网络连接")
return []
ret_plugins = []
add_time = len(online_plugins)
for pid, plugin_info in online_plugins.items():
# 如 package_version 为空,则需要判断插件是否兼容当前版本
if not package_version:
if plugin_info.get(settings.VERSION_FLAG) is not True:
# 插件当前版本不兼容
continue
# 运行状插件
plugin_obj = self._running_plugins.get(pid)
# 非运行态插件
plugin_static = self._plugins.get(pid)
# 基本属性
plugin = schemas.Plugin()
# ID
plugin.id = pid
# 安装状态
if pid in installed_apps and plugin_static:
plugin.installed = True
else:
plugin.installed = False
# 是否有新版本
plugin.has_update = False
if plugin_static:
installed_version = getattr(plugin_static, "plugin_version")
if StringUtils.compare_version(installed_version, plugin_info.get("version")) < 0:
# 需要更新
plugin.has_update = True
# 运行状态
if plugin_obj and hasattr(plugin_obj, "get_state"):
try:
state = plugin_obj.get_state()
except Exception as e:
logger.error(f"获取插件 {pid} 状态出错:{str(e)}")
state = False
plugin.state = state
else:
plugin.state = False
# 是否有详情页面
plugin.has_page = False
if plugin_obj and hasattr(plugin_obj, "get_page"):
if ObjectUtils.check_method(plugin_obj.get_page):
plugin.has_page = True
# 公钥
if plugin_info.get("key"):
plugin.plugin_public_key = plugin_info.get("key")
# 权限
if not self.__set_and_check_auth_level(plugin=plugin, source=plugin_info):
continue
# 名称
if plugin_info.get("name"):
plugin.plugin_name = plugin_info.get("name")
# 描述
if plugin_info.get("description"):
plugin.plugin_desc = plugin_info.get("description")
# 版本
if plugin_info.get("version"):
plugin.plugin_version = plugin_info.get("version")
# 图标
if plugin_info.get("icon"):
plugin.plugin_icon = plugin_info.get("icon")
# 标签
if plugin_info.get("labels"):
plugin.plugin_label = plugin_info.get("labels")
# 作者
if plugin_info.get("author"):
plugin.plugin_author = plugin_info.get("author")
# 更新历史
if plugin_info.get("history"):
plugin.history = plugin_info.get("history")
# 仓库链接
plugin.repo_url = market
# 本地标志
plugin.is_local = False
# 添加顺序
plugin.add_time = add_time
# 汇总
ret_plugins.append(plugin)
add_time -= 1
return ret_plugins
def __set_and_check_auth_level(self, plugin: Union[schemas.Plugin, Type[Any]], def __set_and_check_auth_level(self, plugin: Union[schemas.Plugin, Type[Any]],
source: Optional[Union[dict, Type[Any]]] = None) -> bool: source: Optional[Union[dict, Type[Any]]] = None) -> bool:
""" """
+104 -33
View File
@@ -34,11 +34,11 @@ class PluginHelper(metaclass=Singleton):
self.systemconfig.set(SystemConfigKey.PluginInstallReport, "1") self.systemconfig.set(SystemConfigKey.PluginInstallReport, "1")
@cached(cache=TTLCache(maxsize=1000, ttl=1800)) @cached(cache=TTLCache(maxsize=1000, ttl=1800))
def get_plugins(self, repo_url: str, version: str = None) -> Dict[str, dict]: def get_plugins(self, repo_url: str, package_version: str = None) -> Dict[str, dict]:
""" """
获取Github所有最新插件列表 获取Github所有最新插件列表
:param repo_url: Github仓库地址 :param repo_url: Github仓库地址
:param version: 版本 :param package_version: 首选插件版本 (如 "v2", "v3"),如果不指定则获取 v1 版本
""" """
if not repo_url: if not repo_url:
return {} return {}
@@ -48,7 +48,7 @@ class PluginHelper(metaclass=Singleton):
return {} return {}
raw_url = self._base_url.format(user=user, repo=repo) raw_url = self._base_url.format(user=user, repo=repo)
package_url = f"{raw_url}package.{version}.json" if version else f"{raw_url}package.json" package_url = f"{raw_url}package.{package_version}.json" if package_version else f"{raw_url}package.json"
res = self.__request_with_fallback(package_url, headers=settings.REPO_GITHUB_HEADERS(repo=f"{user}/{repo}")) res = self.__request_with_fallback(package_url, headers=settings.REPO_GITHUB_HEADERS(repo=f"{user}/{repo}"))
if res: if res:
@@ -58,6 +58,38 @@ class PluginHelper(metaclass=Singleton):
logger.error(f"插件包数据解析失败:{res.text}") logger.error(f"插件包数据解析失败:{res.text}")
return {} return {}
def get_plugin_package_version(self, pid: str, repo_url: str, package_version: str = None) -> Optional[str]:
"""
检查并获取指定插件的可用版本,支持多版本优先级加载和版本兼容性检测
1. 如果未指定版本,则使用系统配置的默认版本(通过 settings.VERSION_FLAG 设置)
2. 优先检查指定版本的插件(如 `package.v2.json`
3. 如果插件不存在于指定版本,检查 `package.json` 文件,查看该插件是否兼容指定版本
4. 如果插件不存在或不兼容指定版本,返回 `None`
:param pid: 插件 ID,用于在插件列表中查找
:param repo_url: 插件仓库的 URL,指定用于获取插件信息的 GitHub 仓库地址
:param package_version: 首选插件版本 (如 "v2", "v3"),如不指定则默认使用系统配置的版本
:return: 返回可用的插件版本号 (如 "v2",如果指定版本不可用则返回空字符串表示 v1),如果插件不可用则返回 None
"""
# 如果没有指定版本,则使用当前系统配置的版本(如 "v2"
if not package_version:
package_version = settings.VERSION_FLAG
# 优先检查指定版本的插件,即 package.v(x).json 文件中是否存在该插件,如果存在,返回该版本号
plugins = self.get_plugins(repo_url, package_version)
if pid in plugins:
return package_version
# 如果指定版本的插件不存在,检查全局 package.json 文件,查看插件是否兼容指定的版本
global_plugins = self.get_plugins(repo_url)
plugin = global_plugins.get(pid, None)
# 检查插件是否明确支持当前指定的版本(如 v2 或 v3),如果支持,返回空字符串表示使用 package.jsonv1
if plugin and plugin.get(package_version) is True:
return ""
# 如果所有版本都不存在或插件不兼容,返回 None,表示插件不可用
return None
@staticmethod @staticmethod
def get_repo_info(repo_url: str) -> Tuple[Optional[str], Optional[str]]: def get_repo_info(repo_url: str) -> Tuple[Optional[str], Optional[str]]:
""" """
@@ -116,16 +148,18 @@ class PluginHelper(metaclass=Singleton):
json={"plugins": [{"plugin_id": plugin} for plugin in plugins]}) json={"plugins": [{"plugin_id": plugin} for plugin in plugins]})
return True if res else False return True if res else False
def install(self, pid: str, repo_url: str) -> Tuple[bool, str]: def install(self, pid: str, repo_url: str, package_version: str = None) -> Tuple[bool, str]:
""" """
安装插件,包括依赖安装和文件下载,相关资源支持自动降级策略 安装插件,包括依赖安装和文件下载,相关资源支持自动降级策略
1. 从 GitHub 获取文件列表(包括 requirements.txt 1. 检查并获取插件的指定版本,确认版本兼容性。
2. 删除旧的插件目录 2. 从 GitHub 获取文件列表(包括 requirements.txt
3. 下载并预安装 requirements.txt 中的依赖(如果存在) 3. 删除旧的插件目录
4. 下载并安装插件的其他文件 4. 下载并安装 requirements.txt 中的依赖(如果存在)
5. 再次尝试安装依赖(确保安装完整) 5. 下载并安装插件的其他文件
6. 再次尝试安装依赖(确保安装完整)
:param pid: 插件 ID :param pid: 插件 ID
:param repo_url: 插件仓库地址 :param repo_url: 插件仓库地址
:param package_version: 首选插件版本 (如 "v2", "v3"),如不指定则默认使用系统配置的版本
:return: (是否成功, 错误信息) :return: (是否成功, 错误信息)
""" """
if SystemUtils.is_frozen(): if SystemUtils.is_frozen():
@@ -142,15 +176,31 @@ class PluginHelper(metaclass=Singleton):
user_repo = f"{user}/{repo}" user_repo = f"{user}/{repo}"
# 1. 获取插件文件列表(包括 requirements.txt if not package_version:
file_list, msg = self.__get_file_list(pid.lower(), user_repo) package_version = settings.VERSION_FLAG
# 1. 优先检查指定版本的插件
package_version = self.get_plugin_package_version(pid, repo_url, package_version)
# 如果 package_version 为None,说明没有找到匹配的插件
if package_version is None:
msg = f"{pid} 没有找到适用于当前版本的插件"
logger.debug(msg)
return False, msg
# package_version 为空,表示从 package.json 中找到插件
elif package_version == "":
logger.debug(f"{pid} 从 package.json 中找到适用于当前版本的插件")
else:
logger.debug(f"{pid} 从 package.{package_version}.json 中找到适用于当前版本的插件")
# 2. 获取插件文件列表(包括 requirements.txt
file_list, msg = self.__get_file_list(pid.lower(), user_repo, package_version)
if not file_list: if not file_list:
return False, msg return False, msg
# 2. 删除旧的插件目录 # 3. 删除旧的插件目录
self.__remove_old_plugin(pid.lower()) self.__remove_old_plugin(pid.lower())
# 3. 查找并安装 requirements.txt 中的依赖,确保插件环境的依赖尽可能完整。依赖安装可能失败且不影响插件安装,目前只记录日志 # 4. 查找并安装 requirements.txt 中的依赖,确保插件环境的依赖尽可能完整。依赖安装可能失败且不影响插件安装,目前只记录日志
requirements_file_info = next((f for f in file_list if f.get("name") == "requirements.txt"), None) requirements_file_info = next((f for f in file_list if f.get("name") == "requirements.txt"), None)
if requirements_file_info: if requirements_file_info:
logger.debug(f"{pid} 发现 requirements.txt,提前下载并预安装依赖") logger.debug(f"{pid} 发现 requirements.txt,提前下载并预安装依赖")
@@ -161,34 +211,41 @@ class PluginHelper(metaclass=Singleton):
else: else:
logger.debug(f"{pid} 依赖预安装成功") logger.debug(f"{pid} 依赖预安装成功")
# 4. 下载插件的其他文件 # 5. 下载插件的其他文件
logger.info(f"{pid} 准备开始下载插件文件") logger.info(f"{pid} 准备开始下载插件文件")
success, message = self.__download_files(pid.lower(), file_list, user_repo, True) success, message = self.__download_files(pid.lower(), file_list, user_repo, package_version, True)
if not success: if not success:
logger.error(f"{pid} 下载插件文件失败:{message}") logger.error(f"{pid} 下载插件文件失败:{message}")
return False, message return False, message
else: else:
logger.info(f"{pid} 下载插件文件成功") logger.info(f"{pid} 下载插件文件成功")
# 5. 插件文件安装成功后,再次尝试安装依赖,避免因为遗漏依赖导致的插件运行问题,目前依旧只记录日志 # 6. 插件文件安装成功后,再次尝试安装依赖,避免因为遗漏依赖导致的插件运行问题,目前依旧只记录日志
success, message = self.__install_dependencies_if_required(pid) dependencies_exist, success, message = self.__install_dependencies_if_required(pid)
if not success: if dependencies_exist:
logger.error(f"{pid} 依赖安装失败:{message}") if not success:
else: logger.error(f"{pid} 依赖安装失败:{message}")
logger.info(f"{pid} 依赖安装成功") else:
logger.info(f"{pid} 依赖安装成功")
# 插件安装成功后,统计安装信息 # 插件安装成功后,统计安装信息
self.install_reg(pid) self.install_reg(pid)
return True, "" return True, ""
def __get_file_list(self, pid: str, user_repo: str) -> Tuple[Optional[list], Optional[str]]: def __get_file_list(self, pid: str, user_repo: str, package_version: str = None) -> \
Tuple[Optional[list], Optional[str]]:
""" """
获取插件的文件列表 获取插件的文件列表
:param pid: 插件 ID :param pid: 插件 ID
:param user_repo: GitHub 仓库的 user/repo 路径 :param user_repo: GitHub 仓库的 user/repo 路径
:return: (文件列表, 错误信息) :return: (文件列表, 错误信息)
""" """
file_api = f"https://api.github.com/repos/{user_repo}/contents/plugins/{pid}" file_api = f"https://api.github.com/repos/{user_repo}/contents/plugins"
# 如果 package_version 存在(如 "v2"),则加上版本号
if package_version:
file_api += f".{package_version}"
file_api += f"/{pid}"
res = self.__request_with_fallback(file_api, res = self.__request_with_fallback(file_api,
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo), headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
is_api=True, is_api=True,
@@ -209,8 +266,8 @@ class PluginHelper(metaclass=Singleton):
logger.error(f"插件数据解析失败:{res.text}{e}") logger.error(f"插件数据解析失败:{res.text}{e}")
return None, "插件数据解析失败" return None, "插件数据解析失败"
def __download_files(self, pid: str, file_list: List[dict], user_repo: str, skip_requirements: bool = False) \ def __download_files(self, pid: str, file_list: List[dict], user_repo: str,
-> Tuple[bool, str]: package_version: str = None, skip_requirements: bool = False) -> Tuple[bool, str]:
""" """
下载插件文件 下载插件文件
:param pid: 插件 ID :param pid: 插件 ID
@@ -242,15 +299,21 @@ class PluginHelper(metaclass=Singleton):
elif res.status_code != 200: elif res.status_code != 200:
return False, f"下载文件 {item.get('path')} 失败:{res.status_code}" return False, f"下载文件 {item.get('path')} 失败:{res.status_code}"
# 确保文件路径不包含版本号(如 v2、v3),如果有 package_version,移除路径中的版本号
relative_path = item.get("path")
if package_version:
relative_path = relative_path.replace(f"plugins.{package_version}", "plugins", 1)
# 创建插件文件夹并写入文件 # 创建插件文件夹并写入文件
file_path = Path(settings.ROOT_PATH) / "app" / item.get("path") file_path = Path(settings.ROOT_PATH) / "app" / relative_path
file_path.parent.mkdir(parents=True, exist_ok=True) file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
f.write(res.text) f.write(res.text)
logger.debug(f"文件 {item.get('path')} 下载成功,保存路径:{file_path}") logger.debug(f"文件 {item.get('path')} 下载成功,保存路径:{file_path}")
else: else:
# 如果是子目录,则将子目录内容加入栈中继续处理 # 如果是子目录,则将子目录内容加入栈中继续处理
sub_list, msg = self.__get_file_list(f"{current_pid}/{item.get('name')}", user_repo) sub_list, msg = self.__get_file_list(f"{current_pid}/{item.get('name')}", user_repo,
package_version)
if not sub_list: if not sub_list:
return False, msg return False, msg
stack.append((f"{current_pid}/{item.get('name')}", sub_list)) stack.append((f"{current_pid}/{item.get('name')}", sub_list))
@@ -287,18 +350,26 @@ class PluginHelper(metaclass=Singleton):
return True, "" # 如果 requirements.txt 为空,视作成功 return True, "" # 如果 requirements.txt 为空,视作成功
def __install_dependencies_if_required(self, pid: str) -> Tuple[bool, str]: def __install_dependencies_if_required(self, pid: str) -> Tuple[bool, bool, str]:
""" """
安装插件依赖 安装插件依赖
:param pid: 插件 ID :param pid: 插件 ID
:return: (是否成功, 错误信息) :return: (是否存在依赖,安装是否成功, 错误信息)
""" """
# 定位插件目录和依赖文件
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid.lower() plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid.lower()
requirements_file = plugin_dir / "requirements.txt" requirements_file = plugin_dir / "requirements.txt"
# 检查是否存在 requirements.txt 文件
if requirements_file.exists(): if requirements_file.exists():
logger.info(f"{pid} 存在依赖,开始尝试安装依赖") logger.info(f"{pid} 存在依赖,开始尝试安装依赖")
return self.__pip_install_with_fallback(requirements_file) success, error_message = self.__pip_install_with_fallback(requirements_file)
return True, "" if success:
return True, True, ""
else:
return True, False, error_message
return False, False, "不存在依赖"
@staticmethod @staticmethod
def __remove_old_plugin(pid: str): def __remove_old_plugin(pid: str):