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 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 发送者名称 |
mes | string | 消息内容 |
is_user | boolean | 是否为用户消息 |
is_system | boolean | 是否为系统消息 |
send_date | MessageTimestamp | 发送时间戳 |
gen_started | MessageTimestamp | 生成开始时间 |
gen_finished | MessageTimestamp | 生成结束时间 |
swipes | string[] | 滑动/改写选项 |
swipe_info | SwipeInfo[] | 滑动详细信息 |
swipe_id | number | 当前滑动索引 |
extra | ChatMessageExtra | 扩展数据 |
ChatMetadata 字段
| 字段 | 类型 | 说明 |
|---|---|---|
tainted | boolean | 聊天是否被标记为 "tainted"(有问题的) |
integrity | string | 完整性校验标识 |
scenario | string | 场景名称 |
persona | string | 使用的角色人格 |
variables | Object | 局部变量存储 |
timedWorldInfo | Object | 定时世界信息效果 |
ChatMessageExtra 扩展字段
| 字段 | 类型 | 说明 |
|---|---|---|
api | string | 使用的 API 提供商 |
model | string | 使用的 AI 模型 |
token_count | number | Token 数量统计 |
tool_invocations | ToolInvocation[] | 工具调用记录 |
files | FileAttachment[] | 文件附件 |
media | MediaAttachment[] | 媒体附件(图片/视频) |
display_text | string | 显示文本(可能不同于 mes) |
swipeable | boolean | 是否允许滑动/改写 |
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 | 方法 | 功能 |
|---|---|---|
/save | POST | 保存聊天数据 |
/get | POST | 获取聊天数据 |
/delete | POST | 删除聊天文件 |
/rename | POST | 重命名聊天文件 |
/export | POST | 导出聊天 |
/import | POST | 导入外部格式聊天 |
/search | POST | 搜索聊天内容 |
/recent | POST | 获取最近的聊天 |
/group/get | POST | 获取群组聊天 |
/group/save | POST | 保存群组聊天 |
/group/delete | POST | 删除群组聊天 |
/group/import | POST | 导入群组聊天 |
关键函数
// 保存聊天(带完整性检查)
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.enabled | true | 是否启用聊天备份 |
backups.chat.maxTotalBackups | -1 | 最大备份总数(-1 表示无限制) |
backups.chat.throttleInterval | 10000 | 备份节流间隔(毫秒) |
backups.chat.checkIntegrity | true | 是否启用完整性检查 |
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 | 删除群组及其聊天 |
特殊处理
- 多聊天支持:一个群组可以有多个聊天历史(
chats数组) - 成员管理:支持启用/禁用特定成员
- 生成模式:控制多角色响应方式
- 自动模式:支持自动延迟生成
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 支持的导入格式
| 格式 | 标识 | 来源平台 |
|---|---|---|
| JSONL | jsonl | SillyTavern 原生 |
| Ooba | json (data_visible) | oobabooga Text Generation WebUI |
| Agnai | json (messages) | Agnai Chat |
| CAI | json (histories) | Character.AI Tools |
| Kobold Lite | json (savedsettings) | KoboldAI Lite |
| RisuAI | json (type: risuChat) | RisuAI |
| Chub Chat | jsonl | Chub 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: truename->name(优先使用)data->mestime(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 8601 | 2021-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 的聊天系统设计具有以下特点:
- JSONL 格式:便于流式处理、追加写入和版本控制
- 原子写入:使用 write-file-atomic 确保数据完整性
- 完整性检查:防止并发编辑导致的数据覆盖
- 智能备份:节流控制避免频繁备份,支持配置化管理
- 多格式导入:支持主流 AI 聊天平台的格式转换
- 群组支持:独立存储群组元数据和聊天内容
- 统计追踪:详细的生成时间、字数和滑动统计
文档生成时间:2026-02-01基于 SillyTavern 源代码分析