Files
assist/.kiro/specs/knowledge-tenant-view/design.md
Jeason 7013e9db70 feat: 对话历史页面租户分组展示功能
- 新增 ConversationHistoryManager.get_tenant_summary() 按租户聚合会话统计
- get_sessions_paginated() 和 get_conversation_analytics() 增加 tenant_id 过滤
- 新增 GET /api/conversations/tenants 租户汇总端点
- sessions 和 analytics API 端点支持 tenant_id 查询参数
- 前端实现租户卡片列表视图和租户详情会话表格视图
- 实现面包屑导航、搜索范围限定、统计面板上下文切换
- 会话删除后自动检测空租户并返回列表视图
- dashboard.html 添加租户视图 DOM 容器
- 交互模式与知识库租户分组视图保持一致
2026-04-01 16:11:02 +08:00

311 lines
13 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.
# 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 SummaryAPI 响应结构,非持久化)
```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):
...
```