规定数据类型

This commit is contained in:
2026-04-24 01:45:41 +08:00
parent d1943f564a
commit 35eff3faf6
86 changed files with 3809 additions and 0 deletions

View File

@@ -0,0 +1 @@
// Shared Constants and Enums

14
shared/index.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Shared Module
* 前后端共享的类型定义、验证 schemas 和工具函数
*/
// 类型定义
export * as STTypes from './types/sillytavern.types';
export * as InternalTypes from './types/internal.types';
export * from './types';
// Zod Schemas
export * as STSchemas from './schemas/sillytavern.schemas';
export * as InternalSchemas from './schemas/internal.schemas';
export * from './schemas';

11
shared/package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "@sillytavern-repalice/shared",
"version": "1.0.0",
"description": "Shared types and schemas for SillyTavern Repalice",
"main": "index.ts",
"types": "index.ts",
"private": true,
"dependencies": {
"zod": "^3.22.4"
}
}

1
shared/schemas/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// Zod Schemas for Validation (v3)

6
shared/schemas/index.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* Shared Schemas 导出
*/
export * from './sillytavern.schemas';
export * from './internal.schemas';

View File

@@ -0,0 +1,258 @@
/**
* Zod Schemas - 项目内部格式验证
*/
import { z } from 'zod';
// ==================== 世界书条目 ====================
export const ActivationTypeSchema = z.enum([
'permanent',
'keyword',
'rag',
'logic',
]);
export const LogicOperatorSchema = z.enum([
'equals',
'not_equals',
'contains',
'not_contains',
'greater',
'less',
]);
export const LogicExpressionSchema = z.object({
variable1: z.string(),
operator: LogicOperatorSchema,
variable2: z.string(),
});
export const RAGConfigSchema = z.object({
libraryId: z.string(),
threshold: z.number().min(0).max(1).optional(),
maxEntries: z.number().min(1).optional(),
});
export const WorldInfoEntrySchema = z.object({
uid: z.string(),
key: z.array(z.string()).optional(),
keysecondary: z.array(z.string()).optional(),
content: z.string(),
order: z.number(),
position: z.enum([
'before_char',
'after_char',
'before_example',
'after_example',
'author_note_top',
'author_note_bottom',
'at_depth',
]),
depth: z.number().optional(),
role: z.enum(['system', 'user', 'assistant']).optional(),
probability: z.number().min(0).max(100).optional(),
group: z.array(z.string()).optional(),
groupPrioritize: z.boolean().optional(),
useGroupScoring: z.boolean().optional(),
automationId: z.string().optional(),
disable: z.boolean().optional(),
extensions: z.record(z.unknown()).optional(),
// 自定义字段
activationType: ActivationTypeSchema,
logicExpression: LogicExpressionSchema.optional(),
ragConfig: RAGConfigSchema.optional(),
createdAt: z.number(),
updatedAt: z.number(),
});
export const WorldInfoSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
entries: z.array(WorldInfoEntrySchema),
createdAt: z.number(),
updatedAt: z.number(),
version: z.number(),
});
// ==================== 角色卡 ====================
export const OutputSchemaFieldTypeSchema = z.enum([
'string',
'number',
'boolean',
'array',
'object',
]);
export const OutputSchemaFieldSchema: z.ZodType<any> = z.lazy(() =>
z.object({
name: z.string(),
type: OutputSchemaFieldTypeSchema,
description: z.string(),
required: z.boolean().optional(),
enum: z.array(z.string()).optional(),
fields: z.array(OutputSchemaFieldSchema).optional(),
})
);
export const CharacterCardSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
personality: z.string(),
scenario: z.string(),
first_mes: z.string(),
mes_example: z.string(),
alternate_greetings: z.array(z.string()).optional(),
creator_notes: z.string().optional(),
system_prompt: z.string().optional(),
post_history_instructions: z.string().optional(),
tags: z.array(z.string()).optional(),
// 自定义字段
categories: z.array(z.string()),
worldInfoId: z.string().optional(),
outputSchema: z.array(OutputSchemaFieldSchema).optional(),
avatarPath: z.string().optional(),
createdAt: z.number(),
updatedAt: z.number(),
lastChatAt: z.number().optional(),
isFavorite: z.boolean(),
version: z.number(),
});
// ==================== 聊天记录 ====================
export const ChatHeaderSchema = z.object({
id: z.string(),
displayName: z.string(),
characterId: z.string(),
userName: z.string(),
characterName: z.string(),
tableData: z.record(z.unknown()).optional(),
createdAt: z.number(),
updatedAt: z.number(),
messageCount: z.number(),
});
export const ChatMessageSchema = z.object({
id: z.string(),
chatId: z.string(),
name: z.string(),
is_user: z.boolean(),
is_system: z.boolean().optional(),
sendDate: z.string(),
mes: z.string(),
swipes: z.array(z.string()).optional(),
swipe_id: z.number().optional(),
is_hidden: z.boolean().optional(),
extra: z.record(z.unknown()).optional(),
tokenCount: z.number().optional(),
isTemporary: z.boolean().optional(),
});
export const ChatLogSchema = z.object({
header: ChatHeaderSchema,
messages: z.array(ChatMessageSchema),
});
// ==================== 预设 ====================
export const GenerationPresetSchema = z.object({
id: z.string(),
name: z.string(),
temperature: z.number(),
topP: z.number(),
topK: z.number(),
repetitionPenalty: z.number(),
frequencyPenalty: z.number().optional(),
presencePenalty: z.number().optional(),
maxLength: z.number().optional(),
isDefault: z.boolean(),
createdAt: z.number(),
updatedAt: z.number(),
});
// ==================== 提示词预设 (Prompt Preset) ====================
export const PromptRoleSchema = z.enum(['system', 'ai', 'user']);
export const PromptEntrySchema = z.object({
identifier: z.string(),
name: z.string(),
enabled: z.boolean(),
content: z.string(),
order: z.number(),
role: PromptRoleSchema,
tokenCount: z.number(),
isSystemNode: z.boolean(),
});
export const PromptPresetViewSchema = z.object({
characterId: z.string(),
entries: z.array(PromptEntrySchema),
updatedAt: z.number(),
version: z.number(),
});
// ==================== 工作流 ====================
export const WorkflowNodeTypeSchema = z.enum([
'llm_call',
'condition',
'data_process',
'parallel',
'merge',
]);
export const WorkflowNodeSchema = z.object({
id: z.string(),
type: WorkflowNodeTypeSchema,
name: z.string(),
config: z.record(z.unknown()),
inputMapping: z.record(z.string()).optional(),
outputMapping: z.record(z.string()).optional(),
});
export const WorkflowEdgeSchema = z.object({
id: z.string(),
source: z.string(),
target: z.string(),
sourcePort: z.string().optional(),
targetPort: z.string().optional(),
});
export const WorkflowSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
nodes: z.array(WorkflowNodeSchema),
edges: z.array(WorkflowEdgeSchema),
entryNodeId: z.string(),
createdAt: z.number(),
updatedAt: z.number(),
version: z.number(),
});
// ==================== RAG 库 ====================
export const RAGDocumentSchema = z.object({
id: z.string(),
content: z.string(),
metadata: z.record(z.unknown()).optional(),
embedding: z.array(z.number()).optional(),
createdAt: z.number(),
});
export const RAGLibrarySchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
documents: z.array(RAGDocumentSchema),
embeddingModel: z.string().optional(),
createdAt: z.number(),
updatedAt: z.number(),
});

