Files
MoviePilot/app/application/plugins.py
T

143 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""插件 API 动态路由服务。
把插件 API 的动态注册/移除从 HTTP 端点层下沉到 application 层:
FastAPI 实例由组合根(factory 创建应用后)注入,端点与 Agent 工具
统一经本模块操作路由,消除 api.endpoints 对 factory 的反向依赖。
依赖方向:
api.endpoints.plugin / agent.tools -> application.plugins <- factory(注入实例)
"""
from typing import Optional
from fastapi import FastAPI
from app.adapters.web.plugin.routes import FastAPIDynamicRouteRegistry
from app.application.security.access import verify_apikey, verify_token
from app.db.oper.systemconfig import SystemConfigOper
from app.runtime.config import settings
from app.runtime.extensions.plugin_manager import PluginManager
from app.runtime.log import logger
from app.schemas.types import SystemConfigKey
PROTECTED_ROUTES = {
"/api/v1/openapi.json",
"/docs",
"/docs/oauth2-redirect",
"/redoc",
}
PLUGIN_PREFIX = f"{settings.API_V1_STR}/plugin"
# FastAPI 应用实例:由 factory 在创建应用后调用 register_api_app 注入。
_api_app: Optional[FastAPI] = None
def register_api_app(api_app: FastAPI) -> None:
"""注入 FastAPI 应用实例(组合根在创建应用后调用)。"""
global _api_app
_api_app = api_app
def get_api_app() -> FastAPI:
"""返回已注入的 FastAPI 应用实例。"""
if _api_app is None:
raise RuntimeError("插件路由服务未初始化:请先调用 register_api_app 注入应用实例")
return _api_app
def _route_registry() -> FastAPIDynamicRouteRegistry:
"""组装绑定当前 FastAPI 应用与插件管理器的动态路由适配器。"""
return FastAPIDynamicRouteRegistry(
app=get_api_app(),
plugin_ids=lambda: PluginManager().get_running_plugin_ids(),
plugin_apis=lambda plugin_id: PluginManager().get_plugin_apis(plugin_id),
verify_token=verify_token,
verify_apikey=verify_apikey,
prefix=PLUGIN_PREFIX,
protected_routes=PROTECTED_ROUTES,
log=logger,
)
def register_plugin_api(plugin_id: Optional[str] = None) -> None:
"""
动态注册插件 API
:param plugin_id: 插件 ID,如果为 None,则注册所有插件
"""
_update_plugin_api_routes(plugin_id, action="add")
def remove_plugin_api(plugin_id: str) -> None:
"""
动态移除单个插件的 API
:param plugin_id: 插件 ID
"""
_update_plugin_api_routes(plugin_id, action="remove")
def _update_plugin_api_routes(plugin_id: Optional[str], action: str) -> None:
"""
插件 API 路由注册和移除
:param plugin_id: 插件 ID,如果 action 为 "add" 且 plugin_id 为 None,则处理所有插件
如果 action 为 "remove"plugin_id 必须是有效的插件 ID
:param action: "add" 或 "remove",决定是添加还是移除路由
"""
_route_registry().update(plugin_id, action)
def _remove_routes(plugin_id: str) -> bool:
"""
移除与单个插件相关的路由
:param plugin_id: 插件 ID
:return: 是否有路由被移除
"""
return _route_registry().remove(plugin_id)
def _clean_protected_routes(existing_paths: dict) -> None:
"""
清理受保护的路由,防止在插件操作中被删除或重复添加
:param existing_paths: 当前应用的路由路径映射
"""
_route_registry().clean(existing_paths)
def remove_plugin_from_folders(plugin_id: str):
"""
从所有文件夹中移除指定的插件
:param plugin_id: 要移除的插件ID
"""
try:
config_oper = SystemConfigOper()
# 获取插件文件夹配置
folders = config_oper.get(SystemConfigKey.PluginFolders) or {}
# 标记是否有修改
modified = False
# 遍历所有文件夹,移除指定插件
for folder_name, folder_data in folders.items():
if isinstance(folder_data, dict) and "plugins" in folder_data:
# 新格式:{"plugins": [...], "order": ..., "icon": ...}
if plugin_id in folder_data["plugins"]:
folder_data["plugins"].remove(plugin_id)
logger.info(f"已从文件夹 '{folder_name}' 中移除插件 {plugin_id}")
modified = True
elif isinstance(folder_data, list):
# 旧格式:直接是插件列表
if plugin_id in folder_data:
folder_data.remove(plugin_id)
logger.info(f"已从文件夹 '{folder_name}' 中移除插件 {plugin_id}")
modified = True
# 如果有修改,保存更新后的文件夹配置
if modified:
config_oper.set(SystemConfigKey.PluginFolders, folders)
else:
logger.debug(f"插件 {plugin_id} 不在任何文件夹中,无需移除")
except Exception as e:
logger.error(f"从文件夹中移除插件时出错:{str(e)}")
# 文件夹处理失败不影响插件卸载的整体流程