""" World Book Service 世界书服务层 - 处理世界书及条目的 CRUD 操作 """ import json import os import uuid from pathlib import Path from typing import List, Dict, Any, Optional from datetime import datetime from models.internal import WorldInfo, WorldInfoEntry, ActivationType from models.converters import WorldBookConverter from core.config import settings class WorldBookService: """世界书服务类""" @staticmethod def _get_worldbook_path(name: str) -> Path: """获取世界书文件路径""" return settings.WORLDBOOKS_PATH / f"{name}.json" @staticmethod def _load_worldbook(name: str) -> Optional[Dict[str, Any]]: """加载世界书 JSON 文件""" path = WorldBookService._get_worldbook_path(name) if not path.exists(): return None try: with open(path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: raise ValueError(f"Failed to load worldbook '{name}': {str(e)}") @staticmethod def _save_worldbook(name: str, data: Dict[str, Any]): """保存世界书到 JSON 文件""" path = WorldBookService._get_worldbook_path(name) try: with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) except Exception as e: raise ValueError(f"Failed to save worldbook '{name}': {str(e)}") @staticmethod def list_worldbooks() -> List[Dict[str, Any]]: """ 获取所有世界书的列表(仅基本信息) Returns: 世界书列表,每个包含 name, description, entries_count 等 """ worldbooks = [] for json_file in settings.WORLDBOOKS_PATH.glob("*.json"): try: with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) # 内部格式:entries 是列表 entries = data.get("entries", []) entries_count = len(entries) if isinstance(entries, list) else 0 worldbooks.append({ "name": data.get("name", json_file.stem), "description": data.get("description", ""), "entries_count": entries_count, "createdAt": data.get("createdAt", 0), "updatedAt": data.get("updatedAt", 0) }) except Exception as e: print(f"Error loading worldbook {json_file.name}: {e}") continue # 按更新时间排序 worldbooks.sort(key=lambda x: x.get("updatedAt", 0), reverse=True) return worldbooks @staticmethod def get_worldbook(name: str) -> Dict[str, Any]: """ 获取指定世界书的完整数据 Args: name: 世界书名称 Returns: 世界书完整数据 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") return data @staticmethod def create_worldbook(name: str, description: str = "") -> Dict[str, Any]: """ 创建新世界书 Args: name: 世界书名称 description: 世界书描述 Returns: 创建的世界书数据 """ # 检查是否已存在 if WorldBookService._get_worldbook_path(name).exists(): raise ValueError(f"Worldbook '{name}' already exists") now = int(datetime.now().timestamp()) worldbook_data = { "id": str(uuid.uuid4()), "name": name, "description": description, "entries": [], "createdAt": now, "updatedAt": now, "version": 1 } WorldBookService._save_worldbook(name, worldbook_data) return worldbook_data @staticmethod def update_worldbook(name: str, description: Optional[str] = None) -> Dict[str, Any]: """ 更新世界书基本信息 Args: name: 世界书名称 description: 新的描述(可选) Returns: 更新后的世界书数据 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") if description is not None: data["description"] = description data["updatedAt"] = int(datetime.now().timestamp()) WorldBookService._save_worldbook(name, data) return data @staticmethod def delete_worldbook(name: str) -> bool: """ 删除世界书 Args: name: 世界书名称 Returns: 是否删除成功 """ path = WorldBookService._get_worldbook_path(name) if not path.exists(): raise FileNotFoundError(f"Worldbook '{name}' not found") path.unlink() return True @staticmethod def list_entries(name: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]: """ 获取世界书的条目列表(支持分页) Args: name: 世界书名称 page: 页码,从1开始 page_size: 每页数量,默认20 Returns: 包含条目列表和分页信息的字典 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") # 内部格式:entries 是列表 all_entries = data.get("entries", []) if not isinstance(all_entries, list): all_entries = [] total = len(all_entries) # 计算分页 start_idx = (page - 1) * page_size end_idx = start_idx + page_size paginated_entries = all_entries[start_idx:end_idx] return { "entries": paginated_entries, "total": total, "page": page, "page_size": page_size, "total_pages": (total + page_size - 1) // page_size # 向上取整 } @staticmethod def get_entry(name: str, uid: str) -> Dict[str, Any]: """ 获取世界书的指定条目 Args: name: 世界书名称 uid: 条目 UID Returns: 条目数据 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") # 内部格式:entries 是列表 entries = data.get("entries", []) if not isinstance(entries, list): entries = [] for entry in entries: if entry.get("uid") == uid or str(entry.get("uid")) == uid: return entry raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'") @staticmethod def append_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]: """ 在世界书中追加条目(规范化后写入,与 Chat 侧条目格式一致)。 Args: name: 世界书名称(文件名,不含 .json) entry_data: 条目字段(content、comment、activationType、position 等) Returns: 写入后的规范化条目 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") if not isinstance(data.get("entries"), list): data["entries"] = [] normalized = WorldBookConverter.normalize_entry(entry_data) data["entries"].append(normalized) now = int(datetime.now().timestamp()) data["updatedAt"] = now WorldBookService._save_worldbook(name, data) return normalized @staticmethod def create_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]: """ 在世界书中创建新条目 Args: name: 世界书名称 entry_data: 条目数据(不包含 uid, createdAt, updatedAt) Returns: 创建的条目数据 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") # 生成 UID 和时间戳 now = int(datetime.now().timestamp()) new_entry = { "uid": str(uuid.uuid4()), "key": entry_data.get("key", []), "keysecondary": entry_data.get("keysecondary", []), "content": entry_data.get("content", ""), "activationType": entry_data.get("activationType", ActivationType.KEYWORD.value), "logicExpression": entry_data.get("logicExpression"), "ragConfig": entry_data.get("ragConfig"), "order": entry_data.get("order", 0), "position": entry_data.get("position", "after_char"), "depth": entry_data.get("depth"), "probability": entry_data.get("probability", 100), "group": entry_data.get("group", []), "disable": entry_data.get("disable", False), "createdAt": now, "updatedAt": now } data["entries"].append(new_entry) data["updatedAt"] = now WorldBookService._save_worldbook(name, data) return new_entry @staticmethod def update_entry(name: str, uid: str, entry_data: Dict[str, Any]) -> Dict[str, Any]: """ 更新世界书的指定条目 Args: name: 世界书名称 uid: 条目 UID entry_data: 更新的字段 Returns: 更新后的条目数据 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") for i, entry in enumerate(data.get("entries", [])): if entry.get("uid") == uid: # 更新字段 for key, value in entry_data.items(): if key not in ["uid", "createdAt"]: # 不修改 UID 和创建时间 entry[key] = value # 更新时间戳 entry["updatedAt"] = int(datetime.now().timestamp()) data["entries"][i] = entry data["updatedAt"] = entry["updatedAt"] WorldBookService._save_worldbook(name, data) return entry raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'") @staticmethod def delete_entry(name: str, uid: str) -> bool: """ 删除世界书的指定条目 Args: name: 世界书名称 uid: 条目 UID Returns: 是否删除成功 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") original_length = len(data.get("entries", [])) data["entries"] = [e for e in data.get("entries", []) if e.get("uid") != uid] if len(data["entries"]) == original_length: raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'") data["updatedAt"] = int(datetime.now().timestamp()) WorldBookService._save_worldbook(name, data) return True @staticmethod def import_from_sillytavern(name: str, st_data: Dict[str, Any]) -> Dict[str, Any]: """ 从 SillyTavern 格式导入世界书 Args: name: 世界书名称 st_data: SillyTavern 格式的世界书数据 Returns: 转换后的内部格式世界书数据 """ # 使用转换器进行转换 worldbook_data = WorldBookConverter.st_to_internal(st_data, name) # 保存到文件 WorldBookService._save_worldbook(name, worldbook_data) return worldbook_data @staticmethod def import_internal_format(name: str, internal_data: Dict[str, Any]) -> Dict[str, Any]: """ 直接导入内部格式的世界书(无需转换) Args: name: 世界书名称 internal_data: 内部格式的世界书数据 Returns: 内部格式世界书数据 """ # 确保包含必要的字段 if "name" not in internal_data: internal_data["name"] = name # 规范化所有条目,确保有 trigger_config if "entries" in internal_data and isinstance(internal_data["entries"], list): normalized_entries = [] for entry in internal_data["entries"]: if isinstance(entry, dict): normalized_entry = WorldBookConverter.normalize_entry(entry) normalized_entries.append(normalized_entry) internal_data["entries"] = normalized_entries # 保存文件 WorldBookService._save_worldbook(name, internal_data) return internal_data @staticmethod def export_to_sillytavern(name: str) -> Dict[str, Any]: """ 导出为 SillyTavern 格式 Args: name: 世界书名称 Returns: SillyTavern 格式的世界书数据 """ data = WorldBookService._load_worldbook(name) if not data: raise FileNotFoundError(f"Worldbook '{name}' not found") # 使用转换器进行转换 st_data = WorldBookConverter.internal_to_st(data) return st_data # 全局实例 worldbook_service = WorldBookService()