""" 项目内部数据结构定义 这是本项目真正使用的核心数据模型,所有业务逻辑都基于这些类型。 与 sillytavern.py 不同,这里的模型不参与导入导出兼容,而是专注于: - 内部业务逻辑处理 - API 响应数据结构 - 数据存储格式 - 工作流引擎数据交换 所有从 SillyTavern 导入的数据都会转换为这些内部模型进行处理, 导出时再从内部模型转换回 SillyTavern 格式。 """ from enum import Enum from typing import List, Optional, Dict, Any from pydantic import BaseModel, Field from datetime import datetime # ==================== 世界书 (World Info) ==================== class ActivationType(str, Enum): """ 自定义激活方式类型(4种枚举) 这是项目的核心创新点之一,相比 SillyTavern 的简单 constant/selective 标志, 我们提供了更灵活的激活机制。 """ PERMANENT = 'permanent' # 永久激活 - 始终包含在上下文中 KEYWORD = 'keyword' # 关键词触发 - 匹配关键词时激活 RAG = 'rag' # RAG 检索激活 - 基于向量相似度检索 LOGIC = 'logic' # 逻辑表达式激活 - 基于变量条件判断 class LogicOperator(str, Enum): """逻辑运算符(用于 LOGIC 激活类型)""" EQUALS = 'equals' # 等于 NOT_EQUALS = 'not_equals' # 不等于 CONTAINS = 'contains' # 包含 NOT_CONTAINS = 'not_contains' # 不包含 GREATER = 'greater' # 大于 LESS = 'less' # 小于 class LogicExpression(BaseModel): """ 逻辑表达式结构(用于 LOGIC 激活类型) 示例: variable1="mood", operator="equals", variable2="happy" 表示当 mood 变量等于 happy 时激活该条目 """ variable1: str = Field(..., description="第一个变量名") operator: LogicOperator = Field(..., description="比较运算符") variable2: str = Field(..., description="第二个变量名或值") class RAGConfig(BaseModel): """ RAG 配置(用于 RAG 激活类型) 控制如何从向量数据库中检索相关内容 """ libraryId: str = Field(..., description="绑定的 RAG 库 ID") threshold: Optional[float] = Field(0.7, ge=0, le=1, description="相似度阈值 (0-1)") maxEntries: Optional[int] = Field(5, gt=0, description="最大返回条目数") class WorldInfoEntry(BaseModel): """ 项目内部世界书条目结构 这是世界书的核心单元,每个条目代表一段可以被动态注入到对话上下文中的知识。 相比 SillyTavern,我们添加了 activationType、logicExpression、ragConfig 等高级功能。 """ uid: str = Field(..., description="条目唯一标识符 (UUID)") key: Optional[List[str]] = Field(None, description="主关键词列表 (用于 KEYWORD 激活)") keysecondary: Optional[List[str]] = Field(None, description="次要关键词列表 (可选过滤)") content: str = Field(..., description="条目内容 - 激活时注入的文本") activationType: ActivationType = Field(..., description="激活方式") logicExpression: Optional[LogicExpression] = Field(None, description="逻辑表达式 (LOGIC 类型使用)") ragConfig: Optional[RAGConfig] = Field(None, description="RAG 配置 (RAG 类型使用)") order: int = Field(0, description="插入顺序 - 数值越大越靠近末尾") position: Optional[str] = Field('after_char', description="插入位置") depth: Optional[int] = Field(None, description="插入深度 (当 position='at_depth' 时使用)") probability: Optional[float] = Field(100, ge=0, le=100, description="激活概率 (0-100)") group: Optional[List[str]] = Field(None, description="所属组标签") disable: bool = Field(False, description="是否禁用") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") class WorldInfo(BaseModel): """ 项目内部世界书结构 世界书是角色知识的集合,可以绑定到角色卡上,在对话中动态提供背景信息。 """ id: str = Field(..., description="世界书唯一标识符 (UUID)") name: str = Field(..., description="世界书名称") description: Optional[str] = Field(None, description="世界书描述") entries: List[WorldInfoEntry] = Field(default_factory=list, description="条目数组") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") version: int = Field(1, description="版本号 (用于数据迁移)") # ==================== 角色卡 (Character Card) ==================== class OutputSchemaField(BaseModel): """ Vercel AI SDK Output.object() 的表头定义 用于结构化输出,让 LLM 按照指定格式返回数据。 这是项目的特色功能,支持动态表格生成。 """ name: str = Field(..., description="字段名称") type: str = Field(..., description="字段类型 (string/number/boolean/array/object)") description: str = Field(..., description="字段描述") required: Optional[bool] = Field(None, description="是否必需") enum: Optional[List[str]] = Field(None, description="枚举值 (字符串固定选项)") fields: Optional[List['OutputSchemaField']] = Field(None, description="嵌套字段 (object 类型)") class CharacterCard(BaseModel): """ 项目内部角色卡结构 角色卡是对话 AI 的核心定义,包含人设、场景、开场白等。 相比 SillyTavern,我们添加了 categories、outputSchema、worldInfoId 等功能。 """ id: str = Field(..., description="角色唯一标识符 (UUID)") name: str = Field(..., description="角色名称") description: str = Field(..., description="角色详细描述") personality: str = Field(..., description="角色性格特征") scenario: str = Field(..., description="场景设定") first_mes: str = Field(..., description="首条开场消息") mes_example: str = Field(..., description="对话示例") categories: List[str] = Field(default_factory=list, description="分类标签 (用于前端筛选)") tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (SillyTavern 关键字机制)") worldInfoId: Optional[str] = Field(None, description="绑定的世界书 ID") outputSchema: Optional[List[OutputSchemaField]] = Field(None, description="输出 schema 定义 (结构化输出)") avatarPath: Optional[str] = Field(None, description="角色头像路径") alternate_greetings: Optional[List[str]] = Field(None, description="替代问候语数组") # TODO: 拓展提示词设置(插件/拓展系统预留接口) # - tableMaintenancePrompt: 用于指导 AI 维护动态表格(RPG状态、任务追踪等) # - imageGenerationPrompt: 用于指导 AI 生成图片描述提示词 # 当前状态:字段已定义,默认值为 None,等待插件系统实现 tableMaintenancePrompt: Optional[str] = Field(None, description="动态表格维护提示词 - 指导 AI 如何更新表格数据") imageGenerationPrompt: Optional[str] = Field(None, description="生图提示词模板 - 指导 AI 如何生成图片描述") # ✅ 动态表格数据(SillyTavern 关键字机制扩展) tableHeaders: Optional[List[str]] = Field(None, description="动态表格表头数组") tableDefaults: Optional[Dict[str, Any]] = Field(None, description="动态表格默认值对象") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") lastChatAt: Optional[int] = Field(None, description="最后聊天时间戳") isFavorite: bool = Field(False, description="收藏状态") version: int = Field(1, description="版本号") # ==================== 聊天记录 (Chat Log) ==================== # 历史记录模式枚举 class HistoryMode(str, Enum): """ 历史记录处理模式 - FULL: 全量模式,保留所有消息(需经正则处理) - SUMMARY: 总结模式,定期用LLM总结历史消息 - RAG: RAG模式,基于向量检索(暂不实现) """ FULL = 'full' # 全量模式 SUMMARY = 'summary' # 总结模式 RAG = 'rag' # RAG模式(预留) class SummaryConfig(BaseModel): """ 总结配置 用于控制历史消息的总结行为 """ enabled: bool = Field(True, description="是否启用总结") interval: int = Field(10, ge=2, description="总结间隔(每隔多少条消息总结一次)") includeUserInput: bool = Field(True, description="总结时是否包含用户输入") summaryPrompt: str = Field( "请总结以下对话内容,保留关键信息和上下文。用简洁的语言概括主要事件、人物状态和重要细节。", description="总结提示词" ) maxSummaryLength: int = Field(500, ge=100, description="总结文本的最大长度(字符数)") class ChatHeader(BaseModel): """ 项目内部聊天记录头 包含聊天的元数据,如参与角色、创建时间等。 """ id: str = Field(..., description="聊天唯一标识符 (UUID)") displayName: str = Field(..., description="显示名称 (聊天标题)") characterId: str = Field(..., description="关联的角色卡 ID") userName: str = Field("User", description="用户角色名") characterName: str = Field(..., description="AI 角色名称") tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (从角色卡继承)") # ✅ 历史记录模式配置 historyMode: HistoryMode = Field(HistoryMode.FULL, description="历史记录处理模式 (full/summary/rag)") summaryConfig: Optional[SummaryConfig] = Field(None, description="总结配置 (当 historyMode='summary' 时使用)") summaryCounter: int = Field(0, ge=0, description="总结计数器(独立于楼层,用于跟踪需要总结的消息数)") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") messageCount: int = Field(0, description="消息数量") ragLibraryId: Optional[str] = Field(None, description="关联的 RAG 历史消息库 ID") # Agent workflow engine (optional, backward compatible) workflowTemplateId: Optional[str] = Field(None, description="工作流模板 ID") engineRunId: Optional[str] = Field(None, description="最近一次引擎运行 ID") class ChatMessage(BaseModel): """ 项目内部聊天消息 单条对话消息,支持多版本 (swipes)、token 统计、历史记录总结等功能。 """ id: str = Field(..., description="消息唯一标识符 (UUID)") name: str = Field(..., description="发送者名称") is_user: bool = Field(..., description="是否为用户消息") is_system: Optional[bool] = Field(None, description="是否为系统消息") sendDate: str = Field(..., description="发送日期 ISO 字符串") mes: str = Field(..., description="消息内容文本") chatId: str = Field(..., description="关联的聊天 ID") swipes: Optional[List[str]] = Field(None, description="替换回答数组 (多版本)") swipe_id: Optional[int] = Field(0, description="当前选择的版本索引") tokenCount: Optional[int] = Field(None, description="Token 数量 (用于统计)") isTemporary: Optional[bool] = Field(None, description="是否为临时消息 (未保存)") # ✅ 历史记录总结相关字段 is_summarized: bool = Field(False, description="是否已被总结(中间楼层,内容为空)") is_summary: bool = Field(False, description="是否是总结消息(包含总结文本的楼层)") summary_range: Optional[str] = Field(None, description="总结范围描述(如 'L1-L8',仅在 is_summary=True 时有值)") class ChatLog(BaseModel): """ 项目内部完整聊天记录 包含聊天头和所有消息,是完整的对话历史。 """ header: ChatHeader = Field(..., description="聊天头") messages: List[ChatMessage] = Field(default_factory=list, description="消息列表") # ==================== 预设 (Preset) ==================== class GenerationPreset(BaseModel): """ 项目内部采样参数预设 控制 LLM 生成的参数配置,如温度、top_p 等。 """ id: str = Field(..., description="预设唯一标识符 (UUID)") name: str = Field(..., description="预设名称") temperature: float = Field(1.0, ge=0, le=2, description="温度 (控制随机性)") topP: float = Field(1.0, ge=0, le=1, description="Top P (核采样)") topK: int = Field(0, ge=0, description="Top K") repetitionPenalty: float = Field(1.0, ge=0, description="重复惩罚") frequencyPenalty: Optional[float] = Field(None, description="频率惩罚") presencePenalty: Optional[float] = Field(None, description="存在惩罚") maxLength: Optional[int] = Field(None, gt=0, description="最大生成长度") isDefault: bool = Field(False, description="是否为默认预设") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") # ==================== 提示词预设 (Prompt Preset) ==================== class PromptRole(str, Enum): """ Prompt 角色类型 内部业务层只保留三种角色,简化了 SillyTavern 的复杂角色系统。 """ SYSTEM = 'system' # 系统指令 AI = 'ai' # AI 助手 USER = 'user' # 用户 class PromptEntry(BaseModel): """ 内部业务层 - Prompt 条目 提示词模板的基本单元,可以组合成完整的提示词预设。 这是基于某个 character_id 生成的"当前视图"。 """ identifier: str = Field(..., description="稳定关联键 (用于回写)") name: str = Field(..., description="条目名 (前端显示)") enabled: bool = Field(True, description="是否启用 (当前作用域下的业务状态)") content: str = Field(..., description="条目内容 (静态内容视图)") order: int = Field(..., description="条目顺序 (前端展示和拖拽排序)") role: PromptRole = Field(..., description="角色类型") tokenCount: int = Field(0, description="总 token 数 (派生显示字段)") isSystemNode: bool = Field(False, description="是否固有节点 (不可删除)") class PromptPresetView(BaseModel): """ 内部业务层 - Prompt 预设视图 基于某个 character_id 的"当前视图",包含已排序、已过滤的条目列表。 """ characterId: str = Field(..., description="关联的角色 ID") entries: List[PromptEntry] = Field(default_factory=list, description="当前视图的条目列表") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") version: int = Field(1, description="版本号") # ==================== RAG 配置 ==================== class RAGSearchConfig(BaseModel): """RAG 搜索配置""" topK: int = Field(5, gt=0, description="每次检索返回的结果数") threshold: float = Field(0.7, ge=0, le=1, description="相似度阈值 (0-1)") maxContextLength: int = Field(2000, gt=0, description="最大上下文长度 (字符数)") class CharacterRAGConfig(BaseModel): """ 角色卡 RAG 世界书库配置 记录角色卡关联的 RAG 知识库,用于动态检索相关知识。 """ characterId: str = Field(..., description="角色卡ID") ragLibraryIds: List[str] = Field(default_factory=list, description="关联的RAG库ID列表") enabled: bool = Field(True, description="是否启用") searchConfig: Optional[RAGSearchConfig] = Field(None, description="搜索配置") position: str = Field('after_char', description="RAG内容插入位置") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") class ChatRAGConfig(BaseModel): """ 聊天会话 RAG 历史消息配置 记录聊天会话关联的 RAG 历史消息库,用于智能检索历史对话。 """ chatId: str = Field(..., description="聊天会话ID") ragLibraryId: Optional[str] = Field(None, description="关联的RAG历史消息库ID") enabled: bool = Field(True, description="是否启用") searchConfig: Optional[Dict[str, Any]] = Field(None, description="搜索配置") autoIndex: bool = Field(True, description="是否自动索引新消息") indexConfig: Optional[Dict[str, Any]] = Field(None, description="索引配置") createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳") updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳") # ==================== Token 统计 ==================== class TokenUsageStatus(str, Enum): """Token 使用状态""" COMPLETED = 'completed' # 成功完成 INTERRUPTED = 'interrupted' # 被用户中断 FAILED = 'failed' # 请求失败(API错误等) class TokenUsageRecord(BaseModel): """ Token 使用记录 记录每次 LLM 调用的 token 使用情况,支持按时间、角色、聊天维度统计 """ id: str = Field(..., description="记录唯一标识符 (UUID)") chatId: str = Field(..., description="聊天ID (role_name/chat_name)") roleName: str = Field(..., description="角色名称") chatName: str = Field(..., description="聊天名称") messageId: Optional[str] = Field(None, description="关联的消息ID") floor: Optional[int] = Field(None, description="楼层号") # Token 统计 promptTokens: int = Field(0, description="输入 token 数") completionTokens: int = Field(0, description="输出 token 数") totalTokens: int = Field(0, description="总 token 数") # 状态信息 status: TokenUsageStatus = Field(TokenUsageStatus.COMPLETED, description="请求状态") errorMessage: Optional[str] = Field(None, description="错误信息(如果失败)") # 时间信息 timestamp: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="请求时间戳") duration: Optional[float] = Field(None, description="请求耗时(秒)") # API 信息 model: Optional[str] = Field(None, description="使用的模型") apiProvider: Optional[str] = Field(None, description="API 提供商") apiUrl: Optional[str] = Field(None, description="API URL地址") # ==================== 图片元数据 ==================== class ImageMetadata(BaseModel): """ 图片元数据 记录生成的图片信息,绑定到角色/聊天的特定楼层 """ id: str = Field(..., description="图片唯一标识符 (UUID)") chatId: str = Field(..., description="聊天ID (role_name/chat_name)") roleName: str = Field(..., description="角色名称") chatName: str = Field(..., description="聊天名称") floor: int = Field(..., description="楼层号") # 图片信息 filename: str = Field(..., description="文件名") filepath: str = Field(..., description="文件相对路径") width: Optional[int] = Field(None, description="图片宽度") height: Optional[int] = Field(None, description="图片高度") fileSize: Optional[int] = Field(None, description="文件大小(字节)") # Swipe 支持 swipeIndex: int = Field(0, description="Swipe 索引(同一楼层多张图片)") isCurrentSwipe: bool = Field(True, description="是否为当前显示的 swipe") # 生成信息 prompt: Optional[str] = Field(None, description="生成使用的提示词") negativePrompt: Optional[str] = Field(None, description="负面提示词") seed: Optional[int] = Field(None, description="随机种子") model: Optional[str] = Field(None, description="使用的模型/checkpoint") workflowName: Optional[str] = Field(None, description="使用的工作流名称") # 任务信息 taskId: Optional[str] = Field(None, description="关联的任务ID") generationTime: Optional[float] = Field(None, description="生成耗时(秒)") # 时间信息 createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")