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

View File

@@ -1,6 +1,5 @@
import inspect
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 starlette import status
@@ -10,7 +9,7 @@ from app import schemas
from app.command import Command
from app.core.config import settings
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.user_oper import get_current_active_superuser
from app.factory import app
@@ -221,6 +220,16 @@ def install(plugin_id: str,
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="获取插件表单页面")
def plugin_form(plugin_id: str,
_: 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} 不存在或未加载")
# 渲染模式
render_mode = plugin_instance.get_render_mode()
if render_mode == "vue":
# Vue模式
try:
vue_component_file, model = plugin_instance.get_form_file()
return {
"render_mode": "vue",
"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_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)}")
render_mode, _ = plugin_instance.get_render_mode()
try:
conf, model = plugin_instance.get_form()
return {
"render_mode": render_mode,
"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 {}
@@ -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} 不存在或未加载")
# 渲染模式
render_mode = plugin_instance.get_render_mode()
if render_mode == "vue":
# Vue模式
try:
vue_component_file = plugin_instance.get_page_file()
return {
"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_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)}")
render_mode, _ = plugin_instance.get_render_mode()
try:
page = plugin_instance.get_page()
return {
"render_mode": render_mode,
"page": page or []
}
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_page 出错: {str(e)}")
return {}
@@ -302,62 +286,11 @@ def plugin_dashboard_meta(_: schemas.TokenPayload = Depends(verify_token)) -> Li
@router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置")
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获取插件仪表板
"""
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)}")
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
@router.get("/dashboard/{plugin_id}", summary="获取插件仪表板配置")
@@ -390,8 +323,8 @@ def reset_plugin(plugin_id: str,
return schemas.Response(success=True)
@router.get("/file/{plugin_id}/{filepath:path}", summary="获取插件静态文件")
def plugin_static_file(plugin_id: str, filepath: str, _: schemas.TokenPayload = Depends(verify_token)):
@router.get("/file/{plugin_id}/{filepath:path}", summary="获取插件静态文件")
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 类型
if suffix in ['.js', '.mjs']:
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'
elif not response_type: # 对于其他猜不出的类型
elif not response_type: # 对于其他猜不出的类型
response_type = 'application/octet-stream'
try:

View File

@@ -1,13 +1,16 @@
import concurrent
import concurrent.futures
import importlib.util
import inspect
import os
import time
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
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.observers import Observer
@@ -526,7 +529,40 @@ class PluginManager(metaclass=Singleton):
logger.error(f"获取插件 {plugin_id} 模块出错:{str(e)}")
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)}")
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:
"""
获取插件属性

View File

@@ -84,12 +84,12 @@ class _PluginBase(metaclass=ABCMeta):
pass
@staticmethod
def get_render_mode() -> str:
def get_render_mode() -> Tuple[str, Optional[str]]:
"""
获取插件渲染模式
:return: 渲染模式支持vue/vuetify默认vuetify
:return: 1、渲染模式支持vue/vuetify默认vuetify2、vue模式下编译后文件的相对路径默认为`dist`vuetify模式下为None
"""
return "vuetify"
return "vuetify", None
@abstractmethod
def get_api(self) -> List[Dict[str, Any]]:
@@ -106,34 +106,19 @@ class _PluginBase(metaclass=ABCMeta):
pass
@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/
:return: 1、页面配置2、默认数据结构
"""
pass
@staticmethod
def get_form_file() -> Tuple[str, Dict[str, Any]]:
"""
获取插件配置页面JS代码源文件与get_from二选一使用
:return: 1、编译后的JS代码插件目录下相对路径2、默认数据结构
:return: 1、页面配置vuetify模式或 Nonevue模式2、默认数据结构
"""
pass
@abstractmethod
def get_page(self) -> List[dict]:
def get_page(self) -> Optional[List[dict]]:
"""
拼装插件详情页面,需要返回页面配置,同时附带数据
插件详情页面使用Vuetify组件拼装参考https://vuetifyjs.com/
"""
pass
@staticmethod
def get_page_file() -> Optional[str]:
"""
获取插件数据页面JS代码源文件与get_page二选一使用
:return: 编译后的JS代码插件目录下相对路径
:return: 页面配置vuetify模式或 Nonevue模式
"""
pass
@@ -150,9 +135,9 @@ class _PluginBase(metaclass=ABCMeta):
"""
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配置参考
{
"cols": 12, "md": 6
@@ -164,7 +149,7 @@ class _PluginBase(metaclass=ABCMeta):
"title": "组件标题", // 组件标题,如有将显示该标题,否则显示插件名称
"subtitle": "组件子标题", // 组件子标题,缺省时不展示子标题
}
3、页面配置使用Vuetify组件拼装参考https://vuetifyjs.com/
3、vuetify模式页面配置使用Vuetify组件拼装参考https://vuetifyjs.com/vue模式为None
kwargs参数可获取的值1、user_agent浏览器UA
@@ -172,13 +157,6 @@ class _PluginBase(metaclass=ABCMeta):
"""
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]]]:
"""
获取插件仪表盘元信息

View File

@@ -67,5 +67,3 @@ class PluginDashboard(Plugin):
cols: Optional[dict] = Field(default_factory=dict)
# 页面元素
elements: Optional[List[dict]] = Field(default_factory=list)
# 页面地址
component_url: Optional[str] = None