refactor: 重构项目结构和代码组织

This commit is contained in:
2026-04-23 23:40:28 +08:00
parent 481555c257
commit ab860c61d9
165 changed files with 51 additions and 17248 deletions

View File

@@ -1,13 +0,0 @@
// server/src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局可用,无需在每个模块导入
envFilePath: '.env', // 指定 .env 文件路径
}),
],
})
export class AppModule {}

View File

View File

View File

View File

View File

View File

View File

View File

@@ -1,10 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors(); // Allow cross-origin
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

View File

View File

View File

@@ -1,182 +0,0 @@
/**
* Chat Adapter
* Converts between SillyTavern JSONL format and internal format
*/
import type {
STChatMetadata,
STChatMessage,
STChatSession,
} from '@shared/sillytavern/chat.types';
import type {
ExtendedChatMetadata,
ExtendedChatMessage,
ExtendedChatSession,
DynamicHeader,
} from '@shared/extended/chat-ext.types';
import type { InternalChatSession } from '@shared/adapters/import-export.types';
export class ChatAdapter {
/**
* Import from SillyTavern JSONL format
*/
importFromST(jsonlContent: string): InternalChatSession {
const lines = jsonlContent.trim().split('\n');
if (lines.length === 0) {
throw new Error('Empty JSONL content');
}
// Parse metadata (first line)
const metadataLine = JSON.parse(lines[0]) as STChatMetadata;
const internalMetadata = this.convertMetadata(metadataLine);
// Parse messages (subsequent lines)
const messages: ExtendedChatMessage[] = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim()) {
const stMessage = JSON.parse(lines[i]) as STChatMessage;
messages.push(this.convertMessage(stMessage));
}
}
return {
id: this.generateId(),
characterId: this.generateId(), // Will be set by caller
metadata: internalMetadata,
messages,
createdAt: new Date(metadataLine.create_date),
updatedAt: new Date(),
};
}
/**
* Export to SillyTavern JSONL format
*/
exportToST(chat: InternalChatSession): string {
const lines: string[] = [];
// Convert metadata to ST format
const stMetadata = this.convertMetadataToST(chat.metadata);
lines.push(JSON.stringify(stMetadata));
// Convert messages to ST format
for (const message of chat.messages) {
const stMessage = this.convertMessageToST(message);
lines.push(JSON.stringify(stMessage));
}
return lines.join('\n');
}
/**
* Parse JSONL to structured ST data
*/
parseJSONL(jsonlContent: string): STChatSession {
const lines = jsonlContent.trim().split('\n');
if (lines.length === 0) {
throw new Error('Empty JSONL content');
}
const metadata = JSON.parse(lines[0]) as STChatMetadata;
const messages: STChatMessage[] = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim()) {
messages.push(JSON.parse(lines[i]) as STChatMessage);
}
}
return { metadata, messages };
}
/**
* Convert structured ST data to JSONL
*/
toJSONL(session: STChatSession): string {
const lines: string[] = [JSON.stringify(session.metadata)];
for (const message of session.messages) {
lines.push(JSON.stringify(message));
}
return lines.join('\n');
}
/**
* Convert ST metadata to internal metadata
*/
private convertMetadata(stMetadata: STChatMetadata): ExtendedChatMetadata {
return {
characterName: stMetadata.character_name,
dynamicHeaders: [], // Default empty, will be populated by user
characterDescription: '', // Will be set from character card
characterAvatar: undefined,
aiCharacterName: stMetadata.character_name,
creatorNotes: '',
firstMessages: [],
systemPrompt: '',
extensions: stMetadata.chat_metadata || {},
createdAt: new Date(stMetadata.create_date),
updatedAt: new Date(),
tags: [],
category: undefined,
};
}
/**
* Convert internal metadata to ST metadata
*/
private convertMetadataToST(metadata: ExtendedChatMetadata): STChatMetadata {
return {
user_name: 'User', // Default, can be customized
character_name: metadata.characterName,
create_date: metadata.createdAt.toISOString(),
chat_metadata: metadata.extensions,
};
}
/**
* Convert ST message to internal message
*/
private convertMessage(stMessage: STChatMessage): ExtendedChatMessage {
return {
senderName: stMessage.name,
senderRole: stMessage.is_user ? 'user' : 'assistant',
mes: stMessage.mes,
swipes: stMessage.extra?.swipes,
reasoning: stMessage.extra?.reasoning,
image: stMessage.extra?.image,
audio: stMessage.extra?.audio,
timestamp: new Date(stMessage.send_date),
tokens: undefined,
model: stMessage.extra?.model,
dynamicData: {},
};
}
/**
* Convert internal message to ST message
*/
private convertMessageToST(message: ExtendedChatMessage): STChatMessage {
return {
name: message.senderName,
is_user: message.senderRole === 'user',
send_date: message.timestamp.getTime(),
mes: message.mes,
extra: {
swipes: message.swipes,
reasoning: message.reasoning,
image: message.image,
audio: message.audio,
model: message.model,
},
};
}
/**
* Generate unique ID
*/
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -1,121 +0,0 @@
/**
* Adapter Usage Examples
* Demonstrates how to use the adapters for import/export
*/
import { ChatAdapter } from '../chat/adapters/chat.adapter';
import { WorldInfoAdapter } from '../world-info/adapters/world-info.adapter';
import { PresetAdapter } from '../preset/adapters/preset.adapter';
// ============================================
// Example 1: Chat Import/Export
// ============================================
export function chatExample() {
const adapter = new ChatAdapter();
// Sample SillyTavern JSONL content
const jsonlContent = `{"user_name":"User","character_name":"Assistant","create_date":"2024-01-01T12:00:00.000Z"}
{"name":"User","is_user":true,"send_date":1704115200000,"mes":"你好!"}
{"name":"Assistant","is_user":false,"send_date":1704115210000,"mes":"你好!有什么可以帮助你的吗?"}`;
// Import from ST format
const internalChat = adapter.importFromST(jsonlContent);
console.log('Imported chat:', internalChat.metadata.characterName);
console.log('Message count:', internalChat.messages.length);
// Export back to ST format
const exportedJsonl = adapter.exportToST(internalChat);
console.log('Exported JSONL length:', exportedJsonl.length);
}
// ============================================
// Example 2: World Info with Unified Triggers
// ============================================
export function worldInfoExample() {
const adapter = new WorldInfoAdapter();
// Sample ST World Info
const stWorldInfo = {
name: '奇幻世界',
description: '一个魔法世界',
entries: [
{
key: ['魔法', '法术'],
content: '这个世界充满了魔法能量。',
constant: false,
selective: false,
order: 1,
position: 'after_char' as const,
use_regex: false,
enabled: true,
},
{
key: [],
content: '这是一个永恒的背景设定。',
constant: true, // Permanent trigger
selective: false,
order: 0,
position: 'before_char' as const,
use_regex: false,
enabled: true,
},
],
};
// Import from ST format (converts to unified triggers)
const extendedWorldInfo = adapter.importFromST(stWorldInfo);
console.log('World Info:', extendedWorldInfo.name);
console.log('Entry 1 triggers:', extendedWorldInfo.entries[0].triggers);
// Export back to ST format (converts triggers back to keys + constant)
const exportedST = adapter.exportToST(extendedWorldInfo);
console.log('Exported entries:', exportedST.entries.length);
}
// ============================================
// Example 3: Preset with Position Anchors
// ============================================
export function presetExample() {
const adapter = new PresetAdapter();
// Sample ST Preset
const stPreset = {
name: '创意写作预设',
temp: 0.9,
top_p: 0.95,
top_k: 50,
rep_pen: 1.05,
maxContextTokens: 8192,
streaming: true,
description: '用于创意写作的预设配置',
};
// Import from ST format
const extendedPreset = adapter.importFromST(stPreset);
console.log('Preset:', extendedPreset.name);
console.log('Temperature:', extendedPreset.temperature);
console.log('Entries count:', extendedPreset.entries.length);
console.log('Entry positions:', extendedPreset.entries.map((e) => e.position));
// Export back to ST format
const exportedST = adapter.exportToST(extendedPreset);
console.log('Exported preset:', exportedST.name);
}
// ============================================
// Run examples
// ============================================
if (require.main === module) {
console.log('=== Chat Example ===');
chatExample();
console.log('\n=== World Info Example ===');
worldInfoExample();
console.log('\n=== Preset Example ===');
presetExample();
}

