Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • SillyTavern 聊天系统分析

SillyTavern 聊天系统分析

概述

SillyTavern 的聊天系统采用 JSONL(JSON Lines)格式存储,每个聊天文件包含一个 ChatHeader 作为首行,后续每一行代表一条聊天消息。系统支持个人聊天和群组聊天两种模式,并提供完善的备份、导入导出和统计功能。


1. 聊天数据结构

1.1 Mermaid 类图

1.2 JSONL 格式说明

SillyTavern 使用 JSONL(JSON Lines)格式存储聊天记录,每行一个独立的 JSON 对象:

{"chat_metadata": {...}, "user_name": "unused", "character_name": "unused"}
{"name": "User", "is_user": true, "mes": "Hello!", "send_date": "2024-01-01T12:00:00Z", "extra": {}}
{"name": "Character", "is_user": false, "mes": "Hi there!", "send_date": "2024-01-01T12:00:01Z", "extra": {}}

格式特点:

  • 每行一个有效的 JSON 对象
  • 行与行之间用换行符分隔
  • 不使用数组或对象包裹
  • 支持流式读写
  • 便于追加新消息

1.3 核心字段详解

ChatMessage 核心字段

字段类型说明
namestring发送者名称
messtring消息内容
is_userboolean是否为用户消息
is_systemboolean是否为系统消息
send_dateMessageTimestamp发送时间戳
gen_startedMessageTimestamp生成开始时间
gen_finishedMessageTimestamp生成结束时间
swipesstring[]滑动/改写选项
swipe_infoSwipeInfo[]滑动详细信息
swipe_idnumber当前滑动索引
extraChatMessageExtra扩展数据

ChatMetadata 字段

字段类型说明
taintedboolean聊天是否被标记为 "tainted"(有问题的)
integritystring完整性校验标识
scenariostring场景名称
personastring使用的角色人格
variablesObject局部变量存储
timedWorldInfoObject定时世界信息效果

ChatMessageExtra 扩展字段

字段类型说明
apistring使用的 API 提供商
modelstring使用的 AI 模型
token_countnumberToken 数量统计
tool_invocationsToolInvocation[]工具调用记录
filesFileAttachment[]文件附件
mediaMediaAttachment[]媒体附件(图片/视频)
display_textstring显示文本(可能不同于 mes)
swipeableboolean是否允许滑动/改写

2. 聊天存储系统

2.1 文件存储结构

user-directory/
├── chats/
│   └── {character_name}/
│       ├── 2024-01-01@12h00m00s.jsonl
│       ├── 2024-01-15@15h30m45s.jsonl
│       └── ...
├── groupChats/
│   ├── 1704123456789.jsonl
│   ├── 1704123459999.jsonl
│   └── ...
├── groups/
│   ├── 1704123456789.json
│   └── ...
└── backups/
    ├── chat_{chat_name}_{timestamp}.jsonl
    └── ...

2.2 chats.js 核心功能

chats.js 是聊天系统的核心 endpoint,提供以下功能:

主要 Endpoint

Endpoint方法功能
/savePOST保存聊天数据
/getPOST获取聊天数据
/deletePOST删除聊天文件
/renamePOST重命名聊天文件
/exportPOST导出聊天
/importPOST导入外部格式聊天
/searchPOST搜索聊天内容
/recentPOST获取最近的聊天
/group/getPOST获取群组聊天
/group/savePOST保存群组聊天
/group/deletePOST删除群组聊天
/group/importPOST导入群组聊天

关键函数

// 保存聊天(带完整性检查)
export async function trySaveChat(chatData, filePath, skipIntegrityCheck, handle, cardName, backupDirectory)

// 读取聊天数据
export function getChatData(chatFilePath)

// 获取聊天信息(用于列表展示)
export async function getChatInfo(pathToFile, additionalData, withMetadata)

// 检查聊天完整性
async function checkChatIntegrity(filePath, integritySlug)

安全写入机制

使用 write-file-atomic 库确保原子性写入:

import { sync as writeFileAtomicSync } from 'write-file-atomic';

// 原子写入:先写入临时文件,再重命名
writeFileAtomicSync(filePath, data, 'utf8');

3. 备份机制

3.1 备份配置

从 config.yaml 或环境变量读取配置:

const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean');
const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1, 'number'));
const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000, 'number'));
const checkIntegrity = !!getConfigValue('backups.chat.checkIntegrity', true, 'boolean');
配置项默认值说明
backups.chat.enabledtrue是否启用聊天备份
backups.chat.maxTotalBackups-1最大备份总数(-1 表示无限制)
backups.chat.throttleInterval10000备份节流间隔(毫秒)
backups.chat.checkIntegritytrue是否启用完整性检查

3.2 节流控制(Throttle)

使用 lodash 的 throttle 函数防止频繁备份:

const backupFunctions = new Map();

function getBackupFunction(handle) {
    if (!backupFunctions.has(handle)) {
        backupFunctions.set(handle, _.throttle(backupChat, throttleInterval, { 
            leading: true,  // 首次调用立即执行
            trailing: true  // 节流期结束后执行最后一次
        }));
    }
    return backupFunctions.get(handle);
}

