Files
NovalRedot/backend/app/routers/files.py
ROOG b39377e487
All checks were successful
Deploy Noval to K3s / deploy (push) Successful in 47s
新增文件管理与技能管理功能:
- **文件管理**:
  - 支持文件上传、解析和删除。
  - 限制支持格式(txt, md, docx, pdf),最大 10MB。
  - 提供前端上传区、文件列表及后端解析服务。

- **技能管理**:
  - 新增技能的创建、更新及删除功能。
  - 支持分配自定义提示词和工具权限。
  - 提供技能列表及分组工具选择交互界面。

同时调整相关 API 和前端组件,优化协作体验。
2026-03-22 23:50:14 +08:00

131 lines
4.2 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.

"""文件上传/管理路由"""
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()