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", "执行中" RETRYING = "retrying", "重试中" SUCCESS = "success", "成功" FAILED = "failed", "失败" SKIPPED = "skipped", "跳过" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, related_name="node_runs", ) 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"), ] class WorkflowEvent(models.Model): """Persists workflow events for SSE replay and diagnostics.""" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, related_name="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"), ] class ExportedSummaryFile(models.Model): """Stores generated report files for permission-checked download.""" class ExportType(models.TextChoices): MARKDOWN = "markdown", "Markdown" EXCEL = "excel", "Excel" class Status(models.TextChoices): SUCCESS = "success", "成功" FAILED = "failed", "失败" batch = models.ForeignKey( FileSummaryBatch, on_delete=models.CASCADE, related_name="exports", ) 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"), ]