Commit Graph
124 Commits
Author SHA1 Message Date
lzf_0626 01ee68034d 新增登录接口:账号密码换访问令牌(POST /api/v1/auth/tokens)
背景:客户 / 员工 / 管理员三种身份此前无法区分。但区分逻辑其实早就完备 ——
bootstrap.py 里各 Agent 的 allowed_roles 一直是分开的(CustomerServiceAgent 只要
customer、RiskAgent 要 risk_operator/admin、PlatformProbeAgent 只要 admin),
唯独缺"怎么证明你是谁";sys_user.password_hash 字段也一直存在,只是全是占位符
(种子写 'x'、worker 身份写 !worker-only-no-password-login!),从没写过真实密码。

实现:
- app/service/auth_service.py:bcrypt 校验 + 签发只含 sub 的 JWT + 审计。令牌里只放 sub
  是刻意的:角色/权限/数据范围由 IdentityService 每次请求查库解析
  (identity_repository.load_context),权限变更因此立即生效,现有鉴权链路一行未改。
- app/api/controllers/auth.py + app/api/schemas/auth.py:POST /api/v1/auth/tokens,
  响应含 roles/data_scope 供前端决定进哪个界面(鉴权仍以库里实时数据为准)。
- tools/set_user_password.py:设密码(客户 123456 / 员工 666666 / 管理员 88888888)。
  ⚠️ 脚本与文档均标注"仅限演示环境",这三种弱口令上线前必须更换。
- pyproject / requirements 加 bcrypt(cryptography 只用于 JWT,不提供密码哈希)。

安全约定(逐条有实现与测试):失败不区分原因 —— 用户不存在/密码错/账号停用返回同一条
401,否则接口就成了账号枚举器;用户不存在时也跑一次 bcrypt 以抹掉时序差异;
成功与失败都写 interaction_audit(actor_id 可空正是为失败场景准备的);绝不记录密码。

过程中踩到一个自己挖的坑:给登录路由挂了通用的 enforce_rate_limit,而它声明依赖
build_request_context ⇒ 变成"要登录先登录",所有登录都 401。改为新增
enforce_login_rate_limit:按客户端 IP 独立限流(60 秒 10 次)、不依赖认证上下文。
集成测试据此调整:注入恒放行替身隔离跨用例的计数累积,同时保留一个恒超限用例验证闸门
确实会拦 —— 不能因为加了替身就把这道防线测丢。

接口登记:docs/05 §19 加 A034;并更新 §11 —— 那里原写"JWT 签发、刷新、注销由统一身份
认证模块负责,Agent 平台不重复实现",现注明平台只做登录这一步,刷新/注销仍归该模块。

验证:ruff 干净 / mypy 183 文件 0 错 / 文档守卫 37 份无重号(此前因 docs/21 重号失败)/
unit+contract 1140 passed / integration 90 passed。
2026-09-11 20:43:21 +08:00
lzf_0626 62501747a6 新增 sys_user 认证现状探查(判断"账号密码登录"可行性)
tools/probe_auth_state.py:只读统计 sys_user 的 status / user_type 分布与 password_hash
的格式特征(只输出前缀与长度,不输出完整值)。

结论(docs/evidence/auth-state.json):库里 5 个用户,password_hash 全是占位符 ——
4 个是 'x'(tools/seed_test_rbac.py 写入),1 个是 '!worker-only-no-password-login!'
(alembic 迁移写入的哨兵值,名字本身就表明"不用于密码登录")。
has_real_password_hash = false。

⇒ 在本项目做"账号密码换令牌"不是"加个端点"的事:没有密码可校验,等于要新建一套密码体系
(选算法 + 加依赖 + 定义写入流程 + 改上游数据),而 password_hash 是基线已有字段
(规则 4:不得改变既有业务含义)。docs/05 §11 也已明确该职责属统一身份认证模块。

