Files
meeting-room/backend/meetings/minutes_api.py
flying-hero 6d426db0a4 🦞 飞行侠实现:主持龙虾生成纪要
核心功能:
- 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: 完整流程测试通过

算力分配:
- 中央平台:消息路由 + 数据存储(轻量级)
- 主持龙虾:生成纪要(消耗用户算力)
- 平台留存:纪要供所有参会者下载
2026-04-04 12:42:58 +08:00

224 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)