mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat(plugin): implement fallback mechanism for install plugin
This commit is contained in:
@@ -280,46 +280,6 @@ class Settings(BaseSettings):
|
|||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def PROXY_URLPARSE(self):
|
|
||||||
"""
|
|
||||||
解析地址组成
|
|
||||||
"""
|
|
||||||
if self.PROXY_HOST:
|
|
||||||
parsed_url = urlparse(self.PROXY_HOST)
|
|
||||||
protocol = parsed_url.scheme or "" # 协议
|
|
||||||
username = parsed_url.username or "" # 用户名
|
|
||||||
password = parsed_url.password or "" # 密码
|
|
||||||
host = parsed_url.hostname or "" # 主机
|
|
||||||
port = parsed_url.port or "" # 端口
|
|
||||||
path = parsed_url.path or "" # 路径
|
|
||||||
netloc = parsed_url.netloc or "" # 用户名:密码@主机:端口
|
|
||||||
query = parsed_url.query or "" # 查询参数: ?key=value
|
|
||||||
params = parsed_url.params or "" # 使用;分割的参数
|
|
||||||
fragment = parsed_url.fragment or "" # 片段: #fragment
|
|
||||||
|
|
||||||
if not port:
|
|
||||||
if protocol == "https":
|
|
||||||
port = 443
|
|
||||||
elif protocol == "http":
|
|
||||||
port = 80
|
|
||||||
elif protocol in {"socks5", "socks5h", "socks4", "socks4a"}:
|
|
||||||
port = 1080
|
|
||||||
|
|
||||||
return {
|
|
||||||
"protocol": protocol,
|
|
||||||
"username": username,
|
|
||||||
"password": password,
|
|
||||||
"host": host,
|
|
||||||
"port": port,
|
|
||||||
"path": path,
|
|
||||||
"netloc": netloc,
|
|
||||||
"query": query,
|
|
||||||
"params": params,
|
|
||||||
"fragment": fragment
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def PROXY_SERVER(self):
|
def PROXY_SERVER(self):
|
||||||
if self.PROXY_HOST:
|
if self.PROXY_HOST:
|
||||||
@@ -338,28 +298,6 @@ class Settings(BaseSettings):
|
|||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@property
|
|
||||||
def PIP_OPTIONS(self):
|
|
||||||
"""
|
|
||||||
pip调用附加参数
|
|
||||||
"""
|
|
||||||
protocol = host = port = ""
|
|
||||||
parsed_url = self.PROXY_URLPARSE
|
|
||||||
if parsed_url:
|
|
||||||
protocol = parsed_url.get("scheme", "").lower()
|
|
||||||
host = parsed_url.get("host", "").lower()
|
|
||||||
port = parsed_url.get("port", "")
|
|
||||||
# 优先级:镜像站 > 全局 > 不代理
|
|
||||||
if settings.PIP_PROXY:
|
|
||||||
PIP_OPTIONS = f" -i {settings.PIP_PROXY} "
|
|
||||||
# 全局代理地址
|
|
||||||
elif protocol in {"http", "https", "socks4", "socks4a", "socks5", "socks5h"} and host and port:
|
|
||||||
PIP_OPTIONS = f" --proxy={settings.PROXY_HOST} "
|
|
||||||
# 不使用代理
|
|
||||||
else:
|
|
||||||
PIP_OPTIONS = ""
|
|
||||||
return PIP_OPTIONS
|
|
||||||
|
|
||||||
def REPO_GITHUB_HEADERS(self, repo: str = None):
|
def REPO_GITHUB_HEADERS(self, repo: str = None):
|
||||||
"""
|
"""
|
||||||
Github指定的仓库请求头
|
Github指定的仓库请求头
|
||||||
|
|||||||
+199
-114
@@ -1,8 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Tuple, Optional, List
|
from typing import Dict, Tuple, Optional, List, Any
|
||||||
|
|
||||||
from cachetools import TTLCache, cached
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ from app.schemas.types import SystemConfigKey
|
|||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
from app.utils.url import UrlUtils
|
||||||
|
|
||||||
|
|
||||||
class PluginHelper(metaclass=Singleton):
|
class PluginHelper(metaclass=Singleton):
|
||||||
@@ -20,12 +22,9 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
插件市场管理,下载安装插件到本地
|
插件市场管理,下载安装插件到本地
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_base_url = f"{settings.GITHUB_PROXY}https://raw.githubusercontent.com/%s/%s/main/"
|
_base_url = "https://raw.githubusercontent.com/{user}/{repo}/main/"
|
||||||
|
_install_reg = f"{settings.MP_SERVER_HOST}/plugin/install/{{pid}}"
|
||||||
_install_reg = f"{settings.MP_SERVER_HOST}/plugin/install/%s"
|
|
||||||
|
|
||||||
_install_report = f"{settings.MP_SERVER_HOST}/plugin/install"
|
_install_report = f"{settings.MP_SERVER_HOST}/plugin/install"
|
||||||
|
|
||||||
_install_statistic = f"{settings.MP_SERVER_HOST}/plugin/statistic"
|
_install_statistic = f"{settings.MP_SERVER_HOST}/plugin/statistic"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -35,10 +34,6 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
if self.install_report():
|
if self.install_report():
|
||||||
self.systemconfig.set(SystemConfigKey.PluginInstallReport, "1")
|
self.systemconfig.set(SystemConfigKey.PluginInstallReport, "1")
|
||||||
|
|
||||||
@property
|
|
||||||
def proxies(self):
|
|
||||||
return None if settings.GITHUB_PROXY else settings.PROXY
|
|
||||||
|
|
||||||
@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, version: str = None) -> Dict[str, dict]:
|
||||||
"""
|
"""
|
||||||
@@ -48,27 +43,26 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
if not repo_url:
|
if not repo_url:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
user, repo = self.get_repo_info(repo_url)
|
user, repo = self.get_repo_info(repo_url)
|
||||||
if not user or not repo:
|
if not user or not repo:
|
||||||
return {}
|
return {}
|
||||||
raw_url = self._base_url % (user, 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.{version}.json" if version else f"{raw_url}package.json"
|
||||||
res = RequestUtils(proxies=self.proxies,
|
|
||||||
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}"))
|
||||||
timeout=10).get_res(package_url)
|
|
||||||
if res:
|
if res:
|
||||||
try:
|
try:
|
||||||
return json.loads(res.text)
|
return json.loads(res.text)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.error(f"插件包数据解析失败:{res.text}")
|
logger.error(f"插件包数据解析失败:{res.text}")
|
||||||
return {}
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@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]]:
|
||||||
"""
|
"""
|
||||||
获取Github仓库信息
|
获取GitHub仓库信息
|
||||||
:param repo_url: Github仓库地址
|
|
||||||
"""
|
"""
|
||||||
if not repo_url:
|
if not repo_url:
|
||||||
return None, None
|
return None, None
|
||||||
@@ -79,7 +73,7 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
try:
|
try:
|
||||||
user, repo = repo_url.split("/")[-4:-2]
|
user, repo = repo_url.split("/")[-4:-2]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"解析Github仓库地址失败:{str(e)} - {traceback.format_exc()}")
|
logger.error(f"解析GitHub仓库地址失败:{str(e)} - {traceback.format_exc()}")
|
||||||
return None, None
|
return None, None
|
||||||
return user, repo
|
return user, repo
|
||||||
|
|
||||||
@@ -103,7 +97,8 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
if not pid:
|
if not pid:
|
||||||
return False
|
return False
|
||||||
res = RequestUtils(timeout=5).get_res(self._install_reg % pid)
|
install_reg_url = self._install_reg.format(pid=pid)
|
||||||
|
res = RequestUtils(timeout=5).get_res(install_reg_url)
|
||||||
if res and res.status_code == 200:
|
if res and res.status_code == 200:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -119,13 +114,7 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
res = RequestUtils(content_type="application/json",
|
res = RequestUtils(content_type="application/json",
|
||||||
timeout=5).post(self._install_report,
|
timeout=5).post(self._install_report,
|
||||||
json={
|
json={"plugins": [{"plugin_id": plugin} for plugin in plugins]})
|
||||||
"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) -> Tuple[bool, str]:
|
||||||
@@ -135,103 +124,199 @@ class PluginHelper(metaclass=Singleton):
|
|||||||
if SystemUtils.is_frozen():
|
if SystemUtils.is_frozen():
|
||||||
return False, "可执行文件模式下,只能安装本地插件"
|
return False, "可执行文件模式下,只能安装本地插件"
|
||||||
|
|
||||||
# 从Github的repo_url获取用户和项目名
|
# 验证参数
|
||||||
|
if not pid or not repo_url:
|
||||||
|
return False, "参数错误"
|
||||||
|
|
||||||
|
# 从GitHub的repo_url获取用户和项目名
|
||||||
user, repo = self.get_repo_info(repo_url)
|
user, repo = self.get_repo_info(repo_url)
|
||||||
if not user or not repo:
|
if not user or not repo:
|
||||||
return False, "不支持的插件仓库地址格式"
|
return False, "不支持的插件仓库地址格式"
|
||||||
|
|
||||||
user_repo = f"{user}/{repo}"
|
user_repo = f"{user}/{repo}"
|
||||||
|
|
||||||
def __get_filelist(_p: str) -> Tuple[Optional[list], Optional[str]]:
|
# 获取插件文件列表
|
||||||
"""
|
file_list, msg = self.__get_file_list(pid.lower(), user_repo)
|
||||||
获取插件的文件列表
|
|
||||||
"""
|
|
||||||
file_api = f"https://api.github.com/repos/{user_repo}/contents/plugins/{_p}"
|
|
||||||
r = RequestUtils(proxies=settings.PROXY,
|
|
||||||
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
|
|
||||||
timeout=30).get_res(file_api)
|
|
||||||
if r is None:
|
|
||||||
return None, "连接仓库失败"
|
|
||||||
elif r.status_code != 200:
|
|
||||||
return None, f"连接仓库失败:{r.status_code} - " \
|
|
||||||
f"{'超出速率限制,请配置GITHUB_TOKEN环境变量或稍后重试' if r.status_code == 403 else r.reason}"
|
|
||||||
ret = r.json()
|
|
||||||
if ret and ret[0].get("message") == "Not Found":
|
|
||||||
return None, "插件在仓库中不存在"
|
|
||||||
return ret, ""
|
|
||||||
|
|
||||||
def __download_files(_p: str, _l: List[dict]) -> Tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
下载插件文件
|
|
||||||
"""
|
|
||||||
if not _l:
|
|
||||||
return False, "文件列表为空"
|
|
||||||
for item in _l:
|
|
||||||
if item.get("download_url"):
|
|
||||||
download_url = f"{settings.GITHUB_PROXY}{item.get('download_url')}"
|
|
||||||
# 下载插件文件
|
|
||||||
res = RequestUtils(proxies=self.proxies,
|
|
||||||
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
|
|
||||||
timeout=60).get_res(download_url)
|
|
||||||
if not res:
|
|
||||||
return False, f"文件 {item.get('name')} 下载失败!"
|
|
||||||
elif res.status_code != 200:
|
|
||||||
return False, f"下载文件 {item.get('name')} 失败:{res.status_code} - " \
|
|
||||||
f"{'超出速率限制,请配置GITHUB_TOKEN环境变量或稍后重试' if res.status_code == 403 else res.reason}"
|
|
||||||
# 创建插件文件夹
|
|
||||||
file_path = Path(settings.ROOT_PATH) / "app" / item.get("path")
|
|
||||||
if not file_path.parent.exists():
|
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(res.text)
|
|
||||||
else:
|
|
||||||
# 递归下载子目录
|
|
||||||
p = f"{_p}/{item.get('name')}"
|
|
||||||
l, m = __get_filelist(p)
|
|
||||||
if not l:
|
|
||||||
return False, m
|
|
||||||
__download_files(p, l)
|
|
||||||
return True, ""
|
|
||||||
|
|
||||||
if not pid or not repo_url:
|
|
||||||
return False, "参数错误"
|
|
||||||
|
|
||||||
# 获取插件的文件列表
|
|
||||||
"""
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "__init__.py",
|
|
||||||
"path": "plugins/autobackup/__init__.py",
|
|
||||||
"sha": "cd10eba3f0355d61adeb35561cb26a0a36c15a6c",
|
|
||||||
"size": 12385,
|
|
||||||
"url": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/contents/plugins/autobackup/__init__.py?ref=main",
|
|
||||||
"html_url": "https://github.com/jxxghp/MoviePilot-Plugins/blob/main/plugins/autobackup/__init__.py",
|
|
||||||
"git_url": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/git/blobs/cd10eba3f0355d61adeb35561cb26a0a36c15a6c",
|
|
||||||
"download_url": "https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/plugins/autobackup/__init__.py",
|
|
||||||
"type": "file",
|
|
||||||
"_links": {
|
|
||||||
"self": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/contents/plugins/autobackup/__init__.py?ref=main",
|
|
||||||
"git": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/git/blobs/cd10eba3f0355d61adeb35561cb26a0a36c15a6c",
|
|
||||||
"html": "https://github.com/jxxghp/MoviePilot-Plugins/blob/main/plugins/autobackup/__init__.py"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
# 获取第一级文件列表
|
|
||||||
file_list, msg = __get_filelist(pid.lower())
|
|
||||||
if not file_list:
|
if not file_list:
|
||||||
return False, msg
|
return False, msg
|
||||||
# 本地存在时先删除
|
|
||||||
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid.lower()
|
# 删除旧的插件目录
|
||||||
if plugin_dir.exists():
|
self.__remove_old_plugin(pid.lower())
|
||||||
shutil.rmtree(plugin_dir, ignore_errors=True)
|
|
||||||
# 下载所有文件
|
# 下载所有插件文件
|
||||||
__download_files(pid.lower(), file_list)
|
download_success, download_msg = self.__download_files(pid.lower(), file_list, user_repo)
|
||||||
|
if not download_success:
|
||||||
|
return False, download_msg
|
||||||
|
|
||||||
# 插件目录下如有requirements.txt则安装依赖
|
# 插件目录下如有requirements.txt则安装依赖
|
||||||
requirements_file = plugin_dir / "requirements.txt"
|
success, message = self.__install_dependencies_if_required(pid.lower())
|
||||||
if requirements_file.exists():
|
if not success:
|
||||||
SystemUtils.execute(f"pip install -r {requirements_file} {settings.PIP_OPTIONS} > /dev/null 2>&1")
|
return False, message
|
||||||
# 安装成功后统计
|
|
||||||
|
# 插件安装成功后,统计安装信息
|
||||||
self.install_reg(pid)
|
self.install_reg(pid)
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def __get_file_list(self, pid: str, user_repo: str) -> Tuple[Optional[list], Optional[str]]:
|
||||||
|
"""
|
||||||
|
获取插件的文件列表
|
||||||
|
"""
|
||||||
|
file_api = f"https://api.github.com/repos/{user_repo}/contents/plugins/{pid}"
|
||||||
|
res = self.__request_with_fallback(file_api,
|
||||||
|
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
|
||||||
|
is_api=True,
|
||||||
|
timeout=30)
|
||||||
|
if res is None:
|
||||||
|
return None, "连接仓库失败"
|
||||||
|
elif res.status_code != 200:
|
||||||
|
return None, f"连接仓库失败:{res.status_code} - " \
|
||||||
|
f"{'超出速率限制,请配置GITHUB_TOKEN环境变量或稍后重试' if res.status_code == 403 else res.reason}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret = res.json()
|
||||||
|
if isinstance(ret, list) and len(ret) > 0 and "message" not in ret[0]:
|
||||||
|
return ret, ""
|
||||||
|
else:
|
||||||
|
return None, "插件在仓库中不存在或返回数据格式不正确"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"插件数据解析失败:{res.text},{e}")
|
||||||
|
return None, "插件数据解析失败"
|
||||||
|
|
||||||
|
def __download_files(self, pid: str, file_list: List[dict], user_repo: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
下载插件文件
|
||||||
|
"""
|
||||||
|
if not file_list:
|
||||||
|
return False, "文件列表为空"
|
||||||
|
|
||||||
|
# 使用栈结构来替代递归调用,避免递归深度过大问题
|
||||||
|
stack = [(pid, file_list)]
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
current_pid, current_file_list = stack.pop()
|
||||||
|
|
||||||
|
for item in current_file_list:
|
||||||
|
if item.get("download_url"):
|
||||||
|
logger.debug(f"正在下载文件:{item.get('path')}")
|
||||||
|
res = self.__request_with_fallback(item.get('download_url'),
|
||||||
|
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo))
|
||||||
|
if not res:
|
||||||
|
return False, f"文件 {item.get('path')} 下载失败!"
|
||||||
|
elif res.status_code != 200:
|
||||||
|
return False, f"下载文件 {item.get('path')} 失败:{res.status_code} - " \
|
||||||
|
f"{'超出速率限制,请配置GITHUB_TOKEN环境变量或稍后重试' if res.status_code == 403 else res.reason}"
|
||||||
|
|
||||||
|
# 创建插件文件夹并写入文件
|
||||||
|
file_path = Path(settings.ROOT_PATH) / "app" / item.get("path")
|
||||||
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(res.text)
|
||||||
|
logger.debug(f"文件 {item.get('path')} 下载成功,保存路径:{file_path}")
|
||||||
|
else:
|
||||||
|
# 将子目录加入栈中以便处理
|
||||||
|
sub_list, msg = self.__get_file_list(f"{current_pid}/{item.get('name')}", user_repo)
|
||||||
|
if not sub_list:
|
||||||
|
return False, msg
|
||||||
|
stack.append((f"{current_pid}/{item.get('name')}", sub_list))
|
||||||
|
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
def __install_dependencies_if_required(self, pid: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
安装插件依赖(如果有requirements.txt)
|
||||||
|
"""
|
||||||
|
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid
|
||||||
|
requirements_file = plugin_dir / "requirements.txt"
|
||||||
|
if requirements_file.exists():
|
||||||
|
return self.__pip_install_with_fallback(requirements_file)
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __remove_old_plugin(pid: str):
|
||||||
|
"""
|
||||||
|
删除旧插件
|
||||||
|
"""
|
||||||
|
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid
|
||||||
|
if plugin_dir.exists():
|
||||||
|
shutil.rmtree(plugin_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __pip_install_with_fallback(requirements_file: Path) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
使用自动降级策略 PIP 安装依赖,优先级依次为镜像站、代理、直连
|
||||||
|
:param requirements_file: 依赖的 requirements.txt 文件路径
|
||||||
|
:return: 依赖安装成功返回 (True, ""),失败返回 (False, 错误信息)
|
||||||
|
"""
|
||||||
|
# 构建三种不同策略下的 PIP 命令
|
||||||
|
pip_commands = [
|
||||||
|
["pip", "install", "-r", str(requirements_file), "-i", settings.PIP_PROXY] if settings.PIP_PROXY else None,
|
||||||
|
# 使用镜像站
|
||||||
|
["pip", "install", "-r", str(requirements_file), "--proxy",
|
||||||
|
settings.PROXY_HOST] if settings.PROXY_HOST else None, # 使用代理
|
||||||
|
["pip", "install", "-r", str(requirements_file)] # 直连
|
||||||
|
]
|
||||||
|
|
||||||
|
# 过滤掉 None 的命令
|
||||||
|
pip_commands = [cmd for cmd in pip_commands if cmd is not None]
|
||||||
|
|
||||||
|
for pip_command in pip_commands:
|
||||||
|
try:
|
||||||
|
logger.info(f"尝试使用PIP安装依赖,命令:{' '.join(pip_command)}")
|
||||||
|
# 使用 subprocess.run 捕获标准输出和标准错误
|
||||||
|
result = subprocess.run(pip_command, check=True, text=True,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
logger.info(f"依赖安装成功,输出:{result.stdout}")
|
||||||
|
return True, result.stdout
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
error_message = f"命令:{' '.join(pip_command)},执行失败,错误信息:{e.stderr.strip()}"
|
||||||
|
logger.error(error_message)
|
||||||
|
return False, error_message
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"未知错误,命令:{' '.join(pip_command)},错误:{str(e)}"
|
||||||
|
logger.error(error_message)
|
||||||
|
return False, error_message
|
||||||
|
|
||||||
|
return False, "所有依赖安装方式均失败,请检查网络连接或 PIP 配置"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __request_with_fallback(url: str,
|
||||||
|
headers: Optional[dict] = None,
|
||||||
|
timeout: int = 60,
|
||||||
|
is_api: bool = False) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
使用自动降级策略请求资源,优先级依次为镜像站、代理、直连
|
||||||
|
:param url: 目标URL
|
||||||
|
:param headers: 请求头信息
|
||||||
|
:param timeout: 请求超时时间
|
||||||
|
:param is_api: 是否为GitHub API请求,API请求不走镜像站
|
||||||
|
:return: 请求成功则返回Response,失败返回None
|
||||||
|
"""
|
||||||
|
# 镜像站一般不支持API请求,因此API请求直接跳过镜像站
|
||||||
|
if not is_api and settings.GITHUB_PROXY:
|
||||||
|
proxy_url = f"{UrlUtils.standardize_base_url(settings.GITHUB_PROXY)}{url}"
|
||||||
|
try:
|
||||||
|
res = RequestUtils(headers=headers, timeout=timeout).get_res(url=proxy_url,
|
||||||
|
raise_exception=True)
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"使用镜像站 {settings.GITHUB_PROXY} 访问 {url} 失败: {str(e)}")
|
||||||
|
|
||||||
|
# 使用代理
|
||||||
|
if settings.PROXY_HOST:
|
||||||
|
proxies = {"http": settings.PROXY_HOST, "https": settings.PROXY_HOST}
|
||||||
|
try:
|
||||||
|
res = RequestUtils(headers=headers, proxies=proxies, timeout=timeout).get_res(url=url,
|
||||||
|
raise_exception=True)
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"使用代理 {settings.PROXY_HOST} 访问 {url} 失败: {str(e)}")
|
||||||
|
|
||||||
|
# 最后尝试直连
|
||||||
|
try:
|
||||||
|
res = RequestUtils(headers=headers, timeout=timeout).get_res(url=url,
|
||||||
|
raise_exception=True)
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"直连访问 {url} 失败: {str(e)}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user