📋 飞行侠添加:会议纪要生成

新增:
- 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
This commit is contained in:
2026-04-04 11:39:31 +08:00
parent c510a1e4b2
commit 53c3ac487a
4 changed files with 194 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
from django.contrib import admin 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 django.views.generic import TemplateView
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from meetings.views import MeetingViewSet, ParticipantViewSet from meetings.views import MeetingViewSet, ParticipantViewSet
@@ -12,7 +12,8 @@ router.register(r'meetings/(?P<meeting_pk>[^/.]+)/participants', ParticipantView
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("", TemplateView.as_view(template_name="meeting_room.html"), name="home"), 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/login/", LoginView.as_view()),
path("api/v1/auth/register/", RegisterView.as_view()), path("api/v1/auth/register/", RegisterView.as_view()),
re_path(r'^api/v1/meetings/(?P<pk>[^/.]+)/generate-minutes/$', MeetingViewSet.as_view({'get': 'minutes'}), name='meeting-minutes'),
path("api/v1/", include(router.urls)),
] ]

100
backend/meetings/utils.py Normal file
View File

@@ -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

View File

@@ -274,6 +274,28 @@ class MeetingViewSet(viewsets.ModelViewSet):
) )
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED) 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): class ParticipantViewSet(viewsets.ModelViewSet):

69
backend/test_minutes.py Normal file
View File

@@ -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()