- 新增 ConversationHistoryManager.get_tenant_summary() 按租户聚合会话统计 - get_sessions_paginated() 和 get_conversation_analytics() 增加 tenant_id 过滤 - 新增 GET /api/conversations/tenants 租户汇总端点 - sessions 和 analytics API 端点支持 tenant_id 查询参数 - 前端实现租户卡片列表视图和租户详情会话表格视图 - 实现面包屑导航、搜索范围限定、统计面板上下文切换 - 会话删除后自动检测空租户并返回列表视图 - dashboard.html 添加租户视图 DOM 容器 - 交互模式与知识库租户分组视图保持一致
13 KiB
Design Document: 知识库租户分组展示 (knowledge-tenant-view)
Overview
本设计将知识库管理页面从扁平列表改造为两层结构:第一层按 tenant_id 分组展示租户汇总卡片,第二层展示某租户下的知识条目列表。改造涉及三个层面:
- 后端 API 层 — 在
knowledge_bp中新增租户汇总端点/api/knowledge/tenants,并为现有/api/knowledge和/api/knowledge/stats端点增加tenant_id查询参数支持。 - 业务逻辑层 — 在
KnowledgeManager中新增get_tenant_summary()方法,并为get_knowledge_paginated()、search_knowledge()、get_knowledge_stats()方法增加tenant_id过滤参数。add_knowledge_entry()方法也需接受tenant_id参数。 - 前端展示层 — 在
dashboard.js中实现Tenant_List_View和Tenant_Detail_View两个视图状态的切换逻辑,包括面包屑导航、统计面板上下文切换、搜索范围限定。
数据模型 KnowledgeEntry 已有 tenant_id 字段(String(50), indexed),无需数据库迁移。
Architecture
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 新增/修改方法
# 新增方法
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(现有,无变更)
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 响应结构,非持久化)
{
"tenant_id": "market_a",
"entry_count": 42,
"verified_count": 30,
"category_distribution": {
"FAQ": 20,
"故障排查": 22
}
}
Stats 响应结构(扩展)
{
"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 完整链路
测试配置
from hypothesis import settings
@settings(max_examples=100)
每个属性测试函数头部添加注释引用设计文档中的 Property 编号,例如:
# 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):
...