Files

330 lines
11 KiB
Python

"""
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