Files
wxauto_api/api/plugins.py
T
2026-04-07 14:11:45 +08:00

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)}