Remove TODO.md and update DbService file count interface

- Delete TODO.md as the unified data layer implementation is complete
- Remove unsupported deleteIfZero parameter from updateFileCountV2 call
This commit is contained in:
suyao 2025-09-22 23:08:08 +08:00
parent a0cab3341e
commit c872707791
No known key found for this signature in database
2 changed files with 3 additions and 519 deletions

518
TODO.md
View File

@ -1,518 +0,0 @@
# 统一 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<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
```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<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删除操作重构
删除操作相对独立,风险可控。
##### 需要重构的函数
```typescript
// 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复杂写入操作重构
这批包含最复杂的写入逻辑,需要特别注意。
##### 需要重构的函数
```typescript
// 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更新操作重构
更新操作通常涉及消息编辑、状态更新等。
##### 需要重构的函数
```typescript
// 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
```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<Topic>()
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<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% 重复代码
### 架构优势
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 会话的完全统一,大幅减少了代码重复,提升了系统的可维护性和可扩展性。

View File

@ -87,7 +87,9 @@ export const updateFileCountV2 = async (
deleteIfZero: boolean = false
): Promise<void> => {
try {
await dbService.updateFileCount(fileId, delta, deleteIfZero)
// DbService.updateFileCount only accepts fileId and delta
// deleteIfZero parameter is not currently supported in DbService
await dbService.updateFileCount(fileId, delta)
logger.info('Updated file count', { fileId, delta, deleteIfZero })
} catch (error) {
logger.error('Failed to update file count:', { fileId, delta, error })