【悟凡】真正意义上的净土重生:只保留核心逻辑

This commit is contained in:
2026-04-04 11:19:01 +08:00
commit 6f127936c1
47 changed files with 20847 additions and 0 deletions

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/meetings/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class MeetingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "meetings"

View File

@@ -0,0 +1,248 @@
# Generated by Django 4.2 on 2026-04-04 01:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Meeting",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("topic", models.CharField(max_length=200, verbose_name="会议主题")),
(
"status",
models.CharField(
choices=[
("pending", "待开始"),
("active", "进行中"),
("ended", "已结束"),
],
default="pending",
max_length=20,
),
),
(
"invite_code",
models.CharField(max_length=20, unique=True, verbose_name="邀请码"),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("started_at", models.DateTimeField(blank=True, null=True)),
("ended_at", models.DateTimeField(blank=True, null=True)),
(
"host",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hosted_meetings",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "会议室",
"verbose_name_plural": "会议室",
"db_table": "meetings",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="Participant",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"agent_type",
models.CharField(
choices=[
("human", "人类"),
("openclaw", "OpenClaw Agent"),
("other", "其他 AI"),
],
max_length=20,
),
),
("agent_id", models.CharField(blank=True, max_length=100, null=True)),
(
"agent_name",
models.CharField(max_length=100, verbose_name="Agent 名称"),
),
(
"agent_emoji",
models.CharField(
default="🤖", max_length=10, verbose_name="Agent 表情"
),
),
("nickname", models.CharField(max_length=100, verbose_name="昵称")),
("is_host", models.BooleanField(default=False)),
("joined_at", models.DateTimeField(auto_now_add=True)),
("left_at", models.DateTimeField(blank=True, null=True)),
("api_key", models.CharField(blank=True, max_length=255, null=True)),
(
"meeting",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="participants",
to="meetings.meeting",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "参会者",
"verbose_name_plural": "参会者",
"db_table": "participants",
},
),
migrations.CreateModel(
name="Message",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"is_broadcast",
models.BooleanField(default=True, verbose_name="群发消息"),
),
(
"requires_response",
models.BooleanField(default=False, verbose_name="需要回复"),
),
(
"in_reply_to",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="replies",
to="meetings.message",
),
),
(
"meeting",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="messages",
to="meetings.meeting",
),
),
(
"read_by",
models.ManyToManyField(
blank=True,
related_name="read_messages",
to="meetings.participant",
),
),
(
"sender",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to="meetings.participant",
),
),
],
options={
"verbose_name": "消息",
"verbose_name_plural": "消息",
"db_table": "messages",
"ordering": ["created_at"],
},
),
migrations.CreateModel(
name="MeetingMinutes",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("generated_at", models.DateTimeField(auto_now_add=True)),
("exported_at", models.DateTimeField(blank=True, null=True)),
(
"meeting",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="minutes",
to="meetings.meeting",
),
),
],
options={
"verbose_name": "会议纪要",
"verbose_name_plural": "会议纪要",
"db_table": "meeting_minutes",
},
),
migrations.AddIndex(
model_name="participant",
index=models.Index(
fields=["meeting", "agent_id"], name="participant_meeting_74488d_idx"
),
),
migrations.AddIndex(
model_name="participant",
index=models.Index(
fields=["agent_type", "meeting"], name="participant_agent_t_c4a9dc_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["meeting", "created_at"], name="messages_meeting_b69008_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["is_broadcast", "created_at"],
name="messages_is_broa_fae706_idx",
),
),
]

View File

126
backend/meetings/models.py Normal file
View File

