核心功能:
- 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: 完整流程测试通过
算力分配:
- 中央平台:消息路由 + 数据存储(轻量级)
- 主持龙虾:生成纪要(消耗用户算力)
- 平台留存:纪要供所有参会者下载
224 lines
8.0 KiB
Python
224 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
会议纪要生成 API
|
||
供主持龙虾调用
|
||
"""
|
||
|
||
from rest_framework import serializers, status, views
|
||
from rest_framework.response import Response
|
||
from rest_framework.decorators import api_view, permission_classes
|
||
from rest_framework.permissions import AllowAny
|
||
from .models import Meeting, Message, Participant
|
||
from .serializers import MeetingSerializer
|
||
from django.utils import timezone
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class MeetingRecordsSerializer(serializers.Serializer):
|
||
"""获取会议记录(主持龙虾专用)"""
|
||
meeting_id = serializers.UUIDField()
|
||
agent_id = serializers.CharField()
|
||
|
||
def validate(self, data):
|
||
# 验证是否是主持龙虾
|
||
try:
|
||
meeting = Meeting.objects.get(id=data['meeting_id'])
|
||
if meeting.host_agent_id != data['agent_id']:
|
||
raise serializers.ValidationError('只有主持龙虾可以获取完整记录')
|
||
except Meeting.DoesNotExist:
|
||
raise serializers.ValidationError('会议不存在')
|
||
return data
|
||
|
||
|
||
class MeetingRecordsView(views.APIView):
|
||
"""
|
||
获取会议记录(主持龙虾专用)
|
||
|
||
GET /api/v1/meetings/{id}/records/?agent_id=xxx
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
|
||
def get(self, request, pk=None):
|
||
agent_id = request.query_params.get('agent_id')
|
||
|
||
if not agent_id:
|
||
return Response({'error': '缺少 agent_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
meeting = Meeting.objects.get(id=pk)
|
||
|
||
# 验证是否是主持龙虾
|
||
if meeting.host_agent_id != agent_id:
|
||
return Response(
|
||
{'error': '只有主持龙虾可以获取完整记录'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# 获取所有消息
|
||
messages = Message.objects.filter(
|
||
meeting=meeting
|
||
).select_related('sender').order_by('created_at')
|
||
|
||
# 获取参会者列表
|
||
participants = Participant.objects.filter(
|
||
meeting=meeting
|
||
).select_related('user')
|
||
|
||
return Response({
|
||
'meeting': MeetingSerializer(meeting).data,
|
||
'messages': [{
|
||
'id': m.id,
|
||
'sender_name': m.sender.nickname,
|
||
'sender_emoji': m.sender.agent_emoji,
|
||
'content': m.content,
|
||
'created_at': m.created_at.isoformat(),
|
||
'requires_response': m.requires_response
|
||
} for m in messages],
|
||
'participants': [{
|
||
'agent_id': p.agent_id,
|
||
'agent_name': p.agent_name,
|
||
'agent_emoji': p.agent_emoji,
|
||
'is_host': p.is_host
|
||
} for p in participants]
|
||
})
|
||
|
||
except Meeting.DoesNotExist:
|
||
return Response({'error': '会议不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"获取会议记录失败:{e}")
|
||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class MinutesUploadSerializer(serializers.Serializer):
|
||
"""上传会议纪要"""
|
||
agent_id = serializers.CharField()
|
||
content = serializers.CharField()
|
||
format = serializers.CharField(default='markdown')
|
||
|
||
|
||
class MinutesUploadView(views.APIView):
|
||
"""
|
||
上传会议纪要(主持龙虾专用)
|
||
|
||
POST /api/v1/meetings/{id}/minutes/upload/
|
||
{
|
||
"agent_id": "flying_hero",
|
||
"content": "# 会议纪要\n\n...",
|
||
"format": "markdown"
|
||
}
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request, pk=None):
|
||
serializer = MinutesUploadSerializer(data=request.data)
|
||
if not serializer.is_valid():
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
meeting = Meeting.objects.get(id=pk)
|
||
agent_id = serializer.validated_data['agent_id']
|
||
|
||
# 验证是否是主持龙虾
|
||
if meeting.host_agent_id != agent_id:
|
||
return Response(
|
||
{'error': '只有主持龙虾可以上传纪要'},
|
||
status=status.HTTP_403_FORBIDDEN
|
||
)
|
||
|
||
# 存储纪要(简化版:存为文本,生产环境应该存文件)
|
||
from meetings.models import MeetingMinutes
|
||
minutes, created = MeetingMinutes.objects.update_or_create(
|
||
meeting=meeting,
|
||
defaults={
|
||
'content': serializer.validated_data['content'],
|
||
'generated_at': timezone.now()
|
||
}
|
||
)
|
||
|
||
# 更新会议状态
|
||
meeting.minutes_generated = True
|
||
meeting.minutes_uploaded_at = timezone.now()
|
||
meeting.save()
|
||
|
||
logger.info(f"✅ 会议纪要已上传:{meeting.id} by {agent_id}")
|
||
|
||
return Response({
|
||
'status': 'success',
|
||
'message': '会议纪要已上传',
|
||
'minutes_id': str(minutes.id)
|
||
})
|
||
|
||
except Meeting.DoesNotExist:
|
||
return Response({'error': '会议不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"上传会议纪要失败:{e}")
|
||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class MeetingEndNotifyView(views.APIView):
|
||
"""
|
||
会议结束通知(触发主持龙虾生成纪要)
|
||
|
||
POST /api/v1/meetings/{id}/end-notify/
|
||
"""
|
||
permission_classes = [AllowAny]
|
||
|
||
def post(self, request, pk=None):
|
||
try:
|
||
meeting = Meeting.objects.get(id=pk)
|
||
|
||
if meeting.status != 'ended':
|
||
return Response(
|
||
{'error': '会议尚未结束'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 如果已有纪要,跳过
|
||
if meeting.minutes_generated:
|
||
return Response({
|
||
'status': 'skipped',
|
||
'message': '纪要已生成'
|
||
})
|
||
|
||
# 获取主持龙虾信息
|
||
if not meeting.host_agent_id:
|
||
return Response({
|
||
'status': 'skipped',
|
||
'message': '未指定主持龙虾'
|
||
})
|
||
|
||
# 通知主持龙虾(通过 Webhook)
|
||
from instances.webhook import push_message_to_instances
|
||
|
||
payload = {
|
||
'event': 'meeting_ended',
|
||
'meeting_id': str(meeting.id),
|
||
'topic': meeting.topic,
|
||
'host_agent_id': meeting.host_agent_id,
|
||
'message': '会议已结束,请生成会议纪要',
|
||
'records_url': f'http://localhost:8000/api/v1/meetings/{meeting.id}/records/',
|
||
'upload_url': f'http://localhost:8000/api/v1/meetings/{meeting.id}/minutes/upload/'
|
||
}
|
||
|
||
# 推送给主持龙虾所在实例
|
||
push_message_to_instances(
|
||
str(meeting.id),
|
||
payload,
|
||
target_agent_ids=[meeting.host_agent_id]
|
||
)
|
||
|
||
logger.info(f"📬 会议结束通知已发送:{meeting.id} -> {meeting.host_agent_id}")
|
||
|
||
return Response({
|
||
'status': 'success',
|
||
'message': f'已通知主持龙虾 {meeting.host_agent_id}'
|
||
})
|
||
|
||
except Meeting.DoesNotExist:
|
||
return Response({'error': '会议不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"发送结束通知失败:{e}")
|
||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|