Files
assist/.kiro/specs/conversation-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

14 KiB
Raw Blame 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_ViewTenant_Detail_View 两个视图状态的切换逻辑,包括面包屑导航、统计面板上下文切换、搜索范围限定。

数据模型 ChatSessionConversation 已有 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/&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 已存在于 ChatSessionConversation,聚合查询通过 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 SummaryAPI 响应结构,非持久化)

{
    "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

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
  • 属性测试: hypothesisPython 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):
    ...