核心功能:
- Meeting 模型:添加 host_agent_id, host_instance_id
- 会议纪要 API:记录获取 + 纪要上传 + 结束通知
- 会议结束自动通知主持龙虾生成纪要
- 平台留存纪要供参会者下载
API 端点:
- GET /api/v1/meetings/{id}/records/ - 获取会议记录(主持专用)
- POST /api/v1/meetings/{id}/minutes/upload/ - 上传纪要(主持专用)
- POST /api/v1/meetings/{id}/end-notify/ - 会议结束通知
测试:
- test_host_minutes.py: 完整流程测试通过
算力分配:
- 中央平台:消息路由 + 数据存储(轻量级)
- 主持龙虾:生成纪要(消耗用户算力)
- 平台留存:纪要供所有参会者下载
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
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)
|
||
|
||
# 主持龙虾(负责生成会议纪要)
|
||
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='纪要上传时间')
|
||
|
||
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}"
|