qyqy_develop_1
qyqy_develop
合并 qyqy_develop_1 的第三轮工作:
你说得对:C ≥ R 是风控的口径,客服要走政策原文的矩阵。 上一轮我复核后说"两边一致、无需改动"是错的 —— 我只对比了政策文本与 SuitabilityService,漏了第三个东西:客服自己的知识库。POL-AST-012 就是第十二条矩阵 原文,而客服为 C1-C5 补的「能买什么产品」问答答案直接取自该矩阵(docs/24 第二节)。 于是同一个客服 Agent 对同一个问题给出相反答案: 问"C1 能买什么产品" → 知识检索答 R1、R2 可买(矩阵口径) 问"C1 能买这只 R2 吗" → 适当性出口答不能购买(严格 C ≥ R) 政策冲突的精确位置也不是"矩阵 vs 硬匹配",而是第十四条**第 1 款与它自己的第 2、3 款** 矛盾:第 2 款说"低于一个等级以上"才拒绝(C1→R2 只低 1 级,够不上"以上"),第 3 款禁止 C1 买 R3+、C2 买 R4+(R2 不在禁止列表)。矩阵与第 2、3 款三方一致,孤立的是第 1 款。 改动: - suitability_service.py 新增 MATRIX_ALLOWED / MATRIX_NEEDS_DISCLOSURE,_decide 由 "C < R 即拒绝"改为按矩阵:C1→R2、C2→R3 直接可买;C3→R4、C4→R5 走第十五条豁免档 (reason_code=SUITABLE_WITH_DISCLOSURE,强制揭示 + 确认 + 录音);低两级及以上仍拒绝。 - 客服话术分档:越级档不再说"在您的风险承受能力范围内" —— 那句只对 C ≥ R 成立, 用在 C1 买 R2 上会让客户以为自己的测评本来就覆盖这只产品。 - 风控侧不动,保留 C ≥ R:它要发现的是"越级成交且留痕不全",这个差异是有意保留的。 测试:新增逐格对照政策原文的 25 格矩阵用例、豁免档用例、话术分档用例(+29)。 docs/25 第七节 #1 与 docs/24 第七节同步更正,包括写明我上一轮那个结论错在哪。 门禁:ruff 干净 / mypy 137 文件 / 693 unit+contract / 33 integration。
"缺 meta"很容易被误判成"有":X-Trace-ID 是**响应头**(中间件加),和 body 里的 meta.trace_id 是两件事;错误响应一直有 meta(异常处理器统一加),漏的只有成功路径。 核到三个端点在把 service 的内部结构直接当响应体返回: - GET /conversations/{session_id}/messages → 裸 {"data": [...]},meta 整个缺失, 游标也没地方放(§3.3 要求列表的 data 为纯数组、next_cursor/has_more 进 meta) - POST /conversation-messages/{id}/feedback → 同样没有 meta - GET /knowledge-references/{token} → 直接返回资源对象 改动: - 新增 app/api/views/envelope.py,把 envelope / list_envelope 抽成一份公共实现, 风控链路改为复用它 —— 同一份契约写两遍的结果就是其中一处漏了 meta。 - ConversationService.messages 改为返回内部结构 {items, next_cursor, has_more}, 用 limit + 1 判断 has_more:只看"取满没取满"会把恰好等于 limit 的最后一页说成 还有下一页。next_cursor 取本页最后一条的 message_id —— 游标语义是"取更旧的一页", 天然可续,集成测试本来就是这么翻页的。 - Controller 统一套信封,data 仍是数组、字段名不变,前端不需要改。 测试:新增 tests/unit/api/test_response_envelope.py,断言 set(body) == {"data","meta"} (多或少一个顶层字段都会红),并覆盖 has_more / next_cursor / trace_id; 另更新两处既有断言(limit 20→21、feedback 返回裸对象)。 门禁:ruff 干净 / mypy 138 文件 / 696 unit+contract / 33 integration。
起因是查 Worker 运行状态时发现库里 373 条死信的 last_error 全是裸的 "ValueError" (工具 tools/probe_worker_state.py,证据 docs/evidence/worker-state.json)。 dispatch(run not found)、dispatch_run_completed、dispatch_memory_extraction、 dispatch_profile_rebuild 抛的都是 ValueError,只记类名等于把"哪一处失败"也一起丢了。 但"直接存 str(exc)"是错的:tests/unit/worker/test_outbox_worker.py 那条 RuntimeError("credential=do-not-log") 断言异常消息不得落库 —— 它可能含凭据、SQL 或 客户标识。第一版改动就是这么写的,被这个测试当场拦下(这测试写得值)。 折中: - 新增 OutboxHandlerError(继承 ValueError,这些失败本就是 ValueError 语义,保持 继承关系才不会改动既有的 except ValueError 行为与断言)。它的 reason 由代码写死、 不含任何请求数据,因此可以落库; - safe_error_text:OutboxHandlerError → "类名: 固定文案"(截断 500 字符), 其余异常 → 仍只记类名; - runtime.py 的 5 处 handler 失败改抛 OutboxHandlerError。 测试:新增"固定文案落库"用例;并把既有用例的断言收紧为 last_error == "RuntimeError" (原先只断言"不含 do-not-log",太松,漏掉的情况测不出来)。 顺带产出 tools/probe_worker_state.py(只读):outbox / agent_run 各状态计数、按事件 类型分组、死信原因聚合。当前环境实测 pending 347、dead 373、published 410、 agent_run 无 queued/running。 门禁:ruff 干净 / mypy 138 文件 / 697 unit+contract / 33 integration。
组员的 4 个提交(相对 c11e56c): - 4b0c148 把我这边的风控 P3 收尾与适当性豁免额度合并进 qyqy_develop(PR #4) - 2ebe438 docs: 增加风控 Agent 工具白名单与意图配置 - 5102eaa Merge origin/qyqy_develop into RM2_develop - d2cdbba 修复风控时间口径与 Agent 截断提示 自动合并零冲突,两侧的改动是互补而非重叠: - 时区:我在 list_alerts 做了 from_local,他补的是 list_evidence 与通知列表; - 截断:我加了 data_truncated / evidence_truncated,他在 risk_agent 的 system prompt 里加了第 10 条 —— 这些标记出现时不得按全量证据下结论。方向是一致的。 验证:ruff 干净 / mypy 138 文件 / 703 unit+contract(+6,组员新增的用例)/ 33 integration,均在合并后的工作树上跑过。
4b0c148
2ebe438
5102eaa
d2cdbba
No dependencies set.
The note is not visible to the blocked user.
合并 qyqy_develop_1 的第三轮工作:
"缺 meta"很容易被误判成"有":X-Trace-ID 是**响应头**(中间件加),和 body 里的 meta.trace_id 是两件事;错误响应一直有 meta(异常处理器统一加),漏的只有成功路径。 核到三个端点在把 service 的内部结构直接当响应体返回: - GET /conversations/{session_id}/messages → 裸 {"data": [...]},meta 整个缺失, 游标也没地方放(§3.3 要求列表的 data 为纯数组、next_cursor/has_more 进 meta) - POST /conversation-messages/{id}/feedback → 同样没有 meta - GET /knowledge-references/{token} → 直接返回资源对象 改动: - 新增 app/api/views/envelope.py,把 envelope / list_envelope 抽成一份公共实现, 风控链路改为复用它 —— 同一份契约写两遍的结果就是其中一处漏了 meta。 - ConversationService.messages 改为返回内部结构 {items, next_cursor, has_more}, 用 limit + 1 判断 has_more:只看"取满没取满"会把恰好等于 limit 的最后一页说成 还有下一页。next_cursor 取本页最后一条的 message_id —— 游标语义是"取更旧的一页", 天然可续,集成测试本来就是这么翻页的。 - Controller 统一套信封,data 仍是数组、字段名不变,前端不需要改。 测试:新增 tests/unit/api/test_response_envelope.py,断言 set(body) == {"data","meta"} (多或少一个顶层字段都会红),并覆盖 has_more / next_cursor / trace_id; 另更新两处既有断言(limit 20→21、feedback 返回裸对象)。 门禁:ruff 干净 / mypy 138 文件 / 696 unit+contract / 33 integration。起因是查 Worker 运行状态时发现库里 373 条死信的 last_error 全是裸的 "ValueError" (工具 tools/probe_worker_state.py,证据 docs/evidence/worker-state.json)。 dispatch(run not found)、dispatch_run_completed、dispatch_memory_extraction、 dispatch_profile_rebuild 抛的都是 ValueError,只记类名等于把"哪一处失败"也一起丢了。 但"直接存 str(exc)"是错的:tests/unit/worker/test_outbox_worker.py 那条 RuntimeError("credential=do-not-log") 断言异常消息不得落库 —— 它可能含凭据、SQL 或 客户标识。第一版改动就是这么写的,被这个测试当场拦下(这测试写得值)。 折中: - 新增 OutboxHandlerError(继承 ValueError,这些失败本就是 ValueError 语义,保持 继承关系才不会改动既有的 except ValueError 行为与断言)。它的 reason 由代码写死、 不含任何请求数据,因此可以落库; - safe_error_text:OutboxHandlerError → "类名: 固定文案"(截断 500 字符), 其余异常 → 仍只记类名; - runtime.py 的 5 处 handler 失败改抛 OutboxHandlerError。 测试:新增"固定文案落库"用例;并把既有用例的断言收紧为 last_error == "RuntimeError" (原先只断言"不含 do-not-log",太松,漏掉的情况测不出来)。 顺带产出 tools/probe_worker_state.py(只读):outbox / agent_run 各状态计数、按事件 类型分组、死信原因聚合。当前环境实测 pending 347、dead 373、published 410、 agent_run 无 queued/running。 门禁:ruff 干净 / mypy 138 文件 / 697 unit+contract / 33 integration。