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 容器
- 交互模式与知识库租户分组视图保持一致
This commit is contained in:
2026-04-01 16:11:02 +08:00
parent e14e3ee7a5
commit 7013e9db70
27 changed files with 2753 additions and 276 deletions

View File

@@ -0,0 +1 @@
{"specId": "b7e3c1a2-5f84-4d9e-a1b3-8c6d2e4f7a90", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,319 @@
# 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):
...
```

View File

@@ -0,0 +1,116 @@
# Requirements Document
## Introduction
对话历史租户分组展示功能。当前对话历史页面以扁平的会话列表展示所有 `ChatSession` 记录,缺乏租户(市场)维度的组织结构。本功能将对话历史页面改造为两层结构:第一层按租户分组展示汇总信息(会话总数、消息总数、最近活跃时间等),第二层展示某个租户下的具体会话列表。点击具体会话仍可查看消息详情(保留现有功能)。交互模式与知识库租户分组视图保持一致,包括卡片视图、面包屑导航、搜索范围限定和统计面板上下文切换。
## Glossary
- **Dashboard**: Flask + Jinja2 + Bootstrap 5 构建的 Web 管理后台主页面(`dashboard.html`
- **Conversation_Tab**: Dashboard 中 `#conversation-history-tab` 区域,用于展示和管理对话历史
- **Conversation_API**: Flask Blueprint `conversations_bp`,提供对话相关的 REST API`/api/conversations/*`
- **History_Manager**: `ConversationHistoryManager` 类,封装对话历史的数据库查询与业务逻辑
- **Tenant**: 租户,即市场标识(如 `market_a``market_b`),通过 `ChatSession.tenant_id` 字段区分
- **Tenant_Summary**: 租户汇总信息,包含租户 ID、会话总数、消息总数、活跃会话数、最近活跃时间等聚合数据
- **Tenant_List_View**: 第一层视图,以卡片形式展示所有租户的对话汇总信息
- **Tenant_Detail_View**: 第二层视图,展示某个租户下的具体会话列表(含分页、筛选)
- **ChatSession**: SQLAlchemy 数据模型,包含 `tenant_id``session_id``title``status``message_count``source``created_at``updated_at` 等字段
- **Conversation**: SQLAlchemy 数据模型,表示单条对话消息,包含 `tenant_id``session_id``user_message``assistant_response` 等字段
## Requirements
### Requirement 1: 租户汇总 API
**User Story:** 作为管理员,我希望后端提供按租户分组的对话会话汇总接口,以便前端展示每个租户的对话统计。
#### Acceptance Criteria
1. WHEN a GET request is sent to `/api/conversations/tenants`, THE Conversation_API SHALL return a JSON array of Tenant_Summary objects, each containing `tenant_id`, `session_count`, `message_count`, `active_session_count`, and `last_active_time`
2. THE Conversation_API SHALL compute `session_count` by counting all ChatSession records for each Tenant
3. THE Conversation_API SHALL compute `message_count` by summing the `message_count` field of all ChatSession records for each Tenant
4. THE Conversation_API SHALL compute `active_session_count` by counting ChatSession records with `status == 'active'` for each Tenant
5. THE Conversation_API SHALL sort the Tenant_Summary array by `last_active_time` in descending order
6. WHEN no ChatSession records exist, THE Conversation_API SHALL return an empty JSON array with HTTP status 200
7. IF a database query error occurs, THEN THE Conversation_API SHALL return an error response with HTTP status 500 and a descriptive error message
### Requirement 2: 租户会话列表 API
**User Story:** 作为管理员,我希望后端提供按租户筛选的会话分页接口,以便在点击某个租户后查看该租户下的具体会话列表。
#### Acceptance Criteria
1. WHEN a GET request with query parameter `tenant_id` is sent to `/api/conversations/sessions`, THE Conversation_API SHALL return only the ChatSession records belonging to the specified Tenant
2. THE Conversation_API SHALL support pagination via `page` and `per_page` query parameters when filtering by `tenant_id`
3. THE Conversation_API SHALL support `status` and `search` query parameters for further filtering within a Tenant
4. WHEN the `tenant_id` parameter value does not match any existing ChatSession records, THE Conversation_API SHALL return an empty session list with `total` equal to 0 and HTTP status 200
5. THE History_Manager SHALL accept `tenant_id` as a filter parameter in the `get_sessions_paginated` method and return paginated results scoped to the specified Tenant
### Requirement 3: 租户列表视图(第一层)
**User Story:** 作为管理员,我希望对话历史页面首先展示按租户分组的汇总卡片,以便快速了解各市场的对话活跃度。
#### Acceptance Criteria
1. WHEN the Conversation_Tab is activated, THE Dashboard SHALL display a Tenant_List_View showing one card per Tenant
2. THE Tenant_List_View SHALL display the following information for each Tenant: tenant_id租户名称, session_count会话总数, message_count消息总数, active_session_count活跃会话数, last_active_time最近活跃时间
3. WHEN the Tenant_List_View is loading data, THE Dashboard SHALL display a loading spinner in the Conversation_Tab area
4. WHEN no tenants exist, THE Dashboard SHALL display a placeholder message indicating that no conversation sessions are available
5. THE Tenant_List_View SHALL refresh its data when the user clicks a refresh button
### Requirement 4: 租户详情视图(第二层)
**User Story:** 作为管理员,我希望点击某个租户卡片后能查看该租户下的具体会话列表,以便管理和审查对话内容。
#### Acceptance Criteria
1. WHEN a user clicks on a Tenant card in the Tenant_List_View, THE Dashboard SHALL transition to the Tenant_Detail_View showing ChatSession records for the selected Tenant
2. THE Tenant_Detail_View SHALL display each ChatSession with the following fields: title会话标题, message_count消息数, status状态, source来源, created_at创建时间, updated_at最近更新时间
3. THE Tenant_Detail_View SHALL provide a breadcrumb navigation showing "对话历史 > {tenant_id}" to indicate the current context
4. WHEN the user clicks the breadcrumb "对话历史" link, THE Dashboard SHALL navigate back to the Tenant_List_View
5. THE Tenant_Detail_View SHALL support pagination with configurable page size
6. THE Tenant_Detail_View SHALL support filtering by session status and date range
### Requirement 5: 会话详情查看(第三层保留)
**User Story:** 作为管理员,我希望在租户详情视图中点击某个会话后能查看该会话的消息详情,以便审查具体对话内容。
#### Acceptance Criteria
1. WHEN a user clicks on a ChatSession row in the Tenant_Detail_View, THE Dashboard SHALL display the message detail view showing all Conversation records for the selected ChatSession
2. THE Dashboard SHALL retain the existing message detail display logic and UI layout
3. THE Dashboard SHALL provide a breadcrumb navigation showing "对话历史 > {tenant_id} > {session_title}" in the message detail view
4. WHEN the user clicks the breadcrumb "{tenant_id}" link, THE Dashboard SHALL navigate back to the Tenant_Detail_View for the corresponding Tenant
### Requirement 6: 会话管理操作
**User Story:** 作为管理员,我希望在租户详情视图中能对会话执行删除操作,以便维护对话历史数据。
#### Acceptance Criteria
1. WHILE viewing the Tenant_Detail_View, THE Dashboard SHALL provide a delete button for each ChatSession row
2. WHEN a user deletes a ChatSession in the Tenant_Detail_View, THE Conversation_API SHALL delete the ChatSession and all associated Conversation records
3. WHEN a user deletes a ChatSession, THE Dashboard SHALL refresh the Tenant_Detail_View to reflect the updated data
4. WHEN a user deletes all ChatSession records for a Tenant, THE Dashboard SHALL navigate back to the Tenant_List_View and remove the empty Tenant card
5. IF a ChatSession deletion fails, THEN THE Dashboard SHALL display an error notification with the failure reason
### Requirement 7: 搜索功能适配
**User Story:** 作为管理员,我希望在租户详情视图中搜索会话时,搜索范围限定在当前租户内,以便精确查找。
#### Acceptance Criteria
1. WHILE viewing the Tenant_Detail_View, THE Dashboard SHALL scope the session search to the currently selected Tenant
2. WHEN a search query is submitted in the Tenant_Detail_View, THE Conversation_API SHALL filter search results by the specified `tenant_id`
3. WHEN the search query is cleared, THE Dashboard SHALL restore the full paginated session list for the current Tenant
4. THE History_Manager search method SHALL accept an optional `tenant_id` parameter to limit search scope
### Requirement 8: 统计信息适配
**User Story:** 作为管理员,我希望对话历史统计面板在租户列表视图时展示全局统计,在租户详情视图时展示当前租户的统计,以便获取准确的上下文信息。
#### Acceptance Criteria
1. WHILE the Tenant_List_View is displayed, THE Dashboard SHALL show global conversation statistics (total sessions across all tenants, total messages, total active sessions)
2. WHILE the Tenant_Detail_View is displayed, THE Dashboard SHALL show statistics scoped to the selected Tenant
3. WHEN a GET request with query parameter `tenant_id` is sent to `/api/conversations/analytics`, THE Conversation_API SHALL return analytics data filtered by the specified Tenant
4. WHEN the `tenant_id` parameter is omitted from the analytics request, THE Conversation_API SHALL return global analytics across all tenants

View File

@@ -0,0 +1,142 @@
# Implementation Plan: 对话历史租户分组展示 (conversation-tenant-view)
## Overview
将对话历史页面从扁平会话列表改造为两层结构:第一层按 `tenant_id` 分组展示租户汇总卡片,第二层展示租户下的会话列表。改造涉及 ConversationHistoryManager 业务逻辑层、Flask API 层、前端 dashboard.js 三个层面。交互模式与知识库租户分组视图保持一致。
## Tasks
- [x] 1. ConversationHistoryManager 新增 get_tenant_summary 方法
- [x] 1.1 在 `src/dialogue/conversation_history.py` 中新增 `get_tenant_summary()` 方法
- 使用 SQLAlchemy `GROUP BY ChatSession.tenant_id` 聚合所有 ChatSession 记录
- 计算每个租户的 `session_count`(会话总数)、`message_count`消息总数sum of message_count`active_session_count`status=='active' 的会话数)、`last_active_time`max of updated_at
-`last_active_time` 降序排列
- 数据库异常时返回空列表 `[]`,记录 error 日志
- 无 ChatSession 记录时返回空列表 `[]`
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
- [ ]* 1.2 为 get_tenant_summary 编写属性测试
- **Property 1: Tenant summary aggregation correctness**
- **Property 2: Tenant summary sorted by last_active_time descending**
- 使用 `hypothesis` 生成随机 ChatSession 列表(混合 tenant_id、status、message_count验证聚合正确性和排序
- **Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5**
- [x] 2. ConversationHistoryManager 现有方法增加 tenant_id 过滤
- [x] 2.1 为 `get_sessions_paginated()` 增加 `tenant_id` 可选参数
-`src/dialogue/conversation_history.py` 中修改方法签名,增加 `tenant_id: Optional[str] = None`
-`tenant_id` 不为 None 时,在查询中增加 `ChatSession.tenant_id == tenant_id` 过滤条件
- 返回结构不变,仅过滤范围缩小
- _Requirements: 2.1, 2.2, 2.3, 2.5_
- [ ]* 2.2 为 get_sessions_paginated 的 tenant_id 过滤编写属性测试
- **Property 3: Session filtering by tenant, status, and search**
- **Property 4: Pagination consistency with tenant filter**
- **Validates: Requirements 2.1, 2.2, 2.3**
- [x] 2.3 为 `get_conversation_analytics()` 增加 `tenant_id` 可选参数
-`tenant_id` 不为 None 时,所有统计查询增加 `ChatSession.tenant_id == tenant_id``Conversation.tenant_id == tenant_id` 过滤
- 返回结构不变
- _Requirements: 8.3, 8.4_
- [ ]* 2.4 为 get_conversation_analytics 的 tenant_id 过滤编写属性测试
- **Property 7: Analytics scoped to tenant**
- **Validates: Requirements 8.3, 8.4**
- [x] 3. Checkpoint - 确保后端业务逻辑层完成
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Conversations API 层新增和修改端点
- [x] 4.1 在 `src/web/blueprints/conversations.py` 中新增 `GET /api/conversations/tenants` 端点
- 调用 `history_manager.get_tenant_summary()` 返回租户汇总 JSON 数组
- 使用 try/except 包裹,异常时返回 HTTP 500
- _Requirements: 1.1, 1.5, 1.6, 1.7_
- [x] 4.2 修改 `GET /api/conversations/sessions` 端点,增加 `tenant_id` 查询参数支持
-`request.args` 获取 `tenant_id` 参数,传递给 `history_manager.get_sessions_paginated()`
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 4.3 修改 `GET /api/conversations/analytics` 端点,增加 `tenant_id` 查询参数支持
-`request.args` 获取 `tenant_id` 参数,传递给 `history_manager.get_conversation_analytics()`
- _Requirements: 8.3, 8.4_
- [ ]* 4.4 为新增和修改的 API 端点编写单元测试
- 测试 `/api/conversations/tenants` 返回正确的汇总数据
- 测试各端点的 `tenant_id` 参数过滤行为
- 测试空数据和异常情况
- _Requirements: 1.1, 1.6, 1.7, 2.4_
- [x] 5. Checkpoint - 确保后端 API 层完成
- Ensure all tests pass, ask the user if questions arise.
- [x] 6. 前端 Tenant_List_View租户列表视图
- [x] 6.1 在 `src/web/static/js/dashboard.js` 中实现 `loadConversationTenantList()` 函数
- 请求 `GET /api/conversations/tenants` 获取租户汇总数据
- 渲染租户卡片列表,每张卡片展示 `tenant_id``session_count``message_count``active_session_count``last_active_time`
- 添加加载中 spinner 状态
- 无租户时展示空状态占位提示
- 卡片点击事件绑定,调用 `loadConversationTenantDetail(tenantId)`
- _Requirements: 3.1, 3.2, 3.3, 3.4_
- [x] 6.2 实现刷新按钮功能
- 在对话历史 tab 区域添加刷新按钮,点击时重新调用 `loadConversationTenantList()`
- _Requirements: 3.5_
- [x] 7. 前端 Tenant_Detail_View租户详情视图
- [x] 7.1 实现 `loadConversationTenantDetail(tenantId, page)` 函数
- 请求 `GET /api/conversations/sessions?tenant_id=X&page=P&per_page=N` 获取会话列表
- 渲染会话表格,展示 title、message_count、status、source、created_at、updated_at
- 实现分页控件
- 支持 status 和 date_filter 筛选
- _Requirements: 4.1, 4.2, 4.5, 4.6_
- [x] 7.2 实现面包屑导航 `renderConversationBreadcrumb(tenantId, sessionTitle)`
- 展示 "对话历史 > {tenant_id}" 面包屑(租户详情视图)
- 展示 "对话历史 > {tenant_id} > {session_title}" 面包屑(消息详情视图)
- 点击 "对话历史" 链接时调用 `loadConversationTenantList()` 返回租户列表视图
- 点击 "{tenant_id}" 链接时调用 `loadConversationTenantDetail(tenantId)` 返回租户详情视图
- 管理 `conversationCurrentTenantId` 状态变量控制视图层级
- _Requirements: 4.3, 4.4, 5.3, 5.4_
- [x] 7.3 在 Tenant_Detail_View 中集成会话管理操作
- 每行会话提供删除按钮,调用 `DELETE /api/conversations/sessions/<session_id>`
- 删除成功后刷新当前租户详情视图
- 删除所有会话后自动返回租户列表视图并移除空租户卡片
- 操作失败时通过 `showNotification` 展示错误提示
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [ ]* 7.4 为删除操作编写属性测试
- **Property 5: Session deletion removes session and all associated messages**
- **Validates: Requirements 6.2**
- [x] 8. 前端搜索和统计面板适配
- [x] 8.1 修改搜索功能 `searchConversationSessions()`
- 在 Tenant_Detail_View 中搜索时自动附加 `tenant_id` 参数
- 清空搜索时恢复当前租户的完整分页列表
- _Requirements: 7.1, 7.2, 7.3_
- [ ]* 8.2 为搜索范围限定编写属性测试
- **Property 6: Search results scoped to tenant**
- **Validates: Requirements 7.1, 7.2**
- [x] 8.3 修改 `loadConversationStats(tenantId)` 函数
-`conversationCurrentTenantId` 为 null 时请求全局统计
-`conversationCurrentTenantId` 有值时请求 `GET /api/conversations/analytics?tenant_id=X`
- _Requirements: 8.1, 8.2_
- [x] 9. 前端 HTML 模板更新
- [x] 9.1 在 `src/web/templates/dashboard.html``#conversation-history-tab` 区域添加必要的 DOM 容器
- 添加面包屑容器、租户卡片列表容器、租户详情容器
- 确保与现有 Bootstrap 5 样式一致,与知识库租户视图风格统一
- _Requirements: 3.1, 4.3_
- [x] 10. Final checkpoint - 确保所有功能集成完成
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- 数据模型 `ChatSession``Conversation` 已有 `tenant_id` 字段且已建索引,无需数据库迁移
- 交互模式与知识库租户分组视图 (knowledge-tenant-view) 保持一致

View File

@@ -0,0 +1 @@
{"specId": "0d6981a4-ab44-429e-966d-0874ce82383c", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,310 @@
# 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_View``Tenant_Detail_View` 两个视图状态的切换逻辑,包括面包屑导航、统计面板上下文切换、搜索范围限定。
数据模型 `KnowledgeEntry` 已有 `tenant_id` 字段(`String(50)`, indexed无需数据库迁移。
## Architecture
```mermaid
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 新增/修改方法
```python
# 新增方法
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现有无变更
```python
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 响应结构,非持久化)
```json
{
"tenant_id": "market_a",
"entry_count": 42,
"verified_count": 30,
"category_distribution": {
"FAQ": 20,
"故障排查": 22
}
}
```
### Stats 响应结构(扩展)
```json
{
"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 完整链路
### 测试配置
```python
from hypothesis import settings
@settings(max_examples=100)
```
每个属性测试函数头部添加注释引用设计文档中的 Property 编号,例如:
```python
# 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):
...
```

View File

@@ -0,0 +1,102 @@
# Requirements Document
## Introduction
知识库租户分组展示功能。当前知识库管理页面以扁平列表展示所有知识条目,缺乏租户(市场)维度的组织结构。本功能将知识库页面改造为两层结构:第一层按租户分组展示汇总信息,第二层展示某个租户下的具体知识条目。数据库模型 `KnowledgeEntry` 已有 `tenant_id` 字段,后端需新增按租户聚合的 API前端需实现分组视图与钻取交互。
## Glossary
- **Dashboard**: Flask + Jinja2 + Bootstrap 5 构建的 Web 管理后台主页面(`dashboard.html`
- **Knowledge_Tab**: Dashboard 中 `#knowledge-tab` 区域,用于展示和管理知识库条目
- **Knowledge_API**: Flask Blueprint `knowledge_bp`,提供知识库相关的 REST API`/api/knowledge/*`
- **Knowledge_Manager**: `KnowledgeManager` 类,封装知识库的数据库查询与业务逻辑
- **Tenant**: 租户,即市场标识(如 `market_a``market_b`),通过 `KnowledgeEntry.tenant_id` 字段区分
- **Tenant_Summary**: 租户汇总信息,包含租户 ID、知识条目总数等聚合数据
- **Tenant_List_View**: 第一层视图,以卡片或列表形式展示所有租户的汇总信息
- **Tenant_Detail_View**: 第二层视图,展示某个租户下的具体知识条目列表(含分页、筛选)
- **KnowledgeEntry**: SQLAlchemy 数据模型,包含 `tenant_id``question``answer``category``confidence_score``usage_count``is_verified` 等字段
## Requirements
### Requirement 1: 租户汇总 API
**User Story:** 作为管理员,我希望后端提供按租户分组的知识库汇总接口,以便前端展示每个租户的知识条目统计。
#### Acceptance Criteria
1. WHEN a GET request is sent to `/api/knowledge/tenants`, THE Knowledge_API SHALL return a JSON array of Tenant_Summary objects, each containing `tenant_id`, `entry_count`, `verified_count`, and `category_distribution`
2. THE Knowledge_API SHALL only count active knowledge entries (`is_active == True`) in the Tenant_Summary aggregation
3. THE Knowledge_API SHALL sort the Tenant_Summary array by `entry_count` in descending order
4. WHEN no active knowledge entries exist, THE Knowledge_API SHALL return an empty JSON array with HTTP status 200
5. IF a database query error occurs, THEN THE Knowledge_API SHALL return an error response with HTTP status 500 and a descriptive error message
### Requirement 2: 租户条目列表 API
**User Story:** 作为管理员,我希望后端提供按租户筛选的知识条目分页接口,以便在点击某个租户后查看该租户下的具体知识条目。
#### Acceptance Criteria
1. WHEN a GET request with query parameter `tenant_id` is sent to `/api/knowledge`, THE Knowledge_API SHALL return only the knowledge entries belonging to the specified Tenant
2. THE Knowledge_API SHALL support pagination via `page` and `per_page` query parameters when filtering by `tenant_id`
3. THE Knowledge_API SHALL support `category` and `verified` query parameters for further filtering within a Tenant
4. WHEN the `tenant_id` parameter value does not match any existing entries, THE Knowledge_API SHALL return an empty knowledge list with `total` equal to 0 and HTTP status 200
5. THE Knowledge_Manager SHALL provide a method that accepts `tenant_id` as a filter parameter and returns paginated results
### Requirement 3: 租户列表视图(第一层)
**User Story:** 作为管理员,我希望知识库页面首先展示按租户分组的汇总卡片,以便快速了解各市场的知识库规模。
#### Acceptance Criteria
1. WHEN the Knowledge_Tab is activated, THE Dashboard SHALL display a Tenant_List_View showing one card per Tenant
2. THE Tenant_List_View SHALL display the following information for each Tenant: tenant_id租户名称, entry_count知识条目总数, verified_count已验证条目数
3. WHEN the Tenant_List_View is loading data, THE Dashboard SHALL display a loading spinner in the Knowledge_Tab area
4. WHEN no tenants exist, THE Dashboard SHALL display a placeholder message indicating that no knowledge entries are available
5. THE Tenant_List_View SHALL refresh its data when the user clicks a refresh button
### Requirement 4: 租户详情视图(第二层)
**User Story:** 作为管理员,我希望点击某个租户卡片后能查看该租户下的具体知识条目列表,以便管理和审核知识内容。
#### Acceptance Criteria
1. WHEN a user clicks on a Tenant card in the Tenant_List_View, THE Dashboard SHALL transition to the Tenant_Detail_View showing knowledge entries for the selected Tenant
2. THE Tenant_Detail_View SHALL display each knowledge entry with the following fields: question, answer, category, confidence_score, usage_count, is_verified status
3. THE Tenant_Detail_View SHALL provide a breadcrumb navigation showing "知识库 > {tenant_id}" to indicate the current context
4. WHEN the user clicks the breadcrumb "知识库" link, THE Dashboard SHALL navigate back to the Tenant_List_View
5. THE Tenant_Detail_View SHALL support pagination with configurable page size
6. THE Tenant_Detail_View SHALL support filtering by category and verification status
### Requirement 5: 租户详情视图中的知识条目操作
**User Story:** 作为管理员,我希望在租户详情视图中能对知识条目执行添加、删除、验证等操作,以便维护知识库内容。
#### Acceptance Criteria
1. WHILE viewing the Tenant_Detail_View, THE Dashboard SHALL provide buttons for adding, deleting, verifying, and unverifying knowledge entries
2. WHEN a user adds a new knowledge entry in the Tenant_Detail_View, THE Knowledge_API SHALL associate the new entry with the currently selected Tenant by setting the `tenant_id` field
3. WHEN a user performs a batch operation (batch delete, batch verify, batch unverify) in the Tenant_Detail_View, THE Dashboard SHALL refresh the Tenant_Detail_View to reflect the updated data
4. WHEN a user deletes all entries for a Tenant, THE Dashboard SHALL navigate back to the Tenant_List_View and remove the empty Tenant card
5. IF a knowledge entry operation fails, THEN THE Dashboard SHALL display an error notification with the failure reason
### Requirement 6: 搜索功能适配
**User Story:** 作为管理员,我希望在租户详情视图中搜索知识条目时,搜索范围限定在当前租户内,以便精确查找。
#### Acceptance Criteria
1. WHILE viewing the Tenant_Detail_View, THE Dashboard SHALL scope the knowledge search to the currently selected Tenant
2. WHEN a search query is submitted in the Tenant_Detail_View, THE Knowledge_API SHALL filter search results by the specified `tenant_id`
3. WHEN the search query is cleared, THE Dashboard SHALL restore the full paginated list for the current Tenant
4. THE Knowledge_Manager search method SHALL accept an optional `tenant_id` parameter to limit search scope
### Requirement 7: 统计信息适配
**User Story:** 作为管理员,我希望知识库统计面板在租户列表视图时展示全局统计,在租户详情视图时展示当前租户的统计,以便获取准确的上下文信息。
#### Acceptance Criteria
1. WHILE the Tenant_List_View is displayed, THE Dashboard SHALL show global knowledge statistics (total entries across all tenants, total verified entries, average confidence)
2. WHILE the Tenant_Detail_View is displayed, THE Dashboard SHALL show statistics scoped to the selected Tenant
3. WHEN a GET request with query parameter `tenant_id` is sent to `/api/knowledge/stats`, THE Knowledge_API SHALL return statistics filtered by the specified Tenant
4. WHEN the `tenant_id` parameter is omitted from the stats request, THE Knowledge_API SHALL return global statistics across all tenants

View File

@@ -0,0 +1,157 @@
# Implementation Plan: 知识库租户分组展示 (knowledge-tenant-view)
## Overview
将知识库管理页面从扁平列表改造为两层结构:第一层按租户分组展示汇总卡片,第二层展示租户下的知识条目列表。改造涉及 KnowledgeManager 业务逻辑层、Flask API 层、前端 dashboard.js 三个层面。
## Tasks
- [x] 1. KnowledgeManager 新增 get_tenant_summary 方法
- [x] 1.1 在 `src/knowledge_base/knowledge_manager.py` 中新增 `get_tenant_summary()` 方法
- 使用 SQLAlchemy `GROUP BY tenant_id` 聚合 `is_active == True` 的知识条目
- 返回包含 `tenant_id``entry_count``verified_count``category_distribution` 的字典列表
-`entry_count` 降序排列
- 数据库异常时返回空列表 `[]`,记录 error 日志
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [ ]* 1.2 为 get_tenant_summary 编写属性测试
- **Property 1: Tenant summary correctly aggregates active entries**
- **Property 2: Tenant summary sorted by entry_count descending**
- 使用 `hypothesis` 生成随机 KnowledgeEntry 列表,验证聚合正确性和排序
- **Validates: Requirements 1.1, 1.2, 1.3**
- [x] 2. KnowledgeManager 现有方法增加 tenant_id 过滤
- [x] 2.1 为 `get_knowledge_paginated()` 增加 `tenant_id` 可选参数
-`src/knowledge_base/knowledge_manager.py` 中修改方法签名,增加 `tenant_id: Optional[str] = None`
-`tenant_id` 不为 None 时,在查询中增加 `KnowledgeEntry.tenant_id == tenant_id` 过滤条件
- 返回结构不变,仅过滤范围缩小
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
- [ ]* 2.2 为 get_knowledge_paginated 的 tenant_id 过滤编写属性测试
- **Property 3: Knowledge entry filtering by tenant, category, and verified status**
- **Property 4: Pagination consistency with tenant filter**
- **Validates: Requirements 2.1, 2.2, 2.3**
- [x] 2.3 为 `search_knowledge()` 增加 `tenant_id` 可选参数
- 修改 `search_knowledge()``_search_by_embedding()``_search_by_keyword()` 方法签名
-`tenant_id` 不为 None 时,在查询中增加 tenant_id 过滤条件
- _Requirements: 6.2, 6.4_
- [ ]* 2.4 为 search_knowledge 的 tenant_id 过滤编写属性测试
- **Property 6: Search results scoped to tenant**
- **Validates: Requirements 6.2**
- [x] 2.5 为 `get_knowledge_stats()` 增加 `tenant_id` 可选参数
-`tenant_id` 不为 None 时,所有统计查询增加 tenant_id 过滤
- 返回结构中增加 `tenant_id` 字段(仅当按租户筛选时)
- _Requirements: 7.3, 7.4_
- [ ]* 2.6 为 get_knowledge_stats 的 tenant_id 过滤编写属性测试
- **Property 7: Stats scoped to tenant**
- **Validates: Requirements 7.3, 7.4**
- [x] 2.7 为 `add_knowledge_entry()` 增加 `tenant_id` 可选参数
-`tenant_id` 不为 None 时,新建条目的 `tenant_id` 设为该值
-`tenant_id` 为 None 时,使用 `get_config().server.tenant_id` 作为默认值
- _Requirements: 5.2_
- [ ]* 2.8 为 add_knowledge_entry 的 tenant_id 关联编写属性测试
- **Property 5: New entry tenant association**
- **Validates: Requirements 5.2**
- [x] 3. Checkpoint - 确保后端业务逻辑层完成
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Knowledge API 层新增和修改端点
- [x] 4.1 在 `src/web/blueprints/knowledge.py` 中新增 `GET /api/knowledge/tenants` 端点
- 调用 `knowledge_manager.get_tenant_summary()` 返回租户汇总 JSON 数组
- 使用 `@handle_api_errors` 装饰器处理异常
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 4.2 修改 `GET /api/knowledge` 端点,增加 `tenant_id` 查询参数支持
-`request.args` 获取 `tenant_id` 参数,传递给 `get_knowledge_paginated()`
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 4.3 修改 `GET /api/knowledge/stats` 端点,增加 `tenant_id` 查询参数支持
-`request.args` 获取 `tenant_id` 参数,传递给 `get_knowledge_stats()`
- _Requirements: 7.3, 7.4_
- [x] 4.4 修改 `GET /api/knowledge/search` 端点,增加 `tenant_id` 查询参数支持
-`request.args` 获取 `tenant_id` 参数,传递给 `search_knowledge()`
- _Requirements: 6.2_
- [x] 4.5 修改 `POST /api/knowledge` 端点,从请求体读取 `tenant_id` 字段
-`tenant_id` 传递给 `add_knowledge_entry()`
- _Requirements: 5.2_
- [ ]* 4.6 为新增和修改的 API 端点编写单元测试
- 测试 `/api/knowledge/tenants` 返回正确的汇总数据
- 测试各端点的 `tenant_id` 参数过滤行为
- 测试空数据和异常情况
- _Requirements: 1.1, 1.4, 1.5, 2.4_
- [x] 5. Checkpoint - 确保后端 API 层完成
- Ensure all tests pass, ask the user if questions arise.
- [x] 6. 前端 Tenant_List_View租户列表视图
- [x] 6.1 在 `src/web/static/js/dashboard.js` 中实现 `loadTenantList()` 函数
- 请求 `GET /api/knowledge/tenants` 获取租户汇总数据
- 渲染租户卡片列表,每张卡片展示 `tenant_id``entry_count``verified_count`
- 添加加载中 spinner 状态
- 无租户时展示空状态占位提示
- 卡片点击事件绑定,调用 `loadTenantDetail(tenantId)`
- _Requirements: 3.1, 3.2, 3.3, 3.4_
- [x] 6.2 实现刷新按钮功能
- 在知识库 tab 区域添加刷新按钮,点击时重新调用 `loadTenantList()`
- _Requirements: 3.5_
- [x] 7. 前端 Tenant_Detail_View租户详情视图
- [x] 7.1 实现 `loadTenantDetail(tenantId, page)` 函数
- 请求 `GET /api/knowledge?tenant_id=X&page=P&per_page=N` 获取知识条目
- 渲染知识条目表格,展示 question、answer、category、confidence_score、usage_count、is_verified
- 实现分页控件
- 支持 category 和 verified 筛选下拉框
- _Requirements: 4.1, 4.2, 4.5, 4.6_
- [x] 7.2 实现面包屑导航 `renderBreadcrumb(tenantId)`
- 展示 "知识库 > {tenant_id}" 面包屑
- 点击 "知识库" 链接时调用 `loadTenantList()` 返回租户列表视图
- 管理 `currentTenantId` 状态变量控制视图层级
- _Requirements: 4.3, 4.4_
- [x] 7.3 在 Tenant_Detail_View 中集成知识条目操作按钮
- 复用现有的添加、删除、验证、取消验证按钮逻辑
- 添加知识条目时自动设置 `tenant_id` 为当前选中的租户
- 批量操作(批量删除、批量验证、批量取消验证)后刷新当前视图
- 删除所有条目后自动返回租户列表视图
- 操作失败时通过 `showNotification` 展示错误提示
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 8. 前端搜索和统计面板适配
- [x] 8.1 修改搜索功能,在 Tenant_Detail_View 中自动附加 `tenant_id` 参数
- 搜索请求附加 `&tenant_id=currentTenantId`
- 清空搜索时恢复当前租户的完整分页列表
- _Requirements: 6.1, 6.2, 6.3_
- [x] 8.2 修改 `loadKnowledgeStats()` 函数,根据视图层级请求不同统计
-`currentTenantId` 为 null 时请求全局统计
-`currentTenantId` 有值时请求 `GET /api/knowledge/stats?tenant_id=X`
- _Requirements: 7.1, 7.2_
- [x] 9. 前端 HTML 模板更新
- [x] 9.1 在 `src/web/templates/dashboard.html``#knowledge-tab` 区域添加必要的 DOM 容器
- 添加面包屑容器、租户卡片列表容器、租户详情容器
- 确保与现有 Bootstrap 5 样式一致
- _Requirements: 3.1, 4.3_
- [x] 10. Final checkpoint - 确保所有功能集成完成
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- 数据模型 `KnowledgeEntry` 已有 `tenant_id` 字段且已建索引,无需数据库迁移