from django.db import models from userapp.models import ParadiseUser class Bot(models.Model): name = models.CharField(max_length=255, verbose_name='机器人姓名') description = models.TextField(blank=False, null=False, verbose_name='机器人提示词') class Meta: verbose_name = '机器人' verbose_name_plural = '机器人' def __str__(self): return self.name class ChatMessage(models.Model): SENDER_USER = 'user' SENDER_BOT = 'assistant' SENDER_SYSTEM = 'system' SENDER_CHOICES = [ (SENDER_USER, 'user'), (SENDER_BOT, 'assistant'), (SENDER_SYSTEM, 'system'), ] MESSAGE_TYPE_TEXT = 'text' MESSAGE_TYPE_AUDIO = 'audio' MESSAGE_TYPE_VIDEO = 'video' MESSAGE_TYPE_CHOICES = [ (MESSAGE_TYPE_TEXT, 'text'), (MESSAGE_TYPE_AUDIO, 'audio'), (MESSAGE_TYPE_VIDEO, 'video'), ] user = models.ForeignKey(ParadiseUser, on_delete=models.CASCADE, verbose_name='用户') bot = models.ForeignKey(Bot, on_delete=models.CASCADE, verbose_name='机器人') message = models.TextField(max_length=2048, null=False, blank=False, verbose_name='消息内容') message_audio_url = models.TextField(max_length=2048, blank=True, verbose_name='消息内容语音链接') message_video_url = models.TextField(max_length=2048, blank=True, verbose_name='消息内容视频链接') timestamp = models.DateTimeField(auto_now_add=True, verbose_name='时间戳') sender = models.CharField(max_length=10, choices=SENDER_CHOICES, verbose_name='发送者') message_type = models.CharField(max_length=10, choices=MESSAGE_TYPE_CHOICES, default=MESSAGE_TYPE_TEXT, verbose_name='消息类型') class Meta: ordering = ['timestamp'] # 按时间顺序排序 verbose_name = '聊天消息' verbose_name_plural = '聊天消息' def __str__(self): return f"{self.sender}: {self.message[:50]} ({self.timestamp})" class CredentialSlot(models.Model): """通用凭据槽位(单例)— Milestone v1.0 / Phase 1 全局唯一一条记录,存第三方服务(Kimi / 阿里云 / 火山等)的 APP ID + Access Token。 通过 pk=1 + get_or_create + save() 钩子三件套保证单例: - 任何 .save() 在已有记录时把新对象 pk 改成现有那条 - get_solo() 是单一访问入口(Phase 2/3 视图统一调用) - DB 层无额外约束,绕过 ORM 的 bulk_create / 原始 SQL 不在保护范围 """ app_id = models.CharField( 'APP ID', max_length=128, blank=True, default='', help_text='第三方服务商分配的 APP ID;运营在 Admin 录入' ) access_token = models.CharField( 'Access Token', max_length=512, blank=True, default='', help_text='第三方服务商访问令牌;DB 明文存储,Admin 列表/查看态末 4 位脱敏' ) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: verbose_name = '凭据槽位' verbose_name_plural = '凭据槽位' def __str__(self): return f"凭据槽位 (updated {self.updated_at:%Y-%m-%d %H:%M})" def save(self, *args, **kwargs): # 强制单例:新增时如果已有记录则覆盖到现有 pk if not self.pk and CredentialSlot.objects.exists(): existing = CredentialSlot.objects.first() self.pk = existing.pk super().save(*args, **kwargs) @classmethod def get_solo(cls): """获取单例实例,不存在则用默认值创建(pk=1)""" instance, _ = cls.objects.get_or_create(pk=1) return instance