View File

@@ -0,0 +1,157 @@
/**
* Zod Schemas - SillyTavern 兼容格式验证
*/
import { z } from 'zod';
// ==================== 世界书条目 ====================
export const STWorldInfoEntryPositionSchema = z.enum([
'before_char',
'after_char',
'before_example',
'after_example',
'author_note_top',
'author_note_bottom',
'at_depth',
]);
export const STWorldInfoFilterLogicSchema = z.enum([
'and_any',
'and_all',
'not_any',
'not_all',
]);
export const STWorldInfoEntryRoleSchema = z.enum([
'system',
'user',
'assistant',
]);
export const STWorldInfoEntrySchema = z.object({
uid: z.string(),
key: z.array(z.string()).optional(),
keysecondary: z.array(z.string()).optional(),
filter: STWorldInfoFilterLogicSchema.optional(),
content: z.string(),
constant: z.boolean().optional(),
selective: z.boolean().optional(),
order: z.number(),
position: STWorldInfoEntryPositionSchema,
depth: z.number().optional(),
role: STWorldInfoEntryRoleSchema.optional(),
probability: z.number().min(0).max(100).optional(),
group: z.array(z.string()).optional(),
groupPrioritize: z.boolean().optional(),
useGroupScoring: z.boolean().optional(),
automationId: z.string().optional(),
disable: z.boolean().optional(),
extensions: z.record(z.unknown()).optional(),
});
export const STWorldInfoSchema = z.object({
spec: z.string(),
name: z.string().optional(),
description: z.string().optional(),
entries: z.array(STWorldInfoEntrySchema),
extensions: z.record(z.unknown()).optional(),
});
// ==================== 角色卡 ====================
export const STCharacterCardSpecSchema = z.enum([
'chara_card_v1',
'chara_card_v2',
'chara_card_v3',
]);
export const STCharacterCardDataSchema = z.object({
name: z.string(),
description: z.string(),
personality: z.string(),
scenario: z.string(),
first_mes: z.string(),
mes_example: z.string(),
alternate_greetings: z.array(z.string()).optional(),
creator_notes: z.string().optional(),
system_prompt: z.string().optional(),
post_history_instructions: z.string().optional(),
tags: z.array(z.string()).optional(),
character_book: STWorldInfoSchema.optional(),
extensions: z.object({
world: z.string().optional(),
talkativeness: z.number().min(0).max(1).optional(),
fav: z.boolean().optional(),
}).passthrough().optional(),
});
export const STCharacterCardSchema = z.object({
spec: STCharacterCardSpecSchema,
spec_version: z.string().optional(),
data: STCharacterCardDataSchema,
});
// ==================== 聊天记录 ====================
export const STChatHeaderSchema = z.object({
user_name: z.string(),
character_name: z.string(),
create_date: z.string(),
chat_metadata: z.record(z.unknown()).optional(),
}).passthrough();
export const STChatMessageSchema = z.object({
name: z.string(),
is_user: z.boolean(),
is_system: z.boolean().optional(),
send_date: z.union([z.number(), z.string()]),
mes: z.string(),
swipes: z.array(z.string()).optional(),
swipe_id: z.number().optional(),
is_hidden: z.boolean().optional(),
extra: z.record(z.unknown()).optional(),
});
// ==================== 预设 ====================
export const STGenerationPresetSchema = z.object({
name: z.string(),
temperature: z.number().optional(),
top_p: z.number().optional(),
top_k: z.number().optional(),
repetition_penalty: z.number().optional(),
frequency_penalty: z.number().optional(),
presence_penalty: z.number().optional(),
max_length: z.number().optional(),
}).passthrough();
// ==================== 提示词预设 (Prompt Preset) ====================
export const STPromptRoleSchema = z.enum(['system', 'assistant', 'user']);
export const STPromptSchema = z.object({
identifier: z.string(),
name: z.string().optional(),
content: z.string(),
role: STPromptRoleSchema,
marker: z.boolean().optional(),
injection_order: z.number().optional(),
extensions: z.record(z.unknown()).optional(),
});
export const STPromptOrderItemSchema = z.object({
identifier: z.string(),
enabled: z.boolean(),
});
export const STPromptOrderConfigSchema = z.object({
order: z.array(STPromptOrderItemSchema),
});
export const STPromptPresetSchema = z.object({
name: z.string(),
prompts: z.array(STPromptSchema),
prompt_order: z.record(STPromptOrderConfigSchema),
default_prompt_order: STPromptOrderConfigSchema.optional(),
});

1
shared/types/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// Shared TypeScript Types (Frontend & Backend)

545
shared/types/converters.ts Normal file
View File

