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
|