From 53c3ac487adfeef5d94a06dfd2e9e01007b0df15 Mon Sep 17 00:00:00 2001 From: flying-hero <462087392@qq.com> Date: Sat, 4 Apr 2026 11:39:31 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8B=20=E9=A3=9E=E8=A1=8C=E4=BE=A0?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=EF=BC=9A=E4=BC=9A=E8=AE=AE=E7=BA=AA=E8=A6=81?= =?UTF-8?q?=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - meetings/utils.py: 纪要生成工具函数 - generate_meeting_minutes(): 生成纪要数据 - export_minutes_to_markdown(): 导出 Markdown - meetings/views.py: minutes action - 支持 JSON 和 Markdown 两种格式 - 自动统计参会者消息数 - 提取待办事项 - test_minutes.py: 纪要测试脚本 使用: - GET /api/v1/meetings/{id}/minutes/ → JSON - GET /api/v1/meetings/{id}/minutes/?output=markdown → Markdown --- backend/meeting_room/urls.py | 5 +- backend/meetings/utils.py | 100 +++++++++++++++++++++++++++++++++++ backend/meetings/views.py | 22 ++++++++ backend/test_minutes.py | 69 ++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 backend/meetings/utils.py create mode 100644 backend/test_minutes.py diff --git a/backend/meeting_room/urls.py b/backend/meeting_room/urls.py index f21fa192..3b102edb 100644 --- a/backend/meeting_room/urls.py +++ b/backend/meeting_room/urls.py @@ -1,5 +1,5 @@ from django.contrib import admin -from django.urls import path, include +from django.urls import path, include, re_path from django.views.generic import TemplateView from rest_framework.routers import DefaultRouter from meetings.views import MeetingViewSet, ParticipantViewSet @@ -12,7 +12,8 @@ router.register(r'meetings/(?P[^/.]+)/participants', ParticipantView urlpatterns = [ path("admin/", admin.site.urls), path("", TemplateView.as_view(template_name="meeting_room.html"), name="home"), - path("api/v1/", include(router.urls)), path("api/v1/auth/login/", LoginView.as_view()), path("api/v1/auth/register/", RegisterView.as_view()), + re_path(r'^api/v1/meetings/(?P[^/.]+)/generate-minutes/$', MeetingViewSet.as_view({'get': 'minutes'}), name='meeting-minutes'), + path("api/v1/", include(router.urls)), ] diff --git a/backend/meetings/utils.py b/backend/meetings/utils.py new file mode 100644 index 00000000..5901f33a --- /dev/null +++ b/backend/meetings/utils.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +龙虾议事厅 - 工具函数 +""" + +from .models import Meeting, Message, Participant +from datetime import datetime +from typing import List, Dict + + +def generate_meeting_minutes(meeting_id: str) -> Dict: + """ + 生成会议纪要 + + Args: + meeting_id: 会议 ID + + Returns: + 会议纪要字典 + """ + from django.db.models import Count + + meeting = Meeting.objects.get(id=meeting_id) + messages = Message.objects.filter(meeting=meeting).select_related('sender').order_by('created_at') + participants = Participant.objects.filter(meeting=meeting, left_at__isnull=True) + + # 统计消息数量 + message_count = messages.count() + + # 统计每个参会者的消息数 + participant_stats = {} + for msg in messages: + name = msg.sender.nickname + participant_stats[name] = participant_stats.get(name, 0) + 1 + + # 生成摘要 + if message_count == 0: + summary = "本次会议暂无消息记录。" + else: + # 提取第一条和最后一条消息 + first_msg = messages.first() + last_msg = messages.last() + + summary = f"会议于 {meeting.created_at.strftime('%Y-%m-%d %H:%M')} 开始," + summary += f"共 {message_count} 条消息,{participants.count()} 位参会者。" + + if message_count > 0: + summary += f" 第一条消息:\"{first_msg.content[:50]}...\"" + summary += f" 最后一条消息:\"{last_msg.content[:50]}...\"" + + # 生成待办事项(标记为需要回复的消息) + todos = messages.filter(requires_response=True).values_list('content', flat=True) + + return { + 'meeting_id': meeting.id, + 'topic': meeting.topic, + 'status': meeting.status, + 'created_at': meeting.created_at.isoformat(), + 'duration_minutes': None, # 会议结束前无法计算 + 'participant_count': participants.count(), + 'message_count': message_count, + 'summary': summary, + 'participant_stats': participant_stats, + 'todos': list(todos), + 'generated_at': datetime.now().isoformat() + } + + +def export_minutes_to_markdown(minutes: Dict) -> str: + """ + 将会话纪要导出为 Markdown 格式 + + Args: + minutes: generate_meeting_minutes 返回的字典 + + Returns: + Markdown 格式的会议纪要 + """ + md = f"# 📋 会议纪要\n\n" + md += f"**主题:** {minutes['topic']}\n\n" + md += f"**时间:** {minutes['created_at']}\n\n" + md += f"**状态:** {minutes['status']}\n\n" + md += f"**参会人数:** {minutes['participant_count']}\n\n" + md += f"**消息总数:** {minutes['message_count']}\n\n" + + md += "## 📊 统计\n\n" + for name, count in minutes['participant_stats'].items(): + md += f"- {name}: {count} 条消息\n" + + md += "\n## 📝 摘要\n\n" + md += f"{minutes['summary']}\n\n" + + if minutes['todos']: + md += "## ✅ 待办事项\n\n" + for todo in minutes['todos']: + md += f"- [ ] {todo}\n" + + md += f"\n---\n*生成时间:{minutes['generated_at']}*\n" + + return md diff --git a/backend/meetings/views.py b/backend/meetings/views.py index a7aa14a0..7d39d18c 100644 --- a/backend/meetings/views.py +++ b/backend/meetings/views.py @@ -274,6 +274,28 @@ class MeetingViewSet(viewsets.ModelViewSet): ) 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): diff --git a/backend/test_minutes.py b/backend/test_minutes.py new file mode 100644 index 00000000..9ab67665 --- /dev/null +++ b/backend/test_minutes.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +测试会议纪要生成 +""" + +import requests + +API_BASE = 'http://localhost:8000/api/v1' + +def test_minutes(): + # 登录 + res = requests.post(f'{API_BASE}/auth/login/', json={ + 'username': 'test', + 'password': 'test123' + }) + token = res.json()['token'] + headers = {'Authorization': f'Bearer {token}'} + + # 创建会议 + res = requests.post(f'{API_BASE}/meetings/', json={ + 'topic': '会议纪要测试会议' + }, headers=headers) + meeting_id = res.json()['id'] + print(f"✅ 创建会议:{meeting_id}") + + # 发送几条消息 + messages = [ + "大家好,开始今天的会议!", + "我来汇报一下 Q2 的进度。", + "这个项目需要更多资源支持。", + "好的,我会跟进这件事。", + ] + + for msg in messages: + requests.post( + f'{API_BASE}/meetings/{meeting_id}/send_message/', + json={'content': msg, 'requires_response': '资源' in msg}, + headers=headers + ) + print(f"✅ 发送 {len(messages)} 条消息") + + # 生成纪要(JSON) + res = requests.get(f'{API_BASE}/meetings/{meeting_id}/minutes/') + if res.status_code == 200: + data = res.json() + print(f"\n✅ 会议纪要 (JSON):") + print(f" 主题:{data['topic']}") + print(f" 消息数:{data['message_count']}") + print(f" 摘要:{data['summary'][:100]}...") + print(f" 待办:{len(data['todos'])} 项") + else: + print(f"❌ 生成纪要失败:{res.text}") + return False + + # 生成纪要(Markdown)- 用 minutes action + res = requests.get(f'{API_BASE}/meetings/{meeting_id}/minutes/?output=markdown') + if res.status_code == 200: + data = res.json() + print(f"\n✅ 会议纪要 (Markdown):") + print(data['markdown'][:500]) + else: + print(f"❌ 生成 Markdown 失败:{res.text}") + return False + + print("\n✅ 会议纪要测试通过!") + return True + +if __name__ == '__main__': + test_minutes()