2026-04-04 11:19:01 +08:00
|
|
|
|
from django.db import models
|
|
|
|
|
|
from django.contrib.auth import get_user_model
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Meeting(models.Model):
|
|
|
|
|
|
"""会议室模型"""
|
|
|
|
|
|
STATUS_CHOICES = [
|
|
|
|
|
|
('pending', '待开始'),
|
|
|
|
|
|
('active', '进行中'),
|
|
|
|
|
|
('ended', '已结束'),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
|
|
|
|
topic = models.CharField(max_length=200, verbose_name='会议主题')
|
|
|
|
|
|
host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hosted_meetings')
|
|
|
|
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
|
|
|
|
|
invite_code = models.CharField(max_length=20, unique=True, verbose_name='邀请码')
|
|
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
started_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
|
ended_at = models.DateTimeField(null=True, blank=True)
|
2026-04-05 09:43:17 +08:00
|
|
|
|
expires_at = models.DateTimeField(null=True, blank=True, verbose_name="过期时间")
|
2026-04-04 11:19:01 +08:00
|
|
|
|
|
2026-04-04 12:42:58 +08:00
|
|
|
|
# 主持龙虾(负责生成会议纪要)
|
|
|
|
|
|
host_agent_id = models.CharField(max_length=100, null=True, blank=True, verbose_name='主持 Agent ID')
|
|
|
|
|
|
host_instance_id = models.CharField(max_length=100, null=True, blank=True, verbose_name='主持实例 ID')
|
|
|
|
|
|
minutes_generated = models.BooleanField(default=False, verbose_name='纪要已生成')
|
|
|
|
|
|
minutes_uploaded_at = models.DateTimeField(null=True, blank=True, verbose_name='纪要上传时间')
|
|
|
|
|
|
|
2026-04-04 11:19:01 +08:00
|
|
|
|
class Meta:
|
|
|
|
|
|
db_table = 'meetings'
|
|
|
|
|
|
verbose_name = '会议室'
|
|
|
|
|
|
verbose_name_plural = '会议室'
|
|
|
|
|
|
ordering = ['-created_at']
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"{self.topic} ({self.host.username})"
|
|
|
|
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
|
if not self.invite_code:
|
|
|
|
|
|
self.invite_code = uuid.uuid4().hex[:8].upper()
|
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Participant(models.Model):
|
|
|
|
|
|
"""参会者模型"""
|
|
|
|
|
|
AGENT_TYPE_CHOICES = [
|
|
|
|
|
|
('human', '人类'),
|
|
|
|
|
|
('openclaw', 'OpenClaw Agent'),
|
|
|
|
|
|
('other', '其他 AI'),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='participants')
|
|
|
|
|
|
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
|
|
|
|
|
|
|
|
|
|
|
|
# Agent 信息
|
|
|
|
|
|
agent_type = models.CharField(max_length=20, choices=AGENT_TYPE_CHOICES)
|
|
|
|
|
|
agent_id = models.CharField(max_length=100, null=True, blank=True)
|
|
|
|
|
|
agent_name = models.CharField(max_length=100, verbose_name='Agent 名称')
|
|
|
|
|
|
agent_emoji = models.CharField(max_length=10, default='🤖', verbose_name='Agent 表情')
|
|
|
|
|
|
|
|
|
|
|
|
# 显示信息
|
|
|
|
|
|
nickname = models.CharField(max_length=100, verbose_name='昵称')
|
|
|
|
|
|
is_host = models.BooleanField(default=False)
|
|
|
|
|
|
joined_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
left_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
# API 认证(Agent 用)
|
|
|
|
|
|
api_key = models.CharField(max_length=255, null=True, blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
db_table = 'participants'
|
|
|
|
|
|
verbose_name = '参会者'
|
|
|
|
|
|
verbose_name_plural = '参会者'
|
|
|
|
|
|
indexes = [
|
|
|
|
|
|
models.Index(fields=['meeting', 'agent_id']),
|
|
|
|
|
|
models.Index(fields=['agent_type', 'meeting']),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"{self.agent_emoji} {self.nickname}"
|
|
|
|
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
|
if not self.api_key and self.agent_type != 'human':
|
|
|
|
|
|
self.api_key = uuid.uuid4().hex
|
|
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Message(models.Model):
|
|
|
|
|
|
"""消息模型"""
|
|
|
|
|
|
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='messages')
|
|
|
|
|
|
sender = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name='sent_messages')
|
|
|
|
|
|
content = models.TextField()
|
|
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 信箱机制
|
|
|
|
|
|
is_broadcast = models.BooleanField(default=True, verbose_name='群发消息')
|
|
|
|
|
|
requires_response = models.BooleanField(default=False, verbose_name='需要回复')
|
|
|
|
|
|
in_reply_to = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='replies')
|
|
|
|
|
|
|
|
|
|
|
|
# 读取状态
|
|
|
|
|
|
read_by = models.ManyToManyField(Participant, related_name='read_messages', blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
db_table = 'messages'
|
|
|
|
|
|
verbose_name = '消息'
|
|
|
|
|
|
verbose_name_plural = '消息'
|
|
|
|
|
|
ordering = ['created_at']
|
|
|
|
|
|
indexes = [
|
|
|
|
|
|
models.Index(fields=['meeting', 'created_at']),
|
|
|
|
|
|
models.Index(fields=['is_broadcast', 'created_at']),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"{self.sender.nickname}: {self.content[:50]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MeetingMinutes(models.Model):
|
|
|
|
|
|
"""会议纪要模型"""
|
|
|
|
|
|
meeting = models.OneToOneField(Meeting, on_delete=models.CASCADE, related_name='minutes')
|
|
|
|
|
|
content = models.TextField()
|
|
|
|
|
|
generated_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
exported_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
db_table = 'meeting_minutes'
|
|
|
|
|
|
verbose_name = '会议纪要'
|
|
|
|
|
|
verbose_name_plural = '会议纪要'
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"会议纪要 - {self.meeting.topic}"
|