📋 飞行侠添加:会议纪要生成
新增:
- 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:
@@ -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
100
backend/meetings/utils.py
Normal 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
|
||||||
@@ -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
69
backend/test_minutes.py
Normal 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()
|
||||||
Reference in New Issue
Block a user