diff --git a/apps/api/app/api/routes/asset_libraries.py b/apps/api/app/api/routes/asset_libraries.py index af09c629f..a663f326e 100644 --- a/apps/api/app/api/routes/asset_libraries.py +++ b/apps/api/app/api/routes/asset_libraries.py @@ -5,6 +5,7 @@ from app.dependencies import get_asset_library_repository, get_project_repositor from app.schemas.asset_library import ( AssetLibraryResponse, CreateAssetLibraryRequest, + EnsureDefaultLibraryRequest, ListAssetLibrariesResponse, ) from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -15,7 +16,7 @@ from packages.application import ( GetProjectUseCase, ListAssetLibrariesUseCase, ) -from packages.domain import AssetLibraryKind +from packages.domain import AssetLibrary, AssetLibraryKind router = APIRouter() @@ -43,6 +44,7 @@ def _to_asset_library_response(item) -> AssetLibraryResponse: @router.get("", response_model=ListAssetLibrariesResponse) def list_asset_libraries( project_id: str | None = Query(None), + kind: str | None = Query(None, pattern="^(video|voice|image)$"), authenticated_user: AuthenticatedUser = Depends(get_current_user), asset_library_repository: Any = Depends(get_asset_library_repository), project_repository: Any = Depends(get_project_repository), @@ -66,6 +68,11 @@ def list_asset_libraries( all_items.extend(use_case.execute(proj.id)) items = all_items + # 按 kind 过滤(可选) + if kind: + kind_enum = AssetLibraryKind(kind) + items = [item for item in items if item.kind == kind_enum] + return ListAssetLibrariesResponse(items=[_to_asset_library_response(item) for item in items]) @@ -90,3 +97,53 @@ def create_asset_library( ) ) return _to_asset_library_response(item) + + +# 默认素材库名称映射 +_DEFAULT_LIBRARY_NAMES = { + "video": "视频素材库", + "voice": "配音素材库", + "image": "图片素材库", +} + + +@router.post("/ensure-default", response_model=AssetLibraryResponse) +def ensure_default_library( + request: EnsureDefaultLibraryRequest, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + asset_library_repository: Any = Depends(get_asset_library_repository), + project_repository: Any = Depends(get_project_repository), +) -> AssetLibraryResponse: + """确保项目下指定 kind 的默认素材库存在,已存在则直接返回,不存在则自动创建。""" + project = project_repository.find_by_id(request.project_id) + if project is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + if not project.can_access(authenticated_user.user.id): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to project") + + kind = AssetLibraryKind(request.kind) + + # 查找该项目下同 kind 的素材库,返回第一个 + existing = asset_library_repository.find_by_project(request.project_id) + for lib in existing: + if lib.kind == kind: + return _to_asset_library_response(lib) + + # 不存在 → 自动创建 + import uuid + from datetime import datetime, timezone + + now = datetime.now(timezone.utc) + default_name = _DEFAULT_LIBRARY_NAMES.get(request.kind, f"{request.kind}素材库") + library = AssetLibrary( + id=str(uuid.uuid4()), + project_id=request.project_id, + name=default_name, + kind=kind, + asset_count=0, + total_size=0, + created_at=now, + updated_at=now, + ) + created = asset_library_repository.create(library) + return _to_asset_library_response(created) diff --git a/apps/api/app/api/routes/assets.py b/apps/api/app/api/routes/assets.py index 58c0ce30b..79cc7bf91 100644 --- a/apps/api/app/api/routes/assets.py +++ b/apps/api/app/api/routes/assets.py @@ -20,7 +20,6 @@ from fastapi import APIRouter, Depends, HTTPException, Query from packages.application import ( CreateAssetCommand, CreateAssetUseCase, - ListAssetsUseCase, ) from packages.domain import AssetStatus, ClassificationStatus @@ -82,6 +81,8 @@ def list_assets( library_id: Optional[str] = Query(None), project_id: Optional[str] = Query(None), kind: Optional[str] = Query(None, pattern="^(video|voice|image)$"), + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=500), authenticated_user: AuthenticatedUser = Depends(get_current_user), asset_repository: Any = Depends(get_asset_repository), asset_library_repository: Any = Depends(get_asset_library_repository), @@ -105,28 +106,54 @@ def list_assets( raise HTTPException(status_code=404, detail=f"AssetLibrary {library_id} not found") _check_project_access(library.project_id, user_id, project_repository) if kind: - items = asset_repository.find_by_library_and_file_type(library_id, kind_to_file_type[kind]) + items = asset_repository.find_by_library_and_file_type( + library_id, kind_to_file_type[kind], skip=skip, limit=limit + ) else: - items = asset_repository.find_by_library(library_id) - return ListAssetsResponse(items=[_to_asset_response(item) for item in items]) + items = asset_repository.find_by_library(library_id, skip=skip, limit=limit) + # 计算 total(不分页的全量数量) + all_items = asset_repository.find_by_library(library_id) if kind else items + total = len(_filter_by_kind(all_items)) if kind else len(all_items) + return ListAssetsResponse( + items=[_to_asset_response(item) for item in items], + total=total, + skip=skip, + limit=limit, + ) # 模式2:指定 project_id → 返回该项目所有素材 if project_id: _check_project_access(project_id, user_id, project_repository) - items = asset_repository.find_by_project(project_id) - return ListAssetsResponse(items=[_to_asset_response(item) for item in _filter_by_kind(items)]) + all_items = asset_repository.find_by_project(project_id) + filtered = _filter_by_kind(all_items) + total = len(filtered) + paged = filtered[skip : skip + limit] + return ListAssetsResponse( + items=[_to_asset_response(item) for item in paged], + total=total, + skip=skip, + limit=limit, + ) # 模式3:都不传 → 返回用户可访问的所有项目的所有素材 try: projects = project_repository.find_accessible_projects(user_id) except Exception: logger.exception("查询用户可访问项目失败: user_id=%s", user_id) - return ListAssetsResponse(items=[]) + return ListAssetsResponse(items=[], total=0, skip=skip, limit=limit) all_items = [] for proj in projects: all_items.extend(asset_repository.find_by_project(proj.id)) - return ListAssetsResponse(items=[_to_asset_response(item) for item in _filter_by_kind(all_items)]) + filtered = _filter_by_kind(all_items) + total = len(filtered) + paged = filtered[skip : skip + limit] + return ListAssetsResponse( + items=[_to_asset_response(item) for item in paged], + total=total, + skip=skip, + limit=limit, + ) def _apply_asset_review_status(item, review_status: str): diff --git a/apps/api/app/schemas/asset.py b/apps/api/app/schemas/asset.py index d08f40a57..8b783b539 100644 --- a/apps/api/app/schemas/asset.py +++ b/apps/api/app/schemas/asset.py @@ -55,3 +55,6 @@ class AssetResponse(BaseModel): class ListAssetsResponse(BaseModel): items: list[AssetResponse] + total: int = Field(default=0, ge=0) + skip: int = Field(default=0, ge=0) + limit: int = Field(default=100, ge=1) diff --git a/apps/api/app/schemas/asset_library.py b/apps/api/app/schemas/asset_library.py index 0277fe886..f5f4bf58b 100644 --- a/apps/api/app/schemas/asset_library.py +++ b/apps/api/app/schemas/asset_library.py @@ -18,3 +18,8 @@ class AssetLibraryResponse(BaseModel): class ListAssetLibrariesResponse(BaseModel): items: list[AssetLibraryResponse] + + +class EnsureDefaultLibraryRequest(BaseModel): + project_id: str = Field(..., min_length=1) + kind: str = Field(..., pattern="^(video|voice|image)$") diff --git a/apps/web/src/api/assets.ts b/apps/web/src/api/assets.ts index 2e7a6de59..c2b064c0a 100644 --- a/apps/web/src/api/assets.ts +++ b/apps/web/src/api/assets.ts @@ -129,6 +129,35 @@ export const getAssets = async (libraryId: string): Promise => { return response.data.items || []; }; +/** 按类型获取素材(如 voice/video/image) */ +export const getAssetsByKind = async (kind: string): Promise => { + const response = await apiClient.get("/assets", { + params: { kind }, + }); + return response.data.items || []; +}; + +/** 创建素材(上传文件后调用,附带 metadata) */ +export const createAsset = async (data: { + library_id: string; + name: string; + storage_key: string; + mime_type: string; + metadata?: Record; +}): Promise => { + const response = await apiClient.post("/assets", data); + return response.data; +}; + +/** 更新素材(名称、metadata 等) */ +export const updateAsset = async ( + assetId: string, + data: { name?: string; metadata?: Record }, +): Promise => { + const response = await apiClient.put(`/assets/${assetId}`, data); + return response.data; +}; + /** 更新素材审核状态 */ export const updateAssetReviewStatus = async ( assetId: string,