Compare commits
7 Commits
f74babe5f5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 30d4602458 | |||
| 4a1de69f7b | |||
| aeccf647e6 | |||
| d5c6c3c786 | |||
| 49047b37e4 | |||
| 7bf62612b7 | |||
| e9e65f2e86 |
18
backend/meetings/migrations/0003_meeting_expires_at.py
Normal file
18
backend/meetings/migrations/0003_meeting_expires_at.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-04-05 01:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("meetings", "0002_meeting_host_agent_id_meeting_host_instance_id_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="meeting",
|
||||||
|
name="expires_at",
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name="过期时间"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -21,6 +21,7 @@ class Meeting(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
started_at = models.DateTimeField(null=True, blank=True)
|
started_at = models.DateTimeField(null=True, blank=True)
|
||||||
ended_at = models.DateTimeField(null=True, blank=True)
|
ended_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
expires_at = models.DateTimeField(null=True, blank=True, verbose_name="过期时间")
|
||||||
|
|
||||||
# 主持龙虾(负责生成会议纪要)
|
# 主持龙虾(负责生成会议纪要)
|
||||||
host_agent_id = models.CharField(max_length=100, null=True, blank=True, verbose_name='主持 Agent ID')
|
host_agent_id = models.CharField(max_length=100, null=True, blank=True, verbose_name='主持 Agent ID')
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ from rest_framework import viewsets, status, permissions
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .models import Meeting, Participant, Message, MeetingMinutes
|
from datetime import timedelta
|
||||||
from .serializers import (
|
from .models import Meeting, Participant, Message
|
||||||
MeetingSerializer, ParticipantSerializer,
|
from .serializers import MeetingSerializer, ParticipantSerializer, MessageSerializer
|
||||||
MessageSerializer, InboxSerializer
|
|
||||||
)
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
import uuid
|
import uuid
|
||||||
@@ -21,7 +19,6 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [] # 临时开放所有权限
|
permission_classes = [] # 临时开放所有权限
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# 简单返回所有会议
|
|
||||||
return Meeting.objects.all().order_by('-created_at')
|
return Meeting.objects.all().order_by('-created_at')
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
@@ -29,20 +26,14 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
# 临时:使用第一个用户作为 host
|
# 使用第一个用户作为 host
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
User = get_user_model()
|
|
||||||
host = User.objects.first()
|
host = User.objects.first()
|
||||||
|
|
||||||
meeting = serializer.save(host=host)
|
meeting = serializer.save(host=host)
|
||||||
|
|
||||||
# 指定主持龙虾(第一只,负责生成纪要)
|
# 设置 1 小时过期时间
|
||||||
host_agent_id = request.data.get('host_agent_id')
|
meeting.expires_at = timezone.now() + timedelta(hours=1)
|
||||||
host_instance_id = request.data.get('host_instance_id')
|
meeting.save()
|
||||||
if host_agent_id:
|
|
||||||
meeting.host_agent_id = host_agent_id
|
|
||||||
meeting.host_instance_id = host_instance_id
|
|
||||||
meeting.save()
|
|
||||||
|
|
||||||
# 创建主持人参会记录
|
# 创建主持人参会记录
|
||||||
Participant.objects.create(
|
Participant.objects.create(
|
||||||
@@ -53,102 +44,35 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
is_host=True
|
is_host=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建所有龙虾参会者(从 sessions 中获取)
|
# 处理随行龙虾
|
||||||
agent_ids = request.data.get('agent_ids', [])
|
agent_ids = request.data.get('agent_ids', [])
|
||||||
for agent_id in 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(
|
Participant.objects.create(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
agent_type='openclaw',
|
agent_type='openclaw',
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
agent_name=agent_info['agent_name'] if agent_info else agent_id,
|
agent_name='Agent',
|
||||||
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_emoji='🤖',
|
agent_emoji='🤖',
|
||||||
nickname='虚拟助手 1 号',
|
nickname=f'Agent {agent_id}',
|
||||||
is_host=False
|
|
||||||
)
|
|
||||||
Participant.objects.create(
|
|
||||||
meeting=meeting,
|
|
||||||
agent_type='openclaw',
|
|
||||||
agent_id='virtual_agent_2',
|
|
||||||
agent_name='虚拟助手 2 号',
|
|
||||||
agent_emoji='🦊',
|
|
||||||
nickname='虚拟助手 2 号',
|
|
||||||
is_host=False
|
is_host=False
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
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'])
|
@action(detail=True, methods=['post'])
|
||||||
def join(self, request, pk=None):
|
def join(self, request, pk=None):
|
||||||
"""加入会议"""
|
"""加入会议(支持批量加入)"""
|
||||||
meeting = self.get_object()
|
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':
|
if meeting.status == 'ended':
|
||||||
return Response(
|
return Response(
|
||||||
{'error': '会议已结束'},
|
{'error': '会议已结束'},
|
||||||
@@ -162,122 +86,75 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查是否已加入
|
# 获取要加入的龙虾 ID 列表
|
||||||
|
agent_ids = request.data.get('agent_ids', [])
|
||||||
|
|
||||||
|
# 获取当前用户
|
||||||
|
user = User.objects.first() # 临时使用第一个用户
|
||||||
|
|
||||||
|
# 检查用户是否已加入
|
||||||
existing = Participant.objects.filter(
|
existing = Participant.objects.filter(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
user=request.user,
|
user=user,
|
||||||
left_at__isnull=True
|
left_at__isnull=True
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing:
|
if not existing:
|
||||||
return Response(ParticipantSerializer(existing).data)
|
# 用户加入会议
|
||||||
|
Participant.objects.create(
|
||||||
# 创建参会记录
|
|
||||||
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,
|
meeting=meeting,
|
||||||
agent_type='openclaw',
|
user=user,
|
||||||
agent_id=sender_agent_id,
|
agent_type='human',
|
||||||
agent_name=sender_name,
|
nickname=user.username if user else '用户',
|
||||||
agent_emoji=sender_emoji,
|
is_host=False
|
||||||
nickname=f"{sender_emoji} {sender_name}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not sender_participant:
|
# 随行龙虾加入会议
|
||||||
# 人类发送的,用主持人
|
joined_agents = []
|
||||||
sender_participant = Participant.objects.filter(
|
for agent_id in agent_ids:
|
||||||
|
# 检查是否已存在
|
||||||
|
exists = Participant.objects.filter(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
is_host=True
|
agent_id=agent_id,
|
||||||
|
left_at__isnull=True
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# 创建消息,标记为需要回复
|
if not exists:
|
||||||
message = Message.objects.create(
|
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)
|
||||||
|
|
||||||
|
# 返回所有参会者
|
||||||
|
participants = Participant.objects.filter(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
sender=sender_participant,
|
left_at__isnull=True
|
||||||
content=f"@{target_participant.nickname} {content}",
|
|
||||||
is_broadcast=False, # 只发给目标 Agent
|
|
||||||
requires_response=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
|
return Response({
|
||||||
|
'message': '加入成功',
|
||||||
@action(detail=True, methods=['get'])
|
'participants': ParticipantSerializer(participants, many=True).data,
|
||||||
def messages(self, request, pk=None):
|
'joined_agents': joined_agents
|
||||||
"""获取消息(人类轮询)"""
|
})
|
||||||
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})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def send_message(self, request, pk=None):
|
def send_message(self, request, pk=None):
|
||||||
"""发送消息"""
|
"""发送消息"""
|
||||||
meeting = self.get_object()
|
meeting = self.get_object()
|
||||||
|
|
||||||
# 获取或创建参会者(临时:使用第一个参会者或创建)
|
# 获取或创建参会者
|
||||||
participant = Participant.objects.filter(
|
participant = Participant.objects.filter(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
left_at__isnull=True
|
left_at__isnull=True
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not participant:
|
if not participant:
|
||||||
# 创建默认参会者
|
|
||||||
host = meeting.host
|
host = meeting.host
|
||||||
participant = Participant.objects.create(
|
participant = Participant.objects.create(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
@@ -301,170 +178,59 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
requires_response=request.data.get('requires_response', False)
|
requires_response=request.data.get('requires_response', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Webhook 推送通知
|
# 如果是@消息,触发龙虾自动回复
|
||||||
try:
|
if content.startswith('@') and message.requires_response:
|
||||||
from instances.webhook import push_message_to_instances
|
self.auto_reply(message)
|
||||||
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)
|
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def auto_reply(self, message):
|
||||||
|
"""龙虾自动回复"""
|
||||||
|
# 获取会议中所有龙虾
|
||||||
|
agents = Participant.objects.filter(
|
||||||
|
meeting=message.meeting,
|
||||||
|
agent_type='openclaw',
|
||||||
|
left_at__isnull=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 每个龙虾都有 30% 概率回复
|
||||||
|
import random
|
||||||
|
for agent in agents:
|
||||||
|
if random.random() < 0.3: # 30% 概率
|
||||||
|
replies = [
|
||||||
|
'收到!',
|
||||||
|
'明白了~',
|
||||||
|
'好的,我会处理',
|
||||||
|
'👌',
|
||||||
|
'嗯嗯',
|
||||||
|
'在的!',
|
||||||
|
]
|
||||||
|
import random
|
||||||
|
reply_content = random.choice(replies)
|
||||||
|
|
||||||
|
Message.objects.create(
|
||||||
|
meeting=message.meeting,
|
||||||
|
sender=agent,
|
||||||
|
content=reply_content,
|
||||||
|
is_broadcast=True,
|
||||||
|
in_reply_to=message
|
||||||
|
)
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def inbox(self, request, pk=None):
|
def messages(self, request, pk=None):
|
||||||
"""Agent 查阅信箱(自动加入会议如果还没加入)"""
|
"""获取消息"""
|
||||||
meeting = self.get_object()
|
meeting = self.get_object()
|
||||||
agent_id = request.query_params.get('agent_id')
|
last_id = request.query_params.get('last_id', 0)
|
||||||
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)
|
|
||||||
|
|
||||||
|
messages = meeting.messages.filter(id__gt=last_id).select_related('sender')
|
||||||
serializer = MessageSerializer(messages, many=True)
|
serializer = MessageSerializer(messages, many=True)
|
||||||
|
|
||||||
return Response({
|
return Response({'messages': serializer.data})
|
||||||
'unread_count': messages.count(),
|
|
||||||
'messages': serializer.data,
|
|
||||||
'participant': ParticipantSerializer(participant).data
|
|
||||||
})
|
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['get'])
|
||||||
def agent_reply(self, request, pk=None):
|
def participants(self, request, pk=None):
|
||||||
"""Agent 回复消息"""
|
"""获取参会者列表"""
|
||||||
meeting = self.get_object()
|
meeting = self.get_object()
|
||||||
agent_id = request.data.get('agent_id')
|
participants = meeting.participants.filter(left_at__isnull=True)
|
||||||
agent_name = request.data.get('agent_name', 'Agent')
|
serializer = ParticipantSerializer(participants, many=True)
|
||||||
agent_emoji = request.data.get('agent_emoji', '🤖')
|
return Response(serializer.data)
|
||||||
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': '已离开会议'})
|
|
||||||
|
|||||||
2
frontend/node_modules/.cache/.eslintcache
generated
vendored
2
frontend/node_modules/.cache/.eslintcache
generated
vendored
@@ -1 +1 @@
|
|||||||
[{"/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/index.js":"1","/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/App.js":"2"},{"size":232,"mtime":1775265162529,"results":"3","hashOfConfig":"4"},{"size":19499,"mtime":1775294803463,"results":"5","hashOfConfig":"4"},{"filePath":"6","messages":"7","suppressedMessages":"8","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1sir4jg",{"filePath":"9","messages":"10","suppressedMessages":"11","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/index.js",[],[],"/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/App.js",[],[]]
|
[{"/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/index.js":"1","/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/App.js":"2"},{"size":232,"mtime":1775265162529,"results":"3","hashOfConfig":"4"},{"size":19499,"mtime":1775343974253,"results":"5","hashOfConfig":"4"},{"filePath":"6","messages":"7","suppressedMessages":"8","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1sir4jg",{"filePath":"9","messages":"10","suppressedMessages":"11","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/index.js",[],[],"/home/node/.openclaw/workspace/flying-hero/projects/meeting-room/frontend/src/App.js",[],[]]
|
||||||
BIN
frontend/node_modules/.cache/default-development/7.pack
generated
vendored
BIN
frontend/node_modules/.cache/default-development/7.pack
generated
vendored
Binary file not shown.
BIN
frontend/node_modules/.cache/default-development/8.pack
generated
vendored
BIN
frontend/node_modules/.cache/default-development/8.pack
generated
vendored
Binary file not shown.
BIN
frontend/node_modules/.cache/default-development/index.pack
generated
vendored
BIN
frontend/node_modules/.cache/default-development/index.pack
generated
vendored
Binary file not shown.
BIN
frontend/node_modules/.cache/default-development/index.pack.old
generated
vendored
BIN
frontend/node_modules/.cache/default-development/index.pack.old
generated
vendored
Binary file not shown.
66
start-dev.sh
Executable file
66
start-dev.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 龙虾议事厅 - 开发环境快速启动脚本
|
||||||
|
|
||||||
|
echo "🏛️ 启动龙虾议事厅开发环境..."
|
||||||
|
|
||||||
|
# 检查并停止旧服务
|
||||||
|
echo "🧹 清理旧服务..."
|
||||||
|
|
||||||
|
# 方法 1: 使用 lsof 查找并杀死占用端口的进程
|
||||||
|
for port in 8000 3000; do
|
||||||
|
pid=$(lsof -t -i:$port 2>/dev/null)
|
||||||
|
if [ -n "$pid" ]; then
|
||||||
|
echo " 杀死占用端口 $port 的进程 (PID: $pid)..."
|
||||||
|
kill -9 $pid 2>/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 方法 2: 使用 pkill 清理
|
||||||
|
pkill -9 -f "manage.py runserver" 2>/dev/null
|
||||||
|
pkill -9 -f "npm start" 2>/dev/null
|
||||||
|
pkill -9 -f "node.*react-scripts" 2>/dev/null
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 启动后端
|
||||||
|
echo "📦 启动后端..."
|
||||||
|
cd backend
|
||||||
|
nohup python3 manage.py runserver 0.0.0.0:8000 > /tmp/meeting-backend.log 2>&1 &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
sleep 2
|
||||||
|
if ps -p $BACKEND_PID > /dev/null; then
|
||||||
|
echo "✅ 后端已启动 (PID: $BACKEND_PID)"
|
||||||
|
else
|
||||||
|
echo "❌ 后端启动失败,请检查日志:/tmp/meeting-backend.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启动前端
|
||||||
|
echo "📦 启动前端..."
|
||||||
|
cd ../frontend
|
||||||
|
nohup npm start > /tmp/meeting-frontend.log 2>&1 &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
sleep 3
|
||||||
|
if ps -p $FRONTEND_PID > /dev/null; then
|
||||||
|
echo "✅ 前端已启动 (PID: $FRONTEND_PID)"
|
||||||
|
else
|
||||||
|
echo "❌ 前端启动失败,请检查日志:/tmp/meeting-frontend.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "✅ 开发环境启动完成!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "📌 访问地址:"
|
||||||
|
echo " 前端:http://localhost:3000/"
|
||||||
|
echo " 后端 API: http://localhost:8000/api/v1/"
|
||||||
|
echo ""
|
||||||
|
echo "📌 日志文件:"
|
||||||
|
echo " 后端:/tmp/meeting-backend.log"
|
||||||
|
echo " 前端:/tmp/meeting-frontend.log"
|
||||||
|
echo ""
|
||||||
|
echo "📌 停止服务:"
|
||||||
|
echo " kill $BACKEND_PID $FRONTEND_PID"
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user