Files
group_fqcd_jr/docs/33-ZSY底座扩展确认-回复.md
lzf_0626 244f03917b 回复 ZSY 的底座扩展确认;登记 docs/00 的一处已知偏差;更新 AGENTS.md 过期口径
## 对 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 份无编号冲突。
2026-09-12 11:27:28 +08:00

202 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 回复 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-<uuid>` 这类非数字,任何一次命中就会抛 `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`(建测试账号)。