219 lines
6.5 KiB
Python
219 lines
6.5 KiB
Python
"""
|
||
文件上传 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)}"
|
||
)
|