From a461f782243e3c5b21d5bc8f907ec0068afdb867 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 30 Jun 2025 09:29:35 +0800 Subject: [PATCH] Optimize the installation of plugin dependencies (#700) * Optimize the installation of plugin dependencies * Remove enumerate * Update main params --- Dockerfile | 5 --- backend/app/admin/service/plugin_service.py | 7 ++--- backend/cli.py | 35 ++++++--------------- backend/main.py | 24 ++++++++++++++ backend/plugin/tools.py | 29 +++++++++++++++-- backend/run.py | 12 +++++-- backend/scripts/init_plugin.py | 15 --------- 7 files changed, 72 insertions(+), 55 deletions(-) delete mode 100644 backend/scripts/init_plugin.py diff --git a/Dockerfile b/Dockerfile index 60d016dd..93775879 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,11 +36,6 @@ COPY . /fba COPY --from=builder /usr/local /usr/local -# Install plugin dependencies -WORKDIR /fba -ENV PYTHONPATH=/fba -RUN python3 backend/scripts/init_plugin.py - # === FastAPI server image === FROM base_server AS fastapi_server diff --git a/backend/app/admin/service/plugin_service.py b/backend/app/admin/service/plugin_service.py index cd295d64..5f4225db 100644 --- a/backend/app/admin/service/plugin_service.py +++ b/backend/app/admin/service/plugin_service.py @@ -56,10 +56,9 @@ class PluginService: if not file: raise errors.RequestError(msg='ZIP 压缩包不能为空') return await install_zip_plugin(file) - elif type == PluginType.git: - if not repo_url: - raise errors.RequestError(msg='Git 仓库地址不能为空') - return await install_git_plugin(repo_url) + if not repo_url: + raise errors.RequestError(msg='Git 仓库地址不能为空') + return await install_git_plugin(repo_url) @staticmethod async def uninstall(*, plugin: str): diff --git a/backend/cli.py b/backend/cli.py index 003b69fd..e65fe286 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os + from dataclasses import dataclass from typing import Annotated @@ -7,45 +9,21 @@ import cappa import uvicorn from rich.panel import Panel -from rich.progress import ( - Progress, - SpinnerColumn, - TextColumn, - TimeElapsedColumn, -) from rich.text import Text from backend import console, get_version from backend.common.exception.errors import BaseExceptionMixin from backend.core.conf import settings -from backend.plugin.tools import get_plugins, install_requirements from backend.utils._await import run_await from backend.utils.file_ops import install_git_plugin, install_zip_plugin def run(host: str, port: int, reload: bool, workers: int | None) -> None: - console.print(Text('检测插件依赖...', style='bold cyan')) - - plugins = get_plugins() - - with Progress( - SpinnerColumn(finished_text='[bold green]插件依赖安装完成[/]'), - TextColumn('[green]{task.completed}/{task.total}[/]'), - TimeElapsedColumn(), - console=console, - ) as progress: - task = progress.add_task('安装插件依赖...', total=len(plugins)) - for i, plugin in enumerate(plugins): - install_requirements(plugin) - progress.advance(task) - url = f'http://{host}:{port}' docs_url = url + settings.FASTAPI_DOCS_URL redoc_url = url + settings.FASTAPI_REDOC_URL openapi_url = url + settings.FASTAPI_OPENAPI_URL - console.print(Text('启动 fba 服务...', style='bold magenta')) - panel_content = Text() panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue') panel_content.append(f'📚 Redoc 文档: {redoc_url}\n', style='yellow') @@ -56,7 +34,14 @@ def run(host: str, port: int, reload: bool, workers: int | None) -> None: ) console.print(Panel(panel_content, title='fba 服务信息', border_style='purple', padding=(1, 2))) - uvicorn.run(app='backend.main:app', host=host, port=port, reload=not reload, workers=workers) + uvicorn.run( + app='backend.main:app', + host=host, + port=port, + reload=not reload, + reload_excludes=[os.path.abspath('../.venv' if 'backend' in os.getcwd() else '.venv')], + workers=workers, + ) def install_plugin(path: str, repo_url: str) -> None: diff --git a/backend/main.py b/backend/main.py index 656778e0..7de536aa 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn +from rich.text import Text + +from backend import console from backend.core.registrar import register_app +from backend.plugin.tools import get_plugins, install_requirements +from backend.utils.timezone import timezone + +_print_log_style = f'{timezone.to_str(timezone.now(), "%Y-%m-%d %H:%M:%S.%M0")} | fba | - | ' +console.print(Text(f'{_print_log_style}检测插件依赖...', style='bold cyan')) + +_plugins = get_plugins() + +with Progress( + SpinnerColumn(finished_text=f'[bold green]{_print_log_style}插件准备就绪[/]'), + TextColumn('[bold green]{task.completed}/{task.total}[/]'), + TimeElapsedColumn(), + console=console, +) as progress: + task = progress.add_task('安装插件依赖...', total=len(_plugins)) + for plugin in _plugins: + install_requirements(plugin) + progress.advance(task) + +console.print(Text(f'{_print_log_style}启动服务...', style='bold magenta')) app = register_app() diff --git a/backend/plugin/tools.py b/backend/plugin/tools.py index 56c046f5..a57a0034 100644 --- a/backend/plugin/tools.py +++ b/backend/plugin/tools.py @@ -8,11 +8,13 @@ import sys import warnings from functools import lru_cache +from importlib.metadata import PackageNotFoundError, distribution from typing import Any import rtoml from fastapi import APIRouter, Depends, Request +from packaging.requirements import Requirement from starlette.concurrency import run_in_threadpool from backend.common.enums import StatusType @@ -33,6 +35,10 @@ class PluginInjectError(Exception): """插件注入错误""" +class PluginInstallError(Exception): + """插件安装错误""" + + @lru_cache def get_plugins() -> list[str]: """获取插件列表""" @@ -262,7 +268,24 @@ def install_requirements(plugin: str | None) -> None: for plugin in plugins: requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt') + missing_dependencies = False if os.path.exists(requirements_file): + with open(requirements_file, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + try: + req = Requirement(line) + dependency = req.name.lower() + except Exception as e: + raise PluginInstallError(f'插件 {plugin} 依赖 {line} 格式错误: {str(e)}') from e + try: + distribution(dependency) + except PackageNotFoundError: + missing_dependencies = True + + if missing_dependencies: try: ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade'] pip_install = [sys.executable, '-m', 'pip', 'install', '-r', requirements_file] @@ -271,7 +294,7 @@ def install_requirements(plugin: str | None) -> None: subprocess.check_call(ensurepip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.check_call(pip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError as e: - raise PluginInjectError(f'插件 {plugin} 依赖安装失败:{e.stderr}') from e + raise PluginInstallError(f'插件 {plugin} 依赖安装失败:{e}') from e def uninstall_requirements(plugin: str) -> None: @@ -285,9 +308,9 @@ def uninstall_requirements(plugin: str) -> None: if os.path.exists(requirements_file): try: pip_uninstall = [sys.executable, '-m', 'pip', 'uninstall', '-r', requirements_file, '-y'] - subprocess.check_call(pip_uninstall) + subprocess.check_call(pip_uninstall, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError as e: - raise PluginInjectError(f'插件 {plugin} 依赖卸载失败:{e.stderr}') from e + raise PluginInstallError(f'插件 {plugin} 依赖卸载失败:{e}') from e async def install_requirements_async(plugin: str | None = None) -> None: diff --git a/backend/run.py b/backend/run.py index c6262402..3aeb3fa9 100644 --- a/backend/run.py +++ b/backend/run.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os + import uvicorn if __name__ == '__main__': @@ -7,8 +9,12 @@ if __name__ == '__main__': # 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件 # 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务 try: - config = uvicorn.Config(app='backend.main:app', reload=True) - server = uvicorn.Server(config) - server.run() + uvicorn.run( + app='backend.main:app', + host='127.0.0.1', + port=8000, + reload=True, + reload_excludes=[os.path.abspath('../.venv')], + ) except Exception as e: raise e diff --git a/backend/scripts/init_plugin.py b/backend/scripts/init_plugin.py deleted file mode 100644 index 27136a5b..00000000 --- a/backend/scripts/init_plugin.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from anyio import run - -from backend.plugin.tools import install_requirements_async - - -async def init() -> None: - print('Starting initial plugin') - await install_requirements_async() - print('Plugin successfully installed') - - -if __name__ == '__main__': - run(init) # type: ignore