413 lines
14 KiB
Python
413 lines
14 KiB
Python
# ///
|
|
# plugins.py
|
|
# 描述:插件管理API路由
|
|
# 作者:AI Generated
|
|
# 创建日期:2026-04-05
|
|
# ///
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
from typing import List, Optional, Dict, Any
|
|
from pydantic import BaseModel
|
|
|
|
from plugins.base import PluginRegistry, PluginType
|
|
from utils import require_api_key
|
|
from database_service import get_db_service
|
|
|
|
|
|
router = APIRouter(prefix="/plugins", tags=["插件管理"])
|
|
|
|
|
|
class PluginConfigRequest(BaseModel):
|
|
name: str
|
|
config: Dict[str, Any]
|
|
|
|
|
|
def _sync_plugin_from_db(plugin_name: str):
|
|
db_service = get_db_service()
|
|
if not db_service:
|
|
return
|
|
plugin_config = db_service.get_plugin_config(plugin_name)
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
return
|
|
if plugin_config:
|
|
import json
|
|
plugin.initialize(json.loads(plugin_config.config_json) if plugin_config.config_json else {})
|
|
if plugin_config.enabled:
|
|
plugin.enable()
|
|
else:
|
|
plugin.disable()
|
|
|
|
|
|
@router.get("/", summary="获取所有插件")
|
|
async def get_plugins(_=Depends(require_api_key)) -> Dict[str, Any]:
|
|
import json
|
|
plugins = PluginRegistry.get_all()
|
|
result = []
|
|
for p in plugins:
|
|
info = p.get_info()
|
|
db_service = get_db_service()
|
|
if db_service:
|
|
pc = db_service.get_plugin_config(p.plugin_name)
|
|
if pc:
|
|
info["config"] = json.loads(pc.config_json) if pc.config_json else {}
|
|
info["enabled"] = pc.enabled
|
|
result.append(info)
|
|
return {
|
|
"success": True,
|
|
"plugins": result,
|
|
"count": len(result)
|
|
}
|
|
|
|
|
|
@router.get("/hotloaded", summary="获取热加载插件列表")
|
|
async def get_hotloaded_plugins(_=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
from plugins.plugin_loader import hot_plugin_loader
|
|
plugins = hot_plugin_loader.get_loaded_plugins()
|
|
return {
|
|
"success": True,
|
|
"plugins": plugins,
|
|
"count": len(plugins),
|
|
"plugin_dir": hot_plugin_loader.plugin_dir
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.post("/hotreload", summary="手动触发插件热重载")
|
|
async def trigger_hot_reload(_=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
from plugins.plugin_loader import hot_plugin_loader
|
|
hot_plugin_loader.scan_and_load_plugins()
|
|
plugins = hot_plugin_loader.get_loaded_plugins()
|
|
return {
|
|
"success": True,
|
|
"message": "Hot reload triggered",
|
|
"plugins": plugins
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.get("/type/{plugin_type}", summary="按类型获取插件")
|
|
async def get_plugins_by_type(plugin_type: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
ptype = PluginType(plugin_type)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail=f"Invalid plugin type: {plugin_type}")
|
|
|
|
plugins = PluginRegistry.get_by_type(ptype)
|
|
return {
|
|
"success": True,
|
|
"plugins": [p.get_info() for p in plugins],
|
|
"count": len(plugins)
|
|
}
|
|
|
|
|
|
@router.get("/custom/list", summary="列出自定义插件文件")
|
|
async def list_custom_plugins(_=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
import os
|
|
custom_dir = "/app/plugins/custom"
|
|
if not os.path.exists(custom_dir):
|
|
return {"success": True, "plugins": [], "count": 0}
|
|
|
|
files = []
|
|
for f in os.listdir(custom_dir):
|
|
if f.endswith('.py') and not f.startswith('_'):
|
|
filepath = os.path.join(custom_dir, f)
|
|
stat = os.stat(filepath)
|
|
files.append({
|
|
"name": f,
|
|
"size": stat.st_size,
|
|
"modified": stat.st_mtime,
|
|
"path": filepath
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"plugins": files,
|
|
"count": len(files),
|
|
"directory": custom_dir
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.post("/custom/create", summary="创建自定义插件文件")
|
|
async def create_custom_plugin(name: str, code: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
import os
|
|
custom_dir = "/app/plugins/custom"
|
|
os.makedirs(custom_dir, exist_ok=True)
|
|
|
|
safe_name = name.replace("..", "").replace("/", "").replace("\\", "")
|
|
if not safe_name.endswith(".py"):
|
|
safe_name += ".py"
|
|
|
|
filepath = os.path.join(custom_dir, safe_name)
|
|
with open(filepath, 'w') as f:
|
|
f.write(code)
|
|
|
|
from plugins.plugin_loader import hot_plugin_loader
|
|
hot_plugin_loader.scan_and_load_plugins()
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Plugin {safe_name} created",
|
|
"path": filepath
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.delete("/custom/{plugin_name}", summary="删除自定义插件")
|
|
async def delete_custom_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
import os
|
|
custom_dir = "/app/plugins/custom"
|
|
|
|
safe_name = plugin_name.replace("..", "").replace("/", "").replace("\\", "")
|
|
if not safe_name.endswith(".py"):
|
|
safe_name += ".py"
|
|
|
|
filepath = os.path.join(custom_dir, safe_name)
|
|
if os.path.exists(filepath):
|
|
os.remove(filepath)
|
|
|
|
from plugins.plugin_loader import hot_plugin_loader
|
|
hot_plugin_loader.scan_and_load_plugins()
|
|
|
|
return {"success": True, "message": f"Plugin {safe_name} deleted"}
|
|
else:
|
|
return {"success": False, "error": "Plugin file not found"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.get("/custom/{plugin_name}/download", summary="下载自定义插件源码")
|
|
async def download_custom_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
import os
|
|
custom_dir = "/app/plugins/custom"
|
|
|
|
safe_name = plugin_name.replace("..", "").replace("/", "").replace("\\", "")
|
|
if not safe_name.endswith(".py"):
|
|
safe_name += ".py"
|
|
|
|
filepath = os.path.join(custom_dir, safe_name)
|
|
if os.path.exists(filepath):
|
|
with open(filepath, 'r') as f:
|
|
code = f.read()
|
|
return {
|
|
"success": True,
|
|
"name": safe_name,
|
|
"code": code
|
|
}
|
|
else:
|
|
return {"success": False, "error": "Plugin file not found"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.get("/{plugin_name}", summary="获取插件详情")
|
|
async def get_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
import json
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
info = plugin.get_info()
|
|
db_service = get_db_service()
|
|
if db_service:
|
|
pc = db_service.get_plugin_config(plugin_name)
|
|
if pc:
|
|
info["config"] = json.loads(pc.config_json) if pc.config_json else {}
|
|
info["enabled"] = pc.enabled
|
|
return {"success": True, "plugin": info}
|
|
|
|
|
|
@router.post("/{plugin_name}/enable", summary="启用插件")
|
|
async def enable_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
PluginRegistry.enable(plugin_name)
|
|
db_service = get_db_service()
|
|
if db_service:
|
|
pc = db_service.get_plugin_config(plugin_name)
|
|
if pc:
|
|
db_service.update_plugin_enabled(plugin_name, True)
|
|
else:
|
|
db_service.save_plugin_config(plugin_name, plugin.config or {}, True)
|
|
return {"success": True, "message": f"Plugin '{plugin_name}' enabled"}
|
|
|
|
|
|
@router.post("/{plugin_name}/disable", summary="禁用插件")
|
|
async def disable_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
PluginRegistry.disable(plugin_name)
|
|
db_service = get_db_service()
|
|
if db_service:
|
|
pc = db_service.get_plugin_config(plugin_name)
|
|
if pc:
|
|
db_service.update_plugin_enabled(plugin_name, False)
|
|
else:
|
|
db_service.save_plugin_config(plugin_name, plugin.config or {}, False)
|
|
return {"success": True, "message": f"Plugin '{plugin_name}' disabled"}
|
|
|
|
|
|
@router.put("/{plugin_name}/config", summary="更新插件配置")
|
|
async def update_plugin_config(plugin_name: str,
|
|
request: PluginConfigRequest,
|
|
_=Depends(require_api_key)) -> Dict[str, Any]:
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
|
|
success = plugin.initialize(request.config)
|
|
if not success:
|
|
raise HTTPException(status_code=400, detail=f"Failed to initialize plugin '{plugin_name}'")
|
|
|
|
db_service = get_db_service()
|
|
if db_service:
|
|
pc = db_service.get_plugin_config(plugin_name)
|
|
enabled = pc.enabled if pc else False
|
|
db_service.save_plugin_config(plugin_name, request.config, enabled)
|
|
|
|
return {"success": True, "message": f"Plugin '{plugin_name}' config updated"}
|
|
|
|
|
|
@router.post("/{plugin_name}/execute", summary="执行插件")
|
|
async def execute_plugin(plugin_name: str,
|
|
request: Optional[Dict[str, Any]] = None,
|
|
_=Depends(require_api_key)) -> Dict[str, Any]:
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
|
|
if not plugin.enabled:
|
|
return {"success": False, "error": f"Plugin '{plugin_name}' is disabled"}
|
|
|
|
params = request or {}
|
|
result = plugin.execute(params)
|
|
return {"success": True, "result": result}
|
|
|
|
|
|
@router.get("/{plugin_name}/logs", summary="获取插件日志")
|
|
async def get_plugin_logs(
|
|
plugin_name: str,
|
|
lines: int = 100,
|
|
level: Optional[str] = None,
|
|
_=Depends(require_api_key)
|
|
) -> Dict[str, Any]:
|
|
try:
|
|
import logging
|
|
import os
|
|
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
|
|
log_dir = "/app/data/logs"
|
|
if not os.path.exists(log_dir):
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
log_file = os.path.join(log_dir, f"{plugin_name}.log")
|
|
app_log = os.path.join(log_dir, "app.log")
|
|
|
|
all_logs = []
|
|
|
|
if os.path.exists(log_file):
|
|
with open(log_file, 'r', encoding='utf-8') as f:
|
|
all_logs.extend(f.readlines())
|
|
|
|
if os.path.exists(app_log):
|
|
with open(app_log, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
if plugin_name in line or f"[{plugin_name}]" in line:
|
|
all_logs.append(line)
|
|
|
|
filtered_logs = []
|
|
for line in all_logs:
|
|
if level:
|
|
if level.upper() in line.upper():
|
|
filtered_logs.append(line.strip())
|
|
else:
|
|
filtered_logs.append(line.strip())
|
|
|
|
filtered_logs = filtered_logs[-lines:] if len(filtered_logs) > lines else filtered_logs
|
|
|
|
return {
|
|
"success": True,
|
|
"plugin_name": plugin_name,
|
|
"log_file": log_file,
|
|
"count": len(filtered_logs),
|
|
"logs": filtered_logs
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.get("/{plugin_name}/schema", summary="获取插件配置表单Schema")
|
|
async def get_plugin_schema(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
|
|
schema = None
|
|
if hasattr(plugin, 'get_config_schema'):
|
|
schema = plugin.get_config_schema()
|
|
elif hasattr(plugin, 'config_schema'):
|
|
schema = plugin.config_schema
|
|
|
|
if schema:
|
|
if callable(schema):
|
|
schema = schema()
|
|
if hasattr(schema, 'to_form_schema'):
|
|
return {
|
|
"success": True,
|
|
"schema": schema.to_form_schema(),
|
|
"has_form": True
|
|
}
|
|
return {
|
|
"success": True,
|
|
"schema": schema,
|
|
"has_form": True
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"schema": None,
|
|
"has_form": False,
|
|
"message": "此插件暂无表单配置"
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
@router.post("/{plugin_name}/logs/clear", summary="清空插件日志")
|
|
async def clear_plugin_logs(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
|
|
try:
|
|
import os
|
|
|
|
plugin = PluginRegistry.get(plugin_name)
|
|
if not plugin:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
|
|
|
|
log_file = f"/app/data/logs/{plugin_name}.log"
|
|
|
|
if os.path.exists(log_file):
|
|
with open(log_file, 'w') as f:
|
|
f.write("")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Plugin '{plugin_name}' logs cleared"
|
|
}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|