Initial commit: Project structure and layout

This commit is contained in:
2026-04-22 01:45:29 +08:00
commit 481555c257
67 changed files with 17248 additions and 0 deletions

51
.dockerignore Normal file
View File

@@ -0,0 +1,51 @@
# Dependencies
node_modules/
# Build output
dist/
build/
# Environment files
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
# Data
data/
# Git
.git/
.gitignore
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Documentation
*.md
docs/
# Tests
test/
tests/
coverage/
# Misc
*.tmp
*.temp

61
.gitignore vendored Normal file
View File

@@ -0,0 +1,61 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Testing
coverage/
*.lcov
# Production builds
dist/
build/
server/dist/
client/dist/
# Environment variables
.env
.env.local
.env.*.local
!.env.example
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Data persistence
data/
*.db
*.sqlite
# Temporary files
tmp/
temp/
*.tmp
*.temp
# Docker
docker-compose.override.yml
# Secrets
*.pem
*.key
secrets/
# Shared types build output
shared/dist/

254
ARCHITECTURE_REVIEW.md Normal file
View File

@@ -0,0 +1,254 @@
# 项目架构检查报告
## ✅ 架构合理性评估
### **1. 整体结构评分9/10** ⭐⭐⭐⭐⭐
---
## 📁 目录结构分析
### **✅ 优点**
#### **1. 清晰的三层架构**
```
project/
├── server/ # 后端 (NestJS)
├── client/ # 前端 (Vue3)
├── shared/ # 共享类型系统
├── data/ # 持久化数据
├── docker/ # Docker 配置
└── docs/ # 文档
```
- ✅ 前后端分离明确
- ✅ 共享类型独立管理
- ✅ 数据和配置隔离
#### **2. 后端模块化设计 (DDD)**
```
server/src/
├── core/ # 核心基础设施层
├── modules/ # 业务模块层
├── shared/ # 共享工具层
├── events/ # 事件系统
├── queues/ # 任务队列
└── common/ # 通用基类
```
- ✅ 符合领域驱动设计
- ✅ 职责分离清晰
- ✅ 易于扩展和维护
#### **3. 模块内部结构标准**
```
modules/{module}/
├── controllers/ # API 控制器
├── services/ # 业务逻辑
├── adapters/ # 格式转换适配器
├── dto/ # 数据传输对象
└── interfaces/ # 接口定义
```
- ✅ 遵循 MVC 模式
- ✅ 适配器模式实现良好
- ✅ 依赖注入友好
#### **4. 前端分层合理**
```
client/src/
├── api/ # API 客户端层
├── components/ # UI 组件层
├── views/ # 页面视图层
├── composables/ # 组合式函数
├── stores/ # 状态管理
├── router/ # 路由配置
├── types/ # 类型定义
└── utils/ # 工具函数
```
- ✅ 符合 Vue3 最佳实践
- ✅ 关注点分离
- ✅ 可复用性强
#### **5. 共享类型系统设计**
```
shared/types/
├── sillytavern/ # ST 原生格式(兼容层)
├── extended/ # 扩展格式(增强层)
├── adapters/ # 适配器接口
└── index.ts # 统一导出
```
- ✅ 双层格式设计优秀
- ✅ 完全兼容 SillyTavern
- ✅ 扩展性强
---
## ⚠️ 需要改进的地方
### **1. 缺少配置文件**
```
❌ server/.env.example
❌ client/.env.example
❌ server/tsconfig.build.json (已有但需检查)
```
**建议:**
- 创建 `.env.example` 模板文件
- 确保所有必要的环境变量都有文档
### **2. 模块完整性不一致**
**完整的模块:**
-`chat/` - 有 adapters
-`world-info/` - 有 adapters
-`preset/` - 有 adapters
**不完整的模块:**
- ⚠️ `auth/` - 缺少 adapters
- ⚠️ `llm/` - 缺少 adapters
- ⚠️ `workflow/` - 缺少 adapters
- ⚠️ `data-store/` - 缺少 adapters
- ⚠️ `file-management/` - 缺少 adapters
- ⚠️ `import-export/` - 缺少 adapters
**建议:**
为每个模块添加标准的子目录结构,即使暂时为空。
### **3. 前端目录有空文件夹**
```
client/src/
├── composables/ # 空
├── plugins/ # 空
├── services/ # 空
└── store/ # 空(应该是 stores/
```
**建议:**
- 删除空的 `store/` 目录(已有 `stores/`
-`composables/` 中添加基础 composable
-`plugins/` 中添加必要的插件
### **4. 缺少测试目录结构**
```
tests/ # 空
```
**建议:**
```
tests/
├── unit/ # 单元测试
├── integration/ # 集成测试
└── e2e/ # 端到端测试
```
### **5. Docker 配置可以优化**
**当前配置:**
- ✅ 开发环境热重载配置正确
- ✅ 端口映射合理23337, 23338
- ✅ 卷挂载正确
**建议改进:**
- 添加 `.dockerignore` 文件
- 考虑添加 Nginx 反向代理(生产环境)
- 添加日志卷挂载
---
## 🎯 架构优势总结
### **1. 数据类型设计优秀** ⭐⭐⭐⭐⭐
- 双层格式ST + Extended
- 适配器模式实现完善
- 完全向后兼容
### **2. 模块化程度高** ⭐⭐⭐⭐⭐
- 每个模块独立
- 低耦合高内聚
- 易于团队协作
### **3. 技术栈选择合理** ⭐⭐⭐⭐⭐
- NestJS + Vue3 = 现代全栈
- TypeScript 全程类型安全
- Docker 容器化部署
### **4. 可扩展性强** ⭐⭐⭐⭐⭐
- 事件系统预留
- 任务队列预留
- 微服务演进可能
### **5. 开发体验好** ⭐⭐⭐⭐
- 热重载支持
- 路径别名配置
- 类型提示完整
---
## 📋 建议的改进清单
### **高优先级**
1. ✅ 创建 `.env.example` 文件
2. ✅ 统一模块目录结构
3. ✅ 清理空文件夹
4. ✅ 添加 `.dockerignore`
### **中优先级**
5. ⏳ 创建基础 Composables
6. ⏳ 添加单元测试框架
7. ⏳ 完善 Docker 配置
8. ⏳ 添加 CI/CD 配置
### **低优先级**
9. 📝 编写 API 文档
10. 📝 添加部署指南
11. 📝 创建贡献指南
---
## 🚀 Docker 配置说明
### **端口映射**
- 后端:`23337:3000` (宿主机 23337 → 容器 3000
- 前端:`23338:5173` (宿主机 23338 → 容器 5173
### **热重载支持**
```yaml
volumes:
- ./server:/usr/src/app # 代码挂载
- ./shared:/usr/src/shared # 共享类型挂载
- /usr/src/app/node_modules # 排除 node_modules
```
### **数据持久化**
```yaml
volumes:
- ./data:/usr/src/app/data # 数据库和文件
- ./.env:/usr/src/app/.env:ro # 环境变量(只读)
```
### **健康检查**
```yaml
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api"]
interval: 30s
timeout: 10s
retries: 3
```
---
## ✅ 总体评价
**架构评分9/10**
**优点:**
- 设计思路清晰
- 技术选型现代
- 可扩展性强
- 兼容性好
**待改进:**
- 部分模块不完整
- 缺少一些配置文件
- 测试框架未搭建
**结论:**
整体架构非常合理,具备良好的可扩展性和维护性。只需补充一些细节即可投入开发。

6
client/.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.env
.git
.idea
.vscode

37
client/.env.example Normal file
View File

@@ -0,0 +1,37 @@
# =====================================================
# API Configuration
# =====================================================
VITE_API_URL=http://localhost:23337/api
VITE_WS_URL=ws://localhost:23337
# =====================================================
# Application Configuration
# =====================================================
VITE_APP_NAME=SillyTavern Replace
VITE_APP_VERSION=1.0.0
# =====================================================
# Feature Flags
# =====================================================
VITE_ENABLE_WEBSOCKET=true
VITE_ENABLE_SSE=true
# =====================================================
# LLM Configuration (Public - Safe to Expose)
# =====================================================
VITE_DEFAULT_LLM_PROVIDER=openai
VITE_MAX_TOKENS=4096
VITE_DEFAULT_TEMPERATURE=0.7
# =====================================================
# UI Configuration
# =====================================================
VITE_THEME=light
VITE_LANGUAGE=zh-CN
VITE_ITEMS_PER_PAGE=20
# =====================================================
# Upload Configuration
# =====================================================
VITE_MAX_UPLOAD_SIZE=10485760
VITE_ALLOWED_FILE_TYPES=.json,.csv,.txt,.md,.png,.jpg

17
client/Dockerfile.dev Normal file
View File

@@ -0,0 +1,17 @@
# Development Dockerfile for Frontend (Vue3 + Vite)
FROM node:20-alpine
# Set working directory
WORKDIR /usr/src/app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Expose Vite dev server port
EXPOSE 5173
# Default command (will be overridden by docker-compose)
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]

42
client/README.md Normal file
View File

@@ -0,0 +1,42 @@
# client
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
client/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

13
client/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3148
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
client/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "client",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"pinia": "^3.0.4",
"vue": "^3.5.32",
"vue-router": "^5.0.4"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.2",
"@vitejs/plugin-vue": "^6.0.6",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4",
"typescript": "~6.0.0",
"vite": "^8.0.8",
"vite-plugin-vue-devtools": "^8.1.1",
"vue-tsc": "^3.2.6"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

41
client/src/App.vue Normal file
View File

@@ -0,0 +1,41 @@
<template>
<div class="app-layout">
<!-- 顶部工具栏 -->
<TopToolbar />
<!-- 主体内容区 -->
<div class="main-content">
<!-- 左侧面板 -->
<LeftPanel />
<!-- 中间聊天区 -->
<CenterChat />
<!-- 右侧工具面板 -->
<RightPanel />
</div>
</div>
</template>
<script setup lang="ts">
import TopToolbar from './components/layout/TopToolbar.vue';
import LeftPanel from './components/layout/LeftPanel.vue';
import CenterChat from './components/layout/CenterChat.vue';
import RightPanel from './components/layout/RightPanel.vue';
</script>
<style scoped>
.app-layout {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@@ -0,0 +1,72 @@
<template>
<div :class="['message-item', message.senderRole]">
<div class="message-header">
<span class="sender-name">{{ message.senderName }}</span>
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
</div>
<div class="message-content">{{ message.mes }}</div>
</div>
</template>
<script setup lang="ts">
interface Message {
id: string;
senderName: string;
senderRole: 'user' | 'assistant' | 'system';
mes: string;
timestamp: Date;
}
defineProps<{
message: Message;
}>();
function formatTime(date: Date) {
return new Date(date).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<style scoped>
.message-item {
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
background: #ffffff;
border: 1px solid #e0e0e0;
}
.message-item.user {
background: #e3f2fd;
border-color: #bbdefb;
margin-left: 20%;
}
.message-item.assistant {
background: #f1f8e9;
border-color: #dcedc8;
margin-right: 20%;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: #7f8c8d;
}
.sender-name {
font-weight: 600;
color: #2c3e50;
}
.message-content {
line-height: 1.6;
white-space: pre-wrap;
font-size: 14px;
color: #2c3e50;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="api-config-view">
<h3>🔌 API 配置</h3>
<p>API 配置管理界面</p>
</div>
</template>
<style scoped>
.api-config-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="gallery-view">
<h3>🖼 画廊</h3>
<p>LLM 生成的图片将显示在这里</p>
</div>
</template>
<style scoped>
.gallery-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="preset-view">
<h3> 预设管理</h3>
<p>预设配置和条目展示</p>
</div>
</template>
<style scoped>
.preset-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="world-book-view">
<h3>📚 世界书管理</h3>
<p>全局世界书激活和条目管理</p>
</div>
</template>
<style scoped>
.world-book-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="dice-view">
<h3>🎲 骰子工具</h3>
<p>纯前端骰子功能</p>
</div>
</template>
<style scoped>
.dice-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="rag-view">
<h3>🔍 RAG 检索</h3>
<p>RAG 知识检索功能</p>
</div>
</template>
<style scoped>
.rag-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div class="world-book-call-view">
<h3>📚 世界书调用</h3>
<p>世界书条目调用记录</p>
</div>
</template>
<style scoped>
.world-book-call-view {
padding: 8px;
}
h3 {
font-size: 14px;
color: #2c3e50;
margin: 0 0 12px 0;
}
p {
font-size: 13px;
color: #7f8c8d;
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<main class="center-chat">
<!-- 消息列表 -->
<div class="message-list">
<CenterMessageItem
v-for="msg in messages"
:key="msg.id"
:message="msg"
/>
</div>
<!-- 输入区域 -->
<div class="input-area">
<!-- 选项框 -->
<div class="input-options">
<select v-model="sendOptions.role" class="option-select">
<option value="user">用户</option>
<option value="assistant">AI</option>
</select>
</div>
<!-- 输入框 -->
<textarea
v-model="inputMessage"
class="message-input"
placeholder="输入消息..."
@keydown.enter.ctrl="sendMessage"
></textarea>
<!-- 发送/停止按钮 -->
<button
:class="['send-btn', { sending: isSending }]"
@click="isSending ? stopGeneration() : sendMessage()"
>
{{ isSending ? '⏹ 停止' : '📤 发送' }}
</button>
</div>
</main>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CenterMessageItem from '../features/center/MessageItem.vue';
interface Message {
id: string;
senderName: string;
senderRole: 'user' | 'assistant' | 'system';
mes: string;
timestamp: Date;
}
const messages = ref<Message[]>([
{
id: '1',
senderName: 'Assistant',
senderRole: 'assistant',
mes: '你好!有什么可以帮助你的吗?',
timestamp: new Date(),
},
]);
const inputMessage = ref('');
const isSending = ref(false);
const sendOptions = ref({
role: 'user' as 'user' | 'assistant',
});
function sendMessage() {
if (!inputMessage.value.trim()) return;
const newMessage: Message = {
id: Date.now().toString(),
senderName: sendOptions.value.role === 'user' ? 'User' : 'Assistant',
senderRole: sendOptions.value.role,
mes: inputMessage.value,
timestamp: new Date(),
};
messages.value.push(newMessage);
inputMessage.value = '';
isSending.value = true;
// 模拟 AI 回复
setTimeout(() => {
isSending.value = false;
}, 2000);
}
function stopGeneration() {
isSending.value = false;
}
</script>
<style scoped>
.center-chat {
display: flex;
flex-direction: column;
flex: 1;
background: #ffffff;
overflow: hidden;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.input-area {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 16px;
background: #f8f9fa;
border-top: 2px solid #dee2e6;
flex-shrink: 0;
}
.input-options {
flex-shrink: 0;
}
.option-select {
padding: 8px;
border: 1px solid #ced4da;
border-radius: 4px;
background: white;
font-size: 13px;
}
.message-input {
flex: 1;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
resize: none;
font-size: 14px;
min-height: 60px;
max-height: 200px;
font-family: inherit;
}
.message-input:focus {
outline: none;
border-color: #3498db;
}
.send-btn {
padding: 12px 20px;
border: none;
border-radius: 4px;
background: #3498db;
color: white;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
flex-shrink: 0;
}
.send-btn:hover {
background: #2980b9;
}
.send-btn.sending {
background: #e74c3c;
}
.send-btn.sending:hover {
background: #c0392b;
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<aside class="left-panel">
<!-- 左侧-顶部分页标签 -->
<div class="panel-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.icon }} {{ tab.label }}
</button>
</div>
<!-- 左侧-内容区域 -->
<div class="panel-content">
<LeftGalleryView v-if="currentTab === 'gallery'" />
<LeftApiConfigView v-if="currentTab === 'api'" />
<LeftPresetView v-if="currentTab === 'preset'" />
<LeftWorldBookView v-if="currentTab === 'worldbook'" />
</div>
</aside>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import LeftGalleryView from '../features/left/GalleryView.vue';
import LeftApiConfigView from '../features/left/ApiConfigView.vue';
import LeftPresetView from '../features/left/PresetView.vue';
import LeftWorldBookView from '../features/left/WorldBookView.vue';
const currentTab = ref('gallery');
const tabs = [
{ id: 'gallery', label: '画廊', icon: '🖼️' },
{ id: 'api', label: 'API', icon: '🔌' },
{ id: 'preset', label: '预设', icon: '⚙️' },
{ id: 'worldbook', label: '世界书', icon: '📚' },
];
</script>
<style scoped>
.left-panel {
display: flex;
flex-direction: column;
width: 25%;
min-width: 300px;
max-width: 400px;
background: #ffffff;
border-right: 1px solid #e0e0e0;
flex-shrink: 0;
}
.panel-tabs {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
padding: 6px 8px;
gap: 4px;
flex-shrink: 0;
}
.tab-btn {
flex: 1;
padding: 8px 6px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: #7f8c8d;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.tab-btn:hover {
background: #ffffff;
border-color: #e0e0e0;
color: #2c3e50;
}
.tab-btn.active {
background: #ffffff;
border-color: #3498db;
color: #3498db;
box-shadow: 0 1px 3px rgba(52, 152, 219, 0.1);
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 16px;
background: #fafafa;
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<aside class="right-panel">
<!-- 右侧-顶部分页标签 -->
<div class="panel-tabs">
<button
v-for="tool in availableToolsList"
:key="tool.id"
:class="['tab-btn', { active: isToolActive(tool.id) }]"
@click="selectTool(tool.id)"
>
{{ tool.icon }} {{ tool.shortLabel }}
</button>
</div>
<!-- 右侧-上部分区最新使用的工具 -->
<div class="panel-section top-section">
<div class="section-header">
<span class="section-title">{{ recentTools[0]?.label || '未选择工具' }}</span>
<button class="collapse-btn" @click="toggleTop" title="折叠/展开">
{{ isTopCollapsed ? '▼' : '▲' }}
</button>
</div>
<div v-show="!isTopCollapsed" class="section-content">
<component :is="recentTools[0]?.component" v-if="recentTools[0]" />
<EmptyState v-else message="点击上方标签选择工具" />
</div>
</div>
<!-- 右侧-下部分区次新使用的工具 -->
<div class="panel-section bottom-section">
<div class="section-header">
<span class="section-title">{{ recentTools[1]?.label || '未选择工具' }}</span>
<button class="collapse-btn" @click="toggleBottom" title="折叠/展开">
{{ isBottomCollapsed ? '▼' : '▲' }}
</button>
</div>
<div v-show="!isBottomCollapsed" class="section-content">
<component :is="recentTools[1]?.component" v-if="recentTools[1]" />
<EmptyState v-else message="点击上方标签选择工具" />
</div>
</div>
</aside>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import RightToolDiceView from '../features/right/DiceView.vue';
import RightToolRagView from '../features/right/RagView.vue';
import RightToolWorldBookCallView from '../features/right/WorldBookCallView.vue';
import RightToolDynamicTableView from '../features/right/DynamicTableView.vue';
import EmptyState from '../shared/EmptyState.vue';
interface ToolItem {
id: string;
label: string;
shortLabel: string;
icon: string;
component: any;
}
const isTopCollapsed = ref(false);
const isBottomCollapsed = ref(false);
const recentTools = ref<ToolItem[]>([]);
// 所有可用工具
const availableTools: Record<string, ToolItem> = {
dice: {
id: 'dice',
label: '🎲 骰子工具',
shortLabel: '骰子',
icon: '🎲',
component: RightToolDiceView,
},
rag: {
id: 'rag',
label: '🔍 RAG 检索',
shortLabel: 'RAG',
icon: '🔍',
component: RightToolRagView,
},
worldbook: {
id: 'worldbook',
label: '📚 世界书调用',
shortLabel: '世界书',
icon: '📚',
component: RightToolWorldBookCallView,
},
table: {
id: 'table',
label: '📊 动态表格',
shortLabel: '表格',
icon: '📊',
component: RightToolDynamicTableView,
},
};
// 获取工具列表(用于渲染标签)
const availableToolsList = Object.values(availableTools);
// 判断工具是否在当前显示的两个中
function isToolActive(toolId: string): boolean {
return recentTools.value.some((tool) => tool.id === toolId);
}
// 选择工具(添加到最近使用队列)
function selectTool(toolId: string) {
const tool = availableTools[toolId];
if (!tool) return;
// 移除已存在的相同工具
recentTools.value = recentTools.value.filter((t) => t.id !== toolId);
// 添加到最前面
recentTools.value.unshift(tool);
// 只保留最近两个
if (recentTools.value.length > 2) {
recentTools.value = recentTools.value.slice(0, 2);
}
}
function toggleTop() {
isTopCollapsed.value = !isTopCollapsed.value;
}
function toggleBottom() {
isBottomCollapsed.value = !isBottomCollapsed.value;
}
// 暴露方法供外部调用
defineExpose({
selectTool,
});
// 初始化默认工具
selectTool('dice');
selectTool('worldbook');
</script>
<style scoped>
.right-panel {
display: flex;
flex-direction: column;
width: 25%;
min-width: 300px;
max-width: 400px;
background: #ffffff;
border-left: 1px solid #e0e0e0;
flex-shrink: 0;
}
.panel-tabs {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
padding: 6px 8px;
gap: 4px;
flex-shrink: 0;
overflow-x: auto;
}
.tab-btn {
flex: 1;
min-width: 60px;
padding: 8px 6px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: #7f8c8d;
cursor: pointer;
font-size: 11px;
font-weight: 500;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
white-space: nowrap;
}
.tab-btn:hover {
background: #ffffff;
border-color: #e0e0e0;
color: #2c3e50;
}
.tab-btn.active {
background: #ffffff;
border-color: #3498db;
color: #3498db;
box-shadow: 0 1px 3px rgba(52, 152, 219, 0.1);
}
.panel-section {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
border-bottom: 1px solid #e0e0e0;
}
.panel-section:last-child {
border-bottom: none;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #ffffff;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #2c3e50;
}
.collapse-btn {
padding: 4px 8px;
border: none;
background: transparent;
cursor: pointer;
font-size: 12px;
color: #7f8c8d;
transition: color 0.2s;
border-radius: 3px;
}
.collapse-btn:hover {
color: #2c3e50;
background: #f8f9fa;
}
.section-content {
flex: 1;
overflow-y: auto;
padding: 12px;
background: #fafafa;
}
/* 滚动条样式 */
.panel-tabs::-webkit-scrollbar,
.section-content::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.panel-tabs::-webkit-scrollbar-track,
.section-content::-webkit-scrollbar-track {
background: transparent;
}
.panel-tabs::-webkit-scrollbar-thumb,
.section-content::-webkit-scrollbar-thumb {
background: #d0d0d0;
border-radius: 3px;
}
.panel-tabs::-webkit-scrollbar-thumb:hover,
.section-content::-webkit-scrollbar-thumb:hover {
background: #b0b0b0;
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="right-toolbar">
<span style="color: #bdc3c7; font-size: 12px;">工具栏</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
rightPanelRef?: any;
}>();
</script>
<style scoped>
.right-toolbar {
display: flex;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<header class="top-toolbar">
<div class="toolbar-left">
<!-- API 配置快速切换 -->
<div class="toolbar-item">
<span class="label">API:</span>
<select v-model="currentApi" class="toolbar-select">
<option value="primary"> API</option>
<option value="secondary"> API</option>
</select>
</div>
</div>
<div class="toolbar-center">
<!-- 当前用户角色 -->
<div class="toolbar-item">
<span class="label">用户:</span>
<span class="value">{{ currentUser }}</span>
</div>
<!-- 当前 AI 角色 -->
<div class="toolbar-item">
<span class="label">AI:</span>
<span class="value">{{ currentAICharacter }}</span>
</div>
<!-- 激活的全局世界书 -->
<div class="toolbar-item">
<span class="label">世界书:</span>
<span class="value">{{ activeWorldBooks.length }} 个激活</span>
</div>
</div>
<div class="toolbar-right">
<!-- 设置按钮 -->
<button class="toolbar-btn" @click="openSettings">
设置
</button>
<!-- 拓展按钮 -->
<button class="toolbar-btn" @click="openExtensions">
🧩 拓展
</button>
</div>
</header>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const currentApi = ref('primary');
const currentUser = ref('User');
const currentAICharacter = ref('Assistant');
const activeWorldBooks = ref(['奇幻世界', '现代都市']);
function openSettings() {
console.log('Open settings');
}
function openExtensions() {
console.log('Open extensions');
}
</script>
<style scoped>
.top-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: #2c3e50;
color: white;
border-bottom: 2px solid #34495e;
height: 50px;
flex-shrink: 0;
}
.toolbar-left,
.toolbar-center,
.toolbar-right {
display: flex;
align-items: center;
gap: 16px;
}
.toolbar-item {
display: flex;
align-items: center;
gap: 8px;
}
.label {
font-size: 12px;
opacity: 0.8;
}
.value {
font-size: 14px;
font-weight: 500;
}
.toolbar-select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #34495e;
background: #34495e;
color: white;
font-size: 13px;
}
.toolbar-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
background: #3498db;
color: white;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
}
.toolbar-btn:hover {
background: #2980b9;
}
</style>

View File

@@ -0,0 +1,51 @@
/**
* useApi Composable
* Provides API client with error handling and loading states
*/
import { ref } from 'vue';
import type { Ref } from 'vue';
interface UseApiOptions<T> {
immediate?: boolean;
onError?: (error: Error) => void;
onSuccess?: (data: T) => void;
}
export function useApi<T>(
apiCall: () => Promise<T>,
options: UseApiOptions<T> = {}
) {
const data: Ref<T | null> = ref(null);
const error: Ref<Error | null> = ref(null);
const loading: Ref<boolean> = ref(false);
const execute = async () => {
loading.value = true;
error.value = null;
try {
const result = await apiCall();
data.value = result;
options.onSuccess?.(result);
return result;
} catch (err) {
error.value = err instanceof Error ? err : new Error(String(err));
options.onError?.(error.value);
throw error.value;
} finally {
loading.value = false;
}
};
if (options.immediate) {
execute();
}
return {
data,
error,
loading,
execute,
};
}

14
client/src/main.ts Normal file
View File

@@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

19
client/tsconfig.app.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
// Extra safety for array and object lookups, but may have false positives.
"noUncheckedIndexedAccess": true,
// Path mapping for cleaner imports.
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../shared/types/*"]
},
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
}
}

11
client/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

27
client/tsconfig.node.json Normal file
View File

@@ -0,0 +1,27 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.
"module": "preserve",
"moduleResolution": "bundler",
// Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": ["node"],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true,
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
}

23
client/vite.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@shared': fileURLToPath(new URL('../shared/types', import.meta.url))
}
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})

68
docker-compose.yml Normal file
View File

@@ -0,0 +1,68 @@
version: '3.8'
services:
# Backend Service (NestJS)
backend:
build:
context: ./server
dockerfile: Dockerfile.dev
container_name: sillytavern-backend
restart: unless-stopped
ports:
- "23337:3000" # Map to host port 23337
volumes:
# Mount code for hot reload (development)
- ./server:/usr/src/app
- ./shared:/usr/src/shared
# Exclude node_modules to use container's version
- /usr/src/app/node_modules
# Persistent data
- ./data:/usr/src/app/data
- ./.env:/usr/src/app/.env:ro
environment:
- NODE_ENV=development
- PORT=3000
- DB_DATABASE=./data/db/app.db
- UPLOAD_DIR=./data/files
- LOG_DIR=./data/logs
working_dir: /usr/src/app
command: npm run start:dev
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend Service (Vue3 + Vite)
frontend:
build:
context: ./client
dockerfile: Dockerfile.dev
container_name: sillytavern-frontend
restart: unless-stopped
ports:
- "23338:5173" # Vite dev server port
volumes:
# Mount code for hot reload (development)
- ./client:/usr/src/app
- ./shared:/usr/src/shared
# Exclude node_modules
- /usr/src/app/node_modules
environment:
- NODE_ENV=development
- VITE_API_URL=http://localhost:23337
- VITE_WS_URL=ws://localhost:23337
working_dir: /usr/src/app
command: npm run dev -- --host 0.0.0.0 --port 5173
depends_on:
backend:
condition: service_healthy
networks:
- app-network
networks:
app-network:
driver: bridge

310
make.bat Normal file
View File

@@ -0,0 +1,310 @@
NEW_FILE_CODE
@echo off
setlocal enabledelayedexpansion
echo =====================================================
echo Creating Enterprise-Grade Project Architecture...
echo Backend: NestJS with DDD + Frontend: Vue3 Composition API
echo =====================================================
:: Check if we are in the right directory
if not exist server (
echo [ERROR] Please run this script from project root directory
pause
exit /b
)
:: -------------------------------------------------------
:: 1. Restructure Backend (NestJS)
:: -------------------------------------------------------
echo.
echo [Backend] Creating complete directory structure...
cd server\src
:: Remove existing directories to start fresh
if exist core rmdir /s /q core
if exist modules rmdir /s /q modules
if exist shared rmdir /s /q shared
:: === CORE LAYER ===
echo Creating core layer...
mkdir core
mkdir core\config
mkdir core\database
mkdir core\database\migrations
mkdir core\database\seeds
mkdir core\decorators
mkdir core\filters
mkdir core\guards
mkdir core\interceptors
mkdir core\pipes
mkdir core\middleware
:: === MODULES LAYER ===
echo Creating modules layer...
mkdir modules
:: Auth Module
mkdir modules\auth
mkdir modules\auth\controllers
mkdir modules\auth\services
mkdir modules\auth\strategies
mkdir modules\auth\dto
mkdir modules\auth\interfaces
:: LLM Module
mkdir modules\llm
mkdir modules\llm\controllers
mkdir modules\llm\services
mkdir modules\llm\providers
mkdir modules\llm\dto
mkdir modules\llm\entities
mkdir modules\llm\interfaces
:: Workflow Module
mkdir modules\workflow
mkdir modules\workflow\controllers
mkdir modules\workflow\services
mkdir modules\workflow\engine
mkdir modules\workflow\nodes
mkdir modules\workflow\dto
mkdir modules\workflow\entities
mkdir modules\workflow\interfaces
:: Data Store Module
mkdir modules\data-store
mkdir modules\data-store\controllers
mkdir modules\data-store\services
mkdir modules\data-store\repositories
mkdir modules\data-store\entities
mkdir modules\data-store\schemas
mkdir modules\data-store\dto
mkdir modules\data-store\interfaces
:: Import/Export Module
mkdir modules\import-export
mkdir modules\import-export\controllers
mkdir modules\import-export\services
mkdir modules\import-export\transformers
mkdir modules\import-export\serializers
mkdir modules\import-export\parsers
mkdir modules\import-export\dto
mkdir modules\import-export\interfaces
:: File Management Module
mkdir modules\file-management
mkdir modules\file-management\controllers
mkdir modules\file-management\services
mkdir modules\file-management\storage
mkdir modules\file-management\dto
mkdir modules\file-management\entities
:: === SHARED LAYER ===
echo Creating shared layer...
mkdir shared
mkdir shared\interfaces
mkdir shared\types
mkdir shared\utils
mkdir shared\constants
mkdir shared\dtos
mkdir shared\decorators
:: === EVENTS SYSTEM ===
echo Creating events system...
mkdir events
mkdir events\event-emitter
mkdir events\events
mkdir events\listeners
:: === QUEUES SYSTEM ===
echo Creating queues system...
mkdir queues
mkdir queues\processors
mkdir queues\jobs
mkdir queues\interfaces
:: === COMMON LAYER ===
echo Creating common layer...
mkdir common
mkdir common\base
mkdir common\mixins
mkdir common\exceptions
cd ..\..
echo [Backend] Directory structure created successfully!
:: -------------------------------------------------------
:: 2. Restructure Frontend (Vue3)
:: -------------------------------------------------------
echo.
echo [Frontend] Creating complete directory structure...
cd client\src
:: Remove existing directories to start fresh
if exist api rmdir /s /q api
if exist components rmdir /s /q components
if exist views rmdir /s /q views
if exist composables rmdir /s /q composables
if exist stores rmdir /s /q stores
if exist router rmdir /s /q router
if exist types rmdir /s /q types
if exist utils rmdir /s /q utils
:: === API LAYER ===
echo Creating API layer...
mkdir api
mkdir api\clients
mkdir api\modules
mkdir api\interceptors
:: === COMPONENTS LAYER ===
echo Creating components layer...
mkdir components
mkdir components\base
mkdir components\base\buttons
mkdir components\base\inputs
mkdir components\base\modals
mkdir components\base\layouts
mkdir components\base\tables
mkdir components\base\forms
mkdir components\business
mkdir components\business\llm-chat
mkdir components\business\workflow-canvas
mkdir components\business\data-table
mkdir components\business\file-uploader
mkdir components\business\prompt-editor
mkdir components\business\node-palette
mkdir components\widgets
mkdir components\widgets\status-indicator
mkdir components\widgets\progress-bar
mkdir components\widgets\toast
:: === VIEWS LAYER ===
echo Creating views layer...
mkdir views
mkdir views\dashboard
mkdir views\workflow-editor
mkdir views\llm-playground
mkdir views\data-browser
mkdir views\settings
mkdir views\auth
:: === COMPOSABLES LAYER ===
echo Creating composables layer...
mkdir composables
:: === STORES LAYER ===
echo Creating stores layer...
mkdir stores
mkdir stores\modules
:: === ROUTER LAYER ===
echo Creating router layer...
mkdir router
mkdir router\routes
:: === TYPES LAYER ===
echo Creating types layer...
mkdir types
mkdir types\models
mkdir types\api
mkdir types\common
:: === UTILS LAYER ===
echo Creating utils layer...
mkdir utils
mkdir utils\formatters
mkdir utils\validators
mkdir utils\helpers
mkdir utils\constants
:: === ASSETS LAYER ===
echo Creating assets structure...
if not exist assets mkdir assets
mkdir assets\styles
mkdir assets\icons
mkdir assets\images
:: === PLUGINS LAYER ===
echo Creating plugins layer...
mkdir plugins
cd ..\..
echo [Frontend] Directory structure created successfully!
:: -------------------------------------------------------
:: 3. Create Root Level Directories
:: -------------------------------------------------------
echo.
echo [Infrastructure] Creating infrastructure directories...
if not exist data mkdir data
if not exist data\db mkdir data\db
if not exist data\files mkdir data\files
if not exist data\logs mkdir data\logs
if not exist data\backups mkdir data\backups
if not exist docker mkdir docker
if not exist docker\nginx mkdir docker\nginx
if not exist docker\app mkdir docker\app
if not exist docker\certs mkdir docker\certs
if not exist docs mkdir docs
if not exist scripts mkdir scripts
if not exist tests mkdir tests
echo [Infrastructure] Infrastructure directories created!
:: -------------------------------------------------------
:: 4. Summary
:: -------------------------------------------------------
echo.
echo =====================================================
echo Project architecture created successfully!
echo.
echo Backend Structure (server/src):
echo - core/ Infrastructure layer
echo - modules/ Business modules (DDD)
echo - shared/ Shared utilities
echo - events/ Event system
echo - queues/ Task queues
echo - common/ Base classes
echo.
echo Frontend Structure (client/src):
echo - api/ API clients
echo - components/ UI components
echo - views/ Page views
echo - composables/ Reusable logic
echo - stores/ State management
echo - types/ TypeScript types
echo - utils/ Utility functions
echo.
echo Next steps:
echo 1. cd server ^&^& npm install
echo 2. cd ../client ^&^& npm install
echo 3. Start implementing base classes and interfaces
echo =====================================================
pause

6
server/.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.env
.git
.idea
.vscode

57
server/.env.example Normal file
View File

@@ -0,0 +1,57 @@
# =====================================================
# 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

4
server/.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

24
server/Dockerfile.dev Normal file
View File

@@ -0,0 +1,24 @@
# 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"]

98
server/README.md Normal file
View File

@@ -0,0 +1,98 @@
<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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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).

35
server/eslint.config.mjs Normal file
View File

@@ -0,0 +1,35 @@
// @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" }],
},
},
);

8
server/nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

9906
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

71
server/package.json Normal file
View File

@@ -0,0 +1,71 @@
{
"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"
}
}

13
server/src/app.module.ts Normal file
View File

@@ -0,0 +1,13 @@
// 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 {}

10
server/src/main.ts Normal file
View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,182 @@
/**
* 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

@@ -0,0 +1,121 @@
/**
* 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

@@ -0,0 +1,94 @@
/**
* 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

@@ -0,0 +1,187 @@
/**
* 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

@@ -0,0 +1,29 @@
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();
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

34
server/tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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/*"]
}
}
}

299
shared/types/README.md Normal file
View File

@@ -0,0 +1,299 @@
# 共享类型系统文档
## 概述
本目录包含项目的完整类型系统,采用**双格式兼容**设计:
1. **SillyTavern 原生格式** - 用于导入导出兼容
2. **扩展内部格式** - 用于增强功能和自定义特性
## 目录结构
```
shared/types/
├── sillytavern/ # SillyTavern 原生格式(从官方文档读取)
│ ├── character.types.ts # 角色卡 V2 规范
│ ├── chat.types.ts # 聊天记录JSONL
│ ├── world.types.ts # 世界书 / Lorebook
│ └── preset.types.ts # 生成预设
├── extended/ # 您的自定义扩展
│ ├── chat-ext.types.ts # 扩展聊天(动态表头)
│ ├── preset-ext.types.ts # 扩展预设(位置锚点)
│ └── world-ext.types.ts # 统一触发器系统
├── adapters/ # 导入导出适配器
│ └── import-export.types.ts # 适配器接口
├── common.types.ts # 通用工具类型
└── index.ts # 主导出文件
```
## 核心设计原则
### 1. **双格式系统**
每个数据实体都有**两种表示形式**
- **ST 格式**:精确的 SillyTavern 规范(用于兼容性)
- **扩展格式**:增强了您的自定义功能
示例:
```typescript
// SillyTavern 格式(导入/导出)
interface STChatMessage {
name: string;
is_user: boolean;
mes: string;
// ... ST 字段
}
// 扩展格式(内部使用)
interface ExtendedChatMessage {
senderName: string;
senderRole: 'user' | 'assistant';
mes: string;
reasoning?: string; // 您的自定义字段
image?: string; // 您的自定义字段
audio?: string; // 您的自定义字段
dynamicData?: Record<string, any>; // 动态表头
}
```
### 2. **适配器模式**
适配器处理格式之间的转换:
```typescript
interface ChatAdapter {
importFromST(jsonlContent: string): InternalChatSession;
exportToST(chat: InternalChatSession): string;
}
```
### 3. **保留原始数据**
内部模型保留原始 ST 数据:
```typescript
interface InternalCharacter {
// ... 您的字段
stData?: STCharacterCardV2; // 保留原始数据
}
```
## Data Models
### Chat History (JSONL)
**Metadata (First line):**
- `characterName`: Character name
- `dynamicHeaders`: User-defined dynamic table headers
- `characterDescription`: Character description
- `systemPrompt`: System prompt
- `firstMessages`: Alternate first messages
**Messages (Subsequent lines):**
- `senderName`: Who sent the message
- `senderRole`: user/assistant/system/narrator
- `mes`: Message content
- `swipes`: Alternative responses
- `reasoning`: Chain of thought
- `image/audio`: Media attachments
- `dynamicData`: Custom dynamic header values
### 预设系统
**固定占位符位置(顺序必须保持):**
1. `system` - 系统提示词(来自角色卡)
2. `world_info_before` - 世界书(前置)
3. `persona_description` - 角色设定描述
4. `char_description` - 角色详细描述
5. `world_info_after` - 世界书(后置)
6. `history` - 聊天记录(对话历史)
7. `user_input` - 用户当前输入
**核心参数:**
- `temperature`: 温度(创造性)
- `maxReplyLength`: 最大回复长度
- `topP`, `topK`: 采样参数
- `repetitionPenalty`: 重复惩罚系数
- `entryOrder`: 预设条目名称的有序列表
- `maxContextTokens`: 最大上下文 token 数
- `streaming`: 是否流式传输
**API 配置:**
```typescript
interface ApiConfig {
apiUrl: string; // API 地址
apiKey: string; // API 密钥
apiType: 'primary' | 'secondary' | 'image_gen' | 'rag'; // API 类型
model: string; // 模型名称
}
```
### 世界书(统一触发器系统)
将原有的 `key``constant` 整合为统一的四元素字典:
```typescript
interface TriggerConfig {
permanent: PermanentTrigger; // 永久激活(无需触发器)
keyword: KeywordTrigger; // 关键词触发
rag: RagTrigger; // RAG/数据库触发
variable: VariableTrigger; // 变量比较触发
}
```
**触发器类型详解:**
1. **永久触发器** (`permanent`)
- `enabled`: 是否永久激活
- 无其他条件,始终生效
2. **关键词触发器** (`keyword`)
- `enabled`: 是否启用
- `keywords`: 关键词数组
- `useRegex`: 是否使用正则表达式
- `matchWholeWords`: 是否匹配完整单词
- `caseSensitive`: 是否区分大小写
3. **RAG 触发器** (`rag`)
- `enabled`: 是否启用
- `databaseName`: 关联的数据库/集合名称
- `threshold`: 相似度阈值0-1
- `topK`: 检索的相似条目数量
4. **变量触发器** (`variable`)
- `enabled`: 是否启用
- `variableA`: 变量 A
- `operator`: 运算符(`>`, `<`, `=`, `contains`, `<=`, `>=`, `!=`
- `variableB`: 变量 B可选
- `constantA`: 常量值(可选,替代 variableB
**其他字段:**
- `content`: 条目内容
- `position`: 插入位置(`before_char` / `after_char`
- `scanDepth`: 扫描深度(扫描多少条消息)
- `priority`: 优先级(同位置的先后顺序)
## 使用示例
### 导入角色卡
```typescript
import { CharacterAdapter, STCharacterCardV2 } from '@shared/types';
const adapter: CharacterAdapter = new CharacterAdapterImpl();
// 从 ST 格式导入
const stCard: STCharacterCardV2 = parsePNG('character.png');
const internalChar = adapter.importFromST(stCard);
// 导出为 ST 格式
const stExport = adapter.exportToST(internalChar);
```
### 聊天会话管理
```typescript
import { ChatAdapter } from '@shared/types';
const adapter: ChatAdapter = new ChatAdapterImpl();
// 导入 JSONL
const jsonlContent = await readFile('chat.jsonl');
const chat = adapter.importFromST(jsonlContent);
// 添加带自定义字段的消息
chat.messages.push({
senderName: '用户',
senderRole: 'user',
mes: '你好!',
reasoning: undefined,
dynamicData: { mood: 'happy' }
});
// 导出回 JSONL
const exported = adapter.exportToST(chat);
```
### 世界书统一触发器
```typescript
import { ExtendedWorldInfoEntry, TriggerConfig } from '@shared/types';
const entry: ExtendedWorldInfoEntry = {
id: '1',
content: '魔法剑发出蓝色光芒。',
position: 'after_char',
scanDepth: 5,
priority: 1,
triggers: {
permanent: { enabled: false },
keyword: {
enabled: true,
keywords: ['剑', '武器'],
useRegex: false,
matchWholeWords: true,
caseSensitive: false
},
rag: { enabled: false },
variable: { enabled: false }
},
enabled: true
};
```
## 兼容性说明
### SillyTavern V2 规范
- 完全兼容 [Character Card V2](https://github.com/malfoyslastname/character-card-spec-v2)
- 支持 PNG 嵌入tEXt 数据块)
- 支持 JSON 格式
### 聊天记录
- JSONL 格式(每行一个 JSON 对象)
- 第一行:元数据
- 后续行:消息
- 支持完整性哈希校验
### 世界书
- 兼容 ST Lorebook 格式
- 扩展触发器系统可转换为 `keys` + `constant`
### 预设
- 支持所有标准 ST 参数
- 固定占位符位置用于提示词构建
- 保留条目顺序
## 迁移指南
### 从 SillyTavern 到内部格式
1. 解析 ST 格式PNG/JSON/JSONL
2. 使用适配器转换为内部格式
3. 原始 ST 数据保存在 `stData` 字段中
4. 根据需要添加自定义字段
### 从内部格式到 SillyTavern 格式
1. 使用适配器将内部格式转换为 ST 格式
2. 不在 ST 规范中的自定义字段会被排除
3. 如果可用,使用原始 ST 数据
4. 以所需格式导出PNG/JSON/JSONL
## 类型安全
所有类型都使用 TypeScript 完整定义:
- 核心模型中不使用 `any` 类型
- 严格的空值检查
- 基于枚举的常量
- 全面的接口定义
## 下一步计划
1. 实现适配器类
2. 创建验证函数
3. 为转换逻辑添加单元测试
4. 构建导入导出 UI 组件

View File

@@ -0,0 +1,170 @@
/**
* Import/Export Adapter Interfaces
* Ensures data compatibility between SillyTavern format and internal format
*/
import type { STCharacterCardV2 } from '../sillytavern/character.types';
import type { STChatSession } from '../sillytavern/chat.types';
import type { STWorldInfo } from '../sillytavern/world.types';
import type { STPreset } from '../sillytavern/preset.types';
import type { ExtendedChatMetadata, ExtendedChatMessage } from '../extended/chat-ext.types';
import type { ExtendedPreset } from '../extended/preset-ext.types';
import type { ExtendedWorldInfo } from '../extended/world-ext.types';
/**
* Character Card Adapter
*/
export interface CharacterAdapter {
/**
* Import from SillyTavern format
*/
importFromST(stCard: STCharacterCardV2): InternalCharacter;
/**
* Export to SillyTavern format
*/
exportToST(internalChar: InternalCharacter): STCharacterCardV2;
/**
* Validate SillyTavern format
*/
validateSTFormat(data: any): boolean;
}
/**
* Chat Session Adapter
*/
export interface ChatAdapter {
/**
* Import from SillyTavern JSONL format
*/
importFromST(jsonlContent: string): InternalChatSession;
/**
* Export to SillyTavern JSONL format
*/
exportToST(chat: InternalChatSession): string;
/**
* Parse JSONL to structured data
*/
parseJSONL(jsonlContent: string): STChatSession;
/**
* Convert structured data to JSONL
*/
toJSONL(session: STChatSession): string;
}
/**
* World Info Adapter
*/
export interface WorldInfoAdapter {
/**
* Import from SillyTavern format
*/
importFromST(stWorldInfo: STWorldInfo): ExtendedWorldInfo;
/**
* Export to SillyTavern format
*/
exportToST(extendedWorldInfo: ExtendedWorldInfo): STWorldInfo;
/**
* Convert unified trigger config to ST format
*/
triggersToST(triggers: any): { keys: string[]; constant: boolean };
/**
* Convert ST format to unified trigger config
*/
triggersFromST(keys: string[], constant: boolean): any;
}
/**
* Preset Adapter
*/
export interface PresetAdapter {
/**
* Import from SillyTavern format
*/
importFromST(stPreset: STPreset): ExtendedPreset;
/**
* Export to SillyTavern format
*/
exportToST(extendedPreset: ExtendedPreset): STPreset;
}
/**
* Internal unified data models
*/
export interface InternalCharacter {
id: string;
name: string;
description: string;
personality: string;
scenario: string;
firstMessage: string;
exampleMessages: string;
avatarUrl?: string;
tags: string[];
createdAt: Date;
updatedAt: Date;
// Original ST data preserved
stData?: STCharacterCardV2;
}
export interface InternalChatSession {
id: string;
characterId: string;
metadata: ExtendedChatMetadata;
messages: ExtendedChatMessage[];
createdAt: Date;
updatedAt: Date;
}
/**
* Import result with validation
*/
export interface ImportResult<T = any> {
success: boolean;
data?: T;
errors: ImportError[];
warnings: ImportWarning[];
sourceFormat: string;
importedAt: Date;
}
export interface ImportError {
code: string;
message: string;
field?: string;
severity: 'error' | 'critical';
}
export interface ImportWarning {
code: string;
message: string;
suggestion?: string;
}
/**
* Export options
*/
export interface ExportOptions {
format: 'json' | 'png' | 'jsonl';
includeMetadata: boolean;
includeExtensions: boolean;
compress: boolean;
}
export interface ExportResult {
success: boolean;
content?: string | Blob;
filename: string;
mimeType: string;
error?: string;
}

View File

@@ -0,0 +1,36 @@
/**
* Common shared types
*/
export interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
export interface SoftDeleteEntity {
deletedAt?: Date;
isDeleted: boolean;
}
export type Entity<T = {}> = T & BaseEntity;
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface SortOption {
field: string;
order: 'asc' | 'desc';
}
export interface FilterOption {
field: string;
operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains';
value: any;
}

View File

@@ -0,0 +1,73 @@
/**
* Extended Chat Metadata (Your custom additions)
* This is the "AI character card" part of chat JSONL
*/
export interface ExtendedChatMetadata {
// Core identity
characterName: string;
// Dynamic table headers (user-defined, with defaults)
dynamicHeaders: DynamicHeader[];
// Character info
characterDescription: string;
characterAvatar?: string; // URL or base64
aiCharacterName: string;
creatorNotes?: string;
// Messages
firstMessages: string[]; // List of alternate first messages
systemPrompt: string;
// Extensions (preserve original ST extensions)
extensions?: Record<string, any>;
// Your custom fields
createdAt: Date;
updatedAt: Date;
tags?: string[];
category?: string;
}
export interface DynamicHeader {
id: string;
name: string;
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect';
defaultValue?: any;
options?: string[]; // For select/multiselect types
required?: boolean;
order: number;
}
/**
* Extended Chat Message (Your custom additions)
*/
export interface ExtendedChatMessage {
// Core fields
senderName: string;
senderRole: 'user' | 'assistant' | 'system' | 'narrator';
mes: string; // Message content
// Your custom fields
swipes?: string[]; // Alternative responses
reasoning?: string; // Chain of thought / reasoning process
image?: string; // Base64 or URL
audio?: string; // Base64 or URL
// Metadata
timestamp: Date;
tokens?: number;
model?: string;
// Dynamic data (matches dynamicHeaders)
dynamicData?: Record<string, any>;
}
/**
* Complete Extended Chat Session
*/
export interface ExtendedChatSession {
metadata: ExtendedChatMetadata;
messages: ExtendedChatMessage[];
}

View File

@@ -0,0 +1,70 @@
/**
* Extended Preset Format (Your custom additions)
*/
export interface ExtendedPreset {
// Core sampling parameters
temperature: number;
maxReplyLength: number;
topP: number;
topK: number;
repetitionPenalty: number;
// Ordered list of preset entry names
entryOrder: string[];
// Context settings
maxContextTokens: number;
streaming: boolean;
// Your custom fields
id: string;
name: string;
description?: string;
createdAt: Date;
updatedAt: Date;
// Preset entries with fixed placeholders
entries: PresetEntry[];
}
/**
* Preset Entry with position anchoring
*/
export interface PresetEntry {
name: string;
context: string; // Content/prompt
position: PresetPosition;
order: number;
}
/**
* Fixed placeholder positions for preset entries
* These anchor specific content types in the prompt
*/
export enum PresetPosition {
SYSTEM = 'system', // System prompt (from character card)
WORLD_INFO_BEFORE = 'world_info_before', // World Info (before character)
PERSONA_DESCRIPTION = 'persona_description', // Character persona description
CHAR_DESCRIPTION = 'char_description', // Character detailed description
WORLD_INFO_AFTER = 'world_info_after', // World Info (after character)
HISTORY = 'history', // Chat history
USER_INPUT = 'user_input' // Current user input
}
/**
* API Configuration for LLM
*/
export interface ApiConfig {
apiUrl: string;
apiKey: string;
apiType: ApiType;
model: string;
}
export enum ApiType {
PRIMARY = 'primary', // Main chat/completion API
SECONDARY = 'secondary', // Fallback API
IMAGE_GEN = 'image_gen', // Image generation API
RAG = 'rag' // RAG/Embedding API
}

View File

@@ -0,0 +1,102 @@
/**
* Extended World Info Format (Your custom additions)
* Integrates "key" and "constant" into a unified trigger system
*/
export interface ExtendedWorldInfo {
id: string;
name: string;
description?: string;
entries: ExtendedWorldInfoEntry[];
metadata: WorldInfoMetadata;
createdAt: Date;
updatedAt: Date;
}
export interface ExtendedWorldInfoEntry {
id: string;
content: string;
position: 'before_char' | 'after_char';
scanDepth: number; // How many messages to scan
priority: number; // Order at same position
// Unified trigger system (replaces key + constant)
triggers: TriggerConfig;
enabled: boolean;
// Your custom fields
category?: string;
tags?: string[];
usageCount?: number;
lastUsed?: Date;
}
/**
* Unified Trigger Configuration
* Replaces separate "key" and "constant" fields
*/
export interface TriggerConfig {
permanent: PermanentTrigger; // Always active (no trigger needed)
keyword: KeywordTrigger; // Keyword-based activation
rag: RagTrigger; // RAG/database-based activation
variable: VariableTrigger; // Variable/expression-based activation
}
/**
* Permanent trigger - always active, no conditions
*/
export interface PermanentTrigger {
enabled: boolean;
}
/**
* Keyword trigger - activates when keywords are found
*/
export interface KeywordTrigger {
enabled: boolean;
keywords: string[];
useRegex: boolean;
matchWholeWords: boolean;
caseSensitive: boolean;
}
/**
* RAG trigger - activates based on vector similarity
*/
export interface RagTrigger {
enabled: boolean;
databaseName: string; // Associated database/collection name
threshold?: number; // Similarity threshold (0-1)
topK?: number; // Number of similar entries to retrieve
}
/**
* Variable trigger - activates based on variable comparison
*/
export interface VariableTrigger {
enabled: boolean;
variableA: string;
operator: ComparisonOperator;
variableB?: string; // Can be variable or constant
constantA?: any; // Alternative to variableB
}
export enum ComparisonOperator {
GREATER_THAN = '>',
LESS_THAN = '<',
EQUALS = '=',
CONTAINS = 'contains',
NOT_GREATER_THAN = '<=',
NOT_LESS_THAN = '>=',
NOT_EQUALS = '!='
}
export interface WorldInfoMetadata {
entryCount: number;
categories: string[];
tags: string[];
}

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

@@ -0,0 +1,29 @@
/**
* Shared Types - Main Export File
* Import from this file in your application
*/
// ============================================
// SillyTavern Original Formats (Compatibility Layer)
// ============================================
export * from './sillytavern/character.types';
export * from './sillytavern/chat.types';
export * from './sillytavern/world.types';
export * from './sillytavern/preset.types';
// ============================================
// Extended Data (Your Custom Additions)
// ============================================
export * from './extended/chat-ext.types';
export * from './extended/preset-ext.types';
export * from './extended/world-ext.types';
// ============================================
// Import/Export Adapters
// ============================================
export * from './adapters/import-export.types';
// ============================================
// Common Types
// ============================================
export * from './common.types';

View File

@@ -0,0 +1,97 @@
/**
* SillyTavern Character Card V2 Specification
* Reference: https://github.com/malfoyslastname/character-card-spec-v2
*/
export interface STCharacterCardV2 {
spec: 'chara_card_v2';
spec_version: '2.0';
data: STCharacterData;
}
export interface STCharacterData {
// Core fields
name: string;
description: string;
personality: string;
scenario: string;
first_mes: string;
mes_example: string;
// Optional fields
creator_notes?: string;
system_prompt?: string;
post_history_instructions?: string;
alternate_greetings?: string[];
tags?: string[];
creator?: string;
character_version?: string;
// Character Book (World Info)
character_book?: STCharacterBook;
// Extensions
extensions?: Record<string, any>;
}
export interface STCharacterBook {
entries: STWorldInfoEntry[];
extensions?: Record<string, any>;
}
export interface STWorldInfoEntry {
id?: number;
keys: string[];
keysecondary?: string[];
content: string;
comment?: string;
constant: boolean;
constantRule?: 'none' | 'always' | 'position';
selective: boolean;
order: number;
insertion_order?: number;
position: 'before_char' | 'after_char';
use_regex: boolean;
match_whole_words?: boolean;
case_sensitive?: boolean;
useProbability?: boolean;
probability?: number;
depth?: number;
scan_depth?: number;
group?: string;
group_override?: boolean;
group_weight?: number;
prevent_recursion?: boolean;
delay_until_recursion?: boolean;
display_index?: number;
enabled: boolean;
exclude_from_banbook?: boolean;
linked_entries?: number[];
search_range?: 'both' | 'user' | 'assistant';
role?: number;
vectorized?: boolean;
sticky?: boolean;
cooldown?: number;
delay?: number;
automation_id?: string;
title?: string;
display_title?: string;
}
/**
* V1 Character Card (Legacy)
*/
export interface STCharacterCardV1 {
name: string;
context: string;
greeting: string;
examples: string;
author: string;
first_mes: string;
avatar: string;
chat: string;
mes_example: string;
description: string;
personality: string;
scenario: string;
}

View File

@@ -0,0 +1,73 @@
/**
* SillyTavern Chat History Format (JSONL)
* Each line is a JSON object representing a message or metadata
*/
/**
* Chat Metadata (First line of JSONL file)
*/
export interface STChatMetadata {
user_name: string;
character_name: string;
create_date: string; // ISO 8601 format
chat_metadata?: {
integrity?: string; // SHA256 hash for integrity check
[key: string]: any;
};
}
/**
* Chat Message (Subsequent lines in JSONL file)
*/
export interface STChatMessage {
name: string;
is_user: boolean;
send_date: number; // Unix timestamp
mes: string; // Message content
// Optional fields
force_avatar?: string;
extra?: STMessageExtra;
is_system?: boolean;
}
export interface STMessageExtra {
gen_id?: number;
send_date?: number;
swipes?: string[]; // Alternative responses
swipe_id?: number;
continueHistory?: boolean;
continueSwipeId?: number;
dryReason?: string;
abuse?: boolean;
rollbackChecked?: boolean;
prompt?: any;
memory?: any;
notes?: string;
bias?: any[];
display_text?: string;
model?: string;
model_params?: any;
persona_description?: string;
attachments?: STAttachment[];
reasoning?: string; // Chain of thought / reasoning
image?: string; // Base64 or path
audio?: string; // Base64 or path
}
export interface STAttachment {
type: 'image' | 'file' | 'audio';
path: string;
preview?: string;
name?: string;
size?: number;
mime?: string;
}
/**
* Complete Chat Session
*/
export interface STChatSession {
metadata: STChatMetadata;
messages: STChatMessage[];
}

View File

@@ -0,0 +1,100 @@
/**
* SillyTavern Preset Format
*/
export interface STPreset {
name: string;
// Core sampling parameters
temp?: number; // Temperature
rep_pen?: number; // Repetition penalty
rep_pen_range?: number;
top_p?: number;
top_k?: number;
top_a?: number;
min_p?: number;
typical?: number;
tfs?: number;
epsilon_cutoff?: number;
eta_cutoff?: number;
// Advanced parameters
rep_pen_slope?: number;
guidance_scale?: number;
negative_prompt?: string;
penalty_alpha?: number;
num_beams?: number;
length_penalty?: number;
no_repeat_ngram_size?: number;
encoder_rep_pen?: number;
freq_pen?: number; // Frequency penalty
presence_pen?: number; // Presence penalty
// Sampling options
do_sample?: boolean;
early_stopping?: boolean;
// Dynamic temperature
dynatemp?: boolean;
dynatemp_range?: number;
dynatemp_exponent?: number;
// Smoothing
smoothing_factor?: number;
smoothing_curve?: number;
// DRY (Don't Repeat Yourself)
dry_allowed_length?: number;
dry_multiplier?: number;
dry_base?: number;
dry_sequence_breakers?: string[];
dry_penalty_last_n?: number;
// Token settings
add_bos_token?: boolean;
ban_eos_token?: boolean;
skip_special_tokens?: boolean;
// Mirostat
mirostat_mode?: number;
mirostat_tau?: number;
mirostat_eta?: number;
// Grammar
grammar_string?: string;
json_schema?: any;
// Model specific
model?: string;
sampler_priority?: string[];
sampler_order?: number[];
use_default_badwordsids?: boolean;
seed?: number;
// Context
maxContextTokens?: number;
streaming?: boolean;
// Metadata
description?: string;
created_at?: number;
updated_at?: number;
}
/**
* Preset Entry (for ordered list in preset configuration)
*/
export interface STPresetEntry {
name: string;
context: string; // Content/prompt
position: 'before_char' | 'after_char' | 'system' | 'history' | 'user_input';
order: number;
}
/**
* Quick Preset (Simplified format)
*/
export interface STQuickPreset {
name: string;
settings: Partial<STPreset>;
}

View File

@@ -0,0 +1,55 @@
/**
* SillyTavern World Info (Lorebook) Format
*/
export interface STWorldInfo {
name: string;
description?: string;
entries: STWorldInfoEntry[];
metadata?: STWorldInfoMetadata;
}
export interface STWorldInfoEntry {
uid?: number;
key: string[]; // Trigger keywords
keysecondary?: string[]; // Secondary triggers
comment?: string;
content: string; // Lore content
constant: boolean; // Always active
constantRule?: 'none' | 'always' | 'position';
selective: boolean; // Use secondary keys
order: number; // Insertion order
position: 'before_char' | 'after_char'; // Insertion position
use_regex: boolean;
match_whole_words?: boolean;
case_sensitive?: boolean;
useProbability?: boolean;
probability?: number;
depth?: number; // Scan depth (how many messages to scan)
scan_depth?: number;
group?: string;
group_override?: boolean;
group_weight?: number;
prevent_recursion?: boolean;
delay_until_recursion?: boolean;
display_index?: number;
enabled: boolean;
exclude_from_banbook?: boolean;
linked_entries?: number[];
search_range?: 'both' | 'user' | 'assistant';
role?: number;
vectorized?: boolean; // RAG support
sticky?: boolean;
cooldown?: number;
delay?: number;
automation_id?: string;
title?: string;
display_title?: string;
}
export interface STWorldInfoMetadata {
created_at?: number;
updated_at?: number;
version?: string;
source?: string;
}