# 统一 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 接口 ```typescript // 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 appendMessage(topicId: string, message: Message, blocks: MessageBlock[]): Promise updateMessage(topicId: string, messageId: string, updates: Partial): Promise deleteMessage(topicId: string, messageId: string): Promise // 批量操作 clearMessages(topicId: string): Promise updateBlocks(blocks: MessageBlock[]): Promise } interface MessageExchange { user?: { message: Message, blocks: MessageBlock[] } assistant?: { message: Message, blocks: MessageBlock[] } } ``` #### 1.2 实现 DexieMessageDataSource ```typescript // 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 ```typescript // 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 门面 ```typescript // 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:只读操作重构(风险最低) 这批改动只涉及读取操作,不会影响数据写入,风险最低。 ##### 需要重构的函数 ```typescript // 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:辅助函数重构 这批函数不直接操作数据库,但依赖数据库操作。 ##### 需要重构的函数 ```typescript // getTopic export const getTopicV2 = async (topicId: string): Promise => { 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:删除操作重构 删除操作相对独立,风险可控。 ##### 需要重构的函数 ```typescript // deleteMessageFromDB export const deleteMessageFromDBV2 = async ( topicId: string, messageId: string ): Promise => { await dbService.deleteMessage(topicId, messageId) } // deleteMessagesFromDB export const deleteMessagesFromDBV2 = async ( topicId: string, messageIds: string[] ): Promise => { await dbService.deleteMessages(topicId, messageIds) } // clearMessagesFromDB export const clearMessagesFromDBV2 = async (topicId: string): Promise => { await dbService.clearMessages(topicId) } ``` ##### 测试清单 - [ ] 单个消息删除 - [ ] 批量消息删除 - [ ] 清空所有消息 - [ ] 文件引用计数正确更新 - [ ] Agent Session 删除操作(应为 no-op) #### 2.4 批次4:复杂写入操作重构 这批包含最复杂的写入逻辑,需要特别注意。 ##### 需要重构的函数 ```typescript // saveMessageAndBlocksToDB export const saveMessageAndBlocksToDBV2 = async ( topicId: string, message: Message, blocks: MessageBlock[] ): Promise => { // 移除 isAgentSessionTopicId 判断 await dbService.appendMessage(topicId, message, blocks) } // persistExchange export const persistExchangeV2 = async ( topicId: string, exchange: MessageExchange ): Promise => { 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:更新操作重构 更新操作通常涉及消息编辑、状态更新等。 ##### 需要重构的函数 ```typescript // updateMessage export const updateMessageV2 = async ( topicId: string, messageId: string, updates: Partial ): Promise => { await dbService.updateMessage(topicId, messageId, updates) } // updateSingleBlock export const updateSingleBlockV2 = async ( blockId: string, updates: Partial ): Promise => { await dbService.updateSingleBlock(blockId, updates) } // bulkAddBlocks export const bulkAddBlocksV2 = async (blocks: MessageBlock[]): Promise => { await dbService.bulkAddBlocks(blocks) } ``` ##### 测试清单 - [ ] 消息内容更新 - [ ] 消息状态更新 - [ ] 消息块更新 - [ ] 批量块添加 - [ ] Agent Session 更新操作(应为 no-op) #### 2.6 迁移策略 ##### 阶段1:并行运行(Week 1) ```typescript 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 回滚计划 如果出现问题,按以下步骤回滚: 1. **立即回滚**(< 5分钟) - 关闭 feature flag - 所有流量回到旧实现 2. **修复后重试** - 分析问题原因 - 修复并添加测试 - 小范围测试后重新上线 3. **彻底回滚**(如果问题严重) - 恢复到改动前的代码版本 - 重新评估方案 ### Phase 3: 统一 Hooks 层 #### 3.1 创建统一的 useTopic Hook ```typescript // src/renderer/src/hooks/useTopic.ts export const useTopic = (topicIdOrSessionId: string): Topic => { const topicId = buildTopicId(topicIdOrSessionId) // 处理映射 const [topic, setTopic] = useState() useEffect(() => { dbService.fetchTopic(topicId).then(setTopic) }, [topicId]) return topic } ``` #### 3.2 统一 useTopicMessages ```typescript // 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 ```typescript // src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx const AgentSessionInputbar: FC = ({ agentId, sessionId }) => { const topicId = buildAgentSessionTopicId(sessionId) const assistant = deriveAssistantFromAgent(agentId) // 从 agent 派生 assistant const topic = useTopic(topicId) // 使用统一 hook return } ``` ### Phase 5: 测试和迁移 #### 5.1 单元测试 - [ ] DbService 路由逻辑测试 - [ ] DexieMessageDataSource CRUD 测试 - [ ] AgentMessageDataSource CRUD 测试 - [ ] 数据格式兼容性测试 #### 5.2 集成测试 - [ ] 普通聊天全流程 - [ ] Agent 会话全流程 - [ ] 消息编辑/删除 - [ ] 分支功能 - [ ] 流式响应 #### 5.3 性能测试 - [ ] 大量消息加载 - [ ] 内存占用 - [ ] 响应延迟 ## 优势分析 ### 代码精简度 - **组件层**: 减少 ~500 行(删除 AgentSessionMessages) - **Thunk 层**: 减少 ~300 行(移除条件判断) - **总计减少**: ~40% 重复代码 ### 架构优势 1. **单一职责**: 数据访问逻辑完全独立 2. **开闭原则**: 新增数据源只需实现接口 3. **依赖倒置**: 高层模块不依赖具体实现 4. **接口隔离**: 清晰的 API 边界 ### 维护性提升 - 统一的数据访问接口 - 减少条件判断分支 - 便于单元测试 - 易于调试和追踪 ## 风险控制 ### 潜在风险 1. **数据一致性**: 确保两种数据源的数据格式一致 2. **性能开销**: 门面层可能带来轻微性能损失(<5ms) 3. **缓存策略**: Agent 数据不应缓存到本地数据库 ### 缓解措施 1. 添加数据格式验证层 2. 使用轻量级代理,避免过度抽象 3. 在 DbService 层明确缓存策略 ## 实施建议 ### 渐进式迁移 1. **Week 1**: 实现数据访问层,不改动现有代码 2. **Week 2**: 逐个迁移 thunk 函数,保持向后兼容 3. **Week 3**: 统一组件层,充分测试 ### 回滚策略 - 保留原有代码分支 - 通过 feature flag 控制新旧实现切换 - 分阶段灰度发布 ## 总结 这个方案通过门面模式和统一的数据访问接口,实现了普通聊天和 Agent 会话的完全统一,大幅减少了代码重复,提升了系统的可维护性和可扩展性。