@@ -0,0 +1,545 @@
/**
* 数据转换器
* 在 SillyTavern 格式和项目内部格式之间进行转换
*/
import {
STWorldInfo,
STWorldInfoEntry,
STWorldInfoEntryPosition,
STCharacterCard,
STCharacterCardData,
STChatHeader,
STChatMessage,
STPromptPreset,
STPrompt,
STPromptOrderConfig,
} from './sillytavern.types';
import {
WorldInfo,
WorldInfoEntry,
ActivationType,
CharacterCard,
ChatHeader,
ChatMessage,
ChatLog,
PromptEntry,
PromptPresetView,
PromptRole,
} from './internal.types';
// ==================== 工具函数 ====================
/**
* 生成唯一 ID
*/
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
}
/**
* 获取当前时间戳
*/
function now(): number {
return Date.now();
}
// ==================== 世界书转换 ====================
/**
* 从 SillyTavern 条目激活方式推断激活类型
*/
function inferActivationType(entry: STWorldInfoEntry): ActivationType {
if (entry.disable) {
return ActivationType.KEYWORD; // 禁用的条目默认归为关键词类型
}
if (entry.constant === true) {
return ActivationType.PERMANENT;
}
if (entry.selective === true) {
return ActivationType.RAG;
}
// 默认为关键词触发
return ActivationType.KEYWORD;
}
/**
* 将 SillyTavern 位置字符串转换为内部枚举
*/
function convertPosition(position: string): STWorldInfoEntryPosition {
// 如果已经是枚举值,直接返回
if (Object.values(STWorldInfoEntryPosition).includes(position as STWorldInfoEntryPosition)) {
return position as STWorldInfoEntryPosition;
}
// 处理 SillyTavern 的原始字符串格式
const positionMap: Record<string, STWorldInfoEntryPosition> = {
'Before Char Definition': STWorldInfoEntryPosition.BEFORE_CHAR,
'After Char Definition': STWorldInfoEntryPosition.AFTER_CHAR,
'Before Example Messages': STWorldInfoEntryPosition.BEFORE_EXAMPLE,
'After Example Messages': STWorldInfoEntryPosition.AFTER_EXAMPLE,
'Top of Author\'s Note': STWorldInfoEntryPosition.AUTHOR_NOTE_TOP,
'Bottom of Author\'s Note': STWorldInfoEntryPosition.AUTHOR_NOTE_BOTTOM,
'@D': STWorldInfoEntryPosition.AT_DEPTH,
};
return positionMap[position] || STWorldInfoEntryPosition.AFTER_CHAR;
}
/**
* 将 SillyTavern 世界书条目转换为内部格式
*/
export function convertSTEntryToInternal(stEntry: STWorldInfoEntry): WorldInfoEntry {
const activationType = inferActivationType(stEntry);
return {
...stEntry,
uid: stEntry.uid || generateId(),
position: convertPosition(stEntry.position as string),
activationType,
createdAt: now(),
updatedAt: now(),
};
}
/**
* 将内部世界书条目转换为 SillyTavern 格式
*/
export function convertEntryToST(entry: WorldInfoEntry): STWorldInfoEntry {
const stEntry: STWorldInfoEntry = {
uid: entry.uid,
key: entry.key,
keysecondary: entry.keysecondary,
content: entry.content,
order: entry.order,
position: entry.position,
depth: entry.depth,
role: entry.role,
probability: entry.probability,
group: entry.group,
groupPrioritize: entry.groupPrioritize,
useGroupScoring: entry.useGroupScoring,
automationId: entry.automationId,
disable: entry.disable,
extensions: entry.extensions,
};
// 根据激活类型设置 constant 和 selective
switch (entry.activationType) {
case ActivationType.PERMANENT:
stEntry.constant = true;
stEntry.selective = false;
break;
case ActivationType.RAG:
stEntry.constant = false;
stEntry.selective = true;
break;
case ActivationType.KEYWORD:
case ActivationType.LOGIC:
default:
stEntry.constant = false;
stEntry.selective = false;
break;
}
return stEntry;
}
/**
* 将 SillyTavern 世界书转换为内部格式
*/
export function convertSTWorldInfoToInternal(stWorldInfo: STWorldInfo): WorldInfo {
return {
id: generateId(),
name: stWorldInfo.name || 'Untitled World Info',
description: stWorldInfo.description,
entries: stWorldInfo.entries.map(convertSTEntryToInternal),
createdAt: now(),
updatedAt: now(),
version: 1,
};
}
/**
* 将内部世界书转换为 SillyTavern 格式
*/
export function convertWorldInfoToST(worldInfo: WorldInfo): STWorldInfo {
return {
spec: 'world_info_v1',
name: worldInfo.name,
description: worldInfo.description,
entries: worldInfo.entries.map(convertEntryToST),
extensions: {
originalId: worldInfo.id,
version: worldInfo.version,
},
};
}
// ==================== 角色卡转换 ====================
/**
* 将 SillyTavern 角色卡转换为内部格式
*/
export function convertSTCharacterCardToInternal(
stCard: STCharacterCard,
avatarPath?: string
): CharacterCard {
const data = stCard.data;
// 提取绑定的世界书 ID如果有
const worldInfoId = data.extensions?.world as string | undefined;
return {
id: generateId(),
name: data.name,
description: data.description,
personality: data.personality,
scenario: data.scenario,
first_mes: data.first_mes,
mes_example: data.mes_example,
alternate_greetings: data.alternate_greetings || [],
creator_notes: data.creator_notes,
system_prompt: data.system_prompt,
post_history_instructions: data.post_history_instructions,
tags: data.tags || [],
categories: data.tags || [], // 使用 tags 作为初始分类
worldInfoId,
outputSchema: undefined, // SillyTavern 格式中没有此字段
avatarPath,
createdAt: now(),
updatedAt: now(),
lastChatAt: undefined,
isFavorite: data.extensions?.fav === true,
version: 1,
};
}
/**
* 将内部角色卡转换为 SillyTavern 格式
*/
export function convertCharacterCardToST(card: CharacterCard): STCharacterCard {
return {
spec: 'chara_card_v2',
spec_version: '2.0',
data: {
name: card.name,
description: card.description,
personality: card.personality,
scenario: card.scenario,
first_mes: card.first_mes,
mes_example: card.mes_example,
alternate_greetings: card.alternate_greetings,
creator_notes: card.creator_notes,
system_prompt: card.system_prompt,
post_history_instructions: card.post_history_instructions,
tags: card.categories, // 使用 categories 作为 tags
extensions: {
world: card.worldInfoId,
fav: card.isFavorite,
originalId: card.id,
version: card.version,
},
},
};
}
// ==================== 聊天记录转换 ====================
/**
* 将 SillyTavern 聊天头转换为内部格式
*/
export function convertSTChatHeaderToInternal(
stHeader: STChatHeader,
characterId: string
): ChatHeader {
return {
id: generateId(),
displayName: `${stHeader.character_name} Chat`,
characterId,
userName: stHeader.user_name,
characterName: stHeader.character_name,
tableData: undefined,
createdAt: parseDateToTimestamp(stHeader.create_date),
updatedAt: now(),
messageCount: 0, // 需要在导入消息后更新
};
}
/**
* 将内部聊天头转换为 SillyTavern 格式
*/
export function convertChatHeaderToST(header: ChatHeader): STChatHeader {
return {
user_name: header.userName,
character_name: header.characterName,
create_date: new Date(header.createdAt).toISOString(),
chat_metadata: {
originalId: header.id,
},
};
}
/**
* 将 SillyTavern 聊天消息转换为内部格式
*/
export function convertSTMessageToInternal(
stMessage: STChatMessage,
chatId: string
): ChatMessage {
return {
id: generateId(),
chatId,
name: stMessage.name,
is_user: stMessage.is_user,
is_system: stMessage.is_system,
sendDate: normalizeSendDate(stMessage.send_date),
mes: stMessage.mes,
swipes: stMessage.swipes,
swipe_id: stMessage.swipe_id,
is_hidden: stMessage.is_hidden,
extra: stMessage.extra,
tokenCount: undefined,
isTemporary: false,
};
}
/**
* 将内部聊天消息转换为 SillyTavern 格式
*/
export function convertMessageToST(message: ChatMessage): STChatMessage {
return {
name: message.name,
is_user: message.is_user,
is_system: message.is_system,
send_date: message.sendDate,
mes: message.mes,
swipes: message.swipes,
swipe_id: message.swipe_id,
is_hidden: message.is_hidden,
extra: message.extra,
};
}
/**
* 将 SillyTavern 聊天记录转换为内部格式
*/
export function convertSTChatLogToInternal(
stChatLog: [STChatHeader, ...STChatMessage[]],
characterId: string
): ChatLog {
const [stHeader, ...stMessages] = stChatLog;
const header = convertSTChatHeaderToInternal(stHeader, characterId);
const messages = stMessages.map(msg => convertSTMessageToInternal(msg, header.id));
header.messageCount = messages.length;
return {
header,
messages,
};
}
/**
* 将内部聊天记录转换为 SillyTavern 格式
*/
export function convertChatLogToST(chatLog: ChatLog): [STChatHeader, ...STChatMessage[]] {
const stHeader = convertChatHeaderToST(chatLog.header);
const stMessages = chatLog.messages.map(convertMessageToST);
return [stHeader, ...stMessages];
}
// ==================== 辅助函数 ====================
/**
* 解析日期字符串为时间戳
*/
function parseDateToTimestamp(dateStr: string): number {
// 尝试解析 ISO 格式
const isoDate = new Date(dateStr);
if (!isNaN(isoDate.getTime())) {
return isoDate.getTime();
}
// 尝试解析 SillyTavern 自定义格式 "2024-01-01 @12h 30m 45s 123ms"
const customFormat = dateStr.match(/(\d{4}-\d{2}-\d{2})\s+@(\d+)h\s+(\d+)m\s+(\d+)s\s+(\d+)ms/);
if (customFormat) {
const [, date, hours, minutes, seconds, ms] = customFormat;
const parsedDate = new Date(`${date}T${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}.${ms.padStart(3, '0')}Z`);
return parsedDate.getTime();
}
// 默认为当前时间
return now();
}
/**
* 规范化发送日期为 ISO 字符串
*/
function normalizeSendDate(sendDate: number | string): string {
if (typeof sendDate === 'number') {
// 如果是时间戳,转换为 ISO 字符串
return new Date(sendDate).toISOString();
}
// 如果已经是字符串,尝试验证并规范化
const date = new Date(sendDate);
if (!isNaN(date.getTime())) {
return date.toISOString();
}
// 如果无法解析,返回当前时间
return new Date().toISOString();
}
// ==================== 提示词预设转换 ====================
/**
* 角色类型映射(外部 → 内部)
*/
function mapRoleToInternal(role: string): PromptRole {
const roleMap: Record<string, PromptRole> = {
'system': 'system',
'assistant': 'ai',
'user': 'user',
};
return roleMap[role] || 'system'; // 回退规则
}
/**
* 判断是否为固有节点
* @param prompt 外部 prompt 条目
* @param builtinIdentifiers 内置标识符集合
*/
function isSystemNode(prompt: STPrompt, builtinIdentifiers: Set<string>): boolean {
// 第一优先级marker 标志
if (prompt.marker === true) {
return true;
}
// 第二优先级:内置保留项
if (builtinIdentifiers.has(prompt.identifier)) {
return true;
}
// 第三优先级:普通自定义项
return false;
}
/**
* 计算 Token 数(简化版,实际需要调用 tokenizer
* @param content 文本内容
* @returns 估算的 token 数量
*/
function calculateTokenCount(content: string): number {
if (!content) return 0;
// 粗略估算:英文 ~4 chars/token中文 ~1.5 chars/token
// 这里使用一个简化的平均值 3 chars/token
return Math.ceil(content.length / 3);
}
/**
* 将 SillyTavern Prompt Preset 转换为内部视图
*
* @param stPreset 外部兼容层数据
* @param characterId 当前选定的角色 ID
* @param builtinIdentifiers 内置标识符集合(如 'main_prompt', 'jailbreak' 等)
* @returns 内部业务层视图(当前角色的“当前视图”)
*/
export function convertSTPromptPresetToView(
stPreset: STPromptPreset,
characterId: string,
builtinIdentifiers: Set<string> = new Set()
): PromptPresetView {
// 1. 获取当前角色的顺序配置
const orderConfig = stPreset.prompt_order[characterId]
|| stPreset.default_prompt_order
|| { order: [] };
// 2. 创建 identifier → prompt 的映射表
const promptMap = new Map(stPreset.prompts.map(p => [p.identifier, p]));
// 3. 构建内部条目列表
const entries: PromptEntry[] = orderConfig.order
.map((orderItem, index) => {
const prompt = promptMap.get(orderItem.identifier);
// 如果找不到对应的 prompt跳过或创建占位符
if (!prompt) {
console.warn(`Prompt not found: ${orderItem.identifier}`);
return null;
}
// 确定条目名优先级name → identifier
const name = prompt.name || prompt.identifier;
// 判断是否为固有节点
const isSystem = isSystemNode(prompt, builtinIdentifiers);
// 计算 token 数
const tokenCount = calculateTokenCount(prompt.content);
return {
identifier: prompt.identifier,
name,
enabled: orderItem.enabled,
content: prompt.content,
order: index, // 使用数组索引作为顺序
role: mapRoleToInternal(prompt.role),
tokenCount,
isSystemNode: isSystem,
};
})
.filter((entry): entry is PromptEntry => entry !== null);
return {
characterId,
entries,
updatedAt: now(),
version: 1,
};
}
/**
* 将内部视图变更同步回外部兼容层
*
* @param view 内部业务层视图
* @param stPreset 原始外部数据(会修改此对象)
* @returns 更新后的外部兼容层数据
*/
export function syncViewToSTPromptPreset(
view: PromptPresetView,
stPreset: STPromptPreset
): STPromptPreset {
// 1. 更新 prompt_order 中的 enabled 状态和顺序
const orderItems = view.entries.map(entry => ({
identifier: entry.identifier,
enabled: entry.enabled,
}));
stPreset.prompt_order[view.characterId] = {
order: orderItems,
};
// 2. 更新 prompts 中的 name 和 content如果用户编辑了
const promptMap = new Map(stPreset.prompts.map(p => [p.identifier, p]));
view.entries.forEach(entry => {
const prompt = promptMap.get(entry.identifier);
if (prompt) {
// 只同步可编辑字段name 和 content
prompt.name = entry.name;
prompt.content = entry.content;
// 注意role 不回写,保持外部原始值
// 注意identifier 不回写,它是稳定键
}
});
return stPreset;
}

