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 traceback
|
||||
import asyncio
|
||||
|
||||
from typing import Dict, List, Optional, Tuple, Type, Any
|
||||
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.utils.manifest_utils import VersionComparator
|
||||
from .component_registry import component_registry
|
||||
from .dependency_manager import plugin_dependency_manager
|
||||
|
||||
logger = get_logger("plugin_manager")
|
||||
|
||||
|
|
@ -73,7 +75,13 @@ class PluginManager:
|
|||
total_registered = 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)
|
||||
if load_status:
|
||||
total_registered += 1
|
||||
|
|
@ -84,6 +92,60 @@ class PluginManager:
|
|||
|
||||
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]:
|
||||
# sourcery skip: extract-duplicate-method, extract-method
|
||||
"""
|
||||
|
|
@ -110,6 +172,53 @@ class PluginManager:
|
|||
logger.info(f"插件 {plugin_name} 已禁用,跳过加载")
|
||||
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(
|
||||
plugin_name, plugin_instance.manifest_data
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from pathlib import Path
|
|||
import json
|
||||
from src.common.logger import get_logger
|
||||
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 .token_manager import get_token_manager
|
||||
from .plugin_progress_ws import update_progress
|
||||
|
|
@ -21,21 +22,21 @@ set_update_progress_callback(update_progress)
|
|||
def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||
"""
|
||||
解析版本号字符串
|
||||
|
||||
|
||||
支持格式:
|
||||
- 0.11.2 -> (0, 11, 2)
|
||||
- 0.11.2.snapshot.2 -> (0, 11, 2)
|
||||
|
||||
|
||||
Returns:
|
||||
(major, minor, patch) 三元组
|
||||
"""
|
||||
# 移除 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:
|
||||
# 补齐到 3 位
|
||||
parts.extend(["0"] * (3 - len(parts)))
|
||||
parts.extend(['0'] * (3 - len(parts)))
|
||||
|
||||
try:
|
||||
major = int(parts[0])
|
||||
|
|
@ -49,10 +50,8 @@ def parse_version(version_str: str) -> tuple[int, int, int]:
|
|||
|
||||
# ============ 请求/响应模型 ============
|
||||
|
||||
|
||||
class FetchRawFileRequest(BaseModel):
|
||||
"""获取 Raw 文件请求"""
|
||||
|
||||
owner: str = Field(..., description="仓库所有者", example="MaiM-with-u")
|
||||
repo: str = Field(..., description="仓库名称", example="plugin-repo")
|
||||
branch: str = Field(..., description="分支名称", example="main")
|
||||
|
|
@ -63,7 +62,6 @@ class FetchRawFileRequest(BaseModel):
|
|||
|
||||
class FetchRawFileResponse(BaseModel):
|
||||
"""获取 Raw 文件响应"""
|
||||
|
||||
success: bool = Field(..., description="是否成功")
|
||||
data: Optional[str] = Field(None, description="文件内容")
|
||||
error: Optional[str] = Field(None, description="错误信息")
|
||||
|
|
@ -74,7 +72,6 @@ class FetchRawFileResponse(BaseModel):
|
|||
|
||||
class CloneRepositoryRequest(BaseModel):
|
||||
"""克隆仓库请求"""
|
||||
|
||||
owner: str = Field(..., description="仓库所有者", example="MaiM-with-u")
|
||||
repo: str = Field(..., description="仓库名称", example="plugin-repo")
|
||||
target_path: str = Field(..., description="目标路径(相对于插件目录)")
|
||||
|
|
@ -86,7 +83,6 @@ class CloneRepositoryRequest(BaseModel):
|
|||
|
||||
class CloneRepositoryResponse(BaseModel):
|
||||
"""克隆仓库响应"""
|
||||
|
||||
success: bool = Field(..., description="是否成功")
|
||||
path: Optional[str] = Field(None, description="克隆路径")
|
||||
error: Optional[str] = Field(None, description="错误信息")
|
||||
|
|
@ -98,7 +94,6 @@ class CloneRepositoryResponse(BaseModel):
|
|||
|
||||
class MirrorConfigResponse(BaseModel):
|
||||
"""镜像源配置响应"""
|
||||
|
||||
id: str = Field(..., description="镜像源 ID")
|
||||
name: str = Field(..., description="镜像源名称")
|
||||
raw_prefix: str = Field(..., description="Raw 文件前缀")
|
||||
|
|
@ -109,14 +104,12 @@ class MirrorConfigResponse(BaseModel):
|
|||
|
||||
class AvailableMirrorsResponse(BaseModel):
|
||||
"""可用镜像源列表响应"""
|
||||
|
||||
mirrors: List[MirrorConfigResponse] = Field(..., description="镜像源列表")
|
||||
default_priority: List[str] = Field(..., description="默认优先级顺序(ID 列表)")
|
||||
|
||||
|
||||
class AddMirrorRequest(BaseModel):
|
||||
"""添加镜像源请求"""
|
||||
|
||||
id: str = Field(..., description="镜像源 ID", example="custom-mirror")
|
||||
name: str = Field(..., description="镜像源名称", example="自定义镜像源")
|
||||
raw_prefix: str = Field(..., description="Raw 文件前缀", example="https://example.com/raw")
|
||||
|
|
@ -127,7 +120,6 @@ class AddMirrorRequest(BaseModel):
|
|||
|
||||
class UpdateMirrorRequest(BaseModel):
|
||||
"""更新镜像源请求"""
|
||||
|
||||
name: Optional[str] = Field(None, description="镜像源名称")
|
||||
raw_prefix: Optional[str] = Field(None, description="Raw 文件前缀")
|
||||
clone_prefix: Optional[str] = Field(None, description="克隆前缀")
|
||||
|
|
@ -137,7 +129,6 @@ class UpdateMirrorRequest(BaseModel):
|
|||
|
||||
class GitStatusResponse(BaseModel):
|
||||
"""Git 安装状态响应"""
|
||||
|
||||
installed: bool = Field(..., description="是否已安装 Git")
|
||||
version: Optional[str] = Field(None, description="Git 版本号")
|
||||
path: Optional[str] = Field(None, description="Git 可执行文件路径")
|
||||
|
|
@ -146,7 +137,6 @@ class GitStatusResponse(BaseModel):
|
|||
|
||||
class InstallPluginRequest(BaseModel):
|
||||
"""安装插件请求"""
|
||||
|
||||
plugin_id: str = Field(..., description="插件 ID")
|
||||
repository_url: str = Field(..., description="插件仓库 URL")
|
||||
branch: Optional[str] = Field("main", description="分支名称")
|
||||
|
|
@ -155,7 +145,6 @@ class InstallPluginRequest(BaseModel):
|
|||
|
||||
class VersionResponse(BaseModel):
|
||||
"""麦麦版本响应"""
|
||||
|
||||
version: str = Field(..., description="麦麦版本号")
|
||||
version_major: int = Field(..., description="主版本号")
|
||||
version_minor: int = Field(..., description="次版本号")
|
||||
|
|
@ -164,13 +153,11 @@ class VersionResponse(BaseModel):
|
|||
|
||||
class UninstallPluginRequest(BaseModel):
|
||||
"""卸载插件请求"""
|
||||
|
||||
plugin_id: str = Field(..., description="插件 ID")
|
||||
|
||||
|
||||
class UpdatePluginRequest(BaseModel):
|
||||
"""更新插件请求"""
|
||||
|
||||
plugin_id: str = Field(..., description="插件 ID")
|
||||
repository_url: str = Field(..., description="插件仓库 URL")
|
||||
branch: Optional[str] = Field("main", description="分支名称")
|
||||
|
|
@ -179,24 +166,28 @@ class UpdatePluginRequest(BaseModel):
|
|||
|
||||
# ============ API 路由 ============
|
||||
|
||||
|
||||
@router.get("/version", response_model=VersionResponse)
|
||||
async def get_maimai_version() -> VersionResponse:
|
||||
"""
|
||||
获取麦麦版本信息
|
||||
|
||||
|
||||
此接口无需认证,用于前端检查插件兼容性
|
||||
"""
|
||||
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)
|
||||
async def check_git_status() -> GitStatusResponse:
|
||||
"""
|
||||
检查本机 Git 安装状态
|
||||
|
||||
|
||||
此接口无需认证,用于前端快速检测是否可以使用插件安装功能
|
||||
"""
|
||||
service = get_git_mirror_service()
|
||||
|
|
@ -206,7 +197,9 @@ async def check_git_status() -> GitStatusResponse:
|
|||
|
||||
|
||||
@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"],
|
||||
clone_prefix=m["clone_prefix"],
|
||||
enabled=m["enabled"],
|
||||
priority=m["priority"],
|
||||
priority=m["priority"]
|
||||
)
|
||||
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)
|
||||
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,
|
||||
clone_prefix=request.clone_prefix,
|
||||
enabled=request.enabled,
|
||||
priority=request.priority,
|
||||
priority=request.priority
|
||||
)
|
||||
|
||||
return MirrorConfigResponse(
|
||||
|
|
@ -265,7 +264,7 @@ async def add_mirror(request: AddMirrorRequest, authorization: Optional[str] = H
|
|||
raw_prefix=mirror["raw_prefix"],
|
||||
clone_prefix=mirror["clone_prefix"],
|
||||
enabled=mirror["enabled"],
|
||||
priority=mirror["priority"],
|
||||
priority=mirror["priority"]
|
||||
)
|
||||
except ValueError as 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)
|
||||
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:
|
||||
"""
|
||||
更新镜像源配置
|
||||
|
|
@ -297,7 +298,7 @@ async def update_mirror(
|
|||
raw_prefix=request.raw_prefix,
|
||||
clone_prefix=request.clone_prefix,
|
||||
enabled=request.enabled,
|
||||
priority=request.priority,
|
||||
priority=request.priority
|
||||
)
|
||||
|
||||
if not mirror:
|
||||
|
|
@ -309,7 +310,7 @@ async def update_mirror(
|
|||
raw_prefix=mirror["raw_prefix"],
|
||||
clone_prefix=mirror["clone_prefix"],
|
||||
enabled=mirror["enabled"],
|
||||
priority=mirror["priority"],
|
||||
priority=mirror["priority"]
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -319,7 +320,10 @@ async def update_mirror(
|
|||
|
||||
|
||||
@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,18 +341,22 @@ async def delete_mirror(mirror_id: str, authorization: Optional[str] = Header(No
|
|||
if not success:
|
||||
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)
|
||||
async def fetch_raw_file(
|
||||
request: FetchRawFileRequest, authorization: Optional[str] = Header(None)
|
||||
request: FetchRawFileRequest,
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> FetchRawFileResponse:
|
||||
"""
|
||||
获取 GitHub 仓库的 Raw 文件内容
|
||||
|
||||
|
||||
支持多镜像源自动切换和错误重试
|
||||
|
||||
|
||||
注意:此接口可公开访问,用于获取插件仓库等公开资源
|
||||
"""
|
||||
# Token 验证(可选,用于日志记录)
|
||||
|
|
@ -369,7 +377,7 @@ async def fetch_raw_file(
|
|||
progress=10,
|
||||
message=f"正在获取插件列表: {request.file_path}",
|
||||
total_plugins=0,
|
||||
loaded_plugins=0,
|
||||
loaded_plugins=0
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -382,19 +390,22 @@ async def fetch_raw_file(
|
|||
branch=request.branch,
|
||||
file_path=request.file_path,
|
||||
mirror_id=request.mirror_id,
|
||||
custom_url=request.custom_url,
|
||||
custom_url=request.custom_url
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
# 更新进度:成功获取
|
||||
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:
|
||||
import json
|
||||
|
||||
data = json.loads(result.get("data", "[]"))
|
||||
total = len(data) if isinstance(data, list) else 0
|
||||
|
||||
|
|
@ -404,12 +415,16 @@ async def fetch_raw_file(
|
|||
progress=100,
|
||||
message=f"成功加载 {total} 个插件",
|
||||
total_plugins=total,
|
||||
loaded_plugins=total,
|
||||
loaded_plugins=total
|
||||
)
|
||||
except Exception:
|
||||
# 如果解析失败,仍然发送成功状态
|
||||
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)
|
||||
|
|
@ -419,7 +434,12 @@ async def fetch_raw_file(
|
|||
|
||||
# 发送错误进度
|
||||
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
|
||||
|
|
@ -427,11 +447,12 @@ async def fetch_raw_file(
|
|||
|
||||
@router.post("/clone", response_model=CloneRepositoryResponse)
|
||||
async def clone_repository(
|
||||
request: CloneRepositoryRequest, authorization: Optional[str] = Header(None)
|
||||
request: CloneRepositoryRequest,
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> CloneRepositoryResponse:
|
||||
"""
|
||||
克隆 GitHub 仓库到本地
|
||||
|
||||
|
||||
支持多镜像源自动切换和错误重试
|
||||
"""
|
||||
# Token 验证
|
||||
|
|
@ -440,7 +461,9 @@ async def clone_repository(
|
|||
if not token or not token_manager.verify_token(token):
|
||||
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:
|
||||
# TODO: 验证 target_path 的安全性,防止路径遍历攻击
|
||||
|
|
@ -456,7 +479,7 @@ async def clone_repository(
|
|||
branch=request.branch,
|
||||
mirror_id=request.mirror_id,
|
||||
custom_url=request.custom_url,
|
||||
depth=request.depth,
|
||||
depth=request.depth
|
||||
)
|
||||
|
||||
return CloneRepositoryResponse(**result)
|
||||
|
|
@ -467,10 +490,13 @@ async def clone_repository(
|
|||
|
||||
|
||||
@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]:
|
||||
"""
|
||||
安装插件
|
||||
|
||||
|
||||
从 Git 仓库克隆插件到本地插件目录
|
||||
"""
|
||||
# Token 验证
|
||||
|
|
@ -488,16 +514,16 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
progress=5,
|
||||
message=f"开始安装插件: {request.plugin_id}",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 1. 解析仓库 URL
|
||||
# repository_url 格式: https://github.com/owner/repo
|
||||
repo_url = request.repository_url.rstrip("/")
|
||||
if repo_url.endswith(".git"):
|
||||
repo_url = request.repository_url.rstrip('/')
|
||||
if repo_url.endswith('.git'):
|
||||
repo_url = repo_url[:-4]
|
||||
|
||||
parts = repo_url.split("/")
|
||||
parts = repo_url.split('/')
|
||||
if len(parts) < 2:
|
||||
raise HTTPException(status_code=400, detail="无效的仓库 URL")
|
||||
|
||||
|
|
@ -509,7 +535,7 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
progress=10,
|
||||
message=f"解析仓库信息: {owner}/{repo}",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 2. 确定插件安装路径
|
||||
|
|
@ -523,10 +549,10 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="插件已存在",
|
||||
message=f"插件已存在",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
error="插件已安装,请先卸载",
|
||||
error="插件已安装,请先卸载"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="插件已安装")
|
||||
|
||||
|
|
@ -535,26 +561,31 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
progress=15,
|
||||
message=f"准备克隆到: {target_path}",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 3. 克隆仓库(这里会自动推送 20%-80% 的进度)
|
||||
service = get_git_mirror_service()
|
||||
|
||||
# 如果是 GitHub 仓库,使用镜像源
|
||||
if "github.com" in repo_url:
|
||||
if 'github.com' in repo_url:
|
||||
result = await service.clone_repository(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
target_path=target_path,
|
||||
branch=request.branch,
|
||||
mirror_id=request.mirror_id,
|
||||
depth=1, # 浅克隆,节省时间和空间
|
||||
depth=1 # 浅克隆,节省时间和空间
|
||||
)
|
||||
else:
|
||||
# 自定义仓库,直接使用 URL
|
||||
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"):
|
||||
|
|
@ -565,20 +596,23 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
message="克隆仓库失败",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
error=error_msg,
|
||||
error=error_msg
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
# 4. 验证插件完整性
|
||||
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"
|
||||
if not manifest_path.exists():
|
||||
# 清理失败的安装
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(target_path, ignore_errors=True)
|
||||
|
||||
await update_progress(
|
||||
|
|
@ -587,23 +621,26 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
message="插件缺少 _manifest.json",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
error="无效的插件格式",
|
||||
error="无效的插件格式"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
||||
|
||||
# 5. 读取并验证 manifest
|
||||
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:
|
||||
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)
|
||||
|
||||
# 基本验证
|
||||
required_fields = ["manifest_version", "name", "version", "author"]
|
||||
required_fields = ['manifest_version', 'name', 'version', 'author']
|
||||
for field in required_fields:
|
||||
if field not in manifest:
|
||||
raise ValueError(f"缺少必需字段: {field}")
|
||||
|
|
@ -611,7 +648,6 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
except Exception as e:
|
||||
# 清理失败的安装
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(target_path, ignore_errors=True)
|
||||
|
||||
await update_progress(
|
||||
|
|
@ -620,26 +656,42 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
message="_manifest.json 无效",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
error=str(e),
|
||||
error=str(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. 安装成功
|
||||
await update_progress(
|
||||
stage="success",
|
||||
progress=100,
|
||||
message=f"成功安装插件: {manifest['name']} v{manifest['version']}",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "插件安装成功",
|
||||
"plugin_id": request.plugin_id,
|
||||
"plugin_name": manifest["name"],
|
||||
"version": manifest["version"],
|
||||
"path": str(target_path),
|
||||
"plugin_name": manifest['name'],
|
||||
"version": manifest['version'],
|
||||
"path": str(target_path)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
|
|
@ -653,7 +705,7 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
message="安装失败",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
error=str(e),
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
|
@ -661,11 +713,12 @@ async def install_plugin(request: InstallPluginRequest, authorization: Optional[
|
|||
|
||||
@router.post("/uninstall")
|
||||
async def uninstall_plugin(
|
||||
request: UninstallPluginRequest, authorization: Optional[str] = Header(None)
|
||||
request: UninstallPluginRequest,
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
卸载插件
|
||||
|
||||
|
||||
删除插件目录及其所有文件
|
||||
"""
|
||||
# Token 验证
|
||||
|
|
@ -683,7 +736,7 @@ async def uninstall_plugin(
|
|||
progress=10,
|
||||
message=f"开始卸载插件: {request.plugin_id}",
|
||||
operation="uninstall",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 1. 检查插件是否存在
|
||||
|
|
@ -697,7 +750,7 @@ async def uninstall_plugin(
|
|||
message="插件不存在",
|
||||
operation="uninstall",
|
||||
plugin_id=request.plugin_id,
|
||||
error="插件未安装或已被删除",
|
||||
error="插件未安装或已被删除"
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="插件未安装")
|
||||
|
||||
|
|
@ -706,7 +759,7 @@ async def uninstall_plugin(
|
|||
progress=30,
|
||||
message=f"正在删除插件文件: {plugin_path}",
|
||||
operation="uninstall",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 2. 读取插件信息(用于日志)
|
||||
|
|
@ -716,8 +769,7 @@ async def uninstall_plugin(
|
|||
if manifest_path.exists():
|
||||
try:
|
||||
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)
|
||||
plugin_name = manifest.get("name", request.plugin_id)
|
||||
except Exception:
|
||||
|
|
@ -728,7 +780,7 @@ async def uninstall_plugin(
|
|||
progress=50,
|
||||
message=f"正在删除 {plugin_name}...",
|
||||
operation="uninstall",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 3. 删除插件目录
|
||||
|
|
@ -738,7 +790,6 @@ async def uninstall_plugin(
|
|||
def remove_readonly(func, path, _):
|
||||
"""清除只读属性并删除文件"""
|
||||
import os
|
||||
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
|
|
@ -752,10 +803,15 @@ async def uninstall_plugin(
|
|||
progress=100,
|
||||
message=f"成功卸载插件: {plugin_name}",
|
||||
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:
|
||||
raise
|
||||
|
|
@ -768,7 +824,7 @@ async def uninstall_plugin(
|
|||
message="卸载失败",
|
||||
operation="uninstall",
|
||||
plugin_id=request.plugin_id,
|
||||
error="权限不足,无法删除插件文件",
|
||||
error="权限不足,无法删除插件文件"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail="权限不足,无法删除插件文件") from e
|
||||
|
|
@ -781,17 +837,20 @@ async def uninstall_plugin(
|
|||
message="卸载失败",
|
||||
operation="uninstall",
|
||||
plugin_id=request.plugin_id,
|
||||
error=str(e),
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
|
||||
@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]:
|
||||
"""
|
||||
更新插件
|
||||
|
||||
|
||||
删除旧版本,重新克隆新版本
|
||||
"""
|
||||
# Token 验证
|
||||
|
|
@ -809,7 +868,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
progress=5,
|
||||
message=f"开始更新插件: {request.plugin_id}",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 1. 检查插件是否已安装
|
||||
|
|
@ -823,23 +882,21 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
message="插件不存在",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
error="插件未安装,请先安装",
|
||||
error="插件未安装,请先安装"
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="插件未安装")
|
||||
|
||||
# 2. 读取旧版本信息
|
||||
manifest_path = plugin_path / "_manifest.json"
|
||||
old_version = "unknown"
|
||||
plugin_name = request.plugin_id
|
||||
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
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)
|
||||
old_version = manifest.get("version", "unknown")
|
||||
_plugin_name = manifest.get("name", request.plugin_id)
|
||||
# plugin_name = manifest.get("name", request.plugin_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -848,12 +905,16 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
progress=10,
|
||||
message=f"当前版本: {old_version},准备更新...",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
# 3. 删除旧版本
|
||||
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
|
||||
|
|
@ -862,7 +923,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
def remove_readonly(func, path, _):
|
||||
"""清除只读属性并删除文件"""
|
||||
import os
|
||||
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
|
|
@ -876,14 +936,14 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
progress=30,
|
||||
message="正在准备下载新版本...",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
repo_url = request.repository_url.rstrip("/")
|
||||
if repo_url.endswith(".git"):
|
||||
repo_url = request.repository_url.rstrip('/')
|
||||
if repo_url.endswith('.git'):
|
||||
repo_url = repo_url[:-4]
|
||||
|
||||
parts = repo_url.split("/")
|
||||
parts = repo_url.split('/')
|
||||
if len(parts) < 2:
|
||||
raise HTTPException(status_code=400, detail="无效的仓库 URL")
|
||||
|
||||
|
|
@ -893,18 +953,23 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
# 5. 克隆新版本(这里会推送 35%-85% 的进度)
|
||||
service = get_git_mirror_service()
|
||||
|
||||
if "github.com" in repo_url:
|
||||
if 'github.com' in repo_url:
|
||||
result = await service.clone_repository(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
target_path=plugin_path,
|
||||
branch=request.branch,
|
||||
mirror_id=request.mirror_id,
|
||||
depth=1,
|
||||
depth=1
|
||||
)
|
||||
else:
|
||||
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"):
|
||||
|
|
@ -915,13 +980,17 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
message="下载新版本失败",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
error=error_msg,
|
||||
error=error_msg
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
# 6. 验证新版本
|
||||
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"
|
||||
|
|
@ -930,7 +999,6 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
def remove_readonly(func, path, _):
|
||||
"""清除只读属性并删除文件"""
|
||||
import os
|
||||
|
||||
os.chmod(path, stat.S_IWRITE)
|
||||
func(path)
|
||||
|
||||
|
|
@ -942,18 +1010,31 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
message="新版本缺少 _manifest.json",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
error="无效的插件格式",
|
||||
error="无效的插件格式"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
||||
|
||||
# 7. 读取新版本信息
|
||||
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_version = new_manifest.get("version", "unknown")
|
||||
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}")
|
||||
|
||||
# 8. 推送成功状态
|
||||
|
|
@ -962,7 +1043,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
progress=100,
|
||||
message=f"成功更新 {new_name}: {old_version} → {new_version}",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
plugin_id=request.plugin_id
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -971,7 +1052,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
"plugin_id": request.plugin_id,
|
||||
"plugin_name": new_name,
|
||||
"old_version": old_version,
|
||||
"new_version": new_version,
|
||||
"new_version": new_version
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -984,7 +1065,7 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
message="_manifest.json 无效",
|
||||
operation="update",
|
||||
plugin_id=request.plugin_id,
|
||||
error=str(e),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
||||
|
||||
|
|
@ -994,17 +1075,24 @@ async def update_plugin(request: UpdatePluginRequest, authorization: Optional[st
|
|||
logger.error(f"更新插件失败: {e}", exc_info=True)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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]:
|
||||
"""
|
||||
获取已安装的插件列表
|
||||
|
||||
|
||||
扫描 plugins 目录,返回所有已安装插件的 ID 和基本信息
|
||||
"""
|
||||
# Token 验证
|
||||
|
|
@ -1022,7 +1110,10 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
|||
if not plugins_dir.exists():
|
||||
logger.info("插件目录不存在,创建目录")
|
||||
plugins_dir.mkdir(exist_ok=True)
|
||||
return {"success": True, "plugins": []}
|
||||
return {
|
||||
"success": True,
|
||||
"plugins": []
|
||||
}
|
||||
|
||||
installed_plugins = []
|
||||
|
||||
|
|
@ -1036,7 +1127,7 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
|||
plugin_id = plugin_path.name
|
||||
|
||||
# 跳过隐藏目录和特殊目录
|
||||
if plugin_id.startswith(".") or plugin_id.startswith("__"):
|
||||
if plugin_id.startswith('.') or plugin_id.startswith('__'):
|
||||
continue
|
||||
|
||||
# 读取 _manifest.json
|
||||
|
|
@ -1048,23 +1139,20 @@ async def get_installed_plugins(authorization: Optional[str] = Header(None)) ->
|
|||
|
||||
try:
|
||||
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)
|
||||
|
||||
# 基本验证
|
||||
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 格式无效,跳过")
|
||||
continue
|
||||
|
||||
# 添加到已安装列表(返回完整的 manifest 信息)
|
||||
installed_plugins.append(
|
||||
{
|
||||
"id": plugin_id,
|
||||
"manifest": manifest, # 返回完整的 manifest 对象
|
||||
"path": str(plugin_path.absolute()),
|
||||
}
|
||||
)
|
||||
installed_plugins.append({
|
||||
"id": plugin_id,
|
||||
"manifest": manifest, # 返回完整的 manifest 对象
|
||||
"path": str(plugin_path.absolute())
|
||||
})
|
||||
|
||||
except json.JSONDecodeError as 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)} 个已安装插件")
|
||||
|
||||
return {"success": True, "plugins": installed_plugins, "total": len(installed_plugins)}
|
||||
return {
|
||||
"success": True,
|
||||
"plugins": installed_plugins,
|
||||
"total": len(installed_plugins)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取已安装插件列表失败: {e}", exc_info=True)
|
||||
|
|
|
|||
Loading…
Reference in New Issue