@@ -0,0 +1,126 @@
from django.db import models
from django.contrib.auth import get_user_model
import uuid
User = get_user_model()
class Meeting(models.Model):
"""会议室模型"""
STATUS_CHOICES = [
('pending', '待开始'),
('active', '进行中'),
('ended', '已结束'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
topic = models.CharField(max_length=200, verbose_name='会议主题')
host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hosted_meetings')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
invite_code = models.CharField(max_length=20, unique=True, verbose_name='邀请码')
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
ended_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'meetings'
verbose_name = '会议室'
verbose_name_plural = '会议室'
ordering = ['-created_at']
def __str__(self):
return f"{self.topic} ({self.host.username})"
def save(self, *args, **kwargs):
if not self.invite_code:
self.invite_code = uuid.uuid4().hex[:8].upper()
super().save(*args, **kwargs)
class Participant(models.Model):
"""参会者模型"""
AGENT_TYPE_CHOICES = [
('human', '人类'),
('openclaw', 'OpenClaw Agent'),
('other', '其他 AI'),
]
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='participants')
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
# Agent 信息
agent_type = models.CharField(max_length=20, choices=AGENT_TYPE_CHOICES)
agent_id = models.CharField(max_length=100, null=True, blank=True)
agent_name = models.CharField(max_length=100, verbose_name='Agent 名称')
agent_emoji = models.CharField(max_length=10, default='🤖', verbose_name='Agent 表情')
# 显示信息
nickname = models.CharField(max_length=100, verbose_name='昵称')
is_host = models.BooleanField(default=False)
joined_at = models.DateTimeField(auto_now_add=True)
left_at = models.DateTimeField(null=True, blank=True)
# API 认证Agent 用)
api_key = models.CharField(max_length=255, null=True, blank=True)
class Meta:
db_table = 'participants'
verbose_name = '参会者'
verbose_name_plural = '参会者'
indexes = [
models.Index(fields=['meeting', 'agent_id']),
models.Index(fields=['agent_type', 'meeting']),
]
def __str__(self):
return f"{self.agent_emoji} {self.nickname}"
def save(self, *args, **kwargs):
if not self.api_key and self.agent_type != 'human':
self.api_key = uuid.uuid4().hex
super().save(*args, **kwargs)
class Message(models.Model):
"""消息模型"""
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='messages')
sender = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name='sent_messages')
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# 信箱机制
is_broadcast = models.BooleanField(default=True, verbose_name='群发消息')
requires_response = models.BooleanField(default=False, verbose_name='需要回复')
in_reply_to = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='replies')
# 读取状态
read_by = models.ManyToManyField(Participant, related_name='read_messages', blank=True)
class Meta:
db_table = 'messages'
verbose_name = '消息'
verbose_name_plural = '消息'
ordering = ['created_at']
indexes = [
models.Index(fields=['meeting', 'created_at']),
models.Index(fields=['is_broadcast', 'created_at']),
]
def __str__(self):
return f"{self.sender.nickname}: {self.content[:50]}"
class MeetingMinutes(models.Model):
"""会议纪要模型"""
meeting = models.OneToOneField(Meeting, on_delete=models.CASCADE, related_name='minutes')
content = models.TextField()
generated_at = models.DateTimeField(auto_now_add=True)
exported_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'meeting_minutes'
verbose_name = '会议纪要'
verbose_name_plural = '会议纪要'
def __str__(self):
return f"会议纪要 - {self.meeting.topic}"

View File

