cherry-studio/docs/technical/topic-message-tree.md
lizhixuan c518c9090b docs: add example implementation of Redux Slice for message management
- Introduced a new section in the technical documentation detailing the implementation of a `messages` slice using `createEntityAdapter`.
- Provided TypeScript code for the slice, including actions for adding, updating, and removing messages.
- Summarized the core principles of the slice's design, emphasizing single responsibility, logical separation of concerns, and performance optimization.
- Included a migration strategy for transitioning from the previous state structure to the new message pool approach.
2025-06-12 18:34:36 +08:00

636 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 消息历史版本管理系统设计技术报告(最终版 - 含多模型支持)
## 1. 系统概述
基于现有扁平化架构的最小化扩展,通过 **Topic快照 + Message字段扩展含siblingIds** 实现版本管理、分支对话和多模型并行回复功能。
### 1.1 核心设计理念
- **最小破坏性**:只扩展现有实体,不新增表
- **快照渲染**通过Topic简单快照管理主线渲染顺序
- **关系扩展**通过Message字段实现树状分支、双向链表版本、多模型兄弟关系
## 2. 数据结构设计
### 2.1 实体定义
```typescript
interface Topic {
// === 现有字段保持不变 ===
id: string
name: string
createdAt: string
updatedAt: string
// === 保持简单快照 ===
activeMessageIds: string[] // 当前活跃对话主线的消息ID顺序
}
interface Message {
// === 现有字段保持不变 ===
id: string
role: 'user' | 'assistant' | 'system'
topicId: string
blocks: MessageBlock['id'][]
// === 新增:关系字段 ===
askId?: string // 问答关系assistant指向对应的user消息
parentMessageId?: string // 分支关系:指向回复的目标消息
version?: number // 版本号assistant消息专用
prevVersionId?: string // 版本链表:前一版本
nextVersionId?: string // 版本链表:后一版本
groupRequestId?: string // 请求分组同次API请求的标识
siblingIds?: string[] // 兄弟关系同级多模型回复的ID列表
}
interface MessageBlock {
// === 完全不变 ===
id: string
messageId: string
type: MessageBlockType
content: string
// ...其他现有字段
}
```
### 2.2 数据关系图
```mermaid
graph TB
subgraph "Topic快照层 (主线)"
T[Topic.activeMessageIds: user1→asst1-gpt→user2]
end
subgraph "消息实体层"
U1[User Message 1<br/>id: user1]
A1G["GPT-4 回复<br/>id: asst1-gpt, askId: user1<br/>siblingIds: [asst1-claude]"]
A1C["Claude 回复<br/>id: asst1-claude, askId: user1<br/>siblingIds: [asst1-gpt]"]
U2["User Message 2<br/>id: user2, parentMessageId: asst1-gpt"]
end
subgraph "版本链表层 (隐藏)"
A1GV0[GPT-4 v0<br/>askId: user1, version: 0]
A1GV1[GPT-4 v1<br/>askId: user1, version: 1]
A1GV0 -.->|nextVersionId| A1GV1
A1GV1 -.->|prevVersionId| A1GV0
end
subgraph "分支树层 (隐藏)"
U1B[User Branch 1<br/>parentMessageId: asst1-gpt]
A1B[Assistant Branch 1<br/>askId: user1b]
end
T --> U1
T --> A1G
T --> U2
A1G -.->|askId| U1
A1C -.->|askId| U1
A1G -.->|siblingIds| A1C
A1C -.->|siblingIds| A1G
U2 -.->|parentMessageId| A1G
U1B -.->|parentMessageId| A1G
A1B -.->|askId| U1B
```
## 3. 核心操作流程
### 3.1 发送新消息(多模型)
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
participant API
UI->>Redux: sendMessage(userContent, models[])
Note over Redux: 1. 创建用户消息
Redux->>Redux: userMessage = { id: uuid(), role: 'user', ... }
Note over Redux: 2. 创建助手消息(多模型)
Redux->>Redux: groupRequestId = uuid()
Redux->>Redux: assistantMessages = models.map(m => createAssistant(userMessage.id, m))
Note over Redux: 3. 设置兄弟关系
Redux->>Redux: assistantIds = assistantMessages.map(m => m.id)
loop 每个助手消息
Redux->>Redux: msg.siblingIds = assistantIds.filter(id => id !== msg.id)
end
Note over Redux: 4. 更新Topic快照
Redux->>Redux: newActiveMessageIds = [<br/>...oldIds,<br/>userMessage.id,<br/>assistantMessages[0].id<br/>]
Note over Redux: 5. 原子保存
Redux->>DB: transaction([messages, topics])
DB->>DB: messages.bulkPut([userMessage, ...assistantMessages])
DB->>DB: topics.update(topicId, { activeMessageIds })
Note over Redux: 6. 发送API请求
loop 每个模型
Redux->>API: generateResponse(model, userContent)
end
Redux->>UI: 更新状态
```
**复杂度**O(M) where M = 模型数量
### 3.2 重发消息(版本管理)
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
UI->>Redux: resendMessage(userMessageId)
Note over Redux: 1. 查找现有版本
Redux->>DB: messages.where('askId').equals(userMessageId)
DB-->>Redux: existingVersions[]
Note over Redux: 2. 计算新版本号
Redux->>Redux: latestVersion = max(versions.map(v => v.version))
Redux->>Redux: newVersion = latestVersion + 1
Note over Redux: 3. 创建新版本消息(可能多模型)
Redux->>Redux: newGroupRequestId = uuid()
Redux->>Redux: newVersionMessages = models.map(m => createNewVersion(prevMsg, newVersion, newGroupRequestId))
Note over Redux: 4. 设置新版本的兄弟关系
Redux->>Redux: newVersionIds = newVersionMessages.map(m => m.id)
loop 每个新版本消息
Redux->>Redux: msg.siblingIds = newVersionIds.filter(id => id !== msg.id)
end
Note over Redux: 5. 更新版本链表
Redux->>DB: transaction(messages)
DB->>DB: messages.update(prevMessage.id, { nextVersionId })
DB->>DB: messages.bulkPut(newVersionMessages)
Redux->>UI: 更新状态
```
**复杂度**O(V) 查找 + O(M) 创建
### 3.3 切换活跃模型UI交互
```mermaid
flowchart TD
A[用户在UI上选择其他模型] --> B[获取当前快照]
B --> C[找到当前助手消息在快照中的位置]
C --> D[用新选择的模型消息ID替换快照中的ID]
D --> E[保存到数据库]
E --> F[Redux自动重新渲染]
style A fill:#e1f5fe
style F fill:#c8e6c9
```
```typescript
const switchActiveModel = async (topicId: string, messageIndex: number, newModelMessageId: string) => {
const topic = await db.topics.get(topicId)
const newActiveMessageIds = [...topic.activeMessageIds]
newActiveMessageIds[messageIndex] = newModelMessageId
await db.topics.update(topicId, { activeMessageIds: newActiveMessageIds })
}
```
**复杂度**O(1)
## 4. 字段作用详解
### 4.1 关键字段关系图
```mermaid
graph LR
subgraph "问答关系"
askId[askId<br/>assistant → user<br/>逻辑关系,永久不变]
end
subgraph "分支关系"
parentId[parentMessageId<br/>message → message<br/>分支对话,树状结构]
end
subgraph "版本关系"
version[version + prevVersionId + nextVersionId<br/>同askId下的版本链表]
end
subgraph "请求分组"
groupId[groupRequestId<br/>同次API请求标识<br/>一次性,每次重发都变]
end
subgraph "兄弟关系"
siblingId[siblingIds<br/>同级多模型回复<br/>双向引用]
end
askId -.-> version
askId -.-> siblingId
parentId -.-> askId
groupId -.-> askId
```
### 4.2 字段使用场景
| 字段 | 用途 | 查询场景 | 生命周期 |
| -------------------------------- | ---------- | -------------------------- | -------- |
| **askId** | 问答映射 | 查找用户问题的所有回复版本 | 永久不变 |
| **parentMessageId** | 分支对话 | 查找某消息的回复分支 | 永久不变 |
| **version + prev/nextVersionId** | 版本管理 | 版本历史导航 | 永久不变 |
| **groupRequestId** | 请求追踪 | 批量状态更新、请求监控 | 一次性 |
| **siblingIds** | 多模型并行 | 渲染同级多模型回复 | 永久不变 |
### 4.3 多模型并行渲染示例
```mermaid
graph TD
U1[User: 帮我写个函数<br/>id: user1]
subgraph "第一次请求 (groupRequestId: req1)"
A1["GPT-4 回复<br/>id: asst1-gpt, askId: user1<br/>siblingIds: [asst1-claude]"]
A2["Claude 回复<br/>id: asst1-claude, askId: user1<br/>siblingIds: [asst1-gpt]"]
end
subgraph "Topic快照 (主线)"
T["activeMessageIds: [user1, asst1-gpt]"]
end
subgraph "UI渲染 (通过siblingIds扩展)"
UI_U1[User: 帮我写个函数]
UI_A1["GPT-4 回复 (活跃)"]
UI_A2["Claude 回复 (可选)"]
end
U1 --> A1
U1 --> A2
T --> U1
T --> A1
A1 -.->|siblingIds| A2
A2 -.->|siblingIds| A1
UI_U1 -.-> UI_A1
UI_U1 -.-> UI_A2
```
## 5. 数据查询与状态管理
### 5.1 话题加载流程
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
participant Selector
UI->>Redux: loadTopic(topicId)
Redux->>DB: 并行查询
par 查询消息
DB->>DB: messages.where('topicId').equals(topicId)
and 查询块
DB->>DB: messageBlocks.where('topicId').equals(topicId)
end
DB-->>Redux: { messages[], blocks[] }
Redux->>Redux: 更新实体状态
UI->>Selector: selectActiveConversationWithSiblings(topicId)
Selector->>Redux: 获取Topic.activeMessageIds
Selector->>Redux: 获取messages实体
Selector-->>UI: 按快照顺序的消息列表 (含兄弟节点)
Note over UI: 渲染对话界面 (支持多模型)
```
### 5.2 渲染选择器(含兄弟节点)
```typescript
export const selectActiveConversationWithSiblings = createSelector(
[
(state: RootState, topicId: string) => state.topics.entities[topicId]?.activeMessageIds || [],
(state: RootState) => state.messages.entities,
(state: RootState) => state.messageBlocks.entities
],
(activeMessageIds, messagesEntities, blocksEntities) => {
return activeMessageIds
.map((messageId) => {
const message = messagesEntities[messageId]
if (!message) return null
if (message.role === 'user') {
return { type: 'user', message, blocks: getMessageBlocks(message, blocksEntities) }
} else if (message.role === 'assistant') {
const siblingMessages = (message.siblingIds || []).map((id) => messagesEntities[id]).filter(Boolean)
const allAssistantMessages = [message, ...siblingMessages]
return {
type: 'assistant_group',
messages: allAssistantMessages.map((msg) => ({
message: msg,
blocks: getMessageBlocks(msg, blocksEntities),
isActive: msg.id === messageId
})),
activeMessageId: messageId
}
}
})
.filter(Boolean)
}
)
```
**复杂度**O(N + S) where N = 快照长度, S = 兄弟节点总数
## 6. 时空复杂度分析
### 6.1 核心操作复杂度对比
```mermaid
graph LR
subgraph "现有架构"
A1[加载话题: O(M+B)]
A2[渲染对话: O(M) 需要过滤排序]
A3[发送消息: O(1)]
end
subgraph "新架构 (含多模型)"
B1[加载话题: O(M+B) ✅相同]
B2[渲染对话: O(N+S) ✅更优]
B3[发送消息: O(M_models) ✅相同]
B4[版本切换: O(1) ➕新功能]
B5[重发消息: O(V)+O(M_models) ➕新功能]
B6[模型切换: O(1) ➕新功能]
end
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9
style B4 fill:#fff3e0
style B5 fill:#fff3e0
style B6 fill:#fff3e0
```
### 6.2 性能优势分析
| 操作 | 现有架构 | 新架构 | 优势说明 |
| ------------ | -------------- | ---------------------------- | -------------------- |
| **话题加载** | O(M + B) | O(M + B) | 性能保持不变 |
| **对话渲染** | O(M) 过滤+排序 | **O(N+S)** 直接索引+兄弟扩展 | N << MS通常较小 |
| **发送消息** | O(1) | O(M_models) | 支持多模型合理增长 |
| **版本切换** | 不支持 | **O(1)** | 新功能极佳性能 |
| **模型切换** | 不支持 | **O(1)** | 新功能极佳性能 |
**关键优势**
- **渲染性能提升** O(M) 优化到 O(N+S)长对话场景收益显著
- **多模型支持**通过 siblingIds 优雅实现
- **版本管理**O(1) 的版本/模型切换用户体验极佳
- **向后兼容**现有核心操作性能保持不变
## 7. 数据库Schema演进
### 7.1 Migration策略
```mermaid
flowchart TD
A[现有Schema] --> B[添加字段]
B --> C[创建索引]
C --> D[数据迁移]
D --> E[验证完整性]
B1[Topic: +activeMessageIds]
B2[Message: +askId, +parentMessageId<br/>+version, +prevVersionId<br/>+nextVersionId, +groupRequestId<br/>+siblingIds]
C1[idx_messages_askid_version]
C2[idx_messages_parent]
C3[idx_messages_group_request]
D1[生成activeMessageIds快照]
D2[设置现有assistant消息version=0]
B --> B1
B --> B2
C --> C1
C --> C2
C --> C3
D --> D1
D --> D2
```
### 7.2 SQL Migration
```sql
-- 1. 添加字段
ALTER TABLE topics ADD COLUMN activeMessageIds TEXT; -- JSON数组
ALTER TABLE messages ADD COLUMN askId TEXT;
ALTER TABLE messages ADD COLUMN parentMessageId TEXT;
ALTER TABLE messages ADD COLUMN version INTEGER;
ALTER TABLE messages ADD COLUMN prevVersionId TEXT;
ALTER TABLE messages ADD COLUMN nextVersionId TEXT;
ALTER TABLE messages ADD COLUMN groupRequestId TEXT;
ALTER TABLE messages ADD COLUMN siblingIds TEXT; -- JSON数组
-- 2. 创建索引
CREATE INDEX idx_messages_askid_version ON messages(askId, version);
CREATE INDEX idx_messages_parent ON messages(parentMessageId);
CREATE INDEX idx_messages_group_request ON messages(groupRequestId);
-- 3. 数据迁移
UPDATE messages SET version = 0 WHERE role = 'assistant';
```
## 8. 流式更新兼容性
### 8.1 MessageBlock更新流程
```mermaid
sequenceDiagram
participant Stream
participant Redux
participant DB
participant UI
Note over Stream: 流式内容到达
Stream->>Redux: updateBlock(blockId, content)
Redux->>Redux: updateOneBlock({ id, changes })
Redux->>UI: 立即更新显示
Note over Redux: 节流数据库写入
Redux->>DB: throttledDbUpdate(blockId, content)
Note over Stream,UI: 版本/兄弟关系不影响块更新
```
**关键点**
- MessageBlock 仍然直接关联到 Message
- 版本/兄弟关系在 Message 层面不影响 Block 的流式更新
- 现有的节流机制和更新逻辑完全保持不变
## 9. 系统架构总览
### 9.1 整体架构图
```mermaid
graph TB
subgraph "UI层"
UI1[对话界面]
UI2[版本选择器]
UI3[分支导航]
UI4[模型切换器]
end
subgraph "Redux状态层"
R1[topics: EntityAdapter]
R2[messages: EntityAdapter]
R3[messageBlocks: EntityAdapter]
S1[selectActiveConversationWithSiblings]
S2[selectVersionHistory]
end
subgraph "数据库层"
DB1[(topics表)]
DB2[(messages表)]
DB3[(messageBlocks表)]
end
subgraph "API层"
API1[多模型并行请求]
API2[流式响应处理]
end
UI1 --> S1
UI2 --> S2
UI4 --> S1
S1 --> R1
S1 --> R2
S2 --> R2
R1 <--> DB1
R2 <--> DB2
R3 <--> DB3
R2 --> API1
API2 --> R3
style UI1 fill:#e3f2fd
style R1 fill:#f3e5f5
style R2 fill:#f3e5f5
style R3 fill:#f3e5f5
style DB1 fill:#e8f5e8
style DB2 fill:#e8f5e8
style DB3 fill:#e8f5e8
```
### 9.2 数据流向
```mermaid
flowchart LR
A[用户输入] --> B[创建User Message]
B --> C["创建Assistant Messages (多模型)"]
C --> C1[设置Sibling关系]
C1 --> D["更新Topic快照 (主线)"]
D --> E[API并行请求]
E --> F[流式更新Blocks]
F --> G["UI实时渲染 (含多模型)"]
H[版本切换] --> I[更新快照指针]
I --> G
J[分支对话] --> K[创建分支消息]
K --> D
L[模型切换] --> I
style A fill:#ffebee
style G fill:#e8f5e8
style H fill:#fff3e0
style J fill:#f3e5f5
style L fill:#e1f5fe
```
## 10. Redux Slice 实现范例
根据上述架构设计`messages` slice 将演变为一个纯粹的 `createEntityAdapter` 管理的"消息池"。它只负责高效地存储和访问单个消息实体而不再关心对话的顺序
### `store/messagesSlice.ts`
```typescript
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from './store' // 你的store类型定义
import type { Message } from '@renderer/types/newMessage' // 假设 Message 类型定义在外部
// 1. 创建 Entity Adapter
// 它会自动生成管理实体的reducer逻辑实现一个高效的消息池。
const messagesAdapter = createEntityAdapter<Message>()
// 2. 定义 Slice 的初始状态
// adapter.getInitialState() 会自动创建 { ids: [], entities: {} } 结构
const initialState = messagesAdapter.getInitialState()
// 3. 创建 Slice
const messagesSlice = createSlice({
name: 'messages',
initialState,
// Reducers被极大简化多数直接引用adapter提供的方法
reducers: {
// Action: 添加一条消息
messageAdded: messagesAdapter.addOne,
// Action: 一次性添加或更新多个消息 (高性能)
// 用途: 加载话题历史、发送新一轮问答(user+assistants)
messagesUpserted: messagesAdapter.upsertMany,
// Action: 更新单个消息
// 用途: 流式更新结束、状态变更等
messageUpdated: messagesAdapter.updateOne,
// Action: 删除单个消息
messageRemoved: messagesAdapter.removeOne,
// Action: 删除多个消息
messagesRemoved: messagesAdapter.removeMany,
// Action: 用新数据完全替换消息池
// 用途: 首次加载或强制刷新
messagesSet: messagesAdapter.setAll
}
})
// 4. 导出 Actions
export const { messageAdded, messagesUpserted, messageUpdated, messageRemoved, messagesRemoved, messagesSet } =
messagesSlice.actions
// 5. 导出 Selectors
// Adapter 会自动创建高效的查询函数 (e.g., O(1) by ID)
export const messagesSelectors = messagesAdapter.getSelectors((state: RootState) => state.messages)
// 6. 导出 Reducer
export default messagesSlice.reducer
```
### 核心思想总结
1. **职责单一**: Slice 只做一件事——管理 `Message` 实体它像一个数据库表高效地处理增删改查但对业务逻辑如对话顺序一无所知
2. **逻辑上移**: 所有涉及多个 Slice 的复杂业务逻辑如发送消息切换版本都应封装在 **Thunks** 或其他中间件中Thunk 作为流程协调者 `dispatch` 多个原子化的 Action `messagesSlice` `topicsSlice`以完成一次完整的业务操作并保证数据一致性
3. **性能保证**: `createEntityAdapter` 内部使用哈希表对象来存储实体确保通过 ID 查询消息的操作为 O(1) 复杂度性能极佳
### 旧状态属性迁移
为了完成 `messagesSlice` 向纯粹"消息池"的演进原有的混合状态属性需要被迁移或废弃以实现彻底的职责分离
| 原属性 (`newMessage.ts`) | 处理方式 | 新的归宿 / 说明 |
| :----------------------- | :------------ | :-------------------------------------------------------------------------------------------- |
| `messageIdsByTopic` | **废弃** | 核心职责转移 `topicsSlice` 中的 `activeMessageIds` 字段接管作为渲染快照 |
| `currentTopicId` | **迁移** | 属于UI当前上下文状态应迁移至 `topicsSlice` |
| `loadingByTopic` | **迁移** | 话题的加载状态与话题本身更相关应迁移至 `topicsSlice` |
| `displayCount` | **废弃/迁移** | UI相关的显示逻辑不属于消息数据层建议迁移至专门的 `Slice` 或在相关组件中作为本地状态管理 |