""" Preset Service 预设服务层 - 处理预设的 CRUD 操作 """ import json import os from pathlib import Path from typing import List, Dict, Any, Optional from datetime import datetime from core.config import settings class PresetService: """预设服务类""" @staticmethod def _extract_preset_name_from_filename(filename: str) -> str: """ 从文件名提取预设名称,去掉时间戳和文件后缀 Args: filename: 文件名(不含路径) Returns: 清理后的预设名称 Examples: "Default.json" -> "Default" "MyPreset_1234567890.json" -> "MyPreset" "Test_1714567890123.json" -> "Test" """ # 去掉 .json 后缀 name = filename.replace('.json', '') # 去掉末尾的时间戳(下划线+数字组合) # 匹配模式:_后面跟着10-13位数字(Unix时间戳) import re name = re.sub(r'_\d{10,13}$', '', name) return name @staticmethod def _get_preset_path(name: str) -> Path: """获取预设文件路径""" return settings.PRESET_PATH / f"{name}.json" @staticmethod def _load_preset(name: str) -> Optional[Dict[str, Any]]: """加载预设 JSON 文件""" path = PresetService._get_preset_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 preset '{name}': {str(e)}") @staticmethod def _save_preset(name: str, data: Dict[str, Any]): """保存预设到 JSON 文件""" path = PresetService._get_preset_path(name) try: # 确保 prompts 数组和 prompt_order 的顺序一致 if "prompts" in data and "prompt_order" in data: prompts = data["prompts"] prompt_order = data.get("prompt_order", [{}])[0].get("order", []) if prompts and prompt_order: # 创建 identifier 到 prompt 的映射 prompt_map = {prompt["identifier"]: prompt for prompt in prompts} # 按照 prompt_order 的顺序重新排列 prompts reordered_prompts = [] for order_item in prompt_order: identifier = order_item.get("identifier") if identifier and identifier in prompt_map: reordered_prompts.append(prompt_map[identifier]) # 添加 prompt_order 中不存在的 prompts(如果有) existing_identifiers = {item.get("identifier") for item in prompt_order} for prompt in prompts: if prompt["identifier"] not in existing_identifiers: reordered_prompts.append(prompt) data["prompts"] = reordered_prompts 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 preset '{name}': {str(e)}") @staticmethod def list_presets() -> List[Dict[str, Any]]: """ 获取所有预设的列表(仅基本信息) Returns: 预设列表,每个包含 name, description, component_count, temperature 等 """ presets = [] for json_file in settings.PRESET_PATH.glob("*.json"): try: with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) # 计算组件数量 - 支持 SillyTavern 格式 (prompts) 和内部格式 (entries) prompts = data.get("prompts", []) entries = data.get("entries", []) component_count = len(prompts) if prompts else len(entries) # 提取温度参数 - 使用 SillyTavern 标准字段名 temperature = data.get("temperature", 1.0) # 从文件名提取预设名称(去掉时间戳和后缀) preset_name = PresetService._extract_preset_name_from_filename(json_file.name) preset_info = { "name": preset_name, "description": data.get("description", ""), "component_count": component_count, "temperature": temperature } presets.append(preset_info) except Exception as e: print(f"Error loading preset {json_file.name}: {e}") continue # 按名称排序 presets.sort(key=lambda x: x.get("name", "")) return presets @staticmethod def get_preset(name: str) -> Dict[str, Any]: """ 获取指定预设的完整数据 Args: name: 预设名称 Returns: 预设完整数据 """ data = PresetService._load_preset(name) if not data: raise FileNotFoundError(f"Preset '{name}' not found") return data @staticmethod def create_preset(name: str, preset_data: Dict[str, Any]) -> Dict[str, Any]: """ 创建新预设 Args: name: 预设名称 preset_data: 预设数据 Returns: 创建的预设数据 """ # 检查是否已存在 if PresetService._get_preset_path(name).exists(): raise ValueError(f"Preset '{name}' already exists") # 确保有必要的字段 if "name" not in preset_data: preset_data["name"] = name # 添加时间戳 now = int(datetime.now().timestamp()) if "createdAt" not in preset_data: preset_data["createdAt"] = now if "updatedAt" not in preset_data: preset_data["updatedAt"] = now PresetService._save_preset(name, preset_data) return preset_data @staticmethod def update_preset(name: str, update_data: Dict[str, Any]) -> Dict[str, Any]: """ 更新预设 Args: name: 预设名称 update_data: 要更新的数据 Returns: 更新后的预设数据 """ data = PresetService._load_preset(name) if not data: raise FileNotFoundError(f"Preset '{name}' not found") # 更新字段 for key, value in update_data.items(): if key not in ["name", "createdAt"]: # 不允许修改名称和创建时间 data[key] = value # 更新时间戳 data["updatedAt"] = int(datetime.now().timestamp()) PresetService._save_preset(name, data) return data @staticmethod def delete_preset(name: str) -> bool: """ 删除预设 Args: name: 预设名称 Returns: 是否删除成功 """ path = PresetService._get_preset_path(name) if not path.exists(): raise FileNotFoundError(f"Preset '{name}' not found") path.unlink() return True @staticmethod def rename_preset(old_name: str, new_name: str) -> Dict[str, Any]: """ 重命名预设(同时修改文件名和内部 name 字段) Args: old_name: 原预设名称 new_name: 新预设名称 Returns: 更新后的预设数据 """ # 检查原预设是否存在 old_path = PresetService._get_preset_path(old_name) if not old_path.exists(): raise FileNotFoundError(f"Preset '{old_name}' not found") # 检查新名称是否已存在 new_path = PresetService._get_preset_path(new_name) if new_path.exists() and old_name != new_name: raise ValueError(f"Preset '{new_name}' already exists") # 加载原预设数据 data = PresetService._load_preset(old_name) if not data: raise FileNotFoundError(f"Preset '{old_name}' not found") # 更新内部的 name 字段 data["name"] = new_name # 更新时间戳 data["updatedAt"] = int(datetime.now().timestamp()) # 保存到新文件 PresetService._save_preset(new_name, data) # 删除旧文件(如果名称不同) if old_name != new_name: old_path.unlink() return data @staticmethod def reorder_components(name: str, component_order: List[str]) -> Dict[str, Any]: """ 重新排序预设组件 - 支持 SillyTavern 标准格式 Args: name: 预设名称 component_order: 组件 identifier 列表,按新顺序排列 Returns: 更新后的预设数据 """ data = PresetService._load_preset(name) if not data: raise FileNotFoundError(f"Preset '{name}' not found") # 支持 SillyTavern 格式的 prompts if "prompts" in data and isinstance(data["prompts"], list): # 创建 identifier 到 prompt 的映射 prompt_map = {prompt["identifier"]: prompt for prompt in data["prompts"]} # 按新顺序重新排列 reordered_prompts = [] for identifier in component_order: if identifier in prompt_map: reordered_prompts.append(prompt_map[identifier]) data["prompts"] = reordered_prompts # 更新 prompt_order if "prompt_order" in data and isinstance(data["prompt_order"], list) and len(data["prompt_order"]) > 0: data["prompt_order"][0]["order"] = [ {"identifier": identifier, "enabled": True} for identifier in component_order if identifier in prompt_map ] # 也支持内部格式的 entries(向后兼容) elif "entries" in data and isinstance(data["entries"], list): # 创建 identifier 到 entry 的映射 entry_map = {entry["identifier"]: entry for entry in data["entries"]} # 按新顺序重新排列 reordered_entries = [] for identifier in component_order: if identifier in entry_map: reordered_entries.append(entry_map[identifier]) # 更新 order 字段 for index, entry in enumerate(reordered_entries): entry["order"] = index data["entries"] = reordered_entries # 更新时间戳 data["updatedAt"] = int(datetime.now().timestamp()) PresetService._save_preset(name, data) return data