feat(v2): S1c′ 既有上传入口的批量摄取——S1 首次端到端闭环
T00/04 §4.2 要求的"既有材料上传入口扩展":**不建立平行上传 API**。 POST /documents 增加可选 batch_id / item_id 表单字段;不传时行为完全不变。 - api/prescreen.ingest_into_batch:清单核对 + 状态回写,仍走既有 ProjectAssistantService.upload(),故解析、同哈希复用原件、pending_candidates、 data_revision、mark_stale 等既有行为一字未改 - 服务端**重算** sha256 与大小,不信任客户端清单;不符则条目 failed + 422 - 返回体保持 **document 形状**(既有前端读 body.id / body.candidates), 批次状态作为附加字段 import_batch 给出 - 拒绝重复摄入已解析条目(409)、已取消批次(409)、不在清单中的文件(404) 闭环已成立:登记批次 → 上传 → 解析定位 → 冲突检测 → 人工裁定 → 进事实。 测试:test_prescreen_api.py 新增 BatchIngestTests 10 项(含闭环与既有路径回归)。 全量 235 项 OK(225 → 235);scripts/verify.py 9/9 PASS。 真实 app 冒烟 claude_use/T02/smoke_s1.py 15/15 PASS。 **测试隔离缺陷已修**:BatchIngestTests 初版只 patch 了 prescreen 的 service 工厂, 未 patch api.project_assistant.service,导致不传 batch_id 的上传落到**真实 v2 数据库**。 已补 patch,并清空 v2 数据根从干净状态重跑验证。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Unified project API. Every resource read/write derives scope from its project."""
|
||||
import base64
|
||||
from urllib.parse import quote
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request, UploadFile, File
|
||||
from fastapi.responses import Response
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from api.risk_agent_access import authorize, check_project
|
||||
@@ -221,7 +221,9 @@ def legacy(project_id: str, kind: str, legacy_id: str, request: Request):
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/documents", status_code=201)
|
||||
async def upload(project_id: str, request: Request, file: UploadFile = File(...)):
|
||||
async def upload(project_id: str, request: Request, file: UploadFile = File(...),
|
||||
batch_id: str | None = Form(default=None),
|
||||
item_id: str | None = Form(default=None)):
|
||||
guard(request, project_id)
|
||||
from services.assistant_materials import LIMIT
|
||||
try:
|
||||
@@ -230,6 +232,13 @@ async def upload(project_id: str, request: Request, file: UploadFile = File(...)
|
||||
await file.close()
|
||||
# Parsing is synchronous CPU work; do not block the ASGI event loop.
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
# S1:传入 batch_id 时走批量清单核对与状态回写(api/prescreen.ingest_into_batch)。
|
||||
# 不传时行为与既有完全一致——前端与既有调用方都不受影响。
|
||||
if batch_id:
|
||||
from api.prescreen import ingest_into_batch
|
||||
return await run_in_threadpool(lambda: translate(lambda: ingest_into_batch(
|
||||
project_id, file.filename or "material", content,
|
||||
request.state.identity["id"], batch_id, item_id)))
|
||||
return await run_in_threadpool(lambda: translate(lambda: service().upload(project_id, file.filename or "material", content, request.state.identity["id"])))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user