mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
16 KiB
16 KiB
统一 Chat 和 Agent Session 数据层架构重构方案
目标
通过创建统一的数据访问层,消除 AgentSessionMessages 和 Messages 组件的重复代码,实现普通聊天和 Agent 会话的统一处理。
核心设计
使用门面模式 (Facade Pattern) 和策略模式 (Strategy Pattern) 创建统一的数据访问层,对外提供一致的 API,内部根据 topicId 类型自动路由到不同的数据源。
架构设计
┌─────────────────────────────────────────┐
│ UI Components │
│ (Messages, Inputbar - 完全复用) │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ Hooks & Selectors │
│ (useTopic, useTopicMessages - 统一) │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ Redux Thunks │
│ (不再判断 isAgentSessionTopicId) │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ DbService (门面) │
│ 根据 topicId 内部路由到对应数据源 │
└─────────────────────────────────────────┘
│
┌───────────┴───────────┐
┌──────────────┐ ┌──────────────────┐
│ DexieMessage │ │ AgentMessage │
│ DataSource │ │ DataSource │
│ │ │ │
│ (Dexie) │ │ (IPC/Backend) │
└──────────────┘ └──────────────────┘
实施计划
Phase 1: 创建数据访问层 (src/renderer/src/services/db/)
1.1 定义 MessageDataSource 接口
// src/renderer/src/services/db/types.ts
interface MessageDataSource {
// 读取操作
fetchMessages(topicId: string): Promise<{ messages: Message[], blocks: MessageBlock[] }>
getRawTopic(topicId: string): Promise<{ id: string; messages: Message[] }>
// 写入操作
persistExchange(topicId: string, exchange: MessageExchange): Promise<void>
appendMessage(topicId: string, message: Message, blocks: MessageBlock[]): Promise<void>
updateMessage(topicId: string, messageId: string, updates: Partial<Message>): Promise<void>
deleteMessage(topicId: string, messageId: string): Promise<void>
// 批量操作
clearMessages(topicId: string): Promise<void>
updateBlocks(blocks: MessageBlock[]): Promise<void>
}
interface MessageExchange {
user?: { message: Message, blocks: MessageBlock[] }
assistant?: { message: Message, blocks: MessageBlock[] }
}
1.2 实现 DexieMessageDataSource
// src/renderer/src/services/db/DexieMessageDataSource.ts
class DexieMessageDataSource implements MessageDataSource {
async fetchMessages(topicId: string) {
const topic = await db.topics.get(topicId)
const messages = topic?.messages || []
const messageIds = messages.map(m => m.id)
const blocks = await db.message_blocks.where('messageId').anyOf(messageIds).toArray()
return { messages, blocks }
}
async persistExchange(topicId: string, exchange: MessageExchange) {
// 保存到 Dexie 数据库
await db.transaction('rw', db.topics, db.message_blocks, async () => {
// ... 现有的保存逻辑
})
}
// ... 其他方法实现
}
1.3 实现 AgentMessageDataSource
// src/renderer/src/services/db/AgentMessageDataSource.ts
class AgentMessageDataSource implements MessageDataSource {
async fetchMessages(topicId: string) {
const sessionId = topicId.replace('agent-session:', '')
const historicalMessages = await window.electron.ipcRenderer.invoke(
IpcChannel.AgentMessage_GetHistory,
{ sessionId }
)
const messages: Message[] = []
const blocks: MessageBlock[] = []
for (const msg of historicalMessages) {
if (msg?.message) {
messages.push(msg.message)
if (msg.blocks) blocks.push(...msg.blocks)
}
}
return { messages, blocks }
}
async persistExchange(topicId: string, exchange: MessageExchange) {
const sessionId = topicId.replace('agent-session:', '')
await window.electron.ipcRenderer.invoke(
IpcChannel.AgentMessage_PersistExchange,
{ sessionId, ...exchange }
)
}
// ... 其他方法实现
}
1.4 创建 DbService 门面
// src/renderer/src/services/db/DbService.ts
class DbService {
private dexieSource = new DexieMessageDataSource()
private agentSource = new AgentMessageDataSource()
private getDataSource(topicId: string): MessageDataSource {
if (isAgentSessionTopicId(topicId)) {
return this.agentSource
}
// 未来可扩展其他数据源判断
return this.dexieSource
}
async fetchMessages(topicId: string) {
return this.getDataSource(topicId).fetchMessages(topicId)
}
async persistExchange(topicId: string, exchange: MessageExchange) {
return this.getDataSource(topicId).persistExchange(topicId, exchange)
}
// ... 代理其他方法
}
export const dbService = new DbService()
Phase 2: 重构 Redux Thunks(详细拆分)
由于 messageThunk.ts 改动较大,将 Phase 2 分成多个批次逐步实施:
2.0 准备工作
- 添加 Feature Flag:
USE_UNIFIED_DB_SERVICE - 创建 messageThunk.v2.ts 作为临时过渡文件
- 准备回滚方案
2.1 批次1:只读操作重构(风险最低)
这批改动只涉及读取操作,不会影响数据写入,风险最低。
需要重构的函数
// loadTopicMessagesThunk
export const loadTopicMessagesThunkV2 = (topicId: string, forceReload: boolean = false) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState()
if (!forceReload && state.messages.messageIdsByTopic[topicId]) {
return // 已有缓存
}
try {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: true }))
// 新:统一调用
const { messages, blocks } = await dbService.fetchMessages(topicId)
if (blocks.length > 0) {
dispatch(upsertManyBlocks(blocks))
}
dispatch(newMessagesActions.messagesReceived({ topicId, messages }))
} catch (error) {
logger.error(`Failed to load messages for topic ${topicId}:`, error)
} finally {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
}
}
// getRawTopic
export const getRawTopicV2 = async (topicId: string) => {
return await dbService.getRawTopic(topicId)
}
测试清单
- 普通 Topic 消息加载
- Agent Session 消息加载
- 缓存机制正常工作
- 错误处理
2.2 批次2:辅助函数重构
这批函数不直接操作数据库,但依赖数据库操作。
需要重构的函数
// getTopic
export const getTopicV2 = async (topicId: string): Promise<Topic | undefined> => {
const rawTopic = await dbService.getRawTopic(topicId)
if (!rawTopic) return undefined
return {
id: rawTopic.id,
type: isAgentSessionTopicId(topicId) ? TopicType.AgentSession : TopicType.Chat,
messages: rawTopic.messages,
// ... 其他字段
}
}
// updateFileCount
export const updateFileCountV2 = async (
fileId: string,
delta: number,
deleteIfZero = false
) => {
// 只对 Dexie 数据源有效
if (dbService.supportsFileCount) {
await dbService.updateFileCount(fileId, delta, deleteIfZero)
}
}
测试清单
- getTopic 返回正确的 Topic 类型
- updateFileCount 只在支持的数据源上执行
- 边界条件测试
2.3 批次3:删除操作重构
删除操作相对独立,风险可控。
需要重构的函数
// deleteMessageFromDB
export const deleteMessageFromDBV2 = async (
topicId: string,
messageId: string
): Promise<void> => {
await dbService.deleteMessage(topicId, messageId)
}
// deleteMessagesFromDB
export const deleteMessagesFromDBV2 = async (
topicId: string,
messageIds: string[]
): Promise<void> => {
await dbService.deleteMessages(topicId, messageIds)
}
// clearMessagesFromDB
export const clearMessagesFromDBV2 = async (topicId: string): Promise<void> => {
await dbService.clearMessages(topicId)
}
测试清单
- 单个消息删除
- 批量消息删除
- 清空所有消息
- 文件引用计数正确更新
- Agent Session 删除操作(应为 no-op)
2.4 批次4:复杂写入操作重构
这批包含最复杂的写入逻辑,需要特别注意。
需要重构的函数
// saveMessageAndBlocksToDB
export const saveMessageAndBlocksToDBV2 = async (
topicId: string,
message: Message,
blocks: MessageBlock[]
): Promise<void> => {
// 移除 isAgentSessionTopicId 判断
await dbService.appendMessage(topicId, message, blocks)
}
// persistExchange
export const persistExchangeV2 = async (
topicId: string,
exchange: MessageExchange
): Promise<void> => {
await dbService.persistExchange(topicId, exchange)
}
// sendMessage (最复杂的函数)
export const sendMessageV2 = (userMessage, userMessageBlocks, assistant, topicId, agentSession?) =>
async (dispatch, getState) => {
// 保存用户消息 - 统一接口
await dbService.appendMessage(topicId, userMessage, userMessageBlocks)
dispatch(newMessagesActions.addMessage({ topicId, message: userMessage }))
// ... 创建助手消息 ...
// 保存交换对 - 统一接口
await dbService.persistExchange(topicId, {
user: { message: userMessage, blocks: userMessageBlocks },
assistant: { message: assistantMessage, blocks: [] }
})
}
测试清单
- 普通消息发送流程
- Agent Session 消息发送流程
- 消息块正确保存
- Redux state 正确更新
- 流式响应处理
- 错误处理和重试机制
2.5 批次5:更新操作重构
更新操作通常涉及消息编辑、状态更新等。
需要重构的函数
// updateMessage
export const updateMessageV2 = async (
topicId: string,
messageId: string,
updates: Partial<Message>
): Promise<void> => {
await dbService.updateMessage(topicId, messageId, updates)
}
// updateSingleBlock
export const updateSingleBlockV2 = async (
blockId: string,
updates: Partial<MessageBlock>
): Promise<void> => {
await dbService.updateSingleBlock(blockId, updates)
}
// bulkAddBlocks
export const bulkAddBlocksV2 = async (blocks: MessageBlock[]): Promise<void> => {
await dbService.bulkAddBlocks(blocks)
}
测试清单
- 消息内容更新
- 消息状态更新
- 消息块更新
- 批量块添加
- Agent Session 更新操作(应为 no-op)
2.6 迁移策略
阶段1:并行运行(Week 1)
export const loadTopicMessagesThunk = (topicId: string, forceReload: boolean = false) => {
if (featureFlags.USE_UNIFIED_DB_SERVICE) {
return loadTopicMessagesThunkV2(topicId, forceReload)
}
return loadTopicMessagesThunkOriginal(topicId, forceReload)
}
阶段2:灰度测试(Week 2)
- 10% 用户使用新实现
- 监控性能和错误率
- 收集用户反馈
阶段3:全量迁移(Week 3)
- 100% 用户使用新实现
- 保留 feature flag 一周观察
- 准备回滚方案
阶段4:代码清理(Week 4)
- 移除旧实现代码
- 移除 feature flag
- 更新文档
2.8 回滚计划
如果出现问题,按以下步骤回滚:
-
立即回滚(< 5分钟)
- 关闭 feature flag
- 所有流量回到旧实现
-
修复后重试
- 分析问题原因
- 修复并添加测试
- 小范围测试后重新上线
-
彻底回滚(如果问题严重)
- 恢复到改动前的代码版本
- 重新评估方案
Phase 3: 统一 Hooks 层
3.1 创建统一的 useTopic Hook
// src/renderer/src/hooks/useTopic.ts
export const useTopic = (topicIdOrSessionId: string): Topic => {
const topicId = buildTopicId(topicIdOrSessionId) // 处理映射
const [topic, setTopic] = useState<Topic>()
useEffect(() => {
dbService.fetchTopic(topicId).then(setTopic)
}, [topicId])
return topic
}
3.2 统一 useTopicMessages
// src/renderer/src/hooks/useTopicMessages.ts
export const useTopicMessages = (topicId: string) => {
const messages = useAppSelector(state => selectMessagesForTopic(state, topicId))
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(loadTopicMessagesThunk(topicId))
}, [topicId, dispatch])
return messages // 无需区分数据源
}
Phase 4: UI 组件复用
4.1 直接使用 Messages 组件
- 删除
AgentSessionMessages.tsx - 在 Agent 会话页面直接使用
Messages组件
4.2 轻量化 AgentSessionInputbar
// src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx
const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
const topicId = buildAgentSessionTopicId(sessionId)
const assistant = deriveAssistantFromAgent(agentId) // 从 agent 派生 assistant
const topic = useTopic(topicId) // 使用统一 hook
return <Inputbar assistant={assistant} topic={topic} />
}
Phase 5: 测试和迁移
5.1 单元测试
- DbService 路由逻辑测试
- DexieMessageDataSource CRUD 测试
- AgentMessageDataSource CRUD 测试
- 数据格式兼容性测试
5.2 集成测试
- 普通聊天全流程
- Agent 会话全流程
- 消息编辑/删除
- 分支功能
- 流式响应
5.3 性能测试
- 大量消息加载
- 内存占用
- 响应延迟
优势分析
代码精简度
- 组件层: 减少 ~500 行(删除 AgentSessionMessages)
- Thunk 层: 减少 ~300 行(移除条件判断)
- 总计减少: ~40% 重复代码
架构优势
- 单一职责: 数据访问逻辑完全独立
- 开闭原则: 新增数据源只需实现接口
- 依赖倒置: 高层模块不依赖具体实现
- 接口隔离: 清晰的 API 边界
维护性提升
- 统一的数据访问接口
- 减少条件判断分支
- 便于单元测试
- 易于调试和追踪
风险控制
潜在风险
- 数据一致性: 确保两种数据源的数据格式一致
- 性能开销: 门面层可能带来轻微性能损失(<5ms)
- 缓存策略: Agent 数据不应缓存到本地数据库
缓解措施
- 添加数据格式验证层
- 使用轻量级代理,避免过度抽象
- 在 DbService 层明确缓存策略
实施建议
渐进式迁移
- Week 1: 实现数据访问层,不改动现有代码
- Week 2: 逐个迁移 thunk 函数,保持向后兼容
- Week 3: 统一组件层,充分测试
回滚策略
- 保留原有代码分支
- 通过 feature flag 控制新旧实现切换
- 分阶段灰度发布
总结
这个方案通过门面模式和统一的数据访问接口,实现了普通聊天和 Agent 会话的完全统一,大幅减少了代码重复,提升了系统的可维护性和可扩展性。