Optimize the installation of plugin dependencies (#700)

* Optimize the installation of plugin dependencies

* Remove enumerate

* Update main params
This commit is contained in:
Wu Clan
2025-06-30 09:29:35 +08:00
committed by GitHub
parent c306432708
commit a461f78224
7 changed files with 72 additions and 55 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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()

View File

@@ -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:

View File

@@ -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

View File

@@ -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