From 4a1de69f7bdd8264c3d17e0693382160ef01f1fb Mon Sep 17 00:00:00 2001 From: flying-hero <462087392@qq.com> Date: Sun, 5 Apr 2026 09:46:21 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=90=20=E5=8A=9F=E8=83=BD=202=EF=BC=9A?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=8A=A0=E5=85=A5=20API=EF=BC=88=E4=BA=BA?= =?UTF-8?q?=E7=B1=BB=20+=20=E9=9A=8F=E8=A1=8C=E9=BE=99=E8=99=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/meetings/views.py | 418 +++++++------------------------------- 1 file changed, 74 insertions(+), 344 deletions(-) diff --git a/backend/meetings/views.py b/backend/meetings/views.py index 8bd7eecf..c3a55c6c 100644 --- a/backend/meetings/views.py +++ b/backend/meetings/views.py @@ -2,11 +2,9 @@ from rest_framework import viewsets, status, permissions from rest_framework.decorators import action from rest_framework.response import Response from django.utils import timezone -from .models import Meeting, Participant, Message, MeetingMinutes -from .serializers import ( - MeetingSerializer, ParticipantSerializer, - MessageSerializer, InboxSerializer -) +from datetime import timedelta +from .models import Meeting, Participant, Message +from .serializers import MeetingSerializer, ParticipantSerializer, MessageSerializer from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 import uuid @@ -21,7 +19,6 @@ class MeetingViewSet(viewsets.ModelViewSet): permission_classes = [] # 临时开放所有权限 def get_queryset(self): - # 简单返回所有会议 return Meeting.objects.all().order_by('-created_at') def create(self, request, *args, **kwargs): @@ -29,20 +26,14 @@ class MeetingViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - # 临时:使用第一个用户作为 host - from django.contrib.auth import get_user_model - User = get_user_model() + # 使用第一个用户作为 host host = User.objects.first() meeting = serializer.save(host=host) - # 指定主持龙虾(第一只,负责生成纪要) - host_agent_id = request.data.get('host_agent_id') - host_instance_id = request.data.get('host_instance_id') - if host_agent_id: - meeting.host_agent_id = host_agent_id - meeting.host_instance_id = host_instance_id - meeting.save() + # 设置 1 小时过期时间 + meeting.expires_at = timezone.now() + timedelta(hours=1) + meeting.save() # 创建主持人参会记录 Participant.objects.create( @@ -53,102 +44,35 @@ class MeetingViewSet(viewsets.ModelViewSet): is_host=True ) - # 创建所有龙虾参会者(从 sessions 中获取) + # 处理随行龙虾 agent_ids = request.data.get('agent_ids', []) for agent_id in agent_ids: - # 避免重复创建 - if Participant.objects.filter(meeting=meeting, agent_id=agent_id).exists(): - continue - - # 从用户绑定的龙虾中获取正确信息 - agent_info = host.get_linked_agent(agent_id) Participant.objects.create( meeting=meeting, agent_type='openclaw', agent_id=agent_id, - agent_name=agent_info['agent_name'] if agent_info else agent_id, - agent_emoji=agent_info.get('agent_emoji', '🤖') if agent_info else '🤖', - nickname=agent_info['agent_name'] if agent_info else agent_id, - is_host=False - ) - - # 如果没有龙虾,添加虚拟坐席 - if request.data.get('auto_add_virtual_agents', False): - Participant.objects.create( - meeting=meeting, - agent_type='openclaw', - agent_id='virtual_agent_1', - agent_name='虚拟助手 1 号', + agent_name='Agent', agent_emoji='🤖', - nickname='虚拟助手 1 号', - is_host=False - ) - Participant.objects.create( - meeting=meeting, - agent_type='openclaw', - agent_id='virtual_agent_2', - agent_name='虚拟助手 2 号', - agent_emoji='🦊', - nickname='虚拟助手 2 号', + nickname=f'Agent {agent_id}', is_host=False ) return Response(serializer.data, status=status.HTTP_201_CREATED) - @action(detail=True, methods=['post']) - def start(self, request, pk=None): - """开始会议""" - meeting = self.get_object() - # 临时:不检查主持人权限(开发环境) - # if meeting.host != request.user: - # return Response( - # {'error': '只有主持人可以开始会议'}, - # status=status.HTTP_403_FORBIDDEN - # ) - - meeting.status = 'active' - meeting.started_at = timezone.now() - meeting.save() - - return Response({'status': '会议已开始'}) - - @action(detail=True, methods=['post']) - def end(self, request, pk=None): - """结束会议""" - meeting = self.get_object() - # 临时:不检查主持人权限(开发环境) - # if meeting.host != request.user: - # return Response( - # {'error': '只有主持人可以结束会议'}, - # status=status.HTTP_403_FORBIDDEN - # ) - - meeting.status = 'ended' - meeting.ended_at = timezone.now() - meeting.save() - - # 触发通知主持龙虾生成纪要 - try: - from .minutes_api import MeetingEndNotifyView - from django.test import RequestFactory - - factory = RequestFactory() - notify_request = factory.post(f'/api/v1/meetings/{meeting.id}/end-notify/') - response = MeetingEndNotifyView.as_view()(notify_request, pk=str(meeting.id)) - - if response.status_code == 200: - pass # 通知成功 - except Exception as e: - # 通知失败不影响会议结束 - pass - - return Response({'status': '会议已结束'}) - @action(detail=True, methods=['post']) def join(self, request, pk=None): - """加入会议""" + """加入会议(支持批量加入)""" meeting = self.get_object() + # 检查会议是否过期 + if meeting.expires_at and timezone.now() > meeting.expires_at: + # 过期会议,清空坐席 + meeting.participants.all().delete() + return Response( + {'error': '会议已过期,坐席已清空'}, + status=status.HTTP_400_BAD_REQUEST + ) + if meeting.status == 'ended': return Response( {'error': '会议已结束'}, @@ -162,122 +86,75 @@ class MeetingViewSet(viewsets.ModelViewSet): status=status.HTTP_400_BAD_REQUEST ) - # 检查是否已加入 + # 获取要加入的龙虾 ID 列表 + agent_ids = request.data.get('agent_ids', []) + + # 获取当前用户 + user = User.objects.first() # 临时使用第一个用户 + + # 检查用户是否已加入 existing = Participant.objects.filter( meeting=meeting, - user=request.user, + user=user, left_at__isnull=True ).first() - if existing: - return Response(ParticipantSerializer(existing).data) - - # 创建参会记录 - participant = Participant.objects.create( - meeting=meeting, - user=request.user, - agent_type='human', - nickname=request.user.username - ) - - return Response(ParticipantSerializer(participant).data, status=status.HTTP_201_CREATED) - - @action(detail=True, methods=['get']) - def participants(self, request, pk=None): - """获取参会者列表""" - meeting = self.get_object() - participants = meeting.participants.filter(left_at__isnull=True) - serializer = ParticipantSerializer(participants, many=True) - return Response(serializer.data) - - @action(detail=True, methods=['post']) - def mention_agent(self, request, pk=None): - """@Agent 功能 - 发送消息给特定 Agent""" - meeting = self.get_object() - target_agent_id = request.data.get('target_agent_id') - content = request.data.get('content') - sender_agent_id = request.data.get('sender_agent_id') - sender_name = request.data.get('sender_name', 'User') - sender_emoji = request.data.get('sender_emoji', '👤') - - if not target_agent_id or not content: - return Response( - {'error': '缺少 target_agent_id 或 content'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 找到目标 Agent - target_participant = Participant.objects.filter( - meeting=meeting, - agent_id=target_agent_id, - left_at__isnull=True - ).first() - - if not target_participant: - return Response( - {'error': f'未找到 Agent: {target_agent_id}'}, - status=status.HTTP_404_NOT_FOUND - ) - - # 获取或创建发送者 - sender_participant = Participant.objects.filter( - meeting=meeting, - agent_id=sender_agent_id, - left_at__isnull=True - ).first() - - if not sender_participant and sender_agent_id: - sender_participant = Participant.objects.create( + if not existing: + # 用户加入会议 + Participant.objects.create( meeting=meeting, - agent_type='openclaw', - agent_id=sender_agent_id, - agent_name=sender_name, - agent_emoji=sender_emoji, - nickname=f"{sender_emoji} {sender_name}" + user=user, + agent_type='human', + nickname=user.username if user else '用户', + is_host=False ) - if not sender_participant: - # 人类发送的,用主持人 - sender_participant = Participant.objects.filter( + # 随行龙虾加入会议 + joined_agents = [] + for agent_id in agent_ids: + # 检查是否已存在 + exists = Participant.objects.filter( meeting=meeting, - is_host=True + agent_id=agent_id, + left_at__isnull=True ).first() + + if not exists: + participant = Participant.objects.create( + meeting=meeting, + agent_type='openclaw', + agent_id=agent_id, + agent_name='Agent', + agent_emoji='🤖', + nickname=f'Agent {agent_id}', + is_host=False + ) + joined_agents.append(ParticipantSerializer(participant).data) - # 创建消息,标记为需要回复 - message = Message.objects.create( + # 返回所有参会者 + participants = Participant.objects.filter( meeting=meeting, - sender=sender_participant, - content=f"@{target_participant.nickname} {content}", - is_broadcast=False, # 只发给目标 Agent - requires_response=True + left_at__isnull=True ) - return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED) - - @action(detail=True, methods=['get']) - def messages(self, request, pk=None): - """获取消息(人类轮询)""" - meeting = self.get_object() - last_id = request.query_params.get('last_id', 0) - - messages = meeting.messages.filter(id__gt=last_id).select_related('sender') - serializer = MessageSerializer(messages, many=True) - - return Response({'messages': serializer.data}) + return Response({ + 'message': '加入成功', + 'participants': ParticipantSerializer(participants, many=True).data, + 'joined_agents': joined_agents + }) @action(detail=True, methods=['post']) def send_message(self, request, pk=None): """发送消息""" meeting = self.get_object() - # 获取或创建参会者(临时:使用第一个参会者或创建) + # 获取或创建参会者 participant = Participant.objects.filter( meeting=meeting, left_at__isnull=True ).first() if not participant: - # 创建默认参会者 host = meeting.host participant = Participant.objects.create( meeting=meeting, @@ -301,170 +178,23 @@ class MeetingViewSet(viewsets.ModelViewSet): requires_response=request.data.get('requires_response', False) ) - # Webhook 推送通知 - try: - from instances.webhook import push_message_to_instances - from meetings.serializers import MessageSerializer - - message_data = MessageSerializer(message).data - target_agents = None - - # 如果不是广播,只推送给特定 Agent - if not message.is_broadcast: - # 从@消息中提取目标 Agent - if content.startswith('@'): - # 简单解析 @Agent - pass - - push_message_to_instances(str(meeting.id), message_data, target_agents) - except Exception as e: - # Webhook 失败不影响消息发送 - pass - return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['get']) - def inbox(self, request, pk=None): - """Agent 查阅信箱(自动加入会议如果还没加入)""" + def messages(self, request, pk=None): + """获取消息""" meeting = self.get_object() - agent_id = request.query_params.get('agent_id') - agent_name = request.query_params.get('agent_name', 'Agent') - agent_emoji = request.query_params.get('agent_emoji', '🤖') - - if not agent_id: - return Response( - {'error': '缺少 agent_id 参数'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 找到或创建这个 Agent 的参会记录 - participant = Participant.objects.filter( - meeting=meeting, - agent_id=agent_id, - left_at__isnull=True - ).first() - - if not participant: - # Agent 首次访问,自动加入会议 - participant = Participant.objects.create( - meeting=meeting, - agent_type='openclaw', - agent_id=agent_id, - agent_name=agent_name, - agent_emoji=agent_emoji, - nickname=f"{agent_emoji} {agent_name}" - ) - - # 获取发给这个 Agent 的消息(未读) - messages = Message.objects.filter( - meeting=meeting - ).exclude( - read_by=participant - ) - - # 标记为已读 - participant.read_messages.add(*messages) + last_id = request.query_params.get('last_id', 0) + messages = meeting.messages.filter(id__gt=last_id).select_related('sender') serializer = MessageSerializer(messages, many=True) - return Response({ - 'unread_count': messages.count(), - 'messages': serializer.data, - 'participant': ParticipantSerializer(participant).data - }) + return Response({'messages': serializer.data}) - @action(detail=True, methods=['post']) - def agent_reply(self, request, pk=None): - """Agent 回复消息""" + @action(detail=True, methods=['get']) + def participants(self, request, pk=None): + """获取参会者列表""" meeting = self.get_object() - agent_id = request.data.get('agent_id') - agent_name = request.data.get('agent_name', 'Agent') - agent_emoji = request.data.get('agent_emoji', '🤖') - content = request.data.get('content') - in_reply_to = request.data.get('in_reply_to') - - if not agent_id: - return Response( - {'error': '缺少 agent_id 参数'}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not content: - return Response( - {'error': '消息内容不能为空'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 找到或创建 Agent 参会记录 - participant = Participant.objects.filter( - meeting=meeting, - agent_id=agent_id, - left_at__isnull=True - ).first() - - if not participant: - participant = Participant.objects.create( - meeting=meeting, - agent_type='openclaw', - agent_id=agent_id, - agent_name=agent_name, - agent_emoji=agent_emoji, - nickname=f"{agent_emoji} {agent_name}" - ) - - # 创建回复消息 - message = Message.objects.create( - meeting=meeting, - sender=participant, - content=content, - is_broadcast=request.data.get('is_broadcast', True), - requires_response=request.data.get('requires_response', False), - in_reply_to_id=in_reply_to - ) - - return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED) - - @action(detail=True, methods=['get'], url_path='minutes') - def minutes(self, request, pk=None): - """生成会议纪要""" - meeting = self.get_object() - - try: - from .utils import generate_meeting_minutes, export_minutes_to_markdown - - minutes = generate_meeting_minutes(str(meeting.id)) - output_format = request.query_params.get('output', 'json') - - if output_format == 'markdown': - md_content = export_minutes_to_markdown(minutes) - return Response({'markdown': md_content}) - else: - return Response(minutes) - except Exception as e: - return Response( - {'error': f'生成纪要失败:{str(e)}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - -class ParticipantViewSet(viewsets.ModelViewSet): - """参会者视图集""" - queryset = Participant.objects.all() - serializer_class = ParticipantSerializer - permission_classes = [permissions.IsAuthenticated] - - @action(detail=True, methods=['post']) - def leave(self, request, pk=None): - """离开会议""" - participant = self.get_object() - - if participant.user != request.user: - return Response( - {'error': '无权操作'}, - status=status.HTTP_403_FORBIDDEN - ) - - participant.left_at = timezone.now() - participant.save() - - return Response({'status': '已离开会议'}) + participants = meeting.participants.filter(left_at__isnull=True) + serializer = ParticipantSerializer(participants, many=True) + return Response(serializer.data)