🦐 功能 2:批量加入 API(人类 + 随行龙虾)
This commit is contained in:
@@ -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,19 +26,13 @@ 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
|
||||
# 设置 1 小时过期时间
|
||||
meeting.expires_at = timezone.now() + timedelta(hours=1)
|
||||
meeting.save()
|
||||
|
||||
# 创建主持人参会记录
|
||||
@@ -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)
|
||||
if not existing:
|
||||
# 用户加入会议
|
||||
Participant.objects.create(
|
||||
meeting=meeting,
|
||||
user=user,
|
||||
agent_type='human',
|
||||
nickname=user.username if user else '用户',
|
||||
is_host=False
|
||||
)
|
||||
|
||||
# 创建参会记录
|
||||
# 随行龙虾加入会议
|
||||
joined_agents = []
|
||||
for agent_id in agent_ids:
|
||||
# 检查是否已存在
|
||||
exists = Participant.objects.filter(
|
||||
meeting=meeting,
|
||||
agent_id=agent_id,
|
||||
left_at__isnull=True
|
||||
).first()
|
||||
|
||||
if not exists:
|
||||
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(
|
||||
meeting=meeting,
|
||||
agent_type='openclaw',
|
||||
agent_id=sender_agent_id,
|
||||
agent_name=sender_name,
|
||||
agent_emoji=sender_emoji,
|
||||
nickname=f"{sender_emoji} {sender_name}"
|
||||
agent_id=agent_id,
|
||||
agent_name='Agent',
|
||||
agent_emoji='🤖',
|
||||
nickname=f'Agent {agent_id}',
|
||||
is_host=False
|
||||
)
|
||||
joined_agents.append(ParticipantSerializer(participant).data)
|
||||
|
||||
# 返回所有参会者
|
||||
participants = Participant.objects.filter(
|
||||
meeting=meeting,
|
||||
left_at__isnull=True
|
||||
)
|
||||
|
||||
if not sender_participant:
|
||||
# 人类发送的,用主持人
|
||||
sender_participant = Participant.objects.filter(
|
||||
meeting=meeting,
|
||||
is_host=True
|
||||
).first()
|
||||
|
||||
# 创建消息,标记为需要回复
|
||||
message = Message.objects.create(
|
||||
meeting=meeting,
|
||||
sender=sender_participant,
|
||||
content=f"@{target_participant.nickname} {content}",
|
||||
is_broadcast=False, # 只发给目标 Agent
|
||||
requires_response=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)
|
||||
|
||||
Reference in New Issue
Block a user