290 lines
9.3 KiB
Python
290 lines
9.3 KiB
Python
# 标准库导入
|
||
import os
|
||
import json
|
||
import shutil
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import List, Dict, Any, Optional
|
||
|
||
# 第三方库导入
|
||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||
from fastapi.responses import JSONResponse, FileResponse
|
||
|
||
# 本地模块导入
|
||
from models.internal import WorldInfo, WorldInfoEntry
|
||
from core.config import settings
|
||
from services.worldbook_service import worldbook_service
|
||
|
||
# 配置日志
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 创建路由器
|
||
router = APIRouter(prefix="/worldbooks", tags=["worldbooks"])
|
||
|
||
# 确保世界书目录存在 (由 config.py 中的 settings.ensure_directories() 统一处理,此处保留作为双重保险)
|
||
os.makedirs(settings.WORLDBOOKS_PATH, exist_ok=True)
|
||
|
||
|
||
@router.get("/", response_model=List[Dict[str, Any]])
|
||
async def list_worldbooks():
|
||
"""
|
||
获取所有世界书的列表
|
||
Returns:
|
||
List[Dict[str, Any]]: 世界书列表
|
||
"""
|
||
try:
|
||
return worldbook_service.list_worldbooks()
|
||
except Exception as e:
|
||
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):
|
||
"""
|
||
获取指定名称的世界书
|
||
"""
|
||
try:
|
||
return worldbook_service.get_worldbook(name)
|
||
except FileNotFoundError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to get worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
@router.post("/", response_model=Dict[str, Any])
|
||
async def create_worldbook(
|
||
name: str = Form(...),
|
||
description: str = Form(""),
|
||
file: Optional[UploadFile] = File(None)
|
||
):
|
||
"""
|
||
创建新世界书(可选择导入文件)
|
||
"""
|
||
try:
|
||
# 如果提供了文件,从 SillyTavern 格式导入
|
||
if file:
|
||
content = await file.read()
|
||
st_data = json.loads(content.decode('utf-8'))
|
||
return worldbook_service.import_from_sillytavern(name, st_data)
|
||
else:
|
||
# 创建空世界书
|
||
return worldbook_service.create_worldbook(name, description)
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to create worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
@router.put("/{name}", response_model=Dict[str, Any])
|
||
async def update_worldbook(
|
||
name: str,
|
||
description: Optional[str] = Form(None)
|
||
):
|
||
"""
|
||
更新世界书基本信息
|
||
"""
|
||
try:
|
||
return worldbook_service.update_worldbook(name, description)
|
||
except FileNotFoundError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to update worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
@router.delete("/{name}")
|
||
async def delete_worldbook(name: str):
|
||
"""
|
||
删除世界书
|
||
"""
|
||
try:
|
||
worldbook_service.delete_worldbook(name)
|
||
return {"message": f"Worldbook '{name}' deleted successfully"}
|
||
except FileNotFoundError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to delete 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}/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.post("/{name}/entries", response_model=Dict[str, Any])
|
||
async def create_worldbook_entry(name: str, entry_data: Dict[str, Any]):
|
||
"""
|
||
在世界书中创建新条目
|
||
"""
|
||
try:
|
||
return worldbook_service.create_entry(name, entry_data)
|
||
except FileNotFoundError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to create entry in worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
@router.put("/{name}/entries/{uid}", response_model=Dict[str, Any])
|
||
async def update_worldbook_entry(name: str, uid: str, entry_data: Dict[str, Any]):
|
||
"""
|
||
更新世界书的指定条目
|
||
"""
|
||
try:
|
||
return worldbook_service.update_entry(name, uid, entry_data)
|
||
except FileNotFoundError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to update entry '{uid}' in worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
@router.delete("/{name}/entries/{uid}")
|
||
async def delete_worldbook_entry(name: str, uid: str):
|
||
"""
|
||
删除世界书的指定条目
|
||
"""
|
||
try:
|
||
worldbook_service.delete_entry(name, uid)
|
||
return {"message": f"Entry '{uid}' deleted successfully"}
|
||
except FileNotFoundError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to delete entry '{uid}' from worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
@router.post("/{name}/import", response_model=Dict[str, Any])
|
||
async def import_worldbook(name: str, file: UploadFile = File(...)):
|
||
"""
|
||
从文件导入世界书(自动检测 SillyTavern 或内部格式)
|
||
"""
|
||
try:
|
||
content = await file.read()
|
||
data = json.loads(content.decode('utf-8'))
|
||
|
||
# 智能检测格式
|
||
from models.converters import WorldBookConverter
|
||
format_type = WorldBookConverter.detect_format(data)
|
||
|
||
logger.info(f"检测到世界书格式: {format_type}")
|
||
|
||
if format_type == "sillytavern":
|
||
# SillyTavern 格式,需要转换
|
||
logger.info(f"正在转换 SillyTavern 格式为内部格式")
|
||
return worldbook_service.import_from_sillytavern(name, data)
|
||
elif format_type == "internal":
|
||
# 已经是内部格式,直接保存
|
||
logger.info(f"检测到内部格式,直接保存")
|
||
return worldbook_service.import_internal_format(name, data)
|
||
else:
|
||
raise HTTPException(status_code=400, detail="无法识别的世界书格式")
|
||
|
||
except json.JSONDecodeError:
|
||
raise HTTPException(status_code=400, detail="Invalid JSON format")
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Failed to import worldbook '{name}': {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|