# 入库异步任务化 Spec ## Why 当前 `POST /api/v1/documents` 同步等待整条入库流水线(总结→分类→切分→向量化→写库),真实 Ollama 冒烟单篇耗时 39s+,长文档分钟级,客户端/网关易超时,批量导入不可用。改为后台任务模式:提交即返回 task_id,状态可查,失败任务长期保留便于归因。 ## What Changes - **BREAKING**:`POST /api/v1/documents` 由同步返回 IngestionResult(200)改为立即返回 `{task_id, status}`(HTTP 202),流水线在后台 asyncio 任务中执行 - **新增** `GET /api/v1/documents/tasks/{task_id}`:查询任务状态(pending/各阶段/done/failed)、完成后的 IngestionResult、失败时的 stage 与错误信息 - **新增** `app/core/ingest_tasks.py` IngestTaskManager:提交登记 → 信号量限流并发 → 后台执行 → 状态机推进 → 结果/错误落储 - **状态存储**:Redis 为主(key `ingest_task:{task_id}`,JSON);done TTL 24h,**failed TTL 7 天**(便于失败归因);Redis 不可用时降级进程内字典(重启丢失,日志告警),服务不因此拒绝入库 - **管理页**:入库区块改为「提交 → 轮询任务状态 → 展示进度/结果/失败详情」 - **配置新增**:`ingest_max_concurrency`(默认 2)、`ingest_task_ttl_done`(默认 86400s)、`ingest_task_ttl_failed`(默认 604800s,7 天) - CLAUDE.md API 清单同步更新(标注 POST /documents 行为变更) ## Impact - Affected specs: 文档入库 API、管理页面、任务状态存储 - Affected code: - 修改:[document.py](file:///Users/kplam/coding/QMDSearch/app/api/v1/document.py)(POST 改 202 + 新增任务查询端点)、[config.py](file:///Users/kplam/coding/QMDSearch/app/config.py)、[.env.example](file:///Users/kplam/coding/QMDSearch/.env.example)、[admin.html](file:///Users/kplam/coding/QMDSearch/app/static/admin.html)(入库区块轮询)、[CLAUDE.md](file:///Users/kplam/coding/QMDSearch/CLAUDE.md) - 新增:`app/core/ingest_tasks.py`、`tests/test_ingest_tasks.py`、`tests/test_ingest_task_api.py` ## ADDED Requirements ### Requirement: 异步入库提交 系统 SHALL 将 `POST /api/v1/documents` 改为异步任务模式:校验通过后立即返回 HTTP 202 与 `{task_id, status: "pending"}`,入库流水线在后台任务执行;并发后台任务数受 `ingest_max_concurrency` 信号量限制,超限任务保持 pending 排队。请求体与校验规则(text 非空等)与现状一致,校验失败仍同步返回 1001。 #### Scenario: 提交即返回 - **WHEN** 提交合法文档 - **THEN** 响应 202 + task_id,无需等待流水线完成;后台任务按并发额度开始执行 ### Requirement: 任务状态查询 系统 SHALL 提供 `GET /api/v1/documents/tasks/{task_id}`:返回 `{task_id, status, created_at, updated_at}`;status 取值 `pending | summarizing | classifying | embedding | writing | done | failed`(与流水线阶段一致推进);done 时附 `result`(完整 IngestionResult:document_id/category/tags/总结层级/chunks_count);failed 时附 `error: {stage, message}`(stage 来自 IngestionError,已产出总结不丢失一并附在 error.partial_summary)。task_id 不存在返回 code=1004。 #### Scenario: 生命周期 - **WHEN** 提交后轮询 task_id - **THEN** 依次观察到 pending→阶段状态→done(result 可查,文档已可检索);失败任务观察到 failed + error.stage ### Requirement: 状态存储与保留 任务状态 SHALL 写入 Redis(`ingest_task:{task_id}`,JSON):done TTL=ingest_task_ttl_done(默认 24h),failed TTL=ingest_task_ttl_failed(默认 7 天),进行中状态 TTL 取 done 值。Redis 不可用时降级进程内字典并日志告警,入库与查询照常(重启后历史任务丢失可接受,接口语义不变)。 #### Scenario: Redis 宕机降级 - **WHEN** Redis 连接失败 - **THEN** 提交与查询仍正常,仅日志 warning;进程重启后任务状态丢失 ### Requirement: 管理页入库区块 管理页入库表单 SHALL 改为:提交后展示 task_id 与状态进度条/文本,每 2s 轮询任务接口,done 展示分类/标签/总结层级/chunks_count,failed 展示 error.stage 与 message;轮询期间禁止重复提交,超时(5 分钟)停止轮询并提示可稍后手动查询。 ### Requirement: 并发控制 后台入库 SHALL 用 asyncio 信号量限制并发(默认 2),避免多任务同时打满 Ollama/embedding;排队任务状态保持 pending。 ## MODIFIED Requirements ### Requirement: POST /api/v1/documents(行为变更) 原同步返回 IngestionResult 的行为废弃,改为 202 + task_id。所有既有调用方(管理页、既有测试)同步适配;响应体统一响应包装不变(`{code:0, data:{task_id,status}, message:"ok"}`,HTTP 状态码 202)。 ## REMOVED Requirements ### Requirement: 同步入库等待 **Reason**:长耗时流水线导致客户端超时、无法批量导入。 **Migration**:调用方改为提交后轮询 `GET /api/v1/documents/tasks/{task_id}`;管理页已内置轮询。