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