fix: 修复图片创作删除与垃圾桶预览
This commit is contained in:
@@ -38,6 +38,19 @@ class ImageConversationSerializer(serializers.ModelSerializer):
|
||||
return cached if cached is not None else obj.tasks.count()
|
||||
|
||||
|
||||
class ImageConversationTrashSerializer(ImageConversationSerializer):
|
||||
"""垃圾桶专用对话契约:只额外暴露一张展示封面,不影响普通对话列表查询。"""
|
||||
|
||||
cover_preview_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(ImageConversationSerializer.Meta):
|
||||
fields = [*ImageConversationSerializer.Meta.fields, "cover_preview_url"]
|
||||
read_only_fields = [*ImageConversationSerializer.Meta.read_only_fields, "cover_preview_url"]
|
||||
|
||||
def get_cover_preview_url(self, obj) -> str:
|
||||
return str(getattr(obj, "_trash_cover_preview_url", "") or "")
|
||||
|
||||
|
||||
class AITaskSerializer(serializers.ModelSerializer):
|
||||
model_config = ModelConfigSerializer(read_only=True)
|
||||
# 从 request_payload 抽出的分组/标签信息(由 AITaskViewSet annotate 提供;其它调用处无此注解则为 None)。
|
||||
|
||||
@@ -1128,6 +1128,73 @@ class ImageConversationTests(TestCase):
|
||||
trash_ids = {c["id"] for c in self.client.get("/api/ai/image-conversations/trash/?mode=image").json()["results"]}
|
||||
self.assertNotIn(str(conv.id), trash_ids)
|
||||
|
||||
def test_conversation_trash_uses_latest_generated_image_as_preview(self):
|
||||
conv = ImageConversation.objects.create(
|
||||
team=self.team,
|
||||
created_by=self.user,
|
||||
title="多资产对话",
|
||||
mode=ImageConversation.Mode.IMAGE,
|
||||
)
|
||||
provider = ModelProvider.objects.create(name="conv-cover-provider", display_name="Conv Cover Provider")
|
||||
model = ModelConfig.objects.create(
|
||||
provider=provider,
|
||||
name="conv-cover-model",
|
||||
display_name="Conv Cover Model",
|
||||
capability=ModelConfig.Capability.IMAGE,
|
||||
)
|
||||
assets = []
|
||||
for index, preview_url in enumerate(("http://x/older.png", "http://x/latest.png")):
|
||||
task = AITask.objects.create(
|
||||
team=self.team,
|
||||
created_by=self.user,
|
||||
conversation=conv,
|
||||
task_type=AITask.Type.PRODUCT_IMAGE,
|
||||
status=AITask.Status.SUCCEEDED,
|
||||
model_config=model,
|
||||
idempotency_key=f"conv-cover-task-{index}",
|
||||
)
|
||||
asset = Asset.objects.create(
|
||||
team=self.team,
|
||||
created_by=self.user,
|
||||
name=f"对话生成图 {index}",
|
||||
asset_type=Asset.Type.IMAGE,
|
||||
source=Asset.Source.AI_GENERATED,
|
||||
category=Asset.Category.FREE_CREATE,
|
||||
origin_task=task,
|
||||
)
|
||||
AssetFile.objects.create(
|
||||
asset=asset,
|
||||
object_key=f"free-create/{index}.png",
|
||||
bucket="test",
|
||||
content_type="image/png",
|
||||
preview_url=preview_url,
|
||||
is_primary=True,
|
||||
)
|
||||
assets.append(asset)
|
||||
|
||||
delete = self.client.delete(f"/api/ai/image-conversations/{conv.id}/")
|
||||
self.assertIn(delete.status_code, (204, 200))
|
||||
rows = self.client.get("/api/ai/image-conversations/trash/?mode=image").json()["results"]
|
||||
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["cover_preview_url"], "http://x/latest.png")
|
||||
self.assertEqual(Asset.objects.filter(id__in=[asset.id for asset in assets]).count(), 2)
|
||||
|
||||
def test_conversation_trash_without_generated_image_has_empty_preview(self):
|
||||
conv = ImageConversation.objects.create(
|
||||
team=self.team,
|
||||
created_by=self.user,
|
||||
title="无图对话",
|
||||
mode=ImageConversation.Mode.IMAGE,
|
||||
)
|
||||
|
||||
delete = self.client.delete(f"/api/ai/image-conversations/{conv.id}/")
|
||||
self.assertIn(delete.status_code, (204, 200))
|
||||
rows = self.client.get("/api/ai/image-conversations/trash/?mode=image").json()["results"]
|
||||
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["cover_preview_url"], "")
|
||||
|
||||
def test_direct_asset_delete_hides_image_but_keeps_conversation(self):
|
||||
conv = ImageConversation.objects.create(
|
||||
team=self.team,
|
||||
|
||||
@@ -19,7 +19,12 @@ from apps.products.models import Product
|
||||
|
||||
from .generation_errors import classify_generation_error, public_error_for_task
|
||||
from .models import AITask, ImageConversation, ModelConfig
|
||||
from .serializers import AITaskSerializer, ImageConversationSerializer, ModelConfigSerializer
|
||||
from .serializers import (
|
||||
AITaskSerializer,
|
||||
ImageConversationSerializer,
|
||||
ImageConversationTrashSerializer,
|
||||
ModelConfigSerializer,
|
||||
)
|
||||
from .services import enqueue_standalone_images
|
||||
|
||||
|
||||
@@ -476,6 +481,11 @@ class ImageConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
serializer_class = ImageConversationSerializer
|
||||
queryset = ImageConversation.objects.select_related("product").order_by("-last_active_at")
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "trash":
|
||||
return ImageConversationTrashSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().annotate(_task_count=Count("tasks"))
|
||||
if self.action in ("trash", "restore", "purge"):
|
||||
@@ -524,8 +534,48 @@ class ImageConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
qs = self.filter_queryset(self.get_queryset())
|
||||
page = self.paginate_queryset(qs)
|
||||
if page is not None:
|
||||
self._attach_trash_cover_preview_urls(page)
|
||||
return self.get_paginated_response(self.get_serializer(page, many=True).data)
|
||||
return Response(self.get_serializer(qs, many=True).data)
|
||||
conversations = list(qs)
|
||||
self._attach_trash_cover_preview_urls(conversations)
|
||||
return Response(self.get_serializer(conversations, many=True).data)
|
||||
|
||||
def _attach_trash_cover_preview_urls(self, conversations):
|
||||
"""给当前页对话挂一张展示封面;全部资产仍由对话统一恢复/彻底删除。"""
|
||||
conversation_ids = [conversation.id for conversation in conversations]
|
||||
if not conversation_ids:
|
||||
return
|
||||
covers = {}
|
||||
assets = (
|
||||
Asset.objects.filter(
|
||||
team=self.get_team(),
|
||||
origin_task__conversation_id__in=conversation_ids,
|
||||
origin_task__status=AITask.Status.SUCCEEDED,
|
||||
origin_task__is_deleted=True,
|
||||
origin_task__purged_at__isnull=True,
|
||||
asset_type=Asset.Type.IMAGE,
|
||||
is_deleted=True,
|
||||
purged_at__isnull=True,
|
||||
)
|
||||
.select_related("origin_task")
|
||||
.prefetch_related("files")
|
||||
.order_by("origin_task__conversation_id", "-origin_task__created_at", "created_at", "id")
|
||||
)
|
||||
for asset in assets:
|
||||
conversation_id = asset.origin_task.conversation_id
|
||||
if conversation_id in covers:
|
||||
continue
|
||||
files = sorted(
|
||||
asset.files.all(),
|
||||
key=lambda item: (not item.is_primary, item.created_at, str(item.id)),
|
||||
)
|
||||
for file in files:
|
||||
preview_url = AssetFileSerializer(file).data.get("preview_url", "")
|
||||
if preview_url:
|
||||
covers[conversation_id] = preview_url
|
||||
break
|
||||
for conversation in conversations:
|
||||
conversation._trash_cover_preview_url = covers.get(conversation.id, "")
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="restore")
|
||||
def restore(self, request, pk=None):
|
||||
|
||||
Reference in New Issue
Block a user