from django.conf import settings from django.db import models class Conversation(models.Model): """Stores a user's review-agent conversation shell.""" user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="review_conversations", ) title = models.CharField(max_length=120, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-updated_at", "-id"] def __str__(self) -> str: return self.title or f"对话 {self.pk}" class Message(models.Model): """Stores one user or assistant message in a conversation.""" class Role(models.TextChoices): USER = "user", "用户" ASSISTANT = "assistant", "助手" conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name="messages", ) role = models.CharField(max_length=20, choices=Role.choices) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at", "id"] def __str__(self) -> str: return f"{self.get_role_display()} - {self.conversation_id}" class FileAttachment(models.Model): """Stores an uploaded file version for one conversation.""" class UploadStatus(models.TextChoices): UPLOADED = "uploaded", "已上传" BOUND = "bound", "已绑定" DELETED = "deleted", "已删除" conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name="file_attachments", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="review_file_attachments", ) original_name = models.CharField(max_length=255) version_no = models.PositiveIntegerField(default=1) is_active = models.BooleanField(default=True) storage_path = models.CharField(max_length=500) file_size = models.BigIntegerField(default=0) content_type = models.CharField(max_length=120, blank=True, default="") upload_status = models.CharField( max_length=20, choices=UploadStatus.choices, default=UploadStatus.UPLOADED, ) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "ra_file_attachment" ordering = ["-created_at", "-id"] constraints = [ models.UniqueConstraint( fields=["conversation", "original_name", "version_no"], name="uq_ra_attachment_conv_name_version", ) ] indexes = [ models.Index( fields=["conversation", "created_at"], name="idx_ra_attachment_conv_created", ), models.Index( fields=["user", "created_at"], name="idx_ra_attachment_user_created", ), models.Index( fields=["conversation", "original_name", "is_active"], name="idx_ra_attachment_active", ), ] def __str__(self) -> str: return f"{self.original_name} v{self.version_no}" class FileSummaryBatch(models.Model): """Tracks one automatic file inventory and page-count workflow run.""" class Status(models.TextChoices): PENDING = "pending", "待执行" RUNNING = "running", "执行中" SUCCESS = "success", "成功" FAILED = "failed", "失败" conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name="file_summary_batches", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="review_file_summary_batches", ) trigger_message = models.ForeignKey( Message, on_delete=models.SET_NULL, null=True, blank=True, related_name="triggered_file_summary_batches", ) batch_no = models.CharField(max_length=64, unique=True) product_name = models.CharField(max_length=200, blank=True, default="") status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) total_files = models.IntegerField(default=0) supported_files = models.IntegerField(default=0) success_files = models.IntegerField(default=0) failed_files = models.IntegerField(default=0) unsupported_files = models.IntegerField(default=0) uncertain_files = models.IntegerField(default=0) total_pages = models.IntegerField(default=0) 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) class Meta: db_table = "ra_file_summary_batch" ordering = ["-created_at", "-id"] indexes = [ models.Index(fields=["conversation", "created_at"], name="idx_ra_batch_conv_created"), models.Index(fields=["user", "created_at"], name="idx_ra_batch_user_created"), models.Index(fields=["status", "created_at"], name="idx_ra_batch_status"), ] def __str__(self) -> str: return self.batch_no class FileSummaryBatchAttachment(models.Model): """Binds a workflow batch to the exact attachment versions it uses.""" class SourceRole(models.TextChoices): ARCHIVE = "archive", "压缩包" MULTI_FILE = "multi_file", "多文件" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, related_name="batch_attachments", ) attachment = models.ForeignKey( FileAttachment, on_delete=models.CASCADE, related_name="batch_bindings", ) source_role = models.CharField( max_length=20, choices=SourceRole.choices, default=SourceRole.MULTI_FILE, ) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "ra_file_summary_batch_attachment" constraints = [ models.UniqueConstraint( fields=["batch", "attachment"], name="uq_ra_batch_attachment", ) ] indexes = [ models.Index( fields=["batch", "created_at"], name="idx_ra_batch_attachment_batch", ), models.Index(fields=["attachment"], name="idx_ra_batch_attach_file"), ] class FileSummaryItem(models.Model): """Stores one scanned file and its page-count result.""" class StatisticsStatus(models.TextChoices): SUCCESS = "success", "成功" FAILED = "failed", "失败" UNSUPPORTED = "unsupported", "不支持" UNCERTAIN = "uncertain", "不确定" SKIPPED = "skipped", "跳过" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, related_name="items", ) file_index = models.PositiveIntegerField() directory_level = models.CharField(max_length=300, blank=True, default="") file_name = models.CharField(max_length=255) file_type = models.CharField(max_length=20) relative_path = models.CharField(max_length=500) storage_path = models.CharField(max_length=500) page_count = models.IntegerField(null=True, blank=True) statistics_status = models.CharField( max_length=20, choices=StatisticsStatus.choices, default=StatisticsStatus.SKIPPED, ) retry_count = models.PositiveIntegerField(default=0) error_message = models.TextField(blank=True, default="") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "ra_file_summary_item" ordering = ["file_index", "id"] constraints = [ models.UniqueConstraint( fields=["batch", "relative_path"], name="uq_ra_item_batch_relative_path", ) ] indexes = [ models.Index(fields=["batch", "file_index"], name="idx_ra_item_batch_index"), models.Index(fields=["batch", "statistics_status"], name="idx_ra_item_batch_status"), models.Index(fields=["batch", "file_type"], name="idx_ra_item_batch_type"), ] class WorkflowNodeRun(models.Model): """Stores recoverable status for one workflow node.""" class Status(models.TextChoices): PENDING = "pending", "等待中" RUNNING = "running", "执行中" WAITING_USER = "waiting_user", "等待用户确认" RETRYING = "retrying", "重试中" SUCCESS = "success", "成功" FAILED = "failed", "失败" SKIPPED = "skipped", "跳过" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, null=True, blank=True, related_name="node_runs", ) workflow_type = models.CharField(max_length=40, blank=True, default="file_summary") workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True) node_group = models.CharField(max_length=40, blank=True, default="file_summary") node_code = models.CharField(max_length=40) node_name = models.CharField(max_length=80) status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) progress = models.PositiveIntegerField(default=0) message = models.TextField(blank=True, default="") started_at = models.DateTimeField(null=True, blank=True) finished_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "ra_workflow_node_run" constraints = [ models.UniqueConstraint(fields=["batch", "node_code"], name="uq_ra_node_batch_code") ] indexes = [ models.Index(fields=["batch", "status"], name="idx_ra_node_batch_status"), models.Index( fields=["workflow_type", "workflow_batch_id"], name="idx_ra_node_workflow", ), ] class WorkflowEvent(models.Model): """Persists workflow events for SSE replay and diagnostics.""" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, null=True, blank=True, related_name="events", ) workflow_type = models.CharField(max_length=40, blank=True, default="file_summary") workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True) conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, null=True, blank=True, related_name="workflow_events", ) event_type = models.CharField(max_length=40) payload = models.JSONField(default=dict) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "ra_workflow_event" ordering = ["id"] indexes = [ models.Index(fields=["batch", "id"], name="idx_ra_event_batch_id"), models.Index(fields=["batch", "created_at"], name="idx_ra_event_batch_created"), models.Index( fields=["workflow_type", "workflow_batch_id", "id"], name="idx_ra_event_workflow_id", ), ] class ExportedSummaryFile(models.Model): """Stores generated report files for permission-checked download.""" class ExportType(models.TextChoices): MARKDOWN = "markdown", "Markdown" EXCEL = "excel", "Excel" JSON = "json", "JSON" class Status(models.TextChoices): SUCCESS = "success", "成功" FAILED = "failed", "失败" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, related_name="exports", ) workflow_type = models.CharField(max_length=40, blank=True, default="file_summary") workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True) export_category = models.CharField(max_length=40, blank=True, default="summary") export_type = models.CharField(max_length=20, choices=ExportType.choices) file_name = models.CharField(max_length=255) storage_path = models.CharField(max_length=500) status = models.CharField(max_length=20, choices=Status.choices, default=Status.SUCCESS) error_message = models.TextField(blank=True, default="") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "ra_exported_summary_file" ordering = ["-created_at", "-id"] indexes = [ models.Index(fields=["batch", "export_type"], name="idx_ra_export_batch_type"), models.Index(fields=["batch", "created_at"], name="idx_ra_export_batch_created"), models.Index( fields=["workflow_type", "workflow_batch_id"], name="idx_ra_export_workflow", ), ] class RegulatoryRuleVersion(models.Model): """Tracks the local regulatory rule YAML and its matching RAG index.""" class Status(models.TextChoices): ACTIVE = "active", "启用" OUTDATED = "outdated", "待更新" DISABLED = "disabled", "停用" code = models.CharField(max_length=80, unique=True) name = models.CharField(max_length=160) yaml_path = models.CharField(max_length=500) yaml_hash = models.CharField(max_length=128) rag_collection = models.CharField(max_length=120, blank=True, default="") rag_index_version = models.CharField(max_length=80, blank=True, default="") rag_index_hash = models.CharField(max_length=128, blank=True, default="") status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "ra_regulatory_rule_version" ordering = ["-updated_at", "-id"] indexes = [ models.Index(fields=["code", "status"], name="idx_ra_rule_code_status"), ] def __str__(self) -> str: return self.code class RegulatoryReviewBatch(models.Model): """Tracks one NMPA regulatory review workflow run.""" class Status(models.TextChoices): PENDING = "pending", "待执行" RUNNING = "running", "执行中" WAITING_USER = "waiting_user", "等待用户确认" SUCCESS = "success", "成功" FAILED = "failed", "失败" conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name="regulatory_review_batches", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="review_regulatory_batches", ) trigger_message = models.ForeignKey( Message, on_delete=models.SET_NULL, null=True, blank=True, related_name="triggered_regulatory_batches", ) source_summary_batch = models.ForeignKey( FileSummaryBatch, on_delete=models.PROTECT, related_name="regulatory_review_batches", ) rule_version = models.ForeignKey( RegulatoryRuleVersion, on_delete=models.SET_NULL, null=True, blank=True, related_name="review_batches", ) batch_no = models.CharField(max_length=64, unique=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) condition_json = models.JSONField(default=dict, blank=True) risk_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) class Meta: db_table = "ra_regulatory_review_batch" ordering = ["-created_at", "-id"] indexes = [ models.Index(fields=["conversation", "created_at"], name="idx_ra_rr_batch_conv"), models.Index(fields=["user", "created_at"], name="idx_ra_rr_batch_user"), models.Index(fields=["status", "created_at"], name="idx_ra_rr_batch_status"), ] def __str__(self) -> str: return self.batch_no class RegulatoryIssue(models.Model): """Stores one regulatory finding after risk consolidation.""" class Severity(models.TextChoices): BLOCKING = "blocking", "阻断项" HIGH = "high", "高风险" MEDIUM = "medium", "中风险" LOW = "low", "低风险" INFO = "info", "提示" class Category(models.TextChoices): COMPLETENESS = "completeness", "完整性" STRUCTURE = "structure", "章节" CONSISTENCY = "consistency", "一致性" RAG = "rag", "法规依据" class Status(models.TextChoices): OPEN = "open", "待处理" RESOLVED = "resolved", "已整改" ACCEPTED = "accepted", "已接受" batch = models.ForeignKey( RegulatoryReviewBatch, on_delete=models.CASCADE, related_name="issues", ) rule_code = models.CharField(max_length=120, blank=True, default="") category = models.CharField(max_length=40, choices=Category.choices) severity = models.CharField(max_length=20, choices=Severity.choices) title = models.CharField(max_length=255) detail = models.TextField(blank=True, default="") suggestion = models.TextField(blank=True, default="") status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN) evidence = models.JSONField(default=dict, blank=True) citations = models.JSONField(default=list, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "ra_regulatory_issue" ordering = ["severity", "id"] indexes = [ models.Index(fields=["batch", "severity"], name="idx_ra_rr_issue_severity"), models.Index(fields=["batch", "category"], name="idx_ra_rr_issue_category"), ] def __str__(self) -> str: return self.title class RegulatoryArtifact(models.Model): """Stores regulatory review intermediate and exported artifacts.""" class ArtifactType(models.TextChoices): MARKDOWN = "markdown", "Markdown" EXCEL = "excel", "Excel" JSON = "json", "JSON" TEXT = "text", "文本" batch = models.ForeignKey( RegulatoryReviewBatch, on_delete=models.CASCADE, related_name="artifacts", ) artifact_type = models.CharField(max_length=20, choices=ArtifactType.choices) name = models.CharField(max_length=160) storage_path = models.CharField(max_length=500) content_hash = models.CharField(max_length=128, blank=True, default="") metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "ra_regulatory_artifact" ordering = ["-created_at", "-id"] indexes = [ models.Index(fields=["batch", "artifact_type"], name="idx_ra_rr_artifact_type"), ] class RegulatoryNotificationRecord(models.Model): """Stores mock notification records for future Feishu integration.""" class Channel(models.TextChoices): MOCK = "mock", "模拟" FEISHU = "feishu", "飞书" class Status(models.TextChoices): PENDING = "pending", "待发送" SENT = "sent", "已发送" FAILED = "failed", "失败" batch = models.ForeignKey( RegulatoryReviewBatch, on_delete=models.CASCADE, related_name="notifications", ) channel = models.CharField(max_length=20, choices=Channel.choices, default=Channel.MOCK) target = models.CharField(max_length=160, blank=True, default="") payload = models.JSONField(default=dict, blank=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) error_message = models.TextField(blank=True, default="") created_at = models.DateTimeField(auto_now_add=True) sent_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "ra_regulatory_notification_record" ordering = ["-created_at", "-id"] indexes = [ models.Index(fields=["batch", "status"], name="idx_ra_rr_notify_status"), ]