From d39e3fe2d5474a2fca13214b3957659c98f71d56 Mon Sep 17 00:00:00 2001 From: bruce Date: Sun, 7 Jun 2026 09:35:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(regulatory):=20=E5=A2=9E=E5=8A=A0mock?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../regulatory_review/services/export.py | 33 +++++++- .../services/feishu_notifier.py | 39 +++++++++ review_agent/regulatory_review/workflow.py | 2 + tests/test_regulatory_notification.py | 79 +++++++++++++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 review_agent/regulatory_review/services/feishu_notifier.py create mode 100644 tests/test_regulatory_notification.py diff --git a/review_agent/regulatory_review/services/export.py b/review_agent/regulatory_review/services/export.py index c9aba09..9a84eb8 100644 --- a/review_agent/regulatory_review/services/export.py +++ b/review_agent/regulatory_review/services/export.py @@ -75,6 +75,13 @@ def build_markdown_report(batch: RegulatoryReviewBatch) -> str: passed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_PASSED) failed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_FAILED) lines.append(f"| {record.get('file_summary_batch_no')} | {len(items)} | {passed} | {failed} |") + notifications = _notification_records(batch) + if notifications: + lines.extend(["", "## 通知记录", "", "| 渠道 | 对象 | 状态 | 问题 |", "| --- | --- | --- | --- |"]) + for record in notifications: + lines.append( + f"| {record['channel']} | {record['target'] or '-'} | {record['status']} | {record['payload'].get('title', '-')} |" + ) return "\n".join(lines) @@ -99,6 +106,7 @@ def build_result_payload(batch: RegulatoryReviewBatch) -> dict[str, object]: for issue in batch.issues.order_by("id") ], "review_records": _review_records(batch), + "notifications": _notification_records(batch), } @@ -159,7 +167,7 @@ def _create_excel_export(batch: RegulatoryReviewBatch, path: Path) -> ExportedSu workbook = Workbook() sheet = workbook.active sheet.title = "法规问题清单" - sheet.append(["等级", "类别", "规则", "问题", "状态", "建议", "法规依据"]) + sheet.append(["等级", "类别", "规则", "问题", "状态", "建议", "法规依据", "通知记录"]) for issue in batch.issues.order_by("id"): sheet.append( [ @@ -170,6 +178,7 @@ def _create_excel_export(batch: RegulatoryReviewBatch, path: Path) -> ExportedSu issue.status, issue.suggestion, "; ".join(str(item.get("source", "")) for item in issue.citations), + _notification_summary_for_issue(batch, issue.pk), ] ) workbook.save(path) @@ -192,3 +201,25 @@ def _review_records(batch: RegulatoryReviewBatch) -> list[dict[str, object]]: except (OSError, json.JSONDecodeError): continue return records + + +def _notification_records(batch: RegulatoryReviewBatch) -> list[dict[str, object]]: + return [ + { + "channel": record.channel, + "target": record.target, + "status": record.status, + "payload": record.payload, + "sent_at": record.sent_at.isoformat() if record.sent_at else "", + } + for record in batch.notifications.order_by("created_at", "id") + ] + + +def _notification_summary_for_issue(batch: RegulatoryReviewBatch, issue_id: int) -> str: + records = [ + record + for record in batch.notifications.all() + if isinstance(record.payload, dict) and record.payload.get("issue_id") == issue_id + ] + return "; ".join(f"{record.channel}:{record.status}" for record in records) diff --git a/review_agent/regulatory_review/services/feishu_notifier.py b/review_agent/regulatory_review/services/feishu_notifier.py new file mode 100644 index 0000000..10cd4f8 --- /dev/null +++ b/review_agent/regulatory_review/services/feishu_notifier.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from django.utils import timezone + +from review_agent.models import RegulatoryNotificationRecord, RegulatoryReviewBatch + + +NOTIFIABLE_SEVERITIES = {"blocking", "high", "medium"} + + +def create_mock_notifications(batch: RegulatoryReviewBatch) -> list[RegulatoryNotificationRecord]: + records = [] + existing_issue_ids = { + item.get("issue_id") + for item in RegulatoryNotificationRecord.objects.filter(batch=batch, channel=RegulatoryNotificationRecord.Channel.MOCK).values_list( + "payload", flat=True + ) + if isinstance(item, dict) + } + for issue in batch.issues.order_by("id"): + if issue.severity not in NOTIFIABLE_SEVERITIES or issue.pk in existing_issue_ids: + continue + records.append( + RegulatoryNotificationRecord.objects.create( + batch=batch, + channel=RegulatoryNotificationRecord.Channel.MOCK, + target="法规整改负责人", + status=RegulatoryNotificationRecord.Status.SENT, + sent_at=timezone.now(), + payload={ + "issue_id": issue.pk, + "rule_code": issue.rule_code, + "severity": issue.severity, + "title": issue.title, + "suggestion": issue.suggestion, + }, + ) + ) + return records diff --git a/review_agent/regulatory_review/workflow.py b/review_agent/regulatory_review/workflow.py index f89ff8f..6a0e135 100644 --- a/review_agent/regulatory_review/workflow.py +++ b/review_agent/regulatory_review/workflow.py @@ -20,6 +20,7 @@ from review_agent.models import ( from review_agent.regulatory_review.services.completeness_check import run_completeness_check from review_agent.regulatory_review.services.consistency_check import run_consistency_check from review_agent.regulatory_review.services.export import build_assistant_summary, export_review_results +from review_agent.regulatory_review.services.feishu_notifier import create_mock_notifications from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates from review_agent.regulatory_review.services.risk_assess import persist_findings from review_agent.regulatory_review.services.rule_loader import load_rule_file @@ -195,6 +196,7 @@ class RegulatoryWorkflowExecutor: return if node_code == "risk_assess": issues = persist_findings(self.batch, self.findings) + create_mock_notifications(self.batch) save_artifact( self.batch, name="rag_result_json.json", diff --git a/tests/test_regulatory_notification.py b/tests/test_regulatory_notification.py new file mode 100644 index 0000000..e9c51f6 --- /dev/null +++ b/tests/test_regulatory_notification.py @@ -0,0 +1,79 @@ +import pytest + +from review_agent.models import ( + Conversation, + FileSummaryBatch, + RegulatoryIssue, + RegulatoryNotificationRecord, + RegulatoryReviewBatch, +) +from review_agent.regulatory_review.services.export import build_markdown_report, build_result_payload +from review_agent.regulatory_review.services.feishu_notifier import create_mock_notifications + + +pytestmark = pytest.mark.django_db + + +def test_create_mock_notifications_for_medium_and_above(django_user_model): + user = django_user_model.objects.create_user(username="owner", password="pass") + conversation = Conversation.objects.create(user=user, title="会话") + summary = FileSummaryBatch.objects.create( + conversation=conversation, + user=user, + batch_no="FS-NOTIFY", + status=FileSummaryBatch.Status.SUCCESS, + ) + batch = RegulatoryReviewBatch.objects.create( + conversation=conversation, + user=user, + source_summary_batch=summary, + batch_no="RR-NOTIFY", + ) + high = RegulatoryIssue.objects.create( + batch=batch, + rule_code="attachment4_1_2_application_form", + category=RegulatoryIssue.Category.COMPLETENESS, + severity=RegulatoryIssue.Severity.HIGH, + title="缺少申请表", + ) + RegulatoryIssue.objects.create( + batch=batch, + rule_code="info", + category=RegulatoryIssue.Category.RAG, + severity=RegulatoryIssue.Severity.INFO, + title="提示项", + ) + + records = create_mock_notifications(batch) + + assert len(records) == 1 + assert records[0].channel == RegulatoryNotificationRecord.Channel.MOCK + assert records[0].status == RegulatoryNotificationRecord.Status.SENT + assert records[0].payload["issue_id"] == high.pk + + +def test_notification_records_enter_reports(django_user_model): + user = django_user_model.objects.create_user(username="owner", password="pass") + conversation = Conversation.objects.create(user=user, title="会话") + summary = FileSummaryBatch.objects.create( + conversation=conversation, + user=user, + batch_no="FS-NOTIFY", + status=FileSummaryBatch.Status.SUCCESS, + ) + batch = RegulatoryReviewBatch.objects.create( + conversation=conversation, + user=user, + source_summary_batch=summary, + batch_no="RR-NOTIFY", + ) + RegulatoryNotificationRecord.objects.create( + batch=batch, + channel=RegulatoryNotificationRecord.Channel.MOCK, + target="法规整改负责人", + status=RegulatoryNotificationRecord.Status.SENT, + payload={"title": "缺少申请表", "severity": "high"}, + ) + + assert "通知记录" in build_markdown_report(batch) + assert build_result_payload(batch)["notifications"][0]["channel"] == "mock"