mirror of https://github.com/Mai-with-u/MaiBot.git
添加webui直接下载插件的依赖动态导入
parent
6457df2e61
commit
11ba82d024
|
|
@ -0,0 +1,368 @@
|
||||||
|
import importlib.metadata
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import aiohttp
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
from packaging.utils import canonicalize_name
|
||||||
|
|
||||||
|
from src.common.logger import get_logger
|
||||||
|
from src.plugin_system.base.component_types import PythonDependency
|
||||||
|
|
||||||
|
logger = get_logger("dependency_manager")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DependencyInfo:
|
||||||
|
"""依赖信息
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): 依赖包名称
|
||||||
|
required_version (str): 要求的版本范围
|
||||||
|
installed_version (Optional[str]): 已安装的版本
|
||||||
|
is_satisfied (bool): 是否满足依赖要求
|
||||||
|
install_name (str): 用于pip安装的名称
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
required_version: str
|
||||||
|
installed_version: Optional[str] = None
|
||||||
|
is_satisfied: bool = False
|
||||||
|
install_name: str = "" # 用于pip安装的名称
|
||||||
|
|
||||||
|
class MirrorManager:
|
||||||
|
"""PyPI镜像源管理器"""
|
||||||
|
|
||||||
|
MIRRORS = {
|
||||||
|
"aliyun": "https://mirrors.aliyun.com/pypi/simple",
|
||||||
|
"tsinghua": "https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||||
|
"tencent": "https://mirrors.cloud.tencent.com/pypi/simple",
|
||||||
|
"huawei": "https://repo.huaweicloud.com/repository/pypi/simple",
|
||||||
|
"official": "https://pypi.org/simple"
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._fastest_mirror: Optional[str] = None
|
||||||
|
|
||||||
|
async def get_fastest_mirror(self) -> str:
|
||||||
|
"""获取最快的镜像源
|
||||||
|
|
||||||
|
如果已经测试过,直接返回缓存的结果。
|
||||||
|
否则并发测试所有镜像源的响应速度。
|
||||||
|
"""
|
||||||
|
if self._fastest_mirror:
|
||||||
|
return self._fastest_mirror
|
||||||
|
|
||||||
|
logger.info("正在测试PyPI镜像源速度...")
|
||||||
|
|
||||||
|
async def test_mirror(url: str) -> Tuple[str, float]:
|
||||||
|
try:
|
||||||
|
start = time.time()
|
||||||
|
# 禁用 SSL 验证以避免证书问题干扰测速
|
||||||
|
# trust_env=True 让 aiohttp 读取系统代理设置 (HTTP_PROXY/HTTPS_PROXY),解决 pip 能连但测速连不上的问题
|
||||||
|
async with aiohttp.ClientSession(
|
||||||
|
connector=aiohttp.TCPConnector(ssl=False),
|
||||||
|
trust_env=True
|
||||||
|
) as session:
|
||||||
|
async with session.head(url, timeout=3, allow_redirects=True) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
latency = time.time() - start
|
||||||
|
logger.debug(f"镜像源 {url} 测速成功,延迟: {latency*1000:.2f}ms")
|
||||||
|
return url, latency
|
||||||
|
logger.debug(f"镜像源 {url} 测速失败,状态码: {response.status}")
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.debug(f"镜像源 {url} 连接异常: {repr(e)}")
|
||||||
|
pass
|
||||||
|
return url, float('inf')
|
||||||
|
|
||||||
|
tasks = [test_mirror(url) for _, url in self.MIRRORS.items()]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# 过滤掉超时的inf,按时间排序
|
||||||
|
valid_results = sorted([r for r in results if r[1] != float('inf')], key=lambda x: x[1])
|
||||||
|
|
||||||
|
if valid_results:
|
||||||
|
best_url, latency = valid_results[0]
|
||||||
|
logger.info(f"选用最快镜像源: {best_url} (延迟: {latency*1000:.2f}ms)")
|
||||||
|
self._fastest_mirror = best_url
|
||||||
|
return best_url
|
||||||
|
|
||||||
|
# 默认回退到阿里云
|
||||||
|
logger.warning("所有镜像源测速失败,默认使用阿里云源")
|
||||||
|
return self.MIRRORS["aliyun"]
|
||||||
|
|
||||||
|
class PluginDependencyManager:
|
||||||
|
"""插件依赖管理器
|
||||||
|
|
||||||
|
负责管理插件的Python依赖,包括扫描已安装包、检查依赖状态以及自动安装缺失依赖。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._installed_packages: Dict[str, str] = {}
|
||||||
|
self.mirror_manager = MirrorManager()
|
||||||
|
self.uv_path = shutil.which("uv")
|
||||||
|
if self.uv_path:
|
||||||
|
logger.info(f"检测到 uv 工具: {self.uv_path}")
|
||||||
|
else:
|
||||||
|
logger.info("未检测到 uv 工具,将使用 pip")
|
||||||
|
self.scan_installed_packages()
|
||||||
|
|
||||||
|
def scan_installed_packages(self) -> Dict[str, str]:
|
||||||
|
"""扫描已安装的所有Python包。
|
||||||
|
|
||||||
|
使用 importlib.metadata.distributions() 获取所有已安装的包,
|
||||||
|
并将包名规范化以便后续匹配。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, str]: 包含 {规范化包名: 版本号} 的字典。
|
||||||
|
"""
|
||||||
|
self._installed_packages = {}
|
||||||
|
try:
|
||||||
|
for dist in importlib.metadata.distributions():
|
||||||
|
# 使用 packaging.utils.canonicalize_name 规范化包名
|
||||||
|
name = canonicalize_name(dist.metadata['Name'])
|
||||||
|
version = dist.version
|
||||||
|
self._installed_packages[name] = version
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.error(f"扫描已安装包失败: {e}")
|
||||||
|
return self._installed_packages
|
||||||
|
|
||||||
|
def parse_plugin_dependencies(
|
||||||
|
self,
|
||||||
|
python_dependencies: List[PythonDependency]
|
||||||
|
) -> List[DependencyInfo]:
|
||||||
|
"""解析插件配置中的依赖信息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
python_dependencies: 插件定义的Python依赖列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[DependencyInfo]: 解析后的依赖详细信息列表。
|
||||||
|
"""
|
||||||
|
dependencies = []
|
||||||
|
for dep in python_dependencies:
|
||||||
|
# 使用 install_name 进行查找,因为它是 PyPI 包名
|
||||||
|
# 并使用 canonicalize_name 规范化
|
||||||
|
target_name = dep.install_name or dep.package_name
|
||||||
|
pkg_name_canonical = canonicalize_name(target_name)
|
||||||
|
|
||||||
|
installed_version = self._installed_packages.get(pkg_name_canonical)
|
||||||
|
|
||||||
|
# 构建版本要求字符串
|
||||||
|
specifier_str = dep.version if dep.version else ""
|
||||||
|
|
||||||
|
is_satisfied = False
|
||||||
|
if installed_version:
|
||||||
|
if not specifier_str:
|
||||||
|
is_satisfied = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
spec = SpecifierSet(specifier_str)
|
||||||
|
is_satisfied = spec.contains(installed_version)
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.warning(f"版本说明符解析失败 {dep.package_name} {specifier_str}: {e}")
|
||||||
|
# 如果解析失败,保守起见认为不满足,或者根据策略处理
|
||||||
|
is_satisfied = False
|
||||||
|
|
||||||
|
dependencies.append(DependencyInfo(
|
||||||
|
name=dep.package_name,
|
||||||
|
required_version=specifier_str,
|
||||||
|
installed_version=installed_version,
|
||||||
|
is_satisfied=is_satisfied,
|
||||||
|
install_name=target_name
|
||||||
|
))
|
||||||
|
return dependencies
|
||||||
|
|
||||||
|
def check_dependencies(
|
||||||
|
self,
|
||||||
|
python_dependencies: List[PythonDependency]
|
||||||
|
) -> Tuple[List[DependencyInfo], List[DependencyInfo]]:
|
||||||
|
"""检查插件依赖是否满足。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
python_dependencies: 插件定义的Python依赖列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[DependencyInfo], List[DependencyInfo]]:
|
||||||
|
返回一个元组 (satisfied, unsatisfied),
|
||||||
|
分别包含满足和不满足的依赖信息列表。
|
||||||
|
"""
|
||||||
|
dependencies = self.parse_plugin_dependencies(python_dependencies)
|
||||||
|
satisfied = []
|
||||||
|
unsatisfied = []
|
||||||
|
|
||||||
|
for dep in dependencies:
|
||||||
|
if dep.is_satisfied:
|
||||||
|
satisfied.append(dep)
|
||||||
|
else:
|
||||||
|
unsatisfied.append(dep)
|
||||||
|
|
||||||
|
return satisfied, unsatisfied
|
||||||
|
|
||||||
|
async def _get_install_command(self, args: List[str], upgrade: bool = False) -> List[str]:
|
||||||
|
"""构建安装命令,自动选择 uv 或 pip,并添加镜像源"""
|
||||||
|
mirror_url = await self.mirror_manager.get_fastest_mirror()
|
||||||
|
|
||||||
|
cmd = []
|
||||||
|
if self.uv_path:
|
||||||
|
# uv pip install ...
|
||||||
|
cmd = [self.uv_path, "pip", "install"]
|
||||||
|
else:
|
||||||
|
# python -m pip install ...
|
||||||
|
cmd = [sys.executable, "-m", "pip", "install"]
|
||||||
|
# 添加 pip 的安全/静默参数
|
||||||
|
cmd.extend(["--disable-pip-version-check", "--no-input"])
|
||||||
|
|
||||||
|
if upgrade:
|
||||||
|
cmd.append("--upgrade")
|
||||||
|
|
||||||
|
cmd.extend(["-i", mirror_url])
|
||||||
|
|
||||||
|
# 自动添加 trusted-host 参数,解决部分镜像源 SSL 问题
|
||||||
|
if mirror_url:
|
||||||
|
try:
|
||||||
|
host = urllib.parse.urlparse(mirror_url).hostname
|
||||||
|
if host:
|
||||||
|
cmd.extend(["--trusted-host", host])
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
pass
|
||||||
|
|
||||||
|
cmd.extend(args)
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
def _parse_pip_error(self, output: str) -> str:
|
||||||
|
"""解析并简化 pip 错误信息"""
|
||||||
|
patterns = {
|
||||||
|
"No matching distribution": "包不存在或版本不可用",
|
||||||
|
"Could not find a version": "找不到指定版本",
|
||||||
|
"SSL: CERTIFICATE_VERIFY_FAILED": "SSL 验证失败,尝试检查网络或镜像源",
|
||||||
|
"403": "访问被拒绝(镜像源可能需要认证)",
|
||||||
|
"404": "资源不存在",
|
||||||
|
"Connection refused": "连接被拒绝,检查镜像源地址",
|
||||||
|
"Network is unreachable": "网络不可达",
|
||||||
|
"Permission denied": "权限不足,请尝试使用管理员权限运行",
|
||||||
|
}
|
||||||
|
for k, v in patterns.items():
|
||||||
|
if k in output:
|
||||||
|
return f"{v} (原始错误: {k})"
|
||||||
|
|
||||||
|
# 如果没有匹配到已知模式,返回截断的错误信息
|
||||||
|
return output[:300] + "..." if len(output) > 300 else output
|
||||||
|
|
||||||
|
async def install_from_file(self, file_path: str) -> bool:
|
||||||
|
"""从 requirements.txt 或 pyproject.toml 安装依赖
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 依赖文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 安装是否成功
|
||||||
|
"""
|
||||||
|
if not file_path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"正在从文件安装依赖: {file_path}")
|
||||||
|
|
||||||
|
# 构建参数
|
||||||
|
args = ["-r", file_path]
|
||||||
|
|
||||||
|
# 如果是 pyproject.toml 且使用 pip,可能需要不同的处理
|
||||||
|
# 但 uv pip install -r pyproject.toml 是支持的
|
||||||
|
# 对于 pyproject.toml,通常是 pip install .
|
||||||
|
# 但这里我们假设用户希望安装依赖,而不是安装包本身
|
||||||
|
# 如果是 pyproject.toml,尝试直接作为 requirements 传入
|
||||||
|
|
||||||
|
cmd = await self._get_install_command(args, upgrade=True)
|
||||||
|
|
||||||
|
return await self._run_install_command(cmd)
|
||||||
|
|
||||||
|
async def install_auto_from_directory(self, plugin_dir: str) -> bool:
|
||||||
|
"""自动检测并安装插件目录下的依赖文件
|
||||||
|
优先检查 pyproject.toml,其次检查 requirements.txt
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_dir: 插件目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果找到文件且安装成功返回 True,未找到文件返回 True (视为成功),安装失败返回 False
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
dependency_file = None
|
||||||
|
pyproject_path = os.path.join(plugin_dir, "pyproject.toml")
|
||||||
|
requirements_path = os.path.join(plugin_dir, "requirements.txt")
|
||||||
|
|
||||||
|
if os.path.exists(pyproject_path):
|
||||||
|
dependency_file = pyproject_path
|
||||||
|
elif os.path.exists(requirements_path):
|
||||||
|
dependency_file = requirements_path
|
||||||
|
|
||||||
|
if not dependency_file:
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info(f"在 {plugin_dir} 检测到依赖文件: {dependency_file}")
|
||||||
|
return await self.install_from_file(dependency_file)
|
||||||
|
|
||||||
|
async def install_dependencies(
|
||||||
|
self,
|
||||||
|
dependencies: List[DependencyInfo],
|
||||||
|
*,
|
||||||
|
upgrade: bool = False
|
||||||
|
) -> bool:
|
||||||
|
"""安装缺失或版本不匹配的依赖。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dependencies: 需要安装的依赖信息列表。
|
||||||
|
upgrade: 是否使用 --upgrade 参数升级包。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 安装是否成功。
|
||||||
|
"""
|
||||||
|
if not dependencies:
|
||||||
|
return True
|
||||||
|
|
||||||
|
packages_to_install = []
|
||||||
|
for dep in dependencies:
|
||||||
|
pkg_str = dep.install_name
|
||||||
|
if dep.required_version:
|
||||||
|
if any(op in dep.required_version for op in ['=', '>', '<', '~', '!']):
|
||||||
|
pkg_str += dep.required_version
|
||||||
|
else:
|
||||||
|
pkg_str += f"=={dep.required_version}"
|
||||||
|
packages_to_install.append(pkg_str)
|
||||||
|
|
||||||
|
cmd = await self._get_install_command(packages_to_install, upgrade=upgrade)
|
||||||
|
|
||||||
|
logger.info(f"正在自动安装依赖: {' '.join(packages_to_install)}")
|
||||||
|
return await self._run_install_command(cmd)
|
||||||
|
|
||||||
|
async def _run_install_command(self, cmd: List[str]) -> bool:
|
||||||
|
"""执行安装命令"""
|
||||||
|
try:
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
_, stderr = await process.communicate()
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
logger.info("依赖安装成功")
|
||||||
|
# 安装成功后重新扫描已安装包
|
||||||
|
self.scan_installed_packages()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
error_msg = stderr.decode()
|
||||||
|
friendly_error = self._parse_pip_error(error_msg)
|
||||||
|
logger.error(f"依赖安装失败: {friendly_error}")
|
||||||
|
logger.debug(f"完整错误日志: {error_msg}")
|
||||||
|
return False
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.error(f"执行安装命令失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 全局依赖管理器实例
|
||||||
|
plugin_dependency_manager = PluginDependencyManager()
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Tuple, Type, Any
|
from typing import Dict, List, Optional, Tuple, Type, Any
|
||||||
from importlib.util import spec_from_file_location, module_from_spec
|
from importlib.util import spec_from_file_location, module_from_spec
|
||||||
|
|
@ -11,6 +12,7 @@ from src.plugin_system.base.plugin_base import PluginBase
|
||||||
from src.plugin_system.base.component_types import ComponentType
|
from src.plugin_system.base.component_types import ComponentType
|
||||||
from src.plugin_system.utils.manifest_utils import VersionComparator
|
from src.plugin_system.utils.manifest_utils import VersionComparator
|
||||||
from .component_registry import component_registry
|
from .component_registry import component_registry
|
||||||
|
from .dependency_manager import plugin_dependency_manager
|
||||||
|
|
||||||
logger = get_logger("plugin_manager")
|
logger = get_logger("plugin_manager")
|
||||||
|
|
||||||
|
|
@ -73,7 +75,13 @@ class PluginManager:
|
||||||
total_registered = 0
|
total_registered = 0
|
||||||
total_failed_registration = 0
|
total_failed_registration = 0
|
||||||
|
|
||||||
for plugin_name in self.plugin_classes.keys():
|
# 获取所有已注册的插件名称
|
||||||
|
all_plugin_names = list(self.plugin_classes.keys())
|
||||||
|
|
||||||
|
# 对插件进行拓扑排序
|
||||||
|
sorted_plugin_names = self._sort_plugins_by_dependency(all_plugin_names)
|
||||||
|
|
||||||
|
for plugin_name in sorted_plugin_names:
|
||||||
load_status, count = self.load_registered_plugin_classes(plugin_name)
|
load_status, count = self.load_registered_plugin_classes(plugin_name)
|
||||||
if load_status:
|
if load_status:
|
||||||
total_registered += 1
|
total_registered += 1
|
||||||
|
|
@ -84,6 +92,60 @@ class PluginManager:
|
||||||
|
|
||||||
return total_registered, total_failed_registration
|
return total_registered, total_failed_registration
|
||||||
|
|
||||||
|
def _sort_plugins_by_dependency(self, plugin_names: List[str]) -> List[str]:
|
||||||
|
"""对插件进行拓扑排序,确保依赖加载顺序
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_names: 待排序的插件名称列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: 排序后的插件名称列表,依赖项排在前面。
|
||||||
|
"""
|
||||||
|
# 预实例化所有插件以获取依赖信息
|
||||||
|
temp_instances = {}
|
||||||
|
for name in plugin_names:
|
||||||
|
cls = self.plugin_classes.get(name)
|
||||||
|
path = self.plugin_paths.get(name)
|
||||||
|
if cls and path:
|
||||||
|
try:
|
||||||
|
# 实例化插件以获取依赖信息,但不注册
|
||||||
|
instance = cls(plugin_dir=path)
|
||||||
|
temp_instances[name] = instance
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.warning(f"预加载插件 {name} 以获取依赖信息失败: {e}")
|
||||||
|
|
||||||
|
# 构建依赖图
|
||||||
|
graph = {name: set() for name in plugin_names}
|
||||||
|
for name, instance in temp_instances.items():
|
||||||
|
for dep in instance.dependencies:
|
||||||
|
if dep in plugin_names:
|
||||||
|
graph[name].add(dep)
|
||||||
|
else:
|
||||||
|
logger.warning(f"插件 {name} 依赖未知的插件: {dep}")
|
||||||
|
|
||||||
|
# 拓扑排序
|
||||||
|
result = []
|
||||||
|
visited = set()
|
||||||
|
temp_mark = set()
|
||||||
|
|
||||||
|
def visit(n):
|
||||||
|
if n in temp_mark:
|
||||||
|
logger.error(f"检测到循环依赖: {n}")
|
||||||
|
return
|
||||||
|
if n not in visited:
|
||||||
|
temp_mark.add(n)
|
||||||
|
for m in graph.get(n, []):
|
||||||
|
visit(m)
|
||||||
|
temp_mark.remove(n)
|
||||||
|
visited.add(n)
|
||||||
|
result.append(n)
|
||||||
|
|
||||||
|
for name in plugin_names:
|
||||||
|
if name not in visited:
|
||||||
|
visit(name)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def load_registered_plugin_classes(self, plugin_name: str) -> Tuple[bool, int]:
|
def load_registered_plugin_classes(self, plugin_name: str) -> Tuple[bool, int]:
|
||||||
# sourcery skip: extract-duplicate-method, extract-method
|
# sourcery skip: extract-duplicate-method, extract-method
|
||||||
"""
|
"""
|
||||||
|
|
@ -110,6 +172,53 @@ class PluginManager:
|
||||||
logger.info(f"插件 {plugin_name} 已禁用,跳过加载")
|
logger.info(f"插件 {plugin_name} 已禁用,跳过加载")
|
||||||
return False, 0
|
return False, 0
|
||||||
|
|
||||||
|
# 优先检查并安装配置文件定义的依赖
|
||||||
|
try:
|
||||||
|
# 检查是否有正在运行的事件循环
|
||||||
|
try:
|
||||||
|
asyncio.get_running_loop()
|
||||||
|
|
||||||
|
pass
|
||||||
|
except RuntimeError:
|
||||||
|
# 没有运行的循环,可以使用 asyncio.run
|
||||||
|
asyncio.run(
|
||||||
|
plugin_dependency_manager.install_auto_from_directory(plugin_dir)
|
||||||
|
)
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.error(f"自动安装依赖文件失败: {e}")
|
||||||
|
|
||||||
|
if plugin_instance.python_dependencies:
|
||||||
|
_, unsatisfied = plugin_dependency_manager.check_dependencies(
|
||||||
|
plugin_instance.python_dependencies)
|
||||||
|
if unsatisfied:
|
||||||
|
logger.warning(f"插件 {plugin_name} 缺少Python依赖:"
|
||||||
|
f" {[d.name for d in unsatisfied]},尝试自动安装...")
|
||||||
|
|
||||||
|
# 尝试自动安装
|
||||||
|
install_success = False
|
||||||
|
try:
|
||||||
|
# 检查是否有正在运行的事件循环
|
||||||
|
try:
|
||||||
|
asyncio.get_running_loop()
|
||||||
|
# 如果已经在循环中(例如 WebUI 调用),我们无法在同步方法中阻塞等待异步安装
|
||||||
|
# 记录警告并跳过自动安装,尝试继续加载(如果真的缺依赖,后续会抛出 ImportError)
|
||||||
|
logger.warning(f"检测到运行中的事件循环,跳过插件 {plugin_name} 的元数据依赖自动安装。如果加载失败,请手动安装: {[d.name for d in unsatisfied]}")
|
||||||
|
install_success = False # 标记为未执行安装,但允许后续流程尝试加载
|
||||||
|
except RuntimeError:
|
||||||
|
# 没有运行的循环,可以使用 asyncio.run
|
||||||
|
install_success = asyncio.run(
|
||||||
|
plugin_dependency_manager.install_dependencies(unsatisfied))
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.error(f"自动安装依赖失败: {e}")
|
||||||
|
install_success = False
|
||||||
|
|
||||||
|
if install_success:
|
||||||
|
logger.info(f"插件 {plugin_name} 依赖安装成功,继续加载")
|
||||||
|
elif unsatisfied:
|
||||||
|
logger.warning(f"插件 {plugin_name} 存在未满足的依赖且未能自动安装,将尝试强制加载...")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 检查版本兼容性
|
# 检查版本兼容性
|
||||||
is_compatible, compatibility_error = self._check_plugin_version_compatibility(
|
is_compatible, compatibility_error = self._check_plugin_version_compatibility(
|
||||||
plugin_name, plugin_instance.manifest_data
|
plugin_name, plugin_instance.manifest_data
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from pathlib import Path
|
||||||
import json
|
import json
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.config.config import MMC_VERSION
|
from src.config.config import MMC_VERSION
|
||||||
|
from src.plugin_system.core.dependency_manager import plugin_dependency_manager
|
||||||
from .git_mirror_service import get_git_mirror_service, set_update_progress_callback
|
from .git_mirror_service import get_git_mirror_service, set_update_progress_callback
|
||||||
from .token_manager import get_token_manager
|
from .token_manager import get_token_manager
|
||||||
from .plugin_progress_ws import update_progress
|
from .plugin_progress_ws import update_progress
|
||||||
|
|
@ -30,12 +31,12 @@ def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||||
(major, minor, patch) 三元组
|
(major, minor, patch) 三元组
|
||||||
"""
|
"""
|
||||||
# 移除 snapshot 等后缀
|
# 移除 snapshot 等后缀
|
||||||
base_version = version_str.split(".snapshot")[0].split(".dev")[0].split(".alpha")[0].split(".beta")[0]
|
base_version = version_str.split('.snapshot')[0].split('.dev')[0].split('.alpha')[0].split('.beta')[0]
|
||||||
|
|
||||||
parts = base_version.split(".")
|
parts = base_version.split('.')
|
||||||
if len(parts) < 3:
|
if len(parts) < 3:
|
||||||
# 补齐到 3 位
|
# 补齐到 3 位
|
||||||
parts.extend(["0"] * (3 - len(parts)))
|
parts.extend(['0'] * (3 - len(parts)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
major = int(parts[0])
|
major = int(parts[0])
|
||||||
|
|
@ -49,10 +50,8 @@ def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||||
|
|
||||||
# ============ 请求/响应模型 ============
|
# ============ 请求/响应模型 ============
|
||||||
|
|
||||||
|
|
||||||
class FetchRawFileRequest(BaseModel):
|
class FetchRawFileRequest(BaseModel):
|
||||||
"""获取 Raw 文件请求"""
|
"""获取 Raw 文件请求"""
|
||||||
|
|
||||||
owner: str = Field(..., description="仓库所有者", example="MaiM-with-u")
|
owner: str = Field(..., description="仓库所有者", example="MaiM-with-u")
|
||||||
repo: str = Field(..., description="仓库名称", example="plugin-repo")
|
repo: str = Field(..., description="仓库名称", example="plugin-repo")
|
||||||
branch: str = Field(..., description="分支名称", example="main")
|
branch: str = Field(..., description="分支名称", example="main")
|
||||||
|
|
@ -63,7 +62,6 @@ class FetchRawFileRequest(BaseModel):
|
||||||
|
|
||||||
class FetchRawFileResponse(BaseModel):
|
class FetchRawFileResponse(BaseModel):
|
||||||
"""获取 Raw 文件响应"""
|
"""获取 Raw 文件响应"""
|
||||||
|
|
||||||
success: bool = Field(..., description="是否成功")
|
success: bool = Field(..., description="是否成功")
|
||||||
data: Optional[str] = Field(None, description="文件内容")
|
data: Optional[str] = Field(None, description="文件内容")
|
||||||
error: Optional[str] = Field(None, description="错误信息")
|
error: Optional[str] = Field(None, description="错误信息")
|
||||||
|
|
@ -74,7 +72,6 @@ class FetchRawFileResponse(BaseModel):
|
||||||
|
|
||||||
class CloneRepositoryRequest(BaseModel):
|
class CloneRepositoryRequest(BaseModel):
|
||||||
"""克隆仓库请求"""
|
"""克隆仓库请求"""
|
||||||
|
|
||||||
owner: str = Field(..., description="仓库所有者", example="MaiM-with-u")
|
owner: str = Field(..., description="仓库所有者", example="MaiM-with-u")
|
||||||
repo: str = Field(..., description="仓库名称", example="plugin-repo")
|
repo: str = Field(..., description="仓库名称", example="plugin-repo")
|
||||||
target_path: str = Field(..., description="目标路径(相对于插件目录)")
|
target_path: str = Field(..., description="目标路径(相对于插件目录)")
|
||||||
|
|
@ -86,7 +83,6 @@ class CloneRepositoryRequest(BaseModel):
|
||||||
|
|
||||||
class CloneRepositoryResponse(BaseModel):
|
class CloneRepositoryResponse(BaseModel):
|
||||||
"""克隆仓库响应"""
|
"""克隆仓库响应"""
|
||||||
|
|
||||||
success: bool = Field(..., description="是否成功")
|
success: bool = Field(..., description="是否成功")
|
||||||
path: Optional[str] = Field(None, description="克隆路径")
|
path: Optional[str] = Field(None, description="克隆路径")
|
||||||
error: Optional[str] = Field(None, description="错误信息")
|
error: Optional[str] = Field(None, description="错误信息")
|
||||||
|
|
@ -98,7 +94,6 @@ class CloneRepositoryResponse(BaseModel):
|
||||||
|
|
||||||
class MirrorConfigResponse(BaseModel):
|
class MirrorConfigResponse(BaseModel):
|
||||||
"""镜像源配置响应"""
|
"""镜像源配置响应"""
|
||||||
|
|
||||||
id: str = Field(..., description="镜像源 ID")
|
id: str = Field(..., description="镜像源 ID")
|
||||||
name: str = Field(..., description="镜像源名称")
|
name: str = Field(..., description="镜像源名称")
|
||||||
raw_prefix: str = Field(..., description="Raw 文件前缀")
|
raw_prefix: str = Field(..., description="Raw 文件前缀")
|
||||||
|
|
@ -109,14 +104,12 @@ class MirrorConfigResponse(BaseModel):
|
||||||
|
|
||||||
class AvailableMirrorsResponse(BaseModel):
|
class AvailableMirrorsResponse(BaseModel):
|
||||||
"""可用镜像源列表响应"""
|
"""可用镜像源列表响应"""
|
||||||
|
|
||||||
mirrors: List[MirrorConfigResponse] = Field(..., description="镜像源列表")
|
mirrors: List[MirrorConfigResponse] = Field(..., description="镜像源列表")
|
||||||
default_priority: List[str] = Field(..., description="默认优先级顺序(ID 列表)")
|
default_priority: List[str] = Field(..., description="默认优先级顺序(ID 列表)")
|
||||||
|
|
||||||
|
|
||||||
class AddMirrorRequest(BaseModel):
|
class AddMirrorRequest(BaseModel):
|
||||||
"""添加镜像源请求"""
|
"""添加镜像源请求"""
|
||||||
|
|
||||||
id: str = Field(..., description="镜像源 ID", example="custom-mirror")
|
id: str = Field(..., description="镜像源 ID", example="custom-mirror")
|
||||||
name: str = Field(..., description="镜像源名称", example="自定义镜像源")
|
name: str = Field(..., description="镜像源名称", example="自定义镜像源")
|
||||||
raw_prefix: str = Field(..., description="Raw 文件前缀", example="https://example.com/raw")
|
raw_prefix: str = Field(..., description="Raw 文件前缀", example="https://example.com/raw")
|
||||||
|
|
@ -127,7 +120,6 @@ class AddMirrorRequest(BaseModel):
|
||||||
|
|
||||||
class UpdateMirrorRequest(BaseModel):
|
class UpdateMirrorRequest(BaseModel):
|
||||||
"""更新镜像源请求"""
|
"""更新镜像源请求"""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="镜像源名称")
|
name: Optional[str] = Field(None, description="镜像源名称")
|
||||||
raw_prefix: Optional[str] = Field(None, description="Raw 文件前缀")
|
raw_prefix: Optional[str] = Field(None, description="Raw 文件前缀")
|
||||||
clone_prefix: Optional[str] = Field(None, description="克隆前缀")
|
clone_prefix: Optional[str] = Field(None, description="克隆前缀")
|
||||||
|
|
@ -137,7 +129,6 @@ class UpdateMirrorRequest(BaseModel):
|
||||||
|
|
||||||
class GitStatusResponse(BaseModel):
|
class GitStatusResponse(BaseModel):
|
||||||
"""Git 安装状态响应"""
|
"""Git 安装状态响应"""
|
||||||
|
|
||||||
installed: bool = Field(..., description="是否已安装 Git")
|
installed: bool = Field(..., description="是否已安装 Git")
|
||||||
version: Optional[str] = Field(None, description="Git 版本号")
|
version: Optional[str] = Field(None, description="Git 版本号")
|
||||||
path: Optional[str] = Field(None, description="Git 可执行文件路径")
|
path: Optional[str] = Field(None, description="Git 可执行文件路径")
|
||||||
|
|
@ -146,7 +137,6 @@ class GitStatusResponse(BaseModel):
|
||||||
|
|
||||||
class InstallPluginRequest(BaseModel):
|
class InstallPluginRequest(BaseModel):
|
||||||
"""安装插件请求"""
|
"""安装插件请求"""
|
||||||
|
|
||||||
plugin_id: str = Field(..., description="插件 ID")
|
plugin_id: str = Field(..., description="插件 ID")
|
||||||
repository_url: str = Field(..., description="插件仓库 URL")
|
repository_url: str = Field(..., description="插件仓库 URL")
|
||||||
branch: Optional[str] = Field("main", description="分支名称")
|
branch: Optional[str] = Field("main", description="分支名称")
|
||||||
|
|
@ -155,7 +145,6 @@ class InstallPluginRequest(BaseModel):
|
||||||
|
|
||||||
class VersionResponse(BaseModel):
|
class VersionResponse(BaseModel):
|
||||||
"""麦麦版本响应"""
|
"""麦麦版本响应"""
|
||||||
|
|
||||||
version: str = Field(..., description="麦麦版本号")
|
version: str = Field(..., description="麦麦版本号")
|
||||||
version_major: int = Field(..., description="主版本号")
|
version_major: int = Field(..., description="主版本号")
|
||||||
version_minor: int = Field(..., description="次版本号")
|
version_minor: int = Field(..., description="次版本号")
|
||||||
|
|
@ -164,13 +153,11 @@ class VersionResponse(BaseModel):
|
||||||
|
|
||||||
class UninstallPluginRequest(BaseModel):
|
class UninstallPluginRequest(BaseModel):
|
||||||
"""卸载插件请求"""
|
"""卸载插件请求"""
|
||||||
|
|
||||||
plugin_id: str = Field(..., description="插件 ID")
|
plugin_id: str = Field(..., description="插件 ID")
|
||||||
|
|
||||||
|
|
||||||
class UpdatePluginRequest(BaseModel):
|
class UpdatePluginRequest(BaseModel):
|
||||||
"""更新插件请求"""
|
"""更新插件请求"""
|
||||||
|
|
||||||
plugin_id: str = Field(..., description="插件 ID")
|
plugin_id: str = Field(..., description="插件 ID")
|
||||||
repository_url: str = Field(..., description="插件仓库 URL")
|
repository_url: str = Field(..., description="插件仓库 URL")
|
||||||
branch: Optional[str] = Field("main", description="分支名称")
|
branch: Optional[str] = Field("main", description="分支名称")
|
||||||
|
|
@ -179,7 +166,6 @@ class UpdatePluginRequest(BaseModel):
|
||||||
|
|
||||||
# ============ API 路由 ============
|
# ============ API 路由 ============
|
||||||
|
|
||||||
|
|
||||||
@router.get("/version", response_model=VersionResponse)
|
@router.get("/version", response_model=VersionResponse)
|
||||||
async def get_maimai_version() -> VersionResponse:
|
async def get_maimai_version() -> VersionResponse:
|
||||||
"""
|
"""
|
||||||
|
|
@ -189,7 +175,12 @@ async def get_maimai_version() -> VersionResponse:
|
||||||
"""
|
"""
|
||||||
major, minor, patch = parse_version(MMC_VERSION)
|
major, minor, patch = parse_version(MMC_VERSION)
|
||||||
|
|
||||||
return VersionResponse(version=MMC_VERSION, version_major=major, version_minor=minor, version_patch=patch)
|
return VersionResponse(
|
||||||
|
version=MMC_VERSION,
|
||||||
|
version_major=major,
|
||||||
|
version_minor=minor,
|
||||||
|
version_patch=patch
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/git-status", response_model=GitStatusResponse)
|
@router.get("/git-status", response_model=GitStatusResponse)
|
||||||
|
|
@ -206,7 +197,9 @@ async def check_git_status() -> GitStatusResponse:
|
||||||
|
|
||||||
|
|
||||||
@router.get("/mirrors", response_model=AvailableMirrorsResponse)
|
@router.get("/mirrors", response_model=AvailableMirrorsResponse)
|
||||||
async def get_available_mirrors(authorization: Optional[str] = Header(None)) -> AvailableMirrorsResponse:
|
async def get_available_mirrors(
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
) -> AvailableMirrorsResponse:
|
||||||
"""
|
"""
|
||||||
获取所有可用的镜像源配置
|
获取所有可用的镜像源配置
|
||||||
"""
|
"""
|
||||||
|
|
@ -227,16 +220,22 @@ async def get_available_mirrors(authorization: Optional[str] = Header(None)) ->
|
||||||
raw_prefix=m["raw_prefix"],
|
raw_prefix=m["raw_prefix"],
|
||||||
clone_prefix=m["clone_prefix"],
|
clone_prefix=m["clone_prefix"],
|
||||||
enabled=m["enabled"],
|
enabled=m["enabled"],
|
||||||
priority=m["priority"],
|
priority=m["priority"]
|
||||||
)
|
)
|
||||||
for m in all_mirrors
|
for m in all_mirrors
|
||||||
]
|
]
|
||||||
|
|
||||||
return AvailableMirrorsResponse(mirrors=mirrors, default_priority=config.get_default_priority_list())
|
return AvailableMirrorsResponse(
|
||||||
|
mirrors=mirrors,
|
||||||
|
default_priority=config.get_default_priority_list()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/mirrors", response_model=MirrorConfigResponse)
|
@router.post("/mirrors", response_model=MirrorConfigResponse)
|
||||||
async def add_mirror(request: AddMirrorRequest, authorization: Optional[str] = Header(None)) -> MirrorConfigResponse:
|
async def add_mirror(
|
||||||
|
request: AddMirrorRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
) -> MirrorConfigResponse:
|
||||||
"""
|
"""
|
||||||
添加新的镜像源
|
添加新的镜像源
|
||||||
"""
|
"""
|
||||||
|
|
@ -256,7 +255,7 @@ async def add_mirror(request: AddMirrorRequest, authorization: Optional[str] = H
|
||||||
raw_prefix=request.raw_prefix,
|
raw_prefix=request.raw_prefix,
|
||||||
clone_prefix=request.clone_prefix,
|
clone_prefix=request.clone_prefix,
|
||||||
enabled=request.enabled,
|
enabled=request.enabled,
|
||||||
priority=request.priority,
|
priority=request.priority
|
||||||
)
|
)
|
||||||
|
|
||||||
return MirrorConfigResponse(
|
return MirrorConfigResponse(
|
||||||
|
|
@ -265,7 +264,7 @@ async def add_mirror(request: AddMirrorRequest, authorization: Optional[str] = H
|
||||||
raw_prefix=mirror["raw_prefix"],
|
raw_prefix=mirror["raw_prefix"],
|
||||||
clone_prefix=mirror["clone_prefix"],
|
clone_prefix=mirror["clone_prefix"],
|
||||||
enabled=mirror["enabled"],
|
enabled=mirror["enabled"],
|
||||||
priority=mirror["priority"],
|
priority=mirror["priority"]
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||||
|
|
@ -276,7 +275,9 @@ async def add_mirror(request: AddMirrorRequest, authorization: Optional[str] = H
|
||||||
|
|
||||||
@router.put("/mirrors/{mirror_id}", response_model=MirrorConfigResponse)
|
@router.put("/mirrors/{mirror_id}", response_model=MirrorConfigResponse)
|
||||||
async def update_mirror(
|
async def update_mirror(
|
||||||
mirror_id: str, request: UpdateMirrorRequest, authorization: Optional[str] = Header(None)
|
mirror_id: str,
|
||||||
|
request: UpdateMirrorRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
) -> MirrorConfigResponse:
|
) -> MirrorConfigResponse:
|
||||||
"""
|
"""
|
||||||
更新镜像源配置
|
更新镜像源配置
|
||||||
|
|
@ -297,7 +298,7 @@ async def update_mirror(
|
||||||
raw_prefix=request.raw_prefix,
|
raw_prefix=request.raw_prefix,
|
||||||
clone_prefix=request.clone_prefix,
|
clone_prefix=request.clone_prefix,
|
||||||
enabled=request.enabled,
|
enabled=request.enabled,
|
||||||
priority=request.priority,
|
priority=request.priority
|
||||||
)
|
)
|
||||||
|
|
||||||
if not mirror:
|
if not mirror:
|
||||||
|
|
@ -309,7 +310,7 @@ async def update_mirror(
|
||||||
raw_prefix=mirror["raw_prefix"],
|
raw_prefix=mirror["raw_prefix"],
|
||||||
clone_prefix=mirror["clone_prefix"],
|
clone_prefix=mirror["clone_prefix"],
|
||||||
enabled=mirror["enabled"],
|
enabled=mirror["enabled"],
|
||||||
priority=mirror["priority"],
|
priority=mirror["priority"]
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
@ -319,7 +320,10 @@ async def update_mirror(
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/mirrors/{mirror_id}")
|
@router.delete("/mirrors/{mirror_id}")
|
||||||
async def delete_mirror(mirror_id: str, authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
|
async def delete_mirror(
|
||||||
|
mirror_id: str,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
删除镜像源
|
删除镜像源
|
||||||
"""
|
"""
|
||||||
|
|
@ -337,12 +341,16 @@ async def delete_mirror(mirror_id: str, authorization: Optional[str] = Header(No
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=404, detail=f"未找到镜像源: {mirror_id}")
|
raise HTTPException(status_code=404, detail=f"未找到镜像源: {mirror_id}")
|
||||||
|
|
||||||
return {"success": True, "message": f"已删除镜像源: {mirror_id}"}
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"已删除镜像源: {mirror_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/fetch-raw", response_model=FetchRawFileResponse)
|
@router.post("/fetch-raw", response_model=FetchRawFileResponse)
|
||||||
async def fetch_raw_file(
|
async def fetch_raw_file(
|
||||||
request: FetchRawFileRequest, authorization: Optional[str] = Header(None)
|
request: FetchRawFileRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
) -> FetchRawFileResponse:
|
) -> FetchRawFileResponse:
|
||||||
"""
|
"""
|
||||||
获取 GitHub 仓库的 Raw 文件内容
|
获取 GitHub 仓库的 Raw 文件内容
|
||||||
|
|
@ -369,7 +377,7 @@ async def fetch_raw_file(
|
||||||
progress=10,
|
progress=10,
|
||||||
message=f"正在获取插件列表: {request.file_path}",
|
message=f"正在获取插件列表: {request.file_path}",
|
||||||
total_plugins=0,
|
total_plugins=0,
|
||||||
loaded_plugins=0,
|
loaded_plugins=0
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -382,19 +390,22 @@ async def fetch_raw_file(
|
||||||
branch=request.branch,
|
branch=request.branch,
|
||||||
file_path=request.file_path,
|
file_path=request.file_path,
|
||||||
mirror_id=request.mirror_id,
|
mirror_id=request.mirror_id,
|
||||||
custom_url=request.custom_url,
|
custom_url=request.custom_url
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
# 更新进度:成功获取
|
# 更新进度:成功获取
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="loading", progress=70, message="正在解析插件数据...", total_plugins=0, loaded_plugins=0
|
stage="loading",
|
||||||
|
progress=70,
|
||||||
|
message="正在解析插件数据...",
|
||||||
|
total_plugins=0,
|
||||||
|
loaded_plugins=0
|
||||||
)
|
)
|
||||||
|
|
||||||
# 尝试解析插件数量
|
# 尝试解析插件数量
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
data = json.loads(result.get("data", "[]"))
|
data = json.loads(result.get("data", "[]"))
|
||||||
total = len(data) if isinstance(data, list) else 0
|
total = len(data) if isinstance(data, list) else 0
|
||||||
|
|
||||||
|
|
@ -404,12 +415,16 @@ async def fetch_raw_file(
|
||||||
progress=100,
|
progress=100,
|
||||||
message=f"成功加载 {total} 个插件",
|
message=f"成功加载 {total} 个插件",
|
||||||
total_plugins=total,
|
total_plugins=total,
|
||||||
loaded_plugins=total,
|
loaded_plugins=total
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# 如果解析失败,仍然发送成功状态
|
# 如果解析失败,仍然发送成功状态
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="success", progress=100, message="加载完成", total_plugins=0, loaded_plugins=0
|
stage="success",
|
||||||
|
progress=100,
|
||||||
|
message="加载完成",
|
||||||
|
total_plugins=0,
|
||||||
|
loaded_plugins=0
|
||||||
)
|
)
|
||||||
|
|
||||||
return FetchRawFileResponse(**result)
|
return FetchRawFileResponse(**result)
|
||||||
|
|
@ -419,7 +434,12 @@ async def fetch_raw_file(
|
||||||
|
|
||||||
# 发送错误进度
|
# 发送错误进度
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="error", progress=0, message="加载失败", error=str(e), total_plugins=0, loaded_plugins=0
|
stage="error",
|
||||||
|
progress=0,
|
||||||
|
message="加载失败",
|
||||||
|
error=str(e),
|
||||||
|
total_plugins=0,
|
||||||
|
loaded_plugins=0
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||||
|
|
@ -427,7 +447,8 @@ async def fetch_raw_file(
|
||||||
|
|
||||||
@router.post("/clone", response_model=CloneRepositoryResponse)
|
@router.post("/clone", response_model=CloneRepositoryResponse)
|
||||||
async def clone_repository(
|
async def clone_repository(
|
||||||
request: CloneRepositoryRequest, authorization: Optional[str] = Header(None)
|
request: CloneRepositoryRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
) -> CloneRepositoryResponse:
|
) -> CloneRepositoryResponse:
|
||||||
"""
|
"""
|
||||||
克隆 GitHub 仓库到本地
|
克隆 GitHub 仓库到本地
|
||||||
|
|
@ -440,7 +461,9 @@ async def clone_repository(
|
||||||
if not token or not token_manager.verify_token(token):
|
if not token or not token_manager.verify_token(token):
|
||||||
raise HTTPException(status_code=401, detail="未授权:无效的访问令牌")
|
raise HTTPException(status_code=401, detail="未授权:无效的访问令牌")
|
||||||
|
|
||||||
logger.info(f"收到克隆仓库请求: {request.owner}/{request.repo} -> {request.target_path}")
|
logger.info(
|
||||||
|
f"收到克隆仓库请求: {request.owner}/{request.repo} -> {request.target_path}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# TODO: 验证 target_path 的安全性,防止路径遍历攻击
|
# TODO: 验证 target_path 的安全性,防止路径遍历攻击
|
||||||
|
|
@ -456,7 +479,7 @@ async def clone_repository(
|
||||||
branch=request.branch,
|
branch=request.branch,
|
||||||
mirror_id=request.mirror_id,
|
mirror_id=request.mirror_id,
|
||||||
custom_url=request.custom_url,
|
custom_url=request.custom_url,
|
||||||
depth=request.depth,
|
depth=request.depth
|
||||||
)
|
)
|
||||||
|
|
||||||
return CloneRepositoryResponse(**result)
|
return CloneRepositoryResponse(**result)
|
||||||
|
|
@ -467,7 +490,10 @@ async def clone_repository(
|
||||||
|
|
||||||
|
|
||||||
@router.post("/install")
|
@router.post("/install")
|
||||||
async def install_plugin(request: InstallPluginRequest, authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
|
async def install_plugin(
|
||||||
|
request: InstallPluginRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
安装插件
|
安装插件
|
||||||
|
|
||||||
|
|
@ -488,16 +514,16 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
progress=5,
|
progress=5,
|
||||||
message=f"开始安装插件: {request.plugin_id}",
|
message=f"开始安装插件: {request.plugin_id}",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. 解析仓库 URL
|
# 1. 解析仓库 URL
|
||||||
# repository_url 格式: https://github.com/owner/repo
|
# repository_url 格式: https://github.com/owner/repo
|
||||||
repo_url = request.repository_url.rstrip("/")
|
repo_url = request.repository_url.rstrip('/')
|
||||||
if repo_url.endswith(".git"):
|
if repo_url.endswith('.git'):
|
||||||
repo_url = repo_url[:-4]
|
repo_url = repo_url[:-4]
|
||||||
|
|
||||||
parts = repo_url.split("/")
|
parts = repo_url.split('/')
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
raise HTTPException(status_code=400, detail="无效的仓库 URL")
|
raise HTTPException(status_code=400, detail="无效的仓库 URL")
|
||||||
|
|
||||||
|
|
@ -509,7 +535,7 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
progress=10,
|
progress=10,
|
||||||
message=f"解析仓库信息: {owner}/{repo}",
|
message=f"解析仓库信息: {owner}/{repo}",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. 确定插件安装路径
|
# 2. 确定插件安装路径
|
||||||
|
|
@ -523,10 +549,10 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="error",
|
stage="error",
|
||||||
progress=0,
|
progress=0,
|
||||||
message="插件已存在",
|
message=f"插件已存在",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error="插件已安装,请先卸载",
|
error="插件已安装,请先卸载"
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=400, detail="插件已安装")
|
raise HTTPException(status_code=400, detail="插件已安装")
|
||||||
|
|
||||||
|
|
@ -535,26 +561,31 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
progress=15,
|
progress=15,
|
||||||
message=f"准备克隆到: {target_path}",
|
message=f"准备克隆到: {target_path}",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. 克隆仓库(这里会自动推送 20%-80% 的进度)
|
# 3. 克隆仓库(这里会自动推送 20%-80% 的进度)
|
||||||
service = get_git_mirror_service()
|
service = get_git_mirror_service()
|
||||||
|
|
||||||
# 如果是 GitHub 仓库,使用镜像源
|
# 如果是 GitHub 仓库,使用镜像源
|
||||||
if "github.com" in repo_url:
|
if 'github.com' in repo_url:
|
||||||
result = await service.clone_repository(
|
result = await service.clone_repository(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
repo=repo,
|
repo=repo,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
branch=request.branch,
|
branch=request.branch,
|
||||||
mirror_id=request.mirror_id,
|
mirror_id=request.mirror_id,
|
||||||
depth=1, # 浅克隆,节省时间和空间
|
depth=1 # 浅克隆,节省时间和空间
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 自定义仓库,直接使用 URL
|
# 自定义仓库,直接使用 URL
|
||||||
result = await service.clone_repository(
|
result = await service.clone_repository(
|
||||||
owner=owner, repo=repo, target_path=target_path, branch=request.branch, custom_url=repo_url, depth=1
|
owner=owner,
|
||||||
|
repo=repo,
|
||||||
|
target_path=target_path,
|
||||||
|
branch=request.branch,
|
||||||
|
custom_url=repo_url,
|
||||||
|
depth=1
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result.get("success"):
|
if not result.get("success"):
|
||||||
|
|
@ -565,20 +596,23 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
message="克隆仓库失败",
|
message="克隆仓库失败",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error=error_msg,
|
error=error_msg
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=500, detail=error_msg)
|
raise HTTPException(status_code=500, detail=error_msg)
|
||||||
|
|
||||||
# 4. 验证插件完整性
|
# 4. 验证插件完整性
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="loading", progress=85, message="验证插件文件...", operation="install", plugin_id=request.plugin_id
|
stage="loading",
|
||||||
|
progress=85,
|
||||||
|
message="验证插件文件...",
|
||||||
|
operation="install",
|
||||||
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
manifest_path = target_path / "_manifest.json"
|
manifest_path = target_path / "_manifest.json"
|
||||||
if not manifest_path.exists():
|
if not manifest_path.exists():
|
||||||
# 清理失败的安装
|
# 清理失败的安装
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.rmtree(target_path, ignore_errors=True)
|
shutil.rmtree(target_path, ignore_errors=True)
|
||||||
|
|
||||||
await update_progress(
|
await update_progress(
|
||||||
|
|
@ -587,23 +621,26 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
message="插件缺少 _manifest.json",
|
message="插件缺少 _manifest.json",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error="无效的插件格式",
|
error="无效的插件格式"
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
||||||
|
|
||||||
# 5. 读取并验证 manifest
|
# 5. 读取并验证 manifest
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="loading", progress=90, message="读取插件配置...", operation="install", plugin_id=request.plugin_id
|
stage="loading",
|
||||||
|
progress=90,
|
||||||
|
message="读取插件配置...",
|
||||||
|
operation="install",
|
||||||
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import json as json_module
|
import json as json_module
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
||||||
manifest = json_module.load(f)
|
manifest = json_module.load(f)
|
||||||
|
|
||||||
# 基本验证
|
# 基本验证
|
||||||
required_fields = ["manifest_version", "name", "version", "author"]
|
required_fields = ['manifest_version', 'name', 'version', 'author']
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
if field not in manifest:
|
if field not in manifest:
|
||||||
raise ValueError(f"缺少必需字段: {field}")
|
raise ValueError(f"缺少必需字段: {field}")
|
||||||
|
|
@ -611,7 +648,6 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 清理失败的安装
|
# 清理失败的安装
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.rmtree(target_path, ignore_errors=True)
|
shutil.rmtree(target_path, ignore_errors=True)
|
||||||
|
|
||||||
await update_progress(
|
await update_progress(
|
||||||
|
|
@ -620,26 +656,42 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
message="_manifest.json 无效",
|
message="_manifest.json 无效",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error=str(e),
|
error=str(e)
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
||||||
|
|
||||||
|
# 5.5 安装依赖
|
||||||
|
await update_progress(
|
||||||
|
stage="loading",
|
||||||
|
progress=95,
|
||||||
|
message="正在检查并安装依赖...",
|
||||||
|
operation="install",
|
||||||
|
plugin_id=request.plugin_id
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 自动检测并安装 requirements.txt 或 pyproject.toml
|
||||||
|
await plugin_dependency_manager.install_auto_from_directory(str(target_path))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"依赖安装过程出错: {e}")
|
||||||
|
# 不中断安装流程,但记录日志
|
||||||
|
|
||||||
# 6. 安装成功
|
# 6. 安装成功
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="success",
|
stage="success",
|
||||||
progress=100,
|
progress=100,
|
||||||
message=f"成功安装插件: {manifest['name']} v{manifest['version']}",
|
message=f"成功安装插件: {manifest['name']} v{manifest['version']}",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "插件安装成功",
|
"message": "插件安装成功",
|
||||||
"plugin_id": request.plugin_id,
|
"plugin_id": request.plugin_id,
|
||||||
"plugin_name": manifest["name"],
|
"plugin_name": manifest['name'],
|
||||||
"version": manifest["version"],
|
"version": manifest['version'],
|
||||||
"path": str(target_path),
|
"path": str(target_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
@ -653,7 +705,7 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
message="安装失败",
|
message="安装失败",
|
||||||
operation="install",
|
operation="install",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error=str(e),
|
error=str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||||
|
|
@ -661,7 +713,8 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
||||||
|
|
||||||
@router.post("/uninstall")
|
@router.post("/uninstall")
|
||||||
async def uninstall_plugin(
|
async def uninstall_plugin(
|
||||||
request: UninstallPluginRequest, authorization: Optional[str] = Header(None)
|
request: UninstallPluginRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
卸载插件
|
卸载插件
|
||||||
|
|
@ -683,7 +736,7 @@ async def uninstall_plugin(
|
||||||
progress=10,
|
progress=10,
|
||||||
message=f"开始卸载插件: {request.plugin_id}",
|
message=f"开始卸载插件: {request.plugin_id}",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. 检查插件是否存在
|
# 1. 检查插件是否存在
|
||||||
|
|
@ -697,7 +750,7 @@ async def uninstall_plugin(
|
||||||
message="插件不存在",
|
message="插件不存在",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error="插件未安装或已被删除",
|
error="插件未安装或已被删除"
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=404, detail="插件未安装")
|
raise HTTPException(status_code=404, detail="插件未安装")
|
||||||
|
|
||||||
|
|
@ -706,7 +759,7 @@ async def uninstall_plugin(
|
||||||
progress=30,
|
progress=30,
|
||||||
message=f"正在删除插件文件: {plugin_path}",
|
message=f"正在删除插件文件: {plugin_path}",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. 读取插件信息(用于日志)
|
# 2. 读取插件信息(用于日志)
|
||||||
|
|
@ -716,8 +769,7 @@ async def uninstall_plugin(
|
||||||
if manifest_path.exists():
|
if manifest_path.exists():
|
||||||
try:
|
try:
|
||||||
import json as json_module
|
import json as json_module
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
||||||
manifest = json_module.load(f)
|
manifest = json_module.load(f)
|
||||||
plugin_name = manifest.get("name", request.plugin_id)
|
plugin_name = manifest.get("name", request.plugin_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -728,7 +780,7 @@ async def uninstall_plugin(
|
||||||
progress=50,
|
progress=50,
|
||||||
message=f"正在删除 {plugin_name}...",
|
message=f"正在删除 {plugin_name}...",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. 删除插件目录
|
# 3. 删除插件目录
|
||||||
|
|
@ -738,7 +790,6 @@ async def uninstall_plugin(
|
||||||
def remove_readonly(func, path, _):
|
def remove_readonly(func, path, _):
|
||||||
"""清除只读属性并删除文件"""
|
"""清除只读属性并删除文件"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.chmod(path, stat.S_IWRITE)
|
os.chmod(path, stat.S_IWRITE)
|
||||||
func(path)
|
func(path)
|
||||||
|
|
||||||
|
|
@ -752,10 +803,15 @@ async def uninstall_plugin(
|
||||||
progress=100,
|
progress=100,
|
||||||
message=f"成功卸载插件: {plugin_name}",
|
message=f"成功卸载插件: {plugin_name}",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"success": True, "message": "插件卸载成功", "plugin_id": request.plugin_id, "plugin_name": plugin_name}
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "插件卸载成功",
|
||||||
|
"plugin_id": request.plugin_id,
|
||||||
|
"plugin_name": plugin_name
|
||||||
|
}
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
@ -768,7 +824,7 @@ async def uninstall_plugin(
|
||||||
message="卸载失败",
|
message="卸载失败",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error="权限不足,无法删除插件文件",
|
error="权限不足,无法删除插件文件"
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(status_code=500, detail="权限不足,无法删除插件文件") from e
|
raise HTTPException(status_code=500, detail="权限不足,无法删除插件文件") from e
|
||||||
|
|
@ -781,14 +837,17 @@ async def uninstall_plugin(
|
||||||
message="卸载失败",
|
message="卸载失败",
|
||||||
operation="uninstall",
|
operation="uninstall",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error=str(e),
|
error=str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update")
|
@router.post("/update")
|
||||||
async def update_plugin(request: UpdatePluginRequest, authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
|
async def update_plugin(
|
||||||
|
request: UpdatePluginRequest,
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
更新插件
|
更新插件
|
||||||
|
|
||||||
|
|
@ -809,7 +868,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
progress=5,
|
progress=5,
|
||||||
message=f"开始更新插件: {request.plugin_id}",
|
message=f"开始更新插件: {request.plugin_id}",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. 检查插件是否已安装
|
# 1. 检查插件是否已安装
|
||||||
|
|
@ -823,23 +882,21 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
message="插件不存在",
|
message="插件不存在",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error="插件未安装,请先安装",
|
error="插件未安装,请先安装"
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=404, detail="插件未安装")
|
raise HTTPException(status_code=404, detail="插件未安装")
|
||||||
|
|
||||||
# 2. 读取旧版本信息
|
# 2. 读取旧版本信息
|
||||||
manifest_path = plugin_path / "_manifest.json"
|
manifest_path = plugin_path / "_manifest.json"
|
||||||
old_version = "unknown"
|
old_version = "unknown"
|
||||||
plugin_name = request.plugin_id
|
|
||||||
|
|
||||||
if manifest_path.exists():
|
if manifest_path.exists():
|
||||||
try:
|
try:
|
||||||
import json as json_module
|
import json as json_module
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
||||||
manifest = json_module.load(f)
|
manifest = json_module.load(f)
|
||||||
old_version = manifest.get("version", "unknown")
|
old_version = manifest.get("version", "unknown")
|
||||||
_plugin_name = manifest.get("name", request.plugin_id)
|
# plugin_name = manifest.get("name", request.plugin_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -848,12 +905,16 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
progress=10,
|
progress=10,
|
||||||
message=f"当前版本: {old_version},准备更新...",
|
message=f"当前版本: {old_version},准备更新...",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. 删除旧版本
|
# 3. 删除旧版本
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="loading", progress=20, message="正在删除旧版本...", operation="update", plugin_id=request.plugin_id
|
stage="loading",
|
||||||
|
progress=20,
|
||||||
|
message="正在删除旧版本...",
|
||||||
|
operation="update",
|
||||||
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -862,7 +923,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
def remove_readonly(func, path, _):
|
def remove_readonly(func, path, _):
|
||||||
"""清除只读属性并删除文件"""
|
"""清除只读属性并删除文件"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.chmod(path, stat.S_IWRITE)
|
os.chmod(path, stat.S_IWRITE)
|
||||||
func(path)
|
func(path)
|
||||||
|
|
||||||
|
|
@ -876,14 +936,14 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
progress=30,
|
progress=30,
|
||||||
message="正在准备下载新版本...",
|
message="正在准备下载新版本...",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
repo_url = request.repository_url.rstrip("/")
|
repo_url = request.repository_url.rstrip('/')
|
||||||
if repo_url.endswith(".git"):
|
if repo_url.endswith('.git'):
|
||||||
repo_url = repo_url[:-4]
|
repo_url = repo_url[:-4]
|
||||||
|
|
||||||
parts = repo_url.split("/")
|
parts = repo_url.split('/')
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
raise HTTPException(status_code=400, detail="无效的仓库 URL")
|
raise HTTPException(status_code=400, detail="无效的仓库 URL")
|
||||||
|
|
||||||
|
|
@ -893,18 +953,23 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
# 5. 克隆新版本(这里会推送 35%-85% 的进度)
|
# 5. 克隆新版本(这里会推送 35%-85% 的进度)
|
||||||
service = get_git_mirror_service()
|
service = get_git_mirror_service()
|
||||||
|
|
||||||
if "github.com" in repo_url:
|
if 'github.com' in repo_url:
|
||||||
result = await service.clone_repository(
|
result = await service.clone_repository(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
repo=repo,
|
repo=repo,
|
||||||
target_path=plugin_path,
|
target_path=plugin_path,
|
||||||
branch=request.branch,
|
branch=request.branch,
|
||||||
mirror_id=request.mirror_id,
|
mirror_id=request.mirror_id,
|
||||||
depth=1,
|
depth=1
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await service.clone_repository(
|
result = await service.clone_repository(
|
||||||
owner=owner, repo=repo, target_path=plugin_path, branch=request.branch, custom_url=repo_url, depth=1
|
owner=owner,
|
||||||
|
repo=repo,
|
||||||
|
target_path=plugin_path,
|
||||||
|
branch=request.branch,
|
||||||
|
custom_url=repo_url,
|
||||||
|
depth=1
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result.get("success"):
|
if not result.get("success"):
|
||||||
|
|
@ -915,13 +980,17 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
message="下载新版本失败",
|
message="下载新版本失败",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error=error_msg,
|
error=error_msg
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=500, detail=error_msg)
|
raise HTTPException(status_code=500, detail=error_msg)
|
||||||
|
|
||||||
# 6. 验证新版本
|
# 6. 验证新版本
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="loading", progress=90, message="验证新版本...", operation="update", plugin_id=request.plugin_id
|
stage="loading",
|
||||||
|
progress=90,
|
||||||
|
message="验证新版本...",
|
||||||
|
operation="update",
|
||||||
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
new_manifest_path = plugin_path / "_manifest.json"
|
new_manifest_path = plugin_path / "_manifest.json"
|
||||||
|
|
@ -930,7 +999,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
def remove_readonly(func, path, _):
|
def remove_readonly(func, path, _):
|
||||||
"""清除只读属性并删除文件"""
|
"""清除只读属性并删除文件"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.chmod(path, stat.S_IWRITE)
|
os.chmod(path, stat.S_IWRITE)
|
||||||
func(path)
|
func(path)
|
||||||
|
|
||||||
|
|
@ -942,18 +1010,31 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
message="新版本缺少 _manifest.json",
|
message="新版本缺少 _manifest.json",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error="无效的插件格式",
|
error="无效的插件格式"
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
||||||
|
|
||||||
# 7. 读取新版本信息
|
# 7. 读取新版本信息
|
||||||
try:
|
try:
|
||||||
with open(new_manifest_path, "r", encoding="utf-8") as f:
|
with open(new_manifest_path, 'r', encoding='utf-8') as f:
|
||||||
new_manifest = json_module.load(f)
|
new_manifest = json_module.load(f)
|
||||||
|
|
||||||
new_version = new_manifest.get("version", "unknown")
|
new_version = new_manifest.get("version", "unknown")
|
||||||
new_name = new_manifest.get("name", request.plugin_id)
|
new_name = new_manifest.get("name", request.plugin_id)
|
||||||
|
|
||||||
|
# 7.5 安装依赖
|
||||||
|
await update_progress(
|
||||||
|
stage="loading",
|
||||||
|
progress=95,
|
||||||
|
message="正在更新依赖...",
|
||||||
|
operation="update",
|
||||||
|
plugin_id=request.plugin_id
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await plugin_dependency_manager.install_auto_from_directory(str(plugin_path))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"依赖更新过程出错: {e}")
|
||||||
|
|
||||||
logger.info(f"成功更新插件: {request.plugin_id} {old_version} → {new_version}")
|
logger.info(f"成功更新插件: {request.plugin_id} {old_version} → {new_version}")
|
||||||
|
|
||||||
# 8. 推送成功状态
|
# 8. 推送成功状态
|
||||||
|
|
@ -962,7 +1043,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
progress=100,
|
progress=100,
|
||||||
message=f"成功更新 {new_name}: {old_version} → {new_version}",
|
message=f"成功更新 {new_name}: {old_version} → {new_version}",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -971,7 +1052,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
"plugin_id": request.plugin_id,
|
"plugin_id": request.plugin_id,
|
||||||
"plugin_name": new_name,
|
"plugin_name": new_name,
|
||||||
"old_version": old_version,
|
"old_version": old_version,
|
||||||
"new_version": new_version,
|
"new_version": new_version
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -984,7 +1065,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
message="_manifest.json 无效",
|
message="_manifest.json 无效",
|
||||||
operation="update",
|
operation="update",
|
||||||
plugin_id=request.plugin_id,
|
plugin_id=request.plugin_id,
|
||||||
error=str(e),
|
error=str(e)
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
||||||
|
|
||||||
|
|
@ -994,14 +1075,21 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
||||||
logger.error(f"更新插件失败: {e}", exc_info=True)
|
logger.error(f"更新插件失败: {e}", exc_info=True)
|
||||||
|
|
||||||
await update_progress(
|
await update_progress(
|
||||||
stage="error", progress=0, message="更新失败", operation="update", plugin_id=request.plugin_id, error=str(e)
|
stage="error",
|
||||||
|
progress=0,
|
||||||
|
message="更新失败",
|
||||||
|
operation="update",
|
||||||
|
plugin_id=request.plugin_id,
|
||||||
|
error=str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||||
|
|
||||||
|
|
||||||
@router.get("/installed")
|
@router.get("/installed")
|
||||||
async def get_installed_plugins(authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
|
async def get_installed_plugins(
|
||||||
|
authorization: Optional[str] = Header(None)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取已安装的插件列表
|
获取已安装的插件列表
|
||||||
|
|
||||||
|
|
@ -1022,7 +1110,10 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
||||||
if not plugins_dir.exists():
|
if not plugins_dir.exists():
|
||||||
logger.info("插件目录不存在,创建目录")
|
logger.info("插件目录不存在,创建目录")
|
||||||
plugins_dir.mkdir(exist_ok=True)
|
plugins_dir.mkdir(exist_ok=True)
|
||||||
return {"success": True, "plugins": []}
|
return {
|
||||||
|
"success": True,
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
|
|
||||||
installed_plugins = []
|
installed_plugins = []
|
||||||
|
|
||||||
|
|
@ -1036,7 +1127,7 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
||||||
plugin_id = plugin_path.name
|
plugin_id = plugin_path.name
|
||||||
|
|
||||||
# 跳过隐藏目录和特殊目录
|
# 跳过隐藏目录和特殊目录
|
||||||
if plugin_id.startswith(".") or plugin_id.startswith("__"):
|
if plugin_id.startswith('.') or plugin_id.startswith('__'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 读取 _manifest.json
|
# 读取 _manifest.json
|
||||||
|
|
@ -1048,23 +1139,20 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import json as json_module
|
import json as json_module
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
||||||
manifest = json_module.load(f)
|
manifest = json_module.load(f)
|
||||||
|
|
||||||
# 基本验证
|
# 基本验证
|
||||||
if "name" not in manifest or "version" not in manifest:
|
if 'name' not in manifest or 'version' not in manifest:
|
||||||
logger.warning(f"插件 {plugin_id} 的 _manifest.json 格式无效,跳过")
|
logger.warning(f"插件 {plugin_id} 的 _manifest.json 格式无效,跳过")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 添加到已安装列表(返回完整的 manifest 信息)
|
# 添加到已安装列表(返回完整的 manifest 信息)
|
||||||
installed_plugins.append(
|
installed_plugins.append({
|
||||||
{
|
"id": plugin_id,
|
||||||
"id": plugin_id,
|
"manifest": manifest, # 返回完整的 manifest 对象
|
||||||
"manifest": manifest, # 返回完整的 manifest 对象
|
"path": str(plugin_path.absolute())
|
||||||
"path": str(plugin_path.absolute()),
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.warning(f"插件 {plugin_id} 的 _manifest.json 解析失败: {e}")
|
logger.warning(f"插件 {plugin_id} 的 _manifest.json 解析失败: {e}")
|
||||||
|
|
@ -1075,7 +1163,11 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
||||||
|
|
||||||
logger.info(f"找到 {len(installed_plugins)} 个已安装插件")
|
logger.info(f"找到 {len(installed_plugins)} 个已安装插件")
|
||||||
|
|
||||||
return {"success": True, "plugins": installed_plugins, "total": len(installed_plugins)}
|
return {
|
||||||
|
"success": True,
|
||||||
|
"plugins": installed_plugins,
|
||||||
|
"total": len(installed_plugins)
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取已安装插件列表失败: {e}", exc_info=True)
|
logger.error(f"获取已安装插件列表失败: {e}", exc_info=True)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue