Files
group_fqcd_jr/AGENTS.md
T
lzf_0626 076d786bc6 补齐客服转人工工单流:从"只能看"到"能推进"(基线状态机,不自行发明)
## 问题
`svc_handover_ticket` 的 DDL 与状态机在 `docs/02` §7.2 早就定好了
(pending → assigned → processing → resolved → closed,未解决可 cancelled),
但平台**只有 handover:read(只读队列)**:没有任何入口能改状态、assigned_to /
accepted_at / resolved_at / closed_at / resolution 五列**全库 0 非空**,
于是 40 张工单永远停在 pending —— 用户看到的就是"工单全都长一样"。

## 改了什么
后端:
- 新增 `app/service/customer_service_handover_action_service.py`:五个动作
  (分配/接单/解决/关闭/取消),`SELECT ... FOR UPDATE` 锁单后判状态;
  接单允许从 pending 自助接管(同时记受理人);取消不写 closed_at(该列属 closed 状态);
  每次流转写一条 interaction_audit(handover.assigned/accepted/resolved/closed/cancelled);
  非法流转 409、坐席不存在 422、工单不存在 404;回包不含 customer_id/session_id。
- 只读服务保持只读(读侧与写侧是两条边界,单测守着"读侧不许长出写方法"),
  但列表支持 `?status=` 六态筛选、详情补上受理人与流转时间(坐席侧路由信息,非客户数据)。
- `app/api/controllers/admin.py`:五个 action 端点 A049–A053
  (assignments / acceptances / resolutions / closures / cancellations),
  走 `ApiTransactionService.execute_in` —— 幂等记录与业务写入同事务、重复键回放。
- 权限:新增 `handover:write`(9069,只授 admin),已并进种子
  `tools/seed_test_rbac.py`;配套幂等脚本 `tools/grant_handover_write_permission.py`。

前端(管理员工作台 · 转人工工单页):
- 按状态给按钮(待处理→分配/直接接单、已分配→接单、处理中→解决、已解决→关闭、
  未解决都可取消),加了状态筛选与"刷新";摘要弹窗补上受理人与四个时间点、处置结论。
- api-client 注册五个端点;workspace.js 的 api-client 引用与页面自身的 ?v= 一并升版,
  避免浏览器拿旧缓存(旧缓存里没有这些端点)。

冒烟与测试:
- `tools/e2e_smoke_test.py`:B 段建的测试工单由 F 段走完 分配→接单→解决→关闭 收尾
  —— 既不再把测试件堆在 pending 队列里(此前每次冒烟攒一张),又让每次冒烟都覆盖一遍状态机。
  总数 40 → 44 项,实测 44/44 全绿。
- 新增单测 24 条(状态机合法/非法路径、越权、坐席不存在、审计、视图不泄漏客户标识)
  与一条真机集成用例(HTTP 十步 + 数据库侧审计证据 + 自动清理)。
- 读侧那条"详情不得返回 assigned_to"的旧断言按新口径更新,并写清为什么。

## 验证
- `pytest tests/unit tests/contract` → 1489 passed, 2 skipped, 0 failed
- 新增集成用例通过;`tests/integration` 全量跑时
  `test_memory_extraction` / `test_run_cancellation_mysql` 两条偶发红 —— 单独跑都通过,
  是 AGENTS.md 已登记的"常驻 Worker 抢队列"(跑验收前须先停 Worker)
- `tools/portal_api_check.py` → 41 项通过 39、失败 0
- `tools/e2e_smoke_test.py` → 44/44 全通过
- `python tools/check_rbac_seed_consistency.py` → 通过(种子 63 条权限)
- 真机 HTTP 实测:分配→接单→解决→关闭四步 200 且时间戳齐全;取消路径 200 且 closed_at 为空;
  同键重发回放不二次推进;对已关闭工单再分配 409;风控账号处置 403

