cherry-studio/docs/technical/topic-message-tree.md
suyao 748ca008b4
docs: add technical report for message history version management system
- Introduced a comprehensive design document outlining the architecture and requirements for a message history version management system.
- Added new entities `UserMessage` and `AssistantMessageGroup` to support a directed multi-branch conversation structure and version management.
- Updated existing entities to accommodate the new architecture while maintaining backward compatibility.
- Included performance analysis and migration strategies for transitioning to the new system.
2025-06-12 14:25:42 +08:00

277 lines
8.4 KiB
Markdown
Raw 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. 项目背景与需求
### 1.1 当前架构状态
- 已实现 `Message``MessageBlock` 的扁平化存储
- 使用 Redux `createEntityAdapter` 进行状态管理
- 数据库操作基于单个实体,已弃用 `topic.messages` 大数组
- 缺少版本管理和分支对话功能
### 1.2 设计目标
![image.png](topic-message-tree.png)
在现有扁平化架构基础上,增加:
- 有向多叉树的对话结构管理
- 双向链表的版本管理机制
- 分支对话支持
- 最小化对现有流式更新机制的影响
## 2. 架构演进设计
### 2.1 当前架构 vs 新架构
**当前架构**
```
Topic → Message[] (通过 selector 组装)
Message → MessageBlock[]
```
**新架构**
```
Topic → UserMessage ←→ UserMessage (线性链表)
AssistantMessageGroup ←→ AssistantMessageGroup (版本链表)
Message[] → MessageBlock[]
```
### 2.2 新增实体
| 新增实体 | 作用 | 与现有实体关系 |
| ----------------------- | ---------------------- | ---------------------------------- |
| `UserMessage` | 用户消息节点,树的主干 | 包含现有 user role 的 Message 功能 |
| `AssistantMessageGroup` | 助手回复组,版本容器 | 组织现有 assistant role 的 Message |
**现有实体调整**
- `Message`: 增加 `groupId` 字段,移除 user role
- `MessageBlock`: 保持不变
- `Topic`: 移除弃用字段增加树指针Topic 只维护当前"活跃对话路径"的快照,用户在 UI 上看到的默认对话流就是这个活跃快照
### 2.3 数据结构预览
```typescript
export interface UserMessage {
// 基础标识
id: string
topicId: string
createdAt: string
updatedAt?: string
status: UserMessageStatus
// 消息内容关联
blocks: MessageBlock['id'][]
// 树结构关联 - 邻接列表
parentAssistantGroupId?: string // 指向父助手消息组(回复哪个助手消息)
// 线性链表关联 - 话题内用户消息顺序
prevUserMessageId?: string // 前一个用户消息
nextUserMessageId?: string // 后一个用户消息
}
export interface AssistantMessageGroup {
// 基础标识
id: string
topicId: string
createdAt: string
updatedAt?: string
status: AssistantMessageStatus
// 树结构关联 - 邻接列表
parentUserMessageId: string // 指向父用户消息(回复哪个用户消息)
// 版本链表关联 - 同一用户消息下的不同版本
version: number // 版本号,从 0 开始
prevVersionGroupId?: string // 前一个版本组
nextVersionGroupId?: string // 后一个版本组
isActiveInTopic: boolean // 是否是当前活跃对话路径的快照
// 内容容器关联
messageIds: Message['id'][] // 包含的实际消息ID列表
}
export interface Message {
// 基础标识
id: string
createdAt: string
updatedAt?: string
status: AssistantMessageStatus
// 角色限制 - 不再包含 'user'
role: 'assistant' | 'system'
// 组织关联
groupId: string // 指向所属的 AssistantMessageGroup
// 消息内容关联
blocks: MessageBlock['id'][]
// 向后兼容字段(用于现有代码)
askId?: string // 指向原始 UserMessage ID保持向后兼容
topicId?: string // 冗余字段,便于查询优化
}
export interface Topic {
// 基础标识
id: string
createdAt: string
updatedAt?: string
// 树结构入口指针
rootUserMessageId?: string // 第一个用户消息节点ID
latestUserMessageId?: string // 最新用户消息节点ID
activeConversationPath: {
// 新增:活跃路径快照
userMessageIds: string[] // 按时间顺序的用户消息ID列表
activeGroupIds: string[] // 每个用户消息当前激活的助手组ID
}
/**
* @deprecated 已弃用的扁平数组,保留向后兼容
*/
messages: Message[]
}
```
## 3. 关键操作复杂度对比
### 3.1 变量定义
- `N`: 话题中用户消息轮次数量
- `V`: 每轮平均版本数量
- `M`: 每组平均消息数量
- `B`: 平均消息块数量
### 3.2 操作复杂度对比
| 操作 | 当前方案 | 新方案 | 主要改进 |
| -------------- | --------------------------- | ------------------- | ------------------------------------------------ |
| **发送新消息** | O(1) 新增 + O(1) Topic更新 | O(1) 新增多个实体 | 实体数量增加,但复杂度相同 |
| **重发消息** | O(M×B) 删除旧块 + O(1) 重置 | **O(1) 创建新版本** | **支持版本保留,避免删除操作** |
| **加载话题** | O(N×M+B) 批量查询 | O(N×M+B) 批量查询 | 只加载活跃路径的快照,不加载历史版本,复杂度相同 |
| **删除消息** | O(K) 删除相关实体 | O(K) 删除相关实体 | 复杂度相同 |
| **版本切换** | **不支持** | **O(1) 指针查找** | **新功能** |
| **分支对话** | **不支持** | **O(1) 创建分支** | **新功能** |
### 3.3 性能影响分析
**积极影响**
- **重发操作优化**:从删除重建变为版本创建,避免数据丢失
- **版本管理**O(1) 的版本切换和历史访问
- **并发安全**:版本隔离减少操作冲突
**成本增加**
- **存储空间**:增加 `O(V)` 倍的版本存储开销
- **Selector 复杂度**:树状数据重组逻辑更复杂
## 4. 流式更新兼容性分析
### 4.1 现有机制保持不变
```typescript
// 现有的流式更新机制完全兼容
throttledBlockUpdate(blockId, { content, status }) // 仍然是 O(1)
dispatch(updateOneBlock({ id: blockId, changes })) // 仍然是 O(1)
```
### 4.2 新架构对流式更新的影响
- **MessageBlock 层面**:零影响,仍通过 `messageId` 直接关联
- **Message 层面**:增加 `groupId` 字段,不影响块更新逻辑
- **状态同步**:保持现有的节流机制和事务策略
## 5. Redux 状态管理演进
### 5.1 当前 State 结构
```typescript
// 当前已有
state = {
messages: { ids: [], entities: {} }, // 现有
messageBlocks: { ids: [], entities: {} } // 现有
// 其他现有 slice...
}
```
### 5.2 新增 State 结构
```typescript
// 新增 slice
state = {
// 现有保持不变
messages: { ids: [], entities: {} },
messageBlocks: { ids: [], entities: {} },
// 新增
userMessages: { ids: [], entities: {} },
assistantMessageGroups: { ids: [], entities: {} }
}
```
### 5.3 迁移策略
1. **向后兼容**:现有 `Message``MessageBlock` API 保持不变
2. **渐进式迁移**:新功能使用新实体,现有功能继续工作
3. **Selector 适配**:调整 `selectMessagesForTopic` 以支持新的树状结构
## 6. 数据库迁移策略
### 6.1 Schema 演进
```sql
-- 保持现有表结构
Messages: id(PK), 现有字段..., groupId(新增)
MessageBlocks: id(PK), 现有字段... (无变化)
-- 新增表
UserMessages: id(PK), topicId, parentAssistantGroupId, prevUserMessageId, nextUserMessageId
AssistantMessageGroups: id(PK), topicId, parentUserMessageId, version, prevVersionGroupId, nextVersionGroupId
```
### 6.2 数据迁移
```typescript
// 迁移现有数据到新结构
const migrateExistingMessages = async () => {
// 1. 识别 user role 的 Message转换为 UserMessage
// 2. 按 askId 分组 assistant role 的 Message创建 AssistantMessageGroup
// 3. 更新 Message 实体的 groupId 字段
// 4. 构建链表指针关系
}
```
## 7. 技术债务与风险评估
### 7.1 新增复杂度
- **开发复杂度**:新增 2 个实体类型和相应的 CRUD 操作
- **测试复杂度**:版本管理和分支逻辑的测试覆盖
- **维护复杂度**:更复杂的数据一致性保证
### 7.2 性能风险
- **内存占用**:历史版本累积可能导致内存压力
- **查询性能**:复杂 Selector 可能影响渲染性能
- **存储增长**:版本保留策略需要清理机制
### 7.3 缓解措施
```typescript
// 版本清理策略
const cleanupOldVersions = async (topicId: string, keepVersions: number = 5) => {
// 保留最近 N 个版本,清理旧版本
}
// 惰性加载优化
const loadTopicWithPagination = async (topicId: string, limit: number = 50) => {
// 分页加载历史消息,避免一次性加载全部
}
```