Commit Graph
4 Commits
Author SHA1 Message Date
lzf_0626 790518114b 风控 SSE 内容协商与鉴权时序;docs/05 补齐 413 与风控入口(docs/25 P3 #23 #24 #25)
#23:413 是上传超限的标准语义,前端文档(风控业务演示文档 17)也已按 413 做提示
映射,所以不把代码降成 422,而是在 docs/05 §3.5 状态码表补登 413 —— 契约以"补齐"
而不是"改动"的方式对齐。

#24:/api/v1/risk/daily-report/stream 此前既不校验 Accept,又把鉴权留在 async
generator 内部。后者更隐蔽:StreamingResponse 已经返回、响应头已经发出,403 只能
变成"200 + 半截流"。现在 controller 先 await service.authorize(context) 再判定
Accept,顺序与 §6.4 一致(鉴权先行,不用状态码差异做探测)。SSE 协商逻辑抽到
app/api/dependencies/negotiation.py,与 /agent-runs/{run_id}/events 共用同一口径,
避免同一种客户端在一个端点上 200、另一个端点上 406。

#25:复核后确认前半段不成立 —— §19 末尾写明业务域接口由各自业务文档登记,风控 15 条
端点已在 06-模块接口与字段映射.md 逐条登记。真问题是 §12 表里写的
/api/v1/risk-scans/**、/api/v1/risk-alerts/** 与实际实现 /api/v1/risk/** 不符,
按实际实现更新 §12 并加说明;顺带把风控文档里 /daily-report/mail 的权限从
"按主项目邮件策略执行"改为实际的 risk:report:mail。

新增 tests/unit/api/test_risk_stream_negotiation.py(7 例)。
2026-09-11 14:08:55 +08:00
lzf_0626 e8b075bc7f fix(risk): 列表接口的信封对齐 docs/05 §3.3——游标从 data 挪到 meta
docs/05 §3.3 的列表样例是 data 为**纯数组**、
ext_cursor 与 has_more 放在 meta 里,
并明确「业务接口不得增加其他顶层字段」。而 RiskQueryService._page 返回的
{items, next_cursor, has_more} 被**整体塞进 data** —— 游标因此出现在**业务数据**里,
meta 只剩 trace_id,两处都不符合契约。

改动:
- 新增 _list_envelope(page, context):把 _page 的结构拆成 data = items、
  meta = {trace_id, next_cursor, has_more}。**service 侧不动** —— 它继续返回那个内部
  结构,只是不再直接当 data 用。
- 3 个列表端点改用它:/alerts、/evidence/{source}、/notifications。
  非列表端点(overview、详情、各类写操作)保持原样,不带游标。

**影响调用方**:这是接口形状变更。组员若写了前端读 data.items,需要改成读 data、
并从 meta 取分页元数据。改动依据是 docs/05 这个唯一权威接口文档(§20 也要求实现与
文档同步)。**合并时要提醒组员。**

**实测**(9002 身份):
- /alerts?limit=2 → 顶层键 ['data','meta']、data 是 list(2 条真实数据)、
  meta 键 ['has_more','next_cursor','trace_id']
- /evidence/customers 与 /notifications 同形状
- 对照 /overview(非列表)→ meta 只有 trace_id、不带游标

**测试**:3 处断言从 data["items"] 改为 data;/notifications 那处补上对 meta 的断言;
新增 test_list_endpoints_follow_the_documented_envelope,直接断言"data 是纯数组、
meta 恰好三个键、顶层恰好 data/meta",把 §3.3 的契约钉住。

过程里踩了两个自己的坑:① 忘了 rom typing import Any(与 customer_service.py 同一失误,
被 ruff/mypy 当场抓住);② 漏了 /evidence/{source} 这个列表端点,是测试先失败才发现的。

ruff / mypy(136 文件) / 640 unit+contract / 29 integration 全绿。
2026-09-11 14:01:16 +08:00
lzf_0626 0d3447bde4 fix(risk): 日报邮件端点补上授权校验——它此前是风控里唯一没有校验的端点
**问题**:POST /api/v1/risk/daily-report/mail 原先只取 context 做 401 判定,
**没有任何授权校验**;RiskDailyReportMailService.send 既拿不到 context、也不调用
AuthorizationService。收件人、标题、正文**全部由客户端决定** —— 一旦运维开启 SMTP
(RISK_DAILY_REPORT_MAIL_ENABLED),它就是一个未授权的邮件发送器。
默认关闭(ENABLED 默认 false + DRY_RUN 默认 true)让它至今没出事,但那不是可依赖的保护。

**改动**:
- send 改为 async 并接收 context,入口处 wait AuthorizationService.require(
  context, "risk:report:mail")。校验放在 **service 层**而不是 controller —— 本项目风控
  端点的授权一律落在 service(risk_query / risk_action / risk_scan 等都是这样),
  controller 只负责取 context;这个端点是唯一的例外,现在补齐。
- controller 相应改为 wait ...send(..., context=context)。
- 权限 
isk:report:mail 已在上一轮随另外三个一起创建并授予 risk_operator 与 admin。

**实测**:
- 9002(risk_operator) → **200** + {"status":"disabled","recipient_count":1}(默认关闭)
- 9001(customer)      → **403** AGENT_PERMISSION_DENIED「缺少操作权限」

**测试**:3 个既有用例改为 async 并传入带权限的 context;**新增**
	est_mail_service_requires_the_permission,断言无权限身份必须被拒 —— 钉住本次修复。

ruff / mypy(136 文件) / 623 unit+contract / 29 integration 全绿。
2026-09-11 13:39:50 +08:00
zhangshy a94d5c754d feat: 迁移奶龙风控业务模块与演示文档 2026-09-10 21:03:44 +08:00