顺带记录一个数据质量问题:user_type 取值不统一(employee / 员工 两种写法并存)。
2026-09-11 20:30:27 +08:00
lzf_0626 76e87a33a7 更新 AGENTS.md 的表数口径:51 → 68(场内 51 + 场外/推广 17)
合并袁聪那 17 张表后,本文件仍写"52 张含 alembic_version = 51 张业务表",与
tools/audit_schema.py 实测的 68 张对不上。改为写明构成,并指向
docs/28-场外与推广域数据表登记.md —— 否则下一个人看到 68 会误以为是有人绕过基线
偷偷建表(audit_schema 的期望值是从迁移动态推导的,不是手写清单)。

核验命令里的 .\.venv\Scripts\python.exe 一并改成 python,与本节"各用本机可用的那个"
的口径一致(架构师环境是 conda)。
2026-09-11 20:24:39 +08:00
lzf_0626 3029d0ca9f 更新 release-state 证据:画像工具白名单已补发(release 254 active) 2026-09-11 20:24:07 +08:00
lzf_0626 f09ea9e988 Merge origin/NL_develop:客服画像出口、知识管理三端点、合规语境与知识向量链路
NL 线(含其并入的袁聪场外/推广域)。唯一冲突是 .gitignore —— 双方都往同一区域加了
.workdir/,取对方版本(他的更完整,含 .tmp/ 与说明),顺带修掉我之前用
Add-Content -Encoding utf8 造成的编码混合(read 工具当时报 invalid UTF-8)。

合并后修的问题 —— 都不是"改别人业务逻辑",是让门禁能绿:

1. 缺运行依赖 python-docx。document_parser.py 解析 .docx 用它,但 requirements.txt 与
   pyproject.toml 都没声明 —— 别人环境跑知识入库会直接
   ModuleNotFoundError: No module named 'docx'。已补声明。
2. ruff 7 项:其中 tests/conftest.py 的 F821 Undefined name 'Path'(他的 tmp_path 修复
   写了字符串注解 "Path" 却漏 import,运行时不求值所以没炸,但 mypy/ruff 会抓)、
   tools/publish_customer_service_config.py 的 F841 inherited_keys 死变量(他改同 key
   覆盖、换成 inherited_only 后忘删旧的)、3 处 E501,另 2 项 ruff --fix 自动修复。
3. 合规基线种子未跑:integration 的 test_compliance_seed_mysql 4 个用例要求
   agent_negative_word 有 7 条 active 且已复核、agent_reply_template 覆盖 6 场景。
   跑 tools/seed_compliance_baseline.py(11 条 active 规则 / 6 个场景模板)后 80 passed。

验证:ruff 干净 / mypy 180 文件 0 错 / unit+contract 1140 passed /
integration 80 passed / 表数 68(alembic 已在 20260911_merge_risk_heads)。

唯一失败 tests/unit/repository/test_fund_readonly_contract.py 是双方一致的既有缺陷:
它断言 Base.metadata 里的 fin_* 表集合,而实测为空集 —— 即该测试依赖别的测试先导入模型的
副作用,单独跑必失败。NL 方也明确"不修不报",此处照办,仅记录。
2026-09-11 20:22:59 +08:00
lzf_0626 b5b068065f 修掉合并带入的 3 个 mypy 错(全在袁聪的场外邮件模块,各一行、不动逻辑)
1. offsite_document_recognition_adapter.py:788 —— 冗余 cast。
   `value in ("summary", ...)` 已经把类型收窄到那个字面量联合,cast 多余,删掉即可。
   该文件另有 13 处 cast,删这一个不影响 import。

2. offsite_fund_service.py:1203 —— dict 不变型。
   字面量里只有一个 bool,mypy 推断成 dict[str, bool | None],而 dict 是**不变型**,
   不是 dict[str, object] 的子类型。运行时本来就是合法值,加一行显式标注即可。

3. offsite_fund_service.py:1240 —— 标注宽于实际。
   `get_content()` 只定义在 EmailMessage 上,而调用方传的是
   BytesParser(policy=policy.default).parsebytes(...) 的返回值(本来就是 EmailMessage)。
   形参从基类 Message 改成 EmailMessage;import 同步替换(Message 仅此一处使用)。

