完成大量美化,zustand迁移,动态表格修复
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from backend.services.chat_service import ChatService
|
||||
from backend.core.config import settings
|
||||
from backend.services.chat_service import ChatService
|
||||
from backend.core.config import settings
|
||||
except ImportError:
|
||||
# Docker环境:直接从当前目录导入
|
||||
from services.chat_service import ChatService
|
||||
from core.config import settings
|
||||
# Docker环境:直接从当前目录导入
|
||||
from services.chat_service import ChatService
|
||||
from core.config import settings
|
||||
|
||||
router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
@@ -14,11 +15,23 @@ router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
data_path = Path(settings.DATA_PATH) if hasattr(settings, 'DATA_PATH') else Path("data")
|
||||
chat_service = ChatService(data_path)
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
async def list_all_chats():
|
||||
"""获取所有角色的所有聊天列表"""
|
||||
return chat_service.list_all_chats()
|
||||
|
||||
|
||||
# 注意:路由定义顺序很重要!更具体的路由(更多参数)必须放在前面
|
||||
@router.get("/{role_name}/{chat_name}")
|
||||
async def get_chat(role_name: str, chat_name: str):
|
||||
"""获取指定聊天的完整内容"""
|
||||
try:
|
||||
return chat_service.get_chat(role_name, chat_name)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{role_name}")
|
||||
async def list_role_chats(role_name: str):
|
||||
"""获取指定角色的所有聊天列表"""
|
||||
@@ -30,13 +43,6 @@ async def list_role_chats(role_name: str):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{role_name}/{chat_name}")
|
||||
async def get_chat(role_name: str, chat_name: str):
|
||||
"""获取指定聊天的完整内容"""
|
||||
try:
|
||||
return chat_service.get_chat(role_name, chat_name)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@router.post("/{role_name}", status_code=status.HTTP_201_CREATED)
|
||||
async def create_chat(role_name: str, chat_data: dict):
|
||||
@@ -48,18 +54,21 @@ async def create_chat(role_name: str, chat_data: dict):
|
||||
except FileExistsError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{role_name}/{chat_name}")
|
||||
async def update_chat(role_name: str, chat_name: str, update_data: dict):
|
||||
"""更新聊天元数据"""
|
||||
# TODO: 实现更新聊天元数据功能
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
|
||||
@router.delete("/{role_name}/{chat_name}")
|
||||
async def delete_chat(role_name: str, chat_name: str):
|
||||
"""删除指定聊天"""
|
||||
# TODO: 实现删除聊天功能
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
|
||||
@router.get("/{role_name}/{chat_name}/messages")
|
||||
async def list_messages(role_name: str, chat_name: str):
|
||||
"""获取聊天的所有消息"""
|
||||
@@ -69,6 +78,7 @@ async def list_messages(role_name: str, chat_name: str):
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{role_name}/{chat_name}/messages/{floor}")
|
||||
async def get_message(role_name: str, chat_name: str, floor: int):
|
||||
"""获取指定楼层的消息"""
|
||||
@@ -81,6 +91,7 @@ async def get_message(role_name: str, chat_name: str, floor: int):
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{role_name}/{chat_name}/messages", status_code=status.HTTP_201_CREATED)
|
||||
async def add_message(role_name: str, chat_name: str, message_data: dict):
|
||||
"""向聊天添加新消息"""
|
||||
@@ -91,6 +102,7 @@ async def add_message(role_name: str, chat_name: str, message_data: dict):
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{role_name}/{chat_name}/messages/{floor}")
|
||||
async def update_message(role_name: str, chat_name: str, floor: int, update_data: dict):
|
||||
"""更新指定楼层的消息"""
|
||||
@@ -101,6 +113,7 @@ async def update_message(role_name: str, chat_name: str, floor: int, update_data
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{role_name}/{chat_name}/messages/{floor}")
|
||||
async def delete_message(role_name: str, chat_name: str, floor: int):
|
||||
"""删除指定楼层的消息"""
|
||||
@@ -110,3 +123,14 @@ async def delete_message(role_name: str, chat_name: str, floor: int):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{role_name}/{chat_name}/table")
|
||||
async def update_table_data(role_name: str, chat_name: str, table_update: dict):
|
||||
"""更新表格数据(带时间戳冲突解决)"""
|
||||
try:
|
||||
return chat_service.update_table_data(role_name, chat_name, table_update)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
# TODO: 实现 PresetService 来替代旧的 AIDesignSpec 逻辑
|
||||
# from services.preset_service import PresetService
|
||||
from services.preset_service import PresetService
|
||||
|
||||
router = APIRouter(prefix="/presets", tags=["presets"])
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
async def list_presets():
|
||||
"""获取所有预设列表及其基本信息"""
|
||||
# return await PresetService.list_all_presets()
|
||||
return {"presets": []}
|
||||
try:
|
||||
presets = PresetService.list_presets()
|
||||
response_data = {"presets": presets}
|
||||
print(f"[API] GET /api/presets - 返回数据: {response_data}")
|
||||
return response_data
|
||||
except Exception as e:
|
||||
print(f"[API] GET /api/presets - 错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# 注意:路由定义顺序很重要!更具体的路由(更多参数)必须放在前面
|
||||
@router.get("/{preset_name}/components/{component_id}")
|
||||
async def get_preset_component(preset_name: str, component_id: str):
|
||||
"""获取指定组件的详情"""
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{preset_name}/components")
|
||||
async def list_preset_components(preset_name: str):
|
||||
"""获取预设中的所有组件"""
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{preset_name}")
|
||||
async def get_preset(preset_name: str):
|
||||
"""获取指定预设的完整内容"""
|
||||
# try:
|
||||
# return await PresetService.get_preset(preset_name)
|
||||
# except FileNotFoundError:
|
||||
# raise HTTPException(status_code=404, detail="Preset not found")
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
try:
|
||||
preset_data = PresetService.get_preset(preset_name)
|
||||
return preset_data
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_preset(preset_name: str, preset_data: dict):
|
||||
@@ -34,16 +54,6 @@ async def delete_preset(preset_name: str):
|
||||
"""删除指定预设"""
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{preset_name}/components")
|
||||
async def list_preset_components(preset_name: str):
|
||||
"""获取预设中的所有组件"""
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{preset_name}/components/{component_id}")
|
||||
async def get_preset_component(preset_name: str, component_id: str):
|
||||
"""获取指定组件的详情"""
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/{preset_name}/components", status_code=status.HTTP_201_CREATED)
|
||||
async def add_preset_component(preset_name: str, component_data: dict):
|
||||
"""向预设添加新组件"""
|
||||
|
||||
@@ -38,6 +38,80 @@ async def list_worldbooks():
|
||||
logger.error(f"Failed to list worldbooks: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# 注意:路由定义顺序很重要!更具体的路由(更多参数)必须放在前面
|
||||
@router.get("/{name}/entries/{uid}", response_model=Dict[str, Any])
|
||||
async def get_worldbook_entry(name: str, uid: str):
|
||||
"""
|
||||
获取世界书的指定条目
|
||||
"""
|
||||
try:
|
||||
return worldbook_service.get_entry(name, uid)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get entry '{uid}' from worldbook '{name}': {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{name}/entries", response_model=Dict[str, Any])
|
||||
async def list_worldbook_entries(
|
||||
name: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
):
|
||||
"""
|
||||
获取世界书的条目列表(支持分页)
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
page: 页码,从1开始
|
||||
page_size: 每页数量,默认20
|
||||
"""
|
||||
try:
|
||||
return worldbook_service.list_entries(name, page, page_size)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list entries for worldbook '{name}': {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{name}/export")
|
||||
async def export_worldbook(name: str, format: str = "internal"):
|
||||
"""
|
||||
导出世界书(支持 internal 和 sillytavern 两种格式)
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
format: 导出格式 ('internal' 或 'sillytavern'),默认 internal
|
||||
"""
|
||||
try:
|
||||
if format.lower() == "sillytavern":
|
||||
# 导出为 SillyTavern 格式(可能丢失特殊设置)
|
||||
logger.info(f"导出世界书 '{name}' 为 SillyTavern 格式")
|
||||
st_data = worldbook_service.export_to_sillytavern(name)
|
||||
|
||||
return JSONResponse(
|
||||
content=st_data,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={name}_sillytavern.json"
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 导出为内部格式(保留所有设置)
|
||||
logger.info(f"导出世界书 '{name}' 为内部格式")
|
||||
internal_data = worldbook_service.get_worldbook(name)
|
||||
|
||||
return JSONResponse(
|
||||
content=internal_data,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={name}.json"
|
||||
}
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export worldbook '{name}': {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{name}", response_model=Dict[str, Any])
|
||||
async def get_worldbook(name: str):
|
||||
"""
|
||||
@@ -213,41 +287,3 @@ async def import_worldbook(name: str, file: UploadFile = File(...)):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import worldbook '{name}': {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{name}/export")
|
||||
async def export_worldbook(name: str, format: str = "internal"):
|
||||
"""
|
||||
导出世界书(支持 internal 和 sillytavern 两种格式)
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
format: 导出格式 ('internal' 或 'sillytavern'),默认 internal
|
||||
"""
|
||||
try:
|
||||
if format.lower() == "sillytavern":
|
||||
# 导出为 SillyTavern 格式(可能丢失特殊设置)
|
||||
logger.info(f"导出世界书 '{name}' 为 SillyTavern 格式")
|
||||
st_data = worldbook_service.export_to_sillytavern(name)
|
||||
|
||||
return JSONResponse(
|
||||
content=st_data,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={name}_sillytavern.json"
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 导出为内部格式(保留所有设置)
|
||||
logger.info(f"导出世界书 '{name}' 为内部格式")
|
||||
internal_data = worldbook_service.get_worldbook(name)
|
||||
|
||||
return JSONResponse(
|
||||
content=internal_data,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={name}.json"
|
||||
}
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export worldbook '{name}': {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -3,12 +3,15 @@ from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 1. 动态计算项目根目录
|
||||
# 假设 config.py 位于 backend/core/ 目录下
|
||||
# __file__ 指向本文件的绝对路径
|
||||
# .parent 指向 backend/core/ 目录
|
||||
# .parent.parent 指向 backend/ 目录
|
||||
# .parent.parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
# 在 Docker 环境中:config.py 位于 /app/core/,需要向上2级到 /app/
|
||||
# 在本地开发中:config.py 位于 backend/core/,需要向上3级到项目根目录
|
||||
_config_path = Path(__file__).resolve()
|
||||
if _config_path.parent.parent.name == 'app':
|
||||
# Docker 环境:/app/core/config.py -> /app/
|
||||
PROJECT_ROOT = _config_path.parent.parent
|
||||
else:
|
||||
# 本地开发:backend/core/config.py -> 项目根目录
|
||||
PROJECT_ROOT = _config_path.parent.parent.parent
|
||||
|
||||
# 2. 加载 .env 文件
|
||||
# 假设 .env 文件位于项目根目录下
|
||||
|
||||
@@ -136,6 +136,7 @@ class CharacterCard(BaseModel):
|
||||
first_mes: str = Field(..., description="首条开场消息")
|
||||
mes_example: str = Field(..., description="对话示例")
|
||||
categories: List[str] = Field(default_factory=list, description="分类标签 (用于前端筛选)")
|
||||
tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (SillyTavern 关键字机制)")
|
||||
worldInfoId: Optional[str] = Field(None, description="绑定的世界书 ID")
|
||||
outputSchema: Optional[List[OutputSchemaField]] = Field(None, description="输出 schema 定义 (结构化输出)")
|
||||
avatarPath: Optional[str] = Field(None, description="角色头像路径")
|
||||
@@ -161,7 +162,7 @@ class ChatHeader(BaseModel):
|
||||
characterId: str = Field(..., description="关联的角色卡 ID")
|
||||
userName: str = Field("User", description="用户角色名")
|
||||
characterName: str = Field(..., description="AI 角色名称")
|
||||
tableData: Optional[Dict[str, Any]] = Field(None, description="表格数据 (对应 outputSchema)")
|
||||
tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (从角色卡继承)")
|
||||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||||
messageCount: int = Field(0, description="消息数量")
|
||||
|
||||
@@ -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