feat:LLM 分析文件并生成结构化建议

This commit is contained in:
2026-10-10 06:07:56 -07:00
parent b90842237f
commit e3699d1a30
22 changed files with 457 additions and 327 deletions
+45 -4
View File
@@ -3,13 +3,17 @@
from typing import Annotated, Literal, get_args
from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
from fastapi.responses import JSONResponse
from starlette.types import ASGIApp, Receive, Scope, Send
from app.common.response import ApiResponse, success_response
from app.common.response import ApiResponse, error_content, success_response
from app.core.auth import get_current_user
from app.datafiles.base import MAX_TABLE_ROWS
from app.datafiles.service import (
MAX_FILES_PER_CONVERSATION,
PARSE_CONCURRENCY,
DataFileService,
file_too_large,
)
from app.schemas.base import NonBlankString
from app.schemas.data_file import (
@@ -31,18 +35,55 @@ from app.schemas.user import CurrentUser
limits_router = APIRouter(prefix="/files", tags=["DataFile"])
router = APIRouter(prefix="/files", tags=["DataFile"])
# 文件选择框的过滤条件;后端按文件内容识别格式,不看扩展名
# 翻页取表数据:单页上限,和一张表最多的行数(100 万)
# 翻页取表数据:单页上限,offset 不超过一张表最多的行数
MAX_PAGE_ROWS = 500
MAX_OFFSET = 1_000_000
MAX_OFFSET = MAX_TABLE_ROWS
# 文件选择框的过滤条件;后端按文件内容识别格式,不看扩展名
ACCEPT = ".csv,.tsv,.txt,.xlsx,.xls,.xlsb,.json,.jsonl,.ndjson,.parquet"
# 上传的请求体比文件本身多出 multipart 的分隔行、字段名和会话 ID
UPLOAD_OVERHEAD_BYTES = 64 * 1024
def _service(request: Request) -> DataFileService:
return request.app.state.data_file_service
class UploadSizeLimit:
"""上传接口在读取请求体之前按 Content-Length 拒绝过大的请求
FastAPI 会先把整个 multipart 请求体收完、写进临时文件,再调用接口函数;
只在 DataFileService 里边写边查大小的话,超大的上传已经占满了带宽和磁盘
"""
def __init__(self, app: ASGIApp, path: str):
self.app = app
self.path = path
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if (
scope["type"] == "http"
and scope["method"] == "POST"
and scope["path"] == self.path
and (rejection := self._reject(scope)) is not None
):
await rejection(scope, receive, send)
return
await self.app(scope, receive, send)
@staticmethod
def _reject(scope: Scope) -> JSONResponse | None:
max_mb = scope["app"].state.data_file_service.max_mb
length = dict(scope["headers"]).get(b"content-length")
if length is None or not length.isdigit():
return JSONResponse(error_content(411, "缺少 Content-Length"), 411)
if int(length) > max_mb * 1024 * 1024 + UPLOAD_OVERHEAD_BYTES:
error = file_too_large(max_mb)
return JSONResponse(error_content(error.code, error.msg), error.code)
return None
def _detail(record: DataFileRecord) -> DataFileDetail:
# 按接口模型重新校验,去掉 owner_id、sha256、列概况等只在后端用的字段
return DataFileDetail.model_validate(record.model_dump())