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 解析当前登录用户""" 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})