Files
assist/.kiro/specs/conversation-tenant-view/design.md

320 lines
14 KiB
Markdown
Raw Normal View History

# Design Document: 对话历史租户分组展示 (conversation-tenant-view)
## Overview
本设计将对话历史页面从扁平会话列表改造为两层结构:第一层按 `tenant_id` 分组展示租户汇总卡片(会话总数、消息总数、活跃会话数、最近活跃时间),第二层展示某租户下的具体会话列表。点击会话仍可查看消息详情(保留现有第三层功能)。改造涉及三个层面:
1. **后端 API 层** — 在 `conversations_bp` 中新增租户汇总端点 `GET /api/conversations/tenants`,并为现有 `/api/conversations/sessions``/api/conversations/analytics` 端点增加 `tenant_id` 查询参数支持。
2. **业务逻辑层** — 在 `ConversationHistoryManager` 中新增 `get_tenant_summary()` 方法,并为 `get_sessions_paginated()``get_conversation_analytics()` 方法增加 `tenant_id` 过滤参数。
3. **前端展示层** — 在 `dashboard.js` 中实现 `Tenant_List_View``Tenant_Detail_View` 两个视图状态的切换逻辑,包括面包屑导航、统计面板上下文切换、搜索范围限定。
数据模型 `ChatSession``Conversation` 已有 `tenant_id` 字段(`String(50)`, indexed无需数据库迁移。
交互模式与知识库租户分组视图knowledge-tenant-view保持一致。
## Architecture
```mermaid
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/&lt;id&gt;"]
EP5["DELETE /api/conversations/sessions/&lt;id&gt;"]
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 新增/修改方法
```python
# 新增方法
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现有无变更
```python
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现有无变更
```python
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 SummaryAPI 响应结构,非持久化)
```json
{
"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 完整链路
### 测试配置
```python
from hypothesis import settings
@settings(max_examples=100)
```
每个属性测试函数头部添加注释引用设计文档中的 Property 编号,例如:
```python
# 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):
...
```