""" 文件上传 API 路由 处理 Skill 参考文件的上传、管理和删除 """ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from typing import List, Optional from pydantic import BaseModel import shutil from pathlib import Path from app.core.skills.skill_manager import get_skill_manager, SkillManager from app.utils.logger import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/uploads", tags=["文件上传"]) class FileListResponse(BaseModel): """文件列表响应""" skill_id: str files: List[dict] @router.post("/skills/{skill_id}/references") async def upload_reference_file( skill_id: str, file: UploadFile = File(...), skill_manager: SkillManager = Depends(get_skill_manager) ): """ 上传 Skill 参考文件 上传的文件将保存到 skill 的 references/ 目录 支持的文件类型:.md, .txt, .json, .yaml, .yml """ # 验证 Skill 存在且为 user 类型 skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) if skill.type == 'builtin': raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="不允许上传文件到 builtin Skill" ) # 验证文件类型 allowed_extensions = {'.md', '.txt', '.json', '.yaml', '.yml', '.pdf', '.doc', '.docx'} file_ext = Path(file.filename).suffix.lower() if file_ext not in allowed_extensions: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"不支持的文件类型: {file_ext}。支持的类型: {', '.join(allowed_extensions)}" ) # 确保 references 目录存在 skill_path = skill_manager._find_skill_path(skill_id) references_dir = skill_path.parent / "references" references_dir.mkdir(exist_ok=True) # 保存文件 file_path = references_dir / file.filename try: with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) logger.info(f"文件上传成功: {skill_id}/references/{file.filename}") return { "success": True, "message": "文件上传成功", "file": { "name": file.filename, "path": str(file_path.relative_to(skill_manager.skills_dir)), "size": file_path.stat().st_size } } except Exception as e: logger.error(f"文件上传失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件上传失败: {str(e)}" ) @router.get("/skills/{skill_id}/references", response_model=FileListResponse) async def list_reference_files( skill_id: str, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 列出 Skill 的所有参考文件 """ skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) skill_path = skill_manager._find_skill_path(skill_id) references_dir = skill_path.parent / "references" files = [] if references_dir.exists(): for file_path in references_dir.iterdir(): if file_path.is_file(): files.append({ "name": file_path.name, "size": file_path.stat().st_size, "modified": file_path.stat().st_mtime }) return FileListResponse(skill_id=skill_id, files=files) @router.delete("/skills/{skill_id}/references/{filename}") async def delete_reference_file( skill_id: str, filename: str, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 删除 Skill 的参考文件 """ skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) if skill.type == 'builtin': raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="不允许删除 builtin Skill 的文件" ) skill_path = skill_manager._find_skill_path(skill_id) references_dir = skill_path.parent / "references" file_path = references_dir / filename if not file_path.exists(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"文件不存在: {filename}" ) try: file_path.unlink() logger.info(f"文件删除成功: {skill_id}/references/{filename}") return {"success": True, "message": "文件删除成功"} except Exception as e: logger.error(f"文件删除失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件删除失败: {str(e)}" ) @router.get("/skills/{skill_id}/references/{filename}") async def get_reference_file( skill_id: str, filename: str, skill_manager: SkillManager = Depends(get_skill_manager) ): """ 读取 Skill 参考文件内容(用于预览) """ skill = await skill_manager.load_skill(skill_id) if not skill: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Skill 不存在: {skill_id}" ) skill_path = skill_manager._find_skill_path(skill_id) references_dir = skill_path.parent / "references" file_path = references_dir / filename if not file_path.exists(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"文件不存在: {filename}" ) try: content = file_path.read_text(encoding='utf-8') return { "success": True, "filename": filename, "content": content } except UnicodeDecodeError: # 二进制文件(如 PDF)无法读取为文本 return { "success": True, "filename": filename, "content": None, "binary": True, "message": "这是二进制文件,无法预览内容" } except Exception as e: logger.error(f"文件读取失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件读取失败: {str(e)}" )