From 1b4a10b5baa3aab97ab3dc4f5d0314be536108db Mon Sep 17 00:00:00 2001 From: bruce Date: Sun, 7 Jun 2026 12:17:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(regulatory):=20=E8=87=AA=E5=8A=A8=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E6=B3=95=E8=A7=84=E6=A0=B8=E6=9F=A5=E5=89=8D=E7=BD=AE?= =?UTF-8?q?=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- review_agent/services.py | 53 +++++++++++++++++++++++------ static/js/app.js | 14 ++++++-- tests/test_file_summary_frontend.py | 10 ++++++ tests/test_regulatory_workflow.py | 47 ++++++++++++++++++++++++- 4 files changed, 110 insertions(+), 14 deletions(-) diff --git a/review_agent/services.py b/review_agent/services.py index 9ac3729..de72857 100644 --- a/review_agent/services.py +++ b/review_agent/services.py @@ -10,7 +10,7 @@ from django.utils import timezone from .file_summary.skills.attachment_reader import AttachmentReaderSkill from .file_summary.workflow import create_file_summary_batch, start_file_summary_workflow from .llm import LLMConfigurationError, LLMRequestError, generate_reply, stream_reply -from .models import Conversation, FileAttachment, Message +from .models import Conversation, FileAttachment, FileSummaryBatch, Message from .regulatory_review.workflow import ( create_regulatory_review_batch, find_latest_successful_summary_batch, @@ -227,18 +227,51 @@ def stream_message(conversation: Conversation, content: str): if route.starts_regulatory_review: source_summary_batch = find_latest_successful_summary_batch(conversation) if not source_summary_batch: - reply_content = "请先执行自动汇总,生成成功的文件汇总批次后再启动法规核查。" - assistant_message = append_assistant_message(conversation, reply_content) - yield sse_event("chunk", {"delta": reply_content}) + if not _has_active_attachments(conversation): + reply_content = "请先在当前对话右侧上传需要核查的文件或压缩包,我会先自动汇总再继续法规核查。" + assistant_message = append_assistant_message(conversation, reply_content) + yield sse_event("chunk", {"delta": reply_content}) + yield sse_event( + "done", + { + "assistant_message_id": assistant_message.pk, + "conversation_id": conversation.pk, + "title": conversation.title, + }, + ) + return + summary_batch = create_file_summary_batch( + conversation=conversation, + user=conversation.user, + trigger_message=user_message, + ) yield sse_event( - "done", + "workflow_started", { - "assistant_message_id": assistant_message.pk, - "conversation_id": conversation.pk, - "title": conversation.title, + "workflow_type": "file_summary", + "batch_id": summary_batch.pk, + "batch_no": summary_batch.batch_no, }, ) - return + start_file_summary_workflow(summary_batch, async_run=False) + summary_batch.refresh_from_db() + if summary_batch.status != FileSummaryBatch.Status.SUCCESS: + reply_content = f"已先启动文件目录与页数自动汇总工作流,批次号:{summary_batch.batch_no},但汇总未成功:{summary_batch.error_message or '原因待查看'}。请处理后再启动法规核查。" + assistant_message = append_assistant_message(conversation, reply_content) + yield sse_event("chunk", {"delta": reply_content}) + yield sse_event( + "done", + { + "assistant_message_id": assistant_message.pk, + "conversation_id": conversation.pk, + "title": conversation.title, + }, + ) + return + source_summary_batch = summary_batch + reply_prefix = f"已先启动文件目录与页数自动汇总工作流,批次号:{summary_batch.batch_no},汇总完成后继续法规核查。\n" + else: + reply_prefix = "" batch = create_regulatory_review_batch( conversation=conversation, user=conversation.user, @@ -249,7 +282,7 @@ def stream_message(conversation: Conversation, content: str): batch, async_run=getattr(settings, "REGULATORY_REVIEW_ASYNC", True), ) - reply_content = f"已启动 NMPA 注册资料法规核查工作流,批次号:{batch.batch_no}。" + reply_content = f"{reply_prefix}已启动 NMPA 注册资料法规核查工作流,批次号:{batch.batch_no}。" assistant_message = append_assistant_message(conversation, reply_content) yield sse_event( "workflow_started", diff --git a/static/js/app.js b/static/js/app.js index 675ac08..d1d4c60 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -310,7 +310,7 @@ function appendConversationMessage(message) { if (!message || document.querySelector('.message[data-message-id="' + message.id + '"]')) { - return; + return false; } var label = message.role === "assistant" ? "AI " : "用户 "; label += document.querySelectorAll(".message").length + 1; @@ -320,6 +320,7 @@ if (message.role === "user") { appendNode(created.article.id, label, true); } + return true; } async function refreshConversationMessages() { @@ -337,14 +338,21 @@ return; } var payload = await response.json(); - (payload.messages || []).forEach(appendConversationMessage); + var appendedCount = 0; + (payload.messages || []).forEach(function (message) { + if (appendConversationMessage(message)) { + appendedCount += 1; + } + }); if (payload.latest_message_id) { latestMessageId = Math.max(latestMessageId, payload.latest_message_id); } syncNodeRailVisibility(); bindNodeAnchorClicks(); setActiveNode(); - scrollChatToBottom(); + if (appendedCount > 0) { + scrollChatToBottom(); + } } catch (error) { console.error("Conversation message refresh failed", error); } diff --git a/tests/test_file_summary_frontend.py b/tests/test_file_summary_frontend.py index 87b3a88..cd5473d 100644 --- a/tests/test_file_summary_frontend.py +++ b/tests/test_file_summary_frontend.py @@ -187,6 +187,16 @@ def test_frontend_refreshes_generated_workflow_messages(): assert "data-message-url-template" in script +def test_frontend_only_scrolls_after_appending_new_messages(): + script = open("static/js/app.js", encoding="utf-8").read() + + assert "return false;" in script + assert "return true;" in script + assert "var appendedCount = 0;" in script + assert "if (appendConversationMessage(message))" in script + assert "if (appendedCount > 0)" in script + + def test_frontend_can_replace_partial_stream_content(): script = open("static/js/app.js", encoding="utf-8").read() diff --git a/tests/test_regulatory_workflow.py b/tests/test_regulatory_workflow.py index 893b103..76b0753 100644 --- a/tests/test_regulatory_workflow.py +++ b/tests/test_regulatory_workflow.py @@ -3,6 +3,7 @@ import pytest from review_agent.models import ( Conversation, ExportedSummaryFile, + FileAttachment, FileSummaryBatch, FileSummaryItem, Message, @@ -132,10 +133,54 @@ def test_stream_message_prompts_for_summary_when_missing(monkeypatch, django_use frames = list(stream_message(conversation, "请做法规核查")) joined = "".join(frames) - assert "请先执行自动汇总" in joined + assert "请先在当前对话右侧上传需要核查的文件或压缩包" in joined + assert "我会先自动汇总再继续法规核查" in joined assert not RegulatoryReviewBatch.objects.exists() +def test_stream_message_auto_runs_summary_before_regulatory_review( + monkeypatch, settings, tmp_path, django_user_model +): + settings.MEDIA_ROOT = tmp_path + settings.REGULATORY_REVIEW_ASYNC = False + user = django_user_model.objects.create_user(username="owner", password="pass") + conversation = Conversation.objects.create(user=user, title="会话") + attachment_path = tmp_path / "application.txt" + attachment_path.write_text("产品名称:甲胎蛋白检测试剂盒", encoding="utf-8") + FileAttachment.objects.create( + conversation=conversation, + user=user, + original_name="application.txt", + storage_path=str(attachment_path), + file_size=attachment_path.stat().st_size, + is_active=True, + ) + monkeypatch.setattr( + "review_agent.services.route_message_intent", + lambda conversation, content: SkillRoute( + action="regulatory_review", + workflow_type="regulatory_review", + confidence=0.9, + ), + ) + + def finish_summary(batch, async_run=True): + batch.status = FileSummaryBatch.Status.SUCCESS + batch.save(update_fields=["status"]) + + monkeypatch.setattr("review_agent.services.start_file_summary_workflow", finish_summary) + + frames = list(stream_message(conversation, "进行第一章NMPA 法规核查")) + joined = "".join(frames) + + assert "\"workflow_type\": \"file_summary\"" in joined + assert "\"workflow_type\": \"regulatory_review\"" in joined + assert "已先启动文件目录与页数自动汇总工作流" in joined + assert FileSummaryBatch.objects.filter(conversation=conversation, status=FileSummaryBatch.Status.SUCCESS).exists() + regulatory = RegulatoryReviewBatch.objects.get(conversation=conversation) + assert regulatory.condition_json["rule_scope"]["attachment4_chapter"] == "1" + + def test_stream_message_starts_regulatory_workflow(monkeypatch, settings, django_user_model): settings.REGULATORY_REVIEW_ASYNC = False user = django_user_model.objects.create_user(username="owner", password="pass")