From 4f288a1fefb1a7b234b73ffa4b1f79c56f848ac4 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 11 Jun 2026 21:22:02 -0600 Subject: [PATCH] fix --- prompt/api.py | 25 ++++----- prompt/consumers.py | 24 ++------- prompt/utils.py | 32 ++++++++++++ submission/api.py | 122 ++++++++++++++++++++++++-------------------- 4 files changed, 113 insertions(+), 90 deletions(-) create mode 100644 prompt/utils.py diff --git a/prompt/api.py b/prompt/api.py index 980d62f..d0a1c63 100644 --- a/prompt/api.py +++ b/prompt/api.py @@ -8,6 +8,7 @@ from django.contrib.auth.decorators import login_required from django.db.models import Count, Prefetch from .models import Conversation, Message +from .utils import get_preceding_user_message from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut from account.models import RoleChoices @@ -166,16 +167,15 @@ def delete_message_pair(request, message_id: int): if asst_msg.conversation.user != request.user and request.user.role != RoleChoices.SUPER: raise HttpError(403, "只能删除自己的消息") + if asst_msg.submission_id and request.user.role != RoleChoices.SUPER: + from submission.models import Rating, SubmissionAward + has_ratings = Rating.objects.filter(submission_id=asst_msg.submission_id).exists() + has_awards = SubmissionAward.objects.filter(submission_id=asst_msg.submission_id).exists() + if has_ratings or has_awards: + raise HttpError(400, "该消息关联的提交已被评分或获奖,无法删除") + # Find the preceding user message - user_msg = ( - Message.objects.filter( - conversation=asst_msg.conversation, - created__lt=asst_msg.created, - role="user", - ) - .order_by("-created") - .first() - ) + user_msg = get_preceding_user_message(asst_msg) # Delete messages first, then submission submission_id = asst_msg.submission_id # capture before deletion nulls it @@ -187,10 +187,7 @@ def delete_message_pair(request, message_id: int): submission_deleted = False if submission_id: from submission.models import Submission as SubmissionModel - try: - SubmissionModel.objects.filter(id=submission_id).delete() - submission_deleted = True - except Exception: - pass + SubmissionModel.objects.filter(id=submission_id).delete() + submission_deleted = True return {"deleted": True, "submission_deleted": submission_deleted} diff --git a/prompt/consumers.py b/prompt/consumers.py index 185ed30..e655df0 100644 --- a/prompt/consumers.py +++ b/prompt/consumers.py @@ -1,8 +1,8 @@ import json from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async -from django.db.models import Count -from .models import Conversation, Message +from .models import Message +from .utils import get_or_create_active_conversation from .llm import stream_chat, extract_code, stream_guidance, parse_guidance_response @@ -79,15 +79,7 @@ class PromptConsumer(AsyncWebsocketConsumer): @database_sync_to_async def get_or_create_conversation(self): - conv = ( - Conversation.objects.filter(user=self.user, task_id=self.task_id) - .annotate(msg_count=Count("messages")) - .order_by("-msg_count", "-created") - .first() - ) - if not conv: - conv = Conversation.objects.create(user=self.user, task_id=self.task_id) - return conv + return get_or_create_active_conversation(self.user, self.task_id) @database_sync_to_async def delete_message(self, message): @@ -195,15 +187,7 @@ class GuidanceConsumer(AsyncWebsocketConsumer): @database_sync_to_async def get_or_create_conversation(self): - conv = ( - Conversation.objects.filter(user=self.user, task_id=self.task_id) - .annotate(msg_count=Count("messages")) - .order_by("-msg_count", "-created") - .first() - ) - if not conv: - conv = Conversation.objects.create(user=self.user, task_id=self.task_id) - return conv + return get_or_create_active_conversation(self.user, self.task_id) @database_sync_to_async def delete_message(self, message): diff --git a/prompt/utils.py b/prompt/utils.py new file mode 100644 index 0000000..a52bada --- /dev/null +++ b/prompt/utils.py @@ -0,0 +1,32 @@ +from django.db.models import Count, Q +from .models import Conversation + + +def get_active_conversation(user, task_id): + """Return the conversation with the most messages for this user+task, or None.""" + return ( + Conversation.objects.filter(user=user, task_id=task_id) + .annotate(msg_count=Count("messages")) + .order_by("-msg_count", "-created") + .first() + ) + + +def get_or_create_active_conversation(user, task_id): + conv = get_active_conversation(user, task_id) + if not conv: + conv = Conversation.objects.create(user=user, task_id=task_id) + return conv + + +def get_preceding_user_message(asst_msg): + """Return the user message immediately preceding an assistant message in its conversation.""" + return ( + asst_msg.conversation.messages.filter(role="user") + .filter( + Q(created__lt=asst_msg.created) + | Q(created=asst_msg.created, id__lt=asst_msg.id) + ) + .order_by("-created", "-id") + .first() + ) diff --git a/submission/api.py b/submission/api.py index 80dfb6e..be0dcd7 100644 --- a/submission/api.py +++ b/submission/api.py @@ -1,4 +1,5 @@ import csv +import logging import threading from typing import List, Literal, Optional from urllib.parse import quote @@ -23,7 +24,8 @@ from django.db.models import ( from django.utils import timezone from account.decorators import admin_required, super_required from prompt.models import Conversation, Message -from .classifier import classify_conversation_messages +from prompt.utils import get_active_conversation, get_preceding_user_message +from .classifier import classify_message from .schemas import ( @@ -58,6 +60,7 @@ from task.models import Task from account.models import RoleChoices, User router = Router() +logger = logging.getLogger(__name__) def _validate_item_ordering(value: str): @@ -131,20 +134,19 @@ def create_submission(request, payload: SubmissionIn): task = get_object_or_404(Task, id=payload.task_id) manual_asst_msg = None + linked_msg = None + new_user_msg_id = None + if payload.prompt: - conversation = ( - Conversation.objects.filter(user=request.user, task=task) - .annotate(msg_count=Count("messages")) - .order_by("-msg_count", "-created") - .first() - ) + conversation = get_active_conversation(request.user, task.id) if not conversation: conversation = Conversation.objects.create( user=request.user, task=task, is_active=False ) - Message.objects.create( + user_msg = Message.objects.create( conversation=conversation, role="user", content=payload.prompt, source="manual" ) + new_user_msg_id = user_msg.id manual_asst_msg = Message.objects.create( conversation=conversation, role="assistant", @@ -154,48 +156,60 @@ def create_submission(request, payload: SubmissionIn): code_js=payload.js, source="manual", ) + elif payload.message_id: + linked_msg = Message.objects.filter( + id=payload.message_id, + role="assistant", + conversation__user=request.user, + conversation__task=task, + ).first() + if linked_msg is None: + logger.warning( + "create_submission: message_id %s not found for user %s task %s", + payload.message_id, request.user.id, task.id, + ) + else: + user_msg = get_preceding_user_message(linked_msg) + if user_msg: + new_user_msg_id = user_msg.id + + # Idempotent: re-submitting the same AI message updates its existing submission + # instead of creating an orphaned duplicate. + if linked_msg and linked_msg.submission_id: + submission = linked_msg.submission + submission.html = payload.html + submission.css = payload.css + submission.js = payload.js + submission.save(update_fields=["html", "css", "js"]) else: - conversation = ( - Conversation.objects.filter(user=request.user, task=task) - .annotate(msg_count=Count("messages")) - .order_by("-msg_count", "-created") - .first() + submission = Submission.objects.create( + user=request.user, + task=task, + html=payload.html, + css=payload.css, + js=payload.js, ) - if conversation: - threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start() + # Link assistant message to submission + if manual_asst_msg: + manual_asst_msg.submission = submission + manual_asst_msg.save(update_fields=["submission"]) + elif linked_msg: + linked_msg.submission = submission + linked_msg.save(update_fields=["submission"]) - submission = Submission.objects.create( - user=request.user, - task=task, - html=payload.html, - css=payload.css, - js=payload.js, - ) + # Mark any showcased older submissions from same user+task as stale + SubmissionAward.objects.filter( + submission__user=request.user, + submission__task=task, + is_stale=False, + ).exclude(submission=submission).update(is_stale=True) - # Link assistant message to submission - if manual_asst_msg: - manual_asst_msg.submission = submission - manual_asst_msg.save(update_fields=["submission"]) - elif payload.message_id: - try: - msg = Message.objects.get( - id=payload.message_id, - role="assistant", - conversation__user=request.user, - conversation__task=task, - ) - msg.submission = submission - msg.save(update_fields=["submission"]) - except Message.DoesNotExist: - pass # invalid message_id — submission already created, silently skip - - # Mark any showcased older submissions from same user+task as stale - SubmissionAward.objects.filter( - submission__user=request.user, - submission__task=task, - is_stale=False, - ).exclude(submission=submission).update(is_stale=True) + # Classify only the newly-added prompt message, and only if not already classified + if new_user_msg_id is not None and not Message.objects.filter( + id=new_user_msg_id, prompt_level__isnull=False + ).exists(): + threading.Thread(target=classify_message, args=(new_user_msg_id,), daemon=True).start() return {"id": str(submission.id)} @@ -341,19 +355,15 @@ def delete_submission(request, submission_id: UUID): if submission.user != request.user and request.user.role != RoleChoices.SUPER: raise HttpError(403, "只能删除自己的提交") + if request.user.role != RoleChoices.SUPER: + has_ratings = Rating.objects.filter(submission=submission).exists() + has_awards = SubmissionAward.objects.filter(submission=submission).exists() + if has_ratings or has_awards: + raise HttpError(400, "该提交已被评分或获奖,无法删除") + # 找到关联的助手消息,再找前一条用户消息 asst_msg = Message.objects.filter(submission=submission).first() - user_msg = None - if asst_msg: - user_msg = ( - Message.objects.filter( - conversation=asst_msg.conversation, - created__lt=asst_msg.created, - role="user", - ) - .order_by("-created") - .first() - ) + user_msg = get_preceding_user_message(asst_msg) if asst_msg else None submission.delete() # CASCADE 自动删除关联的 asst_msg