📋 飞行侠添加:会议纪要生成
新增:
- 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.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<meeting_pk>[^/.]+)/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<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
|
||||
@@ -275,6 +275,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):
|
||||
"""参会者视图集"""
|
||||
|
||||
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