- 新增 ConversationHistoryManager.get_tenant_summary() 按租户聚合会话统计 - get_sessions_paginated() 和 get_conversation_analytics() 增加 tenant_id 过滤 - 新增 GET /api/conversations/tenants 租户汇总端点 - sessions 和 analytics API 端点支持 tenant_id 查询参数 - 前端实现租户卡片列表视图和租户详情会话表格视图 - 实现面包屑导航、搜索范围限定、统计面板上下文切换 - 会话删除后自动检测空租户并返回列表视图 - dashboard.html 添加租户视图 DOM 容器 - 交互模式与知识库租户分组视图保持一致
311 lines
13 KiB
Markdown
311 lines
13 KiB
Markdown
# Design Document: 知识库租户分组展示 (knowledge-tenant-view)
|
||
|
||
## Overview
|
||
|
||
本设计将知识库管理页面从扁平列表改造为两层结构:第一层按 `tenant_id` 分组展示租户汇总卡片,第二层展示某租户下的知识条目列表。改造涉及三个层面:
|
||
|
||
1. **后端 API 层** — 在 `knowledge_bp` 中新增租户汇总端点 `/api/knowledge/tenants`,并为现有 `/api/knowledge` 和 `/api/knowledge/stats` 端点增加 `tenant_id` 查询参数支持。
|
||
2. **业务逻辑层** — 在 `KnowledgeManager` 中新增 `get_tenant_summary()` 方法,并为 `get_knowledge_paginated()`、`search_knowledge()`、`get_knowledge_stats()` 方法增加 `tenant_id` 过滤参数。`add_knowledge_entry()` 方法也需接受 `tenant_id` 参数。
|
||
3. **前端展示层** — 在 `dashboard.js` 中实现 `Tenant_List_View` 和 `Tenant_Detail_View` 两个视图状态的切换逻辑,包括面包屑导航、统计面板上下文切换、搜索范围限定。
|
||
|
||
数据模型 `KnowledgeEntry` 已有 `tenant_id` 字段(`String(50)`, indexed),无需数据库迁移。
|
||
|
||
## Architecture
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph Frontend["前端 (dashboard.js)"]
|
||
TLV[Tenant_List_View<br/>租户卡片列表]
|
||
TDV[Tenant_Detail_View<br/>租户知识条目列表]
|
||
Stats[统计面板<br/>全局/租户统计切换]
|
||
Breadcrumb[面包屑导航]
|
||
end
|
||
|
||
subgraph API["Flask Blueprint (knowledge_bp)"]
|
||
EP1["GET /api/knowledge/tenants"]
|
||
EP2["GET /api/knowledge?tenant_id=X"]
|
||
EP3["GET /api/knowledge/stats?tenant_id=X"]
|
||
EP4["GET /api/knowledge/search?q=...&tenant_id=X"]
|
||
EP5["POST /api/knowledge (含 tenant_id)"]
|
||
end
|
||
|
||
subgraph Service["KnowledgeManager"]
|
||
M1[get_tenant_summary]
|
||
M2[get_knowledge_paginated<br/>+tenant_id filter]
|
||
M3[get_knowledge_stats<br/>+tenant_id filter]
|
||
M4[search_knowledge<br/>+tenant_id filter]
|
||
M5[add_knowledge_entry<br/>+tenant_id param]
|
||
end
|
||
|
||
subgraph DB["SQLAlchemy"]
|
||
KE[KnowledgeEntry<br/>tenant_id indexed]
|
||
end
|
||
|
||
TLV -->|点击租户卡片| TDV
|
||
TDV -->|面包屑返回| TLV
|
||
TLV --> EP1
|
||
TDV --> EP2
|
||
TDV --> EP4
|
||
Stats --> EP3
|
||
TDV --> EP5
|
||
|
||
EP1 --> M1
|
||
EP2 --> M2
|
||
EP3 --> M3
|
||
EP4 --> M4
|
||
EP5 --> M5
|
||
|
||
M1 --> KE
|
||
M2 --> KE
|
||
M3 --> KE
|
||
M4 --> KE
|
||
M5 --> KE
|
||
```
|
||
|
||
### 设计决策
|
||
|
||
- **不引入新模型/表**:`tenant_id` 已存在于 `KnowledgeEntry`,聚合查询通过 `GROUP BY` 实现,无需额外的 Tenant 表。
|
||
- **视图状态管理在前端**:使用 JS 变量 `currentTenantId` 控制当前视图层级,避免引入前端路由框架。
|
||
- **统计面板复用**:同一个统计面板根据 `currentTenantId` 是否为 `null` 决定请求全局或租户级统计。
|
||
- **搜索范围自动限定**:当处于 `Tenant_Detail_View` 时,搜索请求自动附加 `tenant_id` 参数。
|
||
|
||
## Components and Interfaces
|
||
|
||
### 1. KnowledgeManager 新增/修改方法
|
||
|
||
```python
|
||
# 新增方法
|
||
def get_tenant_summary(self) -> List[Dict[str, Any]]:
|
||
"""
|
||
按 tenant_id 聚合活跃知识条目,返回租户汇总列表。
|
||
返回格式: [
|
||
{
|
||
"tenant_id": "market_a",
|
||
"entry_count": 42,
|
||
"verified_count": 30,
|
||
"category_distribution": {"FAQ": 20, "故障排查": 22}
|
||
}, ...
|
||
]
|
||
按 entry_count 降序排列。
|
||
"""
|
||
|
||
# 修改方法签名
|
||
def get_knowledge_paginated(
|
||
self, page=1, per_page=10,
|
||
category_filter='', verified_filter='',
|
||
tenant_id: Optional[str] = None # 新增
|
||
) -> Dict[str, Any]
|
||
|
||
def search_knowledge(
|
||
self, query: str, top_k=3,
|
||
verified_only=True,
|
||
tenant_id: Optional[str] = None # 新增
|
||
) -> List[Dict[str, Any]]
|
||
|
||
def get_knowledge_stats(
|
||
self,
|
||
tenant_id: Optional[str] = None # 新增
|
||
) -> Dict[str, Any]
|
||
|
||
def add_knowledge_entry(
|
||
self, question, answer, category,
|
||
confidence_score=0.5, is_verified=False,
|
||
tenant_id: Optional[str] = None # 新增,默认取 config
|
||
) -> bool
|
||
```
|
||
|
||
### 2. Knowledge API 新增/修改端点
|
||
|
||
| 端点 | 方法 | 变更 | 说明 |
|
||
|------|------|------|------|
|
||
| `/api/knowledge/tenants` | GET | 新增 | 返回租户汇总数组 |
|
||
| `/api/knowledge` | GET | 修改 | 增加 `tenant_id` 查询参数 |
|
||
| `/api/knowledge/stats` | GET | 修改 | 增加 `tenant_id` 查询参数 |
|
||
| `/api/knowledge/search` | GET | 修改 | 增加 `tenant_id` 查询参数 |
|
||
| `/api/knowledge` | POST | 修改 | 请求体增加 `tenant_id` 字段 |
|
||
|
||
### 3. 前端组件
|
||
|
||
| 组件 | 职责 |
|
||
|------|------|
|
||
| `loadTenantList()` | 请求 `/api/knowledge/tenants`,渲染租户卡片 |
|
||
| `loadTenantDetail(tenantId, page)` | 请求 `/api/knowledge?tenant_id=X`,渲染知识条目列表 |
|
||
| `renderBreadcrumb(tenantId)` | 渲染面包屑 "知识库 > {tenant_id}" |
|
||
| `loadKnowledgeStats(tenantId)` | 根据 tenantId 是否为 null 请求全局/租户统计 |
|
||
| `searchKnowledge()` | 搜索时自动附加 `currentTenantId` |
|
||
|
||
## Data Models
|
||
|
||
### KnowledgeEntry(现有,无变更)
|
||
|
||
```python
|
||
class KnowledgeEntry(Base):
|
||
__tablename__ = "knowledge_entries"
|
||
id = Column(Integer, primary_key=True)
|
||
tenant_id = Column(String(50), nullable=False, default="default", index=True)
|
||
question = Column(Text, nullable=False)
|
||
answer = Column(Text, nullable=False)
|
||
category = Column(String(100), nullable=False)
|
||
confidence_score = Column(Float, default=0.0)
|
||
usage_count = Column(Integer, default=0)
|
||
created_at = Column(DateTime, default=datetime.now)
|
||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||
is_active = Column(Boolean, default=True)
|
||
is_verified = Column(Boolean, default=False)
|
||
verified_by = Column(String(100))
|
||
verified_at = Column(DateTime)
|
||
vector_embedding = Column(Text)
|
||
search_frequency = Column(Integer, default=0)
|
||
last_accessed = Column(DateTime)
|
||
relevance_score = Column(Float)
|
||
```
|
||
|
||
### Tenant Summary(API 响应结构,非持久化)
|
||
|
||
```json
|
||
{
|
||
"tenant_id": "market_a",
|
||
"entry_count": 42,
|
||
"verified_count": 30,
|
||
"category_distribution": {
|
||
"FAQ": 20,
|
||
"故障排查": 22
|
||
}
|
||
}
|
||
```
|
||
|
||
### Stats 响应结构(扩展)
|
||
|
||
```json
|
||
{
|
||
"total_entries": 100,
|
||
"active_entries": 80,
|
||
"category_distribution": {"FAQ": 40, "故障排查": 60},
|
||
"average_confidence": 0.85,
|
||
"tenant_id": "market_a" // 新增,仅当按租户筛选时返回
|
||
}
|
||
```
|
||
|
||
|
||
## Correctness Properties
|
||
|
||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
||
### Property 1: Tenant summary correctly aggregates active entries
|
||
|
||
*For any* set of `KnowledgeEntry` records with mixed `is_active` and `tenant_id` values, calling `get_tenant_summary()` should return a list where each element's `entry_count` equals the number of active entries for that `tenant_id`, each `verified_count` equals the number of active+verified entries for that `tenant_id`, and each `category_distribution` correctly reflects the category counts of active entries for that `tenant_id`.
|
||
|
||
**Validates: Requirements 1.1, 1.2**
|
||
|
||
### Property 2: Tenant summary sorted by entry_count descending
|
||
|
||
*For any* result returned by `get_tenant_summary()`, the list should be sorted such that for every consecutive pair of elements `(a, b)`, `a.entry_count >= b.entry_count`.
|
||
|
||
**Validates: Requirements 1.3**
|
||
|
||
### Property 3: Knowledge entry filtering by tenant, category, and verified status
|
||
|
||
*For any* combination of `tenant_id`, `category_filter`, and `verified_filter` parameters, all entries returned by `get_knowledge_paginated()` should satisfy all specified filter conditions simultaneously. Specifically: every returned entry's `tenant_id` matches the requested `tenant_id`, every returned entry's `category` matches `category_filter` (if provided), and every returned entry's `is_verified` matches `verified_filter` (if provided).
|
||
|
||
**Validates: Requirements 2.1, 2.3**
|
||
|
||
### Property 4: Pagination consistency with tenant filter
|
||
|
||
*For any* `tenant_id` and valid `page`/`per_page` values, the entries returned by `get_knowledge_paginated(tenant_id=X, page=P, per_page=N)` should be a correct slice of the full filtered result set. The `total` field should equal the count of all matching entries, `total_pages` should equal `ceil(total / per_page)`, and the number of returned entries should equal `min(per_page, total - (page-1)*per_page)` when `page <= total_pages`.
|
||
|
||
**Validates: Requirements 2.2**
|
||
|
||
### Property 5: New entry tenant association
|
||
|
||
*For any* valid `tenant_id` and valid entry data (question, answer, category), calling `add_knowledge_entry(tenant_id=X, ...)` should result in the newly created `KnowledgeEntry` record having `tenant_id == X`. If `tenant_id` is not provided, it should default to the configured `get_config().server.tenant_id`.
|
||
|
||
**Validates: Requirements 5.2**
|
||
|
||
### Property 6: Search results scoped to tenant
|
||
|
||
*For any* search query and `tenant_id`, all results returned by `search_knowledge(query=Q, tenant_id=X)` should have `tenant_id == X`. The result set should be a subset of what `search_knowledge(query=Q)` returns (without tenant filter).
|
||
|
||
**Validates: Requirements 6.2**
|
||
|
||
### Property 7: Stats scoped to tenant
|
||
|
||
*For any* `tenant_id`, the statistics returned by `get_knowledge_stats(tenant_id=X)` should reflect only entries with `tenant_id == X`. Specifically, `total_entries` should equal the count of active entries for that tenant, and `average_confidence` should equal the mean confidence of those entries. When `tenant_id` is omitted, the stats should aggregate across all tenants.
|
||
|
||
**Validates: Requirements 7.3, 7.4**
|
||
|
||
## Error Handling
|
||
|
||
### API 层错误处理
|
||
|
||
所有 API 端点已使用 `@handle_api_errors` 装饰器,该装饰器捕获以下异常:
|
||
|
||
| 异常类型 | HTTP 状态码 | 说明 |
|
||
|----------|------------|------|
|
||
| `ValueError` | 400 | 参数校验失败(如 `page < 1`) |
|
||
| `PermissionError` | 403 | 权限不足 |
|
||
| `Exception` | 500 | 数据库查询失败等未预期错误 |
|
||
|
||
### 业务逻辑层错误处理
|
||
|
||
- `get_tenant_summary()` — 数据库异常时返回空列表 `[]`,记录 error 日志。
|
||
- `get_knowledge_paginated()` — 异常时返回空结构 `{"knowledge": [], "total": 0, ...}`(现有行为保持不变)。
|
||
- `get_knowledge_stats()` — 异常时返回空字典 `{}`(现有行为保持不变)。
|
||
- `add_knowledge_entry()` — 异常时返回 `False`,记录 error 日志。
|
||
|
||
### 前端错误处理
|
||
|
||
- API 请求失败时通过 `showNotification(message, 'error')` 展示错误提示。
|
||
- 网络超时或断连时显示通用错误消息。
|
||
- 批量操作部分失败时显示成功/失败计数。
|
||
|
||
## Testing Strategy
|
||
|
||
### 测试框架
|
||
|
||
- **单元测试**: `pytest`
|
||
- **属性测试**: `hypothesis`(Python property-based testing 库)
|
||
- **每个属性测试最少运行 100 次迭代**
|
||
|
||
### 属性测试(Property-Based Tests)
|
||
|
||
每个 Correctness Property 对应一个属性测试,使用 `hypothesis` 的 `@given` 装饰器生成随机输入。
|
||
|
||
测试标签格式: `Feature: knowledge-tenant-view, Property {number}: {property_text}`
|
||
|
||
| Property | 测试描述 | 生成策略 |
|
||
|----------|---------|---------|
|
||
| Property 1 | 生成随机 KnowledgeEntry 列表(混合 tenant_id、is_active),验证 `get_tenant_summary()` 聚合正确性 | `st.lists(st.builds(KnowledgeEntry, tenant_id=st.sampled_from([...]), is_active=st.booleans()))` |
|
||
| Property 2 | 验证 `get_tenant_summary()` 返回列表按 entry_count 降序 | 复用 Property 1 的生成策略 |
|
||
| Property 3 | 生成随机 tenant_id + category + verified 组合,验证过滤结果一致性 | `st.sampled_from(tenant_ids)`, `st.sampled_from(categories)`, `st.sampled_from(['true','false',''])` |
|
||
| Property 4 | 生成随机 page/per_page,验证分页切片正确性 | `st.integers(min_value=1, max_value=10)` for page/per_page |
|
||
| Property 5 | 生成随机 tenant_id 和条目数据,验证新建条目的 tenant_id | `st.text(min_size=1, max_size=50)` for tenant_id |
|
||
| Property 6 | 生成随机搜索词和 tenant_id,验证搜索结果范围 | `st.text()` for query, `st.sampled_from(tenant_ids)` |
|
||
| Property 7 | 生成随机 tenant_id,验证统计数据与手动聚合一致 | `st.sampled_from(tenant_ids)` + `st.none()` |
|
||
|
||
### 单元测试(Unit Tests)
|
||
|
||
单元测试聚焦于边界情况和具体示例:
|
||
|
||
- **边界**: 无活跃条目时 `get_tenant_summary()` 返回空数组
|
||
- **边界**: 不存在的 `tenant_id` 查询返回空列表 + `total=0`
|
||
- **示例**: 数据库异常时 API 返回 500
|
||
- **示例**: `add_knowledge_entry` 不传 `tenant_id` 时使用配置默认值
|
||
- **集成**: 前端 `loadTenantList()` → API → Manager 完整链路
|
||
|
||
### 测试配置
|
||
|
||
```python
|
||
from hypothesis import settings
|
||
|
||
@settings(max_examples=100)
|
||
```
|
||
|
||
每个属性测试函数头部添加注释引用设计文档中的 Property 编号,例如:
|
||
|
||
```python
|
||
# Feature: knowledge-tenant-view, Property 1: Tenant summary correctly aggregates active entries
|
||
@given(entries=st.lists(knowledge_entry_strategy(), min_size=0, max_size=50))
|
||
def test_tenant_summary_aggregation(entries):
|
||
...
|
||
```
|