Commit Graph
245 Commits
Author SHA1 Message Date
qyqy 7677aeaee1 merge: 并入同事的场外申购/推广/行情/NL2SQL 线(11 提交、334 文件)
冲突仅 3 个文件,全部取并集(双方都没有需要丢弃的改动):
- app/main.py:import 双方路由(我方 knowledge_management + 同事的 offsite_fund/
  promotion_material);include_router 段本已自动合并
- app/service/agent/bootstrap.py:import 与工具注册均取并集
  (query_customer_profile + query_financial_data 都注册)
- tests/integration/test_config_release_mysql.py:outbox 清理同时保留
  架构师的 event_type 限定(防误删其它域 outbox 行)与同事新增的 peer_release_id

同事这轮带入:11 个 alembic 迁移(建 offsite_* / promotion_* 等表)、
场外申购与推广素材 Agent、financial NL2SQL 工具。
注意:本库尚无 offsite_*/promotion_* 表,跑相关测试前需要执行 alembic upgrade。

边界核对:同事的场外代码未写入场内交易表(fin_sim_order/fin_capital_flow/fin_cash_ledger),
符合 AGENTS.md 规则 8。
2026-09-11 18:56:26 +08:00
Windows 6757ca047d docs: record market data pipeline acceptance 2026-09-11 18:46:36 +08:00
qyqy 928d0bcea3 chore: 按评审恢复 5 份文档、审计补 agent_type、修订环境口径
评审意见落地(§3.3 驳回删除 / §3.1 审计补充 / §1.4 解释器 / §2 编号):
- docs/04、06、10、13、99 全部恢复(评审:删除收益为零、保留成本同样为零);
  AGENTS.md 改为"保留但仅作历史参考"并列入 D 类,ARCHIVE 归档说明加作废声明
- 治理审计补留痕:interaction_audit.detail 增加 agent_type 与 governance_rewrite
  (治理层会改写对外输出,事后必须能追溯到是哪个 Agent 触发的;不改表结构,detail 是 JSON 列)
  新增 tests/unit/service/test_agent_persistence_audit.py 锁住该契约
- AGENTS.md 修订环境口径:解释器各用本机可用的那个(.venv 被 gitignore、不进仓库);
  config_release 与 Milvus schema 均属环境数据、不随代码合并,相关结论必须带环境限定;
  测试基线 1034;mypy 数字双方不可比(本机未装 sqlalchemy2-stubs,报错集中在模型层)
- docs/26 JWT 文档:因 21 已被风控迁移清单占用而改名,PR 描述里会单独说明
2026-09-11 18:42:46 +08:00
qyqy 5f82ac5107 feat(knowledge): 检索字段名改为运行时探测,两套集合 schema 都能跑
评审意见 §1.3 要求的修法。背景(双方实测共同确认):同一批集合名在两个开发环境里是两套不同 schema:
  我方:knowledge_id / snippet(无 visibility),行数 106/177/73
  架构师:doc_id / content / chapter / section / doc_no / visibility,行数 125/297/214
上一轮我把字段名硬编码成我方那套,在架构师环境会让 Milvus 报 field doc_id not exist
→ 三集合全失败 → 客服一律转人工(反向亦然)。硬编码任一套都会打挂另一套。

改法(采纳评审建议):
- 新增 app/core/knowledge_schema.py:describe_collection → 逻辑名到物理名映射,按集合缓存;
  缺必需字段的集合明确判为不可用并如实记 degraded,不静默零召回
- 检索服务改为逐集合探测:output_fields 只请求实际存在的字段;visibility 过滤有该字段才拼
- KnowledgeHit 对外形状不变,检索逻辑(字面召回/父子块/去重/置信判定)一行未改

