规定数据类型

This commit is contained in:
2026-04-24 01:45:41 +08:00
parent d1943f564a
commit 35eff3faf6
86 changed files with 3809 additions and 0 deletions

15
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
// Vue3 Frontend Directory Structure

1
client/src/api/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// API Client and Services will be placed here

View File

@@ -0,0 +1 @@
// Static Assets (images, fonts, etc.) will be placed here

View File

@@ -0,0 +1 @@
// Center Panel Components will be placed here

View File

@@ -0,0 +1 @@
// Chat Input Feature will be placed here

View File

@@ -0,0 +1 @@
// Message List Feature will be placed here

View File

@@ -0,0 +1 @@
// Left Panel Components will be placed here

View File

@@ -0,0 +1 @@
// Character List Feature will be placed here

View File

@@ -0,0 +1 @@
// Chat History Feature will be placed here

View File

@@ -0,0 +1 @@
// Right Panel Components will be placed here

View File

@@ -0,0 +1 @@
// Character Detail Feature will be placed here

View File

@@ -0,0 +1 @@
// Workflow Editor Feature will be placed here

View File

@@ -0,0 +1 @@
// Top Bar Components will be placed here

View File

@@ -0,0 +1 @@
// Model Switcher Feature will be placed here

View File

@@ -0,0 +1 @@
// Quick Actions Feature will be placed here

View File

@@ -0,0 +1 @@
// Reusable Base Components will be placed here

View File

@@ -0,0 +1 @@
// Composables (Vue 3 Composition API) will be placed here

View File

@@ -0,0 +1 @@
// Constants and Configuration will be placed here

View File

@@ -0,0 +1 @@
// Layout Components will be placed here

View File

@@ -0,0 +1 @@
// Vue Router Configuration will be placed here

View File

@@ -0,0 +1 @@
// Pinia Stores will be placed here

View File

@@ -0,0 +1 @@
// Global Styles and CSS Variables will be placed here

View File

@@ -0,0 +1 @@
// Utility Functions will be placed here

27
client/tsconfig.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
# NestJS Backend Directory Structure

View File

@@ -0,0 +1 @@
// Controllers will be placed here

View File

@@ -0,0 +1 @@
// Configuration Management will be placed here

View File

@@ -0,0 +1 @@
// Dependency Injection Tokens will be placed here

View File

@@ -0,0 +1 @@
// Exception Filters will be placed here

View File

@@ -0,0 +1 @@
// Guards and Middleware will be placed here

View File

@@ -0,0 +1 @@
// Request/Response Interceptors will be placed here

View File

@@ -0,0 +1 @@
// Utility Functions and Helpers will be placed here

1
server/src/dto/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// DTOs and Validation will be placed here

View File

@@ -0,0 +1 @@
// Interfaces for Dependency Injection will be placed here

1
server/src/llm/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// Vercel AI SDK integration will be placed here

View File

@@ -0,0 +1 @@
// LLM Providers (OpenAI, Claude, Local, etc.) will be placed here

View File

@@ -0,0 +1 @@
// LLM Tools and Function Calling will be placed here

View File

@@ -0,0 +1 @@
// Character Module will be placed here

View 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 {}

View File

@@ -0,0 +1 @@
// Chat Module will be placed here

View 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 {}

View File

@@ -0,0 +1 @@
// Import/Export Module will be placed here

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { ImportExportController } from '../../controllers/import-export.controller';
@Module({
controllers: [ImportExportController],
})
export class ImportExportModule {}

View File

@@ -0,0 +1 @@
// LLM Module will be placed here

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { LLMController } from '../../controllers/llm.controller';
@Module({
controllers: [LLMController],
})
export class LLMModule {}

View File

@@ -0,0 +1 @@
// Workflow Module will be placed here

View File

@@ -0,0 +1,6 @@
import { Module } from '@nestjs/common';
@Module({
imports: [],
})
export class WorkflowModule {}

View File

@@ -0,0 +1 @@
// Persistence Module will be placed here

View File

@@ -0,0 +1 @@
// File System Implementations will be placed here

View File

@@ -0,0 +1 @@
// Repository Interfaces will be placed here

View File

@@ -0,0 +1 @@
// Data Migration Scripts will be placed here

View File

@@ -0,0 +1 @@
// Services (Business Logic) will be placed here

View File

@@ -0,0 +1 @@
// Context Management and Chunking will be placed here

View File

@@ -0,0 +1 @@
// Prompt Assembly and Templates will be placed here

View File

@@ -0,0 +1 @@
// Token Counting and Estimation will be placed here

View File

@@ -0,0 +1 @@
// Workflow Engine will be placed here

1
server/test/e2e/.gitkeep Normal file
View File

@@ -0,0 +1 @@
// E2E Tests will be placed here

View File

@@ -0,0 +1 @@
// Integration Tests will be placed here

View File

@@ -0,0 +1 @@
// Unit Tests will be placed here

28
server/tsconfig.json Normal file
View 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"]
}

View File

@@ -0,0 +1 @@
// Shared Constants and Enums

14
shared/index.ts Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
// Zod Schemas for Validation (v3)

6
shared/schemas/index.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* Shared Schemas 导出
*/
export * from './sillytavern.schemas';
export * from './internal.schemas';

View 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(),
});

View 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
View File

@@ -0,0 +1 @@
// Shared TypeScript Types (Frontend & Backend)

545
shared/types/converters.ts Normal file
View 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;
}

View 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
View File

@@ -0,0 +1,13 @@
/**
* Shared 类型导出
* 统一导出所有共享类型定义
*/
// SillyTavern 兼容格式
export * from './sillytavern.types';
// 项目内部格式
export * from './internal.types';
// 数据转换器
export * from './converters';

View 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;
}

View 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
View File

@@ -0,0 +1 @@
// Shared Utility Functions