From 24446658adf48d783d53e0f44fbaae2bc91aa74a Mon Sep 17 00:00:00 2001 From: bruce Date: Thu, 4 Jun 2026 01:21:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E9=A3=8E=E9=99=A9=E4=B8=8E=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audit/services.py | 26 ++++++++++ apps/audit/views.py | 17 ++++++- templates/audit/log_list.html | 38 ++++++++++----- tests/test_audit.py | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 14 deletions(-) diff --git a/apps/audit/services.py b/apps/audit/services.py index 7a0ef0e..93b4ed4 100644 --- a/apps/audit/services.py +++ b/apps/audit/services.py @@ -94,3 +94,29 @@ def create_notification_record( web_detail_url=web_detail_url, receipt=receipt, ) + + +def build_history_rows(logs) -> list[dict]: + """ + 为处理历史列表补齐风险状态和通知状态。 + + View 只负责收集筛选条件,列表展示所需的聚合字段统一在服务层完成。 + """ + notification_map = { + (item.batch_id, item.conversation_id): item + for item in NotificationRecord.objects.order_by("-created_at") + } + rows = [] + for log in logs: + notification = notification_map.get((log.batch_id, log.conversation_id)) + structured_output = log.structured_output or {} + rows.append( + { + "log": log, + "risk_status": structured_output.get("highest_risk_level") + or structured_output.get("risk_level") + or "-", + "notify_status": notification.message_status if notification else "-", + } + ) + return rows diff --git a/apps/audit/views.py b/apps/audit/views.py index 383720f..294ff1e 100644 --- a/apps/audit/views.py +++ b/apps/audit/views.py @@ -2,24 +2,39 @@ from django.shortcuts import get_object_or_404, render from .models import AgentAuditLog, NotificationRecord from apps.chat.models import Conversation +from .services import build_history_rows def log_list(request): # 处理历史页支持按批次、产品和状态筛选。 scenario_id = (request.GET.get("scenario_id") or "").strip() keyword = (request.GET.get("keyword") or "").strip() + notify_status = (request.GET.get("notify_status") or "").strip() logs = AgentAuditLog.objects.all() if scenario_id: logs = logs.filter(scenario_id=scenario_id) if keyword: logs = logs.filter(product_name__icontains=keyword) | logs.filter(batch_id__icontains=keyword) + if notify_status: + matched_pairs = list( + NotificationRecord.objects.filter(message_status=notify_status).values_list( + "batch_id", + "conversation_id", + ) + ) + logs = [ + log + for log in logs + if (log.batch_id, log.conversation_id) in matched_pairs + ] return render( request, "audit/log_list.html", { - "logs": logs, + "history_rows": build_history_rows(logs), "selected_scenario_id": scenario_id, "keyword": keyword, + "notify_status": notify_status, }, ) diff --git a/templates/audit/log_list.html b/templates/audit/log_list.html index 85fd354..ecdbda6 100644 --- a/templates/audit/log_list.html +++ b/templates/audit/log_list.html @@ -21,7 +21,15 @@ -
+
+ + +
+
清空
@@ -46,29 +54,33 @@ 会话 输入摘要 状态 + 风险状态 + 通知状态 模型 时间 详情 - {% for log in logs %} + {% for row in history_rows %} - {{ log.id }} - {{ log.scenario_name }} - {{ log.product_name|default:"-" }} - {{ log.batch_id|default:"-" }} - {{ log.conversation_id|default:"-" }} - {{ log.get_user_input_summary }} + {{ row.log.id }} + {{ row.log.scenario_name }} + {{ row.log.product_name|default:"-" }} + {{ row.log.batch_id|default:"-" }} + {{ row.log.conversation_id|default:"-" }} + {{ row.log.get_user_input_summary }} - {{ log.get_status_display_text }} + {{ row.log.get_status_display_text }} - {{ log.model_name }} - {{ log.created_at|date:"Y-m-d H:i" }} - 查看详情 + {{ row.risk_status }} + {{ row.notify_status }} + {{ row.log.model_name }} + {{ row.log.created_at|date:"Y-m-d H:i" }} + 查看详情 {% empty %} - 暂无处理历史,先去执行一次审核任务。 + 暂无处理历史,先去执行一次审核任务。 {% endfor %} diff --git a/tests/test_audit.py b/tests/test_audit.py index 36d690b..9ffe012 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -197,6 +197,95 @@ def test_audit_list_supports_batch_and_product_filters(client, db): assert "产品B" not in content +def test_audit_list_shows_risk_and_notification_status(client, db): + create_audit_log( + "document_review", + "注册审核智能体", + "问题一", + AgentResult( + answer="回答一", + status="success", + structured_output={"highest_risk_level": "high"}, + ), + batch_id="SUB-20260604-001", + conversation_id="conv-001", + product_name="产品A", + ) + create_notification_record( + batch_id="SUB-20260604-001", + conversation_id="conv-001", + product_name="产品A", + trigger_source="risk_report", + notify_reason="task_completed", + owner_role="注册资料负责人", + feishu_user_id="ou_demo_1", + message_status="sent", + web_detail_url="https://example.com/detail/1", + receipt={"message_id": "msg-1"}, + ) + + response = client.get(reverse("audit:list")) + + content = response.content.decode("utf-8") + assert response.status_code == 200 + assert "风险状态" in content + assert "high" in content + assert "通知状态" in content + assert "sent" in content + + +def test_audit_list_can_filter_by_notification_status(client, db): + create_audit_log( + "document_review", + "注册审核智能体", + "问题一", + AgentResult(answer="回答一", status="success"), + batch_id="SUB-20260604-001", + conversation_id="conv-001", + product_name="产品A", + ) + create_audit_log( + "document_review", + "注册审核智能体", + "问题二", + AgentResult(answer="回答二", status="failed"), + batch_id="SUB-20260604-002", + conversation_id="conv-002", + product_name="产品B", + ) + create_notification_record( + batch_id="SUB-20260604-001", + conversation_id="conv-001", + product_name="产品A", + trigger_source="risk_report", + notify_reason="task_completed", + owner_role="注册资料负责人", + feishu_user_id="ou_demo_1", + message_status="sent", + web_detail_url="https://example.com/detail/1", + receipt={"message_id": "msg-1"}, + ) + create_notification_record( + batch_id="SUB-20260604-002", + conversation_id="conv-002", + product_name="产品B", + trigger_source="risk_report", + notify_reason="task_failed", + owner_role="注册资料负责人", + feishu_user_id="ou_demo_2", + message_status="failed", + web_detail_url="https://example.com/detail/2", + receipt={"message_id": "msg-2"}, + ) + + response = client.get(reverse("audit:list"), {"notify_status": "failed"}) + + content = response.content.decode("utf-8") + assert response.status_code == 200 + assert "产品B" in content + assert "产品A" not in content + + def test_audit_detail_page_shows_conversation_node_results(client, db): Conversation.objects.create( conversation_id="conv-001",