fix plugins

This commit is contained in:
jxxghp
2025-05-06 11:44:23 +08:00
parent 8ccd1f5fe4
commit d2e5367dc6
4 changed files with 128 additions and 140 deletions
+37 -104
View File
@@ -1,6 +1,5 @@
import inspect
import mimetypes import mimetypes
from typing import Annotated, Any, List, Optional, Callable, Tuple from typing import Annotated, Any, List, Optional
from fastapi import APIRouter, Depends, Header, HTTPException from fastapi import APIRouter, Depends, Header, HTTPException
from starlette import status from starlette import status
@@ -10,7 +9,7 @@ from app import schemas
from app.command import Command from app.command import Command
from app.core.config import settings from app.core.config import settings
from app.core.plugin import PluginManager from app.core.plugin import PluginManager
from app.core.security import verify_apikey, verify_token from app.core.security import verify_apikey, verify_token, verify_apitoken
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.db.user_oper import get_current_active_superuser from app.db.user_oper import get_current_active_superuser
from app.factory import app from app.factory import app
@@ -221,6 +220,16 @@ def install(plugin_id: str,
return schemas.Response(success=True) return schemas.Response(success=True)
@router.get("/remotes", summary="获取插件联邦组件列表", response_model=List[dict])
def remotes(token: str) -> Any:
"""
获取插件联邦组件列表
"""
if token != "moviepilot":
raise HTTPException(status_code=403, detail="Forbidden")
return PluginManager().get_plugin_remotes()
@router.get("/form/{plugin_id}", summary="获取插件表单页面") @router.get("/form/{plugin_id}", summary="获取插件表单页面")
def plugin_form(plugin_id: str, def plugin_form(plugin_id: str,
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> dict: _: schemas.TokenPayload = Depends(get_current_active_superuser)) -> dict:
@@ -232,29 +241,16 @@ def plugin_form(plugin_id: str,
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载")
# 渲染模式 # 渲染模式
render_mode = plugin_instance.get_render_mode() render_mode, _ = plugin_instance.get_render_mode()
if render_mode == "vue": try:
# Vue模式 conf, model = plugin_instance.get_form()
try: return {
vue_component_file, model = plugin_instance.get_form_file() "render_mode": render_mode,
return { "conf": conf,
"render_mode": "vue", "model": PluginManager().get_plugin_config(plugin_id) or model
"component_url": f"/plugin/file/{plugin_id.lower()}/{vue_component_file}", }
"model": PluginManager().get_plugin_config(plugin_id) or model except Exception as e:
} logger.error(f"插件 {plugin_id} 调用方法 get_form 出错: {str(e)}")
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_form_file 出错: {str(e)}")
else:
# Vuetify模式
try:
conf, model = plugin_instance.get_form()
return {
"render_mode": "vuetify",
"conf": conf,
"model": PluginManager().get_plugin_config(plugin_id) or model
}
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_form 出错: {str(e)}")
return {} return {}
@@ -268,27 +264,15 @@ def plugin_page(plugin_id: str, _: schemas.TokenPayload = Depends(get_current_ac
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载")
# 渲染模式 # 渲染模式
render_mode = plugin_instance.get_render_mode() render_mode, _ = plugin_instance.get_render_mode()
if render_mode == "vue": try:
# Vue模式 page = plugin_instance.get_page()
try: return {
vue_component_file = plugin_instance.get_page_file() "render_mode": render_mode,
return { "page": page or []
"render_mode": "vue", }
"component_url": f"/plugin/file/{plugin_id.lower()}/{vue_component_file}", except Exception as e:
} logger.error(f"插件 {plugin_id} 调用方法 get_page 出错: {str(e)}")
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_page_file 出错: {str(e)}")
else:
try:
page = plugin_instance.get_page()
return {
"render_mode": "vuetify",
"page": page or []
}
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_page 出错: {str(e)}")
return {} return {}
@@ -302,62 +286,11 @@ def plugin_dashboard_meta(_: schemas.TokenPayload = Depends(verify_token)) -> Li
@router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置") @router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置")
def plugin_dashboard_by_key(plugin_id: str, key: str, user_agent: Annotated[str | None, Header()] = None, def plugin_dashboard_by_key(plugin_id: str, key: str, user_agent: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Optional[schemas.PluginDashboard]: _: schemas.TokenPayload = Depends(verify_token)) -> Optional[schemas.PluginDashboard]:
""" """
根据插件ID获取插件仪表板 根据插件ID获取插件仪表板
""" """
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
def __get_params_count(func: Callable):
"""
获取函数的参数信息
"""
signature = inspect.signature(func)
return len(signature.parameters)
# 获取插件实例
plugin_instance = PluginManager().running_plugins.get(plugin_id)
if not plugin_instance:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载")
# 渲染模式
render_mode = plugin_instance.get_render_mode()
if render_mode == "vue":
# Vue模式
try:
cols, attrs, vue_component_file = plugin_instance.get_dashboard_file(key=key, user_agent=user_agent)
return schemas.PluginDashboard(
id=plugin_id,
name=plugin_instance.plugin_name,
key=key,
render_mode=render_mode,
cols=cols or {},
attrs=attrs or {},
component_url=f"/plugin/file/{plugin_id.lower()}/{vue_component_file}",
)
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_dashboard_file 出错: {str(e)}")
else:
try:
# 检查方法的参数个数
params_count = __get_params_count(plugin_instance.get_dashboard)
if params_count > 1:
dashboard: Tuple = plugin_instance.get_dashboard(key=key, user_agent=user_agent)
elif params_count > 0:
dashboard: Tuple = plugin_instance.get_dashboard(user_agent=user_agent)
else:
dashboard: Tuple = plugin_instance.get_dashboard()
if dashboard:
cols, attrs, elements = dashboard
return schemas.PluginDashboard(
id=plugin_id,
name=plugin_instance.plugin_name,
key=key,
render_mode="vuetify",
cols=cols or {},
elements=elements,
attrs=attrs or {}
)
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_dashboard 出错: {str(e)}")
@router.get("/dashboard/{plugin_id}", summary="获取插件仪表板配置") @router.get("/dashboard/{plugin_id}", summary="获取插件仪表板配置")
@@ -390,8 +323,8 @@ def reset_plugin(plugin_id: str,
return schemas.Response(success=True) return schemas.Response(success=True)
@router.get("/file/{plugin_id}/{filepath:path}", summary="获取插件静态文件") @router.get("/file/{plugin_id}/{filepath:path}", summary="获取插件静态文件")
def plugin_static_file(plugin_id: str, filepath: str, _: schemas.TokenPayload = Depends(verify_token)): def plugin_static_file(plugin_id: str, filepath: str, _: Annotated[str, Depends(verify_apitoken)]):
""" """
获取插件静态文件 获取插件静态文件
""" """
@@ -413,9 +346,9 @@ def plugin_static_file(plugin_id: str, filepath: str, _: schemas.TokenPayload =
# 强制修正 .mjs 和 .js 的 MIME 类型 # 强制修正 .mjs 和 .js 的 MIME 类型
if suffix in ['.js', '.mjs']: if suffix in ['.js', '.mjs']:
response_type = 'application/javascript' response_type = 'application/javascript'
elif suffix == '.css' and not response_type: # 如果 guess_type 没猜对 css,也修正 elif suffix == '.css' and not response_type: # 如果 guess_type 没猜对 css,也修正
response_type = 'text/css' response_type = 'text/css'
elif not response_type: # 对于其他猜不出的类型 elif not response_type: # 对于其他猜不出的类型
response_type = 'application/octet-stream' response_type = 'application/octet-stream'
try: try:
+81 -2
View File
@@ -1,13 +1,16 @@
import concurrent import concurrent
import concurrent.futures import concurrent.futures
import importlib.util import importlib.util
import inspect
import os import os
import time import time
import traceback import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Type, Union from typing import Any, Dict, List, Optional, Type, Union, Callable, Tuple
from fastapi import HTTPException
from starlette import status
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer from watchdog.observers import Observer
@@ -526,7 +529,40 @@ class PluginManager(metaclass=Singleton):
logger.error(f"获取插件 {plugin_id} 模块出错:{str(e)}") logger.error(f"获取插件 {plugin_id} 模块出错:{str(e)}")
return ret_modules return ret_modules
def get_plugin_dashboard_meta(self): @staticmethod
def get_plugin_remote_entry(plugin_id: str, dist_path: str) -> str:
"""
获取插件的远程入口地址
:param plugin_id: 插件 ID
:param dist_path: 插件的分发路径
:return: 远程入口地址
"""
if dist_path.startswith("/"):
dist_path = dist_path[1:]
if dist_path.endswith("/"):
dist_path = dist_path[:-1]
return f"/plugin/file/{plugin_id.lower()}/{dist_path}/remoteEntry.js?token={settings.API_TOKEN}"
def get_plugin_remotes(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件联邦组件列表
"""
remotes = []
for plugin_id, plugin in self._running_plugins.items():
if pid and pid != plugin_id:
continue
if hasattr(plugin, "get_render_mode"):
render_mode, dist_path = plugin.get_render_mode()
if render_mode != "vue":
continue
remotes.append({
"id": plugin_id,
"url": self.get_plugin_remote_entry(plugin_id, dist_path),
"name": plugin.plugin_name,
})
return remotes
def get_plugin_dashboard_meta(self) -> List[Dict[str, str]]:
""" """
获取所有插件仪表盘元信息 获取所有插件仪表盘元信息
""" """
@@ -556,6 +592,49 @@ class PluginManager(metaclass=Singleton):
logger.error(f"获取插件[{plugin_id}]仪表盘元数据出错:{str(e)}") logger.error(f"获取插件[{plugin_id}]仪表盘元数据出错:{str(e)}")
return dashboard_meta return dashboard_meta
def get_plugin_dashboard(self, pid: str, key: str, user_agent: str = None) -> schemas.PluginDashboard:
"""
获取插件仪表盘
"""
def __get_params_count(func: Callable):
"""
获取函数的参数信息
"""
signature = inspect.signature(func)
return len(signature.parameters)
# 获取插件实例
plugin_instance = self.running_plugins.get(pid)
if not plugin_instance:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {pid} 不存在或未加载")
# 渲染模式
render_mode, _ = plugin_instance.get_render_mode()
# 获取插件仪表板
try:
# 检查方法的参数个数
params_count = __get_params_count(plugin_instance.get_dashboard)
if params_count > 1:
dashboard: Tuple = plugin_instance.get_dashboard(key=key, user_agent=user_agent)
elif params_count > 0:
dashboard: Tuple = plugin_instance.get_dashboard(user_agent=user_agent)
else:
dashboard: Tuple = plugin_instance.get_dashboard()
except Exception as e:
logger.error(f"插件 {pid} 调用方法 get_dashboard 出错: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"插件 {pid} 调用方法 get_dashboard 出错: {str(e)}")
cols, attrs, elements = dashboard
return schemas.PluginDashboard(
id=pid,
name=plugin_instance.plugin_name,
key=key,
render_mode=render_mode,
cols=cols or {},
attrs=attrs or {},
)
def get_plugin_attr(self, pid: str, attr: str) -> Any: def get_plugin_attr(self, pid: str, attr: str) -> Any:
""" """
获取插件属性 获取插件属性
+10 -32
View File
@@ -84,12 +84,12 @@ class _PluginBase(metaclass=ABCMeta):
pass pass
@staticmethod @staticmethod
def get_render_mode() -> str: def get_render_mode() -> Tuple[str, Optional[str]]:
""" """
获取插件渲染模式 获取插件渲染模式
:return: 渲染模式,支持:vue/vuetify,默认vuetify :return: 1、渲染模式,支持:vue/vuetify,默认vuetify;2、vue模式下编译后文件的相对路径,默认为`dist`,vuetify模式下为None
""" """
return "vuetify" return "vuetify", None
@abstractmethod @abstractmethod
def get_api(self) -> List[Dict[str, Any]]: def get_api(self) -> List[Dict[str, Any]]:
@@ -106,34 +106,19 @@ class _PluginBase(metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: def get_form(self) -> Tuple[Optional[List[dict]], Dict[str, Any]]:
""" """
拼装插件配置页面,插件配置页面使用Vuetify组件拼装,参考:https://vuetifyjs.com/ 拼装插件配置页面,插件配置页面使用Vuetify组件拼装,参考:https://vuetifyjs.com/
:return: 1、页面配置;2、默认数据结构 :return: 1、页面配置vuetify模式)或 Nonevue模式)2、默认数据结构
"""
pass
@staticmethod
def get_form_file() -> Tuple[str, Dict[str, Any]]:
"""
获取插件配置页面JS代码源文件(与get_from二选一使用)
:return: 1、编译后的JS代码插件目录下相对路径;2、默认数据结构
""" """
pass pass
@abstractmethod @abstractmethod
def get_page(self) -> List[dict]: def get_page(self) -> Optional[List[dict]]:
""" """
拼装插件详情页面,需要返回页面配置,同时附带数据 拼装插件详情页面,需要返回页面配置,同时附带数据
插件详情页面使用Vuetify组件拼装,参考:https://vuetifyjs.com/ 插件详情页面使用Vuetify组件拼装,参考:https://vuetifyjs.com/
""" :return: 页面配置(vuetify模式)或 Nonevue模式)
pass
@staticmethod
def get_page_file() -> Optional[str]:
"""
获取插件数据页面JS代码源文件(与get_page二选一使用)
:return: 编译后的JS代码插件目录下相对路径
""" """
pass pass
@@ -150,9 +135,9 @@ class _PluginBase(metaclass=ABCMeta):
""" """
pass pass
def get_dashboard(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: def get_dashboard(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], Optional[List[dict]]]]:
""" """
获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(布局、自动刷新等);3、仪表板页面元素配置json(含数据 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(布局、自动刷新等);3、仪表板页面元素配置含数据jsonvuetify)或 Nonevue模式
1、col配置参考: 1、col配置参考:
{ {
"cols": 12, "md": 6 "cols": 12, "md": 6
@@ -164,7 +149,7 @@ class _PluginBase(metaclass=ABCMeta):
"title": "组件标题", // 组件标题,如有将显示该标题,否则显示插件名称 "title": "组件标题", // 组件标题,如有将显示该标题,否则显示插件名称
"subtitle": "组件子标题", // 组件子标题,缺省时不展示子标题 "subtitle": "组件子标题", // 组件子标题,缺省时不展示子标题
} }
3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ 3、vuetify模式页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/vue模式为None
kwargs参数可获取的值:1、user_agent:浏览器UA kwargs参数可获取的值:1、user_agent:浏览器UA
@@ -172,13 +157,6 @@ class _PluginBase(metaclass=ABCMeta):
""" """
pass pass
def get_dashboard_file(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], str]]:
"""
获取插件仪表盘页面JS代码源文件(与get_dashboard二选一使用)
:return: 1、全局配置(布局、自动刷新等);2、仪表板页面元素配置json(含数据);3、编译后的JS代码插件目录下相对路径
"""
pass
def get_dashboard_meta(self) -> Optional[List[Dict[str, str]]]: def get_dashboard_meta(self) -> Optional[List[Dict[str, str]]]:
""" """
获取插件仪表盘元信息 获取插件仪表盘元信息
-2
View File
@@ -67,5 +67,3 @@ class PluginDashboard(Plugin):
cols: Optional[dict] = Field(default_factory=dict) cols: Optional[dict] = Field(default_factory=dict)
# 页面元素 # 页面元素
elements: Optional[List[dict]] = Field(default_factory=list) elements: Optional[List[dict]] = Field(default_factory=list)
# 页面地址
component_url: Optional[str] = None