## 文档
`docs/44-演示流程.md`(场景 4/8 + 命令 + 44 项)、`docs/演示用/后端接口文档`(新增 §11.4b 与
A049–A053)、`docs/演示用/全功能流程-大白话版.md`(工单页签改"读写"+ 已知偏差)、
`AGENTS.md`(9066-9069 号段演进 + 冒烟 44 项)
2026-09-15 00:41:59 +08:00

272 lines
24 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.
# 项目级开发约束
以下规则对人工开发者和编码 Agent 均为强制约束:
1. 数据库以 `docs/00-新数据库基线设计.md` 为不可变业务基线。
2. 允许创建新表,允许在已有表中增加新字段。
3. 禁止重命名或删除已有表。
4. 禁止重命名、删除、复用已有字段,禁止改变已有字段的类型、可空性和既有业务含义。
5. 历史结构无法满足新需求时,使用新增字段、新表、兼容视图或应用双读解决。
6. 架构固定使用 MVC+S;Agent 属于 Service 层。
7. 业务 Agent 必须继承公共 `BaseAgent` 并由 `AgentFactory` 创建,不得绕过公共鉴权、记忆、模型路由、工具、合规、审计和事件流程。
8. 当前系统业务功能只针对场内基金模拟交易;场外基金运营流程独立,不得写入场内交易表。
修改数据库文档或迁移前,必须对比基线并证明没有改变任何已有表名和已有字段定义。
---
## 📖 接手先读(按此顺序,只读这些就够)
> **⭐ 第 0 步先读这个**:`docs/superpowers/handoff/2026-09-11-交接文档-客服Agent与RAG收尾.md`
> —— 客服 Agent + RAG 这条线的交接文档(**含合并完成后的第二次更新**):环境口径、交付内容与
> **可复现验证证据**、合并后修掉的 3 个真机故障、**已知问题清单(逐条标注当前状态)**、Git/PR 状态。
> **主集成分支是 `qyqy_develop`**(ZSY 的客服接入线已由 PR #7 合入,见 `docs/36`);
> 客服/RAG 那条线的个人分支是 **`NL_develop`**(个人分支 → PR 合回 `qyqy_develop`),**不要再用 `6516ccb`**。
> 它是对"当前状态"最准确的一份,读完它再读下面这些。
>
> **🖥️ 前端(2026-09-14 修订)**:**正式前端在 `app/static/portal/`**,由 `app/main.py` 挂载,
> 通过 **`/portal/`** 访问(`/` 与 `/portal/` 都 307 跳到 `/portal/guest/home/`)。
> **实际是 6 个角色目录**(原写"四套页面"已过期,漏了投顾与运营两个):
>
> | 目录 | 角色 | 子页面 |
> |---|---|---|
> | `guest/` | 访客(免登录) | `home/`、`products/`、`product-detail/` |
> | `customer/` | 客户 | `login/`、`dashboard/`、`orders/`、`transactions/`、`holdings/`、`cash-ledger/`、`profit-loss/`、`risk-questionnaire/` |
> | `employee-console/` | 平台管理员 | `login/`、`workspace/` |
> | `employee-risk/` | 风控专员 | `dashboard/` |
> | `employee-advisor/` | 投顾 | `dashboard/` |
> | `employee-operations/` | 运营(场外/推广/NL2SQL) | `dashboard/`、`nl2sql/`、`offsite/`、`promotion/` |
>
> ⚠️ `employee-advisor/` 与 `employee-console/` **是两个不同角色,不是改名关系**,别合并。
> 访客公开产品页已由 `app/api/controllers/public_platform.py` 的 `/products` 等端点供数,
> **不再是** `common/mock-data.js`。**端点表集中在 `common/api-client.js`**,改接口调用请改那一份。
> 启动:`python -m uvicorn app.main:app --port 8000`(注意模块级变量是 **`app`**,不是 `application`)。
> `tools/portal.py`(8101)**只是跨角色联调工具,不是产品前端**,不要再往它加功能。
>
> **🚀 一键启动与演示(2026-09-13 起)**:**不想敲命令就直接双击桌面的
> `启动金融Agent平台.bat`**(仓库里也有一份 `启动平台.bat`)。它按序做六件事:
> 找解释器 → 检查 MySQL/Redis/Milvus → **刷新行情** → 起 **API 与 Agent Worker 两个窗口**
> → **等 API 真正应答** → 自动开浏览器。**重复双击是安全的**(API 按端口、Worker 按进程判断,
> 不会起第二份);参数可透传,如 `启动平台.bat -Port 8100`、`-NoBrowser`、`-SkipPriceSync`。
> bat 由 **`python tools/make_launcher_bat.py`** 生成(改完 `start.ps1` 或想换路径就重跑它,
> 桌面与仓库两份一起更新)—— 不要手写那个 bat,它必须同时满足 **GBK 编码 + CRLF 换行 + 无 BOM**,
> 缺任何一条 cmd 都会解析错乱(LF 换行会把 `echo` 的说明文字当命令执行,
> 实测报 `AT 命令已弃用`、`']' 不是内部或外部命令`)。
> 也可以直接用脚本:`powershell -ExecutionPolicy Bypass -File start.ps1`。
> 演示数据一键准备:`python tools/seed_demo_data.py`(10 步,顺序有依赖,见脚本内表格);
> **演示流程(8 个场景照读版 + 排障表 + 账号速查)见 `docs/44-演示流程.md`**;
> 交付自检(**两条线互补,都跑一遍**):
> `python tools/e2e_smoke_test.py`(**业务链路**冒烟:登录→下单→成交、风控扫描→处置闭环、客服问答与**转人工工单处置闭环**,6 条线 44 项,`--read-only` 不动数据);
> `python tools/portal_api_check.py`(**接口契约**体检:按前端的方式调每个端点,核对状态码、信封形状与字段是否与前端期望一致,41 项;`--write` 加测写操作、`--dangerous` 再加测会改生效配置的操作)。
> ⚠️ **`start.ps1` 必须保存为 UTF-8 with BOM**:Windows PowerShell 5.1 在缺 BOM 时按系统
> ANSI(简中为 GBK)解析,中文注释直接抛 `Unexpected token '[璀﹀憡]'` 这类语法错误。
> 用 `edit`/`write` 类工具改完**务必补回 BOM**(只加字节、别重写换行:
> `d=open(p,'rb').read(); open(p,'wb').write(b'\xef\xbb\xbf'+d)`)。
> ⚠️ **解释器探测必须实测「能 import 依赖」,不能只看 `--version` 成功**:
> 曾经因此选中一个 Python 3.10 环境(本项目用 `datetime.UTC`,3.11+ 才有,且依赖 `asyncmy`),
> 报错却发生在**行情刷新**那一步,看起来像"行情源坏了"。现在的门槛是
> **版本 ≥ 3.11 + `import fastapi, sqlalchemy, asyncmy, pydantic` 通过**。
> 另注:`... 2>&1 | Select-Object -First 1` 会掐断上游 native 进程、把 `$LASTEXITCODE` 弄脏,
> 在探测循环里用它会**把每个候选都判成"无法执行"** —— 先接住输出再取行。
> ⚠️ **行情有效期只有 15 分钟**(`app/service/trade_service.py` 的 `MAX_QUOTE_AGE`),
> 超时后**所有委托一律 503「行情已过期」**且无自动刷新 —— 这是演示最容易翻的一环。
> 补刷用 `python tools/sync_market_prices.py`,**立即生效、无需重启服务**。
>
> **⚠️ 文档现状(2026-09-11 第二次修订)**:本文件原先声明"已删除 5 份编号文档",
> 那条**已作废** —— 经评审,`docs/04`/`06`/`10`/`13`/`99` **全部保留**(架构师明确要求保留:
> 删除收益为零,而保留成本同样为零)。它们的内容**未被核对过、可能过期**,
> 因此**列在下面的 D 类"不要用来判断当前进度"**里,只作历史参考。
> 被删除的只有 10 份**过程产物**,理由与清单见 `docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md`。
### A. 核心 7 份(无论接手哪条线都必读)
| 序 | 文档 | 承载的唯一权威内容 |
|---|---|---|
| 1 | `docs/00-新数据库基线设计.md` | **不可变业务基线**:表/字段业务语义的唯一来源 |
| 2 | `docs/05-接口文档.md` | **接口唯一权威**:信封/错误码/幂等/SSE、§8.3 知识库管理三端点、§8.4 四个只读工具索引与两段式白名单 |
| 3 | `docs/01-通用Agent平台开发设计.md` | MVC+S 分层约束、`BaseAgent` 执行骨架、`AgentFactory` |
| 4 | `docs/02-数据库建表设计.md` | 51 张业务表总览 + DDL + §8 幂等与 Outbox 语义 |
| 5 | `docs/03-平台端到端流程文档.md` | 一次请求从受理→Worker→审计→事件的全链路与降级矩阵 |
| 6 | `docs/08-数据库结构审计基线.md` | 三个审计工具 + `migration_state_check` 的职责;"证明未改基线"的证据出处 |
| 7 | `docs/07-测试问题修复记录.md` | **无替代**:P0-1/2/3 鉴权与 Worker 租约闭环、P1-1~P1-5(含 `api_request_receipt` 事务幂等) |
### B. 按角色补充
| 你接手的是 | 再读这些 |
|---|---|
| **客服 Agent + RAG 这条线** | `docs/14`(接入入口)→ `docs/18`(RAG 三集合路由方案)→ `docs/19`(可运行示例)→ `docs/09`(底座用法与四工具) |
| **整个底座** | 补 `docs/20`(第一版→当前的破坏性改动 + §5 四条尚未修复的偏差 + "跑验收前先停常驻 Worker") |
| **只改某个业务域** | `docs/00` → `docs/05` → `docs/02` → `docs/14` → `docs/19`;行情加 `docs/12`,前端/联调加 `docs/17` |
| **看"现在做到哪了"** | `docs/验收与审计/phase1-acceptance-report.md`(Phase 1 七条验收标准的逐条可复现证据)+ 同目录 `phase1-acceptance-criteria.md`(老师验收标准原文摘录) |
> 注:完整的过程台账(`progress.md`、各 Task 报告、审计报告)在 **`.superpowers/sdd/2026-09-10-客服Agent与RAG-qyqy版/`**,
> 但该目录**被 `.gitignore` 忽略**(属工作区过程产物)—— 因此**结论性文档已复制到 `docs/验收与审计/`** 以保证 git 可见。
> 若要查过程细节再去看 `.superpowers/`;日常接手**只需读本文档列出的这些**。
### C. 同主题的重复文档(读一份即可,避免信息冲突)
| 主题 | 唯一权威 | 重复品(仅历史参考) |
|---|---|---|
| Agent 组员接入 | **`docs/14`** | `docs/11`(旧版说明书)、`docs/15`(详细手册)、`docs/16`(入门易懂版)—— 三份已各自在开头标注"以 `14` 为准" |
| 接口说明 | **`docs/05`** | `docs/17`(易懂版,自述"不替代 05") |
### D. ⚠️ 不要用来判断"当前进度"
| 文件 | 为什么 |
|---|---|
| `TODO.md` | **自 2026-09-09 起未随 Phase 1 更新**:5 处"49 张表"(实为 **89 张业务表**)、T8.1 客服 Agent 整节未勾选但**已交付**、多处标"进行中"其实已完成。**当前进度一律以 `phase1-acceptance-report.md` 为准。** |
| `docs/04` / `06` / `10` / `13` / `99` | 内容**未核对过、可能过期**(自相矛盾 / 结论失效 / 清单不全)。经 2026-09-11 评审**保留**(不再删除),仅作历史参考。**不要用它们判断现状** |
### E. 环境与命令口径(易错点)
- 解释器:本机用 **`.\.venv\Scripts\python.exe`**;架构师环境用 `D:\conda\envs\jr_py313\python.exe`。
两者等价,**各用本机可用的那个**(`.venv` 被 `.gitignore` 忽略、不进仓库,不存在"需要统一"的问题)。
- 数据库现为 **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` 先分清是"库里多表"还是"迁移没进来")。
- ⚠️ **画像有两个存储,别只找 MySQL**:**值在 MySQL**(`fin_customer_profile` / `profile_snapshots` /
`fin_risk_assessment` / `user_facts`,唯一真相),**关系在 Neo4j**(`Customer` 节点 + `Tag`/`Product`
等,单向、幂等、可重建的派生视图)。图不一致时**一律以 MySQL 为准**
(`python tools/reconcile_graph.py <customer_id> [--repair]`)。设计细节见 **`docs/23`(已与代码对齐)**。
- 已注册业务 Agent(**7 个**,见 `app/service/agent/implementations/` 与 `app/service/agent/`):
`FundQueryDemoAgent`、`CustomerServiceAgent`、`RiskAgent`、`PlatformProbeAgent`、
**`AdvisorAgent`**、**`OffsiteFundAgent`**、**`PromotionMaterialAgent`**。
- 已注册公共只读工具:`search_knowledge`(客服知识检索)、`check_suitability`、`query_customer_profile`(画像)、`query_fund_quote`;
**`query_knowledge` 是 `search_knowledge` 的别名**(同一 handler,为兼容一期发布配置与旧客户端保留,见 `bootstrap.py`);
其余业务线工具(风控、投顾、NL2SQL)按各自 Agent 白名单注册,全部在 `bootstrap.py` 的 `get_agent_factory()` 里。
**工具可用范围 = 代码上限 ∩ 当前 active `config_release` 的发布白名单**,缺发布配置则失败关闭。
- ⚠️ **RBAC 权限码的定义源是 `tools/seed_test_rbac.py` 的 `PERMISSIONS`**(**9001-9065 号段,共 65 条**):
那个脚本是 **DELETE 重建**语义(`DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099`),
**没并进它的权限码重建一次就没了**,表现是"接口突然 403"而没有任何报错线索。
`tools/grant_*.py` 只补种子里缺的,且 id 必须与种子**逐条一致** ——
一致性由 `python tools/check_rbac_seed_consistency.py` 及其单测守着
(2026-09-12 曾因两套 id→code 映射并存,让 `advisor` 在种子重建后静默拿到语义错误的权限)。
号段演进(**改这里之前先看这段**):
`9001-9017` 一期公共;`9018-9034` 客服二期/投顾;`9041-9046` 产品治理与候选审核
(**`9035-9040` 与 `4041-4046` 从未建过**,`9041` 是把冲突的 `9020-9035` 挪走的修正结果,见 `docs/36`);
`9047-9050` 风控告警四个;`9051-9056` 推广/NL2SQL/探针;
`9057-9059` 投顾客户范围三项;**`9060-9065` 账户与交易看板六项**(2026-09-12 后追加,**`AGENTS.md` 旧写的「9046」已过期**);
**`9066-9068` 投顾代客三项**(`*:customer` 变体,服务层按 `customer_id == context.user_id` **动态拼**出来,
对账工具抓不到字面量,曾是"投顾一操作客户就整片 403"的根因);
**`9069` 客服转人工工单处置 `handover:write`**(2026-09-14 补:此前只有 `handover:read`,
状态机 `pending→assigned→processing→resolved→closed` 一个动作都没有入口,40 张单子全停在 `pending`;
用 `tools/grant_handover_write_permission.py` 幂等补齐,只授给 `admin`)。
另注:`sys_user` 已改为「存在则更新、不存在才插入」,故重跑种子**不会**再弄丢演示密码。
- ⚠️ **`config_release` 是环境数据,不随代码合并**:本机 active 版本 id 与架构师环境**不同**
(本机是我方发布的客服白名单;他那边还有风控的白名单)。**"白名单已发布"必须带环境限定**,换环境要重发。
⚠️ 常被误读的一点:「架构师环境有**风控的 9 条白名单**」说的是**那台环境 `config_release` 里的 9 个
`agent_tools` 配置项**,**不是**代码里的常量——**别去仓库里找"9 条"这个对象**
(本机发布脚本 `tools/publish_risk_agent_config.py` 只发 4 条:3 工具 + `general` 空)。
发布脚本 `tools/publish_customer_service_config.py`:**同 key 的继承项必须被本次定义覆盖**,
否则旧值会被子集校验 422 拦下整次发布;**继承范围必须覆盖全部三张受管表** ——
它此前只搬 `platform_config_item`,把 `customer_service_chitchat` 提示词静默漏在了旧版本里
(Agent 侧有代码默认值兜底,所以功能看着正常、零告警)。现已改用
`ConfigReleaseService.effective_snapshot()` 并在激活后硬校验条数,不符即失败退出。
另:知识类意图要**同时**发 `search_knowledge`(登录客户走)与 `query_knowledge`
(访客令牌只有 `knowledge:query`),缺哪一条对应人群就一问即失败。
- ⚠️ **Milvus 集合 schema 也因环境而异**:本机是 `knowledge_id`/`snippet`(无 `visibility`),
架构师环境是 `doc_id`/`content`/`visibility`/`chapter`…。**检索层已改为运行时探测字段名**
(`app/core/knowledge_schema.py`)——**不要在任何地方硬编码字段名**,那会把另一套环境打挂。
- ⚠️ **客服/风控对话必须有常驻 Worker**:`python -m app.worker`。Agent 请求是
「受理 202 → Worker 领单 → 落结果」三段式;没有 Worker 时 `agent_run` 会一直停在
`status='queued'`、`worker_id` 为空,而前端只显示"客服响应超时 / 客服繁忙"——
**看起来像链路慢,实际是没人处理**(2026-09-13 访客浮窗"回答超时"就是栽在这里)。
排查第一步:查 `agent_run` 最新那行是不是 `queued`。反过来,跑验收脚本前又要
**先停掉**它,否则会抢队列(见 `docs/20`)。
本机实测(Worker 在跑 + `deepseek-flash`):访客一问端到端 **4.1–4.8 秒**,
其中受理只占 0.05 秒,其余是一次意图分类加一次 embedding 检索。
- ⚠️ **Docker Desktop 不会常驻**:它没运行时 Milvus 不可用(`docker` CLI 报连不上守护进程)。
跑真机验证前先确认 Docker Desktop 在运行。
- ⚠️ **`memory_sync_outbox` 的取值必须是小写英文**(`milvus`/`neo4j`、`upsert`、
`pending`/`failed`/`processed`/`dead`)。`docs/00` §6.4.6 那一栏曾写作大写
`MILVUS`/`NEO4J`、`UPSERT` + 中文 `待处理`,**与全仓实现从未对齐,照它写会静默失效**:
消费端按 `handlers.get(target_store)` 分派、且只领 `status in {"pending","failed"}`,
大写 + 中文两个条件都不满足 ⇒ **事件任何消费者都领不到、永久滞留且不报错**
(唯一键 `(event_uuid, target_store)` 对大小写无约束,MySQL 也不报错)。
取值口径以**主干既有读取方**为准(`projection_reconciliation_service.py`、
`graph_projection_worker.py`),不是文档。详见 `docs/37-记忆投影链路实现说明.md`。
- ⚠️ **一张表只能有一个 ORM 类**:`app/model/` 下曾出现**两个类都映射 `profile_snapshots`**
(`profile.py` 与 `risk_questionnaire.py`),各自单独导入都没事,**同时导入即抛**
`InvalidRequestError: Table 'profile_snapshots' is already defined for this MetaData instance`
—— Worker 既要重建画像又要处理投顾问卷,因此**真的被打挂过**(库里 `memory_sync_outbox`
留下 `last_error='InvalidRequestError'` 的行)。2026-09-12 已修为 re-export,见 `docs/37` §6.2。
**新增模型前先搜一遍 `__tablename__` 有没有被占用。**
- ⚠️ **记忆可读范围只有一个判定口径:`app/core/memory_scope.py`**(2026-09-14 收敛)。
此前**三处各自判断**且口径不一:`governance.recall()` 把 `int(context.user_id)` 当客户号、
`BaseAgent.recall_memory()` 要求"每条记忆的 `customer_id == context.user_id`"、
`review_output()` 的引用校验只认同一条件。后果有两个,方向相反但都致命:
**① 员工身份(风控/投顾/运营/管理员/system)恒空**;**② 越权陷阱** ——
员工号与客户号同号段(演示数据里客户 9001-9020、员工 9002/9020 并存),
`int(user_id)` 撞上真实客户号就会**读到陌生客户的长期记忆并注入提示词**,且不报错。
现口径:**客户身份只读自己**;**员工身份只读 `sys_customer_assignment` 分配给自己的客户**
(`context.customer_ids`,由 `IdentityRepository.load_context()` 读入),归属未维护即
**失败关闭**并在日志点名原因;归属客户按客户号升序、单次上限 `MAX_RECALL_CUSTOMERS=10`。
**改召回、改引用校验、改范围守卫,三处必须一起走这个模块**(单测
`tests/unit/core/test_memory_scope.py` + `tests/unit/service/test_agent_governance.py` 守着)。
遗留:风控扫描上下文是 `user_id="0"`/`roles=("system",)` 且无归属行,因此**仍读不到记忆**
—— 根因是召回发生在 `handle()` 之前、上下文里没有"本次目标客户",见
`docs/演示用/记忆召回恒空-根因与修复-2026-09-14.md` §5.4。
- ⚠️ **长期记忆的 Milvus 集合名是一个代码级常量,不是配置项**:
`app/infrastructure/milvus_profile_projection.py` 的 `PROFILE_COLLECTION`
(`user_long_term_memory_v1`),**写(投影)/读(`bootstrap.get_vector_memory_adapter`)/
删(`projection_cleanup_service`)三侧共用**。曾经读/删两侧读的是 `settings.milvus_collection`
(`.env` 里 `jr_memory`,**该集合从未被创建**):`MilvusClient` 构造不校验集合存在,
于是适配器"构造成功"但每次 `search` 抛异常被吞成 `degraded` ⇒ **语义召回恒
`milvus_unavailable`**;清理则走 `vector_collection_absent` 分支 ⇒ **报成功却一个向量都没删**。
已删除 `Settings.milvus_collection` 并把三侧锁到同一常量(`Settings` 的 `extra="ignore"`
让环境里残留的 `MILVUS_COLLECTION` 被安全忽略)。
**回归守卫:`tests/unit/infrastructure/test_memory_vector_collection_consistency.py`**
—— 这类缺陷之所以能活下来,就是因为两侧单测全绿而**接缝没人守**。
- 测试基线(**2026-09-13 合并组员前端提交之后实测**):
`mypy app` → **249 个文件 0 错**;
`pytest tests/unit tests/contract` → **1376 passed, 2 skipped, 0 failed**;
`pytest tests/integration` → **104 passed**;
`python tools/audit_schema.py` → **89 张业务表**。
⚠️ **用例数会随开发增减,判断健康看"0 failed"而不是看绝对值**。
**2026-09-14 复测(记忆链路修复后)**:`pytest tests/unit tests/contract`
→ **1445 passed, 2 skipped, 1 failed**;`mypy app` → **255 个文件,3 个错**
(均在**组员新提交**的文件里,与本次改动无关:`agent_persistence_service.py:82`
的 `Any` 未导入、`run_query_service.py:79` 实参类型不匹配、`promotion_renderer.py:84`
元组长度不匹配);`ruff check app tests tools alembic hq.py` → **16 个错,同样全在
组员新文件里**(`promotion_renderer.py` 行长/`zip(strict=)`、`run_query_service.py`
导入未排序、`tools/probe_memory_state.py` 无占位符 f-string 等)。
唯一失败用例是 `tests/unit/api/test_portal_frontend.py::test_advisor_workspace_registers_documented_operation_endpoints`
—— **组员正在改投顾页面**(该页已被整体替换成自包含静态页,不再走
`api-client.js`/`app-shell.js`),**故未擅自改动**,交付前需与组员确认这页是否
还应注册 `docs/05` 的端点表。
说明:`agent_persistence_service.py:82` 的 `Any` 未导入**只是局部变量注解**,
运行时不求值(已实测不会 `NameError`),属静态检查级问题。
另:`tests/unit/service/test_offsite_document_recognition_adapter.py` 有 2 个用例在某些环境
会失败 —— 它们断言请求体里是中文原文,而 httpx 会把中文序列化成 `\uXXXX`,属**环境相关**,
不要"修"实现;真要修应改为断言 `json.loads(body)` 后的字段值。
- **mypy:`mypy app` → `Success: no issues found in 249 source files`(0 错)。**
⚠️ 曾在本机报 184 个错,**已查明是环境版本旧**,与代码质量无关 —— 复现矩阵:
| SQLAlchemy | mypy | 报错数 |
|---|---|---|
| 2.0.34(本机旧) | 1.14.1 | **173** |
| 2.0.34 | 1.20.2 | 173 |
| 2.0.52 | 1.14.1 | **6** |
| 2.0.52 | 1.20.2 | **0**(当前) |
⇒ 主因是 **SQLAlchemy 的补丁版本**(旧补丁版类型标注不完整,`BIGINT`/`DATETIME` 被判成未类型化
函数,`app/model/*.py` 每个列定义报一条)。出现"一边上百个错、另一边 0 错"时**先对版本**,
别当代码质量问题;根因是某一侧的虚拟环境没满足 `pyproject.toml` 的
`sqlalchemy>=2.0,<3` / `mypy>=1.14,<2`。
**不要装 `sqlalchemy2-stubs`** —— 那是给 SQLAlchemy **1.4** 用的,2.0 自带 `py.typed`,
装上会按 1.4 API 核对 2.0 代码、换一批新错(`mapped_column` / `DeclarativeBase` 不存在)。
`pyproject.toml` 的 `sqlalchemy>=2.0,<3` 允许范围内补丁版差异会造成量级差异;
若门禁数字要求稳定,需把 SQLAlchemy 钉到具体补丁版(属公共约定,改前先问)。
- ⚠️ **`MILVUS_LOCAL_URI` 配了就会"看着正常、查的是另一个库"**:一旦在 `.env` 里设置它,
健康检查与部分检索链路会指向本地 **Milvus Lite 文件**。团队/生产环境请**保持该变量为空**。
对应的 `milvus-lite` 属**本地开发依赖**,应放在 `pyproject.toml` 的
`optional-dependencies`,**不要进主 `dependencies`**。
- 集成测试前置(**不跑这两步,`tests/integration` 会有 13 个登录/RBAC 用例因 401 而红**,
容易被误判成代码缺陷):先 `python tools/seed_test_rbac.py`(角色/权限/演示账号),
再 `python tools/set_user_password.py`(演示口令,**非幂等**:重复执行等于重设密码)。
接口联调清单见 `docs/32-平台侧交接与联调准备.md`;主干 PR #7 合并的逐项证据见
`docs/36-PR7合并记录与权限号段修正.md`。