feat: complete grounded digital avatar chat experience
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, Body, Header, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
import os
|
||||
import uuid
|
||||
import mimetypes
|
||||
|
||||
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
|
||||
@@ -10,6 +10,8 @@ from models import Avatar, KnowledgeDoc, KnowledgeChunk, QAPair, Authorization,
|
||||
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):
|
||||
@@ -20,6 +22,40 @@ def _resolve_user(authorization: str | None, db: Session):
|
||||
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)):
|
||||
# 仅返回当前登录用户自己的分身;未登录返回空,避免看到种子/他人数据
|
||||
@@ -98,40 +134,3 @@ def delete_avatar(avatar_id: str, db: Session = Depends(get_db)):
|
||||
db.commit()
|
||||
return ok({"success": True})
|
||||
|
||||
|
||||
@router.post("/avatar/{avatar_id}/photo")
|
||||
async def upload_avatar_photo(
|
||||
avatar_id: str,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""上传数字分身头像"""
|
||||
a = db.query(Avatar).filter(Avatar.id == avatar_id).first()
|
||||
if not a:
|
||||
return fail("分身不存在", 404)
|
||||
|
||||
# 验证文件类型
|
||||
if not file.content_type or not file.content_type.startswith("image/"):
|
||||
return fail("仅支持图片文件", 400)
|
||||
|
||||
file_bytes = await file.read()
|
||||
if len(file_bytes) > 5 * 1024 * 1024:
|
||||
return fail("头像文件不能超过5MB", 400)
|
||||
|
||||
# 保存到 uploads 目录
|
||||
ext = mimetypes.guess_extension(file.content_type) or ".jpg"
|
||||
filename = f"avatar-{uuid.uuid4().hex}{ext}"
|
||||
avatar_dir = os.path.join(UPLOAD_DIR, avatar_id)
|
||||
os.makedirs(avatar_dir, exist_ok=True)
|
||||
file_path = os.path.join(avatar_dir, filename)
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file_bytes)
|
||||
|
||||
# 更新数据库
|
||||
photo_url = f"/api/files/{avatar_id}/{filename}"
|
||||
a.photo_url = photo_url
|
||||
db.commit()
|
||||
db.refresh(a)
|
||||
|
||||
return ok(a.to_dict())
|
||||
|
||||
Reference in New Issue
Block a user