Files
group_fqcd_jr/docs/33-ZSY底座扩展确认-回复.md
T
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

10 KiB
Raw Blame History

回复 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 必须是纯数字

这是全平台最硬的一条约束,请务必确认你的实现满足:

# 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(建测试账号)。