节流策略:

  • 每个用户拥有独立的节流函数
  • 默认 10 秒间隔
  • 进程退出时刷新所有待处理的备份

3.3 完整性检查(Integrity Check)

async function checkChatIntegrity(filePath, integritySlug) {
    // 文件不存在时认为完整
    if (!fs.existsSync(filePath)) return true;
    
    // 读取首行获取完整性标识
    const firstLine = await readFirstLine(filePath);
    const jsonData = tryParse(firstLine);
    const chatIntegrity = jsonData?.chat_metadata?.integrity;
    
    // 无完整性元数据时跳过检查
    if (!chatIntegrity) return true;
    
    // 对比完整性标识
    return chatIntegrity === integritySlug;
}

完整性机制:

  • 保存时生成唯一的 integrity slug
  • 下次保存前验证 slug 是否匹配
  • 不匹配时抛出 IntegrityMismatchError
  • 支持 force 参数强制保存

3.4 备份文件命名

const backupFile = path.join(directory, `${CHAT_BACKUPS_PREFIX}${name}_${generateTimestamp()}.jsonl`);
// 示例: chat_charactername_2024-01-01T12-00-00-000Z.jsonl

3.5 backups.js 功能

backups.js 提供备份管理功能:

Endpoint功能
/chat/get获取备份列表
/chat/delete删除指定备份
/chat/download下载备份文件

4. 群组聊天系统

4.1 groups.js 核心功能

groups.js 管理群组元数据,群组聊天存储与常规聊天分离。

Group 数据结构

{
    "id": "1704123456789",
    "name": "Group Chat Name",
    "members": ["char1", "char2", "char3"],
    "disabled_members": [],
    "chat_id": "1704123456789",
    "chats": ["1704123456789", "1704123460000"],
    "generation_mode": 0,
    "activation_strategy": 0,
    "auto_mode_delay": 5,
    "allow_self_responses": false,
    "fav": false
}

Endpoint 列表

Endpoint功能
/all获取所有群组
/create创建新群组
/edit编辑群组
/delete删除群组及其聊天

特殊处理

  1. 多聊天支持:一个群组可以有多个聊天历史(chats 数组)
  2. 成员管理:支持启用/禁用特定成员
  3. 生成模式:控制多角色响应方式
  4. 自动模式:支持自动延迟生成

4.2 群组聊天存储

群组聊天文件存储在 groupChats/ 目录,以群组 ID 命名:

const chatFilePath = path.join(request.user.directories.groupChats, `${id}.jsonl`);

4.3 元数据迁移

支持从旧格式迁移群组元数据:

export async function migrateGroupChatsMetadataFormat(userDirectories)
  • 将 chat_metadata 从群组 JSON 迁移到聊天文件头部
  • 创建备份目录 _group_metadata_update
  • 保留 past_metadata 历史记录

5. 聊天导入导出

5.1 支持的导入格式

格式标识来源平台
JSONLjsonlSillyTavern 原生
Oobajson (data_visible)oobabooga Text Generation WebUI
Agnaijson (messages)Agnai Chat
CAIjson (histories)Character.AI Tools
Kobold Litejson (savedsettings)KoboldAI Lite
RisuAIjson (type: risuChat)RisuAI
Chub ChatjsonlChub Chat

5.2 格式转换逻辑

Ooba (oobabooga)

function importOobaChat(userName, characterName, jsonData) {
    // data_visible: [[user_msg, char_msg], ...]
    for (const arr of jsonData.data_visible) {
        if (arr[0]) { /* 用户消息 */ }
        if (arr[1]) { /* 角色消息 */ }
    }
}

映射关系:

  • data_visible[n][0] -> 用户消息 (is_user: true)
  • data_visible[n][1] -> 角色消息 (is_user: false)

Agnai

function importAgnaiChat(userName, characterName, jsonData) {
    // messages: [{userId?, msg}, ...]
    for (const message of jsonData.messages) {
        const isUser = !!message.userId;
        // msg -> mes
    }
}

映射关系:

  • userId 存在 -> 用户消息
  • userId 不存在 -> 角色消息
  • msg -> mes

CAI (Character.AI)

function importCAIChat(userName, characterName, jsonData) {
    // histories.histories: [{msgs: [{src: {is_human}, text}]}, ...]
    for (const history of jsonData.histories.histories) {
        for (const msg of history.msgs) {
            // src.is_human -> is_user
            // text -> mes
        }
    }
}

特点:

  • 支持多历史导入(多个聊天文件)
  • src.is_human 区分用户/角色

Kobold Lite

function importKoboldLiteChat(userName, characterName, data) {
    const inputToken = '{{[INPUT]}}';
    const outputToken = '{{[OUTPUT]}}';
    
    // actions: [msg, ...]
    // prompt 作为首条消息
    // {{[INPUT]}} 标识用户消息
    // {{[OUTPUT]}} 标识角色消息
}

映射关系:

  • savedsettings.chatname -> 用户名
  • savedsettings.chatopponent -> 角色名
  • actions[] -> 消息数组
  • prompt -> 首条消息

RisuAI

