# Swipe 持久化修复说明 ## 问题描述 之前的实现中,swipes 数据**没有持久化到文件**,导致: 1. 刷新页面后 swipes 丢失 2. 前端负责管理 swipes 状态(错误的设计) 3. 重roll后无法在不同会话间保留多个版本 ## 正确的架构设计 ### 职责划分 **后端(唯一数据源)**: - ✅ 负责 swipes 数据的持久化存储 - ✅ 在重roll时更新消息的 swipes 数组 - ✅ 提供完整的消息数据(包含 swipes)给前端 **前端(只读显示)**: - ✅ 从后端加载消息数据 - ✅ 检测到 swipes 存在时渲染切换按钮 - ✅ 维护当前的 swipe_id(仅用于UI显示,不持久化) --- ## 修复内容 ### 1. 后端:添加 `get_message` 方法 **文件**: `backend/services/chat_service.py` ```python def get_message(self, role_name: str, chat_name: str, floor: int) -> Dict: """获取指定楼层的消息""" # 读取文件并返回指定楼层的消息数据 # 如果不存在则返回 None ``` **作用**:在重roll时获取现有消息,以便更新其 swipes 数组。 --- ### 2. 后端:修改 `_save_messages` 函数 **文件**: `backend/api/routes/chatWsRoute.py` #### 修复前的问题 ```python # ❌ 旧代码:无论什么情况都创建新消息 chat_service.add_message(role_name, chat_name, user_message) chat_service.add_message(role_name, chat_name, ai_message) ``` #### 修复后的逻辑 ```python # ✅ 新代码:区分正常模式和重roll模式 target_floor = request_data.get("floor") is_reroll = target_floor is not None if is_reroll: # 重roll模式:更新现有消息的 swipes 数组 existing_message = chat_service.get_message(role_name, chat_name, target_floor) if existing_message: # 获取现有的 swipes existing_swipes = existing_message.get("swipes", []) current_mes = existing_message.get("mes", "") # 构建新的 swipes 数组 updated_swipes = list(existing_swipes) # 如果当前 mes 不在 swipes 中,先添加它 if current_mes and current_mes not in updated_swipes: updated_swipes.append(current_mes) # 添加新生成的内容 updated_swipes.append(ai_response) # 更新消息(持久化到文件) update_data = { "mes": ai_response, # 显示最新内容 "swipes": updated_swipes, # 更新 swipes 数组 "swipe_id": len(updated_swipes) - 1 # 自动切换到新版本 } chat_service.update_message(role_name, chat_name, target_floor, update_data) else: # 正常模式:创建新的用户消息和AI消息 chat_service.add_message(role_name, chat_name, user_message) chat_service.add_message(role_name, chat_name, ai_message) ``` --- ## 完整流程示例 ### 场景:用户进行第3次重roll #### 初始状态(文件中) ```json { "floor": 5, "mes": "这是第2次重roll的内容", "swipes": [ "这是第1次生成的内容", "这是第2次重roll的内容" ], "swipe_id": 1 } ``` #### 用户操作 1. 右键点击消息,选择"重roll" 2. 前端发送请求:`{ floor: 5, mes: "...", ... }` #### 后端处理 1. 检测到 `floor=5`,判断为重roll模式 2. 调用 `get_message(role, chat, 5)` 获取现有消息 3. 读取 swipes 数组:`["第1次", "第2次"]` 4. 将当前 mes 添加到 swipes(如果不存在) 5. 将新生成的内容添加到 swipes 6. 更新消息: ```json { "mes": "这是第3次重roll的内容", "swipes": [ "这是第1次生成的内容", "这是第2次重roll的内容", "这是第3次重roll的内容" ], "swipe_id": 2 } ``` 7. **持久化到文件** #### 前端接收 1. WebSocket 收到 complete 事件 2. 重新加载聊天历史(或增量更新) 3. 读取到 swipes 数组有3个元素 4. 渲染 swipe 控制按钮:◀ 3/3 ▶ 5. 用户可以通过按钮切换不同版本 #### 刷新页面后 1. 前端重新加载聊天历史 2. 从文件读取到完整的 swipes 数组 3. swipe 功能正常工作 ✅ --- ## 数据结构说明 ### 消息格式(JSONL文件) ```json { "id": "msg_1234567890_ai", "name": "角色名", "is_user": false, "is_system": false, "floor": 5, "sendDate": "2024-01-01T12:00:00", "mes": "当前显示的内容", "swipes": [ "第1个版本的内容", "第2个版本的内容", "第3个版本的内容" ], "swipe_id": 2, "chatId": "角色名/聊天名" } ``` ### 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | `mes` | string | 当前显示的消息内容(与 `swipes[swipe_id]` 一致) | | `swipes` | array | 所有版本的数组(持久化) | | `swipe_id` | number | 当前选中的版本索引(持久化,但前端可以临时覆盖) | --- ## 前端渲染逻辑 ### ChatBox.jsx 中的关键代码 ```javascript // 确定当前显示的消息内容 let currentMes = message.mes; // 默认使用 mes let hasSwipes = message.swipes && message.swipes.length > 0; let currentSwipeIndex = message.swipe_id; if (hasSwipes) { // 如果用户已经手动切换过版本,使用用户选择的版本 if (currentSwipeId[message.floor] !== undefined) { currentSwipeIndex = currentSwipeId[message.floor]; } else { // 否则使用后端保存的 swipe_id currentSwipeIndex = message.swipe_id; } // 从 swipes 数组中读取对应版本的内容 if (currentSwipeIndex >= 0 && currentSwipeIndex < message.swipes.length) { currentMes = message.swipes[currentSwipeIndex]; } } // 渲染 swipe 控制按钮 {hasSwipes && !isUser && (
{currentSwipeIndex + 1}/{message.swipes.length}
)} ``` ### 关键点 1. **后端是权威数据源**:`message.swipes` 和 `message.swipe_id` 来自后端 2. **前端只维护临时状态**:`currentSwipeId` 只在当前会话有效,刷新后从后端重新加载 3. **合并更新**:切换 swipe 时使用 `{ ...currentSwipeId, [messageId]: newIndex }` 保留其他楼层的状态 --- ## 测试步骤 ### 1. 测试重roll持久化 1. 发送一条消息,等待AI回复 2. 右键点击AI消息,选择"重roll" 3. 重复重roll 2-3 次 4. **刷新页面** 5. 观察: - ✅ swipe 控制按钮仍然存在 - ✅ 可以切换到之前的所有版本 - ✅ 每个版本的内容正确显示 ### 2. 测试多次重roll 1. 重roll 5 次 2. 检查后端控制台日志: ``` [WebSocket] 🔄 重roll模式,更新楼层 5 的 swipes [WebSocket] 📊 Swipes 更新: 4 -> 5 [WebSocket] ✅ 楼层 5 已更新,swipes 数量: 5 ``` 3. 打开 JSONL 文件,确认 swipes 数组有 5 个元素 ### 3. 测试正常模式 1. 发送新消息(不是重roll) 2. 检查后端控制台日志: ``` [WebSocket] ➕ 正常模式,创建新消息 [WebSocket] ✅ 新消息已保存: 角色名/聊天名 ``` 3. 确认创建了两条新消息(用户消息 + AI消息) --- ## 相关文件清单 ### 后端文件 - `backend/services/chat_service.py` - 新增 `get_message()` 方法 - `backend/api/routes/chatWsRoute.py` - 修改 `_save_messages()` 函数,区分重roll和正常模式 ### 前端文件 - `frontend/src/components/Mid/ChatBox/ChatBox.jsx` - `handleSwipeChange()` - 合并更新 currentSwipeId - 渲染逻辑 - 从 swipes 数组读取内容 - `frontend/src/Store/Mid/ChatBoxSlice.jsx` - 发送消息时传递 `floor` 参数(重roll时为目标楼层) --- ## 注意事项 ### ⚠️ 重要提醒 1. **不要在前端直接修改 swipes** - swipes 的增删改必须通过后端API - 前端只负责读取和显示 2. **swipe_id 的优先级** - 后端保存的 `message.swipe_id` 是默认值 - 前端的 `currentSwipeId[floor]` 可以临时覆盖(仅当前会话) - 刷新页面后,以后端的 `swipe_id` 为准 3. **兼容性处理** - 旧消息可能没有 swipes 字段 - 代码中使用了 `existing_message.get("swipes", [])` 提供默认值 - 第一次重roll时会初始化 swipes 数组 --- ## 未来改进方向 1. **swipe 管理功能** - 删除某个版本 - 重命名版本(添加备注) - 导出特定版本 2. **性能优化** - swipes 数组过大时,考虑分页加载 - 压缩存储长文本 3. **用户体验** - 显示每个版本的生成时间 - 显示 Token 使用量 - 支持版本对比(diff 视图)