Files
SillyTavern_replica/SWIPE_PERSISTENCE_FIX.md
2026-05-06 00:40:18 +08:00

8.5 KiB
Raw Blame History

Swipe 持久化修复说明

问题描述

之前的实现中swipes 数据没有持久化到文件,导致:

  1. 刷新页面后 swipes 丢失
  2. 前端负责管理 swipes 状态(错误的设计)
  3. 重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
}

用户操作

  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. 更新消息:
    {
      "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文件

{
  "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>
)}

关键点

  1. 后端是权威数据源message.swipesmessage.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 视图)