规定数据类型
This commit is contained in:
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# Application
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
# Data Directory
|
||||
DATA_DIR=./data
|
||||
|
||||
# LLM API Keys (配置你使用的 LLM 提供商)
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
ANTHROPIC_API_KEY=your-anthropic-api-key
|
||||
|
||||
# Optional: Custom LLM Provider Settings
|
||||
LLM_PROVIDER=openai # openai | anthropic | custom
|
||||
LLM_MODEL=gpt-4 # 根据你的 provider 选择合适的模型
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Production
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# Data (optional - uncomment if you don't want to commit data)
|
||||
# data/
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
429
ARCHITECTURE.md
Normal file
429
ARCHITECTURE.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# 项目架构设计文档
|
||||
|
||||
## 📐 整体架构
|
||||
|
||||
本项目采用**前后端分离 + 共享层**的单体仓库(monorepo)架构,严格遵循 MVC 设计模式。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SillyTavern Repalice │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Client │◄──►│ Shared │ │
|
||||
│ │ (Vue3) │ │ (Types) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ ▲ ▲ │
|
||||
│ │ HTTP/API │ Type Safety │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Server │◄──►│ Shared │ │
|
||||
│ │(NestJS) │ │(Schemas) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ Data │ │
|
||||
│ │ (Local) │ │
|
||||
│ └──────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🎯 核心设计决策
|
||||
|
||||
### 1. 双数据格式策略
|
||||
|
||||
**问题**: 需要兼容 SillyTavern 的导入导出,同时支持自定义扩展功能
|
||||
|
||||
**解决方案**:
|
||||
- **外部格式** (`ST*` 前缀): 严格遵循 SillyTavern 规范
|
||||
- **内部格式** (无前缀): 继承并扩展,添加自定义字段
|
||||
- **转换器**: 双向转换,保证数据完整性
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
// SillyTavern 格式
|
||||
interface STWorldInfoEntry {
|
||||
constant?: boolean; // 永久激活标志
|
||||
selective?: boolean; // RAG 标志
|
||||
}
|
||||
|
||||
// 内部格式
|
||||
interface WorldInfoEntry extends STWorldInfoEntry {
|
||||
activationType: ActivationType; // 4种枚举
|
||||
logicExpression?: LogicExpression;
|
||||
ragConfig?: RAGConfig;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. MVC 分层架构
|
||||
|
||||
#### Model (模型层)
|
||||
```
|
||||
server/src/
|
||||
├── services/ # 业务逻辑
|
||||
├── persistence/ # 数据访问
|
||||
│ ├── interfaces/ # 仓储接口
|
||||
│ └── implementations/ # 具体实现
|
||||
└── dto/ # 数据传输对象
|
||||
```
|
||||
|
||||
**职责**:
|
||||
- Service: 业务规则、数据验证、事务管理
|
||||
- Repository: CRUD 操作、数据持久化
|
||||
- DTO: API 输入输出验证
|
||||
|
||||
#### View (视图层)
|
||||
```
|
||||
client/src/
|
||||
├── components/ # UI 组件
|
||||
│ ├── TopBar/
|
||||
│ ├── LeftPanel/
|
||||
│ ├── CenterPanel/
|
||||
│ ├── RightPanel/
|
||||
│ └── common/
|
||||
├── layouts/ # 页面布局
|
||||
└── stores/ # 状态管理
|
||||
```
|
||||
|
||||
**职责**:
|
||||
- Components: UI 渲染、用户交互
|
||||
- Layouts: 页面结构、路由视图
|
||||
- Stores: 客户端状态管理
|
||||
|
||||
#### Controller (控制层)
|
||||
```
|
||||
server/src/controllers/ # API 端点
|
||||
client/src/router/ # 前端路由
|
||||
```
|
||||
|
||||
**职责**:
|
||||
- 接收请求、参数验证
|
||||
- 调用 Service 处理业务
|
||||
- 返回响应
|
||||
|
||||
### 3. 依赖注入 (DI)
|
||||
|
||||
**后端 DI** (NestJS):
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class CharacterService {
|
||||
constructor(
|
||||
@Inject(CHARACTER_REPOSITORY)
|
||||
private repo: CharacterRepository,
|
||||
private llmService: LLMService,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 松耦合:模块间通过接口通信
|
||||
- 可测试:轻松 mock 依赖
|
||||
- 可替换:不同实现无缝切换
|
||||
|
||||
### 4. 工具层 (Tools Layer)
|
||||
|
||||
独立于 MVC 的工具层,提供通用能力:
|
||||
|
||||
```
|
||||
server/src/tools/
|
||||
├── context-chunker.ts # 上下文分块
|
||||
├── prompt-assembler.ts # 提示词组装
|
||||
└── token-counter.ts # Token 计数
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 无状态纯函数
|
||||
- 不依赖 DI 容器
|
||||
- 可在任何层调用
|
||||
|
||||
## 🗂️ 目录结构设计原则
|
||||
|
||||
### 后端 (NestJS)
|
||||
|
||||
```
|
||||
server/src/
|
||||
├── modules/ # 按业务领域划分
|
||||
│ ├── chat/ # 聊天管理
|
||||
│ ├── character/ # 角色卡管理
|
||||
│ ├── llm/ # LLM 集成
|
||||
│ ├── import-export/# 导入导出
|
||||
│ └── workflow/ # 工作流引擎
|
||||
│
|
||||
├── persistence/ # 持久化抽象
|
||||
│ ├── interfaces/ # 仓储接口
|
||||
│ ├── implementations/ # 实现
|
||||
│ └── file-system/ # 文件系统实现
|
||||
│
|
||||
├── core/ # 基础设施
|
||||
│ ├── config/ # 配置管理
|
||||
│ ├── filters/ # 异常过滤
|
||||
│ ├── interceptors/ # 拦截器
|
||||
│ └── guards/ # 守卫
|
||||
│
|
||||
├── controllers/ # 路由层 (横向切割)
|
||||
├── services/ # 业务层 (横向切割)
|
||||
└── tools/ # 工具层 (横向切割)
|
||||
```
|
||||
|
||||
**设计原则**:
|
||||
1. **垂直切片**: 每个 module 自包含 controller + service
|
||||
2. **水平分层**: persistence/core 跨模块复用
|
||||
3. **依赖方向**: controllers → services → persistence
|
||||
|
||||
### 前端 (Vue3)
|
||||
|
||||
```
|
||||
client/src/
|
||||
├── components/ # 按布局区域划分
|
||||
│ ├── TopBar/ # 顶部栏
|
||||
│ ├── LeftPanel/ # 左侧面板
|
||||
│ ├── CenterPanel/ # 中央面板
|
||||
│ ├── RightPanel/ # 右侧面板
|
||||
│ └── common/ # 通用组件
|
||||
│
|
||||
├── layouts/ # 布局模板
|
||||
├── stores/ # 全局状态
|
||||
├── router/ # 路由配置
|
||||
├── composables/ # 组合式函数
|
||||
└── api/ # API 客户端
|
||||
```
|
||||
|
||||
**设计原则**:
|
||||
1. **布局驱动**: 组件按 UI 区域组织
|
||||
2. **关注点分离**: 状态、路由、API 独立管理
|
||||
3. **组合优于继承**: 使用 composables 复用逻辑
|
||||
|
||||
### 共享层 (Shared)
|
||||
|
||||
```
|
||||
shared/
|
||||
├── types/ # TypeScript 类型
|
||||
│ ├── sillytavern.types.ts # ST 兼容格式
|
||||
│ ├── internal.types.ts # 内部扩展格式
|
||||
│ └── converters.ts # 转换器
|
||||
│
|
||||
└── schemas/ # Zod 运行时验证
|
||||
├── sillytavern.schemas.ts
|
||||
└── internal.schemas.ts
|
||||
```
|
||||
|
||||
**设计原则**:
|
||||
1. **单一真相源**: 前后端共用类型定义
|
||||
2. **编译时 + 运行时**: TypeScript + Zod 双重验证
|
||||
3. **向后兼容**: 转换器保证旧数据可用
|
||||
|
||||
## 🔄 数据流
|
||||
|
||||
### 典型请求流程
|
||||
|
||||
```
|
||||
User Action (Frontend)
|
||||
↓
|
||||
Vue Component
|
||||
↓
|
||||
Pinia Store
|
||||
↓
|
||||
API Client (axios)
|
||||
↓
|
||||
HTTP Request
|
||||
↓
|
||||
NestJS Controller
|
||||
↓
|
||||
Service (Business Logic)
|
||||
↓
|
||||
Repository (Data Access)
|
||||
↓
|
||||
File System
|
||||
↓
|
||||
Response (JSON)
|
||||
↓
|
||||
Update UI
|
||||
```
|
||||
|
||||
### 数据转换流程
|
||||
|
||||
```
|
||||
SillyTavern Import File
|
||||
↓
|
||||
Zod Schema Validation (ST Schema)
|
||||
↓
|
||||
Parse to ST Types
|
||||
↓
|
||||
Converter (convertSTToInternal)
|
||||
↓
|
||||
Internal Types
|
||||
↓
|
||||
Save to File System
|
||||
```
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
### 单元测试
|
||||
- **目标**: Services, Tools, Utils
|
||||
- **工具**: Jest
|
||||
- **Mock**: 外部依赖 (Repository, LLM API)
|
||||
|
||||
### 集成测试
|
||||
- **目标**: Controller + Service 协作
|
||||
- **工具**: Supertest
|
||||
- **范围**: 单个模块的完整流程
|
||||
|
||||
### E2E 测试
|
||||
- **目标**: 关键用户流程
|
||||
- **工具**: Playwright / Cypress
|
||||
- **场景**: 创建角色、发送消息、导入导出
|
||||
|
||||
## 🚀 部署架构
|
||||
|
||||
### 开发环境
|
||||
```
|
||||
Client (Vite Dev Server) :5173
|
||||
↓ Proxy /api
|
||||
Server (NestJS) :3000
|
||||
↓
|
||||
Local File System ./data
|
||||
```
|
||||
|
||||
### 生产环境 (Docker)
|
||||
```
|
||||
Client (Nginx) :80
|
||||
↓ Proxy /api
|
||||
Server (Node.js) :3000
|
||||
↓
|
||||
Volume Mount /app/data
|
||||
```
|
||||
|
||||
## 📊 关键技术选型理由
|
||||
|
||||
| 技术 | 选型理由 |
|
||||
|------|---------|
|
||||
| **NestJS** | 内置 DI、模块化、TypeScript 优先、企业级 |
|
||||
| **Vue 3** | 组合式 API、响应式系统、学习曲线平缓 |
|
||||
| **Vite** | 极速开发体验、原生 ESM、热更新快 |
|
||||
| **Zod** | TypeScript 优先、运行时验证、 schema 推导 |
|
||||
| **Vercel AI SDK** | 统一 LLM 接口、流式支持、工具调用 |
|
||||
| **Pinia** | Vue 3 官方推荐、TypeScript 友好、轻量 |
|
||||
| **文件系统** | 零依赖、易备份、用户可控、无需数据库 |
|
||||
|
||||
## 🎨 设计模式应用
|
||||
|
||||
### 1. Repository Pattern
|
||||
```typescript
|
||||
interface CharacterRepository {
|
||||
findById(id: string): Promise<CharacterCard>;
|
||||
save(character: CharacterCard): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
class FileSystemCharacterRepository implements CharacterRepository {
|
||||
// 实现细节隐藏
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Factory Pattern
|
||||
```typescript
|
||||
class LLMProviderFactory {
|
||||
static create(provider: ProviderType): LLMProvider {
|
||||
switch (provider) {
|
||||
case 'openai': return new OpenAIProvider();
|
||||
case 'anthropic': return new AnthropicProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Strategy Pattern
|
||||
```typescript
|
||||
interface ActivationStrategy {
|
||||
shouldActivate(entry: WorldInfoEntry, context: string): boolean;
|
||||
}
|
||||
|
||||
class KeywordActivationStrategy implements ActivationStrategy {
|
||||
// 关键词匹配逻辑
|
||||
}
|
||||
|
||||
class RAGActivationStrategy implements ActivationStrategy {
|
||||
// 向量检索逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Adapter Pattern
|
||||
```typescript
|
||||
// 适配器:SillyTavern 格式 → 内部格式
|
||||
class SillyTavernAdapter {
|
||||
static toInternal(stCard: STCharacterCard): CharacterCard {
|
||||
return convertSTCharacterCardToInternal(stCard);
|
||||
}
|
||||
|
||||
static toExternal(card: CharacterCard): STCharacterCard {
|
||||
return convertCharacterCardToST(card);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 安全性考虑
|
||||
|
||||
1. **输入验证**: Zod schemas 在边界验证所有输入
|
||||
2. **文件上传**: 限制类型、大小,扫描恶意内容
|
||||
3. **API 密钥**: 环境变量管理,不提交到版本控制
|
||||
4. **CORS**: 严格配置允许的来源
|
||||
5. **路径遍历**: 验证文件路径,防止 `../` 攻击
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
1. **懒加载**: 前端路由和组件按需加载
|
||||
2. **缓存**: LLM 响应缓存、角色卡元数据缓存
|
||||
3. **分页**: 聊天记录、角色列表分页加载
|
||||
4. **流式响应**: SSE 传输 LLM 生成内容
|
||||
5. **索引**: 为搜索字段建立内存索引
|
||||
|
||||
## 🔄 扩展性设计
|
||||
|
||||
### 新增 LLM Provider
|
||||
1. 实现 `LLMProvider` 接口
|
||||
2. 注册到 `LLMProviderFactory`
|
||||
3. 更新配置 schema
|
||||
|
||||
### 新增持久化方式
|
||||
1. 实现 Repository 接口
|
||||
2. 创建新模块 (如 `persistence/sqlite/`)
|
||||
3. 通过 DI 切换实现
|
||||
|
||||
### 新增激活方式
|
||||
1. 扩展 `ActivationType` 枚举
|
||||
2. 实现新的 `ActivationStrategy`
|
||||
3. 更新世界书 UI
|
||||
|
||||
## 📝 编码规范
|
||||
|
||||
### TypeScript
|
||||
- 严格模式: `strict: true`
|
||||
- 禁止 `any`,使用 `unknown` 代替
|
||||
- 接口命名不加 `I` 前缀
|
||||
- 枚举使用 PascalCase
|
||||
|
||||
### Vue 3
|
||||
- 优先使用 `<script setup>`
|
||||
- Props 用 TypeScript 定义
|
||||
- 组合式逻辑提取为 composables
|
||||
|
||||
### NestJS
|
||||
- 每个模块一个文件夹
|
||||
- Controller 只处理 HTTP 层
|
||||
- Service 不包含 HTTP 相关代码
|
||||
- 使用 DTO 验证输入
|
||||
|
||||
## 🎯 下一步计划
|
||||
|
||||
1. **实现基础 CRUD**: 角色卡、聊天、世界书
|
||||
2. **集成 Vercel AI SDK**: 流式聊天
|
||||
3. **构建 UI 组件**: 聊天界面、角色编辑器
|
||||
4. **实现转换器**: 完整的导入导出功能
|
||||
5. **添加测试**: 覆盖核心业务逻辑
|
||||
6. **编写文档**: API 文档、用户指南
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2026-04-24
|
||||
314
QUICKSTART.md
Normal file
314
QUICKSTART.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# 快速开始指南
|
||||
|
||||
## 🚀 5分钟上手
|
||||
|
||||
### 前置检查
|
||||
|
||||
确保你的系统已安装:
|
||||
- ✅ Node.js >= 20.x (`node -v`)
|
||||
- ✅ npm >= 10.x (`npm -v`)
|
||||
- ✅ Git (`git --version`)
|
||||
|
||||
### 步骤 1: 克隆项目
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd sillytavern-repalice
|
||||
```
|
||||
|
||||
### 步骤 2: 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
这会安装所有工作区 (shared, server, client) 的依赖。
|
||||
|
||||
### 步骤 3: 配置环境变量
|
||||
|
||||
```bash
|
||||
# 复制示例配置
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑 .env 文件,填入你的 API 密钥
|
||||
# Windows: notepad .env
|
||||
# macOS/Linux: nano .env
|
||||
```
|
||||
|
||||
最小配置示例:
|
||||
```env
|
||||
OPENAI_API_KEY=sk-your-openai-key
|
||||
LLM_PROVIDER=openai
|
||||
LLM_MODEL=gpt-4
|
||||
```
|
||||
|
||||
### 步骤 4: 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 同时启动前后端(推荐)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
或者分别启动:
|
||||
```bash
|
||||
# 终端 1: 启动后端
|
||||
npm run dev:server
|
||||
|
||||
# 终端 2: 启动前端
|
||||
npm run dev:client
|
||||
```
|
||||
|
||||
### 步骤 5: 访问应用
|
||||
|
||||
打开浏览器访问:
|
||||
- **前端**: http://localhost:5173
|
||||
- **后端 API**: http://localhost:3000/api
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
### 快速启动
|
||||
|
||||
```bash
|
||||
# 构建并启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看运行状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 仅构建镜像
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
|
||||
Docker 会自动将数据挂载到 `./data` 目录:
|
||||
```
|
||||
sillytavern-repalice/
|
||||
└── data/ # 此目录会包含所有持久化数据
|
||||
├── characters/
|
||||
├── chats/
|
||||
├── worldinfo/
|
||||
└── presets/
|
||||
```
|
||||
|
||||
## 📂 项目结构速览
|
||||
|
||||
```
|
||||
sillytavern-repalice/
|
||||
├── shared/ # 🔗 前后端共享类型和 schemas
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ └── schemas/ # Zod 验证 schemas
|
||||
│
|
||||
├── server/ # 🖥️ 后端 NestJS
|
||||
│ └── src/
|
||||
│ ├── modules/ # 业务模块
|
||||
│ ├── services/ # 业务逻辑
|
||||
│ └── persistence/ # 数据持久化
|
||||
│
|
||||
├── client/ # 🎨 前端 Vue3
|
||||
│ └── src/
|
||||
│ ├── components/ # UI 组件
|
||||
│ ├── stores/ # 状态管理
|
||||
│ └── router/ # 路由配置
|
||||
│
|
||||
└── data/ # 💾 运行时生成的数据
|
||||
```
|
||||
|
||||
## 🔧 常用命令
|
||||
|
||||
### 开发
|
||||
|
||||
```bash
|
||||
npm run dev # 启动开发环境
|
||||
npm run dev:server # 仅启动后端
|
||||
npm run dev:client # 仅启动前端
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
npm run build # 构建所有项目
|
||||
npm run build:server # 构建后端
|
||||
npm run build:client # 构建前端
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
npm test # 运行所有测试
|
||||
npm run lint # 代码检查
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
npm run docker:up # 启动容器
|
||||
npm run docker:down # 停止容器
|
||||
npm run docker:build # 构建镜像
|
||||
npm run docker:logs # 查看日志
|
||||
```
|
||||
|
||||
## 🎯 第一个任务:创建角色卡
|
||||
|
||||
> ⚠️ 注意: 当前项目处于架构搭建阶段,UI 组件为占位符。以下流程展示预期的使用方式。
|
||||
|
||||
### 通过 API 创建角色
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/characters \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "艾莉丝",
|
||||
"description": "一位勇敢的冒险者",
|
||||
"personality": "开朗、勇敢、幽默",
|
||||
"scenario": "在一个魔法世界中冒险",
|
||||
"first_mes": "你好!我是艾莉丝,很高兴认识你!",
|
||||
"mes_example": ""
|
||||
}'
|
||||
```
|
||||
|
||||
### 导入 SillyTavern 角色卡
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/import/character \
|
||||
-F "file=@/path/to/character.png"
|
||||
```
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 问题: 端口被占用
|
||||
|
||||
**错误**: `Error: listen EADDRINUSE: address already in use :::3000`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# Windows
|
||||
netstat -ano | findstr :3000
|
||||
taskkill /PID <PID> /F
|
||||
|
||||
# macOS/Linux
|
||||
lsof -ti:3000 | xargs kill -9
|
||||
```
|
||||
|
||||
或修改 `.env` 中的 `PORT` 变量。
|
||||
|
||||
### 问题: 依赖安装失败
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 清除缓存
|
||||
npm cache clean --force
|
||||
|
||||
# 删除 node_modules
|
||||
rm -rf node_modules server/node_modules client/node_modules shared/node_modules
|
||||
|
||||
# 重新安装
|
||||
npm install
|
||||
```
|
||||
|
||||
### 问题: TypeScript 编译错误
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 清除构建缓存
|
||||
cd server && rm -rf dist
|
||||
cd client && rm -rf dist
|
||||
|
||||
# 重新构建
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 问题: Docker 容器无法启动
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 查看详细日志
|
||||
docker-compose logs backend
|
||||
docker-compose logs frontend
|
||||
|
||||
# 重建容器
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📚 学习资源
|
||||
|
||||
### 项目文档
|
||||
- [ARCHITECTURE.md](./ARCHITECTURE.md) - 详细架构设计
|
||||
- [README.md](./README.md) - 项目概览
|
||||
|
||||
### 技术栈文档
|
||||
- [NestJS 官方文档](https://docs.nestjs.com/)
|
||||
- [Vue 3 官方文档](https://vuejs.org/)
|
||||
- [Vercel AI SDK](https://sdk.vercel.ai/docs)
|
||||
- [Zod 文档](https://zod.dev/)
|
||||
- [Pinia 文档](https://pinia.vuejs.org/)
|
||||
|
||||
### SillyTavern 参考
|
||||
- [SillyTavern 官方文档](https://st-docs.role.fun/)
|
||||
- [角色卡规范 V2](https://github.com/malfoyslastname/character-card-spec-v2)
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
### 添加新功能
|
||||
|
||||
1. **定义类型** (shared/types/)
|
||||
```typescript
|
||||
// shared/types/my-feature.types.ts
|
||||
export interface MyFeature {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
2. **创建后端模块** (server/src/modules/my-feature/)
|
||||
```typescript
|
||||
// server/src/modules/my-feature/my-feature.module.ts
|
||||
@Module({
|
||||
controllers: [MyFeatureController],
|
||||
providers: [MyFeatureService],
|
||||
})
|
||||
export class MyFeatureModule {}
|
||||
```
|
||||
|
||||
3. **创建前端组件** (client/src/components/MyFeature/)
|
||||
```vue
|
||||
<!-- client/src/components/MyFeature/MyFeature.vue -->
|
||||
<template>
|
||||
<div>My Feature</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
4. **注册模块**
|
||||
- 在 `server/src/app.module.ts` 中添加后端模块
|
||||
- 在 `client/src/router/index.ts` 中添加路由
|
||||
|
||||
### 提交 PR
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启 Pull Request
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- 📖 查阅 [ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||
- 🐛 提交 [GitHub Issue](../../issues)
|
||||
- 💬 参与讨论 (如果有社区)
|
||||
|
||||
---
|
||||
|
||||
**祝你使用愉快!** 🎉
|
||||
|
||||
如有问题或建议,欢迎反馈。
|
||||
252
README.md
Normal file
252
README.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# SillyTavern Repalice
|
||||
|
||||
一个基于 SillyTavern 灵感的 AI 角色扮演聊天应用,具有自定义扩展功能。
|
||||
|
||||
## 🏗️ 技术栈
|
||||
|
||||
- **前端**: Vue 3 + Vite + TypeScript + Pinia
|
||||
- **后端**: NestJS + TypeScript
|
||||
- **共享层**: Zod schemas + TypeScript types
|
||||
- **LLM**: Vercel AI SDK v4
|
||||
- **持久化**: 本地文件系统(无数据库)
|
||||
- **部署**: Docker Compose
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
sillytavern-repalice/
|
||||
├── shared/ # 前后端共享模块
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ │ ├── sillytavern.types.ts # SillyTavern 兼容格式
|
||||
│ │ ├── internal.types.ts # 项目内部扩展格式
|
||||
│ │ └── converters.ts # 数据转换器
|
||||
│ ├── schemas/ # Zod 运行时验证 schemas
|
||||
│ │ ├── sillytavern.schemas.ts
|
||||
│ │ └── internal.schemas.ts
|
||||
│ └── index.ts # 统一导出
|
||||
│
|
||||
├── server/ # 后端 NestJS 应用
|
||||
│ ├── src/
|
||||
│ │ ├── modules/ # 业务模块
|
||||
│ │ │ ├── chat/ # 聊天管理
|
||||
│ │ │ ├── character/ # 角色卡管理
|
||||
│ │ │ ├── llm/ # LLM 集成
|
||||
│ │ │ ├── import-export/ # 导入导出
|
||||
│ │ │ └── workflow/ # 工作流引擎
|
||||
│ │ ├── persistence/ # 持久化层
|
||||
│ │ │ ├── interfaces/ # 仓储接口
|
||||
│ │ │ ├── implementations/ # 具体实现
|
||||
│ │ │ └── file-system/ # 文件系统实现
|
||||
│ │ ├── controllers/ # 路由控制器
|
||||
│ │ ├── services/ # 业务逻辑服务
|
||||
│ │ ├── core/ # 核心基础设施
|
||||
│ │ │ ├── config/ # 配置管理
|
||||
│ │ │ ├── filters/ # 异常过滤器
|
||||
│ │ │ ├── interceptors/ # 拦截器
|
||||
│ │ │ ├── guards/ # 守卫
|
||||
│ │ │ └── di/ # 依赖注入 tokens
|
||||
│ │ ├── tools/ # 工具层
|
||||
│ │ │ ├── context-chunker.ts # 上下文分块
|
||||
│ │ │ ├── prompt-assembler.ts # 提示词组装
|
||||
│ │ │ └── token-counter.ts # Token 计数
|
||||
│ │ ├── dto/ # 数据传输对象
|
||||
│ │ └── interfaces/ # 接口定义
|
||||
│ └── test/ # 测试文件
|
||||
│ ├── unit/ # 单元测试
|
||||
│ ├── integration/ # 集成测试
|
||||
│ └── e2e/ # 端到端测试
|
||||
│
|
||||
├── client/ # 前端 Vue3 应用
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # 组件
|
||||
│ │ │ ├── TopBar/ # 顶部栏
|
||||
│ │ │ ├── LeftPanel/ # 左侧面板(角色列表、聊天历史)
|
||||
│ │ │ ├── CenterPanel/ # 中央面板(聊天界面)
|
||||
│ │ │ ├── RightPanel/ # 右侧面板(角色详情、工作流编辑器)
|
||||
│ │ │ └── common/ # 通用组件
|
||||
│ │ ├── layouts/ # 布局组件
|
||||
│ │ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── composables/ # 组合式函数
|
||||
│ │ ├── api/ # API 客户端
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── constants/ # 常量定义
|
||||
│ │ ├── styles/ # 全局样式
|
||||
│ │ ├── App.vue # 根组件
|
||||
│ │ └── main.ts # 入口文件
|
||||
│ └── public/ # 静态资源
|
||||
│
|
||||
├── data/ # 持久化数据目录(运行时生成)
|
||||
│ ├── characters/ # 角色卡数据
|
||||
│ ├── chats/ # 聊天记录
|
||||
│ ├── worldinfo/ # 世界书数据
|
||||
│ ├── presets/ # 预设配置
|
||||
│ └── workflows/ # 工作流定义
|
||||
│
|
||||
├── docker/ # Docker 配置
|
||||
│ ├── backend.Dockerfile
|
||||
│ ├── frontend.Dockerfile
|
||||
│ └── nginx/
|
||||
│ └── default.conf
|
||||
│
|
||||
├── docker-compose.yml # Docker Compose 配置
|
||||
├── package.json # 根 package.json (monorepo)
|
||||
└── .env.example # 环境变量示例
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Node.js >= 20.x
|
||||
- npm >= 10.x
|
||||
- Docker & Docker Compose (可选,用于容器化部署)
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. **克隆仓库**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd sillytavern-repalice
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件,填入你的 LLM API 密钥
|
||||
```
|
||||
|
||||
4. **启动开发服务器**
|
||||
```bash
|
||||
# 同时启动前后端
|
||||
npm run dev
|
||||
|
||||
# 或者分别启动
|
||||
npm run dev:server # 后端 http://localhost:3000
|
||||
npm run dev:client # 前端 http://localhost:5173
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
npm run docker:build
|
||||
|
||||
# 启动服务
|
||||
npm run docker:up
|
||||
|
||||
# 查看日志
|
||||
npm run docker:logs
|
||||
|
||||
# 停止服务
|
||||
npm run docker:down
|
||||
```
|
||||
|
||||
## 📊 数据架构
|
||||
|
||||
### 双格式设计
|
||||
|
||||
本项目采用**双数据格式**设计:
|
||||
|
||||
1. **SillyTavern 兼容格式** (`shared/types/sillytavern.types.ts`)
|
||||
- 严格遵循 SillyTavern 官方规范
|
||||
- 用于导入导出,确保兼容性
|
||||
- 包含:角色卡 V2/V3、世界书、聊天记录 JSONL
|
||||
|
||||
2. **项目内部格式** (`shared/types/internal.types.ts`)
|
||||
- 继承 SillyTavern 格式并扩展
|
||||
- 添加自定义字段和功能
|
||||
- 支持:激活方式枚举、RAG 配置、逻辑表达式、工作流等
|
||||
|
||||
3. **数据转换器** (`shared/types/converters.ts`)
|
||||
- 在两种格式之间无缝转换
|
||||
- 保证导入导出的完整性
|
||||
|
||||
### 核心数据类型
|
||||
|
||||
- **WorldInfo (世界书)**: 支持 4 种激活方式(永久、关键词、RAG、逻辑表达式)
|
||||
- **CharacterCard (角色卡)**: 扩展了分类标签、输出 schema、头像管理等
|
||||
- **ChatLog (聊天记录)**: 结构化存储,支持表头数据和消息 swipe
|
||||
- **Workflow (工作流)**: 可视化编排,支持并行执行和动态拼接
|
||||
- **GenerationPreset (预设)**: LLM 采样参数配置
|
||||
|
||||
## 🏛️ 架构设计原则
|
||||
|
||||
### MVC 分层架构
|
||||
|
||||
- **Model (模型层)**:
|
||||
- Services (`server/src/services/`)
|
||||
- Persistence (`server/src/persistence/`)
|
||||
|
||||
- **View (视图层)**:
|
||||
- Components (`client/src/components/`)
|
||||
- Layouts (`client/src/layouts/`)
|
||||
|
||||
- **Controller (控制层)**:
|
||||
- Controllers (`server/src/controllers/`)
|
||||
- Router (`client/src/router/`)
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- 后端使用 NestJS 内置 DI 容器
|
||||
- 通过 `@Inject()` 装饰器和 Injection Tokens 实现松耦合
|
||||
- 便于单元测试和模块替换
|
||||
|
||||
### 工具层分离
|
||||
|
||||
- **Tools Layer** (`server/src/tools/`):
|
||||
- Context Chunker: 上下文分块管理
|
||||
- Prompt Assembler: 提示词动态组装
|
||||
- Token Counter: Token 计数和优化
|
||||
|
||||
## 🔧 开发指南
|
||||
|
||||
### 添加新功能模块
|
||||
|
||||
1. 在 `shared/types/` 中定义类型
|
||||
2. 在 `shared/schemas/` 中添加 Zod schema
|
||||
3. 创建后端模块: `server/src/modules/<feature>/`
|
||||
4. 创建前端组件: `client/src/components/<Feature>/`
|
||||
5. 实现数据转换器(如需要)
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm test
|
||||
|
||||
# 单元测试
|
||||
cd server && npm run test
|
||||
|
||||
# E2E 测试
|
||||
cd server && npm run test:e2e
|
||||
```
|
||||
|
||||
## 📝 待办事项
|
||||
|
||||
当前项目已完成基础架构搭建,后续需要实现:
|
||||
|
||||
- [ ] 完整的角色卡 CRUD 操作
|
||||
- [ ] 聊天界面的实时交互
|
||||
- [ ] 世界书的可视化管理
|
||||
- [ ] Vercel AI SDK 集成
|
||||
- [ ] 工作流编辑器
|
||||
- [ ] RAG 库管理
|
||||
- [ ] 导入导出功能
|
||||
- [ ] 数据迁移工具
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- [SillyTavern](https://github.com/SillyTavern/SillyTavern) - 灵感来源
|
||||
- [Vercel AI SDK](https://sdk.vercel.ai/docs) - LLM 抽象层
|
||||
- [NestJS](https://nestjs.com/) - 后端框架
|
||||
- [Vue 3](https://vuejs.org/) - 前端框架
|
||||
333
STATUS.md
Normal file
333
STATUS.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# 项目状态清单
|
||||
|
||||
## ✅ 已完成
|
||||
|
||||
### 架构设计
|
||||
- [x] 确定技术栈 (NestJS + Vue3 + Vercel AI SDK)
|
||||
- [x] 设计 MVC 分层架构
|
||||
- [x] 规划目录结构
|
||||
- [x] 定义依赖注入策略
|
||||
- [x] 设计双数据格式策略
|
||||
|
||||
### 共享层 (Shared)
|
||||
- [x] SillyTavern 兼容类型定义 (`sillytavern.types.ts`)
|
||||
- [x] WorldInfo (世界书)
|
||||
- [x] CharacterCard (角色卡 V2/V3)
|
||||
- [x] ChatLog (聊天记录 JSONL)
|
||||
- [x] GenerationPreset (预设)
|
||||
- [x] 项目内部扩展类型定义 (`internal.types.ts`)
|
||||
- [x] 4种激活方式枚举 (PERMANENT, KEYWORD, RAG, LOGIC)
|
||||
- [x] LogicExpression (逻辑表达式)
|
||||
- [x] RAGConfig (RAG 配置)
|
||||
- [x] OutputSchema (Vercel AI SDK 输出 schema)
|
||||
- [x] Workflow (工作流定义)
|
||||
- [x] 数据转换器 (`converters.ts`)
|
||||
- [x] ST ↔ Internal 双向转换
|
||||
- [x] 日期格式规范化
|
||||
- [x] 激活类型推断
|
||||
- [x] Zod Schemas 运行时验证
|
||||
- [x] SillyTavern schemas
|
||||
- [x] Internal schemas
|
||||
|
||||
### 后端 (Server)
|
||||
- [x] NestJS 项目骨架
|
||||
- [x] TypeScript 配置
|
||||
- [x] 主入口文件 (`main.ts`)
|
||||
- [x] 应用模块 (`app.module.ts`)
|
||||
- [x] 核心基础设施
|
||||
- [x] HTTP 异常过滤器
|
||||
- [x] 日志拦截器
|
||||
- [x] 响应拦截器
|
||||
- [x] 业务模块占位
|
||||
- [x] ChatModule
|
||||
- [x] CharacterModule
|
||||
- [x] LLMModule
|
||||
- [x] ImportExportModule
|
||||
- [x] WorkflowModule
|
||||
- [x] 持久化层骨架
|
||||
- [x] PersistenceModule
|
||||
- [x] FileSystemRepository (占位)
|
||||
|
||||
### 前端 (Client)
|
||||
- [x] Vue3 + Vite 项目骨架
|
||||
- [x] TypeScript 配置
|
||||
- [x] Vite 配置 (含代理)
|
||||
- [x] 路由配置 (`router/index.ts`)
|
||||
- [x] Pinia Store (`useAppStore.ts`)
|
||||
- [x] 全局样式
|
||||
- [x] CSS Reset
|
||||
- [x] CSS Variables (主题系统)
|
||||
- [x] 布局组件
|
||||
- [x] MainLayout
|
||||
- [x] TopBar (占位)
|
||||
- [x] LeftPanel (占位)
|
||||
- [x] CenterPanel (占位)
|
||||
- [x] RightPanel (占位)
|
||||
|
||||
### DevOps
|
||||
- [x] Docker Compose 配置
|
||||
- [x] Backend Dockerfile (多阶段构建)
|
||||
- [x] Frontend Dockerfile (多阶段构建 + Nginx)
|
||||
- [x] Nginx 配置 (反向代理 + 静态资源)
|
||||
- [x] .gitignore
|
||||
- [x] .env.example
|
||||
- [x] Monorepo package.json
|
||||
|
||||
### 文档
|
||||
- [x] README.md (项目概览)
|
||||
- [x] ARCHITECTURE.md (架构设计文档)
|
||||
- [x] QUICKSTART.md (快速开始指南)
|
||||
- [x] STATUS.md (本文件)
|
||||
|
||||
---
|
||||
|
||||
## 🚧 进行中
|
||||
|
||||
无 (当前处于架构搭建完成阶段)
|
||||
|
||||
---
|
||||
|
||||
## ⏳ 待实现
|
||||
|
||||
### 优先级 P0 (核心功能)
|
||||
|
||||
#### 后端实现
|
||||
- [ ] **持久化层**
|
||||
- [ ] FileSystemRepository 完整实现
|
||||
- [ ] 文件路径管理工具
|
||||
- [ ] 原子写入 (防止数据损坏)
|
||||
- [ ] 自动备份机制
|
||||
|
||||
- [ ] **角色卡模块**
|
||||
- [ ] CharacterController (CRUD 端点)
|
||||
- [ ] CharacterService (业务逻辑)
|
||||
- [ ] PNG 元数据解析 (嵌入/提取 JSON)
|
||||
- [ ] 角色卡验证器
|
||||
|
||||
- [ ] **聊天模块**
|
||||
- [ ] ChatController (CRUD 端点)
|
||||
- [ ] ChatService (业务逻辑)
|
||||
- [ ] JSONL 文件读写
|
||||
- [ ] 消息 swipe 管理
|
||||
- [ ] 聊天元数据管理
|
||||
|
||||
- [ ] **LLM 集成**
|
||||
- [ ] Vercel AI SDK 配置
|
||||
- [ ] LLMController (聊天端点)
|
||||
- [ ] LLMService (流式响应)
|
||||
- [ ] Provider Factory (OpenAI, Anthropic)
|
||||
- [ ] Token 计数和限制
|
||||
|
||||
- [ ] **世界书模块**
|
||||
- [ ] WorldInfoController
|
||||
- [ ] WorldInfoService
|
||||
- [ ] 激活策略实现
|
||||
- [ ] KeywordActivationStrategy
|
||||
- [ ] PermanentActivationStrategy
|
||||
- [ ] RAGActivationStrategy
|
||||
- [ ] LogicActivationStrategy
|
||||
- [ ] 关键词匹配引擎
|
||||
|
||||
#### 前端实现
|
||||
- [ ] **API 客户端**
|
||||
- [ ] Axios 实例配置
|
||||
- [ ] 请求/响应拦截器
|
||||
- [ ] 错误处理
|
||||
- [ ] API 方法封装
|
||||
|
||||
- [ ] **状态管理**
|
||||
- [ ] useCharacterStore
|
||||
- [ ] useChatStore
|
||||
- [ ] useWorldInfoStore
|
||||
- [ ] useLLMStore
|
||||
|
||||
- [ ] **TopBar 组件**
|
||||
- [ ] 模型切换器
|
||||
- [ ] 快速操作按钮
|
||||
- [ ] 主题切换
|
||||
|
||||
- [ ] **LeftPanel 组件**
|
||||
- [ ] CharacterList 组件
|
||||
- [ ] 角色卡片展示
|
||||
- [ ] 搜索和过滤
|
||||
- [ ] 拖拽排序
|
||||
- [ ] ChatHistory 组件
|
||||
- [ ] 聊天列表
|
||||
- [ ] 新建聊天
|
||||
- [ ] 删除/重命名
|
||||
|
||||
- [ ] **CenterPanel 组件**
|
||||
- [ ] MessageList 组件
|
||||
- [ ] 消息渲染
|
||||
- [ ] Swipe 功能
|
||||
- [ ] 滚动加载
|
||||
- [ ] ChatInput 组件
|
||||
- [ ] 文本输入框
|
||||
- [ ] 发送按钮
|
||||
- [ ] 快捷命令
|
||||
- [ ] 流式响应显示
|
||||
|
||||
- [ ] **RightPanel 组件**
|
||||
- [ ] CharacterDetail 组件
|
||||
- [ ] 角色信息编辑
|
||||
- [ ] 头像上传
|
||||
- [ ] 世界书绑定
|
||||
- [ ] WorkflowEditor 组件 (基础版)
|
||||
- [ ] 节点拖拽
|
||||
- [ ] 连接线绘制
|
||||
|
||||
### 优先级 P1 (增强功能)
|
||||
|
||||
- [ ] **导入导出模块**
|
||||
- [ ] SillyTavern 格式导入
|
||||
- [ ] SillyTavern 格式导出
|
||||
- [ ] 批量导入
|
||||
- [ ] 数据迁移工具
|
||||
|
||||
- [ ] **工作流引擎**
|
||||
- [ ] WorkflowService (执行引擎)
|
||||
- [ ] 并行节点执行
|
||||
- [ ] 条件分支
|
||||
- [ ] 动态提示词拼接
|
||||
|
||||
- [ ] **RAG 库管理**
|
||||
- [ ] RAGLibrary CRUD
|
||||
- [ ] 文档上传和解析
|
||||
- [ ] Embedding 生成
|
||||
- [ ] 向量检索
|
||||
|
||||
- [ ] **预设管理**
|
||||
- [ ] Preset CRUD
|
||||
- [ ] 预设切换
|
||||
- [ ] 采样参数 UI
|
||||
|
||||
- [ ] **通用组件库**
|
||||
- [ ] BaseButton
|
||||
- [ ] BaseIcon
|
||||
- [ ] BaseModal
|
||||
- [ ] BaseInput
|
||||
- [ ] BaseSelect
|
||||
|
||||
### 优先级 P2 (优化和测试)
|
||||
|
||||
- [ ] **性能优化**
|
||||
- [ ] 聊天列表虚拟滚动
|
||||
- [ ] 图片懒加载
|
||||
- [ ] API 响应缓存
|
||||
- [ ] LLM 响应缓存
|
||||
|
||||
- [ ] **测试**
|
||||
- [ ] 单元测试 (Jest)
|
||||
- [ ] Services 测试
|
||||
- [ ] Tools 测试
|
||||
- [ ] Converters 测试
|
||||
- [ ] 集成测试
|
||||
- [ ] Controller + Service 测试
|
||||
- [ ] E2E 测试 (Playwright)
|
||||
- [ ] 关键用户流程
|
||||
|
||||
- [ ] **安全性**
|
||||
- [ ] 输入 sanitization
|
||||
- [ ] 文件上传扫描
|
||||
- [ ] API 速率限制
|
||||
- [ ] CORS 严格配置
|
||||
|
||||
- [ ] **错误处理**
|
||||
- [ ] 全局错误边界
|
||||
- [ ] 友好的错误提示
|
||||
- [ ] 错误日志记录
|
||||
|
||||
- [ ] **文档**
|
||||
- [ ] API 文档 (Swagger/OpenAPI)
|
||||
- [ ] 用户手册
|
||||
- [ ] 开发者贡献指南
|
||||
|
||||
### 优先级 P3 (额外功能)
|
||||
|
||||
- [ ] **国际化 (i18n)**
|
||||
- [ ] 中文界面
|
||||
- [ ] 英文界面
|
||||
- [ ] 语言切换
|
||||
|
||||
- [ ] **主题系统**
|
||||
- [ ] 暗色/亮色主题
|
||||
- [ ] 自定义主题色
|
||||
- [ ] 主题保存/加载
|
||||
|
||||
- [ ] **快捷键**
|
||||
- [ ] 常用操作快捷键
|
||||
- [ ] 快捷键自定义
|
||||
|
||||
- [ ] **插件系统**
|
||||
- [ ] 插件加载器
|
||||
- [ ] 插件 API
|
||||
- [ ] 示例插件
|
||||
|
||||
---
|
||||
|
||||
## 📊 进度统计
|
||||
|
||||
| 类别 | 已完成 | 总数 | 完成率 |
|
||||
|------|--------|------|--------|
|
||||
| **架构设计** | 5 | 5 | 100% |
|
||||
| **共享层** | 6 | 6 | 100% |
|
||||
| **后端骨架** | 8 | 8 | 100% |
|
||||
| **前端骨架** | 9 | 9 | 100% |
|
||||
| **DevOps** | 7 | 7 | 100% |
|
||||
| **文档** | 4 | 4 | 100% |
|
||||
| **核心功能** | 0 | ~50 | 0% |
|
||||
| **增强功能** | 0 | ~20 | 0% |
|
||||
| **测试优化** | 0 | ~15 | 0% |
|
||||
| **总计** | **39** | **~114** | **~34%** |
|
||||
|
||||
> 注: 当前处于**架构搭建完成**阶段,下一步开始实现核心业务逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一个里程碑
|
||||
|
||||
**Milestone 1: 基础 CRUD (预计 2-3 周)**
|
||||
- [ ] 完整的角色卡管理 (创建、读取、更新、删除)
|
||||
- [ ] 基础的聊天功能 (发送消息、接收回复)
|
||||
- [ ] 简单的 UI 界面 (可操作的组件)
|
||||
- [ ] 本地文件系统持久化
|
||||
|
||||
**Milestone 2: LLM 集成 (预计 1-2 周)**
|
||||
- [ ] Vercel AI SDK 集成
|
||||
- [ ] 流式聊天响应
|
||||
- [ ] 多 Provider 支持
|
||||
|
||||
**Milestone 3: 导入导出 (预计 1 周)**
|
||||
- [ ] SillyTavern 格式兼容
|
||||
- [ ] 数据转换器完善
|
||||
|
||||
**Milestone 4: 高级功能 (预计 3-4 周)**
|
||||
- [ ] 世界书管理
|
||||
- [ ] 工作流编辑器
|
||||
- [ ] RAG 库
|
||||
|
||||
---
|
||||
|
||||
## 📝 开发笔记
|
||||
|
||||
### 当前状态
|
||||
- ✅ 项目架构已搭建完成
|
||||
- ✅ 所有占位文件已创建
|
||||
- ✅ 类型定义完整
|
||||
- ⏸️ 等待开始实现具体业务逻辑
|
||||
|
||||
### 已知问题
|
||||
- 无 (尚未开始实现)
|
||||
|
||||
### 技术债务
|
||||
- 无 (项目刚启动)
|
||||
|
||||
### 风险提示
|
||||
1. **SillyTavern 格式兼容性**: 需要持续对照官方文档验证
|
||||
2. **Vercel AI SDK v4**: 较新版本,可能需要探索最佳实践
|
||||
3. **工作流引擎**: 复杂度较高,需要仔细设计
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-04-24
|
||||
**更新者**: AI Assistant
|
||||
30
client/package.json
Normal file
30
client/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@sillytavern-repalice/client",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend client for SillyTavern Repalice",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.5",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
1
client/src/.gitkeep
Normal file
1
client/src/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Vue3 Frontend Directory Structure
|
||||
1
client/src/api/.gitkeep
Normal file
1
client/src/api/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// API Client and Services will be placed here
|
||||
1
client/src/assets/.gitkeep
Normal file
1
client/src/assets/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Static Assets (images, fonts, etc.) will be placed here
|
||||
1
client/src/components/CenterPanel/.gitkeep
Normal file
1
client/src/components/CenterPanel/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Center Panel Components will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Chat Input Feature will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Message List Feature will be placed here
|
||||
1
client/src/components/LeftPanel/.gitkeep
Normal file
1
client/src/components/LeftPanel/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Left Panel Components will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Character List Feature will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Chat History Feature will be placed here
|
||||
1
client/src/components/RightPanel/.gitkeep
Normal file
1
client/src/components/RightPanel/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Right Panel Components will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Character Detail Feature will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Workflow Editor Feature will be placed here
|
||||
1
client/src/components/TopBar/.gitkeep
Normal file
1
client/src/components/TopBar/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Top Bar Components will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Model Switcher Feature will be placed here
|
||||
@@ -0,0 +1 @@
|
||||
// Quick Actions Feature will be placed here
|
||||
1
client/src/components/common/.gitkeep
Normal file
1
client/src/components/common/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Reusable Base Components will be placed here
|
||||
1
client/src/composables/.gitkeep
Normal file
1
client/src/composables/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Composables (Vue 3 Composition API) will be placed here
|
||||
1
client/src/constants/.gitkeep
Normal file
1
client/src/constants/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Constants and Configuration will be placed here
|
||||
1
client/src/layouts/.gitkeep
Normal file
1
client/src/layouts/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Layout Components will be placed here
|
||||
1
client/src/router/.gitkeep
Normal file
1
client/src/router/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Vue Router Configuration will be placed here
|
||||
1
client/src/stores/.gitkeep
Normal file
1
client/src/stores/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Pinia Stores will be placed here
|
||||
1
client/src/styles/.gitkeep
Normal file
1
client/src/styles/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Global Styles and CSS Variables will be placed here
|
||||
1
client/src/utils/.gitkeep
Normal file
1
client/src/utils/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Utility Functions will be placed here
|
||||
27
client/tsconfig.json
Normal file
27
client/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@shared/*": ["../shared/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
client/tsconfig.node.json
Normal file
10
client/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
22
client/vite.config.ts
Normal file
22
client/vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, '../shared'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "sillytavern-repalice",
|
||||
"version": "1.0.0",
|
||||
"description": "A SillyTavern-inspired AI chat application with custom features",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"shared",
|
||||
"server",
|
||||
"client"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
||||
"dev:server": "cd server && npm run start:dev",
|
||||
"dev:client": "cd client && npm run dev",
|
||||
"build": "npm run build --workspaces",
|
||||
"build:server": "cd server && npm run build",
|
||||
"build:client": "cd client && npm run build",
|
||||
"start": "cd server && npm run start:prod",
|
||||
"test": "npm run test --workspaces",
|
||||
"lint": "npm run lint --workspaces",
|
||||
"docker:up": "docker-compose up -d",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:build": "docker-compose build",
|
||||
"docker:logs": "docker-compose logs -f"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2"
|
||||
}
|
||||
}
|
||||
10
server/nest-cli.json
Normal file
10
server/nest-cli.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"webpack": false,
|
||||
"tsConfigPath": "tsconfig.json"
|
||||
}
|
||||
}
|
||||
54
server/package.json
Normal file
54
server/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@sillytavern-repalice/server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend server for SillyTavern Repalice",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"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": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.22.4",
|
||||
"ai": "^4.0.0",
|
||||
"@ai-sdk/openai": "^1.0.0",
|
||||
"@ai-sdk/anthropic": "^1.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.3.0",
|
||||
"@nestjs/schematics": "^10.1.0",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.1.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
1
server/src/.gitkeep
Normal file
1
server/src/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# NestJS Backend Directory Structure
|
||||
1
server/src/controllers/.gitkeep
Normal file
1
server/src/controllers/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Controllers will be placed here
|
||||
1
server/src/core/config/.gitkeep
Normal file
1
server/src/core/config/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Configuration Management will be placed here
|
||||
1
server/src/core/di/.gitkeep
Normal file
1
server/src/core/di/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Dependency Injection Tokens will be placed here
|
||||
1
server/src/core/filters/.gitkeep
Normal file
1
server/src/core/filters/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Exception Filters will be placed here
|
||||
1
server/src/core/guards/.gitkeep
Normal file
1
server/src/core/guards/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Guards and Middleware will be placed here
|
||||
1
server/src/core/interceptors/.gitkeep
Normal file
1
server/src/core/interceptors/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Request/Response Interceptors will be placed here
|
||||
1
server/src/core/utils/.gitkeep
Normal file
1
server/src/core/utils/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Utility Functions and Helpers will be placed here
|
||||
1
server/src/dto/.gitkeep
Normal file
1
server/src/dto/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// DTOs and Validation will be placed here
|
||||
1
server/src/interfaces/.gitkeep
Normal file
1
server/src/interfaces/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Interfaces for Dependency Injection will be placed here
|
||||
1
server/src/llm/.gitkeep
Normal file
1
server/src/llm/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Vercel AI SDK integration will be placed here
|
||||
1
server/src/llm/providers/.gitkeep
Normal file
1
server/src/llm/providers/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// LLM Providers (OpenAI, Claude, Local, etc.) will be placed here
|
||||
1
server/src/llm/tools/.gitkeep
Normal file
1
server/src/llm/tools/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// LLM Tools and Function Calling will be placed here
|
||||
1
server/src/modules/character/.gitkeep
Normal file
1
server/src/modules/character/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Character Module will be placed here
|
||||
10
server/src/modules/character/character.module.ts
Normal file
10
server/src/modules/character/character.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CharacterController } from '../../controllers/character.controller';
|
||||
import { CharacterService } from '../../services/character.service';
|
||||
|
||||
@Module({
|
||||
controllers: [CharacterController],
|
||||
providers: [CharacterService],
|
||||
exports: [CharacterService],
|
||||
})
|
||||
export class CharacterModule {}
|
||||
1
server/src/modules/chat/.gitkeep
Normal file
1
server/src/modules/chat/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Chat Module will be placed here
|
||||
10
server/src/modules/chat/chat.module.ts
Normal file
10
server/src/modules/chat/chat.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ChatController } from '../../controllers/chat.controller';
|
||||
import { ChatService } from '../../services/chat.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ChatController],
|
||||
providers: [ChatService],
|
||||
exports: [ChatService],
|
||||
})
|
||||
export class ChatModule {}
|
||||
1
server/src/modules/import-export/.gitkeep
Normal file
1
server/src/modules/import-export/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Import/Export Module will be placed here
|
||||
7
server/src/modules/import-export/import-export.module.ts
Normal file
7
server/src/modules/import-export/import-export.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ImportExportController } from '../../controllers/import-export.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [ImportExportController],
|
||||
})
|
||||
export class ImportExportModule {}
|
||||
1
server/src/modules/llm/.gitkeep
Normal file
1
server/src/modules/llm/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// LLM Module will be placed here
|
||||
7
server/src/modules/llm/llm.module.ts
Normal file
7
server/src/modules/llm/llm.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LLMController } from '../../controllers/llm.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [LLMController],
|
||||
})
|
||||
export class LLMModule {}
|
||||
1
server/src/modules/workflow/.gitkeep
Normal file
1
server/src/modules/workflow/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Workflow Module will be placed here
|
||||
6
server/src/modules/workflow/workflow.module.ts
Normal file
6
server/src/modules/workflow/workflow.module.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
})
|
||||
export class WorkflowModule {}
|
||||
1
server/src/persistence/.gitkeep
Normal file
1
server/src/persistence/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Persistence Module will be placed here
|
||||
1
server/src/persistence/implementations/.gitkeep
Normal file
1
server/src/persistence/implementations/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// File System Implementations will be placed here
|
||||
1
server/src/persistence/interfaces/.gitkeep
Normal file
1
server/src/persistence/interfaces/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Repository Interfaces will be placed here
|
||||
1
server/src/persistence/migrations/.gitkeep
Normal file
1
server/src/persistence/migrations/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Data Migration Scripts will be placed here
|
||||
1
server/src/services/.gitkeep
Normal file
1
server/src/services/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Services (Business Logic) will be placed here
|
||||
1
server/src/tools/context/.gitkeep
Normal file
1
server/src/tools/context/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Context Management and Chunking will be placed here
|
||||
1
server/src/tools/prompt/.gitkeep
Normal file
1
server/src/tools/prompt/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Prompt Assembly and Templates will be placed here
|
||||
1
server/src/tools/token/.gitkeep
Normal file
1
server/src/tools/token/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Token Counting and Estimation will be placed here
|
||||
1
server/src/tools/workflow/.gitkeep
Normal file
1
server/src/tools/workflow/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Workflow Engine will be placed here
|
||||
1
server/test/e2e/.gitkeep
Normal file
1
server/test/e2e/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// E2E Tests will be placed here
|
||||
1
server/test/integration/.gitkeep
Normal file
1
server/test/integration/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Integration Tests will be placed here
|
||||
1
server/test/unit/.gitkeep
Normal file
1
server/test/unit/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Unit Tests will be placed here
|
||||
28
server/tsconfig.json
Normal file
28
server/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"paths": {
|
||||
"@shared/*": ["../shared/*"],
|
||||
"@modules/*": ["./src/modules/*"],
|
||||
"@core/*": ["./src/core/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
1
shared/constants/.gitkeep
Normal file
1
shared/constants/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Shared Constants and Enums
|
||||
14
shared/index.ts
Normal file
14
shared/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Shared Module
|
||||
* 前后端共享的类型定义、验证 schemas 和工具函数
|
||||
*/
|
||||
|
||||
// 类型定义
|
||||
export * as STTypes from './types/sillytavern.types';
|
||||
export * as InternalTypes from './types/internal.types';
|
||||
export * from './types';
|
||||
|
||||
// Zod Schemas
|
||||
export * as STSchemas from './schemas/sillytavern.schemas';
|
||||
export * as InternalSchemas from './schemas/internal.schemas';
|
||||
export * from './schemas';
|
||||
11
shared/package.json
Normal file
11
shared/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@sillytavern-repalice/shared",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared types and schemas for SillyTavern Repalice",
|
||||
"main": "index.ts",
|
||||
"types": "index.ts",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
1
shared/schemas/.gitkeep
Normal file
1
shared/schemas/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Zod Schemas for Validation (v3)
|
||||
6
shared/schemas/index.ts
Normal file
6
shared/schemas/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Shared Schemas 导出
|
||||
*/
|
||||
|
||||
export * from './sillytavern.schemas';
|
||||
export * from './internal.schemas';
|
||||
258
shared/schemas/internal.schemas.ts
Normal file
258
shared/schemas/internal.schemas.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Zod Schemas - 项目内部格式验证
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// ==================== 世界书条目 ====================
|
||||
|
||||
export const ActivationTypeSchema = z.enum([
|
||||
'permanent',
|
||||
'keyword',
|
||||
'rag',
|
||||
'logic',
|
||||
]);
|
||||
|
||||
export const LogicOperatorSchema = z.enum([
|
||||
'equals',
|
||||
'not_equals',
|
||||
'contains',
|
||||
'not_contains',
|
||||
'greater',
|
||||
'less',
|
||||
]);
|
||||
|
||||
export const LogicExpressionSchema = z.object({
|
||||
variable1: z.string(),
|
||||
operator: LogicOperatorSchema,
|
||||
variable2: z.string(),
|
||||
});
|
||||
|
||||
export const RAGConfigSchema = z.object({
|
||||
libraryId: z.string(),
|
||||
threshold: z.number().min(0).max(1).optional(),
|
||||
maxEntries: z.number().min(1).optional(),
|
||||
});
|
||||
|
||||
export const WorldInfoEntrySchema = z.object({
|
||||
uid: z.string(),
|
||||
key: z.array(z.string()).optional(),
|
||||
keysecondary: z.array(z.string()).optional(),
|
||||
content: z.string(),
|
||||
order: z.number(),
|
||||
position: z.enum([
|
||||
'before_char',
|
||||
'after_char',
|
||||
'before_example',
|
||||
'after_example',
|
||||
'author_note_top',
|
||||
'author_note_bottom',
|
||||
'at_depth',
|
||||
]),
|
||||
depth: z.number().optional(),
|
||||
role: z.enum(['system', 'user', 'assistant']).optional(),
|
||||
probability: z.number().min(0).max(100).optional(),
|
||||
group: z.array(z.string()).optional(),
|
||||
groupPrioritize: z.boolean().optional(),
|
||||
useGroupScoring: z.boolean().optional(),
|
||||
automationId: z.string().optional(),
|
||||
disable: z.boolean().optional(),
|
||||
extensions: z.record(z.unknown()).optional(),
|
||||
|
||||
// 自定义字段
|
||||
activationType: ActivationTypeSchema,
|
||||
logicExpression: LogicExpressionSchema.optional(),
|
||||
ragConfig: RAGConfigSchema.optional(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
|
||||
export const WorldInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
entries: z.array(WorldInfoEntrySchema),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
// ==================== 角色卡 ====================
|
||||
|
||||
export const OutputSchemaFieldTypeSchema = z.enum([
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array',
|
||||
'object',
|
||||
]);
|
||||
|
||||
export const OutputSchemaFieldSchema: z.ZodType<any> = z.lazy(() =>
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: OutputSchemaFieldTypeSchema,
|
||||
description: z.string(),
|
||||
required: z.boolean().optional(),
|
||||
enum: z.array(z.string()).optional(),
|
||||
fields: z.array(OutputSchemaFieldSchema).optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const CharacterCardSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
personality: z.string(),
|
||||
scenario: z.string(),
|
||||
first_mes: z.string(),
|
||||
mes_example: z.string(),
|
||||
alternate_greetings: z.array(z.string()).optional(),
|
||||
creator_notes: z.string().optional(),
|
||||
system_prompt: z.string().optional(),
|
||||
post_history_instructions: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
|
||||
// 自定义字段
|
||||
categories: z.array(z.string()),
|
||||
worldInfoId: z.string().optional(),
|
||||
outputSchema: z.array(OutputSchemaFieldSchema).optional(),
|
||||
avatarPath: z.string().optional(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
lastChatAt: z.number().optional(),
|
||||
isFavorite: z.boolean(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
// ==================== 聊天记录 ====================
|
||||
|
||||
export const ChatHeaderSchema = z.object({
|
||||
id: z.string(),
|
||||
displayName: z.string(),
|
||||
characterId: z.string(),
|
||||
userName: z.string(),
|
||||
characterName: z.string(),
|
||||
tableData: z.record(z.unknown()).optional(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
messageCount: z.number(),
|
||||
});
|
||||
|
||||
export const ChatMessageSchema = z.object({
|
||||
id: z.string(),
|
||||
chatId: z.string(),
|
||||
name: z.string(),
|
||||
is_user: z.boolean(),
|
||||
is_system: z.boolean().optional(),
|
||||
sendDate: z.string(),
|
||||
mes: z.string(),
|
||||
swipes: z.array(z.string()).optional(),
|
||||
swipe_id: z.number().optional(),
|
||||
is_hidden: z.boolean().optional(),
|
||||
extra: z.record(z.unknown()).optional(),
|
||||
tokenCount: z.number().optional(),
|
||||
isTemporary: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const ChatLogSchema = z.object({
|
||||
header: ChatHeaderSchema,
|
||||
messages: z.array(ChatMessageSchema),
|
||||
});
|
||||
|
||||
// ==================== 预设 ====================
|
||||
|
||||
export const GenerationPresetSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
temperature: z.number(),
|
||||
topP: z.number(),
|
||||
topK: z.number(),
|
||||
repetitionPenalty: z.number(),
|
||||
frequencyPenalty: z.number().optional(),
|
||||
presencePenalty: z.number().optional(),
|
||||
maxLength: z.number().optional(),
|
||||
isDefault: z.boolean(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
|
||||
// ==================== 提示词预设 (Prompt Preset) ====================
|
||||
|
||||
export const PromptRoleSchema = z.enum(['system', 'ai', 'user']);
|
||||
|
||||
export const PromptEntrySchema = z.object({
|
||||
identifier: z.string(),
|
||||
name: z.string(),
|
||||
enabled: z.boolean(),
|
||||
content: z.string(),
|
||||
order: z.number(),
|
||||
role: PromptRoleSchema,
|
||||
tokenCount: z.number(),
|
||||
isSystemNode: z.boolean(),
|
||||
});
|
||||
|
||||
export const PromptPresetViewSchema = z.object({
|
||||
characterId: z.string(),
|
||||
entries: z.array(PromptEntrySchema),
|
||||
updatedAt: z.number(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
// ==================== 工作流 ====================
|
||||
|
||||
export const WorkflowNodeTypeSchema = z.enum([
|
||||
'llm_call',
|
||||
'condition',
|
||||
'data_process',
|
||||
'parallel',
|
||||
'merge',
|
||||
]);
|
||||
|
||||
export const WorkflowNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
type: WorkflowNodeTypeSchema,
|
||||
name: z.string(),
|
||||
config: z.record(z.unknown()),
|
||||
inputMapping: z.record(z.string()).optional(),
|
||||
outputMapping: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const WorkflowEdgeSchema = z.object({
|
||||
id: z.string(),
|
||||
source: z.string(),
|
||||
target: z.string(),
|
||||
sourcePort: z.string().optional(),
|
||||
targetPort: z.string().optional(),
|
||||
});
|
||||
|
||||
export const WorkflowSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
nodes: z.array(WorkflowNodeSchema),
|
||||
edges: z.array(WorkflowEdgeSchema),
|
||||
entryNodeId: z.string(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
// ==================== RAG 库 ====================
|
||||
|
||||
export const RAGDocumentSchema = z.object({
|
||||
id: z.string(),
|
||||
content: z.string(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
embedding: z.array(z.number()).optional(),
|
||||
createdAt: z.number(),
|
||||
});
|
||||
|
||||
export const RAGLibrarySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
documents: z.array(RAGDocumentSchema),
|
||||
embeddingModel: z.string().optional(),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
157
shared/schemas/sillytavern.schemas.ts
Normal file
157
shared/schemas/sillytavern.schemas.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Zod Schemas - SillyTavern 兼容格式验证
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// ==================== 世界书条目 ====================
|
||||
|
||||
export const STWorldInfoEntryPositionSchema = z.enum([
|
||||
'before_char',
|
||||
'after_char',
|
||||
'before_example',
|
||||
'after_example',
|
||||
'author_note_top',
|
||||
'author_note_bottom',
|
||||
'at_depth',
|
||||
]);
|
||||
|
||||
export const STWorldInfoFilterLogicSchema = z.enum([
|
||||
'and_any',
|
||||
'and_all',
|
||||
'not_any',
|
||||
'not_all',
|
||||
]);
|
||||
|
||||
export const STWorldInfoEntryRoleSchema = z.enum([
|
||||
'system',
|
||||
'user',
|
||||
'assistant',
|
||||
]);
|
||||
|
||||
export const STWorldInfoEntrySchema = z.object({
|
||||
uid: z.string(),
|
||||
key: z.array(z.string()).optional(),
|
||||
keysecondary: z.array(z.string()).optional(),
|
||||
filter: STWorldInfoFilterLogicSchema.optional(),
|
||||
content: z.string(),
|
||||
constant: z.boolean().optional(),
|
||||
selective: z.boolean().optional(),
|
||||
order: z.number(),
|
||||
position: STWorldInfoEntryPositionSchema,
|
||||
depth: z.number().optional(),
|
||||
role: STWorldInfoEntryRoleSchema.optional(),
|
||||
probability: z.number().min(0).max(100).optional(),
|
||||
group: z.array(z.string()).optional(),
|
||||
groupPrioritize: z.boolean().optional(),
|
||||
useGroupScoring: z.boolean().optional(),
|
||||
automationId: z.string().optional(),
|
||||
disable: z.boolean().optional(),
|
||||
extensions: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const STWorldInfoSchema = z.object({
|
||||
spec: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
entries: z.array(STWorldInfoEntrySchema),
|
||||
extensions: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
// ==================== 角色卡 ====================
|
||||
|
||||
export const STCharacterCardSpecSchema = z.enum([
|
||||
'chara_card_v1',
|
||||
'chara_card_v2',
|
||||
'chara_card_v3',
|
||||
]);
|
||||
|
||||
export const STCharacterCardDataSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
personality: z.string(),
|
||||
scenario: z.string(),
|
||||
first_mes: z.string(),
|
||||
mes_example: z.string(),
|
||||
alternate_greetings: z.array(z.string()).optional(),
|
||||
creator_notes: z.string().optional(),
|
||||
system_prompt: z.string().optional(),
|
||||
post_history_instructions: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
character_book: STWorldInfoSchema.optional(),
|
||||
extensions: z.object({
|
||||
world: z.string().optional(),
|
||||
talkativeness: z.number().min(0).max(1).optional(),
|
||||
fav: z.boolean().optional(),
|
||||
}).passthrough().optional(),
|
||||
});
|
||||
|
||||
export const STCharacterCardSchema = z.object({
|
||||
spec: STCharacterCardSpecSchema,
|
||||
spec_version: z.string().optional(),
|
||||
data: STCharacterCardDataSchema,
|
||||
});
|
||||
|
||||
// ==================== 聊天记录 ====================
|
||||
|
||||
export const STChatHeaderSchema = z.object({
|
||||
user_name: z.string(),
|
||||
character_name: z.string(),
|
||||
create_date: z.string(),
|
||||
chat_metadata: z.record(z.unknown()).optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const STChatMessageSchema = z.object({
|
||||
name: z.string(),
|
||||
is_user: z.boolean(),
|
||||
is_system: z.boolean().optional(),
|
||||
send_date: z.union([z.number(), z.string()]),
|
||||
mes: z.string(),
|
||||
swipes: z.array(z.string()).optional(),
|
||||
swipe_id: z.number().optional(),
|
||||
is_hidden: z.boolean().optional(),
|
||||
extra: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
// ==================== 预设 ====================
|
||||
|
||||
export const STGenerationPresetSchema = z.object({
|
||||
name: z.string(),
|
||||
temperature: z.number().optional(),
|
||||
top_p: z.number().optional(),
|
||||
top_k: z.number().optional(),
|
||||
repetition_penalty: z.number().optional(),
|
||||
frequency_penalty: z.number().optional(),
|
||||
presence_penalty: z.number().optional(),
|
||||
max_length: z.number().optional(),
|
||||
}).passthrough();
|
||||
|
||||
// ==================== 提示词预设 (Prompt Preset) ====================
|
||||
|
||||
export const STPromptRoleSchema = z.enum(['system', 'assistant', 'user']);
|
||||
|
||||
export const STPromptSchema = z.object({
|
||||
identifier: z.string(),
|
||||
name: z.string().optional(),
|
||||
content: z.string(),
|
||||
role: STPromptRoleSchema,
|
||||
marker: z.boolean().optional(),
|
||||
injection_order: z.number().optional(),
|
||||
extensions: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const STPromptOrderItemSchema = z.object({
|
||||
identifier: z.string(),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const STPromptOrderConfigSchema = z.object({
|
||||
order: z.array(STPromptOrderItemSchema),
|
||||
});
|
||||
|
||||
export const STPromptPresetSchema = z.object({
|
||||
name: z.string(),
|
||||
prompts: z.array(STPromptSchema),
|
||||
prompt_order: z.record(STPromptOrderConfigSchema),
|
||||
default_prompt_order: STPromptOrderConfigSchema.optional(),
|
||||
});
|
||||
1
shared/types/.gitkeep
Normal file
1
shared/types/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Shared TypeScript Types (Frontend & Backend)
|
||||
545
shared/types/converters.ts
Normal file
545
shared/types/converters.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* 数据转换器
|
||||
* 在 SillyTavern 格式和项目内部格式之间进行转换
|
||||
*/
|
||||
|
||||
import {
|
||||
STWorldInfo,
|
||||
STWorldInfoEntry,
|
||||
STWorldInfoEntryPosition,
|
||||
STCharacterCard,
|
||||
STCharacterCardData,
|
||||
STChatHeader,
|
||||
STChatMessage,
|
||||
STPromptPreset,
|
||||
STPrompt,
|
||||
STPromptOrderConfig,
|
||||
} from './sillytavern.types';
|
||||
import {
|
||||
WorldInfo,
|
||||
WorldInfoEntry,
|
||||
ActivationType,
|
||||
CharacterCard,
|
||||
ChatHeader,
|
||||
ChatMessage,
|
||||
ChatLog,
|
||||
PromptEntry,
|
||||
PromptPresetView,
|
||||
PromptRole,
|
||||
} from './internal.types';
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳
|
||||
*/
|
||||
function now(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
// ==================== 世界书转换 ====================
|
||||
|
||||
/**
|
||||
* 从 SillyTavern 条目激活方式推断激活类型
|
||||
*/
|
||||
function inferActivationType(entry: STWorldInfoEntry): ActivationType {
|
||||
if (entry.disable) {
|
||||
return ActivationType.KEYWORD; // 禁用的条目默认归为关键词类型
|
||||
}
|
||||
|
||||
if (entry.constant === true) {
|
||||
return ActivationType.PERMANENT;
|
||||
}
|
||||
|
||||
if (entry.selective === true) {
|
||||
return ActivationType.RAG;
|
||||
}
|
||||
|
||||
// 默认为关键词触发
|
||||
return ActivationType.KEYWORD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 位置字符串转换为内部枚举
|
||||
*/
|
||||
function convertPosition(position: string): STWorldInfoEntryPosition {
|
||||
// 如果已经是枚举值,直接返回
|
||||
if (Object.values(STWorldInfoEntryPosition).includes(position as STWorldInfoEntryPosition)) {
|
||||
return position as STWorldInfoEntryPosition;
|
||||
}
|
||||
|
||||
// 处理 SillyTavern 的原始字符串格式
|
||||
const positionMap: Record<string, STWorldInfoEntryPosition> = {
|
||||
'Before Char Definition': STWorldInfoEntryPosition.BEFORE_CHAR,
|
||||
'After Char Definition': STWorldInfoEntryPosition.AFTER_CHAR,
|
||||
'Before Example Messages': STWorldInfoEntryPosition.BEFORE_EXAMPLE,
|
||||
'After Example Messages': STWorldInfoEntryPosition.AFTER_EXAMPLE,
|
||||
'Top of Author\'s Note': STWorldInfoEntryPosition.AUTHOR_NOTE_TOP,
|
||||
'Bottom of Author\'s Note': STWorldInfoEntryPosition.AUTHOR_NOTE_BOTTOM,
|
||||
'@D': STWorldInfoEntryPosition.AT_DEPTH,
|
||||
};
|
||||
|
||||
return positionMap[position] || STWorldInfoEntryPosition.AFTER_CHAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 世界书条目转换为内部格式
|
||||
*/
|
||||
export function convertSTEntryToInternal(stEntry: STWorldInfoEntry): WorldInfoEntry {
|
||||
const activationType = inferActivationType(stEntry);
|
||||
|
||||
return {
|
||||
...stEntry,
|
||||
uid: stEntry.uid || generateId(),
|
||||
position: convertPosition(stEntry.position as string),
|
||||
activationType,
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部世界书条目转换为 SillyTavern 格式
|
||||
*/
|
||||
export function convertEntryToST(entry: WorldInfoEntry): STWorldInfoEntry {
|
||||
const stEntry: STWorldInfoEntry = {
|
||||
uid: entry.uid,
|
||||
key: entry.key,
|
||||
keysecondary: entry.keysecondary,
|
||||
content: entry.content,
|
||||
order: entry.order,
|
||||
position: entry.position,
|
||||
depth: entry.depth,
|
||||
role: entry.role,
|
||||
probability: entry.probability,
|
||||
group: entry.group,
|
||||
groupPrioritize: entry.groupPrioritize,
|
||||
useGroupScoring: entry.useGroupScoring,
|
||||
automationId: entry.automationId,
|
||||
disable: entry.disable,
|
||||
extensions: entry.extensions,
|
||||
};
|
||||
|
||||
// 根据激活类型设置 constant 和 selective
|
||||
switch (entry.activationType) {
|
||||
case ActivationType.PERMANENT:
|
||||
stEntry.constant = true;
|
||||
stEntry.selective = false;
|
||||
break;
|
||||
case ActivationType.RAG:
|
||||
stEntry.constant = false;
|
||||
stEntry.selective = true;
|
||||
break;
|
||||
case ActivationType.KEYWORD:
|
||||
case ActivationType.LOGIC:
|
||||
default:
|
||||
stEntry.constant = false;
|
||||
stEntry.selective = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return stEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 世界书转换为内部格式
|
||||
*/
|
||||
export function convertSTWorldInfoToInternal(stWorldInfo: STWorldInfo): WorldInfo {
|
||||
return {
|
||||
id: generateId(),
|
||||
name: stWorldInfo.name || 'Untitled World Info',
|
||||
description: stWorldInfo.description,
|
||||
entries: stWorldInfo.entries.map(convertSTEntryToInternal),
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部世界书转换为 SillyTavern 格式
|
||||
*/
|
||||
export function convertWorldInfoToST(worldInfo: WorldInfo): STWorldInfo {
|
||||
return {
|
||||
spec: 'world_info_v1',
|
||||
name: worldInfo.name,
|
||||
description: worldInfo.description,
|
||||
entries: worldInfo.entries.map(convertEntryToST),
|
||||
extensions: {
|
||||
originalId: worldInfo.id,
|
||||
version: worldInfo.version,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 角色卡转换 ====================
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 角色卡转换为内部格式
|
||||
*/
|
||||
export function convertSTCharacterCardToInternal(
|
||||
stCard: STCharacterCard,
|
||||
avatarPath?: string
|
||||
): CharacterCard {
|
||||
const data = stCard.data;
|
||||
|
||||
// 提取绑定的世界书 ID(如果有)
|
||||
const worldInfoId = data.extensions?.world as string | undefined;
|
||||
|
||||
return {
|
||||
id: generateId(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
personality: data.personality,
|
||||
scenario: data.scenario,
|
||||
first_mes: data.first_mes,
|
||||
mes_example: data.mes_example,
|
||||
alternate_greetings: data.alternate_greetings || [],
|
||||
creator_notes: data.creator_notes,
|
||||
system_prompt: data.system_prompt,
|
||||
post_history_instructions: data.post_history_instructions,
|
||||
tags: data.tags || [],
|
||||
categories: data.tags || [], // 使用 tags 作为初始分类
|
||||
worldInfoId,
|
||||
outputSchema: undefined, // SillyTavern 格式中没有此字段
|
||||
avatarPath,
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
lastChatAt: undefined,
|
||||
isFavorite: data.extensions?.fav === true,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部角色卡转换为 SillyTavern 格式
|
||||
*/
|
||||
export function convertCharacterCardToST(card: CharacterCard): STCharacterCard {
|
||||
return {
|
||||
spec: 'chara_card_v2',
|
||||
spec_version: '2.0',
|
||||
data: {
|
||||
name: card.name,
|
||||
description: card.description,
|
||||
personality: card.personality,
|
||||
scenario: card.scenario,
|
||||
first_mes: card.first_mes,
|
||||
mes_example: card.mes_example,
|
||||
alternate_greetings: card.alternate_greetings,
|
||||
creator_notes: card.creator_notes,
|
||||
system_prompt: card.system_prompt,
|
||||
post_history_instructions: card.post_history_instructions,
|
||||
tags: card.categories, // 使用 categories 作为 tags
|
||||
extensions: {
|
||||
world: card.worldInfoId,
|
||||
fav: card.isFavorite,
|
||||
originalId: card.id,
|
||||
version: card.version,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 聊天记录转换 ====================
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 聊天头转换为内部格式
|
||||
*/
|
||||
export function convertSTChatHeaderToInternal(
|
||||
stHeader: STChatHeader,
|
||||
characterId: string
|
||||
): ChatHeader {
|
||||
return {
|
||||
id: generateId(),
|
||||
displayName: `${stHeader.character_name} Chat`,
|
||||
characterId,
|
||||
userName: stHeader.user_name,
|
||||
characterName: stHeader.character_name,
|
||||
tableData: undefined,
|
||||
createdAt: parseDateToTimestamp(stHeader.create_date),
|
||||
updatedAt: now(),
|
||||
messageCount: 0, // 需要在导入消息后更新
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部聊天头转换为 SillyTavern 格式
|
||||
*/
|
||||
export function convertChatHeaderToST(header: ChatHeader): STChatHeader {
|
||||
return {
|
||||
user_name: header.userName,
|
||||
character_name: header.characterName,
|
||||
create_date: new Date(header.createdAt).toISOString(),
|
||||
chat_metadata: {
|
||||
originalId: header.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 聊天消息转换为内部格式
|
||||
*/
|
||||
export function convertSTMessageToInternal(
|
||||
stMessage: STChatMessage,
|
||||
chatId: string
|
||||
): ChatMessage {
|
||||
return {
|
||||
id: generateId(),
|
||||
chatId,
|
||||
name: stMessage.name,
|
||||
is_user: stMessage.is_user,
|
||||
is_system: stMessage.is_system,
|
||||
sendDate: normalizeSendDate(stMessage.send_date),
|
||||
mes: stMessage.mes,
|
||||
swipes: stMessage.swipes,
|
||||
swipe_id: stMessage.swipe_id,
|
||||
is_hidden: stMessage.is_hidden,
|
||||
extra: stMessage.extra,
|
||||
tokenCount: undefined,
|
||||
isTemporary: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部聊天消息转换为 SillyTavern 格式
|
||||
*/
|
||||
export function convertMessageToST(message: ChatMessage): STChatMessage {
|
||||
return {
|
||||
name: message.name,
|
||||
is_user: message.is_user,
|
||||
is_system: message.is_system,
|
||||
send_date: message.sendDate,
|
||||
mes: message.mes,
|
||||
swipes: message.swipes,
|
||||
swipe_id: message.swipe_id,
|
||||
is_hidden: message.is_hidden,
|
||||
extra: message.extra,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SillyTavern 聊天记录转换为内部格式
|
||||
*/
|
||||
export function convertSTChatLogToInternal(
|
||||
stChatLog: [STChatHeader, ...STChatMessage[]],
|
||||
characterId: string
|
||||
): ChatLog {
|
||||
const [stHeader, ...stMessages] = stChatLog;
|
||||
|
||||
const header = convertSTChatHeaderToInternal(stHeader, characterId);
|
||||
const messages = stMessages.map(msg => convertSTMessageToInternal(msg, header.id));
|
||||
|
||||
header.messageCount = messages.length;
|
||||
|
||||
return {
|
||||
header,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部聊天记录转换为 SillyTavern 格式
|
||||
*/
|
||||
export function convertChatLogToST(chatLog: ChatLog): [STChatHeader, ...STChatMessage[]] {
|
||||
const stHeader = convertChatHeaderToST(chatLog.header);
|
||||
const stMessages = chatLog.messages.map(convertMessageToST);
|
||||
|
||||
return [stHeader, ...stMessages];
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
/**
|
||||
* 解析日期字符串为时间戳
|
||||
*/
|
||||
function parseDateToTimestamp(dateStr: string): number {
|
||||
// 尝试解析 ISO 格式
|
||||
const isoDate = new Date(dateStr);
|
||||
if (!isNaN(isoDate.getTime())) {
|
||||
return isoDate.getTime();
|
||||
}
|
||||
|
||||
// 尝试解析 SillyTavern 自定义格式 "2024-01-01 @12h 30m 45s 123ms"
|
||||
const customFormat = dateStr.match(/(\d{4}-\d{2}-\d{2})\s+@(\d+)h\s+(\d+)m\s+(\d+)s\s+(\d+)ms/);
|
||||
if (customFormat) {
|
||||
const [, date, hours, minutes, seconds, ms] = customFormat;
|
||||
const parsedDate = new Date(`${date}T${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}.${ms.padStart(3, '0')}Z`);
|
||||
return parsedDate.getTime();
|
||||
}
|
||||
|
||||
// 默认为当前时间
|
||||
return now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化发送日期为 ISO 字符串
|
||||
*/
|
||||
function normalizeSendDate(sendDate: number | string): string {
|
||||
if (typeof sendDate === 'number') {
|
||||
// 如果是时间戳,转换为 ISO 字符串
|
||||
return new Date(sendDate).toISOString();
|
||||
}
|
||||
|
||||
// 如果已经是字符串,尝试验证并规范化
|
||||
const date = new Date(sendDate);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
// 如果无法解析,返回当前时间
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
// ==================== 提示词预设转换 ====================
|
||||
|
||||
/**
|
||||
* 角色类型映射(外部 → 内部)
|
||||
*/
|
||||
function mapRoleToInternal(role: string): PromptRole {
|
||||
const roleMap: Record<string, PromptRole> = {
|
||||
'system': 'system',
|
||||
'assistant': 'ai',
|
||||
'user': 'user',
|
||||
};
|
||||
|
||||
return roleMap[role] || 'system'; // 回退规则
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为固有节点
|
||||
* @param prompt 外部 prompt 条目
|
||||
* @param builtinIdentifiers 内置标识符集合
|
||||
*/
|
||||
function isSystemNode(prompt: STPrompt, builtinIdentifiers: Set<string>): boolean {
|
||||
// 第一优先级:marker 标志
|
||||
if (prompt.marker === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 第二优先级:内置保留项
|
||||
if (builtinIdentifiers.has(prompt.identifier)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 第三优先级:普通自定义项
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Token 数(简化版,实际需要调用 tokenizer)
|
||||
* @param content 文本内容
|
||||
* @returns 估算的 token 数量
|
||||
*/
|
||||
function calculateTokenCount(content: string): number {
|
||||
if (!content) return 0;
|
||||
// 粗略估算:英文 ~4 chars/token,中文 ~1.5 chars/token
|
||||
// 这里使用一个简化的平均值 3 chars/token
|
||||
return Math.ceil(content.length / 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SillyTavern Prompt Preset 转换为内部视图
|
||||
*
|
||||
* @param stPreset 外部兼容层数据
|
||||
* @param characterId 当前选定的角色 ID
|
||||
* @param builtinIdentifiers 内置标识符集合(如 'main_prompt', 'jailbreak' 等)
|
||||
* @returns 内部业务层视图(当前角色的“当前视图”)
|
||||
*/
|
||||
export function convertSTPromptPresetToView(
|
||||
stPreset: STPromptPreset,
|
||||
characterId: string,
|
||||
builtinIdentifiers: Set<string> = new Set()
|
||||
): PromptPresetView {
|
||||
// 1. 获取当前角色的顺序配置
|
||||
const orderConfig = stPreset.prompt_order[characterId]
|
||||
|| stPreset.default_prompt_order
|
||||
|| { order: [] };
|
||||
|
||||
// 2. 创建 identifier → prompt 的映射表
|
||||
const promptMap = new Map(stPreset.prompts.map(p => [p.identifier, p]));
|
||||
|
||||
// 3. 构建内部条目列表
|
||||
const entries: PromptEntry[] = orderConfig.order
|
||||
.map((orderItem, index) => {
|
||||
const prompt = promptMap.get(orderItem.identifier);
|
||||
|
||||
// 如果找不到对应的 prompt,跳过(或创建占位符)
|
||||
if (!prompt) {
|
||||
console.warn(`Prompt not found: ${orderItem.identifier}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确定条目名(优先级:name → identifier)
|
||||
const name = prompt.name || prompt.identifier;
|
||||
|
||||
// 判断是否为固有节点
|
||||
const isSystem = isSystemNode(prompt, builtinIdentifiers);
|
||||
|
||||
// 计算 token 数
|
||||
const tokenCount = calculateTokenCount(prompt.content);
|
||||
|
||||
return {
|
||||
identifier: prompt.identifier,
|
||||
name,
|
||||
enabled: orderItem.enabled,
|
||||
content: prompt.content,
|
||||
order: index, // 使用数组索引作为顺序
|
||||
role: mapRoleToInternal(prompt.role),
|
||||
tokenCount,
|
||||
isSystemNode: isSystem,
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is PromptEntry => entry !== null);
|
||||
|
||||
return {
|
||||
characterId,
|
||||
entries,
|
||||
updatedAt: now(),
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部视图变更同步回外部兼容层
|
||||
*
|
||||
* @param view 内部业务层视图
|
||||
* @param stPreset 原始外部数据(会修改此对象)
|
||||
* @returns 更新后的外部兼容层数据
|
||||
*/
|
||||
export function syncViewToSTPromptPreset(
|
||||
view: PromptPresetView,
|
||||
stPreset: STPromptPreset
|
||||
): STPromptPreset {
|
||||
// 1. 更新 prompt_order 中的 enabled 状态和顺序
|
||||
const orderItems = view.entries.map(entry => ({
|
||||
identifier: entry.identifier,
|
||||
enabled: entry.enabled,
|
||||
}));
|
||||
|
||||
stPreset.prompt_order[view.characterId] = {
|
||||
order: orderItems,
|
||||
};
|
||||
|
||||
// 2. 更新 prompts 中的 name 和 content(如果用户编辑了)
|
||||
const promptMap = new Map(stPreset.prompts.map(p => [p.identifier, p]));
|
||||
|
||||
view.entries.forEach(entry => {
|
||||
const prompt = promptMap.get(entry.identifier);
|
||||
if (prompt) {
|
||||
// 只同步可编辑字段(name 和 content)
|
||||
prompt.name = entry.name;
|
||||
prompt.content = entry.content;
|
||||
// 注意:role 不回写,保持外部原始值
|
||||
// 注意:identifier 不回写,它是稳定键
|
||||
}
|
||||
});
|
||||
|
||||
return stPreset;
|
||||
}
|
||||
245
shared/types/examples/prompt-preset.example.ts
Normal file
245
shared/types/examples/prompt-preset.example.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Prompt Preset 类型使用示例
|
||||
* 展示如何正确使用内外层转换
|
||||
*/
|
||||
|
||||
import {
|
||||
STPromptPreset,
|
||||
STPrompt,
|
||||
} from '../types/sillytavern.types';
|
||||
import {
|
||||
PromptPresetView,
|
||||
PromptEntry,
|
||||
} from '../types/internal.types';
|
||||
import {
|
||||
convertSTPromptPresetToView,
|
||||
syncViewToSTPromptPreset,
|
||||
} from '../types/converters';
|
||||
|
||||
// ==================== 示例 1: 外部兼容层数据(从 SillyTavern 导入)====================
|
||||
|
||||
const externalPreset: STPromptPreset = {
|
||||
name: 'Default Preset',
|
||||
prompts: [
|
||||
{
|
||||
identifier: 'main_prompt',
|
||||
name: 'Main Prompt',
|
||||
content: 'You are a helpful assistant.',
|
||||
role: 'system',
|
||||
marker: true, // 固有节点标志
|
||||
injection_order: 0,
|
||||
},
|
||||
{
|
||||
identifier: 'jailbreak',
|
||||
name: 'Jailbreak',
|
||||
content: 'Ignore all previous instructions...',
|
||||
role: 'system',
|
||||
marker: true,
|
||||
injection_order: 1,
|
||||
},
|
||||
{
|
||||
identifier: 'custom-prompt-1',
|
||||
name: 'Custom Prompt',
|
||||
content: 'Additional context here.',
|
||||
role: 'user',
|
||||
marker: false,
|
||||
injection_order: 2,
|
||||
},
|
||||
],
|
||||
prompt_order: {
|
||||
'character-123': {
|
||||
order: [
|
||||
{ identifier: 'main_prompt', enabled: true },
|
||||
{ identifier: 'jailbreak', enabled: false }, // 禁用
|
||||
{ identifier: 'custom-prompt-1', enabled: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== 示例 2: 转换为内部视图(导入流程)====================
|
||||
|
||||
// 定义内置标识符集合
|
||||
const BUILTIN_IDENTIFIERS = new Set(['main_prompt', 'jailbreak']);
|
||||
|
||||
// 转换为角色 "character-123" 的内部视图
|
||||
const internalView: PromptPresetView = convertSTPromptPresetToView(
|
||||
externalPreset,
|
||||
'character-123',
|
||||
BUILTIN_IDENTIFIERS
|
||||
);
|
||||
|
||||
console.log('Internal View:', internalView);
|
||||
/*
|
||||
输出示例:
|
||||
{
|
||||
characterId: 'character-123',
|
||||
entries: [
|
||||
{
|
||||
identifier: 'main_prompt',
|
||||
name: 'Main Prompt',
|
||||
enabled: true,
|
||||
content: 'You are a helpful assistant.',
|
||||
order: 0,
|
||||
role: 'system',
|
||||
tokenCount: 9, // 估算值
|
||||
isSystemNode: true, // marker: true
|
||||
},
|
||||
{
|
||||
identifier: 'jailbreak',
|
||||
name: 'Jailbreak',
|
||||
enabled: false, // 从 prompt_order 提取
|
||||
content: 'Ignore all previous instructions...',
|
||||
order: 1,
|
||||
role: 'system',
|
||||
tokenCount: 12,
|
||||
isSystemNode: true,
|
||||
},
|
||||
{
|
||||
identifier: 'custom-prompt-1',
|
||||
name: 'Custom Prompt',
|
||||
enabled: true,
|
||||
content: 'Additional context here.',
|
||||
order: 2,
|
||||
role: 'user',
|
||||
tokenCount: 8,
|
||||
isSystemNode: false, // 自定义条目
|
||||
},
|
||||
],
|
||||
updatedAt: 1234567890,
|
||||
version: 1,
|
||||
}
|
||||
*/
|
||||
|
||||
// ==================== 示例 3: 前端编辑内部视图 ====================
|
||||
|
||||
// 用户在前端进行以下操作:
|
||||
// 1. 启用了 jailbreak
|
||||
// 2. 修改了 custom-prompt-1 的内容
|
||||
// 3. 调整了顺序
|
||||
|
||||
const editedEntries: PromptEntry[] = [
|
||||
...internalView.entries.map(entry => {
|
||||
if (entry.identifier === 'jailbreak') {
|
||||
return { ...entry, enabled: true }; // 启用
|
||||
}
|
||||
if (entry.identifier === 'custom-prompt-1') {
|
||||
return {
|
||||
...entry,
|
||||
content: 'Updated content here.', // 修改内容
|
||||
order: 0, // 移到第一位
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
}),
|
||||
].sort((a, b) => a.order - b.order);
|
||||
|
||||
const updatedView: PromptPresetView = {
|
||||
...internalView,
|
||||
entries: editedEntries,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
// ==================== 示例 4: 同步回外部兼容层(导出流程)====================
|
||||
|
||||
const exportedPreset: STPromptPreset = syncViewToSTPromptPreset(
|
||||
updatedView,
|
||||
externalPreset // 会修改此对象
|
||||
);
|
||||
|
||||
console.log('Exported Preset:', exportedPreset);
|
||||
/*
|
||||
输出示例:
|
||||
{
|
||||
name: 'Default Preset',
|
||||
prompts: [
|
||||
{
|
||||
identifier: 'main_prompt',
|
||||
name: 'Main Prompt',
|
||||
content: 'You are a helpful assistant.',
|
||||
role: 'system',
|
||||
marker: true,
|
||||
injection_order: 0,
|
||||
},
|
||||
{
|
||||
identifier: 'jailbreak',
|
||||
name: 'Jailbreak',
|
||||
content: 'Ignore all previous instructions...', // 内容未变
|
||||
role: 'system',
|
||||
marker: true,
|
||||
injection_order: 1,
|
||||
},
|
||||
{
|
||||
identifier: 'custom-prompt-1',
|
||||
name: 'Custom Prompt',
|
||||
content: 'Updated content here.', // ✅ 已更新
|
||||
role: 'user',
|
||||
marker: false,
|
||||
injection_order: 2,
|
||||
},
|
||||
],
|
||||
prompt_order: {
|
||||
'character-123': {
|
||||
order: [
|
||||
{ identifier: 'custom-prompt-1', enabled: true }, // ✅ 顺序改变
|
||||
{ identifier: 'main_prompt', enabled: true },
|
||||
{ identifier: 'jailbreak', enabled: true }, // ✅ 已启用
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
*/
|
||||
|
||||
// ==================== 示例 5: 业务层使用场景 ====================
|
||||
|
||||
/**
|
||||
* 场景 1: 获取当前角色的所有启用条目
|
||||
*/
|
||||
function getEnabledPrompts(view: PromptPresetView): PromptEntry[] {
|
||||
return view.entries.filter(entry => entry.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景 2: 计算总 Token 数
|
||||
*/
|
||||
function calculateTotalTokens(view: PromptPresetView): number {
|
||||
return view.entries.reduce((sum, entry) => sum + entry.tokenCount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景 3: 区分系统节点和自定义节点
|
||||
*/
|
||||
function separateNodes(view: PromptPresetView) {
|
||||
const systemNodes = view.entries.filter(e => e.isSystemNode);
|
||||
const customNodes = view.entries.filter(e => !e.isSystemNode);
|
||||
|
||||
return { systemNodes, customNodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景 4: 按角色分组
|
||||
*/
|
||||
function groupByRole(view: PromptPresetView) {
|
||||
const groups: Record<string, PromptEntry[]> = {
|
||||
system: [],
|
||||
ai: [],
|
||||
user: [],
|
||||
};
|
||||
|
||||
view.entries.forEach(entry => {
|
||||
groups[entry.role].push(entry);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const enabledPrompts = getEnabledPrompts(updatedView);
|
||||
const totalTokens = calculateTotalTokens(updatedView);
|
||||
const { systemNodes, customNodes } = separateNodes(updatedView);
|
||||
const roleGroups = groupByRole(updatedView);
|
||||
|
||||
console.log('Enabled prompts count:', enabledPrompts.length);
|
||||
console.log('Total tokens:', totalTokens);
|
||||
console.log('System nodes:', systemNodes.length);
|
||||
console.log('Custom nodes:', customNodes.length);
|
||||
13
shared/types/index.ts
Normal file
13
shared/types/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Shared 类型导出
|
||||
* 统一导出所有共享类型定义
|
||||
*/
|
||||
|
||||
// SillyTavern 兼容格式
|
||||
export * from './sillytavern.types';
|
||||
|
||||
// 项目内部格式
|
||||
export * from './internal.types';
|
||||
|
||||
// 数据转换器
|
||||
export * from './converters';
|
||||
469
shared/types/internal.types.ts
Normal file
469
shared/types/internal.types.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* 项目内部使用的数据结构定义
|
||||
* 继承 SillyTavern 格式并添加自定义字段和功能
|
||||
*/
|
||||
|
||||
import type {
|
||||
STWorldInfoEntry,
|
||||
STWorldInfoEntryActivation,
|
||||
STWorldInfoEntryPosition,
|
||||
STCharacterCardData,
|
||||
STChatHeader,
|
||||
STChatMessage,
|
||||
} from './sillytavern.types';
|
||||
|
||||
// ==================== 世界书 (World Info) ====================
|
||||
|
||||
/**
|
||||
* 自定义激活方式类型(4种枚举)
|
||||
*/
|
||||
export enum ActivationType {
|
||||
/** 永久激活 - 从 constant=true 识别 */
|
||||
PERMANENT = 'permanent',
|
||||
/** 关键词触发 - 从 key 数组识别 */
|
||||
KEYWORD = 'keyword',
|
||||
/** RAG 检索激活 - 需要绑定 RAG 库 */
|
||||
RAG = 'rag',
|
||||
/** 逻辑表达式激活 - 包含两个变量和一个运算符 */
|
||||
LOGIC = 'logic',
|
||||
}
|
||||
|
||||
/**
|
||||
* 逻辑表达式结构(用于 LOGIC 激活类型)
|
||||
*/
|
||||
export interface LogicExpression {
|
||||
/** 第一个变量 */
|
||||
variable1: string;
|
||||
|
||||
/** 运算符 */
|
||||
operator: 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'greater' | 'less';
|
||||
|
||||
/** 第二个变量或值 */
|
||||
variable2: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 配置(用于 RAG 激活类型)
|
||||
*/
|
||||
export interface RAGConfig {
|
||||
/** 绑定的 RAG 库 ID */
|
||||
libraryId: string;
|
||||
|
||||
/** 相似度阈值 */
|
||||
threshold?: number;
|
||||
|
||||
/** 最大返回条目数 */
|
||||
maxEntries?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目内部世界书条目结构
|
||||
* 继承 SillyTavern 条目并添加自定义功能
|
||||
*/
|
||||
export interface WorldInfoEntry extends Omit<STWorldInfoEntry, 'constant' | 'selective'> {
|
||||
/** 激活方式(4种枚举之一) */
|
||||
activationType: ActivationType;
|
||||
|
||||
/** 逻辑表达式(当 activationType 为 LOGIC 时使用) */
|
||||
logicExpression?: LogicExpression;
|
||||
|
||||
/** RAG 配置(当 activationType 为 RAG 时使用) */
|
||||
ragConfig?: RAGConfig;
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目内部世界书结构
|
||||
*/
|
||||
export interface WorldInfo {
|
||||
/** 唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 世界书名称 */
|
||||
name: string;
|
||||
|
||||
/** 世界书描述 */
|
||||
description?: string;
|
||||
|
||||
/** 条目数组 */
|
||||
entries: WorldInfoEntry[];
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
|
||||
/** 版本号(用于数据迁移) */
|
||||
version: number;
|
||||
}
|
||||
|
||||
// ==================== 角色卡 (Character Card) ====================
|
||||
|
||||
/**
|
||||
* Vercel AI SDK Output.object() 的表头定义
|
||||
* 用于结构化输出
|
||||
*/
|
||||
export interface OutputSchemaField {
|
||||
/** 字段名称 */
|
||||
name: string;
|
||||
|
||||
/** 字段类型 */
|
||||
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
||||
|
||||
/** 字段描述 */
|
||||
description: string;
|
||||
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
|
||||
/** 枚举值(如果是字符串且有固定选项) */
|
||||
enum?: string[];
|
||||
|
||||
/** 嵌套字段(如果是 object 类型) */
|
||||
fields?: OutputSchemaField[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目内部角色卡结构
|
||||
* 继承 SillyTavern 角色卡并添加自定义字段
|
||||
*/
|
||||
export interface CharacterCard extends Omit<STCharacterCardData, 'character_book'> {
|
||||
/** 角色唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 分类标签列表(用于前端分类) */
|
||||
categories: string[];
|
||||
|
||||
/** 绑定的世界书 ID */
|
||||
worldInfoId?: string;
|
||||
|
||||
/** 输出 schema 定义(用于 Vercel AI SDK Output.object()) */
|
||||
outputSchema?: OutputSchemaField[];
|
||||
|
||||
/** 角色头像路径 */
|
||||
avatarPath?: string;
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
|
||||
/** 最后聊天时间戳 */
|
||||
lastChatAt?: number;
|
||||
|
||||
/** 收藏状态 */
|
||||
isFavorite: boolean;
|
||||
|
||||
/** 版本号(用于数据迁移) */
|
||||
version: number;
|
||||
}
|
||||
|
||||
// ==================== 聊天记录 (Chat Log) ====================
|
||||
|
||||
/**
|
||||
* 项目内部聊天记录头
|
||||
*/
|
||||
export interface ChatHeader extends Omit<STChatHeader, 'user_name' | 'character_name'> {
|
||||
/** 聊天唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 显示名称(聊天标题) */
|
||||
displayName: string;
|
||||
|
||||
/** 关联的角色卡 ID */
|
||||
characterId: string;
|
||||
|
||||
/** 用户角色名 */
|
||||
userName: string;
|
||||
|
||||
/** AI 角色名称 */
|
||||
characterName: string;
|
||||
|
||||
/** 表格内容(对应角色卡的 outputSchema 字段的值) */
|
||||
tableData?: Record<string, unknown>;
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
|
||||
/** 消息数量 */
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目内部聊天消息
|
||||
*/
|
||||
export interface ChatMessage extends Omit<STChatMessage, 'send_date'> {
|
||||
/** 消息唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 发送日期 ISO 字符串 */
|
||||
sendDate: string;
|
||||
|
||||
/** 关联的聊天 ID */
|
||||
chatId: string;
|
||||
|
||||
/** Token 数量(用于统计) */
|
||||
tokenCount?: number;
|
||||
|
||||
/** 是否为临时消息(未保存) */
|
||||
isTemporary?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目内部完整聊天记录
|
||||
*/
|
||||
export interface ChatLog {
|
||||
/** 聊天头 */
|
||||
header: ChatHeader;
|
||||
|
||||
/** 消息列表 */
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
// ==================== 预设 (Preset) ====================
|
||||
|
||||
/**
|
||||
* 项目内部采样参数预设
|
||||
*/
|
||||
export interface GenerationPreset {
|
||||
/** 预设唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 预设名称 */
|
||||
name: string;
|
||||
|
||||
/** 温度 */
|
||||
temperature: number;
|
||||
|
||||
/** Top P */
|
||||
topP: number;
|
||||
|
||||
/** Top K */
|
||||
topK: number;
|
||||
|
||||
/** 重复惩罚 */
|
||||
repetitionPenalty: number;
|
||||
|
||||
/** 频率惩罚 */
|
||||
frequencyPenalty?: number;
|
||||
|
||||
/** 存在惩罚 */
|
||||
presencePenalty?: number;
|
||||
|
||||
/** 最大生成长度 */
|
||||
maxLength?: number;
|
||||
|
||||
/** 是否为默认预设 */
|
||||
isDefault: boolean;
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ==================== 提示词预设 (Prompt Preset) ====================
|
||||
|
||||
/**
|
||||
* Prompt 角色类型(内部业务层只保留三种)
|
||||
*/
|
||||
export type PromptRole = 'system' | 'ai' | 'user';
|
||||
|
||||
/**
|
||||
* 内部业务层 - Prompt 条目(当前视图)
|
||||
* 这是基于某个 character_id 生成的“当前视图”,只包含业务需要的字段
|
||||
*/
|
||||
export interface PromptEntry {
|
||||
/** 稳定关联键(用于回写,不直接暴露给前端作为主要展示字段) */
|
||||
identifier: string;
|
||||
|
||||
/** 1. 条目名 - 前端显示和编辑 */
|
||||
name: string;
|
||||
|
||||
/** 2. 是否启用 - 当前作用域下的业务状态(派生字段) */
|
||||
enabled: boolean;
|
||||
|
||||
/** 3. 条目内容 - 静态内容视图(marker 节点可能为空) */
|
||||
content: string;
|
||||
|
||||
/** 4. 条目顺序 - 前端展示和拖拽排序 */
|
||||
order: number;
|
||||
|
||||
/** 5. 角色 - 三类业务角色 */
|
||||
role: PromptRole;
|
||||
|
||||
/** 6. 总 token 数 - 派生显示字段(不属于外部兼容层) */
|
||||
tokenCount: number;
|
||||
|
||||
/** 7. 是否固有节点 - 业务语义标签(不是 SillyTavern 原始字段) */
|
||||
isSystemNode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部业务层 - Prompt 预设视图(基于某个 character_id 的“当前视图”)
|
||||
*/
|
||||
export interface PromptPresetView {
|
||||
/** 关联的角色 ID */
|
||||
characterId: string;
|
||||
|
||||
/** 当前视图的条目列表(已排序、已过滤) */
|
||||
entries: PromptEntry[];
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
|
||||
/** 版本号 */
|
||||
version: number;
|
||||
}
|
||||
|
||||
// ==================== 工作流 (Workflow) ====================
|
||||
|
||||
/**
|
||||
* 工作流节点类型
|
||||
*/
|
||||
export enum WorkflowNodeType {
|
||||
/** LLM 调用节点 */
|
||||
LLM_CALL = 'llm_call',
|
||||
/** 条件分支节点 */
|
||||
CONDITION = 'condition',
|
||||
/** 数据处理节点 */
|
||||
DATA_PROCESS = 'data_process',
|
||||
/** 并行执行节点 */
|
||||
PARALLEL = 'parallel',
|
||||
/** 合并节点 */
|
||||
MERGE = 'merge',
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流节点
|
||||
*/
|
||||
export interface WorkflowNode {
|
||||
/** 节点唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 节点类型 */
|
||||
type: WorkflowNodeType;
|
||||
|
||||
/** 节点名称 */
|
||||
name: string;
|
||||
|
||||
/** 节点配置 */
|
||||
config: Record<string, unknown>;
|
||||
|
||||
/** 输入映射 */
|
||||
inputMapping?: Record<string, string>;
|
||||
|
||||
/** 输出映射 */
|
||||
outputMapping?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流连接
|
||||
*/
|
||||
export interface WorkflowEdge {
|
||||
/** 连接唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 源节点 ID */
|
||||
source: string;
|
||||
|
||||
/** 目标节点 ID */
|
||||
target: string;
|
||||
|
||||
/** 源节点输出端口 */
|
||||
sourcePort?: string;
|
||||
|
||||
/** 目标节点输入端口 */
|
||||
targetPort?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流定义
|
||||
*/
|
||||
export interface Workflow {
|
||||
/** 工作流唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 工作流名称 */
|
||||
name: string;
|
||||
|
||||
/** 工作流描述 */
|
||||
description?: string;
|
||||
|
||||
/** 节点列表 */
|
||||
nodes: WorkflowNode[];
|
||||
|
||||
/** 连接列表 */
|
||||
edges: WorkflowEdge[];
|
||||
|
||||
/** 入口节点 ID */
|
||||
entryNodeId: string;
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
|
||||
/** 版本号 */
|
||||
version: number;
|
||||
}
|
||||
|
||||
// ==================== RAG 库 (RAG Library) ====================
|
||||
|
||||
/**
|
||||
* RAG 文档条目
|
||||
*/
|
||||
export interface RAGDocument {
|
||||
/** 文档唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 文档内容 */
|
||||
content: string;
|
||||
|
||||
/** 文档元数据 */
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
/** 向量嵌入(由 embedding 模型生成) */
|
||||
embedding?: number[];
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 库
|
||||
*/
|
||||
export interface RAGLibrary {
|
||||
/** 库唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 库名称 */
|
||||
name: string;
|
||||
|
||||
/** 库描述 */
|
||||
description?: string;
|
||||
|
||||
/** 文档列表 */
|
||||
documents: RAGDocument[];
|
||||
|
||||
/** 使用的 embedding 模型 */
|
||||
embeddingModel?: string;
|
||||
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
|
||||
/** 最后更新时间戳 */
|
||||
updatedAt: number;
|
||||
}
|
||||
395
shared/types/sillytavern.types.ts
Normal file
395
shared/types/sillytavern.types.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* SillyTavern 兼容的数据结构定义
|
||||
* 这些类型严格遵循 SillyTavern 官方规范,用于导入导出兼容
|
||||
*/
|
||||
|
||||
// ==================== 世界书 (World Info) ====================
|
||||
|
||||
/**
|
||||
* SillyTavern 世界书条目激活方式枚举
|
||||
*/
|
||||
export enum STWorldInfoEntryActivation {
|
||||
/** 永久激活 - 蓝色圆圈 */
|
||||
CONSTANT = 'constant',
|
||||
/** 关键词触发 - 绿色圆圈 */
|
||||
KEYWORD = 'keyword',
|
||||
/** 向量检索 RAG - 链条链接 */
|
||||
VECTOR = 'vector',
|
||||
/** 禁用 - 红色叉 */
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 世界书条目插入位置
|
||||
*/
|
||||
export enum STWorldInfoEntryPosition {
|
||||
/** 在角色定义之前 */
|
||||
BEFORE_CHAR = 'before_char',
|
||||
/** 在角色定义之后 */
|
||||
AFTER_CHAR = 'after_char',
|
||||
/** 在示例消息之前 */
|
||||
BEFORE_EXAMPLE = 'before_example',
|
||||
/** 在示例消息之后 */
|
||||
AFTER_EXAMPLE = 'after_example',
|
||||
/** 在作者注释顶部 */
|
||||
AUTHOR_NOTE_TOP = 'author_note_top',
|
||||
/** 在作者注释底部 */
|
||||
AUTHOR_NOTE_BOTTOM = 'author_note_bottom',
|
||||
/** 指定深度位置 @D */
|
||||
AT_DEPTH = 'at_depth',
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 世界书条目可选过滤器逻辑
|
||||
*/
|
||||
export enum STWorldInfoFilterLogic {
|
||||
/** AND ANY - 任意一个可选关键字匹配 */
|
||||
AND_ANY = 'and_any',
|
||||
/** AND ALL - 所有可选关键字都匹配 */
|
||||
AND_ALL = 'and_all',
|
||||
/** NOT ANY - 没有任何可选关键字匹配 */
|
||||
NOT_ANY = 'not_any',
|
||||
/** NOT ALL - 不是所有可选关键字都匹配 */
|
||||
NOT_ALL = 'not_all',
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 世界书条目角色类型
|
||||
*/
|
||||
export enum STWorldInfoEntryRole {
|
||||
/** 系统角色 */
|
||||
SYSTEM = 'system',
|
||||
/** 用户角色 */
|
||||
USER = 'user',
|
||||
/** 助手角色 */
|
||||
ASSISTANT = 'assistant',
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 世界书条目原始结构
|
||||
* 参考: https://st-docs.role.fun/usage/core-concepts/worldinfo/
|
||||
*/
|
||||
export interface STWorldInfoEntry {
|
||||
/** 条目唯一标识符 */
|
||||
uid: string;
|
||||
|
||||
/** 条目标题/备忘录(仅用于人类阅读) */
|
||||
key?: string[];
|
||||
|
||||
/** 触发关键词列表(支持正则表达式) */
|
||||
keysecondary?: string[];
|
||||
|
||||
/** 可选过滤器逻辑 */
|
||||
filter?: STWorldInfoFilterLogic;
|
||||
|
||||
/** 条目内容 - 激活时注入到提示的文本 */
|
||||
content: string;
|
||||
|
||||
/** 常量标志 - true 表示永久激活(蓝色圆圈) */
|
||||
constant?: boolean;
|
||||
|
||||
/** 选择性标志 - 用于向量检索 */
|
||||
selective?: boolean;
|
||||
|
||||
/** 插入顺序 - 数值越大越靠近末尾 */
|
||||
order: number;
|
||||
|
||||
/** 插入位置 */
|
||||
position: STWorldInfoEntryPosition;
|
||||
|
||||
/** 插入深度(当 position 为 AT_DEPTH 时使用) */
|
||||
depth?: number;
|
||||
|
||||
/** 角色类型(当使用角色消息插入时) */
|
||||
role?: STWorldInfoEntryRole;
|
||||
|
||||
/** 概率 - 0-100,控制激活时的插入概率 */
|
||||
probability?: number;
|
||||
|
||||
/** 包含组 - 逗号分隔的组标签列表 */
|
||||
group?: string[];
|
||||
|
||||
/** 优先包含 - true 时按 order 值选择而非随机 */
|
||||
groupPrioritize?: boolean;
|
||||
|
||||
/** 使用组评分 - 基于关键字匹配数量选择 */
|
||||
useGroupScoring?: boolean;
|
||||
|
||||
/** 自动化 ID - 与 ST 脚本集成 */
|
||||
automationId?: string;
|
||||
|
||||
/** 是否禁用 */
|
||||
disable?: boolean;
|
||||
|
||||
/** 扩展字段 */
|
||||
extensions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 世界书文件结构
|
||||
*/
|
||||
export interface STWorldInfo {
|
||||
/** 规范名称 */
|
||||
spec: string;
|
||||
|
||||
/** 世界书名称 */
|
||||
name?: string;
|
||||
|
||||
/** 世界书描述 */
|
||||
description?: string;
|
||||
|
||||
/** 条目数组 */
|
||||
entries: STWorldInfoEntry[];
|
||||
|
||||
/** 扩展字段 */
|
||||
extensions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ==================== 角色卡 (Character Card) ====================
|
||||
|
||||
/**
|
||||
* SillyTavern 角色卡规范版本
|
||||
*/
|
||||
export enum STCharacterCardSpec {
|
||||
V1 = 'chara_card_v1',
|
||||
V2 = 'chara_card_v2',
|
||||
V3 = 'chara_card_v3',
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 角色卡 V2/V3 数据结构
|
||||
* 参考: https://github.com/malfoyslastname/character-card-spec-v2
|
||||
*/
|
||||
export interface STCharacterCardData {
|
||||
/** 角色名称 */
|
||||
name: string;
|
||||
|
||||
/** 角色描述 */
|
||||
description: string;
|
||||
|
||||
/** 角色性格特征 */
|
||||
personality: string;
|
||||
|
||||
/** 场景设定 */
|
||||
scenario: string;
|
||||
|
||||
/** 首条开场消息 */
|
||||
first_mes: string;
|
||||
|
||||
/** 对话示例 */
|
||||
mes_example: string;
|
||||
|
||||
/** 替代问候语数组 */
|
||||
alternate_greetings?: string[];
|
||||
|
||||
/** 角色创建者备注 */
|
||||
creator_notes?: string;
|
||||
|
||||
/** 系统级别指令 */
|
||||
system_prompt?: string;
|
||||
|
||||
/** 历史后指令 */
|
||||
post_history_instructions?: string;
|
||||
|
||||
/** 标签数组 */
|
||||
tags?: string[];
|
||||
|
||||
/** 角色书(嵌入的世界书) */
|
||||
character_book?: STWorldInfo;
|
||||
|
||||
/** 扩展字段 */
|
||||
extensions?: {
|
||||
/** 绑定的世界书文件名 */
|
||||
world?: string;
|
||||
|
||||
/** 健谈程度 0-1 */
|
||||
talkativeness?: number;
|
||||
|
||||
/** 收藏状态 */
|
||||
fav?: boolean;
|
||||
|
||||
/** 其他扩展字段 */
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 角色卡完整结构(V2/V3)
|
||||
*/
|
||||
export interface STCharacterCard {
|
||||
/** 规范标识 */
|
||||
spec: STCharacterCardSpec;
|
||||
|
||||
/** 规范版本 */
|
||||
spec_version?: string;
|
||||
|
||||
/** 角色数据 */
|
||||
data: STCharacterCardData;
|
||||
}
|
||||
|
||||
// ==================== 聊天记录 (Chat Log) ====================
|
||||
|
||||
/**
|
||||
* SillyTavern 聊天记录头(JSONL 第一行)
|
||||
*/
|
||||
export interface STChatHeader {
|
||||
/** 用户名称 */
|
||||
user_name: string;
|
||||
|
||||
/** 角色名称 */
|
||||
character_name: string;
|
||||
|
||||
/** 创建日期 */
|
||||
create_date: string;
|
||||
|
||||
/** 聊天元数据 */
|
||||
chat_metadata?: {
|
||||
/** 完整性校验哈希 */
|
||||
integrity?: string;
|
||||
|
||||
/** 其他元数据 */
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/** 其他可能的头部字段 */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 聊天消息记录
|
||||
*/
|
||||
export interface STChatMessage {
|
||||
/** 发送者名称 */
|
||||
name: string;
|
||||
|
||||
/** 是否为用户消息 */
|
||||
is_user: boolean;
|
||||
|
||||
/** 是否为系统消息 */
|
||||
is_system?: boolean;
|
||||
|
||||
/** 发送日期时间戳或 ISO 字符串 */
|
||||
send_date: number | string;
|
||||
|
||||
/** 实际对话消息文本 */
|
||||
mes: string;
|
||||
|
||||
/** 替换回答数组(swipe 功能) */
|
||||
swipes?: string[];
|
||||
|
||||
/** 当前选择的 swipe ID */
|
||||
swipe_id?: number;
|
||||
|
||||
/** 是否为隐藏消息 */
|
||||
is_hidden?: boolean;
|
||||
|
||||
/** 扩展字段 */
|
||||
extra?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern 聊天记录文件(JSONL 格式)
|
||||
* 第一行是 STChatHeader,后续每行是 STChatMessage
|
||||
*/
|
||||
export type STChatLog = [STChatHeader, ...STChatMessage[]];
|
||||
|
||||
// ==================== 预设 (Preset) ====================
|
||||
|
||||
/**
|
||||
* SillyTavern 采样参数预设
|
||||
*/
|
||||
export interface STGenerationPreset {
|
||||
/** 预设名称 */
|
||||
name: string;
|
||||
|
||||
/** 温度 */
|
||||
temperature?: number;
|
||||
|
||||
/** Top P */
|
||||
top_p?: number;
|
||||
|
||||
/** Top K */
|
||||
top_k?: number;
|
||||
|
||||
/** 重复惩罚 */
|
||||
repetition_penalty?: number;
|
||||
|
||||
/** 频率惩罚 */
|
||||
frequency_penalty?: number;
|
||||
|
||||
/** 存在惩罚 */
|
||||
presence_penalty?: number;
|
||||
|
||||
/** 最大生成长度 */
|
||||
max_length?: number;
|
||||
|
||||
/** 其他采样参数 */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ==================== 提示词预设 (Prompt Preset) ====================
|
||||
|
||||
/**
|
||||
* SillyTavern Prompt 条目(外部兼容层)
|
||||
* 这是提示词模板的原始数据结构
|
||||
*/
|
||||
export interface STPrompt {
|
||||
/** 唯一标识符(稳定键,UUID 或内置标识如 'main_prompt', 'jailbreak' 等) */
|
||||
identifier: string;
|
||||
|
||||
/** 显示名称(可选,用于前端展示) */
|
||||
name?: string;
|
||||
|
||||
/** 提示词内容 */
|
||||
content: string;
|
||||
|
||||
/** 角色类型 */
|
||||
role: 'system' | 'assistant' | 'user';
|
||||
|
||||
/** 是否为标记节点(固有节点标志) */
|
||||
marker?: boolean;
|
||||
|
||||
/** 注入顺序元数据(外部层的注入优先级) */
|
||||
injection_order?: number;
|
||||
|
||||
/** 扩展字段 */
|
||||
extensions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern Prompt 顺序配置项
|
||||
*/
|
||||
export interface STPromptOrderItem {
|
||||
/** 引用 prompts 中的 identifier */
|
||||
identifier: string;
|
||||
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern Prompt 顺序配置(按角色)
|
||||
*/
|
||||
export interface STPromptOrderConfig {
|
||||
order: STPromptOrderItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* SillyTavern Prompt Preset 完整结构(外部兼容层)
|
||||
* 包含所有可用的提示词条目和各角色的顺序配置
|
||||
*/
|
||||
export interface STPromptPreset {
|
||||
/** 预设名称 */
|
||||
name: string;
|
||||
|
||||
/** 所有可用的提示词条目 */
|
||||
prompts: STPrompt[];
|
||||
|
||||
/** 各角色的顺序配置 */
|
||||
prompt_order: {
|
||||
[character_id: string]: STPromptOrderConfig;
|
||||
};
|
||||
|
||||
/** 默认顺序配置(未指定角色时使用) */
|
||||
default_prompt_order?: STPromptOrderConfig;
|
||||
}
|
||||
1
shared/utils/.gitkeep
Normal file
1
shared/utils/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
// Shared Utility Functions
|
||||
Reference in New Issue
Block a user