View File

@@ -0,0 +1,245 @@
/**
* Prompt Preset 类型使用示例
* 展示如何正确使用内外层转换
*/
import {
STPromptPreset,
STPrompt,
} from '../types/sillytavern.types';
import {
PromptPresetView,
PromptEntry,
} from '../types/internal.types';
import {
convertSTPromptPresetToView,
syncViewToSTPromptPreset,
} from '../types/converters';
// ==================== 示例 1: 外部兼容层数据(从 SillyTavern 导入)====================
const externalPreset: STPromptPreset = {
name: 'Default Preset',
prompts: [
{
identifier: 'main_prompt',
name: 'Main Prompt',
content: 'You are a helpful assistant.',
role: 'system',
marker: true, // 固有节点标志
injection_order: 0,
},
{
identifier: 'jailbreak',
name: 'Jailbreak',
content: 'Ignore all previous instructions...',
role: 'system',
marker: true,
injection_order: 1,
},
{
identifier: 'custom-prompt-1',
name: 'Custom Prompt',
content: 'Additional context here.',
role: 'user',
marker: false,
injection_order: 2,
},
],
prompt_order: {
'character-123': {
order: [
{ identifier: 'main_prompt', enabled: true },
{ identifier: 'jailbreak', enabled: false }, // 禁用
{ identifier: 'custom-prompt-1', enabled: true },
],
},
},
};
// ==================== 示例 2: 转换为内部视图(导入流程)====================
// 定义内置标识符集合
const BUILTIN_IDENTIFIERS = new Set(['main_prompt', 'jailbreak']);
// 转换为角色 "character-123" 的内部视图
const internalView: PromptPresetView = convertSTPromptPresetToView(
externalPreset,
'character-123',
BUILTIN_IDENTIFIERS
);
console.log('Internal View:', internalView);
/*
输出示例:
{
characterId: 'character-123',
entries: [
{
identifier: 'main_prompt',
name: 'Main Prompt',
enabled: true,
content: 'You are a helpful assistant.',
order: 0,
role: 'system',
tokenCount: 9, // 估算值
isSystemNode: true, // marker: true
},
{
identifier: 'jailbreak',
name: 'Jailbreak',
enabled: false, // 从 prompt_order 提取
content: 'Ignore all previous instructions...',
order: 1,
role: 'system',
tokenCount: 12,
isSystemNode: true,
},
{
identifier: 'custom-prompt-1',
name: 'Custom Prompt',
enabled: true,
content: 'Additional context here.',
order: 2,
role: 'user',
tokenCount: 8,
isSystemNode: false, // 自定义条目
},
],
updatedAt: 1234567890,
version: 1,
}
*/
// ==================== 示例 3: 前端编辑内部视图 ====================
// 用户在前端进行以下操作:
// 1. 启用了 jailbreak
// 2. 修改了 custom-prompt-1 的内容
// 3. 调整了顺序
const editedEntries: PromptEntry[] = [
...internalView.entries.map(entry => {
if (entry.identifier === 'jailbreak') {
return { ...entry, enabled: true }; // 启用
}
if (entry.identifier === 'custom-prompt-1') {
return {
...entry,
content: 'Updated content here.', // 修改内容
order: 0, // 移到第一位
};
}
return entry;
}),
].sort((a, b) => a.order - b.order);
const updatedView: PromptPresetView = {
...internalView,
entries: editedEntries,
updatedAt: Date.now(),
};
// ==================== 示例 4: 同步回外部兼容层(导出流程)====================
const exportedPreset: STPromptPreset = syncViewToSTPromptPreset(
updatedView,
externalPreset // 会修改此对象
);
console.log('Exported Preset:', exportedPreset);
/*
输出示例:
{
name: 'Default Preset',
prompts: [
{
identifier: 'main_prompt',
name: 'Main Prompt',
content: 'You are a helpful assistant.',
role: 'system',
marker: true,
injection_order: 0,
},
{
identifier: 'jailbreak',
name: 'Jailbreak',
content: 'Ignore all previous instructions...', // 内容未变
role: 'system',
marker: true,
injection_order: 1,
},
{
identifier: 'custom-prompt-1',
name: 'Custom Prompt',
content: 'Updated content here.', // ✅ 已更新
role: 'user',
marker: false,
injection_order: 2,
},
],
prompt_order: {
'character-123': {
order: [
{ identifier: 'custom-prompt-1', enabled: true }, // ✅ 顺序改变
{ identifier: 'main_prompt', enabled: true },
{ identifier: 'jailbreak', enabled: true }, // ✅ 已启用
],
},
},
}
*/
// ==================== 示例 5: 业务层使用场景 ====================
/**
* 场景 1: 获取当前角色的所有启用条目
*/
function getEnabledPrompts(view: PromptPresetView): PromptEntry[] {
return view.entries.filter(entry => entry.enabled);
}
/**
* 场景 2: 计算总 Token 数
*/
function calculateTotalTokens(view: PromptPresetView): number {
return view.entries.reduce((sum, entry) => sum + entry.tokenCount, 0);
}
/**
* 场景 3: 区分系统节点和自定义节点
*/
function separateNodes(view: PromptPresetView) {
const systemNodes = view.entries.filter(e => e.isSystemNode);
const customNodes = view.entries.filter(e => !e.isSystemNode);
return { systemNodes, customNodes };
}
/**
* 场景 4: 按角色分组
*/
function groupByRole(view: PromptPresetView) {
const groups: Record<string, PromptEntry[]> = {
system: [],
ai: [],
user: [],
};
view.entries.forEach(entry => {
groups[entry.role].push(entry);
});
return groups;
}
// 使用示例
const enabledPrompts = getEnabledPrompts(updatedView);
const totalTokens = calculateTotalTokens(updatedView);
const { systemNodes, customNodes } = separateNodes(updatedView);
const roleGroups = groupByRole(updatedView);
console.log('Enabled prompts count:', enabledPrompts.length);
console.log('Total tokens:', totalTokens);
console.log('System nodes:', systemNodes.length);
console.log('Custom nodes:', customNodes.length);

