223 lines
7.5 KiB
Python
223 lines
7.5 KiB
Python
"""Plugin API 路由 - Phase 8.6"""
|
|
|
|
import os
|
|
import tempfile
|
|
import zipfile
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from app.agents.plugins import get_plugin_manager, PluginManifest
|
|
|
|
router = APIRouter(prefix="/api/plugins", tags=["Plugins"])
|
|
|
|
|
|
class PluginInfo(BaseModel):
|
|
"""插件信息"""
|
|
|
|
id: str
|
|
name: str
|
|
version: str
|
|
description: str
|
|
author: str
|
|
enabled: bool
|
|
main: str
|
|
|
|
|
|
class PluginInstallRequest(BaseModel):
|
|
"""插件安装请求"""
|
|
|
|
plugin_path: str
|
|
|
|
|
|
class PluginListResponse(BaseModel):
|
|
"""插件列表响应"""
|
|
|
|
plugins: list[dict[str, Any]]
|
|
count: int
|
|
|
|
|
|
# 全局插件市场(简单内存实现)
|
|
_plugin_marketplace: list[dict[str, str]] = []
|
|
|
|
|
|
def _manifest_to_dict(manifest: PluginManifest, enabled: bool) -> dict[str, Any]:
|
|
"""将 PluginManifest 转换为字典"""
|
|
return {
|
|
"id": manifest.id,
|
|
"name": manifest.name,
|
|
"version": manifest.version,
|
|
"description": manifest.description,
|
|
"author": manifest.author,
|
|
"enabled": enabled,
|
|
"main": manifest.main,
|
|
}
|
|
|
|
|
|
@router.get("", response_model=PluginListResponse)
|
|
async def list_plugins() -> PluginListResponse:
|
|
"""列出所有已安装的插件"""
|
|
manager = get_plugin_manager()
|
|
plugins = manager.list_plugins()
|
|
result = []
|
|
for p in plugins:
|
|
enabled = manager.is_enabled(p.id)
|
|
result.append(_manifest_to_dict(p, enabled))
|
|
return PluginListResponse(plugins=result, count=len(result))
|
|
|
|
|
|
@router.get("/{plugin_id}", response_model=dict[str, Any])
|
|
async def get_plugin(plugin_id: str) -> dict[str, Any]:
|
|
"""获取指定插件信息"""
|
|
manager = get_plugin_manager()
|
|
manifest = manager.get_plugin(plugin_id)
|
|
if not manifest:
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_id}' not found")
|
|
enabled = manager.is_enabled(plugin_id)
|
|
return _manifest_to_dict(manifest, enabled)
|
|
|
|
|
|
@router.post("/install", response_model=dict[str, str])
|
|
async def install_plugin(request: PluginInstallRequest) -> dict[str, str]:
|
|
"""安装插件"""
|
|
manager = get_plugin_manager()
|
|
if not os.path.exists(request.plugin_path):
|
|
raise HTTPException(status_code=400, detail="Plugin path does not exist")
|
|
|
|
if manager.install(request.plugin_path):
|
|
return {"status": "installed", "path": request.plugin_path}
|
|
raise HTTPException(status_code=500, detail="Failed to install plugin")
|
|
|
|
|
|
@router.post("/{plugin_id}/enable", response_model=dict[str, str])
|
|
async def enable_plugin(plugin_id: str) -> dict[str, str]:
|
|
"""启用插件"""
|
|
manager = get_plugin_manager()
|
|
if manager.enable(plugin_id):
|
|
return {"status": "enabled", "plugin_id": plugin_id}
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_id}' not found")
|
|
|
|
|
|
@router.post("/{plugin_id}/disable", response_model=dict[str, str])
|
|
async def disable_plugin(plugin_id: str) -> dict[str, str]:
|
|
"""禁用插件"""
|
|
manager = get_plugin_manager()
|
|
if manager.disable(plugin_id):
|
|
return {"status": "disabled", "plugin_id": plugin_id}
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_id}' not found")
|
|
|
|
|
|
@router.delete("/{plugin_id}", response_model=dict[str, str])
|
|
async def uninstall_plugin(plugin_id: str) -> dict[str, str]:
|
|
"""卸载插件"""
|
|
manager = get_plugin_manager()
|
|
if manager.uninstall(plugin_id):
|
|
return {"status": "uninstalled", "plugin_id": plugin_id}
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_id}' not found")
|
|
|
|
|
|
@router.post("/{plugin_id}/reload", response_model=dict[str, str])
|
|
async def reload_plugin(plugin_id: str) -> dict[str, str]:
|
|
"""重新加载插件"""
|
|
manager = get_plugin_manager()
|
|
if manager.reload(plugin_id):
|
|
return {"status": "reloaded", "plugin_id": plugin_id}
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_id}' not found")
|
|
|
|
|
|
# === Plugin Marketplace ===
|
|
|
|
_marketplace_router = APIRouter(prefix="/api/marketplace", tags=["Plugin Marketplace"])
|
|
|
|
|
|
@_marketplace_router.get("/plugins", response_model=dict[str, Any])
|
|
async def search_marketplace_plugins(
|
|
query: str | None = None,
|
|
category: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""搜索插件市场"""
|
|
results = _plugin_marketplace
|
|
if query:
|
|
results = [
|
|
p
|
|
for p in results
|
|
if query.lower() in p.get("name", "").lower()
|
|
or query.lower() in p.get("description", "").lower()
|
|
]
|
|
if category:
|
|
results = [p for p in results if p.get("category") == category]
|
|
return {"plugins": results, "count": len(results)}
|
|
|
|
|
|
@_marketplace_router.get("/plugins/{plugin_id}", response_model=dict[str, Any])
|
|
async def get_marketplace_plugin(plugin_id: str) -> dict[str, Any]:
|
|
"""获取市场中的插件详情"""
|
|
for plugin in _plugin_marketplace:
|
|
if plugin.get("id") == plugin_id:
|
|
return plugin
|
|
raise HTTPException(status_code=404, detail=f"Plugin '{plugin_id}' not found in marketplace")
|
|
|
|
|
|
@_marketplace_router.post("/plugins", response_model=dict[str, str])
|
|
async def add_to_marketplace(plugin: dict[str, str]) -> dict[str, str]:
|
|
"""添加插件到市场(仅供测试/开发)"""
|
|
if "id" not in plugin or "name" not in plugin:
|
|
raise HTTPException(status_code=400, detail="Plugin must have id and name")
|
|
# 移除已存在的同 ID 插件
|
|
global _plugin_marketplace
|
|
_plugin_marketplace = [p for p in _plugin_marketplace if p.get("id") != plugin["id"]]
|
|
_plugin_marketplace.append(plugin)
|
|
return {"status": "added", "id": plugin["id"]}
|
|
|
|
|
|
@_marketplace_router.post("/plugins/{plugin_id}/download", response_model=dict[str, str])
|
|
async def download_plugin(plugin_id: str) -> dict[str, str]:
|
|
"""从市场下载并安装插件"""
|
|
# Find plugin in marketplace
|
|
plugin = None
|
|
for p in _plugin_marketplace:
|
|
if p.get("id") == plugin_id:
|
|
plugin = p
|
|
break
|
|
|
|
if not plugin:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Plugin '{plugin_id}' not found in marketplace"
|
|
)
|
|
|
|
download_url = plugin.get("download_url")
|
|
if not download_url:
|
|
raise HTTPException(status_code=400, detail="Plugin has no download URL")
|
|
|
|
try:
|
|
# Download the plugin archive
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(download_url, timeout=60.0)
|
|
response.raise_for_status()
|
|
archive_content = response.content
|
|
|
|
# Extract to temp directory and install
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
archive_path = os.path.join(temp_dir, "plugin.zip")
|
|
with open(archive_path, "wb") as f:
|
|
f.write(archive_content)
|
|
|
|
extract_dir = os.path.join(temp_dir, "extracted")
|
|
with zipfile.ZipFile(archive_path, "r") as zf:
|
|
zf.extractall(extract_dir)
|
|
|
|
# Install the plugin
|
|
manager = get_plugin_manager()
|
|
if manager.install(extract_dir):
|
|
return {"status": "installed", "plugin_id": plugin_id}
|
|
raise HTTPException(status_code=500, detail="Failed to install plugin")
|
|
|
|
except httpx.HTTPError as e:
|
|
raise HTTPException(status_code=502, detail=f"Download failed: {str(e)}")
|
|
except zipfile.BadZipFile:
|
|
raise HTTPException(status_code=502, detail="Invalid plugin archive")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Installation failed: {str(e)}")
|