三条都不影响运行:tests/unit/worker/test_offsite_mail_worker.py 的 6 个用例覆盖的正是
这条路径,修前修后都通过。修的意义在于——strict=true 下留着这 3 个错,mypy 在这个文件上
就失去价值,而 offsite_* 是刚合并进来、最需要类型检查兜底的新域。

验证:mypy 163 文件 0 错 / ruff 干净 / unit+contract 810 passed / integration 76 passed。

(另:本机 pytest 默认 basetemp 被权限占住的问题仍在,用重定向 TEMP 绕开,注意先建目录;
NL_develop 的 conftest 修复合并后可根治。)
2026-09-11 20:15:17 +08:00
lzf_0626 f60915b70b 补声明测试依赖 aiosqlite(合并袁聪线后暴露)
合并后 tests/unit/worker/test_offsite_mail_worker.py 的 6 个用例直接
ModuleNotFoundError: No module named 'aiosqlite' —— 它们走 SQLAlchemy 的 SQLite
内存库,而 aiosqlite 既不在 requirements.txt、也不在 pyproject 的 dev 依赖里
(dev 原先只有 pytest / pytest-asyncio / ruff / mypy)。

后果不是"本机缺个包":任何人 clone 下来、按文档装完依赖、跑测试,这 6 个用例必然
失败,而且报错指向 SQLAlchemy 内部(aiosqlite.py:480),看不出是缺依赖。
已加入 dev 依赖并注明用途。

同时记录一个本机环境问题(未改代码):pytest 的默认 basetemp
%TEMP%\pytest-of-Windows 被权限更高的会话占住,make_numbered_dir 扫不动 ⇒ 18 个
setup ERROR。NL_develop 的 conftest 修复(tmp_path 落到仓库内 .workdir/)能根治,
等合并;本次用重定向 $env:TEMP 临时绕开(--basetemp 无效,因为 pytest 仍会扫默认 root)。
另需注意 .workdir 目前不在 .gitignore 里,那套修复合并进来前要补上。

合并后现状:unit+contract 810 passed / integration 76 passed / ruff 干净 /
表数 68(alembic 已 upgrade 到位)/ mypy 3 个错(全在袁聪的 offsite_fund_service.py,
属真实类型问题,未擅自改别人的业务代码)。
2026-09-11 20:08:13 +08:00
qyqy 58849addcc docs: 交付说明补第二轮批复(含 mypy 归因纠正);修正残留过期数字
上一轮我只更新了接手文档,**漏了交付说明**——它的末次更新(19:15)早于架构师 19:43 的
第二轮答复,导致它还写着已被推翻的结论。本次补齐:

- 新增 §0-B:架构师第二轮的 9 条批复逐条落地对照(含"mypy 归因纠正""docs/00 不改另立登记"
  "两件事归他""那 2 个失败按新引入对待")
- §5.2 整体重写:**认错并给出四组对照实验**(SQLAlchemy 2.0.34+1.14→173、2.0.52+1.14→6、
  2.0.52+1.20→3),说明真因是 SQLAlchemy **补丁版**而非"缺类型存根";环境已升级到
  2.0.52+1.20.2,mypy 184→3;并列出剩余 3 个错全在同事那条线的文件里
- §5.1 接受架构师口径:那 2 个失败**不是"双方共有的既有失败"**,应作为合并后**新引入的失败**跟踪
- §2 净差异分两段重算:本线增量 16 模块/17 测试/5 工具 vs 同事那条线 334 文件/11 迁移
- §6/§7 更新为第二轮后的状态(哪些已批准、哪些归他、哪些已落地)
- 修正残留过期数字:结构审计 51→68 张表、去掉"缺 SQLAlchemy 2.0 类型信息"的错误论断
- 章节号整理:两个同名 "## 0" → "## 0-A / ## 0-B"

