完成大量美化

This commit is contained in:
2026-05-01 15:44:14 +08:00
parent 6b65b24b0f
commit f0e7e75ffb
36 changed files with 4393 additions and 1506 deletions

View File

@@ -0,0 +1,292 @@
"""
角色卡 API 路由
"""
from fastapi import APIRouter, HTTPException, UploadFile, File
from fastapi.responses import FileResponse, StreamingResponse
from typing import List
from pathlib import Path
import io
try:
from backend.services.character_service import CharacterService
except ImportError:
from services.character_service import CharacterService
router = APIRouter(prefix="/characters", tags=["characters"])
character_service = CharacterService()
@router.get("/", response_model=List[dict])
async def list_characters():
"""
获取所有角色卡列表
Returns:
按最后聊天时间排序的角色卡列表
"""
characters = character_service.scan_all_characters()
return [c.dict() for c in characters]
@router.get("/{name}", response_model=dict)
async def get_character(name: str):
"""
获取指定角色卡
Args:
name: 角色名URL编码
"""
character = character_service.get_character_by_name(name)
if not character:
raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在")
return character.dict()
@router.post("/", response_model=dict)
async def create_character(character_data: dict):
"""
创建新角色卡
Request Body:
{
"name": "角色名",
"description": "描述",
"personality": "性格",
"scenario": "场景",
"first_mes": "开场白",
"categories": ["分类1", "分类2"],
"tags": ["tag1", "tag2"]
}
"""
try:
character = character_service.create_character(character_data)
return {
"success": True,
"character": character.dict()
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{name}", response_model=dict)
async def update_character(name: str, updates: dict):
"""
更新角色卡
Args:
name: 角色名
updates: 要更新的字段
"""
try:
character = character_service.update_character(name, updates)
return {
"success": True,
"character": character.dict()
}
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{name}")
async def delete_character(name: str):
"""
删除角色卡及其所有聊天记录
"""
success = character_service.delete_character(name)
if not success:
raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在")
return {"success": True, "message": f"角色 '{name}' 已删除"}
@router.get("/{name}/avatar")
async def get_avatar(name: str):
"""
获取角色头像
Returns:
PNG 图片文件或 404
"""
char_folder = character_service.characters_dir / name
avatar_file = char_folder / "avatar.png"
if not avatar_file.exists():
# 返回默认头像
default_avatar = Path("data/images/avatars/fallback.png")
if default_avatar.exists():
return FileResponse(default_avatar, media_type="image/png")
raise HTTPException(status_code=404, detail="头像不存在")
return FileResponse(avatar_file, media_type="image/png")
@router.post("/{name}/avatar")
async def upload_avatar(name: str, file: UploadFile = File(...)):
"""
上传角色头像
Args:
name: 角色名
file: PNG 图片文件
"""
# 验证文件类型
if not file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="只支持图片文件")
# 检查角色是否存在
character = character_service.get_character_by_name(name)
if not character:
raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在")
# 保存图片
image_data = await file.read()
avatar_path = character_service.save_avatar(name, image_data)
return {
"success": True,
"avatar_path": avatar_path
}
@router.get("/{name}/chats")
async def list_chats(name: str):
"""
获取角色的所有聊天列表
Returns:
聊天文件列表(包含最后一条消息预览)
"""
char_folder = character_service.characters_dir / name
chats_dir = char_folder / "chats"
if not chats_dir.exists():
return {"chats": []}
import json
from datetime import datetime
chats = []
for chat_file in chats_dir.glob("*.jsonl"):
try:
with open(chat_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not lines:
continue
# 第一行是header
header = json.loads(lines[0])
# 计算消息数量排除header
message_count = len(lines) - 1
# 获取最后修改时间
last_modified = datetime.fromtimestamp(
chat_file.stat().st_mtime
).isoformat()
# 获取最后一条消息预览
last_message = ""
if message_count > 0:
try:
last_msg_data = json.loads(lines[-1])
last_message = last_msg_data.get("mes", "")
except:
pass
chats.append({
"chat_name": chat_file.stem,
"user_name": header.get("user_name", "User"),
"character_name": header.get("character_name", ""),
"last_modified": last_modified,
"message_count": message_count,
"last_message": last_message
})
except Exception as e:
# 如果解析失败,使用基本信息
chats.append({
"chat_name": chat_file.stem,
"last_modified": datetime.fromtimestamp(chat_file.stat().st_mtime).isoformat(),
"message_count": 0,
"last_message": ""
})
# 按修改时间排序
chats.sort(key=lambda c: c.get('last_modified', ''), reverse=True)
return {"chats": chats}
@router.post("/import")
async def import_character(file: UploadFile = File(...)):
"""
导入角色卡(支持 PNG 或 JSON
- PNG: 自动提取嵌入数据,创建文件夹
- JSON: 创建文件夹并保存
"""
content = await file.read()
filename = file.filename
if filename.endswith('.png'):
# 导入 PNG
try:
character = character_service.import_from_png(content, filename)
return {
"success": True,
"character": character.dict(),
"format": "png_embedded"
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}")
elif filename.endswith('.json'):
# 导入 JSON
try:
import json
data = json.loads(content.decode('utf-8'))
character = character_service.create_character(data)
return {
"success": True,
"character": character.dict(),
"format": "json"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}")
else:
raise HTTPException(status_code=400, detail="不支持的文件格式")
@router.post("/{name}/export/png")
async def export_character_as_png(name: str):
"""
导出角色为 SillyTavern PNG 格式
Returns:
PNG 文件下载
"""
try:
png_data = character_service.export_as_png(name)
return StreamingResponse(
io.BytesIO(png_data),
media_type="image/png",
headers={
"Content-Disposition": f"attachment; filename={name}.png"
}
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"角色 '{name}' 不存在")
except Exception as e:
raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")