diff --git a/apps/api/app/api/routes/assets.py b/apps/api/app/api/routes/assets.py index 187c59711..58c0ce30b 100644 --- a/apps/api/app/api/routes/assets.py +++ b/apps/api/app/api/routes/assets.py @@ -8,7 +8,13 @@ from app.dependencies import ( get_asset_repository, get_project_repository, ) -from app.schemas.asset import AssetResponse, CreateAssetRequest, ListAssetsResponse, UpdateAssetReviewRequest +from app.schemas.asset import ( + AssetResponse, + CreateAssetRequest, + ListAssetsResponse, + UpdateAssetRequest, + UpdateAssetReviewRequest, +) from fastapi import APIRouter, Depends, HTTPException, Query from packages.application import ( @@ -75,6 +81,7 @@ def _check_project_access(project_id: str, user_id: str, project_repository) -> def list_assets( library_id: Optional[str] = Query(None), project_id: Optional[str] = Query(None), + kind: Optional[str] = Query(None, pattern="^(video|voice|image)$"), authenticated_user: AuthenticatedUser = Depends(get_current_user), asset_repository: Any = Depends(get_asset_repository), asset_library_repository: Any = Depends(get_asset_library_repository), @@ -82,20 +89,32 @@ def list_assets( ) -> ListAssetsResponse: user_id = authenticated_user.user.id + # kind → file_type 映射(voice 对应 audio) + kind_to_file_type = {"video": "video", "voice": "audio", "image": "image"} + + def _filter_by_kind(items): + if not kind: + return items + ft = kind_to_file_type.get(kind) + return [item for item in items if item.mime_type and item.mime_type.startswith(ft or "")] + # 模式1:指定 library_id → 返回该库的素材 if library_id: library = asset_library_repository.get(library_id) if library is None: raise HTTPException(status_code=404, detail=f"AssetLibrary {library_id} not found") _check_project_access(library.project_id, user_id, project_repository) - items = asset_repository.find_by_library(library_id) + if kind: + items = asset_repository.find_by_library_and_file_type(library_id, kind_to_file_type[kind]) + else: + items = asset_repository.find_by_library(library_id) return ListAssetsResponse(items=[_to_asset_response(item) for item in items]) # 模式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 items]) + return ListAssetsResponse(items=[_to_asset_response(item) for item in _filter_by_kind(items)]) # 模式3:都不传 → 返回用户可访问的所有项目的所有素材 try: @@ -107,7 +126,7 @@ def list_assets( 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 all_items]) + return ListAssetsResponse(items=[_to_asset_response(item) for item in _filter_by_kind(all_items)]) def _apply_asset_review_status(item, review_status: str): @@ -135,6 +154,59 @@ def update_asset_review_status( return _to_asset_response(updated) +@router.get("/{asset_id}", response_model=AssetResponse) +def get_asset( + asset_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + asset_repository: Any = Depends(get_asset_repository), + project_repository: Any = Depends(get_project_repository), +) -> AssetResponse: + item = asset_repository.find_by_id(asset_id) + if item is None: + raise HTTPException(status_code=404, detail=f"Asset {asset_id} not found") + _check_project_access(item.project_id, authenticated_user.user.id, project_repository) + return _to_asset_response(item) + + +@router.put("/{asset_id}", response_model=AssetResponse) +def update_asset( + asset_id: str, + request: UpdateAssetRequest, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + asset_repository: Any = Depends(get_asset_repository), + project_repository: Any = Depends(get_project_repository), +) -> AssetResponse: + item = asset_repository.find_by_id(asset_id) + if item is None: + raise HTTPException(status_code=404, detail=f"Asset {asset_id} not found") + _check_project_access(item.project_id, authenticated_user.user.id, project_repository) + + # 合并可修改字段 + if request.name is not None: + item.name = request.name + if request.metadata is not None: + item.metadata = {**item.metadata, **request.metadata} + if request.tags is not None: + item.metadata = {**item.metadata, "tags": request.tags} + + updated = asset_repository.update(item) + return _to_asset_response(updated) + + +@router.delete("/{asset_id}", status_code=204) +def delete_asset( + asset_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + asset_repository: Any = Depends(get_asset_repository), + project_repository: Any = Depends(get_project_repository), +) -> None: + item = asset_repository.find_by_id(asset_id) + if item is None: + raise HTTPException(status_code=404, detail=f"Asset {asset_id} not found") + _check_project_access(item.project_id, authenticated_user.user.id, project_repository) + asset_repository.delete(asset_id) + + @router.post("", response_model=AssetResponse) def create_asset( request: CreateAssetRequest, diff --git a/apps/api/app/schemas/asset.py b/apps/api/app/schemas/asset.py index ac2e8fb4b..d08f40a57 100644 --- a/apps/api/app/schemas/asset.py +++ b/apps/api/app/schemas/asset.py @@ -25,6 +25,12 @@ class UpdateAssetReviewRequest(BaseModel): review_status: str = Field(..., pattern="^(pending_review|approved|rejected)$") +class UpdateAssetRequest(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=100) + metadata: dict[str, object] | None = None + tags: list[str] | None = None + + class AssetResponse(BaseModel): id: str project_id: str diff --git a/packages/adapters/in_memory/asset_repository.py b/packages/adapters/in_memory/asset_repository.py index 3a3393967..7b6874a43 100755 --- a/packages/adapters/in_memory/asset_repository.py +++ b/packages/adapters/in_memory/asset_repository.py @@ -26,6 +26,13 @@ class InMemoryAssetRepository: """Alias for list_by_library to match the port interface.""" return self.list_by_library(library_id) + def find_by_library_and_file_type(self, library_id: str, file_type: str) -> list[Asset]: + return [ + asset + for asset in self._assets.values() + if asset.library_id == library_id and asset.mime_type and asset.mime_type.startswith(file_type) + ] + def update(self, asset: Asset) -> Asset: self._assets[asset.id] = asset return asset diff --git a/packages/adapters/sqlalchemy_impl/asset_repository.py b/packages/adapters/sqlalchemy_impl/asset_repository.py index e0eef97a9..d14fa3e9f 100644 --- a/packages/adapters/sqlalchemy_impl/asset_repository.py +++ b/packages/adapters/sqlalchemy_impl/asset_repository.py @@ -37,6 +37,22 @@ class SQLAlchemyAssetRepository: ) return [self._to_domain(model) for model in models] + def find_by_library_and_file_type( + self, + library_id: str, + file_type: str, + skip: int = 0, + limit: int = 100, + ) -> list[Asset]: + models = ( + self.session.query(AssetModel) + .filter(AssetModel.asset_library_id == library_id, AssetModel.file_type == file_type) + .offset(skip) + .limit(limit) + .all() + ) + return [self._to_domain(model) for model in models] + def find_by_id(self, asset_id: str) -> Asset | None: model = self.session.query(AssetModel).filter(AssetModel.id == asset_id).first() if model is None: diff --git a/packages/ports/asset_repository.py b/packages/ports/asset_repository.py index e5643a77f..3bee42356 100644 --- a/packages/ports/asset_repository.py +++ b/packages/ports/asset_repository.py @@ -32,6 +32,16 @@ class AssetRepository(ABC): ) -> list[Asset]: pass + @abstractmethod + def find_by_library_and_file_type( + self, + library_id: str, + file_type: str, + skip: int = 0, + limit: int = 100, + ) -> list[Asset]: + pass + @abstractmethod def update(self, asset: Asset) -> Asset: pass