All checks were successful
Deploy Noval to K3s / deploy (push) Successful in 47s
- **文件管理**: - 支持文件上传、解析和删除。 - 限制支持格式(txt, md, docx, pdf),最大 10MB。 - 提供前端上传区、文件列表及后端解析服务。 - **技能管理**: - 新增技能的创建、更新及删除功能。 - 支持分配自定义提示词和工具权限。 - 提供技能列表及分组工具选择交互界面。 同时调整相关 API 和前端组件,优化协作体验。
131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
"""文件上传/管理路由"""
|
||
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||
from sqlmodel import Session, select
|
||
|
||
from ..database import engine, DATA_DIR
|
||
from ..models import Novel, NovelFile
|
||
from ..schemas import FileRead, FileList
|
||
from ..services.file_parser import parse_file, ALLOWED_EXTENSIONS, MAX_FILE_SIZE
|
||
|
||
router = APIRouter(prefix="/api/novels/{novel_id}/files", tags=["files"])
|
||
|
||
# 上传文件存储根目录
|
||
UPLOAD_DIR = DATA_DIR / "uploads"
|
||
|
||
|
||
def _get_novel_upload_dir(novel_id: int) -> Path:
|
||
"""获取小说的上传目录,不存在则创建"""
|
||
d = UPLOAD_DIR / str(novel_id)
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
return d
|
||
|
||
|
||
@router.post("", response_model=FileRead, status_code=201)
|
||
async def upload_file(novel_id: int, file: UploadFile = File(...)):
|
||
"""上传文件并解析为纯文本"""
|
||
with Session(engine) as session:
|
||
# 校验小说存在
|
||
novel = session.get(Novel, novel_id)
|
||
if not novel:
|
||
raise HTTPException(status_code=404, detail="小说不存在")
|
||
|
||
# 校验扩展名
|
||
if not file.filename:
|
||
raise HTTPException(status_code=400, detail="缺少文件名")
|
||
ext = Path(file.filename).suffix.lower()
|
||
if ext not in ALLOWED_EXTENSIONS:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"不支持的文件格式 {ext},仅支持: {', '.join(sorted(ALLOWED_EXTENSIONS))}",
|
||
)
|
||
|
||
# 读取文件内容
|
||
content = await file.read()
|
||
file_size = len(content)
|
||
|
||
if file_size > MAX_FILE_SIZE:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"文件过大({file_size / 1024 / 1024:.1f}MB),最大允许 10MB",
|
||
)
|
||
|
||
# 生成存储名并保存原始文件
|
||
stored_name = f"{uuid.uuid4().hex}{ext}"
|
||
upload_dir = _get_novel_upload_dir(novel_id)
|
||
file_path = upload_dir / stored_name
|
||
file_path.write_bytes(content)
|
||
|
||
# 解析纯文本
|
||
try:
|
||
text_content = parse_file(file_path, file.content_type or "")
|
||
except Exception as e:
|
||
# 解析失败,清理已保存的文件
|
||
file_path.unlink(missing_ok=True)
|
||
raise HTTPException(status_code=400, detail=f"文件解析失败: {e}")
|
||
|
||
# 保存纯文本缓存
|
||
text_path = upload_dir / f"{stored_name}.txt"
|
||
text_path.write_text(text_content, encoding="utf-8")
|
||
|
||
# 写入数据库
|
||
with Session(engine) as session:
|
||
novel_file = NovelFile(
|
||
novel_id=novel_id,
|
||
filename=file.filename,
|
||
stored_name=stored_name,
|
||
file_size=file_size,
|
||
mime_type=file.content_type or "",
|
||
text_length=len(text_content),
|
||
)
|
||
session.add(novel_file)
|
||
session.commit()
|
||
session.refresh(novel_file)
|
||
return novel_file
|
||
|
||
|
||
@router.get("", response_model=FileList)
|
||
def list_files(novel_id: int):
|
||
"""列出小说关联的所有文件"""
|
||
with Session(engine) as session:
|
||
stmt = (
|
||
select(NovelFile)
|
||
.where(NovelFile.novel_id == novel_id)
|
||
.order_by(NovelFile.created_at.desc())
|
||
)
|
||
files = session.exec(stmt).all()
|
||
items = [
|
||
FileRead(
|
||
id=f.id, # type: ignore
|
||
novel_id=f.novel_id,
|
||
filename=f.filename,
|
||
file_size=f.file_size,
|
||
mime_type=f.mime_type,
|
||
text_length=f.text_length,
|
||
created_at=f.created_at,
|
||
)
|
||
for f in files
|
||
]
|
||
return FileList(items=items, total=len(items))
|
||
|
||
|
||
@router.delete("/{file_id}", status_code=204)
|
||
def delete_file(novel_id: int, file_id: int):
|
||
"""删除文件(数据库记录 + 磁盘文件)"""
|
||
with Session(engine) as session:
|
||
novel_file = session.get(NovelFile, file_id)
|
||
if not novel_file or novel_file.novel_id != novel_id:
|
||
raise HTTPException(status_code=404, detail="文件不存在")
|
||
|
||
# 删除磁盘文件
|
||
upload_dir = _get_novel_upload_dir(novel_id)
|
||
(upload_dir / novel_file.stored_name).unlink(missing_ok=True)
|
||
(upload_dir / f"{novel_file.stored_name}.txt").unlink(missing_ok=True)
|
||
|
||
# 删除数据库记录
|
||
session.delete(novel_file)
|
||
session.commit()
|