refactor: 重构项目结构和代码组织
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
@@ -1,57 +0,0 @@
|
||||
# =====================================================
|
||||
# Server Configuration
|
||||
# =====================================================
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
API_PREFIX=/api
|
||||
|
||||
# =====================================================
|
||||
# Database Configuration (File-based, no DB server needed)
|
||||
# =====================================================
|
||||
DB_TYPE=better-sqlite3
|
||||
DB_DATABASE=./data/db/app.db
|
||||
|
||||
# =====================================================
|
||||
# JWT Authentication
|
||||
# =====================================================
|
||||
JWT_SECRET=change-this-to-a-random-string-at-least-32-chars
|
||||
JWT_EXPIRES_IN=7d
|
||||
JWT_REFRESH_SECRET=change-this-to-another-random-string
|
||||
JWT_REFRESH_EXPIRES_IN=30d
|
||||
|
||||
# =====================================================
|
||||
# LLM Provider API Keys (Backend Only - Secure)
|
||||
# =====================================================
|
||||
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||
ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here
|
||||
GOOGLE_API_KEY=your-google-api-key-here
|
||||
LOCAL_LLM_URL=http://localhost:11434
|
||||
|
||||
# =====================================================
|
||||
# File Storage
|
||||
# =====================================================
|
||||
UPLOAD_DIR=./data/files
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# =====================================================
|
||||
# CORS Configuration
|
||||
# =====================================================
|
||||
CORS_ORIGIN=http://localhost:23338
|
||||
|
||||
# =====================================================
|
||||
# Rate Limiting
|
||||
# =====================================================
|
||||
RATE_LIMIT_TTL=60
|
||||
RATE_LIMIT_MAX=100
|
||||
|
||||
# =====================================================
|
||||
# Workflow Configuration
|
||||
# =====================================================
|
||||
WORKFLOW_MAX_PARALLEL=5
|
||||
WORKFLOW_TIMEOUT=300000
|
||||
|
||||
# =====================================================
|
||||
# Logging
|
||||
# =====================================================
|
||||
LOG_LEVEL=debug
|
||||
LOG_DIR=./data/logs
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# Development Dockerfile for Backend (NestJS)
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install wget for healthcheck
|
||||
RUN apk add --no-cache wget
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy tsconfig and other config files
|
||||
COPY tsconfig*.json ./
|
||||
COPY nest-cli.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Default command (will be overridden by docker-compose)
|
||||
CMD ["npm", "run", "start:dev"]
|
||||
@@ -1,98 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
@@ -1,35 +0,0 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
9906
server/package-lock.json
generated
9906
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^17.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
0
server/src/controllers/character.controller.ts
Normal file
0
server/src/controllers/character.controller.ts
Normal file
0
server/src/controllers/chat.controller.ts
Normal file
0
server/src/controllers/chat.controller.ts
Normal file
0
server/src/controllers/import-export.controller.ts
Normal file
0
server/src/controllers/import-export.controller.ts
Normal file
0
server/src/controllers/llm.controller.ts
Normal file
0
server/src/controllers/llm.controller.ts
Normal file
0
server/src/core/config/config.service.ts
Normal file
0
server/src/core/config/config.service.ts
Normal file
0
server/src/core/di/tokens.ts
Normal file
0
server/src/core/di/tokens.ts
Normal file
0
server/src/core/filters/http-exception.filter.ts
Normal file
0
server/src/core/filters/http-exception.filter.ts
Normal file
0
server/src/core/interceptors/logging.interceptor.ts
Normal file
0
server/src/core/interceptors/logging.interceptor.ts
Normal file
0
server/src/core/utils/helpers.ts
Normal file
0
server/src/core/utils/helpers.ts
Normal file
0
server/src/llm/ai-sdk.service.ts
Normal file
0
server/src/llm/ai-sdk.service.ts
Normal file
0
server/src/llm/llm.module.ts
Normal file
0
server/src/llm/llm.module.ts
Normal file
0
server/src/llm/providers/claude.provider.ts
Normal file
0
server/src/llm/providers/claude.provider.ts
Normal file
0
server/src/llm/providers/local.provider.ts
Normal file
0
server/src/llm/providers/local.provider.ts
Normal file
0
server/src/llm/providers/openai.provider.ts
Normal file
0
server/src/llm/providers/openai.provider.ts
Normal file
0
server/src/llm/tools/tool-registry.ts
Normal file
0
server/src/llm/tools/tool-registry.ts
Normal 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();
|
||||
|
||||
0
server/src/modules/character.module.ts
Normal file
0
server/src/modules/character.module.ts
Normal file
0
server/src/modules/chat.module.ts
Normal file
0
server/src/modules/chat.module.ts
Normal 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)}`;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
0
server/src/modules/llm.module.ts
Normal file
0
server/src/modules/llm.module.ts
Normal 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)}`;
|
||||
}
|
||||
}
|
||||
@@ -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)}`;
|
||||
}
|
||||
}
|
||||
0
server/src/persistence/file-system/file.manager.ts
Normal file
0
server/src/persistence/file-system/file.manager.ts
Normal file
0
server/src/persistence/file-system/lock.manager.ts
Normal file
0
server/src/persistence/file-system/lock.manager.ts
Normal file
0
server/src/persistence/interfaces/i-chat-repo.ts
Normal file
0
server/src/persistence/interfaces/i-chat-repo.ts
Normal file
0
server/src/persistence/persistence.module.ts
Normal file
0
server/src/persistence/persistence.module.ts
Normal file
0
server/src/services/character.service.ts
Normal file
0
server/src/services/character.service.ts
Normal file
0
server/src/services/chat.service.ts
Normal file
0
server/src/services/chat.service.ts
Normal file
0
server/src/services/data-migration.service.ts
Normal file
0
server/src/services/data-migration.service.ts
Normal file
0
server/src/services/workflow.service.ts
Normal file
0
server/src/services/workflow.service.ts
Normal file
0
server/src/tools/context-chunker.ts
Normal file
0
server/src/tools/context-chunker.ts
Normal file
0
server/src/tools/prompt-assembler.ts
Normal file
0
server/src/tools/prompt-assembler.ts
Normal file
0
server/src/tools/token-counter.ts
Normal file
0
server/src/tools/token-counter.ts
Normal file
@@ -1,29 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
0
server/test/e2e/character.e2e-spec.ts
Normal file
0
server/test/e2e/character.e2e-spec.ts
Normal file
0
server/test/e2e/chat.e2e-spec.ts
Normal file
0
server/test/e2e/chat.e2e-spec.ts
Normal file
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
0
server/test/unit/character.service.spec.ts
Normal file
0
server/test/unit/character.service.spec.ts
Normal file
0
server/test/unit/chat.service.spec.ts
Normal file
0
server/test/unit/chat.service.spec.ts
Normal file
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"paths": {
|
||||
"@shared/*": ["../shared/types/*"],
|
||||
"@core/*": ["src/core/*"],
|
||||
"@modules/*": ["src/modules/*"],
|
||||
"@common/*": ["src/common/*"],
|
||||
"@events/*": ["src/events/*"],
|
||||
"@queues/*": ["src/queues/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user