测试:新增 17 个探测单测;架构师的关键词召回测试参数化为两套 schema 各跑一遍。
2026-09-11 18:42:42 +08:00
Windows 930dd59d67 feat: complete advisory market data refresh pipeline 2026-09-11 18:39:57 +08:00
张胜宇 1eb04f28b1 test: fix mysql profile snapshot cleanup 2026-09-11 18:09:36 +08:00
Windows 95633188f1 test: extend advisor e2e fixtures 2026-09-11 17:59:47 +08:00
张胜宇 27174b9ce2 feat: generate approved profile snapshots 2026-09-11 17:58:46 +08:00
张胜宇 a769130658 feat: add profile candidate review workflow 2026-09-11 17:47:06 +08:00
yuancong_0626 3f7c5ca74f merge: 同步 origin/qyqy_develop(风控扫描、知识检索、客服 Agent、协商等)
冲突处理:均为双方各自新增,按并集保留——
- .gitignore:本地 data/logs 忽略项 + 远端 .dsh-drop/
- app/core/config.py:offsite/promotion 与 risk_scan 配置项并存
- app/service/agent/bootstrap.py:场外/推介/风控/NL2SQL/知识检索 工具与 Agent 全部注册
- app/worker/__main__.py:场外邮件 Worker 接线 + runtime 关系服务/投影清理注入并存

收尾:新增 20260911_merge_risk_heads 收敛迁移双 head;按 docs/21 生成 config/jwt/dev 开发密钥。
2026-09-11 17:32:47 +08:00
yuancong_0626 61861cdef6 袁聪merge:合并分支 2026-09-11 17:26:18 +08:00
Windows 3e4f21388c test: complete advisor full-stack acceptance record 2026-09-11 17:19:40 +08:00
张胜宇 3a21afa847 feat: add customer profile candidate flow 2026-09-11 17:16:43 +08:00
yuancong_0626 0a78a9df76 忽略邮件落盘和日志目录 2026-09-11 17:02:14 +08:00
yuancong_0626 fd9598efdf 袁聪的第二次提交,项目已完整 2026-09-11 16:57:47 +08:00
lzf_0626 927a6fecc0 评审 NL_develop 交付说明:三个环境前提必须先对齐
产出 docs/NL_develop交付说明-评审意见.md(可直接转给组员),以及一个只读探查工具
tools/probe_knowledge_collections.py(Milvus 集合 schema 与行数)。

核查中发现的硬矛盾,都有可复核的证据:

1. 配置库不是同一个。对方说 active=216、合并前生效版本是 186;我这边实测 active=201,
   最近 5 条 id 就是 201/198/197/196/195 —— release.id 自增,同库不可能一边 216 一边 201。
   所以他以为已发布的 customer_service:faq=[search_knowledge, query_customer_profile]
   在这边并不存在,而他的画像出口复用的正是这个 key ⇒ 合并后被 ToolExecutor 失败关闭。
   同理他说的 fund_query_demo:fund_quote 缺失,我这边是存在的。

2. Milvus 也不是同一个。对方说现库字段是 knowledge_id/snippet、无 visibility、行数
   106/177/73;我这边实测是 doc_id/content/chapter/section/.../visibility 共 15 个字段、
   行数 125/297/214,且与 knowledge_search_service.py 的 OUTPUT_FIELDS 逐字一致。
   他这次的字段映射改动(doc_id→knowledge_id)在这边会直接报 field knowledge_id not
   exist,把客服知识检索整条打挂 —— 比他自述的"关闭 visibility 隔离"严重得多。
   建议不是 A/B/C 三选一,而是第四种:运行时探测字段名,两套 schema 都能跑。

3. 解释器不同。他用 .venv(项目里不存在,那是他机器上的 gitignore 目录),约定是
   D:\conda\envs\jr_py313。所以"mypy 151→181"跑不到本基线 —— 这边是 138 文件 0 错。

另指出:docs/26-JWT密钥管理与轮换.md 会与已存在的 docs/21-JWT密钥管理与轮换.md 重复,
建议并入 21;驳回删除 docs/04/06/10/13/99。