文档守卫 35 份无重号;mypy 3 错;测试 1218 passed。
2026-09-11 20:06:23 +08:00
qyqy f7a666fb5a fix(env)+docs: 按架构师第二轮纠正 mypy 根因(环境版本);补场外/推广域表登记
架构师第二轮答复里有一条技术纠正是对的,我原归因错了:
- 我原判断"184 个 mypy 错是因为缺 SQLAlchemy 2.0 类型信息"——**方向对了一半、结论反了**。
  SQLAlchemy 2.0 自带 py.typed,根本不需要 sqlalchemy2-stubs(那是 1.4 用的)。
- 实测复现矩阵(决定性证据):
    SQLAlchemy 2.0.34 + mypy 1.14.1 → 173 错(我原来的环境)
    SQLAlchemy 2.0.34 + mypy 1.20.2 → 173 错
    SQLAlchemy 2.0.52 + mypy 1.14.1 →   6 错
    SQLAlchemy 2.0.52 + mypy 1.20.2 →   3 错
  ⇒ 主因是 SQLAlchemy 的**补丁版本**(173→6),mypy 版本是次因(6→3)。
- 已把环境升到 SQLAlchemy 2.0.52 + mypy 1.20.2(均在 pyproject 约束内),
  mypy 从 184 降到 **3 个错**(剩下的 3 个全在同事的 offsite 文件里,非本线代码)。
- 没有把 sqlalchemy2-stubs 写进依赖(采纳架构师明确要求)。
- 交接文档已重写该条,并留下"两个反面教训":不要装 1.4 的存根包;补丁版差异会造成量级差异,
  若要门禁稳定需把 SQLAlchemy 钉到具体补丁版(属公共约定,未擅自改)。

按架构师裁决补文档(他裁定:不动 docs/00,另立登记):
- 新增 docs/28-场外与推广域数据表登记.md:17 张表逐表登记(表名/来源迁移/归属域/当前行数),
  并做规则 8 的**两向边界核对**——场内代码零引用这 17 张表(config.py 里的 offsite_ 只是配置项名)、
  场外代码零写场内交易表
- docs/08 审计口径更新为「场内 51 + 场外/推广 17 = 68」,并新增"第四种坏状态"
  (alembic_version 已指向新 revision 但表没建)的处置说明
