2026-01-25 19:27:44 +08:00

219 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
文件上传 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)}"
)