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
-5
View File
@@ -36,11 +36,6 @@ COPY . /fba
COPY --from=builder /usr/local /usr/local 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 === # === FastAPI server image ===
FROM base_server AS fastapi_server FROM base_server AS fastapi_server
+3 -4
View File
@@ -56,10 +56,9 @@ class PluginService:
if not file: if not file:
raise errors.RequestError(msg='ZIP 压缩包不能为空') raise errors.RequestError(msg='ZIP 压缩包不能为空')
return await install_zip_plugin(file) return await install_zip_plugin(file)
elif type == PluginType.git: if not repo_url:
if not repo_url: raise errors.RequestError(msg='Git 仓库地址不能为空')
raise errors.RequestError(msg='Git 仓库地址不能为空') return await install_git_plugin(repo_url)
return await install_git_plugin(repo_url)
@staticmethod @staticmethod
async def uninstall(*, plugin: str): async def uninstall(*, plugin: str):
+10 -25
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
from dataclasses import dataclass from dataclasses import dataclass
from typing import Annotated from typing import Annotated
@@ -7,45 +9,21 @@ import cappa
import uvicorn import uvicorn
from rich.panel import Panel from rich.panel import Panel
from rich.progress import (
Progress,
SpinnerColumn,
TextColumn,
TimeElapsedColumn,
)
from rich.text import Text from rich.text import Text
from backend import console, get_version from backend import console, get_version
from backend.common.exception.errors import BaseExceptionMixin from backend.common.exception.errors import BaseExceptionMixin
from backend.core.conf import settings 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._await import run_await
from backend.utils.file_ops import install_git_plugin, install_zip_plugin from backend.utils.file_ops import install_git_plugin, install_zip_plugin
def run(host: str, port: int, reload: bool, workers: int | None) -> None: 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}' url = f'http://{host}:{port}'
docs_url = url + settings.FASTAPI_DOCS_URL docs_url = url + settings.FASTAPI_DOCS_URL
redoc_url = url + settings.FASTAPI_REDOC_URL redoc_url = url + settings.FASTAPI_REDOC_URL
openapi_url = url + settings.FASTAPI_OPENAPI_URL openapi_url = url + settings.FASTAPI_OPENAPI_URL
console.print(Text('启动 fba 服务...', style='bold magenta'))
panel_content = Text() panel_content = Text()
panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue') panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue')
panel_content.append(f'📚 Redoc 文档: {redoc_url}\n', style='yellow') 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))) 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: def install_plugin(path: str, repo_url: str) -> None:
+24
View File
@@ -1,5 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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.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() app = register_app()
+26 -3
View File
@@ -8,11 +8,13 @@ import sys
import warnings import warnings
from functools import lru_cache from functools import lru_cache
from importlib.metadata import PackageNotFoundError, distribution
from typing import Any from typing import Any
import rtoml import rtoml
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from packaging.requirements import Requirement
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from backend.common.enums import StatusType from backend.common.enums import StatusType
@@ -33,6 +35,10 @@ class PluginInjectError(Exception):
"""插件注入错误""" """插件注入错误"""
class PluginInstallError(Exception):
"""插件安装错误"""
@lru_cache @lru_cache
def get_plugins() -> list[str]: def get_plugins() -> list[str]:
"""获取插件列表""" """获取插件列表"""
@@ -262,7 +268,24 @@ def install_requirements(plugin: str | None) -> None:
for plugin in plugins: for plugin in plugins:
requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt') requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt')
missing_dependencies = False
if os.path.exists(requirements_file): 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: try:
ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade'] ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade']
pip_install = [sys.executable, '-m', 'pip', 'install', '-r', requirements_file] 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(ensurepip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.check_call(pip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.check_call(pip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError as e: 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: def uninstall_requirements(plugin: str) -> None:
@@ -285,9 +308,9 @@ def uninstall_requirements(plugin: str) -> None:
if os.path.exists(requirements_file): if os.path.exists(requirements_file):
try: try:
pip_uninstall = [sys.executable, '-m', 'pip', 'uninstall', '-r', requirements_file, '-y'] 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: 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: async def install_requirements_async(plugin: str | None = None) -> None:
+9 -3
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import uvicorn import uvicorn
if __name__ == '__main__': if __name__ == '__main__':
@@ -7,8 +9,12 @@ if __name__ == '__main__':
# 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件 # 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件
# 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务 # 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
try: try:
config = uvicorn.Config(app='backend.main:app', reload=True) uvicorn.run(
server = uvicorn.Server(config) app='backend.main:app',
server.run() host='127.0.0.1',
port=8000,
reload=True,
reload_excludes=[os.path.abspath('../.venv')],
)
except Exception as e: except Exception as e:
raise e raise e
-15
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