Initial commit: Project structure and layout
This commit is contained in:
51
.dockerignore
Normal file
51
.dockerignore
Normal 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
61
.gitignore
vendored
Normal 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
254
ARCHITECTURE_REVIEW.md
Normal 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
6
client/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
37
client/.env.example
Normal file
37
client/.env.example
Normal 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
17
client/Dockerfile.dev
Normal 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
42
client/README.md
Normal 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
1
client/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
13
client/index.html
Normal file
13
client/index.html
Normal 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
3148
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
client/package.json
Normal file
33
client/package.json
Normal 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
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
41
client/src/App.vue
Normal 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>
|
||||||
86
client/src/assets/base.css
Normal file
86
client/src/assets/base.css
Normal 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;
|
||||||
|
}
|
||||||
1
client/src/assets/logo.svg
Normal file
1
client/src/assets/logo.svg
Normal 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 |
35
client/src/assets/main.css
Normal file
35
client/src/assets/main.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
client/src/components/features/center/MessageItem.vue
Normal file
72
client/src/components/features/center/MessageItem.vue
Normal 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>
|
||||||
21
client/src/components/features/left/ApiConfigView.vue
Normal file
21
client/src/components/features/left/ApiConfigView.vue
Normal 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>
|
||||||
21
client/src/components/features/left/GalleryView.vue
Normal file
21
client/src/components/features/left/GalleryView.vue
Normal 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>
|
||||||
21
client/src/components/features/left/PresetView.vue
Normal file
21
client/src/components/features/left/PresetView.vue
Normal 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>
|
||||||
21
client/src/components/features/left/WorldBookView.vue
Normal file
21
client/src/components/features/left/WorldBookView.vue
Normal 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>
|
||||||
21
client/src/components/features/right/DiceVice.vue
Normal file
21
client/src/components/features/right/DiceVice.vue
Normal 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>
|
||||||
21
client/src/components/features/right/RagView.vue
Normal file
21
client/src/components/features/right/RagView.vue
Normal 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>
|
||||||
21
client/src/components/features/right/WorldBookCallView.vue
Normal file
21
client/src/components/features/right/WorldBookCallView.vue
Normal 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>
|
||||||
175
client/src/components/layout/CenterChat.vue
Normal file
175
client/src/components/layout/CenterChat.vue
Normal 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>
|
||||||
99
client/src/components/layout/LeftPanel.vue
Normal file
99
client/src/components/layout/LeftPanel.vue
Normal 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>
|
||||||
267
client/src/components/layout/RightPanel.vue
Normal file
267
client/src/components/layout/RightPanel.vue
Normal 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>
|
||||||
18
client/src/components/layout/RightToolbar.vue
Normal file
18
client/src/components/layout/RightToolbar.vue
Normal 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>
|
||||||
125
client/src/components/layout/TopToolbar.vue
Normal file
125
client/src/components/layout/TopToolbar.vue
Normal 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>
|
||||||
51
client/src/composables/useApi.ts
Normal file
51
client/src/composables/useApi.ts
Normal 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
14
client/src/main.ts
Normal 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
19
client/tsconfig.app.json
Normal 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
11
client/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
client/tsconfig.node.json
Normal file
27
client/tsconfig.node.json
Normal 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
23
client/vite.config.ts
Normal 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
68
docker-compose.yml
Normal 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
310
make.bat
Normal 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
6
server/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
57
server/.env.example
Normal file
57
server/.env.example
Normal 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
4
server/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
24
server/Dockerfile.dev
Normal file
24
server/Dockerfile.dev
Normal 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
98
server/README.md
Normal 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>
|
||||||
|
<!--[](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).
|
||||||
35
server/eslint.config.mjs
Normal file
35
server/eslint.config.mjs
Normal 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
8
server/nest-cli.json
Normal 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
9906
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
71
server/package.json
Normal file
71
server/package.json
Normal 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
13
server/src/app.module.ts
Normal 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
10
server/src/main.ts
Normal 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();
|
||||||
182
server/src/modules/chat/adapters/chat.adapter.ts
Normal file
182
server/src/modules/chat/adapters/chat.adapter.ts
Normal 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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
server/src/modules/examples/adapter-examples.ts
Normal file
121
server/src/modules/examples/adapter-examples.ts
Normal 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();
|
||||||
|
}
|
||||||
94
server/src/modules/preset/adapters/preset.adapter.ts
Normal file
94
server/src/modules/preset/adapters/preset.adapter.ts
Normal 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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
server/src/modules/world-info/adapters/world-info.adapter.ts
Normal file
187
server/src/modules/world-info/adapters/world-info.adapter.ts
Normal 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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
server/test/app.e2e-spec.ts
Normal file
29
server/test/app.e2e-spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
9
server/test/jest-e2e.json
Normal file
9
server/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
server/tsconfig.build.json
Normal file
4
server/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
34
server/tsconfig.json
Normal file
34
server/tsconfig.json
Normal 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
299
shared/types/README.md
Normal 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 组件
|
||||||
170
shared/types/adapters/import-export.types.ts
Normal file
170
shared/types/adapters/import-export.types.ts
Normal 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;
|
||||||
|
}
|
||||||
36
shared/types/common.types.ts
Normal file
36
shared/types/common.types.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
73
shared/types/extended/chat-ext.types.ts
Normal file
73
shared/types/extended/chat-ext.types.ts
Normal 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[];
|
||||||
|
}
|
||||||
70
shared/types/extended/preset-ext.types.ts
Normal file
70
shared/types/extended/preset-ext.types.ts
Normal 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
|
||||||
|
}
|
||||||
102
shared/types/extended/world-ext.types.ts
Normal file
102
shared/types/extended/world-ext.types.ts
Normal 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
29
shared/types/index.ts
Normal 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';
|
||||||
97
shared/types/sillytavern/character.types.ts
Normal file
97
shared/types/sillytavern/character.types.ts
Normal 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;
|
||||||
|
}
|
||||||
73
shared/types/sillytavern/chat.types.ts
Normal file
73
shared/types/sillytavern/chat.types.ts
Normal 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[];
|
||||||
|
}
|
||||||
100
shared/types/sillytavern/preset.types.ts
Normal file
100
shared/types/sillytavern/preset.types.ts
Normal 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>;
|
||||||
|
}
|
||||||
55
shared/types/sillytavern/world.types.ts
Normal file
55
shared/types/sillytavern/world.types.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user