Files
QMDSearch/.trae/specs/add-file-upload-support/spec.md
T
kplam dce9e31bde feat: 新增多格式文件上传入库与认证体系
- 新增 JWT 认证模块,支持登录/注册/用户管理
- 新增文件上传接口,支持 .txt/.md/.html/.pdf/.docx 等格式解析入库
- 新增检索结果 AI 总结功能
- 新增文本去重缓存机制
- 新增全局认证夹具简化测试
- 新增配置项与环境变量支持
- 完善文档与测试覆盖
2026-07-30 10:30:15 +08:00

91 lines
6.6 KiB
Markdown
Raw 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.
# 多格式文件上传入库 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-datafile + 可选 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** 返回 202data.task_id 非空、data.status=="pending"、data.saved_path 指向已存在文件;任务管理器收到 DocumentInputtext 与文件解码一致、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** 返回 202saved_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)。