330 lines
11 KiB
Python
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
|