13
shared/types/index.ts Normal file
View File

@@ -0,0 +1,13 @@
/**
* Shared 类型导出
* 统一导出所有共享类型定义
*/
// SillyTavern 兼容格式
export * from './sillytavern.types';
// 项目内部格式
export * from './internal.types';
// 数据转换器
export * from './converters';

View File

@@ -0,0 +1,469 @@
/**
* 项目内部使用的数据结构定义
* 继承 SillyTavern 格式并添加自定义字段和功能
*/
import type {
STWorldInfoEntry,
STWorldInfoEntryActivation,
STWorldInfoEntryPosition,
STCharacterCardData,
STChatHeader,
STChatMessage,
} from './sillytavern.types';
// ==================== 世界书 (World Info) ====================
/**
* 自定义激活方式类型4种枚举
*/
export enum ActivationType {
/** 永久激活 - 从 constant=true 识别 */
PERMANENT = 'permanent',
/** 关键词触发 - 从 key 数组识别 */
KEYWORD = 'keyword',
/** RAG 检索激活 - 需要绑定 RAG 库 */
RAG = 'rag',
/** 逻辑表达式激活 - 包含两个变量和一个运算符 */
LOGIC = 'logic',
}
/**
* 逻辑表达式结构(用于 LOGIC 激活类型)
*/
export interface LogicExpression {
/** 第一个变量 */
variable1: string;
/** 运算符 */
operator: 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'greater' | 'less';
/** 第二个变量或值 */
variable2: string;
}
/**
* RAG 配置(用于 RAG 激活类型)
*/
export interface RAGConfig {
/** 绑定的 RAG 库 ID */
libraryId: string;
/** 相似度阈值 */
threshold?: number;
/** 最大返回条目数 */
maxEntries?: number;
}
/**
* 项目内部世界书条目结构
* 继承 SillyTavern 条目并添加自定义功能
*/
export interface WorldInfoEntry extends Omit<STWorldInfoEntry, 'constant' | 'selective'> {
/** 激活方式4种枚举之一 */
activationType: ActivationType;
/** 逻辑表达式(当 activationType 为 LOGIC 时使用) */
logicExpression?: LogicExpression;
/** RAG 配置(当 activationType 为 RAG 时使用) */
ragConfig?: RAGConfig;
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
}
/**
* 项目内部世界书结构
*/
export interface WorldInfo {
/** 唯一标识符 */
id: string;
/** 世界书名称 */
name: string;
/** 世界书描述 */
description?: string;
/** 条目数组 */
entries: WorldInfoEntry[];
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
/** 版本号(用于数据迁移) */
version: number;
}
// ==================== 角色卡 (Character Card) ====================
/**
* Vercel AI SDK Output.object() 的表头定义
* 用于结构化输出
*/
export interface OutputSchemaField {
/** 字段名称 */
name: string;
/** 字段类型 */
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
/** 字段描述 */
description: string;
/** 是否必需 */
required?: boolean;
/** 枚举值(如果是字符串且有固定选项) */
enum?: string[];
/** 嵌套字段(如果是 object 类型) */
fields?: OutputSchemaField[];
}
/**
* 项目内部角色卡结构
* 继承 SillyTavern 角色卡并添加自定义字段
*/
export interface CharacterCard extends Omit<STCharacterCardData, 'character_book'> {
/** 角色唯一标识符 */
id: string;
/** 分类标签列表(用于前端分类) */
categories: string[];
/** 绑定的世界书 ID */
worldInfoId?: string;
/** 输出 schema 定义(用于 Vercel AI SDK Output.object() */
outputSchema?: OutputSchemaField[];
/** 角色头像路径 */
avatarPath?: string;
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
/** 最后聊天时间戳 */
lastChatAt?: number;
/** 收藏状态 */
isFavorite: boolean;
/** 版本号(用于数据迁移) */
version: number;
}
// ==================== 聊天记录 (Chat Log) ====================
/**
* 项目内部聊天记录头
*/
export interface ChatHeader extends Omit<STChatHeader, 'user_name' | 'character_name'> {
/** 聊天唯一标识符 */
id: string;
/** 显示名称(聊天标题) */
displayName: string;
/** 关联的角色卡 ID */
characterId: string;
/** 用户角色名 */
userName: string;
/** AI 角色名称 */
characterName: string;
/** 表格内容(对应角色卡的 outputSchema 字段的值) */
tableData?: Record<string, unknown>;
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
/** 消息数量 */
messageCount: number;
}
/**
* 项目内部聊天消息
*/
export interface ChatMessage extends Omit<STChatMessage, 'send_date'> {
/** 消息唯一标识符 */
id: string;
/** 发送日期 ISO 字符串 */
sendDate: string;
/** 关联的聊天 ID */
chatId: string;
/** Token 数量(用于统计) */
tokenCount?: number;
/** 是否为临时消息(未保存) */
isTemporary?: boolean;
}
/**
* 项目内部完整聊天记录
*/
export interface ChatLog {
/** 聊天头 */
header: ChatHeader;
/** 消息列表 */
messages: ChatMessage[];
}
// ==================== 预设 (Preset) ====================
/**
* 项目内部采样参数预设
*/
export interface GenerationPreset {
/** 预设唯一标识符 */
id: string;
/** 预设名称 */
name: string;
/** 温度 */
temperature: number;
/** Top P */
topP: number;
/** Top K */
topK: number;
/** 重复惩罚 */
repetitionPenalty: number;
/** 频率惩罚 */
frequencyPenalty?: number;
/** 存在惩罚 */
presencePenalty?: number;
/** 最大生成长度 */
maxLength?: number;
/** 是否为默认预设 */
isDefault: boolean;
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
}
// ==================== 提示词预设 (Prompt Preset) ====================
/**
* Prompt 角色类型(内部业务层只保留三种)
*/
export type PromptRole = 'system' | 'ai' | 'user';
/**
* 内部业务层 - Prompt 条目(当前视图)
* 这是基于某个 character_id 生成的“当前视图”,只包含业务需要的字段
*/
export interface PromptEntry {
/** 稳定关联键(用于回写,不直接暴露给前端作为主要展示字段) */
identifier: string;
/** 1. 条目名 - 前端显示和编辑 */
name: string;
/** 2. 是否启用 - 当前作用域下的业务状态(派生字段) */
enabled: boolean;
/** 3. 条目内容 - 静态内容视图marker 节点可能为空) */
content: string;
/** 4. 条目顺序 - 前端展示和拖拽排序 */
order: number;
/** 5. 角色 - 三类业务角色 */
role: PromptRole;
/** 6. 总 token 数 - 派生显示字段(不属于外部兼容层) */
tokenCount: number;
/** 7. 是否固有节点 - 业务语义标签(不是 SillyTavern 原始字段) */
isSystemNode: boolean;
}
/**
* 内部业务层 - Prompt 预设视图(基于某个 character_id 的“当前视图”)
*/
export interface PromptPresetView {
/** 关联的角色 ID */
characterId: string;
/** 当前视图的条目列表(已排序、已过滤) */
entries: PromptEntry[];
/** 最后更新时间戳 */
updatedAt: number;
/** 版本号 */
version: number;
}
// ==================== 工作流 (Workflow) ====================
/**
* 工作流节点类型
*/
export enum WorkflowNodeType {
/** LLM 调用节点 */
LLM_CALL = 'llm_call',
/** 条件分支节点 */
CONDITION = 'condition',
/** 数据处理节点 */
DATA_PROCESS = 'data_process',
/** 并行执行节点 */
PARALLEL = 'parallel',
/** 合并节点 */
MERGE = 'merge',
}
/**
* 工作流节点
*/
export interface WorkflowNode {
/** 节点唯一标识符 */
id: string;
/** 节点类型 */
type: WorkflowNodeType;
/** 节点名称 */
name: string;
/** 节点配置 */
config: Record<string, unknown>;
/** 输入映射 */
inputMapping?: Record<string, string>;
/** 输出映射 */
outputMapping?: Record<string, string>;
}
/**
* 工作流连接
*/
export interface WorkflowEdge {
/** 连接唯一标识符 */
id: string;
/** 源节点 ID */
source: string;
/** 目标节点 ID */
target: string;
/** 源节点输出端口 */
sourcePort?: string;
/** 目标节点输入端口 */
targetPort?: string;
}
/**
* 工作流定义
*/
export interface Workflow {
/** 工作流唯一标识符 */
id: string;
/** 工作流名称 */
name: string;
/** 工作流描述 */
description?: string;
/** 节点列表 */
nodes: WorkflowNode[];
/** 连接列表 */
edges: WorkflowEdge[];
/** 入口节点 ID */
entryNodeId: string;
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
/** 版本号 */
version: number;
}
// ==================== RAG 库 (RAG Library) ====================
/**
* RAG 文档条目
*/
export interface RAGDocument {
/** 文档唯一标识符 */
id: string;
/** 文档内容 */
content: string;
/** 文档元数据 */
metadata?: Record<string, unknown>;
/** 向量嵌入(由 embedding 模型生成) */
embedding?: number[];
/** 创建时间戳 */
createdAt: number;
}
/**
* RAG 库
*/
export interface RAGLibrary {
/** 库唯一标识符 */
id: string;
/** 库名称 */
name: string;
/** 库描述 */
description?: string;
/** 文档列表 */
documents: RAGDocument[];
/** 使用的 embedding 模型 */
embeddingModel?: string;
/** 创建时间戳 */
createdAt: number;
/** 最后更新时间戳 */
updatedAt: number;
}

