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