feat: 新增多格式文件上传入库与认证体系
- 新增 JWT 认证模块,支持登录/注册/用户管理 - 新增文件上传接口,支持 .txt/.md/.html/.pdf/.docx 等格式解析入库 - 新增检索结果 AI 总结功能 - 新增文本去重缓存机制 - 新增全局认证夹具简化测试 - 新增配置项与环境变量支持 - 完善文档与测试覆盖
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# 多格式文件上传入库 Spec
|
||||
|
||||
## Why
|
||||
|
||||
QMDSearch 现有入库接口 `POST /api/v1/documents` 仅接受 JSON 文本字段,用户必须先将文件内容贴成 text 才能入库;对 .md/.html/.pdf/.docx 等常见知识载体缺少直接通道。引入文件上传端点可减少入库摩擦、保留原始文件元数据、让管理后台一站式完成「上传→解析→入库→轮询任务」全流程。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `app/core/file_parser.py`:按扩展名分发解析器(.txt/.md/.html/.htm/.pdf/.docx),统一异常包装。
|
||||
- 新增 `POST /api/v1/documents/upload` 端点:multipart/form-data 接收文件 + 可选 title/source/metadata,校验扩展名/大小→提取文本→落盘→复用现有 IngestTaskManager 提交异步入库流水线,返回 202 + task_id + saved_path。
|
||||
- 配置项:`upload_dir` / `upload_max_size_mb` / `upload_allowed_extensions` 注入到 `Settings`。
|
||||
- 应用启动 lifespan 中 `mkdir -p upload_dir`(失败仅 warning 不阻塞启动)。
|
||||
- 依赖:`pyproject.toml` 增加 `python-multipart` / `pypdf` / `python-docx`。
|
||||
- 环境变量模板 `.env.example` 增加 `--- 文件上传 ---` 段落。
|
||||
- 管理后台 `admin.html` 在入库区块新增「文件上传」子表单:file input + 可选 title/source + 提交按钮,构造 FormData 调用 upload 端点,复用 `ingestPollTimer` 轮询 UI;提交期间禁用按钮。
|
||||
- `CLAUDE.md` API 清单加入 `POST /api/v1/documents/upload`,并在「文档入库与三级总结」节简述文件上传通道。
|
||||
- 单元测试:`tests/test_file_parser.py`(13 例覆盖各格式与异常路径)、`tests/test_document_upload_api.py`(10 例覆盖合法上传/不支持扩展名/超大/空文本/损坏 PDF/落盘失败降级)。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: 无(首次新增能力,不修改既有 spec)
|
||||
- Affected code:
|
||||
- 新增:`app/core/file_parser.py`、`tests/test_file_parser.py`、`tests/test_document_upload_api.py`
|
||||
- 修改:`app/api/v1/document.py`、`app/config.py`、`app/main.py`、`app/static/admin.html`、`pyproject.toml`、`.env.example`、`CLAUDE.md`
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 多格式文件文本提取
|
||||
|
||||
系统 SHALL 提供 `parse_file(filename, content)` 函数,按扩展名分发到对应解析器:.txt/.md 走 UTF-8 解码(errors=replace 兜底);.html/.htm 走标准库 html.parser 剥离 script/style 后提取可见文本;.pdf 走 pypdf 逐页 extract_text 拼接;.docx 走 python-docx 段落文本拼接(不含表格/页眉页脚)。未识别扩展名 SHALL 抛 `ValueError("不支持的文件类型: {ext}")`;解析异常 SHALL 包装为 `ValueError("文件解析失败: {detail}")` 并保留原异常链。
|
||||
|
||||
#### Scenario: 支持的扩展名提取成功
|
||||
- **WHEN** 调用 `parse_file("notes.md", b"# Hello\n\nWorld")`
|
||||
- **THEN** 返回字符串 `"# Hello\n\nWorld"`
|
||||
|
||||
#### Scenario: 不支持的扩展名
|
||||
- **WHEN** 调用 `parse_file("data.xlsx", b"binary")`
|
||||
- **THEN** 抛 `ValueError`,message 含 "不支持的文件类型: .xlsx"
|
||||
|
||||
#### Scenario: 损坏 PDF
|
||||
- **WHEN** 调用 `parse_file("bad.pdf", b"not a real pdf")`
|
||||
- **THEN** 抛 `ValueError`,message 含 "文件解析失败"
|
||||
|
||||
### Requirement: 文件上传入库端点
|
||||
|
||||
系统 SHALL 提供 `POST /api/v1/documents/upload` 端点,接收 multipart/form-data(file + 可选 title/source/metadata JSON 字符串),返回 `202` + `{code:0, data:{task_id, status:"pending", saved_path}}`。端点流程 SHALL 依次:扩展名校验(基于 `settings.upload_allowed_extensions`)→ 大小校验(`settings.upload_max_size_mb`)→ `parse_file` 提取文本→ 落盘到 `settings.upload_dir`(失败仅 warning 不阻塞)→ 合并用户 metadata(落盘元数据 raw_file_path/original_filename/original_size_bytes 优先级更高)→ 默认 title 取文件名 stem、默认 source 取 `file:{原文件名}` → 提交 IngestTaskManager → 返回 202。
|
||||
|
||||
#### Scenario: 合法 .md 上传
|
||||
- **WHEN** POST /api/v1/documents/upload 携带 file=notes.md(内容 "# Hello\n\nWorld")、title="我的笔记"、source="manual"
|
||||
- **THEN** 返回 202,data.task_id 非空、data.status=="pending"、data.saved_path 指向已存在文件;任务管理器收到 DocumentInput,text 与文件解码一致、metadata.original_filename=="notes.md"
|
||||
|
||||
#### Scenario: 不支持扩展名
|
||||
- **WHEN** POST 上传 data.xlsx
|
||||
- **THEN** 返回 code=1001、message 含 ".xlsx",任务管理器未收到任何提交
|
||||
|
||||
#### Scenario: 超大文件
|
||||
- **WHEN** upload_max_size_mb=1 且上传 2MB txt
|
||||
- **THEN** 返回 code=1001、message 含 "大小上限"
|
||||
|
||||
#### Scenario: 解析后为空文本
|
||||
- **WHEN** 上传 blank.txt 内容仅空白
|
||||
- **THEN** 返回 code=1001、message 含 "无法从文件提取文本"
|
||||
|
||||
#### Scenario: 落盘失败降级
|
||||
- **WHEN** upload_dir 指向已存在的文件(mkdir 失败)
|
||||
- **THEN** 返回 202,saved_path=="",DocumentInput.metadata 不含 raw_file_path/original_filename
|
||||
|
||||
### Requirement: 应用启动初始化 upload_dir
|
||||
|
||||
应用 lifespan SHALL 在 `ensure_default_admin` 之后尝试 `Path(settings.upload_dir).mkdir(parents=True, exist_ok=True)`,失败仅 warning 不阻塞启动。
|
||||
|
||||
### Requirement: 管理后台文件上传 UI
|
||||
|
||||
`admin.html` 入库区块 SHALL 在现有「文本入库」表单旁新增「文件上传」子表单,包含:file input(accept 当前支持的扩展名)+ 可选标题输入框 + 可选来源输入框 + 提交按钮。提交时构造 FormData 调用 `/api/v1/documents/upload`,复用现有 `ingestPollTimer` 与 ingest-result 渲染逻辑展示任务进度与最终结果。提交期间 SHALL 禁用提交按钮并显示「上传中…」文案。
|
||||
|
||||
#### Scenario: 用户上传文件
|
||||
- **WHEN** 用户选择 notes.md 文件、填标题、点提交
|
||||
- **THEN** 按钮禁用显示「上传中…」,FormData 含 file/title,请求返回 202 后开始轮询 task 状态直到 done/failed/timeout
|
||||
|
||||
### Requirement: 配置项与环境变量
|
||||
|
||||
`Settings` SHALL 新增三项配置:`upload_dir: str = "./uploads"`、`upload_max_size_mb: int = 20`、`upload_allowed_extensions: str = ".txt,.md,.html,.htm,.pdf,.docx"`。`.env.example` SHALL 增加 `--- 文件上传 ---` 段落包含对应环境变量 `UPLOAD_DIR` / `UPLOAD_MAX_SIZE_MB` / `UPLOAD_ALLOWED_EXTENSIONS`。
|
||||
|
||||
### Requirement: 依赖声明
|
||||
|
||||
`pyproject.toml` dependencies SHALL 增加:`python-multipart>=0.0.20`(FastAPI 文件上传依赖)、`pypdf>=5.1.0`(PDF 解析)、`python-docx>=1.1.2`(DOCX 解析)。
|
||||
|
||||
### Requirement: 文档同步
|
||||
|
||||
`CLAUDE.md` API 清单 SHALL 加入 `POST /api/v1/documents/upload` 行;「文档入库与三级总结」节 SHALL 简述文件上传通道(multipart 入口→parse_file 提取→复用现有 IngestTaskManager)。
|
||||
Reference in New Issue
Block a user