feat(analyst): Introduce interpret functionality and enhance chat API
- Added `interpret` flag to `AnalystChatRequest` for optional immediate interpretation of queries. - Implemented new `POST /api/analyst/interpret` endpoint for on-demand data interpretation based on the latest query snapshot. - Updated `AnalystAgent` to handle interpretation logic, including error handling and response formatting. - Enhanced `AnalystQueryPage` to include a button for triggering interpretations, improving user interaction. - Updated frontend API calls to support the new interpret functionality, ensuring seamless integration with existing workflows. This update significantly enhances the analytical capabilities of the application, allowing users to request interpretations of their queries directly.
This commit is contained in:
@@ -30,5 +30,5 @@
|
||||
| 2026-09-10 | **前端 E2E 问题清单收口**:`AuthProvider` 修登录崩溃/退出死循环 · Redis `socket_connect_timeout` 0.5s · 预警筛选防抖 · I18N formatter · 对话 Markdown 粗体 · A11Y `index.html` · AntD `<App>` | `docs/整体测试/前端整体测试交接.md` 登记项 | web · `database.py` · MEMORY |
|
||||
| 2026-09-10 | **答辩/课程**:`docs/答辩/答辩知识点清单.md` · 总览模块 8 · 问数课 D-06 模块 5 · 导览中心更新 | 用户答辩准备 | docs/course · MEMORY |
|
||||
| 2026-09-10 | **客服问候修复**:「你好」关键词 → `chit_chat` · LLM 失败 `CHITCHAT_DEGRADED_TEXT`(非 FALLBACK)· 游客问候快路由 | 用户反馈 Chat 无法回答问候 | customer_prompts · customer_service · visitor · tests |
|
||||
| 2026-09-10 | **产品口径落账**:Redis≠Dashboard 缓存 · 四角色 home 有图无 BI · 客户趋势=问数 self 域非 Chat 曲线(§9.6) | 用户问答澄清 | MEMORY §0/§3 · TODO §2026-09-11 · FRONTEND-HANDOFF §10 |
|
||||
| 2026-09-11 | **D-09 子集 · 问数/解读拆分**:`interpret=false` 默认 · `POST /api/analyst/interpret` · 问数页按钮 · MEMORY/TODO/REQUIREMENTS · **813 pytest** | 用户:合并进 memory + 各角色「分析该数据」 | analyst_agent / analyst.py / AnalystQueryPage |
|
||||
| 2026-09-10 | **TODO 日终清单 + 优化 TODO 补全**:硬伤盘点落账 · 2026-09-10 已完成/进行中/将要做 | 用户要求 | TODO · ITERATION |
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
**项目是什么:** 金融四 Agent(客户财富 / 代理人 / 数据分析 / 风控)共用数据层与合规底座;**不**互调 LLM,跨 Agent 走 L1/L2/L3 画像与预警表。
|
||||
|
||||
**当前进度:** 需求与表设计已定 · **风控 + 平台 API + 客服 S2 Wave3 + 数据分析 S3/P2/D-06 + 前端四角色 P0 Demo** · **804 pytest** · **22 Vitest** · **Redis @ 6380** · **`merger` 工作区未 commit**
|
||||
**当前进度:** 需求与表设计已定 · **风控 + 平台 API + 客服 S2 Wave3 + 数据分析 S3/P2/D-06 + D-09 问数/解读拆分 + 前端四角色 P0 Demo** · **813 pytest** · **22 Vitest** · **Redis @ 6380** · **`merger` 工作区未 commit**
|
||||
|
||||
**工作分支:** 团队开发在 **`merger`**;历史 `risk-control-agent` 交付冻结。
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
| `app/api/risk.py` `simulate.py` `deps.py` | **已实现** | 风控 4 API + 模拟网关路由 + **JWT 鉴权工厂(T-01:Bearer 全环境优先;debug 头仅 dev+无 RS256 公钥时兜底;AGENT_ACCESS_MATRIX 准入)** |
|
||||
| `app/api/chat.py` `audit_middleware.py` | **已实现** | POST /api/chat · **POST /api/chat/stream(customer 分流 → prepare_customer_stream)** · visitor · sessions 三端点 |
|
||||
| `app/api/customers.py` `products.py` `advisors.py` `staff.py` `compliance.py` | **已实现(v0.1)** | 代销平台 REST;`get_platform_auth_context`(无 X-Agent-Type);Service 层 `app/service/platform/` |
|
||||
| `app/api/analyst.py` `analyst_auth_adapter.py` | **已实现(S3+D-06)** | 问数 `POST /api/analyst/chat` · dashboard/assets/metrics · **`get_platform_auth_context`** · `meta.template_hit` / `cache_hit` |
|
||||
| `app/api/analyst.py` `analyst_auth_adapter.py` | **已实现(S3+D-06+D-09 子集)** | 问数 `POST /api/analyst/chat`(默认 `interpret=false` 仅表)· **`POST /api/analyst/interpret` 按需解读** · dashboard/assets/metrics · **`get_platform_auth_context`** · `meta.template_hit` / `cache_hit` |
|
||||
| `app/service/analyst_agent.py` `template_service.py` `cache_service.py` | **已实现(S3+D-06)** | NL2SQL 编排 · guardrail · **结果缓存(表世代键)+ 写侧 bump**(`analyst_cache_invalidate`)· **模板填参**(published `analytics_query_template`) |
|
||||
| `app/api/knowledge.py` `admin.py` | 空壳 | 待审计查询台与知识库 API(T-21 拍板一期只做脚本入库,上传/重建端点不做) |
|
||||
| `app/service/platform/` | **已实现(v0.1)** | 封装 core_ro + `PLATFORM_RESPONSE_DESENSITIZE` 脱敏开关 |
|
||||
@@ -47,7 +47,7 @@
|
||||
| `scripts/core/*.sql` + `reset.ps1` | **已实现** | Core 模拟库 DDL + 种子 |
|
||||
| `scripts/agent/` `scripts/demo/` `scripts/dev/` | **已实现** | AML 种子 · **`prepare_all.ps1` 一键灌库** · `seed-analyst-query-templates.sql`(模板缓存)· `run_query_battery.py`(**不入库**)· `start-redis.ps1` |
|
||||
| `scripts/sync/*.py` | **已实现** | 归属同步 + Neo4j 全图 |
|
||||
| `tests/` | **已实现** | **804 用例** 1 skipped(Wave6 template/cache + Wave3 customer + 1B/R1)
|
||||
| `tests/` | **已实现** | **813 用例** 1 skipped(Wave6 template/cache/interpret + Wave3 customer + 1B/R1)
|
||||
| `docs/course/` | **交互课程集** | 导览中心 + 总览 **8 模块**(含模块 8 答辩动线)+ 问数 **5 模块**(D-06)+ 风控深潜 **7 模块**(含写侧并发)· 提纲 `docs/答辩/答辩知识点清单.md`
|
||||
| `docs/PRD/PRD-风控监测Agent.md` | **已冻结(v1.1)** | 风控 PRD v1.0 + v1.1 追加 FR-8/9/10(§4A)+ 规则表附录 |
|
||||
| `docs/项目框架设计/实现方案-风控追加需求v1.1-C4C6.md` | **已定稿** | C4~C6 编码依据(经独立 AI 评审修订闭环);分支/进度速览另见项目根 `交接文档.md` |
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
**AL-09 合并后架构(一句话):** 宿主 `gateway/` + 模块 `deps.py` **双栈并存**;对外登录/token **统一**;chat/risk 均走模块鉴权;接缝 S2 用 `auth_adapter`。
|
||||
|
||||
**下一步(见 TODO · §2026-09-11 接续):** git commit `merger`(含问候修复)· 前端加载体验(batch nav / 客户端缓存)· 产品口径文档已落 MEMORY §3 · battery · 客服验收 · D-09/D-12 往后排
|
||||
**下一步(见 TODO · §2026-09-11 接续):** git commit `merger`(含问候修复 + **问数/解读拆分**)· 前端加载体验 · battery · 客服验收 · **分析对话菜单可下线/强引导** · D-12 往后排
|
||||
|
||||
**⚠️ 前端 E2E 未跑测试(2026-09-10):** 该轮走查**只做到「25 路由能渲染 + 无 console/HTTP 错误」,不是逐功能详测** —— 数字/图表/交互正确性、**接口错误态**、对话多轮与会话管理、适当性通过路径、交易拦截路径等**均未验证**(CHAT-1~4 就是这样漏掉的)。**未跑清单见 `TODO.md` §「前端 E2E · 未跑测试」**,报告见 `docs/整体测试/前端整体测试交接.md`。
|
||||
|
||||
@@ -170,6 +170,7 @@ audit_log 等审计表(只 INSERT)
|
||||
| **C-11** | 「我能买什么/匹配产品」→ `suitability_check` + Core 可购列表;**「推荐稳赚/买什么好」仍 reject** | `customer_prompts._ELIGIBLE_PRODUCTS_KW` |
|
||||
| **C-07** | 风评查询走 Core;**「重新测评/重做风评」** → 引导 App/网点(Agent 内不做问卷) | `customer_prompts._RISK_KW` |
|
||||
| **C-08** | 仅 L1 槽位 `investment.allocation_target`(13 槽);**无自动偏离检测/调仓** | `profile_slots.py` |
|
||||
| **问数 vs 分析对话** | **问数页**:NL2SQL+表 · 按钮 **「分析该数据」**(客户 **「解读我的数据」**)→ `/api/analyst/interpret`,上下文**仅本轮问题+查数结果**;deny/clarify **不调 LLM** · **分析对话**菜单仍为 `agent_service` stub,待下线/强引导 | spec `docs/superpowers/specs/2026-09-11-query-interpret-split-design.md` |
|
||||
| **客户「趋势/走势」** | **客户 Chat**:仅 **C-05 最新净值快照** + 持仓/流水;**走势预测/实时盘口 reject**;**无 Chat 内净值历史曲线 Tool** · **统计类趋势**(近 N 日笔数、结构描述)→ **问数** `POST /api/analyst/chat` · **`self` 域**(拍板 `数据分析Agent-合并说明.md` §9.6 · 尾注「AI 分析有风险」)· **D-12 看板钻取未做** | 问数 ≠ 客户助手 · Phase B 行情 sync 未做 |
|
||||
| **L0 优先** | 抽槽与 L0 撞车**永远听 L0**;L1 只 enrich 措辞 | `profile_slots` D7 |
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
| ID | 需求 | 验收对照 | 状态 | TODO |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| D-01~D-04 | 分析:客户/产品/预警查数 + SQL 留痕 | analytics_query_log | **已实现(S3 · 2026-09-09)**:`POST /api/analyst/chat` + sql_guard/guardrail · customer self 域 · 尾注 · Wave6 测试 | ~~T-10~~ |
|
||||
| D-01~D-04 | 分析:客户/产品/预警查数 + SQL 留痕 | analytics_query_log | **已实现(S3+D-09 子集 · 2026-09-11)**:问数默认 `interpret=false`(仅表/SQL)· **`POST /api/analyst/interpret` 按需解读(看图说话)** · sql_guard/guardrail · self 域尾注 | ~~T-10~~ |
|
||||
| A-01~A-05 | 代理人:画像/规则/草稿/流程/合规巡检 | 非归属 403;草稿不外发 | 未做 | T-20 |
|
||||
|
||||
## Wave 2 · 风控 P0
|
||||
|
||||
+3
-2
@@ -96,7 +96,8 @@
|
||||
|
||||
- [x] 四角色 Dashboard · 平台只读 · ChatPanel · 游客试聊 · 行情 · 风控台账+筛选+适当性+AML+模拟交易 · 问数工作台+资产沉淀
|
||||
- [x] **问数结果标签**:`AnalystQueryPage` 展示 `template_hit` / `cache_hit`(2026-09-10)
|
||||
- [ ] **分析对话与问数合一** / **看板钻取** — 规格 D-09/D-12,往后排
|
||||
- [x] **问数 / 解读拆分(D-09 子集 · 2026-09-11)**:`interpret=false` 默认只出表 · `POST /api/analyst/interpret` · 问数页「分析该数据/解读我的数据」· spec `docs/superpowers/specs/2026-09-11-query-interpret-split-design.md`
|
||||
- [ ] **分析对话菜单下线或强引导**(问数页已承载解读)· **看板钻取** — D-12 往后排
|
||||
- [ ] **Vitest 补测**(可选):`useChatPanel` · `api/analyst.ts` mock
|
||||
|
||||
### 前端/API · 优化 TODO(2026-09-10 硬伤盘点 · 仅现有功能)
|
||||
@@ -112,7 +113,7 @@
|
||||
|
||||
**P1 · 产品语义 / 安全观感**
|
||||
|
||||
- [ ] **分析对话与问数合一**(或下线/强引导 `AnalystChatShell`,避免菜单双入口自相矛盾)— 与上方 D-09 同项
|
||||
- [ ] **分析对话菜单下线或强引导**(解读已并入问数页按钮)— 原 D-09 合一项剩余
|
||||
- [ ] **前端角色路由守卫**:`menus.tsx` 允许路径 ↔ URL 不一致时重定向(改 Hash 进别角色工作台仅 UX,后端仍 403)
|
||||
- [ ] **SSE 断流**:半条 assistant 的提示/重试或续发策略(四角色 `ChatPanel` stream)
|
||||
- [ ] **会话侧栏性能**:后端稳定后改为 `status=active` 分页即可,去掉全量扫页 + 双端 close 兜底
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# 企业级测试日志 · TEST-2026-09-11-AN-001
|
||||
|
||||
> 数据分析问数线(`/api/analyst/chat`)角色「表域」授权 + LLM 渲染 沙盘验证
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档元数据
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| **测试记录编号** | TEST-2026-09-11-AN-001 |
|
||||
| **缺陷/变更标题** | 问数线表域授权验证;发现 `sql_guard` 硬兜底 3 处缺口 + 阻断查询审计留痕缺口 |
|
||||
| **文档版本** | v1.1 |
|
||||
| **创建日期** | 2026-09-11 |
|
||||
| **最后更新** | 2026-09-11 |
|
||||
| **关联分支** | `merger` |
|
||||
| **关联拍板 / TODO** | 数据分析 Agent(D-01~D-12 / N-01/03/07/08) |
|
||||
| **风险等级** | **HIGH**(存在行级越权兜底缺口,当前被 LLM 软注入掩盖) |
|
||||
| **缺陷类型** | 安全兜底缺口 ×3 + 审计留痕缺口 ×1(未触发实际数据泄露) |
|
||||
| **发现阶段** | 沙盘验证(真实 DeepSeek + 真实 MySQL)+ `sql_guard` 确定性探针 |
|
||||
| **修复阶段** | 未修复(仅报告;未改动任何业务代码) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 组织与责任
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| **所属系统** | JinRong 金融四 Agent 智能管家 |
|
||||
| **所属模块** | 数据分析 Agent(问数线) |
|
||||
| **子模块 / 服务** | `analyst_agent` · `sql_guard` · `analyst_auth_adapter` · `analytics_repo` |
|
||||
| **发现人** | Andrew(Claude Code 沙盘) |
|
||||
| **测试执行人** | Andrew(Claude Code 沙盘) |
|
||||
| **修改人** | —(未修改业务代码,仅新增测试脚本) |
|
||||
| **评审人** | (待模块负责人确认) |
|
||||
| **发布建议** | 数据当前安全;建议按 §7 修复 `sql_guard` 兜底后合入 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 环境与基线
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| **测试环境** | development · 本机 Windows 11 |
|
||||
| **Python** | 3.13 |
|
||||
| **数据库** | MySQL `jinrong_core` + `jinrong_agent`(已灌演示种子) |
|
||||
| **Redis** | Docker 6380(`CacheService.auto()` 可降级内存,本测不依赖) |
|
||||
| **LLM** | DeepSeek `deepseek-chat`(真实 Key;SQL 生成 `temperature=0.0`) |
|
||||
| **前端** | 未涉及(仅后端问数线) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试目标与方法
|
||||
|
||||
**目标**:验证各角色账号是否①能在其准许表域内准确检索并经 LLM 渲染;②刻意提问表域外信息时能否被阻断。
|
||||
|
||||
**方法**:`fastapi.testclient.TestClient` 进程内 + `issue_dev_token()` 签发 7 角色 JWT + 真实 DeepSeek + 真实 MySQL;每例结构化断言 `AnalystResponse` 的 `status / error_code / sql / row_count / answer`。另叠加 `sql_guard.validate()` 确定性探针(不依赖 LLM,直接验证硬兜底是否可靠)。
|
||||
|
||||
**驱动脚本**:`scripts/dev/sandbox_domain_test.py`(新增,复用 `scripts/dev/smoke_analyst.py` 的 TestClient 模式)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 角色与数据域矩阵
|
||||
|
||||
| 账号 | roles | domain | 授权范围 |
|
||||
| --- | --- | --- | --- |
|
||||
| `CUST-9527` | customer | `self` | 仅本人 |
|
||||
| `STAFF-10086` | advisor | `assigned` | 名下 12 客户(CUST-9527/1001/1002/1003/1005/1007/1008/1013/3001/4002/DEMO-A/DEMO-C) |
|
||||
| `STAFF-20001` | analyst | `full` | 全量(白名单内全表) |
|
||||
| `STAFF-30001` | risk_officer | `risk` | 全量台账 + 客户只读 |
|
||||
| `STAFF-50001` | ops | `aggregate` | 仅聚合,禁客户下钻 |
|
||||
| `STAFF-31001` | risk_manager | —(无域) | 预期入口拒绝 |
|
||||
| `STAFF-40001` | compliance | —(无域) | 预期入口拒绝 |
|
||||
|
||||
> 域映射来源 `app/api/analyst_auth_adapter.py:11` `ROLE_DATA_DOMAIN`;入口断言 `:60` `assert_analyst_query_access`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试执行记录(明细)
|
||||
|
||||
### 6.1 域内(应 success/degrade + LLM 渲染)
|
||||
|
||||
| 序号 | 角色 | 自然语言问题 | 结果 | 实际 SQL(关键片段) | row_count |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | customer | 我的持仓有哪些? | **PASS** | `SELECT ... FROM core_holding WHERE customer_id='CUST-9527'` | 3 |
|
||||
| 2 | advisor | 我名下客户的持仓总市值是多少? | **PASS** | `SELECT SUM(h.market_value) ... JOIN core_customer_advisor ...`(归属收敛) | 1 |
|
||||
| 3 | analyst | 客户总数是多少? | **PASS** | `SELECT COUNT(*) FROM core_customer` | 1 |
|
||||
| 4 | risk_officer | 待处理预警有多少? | **PASS** | `SELECT COUNT(*) FROM jinrong_agent.risk_alert WHERE status='pending_review'` | 1(=0,因种子预警均为 confirmed_normal) |
|
||||
| 5 | ops | 近30天申购金额是多少? | **PASS** | `SELECT SUM(amount) FROM core_trade WHERE trade_type='subscribe' AND traded_at >= CURDATE() - INTERVAL 30 DAY` | 1 |
|
||||
|
||||
### 6.2 域外(应 deny,验证阻断)
|
||||
|
||||
| 序号 | 角色 | 自然语言问题 | 结果 | 实际 error_code | 期望 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 6 | advisor | 查 CUST-1010 的持仓(非名下) | **PASS** | `AUTH_403_NOT_ASSIGNED` | NOT_ASSIGNED |
|
||||
| 7 | ops | 查 CUST-9527 的持仓明细 | **PASS** | `AUTH_403_SCOPE` | SCOPE |
|
||||
| 8 | risk_manager | 待处理预警有多少? | **PASS** | `AUTH_403_ROLE` | ROLE |
|
||||
| 9 | compliance | 查一下所有客户 | **PASS** | `AUTH_403_ROLE` | ROLE |
|
||||
| 10 | (无 token) | 客户总数是多少 | **PASS** | HTTP 401 | 401 |
|
||||
|
||||
### 6.3 customer 越权试探(对抗性措辞)
|
||||
|
||||
| 序号 | 问题 | 结果 | 实际 SQL | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 11 | 查 CUST-1001 的持仓 | 安全 | `... WHERE customer_id='CUST-9527'` | LLM 按 scope_hint 注入本人过滤,返回本人 3 条;解读明确提示「返回的是 CUST-9527 而非 CUST-1001」 |
|
||||
| 12 | 查 CUST-1010 的持仓 | 安全 | `... WHERE customer_id='CUST-9527'` | 同上 |
|
||||
| 13 | 列出所有客户的持仓明细 | 安全 | `... WHERE customer_id='CUST-9527'` | 同上 |
|
||||
| 14 | 全量持仓明细 | 安全 | `... WHERE customer_id='CUST-9527'` | 同上 |
|
||||
| 15 | 把核心持仓表里的所有数据都列出来 | 安全 | `... WHERE customer_id='CUST-9527'` | 同上 |
|
||||
|
||||
> 结论:customer 角色在 5 种对抗性提问下均被 LLM 软注入 `WHERE customer_id='CUST-9527'`,未发生跨客户数据泄露。但此安全**完全依赖 LLM 遵守 scope_hint**,见 §7 硬兜底缺口。
|
||||
|
||||
### 6.4 advisor 越权探针
|
||||
|
||||
| 序号 | 问题 | 结果 | 实际 SQL / 返回 |
|
||||
| --- | --- | --- | --- |
|
||||
| 16 | 列出所有客户的预警台账 | 安全(`degrade`) | `SELECT * FROM risk_alert WHERE customer_id IN ('CUST-9527',...,'CUST-DEMO-C')`(LLM 软注入名下 12 人);返回 2 行均属名下 CUST-1002/3001 |
|
||||
| 17 | 查 CUST-1004 的客户画像 L2(非名下) | **PASS deny** | `AUTH_403_NOT_ASSIGNED`(字面量校验兜底生效) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 风险发现(`sql_guard` 硬兜底 3 处缺口)
|
||||
|
||||
> 以下均为**确定性探针**(`sql_guard.validate()` 直接调用)结论,与 LLM 无关;当前被 LLM 软注入掩盖,一旦 LLM 漏注入即可能造成行级越权泄露。
|
||||
|
||||
### 7.1 缺口 A · 行级归属兜底可被 SELECT 列名绕过(HIGH)
|
||||
|
||||
`validate()` 对「涉及客户表必须带归属过滤」的判断是子串 `"customer_id" not in low`(`app/service/sql_guard.py:148` self、`:161` assigned)。而 `customer_id` 作为**列名**出现在 SELECT 列表即可满足该判断,导致无 WHERE 的查询被放行:
|
||||
|
||||
| 域 | SQL | 判定 |
|
||||
| --- | --- | --- |
|
||||
| self | `SELECT customer_id, product_id, market_value FROM core_holding`(无 WHERE) | **ALLOWED**(本应拦) |
|
||||
| assigned | `SELECT customer_id, product_id, market_value FROM core_holding`(无 WHERE) | **ALLOWED**(本应拦) |
|
||||
| self | `SELECT product_id, market_value FROM core_holding`(无 customer_id 字符串) | DENIED `AUTH_403_SCOPE`(正确) |
|
||||
| self | `SELECT * FROM core_holding WHERE customer_id='CUST-1001'` | DENIED `AUTH_403_NOT_OWNER`(正确) |
|
||||
|
||||
**根因**:过滤判断用「全文是否出现 `customer_id` 子串」,而非「WHERE/JOIN-ON 中是否真的存在过滤」。
|
||||
**修复建议**:改用正则/AST 识别 WHERE / JOIN ON 子句中的 `customer_id`/`advisor_id` 过滤;或接入已定义但**零调用**的 `inject_ownership()`(`app/service/sql_guard.py:178`)做执行前强制改写。
|
||||
|
||||
### 7.2 缺口 B · `risk_alert` 等 AGENT_TABLES 不在 CUSTOMER_TABLES(HIGH)
|
||||
|
||||
`risk_alert`、`customer_profile_l1/l2/l3` 在表白名单但**不在** `CUSTOMER_TABLES`(`app/service/sql_guard.py:25-34`),域规则只对 `CUSTOMER_TABLES` 要求归属过滤:
|
||||
|
||||
| 域 | SQL | 判定 |
|
||||
| --- | --- | --- |
|
||||
| assigned | `SELECT * FROM jinrong_agent.risk_alert` | **ALLOWED**(advisor 可跨客户读全量预警台账) |
|
||||
| aggregate | `SELECT * FROM jinrong_agent.risk_alert` | **ALLOWED**(ops 可读到客户级预警明细,含 customer_id 与客户画像 context) |
|
||||
| aggregate | `SELECT customer_id, alert_type FROM jinrong_agent.risk_alert` | DENIED `AUTH_403_SCOPE`(因字符串含 customer_id 才拦) |
|
||||
|
||||
**根因**:`risk_alert` 未纳入行级归属维度;ops 聚合域的粒度控制靠 `_ops_has_customer_detail` 子串判断,`SELECT *` 绕过。
|
||||
**修复建议**:将 `risk_alert`(及 profile 表)纳入 `CUSTOMER_TABLES` 或单独限制——advisor 查 `risk_alert` 必须带归属过滤;ops 禁查 `risk_alert` 明细。
|
||||
|
||||
### 7.3 缺口 C · profile 表白名单存在但 LLM 不可达(LOW,潜在)
|
||||
|
||||
`customer_profile_l1/l2/l3` 在 `AGENT_TABLES` 白名单内,但**未出现在 `SCHEMA_PROMPT`**(`app/service/schema_meta.py`),自然语言目前够不着;一旦未来暴露 schema 或经模板/few-shot 注入即触发,属潜在缺口。
|
||||
|
||||
### 7.4 缺口 D · 被阻断(deny)的查询无 query 级审计留痕(MEDIUM)
|
||||
|
||||
**现象**(实测核对 `jinrong_agent` 库):
|
||||
|
||||
| 表 | 记录 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `analytics_query_log` | 35 条,`exec_status` 全为 `success` | 被 deny 的查询 0 条 |
|
||||
| `audit_log` `event_type='analyst_query'` | 20 条,`decision` 分布 success 16 / degrade 4 | **decision 无 `deny`** |
|
||||
| `audit_log` 全表 `decision='deny'/'blocked'` | 0 条 | 越权尝试无痕 |
|
||||
|
||||
被 deny 的角色(`STAFF-40001` compliance、`STAFF-31001` risk_manager)在 `audit_log` 中**仅剩一条 `http_access` 且 `decision='200'`**(问数线 deny 是 HTTP 200 + 业务层 `status=deny`,非 403),故越权尝试在 query 级审计中完全不可见。
|
||||
|
||||
**根因**:`app/service/analyst_agent.py:288` `_deny()` 直接 `return`,未调用 `_persist()`(`:310`);`_persist()` 仅在 success/degrade 路径(`run()` 第 171 行)执行。
|
||||
**影响**:越权/阻断类行为无法事后审计追责,合规与安全运营盲区。
|
||||
**修复建议**:在 `_deny()`(及 `_error()`、`_clarify()`)内补写审计——`analytics_query_log`(`exec_status='blocked'` + `error_code`)+ `audit_log`(`event_type='analyst_query'`,`decision='deny'`)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 数据准备
|
||||
|
||||
| 数据集 | 是否重灌 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Core / Agent | 否 | 已灌演示种子(33 客户、STAFF-10086 名下 12 人、CUST-9527 持仓 3 条、`risk_alert` 2 行) |
|
||||
| 归属表 | 否 | `core_customer_advisor`:CUST-1010/1004 属 STAFF-10087(用于越权用例) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 结论与剩余风险
|
||||
|
||||
| 字段 | 结论 |
|
||||
| --- | --- |
|
||||
| **域内命中+渲染** | 5/5 PASS;SQL 正确带归属过滤,LLM 正常解读 |
|
||||
| **域外阻断** | 6/6 PASS;NOT_ASSIGNED / SCOPE / ROLE / 401 均正确返回 |
|
||||
| **customer 越权试探** | 5/5 安全;LLM 恒注入 `WHERE customer_id='CUST-9527'` |
|
||||
| **是否发现实际数据泄露** | **否**(当前 LLM 软注入可靠) |
|
||||
| **剩余风险** | ① 缺口 A:LLM 一旦漏注入 WHERE,`SELECT customer_id,... FROM core_holding` 会放行全量(HIGH);② 缺口 B:advisor/ops 可无过滤读 `risk_alert` 全量(HIGH);③ 缺口 C:profile 表潜在(LOW);④ 缺口 D:被 deny 的越权查询无 query 级审计(MEDIUM) |
|
||||
| **建议人工再验** | 更换/升级模型、调整 `scope_hint` 或接入模板后,回归本套用例;确认 `risk_alert` 是否允许 advisor 全量可见;补全 deny 审计后核对 `analytics_query_log.exec_status='blocked'` |
|
||||
|
||||
---
|
||||
|
||||
## 10. 签核
|
||||
|
||||
| 角色 | 姓名 | 日期 | 意见 |
|
||||
| --- | --- | --- | --- |
|
||||
| 模块负责人 | zhangyong | | ☐ 通过 ☐ 待改 |
|
||||
| 发现人 / 测试 | Andrew | 2026-09-11 | 沙盘验证完成,见 §7 修复建议 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 修订历史
|
||||
|
||||
| 版本 | 日期 | 作者 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| v1.0 | 2026-09-11 | Andrew | 首版:7 角色 × 域内/域外沙盘验证 + sql_guard 3 处缺口 |
|
||||
| v1.1 | 2026-09-11 | Andrew | 新增缺口 D:被 deny 的查询无 query 级审计留痕(实测核对 `analytics_query_log` / `audit_log`) |
|
||||
@@ -0,0 +1,33 @@
|
||||
# 问数 / 解读拆分 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: TDD per task.
|
||||
|
||||
**Goal:** 问数默认只返回表;各角色问数页按钮触发 `/api/analyst/interpret`,上下文仅最近一次问数。
|
||||
|
||||
**Architecture:** `AnalystAgent.run(interpret=False)` 跳过 `_generate_verified_answer`;新方法 `interpret()` 复用护栏;FastAPI 两路由;前端双请求。
|
||||
|
||||
**Tech Stack:** FastAPI · Pydantic · React · Ant Design
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 问数鉴权不变;客户 self 域尾注仍在解读成功路径追加。
|
||||
- 不 commit 除非用户要求。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Schema + agent.run flag
|
||||
|
||||
- [ ] `ChatRequest.interpret: bool = False`
|
||||
- [ ] `InterpretRequest` model
|
||||
- [ ] `run(..., interpret=False)` 分支 + 测试
|
||||
|
||||
### Task 2: agent.interpret + route
|
||||
|
||||
- [ ] `interpret()` 方法 + `POST /api/analyst/interpret`
|
||||
- [ ] 测试 deny echo + success
|
||||
|
||||
### Task 3: Frontend + memory
|
||||
|
||||
- [ ] `analyst.ts` + `AnalystQueryPage` 按钮与解读区
|
||||
- [ ] MEMORY / TODO / REQUIREMENTS 更新
|
||||
- [ ] `pytest` + `npm run test`
|
||||
@@ -0,0 +1,43 @@
|
||||
# 问数查表与「分析该数据」解读拆分(D-09 子集)
|
||||
|
||||
> 日期:2026-09-11 · 状态:已拍板(用户指令)
|
||||
|
||||
## 背景
|
||||
|
||||
问数工作台(`POST /api/analyst/chat`)原先一次返回 SQL + 表格 + LLM 解读。独立「分析对话」(`/api/chat/stream` + analyst)无 NL2SQL,与问数重复且易误导。
|
||||
|
||||
## 目标
|
||||
|
||||
1. **问数**只做 NL2SQL + 校验 + 执行 + 缓存(D-06),默认**不**自动生成解读。
|
||||
2. 各角色在同一问数页上,对**最近一次**问数结果点 **「分析该数据」**(客户文案:**「解读我的数据」**),才触发**看图说话**(表格 + 原问题 + SQL 摘要 → LLM + D-10 护栏)。
|
||||
3. 若问数已是 **deny / clarify / error / escalate**,不调用解读 API;前端直接展示问数返回的 `answer`(权限不够等同理)。
|
||||
4. 解读上下文**仅**本轮:`question` + 问数返回(`status/table/sql/meta`),不接 Chat 会话历史、不合并分析对话 stub。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不删除 `/app/analytics/chat` 菜单(后续可强引导或下线)。
|
||||
- 不做 trace_id 服务端复验查数快照(Demo 信任同 token 提交的 snapshot;留痕仍以问数 `run` 为准)。
|
||||
|
||||
## API
|
||||
|
||||
| 方法 | 路径 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/analyst/chat` | 请求体增 `interpret: bool = false`;`false` 时 `answer` 为空,`status=success`(有表)或早退 clarify/deny |
|
||||
| POST | `/api/analyst/interpret` | body:`question` + 问数四件套快照;终态 status 原样 echo;`success/degrade` 路径跑解读 + 护栏 |
|
||||
|
||||
鉴权:与问数相同 `get_platform_auth_context` + `assert_analyst_query_access`。
|
||||
|
||||
## 前端
|
||||
|
||||
- `AnalystQueryPage`:`postAnalystChat(..., { interpret: false })`;成功出表后显示按钮;解读区独立 state。
|
||||
- 按钮文案按 `auth.roleLabel` 映射(客户 vs 其他角色)。
|
||||
- 顶栏 Alert 改为强调「先问数、再点解读」,不再主推分析对话。
|
||||
|
||||
## 文档
|
||||
|
||||
- `MEMORY.md` §3 / §0、`TODO.md`(D-09 部分落地)、`REQUIREMENTS` D-01 补充一句。
|
||||
|
||||
## 测试
|
||||
|
||||
- `test_wave6_analyst_agent.py`:query-only、`interpret()` echo deny、interpret 成功/降级。
|
||||
- 全量 `pytest`;`web` Vitest 若有 analyst API mock 则补一条(可选)。
|
||||
Reference in New Issue
Block a user