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

13 KiB
Raw Blame History

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_ViewTenant_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 SummaryAPI 响应结构,非持久化)

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