Files
huihuiSquare/digital-avatar-app/backend/routers/avatars.py
T

137 lines
5.2 KiB
Python

import os
import uuid
from fastapi import APIRouter, Depends, Body, Header, UploadFile, File, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from routers.knowledge import UPLOAD_DIR
from models import Avatar, KnowledgeDoc, KnowledgeChunk, QAPair, Authorization, User
from responses import ok, fail
router = APIRouter(tags=["分身"])
ALLOWED_AVATAR_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
MAX_AVATAR_BYTES = 5 * 1024 * 1024
def _resolve_user(authorization: str | None, db: Session):
"""从 Authorization: Bearer <app_token> 解析当前登录用户"""
if not authorization:
return None
token = authorization.replace("Bearer ", "", 1).replace("bearer ", "", 1).strip()
return db.query(User).filter(User.app_token == token).first()
def _require_owned_avatar(db: Session, avatar_id: str, authorization: str | None):
avatar = db.query(Avatar).filter(Avatar.id == avatar_id).first()
if not avatar:
raise HTTPException(status_code=404, detail="分身不存在")
user = _resolve_user(authorization, db)
if not user:
raise HTTPException(status_code=401, detail="未登录")
if avatar.owner_id and avatar.owner_id != user.huihui_user_id:
raise HTTPException(status_code=403, detail="无权访问该分身")
return avatar
@router.post("/avatar/{avatar_id}/photo")
async def upload_avatar_photo(
avatar_id: str,
file: UploadFile = File(...),
authorization: str = Header(None),
db: Session = Depends(get_db),
):
_require_owned_avatar(db, avatar_id, authorization)
extension = os.path.splitext(file.filename or "")[1].lower()
if extension not in ALLOWED_AVATAR_EXTENSIONS or not (file.content_type or "").startswith("image/"):
return fail("仅支持 JPG、PNG、WebP 或 GIF 图片", code=400)
content = await file.read()
if len(content) > MAX_AVATAR_BYTES:
return fail("头像图片不能超过 5MB", code=400)
avatar_dir = os.path.join(UPLOAD_DIR, avatar_id)
os.makedirs(avatar_dir, exist_ok=True)
stored_name = f"avatar-{uuid.uuid4().hex}{extension}"
with open(os.path.join(avatar_dir, stored_name), "wb") as stream:
stream.write(content)
return ok({"photoUrl": f"/api/files/{avatar_id}/{stored_name}"})
@router.get("/avatar")
def list_avatars(page: int = 1, limit: int = 20, authorization: str = Header(None), db: Session = Depends(get_db)):
# 仅返回当前登录用户自己的分身;未登录返回空,避免看到种子/他人数据
user = _resolve_user(authorization, db)
if not user:
return ok({"data": [], "total": 0})
q = db.query(Avatar).filter(Avatar.owner_id == user.huihui_user_id)
total = q.count()
items = (
q.order_by(Avatar.created_at.desc())
.offset((page - 1) * limit)
.limit(limit)
.all()
)
return ok({"data": [a.to_dict() for a in items], "total": total})
@router.get("/avatar/{avatar_id}")
def get_avatar(avatar_id: str, db: Session = Depends(get_db)):
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
if not a:
return fail("分身不存在", 404)
return ok(a.to_dict())
@router.post("/avatar")
def create_avatar(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)):
user = _resolve_user(authorization, db)
a = Avatar(
owner_id=user.huihui_user_id if user else "",
name=payload.get("name", "未命名分身"),
display_name=payload.get("displayName", "") or payload.get("display_name", ""),
description=payload.get("description", ""),
photo_url=payload.get("photoUrl", "") or payload.get("photo_url", ""),
emoji=payload.get("emoji", "🤖"),
status=payload.get("status", "active"),
token_balance=payload.get("tokenBalance", 0),
config=payload.get("config", {}) or {},
)
db.add(a)
db.commit()
db.refresh(a)
return ok(a.to_dict())
@router.put("/avatar/{avatar_id}")
def update_avatar(avatar_id: str, payload: dict = Body(...), db: Session = Depends(get_db)):
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
if not a:
return fail("分身不存在", 404)
mapping = {
"displayName": "display_name",
"photoUrl": "photo_url",
"tokenBalance": "token_balance",
}
for key in ("name", "displayName", "description", "photoUrl", "emoji", "status", "tokenBalance", "config"):
if key in payload:
col = mapping.get(key, key)
setattr(a, col, payload[key])
db.commit()
db.refresh(a)
return ok(a.to_dict())
@router.delete("/avatar/{avatar_id}")
def delete_avatar(avatar_id: str, db: Session = Depends(get_db)):
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
if not a:
return fail("分身不存在", 404)
# 级联清理关联数据,避免孤儿记录
db.query(KnowledgeDoc).filter(KnowledgeDoc.avatar_id == avatar_id).delete()
db.query(KnowledgeChunk).filter(KnowledgeChunk.avatar_id == avatar_id).delete()
db.query(QAPair).filter(QAPair.avatar_id == avatar_id).delete()
db.query(Authorization).filter(Authorization.avatar_id == avatar_id).delete()
db.delete(a)
db.commit()
return ok({"success": True})