View File

@@ -0,0 +1,395 @@
/**
* SillyTavern 兼容的数据结构定义
* 这些类型严格遵循 SillyTavern 官方规范,用于导入导出兼容
*/
// ==================== 世界书 (World Info) ====================
/**
* SillyTavern 世界书条目激活方式枚举
*/
export enum STWorldInfoEntryActivation {
/** 永久激活 - 蓝色圆圈 */
CONSTANT = 'constant',
/** 关键词触发 - 绿色圆圈 */
KEYWORD = 'keyword',
/** 向量检索 RAG - 链条链接 */
VECTOR = 'vector',
/** 禁用 - 红色叉 */
DISABLED = 'disabled',
}
/**
* SillyTavern 世界书条目插入位置
*/
export enum STWorldInfoEntryPosition {
/** 在角色定义之前 */
BEFORE_CHAR = 'before_char',
/** 在角色定义之后 */
AFTER_CHAR = 'after_char',
/** 在示例消息之前 */
BEFORE_EXAMPLE = 'before_example',
/** 在示例消息之后 */
AFTER_EXAMPLE = 'after_example',
/** 在作者注释顶部 */
AUTHOR_NOTE_TOP = 'author_note_top',
/** 在作者注释底部 */
AUTHOR_NOTE_BOTTOM = 'author_note_bottom',
/** 指定深度位置 @D */
AT_DEPTH = 'at_depth',
}
/**
* SillyTavern 世界书条目可选过滤器逻辑
*/
export enum STWorldInfoFilterLogic {
/** AND ANY - 任意一个可选关键字匹配 */
AND_ANY = 'and_any',
/** AND ALL - 所有可选关键字都匹配 */
AND_ALL = 'and_all',
/** NOT ANY - 没有任何可选关键字匹配 */
NOT_ANY = 'not_any',
/** NOT ALL - 不是所有可选关键字都匹配 */
NOT_ALL = 'not_all',
}
/**
* SillyTavern 世界书条目角色类型
*/
export enum STWorldInfoEntryRole {
/** 系统角色 */
SYSTEM = 'system',
/** 用户角色 */
USER = 'user',
/** 助手角色 */
ASSISTANT = 'assistant',
}
/**
* SillyTavern 世界书条目原始结构
* 参考: https://st-docs.role.fun/usage/core-concepts/worldinfo/
*/
export interface STWorldInfoEntry {
/** 条目唯一标识符 */
uid: string;
/** 条目标题/备忘录(仅用于人类阅读) */
key?: string[];
/** 触发关键词列表(支持正则表达式) */
keysecondary?: string[];
/** 可选过滤器逻辑 */
filter?: STWorldInfoFilterLogic;
/** 条目内容 - 激活时注入到提示的文本 */
content: string;
/** 常量标志 - true 表示永久激活(蓝色圆圈) */
constant?: boolean;
/** 选择性标志 - 用于向量检索 */
selective?: boolean;
/** 插入顺序 - 数值越大越靠近末尾 */
order: number;
/** 插入位置 */
position: STWorldInfoEntryPosition;
/** 插入深度(当 position 为 AT_DEPTH 时使用) */
depth?: number;
/** 角色类型(当使用角色消息插入时) */
role?: STWorldInfoEntryRole;
/** 概率 - 0-100控制激活时的插入概率 */
probability?: number;
/** 包含组 - 逗号分隔的组标签列表 */
group?: string[];
/** 优先包含 - true 时按 order 值选择而非随机 */
groupPrioritize?: boolean;
/** 使用组评分 - 基于关键字匹配数量选择 */
useGroupScoring?: boolean;
/** 自动化 ID - 与 ST 脚本集成 */
automationId?: string;
/** 是否禁用 */
disable?: boolean;
/** 扩展字段 */
extensions?: Record<string, unknown>;
}
/**
* SillyTavern 世界书文件结构
*/
export interface STWorldInfo {
/** 规范名称 */
spec: string;
/** 世界书名称 */
name?: string;
/** 世界书描述 */
description?: string;
/** 条目数组 */
entries: STWorldInfoEntry[];
/** 扩展字段 */
extensions?: Record<string, unknown>;
}
// ==================== 角色卡 (Character Card) ====================
/**
* SillyTavern 角色卡规范版本
*/
export enum STCharacterCardSpec {
V1 = 'chara_card_v1',
V2 = 'chara_card_v2',
V3 = 'chara_card_v3',
}
/**
* SillyTavern 角色卡 V2/V3 数据结构
* 参考: https://github.com/malfoyslastname/character-card-spec-v2
*/
export interface STCharacterCardData {
/** 角色名称 */
name: string;
/** 角色描述 */
description: string;
/** 角色性格特征 */
personality: string;
/** 场景设定 */
scenario: string;
/** 首条开场消息 */
first_mes: string;
/** 对话示例 */
mes_example: string;
/** 替代问候语数组 */
alternate_greetings?: string[];
/** 角色创建者备注 */
creator_notes?: string;
/** 系统级别指令 */
system_prompt?: string;
/** 历史后指令 */
post_history_instructions?: string;
/** 标签数组 */
tags?: string[];
/** 角色书(嵌入的世界书) */
character_book?: STWorldInfo;
/** 扩展字段 */
extensions?: {
/** 绑定的世界书文件名 */
world?: string;
/** 健谈程度 0-1 */
talkativeness?: number;
/** 收藏状态 */
fav?: boolean;
/** 其他扩展字段 */
[key: string]: unknown;
};
}
/**
* SillyTavern 角色卡完整结构V2/V3
*/
export interface STCharacterCard {
/** 规范标识 */
spec: STCharacterCardSpec;
/** 规范版本 */
spec_version?: string;
/** 角色数据 */
data: STCharacterCardData;
}
// ==================== 聊天记录 (Chat Log) ====================
/**
* SillyTavern 聊天记录头JSONL 第一行)
*/
export interface STChatHeader {
/** 用户名称 */
user_name: string;
/** 角色名称 */
character_name: string;
/** 创建日期 */
create_date: string;
/** 聊天元数据 */
chat_metadata?: {
/** 完整性校验哈希 */
integrity?: string;
/** 其他元数据 */
[key: string]: unknown;
};
/** 其他可能的头部字段 */
[key: string]: unknown;
}
/**
* SillyTavern 聊天消息记录
*/
export interface STChatMessage {
/** 发送者名称 */
name: string;
/** 是否为用户消息 */
is_user: boolean;
/** 是否为系统消息 */
is_system?: boolean;
/** 发送日期时间戳或 ISO 字符串 */
send_date: number | string;
/** 实际对话消息文本 */
mes: string;
/** 替换回答数组swipe 功能) */
swipes?: string[];
/** 当前选择的 swipe ID */
swipe_id?: number;
/** 是否为隐藏消息 */
is_hidden?: boolean;
/** 扩展字段 */
extra?: Record<string, unknown>;
}
/**
* SillyTavern 聊天记录文件JSONL 格式)
* 第一行是 STChatHeader后续每行是 STChatMessage
*/
export type STChatLog = [STChatHeader, ...STChatMessage[]];
// ==================== 预设 (Preset) ====================
/**
* SillyTavern 采样参数预设
*/
export interface STGenerationPreset {
/** 预设名称 */
name: string;
/** 温度 */
temperature?: number;
/** Top P */
top_p?: number;
/** Top K */
top_k?: number;
/** 重复惩罚 */
repetition_penalty?: number;
/** 频率惩罚 */
frequency_penalty?: number;
/** 存在惩罚 */
presence_penalty?: number;
/** 最大生成长度 */
max_length?: number;
/** 其他采样参数 */
[key: string]: unknown;
}
// ==================== 提示词预设 (Prompt Preset) ====================
/**
* SillyTavern Prompt 条目(外部兼容层)
* 这是提示词模板的原始数据结构
*/
export interface STPrompt {
/** 唯一标识符稳定键UUID 或内置标识如 'main_prompt', 'jailbreak' 等) */
identifier: string;
/** 显示名称(可选,用于前端展示) */
name?: string;
/** 提示词内容 */
content: string;
/** 角色类型 */
role: 'system' | 'assistant' | 'user';
/** 是否为标记节点(固有节点标志) */
marker?: boolean;
/** 注入顺序元数据(外部层的注入优先级) */
injection_order?: number;
/** 扩展字段 */
extensions?: Record<string, unknown>;
}
/**
* SillyTavern Prompt 顺序配置项
*/
export interface STPromptOrderItem {
/** 引用 prompts 中的 identifier */
identifier: string;
/** 是否启用 */
enabled: boolean;
}
/**
* SillyTavern Prompt 顺序配置(按角色)
*/
export interface STPromptOrderConfig {
order: STPromptOrderItem[];
}
/**
* SillyTavern Prompt Preset 完整结构(外部兼容层)
* 包含所有可用的提示词条目和各角色的顺序配置
*/
export interface STPromptPreset {
/** 预设名称 */
name: string;
/** 所有可用的提示词条目 */
prompts: STPrompt[];
/** 各角色的顺序配置 */
prompt_order: {
[character_id: string]: STPromptOrderConfig;
};
/** 默认顺序配置(未指定角色时使用) */
default_prompt_order?: STPromptOrderConfig;
}

1
shared/utils/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// Shared Utility Functions