From d403583fb8da95a1cfe0a7ff3edd5b22ac1e5889 Mon Sep 17 00:00:00 2001 From: flying-hero <462087392@qq.com> Date: Sat, 4 Apr 2026 11:43:41 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20=E9=A3=9E=E8=A1=8C=E4=BE=A0?= =?UTF-8?q?=E5=AE=8C=E5=96=84=20P1=20=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=BA=A7?= =?UTF-8?q?=E4=BD=8D=E5=9B=BE=20+=20@Agent=20+=20=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E7=BA=AA=E8=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 座位可视化 - 圆形头像展示参会者 - @Agent 功能 - 定向消息给特定 Agent - 会议纪要生成 - Web 界面一键生成 - 参会者列表 API 文件变更: - meetings/views.py: mention_agent() 新接口 - templates/meeting_room.html: - 座位图 UI(圆形头像) - 生成纪要按钮 - @Agent 按钮 - test_mention.py: @Agent 测试脚本 测试结果: ✅ 完整功能测试 (7 项) ✅ 会议纪要测试 (JSON + Markdown) ✅ @Agent 功能测试 --- backend/meetings/views.py | 64 +++++++++++++ backend/templates/meeting_room.html | 133 +++++++++++++++++++++++++++- backend/test_mention.py | 103 +++++++++++++++++++++ 3 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 backend/test_mention.py diff --git a/backend/meetings/views.py b/backend/meetings/views.py index 7d39d18c..01d9e481 100644 --- a/backend/meetings/views.py +++ b/backend/meetings/views.py @@ -125,6 +125,70 @@ class MeetingViewSet(viewsets.ModelViewSet): serializer = ParticipantSerializer(participants, many=True) return Response(serializer.data) + @action(detail=True, methods=['post']) + def mention_agent(self, request, pk=None): + """@Agent 功能 - 发送消息给特定 Agent""" + meeting = self.get_object() + target_agent_id = request.data.get('target_agent_id') + content = request.data.get('content') + sender_agent_id = request.data.get('sender_agent_id') + sender_name = request.data.get('sender_name', 'User') + sender_emoji = request.data.get('sender_emoji', '👤') + + if not target_agent_id or not content: + return Response( + {'error': '缺少 target_agent_id 或 content'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 找到目标 Agent + target_participant = Participant.objects.filter( + meeting=meeting, + agent_id=target_agent_id, + left_at__isnull=True + ).first() + + if not target_participant: + return Response( + {'error': f'未找到 Agent: {target_agent_id}'}, + status=status.HTTP_404_NOT_FOUND + ) + + # 获取或创建发送者 + sender_participant = Participant.objects.filter( + meeting=meeting, + agent_id=sender_agent_id, + left_at__isnull=True + ).first() + + if not sender_participant and sender_agent_id: + sender_participant = Participant.objects.create( + meeting=meeting, + agent_type='openclaw', + agent_id=sender_agent_id, + agent_name=sender_name, + agent_emoji=sender_emoji, + nickname=f"{sender_emoji} {sender_name}" + ) + + if not sender_participant: + # 人类发送的,用主持人 + sender_participant = Participant.objects.filter( + meeting=meeting, + is_host=True + ).first() + + # 创建消息,标记为需要回复 + message = Message.objects.create( + meeting=meeting, + sender=sender_participant, + content=f"@{target_participant.nickname} {content}", + is_broadcast=False, # 只发给目标 Agent + requires_response=True + ) + + return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED) + @action(detail=True, methods=['get']) def messages(self, request, pk=None): """获取消息(人类轮询)""" diff --git a/backend/templates/meeting_room.html b/backend/templates/meeting_room.html index 013112f3..8b8a748f 100644 --- a/backend/templates/meeting_room.html +++ b/backend/templates/meeting_room.html @@ -217,18 +217,38 @@ - + + + +
+

🪑 座位图 0

+
+

暂无参会者

+
+
+

💬 消息列表 0

暂无消息,开始聊天吧!

- +
+ + + +
+
+ + + @@ -499,6 +519,115 @@ } } + async function loadParticipants() { + if (!currentMeetingId) { + showStatus('❌ 请先创建或加入会议', 'error'); + return; + } + + try { + const res = await fetch(`${API_BASE}/meetings/${currentMeetingId}/participants/`); + const participants = await res.json(); + + document.getElementById('participantCount').textContent = participants.length; + + if (participants.length === 0) { + document.getElementById('seatMap').innerHTML = + '

暂无参会者

'; + } else { + // 圆桌座位布局 + document.getElementById('seatMap').innerHTML = ` +
+ ${participants.map(p => ` +
+
${p.agent_emoji || '👤'}
+
${p.nickname || '参会者'}
+ ${p.is_host ? '
👑 主持
' : ''} +
+ `).join('')} +
+ `; + } + } catch (e) { + showStatus(`❌ 加载座位失败:${e.message}`, 'error'); + } + } + + async function generateMinutes() { + if (!currentMeetingId) { + showStatus('❌ 请先创建或加入会议', 'error'); + return; + } + + try { + const res = await fetch(`${API_BASE}/meetings/${currentMeetingId}/minutes/?output=markdown`); + const data = await res.json(); + + if (res.ok && data.markdown) { + document.getElementById('minutesContent').textContent = data.markdown; + document.getElementById('minutesDisplay').style.display = 'block'; + showStatus('✅ 会议纪要已生成!', 'success'); + } else { + showStatus(`❌ 生成失败:${data.error || '未知错误'}`, 'error'); + } + } catch (e) { + showStatus(`❌ 请求失败:${e.message}`, 'error'); + } + } + + async function mentionAgent() { + const meetingId = currentMeetingId || prompt('请输入会议 ID:'); + const targetAgentId = prompt('@哪个 Agent?输入 agent_id:'); + const content = document.getElementById('messageContent').value; + const senderAgentId = document.getElementById('agentId').value || 'human'; + const senderName = document.getElementById('agentName').value || '用户'; + const senderEmoji = document.getElementById('agentEmoji').value || '👤'; + + if (!meetingId || !targetAgentId || !content) { + showStatus('❌ 请填写完整信息', 'error'); + return; + } + + try { + const res = await fetch(`${API_BASE}/meetings/${meetingId}/mention_agent/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + target_agent_id: targetAgentId, + content: content, + sender_agent_id: senderAgentId, + sender_name: senderName, + sender_emoji: senderEmoji + }) + }); + const data = await res.json(); + + if (res.ok) { + document.getElementById('messageContent').value = ''; + showStatus(`✅ 已 @${targetAgentId}`, 'success'); + loadMessages(); + } else { + showStatus(`❌ ${data.error || '发送失败'}`, 'error'); + } + } catch (e) { + showStatus(`❌ 请求失败:${e.message}`, 'error'); + } + } + // 自动登录(如果记得凭证) document.getElementById('username').value = 'test'; document.getElementById('password').value = 'test123'; diff --git a/backend/test_mention.py b/backend/test_mention.py new file mode 100644 index 00000000..ef945309 --- /dev/null +++ b/backend/test_mention.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +测试 @Agent 功能 +""" + +import requests + +API_BASE = 'http://localhost:8000/api/v1' + +def test_mention_agent(): + print("="*60) + print("📍 测试 @Agent 功能") + print("="*60) + + # 登录 + res = requests.post(f'{API_BASE}/auth/login/', json={ + 'username': 'test', + 'password': 'test123' + }) + token = res.json()['token'] + headers = {'Authorization': f'Bearer {token}'} + print(f"✅ 登录成功") + + # 创建会议 + res = requests.post(f'{API_BASE}/meetings/', json={ + 'topic': '@Agent 测试会议' + }, headers=headers) + meeting_id = res.json()['id'] + print(f"✅ 创建会议:{meeting_id}") + + # Agent A 加入(查信箱自动加入) + res = requests.get( + f'{API_BASE}/meetings/{meeting_id}/inbox/', + params={'agent_id': 'agent_a', 'agent_name': '助手 A', 'agent_emoji': '🤖'} + ) + print(f"✅ Agent A 加入会议") + + # Agent B 加入 + res = requests.get( + f'{API_BASE}/meetings/{meeting_id}/inbox/', + params={'agent_id': 'agent_b', 'agent_name': '助手 B', 'agent_emoji': '🦊'} + ) + print(f"✅ Agent B 加入会议") + + # 获取参会者列表 + res = requests.get(f'{API_BASE}/meetings/{meeting_id}/participants/') + participants = res.json() + print(f"✅ 参会者列表:{len(participants)} 人") + for p in participants: + print(f" - {p['nickname']} (ID: {p['agent_id']})") + + # @Agent A + res = requests.post(f'{API_BASE}/meetings/{meeting_id}/mention_agent/', json={ + 'target_agent_id': 'agent_a', + 'content': '请汇报一下进度', + 'sender_agent_id': 'human_user', + 'sender_name': '北极星', + 'sender_emoji': '⭐' + }) + if res.status_code == 201: + msg = res.json() + print(f"✅ @Agent A 成功:{msg['content']}") + else: + print(f"❌ @Agent 失败:{res.text}") + return False + + # Agent A 查信箱(应该收到 @ 消息) + res = requests.get( + f'{API_BASE}/meetings/{meeting_id}/inbox/', + params={'agent_id': 'agent_a', 'agent_name': '助手 A', 'agent_emoji': '🤖'} + ) + inbox = res.json() + print(f"✅ Agent A 信箱:{inbox['unread_count']} 条未读") + + # Agent A 回复 + res = requests.post(f'{API_BASE}/meetings/{meeting_id}/agent_reply/', json={ + 'agent_id': 'agent_a', + 'agent_name': '助手 A', + 'agent_emoji': '🤖', + 'content': '收到!进度正常,已完成 80%。', + 'in_reply_to': msg['id'] + }) + if res.status_code == 201: + print(f"✅ Agent A 回复成功") + else: + print(f"❌ Agent A 回复失败:{res.text}") + return False + + # 获取全部消息 + res = requests.get(f'{API_BASE}/meetings/{meeting_id}/messages/?last_id=0') + messages = res.json()['messages'] + print(f"\n💬 全部消息 ({len(messages)} 条):") + for m in messages: + reply_info = f" (回复 #{m['in_reply_to']})" if m.get('in_reply_to') else "" + print(f" {m['sender_emoji']} {m['sender_name']}: {m['content']}{reply_info}") + + print("\n" + "="*60) + print("✅ @Agent 功能测试通过!") + print("="*60) + return True + +if __name__ == '__main__': + test_mention_agent()