feat(v2): S1 接口层——材料导入与事实冲突的 8 个端点

T02 第三个子卡。命名空间按 T00/04 §1 定案 P1 挂 project-assistant 之下,
复用现有鉴权与同源代理,鉴权/代理代码零改动。

新增 backend/api/prescreen.py(7 路径 / 8 操作)+ test_prescreen_api.py(27 项)。
app.py 仅新增 1 个 include_router 与 1 个异常处理器注册。

落实:
- 404 统一口径(03 §2):不存在与无访问权一律 404,不泄露对象存在性。
  既有 check_project 抛 403 且被 5 个模块共用,故不改它,由本模块 scope() 包装。
  实测:无权项目返回 404 NOT_FOUND 而非 403。
- 错误信封(D2):只在新路由输出 {error:{code,message,retryable,request_id}}。
- E-09 与 E-05 精确区分:新增 store.IdempotencyConflict,不再靠字符串比对猜码。
- 客户端清单只接受 4 个字段;id/status/received_at 由服务端生成——
  状态机写权不下放。manifest 校验因此从"整对象"改为"4 字段 + 服务端补全"。

**实测发现的缺口并已修复**:Input(extra="forbid") 的校验错误由 FastAPI 校验层在
进入端点前抛出,不经过 run(),导致同一命名空间出现两种错误形状。现注册
RequestValidationError 处理器,但**只在 prescreen 路径生效**,其它路径交回
FastAPI 默认实现——既有端点形状经实测确认未变(含校验错误的形状)。

验证:
- scripts/verify.py 9/9 PASS(exit 0);全量单测 225 项 OK(196 → 225)
- claude_use/T02/smoke_s1.py 在真实 app.py 上 10/10 PASS:覆盖路由注册、
  真实前缀下的 manager 鉴权放行、404 统一、信封形状、以及既有端点未被污染

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:45:07 +08:00
co-authored by Claude Code
parent c59fff68d3
commit fcf4d8c948
6 changed files with 695 additions and 5 deletions
+9 -1
View File
@@ -103,7 +103,7 @@ def idempotent_lookup(db, actor, operation, key, request_hash):
if not prior:
return None
if prior[0] != request_hash:
raise Conflict("幂等键已用于不同请求")
raise IdempotencyConflict("幂等键已用于不同请求")
return prior[1]
@@ -160,3 +160,11 @@ def time_listing(db, project_ids=None):
class Conflict(ValueError):
pass
class IdempotencyConflict(Conflict):
"""幂等键已用于不同载荷(E-09 IDEMPOTENCY_CONFLICT)。
与普通 Conflict(E-05 REVISION_CONFLICT)区分:两者的用户文案与恢复动作不同,
接口层需要能分别映射为不同的错误码,故不用字符串比对来猜。
"""