四项待裁决的答复:同意 agent_type 方案(要求补审计);字段映射改为运行时探测;
驳回删除编号文档;26 并入 21。
2026-09-11 16:51:52 +08:00
Windows 852bafb374 feat: add profile drift governance workflow 2026-09-11 16:42:31 +08:00
lzf_0626 5bbf8481ef Merge pull request '信封补齐、适当性矩阵修正与 Worker 失败原因落库' (#5) from qyqy_develop_1 into qyqy_develop
合并 qyqy_develop_1 第三轮工作:meta 信封补齐与公共化、适当性按第十二条匹配矩阵、Worker 失败原因区分固定文案与异常消息。
2026-09-11 16:42:29 +08:00
zhangshy a122f7afba 修复风控预警记录查询与准备脚本 2026-09-11 16:40:15 +08:00
lzf_0626 fcd3eaadd6 Merge origin/qyqy_develop:风控时间口径补齐 + Agent 截断提示
组员的 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,均在合并后的工作树上跑过。
2026-09-11 16:33:55 +08:00
lzf_0626 8b8883ccca Worker 失败原因不再只留类名:区分"可落库的固定文案"与"异常消息"
起因是查 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。
2026-09-11 16:28:19 +08:00
Windows 4088c634de feat: complete advisor conversation loop 2026-09-11 16:12:38 +08:00
张胜宇 ef098e6a4b feat: complete customer service safety and handover flow 2026-09-11 16:11:30 +08:00
qyqy e342670130 docs: 修正交付说明的净差异计数(新增本文档自身后 92→93、新增 50→51) 2026-09-11 16:09:16 +08:00
qyqy ac0b973006 docs: 新增给架构师的交付说明(PR 评审用)
- 与'接手 AI 交接文档'定位区分:这份讲'动了架构师什么、为什么、要不要他点头'
- 逐文件列出对其 7 个核心文件的改动与理由(base.py 协议变更、knowledge_search_service
  字段映射的三选一裁决、governance 免责声明范围、model_gateway 映射并入、bootstrap/runtime 并集)
- 明示 6 项待裁决/知晓事项(含 5 份文档删除、config_release 216 已生效、fund_query_demo 白名单缺失)
- 证据段附可复现命令与真机结果;mypy 181 的错数按'谁引入'拆分为 170/8/3
- 已知限制 4 条(memory_sync_outbox 无消费者、mypy 归属、Redis 降级、文档删除的连带说明)
2026-09-11 15:59:29 +08:00
qyqy 96a6e01634 docs: 交接文档与 AGENTS.md 同步到合并后状态
- 交接文档 §0/§5/§6 重写:分支改为 NL_develop、测试基线 1013、已知问题逐条标注当前状态、
  Git 段改为已推送 + PR 注意事项(5 份文档删除/26 号新增/字段映射三处跨分支决策)
- AGENTS.md 入口指向与基线数字同步:工具名改 search_knowledge、生效版本 id=216、
  测试 1013、mypy 181(含归属说明)、新增 Agent 与文档编号现状
2026-09-11 15:41:49 +08:00
qyqy fb7d2f7b6d merge: 跟进架构师最新 qyqy_develop(38 提交)
- model_gateway 冲突取并集:保留本线对 intent_classification→chat 的修正与空集回退,
  并入架构师补充的 5 个风控 task_type;未登记 task_type 的告警留痕一并保留
- docs/25 撞号(本人 JWT 文档 vs 架构师风控评审报告)→ 本人让号到 docs/26,同步 docs/19 引用
- 架构师恢复的 5 份编号文档(04/06/10/13/99)保留其版本(那 5 份已无引用,仅编号占位)
- bootstrap/model_gateway/docs/05/test_risk_agent_contract 自动合并成功

测试:1013 passed / 1 failed(既有空集缺陷)
真机:知识问答 succeeded 且声明仅 1 条;画像问答 succeeded;三端点 403/201/200/404 全绿
配置:release 216 仍为生效版本,工具白名单未被顶掉
2026-09-11 15:40:32 +08:00
qyqy cbd6de2721 Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-latest
# Conflicts:
#	app/service/model_gateway.py
2026-09-11 15:38:11 +08:00
Windows e8ec0af398 feat: add advisor recommendation review flow 2026-09-11 15:33:27 +08:00
qyqy 395bad25b7 fix(knowledge): 检索服务适配现库集合 schema、发布对齐的工具白名单、免责声明只由治理层注入
合并暴露的三个真机问题(单测全绿但线上必挂):

1. 检索服务字段名与现库集合不符 -> 静默零召回
   架构师那套按 load_knowledge_milvus.py 的 schema 读 doc_id/content/chapter/section/
   visibility,而现库三集合的真实字段是 knowledge_id/title/snippet/tags/version/intent。
   Milvus 对不存在的字段直接报错 -> 三集合全失败 -> degraded -> 客服一律转人工。
   改法:只改读取侧,用 _FIELD_ALIASES 映射;KnowledgeHit 对外形状不变(下游与测试不动)。
   代价已注明:没有 visibility 字段 -> 检索层内部资料硬隔离失效(现库 356 行均为对外知识)。

2. 发布白名单与代码上限不匹配 -> AGENT_PERMISSION_DENIED
   active 版本白名单是 query_knowledge,而合并后代码上限是 search_knowledge/check_suitability/
   query_customer_profile -> 交集为空 -> 所有知识问题 failed。
   改法:publish_customer_service_config.py 补 PROFILE_TOOL 进 faq 白名单,并让同 key 的
   继承项被本次定义覆盖(旧值原样继承会被子集校验 422 拒掉整次发布)。已激活版本 216。

3. 免责声明重复出现
   Agent 自己拼一句 + 治理层追加权威话术 -> 客户看到两条。改为只由治理层注入
   (话术属发布配置,改文案不该改代码)。风控等内部 Agent 不注入(结构化输出不被污染)。

测试:934 passed / 1 failed(test_fund_readonly_contract 既有空集缺陷,与本线无关)
真机:知识问答两问 succeeded 且只带一条声明;画像问答 succeeded;知识库三端点全绿
2026-09-11 15:27:16 +08:00
zhangshy d2cdbbac01 修复风控时间口径与Agent截断提示 2026-09-11 15:25:45 +08:00
lzf_0626 8d79bd9767 补齐三个成功响应缺失的 meta 信封;信封实现抽为公共(docs/05 §3.3)
"缺 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。
2026-09-11 15:21:36 +08:00
Windows 13bb4f257d feat: add allocation backtest evidence 2026-09-11 15:08:37 +08:00
qyqy 57c4add7d8 merge: 客服Agent+RAG+画像 与 架构师最新 qyqy_develop 合并
- customer_service.py 以架构师实现为骨架(三档置信/适当性/会话记忆/话题矩阵),嫁接本人画像出口
- 知识检索契约合并两条链路:架构师 search_knowledge(KnowledgeSearchInput) + 本线
  query_knowledge 链路所需常量(ALLOWED_CONSTANTS/VECTOR_DIM/intent_for_qa_id)
- bootstrap 保留架构师 6 工具/3 Agent,补回 query_customer_profile 与 get_milvus_knowledge_writer
- model_gateway 能力映射修正 intent_classification→chat,保留空集回退兜底
- governance 免责声明限定面向客户 Agent(agent_type 由定义透传),风控结构化输出不再被追加
- 修 JWT 密钥路径(config/jwt/dev)、文档 21 号撞号→25
- 测试基线 934 passed / 1 failed(既有空集缺陷)
2026-09-11 15:06:44 +08:00
zhangshy 5102eaac64 Merge remote-tracking branch 'origin/qyqy_develop' into RM2_develop 2026-09-11 15:06:42 +08:00
zhangshy 2ebe438ba5 docs: 增加风控Agent工具白名单与意图配置 2026-09-11 15:06:06 +08:00
lzf_0626 575b4c2baa 客服适当性改按第十二条匹配矩阵(更正我上一轮的复核结论)
你说得对: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。
2026-09-11 14:47:08 +08:00
wangjianlong_0626 e4c4099aaa wip: 客服Agent + RAG + 画像收尾(基于 6516ccb) 2026-09-11 14:46:40 +08:00
Windows ff71a1a724 feat: add dynamic advisor asset allocation 2026-09-11 14:44:29 +08:00
lzf_0626 4b0c148ae8 Merge pull request '风控 P3 收尾与适当性豁免额度校验' (#4) from qyqy_develop_1 into qyqy_develop
合并 qyqy_develop_1 第二轮工作:第十五条豁免额度校验、风控接口对齐信封/游标/幂等/SSE、trigger_rule_codes JSON 多值索引、docs/24 状态同步。
2026-09-11 14:38:49 +08:00
lzf_0626 c11e56c7aa 同步 docs/24 的过期状态;登录端点不改(docs/05 明确平台不实现)
docs/24 是客服阶段的总结,正文保留为当时的事实,本次做两件事:

1. 刷新"当前状态"表的数字:mypy 113→137 文件、unit+contract 497→664、
   integration 29→33、active 发布 181(5 项)→201(10 项:9 条 agent_tools + 1 条客服
   闲聊提示词)。数字由 tools/probe_release_state.py 只读查库得到,证据留档
   docs/evidence/release-state.json。
2. 追加第七节"后续更新",记录阶段总结写完之后的变化:两件待决策事项已落地(闲聊
   提示词已按继承语义发布、适当性两侧口径复核通过)、风控合并后的处理要点、以及
   三个仍然开着的口子(热线占位符、like %% 全表扫、offset 游标并发跳行)。

登录端点:**不改**。docs/05 §11 明确"JWT 签发、刷新、注销由统一身份认证模块负责,
Agent 平台不重复实现",本平台只做校验(app/core/security.py)。第五节第 8 条据此改写,
同时把第 7 条"风控尚未启动"标记为已完成。

顺带修正"已知缺口"里的一条:列表接口的 next_cursor/has_more 风控侧已补齐,客服侧仍待办。
2026-09-11 14:26:17 +08:00
lzf_0626 ff681df143 落地第十五条豁免额度校验;登记三处业务裁定
一、第十五条豁免规则(docs/25 第七节 #2,业务裁定:实现)

政策原文:C3→R4 签署风险揭示书后**可买**,但单只 R4 持仓不超过总资产 20%;
C4→R5 同理,上限 10%。越级购买本身不是违规,超出额度才是 —— 原先扫描侧只看
"留痕是否齐全",于是"签了字但买超额度"这种明确违规没有预警;研判侧也把豁免的
前提条件(留痕齐全)当成了结论,直接判"疑似误报"。

- 扫描侧:新增 EXEMPTION_LIMITS 与 RiskRuleEngine._exemption_state,核算
  "单只持仓 / 总资产"并写进证据快照;触发条件改为
  gap > 0 and (missing_trace or 超出额度)。
- 研判侧:_assess_rw007 先判额度再判留痕。超限 → 证据支持风险;留痕齐全且在额度
  内 → 疑似误报;留痕齐全但快照缺总资产/持仓 → 继续复核。

数据前提(tools/probe_exemption_data.py,证据见 docs/evidence/exemption-data-probe.json):
库内 fin_customer_profile 仅 1 行且 total_asset = 0.00、fin_holding 0 行、无任何申购
交易 —— 这条规则当前不会被触发,与 behavior_score 同源(画像与持仓由本项目之外的
流程写入)。因此刻意不把"算不出来"当成"超限":拿 0 去算会让每一笔 C3→R4 都变成违规,
豁免规则反倒成了误报源。上游把数据写入后无需再改代码即可生效。

二、三处业务裁定(此前挂在"待裁定")

- 模型网关 chat + tools 入口:本轮不补,按基座能力缺口记录。它要贯穿
  ModelGateway → … → BaseAgent 整条链路,属公共契约变更,演示联调期影响面大于收益。
- exclude(关闭误报)是否必须先"调查中":保持现状,不加门禁。
- 政策冲突:以第十四条 C ≥ R 为准;客服侧 check_suitability 复核后确认本来就按
  C ≥ R 实现,无需改动。

三、其他

- 新增 tests/unit/service/test_risk_judgement_rw007.py(6 例)与扫描侧 4 例。
- 风控文档 03/05 同步 RW-007 的豁免额度条件与研判口径。
2026-09-11 14:24:45 +08:00
lzf_0626 df96275ab5 给 trigger_rule_codes 加 JSON 多值索引;登记 P3 处理结果(docs/25 P3 #21)
实测确认 #21 描述准确:fin_risk_alert 只有 11 个普通 BTREE 索引 + 主键 + alert_no
唯一键,规则命中的 JSON_CONTAINS 查询 EXPLAIN 为 type=ALL、possible_keys=NULL,
即全表扫。MySQL 8.0.27 支持多值索引,故新增迁移 20260911_risk_rule_index:

    ADD INDEX idx_fin_risk_alert_trigger_rule_codes
      ((CAST(`trigger_rule_codes` AS CHAR(16) ARRAY)))

迁移幂等(先查 information_schema.STATISTICS),upgrade/downgrade 往返已验证。
生效后 EXPLAIN 变为 access_type=range 且 key 命中该索引,原始证据留档在
docs/evidence/risk-index-probe.json(由 tools/probe_risk_index.py 生成,只读探查)。

只解决一半,另一半如实记为限制:若干 like(f"%{keyword}%") 全表扫无法用 B-tree 索引,
根治需全文索引 + 中文分词组件(部署依赖),本轮不做。

revision 名刻意压到 32 字符以内 —— alembic_version.version_num 是 VARCHAR(32),
超长会在写版本号时报 1406,而 DDL 是非事务的,那时索引已经建好了。

docs/25 追加"P3 处理结果"表,逐条登记 17-25 的状态:#19 是协议级重做(keyset 分页)
不单方面改,#21 部分修复,#25 前半段不成立,其余已修。
2026-09-11 14:19:05 +08:00
lzf_0626 661b17c244 风控全量查询封顶并在响应里标注截断(docs/25 P3 #20)
预警详情里的资金流水/持仓/登录记录、以及日报的四组预警都是"一次性取全量"。某个客户
的历史数据一旦异常膨胀,单次响应就能把内存和连接拖垮;登录记录此前更是连时间窗都
没有。

处理原则是"封顶但**不静默**":详情证据每类最多 200 条、日报每组最多 5000 条,用
`limit + 1` 多取一行判断是否真被截断(只看"取满没取满"会把恰好等于上限的正常数据
误报成截断),被截断时:

- 详情新增 `evidence_truncated: string[]`,列出被截断的证据类型;
- 日报新增 `data_truncated: bool` —— 日报的 daily_alert_count 等数字直接来自行数,
  静默截断等于给出一份看起来正常、实际少统计的日报,那比报错更难发现。

`scalars(...)` 统一用 `list(...)` 而不是 `.all()`,实现不再依赖 ScalarResult 的专有方法。

新增 tests/unit/repository/test_risk_repository_limits.py(3 例,用 literal_binds
编译 SQL,确保断言看到的是 201 而不只是"有 LIMIT"),并补日报与详情的标记用例;
风控文档 06 同步登记两个新字段与写接口的幂等要求。
2026-09-11 14:16:07 +08:00
Windows 8ffd08b4ce feat: add portfolio graph enrichment 2026-09-11 14:14:50 +08:00
lzf_0626 15866d4564 风控写接口接入平台幂等(docs/25 P3 #22)
docs/05 §5.1 把"业务写接口"列为必须携带 Idempotency-Key 的接口,风控 6 个 POST
(手工扫描、确认接收、进入调查、关闭误报、完成结案、升级处理)此前一个都没带,
重复提交会二次驱动状态机。

复用平台的 api_request_receipt 与 ApiTransactionService,但新增 execute_in:原来的
execute 自己开 SessionFactory() 和 session.begin(),而 RiskActionService._finish
会在内部 commit,套进去就成了"内层提交外层事务"。execute_in 改为在调用方传入的
session 上读写幂等记录,幂等记录因此与业务写入同处一个事务(§5.2)。

scope 用实际路径(含 alert_no)而不是路由模板:§5.1 的幂等范围是
user_id + method + normalized_path + idempotency_key,把路径参数折成模板会让同一个键
在不同预警之间互相回放 —— 那是把两次不同资源的操作当成一次。

测试:单测加幂等透传替身与"缺键即 422"用例;新增
tests/integration/test_risk_idempotency_mysql.py,覆盖同键回放不重复执行、同键不同
正文 409、缺键/非 ASCII 拒绝,以及端到端"重复 POST 只调用一次处置逻辑"。
2026-09-11 14:12:56 +08:00
Windows 47a5ca498c feat: complete advisor product suitability gates 2026-09-11 14:09:23 +08:00
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 a572c09a5c 风控列表游标绑定用户与查询条件(docs/25 遗留 P3)
docs/05 §3.8 要求游标绑定用户、查询条件、排序字段和方向,此前实现只把 offset
用 base64 包了一层:任何登录用户拿到别人的游标都能继续翻,换个筛选条件也能继续翻
(偏移量对不上就静默返回错页)。现在游标里携带 SHA-256 指纹:

- 指纹口径 = user_id + data_scope/customer_ids + 查询条件(排除 limit/cursor)
- 刻意排除 limit:它是分页参数、不是查询条件,算进去只会让翻页时改页大小失效
- /evidence/{source} 的 source 是路径参数,单独并入指纹,否则 customers 的
  游标能直接拿去翻 products
- 指纹不符一律 InvalidCursorError -> 400 INVALID_CURSOR(docs/05 §3.6)

新增 2 个单测:换用户/换筛选/换 data_scope 失效、改 limit 仍有效、
不同证据类型游标不互通。
2026-09-11 14:05:37 +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