diff --git a/backend/api/routes/worldbooksRoute.py b/backend/api/routes/worldbooksRoute.py index 51fb451..7b5878e 100644 --- a/backend/api/routes/worldbooksRoute.py +++ b/backend/api/routes/worldbooksRoute.py @@ -9,16 +9,18 @@ from typing import List, Dict, Any, Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Form from fastapi.responses import JSONResponse, FileResponse +# 本地模块导入 # 本地模块导入 from backend.core.models.WorldBook import WorldBook from backend.core.models.WorldItem import ( - WorldInfoEntry, + WorldItem, TriggerConfig, KeywordTriggerConfig, RAGTriggerConfig, ConditionTriggerConfig, TriggerStrategy ) + from backend.core.config import settings # 配置日志 @@ -94,7 +96,6 @@ async def get_worldbook(name: str): @router.post("/", response_model=Dict[str, Any]) async def create_worldbook( name: str = Form(...), - description: str = Form(""), file: Optional[UploadFile] = File(None) ): """ @@ -121,7 +122,6 @@ async def create_worldbook( world_book = WorldBook.load(Path(temp_path).stem) # 更新名称和描述 world_book.name = name - world_book.description = description # 保存世界书 world_book.save() logger.info(f"从文件创建世界书: {name}") @@ -131,7 +131,7 @@ async def create_worldbook( os.remove(temp_path) else: # 创建空世界书 - world_book = WorldBook.create_empty(name, description) + world_book = WorldBook.create_empty(name) logger.info(f"创建空世界书: {name}") return world_book.to_dict() @@ -145,7 +145,6 @@ async def create_worldbook( @router.put("/{name}", response_model=Dict[str, Any]) async def update_worldbook( name: str, - description: Optional[str] = Form(None), file: Optional[UploadFile] = File(None) ): """ @@ -185,10 +184,6 @@ async def update_worldbook( if os.path.exists(temp_path): os.remove(temp_path) - # 更新描述(如果提供) - if description is not None: - world_book.description = description - # 保存世界书 world_book.save() logger.info(f"更新世界书: {name}") @@ -289,7 +284,7 @@ async def get_worldbook_entry(name: str, uid: int): raise HTTPException(status_code=404, detail=f"条目 UID {uid} 不存在") logger.info(f"获取世界书 {name} 的条目: UID={uid}") - return entry.dict() + return entry.to_dict() except HTTPException: raise except Exception as e: @@ -350,7 +345,7 @@ async def create_worldbook_entry(name: str, entry_data: Dict[str, Any]): entry_data["trigger_config"] = trigger_config # 创建条目 - entry = WorldInfoEntry(**entry_data) + entry = WorldItem.Entry(**entry_data) # 添加条目 world_book.add_entry(entry) @@ -359,7 +354,7 @@ async def create_worldbook_entry(name: str, entry_data: Dict[str, Any]): world_book.save() logger.info(f"在世界书 {name} 中创建条目: UID={entry.uid}") - return entry.dict() + return entry.to_dict() except HTTPException: raise except Exception as e: @@ -424,8 +419,7 @@ async def update_worldbook_entry(name: str, uid: int, entry_data: Dict[str, Any] # 设置触发配置 entry_data["trigger_config"] = trigger_config - # 过滤无效字段,只保留 WorldInfoEntry 中存在的字段 - valid_fields = WorldInfoEntry.__fields__.keys() + valid_fields = WorldItem.Entry.model_fields.keys() filtered_data = {k: v for k, v in entry_data.items() if k in valid_fields} # 更新条目 @@ -440,7 +434,7 @@ async def update_worldbook_entry(name: str, uid: int, entry_data: Dict[str, Any] entry = world_book.get_entry(uid) logger.info(f"更新世界书 {name} 的条目: UID={uid}") - return entry.dict() + return entry.to_dict() except HTTPException: raise except Exception as e: diff --git a/backend/core/models/WorldBook.py b/backend/core/models/WorldBook.py index 0700d3d..0066239 100644 --- a/backend/core/models/WorldBook.py +++ b/backend/core/models/WorldBook.py @@ -3,8 +3,8 @@ import os import logging from typing import Dict, List, Optional, Any from pathlib import Path -from pydantic import BaseModel, Field, validator -from .WorldItem import WorldInfoEntry, TriggerStrategy +from pydantic import BaseModel, Field, field_validator +from .WorldItem import WorldItem, TriggerStrategy from backend.core.config import settings # 配置日志 @@ -20,12 +20,13 @@ class WorldBook(BaseModel): name: str = Field(..., description="世界书名称") # 条目集合 - entries: Dict[str, WorldInfoEntry] = Field( + entries: Dict[str, WorldItem.Entry] = Field( default_factory=dict, description="世界书条目字典对象 (Key-Value Map)" ) - @validator('entries') + @field_validator('entries') + @classmethod def validate_entries_unique_uid(cls, v): """验证条目 UID 的唯一性""" uids = [entry.uid for entry in v.values()] @@ -91,7 +92,7 @@ class WorldBook(BaseModel): logger.info(f"创建空白世界书: {name}") return world_book - def add_entry(self, entry: WorldInfoEntry) -> None: + def add_entry(self, entry: WorldItem.Entry) -> None: """ 添加世界书条目 @@ -127,7 +128,7 @@ class WorldBook(BaseModel): logger.warning(f"尝试移除不存在的条目: 世界书 {self.name} 中未找到 UID={uid}") return False - def get_entry(self, uid: int) -> Optional[WorldInfoEntry]: + def get_entry(self, uid: int) -> Optional[WorldItem.Entry]: """ 获取指定 UID 的世界书条目 @@ -135,7 +136,7 @@ class WorldBook(BaseModel): uid: 条目 UID Returns: - Optional[WorldInfoEntry]: 找到的条目,未找到返回 None + Optional[WorldItem.Entry]: 找到的条目,未找到返回 None """ entry_key = str(uid) entry = self.entries.get(entry_key) @@ -167,7 +168,7 @@ class WorldBook(BaseModel): logger.info(f"已更新世界书 {self.name} 中的条目: UID={uid}, 更新字段={list(kwargs.keys())}") return True - def filter_by_position(self, position: int) -> List[WorldInfoEntry]: + def filter_by_position(self, position: int) -> List[WorldItem.Entry]: """ 根据位置筛选条目 @@ -175,7 +176,7 @@ class WorldBook(BaseModel): position: 位置值 Returns: - List[WorldInfoEntry]: 筛选后的条目列表 + List[WorldItem.Entry]: 筛选后的条目列表 """ filtered_entries = [ entry for entry in self.entries.values() @@ -227,12 +228,11 @@ class WorldBook(BaseModel): """ result = { 'name': self.name, - 'entries': {uid: entry.dict() for uid, entry in self.entries.items()} + 'entries': {uid: entry.model_dump() for uid, entry in self.entries.items()} } logger.debug(f"将世界书 {self.name} 转换为字典") return result - @classmethod @classmethod def load(cls, name: str) -> 'WorldBook': """ @@ -277,7 +277,10 @@ class WorldBook(BaseModel): # 转换标准格式的条目 for uid, entry_data in entries_dict.items(): try: - world_entry = WorldInfoEntry.from_sillytavern_data(entry_data) + # 先使用 WorldItem 解析数据 + world_item = WorldItem.from_sillytavern_data(entry_data) + # 然后转换为 Entry + world_entry = world_item.to_entry() world_book.add_entry(world_entry) except Exception as e: logger.warning(f"跳过条目 {uid},解析失败: {e}") @@ -336,26 +339,20 @@ class WorldBook(BaseModel): List[Dict[str, Any]]: 包含 trigger (key) 和 content 的列表 """ result = [] - for uid, entry in self.entries.items(): - # 获取启用的触发策略 + for entry in self.entries.values(): + entry_dict = entry.to_dict() + # 添加额外的触发相关信息 enabled_triggers = entry.trigger_config.get_enabled_triggers() - - # 检查是否启用了关键词触发 keyword_enabled, keyword_config = entry.trigger_config.get_trigger(TriggerStrategy.KEYWORD) - triggers = keyword_config.key if keyword_enabled and keyword_config else [] - - # 检查是否启用了永久触发 constant_enabled, _ = entry.trigger_config.get_trigger(TriggerStrategy.CONSTANT) - result.append({ - "uid": entry.uid, - "comment": entry.comment, - "triggers": triggers, - "content": entry.content, - "position": entry.position, + entry_dict.update({ + "triggers": keyword_config.key if keyword_enabled and keyword_config else [], "constant": constant_enabled, "trigger_strategies": [strategy.value for strategy in enabled_triggers] }) + result.append(entry_dict) + logger.debug(f"列出世界书 {self.name} 的触发词和内容: 条目数={len(result)}") return result @@ -366,26 +363,7 @@ class WorldBook(BaseModel): Returns: List[Dict[str, Any]]: 包含核心信息的条目列表 """ - result = [] - for uid, entry in self.entries.items(): - # 获取启用的触发策略 - enabled_triggers = entry.trigger_config.get_enabled_triggers() - - # 检查是否启用了永久触发 - constant_enabled, _ = entry.trigger_config.get_trigger(TriggerStrategy.CONSTANT) - - result.append({ - "uid": entry.uid, - "comment": entry.comment, - "content": entry.content, - "constant": constant_enabled, - "position": entry.position, - "order": entry.order, - "role": entry.role, - "disable": entry.disable, - "trigger_strategies": [strategy.value for strategy in enabled_triggers], - "trigger_params": entry.get_trigger_params() - }) + result = [entry.to_dict() for entry in self.entries.values()] logger.debug(f"获取世界书 {self.name} 的所有条目: 条目数={len(result)}") return result @@ -434,7 +412,7 @@ class WorldBook(BaseModel): if __name__ == "__main__": try: # 创建空白世界书 - world_book = WorldBook.create_empty("test_worldbook", "测试世界书") + world_book = WorldBook.create_empty("test_worldbook") # 加载世界书 world_book = WorldBook.load("test_worldbook") diff --git a/backend/core/models/WorldItem.py b/backend/core/models/WorldItem.py index 0643eb6..44a9100 100644 --- a/backend/core/models/WorldItem.py +++ b/backend/core/models/WorldItem.py @@ -1,13 +1,108 @@ import logging from enum import Enum from typing import List, Optional, Dict, Any, Union -from pydantic import BaseModel, Field, validator -from pydantic import Extra +from pydantic import BaseModel, Field, field_validator, ConfigDict # 配置日志 logger = logging.getLogger(__name__) +class WorldInfoPosition(Enum): + """ + SillyTavern 世界书条目插入位置枚举 + + 注意:枚举值的顺序(0-4)并不完全代表物理顺序! + 以下是按照 Prompt 从上到下的真实物理顺序排列的: + """ + + # --- 1. 顶部区域 --- + # (System Prompt 在这里,不可插入) + + # --- 2. 核心指令区 (Position 4 实际上在这里) --- + SYSTEM_PROMPT = 4 + """ + 物理位置:紧跟在系统提示词之后,角色定义之前。 + 语境:最高优先级的规则。 + 用途:作者注释、核心系统规则。AI 在读人设前就会先读到这个。 + """ + + # --- 3. 角色人设区 (Position 0 实际上在这里) --- + # (Character Definition 在这里) + + CHAR_AFTER = 0 + """ + 物理位置:紧跟在角色定义之后。 + 语境:角色固有属性。 + 用途:性格、外貌、长期设定。 + """ + + # --- 4. 示例对话区 --- + EXAMPLE_BEFORE = 2 + """ + 物理位置:在示例对话块之前。 + """ + + EXAMPLE_AFTER = 3 + """ + 物理位置:在示例对话块之后。 + """ + + # --- 5. 底部区域 --- + # (Chat History 在这里) + # (User Input 在这里 - 最新输入) + + # --- 6. 动态深度区 (Depth / d0-d99) --- + # 这是你强调的"第 6 个插入区" + # 它不是一个固定的物理点,而是一个动态区域 + + DEPTH_HISTORY = 4 + """ + 物理位置: + - d0: 在 [用户最新输入] 之前,[AI 回复] 之前。 + - d0~d99: 在 [Chat History] 内部,倒数第 N 条消息之前。 + + 语境: + - d0: 即时状态("现在正在发生")。 + - d1+: 历史背景("当时就在那里")。 + + 用途: + 这是最灵活的插入区,利用 Depth 字段来精确控制条目在对话流中的位置。 + """ + + @classmethod + def get_description(cls, position: int) -> str: + """ + 获取位置描述 + + Args: + position: 位置值 + + Returns: + str: 位置描述 + """ + position_map = { + 0: "角色定义之后", + 1: "角色定义之后 (最常用)", + 2: "示例对话之前", + 3: "示例对话之后", + 4: "系统提示 / 作者注释 (底部) 或 历史记录深度插入" + } + return position_map.get(position, "未知位置") + + @classmethod + def is_depth_position(cls, position: int) -> bool: + """ + 判断是否为深度插入位置 + + Args: + position: 位置值 + + Returns: + bool: 是否为深度插入位置 + """ + return position == cls.DEPTH_HISTORY.value + + class TriggerStrategy(str, Enum): """ 触发策略枚举 @@ -18,6 +113,15 @@ class TriggerStrategy(str, Enum): CONDITION = "condition" # 逻辑条件触发 +class RAGTriggerConfig(BaseModel): + """ + RAG触发配置 + """ + threshold: float = Field(0.75, description="RAG 相似度阈值") + top_k: int = Field(5, description="返回的匹配条目数") + query_template: Optional[str] = Field(None, description="检索用的查询模板") + + class KeywordTriggerConfig(BaseModel): """ 关键词触发配置 @@ -30,15 +134,6 @@ class KeywordTriggerConfig(BaseModel): caseSensitive: bool = Field(False, description="是否区分大小写") -class RAGTriggerConfig(BaseModel): - """ - RAG触发配置 - """ - threshold: float = Field(0.75, description="RAG 相似度阈值") - top_k: int = Field(5, description="返回的匹配条目数") - query_template: Optional[str] = Field(None, description="检索用的查询模板") - - class ConditionTriggerConfig(BaseModel): """ 条件触发配置 @@ -64,8 +159,7 @@ class TriggerConfig(BaseModel): description="触发配置字典,键为触发策略,值为[是否启用, 对应配置]" ) - class Config: - extra = Extra.forbid # 禁止额外字段 + model_config = ConfigDict(extra='forbid') def set_trigger(self, strategy: TriggerStrategy, enabled: bool, config: Optional[Union[KeywordTriggerConfig, RAGTriggerConfig, ConditionTriggerConfig]] = None @@ -103,187 +197,233 @@ class TriggerConfig(BaseModel): return [strategy for strategy, (enabled, _) in self.triggers.items() if enabled] -class WorldInfoEntry(BaseModel): +class WorldItem(BaseModel): """ - 世界书条目模型 - 完整兼容所有可能的属性字段 + 世界书条目完整模型 + 包含所有 SillyTavern 世界书条目属性,用于导入导出 """ - # A. 基础定义 (Base Fields) + + class Entry(BaseModel): + """ + 世界书条目模型 + 精简版,只包含必要字段,用于实际使用 + """ + # 基础定义 + uid: int = Field(..., description="唯一标识符") + content: str = Field(..., description="注入到 Prompt 的实际文本内容") + comment: str = Field("", description="条目名、备注") + + # 注入与排序 + position: int = Field(0, + description="插入位置 (0=角色定义之前, 1=角色定义之后, 2=示例对话之前, 3=示例对话之后, 4=系统提示/作者注释)") + order: int = Field(100, description="注入顺序权重,数字越小优先级越高") + depth: int = Field(4, description="扫描深度,0为最深/最高,4为标准") + + # 触发配置 + trigger_config: Optional[TriggerConfig] = Field( + default_factory=TriggerConfig, + description="触发配置,为空表示无需触发配置" + ) + # 角色匹配 + role: int = Field(0, description="角色匹配 (0=Both, 1=User, 2=Assistant)") + + # 条目启用状态 + enabled: bool = Field(True, description="条目是否启用(启用才会被插入到LLM)") + + @field_validator('position') + @classmethod + def validate_position(cls, v): + """验证 position 值是否在有效范围内""" + if v not in [0, 1, 2, 3, 4]: + logger.warning(f"无效的 position 值: {v},将使用默认值 1") + return 1 + return v + + def to_dict(self) -> Dict[str, Any]: + """ + 转换为字典 + + Returns: + Dict[str, Any]: 字典数据 + """ + return self.dict() + + def get_trigger_params(self) -> Dict[str, Any]: + """ + 获取触发策略所需的参数 + + Returns: + Dict[str, Any]: 触发参数字典 + """ + params = {} + + try: + # 获取所有启用的触发策略 + enabled_triggers = self.trigger_config.get_enabled_triggers() + + # 处理 RAG 触发 + if TriggerStrategy.RAG in enabled_triggers: + _, rag_config = self.trigger_config.get_trigger(TriggerStrategy.RAG) + if rag_config: + params["threshold"] = rag_config.threshold + params["top_k"] = rag_config.top_k + params["query_template"] = rag_config.query_template + params["vectorized"] = True + + # 处理关键词触发 + if TriggerStrategy.KEYWORD in enabled_triggers: + _, keyword_config = self.trigger_config.get_trigger(TriggerStrategy.KEYWORD) + if keyword_config: + params["key"] = keyword_config.key + params["keysecondary"] = keyword_config.keysecondary + params["selective"] = keyword_config.selective + params["selectiveLogic"] = keyword_config.selectiveLogic + params["matchWholeWords"] = keyword_config.matchWholeWords + params["caseSensitive"] = keyword_config.caseSensitive + + # 处理条件触发 + if TriggerStrategy.CONDITION in enabled_triggers: + _, condition_config = self.trigger_config.get_trigger(TriggerStrategy.CONDITION) + if condition_config: + params["variable_a"] = condition_config.variable_a + params["operator"] = condition_config.operator + params["variable_b"] = condition_config.variable_b + except Exception as e: + # 如果获取触发参数失败,返回空字典,表示使用默认的永久触发 + logger.warning(f"条目 {self.uid} 的触发参数获取失败: {str(e)},使用默认的永久触发") + + return params + + # 基础定义 uid: int = Field(..., description="唯一标识符") - key: List[str] = Field(default_factory=list, description="条目名") content: str = Field(..., description="注入到 Prompt 的实际文本内容") - comment: str = Field("", description="备注") + comment: str = Field("", description="条目名、备注") - # B. 注入与排序 (Injection & Order) - position: int = Field(0, description="插入位置") - order: int = Field(100, description="注入顺序权重") - depth: int = Field(4, description="扫描深度") - scanDepth: Optional[int] = Field(None, description="显式扫描深度") - addMemo: bool = Field(False, description="是否添加备忘录标记") + # 注入与排序 + position: int = Field(0, + description="插入位置 (0=角色定义之前, 1=角色定义之后, 2=示例对话之前, 3=示例对话之后, 4=系统提示/作者注释)") + order: int = Field(100, description="注入顺序权重,数字越小优先级越高") + depth: int = Field(4, description="扫描深度,0为最深/最高,4为标准") - # C. 触发配置 (Trigger Configuration) + # 触发配置 trigger_config: TriggerConfig = Field( default_factory=TriggerConfig, description="触发配置" ) - # D. 高级匹配 (Advanced Matching) + # 角色匹配 role: int = Field(0, description="角色匹配 (0=Both, 1=User, 2=Assistant)") + + # 条目启用状态 + enabled: bool = Field(True, description="条目是否启用(启用才会被插入到LLM)") + + # 触发相关属性 + vectorized: bool = Field(False, description="是否使用向量检索(RAG触发)") + selective: bool = Field(True, description="是否开启选择性匹配(关键词触发)") + selectiveLogic: int = Field(0, description="逻辑模式 (0=OR, 1=AND)") + constant: bool = Field(False, description="是否永久触发") + + # 关键词相关 + key: List[str] = Field(default_factory=list, description="主关键词数组") + keysecondary: List[str] = Field(default_factory=list, description="次要关键词数组") + matchWholeWords: Optional[bool] = Field(None, description="是否全词匹配") + caseSensitive: Optional[bool] = Field(None, description="是否区分大小写") + + # RAG相关 + rag_threshold: Optional[float] = Field(None, description="RAG 相似度阈值") + top_k: Optional[int] = Field(None, description="返回的匹配条目数") + query_template: Optional[str] = Field(None, description="检索用的查询模板") + + # 条目控制 + addMemo: bool = Field(True, description="是否添加备忘") + disable: bool = Field(False, description="是否禁用") + ignoreBudget: bool = Field(False, description="是否忽略预算") + excludeRecursion: bool = Field(True, description="是否排除递归") + preventRecursion: bool = Field(True, description="是否阻止递归") + matchPersonaDescription: bool = Field(False, description="是否匹配人设描述") + matchCharacterDescription: bool = Field(False, description="是否匹配角色描述") + matchCharacterPersonality: bool = Field(False, description="是否匹配角色性格") + matchCharacterDepthPrompt: bool = Field(False, description="是否匹配深度提示") + matchScenario: bool = Field(False, description="是否匹配场景") + matchCreatorNotes: bool = Field(False, description="是否匹配作者笔记") + delayUntilRecursion: bool = Field(False, description="是否延迟递归") + + # 概率相关 + probability: int = Field(100, description="触发概率 (0-100)") + useProbability: bool = Field(True, description="是否使用概率") + + # 分组相关 + group: str = Field("", description="分组名称") + groupOverride: bool = Field(False, description="是否覆盖分组") + groupWeight: int = Field(100, description="分组权重") useGroupScoring: bool = Field(False, description="是否使用分组评分") - # E. 分组设置 (Grouping) - group: Optional[str] = Field(None, description="分组名称") - groupOverride: bool = Field(False, description="是否覆盖分组限制") - groupWeight: int = Field(100, description="分组权重") + # 其他属性 + scanDepth: Optional[int] = Field(None, description="扫描深度") + automationId: str = Field("", description="自动化ID") + sticky: int = Field(0, description="粘性") + cooldown: int = Field(0, description="冷却时间(秒)") + delay: int = Field(0, description="延迟时间(秒)") + displayIndex: int = Field(0, description="显示索引") - # F. 递归与防抖 (Recursion) - excludeRecursion: bool = Field(True, description="排除递归") - preventRecursion: bool = Field(True, description="防止递归") - delayUntilRecursion: bool = Field(False, description="延迟直到递归发生") + # 角色过滤器 + characterFilter: Dict[str, Any] = Field( + default_factory=lambda: {"isExclude": False, "names": [], "tags": []}, + description="角色过滤器" + ) - # G. 状态与元数据 (State & Metadata) - disable: bool = Field(False, description="是否禁用该条目") - ignoreBudget: bool = Field(False, description="忽略 Token 预算") - outletName: Optional[str] = Field(None, description="出口名称") - automationId: Optional[str] = Field(None, description="自动化 ID") - sticky: int = Field(0, description="粘性值") - cooldown: int = Field(0, description="冷却时间") - delay: int = Field(0, description="延迟触发") - triggers: List[str] = Field(default_factory=list, description="触发器数组") - displayIndex: int = Field(0, description="UI 显示索引") - vectorized: bool = Field(False, description="是否已向量化") + # 验证器 + @field_validator('position') + @classmethod + def validate_position(cls, v): + """验证 position 值是否在有效范围内""" + if v not in [0, 1, 2, 3, 4]: + logger.warning(f"无效的 position 值: {v},将使用默认值 1") + return 1 + return v - # H. 兼容性字段 (Compatibility) - matchPersonaDescription: bool = Field(False, description="匹配人设描述") - matchCharacterDescription: bool = Field(False, description="匹配角色描述") - matchCharacterPersonality: bool = Field(False, description="匹配角色性格") - matchCharacterDepthPrompt: bool = Field(False, description="匹配深度提示词") - matchScenario: bool = Field(False, description="匹配场景") - matchCreatorNotes: bool = Field(False, description="匹配作者注释") - - # I. 嵌套对象 (Nested Objects) - characterFilter: Optional[Dict[str, Any]] = Field(None, description="角色过滤器配置") - extensions: Optional[Dict[str, Any]] = Field(None, description="扩展属性") + @field_validator('role') + @classmethod + def validate_role(cls, v): + """验证 role 值是否在有效范围内""" + if v not in [0, 1, 2]: + logger.warning(f"无效的 role 值: {v},将使用默认值 2") + return 2 + return v @classmethod - def from_sillytavern_data(cls, data: Dict[str, Any]) -> 'WorldInfoEntry': + def from_dict(cls, data: Dict[str, Any]) -> 'WorldItem': """ - 从 SillyTavern 格式的数据创建条目 + 从字典创建 WorldItem 对象 Args: - data: SillyTavern 格式的条目数据 + data: 字典数据 Returns: - WorldInfoEntry: 世界书条目对象 - - Raises: - ValueError: 数据格式无效 + WorldItem: WorldItem 对象 """ - try: - # 提取基本字段 - uid = int(data.get("uid", data.get("id", 0))) # 条目唯一标识符 - key = data.get("key", data.get("keys", [])) # 条目触发关键词数组,用于匹配和触发该条目 - keysecondary = data.get("keysecondary", data.get("secondary_keys", [])) # 次要关键词数组,用于扩展触发条件 - comment = data.get("comment", "") # 条目备注说明,用于描述条目的用途和特点 - content = data.get("content", "") # 条目的实际内容,将被注入到提示词中 - constant = data.get("constant", False) # 是否常驻注入,true表示始终注入 - selective = data.get("selective", True) # 是否启用选择性匹配,true表示仅关键词匹配时注入 - order = data.get("order", data.get("insertion_order", 100)) # 条目注入顺序权重,数字越小越优先 + return cls(**data) - # 处理 position 字段,确保为整数类型 - position = data.get("position", 0) - if isinstance(position, str): - # 如果是字符串,尝试转换为整数 - try: - position = int(position) - except ValueError: - # 如果转换失败,使用默认值 - logger.warning(f"条目 {uid} 的 position 字段值 '{position}' 无法转换为整数,使用默认值 0") - position = 0 + def to_dict(self) -> Dict[str, Any]: + """ + 转换为字典 - # 处理触发配置,默认使用永久触发 - try: - trigger_config = TriggerConfig() + Returns: + Dict[str, Any]: 字典数据 + """ + return self.dict() - # 设置永久触发 - trigger_config.set_trigger(TriggerStrategy.CONSTANT, constant) + def to_entry(self) -> Entry: + """ + 转换为 Entry 对象 - # 设置关键词触发 - if not constant and key: - keyword_config = KeywordTriggerConfig( - key=key, - keysecondary=keysecondary, - selective=selective, - selectiveLogic=data.get("selectiveLogic", 0), - matchWholeWords=data.get("matchWholeWords", False), - caseSensitive=data.get("caseSensitive", False) - ) - trigger_config.set_trigger(TriggerStrategy.KEYWORD, True, keyword_config) - - # 设置RAG触发 - if "rag_threshold" in data or "top_k" in data or "query_template" in data: - rag_config = RAGTriggerConfig( - threshold=data.get("rag_threshold", 0.75), - top_k=data.get("top_k", 5), - query_template=data.get("query_template", None) - ) - trigger_config.set_trigger(TriggerStrategy.RAG, True, rag_config) - - # 设置条件触发 - if "variable_a" in data and "operator" in data and "variable_b" in data: - condition_config = ConditionTriggerConfig( - variable_a=data.get("variable_a", ""), - operator=data.get("operator", "="), - variable_b=data.get("variable_b", "") - ) - trigger_config.set_trigger(TriggerStrategy.CONDITION, True, condition_config) - except Exception as e: - # 如果触发配置解析失败,使用默认的永久触发配置 - logger.warning(f"条目 {uid} 的触发配置解析失败: {str(e)},使用默认的永久触发配置") - trigger_config = TriggerConfig() - trigger_config.set_trigger(TriggerStrategy.CONSTANT, True) - - # 处理其他字段 - probability = data.get("probability", 100) - useProbability = data.get("useProbability", False) - group = data.get("group", None) - use_regex = data.get("useRegex", False) - - # 处理 extensions 字段(如果存在) - extensions = data.get("extensions", {}) - if extensions: - probability = extensions.get("probability", probability) - useProbability = extensions.get("useProbability", useProbability) - group = extensions.get("group", group) - - # 创建条目对象 - entry = cls( - uid=uid, - key=key, # 保留key字段用于兼容 - comment=comment, - content=content, - position=position, - order=order, - trigger_config=trigger_config, - group=group, - probability=probability, - useProbability=useProbability, - use_regex=use_regex - ) - - # 复制其他字段(如果存在) - for key, value in data.items(): - if hasattr(entry, key) and key not in ["uid", "key", "comment", "content", - "constant", "position", "order", "probability", - "useProbability", "group", "use_regex", - "keysecondary", "selective", "selectiveLogic", - "matchWholeWords", "caseSensitive"]: - setattr(entry, key, value) - - return entry - except Exception as e: - logger.error(f"从 SillyTavern 数据创建条目失败: {str(e)}") - raise ValueError(f"从 SillyTavern 数据创建条目失败: {str(e)}") + Returns: + Entry: Entry 对象 + """ + # 转换为 SillyTavern 格式的字典 + sillytavern_dict = self.to_sillytavern_dict() + # 创建 Entry 对象 + return self.Entry(**sillytavern_dict) def to_sillytavern_dict(self) -> Dict[str, Any]: """ @@ -294,96 +434,393 @@ class WorldInfoEntry(BaseModel): """ result = { "uid": self.uid, - "key": self.key, - "comment": self.comment, "content": self.content, + "comment": self.comment, "position": self.position, "order": self.order, + "depth": self.depth, + "role": self.role, + "enabled": self.enabled, + "vectorized": self.vectorized, + "selective": self.selective, + "selectiveLogic": self.selectiveLogic, + "constant": self.constant, + "key": self.key, + "keysecondary": self.keysecondary, + "matchWholeWords": self.matchWholeWords, + "caseSensitive": self.caseSensitive, + "addMemo": self.addMemo, + "disable": self.disable, + "ignoreBudget": self.ignoreBudget, + "excludeRecursion": self.excludeRecursion, + "preventRecursion": self.preventRecursion, + "matchPersonaDescription": self.matchPersonaDescription, + "matchCharacterDescription": self.matchCharacterDescription, + "matchCharacterPersonality": self.matchCharacterPersonality, + "matchCharacterDepthPrompt": self.matchCharacterDepthPrompt, + "matchScenario": self.matchScenario, + "matchCreatorNotes": self.matchCreatorNotes, + "delayUntilRecursion": self.delayUntilRecursion, "probability": self.probability, "useProbability": self.useProbability, "group": self.group, - "useRegex": getattr(self, 'use_regex', False), - "preventRecursion": self.preventRecursion, - "excludeRecursion": self.excludeRecursion + "groupOverride": self.groupOverride, + "groupWeight": self.groupWeight, + "scanDepth": self.scanDepth, + "automationId": self.automationId, + "sticky": self.sticky, + "cooldown": self.cooldown, + "delay": self.delay, + "displayIndex": self.displayIndex, + "characterFilter": self.characterFilter } - # 根据触发策略设置对应的字段 - try: - # 检查永久触发是否启用 - constant_enabled, _ = self.trigger_config.get_trigger(TriggerStrategy.CONSTANT) - result["constant"] = constant_enabled + # 添加 RAG 相关字段 + if self.vectorized: + result["rag_threshold"] = self.rag_threshold + result["top_k"] = self.top_k + result["query_template"] = self.query_template - # 检查关键词触发是否启用 - keyword_enabled, keyword_config = self.trigger_config.get_trigger(TriggerStrategy.KEYWORD) - if keyword_enabled and keyword_config: - result["selective"] = keyword_config.selective - result["keysecondary"] = keyword_config.keysecondary - result["selectiveLogic"] = keyword_config.selectiveLogic - result["matchWholeWords"] = keyword_config.matchWholeWords - result["caseSensitive"] = keyword_config.caseSensitive - - # 检查RAG触发是否启用 - rag_enabled, rag_config = self.trigger_config.get_trigger(TriggerStrategy.RAG) - if rag_enabled and rag_config: - result["rag_threshold"] = rag_config.threshold - result["top_k"] = rag_config.top_k - result["query_template"] = rag_config.query_template - - # 检查条件触发是否启用 - condition_enabled, condition_config = self.trigger_config.get_trigger(TriggerStrategy.CONDITION) - if condition_enabled and condition_config: + # 添加条件触发相关字段 + if TriggerStrategy.CONDITION in self.trigger_config.get_enabled_triggers(): + condition_config = self.trigger_config.get_trigger(TriggerStrategy.CONDITION)[1] + if condition_config: result["variable_a"] = condition_config.variable_a result["operator"] = condition_config.operator result["variable_b"] = condition_config.variable_b - except Exception as e: - # 如果触发配置导出失败,使用默认的永久触发配置 - logger.warning(f"条目 {self.uid} 的触发配置导出失败: {str(e)},使用默认的永久触发配置") - result["constant"] = True return result - def get_trigger_params(self) -> Dict[str, Any]: + @classmethod + def from_sillytavern_data(cls, data: Dict[str, Any]) -> 'WorldItem': """ - 获取触发策略所需的参数 + 从 SillyTavern 格式的数据创建 WorldItem 对象 + + Args: + data: SillyTavern 格式的条目数据 Returns: - Dict[str, Any]: 触发参数字典 + WorldItem: WorldItem 对象 """ - params = {} + + constant = data.get("constant", False) + if isinstance(constant, str): + constant = constant.lower() in ('true', '1', 'yes') + + enabled = data.get("enabled", True) + if isinstance(enabled, str): + enabled = enabled.lower() in ('true', '1', 'yes') try: - # 获取所有启用的触发策略 - enabled_triggers = self.trigger_config.get_enabled_triggers() + # 提取必要字段 + uid = int(data.get("uid", data.get("id", 0))) + content = data.get("content", "") + comment = data.get("comment", "") + position = data.get("position", 0) + order = data.get("order", 100) + depth = data.get("depth", 4) + role = data.get("role", 0) + enabled = data.get("enabled", True) - # 处理关键词触发 - if TriggerStrategy.KEYWORD in enabled_triggers: - _, keyword_config = self.trigger_config.get_trigger(TriggerStrategy.KEYWORD) - if keyword_config: - params["key"] = keyword_config.key - params["keysecondary"] = keyword_config.keysecondary - params["selective"] = keyword_config.selective - params["selectiveLogic"] = keyword_config.selectiveLogic - params["matchWholeWords"] = keyword_config.matchWholeWords - params["caseSensitive"] = keyword_config.caseSensitive + # 处理 position 字段,确保为整数类型 + if isinstance(position, str): + try: + position = int(position) + except ValueError: + logger.warning(f"条目 {uid} 的 position 字段值 '{position}' 无法转换为整数,使用默认值 0") + position = 0 - # 处理RAG触发 - if TriggerStrategy.RAG in enabled_triggers: - _, rag_config = self.trigger_config.get_trigger(TriggerStrategy.RAG) - if rag_config: - params["threshold"] = rag_config.threshold - params["top_k"] = rag_config.top_k - params["query_template"] = rag_config.query_template - params["vectorized"] = self.vectorized + # 初始化触发配置 + trigger_config = TriggerConfig() - # 处理条件触发 - if TriggerStrategy.CONDITION in enabled_triggers: - _, condition_config = self.trigger_config.get_trigger(TriggerStrategy.CONDITION) - if condition_config: - params["variable_a"] = condition_config.variable_a - params["operator"] = condition_config.operator - params["variable_b"] = condition_config.variable_b + # 读取触发相关字段,并进行类型转换 + vectorized = data.get("vectorized", False) + if isinstance(vectorized, str): + vectorized = vectorized.lower() in ('true', '1', 'yes') + + selective = data.get("selective", True) + if isinstance(selective, str): + selective = selective.lower() in ('true', '1', 'yes') + + constant = data.get("constant", False) + if isinstance(constant, str): + constant = constant.lower() in ('true', '1', 'yes') + + # 初始化变量,确保它们始终有值 + key = [] + keysecondary = [] + selectiveLogic = 0 + matchWholeWords = False + caseSensitive = False + + # 判断触发策略并设置对应的触发配置 + # 优先级:vectorized > constant > selective + if vectorized: + # RAG 触发 + rag_config = RAGTriggerConfig( + threshold=float(data.get("rag_threshold", 0.75)), + top_k=int(data.get("top_k", 5)), + query_template=data.get("query_template", None) + ) + trigger_config.set_trigger(TriggerStrategy.RAG, True, rag_config) + elif constant: + # 永久触发 + trigger_config.set_trigger(TriggerStrategy.CONSTANT, True) + elif selective: + # 关键词触发 + key = data.get("key", []) + keysecondary = data.get("keysecondary", data.get("secondary_keys", [])) + selectiveLogic = int(data.get("selectiveLogic", 0)) + + # 处理 matchWholeWords 字段 + matchWholeWords = data.get("matchWholeWords", False) + if matchWholeWords is None: + matchWholeWords = False + elif isinstance(matchWholeWords, str): + matchWholeWords = matchWholeWords.lower() in ('true', '1', 'yes') + + # 处理 caseSensitive 字段 + caseSensitive = data.get("caseSensitive", False) + if caseSensitive is None: + caseSensitive = False + elif isinstance(caseSensitive, str): + caseSensitive = caseSensitive.lower() in ('true', '1', 'yes') + + keyword_config = KeywordTriggerConfig( + key=key, + keysecondary=keysecondary, + selective=selective, + selectiveLogic=selectiveLogic, + matchWholeWords=matchWholeWords, + caseSensitive=caseSensitive + ) + trigger_config.set_trigger(TriggerStrategy.KEYWORD, True, keyword_config) + else: + # 默认使用永久触发 + trigger_config.set_trigger(TriggerStrategy.CONSTANT, True) + + # 检查是否有条件触发(虽然 JSON 中没有对应字段,但需要保留兼容性) + if "variable_a" in data and "operator" in data and "variable_b" in data: + condition_config = ConditionTriggerConfig( + variable_a=data.get("variable_a", ""), + operator=data.get("operator", "="), + variable_b=data.get("variable_b", "") + ) + trigger_config.set_trigger(TriggerStrategy.CONDITION, True, condition_config) + + # 创建 WorldItem 对象 + return cls( + uid=uid, + content=content, + comment=comment, + position=position, + order=order, + depth=depth, + trigger_config=trigger_config, + role=role, + enabled=enabled, + vectorized=vectorized, + selective=selective, + selectiveLogic=selectiveLogic, + constant=constant, + key=key, + keysecondary=keysecondary, + matchWholeWords=matchWholeWords, + caseSensitive=caseSensitive, + rag_threshold=float(data.get("rag_threshold", None)) if vectorized else None, + top_k=int(data.get("top_k", None)) if vectorized else None, + query_template=data.get("query_template", None), + addMemo=data.get("addMemo", True), + disable=data.get("disable", False), + ignoreBudget=data.get("ignoreBudget", False), + excludeRecursion=data.get("excludeRecursion", True), + preventRecursion=data.get("preventRecursion", True), + matchPersonaDescription=data.get("matchPersonaDescription", False), + matchCharacterDescription=data.get("matchCharacterDescription", False), + matchCharacterPersonality=data.get("matchCharacterPersonality", False), + matchCharacterDepthPrompt=data.get("matchCharacterDepthPrompt", False), + matchScenario=data.get("matchScenario", False), + matchCreatorNotes=data.get("matchCreatorNotes", False), + delayUntilRecursion=data.get("delayUntilRecursion", False), + probability=data.get("probability", 100), + useProbability=data.get("useProbability", True), + group=data.get("group", ""), + groupOverride=data.get("groupOverride", False), + groupWeight=data.get("groupWeight", 100), + useGroupScoring=data.get("useGroupScoring", False), + scanDepth=data.get("scanDepth", None), + automationId=data.get("automationId", ""), + sticky=data.get("sticky", 0), + cooldown=data.get("cooldown", 0), + delay=data.get("delay", 0), + displayIndex=data.get("displayIndex", 0), + characterFilter=data.get("characterFilter", {"isExclude": False, "names": [], "tags": []}) + ) except Exception as e: - # 如果获取触发参数失败,返回空字典,表示使用默认的永久触发 - logger.warning(f"条目 {self.uid} 的触发参数获取失败: {str(e)},使用默认的永久触发") + logger.error(f"从 SillyTavern 数据创建 WorldItem 失败: {str(e)}") + raise ValueError(f"从 SillyTavern 数据创建 WorldItem 失败: {str(e)}") - return params \ No newline at end of file + +if __name__ == "__main__": + """ + 测试入口:用于调试 WorldItem 的解析和转换功能 + 可以像断点调试一样查看内部执行过程 + """ + import json + import sys + from pathlib import Path + + # 测试用例1:基本条目 + test_data_1 = { + "uid": 0, + "content": "测试内容", + "comment": "测试条目", + "position": 0, + "order": 100, + "depth": 4, + "role": 0, + "vectorized": False, + "selective": True, + "selectiveLogic": 0, + "constant": False, + "key": ["测试关键词"], + "keysecondary": [], + "matchWholeWords": False, + "caseSensitive": False, + "addMemo": True, + "disable": False, + "ignoreBudget": False, + "excludeRecursion": True, + "preventRecursion": True + } + + # 测试用例2:RAG触发 + test_data_2 = { + "uid": 1, + "content": "RAG测试内容", + "comment": "RAG测试条目", + "position": 4, + "order": 50, + "depth": 0, + "role": 0, + "vectorized": True, + "rag_threshold": 0.8, + "top_k": 10, + "query_template": "测试模板" + } + + # 测试用例3:条件触发 + test_data_3 = { + "uid": 2, + "content": "条件触发测试", + "comment": "条件触发条目", + "position": 1, + "order": 75, + "depth": 2, + "role": 0, + "variable_a": "好感度", + "operator": ">", + "variable_b": "50" + } + + print("=" * 60) + print("开始测试 WorldItem 解析功能") + print("=" * 60) + + try: + # 测试1:解析基本条目 + print("\n【测试1】解析基本条目...") + item1 = WorldItem.from_sillytavern_data(test_data_1) + print(f"✓ 解析成功: {item1.comment}") + print(f" - UID: {item1.uid}") + print(f" - Position: {item1.position}") + print(f" - 触发策略和配置:") + for strategy in item1.trigger_config.get_enabled_triggers(): + enabled, config = item1.trigger_config.get_trigger(strategy) + print(f" * {strategy.value}: 启用={enabled}, 配置={config}") + + # 测试2:解析RAG触发条目 + print("\n【测试2】解析RAG触发条目...") + item2 = WorldItem.from_sillytavern_data(test_data_2) + print(f"✓ 解析成功: {item2.comment}") + print(f" - UID: {item2.uid}") + print(f" - Position: {item2.position}") + print(f" - 触发策略和配置:") + for strategy in item2.trigger_config.get_enabled_triggers(): + enabled, config = item2.trigger_config.get_trigger(strategy) + print(f" * {strategy.value}: 启用={enabled}, 配置={config}") + if item2.rag_threshold: + print(f" - RAG阈值: {item2.rag_threshold}") + + # 测试3:解析条件触发条目 + print("\n【测试3】解析条件触发条目...") + item3 = WorldItem.from_sillytavern_data(test_data_3) + print(f"✓ 解析成功: {item3.comment}") + print(f" - UID: {item3.uid}") + print(f" - Position: {item3.position}") + print(f" - 触发策略和配置:") + for strategy in item3.trigger_config.get_enabled_triggers(): + enabled, config = item3.trigger_config.get_trigger(strategy) + print(f" * {strategy.value}: 启用={enabled}, 配置={config}") + + # 测试4:从文件读取实际数据 + print("\n【测试4】从实际JSON文件读取...") + # 从当前文件位置向上查找项目根目录 + current_file = Path(__file__).resolve() + project_root = current_file + while project_root.name != "llm_workflow_engine" and project_root.parent != project_root: + project_root = project_root.parent + + # 构建正确的文件路径 + json_path = project_root / "data" / "worldbooks" / "卡立创-v5.json" + print(f"查找文件路径: {json_path}") + + if json_path.exists(): + with open(json_path, 'r', encoding='utf-8') as f: + worldbook_data = json.load(f) + entries = worldbook_data.get('entries', {}) + print(f"找到 {len(entries)} 个条目") + + # 只测试前3个条目 + for uid, entry_data in list(entries.items())[:3]: + try: + item = WorldItem.from_sillytavern_data(entry_data) + print(f"\n✓ 条目 {uid} 解析成功:") + print(f" - 备注: {item.comment}") + print(f" - UID: {item.uid}") + print(f" - Position: {item.position}") + print(f" - 触发策略和配置:") + for strategy in item.trigger_config.get_enabled_triggers(): + enabled, config = item.trigger_config.get_trigger(strategy) + print(f" * {strategy.value}: 启用={enabled}, 配置={config}") + except Exception as e: + print(f"\n✗ 条目 {uid} 解析失败: {str(e)}") + import traceback + + traceback.print_exc() + else: + print(f"⚠ 文件不存在: {json_path}") + print(f"请确认文件路径是否正确") + # 列出可能的文件位置 + possible_paths = [ + project_root / "data" / "worldbooks", + project_root / "backend" / "data" / "worldbooks", + current_file.parent.parent.parent / "data" / "worldbooks" + ] + print("\n可能的文件位置:") + for path in possible_paths: + if path.exists(): + print(f" ✓ {path}") + for file in path.glob("*.json"): + print(f" - {file.name}") + + print("\n" + "=" * 60) + print("所有测试完成!") + print("=" * 60) + + except Exception as e: + print(f"\n✗ 测试失败: {str(e)}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/frontend-react/src/Store/Slices/LeftTabsSlices/WorldBookSlice.jsx b/frontend-react/src/Store/Slices/LeftTabsSlices/WorldBookSlice.jsx index 09985c8..3c9c4eb 100644 --- a/frontend-react/src/Store/Slices/LeftTabsSlices/WorldBookSlice.jsx +++ b/frontend-react/src/Store/Slices/LeftTabsSlices/WorldBookSlice.jsx @@ -1,5 +1,28 @@ import { create } from 'zustand'; +// LocalStorage 键名 +const GLOBAL_WORLDBOOKS_KEY = 'global_worldbooks'; + +// 辅助函数:从 LocalStorage 加载全局世界书 +const loadGlobalWorldBooks = () => { + try { + const stored = localStorage.getItem(GLOBAL_WORLDBOOKS_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.error('加载全局世界书失败:', error); + return []; + } +}; + +// 辅助函数:保存全局世界书到 LocalStorage +const saveGlobalWorldBooks = (globalBooks) => { + try { + localStorage.setItem(GLOBAL_WORLDBOOKS_KEY, JSON.stringify(globalBooks)); + } catch (error) { + console.error('保存全局世界书失败:', error); + } +}; + // 辅助函数:处理 API 响应 const handleResponse = async (response) => { if (!response.ok) { @@ -26,7 +49,7 @@ const handleFileDownload = async (response, filename) => { const useWorldBookStore = create((set, get) => ({ // 状态 worldBooks: [], // 世界书列表 - globalWorldBooks: [], // 全局世界书列表 + globalWorldBooks: loadGlobalWorldBooks(), // 从 LocalStorage 初始化全局世界书列表 currentWorldBook: null, // 当前选中的世界书 currentEntries: [], // 当前世界书的条目列表 currentEntry: null, // 当前选中的条目 @@ -58,14 +81,7 @@ const useWorldBookStore = create((set, get) => ({ toggleGlobalWorldBook: async (name, isGlobal) => { set({ loading: true, error: null, success: false }); try { - // 先获取当前世界书的信息 - const currentBook = get().worldBooks.find(wb => wb.name === name); - if (!currentBook) { - throw new Error(`世界书 "${name}" 不存在`); - } - const formData = new FormData(); - formData.append('description', currentBook.description); formData.append('is_global', isGlobal); const response = await fetch(`/api/worldbooks/${name}`, { @@ -100,6 +116,9 @@ const useWorldBookStore = create((set, get) => ({ } } + // 保存到 LocalStorage + saveGlobalWorldBooks(updatedGlobalBooks); + return { loading: false, worldBooks: updatedWorldBooks, @@ -108,7 +127,6 @@ const useWorldBookStore = create((set, get) => ({ ? data : state.currentWorldBook, success: true, - message: isGlobal ? `已将 "${name}" 设置为全局世界书` : `已取消 "${name}" 的全局世界书状态` }; }); @@ -130,8 +148,8 @@ const useWorldBookStore = create((set, get) => ({ const response = await fetch(`/api/worldbooks/`); const data = await handleResponse(response); - // 筛选出全局世界书 - const globalBooks = data.filter(book => book.is_global); + // 从 LocalStorage 获取全局世界书列表 + const globalBooks = loadGlobalWorldBooks(); set({ loading: false, @@ -171,12 +189,11 @@ const useWorldBookStore = create((set, get) => ({ }, // 异步操作:创建世界书 - createWorldBook: async ({ name, description, is_global, file }) => { + createWorldBook: async ({ name, is_global, file }) => { set({ loading: true, error: null, success: false }); try { const formData = new FormData(); formData.append('name', name); - formData.append('description', description || ''); if (is_global !== undefined) { formData.append('is_global', is_global); } @@ -193,9 +210,13 @@ const useWorldBookStore = create((set, get) => ({ set(state => { const newWorldBooks = [...state.worldBooks, data]; - const newGlobalBooks = data.is_global - ? [...state.globalWorldBooks, data] - : state.globalWorldBooks; + let newGlobalBooks = [...state.globalWorldBooks]; + + // 如果是世界书被标记为全局,添加到全局列表 + if (data.is_global) { + newGlobalBooks = [...newGlobalBooks, data]; + saveGlobalWorldBooks(newGlobalBooks); + } return { loading: false, @@ -219,13 +240,10 @@ const useWorldBookStore = create((set, get) => ({ }, // 异步操作:更新世界书 - updateWorldBook: async ({ name, description, is_global, file }) => { + updateWorldBook: async ({ name, is_global, file }) => { set({ loading: true, error: null, success: false }); try { const formData = new FormData(); - if (description !== undefined) { - formData.append('description', description); - } if (is_global !== undefined) { formData.append('is_global', is_global); } @@ -265,6 +283,9 @@ const useWorldBookStore = create((set, get) => ({ } } + // 保存到 LocalStorage + saveGlobalWorldBooks(updatedGlobalBooks); + return { loading: false, worldBooks: updatedWorldBooks, @@ -302,6 +323,9 @@ const useWorldBookStore = create((set, get) => ({ const filteredWorldBooks = state.worldBooks.filter(wb => wb.name !== name); const filteredGlobalBooks = state.globalWorldBooks.filter(wb => wb.name !== name); + // 保存到 LocalStorage + saveGlobalWorldBooks(filteredGlobalBooks); + return { loading: false, worldBooks: filteredWorldBooks, @@ -387,144 +411,142 @@ const useWorldBookStore = create((set, get) => ({ } }, - // 异步操作:创建世界书条目 - createWorldBookEntry: async (name, entryData) => { - set({ loading: true, error: null, success: false }); - try { - // 处理触发配置数据 - const processedEntryData = { ...entryData }; - if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) { - // 创建新的触发配置对象 - const triggerConfig = { - triggers: {} - }; + // 异步操作:创建世界书条目 + createWorldBookEntry: async (name, entryData) => { + set({ loading: true, error: null, success: false }); + try { + // 处理触发配置数据 + const processedEntryData = { ...entryData }; + if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) { + // 创建新的触发配置对象 + const triggerConfig = { + triggers: {} + }; - // 处理每个触发策略 - for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) { - if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) { - triggerConfig.triggers[strategy] = [ - triggerInfo[0], // 是否启用 - triggerInfo[1] // 配置对象 - ]; - } else { - // 如果格式不正确,设置为不启用 - triggerConfig.triggers[strategy] = [false, null]; - } + // 处理每个触发策略 + for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) { + if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) { + triggerConfig.triggers[strategy] = [ + triggerInfo[0], // 是否启用 + triggerInfo[1] // 配置对象 + ]; + } else { + // 如果格式不正确,设置为不启用 + triggerConfig.triggers[strategy] = [false, null]; } - - processedEntryData.trigger_config = triggerConfig; } - const response = await fetch(`/api/worldbooks/${name}/entries`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(processedEntryData) - }); + processedEntryData.trigger_config = triggerConfig; + } - const data = await handleResponse(response); + const response = await fetch(`/api/worldbooks/${name}/entries`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(processedEntryData) + }); - set(state => { - if (state.currentWorldBook?.name === name) { - return { - loading: false, - currentEntries: [...state.currentEntries, data], - success: true, - message: '条目创建成功' - }; - } + const data = await handleResponse(response); + + set(state => { + if (state.currentWorldBook?.name === name) { return { loading: false, + currentEntries: [...state.currentEntries, data], success: true, message: '条目创建成功' }; - }); - - return data; - } catch (error) { - set({ + } + return { loading: false, - error: error.message, - success: false - }); - throw error; - } - }, + success: true, + message: '条目创建成功' + }; + }); + return data; + } catch (error) { + set({ + loading: false, + error: error.message, + success: false + }); + throw error; + } + }, - // 异步操作:更新世界书条目 - updateWorldBookEntry: async (name, uid, entryData) => { - set({ loading: true, error: null, success: false }); - try { - // 处理触发配置数据 - const processedEntryData = { ...entryData }; - if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) { - // 创建新的触发配置对象 - const triggerConfig = { - triggers: {} - }; + // 异步操作:更新世界书条目 + updateWorldBookEntry: async (name, uid, entryData) => { + set({ loading: true, error: null, success: false }); + try { + // 处理触发配置数据 + const processedEntryData = { ...entryData }; + if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) { + // 创建新的触发配置对象 + const triggerConfig = { + triggers: {} + }; - // 处理每个触发策略 - for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) { - if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) { - triggerConfig.triggers[strategy] = [ - triggerInfo[0], // 是否启用 - triggerInfo[1] // 配置对象 - ]; - } else { - // 如果格式不正确,设置为不启用 - triggerConfig.triggers[strategy] = [false, null]; - } + // 处理每个触发策略 + for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) { + if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) { + triggerConfig.triggers[strategy] = [ + triggerInfo[0], // 是否启用 + triggerInfo[1] // 配置对象 + ]; + } else { + // 如果格式不正确,设置为不启用 + triggerConfig.triggers[strategy] = [false, null]; } - - processedEntryData.trigger_config = triggerConfig; } - const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(processedEntryData) - }); + processedEntryData.trigger_config = triggerConfig; + } - const data = await handleResponse(response); + const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(processedEntryData) + }); - set(state => { - if (state.currentWorldBook?.name === name) { - const updatedEntries = state.currentEntries.map(entry => - entry.uid === data.uid ? data : entry - ); + const data = await handleResponse(response); + + set(state => { + if (state.currentWorldBook?.name === name) { + const updatedEntries = state.currentEntries.map(entry => + entry.uid === data.uid ? data : entry + ); - return { - loading: false, - currentEntries: updatedEntries, - currentEntry: state.currentEntry?.uid === data.uid - ? data - : state.currentEntry, - success: true, - message: '条目更新成功' - }; - } return { loading: false, + currentEntries: updatedEntries, + currentEntry: state.currentEntry?.uid === data.uid + ? data + : state.currentEntry, success: true, message: '条目更新成功' }; - }); - - return data; - } catch (error) { - set({ + } + return { loading: false, - error: error.message, - success: false - }); - throw error; - } - }, + success: true, + message: '条目更新成功' + }; + }); + return data; + } catch (error) { + set({ + loading: false, + error: error.message, + success: false + }); + throw error; + } + }, // 异步操作:删除世界书条目 deleteWorldBookEntry: async (name, uid) => { @@ -613,6 +635,9 @@ const useWorldBookStore = create((set, get) => ({ } } + // 保存到 LocalStorage + saveGlobalWorldBooks(updatedGlobalBooks); + return { loading: false, worldBooks: updatedWorldBooks, @@ -668,4 +693,4 @@ const useWorldBookStore = create((set, get) => ({ }, })); -export default useWorldBookStore; +export default useWorldBookStore; \ No newline at end of file diff --git a/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx b/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx index 1c6f4d2..536a5a5 100644 --- a/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx +++ b/frontend-react/src/components/SideBarLeft/tab/WorldBook.jsx @@ -29,70 +29,42 @@ const WorldBook = () => { clearSuccess, } = useWorldBookStore(); - const [newEntry, setNewEntry] = useState({ - uid: 0, - key: [], - keysecondary: [], - content: '', - comment: '', - constant: false, - position: 0, - order: 100, - depth:4, - selective: true, - selectiveLogic: 0, - probability: 100, - useProbability: false, - role: 0, - caseSensitive: false, - matchWholeWords: false, - useGroupScoring: false, - group: '', - groupOverride: false, - groupWeight: 100, - excludeRecursion: true, - preventRecursion: true, - delayUntilRecursion: false, - disable: false, - ignoreBudget: false, - outletName: '', - automationId: '', - sticky: 0, - cooldown: 0, - delay: 0, - triggers: [], - displayIndex: 0, - vectorized: false, - // 新的触发配置结构 - trigger_config: { - triggers: { - constant: [true, null], - keyword: [false, { - key: [], - keysecondary: [], - selective: true, - selectiveLogic: 0, - matchWholeWords: false, - caseSensitive: false - }], - rag: [false, { - threshold: 0.75, - top_k: 5, - query_template: null - }], - condition: [false, { - variable_a: '', - operator: '=', - variable_b: '' - }] - } + const [newEntry, setNewEntry] = useState({ + uid: 0, + content: '', + comment: '', + position: 0, + order: 100, + depth: 4, + role: 0, + trigger_config: { + triggers: { + constant: [true, null], + keyword: [false, { + key: [], + keysecondary: [], + selective: true, + selectiveLogic: 0, + matchWholeWords: false, + caseSensitive: false + }], + rag: [false, { + threshold: 0.75, + top_k: 5, + query_template: null + }], + condition: [false, { + variable_a: '', + operator: '=', + variable_b: '' + }] } - }); - + } + }); const [showEditPanel, setShowEditPanel] = useState(false); const [showWorldBookDropdown, setShowWorldBookDropdown] = useState(false); - const [showGlobalDropdown, setShowGlobalDropdown] = useState(false); + const [activeTriggerStrategy, setActiveTriggerStrategy] = useState('constant'); useEffect(() => { fetchWorldBooks(); @@ -112,13 +84,26 @@ const WorldBook = () => { } }, [error, clearError]); + useEffect(() => { + if (currentEntry && currentEntry.trigger_config) { + const triggers = currentEntry.trigger_config.triggers; + if (triggers.constant[0]) { + setActiveTriggerStrategy('constant'); + } else if (triggers.keyword[0]) { + setActiveTriggerStrategy('keyword'); + } else if (triggers.rag[0]) { + setActiveTriggerStrategy('rag'); + } else if (triggers.condition[0]) { + setActiveTriggerStrategy('condition'); + } + } + }, [currentEntry]); + const handleCreateWorldBook = async () => { const name = prompt('请输入世界书名称:'); - const description = prompt('请输入世界书描述(可选):') || ''; if (name) { try { - await createWorldBook({ name, description }); - // 创建成功后自动选择新创建的世界书 + await createWorldBook({ name }); const newBook = worldBooks.find(wb => wb.name === name); if (newBook) { handleSelectWorldBook(newBook); @@ -147,134 +132,85 @@ const WorldBook = () => { } }; - const handleAddEntry = async () => { - if (!currentWorldBook) return; + const handleAddEntry = async () => { + if (!currentWorldBook) return; - // 生成新的UID - const maxUid = currentEntries.reduce((max, entry) => Math.max(max, entry.uid), 0); - const newUid = maxUid + 1; + const maxUid = currentEntries.reduce((max, entry) => Math.max(max, entry.uid), 0); + const newUid = maxUid + 1; - // 处理触发配置数据 - const triggerConfig = { - triggers: { - constant: [newEntry.constant, null], - keyword: [!newEntry.constant && newEntry.key.length > 0, { - key: newEntry.key, - keysecondary: newEntry.keysecondary, - selective: newEntry.selective, - selectiveLogic: newEntry.selectiveLogic, - matchWholeWords: newEntry.matchWholeWords, - caseSensitive: newEntry.caseSensitive - }], - rag: [false, { - threshold: newEntry.rag_threshold || 0.75, - top_k: 5, - query_template: null - }], - condition: [false, { - variable_a: '', - operator: '=', - variable_b: '' - }] - } - }; - - const entryData = { - uid: newUid, - content: newEntry.content, - comment: newEntry.comment, - position: newEntry.position, - order: newEntry.order, - depth: newEntry.depth, - role: newEntry.role, - disable: newEntry.disable, - ignoreBudget: newEntry.ignoreBudget, - outletName: newEntry.outletName, - automationId: newEntry.automationId, - sticky: newEntry.sticky, - cooldown: newEntry.cooldown, - delay: newEntry.delay, - triggers: newEntry.triggers, - displayIndex: newEntry.displayIndex, - vectorized: newEntry.vectorized, - useGroupScoring: newEntry.useGroupScoring, - group: newEntry.group, - groupOverride: newEntry.groupOverride, - groupWeight: newEntry.groupWeight, - excludeRecursion: newEntry.excludeRecursion, - preventRecursion: newEntry.preventRecursion, - delayUntilRecursion: newEntry.delayUntilRecursion, - probability: newEntry.probability, - useProbability: newEntry.useProbability, - trigger_config: triggerConfig - }; - - try { - await createWorldBookEntry(currentWorldBook.name, entryData); - // 重置新条目表单 - setNewEntry({ - uid: 0, - key: [], - keysecondary: [], - content: '', - comment: '', - constant: false, - position: 0, - order: 100, - depth:4, - selective: true, - selectiveLogic: 0, - probability: 100, - useProbability: false, - role: 0, - caseSensitive: false, - matchWholeWords: false, - useGroupScoring: false, - group: '', - groupOverride: false, - groupWeight: 100, - excludeRecursion: true, - preventRecursion: true, - delayUntilRecursion: false, - disable: false, - ignoreBudget: false, - outletName: '', - automationId: '', - sticky: 0, - cooldown: 0, - delay: 0, - triggers: [], - displayIndex: 0, - vectorized: false, - trigger_config: { - triggers: { - constant: [true, null], - keyword: [false, { - key: [], - keysecondary: [], - selective: true, - selectiveLogic: 0, - matchWholeWords: false, - caseSensitive: false - }], - rag: [false, { - threshold: 0.75, - top_k: 5, - query_template: null - }], - condition: [false, { - variable_a: '', - operator: '=', - variable_b: '' - }] - } - } - }); - } catch (err) { - console.error('添加条目失败:', err); + const triggerConfig = { + triggers: { + constant: [newEntry.trigger_config.triggers.constant[0], null], + keyword: [!newEntry.trigger_config.triggers.constant[0] && newEntry.trigger_config.triggers.keyword[1].key.length > 0, { + key: newEntry.trigger_config.triggers.keyword[1].key, + keysecondary: newEntry.trigger_config.triggers.keyword[1].keysecondary, + selective: newEntry.trigger_config.triggers.keyword[1].selective, + selectiveLogic: newEntry.trigger_config.triggers.keyword[1].selectiveLogic, + matchWholeWords: newEntry.trigger_config.triggers.keyword[1].matchWholeWords, + caseSensitive: newEntry.trigger_config.triggers.keyword[1].caseSensitive + }], + rag: [false, { + threshold: 0.75, + top_k: 5, + query_template: null + }], + condition: [false, { + variable_a: '', + operator: '=', + variable_b: '' + }] } }; + const entryData = { + uid: newUid, + content: newEntry.content, + comment: newEntry.comment, + position: newEntry.position, + order: newEntry.order, + depth: newEntry.depth, + role: newEntry.role, + trigger_config: triggerConfig + }; + + try { + await createWorldBookEntry(currentWorldBook.name, entryData); + setNewEntry({ + uid: 0, + content: '', + comment: '', + position: 0, + order: 100, + depth: 4, + role: 0, + trigger_config: { + triggers: { + constant: [true, null], + keyword: [false, { + key: [], + keysecondary: [], + selective: true, + selectiveLogic: 0, + matchWholeWords: false, + caseSensitive: false + }], + rag: [false, { + threshold: 0.75, + top_k: 5, + query_template: null + }], + condition: [false, { + variable_a: '', + operator: '=', + variable_b: '' + }] + } + } + }); + } catch (err) { + console.error('添加条目失败:', err); + } + }; const handleEntryClick = (entry) => { setCurrentEntry(entry); @@ -293,6 +229,58 @@ const WorldBook = () => { } }; + const handleTriggerStrategyChange = async (strategy) => { + if (!currentEntry || !currentWorldBook) return; + + setActiveTriggerStrategy(strategy); + + const updatedTriggers = { + constant: [false, null], + keyword: [false, { + key: [], + keysecondary: [], + selective: true, + selectiveLogic: 0, + matchWholeWords: false, + caseSensitive: false + }], + rag: [false, { + threshold: 0.75, + top_k: 5, + query_template: null + }], + condition: [false, { + variable_a: '', + operator: '=', + variable_b: '' + }] + }; + + if (strategy !== 'constant') { + updatedTriggers[strategy][0] = true; + } else { + updatedTriggers.constant[0] = true; + } + + const updatedTriggerConfig = { + ...currentEntry.trigger_config, + triggers: updatedTriggers + }; + + try { + await updateWorldBookEntry(currentWorldBook.name, currentEntry.uid, { + ...currentEntry, + trigger_config: updatedTriggerConfig + }); + setCurrentEntry({ + ...currentEntry, + trigger_config: updatedTriggerConfig + }); + } catch (err) { + console.error('更新触发策略失败:', err); + } + }; + const handleDeleteEntry = async () => { if (!currentEntry || !currentWorldBook) return; @@ -334,7 +322,6 @@ const WorldBook = () => { try { await useWorldBookStore.getState().importWorldBook(name, file); await fetchWorldBooks(); - // 导入成功后选择新导入的世界书 const importedBook = worldBooks.find(wb => wb.name === name); if (importedBook) { handleSelectWorldBook(importedBook); @@ -356,11 +343,22 @@ const WorldBook = () => { } }; + const getPositionInfo = (position) => { + const positions = { + 0: { label: '角色定义之后', weight: '高', desc: 'AI读完人设紧接着就读到这里,非常适合补充角色的详细设定、性格细节或特殊规则' }, + 1: { label: '角色定义之前', weight: '中', desc: '在角色卡内容的最上方,通常用于定义角色的基础背景,让人设部分来解释这些背景' }, + 2: { label: '示例对话之前', weight: '低', desc: '在对话示例的最上方' }, + 3: { label: '示例对话之后', weight: '低', desc: '用于在对话开始前提供最后的上下文补充' }, + 4: { label: '系统提示/作者注释', weight: '极高', desc: 'AI对最近看到的信息记忆最清晰,适合动态信息、当前场景描述或临时规则' }, + 5: { label: '作为系统消息', weight: '最高', desc: '强制作为System Prompt插入,通常用于强制指令' } + }; + return positions[position] || positions[0]; + }; + return (