From f0286264e27e030f4d8685aef635c0034e503fd3 Mon Sep 17 00:00:00 2001 From: bruce Date: Wed, 10 Jun 2026 19:49:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(regulatory-info-package):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=9D=90=E6=96=99=E5=8C=85=E6=95=B0=E6=8D=AE=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- review_agent/file_summary/views.py | 18 + ..._regulatoryinfopackageartifact_and_more.py | 388 ++++++++++++++++++ review_agent/models.py | 187 ++++++++- 3 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 review_agent/migrations/0009_regulatoryinfopackageartifact_and_more.py diff --git a/review_agent/file_summary/views.py b/review_agent/file_summary/views.py index b71ed75..f475d95 100644 --- a/review_agent/file_summary/views.py +++ b/review_agent/file_summary/views.py @@ -14,6 +14,7 @@ from review_agent.models import ( ExportedSummaryFile, FileAttachment, Message, + RegulatoryInfoPackageBatch, RegulatoryReviewBatch, ) from review_agent.models import FileSummaryBatch, WorkflowEvent @@ -304,14 +305,20 @@ def export_download(request, export_id: int): extra={"export_id": exported.pk, "storage_path": exported.storage_path}, ) return JsonResponse({"error": "文件不存在。"}, status=404) + suffix = Path(exported.file_name).suffix.lower() content_types = { ExportedSummaryFile.ExportType.MARKDOWN: "text/markdown; charset=utf-8", ExportedSummaryFile.ExportType.EXCEL: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ExportedSummaryFile.ExportType.JSON: "application/json; charset=utf-8", ExportedSummaryFile.ExportType.WORD: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ExportedSummaryFile.ExportType.PDF: "application/pdf", + ExportedSummaryFile.ExportType.ZIP: "application/zip", } content_type = content_types.get(exported.export_type, "application/octet-stream") + if exported.export_type == ExportedSummaryFile.ExportType.WORD and suffix == ".doc": + content_type = "application/msword" + elif exported.export_type == ExportedSummaryFile.ExportType.WORD and suffix == ".docx": + content_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" logger.info( "Export download started", extra={ @@ -342,6 +349,17 @@ def _export_for_user(user, export_id: int) -> ExportedSummaryFile | None: is_deleted=False, ).exists() return exported if allowed else None + if exported.workflow_type == "regulatory_info_package": + if not exported.workflow_batch_id: + return None + allowed = RegulatoryInfoPackageBatch.objects.filter( + pk=exported.workflow_batch_id, + conversation__user=user, + is_deleted=False, + ).exists() + return exported if allowed else None + if exported.batch_id is None: + return None if exported.batch.user_id != user.pk: return None return exported diff --git a/review_agent/migrations/0009_regulatoryinfopackageartifact_and_more.py b/review_agent/migrations/0009_regulatoryinfopackageartifact_and_more.py new file mode 100644 index 0000000..c36473d --- /dev/null +++ b/review_agent/migrations/0009_regulatoryinfopackageartifact_and_more.py @@ -0,0 +1,388 @@ +# Generated by Django 5.2.14 on 2026-06-10 11:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("review_agent", "0008_knowledgebasedocument"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="RegulatoryInfoPackageArtifact", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "artifact_type", + models.CharField( + choices=[ + ("template_copy", "模板副本"), + ("instruction_extract", "说明书抽取结果"), + ("field_extract_result", "字段抽取结果"), + ("merged_fields", "合并字段"), + ("generated_document", "生成文件"), + ("traceability", "追溯清单"), + ("zip_package", "ZIP包"), + ("notification_record", "通知记录"), + ], + max_length=60, + ), + ), + ( + "file_format", + models.CharField( + choices=[ + ("json", "JSON"), + ("excel", "Excel"), + ("docx", "DOCX"), + ("doc", "DOC"), + ("zip", "ZIP"), + ("markdown", "Markdown"), + ], + max_length=20, + ), + ), + ("name", models.CharField(max_length=160)), + ("file_name", models.CharField(max_length=255)), + ("storage_path", models.CharField(max_length=500)), + ("file_size", models.BigIntegerField(default=0)), + ( + "content_hash", + models.CharField(blank=True, default="", max_length=128), + ), + ("metadata", models.JSONField(blank=True, default=dict)), + ( + "created_by_node", + models.CharField(blank=True, default="", max_length=60), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("is_deleted", models.BooleanField(default=False)), + ], + options={ + "db_table": "ra_regulatory_info_package_artifact", + "ordering": ["-created_at", "-id"], + }, + ), + migrations.CreateModel( + name="RegulatoryInfoPackageBatch", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "source_summary_item_id", + models.PositiveBigIntegerField(blank=True, null=True), + ), + ("batch_no", models.CharField(max_length=64, unique=True)), + ( + "status", + models.CharField( + choices=[ + ("pending", "待执行"), + ("running", "执行中"), + ("waiting_user", "等待用户"), + ("success", "成功"), + ("partial_success", "部分成功"), + ("failed", "失败"), + ("cancelled", "已取消"), + ], + default="pending", + max_length=30, + ), + ), + ( + "source_file_name", + models.CharField(blank=True, default="", max_length=255), + ), + ( + "source_storage_path", + models.CharField(blank=True, default="", max_length=500), + ), + ( + "product_name", + models.CharField(blank=True, default="", max_length=200), + ), + ( + "output_zip_name", + models.CharField( + blank=True, + default="第1章 监管信息(预生成版).zip", + max_length=255, + ), + ), + ("generated_files", models.JSONField(blank=True, default=list)), + ("missing_fields", models.JSONField(blank=True, default=list)), + ("llm_only_fields", models.JSONField(blank=True, default=list)), + ("conflict_fields", models.JSONField(blank=True, default=list)), + ("risk_notes", models.JSONField(blank=True, default=list)), + ( + "template_config_version", + models.CharField(blank=True, default="", max_length=80), + ), + ( + "template_config_hash", + models.CharField(blank=True, default="", max_length=128), + ), + ("adapter_summary", models.JSONField(blank=True, default=dict)), + ("work_dir", models.CharField(blank=True, default="", max_length=500)), + ("error_message", models.TextField(blank=True, default="")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("started_at", models.DateTimeField(blank=True, null=True)), + ("finished_at", models.DateTimeField(blank=True, null=True)), + ("archived_at", models.DateTimeField(blank=True, null=True)), + ("is_deleted", models.BooleanField(default=False)), + ], + options={ + "db_table": "ra_regulatory_info_package_batch", + "ordering": ["-created_at", "-id"], + }, + ), + migrations.CreateModel( + name="RegulatoryInfoPackageNotificationRecord", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "channel", + models.CharField( + choices=[ + ("feishu_cli", "飞书 CLI"), + ("feishu_api", "飞书 API"), + ("mock", "模拟"), + ], + default="mock", + max_length=30, + ), + ), + ("export_ids", models.JSONField(blank=True, default=list)), + ("message_summary", models.TextField(blank=True, default="")), + ( + "send_status", + models.CharField( + choices=[ + ("pending", "待发送"), + ("success", "成功"), + ("failed", "失败"), + ], + default="pending", + max_length=20, + ), + ), + ("retry_count", models.PositiveIntegerField(default=0)), + ( + "external_message_id", + models.CharField(blank=True, default="", max_length=120), + ), + ("error_message", models.TextField(blank=True, default="")), + ("sent_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("is_deleted", models.BooleanField(default=False)), + ], + options={ + "db_table": "ra_regulatory_info_package_notification_record", + "ordering": ["-created_at", "-id"], + }, + ), + migrations.AlterField( + model_name="exportedsummaryfile", + name="batch", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="exports", + to="review_agent.filesummarybatch", + ), + ), + migrations.AlterField( + model_name="exportedsummaryfile", + name="export_type", + field=models.CharField( + choices=[ + ("markdown", "Markdown"), + ("excel", "Excel"), + ("json", "JSON"), + ("word", "Word"), + ("pdf", "PDF"), + ("zip", "ZIP"), + ], + max_length=20, + ), + ), + migrations.AddConstraint( + model_name="workflownoderun", + constraint=models.UniqueConstraint( + fields=("workflow_type", "workflow_batch_id", "node_code"), + name="uq_ra_node_workflow_batch_code", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagebatch", + name="conversation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="regulatory_info_package_batches", + to="review_agent.conversation", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagebatch", + name="source_attachment", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="regulatory_info_package_batches", + to="review_agent.fileattachment", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagebatch", + name="source_summary_batch", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="regulatory_info_package_batches", + to="review_agent.filesummarybatch", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagebatch", + name="trigger_message", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="triggered_regulatory_info_package_batches", + to="review_agent.message", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagebatch", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="review_regulatory_info_package_batches", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="regulatoryinfopackageartifact", + name="batch", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="artifacts", + to="review_agent.regulatoryinfopackagebatch", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagenotificationrecord", + name="batch", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to="review_agent.regulatoryinfopackagebatch", + ), + ), + migrations.AddField( + model_name="regulatoryinfopackagenotificationrecord", + name="recipient", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="regulatory_info_package_notifications", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagebatch", + index=models.Index( + fields=["conversation", "status"], name="idx_ra_rip_batch_conv_status" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagebatch", + index=models.Index( + fields=["user", "created_at"], name="idx_ra_rip_batch_user_created" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagebatch", + index=models.Index( + fields=["source_attachment"], name="idx_ra_rip_batch_attachment" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagebatch", + index=models.Index( + fields=["source_summary_batch"], name="idx_ra_rip_batch_summary" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagebatch", + index=models.Index(fields=["created_at"], name="idx_ra_rip_batch_created"), + ), + migrations.AddIndex( + model_name="regulatoryinfopackageartifact", + index=models.Index( + fields=["batch", "artifact_type"], name="idx_ra_rip_artifact_batch_type" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackageartifact", + index=models.Index( + fields=["file_format"], name="idx_ra_rip_artifact_format" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackageartifact", + index=models.Index( + fields=["created_at"], name="idx_ra_rip_artifact_created" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagenotificationrecord", + index=models.Index( + fields=["batch", "created_at"], name="idx_ra_rip_notify_batch" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagenotificationrecord", + index=models.Index( + fields=["recipient", "send_status"], name="idx_ra_rip_notify_recipient" + ), + ), + migrations.AddIndex( + model_name="regulatoryinfopackagenotificationrecord", + index=models.Index( + fields=["send_status", "retry_count"], name="idx_ra_rip_notify_status" + ), + ), + ] diff --git a/review_agent/models.py b/review_agent/models.py index 6189a69..16da526 100644 --- a/review_agent/models.py +++ b/review_agent/models.py @@ -280,7 +280,11 @@ class WorkflowNodeRun(models.Model): class Meta: db_table = "ra_workflow_node_run" constraints = [ - models.UniqueConstraint(fields=["batch", "node_code"], name="uq_ra_node_batch_code") + models.UniqueConstraint(fields=["batch", "node_code"], name="uq_ra_node_batch_code"), + models.UniqueConstraint( + fields=["workflow_type", "workflow_batch_id", "node_code"], + name="uq_ra_node_workflow_batch_code", + ), ] indexes = [ models.Index(fields=["batch", "status"], name="idx_ra_node_batch_status"), @@ -336,6 +340,7 @@ class ExportedSummaryFile(models.Model): JSON = "json", "JSON" WORD = "word", "Word" PDF = "pdf", "PDF" + ZIP = "zip", "ZIP" class Status(models.TextChoices): SUCCESS = "success", "成功" @@ -345,6 +350,8 @@ class ExportedSummaryFile(models.Model): FileSummaryBatch, on_delete=models.CASCADE, related_name="exports", + null=True, + blank=True, ) workflow_type = models.CharField(max_length=40, blank=True, default="file_summary") workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True) @@ -524,6 +531,87 @@ class ApplicationFormFillBatch(models.Model): return self.batch_no +class RegulatoryInfoPackageBatch(models.Model): + """Tracks one Chapter 1 regulatory information package workflow run.""" + + class Status(models.TextChoices): + PENDING = "pending", "待执行" + RUNNING = "running", "执行中" + WAITING_USER = "waiting_user", "等待用户" + SUCCESS = "success", "成功" + PARTIAL_SUCCESS = "partial_success", "部分成功" + FAILED = "failed", "失败" + CANCELLED = "cancelled", "已取消" + + conversation = models.ForeignKey( + Conversation, + on_delete=models.CASCADE, + related_name="regulatory_info_package_batches", + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="review_regulatory_info_package_batches", + ) + trigger_message = models.ForeignKey( + Message, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="triggered_regulatory_info_package_batches", + ) + source_attachment = models.ForeignKey( + FileAttachment, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="regulatory_info_package_batches", + ) + source_summary_batch = models.ForeignKey( + FileSummaryBatch, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="regulatory_info_package_batches", + ) + source_summary_item_id = models.PositiveBigIntegerField(null=True, blank=True) + batch_no = models.CharField(max_length=64, unique=True) + status = models.CharField(max_length=30, choices=Status.choices, default=Status.PENDING) + source_file_name = models.CharField(max_length=255, blank=True, default="") + source_storage_path = models.CharField(max_length=500, blank=True, default="") + product_name = models.CharField(max_length=200, blank=True, default="") + output_zip_name = models.CharField(max_length=255, blank=True, default="第1章 监管信息(预生成版).zip") + generated_files = models.JSONField(default=list, blank=True) + missing_fields = models.JSONField(default=list, blank=True) + llm_only_fields = models.JSONField(default=list, blank=True) + conflict_fields = models.JSONField(default=list, blank=True) + risk_notes = models.JSONField(default=list, blank=True) + template_config_version = models.CharField(max_length=80, blank=True, default="") + template_config_hash = models.CharField(max_length=128, blank=True, default="") + adapter_summary = models.JSONField(default=dict, blank=True) + work_dir = models.CharField(max_length=500, blank=True, default="") + error_message = models.TextField(blank=True, default="") + created_at = models.DateTimeField(auto_now_add=True) + started_at = models.DateTimeField(null=True, blank=True) + finished_at = models.DateTimeField(null=True, blank=True) + archived_at = models.DateTimeField(null=True, blank=True) + is_deleted = models.BooleanField(default=False) + + class Meta: + db_table = "ra_regulatory_info_package_batch" + ordering = ["-created_at", "-id"] + indexes = [ + models.Index(fields=["conversation", "status"], name="idx_ra_rip_batch_conv_status"), + models.Index(fields=["user", "created_at"], name="idx_ra_rip_batch_user_created"), + models.Index(fields=["source_attachment"], name="idx_ra_rip_batch_attachment"), + models.Index(fields=["source_summary_batch"], name="idx_ra_rip_batch_summary"), + models.Index(fields=["created_at"], name="idx_ra_rip_batch_created"), + ] + + def __str__(self) -> str: + return self.batch_no + + class RegulatoryReviewBatch(models.Model): """Tracks one NMPA regulatory review workflow run.""" @@ -745,6 +833,54 @@ class ApplicationFormFillArtifact(models.Model): ] +class RegulatoryInfoPackageArtifact(models.Model): + """Stores regulatory information package intermediate and generated files.""" + + class ArtifactType(models.TextChoices): + TEMPLATE_COPY = "template_copy", "模板副本" + INSTRUCTION_EXTRACT = "instruction_extract", "说明书抽取结果" + FIELD_EXTRACT_RESULT = "field_extract_result", "字段抽取结果" + MERGED_FIELDS = "merged_fields", "合并字段" + GENERATED_DOCUMENT = "generated_document", "生成文件" + TRACEABILITY = "traceability", "追溯清单" + ZIP_PACKAGE = "zip_package", "ZIP包" + NOTIFICATION_RECORD = "notification_record", "通知记录" + + class FileFormat(models.TextChoices): + JSON = "json", "JSON" + EXCEL = "excel", "Excel" + DOCX = "docx", "DOCX" + DOC = "doc", "DOC" + ZIP = "zip", "ZIP" + MARKDOWN = "markdown", "Markdown" + + batch = models.ForeignKey( + RegulatoryInfoPackageBatch, + on_delete=models.CASCADE, + related_name="artifacts", + ) + artifact_type = models.CharField(max_length=60, choices=ArtifactType.choices) + file_format = models.CharField(max_length=20, choices=FileFormat.choices) + name = models.CharField(max_length=160) + file_name = models.CharField(max_length=255) + storage_path = models.CharField(max_length=500) + file_size = models.BigIntegerField(default=0) + content_hash = models.CharField(max_length=128, blank=True, default="") + metadata = models.JSONField(default=dict, blank=True) + created_by_node = models.CharField(max_length=60, blank=True, default="") + created_at = models.DateTimeField(auto_now_add=True) + is_deleted = models.BooleanField(default=False) + + class Meta: + db_table = "ra_regulatory_info_package_artifact" + ordering = ["-created_at", "-id"] + indexes = [ + models.Index(fields=["batch", "artifact_type"], name="idx_ra_rip_artifact_batch_type"), + models.Index(fields=["file_format"], name="idx_ra_rip_artifact_format"), + models.Index(fields=["created_at"], name="idx_ra_rip_artifact_created"), + ] + + class ApplicationFormFillNotificationRecord(models.Model): """Stores mock/Feishu notification records for application-form auto-fill.""" @@ -795,6 +931,55 @@ class ApplicationFormFillNotificationRecord(models.Model): ] +class RegulatoryInfoPackageNotificationRecord(models.Model): + """Stores mock/Feishu notification records for regulatory info packages.""" + + class Channel(models.TextChoices): + FEISHU_CLI = "feishu_cli", "飞书 CLI" + FEISHU_API = "feishu_api", "飞书 API" + MOCK = "mock", "模拟" + + class SendStatus(models.TextChoices): + PENDING = "pending", "待发送" + SUCCESS = "success", "成功" + FAILED = "failed", "失败" + + batch = models.ForeignKey( + RegulatoryInfoPackageBatch, + on_delete=models.CASCADE, + related_name="notifications", + ) + recipient = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="regulatory_info_package_notifications", + ) + channel = models.CharField(max_length=30, choices=Channel.choices, default=Channel.MOCK) + export_ids = models.JSONField(default=list, blank=True) + message_summary = models.TextField(blank=True, default="") + send_status = models.CharField( + max_length=20, + choices=SendStatus.choices, + default=SendStatus.PENDING, + ) + retry_count = models.PositiveIntegerField(default=0) + external_message_id = models.CharField(max_length=120, blank=True, default="") + error_message = models.TextField(blank=True, default="") + sent_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_deleted = models.BooleanField(default=False) + + class Meta: + db_table = "ra_regulatory_info_package_notification_record" + ordering = ["-created_at", "-id"] + indexes = [ + models.Index(fields=["batch", "created_at"], name="idx_ra_rip_notify_batch"), + models.Index(fields=["recipient", "send_status"], name="idx_ra_rip_notify_recipient"), + models.Index(fields=["send_status", "retry_count"], name="idx_ra_rip_notify_status"), + ] + + class FeishuUserMapping(models.Model): """Maps a system user to Feishu identifiers maintained by Admin."""