- 澄清一个易误读点:audit_schema.py 的期望集合是**动态推导**的(读 baseline_generated.sql
  + 扫描 alembic/versions/*.py),表数 51→68 是自动结果,**没有人手工改期望值**;
  代价是只增不减(DROP 表会报 missing 假失败)

测试:3 failed(1 既有 + 2 环境相关)/ 1218 passed;文档守卫 35 份无重号;
mypy 3 错(180 文件);结构审计 68 张表通过。
2026-09-11 20:00:56 +08:00
lzf_0626 9957ddc191 Merge pull request 'NL_develop 评审答复与画像白名单补发脚本' (#6) from qyqy_develop_1 into qyqy_develop
合并 qyqy_develop_1 第四轮工作:NL_develop 评审答复、画像工具白名单补发脚本、docs/08 §8。
2026-09-11 19:58:29 +08:00
lzf_0626 8268646b0c 答复文档补充 §8:两个既有发布脚本会丢提示词(写补发脚本时发现)
publish_customer_service_config.py 与 publish_risk_agent_config.py 的
active_config_items() 都自己写 SQL、只查 platform_config_item,而
ConfigReleaseService.effective_snapshot() 要读全部三张受管表。用它们发版会把生效版本里的
提示词一起清掉 —— 且没有任何声音:_warn_dropped_items() 只告警不阻断激活,Agent 侧
_chitchat_prompt 有兜底会回落代码默认值,功能看着正常(与 release 174 那次事故同型)。

已在答复文档里建议改用 effective_snapshot(),并列出三个细节:同 key 覆盖(对方已修)、
JSON 列必须归一化(SELECT * 读出来可能是字符串,不解析会静默走进"没有该 key"分支)、
提示词搬家要重分配 version 且按 PromptPayload 字段挑(漏 input_schema/output_schema 即丢)。

另提醒时序:admin 端校验「配置 ⊆ 代码 allowed_tools」,画像工具那一版必须等代码合并后才能发,
否则 422 且报错只有"配置超出 Agent 工具上限"。
2026-09-11 19:52:08 +08:00
lzf_0626 6c09cdecbd 新增补发画像工具白名单的脚本(dry-run + 合并前防呆)
tools/publish_profile_tool_whitelist.py:把 query_customer_profile 加进
customer_service:faq,供 NL_develop 合并后在本环境补发配置。

关键设计都来自这轮评审核实到的事实:

1. 继承走 ConfigReleaseService.effective_snapshot(),一次拿全三张受管表
   (platform_config_item / prompt_template_version / model_routing_rule)。
   既有的两个 agent_tools 发布脚本都绕开它、自己写 SQL 只查第一张 —— 用它们发版
   会把 201 里那条客服闲聊提示词一起清掉,而 Agent 侧有兜底、功能看着正常、没有告警
   (这个事故在本项目真实发生过一次)。
2. 同 key 覆盖:继承项里的 customer_service:faq 必须被本次新值覆盖,否则旧值
   ["search_knowledge"] 会被 admin 端校验拦下、整次发布失败。
3. 提示词搬运重分配 version(唯一键含 version),并按 PromptPayload 的 9 个字段挑字段、
   归一化 input_schema/output_schema —— 漏带就是静默丢失。
4. 前置防呆:admin 端校验「配置 ⊆ 代码 allowed_tools」(admin_service.py:219-220),
   合并前跑必然 422,而报错只有"配置超出 Agent 工具上限"。脚本先查代码上限:
   dry-run 给警告后继续预览,正式跑直接中止。
5. JSON 列归一化:SELECT * 读出来的 JSON 列可能是字符串,不解析会静默走进
   "生效版本里没有该 key"的分支(本脚本第一版就是这么错的,诊断打印才定位到)。

实测:--dry-run 打印 9 条配置项 + 1 条提示词(v2→v3 重分配)与唯一变化;
正式跑因代码尚未合并而中止,未产生任何写入。
2026-09-11 19:51:44 +08:00
lzf_0626 d28ccdd88c 答复 NL_develop 第二轮:批准整改,纠正 mypy 归因,裁定 docs/00 不动
产出 docs/NL_develop评审回复-架构师答复.md(可直接转给对方)。三条核实结论:

1. docs/21 在我这边本来就重号(21-JWT密钥管理与轮换.md + 21-风控业务第二版迁移
   清单.md),tools/check_authoritative_docs.py 当前就是失败的 —— 我此前只跑
   ruff/mypy/pytest,没跑到文档守卫。对方的让号顺手修好了这个既有故障,
   故批准 docs/26 与 docs/27 两处让号。

2. 对方的 mypy 归因需要纠正。他说主因是缺 SQLAlchemy 2.0 类型信息;我这边
   mypy 1.20.2 + SQLAlchemy 2.0.52 + 未装 sqlalchemy2-stubs → 0 错,而
   pyproject.toml 约束是 sqlalchemy>=2.0,<3 / mypy>=1.14,<2。SQLAlchemy 2.0 自带
   py.typed,sqlalchemy2-stubs 是给 1.4 用的,装了反而按 1.4 的 API 报错(他自己也
   观察到"还会换一批新错")。真实根因是他的 .venv 没满足 pyproject 约束。

3. 他的迁移警告对我不适用。我这边 alembic current = heads = 20260911_risk_rule_index
   (单一 head,只有我自己的迁移),schema audit 报 51 business tables —— 即"表没建、
   版本号也没跑",与他那边"版本号跑了、表没建"是两种不同的坏状态。合并后我必须补跑
   alembic upgrade heads。

裁定:
- 补发配置(customer_service:faq 加 query_customer_profile)归我做;
- memory_sync_outbox / GraphProjectionWorker 消费端归我(生产者在他那边);
- 两处让号批准;
- docs/00 不动,另立文档登记场外/推广域 17 张表,并更新 docs/08 审计基线口径
  (依据 AGENTS.md 规则 8:场外基金运营流程独立,不得写入场内交易表)。
2026-09-11 19:40:17 +08:00
zhangshy beb67f0a26 优化奶龙字段语义理解 2026-09-11 19:37:26 +08:00
qyqy f1b8cb859a docs: 新增《接手文档 · NL_develop 这条线(给架构师)》
面向"接手维护"而非"review"的第三份文档,与另两份分工明确:
- 交付说明 → review 用(我动了你什么、为什么)
- 评审意见回复 → 对照你的评审意见
- **本文** → 接手维护用(起点在哪、有哪些坑、哪些待裁决)
- 给下一位开发者 → docs/superpowers/handoff/…

内容要点:
- §1 三条最要紧的结论:① 两台机器不是同一套环境(附三处实测证据)并由此立下两条硬纪律
  (环境相关结论必须带环境限定、禁止硬编码 Milvus 字段名);② **表数已从 51 变 68**
  (同事 11 个迁移新建 17 张 offsite_*/promotion_* 表),而 docs/00 基线与 docs/02 未更新
  —— 已提示、未擅自改不可变基线;③ 测试 3 failed 均非代码缺陷(附证据)
