- 新增 ConversationHistoryManager.get_tenant_summary() 按租户聚合会话统计 - get_sessions_paginated() 和 get_conversation_analytics() 增加 tenant_id 过滤 - 新增 GET /api/conversations/tenants 租户汇总端点 - sessions 和 analytics API 端点支持 tenant_id 查询参数 - 前端实现租户卡片列表视图和租户详情会话表格视图 - 实现面包屑导航、搜索范围限定、统计面板上下文切换 - 会话删除后自动检测空租户并返回列表视图 - dashboard.html 添加租户视图 DOM 容器 - 交互模式与知识库租户分组视图保持一致
14 KiB
Design Document: 对话历史租户分组展示 (conversation-tenant-view)
Overview
本设计将对话历史页面从扁平会话列表改造为两层结构:第一层按 tenant_id 分组展示租户汇总卡片(会话总数、消息总数、活跃会话数、最近活跃时间),第二层展示某租户下的具体会话列表。点击会话仍可查看消息详情(保留现有第三层功能)。改造涉及三个层面:
- 后端 API 层 — 在
conversations_bp中新增租户汇总端点GET /api/conversations/tenants,并为现有/api/conversations/sessions和/api/conversations/analytics端点增加tenant_id查询参数支持。 - 业务逻辑层 — 在
ConversationHistoryManager中新增get_tenant_summary()方法,并为get_sessions_paginated()、get_conversation_analytics()方法增加tenant_id过滤参数。 - 前端展示层 — 在
dashboard.js中实现Tenant_List_View和Tenant_Detail_View两个视图状态的切换逻辑,包括面包屑导航、统计面板上下文切换、搜索范围限定。
数据模型 ChatSession 和 Conversation 已有 tenant_id 字段(String(50), indexed),无需数据库迁移。
交互模式与知识库租户分组视图(knowledge-tenant-view)保持一致。
Architecture
graph TD
subgraph Frontend["前端 (dashboard.js)"]
TLV[Tenant_List_View<br/>租户卡片列表]
TDV[Tenant_Detail_View<br/>租户会话列表]
MDV[Message_Detail_View<br/>会话消息详情]
Stats[统计面板<br/>全局/租户统计切换]
Breadcrumb[面包屑导航]
end
subgraph API["Flask Blueprint (conversations_bp)"]
EP1["GET /api/conversations/tenants"]
EP2["GET /api/conversations/sessions?tenant_id=X"]
EP3["GET /api/conversations/analytics?tenant_id=X"]
EP4["GET /api/conversations/sessions/<id>"]
EP5["DELETE /api/conversations/sessions/<id>"]
end
subgraph Service["ConversationHistoryManager"]
M1[get_tenant_summary]
M2[get_sessions_paginated<br/>+tenant_id filter]
M3[get_conversation_analytics<br/>+tenant_id filter]
M4[get_session_messages]
M5[delete_session]
end
subgraph DB["SQLAlchemy"]
CS[ChatSession<br/>tenant_id indexed]
CV[Conversation<br/>tenant_id indexed]
end
TLV -->|点击租户卡片| TDV
TDV -->|点击会话行| MDV
TDV -->|面包屑返回| TLV
MDV -->|面包屑返回| TDV
TLV --> EP1
TDV --> EP2
Stats --> EP3
MDV --> EP4
TDV --> EP5
EP1 --> M1
EP2 --> M2
EP3 --> M3
EP4 --> M4
EP5 --> M5
M1 --> CS
M2 --> CS
M3 --> CS & CV
M4 --> CV
M5 --> CS & CV
设计决策
- 不引入新模型/表:
tenant_id已存在于ChatSession和Conversation,聚合查询通过GROUP BY实现,无需额外的 Tenant 表。 - 视图状态管理在前端:使用 JS 变量
conversationCurrentTenantId控制当前视图层级,避免引入前端路由框架。与 knowledge-tenant-view 的currentTenantId模式一致。 - 统计面板复用:同一个统计面板根据
conversationCurrentTenantId是否为null决定请求全局或租户级统计。 - 搜索范围自动限定:当处于
Tenant_Detail_View时,搜索请求自动附加tenant_id参数。 - 复用现有删除逻辑:
delete_session()已实现删除会话及关联消息,无需修改。
Components and Interfaces
1. ConversationHistoryManager 新增/修改方法
# 新增方法
def get_tenant_summary(self) -> List[Dict[str, Any]]:
"""
按 tenant_id 聚合 ChatSession,返回租户汇总列表。
返回格式: [
{
"tenant_id": "market_a",
"session_count": 15,
"message_count": 230,
"active_session_count": 5,
"last_active_time": "2026-03-20T10:30:00"
}, ...
]
按 last_active_time 降序排列。
"""
# 修改方法签名
def get_sessions_paginated(
self,
page: int = 1,
per_page: int = 20,
status: Optional[str] = None,
search: str = '',
date_filter: str = '',
tenant_id: Optional[str] = None # 新增
) -> Dict[str, Any]
def get_conversation_analytics(
self,
work_order_id: Optional[int] = None,
days: int = 7,
tenant_id: Optional[str] = None # 新增
) -> Dict[str, Any]
2. Conversations API 新增/修改端点
| 端点 | 方法 | 变更 | 说明 |
|---|---|---|---|
/api/conversations/tenants |
GET | 新增 | 返回租户汇总数组 |
/api/conversations/sessions |
GET | 修改 | 增加 tenant_id 查询参数 |
/api/conversations/analytics |
GET | 修改 | 增加 tenant_id 查询参数 |
现有端点保持不变:
GET /api/conversations/sessions/<session_id>— 获取会话消息详情DELETE /api/conversations/sessions/<session_id>— 删除会话
3. 前端组件
| 组件/函数 | 职责 |
|---|---|
loadConversationTenantList() |
请求 /api/conversations/tenants,渲染租户卡片 |
loadConversationTenantDetail(tenantId, page) |
请求 /api/conversations/sessions?tenant_id=X,渲染会话列表 |
renderConversationBreadcrumb(tenantId, sessionTitle) |
渲染面包屑 "对话历史 > {tenant_id}" 或 "对话历史 > {tenant_id} > {session_title}" |
loadConversationStats(tenantId) |
根据 tenantId 是否为 null 请求全局/租户统计 |
searchConversationSessions() |
搜索时自动附加 conversationCurrentTenantId |
Data Models
ChatSession(现有,无变更)
class ChatSession(Base):
__tablename__ = "chat_sessions"
id = Column(Integer, primary_key=True)
tenant_id = Column(String(50), nullable=False, default="default", index=True)
session_id = Column(String(100), unique=True, nullable=False)
user_id = Column(String(100), nullable=True)
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=True)
title = Column(String(200), nullable=True)
status = Column(String(20), default="active") # active, ended
message_count = Column(Integer, default=0)
source = Column(String(50), nullable=True)
ip_address = Column(String(45), nullable=True)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
ended_at = Column(DateTime, nullable=True)
Conversation(现有,无变更)
class Conversation(Base):
__tablename__ = "conversations"
id = Column(Integer, primary_key=True)
tenant_id = Column(String(50), nullable=False, default="default", index=True)
session_id = Column(String(100), ForeignKey("chat_sessions.session_id"), nullable=True)
work_order_id = Column(Integer, ForeignKey("work_orders.id"))
user_message = Column(Text, nullable=False)
assistant_response = Column(Text, nullable=False)
timestamp = Column(DateTime, default=datetime.now)
confidence_score = Column(Float)
response_time = Column(Float)
# ... 其他字段
Tenant Summary(API 响应结构,非持久化)
{
"tenant_id": "market_a",
"session_count": 15,
"message_count": 230,
"active_session_count": 5,
"last_active_time": "2026-03-20T10:30:00"
}
Analytics 响应结构(扩展)
现有 analytics 响应增加 tenant_id 字段(仅当按租户筛选时返回),其余结构不变。
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 aggregation correctness
For any set of ChatSession records with mixed tenant_id, status, and message_count values, calling get_tenant_summary() should return a list where each element's session_count equals the number of ChatSession records for that tenant_id, each message_count equals the sum of message_count fields for that tenant_id, and each active_session_count equals the count of ChatSession records with status == 'active' for that tenant_id.
Validates: Requirements 1.1, 1.2, 1.3, 1.4
Property 2: Tenant summary sorted by last_active_time 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.last_active_time >= b.last_active_time.
Validates: Requirements 1.5
Property 3: Session filtering by tenant, status, and search
For any combination of tenant_id, status, and search parameters, all sessions returned by get_sessions_paginated() should satisfy all specified filter conditions simultaneously. Specifically: every returned session's tenant_id matches the requested tenant_id, every returned session's status matches the status filter (if provided), and every returned session's title or session_id contains the search string (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 sessions returned by get_sessions_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 sessions, total_pages should equal ceil(total / per_page), and the number of returned sessions should equal min(per_page, total - (page-1)*per_page) when page <= total_pages.
Validates: Requirements 2.2
Property 5: Session deletion removes session and all associated messages
For any ChatSession and its associated Conversation records, after calling delete_session(session_id), querying for the ChatSession by session_id should return no results, and querying for Conversation records with that session_id should also return no results.
Validates: Requirements 6.2
Property 6: Search results scoped to tenant
For any search query and tenant_id, all sessions returned by get_sessions_paginated(search=Q, tenant_id=X) should have tenant_id == X. The result set should be a subset of what get_sessions_paginated(search=Q) returns (without tenant filter).
Validates: Requirements 7.1, 7.2
Property 7: Analytics scoped to tenant
For any tenant_id, the analytics returned by get_conversation_analytics(tenant_id=X) should reflect only ChatSession and Conversation records with tenant_id == X. When tenant_id is omitted, the analytics should aggregate across all tenants. Specifically, the conversation total count with a tenant filter should be less than or equal to the global total count.
Validates: Requirements 8.3, 8.4
Error Handling
API 层错误处理
所有 API 端点使用 try/except 包裹,捕获异常后返回统一错误格式:
| 异常场景 | HTTP 状态码 | 响应格式 |
|---|---|---|
参数校验失败(如 page < 1) |
400 | {"error": "描述信息"} |
| 数据库查询异常 | 500 | {"error": "描述信息"} |
| 正常但无数据 | 200 | 空数组 [] 或 {"sessions": [], "total": 0} |
业务逻辑层错误处理
get_tenant_summary()— 数据库异常时返回空列表[],记录 error 日志。get_sessions_paginated()— 异常时返回空结构{"sessions": [], "total": 0, ...}(现有行为保持不变)。get_conversation_analytics()— 异常时返回空字典{}(现有行为保持不变)。delete_session()— 异常时返回False,记录 error 日志(现有行为保持不变)。
前端错误处理
- API 请求失败时通过
showNotification(message, 'error')展示错误提示。 - 网络超时或断连时显示通用错误消息。
- 删除操作失败时显示具体失败原因。
Testing Strategy
测试框架
- 单元测试:
pytest - 属性测试:
hypothesis(Python property-based testing 库) - 每个属性测试最少运行 100 次迭代
属性测试(Property-Based Tests)
每个 Correctness Property 对应一个属性测试,使用 hypothesis 的 @given 装饰器生成随机输入。
测试标签格式: Feature: conversation-tenant-view, Property {number}: {property_text}
| Property | 测试描述 | 生成策略 |
|---|---|---|
| Property 1 | 生成随机 ChatSession 列表(混合 tenant_id、status、message_count),验证 get_tenant_summary() 聚合正确性 |
st.lists(st.builds(ChatSession, tenant_id=st.sampled_from([...]), status=st.sampled_from(['active','ended']), message_count=st.integers(min_value=0, max_value=100))) |
| Property 2 | 验证 get_tenant_summary() 返回列表按 last_active_time 降序 |
复用 Property 1 的生成策略 |
| Property 3 | 生成随机 tenant_id + status + search 组合,验证过滤结果一致性 | st.sampled_from(tenant_ids), st.sampled_from(['active','ended','']), st.text(min_size=0, max_size=20) |
| Property 4 | 生成随机 page/per_page,验证分页切片正确性 | st.integers(min_value=1, max_value=10) for page/per_page |
| Property 5 | 创建随机会话及关联消息,删除后验证两者均不存在 | st.text(min_size=1, max_size=50) for session_id, st.integers(min_value=1, max_value=10) for message count |
| Property 6 | 生成随机搜索词和 tenant_id,验证搜索结果范围 | st.text() for query, st.sampled_from(tenant_ids) |
| Property 7 | 生成随机 tenant_id,验证 analytics 数据与手动聚合一致 | st.sampled_from(tenant_ids) + st.none() |
单元测试(Unit Tests)
单元测试聚焦于边界情况和具体示例:
- 边界: 无 ChatSession 记录时
get_tenant_summary()返回空数组 - 边界: 不存在的
tenant_id查询返回空列表 +total=0 - 示例: 数据库异常时 API 返回 500
- 示例: 删除最后一个会话后租户从汇总中消失
- 集成: 前端
loadConversationTenantList()→ API → Manager 完整链路
测试配置
from hypothesis import settings
@settings(max_examples=100)
每个属性测试函数头部添加注释引用设计文档中的 Property 编号,例如:
# Feature: conversation-tenant-view, Property 1: Tenant summary aggregation correctness
@given(sessions=st.lists(chat_session_strategy(), min_size=0, max_size=50))
def test_tenant_summary_aggregation(sessions):
...