Files
group_fqcd_jr/docs/36-PR7合并记录与权限号段修正.md
T
lzf_0626 36c7a9d8d2 文档:审查报告入库 + 全量校对补注
## 新入库(`docs/演示用/`)

- `代码库全面审查报告-2026-09-14.md`
- `代码修改方案-2026-09-14.md`
- `记忆系统排查报告-2026-09-14.md`
- `记忆系统修复文档-2026-09-14.md`
- `文档一致性审计报告-2026-09-14.md`
- `多Worker接入方案-2026-09-14.md`

## 全量校对(32 个既有文档 + `AGENTS.md`)

跨 39 个文件、**1125 insertions / 148 deletions**。

⚠️ **这批改动同样不是本次会话写的**。我抽样核对过性质:是**实质内容补充**而不是
格式/换行转换。例如 `docs/44-演示流程.md` 新增两条"2026-09-14 补注":

- `启动金融Agent平台.bat` 只在**桌面**上,仓库里只有 `启动平台.bat` 这一份
  (两份由同一个 `tools/make_launcher_bat.py` 产出,改完 `start.ps1` 重跑它一起更新);
- `advisor_t`(9020) 与 `offsite_t`(9006) **不在 `tools/seed_test_rbac.py` 的演示用户里**
  (那里只有 `cust_t`/`risk_t`/`admin_t`/`review_t` 四个),由 `grant_*.py` 系列创建,
  **重跑种子不会重建它们** —— 换机器时这两个账号登录失败,要先查 `sys_user` 有没有这两行,
  而不是查密码。

这两条都是对的地方,与我这一路踩到的现象一致(我确实用到了 `advisor_t`/`offsite_t`)。

**我没有逐字审阅全部 39 个文件**,只抽样确认了改动性质与规模。若其中有需要复核的段落,
请指明文件,我逐处核对。
2026-09-14 20:36:00 +08:00

