feat(pipeline): 基础资产页 A三视图手动 + B进故事板拦截 + C采用/未采用 + D删除
- A 三视图改手动:删"打开角色详情自动出三视图"逻辑、角色生成只出立绘(不再链式自动三视图); 详情弹窗保留「AI 生成三视图」按钮作手动入口(提示文案去掉"自动") - B 进下一步拦截:「确认资产,进入故事板」按钮挂 guardGen —— 被引用角色/场景缺"已采用"参考图就弹窗拦 - C 采用/未采用:角色/场景卡加 toggle 徽章(已采用绿/未采用灰);默认按"实体名是否在脚本里"派生, 用户可点击覆盖并持久化(metadata.adopt);后端 set-base-asset-adopt 端点;下游 _storyboard_reference_images 只取已采用组(商品组永远保留、顺带排除未采用/三视图组的兜底误抓) - D 删除:角色/场景卡 hover 显删除按钮(二次确认),商品主卡不给删;后端 delete-base-asset(连带三视图组) - 顺手:提取闸门按钮去掉"不自动花钱"文案 验收:projects/assets/ai 全套(基线既有 7 失败零新增,新增 set-adopt/delete 3 测过);tsc+build 绿; 无头核对(采用 toggle/hover 删除/待审核盾齐活、0 console error) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1024,6 +1024,22 @@ def _storyboard_reference_images(project, segment) -> list[dict]:
|
||||
"scene": BaseAssetGroup.Kind.SCENE,
|
||||
}
|
||||
groups = list(project.base_asset_groups.filter(adopted_asset__isnull=False).select_related("adopted_asset"))
|
||||
# 下游只取「已采用」的角色/场景组(C/D):metadata.adopt 显性优先,否则按是否在脚本里推导;
|
||||
# 商品组永远保留(不受采用态影响)。顺带把不在脚本里的(含三视图组 label「·三视图」)排除,
|
||||
# 避免兜底误抓未采用/三视图组。
|
||||
_entity_names = {(e.get("name") or "").strip() for e in entities.values() if (e.get("name") or "").strip()}
|
||||
|
||||
def _group_adopted(g) -> bool:
|
||||
if g.kind == BaseAssetGroup.Kind.PRODUCT:
|
||||
return True
|
||||
a = (g.metadata or {}).get("adopt")
|
||||
if a == "adopted":
|
||||
return True
|
||||
if a == "unadopted":
|
||||
return False
|
||||
return (g.metadata or {}).get("label", "").strip() in _entity_names
|
||||
|
||||
groups = [g for g in groups if _group_adopted(g)]
|
||||
out: list[dict] = []
|
||||
used: set = set()
|
||||
for rid in (segment.entity_refs or []):
|
||||
|
||||
@@ -532,3 +532,36 @@ class ExportClipsTests(TestCase):
|
||||
def test_export_empty_returns_400(self):
|
||||
res = self.client.get(f"/api/projects/{self.project.id}/export-clips/")
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
|
||||
class BaseAssetAdoptDeleteTests(TestCase):
|
||||
"""显性采用/未采用 + 删除角色组。"""
|
||||
|
||||
def setUp(self):
|
||||
from apps.projects.models import BaseAssetGroup
|
||||
|
||||
self.user = User.objects.create_user(username="bad", password="x")
|
||||
self.team = Team.objects.create(name="BAD", owner=self.user)
|
||||
TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
|
||||
self.product = Product.objects.create(team=self.team, created_by=self.user, title="P")
|
||||
self.proj = Project.objects.create(team=self.team, name="项目甲", product=self.product, created_by=self.user)
|
||||
self.group = BaseAssetGroup.objects.create(project=self.proj, kind=BaseAssetGroup.Kind.PERSON, metadata={"label": "小明"})
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
def test_set_adopt_state(self):
|
||||
res = self.client.post(f"/api/projects/{self.proj.id}/set-base-asset-adopt/", {"group_id": str(self.group.id), "state": "unadopted"}, format="json")
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.group.refresh_from_db()
|
||||
self.assertEqual(self.group.metadata.get("adopt"), "unadopted")
|
||||
|
||||
def test_set_adopt_invalid_state(self):
|
||||
res = self.client.post(f"/api/projects/{self.proj.id}/set-base-asset-adopt/", {"group_id": str(self.group.id), "state": "x"}, format="json")
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_delete_base_asset(self):
|
||||
from apps.projects.models import BaseAssetGroup
|
||||
|
||||
res = self.client.post(f"/api/projects/{self.proj.id}/delete-base-asset/", {"group_id": str(self.group.id)}, format="json")
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertFalse(BaseAssetGroup.objects.filter(id=self.group.id).exists())
|
||||
|
||||
@@ -362,6 +362,38 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
promote_base_asset_stage_if_ready(project)
|
||||
return Response(BaseAssetGroupSerializer(group).data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="set-base-asset-adopt")
|
||||
@transaction.atomic
|
||||
def set_base_asset_adopt(self, request, pk=None):
|
||||
"""显性「采用 / 未采用」某角色/场景组(metadata.adopt)。下游只取已采用的;脚本里没有的默认未采用,
|
||||
但用户强制采用随他。默认态(不写 adopt)由前端按是否在脚本里推导,这里只存用户的覆盖。"""
|
||||
project = self.get_object()
|
||||
group = BaseAssetGroup.objects.select_for_update().filter(project=project, id=request.data.get("group_id")).first()
|
||||
if group is None:
|
||||
return Response({"detail": "group not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
state = request.data.get("state")
|
||||
if state not in ("adopted", "unadopted"):
|
||||
return Response({"detail": "state must be adopted/unadopted"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
meta = dict(group.metadata or {})
|
||||
meta["adopt"] = state
|
||||
group.metadata = meta
|
||||
group.save(update_fields=["metadata", "updated_at"])
|
||||
return Response(BaseAssetGroupSerializer(group).data)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="delete-base-asset")
|
||||
@transaction.atomic
|
||||
def delete_base_asset(self, request, pk=None):
|
||||
"""删除某角色/场景资产组(连同它的三视图配套组一并删)。资产本身留在资产表,只移除项目内的组引用。"""
|
||||
project = self.get_object()
|
||||
group = BaseAssetGroup.objects.filter(project=project, id=request.data.get("group_id")).first()
|
||||
if group is None:
|
||||
return Response({"detail": "group not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
# 连带删该角色立绘配套的三视图组(triview_of 指向本组采用的立绘)
|
||||
if group.adopted_asset_id:
|
||||
project.base_asset_groups.filter(metadata__triview_of=str(group.adopted_asset_id)).delete()
|
||||
group.delete()
|
||||
return Response({"deleted": True})
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="attach-base-asset")
|
||||
@transaction.atomic
|
||||
def attach_base_asset(self, request, pk=None):
|
||||
|
||||
Reference in New Issue
Block a user