View File

View File

@@ -1,94 +0,0 @@
/**
* Preset Adapter
* Converts between SillyTavern preset format and extended format with position anchors
*/
import type { STPreset } from '@shared/sillytavern/preset.types';
import type { ExtendedPreset, PresetEntry, PresetPosition } from '@shared/extended/preset-ext.types';
export class PresetAdapter {
/**
* Import from SillyTavern format
*/
importFromST(stPreset: STPreset): ExtendedPreset {
return {
id: this.generateId(),
name: stPreset.name,
description: stPreset.description,
temperature: stPreset.temp || 0.7,
maxReplyLength: 500, // Default
topP: stPreset.top_p || 0.9,
topK: stPreset.top_k || 40,
repetitionPenalty: stPreset.rep_pen || 1.1,
entryOrder: [], // Will be populated by user
maxContextTokens: stPreset.maxContextTokens || 4096,
streaming: stPreset.streaming || false,
createdAt: new Date(stPreset.created_at || Date.now()),
updatedAt: new Date(stPreset.updated_at || Date.now()),
entries: this.createDefaultEntries(),
};
}
/**
* Export to SillyTavern format
*/
exportToST(extendedPreset: ExtendedPreset): STPreset {
return {
name: extendedPreset.name,
description: extendedPreset.description,
temp: extendedPreset.temperature,
top_p: extendedPreset.topP,
top_k: extendedPreset.topK,
rep_pen: extendedPreset.repetitionPenalty,
maxContextTokens: extendedPreset.maxContextTokens,
streaming: extendedPreset.streaming,
created_at: extendedPreset.createdAt.getTime(),
updated_at: extendedPreset.updatedAt.getTime(),
};
}
/**
* Create default preset entries with fixed positions
*/
private createDefaultEntries(): PresetEntry[] {
const positions: PresetPosition[] = [
'system',
'world_info_before',
'persona_description',
'char_description',
'world_info_after',
'history',
'user_input',
];
return positions.map((position, index) => ({
name: this.getPositionName(position),
context: '',
position,
order: index,
}));
}
/**
* Get human-readable name for position
*/
private getPositionName(position: PresetPosition): string {
const names: Record<PresetPosition, string> = {
system: '系统提示词',
world_info_before: '世界书(前置)',
persona_description: '角色设定描述',
char_description: '角色详细描述',
world_info_after: '世界书(后置)',
history: '聊天记录',
user_input: '用户输入',
};
return names[position];
}
/**
* Generate unique ID
*/
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -1,187 +0,0 @@
/**
* World Info Adapter
* Converts between SillyTavern format and extended format with unified triggers
*/
import type { STWorldInfo, STWorldInfoEntry } from '@shared/sillytavern/world.types';
import type {
ExtendedWorldInfo,
ExtendedWorldInfoEntry,
TriggerConfig,
} from '@shared/extended/world-ext.types';
export class WorldInfoAdapter {
/**
* Import from SillyTavern format
*/
importFromST(stWorldInfo: STWorldInfo): ExtendedWorldInfo {
return {
id: this.generateId(),
name: stWorldInfo.name,
description: stWorldInfo.description,
entries: stWorldInfo.entries.map((entry) => this.importEntryFromST(entry)),
metadata: {
entryCount: stWorldInfo.entries.length,
categories: [],
tags: [],
},
createdAt: new Date(),
updatedAt: new Date(),
};
}
/**
* Export to SillyTavern format
*/
exportToST(extendedWorldInfo: ExtendedWorldInfo): STWorldInfo {
return {
name: extendedWorldInfo.name,
description: extendedWorldInfo.description,
entries: extendedWorldInfo.entries.map((entry) => this.exportEntryToST(entry)),
metadata: {
created_at: extendedWorldInfo.createdAt.getTime(),
updated_at: extendedWorldInfo.updatedAt.getTime(),
},
};
}
/**
* Convert unified trigger config to ST format (keys + constant)
*/
triggersToST(triggers: TriggerConfig): { keys: string[]; constant: boolean } {
// If permanent trigger is enabled, set constant = true
if (triggers.permanent.enabled) {
return {
keys: [],
constant: true,
};
}
// Otherwise, use keyword triggers
if (triggers.keyword.enabled) {
return {
keys: triggers.keyword.keywords,
constant: false,
};
}
// Default: no triggers
return {
keys: [],
constant: false,
};
}
/**
* Convert ST format (keys + constant) to unified trigger config
*/
triggersFromST(keys: string[], constant: boolean): TriggerConfig {
if (constant) {
// Permanent trigger
return {
permanent: { enabled: true },
keyword: {
enabled: false,
keywords: [],
useRegex: false,
matchWholeWords: false,
caseSensitive: false,
},
rag: { enabled: false, databaseName: '' },
variable: {
enabled: false,
variableA: '',
operator: '=',
},
};
} else if (keys && keys.length > 0) {
// Keyword trigger
return {
permanent: { enabled: false },
keyword: {
enabled: true,
keywords: keys,
useRegex: false,
matchWholeWords: true,
caseSensitive: false,
},
rag: { enabled: false, databaseName: '' },
variable: {
enabled: false,
variableA: '',
operator: '=',
},
};
} else {
// No triggers
return {
permanent: { enabled: false },
keyword: {
enabled: false,
keywords: [],
useRegex: false,
matchWholeWords: false,
caseSensitive: false,
},
rag: { enabled: false, databaseName: '' },
variable: {
enabled: false,
variableA: '',
operator: '=',
},
};
}
}
/**
* Import single entry from ST format
*/
private importEntryFromST(stEntry: STWorldInfoEntry): ExtendedWorldInfoEntry {
const triggers = this.triggersFromST(stEntry.key, stEntry.constant);
return {
id: stEntry.uid?.toString() || this.generateId(),
content: stEntry.content,
position: stEntry.position,
scanDepth: stEntry.depth || stEntry.scan_depth || 4,
priority: stEntry.order,
triggers,
enabled: stEntry.enabled,
category: undefined,
tags: [],
usageCount: 0,
lastUsed: undefined,
};
}
/**
* Export single entry to ST format
*/
private exportEntryToST(entry: ExtendedWorldInfoEntry): STWorldInfoEntry {
const { keys, constant } = this.triggersToST(entry.triggers);
return {
uid: parseInt(entry.id) || undefined,
key: keys,
content: entry.content,
constant,
selective: entry.triggers.keyword.enabled && entry.triggers.keyword.keywords.length > 1,
order: entry.priority,
position: entry.position,
use_regex: entry.triggers.keyword.useRegex,
match_whole_words: entry.triggers.keyword.matchWholeWords,
case_sensitive: entry.triggers.keyword.caseSensitive,
depth: entry.scanDepth,
enabled: entry.enabled,
comment: undefined,
display_index: undefined,
};
}
/**
* Generate unique ID
*/
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

View File

View File

View File

View File

View File