193 lines
11 KiB
Markdown
Raw 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.
# PR #7 合并记录与权限号段修正
> **合并时间**:2026-09-12
> **合并提交**:`4413644`(`merge: 合并 ZSY 的客服 Agent 接入(访客身份、画像候选、转人工工单)—— PR #7`)
> **来源**:`origin/ZSY_develop` = `f68b052` → `qyqy_develop`
> **规模**:90 文件、+7897 / −73
> **门禁**:全部通过(见 §2),数据库侧无迁移、表结构未变
>
> ---
> ## ⚠️ 口径修正(2026-09-14):本文的"权限条数"是**当时快照**,现已增长
>
> 本文正文出现的 **40 条 / 45 条 / 38 条**都是 **2026-09-12 当天**的实测值,不是当前值。
> 现状(直接查 `tools/seed_test_rbac.py` 的 `PERMISSIONS`):
>
> | 口径 | 值 |
> |---|---|
> | **权限号段** | **`9001`–`9065`(共 65 条)** |
> | 区间归属 | `9001-9017` 一期公共;`9018-9034` 客服二期/投顾;`9041-9046` 产品治理与候选审核;`9047-9050` 风控告警;`9051-9056` 推广/NL2SQL/探针;`9057-9059` 投顾客户范围;`9060-9065` 账户与交易看板 |
> | **从未存在** | `9035-9040`、`4041-4046`(后者是 `9041-9046` 的笔误) |
>
> ⚠️ 本文 §4 的号段表到 `9046` 结束是正确的(那正是当时的上界),**但不要据此认为 9046 是终点**。
> 权威出处是 `tools/seed_test_rbac.py` 的 `PERMISSIONS`(该脚本是
> `DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099` 的**重建**语义:
> 没并进种子的权限码,重建一次就没了,表现是"接口突然 403"而无报错线索)。
> 一致性由 `python tools/check_rbac_seed_consistency.py` 及其单测守着。
---
## 1. 合并内容
ZSY 的客服 Agent 接入线,含三块新能力与一次合规清理:
| 块 | 内容 |
|---|---|
| 访客身份 | `VisitorTokenIssuer`(`/api/v1/auth/visitor-tokens`)、`security.py` 的 `visitor:true` 分支、`auth.py` 跳过 `IdentityService.resolve()` |
| 画像候选(二期) | 候选生成 Worker、用户确认 / 管理员审核四个端点、Milvus + Neo4j 投影适配器、`memory_sync_outbox` 消费者 |
| 转人工工单 | 工单只读管理端点、转接上下文构造、会话凭据最小化(`conversation_privacy.py`) |
| 合规清理(`9aaacc2`) | 删除生产死代码 `knowledge_tool_service.py` 与硬编码 Milvus 字段名的 `milvus_knowledge_adapter.py`(9 文件 +189/−483) |
评审往返见 `docs/33`(v1 回复)、`docs/34`(v2 回复)、`docs/35`(v3 回执核实)。
---
## 2. 合并后门禁实测
| 项 | 结果 |
|---|---|
| `git merge f68b052 --no-ff` | ✅ 无冲突 |
| `ruff check app tests tools` | ✅ 干净 |
| `mypy app` | ✅ **244 文件 0 错**(基线 228,+16 为新模块) |
| `python tools/check_authoritative_docs.py` | ✅ **49 份文档无撞号** |
| `pytest tests/unit tests/contract` | ✅ **1275 passed, 2 skipped, 0 failed**(基线 1207,+68 为新用例) |
| `alembic current` | ✅ 已在 `20260911_merge_adv_risk_heads`(head),`upgrade head` 无操作 → **无新迁移** |
| `python tools/audit_schema.py` | ✅ **89 business tables, no missing or unexpected tables** |
> 说明:`f68b052` **单独一棵树**跑文档守卫会报 `21`/`22` 撞车 —— 那是**我方基线**的问题
> (投顾那两份的改名在我方 `5018f11`,不在 ZSY 的基线上)。合并后消失:`21`/`22` 归
> 风控与四大 Agent,投顾挪到 `30`/`31`。**不是 ZSY 引入的。**
---
## 3. 合并时顺手修掉的两处
### 3.1 `tests/unit/core/test_security.py` 的密钥路径漂移(2 行)
第 48、108 行原本硬编码 `Path("config/jwt/jwt-private.pem")`,**漏了 `dev/`**:
- `tools/generate_jwt_keys.py:107` 的默认 `--out-dir` 是 `config/jwt/dev`;
- `app/core/config.py:26-27` 的默认值也是 `config/jwt/dev/...`;
- 同一个文件第 13 行已定义 `DEV_KEY_DIR = Path("config/jwt/dev")`,注释写着
「路径只在这里定义一次……避免多处硬编码各自漂移」。
后果:在**没有** `config/jwt/jwt-private.pem` 的环境里必红 2 个用例(本机即如此)。
已改为 `(DEV_KEY_DIR / "jwt-private.pem")`,与第 132 行写法一致。全仓这种写法只此 2 处,
其余 12 处引用都是 `config/jwt/dev/`。
### 3.2 权限号段冲突(**我方环境的历史遗留,ZSY 不受影响**)
**问题**:库里有一批 `9020-9035`,是本方 `tools/grant_advisor_role.py` 用**旧号段**建的;
而投顾线(`bbf623a`)后来把 `9020-9034`(15 个)写进了公共种子 `seed_test_rbac.py`。
两套 **id→code 映射不同**:
| id | 种子(投顾线) | 库里(旧 `grant_advisor_role.py`) |
|---|---|---|
| 9020 | `investment-goal:write:self` | `asset-allocation:generate:self` |
| 9023 | `product-recommendation:generate:self` | `investment-goal:review` |
| 9031 | `product-recommendation:review` | `product-governance:read` |
**后果**:种子的清理是 `DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099`,
但角色绑定只清 `role_id IN (9001,9002,9003)` —— **`advisor`(9004) 的绑定不在清理范围内**。
所以跑一次种子会把 `9020-9034` 换成种子的语义,而 advisor 的绑定仍指向这些 id ⇒
**advisor 会拿到错误的权限组合**,且不会报错。
**处置**(已改代码,见下):权限号段整体上移到 **9041-9046**,与种子的 `9001-9034` 和
库里的旧数据都不冲突;并把"权限码的定义以种子为准"写进了三个文件的文档字符串。
> ZSY 侧不受影响:他的库只有 `9001-9018`,没有这批旧数据。
---
## 4. 代码改动清单
| 文件 | 改动 |
|---|---|
| `tests/unit/core/test_security.py` | 2 行改用 `DEV_KEY_DIR`(§3.1) |
| `tools/seed_test_rbac.py` | `PERMISSIONS` 增 `9041-9046`(投顾治理类 3 个 + 客服二期 3 个);`CUSTOMER_PERMISSIONS` 增 `9044`;文档字符串写清「DELETE 重建」的两个后果 |
| `tools/grant_advisor_role.py` | `ADVISOR_PERMISSIONS` 由 16 个收敛为**种子里缺的 3 个**(`9041-9043`);文档字符串记录号段冲突与处置顺序 |
| `tools/grant_customer_service_phase2_permissions.py` | 权限 id `9036-9038` → `9044-9046`;文档字符串改为「已并入种子,本脚本仅作幂等补齐」 |
新增的 6 个权限码:
| id | 权限码 | 授给 |
|---|---|---|
| 9041 | `product-governance:read` | admin(治理类) |
| 9042 | `product-governance:review` | admin |
| 9043 | `product-governance:sync` | admin |
| 9044 | `memory:candidate:confirm` | **customer**(确认自己的候选)+ admin |
| 9045 | `memory:candidate:review` | admin |
| 9046 | `handover:read` | admin |
一致性自检(只读):**当时**种子 40 条权限、id 唯一、无重复;两个 `grant_*.py` 的每一条
`(id, code)` 都与种子逐字一致;`CUSTOMER_PERMISSIONS` 引用的 id 全部存在。
(⚠️ 2026-09-14:现为 **65 条**,号段 `9001-9065`。)
---
## 5. 本机 RBAC 对齐:最终执行记录
分两轮完成。**第一轮原计划是"种子 + 三个 grant 脚本",但种子当场跑不通**(见 §5.1),
于是先改用不依赖种子的方案把功能补齐;随后按项目方决定修好种子,**第二轮完整跑通**。
### 5.1 第一轮撞到的破坏:`seed_test_rbac.py` 在这台环境上跑不通(已修)
```
sqlalchemy.exc.IntegrityError: (1451, 'Cannot delete or update a parent row: a foreign key
constraint fails (`jr`.`advisor_profile_tag`, CONSTRAINT `fk_advisor_profile_tag_customer`
FOREIGN KEY (`customer_id`) REFERENCES `sys_user` (`id`))')
[SQL: DELETE FROM sys_user WHERE id IN (9001,9002,9003)]
```
投顾线引入的 `advisor_profile_tag` 有 FK 指向 `sys_user`,而库里有数据引用 `9001-9003`,
于是种子最后那步 `DELETE FROM sys_user` 被外键拒绝。**因为 `commit()` 在最后,种子是原子的**
(失败即完整回滚,**当时**实测:权限仍 38 条、三个用户密码完好),但任何人在这台环境、
或任何有投顾数据的环境上跑种子都会失败 —— 而且外部表现只是"什么都没发生"。
**修法(已落地)**:把 `DELETE FROM sys_user` 换成「存在则 UPDATE 非密码字段、不存在才 INSERT」,
且**不覆盖 `password_hash`**。附带好处:重跑种子不再弄丢演示密码,也不需要事后补
`set_user_password.py` —— 实测两轮之后 `9001`/`9002`/`9003`/`9020` 的 bcrypt 密码都还在。
考虑过但**未采用**:① 种子完全不碰 `sys_user`(把用户创建交给 `create_test_user.py`,代价是
"新建环境多一步");② 给该 FK 加 `ON DELETE CASCADE`(改已有约束,牵涉规则 4,不做)。
### 5.2 对齐后的最终实测状态
> ⚠️ **下表是 2026-09-12 当天的实测快照**。权限总数与绑定数此后已随新增权限码变动
> (现共 65 条,见文件头 ⚠️ 块);测试用例数也已增长(`tests/unit tests/contract` 不再只有 1275 项)。
| 项 | 值(2026-09-12) |
|---|---|
| 权限总数 | ~~**45 条**~~ → 现 **65 条** |
| 绑定数 | `admin 44 / customer 20 / risk_operator 10 / advisor 10 / operator 1` |
| 原先缺的 `9018 knowledge:query` | ✅ 已补(种子重建带入) |
| 原先缺的 `9019 knowledge:manage` | ✅ 已补 |
| `9041-9043` 投顾治理类 | ✅ 就位 |
| `9044-9046` 客服二期 | ✅ 就位;实测 `memory:candidate:confirm` → `customer` + `admin`,另两个 → `admin` |
| 演示密码 | ✅ 四个账号 bcrypt 密码完好,**未重设** |
| `pytest tests/integration/test_auth_login_mysql.py tests/integration/test_rbac_read_mysql.py` | ✅ **19 passed** |
| `pytest tests/unit tests/contract` | ✅ **1275 passed, 2 skipped** |
执行顺序(有依赖,不能颠倒):
```
清 advisor(9004) 旧绑定 → seed_test_rbac.py → grant_advisor_role.py
→ grant_customer_service_phase2_permissions.py → grant_risk_permissions.py
```
第一步必须做,原因见 §3.2;后两个 `grant_*.py` 里风控那 4 个权限是**必要的幂等补齐** ——
种子不重建 `1986...` 号段的风控权限,却会清掉 `risk_operator`/`admin` 对它们的绑定(本次实测补了 8 条)。
---
## 6. 遗留事项
| 项 | 归属 | 状态 |
|---|---|---|
| 品牌名(`COMPANY` 南方科技 → 奶龙基金责任有限公司) | ZSY 上报项目方 | 走 (b):保持现状,定后单行 revert,须与 `customer_service_rules.py:116/129` 一起处理 |
| 候选流程缺走 RBAC 的集成用例(服务层测试测不出"权限码不存在") | ZSY | 他认账,承诺补 |
| 只读真机冒烟(用真实令牌打 §19 新增端点,403 当失败) | 待 ZSY 回话 | 已提议 |
| 环境数据不可跨环境 | 全员 | 第 4 次踩坑,已写进种子文档字符串 |
| `docs/05` §19 端点编号 | 已解决 | `A039`/`A040`,全表 62 个编号唯一 |
| `seed_test_rbac.py` 被投顾 FK 挡住 | ✅ 已修 | 改 `sys_user` 为 UPSERT 且不覆盖密码,见 §5.1;重跑种子不再弄丢演示密码 |
| 本机缺 `9018 knowledge:query` / `9019 knowledge:manage` | ✅ 已补 | 见 §5.2,客服检索工具的必需权限已就位 |