- §2 环境口径(含 Docker Desktop 不常驻、mypy 不可比的实测根因)
- §3 分支与提交 + 两个可回退的备份分支
- §4 交付清单(8 项,含状态与位置)
- §5 必须知道的 5 个坑(Milvus 读一致性与字节截断、config_release 整版本替换、
  两个 outbox 不能混、审计表无 agent_type 列)
- §6 待你裁决的 3 件事(白名单补发归属、接线归属、两处文档让号)
- §7 建议你拍板的两件事(文档体系口径、docs/00 是否补 17 张新表)
- §8 接手后先跑的 5 条自检命令
- §9 四份文档的分工表(防止读串)
2026-09-11 19:32:19 +08:00
qyqy c6a52a36a8 docs: 新增《评审意见回复》,逐条对照架构师的评审意见
按评审意见结构逐条回复,每条都给实测证据与落地位置:
- §1 环境不是同一套(三处独立证据:config_release 总 4 条 vs 最高 201、
  Milvus 字段/行数三项全不同、代理表命名不同),并据此提出团队纪律
  "环境相关结论必须带环境限定"
- §1.3 承认我原先的 A/B/C 三分法**前提就错了**(以为是"选哪套 schema"的决策问题,
  实际是"两套环境"的兼容问题),已按架构师方案改为运行时探测并用双 schema 测试锁住
- §1.4 给出 mypy 差异的根因(本机缺 SQLAlchemy 2.0 类型信息,装 sqlalchemy2-stubs
  181→43、卸载回 184),明确不再拿该数字当结论,也不擅自改 mypy 公共配置
- §2 澄清 docs/26 是重命名而非并存(21 已被风控迁移清单占用)
- §3.1 审计留痕已补并有真机证据(含改动前 false 的对照行)
- §4 给出 memory_sync_outbox / GraphProjectionWorker 的**双环境对照表**,
  证明与架构师的发现同源(组件写好、线没接),且我方更进一步(事件已写进表里)
- §3 告知同事那条线已并入、11 个 alembic 迁移已执行(此前"版本号跑了表没建"),
  并说明我方修的两处重号与测试临时目录问题
- §5 列出需要架构师确认的三件事(配置补发归属、接线归属、两处让号)
2026-09-11 19:15:50 +08:00
qyqy f2ac8a4460 docs: 交付说明按评审意见第二次修订;修同事带入的文档重号
评审意见逐条落地(详见文档新增的 §0 对照表):
- §1.1/§1.2/§4:确认两台机器连的**不是同一套 MySQL/Milvus**(我方 config_release 总共 4 条、
  最高 216;架构师侧 201 + 9 条白名单),据此把"216 已在共享库生效"整体改写为
  "**我方环境**已发布,你那台需补发";补发由架构师做(配置属环境数据,不随代码合并)
