From 244f03917bbeeafe312a01f3cd18c9772639fd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Sat, 12 Sep 2026 11:27:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E5=A4=8D=20ZSY=20=E7=9A=84=E5=BA=95?= =?UTF-8?q?=E5=BA=A7=E6=89=A9=E5=B1=95=E7=A1=AE=E8=AE=A4=EF=BC=9B=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=20docs/00=20=E7=9A=84=E4=B8=80=E5=A4=84=E5=B7=B2?= =?UTF-8?q?=E7=9F=A5=E5=81=8F=E5=B7=AE=EF=BC=9B=E6=9B=B4=E6=96=B0=20AGENTS?= =?UTF-8?q?.md=20=E8=BF=87=E6=9C=9F=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 对 ZSY 三件事的核实与裁决(docs/33-ZSY底座扩展确认-回复.md) 1. **访客身份**:`data_scope="public"` 我扫了全平台 26 个消费点,对未知值的行为一致 fail closed(6 处 `== "all"` 判假、1 处 `!= "all"` 会要求 customer_ids 非空否则 403、 1 处显式白名单直接拒),**安全,不用改**。但有一条硬约束必须先确认:全平台有 **72 处 `int(context.user_id)`,只有 3 处做了防御**——若访客的 sub 不是纯数字, 其中"权限被拒时要写审计"的路径会把本该 403 的情况变成 500。已要求他确认 sub 为纯十进制数字、且 visitor 分支复用同一套 `jwt.decode`(含 isdecimal 校验), 而不是另写第二个鉴权入口。 2. **客服 Agent**:`allowed_roles` 扩集合与确定性安全路由可接受;`recalls_customer_memory` 是新声明字段,已要求**默认值必须是 True**(否则会静默关掉所有既有 Agent 的记忆召回, 界面看不出来、只表现为回答变差)。品牌名(南方科技 → 奶龙基金责任有限公司)属业务 口径,已上报项目方定,不由技术侧拍板。 3. **current_customer_id**:**他的判断正确**。我独立实测 information_schema: `EXTRA=''` 且 `GENERATION_EXPRESSION=''`,配合 baseline_generated.sql 无 GENERATED 子句、seed_profile_demo.py 的注释、profile_repository.py 用原生 SQL 显式写入, 四处一致 ⇒ `docs/00` 第 783 行"生成列"的描述是错的。处置:按真实 schema 映射成 普通可空列、**不改 docs/00**(规则 1)、**不补迁移**(DDL 本身正确,为让文档成真而加 生成列会改变既有列语义,违反规则 4)、但**必须登记这处偏差**。 另:他删了两个死代码文件(含一处硬编码 Milvus 字段名,违反 AGENTS.md §E),方向认同, 但要求他在 PR 里附上两条 git grep 的实际输出以证明"全仓唯一引用是它自己的单测"。 ## 落实我在回复里承诺的两件事 - `docs/08-数据库结构审计基线.md` 新增"六、已知文档偏差",逐条登记上述偏差(含四处 证据与处置口径),并注明发现方式; - `AGENTS.md` 环境口径新增 `MILVUS_LOCAL_URI` 的坑:配了会让健康检查与部分检索指向 本地 Milvus Lite 文件,出现"健康检查正常、实际查的是另一个库";并注明 `milvus-lite` 属本地开发依赖,应放 `optional-dependencies` 而非主依赖。 ## 顺带更新 AGENTS.md 的过期口径 - 表数 68 → **89**(场内 51 + 场外/推广 17 + 投顾 21),并注明投顾那 21 张的登记文档待补; - 测试基线从"1034 passed / 1 failed"改为实测值:ruff 干净 / mypy 228 文件 0 错 / 1207 passed 0 failed / integration 99 passed,并指向 docs/32; - mypy 那条从"本机 181 错、双方不可比"改成可操作的判据:先对版本,根因是某一侧虚拟环境 没满足 pyproject 的 sqlalchemy>=2.0,<3 / mypy>=1.14,<2;并写明**不要装 sqlalchemy2-stubs** (那是给 1.4 的,2.0 自带 py.typed,装了反而按 1.4 API 报一批新错)。 文档守卫:42 份无编号冲突。 --- AGENTS.md | 25 ++-- docs/08-数据库结构审计基线.md | 14 +++ docs/33-ZSY底座扩展确认-回复.md | 201 ++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 docs/33-ZSY底座扩展确认-回复.md diff --git a/AGENTS.md b/AGENTS.md index 7dd6a40..5f747b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,9 +72,12 @@ - 解释器:本机用 **`.\.venv\Scripts\python.exe`**;架构师环境用 `D:\conda\envs\jr_py313\python.exe`。 两者等价,**各用本机可用的那个**(`.venv` 被 `.gitignore` 忽略、不进仓库,不存在"需要统一"的问题)。 -- 数据库现为 **69 张表**(含 `alembic_version`)= **68 张业务表** = **场内 51 + 场外/推广 17**。 - 后 17 张(`offsite_*` / `promotion_*`)**不进 `docs/00` 基线**(规则 8:场外基金运营流程独立), - 逐表登记见 `docs/28-场外与推广域数据表登记.md`。核验命令:`python tools/audit_schema.py`。 +- 数据库现为 **90 张表**(含 `alembic_version`)= **89 张业务表** = + **场内 51 + 场外/推广 17 + 投顾 21**。 + 后 38 张(`offsite_*` / `promotion_*` / `advisor_*`)**不进 `docs/00` 基线**(规则 8): + 场外/推广那 17 张逐表登记见 `docs/28-场外与推广域数据表登记.md`; + **投顾那 21 张的登记文档待补**(按同样口径另立一份)。 + 核验命令:`python tools/audit_schema.py`(若报 `unexpected` 先分清是"库里多表"还是"迁移没进来")。 - 已注册业务 Agent:`FundQueryDemoAgent`、`CustomerServiceAgent`、`RiskAgent`、`PlatformProbeAgent`(见 `app/service/agent/bootstrap.py`)。 - 已注册公共只读工具:`search_knowledge`(客服知识检索)、`check_suitability`、`query_customer_profile`(画像)、`query_fund_quote`; **工具可用范围 = 代码上限 ∩ 当前 active `config_release` 的发布白名单**,缺发布配置则失败关闭。 @@ -86,7 +89,15 @@ (`app/core/knowledge_schema.py`)——**不要在任何地方硬编码字段名**,那会把另一套环境打挂。 - ⚠️ **Docker Desktop 不会常驻**:它没运行时 Milvus 不可用(`docker` CLI 报连不上守护进程)。 跑真机验证前先确认 Docker Desktop 在运行。 -- 测试基线:`1 failed, 1034 passed, 2 skipped`(2026-09-11 实测);唯一失败是 `tests/unit/repository/test_fund_readonly_contract.py`(**底座既有缺陷,不要修也不要报**)。 -- mypy:本机 `mypy app` 报 181 个错,其中 170 个集中在 `app/model/` 的模型文件(**本机未安装 `sqlalchemy2-stubs`**, - SQLAlchemy 的 `BIGINT`/`DATETIME` 被判成未类型化函数);架构师环境报 0 错。 - **这个数字双方不可比**,不要拿它当结论;只需保证"不比自己改动前更多"。 +- ⚠️ **`MILVUS_LOCAL_URI` 配了就会"看着正常、查的是另一个库"**:一旦在 `.env` 里设置它, + 健康检查与部分检索链路会指向本地 **Milvus Lite 文件**。团队/生产环境请**保持该变量为空**。 + 对应的 `milvus-lite` 属**本地开发依赖**,应放在 `pyproject.toml` 的 + `optional-dependencies`,**不要进主 `dependencies`**。 +- 测试基线(2026-09-11 架构师环境实测):`ruff` 干净 / `mypy app` **228 个文件 0 错** / + `pytest tests/unit tests/contract` → **1207 passed, 2 skipped, 0 failed** / + `pytest tests/integration` → **99 passed**。完整口径与联调清单见 + `docs/32-平台侧交接与联调准备.md`。 +- ⚠️ **mypy 与测试数必须带环境读**:出现"一边上百个错、另一边 0 错"时先对版本,别当代码质量问题。 + 已知根因是某一侧的虚拟环境没满足 `pyproject.toml` 的 `sqlalchemy>=2.0,<3` / `mypy>=1.14,<2`。 + **不要装 `sqlalchemy2-stubs`** —— 那是给 SQLAlchemy 1.4 的,2.0 自带 `py.typed`, + 装了反而按 1.4 的 API 报一批新错(`mapped_column` / `DeclarativeBase` 不存在)。 diff --git a/docs/08-数据库结构审计基线.md b/docs/08-数据库结构审计基线.md index c3f5103..1a170e9 100644 --- a/docs/08-数据库结构审计基线.md +++ b/docs/08-数据库结构审计基线.md @@ -84,3 +84,17 @@ ORM 映射 ↔ 库列名(生成列单独归类为 note)。任一不一致返 数据库约束仍以 `docs/00-新数据库基线设计.md` 为业务权威。指纹与约束审计只证明结构一致, 不替代状态机、并发语义和跨存储投影(Milvus/Neo4j)的业务验收。 + +## 六、已知文档偏差(`docs/00` 与真实 schema 不一致之处) + +`docs/00` 是不可变业务基线(`AGENTS.md` 规则 1),**不因下列偏差修改**;但偏差必须登记, +否则每换一个人接手就会重新踩一次。发现新偏差请追加到本节。 + +| # | 位置 | 文档描述 | 真实 schema | 处置 | +|---|---|---|---|---| +| 1 | `profile_snapshots.current_customer_id` | `docs/00` 第 783 行描述为**「生成列」** | **不是生成列**:`information_schema.COLUMNS` 实测 `EXTRA=''`、`GENERATION_EXPRESSION=''`;`alembic/baseline_generated.sql` 的建表语句无 `GENERATED` 子句;`tools/seed_profile_demo.py` 注释写明「不是生成列,必须显式写入」;`profile_repository.py` 用原生 SQL 显式写入 | 按真实 schema 映射成**普通可空列**;**不改 `docs/00`**;**不补迁移**(DDL 本身正确,错的只是文档描述;为"让文档成真"而加生成列会改变既有列语义,违反规则 4)。四处证据一致,2026-09-11 复核 | + +> 发现方式:ZSY 那条线在合并前先按 `AGENTS.md` 与 `docs/00/01/05/09/14/20` 逐条过规则, +> 撞到"文档说生成列、代码却显式写入"这处矛盾后做了实测。这个自查习惯值得保持—— +> 本次三家里,只有这条线在合之前把"撞到底座规则"的地方单独清理成了一个提交。 + diff --git a/docs/33-ZSY底座扩展确认-回复.md b/docs/33-ZSY底座扩展确认-回复.md new file mode 100644 index 0000000..5f53ebc --- /dev/null +++ b/docs/33-ZSY底座扩展确认-回复.md @@ -0,0 +1,201 @@ +# 回复 ZSY:底座扩展确认(v1) + +> **致**:ZSY +> **被回复**:`致qyqy_底座扩展确认沟通文案_v1.md` +> **我方基线**:`qyqy_develop`(含你合并的 `c4a73b7` 之后的平台侧改动) +> **结论**:**三件事方向都可以接受,但第 1 件有一条硬约束必须先确认**(见 §1.2); +> 第 3 件的判断你是对的,`docs/00` 那处描述确实错了,处置方式见 §3。 + +--- + +## 1. 访客身份(`security.py` + `auth.py`) + +### 1.1 `data_scope="public"` 这个新值,我替你核过了:安全 + +我扫了全平台 `data_scope` 的**全部 26 个消费点**,对未知值的行为是**一致 fail closed** 的: + +| 写法 | 处数 | `"public"` 的结果 | +|---|---|---| +| `== "all"` | 6 | 判假 ⇒ 走保守分支 | +| `!= "all"` | 1(`risk_analysis_service.py:126`) | 进入分支 ⇒ **要求 `customer_ids` 非空,否则 `ForbiddenAgentError`** | +| 显式白名单 `{"self","own_customers","all"}` | 1(`financial_nl2sql_service.py:286`) | 不在集合内 ⇒ 直接拒绝 | + +访客的 `customer_ids` 本来就是空的,所以 `!= "all"` 那一支会**先拒后查**。 + +**结论**:新增 `public` 不会意外放行任何数据,**不用改这 26 处**。 +顺带说明它的实际作用:访客要的是"查公开知识",那条路径走 `knowledge:query`**权限**、 +不读 `data_scope`,所以 `public` 目前主要是**语义标注**——这没问题,标注清楚比复用 `self` 更好。 + +### 1.2 ⚠️ 必须先确认:访客令牌的 `sub` **必须是纯数字** + +这是全平台最硬的一条约束,请务必确认你的实现满足: + +```python +# app/core/security.py:57-61 —— 现有校验 +subject = claims["sub"] +if (not isinstance(subject, str) or not subject.isascii() + or not subject.isdecimal() or len(subject) > 20 + or not 0 < int(subject) <= 18446744073709551615): + raise UnauthorizedAgentError("invalid subject") +``` + +即使这个校验通过了,平台里仍有 **72 处 `int(context.user_id)`**,而**只有 3 处做了防御**: + +``` +有防御:offsite_fund_service.py:2458 / offsite_nl2sql_adapter.py:68 / suitability_service.py:250 + (写法都是 int(...) if context.user_id.isdigit()/isdecimal() else None) + +没有防御:其余 69 处,例如 + authorization_service.py:21 actor_id=int(context.user_id) ← 一旦发生权限拒绝就要写这条审计 + tool_executor.py:138 actor_id=int(context.user_id) + identity_repository.py:18 int(identity.user_id) + conversation_service.py:36/60/63/68/87 + public_platform_service.py:36/44/77 + …(共 69 处) +``` + +**所以如果访客的 `sub` 是 `visitor-` 这类非数字,任何一次命中就会抛 `ValueError`**—— +而且其中好几处是"权限被拒时要写审计"的路径,表现为**本该 403 的地方变成 500**。 + +**请你确认两点**: + +1. 访客令牌的 `sub` 是**纯十进制数字**(例如保留 id `0`,或某个不与 `sys_user` 冲突的号段); +2. `visitor` 分支**复用了同一个 `jwt.decode`**(含 `algorithms` / `iss` / `aud` / `exp` / `nbf` / `jti` + 以及上面那段 `sub` 校验),只是**跳过 `IdentityService.resolve()`**——而不是自己另写一套。 + 另写一套等于开了第二个鉴权入口,这与"单点鉴权"的约定冲突。 + +> 如果 `sub` 只能是字符串,那正确做法**不是**改成访客专用校验,而是把那 69 处 +> `int(context.user_id)` 一并收敛成 `app/core/` 里的一个转换函数(非数字返回 `None`)。 +> 那是一次跨模块改动,**建议单开一轮做**,不要混在访客功能里。 + +### 1.3 两条小约束 + +- **权限集保持最小**:`("agent:run", "knowledge:query")` 看起来正好,请**不要**顺手加 + `knowledge:reference:read`(那是读"知识引用令牌"用的,访客不需要)。 +- **访客不产生客户侧副作用**:`recalls_customer_memory=False` 走对了方向; + 另外确认访客**不写** `interaction_audit.target_customer_id`(留 `None`)。 + +--- + +## 2. 客服 Agent 的角色与行为 + +### 2.1 `allowed_roles` 扩为 `("visitor","customer")` —— 可以 + +访客要用客服 Agent,这是必需的。`AgentDefinition.allowed_roles` 本来就是声明式的, +扩集合不改语义。 + +### 2.2 `recalls_customer_memory=False` —— 可以,但**默认值必须是 `True`** + +这是**新增声明字段**,现有代码里没有它(我 grep 过 `app/service/agent/`,只有 `recall_memory` +这个 base 方法)。请务必保证: + +``` +默认值 = True(即"照常召回"),只有显式声明 False 的 Agent 才跳过 +``` + +否则一旦默认成 `False`,**所有既有 Agent(客服、风控、投顾、demo)的记忆召回会被静默关掉**—— +而它在界面上看不出来,只会表现为"回答变差"。 + +### 2.3 `handle()` 里插入确定性安全路由 —— 可以,但请补测试 + +"安全/合规/账户/人工优先于检索"这个顺序是对的(客服那边原本就有类似的 +`is_profile_question()` 确定性前置)。请为每条分支补一个用例,锁住"优先级顺序"本身—— +这类路由最容易在后续合并里被悄悄挪位置。 + +### 2.4 品牌名 —— **这条我不能定,需要项目方拍板** + +`COMPANY` 现在确实是 `"南方科技"`(`customer_service.py:153`)。但你改成的 +**「奶龙基金责任有限公司」是风控那条线的品牌**(`docs/风控业务演示文档/09-奶龙风控智能助手说明.md`)。 + +所以这不是"改个常量",而是**两个业务线是否共用一个品牌主体**的问题。我倾向统一(同一家公司 +的客服和风控不该是两个主体),但这属于业务口径,**我已把它单独列给项目方定**, +定了我立刻同步。 + +--- + +## 3. `current_customer_id` —— **你的判断是对的,`docs/00` 那处描述错了** + +我独立核了一遍,你的证据全部成立,而且我这里也有了直接证据: + +``` +information_schema.COLUMNS(本机实测): + COLUMN_NAME = current_customer_id + COLUMN_TYPE = bigint unsigned + IS_NULLABLE = YES + EXTRA = "" ← 不是生成列、也不是自增 + GENERATION_EXPRESSION = "" ← 没有生成表达式 +``` + +与 `alembic/baseline_generated.sql` 没有 `GENERATED` 子句、`tools/seed_profile_demo.py` +的注释、`profile_repository.py` 用原生 SQL 显式写入——**四处一致**。 + +**处置(我的裁定)**: + +1. **按真实 schema 映射成普通可空列** —— 你已经这么做了,**对**; +2. **`docs/00` 不改** —— 规则 1 明确它是**不可变**业务基线,哪怕某处描述有误也不由我们改; +3. **但要把这处偏差登记下来**,否则第 5 个人还会再踩一次。我会在 + `docs/08-数据库结构审计基线.md`(审计口径文档)里记一条"已知文档偏差"; +4. **不建议补迁移**:DDL 本来就是对的(普通可空列),错的只是文档描述。为"让文档变成真的" + 去加一条生成列迁移,会**改变既有列语义**(规则 4)。 +5. 同一张表**两个声明类**(`risk_questionnaire.py` 内联 + `profile.py`)会让 SQLAlchemy + 直接拒绝导入模型——这个必须去重,你做的方向对;请统一从 `profile.py` 导出。 + +--- + +## 4. 顺带三件小事 + +| 项 | 我的意见 | +|---|---| +| `query_knowledge` 与 `search_knowledge` 并存 | **可以保留**(都指向同一 handler、实际范围由 `config_release` 白名单收口),但请在 `docs/05` §8.4 或你的接入说明里**明确写成"别名"**,否则下一个人会以为平台有两个检索工具 | +| `.gitignore` 加 `.worktrees/` 和 `data/milvus/` | **可以**。顺带提醒:`.workdir/` 也已在里面(NL 那边的 conftest 用它落测试临时目录) | +| 依赖新增 `milvus-lite` | **请放进 `pyproject.toml` 的 `optional-dependencies`**,**不要进主 `dependencies`**。它只是本地开发用,进主依赖会让生产环境多背一个包,而且 `MILVUS_LOCAL_URI` 那条坑正是它引入的 | + +**你提的 `MILVUS_LOCAL_URI` 那个坑很有价值**:一旦在 `.env` 里配了,健康检查会指向本地 +Milvus Lite 文件,出现"健康检查正常、实际查的是另一个库"。建议把它写进 `AGENTS.md` 的 +环境口径一节(和 `config_release`、Milvus schema 那几条并列)——我来加,你确认措辞即可。 + +--- + +## 5. 关于你删掉的两个文件 + +`app/service/knowledge_tool_service.py` 与 `app/infrastructure/milvus_knowledge_adapter.py`, +你判断是生产死代码、且后者硬编码了 Milvus 字段名(违反 `AGENTS.md` §E)——**方向我认同**, +硬编码字段名那条尤其该删(另一套环境会被打挂)。 + +但**我这边看不到你的分支**(`ZSY_develop` 未推送),所以无法独立核实"全仓唯一引用是它自己的单测"。 +请在 PR 描述里附上两条 grep 的实际输出,例如: + +``` +git grep -n "knowledge_tool_service" -- app/ tests/ tools/ +git grep -n "milvus_knowledge_adapter" -- app/ tests/ tools/ +``` + +只要输出里只剩它们自身(和各自的单测),我就照批。 + +--- + +## 6. 你处理得好的两处(照做) + +1. **`docs/05` 的 `§8.5`**:没有把既有的 `§8.2`/`§8.3` 往后挤,而是新增到末尾, + `AGENTS.md`/`docs/09`/`docs/14` 里对 §8.3、§8.4 的引用继续成立 —— 这是对的, + 编号被复用比编号不够更麻烦(我们这边刚因两家同时占用 `docs/21`、`docs/22` 让过两次号); +2. **合并前先按底座规则自查、把撞规则的地方清理成独立提交**(`9aaacc2`, + 9 文件 +189/−483)—— 这个习惯请保持,它让评审能按提交看,而不是在一大坨 diff 里找。 + +--- + +## 7. 结论与下一步 + +**可以推进**:§1.3、§2.1、§2.3、§3 的处置、§4、§5(附 grep 证据后)。 + +**需你先补一句确认**:§1.2 的 `sub` 是否为纯数字 + 是否复用同一套 `decode` 校验; +§2.2 的默认值是否为 `True`。 + +**需项目方拍板**(我已上报,不占你时间):§2.4 的品牌名。 + +这三条确认后,你推 `ZSY_develop` 并开 PR,我这边按 `docs/32-平台侧交接与联调准备.md` §5 +的清单合:`alembic upgrade head` → 表审计 → 文档守卫 → 四项门禁 → 真机联调。 + +> 附:我方最近新增的与你这条线相邻的东西,联调时可能用得上 —— +> `POST /api/v1/auth/tokens`(账号密码登录)、`GET /api/v1/admin/roles` 等四个 RBAC 只读接口、 +> `tools/login_console.py`(浏览器登录测试台)、`tools/create_test_user.py`(建测试账号)。