message / plugins /cache /api.py
hunian
refactor(plugins): 插件短名并统一 MCP tool 为 {plugin}-{tool}
cc826a1
Raw
History Blame Contribute Delete
7.16 kB
import os
import uuid
import asyncio
import logging
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
router = APIRouter()
# 全局变量,用于存储插件实例
plugin = None
cleanup_task_handle = None
logger = logging.getLogger(__name__)
def stop_cleanup_task():
"""停止清理任务"""
global cleanup_task_handle
if cleanup_task_handle and not cleanup_task_handle.done():
cleanup_task_handle.cancel()
cleanup_task_handle = None
logger.info("文件清理定时任务已停止")
def start_cleanup_task():
"""启动清理任务"""
global cleanup_task_handle
if plugin and not cleanup_task_handle:
try:
loop = asyncio.get_event_loop()
if loop.is_running():
cleanup_task_handle = loop.create_task(periodic_cleanup())
logger.info("文件清理定时任务已启动")
else:
logger.warning("事件循环未运行,无法启动定时任务")
except RuntimeError as e:
logger.warning(f"启动定时任务失败: {e}")
def set_plugin_instance(plugin_instance):
"""由系统调用,注入插件实例"""
global plugin
plugin = plugin_instance
start_cleanup_task()
async def periodic_cleanup():
"""定期清理任务"""
while True:
try:
await asyncio.sleep(3600) # 每小时检查一次
if plugin:
await plugin.cleanup_expired_files()
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"清理任务出错: {e}")
@router.post("/upload")
async def upload_file(
file: UploadFile = File(...),
file_type: str = Form("temp"), # "temp" 或 "permanent"
):
"""上传文件接口
参数:
- file: 上传的文件
- file_type: 文件类型,"temp" 为临时文件(8小时后自动删除),"permanent" 为永久文件
返回:
{
"file_id": "生成的文件ID",
"filename": "文件名",
"url": "临时访问URL",
"type": "文件类型",
"created": "创建时间"
}
"""
if not plugin:
raise HTTPException(status_code=500, detail="插件未加载")
try:
# 验证文件类型
if file_type not in ["temp", "permanent"]:
raise HTTPException(
status_code=400, detail="无效的文件类型,应为 'temp' 或 'permanent'"
)
# 允许的文件类型
allowed_extensions = {
"image": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"],
"video": [".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm"],
"audio": [".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a"],
"document": [".md", ".txt", ".pdf", ".doc", ".docx"],
}
file_ext = Path(file.filename).suffix.lower()
file_category = None
for category, extensions in allowed_extensions.items():
if file_ext in extensions:
file_category = category
break
if not file_category:
raise HTTPException(
status_code=400,
detail=f"不支持的文件格式: {file_ext}。支持的格式: {', '.join(sum(allowed_extensions.values(), []))}",
)
# 生成文件ID
file_id = str(uuid.uuid4())
# 读取文件内容
content = await file.read()
# 保存文件
is_temp = file_type == "temp"
success = await plugin.save_file(
file_id, content, file.filename, is_temp=is_temp
)
if not success:
raise HTTPException(status_code=500, detail="保存文件失败")
# 获取元数据
metadata = plugin.file_metadata[file_id]
return {
"file_id": file_id,
"filename": file.filename,
"url": f"/plugins/cache/api/file/{file_id}",
"type": file_type,
"created": datetime.fromtimestamp(metadata["created"]).isoformat(),
"message": "文件上传成功",
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"上传文件时出错: {str(e)}")
@router.get("/list")
async def list_files():
"""获取文件列表接口
返回:
{
"files": [
{
"file_id": "文件ID",
"name": "文件名",
"type": "temp/permanent",
"created": "创建时间",
"url": "临时访问URL"
}
],
"total": 文件总数
}
"""
if not plugin:
raise HTTPException(status_code=500, detail="插件未加载")
try:
file_list = plugin.get_file_list()
return {"files": file_list, "total": len(file_list)}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取文件列表失败: {str(e)}")
@router.get("/file/{file_id}")
async def download_file(file_id: str):
"""下载文件接口
参数:
- file_id: 文件ID
返回:文件内容
"""
if not plugin:
raise HTTPException(status_code=500, detail="插件未加载")
try:
file_path = plugin.get_file_path(file_id)
if not file_path:
raise HTTPException(status_code=404, detail="文件不存在")
file_path_obj = Path(file_path)
if not file_path_obj.exists():
raise HTTPException(status_code=404, detail="文件不存在")
# 返回文件
return FileResponse(
path=file_path_obj,
filename=Path(file_path).name,
media_type="application/octet-stream",
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"下载文件失败: {str(e)}")
@router.delete("/file/{file_id}")
async def delete_file(file_id: str):
"""删除文件接口
参数:
- file_id: 文件ID
返回:
{
"success": true/false,
"message": "删除结果消息"
}
"""
if not plugin:
raise HTTPException(status_code=500, detail="插件未加载")
try:
success = await plugin.delete_file(file_id)
if not success:
raise HTTPException(status_code=404, detail="文件不存在或已删除")
return {"success": True, "message": "文件删除成功"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"删除文件失败: {str(e)}")
@router.get("/status")
async def get_status():
"""获取插件状态接口
返回:
{
"name": "插件名称",
"enabled": true/false,
"file_count": 文件总数,
"cache_dir": "缓存目录路径"
}
"""
if not plugin:
return {"error": "插件未加载"}
return plugin.get_status()