- §1.3:字段映射已改为运行时探测(见上一提交),文档里的 A/B/C 三选一整体替换为探测方案说明
- §1.4:mypy 那条改写——查清主因是**本机缺 SQLAlchemy 2.0 类型信息**(装 sqlalchemy2-stubs
  181→43,卸载回 184),明确"本机数字不可作为质量结论、两边不可比";我文件里的 8 个真实错误已修
- §2:明确 docs/26 是**重命名**(原 21,因 21 已被风控迁移清单占用),不是新增、不会并存
- §3.1:审计留痕已补(agent_type + governance_rewrite)
- §3.3:5 份文档删除**已撤回**(上一提交)
- §5/§7:更新验证数字(1218 passed)、两个环境相关失败的证据、memory_sync_outbox 双环境对照表

另修一处**同事那条线带入的重号**:docs/15-金融NL2SQL工具接入说明.md 与既有
docs/15-Agent组员详细开发与使用手册.md 撞号 → 新那份让号到 docs/27
(依据:手册被 docs/16、docs/17、AGENTS.md 三处引用,改名代价更大)。守卫恢复通过(32 份)。
2026-09-11 19:15:23 +08:00
qyqy 8cff6b35f9 fix(tests): tmp_path 落点改到仓库内,消除 33 个与缺陷无关的 setup 失败
问题:本机 `%TEMP%\pytest-of-Windows` 被权限更高的会话建过,当前用户无权写入,
于是所有用 `tmp_path` 的用例在 setup 阶段批量 ERROR(WinError 5)——实测 33 个,
散布在 offsite 附件预览、通知发送、promotion、worker 等处。这类噪声会让
"到底哪里坏了"完全看不出来(同事这批新用例首次把它暴露出来)。

修法:在 tests/conftest.py 覆盖 `tmp_path`,把根目录改到仓库内 `.workdir/pytest-tmp`
(已在 .gitignore)。语义不变——每个用例仍拿到一个**新建的空目录**(残留目录会污染断言)。
只覆盖 `tmp_path` 而不动 `tmp_path_factory`:后者是 pytest 私有构造,参数随版本变化
(实测直接实例化 `TempPathFactory(...)` 会 TypeError),而本仓库用例只用 `tmp_path`。

同时把 pyproject.toml 里那条 `basetemp = ...` 删掉:pytest **只在命令行认 basetemp**,
写在 ini 里会被静默忽略(实测无效),留着会让人误以为已配好。改为注释指向 conftest。

效果:不带任何参数 `pytest -q` 从「3 failed + 33 errors」变为「3 failed,0 error」。
2026-09-11 19:13:43 +08:00
qyqy 636dcbbe7e chore: 修我文件里的 mypy 类型错误(8 处)
源自评审 §1.4「数字不可比」引发的核查,结论比预想更有价值:

**mypy 报错的主因不是代码质量,而是本机缺 SQLAlchemy 2.0 的类型信息。**
装上 `sqlalchemy2-stubs` 后 181 → 43(该类存根是 2.0 之前的旧包,会换一批新错:
`mapped_column`/`DeclarativeBase` 不存在),卸载后回到 184。**本机 mypy 数字不可作为
质量结论,双方也不可比。** 但那 184 里有 8 个是**我文件里的真实错误**,已修:

- `knowledge_retrieval_service`:返回类型 `Mapping` → `dict`(回表后要就地补写
  `score`/`intent`,而 `Mapping` 是只读协议);`ids` 显式标注并过滤 `None`;
  去掉 3 处已失效的 `type: ignore`(strict 下 unused-ignore 本身是错误)
- `knowledge_management`:服务工厂返回类型 `Any` → `KnowledgeManagementService`
  (`TYPE_CHECKING` 期导入,运行时仍惰性,不引入循环依赖),消掉 3 个 `no-any-return`

未动的:`model_gateway` 2 处 `dict-item`(`ModelEndpointConfig` 实际具备协议要求的
全部字段,属 SQLAlchemy `Mapped[T]` 在缺存根时的消解问题,不是真缺陷,不用 cast 掩盖)。
2026-09-11 19:13:43 +08:00
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
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
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
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
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
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
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
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
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
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