function importRisuChat(userName, characterName, jsonData) {
    // data.message: [{role, name, time, data}, ...]
    for (const message of jsonData.data.message) {
        const isUser = message.role === 'user';
        // data -> mes
        // time -> send_date (转换为 ISO 8601)
    }
}

映射关系:

  • role: 'user' -> is_user: true
  • name -> name(优先使用)
  • data -> mes
  • time(Unix 时间戳)-> send_date(ISO 8601)

Chub Chat

function flattenChubChat(userName, characterName, lines) {
    // 处理嵌套的 mes.message 结构
    // 处理 swipes 数组中的嵌套消息
    function flattenSwipe(swipe) {
        return swipe.message ? swipe.message : swipe;
    }
}

特殊处理:

  • 扁平化嵌套的 mes.message 结构
  • 处理 swipes 数组中的复杂对象

5.3 导出格式

JSONL 导出

直接返回原始文件内容:

if (request.body.format === 'jsonl') {
    const rawFile = fs.readFileSync(filename, 'utf8');
    return response.status(200).json({ result: rawFile });
}

文本导出

转换为可读文本格式:

rl.on('line', (line) => {
    const data = JSON.parse(line);
    if (data.is_system) return; // 跳过系统消息
    if (data.mes) {
        const name = data.name;
        const message = (data?.extra?.display_text || data?.mes || '').replace(/\r?\n/g, '\n');
        buffer += `${name}: ${message}\n\n`;
    }
});

6. 聊天统计系统

6.1 stats.js 核心功能

stats.js 提供聊天统计数据的收集、计算和存储。

6.2 统计数据类型

统计项说明
total_gen_time总生成时间(毫秒)
user_word_count用户消息字数
non_user_word_count非用户消息字数
user_msg_count用户消息数量
non_user_msg_count非用户消息数量
total_swipe_count滑动/改写总次数
chat_size聊天文件总大小(字节)
date_last_chat最后聊天时间
date_first_chat首次聊天时间

6.3 统计计算逻辑

function calculateTotalGenTimeAndWordCount(chatDir, chat, uniqueGenStartTimes) {
    for (let line of lines) {
        const json = JSON.parse(line);
        
        // 计算生成时间
        if (json.gen_started && json.gen_finished) {
            totalGenTime += calculateGenTime(json.gen_started, json.gen_finished);
        }
        
        // 计算字数
        if (json.mes) {
            const wordCount = countWordsInString(json.mes);
            json.is_user ? userWordCount += wordCount : nonUserWordCount += wordCount;
            json.is_user ? userMsgCount++ : nonUserMsgCount++;
        }
        
        // 计算滑动次数
        if (json.swipes && json.swipes.length > 1) {
            totalSwipeCount += json.swipes.length - 1;
        }
    }
}

6.4 时间戳解析

支持多种时间戳格式:

格式示例
Unix 时间戳1609459200
ISO 86012021-01-01T00:00:00Z
ST 人性化格式2024-07-12@01h31m37s123ms
美式日期June 19, 2023 2:20pm

6.5 统计存储

统计存储在用户目录的 stats.json 文件中:

{
    "Character1.png": {
        "total_gen_time": 123456,
        "user_word_count": 5000,
        "non_user_word_count": 15000,
        "user_msg_count": 100,
        "non_user_msg_count": 100,
        "total_swipe_count": 50,
        "chat_size": 102400,
        "date_last_chat": 1704123456789,
        "date_first_chat": 1704000000000
    },
    "timestamp": 1704123459999
}

6.6 Endpoint

Endpoint功能
/get获取当前用户的统计
/recreate重新计算所有统计
/update更新统计(从客户端)

7. 技术实现细节

7.1 文件操作安全

// 使用 sanitize-filename 防止路径遍历
import sanitize from 'sanitize-filename';
const safeFileName = sanitize(fileName);

// 使用 try-catch 包装文件操作
function tryReadFileSync(filePath) {
    try {
        return fs.readFileSync(filePath, 'utf8');
    } catch (error) {
        console.warn(`Failed to read file: ${filePath}`, error);
        return null;
    }
}

7.2 流式读取

对于大聊天文件使用流式读取:

const fileStream = fs.createReadStream(pathToFile);
const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
});

rl.on('line', (line) => {
    const jsonData = tryParse(line);
    // 处理每行数据
});

7.3 错误处理

自定义错误类型:

class IntegrityMismatchError extends Error {
    constructor(...params) {
        super(...params);
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, IntegrityMismatchError);
        }
        this.date = new Date();
    }
}

8. 总结

SillyTavern 的聊天系统设计具有以下特点:

  1. JSONL 格式:便于流式处理、追加写入和版本控制
  2. 原子写入:使用 write-file-atomic 确保数据完整性
  3. 完整性检查:防止并发编辑导致的数据覆盖
  4. 智能备份:节流控制避免频繁备份,支持配置化管理
  5. 多格式导入:支持主流 AI 聊天平台的格式转换
  6. 群组支持:独立存储群组元数据和聊天内容
  7. 统计追踪:详细的生成时间、字数和滑动统计

文档生成时间:2026-02-01基于 SillyTavern 源代码分析