完成大量美化,zustand迁移,动态表格修复
This commit is contained in:
@@ -3,9 +3,7 @@
|
||||
|
||||
包含项目的核心业务逻辑,协调 Models、Utils 和 LLM 组件。
|
||||
"""
|
||||
from .prompt_assembler import PromptAssembler, PromptConfig
|
||||
# 注意:不在这里自动导入模块,避免循环依赖和缺失依赖问题
|
||||
# 需要使用时请显式导入,例如:from services.preset_service import PresetService
|
||||
|
||||
__all__ = [
|
||||
'PromptAssembler',
|
||||
'PromptConfig',
|
||||
]
|
||||
__all__ = []
|
||||
|
||||
@@ -50,6 +50,7 @@ class CharacterCardConverter:
|
||||
first_mes=data.get('first_mes', ''),
|
||||
mes_example=data.get('mes_example', ''),
|
||||
categories=[], # ST没有categories
|
||||
tableHeaders=[], # ST没有tableHeaders
|
||||
worldInfoId=extensions.get('world'),
|
||||
outputSchema=None, # ST不支持结构化输出
|
||||
avatarPath=avatar_path,
|
||||
|
||||
@@ -95,11 +95,11 @@ class CharacterService:
|
||||
first_mes=data.get('first_mes', ''),
|
||||
mes_example=data.get('mes_example', ''),
|
||||
categories=data.get('categories', []),
|
||||
tags=data.get('tags', []), # ✅ 使用标签数组
|
||||
worldInfoId=data.get('worldInfoId'),
|
||||
outputSchema=data.get('outputSchema'),
|
||||
avatarPath=avatar_path,
|
||||
alternate_greetings=data.get('alternate_greetings', []),
|
||||
tags=data.get('tags', []),
|
||||
createdAt=data.get('createdAt', int(datetime.now().timestamp())),
|
||||
updatedAt=data.get('updatedAt', int(datetime.now().timestamp())),
|
||||
lastChatAt=last_chat_at,
|
||||
|
||||
@@ -156,6 +156,7 @@ class ChatService:
|
||||
messages.append(msg_data)
|
||||
|
||||
return {
|
||||
"header": header, # 完整的 header,包含 tableHeaders, tableDefaults, tableData
|
||||
"metadata": {
|
||||
"user_name": header.get("user_name", "User"),
|
||||
"character_name": header.get("character_name", ""),
|
||||
@@ -192,6 +193,21 @@ class ChatService:
|
||||
# 创建角色目录
|
||||
chat_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 尝试从角色卡获取 tags(关键字列表)
|
||||
tags = []
|
||||
|
||||
try:
|
||||
character_file = Path("data/characters") / role_name / "character.json"
|
||||
if character_file.exists():
|
||||
with open(character_file, 'r', encoding='utf-8') as f:
|
||||
character_data = json.load(f)
|
||||
|
||||
tags = character_data.get('tags', [])
|
||||
|
||||
logger.info(f"从角色卡 {role_name} 继承标签: {tags}")
|
||||
except Exception as e:
|
||||
logger.warning(f"读取角色卡失败,使用空标签: {e}")
|
||||
|
||||
# 构建header
|
||||
header = {
|
||||
"user_name": metadata.get("user_name", "User") if metadata else "User",
|
||||
@@ -207,7 +223,8 @@ class ChatService:
|
||||
"timedWorldInfo": {},
|
||||
"variables": {},
|
||||
"tainted": False,
|
||||
"lastInContextMessageId": -1
|
||||
"lastInContextMessageId": -1,
|
||||
"tags": tags # ✅ 使用标签数组替代 tableHeaders/tableDefaults/tableData
|
||||
}
|
||||
|
||||
# 写入header
|
||||
@@ -381,3 +398,57 @@ class ChatService:
|
||||
except Exception as e:
|
||||
logger.error(f"删除消息失败 {role_name}/{chat_name}/{floor}: {str(e)}")
|
||||
raise
|
||||
|
||||
def update_table_data(self, role_name: str, chat_name: str, table_update: Dict) -> Dict:
|
||||
"""
|
||||
更新标签数据(SillyTavern 关键字机制)
|
||||
|
||||
Args:
|
||||
role_name: 角色名称
|
||||
chat_name: 聊天名称
|
||||
table_update: 包含 tags 数组的字典
|
||||
|
||||
Returns:
|
||||
Dict: 更新后的标签数据
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 聊天文件不存在
|
||||
"""
|
||||
chat_file = self.chat_dir / role_name / f"{chat_name}.jsonl"
|
||||
|
||||
if not chat_file.exists():
|
||||
raise FileNotFoundError(f"Chat not found: {role_name}/{chat_name}")
|
||||
|
||||
try:
|
||||
with open(chat_file, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
if not lines:
|
||||
raise ValueError(f"Empty chat file: {role_name}/{chat_name}")
|
||||
|
||||
# 读取 header
|
||||
header = json.loads(lines[0])
|
||||
|
||||
# 获取新的标签数组
|
||||
new_tags = table_update.get('tags', [])
|
||||
|
||||
# 更新 header 中的 tags
|
||||
header['tags'] = new_tags
|
||||
|
||||
# 写回文件
|
||||
lines[0] = json.dumps(header, ensure_ascii=False) + '\n'
|
||||
|
||||
with open(chat_file, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
logger.info(f"标签数据已更新: {role_name}/{chat_name}, 标签数: {len(new_tags)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tags": new_tags,
|
||||
"tagCount": len(new_tags)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新标签数据失败 {role_name}/{chat_name}: {str(e)}")
|
||||
raise
|
||||
|
||||
203
backend/services/preset_service.py
Normal file
203
backend/services/preset_service.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
|
||||
# 计算组件数量
|
||||
entries = data.get("entries", [])
|
||||
prompts = data.get("prompts", [])
|
||||
component_count = len(entries) if entries else len(prompts)
|
||||
|
||||
# 提取温度参数(支持内部结构和 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
|
||||
Reference in New Issue
Block a user