@@ -0,0 +1,76 @@
from rest_framework import serializers
from .models import Meeting, Participant, Message, MeetingMinutes
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
"""用户序列化器"""
class Meta:
model = User
fields = ['id', 'username', 'email', 'created_at']
class MeetingSerializer(serializers.ModelSerializer):
"""会议室序列化器"""
host_name = serializers.CharField(source='host.username', read_only=True)
participant_count = serializers.SerializerMethodField()
class Meta:
model = Meeting
fields = [
'id', 'topic', 'host', 'host_name', 'status', 'invite_code',
'created_at', 'started_at', 'ended_at', 'participant_count'
]
read_only_fields = ['host', 'invite_code', 'status']
def get_participant_count(self, obj):
return obj.participants.filter(left_at__isnull=True).count()
def create(self, validated_data):
# 使用第一个用户作为 host临时方案
host = User.objects.first()
validated_data['host'] = host
return super().create(validated_data)
class ParticipantSerializer(serializers.ModelSerializer):
"""参会者序列化器"""
class Meta:
model = Participant
fields = [
'id', 'meeting', 'user', 'agent_type', 'agent_id',
'agent_name', 'agent_emoji', 'nickname', 'is_host',
'joined_at', 'api_key'
]
read_only_fields = ['api_key', 'joined_at']
class MessageSerializer(serializers.ModelSerializer):
"""消息序列化器"""
sender_name = serializers.CharField(source='sender.nickname', read_only=True)
sender_emoji = serializers.CharField(source='sender.agent_emoji', read_only=True)
class Meta:
model = Message
fields = [
'id', 'meeting', 'sender', 'sender_name', 'sender_emoji',
'content', 'created_at', 'is_broadcast', 'requires_response',
'in_reply_to', 'read_by'
]
read_only_fields = ['sender', 'created_at']
class InboxSerializer(serializers.Serializer):
"""信箱序列化器"""
unread_count = serializers.IntegerField()
messages = MessageSerializer(many=True)
class MeetingMinutesSerializer(serializers.ModelSerializer):
"""会议纪要序列化器"""
class Meta:
model = MeetingMinutes
fields = ['meeting', 'content', 'generated_at', 'exported_at']
read_only_fields = ['meeting', 'generated_at']

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

236
backend/meetings/views.py Normal file
View File

@@ -0,0 +1,236 @@
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 django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404
import uuid
User = get_user_model()
class MeetingViewSet(viewsets.ModelViewSet):
"""会议室视图集"""
queryset = Meeting.objects.all()
serializer_class = MeetingSerializer
permission_classes = [] # 临时开放所有权限
def get_queryset(self):
# 简单返回所有会议
return Meeting.objects.all().order_by('-created_at')
def create(self, request, *args, **kwargs):
"""创建会议"""
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 = User.objects.first()
meeting = serializer.save(host=host)
# 创建主持人参会记录
Participant.objects.create(
meeting=meeting,
user=host,
agent_type='human',
nickname=host.username,
is_host=True
)
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()
return Response({'status': '会议已结束'})
@action(detail=True, methods=['post'])
def join(self, request, pk=None):
"""加入会议"""
meeting = self.get_object()
if meeting.status == 'ended':
return Response(
{'error': '会议已结束'},
status=status.HTTP_400_BAD_REQUEST
)
invite_code = request.data.get('invite_code')
if invite_code != meeting.invite_code:
return Response(
{'error': '邀请码错误'},
status=status.HTTP_400_BAD_REQUEST
)
# 检查是否已加入
existing = Participant.objects.filter(
meeting=meeting,
user=request.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=['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})
@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,
user=host,
agent_type='human',
nickname=host.username
)
content = request.data.get('content')
if not content:
return Response(
{'error': '消息内容不能为空'},
status=status.HTTP_400_BAD_REQUEST
)
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)
)
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['get'])
def inbox(self, request, pk=None):
"""Agent 查阅信箱"""
meeting = self.get_object()
agent_id = request.query_params.get('agent_id')
if not agent_id:
return Response(
{'error': '缺少 agent_id 参数'},
status=status.HTTP_400_BAD_REQUEST
)
# 找到这个 Agent 的参会记录
participant = get_object_or_404(
Participant,
meeting=meeting,
agent_id=agent_id,
left_at__isnull=True
)
# 获取发给这个 Agent 的消息(未读)
messages = Message.objects.filter(
meeting=meeting
).exclude(
read_by=participant
)
# 如果是群发消息,所有人都能看到
# 如果是指定消息,需要检查 recipients
# 简化版:所有未读消息都返回
serializer = MessageSerializer(messages, many=True)
return Response({
'unread_count': messages.count(),
'messages': serializer.data
})
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': '已离开会议'})